Recordings: Choose app instead of custom title
This commit is contained in:
@@ -77,6 +77,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 */; };
|
||||
549D6ED524D5BFDB0032E498 /* TVCAppSearch.swift in Sources */ = {isa = PBXBuildFile; fileRef = 549D6ED424D5BFDB0032E498 /* TVCAppSearch.swift */; };
|
||||
549ECD9D24A7AD550097571C /* CustomAlert.swift in Sources */ = {isa = PBXBuildFile; fileRef = 549ECD9C24A7AD550097571C /* CustomAlert.swift */; };
|
||||
54B34594240E6343004C53CC /* TVCFilter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 54B34593240E6343004C53CC /* TVCFilter.swift */; };
|
||||
54B34596240F0513004C53CC /* TableView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 54B34595240F0513004C53CC /* TableView.swift */; };
|
||||
@@ -85,7 +86,7 @@
|
||||
54B345AB241BBA5B004C53CC /* AlertSheet.swift in Sources */ = {isa = PBXBuildFile; fileRef = 54B345AA241BBA5B004C53CC /* AlertSheet.swift */; };
|
||||
54B345AD241BBB00004C53CC /* DBExtensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 54B345AC241BBB00004C53CC /* DBExtensions.swift */; };
|
||||
54B345B0242264F8004C53CC /* third-level.txt in Resources */ = {isa = PBXBuildFile; fileRef = 54B345AF242264F8004C53CC /* third-level.txt */; };
|
||||
54C056DB23E9E36E00214A3F /* AppInfoType.swift in Sources */ = {isa = PBXBuildFile; fileRef = 54C056DA23E9E36E00214A3F /* AppInfoType.swift */; };
|
||||
54C056DB23E9E36E00214A3F /* AppStoreSearch.swift in Sources */ = {isa = PBXBuildFile; fileRef = 54C056DA23E9E36E00214A3F /* AppStoreSearch.swift */; };
|
||||
54C056DD23E9EEF700214A3F /* BundleIcon.swift in Sources */ = {isa = PBXBuildFile; fileRef = 54C056DC23E9EEF700214A3F /* BundleIcon.swift */; };
|
||||
54CA01D32426B23D003A5E04 /* Resolver.swift in Sources */ = {isa = PBXBuildFile; fileRef = 54CA01D22426B23D003A5E04 /* Resolver.swift */; };
|
||||
54CA01D52426B252003A5E04 /* SafeDict.swift in Sources */ = {isa = PBXBuildFile; fileRef = 54CA01D42426B251003A5E04 /* SafeDict.swift */; };
|
||||
@@ -270,6 +271,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>"; };
|
||||
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>"; };
|
||||
54B34593240E6343004C53CC /* TVCFilter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TVCFilter.swift; sourceTree = "<group>"; };
|
||||
54B34595240F0513004C53CC /* TableView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TableView.swift; sourceTree = "<group>"; };
|
||||
@@ -279,7 +281,7 @@
|
||||
54B345AC241BBB00004C53CC /* DBExtensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DBExtensions.swift; sourceTree = "<group>"; };
|
||||
54B345AF242264F8004C53CC /* third-level.txt */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text; path = "third-level.txt"; sourceTree = "<group>"; };
|
||||
54B7562223D7B2DC008F0C41 /* DBCore.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DBCore.swift; sourceTree = "<group>"; };
|
||||
54C056DA23E9E36E00214A3F /* AppInfoType.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppInfoType.swift; sourceTree = "<group>"; };
|
||||
54C056DA23E9E36E00214A3F /* AppStoreSearch.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppStoreSearch.swift; sourceTree = "<group>"; };
|
||||
54C056DC23E9EEF700214A3F /* BundleIcon.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BundleIcon.swift; sourceTree = "<group>"; };
|
||||
54CA00D62426A803003A5E04 /* CocoaAsyncSocket.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = CocoaAsyncSocket.h; sourceTree = "<group>"; };
|
||||
54CA01D22426B23D003A5E04 /* Resolver.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Resolver.swift; sourceTree = "<group>"; };
|
||||
@@ -432,6 +434,8 @@
|
||||
540E67832433FAFE00871BBE /* TVCPreviousRecords.swift */,
|
||||
540E67812433483D00871BBE /* VCEditRecording.swift */,
|
||||
5458EBBF243A3F2200CFEB15 /* TVCRecordingDetails.swift */,
|
||||
549D6ED424D5BFDB0032E498 /* TVCAppSearch.swift */,
|
||||
54B345B12422E029004C53CC /* App Icons */,
|
||||
);
|
||||
path = Recordings;
|
||||
sourceTree = "<group>";
|
||||
@@ -482,7 +486,6 @@
|
||||
540C6454240D5BAE00E948F9 /* Requests */,
|
||||
540E677E242D2CD200871BBE /* Recordings */,
|
||||
540C6455240D5BD200E948F9 /* Settings */,
|
||||
54B345B12422E029004C53CC /* unused */,
|
||||
541AC5E02399498B00A769D7 /* LaunchScreen.storyboard */,
|
||||
541AC5DB2399498A00A769D7 /* Main.storyboard */,
|
||||
543078C124B60F3B00278F2D /* Settings.storyboard */,
|
||||
@@ -593,13 +596,13 @@
|
||||
path = Extensions;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
54B345B12422E029004C53CC /* unused */ = {
|
||||
54B345B12422E029004C53CC /* App Icons */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
54C056DC23E9EEF700214A3F /* BundleIcon.swift */,
|
||||
54C056DA23E9E36E00214A3F /* AppInfoType.swift */,
|
||||
54C056DA23E9E36E00214A3F /* AppStoreSearch.swift */,
|
||||
);
|
||||
path = unused;
|
||||
path = "App Icons";
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
54CA00D52426A7F2003A5E04 /* robbiehanson-CocoaAsyncSocket */ = {
|
||||
@@ -1009,9 +1012,10 @@
|
||||
544F912024A67EC5001D4B00 /* TVCOccurrenceContext.swift in Sources */,
|
||||
54448A30248647D900771C96 /* Time.swift in Sources */,
|
||||
54953E6123E0D69A0054345C /* TVCHosts.swift in Sources */,
|
||||
549D6ED524D5BFDB0032E498 /* TVCAppSearch.swift in Sources */,
|
||||
54751E512423955100168273 /* URL.swift in Sources */,
|
||||
54953E5F23DEBE840054345C /* TVCDomains.swift in Sources */,
|
||||
54C056DB23E9E36E00214A3F /* AppInfoType.swift in Sources */,
|
||||
54C056DB23E9E36E00214A3F /* AppStoreSearch.swift in Sources */,
|
||||
54D8B98624796E9900EB2414 /* GroupedDomainDataSource.swift in Sources */,
|
||||
541075CE24C9D43A00D6F1BF /* UNNotification.swift in Sources */,
|
||||
54953E6F23E44CD00054345C /* TVCHostDetails.swift in Sources */,
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<document type="com.apple.InterfaceBuilder3.CocoaTouch.Storyboard.XIB" version="3.0" toolsVersion="16097" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" useSafeAreas="YES" colorMatched="YES" initialViewController="sfA-EG-18J">
|
||||
<document type="com.apple.InterfaceBuilder3.CocoaTouch.Storyboard.XIB" version="3.0" toolsVersion="16097.2" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" useSafeAreas="YES" colorMatched="YES" initialViewController="sfA-EG-18J">
|
||||
<device id="retina4_0" orientation="portrait" appearance="light"/>
|
||||
<dependencies>
|
||||
<deployment identifier="iOS"/>
|
||||
@@ -893,39 +893,55 @@
|
||||
</navigationItem>
|
||||
</items>
|
||||
<connections>
|
||||
<outletCollection property="gestureRecognizers" destination="klV-Ed-xzV" appends="YES" id="Huf-jb-4Ef"/>
|
||||
<outletCollection property="gestureRecognizers" destination="B0n-l6-MKc" appends="YES" id="57w-bJ-Vjh"/>
|
||||
</connections>
|
||||
</navigationBar>
|
||||
<stackView opaque="NO" contentMode="scaleToFill" axis="vertical" alignment="center" spacing="8" translatesAutoresizingMaskIntoConstraints="NO" id="xdn-EU-IMx">
|
||||
<rect key="frame" x="16" y="56" width="288" height="355"/>
|
||||
<subviews>
|
||||
<view contentMode="scaleToFill" translatesAutoresizingMaskIntoConstraints="NO" id="Guy-Ra-fpS" userLabel="Title">
|
||||
<rect key="frame" x="0.0" y="0.0" width="288" height="58"/>
|
||||
<rect key="frame" x="0.0" y="0.0" width="288" height="40"/>
|
||||
<subviews>
|
||||
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="Title" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" id="Et0-8d-CId">
|
||||
<rect key="frame" x="0.0" y="0.0" width="288" height="24"/>
|
||||
<autoresizingMask key="autoresizingMask" widthSizable="YES" flexibleMaxY="YES"/>
|
||||
<fontDescription key="fontDescription" style="UICTFontTextStyleTitle2"/>
|
||||
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="AppCheck" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="Et0-8d-CId">
|
||||
<rect key="frame" x="0.0" y="2" width="80" height="20.5"/>
|
||||
<fontDescription key="fontDescription" style="UICTFontTextStyleBody"/>
|
||||
<nil key="textColor"/>
|
||||
<nil key="highlightedColor"/>
|
||||
</label>
|
||||
<textField opaque="NO" contentMode="scaleToFill" contentHorizontalAlignment="left" contentVerticalAlignment="center" placeholder="Unnamed Recording #12345678" textAlignment="natural" minimumFontSize="17" clearButtonMode="whileEditing" id="OCX-wu-l5d">
|
||||
<rect key="frame" x="4" y="24" width="280" height="34"/>
|
||||
<autoresizingMask key="autoresizingMask" widthSizable="YES" flexibleMaxY="YES"/>
|
||||
<fontDescription key="fontDescription" style="UICTFontTextStyleBody"/>
|
||||
<textInputTraits key="textInputTraits" returnKeyType="next"/>
|
||||
<connections>
|
||||
<outlet property="delegate" destination="VRk-wv-rhk" id="uJL-hB-9w7"/>
|
||||
</connections>
|
||||
</textField>
|
||||
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="University Bamberg" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="l8O-Kw-uc8">
|
||||
<rect key="frame" x="0.0" y="23.5" width="111" height="14.5"/>
|
||||
<fontDescription key="fontDescription" style="UICTFontTextStyleCaption1"/>
|
||||
<color key="textColor" systemColor="secondaryLabelColor" red="0.23529411759999999" green="0.23529411759999999" blue="0.26274509800000001" alpha="0.59999999999999998" colorSpace="custom" customColorSpace="sRGB"/>
|
||||
<nil key="highlightedColor"/>
|
||||
</label>
|
||||
<imageView clipsSubviews="YES" userInteractionEnabled="NO" contentMode="scaleAspectFit" horizontalHuggingPriority="251" verticalHuggingPriority="251" image="LaunchIcon.png" translatesAutoresizingMaskIntoConstraints="NO" id="rbW-pK-Kct">
|
||||
<rect key="frame" x="248" y="0.0" width="40" height="40"/>
|
||||
<constraints>
|
||||
<constraint firstAttribute="width" secondItem="rbW-pK-Kct" secondAttribute="height" multiplier="1:1" id="dV9-kR-y39"/>
|
||||
</constraints>
|
||||
</imageView>
|
||||
</subviews>
|
||||
<color key="backgroundColor" systemColor="systemBackgroundColor" cocoaTouchSystemColor="whiteColor"/>
|
||||
<gestureRecognizers/>
|
||||
<constraints>
|
||||
<constraint firstAttribute="height" constant="58" id="5ew-Cq-VKh"/>
|
||||
<constraint firstAttribute="height" 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 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="rbW-pK-Kct" firstAttribute="height" secondItem="Guy-Ra-fpS" secondAttribute="height" id="zLK-Gu-HcF"/>
|
||||
</constraints>
|
||||
<connections>
|
||||
<outletCollection property="gestureRecognizers" destination="Jab-q2-U9X" appends="YES" id="V27-A7-AL5"/>
|
||||
</connections>
|
||||
</view>
|
||||
<view contentMode="scaleToFill" translatesAutoresizingMaskIntoConstraints="NO" id="ybL-UG-dwT" userLabel="Notes">
|
||||
<rect key="frame" x="0.0" y="66" width="288" height="190"/>
|
||||
<rect key="frame" x="0.0" y="48" width="288" height="208"/>
|
||||
<subviews>
|
||||
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="Notes" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" id="QJp-6C-yoZ">
|
||||
<rect key="frame" x="0.0" y="0.0" width="288" height="24"/>
|
||||
@@ -935,7 +951,7 @@
|
||||
<nil key="highlightedColor"/>
|
||||
</label>
|
||||
<textView clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="scaleToFill" showsHorizontalScrollIndicator="NO" translatesAutoresizingMaskIntoConstraints="NO" id="NXU-yU-eST">
|
||||
<rect key="frame" x="0.0" y="24" width="288" height="166"/>
|
||||
<rect key="frame" x="0.0" y="24" width="288" height="184"/>
|
||||
<color key="backgroundColor" systemColor="systemBackgroundColor" cocoaTouchSystemColor="whiteColor"/>
|
||||
<string key="text">1. Line
|
||||
2. Line
|
||||
@@ -987,8 +1003,8 @@ Duration: 60:00</string>
|
||||
</view>
|
||||
</subviews>
|
||||
<constraints>
|
||||
<constraint firstItem="ybL-UG-dwT" firstAttribute="width" secondItem="Guy-Ra-fpS" secondAttribute="width" id="PUH-xO-ZbD"/>
|
||||
<constraint firstItem="QiY-Mm-Dej" firstAttribute="width" secondItem="Guy-Ra-fpS" secondAttribute="width" id="U6e-10-j55"/>
|
||||
<constraint firstItem="ybL-UG-dwT" firstAttribute="width" secondItem="xdn-EU-IMx" secondAttribute="width" id="PUH-xO-ZbD"/>
|
||||
<constraint firstItem="QiY-Mm-Dej" firstAttribute="width" secondItem="xdn-EU-IMx" secondAttribute="width" id="U6e-10-j55"/>
|
||||
<constraint firstItem="Guy-Ra-fpS" firstAttribute="width" secondItem="xdn-EU-IMx" secondAttribute="width" id="ZCJ-ol-1Jv"/>
|
||||
</constraints>
|
||||
</stackView>
|
||||
@@ -1006,22 +1022,30 @@ Duration: 60:00</string>
|
||||
<viewLayoutGuide key="safeArea" id="fMa-Lq-tGz"/>
|
||||
</view>
|
||||
<connections>
|
||||
<outlet property="appDeveloper" destination="l8O-Kw-uc8" id="dfg-s6-Biz"/>
|
||||
<outlet property="appIcon" destination="rbW-pK-Kct" id="VlO-fG-y1a"/>
|
||||
<outlet property="appTitle" destination="Et0-8d-CId" id="HgD-oI-0J8"/>
|
||||
<outlet property="buttonCancel" destination="TGg-60-wZW" id="5Ej-7t-jaD"/>
|
||||
<outlet property="buttonSave" destination="rWg-hE-Ydl" id="zfM-kx-erX"/>
|
||||
<outlet property="chooseAppTap" destination="Jab-q2-U9X" id="Tzv-lm-sUm"/>
|
||||
<outlet property="inputDetails" destination="pql-H5-k6U" id="NXm-8f-5E6"/>
|
||||
<outlet property="inputNotes" destination="NXU-yU-eST" id="c2n-cG-aLq"/>
|
||||
<outlet property="inputTitle" destination="OCX-wu-l5d" id="PeC-F5-4mx"/>
|
||||
<outlet property="noteBottom" destination="vFS-tG-E43" id="Bxh-Tl-E2U"/>
|
||||
</connections>
|
||||
</viewController>
|
||||
<placeholder placeholderIdentifier="IBFirstResponder" id="KN7-F1-BOL" userLabel="First Responder" customClass="UIResponder" sceneMemberID="firstResponder"/>
|
||||
<tapGestureRecognizer id="klV-Ed-xzV">
|
||||
<tapGestureRecognizer id="Jab-q2-U9X">
|
||||
<connections>
|
||||
<action selector="hideKeyboard" destination="VRk-wv-rhk" id="iDb-kK-nli"/>
|
||||
<segue destination="qNp-w1-7Md" kind="modal" id="22y-Dy-3xf"/>
|
||||
</connections>
|
||||
</tapGestureRecognizer>
|
||||
<tapGestureRecognizer id="B0n-l6-MKc">
|
||||
<connections>
|
||||
<action selector="hideKeyboard" destination="VRk-wv-rhk" id="eMC-Nn-xoE"/>
|
||||
</connections>
|
||||
</tapGestureRecognizer>
|
||||
</objects>
|
||||
<point key="canvasLocation" x="2100" y="-550"/>
|
||||
<point key="canvasLocation" x="1400" y="150"/>
|
||||
</scene>
|
||||
<!--Logs-->
|
||||
<scene sceneID="DxJ-8o-gTM">
|
||||
@@ -1117,10 +1141,66 @@ Duration: 60:00</string>
|
||||
</connections>
|
||||
</barButtonItem>
|
||||
</navigationItem>
|
||||
<simulatedNavigationBarMetrics key="simulatedTopBarMetrics" prompted="NO"/>
|
||||
</tableViewController>
|
||||
<placeholder placeholderIdentifier="IBFirstResponder" id="lan-I9-b0a" userLabel="First Responder" customClass="UIResponder" sceneMemberID="firstResponder"/>
|
||||
</objects>
|
||||
<point key="canvasLocation" x="2800" y="-550"/>
|
||||
<point key="canvasLocation" x="2100" y="-550"/>
|
||||
</scene>
|
||||
<!--App Search-->
|
||||
<scene sceneID="n6R-Wm-XxF">
|
||||
<objects>
|
||||
<tableViewController id="qNp-w1-7Md" customClass="TVCAppSearch" customModule="AppCheck" customModuleProvider="target" sceneMemberID="viewController">
|
||||
<tableView key="view" clipsSubviews="YES" contentMode="scaleToFill" alwaysBounceVertical="YES" dataMode="prototypes" style="plain" separatorStyle="default" rowHeight="-1" estimatedRowHeight="-1" sectionHeaderHeight="28" sectionFooterHeight="28" id="WYZ-wA-6Rh">
|
||||
<rect key="frame" x="0.0" y="0.0" width="320" height="421"/>
|
||||
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
|
||||
<color key="backgroundColor" systemColor="systemBackgroundColor" cocoaTouchSystemColor="whiteColor"/>
|
||||
<searchBar key="tableHeaderView" contentMode="redraw" preservesSuperviewLayoutMargins="YES" placeholder="Search AppStore" showsCancelButton="YES" id="9dl-ZI-85h">
|
||||
<rect key="frame" x="0.0" y="0.0" width="320" height="56"/>
|
||||
<autoresizingMask key="autoresizingMask" widthSizable="YES" flexibleMaxY="YES"/>
|
||||
<textInputTraits key="textInputTraits" returnKeyType="search" enablesReturnKeyAutomatically="YES"/>
|
||||
<connections>
|
||||
<outlet property="delegate" destination="qNp-w1-7Md" id="vcp-BS-7xF"/>
|
||||
</connections>
|
||||
</searchBar>
|
||||
<prototypes>
|
||||
<tableViewCell clipsSubviews="YES" contentMode="scaleToFill" preservesSuperviewLayoutMargins="YES" selectionStyle="default" indentationWidth="10" reuseIdentifier="AppStoreSearchCell" textLabel="yIh-WB-0rK" detailTextLabel="PDe-1x-vle" style="IBUITableViewCellStyleSubtitle" id="Q8M-0Q-Mc7">
|
||||
<rect key="frame" x="0.0" y="84" width="320" height="57.5"/>
|
||||
<autoresizingMask key="autoresizingMask"/>
|
||||
<tableViewCellContentView key="contentView" opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center" preservesSuperviewLayoutMargins="YES" insetsLayoutMarginsFromSafeArea="NO" tableViewCell="Q8M-0Q-Mc7" id="BPc-V2-I7v">
|
||||
<rect key="frame" x="0.0" y="0.0" width="320" height="57.5"/>
|
||||
<autoresizingMask key="autoresizingMask"/>
|
||||
<subviews>
|
||||
<label opaque="NO" multipleTouchEnabled="YES" contentMode="left" insetsLayoutMarginsFromSafeArea="NO" text="Title" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" id="yIh-WB-0rK">
|
||||
<rect key="frame" x="16" y="9" width="33.5" height="20.5"/>
|
||||
<autoresizingMask key="autoresizingMask"/>
|
||||
<fontDescription key="fontDescription" style="UICTFontTextStyleBody"/>
|
||||
<nil key="textColor"/>
|
||||
<nil key="highlightedColor"/>
|
||||
</label>
|
||||
<label opaque="NO" multipleTouchEnabled="YES" contentMode="left" insetsLayoutMarginsFromSafeArea="NO" text="Detail" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" id="PDe-1x-vle">
|
||||
<rect key="frame" x="16" y="32.5" width="33" height="14.5"/>
|
||||
<autoresizingMask key="autoresizingMask"/>
|
||||
<fontDescription key="fontDescription" style="UICTFontTextStyleCaption1"/>
|
||||
<color key="textColor" systemColor="secondaryLabelColor" red="0.23529411759999999" green="0.23529411759999999" blue="0.26274509800000001" alpha="0.59999999999999998" colorSpace="custom" customColorSpace="sRGB"/>
|
||||
<nil key="highlightedColor"/>
|
||||
</label>
|
||||
</subviews>
|
||||
</tableViewCellContentView>
|
||||
</tableViewCell>
|
||||
</prototypes>
|
||||
<connections>
|
||||
<outlet property="dataSource" destination="qNp-w1-7Md" id="Jdv-03-iVJ"/>
|
||||
<outlet property="delegate" destination="qNp-w1-7Md" id="3vD-b3-Ake"/>
|
||||
</connections>
|
||||
</tableView>
|
||||
<connections>
|
||||
<outlet property="searchBar" destination="9dl-ZI-85h" id="8Zr-E0-mzs"/>
|
||||
</connections>
|
||||
</tableViewController>
|
||||
<placeholder placeholderIdentifier="IBFirstResponder" id="PhN-mC-C3W" userLabel="First Responder" customClass="UIResponder" sceneMemberID="firstResponder"/>
|
||||
</objects>
|
||||
<point key="canvasLocation" x="2100" y="150"/>
|
||||
</scene>
|
||||
<!--Settings-->
|
||||
<scene sceneID="OEQ-fb-haL">
|
||||
@@ -1151,9 +1231,10 @@ Duration: 60:00</string>
|
||||
</scene>
|
||||
</scenes>
|
||||
<inferredMetricsTieBreakers>
|
||||
<segue reference="ueN-6L-cP7"/>
|
||||
<segue reference="vf1-07-AS4"/>
|
||||
</inferredMetricsTieBreakers>
|
||||
<resources>
|
||||
<image name="LaunchIcon.png" width="128" height="128"/>
|
||||
<image name="filter-clear" width="20" height="20"/>
|
||||
<image name="intersection" width="25" height="25"/>
|
||||
<image name="journal" width="25" height="25"/>
|
||||
|
||||
@@ -20,9 +20,13 @@ extension SQLiteDatabase {
|
||||
try ifStep(stmt, SQLITE_ROW)
|
||||
return sqlite3_column_int(stmt, 0)
|
||||
}
|
||||
if version != 1 {
|
||||
if version != 2 {
|
||||
// version 0 -> 1: req(domain) -> heap(fqdn, domain)
|
||||
try run(sql: "PRAGMA user_version = 1;")
|
||||
// version 1 -> 2: rec(+subtitle)
|
||||
if version == 1 {
|
||||
try run(sql: "ALTER TABLE rec ADD COLUMN subtitle TEXT;")
|
||||
}
|
||||
try run(sql: "PRAGMA user_version = 2;")
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -281,7 +285,7 @@ extension SQLiteDatabase {
|
||||
// MARK: - Recordings
|
||||
|
||||
extension CreateTable {
|
||||
/// `id`: Primary, `start`: Timestamp, `stop`: Timestamp, `appid`: String, `title`: String, `notes`: String
|
||||
/// `id`: Primary, `start`: Timestamp, `stop`: Timestamp, `appid`: String, `title`: String, `subtitle`: String, `notes`: String
|
||||
static var rec: String {"""
|
||||
CREATE TABLE IF NOT EXISTS rec(
|
||||
id INTEGER PRIMARY KEY,
|
||||
@@ -289,6 +293,7 @@ extension CreateTable {
|
||||
stop INTEGER,
|
||||
appid TEXT,
|
||||
title TEXT,
|
||||
subtitle TEXT,
|
||||
notes TEXT
|
||||
);
|
||||
"""}
|
||||
@@ -300,8 +305,10 @@ struct Recording {
|
||||
let stop: Timestamp?
|
||||
var appId: String? = nil
|
||||
var title: String? = nil
|
||||
var subtitle: String? = nil
|
||||
var notes: String? = nil
|
||||
}
|
||||
typealias AppBundleInfo = (bundleId: String, name: String?, author: String?)
|
||||
|
||||
extension SQLiteDatabase {
|
||||
|
||||
@@ -328,8 +335,8 @@ extension SQLiteDatabase {
|
||||
|
||||
/// Update given recording by replacing `title`, `appid`, and `notes` with new values.
|
||||
func recordingUpdate(_ r: Recording) {
|
||||
try? run(sql: "UPDATE rec SET title = ?, appid = ?, notes = ? WHERE id = ? LIMIT 1;",
|
||||
bind: [BindTextOrNil(r.title), BindTextOrNil(r.appId), BindTextOrNil(r.notes), BindInt64(r.id)]) { stmt -> Void in
|
||||
try? run(sql: "UPDATE rec SET appid = ?, title = ?, subtitle = ?, notes = ? WHERE id = ? LIMIT 1;",
|
||||
bind: [BindTextOrNil(r.appId), BindTextOrNil(r.title), BindTextOrNil(r.subtitle), BindTextOrNil(r.notes), BindInt64(r.id)]) { stmt -> Void in
|
||||
sqlite3_step(stmt)
|
||||
}
|
||||
}
|
||||
@@ -353,12 +360,13 @@ extension SQLiteDatabase {
|
||||
stop: end == 0 ? nil : end,
|
||||
appId: col_text(stmt, 3),
|
||||
title: col_text(stmt, 4),
|
||||
notes: col_text(stmt, 5))
|
||||
subtitle: col_text(stmt, 5),
|
||||
notes: col_text(stmt, 6))
|
||||
}
|
||||
|
||||
/// `WHERE stop IS NULL`
|
||||
func recordingGetOngoing() -> Recording? {
|
||||
try? run(sql: "SELECT * FROM rec WHERE stop IS NULL LIMIT 1;") {
|
||||
try? run(sql: "SELECT id, start, stop, appid, title, subtitle, notes FROM rec WHERE stop IS NULL LIMIT 1;") {
|
||||
try ifStep($0, SQLITE_ROW)
|
||||
return readRecording($0)
|
||||
}
|
||||
@@ -374,18 +382,26 @@ extension SQLiteDatabase {
|
||||
|
||||
/// `WHERE stop IS NOT NULL`
|
||||
func recordingGetAll() -> [Recording]? {
|
||||
try? run(sql: "SELECT * FROM rec WHERE stop IS NOT NULL;") {
|
||||
try? run(sql: "SELECT id, start, stop, appid, title, subtitle, notes FROM rec WHERE stop IS NOT NULL;") {
|
||||
allRows($0) { readRecording($0) }
|
||||
}
|
||||
}
|
||||
|
||||
/// `WHERE id = ?`
|
||||
private func recordingGet(withID: sqlite3_int64) throws -> Recording {
|
||||
try run(sql: "SELECT * FROM rec WHERE id = ? LIMIT 1;", bind: [BindInt64(withID)]) {
|
||||
try run(sql: "SELECT id, start, stop, appid, title, subtitle, notes FROM rec WHERE id = ? LIMIT 1;", bind: [BindInt64(withID)]) {
|
||||
try ifStep($0, SQLITE_ROW)
|
||||
return readRecording($0)
|
||||
}
|
||||
}
|
||||
|
||||
func appBundleList() -> [AppBundleInfo]? {
|
||||
try? run(sql: "SELECT appid, title, subtitle FROM rec WHERE appid IS NOT NULL GROUP BY appid ORDER BY title ASC;") {
|
||||
allRows($0) {
|
||||
AppBundleInfo(col_text($0, 0)!, col_text($0, 1), col_text($0, 2))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -35,5 +35,6 @@ extension FilterOptions {
|
||||
extension Recording {
|
||||
var fallbackTitle: String { get { "Unnamed Recording #\(id)" } }
|
||||
var duration: Timestamp? { get { stop == nil ? nil : stop! - start } }
|
||||
var isLongTerm: Bool { (duration ?? 0) > Timestamp.hours(1) }
|
||||
}
|
||||
|
||||
|
||||
@@ -52,5 +52,10 @@ enum RecordingsDB {
|
||||
static func deleteSingle(_ r: Recording, domain: String, ts: Timestamp) -> Bool {
|
||||
(try? AppDB?.recordingLogsDelete(r.id, singleEntry: ts, domain: domain)) ?? false
|
||||
}
|
||||
|
||||
/// Return list of previously used apps found in all recordings.
|
||||
static func appList() -> [AppBundleInfo] {
|
||||
AppDB?.appBundleList() ?? []
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
import Foundation
|
||||
|
||||
fileprivate extension FileManager {
|
||||
// func exportDir() -> URL {
|
||||
// try! url(for: .documentDirectory, in: .userDomainMask, appropriateFor: nil, create: true)
|
||||
// }
|
||||
func documentDir() -> URL {
|
||||
try! url(for: .documentDirectory, in: .userDomainMask, appropriateFor: nil, create: true)
|
||||
}
|
||||
func appGroupDir() -> URL {
|
||||
containerURL(forSecurityApplicationGroupIdentifier: "group.de.uni-bamberg.psi.AppCheck")!
|
||||
}
|
||||
@@ -25,7 +25,32 @@ extension FileManager {
|
||||
}
|
||||
|
||||
extension URL {
|
||||
// static func exportDir() -> URL { FileManager.default.exportDir() }
|
||||
static func documentDir() -> URL { FileManager.default.documentDir() }
|
||||
static func appGroupDir() -> URL { FileManager.default.appGroupDir() }
|
||||
static func internalDB() -> URL { FileManager.default.internalDB() }
|
||||
|
||||
static func make(_ base: String, params: [String : String]) -> URL? {
|
||||
guard var components = URLComponents(string: base) else {
|
||||
return nil
|
||||
}
|
||||
components.queryItems = params.map {
|
||||
URLQueryItem(name: $0, value: $1)
|
||||
}
|
||||
components.percentEncodedQuery = components.percentEncodedQuery?.replacingOccurrences(of: "+", with: "%2B")
|
||||
return components.url
|
||||
}
|
||||
|
||||
func download(to file: URL, onSuccess: @escaping () -> Void) {
|
||||
URLSession.shared.downloadTask(with: self) { location, response, error in
|
||||
if let loc = location {
|
||||
try? FileManager.default.removeItem(at: file)
|
||||
do {
|
||||
try FileManager.default.moveItem(at: loc, to: file)
|
||||
onSuccess()
|
||||
} catch {
|
||||
NSLog("[VPN.ERROR] \(error)")
|
||||
}
|
||||
}
|
||||
}.resume()
|
||||
}
|
||||
}
|
||||
|
||||
52
main/Recordings/App Icons/AppStoreSearch.swift
Normal file
52
main/Recordings/App Icons/AppStoreSearch.swift
Normal file
@@ -0,0 +1,52 @@
|
||||
import Foundation
|
||||
import UIKit
|
||||
|
||||
extension URL {
|
||||
static func appStoreSearch(query: String) -> URL {
|
||||
// https://itunes.apple.com/lookup?bundleId=...
|
||||
URL.make("https://itunes.apple.com/search", params: [
|
||||
"media" : "software",
|
||||
"limit" : "25",
|
||||
"country" : NSLocale.current.regionCode ?? "DE",
|
||||
"version" : "2",
|
||||
"term" : query,
|
||||
])!
|
||||
}
|
||||
}
|
||||
|
||||
struct AppStoreSearch {
|
||||
struct Result {
|
||||
let bundleId, name: String
|
||||
let developer, imageURL: String?
|
||||
}
|
||||
|
||||
static func search(_ term: String, _ closure: @escaping ([Result]?) -> Void) {
|
||||
URLSession.shared.dataTask(with: .init(url: .appStoreSearch(query: term))) { data, response, error in
|
||||
guard let data = data, error == nil,
|
||||
let response = response as? HTTPURLResponse,
|
||||
(200 ..< 300) ~= response.statusCode else {
|
||||
closure(nil)
|
||||
return
|
||||
}
|
||||
closure(jsonSearchToList(data))
|
||||
}.resume()
|
||||
}
|
||||
|
||||
private static func jsonSearchToList(_ data: Data) -> [Result]? {
|
||||
guard let json = (try? JSONSerialization.jsonObject(with: data, options: .fragmentsAllowed)) as? [String: Any],
|
||||
let resAll = json["results"] as? [Any] else {
|
||||
return nil
|
||||
}
|
||||
return resAll.compactMap {
|
||||
guard let res = $0 as? [String: Any],
|
||||
let bndl = res["bundleId"] as? String,
|
||||
let name = res["trackName"] as? String // trackCensoredName
|
||||
else {
|
||||
return nil
|
||||
}
|
||||
let seller = res["sellerName"] as? String // artistName
|
||||
let image = res["artworkUrl60"] as? String // artworkUrl100
|
||||
return Result(bundleId: bndl, name: name, developer: seller, imageURL: image)
|
||||
}
|
||||
}
|
||||
}
|
||||
106
main/Recordings/App Icons/BundleIcon.swift
Normal file
106
main/Recordings/App Icons/BundleIcon.swift
Normal file
@@ -0,0 +1,106 @@
|
||||
import UIKit
|
||||
|
||||
extension CGContext {
|
||||
func lineFromTo(x1: CGFloat, y1: CGFloat, x2: CGFloat, y2: CGFloat) {
|
||||
self.move(to: CGPoint(x: x1, y: y1))
|
||||
self.addLine(to: CGPoint(x: x2, y: y2))
|
||||
}
|
||||
}
|
||||
|
||||
struct BundleIcon {
|
||||
|
||||
static let unknown : UIImage? = {
|
||||
let rect = CGRect(x: 0, y: 0, width: 30, height: 30)
|
||||
UIGraphicsBeginImageContextWithOptions(rect.size, false, 0.0)
|
||||
|
||||
let context = UIGraphicsGetCurrentContext()!
|
||||
let lineWidth: CGFloat = 0.5
|
||||
let corner: CGFloat = 6.75
|
||||
let c = corner / CGFloat.pi + lineWidth/2
|
||||
let sz: CGFloat = rect.height
|
||||
let m = sz / 2
|
||||
let r1 = 0.2 * sz, r2 = sqrt(2 * r1 * r1)
|
||||
|
||||
// diagonal
|
||||
context.lineFromTo(x1: c, y1: c, x2: sz-c, y2: sz-c)
|
||||
context.lineFromTo(x1: c, y1: sz-c, x2: sz-c, y2: c)
|
||||
// horizontal
|
||||
context.lineFromTo(x1: 0, y1: m, x2: sz, y2: m)
|
||||
context.lineFromTo(x1: 0, y1: m + r1, x2: sz, y2: m + r1)
|
||||
context.lineFromTo(x1: 0, y1: m - r1, x2: sz, y2: m - r1)
|
||||
// vertical
|
||||
context.lineFromTo(x1: m, y1: 0, x2: m, y2: sz)
|
||||
context.lineFromTo(x1: m + r1, y1: 0, x2: m + r1, y2: sz)
|
||||
context.lineFromTo(x1: m - r1, y1: 0, x2: m - r1, y2: sz)
|
||||
// circles
|
||||
context.addEllipse(in: CGRect(x: m - r1, y: m - r1, width: 2*r1, height: 2*r1))
|
||||
context.addEllipse(in: CGRect(x: m - r2, y: m - r2, width: 2*r2, height: 2*r2))
|
||||
let r3 = CGRect(x: c, y: c, width: sz - 2*c, height: sz - 2*c)
|
||||
context.addEllipse(in: r3)
|
||||
context.addRect(r3)
|
||||
|
||||
UIColor.clear.setFill()
|
||||
UIColor.gray.setStroke()
|
||||
let rounded = UIBezierPath(roundedRect: rect.insetBy(dx: lineWidth/2, dy: lineWidth/2), cornerRadius: corner)
|
||||
rounded.lineWidth = lineWidth
|
||||
rounded.stroke()
|
||||
|
||||
let img = UIGraphicsGetImageFromCurrentImageContext()
|
||||
UIGraphicsEndImageContext()
|
||||
return img
|
||||
}()
|
||||
|
||||
private static let apple : UIImage? = {
|
||||
let rect = CGRect(x: 0, y: 0, width: 30, height: 30)
|
||||
UIGraphicsBeginImageContextWithOptions(rect.size, false, 0.0)
|
||||
|
||||
// #colorLiteral(red: 0.5843137503, green: 0.8235294223, blue: 0.4196078479, alpha: 1).setFill()
|
||||
// UIBezierPath(roundedRect: rect, cornerRadius: 0).fill()
|
||||
// print("drawing")
|
||||
let fs = 36 as CGFloat
|
||||
let hFont = UIFont.systemFont(ofSize: fs)
|
||||
var attrib = [
|
||||
NSAttributedString.Key.font: hFont,
|
||||
NSAttributedString.Key.foregroundColor: UIColor.gray
|
||||
]
|
||||
|
||||
let str = "" as NSString
|
||||
let actualHeight = str.size(withAttributes: attrib).height
|
||||
attrib[NSAttributedString.Key.font] = hFont.withSize(fs * fs / actualHeight)
|
||||
|
||||
let strW = str.size(withAttributes: attrib).width
|
||||
str.draw(at: CGPoint(x: (rect.size.width - strW) / 2.0, y: -3), withAttributes: attrib)
|
||||
|
||||
let img = UIGraphicsGetImageFromCurrentImageContext()
|
||||
UIGraphicsEndImageContext()
|
||||
return img
|
||||
}()
|
||||
|
||||
private static let cacheDir = URL.documentDir().appendingPathComponent("app-store-search-cache", isDirectory:true)
|
||||
|
||||
private static func local(_ bundleId: String) -> URL {
|
||||
cacheDir.appendingPathComponent("\(bundleId).img")
|
||||
}
|
||||
|
||||
static func initCache() {
|
||||
try? FileManager.default.createDirectory(at: cacheDir, withIntermediateDirectories: true, attributes: nil)
|
||||
}
|
||||
|
||||
static func image(_ bundleId: String?, ifNotStored: (() -> Void)? = nil) -> UIImage? {
|
||||
guard let appId = bundleId else {
|
||||
return unknown
|
||||
}
|
||||
guard let data = try? Data(contentsOf: local(appId)),
|
||||
let img = UIImage(data: data, scale: 2.0) else {
|
||||
ifNotStored?()
|
||||
return appId.hasPrefix("com.apple.") ? apple : unknown
|
||||
}
|
||||
return img
|
||||
}
|
||||
|
||||
static func download(_ bundleId: String, urlStr: String, whenDone: @escaping () -> Void) {
|
||||
if let url = URL(string: urlStr) {
|
||||
url.download(to: local(bundleId), onSuccess: whenDone)
|
||||
}
|
||||
}
|
||||
}
|
||||
166
main/Recordings/TVCAppSearch.swift
Normal file
166
main/Recordings/TVCAppSearch.swift
Normal file
@@ -0,0 +1,166 @@
|
||||
import UIKit
|
||||
|
||||
protocol TVCAppSearchDelegate {
|
||||
func appSearch(didSelect bundleId: String, appName: String?, developer: String?)
|
||||
}
|
||||
|
||||
class TVCAppSearch: UITableViewController, UISearchBarDelegate {
|
||||
|
||||
private var dataSource: [AppStoreSearch.Result] = []
|
||||
private var dataSourceLocal: [AppBundleInfo] = []
|
||||
private var isLoading: Bool = false
|
||||
private var searchActive: Bool = false
|
||||
var delegate: TVCAppSearchDelegate?
|
||||
|
||||
@IBOutlet private var searchBar: UISearchBar!
|
||||
|
||||
override func viewDidLoad() {
|
||||
super.viewDidLoad()
|
||||
BundleIcon.initCache()
|
||||
dataSourceLocal = AppDB?.appBundleList() ?? []
|
||||
}
|
||||
|
||||
override var keyCommands: [UIKeyCommand]? {
|
||||
[UIKeyCommand(input: UIKeyCommand.inputEscape, modifierFlags: [], action: #selector(closeThis))]
|
||||
}
|
||||
|
||||
@objc private func closeThis() {
|
||||
searchBar.endEditing(true)
|
||||
dismiss(animated: true)
|
||||
}
|
||||
|
||||
// MARK: - Table View Data Source
|
||||
|
||||
override func numberOfSections(in _: UITableView) -> Int {
|
||||
dataSourceLocal.count > 0 ? 2 : 1
|
||||
}
|
||||
|
||||
override func tableView(_: UITableView, numberOfRowsInSection section: Int) -> Int {
|
||||
switch section {
|
||||
case 0: return max(1, dataSource.count) + (searchActive ? 1 : 0)
|
||||
case 1: return dataSourceLocal.count
|
||||
default: preconditionFailure()
|
||||
}
|
||||
}
|
||||
|
||||
override func tableView(_ tableView: UITableView, titleForHeaderInSection section: Int) -> String? {
|
||||
switch section {
|
||||
case 0: return "AppStore"
|
||||
case 1: return "Found in other recordings"
|
||||
default: preconditionFailure()
|
||||
}
|
||||
}
|
||||
|
||||
override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
|
||||
let cell = tableView.dequeueReusableCell(withIdentifier: "AppStoreSearchCell")!
|
||||
let bundleId: String
|
||||
let altLoadUrl: String?
|
||||
|
||||
switch indexPath.section {
|
||||
case 0:
|
||||
guard dataSource.count > 0, indexPath.row < dataSource.count else {
|
||||
if indexPath.row == 0 {
|
||||
cell.textLabel?.text = isLoading ? "Loading …" : "no results"
|
||||
cell.isUserInteractionEnabled = false
|
||||
} else {
|
||||
cell.textLabel?.text = "Create manually …"
|
||||
}
|
||||
cell.detailTextLabel?.text = nil
|
||||
cell.imageView?.image = nil
|
||||
return cell
|
||||
}
|
||||
let src = dataSource[indexPath.row]
|
||||
bundleId = src.bundleId
|
||||
altLoadUrl = src.imageURL
|
||||
cell.textLabel?.text = src.name
|
||||
cell.detailTextLabel?.text = src.developer
|
||||
case 1:
|
||||
let src = dataSourceLocal[indexPath.row]
|
||||
bundleId = src.bundleId
|
||||
altLoadUrl = nil
|
||||
cell.textLabel?.text = src.name
|
||||
cell.detailTextLabel?.text = src.author
|
||||
default:
|
||||
preconditionFailure()
|
||||
}
|
||||
|
||||
cell.imageView?.image = BundleIcon.image(bundleId) {
|
||||
guard let url = altLoadUrl else { return }
|
||||
BundleIcon.download(bundleId, urlStr: url) {
|
||||
DispatchQueue.main.async {
|
||||
tableView.reloadRows(at: [indexPath], with: .automatic)
|
||||
}
|
||||
}
|
||||
}
|
||||
cell.isUserInteractionEnabled = true
|
||||
cell.imageView?.layer.cornerRadius = 6.75
|
||||
cell.imageView?.layer.masksToBounds = true
|
||||
return cell
|
||||
}
|
||||
|
||||
override func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
|
||||
tableView.deselectRow(at: indexPath, animated: true)
|
||||
switch indexPath.section {
|
||||
case 0:
|
||||
guard indexPath.row < dataSource.count else {
|
||||
let alert = AskAlert(title: "App Name",
|
||||
text: "Be as descriptive as possible. Preferably use app bundle id if available. Alternatively use app name or a link to a public repository.",
|
||||
buttonText: "Set") {
|
||||
self.delegate?.appSearch(didSelect: "un.known", appName: $0.textFields?.first?.text, developer: nil)
|
||||
self.closeThis()
|
||||
}
|
||||
alert.addTextField { $0.placeholder = "com.apple.notes" }
|
||||
alert.presentIn(self)
|
||||
return
|
||||
}
|
||||
let src = dataSource[indexPath.row]
|
||||
delegate?.appSearch(didSelect: src.bundleId, appName: src.name, developer: src.developer)
|
||||
case 1:
|
||||
let src = dataSourceLocal[indexPath.row]
|
||||
delegate?.appSearch(didSelect: src.bundleId, appName: src.name, developer: src.author)
|
||||
default: preconditionFailure()
|
||||
}
|
||||
closeThis()
|
||||
}
|
||||
|
||||
|
||||
// MARK: - Search Bar Delegate
|
||||
|
||||
func searchBar(_ searchBar: UISearchBar, textDidChange searchText: String) {
|
||||
NSObject.cancelPreviousPerformRequests(withTarget: self, selector: #selector(performSearch), object: nil)
|
||||
isLoading = true
|
||||
tableView.reloadData()
|
||||
if searchText.count > 0 {
|
||||
perform(#selector(performSearch), with: nil, afterDelay: 0.4)
|
||||
} else {
|
||||
performSearch()
|
||||
}
|
||||
}
|
||||
|
||||
/// Internal callback function for delayed text evaluation.
|
||||
/// This way we can avoid unnecessary searches while user is typing.
|
||||
@objc private func performSearch() {
|
||||
isLoading = false
|
||||
let term = searchBar.text?.lowercased() ?? ""
|
||||
searchActive = term.count > 0
|
||||
guard searchActive else {
|
||||
dataSource = []
|
||||
tableView.reloadData()
|
||||
return
|
||||
}
|
||||
AppStoreSearch.search(term) {
|
||||
self.dataSource = $0 ?? []
|
||||
DispatchQueue.main.async {
|
||||
self.tableView.reloadData()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func searchBarSearchButtonClicked(_ searchBar: UISearchBar) {
|
||||
searchBar.endEditing(true)
|
||||
}
|
||||
|
||||
func searchBarCancelButtonClicked(_ searchBar: UISearchBar) {
|
||||
closeThis()
|
||||
}
|
||||
}
|
||||
@@ -64,12 +64,22 @@ 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]
|
||||
cell.textLabel?.text = x.title ?? x.fallbackTitle
|
||||
cell.textLabel?.textColor = (x.title == nil) ? .systemGray : nil
|
||||
cell.detailTextLabel?.text = "at \(DateFormat.seconds(x.start)), duration: \(TimeFormat.from(x.duration ?? 0))"
|
||||
cell.detailTextLabel?.text = "at \(DateFormat.minutes(x.start)), duration: \(TimeFormat.from(x.duration ?? 0))"
|
||||
cell.imageView?.image = x.isLongTerm ? nil : BundleIcon.image(x.appId)
|
||||
cell.imageView?.layer.cornerRadius = 6.75
|
||||
cell.imageView?.layer.masksToBounds = true
|
||||
return cell
|
||||
}
|
||||
|
||||
|
||||
@@ -2,7 +2,7 @@ import UIKit
|
||||
|
||||
class TVCRecordingDetails: UITableViewController, EditActionsRemove {
|
||||
var record: Recording!
|
||||
private lazy var isLongRecording: Bool = (record.duration ?? 0) > Timestamp.hours(1)
|
||||
private lazy var isLongRecording: Bool = record.isLongTerm
|
||||
|
||||
private var showRaw: Bool = false
|
||||
/// Sorted by `ts` in ascending order (oldest first)
|
||||
|
||||
@@ -1,19 +1,42 @@
|
||||
import UIKit
|
||||
|
||||
class VCEditRecording: UIViewController, UITextFieldDelegate, UITextViewDelegate {
|
||||
class VCEditRecording: UIViewController, UITextFieldDelegate, UITextViewDelegate, TVCAppSearchDelegate {
|
||||
|
||||
var record: Recording!
|
||||
var deleteOnCancel: Bool = false
|
||||
var appId: String?
|
||||
|
||||
@IBOutlet private var buttonCancel: UIBarButtonItem!
|
||||
@IBOutlet private var buttonSave: UIBarButtonItem!
|
||||
@IBOutlet private var inputTitle: UITextField!
|
||||
@IBOutlet private var appTitle: UILabel!
|
||||
@IBOutlet private var appDeveloper: UILabel!
|
||||
@IBOutlet private var appIcon: UIImageView!
|
||||
@IBOutlet private var inputNotes: UITextView!
|
||||
@IBOutlet private var inputDetails: UITextView!
|
||||
@IBOutlet private var noteBottom: NSLayoutConstraint!
|
||||
|
||||
@IBOutlet private var chooseAppTap: UITapGestureRecognizer!
|
||||
|
||||
override func viewDidLoad() {
|
||||
inputTitle.placeholder = record.fallbackTitle
|
||||
inputTitle.text = record.title
|
||||
if record.isLongTerm {
|
||||
appId = nil
|
||||
appIcon.image = nil
|
||||
appTitle.text = "Background Recording"
|
||||
appDeveloper.text = nil
|
||||
chooseAppTap.isEnabled = false
|
||||
} else {
|
||||
appId = record.appId
|
||||
appIcon.image = BundleIcon.image(record.appId)
|
||||
appIcon.layer.cornerRadius = 6.75
|
||||
appIcon.layer.masksToBounds = true
|
||||
if record.appId == nil {
|
||||
appTitle.text = "Tap here to choose app"
|
||||
appDeveloper.text = record.title
|
||||
} else {
|
||||
appTitle.text = record.title ?? record.fallbackTitle
|
||||
appDeveloper.text = record.subtitle
|
||||
}
|
||||
}
|
||||
inputNotes.text = record.notes
|
||||
inputDetails.text = """
|
||||
Start: \(DateFormat.seconds(record.start))
|
||||
@@ -31,6 +54,11 @@ class VCEditRecording: UIViewController, UITextFieldDelegate, UITextViewDelegate
|
||||
UIResponder.keyboardWillHideNotification.observe(call: #selector(keyboardWillHide), on: self)
|
||||
}
|
||||
|
||||
override func prepare(for segue: UIStoryboardSegue, sender: Any?) {
|
||||
if let tvc = segue.destination as? TVCAppSearch {
|
||||
tvc.delegate = self
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: Save & Cancel Buttons
|
||||
|
||||
@@ -41,7 +69,15 @@ class VCEditRecording: UIViewController, UITextFieldDelegate, UITextViewDelegate
|
||||
deleteOnCancel = false
|
||||
}
|
||||
QLog.Debug("updating record #\(record.id)")
|
||||
record.title = (inputTitle.text == "") ? nil : inputTitle.text
|
||||
if let id = appId, id != "" {
|
||||
record.appId = id
|
||||
record.title = (appTitle.text == "") ? nil : appTitle.text
|
||||
record.subtitle = (appDeveloper.text == "") ? nil : appDeveloper.text
|
||||
} else {
|
||||
record.appId = nil
|
||||
record.title = nil
|
||||
record.subtitle = nil
|
||||
}
|
||||
record.notes = (inputNotes.text == "") ? nil : inputNotes.text
|
||||
dismiss(animated: true) {
|
||||
RecordingsDB.update(self.record)
|
||||
@@ -121,11 +157,18 @@ class VCEditRecording: UIViewController, UITextFieldDelegate, UITextViewDelegate
|
||||
func textViewDidChange(_ _: UITextView) { validateSaveButton() }
|
||||
|
||||
private func validateSaveButton() {
|
||||
let changed = (inputTitle.text != record.title ?? "" || inputNotes.text != record.notes ?? "")
|
||||
let changed = (appId != record.appId
|
||||
|| (appTitle.text != record.title && appTitle.text != "Tap here to choose app")
|
||||
|| appDeveloper.text != record.subtitle
|
||||
|| inputNotes.text != record.notes ?? "")
|
||||
buttonSave.isEnabled = changed || deleteOnCancel // always allow save for new recordings
|
||||
}
|
||||
|
||||
func textFieldShouldReturn(_ textField: UITextField) -> Bool {
|
||||
textField == inputTitle ? inputNotes.becomeFirstResponder() : true
|
||||
func appSearch(didSelect bundleId: String, appName: String?, developer: String?) {
|
||||
appId = bundleId
|
||||
appTitle.text = appName
|
||||
appDeveloper.text = developer
|
||||
appIcon.image = BundleIcon.image(bundleId)
|
||||
validateSaveButton()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -23,6 +23,8 @@ class VCRecordings: UIViewController, UINavigationControllerDelegate {
|
||||
super.viewWillAppear(animated)
|
||||
if currentRecording != nil { startTimer(animate: false) }
|
||||
navigationController?.setNavigationBarHidden(true, animated: animated)
|
||||
// set hidden in will appear causes UITableViewAlertForLayoutOutsideViewHierarchy
|
||||
// but otherwise navBar is visible during transition
|
||||
}
|
||||
|
||||
override func viewWillDisappear(_ animated: Bool) {
|
||||
@@ -120,7 +122,7 @@ class VCRecordings: UIViewController, UINavigationControllerDelegate {
|
||||
"Use the App as you would normally. Try to get to all corners and functionality the App provides. " +
|
||||
"When you feel that you have captured enough content, come back to ").italic("AppCheck").normal(" and stop the recording." +
|
||||
"\n\n" +
|
||||
"Upon completion you will find your recording in the 'Previous Recordings' section. " +
|
||||
"Upon completion you will find your recording in the section below. " +
|
||||
"You can review your results and remove user specific information if necessary.")
|
||||
))
|
||||
x.addSheet().addArrangedSubview(QuickUI.text(attributed: NSMutableAttributedString()
|
||||
|
||||
@@ -1,129 +0,0 @@
|
||||
import Foundation
|
||||
import UIKit
|
||||
|
||||
private let fm = FileManager.default
|
||||
private let documentsDir = try! fm.url(for: .documentDirectory, in: .userDomainMask, appropriateFor: nil, create: true)
|
||||
private let bundleInfoDir = documentsDir.appendingPathComponent("bundleInfo", isDirectory:true)
|
||||
|
||||
|
||||
struct AppInfoType : Decodable {
|
||||
var id: String
|
||||
var name: String?
|
||||
var seller: String?
|
||||
var imageURL: URL?
|
||||
private var remoteImgURL: String?
|
||||
private var cache: Bool?
|
||||
private let localJSON: URL
|
||||
private let localImgURL: URL
|
||||
|
||||
static func initWorkingDir() {
|
||||
try? fm.createDirectory(at: bundleInfoDir, withIntermediateDirectories: true, attributes: nil)
|
||||
// print("init dir: \(bundleInfoDir)")
|
||||
}
|
||||
|
||||
init(id: String) {
|
||||
self.id = id
|
||||
if id == "" {
|
||||
name = "–?–"
|
||||
cache = true
|
||||
localJSON = URL(fileURLWithPath: "")
|
||||
localImgURL = localJSON
|
||||
} else {
|
||||
localJSON = bundleInfoDir.appendingPathComponent("\(id).json")
|
||||
localImgURL = bundleInfoDir.appendingPathComponent("\(id).img")
|
||||
reload()
|
||||
}
|
||||
}
|
||||
|
||||
mutating func reload() {
|
||||
if fm.fileExists(atPath: localImgURL.path) {
|
||||
imageURL = localImgURL
|
||||
}
|
||||
guard name == nil, seller == nil,
|
||||
fm.fileExists(atPath: localJSON.path),
|
||||
let attr = try? fm.attributesOfItem(atPath: localJSON.path),
|
||||
attr[FileAttributeKey.size] as! UInt64 > 0 else
|
||||
{
|
||||
// process json only if attributes not set yet,
|
||||
// OR json doesn't exist, OR json is empty
|
||||
return
|
||||
}
|
||||
(name, seller, remoteImgURL) = parseJSON(localJSON)
|
||||
|
||||
if remoteImgURL == nil || imageURL != nil {
|
||||
cache = true
|
||||
}
|
||||
}
|
||||
|
||||
func getImage() -> UIImage? {
|
||||
if let img = imageURL, let data = try? Data(contentsOf: img) {
|
||||
return UIImage(data: data, scale: 2.0)
|
||||
} else if id.hasPrefix("com.apple.") {
|
||||
return appIconApple
|
||||
} else {
|
||||
return appIconUnknown
|
||||
}
|
||||
}
|
||||
|
||||
private func parseJSON(_ location: URL) -> (name: String?, seller: String?, image: String?) {
|
||||
do {
|
||||
let data = try Data.init(contentsOf: location)
|
||||
if
|
||||
let json = try JSONSerialization.jsonObject(with: data, options: .fragmentsAllowed) as? [String: Any],
|
||||
let resAll = json["results"] as? [Any],
|
||||
let res = resAll.first as? [String: Any]
|
||||
{
|
||||
let name = res["trackName"] as? String // trackCensoredName
|
||||
let seller = res["sellerName"] as? String // artistName
|
||||
let image = res["artworkUrl60"] as? String // artworkUrl100
|
||||
return (name, seller, image)
|
||||
} else if id.hasPrefix("com.apple.") {
|
||||
return (String(id.dropFirst(10)), "Apple Inc.", nil)
|
||||
}
|
||||
} catch {}
|
||||
return (nil, nil, nil)
|
||||
}
|
||||
|
||||
mutating func updateIfNeeded(_ updateClosure: () -> Void) {
|
||||
guard cache == nil,
|
||||
let safeId = id.addingPercentEncoding(withAllowedCharacters: .urlQueryAllowed) else {
|
||||
return
|
||||
}
|
||||
cache = false // meaning: hasn't downloaded yet, but is about to do
|
||||
// print("downloading \(id)")
|
||||
_ = downloadURL("https://itunes.apple.com/lookup?bundleId=\(safeId)", toFile: localJSON).flatMap{
|
||||
// print("downloading \(id) done.")
|
||||
reload()
|
||||
updateClosure()
|
||||
return downloadURL(remoteImgURL, toFile: localImgURL)
|
||||
}.map{
|
||||
// print("downloading \(id) image done.")
|
||||
reload()
|
||||
updateClosure()
|
||||
}
|
||||
}
|
||||
|
||||
enum NetworkError: Error {
|
||||
case url
|
||||
}
|
||||
|
||||
private func downloadURL(_ urlStr: String?, toFile: URL) -> Result<Void, Error> {
|
||||
guard let urlStr = urlStr, let url = URL(string: urlStr) else {
|
||||
return .failure(NetworkError.url)
|
||||
}
|
||||
var result: Result<Void, Error>!
|
||||
let semaphore = DispatchSemaphore(value: 0)
|
||||
URLSession.shared.downloadTask(with: url) { location, response, error in
|
||||
if let loc = location {
|
||||
try? fm.removeItem(at: toFile)
|
||||
try? fm.moveItem(at: loc, to: toFile)
|
||||
result = .success(())
|
||||
} else {
|
||||
result = .failure(error!)
|
||||
}
|
||||
semaphore.signal()
|
||||
}.resume()
|
||||
_ = semaphore.wait(wallTimeout: .distantFuture)
|
||||
return result
|
||||
}
|
||||
}
|
||||
@@ -1,78 +0,0 @@
|
||||
import UIKit
|
||||
|
||||
let appIconApple = generateAppleIcon()
|
||||
let appIconUnknown = generateUnknownIcon()
|
||||
|
||||
func generateAppleIcon() -> UIImage? {
|
||||
let rect = CGRect(x: 0, y: 0, width: 30, height: 30)
|
||||
UIGraphicsBeginImageContextWithOptions(rect.size, false, 0.0)
|
||||
|
||||
// #colorLiteral(red: 0.5843137503, green: 0.8235294223, blue: 0.4196078479, alpha: 1).setFill()
|
||||
// UIBezierPath(roundedRect: rect, cornerRadius: 0).fill()
|
||||
// print("drawing")
|
||||
let fs = 36 as CGFloat
|
||||
let hFont = UIFont.systemFont(ofSize: fs)
|
||||
var attrib = [
|
||||
NSAttributedString.Key.font: hFont,
|
||||
NSAttributedString.Key.foregroundColor: UIColor.gray
|
||||
]
|
||||
|
||||
let str = "" as NSString
|
||||
let actualHeight = str.size(withAttributes: attrib).height
|
||||
attrib[NSAttributedString.Key.font] = hFont.withSize(fs * fs / actualHeight)
|
||||
|
||||
let strW = str.size(withAttributes: attrib).width
|
||||
str.draw(at: CGPoint(x: (rect.size.width - strW) / 2.0, y: -3), withAttributes: attrib)
|
||||
|
||||
let img = UIGraphicsGetImageFromCurrentImageContext()
|
||||
UIGraphicsEndImageContext()
|
||||
return img
|
||||
}
|
||||
|
||||
func generateUnknownIcon() -> UIImage? {
|
||||
let rect = CGRect(x: 0, y: 0, width: 30, height: 30)
|
||||
UIGraphicsBeginImageContextWithOptions(rect.size, false, 0.0)
|
||||
|
||||
let context = UIGraphicsGetCurrentContext()!
|
||||
let lineWidth: CGFloat = 0.5
|
||||
let corner: CGFloat = 6.75
|
||||
let c = corner / CGFloat.pi + lineWidth/2
|
||||
let sz: CGFloat = rect.height
|
||||
let m = sz / 2
|
||||
let r1 = 0.2 * sz, r2 = sqrt(2 * r1 * r1)
|
||||
|
||||
// diagonal
|
||||
context.lineFromTo(x1: c, y1: c, x2: sz-c, y2: sz-c)
|
||||
context.lineFromTo(x1: c, y1: sz-c, x2: sz-c, y2: c)
|
||||
// horizontal
|
||||
context.lineFromTo(x1: 0, y1: m, x2: sz, y2: m)
|
||||
context.lineFromTo(x1: 0, y1: m + r1, x2: sz, y2: m + r1)
|
||||
context.lineFromTo(x1: 0, y1: m - r1, x2: sz, y2: m - r1)
|
||||
// vertical
|
||||
context.lineFromTo(x1: m, y1: 0, x2: m, y2: sz)
|
||||
context.lineFromTo(x1: m + r1, y1: 0, x2: m + r1, y2: sz)
|
||||
context.lineFromTo(x1: m - r1, y1: 0, x2: m - r1, y2: sz)
|
||||
// circles
|
||||
context.addEllipse(in: CGRect(x: m - r1, y: m - r1, width: 2*r1, height: 2*r1))
|
||||
context.addEllipse(in: CGRect(x: m - r2, y: m - r2, width: 2*r2, height: 2*r2))
|
||||
let r3 = CGRect(x: c, y: c, width: sz - 2*c, height: sz - 2*c)
|
||||
context.addEllipse(in: r3)
|
||||
context.addRect(r3)
|
||||
|
||||
UIColor.clear.setFill()
|
||||
UIColor.gray.setStroke()
|
||||
let rounded = UIBezierPath(roundedRect: rect.insetBy(dx: lineWidth/2, dy: lineWidth/2), cornerRadius: corner)
|
||||
rounded.lineWidth = lineWidth
|
||||
rounded.stroke()
|
||||
|
||||
let img = UIGraphicsGetImageFromCurrentImageContext()
|
||||
UIGraphicsEndImageContext()
|
||||
return img
|
||||
}
|
||||
|
||||
extension CGContext {
|
||||
func lineFromTo(x1: CGFloat, y1: CGFloat, x2: CGFloat, y2: CGFloat) {
|
||||
self.move(to: CGPoint(x: x1, y: y1))
|
||||
self.addLine(to: CGPoint(x: x2, y: y2))
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user