Compare commits
7 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
d96ced48c9 | ||
|
|
0b6dbfd888 | ||
|
|
96656438c6 | ||
|
|
4b32df5683 | ||
|
|
0758bd7dec | ||
|
|
171dabd83a | ||
|
|
6182a99ebd |
@@ -16,6 +16,8 @@
|
||||
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 */; };
|
||||
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 */; };
|
||||
@@ -176,6 +178,8 @@
|
||||
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>"; };
|
||||
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>"; };
|
||||
@@ -328,6 +332,7 @@
|
||||
5412F8ED24571B8100A63D7A /* VCDateFilter.swift */,
|
||||
54953E6023E0D69A0054345C /* TVCHosts.swift */,
|
||||
54953E6E23E44CD00054345C /* TVCHostDetails.swift */,
|
||||
541FC47424A12CE9009154D8 /* Analytics */,
|
||||
);
|
||||
path = Requests;
|
||||
sourceTree = "<group>";
|
||||
@@ -394,6 +399,14 @@
|
||||
path = main;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
541FC47424A12CE9009154D8 /* Analytics */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
541FC47724A1453F009154D8 /* VCCoOccurrence.swift */,
|
||||
);
|
||||
path = Analytics;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
542E2A9B24051F79001462DC /* media */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
@@ -425,6 +438,7 @@
|
||||
54D8B979246C9F2000EB2414 /* FilterPipeline.swift */,
|
||||
54448A3124899A4000771C96 /* SearchBarManager.swift */,
|
||||
54EFA4E5248EEE240022D618 /* DatePickerAlert.swift */,
|
||||
541FC47524A12D01009154D8 /* IBViews.swift */,
|
||||
);
|
||||
path = "Common Classes";
|
||||
sourceTree = "<group>";
|
||||
@@ -830,6 +844,7 @@
|
||||
540E67842433FAFE00871BBE /* TVCPreviousRecords.swift in Sources */,
|
||||
54B345A6241BB982004C53CC /* Notifications.swift in Sources */,
|
||||
54448A2E2486464F00771C96 /* Array.swift in Sources */,
|
||||
541FC47824A1453F009154D8 /* VCCoOccurrence.swift in Sources */,
|
||||
54B345AB241BBA5B004C53CC /* AlertSheet.swift in Sources */,
|
||||
544C95262407B1C700AB89D0 /* SharedState.swift in Sources */,
|
||||
545DDDCF243E6267003B6544 /* TutorialSheet.swift in Sources */,
|
||||
@@ -855,6 +870,7 @@
|
||||
5412F8EE24571B8200A63D7A /* VCDateFilter.swift in Sources */,
|
||||
545DDDD124436983003B6544 /* QuickUI.swift in Sources */,
|
||||
541AC5D82399498A00A769D7 /* AppDelegate.swift in Sources */,
|
||||
541FC47624A12D01009154D8 /* IBViews.swift in Sources */,
|
||||
540E67822433483D00871BBE /* VCEditRecording.swift in Sources */,
|
||||
54EFA4E82491A16A0022D618 /* Font.swift in Sources */,
|
||||
54D8B97A246C9F2000EB2414 /* FilterPipeline.swift in Sources */,
|
||||
@@ -1111,7 +1127,7 @@
|
||||
ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES;
|
||||
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
|
||||
CODE_SIGN_ENTITLEMENTS = main/main.entitlements;
|
||||
CURRENT_PROJECT_VERSION = 21;
|
||||
CURRENT_PROJECT_VERSION = 22;
|
||||
INFOPLIST_FILE = main/Info.plist;
|
||||
LD_RUNPATH_SEARCH_PATHS = (
|
||||
"$(inherited)",
|
||||
@@ -1130,7 +1146,7 @@
|
||||
ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES;
|
||||
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
|
||||
CODE_SIGN_ENTITLEMENTS = main/main.entitlements;
|
||||
CURRENT_PROJECT_VERSION = 21;
|
||||
CURRENT_PROJECT_VERSION = 22;
|
||||
INFOPLIST_FILE = main/Info.plist;
|
||||
LD_RUNPATH_SEARCH_PATHS = (
|
||||
"$(inherited)",
|
||||
@@ -1149,7 +1165,7 @@
|
||||
CLANG_ENABLE_MODULES = YES;
|
||||
CODE_SIGN_ENTITLEMENTS = GlassVPN/GlassVPN.entitlements;
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
CURRENT_PROJECT_VERSION = 21;
|
||||
CURRENT_PROJECT_VERSION = 22;
|
||||
INFOPLIST_FILE = GlassVPN/Info.plist;
|
||||
MARKETING_VERSION = 1.0.0;
|
||||
PRODUCT_BUNDLE_IDENTIFIER = "de.uni-bamberg.psi.AppCheck.VPN";
|
||||
@@ -1167,7 +1183,7 @@
|
||||
CLANG_ENABLE_MODULES = YES;
|
||||
CODE_SIGN_ENTITLEMENTS = GlassVPN/GlassVPN.entitlements;
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
CURRENT_PROJECT_VERSION = 21;
|
||||
CURRENT_PROJECT_VERSION = 22;
|
||||
INFOPLIST_FILE = GlassVPN/Info.plist;
|
||||
MARKETING_VERSION = 1.0.0;
|
||||
PRODUCT_BUNDLE_IDENTIFIER = "de.uni-bamberg.psi.AppCheck.VPN";
|
||||
|
||||
BIN
main/Assets.xcassets/.DS_Store
vendored
Normal file
BIN
main/Assets.xcassets/.DS_Store
vendored
Normal file
Binary file not shown.
23
main/Assets.xcassets/intersection.imageset/Contents.json
vendored
Normal file
23
main/Assets.xcassets/intersection.imageset/Contents.json
vendored
Normal file
@@ -0,0 +1,23 @@
|
||||
{
|
||||
"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
|
||||
}
|
||||
}
|
||||
BIN
main/Assets.xcassets/intersection.imageset/img.png
vendored
Normal file
BIN
main/Assets.xcassets/intersection.imageset/img.png
vendored
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 385 B |
BIN
main/Assets.xcassets/intersection.imageset/img@2x.png
vendored
Normal file
BIN
main/Assets.xcassets/intersection.imageset/img@2x.png
vendored
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 695 B |
BIN
main/Assets.xcassets/intersection.imageset/img@3x.png
vendored
Normal file
BIN
main/Assets.xcassets/intersection.imageset/img@3x.png
vendored
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 1.0 KiB |
@@ -160,7 +160,7 @@
|
||||
<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" id="jlK-69-8hl"/>
|
||||
<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>
|
||||
@@ -326,11 +326,6 @@
|
||||
</barButtonItem>
|
||||
<barButtonItem enabled="NO" title="7 days" id="wxA-bC-1pN"/>
|
||||
</leftBarButtonItems>
|
||||
<barButtonItem key="rightBarButtonItem" systemItem="search" id="FHY-of-M4V">
|
||||
<connections>
|
||||
<action selector="searchButtonTapped:" destination="pdd-aM-sKl" id="HH1-6f-mcM"/>
|
||||
</connections>
|
||||
</barButtonItem>
|
||||
</navigationItem>
|
||||
<connections>
|
||||
<outlet property="filterButton" destination="FZm-Ld-jJE" id="g96-Q2-cYX"/>
|
||||
@@ -383,13 +378,7 @@
|
||||
<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">
|
||||
<barButtonItem key="rightBarButtonItem" systemItem="search" id="cBL-dP-ig1">
|
||||
<connections>
|
||||
<action selector="searchButtonTapped:" destination="WcC-nb-Vf5" id="QFl-Me-lc6"/>
|
||||
</connections>
|
||||
</barButtonItem>
|
||||
</navigationItem>
|
||||
<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>
|
||||
@@ -403,9 +392,20 @@
|
||||
<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"/>
|
||||
<tabBar key="tableHeaderView" contentMode="scaleToFill" fixedFrame="YES" translucent="NO" translatesAutoresizingMaskIntoConstraints="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="h7Z-Qr-pJ5" id="qNN-nI-Kub"/>
|
||||
</connections>
|
||||
</tabBar>
|
||||
<prototypes>
|
||||
<tableViewCell clipsSubviews="YES" contentMode="scaleToFill" preservesSuperviewLayoutMargins="YES" selectionStyle="default" indentationWidth="10" reuseIdentifier="HostDetailCell" textLabel="J2P-mU-Vad" detailTextLabel="eWb-mX-udN" style="IBUITableViewCellStyleValue1" id="ZCA-Dz-i92">
|
||||
<rect key="frame" x="0.0" y="28" width="320" height="43.5"/>
|
||||
<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="320" height="43.5"/>
|
||||
@@ -435,11 +435,211 @@
|
||||
</connections>
|
||||
</tableView>
|
||||
<navigationItem key="navigationItem" title="Occurrences" prompt="com.domain.network.cdn" id="bys-2u-rHs"/>
|
||||
<connections>
|
||||
<outlet property="actionsBar" destination="1Jy-zg-CXR" id="7x3-Vy-i9C"/>
|
||||
<segue destination="W5Q-oz-bFb" kind="modal" identifier="segueAnalysisCoOccurrence" id="ukY-Dy-AIA"/>
|
||||
</connections>
|
||||
</tableViewController>
|
||||
<placeholder placeholderIdentifier="IBFirstResponder" id="UxH-PH-KQy" userLabel="First Responder" customClass="UIResponder" sceneMemberID="firstResponder"/>
|
||||
</objects>
|
||||
<point key="canvasLocation" x="2100" y="-1250"/>
|
||||
</scene>
|
||||
<!--Co Occurrence-->
|
||||
<scene sceneID="Gbm-AP-b72">
|
||||
<objects>
|
||||
<viewController 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="548"/>
|
||||
<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="56"/>
|
||||
<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" id="bTi-7F-CFS">
|
||||
<button key="customView" opaque="NO" contentMode="scaleToFill" contentHorizontalAlignment="center" contentVerticalAlignment="center" buttonType="infoLight" showsTouchWhenHighlighted="YES" lineBreakMode="middleTruncation" id="kqK-SL-CxZ">
|
||||
<rect key="frame" x="279" y="16" width="25" height="24"/>
|
||||
<autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMaxY="YES"/>
|
||||
<connections>
|
||||
<action selector="showInfoScreen" destination="W5Q-oz-bFb" eventType="touchUpInside" id="TuI-R9-PNr"/>
|
||||
</connections>
|
||||
</button>
|
||||
</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="56" width="320" height="492"/>
|
||||
<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" adjustsFontSizeToFit="NO" 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="2800" y="-1250"/>
|
||||
</scene>
|
||||
<!--Recordings-->
|
||||
<scene sceneID="ODR-PD-nTU">
|
||||
<objects>
|
||||
@@ -1042,6 +1242,7 @@ Duration: 60:00</string>
|
||||
</inferredMetricsTieBreakers>
|
||||
<resources>
|
||||
<image name="filter-clear" width="20" height="20"/>
|
||||
<image name="intersection" width="25" height="25"/>
|
||||
<image name="journal" width="25" height="25"/>
|
||||
<image name="settings" width="25" height="25"/>
|
||||
<image name="tag" width="25" height="25"/>
|
||||
|
||||
83
main/Common Classes/IBViews.swift
Normal file
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 = .sysFg
|
||||
@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)))
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -36,6 +36,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
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,107 +1,48 @@
|
||||
import UIKit
|
||||
|
||||
/// Assigns a `UISearchBar` to the `tableHeaderView` property of a `UITableView`.
|
||||
class SearchBarManager: NSObject, UISearchBarDelegate {
|
||||
class SearchBarManager: NSObject, UISearchResultsUpdating {
|
||||
|
||||
private weak var tableView: UITableView?
|
||||
private let searchBar: UISearchBar
|
||||
private(set) var active: Bool = false
|
||||
|
||||
typealias OnChange = (String) -> Void
|
||||
typealias OnHide = () -> Void
|
||||
private var onChangeCallback: OnChange!
|
||||
private var onHideCallback: OnHide?
|
||||
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 tableView: The `tableHeaderView` property is used for display.
|
||||
required init(on tableView: UITableView) {
|
||||
self.tableView = tableView
|
||||
searchBar = UISearchBar(frame: CGRect.init(x: 0, y: 0, width: 20, height: 10))
|
||||
searchBar.sizeToFit() // sets height, width is set by table view header
|
||||
searchBar.showsCancelButton = true
|
||||
searchBar.autocapitalizationType = .none
|
||||
searchBar.autocorrectionType = .no
|
||||
/// - 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()
|
||||
searchBar.delegate = self
|
||||
|
||||
UITextField.appearance(whenContainedInInstancesOf: [UISearchBar.self])
|
||||
.defaultTextAttributes = [.font: UIFont.preferredFont(forTextStyle: .body)]
|
||||
}
|
||||
|
||||
|
||||
// MARK: Show & Hide
|
||||
|
||||
/// Insert search bar in `tableView` and call `reloadData()` after animation.
|
||||
/// - Parameters:
|
||||
/// - onHide: Code that will be executed once the search bar is dismissed.
|
||||
/// - onChange: Code that will be executed every time the user changes the text (with 0.2s delay)
|
||||
func show(onHide: OnHide? = nil, onChange: @escaping OnChange) {
|
||||
onChangeCallback = onChange
|
||||
onHideCallback = onHide
|
||||
setSearchBarHidden(false)
|
||||
}
|
||||
|
||||
/// Remove search bar from `tableView` and call `reloadData()` after animation.
|
||||
func hide() {
|
||||
setSearchBarHidden(true)
|
||||
}
|
||||
|
||||
/// Internal method to insert or remove the `UISearchBar` as `tableHeaderView`
|
||||
private func setSearchBarHidden(_ flag: Bool) {
|
||||
active = !flag
|
||||
searchBar.text = nil
|
||||
guard let tv = tableView else {
|
||||
hideAndRelease()
|
||||
return
|
||||
}
|
||||
let h = searchBar.frame.height
|
||||
if active {
|
||||
tv.scrollToTop(animated: false)
|
||||
tv.tableHeaderView = searchBar
|
||||
tv.frame.origin.y -= h
|
||||
tv.frame.size.height += h
|
||||
UIView.animate(withDuration: 0.3, animations: {
|
||||
tv.frame.origin.y += h
|
||||
tv.frame.size.height -= h
|
||||
}) { _ in
|
||||
tv.reloadData()
|
||||
self.searchBar.becomeFirstResponder()
|
||||
}
|
||||
/// 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 {
|
||||
searchBar.resignFirstResponder()
|
||||
UIView.animate(withDuration: 0.3, animations: {
|
||||
tv.frame.origin.y -= h
|
||||
tv.frame.size.height += h
|
||||
tv.scrollToTop(animated: false) // false to let UIView animate the change
|
||||
}) { _ in
|
||||
tv.frame.origin.y += h
|
||||
tv.frame.size.height -= h
|
||||
self.hideAndRelease()
|
||||
tv.reloadData()
|
||||
}
|
||||
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)
|
||||
}
|
||||
}
|
||||
|
||||
/// Call `OnHide` closure (if set), then release strong closure references.
|
||||
private func hideAndRelease() {
|
||||
tableView?.tableHeaderView = nil
|
||||
onHideCallback?()
|
||||
onHideCallback = nil
|
||||
onChangeCallback = nil
|
||||
}
|
||||
|
||||
|
||||
// MARK: Search Bar Delegate
|
||||
|
||||
func searchBarCancelButtonClicked(_ _: UISearchBar) {
|
||||
setSearchBarHidden(true)
|
||||
}
|
||||
|
||||
func searchBarSearchButtonClicked(_ searchBar: UISearchBar) {
|
||||
searchBar.resignFirstResponder()
|
||||
}
|
||||
|
||||
func searchBar(_ _: UISearchBar, textDidChange _: String) {
|
||||
/// Search callback
|
||||
func updateSearchResults(for controller: UISearchController) {
|
||||
NSObject.cancelPreviousPerformRequests(withTarget: self, selector: #selector(performSearch), object: nil)
|
||||
perform(#selector(performSearch), with: nil, afterDelay: 0.2)
|
||||
}
|
||||
@@ -109,7 +50,8 @@ class SearchBarManager: NSObject, UISearchBarDelegate {
|
||||
/// Internal callback function for delayed text evaluation.
|
||||
/// This way we can avoid unnecessary searches while user is typing.
|
||||
@objc private func performSearch() {
|
||||
onChangeCallback(searchBar.text ?? "")
|
||||
tableView?.reloadData()
|
||||
term = controller.searchBar.text?.lowercased() ?? ""
|
||||
isActive = term.count > 0
|
||||
onChangeCallback(term)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -105,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,7 +133,7 @@ class TutorialSheet: UIViewController, UIScrollViewDelegate {
|
||||
|
||||
pager.anchor([.top, .left, .right], to: sheetBg)
|
||||
pageScroll.topAnchor =&= pager.bottomAnchor
|
||||
pageScroll.anchor([.left, .right, .top, .bottom], to: sheetBg, margin: cornerRadius/2) | .defaultHigh
|
||||
pageScroll.anchor([.left, .right, .top, .bottom], to: sheetBg, margin: sheetInset) | .defaultHigh
|
||||
button.topAnchor =&= pageScroll.bottomAnchor
|
||||
button.anchor([.bottom, .centerX], to: sheetBg)
|
||||
// button.bottomAnchor =&= sheetBg.bottomAnchor - 30
|
||||
|
||||
@@ -172,15 +172,11 @@ extension SQLiteDatabase {
|
||||
/// 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 `
|
||||
/// - ts1: Restrict result set `ts >= ?`
|
||||
/// - ts2: Restrict result set `ts < ?`
|
||||
/// - 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 = (0,0), since ts1: Timestamp = 0, upto ts2: Timestamp = 0,
|
||||
matchingDomain: String? = nil, parentDomain: String? = nil) -> [GroupedDomain]?
|
||||
{
|
||||
let Where = WhereClauseBuilder().and(in: range).and(min: ts1, max: ts2)
|
||||
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"
|
||||
@@ -206,7 +202,7 @@ extension SQLiteDatabase {
|
||||
/// - 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 = (0,0)) -> [GroupedTsOccurrence]? {
|
||||
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) {
|
||||
@@ -218,6 +214,71 @@ extension SQLiteDatabase {
|
||||
|
||||
|
||||
|
||||
// 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(_ fqdn: String) -> [Timestamp]? {
|
||||
try? run(sql: "SELECT DISTINCT ts FROM heap WHERE fqdn = ? ORDER BY ts;", bind: [BindText(fqdn)]) {
|
||||
allRows($0) { sqlite3_column_int64($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 fqdn: String) -> [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 fqdn != ? AND dist <= ?
|
||||
) GROUP BY fqdn
|
||||
) ORDER BY rank ASC LIMIT 99;
|
||||
""", bind: [BindInt64(dt), BindInt64(low), BindInt64(high), BindText(fqdn), BindInt64(dt)]) {
|
||||
allRows($0) {
|
||||
(readText($0, 0) ?? "", sqlite3_column_int($0, 1), sqlite3_column_double($0, 2), sqlite3_column_double($0, 3))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
// MARK: - Recordings
|
||||
|
||||
extension CreateTable {
|
||||
|
||||
@@ -138,6 +138,7 @@ extension SQLiteDatabase {
|
||||
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))") }
|
||||
|
||||
@@ -15,10 +15,13 @@ class GroupedDomainDataSource: FilterPipelineDelegate, SyncUpdateDelegate {
|
||||
|
||||
let parent: String?
|
||||
private let pipeline = FilterPipeline<GroupedDomain>()
|
||||
private lazy var search = SearchBarManager(on: delegate!.tableView)
|
||||
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 {
|
||||
@@ -28,6 +31,13 @@ class GroupedDomainDataSource: FilterPipelineDelegate, SyncUpdateDelegate {
|
||||
/// - 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)
|
||||
|
||||
@@ -222,36 +232,6 @@ extension GroupedDomainDataSource {
|
||||
}
|
||||
|
||||
|
||||
// ################################
|
||||
// #
|
||||
// # MARK: - Search
|
||||
// #
|
||||
// ################################
|
||||
|
||||
extension GroupedDomainDataSource {
|
||||
// TODO: permanently show search bar as table header?
|
||||
func toggleSearch() {
|
||||
if search.active { search.hide() }
|
||||
else {
|
||||
// Begin animations group. Otherwise the `scrollToTop` animation is broken.
|
||||
// This is due to `addFilter` calling `reloadData()` before `search.show()` can animate it.
|
||||
cellAnimationsGroup()
|
||||
var searchTerm = ""
|
||||
pipeline.addFilter("search") {
|
||||
$0.domain.lowercased().contains(searchTerm)
|
||||
}
|
||||
search.show(onHide: { [unowned self] in
|
||||
self.pipeline.removeFilter(withId: "search")
|
||||
}, onChange: { [unowned self] in
|
||||
searchTerm = $0.lowercased()
|
||||
self.pipeline.reloadFilter(withId: "search")
|
||||
})
|
||||
cellAnimationsCommit()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// ##########################
|
||||
// #
|
||||
// # MARK: - Edit Row
|
||||
@@ -308,7 +288,7 @@ extension GroupedDomainEditRow {
|
||||
// MARK: Extensions
|
||||
extension TVCDomains : GroupedDomainEditRow {
|
||||
override func tableView(_ tableView: UITableView, editActionsForRowAt indexPath: IndexPath) -> [UITableViewRowAction]? {
|
||||
getRowActionsIOS9(indexPath)
|
||||
getRowActionsIOS9(indexPath, tableView)
|
||||
}
|
||||
@available(iOS 11.0, *)
|
||||
override func tableView(_ tableView: UITableView, trailingSwipeActionsConfigurationForRowAt indexPath: IndexPath) -> UISwipeActionsConfiguration? {
|
||||
@@ -318,7 +298,7 @@ extension TVCDomains : GroupedDomainEditRow {
|
||||
|
||||
extension TVCHosts : GroupedDomainEditRow {
|
||||
override func tableView(_ tableView: UITableView, editActionsForRowAt indexPath: IndexPath) -> [UITableViewRowAction]? {
|
||||
getRowActionsIOS9(indexPath)
|
||||
getRowActionsIOS9(indexPath, tableView)
|
||||
}
|
||||
@available(iOS 11.0, *)
|
||||
override func tableView(_ tableView: UITableView, trailingSwipeActionsConfigurationForRowAt indexPath: IndexPath) -> UISwipeActionsConfiguration? {
|
||||
|
||||
@@ -159,7 +159,6 @@ class SyncUpdate {
|
||||
notify(insert: r, .Latest)
|
||||
}
|
||||
} else if range != nil {
|
||||
// FIXME: removing latest entries will invalidate "last changed" label
|
||||
if let r = rows(from(new!), to(old), scope: range!) {
|
||||
notify(remove: r, .Latest)
|
||||
}
|
||||
|
||||
@@ -13,6 +13,14 @@ extension UIFont {
|
||||
}
|
||||
}
|
||||
|
||||
extension NSAttributedString {
|
||||
static func image(_ img: UIImage) -> Self {
|
||||
let att = NSTextAttachment()
|
||||
att.image = img
|
||||
return self.init(attachment: att)
|
||||
}
|
||||
}
|
||||
|
||||
extension NSMutableAttributedString {
|
||||
static private var def: UIFont = .preferredFont(forTextStyle: .body)
|
||||
|
||||
@@ -31,4 +39,13 @@ extension NSMutableAttributedString {
|
||||
]))
|
||||
return self
|
||||
}
|
||||
|
||||
func centered(_ content: NSAttributedString) -> Self {
|
||||
let before = length
|
||||
append(content)
|
||||
let ps = NSMutableParagraphStyle()
|
||||
ps.alignment = .center
|
||||
addAttribute(.paragraphStyle, value: ps, range: .init(location: before, length: content.length))
|
||||
return self
|
||||
}
|
||||
}
|
||||
|
||||
@@ -25,6 +25,12 @@ enum Pref {
|
||||
set { Pref.Bool(newValue, "didShowTutorialRecordings") }
|
||||
}
|
||||
}
|
||||
enum ContextAnalyis {
|
||||
static var CoOccurrenceTime: Int? {
|
||||
get { Pref.Any("contextAnalyisCoOccurrenceTime") as? Int }
|
||||
set { Pref.Any(newValue, "contextAnalyisCoOccurrenceTime") }
|
||||
}
|
||||
}
|
||||
enum DateFilter {
|
||||
static var Kind: DateFilterKind {
|
||||
get { DateFilterKind(rawValue: Pref.Int("dateFilterType"))! }
|
||||
|
||||
@@ -44,16 +44,11 @@ extension UITableView {
|
||||
isFrontmost ? moveRow(at: IndexPath(row: from), to: IndexPath(row: to)) : reloadData()
|
||||
}
|
||||
|
||||
/// Scroll table to top (while respecting `contentInset`)
|
||||
func scrollToTop(animated: Bool) {
|
||||
let top: CGFloat
|
||||
if #available(iOS 11.0, *) {
|
||||
top = adjustedContentInset.top
|
||||
} else {
|
||||
top = contentInset.top
|
||||
}
|
||||
if contentOffset.y != -top {
|
||||
setContentOffset(.init(x: 0, y: -top), animated: animated)
|
||||
/// Recalculate and apply new `tableHeaderView` height.
|
||||
func sizeHeaderToFit() {
|
||||
if let head = tableHeaderView {
|
||||
head.frame.size.height = head.systemLayoutSizeFitting(UIView.layoutFittingCompressedSize).height
|
||||
tableHeaderView = head
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -73,10 +68,13 @@ protocol EditableRows {
|
||||
}
|
||||
|
||||
extension EditableRows where Self: UITableViewDelegate {
|
||||
func getRowActionsIOS9(_ index: IndexPath) -> [UITableViewRowAction]? {
|
||||
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) }
|
||||
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
|
||||
}
|
||||
|
||||
@@ -77,7 +77,7 @@ class TVCPreviousRecords: UITableViewController, EditActionsRemove {
|
||||
// MARK: - Editing
|
||||
|
||||
override func tableView(_ tableView: UITableView, editActionsForRowAt indexPath: IndexPath) -> [UITableViewRowAction]? {
|
||||
getRowActionsIOS9(indexPath)
|
||||
getRowActionsIOS9(indexPath, tableView)
|
||||
}
|
||||
@available(iOS 11.0, *)
|
||||
override func tableView(_ tableView: UITableView, trailingSwipeActionsConfigurationForRowAt indexPath: IndexPath) -> UISwipeActionsConfiguration? {
|
||||
|
||||
@@ -26,7 +26,7 @@ class TVCRecordingDetails: UITableViewController, EditActionsRemove {
|
||||
// MARK: - Editing
|
||||
|
||||
override func tableView(_ tableView: UITableView, editActionsForRowAt indexPath: IndexPath) -> [UITableViewRowAction]? {
|
||||
getRowActionsIOS9(indexPath)
|
||||
getRowActionsIOS9(indexPath, tableView)
|
||||
}
|
||||
@available(iOS 11.0, *)
|
||||
override func tableView(_ tableView: UITableView, trailingSwipeActionsConfigurationForRowAt indexPath: IndexPath) -> UISwipeActionsConfiguration? {
|
||||
|
||||
169
main/Requests/Analytics/VCCoOccurrence.swift
Normal file
169
main/Requests/Analytics/VCCoOccurrence.swift
Normal file
@@ -0,0 +1,169 @@
|
||||
import UIKit
|
||||
|
||||
class VCCoOccurrence: UIViewController, UITableViewDataSource {
|
||||
var fqdn: String!
|
||||
private var dataSource: [ContextAnalysisResult] = []
|
||||
|
||||
@IBOutlet private var tableView: UITableView!
|
||||
@IBOutlet private var timeSegment: UISegmentedControl!
|
||||
private let availableTimes = [0, 5, 15, 30]
|
||||
private var selectedTime = -1 {
|
||||
didSet { logTimeDelta = log(CGFloat(max(2, selectedTime+1))) }
|
||||
}
|
||||
private var logTimeDelta: CGFloat = 1
|
||||
private var logMaxCount: CGFloat = 1
|
||||
|
||||
override func viewDidLoad() {
|
||||
super.viewDidLoad()
|
||||
selectedTime = Pref.ContextAnalyis.CoOccurrenceTime ?? 5 // calls `didSet` and `logTimeDelta`
|
||||
timeSegment.removeAllSegments() // clear IB values
|
||||
for (i, time) in availableTimes.enumerated() {
|
||||
timeSegment.insertSegment(withTitle: TimeFormat(.abbreviated).from(seconds: time), at: i, animated: false)
|
||||
if time == selectedTime {
|
||||
timeSegment.selectedSegmentIndex = i
|
||||
}
|
||||
}
|
||||
reloadDataSource()
|
||||
}
|
||||
|
||||
func reloadDataSource() {
|
||||
dataSource = [("Loading …", 0, 0, 0)]
|
||||
logMaxCount = 1
|
||||
tableView.reloadData()
|
||||
let domain = fqdn!
|
||||
let time = Timestamp(selectedTime)
|
||||
DispatchQueue.global().async { [weak self] in
|
||||
let temp: [ContextAnalysisResult]
|
||||
let total: Int32
|
||||
if let db = AppDB,
|
||||
let times = db.dnsLogsUniqTs(domain), times.count > 0,
|
||||
let result = db.contextAnalysis(coOccurrence: times, plusMinus: time, exclude: domain),
|
||||
result.count > 0
|
||||
{
|
||||
temp = result
|
||||
var sum: Int32 = 0
|
||||
for x in result { sum += x.count }
|
||||
total = sum // if statement guarantees >= 1
|
||||
} else {
|
||||
temp = []
|
||||
total = 1
|
||||
}
|
||||
DispatchQueue.main.sync { [weak self] in
|
||||
self?.dataSource = temp
|
||||
self?.logMaxCount = log(CGFloat(total + 1))
|
||||
self?.tableView.reloadData()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@IBAction func didChangeTime(_ sender: UISegmentedControl) {
|
||||
selectedTime = availableTimes[sender.selectedSegmentIndex]
|
||||
Pref.ContextAnalyis.CoOccurrenceTime = selectedTime
|
||||
reloadDataSource()
|
||||
}
|
||||
|
||||
@IBAction func didClose(_ sender: UIBarButtonItem) {
|
||||
dismiss(animated: true)
|
||||
}
|
||||
|
||||
|
||||
// MARK: - Table View Data Source
|
||||
|
||||
func tableView(_ _: UITableView, numberOfRowsInSection _: Int) -> Int {
|
||||
dataSource.count
|
||||
}
|
||||
|
||||
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
|
||||
let cell = tableView.dequeueReusableCell(withIdentifier: "CoOccurrenceCell") as! CoOccurrenceCell
|
||||
let src = dataSource[indexPath.row]
|
||||
cell.title.text = src.domain
|
||||
cell.rank.text = "\(indexPath.row + 1)."
|
||||
cell.count.text = "\(src.count)"
|
||||
cell.avgdiff.text = String(format: "%.2fs", src.avg)
|
||||
|
||||
// log percentage of total co-occurrence count + 1 (min: log(2))
|
||||
cell.countMeter.percent = (log(CGFloat(src.count + 1)) / logMaxCount)
|
||||
// log percentage of selected time window (0s/5s/15s/30s) + 1 (min: log(2))
|
||||
cell.avgdiffMeter.percent = 1 - (log(CGFloat(src.avg + 1)) / logTimeDelta)
|
||||
return cell
|
||||
}
|
||||
}
|
||||
|
||||
class CoOccurrenceCell: UITableViewCell {
|
||||
@IBOutlet var title: UILabel!
|
||||
@IBOutlet var rank: TagLabel!
|
||||
@IBOutlet var count: TagLabel!
|
||||
@IBOutlet var avgdiff: TagLabel!
|
||||
@IBOutlet var countMeter: MeterBar!
|
||||
@IBOutlet var avgdiffMeter: MeterBar!
|
||||
}
|
||||
|
||||
|
||||
// MARK: - Tutorial Screen
|
||||
|
||||
extension VCCoOccurrence {
|
||||
|
||||
@IBAction func showInfoScreen() {
|
||||
let sampleCell: UIImage = {
|
||||
let cell = tableView.dequeueReusableCell(withIdentifier: "CoOccurrenceCell") as! CoOccurrenceCell
|
||||
cell.title.text = "example.org"
|
||||
cell.rank.text = "9."
|
||||
cell.count.text = "14"
|
||||
cell.avgdiff.text = String(format: "%.2fs", 0.71)
|
||||
cell.countMeter.percent = 0.35
|
||||
cell.avgdiffMeter.percent = 0.95
|
||||
|
||||
// Bug: Sometimes dequeue will return a "broken" hidden cell.
|
||||
// It can't be set visible and thus can't render an image.
|
||||
// Funnily `cell.contentView` can rendered.
|
||||
let theView = cell.isHidden ? cell.contentView : cell
|
||||
|
||||
// resize view to fit into tutorial sheet
|
||||
let minWidth = TutorialSheet.verticalWidth - 10 //-> 2 * textContainer.lineFragmentPadding
|
||||
theView.frame.size = theView.systemLayoutSizeFitting(UIView.layoutFittingCompressedSize)
|
||||
theView.frame.size.width = min(theView.frame.size.width, minWidth)
|
||||
// set width in two steps because first call may change layoutMargins
|
||||
theView.frame.size.width += theView.layoutMargins.left + theView.layoutMargins.right
|
||||
// FIXME: In case `hidden == false`, backgroundColor will be black in Dark mode.
|
||||
theView.backgroundColor = tableView.backgroundColor
|
||||
return theView.asImage(insets: theView.layoutMargins)
|
||||
}()
|
||||
|
||||
let x = TutorialSheet()
|
||||
x.addSheet().addArrangedSubview(QuickUI.text(attributed: NSMutableAttributedString()
|
||||
.h3("Co-Occurrence")
|
||||
.normal(" allows you to find requests that happen often at the same time as the selected domain. " +
|
||||
"Hence it will give you a hint what Apps might be involved in the activity." +
|
||||
"\n\nHow do you interpret these results? Lets look at an example:\n\n")
|
||||
.centered(.image(sampleCell))
|
||||
.normal("\n\nThe domain ").bold("example.org").normal(" had ").bold("14").normal(" requests with an ").italic("average time divergence").normal(" of ").bold("0.71 seconds").normal(". " +
|
||||
"That is, these 14 domain calls happend, on average, less then a second before or after the original request of the selected domain." +
|
||||
"\n\nClose temporal proximity and high occurrence counts are both indicators for domain correlation. " +
|
||||
"Results are sorted by a ranking index (").bold("9.").normal(") which strikes a balance between the two. " +
|
||||
"Preferring entries with higher counts as well as low time divergence.")
|
||||
.italic("\n\nTip: ").normal("As a visual guide you can look for the colored bar beside each value. " +
|
||||
"The larger the bar, the greater the correlation.")
|
||||
))
|
||||
|
||||
x.present(in: self)
|
||||
}
|
||||
}
|
||||
|
||||
fileprivate extension UIView {
|
||||
func asImage(insets: UIEdgeInsets = .zero) -> UIImage {
|
||||
if #available(iOS 10.0, *) {
|
||||
let renderer = UIGraphicsImageRenderer(bounds: bounds.inset(by: insets))
|
||||
return renderer.image { rendererContext in
|
||||
layer.render(in: rendererContext.cgContext)
|
||||
}
|
||||
} else {
|
||||
UIGraphicsBeginImageContext(bounds.inset(by: insets).size)
|
||||
let ctx = UIGraphicsGetCurrentContext()!
|
||||
ctx.translateBy(x: -insets.left, y: -insets.top)
|
||||
layer.render(in:ctx)
|
||||
let image = UIGraphicsGetImageFromCurrentImageContext()
|
||||
UIGraphicsEndImageContext()
|
||||
return UIImage(cgImage: image!.cgImage!)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -14,6 +14,11 @@ class TVCDomains: UITableViewController, UISearchBarDelegate, GroupedDomainDataS
|
||||
source.delegate = self // init lazy var, ready for tableView data source
|
||||
}
|
||||
|
||||
override func viewDidAppear(_ animated: Bool) {
|
||||
// iOS 11+ fix: fuse after `didAppear` to hide on app launch
|
||||
source.search.fuseWith(tableViewController: self)
|
||||
}
|
||||
|
||||
override func prepare(for segue: UIStoryboardSegue, sender: Any?) {
|
||||
if let index = tableView.indexPathForSelectedRow?.row {
|
||||
(segue.destination as? TVCHosts)?.parentDomain = source[index].domain
|
||||
@@ -21,13 +26,6 @@ class TVCDomains: UITableViewController, UISearchBarDelegate, GroupedDomainDataS
|
||||
}
|
||||
|
||||
|
||||
// MARK: - Search
|
||||
|
||||
@IBAction private func searchButtonTapped(_ sender: UIBarButtonItem) {
|
||||
source.toggleSearch()
|
||||
}
|
||||
|
||||
|
||||
// MARK: - Filter
|
||||
|
||||
@IBAction private func filterButtonTapped(_ sender: UIBarButtonItem) {
|
||||
|
||||
@@ -1,7 +1,9 @@
|
||||
import UIKit
|
||||
|
||||
class TVCHostDetails: UITableViewController, SyncUpdateDelegate {
|
||||
class TVCHostDetails: UITableViewController, SyncUpdateDelegate, UITabBarDelegate {
|
||||
|
||||
@IBOutlet private var actionsBar: UITabBar!
|
||||
|
||||
public var fullDomain: String!
|
||||
private var dataSource: [GroupedTsOccurrence] = []
|
||||
// TODO: respect date reverse sort order
|
||||
@@ -12,7 +14,9 @@ class TVCHostDetails: UITableViewController, SyncUpdateDelegate {
|
||||
sync.addObserver(self) // calls `syncUpdate(reset:)`
|
||||
if #available(iOS 10.0, *) {
|
||||
sync.allowPullToRefresh(onTVC: self, forObserver: self)
|
||||
actionsBar.unselectedItemTintColor = .systemBlue
|
||||
}
|
||||
UIDevice.orientationDidChangeNotification.observe(call: #selector(didChangeOrientation), on: self)
|
||||
}
|
||||
|
||||
// MARK: - Table View Data Source
|
||||
@@ -29,6 +33,30 @@ class TVCHostDetails: UITableViewController, SyncUpdateDelegate {
|
||||
}
|
||||
}
|
||||
|
||||
// #########################
|
||||
// #
|
||||
// # MARK: - Tab Bar
|
||||
// #
|
||||
// #########################
|
||||
|
||||
extension TVCHostDetails {
|
||||
|
||||
@objc private func didChangeOrientation(_ sender: Notification) {
|
||||
tableView.sizeHeaderToFit() // otherwise TabBar won't compress
|
||||
}
|
||||
|
||||
func tabBar(_ tabBar: UITabBar, didSelect item: UITabBarItem) {
|
||||
tabBar.selectedItem = nil
|
||||
performSegue(withIdentifier: "segueAnalysisCoOccurrence", sender: nil)
|
||||
}
|
||||
|
||||
override func prepare(for segue: UIStoryboardSegue, sender: Any?) {
|
||||
if segue.identifier == "segueAnalysisCoOccurrence" {
|
||||
(segue.destination as? VCCoOccurrence)?.fqdn = fullDomain
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ################################
|
||||
// #
|
||||
// # MARK: - Partial Update
|
||||
|
||||
@@ -12,6 +12,7 @@ class TVCHosts: UITableViewController, GroupedDomainDataSourceDelegate {
|
||||
super.viewDidLoad()
|
||||
isSpecial = (parentDomain.first == "#") // aka: "# IP address"
|
||||
source.delegate = self // init lazy var, ready for tableView data source
|
||||
source.search.fuseWith(tableViewController: self)
|
||||
}
|
||||
|
||||
override func prepare(for segue: UIStoryboardSegue, sender: Any?) {
|
||||
@@ -20,11 +21,6 @@ class TVCHosts: UITableViewController, GroupedDomainDataSourceDelegate {
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Search
|
||||
|
||||
@IBAction private func searchButtonTapped(_ sender: UIBarButtonItem) {
|
||||
source.toggleSearch()
|
||||
}
|
||||
|
||||
// MARK: - Table View Data Source
|
||||
|
||||
|
||||
@@ -118,34 +118,3 @@ class VCDateFilter: UIViewController, UIGestureRecognizerDelegate {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// 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()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -64,7 +64,7 @@ class TVCFilter: UITableViewController, EditActionsRemove {
|
||||
// MARK: - Editing
|
||||
|
||||
override func tableView(_ tableView: UITableView, editActionsForRowAt indexPath: IndexPath) -> [UITableViewRowAction]? {
|
||||
getRowActionsIOS9(indexPath)
|
||||
getRowActionsIOS9(indexPath, tableView)
|
||||
}
|
||||
@available(iOS 11.0, *)
|
||||
override func tableView(_ tableView: UITableView, trailingSwipeActionsConfigurationForRowAt indexPath: IndexPath) -> UISwipeActionsConfiguration? {
|
||||
|
||||
Reference in New Issue
Block a user