Improve recording contribution view. Replace TextView with interactive TableView.
This commit is contained in:
@@ -85,6 +85,7 @@
|
||||
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 */; };
|
||||
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 */; };
|
||||
@@ -180,7 +181,7 @@
|
||||
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 /* VCShareRecording.swift in Sources */ = {isa = PBXBuildFile; fileRef = 54CFE86724E3F401001687DD /* VCShareRecording.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 */; };
|
||||
@@ -290,6 +291,7 @@
|
||||
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>"; };
|
||||
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>"; };
|
||||
@@ -388,7 +390,7 @@
|
||||
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 /* VCShareRecording.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VCShareRecording.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>"; };
|
||||
@@ -455,7 +457,8 @@
|
||||
540E677F242D2CF100871BBE /* VCRecordings.swift */,
|
||||
540E67832433FAFE00871BBE /* TVCPreviousRecords.swift */,
|
||||
5458EBBF243A3F2200CFEB15 /* TVCRecordingDetails.swift */,
|
||||
54CFE86724E3F401001687DD /* VCShareRecording.swift */,
|
||||
54CFE86724E3F401001687DD /* TVCShareRecording.swift */,
|
||||
549A96D52501198400C565FA /* VCEditText.swift */,
|
||||
540E67812433483D00871BBE /* VCEditRecording.swift */,
|
||||
549D6ED424D5BFDB0032E498 /* TVCAppSearch.swift */,
|
||||
54B345B12422E029004C53CC /* App Icons */,
|
||||
@@ -1071,7 +1074,7 @@
|
||||
54448A30248647D900771C96 /* Time.swift in Sources */,
|
||||
54953E6123E0D69A0054345C /* TVCHosts.swift in Sources */,
|
||||
549D6ED524D5BFDB0032E498 /* TVCAppSearch.swift in Sources */,
|
||||
54CFE86824E3F401001687DD /* VCShareRecording.swift in Sources */,
|
||||
54CFE86824E3F401001687DD /* TVCShareRecording.swift in Sources */,
|
||||
54751E512423955100168273 /* URL.swift in Sources */,
|
||||
54953E5F23DEBE840054345C /* TVCDomains.swift in Sources */,
|
||||
54C056DB23E9E36E00214A3F /* AppStoreSearch.swift in Sources */,
|
||||
@@ -1084,6 +1087,7 @@
|
||||
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 */,
|
||||
|
||||
@@ -28,6 +28,18 @@ enum RecordingsDB {
|
||||
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)
|
||||
|
||||
@@ -295,7 +295,7 @@
|
||||
<rightBarButtonItems>
|
||||
<barButtonItem systemItem="action" id="UkE-Wi-JjW">
|
||||
<connections>
|
||||
<segue destination="P0a-ZP-uEV" kind="modal" identifier="openContributeSegue" id="s3J-zL-4zK"/>
|
||||
<segue destination="1e2-YP-lHV" kind="push" id="JL4-tN-vZg"/>
|
||||
</connections>
|
||||
</barButtonItem>
|
||||
<barButtonItem image="line-expand" id="xLc-O7-KVB">
|
||||
@@ -314,66 +314,179 @@
|
||||
</objects>
|
||||
<point key="canvasLocation" x="1400" y="0.0"/>
|
||||
</scene>
|
||||
<!--Share Recording-->
|
||||
<scene sceneID="9Zh-he-N6f">
|
||||
<!--Contribute-->
|
||||
<scene sceneID="np1-8y-nci">
|
||||
<objects>
|
||||
<viewController id="P0a-ZP-uEV" customClass="VCShareRecording" customModule="AppCheck" customModuleProvider="target" sceneMemberID="viewController">
|
||||
<view key="view" contentMode="scaleToFill" id="kVd-y1-MmT">
|
||||
<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>
|
||||
<navigationBar contentMode="scaleToFill" translatesAutoresizingMaskIntoConstraints="NO" id="dN0-a9-t4G">
|
||||
<rect key="frame" x="0.0" y="0.0" width="320" height="44"/>
|
||||
<items>
|
||||
<navigationItem title="Contribute" id="a43-mu-gQ2">
|
||||
<barButtonItem key="leftBarButtonItem" systemItem="cancel" id="6J3-R1-kRM">
|
||||
<connections>
|
||||
<action selector="closeView" destination="P0a-ZP-uEV" id="cno-MA-xos"/>
|
||||
</connections>
|
||||
</barButtonItem>
|
||||
<barButtonItem key="rightBarButtonItem" title="Send" style="done" id="PWY-06-ykI">
|
||||
<connections>
|
||||
<action selector="shareRecording:" destination="P0a-ZP-uEV" id="vCR-9d-91D"/>
|
||||
</connections>
|
||||
</barButtonItem>
|
||||
</navigationItem>
|
||||
</items>
|
||||
</navigationBar>
|
||||
<textView clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="scaleToFill" editable="NO" textAlignment="natural" selectable="NO" translatesAutoresizingMaskIntoConstraints="NO" id="fFm-v5-DGy">
|
||||
<rect key="frame" x="0.0" y="45" width="320" height="324.5"/>
|
||||
<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"/>
|
||||
<string key="text">Lorem ipsum dolor sit er elit lamet, consectetaur cillium adipisicing pecu, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum. Nam liber te conscient to factor tum poen legum odioque civiuda.</string>
|
||||
<color key="textColor" systemColor="labelColor" cocoaTouchSystemColor="darkTextColor"/>
|
||||
<fontDescription key="fontDescription" style="UICTFontTextStyleBody"/>
|
||||
<fontDescription key="fontDescription" style="UICTFontTextStyleSubhead"/>
|
||||
<textInputTraits key="textInputTraits" autocapitalizationType="sentences"/>
|
||||
</textView>
|
||||
<activityIndicatorView hidden="YES" opaque="NO" contentMode="scaleToFill" horizontalHuggingPriority="750" verticalHuggingPriority="750" hidesWhenStopped="YES" style="whiteLarge" translatesAutoresizingMaskIntoConstraints="NO" id="eWC-xB-CJe">
|
||||
<rect key="frame" x="275" y="3.5" width="37" height="37"/>
|
||||
</activityIndicatorView>
|
||||
</subviews>
|
||||
<color key="backgroundColor" systemColor="systemBackgroundColor" cocoaTouchSystemColor="whiteColor"/>
|
||||
<constraints>
|
||||
<constraint firstItem="fFm-v5-DGy" firstAttribute="leading" secondItem="lie-9P-DtH" secondAttribute="leading" id="2Ae-cc-xkI"/>
|
||||
<constraint firstItem="fFm-v5-DGy" firstAttribute="top" secondItem="dN0-a9-t4G" secondAttribute="bottom" constant="1" id="2QI-wU-51t"/>
|
||||
<constraint firstItem="dN0-a9-t4G" firstAttribute="top" secondItem="lie-9P-DtH" secondAttribute="top" id="8eL-tr-3IN"/>
|
||||
<constraint firstItem="dN0-a9-t4G" firstAttribute="leading" secondItem="lie-9P-DtH" secondAttribute="leading" id="BWF-aS-ah3"/>
|
||||
<constraint firstItem="lie-9P-DtH" firstAttribute="trailing" secondItem="fFm-v5-DGy" secondAttribute="trailing" id="GRg-BF-rYE"/>
|
||||
<constraint firstItem="lie-9P-DtH" firstAttribute="bottom" secondItem="fFm-v5-DGy" secondAttribute="bottom" id="IE3-o5-suZ"/>
|
||||
<constraint firstItem="eWC-xB-CJe" firstAttribute="trailing" secondItem="dN0-a9-t4G" secondAttribute="trailingMargin" id="NgN-7W-Ecm"/>
|
||||
<constraint firstItem="lie-9P-DtH" firstAttribute="trailing" secondItem="dN0-a9-t4G" secondAttribute="trailing" id="UuJ-xj-xBK"/>
|
||||
<constraint firstItem="eWC-xB-CJe" firstAttribute="centerY" secondItem="dN0-a9-t4G" secondAttribute="centerY" id="jvl-OA-Vdr"/>
|
||||
<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="lie-9P-DtH"/>
|
||||
<viewLayoutGuide key="safeArea" id="5JR-8n-qUg"/>
|
||||
</view>
|
||||
<extendedEdge key="edgesForExtendedLayout"/>
|
||||
<navigationItem key="navigationItem" title="Edit Notes" id="Ex8-YV-Ebv"/>
|
||||
<connections>
|
||||
<outlet property="sendActivity" destination="eWC-xB-CJe" id="rx3-Jz-ppT"/>
|
||||
<outlet property="sendButton" destination="PWY-06-ykI" id="eEf-hW-VIs"/>
|
||||
<outlet property="text" destination="fFm-v5-DGy" id="fwm-RE-gHu"/>
|
||||
<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="AhG-fQ-OzZ" userLabel="First Responder" customClass="UIResponder" sceneMemberID="firstResponder"/>
|
||||
<placeholder placeholderIdentifier="IBFirstResponder" id="l5b-JD-MQb" userLabel="First Responder" customClass="UIResponder" sceneMemberID="firstResponder"/>
|
||||
</objects>
|
||||
<point key="canvasLocation" x="2100" y="0.0"/>
|
||||
<point key="canvasLocation" x="2800" y="0.0"/>
|
||||
</scene>
|
||||
<!--Edit Recording-->
|
||||
<scene sceneID="pqx-CU-4AP">
|
||||
@@ -432,16 +545,16 @@
|
||||
<color key="backgroundColor" systemColor="systemBackgroundColor" cocoaTouchSystemColor="whiteColor"/>
|
||||
<gestureRecognizers/>
|
||||
<constraints>
|
||||
<constraint firstAttribute="height" constant="40" id="5ew-Cq-VKh"/>
|
||||
<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" constant="2" id="Tpw-nU-HHb"/>
|
||||
<constraint firstItem="l8O-Kw-uc8" firstAttribute="top" secondItem="Et0-8d-CId" secondAttribute="bottom" constant="1" id="Wpc-8H-6b8"/>
|
||||
<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" constant="2" id="nXu-FP-JVX"/>
|
||||
<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>
|
||||
@@ -466,7 +579,7 @@
|
||||
3. Line
|
||||
4. Line</string>
|
||||
<color key="textColor" systemColor="labelColor" cocoaTouchSystemColor="darkTextColor"/>
|
||||
<fontDescription key="fontDescription" style="UICTFontTextStyleFootnote"/>
|
||||
<fontDescription key="fontDescription" style="UICTFontTextStyleSubhead"/>
|
||||
<textInputTraits key="textInputTraits" autocapitalizationType="sentences"/>
|
||||
<connections>
|
||||
<outlet property="delegate" destination="VRk-wv-rhk" id="vej-jI-13V"/>
|
||||
|
||||
@@ -64,13 +64,6 @@ class TVCPreviousRecords: UITableViewController, EditActionsRemove {
|
||||
dataSource.count
|
||||
}
|
||||
|
||||
// override func tableView(_ tableView: UITableView, viewForHeaderInSection section: Int) -> UIView? {
|
||||
// let lbl = QuickUI.label("Previous Recordings", align: .center)
|
||||
// lbl.font = lbl.font.bold()
|
||||
// lbl.backgroundColor = .sysBackground
|
||||
// return lbl
|
||||
// }
|
||||
|
||||
override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
|
||||
let cell = tableView.dequeueReusableCell(withIdentifier: "PreviousRecordCell")!
|
||||
let x = dataSource[indexPath.row]
|
||||
@@ -80,6 +73,10 @@ class TVCPreviousRecords: UITableViewController, EditActionsRemove {
|
||||
cell.imageView?.image = x.isLongTerm ? nil : BundleIcon.image(x.appId)
|
||||
cell.imageView?.layer.cornerRadius = 6.75
|
||||
cell.imageView?.layer.masksToBounds = true
|
||||
if #available(iOS 11, *) {} else {
|
||||
cell.textLabel?.numberOfLines = 1
|
||||
cell.detailTextLabel?.numberOfLines = 1
|
||||
}
|
||||
return cell
|
||||
}
|
||||
|
||||
|
||||
@@ -45,7 +45,7 @@ class TVCRecordingDetails: UITableViewController, EditActionsRemove {
|
||||
}
|
||||
|
||||
override func prepare(for segue: UIStoryboardSegue, sender: Any?) {
|
||||
if let tgt = segue.destination as? VCShareRecording {
|
||||
if let tgt = segue.destination as? TVCShareRecording {
|
||||
tgt.record = self.record
|
||||
}
|
||||
}
|
||||
|
||||
269
main/Recordings/TVCShareRecording.swift
Normal file
269
main/Recordings/TVCShareRecording.swift
Normal file
@@ -0,0 +1,269 @@
|
||||
import UIKit
|
||||
|
||||
class TVCShareRecording : UITableViewController, UITextViewDelegate, VCEditTextDelegate {
|
||||
|
||||
@IBOutlet private var sendButton: UIBarButtonItem!
|
||||
|
||||
// vars
|
||||
var record: Recording!
|
||||
private var shareNotes: Bool = false // opt-in
|
||||
private lazy var hasNotes: Bool = (self.record.notes != nil)
|
||||
private lazy var editedNotes: String = self.record.notes ?? ""
|
||||
private lazy var weekInYear: String = {
|
||||
let comp = Calendar.current.dateComponents(
|
||||
[.weekOfYear, .yearForWeekOfYear], from: Date(self.record.start))
|
||||
return "\(comp.yearForWeekOfYear ?? 0).\(comp.weekOfYear ?? 0)"
|
||||
}()
|
||||
|
||||
// Data source
|
||||
private lazy var dataSource: [String : [Timestamp]] = RecordingsDB.detailCluster(self.record)
|
||||
|
||||
private lazy var dataSourceKeyValue: [(key: String, value: String)] = [
|
||||
("Date", self.weekInYear),
|
||||
("Rec-Length", "\(self.record.duration) sec"),
|
||||
("App-Bundle", self.record.appId ?? " – "),
|
||||
("App-Name", self.record.title ?? " – "),
|
||||
("Notes", " – ") // see delegate below
|
||||
]
|
||||
|
||||
private lazy var dataSourceLogs: [(domain: String, occurrences: String, enabled: Bool)] = self.dataSource.map {
|
||||
($0.key, $0.value.map{"\($0)"}.joined(separator: ", "), true)
|
||||
}.sorted(by: { $0.domain < $1.domain })
|
||||
|
||||
|
||||
override func viewDidLoad() {
|
||||
super.viewDidLoad()
|
||||
|
||||
if record.isShared {
|
||||
sendButton.tintColor = .gray
|
||||
}
|
||||
}
|
||||
|
||||
private func reloadNotes() {
|
||||
tableView.reloadRows(at: [
|
||||
IndexPath(row: 0, section: 1), // edit field
|
||||
IndexPath(row: 4, section: 2) // display field
|
||||
], with: .automatic)
|
||||
}
|
||||
|
||||
|
||||
// MARK: - User Interaction
|
||||
|
||||
@IBAction private func didChangeNotesCheckbox(_ sender: UISwitch) {
|
||||
shareNotes = sender.isOn
|
||||
reloadNotes()
|
||||
}
|
||||
|
||||
override func prepare(for segue: UIStoryboardSegue, sender: Any?) {
|
||||
if let dest = segue.destination as? VCEditText {
|
||||
dest.text = editedNotes
|
||||
dest.delegate = self
|
||||
}
|
||||
}
|
||||
|
||||
func editText(didFinish text: String) {
|
||||
editedNotes = text
|
||||
reloadNotes()
|
||||
}
|
||||
|
||||
@IBAction private func shareRecording(_ sender: UIBarButtonItem) {
|
||||
guard !record.isShared else {
|
||||
showAlertAlreadyShared()
|
||||
return
|
||||
}
|
||||
navigationItem.rightBarButtonItem = {
|
||||
let v = UIView()
|
||||
let activity = UIActivityIndicatorView()
|
||||
v.addSubview(activity)
|
||||
activity.anchor([.centerX, .centerY], to: v)
|
||||
activity.startAnimating()
|
||||
v.widthAnchor =&= 2 * activity.widthAnchor
|
||||
return UIBarButtonItem(customView: v)
|
||||
}()
|
||||
|
||||
postToServer() { [weak self] in
|
||||
self?.navigationItem.rightBarButtonItem = self?.sendButton
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// MARK: - Table Data Source
|
||||
|
||||
override func numberOfSections(in _: UITableView) -> Int { 4 }
|
||||
|
||||
override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
|
||||
switch section {
|
||||
case 0: return 1 // description
|
||||
case 1: return hasNotes ? 2 : 0 // notes + checkbox
|
||||
case 2: return dataSourceKeyValue.count
|
||||
case 3: return dataSourceLogs.count
|
||||
default: preconditionFailure()
|
||||
}
|
||||
}
|
||||
|
||||
override func tableView(_ tableView: UITableView, titleForHeaderInSection section: Int) -> String? {
|
||||
switch section {
|
||||
case 0: return "Review before sending"
|
||||
case 1: return hasNotes ? "Notes" : nil
|
||||
case 2: return "Send to server"
|
||||
case 3: return "Logs"
|
||||
default: return nil
|
||||
}
|
||||
}
|
||||
|
||||
override func tableView(_ tableView: UITableView, titleForFooterInSection section: Int) -> String? {
|
||||
switch section {
|
||||
case 0: return "You can tap on a domain cell to exclude it from the upload."
|
||||
case 2: return "Below you see the domain names, followed by a list of relative time offsets (in seconds)."
|
||||
default: return nil
|
||||
}
|
||||
}
|
||||
|
||||
override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
|
||||
let cell: UITableViewCell
|
||||
switch indexPath.section {
|
||||
case 0:
|
||||
cell = tableView.dequeueReusableCell(withIdentifier: "shareTextCell")!
|
||||
cell.textLabel?.text = """
|
||||
You are about to upload the following information to our servers.
|
||||
The data is anonymized in regards to device identifiers and time of recording. However, it is not anonymous to the domains requested during the recording.
|
||||
"""
|
||||
case 1:
|
||||
switch indexPath.row {
|
||||
case 0:
|
||||
cell = tableView.dequeueReusableCell(withIdentifier: "shareOpenTextCell")!
|
||||
cell.textLabel?.text = editedNotes
|
||||
cell.textLabel?.textColor = shareNotes ? nil : .gray
|
||||
case 1:
|
||||
cell = tableView.dequeueReusableCell(withIdentifier: "shareCheckboxCell")!
|
||||
cell.textLabel?.text = "Upload your notes?"
|
||||
let accessory = cell.accessoryView as! UISwitch
|
||||
accessory.isOn = shareNotes
|
||||
default: preconditionFailure()
|
||||
}
|
||||
case 2:
|
||||
cell = tableView.dequeueReusableCell(withIdentifier: "shareKeyValueCell")!
|
||||
let src = dataSourceKeyValue[indexPath.row]
|
||||
cell.textLabel?.text = src.key
|
||||
let flag = shareNotes && indexPath.row == 4
|
||||
cell.detailTextLabel?.text = flag ? editedNotes : src.value
|
||||
case 3:
|
||||
cell = tableView.dequeueReusableCell(withIdentifier: "shareLogCell")!
|
||||
let src = dataSourceLogs[indexPath.row]
|
||||
let sent = src.enabled
|
||||
cell.textLabel?.text = src.domain
|
||||
cell.detailTextLabel?.text = sent ? src.occurrences : "don't upload"
|
||||
cell.accessoryType = sent ? .checkmark : .none
|
||||
cell.textLabel?.isEnabled = sent
|
||||
cell.detailTextLabel?.isEnabled = sent
|
||||
default:
|
||||
preconditionFailure()
|
||||
}
|
||||
if #available(iOS 11, *) {} else {
|
||||
cell.detailTextLabel?.numberOfLines = 1
|
||||
}
|
||||
return cell
|
||||
}
|
||||
|
||||
override func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
|
||||
guard indexPath.section == 3 else { return }
|
||||
dataSourceLogs[indexPath.row].enabled = !dataSourceLogs[indexPath.row].enabled
|
||||
tableView.deselectRow(at: indexPath, animated: true)
|
||||
tableView.reloadRows(at: [indexPath], with: .automatic)
|
||||
}
|
||||
|
||||
|
||||
// MARK: - Upload
|
||||
|
||||
private func postToServer(_ onceLoaded: @escaping () -> Void) {
|
||||
// prepare json
|
||||
let json = try? JSONSerialization.data(withJSONObject: [
|
||||
"v" : 1,
|
||||
"date" : weekInYear,
|
||||
"duration" : record.duration,
|
||||
"app-bundle" : record.appId ?? "",
|
||||
"app-name" : record.title ?? "",
|
||||
"notes" : shareNotes ? editedNotes : "",
|
||||
"logs" : []
|
||||
])
|
||||
|
||||
// prepare post request
|
||||
let url = URL(string: "https://appchk.de/api/v1/contribute/")!
|
||||
var request = URLRequest(url: url)
|
||||
request.httpMethod = "POST"
|
||||
request.httpBody = json
|
||||
var rec = record! // store temporarily so self can be released
|
||||
|
||||
// send to server
|
||||
URLSession.shared.dataTask(with: request) { data, response, error in
|
||||
DispatchQueue.main.async { [weak self] in
|
||||
onceLoaded()
|
||||
guard error == nil, let data = data,
|
||||
let response = response as? HTTPURLResponse else {
|
||||
self?.banner(.fail, "\(error?.localizedDescription ?? "Unkown error occurred")")
|
||||
return
|
||||
}
|
||||
guard let json = (try? JSONSerialization.jsonObject(with: data, options: [])) as? [String: Any],
|
||||
let v = json["v"] as? Int, v > 0 else {
|
||||
QLog.Warning("Couldn't contribute: Not JSON or no version key")
|
||||
self?.banner(.fail, "Server couldn't parse request.\nTry again later.")
|
||||
return
|
||||
}
|
||||
let status = json["status"] as? String ?? "unkown reason"
|
||||
guard status == "ok", (200 ... 299) ~= response.statusCode else {
|
||||
QLog.Warning("Couldn't contribute: \(status)")
|
||||
self?.banner(.fail, "Error: \(status)")
|
||||
return
|
||||
}
|
||||
// update db, mark record as shared
|
||||
rec.uploadkey = json["key"] as? String ?? "_"
|
||||
self?.record = rec // in case view is still open
|
||||
RecordingsDB.update(rec) // rec cause self may not be available
|
||||
self?.sendButton.tintColor = .gray
|
||||
// notify user about results
|
||||
if v == 1, let urlStr = json["url"] as? String {
|
||||
let nextUpdateIn = json["when"] as? Int
|
||||
self?.showAlertAvailableSoon(urlStr, when: nextUpdateIn)
|
||||
}
|
||||
self?.banner(.ok, "Thank you for your contribution.")
|
||||
}
|
||||
}.resume()
|
||||
}
|
||||
|
||||
|
||||
// MARK: - Alerts & Banner
|
||||
|
||||
private func banner(_ style: NotificationBanner.Style, _ msg: String) {
|
||||
NotificationBanner(msg, style: style).present(in: self)
|
||||
}
|
||||
|
||||
private func showAlertAvailableSoon(_ urlStr: String, when: Int?) {
|
||||
var msg = "Your contribution is being processed and will be available "
|
||||
if let when = when {
|
||||
if when < 61 {
|
||||
msg += "in approx. \(when) sec. "
|
||||
} else {
|
||||
let fmt = TimeFormat.from(Timestamp(when))
|
||||
msg += "in \(fmt) min. "
|
||||
}
|
||||
} else {
|
||||
msg += "shortly. "
|
||||
}
|
||||
msg += "Open results webpage now?"
|
||||
AskAlert(title: "Thank you", text: msg, buttonText: "Show results", cancelButton: "Not now") { _ in
|
||||
if let url = URL(string: urlStr) {
|
||||
UIApplication.shared.openURL(url)
|
||||
}
|
||||
}.presentIn(self)
|
||||
}
|
||||
|
||||
private func showAlertAlreadyShared() {
|
||||
let alert = Alert(title: nil, text: "You already shared this recording.")
|
||||
if let bid = record.appId, bid.isValidBundleId() {
|
||||
alert.addAction(UIAlertAction.init(title: "Open results", style: .default, handler: { _ in
|
||||
URL(string: "https://appchk.de/redirect.html?id=\(bid)")?.open()
|
||||
}))
|
||||
}
|
||||
alert.presentIn(self)
|
||||
}
|
||||
}
|
||||
39
main/Recordings/VCEditText.swift
Normal file
39
main/Recordings/VCEditText.swift
Normal file
@@ -0,0 +1,39 @@
|
||||
import UIKit
|
||||
|
||||
protocol VCEditTextDelegate {
|
||||
func editText(didFinish text: String)
|
||||
}
|
||||
|
||||
class VCEditText: UIViewController, UITextViewDelegate {
|
||||
|
||||
var text: String!
|
||||
var delegate: VCEditTextDelegate!
|
||||
|
||||
@IBOutlet private var textView: UITextView!
|
||||
@IBOutlet private var textBottom: NSLayoutConstraint!
|
||||
|
||||
override func viewDidLoad() {
|
||||
super.viewDidLoad()
|
||||
textView.text = text
|
||||
textView.becomeFirstResponder()
|
||||
|
||||
UIResponder.keyboardWillShowNotification.observe(call: #selector(keyboardWillShow), on: self)
|
||||
UIResponder.keyboardWillHideNotification.observe(call: #selector(keyboardWillHide), on: self)
|
||||
}
|
||||
|
||||
override func viewDidDisappear(_ animated: Bool) {
|
||||
super.viewDidDisappear(animated)
|
||||
delegate.editText(didFinish: textView.text)
|
||||
}
|
||||
|
||||
|
||||
// MARK: - Adapt to Keyboard
|
||||
|
||||
@objc func keyboardWillShow(_ notification: NSNotification) {
|
||||
textBottom.constant = (notification.userInfo?[UIResponder.keyboardFrameEndUserInfoKey] as? NSValue)?.cgRectValue.height ?? 0
|
||||
}
|
||||
|
||||
@objc func keyboardWillHide(_ notification: NSNotification) {
|
||||
textBottom.constant = 0
|
||||
}
|
||||
}
|
||||
@@ -1,159 +0,0 @@
|
||||
import UIKit
|
||||
|
||||
class VCShareRecording : UIViewController {
|
||||
|
||||
var record: Recording!
|
||||
private var jsonData: Data?
|
||||
|
||||
@IBOutlet private var text : UITextView!
|
||||
@IBOutlet private var sendButton: UIBarButtonItem!
|
||||
@IBOutlet private var sendActivity : UIActivityIndicatorView!
|
||||
|
||||
override func viewDidLoad() {
|
||||
super.viewDidLoad()
|
||||
|
||||
if record.isShared {
|
||||
sendButton.tintColor = .gray
|
||||
}
|
||||
|
||||
let start = record.start
|
||||
let comp = Calendar.current.dateComponents([.weekOfYear, .yearForWeekOfYear], from: Date(start))
|
||||
let wkYear = "\(comp.yearForWeekOfYear ?? 0).\(comp.weekOfYear ?? 0)"
|
||||
let lenSec = record.duration
|
||||
|
||||
let res = RecordingsDB.details(record)
|
||||
var cluster: [String : [Timestamp]] = [:]
|
||||
for (dom, ts) in res {
|
||||
if cluster[dom] == nil {
|
||||
cluster[dom] = []
|
||||
}
|
||||
cluster[dom]?.append(ts - start)
|
||||
}
|
||||
let domList = cluster.reduce("") {
|
||||
$0 + "\($1.key) : \($1.value.map{"\($0)"}.joined(separator: ", "))\n"
|
||||
}
|
||||
text.attributedText = NSMutableAttributedString()
|
||||
.h2("Review before sending\n")
|
||||
.normal("\nRead carefully. " +
|
||||
"You are about to upload the following information to our servers. " +
|
||||
"The data is anonymized in regards to device identifiers and time of recording. " +
|
||||
"It is however not anonymous to the domains requested during the recording." +
|
||||
"\n\n" +
|
||||
"If necessary, you can cancel this dialog and return to the recording overview. " +
|
||||
"Use swipe to delete individual domains." +
|
||||
"\n\n")
|
||||
.bold("Send to server:\n")
|
||||
.italic("\nDate: ", .callout).bold(wkYear, .callout)
|
||||
.italic("\nRec-Length: ", .callout).bold("\(lenSec) sec", .callout)
|
||||
.italic("\nApp-Bundle: ", .callout).bold(record.appId ?? "–", .callout)
|
||||
.italic("\nApp-Name: ", .callout).bold(record.title ?? "–", .callout)
|
||||
.italic("\n\n[domain name] : [relative time offsets]\n", .callout)
|
||||
.bold(domList, .callout)
|
||||
|
||||
let json: [String : Any] = [
|
||||
"v" : 1,
|
||||
"date" : wkYear,
|
||||
"duration" : lenSec,
|
||||
"app-bundle" : record.appId ?? "",
|
||||
"app-name" : record.title ?? "",
|
||||
"logs" : cluster
|
||||
]
|
||||
jsonData = try? JSONSerialization.data(withJSONObject: json)
|
||||
}
|
||||
|
||||
@IBAction private func closeView() {
|
||||
dismiss(animated: true)
|
||||
}
|
||||
|
||||
@IBAction private func shareRecording(_ sender: UIBarButtonItem) {
|
||||
guard !record.isShared else {
|
||||
showAlertAlreadyShared()
|
||||
return
|
||||
}
|
||||
sender.isEnabled = false
|
||||
sendActivity.startAnimating()
|
||||
postToServer() { [weak self, weak sender] in
|
||||
self?.sendActivity.stopAnimating()
|
||||
sender?.isEnabled = true
|
||||
}
|
||||
}
|
||||
|
||||
private func postToServer(_ onceLoaded: @escaping () -> Void) {
|
||||
let url = URL(string: "http://127.0.0.1/api/v1/contribute/")!
|
||||
var request = URLRequest(url: url)
|
||||
request.httpMethod = "POST"
|
||||
request.httpBody = jsonData
|
||||
var rec = record! // store temporarily so self can be released
|
||||
|
||||
URLSession.shared.dataTask(with: request) { data, response, error in
|
||||
DispatchQueue.main.async { [weak self] in
|
||||
onceLoaded()
|
||||
guard error == nil, let data = data,
|
||||
let response = response as? HTTPURLResponse else {
|
||||
self?.banner(.fail, "\(error?.localizedDescription ?? "Unkown error occurred")")
|
||||
return
|
||||
}
|
||||
guard let json = (try? JSONSerialization.jsonObject(with: data, options: [])) as? [String: Any],
|
||||
let v = json["v"] as? Int, v > 0 else {
|
||||
QLog.Warning("Couldn't contribute: Not JSON or no version key")
|
||||
self?.banner(.fail, "Server couldn't parse request.\nTry again later.")
|
||||
return
|
||||
}
|
||||
let status = json["status"] as? String ?? "unkown reason"
|
||||
guard status == "ok", (200 ... 299) ~= response.statusCode else {
|
||||
QLog.Warning("Couldn't contribute: \(status)")
|
||||
self?.banner(.fail, "Error: \(status)")
|
||||
return
|
||||
}
|
||||
// update db, mark record as shared
|
||||
rec.uploadkey = json["key"] as? String ?? "_"
|
||||
self?.record = rec // in case view is still open
|
||||
RecordingsDB.update(rec) // rec cause self may not be available
|
||||
self?.sendButton.tintColor = .gray
|
||||
// notify user about results
|
||||
var autoHide = true
|
||||
if v == 1, let urlStr = json["url"] as? String {
|
||||
let nextUpdateIn = json["when"] as? Int
|
||||
self?.showAlertAvailableSoon(urlStr, when: nextUpdateIn)
|
||||
autoHide = false
|
||||
}
|
||||
self?.banner(.ok, "Thank you for your contribution.",
|
||||
autoHide ? { [weak self] in self?.closeView() } : nil)
|
||||
}
|
||||
}.resume()
|
||||
}
|
||||
|
||||
private func banner(_ style: NotificationBanner.Style, _ msg: String, _ closure: (() -> Void)? = nil) {
|
||||
NotificationBanner(msg, style: style).present(in: self, onClose: closure)
|
||||
}
|
||||
|
||||
private func showAlertAvailableSoon(_ urlStr: String, when: Int?) {
|
||||
var msg = "Your contribution is being processed and will be available "
|
||||
if let when = when {
|
||||
if when < 61 {
|
||||
msg += "in approx. \(when) sec. "
|
||||
} else {
|
||||
let fmt = TimeFormat.from(Timestamp(when))
|
||||
msg += "in \(fmt) min. "
|
||||
}
|
||||
} else {
|
||||
msg += "shortly. "
|
||||
}
|
||||
msg += "Open results webpage now?"
|
||||
AskAlert(title: "Thank you", text: msg, buttonText: "Show results", cancelButton: "Not now") { _ in
|
||||
if let url = URL(string: urlStr) {
|
||||
UIApplication.shared.openURL(url)
|
||||
}
|
||||
}.presentIn(self)
|
||||
}
|
||||
|
||||
private func showAlertAlreadyShared() {
|
||||
let alert = Alert(title: nil, text: "You already shared this recording.")
|
||||
if let bid = record.appId, bid.isValidBundleId() {
|
||||
alert.addAction(UIAlertAction.init(title: "Open results", style: .default, handler: { _ in
|
||||
URL(string: "http://127.0.0.1/redirect.html?id=\(bid)")?.open()
|
||||
}))
|
||||
}
|
||||
alert.presentIn(self)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user