Compare commits
103 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
5d94fe3a0d | ||
|
|
fb680d669b | ||
|
|
6409e5eaf3 | ||
|
|
39ca9dbdb1 | ||
|
|
27ab2a621a | ||
|
|
3f572eeb15 | ||
|
|
e83540d5de | ||
|
|
847556bec1 | ||
|
|
42b045fb85 | ||
|
|
35a211f87f | ||
|
|
d2fa67e0e3 | ||
|
|
b8660c9a35 | ||
|
|
8cd3f7fb3a | ||
|
|
2ee0272a05 | ||
|
|
4ae82fc763 | ||
|
|
aac42d7eff | ||
|
|
8bb77ef741 | ||
|
|
ff4218981f | ||
|
|
7b7c5f3d9a | ||
|
|
1c203e39c3 | ||
|
|
7dbf21d564 | ||
|
|
8fcb5ad874 | ||
|
|
b4bf705b7f | ||
|
|
69d8321180 | ||
|
|
b03daeca66 | ||
|
|
c502484bcf | ||
|
|
448d69c6d8 | ||
|
|
42aa7cf926 | ||
|
|
52fa2e460e | ||
|
|
8855ae754a | ||
|
|
908a909c87 | ||
|
|
41aee797a9 | ||
|
|
685f636d5b | ||
|
|
4af56b0cb1 | ||
|
|
a3973c7e9a | ||
|
|
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 | ||
|
|
d96ced48c9 | ||
|
|
0b6dbfd888 | ||
|
|
96656438c6 | ||
|
|
4b32df5683 | ||
|
|
0758bd7dec | ||
|
|
171dabd83a | ||
|
|
6182a99ebd | ||
|
|
8bfedda3ab | ||
|
|
26f6ea1a9a | ||
|
|
778f377e42 | ||
|
|
f284365469 | ||
|
|
5dfb7d4ba4 | ||
|
|
bb9c3a3034 | ||
|
|
8cf872a4b0 | ||
|
|
e813230824 | ||
|
|
e8bfde9243 | ||
|
|
e947ad6d4d | ||
|
|
0a53898797 | ||
|
|
946acc2460 | ||
|
|
e13b3df2c4 | ||
|
|
7df2fe421e | ||
|
|
b4b89f8bb4 | ||
|
|
db41e68f35 | ||
|
|
5acd9bbcc6 | ||
|
|
23eab2310f | ||
|
|
80829ad015 | ||
|
|
661bf5d30a | ||
|
|
38f4166503 | ||
|
|
d96038c7e3 | ||
|
|
7d6b071d8a | ||
|
|
b17fb3c354 | ||
|
|
10b43a0f67 | ||
|
|
4092a9ba55 | ||
|
|
2d35c863e4 | ||
|
|
8424c161b9 | ||
|
|
9485d7e9b5 | ||
|
|
9f26bdfba1 |
@@ -7,41 +7,98 @@
|
||||
objects = {
|
||||
|
||||
/* Begin PBXBuildFile section */
|
||||
540C6457240D929300E948F9 /* EditableRows.swift in Sources */ = {isa = PBXBuildFile; fileRef = 540C6456240D929300E948F9 /* EditableRows.swift */; };
|
||||
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 */; };
|
||||
540E67822433483D00871BBE /* VCEditRecording.swift in Sources */ = {isa = PBXBuildFile; fileRef = 540E67812433483D00871BBE /* VCEditRecording.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 */; };
|
||||
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 */; };
|
||||
541AC5D82399498A00A769D7 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 541AC5D72399498A00A769D7 /* AppDelegate.swift */; };
|
||||
541AC5DD2399498A00A769D7 /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 541AC5DB2399498A00A769D7 /* Main.storyboard */; };
|
||||
541AC5DF2399498B00A769D7 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 541AC5DE2399498B00A769D7 /* Assets.xcassets */; };
|
||||
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 */; };
|
||||
541FC47824A1453F009154D8 /* VCCoOccurrence.swift in Sources */ = {isa = PBXBuildFile; fileRef = 541FC47724A1453F009154D8 /* VCCoOccurrence.swift */; };
|
||||
541FC9872497D81C00962623 /* TheGreatDestroyer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 541FC9862497D81C00962623 /* TheGreatDestroyer.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 */; };
|
||||
543CDB2523EEE61900B7F323 /* GlassVPN.appex in Embed App Extensions */ = {isa = PBXBuildFile; fileRef = 543CDB1D23EEE61900B7F323 /* GlassVPN.appex */; settings = {ATTRIBUTES = (RemoveHeadersOnCopy, ); }; };
|
||||
544C95262407B1C700AB89D0 /* SharedState.swift in Sources */ = {isa = PBXBuildFile; fileRef = 544C95252407B1C700AB89D0 /* SharedState.swift */; };
|
||||
54448A2E2486464F00771C96 /* Array.swift in Sources */ = {isa = PBXBuildFile; fileRef = 54448A2D2486464F00771C96 /* Array.swift */; };
|
||||
54448A30248647D900771C96 /* Time.swift in Sources */ = {isa = PBXBuildFile; fileRef = 54448A2F248647D900771C96 /* Time.swift */; };
|
||||
54448A3224899A4000771C96 /* SearchBarManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 54448A3124899A4000771C96 /* SearchBarManager.swift */; };
|
||||
544F912024A67EC5001D4B00 /* TVCOccurrenceContext.swift in Sources */ = {isa = PBXBuildFile; fileRef = 544F911F24A67EC5001D4B00 /* TVCOccurrenceContext.swift */; };
|
||||
5458EBC0243A3F2200CFEB15 /* TVCRecordingDetails.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5458EBBF243A3F2200CFEB15 /* TVCRecordingDetails.swift */; };
|
||||
545DDDCF243E6267003B6544 /* TutorialSheet.swift in Sources */ = {isa = PBXBuildFile; fileRef = 545DDDCE243E6267003B6544 /* TutorialSheet.swift */; };
|
||||
545DDDD124436983003B6544 /* QuickUI.swift in Sources */ = {isa = PBXBuildFile; fileRef = 545DDDD024436983003B6544 /* QuickUI.swift */; };
|
||||
545DDDD424466D37003B6544 /* AutoLayout.swift in Sources */ = {isa = PBXBuildFile; fileRef = 545DDDD324466D37003B6544 /* AutoLayout.swift */; };
|
||||
546063E523FEFAFE008F505A /* SQDB.swift in Sources */ = {isa = PBXBuildFile; fileRef = 54B7562223D7B2DC008F0C41 /* SQDB.swift */; };
|
||||
546063E523FEFAFE008F505A /* DBCore.swift in Sources */ = {isa = PBXBuildFile; fileRef = 54B7562223D7B2DC008F0C41 /* DBCore.swift */; };
|
||||
54686A7624F8062C0084934D /* NotificationBanner.swift in Sources */ = {isa = PBXBuildFile; fileRef = 54686A7524F8062C0084934D /* NotificationBanner.swift */; };
|
||||
54686A8524FD0A3F0084934D /* tut-recording-howto.md in Resources */ = {isa = PBXBuildFile; fileRef = 54686A8424FD0A3F0084934D /* tut-recording-howto.md */; };
|
||||
54686A8724FD27AA0084934D /* TinyMarkdown.swift in Sources */ = {isa = PBXBuildFile; fileRef = 54686A8624FD26410084934D /* TinyMarkdown.swift */; };
|
||||
54686A8D24FD428C0084934D /* tut-welcome-1.md in Resources */ = {isa = PBXBuildFile; fileRef = 54686A8824FD31580084934D /* tut-welcome-1.md */; };
|
||||
54686A8E24FD42950084934D /* tut-welcome-2.md in Resources */ = {isa = PBXBuildFile; fileRef = 54686A8B24FD3F180084934D /* tut-welcome-2.md */; };
|
||||
54686A8F24FD42950084934D /* tut-recording-1.md in Resources */ = {isa = PBXBuildFile; fileRef = 54686A8A24FD3F100084934D /* tut-recording-1.md */; };
|
||||
54686A9024FD42950084934D /* tut-recording-2.md in Resources */ = {isa = PBXBuildFile; fileRef = 54686A8924FD31630084934D /* tut-recording-2.md */; };
|
||||
54686A9124FD42950084934D /* tut-cooccurrence.md in Resources */ = {isa = PBXBuildFile; fileRef = 54686A8C24FD3F630084934D /* tut-cooccurrence.md */; };
|
||||
54751E512423955100168273 /* URL.swift in Sources */ = {isa = PBXBuildFile; fileRef = 54751E502423955000168273 /* URL.swift */; };
|
||||
54751E522423955100168273 /* URL.swift in Sources */ = {isa = PBXBuildFile; fileRef = 54751E502423955000168273 /* URL.swift */; };
|
||||
54953E3323DC752E0054345C /* SQDB.swift in Sources */ = {isa = PBXBuildFile; fileRef = 54B7562223D7B2DC008F0C41 /* SQDB.swift */; };
|
||||
54953E3323DC752E0054345C /* DBCore.swift in Sources */ = {isa = PBXBuildFile; fileRef = 54B7562223D7B2DC008F0C41 /* DBCore.swift */; };
|
||||
54953E5F23DEBE840054345C /* TVCDomains.swift in Sources */ = {isa = PBXBuildFile; fileRef = 54953E5E23DEBE840054345C /* TVCDomains.swift */; };
|
||||
54953E6123E0D69A0054345C /* TVCHosts.swift in Sources */ = {isa = PBXBuildFile; fileRef = 54953E6023E0D69A0054345C /* TVCHosts.swift */; };
|
||||
54953E6F23E44CD00054345C /* TVCHostDetails.swift in Sources */ = {isa = PBXBuildFile; fileRef = 54953E6E23E44CD00054345C /* TVCHostDetails.swift */; };
|
||||
54953E7123E473F10054345C /* Settings.bundle in Resources */ = {isa = PBXBuildFile; fileRef = 54953E7023E473F10054345C /* Settings.bundle */; };
|
||||
549A96D62501198400C565FA /* VCEditText.swift in Sources */ = {isa = PBXBuildFile; fileRef = 549A96D52501198400C565FA /* VCEditText.swift */; };
|
||||
549A96DA250419B200C565FA /* CoOccurrence.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 549A96D8250419B200C565FA /* CoOccurrence.storyboard */; };
|
||||
549D6ED524D5BFDB0032E498 /* TVCAppSearch.swift in Sources */ = {isa = PBXBuildFile; fileRef = 549D6ED424D5BFDB0032E498 /* TVCAppSearch.swift */; };
|
||||
549ECD9D24A7AD550097571C /* CustomAlert.swift in Sources */ = {isa = PBXBuildFile; fileRef = 549ECD9C24A7AD550097571C /* CustomAlert.swift */; };
|
||||
54A0CC0924E30C56009B5EC1 /* Recordings.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 54A0CC0724E30C56009B5EC1 /* Recordings.storyboard */; };
|
||||
54A0CC0C24E30D6F009B5EC1 /* Requests.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 54A0CC0A24E30D6F009B5EC1 /* Requests.storyboard */; };
|
||||
54B34594240E6343004C53CC /* TVCFilter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 54B34593240E6343004C53CC /* TVCFilter.swift */; };
|
||||
54B34596240F0513004C53CC /* TableView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 54B34595240F0513004C53CC /* TableView.swift */; };
|
||||
54B345992414F491004C53CC /* DBWrapper.swift in Sources */ = {isa = PBXBuildFile; fileRef = 54B345982414F491004C53CC /* DBWrapper.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 */; };
|
||||
54B345AD241BBB00004C53CC /* DBExtensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 54B345AC241BBB00004C53CC /* DBExtensions.swift */; };
|
||||
54B345B0242264F8004C53CC /* third-level.txt in Resources */ = {isa = PBXBuildFile; fileRef = 54B345AF242264F8004C53CC /* third-level.txt */; };
|
||||
54C056DB23E9E36E00214A3F /* AppInfoType.swift in Sources */ = {isa = PBXBuildFile; fileRef = 54C056DA23E9E36E00214A3F /* AppInfoType.swift */; };
|
||||
54C056DB23E9E36E00214A3F /* AppStoreSearch.swift in Sources */ = {isa = PBXBuildFile; fileRef = 54C056DA23E9E36E00214A3F /* AppStoreSearch.swift */; };
|
||||
54C056DD23E9EEF700214A3F /* BundleIcon.swift in Sources */ = {isa = PBXBuildFile; fileRef = 54C056DC23E9EEF700214A3F /* BundleIcon.swift */; };
|
||||
54CA01D32426B23D003A5E04 /* Resolver.swift in Sources */ = {isa = PBXBuildFile; fileRef = 54CA01D22426B23D003A5E04 /* Resolver.swift */; };
|
||||
54CA01D52426B252003A5E04 /* SafeDict.swift in Sources */ = {isa = PBXBuildFile; fileRef = 54CA01D42426B251003A5E04 /* SafeDict.swift */; };
|
||||
@@ -123,6 +180,26 @@
|
||||
54CA02BE2426D4F3003A5E04 /* DDLog.swift in Sources */ = {isa = PBXBuildFile; fileRef = 54CA02BD2426D4F3003A5E04 /* DDLog.swift */; };
|
||||
54CA02C32426DCCD003A5E04 /* GCDAsyncSocket.m in Sources */ = {isa = PBXBuildFile; fileRef = 54CA02BF2426DCCC003A5E04 /* GCDAsyncSocket.m */; };
|
||||
54CA02C42426DCCD003A5E04 /* GCDAsyncUdpSocket.m in Sources */ = {isa = PBXBuildFile; fileRef = 54CA02C02426DCCD003A5E04 /* GCDAsyncUdpSocket.m */; };
|
||||
54CE8BC424B1ED2100CC1756 /* PushNotification.swift in Sources */ = {isa = PBXBuildFile; fileRef = 54CE8BC324B1ED2100CC1756 /* PushNotification.swift */; };
|
||||
54CE8BC524B1ED2100CC1756 /* PushNotification.swift in Sources */ = {isa = PBXBuildFile; fileRef = 54CE8BC324B1ED2100CC1756 /* PushNotification.swift */; };
|
||||
54CFE86824E3F401001687DD /* TVCShareRecording.swift in Sources */ = {isa = PBXBuildFile; fileRef = 54CFE86724E3F401001687DD /* TVCShareRecording.swift */; };
|
||||
54D8B97A246C9F2000EB2414 /* FilterPipeline.swift in Sources */ = {isa = PBXBuildFile; fileRef = 54D8B979246C9F2000EB2414 /* FilterPipeline.swift */; };
|
||||
54D8B97C2471A7E000EB2414 /* String.swift in Sources */ = {isa = PBXBuildFile; fileRef = 54D8B97B2471A7E000EB2414 /* String.swift */; };
|
||||
54D8B97E2471B88900EB2414 /* DBCommon.swift in Sources */ = {isa = PBXBuildFile; fileRef = 54D8B97D2471B88900EB2414 /* DBCommon.swift */; };
|
||||
54D8B97F2471B89100EB2414 /* DBCommon.swift in Sources */ = {isa = PBXBuildFile; fileRef = 54D8B97D2471B88900EB2414 /* DBCommon.swift */; };
|
||||
54D8B9832471BD8100EB2414 /* DBAppOnly.swift in Sources */ = {isa = PBXBuildFile; fileRef = 54D8B9822471BD8100EB2414 /* DBAppOnly.swift */; };
|
||||
54D8B98624796E9900EB2414 /* GroupedDomainDataSource.swift in Sources */ = {isa = PBXBuildFile; fileRef = 54D8B98524796E9800EB2414 /* GroupedDomainDataSource.swift */; };
|
||||
54E540F2247C423200F7C34A /* DomainFilter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 54E540F1247C423200F7C34A /* DomainFilter.swift */; };
|
||||
54E540F4247D3F2600F7C34A /* SimulatorVPN.swift in Sources */ = {isa = PBXBuildFile; fileRef = 54E540F3247D3F2600F7C34A /* SimulatorVPN.swift */; };
|
||||
54E540F8247DB90F00F7C34A /* RecordingsDB.swift in Sources */ = {isa = PBXBuildFile; fileRef = 54E540F7247DB90F00F7C34A /* RecordingsDB.swift */; };
|
||||
54E540FA2482414800F7C34A /* SyncUpdate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 54E540F92482414800F7C34A /* SyncUpdate.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 */; };
|
||||
/* End PBXBuildFile section */
|
||||
|
||||
/* Begin PBXContainerItemProxy section */
|
||||
@@ -150,10 +227,19 @@
|
||||
/* End PBXCopyFilesBuildPhase section */
|
||||
|
||||
/* Begin PBXFileReference section */
|
||||
540C6456240D929300E948F9 /* EditableRows.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EditableRows.swift; sourceTree = "<group>"; };
|
||||
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>"; };
|
||||
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>"; };
|
||||
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>"; };
|
||||
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>"; };
|
||||
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>"; };
|
||||
@@ -161,33 +247,66 @@
|
||||
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>"; };
|
||||
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>"; };
|
||||
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>"; };
|
||||
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>"; };
|
||||
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; };
|
||||
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>"; };
|
||||
543CDB2223EEE61900B7F323 /* GlassVPN.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = GlassVPN.entitlements; sourceTree = "<group>"; };
|
||||
544C95252407B1C700AB89D0 /* SharedState.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SharedState.swift; sourceTree = "<group>"; };
|
||||
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>"; };
|
||||
54448A3124899A4000771C96 /* SearchBarManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SearchBarManager.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>"; };
|
||||
545DDDCE243E6267003B6544 /* TutorialSheet.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TutorialSheet.swift; sourceTree = "<group>"; };
|
||||
545DDDD024436983003B6544 /* QuickUI.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = QuickUI.swift; sourceTree = "<group>"; };
|
||||
545DDDD324466D37003B6544 /* AutoLayout.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AutoLayout.swift; sourceTree = "<group>"; };
|
||||
54686A7524F8062C0084934D /* NotificationBanner.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NotificationBanner.swift; sourceTree = "<group>"; };
|
||||
54686A8424FD0A3F0084934D /* tut-recording-howto.md */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = net.daringfireball.markdown; path = "tut-recording-howto.md"; sourceTree = "<group>"; };
|
||||
54686A8624FD26410084934D /* TinyMarkdown.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TinyMarkdown.swift; sourceTree = "<group>"; };
|
||||
54686A8824FD31580084934D /* tut-welcome-1.md */ = {isa = PBXFileReference; lastKnownFileType = net.daringfireball.markdown; path = "tut-welcome-1.md"; sourceTree = "<group>"; };
|
||||
54686A8924FD31630084934D /* tut-recording-2.md */ = {isa = PBXFileReference; lastKnownFileType = net.daringfireball.markdown; path = "tut-recording-2.md"; sourceTree = "<group>"; };
|
||||
54686A8A24FD3F100084934D /* tut-recording-1.md */ = {isa = PBXFileReference; lastKnownFileType = net.daringfireball.markdown; path = "tut-recording-1.md"; sourceTree = "<group>"; };
|
||||
54686A8B24FD3F180084934D /* tut-welcome-2.md */ = {isa = PBXFileReference; lastKnownFileType = net.daringfireball.markdown; path = "tut-welcome-2.md"; sourceTree = "<group>"; };
|
||||
54686A8C24FD3F630084934D /* tut-cooccurrence.md */ = {isa = PBXFileReference; lastKnownFileType = net.daringfireball.markdown; path = "tut-cooccurrence.md"; sourceTree = "<group>"; };
|
||||
54751E502423955000168273 /* URL.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = URL.swift; sourceTree = "<group>"; };
|
||||
548B1F9423D338EC005B047C /* main.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = main.entitlements; sourceTree = "<group>"; };
|
||||
54953E5E23DEBE840054345C /* TVCDomains.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TVCDomains.swift; sourceTree = "<group>"; };
|
||||
54953E6023E0D69A0054345C /* TVCHosts.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TVCHosts.swift; sourceTree = "<group>"; };
|
||||
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>"; };
|
||||
549A96D52501198400C565FA /* VCEditText.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VCEditText.swift; sourceTree = "<group>"; };
|
||||
549A96D9250419B200C565FA /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/CoOccurrence.storyboard; sourceTree = "<group>"; };
|
||||
549D6ED424D5BFDB0032E498 /* TVCAppSearch.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TVCAppSearch.swift; sourceTree = "<group>"; };
|
||||
549ECD9C24A7AD550097571C /* CustomAlert.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CustomAlert.swift; sourceTree = "<group>"; };
|
||||
54A0CC0824E30C56009B5EC1 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/Recordings.storyboard; sourceTree = "<group>"; };
|
||||
54A0CC0B24E30D6F009B5EC1 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/Requests.storyboard; sourceTree = "<group>"; };
|
||||
54B34593240E6343004C53CC /* TVCFilter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TVCFilter.swift; sourceTree = "<group>"; };
|
||||
54B34595240F0513004C53CC /* TableView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TableView.swift; sourceTree = "<group>"; };
|
||||
54B345982414F491004C53CC /* DBWrapper.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DBWrapper.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>"; };
|
||||
54B345AC241BBB00004C53CC /* DBExtensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DBExtensions.swift; sourceTree = "<group>"; };
|
||||
54B345AF242264F8004C53CC /* third-level.txt */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text; path = "third-level.txt"; sourceTree = "<group>"; };
|
||||
54B7562223D7B2DC008F0C41 /* SQDB.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SQDB.swift; sourceTree = "<group>"; };
|
||||
54C056DA23E9E36E00214A3F /* AppInfoType.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppInfoType.swift; sourceTree = "<group>"; };
|
||||
54B7562223D7B2DC008F0C41 /* DBCore.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DBCore.swift; sourceTree = "<group>"; };
|
||||
54C056DA23E9E36E00214A3F /* AppStoreSearch.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppStoreSearch.swift; sourceTree = "<group>"; };
|
||||
54C056DC23E9EEF700214A3F /* BundleIcon.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BundleIcon.swift; sourceTree = "<group>"; };
|
||||
54CA00D62426A803003A5E04 /* CocoaAsyncSocket.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = CocoaAsyncSocket.h; sourceTree = "<group>"; };
|
||||
54CA01D22426B23D003A5E04 /* Resolver.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Resolver.swift; sourceTree = "<group>"; };
|
||||
@@ -272,6 +391,23 @@
|
||||
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>"; };
|
||||
54CA02C22426DCCD003A5E04 /* GCDAsyncUdpSocket.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = GCDAsyncUdpSocket.h; sourceTree = "<group>"; };
|
||||
54CE8BC324B1ED2100CC1756 /* PushNotification.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PushNotification.swift; sourceTree = "<group>"; };
|
||||
54CFE86724E3F401001687DD /* TVCShareRecording.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TVCShareRecording.swift; sourceTree = "<group>"; };
|
||||
54D8B979246C9F2000EB2414 /* FilterPipeline.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FilterPipeline.swift; sourceTree = "<group>"; };
|
||||
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>"; };
|
||||
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>"; };
|
||||
54E540F1247C423200F7C34A /* DomainFilter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DomainFilter.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>"; };
|
||||
54E540F92482414800F7C34A /* SyncUpdate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SyncUpdate.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>"; };
|
||||
/* End PBXFileReference section */
|
||||
|
||||
/* Begin PBXFrameworksBuildPhase section */
|
||||
@@ -296,8 +432,11 @@
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
54953E5E23DEBE840054345C /* TVCDomains.swift */,
|
||||
5412F8ED24571B8100A63D7A /* VCDateFilter.swift */,
|
||||
54953E6023E0D69A0054345C /* TVCHosts.swift */,
|
||||
54953E6E23E44CD00054345C /* TVCHostDetails.swift */,
|
||||
544F911F24A67EC5001D4B00 /* TVCOccurrenceContext.swift */,
|
||||
541FC47424A12CE9009154D8 /* Analysis */,
|
||||
);
|
||||
path = Requests;
|
||||
sourceTree = "<group>";
|
||||
@@ -307,6 +446,9 @@
|
||||
children = (
|
||||
542E2A9924051556001462DC /* TVCSettings.swift */,
|
||||
54B34593240E6343004C53CC /* TVCFilter.swift */,
|
||||
5412FCBF24C628F9000DE429 /* TVCReminderAlerts.swift */,
|
||||
5412FCC124C628FA000DE429 /* TVCConnectionAlerts.swift */,
|
||||
5412FCC024C628F9000DE429 /* TVCChooseAlertTone.swift */,
|
||||
);
|
||||
path = Settings;
|
||||
sourceTree = "<group>";
|
||||
@@ -316,12 +458,27 @@
|
||||
children = (
|
||||
540E677F242D2CF100871BBE /* VCRecordings.swift */,
|
||||
540E67832433FAFE00871BBE /* TVCPreviousRecords.swift */,
|
||||
540E67812433483D00871BBE /* VCEditRecording.swift */,
|
||||
5458EBBF243A3F2200CFEB15 /* TVCRecordingDetails.swift */,
|
||||
54CFE86724E3F401001687DD /* TVCShareRecording.swift */,
|
||||
549A96D52501198400C565FA /* VCEditText.swift */,
|
||||
540E67812433483D00871BBE /* VCEditRecording.swift */,
|
||||
549D6ED424D5BFDB0032E498 /* TVCAppSearch.swift */,
|
||||
54B345B12422E029004C53CC /* App Icons */,
|
||||
);
|
||||
path = Recordings;
|
||||
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 = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
@@ -345,17 +502,19 @@
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
54B3459A2415651C004C53CC /* DB */,
|
||||
54E540F0247C386500F7C34A /* Data Source */,
|
||||
54B345A4241BB975004C53CC /* Extensions */,
|
||||
545DDDD224436A03003B6544 /* Common Classes */,
|
||||
541075D324CE284700D6F1BF /* Push Notifications */,
|
||||
548B1F9423D338EC005B047C /* main.entitlements */,
|
||||
541AC5D72399498A00A769D7 /* AppDelegate.swift */,
|
||||
54E67E4A24A8C6370025D261 /* GlassVPN.swift */,
|
||||
541075D824CE2C7200D6F1BF /* GlassVPNHook.swift */,
|
||||
542E2A972404973F001462DC /* TBCMain.swift */,
|
||||
540C6454240D5BAE00E948F9 /* Requests */,
|
||||
540E677E242D2CD200871BBE /* Recordings */,
|
||||
540C6455240D5BD200E948F9 /* Settings */,
|
||||
54B345B12422E029004C53CC /* unused */,
|
||||
541AC5DB2399498A00A769D7 /* Main.storyboard */,
|
||||
541AC5E02399498B00A769D7 /* LaunchScreen.storyboard */,
|
||||
54A0CC0D24E314B6009B5EC1 /* GUI */,
|
||||
541AC5DE2399498B00A769D7 /* Assets.xcassets */,
|
||||
541AC5E32399498B00A769D7 /* Info.plist */,
|
||||
54953E7023E473F10054345C /* Settings.bundle */,
|
||||
@@ -363,15 +522,44 @@
|
||||
path = main;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
541FC47424A12CE9009154D8 /* Analysis */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
5404AEEE24ACC089003B2F54 /* VCAnalysisBar.swift */,
|
||||
541FC47724A1453F009154D8 /* VCCoOccurrence.swift */,
|
||||
);
|
||||
path = Analysis;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
542E2A9B24051F79001462DC /* media */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
54686A8324FD0A3F0084934D /* tutorials */,
|
||||
5430789E24B5E10E00278F2D /* sounds */,
|
||||
541A957523E602DF00C09C19 /* LaunchIcon.png */,
|
||||
54B345AF242264F8004C53CC /* third-level.txt */,
|
||||
);
|
||||
path = media;
|
||||
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 */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
@@ -389,18 +577,56 @@
|
||||
545DDDD224436A03003B6544 /* Common Classes */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
54E67E4824A8B1280025D261 /* Prefs.swift */,
|
||||
54E67E4524A8B0FE0025D261 /* PrefsShared.swift */,
|
||||
545DDDD024436983003B6544 /* QuickUI.swift */,
|
||||
545DDDCE243E6267003B6544 /* TutorialSheet.swift */,
|
||||
540C6456240D929300E948F9 /* EditableRows.swift */,
|
||||
54686A8624FD26410084934D /* TinyMarkdown.swift */,
|
||||
54D8B979246C9F2000EB2414 /* FilterPipeline.swift */,
|
||||
54448A3124899A4000771C96 /* SearchBarManager.swift */,
|
||||
549ECD9C24A7AD550097571C /* CustomAlert.swift */,
|
||||
541FC47524A12D01009154D8 /* IBViews.swift */,
|
||||
5404AEEC24A95F3F003B2F54 /* SlideInAnimation.swift */,
|
||||
541075D024CDBA0000D6F1BF /* ThrottledBatchQueue.swift */,
|
||||
54686A7524F8062C0084934D /* NotificationBanner.swift */,
|
||||
);
|
||||
path = "Common Classes";
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
54686A8324FD0A3F0084934D /* tutorials */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
54686A8824FD31580084934D /* tut-welcome-1.md */,
|
||||
54686A8B24FD3F180084934D /* tut-welcome-2.md */,
|
||||
54686A8A24FD3F100084934D /* tut-recording-1.md */,
|
||||
54686A8924FD31630084934D /* tut-recording-2.md */,
|
||||
54686A8424FD0A3F0084934D /* tut-recording-howto.md */,
|
||||
54686A8C24FD3F630084934D /* tut-cooccurrence.md */,
|
||||
);
|
||||
path = tutorials;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
54A0CC0D24E314B6009B5EC1 /* GUI */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
541AC5E02399498B00A769D7 /* LaunchScreen.storyboard */,
|
||||
541AC5DB2399498A00A769D7 /* Main.storyboard */,
|
||||
54A0CC0A24E30D6F009B5EC1 /* Requests.storyboard */,
|
||||
549A96D8250419B200C565FA /* CoOccurrence.storyboard */,
|
||||
54A0CC0724E30C56009B5EC1 /* Recordings.storyboard */,
|
||||
543078C124B60F3B00278F2D /* Settings.storyboard */,
|
||||
);
|
||||
path = GUI;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
54B3459A2415651C004C53CC /* DB */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
54B7562223D7B2DC008F0C41 /* SQDB.swift */,
|
||||
54B345982414F491004C53CC /* DBWrapper.swift */,
|
||||
54B7562223D7B2DC008F0C41 /* DBCore.swift */,
|
||||
54D8B97D2471B88900EB2414 /* DBCommon.swift */,
|
||||
54D8B9822471BD8100EB2414 /* DBAppOnly.swift */,
|
||||
54B345AC241BBB00004C53CC /* DBExtensions.swift */,
|
||||
541FC9862497D81C00962623 /* TheGreatDestroyer.swift */,
|
||||
);
|
||||
path = DB;
|
||||
sourceTree = "<group>";
|
||||
@@ -408,25 +634,30 @@
|
||||
54B345A4241BB975004C53CC /* Extensions */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
544C95252407B1C700AB89D0 /* SharedState.swift */,
|
||||
54B345A8241BBA0B004C53CC /* Generic.swift */,
|
||||
54B345A8241BBA0B004C53CC /* Logging.swift */,
|
||||
54E67E4E24A8E2910025D261 /* Equatable.swift */,
|
||||
54B345A5241BB982004C53CC /* Notifications.swift */,
|
||||
54B345AC241BBB00004C53CC /* DBExtensions.swift */,
|
||||
54B345AA241BBA5B004C53CC /* AlertSheet.swift */,
|
||||
54B34595240F0513004C53CC /* TableView.swift */,
|
||||
54E67E5024A8E8820025D261 /* View.swift */,
|
||||
541DCA6024A6B0F6005F1A4B /* Color.swift */,
|
||||
54448A2F248647D900771C96 /* Time.swift */,
|
||||
54751E502423955000168273 /* URL.swift */,
|
||||
54EFA4E72491A16A0022D618 /* Font.swift */,
|
||||
54448A2D2486464F00771C96 /* Array.swift */,
|
||||
54D8B97B2471A7E000EB2414 /* String.swift */,
|
||||
54B34595240F0513004C53CC /* TableView.swift */,
|
||||
545DDDD324466D37003B6544 /* AutoLayout.swift */,
|
||||
);
|
||||
path = Extensions;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
54B345B12422E029004C53CC /* unused */ = {
|
||||
54B345B12422E029004C53CC /* App Icons */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
54C056DC23E9EEF700214A3F /* BundleIcon.swift */,
|
||||
54C056DA23E9E36E00214A3F /* AppInfoType.swift */,
|
||||
54C056DA23E9E36E00214A3F /* AppStoreSearch.swift */,
|
||||
);
|
||||
path = unused;
|
||||
path = "App Icons";
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
54CA00D52426A7F2003A5E04 /* robbiehanson-CocoaAsyncSocket */ = {
|
||||
@@ -652,6 +883,18 @@
|
||||
path = ProxySocket;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
54E540F0247C386500F7C34A /* Data Source */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
54E540F3247D3F2600F7C34A /* SimulatorVPN.swift */,
|
||||
54E540F92482414800F7C34A /* SyncUpdate.swift */,
|
||||
54D8B98524796E9800EB2414 /* GroupedDomainDataSource.swift */,
|
||||
54E540F1247C423200F7C34A /* DomainFilter.swift */,
|
||||
54E540F7247DB90F00F7C34A /* RecordingsDB.swift */,
|
||||
);
|
||||
path = "Data Source";
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
/* End PBXGroup section */
|
||||
|
||||
/* Begin PBXNativeTarget section */
|
||||
@@ -746,11 +989,32 @@
|
||||
isa = PBXResourcesBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
543078AC24B5E12500278F2D /* typewriter2.caf in Resources */,
|
||||
54953E7123E473F10054345C /* Settings.bundle in Resources */,
|
||||
54686A9024FD42950084934D /* tut-recording-2.md in Resources */,
|
||||
543078B024B5E12500278F2D /* plop2.caf in Resources */,
|
||||
541AC5E22399498B00A769D7 /* LaunchScreen.storyboard in Resources */,
|
||||
54A0CC0924E30C56009B5EC1 /* Recordings.storyboard in Resources */,
|
||||
54686A8D24FD428C0084934D /* tut-welcome-1.md in Resources */,
|
||||
543078B824B5E12500278F2D /* wood2.caf in Resources */,
|
||||
543078BE24B5E12500278F2D /* drum2.caf in Resources */,
|
||||
543078B424B5E12500278F2D /* snap1.caf in Resources */,
|
||||
541AC5DF2399498B00A769D7 /* Assets.xcassets in Resources */,
|
||||
543078AE24B5E12500278F2D /* wood1.caf in Resources */,
|
||||
54686A8524FD0A3F0084934D /* tut-recording-howto.md in Resources */,
|
||||
541AC5DD2399498A00A769D7 /* Main.storyboard in Resources */,
|
||||
54B345B0242264F8004C53CC /* third-level.txt in Resources */,
|
||||
54686A8F24FD42950084934D /* tut-recording-1.md in Resources */,
|
||||
543078BA24B5E12500278F2D /* typewriter1.caf in Resources */,
|
||||
543078B224B5E12500278F2D /* plop1.caf in Resources */,
|
||||
543078B624B5E12500278F2D /* drum1.caf in Resources */,
|
||||
543078BC24B5E12500278F2D /* clock.caf in Resources */,
|
||||
543078C324B60F3B00278F2D /* Settings.storyboard in Resources */,
|
||||
543078AA24B5E12500278F2D /* snap2.caf in Resources */,
|
||||
54A0CC0C24E30D6F009B5EC1 /* Requests.storyboard in Resources */,
|
||||
549A96DA250419B200C565FA /* CoOccurrence.storyboard in Resources */,
|
||||
54686A9124FD42950084934D /* tut-cooccurrence.md in Resources */,
|
||||
54686A8E24FD42950084934D /* tut-welcome-2.md in Resources */,
|
||||
541A957623E602DF00C09C19 /* LaunchIcon.png in Resources */,
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
@@ -759,6 +1023,17 @@
|
||||
isa = PBXResourcesBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
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;
|
||||
};
|
||||
@@ -769,32 +1044,70 @@
|
||||
isa = PBXSourcesBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
54E67E4924A8B1280025D261 /* Prefs.swift in Sources */,
|
||||
54E540F8247DB90F00F7C34A /* RecordingsDB.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 */,
|
||||
54B345AD241BBB00004C53CC /* DBExtensions.swift in Sources */,
|
||||
54E540F2247C423200F7C34A /* DomainFilter.swift in Sources */,
|
||||
54D8B97E2471B88900EB2414 /* DBCommon.swift in Sources */,
|
||||
54D8B9832471BD8100EB2414 /* DBAppOnly.swift in Sources */,
|
||||
5412FCC224C628FA000DE429 /* TVCReminderAlerts.swift in Sources */,
|
||||
54E67E4D24A8E20D0025D261 /* TVCSettings.swift in Sources */,
|
||||
540E67842433FAFE00871BBE /* TVCPreviousRecords.swift in Sources */,
|
||||
54B345A6241BB982004C53CC /* Notifications.swift in Sources */,
|
||||
54448A2E2486464F00771C96 /* Array.swift in Sources */,
|
||||
54E67E4B24A8C6370025D261 /* GlassVPN.swift in Sources */,
|
||||
54686A7624F8062C0084934D /* NotificationBanner.swift in Sources */,
|
||||
541FC47824A1453F009154D8 /* VCCoOccurrence.swift in Sources */,
|
||||
54B345AB241BBA5B004C53CC /* AlertSheet.swift in Sources */,
|
||||
544C95262407B1C700AB89D0 /* SharedState.swift in Sources */,
|
||||
541DCA6124A6B0F6005F1A4B /* Color.swift in Sources */,
|
||||
545DDDCF243E6267003B6544 /* TutorialSheet.swift in Sources */,
|
||||
54B345A9241BBA0B004C53CC /* Generic.swift in Sources */,
|
||||
54B345A9241BBA0B004C53CC /* Logging.swift in Sources */,
|
||||
54B34596240F0513004C53CC /* TableView.swift in Sources */,
|
||||
540E6780242D2CF100871BBE /* VCRecordings.swift in Sources */,
|
||||
54953E3323DC752E0054345C /* SQDB.swift in Sources */,
|
||||
541075D124CDBA0000D6F1BF /* ThrottledBatchQueue.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 */,
|
||||
54953E6123E0D69A0054345C /* TVCHosts.swift in Sources */,
|
||||
540C6457240D929300E948F9 /* EditableRows.swift in Sources */,
|
||||
549D6ED524D5BFDB0032E498 /* TVCAppSearch.swift in Sources */,
|
||||
54CFE86824E3F401001687DD /* TVCShareRecording.swift in Sources */,
|
||||
54751E512423955100168273 /* URL.swift in Sources */,
|
||||
542E2A9A24051556001462DC /* TVCSettings.swift in Sources */,
|
||||
54953E5F23DEBE840054345C /* TVCDomains.swift in Sources */,
|
||||
54C056DB23E9E36E00214A3F /* AppInfoType.swift in Sources */,
|
||||
54C056DB23E9E36E00214A3F /* AppStoreSearch.swift in Sources */,
|
||||
54D8B98624796E9900EB2414 /* GroupedDomainDataSource.swift in Sources */,
|
||||
541075CE24C9D43A00D6F1BF /* UNNotification.swift in Sources */,
|
||||
54953E6F23E44CD00054345C /* TVCHostDetails.swift in Sources */,
|
||||
54D8B97C2471A7E000EB2414 /* String.swift in Sources */,
|
||||
54E67E5124A8E8820025D261 /* View.swift in Sources */,
|
||||
54C056DD23E9EEF700214A3F /* BundleIcon.swift in Sources */,
|
||||
542E2A982404973F001462DC /* TBCMain.swift in Sources */,
|
||||
54448A3224899A4000771C96 /* SearchBarManager.swift in Sources */,
|
||||
5458EBC0243A3F2200CFEB15 /* TVCRecordingDetails.swift in Sources */,
|
||||
549A96D62501198400C565FA /* VCEditText.swift in Sources */,
|
||||
541FC9872497D81C00962623 /* TheGreatDestroyer.swift in Sources */,
|
||||
54686A8724FD27AA0084934D /* TinyMarkdown.swift in Sources */,
|
||||
5412F8EE24571B8200A63D7A /* VCDateFilter.swift in Sources */,
|
||||
5412FCC424C628FA000DE429 /* TVCConnectionAlerts.swift in Sources */,
|
||||
543078C924B75CEA00278F2D /* PushNotificationAppOnly.swift in Sources */,
|
||||
545DDDD124436983003B6544 /* QuickUI.swift in Sources */,
|
||||
541AC5D82399498A00A769D7 /* AppDelegate.swift in Sources */,
|
||||
54B345992414F491004C53CC /* DBWrapper.swift in Sources */,
|
||||
541FC47624A12D01009154D8 /* IBViews.swift in Sources */,
|
||||
540E67822433483D00871BBE /* VCEditRecording.swift in Sources */,
|
||||
54EFA4E82491A16A0022D618 /* Font.swift in Sources */,
|
||||
54D8B97A246C9F2000EB2414 /* FilterPipeline.swift in Sources */,
|
||||
54B34594240E6343004C53CC /* TVCFilter.swift in Sources */,
|
||||
549ECD9D24A7AD550097571C /* CustomAlert.swift in Sources */,
|
||||
54CE8BC424B1ED2100CC1756 /* PushNotification.swift in Sources */,
|
||||
54E540FA2482414800F7C34A /* SyncUpdate.swift in Sources */,
|
||||
5404AEED24A95F3F003B2F54 /* SlideInAnimation.swift in Sources */,
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
};
|
||||
@@ -812,6 +1125,7 @@
|
||||
54CA02722426B2FD003A5E04 /* IPInterval.swift in Sources */,
|
||||
54CA029A2426B2FD003A5E04 /* Observer.swift in Sources */,
|
||||
54CA025C2426B2FD003A5E04 /* ConnectSession.swift in Sources */,
|
||||
5404AEEB24A90717003B2F54 /* PrefsShared.swift in Sources */,
|
||||
54CA02C42426DCCD003A5E04 /* GCDAsyncUdpSocket.m in Sources */,
|
||||
54CA02BA2426B2FD003A5E04 /* ProxySocket.swift in Sources */,
|
||||
54CA025E2426B2FD003A5E04 /* ResponseGeneratorFactory.swift in Sources */,
|
||||
@@ -835,6 +1149,7 @@
|
||||
54CA02A82426B2FD003A5E04 /* SOCKS5Adapter.swift in Sources */,
|
||||
54CA02792426B2FD003A5E04 /* Checksum.swift in Sources */,
|
||||
54CA02AD2426B2FD003A5E04 /* RejectAdapterFactory.swift in Sources */,
|
||||
541075D224CDBA0000D6F1BF /* ThrottledBatchQueue.swift in Sources */,
|
||||
54CA02672426B2FD003A5E04 /* RawTCPSocketProtocol.swift in Sources */,
|
||||
54CA02602426B2FD003A5E04 /* GCDProxyServer.swift in Sources */,
|
||||
54CA026B2426B2FD003A5E04 /* GCDTCPSocket.swift in Sources */,
|
||||
@@ -845,15 +1160,19 @@
|
||||
54CA02B02426B2FD003A5E04 /* SecureHTTPAdapterFactory.swift in Sources */,
|
||||
54CA02A62426B2FD003A5E04 /* AdapterSocket.swift in Sources */,
|
||||
54CA02742426B2FD003A5E04 /* IPMask.swift in Sources */,
|
||||
541075CF24C9D43A00D6F1BF /* UNNotification.swift in Sources */,
|
||||
54CA02BB2426B2FD003A5E04 /* SOCKS5ProxySocket.swift in Sources */,
|
||||
54D8B97F2471B89100EB2414 /* DBCommon.swift in Sources */,
|
||||
54CA02A42426B2FD003A5E04 /* SecureHTTPAdapter.swift in Sources */,
|
||||
54CA02942426B2FD003A5E04 /* DNSResolver.swift in Sources */,
|
||||
54CA025F2426B2FD003A5E04 /* ProxyServer.swift in Sources */,
|
||||
54CA02842426B2FD003A5E04 /* Rule.swift in Sources */,
|
||||
54CE8BC524B1ED2100CC1756 /* PushNotification.swift in Sources */,
|
||||
54CA02B92426B2FD003A5E04 /* DirectProxySocket.swift in Sources */,
|
||||
54751E522423955100168273 /* URL.swift in Sources */,
|
||||
54CA02A92426B2FD003A5E04 /* RejectAdapter.swift in Sources */,
|
||||
54CA02732426B2FD003A5E04 /* IPPool.swift in Sources */,
|
||||
541075D624CE286200D6F1BF /* CachedConnectionAlert.swift in Sources */,
|
||||
54CA027E2426B2FD003A5E04 /* DomainListRule.swift in Sources */,
|
||||
54CA02782426B2FD003A5E04 /* BinaryDataScanner.swift in Sources */,
|
||||
54CA02B12426B2FD003A5E04 /* ServerAdapterFactory.swift in Sources */,
|
||||
@@ -877,13 +1196,14 @@
|
||||
54CA02972426B2FD003A5E04 /* IPPacket.swift in Sources */,
|
||||
54CA026A2426B2FD003A5E04 /* RawSocketFactory.swift in Sources */,
|
||||
54CA02A02426B2FD003A5E04 /* TunnelEvent.swift in Sources */,
|
||||
546063E523FEFAFE008F505A /* SQDB.swift in Sources */,
|
||||
546063E523FEFAFE008F505A /* DBCore.swift in Sources */,
|
||||
54CA02872426B2FD003A5E04 /* IPRangeListRule.swift in Sources */,
|
||||
54CA02922426B2FD003A5E04 /* DNSSession.swift in Sources */,
|
||||
54CA026D2426B2FD003A5E04 /* Opt.swift in Sources */,
|
||||
54CA02B32426B2FD003A5E04 /* HTTPAdapterFactory.swift in Sources */,
|
||||
54CA02702426B2FD003A5E04 /* HTTPStreamScanner.swift in Sources */,
|
||||
54CA02812426B2FD003A5E04 /* DNSFailRule.swift in Sources */,
|
||||
541075DA24CE2C7200D6F1BF /* GlassVPNHook.swift in Sources */,
|
||||
54CA02AC2426B2FD003A5E04 /* AuthenticationServerAdapterFactory.swift in Sources */,
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
@@ -915,6 +1235,38 @@
|
||||
name = LaunchScreen.storyboard;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
543078C124B60F3B00278F2D /* Settings.storyboard */ = {
|
||||
isa = PBXVariantGroup;
|
||||
children = (
|
||||
543078C224B60F3B00278F2D /* Base */,
|
||||
);
|
||||
name = Settings.storyboard;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
549A96D8250419B200C565FA /* CoOccurrence.storyboard */ = {
|
||||
isa = PBXVariantGroup;
|
||||
children = (
|
||||
549A96D9250419B200C565FA /* Base */,
|
||||
);
|
||||
name = CoOccurrence.storyboard;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
54A0CC0724E30C56009B5EC1 /* Recordings.storyboard */ = {
|
||||
isa = PBXVariantGroup;
|
||||
children = (
|
||||
54A0CC0824E30C56009B5EC1 /* Base */,
|
||||
);
|
||||
name = Recordings.storyboard;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
54A0CC0A24E30D6F009B5EC1 /* Requests.storyboard */ = {
|
||||
isa = PBXVariantGroup;
|
||||
children = (
|
||||
54A0CC0B24E30D6F009B5EC1 /* Base */,
|
||||
);
|
||||
name = Requests.storyboard;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
/* End PBXVariantGroup section */
|
||||
|
||||
/* Begin XCBuildConfiguration section */
|
||||
@@ -1045,7 +1397,7 @@
|
||||
ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES;
|
||||
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
|
||||
CODE_SIGN_ENTITLEMENTS = main/main.entitlements;
|
||||
CURRENT_PROJECT_VERSION = 15;
|
||||
CURRENT_PROJECT_VERSION = 28;
|
||||
INFOPLIST_FILE = main/Info.plist;
|
||||
LD_RUNPATH_SEARCH_PATHS = (
|
||||
"$(inherited)",
|
||||
@@ -1064,7 +1416,7 @@
|
||||
ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES;
|
||||
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
|
||||
CODE_SIGN_ENTITLEMENTS = main/main.entitlements;
|
||||
CURRENT_PROJECT_VERSION = 15;
|
||||
CURRENT_PROJECT_VERSION = 28;
|
||||
INFOPLIST_FILE = main/Info.plist;
|
||||
LD_RUNPATH_SEARCH_PATHS = (
|
||||
"$(inherited)",
|
||||
@@ -1083,7 +1435,7 @@
|
||||
CLANG_ENABLE_MODULES = YES;
|
||||
CODE_SIGN_ENTITLEMENTS = GlassVPN/GlassVPN.entitlements;
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
CURRENT_PROJECT_VERSION = 15;
|
||||
CURRENT_PROJECT_VERSION = 28;
|
||||
INFOPLIST_FILE = GlassVPN/Info.plist;
|
||||
MARKETING_VERSION = 1.0.0;
|
||||
PRODUCT_BUNDLE_IDENTIFIER = "de.uni-bamberg.psi.AppCheck.VPN";
|
||||
@@ -1101,7 +1453,7 @@
|
||||
CLANG_ENABLE_MODULES = YES;
|
||||
CODE_SIGN_ENTITLEMENTS = GlassVPN/GlassVPN.entitlements;
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
CURRENT_PROJECT_VERSION = 15;
|
||||
CURRENT_PROJECT_VERSION = 28;
|
||||
INFOPLIST_FILE = GlassVPN/Info.plist;
|
||||
MARKETING_VERSION = 1.0.0;
|
||||
PRODUCT_BUNDLE_IDENTIFIER = "de.uni-bamberg.psi.AppCheck.VPN";
|
||||
|
||||
@@ -1,13 +1,13 @@
|
||||
import NetworkExtension
|
||||
|
||||
fileprivate var db: SQLiteDatabase?
|
||||
fileprivate var domainFilters: [String : FilterOptions] = [:]
|
||||
fileprivate var hook : GlassVPNHook!
|
||||
|
||||
// MARK: ObserverFactory
|
||||
|
||||
class LDObserverFactory: ObserverFactory {
|
||||
|
||||
|
||||
override func getObserverForProxySocket(_ socket: ProxySocket) -> Observer<ProxySocketEvent>? {
|
||||
// TODO: replace NEKit with custom proxy with minimal footprint
|
||||
return LDProxySocketObserver()
|
||||
}
|
||||
|
||||
@@ -15,103 +15,116 @@ class LDObserverFactory: ObserverFactory {
|
||||
override func signal(_ event: ProxySocketEvent) {
|
||||
switch event {
|
||||
case .receivedRequest(let session, let socket):
|
||||
DDLogDebug("DNS: \(session.host)")
|
||||
let match = domainFilters.first { session.host == $0.key || session.host.hasSuffix("." + $0.key) }
|
||||
let block = match?.value.contains(.blocked) ?? false
|
||||
let ignore = match?.value.contains(.ignored) ?? false
|
||||
if !ignore { try? db?.insertDNSQuery(session.host, blocked: block) }
|
||||
else { DDLogDebug("ignored") }
|
||||
if block { DDLogDebug("blocked"); socket.forceDisconnect() }
|
||||
let kill = hook.processDNSRequest(session.host)
|
||||
if kill { socket.forceDisconnect() }
|
||||
default:
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// MARK: NEPacketTunnelProvider
|
||||
|
||||
class PacketTunnelProvider: NEPacketTunnelProvider {
|
||||
|
||||
let proxyServerPort: UInt16 = 9090
|
||||
let proxyServerAddress = "127.0.0.1"
|
||||
var proxyServer: GCDHTTPProxyServer!
|
||||
private let proxyServerPort: UInt16 = 9090
|
||||
private let proxyServerAddress = "127.0.0.1"
|
||||
private var proxyServer: GCDHTTPProxyServer!
|
||||
|
||||
func reloadDomainFilter() {
|
||||
domainFilters = db?.loadFilters() ?? [:]
|
||||
}
|
||||
|
||||
override func startTunnel(options: [String : NSObject]?, completionHandler: @escaping (Error?) -> Void) {
|
||||
DDLogVerbose("startTunnel")
|
||||
// MARK: Delegate
|
||||
|
||||
override func startTunnel(options: [String : NSObject]?, completionHandler: @escaping (Error?) -> Void) {
|
||||
DDLogVerbose("startTunnel with with options: \(String(describing: options))")
|
||||
PrefsShared.registerDefaults()
|
||||
do {
|
||||
db = try SQLiteDatabase.open()
|
||||
db!.initScheme()
|
||||
try SQLiteDatabase.open().initCommonScheme()
|
||||
} catch {
|
||||
completionHandler(error)
|
||||
completionHandler(error) // if we cant open db, fail immediately
|
||||
return
|
||||
}
|
||||
if proxyServer != nil {
|
||||
proxyServer.stop()
|
||||
}
|
||||
proxyServer = nil
|
||||
|
||||
reloadDomainFilter()
|
||||
// stop previous if any
|
||||
if proxyServer != nil { proxyServer.stop() }
|
||||
proxyServer = nil
|
||||
|
||||
// Create proxy
|
||||
let settings = NEPacketTunnelNetworkSettings(tunnelRemoteAddress: proxyServerAddress)
|
||||
settings.mtu = NSNumber(value: 1500)
|
||||
|
||||
let proxySettings = NEProxySettings()
|
||||
proxySettings.httpEnabled = true;
|
||||
proxySettings.httpServer = NEProxyServer(address: proxyServerAddress, port: Int(proxyServerPort))
|
||||
proxySettings.httpsEnabled = true;
|
||||
proxySettings.httpsServer = NEProxyServer(address: proxyServerAddress, port: Int(proxyServerPort))
|
||||
proxySettings.excludeSimpleHostnames = false;
|
||||
proxySettings.exceptionList = []
|
||||
proxySettings.matchDomains = [""]
|
||||
|
||||
settings.dnsSettings = NEDNSSettings(servers: ["127.0.0.1"])
|
||||
settings.proxySettings = proxySettings;
|
||||
RawSocketFactory.TunnelProvider = self
|
||||
ObserverFactory.currentFactory = LDObserverFactory()
|
||||
willInitProxy()
|
||||
|
||||
self.setTunnelNetworkSettings(settings) { error in
|
||||
self.setTunnelNetworkSettings(createProxy()) { error in
|
||||
guard error == nil else {
|
||||
DDLogError("setTunnelNetworkSettings error: \(String(describing: error))")
|
||||
DDLogError("setTunnelNetworkSettings error: \(error!)")
|
||||
completionHandler(error)
|
||||
return
|
||||
}
|
||||
DDLogVerbose("setTunnelNetworkSettings success \(self.packetFlow)")
|
||||
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)
|
||||
}
|
||||
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)")
|
||||
db = nil
|
||||
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)
|
||||
settings.mtu = NSNumber(value: 1500)
|
||||
|
||||
let proxySettings = NEProxySettings()
|
||||
proxySettings.httpEnabled = true;
|
||||
proxySettings.httpServer = NEProxyServer(address: proxyServerAddress, port: Int(proxyServerPort))
|
||||
proxySettings.httpsEnabled = true;
|
||||
proxySettings.httpsServer = NEProxyServer(address: proxyServerAddress, port: Int(proxyServerPort))
|
||||
proxySettings.excludeSimpleHostnames = false;
|
||||
proxySettings.exceptionList = []
|
||||
proxySettings.matchDomains = [""]
|
||||
|
||||
settings.dnsSettings = NEDNSSettings(servers: ["127.0.0.1"])
|
||||
settings.proxySettings = proxySettings;
|
||||
RawSocketFactory.TunnelProvider = self
|
||||
ObserverFactory.currentFactory = LDObserverFactory()
|
||||
return settings
|
||||
}
|
||||
|
||||
private func didInitProxy() {
|
||||
if PrefsShared.RestartReminder.Enabled {
|
||||
PushNotification.scheduleRestartReminderBadge(on: false)
|
||||
PushNotification.cancel(.CantStopMeNowReminder)
|
||||
}
|
||||
}
|
||||
|
||||
private func shutdown() {
|
||||
// proxy
|
||||
DNSServer.currentServer = nil
|
||||
RawSocketFactory.TunnelProvider = nil
|
||||
ObserverFactory.currentFactory = nil
|
||||
proxyServer.stop()
|
||||
proxyServer = nil
|
||||
completionHandler()
|
||||
exit(EXIT_SUCCESS)
|
||||
}
|
||||
|
||||
override func handleAppMessage(_ messageData: Data, completionHandler: ((Data?) -> Void)?) {
|
||||
DDLogVerbose("handleAppMessage")
|
||||
reloadDomainFilter()
|
||||
}
|
||||
RawSocketFactory.TunnelProvider = nil
|
||||
ObserverFactory.currentFactory = nil
|
||||
proxyServer.stop()
|
||||
proxyServer = nil
|
||||
// custom
|
||||
hook.cleanUp()
|
||||
hook = nil
|
||||
if PrefsShared.RestartReminder.Enabled {
|
||||
PushNotification.scheduleRestartReminderBadge(on: true)
|
||||
PushNotification.scheduleRestartReminderBanner()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
35
GlassVPN/robbiehanson-CocoaAsyncSocket/LICENSE.txt
Normal file
@@ -0,0 +1,35 @@
|
||||
This library is in the public domain.
|
||||
However, not all organizations are allowed to use such a license.
|
||||
For example, Germany doesn't recognize the Public Domain and one is not allowed to use libraries under such license (or similar).
|
||||
|
||||
Thus, the library is now dual licensed,
|
||||
and one is allowed to choose which license they would like to use.
|
||||
|
||||
##################################################
|
||||
License Option #1 :
|
||||
##################################################
|
||||
|
||||
Public Domain
|
||||
|
||||
##################################################
|
||||
License Option #2 :
|
||||
##################################################
|
||||
|
||||
Software License Agreement (BSD License)
|
||||
|
||||
Copyright (c) 2017, Deusty, LLC
|
||||
All rights reserved.
|
||||
|
||||
Redistribution and use of this software in source and binary forms,
|
||||
with or without modification, are permitted provided that the following conditions are met:
|
||||
|
||||
* Redistributions of source code must retain the above
|
||||
copyright notice, this list of conditions and the
|
||||
following disclaimer.
|
||||
|
||||
* Neither the name of Deusty LLC nor the names of its
|
||||
contributors may be used to endorse or promote products
|
||||
derived from this software without specific prior
|
||||
written permission of Deusty LLC.
|
||||
|
||||
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||
27
GlassVPN/zhuhaow-NEKit/LICENSE.md
Normal file
@@ -0,0 +1,27 @@
|
||||
Copyright (c) 2016, Zhuhao Wang
|
||||
All rights reserved.
|
||||
|
||||
Redistribution and use in source and binary forms, with or without
|
||||
modification, are permitted provided that the following conditions are met:
|
||||
|
||||
* Redistributions of source code must retain the above copyright notice, this
|
||||
list of conditions and the following disclaimer.
|
||||
|
||||
* Redistributions in binary form must reproduce the above copyright notice,
|
||||
this list of conditions and the following disclaimer in the documentation
|
||||
and/or other materials provided with the distribution.
|
||||
|
||||
* Neither the name of NEKit nor the names of its
|
||||
contributors may be used to endorse or promote products derived from
|
||||
this software without specific prior written permission.
|
||||
|
||||
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
|
||||
AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
|
||||
IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
|
||||
DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
|
||||
FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
|
||||
DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
|
||||
SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
|
||||
CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
|
||||
OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
|
||||
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||
@@ -141,7 +141,14 @@ public class NWTCPSocket: NSObject, RawTCPSocketProtocol {
|
||||
|
||||
connection!.readMinimumLength(1, maximumLength: Opt.MAXNWTCPSocketReadDataSize) { data, error in
|
||||
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.disconnect()
|
||||
}
|
||||
|
||||
9
GlassVPN/zhuhaow-Resolver/License
Normal file
@@ -0,0 +1,9 @@
|
||||
The MIT License
|
||||
|
||||
Copyright 2018 Zhuhao Wang
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
55
README.md
Normal file
@@ -0,0 +1,55 @@
|
||||
AppCheck – Privacy Monitor
|
||||
==========================
|
||||
|
||||
A pocket DNS monitor and network filter.
|
||||
|
||||

|
||||
|
||||
|
||||
## What is it?
|
||||
|
||||
AppCheck helps you identify which applications communicate with third parties.
|
||||
It does so by logging network requests.
|
||||
AppCheck learns only the destination addresses, not the actual data that is exchanged.
|
||||
|
||||
Your data belongs to you.
|
||||
Therefore, monitoring and analysis take place on your device only.
|
||||
The app does not share any data with us or any other third-party – unless you choose to.
|
||||
|
||||
Join [Testflight beta](https://testflight.apple.com/join/9jjaFeHO)
|
||||
|
||||
|
||||
### How does it work?
|
||||
|
||||
AppCheck creates a local VPN tunnel to intercept all network connections.
|
||||
For each connection AppCheck looks into the DNS headers only, namely the domain names.
|
||||
These domain names are logged in the background while the VPN is active.
|
||||
That means, AppCheck does not have to be active in the foreground all the time.
|
||||
|
||||
|
||||
## Features
|
||||
|
||||
- See outgoing (DNS) network requests in real-time
|
||||
- See history of previous connections
|
||||
- Block unwanted traffic based on domain names
|
||||
- Record app specific activity<sup>1</sup>
|
||||
- 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
|
||||
- Alert Monitor & reminder
|
||||
- Participate in privacy research
|
||||
- Contribute your results
|
||||
- See what others have unveiled
|
||||
- How much traffic does this app produce?
|
||||
|
||||
|
||||
<sup>1</sup> Due to technical limitations, recordings can not be restricted to a single application. Remember to force-quit all other applications before starting a recording.
|
||||
|
||||
|
||||
## Research Project
|
||||
|
||||
*information will be added soon™*
|
||||
|
||||
BIN
doc/screenshot.png
Normal file
|
After Width: | Height: | Size: 158 KiB |
@@ -1,134 +1,39 @@
|
||||
import UIKit
|
||||
import NetworkExtension
|
||||
|
||||
let VPNConfigBundleIdentifier = "de.uni-bamberg.psi.AppCheck.VPN"
|
||||
|
||||
@UIApplicationMain
|
||||
class AppDelegate: UIResponder, UIApplicationDelegate {
|
||||
|
||||
var window: UIWindow?
|
||||
var managerVPN: NETunnelProviderManager?
|
||||
|
||||
func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
|
||||
if UserDefaults.standard.bool(forKey: "kill_db") {
|
||||
UserDefaults.standard.set(false, forKey: "kill_db")
|
||||
SQLiteDatabase.destroyDatabase()
|
||||
}
|
||||
try? SQLiteDatabase.open().initScheme()
|
||||
|
||||
DBWrp.initContentOfDB()
|
||||
|
||||
loadVPN { mgr in
|
||||
self.managerVPN = mgr
|
||||
self.postVPNState()
|
||||
if let db = AppDB {
|
||||
db.initCommonScheme()
|
||||
db.initAppOnlyScheme()
|
||||
}
|
||||
NSNotification.Name.NEVPNStatusDidChange.observe(call: #selector(vpnStatusChanged(_:)), on: self)
|
||||
NotifyFilterChanged.observe(call: #selector(filterDidChange), on: self)
|
||||
|
||||
Prefs.registerDefaults()
|
||||
PrefsShared.registerDefaults()
|
||||
|
||||
#if IOS_SIMULATOR
|
||||
SimulatorVPN.load()
|
||||
#endif
|
||||
|
||||
sync.start()
|
||||
return true
|
||||
}
|
||||
|
||||
@objc private func vpnStatusChanged(_ notification: Notification) {
|
||||
postRawVPNState((notification.object as? NETunnelProviderSession)?.status ?? .invalid)
|
||||
}
|
||||
|
||||
@objc private func filterDidChange() {
|
||||
// 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)
|
||||
func applicationDidBecomeActive(_ application: UIApplication) {
|
||||
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!)
|
||||
// This is a known issue and tolerated.
|
||||
}
|
||||
}
|
||||
|
||||
extension URL {
|
||||
@discardableResult func open() -> Bool { UIApplication.shared.openURL(self) }
|
||||
}
|
||||
|
||||
BIN
main/Assets.xcassets/.DS_Store
vendored
Normal file
26
main/Assets.xcassets/circle-check.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/circle-check.imageset/img.png
vendored
Normal file
|
After Width: | Height: | Size: 247 B |
BIN
main/Assets.xcassets/circle-check.imageset/img@2x.png
vendored
Normal file
|
After Width: | Height: | Size: 423 B |
BIN
main/Assets.xcassets/circle-check.imageset/img@3x.png
vendored
Normal file
|
After Width: | Height: | Size: 572 B |
26
main/Assets.xcassets/circle-x.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/circle-x.imageset/img.png
vendored
Normal file
|
After Width: | Height: | Size: 235 B |
BIN
main/Assets.xcassets/circle-x.imageset/img@2x.png
vendored
Normal file
|
After Width: | Height: | Size: 400 B |
BIN
main/Assets.xcassets/circle-x.imageset/img@3x.png
vendored
Normal file
|
After Width: | Height: | Size: 530 B |
26
main/Assets.xcassets/detail-help.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/detail-help.imageset/img.png
vendored
Normal file
|
After Width: | Height: | Size: 322 B |
BIN
main/Assets.xcassets/detail-help.imageset/img@2x.png
vendored
Normal file
|
After Width: | Height: | Size: 510 B |
BIN
main/Assets.xcassets/detail-help.imageset/img@3x.png
vendored
Normal file
|
After Width: | Height: | Size: 701 B |
26
main/Assets.xcassets/filter-clear.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/filter-clear.imageset/img.png
vendored
Normal file
|
After Width: | Height: | Size: 187 B |
BIN
main/Assets.xcassets/filter-clear.imageset/img@2x.png
vendored
Normal file
|
After Width: | Height: | Size: 289 B |
BIN
main/Assets.xcassets/filter-clear.imageset/img@3x.png
vendored
Normal file
|
After Width: | Height: | Size: 395 B |
26
main/Assets.xcassets/filter-filled.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/filter-filled.imageset/img.png
vendored
Normal file
|
After Width: | Height: | Size: 155 B |
BIN
main/Assets.xcassets/filter-filled.imageset/img@2x.png
vendored
Normal file
|
After Width: | Height: | Size: 226 B |
BIN
main/Assets.xcassets/filter-filled.imageset/img@3x.png
vendored
Normal file
|
After Width: | Height: | Size: 302 B |
26
main/Assets.xcassets/intersection.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/intersection.imageset/img.png
vendored
Normal file
|
After Width: | Height: | Size: 385 B |
BIN
main/Assets.xcassets/intersection.imageset/img@2x.png
vendored
Normal file
|
After Width: | Height: | Size: 695 B |
BIN
main/Assets.xcassets/intersection.imageset/img@3x.png
vendored
Normal file
|
After Width: | Height: | Size: 1.0 KiB |
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 |
@@ -1,779 +0,0 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<document type="com.apple.InterfaceBuilder3.CocoaTouch.Storyboard.XIB" version="3.0" toolsVersion="16096" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" useSafeAreas="YES" colorMatched="YES" initialViewController="sfA-EG-18J">
|
||||
<device id="retina4_0" orientation="portrait" appearance="light"/>
|
||||
<dependencies>
|
||||
<deployment identifier="iOS"/>
|
||||
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="16087"/>
|
||||
<capability name="Safe area layout guides" minToolsVersion="9.0"/>
|
||||
<capability name="documents saved in the Xcode 8 format" minToolsVersion="8.0"/>
|
||||
</dependencies>
|
||||
<scenes>
|
||||
<!--Main-->
|
||||
<scene sceneID="7Rl-BK-ry5">
|
||||
<objects>
|
||||
<tabBarController id="sfA-EG-18J" customClass="TBCMain" customModule="AppCheck" customModuleProvider="target" sceneMemberID="viewController">
|
||||
<tabBar key="tabBar" contentMode="scaleToFill" insetsLayoutMarginsFromSafeArea="NO" id="qza-ey-Iaz">
|
||||
<rect key="frame" x="0.0" y="0.0" width="414" height="49"/>
|
||||
<autoresizingMask key="autoresizingMask"/>
|
||||
<color key="backgroundColor" white="0.0" alpha="0.0" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
|
||||
</tabBar>
|
||||
<connections>
|
||||
<segue destination="RcB-4v-fd4" kind="relationship" relationship="viewControllers" id="cmC-pu-5n2"/>
|
||||
<segue destination="hm5-7q-Zfi" kind="relationship" relationship="viewControllers" id="pfK-BR-9lf"/>
|
||||
<segue destination="dIk-JY-9vE" kind="relationship" relationship="viewControllers" id="AwW-3j-iAg"/>
|
||||
</connections>
|
||||
</tabBarController>
|
||||
<placeholder placeholderIdentifier="IBFirstResponder" id="RDz-8t-yhN" userLabel="First Responder" customClass="UIResponder" sceneMemberID="firstResponder"/>
|
||||
</objects>
|
||||
<point key="canvasLocation" x="-819" y="150"/>
|
||||
</scene>
|
||||
<!--Requests-->
|
||||
<scene sceneID="bDO-X1-bCe">
|
||||
<objects>
|
||||
<navigationController id="RcB-4v-fd4" sceneMemberID="viewController">
|
||||
<tabBarItem key="tabBarItem" title="Requests" image="journal" id="Sj5-Kb-Li8"/>
|
||||
<navigationBar key="navigationBar" contentMode="scaleToFill" insetsLayoutMarginsFromSafeArea="NO" id="HWd-73-m8j">
|
||||
<rect key="frame" x="0.0" y="0.0" width="320" height="44"/>
|
||||
<autoresizingMask key="autoresizingMask"/>
|
||||
</navigationBar>
|
||||
<connections>
|
||||
<segue destination="pdd-aM-sKl" kind="relationship" relationship="rootViewController" id="oMe-a0-xN7"/>
|
||||
</connections>
|
||||
</navigationController>
|
||||
<placeholder placeholderIdentifier="IBFirstResponder" id="8j4-AX-JBN" userLabel="First Responder" customClass="UIResponder" sceneMemberID="firstResponder"/>
|
||||
</objects>
|
||||
<point key="canvasLocation" x="0.0" y="-1250"/>
|
||||
</scene>
|
||||
<!--Domains-->
|
||||
<scene sceneID="MN1-aZ-cZt">
|
||||
<objects>
|
||||
<tableViewController id="pdd-aM-sKl" customClass="TVCDomains" customModule="AppCheck" customModuleProvider="target" sceneMemberID="viewController">
|
||||
<tableView key="view" clipsSubviews="YES" contentMode="scaleToFill" alwaysBounceVertical="YES" dataMode="prototypes" style="plain" separatorStyle="default" rowHeight="-1" estimatedRowHeight="-1" sectionHeaderHeight="28" sectionFooterHeight="28" id="kj3-8X-TyT">
|
||||
<rect key="frame" x="0.0" y="0.0" width="320" height="568"/>
|
||||
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
|
||||
<color key="backgroundColor" systemColor="systemBackgroundColor" cocoaTouchSystemColor="whiteColor"/>
|
||||
<prototypes>
|
||||
<tableViewCell clipsSubviews="YES" contentMode="scaleToFill" insetsLayoutMarginsFromSafeArea="NO" selectionStyle="default" accessoryType="disclosureIndicator" hidesAccessoryWhenEditing="NO" indentationWidth="10" reuseIdentifier="DomainCell" textLabel="0HB-5f-eB1" detailTextLabel="MRe-Eq-gvc" style="IBUITableViewCellStyleSubtitle" id="F8D-aK-j1W">
|
||||
<rect key="frame" x="0.0" y="28" width="320" height="55.5"/>
|
||||
<autoresizingMask key="autoresizingMask"/>
|
||||
<tableViewCellContentView key="contentView" opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center" insetsLayoutMarginsFromSafeArea="NO" tableViewCell="F8D-aK-j1W" id="FY2-xr-hqh">
|
||||
<rect key="frame" x="0.0" y="0.0" width="293" height="55.5"/>
|
||||
<autoresizingMask key="autoresizingMask"/>
|
||||
<subviews>
|
||||
<label opaque="NO" multipleTouchEnabled="YES" contentMode="left" insetsLayoutMarginsFromSafeArea="NO" text="Title" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" id="0HB-5f-eB1">
|
||||
<rect key="frame" x="16" y="10" width="33.5" height="20.5"/>
|
||||
<autoresizingMask key="autoresizingMask"/>
|
||||
<fontDescription key="fontDescription" type="system" pointSize="17"/>
|
||||
<nil key="textColor"/>
|
||||
<nil key="highlightedColor"/>
|
||||
</label>
|
||||
<label opaque="NO" multipleTouchEnabled="YES" contentMode="left" insetsLayoutMarginsFromSafeArea="NO" text="Subtitle" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" id="MRe-Eq-gvc">
|
||||
<rect key="frame" x="16" y="31.5" width="44" height="14.5"/>
|
||||
<autoresizingMask key="autoresizingMask"/>
|
||||
<fontDescription key="fontDescription" type="system" pointSize="12"/>
|
||||
<color key="textColor" systemColor="secondaryLabelColor" red="0.23529411759999999" green="0.23529411759999999" blue="0.26274509800000001" alpha="0.59999999999999998" colorSpace="custom" customColorSpace="sRGB"/>
|
||||
<nil key="highlightedColor"/>
|
||||
</label>
|
||||
</subviews>
|
||||
</tableViewCellContentView>
|
||||
<connections>
|
||||
<segue destination="WcC-nb-Vf5" kind="push" id="EVQ-hO-JE9"/>
|
||||
</connections>
|
||||
</tableViewCell>
|
||||
</prototypes>
|
||||
<connections>
|
||||
<outlet property="dataSource" destination="pdd-aM-sKl" id="4fX-iP-7Oa"/>
|
||||
<outlet property="delegate" destination="pdd-aM-sKl" id="3RN-az-SYU"/>
|
||||
</connections>
|
||||
</tableView>
|
||||
<navigationItem key="navigationItem" title="Domains" id="nY5-jL-QT9"/>
|
||||
</tableViewController>
|
||||
<placeholder placeholderIdentifier="IBFirstResponder" id="jfx-iA-E0v" userLabel="First Responder" customClass="UIResponder" sceneMemberID="firstResponder"/>
|
||||
</objects>
|
||||
<point key="canvasLocation" x="700" y="-1250"/>
|
||||
</scene>
|
||||
<!--Hosts-->
|
||||
<scene sceneID="ZCV-Yx-jjW">
|
||||
<objects>
|
||||
<tableViewController id="WcC-nb-Vf5" customClass="TVCHosts" customModule="AppCheck" customModuleProvider="target" sceneMemberID="viewController">
|
||||
<tableView key="view" clipsSubviews="YES" contentMode="scaleToFill" alwaysBounceVertical="YES" dataMode="prototypes" style="plain" separatorStyle="default" rowHeight="-1" estimatedRowHeight="-1" sectionHeaderHeight="28" sectionFooterHeight="28" id="nRF-dc-dC2">
|
||||
<rect key="frame" x="0.0" y="0.0" width="320" height="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="disclosureIndicator" indentationWidth="10" reuseIdentifier="HostCell" textLabel="Rnk-SP-UHm" detailTextLabel="ovQ-lJ-hWJ" style="IBUITableViewCellStyleSubtitle" id="uv0-9B-Zbb">
|
||||
<rect key="frame" x="0.0" y="28" width="320" height="55.5"/>
|
||||
<autoresizingMask key="autoresizingMask"/>
|
||||
<tableViewCellContentView key="contentView" opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center" preservesSuperviewLayoutMargins="YES" insetsLayoutMarginsFromSafeArea="NO" tableViewCell="uv0-9B-Zbb" id="6vH-Du-gCg">
|
||||
<rect key="frame" x="0.0" y="0.0" width="293" height="55.5"/>
|
||||
<autoresizingMask key="autoresizingMask"/>
|
||||
<subviews>
|
||||
<label opaque="NO" multipleTouchEnabled="YES" contentMode="left" insetsLayoutMarginsFromSafeArea="NO" text="Title" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" id="Rnk-SP-UHm">
|
||||
<rect key="frame" x="16" y="10" width="33.5" height="20.5"/>
|
||||
<autoresizingMask key="autoresizingMask"/>
|
||||
<fontDescription key="fontDescription" type="system" pointSize="17"/>
|
||||
<nil key="textColor"/>
|
||||
<nil key="highlightedColor"/>
|
||||
</label>
|
||||
<label opaque="NO" multipleTouchEnabled="YES" contentMode="left" insetsLayoutMarginsFromSafeArea="NO" text="Subtitle" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" id="ovQ-lJ-hWJ">
|
||||
<rect key="frame" x="16" y="31.5" width="44" height="14.5"/>
|
||||
<autoresizingMask key="autoresizingMask"/>
|
||||
<fontDescription key="fontDescription" type="system" pointSize="12"/>
|
||||
<color key="textColor" systemColor="secondaryLabelColor" red="0.23529411759999999" green="0.23529411759999999" blue="0.26274509800000001" alpha="0.59999999999999998" colorSpace="custom" customColorSpace="sRGB"/>
|
||||
<nil key="highlightedColor"/>
|
||||
</label>
|
||||
</subviews>
|
||||
</tableViewCellContentView>
|
||||
<connections>
|
||||
<segue destination="h7Z-Qr-pJ5" kind="push" id="TPa-Zn-eOs"/>
|
||||
</connections>
|
||||
</tableViewCell>
|
||||
</prototypes>
|
||||
<connections>
|
||||
<outlet property="dataSource" destination="WcC-nb-Vf5" id="szM-iI-Jgi"/>
|
||||
<outlet property="delegate" destination="WcC-nb-Vf5" id="sBd-BW-Wg6"/>
|
||||
</connections>
|
||||
</tableView>
|
||||
<navigationItem key="navigationItem" title="Hosts" prompt="com.app.Example" id="TvD-8U-F05"/>
|
||||
</tableViewController>
|
||||
<placeholder placeholderIdentifier="IBFirstResponder" id="Gdi-Xi-JUL" userLabel="First Responder" customClass="UIResponder" sceneMemberID="firstResponder"/>
|
||||
</objects>
|
||||
<point key="canvasLocation" x="1400" y="-1250"/>
|
||||
</scene>
|
||||
<!--Occurrences-->
|
||||
<scene sceneID="ws3-sK-l8m">
|
||||
<objects>
|
||||
<tableViewController 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">
|
||||
<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="HostDetailCell" textLabel="J2P-mU-Vad" style="IBUITableViewCellStyleDefault" id="ZCA-Dz-i92">
|
||||
<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="ZCA-Dz-i92" id="nxe-48-jAQ">
|
||||
<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" enabled="NO" adjustsFontSizeToFit="NO" id="J2P-mU-Vad">
|
||||
<rect key="frame" x="16" y="0.0" width="288" height="43.5"/>
|
||||
<autoresizingMask key="autoresizingMask"/>
|
||||
<fontDescription key="fontDescription" type="system" pointSize="17"/>
|
||||
<nil key="textColor"/>
|
||||
<nil key="highlightedColor"/>
|
||||
</label>
|
||||
</subviews>
|
||||
</tableViewCellContentView>
|
||||
</tableViewCell>
|
||||
</prototypes>
|
||||
<connections>
|
||||
<outlet property="dataSource" destination="h7Z-Qr-pJ5" id="fyW-Av-fWY"/>
|
||||
<outlet property="delegate" destination="h7Z-Qr-pJ5" id="gBq-jA-u5V"/>
|
||||
</connections>
|
||||
</tableView>
|
||||
<navigationItem key="navigationItem" title="Occurrences" prompt="com.domain.network.cdn" id="bys-2u-rHs"/>
|
||||
</tableViewController>
|
||||
<placeholder placeholderIdentifier="IBFirstResponder" id="UxH-PH-KQy" userLabel="First Responder" customClass="UIResponder" sceneMemberID="firstResponder"/>
|
||||
</objects>
|
||||
<point key="canvasLocation" x="2100" y="-1250"/>
|
||||
</scene>
|
||||
<!--Recordings-->
|
||||
<scene sceneID="ODR-PD-nTU">
|
||||
<objects>
|
||||
<viewController id="hm5-7q-Zfi" customClass="VCRecordings" customModule="AppCheck" customModuleProvider="target" sceneMemberID="viewController">
|
||||
<view key="view" contentMode="scaleToFill" id="JYr-yE-eGS">
|
||||
<rect key="frame" x="0.0" y="0.0" width="320" height="568"/>
|
||||
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
|
||||
<subviews>
|
||||
<stackView opaque="NO" contentMode="scaleToFill" axis="vertical" spacing="8" translatesAutoresizingMaskIntoConstraints="NO" id="Wz5-zb-gwz">
|
||||
<rect key="frame" x="0.0" y="0.0" width="320" height="519"/>
|
||||
<subviews>
|
||||
<view clipsSubviews="YES" contentMode="scaleToFill" translatesAutoresizingMaskIntoConstraints="NO" id="La3-9e-6TK" userLabel="NewRec">
|
||||
<rect key="frame" x="0.0" y="0.0" width="320" height="90"/>
|
||||
<subviews>
|
||||
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="00:00.000" textAlignment="center" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" id="rbR-np-cXD">
|
||||
<rect key="frame" x="0.0" y="0.0" width="320" height="54"/>
|
||||
<autoresizingMask key="autoresizingMask" widthSizable="YES" flexibleMaxY="YES"/>
|
||||
<accessibility key="accessibilityConfiguration">
|
||||
<accessibilityTraits key="traits" staticText="YES" updatesFrequently="YES"/>
|
||||
</accessibility>
|
||||
<fontDescription key="fontDescription" type="system" weight="ultraLight" pointSize="32"/>
|
||||
<nil key="textColor"/>
|
||||
<nil key="highlightedColor"/>
|
||||
</label>
|
||||
<button opaque="NO" contentMode="scaleToFill" contentHorizontalAlignment="center" contentVerticalAlignment="center" buttonType="roundedRect" lineBreakMode="middleTruncation" id="vAq-EZ-Gmx">
|
||||
<rect key="frame" x="0.0" y="54" width="320" height="36"/>
|
||||
<autoresizingMask key="autoresizingMask" widthSizable="YES" flexibleMaxY="YES"/>
|
||||
<fontDescription key="fontDescription" style="UICTFontTextStyleTitle2"/>
|
||||
<state key="normal" title="Stop Recording">
|
||||
<color key="titleColor" systemColor="systemRedColor" red="1" green="0.23137254900000001" blue="0.18823529410000001" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
|
||||
</state>
|
||||
<connections>
|
||||
<action selector="startRecordingButtonTapped:" destination="hm5-7q-Zfi" eventType="touchUpInside" id="hEp-UI-i6R"/>
|
||||
</connections>
|
||||
</button>
|
||||
</subviews>
|
||||
<color key="backgroundColor" systemColor="systemBackgroundColor" cocoaTouchSystemColor="whiteColor"/>
|
||||
<constraints>
|
||||
<constraint firstAttribute="height" constant="90" id="bqy-bR-yVI"/>
|
||||
</constraints>
|
||||
</view>
|
||||
<containerView opaque="NO" contentMode="scaleToFill" translatesAutoresizingMaskIntoConstraints="NO" id="v3Z-HR-abM">
|
||||
<rect key="frame" x="0.0" y="98" width="320" height="421"/>
|
||||
<connections>
|
||||
<segue destination="C7Q-Vu-xAC" kind="embed" id="ZTW-t1-5G1"/>
|
||||
</connections>
|
||||
</containerView>
|
||||
</subviews>
|
||||
</stackView>
|
||||
</subviews>
|
||||
<color key="backgroundColor" systemColor="systemBackgroundColor" cocoaTouchSystemColor="whiteColor"/>
|
||||
<constraints>
|
||||
<constraint firstItem="Wz5-zb-gwz" firstAttribute="leading" secondItem="lFq-fl-zah" secondAttribute="leading" id="Sjv-qq-h50"/>
|
||||
<constraint firstItem="Wz5-zb-gwz" firstAttribute="trailing" secondItem="lFq-fl-zah" secondAttribute="trailing" id="hhA-jU-DJS"/>
|
||||
<constraint firstItem="Wz5-zb-gwz" firstAttribute="bottom" secondItem="lFq-fl-zah" secondAttribute="bottom" id="m6I-NP-LhY"/>
|
||||
<constraint firstItem="Wz5-zb-gwz" firstAttribute="top" secondItem="lFq-fl-zah" secondAttribute="top" id="pEc-Cz-v9f"/>
|
||||
</constraints>
|
||||
<viewLayoutGuide key="safeArea" id="lFq-fl-zah"/>
|
||||
</view>
|
||||
<tabBarItem key="tabBarItem" title="Recordings" image="tag" id="mGk-aq-MRP"/>
|
||||
<connections>
|
||||
<outlet property="startButton" destination="vAq-EZ-Gmx" id="FSo-GH-jtd"/>
|
||||
<outlet property="startNewRecView" destination="La3-9e-6TK" id="I5w-aK-DlY"/>
|
||||
<outlet property="timeLabel" destination="rbR-np-cXD" id="EEe-8F-HT6"/>
|
||||
</connections>
|
||||
</viewController>
|
||||
<placeholder placeholderIdentifier="IBFirstResponder" id="Wfy-Tp-A9o" userLabel="First Responder" customClass="UIResponder" sceneMemberID="firstResponder"/>
|
||||
</objects>
|
||||
<point key="canvasLocation" x="0.0" y="-550"/>
|
||||
</scene>
|
||||
<!--Navigation Controller-->
|
||||
<scene sceneID="GQx-dK-qb5">
|
||||
<objects>
|
||||
<navigationController id="C7Q-Vu-xAC" sceneMemberID="viewController">
|
||||
<navigationItem key="navigationItem" id="mCN-Hk-Z5i"/>
|
||||
<navigationBar key="navigationBar" contentMode="scaleToFill" insetsLayoutMarginsFromSafeArea="NO" translucent="NO" id="ByI-P4-oVv">
|
||||
<rect key="frame" x="0.0" y="0.0" width="320" height="44"/>
|
||||
<autoresizingMask key="autoresizingMask"/>
|
||||
</navigationBar>
|
||||
<toolbar key="toolbar" opaque="NO" clearsContextBeforeDrawing="NO" contentMode="scaleToFill" insetsLayoutMarginsFromSafeArea="NO" id="NtQ-rp-G6c">
|
||||
<autoresizingMask key="autoresizingMask"/>
|
||||
</toolbar>
|
||||
<connections>
|
||||
<segue destination="Fln-DD-aId" kind="relationship" relationship="rootViewController" id="smF-1g-aDM"/>
|
||||
</connections>
|
||||
</navigationController>
|
||||
<placeholder placeholderIdentifier="IBFirstResponder" id="GEP-3e-6Ko" userLabel="First Responder" customClass="UIResponder" sceneMemberID="firstResponder"/>
|
||||
</objects>
|
||||
<point key="canvasLocation" x="700" y="-550"/>
|
||||
</scene>
|
||||
<!--Previous Recordings-->
|
||||
<scene sceneID="RqA-Jc-FDE">
|
||||
<objects>
|
||||
<tableViewController id="Fln-DD-aId" customClass="TVCPreviousRecords" customModule="AppCheck" customModuleProvider="target" sceneMemberID="viewController">
|
||||
<tableView key="view" clipsSubviews="YES" contentMode="scaleToFill" alwaysBounceVertical="YES" dataMode="prototypes" style="plain" separatorStyle="default" rowHeight="-1" estimatedRowHeight="-1" sectionHeaderHeight="28" sectionFooterHeight="28" id="7cH-g6-H5z">
|
||||
<rect key="frame" x="0.0" y="0.0" width="320" height="377"/>
|
||||
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
|
||||
<color key="backgroundColor" systemColor="systemBackgroundColor" cocoaTouchSystemColor="whiteColor"/>
|
||||
<prototypes>
|
||||
<tableViewCell clipsSubviews="YES" contentMode="scaleToFill" preservesSuperviewLayoutMargins="YES" selectionStyle="default" accessoryType="detailButton" indentationWidth="10" reuseIdentifier="PreviousRecordCell" textLabel="hr0-Xt-5gV" detailTextLabel="Xav-Ub-clj" style="IBUITableViewCellStyleSubtitle" id="3kW-3B-1bx">
|
||||
<rect key="frame" x="0.0" y="28" width="320" height="55.5"/>
|
||||
<autoresizingMask key="autoresizingMask"/>
|
||||
<tableViewCellContentView key="contentView" opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center" preservesSuperviewLayoutMargins="YES" insetsLayoutMarginsFromSafeArea="NO" tableViewCell="3kW-3B-1bx" id="OKV-a6-jjd">
|
||||
<rect key="frame" x="0.0" y="0.0" width="280" height="55.5"/>
|
||||
<autoresizingMask key="autoresizingMask"/>
|
||||
<subviews>
|
||||
<label opaque="NO" multipleTouchEnabled="YES" contentMode="left" insetsLayoutMarginsFromSafeArea="NO" text="Title" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" id="hr0-Xt-5gV">
|
||||
<rect key="frame" x="16" y="10" width="33.5" height="20.5"/>
|
||||
<autoresizingMask key="autoresizingMask"/>
|
||||
<fontDescription key="fontDescription" type="system" pointSize="17"/>
|
||||
<nil key="textColor"/>
|
||||
<nil key="highlightedColor"/>
|
||||
</label>
|
||||
<label opaque="NO" multipleTouchEnabled="YES" contentMode="left" insetsLayoutMarginsFromSafeArea="NO" text="Subtitle" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" id="Xav-Ub-clj">
|
||||
<rect key="frame" x="16" y="31.5" width="44" height="14.5"/>
|
||||
<autoresizingMask key="autoresizingMask"/>
|
||||
<fontDescription key="fontDescription" type="system" pointSize="12"/>
|
||||
<color key="textColor" systemColor="secondaryLabelColor" red="0.23529411759999999" green="0.23529411759999999" blue="0.26274509800000001" alpha="0.59999999999999998" colorSpace="custom" customColorSpace="sRGB"/>
|
||||
<nil key="highlightedColor"/>
|
||||
</label>
|
||||
</subviews>
|
||||
</tableViewCellContentView>
|
||||
<connections>
|
||||
<segue destination="50g-BI-Q6S" kind="push" identifier="openRecordDetailsSegue" id="arP-jR-O9d"/>
|
||||
</connections>
|
||||
</tableViewCell>
|
||||
</prototypes>
|
||||
<connections>
|
||||
<outlet property="dataSource" destination="Fln-DD-aId" id="oHb-mU-M1Z"/>
|
||||
<outlet property="delegate" destination="Fln-DD-aId" id="6PY-c0-Nfp"/>
|
||||
</connections>
|
||||
</tableView>
|
||||
<navigationItem key="navigationItem" title="Previous Recordings" id="ow1-cy-qXt"/>
|
||||
<connections>
|
||||
<segue destination="VRk-wv-rhk" kind="modal" identifier="editRecordSegue" id="8rY-sA-Iig"/>
|
||||
</connections>
|
||||
</tableViewController>
|
||||
<placeholder placeholderIdentifier="IBFirstResponder" id="Lta-uo-x4m" userLabel="First Responder" customClass="UIResponder" sceneMemberID="firstResponder"/>
|
||||
</objects>
|
||||
<point key="canvasLocation" x="1400" y="-550"/>
|
||||
</scene>
|
||||
<!--Edit Recording-->
|
||||
<scene sceneID="pqx-CU-4AP">
|
||||
<objects>
|
||||
<viewController id="VRk-wv-rhk" customClass="VCEditRecording" customModule="AppCheck" customModuleProvider="target" sceneMemberID="viewController">
|
||||
<view key="view" contentMode="scaleToFill" id="rXz-Mk-wrK">
|
||||
<rect key="frame" x="0.0" y="0.0" width="320" height="421"/>
|
||||
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
|
||||
<subviews>
|
||||
<navigationBar contentMode="scaleToFill" translucent="NO" translatesAutoresizingMaskIntoConstraints="NO" id="2yS-xK-Wac">
|
||||
<rect key="frame" x="0.0" y="0.0" width="320" height="44"/>
|
||||
<gestureRecognizers/>
|
||||
<items>
|
||||
<navigationItem title="Edit" id="JSi-oz-VRx">
|
||||
<barButtonItem key="leftBarButtonItem" systemItem="cancel" id="TGg-60-wZW">
|
||||
<connections>
|
||||
<action selector="didTapCancel:" destination="VRk-wv-rhk" id="Kff-ed-gdd"/>
|
||||
</connections>
|
||||
</barButtonItem>
|
||||
<barButtonItem key="rightBarButtonItem" enabled="NO" style="done" systemItem="save" id="rWg-hE-Ydl">
|
||||
<connections>
|
||||
<action selector="didTapSave:" destination="VRk-wv-rhk" id="Xee-qo-bQx"/>
|
||||
</connections>
|
||||
</barButtonItem>
|
||||
</navigationItem>
|
||||
</items>
|
||||
<connections>
|
||||
<outletCollection property="gestureRecognizers" destination="klV-Ed-xzV" appends="YES" id="Huf-jb-4Ef"/>
|
||||
</connections>
|
||||
</navigationBar>
|
||||
<stackView opaque="NO" contentMode="scaleToFill" axis="vertical" alignment="center" spacing="8" translatesAutoresizingMaskIntoConstraints="NO" id="xdn-EU-IMx">
|
||||
<rect key="frame" x="16" y="56" width="288" height="355"/>
|
||||
<subviews>
|
||||
<view contentMode="scaleToFill" translatesAutoresizingMaskIntoConstraints="NO" id="Guy-Ra-fpS" userLabel="Title">
|
||||
<rect key="frame" x="0.0" y="0.0" width="288" height="58"/>
|
||||
<subviews>
|
||||
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="Title" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" id="Et0-8d-CId">
|
||||
<rect key="frame" x="0.0" y="0.0" width="288" height="24"/>
|
||||
<autoresizingMask key="autoresizingMask" widthSizable="YES" flexibleMaxY="YES"/>
|
||||
<fontDescription key="fontDescription" style="UICTFontTextStyleTitle2"/>
|
||||
<nil key="textColor"/>
|
||||
<nil key="highlightedColor"/>
|
||||
</label>
|
||||
<textField opaque="NO" contentMode="scaleToFill" contentHorizontalAlignment="left" contentVerticalAlignment="center" placeholder="Unnamed Recording #12345678" textAlignment="natural" minimumFontSize="17" clearButtonMode="whileEditing" id="OCX-wu-l5d">
|
||||
<rect key="frame" x="4" y="24" width="280" height="34"/>
|
||||
<autoresizingMask key="autoresizingMask" widthSizable="YES" flexibleMaxY="YES"/>
|
||||
<fontDescription key="fontDescription" style="UICTFontTextStyleBody"/>
|
||||
<textInputTraits key="textInputTraits" returnKeyType="next"/>
|
||||
<connections>
|
||||
<outlet property="delegate" destination="VRk-wv-rhk" id="uJL-hB-9w7"/>
|
||||
</connections>
|
||||
</textField>
|
||||
</subviews>
|
||||
<color key="backgroundColor" systemColor="systemBackgroundColor" cocoaTouchSystemColor="whiteColor"/>
|
||||
<constraints>
|
||||
<constraint firstAttribute="height" constant="58" id="5ew-Cq-VKh"/>
|
||||
</constraints>
|
||||
</view>
|
||||
<view contentMode="scaleToFill" translatesAutoresizingMaskIntoConstraints="NO" id="ybL-UG-dwT" userLabel="Notes">
|
||||
<rect key="frame" x="0.0" y="66" width="288" height="190"/>
|
||||
<subviews>
|
||||
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="Notes" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" id="QJp-6C-yoZ">
|
||||
<rect key="frame" x="0.0" y="0.0" width="288" height="24"/>
|
||||
<autoresizingMask key="autoresizingMask" widthSizable="YES" flexibleMaxY="YES"/>
|
||||
<fontDescription key="fontDescription" style="UICTFontTextStyleTitle2"/>
|
||||
<nil key="textColor"/>
|
||||
<nil key="highlightedColor"/>
|
||||
</label>
|
||||
<textView clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="scaleToFill" showsHorizontalScrollIndicator="NO" translatesAutoresizingMaskIntoConstraints="NO" id="NXU-yU-eST">
|
||||
<rect key="frame" x="0.0" y="24" width="288" height="166"/>
|
||||
<color key="backgroundColor" systemColor="systemBackgroundColor" cocoaTouchSystemColor="whiteColor"/>
|
||||
<string key="text">1. Line
|
||||
2. Line
|
||||
3. Line
|
||||
4. Line</string>
|
||||
<color key="textColor" systemColor="labelColor" cocoaTouchSystemColor="darkTextColor"/>
|
||||
<fontDescription key="fontDescription" type="system" pointSize="14"/>
|
||||
<textInputTraits key="textInputTraits" autocapitalizationType="sentences"/>
|
||||
<connections>
|
||||
<outlet property="delegate" destination="VRk-wv-rhk" id="vej-jI-13V"/>
|
||||
</connections>
|
||||
</textView>
|
||||
</subviews>
|
||||
<color key="backgroundColor" systemColor="systemBackgroundColor" cocoaTouchSystemColor="whiteColor"/>
|
||||
<constraints>
|
||||
<constraint firstItem="NXU-yU-eST" firstAttribute="leading" secondItem="ybL-UG-dwT" secondAttribute="leading" id="D6U-8L-f9m"/>
|
||||
<constraint firstAttribute="height" relation="greaterThanOrEqual" priority="750" constant="107" id="Pfy-uW-kRl"/>
|
||||
<constraint firstItem="NXU-yU-eST" firstAttribute="trailing" secondItem="ybL-UG-dwT" secondAttribute="trailing" id="eBc-6g-nWr"/>
|
||||
<constraint firstItem="NXU-yU-eST" firstAttribute="top" secondItem="QJp-6C-yoZ" secondAttribute="bottom" id="mnZ-WQ-LX8"/>
|
||||
<constraint firstItem="NXU-yU-eST" firstAttribute="bottom" secondItem="ybL-UG-dwT" secondAttribute="bottom" id="vFS-tG-E43"/>
|
||||
</constraints>
|
||||
</view>
|
||||
<view contentMode="scaleToFill" translatesAutoresizingMaskIntoConstraints="NO" id="QiY-Mm-Dej" userLabel="Details">
|
||||
<rect key="frame" x="0.0" y="264" width="288" height="91"/>
|
||||
<subviews>
|
||||
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="Details" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" id="FR1-Nt-XuB">
|
||||
<rect key="frame" x="0.0" y="0.0" width="288" height="24"/>
|
||||
<autoresizingMask key="autoresizingMask" widthSizable="YES" flexibleMaxY="YES"/>
|
||||
<fontDescription key="fontDescription" style="UICTFontTextStyleTitle2"/>
|
||||
<nil key="textColor"/>
|
||||
<nil key="highlightedColor"/>
|
||||
</label>
|
||||
<textView clipsSubviews="YES" multipleTouchEnabled="YES" userInteractionEnabled="NO" contentMode="scaleToFill" fixedFrame="YES" bounces="NO" scrollEnabled="NO" showsHorizontalScrollIndicator="NO" showsVerticalScrollIndicator="NO" bouncesZoom="NO" editable="NO" selectable="NO" translatesAutoresizingMaskIntoConstraints="NO" id="pql-H5-k6U">
|
||||
<rect key="frame" x="0.0" y="24" width="288" height="67"/>
|
||||
<autoresizingMask key="autoresizingMask" widthSizable="YES" flexibleMaxY="YES"/>
|
||||
<color key="backgroundColor" systemColor="systemBackgroundColor" cocoaTouchSystemColor="whiteColor"/>
|
||||
<string key="text">Start: 1970-01-01 01:00
|
||||
End: 1970-01-01 02:00
|
||||
Duration: 60:00</string>
|
||||
<color key="textColor" systemColor="labelColor" cocoaTouchSystemColor="darkTextColor"/>
|
||||
<fontDescription key="fontDescription" type="system" weight="light" pointSize="14"/>
|
||||
<textInputTraits key="textInputTraits" autocapitalizationType="sentences"/>
|
||||
</textView>
|
||||
</subviews>
|
||||
<color key="backgroundColor" systemColor="systemBackgroundColor" cocoaTouchSystemColor="whiteColor"/>
|
||||
<constraints>
|
||||
<constraint firstAttribute="height" priority="250" constant="91" id="or7-9o-FZb"/>
|
||||
</constraints>
|
||||
</view>
|
||||
</subviews>
|
||||
<constraints>
|
||||
<constraint firstItem="ybL-UG-dwT" firstAttribute="width" secondItem="Guy-Ra-fpS" secondAttribute="width" id="PUH-xO-ZbD"/>
|
||||
<constraint firstItem="QiY-Mm-Dej" firstAttribute="width" secondItem="Guy-Ra-fpS" secondAttribute="width" id="U6e-10-j55"/>
|
||||
<constraint firstItem="Guy-Ra-fpS" firstAttribute="width" secondItem="xdn-EU-IMx" secondAttribute="width" id="ZCJ-ol-1Jv"/>
|
||||
</constraints>
|
||||
</stackView>
|
||||
</subviews>
|
||||
<color key="backgroundColor" systemColor="systemBackgroundColor" cocoaTouchSystemColor="whiteColor"/>
|
||||
<constraints>
|
||||
<constraint firstItem="2yS-xK-Wac" firstAttribute="trailing" secondItem="fMa-Lq-tGz" secondAttribute="trailing" id="1io-bA-4p9"/>
|
||||
<constraint firstItem="2yS-xK-Wac" firstAttribute="leading" secondItem="fMa-Lq-tGz" secondAttribute="leading" id="Fv1-fO-22V"/>
|
||||
<constraint firstItem="xdn-EU-IMx" firstAttribute="leading" secondItem="fMa-Lq-tGz" secondAttribute="leading" constant="16" id="JuR-Ro-IPi"/>
|
||||
<constraint firstItem="xdn-EU-IMx" firstAttribute="top" secondItem="2yS-xK-Wac" secondAttribute="bottom" constant="12" id="Lec-83-aaD"/>
|
||||
<constraint firstItem="xdn-EU-IMx" firstAttribute="trailing" secondItem="fMa-Lq-tGz" secondAttribute="trailing" constant="-16" id="hhC-bL-G3S"/>
|
||||
<constraint firstItem="xdn-EU-IMx" firstAttribute="bottom" secondItem="fMa-Lq-tGz" secondAttribute="bottom" constant="-10" id="p7W-sr-Wch"/>
|
||||
<constraint firstItem="2yS-xK-Wac" firstAttribute="top" secondItem="fMa-Lq-tGz" secondAttribute="top" id="yKh-gv-mgg"/>
|
||||
</constraints>
|
||||
<viewLayoutGuide key="safeArea" id="fMa-Lq-tGz"/>
|
||||
</view>
|
||||
<connections>
|
||||
<outlet property="buttonCancel" destination="TGg-60-wZW" id="5Ej-7t-jaD"/>
|
||||
<outlet property="buttonSave" destination="rWg-hE-Ydl" id="zfM-kx-erX"/>
|
||||
<outlet property="inputDetails" destination="pql-H5-k6U" id="NXm-8f-5E6"/>
|
||||
<outlet property="inputNotes" destination="NXU-yU-eST" id="c2n-cG-aLq"/>
|
||||
<outlet property="inputTitle" destination="OCX-wu-l5d" id="PeC-F5-4mx"/>
|
||||
<outlet property="noteBottom" destination="vFS-tG-E43" id="Bxh-Tl-E2U"/>
|
||||
</connections>
|
||||
</viewController>
|
||||
<placeholder placeholderIdentifier="IBFirstResponder" id="KN7-F1-BOL" userLabel="First Responder" customClass="UIResponder" sceneMemberID="firstResponder"/>
|
||||
<tapGestureRecognizer id="klV-Ed-xzV">
|
||||
<connections>
|
||||
<action selector="hideKeyboard" destination="VRk-wv-rhk" id="iDb-kK-nli"/>
|
||||
</connections>
|
||||
</tapGestureRecognizer>
|
||||
</objects>
|
||||
<point key="canvasLocation" x="2100" y="-550"/>
|
||||
</scene>
|
||||
<!--Logs-->
|
||||
<scene sceneID="DxJ-8o-gTM">
|
||||
<objects>
|
||||
<tableViewController id="50g-BI-Q6S" customClass="TVCRecordingDetails" customModule="AppCheck" customModuleProvider="target" sceneMemberID="viewController">
|
||||
<tableView key="view" clipsSubviews="YES" contentMode="scaleToFill" alwaysBounceVertical="YES" dataMode="prototypes" style="plain" separatorStyle="default" allowsSelection="NO" rowHeight="-1" estimatedRowHeight="-1" sectionHeaderHeight="28" sectionFooterHeight="28" id="cLV-Db-JxM">
|
||||
<rect key="frame" x="0.0" y="0.0" width="320" height="377"/>
|
||||
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
|
||||
<color key="backgroundColor" systemColor="systemBackgroundColor" cocoaTouchSystemColor="whiteColor"/>
|
||||
<prototypes>
|
||||
<tableViewCell clipsSubviews="YES" contentMode="scaleToFill" preservesSuperviewLayoutMargins="YES" selectionStyle="default" indentationWidth="10" reuseIdentifier="PreviousRecordDetailCell" textLabel="rN0-kA-Eln" detailTextLabel="xRp-XG-oKf" style="IBUITableViewCellStyleValue1" id="ceT-cF-lLF">
|
||||
<rect key="frame" x="0.0" y="28" width="320" height="43.5"/>
|
||||
<autoresizingMask key="autoresizingMask"/>
|
||||
<tableViewCellContentView key="contentView" opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center" preservesSuperviewLayoutMargins="YES" insetsLayoutMarginsFromSafeArea="NO" tableViewCell="ceT-cF-lLF" id="c5Y-xg-hSL">
|
||||
<rect key="frame" x="0.0" y="0.0" width="320" height="43.5"/>
|
||||
<autoresizingMask key="autoresizingMask"/>
|
||||
<subviews>
|
||||
<label opaque="NO" multipleTouchEnabled="YES" contentMode="left" insetsLayoutMarginsFromSafeArea="NO" text="Title" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" id="rN0-kA-Eln">
|
||||
<rect key="frame" x="16" y="12" width="33.5" height="20.5"/>
|
||||
<autoresizingMask key="autoresizingMask"/>
|
||||
<fontDescription key="fontDescription" type="system" pointSize="17"/>
|
||||
<nil key="textColor"/>
|
||||
<nil key="highlightedColor"/>
|
||||
</label>
|
||||
<label opaque="NO" multipleTouchEnabled="YES" contentMode="left" insetsLayoutMarginsFromSafeArea="NO" text="Detail" textAlignment="right" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" id="xRp-XG-oKf">
|
||||
<rect key="frame" x="260" y="12" width="44" height="20.5"/>
|
||||
<autoresizingMask key="autoresizingMask"/>
|
||||
<fontDescription key="fontDescription" type="system" pointSize="17"/>
|
||||
<color key="textColor" systemColor="secondaryLabelColor" red="0.23529411759999999" green="0.23529411759999999" blue="0.26274509800000001" alpha="0.59999999999999998" colorSpace="custom" customColorSpace="sRGB"/>
|
||||
<nil key="highlightedColor"/>
|
||||
</label>
|
||||
</subviews>
|
||||
</tableViewCellContentView>
|
||||
</tableViewCell>
|
||||
</prototypes>
|
||||
<connections>
|
||||
<outlet property="dataSource" destination="50g-BI-Q6S" id="SFM-IM-FRx"/>
|
||||
<outlet property="delegate" destination="50g-BI-Q6S" id="LBY-sp-dg0"/>
|
||||
</connections>
|
||||
</tableView>
|
||||
<navigationItem key="navigationItem" title="Logs" id="AXT-fV-keV"/>
|
||||
</tableViewController>
|
||||
<placeholder placeholderIdentifier="IBFirstResponder" id="lan-I9-b0a" userLabel="First Responder" customClass="UIResponder" sceneMemberID="firstResponder"/>
|
||||
</objects>
|
||||
<point key="canvasLocation" x="2800" y="-550"/>
|
||||
</scene>
|
||||
<!--Settings-->
|
||||
<scene sceneID="OEQ-fb-haL">
|
||||
<objects>
|
||||
<navigationController id="dIk-JY-9vE" sceneMemberID="viewController">
|
||||
<tabBarItem key="tabBarItem" title="Settings" image="settings" id="dQu-wE-a8u"/>
|
||||
<navigationBar key="navigationBar" contentMode="scaleToFill" insetsLayoutMarginsFromSafeArea="NO" id="yYW-rX-VnB">
|
||||
<rect key="frame" x="0.0" y="0.0" width="320" height="44"/>
|
||||
<autoresizingMask key="autoresizingMask"/>
|
||||
</navigationBar>
|
||||
<connections>
|
||||
<segue destination="qdB-ZO-LHY" kind="relationship" relationship="rootViewController" id="qJW-Jc-O4D"/>
|
||||
</connections>
|
||||
</navigationController>
|
||||
<placeholder placeholderIdentifier="IBFirstResponder" id="bg9-bR-vlx" userLabel="First Responder" customClass="UIResponder" sceneMemberID="firstResponder"/>
|
||||
</objects>
|
||||
<point key="canvasLocation" x="0.0" y="150"/>
|
||||
</scene>
|
||||
<!--Settings-->
|
||||
<scene sceneID="gEe-ny-NaU">
|
||||
<objects>
|
||||
<tableViewController id="qdB-ZO-LHY" customClass="TVCSettings" customModule="AppCheck" customModuleProvider="target" sceneMemberID="viewController">
|
||||
<tableView key="view" clipsSubviews="YES" contentMode="scaleToFill" bounces="NO" alwaysBounceVertical="YES" dataMode="static" style="grouped" separatorStyle="default" rowHeight="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" 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" type="system" pointSize="17"/>
|
||||
<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="14" width="91" height="16"/>
|
||||
<autoresizingMask key="autoresizingMask"/>
|
||||
<fontDescription key="fontDescription" type="system" pointSize="13"/>
|
||||
<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="14" width="64.5" height="16"/>
|
||||
<autoresizingMask key="autoresizingMask"/>
|
||||
<fontDescription key="fontDescription" type="system" pointSize="13"/>
|
||||
<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="14" width="91" height="16"/>
|
||||
<autoresizingMask key="autoresizingMask"/>
|
||||
<fontDescription key="fontDescription" type="system" pointSize="13"/>
|
||||
<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="14" width="64.5" height="16"/>
|
||||
<autoresizingMask key="autoresizingMask"/>
|
||||
<fontDescription key="fontDescription" type="system" pointSize="13"/>
|
||||
<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="Other Settings" id="wLR-T2-Qxm">
|
||||
<cells>
|
||||
<tableViewCell clipsSubviews="YES" contentMode="scaleToFill" preservesSuperviewLayoutMargins="YES" selectionStyle="none" indentationWidth="10" id="Qyy-0U-yhd">
|
||||
<rect key="frame" x="0.0" y="299.5" width="320" height="44"/>
|
||||
<autoresizingMask key="autoresizingMask"/>
|
||||
<tableViewCellContentView key="contentView" opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center" preservesSuperviewLayoutMargins="YES" insetsLayoutMarginsFromSafeArea="NO" tableViewCell="Qyy-0U-yhd" id="Mfs-fu-W5k">
|
||||
<rect key="frame" x="0.0" y="0.0" width="320" height="44"/>
|
||||
<autoresizingMask key="autoresizingMask"/>
|
||||
<subviews>
|
||||
<button opaque="NO" contentMode="scaleToFill" contentHorizontalAlignment="center" contentVerticalAlignment="center" buttonType="roundedRect" lineBreakMode="middleTruncation" translatesAutoresizingMaskIntoConstraints="NO" id="9Ko-sD-7x0">
|
||||
<rect key="frame" x="125" y="7" width="70" height="30"/>
|
||||
<state key="normal" title="Export DB"/>
|
||||
<connections>
|
||||
<action selector="exportDB:" destination="qdB-ZO-LHY" eventType="touchUpInside" id="3gu-WF-3Xa"/>
|
||||
</connections>
|
||||
</button>
|
||||
</subviews>
|
||||
<constraints>
|
||||
<constraint firstItem="9Ko-sD-7x0" firstAttribute="centerX" secondItem="Mfs-fu-W5k" secondAttribute="centerX" id="LzG-xg-XTg"/>
|
||||
<constraint firstItem="9Ko-sD-7x0" firstAttribute="centerY" secondItem="Mfs-fu-W5k" secondAttribute="centerY" id="SXw-dC-2kl"/>
|
||||
</constraints>
|
||||
</tableViewCellContentView>
|
||||
</tableViewCell>
|
||||
<tableViewCell clipsSubviews="YES" contentMode="scaleToFill" preservesSuperviewLayoutMargins="YES" selectionStyle="none" indentationWidth="10" id="wzU-8s-HGb">
|
||||
<rect key="frame" x="0.0" y="343.5" width="320" height="44"/>
|
||||
<autoresizingMask key="autoresizingMask"/>
|
||||
<tableViewCellContentView key="contentView" opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center" preservesSuperviewLayoutMargins="YES" insetsLayoutMarginsFromSafeArea="NO" tableViewCell="wzU-8s-HGb" id="aNM-6U-bho">
|
||||
<rect key="frame" x="0.0" y="0.0" width="320" height="44"/>
|
||||
<autoresizingMask key="autoresizingMask"/>
|
||||
<subviews>
|
||||
<button opaque="NO" contentMode="scaleToFill" contentHorizontalAlignment="center" contentVerticalAlignment="center" buttonType="roundedRect" lineBreakMode="middleTruncation" translatesAutoresizingMaskIntoConstraints="NO" id="S6B-i8-CoC">
|
||||
<rect key="frame" x="74.5" y="7" width="171" height="30"/>
|
||||
<state key="normal" title="Reset Introduction Alerts"/>
|
||||
<connections>
|
||||
<action selector="resetTutorialAlerts:" destination="qdB-ZO-LHY" eventType="touchUpInside" id="0GX-Ko-bk2"/>
|
||||
</connections>
|
||||
</button>
|
||||
</subviews>
|
||||
<constraints>
|
||||
<constraint firstItem="S6B-i8-CoC" firstAttribute="centerY" secondItem="aNM-6U-bho" secondAttribute="centerY" id="Wet-iT-mke"/>
|
||||
<constraint firstItem="S6B-i8-CoC" firstAttribute="centerX" secondItem="aNM-6U-bho" secondAttribute="centerX" id="qM6-0t-1m4"/>
|
||||
</constraints>
|
||||
</tableViewCellContentView>
|
||||
</tableViewCell>
|
||||
<tableViewCell clipsSubviews="YES" contentMode="scaleToFill" preservesSuperviewLayoutMargins="YES" selectionStyle="none" indentationWidth="10" id="a9C-Qy-pOf">
|
||||
<rect key="frame" x="0.0" y="387.5" width="320" height="44"/>
|
||||
<autoresizingMask key="autoresizingMask"/>
|
||||
<tableViewCellContentView key="contentView" opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center" preservesSuperviewLayoutMargins="YES" insetsLayoutMarginsFromSafeArea="NO" tableViewCell="a9C-Qy-pOf" id="cUk-4x-Weg">
|
||||
<rect key="frame" x="0.0" y="0.0" width="320" height="44"/>
|
||||
<autoresizingMask key="autoresizingMask"/>
|
||||
<subviews>
|
||||
<button opaque="NO" contentMode="scaleToFill" contentHorizontalAlignment="center" contentVerticalAlignment="center" buttonType="roundedRect" lineBreakMode="middleTruncation" translatesAutoresizingMaskIntoConstraints="NO" id="17e-nR-aCh">
|
||||
<rect key="frame" x="111.5" y="7" width="97" height="30"/>
|
||||
<state key="normal" title="Delete all logs">
|
||||
<color key="titleColor" systemColor="systemRedColor" red="1" green="0.23137254900000001" blue="0.18823529410000001" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
|
||||
</state>
|
||||
<connections>
|
||||
<action selector="clearDatabaseResults:" destination="qdB-ZO-LHY" eventType="touchUpInside" id="Rep-Do-4OQ"/>
|
||||
</connections>
|
||||
</button>
|
||||
</subviews>
|
||||
<constraints>
|
||||
<constraint firstItem="17e-nR-aCh" firstAttribute="centerX" secondItem="cUk-4x-Weg" secondAttribute="centerX" id="dU5-1x-ETF"/>
|
||||
<constraint firstItem="17e-nR-aCh" firstAttribute="centerY" secondItem="cUk-4x-Weg" secondAttribute="centerY" id="nLq-yi-u2E"/>
|
||||
</constraints>
|
||||
</tableViewCellContentView>
|
||||
</tableViewCell>
|
||||
</cells>
|
||||
</tableViewSection>
|
||||
</sections>
|
||||
<connections>
|
||||
<outlet property="dataSource" destination="qdB-ZO-LHY" id="RH3-xR-dpC"/>
|
||||
<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>
|
||||
<point key="canvasLocation" x="700" y="150"/>
|
||||
</scene>
|
||||
<!--Domains-->
|
||||
<scene sceneID="218-uP-X7b">
|
||||
<objects>
|
||||
<tableViewController id="q3B-Yi-1bx" customClass="TVCFilter" customModule="AppCheck" customModuleProvider="target" sceneMemberID="viewController">
|
||||
<tableView key="view" clipsSubviews="YES" contentMode="scaleToFill" alwaysBounceVertical="YES" dataMode="prototypes" style="plain" separatorStyle="default" 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" type="system" pointSize="17"/>
|
||||
<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>
|
||||
<inferredMetricsTieBreakers>
|
||||
<segue reference="EzT-Xq-wka"/>
|
||||
</inferredMetricsTieBreakers>
|
||||
<resources>
|
||||
<image name="journal" width="25" height="25"/>
|
||||
<image name="settings" width="25" height="25"/>
|
||||
<image name="tag" width="25" height="25"/>
|
||||
</resources>
|
||||
</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]`
|
||||
lazy var buttonsBar: UIStackView = {
|
||||
let cancel = QuickUI.button("Cancel", target: self, action: #selector(didTapCancel))
|
||||
let save = QuickUI.button("Save", target: self, action: #selector(didTapSave))
|
||||
save.titleLabel?.font = save.titleLabel?.font.bold()
|
||||
let bar = UIStackView(arrangedSubviews: [cancel, save])
|
||||
bar.axis = .horizontal
|
||||
bar.distribution = .equalSpacing
|
||||
return bar
|
||||
}()
|
||||
|
||||
|
||||
// MARK: - Init
|
||||
|
||||
required init?(coder: NSCoder) { fatalError("init(coder:) has not been implemented") }
|
||||
|
||||
init(title: String? = nil, detail: String? = nil, view custom: CustomView) {
|
||||
alertTitle = title
|
||||
alertDetail = detail
|
||||
customView = custom
|
||||
super.init(nibName: nil, bundle: nil)
|
||||
}
|
||||
|
||||
override var isModalInPresentation: Bool { set{} get{true} }
|
||||
override var modalPresentationStyle: UIModalPresentationStyle { set{} get{.custom} }
|
||||
override var transitioningDelegate: UIViewControllerTransitioningDelegate? {
|
||||
set {} get {
|
||||
SlideInTransitioningDelegate(for: .bottom, modal: true)
|
||||
}
|
||||
}
|
||||
|
||||
internal override func loadView() {
|
||||
let control = UIView()
|
||||
control.backgroundColor = .sysBackground
|
||||
view = control
|
||||
|
||||
var tmpPrevivous: UIView? = nil
|
||||
|
||||
func adaptive(margin: CGFloat, _ fn: () -> NSLayoutConstraint) {
|
||||
regularConstraints.append(fn() + margin)
|
||||
compactConstraints.append(fn() + margin/2)
|
||||
}
|
||||
|
||||
func addLabel(_ lbl: UILabel) {
|
||||
lbl.numberOfLines = 0
|
||||
control.addSubview(lbl)
|
||||
lbl.anchor([.leading, .trailing], to: control.layoutMarginsGuide)
|
||||
if let p = tmpPrevivous {
|
||||
adaptive(margin: 16) { lbl.topAnchor =&= p.bottomAnchor }
|
||||
} else {
|
||||
adaptive(margin: 12) { lbl.topAnchor =&= control.layoutMarginsGuide.topAnchor }
|
||||
}
|
||||
tmpPrevivous = lbl
|
||||
}
|
||||
|
||||
// Alert title & description
|
||||
if let t = alertTitle {
|
||||
let lbl = QuickUI.label(t, align: .center, style: .subheadline)
|
||||
lbl.font = lbl.font.bold()
|
||||
addLabel(lbl)
|
||||
}
|
||||
|
||||
if let d = alertDetail {
|
||||
addLabel(QuickUI.label(d, align: .center, style: .footnote))
|
||||
}
|
||||
|
||||
// User content
|
||||
control.addSubview(customView)
|
||||
customView.anchor([.leading, .trailing], to: control)
|
||||
if let p = tmpPrevivous {
|
||||
customView.topAnchor =&= p.bottomAnchor | .defaultHigh
|
||||
} else {
|
||||
customView.topAnchor =&= control.layoutMarginsGuide.topAnchor
|
||||
}
|
||||
|
||||
// Action buttons
|
||||
control.addSubview(buttonsBar)
|
||||
buttonsBar.anchor([.leading, .trailing], to: control.layoutMarginsGuide, margin: 8)
|
||||
buttonsBar.topAnchor =&= customView.bottomAnchor | .defaultHigh
|
||||
|
||||
adaptive(margin: 12) { control.layoutMarginsGuide.bottomAnchor =&= buttonsBar.bottomAnchor }
|
||||
|
||||
adaptToNewTraits(traitCollection)
|
||||
view.frame.size = view.systemLayoutSizeFitting(UIView.layoutFittingCompressedSize)
|
||||
}
|
||||
|
||||
|
||||
// MARK: - Adaptive Traits
|
||||
|
||||
private var compactConstraints: [NSLayoutConstraint] = []
|
||||
private var regularConstraints: [NSLayoutConstraint] = []
|
||||
|
||||
private func adaptToNewTraits(_ traits: UITraitCollection) {
|
||||
let flag = traits.verticalSizeClass == .compact
|
||||
NSLayoutConstraint.deactivate(flag ? regularConstraints : compactConstraints)
|
||||
NSLayoutConstraint.activate(flag ? compactConstraints : regularConstraints)
|
||||
view.setNeedsLayout()
|
||||
}
|
||||
|
||||
override func willTransition(to newCollection: UITraitCollection, with coordinator: UIViewControllerTransitionCoordinator) {
|
||||
super.willTransition(to: newCollection, with: coordinator)
|
||||
adaptToNewTraits(newCollection)
|
||||
}
|
||||
|
||||
// MARK: - User Interaction
|
||||
|
||||
override var keyCommands: [UIKeyCommand]? {
|
||||
[UIKeyCommand(input: UIKeyCommand.inputEscape, modifierFlags: [], action: #selector(didTapCancel))]
|
||||
}
|
||||
|
||||
@objc private func didTapCancel() {
|
||||
callback = nil
|
||||
dismiss(animated: true)
|
||||
}
|
||||
|
||||
@objc private func didTapSave() {
|
||||
dismiss(animated: true) {
|
||||
self.callback?(self.customView)
|
||||
self.callback = nil
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// MARK: - Present & Dismiss
|
||||
|
||||
func present(in viewController: UIViewController, onSuccess: @escaping (CustomView) -> Void) {
|
||||
callback = onSuccess
|
||||
viewController.present(self, animated: true)
|
||||
}
|
||||
}
|
||||
|
||||
// ###################################
|
||||
// #
|
||||
// # MARK: - Date Picker Alert
|
||||
// #
|
||||
// ###################################
|
||||
|
||||
class DatePickerAlert : CustomAlert<UIDatePicker> {
|
||||
|
||||
let datePicker = UIDatePicker()
|
||||
|
||||
required init?(coder: NSCoder) { fatalError("init(coder:) has not been implemented") }
|
||||
|
||||
init(title: String? = nil, detail: String? = nil, initial date: Date? = nil) {
|
||||
if let date = date {
|
||||
datePicker.setDate(date, animated: false)
|
||||
}
|
||||
super.init(title: title, detail: detail, view: datePicker)
|
||||
|
||||
let now = QuickUI.button("Now", target: self, action: #selector(didTapNow))
|
||||
now.titleLabel?.font = now.titleLabel?.font.bold()
|
||||
now.setTitleColor(.sysLabel, for: .normal)
|
||||
buttonsBar.insertArrangedSubview(now, at: 1)
|
||||
}
|
||||
|
||||
@objc private func didTapNow() {
|
||||
datePicker.date = Date()
|
||||
}
|
||||
|
||||
func present(in viewController: UIViewController, onSuccess: @escaping (UIDatePicker, Date) -> Void) {
|
||||
super.present(in: viewController) {
|
||||
onSuccess($0, $0.date)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// #######################################
|
||||
// #
|
||||
// # MARK: - Duration Picker Alert
|
||||
// #
|
||||
// #######################################
|
||||
|
||||
class DurationPickerAlert: CustomAlert<UIPickerView>, UIPickerViewDataSource, UIPickerViewDelegate {
|
||||
|
||||
let pickerView = UIPickerView()
|
||||
private let dataSource: [[String]]
|
||||
private let compWidths: [CGFloat]
|
||||
|
||||
required init?(coder: NSCoder) { fatalError("init(coder:) has not been implemented") }
|
||||
|
||||
/// - Parameter options: [[List of labels] per component]
|
||||
/// - Parameter widths: If `nil` set all components to equal width
|
||||
init(title: String? = nil, detail: String? = nil, options: [[String]], widths: [CGFloat]? = nil) {
|
||||
assert(widths == nil || widths!.count == options.count, "widths.count != options.count")
|
||||
|
||||
dataSource = options
|
||||
compWidths = widths ?? options.map { _ in 1 / CGFloat(options.count) }
|
||||
|
||||
super.init(title: title, detail: detail, view: pickerView)
|
||||
|
||||
pickerView.dataSource = self
|
||||
pickerView.delegate = self
|
||||
}
|
||||
|
||||
func numberOfComponents(in _: UIPickerView) -> Int {
|
||||
dataSource.count
|
||||
}
|
||||
func pickerView(_: UIPickerView, numberOfRowsInComponent c: Int) -> Int {
|
||||
dataSource[c].count
|
||||
}
|
||||
func pickerView(_: UIPickerView, titleForRow r: Int, forComponent c: Int) -> String? {
|
||||
dataSource[c][r]
|
||||
}
|
||||
func pickerView(_ pickerView: UIPickerView, widthForComponent c: Int) -> CGFloat {
|
||||
compWidths[c] * pickerView.frame.width
|
||||
}
|
||||
|
||||
func present(in viewController: UIViewController, onSuccess: @escaping (UIPickerView, [Int]) -> Void) {
|
||||
super.present(in: viewController) {
|
||||
onSuccess($0, $0.selection)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
extension UIPickerView {
|
||||
var selection: [Int] {
|
||||
get { (0..<numberOfComponents).map { selectedRow(inComponent: $0) } }
|
||||
set { setSelection(newValue) }
|
||||
}
|
||||
/// - Warning: Does not check for boundaries!
|
||||
func setSelection(_ selection: [Int], animated: Bool = false) {
|
||||
assert(selection.count == numberOfComponents, "selection.count != components.count")
|
||||
for (c, i) in selection.enumerated() {
|
||||
selectRow(i, inComponent: c, animated: animated)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,147 +0,0 @@
|
||||
import UIKit
|
||||
|
||||
public enum RowAction {
|
||||
case ignore, block, delete
|
||||
// static let all: [RowAction] = [.ignore, .block, .delete]
|
||||
}
|
||||
|
||||
// MARK: - Generic
|
||||
|
||||
protocol EditableRows {
|
||||
func editableRowUserInfo(_ index: IndexPath) -> Any?
|
||||
func editableRowActions(_ index: IndexPath) -> [(RowAction, String)]
|
||||
func editableRowActionColor(_ index: IndexPath, _ action: RowAction) -> UIColor?
|
||||
@discardableResult func editableRowCallback(_ atIndexPath: IndexPath, _ action: RowAction, _ userInfo: Any?) -> Bool
|
||||
}
|
||||
|
||||
extension EditableRows where Self: UITableViewController {
|
||||
fileprivate func getRowActionsIOS9(_ index: IndexPath) -> [UITableViewRowAction]? {
|
||||
let userInfo = editableRowUserInfo(index)
|
||||
return editableRowActions(index).compactMap { a,t in
|
||||
let x = UITableViewRowAction(style: a == .delete ? .destructive : .normal, title: t) { self.editableRowCallback($1, a, userInfo) }
|
||||
if let color = editableRowActionColor(index, a) {
|
||||
x.backgroundColor = color
|
||||
}
|
||||
return x
|
||||
}
|
||||
}
|
||||
@available(iOS 11.0, *)
|
||||
fileprivate func getRowActionsIOS11(_ index: IndexPath) -> UISwipeActionsConfiguration? {
|
||||
let userInfo = editableRowUserInfo(index)
|
||||
return UISwipeActionsConfiguration(actions: editableRowActions(index).compactMap { a,t in
|
||||
let x = UIContextualAction(style: a == .delete ? .destructive : .normal, title: t) { $2(self.editableRowCallback(index, a, userInfo)) }
|
||||
x.backgroundColor = editableRowActionColor(index, a)
|
||||
return x
|
||||
})
|
||||
}
|
||||
func editableRowUserInfo(_ index: IndexPath) -> Any? { nil }
|
||||
}
|
||||
|
||||
|
||||
|
||||
// MARK: - Edit Ignore-Block-Delete
|
||||
|
||||
protocol EditActionsIgnoreBlockDelete : EditableRows {
|
||||
var dataSource: [GroupedDomain] { get set }
|
||||
}
|
||||
extension EditActionsIgnoreBlockDelete where Self: UITableViewController {
|
||||
func editableRowActions(_ index: IndexPath) -> [(RowAction, String)] {
|
||||
let x = dataSource[index.row]
|
||||
if x.domain.starts(with: "#") {
|
||||
return [(.delete, "Delete")]
|
||||
}
|
||||
let b = x.options?.contains(.blocked) ?? false
|
||||
let i = x.options?.contains(.ignored) ?? false
|
||||
return [(.delete, "Delete"), (.block, b ? "Unblock" : "Block"), (.ignore, i ? "Unignore" : "Ignore")]
|
||||
}
|
||||
|
||||
func editableRowActionColor(_: IndexPath, _ action: RowAction) -> UIColor? {
|
||||
action == .block ? .systemOrange : nil
|
||||
}
|
||||
|
||||
func editableRowUserInfo(_ index: IndexPath) -> Any? { dataSource[index.row] }
|
||||
|
||||
func editableRowCallback(_ index: IndexPath, _ action: RowAction, _ userInfo: Any?) -> Bool {
|
||||
let entry = userInfo as! GroupedDomain
|
||||
switch action {
|
||||
case .ignore: showFilterSheet(entry, .ignored)
|
||||
case .block: showFilterSheet(entry, .blocked)
|
||||
case .delete:
|
||||
AlertDeleteLogs(entry.domain, latest: entry.lastModified) {
|
||||
DBWrp.deleteHistory(domain: entry.domain, since: $0)
|
||||
}.presentIn(self)
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
private func showFilterSheet(_ entry: GroupedDomain, _ filter: FilterOptions) {
|
||||
if entry.options?.contains(filter) ?? false {
|
||||
DBWrp.updateFilter(entry.domain, remove: filter)
|
||||
} else {
|
||||
// TODO: alert sheet
|
||||
DBWrp.updateFilter(entry.domain, add: filter)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: Extensions
|
||||
extension TVCDomains : EditActionsIgnoreBlockDelete {
|
||||
override func tableView(_ tableView: UITableView, editActionsForRowAt indexPath: IndexPath) -> [UITableViewRowAction]? {
|
||||
getRowActionsIOS9(indexPath)
|
||||
}
|
||||
@available(iOS 11.0, *)
|
||||
override func tableView(_ tableView: UITableView, trailingSwipeActionsConfigurationForRowAt indexPath: IndexPath) -> UISwipeActionsConfiguration? {
|
||||
getRowActionsIOS11(indexPath)
|
||||
}
|
||||
}
|
||||
|
||||
extension TVCHosts : EditActionsIgnoreBlockDelete {
|
||||
override func tableView(_ tableView: UITableView, editActionsForRowAt indexPath: IndexPath) -> [UITableViewRowAction]? {
|
||||
getRowActionsIOS9(indexPath)
|
||||
}
|
||||
@available(iOS 11.0, *)
|
||||
override func tableView(_ tableView: UITableView, trailingSwipeActionsConfigurationForRowAt indexPath: IndexPath) -> UISwipeActionsConfiguration? {
|
||||
getRowActionsIOS11(indexPath)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
// MARK: - Edit Remove
|
||||
|
||||
protocol EditActionsRemove : EditableRows {}
|
||||
extension EditActionsRemove where Self: UITableViewController {
|
||||
func editableRowActions(_: IndexPath) -> [(RowAction, String)] { [(.delete, "Remove")] }
|
||||
func editableRowActionColor(_: IndexPath, _: RowAction) -> UIColor? { nil }
|
||||
}
|
||||
|
||||
// MARK: Extensions
|
||||
extension TVCFilter : EditableRows {
|
||||
override func tableView(_ tableView: UITableView, editActionsForRowAt indexPath: IndexPath) -> [UITableViewRowAction]? {
|
||||
getRowActionsIOS9(indexPath)
|
||||
}
|
||||
@available(iOS 11.0, *)
|
||||
override func tableView(_ tableView: UITableView, trailingSwipeActionsConfigurationForRowAt indexPath: IndexPath) -> UISwipeActionsConfiguration? {
|
||||
getRowActionsIOS11(indexPath)
|
||||
}
|
||||
}
|
||||
|
||||
extension TVCPreviousRecords : EditableRows {
|
||||
override func tableView(_ tableView: UITableView, editActionsForRowAt indexPath: IndexPath) -> [UITableViewRowAction]? {
|
||||
getRowActionsIOS9(indexPath)
|
||||
}
|
||||
@available(iOS 11.0, *)
|
||||
override func tableView(_ tableView: UITableView, trailingSwipeActionsConfigurationForRowAt indexPath: IndexPath) -> UISwipeActionsConfiguration? {
|
||||
getRowActionsIOS11(indexPath)
|
||||
}
|
||||
}
|
||||
|
||||
extension TVCRecordingDetails : EditableRows {
|
||||
override func tableView(_ tableView: UITableView, editActionsForRowAt indexPath: IndexPath) -> [UITableViewRowAction]? {
|
||||
getRowActionsIOS9(indexPath)
|
||||
}
|
||||
@available(iOS 11.0, *)
|
||||
override func tableView(_ tableView: UITableView, trailingSwipeActionsConfigurationForRowAt indexPath: IndexPath) -> UISwipeActionsConfiguration? {
|
||||
getRowActionsIOS11(indexPath)
|
||||
}
|
||||
}
|
||||
418
main/Common Classes/FilterPipeline.swift
Normal file
@@ -0,0 +1,418 @@
|
||||
import UIKit
|
||||
|
||||
protocol FilterPipelineDelegate: AnyObject {
|
||||
/// Call `reloadData()`
|
||||
func filterPipelineDidReset()
|
||||
/// Call `safeDeleteRows()`
|
||||
func filterPipeline(delete rows: [Int])
|
||||
/// Call `safeInsertRow()`
|
||||
func filterPipeline(insert row: Int)
|
||||
/// Call `safeReloadRow()`
|
||||
func filterPipeline(update row: Int)
|
||||
/// Call `safeMoveRow()`
|
||||
func filterPipeline(move oldRow: Int, to newRow: Int)
|
||||
}
|
||||
|
||||
// MARK: - FilterPipeline
|
||||
|
||||
class FilterPipeline<T> {
|
||||
|
||||
private(set) fileprivate var dataSource: [T] = []
|
||||
|
||||
private var pipeline: [PipelineFilter<T>] = []
|
||||
private var display: PipelineSorting<T>!
|
||||
weak var delegate: FilterPipelineDelegate?
|
||||
|
||||
/// - Returns: Number of elements in `projection`
|
||||
@inline(__always) func displayObjectCount() -> Int { display.projection.count }
|
||||
|
||||
/// Dereference `projection` index to `dataSource` index
|
||||
/// - Complexity: O(1)
|
||||
@inline(__always) func displayObject(at index: Int) -> T { dataSource[display.projection[index]] }
|
||||
|
||||
/// Search and return first element in `dataSource` that matches `predicate`.
|
||||
/// - Returns: Index in `dataSource` and found object or `nil` if no matching item found.
|
||||
/// - Complexity: O(*n*), where *n* is the length of the `dataSource`.
|
||||
func dataSourceGet(where predicate: ((T) -> Bool)) -> (index: Int, object: T)? {
|
||||
// TODO: use sorted dataSource for binary lookup?
|
||||
// would require to shift filter and sorting indices for every new element
|
||||
guard let i = dataSource.firstIndex(where: predicate) else {
|
||||
return nil
|
||||
}
|
||||
return (i, dataSource[i])
|
||||
}
|
||||
|
||||
/// Set new data source and re-built filter and display sorting order.
|
||||
/// - Note: Will call `filterPipelineDidReset()`
|
||||
func reset(dataSource: [T]) {
|
||||
self.dataSource = dataSource
|
||||
self.resetFilters()
|
||||
delegate?.filterPipelineDidReset()
|
||||
}
|
||||
|
||||
/// Returns the index set of either the last filter layer, or `dataSource` if no filter is set.
|
||||
fileprivate func lastLayerIndices() -> [Int] {
|
||||
pipeline.last?.selection ?? dataSource.indices.arr()
|
||||
}
|
||||
|
||||
/// Get pipeline index of filter with given identifier
|
||||
private func indexOfFilter(_ identifier: String) -> Int? {
|
||||
pipeline.firstIndex(where: {$0.id == identifier})
|
||||
}
|
||||
|
||||
|
||||
// MARK: manage pipeline
|
||||
|
||||
/// Add new filter layer. Each layer is applied upon the previous layer. Therefore, each filter
|
||||
/// can only restrict the display further. A filter cannot introduce previously removed elements.
|
||||
/// - Warning: Use `[unowned self]` to prevent retain cycles!
|
||||
/// - Note: Will call `filterPipelineDidReset()`
|
||||
/// - Parameters:
|
||||
/// - identifier: Use this id to find the filter again. For reload and remove operations.
|
||||
/// - otherId: If `nil` or non-existent the new filter will be appended at the end.
|
||||
/// - predicate: Return `true` if you want to keep the element.
|
||||
func addFilter(_ identifier: String, before otherId: String? = nil, _ predicate: @escaping PipelineFilter<T>.Predicate) {
|
||||
guard indexOfFilter(identifier) == nil else { return }
|
||||
let newFilter = PipelineFilter(identifier, predicate)
|
||||
if let other = otherId, let i = indexOfFilter(other) {
|
||||
pipeline.insert(newFilter, at: i)
|
||||
resetFilters(startingAt: i)
|
||||
} else {
|
||||
newFilter.reset(to: dataSource, previous: lastLayerIndices())
|
||||
pipeline.append(newFilter)
|
||||
display?.apply(moreRestrictive: newFilter.selection)
|
||||
}
|
||||
delegate?.filterPipelineDidReset()
|
||||
}
|
||||
|
||||
/// Find and remove filter with given identifier. Will automatically update remaining filters and display sorting.
|
||||
/// - Note: Will call `filterPipelineDidReset()`
|
||||
func removeFilter(withId ident: String) {
|
||||
guard let i = indexOfFilter(ident) else { return }
|
||||
pipeline.remove(at: i)
|
||||
if i == pipeline.count {
|
||||
// only if we don't reset other layers we can assure `toLessRestrictive`
|
||||
display?.apply(lessRestrictive: lastLayerIndices())
|
||||
} else {
|
||||
resetFilters(startingAt: i)
|
||||
}
|
||||
delegate?.filterPipelineDidReset()
|
||||
}
|
||||
|
||||
/// Start filter evaluation on all entries from previous filter.
|
||||
/// - Note: Will call `filterPipelineDidReset()`
|
||||
func reloadFilter(withId ident: String) {
|
||||
guard let i = indexOfFilter(ident) else { return }
|
||||
resetFilters(startingAt: i)
|
||||
delegate?.filterPipelineDidReset()
|
||||
}
|
||||
|
||||
/// Sets the sort and display order. You should set the `delegate` to automatically update your `tableView`.
|
||||
/// - Warning: Use `[unowned self]` to prevent retain cycles!
|
||||
/// - Note: Will call `filterPipelineDidReset()`
|
||||
/// - Parameter predicate: Return `true` if first element should be sorted before second element.
|
||||
func setSorting(_ predicate: @escaping PipelineSorting<T>.Predicate) {
|
||||
display = .init(predicate, pipe: self)
|
||||
delegate?.filterPipelineDidReset()
|
||||
}
|
||||
|
||||
/// Will reverse the current display order without resorting. This is faster than setting a new sorting `predicate`.
|
||||
/// However, the `predicate` must be dynamic and support a sort order flag.
|
||||
/// - Note: Will call `filterPipelineDidReset()`
|
||||
/// - Warning: Make sure `predicate` does reflect the change or it will lead to data inconsistency!
|
||||
func reverseSorting() {
|
||||
// TODO: use semaphore to prevent concurrent edits
|
||||
display?.reverseOrder()
|
||||
delegate?.filterPipelineDidReset()
|
||||
}
|
||||
|
||||
/// Re-built filter and display sorting order.
|
||||
/// - Parameter index: Must be: `index <= pipeline.count`
|
||||
private func resetFilters(startingAt index: Int = 0) {
|
||||
for i in index..<pipeline.count {
|
||||
pipeline[i].reset(to: dataSource, previous: (i>0)
|
||||
? pipeline[i-1].selection : dataSource.indices.arr())
|
||||
}
|
||||
// Reset is NOT less-restrictive because filters are dynamic
|
||||
// Calling reset on a filter twice may yield different results
|
||||
// E.g. if filter uses variables outside of scope (current time, search term)
|
||||
display?.reset(to: lastLayerIndices())
|
||||
}
|
||||
|
||||
/// Push object through filter pipeline to check whether it survives all filters.
|
||||
/// - Parameter index: The index of the object in the original `dataSource`
|
||||
/// - Returns: `changed` is `true` if element persists or should be removed with this update.
|
||||
/// `display` indicates whther element should be shown (`true`) or hidden (`false`).
|
||||
/// - Complexity: O(*m* log *n*), where *m* is the number of filters and *n* the number of elements in each filter.
|
||||
private func processPipeline(with obj: T, at index: Int) -> (changed: Bool, display: Bool) {
|
||||
var keepGoing = true
|
||||
for filter in pipeline {
|
||||
let lastIndex: Int?
|
||||
if keepGoing {
|
||||
(keepGoing, lastIndex) = filter.update(obj, at: index)
|
||||
} else {
|
||||
lastIndex = filter.remove(dataSource: index)
|
||||
}
|
||||
// if it isnt in this layer, it wont appear in the following either
|
||||
if lastIndex == nil { return (false, false) }
|
||||
}
|
||||
return (true, keepGoing)
|
||||
}
|
||||
|
||||
|
||||
// MARK: data updates
|
||||
|
||||
/// Add new element to the original `dataSource` and immediately apply filter and sorting.
|
||||
/// - Note: Will call `filterPipeline(insert:)` if not filtered.
|
||||
/// - Complexity: O((*m*+1) log *n*), where *m* is the number of filters and *n* the number of elements in each filter.
|
||||
func addNew(_ obj: T) {
|
||||
let index = dataSource.count
|
||||
dataSource.append(obj)
|
||||
for filter in pipeline {
|
||||
if filter.add(obj, at: index) == nil { return }
|
||||
}
|
||||
// survived all filters
|
||||
let displayIndex = display.insertNew(index)
|
||||
delegate?.filterPipeline(insert: displayIndex)
|
||||
}
|
||||
|
||||
/// Update element at `index` in the original `dataSource` and immediately re-apply filter and sorting.
|
||||
/// - Note: Will call `filterPipeline(delete:)`, `(insert:)`, `(update:)`, or `(move:)`
|
||||
/// - Parameters:
|
||||
/// - obj: Element to be added. Will overwrite previous `dataSource` object.
|
||||
/// - index: Index in the original `dataSource`
|
||||
/// - Complexity: O(*n* + (*m*+1) log *n*), where *m* is the number of filters and *n* the number of elements in each filter / projection.
|
||||
func update(_ obj: T, at index: Int) {
|
||||
let status = processPipeline(with: obj, at: index)
|
||||
guard status.changed else {
|
||||
dataSource[index] = obj // we need to update anyway
|
||||
return
|
||||
}
|
||||
let oldPos = display.deleteOld(index)
|
||||
dataSource[index] = obj
|
||||
guard status.display else {
|
||||
if oldPos != -1 { delegate?.filterPipeline(delete: [oldPos]) }
|
||||
return
|
||||
}
|
||||
let newPos = display.insertNew(index, previousIndex: oldPos)
|
||||
if oldPos == -1 {
|
||||
delegate?.filterPipeline(insert: newPos)
|
||||
} else {
|
||||
if oldPos == newPos {
|
||||
delegate?.filterPipeline(update: oldPos)
|
||||
} else {
|
||||
delegate?.filterPipeline(move: oldPos, to: newPos)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Remove elements from the original `dataSource`, from all filters, and from display sorting.
|
||||
/// - Note: Will call `filterPipeline(delete:)` if `sorted` array is not empty.
|
||||
/// - Parameter sorted: Indices in the original `dataSource`
|
||||
/// - Complexity: O(*t*(*m*+*n*) + *m* log *n*), where *t* is the number of filters,
|
||||
/// *m* the number of elements in each filter / projection, and *n* the length of `sorted` indices.
|
||||
func remove(indices sorted: [Int]) {
|
||||
guard sorted.count > 0 else { return }
|
||||
for i in sorted.reversed() {
|
||||
dataSource.remove(at: i)
|
||||
}
|
||||
for filter in pipeline {
|
||||
filter.shiftRemove(indices: sorted)
|
||||
}
|
||||
let indices = display.shiftRemove(indices: sorted)
|
||||
delegate?.filterPipeline(delete: indices)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// MARK: - Filter
|
||||
|
||||
class PipelineFilter<T>: CustomStringConvertible {
|
||||
var description: String { "\(Self.self)(id: \(id))" }
|
||||
|
||||
typealias Predicate = (T) -> Bool
|
||||
|
||||
let id: String
|
||||
private(set) var selection: [Int] = []
|
||||
private let shouldPersist: Predicate
|
||||
|
||||
/// - Parameter predicate: Return `true` if you want to keep the element
|
||||
required init(_ identifier: String, _ predicate: @escaping Predicate) {
|
||||
self.id = identifier
|
||||
shouldPersist = predicate
|
||||
}
|
||||
|
||||
/// Reset `selection` by copying the indices and applying the filter function
|
||||
fileprivate func reset(to dataSource: [T], previous filterIndices: [Int]) {
|
||||
selection = filterIndices
|
||||
selection.removeAll { !shouldPersist(dataSource[$0]) }
|
||||
}
|
||||
|
||||
/// Apply filter to `obj` and either insert or do nothing.
|
||||
/// - Parameters:
|
||||
/// - obj: Object that should be inserted if filter allows.
|
||||
/// - index: Index of object in original `dataSource`
|
||||
/// - Returns: Index in `selection` or `nil` if `obj` is removed by the filter.
|
||||
/// - Complexity:
|
||||
/// * O(1), if `index` is appended at end.
|
||||
/// * O(log *n*), where *n* is the length of the `selection`.
|
||||
fileprivate func add(_ obj: T, at index: Int) -> Int? {
|
||||
guard shouldPersist(obj) else {
|
||||
return nil
|
||||
}
|
||||
if selection.last ?? 0 < index { // in case we only append at end
|
||||
selection.append(index)
|
||||
return selection.count - 1
|
||||
}
|
||||
return selection.binTreeInsert(index, compare: (<))
|
||||
}
|
||||
|
||||
/// Search and remove original `dataSource` index
|
||||
/// - Parameter index: Index of object in original `dataSource`
|
||||
/// - Returns: Index of removed element in `selection` or `nil` if element does not exist
|
||||
/// - Complexity: O(log *n*), where *n* is the length of the `selection`.
|
||||
fileprivate func remove(dataSource index: Int) -> Int? {
|
||||
selection.binTreeRemove(index, compare: (<))
|
||||
}
|
||||
|
||||
/// Perform filter check and update internal `selection` indices.
|
||||
/// - Parameters:
|
||||
/// - obj: Object that was inserted or updated.
|
||||
/// - index: Index where the object is located after the update.
|
||||
/// - Returns: `keep` indicates whether the value should be displayed (`true`) or hidden (`false`).
|
||||
/// `idx` contains the selection filter index or `nil` if the value should be removed.
|
||||
/// - Complexity: O(log *n*), where *n* is the length of the `selection`.
|
||||
fileprivate func update(_ obj: T, at index: Int) -> (keep: Bool, idx: Int?) {
|
||||
let currentIndex = selection.binTreeIndex(of: index, compare: (<), mustExist: true)
|
||||
if shouldPersist(obj) {
|
||||
return (true, currentIndex ?? selection.binTreeInsert(index, compare: (<)))
|
||||
}
|
||||
if let i = currentIndex { selection.remove(at: i) }
|
||||
return (false, currentIndex)
|
||||
}
|
||||
|
||||
/// Instead of re-sorting we can decrement all remaining elements after X.
|
||||
/// - Parameter sorted: Elements to remove from collection
|
||||
/// - Complexity: O(*m*+*n*), where *m* is the length of the `selection`.
|
||||
/// *n* is equal to: *length of selection* `-` *index of first element* of `sorted` indices
|
||||
fileprivate func shiftRemove(indices sorted: [Int]) {
|
||||
guard sorted.count > 0 else {
|
||||
return
|
||||
}
|
||||
var list = sorted
|
||||
var del = list.popLast()
|
||||
for (i, val) in selection.enumerated().reversed() {
|
||||
while let d = del, d > val {
|
||||
del = list.popLast()
|
||||
}
|
||||
guard let d = del else { break }
|
||||
if d < val { selection[i] -= (list.count + 1) }
|
||||
else if d == val { selection.remove(at: i) }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// MARK: - Sorting
|
||||
|
||||
class PipelineSorting<T> {
|
||||
typealias Predicate = (T, T) -> Bool
|
||||
|
||||
private(set) var projection: [Int] = []
|
||||
private let comperator: (Int, Int) -> Bool // links to pipeline.dataSource
|
||||
|
||||
/// Create a fresh, already sorted, display order projection.
|
||||
/// - Parameter predicate: Return `true` if first element should be sorted before second element.
|
||||
/// - Complexity: O(*n* log *n*), where *n* is the length of the `filter`.
|
||||
required init(_ predicate: @escaping Predicate, pipe: FilterPipeline<T>) {
|
||||
comperator = { [unowned pipe] in
|
||||
predicate(pipe.dataSource[$0], pipe.dataSource[$1])
|
||||
}
|
||||
reset(to: pipe.lastLayerIndices())
|
||||
}
|
||||
|
||||
/// - Warning: Make sure `predicate` does reflect the change. Or it will lead to data inconsistency.
|
||||
/// - Complexity: O(*n*), where *n* is the length of the `filter`.
|
||||
fileprivate func reverseOrder() {
|
||||
projection.reverse()
|
||||
}
|
||||
|
||||
/// Replace current `projection` with new filter indices and apply sorting.
|
||||
/// - Complexity: O(*n* log *n*), where *n* is the length of the `filter`.
|
||||
fileprivate func reset(to filterIndices: [Int]) {
|
||||
projection = filterIndices.sorted(by: comperator)
|
||||
}
|
||||
|
||||
/// After adding a new layer of filtering the new layer can only restrict the display even further.
|
||||
/// Therefore, indices that were removed in the last layer will be removed from the projection too.
|
||||
/// - Complexity: O(*m* log *n*), where *m* is the length of the `projection` and *n* the length of the `filter`.
|
||||
fileprivate func apply(moreRestrictive filterIndices: [Int]) {
|
||||
projection.removeAll { !filterIndices.binTreeExists($0, compare: (<)) }
|
||||
}
|
||||
|
||||
/// After removing a layer of filtering the previous layers are less restrictive and thus contain more indices.
|
||||
/// Therefore, the difference between both index sets will be inserted into the projection.
|
||||
/// - Complexity: O(*m* log *n*), where *m* is the difference to the previous layer and *n* is the length of the `projection`.
|
||||
fileprivate func apply(lessRestrictive filterIndices: [Int]) {
|
||||
for x in filterIndices.difference(toSubset: projection.sorted(), compare: (<)) {
|
||||
insertNew(x)
|
||||
}
|
||||
}
|
||||
|
||||
/// Add new element and automatically sort according to predicate
|
||||
/// - Parameters:
|
||||
/// - index: Index of the element position in the original `dataSource`
|
||||
/// - prev: If greater than `0`, try re-insert at the same position.
|
||||
/// - Returns: Index in the projection
|
||||
/// - Complexity: O(log *n*), where *n* is the length of the `projection`.
|
||||
@discardableResult fileprivate func insertNew(_ index: Int, previousIndex prev: Int = -1) -> Int {
|
||||
if prev >= 0, prev <= projection.count { // '<=' because previous delete removed one element
|
||||
if (prev == 0 || !comperator(index, projection[prev - 1])),
|
||||
(prev == projection.count || !comperator(projection[prev], index)) {
|
||||
// If element can be inserted at the same position without resorting, do that
|
||||
projection.insert(index, at: prev)
|
||||
return prev
|
||||
}
|
||||
}
|
||||
return projection.binTreeInsert(index, compare: comperator)
|
||||
}
|
||||
|
||||
/// Remove element from projection
|
||||
/// - Parameter index: Index of the element position in the original `dataSource`
|
||||
/// - Returns: Index in the projection or `-1` if element did not exist
|
||||
/// - Complexity: O(*n*), where *n* is the length of the `projection`.
|
||||
fileprivate func deleteOld(_ index: Int) -> Int {
|
||||
guard let i = projection.firstIndex(of: index) else {
|
||||
return -1
|
||||
}
|
||||
projection.remove(at: i)
|
||||
return i
|
||||
}
|
||||
|
||||
/// Instead of re-sorting we can decrement all remaining elements after X.
|
||||
/// - Parameter sorted: Elements to remove from collection
|
||||
/// - Returns: List of `projection` indices that were removed (reverse sort order)
|
||||
/// - Complexity: O(*m* log *n*), where *m* is the length of the `projection` and *n* is the length of `sorted`.
|
||||
@discardableResult fileprivate func shiftRemove(indices sorted: [Int]) -> [Int] {
|
||||
guard sorted.count > 0 else {
|
||||
return []
|
||||
}
|
||||
var listOfDeletes: [Int] = []
|
||||
let min = sorted.first!, max = sorted.last!
|
||||
for (i, val) in projection.enumerated().reversed() {
|
||||
guard val >= min else { continue }
|
||||
if val > max {
|
||||
projection[i] -= sorted.count
|
||||
} else {
|
||||
let c = sorted.binTreeIndex(of: val, compare: (<))!
|
||||
if val == sorted[c] {
|
||||
projection.remove(at: i)
|
||||
listOfDeletes.append(i)
|
||||
} else {
|
||||
projection[i] -= c
|
||||
}
|
||||
}
|
||||
}
|
||||
return listOfDeletes
|
||||
}
|
||||
}
|
||||
83
main/Common Classes/IBViews.swift
Normal file
@@ -0,0 +1,83 @@
|
||||
import UIKit
|
||||
import CoreGraphics
|
||||
|
||||
// MARK: White Triangle Popup Arrow
|
||||
|
||||
@IBDesignable
|
||||
class PopupTriangle: UIView {
|
||||
@IBInspectable var rotation: CGFloat = 0
|
||||
@IBInspectable var color: UIColor = .black
|
||||
|
||||
override func draw(_ rect: CGRect) {
|
||||
guard let c = UIGraphicsGetCurrentContext() else { return }
|
||||
let w = rect.width, h = rect.height
|
||||
switch rotation {
|
||||
case 90: // right
|
||||
c.lineFromTo(x1: 0, y1: 0, x2: w, y2: h/2)
|
||||
c.addLine(to: CGPoint(x: 0, y: h))
|
||||
case 180: // bottom
|
||||
c.lineFromTo(x1: w, y1: 0, x2: w/2, y2: h)
|
||||
c.addLine(to: CGPoint(x: 0, y: 0))
|
||||
case 270: // left
|
||||
c.lineFromTo(x1: w, y1: h, x2: 0, y2: h/2)
|
||||
c.addLine(to: CGPoint(x: w, y: 0))
|
||||
default: // top
|
||||
c.lineFromTo(x1: 0, y1: h, x2: w/2, y2: 0)
|
||||
c.addLine(to: CGPoint(x: w, y: h))
|
||||
}
|
||||
c.closePath()
|
||||
c.setFillColor(color.cgColor)
|
||||
c.fillPath()
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// MARK: Label as Tag Bubble
|
||||
|
||||
@IBDesignable
|
||||
class TagLabel: UILabel {
|
||||
private var em: CGFloat { font.pointSize }
|
||||
@IBInspectable var padTop: CGFloat = 0
|
||||
@IBInspectable var padLeft: CGFloat = 0
|
||||
@IBInspectable var padRight: CGFloat = 0
|
||||
@IBInspectable var padBottom: CGFloat = 0
|
||||
private var padding: UIEdgeInsets {
|
||||
.init(top: padTop + em/6, left: padLeft + em/3,
|
||||
bottom: padBottom + em/6, right: padRight + em/3)
|
||||
}
|
||||
|
||||
override func textRect(forBounds bounds: CGRect, limitedToNumberOfLines numberOfLines: Int) -> CGRect {
|
||||
let i = padding
|
||||
let ii = UIEdgeInsets(top: -i.top, left: -i.left, bottom: -i.bottom, right: -i.right)
|
||||
return super.textRect(forBounds: bounds.inset(by: i),
|
||||
limitedToNumberOfLines: numberOfLines).inset(by: ii)
|
||||
}
|
||||
|
||||
override func drawText(in rect: CGRect) {
|
||||
layer.masksToBounds = true
|
||||
layer.cornerRadius = em/2.5
|
||||
super.drawText(in: rect.inset(by: padding))
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// MARK: Percentage meter
|
||||
|
||||
@IBDesignable
|
||||
class MeterBar: UIView {
|
||||
@IBInspectable var percent: CGFloat = 0 { didSet { setNeedsDisplay() } }
|
||||
@IBInspectable var barColor: UIColor = .sysLink
|
||||
@IBInspectable var horizontal: Bool = false
|
||||
|
||||
private var normPercent: CGFloat { 1 - max(0, min(percent, 1)) }
|
||||
|
||||
override func draw(_ rect: CGRect) {
|
||||
let c = UIGraphicsGetCurrentContext()
|
||||
c?.setFillColor(barColor.cgColor)
|
||||
if horizontal {
|
||||
c?.fill(rect.insetBy(dx: normPercent * (rect.width/2), dy: 0))
|
||||
} else {
|
||||
c?.fill(rect.insetBy(dx: 0, dy: normPercent * (rect.height/2)))
|
||||
}
|
||||
}
|
||||
}
|
||||
69
main/Common Classes/NotificationBanner.swift
Normal file
@@ -0,0 +1,69 @@
|
||||
import UIKit
|
||||
|
||||
struct NotificationBanner {
|
||||
enum Style {
|
||||
case fail, ok
|
||||
}
|
||||
|
||||
let view: UIView
|
||||
|
||||
init(_ msg: String, style: Style) {
|
||||
let bg, fg: UIColor
|
||||
let imgName: String
|
||||
switch style {
|
||||
case .fail:
|
||||
bg = .systemRed
|
||||
fg = UIColor.black.withAlphaComponent(0.80)
|
||||
imgName = "circle-x"
|
||||
case .ok:
|
||||
bg = .systemGreen
|
||||
fg = UIColor.black.withAlphaComponent(0.65)
|
||||
imgName = "circle-check"
|
||||
}
|
||||
view = UIView()
|
||||
view.backgroundColor = bg
|
||||
let lbl = QuickUI.label(msg, style: .callout)
|
||||
lbl.textColor = fg
|
||||
lbl.numberOfLines = 0
|
||||
lbl.font = lbl.font.bold()
|
||||
let img = QuickUI.image(UIImage(named: imgName))
|
||||
img.tintColor = fg
|
||||
view.addSubview(lbl)
|
||||
view.addSubview(img)
|
||||
img.anchor([.centerY], to: view.layoutMarginsGuide)
|
||||
lbl.anchor([.top, .bottom, .trailing], to: view.layoutMarginsGuide)
|
||||
img.widthAnchor =&= 25
|
||||
img.heightAnchor =&= 25
|
||||
if #available(iOS 11, *) {
|
||||
img.leadingAnchor =&= view.layoutMarginsGuide.leadingAnchor
|
||||
} else {
|
||||
img.leadingAnchor =&= view.leadingAnchor + 8
|
||||
}
|
||||
lbl.leadingAnchor =&= img.trailingAnchor + 8
|
||||
img.bottomAnchor =<= view.bottomAnchor - 8 | .init(rawValue: 999)
|
||||
lbl.bottomAnchor =<= view.bottomAnchor - 8 | .init(rawValue: 999)
|
||||
}
|
||||
|
||||
/// Animate header banner from the top of the view. Show for `delay` seconds and then hide again.
|
||||
/// - Parameter onClose: Run after the close animation finishes.
|
||||
func present(in vc: UIViewController, hideAfter delay: TimeInterval = 3, onClose: (() -> Void)? = nil) {
|
||||
vc.view.addSubview(view)
|
||||
view.anchor([.leading, .trailing], to: vc.view!)
|
||||
view.widthAnchor =&= vc.view!.widthAnchor // Bug? left-right is not sufficient
|
||||
vc.view.layoutIfNeeded() // sets the height
|
||||
let h = view.frame.height
|
||||
let constraint = view.topAnchor =&= vc.view.topAnchor - h
|
||||
vc.view.layoutIfNeeded() // hide view
|
||||
UIView.animate(withDuration: 0.3, animations: {
|
||||
constraint.constant = 0
|
||||
vc.view.layoutIfNeeded() // animate view
|
||||
UIView.animate(withDuration: 0.3, delay: delay, options: .curveLinear, animations: {
|
||||
constraint.constant = -h
|
||||
vc.view.layoutIfNeeded() // hide again
|
||||
}, completion: { _ in
|
||||
self.view.removeFromSuperview()
|
||||
onClose?()
|
||||
})
|
||||
})
|
||||
}
|
||||
}
|
||||
126
main/Common Classes/Prefs.swift
Normal file
@@ -0,0 +1,126 @@
|
||||
import Foundation
|
||||
|
||||
enum Prefs {
|
||||
private static var suite: UserDefaults { UserDefaults.standard }
|
||||
|
||||
private static func Int(_ key: String) -> Int { suite.integer(forKey: key) }
|
||||
private static func Int(_ key: String, _ val: Int) { suite.set(val, forKey: key) }
|
||||
private static func Bool(_ key: String) -> Bool { suite.bool(forKey: key) }
|
||||
private static func Bool(_ key: String, _ val: Bool) { suite.set(val, forKey: key) }
|
||||
private static func Str(_ key: String) -> String? { suite.string(forKey: key) }
|
||||
private static func Str(_ key: String, _ val: String?) { suite.set(val, forKey: key) }
|
||||
private static func Obj(_ key: String) -> Any? { suite.object(forKey: key) }
|
||||
private static func Obj(_ key: String, _ val: Any?) { suite.set(val, forKey: key) }
|
||||
|
||||
static func registerDefaults() {
|
||||
suite.register(defaults: [
|
||||
"RecordingReminderEnabled" : true,
|
||||
"contextAnalyisCoOccurrenceTime" : 5,
|
||||
])
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// MARK: - Tutorial
|
||||
|
||||
extension Prefs {
|
||||
enum DidShowTutorial {
|
||||
static var Welcome: Bool {
|
||||
get { Prefs.Bool("didShowTutorialAppWelcome") }
|
||||
set { Prefs.Bool("didShowTutorialAppWelcome", newValue) }
|
||||
}
|
||||
static var Recordings: Bool {
|
||||
get { Prefs.Bool("didShowTutorialRecordings") }
|
||||
set { Prefs.Bool("didShowTutorialRecordings", newValue) }
|
||||
}
|
||||
static var RecordingHowTo: Bool {
|
||||
get { Prefs.Bool("didShowTutorialRecordingHowTo") }
|
||||
set { Prefs.Bool("didShowTutorialRecordingHowTo", newValue) }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// MARK: - Date Filter
|
||||
|
||||
enum DateFilterKind: Int {
|
||||
case Off = 0, LastXMin = 1, ABRange = 2;
|
||||
}
|
||||
enum DateFilterOrderBy: Int {
|
||||
case Date = 0, Name = 1, Count = 2;
|
||||
}
|
||||
|
||||
extension Prefs {
|
||||
enum DateFilter {
|
||||
static var Kind: DateFilterKind {
|
||||
get { DateFilterKind(rawValue: Prefs.Int("dateFilterType"))! }
|
||||
set { Prefs.Int("dateFilterType", newValue.rawValue) }
|
||||
}
|
||||
/// Default: `0` (disabled)
|
||||
static var LastXMin: Int {
|
||||
get { Prefs.Int("dateFilterLastXMin") }
|
||||
set { Prefs.Int("dateFilterLastXMin", newValue) }
|
||||
}
|
||||
/// Default: `nil` (disabled)
|
||||
static var RangeA: Timestamp? {
|
||||
get { Prefs.Obj("dateFilterRangeA") as? Timestamp }
|
||||
set { Prefs.Obj("dateFilterRangeA", newValue) }
|
||||
}
|
||||
/// Default: `nil` (disabled)
|
||||
static var RangeB: Timestamp? {
|
||||
get { Prefs.Obj("dateFilterRangeB") as? Timestamp }
|
||||
set { Prefs.Obj("dateFilterRangeB", newValue) }
|
||||
}
|
||||
/// default: `.Date`
|
||||
static var OrderBy: DateFilterOrderBy {
|
||||
get { DateFilterOrderBy(rawValue: Prefs.Int("dateFilterOderType"))! }
|
||||
set { Prefs.Int("dateFilterOderType", newValue.rawValue) }
|
||||
}
|
||||
/// default: `false` (Desc)
|
||||
static var OrderAsc: Bool {
|
||||
get { Prefs.Bool("dateFilterOderAsc") }
|
||||
set { Prefs.Bool("dateFilterOderAsc", newValue) }
|
||||
}
|
||||
|
||||
/// - Returns: Timestamp restriction depending on current selected date filter.
|
||||
/// - `Off` : `(nil, nil)`
|
||||
/// - `LastXMin` : `(now-LastXMin, nil)`
|
||||
/// - `ABRange` : `(RangeA, RangeB)`
|
||||
static func restrictions() -> (type: DateFilterKind, earliest: Timestamp?, latest: Timestamp?) {
|
||||
let type = Kind
|
||||
switch type {
|
||||
case .Off: return (type, nil, nil)
|
||||
case .LastXMin: return (type, Timestamp.past(minutes: Prefs.DateFilter.LastXMin), nil)
|
||||
case .ABRange: return (type, Prefs.DateFilter.RangeA, Prefs.DateFilter.RangeB)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// MARK: - ContextAnalyis
|
||||
|
||||
extension Prefs {
|
||||
enum ContextAnalyis {
|
||||
static var CoOccurrenceTime: Int {
|
||||
get { Prefs.Int("contextAnalyisCoOccurrenceTime") }
|
||||
set { Prefs.Int("contextAnalyisCoOccurrenceTime", newValue) }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// MARK: - Notifications
|
||||
|
||||
extension Prefs {
|
||||
enum RecordingReminder {
|
||||
static var Enabled: Bool {
|
||||
get { Prefs.Bool("RecordingReminderEnabled") }
|
||||
set { Prefs.Bool("RecordingReminderEnabled", newValue) }
|
||||
}
|
||||
static var Sound: String {
|
||||
get { Prefs.Str("RecordingReminderSound") ?? "#default" }
|
||||
set { Prefs.Str("RecordingReminderSound", newValue) }
|
||||
}
|
||||
}
|
||||
}
|
||||
96
main/Common Classes/PrefsShared.swift
Normal file
@@ -0,0 +1,96 @@
|
||||
import Foundation
|
||||
|
||||
enum PrefsShared {
|
||||
private static var suite: UserDefaults { UserDefaults(suiteName: "group.de.uni-bamberg.psi.AppCheck")! }
|
||||
|
||||
private static func Int(_ key: String) -> Int { suite.integer(forKey: key) }
|
||||
private static func Int(_ key: String, _ val: Int) { suite.set(val, forKey: key); suite.synchronize() }
|
||||
private static func Bool(_ key: String) -> Bool { suite.bool(forKey: key) }
|
||||
private static func Bool(_ key: String, _ val: Bool) { suite.set(val, forKey: key); suite.synchronize() }
|
||||
private static func Str(_ key: String) -> String? { suite.string(forKey: key) }
|
||||
private static func Str(_ key: String, _ val: String?) { suite.set(val, forKey: key); suite.synchronize() }
|
||||
|
||||
static func registerDefaults() {
|
||||
suite.register(defaults: [
|
||||
"RestartReminderEnabled" : true,
|
||||
"RestartReminderWithText" : true,
|
||||
"RestartReminderWithBadge" : true,
|
||||
"ConnectionAlertsListsElse" : true,
|
||||
])
|
||||
}
|
||||
|
||||
static var AutoDeleteLogsDays: Int {
|
||||
get { Int("AutoDeleteLogsDays") }
|
||||
set { Int("AutoDeleteLogsDays", newValue) }
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// MARK: - Recording State
|
||||
|
||||
enum CurrentRecordingState : Int {
|
||||
case Off = 0, App = 1, Background = 2
|
||||
}
|
||||
|
||||
extension PrefsShared {
|
||||
static var CurrentlyRecording: CurrentRecordingState {
|
||||
get { CurrentRecordingState(rawValue: Int("CurrentlyRecording")) ?? .Off }
|
||||
set { Int("CurrentlyRecording", newValue.rawValue) }
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// MARK: - Notifications
|
||||
|
||||
extension PrefsShared {
|
||||
enum RestartReminder {
|
||||
static var Enabled: Bool {
|
||||
get { PrefsShared.Bool("RestartReminderEnabled") }
|
||||
set { PrefsShared.Bool("RestartReminderEnabled", newValue) }
|
||||
}
|
||||
static var WithText: Bool {
|
||||
get { PrefsShared.Bool("RestartReminderWithText") }
|
||||
set { PrefsShared.Bool("RestartReminderWithText", newValue) }
|
||||
}
|
||||
static var WithBadge: Bool {
|
||||
get { PrefsShared.Bool("RestartReminderWithBadge") }
|
||||
set { PrefsShared.Bool("RestartReminderWithBadge", newValue) }
|
||||
}
|
||||
static var Sound: String {
|
||||
get { PrefsShared.Str("RestartReminderSound") ?? "#default" }
|
||||
set { PrefsShared.Str("RestartReminderSound", newValue) }
|
||||
}
|
||||
}
|
||||
enum ConnectionAlerts {
|
||||
static var Enabled: Bool {
|
||||
get { PrefsShared.Bool("ConnectionAlertsEnabled") }
|
||||
set { PrefsShared.Bool("ConnectionAlertsEnabled", newValue) }
|
||||
}
|
||||
static var Sound: String {
|
||||
get { PrefsShared.Str("ConnectionAlertsSound") ?? "#default" }
|
||||
set { PrefsShared.Str("ConnectionAlertsSound", newValue) }
|
||||
}
|
||||
static var ExcludeMode: Bool {
|
||||
get { PrefsShared.Bool("ConnectionAlertsExcludeMode") }
|
||||
set { PrefsShared.Bool("ConnectionAlertsExcludeMode", newValue) }
|
||||
}
|
||||
enum Lists {
|
||||
static var CustomA: Bool {
|
||||
get { PrefsShared.Bool("ConnectionAlertsListsCustomA") }
|
||||
set { PrefsShared.Bool("ConnectionAlertsListsCustomA", newValue) }
|
||||
}
|
||||
static var CustomB: Bool {
|
||||
get { PrefsShared.Bool("ConnectionAlertsListsCustomB") }
|
||||
set { PrefsShared.Bool("ConnectionAlertsListsCustomB", newValue) }
|
||||
}
|
||||
static var Blocked: Bool {
|
||||
get { PrefsShared.Bool("ConnectionAlertsListsBlocked") }
|
||||
set { PrefsShared.Bool("ConnectionAlertsListsBlocked", newValue) }
|
||||
}
|
||||
static var Else: Bool {
|
||||
get { PrefsShared.Bool("ConnectionAlertsListsElse") }
|
||||
set { PrefsShared.Bool("ConnectionAlertsListsElse", newValue) }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -2,10 +2,25 @@ import UIKit
|
||||
|
||||
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 {
|
||||
let x = UIButton(type: .roundedRect)
|
||||
x.setTitle(title, for: .normal)
|
||||
x.titleLabel?.font = .preferredFont(forTextStyle: .body)
|
||||
x.constrainHuggingCompression(.vertical, .defaultHigh)
|
||||
x.sizeToFit()
|
||||
if let a = action { x.addTarget(target, action: a, for: .touchUpInside) }
|
||||
if #available(iOS 10.0, *) {
|
||||
@@ -36,35 +51,8 @@ struct QuickUI {
|
||||
static func text(attributed: NSAttributedString, frame: CGRect = CGRect.zero) -> UITextView {
|
||||
let txt = self.text("", frame: frame)
|
||||
txt.attributedText = attributed
|
||||
txt.textContainerInset = .zero
|
||||
//txt.textContainer.lineFragmentPadding = 0 // remove left right padding
|
||||
return txt
|
||||
}
|
||||
}
|
||||
|
||||
extension NSMutableAttributedString {
|
||||
static private var def: UIFont = .preferredFont(forTextStyle: .body)
|
||||
|
||||
func normal(_ str: String, _ style: UIFont.TextStyle = .body) -> Self { append(str, withFont: .preferredFont(forTextStyle: style)) }
|
||||
func bold(_ str: String, _ style: UIFont.TextStyle = .body) -> Self { append(str, withFont: UIFont.preferredFont(forTextStyle: style).bold()) }
|
||||
func italic(_ str: String, _ style: UIFont.TextStyle = .body) -> Self { append(str, withFont: UIFont.preferredFont(forTextStyle: style).italic()) }
|
||||
|
||||
func h1(_ str: String) -> Self { normal(str, .title1) }
|
||||
func h2(_ str: String) -> Self { normal(str, .title2) }
|
||||
func h3(_ str: String) -> Self { normal(str, .title3) }
|
||||
|
||||
private func append(_ str: String, withFont: UIFont) -> Self {
|
||||
append(NSAttributedString(string: str, attributes: [
|
||||
.font : withFont,
|
||||
.foregroundColor : UIColor.sysFg
|
||||
]))
|
||||
return self
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
extension UIFont {
|
||||
func withTraits(traits: UIFontDescriptor.SymbolicTraits) -> UIFont {
|
||||
UIFont(descriptor: fontDescriptor.withSymbolicTraits(traits)!, size: 0) // keep size as is
|
||||
}
|
||||
func bold() -> UIFont { withTraits(traits: .traitBold) }
|
||||
func italic() -> UIFont { withTraits(traits: .traitItalic) }
|
||||
}
|
||||
|
||||
64
main/Common Classes/SearchBarManager.swift
Normal file
@@ -0,0 +1,64 @@
|
||||
import UIKit
|
||||
|
||||
class SearchBarManager: NSObject, UISearchResultsUpdating {
|
||||
|
||||
private(set) var isActive = false
|
||||
private(set) var term = ""
|
||||
private lazy var controller: UISearchController = {
|
||||
let x = UISearchController(searchResultsController: nil)
|
||||
x.searchBar.autocapitalizationType = .none
|
||||
x.searchBar.autocorrectionType = .no
|
||||
x.obscuresBackgroundDuringPresentation = false
|
||||
x.searchResultsUpdater = self
|
||||
return x
|
||||
}()
|
||||
private weak var tvc: UITableViewController?
|
||||
private let onChangeCallback: (String) -> Void
|
||||
|
||||
/// Prepare `UISearchBar` for user input
|
||||
/// - Parameter onChange: Code that will be executed every time the user changes the text (with 0.2s delay)
|
||||
required init(onChange: @escaping (String) -> Void) {
|
||||
onChangeCallback = onChange
|
||||
super.init()
|
||||
|
||||
UITextField.appearance(whenContainedInInstancesOf: [UISearchBar.self])
|
||||
.defaultTextAttributes = [.font: UIFont.preferredFont(forTextStyle: .body)]
|
||||
}
|
||||
|
||||
/// Assigns the `UISearchBar` to `tableView.tableHeaderView` (iOS 9) or `navigationItem.searchController` (iOS 11).
|
||||
func fuseWith(tableViewController: UITableViewController?) {
|
||||
guard tvc !== tableViewController else { return }
|
||||
tvc = tableViewController
|
||||
|
||||
if #available(iOS 11.0, *) {
|
||||
tvc?.navigationItem.searchController = controller
|
||||
} 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"
|
||||
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.tableHeaderView = controller.searchBar
|
||||
tvc?.tableView.setContentOffset(.init(x: 0, y: controller.searchBar.frame.height), animated: false)
|
||||
}
|
||||
}
|
||||
|
||||
/// Search callback
|
||||
internal func updateSearchResults(for controller: UISearchController) {
|
||||
NSObject.cancelPreviousPerformRequests(withTarget: self, selector: #selector(performSearch), object: nil)
|
||||
perform(#selector(performSearch), with: nil, afterDelay: 0.2)
|
||||
}
|
||||
|
||||
/// Internal callback function for delayed text evaluation.
|
||||
/// This way we can avoid unnecessary searches while user is typing.
|
||||
@objc private func performSearch() {
|
||||
term = controller.searchBar.text?.lowercased() ?? ""
|
||||
isActive = term.count > 0
|
||||
onChangeCallback(term)
|
||||
}
|
||||
}
|
||||
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)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
68
main/Common Classes/TinyMarkdown.swift
Normal file
@@ -0,0 +1,68 @@
|
||||
import UIKit
|
||||
|
||||
struct TinyMarkdown {
|
||||
/// Load markdown file and run through a (very) simple parser (see below).
|
||||
/// - Parameters:
|
||||
/// - filename: Will automatically append `.md` extension
|
||||
/// - replacements: Replace a single occurrence of search string with an attributed replacement.
|
||||
static func load(_ filename: String, replacements: [String : NSMutableAttributedString] = [:]) -> UITextView {
|
||||
let url = Bundle.main.url(forResource: filename, withExtension: "md")!
|
||||
let str = NSMutableAttributedString(withMarkdown: try! String(contentsOf: url))
|
||||
for (key, val) in replacements {
|
||||
guard let r = str.string.range(of: key) else {
|
||||
QLog.Debug("WARN: markdown key '\(key)' does not exist in \(filename)")
|
||||
continue
|
||||
}
|
||||
str.replaceCharacters(in: NSRange(r, in: str.string), with: val)
|
||||
}
|
||||
return QuickUI.text(attributed: str)
|
||||
}
|
||||
}
|
||||
|
||||
extension NSMutableAttributedString {
|
||||
/// Supports only: `#h1`, `##h2`, `###h3`, `_italic_`, `__bold__`, `___boldItalic___`
|
||||
convenience init(withMarkdown content: String) {
|
||||
self.init()
|
||||
let emph = try! NSRegularExpression(pattern: #"(?<=(^|\W))(_{1,3})(\S|\S.*?\S)\2"#, options: [])
|
||||
beginEditing()
|
||||
content.enumerateLines { (line, _) in
|
||||
if line.starts(with: "#") {
|
||||
var h = 0
|
||||
for char in line {
|
||||
if char == "#" { h += 1 }
|
||||
else { break }
|
||||
}
|
||||
var line = line
|
||||
line.removeFirst(h)
|
||||
line = line.trimmingCharacters(in: CharacterSet(charactersIn: " "))
|
||||
switch h {
|
||||
case 1: self.h1(line + "\n")
|
||||
case 2: self.h2(line + "\n")
|
||||
default: self.h3(line + "\n")
|
||||
}
|
||||
} else {
|
||||
let nsline = line as NSString
|
||||
let range = NSRange(location: 0, length: nsline.length)
|
||||
var i = 0
|
||||
for x in emph.matches(in: line, options: [], range: range) {
|
||||
let r = x.range
|
||||
self.normal(nsline.substring(from: i, to: r.location))
|
||||
i = r.upperBound
|
||||
let before = nsline.substring(with: r)
|
||||
let after = before.trimmingCharacters(in: CharacterSet(charactersIn: "_"))
|
||||
switch (before.count - after.count) / 2 {
|
||||
case 1: self.italic(after)
|
||||
case 2: self.bold(after)
|
||||
default: self.boldItalic(after)
|
||||
}
|
||||
}
|
||||
if i < range.length {
|
||||
self.normal(nsline.substring(from: i, to: range.length) + "\n")
|
||||
} else {
|
||||
self.normal("\n")
|
||||
}
|
||||
}
|
||||
}
|
||||
endEditing()
|
||||
}
|
||||
}
|
||||
@@ -1,11 +1,18 @@
|
||||
import UIKit
|
||||
|
||||
fileprivate let margin: CGFloat = 20
|
||||
fileprivate let cornerRadius: CGFloat = 15
|
||||
fileprivate let uniRect = CGRect(x: 0, y: 0, width: 500, height: 500)
|
||||
fileprivate var margin: CGFloat { 20 }
|
||||
fileprivate var sheetInset: CGFloat { cornerRadius/2 }
|
||||
fileprivate var cornerRadius: CGFloat { 15 }
|
||||
fileprivate var uniRect: CGRect { CGRect(x: 0, y: 0, width: 500, height: 500) }
|
||||
|
||||
class TutorialSheet: UIViewController, UIScrollViewDelegate {
|
||||
|
||||
/// Maximum displayable width of a Tutorial Sheet in portrait mode.
|
||||
public static var verticalWidth: CGFloat {
|
||||
let s = UIScreen.main.bounds.size
|
||||
return min(s.width, s.height) - 2 * (margin + sheetInset)
|
||||
}
|
||||
|
||||
public var buttonTitleNext: String = "Next"
|
||||
public var buttonTitleDone: String = "Close"
|
||||
|
||||
@@ -18,7 +25,7 @@ class TutorialSheet: UIViewController, UIScrollViewDelegate {
|
||||
private let sheetBg: UIView = {
|
||||
let x = UIView(frame: uniRect)
|
||||
x.autoresizingMask = [.flexibleWidth, .flexibleHeight]
|
||||
x.backgroundColor = .sysBg
|
||||
x.backgroundColor = .sysBackground
|
||||
x.layer.cornerRadius = cornerRadius
|
||||
x.layer.shadowColor = UIColor.black.cgColor
|
||||
x.layer.shadowRadius = 10
|
||||
@@ -30,8 +37,8 @@ class TutorialSheet: UIViewController, UIScrollViewDelegate {
|
||||
private let pager: UIPageControl = {
|
||||
let x = UIPageControl(frame: uniRect)
|
||||
x.frame.size.height = x.size(forNumberOfPages: 1).height
|
||||
x.currentPageIndicatorTintColor = UIColor.sysFg.withAlphaComponent(0.5)
|
||||
x.pageIndicatorTintColor = UIColor.sysFg.withAlphaComponent(0.25)
|
||||
x.currentPageIndicatorTintColor = UIColor.sysLabel.withAlphaComponent(0.5)
|
||||
x.pageIndicatorTintColor = UIColor.sysLabel.withAlphaComponent(0.25)
|
||||
x.numberOfPages = 0
|
||||
x.hidesForSinglePage = true
|
||||
x.addTarget(self, action: #selector(pagerDidChange), for: .valueChanged)
|
||||
@@ -47,13 +54,12 @@ class TutorialSheet: UIViewController, UIScrollViewDelegate {
|
||||
|
||||
let content = UIView()
|
||||
x.addSubview(content)
|
||||
content.translatesAutoresizingMaskIntoConstraints = false
|
||||
content.anchor([.left, .right, .top, .bottom], to: x)
|
||||
content.anchor([.width, .height], to: x) | .defaultLow
|
||||
return x
|
||||
}()
|
||||
|
||||
private let button: UIButton = {
|
||||
private lazy var button: UIButton = {
|
||||
let x = QuickUI.button("", target: self, action: #selector(buttonTapped))
|
||||
x.contentEdgeInsets = UIEdgeInsets(all: 8)
|
||||
return x
|
||||
@@ -62,7 +68,7 @@ class TutorialSheet: UIViewController, UIScrollViewDelegate {
|
||||
|
||||
// MARK: Init
|
||||
|
||||
required init?(coder: NSCoder) { super.init(coder: coder) }
|
||||
required init?(coder: NSCoder) { fatalError("init(coder:) has not been implemented") }
|
||||
|
||||
required init() {
|
||||
super.init(nibName: nil, bundle: nil)
|
||||
@@ -98,7 +104,6 @@ class TutorialSheet: UIViewController, UIScrollViewDelegate {
|
||||
pager.numberOfPages += 1
|
||||
updateButtonTitle()
|
||||
let x = UIStackView(frame: pageScroll.bounds)
|
||||
x.translatesAutoresizingMaskIntoConstraints = false
|
||||
x.axis = .vertical
|
||||
x.backgroundColor = UIColor.black
|
||||
x.isOpaque = true
|
||||
@@ -107,7 +112,8 @@ class TutorialSheet: UIViewController, UIScrollViewDelegate {
|
||||
}
|
||||
let prev = content.subviews.last
|
||||
content.addSubview(x)
|
||||
x.anchor([.top, .width, .height], to: pageScroll)
|
||||
x.anchor([.top, .height], to: pageScroll)
|
||||
x.widthAnchor =&= sheetBg.widthAnchor - 2 * sheetInset
|
||||
x.leadingAnchor =&= (prev==nil ? content.leadingAnchor : prev!.trailingAnchor)
|
||||
lastAnchor?.isActive = false
|
||||
lastAnchor = (x.trailingAnchor =&= pageScroll.trailingAnchor)
|
||||
@@ -125,12 +131,10 @@ class TutorialSheet: UIViewController, UIScrollViewDelegate {
|
||||
sheetBg.addSubview(pageScroll)
|
||||
sheetBg.addSubview(button)
|
||||
|
||||
for x in sheetBg.subviews { x.translatesAutoresizingMaskIntoConstraints = false }
|
||||
|
||||
pager.anchor([.top, .left, .right], to: sheetBg)
|
||||
pageScroll.topAnchor =&= pager.bottomAnchor
|
||||
pageScroll.anchor([.left, .right, .top, .bottom], to: sheetBg, margin: cornerRadius/2) | .defaultHigh
|
||||
button.topAnchor =&= pageScroll.bottomAnchor
|
||||
pageScroll.topAnchor =&= pager.bottomAnchor | .defaultHigh
|
||||
pageScroll.anchor([.left, .right, .top, .bottom], to: sheetBg, margin: sheetInset) | .defaultHigh
|
||||
button.topAnchor =&= pageScroll.bottomAnchor | .defaultHigh
|
||||
button.anchor([.bottom, .centerX], to: sheetBg)
|
||||
// button.bottomAnchor =&= sheetBg.bottomAnchor - 30
|
||||
// button.centerXAnchor =&= sheetBg.centerXAnchor
|
||||
|
||||
505
main/DB/DBAppOnly.swift
Normal file
@@ -0,0 +1,505 @@
|
||||
import Foundation
|
||||
import SQLite3
|
||||
|
||||
typealias Timestamp = sqlite3_int64
|
||||
|
||||
extension SQLiteDatabase {
|
||||
func initAppOnlyScheme() {
|
||||
try? run(sql: CreateTable.heap)
|
||||
try? run(sql: CreateTable.rec)
|
||||
try? run(sql: CreateTable.recLog)
|
||||
do {
|
||||
try migrateDB()
|
||||
} catch {
|
||||
QLog.Error("during migration: \(error)")
|
||||
}
|
||||
}
|
||||
|
||||
func migrateDB() throws {
|
||||
let version = try run(sql: "PRAGMA user_version;") { stmt -> Int32 in
|
||||
try ifStep(stmt, SQLITE_ROW)
|
||||
return sqlite3_column_int(stmt, 0)
|
||||
}
|
||||
if version != 2 {
|
||||
QLog.Info("migrate db \(version) -> 2")
|
||||
// version 0 -> 1: req(domain) -> heap(fqdn, domain)
|
||||
// version 1 -> 2: rec(+subtitle, +opt)
|
||||
if version == 1 {
|
||||
transaction("""
|
||||
ALTER TABLE rec ADD COLUMN subtitle TEXT;
|
||||
ALTER TABLE rec ADD COLUMN uploadkey TEXT;
|
||||
""")
|
||||
}
|
||||
try run(sql: "PRAGMA user_version = 2;")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private enum TableName: String {
|
||||
case heap, cache
|
||||
}
|
||||
|
||||
extension SQLiteDatabase {
|
||||
fileprivate func lastRowId(_ table: TableName) -> SQLiteRowID {
|
||||
(try? run(sql:"SELECT rowid FROM \(table.rawValue) ORDER BY rowid DESC LIMIT 1;") {
|
||||
try ifStep($0, SQLITE_ROW)
|
||||
return sqlite3_column_int64($0, 0)
|
||||
}) ?? 0
|
||||
}
|
||||
|
||||
fileprivate func col_ts(_ stmt: OpaquePointer, _ col: Int32) -> Timestamp {
|
||||
sqlite3_column_int64(stmt, col)
|
||||
}
|
||||
}
|
||||
|
||||
class WhereClauseBuilder: CustomStringConvertible {
|
||||
var description: String = ""
|
||||
private let prefix: String
|
||||
private(set) var bindings: [DBBinding] = []
|
||||
|
||||
init(prefix p: String = "WHERE") { prefix = "\(p) " }
|
||||
|
||||
/// Append new clause by either prepending `WHERE` prefix or placing `AND` between clauses.
|
||||
@discardableResult func and(_ clause: String, _ bind: DBBinding ...) -> Self {
|
||||
description.append((description=="" ? prefix : " AND ") + clause)
|
||||
bindings.append(contentsOf: bind)
|
||||
return self
|
||||
}
|
||||
/// Restrict to `rowid >= {range}.start AND rowid <= {range}.end`.
|
||||
/// Omitted if range is `nil` or individually if a value is `0`.
|
||||
@discardableResult func and(in range: SQLiteRowRange) -> Self {
|
||||
if range.start != 0 { and("rowid >= ?", BindInt64(range.start)) }
|
||||
if range.end != 0 { and("rowid <= ?", BindInt64(range.end)) }
|
||||
return self
|
||||
}
|
||||
/// Restrict to `ts >= {min} AND ts < {max}`. Omit one or the other if value is `0`.
|
||||
@discardableResult func and(min: Timestamp = 0, max: Timestamp = 0) -> Self {
|
||||
if min != 0 { and("ts >= ?", BindInt64(min)) }
|
||||
if max != 0 { and("ts < ?", BindInt64(max)) }
|
||||
return self
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
// MARK: - DNSLog
|
||||
|
||||
extension CreateTable {
|
||||
/// `ts`: Timestamp, `fqdn`: String, `domain`: String, `opt`: Int
|
||||
static var heap: String {"""
|
||||
CREATE TABLE IF NOT EXISTS heap(
|
||||
ts INTEGER DEFAULT (strftime('%s','now')),
|
||||
fqdn TEXT NOT NULL,
|
||||
domain TEXT NOT NULL,
|
||||
opt INTEGER
|
||||
);
|
||||
"""} // opt currently only used as "blocked" flag
|
||||
}
|
||||
|
||||
struct GroupedDomain {
|
||||
let domain: String, total: Int32, blocked: Int32, lastModified: Timestamp
|
||||
var options: FilterOptions? = nil
|
||||
}
|
||||
typealias GroupedTsOccurrence = (ts: Timestamp, total: Int32, blocked: Int32)
|
||||
typealias DomainTsPair = (domain: String, ts: Timestamp)
|
||||
|
||||
extension SQLiteDatabase {
|
||||
|
||||
// MARK: write
|
||||
|
||||
/// Move newest entries from `cache` to `heap` and return range (in `heap`) of newly inserted entries.
|
||||
/// - Returns: `nil` in case no entries were transmitted.
|
||||
@discardableResult func dnsLogsPersist() -> SQLiteRowRange? {
|
||||
guard lastRowId(.cache) > 0 else { return nil }
|
||||
let before = lastRowId(.heap) + 1
|
||||
createFunction("domainof") { ($0.first as! String).extractDomain() }
|
||||
transaction("""
|
||||
INSERT INTO heap(ts,fqdn,domain,opt) SELECT ts,dns,domainof(dns),nullif(opt&1,0) FROM cache;
|
||||
DELETE FROM cache;
|
||||
""")
|
||||
let after = lastRowId(.heap)
|
||||
return (before > after) ? nil : (before, after)
|
||||
}
|
||||
|
||||
/// `DELETE FROM cache; DELETE FROM heap;`
|
||||
func dnsLogsDeleteAll() throws {
|
||||
try? run(sql: "DELETE FROM cache; DELETE FROM heap;")
|
||||
vacuum()
|
||||
}
|
||||
|
||||
/// Delete rows matching `ts >= ? AND domain = ?`
|
||||
/// - Parameter strict: If `true`, use `fqdn` instead of `domain` column
|
||||
/// - Returns: Number of changes aka. Number of rows deleted
|
||||
@discardableResult func dnsLogsDelete(_ domain: String, strict: Bool, since ts: Timestamp = 0) -> Int32 {
|
||||
let Where = WhereClauseBuilder().and(min: ts)
|
||||
Where.and("\(strict ? "fqdn" : "domain") = ?", BindText(domain)) // (fqdn = ? OR fqdn LIKE '%.' || ?)
|
||||
return (try? run(sql: "DELETE FROM heap \(Where);", bind: Where.bindings) { stmt -> Int32 in
|
||||
try ifStep(stmt, SQLITE_DONE)
|
||||
return numberOfChanges
|
||||
}) ?? 0
|
||||
}
|
||||
|
||||
// MARK: read
|
||||
|
||||
/// `SELECT min(ts) FROM heap`
|
||||
func dnsLogsMinDate() -> Timestamp? {
|
||||
try? run(sql:"SELECT min(ts) FROM heap") {
|
||||
try ifStep($0, SQLITE_ROW)
|
||||
return col_ts($0, 0)
|
||||
}
|
||||
}
|
||||
|
||||
/// Select min and max row id with given condition `ts >= ? AND ts < ?`
|
||||
/// - Parameters:
|
||||
/// - ts1: Restrict min `rowid` to `ts >= ?`. Pass `0` to omit restriction.
|
||||
/// - ts2: Restrict max `rowid` to `ts < ?`. Pass `0` to omit restriction.
|
||||
/// - range: If set, only look at the specified range. Default: `(0,0)`
|
||||
/// - Returns: `nil` in case no rows are matching the condition
|
||||
func dnsLogsRowRange(between ts1: Timestamp, and ts2: Timestamp, within range: SQLiteRowRange = (0,0)) -> SQLiteRowRange? {
|
||||
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) {
|
||||
try ifStep($0, SQLITE_ROW)
|
||||
let max = col_ts($0, 1)
|
||||
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))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Group DNS logs by domain, count occurences and number of blocked requests.
|
||||
/// - Parameters:
|
||||
/// - range: Whenever possible set range to improve SQL lookup times. `start <= rowid <= end `
|
||||
/// - matchingDomain: Restrict `(fqdn|domain) = ?`. Which column is used is determined by `parentDomain`.
|
||||
/// - parentDomain: If `nil` returns `domain` column. Else returns `fqdn` column with restriction on `domain == parentDomain`.
|
||||
/// - Returns: List of grouped domains with no particular sorting order.
|
||||
func dnsLogsGrouped(range: SQLiteRowRange, matchingDomain: String? = nil, parentDomain: String? = nil) -> [GroupedDomain]? {
|
||||
let Where = WhereClauseBuilder().and(in: range)
|
||||
let col: String // fqdn or domain
|
||||
if let parent = parentDomain { // is subdomain
|
||||
col = "fqdn"
|
||||
Where.and("domain = ?", BindText(parent))
|
||||
} else {
|
||||
col = "domain"
|
||||
}
|
||||
if let matching = matchingDomain { // (fqdn = ? OR fqdn LIKE '%.' || ?)
|
||||
Where.and("\(col) = ?", BindText(matching))
|
||||
}
|
||||
return try? run(sql: "SELECT \(col), COUNT(*), COUNT(opt), MAX(ts) FROM heap \(Where) GROUP BY \(col);", bind: Where.bindings) {
|
||||
allRows($0) {
|
||||
GroupedDomain(domain: col_text($0, 0) ?? "",
|
||||
total: sqlite3_column_int($0, 1),
|
||||
blocked: sqlite3_column_int($0, 2),
|
||||
lastModified: col_ts($0, 3))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Get list or individual DNS entries. Mutliple entries in the very same second are grouped.
|
||||
/// - Parameters:
|
||||
/// - fqdn: Exact match for domain name `fqdn = ?`
|
||||
/// - range: Whenever possible set range to improve SQL lookup times. `start <= rowid <= end `
|
||||
/// - Returns: List sorted by reverse timestamp order (newest first)
|
||||
func timesForDomain(_ fqdn: String, range: SQLiteRowRange) -> [GroupedTsOccurrence]? {
|
||||
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) {
|
||||
allRows($0) {
|
||||
(col_ts($0, 0), sqlite3_column_int($0, 1), sqlite3_column_int($0, 2))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
// MARK: - Context Analysis
|
||||
|
||||
typealias ContextAnalysisResult = (domain: String, count: Int32, avg: Double, rank: Double)
|
||||
|
||||
extension SQLiteDatabase {
|
||||
/// Number of times how often given `fqdn` appears in the database
|
||||
func dnsLogsCount(fqdn: String) -> Int? {
|
||||
try? run(sql: "SELECT COUNT(*) FROM heap WHERE fqdn = ?;", bind: [BindText(fqdn)]) {
|
||||
try ifStep($0, SQLITE_ROW)
|
||||
return Int(sqlite3_column_int($0, 0))
|
||||
}
|
||||
}
|
||||
|
||||
/// Get sorted, unique list of `ts` with given `fqdn`.
|
||||
func dnsLogsUniqTs(_ domain: String, isFQDN flag: Bool) -> [Timestamp]? {
|
||||
try? run(sql: "SELECT DISTINCT ts FROM heap WHERE \(flag ? "fqdn" : "domain") = ? ORDER BY ts;",
|
||||
bind: [BindText(domain)]) {
|
||||
allRows($0) { col_ts($0, 0) }
|
||||
}
|
||||
}
|
||||
|
||||
/// Find other domains occurring regularly at roughly the same time as `fqdn`.
|
||||
/// - Warning: `times` list must be **sorted** by time in ascending order.
|
||||
/// - Parameters:
|
||||
/// - times: List of `ts` from `dnsLogsUniqTs(fqdn)`
|
||||
/// - dt: Search for `ts - dt <= X <= ts + dt`
|
||||
/// - fqdn: Rows matching this domain will be excluded from the result set.
|
||||
/// - Returns: List of tuples ordered by rank (ASC).
|
||||
func contextAnalysis(coOccurrence times: [Timestamp], plusMinus dt: Timestamp, exclude domain: String, isFQDN flag: Bool) -> [ContextAnalysisResult]? {
|
||||
guard times.count > 0 else { return nil }
|
||||
createFunction("fnDist") {
|
||||
let x = $0.first as! Timestamp
|
||||
let i = times.binTreeIndex(of: x, compare: <)!
|
||||
let dist: Timestamp
|
||||
switch i {
|
||||
case 0: dist = times[0] - x
|
||||
case times.count: dist = x - times[i-1]
|
||||
default: dist = min(times[i] - x, x - times[i-1])
|
||||
}
|
||||
return dist
|
||||
}
|
||||
// `avg ^ 2`: prefer results that are closer to `times`
|
||||
// `_ / count`: prefer results with higher occurrence count
|
||||
// `time / 2`: Weighting factor (low: prefer close, high: prefer count)
|
||||
// `time` helpful esp. for smaller spans. `avg^2` will raise faster anyway.
|
||||
let fnRank = "(avg * avg + (? / 2.0) + 1) / count" // +1 in case time == 0 -> avg^2 == 0
|
||||
// improve query by excluding entries that are: before the first, or after the last ts
|
||||
let low = times.first! - dt
|
||||
let high = times.last! + dt
|
||||
return try? run(sql: """
|
||||
SELECT fqdn, count, avg, (\(fnRank)) rank FROM (
|
||||
SELECT fqdn, COUNT(*) count, AVG(dist) avg FROM (
|
||||
SELECT fqdn, fnDist(ts) dist FROM heap
|
||||
WHERE ts BETWEEN ? AND ? AND \(flag ? "fqdn" : "domain") != ? AND dist <= ?
|
||||
) GROUP BY fqdn
|
||||
) ORDER BY rank ASC LIMIT 99;
|
||||
""", bind: [BindInt64(dt), BindInt64(low), BindInt64(high), BindText(domain), BindInt64(dt)]) {
|
||||
allRows($0) {
|
||||
(col_text($0, 0) ?? "", sqlite3_column_int($0, 1), sqlite3_column_double($0, 2), sqlite3_column_double($0, 3))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
// MARK: - Recordings
|
||||
|
||||
extension CreateTable {
|
||||
/// `id`: Primary, `start`: Timestamp, `stop`: Timestamp, `appid`: String, `title`: String, `subtitle`: String, `notes`: String
|
||||
static var rec: String {"""
|
||||
CREATE TABLE IF NOT EXISTS rec(
|
||||
id INTEGER PRIMARY KEY,
|
||||
start INTEGER DEFAULT (strftime('%s','now')),
|
||||
stop INTEGER,
|
||||
appid TEXT,
|
||||
title TEXT,
|
||||
subtitle TEXT,
|
||||
notes TEXT,
|
||||
uploadkey TEXT
|
||||
);
|
||||
"""}
|
||||
}
|
||||
|
||||
let readRecordingSelect = "id, start, stop, appid, title, subtitle, notes, uploadkey"
|
||||
struct Recording {
|
||||
let id: sqlite3_int64
|
||||
let start: Timestamp
|
||||
let stop: Timestamp?
|
||||
var appId: String? = nil
|
||||
var title: String? = nil
|
||||
var subtitle: String? = nil
|
||||
var notes: String? = nil
|
||||
var uploadkey: String? = nil
|
||||
}
|
||||
typealias AppBundleInfo = (bundleId: String, name: String?, author: String?)
|
||||
|
||||
extension SQLiteDatabase {
|
||||
|
||||
// MARK: write
|
||||
|
||||
/// Create new recording with `stop` set to `NULL`.
|
||||
func recordingStartNew() throws -> Recording {
|
||||
try run(sql: "INSERT INTO rec (stop) VALUES (NULL);") { stmt -> Recording in
|
||||
try ifStep(stmt, SQLITE_DONE)
|
||||
return try recordingGet(withID: lastInsertedRow)
|
||||
}
|
||||
}
|
||||
|
||||
/// Update given recording by setting `stop` to current time.
|
||||
func recordingStop(_ r: inout Recording) {
|
||||
guard r.stop == nil else { return }
|
||||
let theID = r.id
|
||||
try? run(sql: "UPDATE rec SET stop = (strftime('%s','now')) WHERE id = ? LIMIT 1;",
|
||||
bind: [BindInt64(theID)]) { stmt -> Void in
|
||||
try ifStep(stmt, SQLITE_DONE)
|
||||
r = try recordingGet(withID: theID)
|
||||
}
|
||||
}
|
||||
|
||||
/// Update given recording by replacing `title`, `appid`, and `notes` with new values.
|
||||
func recordingUpdate(_ r: Recording) {
|
||||
try? run(sql: "UPDATE rec SET appid = ?, title = ?, subtitle = ?, notes = ?, uploadkey = ? WHERE id = ? LIMIT 1;",
|
||||
bind: [BindTextOrNil(r.appId), BindTextOrNil(r.title), BindTextOrNil(r.subtitle),
|
||||
BindTextOrNil(r.notes), BindTextOrNil(r.uploadkey), BindInt64(r.id)]) { stmt -> Void in
|
||||
sqlite3_step(stmt)
|
||||
}
|
||||
}
|
||||
|
||||
/// Delete recording and all of its entries.
|
||||
/// - Returns: `true` on success
|
||||
func recordingDelete(_ r: Recording) throws -> Bool {
|
||||
_ = try? recordingLogsDelete(r.id)
|
||||
return try run(sql: "DELETE FROM rec WHERE id = ? LIMIT 1;", bind: [BindInt64(r.id)]) {
|
||||
try ifStep($0, SQLITE_DONE)
|
||||
return numberOfChanges > 0
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: read
|
||||
|
||||
private func readRecording(_ stmt: OpaquePointer) -> Recording {
|
||||
let end = col_ts(stmt, 2)
|
||||
return Recording(id: sqlite3_column_int64(stmt, 0),
|
||||
start: col_ts(stmt, 1),
|
||||
stop: end == 0 ? nil : end,
|
||||
appId: col_text(stmt, 3),
|
||||
title: col_text(stmt, 4),
|
||||
subtitle: col_text(stmt, 5),
|
||||
notes: col_text(stmt, 6),
|
||||
uploadkey: col_text(stmt, 7))
|
||||
}
|
||||
|
||||
/// `WHERE stop IS NULL`
|
||||
func recordingGetOngoing() -> Recording? {
|
||||
try? run(sql: "SELECT \(readRecordingSelect) FROM rec WHERE stop IS NULL LIMIT 1;") {
|
||||
try ifStep($0, SQLITE_ROW)
|
||||
return readRecording($0)
|
||||
}
|
||||
}
|
||||
|
||||
/// Get `Timestamp` of last recording.
|
||||
func recordingLastTimestamp() -> Timestamp? {
|
||||
try? run(sql: "SELECT stop FROM rec WHERE stop IS NOT NULL ORDER BY rowid DESC LIMIT 1;") {
|
||||
try ifStep($0, SQLITE_ROW)
|
||||
return col_ts($0, 0)
|
||||
}
|
||||
}
|
||||
|
||||
/// `WHERE stop IS NOT NULL`
|
||||
func recordingGetAll() -> [Recording]? {
|
||||
try? run(sql: "SELECT \(readRecordingSelect) FROM rec WHERE stop IS NOT NULL;") {
|
||||
allRows($0) { readRecording($0) }
|
||||
}
|
||||
}
|
||||
|
||||
/// `WHERE id = ?`
|
||||
private func recordingGet(withID: sqlite3_int64) throws -> Recording {
|
||||
try run(sql: "SELECT \(readRecordingSelect) FROM rec WHERE id = ? LIMIT 1;", bind: [BindInt64(withID)]) {
|
||||
try ifStep($0, SQLITE_ROW)
|
||||
return readRecording($0)
|
||||
}
|
||||
}
|
||||
|
||||
func appBundleList() -> [AppBundleInfo]? {
|
||||
try? run(sql: "SELECT appid, title, subtitle FROM rec WHERE appid IS NOT NULL GROUP BY appid ORDER BY title ASC;") {
|
||||
allRows($0) {
|
||||
AppBundleInfo(col_text($0, 0)!, col_text($0, 1), col_text($0, 2))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
// MARK: - RecordingLog
|
||||
|
||||
extension CreateTable {
|
||||
/// `rid`: Reference `rec(id)`, `ts`: Timestamp, `domain`: String
|
||||
static var recLog: String {"""
|
||||
CREATE TABLE IF NOT EXISTS recLog(
|
||||
rid INTEGER REFERENCES rec(id) ON DELETE CASCADE,
|
||||
ts INTEGER,
|
||||
domain TEXT
|
||||
);
|
||||
"""}
|
||||
}
|
||||
|
||||
extension SQLiteDatabase {
|
||||
|
||||
// MARK: write
|
||||
|
||||
/// Duplicate and copy all log entries for given recording to `recLog` table
|
||||
func recordingLogsPersist(_ r: Recording) {
|
||||
guard let end = r.stop else { return }
|
||||
// TODO: make sure cache entries get copied too.
|
||||
// either by copying them directly from cache or perform sync first
|
||||
try? run(sql: """
|
||||
INSERT INTO recLog (rid, ts, domain) SELECT ?, ts, fqdn FROM heap
|
||||
WHERE heap.ts >= ? AND heap.ts <= ?
|
||||
""", bind: [BindInt64(r.id), BindInt64(r.start), BindInt64(end)]) {
|
||||
try ifStep($0, SQLITE_DONE)
|
||||
}
|
||||
}
|
||||
|
||||
/// Delete all log entries with given recording id. Optional: only delete entries for a single domain
|
||||
/// - Parameter d: If `nil` remove all entries for given recording
|
||||
/// - Returns: Number of deleted rows
|
||||
func recordingLogsDelete(_ recId: sqlite3_int64, matchingDomain d: String? = nil) throws -> Int32 {
|
||||
try run(sql: "DELETE FROM recLog WHERE rid = ? \(d==nil ? "" : "AND domain = ?");",
|
||||
bind: [BindInt64(recId), d==nil ? nil : BindText(d!)]) {
|
||||
try ifStep($0, SQLITE_DONE)
|
||||
return numberOfChanges
|
||||
}
|
||||
}
|
||||
|
||||
/// 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
|
||||
|
||||
/// List of domains and count occurences for given recording.
|
||||
/// - Returns: List of `(domain, ts)` pairs. Sorted by `ts` in ascending order (oldest first)
|
||||
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)]) {
|
||||
allRows($0) { (col_text($0, 0) ?? "", col_ts($0, 1)) }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
// MARK: - DBSettings
|
||||
|
||||
//extension CreateTable {
|
||||
// static var settings: String {
|
||||
// "CREATE TABLE IF NOT EXISTS settings(key TEXT UNIQUE NOT NULL, val TEXT);"
|
||||
// }
|
||||
//}
|
||||
//
|
||||
//extension SQLiteDatabase {
|
||||
// func getSetting(for key: String) -> String? {
|
||||
// try? run(sql: "SELECT val FROM settings WHERE key = ?;",
|
||||
// bind: [BindText(key)]) { readText($0, 0) }
|
||||
// }
|
||||
// func setSetting(_ value: String?, for key: String) {
|
||||
// if let value = value {
|
||||
// try? run(sql: "INSERT OR REPLACE INTO settings (key, val) VALUES (?, ?);",
|
||||
// bind: [BindText(value), BindText(key)]) { step($0) }
|
||||
// } else {
|
||||
// try? run(sql: "DELETE FROM settings WHERE key = ?;",
|
||||
// bind: [BindText(key)]) { step($0) }
|
||||
// }
|
||||
// }
|
||||
//}
|
||||
113
main/DB/DBCommon.swift
Normal file
@@ -0,0 +1,113 @@
|
||||
import Foundation
|
||||
import SQLite3
|
||||
|
||||
enum CreateTable {} // used for CREATE TABLE statements
|
||||
|
||||
extension SQLiteDatabase {
|
||||
func initCommonScheme() {
|
||||
try? run(sql: CreateTable.cache)
|
||||
try? run(sql: CreateTable.filter)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// MARK: - transit
|
||||
|
||||
extension CreateTable {
|
||||
/// `ts`: Timestamp, `dns`: String, `opt`: Int
|
||||
static var cache: String {"""
|
||||
CREATE TABLE IF NOT EXISTS cache(
|
||||
ts INTEGER DEFAULT (strftime('%s','now')),
|
||||
dns TEXT NOT NULL,
|
||||
opt INTEGER
|
||||
);
|
||||
"""}
|
||||
}
|
||||
|
||||
extension SQLiteDatabase {
|
||||
// /// `INSERT INTO cache (dns, opt) VALUES (?, ?);`
|
||||
// func logWritePrepare() throws -> OpaquePointer {
|
||||
// try prepare(sql: "INSERT INTO cache (dns, opt) VALUES (?, ?);")
|
||||
// }
|
||||
// /// `prep` must exist and be initialized with `logWritePrepare()`
|
||||
// func logWrite(_ pStmt: OpaquePointer!, _ domain: String, blocked: Bool = false) throws {
|
||||
// guard let prep = pStmt else {
|
||||
// return
|
||||
// }
|
||||
// try prepared(run: prep, bind: [BindText(domain), BindInt32(blocked ? 1 : 0)])
|
||||
// }
|
||||
/// `INSERT INTO cache (dns, opt) VALUES (?, ?);`
|
||||
func logWrite(_ domain: String, blocked: Bool = false) throws {
|
||||
try self.run(sql: "INSERT INTO cache (dns, opt) VALUES (?, ?);",
|
||||
bind: [BindText(domain), BindInt32(blocked ? 1 : 0)])
|
||||
{ try ifStep($0, SQLITE_DONE) }
|
||||
}
|
||||
|
||||
/// `DELETE FROM cache WHERE ts < (now - ? days);`
|
||||
/// - Parameter days: if `0` or negative, this function does nothing.
|
||||
/// - Returns: `true` if at least one row was deleted.
|
||||
@discardableResult func dnsLogsDeleteOlderThan(days: Int) throws -> Bool {
|
||||
guard days > 0 else { return false }
|
||||
func delFrom(_ table: String) throws -> Bool {
|
||||
return try self.run(sql: "DELETE FROM \(table) WHERE ts < strftime('%s', 'now', ?);",
|
||||
bind: [BindText("-\(days) days")]) {
|
||||
try ifStep($0, SQLITE_DONE)
|
||||
return numberOfChanges > 0
|
||||
}
|
||||
}
|
||||
let didDelHeap = try delFrom("heap")
|
||||
let didDelCache = try delFrom("cache")
|
||||
return didDelHeap || didDelCache
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// MARK: - filter
|
||||
|
||||
extension CreateTable {
|
||||
/// `domain`: String, `opt`: Int
|
||||
static var filter: String {"""
|
||||
CREATE TABLE IF NOT EXISTS filter(
|
||||
domain TEXT UNIQUE NOT NULL,
|
||||
opt INTEGER
|
||||
);
|
||||
"""}
|
||||
}
|
||||
|
||||
struct FilterOptions: OptionSet {
|
||||
let rawValue: Int32
|
||||
static let none = FilterOptions([])
|
||||
static let blocked = FilterOptions(rawValue: 1 << 0)
|
||||
static let ignored = FilterOptions(rawValue: 1 << 1)
|
||||
static let customA = FilterOptions(rawValue: 1 << 2)
|
||||
static let customB = FilterOptions(rawValue: 1 << 3)
|
||||
static let any = FilterOptions(rawValue: 0b1111)
|
||||
}
|
||||
|
||||
extension SQLiteDatabase {
|
||||
func loadFilters(where matching: FilterOptions? = nil) -> [String : FilterOptions]? {
|
||||
let rv = matching?.rawValue ?? 0
|
||||
return try? run(sql: "SELECT domain, opt FROM filter \(rv>0 ? "WHERE opt & ?" : "");",
|
||||
bind: rv>0 ? [BindInt32(rv)] : []) {
|
||||
allRowsKeyed($0) {
|
||||
(key: col_text($0, 0) ?? "",
|
||||
value: FilterOptions(rawValue: sqlite3_column_int($0, 1)))
|
||||
}
|
||||
}
|
||||
}
|
||||
func setFilter(_ domain: String, _ value: FilterOptions?) {
|
||||
if let rv = value?.rawValue, rv > 0 {
|
||||
try? run(sql: "INSERT OR REPLACE INTO filter (domain, opt) VALUES (?, ?);",
|
||||
bind: [BindText(domain), BindInt32(rv)]) { _ = sqlite3_step($0) }
|
||||
} else {
|
||||
try? run(sql: "DELETE FROM filter WHERE domain = ? LIMIT 1;",
|
||||
bind: [BindText(domain)]) { _ = sqlite3_step($0) }
|
||||
}
|
||||
}
|
||||
// func loadFilterCount() -> (blocked: Int32, ignored: Int32)? {
|
||||
// try? run(sql: "SELECT SUM(opt&1), SUM(opt&2)/2 FROM filter;") {
|
||||
// try ifStep($0, SQLITE_ROW)
|
||||
// return (sqlite3_column_int($0, 0), sqlite3_column_int($0, 1))
|
||||
// }
|
||||
// }
|
||||
}
|
||||
252
main/DB/DBCore.swift
Normal file
@@ -0,0 +1,252 @@
|
||||
import Foundation
|
||||
import SQLite3
|
||||
|
||||
// iOS 9.3 uses SQLite 3.8.10
|
||||
|
||||
enum SQLiteError: Error {
|
||||
case OpenDatabase(message: String)
|
||||
case Prepare(message: String)
|
||||
case Step(message: String)
|
||||
case Bind(message: String)
|
||||
}
|
||||
|
||||
/// `try? SQLiteDatabase.open()`
|
||||
var AppDB: SQLiteDatabase? { get { try? SQLiteDatabase.open() } }
|
||||
typealias SQLiteRowID = sqlite3_int64
|
||||
/// `0` indicates an unbound edge.
|
||||
typealias SQLiteRowRange = (start: SQLiteRowID, end: SQLiteRowID)
|
||||
|
||||
// MARK: - SQLiteDatabase
|
||||
|
||||
class SQLiteDatabase {
|
||||
fileprivate var functions = [String: [Int: Function]]()
|
||||
private let dbPointer: OpaquePointer?
|
||||
private init(dbPointer: OpaquePointer?) {
|
||||
self.dbPointer = dbPointer
|
||||
}
|
||||
|
||||
fileprivate var errorMessage: String {
|
||||
if let errorPointer = sqlite3_errmsg(dbPointer) {
|
||||
let errorMessage = String(cString: errorPointer)
|
||||
return errorMessage
|
||||
} else {
|
||||
return "No error message provided from sqlite."
|
||||
}
|
||||
}
|
||||
|
||||
deinit {
|
||||
sqlite3_close_v2(dbPointer)
|
||||
}
|
||||
|
||||
static func destroyDatabase(path: String = URL.internalDB().relativePath) {
|
||||
if FileManager.default.fileExists(atPath: path) {
|
||||
do { try FileManager.default.removeItem(atPath: path) }
|
||||
catch { print("Could not destroy database file: \(path)") }
|
||||
}
|
||||
}
|
||||
|
||||
static func open(path: String = URL.internalDB().relativePath) throws -> SQLiteDatabase {
|
||||
var db: OpaquePointer?
|
||||
if sqlite3_open_v2(path, &db, SQLITE_OPEN_READWRITE | SQLITE_OPEN_CREATE | SQLITE_OPEN_FULLMUTEX, nil) == SQLITE_OK {
|
||||
sqlite3_busy_timeout(db, 800)
|
||||
return SQLiteDatabase(dbPointer: db)
|
||||
} else {
|
||||
defer { sqlite3_close_v2(db) }
|
||||
if let errorPointer = sqlite3_errmsg(db) {
|
||||
let message = String(cString: errorPointer)
|
||||
throw SQLiteError.OpenDatabase(message: message)
|
||||
} else {
|
||||
throw SQLiteError.OpenDatabase(message: "No error message provided from sqlite.")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func run<T>(sql: String, bind: [DBBinding?] = [], step: (OpaquePointer) throws -> T) throws -> T {
|
||||
// print("SQL run: \(sql)")
|
||||
// for x in bind where x != nil {
|
||||
// print(" -> \(x!)")
|
||||
// }
|
||||
var statement: OpaquePointer?
|
||||
guard sqlite3_prepare_v2(dbPointer, sql, -1, &statement, nil) == SQLITE_OK,
|
||||
let stmt = statement else {
|
||||
throw SQLiteError.Prepare(message: errorMessage)
|
||||
}
|
||||
defer { sqlite3_finalize(stmt) }
|
||||
var col: Int32 = 0
|
||||
for b in bind.compactMap({$0}) {
|
||||
col += 1
|
||||
guard b.bind(stmt, col) == SQLITE_OK else {
|
||||
throw SQLiteError.Bind(message: errorMessage)
|
||||
}
|
||||
}
|
||||
return try step(stmt)
|
||||
}
|
||||
|
||||
func run(sql: String) throws {
|
||||
// print("SQL exec: \(sql)")
|
||||
var err: UnsafeMutablePointer<Int8>? = nil
|
||||
if sqlite3_exec(dbPointer, sql, nil, nil, &err) != SQLITE_OK {
|
||||
let errMsg = (err != nil) ? String(cString: err!) : "Unknown execution error"
|
||||
sqlite3_free(err);
|
||||
throw SQLiteError.Step(message: errMsg)
|
||||
}
|
||||
}
|
||||
|
||||
/// `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 {
|
||||
guard sqlite3_step(stmt) == expected else {
|
||||
throw SQLiteError.Step(message: errorMessage)
|
||||
}
|
||||
}
|
||||
|
||||
func vacuum() { NSLog("[SQL] VACUUM"); try? run(sql: "VACUUM;"); }
|
||||
func rollback() { NSLog("[SQL] ROLLBACK"); try? run(sql: "ROLLBACK;"); }
|
||||
}
|
||||
|
||||
|
||||
// MARK: - Custom Functions
|
||||
|
||||
// let SQLITE_STATIC = unsafeBitCast(0, sqlite3_destructor_type.self)
|
||||
let SQLITE_TRANSIENT = unsafeBitCast(-1, to: sqlite3_destructor_type.self)
|
||||
|
||||
public struct Blob {
|
||||
public let bytes: [UInt8]
|
||||
public init(bytes: [UInt8]) { self.bytes = bytes }
|
||||
public init(bytes: UnsafeRawPointer, length: Int) {
|
||||
let i8bufptr = UnsafeBufferPointer(start: bytes.assumingMemoryBound(to: UInt8.self), count: length)
|
||||
self.init(bytes: [UInt8](i8bufptr))
|
||||
}
|
||||
}
|
||||
|
||||
extension SQLiteDatabase {
|
||||
fileprivate typealias Function = @convention(block) (OpaquePointer?, Int32, UnsafeMutablePointer<OpaquePointer?>?) -> Void
|
||||
|
||||
func createFunction(_ function: String, argumentCount: UInt? = nil, deterministic: Bool = false, _ block: @escaping (_ args: [Any?]) -> Any?) {
|
||||
let argc = argumentCount.map { Int($0) } ?? -1
|
||||
let box: Function = { context, argc, argv in
|
||||
let arguments: [Any?] = (0..<Int(argc)).map {
|
||||
let value = argv![$0]
|
||||
switch sqlite3_value_type(value) {
|
||||
case SQLITE_BLOB: return Blob(bytes: sqlite3_value_blob(value), length: Int(sqlite3_value_bytes(value)))
|
||||
case SQLITE_FLOAT: return sqlite3_value_double(value)
|
||||
case SQLITE_INTEGER: return sqlite3_value_int64(value)
|
||||
case SQLITE_NULL: return nil
|
||||
case SQLITE_TEXT: return String(cString: UnsafePointer(sqlite3_value_text(value)))
|
||||
case let type: fatalError("unsupported value type: \(type)")
|
||||
}
|
||||
}
|
||||
let result = block(arguments)
|
||||
if let r = result as? Blob { sqlite3_result_blob(context, r.bytes, Int32(r.bytes.count), nil) }
|
||||
else if let r = result as? Double { sqlite3_result_double(context, r) }
|
||||
else if let r = result as? Int64 { sqlite3_result_int64(context, r) }
|
||||
else if let r = result as? Bool { sqlite3_result_int(context, r ? 1 : 0) }
|
||||
else if let r = result as? String { sqlite3_result_text(context, r, Int32(r.count), SQLITE_TRANSIENT) }
|
||||
else if result == nil { sqlite3_result_null(context) }
|
||||
else { fatalError("unsupported result type: \(String(describing: result))") }
|
||||
}
|
||||
var flags = SQLITE_UTF8
|
||||
if deterministic {
|
||||
flags |= SQLITE_DETERMINISTIC
|
||||
}
|
||||
sqlite3_create_function_v2(dbPointer, function, Int32(argc), flags, unsafeBitCast(box, to: UnsafeMutableRawPointer.self), { context, argc, value in
|
||||
let function = unsafeBitCast(sqlite3_user_data(context), to: Function.self)
|
||||
function(context, argc, value)
|
||||
}, nil, nil, nil)
|
||||
if functions[function] == nil { functions[function] = [:] }
|
||||
functions[function]?[argc] = box
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// MARK: - Bindings
|
||||
|
||||
protocol DBBinding {
|
||||
func bind(_ stmt: OpaquePointer!, _ col: Int32) -> Int32
|
||||
}
|
||||
|
||||
struct BindNull : DBBinding {
|
||||
func bind(_ stmt: OpaquePointer!, _ col: Int32) -> Int32 { sqlite3_bind_null(stmt, col) }
|
||||
}
|
||||
|
||||
struct BindInt32 : DBBinding {
|
||||
let raw: Int32
|
||||
init(_ value: Int32) { raw = value }
|
||||
func bind(_ stmt: OpaquePointer!, _ col: Int32) -> Int32 { sqlite3_bind_int(stmt, col, raw) }
|
||||
}
|
||||
|
||||
struct BindInt64 : DBBinding {
|
||||
let raw: sqlite3_int64
|
||||
init(_ value: sqlite3_int64) { raw = value }
|
||||
func bind(_ stmt: OpaquePointer!, _ col: Int32) -> Int32 { sqlite3_bind_int64(stmt, col, raw) }
|
||||
}
|
||||
|
||||
struct BindText : DBBinding {
|
||||
let raw: String
|
||||
init(_ value: String) { raw = value }
|
||||
func bind(_ stmt: OpaquePointer!, _ col: Int32) -> Int32 { sqlite3_bind_text(stmt, col, (raw as NSString).utf8String, -1, nil) }
|
||||
}
|
||||
|
||||
struct BindTextOrNil : DBBinding {
|
||||
let raw: String?
|
||||
init(_ value: String?) { raw = value }
|
||||
func bind(_ stmt: OpaquePointer!, _ col: Int32) -> Int32 { sqlite3_bind_text(stmt, col, (raw == nil) ? nil : (raw! as NSString).utf8String, -1, nil) }
|
||||
}
|
||||
|
||||
// MARK: - Easy Access func
|
||||
|
||||
extension SQLiteDatabase {
|
||||
var numberOfChanges: Int32 { get { sqlite3_changes(dbPointer) } }
|
||||
var lastInsertedRow: SQLiteRowID { get { sqlite3_last_insert_rowid(dbPointer) } }
|
||||
|
||||
func col_text(_ stmt: OpaquePointer, _ col: Int32) -> String? {
|
||||
let val = sqlite3_column_text(stmt, col)
|
||||
return (val != nil ? String(cString: val!) : nil)
|
||||
}
|
||||
|
||||
func allRows<T>(_ stmt: OpaquePointer, _ fn: (OpaquePointer) -> T) -> [T] {
|
||||
var r: [T] = []
|
||||
while (sqlite3_step(stmt) == SQLITE_ROW) { r.append(fn(stmt)) }
|
||||
return r
|
||||
}
|
||||
|
||||
func allRowsKeyed<T,U>(_ stmt: OpaquePointer, _ fn: (OpaquePointer) -> (key: T, value: U)) -> [T:U] {
|
||||
var r: [T:U] = [:]
|
||||
while (sqlite3_step(stmt) == SQLITE_ROW) { let (k,v) = fn(stmt); r[k] = v }
|
||||
return r
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// MARK: - Prepared Statement
|
||||
|
||||
extension SQLiteDatabase {
|
||||
func prepare(sql: String) throws -> OpaquePointer {
|
||||
var pStmt: OpaquePointer?
|
||||
guard sqlite3_prepare_v2(dbPointer, sql, -1, &pStmt, nil) == SQLITE_OK, let S = pStmt else {
|
||||
sqlite3_finalize(pStmt)
|
||||
throw SQLiteError.Prepare(message: errorMessage)
|
||||
}
|
||||
return S
|
||||
}
|
||||
|
||||
@discardableResult func prepared(run pStmt: OpaquePointer!, bind: [DBBinding?] = []) throws -> Int32 {
|
||||
defer { sqlite3_reset(pStmt) }
|
||||
var col: Int32 = 0
|
||||
for b in bind.compactMap({$0}) {
|
||||
col += 1
|
||||
guard b.bind(pStmt, col) == SQLITE_OK else {
|
||||
throw SQLiteError.Bind(message: errorMessage)
|
||||
}
|
||||
}
|
||||
return sqlite3_step(pStmt)
|
||||
}
|
||||
|
||||
func prepared(finalize pStmt: OpaquePointer!) {
|
||||
sqlite3_finalize(pStmt)
|
||||
}
|
||||
}
|
||||
43
main/DB/DBExtensions.swift
Normal file
@@ -0,0 +1,43 @@
|
||||
import UIKit
|
||||
|
||||
extension GroupedDomain {
|
||||
/// Return new `GroupedDomain` by adding `total` and `blocked` counts. Set `lastModified` to the maximum of the two.
|
||||
static func +(a: GroupedDomain, b: GroupedDomain) -> Self {
|
||||
GroupedDomain(domain: a.domain, total: a.total + b.total, blocked: a.blocked + b.blocked,
|
||||
lastModified: max(a.lastModified, b.lastModified), options: a.options ?? b.options )
|
||||
}
|
||||
/// Return new `GroupedDomain` by subtracting `total` and `blocked` counts.
|
||||
static func -(a: GroupedDomain, b: GroupedDomain) -> Self {
|
||||
GroupedDomain(domain: a.domain, total: a.total - b.total, blocked: a.blocked - b.blocked,
|
||||
lastModified: a.lastModified, options: a.options )
|
||||
}
|
||||
}
|
||||
|
||||
extension GroupedDomain {
|
||||
var detailCellText: String { get {
|
||||
return blocked > 0
|
||||
? "\(DateFormat.seconds(lastModified)) — \(blocked)/\(total) blocked"
|
||||
: "\(DateFormat.seconds(lastModified)) — \(total)"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
extension FilterOptions {
|
||||
func tableRowImage() -> UIImage? {
|
||||
let blocked = contains(.blocked)
|
||||
let ignored = contains(.ignored)
|
||||
if blocked { return UIImage(named: ignored ? "block_ignore" : "shield-x") }
|
||||
if ignored { return UIImage(named: "quicklook-not") }
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
extension Recording {
|
||||
var fallbackTitle: String { get {
|
||||
isLongTerm ? "Background Recording" : "Unnamed Recording #\(id)"
|
||||
} }
|
||||
var duration: Timestamp { get { (stop ?? .now()) - start } }
|
||||
var isLongTerm: Bool { duration > Timestamp.hours(1) }
|
||||
var isShared: Bool { uploadkey?.count ?? 0 > 0}
|
||||
}
|
||||
|
||||
@@ -1,304 +0,0 @@
|
||||
import UIKit
|
||||
|
||||
let DBWrp = DBWrapper()
|
||||
fileprivate var AppDB: SQLiteDatabase? { get { try? SQLiteDatabase.open() } }
|
||||
|
||||
class DBWrapper {
|
||||
private var latestModification: Timestamp = 0
|
||||
private var dataA: [GroupedDomain] = [] // Domains
|
||||
private var dataB: [[GroupedDomain]] = [] // Hosts
|
||||
private var dataF: [String : FilterOptions] = [:] // Filters
|
||||
private let Q = DispatchQueue(label: "de.uni-bamberg.psi.AppCheck.db-wrapper-queue", attributes: .concurrent)
|
||||
|
||||
// auto update rows callback
|
||||
var currentlyOpenParent: String?
|
||||
weak var dataA_delegate: IncrementalDataSourceUpdate?
|
||||
weak var dataB_delegate: IncrementalDataSourceUpdate?
|
||||
func dataB_delegate(_ parent: String) -> IncrementalDataSourceUpdate? {
|
||||
(currentlyOpenParent == parent) ? dataB_delegate : nil
|
||||
}
|
||||
|
||||
|
||||
// MARK: - Data Source Getter
|
||||
|
||||
func listOfDomains() -> [GroupedDomain] {
|
||||
Q.sync() { dataA }
|
||||
}
|
||||
|
||||
func listOfHosts(_ parent: String) -> [GroupedDomain] {
|
||||
Q.sync() { dataB[ifExist: dataA_index(of: parent)] ?? [] }
|
||||
}
|
||||
|
||||
func dataF_list(_ filter: FilterOptions) -> [String] {
|
||||
Q.sync() { dataF.compactMap { $1.contains(filter) ? $0 : nil } }.sorted()
|
||||
}
|
||||
|
||||
func dataF_counts() -> (blocked: Int, ignored: Int) {
|
||||
Q.sync() { dataF.reduce((0, 0)) {
|
||||
($0.0 + ($1.1.contains(.blocked) ? 1 : 0),
|
||||
$0.1 + ($1.1.contains(.ignored) ? 1 : 0)) }}
|
||||
}
|
||||
|
||||
func listOfTimes(_ domain: String?) -> [(Timestamp, Bool)] {
|
||||
guard let domain = domain else { return [] }
|
||||
return AppDB?.timesForDomain(domain)?.reversed() ?? []
|
||||
}
|
||||
|
||||
|
||||
// MARK: - Init
|
||||
|
||||
func initContentOfDB() {
|
||||
QLog.Debug("SQLite path: \(URL.internalDB())")
|
||||
DispatchQueue.global().async {
|
||||
#if IOS_SIMULATOR
|
||||
self.generateTestData()
|
||||
DispatchQueue.main.async {
|
||||
// dont know why main queue is needed, wont start otherwise
|
||||
Timer.repeating(2, call: #selector(self.insertRandomEntry), on: self)
|
||||
}
|
||||
#endif
|
||||
self.dataF_init()
|
||||
self.dataAB_init()
|
||||
self.autoSyncTimer_init()
|
||||
}
|
||||
}
|
||||
|
||||
private func dataF_init() {
|
||||
let list = AppDB?.loadFilters() ?? [:]
|
||||
Q.async(flags: .barrier) {
|
||||
self.dataF = list
|
||||
NotifyFilterChanged.postAsyncMain()
|
||||
}
|
||||
}
|
||||
|
||||
private func dataAB_init() {
|
||||
let list = AppDB?.domainList()
|
||||
Q.async(flags: .barrier) {
|
||||
self.dataA = []
|
||||
self.dataB = []
|
||||
self.latestModification = 0
|
||||
if let allDomains = list {
|
||||
for (parent, parts) in self.groupBySubdomains(allDomains) {
|
||||
self.dataA.append(parent)
|
||||
self.dataB.append(parts)
|
||||
self.latestModification = max(parent.lastModified, self.latestModification)
|
||||
}
|
||||
}
|
||||
NotifyLogHistoryReset.postAsyncMain()
|
||||
}
|
||||
}
|
||||
|
||||
/// Auto sync new logs every 7 seconds.
|
||||
private func autoSyncTimer_init() {
|
||||
Q.async() { // using Q to start timer only after init data A,B,F
|
||||
DispatchQueue.main.async {
|
||||
// dont know why main queue is needed, wont start otherwise
|
||||
Timer.repeating(7, call: #selector(self.syncNewestLogs), on: self)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// MARK: - Partial Update History
|
||||
|
||||
@objc private func syncNewestLogs() {
|
||||
//QLog.Debug("\(#function)")
|
||||
#if !IOS_SIMULATOR
|
||||
guard currentVPNState == .on else { return }
|
||||
#endif
|
||||
guard let res = AppDB?.domainList(since: latestModification), res.count > 0 else {
|
||||
return
|
||||
}
|
||||
QLog.Info("auto sync \(res.count) new logs")
|
||||
Q.async(flags: .barrier) {
|
||||
var c = 0
|
||||
for (parent, parts) in self.groupBySubdomains(res) {
|
||||
if let i = self.dataA_index(of: parent.domain) {
|
||||
self.mergeExistingParts(parent.domain, at: i, newChildren: parts)
|
||||
|
||||
let merged = parent + self.dataA.remove(at: i)
|
||||
self.dataA.insert(merged, at: c)
|
||||
self.dataB.insert(self.dataB.remove(at: i), at: c)
|
||||
self.dataA_delegate?.moveRow(merged, from: i, to: c)
|
||||
} else {
|
||||
self.dataA.insert(parent, at: c)
|
||||
self.dataB.insert(parts, at: c)
|
||||
self.dataA_delegate?.insertRow(parent, at: c)
|
||||
}
|
||||
c += 1
|
||||
self.latestModification = max(parent.lastModified, self.latestModification)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private func mergeExistingParts(_ dom: String, at index: Int, newChildren: [GroupedDomain]) {
|
||||
let tvc = dataB_delegate(dom)
|
||||
var i = 0
|
||||
for child in newChildren {
|
||||
if let u = dataB[index].firstIndex(where: { $0.domain == child.domain }) {
|
||||
let merged = child + dataB[index].remove(at: u)
|
||||
dataB[index].insert(merged, at: i)
|
||||
tvc?.moveRow(merged, from: u, to: i)
|
||||
} else {
|
||||
dataB[index].insert(child, at: i)
|
||||
tvc?.insertRow(child, at: i)
|
||||
}
|
||||
i += 1
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// MARK: - Delete History
|
||||
|
||||
func deleteHistory() {
|
||||
DispatchQueue.global().async {
|
||||
try? AppDB?.destroyContent()
|
||||
AppDB?.vacuum()
|
||||
self.dataAB_init()
|
||||
}
|
||||
}
|
||||
|
||||
func deleteHistory(domain: String, since ts: Timestamp) {
|
||||
DispatchQueue.global().async {
|
||||
let modified = (try? AppDB?.deleteRows(matching: domain, since: ts)) ?? 0
|
||||
guard modified > 0 else {
|
||||
return // nothing has changed
|
||||
}
|
||||
AppDB?.vacuum()
|
||||
self.Q.async(flags: .barrier) {
|
||||
guard let index = self.dataA_index(of: domain) else {
|
||||
return // nothing has changed
|
||||
}
|
||||
let parentDom = self.dataA[index].domain
|
||||
guard let list = AppDB?.domainList(matching: parentDom), list.count > 0 else {
|
||||
self.dataA.remove(at: index)
|
||||
self.dataB.remove(at: index)
|
||||
self.dataA_delegate?.deleteRow(at: index)
|
||||
self.dataB_delegate(parentDom)?.replaceData(with: [])
|
||||
return // nothing left, after deleting matching rows
|
||||
}
|
||||
// else: incremental update, replace whole list
|
||||
self.dataA[index] = list.merge(parentDom, options: self.dataF[parentDom])
|
||||
self.dataA_delegate?.replaceRow(self.dataA[index], at: index)
|
||||
self.dataB[index].removeAll()
|
||||
for var child in list {
|
||||
child.options = self.dataF[child.domain]
|
||||
self.dataB[index].append(child)
|
||||
}
|
||||
self.dataB_delegate(parentDom)?.replaceData(with: self.dataB[index])
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// MARK: - Partial Update Filter
|
||||
|
||||
func updateFilter(_ domain: String, add: FilterOptions) {
|
||||
updateFilter(domain, set: (dataF[domain] ?? FilterOptions()).union(add))
|
||||
}
|
||||
|
||||
func updateFilter(_ domain: String, remove: FilterOptions) {
|
||||
updateFilter(domain, set: dataF[domain]?.subtracting(remove))
|
||||
}
|
||||
|
||||
/// - Parameters:
|
||||
/// - set: Remove a filter with `nil` or `.none`
|
||||
private func updateFilter(_ domain: String, set: FilterOptions?) {
|
||||
AppDB?.setFilter(domain, set)
|
||||
Q.async(flags: .barrier) {
|
||||
self.dataF[domain] = set
|
||||
if let i = self.dataA_index(of: domain) {
|
||||
if domain == self.dataA[i].domain {
|
||||
self.dataA[i].options = (set == FilterOptions.none) ? nil : set
|
||||
self.dataA_delegate?.replaceRow(self.dataA[i], at: i)
|
||||
}
|
||||
if let u = self.dataB[i].firstIndex(where: { $0.domain == domain }) {
|
||||
self.dataB[i][u].options = (set == FilterOptions.none) ? nil : set
|
||||
self.dataB_delegate(self.dataA[i].domain)?.replaceRow(self.dataB[i][u], at: u)
|
||||
}
|
||||
}
|
||||
NotifyFilterChanged.postAsyncMain()
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// MARK: - Recordings
|
||||
|
||||
func listOfRecordings() -> [Recording] { AppDB?.allRecordings() ?? [] }
|
||||
func recordingGetCurrent() -> Recording? { AppDB?.ongoingRecording() }
|
||||
func recordingStartNew() -> Recording? { try? AppDB?.startNewRecording() }
|
||||
|
||||
func recordingStop(_ r: inout Recording) { AppDB?.stopRecording(&r) }
|
||||
func recordingPersist(_ r: Recording) { AppDB?.persistRecordingLogs(r) }
|
||||
func recordingDetails(_ r: Recording) -> [RecordLog] { AppDB?.getRecordingsLogs(r) ?? [] }
|
||||
|
||||
func recordingUpdate(_ r: Recording) {
|
||||
AppDB?.updateRecording(r)
|
||||
NotifyRecordingChanged.post((r, false))
|
||||
}
|
||||
|
||||
func recordingDelete(_ r: Recording) {
|
||||
if (try? AppDB?.deleteRecording(r)) == true {
|
||||
NotifyRecordingChanged.post((r, true))
|
||||
}
|
||||
}
|
||||
|
||||
func recordingDeleteDetails(_ r: Recording, domain: String?) -> Bool {
|
||||
((try? AppDB?.deleteRecordingLogs(r.id, matchingDomain: domain)) ?? 0) > 0
|
||||
}
|
||||
|
||||
|
||||
// MARK: - Helper methods
|
||||
|
||||
private func dataA_index(of domain: String) -> Int? {
|
||||
dataA.firstIndex { domain.isSubdomain(of: $0.domain) }
|
||||
}
|
||||
|
||||
private func groupBySubdomains(_ allDomains: [GroupedDomain]) -> [(parent: GroupedDomain, parts: [GroupedDomain])] {
|
||||
var i: Int = 0
|
||||
var indexOf: [String: Int] = [:]
|
||||
var res: [(domain: String, list: [GroupedDomain])] = []
|
||||
for var x in allDomains {
|
||||
let domain = x.domain.splitDomainAndHost().domain
|
||||
x.options = dataF[x.domain]
|
||||
if let y = indexOf[domain] {
|
||||
res[y].list.append(x)
|
||||
} else {
|
||||
res.append((domain, [x]))
|
||||
indexOf[domain] = i
|
||||
i += 1
|
||||
}
|
||||
}
|
||||
return res.map { ($1.merge($0, options: self.dataF[$0]), $1) }
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// MARK: - Test Data
|
||||
|
||||
extension DBWrapper {
|
||||
private func generateTestData() {
|
||||
guard let db = AppDB else { return }
|
||||
let deleted = (try? db.deleteRows(matching: "test.com")) ?? 0
|
||||
QLog.Debug("Deleting \(deleted) rows matching 'test.com'")
|
||||
|
||||
QLog.Debug("Writing 33 test logs")
|
||||
try? db.insertDNSQuery("keeptest.com", blocked: false)
|
||||
for _ in 1...4 { try? db.insertDNSQuery("test.com", blocked: false) }
|
||||
for _ in 1...7 { try? db.insertDNSQuery("i.test.com", blocked: false) }
|
||||
for i in 1...8 { try? db.insertDNSQuery("b.test.com", blocked: i>5) }
|
||||
for i in 1...13 { try? db.insertDNSQuery("bi.test.com", blocked: i%2==0) }
|
||||
|
||||
QLog.Debug("Creating 4 filters")
|
||||
db.setFilter("b.test.com", .blocked)
|
||||
db.setFilter("i.test.com", .ignored)
|
||||
db.setFilter("bi.test.com", [.blocked, .ignored])
|
||||
|
||||
QLog.Debug("Done")
|
||||
}
|
||||
|
||||
@objc private func insertRandomEntry() {
|
||||
//QLog.Debug("Inserting 1 periodic log entry")
|
||||
try? AppDB?.insertDNSQuery("\(arc4random() % 5).count.test.com", blocked: true)
|
||||
}
|
||||
}
|
||||
@@ -1,464 +0,0 @@
|
||||
import Foundation
|
||||
import SQLite3
|
||||
|
||||
typealias Timestamp = Int64
|
||||
struct GroupedDomain {
|
||||
let domain: String, total: Int32, blocked: Int32, lastModified: Timestamp
|
||||
var options: FilterOptions? = nil
|
||||
}
|
||||
|
||||
struct FilterOptions: OptionSet {
|
||||
let rawValue: Int32
|
||||
static let none = FilterOptions([])
|
||||
static let blocked = FilterOptions(rawValue: 1 << 0)
|
||||
static let ignored = FilterOptions(rawValue: 1 << 1)
|
||||
static let any = FilterOptions(rawValue: 0b11)
|
||||
}
|
||||
|
||||
enum SQLiteError: Error {
|
||||
case OpenDatabase(message: String)
|
||||
case Prepare(message: String)
|
||||
case Step(message: String)
|
||||
case Bind(message: String)
|
||||
}
|
||||
|
||||
|
||||
// MARK: - SQLiteDatabase
|
||||
|
||||
class SQLiteDatabase {
|
||||
private let dbPointer: OpaquePointer?
|
||||
private init(dbPointer: OpaquePointer?) {
|
||||
self.dbPointer = dbPointer
|
||||
}
|
||||
|
||||
fileprivate var errorMessage: String {
|
||||
if let errorPointer = sqlite3_errmsg(dbPointer) {
|
||||
let errorMessage = String(cString: errorPointer)
|
||||
return errorMessage
|
||||
} else {
|
||||
return "No error message provided from sqlite."
|
||||
}
|
||||
}
|
||||
|
||||
deinit {
|
||||
sqlite3_close(dbPointer)
|
||||
}
|
||||
|
||||
static func destroyDatabase(path: String = URL.internalDB().relativePath) {
|
||||
if FileManager.default.fileExists(atPath: path) {
|
||||
do { try FileManager.default.removeItem(atPath: path) }
|
||||
catch { print("Could not destroy database file: \(path)") }
|
||||
}
|
||||
}
|
||||
|
||||
// static func export() throws -> URL {
|
||||
// let fmt = DateFormatter()
|
||||
// fmt.dateFormat = "yyyy-MM-dd"
|
||||
// let dest = FileManager.default.exportDir().appendingPathComponent("\(fmt.string(from: Date()))-dns-log.sqlite")
|
||||
// try? FileManager.default.removeItem(at: dest)
|
||||
// try FileManager.default.copyItem(at: FileManager.default.internalDB(), to: dest)
|
||||
// return dest
|
||||
// }
|
||||
|
||||
static func open(path: String = URL.internalDB().relativePath) throws -> SQLiteDatabase {
|
||||
var db: OpaquePointer?
|
||||
//sqlite3_open_v2(path, &db, SQLITE_OPEN_READWRITE | SQLITE_OPEN_CREATE | SQLITE_OPEN_SHAREDCACHE, nil)
|
||||
if sqlite3_open(path, &db) == SQLITE_OK {
|
||||
return SQLiteDatabase(dbPointer: db)
|
||||
} else {
|
||||
defer {
|
||||
if db != nil {
|
||||
sqlite3_close(db)
|
||||
}
|
||||
}
|
||||
if let errorPointer = sqlite3_errmsg(db) {
|
||||
let message = String(cString: errorPointer)
|
||||
throw SQLiteError.OpenDatabase(message: message)
|
||||
} else {
|
||||
throw SQLiteError.OpenDatabase(message: "No error message provided from sqlite.")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func run<T>(sql: String, bind: ((OpaquePointer) -> Bool)?, step: (OpaquePointer) throws -> T) throws -> T {
|
||||
var statement: OpaquePointer?
|
||||
guard sqlite3_prepare_v2(dbPointer, sql, -1, &statement, nil) == SQLITE_OK,
|
||||
let stmt = statement else {
|
||||
throw SQLiteError.Prepare(message: errorMessage)
|
||||
}
|
||||
defer { sqlite3_finalize(stmt) }
|
||||
guard bind?(stmt) ?? true else {
|
||||
throw SQLiteError.Bind(message: errorMessage)
|
||||
}
|
||||
return try step(stmt)
|
||||
}
|
||||
|
||||
func ifStep(_ stmt: OpaquePointer, _ expected: Int32) throws {
|
||||
guard sqlite3_step(stmt) == expected else {
|
||||
throw SQLiteError.Step(message: errorMessage)
|
||||
}
|
||||
}
|
||||
|
||||
func createTable(table: SQLTable.Type) throws {
|
||||
try run(sql: table.createStatement, bind: nil) {
|
||||
try ifStep($0, SQLITE_DONE)
|
||||
}
|
||||
}
|
||||
|
||||
func vacuum() {
|
||||
try? run(sql: "VACUUM;", bind: nil) { try ifStep($0, SQLITE_DONE) }
|
||||
}
|
||||
}
|
||||
|
||||
protocol SQLTable {
|
||||
static var createStatement: String { get }
|
||||
}
|
||||
|
||||
|
||||
// MARK: - Easy Access func
|
||||
|
||||
private extension SQLiteDatabase {
|
||||
func bindInt(_ stmt: OpaquePointer, _ col: Int32, _ value: Int32) -> Bool {
|
||||
sqlite3_bind_int(stmt, col, value) == SQLITE_OK
|
||||
}
|
||||
|
||||
func bindInt64(_ stmt: OpaquePointer, _ col: Int32, _ value: sqlite3_int64) -> Bool {
|
||||
sqlite3_bind_int64(stmt, col, value) == SQLITE_OK
|
||||
}
|
||||
|
||||
func bindText(_ stmt: OpaquePointer, _ col: Int32, _ value: String) -> Bool {
|
||||
sqlite3_bind_text(stmt, col, (value as NSString).utf8String, -1, nil) == SQLITE_OK
|
||||
}
|
||||
|
||||
func bindTextOrNil(_ stmt: OpaquePointer, _ col: Int32, _ value: String?) -> Bool {
|
||||
sqlite3_bind_text(stmt, col, (value == nil) ? nil : (value! as NSString).utf8String, -1, nil) == SQLITE_OK
|
||||
}
|
||||
|
||||
func readText(_ stmt: OpaquePointer, _ col: Int32) -> String? {
|
||||
let val = sqlite3_column_text(stmt, col)
|
||||
return (val != nil ? String(cString: val!) : nil)
|
||||
}
|
||||
|
||||
func allRows<T>(_ stmt: OpaquePointer, _ fn: (OpaquePointer) -> T) -> [T] {
|
||||
var r: [T] = []
|
||||
while (sqlite3_step(stmt) == SQLITE_ROW) { r.append(fn(stmt)) }
|
||||
return r
|
||||
}
|
||||
|
||||
func allRowsKeyed<T,U>(_ stmt: OpaquePointer, _ fn: (OpaquePointer) -> (key: T, value: U)) -> [T:U] {
|
||||
var r: [T:U] = [:]
|
||||
while (sqlite3_step(stmt) == SQLITE_ROW) { let (k,v) = fn(stmt); r[k] = v }
|
||||
return r
|
||||
}
|
||||
}
|
||||
|
||||
extension SQLiteDatabase {
|
||||
func initScheme() {
|
||||
try? self.createTable(table: DNSQueryT.self)
|
||||
try? self.createTable(table: DNSFilterT.self)
|
||||
try? self.createTable(table: Recording.self)
|
||||
try? self.createTable(table: RecordingLog.self)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// MARK: - DNSQueryT
|
||||
|
||||
private struct DNSQueryT: SQLTable {
|
||||
let ts: Timestamp
|
||||
let domain: String
|
||||
let wasBlocked: Bool
|
||||
let options: FilterOptions
|
||||
static var createStatement: String {
|
||||
return """
|
||||
CREATE TABLE IF NOT EXISTS req(
|
||||
ts INTEGER DEFAULT (strftime('%s','now')),
|
||||
domain TEXT NOT NULL,
|
||||
logOpt INTEGER DEFAULT 0
|
||||
);
|
||||
"""
|
||||
}
|
||||
}
|
||||
|
||||
extension SQLiteDatabase {
|
||||
|
||||
// MARK: insert
|
||||
|
||||
func insertDNSQuery(_ domain: String, blocked: Bool) throws {
|
||||
try? run(sql: "INSERT INTO req (domain, logOpt) VALUES (?, ?);", bind: {
|
||||
self.bindText($0, 1, domain) && self.bindInt($0, 2, blocked ? 1 : 0)
|
||||
}) {
|
||||
try ifStep($0, SQLITE_DONE)
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: delete
|
||||
|
||||
func destroyContent() throws {
|
||||
try? run(sql: "DROP TABLE IF EXISTS req;", bind: nil) {
|
||||
try ifStep($0, SQLITE_DONE)
|
||||
}
|
||||
try? createTable(table: DNSQueryT.self)
|
||||
}
|
||||
|
||||
/// Delete rows matching `ts >= ? AND "domain" OR "*.domain"`
|
||||
@discardableResult func deleteRows(matching domain: String, since ts: Timestamp = 0) throws -> Int32 {
|
||||
try run(sql: "DELETE FROM req WHERE ts >= ? AND (domain = ? OR domain LIKE '%.' || ?);", bind: {
|
||||
self.bindInt64($0, 1, ts) && self.bindText($0, 2, domain) && self.bindText($0, 3, domain)
|
||||
}) { stmt -> Int32 in
|
||||
try ifStep(stmt, SQLITE_DONE)
|
||||
return sqlite3_changes(dbPointer)
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: read
|
||||
|
||||
func readGroupedDomain(_ stmt: OpaquePointer) -> GroupedDomain {
|
||||
GroupedDomain(domain: readText(stmt, 0) ?? "",
|
||||
total: sqlite3_column_int(stmt, 1),
|
||||
blocked: sqlite3_column_int(stmt, 2),
|
||||
lastModified: sqlite3_column_int64(stmt, 3))
|
||||
}
|
||||
|
||||
func domainList(since ts: Timestamp = 0) -> [GroupedDomain]? {
|
||||
try? run(sql: "SELECT domain, COUNT(*), SUM(logOpt&1), MAX(ts) FROM req \(ts == 0 ? "" : "WHERE ts > ?") GROUP BY domain ORDER BY 4 DESC;", bind: {
|
||||
ts == 0 || self.bindInt64($0, 1, ts)
|
||||
}) {
|
||||
allRows($0) { readGroupedDomain($0) }
|
||||
}
|
||||
}
|
||||
|
||||
func domainList(matching domain: String) -> [GroupedDomain]? {
|
||||
try? run(sql: "SELECT domain, COUNT(*), SUM(logOpt&1), MAX(ts) FROM req WHERE (domain = ? OR domain LIKE '%.' || ?) GROUP BY domain ORDER BY 4 DESC;", bind: {
|
||||
self.bindText($0, 1, domain) && self.bindText($0, 2, domain)
|
||||
}) {
|
||||
allRows($0) { readGroupedDomain($0) }
|
||||
}
|
||||
}
|
||||
|
||||
func timesForDomain(_ fullDomain: String) -> [(Timestamp, Bool)]? {
|
||||
try? run(sql: "SELECT ts, logOpt FROM req WHERE domain = ?;", bind: {
|
||||
self.bindText($0, 1, fullDomain)
|
||||
}) {
|
||||
allRows($0) { (sqlite3_column_int64($0, 0), sqlite3_column_int($0, 1) > 0) }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// MARK: - DNSFilterT
|
||||
|
||||
private struct DNSFilterT: SQLTable {
|
||||
let domain: String
|
||||
let options: FilterOptions
|
||||
static var createStatement: String {
|
||||
return """
|
||||
CREATE TABLE IF NOT EXISTS filter(
|
||||
domain TEXT UNIQUE NOT NULL,
|
||||
opt INTEGER DEFAULT 0
|
||||
);
|
||||
"""
|
||||
}
|
||||
}
|
||||
|
||||
extension SQLiteDatabase {
|
||||
|
||||
// MARK: read
|
||||
|
||||
func loadFilters() -> [String : FilterOptions]? {
|
||||
try? run(sql: "SELECT domain, opt FROM filter;", bind: nil) {
|
||||
allRowsKeyed($0) {
|
||||
(key: readText($0, 0) ?? "",
|
||||
value: FilterOptions(rawValue: sqlite3_column_int($0, 1)))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: write
|
||||
|
||||
func setFilter(_ domain: String, _ value: FilterOptions?) {
|
||||
func removeFilter() {
|
||||
try? run(sql: "DELETE FROM filter WHERE domain = ? LIMIT 1;", bind: {
|
||||
self.bindText($0, 1, domain)
|
||||
}) { stmt -> Void in
|
||||
sqlite3_step(stmt)
|
||||
}
|
||||
}
|
||||
guard let rv = value?.rawValue, rv > 0 else {
|
||||
removeFilter()
|
||||
return
|
||||
}
|
||||
func createFilter() throws {
|
||||
try run(sql: "INSERT OR FAIL INTO filter (domain, opt) VALUES (?, ?);", bind: {
|
||||
self.bindText($0, 1, domain) && self.bindInt($0, 2, rv)
|
||||
}) {
|
||||
try ifStep($0, SQLITE_DONE)
|
||||
}
|
||||
}
|
||||
func updateFilter() {
|
||||
try? run(sql: "UPDATE filter SET opt = ? WHERE domain = ? LIMIT 1;", bind: {
|
||||
self.bindInt($0, 1, rv) && self.bindText($0, 2, domain)
|
||||
}) { stmt -> Void in
|
||||
sqlite3_step(stmt)
|
||||
}
|
||||
}
|
||||
do { try createFilter() } catch { updateFilter() }
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// MARK: - Recordings
|
||||
|
||||
struct Recording: SQLTable {
|
||||
let id: sqlite3_int64
|
||||
let start: Timestamp
|
||||
let stop: Timestamp?
|
||||
var appId: String? = nil
|
||||
var title: String? = nil
|
||||
var notes: String? = nil
|
||||
static var createStatement: String {
|
||||
return """
|
||||
CREATE TABLE IF NOT EXISTS rec(
|
||||
id INTEGER PRIMARY KEY,
|
||||
start INTEGER DEFAULT (strftime('%s','now')),
|
||||
stop INTEGER,
|
||||
appid TEXT,
|
||||
title TEXT,
|
||||
notes TEXT
|
||||
);
|
||||
"""
|
||||
}
|
||||
}
|
||||
|
||||
extension SQLiteDatabase {
|
||||
|
||||
// MARK: write
|
||||
|
||||
func startNewRecording() throws -> Recording {
|
||||
try run(sql: "INSERT INTO rec (stop) VALUES (NULL);", bind: nil) { stmt -> Recording in
|
||||
try ifStep(stmt, SQLITE_DONE)
|
||||
return try getRecording(withID: sqlite3_last_insert_rowid(dbPointer))
|
||||
}
|
||||
}
|
||||
|
||||
func stopRecording(_ r: inout Recording) {
|
||||
guard r.stop == nil else { return }
|
||||
let theID = r.id
|
||||
try? run(sql: "UPDATE rec SET stop = (strftime('%s','now')) WHERE id = ? LIMIT 1;", bind: {
|
||||
self.bindInt64($0, 1, theID)
|
||||
}) { stmt -> Void in
|
||||
try ifStep(stmt, SQLITE_DONE)
|
||||
r = try getRecording(withID: theID)
|
||||
}
|
||||
}
|
||||
|
||||
func updateRecording(_ r: Recording) {
|
||||
try? run(sql: "UPDATE rec SET title = ?, appid = ?, notes = ? WHERE id = ? LIMIT 1;", bind: {
|
||||
self.bindTextOrNil($0, 1, r.title) && self.bindTextOrNil($0, 2, r.appId)
|
||||
&& self.bindTextOrNil($0, 3, r.notes) && self.bindInt64($0, 4, r.id)
|
||||
}) { stmt -> Void in
|
||||
sqlite3_step(stmt)
|
||||
}
|
||||
}
|
||||
|
||||
func deleteRecording(_ r: Recording) throws -> Bool {
|
||||
_ = try? deleteRecordingLogs(r.id)
|
||||
return try run(sql: "DELETE FROM rec WHERE id = ? LIMIT 1;", bind: {
|
||||
self.bindInt64($0, 1, r.id)
|
||||
}) {
|
||||
try ifStep($0, SQLITE_DONE)
|
||||
return sqlite3_changes(dbPointer) > 0
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: read
|
||||
|
||||
func readRecording(_ stmt: OpaquePointer) -> Recording {
|
||||
let end = sqlite3_column_int64(stmt, 2)
|
||||
return Recording(id: sqlite3_column_int64(stmt, 0),
|
||||
start: sqlite3_column_int64(stmt, 1),
|
||||
stop: end == 0 ? nil : end,
|
||||
appId: readText(stmt, 3),
|
||||
title: readText(stmt, 4),
|
||||
notes: readText(stmt, 5))
|
||||
}
|
||||
|
||||
func ongoingRecording() -> Recording? {
|
||||
try? run(sql: "SELECT * FROM rec WHERE stop IS NULL LIMIT 1;", bind: nil) {
|
||||
try ifStep($0, SQLITE_ROW)
|
||||
return readRecording($0)
|
||||
}
|
||||
}
|
||||
|
||||
func allRecordings() -> [Recording]? {
|
||||
try? run(sql: "SELECT * FROM rec WHERE stop IS NOT NULL;", bind: nil) {
|
||||
allRows($0) { readRecording($0) }
|
||||
}
|
||||
}
|
||||
|
||||
func getRecording(withID: sqlite3_int64) throws -> Recording {
|
||||
try run(sql: "SELECT * FROM rec WHERE id = ? LIMIT 1;", bind: {
|
||||
self.bindInt64($0, 1, withID)
|
||||
}) {
|
||||
try ifStep($0, SQLITE_ROW)
|
||||
return readRecording($0)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// MARK:
|
||||
|
||||
private struct RecordingLog: SQLTable {
|
||||
let rID: Int32
|
||||
let ts: Timestamp
|
||||
let domain: String
|
||||
static var createStatement: String {
|
||||
return """
|
||||
CREATE TABLE IF NOT EXISTS recLog(
|
||||
rid INTEGER REFERENCES rec(id) ON DELETE CASCADE,
|
||||
ts INTEGER,
|
||||
domain TEXT
|
||||
);
|
||||
"""
|
||||
}
|
||||
}
|
||||
|
||||
extension SQLiteDatabase {
|
||||
|
||||
// MARK: write
|
||||
|
||||
func persistRecordingLogs(_ r: Recording) {
|
||||
guard let end = r.stop else {
|
||||
return
|
||||
}
|
||||
try? run(sql: """
|
||||
INSERT INTO recLog (rid, ts, domain) SELECT ?, ts, domain FROM req
|
||||
WHERE req.ts >= ? AND req.ts <= ?
|
||||
""", bind: {
|
||||
self.bindInt64($0, 1, r.id) && self.bindInt64($0, 2, r.start) && self.bindInt64($0, 3, end)
|
||||
}) {
|
||||
try ifStep($0, SQLITE_DONE)
|
||||
}
|
||||
}
|
||||
|
||||
func deleteRecordingLogs(_ recId: sqlite3_int64, matchingDomain d: String? = nil) throws -> Int32 {
|
||||
try run(sql: "DELETE FROM recLog WHERE rid = ? \(d==nil ? "" : "AND domain = ?");", bind: {
|
||||
self.bindInt64($0, 1, recId) && (d==nil ? true : self.bindTextOrNil($0, 2,d))
|
||||
}) {
|
||||
try ifStep($0, SQLITE_DONE)
|
||||
return sqlite3_changes(dbPointer)
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: read
|
||||
|
||||
func getRecordingsLogs(_ r: Recording) -> [RecordLog]? {
|
||||
try? run(sql: "SELECT domain, COUNT() FROM recLog WHERE rid = ? GROUP BY domain;", bind: {
|
||||
self.bindInt64($0, 1, r.id)
|
||||
}) {
|
||||
allRows($0) { (readText($0, 0), sqlite3_column_int($0, 1)) }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
typealias RecordLog = (domain: String?, count: Int32)
|
||||
46
main/DB/TheGreatDestroyer.swift
Normal file
@@ -0,0 +1,46 @@
|
||||
import Foundation
|
||||
|
||||
struct TheGreatDestroyer {
|
||||
|
||||
/// Callback fired when user performs row edit -> delete action
|
||||
static func deleteLogs(domain: String, since ts: Timestamp, strict flag: Bool) {
|
||||
sync.pause()
|
||||
DispatchQueue.global().async {
|
||||
defer { sync.continue() }
|
||||
guard let db = AppDB, db.dnsLogsDelete(domain, strict: flag, since: ts) > 0 else {
|
||||
return // nothing has changed
|
||||
}
|
||||
db.vacuum()
|
||||
sync.needsReloadDB(domain: domain)
|
||||
}
|
||||
}
|
||||
|
||||
/// Fired when user taps on Settings -> "Delete All Logs"
|
||||
static func deleteAllLogs() {
|
||||
sync.pause()
|
||||
DispatchQueue.global().async {
|
||||
defer { sync.continue() }
|
||||
do {
|
||||
try AppDB?.dnsLogsDeleteAll()
|
||||
sync.needsReloadDB()
|
||||
} catch {}
|
||||
}
|
||||
}
|
||||
|
||||
/// Fired when user changes Settings -> "Auto-delete logs" and every time the App enters foreground
|
||||
static func deleteLogs(olderThan days: Int) {
|
||||
guard days > 0 else { return }
|
||||
sync.pause()
|
||||
DispatchQueue.global().async {
|
||||
defer { sync.continue() }
|
||||
QLog.Info("Auto-delete logs")
|
||||
do {
|
||||
if try AppDB!.dnsLogsDeleteOlderThan(days: days) {
|
||||
sync.needsReloadDB()
|
||||
}
|
||||
} catch {
|
||||
QLog.Warning("Couldn't auto-delete logs, \(error)")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
52
main/Data Source/DomainFilter.swift
Normal file
@@ -0,0 +1,52 @@
|
||||
import Foundation
|
||||
|
||||
enum DomainFilter {
|
||||
static private var data = AppDB?.loadFilters() ?? [:]
|
||||
|
||||
/// Get filter with given `domain` name
|
||||
@inline(__always) static subscript(_ domain: String) -> FilterOptions? {
|
||||
data[domain]
|
||||
}
|
||||
|
||||
/// Update local memory object by loading values from persistent db.
|
||||
/// - Note: Will trigger `NotifyDNSFilterChanged` notification.
|
||||
// static func reload() {
|
||||
// data = AppDB?.loadFilters() ?? [:]
|
||||
// NotifyDNSFilterChanged.post()
|
||||
// }
|
||||
|
||||
/// Get list of domains (sorted by name) which do contain the given filter
|
||||
static func list(where matching: FilterOptions) -> [String] {
|
||||
data.compactMap { $1.contains(matching) ? $0 : nil }.sorted()
|
||||
}
|
||||
|
||||
/// Get total number of blocked and ignored domains. Shown in settings overview.
|
||||
static func counts() -> (blocked: Int, ignored: Int, listCustomA: Int, listCustomB: Int) {
|
||||
data.reduce(into: (0, 0, 0, 0)) {
|
||||
if $1.1.contains(.blocked) { $0.0 += 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.
|
||||
/// - Note: Will trigger `NotifyDNSFilterChanged` notification.
|
||||
static func update(_ domain: String, add filter: FilterOptions) {
|
||||
update(domain, set: (data[domain] ?? FilterOptions()).union(filter))
|
||||
}
|
||||
|
||||
/// Subtract `filter` from set.
|
||||
/// - Note: Will trigger `NotifyDNSFilterChanged` notification.
|
||||
static func update(_ domain: String, remove filter: FilterOptions) {
|
||||
update(domain, set: data[domain]?.subtracting(filter))
|
||||
}
|
||||
|
||||
/// Update persistent db, local memory object, and post notification to subscribers
|
||||
/// - Parameter set: Remove a filter with `nil` or `.none`
|
||||
static private func update(_ domain: String, set: FilterOptions?) {
|
||||
AppDB?.setFilter(domain, set)
|
||||
data[domain] = (set == FilterOptions.none) ? nil : set
|
||||
NotifyDNSFilterChanged.post(domain)
|
||||
}
|
||||
}
|
||||
307
main/Data Source/GroupedDomainDataSource.swift
Normal file
@@ -0,0 +1,307 @@
|
||||
import UIKit
|
||||
|
||||
protocol GroupedDomainDataSourceDelegate: UITableViewController {
|
||||
/// Currently only called when a row is moved and the `tableView` is frontmost.
|
||||
func groupedDomainDataSource(needsUpdate row: Int)
|
||||
}
|
||||
|
||||
// ##########################
|
||||
// #
|
||||
// # MARK: DataSource
|
||||
// #
|
||||
// ##########################
|
||||
|
||||
class GroupedDomainDataSource: FilterPipelineDelegate, SyncUpdateDelegate {
|
||||
|
||||
let parent: String?
|
||||
private let pipeline = FilterPipeline<GroupedDomain>()
|
||||
private var currentOrder: DateFilterOrderBy = .Date
|
||||
private var orderAsc = false
|
||||
|
||||
private(set) lazy var search = SearchBarManager { [unowned self] _ in
|
||||
self.pipeline.reloadFilter(withId: "search")
|
||||
}
|
||||
|
||||
/// Will init `sync.allowPullToRefresh()` on `tableView.refreshControl` as well.
|
||||
weak var delegate: GroupedDomainDataSourceDelegate? {
|
||||
willSet { if #available(iOS 10.0, *), newValue !== delegate {
|
||||
sync.allowPullToRefresh(onTVC: newValue, forObserver: self)
|
||||
}}}
|
||||
|
||||
/// - Note: Will call `tableview.reloadData()`
|
||||
init(withParent: String?) {
|
||||
parent = withParent
|
||||
let len: Int
|
||||
if let p = withParent, p.first != "#" { len = p.count } else { len = 0 }
|
||||
|
||||
pipeline.addFilter("search") { [unowned self] in
|
||||
!self.search.isActive ||
|
||||
$0.domain.prefix($0.domain.count - len).lowercased().contains(self.search.term)
|
||||
}
|
||||
pipeline.delegate = self
|
||||
resetSortingOrder(force: true)
|
||||
|
||||
NotifyDNSFilterChanged.observe(call: #selector(didChangeDomainFilter), on: self)
|
||||
NotifySortOrderChanged.observe(call: #selector(didChangeSortOrder), on: self)
|
||||
|
||||
sync.addObserver(self) // calls syncUpdate(reset:)
|
||||
}
|
||||
|
||||
/// Callback fired when user changes date filter settings. (`NotifySortOrderChanged` notification)
|
||||
@objc private func didChangeSortOrder(_ notification: Notification) {
|
||||
resetSortingOrder()
|
||||
}
|
||||
|
||||
/// 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.
|
||||
private func resetSortingOrder(force: Bool = false) {
|
||||
let orderAscChanged = (orderAsc <-? Prefs.DateFilter.OrderAsc)
|
||||
let orderTypChanged = (currentOrder <-? Prefs.DateFilter.OrderBy)
|
||||
if orderTypChanged || force {
|
||||
switch currentOrder {
|
||||
case .Date:
|
||||
pipeline.setSorting { [unowned self] in
|
||||
self.orderAsc ? $0.lastModified < $1.lastModified : $0.lastModified > $1.lastModified
|
||||
}
|
||||
case .Name:
|
||||
pipeline.setSorting { [unowned self] in
|
||||
self.orderAsc ? $0.domain < $1.domain : $0.domain > $1.domain
|
||||
}
|
||||
case .Count:
|
||||
pipeline.setSorting { [unowned self] in
|
||||
self.orderAsc ? $0.total < $1.total : $0.total > $1.total
|
||||
}
|
||||
}
|
||||
} else if orderAscChanged {
|
||||
pipeline.reverseSorting()
|
||||
}
|
||||
}
|
||||
|
||||
/// Callback fired when user edits list of `blocked` or `ignored` domains in settings. (`NotifyDNSFilterChanged` notification)
|
||||
@objc private func didChangeDomainFilter(_ notification: Notification) {
|
||||
guard let domain = notification.object as? String else {
|
||||
preconditionFailure("Domain independent filter reset not implemented") // `syncUpdate(reset:)` async!
|
||||
}
|
||||
if let x = pipeline.dataSourceGet(where: { $0.domain == domain }) {
|
||||
var obj = x.object
|
||||
obj.options = DomainFilter[domain]
|
||||
pipeline.update(obj, at: x.index)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// MARK: Table View Data Source
|
||||
|
||||
@inline(__always) var numberOfRows: Int { get { pipeline.displayObjectCount() } }
|
||||
|
||||
@inline(__always) subscript(_ row: Int) -> GroupedDomain { pipeline.displayObject(at: row) }
|
||||
}
|
||||
|
||||
|
||||
// ################################
|
||||
// #
|
||||
// # MARK: - Partial Update
|
||||
// #
|
||||
// ################################
|
||||
|
||||
extension GroupedDomainDataSource {
|
||||
|
||||
func syncUpdate(_: SyncUpdate, reset rows: SQLiteRowRange) {
|
||||
var logs = AppDB?.dnsLogsGrouped(range: rows, parentDomain: parent) ?? []
|
||||
for (i, val) in logs.enumerated() {
|
||||
logs[i].options = DomainFilter[val.domain]
|
||||
}
|
||||
DispatchQueue.main.sync {
|
||||
pipeline.reset(dataSource: logs)
|
||||
}
|
||||
}
|
||||
|
||||
func syncUpdate(_: SyncUpdate, insert rows: SQLiteRowRange, affects: SyncUpdateEnd) {
|
||||
guard let latest = AppDB?.dnsLogsGrouped(range: rows, parentDomain: parent) else {
|
||||
assertionFailure("NotifySyncInsert fired with empty range")
|
||||
return
|
||||
}
|
||||
DispatchQueue.main.sync {
|
||||
cellAnimationsGroup(if: latest.count > 14)
|
||||
for x in latest {
|
||||
if let (i, obj) = pipeline.dataSourceGet(where: { $0.domain == x.domain }) {
|
||||
pipeline.update(obj + x, at: i)
|
||||
} else {
|
||||
var y = x
|
||||
y.options = DomainFilter[x.domain]
|
||||
pipeline.addNew(y)
|
||||
}
|
||||
}
|
||||
cellAnimationsCommit()
|
||||
}
|
||||
}
|
||||
|
||||
func syncUpdate(_ sender: SyncUpdate, remove rows: SQLiteRowRange, affects: SyncUpdateEnd) {
|
||||
if affects == .Latest {
|
||||
// TODO: alternatively query last modified from db (last entry _before_ range)
|
||||
syncUpdate(sender, reset: sender.rows)
|
||||
return
|
||||
}
|
||||
guard let outdated = AppDB?.dnsLogsGrouped(range: rows, parentDomain: parent),
|
||||
outdated.count > 0 else {
|
||||
return
|
||||
}
|
||||
DispatchQueue.main.sync {
|
||||
cellAnimationsGroup(if: outdated.count > 14)
|
||||
var listOfDeletes: [Int] = []
|
||||
for x in outdated {
|
||||
guard let (i, obj) = pipeline.dataSourceGet(where: { $0.domain == x.domain }) else {
|
||||
assertionFailure("Try to remove non-existent element")
|
||||
continue // should never happen
|
||||
}
|
||||
if obj.total > x.total {
|
||||
pipeline.update(obj - x, at: i)
|
||||
} else {
|
||||
listOfDeletes.append(i)
|
||||
}
|
||||
}
|
||||
pipeline.remove(indices: listOfDeletes.sorted())
|
||||
cellAnimationsCommit()
|
||||
}
|
||||
}
|
||||
|
||||
func syncUpdate(_ sender: SyncUpdate, partialRemove affectedFQDN: String) {
|
||||
let affectedParent = affectedFQDN.extractDomain()
|
||||
guard parent == nil || parent == affectedParent else {
|
||||
return // does not affect current table
|
||||
}
|
||||
let affected = (parent == nil ? affectedParent : affectedFQDN)
|
||||
let updated = AppDB?.dnsLogsGrouped(range: sender.rows, matchingDomain: affected, parentDomain: parent)?.first
|
||||
DispatchQueue.main.sync {
|
||||
guard let old = pipeline.dataSourceGet(where: { $0.domain == affected }) else {
|
||||
// can only happen if delete sheet is open while background sync removed the element
|
||||
return
|
||||
}
|
||||
if var updated = updated {
|
||||
assert(old.object.domain == updated.domain)
|
||||
updated.options = DomainFilter[updated.domain]
|
||||
pipeline.update(updated, at: old.index)
|
||||
} else {
|
||||
pipeline.remove(indices: [old.index])
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// #################################
|
||||
// #
|
||||
// # MARK: - Cell Animations
|
||||
// #
|
||||
// #################################
|
||||
|
||||
extension GroupedDomainDataSource {
|
||||
/// Sets `pipeline.delegate = nil` to disable individual cell animations (update, insert, delete & move).
|
||||
private func cellAnimationsGroup(if condition: Bool = true) {
|
||||
if condition || delegate?.tableView.isFrontmost == false {
|
||||
pipeline.delegate = nil
|
||||
}
|
||||
}
|
||||
/// No-Op if cell animations are enabled already.
|
||||
/// Else, set `pipeline.delegate = self` and perform `reloadData()`.
|
||||
private func cellAnimationsCommit() {
|
||||
if pipeline.delegate == nil {
|
||||
pipeline.delegate = self
|
||||
delegate?.tableView.reloadData()
|
||||
}
|
||||
}
|
||||
|
||||
// TODO: Collect animations and post them in a single animations block.
|
||||
// This will require enormous work to translate them into a final set.
|
||||
func filterPipelineDidReset() { delegate?.tableView.reloadData() }
|
||||
func filterPipeline(delete rows: [Int]) { delegate?.tableView.safeDeleteRows(rows) }
|
||||
func filterPipeline(insert row: Int) { delegate?.tableView.safeInsertRow(row, with: .left) }
|
||||
func filterPipeline(update row: Int) {
|
||||
guard let tv = delegate?.tableView else { return }
|
||||
if !tv.isEditing { tv.safeReloadRow(row) }
|
||||
else if tv.isFrontmost == true {
|
||||
delegate?.groupedDomainDataSource(needsUpdate: row)
|
||||
}
|
||||
}
|
||||
func filterPipeline(move oldRow: Int, to newRow: Int) {
|
||||
delegate?.tableView.safeMoveRow(oldRow, to: newRow)
|
||||
if delegate?.tableView.isFrontmost == true {
|
||||
delegate?.groupedDomainDataSource(needsUpdate: newRow)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// ##########################
|
||||
// #
|
||||
// # MARK: - Edit Row
|
||||
// #
|
||||
// ##########################
|
||||
|
||||
protocol GroupedDomainEditRow : UIViewController, EditableRows {
|
||||
var source: GroupedDomainDataSource { get }
|
||||
}
|
||||
|
||||
extension GroupedDomainEditRow {
|
||||
|
||||
func editableRowActions(_ index: IndexPath) -> [(RowAction, String)] {
|
||||
let x = source[index.row]
|
||||
if x.domain.starts(with: "#") {
|
||||
return [(.delete, "Delete")]
|
||||
}
|
||||
let b = x.options?.contains(.blocked) ?? false
|
||||
let i = x.options?.contains(.ignored) ?? false
|
||||
return [(.delete, "Delete"), (.block, b ? "Unblock" : "Block"), (.ignore, i ? "Unignore" : "Ignore")]
|
||||
}
|
||||
|
||||
func editableRowActionColor(_: IndexPath, _ action: RowAction) -> UIColor? {
|
||||
action == .block ? .systemOrange : nil
|
||||
}
|
||||
|
||||
func editableRowUserInfo(_ index: IndexPath) -> Any? { source[index.row] }
|
||||
|
||||
func editableRowCallback(_ index: IndexPath, _ action: RowAction, _ userInfo: Any?) -> Bool {
|
||||
let entry = userInfo as! GroupedDomain
|
||||
switch action {
|
||||
case .ignore: showFilterSheet(entry, .ignored)
|
||||
case .block: showFilterSheet(entry, .blocked)
|
||||
case .delete:
|
||||
let name = entry.domain
|
||||
let flag = (source.parent != nil)
|
||||
AlertDeleteLogs(name, latest: entry.lastModified) {
|
||||
TheGreatDestroyer.deleteLogs(domain: name, since: $0, strict: flag)
|
||||
}.presentIn(self)
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
private func showFilterSheet(_ entry: GroupedDomain, _ filter: FilterOptions) {
|
||||
if entry.options?.contains(filter) ?? false {
|
||||
DomainFilter.update(entry.domain, remove: filter)
|
||||
} else {
|
||||
// TODO: alert sheet
|
||||
DomainFilter.update(entry.domain, add: filter)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: Extensions
|
||||
extension TVCDomains : GroupedDomainEditRow {
|
||||
override func tableView(_ tableView: UITableView, editActionsForRowAt indexPath: IndexPath) -> [UITableViewRowAction]? {
|
||||
getRowActionsIOS9(indexPath, tableView)
|
||||
}
|
||||
@available(iOS 11.0, *)
|
||||
override func tableView(_ tableView: UITableView, trailingSwipeActionsConfigurationForRowAt indexPath: IndexPath) -> UISwipeActionsConfiguration? {
|
||||
getRowActionsIOS11(indexPath)
|
||||
}
|
||||
}
|
||||
|
||||
extension TVCHosts : GroupedDomainEditRow {
|
||||
override func tableView(_ tableView: UITableView, editActionsForRowAt indexPath: IndexPath) -> [UITableViewRowAction]? {
|
||||
getRowActionsIOS9(indexPath, tableView)
|
||||
}
|
||||
@available(iOS 11.0, *)
|
||||
override func tableView(_ tableView: UITableView, trailingSwipeActionsConfigurationForRowAt indexPath: IndexPath) -> UISwipeActionsConfiguration? {
|
||||
getRowActionsIOS11(indexPath)
|
||||
}
|
||||
}
|
||||
73
main/Data Source/RecordingsDB.swift
Normal file
@@ -0,0 +1,73 @@
|
||||
import Foundation
|
||||
|
||||
enum RecordingsDB {
|
||||
/// Get last started recording (where `start` is set, but `stop` is not)
|
||||
static func getCurrent() -> Recording? { AppDB?.recordingGetOngoing() }
|
||||
|
||||
/// Create new recording and set `start` timestamp to `now()`
|
||||
static func startNew() -> Recording? { try? AppDB?.recordingStartNew() }
|
||||
|
||||
/// Finalize recording by setting the `stop` timestamp to `now()`
|
||||
static func stop(_ r: inout Recording) { AppDB?.recordingStop(&r) }
|
||||
|
||||
/// Get list of all recordings
|
||||
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
|
||||
static func persist(_ r: Recording) {
|
||||
sync.syncNow { // persist changes in cache before copying recording details
|
||||
AppDB?.recordingLogsPersist(r)
|
||||
}
|
||||
}
|
||||
|
||||
/// Get list of domains that occured during the recording
|
||||
static func details(_ r: Recording) -> [DomainTsPair] {
|
||||
AppDB?.recordingLogsGet(r) ?? []
|
||||
}
|
||||
|
||||
/// Get dictionary of domains with `ts` in ascending order.
|
||||
static func detailCluster(_ r: Recording) -> [String : [Timestamp]] {
|
||||
var cluster: [String : [Timestamp]] = [:]
|
||||
for (dom, ts) in details(r) {
|
||||
if cluster[dom] == nil {
|
||||
cluster[dom] = []
|
||||
}
|
||||
cluster[dom]!.append(ts - r.start)
|
||||
}
|
||||
return cluster
|
||||
}
|
||||
|
||||
/// Update `title`, `appid`, and `notes` and post `NotifyRecordingChanged` notification.
|
||||
static func update(_ r: Recording) {
|
||||
AppDB?.recordingUpdate(r)
|
||||
NotifyRecordingChanged.post((r, false))
|
||||
}
|
||||
|
||||
/// Delete whole recording including all entries and post `NotifyRecordingChanged` notification.
|
||||
static func delete(_ r: Recording) {
|
||||
if (try? AppDB?.recordingDelete(r)) == true {
|
||||
NotifyRecordingChanged.post((r, true))
|
||||
}
|
||||
}
|
||||
|
||||
/// Delete individual entries from recording while keeping the recording alive.
|
||||
/// - Returns: `true` if at least one row is deleted.
|
||||
static func deleteDetails(_ r: Recording, domain: String) -> Bool {
|
||||
((try? AppDB?.recordingLogsDelete(r.id, matchingDomain: domain)) ?? 0) > 0
|
||||
}
|
||||
|
||||
/// Delete individual entries from recording while keeping the recording alive.
|
||||
/// - Returns: `true` if at least one row is deleted.
|
||||
static func deleteSingle(_ r: Recording, domain: String, ts: Timestamp) -> Bool {
|
||||
(try? AppDB?.recordingLogsDelete(r.id, singleEntry: ts, domain: domain)) ?? false
|
||||
}
|
||||
|
||||
/// Return list of previously used apps found in all recordings.
|
||||
static func appList() -> [AppBundleInfo] {
|
||||
AppDB?.appBundleList() ?? []
|
||||
}
|
||||
}
|
||||
|
||||
65
main/Data Source/SimulatorVPN.swift
Normal file
@@ -0,0 +1,65 @@
|
||||
import Foundation
|
||||
|
||||
#if IOS_SIMULATOR
|
||||
|
||||
fileprivate var hook : GlassVPNHook!
|
||||
|
||||
class SimulatorVPN {
|
||||
static var timer: Timer?
|
||||
|
||||
static func load() {
|
||||
QLog.Debug("SQLite path: \(URL.internalDB())")
|
||||
|
||||
let db = AppDB!
|
||||
let deleted = db.dnsLogsDelete("test.com", strict: false)
|
||||
try? db.run(sql: "DELETE FROM cache;")
|
||||
QLog.Debug("Deleting \(deleted) rows matching 'test.com' (+ \(db.numberOfChanges) in cache)")
|
||||
|
||||
QLog.Debug("Writing 33 test logs")
|
||||
try? db.logWrite("keeptest.com", blocked: false)
|
||||
for _ in 1...4 { try? db.logWrite("test.com", blocked: false) }
|
||||
for _ in 1...7 { try? db.logWrite("i.test.com", blocked: false) }
|
||||
for i in 1...8 { try? db.logWrite("b.test.com", blocked: i>5) }
|
||||
for i in 1...13 { try? db.logWrite("bi.test.com", blocked: i%2==0) }
|
||||
|
||||
db.dnsLogsPersist()
|
||||
|
||||
QLog.Debug("Creating 4 filters")
|
||||
db.setFilter("b.test.com", .blocked)
|
||||
db.setFilter("i.test.com", .ignored)
|
||||
db.setFilter("bi.test.com", [.blocked, .ignored])
|
||||
|
||||
QLog.Debug("Done")
|
||||
}
|
||||
|
||||
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() {
|
||||
//QLog.Debug("Inserting 1 periodic log entry")
|
||||
let rand = arc4random() % 8
|
||||
let domain: String
|
||||
switch rand {
|
||||
case 6: domain = "tmp.b.test.com"
|
||||
case 7: domain = "tmp.i.test.com"
|
||||
case 8: domain = "tmp.bi.test.com"
|
||||
default: domain = "\(rand).count.test.com"
|
||||
}
|
||||
let kill = hook.processDNSRequest(domain)
|
||||
if kill { QLog.Info("Blocked: \(domain)") }
|
||||
}
|
||||
|
||||
static func sendMsg(_ messageData: Data) {
|
||||
hook.handleAppMessage(messageData)
|
||||
}
|
||||
}
|
||||
#endif
|
||||
281
main/Data Source/SyncUpdate.swift
Normal file
@@ -0,0 +1,281 @@
|
||||
import UIKit
|
||||
|
||||
let sync = SyncUpdate(periodic: 7)
|
||||
|
||||
class SyncUpdate {
|
||||
private var lastSync: TimeInterval = 0
|
||||
private var timer: Timer!
|
||||
private var paused: Int = 1 // first start() will decrement
|
||||
|
||||
private var filterType: DateFilterKind
|
||||
private var range: SQLiteRowRange? // written in reloadRangeFromDB()
|
||||
/// `tsEarliest ?? 0`
|
||||
private var tsMin: Timestamp { tsEarliest ?? 0 }
|
||||
/// `(tsLatest + 1) ?? 0`
|
||||
private var tsMax: Timestamp { (tsLatest ?? -1) + 1 }
|
||||
|
||||
/// Returns invalid range `(-1,-1)` if collection contains no rows
|
||||
var rows: SQLiteRowRange { get { range ?? (-1,-1) } }
|
||||
private(set) var tsEarliest: Timestamp? // as set per user, not actual earliest
|
||||
private(set) var tsLatest: Timestamp? // as set per user, not actual latest
|
||||
|
||||
|
||||
fileprivate init(periodic interval: TimeInterval) {
|
||||
(filterType, tsEarliest, tsLatest) = Prefs.DateFilter.restrictions()
|
||||
reloadRangeFromDB()
|
||||
|
||||
NotifyDateFilterChanged.observe(call: #selector(didChangeDateFilter), on: self)
|
||||
timer = Timer.repeating(interval, call: #selector(periodicUpdate), on: self)
|
||||
syncNow() // because timer will only fire after interval
|
||||
}
|
||||
|
||||
/// Callback fired every `7` seconds.
|
||||
@objc private func periodicUpdate() { if paused == 0 { syncNow() } }
|
||||
|
||||
/// Callback fired when user changes `DateFilter` on root tableView controller
|
||||
@objc private func didChangeDateFilter() {
|
||||
self.pause()
|
||||
let filter = Prefs.DateFilter.restrictions()
|
||||
filterType = filter.type
|
||||
DispatchQueue.global().async {
|
||||
// Not necessary, but improve execution order (delete then insert).
|
||||
if self.tsMin <= (filter.earliest ?? 0) {
|
||||
self.set(newEarliest: filter.earliest)
|
||||
self.set(newLatest: filter.latest)
|
||||
} else {
|
||||
self.set(newLatest: filter.latest)
|
||||
self.set(newEarliest: filter.earliest)
|
||||
}
|
||||
self.continue()
|
||||
}
|
||||
}
|
||||
|
||||
/// - Warning: Always call from a background thread!
|
||||
func needsReloadDB(domain: String? = nil) {
|
||||
assert(!Thread.isMainThread)
|
||||
reloadRangeFromDB()
|
||||
if let dom = domain {
|
||||
notifyObservers { $0.syncUpdate(self, partialRemove: dom) }
|
||||
} else {
|
||||
notifyObservers { $0.syncUpdate(self, reset: rows) }
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// MARK: - Sync Now
|
||||
|
||||
/// This will immediately resume timer updates, ignoring previous `pause()` requests.
|
||||
func start() { paused = 0 }
|
||||
|
||||
/// All calls must be balanced with `continue()` calls.
|
||||
/// Can be nested within other `pause-continue` pairs.
|
||||
/// - Warning: An execution branch that results in unbalanced pairs will completely disable updates!
|
||||
func pause() { paused += 1 }
|
||||
|
||||
/// Must be balanced with a `pause()` call. A `continue()` without a `pause()` is a `nop`.
|
||||
/// - Note: Internally the sync timer keeps running. The `pause` will simply ignore execution during that time.
|
||||
func `continue`() { if paused > 0 { paused -= 1 } }
|
||||
|
||||
/// Persist logs from cache and notify all observers. (`NotifySyncInsert`)
|
||||
/// Determine rows of outdated entries that should be removed and notify observers as well. (`NotifySyncRemove`)
|
||||
/// - Note: This method is rate limited. Sync will be performed at most once per second.
|
||||
/// - Note: This method returns immediatelly. Syncing is done in a background thread.
|
||||
/// - Parameter block: **Always** called on a background thread!
|
||||
func syncNow(whenDone block: (() -> Void)? = nil) {
|
||||
let now = Date().timeIntervalSince1970
|
||||
guard (now - lastSync) > 1 else { // rate limiting
|
||||
if let b = block { DispatchQueue.global().async { b() } }
|
||||
return
|
||||
}
|
||||
lastSync = now
|
||||
self.pause() // reduce concurrent load
|
||||
DispatchQueue.global().async {
|
||||
self.internalSync()
|
||||
block?()
|
||||
self.continue()
|
||||
}
|
||||
}
|
||||
|
||||
/// Called by `syncNow()`. Split to a separate func to reduce `self.` cluttering
|
||||
private func internalSync() {
|
||||
assert(!Thread.isMainThread)
|
||||
// Always persist logs ...
|
||||
if let newest = AppDB?.dnsLogsPersist() { // move cache -> heap
|
||||
if filterType == .ABRange {
|
||||
// ... even if we filter a few later
|
||||
if let r = rows(tsMin, tsMax, scope: newest) {
|
||||
notify(insert: r, .Latest)
|
||||
}
|
||||
} else {
|
||||
notify(insert: newest, .Latest)
|
||||
}
|
||||
}
|
||||
if filterType == .LastXMin {
|
||||
set(newEarliest: Timestamp.past(minutes: Prefs.DateFilter.LastXMin))
|
||||
}
|
||||
// TODO: periodic hard delete old logs (will reset rowids!)
|
||||
}
|
||||
|
||||
|
||||
// MARK: - Internal
|
||||
|
||||
private func rows(_ ts1: Timestamp, _ ts2: Timestamp, scope: SQLiteRowRange = (0,0)) -> SQLiteRowRange? {
|
||||
AppDB?.dnsLogsRowRange(between: ts1, and: ts2, within: scope)
|
||||
}
|
||||
|
||||
private func reloadRangeFromDB() {
|
||||
// `nil` is not SQLiteRowRange(0,0) aka. full collection.
|
||||
// `nil` means invalid range. e.g. ts restriction too high or empty db.
|
||||
range = rows(tsMin, tsMax)
|
||||
}
|
||||
|
||||
/// Update internal `tsEarliest`, then post `NotifySyncInsert` or `NotifySyncRemove` notification with row ids.
|
||||
/// - Warning: Always call from a background thread!
|
||||
private func set(newEarliest: Timestamp?) {
|
||||
func from(_ t: Timestamp?) -> Timestamp { t ?? 0 }
|
||||
func to(_ t: Timestamp) -> Timestamp { tsLatest == nil ? t : min(t, tsMax) }
|
||||
|
||||
if let (old, new) = tsEarliest <-/ newEarliest {
|
||||
if old != nil, (new == nil || new! < old!) {
|
||||
if let r = rows(from(new), to(old!), scope: (0, range?.start ?? 0)) {
|
||||
notify(insert: r, .Earliest)
|
||||
}
|
||||
} else if range != nil {
|
||||
if let r = rows(from(old), to(new!), scope: range!) {
|
||||
notify(remove: r, .Earliest)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Update internal `tsLatest`, then post `NotifySyncInsert` or `NotifySyncRemove` notification with row ids.
|
||||
/// - Warning: Always call from a background thread!
|
||||
private func set(newLatest: Timestamp?) {
|
||||
func from(_ t: Timestamp) -> Timestamp { max(t + 1, tsMin) }
|
||||
func to(_ t: Timestamp?) -> Timestamp { t == nil ? 0 : t! + 1 }
|
||||
// +1: include upper end because `dnsLogsRowRange` selects `ts < X`
|
||||
|
||||
if let (old, new) = tsLatest <-/ newLatest {
|
||||
if old != nil, (new == nil || old! < new!) {
|
||||
if let r = rows(from(old!), to(new), scope: (range?.end ?? 0, 0)) {
|
||||
notify(insert: r, .Latest)
|
||||
}
|
||||
} else if range != nil {
|
||||
if let r = rows(from(new!), to(old), scope: range!) {
|
||||
notify(remove: r, .Latest)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// - Warning: Always call from a background thread!
|
||||
private func notify(insert r: SQLiteRowRange, _ end: SyncUpdateEnd) {
|
||||
if range == nil { range = r }
|
||||
else {
|
||||
switch end {
|
||||
case .Earliest: range!.start = r.start
|
||||
case .Latest: range!.end = r.end
|
||||
}
|
||||
}
|
||||
notifyObservers { $0.syncUpdate(self, insert: r, affects: end) }
|
||||
}
|
||||
|
||||
/// - Warning: `range` must not be `nil`!
|
||||
/// - Warning: Always call from a background thread!
|
||||
private func notify(remove r: SQLiteRowRange, _ end: SyncUpdateEnd) {
|
||||
switch end {
|
||||
case .Earliest: range!.start = r.end + 1
|
||||
case .Latest: range!.end = r.start - 1
|
||||
}
|
||||
if range!.start > range!.end { range = nil }
|
||||
notifyObservers { $0.syncUpdate(self, remove: r, affects: end) }
|
||||
}
|
||||
|
||||
|
||||
// MARK: - Observer List
|
||||
|
||||
private var observers: [WeakObserver] = []
|
||||
|
||||
/// Add `delegate` to observer list and immediatelly call `syncUpdate(reset:)` (on background thread).
|
||||
func addObserver(_ delegate: SyncUpdateDelegate) {
|
||||
observers.removeAll { $0.target == nil }
|
||||
observers.append(.init(target: delegate))
|
||||
DispatchQueue.global().async {
|
||||
delegate.syncUpdate(self, reset: self.rows)
|
||||
}
|
||||
}
|
||||
|
||||
/// - Warning: Always call from a background thread!
|
||||
private func notifyObservers(_ block: (SyncUpdateDelegate) -> Void) {
|
||||
assert(!Thread.isMainThread)
|
||||
self.pause()
|
||||
for o in observers where o.target != nil { block(o.target!) }
|
||||
self.continue()
|
||||
}
|
||||
}
|
||||
|
||||
/// Wrapper class for `SyncUpdateDelegate` that supports weak references
|
||||
private struct WeakObserver {
|
||||
weak var target: SyncUpdateDelegate?
|
||||
weak var pullToRefresh: UIRefreshControl?
|
||||
}
|
||||
|
||||
enum SyncUpdateEnd { case Earliest, Latest }
|
||||
|
||||
protocol SyncUpdateDelegate : AnyObject {
|
||||
/// `SyncUpdate` has unpredictable changes. Reload your `dataSource`.
|
||||
/// - Warning: This function will **always** be called from a background thread.
|
||||
func syncUpdate(_ sender: SyncUpdate, reset rows: SQLiteRowRange)
|
||||
|
||||
/// `SyncUpdate` added new `rows` to database. Sync changes to your `dataSource`.
|
||||
/// - Warning: This function will **always** be called from a background thread.
|
||||
func syncUpdate(_ sender: SyncUpdate, insert rows: SQLiteRowRange, affects: SyncUpdateEnd)
|
||||
|
||||
/// `SyncUpdate` outdated some `rows` in database. Sync changes to your `dataSource`.
|
||||
/// - Warning: This function will **always** be called from a background thread.
|
||||
func syncUpdate(_ sender: SyncUpdate, remove rows: SQLiteRowRange, affects: SyncUpdateEnd)
|
||||
|
||||
/// Background process did delete some entries in database that match `affectedDomain`.
|
||||
/// Update or remove entries from your `dataSource`.
|
||||
/// - Warning: This function will **always** be called from a background thread.
|
||||
func syncUpdate(_ sender: SyncUpdate, partialRemove affectedDomain: String)
|
||||
}
|
||||
|
||||
|
||||
// MARK: - Pull-To-Refresh
|
||||
|
||||
@available(iOS 10.0, *)
|
||||
extension SyncUpdate {
|
||||
|
||||
/// Add Pull-To-Refresh control to `tableViewController`. On action notify `observer.syncUpdate(reset:)`
|
||||
/// - Warning: Must be called after `addObserver()` such that `observer` exists in list of observers.
|
||||
func allowPullToRefresh(onTVC tableViewController: UITableViewController?, forObserver: SyncUpdateDelegate) {
|
||||
guard let i = observers.firstIndex(where: { $0.target === forObserver }) else {
|
||||
assertionFailure("You must add the observer before enabling Pull-To-Refresh!")
|
||||
return
|
||||
}
|
||||
// remove previous
|
||||
observers[i].pullToRefresh?.removeTarget(self, action: #selector(pullToRefresh), for: .valueChanged)
|
||||
observers[i].pullToRefresh = nil
|
||||
if let tvc = tableViewController {
|
||||
let rc = UIRefreshControl()
|
||||
rc.addTarget(self, action: #selector(pullToRefresh), for: .valueChanged)
|
||||
tvc.tableView.refreshControl = rc
|
||||
observers[i].pullToRefresh = rc
|
||||
}
|
||||
}
|
||||
|
||||
/// Pull-To-Refresh callback method. Find observer with corresponding `RefreshControl` and notify `syncUpdate(reset:)`
|
||||
@objc private func pullToRefresh(sender: UIRefreshControl) {
|
||||
guard let x = observers.first(where: { $0.pullToRefresh === sender }) else {
|
||||
assertionFailure("Should never happen. RefreshControl removed from table view while keeping it active somewhere else.")
|
||||
return
|
||||
}
|
||||
syncNow {
|
||||
x.target?.syncUpdate(self, reset: self.rows)
|
||||
DispatchQueue.main.sync {
|
||||
sender.endRefreshing()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,8 +1,8 @@
|
||||
import UIKit
|
||||
|
||||
extension UIAlertController {
|
||||
func presentIn(_ viewController: UIViewController?) {
|
||||
viewController?.present(self, animated: true, completion: nil)
|
||||
func presentIn(_ viewController: UIViewController) {
|
||||
viewController.present(self, animated: true)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -31,19 +31,28 @@ func ErrorAlert(_ errorDescription: String, buttonText: String = "Dismiss") -> U
|
||||
/// - Parameters:
|
||||
/// - buttonText: Default: `"Continue"`
|
||||
/// - buttonStyle: Default: `.default`
|
||||
func AskAlert(title: String?, text: String?, buttonText: String = "Continue", buttonStyle: UIAlertAction.Style = .default, action: @escaping (UIAlertController) -> Void) -> UIAlertController {
|
||||
let alert = Alert(title: title, text: text, buttonText: "Cancel")
|
||||
func AskAlert(title: String?, text: String?, buttonText: String = "Continue", cancelButton: String = "Cancel", buttonStyle: UIAlertAction.Style = .default, action: @escaping (UIAlertController) -> Void) -> UIAlertController {
|
||||
let alert = Alert(title: title, text: text, buttonText: cancelButton)
|
||||
alert.addAction(UIAlertAction(title: buttonText, style: buttonStyle) { _ in action(alert) })
|
||||
return alert
|
||||
}
|
||||
|
||||
/// Show alert hinting the user to go to system settings and re-enable notifications.
|
||||
func NotificationsDisabledAlert(presentIn viewController: UIViewController) {
|
||||
AskAlert(title: "Notifications Disabled",
|
||||
text: "Go to System Settings > Notifications > AppCheck to re-enable notifications.",
|
||||
buttonText: "Open settings") { _ in
|
||||
URL(string: UIApplication.openSettingsURLString)?.open()
|
||||
}.presentIn(viewController)
|
||||
}
|
||||
|
||||
// MARK: Alert with multiple options
|
||||
|
||||
/// - Parameters:
|
||||
/// - buttons: Default: `[]`
|
||||
/// - lastIsDestructive: Default: `false`
|
||||
/// - cancelButtonText: Default: `"Dismiss"`
|
||||
func BottomAlert(title: String?, text: String?, buttons: [String] = [], lastIsDestructive: Bool = false, cancelButtonText: String = "Dismiss", callback: @escaping (_ index: Int?) -> Void) -> UIAlertController {
|
||||
func BottomAlert(title: String?, text: String?, buttons: [String] = [], lastIsDestructive: Bool = false, cancelButtonText: String = "Cancel", callback: @escaping (_ index: Int?) -> Void) -> UIAlertController {
|
||||
let alert = UIAlertController(title: title, message: text, preferredStyle: .actionSheet)
|
||||
for (i, btn) in buttons.enumerated() {
|
||||
let dangerous = (lastIsDestructive && i + 1 == buttons.count)
|
||||
@@ -54,21 +63,13 @@ func BottomAlert(title: String?, text: String?, buttons: [String] = [], lastIsDe
|
||||
}
|
||||
|
||||
func AlertDeleteLogs(_ domain: String, latest: Timestamp, success: @escaping (_ tsMin: Timestamp) -> Void) -> UIAlertController {
|
||||
let sinceNow = Timestamp.now() - latest
|
||||
var buttons = ["Last 5 minutes", "Last 15 minutes", "Last hour", "Last 24 hours", "Delete everything"]
|
||||
var times: [Timestamp] = [300, 900, 3600, 86400]
|
||||
while times.count > 0, times[0] < sinceNow {
|
||||
buttons.removeFirst()
|
||||
times.removeFirst()
|
||||
}
|
||||
return BottomAlert(title: "Delete logs", text: "Delete logs for domain '\(domain)'", buttons: buttons, lastIsDestructive: true, cancelButtonText: "Cancel") {
|
||||
guard let idx = $0 else {
|
||||
return
|
||||
}
|
||||
if idx >= times.count {
|
||||
success(0)
|
||||
} else {
|
||||
success(Timestamp(Date().timeIntervalSince1970) - times[idx])
|
||||
let minutesPassed = (Timestamp.now() - latest) / 60
|
||||
let times: [Int] = [5, 15, 60, 1440].compactMap { minutesPassed < $0 ? $0 : nil }
|
||||
let fmt = TimeFormat(.full, allowed: [.hour, .minute])
|
||||
let labels = times.map { "Last " + (fmt.from(minutes: $0) ?? "?") }
|
||||
return BottomAlert(title: "Delete logs", text: "Delete logs for domain '\(domain)'", buttons: labels + ["Delete everything"], lastIsDestructive: true) {
|
||||
if let i = $0 {
|
||||
success(i < times.count ? Timestamp.past(minutes: times[i]) : 0)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
88
main/Extensions/Array.swift
Normal file
@@ -0,0 +1,88 @@
|
||||
import Foundation
|
||||
|
||||
//extension Collection {
|
||||
// subscript(ifExist i: Index?) -> Iterator.Element? {
|
||||
// guard let i = i else { return nil }
|
||||
// return indices.contains(i) ? self[i] : nil
|
||||
// }
|
||||
//}
|
||||
|
||||
extension Range where Bound == Int {
|
||||
@inline(__always) func arr() -> [Bound] { self.map { $0 } }
|
||||
}
|
||||
|
||||
|
||||
// MARK: - Sorted Array
|
||||
|
||||
extension Array {
|
||||
typealias CompareFn = (Element, Element) -> Bool
|
||||
|
||||
/// Binary tree search operation.
|
||||
/// - Warning: Array must be sorted already.
|
||||
/// - Parameters:
|
||||
/// - mustExist: Determine whether to return low index or `nil` if element is missing.
|
||||
/// - first: If `true`, keep searching for first matching element.
|
||||
/// - Returns: Index or `nil` (only if `mustExist = true` and element does not exist).
|
||||
/// - Complexity: O(log *n*), where *n* is the length of the array.
|
||||
func binTreeIndex(of element: Element, compare fn: CompareFn, mustExist: Bool = false, findFirst: Bool = false) -> Int? {
|
||||
var found = false
|
||||
var lo = 0, hi = self.count - 1
|
||||
while lo <= hi {
|
||||
let mid = (lo + hi)/2
|
||||
if fn(self[mid], element) {
|
||||
lo = mid + 1
|
||||
} else if fn(element, self[mid]) {
|
||||
hi = mid - 1
|
||||
} else {
|
||||
if !findFirst { return mid } // exit early if we dont care about first index
|
||||
hi = mid - 1
|
||||
found = true
|
||||
}
|
||||
}
|
||||
return (mustExist && !found) ? nil : lo // not found, would be inserted at position lo
|
||||
}
|
||||
|
||||
/// Binary tree lookup whether element exists. Performs `binTreeIndex(of:compare:mustExist:)` internally.
|
||||
func binTreeExists(_ element: Element, compare fn: CompareFn) -> Bool {
|
||||
binTreeIndex(of: element, compare: fn, mustExist: true) != nil
|
||||
}
|
||||
|
||||
/// Binary tree insert operation
|
||||
/// - Warning: Array must be sorted already.
|
||||
/// - Returns: Index at which `elem` was inserted
|
||||
/// - Complexity: O(log *n*), where *n* is the length of the array.
|
||||
@discardableResult mutating func binTreeInsert(_ elem: Element, compare fn: CompareFn) -> Int {
|
||||
let newIndex = binTreeIndex(of: elem, compare: fn)!
|
||||
insert(elem, at: newIndex)
|
||||
return newIndex
|
||||
}
|
||||
|
||||
/// Binary tree remove operation
|
||||
/// - Warning: Array must be sorted already.
|
||||
/// - Returns: Index of removed `elem` or `nil` if it does not exist
|
||||
/// - Complexity: O(log *n*), where *n* is the length of the array.
|
||||
@discardableResult mutating func binTreeRemove(_ elem: Element, compare fn: CompareFn) -> Int? {
|
||||
if let i = binTreeIndex(of: elem, compare: fn, mustExist: true) {
|
||||
remove(at: i)
|
||||
return i
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
/// Sorted synchronous comparison between elements
|
||||
/// - Parameter sortedSubset: Must be a strict subset of the sorted array.
|
||||
/// - Returns: List of elements that are **not** present in `sortedSubset`.
|
||||
/// - Complexity: O(*m*+*n*), where *n* is the length of the array and *m* the length of the `sortedSubset`.
|
||||
/// If indices are found earlier, *n* may be significantly less (on average: `n/2`)
|
||||
func difference(toSubset sortedSubset: [Element], compare fn: CompareFn) -> [Element] {
|
||||
var result: [Element] = []
|
||||
var iter = makeIterator()
|
||||
for rhs in sortedSubset {
|
||||
while let lhs = iter.next(), fn(lhs, rhs) {
|
||||
result.append(lhs)
|
||||
}
|
||||
}
|
||||
result.append(contentsOf: iter)
|
||||
return result
|
||||
}
|
||||
}
|
||||
@@ -47,6 +47,15 @@ extension NSLayoutConstraint {
|
||||
@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
|
||||
|
||||
@@ -59,6 +68,7 @@ extension UIView {
|
||||
private static let inverseItem: [NSLayoutConstraint.Attribute] = [.right, .bottom, .trailing, .lastBaseline, .rightMargin, .bottomMargin, .trailingMargin]
|
||||
|
||||
/// Create and active constraints for provided edges. Constraints will anchor the same edge on both `self` and `other`.
|
||||
/// - Note: Will set `translatesAutoresizingMaskIntoConstraints = false`
|
||||
/// - Parameters:
|
||||
/// - edges: List of constraint attributes, e.g. `[.top, .bottom, .left, .right]`
|
||||
/// - other: Instance to bind to, e.g. `UIView` or `UILayoutGuide`
|
||||
@@ -66,11 +76,18 @@ extension UIView {
|
||||
/// - rel: Constraint relation. (Default: `.equal`)
|
||||
/// - Returns: List of created and active constraints
|
||||
@discardableResult func anchor(_ edges: [NSLayoutConstraint.Attribute], to other: Any, margin: CGFloat = 0, if rel: NSLayoutConstraint.Relation = .equal) -> [NSLayoutConstraint] {
|
||||
edges.map {
|
||||
translatesAutoresizingMaskIntoConstraints = false
|
||||
return edges.map {
|
||||
let (A, B) = UIView.inverseItem.contains($0) ? (other, self) : (self, other)
|
||||
return NSLayoutConstraint(item: A, attribute: $0, relatedBy: rel, toItem: B, attribute: $0, multiplier: 1, constant: margin).on()
|
||||
}
|
||||
}
|
||||
|
||||
/// 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 {
|
||||
|
||||
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
|
||||
}
|
||||
}
|
||||
@@ -1,32 +0,0 @@
|
||||
import Foundation
|
||||
|
||||
extension GroupedDomain {
|
||||
static func +(a: GroupedDomain, b: GroupedDomain) -> Self {
|
||||
GroupedDomain(domain: a.domain, total: a.total + b.total, blocked: a.blocked + b.blocked,
|
||||
lastModified: max(a.lastModified, b.lastModified), options: a.options ?? b.options )
|
||||
}
|
||||
}
|
||||
|
||||
extension Array where Element == GroupedDomain {
|
||||
func merge(_ domain: String, options opt: FilterOptions? = nil) -> GroupedDomain {
|
||||
var b: Int32 = 0, t: Int32 = 0, m: Timestamp = 0
|
||||
for x in self {
|
||||
b += x.blocked
|
||||
t += x.total
|
||||
m = Swift.max(m, x.lastModified)
|
||||
}
|
||||
return GroupedDomain(domain: domain, total: t, blocked: b, lastModified: m, options: opt)
|
||||
}
|
||||
}
|
||||
|
||||
extension Recording {
|
||||
var fallbackTitle: String { get { "Unnamed Recording #\(id)" } }
|
||||
var duration: Timestamp? { get { stop == nil ? nil : stop! - start } }
|
||||
var durationString: String? { get { stop == nil ? nil : TimeFormat.from(duration!) } }
|
||||
}
|
||||
|
||||
extension Timestamp {
|
||||
func asDateTime() -> String { dateTimeFormat.string(from: self) }
|
||||
func toDate() -> Date { Date(timeIntervalSince1970: Double(self)) }
|
||||
static func now() -> Timestamp { Timestamp(Date().timeIntervalSince1970) }
|
||||
}
|
||||
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
|
||||
}
|
||||
}
|
||||
48
main/Extensions/Font.swift
Normal file
@@ -0,0 +1,48 @@
|
||||
import UIKit
|
||||
|
||||
extension UIFont {
|
||||
func withTraits(traits: UIFontDescriptor.SymbolicTraits) -> UIFont {
|
||||
UIFont(descriptor: fontDescriptor.withSymbolicTraits(traits)!, size: 0) // keep size as is
|
||||
}
|
||||
func bold() -> UIFont { withTraits(traits: .traitBold) }
|
||||
func italic() -> UIFont { withTraits(traits: .traitItalic) }
|
||||
func boldItalic() -> UIFont { withTraits(traits: [.traitBold, .traitItalic]) }
|
||||
func monoSpace() -> UIFont {
|
||||
let traits = fontDescriptor.object(forKey: .traits) as? [UIFontDescriptor.TraitKey: Any] ?? [:]
|
||||
let weight = (traits[.weight] as? CGFloat) ?? UIFont.Weight.regular.rawValue
|
||||
return .monospacedDigitSystemFont(ofSize: pointSize, weight: .init(rawValue: weight))
|
||||
}
|
||||
}
|
||||
|
||||
extension NSMutableAttributedString {
|
||||
convenience init(image: UIImage, centered: Bool = false) {
|
||||
self.init()
|
||||
let att = NSTextAttachment()
|
||||
att.image = image
|
||||
append(.init(attachment: att))
|
||||
if centered {
|
||||
let ps = NSMutableParagraphStyle()
|
||||
ps.alignment = .center
|
||||
addAttribute(.paragraphStyle, value: ps, range: .init(location: 0, length: length))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
extension NSMutableAttributedString {
|
||||
@discardableResult func normal(_ str: String, _ style: UIFont.TextStyle = .body) -> Self { append(str, withFont: .preferredFont(forTextStyle: style)) }
|
||||
@discardableResult func bold(_ str: String, _ style: UIFont.TextStyle = .body) -> Self { append(str, withFont: UIFont.preferredFont(forTextStyle: style).bold()) }
|
||||
@discardableResult func italic(_ str: String, _ style: UIFont.TextStyle = .body) -> Self { append(str, withFont: UIFont.preferredFont(forTextStyle: style).italic()) }
|
||||
@discardableResult func boldItalic(_ str: String, _ style: UIFont.TextStyle = .body) -> Self { append(str, withFont: UIFont.preferredFont(forTextStyle: style).boldItalic()) }
|
||||
|
||||
@discardableResult func h1(_ str: String) -> Self { normal(str, .title1) }
|
||||
@discardableResult func h2(_ str: String) -> Self { normal(str, .title2) }
|
||||
@discardableResult func h3(_ str: String) -> Self { normal(str, .title3) }
|
||||
|
||||
private func append(_ str: String, withFont: UIFont) -> Self {
|
||||
append(NSAttributedString(string: str, attributes: [
|
||||
.font : withFont,
|
||||
.foregroundColor : UIColor.sysLabel
|
||||
]))
|
||||
return self
|
||||
}
|
||||
}
|
||||
@@ -1,113 +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 Collection {
|
||||
subscript(ifExist i: Index?) -> Iterator.Element? {
|
||||
guard let i = i else { return nil }
|
||||
return indices.contains(i) ? self[i] : nil
|
||||
}
|
||||
}
|
||||
|
||||
var listOfSLDs: [String : [String : Bool]] = {
|
||||
let path = Bundle.main.url(forResource: "third-level", withExtension: "txt")
|
||||
let content = try! String(contentsOf: path!)
|
||||
var res: [String : [String : Bool]] = [:]
|
||||
content.enumerateLines { line, _ in
|
||||
let dom = line.split(separator: ".")
|
||||
let tld = String(dom.first!)
|
||||
let sld = String(dom.last!)
|
||||
if res[tld] == nil { res[tld] = [:] }
|
||||
res[tld]![sld] = true
|
||||
}
|
||||
return res
|
||||
}()
|
||||
|
||||
extension String {
|
||||
/// Check if string is equal to `domain` or ends with `.domain`
|
||||
func isSubdomain(of domain: String) -> Bool { self == domain || self.hasSuffix("." + domain) }
|
||||
/// Split string into top level domain part and host part
|
||||
func splitDomainAndHost() -> (domain: String, host: String?) {
|
||||
let lastChr = last?.asciiValue ?? 0
|
||||
guard lastChr > UInt8(ascii: "9") || lastChr < UInt8(ascii: "0") else { // IP address
|
||||
return (domain: "# IP connection", host: self)
|
||||
}
|
||||
var parts = components(separatedBy: ".")
|
||||
guard let tld = parts.popLast(), let sld = parts.popLast() else {
|
||||
return (domain: self, host: nil) // no subdomains, just plain SLD
|
||||
}
|
||||
var ending = sld + "." + tld
|
||||
if listOfSLDs[tld]?[sld] ?? false, let rld = parts.popLast() {
|
||||
ending = rld + "." + ending
|
||||
}
|
||||
return (domain: ending, host: parts.joined(separator: "."))
|
||||
}
|
||||
/// Returns `true` if String matches list of known second level domains (e.g., `co.uk`).
|
||||
func isKnownSLD() -> Bool {
|
||||
let parts = components(separatedBy: ".")
|
||||
return parts.count == 2 && listOfSLDs[parts.last!]?[parts.first!] ?? false
|
||||
}
|
||||
}
|
||||
|
||||
extension Timer {
|
||||
@discardableResult static func repeating(_ interval: TimeInterval, call selector: Selector, on target: Any, userInfo: Any? = nil) -> Timer {
|
||||
Timer.scheduledTimer(timeInterval: interval, target: target, selector: selector,
|
||||
userInfo: userInfo, repeats: true)
|
||||
}
|
||||
}
|
||||
|
||||
extension DateFormatter {
|
||||
convenience init(withFormat: String) {
|
||||
self.init()
|
||||
dateFormat = withFormat
|
||||
}
|
||||
func with(format: String) -> Self {
|
||||
dateFormat = format
|
||||
return self
|
||||
}
|
||||
func string(from ts: Timestamp) -> String {
|
||||
string(from: Date.init(timeIntervalSince1970: Double(ts)))
|
||||
}
|
||||
}
|
||||
|
||||
struct TimeFormat {
|
||||
static func from(_ duration: Timestamp) -> String {
|
||||
String(format: "%02d:%02d", duration / 60, duration % 60)
|
||||
}
|
||||
static func from(_ duration: TimeInterval, millis: Bool = false) -> String {
|
||||
let t = Int(duration)
|
||||
if millis {
|
||||
let mil = Int(duration * 1000) % 1000
|
||||
return String(format: "%02d:%02d.%03d", t / 60, t % 60, mil)
|
||||
}
|
||||
return String(format: "%02d:%02d", t / 60, t % 60)
|
||||
}
|
||||
static func since(_ date: Date, millis: Bool = false) -> String {
|
||||
from(Date().timeIntervalSince(date), millis: millis)
|
||||
}
|
||||
}
|
||||
|
||||
extension UIColor {
|
||||
static var sysBg: UIColor { get { if #available(iOS 13.0, *) { return .systemBackground } else { return .white } }}
|
||||
static var sysFg: UIColor { get { if #available(iOS 13.0, *) { return .label } else { return .black } }}
|
||||
}
|
||||
|
||||
extension UIEdgeInsets {
|
||||
init(all: CGFloat = 0, top: CGFloat? = nil, left: CGFloat? = nil, bottom: CGFloat? = nil, right: CGFloat? = nil) {
|
||||
self.init(top: top ?? all, left: left ?? all, bottom: bottom ?? all, right: right ?? all)
|
||||
}
|
||||
}
|
||||
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,8 +1,9 @@
|
||||
import Foundation
|
||||
|
||||
let NotifyVPNStateChanged = NSNotification.Name("GlassVPNStateChanged") // VPNState!
|
||||
let NotifyFilterChanged = NSNotification.Name("PSIFilterSettingsChanged") // nil!
|
||||
let NotifyLogHistoryReset = NSNotification.Name("PSILogHistoryReset") // nil!
|
||||
let NotifyVPNStateChanged = NSNotification.Name("GlassVPNStateChanged") // nil!
|
||||
let NotifyDNSFilterChanged = NSNotification.Name("PSIDNSFilterSettingsChanged") // domain: String!
|
||||
let NotifyDateFilterChanged = NSNotification.Name("PSIDateFilterSettingsChanged") // nil!
|
||||
let NotifySortOrderChanged = NSNotification.Name("PSIDateFilterSortOrderChanged") // nil!
|
||||
let NotifyRecordingChanged = NSNotification.Name("PSIRecordingChanged") // (Recording, deleted: Bool)!
|
||||
|
||||
|
||||
|
||||
@@ -1,8 +0,0 @@
|
||||
import Foundation
|
||||
|
||||
let dateTimeFormat = DateFormatter(withFormat: "yyyy-MM-dd HH:mm:ss")
|
||||
var currentVPNState: VPNState = .off
|
||||
|
||||
public enum VPNState : Int {
|
||||
case on = 1, inbetween, off
|
||||
}
|
||||
55
main/Extensions/String.swift
Normal file
@@ -0,0 +1,55 @@
|
||||
import UIKit
|
||||
|
||||
extension String {
|
||||
/// Check if string is equal to `domain` or ends with `.domain`
|
||||
func isSubdomain(of domain: String) -> Bool { self == domain || self.hasSuffix("." + domain) }
|
||||
|
||||
/// Extract second or third level domain name
|
||||
func extractDomain() -> String {
|
||||
let lastChr = last?.asciiValue ?? 0
|
||||
guard lastChr > UInt8(ascii: "9") || lastChr < UInt8(ascii: "0") else { // IP address
|
||||
return "# IP"
|
||||
}
|
||||
var parts = components(separatedBy: ".")
|
||||
guard let tld = parts.popLast(), let sld = parts.popLast() else {
|
||||
return self // no subdomains, just plain SLD
|
||||
}
|
||||
if listOfSLDs[tld]?[sld] ?? false, let rld = parts.popLast() {
|
||||
return rld + "." + sld + "." + tld
|
||||
}
|
||||
return sld + "." + tld
|
||||
}
|
||||
|
||||
/// Returns `true` if String matches list of known second level domains (e.g., `co.uk`).
|
||||
func isKnownSLD() -> Bool {
|
||||
let parts = components(separatedBy: ".")
|
||||
return parts.count == 2 && listOfSLDs[parts.last!]?[parts.first!] ?? false
|
||||
}
|
||||
|
||||
func isValidBundleId() -> Bool {
|
||||
let regex = try! NSRegularExpression(pattern: #"^[A-Za-z0-9\.\-]{1,155}$"#, options: .anchorsMatchLines)
|
||||
let range = NSRange(location: 0, length: self.utf16.count)
|
||||
let matches = regex.matches(in: self, options: .anchored, range: range)
|
||||
return matches.count == 1
|
||||
}
|
||||
}
|
||||
|
||||
private var listOfSLDs: [String : [String : Bool]] = {
|
||||
let path = Bundle.main.url(forResource: "third-level", withExtension: "txt")
|
||||
let content = try! String(contentsOf: path!)
|
||||
var res: [String : [String : Bool]] = [:]
|
||||
content.enumerateLines { line, _ in
|
||||
let dom = line.split(separator: ".")
|
||||
let tld = String(dom.first!)
|
||||
let sld = String(dom.last!)
|
||||
if res[tld] == nil { res[tld] = [:] }
|
||||
res[tld]![sld] = true
|
||||
}
|
||||
return res
|
||||
}()
|
||||
|
||||
extension NSString {
|
||||
func substring(from: Int, to: Int) -> String {
|
||||
substring(with: NSRange(location: from, length: to - from))
|
||||
}
|
||||
}
|
||||
@@ -1,98 +1,123 @@
|
||||
import UIKit
|
||||
|
||||
extension GroupedDomain {
|
||||
var detailCellText: String { get {
|
||||
return blocked > 0
|
||||
? "\(lastModified.asDateTime()) — \(blocked)/\(total) blocked"
|
||||
: "\(lastModified.asDateTime()) — \(total)"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
extension FilterOptions {
|
||||
func tableRowImage() -> UIImage? {
|
||||
let blocked = contains(.blocked)
|
||||
let ignored = contains(.ignored)
|
||||
if blocked { return UIImage(named: ignored ? "block_ignore" : "shield-x") }
|
||||
if ignored { return UIImage(named: "quicklook-not") }
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: Pull-to-Refresh
|
||||
|
||||
extension UIRefreshControl {
|
||||
convenience init(call: Selector, on: UITableViewController) {
|
||||
self.init()
|
||||
addTarget(on, action: call, for: .valueChanged)
|
||||
addTarget(self, action: #selector(endRefreshing), for: .valueChanged)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// MARK: - Incremental Update Delegate
|
||||
|
||||
protocol IncrementalDataSourceUpdate : UITableViewController {
|
||||
var dataSource: [GroupedDomain] { get set }
|
||||
}
|
||||
|
||||
extension IncrementalDataSourceUpdate {
|
||||
func ifDisplayed(_ block: () -> Void) {
|
||||
DispatchQueue.main.sync {
|
||||
if self.tableView.window?.isKeyWindow ?? false {
|
||||
block()
|
||||
// TODO: custom handling if cell is being edited
|
||||
} else {
|
||||
self.tableView.reloadData()
|
||||
}
|
||||
}
|
||||
}
|
||||
func insertRow(_ obj: GroupedDomain, at index: Int) {
|
||||
dataSource.insert(obj, at: index)
|
||||
ifDisplayed {
|
||||
self.tableView.insertRows(at: [IndexPath(row: index)], with: .left)
|
||||
}
|
||||
}
|
||||
func moveRow(_ obj: GroupedDomain, from: Int, to: Int) {
|
||||
dataSource.remove(at: from)
|
||||
dataSource.insert(obj, at: to)
|
||||
ifDisplayed {
|
||||
let source = IndexPath(row: from)
|
||||
let cell = self.tableView.cellForRow(at: source)
|
||||
cell?.detailTextLabel?.text = obj.detailCellText
|
||||
self.tableView.moveRow(at: source, to: IndexPath(row: to))
|
||||
}
|
||||
}
|
||||
func replaceRow(_ obj: GroupedDomain, at index: Int) {
|
||||
dataSource[index] = obj
|
||||
ifDisplayed {
|
||||
self.tableView.reloadRows(at: [IndexPath(row: index)], with: .automatic)
|
||||
}
|
||||
}
|
||||
func deleteRow(at index: Int) {
|
||||
dataSource.remove(at: index)
|
||||
ifDisplayed {
|
||||
self.tableView.deleteRows(at: [IndexPath(row: index)], with: .automatic)
|
||||
}
|
||||
}
|
||||
func replaceData(with newData: [GroupedDomain]) {
|
||||
dataSource = newData
|
||||
ifDisplayed {
|
||||
self.tableView.reloadData()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
extension IndexPath {
|
||||
/// Convenience init with `section: 0`
|
||||
public init(row: Int) { self.init(row: row, section: 0) }
|
||||
}
|
||||
|
||||
extension UIRefreshControl {
|
||||
convenience init(call: Selector, on target: Any) {
|
||||
self.init()
|
||||
addTarget(target, action: call, for: .valueChanged)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// MARK: - UITableView
|
||||
|
||||
extension UITableView {
|
||||
/// Returns `true` if this `tableView` is the currently frontmost visible
|
||||
var isFrontmost: Bool { window?.isKeyWindow ?? false }
|
||||
|
||||
/// If frontmost window, perform `insertRows()`; If not, perform `reloadData()`
|
||||
func safeInsertRow(_ index: Int, with animation: UITableView.RowAnimation = .automatic) {
|
||||
isFrontmost ? insertRows(at: [IndexPath(row: index)], with: animation) : reloadData()
|
||||
}
|
||||
/// If frontmost window, perform `insertRows()`; If not, perform `reloadData()`
|
||||
func safeInsertRows(_ range: Range<Int>, with animation: UITableView.RowAnimation = .automatic) {
|
||||
isFrontmost ? insertRows(at: range.map {IndexPath(row: $0)}, with: animation) : reloadData()
|
||||
}
|
||||
/// If frontmost window, perform `deleteRows()`; If not, perform `reloadData()`
|
||||
func safeDeleteRows(_ indices: [Int], with animation: UITableView.RowAnimation = .automatic) {
|
||||
isFrontmost ? deleteRows(at: indices.map {IndexPath(row: $0)}, with: animation) : reloadData()
|
||||
}
|
||||
/// If frontmost window, perform `deleteRows()`; If not, perform `reloadData()`
|
||||
func safeDeleteRows(_ range: Range<Int>, with animation: UITableView.RowAnimation = .automatic) {
|
||||
isFrontmost ? deleteRows(at: range.map {IndexPath(row: $0)}, with: animation) : reloadData()
|
||||
}
|
||||
/// If frontmost window, perform `reloadRows()`; If not, perform `reloadData()`
|
||||
func safeReloadRow(_ index: Int, with animation: UITableView.RowAnimation = .automatic) {
|
||||
isFrontmost ? reloadRows(at: [IndexPath(row: index)], with: animation) : reloadData()
|
||||
}
|
||||
/// If frontmost window, perform `moveRow()`; If not, perform `reloadData()`
|
||||
func safeMoveRow(_ from: Int, to: Int) {
|
||||
isFrontmost ? moveRow(at: IndexPath(row: from), to: IndexPath(row: to)) : reloadData()
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// MARK: - EditableRows
|
||||
|
||||
public enum RowAction {
|
||||
case ignore, block, delete
|
||||
}
|
||||
|
||||
protocol EditableRows {
|
||||
func editableRowUserInfo(_ index: IndexPath) -> Any?
|
||||
func editableRowActions(_ index: IndexPath) -> [(RowAction, String)]
|
||||
func editableRowActionColor(_ index: IndexPath, _ action: RowAction) -> UIColor?
|
||||
@discardableResult func editableRowCallback(_ atIndexPath: IndexPath, _ action: RowAction, _ userInfo: Any?) -> Bool
|
||||
}
|
||||
|
||||
extension EditableRows where Self: UITableViewDelegate {
|
||||
func getRowActionsIOS9(_ index: IndexPath, _ table: UITableView) -> [UITableViewRowAction]? {
|
||||
let userInfo = editableRowUserInfo(index)
|
||||
return editableRowActions(index).compactMap { a,t in
|
||||
let x = UITableViewRowAction(style: a == .delete ? .destructive : .normal, title: t) {
|
||||
self.editableRowCallback($1, a, userInfo)
|
||||
table.isEditing = false
|
||||
}
|
||||
if let color = editableRowActionColor(index, a) {
|
||||
x.backgroundColor = color
|
||||
}
|
||||
return x
|
||||
}
|
||||
}
|
||||
@available(iOS 11.0, *)
|
||||
func getRowActionsIOS11(_ index: IndexPath) -> UISwipeActionsConfiguration? {
|
||||
let userInfo = editableRowUserInfo(index)
|
||||
return UISwipeActionsConfiguration(actions: editableRowActions(index).compactMap { a,t in
|
||||
let x = UIContextualAction(style: a == .delete ? .destructive : .normal, title: t) { $2(self.editableRowCallback(index, a, userInfo)) }
|
||||
x.backgroundColor = editableRowActionColor(index, a)
|
||||
return x
|
||||
})
|
||||
}
|
||||
func editableRowUserInfo(_ index: IndexPath) -> Any? { nil }
|
||||
}
|
||||
|
||||
protocol EditActionsRemove : EditableRows {}
|
||||
extension EditActionsRemove where Self: UITableViewController {
|
||||
func editableRowActions(_: IndexPath) -> [(RowAction, String)] { [(.delete, "Remove")] }
|
||||
func editableRowActionColor(_: IndexPath, _: RowAction) -> UIColor? { nil }
|
||||
}
|
||||
|
||||
|
||||
// MARK: - Table Cell Tap Menu
|
||||
|
||||
struct TableCellTapMenu {
|
||||
private var index: Int = Int.max
|
||||
|
||||
mutating func reset() { index = Int.max }
|
||||
|
||||
/// Create a new tap manu and shows it immediatelly. With optional buttons.
|
||||
mutating func start(_ tableView: UITableView, _ indexPath: IndexPath, items: [UIMenuItem]? = nil) -> Bool {
|
||||
let menu = UIMenuController.shared
|
||||
if index == indexPath.row {
|
||||
menu.setMenuVisible(false, animated: true)
|
||||
reset()
|
||||
return false
|
||||
}
|
||||
index = indexPath.row
|
||||
let cell = tableView.cellForRow(at: indexPath)!
|
||||
menu.setTargetRect(cell.bounds, in: cell)
|
||||
menu.menuItems = items
|
||||
menu.setMenuVisible(true, animated: true)
|
||||
return true
|
||||
}
|
||||
|
||||
/// Returns the item if the array index is in bounds.
|
||||
func getSelected<T>(_ source: [T]) -> T? {
|
||||
guard index < source.count else { return nil }
|
||||
return source[index]
|
||||
}
|
||||
}
|
||||
|
||||
106
main/Extensions/Time.swift
Normal file
@@ -0,0 +1,106 @@
|
||||
import Foundation
|
||||
|
||||
extension DateFormatter {
|
||||
convenience init(withFormat: String) {
|
||||
self.init()
|
||||
dateFormat = withFormat
|
||||
}
|
||||
}
|
||||
|
||||
extension Date {
|
||||
/// Convert `Timestamp` to `Date`
|
||||
init(_ ts: Timestamp) { self.init(timeIntervalSince1970: Double(ts)) }
|
||||
/// Convert `Date` to `Timestamp`
|
||||
var timestamp: Timestamp { get { Timestamp(self.timeIntervalSince1970) } }
|
||||
}
|
||||
|
||||
extension Timestamp {
|
||||
/// Current time as `Timestamp` (second accuracy)
|
||||
static func now() -> Timestamp { Date().timestamp }
|
||||
/// Create `Timestamp` with `now() - 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 {
|
||||
/// Recurring timer maintains a strong reference to `target`.
|
||||
@discardableResult static func repeating(_ interval: TimeInterval, call selector: Selector, on target: Any, userInfo: Any? = nil) -> Timer {
|
||||
Timer.scheduledTimer(timeInterval: interval, target: target, selector: selector,
|
||||
userInfo: userInfo, repeats: true)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// MARK: - DateFormat
|
||||
|
||||
enum DateFormat {
|
||||
private static let _hms = DateFormatter(withFormat: "yyyy-MM-dd HH:mm:ss")
|
||||
private static let _hm = DateFormatter(withFormat: "yyyy-MM-dd HH:mm")
|
||||
|
||||
/// Format: `yyyy-MM-dd HH:mm:ss`
|
||||
static func seconds(_ date: Date) -> String { _hms.string(from: date) }
|
||||
/// Format: `yyyy-MM-dd HH:mm:ss`
|
||||
static func seconds(_ ts: Timestamp) -> String { _hms.string(from: Date(ts)) }
|
||||
/// Format: `yyyy-MM-dd HH:mm`
|
||||
static func minutes(_ date: Date) -> String { _hm.string(from: date) }
|
||||
/// Format: `yyyy-MM-dd HH:mm`
|
||||
static func minutes(_ ts: Timestamp) -> String { _hm.string(from: Date(ts)) }
|
||||
}
|
||||
|
||||
|
||||
// MARK: - TimeFormat
|
||||
|
||||
struct TimeFormat {
|
||||
private var formatter: DateComponentsFormatter
|
||||
|
||||
/// Init new formatter with exactly 1 unit count. E.g., `61 min -> 1 hr`
|
||||
/// - Parameter allowed: Default: `[.day, .hour, .minute, .second]`
|
||||
init(_ style: DateComponentsFormatter.UnitsStyle, allowed: NSCalendar.Unit = [.day, .hour, .minute, .second]) {
|
||||
formatter = DateComponentsFormatter()
|
||||
formatter.maximumUnitCount = 1
|
||||
formatter.allowedUnits = allowed
|
||||
formatter.unitsStyle = style
|
||||
}
|
||||
|
||||
/// Formatted duration string, e.g., `20 min` or `7 days`
|
||||
func from(days: Int = 0, hours: Int = 0, minutes: Int = 0, seconds: Int = 0) -> String? {
|
||||
formatter.string(from: DateComponents(day: days, hour: hours, minute: minutes, second: seconds))
|
||||
}
|
||||
|
||||
// MARK: static
|
||||
|
||||
/// Time string with format `[HH:]mm:ss` (hours prepended only if duration is 1h+)
|
||||
static func from(_ duration: Timestamp) -> String {
|
||||
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 `mm:ss` or `mm:ss.SSS`
|
||||
static func from(_ duration: TimeInterval, millis: Bool = false, hours: Bool = false) -> String {
|
||||
let t = Int(duration)
|
||||
let min = t / 60
|
||||
let sec = t % 60
|
||||
if millis {
|
||||
let mil = Int(duration * 1000) % 1000
|
||||
return String(format: "%02d:%02d.%03d", min, sec, mil)
|
||||
} else if hours {
|
||||
return String(format: "%02d:%02d:%02d", min / 60, min % 60, sec)
|
||||
}
|
||||
return String(format: "%02d:%02d", min, sec)
|
||||
}
|
||||
|
||||
/// Duration string with format `mm:ss` or `mm:ss.SSS` or `HH:mm:ss` since reference date
|
||||
static func since(_ date: Date, millis: Bool = false, hours: Bool = false) -> String {
|
||||
from(Date().timeIntervalSince(date), millis: millis, hours: hours)
|
||||
}
|
||||
}
|
||||
@@ -1,7 +1,7 @@
|
||||
import Foundation
|
||||
|
||||
fileprivate extension FileManager {
|
||||
func exportDir() -> URL {
|
||||
func documentDir() -> URL {
|
||||
try! url(for: .documentDirectory, in: .userDomainMask, appropriateFor: nil, create: true)
|
||||
}
|
||||
func appGroupDir() -> URL {
|
||||
@@ -12,8 +12,47 @@ fileprivate extension FileManager {
|
||||
}
|
||||
}
|
||||
|
||||
extension FileManager {
|
||||
func sizeOf(path: String) -> Int64? {
|
||||
try? attributesOfItem(atPath: path)[.size] as? Int64
|
||||
}
|
||||
func readableSizeOf(path: String) -> String? {
|
||||
guard let fSize = sizeOf(path: path) else { return nil }
|
||||
let bcf = ByteCountFormatter()
|
||||
bcf.countStyle = .file
|
||||
return bcf.string(fromByteCount: fSize)
|
||||
}
|
||||
}
|
||||
|
||||
extension URL {
|
||||
static func exportDir() -> URL { FileManager.default.exportDir() }
|
||||
static func documentDir() -> URL { FileManager.default.documentDir() }
|
||||
static func appGroupDir() -> URL { FileManager.default.appGroupDir() }
|
||||
static func internalDB() -> URL { FileManager.default.internalDB() }
|
||||
|
||||
static func make(_ base: String, params: [String : String]) -> URL? {
|
||||
guard var components = URLComponents(string: base) else {
|
||||
return nil
|
||||
}
|
||||
components.queryItems = params.map {
|
||||
URLQueryItem(name: $0, value: $1)
|
||||
}
|
||||
components.percentEncodedQuery = components.percentEncodedQuery?.replacingOccurrences(of: "+", with: "%2B")
|
||||
return components.url
|
||||
}
|
||||
|
||||
@discardableResult func download(to file: URL, onSuccess: @escaping () -> Void) -> URLSessionDownloadTask {
|
||||
let task = URLSession.shared.downloadTask(with: self) { location, response, error in
|
||||
if let loc = location {
|
||||
try? FileManager.default.removeItem(at: file)
|
||||
do {
|
||||
try FileManager.default.moveItem(at: loc, to: file)
|
||||
onSuccess()
|
||||
} catch {
|
||||
NSLog("[VPN.ERROR] \(error)")
|
||||
}
|
||||
}
|
||||
}
|
||||
task.resume()
|
||||
return task
|
||||
}
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
}
|
||||
207
main/GUI/Base.lproj/CoOccurrence.storyboard
Normal file
@@ -0,0 +1,207 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<document type="com.apple.InterfaceBuilder3.CocoaTouch.Storyboard.XIB" version="3.0" toolsVersion="16097.2" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" useSafeAreas="YES" colorMatched="YES" initialViewController="W5Q-oz-bFb">
|
||||
<device id="retina4_0" orientation="portrait" appearance="light"/>
|
||||
<dependencies>
|
||||
<deployment identifier="iOS"/>
|
||||
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="16087"/>
|
||||
<capability name="Safe area layout guides" minToolsVersion="9.0"/>
|
||||
<capability name="documents saved in the Xcode 8 format" minToolsVersion="8.0"/>
|
||||
</dependencies>
|
||||
<scenes>
|
||||
<!--Co Occurrence-->
|
||||
<scene sceneID="Gbm-AP-b72">
|
||||
<objects>
|
||||
<viewController storyboardIdentifier="IBCoOccurrence" id="W5Q-oz-bFb" customClass="VCCoOccurrence" customModule="AppCheck" customModuleProvider="target" sceneMemberID="viewController">
|
||||
<view key="view" contentMode="scaleToFill" id="f34-NO-d8f">
|
||||
<rect key="frame" x="0.0" y="0.0" width="320" height="568"/>
|
||||
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
|
||||
<subviews>
|
||||
<navigationBar contentMode="scaleToFill" translatesAutoresizingMaskIntoConstraints="NO" id="rvt-nC-2Zr">
|
||||
<rect key="frame" x="0.0" y="0.0" width="320" height="44"/>
|
||||
<items>
|
||||
<navigationItem title="Co-Occurrence" id="csY-x8-Rpe">
|
||||
<barButtonItem key="leftBarButtonItem" systemItem="done" id="eg9-p3-Xas">
|
||||
<connections>
|
||||
<action selector="didClose:" destination="W5Q-oz-bFb" id="wyw-vo-6xL"/>
|
||||
</connections>
|
||||
</barButtonItem>
|
||||
<barButtonItem key="rightBarButtonItem" image="detail-help" id="RTh-uI-ST6">
|
||||
<connections>
|
||||
<action selector="showInfoScreen" destination="W5Q-oz-bFb" id="xoS-z7-PHr"/>
|
||||
</connections>
|
||||
</barButtonItem>
|
||||
</navigationItem>
|
||||
</items>
|
||||
</navigationBar>
|
||||
<tableView clipsSubviews="YES" contentMode="scaleToFill" alwaysBounceVertical="YES" dataMode="prototypes" style="plain" separatorStyle="default" allowsSelection="NO" rowHeight="-1" estimatedRowHeight="-1" sectionHeaderHeight="28" sectionFooterHeight="28" translatesAutoresizingMaskIntoConstraints="NO" id="PGb-pB-cfO">
|
||||
<rect key="frame" x="0.0" y="44" width="320" height="524"/>
|
||||
<color key="backgroundColor" systemColor="systemBackgroundColor" cocoaTouchSystemColor="whiteColor"/>
|
||||
<segmentedControl key="tableHeaderView" opaque="NO" contentMode="scaleToFill" contentHorizontalAlignment="left" contentVerticalAlignment="top" segmentControlStyle="plain" selectedSegmentIndex="0" id="7ye-tU-pdo">
|
||||
<rect key="frame" x="0.0" y="0.0" width="320" height="32"/>
|
||||
<autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMaxY="YES"/>
|
||||
<segments>
|
||||
<segment title="10s"/>
|
||||
<segment title="30s"/>
|
||||
</segments>
|
||||
<connections>
|
||||
<action selector="didChangeTime:" destination="W5Q-oz-bFb" eventType="valueChanged" id="c5h-JG-S19"/>
|
||||
</connections>
|
||||
</segmentedControl>
|
||||
<prototypes>
|
||||
<tableViewCell clipsSubviews="YES" contentMode="scaleToFill" preservesSuperviewLayoutMargins="YES" selectionStyle="default" indentationWidth="10" reuseIdentifier="CoOccurrenceCell" rowHeight="72" id="2qH-Bh-644" customClass="CoOccurrenceCell" customModule="AppCheck" customModuleProvider="target">
|
||||
<rect key="frame" x="0.0" y="60" width="320" height="72"/>
|
||||
<autoresizingMask key="autoresizingMask"/>
|
||||
<tableViewCellContentView key="contentView" opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center" preservesSuperviewLayoutMargins="YES" insetsLayoutMarginsFromSafeArea="NO" tableViewCell="2qH-Bh-644" id="Lwk-Uj-viQ">
|
||||
<rect key="frame" x="0.0" y="0.0" width="320" height="72"/>
|
||||
<autoresizingMask key="autoresizingMask"/>
|
||||
<subviews>
|
||||
<label opaque="NO" multipleTouchEnabled="YES" contentMode="left" insetsLayoutMarginsFromSafeArea="NO" text="99." textAlignment="center" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="qaw-ql-zIB" customClass="TagLabel" customModule="AppCheck" customModuleProvider="target">
|
||||
<rect key="frame" x="15" y="39.5" width="32" height="21.5"/>
|
||||
<color key="backgroundColor" systemColor="secondarySystemBackgroundColor" red="0.94901960780000005" green="0.94901960780000005" blue="0.96862745100000003" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
|
||||
<constraints>
|
||||
<constraint firstAttribute="width" secondItem="qaw-ql-zIB" secondAttribute="height" multiplier="3:2" id="VOJ-f5-xhk"/>
|
||||
</constraints>
|
||||
<fontDescription key="fontDescription" style="UICTFontTextStyleFootnote"/>
|
||||
<nil key="textColor"/>
|
||||
<nil key="highlightedColor"/>
|
||||
</label>
|
||||
<label opaque="NO" multipleTouchEnabled="YES" contentMode="left" insetsLayoutMarginsFromSafeArea="NO" text="Title" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" minimumScaleFactor="0.80000000000000004" translatesAutoresizingMaskIntoConstraints="NO" id="zbU-wC-qJG">
|
||||
<rect key="frame" x="15" y="11" width="290" height="20.5"/>
|
||||
<fontDescription key="fontDescription" style="UICTFontTextStyleBody"/>
|
||||
<nil key="textColor"/>
|
||||
<nil key="highlightedColor"/>
|
||||
</label>
|
||||
<label opaque="NO" multipleTouchEnabled="YES" contentMode="left" horizontalCompressionResistancePriority="500" insetsLayoutMarginsFromSafeArea="NO" text="Count" textAlignment="right" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="JWp-6l-HTJ">
|
||||
<rect key="frame" x="109.5" y="42.5" width="37" height="16"/>
|
||||
<fontDescription key="fontDescription" style="UICTFontTextStyleFootnote"/>
|
||||
<color key="textColor" systemColor="secondaryLabelColor" red="0.23529411759999999" green="0.23529411759999999" blue="0.26274509800000001" alpha="0.59999999999999998" colorSpace="custom" customColorSpace="sRGB"/>
|
||||
<nil key="highlightedColor"/>
|
||||
</label>
|
||||
<label opaque="NO" multipleTouchEnabled="YES" contentMode="left" insetsLayoutMarginsFromSafeArea="NO" text="5900" textAlignment="natural" lineBreakMode="clip" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="q5v-FM-iGo" customClass="TagLabel" customModule="AppCheck" customModuleProvider="target">
|
||||
<rect key="frame" x="150.5" y="39.5" width="42.5" height="21.5"/>
|
||||
<color key="backgroundColor" systemColor="secondarySystemBackgroundColor" red="0.94901960780000005" green="0.94901960780000005" blue="0.96862745100000003" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
|
||||
<fontDescription key="fontDescription" style="UICTFontTextStyleFootnote"/>
|
||||
<nil key="textColor"/>
|
||||
<nil key="highlightedColor"/>
|
||||
<userDefinedRuntimeAttributes>
|
||||
<userDefinedRuntimeAttribute type="number" keyPath="padRight">
|
||||
<real key="value" value="1"/>
|
||||
</userDefinedRuntimeAttribute>
|
||||
</userDefinedRuntimeAttributes>
|
||||
</label>
|
||||
<label opaque="NO" multipleTouchEnabled="YES" contentMode="left" insetsLayoutMarginsFromSafeArea="NO" text="10.35s" textAlignment="natural" lineBreakMode="clip" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="zCg-I0-4Tz" customClass="TagLabel" customModule="AppCheck" customModuleProvider="target">
|
||||
<rect key="frame" x="255.5" y="39.5" width="49.5" height="21.5"/>
|
||||
<color key="backgroundColor" systemColor="secondarySystemBackgroundColor" red="0.94901960780000005" green="0.94901960780000005" blue="0.96862745100000003" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
|
||||
<fontDescription key="fontDescription" style="UICTFontTextStyleFootnote"/>
|
||||
<nil key="textColor"/>
|
||||
<nil key="highlightedColor"/>
|
||||
<userDefinedRuntimeAttributes>
|
||||
<userDefinedRuntimeAttribute type="number" keyPath="padRight">
|
||||
<real key="value" value="1"/>
|
||||
</userDefinedRuntimeAttribute>
|
||||
</userDefinedRuntimeAttributes>
|
||||
</label>
|
||||
<label opaque="NO" multipleTouchEnabled="YES" contentMode="left" horizontalHuggingPriority="750" horizontalCompressionResistancePriority="400" insetsLayoutMarginsFromSafeArea="NO" text="Diverge" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="T4X-cn-msT">
|
||||
<rect key="frame" x="205" y="42.5" width="46.5" height="16"/>
|
||||
<fontDescription key="fontDescription" style="UICTFontTextStyleFootnote"/>
|
||||
<color key="textColor" systemColor="secondaryLabelColor" red="0.23529411759999999" green="0.23529411759999999" blue="0.26274509800000001" alpha="0.59999999999999998" colorSpace="custom" customColorSpace="sRGB"/>
|
||||
<nil key="highlightedColor"/>
|
||||
</label>
|
||||
<view opaque="NO" contentMode="scaleToFill" translatesAutoresizingMaskIntoConstraints="NO" id="9Bb-e5-D3O" customClass="MeterBar" customModule="AppCheck" customModuleProvider="target">
|
||||
<rect key="frame" x="190" y="39.5" width="3" height="21.5"/>
|
||||
<constraints>
|
||||
<constraint firstAttribute="width" constant="3" id="wWb-VG-Kqa"/>
|
||||
</constraints>
|
||||
<userDefinedRuntimeAttributes>
|
||||
<userDefinedRuntimeAttribute type="number" keyPath="percent">
|
||||
<real key="value" value="1"/>
|
||||
</userDefinedRuntimeAttribute>
|
||||
<userDefinedRuntimeAttribute type="color" keyPath="barColor">
|
||||
<color key="value" systemColor="systemOrangeColor" red="1" green="0.58431372550000005" blue="0.0" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
|
||||
</userDefinedRuntimeAttribute>
|
||||
</userDefinedRuntimeAttributes>
|
||||
</view>
|
||||
<view opaque="NO" contentMode="scaleToFill" translatesAutoresizingMaskIntoConstraints="NO" id="JwY-mq-rYZ" customClass="MeterBar" customModule="AppCheck" customModuleProvider="target">
|
||||
<rect key="frame" x="302" y="39.5" width="3" height="21.5"/>
|
||||
<constraints>
|
||||
<constraint firstAttribute="width" constant="3" id="Tta-m5-vwa"/>
|
||||
</constraints>
|
||||
<userDefinedRuntimeAttributes>
|
||||
<userDefinedRuntimeAttribute type="number" keyPath="percent">
|
||||
<real key="value" value="1"/>
|
||||
</userDefinedRuntimeAttribute>
|
||||
<userDefinedRuntimeAttribute type="color" keyPath="barColor">
|
||||
<color key="value" systemColor="systemOrangeColor" red="1" green="0.58431372550000005" blue="0.0" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
|
||||
</userDefinedRuntimeAttribute>
|
||||
</userDefinedRuntimeAttributes>
|
||||
</view>
|
||||
</subviews>
|
||||
<constraints>
|
||||
<constraint firstItem="qaw-ql-zIB" firstAttribute="height" secondItem="q5v-FM-iGo" secondAttribute="height" id="2Ug-qN-ido"/>
|
||||
<constraint firstItem="zbU-wC-qJG" firstAttribute="leading" secondItem="Lwk-Uj-viQ" secondAttribute="leadingMargin" id="2Zo-jC-08y"/>
|
||||
<constraint firstItem="JwY-mq-rYZ" firstAttribute="top" secondItem="zCg-I0-4Tz" secondAttribute="top" id="3MU-gk-eUU"/>
|
||||
<constraint firstItem="T4X-cn-msT" firstAttribute="height" secondItem="zCg-I0-4Tz" secondAttribute="height" multiplier="0.75" id="AwE-JC-MFF"/>
|
||||
<constraint firstAttribute="bottomMargin" secondItem="q5v-FM-iGo" secondAttribute="bottom" id="B2M-MQ-kAw"/>
|
||||
<constraint firstItem="9Bb-e5-D3O" firstAttribute="bottom" secondItem="q5v-FM-iGo" secondAttribute="bottom" id="Efb-Ud-lxb"/>
|
||||
<constraint firstItem="JWp-6l-HTJ" firstAttribute="height" secondItem="q5v-FM-iGo" secondAttribute="height" multiplier="0.75" id="Gfb-up-g1b"/>
|
||||
<constraint firstItem="JwY-mq-rYZ" firstAttribute="trailing" secondItem="zCg-I0-4Tz" secondAttribute="trailing" id="RlS-DQ-pdh"/>
|
||||
<constraint firstItem="zCg-I0-4Tz" firstAttribute="leading" secondItem="T4X-cn-msT" secondAttribute="trailing" constant="4" id="VpT-5w-aKh"/>
|
||||
<constraint firstAttribute="trailingMargin" secondItem="zCg-I0-4Tz" secondAttribute="trailing" id="ai7-PW-ISq"/>
|
||||
<constraint firstItem="qaw-ql-zIB" firstAttribute="leading" secondItem="Lwk-Uj-viQ" secondAttribute="leadingMargin" id="bGT-hc-lSG"/>
|
||||
<constraint firstItem="JWp-6l-HTJ" firstAttribute="height" secondItem="T4X-cn-msT" secondAttribute="height" id="cKO-4d-ikl"/>
|
||||
<constraint firstItem="JWp-6l-HTJ" firstAttribute="centerY" secondItem="q5v-FM-iGo" secondAttribute="centerY" id="dZr-0G-1sp"/>
|
||||
<constraint firstAttribute="trailingMargin" secondItem="zbU-wC-qJG" secondAttribute="trailing" id="e7x-RS-YWo"/>
|
||||
<constraint firstItem="JwY-mq-rYZ" firstAttribute="bottom" secondItem="zCg-I0-4Tz" secondAttribute="bottom" id="fAV-yh-H1r"/>
|
||||
<constraint firstItem="9Bb-e5-D3O" firstAttribute="trailing" secondItem="q5v-FM-iGo" secondAttribute="trailing" id="fFF-y3-qOe"/>
|
||||
<constraint firstItem="qaw-ql-zIB" firstAttribute="top" secondItem="zbU-wC-qJG" secondAttribute="bottom" constant="8" symbolic="YES" id="fgw-q8-YRD"/>
|
||||
<constraint firstItem="9Bb-e5-D3O" firstAttribute="top" secondItem="q5v-FM-iGo" secondAttribute="top" id="idg-nm-vIj"/>
|
||||
<constraint firstItem="T4X-cn-msT" firstAttribute="leading" secondItem="q5v-FM-iGo" secondAttribute="trailing" constant="12" id="kZj-Tn-BQ3"/>
|
||||
<constraint firstItem="zbU-wC-qJG" firstAttribute="top" secondItem="Lwk-Uj-viQ" secondAttribute="top" constant="11" id="o7o-M0-sA2"/>
|
||||
<constraint firstItem="q5v-FM-iGo" firstAttribute="top" secondItem="zbU-wC-qJG" secondAttribute="bottom" constant="8" symbolic="YES" id="peW-Pg-5WC"/>
|
||||
<constraint firstItem="zCg-I0-4Tz" firstAttribute="top" secondItem="zbU-wC-qJG" secondAttribute="bottom" constant="8" symbolic="YES" id="ttp-yA-tsi"/>
|
||||
<constraint firstItem="T4X-cn-msT" firstAttribute="centerY" secondItem="zCg-I0-4Tz" secondAttribute="centerY" id="tz9-Vr-fB6"/>
|
||||
<constraint firstItem="JWp-6l-HTJ" firstAttribute="leading" relation="greaterThanOrEqual" secondItem="qaw-ql-zIB" secondAttribute="trailing" constant="8" symbolic="YES" id="xFl-RU-Ynw"/>
|
||||
<constraint firstItem="q5v-FM-iGo" firstAttribute="leading" secondItem="JWp-6l-HTJ" secondAttribute="trailing" constant="4" id="xHw-Pf-daH"/>
|
||||
</constraints>
|
||||
</tableViewCellContentView>
|
||||
<connections>
|
||||
<outlet property="avgdiff" destination="zCg-I0-4Tz" id="Jno-Yc-ngL"/>
|
||||
<outlet property="avgdiffMeter" destination="JwY-mq-rYZ" id="QNx-rP-17Z"/>
|
||||
<outlet property="count" destination="q5v-FM-iGo" id="AFk-93-mhs"/>
|
||||
<outlet property="countMeter" destination="9Bb-e5-D3O" id="zqt-dT-ecT"/>
|
||||
<outlet property="rank" destination="qaw-ql-zIB" id="q6Y-JS-NFU"/>
|
||||
<outlet property="title" destination="zbU-wC-qJG" id="hgV-L0-blX"/>
|
||||
</connections>
|
||||
</tableViewCell>
|
||||
</prototypes>
|
||||
<connections>
|
||||
<outlet property="dataSource" destination="W5Q-oz-bFb" id="7lD-aQ-QhQ"/>
|
||||
</connections>
|
||||
</tableView>
|
||||
</subviews>
|
||||
<color key="backgroundColor" systemColor="systemBackgroundColor" cocoaTouchSystemColor="whiteColor"/>
|
||||
<constraints>
|
||||
<constraint firstItem="PGb-pB-cfO" firstAttribute="top" secondItem="rvt-nC-2Zr" secondAttribute="bottom" id="Edp-lx-Xld"/>
|
||||
<constraint firstItem="4eZ-5P-8sz" firstAttribute="bottom" secondItem="PGb-pB-cfO" secondAttribute="bottom" id="OAG-HL-4N4"/>
|
||||
<constraint firstItem="PGb-pB-cfO" firstAttribute="leading" secondItem="4eZ-5P-8sz" secondAttribute="leading" id="V6d-HM-JzJ"/>
|
||||
<constraint firstItem="4eZ-5P-8sz" firstAttribute="trailing" secondItem="rvt-nC-2Zr" secondAttribute="trailing" id="cmE-iH-06W"/>
|
||||
<constraint firstItem="rvt-nC-2Zr" firstAttribute="top" secondItem="4eZ-5P-8sz" secondAttribute="top" id="epT-LW-CJV"/>
|
||||
<constraint firstItem="4eZ-5P-8sz" firstAttribute="trailing" secondItem="PGb-pB-cfO" secondAttribute="trailing" id="j8i-8q-qGS"/>
|
||||
<constraint firstItem="rvt-nC-2Zr" firstAttribute="leading" secondItem="4eZ-5P-8sz" secondAttribute="leading" id="skN-SN-Wu7"/>
|
||||
</constraints>
|
||||
<viewLayoutGuide key="safeArea" id="4eZ-5P-8sz"/>
|
||||
</view>
|
||||
<connections>
|
||||
<outlet property="tableView" destination="PGb-pB-cfO" id="5gT-KC-ce5"/>
|
||||
<outlet property="timeSegment" destination="7ye-tU-pdo" id="2ys-X4-Jff"/>
|
||||
</connections>
|
||||
</viewController>
|
||||
<placeholder placeholderIdentifier="IBFirstResponder" id="yYY-5U-gct" userLabel="First Responder" customClass="UIResponder" sceneMemberID="firstResponder"/>
|
||||
</objects>
|
||||
<point key="canvasLocation" x="0.0" y="0.0"/>
|
||||
</scene>
|
||||
</scenes>
|
||||
<resources>
|
||||
<image name="detail-help" width="22" height="22"/>
|
||||
</resources>
|
||||
</document>
|
||||
116
main/GUI/Base.lproj/Main.storyboard
Normal file
@@ -0,0 +1,116 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<document type="com.apple.InterfaceBuilder3.CocoaTouch.Storyboard.XIB" version="3.0" toolsVersion="16097.2" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" useSafeAreas="YES" colorMatched="YES" initialViewController="sfA-EG-18J">
|
||||
<device id="retina4_0" orientation="portrait" appearance="light"/>
|
||||
<dependencies>
|
||||
<deployment identifier="iOS"/>
|
||||
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="16087"/>
|
||||
<capability name="documents saved in the Xcode 8 format" minToolsVersion="8.0"/>
|
||||
</dependencies>
|
||||
<scenes>
|
||||
<!--Main-->
|
||||
<scene sceneID="7Rl-BK-ry5">
|
||||
<objects>
|
||||
<tabBarController id="sfA-EG-18J" customClass="TBCMain" customModule="AppCheck" customModuleProvider="target" sceneMemberID="viewController">
|
||||
<tabBar key="tabBar" contentMode="scaleToFill" insetsLayoutMarginsFromSafeArea="NO" id="qza-ey-Iaz">
|
||||
<rect key="frame" x="0.0" y="0.0" width="414" height="49"/>
|
||||
<autoresizingMask key="autoresizingMask"/>
|
||||
<color key="backgroundColor" white="0.0" alpha="0.0" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
|
||||
</tabBar>
|
||||
<connections>
|
||||
<segue destination="RcB-4v-fd4" kind="relationship" relationship="viewControllers" id="cmC-pu-5n2"/>
|
||||
<segue destination="xaF-Q9-CPX" kind="relationship" relationship="viewControllers" id="bi1-vo-w1d"/>
|
||||
<segue destination="dIk-JY-9vE" kind="relationship" relationship="viewControllers" id="ude-bF-ump"/>
|
||||
</connections>
|
||||
</tabBarController>
|
||||
<placeholder placeholderIdentifier="IBFirstResponder" id="RDz-8t-yhN" userLabel="First Responder" customClass="UIResponder" sceneMemberID="firstResponder"/>
|
||||
</objects>
|
||||
<point key="canvasLocation" x="0.0" y="-150"/>
|
||||
</scene>
|
||||
<!--Requests-->
|
||||
<scene sceneID="bDO-X1-bCe">
|
||||
<objects>
|
||||
<navigationController id="RcB-4v-fd4" sceneMemberID="viewController">
|
||||
<tabBarItem key="tabBarItem" title="Requests" image="journal" id="Sj5-Kb-Li8"/>
|
||||
<navigationBar key="navigationBar" contentMode="scaleToFill" insetsLayoutMarginsFromSafeArea="NO" id="HWd-73-m8j">
|
||||
<rect key="frame" x="0.0" y="0.0" width="320" height="44"/>
|
||||
<autoresizingMask key="autoresizingMask"/>
|
||||
</navigationBar>
|
||||
<connections>
|
||||
<segue destination="H0N-TG-Ck1" kind="relationship" relationship="rootViewController" id="jDh-HG-7ke"/>
|
||||
</connections>
|
||||
</navigationController>
|
||||
<placeholder placeholderIdentifier="IBFirstResponder" id="8j4-AX-JBN" userLabel="First Responder" customClass="UIResponder" sceneMemberID="firstResponder"/>
|
||||
</objects>
|
||||
<point key="canvasLocation" x="-700" y="650"/>
|
||||
</scene>
|
||||
<!--Requests-->
|
||||
<scene sceneID="kwb-zk-i3S">
|
||||
<objects>
|
||||
<viewControllerPlaceholder storyboardName="Requests" id="H0N-TG-Ck1" sceneMemberID="viewController">
|
||||
<navigationItem key="navigationItem" id="cLA-5B-TZx"/>
|
||||
</viewControllerPlaceholder>
|
||||
<placeholder placeholderIdentifier="IBFirstResponder" id="rwq-DI-GUl" userLabel="First Responder" customClass="UIResponder" sceneMemberID="firstResponder"/>
|
||||
</objects>
|
||||
<point key="canvasLocation" x="-700" y="1150"/>
|
||||
</scene>
|
||||
<!--Recordings-->
|
||||
<scene sceneID="V1I-GW-1gY">
|
||||
<objects>
|
||||
<placeholder placeholderIdentifier="IBFirstResponder" id="df6-nz-nUq" userLabel="First Responder" customClass="UIResponder" sceneMemberID="firstResponder"/>
|
||||
<navigationController id="xaF-Q9-CPX" sceneMemberID="viewController">
|
||||
<tabBarItem key="tabBarItem" title="Recordings" image="tag" id="5ww-wt-MOH"/>
|
||||
<navigationBar key="navigationBar" contentMode="scaleToFill" insetsLayoutMarginsFromSafeArea="NO" id="ZFR-QK-Ofl">
|
||||
<rect key="frame" x="0.0" y="0.0" width="320" height="44"/>
|
||||
<autoresizingMask key="autoresizingMask"/>
|
||||
</navigationBar>
|
||||
<connections>
|
||||
<segue destination="thF-Z0-CBq" kind="relationship" relationship="rootViewController" id="BuU-Uy-Zhv"/>
|
||||
</connections>
|
||||
</navigationController>
|
||||
</objects>
|
||||
<point key="canvasLocation" x="0.0" y="650"/>
|
||||
</scene>
|
||||
<!--Recordings-->
|
||||
<scene sceneID="oLG-Zp-Qm7">
|
||||
<objects>
|
||||
<viewControllerPlaceholder storyboardName="Recordings" id="thF-Z0-CBq" sceneMemberID="viewController">
|
||||
<navigationItem key="navigationItem" id="kMV-TC-eKI"/>
|
||||
</viewControllerPlaceholder>
|
||||
<placeholder placeholderIdentifier="IBFirstResponder" id="B2d-DP-1Yp" userLabel="First Responder" customClass="UIResponder" sceneMemberID="firstResponder"/>
|
||||
</objects>
|
||||
<point key="canvasLocation" x="0.0" y="1150"/>
|
||||
</scene>
|
||||
<!--Settings-->
|
||||
<scene sceneID="OEQ-fb-haL">
|
||||
<objects>
|
||||
<navigationController id="dIk-JY-9vE" sceneMemberID="viewController">
|
||||
<tabBarItem key="tabBarItem" title="Settings" image="settings" id="dQu-wE-a8u"/>
|
||||
<navigationBar key="navigationBar" contentMode="scaleToFill" insetsLayoutMarginsFromSafeArea="NO" id="yYW-rX-VnB">
|
||||
<rect key="frame" x="0.0" y="0.0" width="320" height="44"/>
|
||||
<autoresizingMask key="autoresizingMask"/>
|
||||
</navigationBar>
|
||||
<connections>
|
||||
<segue destination="cV2-If-0fV" kind="relationship" relationship="rootViewController" id="vQq-KE-MOO"/>
|
||||
</connections>
|
||||
</navigationController>
|
||||
<placeholder placeholderIdentifier="IBFirstResponder" id="bg9-bR-vlx" userLabel="First Responder" customClass="UIResponder" sceneMemberID="firstResponder"/>
|
||||
</objects>
|
||||
<point key="canvasLocation" x="700" y="650"/>
|
||||
</scene>
|
||||
<!--Settings-->
|
||||
<scene sceneID="l0f-fL-3tG">
|
||||
<objects>
|
||||
<viewControllerPlaceholder storyboardName="Settings" id="cV2-If-0fV" sceneMemberID="viewController">
|
||||
<navigationItem key="navigationItem" id="wlO-Ea-6he"/>
|
||||
</viewControllerPlaceholder>
|
||||
<placeholder placeholderIdentifier="IBFirstResponder" id="i3W-Ff-rJL" userLabel="First Responder" customClass="UIResponder" sceneMemberID="firstResponder"/>
|
||||
</objects>
|
||||
<point key="canvasLocation" x="700" y="1150"/>
|
||||
</scene>
|
||||
</scenes>
|
||||
<resources>
|
||||
<image name="journal" width="25" height="25"/>
|
||||
<image name="settings" width="25" height="25"/>
|
||||
<image name="tag" width="25" height="25"/>
|
||||
</resources>
|
||||
</document>
|
||||
742
main/GUI/Base.lproj/Recordings.storyboard
Normal file
@@ -0,0 +1,742 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<document type="com.apple.InterfaceBuilder3.CocoaTouch.Storyboard.XIB" version="3.0" toolsVersion="16097.2" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" useSafeAreas="YES" colorMatched="YES" initialViewController="hm5-7q-Zfi">
|
||||
<device id="retina4_0" orientation="portrait" appearance="light"/>
|
||||
<dependencies>
|
||||
<deployment identifier="iOS"/>
|
||||
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="16087"/>
|
||||
<capability name="Safe area layout guides" minToolsVersion="9.0"/>
|
||||
<capability name="documents saved in the Xcode 8 format" minToolsVersion="8.0"/>
|
||||
</dependencies>
|
||||
<scenes>
|
||||
<!--Recordings-->
|
||||
<scene sceneID="ODR-PD-nTU">
|
||||
<objects>
|
||||
<viewController id="hm5-7q-Zfi" customClass="VCRecordings" customModule="AppCheck" customModuleProvider="target" sceneMemberID="viewController">
|
||||
<view key="view" contentMode="scaleToFill" id="JYr-yE-eGS">
|
||||
<rect key="frame" x="0.0" y="0.0" width="320" height="519"/>
|
||||
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
|
||||
<subviews>
|
||||
<stackView opaque="NO" contentMode="scaleToFill" axis="vertical" translatesAutoresizingMaskIntoConstraints="NO" id="Wz5-zb-gwz">
|
||||
<rect key="frame" x="0.0" y="0.0" width="320" height="519"/>
|
||||
<subviews>
|
||||
<view contentMode="scaleToFill" translatesAutoresizingMaskIntoConstraints="NO" id="ppJ-js-Wwz">
|
||||
<rect key="frame" x="0.0" y="0.0" width="320" height="40"/>
|
||||
<subviews>
|
||||
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="Start new recording" textAlignment="center" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="sYd-b2-Puz">
|
||||
<rect key="frame" x="20" y="8" width="280" height="24"/>
|
||||
<fontDescription key="fontDescription" style="UICTFontTextStyleTitle2"/>
|
||||
<nil key="textColor"/>
|
||||
<nil key="highlightedColor"/>
|
||||
</label>
|
||||
<button opaque="NO" contentMode="scaleToFill" contentHorizontalAlignment="center" contentVerticalAlignment="center" lineBreakMode="middleTruncation" translatesAutoresizingMaskIntoConstraints="NO" id="By4-Qr-Zg2">
|
||||
<rect key="frame" x="278" y="9" width="22" height="22"/>
|
||||
<state key="normal" image="detail-help"/>
|
||||
<connections>
|
||||
<action selector="showInfo:" destination="hm5-7q-Zfi" eventType="touchUpInside" id="AiT-rb-53G"/>
|
||||
</connections>
|
||||
</button>
|
||||
</subviews>
|
||||
<color key="backgroundColor" systemColor="systemBackgroundColor" cocoaTouchSystemColor="whiteColor"/>
|
||||
<constraints>
|
||||
<constraint firstItem="sYd-b2-Puz" firstAttribute="leading" secondItem="ppJ-js-Wwz" secondAttribute="leading" constant="20" symbolic="YES" id="35n-12-YFH"/>
|
||||
<constraint firstAttribute="bottom" secondItem="sYd-b2-Puz" secondAttribute="bottom" priority="999" constant="8" id="4We-J8-dH8"/>
|
||||
<constraint firstAttribute="trailing" secondItem="sYd-b2-Puz" secondAttribute="trailing" constant="20" symbolic="YES" id="7eW-5L-F4Z"/>
|
||||
<constraint firstItem="sYd-b2-Puz" firstAttribute="top" secondItem="ppJ-js-Wwz" secondAttribute="top" priority="999" constant="8" id="S3K-8P-agX"/>
|
||||
<constraint firstItem="By4-Qr-Zg2" firstAttribute="centerY" secondItem="sYd-b2-Puz" secondAttribute="centerY" id="ScC-FQ-oU3"/>
|
||||
<constraint firstAttribute="trailing" secondItem="By4-Qr-Zg2" secondAttribute="trailing" constant="20" symbolic="YES" id="yLs-7S-Uwz"/>
|
||||
</constraints>
|
||||
</view>
|
||||
<view clipsSubviews="YES" contentMode="scaleToFill" translatesAutoresizingMaskIntoConstraints="NO" id="La3-9e-6TK">
|
||||
<rect key="frame" x="0.0" y="40" width="320" height="55"/>
|
||||
<subviews>
|
||||
<segmentedControl opaque="NO" contentMode="scaleToFill" contentHorizontalAlignment="left" contentVerticalAlignment="top" segmentControlStyle="plain" momentary="YES" translatesAutoresizingMaskIntoConstraints="NO" id="2MI-6l-YQt">
|
||||
<rect key="frame" x="20" y="8" width="280" height="32"/>
|
||||
<segments>
|
||||
<segment title="App"/>
|
||||
<segment title="Background"/>
|
||||
</segments>
|
||||
<connections>
|
||||
<action selector="startRecording:" destination="hm5-7q-Zfi" eventType="valueChanged" id="4p5-9Q-clW"/>
|
||||
</connections>
|
||||
</segmentedControl>
|
||||
</subviews>
|
||||
<color key="backgroundColor" systemColor="systemBackgroundColor" cocoaTouchSystemColor="whiteColor"/>
|
||||
<constraints>
|
||||
<constraint firstItem="2MI-6l-YQt" firstAttribute="top" secondItem="La3-9e-6TK" secondAttribute="top" priority="999" constant="8" id="TTg-gG-wEP"/>
|
||||
<constraint firstAttribute="bottom" secondItem="2MI-6l-YQt" secondAttribute="bottom" priority="999" constant="16" id="WPY-VT-xfo"/>
|
||||
<constraint firstAttribute="trailing" secondItem="2MI-6l-YQt" secondAttribute="trailing" constant="20" id="hhi-TT-2VS"/>
|
||||
<constraint firstItem="2MI-6l-YQt" firstAttribute="leading" secondItem="La3-9e-6TK" secondAttribute="leading" constant="20" id="p5F-L6-whI"/>
|
||||
</constraints>
|
||||
</view>
|
||||
<view contentMode="scaleToFill" translatesAutoresizingMaskIntoConstraints="NO" id="9Yj-FX-eFd">
|
||||
<rect key="frame" x="0.0" y="95" width="320" height="54.5"/>
|
||||
<subviews>
|
||||
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="00:00.000" textAlignment="center" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="rbR-np-cXD">
|
||||
<rect key="frame" x="8" y="8" width="200" height="38.5"/>
|
||||
<accessibility key="accessibilityConfiguration">
|
||||
<accessibilityTraits key="traits" staticText="YES" updatesFrequently="YES"/>
|
||||
</accessibility>
|
||||
<fontDescription key="fontDescription" type="system" weight="ultraLight" pointSize="32"/>
|
||||
<nil key="textColor"/>
|
||||
<nil key="highlightedColor"/>
|
||||
</label>
|
||||
<button opaque="NO" contentMode="scaleToFill" contentHorizontalAlignment="center" contentVerticalAlignment="center" buttonType="roundedRect" lineBreakMode="middleTruncation" translatesAutoresizingMaskIntoConstraints="NO" id="vAq-EZ-Gmx">
|
||||
<rect key="frame" x="212" y="8" width="100" height="38.5"/>
|
||||
<fontDescription key="fontDescription" style="UICTFontTextStyleTitle2"/>
|
||||
<state key="normal" title="Stop">
|
||||
<color key="titleColor" systemColor="systemRedColor" red="1" green="0.23137254900000001" blue="0.18823529410000001" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
|
||||
</state>
|
||||
<connections>
|
||||
<action selector="stopRecording:" destination="hm5-7q-Zfi" eventType="touchUpInside" id="CWK-Tg-Tb8"/>
|
||||
</connections>
|
||||
</button>
|
||||
</subviews>
|
||||
<color key="backgroundColor" systemColor="systemBackgroundColor" cocoaTouchSystemColor="whiteColor"/>
|
||||
<constraints>
|
||||
<constraint firstItem="rbR-np-cXD" firstAttribute="leading" secondItem="9Yj-FX-eFd" secondAttribute="leading" constant="8" id="610-aO-U6Y"/>
|
||||
<constraint firstItem="vAq-EZ-Gmx" firstAttribute="top" secondItem="9Yj-FX-eFd" secondAttribute="top" priority="999" constant="8" id="CKZ-8m-kmt"/>
|
||||
<constraint firstItem="vAq-EZ-Gmx" firstAttribute="leading" secondItem="rbR-np-cXD" secondAttribute="trailing" constant="4" id="Cya-vS-90h"/>
|
||||
<constraint firstAttribute="trailing" secondItem="vAq-EZ-Gmx" secondAttribute="trailing" constant="8" id="DhM-H2-4I7"/>
|
||||
<constraint firstItem="rbR-np-cXD" firstAttribute="width" secondItem="vAq-EZ-Gmx" secondAttribute="width" multiplier="2" id="REp-Ug-zuV"/>
|
||||
<constraint firstAttribute="bottom" secondItem="rbR-np-cXD" secondAttribute="bottom" priority="999" constant="8" id="Ukk-3a-Vsi"/>
|
||||
<constraint firstAttribute="bottom" secondItem="vAq-EZ-Gmx" secondAttribute="bottom" priority="999" constant="8" id="dV2-g0-7gH"/>
|
||||
<constraint firstItem="rbR-np-cXD" firstAttribute="top" secondItem="9Yj-FX-eFd" secondAttribute="top" priority="999" constant="8" id="mrE-Ej-dqM"/>
|
||||
</constraints>
|
||||
</view>
|
||||
<containerView opaque="NO" contentMode="scaleToFill" translatesAutoresizingMaskIntoConstraints="NO" id="v3Z-HR-abM">
|
||||
<rect key="frame" x="0.0" y="149.5" width="320" height="369.5"/>
|
||||
<connections>
|
||||
<segue destination="Fln-DD-aId" kind="embed" id="eFH-Rl-aBL"/>
|
||||
</connections>
|
||||
</containerView>
|
||||
</subviews>
|
||||
</stackView>
|
||||
</subviews>
|
||||
<color key="backgroundColor" systemColor="systemBackgroundColor" cocoaTouchSystemColor="whiteColor"/>
|
||||
<constraints>
|
||||
<constraint firstItem="Wz5-zb-gwz" firstAttribute="leading" secondItem="lFq-fl-zah" secondAttribute="leading" id="Sjv-qq-h50"/>
|
||||
<constraint firstItem="Wz5-zb-gwz" firstAttribute="trailing" secondItem="lFq-fl-zah" secondAttribute="trailing" id="hhA-jU-DJS"/>
|
||||
<constraint firstItem="Wz5-zb-gwz" firstAttribute="bottom" secondItem="lFq-fl-zah" secondAttribute="bottom" id="m6I-NP-LhY"/>
|
||||
<constraint firstItem="Wz5-zb-gwz" firstAttribute="top" secondItem="lFq-fl-zah" secondAttribute="top" id="pEc-Cz-v9f"/>
|
||||
</constraints>
|
||||
<viewLayoutGuide key="safeArea" id="lFq-fl-zah"/>
|
||||
</view>
|
||||
<navigationItem key="navigationItem" id="ceR-rC-rur"/>
|
||||
<nil key="simulatedTopBarMetrics"/>
|
||||
<simulatedTabBarMetrics key="simulatedBottomBarMetrics" translucent="NO"/>
|
||||
<connections>
|
||||
<outlet property="buttonView" destination="La3-9e-6TK" id="UMg-xx-6OV"/>
|
||||
<outlet property="headerView" destination="ppJ-js-Wwz" id="68u-8M-R2Q"/>
|
||||
<outlet property="runningView" destination="9Yj-FX-eFd" id="L2C-YR-2HN"/>
|
||||
<outlet property="stopButton" destination="vAq-EZ-Gmx" id="XiW-1H-I9y"/>
|
||||
<outlet property="timeLabel" destination="rbR-np-cXD" id="EEe-8F-HT6"/>
|
||||
</connections>
|
||||
</viewController>
|
||||
<placeholder placeholderIdentifier="IBFirstResponder" id="Wfy-Tp-A9o" userLabel="First Responder" customClass="UIResponder" sceneMemberID="firstResponder"/>
|
||||
</objects>
|
||||
<point key="canvasLocation" x="0.0" y="0.0"/>
|
||||
</scene>
|
||||
<!--Previous Recordings-->
|
||||
<scene sceneID="RqA-Jc-FDE">
|
||||
<objects>
|
||||
<tableViewController id="Fln-DD-aId" customClass="TVCPreviousRecords" customModule="AppCheck" customModuleProvider="target" sceneMemberID="viewController">
|
||||
<tableView key="view" clipsSubviews="YES" contentMode="scaleToFill" alwaysBounceVertical="YES" dataMode="prototypes" style="plain" separatorStyle="default" rowHeight="-1" estimatedRowHeight="-1" sectionHeaderHeight="28" sectionFooterHeight="28" id="7cH-g6-H5z">
|
||||
<rect key="frame" x="0.0" y="0.0" width="320" height="369.5"/>
|
||||
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
|
||||
<color key="backgroundColor" systemColor="systemBackgroundColor" cocoaTouchSystemColor="whiteColor"/>
|
||||
<prototypes>
|
||||
<tableViewCell clipsSubviews="YES" contentMode="scaleToFill" preservesSuperviewLayoutMargins="YES" selectionStyle="default" accessoryType="detailButton" indentationWidth="10" reuseIdentifier="PreviousRecordCell" textLabel="hr0-Xt-5gV" detailTextLabel="Xav-Ub-clj" style="IBUITableViewCellStyleSubtitle" id="3kW-3B-1bx">
|
||||
<rect key="frame" x="0.0" y="28" width="320" height="57.5"/>
|
||||
<autoresizingMask key="autoresizingMask"/>
|
||||
<tableViewCellContentView key="contentView" opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center" preservesSuperviewLayoutMargins="YES" insetsLayoutMarginsFromSafeArea="NO" tableViewCell="3kW-3B-1bx" id="OKV-a6-jjd">
|
||||
<rect key="frame" x="0.0" y="0.0" width="280" height="57.5"/>
|
||||
<autoresizingMask key="autoresizingMask"/>
|
||||
<subviews>
|
||||
<label opaque="NO" multipleTouchEnabled="YES" contentMode="left" insetsLayoutMarginsFromSafeArea="NO" text="Title" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" minimumScaleFactor="0.80000000000000004" id="hr0-Xt-5gV">
|
||||
<rect key="frame" x="16" y="9" width="33.5" height="20.5"/>
|
||||
<autoresizingMask key="autoresizingMask"/>
|
||||
<fontDescription key="fontDescription" style="UICTFontTextStyleBody"/>
|
||||
<nil key="textColor"/>
|
||||
<nil key="highlightedColor"/>
|
||||
</label>
|
||||
<label opaque="NO" multipleTouchEnabled="YES" contentMode="left" insetsLayoutMarginsFromSafeArea="NO" text="Subtitle" textAlignment="natural" lineBreakMode="tailTruncation" numberOfLines="2" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" id="Xav-Ub-clj">
|
||||
<rect key="frame" x="16" y="32.5" width="44" height="14.5"/>
|
||||
<autoresizingMask key="autoresizingMask"/>
|
||||
<fontDescription key="fontDescription" style="UICTFontTextStyleCaption1"/>
|
||||
<color key="textColor" systemColor="secondaryLabelColor" red="0.23529411759999999" green="0.23529411759999999" blue="0.26274509800000001" alpha="0.59999999999999998" colorSpace="custom" customColorSpace="sRGB"/>
|
||||
<nil key="highlightedColor"/>
|
||||
</label>
|
||||
</subviews>
|
||||
</tableViewCellContentView>
|
||||
<connections>
|
||||
<segue destination="50g-BI-Q6S" kind="push" identifier="openRecordDetailsSegue" id="arP-jR-O9d"/>
|
||||
</connections>
|
||||
</tableViewCell>
|
||||
</prototypes>
|
||||
<connections>
|
||||
<outlet property="dataSource" destination="Fln-DD-aId" id="oHb-mU-M1Z"/>
|
||||
<outlet property="delegate" destination="Fln-DD-aId" id="6PY-c0-Nfp"/>
|
||||
</connections>
|
||||
</tableView>
|
||||
<navigationItem key="navigationItem" title="Previous Recordings" id="ow1-cy-qXt"/>
|
||||
<connections>
|
||||
<segue destination="VRk-wv-rhk" kind="modal" identifier="editRecordSegue" id="8rY-sA-Iig"/>
|
||||
</connections>
|
||||
</tableViewController>
|
||||
<placeholder placeholderIdentifier="IBFirstResponder" id="Lta-uo-x4m" userLabel="First Responder" customClass="UIResponder" sceneMemberID="firstResponder"/>
|
||||
</objects>
|
||||
<point key="canvasLocation" x="700" y="0.0"/>
|
||||
</scene>
|
||||
<!--Logs-->
|
||||
<scene sceneID="DxJ-8o-gTM">
|
||||
<objects>
|
||||
<tableViewController id="50g-BI-Q6S" customClass="TVCRecordingDetails" customModule="AppCheck" customModuleProvider="target" sceneMemberID="viewController">
|
||||
<tableView key="view" clipsSubviews="YES" contentMode="scaleToFill" alwaysBounceVertical="YES" dataMode="prototypes" style="plain" separatorStyle="default" rowHeight="-1" estimatedRowHeight="-1" sectionHeaderHeight="28" sectionFooterHeight="28" id="cLV-Db-JxM">
|
||||
<rect key="frame" x="0.0" y="0.0" width="320" height="369.5"/>
|
||||
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
|
||||
<color key="backgroundColor" systemColor="systemBackgroundColor" cocoaTouchSystemColor="whiteColor"/>
|
||||
<prototypes>
|
||||
<tableViewCell clipsSubviews="YES" contentMode="scaleToFill" preservesSuperviewLayoutMargins="YES" selectionStyle="none" indentationWidth="10" reuseIdentifier="RecordDetailCountedCell" textLabel="rN0-kA-Eln" detailTextLabel="xRp-XG-oKf" style="IBUITableViewCellStyleValue1" id="ceT-cF-lLF">
|
||||
<rect key="frame" x="0.0" y="28" width="320" height="43.5"/>
|
||||
<autoresizingMask key="autoresizingMask"/>
|
||||
<tableViewCellContentView key="contentView" opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center" preservesSuperviewLayoutMargins="YES" insetsLayoutMarginsFromSafeArea="NO" tableViewCell="ceT-cF-lLF" id="c5Y-xg-hSL">
|
||||
<rect key="frame" x="0.0" y="0.0" width="320" height="43.5"/>
|
||||
<autoresizingMask key="autoresizingMask"/>
|
||||
<subviews>
|
||||
<label opaque="NO" multipleTouchEnabled="YES" contentMode="left" insetsLayoutMarginsFromSafeArea="NO" text="Title" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" minimumScaleFactor="0.80000000000000004" id="rN0-kA-Eln">
|
||||
<rect key="frame" x="16" y="12" width="33.5" height="20.5"/>
|
||||
<autoresizingMask key="autoresizingMask"/>
|
||||
<fontDescription key="fontDescription" style="UICTFontTextStyleBody"/>
|
||||
<nil key="textColor"/>
|
||||
<nil key="highlightedColor"/>
|
||||
</label>
|
||||
<label opaque="NO" multipleTouchEnabled="YES" contentMode="left" insetsLayoutMarginsFromSafeArea="NO" text="Detail" textAlignment="right" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" id="xRp-XG-oKf">
|
||||
<rect key="frame" x="260" y="12" width="44" height="20.5"/>
|
||||
<autoresizingMask key="autoresizingMask"/>
|
||||
<fontDescription key="fontDescription" style="UICTFontTextStyleBody"/>
|
||||
<color key="textColor" systemColor="secondaryLabelColor" red="0.23529411759999999" green="0.23529411759999999" blue="0.26274509800000001" alpha="0.59999999999999998" colorSpace="custom" customColorSpace="sRGB"/>
|
||||
<nil key="highlightedColor"/>
|
||||
</label>
|
||||
</subviews>
|
||||
</tableViewCellContentView>
|
||||
</tableViewCell>
|
||||
<tableViewCell clipsSubviews="YES" contentMode="scaleToFill" preservesSuperviewLayoutMargins="YES" selectionStyle="none" indentationWidth="10" reuseIdentifier="RecordDetailShortCell" textLabel="rIc-r4-6pg" detailTextLabel="0pW-ZC-wmh" style="IBUITableViewCellStyleValue2" id="hzU-cx-nIs">
|
||||
<rect key="frame" x="0.0" y="71.5" width="320" height="43.5"/>
|
||||
<autoresizingMask key="autoresizingMask"/>
|
||||
<tableViewCellContentView key="contentView" opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center" preservesSuperviewLayoutMargins="YES" insetsLayoutMarginsFromSafeArea="NO" tableViewCell="hzU-cx-nIs" id="scX-pQ-E7z">
|
||||
<rect key="frame" x="0.0" y="0.0" width="320" height="43.5"/>
|
||||
<autoresizingMask key="autoresizingMask"/>
|
||||
<subviews>
|
||||
<label opaque="NO" multipleTouchEnabled="YES" contentMode="left" insetsLayoutMarginsFromSafeArea="NO" text="Title" textAlignment="right" lineBreakMode="headTruncation" numberOfLines="2" baselineAdjustment="alignBaselines" minimumScaleFactor="0.80000000000000004" id="rIc-r4-6pg">
|
||||
<rect key="frame" x="16" y="12" width="91" height="20.5"/>
|
||||
<autoresizingMask key="autoresizingMask"/>
|
||||
<fontDescription key="fontDescription" style="UICTFontTextStyleBody"/>
|
||||
<color key="textColor" systemColor="secondaryLabelColor" red="0.23529411759999999" green="0.23529411759999999" blue="0.26274509800000001" alpha="0.59999999999999998" colorSpace="custom" customColorSpace="sRGB"/>
|
||||
<nil key="highlightedColor"/>
|
||||
</label>
|
||||
<label opaque="NO" multipleTouchEnabled="YES" contentMode="left" insetsLayoutMarginsFromSafeArea="NO" text="Detail" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" minimumScaleFactor="0.80000000000000004" id="0pW-ZC-wmh">
|
||||
<rect key="frame" x="113" y="12" width="44" height="20.5"/>
|
||||
<autoresizingMask key="autoresizingMask"/>
|
||||
<fontDescription key="fontDescription" style="UICTFontTextStyleBody"/>
|
||||
<nil key="textColor"/>
|
||||
<nil key="highlightedColor"/>
|
||||
</label>
|
||||
</subviews>
|
||||
</tableViewCellContentView>
|
||||
</tableViewCell>
|
||||
<tableViewCell clipsSubviews="YES" contentMode="scaleToFill" preservesSuperviewLayoutMargins="YES" selectionStyle="none" indentationWidth="10" reuseIdentifier="RecordDetailLongCell" textLabel="xDy-8J-JFT" detailTextLabel="kgF-BN-FdV" style="IBUITableViewCellStyleSubtitle" id="Q4T-JJ-fqY">
|
||||
<rect key="frame" x="0.0" y="115" width="320" height="57.5"/>
|
||||
<autoresizingMask key="autoresizingMask"/>
|
||||
<tableViewCellContentView key="contentView" opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center" preservesSuperviewLayoutMargins="YES" insetsLayoutMarginsFromSafeArea="NO" tableViewCell="Q4T-JJ-fqY" id="8hy-Rg-b6Q">
|
||||
<rect key="frame" x="0.0" y="0.0" width="320" height="57.5"/>
|
||||
<autoresizingMask key="autoresizingMask"/>
|
||||
<subviews>
|
||||
<label opaque="NO" multipleTouchEnabled="YES" contentMode="left" insetsLayoutMarginsFromSafeArea="NO" text="Title" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" minimumScaleFactor="0.80000000000000004" id="xDy-8J-JFT">
|
||||
<rect key="frame" x="16" y="9" width="33.5" height="20.5"/>
|
||||
<autoresizingMask key="autoresizingMask"/>
|
||||
<fontDescription key="fontDescription" style="UICTFontTextStyleBody"/>
|
||||
<nil key="textColor"/>
|
||||
<nil key="highlightedColor"/>
|
||||
</label>
|
||||
<label opaque="NO" multipleTouchEnabled="YES" contentMode="left" insetsLayoutMarginsFromSafeArea="NO" text="Subtitle" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" minimumScaleFactor="0.80000000000000004" adjustsFontSizeToFit="NO" id="kgF-BN-FdV">
|
||||
<rect key="frame" x="16" y="32.5" width="44" height="14.5"/>
|
||||
<autoresizingMask key="autoresizingMask"/>
|
||||
<fontDescription key="fontDescription" style="UICTFontTextStyleCaption1"/>
|
||||
<color key="textColor" systemColor="secondaryLabelColor" red="0.23529411759999999" green="0.23529411759999999" blue="0.26274509800000001" alpha="0.59999999999999998" colorSpace="custom" customColorSpace="sRGB"/>
|
||||
<nil key="highlightedColor"/>
|
||||
</label>
|
||||
</subviews>
|
||||
</tableViewCellContentView>
|
||||
</tableViewCell>
|
||||
<tableViewCell clipsSubviews="YES" contentMode="scaleToFill" preservesSuperviewLayoutMargins="YES" selectionStyle="none" indentationWidth="10" reuseIdentifier="RecordNoResultsCell" textLabel="bmQ-Cn-BOm" style="IBUITableViewCellStyleDefault" id="JZ4-vZ-MnG">
|
||||
<rect key="frame" x="0.0" y="172.5" width="320" height="43.5"/>
|
||||
<autoresizingMask key="autoresizingMask"/>
|
||||
<tableViewCellContentView key="contentView" opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center" preservesSuperviewLayoutMargins="YES" insetsLayoutMarginsFromSafeArea="NO" tableViewCell="JZ4-vZ-MnG" id="TWb-p9-EMM">
|
||||
<rect key="frame" x="0.0" y="0.0" width="320" height="43.5"/>
|
||||
<autoresizingMask key="autoresizingMask"/>
|
||||
<subviews>
|
||||
<label opaque="NO" multipleTouchEnabled="YES" contentMode="left" insetsLayoutMarginsFromSafeArea="NO" text="– no results –" textAlignment="center" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" minimumScaleFactor="0.80000001192092896" adjustsFontSizeToFit="NO" id="bmQ-Cn-BOm">
|
||||
<rect key="frame" x="16" y="0.0" width="288" height="43.5"/>
|
||||
<autoresizingMask key="autoresizingMask"/>
|
||||
<fontDescription key="fontDescription" style="UICTFontTextStyleBody"/>
|
||||
<color key="textColor" systemColor="secondaryLabelColor" red="0.23529411759999999" green="0.23529411759999999" blue="0.26274509800000001" alpha="0.59999999999999998" colorSpace="custom" customColorSpace="sRGB"/>
|
||||
<nil key="highlightedColor"/>
|
||||
</label>
|
||||
</subviews>
|
||||
</tableViewCellContentView>
|
||||
</tableViewCell>
|
||||
</prototypes>
|
||||
<connections>
|
||||
<outlet property="dataSource" destination="50g-BI-Q6S" id="SFM-IM-FRx"/>
|
||||
<outlet property="delegate" destination="50g-BI-Q6S" id="LBY-sp-dg0"/>
|
||||
</connections>
|
||||
</tableView>
|
||||
<navigationItem key="navigationItem" title="Logs" id="AXT-fV-keV">
|
||||
<rightBarButtonItems>
|
||||
<barButtonItem systemItem="action" id="UkE-Wi-JjW">
|
||||
<connections>
|
||||
<segue destination="1e2-YP-lHV" kind="push" id="JL4-tN-vZg"/>
|
||||
</connections>
|
||||
</barButtonItem>
|
||||
<barButtonItem image="line-expand" id="xLc-O7-KVB">
|
||||
<connections>
|
||||
<action selector="toggleDisplayStyle:" destination="50g-BI-Q6S" id="3wo-9O-7gV"/>
|
||||
</connections>
|
||||
</barButtonItem>
|
||||
</rightBarButtonItems>
|
||||
</navigationItem>
|
||||
<simulatedNavigationBarMetrics key="simulatedTopBarMetrics" prompted="NO"/>
|
||||
</tableViewController>
|
||||
<placeholder placeholderIdentifier="IBFirstResponder" id="lan-I9-b0a" userLabel="First Responder" customClass="UIResponder" sceneMemberID="firstResponder"/>
|
||||
</objects>
|
||||
<point key="canvasLocation" x="1400" y="0.0"/>
|
||||
</scene>
|
||||
<!--Contribute-->
|
||||
<scene sceneID="np1-8y-nci">
|
||||
<objects>
|
||||
<tableViewController id="1e2-YP-lHV" customClass="TVCShareRecording" customModule="AppCheck" customModuleProvider="target" sceneMemberID="viewController">
|
||||
<tableView key="view" clipsSubviews="YES" contentMode="scaleToFill" alwaysBounceVertical="YES" dataMode="prototypes" style="grouped" separatorStyle="default" rowHeight="-1" estimatedRowHeight="-1" sectionHeaderHeight="18" sectionFooterHeight="18" id="Q9f-Bw-9h3">
|
||||
<rect key="frame" x="0.0" y="0.0" width="320" height="369.5"/>
|
||||
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
|
||||
<color key="backgroundColor" systemColor="systemBackgroundColor" cocoaTouchSystemColor="whiteColor"/>
|
||||
<prototypes>
|
||||
<tableViewCell clipsSubviews="YES" userInteractionEnabled="NO" contentMode="scaleToFill" preservesSuperviewLayoutMargins="YES" selectionStyle="none" indentationWidth="10" reuseIdentifier="shareTextCell" textLabel="Jpk-wK-OOM" style="IBUITableViewCellStyleDefault" id="9z8-9E-JVQ">
|
||||
<rect key="frame" x="0.0" y="55.5" width="320" height="43.5"/>
|
||||
<autoresizingMask key="autoresizingMask"/>
|
||||
<tableViewCellContentView key="contentView" opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center" preservesSuperviewLayoutMargins="YES" insetsLayoutMarginsFromSafeArea="NO" tableViewCell="9z8-9E-JVQ" id="TtO-r6-0fS">
|
||||
<rect key="frame" x="0.0" y="0.0" width="320" height="43.5"/>
|
||||
<autoresizingMask key="autoresizingMask"/>
|
||||
<subviews>
|
||||
<label opaque="NO" multipleTouchEnabled="YES" contentMode="left" insetsLayoutMarginsFromSafeArea="NO" text="Description" lineBreakMode="tailTruncation" numberOfLines="0" baselineAdjustment="alignBaselines" minimumScaleFactor="0.90000000000000002" id="Jpk-wK-OOM">
|
||||
<rect key="frame" x="16" y="0.0" width="288" height="43.5"/>
|
||||
<autoresizingMask key="autoresizingMask"/>
|
||||
<fontDescription key="fontDescription" style="UICTFontTextStyleSubhead"/>
|
||||
<nil key="highlightedColor"/>
|
||||
</label>
|
||||
</subviews>
|
||||
</tableViewCellContentView>
|
||||
</tableViewCell>
|
||||
<tableViewCell clipsSubviews="YES" contentMode="scaleToFill" preservesSuperviewLayoutMargins="YES" selectionStyle="none" indentationWidth="10" reuseIdentifier="shareCheckboxCell" textLabel="OM7-b6-6E8" style="IBUITableViewCellStyleDefault" id="q9q-X3-TNl">
|
||||
<rect key="frame" x="0.0" y="99" width="320" height="43.5"/>
|
||||
<autoresizingMask key="autoresizingMask"/>
|
||||
<tableViewCellContentView key="contentView" opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center" preservesSuperviewLayoutMargins="YES" insetsLayoutMarginsFromSafeArea="NO" tableViewCell="q9q-X3-TNl" id="Hf9-5j-89Q">
|
||||
<rect key="frame" x="0.0" y="0.0" width="320" height="43.5"/>
|
||||
<autoresizingMask key="autoresizingMask"/>
|
||||
<subviews>
|
||||
<label opaque="NO" multipleTouchEnabled="YES" contentMode="left" insetsLayoutMarginsFromSafeArea="NO" text="Title" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" id="OM7-b6-6E8">
|
||||
<rect key="frame" x="16" y="0.0" width="288" height="43.5"/>
|
||||
<autoresizingMask key="autoresizingMask"/>
|
||||
<fontDescription key="fontDescription" style="UICTFontTextStyleBody"/>
|
||||
<nil key="highlightedColor"/>
|
||||
</label>
|
||||
<switch opaque="NO" contentMode="scaleToFill" horizontalHuggingPriority="750" verticalHuggingPriority="750" contentHorizontalAlignment="center" contentVerticalAlignment="center" on="YES" translatesAutoresizingMaskIntoConstraints="NO" id="5am-ax-7O0">
|
||||
<rect key="frame" x="257" y="6" width="49" height="31"/>
|
||||
<autoresizingMask key="autoresizingMask" flexibleMinX="YES" flexibleMaxY="YES"/>
|
||||
<connections>
|
||||
<action selector="didChangeNotesCheckbox:" destination="1e2-YP-lHV" eventType="valueChanged" id="xzg-3J-0IT"/>
|
||||
</connections>
|
||||
</switch>
|
||||
</subviews>
|
||||
</tableViewCellContentView>
|
||||
<connections>
|
||||
<outlet property="accessoryView" destination="5am-ax-7O0" id="fw9-Nb-leO"/>
|
||||
</connections>
|
||||
</tableViewCell>
|
||||
<tableViewCell clipsSubviews="YES" contentMode="scaleToFill" preservesSuperviewLayoutMargins="YES" selectionStyle="default" accessoryType="disclosureIndicator" indentationWidth="10" reuseIdentifier="shareOpenTextCell" textLabel="842-tZ-Dai" style="IBUITableViewCellStyleDefault" id="Fiz-tT-R1A">
|
||||
<rect key="frame" x="0.0" y="142.5" width="320" height="43.5"/>
|
||||
<autoresizingMask key="autoresizingMask"/>
|
||||
<tableViewCellContentView key="contentView" opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center" preservesSuperviewLayoutMargins="YES" insetsLayoutMarginsFromSafeArea="NO" tableViewCell="Fiz-tT-R1A" id="ZfU-g2-ohM">
|
||||
<rect key="frame" x="0.0" y="0.0" width="293" height="43.5"/>
|
||||
<autoresizingMask key="autoresizingMask"/>
|
||||
<subviews>
|
||||
<label opaque="NO" multipleTouchEnabled="YES" contentMode="left" insetsLayoutMarginsFromSafeArea="NO" text="Open Text" lineBreakMode="tailTruncation" numberOfLines="6" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" id="842-tZ-Dai">
|
||||
<rect key="frame" x="16" y="0.0" width="269" height="43.5"/>
|
||||
<autoresizingMask key="autoresizingMask"/>
|
||||
<fontDescription key="fontDescription" style="UICTFontTextStyleSubhead"/>
|
||||
<nil key="highlightedColor"/>
|
||||
</label>
|
||||
</subviews>
|
||||
</tableViewCellContentView>
|
||||
<connections>
|
||||
<segue destination="5DP-MB-rOM" kind="push" id="sLA-GQ-Jxx"/>
|
||||
</connections>
|
||||
</tableViewCell>
|
||||
<tableViewCell clipsSubviews="YES" userInteractionEnabled="NO" contentMode="scaleToFill" preservesSuperviewLayoutMargins="YES" selectionStyle="none" indentationWidth="10" reuseIdentifier="shareKeyValueCell" textLabel="skk-x9-pZl" detailTextLabel="Z4C-mX-qNQ" style="IBUITableViewCellStyleValue2" id="3xN-XZ-3WD">
|
||||
<rect key="frame" x="0.0" y="186" width="320" height="43.5"/>
|
||||
<autoresizingMask key="autoresizingMask"/>
|
||||
<tableViewCellContentView key="contentView" opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center" preservesSuperviewLayoutMargins="YES" insetsLayoutMarginsFromSafeArea="NO" tableViewCell="3xN-XZ-3WD" id="V5u-dB-PAq">
|
||||
<rect key="frame" x="0.0" y="0.0" width="320" height="43.5"/>
|
||||
<autoresizingMask key="autoresizingMask"/>
|
||||
<subviews>
|
||||
<label opaque="NO" multipleTouchEnabled="YES" contentMode="left" insetsLayoutMarginsFromSafeArea="NO" text="Title" textAlignment="right" lineBreakMode="tailTruncation" numberOfLines="2" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" id="skk-x9-pZl">
|
||||
<rect key="frame" x="16" y="13" width="91" height="18"/>
|
||||
<autoresizingMask key="autoresizingMask"/>
|
||||
<fontDescription key="fontDescription" style="UICTFontTextStyleSubhead"/>
|
||||
<color key="textColor" systemColor="secondaryLabelColor" red="0.23529411759999999" green="0.23529411759999999" blue="0.26274509800000001" alpha="0.59999999999999998" colorSpace="custom" customColorSpace="sRGB"/>
|
||||
<nil key="highlightedColor"/>
|
||||
</label>
|
||||
<label opaque="NO" multipleTouchEnabled="YES" contentMode="left" insetsLayoutMarginsFromSafeArea="NO" text="Detail" textAlignment="natural" lineBreakMode="tailTruncation" numberOfLines="2" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" id="Z4C-mX-qNQ">
|
||||
<rect key="frame" x="113" y="13" width="39.5" height="18"/>
|
||||
<autoresizingMask key="autoresizingMask"/>
|
||||
<fontDescription key="fontDescription" style="UICTFontTextStyleSubhead"/>
|
||||
<nil key="textColor"/>
|
||||
<nil key="highlightedColor"/>
|
||||
</label>
|
||||
</subviews>
|
||||
</tableViewCellContentView>
|
||||
</tableViewCell>
|
||||
<tableViewCell clipsSubviews="YES" contentMode="scaleToFill" preservesSuperviewLayoutMargins="YES" selectionStyle="default" accessoryType="checkmark" indentationWidth="10" reuseIdentifier="shareLogCell" textLabel="c0B-OV-ujb" detailTextLabel="sAD-Ns-DV6" style="IBUITableViewCellStyleSubtitle" id="ilN-ct-Db4">
|
||||
<rect key="frame" x="0.0" y="229.5" width="320" height="58.5"/>
|
||||
<autoresizingMask key="autoresizingMask"/>
|
||||
<tableViewCellContentView key="contentView" opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center" preservesSuperviewLayoutMargins="YES" insetsLayoutMarginsFromSafeArea="NO" tableViewCell="ilN-ct-Db4" id="xMK-8l-diB">
|
||||
<rect key="frame" x="0.0" y="0.0" width="280" height="58.5"/>
|
||||
<autoresizingMask key="autoresizingMask"/>
|
||||
<subviews>
|
||||
<label opaque="NO" multipleTouchEnabled="YES" contentMode="left" insetsLayoutMarginsFromSafeArea="NO" text="Title" textAlignment="natural" lineBreakMode="tailTruncation" numberOfLines="2" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" id="c0B-OV-ujb">
|
||||
<rect key="frame" x="16" y="8" width="33.5" height="20.5"/>
|
||||
<autoresizingMask key="autoresizingMask"/>
|
||||
<fontDescription key="fontDescription" style="UICTFontTextStyleBody"/>
|
||||
<nil key="highlightedColor"/>
|
||||
</label>
|
||||
<label opaque="NO" multipleTouchEnabled="YES" contentMode="left" insetsLayoutMarginsFromSafeArea="NO" text="Detail" textAlignment="natural" lineBreakMode="tailTruncation" numberOfLines="4" baselineAdjustment="alignBaselines" adjustsLetterSpacingToFitWidth="YES" adjustsFontSizeToFit="NO" id="sAD-Ns-DV6">
|
||||
<rect key="frame" x="16" y="31.5" width="35" height="16"/>
|
||||
<autoresizingMask key="autoresizingMask"/>
|
||||
<fontDescription key="fontDescription" style="UICTFontTextStyleFootnote"/>
|
||||
<color key="textColor" systemColor="secondaryLabelColor" red="0.23529411759999999" green="0.23529411759999999" blue="0.26274509800000001" alpha="0.59999999999999998" colorSpace="custom" customColorSpace="sRGB"/>
|
||||
<nil key="highlightedColor"/>
|
||||
</label>
|
||||
</subviews>
|
||||
</tableViewCellContentView>
|
||||
</tableViewCell>
|
||||
</prototypes>
|
||||
<connections>
|
||||
<outlet property="dataSource" destination="1e2-YP-lHV" id="nSk-Aa-sk1"/>
|
||||
<outlet property="delegate" destination="1e2-YP-lHV" id="bQg-k9-Jvt"/>
|
||||
</connections>
|
||||
</tableView>
|
||||
<navigationItem key="navigationItem" title="Contribute" id="rd1-ra-OBF">
|
||||
<barButtonItem key="rightBarButtonItem" title="Send" id="N7P-2k-lkO">
|
||||
<connections>
|
||||
<action selector="shareRecording:" destination="1e2-YP-lHV" id="QrV-1X-bQ3"/>
|
||||
</connections>
|
||||
</barButtonItem>
|
||||
</navigationItem>
|
||||
<connections>
|
||||
<outlet property="sendButton" destination="N7P-2k-lkO" id="POi-w1-U0C"/>
|
||||
</connections>
|
||||
</tableViewController>
|
||||
<placeholder placeholderIdentifier="IBFirstResponder" id="cIS-61-X0s" userLabel="First Responder" customClass="UIResponder" sceneMemberID="firstResponder"/>
|
||||
</objects>
|
||||
<point key="canvasLocation" x="2100" y="0.0"/>
|
||||
</scene>
|
||||
<!--Edit Notes-->
|
||||
<scene sceneID="1PP-Uc-VkW">
|
||||
<objects>
|
||||
<viewController hidesBottomBarWhenPushed="YES" id="5DP-MB-rOM" customClass="VCEditText" customModule="AppCheck" customModuleProvider="target" sceneMemberID="viewController">
|
||||
<view key="view" contentMode="scaleToFill" preservesSuperviewLayoutMargins="YES" translatesAutoresizingMaskIntoConstraints="NO" id="F5N-5G-rm9">
|
||||
<rect key="frame" x="0.0" y="0.0" width="320" height="325.5"/>
|
||||
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
|
||||
<subviews>
|
||||
<textView clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="scaleToFill" showsHorizontalScrollIndicator="NO" contentInsetAdjustmentBehavior="never" text="Lorem ipsum dolor sit er elit lamet" textAlignment="natural" translatesAutoresizingMaskIntoConstraints="NO" id="pCU-n1-q6J">
|
||||
<rect key="frame" x="0.0" y="0.0" width="320" height="369.5"/>
|
||||
<color key="backgroundColor" systemColor="systemBackgroundColor" cocoaTouchSystemColor="whiteColor"/>
|
||||
<color key="textColor" systemColor="labelColor" cocoaTouchSystemColor="darkTextColor"/>
|
||||
<fontDescription key="fontDescription" style="UICTFontTextStyleSubhead"/>
|
||||
<textInputTraits key="textInputTraits" autocapitalizationType="sentences"/>
|
||||
</textView>
|
||||
</subviews>
|
||||
<color key="backgroundColor" systemColor="systemBackgroundColor" cocoaTouchSystemColor="whiteColor"/>
|
||||
<constraints>
|
||||
<constraint firstItem="pCU-n1-q6J" firstAttribute="leading" secondItem="5JR-8n-qUg" secondAttribute="leading" id="7fM-0S-SUi"/>
|
||||
<constraint firstItem="5JR-8n-qUg" firstAttribute="bottom" secondItem="pCU-n1-q6J" secondAttribute="bottom" id="AG3-zo-ek0"/>
|
||||
<constraint firstItem="pCU-n1-q6J" firstAttribute="top" secondItem="5JR-8n-qUg" secondAttribute="top" id="IM4-ty-wxG"/>
|
||||
<constraint firstItem="pCU-n1-q6J" firstAttribute="trailing" secondItem="5JR-8n-qUg" secondAttribute="trailing" id="dMB-pY-HEO"/>
|
||||
</constraints>
|
||||
<viewLayoutGuide key="safeArea" id="5JR-8n-qUg"/>
|
||||
</view>
|
||||
<extendedEdge key="edgesForExtendedLayout"/>
|
||||
<navigationItem key="navigationItem" title="Edit Notes" id="Ex8-YV-Ebv"/>
|
||||
<connections>
|
||||
<outlet property="textBottom" destination="AG3-zo-ek0" id="EXG-Zb-UGb"/>
|
||||
<outlet property="textView" destination="pCU-n1-q6J" id="K4D-t0-F9P"/>
|
||||
</connections>
|
||||
</viewController>
|
||||
<placeholder placeholderIdentifier="IBFirstResponder" id="l5b-JD-MQb" userLabel="First Responder" customClass="UIResponder" sceneMemberID="firstResponder"/>
|
||||
</objects>
|
||||
<point key="canvasLocation" x="2800" y="0.0"/>
|
||||
</scene>
|
||||
<!--Edit Recording-->
|
||||
<scene sceneID="pqx-CU-4AP">
|
||||
<objects>
|
||||
<viewController id="VRk-wv-rhk" customClass="VCEditRecording" customModule="AppCheck" customModuleProvider="target" sceneMemberID="viewController">
|
||||
<view key="view" contentMode="scaleToFill" id="rXz-Mk-wrK">
|
||||
<rect key="frame" x="0.0" y="0.0" width="320" height="369.5"/>
|
||||
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
|
||||
<subviews>
|
||||
<navigationBar contentMode="scaleToFill" translucent="NO" translatesAutoresizingMaskIntoConstraints="NO" id="2yS-xK-Wac">
|
||||
<rect key="frame" x="0.0" y="0.0" width="320" height="44"/>
|
||||
<gestureRecognizers/>
|
||||
<items>
|
||||
<navigationItem title="Edit" id="JSi-oz-VRx">
|
||||
<barButtonItem key="leftBarButtonItem" systemItem="cancel" id="TGg-60-wZW">
|
||||
<connections>
|
||||
<action selector="didTapCancel" destination="VRk-wv-rhk" id="idg-Q7-qLu"/>
|
||||
</connections>
|
||||
</barButtonItem>
|
||||
<barButtonItem key="rightBarButtonItem" enabled="NO" style="done" systemItem="save" id="rWg-hE-Ydl">
|
||||
<connections>
|
||||
<action selector="didTapSave" destination="VRk-wv-rhk" id="fPE-i2-I06"/>
|
||||
</connections>
|
||||
</barButtonItem>
|
||||
</navigationItem>
|
||||
</items>
|
||||
<connections>
|
||||
<outletCollection property="gestureRecognizers" destination="B0n-l6-MKc" appends="YES" id="57w-bJ-Vjh"/>
|
||||
</connections>
|
||||
</navigationBar>
|
||||
<stackView opaque="NO" contentMode="scaleToFill" axis="vertical" alignment="center" spacing="8" translatesAutoresizingMaskIntoConstraints="NO" id="xdn-EU-IMx">
|
||||
<rect key="frame" x="16" y="52" width="288" height="307.5"/>
|
||||
<subviews>
|
||||
<view contentMode="scaleToFill" translatesAutoresizingMaskIntoConstraints="NO" id="Guy-Ra-fpS" userLabel="Title">
|
||||
<rect key="frame" x="0.0" y="0.0" width="288" height="40"/>
|
||||
<subviews>
|
||||
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="AppCheck" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="Et0-8d-CId">
|
||||
<rect key="frame" x="0.0" y="2" width="80" height="20.5"/>
|
||||
<fontDescription key="fontDescription" style="UICTFontTextStyleBody"/>
|
||||
<nil key="textColor"/>
|
||||
<nil key="highlightedColor"/>
|
||||
</label>
|
||||
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="University Bamberg" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="l8O-Kw-uc8">
|
||||
<rect key="frame" x="0.0" y="23.5" width="111" height="14.5"/>
|
||||
<fontDescription key="fontDescription" style="UICTFontTextStyleCaption1"/>
|
||||
<color key="textColor" systemColor="secondaryLabelColor" red="0.23529411759999999" green="0.23529411759999999" blue="0.26274509800000001" alpha="0.59999999999999998" colorSpace="custom" customColorSpace="sRGB"/>
|
||||
<nil key="highlightedColor"/>
|
||||
</label>
|
||||
<imageView clipsSubviews="YES" userInteractionEnabled="NO" contentMode="scaleAspectFit" horizontalHuggingPriority="251" verticalHuggingPriority="251" image="LaunchIcon.png" translatesAutoresizingMaskIntoConstraints="NO" id="rbW-pK-Kct">
|
||||
<rect key="frame" x="248" y="0.0" width="40" height="40"/>
|
||||
<constraints>
|
||||
<constraint firstAttribute="width" secondItem="rbW-pK-Kct" secondAttribute="height" multiplier="1:1" id="dV9-kR-y39"/>
|
||||
</constraints>
|
||||
</imageView>
|
||||
</subviews>
|
||||
<color key="backgroundColor" systemColor="systemBackgroundColor" cocoaTouchSystemColor="whiteColor"/>
|
||||
<gestureRecognizers/>
|
||||
<constraints>
|
||||
<constraint firstAttribute="height" priority="999" constant="40" id="5ew-Cq-VKh"/>
|
||||
<constraint firstItem="rbW-pK-Kct" firstAttribute="leading" relation="greaterThanOrEqual" secondItem="Et0-8d-CId" secondAttribute="trailing" constant="4" id="EzM-br-BIF"/>
|
||||
<constraint firstItem="Et0-8d-CId" firstAttribute="leading" secondItem="Guy-Ra-fpS" secondAttribute="leading" id="F1b-aQ-6rA"/>
|
||||
<constraint firstAttribute="bottom" secondItem="l8O-Kw-uc8" secondAttribute="bottom" priority="999" constant="2" id="Tpw-nU-HHb"/>
|
||||
<constraint firstItem="l8O-Kw-uc8" firstAttribute="top" secondItem="Et0-8d-CId" secondAttribute="bottom" priority="999" constant="1" id="Wpc-8H-6b8"/>
|
||||
<constraint firstItem="l8O-Kw-uc8" firstAttribute="leading" secondItem="Guy-Ra-fpS" secondAttribute="leading" id="Xmq-Pl-TrJ"/>
|
||||
<constraint firstItem="rbW-pK-Kct" firstAttribute="centerY" secondItem="Guy-Ra-fpS" secondAttribute="centerY" id="bYB-Jd-Meb"/>
|
||||
<constraint firstAttribute="trailing" secondItem="rbW-pK-Kct" secondAttribute="trailing" id="bsC-L7-fZn"/>
|
||||
<constraint firstItem="rbW-pK-Kct" firstAttribute="leading" relation="greaterThanOrEqual" secondItem="l8O-Kw-uc8" secondAttribute="trailing" constant="4" id="fJ8-TH-hwT"/>
|
||||
<constraint firstItem="Et0-8d-CId" firstAttribute="top" secondItem="Guy-Ra-fpS" secondAttribute="top" priority="999" constant="2" id="nXu-FP-JVX"/>
|
||||
<constraint firstItem="rbW-pK-Kct" firstAttribute="height" secondItem="Guy-Ra-fpS" secondAttribute="height" id="zLK-Gu-HcF"/>
|
||||
</constraints>
|
||||
<connections>
|
||||
<outletCollection property="gestureRecognizers" destination="Jab-q2-U9X" appends="YES" id="V27-A7-AL5"/>
|
||||
</connections>
|
||||
</view>
|
||||
<view contentMode="scaleToFill" translatesAutoresizingMaskIntoConstraints="NO" id="ybL-UG-dwT" userLabel="Notes">
|
||||
<rect key="frame" x="0.0" y="48" width="288" height="160.5"/>
|
||||
<subviews>
|
||||
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="Notes" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" id="QJp-6C-yoZ">
|
||||
<rect key="frame" x="0.0" y="0.0" width="288" height="24"/>
|
||||
<autoresizingMask key="autoresizingMask" widthSizable="YES" flexibleMaxY="YES"/>
|
||||
<fontDescription key="fontDescription" style="UICTFontTextStyleTitle2"/>
|
||||
<nil key="textColor"/>
|
||||
<nil key="highlightedColor"/>
|
||||
</label>
|
||||
<textView clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="scaleToFill" showsHorizontalScrollIndicator="NO" translatesAutoresizingMaskIntoConstraints="NO" id="NXU-yU-eST">
|
||||
<rect key="frame" x="0.0" y="24" width="288" height="136.5"/>
|
||||
<color key="backgroundColor" systemColor="systemBackgroundColor" cocoaTouchSystemColor="whiteColor"/>
|
||||
<string key="text">1. Line
|
||||
2. Line
|
||||
3. Line
|
||||
4. Line</string>
|
||||
<color key="textColor" systemColor="labelColor" cocoaTouchSystemColor="darkTextColor"/>
|
||||
<fontDescription key="fontDescription" style="UICTFontTextStyleSubhead"/>
|
||||
<textInputTraits key="textInputTraits" autocapitalizationType="sentences"/>
|
||||
<connections>
|
||||
<outlet property="delegate" destination="VRk-wv-rhk" id="vej-jI-13V"/>
|
||||
</connections>
|
||||
</textView>
|
||||
</subviews>
|
||||
<color key="backgroundColor" systemColor="systemBackgroundColor" cocoaTouchSystemColor="whiteColor"/>
|
||||
<constraints>
|
||||
<constraint firstItem="NXU-yU-eST" firstAttribute="leading" secondItem="ybL-UG-dwT" secondAttribute="leading" id="D6U-8L-f9m"/>
|
||||
<constraint firstAttribute="height" relation="greaterThanOrEqual" priority="750" constant="107" id="Pfy-uW-kRl"/>
|
||||
<constraint firstItem="NXU-yU-eST" firstAttribute="trailing" secondItem="ybL-UG-dwT" secondAttribute="trailing" id="eBc-6g-nWr"/>
|
||||
<constraint firstItem="NXU-yU-eST" firstAttribute="top" secondItem="QJp-6C-yoZ" secondAttribute="bottom" id="mnZ-WQ-LX8"/>
|
||||
<constraint firstItem="NXU-yU-eST" firstAttribute="bottom" secondItem="ybL-UG-dwT" secondAttribute="bottom" id="vFS-tG-E43"/>
|
||||
</constraints>
|
||||
</view>
|
||||
<view contentMode="scaleToFill" translatesAutoresizingMaskIntoConstraints="NO" id="QiY-Mm-Dej" userLabel="Details">
|
||||
<rect key="frame" x="0.0" y="216.5" width="288" height="91"/>
|
||||
<subviews>
|
||||
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="Details" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" id="FR1-Nt-XuB">
|
||||
<rect key="frame" x="0.0" y="0.0" width="288" height="24"/>
|
||||
<autoresizingMask key="autoresizingMask" widthSizable="YES" flexibleMaxY="YES"/>
|
||||
<fontDescription key="fontDescription" style="UICTFontTextStyleTitle2"/>
|
||||
<nil key="textColor"/>
|
||||
<nil key="highlightedColor"/>
|
||||
</label>
|
||||
<textView clipsSubviews="YES" multipleTouchEnabled="YES" userInteractionEnabled="NO" contentMode="scaleToFill" bounces="NO" scrollEnabled="NO" showsHorizontalScrollIndicator="NO" showsVerticalScrollIndicator="NO" bouncesZoom="NO" editable="NO" selectable="NO" translatesAutoresizingMaskIntoConstraints="NO" id="pql-H5-k6U">
|
||||
<rect key="frame" x="0.0" y="24" width="288" height="67"/>
|
||||
<color key="backgroundColor" systemColor="systemBackgroundColor" cocoaTouchSystemColor="whiteColor"/>
|
||||
<string key="text">Start: 1970-01-01 01:00
|
||||
End: 1970-01-01 02:00
|
||||
Duration: 60:00</string>
|
||||
<color key="textColor" systemColor="labelColor" cocoaTouchSystemColor="darkTextColor"/>
|
||||
<fontDescription key="fontDescription" type="system" weight="light" pointSize="14"/>
|
||||
<textInputTraits key="textInputTraits" autocapitalizationType="sentences"/>
|
||||
</textView>
|
||||
<button opaque="NO" contentMode="scaleToFill" contentHorizontalAlignment="right" contentVerticalAlignment="center" lineBreakMode="middleTruncation" translatesAutoresizingMaskIntoConstraints="NO" id="LOr-e7-foG">
|
||||
<rect key="frame" x="268" y="46.5" width="20" height="22"/>
|
||||
<state key="normal" image="filter-clear"/>
|
||||
<connections>
|
||||
<action selector="didTapFilter" destination="VRk-wv-rhk" eventType="touchUpInside" id="5NH-qa-yMg"/>
|
||||
</connections>
|
||||
</button>
|
||||
</subviews>
|
||||
<color key="backgroundColor" systemColor="systemBackgroundColor" cocoaTouchSystemColor="whiteColor"/>
|
||||
<constraints>
|
||||
<constraint firstAttribute="trailing" secondItem="pql-H5-k6U" secondAttribute="trailing" id="44x-p3-qWK"/>
|
||||
<constraint firstItem="LOr-e7-foG" firstAttribute="trailing" secondItem="pql-H5-k6U" secondAttribute="trailing" id="5sr-Cf-h0c"/>
|
||||
<constraint firstItem="pql-H5-k6U" firstAttribute="top" secondItem="FR1-Nt-XuB" secondAttribute="bottom" id="As8-Px-t6G"/>
|
||||
<constraint firstItem="pql-H5-k6U" firstAttribute="leading" secondItem="QiY-Mm-Dej" secondAttribute="leading" id="ItB-cO-reV"/>
|
||||
<constraint firstItem="LOr-e7-foG" firstAttribute="centerY" secondItem="pql-H5-k6U" secondAttribute="centerY" id="OLu-MI-3sa"/>
|
||||
<constraint firstAttribute="height" priority="250" constant="91" id="or7-9o-FZb"/>
|
||||
<constraint firstAttribute="bottom" secondItem="pql-H5-k6U" secondAttribute="bottom" id="pbd-43-eN0"/>
|
||||
</constraints>
|
||||
</view>
|
||||
</subviews>
|
||||
<constraints>
|
||||
<constraint firstItem="ybL-UG-dwT" firstAttribute="width" secondItem="xdn-EU-IMx" secondAttribute="width" id="PUH-xO-ZbD"/>
|
||||
<constraint firstItem="QiY-Mm-Dej" firstAttribute="width" secondItem="xdn-EU-IMx" secondAttribute="width" id="U6e-10-j55"/>
|
||||
<constraint firstItem="Guy-Ra-fpS" firstAttribute="width" secondItem="xdn-EU-IMx" secondAttribute="width" id="ZCJ-ol-1Jv"/>
|
||||
</constraints>
|
||||
</stackView>
|
||||
</subviews>
|
||||
<color key="backgroundColor" systemColor="systemBackgroundColor" cocoaTouchSystemColor="whiteColor"/>
|
||||
<constraints>
|
||||
<constraint firstItem="2yS-xK-Wac" firstAttribute="trailing" secondItem="fMa-Lq-tGz" secondAttribute="trailing" id="1io-bA-4p9"/>
|
||||
<constraint firstItem="2yS-xK-Wac" firstAttribute="leading" secondItem="fMa-Lq-tGz" secondAttribute="leading" id="Fv1-fO-22V"/>
|
||||
<constraint firstItem="xdn-EU-IMx" firstAttribute="leading" secondItem="fMa-Lq-tGz" secondAttribute="leading" constant="16" id="JuR-Ro-IPi"/>
|
||||
<constraint firstItem="xdn-EU-IMx" firstAttribute="top" secondItem="2yS-xK-Wac" secondAttribute="bottom" constant="8" id="Lec-83-aaD"/>
|
||||
<constraint firstItem="fMa-Lq-tGz" firstAttribute="trailing" secondItem="xdn-EU-IMx" secondAttribute="trailing" constant="16" id="hhC-bL-G3S"/>
|
||||
<constraint firstItem="fMa-Lq-tGz" firstAttribute="bottom" secondItem="xdn-EU-IMx" secondAttribute="bottom" constant="10" id="p7W-sr-Wch"/>
|
||||
<constraint firstItem="2yS-xK-Wac" firstAttribute="top" secondItem="fMa-Lq-tGz" secondAttribute="top" id="yKh-gv-mgg"/>
|
||||
</constraints>
|
||||
<viewLayoutGuide key="safeArea" id="fMa-Lq-tGz"/>
|
||||
</view>
|
||||
<connections>
|
||||
<outlet property="appDeveloper" destination="l8O-Kw-uc8" id="dfg-s6-Biz"/>
|
||||
<outlet property="appIcon" destination="rbW-pK-Kct" id="VlO-fG-y1a"/>
|
||||
<outlet property="appTitle" destination="Et0-8d-CId" id="HgD-oI-0J8"/>
|
||||
<outlet property="buttonCancel" destination="TGg-60-wZW" id="5Ej-7t-jaD"/>
|
||||
<outlet property="buttonSave" destination="rWg-hE-Ydl" id="zfM-kx-erX"/>
|
||||
<outlet property="chooseAppTap" destination="Jab-q2-U9X" id="Tzv-lm-sUm"/>
|
||||
<outlet property="inputDetails" destination="pql-H5-k6U" id="NXm-8f-5E6"/>
|
||||
<outlet property="inputNotes" destination="NXU-yU-eST" id="c2n-cG-aLq"/>
|
||||
<outlet property="noteBottom" destination="vFS-tG-E43" id="Bxh-Tl-E2U"/>
|
||||
</connections>
|
||||
</viewController>
|
||||
<placeholder placeholderIdentifier="IBFirstResponder" id="KN7-F1-BOL" userLabel="First Responder" customClass="UIResponder" sceneMemberID="firstResponder"/>
|
||||
<tapGestureRecognizer id="Jab-q2-U9X">
|
||||
<connections>
|
||||
<segue destination="qNp-w1-7Md" kind="modal" id="22y-Dy-3xf"/>
|
||||
</connections>
|
||||
</tapGestureRecognizer>
|
||||
<tapGestureRecognizer id="B0n-l6-MKc">
|
||||
<connections>
|
||||
<action selector="hideKeyboard" destination="VRk-wv-rhk" id="eMC-Nn-xoE"/>
|
||||
</connections>
|
||||
</tapGestureRecognizer>
|
||||
</objects>
|
||||
<point key="canvasLocation" x="700" y="700"/>
|
||||
</scene>
|
||||
<!--App Search-->
|
||||
<scene sceneID="n6R-Wm-XxF">
|
||||
<objects>
|
||||
<tableViewController id="qNp-w1-7Md" customClass="TVCAppSearch" customModule="AppCheck" customModuleProvider="target" sceneMemberID="viewController">
|
||||
<tableView key="view" clipsSubviews="YES" contentMode="scaleToFill" alwaysBounceVertical="YES" dataMode="prototypes" style="plain" separatorStyle="default" rowHeight="-1" estimatedRowHeight="-1" sectionHeaderHeight="28" sectionFooterHeight="28" id="WYZ-wA-6Rh">
|
||||
<rect key="frame" x="0.0" y="0.0" width="320" height="369.5"/>
|
||||
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
|
||||
<color key="backgroundColor" systemColor="systemBackgroundColor" cocoaTouchSystemColor="whiteColor"/>
|
||||
<searchBar key="tableHeaderView" contentMode="redraw" preservesSuperviewLayoutMargins="YES" placeholder="Search AppStore" showsCancelButton="YES" id="9dl-ZI-85h">
|
||||
<rect key="frame" x="0.0" y="0.0" width="320" height="56"/>
|
||||
<autoresizingMask key="autoresizingMask" widthSizable="YES" flexibleMaxY="YES"/>
|
||||
<textInputTraits key="textInputTraits" returnKeyType="search" enablesReturnKeyAutomatically="YES"/>
|
||||
<connections>
|
||||
<outlet property="delegate" destination="qNp-w1-7Md" id="vcp-BS-7xF"/>
|
||||
</connections>
|
||||
</searchBar>
|
||||
<prototypes>
|
||||
<tableViewCell clipsSubviews="YES" contentMode="scaleToFill" preservesSuperviewLayoutMargins="YES" selectionStyle="default" indentationWidth="10" reuseIdentifier="AppStoreSearchCell" textLabel="yIh-WB-0rK" detailTextLabel="PDe-1x-vle" style="IBUITableViewCellStyleSubtitle" id="Q8M-0Q-Mc7">
|
||||
<rect key="frame" x="0.0" y="84" width="320" height="57.5"/>
|
||||
<autoresizingMask key="autoresizingMask"/>
|
||||
<tableViewCellContentView key="contentView" opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center" preservesSuperviewLayoutMargins="YES" insetsLayoutMarginsFromSafeArea="NO" tableViewCell="Q8M-0Q-Mc7" id="BPc-V2-I7v">
|
||||
<rect key="frame" x="0.0" y="0.0" width="320" height="57.5"/>
|
||||
<autoresizingMask key="autoresizingMask"/>
|
||||
<subviews>
|
||||
<label opaque="NO" multipleTouchEnabled="YES" contentMode="left" insetsLayoutMarginsFromSafeArea="NO" text="Title" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" id="yIh-WB-0rK">
|
||||
<rect key="frame" x="16" y="9" width="33.5" height="20.5"/>
|
||||
<autoresizingMask key="autoresizingMask"/>
|
||||
<fontDescription key="fontDescription" style="UICTFontTextStyleBody"/>
|
||||
<nil key="textColor"/>
|
||||
<nil key="highlightedColor"/>
|
||||
</label>
|
||||
<label opaque="NO" multipleTouchEnabled="YES" contentMode="left" insetsLayoutMarginsFromSafeArea="NO" text="Detail" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" id="PDe-1x-vle">
|
||||
<rect key="frame" x="16" y="32.5" width="33" height="14.5"/>
|
||||
<autoresizingMask key="autoresizingMask"/>
|
||||
<fontDescription key="fontDescription" style="UICTFontTextStyleCaption1"/>
|
||||
<color key="textColor" systemColor="secondaryLabelColor" red="0.23529411759999999" green="0.23529411759999999" blue="0.26274509800000001" alpha="0.59999999999999998" colorSpace="custom" customColorSpace="sRGB"/>
|
||||
<nil key="highlightedColor"/>
|
||||
</label>
|
||||
</subviews>
|
||||
</tableViewCellContentView>
|
||||
</tableViewCell>
|
||||
</prototypes>
|
||||
<connections>
|
||||
<outlet property="dataSource" destination="qNp-w1-7Md" id="Jdv-03-iVJ"/>
|
||||
<outlet property="delegate" destination="qNp-w1-7Md" id="3vD-b3-Ake"/>
|
||||
</connections>
|
||||
</tableView>
|
||||
<connections>
|
||||
<outlet property="searchBar" destination="9dl-ZI-85h" id="8Zr-E0-mzs"/>
|
||||
</connections>
|
||||
</tableViewController>
|
||||
<placeholder placeholderIdentifier="IBFirstResponder" id="PhN-mC-C3W" userLabel="First Responder" customClass="UIResponder" sceneMemberID="firstResponder"/>
|
||||
</objects>
|
||||
<point key="canvasLocation" x="1400" y="700"/>
|
||||
</scene>
|
||||
</scenes>
|
||||
<resources>
|
||||
<image name="LaunchIcon.png" width="128" height="128"/>
|
||||
<image name="detail-help" width="22" height="22"/>
|
||||
<image name="filter-clear" width="20" height="20"/>
|
||||
<image name="line-expand" width="20" height="20"/>
|
||||
</resources>
|
||||
</document>
|
||||
506
main/GUI/Base.lproj/Requests.storyboard
Normal file
@@ -0,0 +1,506 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<document type="com.apple.InterfaceBuilder3.CocoaTouch.Storyboard.XIB" version="3.0" toolsVersion="16097.2" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" useSafeAreas="YES" colorMatched="YES" initialViewController="pdd-aM-sKl">
|
||||
<device id="retina4_0" orientation="portrait" appearance="light"/>
|
||||
<dependencies>
|
||||
<deployment identifier="iOS"/>
|
||||
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="16087"/>
|
||||
<capability name="Safe area layout guides" minToolsVersion="9.0"/>
|
||||
<capability name="documents saved in the Xcode 8 format" minToolsVersion="8.0"/>
|
||||
</dependencies>
|
||||
<scenes>
|
||||
<!--Domains-->
|
||||
<scene sceneID="MN1-aZ-cZt">
|
||||
<objects>
|
||||
<tableViewController id="pdd-aM-sKl" customClass="TVCDomains" customModule="AppCheck" customModuleProvider="target" sceneMemberID="viewController">
|
||||
<tableView key="view" clipsSubviews="YES" contentMode="scaleToFill" alwaysBounceVertical="YES" dataMode="prototypes" style="plain" separatorStyle="default" rowHeight="-1" estimatedRowHeight="-1" sectionHeaderHeight="28" sectionFooterHeight="28" id="kj3-8X-TyT">
|
||||
<rect key="frame" x="0.0" y="0.0" width="320" height="519"/>
|
||||
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
|
||||
<color key="backgroundColor" systemColor="systemBackgroundColor" cocoaTouchSystemColor="whiteColor"/>
|
||||
<prototypes>
|
||||
<tableViewCell clipsSubviews="YES" contentMode="scaleToFill" insetsLayoutMarginsFromSafeArea="NO" selectionStyle="default" accessoryType="disclosureIndicator" hidesAccessoryWhenEditing="NO" indentationWidth="10" reuseIdentifier="DomainCell" textLabel="0HB-5f-eB1" detailTextLabel="MRe-Eq-gvc" style="IBUITableViewCellStyleSubtitle" id="F8D-aK-j1W">
|
||||
<rect key="frame" x="0.0" y="28" width="320" height="57.5"/>
|
||||
<autoresizingMask key="autoresizingMask"/>
|
||||
<tableViewCellContentView key="contentView" opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center" insetsLayoutMarginsFromSafeArea="NO" tableViewCell="F8D-aK-j1W" id="FY2-xr-hqh">
|
||||
<rect key="frame" x="0.0" y="0.0" width="293" height="57.5"/>
|
||||
<autoresizingMask key="autoresizingMask"/>
|
||||
<subviews>
|
||||
<label opaque="NO" multipleTouchEnabled="YES" contentMode="left" insetsLayoutMarginsFromSafeArea="NO" text="Title" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" minimumScaleFactor="0.80000000000000004" id="0HB-5f-eB1">
|
||||
<rect key="frame" x="16" y="9" width="33.5" height="20.5"/>
|
||||
<autoresizingMask key="autoresizingMask"/>
|
||||
<fontDescription key="fontDescription" style="UICTFontTextStyleBody"/>
|
||||
<nil key="textColor"/>
|
||||
<nil key="highlightedColor"/>
|
||||
</label>
|
||||
<label opaque="NO" multipleTouchEnabled="YES" contentMode="left" insetsLayoutMarginsFromSafeArea="NO" text="Subtitle" textAlignment="natural" lineBreakMode="tailTruncation" numberOfLines="2" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" id="MRe-Eq-gvc">
|
||||
<rect key="frame" x="16" y="32.5" width="44" height="14.5"/>
|
||||
<autoresizingMask key="autoresizingMask"/>
|
||||
<fontDescription key="fontDescription" style="UICTFontTextStyleCaption1"/>
|
||||
<color key="textColor" systemColor="secondaryLabelColor" red="0.23529411759999999" green="0.23529411759999999" blue="0.26274509800000001" alpha="0.59999999999999998" colorSpace="custom" customColorSpace="sRGB"/>
|
||||
<nil key="highlightedColor"/>
|
||||
</label>
|
||||
</subviews>
|
||||
</tableViewCellContentView>
|
||||
<connections>
|
||||
<segue destination="WcC-nb-Vf5" kind="push" id="EVQ-hO-JE9"/>
|
||||
</connections>
|
||||
</tableViewCell>
|
||||
</prototypes>
|
||||
<connections>
|
||||
<outlet property="dataSource" destination="pdd-aM-sKl" id="4fX-iP-7Oa"/>
|
||||
<outlet property="delegate" destination="pdd-aM-sKl" id="3RN-az-SYU"/>
|
||||
</connections>
|
||||
</tableView>
|
||||
<navigationItem key="navigationItem" title="Domains" id="nY5-jL-QT9">
|
||||
<leftBarButtonItems>
|
||||
<barButtonItem image="filter-clear" id="FZm-Ld-jJE">
|
||||
<connections>
|
||||
<action selector="filterButtonTapped:" destination="pdd-aM-sKl" id="Xyy-LF-eCF"/>
|
||||
</connections>
|
||||
</barButtonItem>
|
||||
<barButtonItem enabled="NO" title="7 days" id="wxA-bC-1pN"/>
|
||||
</leftBarButtonItems>
|
||||
</navigationItem>
|
||||
<simulatedNavigationBarMetrics key="simulatedTopBarMetrics" prompted="NO"/>
|
||||
<simulatedTabBarMetrics key="simulatedBottomBarMetrics" translucent="NO"/>
|
||||
<connections>
|
||||
<outlet property="filterButton" destination="FZm-Ld-jJE" id="g96-Q2-cYX"/>
|
||||
<outlet property="filterButtonDetail" destination="wxA-bC-1pN" id="CgP-oz-aXa"/>
|
||||
</connections>
|
||||
</tableViewController>
|
||||
<placeholder placeholderIdentifier="IBFirstResponder" id="jfx-iA-E0v" userLabel="First Responder" customClass="UIResponder" sceneMemberID="firstResponder"/>
|
||||
</objects>
|
||||
<point key="canvasLocation" x="0.0" y="0.0"/>
|
||||
</scene>
|
||||
<!--Hosts-->
|
||||
<scene sceneID="ZCV-Yx-jjW">
|
||||
<objects>
|
||||
<tableViewController storyboardIdentifier="requestsHosts" id="WcC-nb-Vf5" customClass="TVCHosts" customModule="AppCheck" customModuleProvider="target" sceneMemberID="viewController">
|
||||
<tableView key="view" clipsSubviews="YES" contentMode="scaleToFill" alwaysBounceVertical="YES" dataMode="prototypes" style="plain" separatorStyle="default" rowHeight="-1" estimatedRowHeight="-1" sectionHeaderHeight="28" sectionFooterHeight="28" id="nRF-dc-dC2">
|
||||
<rect key="frame" x="0.0" y="0.0" width="320" height="519"/>
|
||||
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
|
||||
<color key="backgroundColor" systemColor="systemBackgroundColor" cocoaTouchSystemColor="whiteColor"/>
|
||||
<containerView key="tableHeaderView" opaque="NO" contentMode="scaleToFill" fixedFrame="YES" translatesAutoresizingMaskIntoConstraints="NO" id="kh4-PQ-hy6">
|
||||
<rect key="frame" x="0.0" y="0.0" width="320" height="49"/>
|
||||
<autoresizingMask key="autoresizingMask" widthSizable="YES" flexibleMinY="YES"/>
|
||||
<connections>
|
||||
<segue destination="1ba-SA-8sT" kind="embed" id="vf1-07-AS4"/>
|
||||
</connections>
|
||||
</containerView>
|
||||
<prototypes>
|
||||
<tableViewCell clipsSubviews="YES" contentMode="scaleToFill" preservesSuperviewLayoutMargins="YES" selectionStyle="default" accessoryType="disclosureIndicator" indentationWidth="10" reuseIdentifier="HostCell" textLabel="Rnk-SP-UHm" detailTextLabel="ovQ-lJ-hWJ" style="IBUITableViewCellStyleSubtitle" id="uv0-9B-Zbb">
|
||||
<rect key="frame" x="0.0" y="77" width="320" height="57.5"/>
|
||||
<autoresizingMask key="autoresizingMask"/>
|
||||
<tableViewCellContentView key="contentView" opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center" preservesSuperviewLayoutMargins="YES" insetsLayoutMarginsFromSafeArea="NO" tableViewCell="uv0-9B-Zbb" id="6vH-Du-gCg">
|
||||
<rect key="frame" x="0.0" y="0.0" width="293" height="57.5"/>
|
||||
<autoresizingMask key="autoresizingMask"/>
|
||||
<subviews>
|
||||
<label opaque="NO" multipleTouchEnabled="YES" contentMode="left" insetsLayoutMarginsFromSafeArea="NO" text="Title" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" id="Rnk-SP-UHm">
|
||||
<rect key="frame" x="16" y="9" width="33.5" height="20.5"/>
|
||||
<autoresizingMask key="autoresizingMask"/>
|
||||
<fontDescription key="fontDescription" style="UICTFontTextStyleBody"/>
|
||||
<nil key="textColor"/>
|
||||
<nil key="highlightedColor"/>
|
||||
</label>
|
||||
<label opaque="NO" multipleTouchEnabled="YES" contentMode="left" insetsLayoutMarginsFromSafeArea="NO" text="Subtitle" textAlignment="natural" lineBreakMode="tailTruncation" numberOfLines="2" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" id="ovQ-lJ-hWJ">
|
||||
<rect key="frame" x="16" y="32.5" width="44" height="14.5"/>
|
||||
<autoresizingMask key="autoresizingMask"/>
|
||||
<fontDescription key="fontDescription" style="UICTFontTextStyleCaption1"/>
|
||||
<color key="textColor" systemColor="secondaryLabelColor" red="0.23529411759999999" green="0.23529411759999999" blue="0.26274509800000001" alpha="0.59999999999999998" colorSpace="custom" customColorSpace="sRGB"/>
|
||||
<nil key="highlightedColor"/>
|
||||
</label>
|
||||
</subviews>
|
||||
</tableViewCellContentView>
|
||||
<connections>
|
||||
<segue destination="h7Z-Qr-pJ5" kind="push" id="TPa-Zn-eOs"/>
|
||||
</connections>
|
||||
</tableViewCell>
|
||||
</prototypes>
|
||||
<connections>
|
||||
<outlet property="dataSource" destination="WcC-nb-Vf5" id="szM-iI-Jgi"/>
|
||||
<outlet property="delegate" destination="WcC-nb-Vf5" id="sBd-BW-Wg6"/>
|
||||
</connections>
|
||||
</tableView>
|
||||
<navigationItem key="navigationItem" title="Hosts" prompt="com.app.Example" id="TvD-8U-F05"/>
|
||||
</tableViewController>
|
||||
<placeholder placeholderIdentifier="IBFirstResponder" id="Gdi-Xi-JUL" userLabel="First Responder" customClass="UIResponder" sceneMemberID="firstResponder"/>
|
||||
</objects>
|
||||
<point key="canvasLocation" x="700" y="0.0"/>
|
||||
</scene>
|
||||
<!--Occurrences-->
|
||||
<scene sceneID="ws3-sK-l8m">
|
||||
<objects>
|
||||
<tableViewController storyboardIdentifier="requestsOccurrences" id="h7Z-Qr-pJ5" customClass="TVCHostDetails" customModule="AppCheck" customModuleProvider="target" sceneMemberID="viewController">
|
||||
<tableView key="view" clipsSubviews="YES" contentMode="scaleToFill" alwaysBounceVertical="YES" dataMode="prototypes" style="plain" separatorStyle="default" rowHeight="-1" estimatedRowHeight="-1" sectionHeaderHeight="28" sectionFooterHeight="28" id="4ms-FO-Fge">
|
||||
<rect key="frame" x="0.0" y="0.0" width="320" height="519"/>
|
||||
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
|
||||
<color key="backgroundColor" systemColor="systemBackgroundColor" cocoaTouchSystemColor="whiteColor"/>
|
||||
<containerView key="tableHeaderView" opaque="NO" contentMode="scaleToFill" fixedFrame="YES" translatesAutoresizingMaskIntoConstraints="NO" id="SxM-2c-aJb">
|
||||
<rect key="frame" x="0.0" y="0.0" width="320" height="49"/>
|
||||
<autoresizingMask key="autoresizingMask" widthSizable="YES" flexibleMinY="YES"/>
|
||||
<connections>
|
||||
<segue destination="1ba-SA-8sT" kind="embed" id="ueN-6L-cP7"/>
|
||||
</connections>
|
||||
</containerView>
|
||||
<prototypes>
|
||||
<tableViewCell clipsSubviews="YES" contentMode="scaleToFill" preservesSuperviewLayoutMargins="YES" selectionStyle="default" accessoryType="disclosureIndicator" indentationWidth="10" reuseIdentifier="HostDetailCell" textLabel="J2P-mU-Vad" detailTextLabel="eWb-mX-udN" style="IBUITableViewCellStyleValue1" id="ZCA-Dz-i92">
|
||||
<rect key="frame" x="0.0" y="77" width="320" height="43.5"/>
|
||||
<autoresizingMask key="autoresizingMask"/>
|
||||
<tableViewCellContentView key="contentView" opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center" preservesSuperviewLayoutMargins="YES" insetsLayoutMarginsFromSafeArea="NO" tableViewCell="ZCA-Dz-i92" id="nxe-48-jAQ">
|
||||
<rect key="frame" x="0.0" y="0.0" width="293" height="43.5"/>
|
||||
<autoresizingMask key="autoresizingMask"/>
|
||||
<subviews>
|
||||
<label opaque="NO" multipleTouchEnabled="YES" contentMode="left" insetsLayoutMarginsFromSafeArea="NO" text="Title" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" minimumScaleFactor="0.80000000000000004" id="J2P-mU-Vad">
|
||||
<rect key="frame" x="16" y="12" width="33.5" height="20.5"/>
|
||||
<autoresizingMask key="autoresizingMask"/>
|
||||
<fontDescription key="fontDescription" style="UICTFontTextStyleBody"/>
|
||||
<nil key="textColor"/>
|
||||
<nil key="highlightedColor"/>
|
||||
</label>
|
||||
<label opaque="NO" multipleTouchEnabled="YES" contentMode="left" insetsLayoutMarginsFromSafeArea="NO" text="Detail" textAlignment="right" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" id="eWb-mX-udN">
|
||||
<rect key="frame" x="241" y="12" width="44" height="20.5"/>
|
||||
<autoresizingMask key="autoresizingMask"/>
|
||||
<fontDescription key="fontDescription" style="UICTFontTextStyleBody"/>
|
||||
<color key="textColor" systemColor="secondaryLabelColor" red="0.23529411759999999" green="0.23529411759999999" blue="0.26274509800000001" alpha="0.59999999999999998" colorSpace="custom" customColorSpace="sRGB"/>
|
||||
<nil key="highlightedColor"/>
|
||||
</label>
|
||||
</subviews>
|
||||
</tableViewCellContentView>
|
||||
<connections>
|
||||
<segue destination="rjy-Di-Cru" kind="push" id="SfC-iY-Ce0"/>
|
||||
</connections>
|
||||
</tableViewCell>
|
||||
</prototypes>
|
||||
<connections>
|
||||
<outlet property="dataSource" destination="h7Z-Qr-pJ5" id="fyW-Av-fWY"/>
|
||||
<outlet property="delegate" destination="h7Z-Qr-pJ5" id="gBq-jA-u5V"/>
|
||||
</connections>
|
||||
</tableView>
|
||||
<navigationItem key="navigationItem" title="Occurrences" prompt="com.domain.network.cdn" id="bys-2u-rHs"/>
|
||||
</tableViewController>
|
||||
<placeholder placeholderIdentifier="IBFirstResponder" id="UxH-PH-KQy" userLabel="First Responder" customClass="UIResponder" sceneMemberID="firstResponder"/>
|
||||
</objects>
|
||||
<point key="canvasLocation" x="1400" y="0.0"/>
|
||||
</scene>
|
||||
<!--Occurrence Context-->
|
||||
<scene sceneID="A1T-7G-agr">
|
||||
<objects>
|
||||
<tableViewController id="rjy-Di-Cru" customClass="TVCOccurrenceContext" customModule="AppCheck" customModuleProvider="target" sceneMemberID="viewController">
|
||||
<tableView key="view" clipsSubviews="YES" contentMode="scaleToFill" alwaysBounceVertical="YES" dataMode="prototypes" style="plain" separatorStyle="default" rowHeight="-1" estimatedRowHeight="-1" sectionHeaderHeight="28" sectionFooterHeight="28" id="EfM-yv-85f">
|
||||
<rect key="frame" x="0.0" y="0.0" width="320" height="519"/>
|
||||
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
|
||||
<color key="backgroundColor" systemColor="systemBackgroundColor" cocoaTouchSystemColor="whiteColor"/>
|
||||
<prototypes>
|
||||
<tableViewCell clipsSubviews="YES" contentMode="scaleToFill" preservesSuperviewLayoutMargins="YES" selectionStyle="none" indentationWidth="10" reuseIdentifier="OccurrenceContextCell" textLabel="xgq-hW-e3R" detailTextLabel="No8-Bf-ptL" style="IBUITableViewCellStyleValue2" id="KQh-Ei-If8">
|
||||
<rect key="frame" x="0.0" y="28" width="320" height="43.5"/>
|
||||
<autoresizingMask key="autoresizingMask"/>
|
||||
<tableViewCellContentView key="contentView" opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center" preservesSuperviewLayoutMargins="YES" insetsLayoutMarginsFromSafeArea="NO" tableViewCell="KQh-Ei-If8" id="i32-u4-1Q8">
|
||||
<rect key="frame" x="0.0" y="0.0" width="320" height="43.5"/>
|
||||
<autoresizingMask key="autoresizingMask"/>
|
||||
<subviews>
|
||||
<label opaque="NO" multipleTouchEnabled="YES" contentMode="left" insetsLayoutMarginsFromSafeArea="NO" text="Title" textAlignment="right" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" id="xgq-hW-e3R">
|
||||
<rect key="frame" x="16" y="12" width="91" height="20.5"/>
|
||||
<autoresizingMask key="autoresizingMask"/>
|
||||
<fontDescription key="fontDescription" style="UICTFontTextStyleBody"/>
|
||||
<color key="textColor" systemColor="secondaryLabelColor" red="0.23529411759999999" green="0.23529411759999999" blue="0.26274509800000001" alpha="0.59999999999999998" colorSpace="custom" customColorSpace="sRGB"/>
|
||||
<nil key="highlightedColor"/>
|
||||
</label>
|
||||
<label opaque="NO" multipleTouchEnabled="YES" contentMode="left" insetsLayoutMarginsFromSafeArea="NO" text="Subtitle" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" minimumScaleFactor="0.80000000000000004" id="No8-Bf-ptL">
|
||||
<rect key="frame" x="113" y="12" width="59" height="20.5"/>
|
||||
<autoresizingMask key="autoresizingMask"/>
|
||||
<fontDescription key="fontDescription" style="UICTFontTextStyleBody"/>
|
||||
<nil key="textColor"/>
|
||||
<nil key="highlightedColor"/>
|
||||
</label>
|
||||
</subviews>
|
||||
</tableViewCellContentView>
|
||||
</tableViewCell>
|
||||
</prototypes>
|
||||
<connections>
|
||||
<outlet property="dataSource" destination="rjy-Di-Cru" id="6CT-Vd-Ixn"/>
|
||||
<outlet property="delegate" destination="rjy-Di-Cru" id="JtY-um-ZTF"/>
|
||||
</connections>
|
||||
</tableView>
|
||||
<navigationItem key="navigationItem" title="Occurrence Context" id="2mj-It-uND">
|
||||
<barButtonItem key="rightBarButtonItem" image="jump-to-target" id="TqX-qO-B3s">
|
||||
<connections>
|
||||
<action selector="jumpToTsZero" destination="rjy-Di-Cru" id="RS6-IO-hi4"/>
|
||||
</connections>
|
||||
</barButtonItem>
|
||||
</navigationItem>
|
||||
</tableViewController>
|
||||
<placeholder placeholderIdentifier="IBFirstResponder" id="cYd-oX-akc" userLabel="First Responder" customClass="UIResponder" sceneMemberID="firstResponder"/>
|
||||
</objects>
|
||||
<point key="canvasLocation" x="2100" y="0.0"/>
|
||||
</scene>
|
||||
<!--Date Filter-->
|
||||
<scene sceneID="GqC-c0-bWe">
|
||||
<objects>
|
||||
<viewController storyboardIdentifier="domainFilter" modalTransitionStyle="crossDissolve" id="r7v-PM-PrR" customClass="VCDateFilter" customModule="AppCheck" customModuleProvider="target" sceneMemberID="viewController">
|
||||
<view key="view" contentMode="scaleToFill" id="QBv-5g-BTH">
|
||||
<rect key="frame" x="0.0" y="0.0" width="320" height="568"/>
|
||||
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
|
||||
<subviews>
|
||||
<navigationBar hidden="YES" contentMode="scaleToFill" translatesAutoresizingMaskIntoConstraints="NO" id="jAM-LN-evh">
|
||||
<rect key="frame" x="0.0" y="0.0" width="320" height="44"/>
|
||||
<items>
|
||||
<navigationItem title="Placeholder" id="s5o-aw-nIo">
|
||||
<barButtonItem key="leftBarButtonItem" title="Item" image="filter-clear" id="oMW-R3-3Eh"/>
|
||||
</navigationItem>
|
||||
</items>
|
||||
</navigationBar>
|
||||
<view contentMode="scaleToFill" translatesAutoresizingMaskIntoConstraints="NO" id="pEc-vv-7Ts">
|
||||
<rect key="frame" x="8" y="64" width="233.5" height="391.5"/>
|
||||
<subviews>
|
||||
<segmentedControl opaque="NO" contentMode="scaleToFill" contentHorizontalAlignment="left" contentVerticalAlignment="top" segmentControlStyle="plain" selectedSegmentIndex="0" translatesAutoresizingMaskIntoConstraints="NO" id="UNT-qn-2cg">
|
||||
<rect key="frame" x="8" y="8" width="217.5" height="32"/>
|
||||
<segments>
|
||||
<segment title="Most recent"/>
|
||||
<segment title="Date Range"/>
|
||||
</segments>
|
||||
<connections>
|
||||
<action selector="didChangeFilterBy:" destination="r7v-PM-PrR" eventType="valueChanged" id="kM6-QE-ZGV"/>
|
||||
</connections>
|
||||
</segmentedControl>
|
||||
<stackView opaque="NO" contentMode="scaleToFill" axis="vertical" spacing="15" translatesAutoresizingMaskIntoConstraints="NO" id="gEf-Ra-RyA">
|
||||
<rect key="frame" x="10" y="47" width="213.5" height="334.5"/>
|
||||
<subviews>
|
||||
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="Show entries no older than" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="UBq-oH-pKp">
|
||||
<rect key="frame" x="0.0" y="0.0" width="213.5" height="20.5"/>
|
||||
<fontDescription key="fontDescription" style="UICTFontTextStyleHeadline"/>
|
||||
<nil key="textColor"/>
|
||||
<nil key="highlightedColor"/>
|
||||
</label>
|
||||
<view contentMode="scaleToFill" translatesAutoresizingMaskIntoConstraints="NO" id="ucF-MH-iRP">
|
||||
<rect key="frame" x="0.0" y="35.5" width="213.5" height="50"/>
|
||||
<subviews>
|
||||
<slider opaque="NO" contentMode="scaleToFill" contentHorizontalAlignment="center" contentVerticalAlignment="center" value="0.5" minValue="0.0" maxValue="1" translatesAutoresizingMaskIntoConstraints="NO" id="qhe-6d-hGB">
|
||||
<rect key="frame" x="-2" y="0.0" width="155.5" height="51"/>
|
||||
<connections>
|
||||
<action selector="durationSliderChanged:" destination="r7v-PM-PrR" eventType="valueChanged" id="nQB-w9-dY1"/>
|
||||
</connections>
|
||||
</slider>
|
||||
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="7 days" textAlignment="center" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="ika-su-PZQ">
|
||||
<rect key="frame" x="159.5" y="16" width="54" height="18"/>
|
||||
<constraints>
|
||||
<constraint firstAttribute="width" secondItem="ika-su-PZQ" secondAttribute="height" multiplier="3" id="o9m-cy-kkn"/>
|
||||
</constraints>
|
||||
<fontDescription key="fontDescription" style="UICTFontTextStyleSubhead"/>
|
||||
<nil key="textColor"/>
|
||||
<nil key="highlightedColor"/>
|
||||
</label>
|
||||
</subviews>
|
||||
<color key="backgroundColor" systemColor="systemBackgroundColor" cocoaTouchSystemColor="whiteColor"/>
|
||||
<constraints>
|
||||
<constraint firstItem="qhe-6d-hGB" firstAttribute="leading" secondItem="ucF-MH-iRP" secondAttribute="leading" id="EGU-ln-lJF"/>
|
||||
<constraint firstItem="ika-su-PZQ" firstAttribute="trailing" secondItem="ucF-MH-iRP" secondAttribute="trailing" id="KLs-Ft-wSo"/>
|
||||
<constraint firstItem="qhe-6d-hGB" firstAttribute="bottom" secondItem="ucF-MH-iRP" secondAttribute="bottom" id="PTR-Et-Klv"/>
|
||||
<constraint firstItem="ika-su-PZQ" firstAttribute="leading" secondItem="qhe-6d-hGB" secondAttribute="trailing" constant="8" symbolic="YES" id="aHy-IX-X4B"/>
|
||||
<constraint firstItem="ika-su-PZQ" firstAttribute="centerY" secondItem="qhe-6d-hGB" secondAttribute="centerY" id="cHs-5z-CHK"/>
|
||||
<constraint firstItem="qhe-6d-hGB" firstAttribute="top" secondItem="ucF-MH-iRP" secondAttribute="top" id="eJC-d4-zg0"/>
|
||||
</constraints>
|
||||
</view>
|
||||
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="Show entries within range" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="rtf-o1-gk6">
|
||||
<rect key="frame" x="0.0" y="100.5" width="213.5" height="20.5"/>
|
||||
<fontDescription key="fontDescription" style="UICTFontTextStyleHeadline"/>
|
||||
<nil key="textColor"/>
|
||||
<nil key="highlightedColor"/>
|
||||
</label>
|
||||
<view contentMode="scaleToFill" translatesAutoresizingMaskIntoConstraints="NO" id="9As-hA-MKt">
|
||||
<rect key="frame" x="0.0" y="136" width="213.5" height="74"/>
|
||||
<subviews>
|
||||
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="From:" textAlignment="right" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="wAd-o2-PHY">
|
||||
<rect key="frame" x="0.0" y="6.5" width="44" height="20.5"/>
|
||||
<fontDescription key="fontDescription" style="UICTFontTextStyleBody"/>
|
||||
<nil key="textColor"/>
|
||||
<nil key="highlightedColor"/>
|
||||
</label>
|
||||
<button opaque="NO" contentMode="scaleToFill" contentHorizontalAlignment="center" contentVerticalAlignment="center" buttonType="roundedRect" lineBreakMode="middleTruncation" translatesAutoresizingMaskIntoConstraints="NO" id="FVD-kB-91w">
|
||||
<rect key="frame" x="52" y="0.0" width="161.5" height="33"/>
|
||||
<fontDescription key="fontDescription" style="UICTFontTextStyleHeadline"/>
|
||||
<state key="normal" title="1970-01-01 01:00"/>
|
||||
<connections>
|
||||
<action selector="didTapRangeButton:" destination="r7v-PM-PrR" eventType="touchUpInside" id="g05-Sc-0P2"/>
|
||||
</connections>
|
||||
</button>
|
||||
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="To:" textAlignment="right" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="fzL-94-c0l">
|
||||
<rect key="frame" x="0.0" y="47.5" width="44" height="20.5"/>
|
||||
<fontDescription key="fontDescription" style="UICTFontTextStyleBody"/>
|
||||
<nil key="textColor"/>
|
||||
<nil key="highlightedColor"/>
|
||||
</label>
|
||||
<button opaque="NO" contentMode="scaleToFill" contentHorizontalAlignment="center" contentVerticalAlignment="center" buttonType="roundedRect" lineBreakMode="middleTruncation" translatesAutoresizingMaskIntoConstraints="NO" id="IG3-Wc-UI4">
|
||||
<rect key="frame" x="52" y="41" width="161.5" height="33"/>
|
||||
<fontDescription key="fontDescription" style="UICTFontTextStyleHeadline"/>
|
||||
<state key="normal" title="1970-01-01 01:00"/>
|
||||
<connections>
|
||||
<action selector="didTapRangeButton:" destination="r7v-PM-PrR" eventType="touchUpInside" id="63v-sy-Bpo"/>
|
||||
</connections>
|
||||
</button>
|
||||
</subviews>
|
||||
<color key="backgroundColor" systemColor="systemBackgroundColor" cocoaTouchSystemColor="whiteColor"/>
|
||||
<constraints>
|
||||
<constraint firstItem="FVD-kB-91w" firstAttribute="top" secondItem="9As-hA-MKt" secondAttribute="top" id="J2e-NO-NUD"/>
|
||||
<constraint firstItem="IG3-Wc-UI4" firstAttribute="top" secondItem="FVD-kB-91w" secondAttribute="bottom" constant="8" symbolic="YES" id="Jyg-b6-5rH"/>
|
||||
<constraint firstItem="fzL-94-c0l" firstAttribute="centerY" secondItem="IG3-Wc-UI4" secondAttribute="centerY" id="LAG-jg-nBi"/>
|
||||
<constraint firstItem="wAd-o2-PHY" firstAttribute="trailing" secondItem="fzL-94-c0l" secondAttribute="trailing" id="Lav-7y-PUJ"/>
|
||||
<constraint firstItem="IG3-Wc-UI4" firstAttribute="trailing" secondItem="9As-hA-MKt" secondAttribute="trailing" id="XqI-1p-FNv"/>
|
||||
<constraint firstItem="FVD-kB-91w" firstAttribute="leading" secondItem="wAd-o2-PHY" secondAttribute="trailing" constant="8" symbolic="YES" id="YlE-w7-CZ5"/>
|
||||
<constraint firstItem="FVD-kB-91w" firstAttribute="trailing" secondItem="9As-hA-MKt" secondAttribute="trailing" id="a6D-1D-HvF"/>
|
||||
<constraint firstItem="wAd-o2-PHY" firstAttribute="leading" secondItem="fzL-94-c0l" secondAttribute="leading" id="bM0-gJ-IW5"/>
|
||||
<constraint firstItem="wAd-o2-PHY" firstAttribute="centerY" secondItem="FVD-kB-91w" secondAttribute="centerY" id="g7F-LP-PQQ"/>
|
||||
<constraint firstItem="IG3-Wc-UI4" firstAttribute="bottom" secondItem="9As-hA-MKt" secondAttribute="bottom" priority="750" id="jlK-69-8hl"/>
|
||||
<constraint firstItem="IG3-Wc-UI4" firstAttribute="leading" secondItem="fzL-94-c0l" secondAttribute="trailing" constant="8" symbolic="YES" id="pcE-Gv-oj7"/>
|
||||
<constraint firstItem="wAd-o2-PHY" firstAttribute="leading" secondItem="9As-hA-MKt" secondAttribute="leading" id="zgR-pJ-vFs"/>
|
||||
</constraints>
|
||||
</view>
|
||||
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="Order by" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="9Fe-5F-TVt">
|
||||
<rect key="frame" x="0.0" y="225" width="213.5" height="20.5"/>
|
||||
<fontDescription key="fontDescription" style="UICTFontTextStyleHeadline"/>
|
||||
<nil key="textColor"/>
|
||||
<nil key="highlightedColor"/>
|
||||
</label>
|
||||
<view contentMode="scaleToFill" translatesAutoresizingMaskIntoConstraints="NO" id="cWy-un-IHC">
|
||||
<rect key="frame" x="0.0" y="260.5" width="213.5" height="74"/>
|
||||
<subviews>
|
||||
<segmentedControl opaque="NO" contentMode="scaleToFill" contentHorizontalAlignment="left" contentVerticalAlignment="top" segmentControlStyle="plain" selectedSegmentIndex="0" translatesAutoresizingMaskIntoConstraints="NO" id="UKE-MR-kRJ">
|
||||
<rect key="frame" x="-2" y="0.0" width="217.5" height="36"/>
|
||||
<segments>
|
||||
<segment title="Date"/>
|
||||
<segment title="Name"/>
|
||||
<segment title="Count"/>
|
||||
</segments>
|
||||
</segmentedControl>
|
||||
<segmentedControl opaque="NO" contentMode="scaleToFill" contentHorizontalAlignment="left" contentVerticalAlignment="top" segmentControlStyle="plain" selectedSegmentIndex="0" translatesAutoresizingMaskIntoConstraints="NO" id="eG2-a4-zm5">
|
||||
<rect key="frame" x="-2" y="43" width="217.5" height="32"/>
|
||||
<segments>
|
||||
<segment title="Ascending"/>
|
||||
<segment title="Descending"/>
|
||||
</segments>
|
||||
</segmentedControl>
|
||||
</subviews>
|
||||
<color key="backgroundColor" systemColor="systemBackgroundColor" cocoaTouchSystemColor="whiteColor"/>
|
||||
<constraints>
|
||||
<constraint firstItem="eG2-a4-zm5" firstAttribute="top" secondItem="UKE-MR-kRJ" secondAttribute="bottom" constant="8" symbolic="YES" id="6oC-bZ-XdM"/>
|
||||
<constraint firstItem="eG2-a4-zm5" firstAttribute="leading" secondItem="cWy-un-IHC" secondAttribute="leading" constant="-2" id="7R0-qB-J0u"/>
|
||||
<constraint firstAttribute="bottom" secondItem="eG2-a4-zm5" secondAttribute="bottom" id="JbN-vA-Rd5"/>
|
||||
<constraint firstItem="UKE-MR-kRJ" firstAttribute="top" secondItem="cWy-un-IHC" secondAttribute="top" id="L21-Kf-g2d"/>
|
||||
<constraint firstAttribute="trailing" secondItem="eG2-a4-zm5" secondAttribute="trailing" constant="-2" id="cbD-H9-e1Q"/>
|
||||
<constraint firstItem="UKE-MR-kRJ" firstAttribute="leading" secondItem="cWy-un-IHC" secondAttribute="leading" constant="-2" id="lKB-g4-asw"/>
|
||||
<constraint firstAttribute="trailing" secondItem="UKE-MR-kRJ" secondAttribute="trailing" constant="-2" id="xIa-X2-0Lp"/>
|
||||
</constraints>
|
||||
</view>
|
||||
</subviews>
|
||||
</stackView>
|
||||
</subviews>
|
||||
<color key="backgroundColor" systemColor="systemBackgroundColor" cocoaTouchSystemColor="whiteColor"/>
|
||||
<constraints>
|
||||
<constraint firstItem="UNT-qn-2cg" firstAttribute="top" secondItem="pEc-vv-7Ts" secondAttribute="top" constant="8" id="Awu-uv-9wF"/>
|
||||
<constraint firstItem="UNT-qn-2cg" firstAttribute="leading" secondItem="pEc-vv-7Ts" secondAttribute="leading" constant="8" id="Icx-YR-5bc"/>
|
||||
<constraint firstItem="gEf-Ra-RyA" firstAttribute="top" secondItem="UNT-qn-2cg" secondAttribute="bottom" constant="8" symbolic="YES" id="QPi-aa-6ff"/>
|
||||
<constraint firstItem="UNT-qn-2cg" firstAttribute="trailing" secondItem="pEc-vv-7Ts" secondAttribute="trailing" constant="-8" id="Sof-6L-T2D"/>
|
||||
<constraint firstItem="gEf-Ra-RyA" firstAttribute="bottom" secondItem="pEc-vv-7Ts" secondAttribute="bottom" constant="-10" id="TMx-5J-z2P"/>
|
||||
<constraint firstItem="gEf-Ra-RyA" firstAttribute="leading" secondItem="pEc-vv-7Ts" secondAttribute="leading" constant="10" id="U6l-7M-bm4"/>
|
||||
<constraint firstItem="gEf-Ra-RyA" firstAttribute="trailing" secondItem="pEc-vv-7Ts" secondAttribute="trailing" constant="-10" id="YKE-TR-fTB"/>
|
||||
</constraints>
|
||||
<userDefinedRuntimeAttributes>
|
||||
<userDefinedRuntimeAttribute type="number" keyPath="layer.cornerRadius">
|
||||
<integer key="value" value="12"/>
|
||||
</userDefinedRuntimeAttribute>
|
||||
</userDefinedRuntimeAttributes>
|
||||
</view>
|
||||
<view contentMode="scaleToFill" translatesAutoresizingMaskIntoConstraints="NO" id="sAi-8j-0n1" customClass="PopupTriangle" customModule="AppCheck" customModuleProvider="target">
|
||||
<rect key="frame" x="14" y="46" width="28" height="22"/>
|
||||
<color key="backgroundColor" white="0.0" alpha="0.0" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
|
||||
<constraints>
|
||||
<constraint firstAttribute="height" constant="22" id="MaD-aD-U8h"/>
|
||||
<constraint firstAttribute="width" constant="28" id="ntP-rP-UMh"/>
|
||||
</constraints>
|
||||
<userDefinedRuntimeAttributes>
|
||||
<userDefinedRuntimeAttribute type="color" keyPath="color">
|
||||
<color key="value" systemColor="systemBackgroundColor" cocoaTouchSystemColor="whiteColor"/>
|
||||
</userDefinedRuntimeAttribute>
|
||||
</userDefinedRuntimeAttributes>
|
||||
</view>
|
||||
</subviews>
|
||||
<color key="backgroundColor" white="0.0" alpha="0.20000000000000001" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
|
||||
<gestureRecognizers/>
|
||||
<constraints>
|
||||
<constraint firstItem="sAi-8j-0n1" firstAttribute="bottom" secondItem="pEc-vv-7Ts" secondAttribute="top" constant="4" id="DCq-Ps-sQo"/>
|
||||
<constraint firstItem="pEc-vv-7Ts" firstAttribute="top" secondItem="jAM-LN-evh" secondAttribute="bottom" constant="20" id="EdA-nv-DEa"/>
|
||||
<constraint firstItem="pEc-vv-7Ts" firstAttribute="leading" secondItem="u0F-hK-vVD" secondAttribute="leading" constant="8" id="Iow-GV-Lxy"/>
|
||||
<constraint firstItem="jAM-LN-evh" firstAttribute="trailing" secondItem="u0F-hK-vVD" secondAttribute="trailing" id="Lju-K6-G89"/>
|
||||
<constraint firstItem="jAM-LN-evh" firstAttribute="top" secondItem="u0F-hK-vVD" secondAttribute="top" id="MqW-YU-POp"/>
|
||||
<constraint firstItem="u0F-hK-vVD" firstAttribute="trailing" relation="greaterThanOrEqual" secondItem="pEc-vv-7Ts" secondAttribute="trailing" constant="8" id="V9T-2Y-oNy"/>
|
||||
<constraint firstItem="sAi-8j-0n1" firstAttribute="leading" secondItem="pEc-vv-7Ts" secondAttribute="leading" constant="6" id="cXH-3c-s6t"/>
|
||||
<constraint firstItem="jAM-LN-evh" firstAttribute="leading" secondItem="u0F-hK-vVD" secondAttribute="leading" id="ula-eW-vAq"/>
|
||||
</constraints>
|
||||
<viewLayoutGuide key="safeArea" id="u0F-hK-vVD"/>
|
||||
<connections>
|
||||
<outletCollection property="gestureRecognizers" destination="oRi-17-cVM" appends="YES" id="2pk-5e-eJD"/>
|
||||
</connections>
|
||||
</view>
|
||||
<extendedEdge key="edgesForExtendedLayout" bottom="YES"/>
|
||||
<connections>
|
||||
<outlet property="buttonRangeEnd" destination="IG3-Wc-UI4" id="wAd-ca-bVQ"/>
|
||||
<outlet property="buttonRangeStart" destination="FVD-kB-91w" id="HbX-Vl-uBE"/>
|
||||
<outlet property="durationLabel" destination="ika-su-PZQ" id="1Br-vu-xir"/>
|
||||
<outlet property="durationSlider" destination="qhe-6d-hGB" id="wph-zX-WIz"/>
|
||||
<outlet property="durationTitle" destination="UBq-oH-pKp" id="BEd-Lo-a2v"/>
|
||||
<outlet property="durationView" destination="ucF-MH-iRP" id="TCI-Pp-drf"/>
|
||||
<outlet property="filterBy" destination="UNT-qn-2cg" id="M1J-n8-LHq"/>
|
||||
<outlet property="orderbyAsc" destination="eG2-a4-zm5" id="II1-hc-pyZ"/>
|
||||
<outlet property="orderbyType" destination="UKE-MR-kRJ" id="fK7-dW-MLd"/>
|
||||
<outlet property="rangeTitle" destination="rtf-o1-gk6" id="2DY-xP-VOg"/>
|
||||
<outlet property="rangeView" destination="9As-hA-MKt" id="0Mq-Gi-nF6"/>
|
||||
</connections>
|
||||
</viewController>
|
||||
<placeholder placeholderIdentifier="IBFirstResponder" id="xTS-RW-xLN" userLabel="First Responder" customClass="UIResponder" sceneMemberID="firstResponder"/>
|
||||
<tapGestureRecognizer id="oRi-17-cVM">
|
||||
<connections>
|
||||
<outlet property="delegate" destination="r7v-PM-PrR" id="JME-7W-w51"/>
|
||||
</connections>
|
||||
</tapGestureRecognizer>
|
||||
</objects>
|
||||
<point key="canvasLocation" x="0.0" y="-700"/>
|
||||
</scene>
|
||||
<!--Analysis Bar-->
|
||||
<scene sceneID="1qq-WD-Lqq">
|
||||
<objects>
|
||||
<viewController id="1ba-SA-8sT" customClass="VCAnalysisBar" customModule="AppCheck" customModuleProvider="target" sceneMemberID="viewController">
|
||||
<view key="view" contentMode="scaleToFill" id="qp6-er-N6U">
|
||||
<rect key="frame" x="0.0" y="0.0" width="320" height="49"/>
|
||||
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
|
||||
<subviews>
|
||||
<tabBar contentMode="scaleToFill" translucent="NO" id="1Jy-zg-CXR">
|
||||
<rect key="frame" x="0.0" y="0.0" width="320" height="49"/>
|
||||
<autoresizingMask key="autoresizingMask" widthSizable="YES" flexibleMaxY="YES"/>
|
||||
<color key="backgroundColor" white="0.0" alpha="0.0" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
|
||||
<items>
|
||||
<tabBarItem title="Co-Occurrence" image="intersection" id="KXh-kQ-rAF"/>
|
||||
</items>
|
||||
<connections>
|
||||
<outlet property="delegate" destination="1ba-SA-8sT" id="bRS-kh-dOv"/>
|
||||
</connections>
|
||||
</tabBar>
|
||||
</subviews>
|
||||
<color key="backgroundColor" systemColor="systemBackgroundColor" cocoaTouchSystemColor="whiteColor"/>
|
||||
<viewLayoutGuide key="safeArea" id="dtz-KG-P4C"/>
|
||||
</view>
|
||||
<connections>
|
||||
<outlet property="tabBar" destination="1Jy-zg-CXR" id="VTV-xq-Aou"/>
|
||||
</connections>
|
||||
</viewController>
|
||||
<placeholder placeholderIdentifier="IBFirstResponder" id="XnK-B9-RSJ" userLabel="First Responder" customClass="UIResponder" sceneMemberID="firstResponder"/>
|
||||
</objects>
|
||||
<point key="canvasLocation" x="700" y="-700"/>
|
||||
</scene>
|
||||
</scenes>
|
||||
<inferredMetricsTieBreakers>
|
||||
<segue reference="vf1-07-AS4"/>
|
||||
</inferredMetricsTieBreakers>
|
||||
<resources>
|
||||
<image name="filter-clear" width="20" height="20"/>
|
||||
<image name="intersection" width="25" height="25"/>
|
||||
<image name="jump-to-target" width="20" height="20"/>
|
||||
</resources>
|
||||
</document>
|
||||
837
main/GUI/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" rowHeight="-1" estimatedRowHeight="-1" sectionHeaderHeight="28" sectionFooterHeight="28" id="GSg-ZZ-F8J">
|
||||
<rect key="frame" x="0.0" y="0.0" width="320" height="568"/>
|
||||
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
|
||||
<color key="backgroundColor" systemColor="systemBackgroundColor" cocoaTouchSystemColor="whiteColor"/>
|
||||
<prototypes>
|
||||
<tableViewCell clipsSubviews="YES" contentMode="scaleToFill" preservesSuperviewLayoutMargins="YES" selectionStyle="none" indentationWidth="10" reuseIdentifier="DomainFilterCell" textLabel="MrS-rb-RLB" style="IBUITableViewCellStyleDefault" id="EO2-ww-xuz">
|
||||
<rect key="frame" x="0.0" y="28" width="320" height="43.5"/>
|
||||
<autoresizingMask key="autoresizingMask"/>
|
||||
<tableViewCellContentView key="contentView" opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center" preservesSuperviewLayoutMargins="YES" insetsLayoutMarginsFromSafeArea="NO" tableViewCell="EO2-ww-xuz" id="AtR-ce-uYs">
|
||||
<rect key="frame" x="0.0" y="0.0" width="320" height="43.5"/>
|
||||
<autoresizingMask key="autoresizingMask"/>
|
||||
<subviews>
|
||||
<label opaque="NO" multipleTouchEnabled="YES" contentMode="left" insetsLayoutMarginsFromSafeArea="NO" text="Title" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" minimumScaleFactor="0.80000000000000004" id="MrS-rb-RLB">
|
||||
<rect key="frame" x="16" y="0.0" width="288" height="43.5"/>
|
||||
<autoresizingMask key="autoresizingMask"/>
|
||||
<fontDescription key="fontDescription" style="UICTFontTextStyleBody"/>
|
||||
<nil key="textColor"/>
|
||||
<nil key="highlightedColor"/>
|
||||
</label>
|
||||
</subviews>
|
||||
</tableViewCellContentView>
|
||||
</tableViewCell>
|
||||
</prototypes>
|
||||
<connections>
|
||||
<outlet property="dataSource" destination="q3B-Yi-1bx" id="eWw-VO-n1c"/>
|
||||
<outlet property="delegate" destination="q3B-Yi-1bx" id="02X-f0-d1a"/>
|
||||
</connections>
|
||||
</tableView>
|
||||
<navigationItem key="navigationItem" title="Domains" id="FWA-IG-VIb">
|
||||
<barButtonItem key="rightBarButtonItem" systemItem="add" id="RFW-bp-wwH">
|
||||
<connections>
|
||||
<action selector="addNewFilter" destination="q3B-Yi-1bx" id="JID-eH-y0p"/>
|
||||
</connections>
|
||||
</barButtonItem>
|
||||
</navigationItem>
|
||||
</tableViewController>
|
||||
<placeholder placeholderIdentifier="IBFirstResponder" id="Xzo-dO-WpK" userLabel="First Responder" customClass="UIResponder" sceneMemberID="firstResponder"/>
|
||||
</objects>
|
||||
<point key="canvasLocation" x="1500" y="0.0"/>
|
||||
</scene>
|
||||
<!--Reminders-->
|
||||
<scene sceneID="fWF-ss-cNz">
|
||||
<objects>
|
||||
<tableViewController id="JYM-cs-i4H" customClass="TVCReminderAlerts" customModule="AppCheck" customModuleProvider="target" sceneMemberID="viewController">
|
||||
<tableView key="view" clipsSubviews="YES" contentMode="scaleToFill" alwaysBounceVertical="YES" dataMode="static" style="grouped" separatorStyle="default" rowHeight="-1" estimatedRowHeight="-1" sectionHeaderHeight="18" sectionFooterHeight="18" id="Dop-3B-Uvh">
|
||||
<rect key="frame" x="0.0" y="0.0" width="320" height="568"/>
|
||||
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
|
||||
<color key="backgroundColor" systemColor="systemBackgroundColor" cocoaTouchSystemColor="whiteColor"/>
|
||||
<sections>
|
||||
<tableViewSection headerTitle="Restart Reminder" id="UOi-fT-8Vh">
|
||||
<string key="footerTitle">If VPN stops accidentally, show a notification 5 minutes later. It will remind you to re-enable the VPN after system reboot.
|
||||
|
||||
if Notification is enabled, show a notification banner once, stating the VPN has stopped.
|
||||
|
||||
If App Badge is enabled, display the letter "1" on the homescreen app icon, as long as the VPN is not running.</string>
|
||||
<cells>
|
||||
<tableViewCell clipsSubviews="YES" contentMode="scaleToFill" preservesSuperviewLayoutMargins="YES" selectionStyle="none" indentationWidth="10" textLabel="dl8-4J-a0L" style="IBUITableViewCellStyleDefault" id="Z2r-Kz-TCt">
|
||||
<rect key="frame" x="0.0" y="55.5" width="320" height="43.5"/>
|
||||
<autoresizingMask key="autoresizingMask"/>
|
||||
<tableViewCellContentView key="contentView" opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center" preservesSuperviewLayoutMargins="YES" insetsLayoutMarginsFromSafeArea="NO" tableViewCell="Z2r-Kz-TCt" id="rEy-qO-PEN">
|
||||
<rect key="frame" x="0.0" y="0.0" width="320" height="43.5"/>
|
||||
<autoresizingMask key="autoresizingMask"/>
|
||||
<subviews>
|
||||
<label opaque="NO" multipleTouchEnabled="YES" contentMode="left" insetsLayoutMarginsFromSafeArea="NO" text="Warn If VPN Stops" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" id="dl8-4J-a0L">
|
||||
<rect key="frame" x="16" y="0.0" width="288" height="43.5"/>
|
||||
<autoresizingMask key="autoresizingMask"/>
|
||||
<fontDescription key="fontDescription" style="UICTFontTextStyleBody"/>
|
||||
<nil key="textColor"/>
|
||||
<nil key="highlightedColor"/>
|
||||
</label>
|
||||
<switch opaque="NO" contentMode="scaleToFill" horizontalHuggingPriority="750" verticalHuggingPriority="750" contentHorizontalAlignment="center" contentVerticalAlignment="center" on="YES" id="zaV-mh-eqb">
|
||||
<rect key="frame" x="252" y="6" width="54" height="31"/>
|
||||
<autoresizingMask key="autoresizingMask" flexibleMinX="YES" flexibleMaxY="YES"/>
|
||||
<connections>
|
||||
<action selector="toggleAllowRestartReminder:" destination="JYM-cs-i4H" eventType="valueChanged" id="F4e-k2-bni"/>
|
||||
</connections>
|
||||
</switch>
|
||||
</subviews>
|
||||
</tableViewCellContentView>
|
||||
<connections>
|
||||
<outlet property="accessoryView" destination="zaV-mh-eqb" id="irZ-hk-KoR"/>
|
||||
</connections>
|
||||
</tableViewCell>
|
||||
<tableViewCell clipsSubviews="YES" contentMode="scaleToFill" preservesSuperviewLayoutMargins="YES" selectionStyle="none" indentationWidth="10" textLabel="XC6-mj-vkg" style="IBUITableViewCellStyleDefault" id="5sU-vh-JDf">
|
||||
<rect key="frame" x="0.0" y="99" width="320" height="43.5"/>
|
||||
<autoresizingMask key="autoresizingMask"/>
|
||||
<tableViewCellContentView key="contentView" opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center" preservesSuperviewLayoutMargins="YES" insetsLayoutMarginsFromSafeArea="NO" tableViewCell="5sU-vh-JDf" id="MDI-fb-989">
|
||||
<rect key="frame" x="0.0" y="0.0" width="320" height="43.5"/>
|
||||
<autoresizingMask key="autoresizingMask"/>
|
||||
<subviews>
|
||||
<label opaque="NO" multipleTouchEnabled="YES" contentMode="left" insetsLayoutMarginsFromSafeArea="NO" text="… with Notification" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" id="XC6-mj-vkg">
|
||||
<rect key="frame" x="16" y="0.0" width="288" height="43.5"/>
|
||||
<autoresizingMask key="autoresizingMask"/>
|
||||
<fontDescription key="fontDescription" style="UICTFontTextStyleBody"/>
|
||||
<nil key="textColor"/>
|
||||
<nil key="highlightedColor"/>
|
||||
</label>
|
||||
<switch opaque="NO" contentMode="scaleToFill" horizontalHuggingPriority="750" verticalHuggingPriority="750" contentHorizontalAlignment="center" contentVerticalAlignment="center" on="YES" id="HaE-En-NH3">
|
||||
<rect key="frame" x="252" y="6" width="54" height="31"/>
|
||||
<autoresizingMask key="autoresizingMask" flexibleMinX="YES" flexibleMaxY="YES"/>
|
||||
<connections>
|
||||
<action selector="toggleAllowRestartNotify:" destination="JYM-cs-i4H" eventType="valueChanged" id="12C-h5-mrR"/>
|
||||
</connections>
|
||||
</switch>
|
||||
</subviews>
|
||||
</tableViewCellContentView>
|
||||
<connections>
|
||||
<outlet property="accessoryView" destination="HaE-En-NH3" id="dld-32-3vc"/>
|
||||
</connections>
|
||||
</tableViewCell>
|
||||
<tableViewCell clipsSubviews="YES" contentMode="scaleToFill" preservesSuperviewLayoutMargins="YES" selectionStyle="none" indentationWidth="10" textLabel="wHg-Wo-szR" style="IBUITableViewCellStyleDefault" id="01e-KG-qDH">
|
||||
<rect key="frame" x="0.0" y="142.5" width="320" height="43.5"/>
|
||||
<autoresizingMask key="autoresizingMask"/>
|
||||
<tableViewCellContentView key="contentView" opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center" preservesSuperviewLayoutMargins="YES" insetsLayoutMarginsFromSafeArea="NO" tableViewCell="01e-KG-qDH" id="XWV-FF-VxR">
|
||||
<rect key="frame" x="0.0" y="0.0" width="320" height="43.5"/>
|
||||
<autoresizingMask key="autoresizingMask"/>
|
||||
<subviews>
|
||||
<label opaque="NO" multipleTouchEnabled="YES" contentMode="left" insetsLayoutMarginsFromSafeArea="NO" text="… with App Badge" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" id="wHg-Wo-szR">
|
||||
<rect key="frame" x="16" y="0.0" width="288" height="43.5"/>
|
||||
<autoresizingMask key="autoresizingMask"/>
|
||||
<fontDescription key="fontDescription" style="UICTFontTextStyleBody"/>
|
||||
<nil key="textColor"/>
|
||||
<nil key="highlightedColor"/>
|
||||
</label>
|
||||
<switch opaque="NO" contentMode="scaleToFill" horizontalHuggingPriority="750" verticalHuggingPriority="750" contentHorizontalAlignment="center" contentVerticalAlignment="center" on="YES" id="N2Q-cU-pkd">
|
||||
<rect key="frame" x="252" y="6" width="54" height="31"/>
|
||||
<autoresizingMask key="autoresizingMask" flexibleMinX="YES" flexibleMaxY="YES"/>
|
||||
<connections>
|
||||
<action selector="toggleAllowRestartBadge:" destination="JYM-cs-i4H" eventType="valueChanged" id="76l-6y-fOu"/>
|
||||
</connections>
|
||||
</switch>
|
||||
</subviews>
|
||||
</tableViewCellContentView>
|
||||
<connections>
|
||||
<outlet property="accessoryView" destination="N2Q-cU-pkd" id="6LN-Zw-4Nf"/>
|
||||
</connections>
|
||||
</tableViewCell>
|
||||
<tableViewCell clipsSubviews="YES" contentMode="scaleToFill" preservesSuperviewLayoutMargins="YES" selectionStyle="default" accessoryType="disclosureIndicator" indentationWidth="10" textLabel="UBT-zI-VNd" detailTextLabel="tj7-1l-bts" style="IBUITableViewCellStyleValue1" id="pAS-8r-oS5">
|
||||
<rect key="frame" x="0.0" y="186" width="320" height="43.5"/>
|
||||
<autoresizingMask key="autoresizingMask"/>
|
||||
<tableViewCellContentView key="contentView" opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center" preservesSuperviewLayoutMargins="YES" insetsLayoutMarginsFromSafeArea="NO" tableViewCell="pAS-8r-oS5" id="iMr-Gb-dhX">
|
||||
<rect key="frame" x="0.0" y="0.0" width="293" height="43.5"/>
|
||||
<autoresizingMask key="autoresizingMask"/>
|
||||
<subviews>
|
||||
<label opaque="NO" multipleTouchEnabled="YES" contentMode="left" insetsLayoutMarginsFromSafeArea="NO" text="Sound" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" id="UBT-zI-VNd">
|
||||
<rect key="frame" x="16" y="12" width="49.5" height="20.5"/>
|
||||
<autoresizingMask key="autoresizingMask"/>
|
||||
<fontDescription key="fontDescription" style="UICTFontTextStyleBody"/>
|
||||
<nil key="textColor"/>
|
||||
<nil key="highlightedColor"/>
|
||||
</label>
|
||||
<label opaque="NO" multipleTouchEnabled="YES" contentMode="left" insetsLayoutMarginsFromSafeArea="NO" text="Detail" textAlignment="right" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" id="tj7-1l-bts">
|
||||
<rect key="frame" x="241" y="12" width="44" height="20.5"/>
|
||||
<autoresizingMask key="autoresizingMask"/>
|
||||
<fontDescription key="fontDescription" style="UICTFontTextStyleBody"/>
|
||||
<color key="textColor" systemColor="secondaryLabelColor" red="0.23529411759999999" green="0.23529411759999999" blue="0.26274509800000001" alpha="0.59999999999999998" colorSpace="custom" customColorSpace="sRGB"/>
|
||||
<nil key="highlightedColor"/>
|
||||
</label>
|
||||
</subviews>
|
||||
</tableViewCellContentView>
|
||||
<connections>
|
||||
<segue destination="I37-dZ-c9Q" kind="push" identifier="segueSoundRestartReminder" id="nBh-15-nBq"/>
|
||||
</connections>
|
||||
</tableViewCell>
|
||||
</cells>
|
||||
</tableViewSection>
|
||||
<tableViewSection headerTitle="Recording Reminder" footerTitle="Very sporadic reminder. Triggered if your last recording is older than two weeks." id="9Wn-bc-wKX">
|
||||
<cells>
|
||||
<tableViewCell clipsSubviews="YES" contentMode="scaleToFill" preservesSuperviewLayoutMargins="YES" selectionStyle="none" indentationWidth="10" textLabel="u55-7Z-Q0Q" style="IBUITableViewCellStyleDefault" id="87B-MT-J9s">
|
||||
<rect key="frame" x="0.0" y="449" width="320" height="43.5"/>
|
||||
<autoresizingMask key="autoresizingMask"/>
|
||||
<tableViewCellContentView key="contentView" opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center" preservesSuperviewLayoutMargins="YES" insetsLayoutMarginsFromSafeArea="NO" tableViewCell="87B-MT-J9s" id="79D-rZ-spg">
|
||||
<rect key="frame" x="0.0" y="0.0" width="320" height="43.5"/>
|
||||
<autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMaxY="YES"/>
|
||||
<subviews>
|
||||
<label opaque="NO" multipleTouchEnabled="YES" contentMode="left" insetsLayoutMarginsFromSafeArea="NO" text="Recording Reminder" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" id="u55-7Z-Q0Q">
|
||||
<rect key="frame" x="16" y="0.0" width="288" height="43.5"/>
|
||||
<autoresizingMask key="autoresizingMask"/>
|
||||
<fontDescription key="fontDescription" style="UICTFontTextStyleBody"/>
|
||||
<nil key="textColor"/>
|
||||
<nil key="highlightedColor"/>
|
||||
</label>
|
||||
<switch opaque="NO" contentMode="scaleToFill" horizontalHuggingPriority="750" verticalHuggingPriority="750" contentHorizontalAlignment="center" contentVerticalAlignment="center" on="YES" translatesAutoresizingMaskIntoConstraints="NO" id="mTm-Rm-1RQ">
|
||||
<rect key="frame" x="255" y="6" width="49" height="31"/>
|
||||
<autoresizingMask key="autoresizingMask" flexibleMinX="YES" flexibleMaxY="YES"/>
|
||||
<connections>
|
||||
<action selector="toggleAllowRecordingReminder:" destination="JYM-cs-i4H" eventType="valueChanged" id="unC-Ur-jPM"/>
|
||||
</connections>
|
||||
</switch>
|
||||
</subviews>
|
||||
</tableViewCellContentView>
|
||||
<connections>
|
||||
<outlet property="accessoryView" destination="mTm-Rm-1RQ" id="MRe-3h-xfa"/>
|
||||
</connections>
|
||||
</tableViewCell>
|
||||
<tableViewCell clipsSubviews="YES" contentMode="scaleToFill" preservesSuperviewLayoutMargins="YES" selectionStyle="default" accessoryType="disclosureIndicator" indentationWidth="10" textLabel="VYC-xF-awf" detailTextLabel="Ywb-pT-l1W" style="IBUITableViewCellStyleValue1" id="UkL-k7-bB7">
|
||||
<rect key="frame" x="0.0" y="492.5" width="320" height="43.5"/>
|
||||
<autoresizingMask key="autoresizingMask"/>
|
||||
<tableViewCellContentView key="contentView" opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center" preservesSuperviewLayoutMargins="YES" insetsLayoutMarginsFromSafeArea="NO" tableViewCell="UkL-k7-bB7" id="fZv-NH-YA3">
|
||||
<rect key="frame" x="0.0" y="0.0" width="293" height="43.5"/>
|
||||
<autoresizingMask key="autoresizingMask"/>
|
||||
<subviews>
|
||||
<label opaque="NO" multipleTouchEnabled="YES" contentMode="left" insetsLayoutMarginsFromSafeArea="NO" text="Sound" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" id="VYC-xF-awf">
|
||||
<rect key="frame" x="16" y="12" width="49.5" height="20.5"/>
|
||||
<autoresizingMask key="autoresizingMask"/>
|
||||
<fontDescription key="fontDescription" style="UICTFontTextStyleBody"/>
|
||||
<nil key="textColor"/>
|
||||
<nil key="highlightedColor"/>
|
||||
</label>
|
||||
<label opaque="NO" multipleTouchEnabled="YES" contentMode="left" insetsLayoutMarginsFromSafeArea="NO" text="Detail" textAlignment="right" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" id="Ywb-pT-l1W">
|
||||
<rect key="frame" x="241" y="12" width="44" height="20.5"/>
|
||||
<autoresizingMask key="autoresizingMask"/>
|
||||
<fontDescription key="fontDescription" style="UICTFontTextStyleBody"/>
|
||||
<color key="textColor" systemColor="secondaryLabelColor" red="0.23529411759999999" green="0.23529411759999999" blue="0.26274509800000001" alpha="0.59999999999999998" colorSpace="custom" customColorSpace="sRGB"/>
|
||||
<nil key="highlightedColor"/>
|
||||
</label>
|
||||
</subviews>
|
||||
</tableViewCellContentView>
|
||||
<connections>
|
||||
<segue destination="I37-dZ-c9Q" kind="push" identifier="segueSoundRecordingReminder" id="xKB-i8-J9c"/>
|
||||
</connections>
|
||||
</tableViewCell>
|
||||
</cells>
|
||||
</tableViewSection>
|
||||
</sections>
|
||||
<connections>
|
||||
<outlet property="dataSource" destination="JYM-cs-i4H" id="1ji-9q-6qB"/>
|
||||
<outlet property="delegate" destination="JYM-cs-i4H" id="YU7-R1-VjB"/>
|
||||
</connections>
|
||||
</tableView>
|
||||
<navigationItem key="navigationItem" title="Reminders" id="Z9N-kQ-xhI"/>
|
||||
<connections>
|
||||
<outlet property="recordingAllow" destination="mTm-Rm-1RQ" id="tqz-Pk-pSi"/>
|
||||
<outlet property="recordingSound" destination="UkL-k7-bB7" id="Y9s-fL-cQU"/>
|
||||
<outlet property="restartAllow" destination="zaV-mh-eqb" id="zcQ-1e-i4H"/>
|
||||
<outlet property="restartAllowBadge" destination="N2Q-cU-pkd" id="RPU-f6-Dv3"/>
|
||||
<outlet property="restartAllowNotify" destination="HaE-En-NH3" id="1BN-5h-zNR"/>
|
||||
<outlet property="restartSound" destination="pAS-8r-oS5" id="9Dy-QR-WXq"/>
|
||||
</connections>
|
||||
</tableViewController>
|
||||
<placeholder placeholderIdentifier="IBFirstResponder" id="W7H-LK-3wW" userLabel="First Responder" customClass="UIResponder" sceneMemberID="firstResponder"/>
|
||||
</objects>
|
||||
<point key="canvasLocation" x="-200" y="768"/>
|
||||
</scene>
|
||||
<!--Sound-->
|
||||
<scene sceneID="3a5-wn-tdm">
|
||||
<objects>
|
||||
<tableViewController id="I37-dZ-c9Q" customClass="TVCChooseAlertTone" customModule="AppCheck" customModuleProvider="target" sceneMemberID="viewController">
|
||||
<tableView key="view" clipsSubviews="YES" contentMode="scaleToFill" alwaysBounceVertical="YES" dataMode="prototypes" style="grouped" separatorStyle="default" rowHeight="-1" estimatedRowHeight="-1" sectionHeaderHeight="18" sectionFooterHeight="18" id="w2R-BE-lM4">
|
||||
<rect key="frame" x="0.0" y="0.0" width="320" height="568"/>
|
||||
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
|
||||
<color key="backgroundColor" systemColor="systemBackgroundColor" cocoaTouchSystemColor="whiteColor"/>
|
||||
<prototypes>
|
||||
<tableViewCell clipsSubviews="YES" contentMode="scaleToFill" preservesSuperviewLayoutMargins="YES" selectionStyle="default" accessoryType="checkmark" indentationWidth="10" reuseIdentifier="SettingsAlertToneCell" textLabel="O50-Db-5TI" style="IBUITableViewCellStyleDefault" id="38V-eP-HSv">
|
||||
<rect key="frame" x="0.0" y="55.5" width="320" height="43.5"/>
|
||||
<autoresizingMask key="autoresizingMask"/>
|
||||
<tableViewCellContentView key="contentView" opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center" preservesSuperviewLayoutMargins="YES" insetsLayoutMarginsFromSafeArea="NO" tableViewCell="38V-eP-HSv" id="hoG-Cy-sHr">
|
||||
<rect key="frame" x="0.0" y="0.0" width="280" height="43.5"/>
|
||||
<autoresizingMask key="autoresizingMask"/>
|
||||
<subviews>
|
||||
<label opaque="NO" multipleTouchEnabled="YES" contentMode="left" insetsLayoutMarginsFromSafeArea="NO" text="Title" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" id="O50-Db-5TI">
|
||||
<rect key="frame" x="16" y="0.0" width="256" height="43.5"/>
|
||||
<autoresizingMask key="autoresizingMask"/>
|
||||
<fontDescription key="fontDescription" style="UICTFontTextStyleBody"/>
|
||||
<nil key="textColor"/>
|
||||
<nil key="highlightedColor"/>
|
||||
</label>
|
||||
</subviews>
|
||||
</tableViewCellContentView>
|
||||
</tableViewCell>
|
||||
</prototypes>
|
||||
<sections/>
|
||||
<connections>
|
||||
<outlet property="dataSource" destination="I37-dZ-c9Q" id="aji-Ci-VQs"/>
|
||||
<outlet property="delegate" destination="I37-dZ-c9Q" id="YUg-WL-kh8"/>
|
||||
</connections>
|
||||
</tableView>
|
||||
<navigationItem key="navigationItem" title="Sound" id="5hI-rp-d1Z"/>
|
||||
</tableViewController>
|
||||
<placeholder placeholderIdentifier="IBFirstResponder" id="NXc-3P-uoW" userLabel="First Responder" customClass="UIResponder" sceneMemberID="firstResponder"/>
|
||||
</objects>
|
||||
<point key="canvasLocation" x="1500" y="768"/>
|
||||
</scene>
|
||||
<!--Connection Alerts-->
|
||||
<scene sceneID="JyV-QU-Dw8">
|
||||
<objects>
|
||||
<tableViewController id="D2a-Po-vDU" customClass="TVCConnectionAlerts" customModule="AppCheck" customModuleProvider="target" sceneMemberID="viewController">
|
||||
<tableView key="view" clipsSubviews="YES" contentMode="scaleToFill" alwaysBounceVertical="YES" dataMode="static" style="grouped" separatorStyle="default" rowHeight="-1" estimatedRowHeight="-1" sectionHeaderHeight="18" sectionFooterHeight="18" id="u0e-LW-1Qh">
|
||||
<rect key="frame" x="0.0" y="0.0" width="320" height="568"/>
|
||||
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
|
||||
<color key="backgroundColor" systemColor="systemBackgroundColor" cocoaTouchSystemColor="whiteColor"/>
|
||||
<sections>
|
||||
<tableViewSection headerTitle="Connection Alerts" footerTitle="Get a notification whenever a specific DNS request occurs." id="4OJ-qA-l8L">
|
||||
<cells>
|
||||
<tableViewCell clipsSubviews="YES" contentMode="scaleToFill" preservesSuperviewLayoutMargins="YES" selectionStyle="none" indentationWidth="10" textLabel="9g7-rO-sIS" style="IBUITableViewCellStyleDefault" id="8hz-pm-rV5">
|
||||
<rect key="frame" x="0.0" y="55.5" width="320" height="43.5"/>
|
||||
<autoresizingMask key="autoresizingMask"/>
|
||||
<tableViewCellContentView key="contentView" opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center" preservesSuperviewLayoutMargins="YES" insetsLayoutMarginsFromSafeArea="NO" tableViewCell="8hz-pm-rV5" id="fz3-2p-ave">
|
||||
<rect key="frame" x="0.0" y="0.0" width="320" height="43.5"/>
|
||||
<autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMaxY="YES"/>
|
||||
<subviews>
|
||||
<label opaque="NO" multipleTouchEnabled="YES" contentMode="left" insetsLayoutMarginsFromSafeArea="NO" text="Enable Alerts" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" id="9g7-rO-sIS">
|
||||
<rect key="frame" x="16" y="0.0" width="288" height="43.5"/>
|
||||
<autoresizingMask key="autoresizingMask"/>
|
||||
<fontDescription key="fontDescription" style="UICTFontTextStyleBody"/>
|
||||
<nil key="textColor"/>
|
||||
<nil key="highlightedColor"/>
|
||||
</label>
|
||||
<switch opaque="NO" contentMode="scaleToFill" horizontalHuggingPriority="750" verticalHuggingPriority="750" contentHorizontalAlignment="center" contentVerticalAlignment="center" on="YES" translatesAutoresizingMaskIntoConstraints="NO" id="who-8G-voz">
|
||||
<rect key="frame" x="256" y="6" width="48" height="31"/>
|
||||
<autoresizingMask key="autoresizingMask" flexibleMinX="YES" flexibleMaxY="YES"/>
|
||||
<connections>
|
||||
<action selector="toggleShowNotifications:" destination="D2a-Po-vDU" eventType="valueChanged" id="Thg-6R-7wM"/>
|
||||
</connections>
|
||||
</switch>
|
||||
</subviews>
|
||||
</tableViewCellContentView>
|
||||
<connections>
|
||||
<outlet property="accessoryView" destination="who-8G-voz" id="6YE-dG-ThI"/>
|
||||
</connections>
|
||||
</tableViewCell>
|
||||
<tableViewCell clipsSubviews="YES" contentMode="scaleToFill" preservesSuperviewLayoutMargins="YES" selectionStyle="default" accessoryType="disclosureIndicator" indentationWidth="10" textLabel="pck-pT-tnX" detailTextLabel="l8v-5i-Zue" style="IBUITableViewCellStyleValue1" id="laE-pg-nAE">
|
||||
<rect key="frame" x="0.0" y="99" width="320" height="43.5"/>
|
||||
<autoresizingMask key="autoresizingMask"/>
|
||||
<tableViewCellContentView key="contentView" opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center" preservesSuperviewLayoutMargins="YES" insetsLayoutMarginsFromSafeArea="NO" tableViewCell="laE-pg-nAE" id="157-KR-1R5">
|
||||
<rect key="frame" x="0.0" y="0.0" width="293" height="43.5"/>
|
||||
<autoresizingMask key="autoresizingMask"/>
|
||||
<subviews>
|
||||
<label opaque="NO" multipleTouchEnabled="YES" contentMode="left" insetsLayoutMarginsFromSafeArea="NO" text="Sound" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" id="pck-pT-tnX">
|
||||
<rect key="frame" x="16" y="12" width="49.5" height="20.5"/>
|
||||
<autoresizingMask key="autoresizingMask"/>
|
||||
<fontDescription key="fontDescription" style="UICTFontTextStyleBody"/>
|
||||
<nil key="textColor"/>
|
||||
<nil key="highlightedColor"/>
|
||||
</label>
|
||||
<label opaque="NO" multipleTouchEnabled="YES" contentMode="left" insetsLayoutMarginsFromSafeArea="NO" text="Detail" textAlignment="right" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" id="l8v-5i-Zue">
|
||||
<rect key="frame" x="241" y="12" width="44" height="20.5"/>
|
||||
<autoresizingMask key="autoresizingMask"/>
|
||||
<fontDescription key="fontDescription" style="UICTFontTextStyleBody"/>
|
||||
<color key="textColor" systemColor="secondaryLabelColor" red="0.23529411759999999" green="0.23529411759999999" blue="0.26274509800000001" alpha="0.59999999999999998" colorSpace="custom" customColorSpace="sRGB"/>
|
||||
<nil key="highlightedColor"/>
|
||||
</label>
|
||||
</subviews>
|
||||
</tableViewCellContentView>
|
||||
<connections>
|
||||
<segue destination="I37-dZ-c9Q" kind="push" id="tUF-Kv-koO"/>
|
||||
</connections>
|
||||
</tableViewCell>
|
||||
</cells>
|
||||
</tableViewSection>
|
||||
<tableViewSection headerTitle="Operation Mode" footerTitle="Select whether you'd like to manually include or manually exclude specific domains from notifications." id="9fV-UJ-d1S">
|
||||
<cells>
|
||||
<tableViewCell clipsSubviews="YES" contentMode="scaleToFill" preservesSuperviewLayoutMargins="YES" selectionStyle="default" accessoryType="checkmark" indentationWidth="10" textLabel="Lug-Bp-oz0" style="IBUITableViewCellStyleDefault" id="ZMb-xn-r8o">
|
||||
<rect key="frame" x="0.0" y="234" width="320" height="43.5"/>
|
||||
<autoresizingMask key="autoresizingMask"/>
|
||||
<tableViewCellContentView key="contentView" opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center" preservesSuperviewLayoutMargins="YES" insetsLayoutMarginsFromSafeArea="NO" tableViewCell="ZMb-xn-r8o" id="eT6-OU-8eU">
|
||||
<rect key="frame" x="0.0" y="0.0" width="280" height="43.5"/>
|
||||
<autoresizingMask key="autoresizingMask"/>
|
||||
<subviews>
|
||||
<label opaque="NO" multipleTouchEnabled="YES" contentMode="left" insetsLayoutMarginsFromSafeArea="NO" text="Notify only selected lists" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" id="Lug-Bp-oz0">
|
||||
<rect key="frame" x="16" y="0.0" width="256" height="43.5"/>
|
||||
<autoresizingMask key="autoresizingMask"/>
|
||||
<fontDescription key="fontDescription" style="UICTFontTextStyleBody"/>
|
||||
<nil key="textColor"/>
|
||||
<nil key="highlightedColor"/>
|
||||
</label>
|
||||
</subviews>
|
||||
</tableViewCellContentView>
|
||||
</tableViewCell>
|
||||
<tableViewCell clipsSubviews="YES" contentMode="scaleToFill" preservesSuperviewLayoutMargins="YES" selectionStyle="default" accessoryType="checkmark" indentationWidth="10" textLabel="FRE-W4-dw2" style="IBUITableViewCellStyleDefault" id="47P-B8-Sul">
|
||||
<rect key="frame" x="0.0" y="277.5" width="320" height="43.5"/>
|
||||
<autoresizingMask key="autoresizingMask"/>
|
||||
<tableViewCellContentView key="contentView" opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center" preservesSuperviewLayoutMargins="YES" insetsLayoutMarginsFromSafeArea="NO" tableViewCell="47P-B8-Sul" id="RbD-bN-NCj">
|
||||
<rect key="frame" x="0.0" y="0.0" width="280" height="43.5"/>
|
||||
<autoresizingMask key="autoresizingMask"/>
|
||||
<subviews>
|
||||
<label opaque="NO" multipleTouchEnabled="YES" contentMode="left" insetsLayoutMarginsFromSafeArea="NO" text="Notify all, except selected" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" id="FRE-W4-dw2">
|
||||
<rect key="frame" x="16" y="0.0" width="256" height="43.5"/>
|
||||
<autoresizingMask key="autoresizingMask"/>
|
||||
<fontDescription key="fontDescription" style="UICTFontTextStyleBody"/>
|
||||
<nil key="textColor"/>
|
||||
<nil key="highlightedColor"/>
|
||||
</label>
|
||||
</subviews>
|
||||
</tableViewCellContentView>
|
||||
</tableViewCell>
|
||||
</cells>
|
||||
</tableViewSection>
|
||||
<tableViewSection headerTitle="Include" id="Lus-cA-eCF">
|
||||
<cells>
|
||||
<tableViewCell clipsSubviews="YES" contentMode="scaleToFill" preservesSuperviewLayoutMargins="YES" selectionStyle="default" accessoryType="checkmark" indentationWidth="10" textLabel="be7-GU-inU" style="IBUITableViewCellStyleDefault" id="2bN-EB-rDk">
|
||||
<rect key="frame" x="0.0" y="421" width="320" height="43.5"/>
|
||||
<autoresizingMask key="autoresizingMask"/>
|
||||
<tableViewCellContentView key="contentView" opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center" preservesSuperviewLayoutMargins="YES" insetsLayoutMarginsFromSafeArea="NO" tableViewCell="2bN-EB-rDk" id="UxC-Sm-W4n">
|
||||
<rect key="frame" x="0.0" y="0.0" width="280" height="43.5"/>
|
||||
<autoresizingMask key="autoresizingMask"/>
|
||||
<subviews>
|
||||
<label opaque="NO" multipleTouchEnabled="YES" contentMode="left" insetsLayoutMarginsFromSafeArea="NO" text="Blocked Domains" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" id="be7-GU-inU">
|
||||
<rect key="frame" x="16" y="0.0" width="256" height="43.5"/>
|
||||
<autoresizingMask key="autoresizingMask"/>
|
||||
<fontDescription key="fontDescription" style="UICTFontTextStyleBody"/>
|
||||
<nil key="textColor"/>
|
||||
<nil key="highlightedColor"/>
|
||||
</label>
|
||||
</subviews>
|
||||
</tableViewCellContentView>
|
||||
</tableViewCell>
|
||||
<tableViewCell clipsSubviews="YES" contentMode="scaleToFill" preservesSuperviewLayoutMargins="YES" selectionStyle="default" accessoryType="checkmark" indentationWidth="10" textLabel="fYg-Mq-C4Q" style="IBUITableViewCellStyleDefault" id="UTd-2r-8c7">
|
||||
<rect key="frame" x="0.0" y="464.5" width="320" height="43.5"/>
|
||||
<autoresizingMask key="autoresizingMask"/>
|
||||
<tableViewCellContentView key="contentView" opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center" preservesSuperviewLayoutMargins="YES" insetsLayoutMarginsFromSafeArea="NO" tableViewCell="UTd-2r-8c7" id="Z6Y-qL-4bw">
|
||||
<rect key="frame" x="0.0" y="0.0" width="280" height="43.5"/>
|
||||
<autoresizingMask key="autoresizingMask"/>
|
||||
<subviews>
|
||||
<label opaque="NO" multipleTouchEnabled="YES" contentMode="left" insetsLayoutMarginsFromSafeArea="NO" text="Domains of List A" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" id="fYg-Mq-C4Q">
|
||||
<rect key="frame" x="16" y="0.0" width="256" height="43.5"/>
|
||||
<autoresizingMask key="autoresizingMask"/>
|
||||
<fontDescription key="fontDescription" style="UICTFontTextStyleBody"/>
|
||||
<nil key="textColor"/>
|
||||
<nil key="highlightedColor"/>
|
||||
</label>
|
||||
</subviews>
|
||||
</tableViewCellContentView>
|
||||
</tableViewCell>
|
||||
<tableViewCell clipsSubviews="YES" contentMode="scaleToFill" preservesSuperviewLayoutMargins="YES" selectionStyle="default" accessoryType="checkmark" indentationWidth="10" textLabel="f4F-j9-uiy" style="IBUITableViewCellStyleDefault" id="dk0-Vg-0Zl">
|
||||
<rect key="frame" x="0.0" y="508" width="320" height="43.5"/>
|
||||
<autoresizingMask key="autoresizingMask"/>
|
||||
<tableViewCellContentView key="contentView" opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center" preservesSuperviewLayoutMargins="YES" insetsLayoutMarginsFromSafeArea="NO" tableViewCell="dk0-Vg-0Zl" id="69j-ye-ziM">
|
||||
<rect key="frame" x="0.0" y="0.0" width="280" height="43.5"/>
|
||||
<autoresizingMask key="autoresizingMask"/>
|
||||
<subviews>
|
||||
<label opaque="NO" multipleTouchEnabled="YES" contentMode="left" insetsLayoutMarginsFromSafeArea="NO" text="Domains of List B" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" id="f4F-j9-uiy">
|
||||
<rect key="frame" x="16" y="0.0" width="256" height="43.5"/>
|
||||
<autoresizingMask key="autoresizingMask"/>
|
||||
<fontDescription key="fontDescription" style="UICTFontTextStyleBody"/>
|
||||
<nil key="textColor"/>
|
||||
<nil key="highlightedColor"/>
|
||||
</label>
|
||||
</subviews>
|
||||
</tableViewCellContentView>
|
||||
</tableViewCell>
|
||||
<tableViewCell clipsSubviews="YES" contentMode="scaleToFill" preservesSuperviewLayoutMargins="YES" selectionStyle="default" accessoryType="checkmark" indentationWidth="10" textLabel="RiJ-Eq-LiA" style="IBUITableViewCellStyleDefault" id="VTi-I6-dXQ">
|
||||
<rect key="frame" x="0.0" y="551.5" width="320" height="43.5"/>
|
||||
<autoresizingMask key="autoresizingMask"/>
|
||||
<tableViewCellContentView key="contentView" opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center" preservesSuperviewLayoutMargins="YES" insetsLayoutMarginsFromSafeArea="NO" tableViewCell="VTi-I6-dXQ" id="PbB-ri-ibd">
|
||||
<rect key="frame" x="0.0" y="0.0" width="280" height="43.5"/>
|
||||
<autoresizingMask key="autoresizingMask"/>
|
||||
<subviews>
|
||||
<label opaque="NO" multipleTouchEnabled="YES" contentMode="left" insetsLayoutMarginsFromSafeArea="NO" text="Not in Any List" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" id="RiJ-Eq-LiA">
|
||||
<rect key="frame" x="16" y="0.0" width="256" height="43.5"/>
|
||||
<autoresizingMask key="autoresizingMask"/>
|
||||
<fontDescription key="fontDescription" style="UICTFontTextStyleBody"/>
|
||||
<nil key="textColor"/>
|
||||
<nil key="highlightedColor"/>
|
||||
</label>
|
||||
</subviews>
|
||||
</tableViewCellContentView>
|
||||
</tableViewCell>
|
||||
</cells>
|
||||
</tableViewSection>
|
||||
<tableViewSection headerTitle="Custom Lists" footerTitle="" id="5bN-ic-93C">
|
||||
<cells>
|
||||
<tableViewCell clipsSubviews="YES" contentMode="scaleToFill" preservesSuperviewLayoutMargins="YES" selectionStyle="default" accessoryType="disclosureIndicator" indentationWidth="10" textLabel="1fx-L3-esi" detailTextLabel="SAD-ad-RUa" style="IBUITableViewCellStyleValue2" id="fzJ-h6-8Ll">
|
||||
<rect key="frame" x="0.0" y="658.5" width="320" height="44"/>
|
||||
<autoresizingMask key="autoresizingMask"/>
|
||||
<tableViewCellContentView key="contentView" opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center" preservesSuperviewLayoutMargins="YES" insetsLayoutMarginsFromSafeArea="NO" tableViewCell="fzJ-h6-8Ll" id="u6I-My-k4J">
|
||||
<rect key="frame" x="0.0" y="0.0" width="293" height="44"/>
|
||||
<autoresizingMask key="autoresizingMask"/>
|
||||
<subviews>
|
||||
<label opaque="NO" multipleTouchEnabled="YES" contentMode="left" insetsLayoutMarginsFromSafeArea="NO" text="List A" textAlignment="right" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" id="1fx-L3-esi">
|
||||
<rect key="frame" x="16" y="13" width="91" height="18"/>
|
||||
<autoresizingMask key="autoresizingMask"/>
|
||||
<fontDescription key="fontDescription" style="UICTFontTextStyleSubhead"/>
|
||||
<nil key="textColor"/>
|
||||
<nil key="highlightedColor"/>
|
||||
</label>
|
||||
<label opaque="NO" multipleTouchEnabled="YES" contentMode="left" insetsLayoutMarginsFromSafeArea="NO" text="0 Domains" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" id="SAD-ad-RUa">
|
||||
<rect key="frame" x="113" y="13" width="73" height="18"/>
|
||||
<autoresizingMask key="autoresizingMask"/>
|
||||
<fontDescription key="fontDescription" style="UICTFontTextStyleSubhead"/>
|
||||
<color key="textColor" systemColor="secondaryLabelColor" red="0.23529411759999999" green="0.23529411759999999" blue="0.26274509800000001" alpha="0.59999999999999998" colorSpace="custom" customColorSpace="sRGB"/>
|
||||
<nil key="highlightedColor"/>
|
||||
</label>
|
||||
</subviews>
|
||||
</tableViewCellContentView>
|
||||
<connections>
|
||||
<segue destination="q3B-Yi-1bx" kind="push" identifier="segueFilterListCustomA" id="2ak-aX-wVl"/>
|
||||
</connections>
|
||||
</tableViewCell>
|
||||
<tableViewCell clipsSubviews="YES" contentMode="scaleToFill" preservesSuperviewLayoutMargins="YES" selectionStyle="default" accessoryType="disclosureIndicator" indentationWidth="10" textLabel="bP5-xR-UWP" detailTextLabel="3b0-Aj-9So" style="IBUITableViewCellStyleValue2" id="uTh-Xw-vp2">
|
||||
<rect key="frame" x="0.0" y="702.5" width="320" height="44"/>
|
||||
<autoresizingMask key="autoresizingMask"/>
|
||||
<tableViewCellContentView key="contentView" opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center" preservesSuperviewLayoutMargins="YES" insetsLayoutMarginsFromSafeArea="NO" tableViewCell="uTh-Xw-vp2" id="h1L-kH-yQX">
|
||||
<rect key="frame" x="0.0" y="0.0" width="293" height="44"/>
|
||||
<autoresizingMask key="autoresizingMask"/>
|
||||
<subviews>
|
||||
<label opaque="NO" multipleTouchEnabled="YES" contentMode="left" insetsLayoutMarginsFromSafeArea="NO" text="List B" textAlignment="right" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" id="bP5-xR-UWP">
|
||||
<rect key="frame" x="16" y="13" width="91" height="18"/>
|
||||
<autoresizingMask key="autoresizingMask"/>
|
||||
<fontDescription key="fontDescription" style="UICTFontTextStyleSubhead"/>
|
||||
<nil key="textColor"/>
|
||||
<nil key="highlightedColor"/>
|
||||
</label>
|
||||
<label opaque="NO" multipleTouchEnabled="YES" contentMode="left" insetsLayoutMarginsFromSafeArea="NO" text="0 Domains" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" id="3b0-Aj-9So">
|
||||
<rect key="frame" x="113" y="13" width="73" height="18"/>
|
||||
<autoresizingMask key="autoresizingMask"/>
|
||||
<fontDescription key="fontDescription" style="UICTFontTextStyleSubhead"/>
|
||||
<color key="textColor" systemColor="secondaryLabelColor" red="0.23529411759999999" green="0.23529411759999999" blue="0.26274509800000001" alpha="0.59999999999999998" colorSpace="custom" customColorSpace="sRGB"/>
|
||||
<nil key="highlightedColor"/>
|
||||
</label>
|
||||
</subviews>
|
||||
</tableViewCellContentView>
|
||||
<connections>
|
||||
<segue destination="q3B-Yi-1bx" kind="push" identifier="segueFilterListCustomB" id="6wc-d1-VYY"/>
|
||||
</connections>
|
||||
</tableViewCell>
|
||||
</cells>
|
||||
</tableViewSection>
|
||||
</sections>
|
||||
<connections>
|
||||
<outlet property="dataSource" destination="D2a-Po-vDU" id="4t8-lb-7wO"/>
|
||||
<outlet property="delegate" destination="D2a-Po-vDU" id="I1e-X5-6xm"/>
|
||||
</connections>
|
||||
</tableView>
|
||||
<navigationItem key="navigationItem" title="Connection Alerts" id="5Re-pU-mt2"/>
|
||||
<connections>
|
||||
<outlet property="cellSound" destination="laE-pg-nAE" id="Qd5-mf-wox"/>
|
||||
<outlet property="listsCustomA" destination="fzJ-h6-8Ll" id="77h-42-70y"/>
|
||||
<outlet property="listsCustomB" destination="uTh-Xw-vp2" id="6OI-pU-Vff"/>
|
||||
<outlet property="showNotifications" destination="who-8G-voz" id="cUz-Bg-ftS"/>
|
||||
</connections>
|
||||
</tableViewController>
|
||||
<placeholder placeholderIdentifier="IBFirstResponder" id="1z3-Sx-YAL" userLabel="First Responder" customClass="UIResponder" sceneMemberID="firstResponder"/>
|
||||
</objects>
|
||||
<point key="canvasLocation" x="650" y="384"/>
|
||||
</scene>
|
||||
</scenes>
|
||||
<inferredMetricsTieBreakers>
|
||||
<segue reference="tUF-Kv-koO"/>
|
||||
<segue reference="6wc-d1-VYY"/>
|
||||
</inferredMetricsTieBreakers>
|
||||
</document>
|
||||
164
main/GlassVPN.swift
Normal file
@@ -0,0 +1,164 @@
|
||||
import NetworkExtension
|
||||
|
||||
let GlassVPN = GlassVPNManager()
|
||||
|
||||
enum VPNState : Int { case on = 1, inbetween, off }
|
||||
|
||||
final class GlassVPNManager {
|
||||
static let bundleIdentifier = "de.uni-bamberg.psi.AppCheck.VPN"
|
||||
private var managerVPN: NETunnelProviderManager?
|
||||
private(set) var state: VPNState = .off
|
||||
|
||||
fileprivate init() {
|
||||
#if IOS_SIMULATOR
|
||||
postProcessedVPNState(.on)
|
||||
SimulatorVPN.start()
|
||||
#else
|
||||
NETunnelProviderManager.loadAllFromPreferences { managers, error in
|
||||
self.managerVPN = managers?.first {
|
||||
($0.protocolConfiguration as? NETunnelProviderProtocol)?
|
||||
.providerBundleIdentifier == GlassVPNManager.bundleIdentifier
|
||||
}
|
||||
guard let mgr = self.managerVPN else {
|
||||
self.postRawVPNState(.invalid)
|
||||
return
|
||||
}
|
||||
mgr.loadFromPreferences { _ in
|
||||
self.postRawVPNState(mgr.connection.status)
|
||||
}
|
||||
}
|
||||
NSNotification.Name.NEVPNStatusDidChange.observe(call: #selector(vpnStatusChanged(_:)), on: self)
|
||||
#endif
|
||||
NotifyDNSFilterChanged.observe(call: #selector(didChangeDomainFilter), on: self)
|
||||
}
|
||||
|
||||
func setEnabled(_ newState: Bool) {
|
||||
#if IOS_SIMULATOR
|
||||
postProcessedVPNState(newState ? .on : .off)
|
||||
newState ? SimulatorVPN.start() : SimulatorVPN.stop()
|
||||
#else
|
||||
guard let mgr = self.managerVPN else {
|
||||
self.createNewVPN { manager in
|
||||
self.managerVPN = manager
|
||||
self.setEnabled(newState)
|
||||
}
|
||||
return
|
||||
}
|
||||
let state = mgr.isEnabled && (mgr.connection.status == .connected)
|
||||
if state != newState {
|
||||
self.updateVPN({ mgr.isEnabled = true }) {
|
||||
newState ? try? mgr.connection.startVPNTunnel() : mgr.connection.stopVPNTunnel()
|
||||
}
|
||||
}
|
||||
#endif
|
||||
}
|
||||
|
||||
/// Notify VPN extension about changes
|
||||
/// - Returns: `true` on success, `false` if VPN is off or message could not be converted to `.utf8`
|
||||
@discardableResult func send(_ message: VPNAppMessage) -> Bool {
|
||||
#if IOS_SIMULATOR
|
||||
if state == .on, let data = message.raw {
|
||||
SimulatorVPN.sendMsg(data)
|
||||
return true
|
||||
}
|
||||
#else
|
||||
if let session = self.managerVPN?.connection as? NETunnelProviderSession,
|
||||
session.status == .connected, let data = message.raw {
|
||||
do {
|
||||
try session.sendProviderMessage(data, responseHandler: nil)
|
||||
return true
|
||||
} catch {}
|
||||
}
|
||||
#endif
|
||||
return false
|
||||
}
|
||||
|
||||
|
||||
// MARK: - Notify callback
|
||||
|
||||
@objc private func vpnStatusChanged(_ notification: Notification) {
|
||||
postRawVPNState((notification.object as? NETunnelProviderSession)?.status ?? .invalid)
|
||||
}
|
||||
|
||||
@objc private func didChangeDomainFilter(_ notification: Notification) {
|
||||
send(.filterUpdate(domain: notification.object as? String))
|
||||
}
|
||||
|
||||
|
||||
// MARK: - Manage configuration
|
||||
|
||||
private func createNewVPN(_ success: @escaping (_ manager: NETunnelProviderManager) -> Void) {
|
||||
let mgr = NETunnelProviderManager()
|
||||
mgr.localizedDescription = "AppCheck Monitor"
|
||||
let proto = NETunnelProviderProtocol()
|
||||
proto.providerBundleIdentifier = GlassVPNManager.bundleIdentifier
|
||||
proto.serverAddress = "127.0.0.1"
|
||||
mgr.protocolConfiguration = proto
|
||||
mgr.isEnabled = true
|
||||
mgr.saveToPreferences { error in
|
||||
guard error == nil else {
|
||||
self.postProcessedVPNState(.off)
|
||||
//ErrorAlert(error!).presentIn(self.window?.rootViewController)
|
||||
return
|
||||
}
|
||||
success(mgr)
|
||||
}
|
||||
}
|
||||
|
||||
private func updateVPN(_ body: @escaping () -> Void, _ onSuccess: @escaping () -> Void) {
|
||||
self.managerVPN?.loadFromPreferences { error in
|
||||
guard error == nil else { return }
|
||||
body()
|
||||
self.managerVPN?.saveToPreferences { error in
|
||||
guard error == nil else { return }
|
||||
onSuccess()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// MARK: - Post Notifications
|
||||
|
||||
private func postRawVPNState(_ origState: NEVPNStatus) {
|
||||
let state: VPNState
|
||||
switch origState {
|
||||
case .connected: state = .on
|
||||
case .connecting, .disconnecting, .reasserting: state = .inbetween
|
||||
case .invalid, .disconnected: fallthrough
|
||||
@unknown default: state = .off
|
||||
}
|
||||
postProcessedVPNState(state)
|
||||
}
|
||||
|
||||
private func postProcessedVPNState(_ state: VPNState) {
|
||||
self.state = state
|
||||
NotifyVPNStateChanged.post()
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// ---------------------------------------------------------------
|
||||
// |
|
||||
// | MARK: - VPN message
|
||||
// |
|
||||
// ---------------------------------------------------------------
|
||||
|
||||
struct VPNAppMessage {
|
||||
let raw: Data?
|
||||
init(_ string: String) { raw = string.data(using: .utf8) }
|
||||
|
||||
static func filterUpdate(domain: String? = nil) -> Self {
|
||||
.init("filter-update:\(domain ?? "")")
|
||||
}
|
||||
static func autoDelete(after interval: Int) -> Self {
|
||||
.init("auto-delete:\(interval)")
|
||||
}
|
||||
/// Only used for connection alert notifications
|
||||
static func notificationSettingsChanged() -> Self {
|
||||
.init("notify-prefs-change:1")
|
||||
}
|
||||
/// Triggered whenever user taps on the start/stop recording button
|
||||
static func isRecording(_ state: CurrentRecordingState) -> Self {
|
||||
.init("recording-now:\(state.rawValue)")
|
||||
}
|
||||
}
|
||||
146
main/GlassVPNHook.swift
Normal file
@@ -0,0 +1,146 @@
|
||||
import Foundation
|
||||
|
||||
class GlassVPNHook {
|
||||
|
||||
private let queue = DispatchQueue.init(label: "PSIGlassDNSQueue", qos: .userInteractive, target: .main)
|
||||
|
||||
private var filterDomains: [String]!
|
||||
private var filterOptions: [(block: Bool, ignore: Bool, customA: Bool, customB: Bool)]!
|
||||
private var autoDeleteTimer: Timer? = nil
|
||||
private var cachedNotify: CachedConnectionAlert!
|
||||
private var currentlyRecording: Bool = false
|
||||
|
||||
init() { reset() }
|
||||
|
||||
/// Reload from stored settings and rebuilt binary search tree
|
||||
private func reset() {
|
||||
reloadDomainFilter()
|
||||
setAutoDelete(PrefsShared.AutoDeleteLogsDays)
|
||||
cachedNotify = CachedConnectionAlert()
|
||||
currentlyRecording = PrefsShared.CurrentlyRecording != .Off
|
||||
}
|
||||
|
||||
/// Invalidate auto-delete timer and release stored properties. You should nullify this instance afterwards.
|
||||
func cleanUp() {
|
||||
filterDomains = nil
|
||||
filterOptions = nil
|
||||
autoDeleteTimer?.fire() // one last time before we quit
|
||||
autoDeleteTimer?.invalidate()
|
||||
cachedNotify = nil
|
||||
currentlyRecording = false
|
||||
}
|
||||
|
||||
/// Call this method from `PacketTunnelProvider.handleAppMessage(_:completionHandler:)`
|
||||
func handleAppMessage(_ messageData: Data) {
|
||||
let message = String(data: messageData, encoding: .utf8)
|
||||
if let msg = message, let i = msg.firstIndex(of: ":") {
|
||||
let action = msg.prefix(upTo: i)
|
||||
let value = msg.suffix(from: msg.index(after: i))
|
||||
switch action {
|
||||
case "filter-update":
|
||||
reloadDomainFilter() // TODO: reload only selected domain?
|
||||
return
|
||||
case "auto-delete":
|
||||
setAutoDelete(Int(value) ?? PrefsShared.AutoDeleteLogsDays)
|
||||
return
|
||||
case "notify-prefs-change":
|
||||
cachedNotify = CachedConnectionAlert()
|
||||
return
|
||||
case "recording-now":
|
||||
let newState = CurrentRecordingState(rawValue: Int(value) ?? 0)
|
||||
currentlyRecording = newState != .Off
|
||||
return
|
||||
default: break
|
||||
}
|
||||
}
|
||||
NSLog("[VPN.WARN] This should never happen! Received unknown handleAppMessage: \(message ?? messageData.base64EncodedString())")
|
||||
reset() // just in case we fallback to do everything
|
||||
}
|
||||
|
||||
|
||||
// MARK: - Process DNS Request
|
||||
|
||||
/// Log domain request and post notification (if enabled).
|
||||
/// - Returns: `true` if the request shoud be blocked.
|
||||
func processDNSRequest(_ domain: String) -> Bool {
|
||||
let i = filterIndex(for: domain)
|
||||
let (block, ignore, cA, cB) = (i<0) ? (false, false, false, false) : filterOptions[i]
|
||||
if ignore, !currentlyRecording {
|
||||
return block
|
||||
}
|
||||
let blockActive = block && !currentlyRecording
|
||||
queue.async {
|
||||
do { try AppDB?.logWrite(domain, blocked: blockActive) }
|
||||
catch { NSLog("[VPN.WARN] Couldn't write: \(error)") }
|
||||
}
|
||||
// TODO: disable notifications during recording?
|
||||
cachedNotify.postOrIgnore(domain, blck: block, custA: cA, custB: cB)
|
||||
// TODO: wait for notify response to block or allow connection
|
||||
return blockActive
|
||||
}
|
||||
|
||||
/// Build binary tree for reverse DNS lookup
|
||||
private func reloadDomainFilter() {
|
||||
let tmp = AppDB?.loadFilters()?.map({
|
||||
(String($0.reversed()), $1)
|
||||
}).sorted(by: { $0.0 < $1.0 }) ?? []
|
||||
let t1 = tmp.map { $0.0 }
|
||||
let t2 = tmp.map { ($1.contains(.blocked),
|
||||
$1.contains(.ignored),
|
||||
$1.contains(.customA),
|
||||
$1.contains(.customB)) }
|
||||
filterDomains = t1
|
||||
filterOptions = t2
|
||||
}
|
||||
|
||||
/// Lookup for reverse DNS binary tree
|
||||
private func filterIndex(for domain: String) -> Int {
|
||||
let reverseDomain = String(domain.reversed())
|
||||
var lo = 0, hi = filterDomains.count - 1
|
||||
while lo <= hi {
|
||||
let mid = (lo + hi)/2
|
||||
if filterDomains[mid] < reverseDomain {
|
||||
lo = mid + 1
|
||||
} else if reverseDomain < filterDomains[mid] {
|
||||
hi = mid - 1
|
||||
} else {
|
||||
return mid
|
||||
}
|
||||
}
|
||||
if lo > 0, reverseDomain.hasPrefix(filterDomains[lo - 1] + ".") {
|
||||
return lo - 1
|
||||
}
|
||||
return -1
|
||||
}
|
||||
|
||||
|
||||
// MARK: - Auto-delete Timer
|
||||
|
||||
/// Prepare auto-delete timer with interval between 1 hr - 1 day.
|
||||
/// - Parameter days: Max age to keep when deleting
|
||||
private func setAutoDelete(_ days: Int) {
|
||||
autoDeleteTimer?.invalidate()
|
||||
guard days > 0 else { return }
|
||||
// Repeat interval uses days as hours. min 1 hr, max 24 hrs.
|
||||
let interval = TimeInterval(min(24, days) * 60 * 60)
|
||||
autoDeleteTimer = Timer.scheduledTimer(timeInterval: interval,
|
||||
target: self, selector: #selector(autoDeleteNow),
|
||||
userInfo: days, repeats: true)
|
||||
autoDeleteTimer!.fire()
|
||||
}
|
||||
|
||||
/// Callback fired when old data should be deleted.
|
||||
@objc private func autoDeleteNow(_ sender: Timer) {
|
||||
NSLog("[VPN.INFO] Auto-delete old logs")
|
||||
queue.async {
|
||||
do {
|
||||
try AppDB?.dnsLogsDeleteOlderThan(days: sender.userInfo as! Int)
|
||||
} catch {
|
||||
NSLog("[VPN.WARN] Couldn't delete logs, will retry in 5 minutes. \(error)")
|
||||
if sender.isValid {
|
||||
sender.fireDate = Date().addingTimeInterval(300) // retry in 5 min
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
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"))
|
||||
}
|
||||
}
|
||||
}
|
||||