Improve recording contribution view. Replace TextView with interactive TableView.

This commit is contained in:
relikd
2020-09-04 09:14:23 +02:00
parent 4ae82fc763
commit 2ee0272a05
8 changed files with 495 additions and 220 deletions

View File

@@ -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 */,

View File

@@ -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)

View File

@@ -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"/>

View File

@@ -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
}

View File

@@ -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
}
}

View 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)
}
}

View File

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

View File

@@ -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)
}
}