Merge branch 'release/0.2.2'
This commit is contained in:
commit
02fe05c32f
32 changed files with 664 additions and 400 deletions
|
|
@ -14,21 +14,24 @@ testing. Thank you.
|
|||
|
||||
## Features
|
||||
|
||||
- Try to be compatible with Password Store command line tool
|
||||
- Try to be compatible with the Password Store command line tool
|
||||
- Support to view, copy, add, edit password entries
|
||||
- Encrypt and decrypt password entries by PGP keys
|
||||
- Synchronize with you password Git repository
|
||||
- Synchronize with your password Git repository
|
||||
- User-friendly interface: search, long press to copy, copy and open link, etc.
|
||||
- Support one-time password (OTP) tokens
|
||||
- Written in Swift
|
||||
- No need to jailbreak your devices
|
||||
- Get from App Store (stay tuned, under review)
|
||||
- Get from App Store (stay tuned)
|
||||
|
||||
## Screenshots
|
||||
|
||||
<p>
|
||||
<img src="screenshot/preview.gif" width="200"/>
|
||||
<img src="screenshot/screenshot1.png" width="200"/>
|
||||
<img src="screenshot/screenshot2.png" width="200"/>
|
||||
<img src="screenshot/screenshot3.png" width="200"/>
|
||||
</p>
|
||||
|
||||
## Build
|
||||
|
||||
|
|
|
|||
|
|
@ -1,11 +1,11 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<document type="com.apple.InterfaceBuilder3.CocoaTouch.Storyboard.XIB" version="3.0" toolsVersion="12106.1" systemVersion="16E183b" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" useTraitCollections="YES" colorMatched="YES" initialViewController="YoR-iB-XAd">
|
||||
<document type="com.apple.InterfaceBuilder3.CocoaTouch.Storyboard.XIB" version="3.0" toolsVersion="11762" systemVersion="16D32" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" useTraitCollections="YES" colorMatched="YES" initialViewController="YoR-iB-XAd">
|
||||
<device id="retina5_5" orientation="portrait">
|
||||
<adaptation id="fullscreen"/>
|
||||
</device>
|
||||
<dependencies>
|
||||
<deployment identifier="iOS"/>
|
||||
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="12074.1"/>
|
||||
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="11757"/>
|
||||
<capability name="Constraints to layout margins" minToolsVersion="6.0"/>
|
||||
<capability name="documents saved in the Xcode 8 format" minToolsVersion="8.0"/>
|
||||
</dependencies>
|
||||
|
|
@ -288,20 +288,20 @@
|
|||
<rect key="frame" x="0.0" y="28" width="414" height="44"/>
|
||||
<autoresizingMask key="autoresizingMask"/>
|
||||
<tableViewCellContentView key="contentView" opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center" tableViewCell="OfB-1N-1Am" id="fh0-au-C6q">
|
||||
<rect key="frame" x="0.0" y="0.0" width="414" height="43.5"/>
|
||||
<rect key="frame" x="0.0" y="0.0" width="414" height="44"/>
|
||||
<autoresizingMask key="autoresizingMask"/>
|
||||
<subviews>
|
||||
<label opaque="NO" userInteractionEnabled="NO" tag="101" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="00/00/00" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="hj4-EP-LFW">
|
||||
<rect key="frame" x="15" y="8" width="67" height="28"/>
|
||||
<label opaque="NO" userInteractionEnabled="NO" tag="101" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="00/00/0000" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="hj4-EP-LFW">
|
||||
<rect key="frame" x="15" y="8" width="89" height="28"/>
|
||||
<constraints>
|
||||
<constraint firstAttribute="width" constant="67" id="jNO-Xy-H99"/>
|
||||
<constraint firstAttribute="width" constant="89" id="jNO-Xy-H99"/>
|
||||
</constraints>
|
||||
<fontDescription key="fontDescription" type="system" pointSize="15"/>
|
||||
<color key="textColor" red="0.5568627451" green="0.5568627451" blue="0.57647058819999997" alpha="1" colorSpace="calibratedRGB"/>
|
||||
<nil key="highlightedColor"/>
|
||||
</label>
|
||||
<label opaque="NO" userInteractionEnabled="NO" tag="102" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="Edit password for totp-secret/github.com using vi." textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="VFa-a4-XsP">
|
||||
<rect key="frame" x="82" y="12.666666666666664" width="324" height="18"/>
|
||||
<rect key="frame" x="104" y="12.666666666666664" width="302" height="18"/>
|
||||
<fontDescription key="fontDescription" type="system" pointSize="15"/>
|
||||
<nil key="textColor"/>
|
||||
<nil key="highlightedColor"/>
|
||||
|
|
@ -340,10 +340,10 @@
|
|||
<tableViewSection headerTitle="Git Repository URL" id="pbe-W6-w4V">
|
||||
<cells>
|
||||
<tableViewCell clipsSubviews="YES" contentMode="scaleToFill" selectionStyle="none" indentationWidth="10" reuseIdentifier="gitRepositoryURLTabelViewCell" rowHeight="52" id="FRr-pf-aPO">
|
||||
<rect key="frame" x="0.0" y="55.5" width="414" height="52"/>
|
||||
<rect key="frame" x="0.0" y="55" width="414" height="52"/>
|
||||
<autoresizingMask key="autoresizingMask"/>
|
||||
<tableViewCellContentView key="contentView" opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center" tableViewCell="FRr-pf-aPO" id="60A-PS-qGe">
|
||||
<rect key="frame" x="0.0" y="0.0" width="414" height="52"/>
|
||||
<rect key="frame" x="0.0" y="0.0" width="414" height="51"/>
|
||||
<autoresizingMask key="autoresizingMask"/>
|
||||
<subviews>
|
||||
<textField opaque="NO" clipsSubviews="YES" contentMode="scaleToFill" contentHorizontalAlignment="left" contentVerticalAlignment="center" placeholder="Git Repository URL" textAlignment="natural" minimumFontSize="17" clearButtonMode="whileEditing" translatesAutoresizingMaskIntoConstraints="NO" id="EVT-VU-sCi">
|
||||
|
|
@ -366,10 +366,10 @@
|
|||
<tableViewSection headerTitle="Username" id="fRu-A2-SCk">
|
||||
<cells>
|
||||
<tableViewCell clipsSubviews="YES" contentMode="scaleToFill" selectionStyle="none" indentationWidth="10" reuseIdentifier="usernameTableVIewCell" rowHeight="52" id="tnj-5U-kMB">
|
||||
<rect key="frame" x="0.0" y="163.5" width="414" height="52"/>
|
||||
<rect key="frame" x="0.0" y="163" width="414" height="52"/>
|
||||
<autoresizingMask key="autoresizingMask"/>
|
||||
<tableViewCellContentView key="contentView" opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center" tableViewCell="tnj-5U-kMB" id="f0c-pI-MSJ">
|
||||
<rect key="frame" x="0.0" y="0.0" width="414" height="52"/>
|
||||
<rect key="frame" x="0.0" y="0.0" width="414" height="51"/>
|
||||
<autoresizingMask key="autoresizingMask"/>
|
||||
<subviews>
|
||||
<textField opaque="NO" clipsSubviews="YES" contentMode="scaleToFill" contentHorizontalAlignment="left" contentVerticalAlignment="center" placeholder="Username" textAlignment="natural" minimumFontSize="17" clearButtonMode="whileEditing" translatesAutoresizingMaskIntoConstraints="NO" id="TMg-Gk-7nG">
|
||||
|
|
@ -392,10 +392,10 @@
|
|||
<tableViewSection headerTitle="Authentication Method" id="h0N-tI-shZ">
|
||||
<cells>
|
||||
<tableViewCell clipsSubviews="YES" contentMode="scaleToFill" selectionStyle="blue" hidesAccessoryWhenEditing="NO" indentationLevel="2" indentationWidth="0.0" shouldIndentWhileEditing="NO" id="KrP-nb-haa">
|
||||
<rect key="frame" x="0.0" y="271.5" width="414" height="44"/>
|
||||
<rect key="frame" x="0.0" y="271" width="414" height="44"/>
|
||||
<autoresizingMask key="autoresizingMask"/>
|
||||
<tableViewCellContentView key="contentView" opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center" tableViewCell="KrP-nb-haa" id="1uB-oE-kfI">
|
||||
<rect key="frame" x="0.0" y="0.0" width="414" height="44"/>
|
||||
<rect key="frame" x="0.0" y="0.0" width="414" height="43"/>
|
||||
<autoresizingMask key="autoresizingMask"/>
|
||||
<subviews>
|
||||
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="Password" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="LfQ-Af-j2O">
|
||||
|
|
@ -423,10 +423,10 @@
|
|||
<inset key="separatorInset" minX="62" minY="0.0" maxX="0.0" maxY="0.0"/>
|
||||
</tableViewCell>
|
||||
<tableViewCell clipsSubviews="YES" contentMode="scaleToFill" selectionStyle="blue" accessoryType="detailButton" hidesAccessoryWhenEditing="NO" indentationLevel="2" indentationWidth="0.0" shouldIndentWhileEditing="NO" id="Qmt-bo-CuJ">
|
||||
<rect key="frame" x="0.0" y="315.5" width="414" height="44"/>
|
||||
<rect key="frame" x="0.0" y="315" width="414" height="44"/>
|
||||
<autoresizingMask key="autoresizingMask"/>
|
||||
<tableViewCellContentView key="contentView" opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center" tableViewCell="Qmt-bo-CuJ" id="p3u-8b-h3U">
|
||||
<rect key="frame" x="0.0" y="0.0" width="367" height="44"/>
|
||||
<rect key="frame" x="0.0" y="0.0" width="367" height="43"/>
|
||||
<autoresizingMask key="autoresizingMask"/>
|
||||
<subviews>
|
||||
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="SSH Key" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="Ezz-76-a53">
|
||||
|
|
@ -503,7 +503,7 @@
|
|||
<rect key="frame" x="0.0" y="35" width="414" height="52"/>
|
||||
<autoresizingMask key="autoresizingMask"/>
|
||||
<tableViewCellContentView key="contentView" opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center" tableViewCell="BYZ-9g-xZy" id="Zfn-rK-sN1">
|
||||
<rect key="frame" x="0.0" y="0.0" width="414" height="51.5"/>
|
||||
<rect key="frame" x="0.0" y="0.0" width="414" height="52"/>
|
||||
<autoresizingMask key="autoresizingMask"/>
|
||||
<subviews>
|
||||
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="Public Key URL" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="dWi-eh-7Eq">
|
||||
|
|
@ -533,7 +533,7 @@
|
|||
<rect key="frame" x="0.0" y="87" width="414" height="52"/>
|
||||
<autoresizingMask key="autoresizingMask"/>
|
||||
<tableViewCellContentView key="contentView" opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center" tableViewCell="vpk-J8-j7t" id="1td-qT-6ts">
|
||||
<rect key="frame" x="0.0" y="0.0" width="414" height="51.5"/>
|
||||
<rect key="frame" x="0.0" y="0.0" width="414" height="52"/>
|
||||
<autoresizingMask key="autoresizingMask"/>
|
||||
<subviews>
|
||||
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="Private Key URL" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="Qht-RC-Yeg">
|
||||
|
|
@ -727,6 +727,9 @@
|
|||
</connections>
|
||||
</barButtonItem>
|
||||
</navigationItem>
|
||||
<connections>
|
||||
<segue destination="HB6-Yu-Y3J" kind="unwind" identifier="deletePasswordSegue" unwindAction="deletePasswordWithSegue:" id="L1Z-64-EZh"/>
|
||||
</connections>
|
||||
</tableViewController>
|
||||
<placeholder placeholderIdentifier="IBFirstResponder" id="HlX-6r-eOU" userLabel="First Responder" sceneMemberID="firstResponder"/>
|
||||
<exit id="HB6-Yu-Y3J" userLabel="Exit" sceneMemberID="exit"/>
|
||||
|
|
@ -788,7 +791,7 @@
|
|||
<rect key="frame" x="0.0" y="35" width="414" height="52"/>
|
||||
<autoresizingMask key="autoresizingMask"/>
|
||||
<tableViewCellContentView key="contentView" opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center" tableViewCell="Jy1-4S-Lvf" id="tJE-ww-okf">
|
||||
<rect key="frame" x="0.0" y="0.0" width="414" height="51.5"/>
|
||||
<rect key="frame" x="0.0" y="0.0" width="414" height="52"/>
|
||||
<autoresizingMask key="autoresizingMask"/>
|
||||
<subviews>
|
||||
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="Public Key URL" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="Oys-xP-ZrB">
|
||||
|
|
@ -818,7 +821,7 @@
|
|||
<rect key="frame" x="0.0" y="87" width="414" height="52"/>
|
||||
<autoresizingMask key="autoresizingMask"/>
|
||||
<tableViewCellContentView key="contentView" opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center" tableViewCell="MA5-lE-8dT" id="pTv-Wj-psC">
|
||||
<rect key="frame" x="0.0" y="0.0" width="414" height="51.5"/>
|
||||
<rect key="frame" x="0.0" y="0.0" width="414" height="52"/>
|
||||
<autoresizingMask key="autoresizingMask"/>
|
||||
<subviews>
|
||||
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="Private Key URL" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="C2w-dd-roS">
|
||||
|
|
@ -848,7 +851,7 @@
|
|||
<rect key="frame" x="0.0" y="139" width="414" height="52"/>
|
||||
<autoresizingMask key="autoresizingMask"/>
|
||||
<tableViewCellContentView key="contentView" opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center" tableViewCell="yER-vT-YTO" id="Mip-zw-xLu">
|
||||
<rect key="frame" x="0.0" y="0.0" width="414" height="51.5"/>
|
||||
<rect key="frame" x="0.0" y="0.0" width="414" height="52"/>
|
||||
<autoresizingMask key="autoresizingMask"/>
|
||||
<subviews>
|
||||
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="Passphrase" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="Rnj-2x-ksO">
|
||||
|
|
@ -950,7 +953,28 @@
|
|||
<rect key="frame" x="0.0" y="0.0" width="414" height="43.666666666666664"/>
|
||||
<autoresizingMask key="autoresizingMask"/>
|
||||
<subviews>
|
||||
<label opaque="NO" multipleTouchEnabled="YES" contentMode="left" text="Discard All Local Changes" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" id="Jwg-mt-woS">
|
||||
<label opaque="NO" multipleTouchEnabled="YES" contentMode="left" text="Encrypt in ASCII-Armored" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" id="Jwg-mt-woS">
|
||||
<rect key="frame" x="15" y="0.0" width="384" height="43.666666666666664"/>
|
||||
<autoresizingMask key="autoresizingMask"/>
|
||||
<fontDescription key="fontDescription" type="system" pointSize="17"/>
|
||||
<nil key="textColor"/>
|
||||
<nil key="highlightedColor"/>
|
||||
</label>
|
||||
</subviews>
|
||||
</tableViewCellContentView>
|
||||
</tableViewCell>
|
||||
</cells>
|
||||
</tableViewSection>
|
||||
<tableViewSection id="aVR-FE-jMg">
|
||||
<cells>
|
||||
<tableViewCell clipsSubviews="YES" contentMode="scaleToFill" selectionStyle="default" indentationWidth="10" textLabel="zrl-v3-fxg" style="IBUITableViewCellStyleDefault" id="Jm8-B5-wKx" userLabel="Discard Changes Table View Cell">
|
||||
<rect key="frame" x="0.0" y="115" width="414" height="44"/>
|
||||
<autoresizingMask key="autoresizingMask"/>
|
||||
<tableViewCellContentView key="contentView" opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center" tableViewCell="Jm8-B5-wKx" id="tjS-Q6-y2M">
|
||||
<rect key="frame" x="0.0" y="0.0" width="414" height="43.666666666666664"/>
|
||||
<autoresizingMask key="autoresizingMask"/>
|
||||
<subviews>
|
||||
<label opaque="NO" multipleTouchEnabled="YES" contentMode="left" text="Discard All Local Changes" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" id="zrl-v3-fxg">
|
||||
<rect key="frame" x="15" y="0.0" width="384" height="43.666666666666664"/>
|
||||
<autoresizingMask key="autoresizingMask"/>
|
||||
<fontDescription key="fontDescription" type="system" pointSize="17"/>
|
||||
|
|
@ -965,7 +989,7 @@
|
|||
<tableViewSection id="ujw-Wl-vs1">
|
||||
<cells>
|
||||
<tableViewCell clipsSubviews="YES" contentMode="scaleToFill" selectionStyle="default" indentationWidth="10" textLabel="K2K-Bx-g7Z" style="IBUITableViewCellStyleDefault" id="NI1-Kd-hyH">
|
||||
<rect key="frame" x="0.0" y="115" width="414" height="44"/>
|
||||
<rect key="frame" x="0.0" y="195" width="414" height="44"/>
|
||||
<autoresizingMask key="autoresizingMask"/>
|
||||
<tableViewCellContentView key="contentView" opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center" tableViewCell="NI1-Kd-hyH" id="yLe-T2-TWF">
|
||||
<rect key="frame" x="0.0" y="0.0" width="414" height="43.666666666666664"/>
|
||||
|
|
@ -990,7 +1014,8 @@
|
|||
</connections>
|
||||
</tableView>
|
||||
<connections>
|
||||
<outlet property="discardChangesTableViewCell" destination="tHt-Ro-0HF" id="E7G-0v-MBB"/>
|
||||
<outlet property="discardChangesTableViewCell" destination="Jm8-B5-wKx" id="rfA-G3-2OE"/>
|
||||
<outlet property="encryptInASCIIArmoredTableViewCell" destination="tHt-Ro-0HF" id="tOi-Sj-mLJ"/>
|
||||
<outlet property="eraseDataTableViewCell" destination="NI1-Kd-hyH" id="NtJ-f4-oxb"/>
|
||||
</connections>
|
||||
</tableViewController>
|
||||
|
|
@ -1110,7 +1135,7 @@
|
|||
<rect key="frame" x="0.0" y="35" width="414" height="170"/>
|
||||
<autoresizingMask key="autoresizingMask"/>
|
||||
<tableViewCellContentView key="contentView" opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center" tableViewCell="Pv0-ev-stj" id="ywz-II-W1g">
|
||||
<rect key="frame" x="0.0" y="0.0" width="414" height="169.5"/>
|
||||
<rect key="frame" x="0.0" y="0.0" width="414" height="170"/>
|
||||
<autoresizingMask key="autoresizingMask"/>
|
||||
<subviews>
|
||||
<textView clipsSubviews="YES" userInteractionEnabled="NO" contentMode="scaleToFill" bounces="NO" scrollEnabled="NO" showsHorizontalScrollIndicator="NO" showsVerticalScrollIndicator="NO" delaysContentTouches="NO" canCancelContentTouches="NO" bouncesZoom="NO" editable="NO" usesAttributedText="YES" selectable="NO" translatesAutoresizingMaskIntoConstraints="NO" id="xzQ-5d-kdL">
|
||||
|
|
@ -1168,7 +1193,7 @@
|
|||
<rect key="frame" x="0.0" y="261" width="414" height="160"/>
|
||||
<autoresizingMask key="autoresizingMask"/>
|
||||
<tableViewCellContentView key="contentView" opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center" tableViewCell="Lom-iT-l16" id="eya-Tv-r0q">
|
||||
<rect key="frame" x="0.0" y="0.0" width="414" height="159.5"/>
|
||||
<rect key="frame" x="0.0" y="0.0" width="414" height="160"/>
|
||||
<autoresizingMask key="autoresizingMask"/>
|
||||
<subviews>
|
||||
<textView clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="scaleToFill" translatesAutoresizingMaskIntoConstraints="NO" id="oyB-oI-1fS">
|
||||
|
|
@ -1224,7 +1249,7 @@ pfZ36xQbOAQYKKf6ZTT5R/Y=
|
|||
<rect key="frame" x="0.0" y="477" width="414" height="160"/>
|
||||
<autoresizingMask key="autoresizingMask"/>
|
||||
<tableViewCellContentView key="contentView" opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center" tableViewCell="J8U-ev-FRQ" id="eb0-vb-Fcc">
|
||||
<rect key="frame" x="0.0" y="0.0" width="414" height="159.5"/>
|
||||
<rect key="frame" x="0.0" y="0.0" width="414" height="160"/>
|
||||
<autoresizingMask key="autoresizingMask"/>
|
||||
<subviews>
|
||||
<textView clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="scaleToFill" translatesAutoresizingMaskIntoConstraints="NO" id="lrQ-Ln-ZOv">
|
||||
|
|
|
|||
|
|
@ -10,26 +10,19 @@ import UIKit
|
|||
|
||||
class AboutRepositoryTableViewController: BasicStaticTableViewController {
|
||||
|
||||
var needRefresh = false
|
||||
var indicatorLabel: UILabel!
|
||||
var indicator: UIActivityIndicatorView!
|
||||
let passwordStore = PasswordStore.shared
|
||||
private var needRefresh = false
|
||||
private var indicator: UIActivityIndicatorView = {
|
||||
let indicator = UIActivityIndicatorView(activityIndicatorStyle: .gray)
|
||||
return indicator
|
||||
}()
|
||||
private let passwordStore = PasswordStore.shared
|
||||
|
||||
override func viewDidLoad() {
|
||||
navigationItemTitle = "About Repository"
|
||||
super.viewDidLoad()
|
||||
|
||||
indicatorLabel = UILabel(frame: CGRect(x: 0, y: 0, width: view.frame.width, height: 21))
|
||||
indicatorLabel.center = CGPoint(x: view.frame.size.width / 2, y: view.frame.size.height * 0.382 + 22)
|
||||
indicatorLabel.backgroundColor = UIColor.clear
|
||||
indicatorLabel.textColor = UIColor.gray
|
||||
indicatorLabel.text = "calculating"
|
||||
indicatorLabel.textAlignment = .center
|
||||
indicatorLabel.font = UIFont.preferredFont(forTextStyle: .footnote)
|
||||
indicator = UIActivityIndicatorView(activityIndicatorStyle: .gray)
|
||||
indicator.center = CGPoint(x: view.frame.size.width / 2, y: view.frame.size.height * 0.382)
|
||||
|
||||
indicator.center = CGPoint(x: view.bounds.midX, y: view.bounds.height * 0.382)
|
||||
tableView.addSubview(indicator)
|
||||
tableView.addSubview(indicatorLabel)
|
||||
|
||||
setTableData()
|
||||
|
||||
|
|
@ -40,7 +33,6 @@ class AboutRepositoryTableViewController: BasicStaticTableViewController {
|
|||
override func viewWillAppear(_ animated: Bool) {
|
||||
super.viewWillAppear(animated)
|
||||
if needRefresh {
|
||||
indicatorLabel.text = "reloading"
|
||||
setTableData()
|
||||
needRefresh = false
|
||||
}
|
||||
|
|
@ -51,45 +43,32 @@ class AboutRepositoryTableViewController: BasicStaticTableViewController {
|
|||
// clear current contents (if any)
|
||||
self.tableData.removeAll(keepingCapacity: true)
|
||||
self.tableView.reloadData()
|
||||
indicatorLabel.isHidden = false
|
||||
indicator.startAnimating()
|
||||
|
||||
// reload the table
|
||||
DispatchQueue.global(qos: .userInitiated).async {
|
||||
let numberFormatter = NumberFormatter()
|
||||
numberFormatter.numberStyle = NumberFormatter.Style.decimal
|
||||
let fm = FileManager.default
|
||||
|
||||
let passwordEntities = self.passwordStore.fetchPasswordEntityCoreData(withDir: false)
|
||||
let numberOfPasswords = numberFormatter.string(from: NSNumber(value: passwordEntities.count))!
|
||||
|
||||
var size = UInt64(0)
|
||||
do {
|
||||
if fm.fileExists(atPath: self.passwordStore.storeURL.path) {
|
||||
size = try fm.allocatedSizeOfDirectoryAtURL(directoryURL: self.passwordStore.storeURL)
|
||||
}
|
||||
} catch {
|
||||
print(error)
|
||||
}
|
||||
let sizeOfRepository = ByteCountFormatter.string(fromByteCount: Int64(size), countStyle: ByteCountFormatter.CountStyle.file)
|
||||
let numberOfPasswordsString = numberFormatter.string(from: NSNumber(value: self.passwordStore.numberOfPasswords))!
|
||||
let sizeOfRepositoryString = ByteCountFormatter.string(fromByteCount: Int64(self.passwordStore.sizeOfRepositoryByteCount), countStyle: ByteCountFormatter.CountStyle.file)
|
||||
|
||||
let numberOfCommits = self.passwordStore.storeRepository?.numberOfCommits(inCurrentBranch: NSErrorPointer(nilLiteral: ())) ?? 0
|
||||
let numberOfCommitsString = numberFormatter.string(from: NSNumber(value: numberOfCommits))!
|
||||
|
||||
|
||||
DispatchQueue.main.async { [weak self] in
|
||||
let type = UITableViewCellAccessoryType.none
|
||||
self?.tableData = [
|
||||
// section 0
|
||||
[[.style: CellDataStyle.value1, .accessoryType: type, .title: "Passwords", .detailText: numberOfPasswords],
|
||||
[.style: CellDataStyle.value1, .accessoryType: type, .title: "Size", .detailText: sizeOfRepository], [.style: CellDataStyle.value1, .accessoryType: type, .title: "Unsynced", .detailText: String(self?.passwordStore.getNumberOfUnsyncedPasswords() ?? 0)],
|
||||
[[.style: CellDataStyle.value1, .accessoryType: type, .title: "Passwords", .detailText: numberOfPasswordsString],
|
||||
[.style: CellDataStyle.value1, .accessoryType: type, .title: "Size", .detailText: sizeOfRepositoryString],
|
||||
[.style: CellDataStyle.value1, .accessoryType: type, .title: "Local Commits", .detailText: String(self?.passwordStore.numberOfLocalCommits() ?? 0)],
|
||||
[.style: CellDataStyle.value1, .accessoryType: type, .title: "Last Synced", .detailText: Utils.getLastUpdatedTimeString()],
|
||||
[.style: CellDataStyle.value1, .accessoryType: type, .title: "Commits", .detailText: numberOfCommitsString],
|
||||
[.title: "Commit Logs", .action: "segue", .link: "showCommitLogsSegue"],
|
||||
],
|
||||
]
|
||||
self?.indicator.stopAnimating()
|
||||
self?.indicatorLabel.isHidden = true
|
||||
self?.tableView.reloadData()
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -14,6 +14,7 @@ class AboutTableViewController: BasicStaticTableViewController {
|
|||
tableData = [
|
||||
// section 0
|
||||
[[.title: "Website", .action: "link", .link: "https://github.com/mssun/pass-ios.git"],
|
||||
[.title: "Help", .action: "link", .link: "https://github.com/mssun/passforios/wiki"],
|
||||
[.title: "Contact Developer", .action: "link", .link: "mailto:bob@mssun.me?subject=passforiOS"],],
|
||||
|
||||
// section 1,
|
||||
|
|
|
|||
|
|
@ -10,8 +10,6 @@ import UIKit
|
|||
import SwiftyUserDefaults
|
||||
|
||||
class AddPasswordTableViewController: PasswordEditorTableViewController {
|
||||
|
||||
var password: Password?
|
||||
var tempContent: String = ""
|
||||
let passwordStore = PasswordStore.shared
|
||||
|
||||
|
|
@ -28,20 +26,29 @@ class AddPasswordTableViewController: PasswordEditorTableViewController {
|
|||
override func shouldPerformSegue(withIdentifier identifier: String, sender: Any?) -> Bool {
|
||||
if identifier == "saveAddPasswordSegue" {
|
||||
// check PGP key
|
||||
if passwordStore.privateKey == nil {
|
||||
guard passwordStore.privateKey != nil else {
|
||||
let alertTitle = "Cannot Add Password"
|
||||
let alertMessage = "PGP Key is not set. Please set your PGP Key first."
|
||||
Utils.alert(title: alertTitle, message: alertMessage, controller: self, completion: nil)
|
||||
return false
|
||||
}
|
||||
|
||||
// check name
|
||||
let nameCell = tableView.cellForRow(at: IndexPath(row: 0, section: 0)) as! TextFieldTableViewCell
|
||||
if nameCell.getContent()!.isEmpty {
|
||||
guard nameCell.getContent()!.isEmpty == false else {
|
||||
let alertTitle = "Cannot Add Password"
|
||||
let alertMessage = "Please fill in the name."
|
||||
Utils.alert(title: alertTitle, message: alertMessage, controller: self, completion: nil)
|
||||
return false
|
||||
}
|
||||
|
||||
// check "/"
|
||||
guard nameCell.getContent()!.contains("/") == false else {
|
||||
let alertTitle = "Cannot Add Password"
|
||||
let alertMessage = "Illegal character."
|
||||
Utils.alert(title: alertTitle, message: alertMessage, controller: self, completion: nil)
|
||||
return false
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
|
|
|||
|
|
@ -8,20 +8,34 @@
|
|||
|
||||
import UIKit
|
||||
import SVProgressHUD
|
||||
import SwiftyUserDefaults
|
||||
|
||||
class AdvancedSettingsTableViewController: UITableViewController {
|
||||
|
||||
@IBOutlet weak var encryptInASCIIArmoredTableViewCell: UITableViewCell!
|
||||
@IBOutlet weak var eraseDataTableViewCell: UITableViewCell!
|
||||
@IBOutlet weak var discardChangesTableViewCell: UITableViewCell!
|
||||
let passwordStore = PasswordStore.shared
|
||||
|
||||
let encryptInASCIIArmoredSwitch: UISwitch = {
|
||||
let uiSwitch = UISwitch()
|
||||
uiSwitch.onTintColor = Globals.blue
|
||||
uiSwitch.sizeToFit()
|
||||
uiSwitch.addTarget(self, action: #selector(encryptInASCIIArmoredAction(_:)), for: UIControlEvents.valueChanged)
|
||||
return uiSwitch
|
||||
}()
|
||||
|
||||
override func viewDidLoad() {
|
||||
super.viewDidLoad()
|
||||
navigationItem.title = "Advanced"
|
||||
encryptInASCIIArmoredSwitch.isOn = Defaults[.encryptInArmored]
|
||||
encryptInASCIIArmoredTableViewCell.accessoryView = encryptInASCIIArmoredSwitch
|
||||
encryptInASCIIArmoredTableViewCell.selectionStyle = .none
|
||||
}
|
||||
|
||||
override func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
|
||||
tableView.deselectRow(at: indexPath, animated: true)
|
||||
if tableView.cellForRow(at: indexPath) == eraseDataTableViewCell {
|
||||
print("erase data")
|
||||
let alert = UIAlertController(title: "Erase Password Store Data?", message: "This will delete all local data and settings. Password store data on your remote server will not be affected.", preferredStyle: UIAlertControllerStyle.alert)
|
||||
alert.addAction(UIAlertAction(title: "Erase Password Data", style: UIAlertActionStyle.destructive, handler: {[unowned self] (action) -> Void in
|
||||
SVProgressHUD.show(withStatus: "Erasing ...")
|
||||
|
|
@ -32,7 +46,6 @@ class AdvancedSettingsTableViewController: UITableViewController {
|
|||
}))
|
||||
alert.addAction(UIAlertAction(title: "Dismiss", style: UIAlertActionStyle.cancel, handler:nil))
|
||||
self.present(alert, animated: true, completion: nil)
|
||||
tableView.deselectRow(at: indexPath, animated: true)
|
||||
} else if tableView.cellForRow(at: indexPath) == discardChangesTableViewCell {
|
||||
let alert = UIAlertController(title: "Discard All Changes?", message: "Do you want to permanently discard all changes to the local copy of your password data? You cannot undo this action.", preferredStyle: UIAlertControllerStyle.alert)
|
||||
alert.addAction(UIAlertAction(title: "Discard All Changes", style: UIAlertActionStyle.destructive, handler: {[unowned self] (action) -> Void in
|
||||
|
|
@ -52,9 +65,7 @@ class AdvancedSettingsTableViewController: UITableViewController {
|
|||
}
|
||||
SVProgressHUD.dismiss(withDelay: 1)
|
||||
} catch {
|
||||
DispatchQueue.main.async {
|
||||
Utils.alert(title: "Error", message: error.localizedDescription, controller: self, completion: nil)
|
||||
}
|
||||
Utils.alert(title: "Error", message: error.localizedDescription, controller: self, completion: nil)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -62,8 +73,11 @@ class AdvancedSettingsTableViewController: UITableViewController {
|
|||
}))
|
||||
alert.addAction(UIAlertAction(title: "Dismiss", style: UIAlertActionStyle.cancel, handler:nil))
|
||||
self.present(alert, animated: true, completion: nil)
|
||||
tableView.deselectRow(at: indexPath, animated: true)
|
||||
}
|
||||
}
|
||||
|
||||
func encryptInASCIIArmoredAction(_ sender: Any?) {
|
||||
Defaults[.encryptInArmored] = encryptInASCIIArmoredSwitch.isOn
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
|||
|
|
@ -9,15 +9,13 @@
|
|||
import UIKit
|
||||
|
||||
class EditPasswordTableViewController: PasswordEditorTableViewController {
|
||||
|
||||
var password: Password?
|
||||
|
||||
override func viewDidLoad() {
|
||||
tableData = [
|
||||
[[.type: PasswordEditorCellType.textFieldCell, .title: "name", .content: password!.name]],
|
||||
[[.type: PasswordEditorCellType.fillPasswordCell, .title: "password", .content: password!.password],
|
||||
[.type: PasswordEditorCellType.passwordLengthCell, .title: "passwordlength"]],
|
||||
[[.type: PasswordEditorCellType.textViewCell, .title: "additions", .content: password!.getAdditionsPlainText()]],
|
||||
[[.type: PasswordEditorCellType.deletePasswordCell]],
|
||||
]
|
||||
super.viewDidLoad()
|
||||
}
|
||||
|
|
@ -43,10 +41,11 @@ class EditPasswordTableViewController: PasswordEditorTableViewController {
|
|||
var cellContents = [String: String]()
|
||||
for cell in cells {
|
||||
let indexPath = tableView.indexPath(for: cell)!
|
||||
let contentCell = cell as! ContentTableViewCell
|
||||
let cellTitle = tableData[indexPath.section][indexPath.row][.title] as! String
|
||||
if let cellContent = contentCell.getContent() {
|
||||
cellContents[cellTitle] = cellContent
|
||||
if let contentCell = cell as? ContentTableViewCell {
|
||||
let cellTitle = tableData[indexPath.section][indexPath.row][.title] as! String
|
||||
if let cellContent = contentCell.getContent() {
|
||||
cellContents[cellTitle] = cellContent
|
||||
}
|
||||
}
|
||||
}
|
||||
var plainText = ""
|
||||
|
|
|
|||
|
|
@ -169,10 +169,12 @@ class GeneralSettingsTableViewController: BasicStaticTableViewController {
|
|||
|
||||
func hideUnknownSwitchAction(_ sender: Any?) {
|
||||
Defaults[.isHideUnknownOn] = hideUnknownSwitch.isOn
|
||||
NotificationCenter.default.post(name: .passwordDetailDisplaySettingChanged, object: nil)
|
||||
}
|
||||
|
||||
func hideOTPSwitchAction(_ sender: Any?) {
|
||||
Defaults[.isHideOTPOn] = hideOTPSwitch.isOn
|
||||
NotificationCenter.default.post(name: .passwordDetailDisplaySettingChanged, object: nil)
|
||||
}
|
||||
|
||||
func rememberPassphraseSwitchAction(_ sender: Any?) {
|
||||
|
|
@ -184,7 +186,7 @@ class GeneralSettingsTableViewController: BasicStaticTableViewController {
|
|||
|
||||
func showFolderSwitchAction(_ sender: Any?) {
|
||||
Defaults[.isShowFolderOn] = showFolderSwitch.isOn
|
||||
NotificationCenter.default.post(name: .passwordStoreUpdated, object: nil)
|
||||
NotificationCenter.default.post(name: .passwordDisplaySettingChanged, object: nil)
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
|||
|
|
@ -77,15 +77,12 @@ class GitServerSettingTableViewController: UITableViewController {
|
|||
|
||||
override func shouldPerformSegue(withIdentifier identifier: String, sender: Any?) -> Bool {
|
||||
if identifier == "saveGitServerSettingSegue" {
|
||||
if gitRepositoryURLTextField.text == "" || authenticationMethod == nil {
|
||||
var alertMessage = ""
|
||||
if gitRepositoryURLTextField.text == "" {
|
||||
alertMessage = "Git Server is not set. Please set the Git server first."
|
||||
}
|
||||
if authenticationMethod == nil {
|
||||
alertMessage = "Authentication method is not set. Please set your authentication method first."
|
||||
}
|
||||
Utils.alert(title: "Cannot Save Settings", message: alertMessage, controller: self, completion: nil)
|
||||
guard let _ = URL(string: gitRepositoryURLTextField.text!) else {
|
||||
Utils.alert(title: "Cannot Save", message: "Git Server is not set.", controller: self, completion: nil)
|
||||
return false
|
||||
}
|
||||
guard authenticationMethod != nil else {
|
||||
Utils.alert(title: "Cannot Save", message: "Authentication method is not set.", controller: self, completion: nil)
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -26,24 +26,15 @@ class PGPKeySettingTableViewController: UITableViewController {
|
|||
|
||||
override func shouldPerformSegue(withIdentifier identifier: String, sender: Any?) -> Bool {
|
||||
if identifier == "savePGPKeySegue" {
|
||||
guard pgpPublicKeyURLTextField.text != nil else {
|
||||
return false
|
||||
}
|
||||
guard pgpPrivateKeyURLTextField.text != nil else {
|
||||
return false
|
||||
}
|
||||
|
||||
guard URL(string: pgpPublicKeyURLTextField.text!) != nil else {
|
||||
guard let pgpPublicKeyURL = URL(string: pgpPublicKeyURLTextField.text!) else {
|
||||
Utils.alert(title: "Cannot Save", message: "Please set Public Key URL first.", controller: self, completion: nil)
|
||||
return false
|
||||
}
|
||||
guard URL(string: pgpPrivateKeyURLTextField.text!) != nil else {
|
||||
guard let pgpPrivateKeyURL = URL(string: pgpPrivateKeyURLTextField.text!) else {
|
||||
Utils.alert(title: "Cannot Save", message: "Please set Private Key URL first.", controller: self, completion: nil)
|
||||
return false
|
||||
}
|
||||
|
||||
if URL(string: pgpPublicKeyURLTextField.text!)!.scheme! == "http" &&
|
||||
URL(string: pgpPrivateKeyURLTextField.text!)!.scheme! == "http" {
|
||||
guard pgpPublicKeyURL.scheme! == "https", pgpPrivateKeyURL.scheme! == "https" else {
|
||||
Utils.alert(title: "Cannot Save Settings", message: "HTTP connection is not supported.", controller: self, completion: nil)
|
||||
return false
|
||||
}
|
||||
|
|
@ -55,7 +46,9 @@ class PGPKeySettingTableViewController: UITableViewController {
|
|||
let alert = UIAlertController(title: "Passphrase", message: "Please fill in the passphrase of your PGP secret key.", preferredStyle: UIAlertControllerStyle.alert)
|
||||
alert.addAction(UIAlertAction(title: "OK", style: UIAlertActionStyle.default, handler: {_ in
|
||||
self.pgpPassphrase = alert.textFields?.first?.text
|
||||
self.performSegue(withIdentifier: "savePGPKeySegue", sender: self)
|
||||
if self.shouldPerformSegue(withIdentifier: "savePGPKeySegue", sender: self) {
|
||||
self.performSegue(withIdentifier: "savePGPKeySegue", sender: self)
|
||||
}
|
||||
}))
|
||||
alert.addTextField(configurationHandler: {(textField: UITextField!) in
|
||||
textField.text = self.pgpPassphrase
|
||||
|
|
|
|||
|
|
@ -13,26 +13,25 @@ import SVProgressHUD
|
|||
|
||||
class PasswordDetailTableViewController: UITableViewController, UIGestureRecognizerDelegate {
|
||||
var passwordEntity: PasswordEntity?
|
||||
var passwordCategoryText = ""
|
||||
var password: Password?
|
||||
var passwordImage: UIImage?
|
||||
var oneTimePasswordIndexPath : IndexPath?
|
||||
var shouldPopCurrentView = false
|
||||
let passwordStore = PasswordStore.shared
|
||||
private var password: Password?
|
||||
private var passwordCategoryText = ""
|
||||
private var passwordImage: UIImage?
|
||||
private var oneTimePasswordIndexPath : IndexPath?
|
||||
private var shouldPopCurrentView = false
|
||||
private let passwordStore = PasswordStore.shared
|
||||
|
||||
let indicator: UIActivityIndicatorView = {
|
||||
private let indicator: UIActivityIndicatorView = {
|
||||
let indicator = UIActivityIndicatorView(activityIndicatorStyle: .gray)
|
||||
indicator.center = CGPoint(x: UIScreen.main.bounds.width / 2, y: UIScreen.main.bounds.height * 0.382)
|
||||
return indicator
|
||||
}()
|
||||
|
||||
lazy var editUIBarButtonItem: UIBarButtonItem = {
|
||||
private lazy var editUIBarButtonItem: UIBarButtonItem = {
|
||||
let uiBarButtonItem = UIBarButtonItem(barButtonSystemItem: .edit, target: self, action: #selector(pressEdit(_:)))
|
||||
return uiBarButtonItem
|
||||
}()
|
||||
|
||||
|
||||
struct TableCell {
|
||||
private struct TableCell {
|
||||
var title: String
|
||||
var content: String
|
||||
init() {
|
||||
|
|
@ -46,12 +45,12 @@ class PasswordDetailTableViewController: UITableViewController, UIGestureRecogni
|
|||
}
|
||||
}
|
||||
|
||||
struct TableSection {
|
||||
private struct TableSection {
|
||||
var title: String
|
||||
var item: Array<TableCell>
|
||||
}
|
||||
|
||||
var tableData = Array<TableSection>()
|
||||
private var tableData = Array<TableSection>()
|
||||
|
||||
override func viewDidLoad() {
|
||||
super.viewDidLoad()
|
||||
|
|
@ -67,7 +66,6 @@ class PasswordDetailTableViewController: UITableViewController, UIGestureRecogni
|
|||
tableView.contentInset = UIEdgeInsetsMake(-36, 0, 0, 0);
|
||||
tableView.rowHeight = UITableViewAutomaticDimension
|
||||
tableView.estimatedRowHeight = 52
|
||||
|
||||
|
||||
indicator.startAnimating()
|
||||
tableView.addSubview(indicator)
|
||||
|
|
@ -96,16 +94,35 @@ class PasswordDetailTableViewController: UITableViewController, UIGestureRecogni
|
|||
self.present(alert, animated: true, completion: nil)
|
||||
}
|
||||
|
||||
self.setupUpdateOneTimePassword()
|
||||
self.addNotificationObservers()
|
||||
|
||||
self.setupOneTimePasswordAutoRefresh()
|
||||
|
||||
// pop the current view because this password might be "discarded"
|
||||
NotificationCenter.default.addObserver(self, selector: #selector(setShouldPopCurrentView), name: .passwordStoreChangeDiscarded, object: nil)
|
||||
|
||||
// reset the data table if some password (maybe another one) has been updated
|
||||
NotificationCenter.default.addObserver(self, selector: #selector(showPassword), name: .passwordStoreUpdated, object: nil)
|
||||
|
||||
// reset the data table if the disaply settings have been changed
|
||||
NotificationCenter.default.addObserver(self, selector: #selector(showPassword), name: .passwordDetailDisplaySettingChanged, object: nil)
|
||||
}
|
||||
|
||||
func decryptThenShowPassword(passphrase: String) {
|
||||
override func viewDidAppear(_ animated: Bool) {
|
||||
super.viewWillAppear(animated)
|
||||
if self.shouldPopCurrentView {
|
||||
let alert = UIAlertController(title: "Notice", message: "All previous local changes have been discarded. Your current Password Store will be shown.", preferredStyle: UIAlertControllerStyle.alert)
|
||||
alert.addAction(UIAlertAction(title: "OK", style: UIAlertActionStyle.default, handler: {_ in
|
||||
_ = self.navigationController?.popViewController(animated: true)
|
||||
}))
|
||||
self.present(alert, animated: true, completion: nil)
|
||||
}
|
||||
}
|
||||
|
||||
private func decryptThenShowPassword(passphrase: String) {
|
||||
if Defaults[.isRememberPassphraseOn] {
|
||||
self.passwordStore.pgpKeyPassphrase = passphrase
|
||||
}
|
||||
DispatchQueue.global(qos: .userInitiated).async {
|
||||
// decrypt password
|
||||
do {
|
||||
self.password = try self.passwordEntity!.decrypt(passphrase: passphrase)!
|
||||
} catch {
|
||||
|
|
@ -118,39 +135,42 @@ class PasswordDetailTableViewController: UITableViewController, UIGestureRecogni
|
|||
}
|
||||
return
|
||||
}
|
||||
|
||||
let password = self.password!
|
||||
DispatchQueue.main.async { [weak self] in
|
||||
self?.showPassword(password: password)
|
||||
// display password
|
||||
self.showPassword()
|
||||
}
|
||||
}
|
||||
|
||||
@objc private func showPassword() {
|
||||
DispatchQueue.main.async { [weak self] in
|
||||
self?.indicator.stopAnimating()
|
||||
self?.setTableData()
|
||||
UIView.performWithoutAnimation {
|
||||
self?.tableView.reloadData()
|
||||
// add layoutIfNeeded solves the "flickering problem" during refresh
|
||||
self?.tableView.layoutIfNeeded()
|
||||
}
|
||||
self?.editUIBarButtonItem.isEnabled = true
|
||||
if let urlString = self?.password?.getURLString() {
|
||||
if self?.passwordEntity?.image == nil {
|
||||
self?.updatePasswordImage(urlString: urlString)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func showPassword(password: Password) {
|
||||
setTableData()
|
||||
self.tableView.reloadData()
|
||||
indicator.stopAnimating()
|
||||
editUIBarButtonItem.isEnabled = true
|
||||
if let urlString = password.getURLString() {
|
||||
if self.passwordEntity?.image == nil{
|
||||
self.updatePasswordImage(urlString: urlString)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func setupUpdateOneTimePassword() {
|
||||
private func setupOneTimePasswordAutoRefresh() {
|
||||
Timer.scheduledTimer(withTimeInterval: 1, repeats: true) {
|
||||
[weak self] timer in
|
||||
// bail out of the timer code if the object has been freed
|
||||
guard let strongSelf = self,
|
||||
let token = strongSelf.password?.otpToken,
|
||||
let otpType = strongSelf.password?.otpType,
|
||||
otpType != .none,
|
||||
let indexPath = strongSelf.oneTimePasswordIndexPath,
|
||||
let cell = strongSelf.tableView.cellForRow(at: indexPath) as? LabelTableViewCell else {
|
||||
return
|
||||
}
|
||||
switch token.generator.factor {
|
||||
case .timer:
|
||||
// totp
|
||||
switch otpType {
|
||||
case .totp:
|
||||
if let (title, otp) = strongSelf.password?.getOtpStrings() {
|
||||
strongSelf.tableData[indexPath.section].item[indexPath.row].title = title
|
||||
strongSelf.tableData[indexPath.section].item[indexPath.row].content = otp
|
||||
|
|
@ -163,16 +183,19 @@ class PasswordDetailTableViewController: UITableViewController, UIGestureRecogni
|
|||
}
|
||||
}
|
||||
|
||||
func pressEdit(_ sender: Any?) {
|
||||
print("pressEdit")
|
||||
@objc private func pressEdit(_ sender: Any?) {
|
||||
performSegue(withIdentifier: "editPasswordSegue", sender: self)
|
||||
}
|
||||
|
||||
@IBAction func cancelEditPassword(segue: UIStoryboardSegue) {
|
||||
@objc private func setShouldPopCurrentView() {
|
||||
self.shouldPopCurrentView = true
|
||||
}
|
||||
|
||||
@IBAction private func cancelEditPassword(segue: UIStoryboardSegue) {
|
||||
|
||||
}
|
||||
|
||||
@IBAction func saveEditPassword(segue: UIStoryboardSegue) {
|
||||
@IBAction private func saveEditPassword(segue: UIStoryboardSegue) {
|
||||
if self.password!.changed {
|
||||
SVProgressHUD.show(withStatus: "Saving")
|
||||
DispatchQueue.global(qos: .userInitiated).async {
|
||||
|
|
@ -193,7 +216,13 @@ class PasswordDetailTableViewController: UITableViewController, UIGestureRecogni
|
|||
}
|
||||
}
|
||||
|
||||
func setTableData() {
|
||||
@IBAction private func deletePassword(segue: UIStoryboardSegue) {
|
||||
print("delete")
|
||||
passwordStore.delete(passwordEntity: passwordEntity!)
|
||||
let _ = navigationController?.popViewController(animated: true)
|
||||
}
|
||||
|
||||
private func setTableData() {
|
||||
self.tableData = Array<TableSection>()
|
||||
tableData.append(TableSection(title: "", item: []))
|
||||
tableData[0].item.append(TableCell())
|
||||
|
|
@ -206,26 +235,12 @@ class PasswordDetailTableViewController: UITableViewController, UIGestureRecogni
|
|||
self.tableData[tableDataIndex].item.append(TableCell(title: "password", content: password.password))
|
||||
|
||||
// show one time password
|
||||
if let token = password.otpToken {
|
||||
switch token.generator.factor {
|
||||
case .counter(_):
|
||||
// counter-based one time password
|
||||
if password.otpType != .none {
|
||||
if let (title, otp) = self.password?.getOtpStrings() {
|
||||
self.tableData.append(TableSection(title: "One time password", item: []))
|
||||
tableDataIndex += 1
|
||||
oneTimePasswordIndexPath = IndexPath(row: 0, section: tableDataIndex)
|
||||
if let crtPassword = password.otpToken?.currentPassword {
|
||||
self.tableData[tableDataIndex].item.append(TableCell(title: "HMAC-based", content: crtPassword))
|
||||
}
|
||||
case .timer(let period):
|
||||
// time-based one time password
|
||||
self.tableData.append(TableSection(title: "One time password", item: []))
|
||||
tableDataIndex += 1
|
||||
oneTimePasswordIndexPath = IndexPath(row: 0, section: tableDataIndex)
|
||||
if let crtPassword = password.otpToken?.currentPassword {
|
||||
let timeSinceEpoch = Date().timeIntervalSince1970
|
||||
let validTime = Int(period - timeSinceEpoch.truncatingRemainder(dividingBy: period))
|
||||
self.tableData[tableDataIndex].item.append(TableCell(title: "time-based (expiring in \(validTime)s)", content: crtPassword))
|
||||
}
|
||||
self.tableData[tableDataIndex].item.append(TableCell(title: title, content: otp))
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -233,7 +248,7 @@ class PasswordDetailTableViewController: UITableViewController, UIGestureRecogni
|
|||
let filteredAdditionKeys = password.additionKeys.filter {
|
||||
$0.lowercased() != "username" &&
|
||||
$0.lowercased() != "password" &&
|
||||
(!$0.hasPrefix("unknown") || !Defaults[.isHideOTPOn]) &&
|
||||
(!$0.hasPrefix("unknown") || !Defaults[.isHideUnknownOn]) &&
|
||||
(!Password.otpKeywords.contains($0) || !Defaults[.isHideOTPOn]) }
|
||||
|
||||
if filteredAdditionKeys.count > 0 {
|
||||
|
|
@ -255,7 +270,7 @@ class PasswordDetailTableViewController: UITableViewController, UIGestureRecogni
|
|||
}
|
||||
}
|
||||
|
||||
func updatePasswordImage(urlString: String) {
|
||||
private func updatePasswordImage(urlString: String) {
|
||||
var newUrlString = urlString
|
||||
if urlString.lowercased().hasPrefix("http://") {
|
||||
// try to replace http url to https url
|
||||
|
|
@ -294,7 +309,7 @@ class PasswordDetailTableViewController: UITableViewController, UIGestureRecogni
|
|||
}
|
||||
}
|
||||
|
||||
func tapMenu(recognizer: UITapGestureRecognizer) {
|
||||
@objc private func tapMenu(recognizer: UITapGestureRecognizer) {
|
||||
if recognizer.state == UIGestureRecognizerState.ended {
|
||||
let tapLocation = recognizer.location(in: self.tableView)
|
||||
if let tapIndexPath = self.tableView.indexPathForRow(at: tapLocation) {
|
||||
|
|
@ -303,9 +318,9 @@ class PasswordDetailTableViewController: UITableViewController, UIGestureRecogni
|
|||
let menuController = UIMenuController.shared
|
||||
let revealItem = UIMenuItem(title: "Reveal", action: #selector(LabelTableViewCell.revealPassword(_:)))
|
||||
let concealItem = UIMenuItem(title: "Conceal", action: #selector(LabelTableViewCell.concealPassword(_:)))
|
||||
let nextPasswordItem = UIMenuItem(title: "Next Password", action: #selector(LabelTableViewCell.nextPassword(_:)))
|
||||
let nextHOTPItem = UIMenuItem(title: "Next Password", action: #selector(LabelTableViewCell.getNextHOTP(_:)))
|
||||
let openURLItem = UIMenuItem(title: "Copy Password & Open Link", action: #selector(LabelTableViewCell.openLink(_:)))
|
||||
menuController.menuItems = [revealItem, concealItem, nextPasswordItem, openURLItem]
|
||||
menuController.menuItems = [revealItem, concealItem, nextHOTPItem, openURLItem]
|
||||
menuController.setTargetRect(tappedCell.contentLabel.frame, in: tappedCell.contentLabel.superview!)
|
||||
menuController.setMenuVisible(true, animated: true)
|
||||
}
|
||||
|
|
@ -313,6 +328,46 @@ class PasswordDetailTableViewController: UITableViewController, UIGestureRecogni
|
|||
}
|
||||
}
|
||||
|
||||
func getNextHOTP() {
|
||||
guard password != nil, passwordEntity != nil, password?.otpType == .hotp else {
|
||||
DispatchQueue.main.async {
|
||||
Utils.alert(title: "Error", message: "Get next password of a non-HOTP entry.", controller: self, completion: nil)
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
// increase HOTP counter
|
||||
password!.increaseHotpCounter()
|
||||
|
||||
// copy HOTP to pasteboard
|
||||
if let plainPassword = password!.getOtp() {
|
||||
Utils.copyToPasteboard(textToCopy: plainPassword)
|
||||
}
|
||||
|
||||
// commit the change of HOTP counter
|
||||
if password!.changed {
|
||||
DispatchQueue.global(qos: .userInitiated).async {
|
||||
self.passwordStore.update(passwordEntity: self.passwordEntity!, password: self.password!, progressBlock: {_ in })
|
||||
DispatchQueue.main.async {
|
||||
self.passwordEntity!.synced = false
|
||||
self.passwordStore.saveUpdated(passwordEntity: self.passwordEntity!)
|
||||
SVProgressHUD.showSuccess(withStatus: "Password Copied\nCounter Updated")
|
||||
SVProgressHUD.dismiss(withDelay: 1)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func openLink() {
|
||||
guard let urlString = self.password?.getURLString(), let url = URL(string: urlString) else {
|
||||
DispatchQueue.main.async {
|
||||
Utils.alert(title: "Error", message: "Cannot find a valid URL", controller: self, completion: nil)
|
||||
}
|
||||
return;
|
||||
}
|
||||
Utils.copyToPasteboard(textToCopy: password?.password)
|
||||
UIApplication.shared.open(url, options: [:], completionHandler: nil)
|
||||
}
|
||||
|
||||
override func numberOfSections(in tableView: UITableView) -> Int {
|
||||
return tableData.count
|
||||
|
|
@ -340,7 +395,7 @@ class PasswordDetailTableViewController: UITableViewController, UIGestureRecogni
|
|||
let cell = tableView.dequeueReusableCell(withIdentifier: "labelCell", for: indexPath) as! LabelTableViewCell
|
||||
let titleData = tableData[sectionIndex].item[rowIndex].title
|
||||
let contentData = tableData[sectionIndex].item[rowIndex].content
|
||||
cell.passwordTableView = self
|
||||
cell.delegatePasswordTableView = self
|
||||
cell.isPasswordCell = (titleData.lowercased() == "password" ? true : false)
|
||||
cell.isURLCell = (titleData.lowercased() == "url" ? true : false)
|
||||
cell.isHOTPCell = (titleData == "HMAC-based" ? true : false)
|
||||
|
|
@ -381,23 +436,4 @@ class PasswordDetailTableViewController: UITableViewController, UIGestureRecogni
|
|||
override func tableView(_ tableView: UITableView, shouldShowMenuForRowAt indexPath: IndexPath) -> Bool {
|
||||
return true
|
||||
}
|
||||
|
||||
private func addNotificationObservers() {
|
||||
NotificationCenter.default.addObserver(self, selector: #selector(setShouldPopCurrentView), name: .passwordStoreChangeDiscarded, object: nil)
|
||||
}
|
||||
|
||||
func setShouldPopCurrentView() {
|
||||
self.shouldPopCurrentView = true
|
||||
}
|
||||
|
||||
override func viewDidAppear(_ animated: Bool) {
|
||||
super.viewWillAppear(animated)
|
||||
if self.shouldPopCurrentView {
|
||||
let alert = UIAlertController(title: "Notice", message: "All previous local changes have been discarded. Your current Password Store will be shown.", preferredStyle: UIAlertControllerStyle.alert)
|
||||
alert.addAction(UIAlertAction(title: "OK", style: UIAlertActionStyle.default, handler: {_ in
|
||||
_ = self.navigationController?.popViewController(animated: true)
|
||||
}))
|
||||
self.present(alert, animated: true, completion: nil)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -8,23 +8,40 @@
|
|||
|
||||
import UIKit
|
||||
enum PasswordEditorCellType {
|
||||
case textFieldCell, textViewCell, fillPasswordCell, passwordLengthCell
|
||||
case textFieldCell, textViewCell, fillPasswordCell, passwordLengthCell, deletePasswordCell
|
||||
}
|
||||
|
||||
enum PasswordEditorCellKey {
|
||||
case type, title, content, placeholders
|
||||
}
|
||||
|
||||
class PasswordEditorTableViewController: UITableViewController, FillPasswordTableViewCellDelegate {
|
||||
var navigationItemTitle: String?
|
||||
class PasswordEditorTableViewController: UITableViewController, FillPasswordTableViewCellDelegate, PasswordSettingSliderTableViewCellDelegate, UIGestureRecognizerDelegate {
|
||||
|
||||
var tableData = [
|
||||
[Dictionary<PasswordEditorCellKey, Any>]
|
||||
]()
|
||||
var sectionHeaderTitles = ["name", "password", "additions"].map {$0.uppercased()}
|
||||
var sectionFooterTitles = ["", "", "It is recommended to use \"key: value\" format to store additional fields as follows:\n url: https://www.apple.com\n username: passforios@gmail.com."]
|
||||
]()
|
||||
var password: Password?
|
||||
|
||||
private var navigationItemTitle: String?
|
||||
|
||||
private var sectionHeaderTitles = ["name", "password", "additions",""].map {$0.uppercased()}
|
||||
private var sectionFooterTitles = ["", "", "Use \"key: value\" format for additional fields.", ""]
|
||||
private let passwordSection = 1
|
||||
private var hidePasswordSettings = true
|
||||
|
||||
private var fillPasswordCell: FillPasswordTableViewCell?
|
||||
private var passwordLengthCell: SliderTableViewCell?
|
||||
private var deletePasswordCell: UITableViewCell?
|
||||
|
||||
override func loadView() {
|
||||
super.loadView()
|
||||
|
||||
deletePasswordCell = UITableViewCell(style: .default, reuseIdentifier: "default")
|
||||
deletePasswordCell!.textLabel?.text = "Delete Password"
|
||||
deletePasswordCell!.textLabel?.textColor = Globals.red
|
||||
deletePasswordCell?.selectionStyle = .default
|
||||
}
|
||||
|
||||
var passwordLengthCell: SliderTableViewCell?
|
||||
|
||||
override func viewDidLoad() {
|
||||
super.viewDidLoad()
|
||||
navigationItem.title = navigationItemTitle
|
||||
|
|
@ -35,33 +52,40 @@ class PasswordEditorTableViewController: UITableViewController, FillPasswordTabl
|
|||
|
||||
tableView.rowHeight = UITableViewAutomaticDimension
|
||||
tableView.estimatedRowHeight = 48
|
||||
tableView.allowsSelection = false
|
||||
self.tableView.sectionFooterHeight = UITableViewAutomaticDimension;
|
||||
self.tableView.estimatedSectionFooterHeight = 0;
|
||||
|
||||
let tapGesture = UITapGestureRecognizer(target: self, action: #selector(tableTapped))
|
||||
tapGesture.delegate = self
|
||||
tapGesture.cancelsTouchesInView = false
|
||||
tableView.addGestureRecognizer(tapGesture)
|
||||
}
|
||||
|
||||
override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
|
||||
let cellData = tableData[indexPath.section][indexPath.row]
|
||||
var cell = ContentTableViewCell()
|
||||
|
||||
switch cellData[PasswordEditorCellKey.type] as! PasswordEditorCellType {
|
||||
case .textViewCell:
|
||||
cell = tableView.dequeueReusableCell(withIdentifier: "textViewCell", for: indexPath) as! ContentTableViewCell
|
||||
let cell = tableView.dequeueReusableCell(withIdentifier: "textViewCell", for: indexPath) as! ContentTableViewCell
|
||||
cell.setContent(content: cellData[PasswordEditorCellKey.content] as? String)
|
||||
return cell
|
||||
case .fillPasswordCell:
|
||||
let fillPasswordCell = tableView.dequeueReusableCell(withIdentifier: "fillPasswordCell", for: indexPath) as?FillPasswordTableViewCell
|
||||
fillPasswordCell = tableView.dequeueReusableCell(withIdentifier: "fillPasswordCell", for: indexPath) as? FillPasswordTableViewCell
|
||||
fillPasswordCell?.delegate = self
|
||||
cell = fillPasswordCell!
|
||||
fillPasswordCell?.setContent(content: cellData[PasswordEditorCellKey.content] as? String)
|
||||
return fillPasswordCell!
|
||||
case .passwordLengthCell:
|
||||
passwordLengthCell = tableView.dequeueReusableCell(withIdentifier: "passwordLengthCell", for: indexPath) as? SliderTableViewCell
|
||||
passwordLengthCell?.reset(title: "Length", minimumValue: Globals.passwordMinimumLength, maximumValue: Globals.passwordMaximumLength, defaultValue: Globals.passwordDefaultLength)
|
||||
cell = passwordLengthCell!
|
||||
passwordLengthCell?.delegate = self
|
||||
return passwordLengthCell!
|
||||
case .deletePasswordCell:
|
||||
return deletePasswordCell!
|
||||
default:
|
||||
cell = tableView.dequeueReusableCell(withIdentifier: "textFieldCell", for: indexPath) as! ContentTableViewCell
|
||||
let cell = tableView.dequeueReusableCell(withIdentifier: "textFieldCell", for: indexPath) as! ContentTableViewCell
|
||||
cell.setContent(content: cellData[PasswordEditorCellKey.content] as? String)
|
||||
return cell
|
||||
}
|
||||
if let content = cellData[PasswordEditorCellKey.content] as? String {
|
||||
cell.setContent(content: content)
|
||||
}
|
||||
return cell
|
||||
}
|
||||
|
||||
override func tableView(_ tableView: UITableView, heightForHeaderInSection section: Int) -> CGFloat {
|
||||
|
|
@ -73,9 +97,14 @@ class PasswordEditorTableViewController: UITableViewController, FillPasswordTabl
|
|||
}
|
||||
|
||||
override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
|
||||
return tableData[section].count
|
||||
if section == passwordSection, hidePasswordSettings {
|
||||
// hide the password section, only the password should be shown
|
||||
return 1
|
||||
} else {
|
||||
return tableData[section].count
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
override func tableView(_ tableView: UITableView, titleForHeaderInSection section: Int) -> String? {
|
||||
return sectionHeaderTitles[section]
|
||||
}
|
||||
|
|
@ -84,8 +113,81 @@ class PasswordEditorTableViewController: UITableViewController, FillPasswordTabl
|
|||
return sectionFooterTitles[section]
|
||||
}
|
||||
|
||||
func generatePassword() -> String {
|
||||
override func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
|
||||
if tableView.cellForRow(at: indexPath) == deletePasswordCell {
|
||||
let alert = UIAlertController(title: "Delete Password?", message: nil, preferredStyle: UIAlertControllerStyle.alert)
|
||||
alert.addAction(UIAlertAction(title: "Delete", style: UIAlertActionStyle.destructive, handler: {[unowned self] (action) -> Void in
|
||||
self.performSegue(withIdentifier: "deletePasswordSegue", sender: self)
|
||||
}))
|
||||
alert.addAction(UIAlertAction(title: "Cancel", style: UIAlertActionStyle.cancel, handler:nil))
|
||||
self.present(alert, animated: true, completion: nil)
|
||||
}
|
||||
tableView.deselectRow(at: indexPath, animated: true)
|
||||
}
|
||||
|
||||
// generate password, copy to pasteboard, and set the cell
|
||||
// check whether the current password looks like an OTP field
|
||||
func generateAndCopyPassword() {
|
||||
if let currentPassword = fillPasswordCell?.getContent(),
|
||||
Password.LooksLikeOTP(line: currentPassword) {
|
||||
let alert = UIAlertController(title: "Overwrite?", message: "Overwrite the one-time password configuration?", preferredStyle: UIAlertControllerStyle.alert)
|
||||
alert.addAction(UIAlertAction(title: "Yes", style: UIAlertActionStyle.destructive, handler: {_ in
|
||||
self.generateAndCopyPasswordNoOtpCheck()
|
||||
}))
|
||||
alert.addAction(UIAlertAction(title: "Cancel", style: UIAlertActionStyle.cancel, handler: nil))
|
||||
self.present(alert, animated: true, completion: nil)
|
||||
} else {
|
||||
self.generateAndCopyPasswordNoOtpCheck()
|
||||
}
|
||||
}
|
||||
|
||||
// generate the password, don't care whether the original line is otp
|
||||
func generateAndCopyPasswordNoOtpCheck() {
|
||||
// show password settings (e.g., the length slider)
|
||||
if hidePasswordSettings == true {
|
||||
hidePasswordSettings = false
|
||||
tableView.reloadSections([passwordSection], with: .fade)
|
||||
}
|
||||
let length = passwordLengthCell?.roundedValue ?? Globals.passwordDefaultLength
|
||||
return Utils.generatePassword(length: length)
|
||||
let plainPassword = Utils.generatePassword(length: length)
|
||||
Utils.copyToPasteboard(textToCopy: plainPassword)
|
||||
|
||||
// update tableData so to make sure reloadData() works correctly
|
||||
tableData[passwordSection][0][PasswordEditorCellKey.content] = plainPassword
|
||||
|
||||
// update cell manually, no need to call reloadData()
|
||||
fillPasswordCell?.setContent(content: plainPassword)
|
||||
}
|
||||
|
||||
func tableTapped(recognizer: UITapGestureRecognizer) {
|
||||
if recognizer.state == UIGestureRecognizerState.ended {
|
||||
let tapLocation = recognizer.location(in: self.tableView)
|
||||
let tapIndexPath = self.tableView.indexPathForRow(at: tapLocation)
|
||||
|
||||
// do nothing, if delete is tapped (a temporary solution)
|
||||
if tapIndexPath != nil, deletePasswordCell != nil,
|
||||
tableView.cellForRow(at: tapIndexPath!) == deletePasswordCell {
|
||||
return
|
||||
}
|
||||
|
||||
// hide password settings (e.g., the length slider)
|
||||
if tapIndexPath?.section != passwordSection, hidePasswordSettings == false {
|
||||
hidePasswordSettings = true
|
||||
tableView.reloadSections([passwordSection], with: .fade)
|
||||
// select the row at tapIndexPath manually
|
||||
if tapIndexPath != nil {
|
||||
self.tableView(self.tableView, didSelectRowAt: tapIndexPath!)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func gestureRecognizer(_ gestureRecognizer: UIGestureRecognizer, shouldRecognizeSimultaneouslyWith otherGestureRecognizer: UIGestureRecognizer) -> Bool {
|
||||
if gestureRecognizer is UITapGestureRecognizer {
|
||||
// so that the tap gesture could be passed by
|
||||
return true
|
||||
} else {
|
||||
return false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -25,13 +25,14 @@ class PasswordsViewController: UIViewController, UITableViewDataSource, UITableV
|
|||
private var passwordsTableEntries: [PasswordsTableEntry] = []
|
||||
private var filteredPasswordsTableEntries: [PasswordsTableEntry] = []
|
||||
private var parentPasswordEntity: PasswordEntity? = nil
|
||||
let passwordStore = PasswordStore.shared
|
||||
private let passwordStore = PasswordStore.shared
|
||||
|
||||
private var tapTabBarTime: TimeInterval = 0
|
||||
|
||||
var sections : [(index: Int, length :Int, title: String)] = Array()
|
||||
var searchActive : Bool = false
|
||||
lazy var searchController: UISearchController = {
|
||||
private var sections : [(index: Int, length :Int, title: String)] = Array()
|
||||
private var searchActive : Bool = false
|
||||
|
||||
private lazy var searchController: UISearchController = {
|
||||
let uiSearchController = UISearchController(searchResultsController: nil)
|
||||
uiSearchController.searchResultsUpdater = self
|
||||
uiSearchController.dimsBackgroundDuringPresentation = false
|
||||
|
|
@ -40,20 +41,40 @@ class PasswordsViewController: UIViewController, UITableViewDataSource, UITableV
|
|||
uiSearchController.searchBar.sizeToFit()
|
||||
return uiSearchController
|
||||
}()
|
||||
lazy var refreshControl: UIRefreshControl = {
|
||||
let refreshControl = UIRefreshControl()
|
||||
refreshControl.addTarget(self, action: #selector(PasswordsViewController.handleRefresh(_:)), for: UIControlEvents.valueChanged)
|
||||
return refreshControl
|
||||
private lazy var syncControl: UIRefreshControl = {
|
||||
let syncControl = UIRefreshControl()
|
||||
syncControl.addTarget(self, action: #selector(handleRefresh(_:)), for: UIControlEvents.valueChanged)
|
||||
return syncControl
|
||||
}()
|
||||
lazy var searchBarView: UIView = {
|
||||
let uiView = UIView(frame: CGRect(x: 0, y: 64, width: UIScreen.main.bounds.width, height: 44))
|
||||
private lazy var searchBarView: UIView = {
|
||||
let uiView = UIView(frame: CGRect(x: 0, y: 64, width: self.view.bounds.width, height: 44))
|
||||
uiView.addSubview(self.searchController.searchBar)
|
||||
return uiView
|
||||
}()
|
||||
lazy var backUIBarButtonItem: UIBarButtonItem = {
|
||||
private lazy var backUIBarButtonItem: UIBarButtonItem = {
|
||||
let backUIBarButtonItem = UIBarButtonItem(title: "Back", style: .plain, target: self, action: #selector(self.backAction(_:)))
|
||||
return backUIBarButtonItem
|
||||
}()
|
||||
|
||||
private lazy var transitionFromRight: CATransition = {
|
||||
let transition = CATransition()
|
||||
transition.type = kCATransitionPush
|
||||
transition.timingFunction = CAMediaTimingFunction(name: kCAMediaTimingFunctionEaseInEaseOut)
|
||||
transition.fillMode = kCAFillModeForwards
|
||||
transition.duration = 0.25
|
||||
transition.subtype = kCATransitionFromRight
|
||||
return transition
|
||||
}()
|
||||
|
||||
private lazy var transitionFromLeft: CATransition = {
|
||||
let transition = CATransition()
|
||||
transition.type = kCATransitionPush
|
||||
transition.timingFunction = CAMediaTimingFunction(name: kCAMediaTimingFunctionEaseInEaseOut)
|
||||
transition.fillMode = kCAFillModeForwards
|
||||
transition.duration = 0.25
|
||||
transition.subtype = kCATransitionFromLeft
|
||||
return transition
|
||||
}()
|
||||
|
||||
@IBOutlet weak var tableView: UITableView!
|
||||
|
||||
|
|
@ -102,11 +123,11 @@ class PasswordsViewController: UIViewController, UITableViewDataSource, UITableV
|
|||
}
|
||||
}
|
||||
|
||||
func syncPasswords() {
|
||||
private func syncPasswords() {
|
||||
SVProgressHUD.setDefaultMaskType(.black)
|
||||
SVProgressHUD.setDefaultStyle(.light)
|
||||
SVProgressHUD.show(withStatus: "Sync Password Store")
|
||||
let numberOfUnsyncedPasswords = self.passwordStore.getNumberOfUnsyncedPasswords()
|
||||
let numberOfLocalCommits = self.passwordStore.numberOfLocalCommits()
|
||||
DispatchQueue.global(qos: .userInitiated).async { [unowned self] in
|
||||
do {
|
||||
try self.passwordStore.pullRepository(transferProgressBlock: {(git_transfer_progress, stop) in
|
||||
|
|
@ -114,7 +135,7 @@ class PasswordsViewController: UIViewController, UITableViewDataSource, UITableV
|
|||
SVProgressHUD.showProgress(Float(git_transfer_progress.pointee.received_objects)/Float(git_transfer_progress.pointee.total_objects), status: "Pull Remote Repository")
|
||||
}
|
||||
})
|
||||
if numberOfUnsyncedPasswords > 0 {
|
||||
if numberOfLocalCommits > 0 {
|
||||
try self.passwordStore.pushRepository(transferProgressBlock: {(current, total, bytes, stop) in
|
||||
DispatchQueue.main.async {
|
||||
SVProgressHUD.showProgress(Float(current)/Float(total), status: "Push Remote Repository")
|
||||
|
|
@ -122,11 +143,7 @@ class PasswordsViewController: UIViewController, UITableViewDataSource, UITableV
|
|||
})
|
||||
}
|
||||
DispatchQueue.main.async {
|
||||
self.passwordStore.updatePasswordEntityCoreData()
|
||||
self.initPasswordsTableEntries(parent: nil)
|
||||
self.reloadTableView(data: self.passwordsTableEntries)
|
||||
self.passwordStore.setAllSynced()
|
||||
self.setNavigationItemTitle()
|
||||
self.reloadTableView(parent: nil)
|
||||
Defaults[.lastUpdatedTime] = Date()
|
||||
Defaults[.gitRepositoryPasswordAttempts] = 0
|
||||
SVProgressHUD.showSuccess(withStatus: "Done")
|
||||
|
|
@ -140,26 +157,27 @@ class PasswordsViewController: UIViewController, UITableViewDataSource, UITableV
|
|||
}
|
||||
}
|
||||
|
||||
private func addNotificationObservers() {
|
||||
NotificationCenter.default.addObserver(self, selector: #selector(PasswordsViewController.actOnPasswordStoreUpdatedNotification), name: .passwordStoreUpdated, object: nil)
|
||||
NotificationCenter.default.addObserver(self, selector: #selector(PasswordsViewController.actOnSearchNotification), name: .passwordSearch, object: nil)
|
||||
}
|
||||
|
||||
override func viewDidLoad() {
|
||||
super.viewDidLoad()
|
||||
setNavigationItemTitle()
|
||||
initPasswordsTableEntries(parent: nil)
|
||||
addNotificationObservers()
|
||||
generateSections(item: passwordsTableEntries)
|
||||
|
||||
tabBarController!.delegate = self
|
||||
tableView.delegate = self
|
||||
tableView.dataSource = self
|
||||
definesPresentationContext = true
|
||||
view.addSubview(searchBarView)
|
||||
tableView.insertSubview(refreshControl, at: 0)
|
||||
tableView.insertSubview(syncControl, at: 0)
|
||||
SVProgressHUD.setDefaultMaskType(.black)
|
||||
updateRefreshControlTitle()
|
||||
tableView.register(UINib(nibName: "PasswordWithFolderTableViewCell", bundle: nil), forCellReuseIdentifier: "passwordWithFolderTableViewCell")
|
||||
|
||||
// initialize the password table
|
||||
reloadTableView(parent: nil)
|
||||
|
||||
// reset the data table if some password (maybe another one) has been updated
|
||||
NotificationCenter.default.addObserver(self, selector: #selector(reloadTableView as (Void) -> Void), name: .passwordStoreUpdated, object: nil)
|
||||
// reset the data table if the disaply settings have been changed
|
||||
NotificationCenter.default.addObserver(self, selector: #selector(reloadTableView as (Void) -> Void), name: .passwordDisplaySettingChanged, object: nil)
|
||||
NotificationCenter.default.addObserver(self, selector: #selector(actOnSearchNotification), name: .passwordSearch, object: nil)
|
||||
}
|
||||
|
||||
override func viewWillAppear(_ animated: Bool) {
|
||||
|
|
@ -243,15 +261,13 @@ class PasswordsViewController: UIViewController, UITableViewDataSource, UITableV
|
|||
} else {
|
||||
tableView.deselectRow(at: indexPath, animated: true)
|
||||
searchController.isActive = false
|
||||
initPasswordsTableEntries(parent: entry.passwordEntity)
|
||||
reloadTableView(data: passwordsTableEntries)
|
||||
reloadTableView(parent: entry.passwordEntity, anim: transitionFromRight)
|
||||
}
|
||||
}
|
||||
|
||||
func backAction(_ sender: Any?) {
|
||||
guard Defaults[.isShowFolderOn] else { return }
|
||||
initPasswordsTableEntries(parent: parentPasswordEntity?.parent)
|
||||
reloadTableView(data: passwordsTableEntries)
|
||||
reloadTableView(parent: parentPasswordEntity?.parent, anim: transitionFromLeft)
|
||||
}
|
||||
|
||||
func longPressAction(_ gesture: UILongPressGestureRecognizer) {
|
||||
|
|
@ -279,7 +295,7 @@ class PasswordsViewController: UIViewController, UITableViewDataSource, UITableV
|
|||
copyToPasteboard(from: indexPath)
|
||||
}
|
||||
|
||||
func copyToPasteboard(from indexPath: IndexPath) {
|
||||
private func copyToPasteboard(from indexPath: IndexPath) {
|
||||
guard self.passwordStore.privateKey != nil else {
|
||||
Utils.alert(title: "Cannot Copy Password", message: "PGP Key is not set. Please set your PGP Key first.", controller: self, completion: nil)
|
||||
return
|
||||
|
|
@ -305,7 +321,7 @@ class PasswordsViewController: UIViewController, UITableViewDataSource, UITableV
|
|||
|
||||
}
|
||||
|
||||
func decryptThenCopyPassword(passwordEntity: PasswordEntity, passphrase: String) {
|
||||
private func decryptThenCopyPassword(passwordEntity: PasswordEntity, passphrase: String) {
|
||||
SVProgressHUD.setDefaultMaskType(.black)
|
||||
SVProgressHUD.setDefaultStyle(.dark)
|
||||
SVProgressHUD.show(withStatus: "Decrypting")
|
||||
|
|
@ -327,7 +343,7 @@ class PasswordsViewController: UIViewController, UITableViewDataSource, UITableV
|
|||
}
|
||||
}
|
||||
|
||||
func generateSections(item: [PasswordsTableEntry]) {
|
||||
private func generateSections(item: [PasswordsTableEntry]) {
|
||||
sections.removeAll()
|
||||
guard item.count != 0 else {
|
||||
return
|
||||
|
|
@ -349,27 +365,6 @@ class PasswordsViewController: UIViewController, UITableViewDataSource, UITableV
|
|||
sections.append(newSection)
|
||||
}
|
||||
|
||||
func actOnPasswordStoreUpdatedNotification() {
|
||||
initPasswordsTableEntries(parent: nil)
|
||||
reloadTableView(data: passwordsTableEntries)
|
||||
setNavigationItemTitle()
|
||||
}
|
||||
|
||||
private func setNavigationItemTitle() {
|
||||
var title = ""
|
||||
if parentPasswordEntity != nil {
|
||||
title = parentPasswordEntity!.name!
|
||||
} else {
|
||||
title = "Password Store"
|
||||
}
|
||||
let numberOfUnsynced = self.passwordStore.getNumberOfUnsyncedPasswords()
|
||||
if numberOfUnsynced == 0 {
|
||||
navigationItem.title = "\(title)"
|
||||
} else {
|
||||
navigationItem.title = "\(title) (\(numberOfUnsynced))"
|
||||
}
|
||||
}
|
||||
|
||||
func actOnSearchNotification() {
|
||||
searchController.searchBar.becomeFirstResponder()
|
||||
}
|
||||
|
|
@ -410,27 +405,47 @@ class PasswordsViewController: UIViewController, UITableViewDataSource, UITableV
|
|||
}
|
||||
}
|
||||
|
||||
func updateRefreshControlTitle() {
|
||||
var atribbutedTitle = "Pull to Sync Password Store"
|
||||
atribbutedTitle = "Last Synced: \(Utils.getLastUpdatedTimeString())"
|
||||
refreshControl.attributedTitle = NSAttributedString(string: atribbutedTitle)
|
||||
}
|
||||
|
||||
func reloadTableView(data: [PasswordsTableEntry]) {
|
||||
setNavigationItemTitle()
|
||||
private func reloadTableView(data: [PasswordsTableEntry], anim: CAAnimation? = nil) {
|
||||
// set navigation item
|
||||
var numberOfLocalCommitsString = ""
|
||||
let numberOfLocalCommits = self.passwordStore.numberOfLocalCommits()
|
||||
if numberOfLocalCommits > 0 {
|
||||
numberOfLocalCommitsString = " (\(numberOfLocalCommits))"
|
||||
}
|
||||
if parentPasswordEntity != nil {
|
||||
navigationItem.title = "\(parentPasswordEntity!.name!)\(numberOfLocalCommitsString)"
|
||||
navigationItem.leftBarButtonItem = backUIBarButtonItem
|
||||
} else {
|
||||
navigationItem.title = "Password Store\(numberOfLocalCommitsString)"
|
||||
navigationItem.leftBarButtonItem = nil
|
||||
}
|
||||
|
||||
// set the password table
|
||||
generateSections(item: data)
|
||||
if anim != nil {
|
||||
self.tableView.layer.add(anim!, forKey: "UITableViewReloadDataAnimationKey")
|
||||
}
|
||||
tableView.reloadData()
|
||||
updateRefreshControlTitle()
|
||||
self.tableView.layer.removeAnimation(forKey: "UITableViewReloadDataAnimationKey")
|
||||
|
||||
// set the sync control title
|
||||
let atribbutedTitle = "Last Synced: \(Utils.getLastUpdatedTimeString())"
|
||||
syncControl.attributedTitle = NSAttributedString(string: atribbutedTitle)
|
||||
}
|
||||
|
||||
func handleRefresh(_ refreshControl: UIRefreshControl) {
|
||||
private func reloadTableView(parent: PasswordEntity?, anim: CAAnimation? = nil) {
|
||||
initPasswordsTableEntries(parent: parent)
|
||||
reloadTableView(data: passwordsTableEntries, anim: anim)
|
||||
}
|
||||
|
||||
func reloadTableView() {
|
||||
initPasswordsTableEntries(parent: nil)
|
||||
reloadTableView(data: passwordsTableEntries)
|
||||
}
|
||||
|
||||
func handleRefresh(_ syncControl: UIRefreshControl) {
|
||||
syncPasswords()
|
||||
refreshControl.endRefreshing()
|
||||
syncControl.endRefreshing()
|
||||
}
|
||||
|
||||
func tabBarController(_ tabBarController: UITabBarController, didSelect viewController: UIViewController) {
|
||||
|
|
|
|||
|
|
@ -32,22 +32,22 @@ class SSHKeySettingTableViewController: UITableViewController {
|
|||
}
|
||||
|
||||
func doneButtonTapped(_ sender: UIButton) {
|
||||
guard URL(string: publicKeyURLTextField.text!) != nil else {
|
||||
guard let publicKeyURL = URL(string: publicKeyURLTextField.text!) else {
|
||||
Utils.alert(title: "Cannot Save", message: "Please set Public Key URL first.", controller: self, completion: nil)
|
||||
return
|
||||
}
|
||||
guard URL(string: privateKeyURLTextField.text!) != nil else {
|
||||
guard let privateKeyURL = URL(string: privateKeyURLTextField.text!) else {
|
||||
Utils.alert(title: "Cannot Save", message: "Please set Private Key URL first.", controller: self, completion: nil)
|
||||
return
|
||||
}
|
||||
|
||||
Defaults[.gitRepositorySSHPublicKeyURL] = URL(string: publicKeyURLTextField.text!)
|
||||
Defaults[.gitRepositorySSHPrivateKeyURL] = URL(string: privateKeyURLTextField.text!)
|
||||
Defaults[.gitRepositorySSHPublicKeyURL] = publicKeyURL
|
||||
Defaults[.gitRepositorySSHPrivateKeyURL] = privateKeyURL
|
||||
Utils.addPasswordToKeychain(name: "gitRepositorySSHPrivateKeyPassphrase", password: passphraseTextField.text!)
|
||||
|
||||
do {
|
||||
try Data(contentsOf: Defaults[.gitRepositorySSHPublicKeyURL]!).write(to: Globals.sshPublicKeyURL, options: .atomic)
|
||||
try Data(contentsOf: Defaults[.gitRepositorySSHPrivateKeyURL]!).write(to: Globals.sshPrivateKeyURL, options: .atomic)
|
||||
try Data(contentsOf: publicKeyURL).write(to: Globals.sshPublicKeyURL, options: .atomic)
|
||||
try Data(contentsOf: privateKeyURL).write(to: Globals.sshPrivateKeyURL, options: .atomic)
|
||||
} catch {
|
||||
Utils.alert(title: "Error", message: error.localizedDescription, controller: self, completion: nil)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -140,7 +140,6 @@ class SettingsTableViewController: UITableViewController {
|
|||
}
|
||||
})
|
||||
DispatchQueue.main.async {
|
||||
self.passwordStore.updatePasswordEntityCoreData()
|
||||
Defaults[.lastUpdatedTime] = Date()
|
||||
Defaults[.gitRepositoryURL] = URL(string: gitRepostiroyURL)
|
||||
Defaults[.gitRepositoryUsername] = username
|
||||
|
|
|
|||
|
|
@ -10,7 +10,6 @@ import Foundation
|
|||
import SwiftyUserDefaults
|
||||
|
||||
extension DefaultsKeys {
|
||||
// static let pgpKeyURL = DefaultsKey<URL?>("pgpKeyURL")
|
||||
static let pgpKeySource = DefaultsKey<String?>("pgpKeySource")
|
||||
static let pgpPublicKeyURL = DefaultsKey<URL?>("pgpPublicKeyURL")
|
||||
static let pgpPrivateKeyURL = DefaultsKey<URL?>("pgpPrivateKeyURL")
|
||||
|
|
@ -35,6 +34,6 @@ extension DefaultsKeys {
|
|||
static let isRememberPassphraseOn = DefaultsKey<Bool>("isRememberPassphraseOn")
|
||||
static let isShowFolderOn = DefaultsKey<Bool>("isShowFolderOn")
|
||||
static let passwordGeneratorFlavor = DefaultsKey<String>("passwordGeneratorFlavor")
|
||||
|
||||
|
||||
|
||||
static let encryptInArmored = DefaultsKey<Bool>("encryptInArmored")
|
||||
}
|
||||
|
|
|
|||
|
|
@ -28,6 +28,9 @@ class Globals {
|
|||
static let passwordMaximumLength = 24
|
||||
static let passwordDefaultLength = 16
|
||||
|
||||
static let passwordDots = "••••••••••••"
|
||||
static let passwordFonts = "Menlo"
|
||||
|
||||
private init() { }
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -13,4 +13,7 @@ extension Notification.Name {
|
|||
static let passwordStoreErased = Notification.Name("passwordStoreErased")
|
||||
static let passwordStoreChangeDiscarded = Notification.Name("passwordStoreChangeDiscarded")
|
||||
static let passwordSearch = Notification.Name("passwordSearch")
|
||||
|
||||
static let passwordDisplaySettingChanged = Notification.Name("passwordDisplaySettingChanged")
|
||||
static let passwordDetailDisplaySettingChanged = Notification.Name("passwordDetailDisplaySettingChanged")
|
||||
}
|
||||
|
|
|
|||
|
|
@ -133,7 +133,7 @@ class Utils {
|
|||
for (index, element) in plainPassword.unicodeScalars.enumerated() {
|
||||
if NSCharacterSet.decimalDigits.contains(element) {
|
||||
attributedPassword.addAttribute(NSForegroundColorAttributeName, value: Globals.red, range: NSRange(location: index, length: 1))
|
||||
} else if NSCharacterSet.punctuationCharacters.contains(element) {
|
||||
} else if !NSCharacterSet.letters.contains(element) {
|
||||
attributedPassword.addAttribute(NSForegroundColorAttributeName, value: Globals.blue, range: NSRange(location: index, length: 1))
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -17,7 +17,7 @@
|
|||
<key>CFBundlePackageType</key>
|
||||
<string>APPL</string>
|
||||
<key>CFBundleShortVersionString</key>
|
||||
<string>0.2.1</string>
|
||||
<string>0.2.2</string>
|
||||
<key>CFBundleVersion</key>
|
||||
<string>1</string>
|
||||
<key>ITSAppUsesNonExemptEncryption</key>
|
||||
|
|
|
|||
|
|
@ -17,16 +17,35 @@ struct AdditionField {
|
|||
}
|
||||
|
||||
class Password {
|
||||
static let otpKeywords = ["otp_secret", "otp_type", "otp_algorithm", "otp_period", "otp_digits", "otp_counter"]
|
||||
static let otpKeywords = ["otp_secret", "otp_type", "otp_algorithm", "otp_period", "otp_digits", "otp_counter", "otpauth"]
|
||||
|
||||
var name = ""
|
||||
var password = ""
|
||||
var additions = [String: String]()
|
||||
var additionKeys = [String]()
|
||||
var plainText = ""
|
||||
var changed = false
|
||||
var firstLineIsOTPField = false
|
||||
var otpToken: Token?
|
||||
|
||||
private var plainText = ""
|
||||
private var firstLineIsOTPField = false
|
||||
private var otpToken: Token?
|
||||
|
||||
enum OtpType {
|
||||
case totp, hotp, none
|
||||
}
|
||||
|
||||
var otpType: OtpType {
|
||||
get {
|
||||
guard let token = self.otpToken else {
|
||||
return OtpType.none
|
||||
}
|
||||
switch token.generator.factor {
|
||||
case .counter:
|
||||
return OtpType.hotp
|
||||
case .timer:
|
||||
return OtpType.totp
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
init(name: String, plainText: String) {
|
||||
self.initEverything(name: name, plainText: plainText)
|
||||
|
|
@ -39,7 +58,7 @@ class Password {
|
|||
}
|
||||
}
|
||||
|
||||
func initEverything(name: String, plainText: String) {
|
||||
private func initEverything(name: String, plainText: String) {
|
||||
self.name = name
|
||||
self.plainText = plainText
|
||||
|
||||
|
|
@ -57,7 +76,7 @@ class Password {
|
|||
|
||||
// check whether the first line of the plainText looks like an otp entry
|
||||
let (key, value) = Password.getKeyValuePair(from: plainTextSplit[0])
|
||||
if key != nil && Password.otpKeywords.contains(key!) {
|
||||
if Password.otpKeywords.contains(key ?? "") {
|
||||
firstLineIsOTPField = true
|
||||
self.additions[key!] = value
|
||||
self.additionKeys.insert(key!, at: 0)
|
||||
|
|
@ -79,7 +98,7 @@ class Password {
|
|||
|
||||
// return a key-value pair from the line
|
||||
// key might be nil, if there is no ":" in the line
|
||||
static func getKeyValuePair(from line: String) -> (key: String?, value: String) {
|
||||
static private func getKeyValuePair(from line: String) -> (key: String?, value: String) {
|
||||
let items = line.characters.split(separator: ":", maxSplits: 1, omittingEmptySubsequences: true).map(String.init)
|
||||
var key : String?
|
||||
var value = ""
|
||||
|
|
@ -92,7 +111,7 @@ class Password {
|
|||
return (key, value)
|
||||
}
|
||||
|
||||
static func getAdditionFields(from additionFieldsPlainText: String) -> ([String: String], [String]){
|
||||
static private func getAdditionFields(from additionFieldsPlainText: String) -> ([String: String], [String]){
|
||||
var additions = [String: String]()
|
||||
var additionKeys = [String]()
|
||||
var unknownIndex = 0
|
||||
|
|
@ -125,7 +144,7 @@ class Password {
|
|||
}
|
||||
}
|
||||
|
||||
func getPlainText() -> String {
|
||||
private func getPlainText() -> String {
|
||||
return self.plainText
|
||||
}
|
||||
|
||||
|
|
@ -140,21 +159,37 @@ class Password {
|
|||
/*
|
||||
Set otpType and otpToken, if we are able to construct a valid token.
|
||||
|
||||
Example of TOTP fields
|
||||
Example of TOTP otpauth
|
||||
(Key Uri Format: https://github.com/google/google-authenticator/wiki/Key-Uri-Format)
|
||||
otpauth://totp/totp-secret?secret=AAAAAAAAAAAAAAAA&issuer=totp-secret
|
||||
|
||||
Example of TOTP fields [Legacy, lower priority]
|
||||
otp_secret: secretsecretsecretsecretsecretsecret
|
||||
otp_type: totp
|
||||
otp_algorithm: sha1 (default: sha1, optional)
|
||||
otp_period: 30 (default: 30, optional)
|
||||
otp_digits: 6 (default: 6, optional)
|
||||
|
||||
Example of HOTP fields
|
||||
Example of HOTP fields [Legacy, lower priority]
|
||||
otp_secret: secretsecretsecretsecretsecretsecret
|
||||
otp_type: hotp
|
||||
otp_counter: 1
|
||||
otp_digits: 6 (default: 6, optional)
|
||||
|
||||
*/
|
||||
func updateOtpToken() {
|
||||
private func updateOtpToken() {
|
||||
// get otpauth, if we are able to generate a token, return
|
||||
if var otpauthString = getAdditionValue(withKey: "otpauth") {
|
||||
if !otpauthString.hasPrefix("otpauth:") {
|
||||
otpauthString = "otpauth:\(otpauthString)"
|
||||
}
|
||||
if let otpauthUrl = URL(string: otpauthString),
|
||||
let token = Token(url: otpauthUrl) {
|
||||
self.otpToken = token
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
// get secret data
|
||||
guard let secretString = getAdditionValue(withKey: "otp_secret"),
|
||||
let secretData = MF_Base32Codec.data(fromBase32String: secretString),
|
||||
|
|
@ -175,11 +210,11 @@ class Password {
|
|||
if let algoString = getAdditionValue(withKey: "otp_algorithm") {
|
||||
switch algoString.lowercased() {
|
||||
case "sha256":
|
||||
algorithm = Generator.Algorithm.sha256
|
||||
algorithm = .sha256
|
||||
case "sha512":
|
||||
algorithm = Generator.Algorithm.sha512
|
||||
algorithm = .sha512
|
||||
default:
|
||||
algorithm = Generator.Algorithm.sha1
|
||||
algorithm = .sha1
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -259,4 +294,18 @@ class Password {
|
|||
let otp = self.otpToken?.currentPassword ?? "error"
|
||||
return (description, otp)
|
||||
}
|
||||
|
||||
// return the password strings
|
||||
func getOtp() -> String? {
|
||||
if let otp = self.otpToken?.currentPassword {
|
||||
return otp
|
||||
} else {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
static func LooksLikeOTP(line: String) -> Bool {
|
||||
let (key, _) = getKeyValuePair(from: line)
|
||||
return Password.otpKeywords.contains(key ?? "")
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -35,7 +35,7 @@ extension PasswordEntity {
|
|||
name = password.name
|
||||
let plainData = password.getPlainData()
|
||||
let pgp = PasswordStore.shared.pgp
|
||||
let encryptedData = try pgp.encryptData(plainData, usingPublicKey: pgp.getKeysOf(.public)[0], armored: false)
|
||||
let encryptedData = try pgp.encryptData(plainData, usingPublicKey: pgp.getKeysOf(.public)[0], armored: Defaults[.encryptInArmored])
|
||||
return encryptedData
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -136,6 +136,23 @@ class PasswordStore {
|
|||
}
|
||||
|
||||
let context = (UIApplication.shared.delegate as! AppDelegate).persistentContainer.viewContext
|
||||
|
||||
var numberOfPasswords : Int {
|
||||
return self.fetchPasswordEntityCoreData(withDir: false).count
|
||||
}
|
||||
|
||||
var sizeOfRepositoryByteCount : UInt64 {
|
||||
let fm = FileManager.default
|
||||
var size = UInt64(0)
|
||||
do {
|
||||
if fm.fileExists(atPath: self.storeURL.path) {
|
||||
size = try fm.allocatedSizeOfDirectoryAtURL(directoryURL: self.storeURL)
|
||||
}
|
||||
} catch {
|
||||
print(error)
|
||||
}
|
||||
return size
|
||||
}
|
||||
|
||||
|
||||
private init() {
|
||||
|
|
@ -285,6 +302,8 @@ class PasswordStore {
|
|||
}
|
||||
storeRepository = try GTRepository(url: storeURL)
|
||||
gitCredential = credential
|
||||
self.updatePasswordEntityCoreData()
|
||||
|
||||
NotificationCenter.default.post(name: .passwordStoreUpdated, object: nil)
|
||||
}
|
||||
|
||||
|
|
@ -298,12 +317,12 @@ class PasswordStore {
|
|||
]
|
||||
let remote = try GTRemote(name: "origin", in: storeRepository!)
|
||||
try storeRepository?.pull((storeRepository?.currentBranch())!, from: remote, withOptions: options, progress: transferProgressBlock)
|
||||
self.setAllSynced()
|
||||
self.updatePasswordEntityCoreData()
|
||||
NotificationCenter.default.post(name: .passwordStoreUpdated, object: nil)
|
||||
}
|
||||
|
||||
|
||||
|
||||
func updatePasswordEntityCoreData() {
|
||||
private func updatePasswordEntityCoreData() {
|
||||
deleteCoreData(entityName: "PasswordEntity")
|
||||
let fm = FileManager.default
|
||||
do {
|
||||
|
|
@ -359,6 +378,9 @@ class PasswordStore {
|
|||
}
|
||||
|
||||
func getRecentCommits(count: Int) -> [GTCommit] {
|
||||
guard storeRepository != nil else {
|
||||
return []
|
||||
}
|
||||
var commits = [GTCommit]()
|
||||
do {
|
||||
let enumerator = try GTEnumerator(repository: storeRepository!)
|
||||
|
|
@ -482,19 +504,17 @@ class PasswordStore {
|
|||
return nil
|
||||
}
|
||||
|
||||
func createRemoveCommitInRepository(message: String, filename: String, progressBlock: (_ progress: Float) -> Void) -> GTCommit? {
|
||||
func createRemoveCommitInRepository(message: String, path: String) -> GTCommit? {
|
||||
do {
|
||||
try storeRepository?.index().removeFile(filename)
|
||||
try storeRepository?.index().removeFile(path)
|
||||
try storeRepository?.index().write()
|
||||
let newTree = try storeRepository!.index().writeTree()
|
||||
let headReference = try storeRepository!.headReference()
|
||||
let commitEnum = try GTEnumerator(repository: storeRepository!)
|
||||
try commitEnum.pushSHA(headReference.targetOID.sha!)
|
||||
let parent = commitEnum.nextObject() as! GTCommit
|
||||
progressBlock(0.5)
|
||||
let signature = gitSignatureForNow
|
||||
let commit = try storeRepository!.createCommit(with: newTree, message: message, author: signature, committer: signature, parents: [parent], updatingReferenceNamed: headReference.name)
|
||||
progressBlock(0.7)
|
||||
return commit
|
||||
} catch {
|
||||
print(error)
|
||||
|
|
@ -565,6 +585,18 @@ class PasswordStore {
|
|||
}
|
||||
}
|
||||
|
||||
public func delete(passwordEntity: PasswordEntity) {
|
||||
Utils.removeFileIfExists(at: storeURL.appendingPathComponent(passwordEntity.path!))
|
||||
let _ = createRemoveCommitInRepository(message: "Remove \(passwordEntity.nameWithCategory) from store using Pass for iOS", path: passwordEntity.path!)
|
||||
context.delete(passwordEntity)
|
||||
do {
|
||||
try context.save()
|
||||
} catch {
|
||||
fatalError("Failed to delete a PasswordEntity: \(error)")
|
||||
}
|
||||
NotificationCenter.default.post(name: .passwordStoreUpdated, object: nil)
|
||||
}
|
||||
|
||||
func saveUpdated(passwordEntity: PasswordEntity) {
|
||||
do {
|
||||
try context.save()
|
||||
|
|
@ -634,6 +666,40 @@ class PasswordStore {
|
|||
|
||||
// return the number of discarded commits
|
||||
func reset() throws -> Int {
|
||||
// get a list of local commits
|
||||
if let localCommits = try getLocalCommits(),
|
||||
localCommits.count > 0 {
|
||||
// get the oldest local commit
|
||||
guard let firstLocalCommit = localCommits.last,
|
||||
firstLocalCommit.parents.count == 1,
|
||||
let newHead = firstLocalCommit.parents.first else {
|
||||
throw NSError(domain: "me.mssun.pass.error", code: 1, userInfo: [NSLocalizedDescriptionKey: "Cannot decide how to reset."])
|
||||
}
|
||||
try self.storeRepository?.reset(to: newHead, resetType: GTRepositoryResetType.hard)
|
||||
self.setAllSynced()
|
||||
self.updatePasswordEntityCoreData()
|
||||
NotificationCenter.default.post(name: .passwordStoreUpdated, object: nil)
|
||||
NotificationCenter.default.post(name: .passwordStoreChangeDiscarded, object: nil)
|
||||
return localCommits.count
|
||||
} else {
|
||||
return 0 // no new commit
|
||||
}
|
||||
}
|
||||
|
||||
func numberOfLocalCommits() -> Int {
|
||||
do {
|
||||
if let localCommits = try getLocalCommits() {
|
||||
return localCommits.count
|
||||
} else {
|
||||
return 0
|
||||
}
|
||||
} catch {
|
||||
print(error)
|
||||
}
|
||||
return 0
|
||||
}
|
||||
|
||||
private func getLocalCommits() throws -> [GTCommit]? {
|
||||
// get the remote origin/master branch
|
||||
guard let remoteBranches = try storeRepository?.remoteBranches(),
|
||||
let index = remoteBranches.index(where: { $0.shortName == "master" })
|
||||
|
|
@ -644,22 +710,6 @@ class PasswordStore {
|
|||
//print("remoteMasterBranch \(remoteMasterBranch)")
|
||||
|
||||
// get a list of local commits
|
||||
if let localCommits = try storeRepository?.localCommitsRelative(toRemoteBranch: remoteMasterBranch),
|
||||
localCommits.count > 0 {
|
||||
// get the oldest local commit
|
||||
guard let firstLocalCommit = localCommits.last,
|
||||
firstLocalCommit.parents.count == 1,
|
||||
let newHead = firstLocalCommit.parents.first else {
|
||||
throw NSError(domain: "me.mssun.pass.error", code: 1, userInfo: [NSLocalizedDescriptionKey: "Cannot decide how to reset."])
|
||||
}
|
||||
try self.storeRepository?.reset(to: newHead, resetType: GTRepositoryResetType.hard)
|
||||
self.updatePasswordEntityCoreData()
|
||||
NotificationCenter.default.post(name: .passwordStoreUpdated, object: nil)
|
||||
NotificationCenter.default.post(name: .passwordStoreChangeDiscarded, object: nil)
|
||||
self.setAllSynced()
|
||||
return localCommits.count
|
||||
} else {
|
||||
return 0 // no new commit
|
||||
}
|
||||
return try storeRepository?.localCommitsRelative(toRemoteBranch: remoteMasterBranch)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -23,6 +23,6 @@ class ContentTableViewCell: UITableViewCell {
|
|||
return nil
|
||||
}
|
||||
|
||||
func setContent(content: String) { }
|
||||
func setContent(content: String?) { }
|
||||
|
||||
}
|
||||
|
|
|
|||
|
|
@ -9,7 +9,7 @@
|
|||
import UIKit
|
||||
|
||||
protocol FillPasswordTableViewCellDelegate {
|
||||
func generatePassword() -> String
|
||||
func generateAndCopyPassword()
|
||||
}
|
||||
|
||||
class FillPasswordTableViewCell: ContentTableViewCell {
|
||||
|
|
@ -20,6 +20,7 @@ class FillPasswordTableViewCell: ContentTableViewCell {
|
|||
override func awakeFromNib() {
|
||||
super.awakeFromNib()
|
||||
// Initialization code
|
||||
contentTextField.font = UIFont(name: Globals.passwordFonts, size: (contentTextField.font?.pointSize)!)
|
||||
}
|
||||
|
||||
override func setSelected(_ selected: Bool, animated: Bool) {
|
||||
|
|
@ -29,16 +30,19 @@ class FillPasswordTableViewCell: ContentTableViewCell {
|
|||
}
|
||||
|
||||
@IBAction func generatePassword(_ sender: UIButton) {
|
||||
let plainPassword = self.delegate?.generatePassword() ?? Utils.generatePassword(length: 16)
|
||||
contentTextField.attributedText = Utils.attributedPassword(plainPassword: plainPassword)
|
||||
Utils.copyToPasteboard(textToCopy: plainPassword)
|
||||
self.delegate?.generateAndCopyPassword()
|
||||
}
|
||||
|
||||
// re-color
|
||||
@IBAction func textFieldDidChange(_ sender: UITextField) {
|
||||
contentTextField.attributedText = Utils.attributedPassword(plainPassword: sender.text ?? "")
|
||||
}
|
||||
|
||||
override func getContent() -> String? {
|
||||
return contentTextField.attributedText?.string
|
||||
}
|
||||
|
||||
override func setContent(content: String) {
|
||||
contentTextField.attributedText = Utils.attributedPassword(plainPassword: content)
|
||||
override func setContent(content: String?) {
|
||||
contentTextField.attributedText = Utils.attributedPassword(plainPassword: content ?? "")
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -27,6 +27,9 @@
|
|||
<nil key="textColor"/>
|
||||
<fontDescription key="fontDescription" style="UICTFontTextStyleBody"/>
|
||||
<textInputTraits key="textInputTraits" autocorrectionType="no" spellCheckingType="no" keyboardType="alphabet"/>
|
||||
<connections>
|
||||
<action selector="textFieldDidChange:" destination="KGk-i7-Jjw" eventType="editingChanged" id="U0t-2B-JxY"/>
|
||||
</connections>
|
||||
</textField>
|
||||
<button opaque="NO" contentMode="scaleToFill" contentHorizontalAlignment="center" contentVerticalAlignment="center" buttonType="roundedRect" reversesTitleShadowWhenHighlighted="YES" lineBreakMode="middleTruncation" translatesAutoresizingMaskIntoConstraints="NO" id="hTh-ek-Xam">
|
||||
<rect key="frame" x="243" y="-0.5" width="64" height="89.5"/>
|
||||
|
|
|
|||
|
|
@ -19,15 +19,13 @@ class LabelTableViewCell: UITableViewCell {
|
|||
|
||||
@IBOutlet weak var contentLabel: UILabel!
|
||||
@IBOutlet weak var titleLabel: UILabel!
|
||||
let passwordStore = PasswordStore.shared
|
||||
|
||||
var isPasswordCell = false
|
||||
var isURLCell = false
|
||||
var isReveal = false
|
||||
var isHOTPCell = false
|
||||
let passwordDots = "••••••••••••"
|
||||
|
||||
weak var passwordTableView : PasswordDetailTableViewController?
|
||||
weak var delegatePasswordTableView : PasswordDetailTableViewController?
|
||||
|
||||
var cellData: LabelTableViewCellData? {
|
||||
didSet {
|
||||
|
|
@ -36,14 +34,14 @@ class LabelTableViewCell: UITableViewCell {
|
|||
if isReveal {
|
||||
contentLabel.attributedText = Utils.attributedPassword(plainPassword: cellData?.content ?? "")
|
||||
} else {
|
||||
contentLabel.text = passwordDots
|
||||
contentLabel.text = Globals.passwordDots
|
||||
}
|
||||
contentLabel.font = UIFont(name: "Menlo", size: contentLabel.font.pointSize)
|
||||
contentLabel.font = UIFont(name: Globals.passwordFonts, size: contentLabel.font.pointSize)
|
||||
} else if isHOTPCell {
|
||||
if isReveal {
|
||||
contentLabel.text = cellData?.content ?? ""
|
||||
} else {
|
||||
contentLabel.text = passwordDots
|
||||
contentLabel.text = Globals.passwordDots
|
||||
}
|
||||
} else {
|
||||
contentLabel.text = cellData?.content
|
||||
|
|
@ -78,9 +76,9 @@ class LabelTableViewCell: UITableViewCell {
|
|||
}
|
||||
if isHOTPCell {
|
||||
if isReveal {
|
||||
return action == #selector(copy(_:)) || action == #selector(LabelTableViewCell.concealPassword(_:)) || action == #selector(LabelTableViewCell.nextPassword(_:))
|
||||
return action == #selector(copy(_:)) || action == #selector(LabelTableViewCell.concealPassword(_:)) || action == #selector(LabelTableViewCell.getNextHOTP(_:))
|
||||
} else {
|
||||
return action == #selector(copy(_:)) || action == #selector(LabelTableViewCell.revealPassword(_:)) || action == #selector(LabelTableViewCell.nextPassword(_:))
|
||||
return action == #selector(copy(_:)) || action == #selector(LabelTableViewCell.revealPassword(_:)) || action == #selector(LabelTableViewCell.getNextHOTP(_:))
|
||||
}
|
||||
}
|
||||
return action == #selector(copy(_:))
|
||||
|
|
@ -104,48 +102,17 @@ class LabelTableViewCell: UITableViewCell {
|
|||
}
|
||||
|
||||
func concealPassword(_ sender: Any?) {
|
||||
contentLabel.text = passwordDots
|
||||
contentLabel.text = Globals.passwordDots
|
||||
isReveal = false
|
||||
}
|
||||
|
||||
func nextPassword(_ sender: Any?) {
|
||||
guard let password = passwordTableView?.password,
|
||||
let passwordEntity = passwordTableView?.passwordEntity else {
|
||||
print("Cannot find password/passwordEntity of a cell")
|
||||
return;
|
||||
}
|
||||
|
||||
// increase HOTP counter
|
||||
password.increaseHotpCounter()
|
||||
|
||||
// only the HOTP password needs update
|
||||
if let plainPassword = password.otpToken?.currentPassword {
|
||||
cellData?.content = plainPassword
|
||||
// contentLabel will be updated automatically
|
||||
}
|
||||
|
||||
// commit
|
||||
if password.changed {
|
||||
DispatchQueue.global(qos: .userInitiated).async {
|
||||
self.passwordStore.update(passwordEntity: passwordEntity, password: password, progressBlock: {_ in })
|
||||
DispatchQueue.main.async {
|
||||
passwordEntity.synced = false
|
||||
self.passwordStore.saveUpdated(passwordEntity: passwordEntity)
|
||||
// reload so that the "unsynced" symbol could be added
|
||||
self.passwordTableView?.tableView.reloadRows(at: [IndexPath(row: 0, section: 0)], with: UITableViewRowAnimation.automatic)
|
||||
SVProgressHUD.showSuccess(withStatus: "Password Copied\nCounter Updated")
|
||||
SVProgressHUD.dismiss(withDelay: 1)
|
||||
}
|
||||
}
|
||||
}
|
||||
func openLink(_ sender: Any?) {
|
||||
// if isURLCell, passwordTableView should not be nil
|
||||
delegatePasswordTableView!.openLink()
|
||||
}
|
||||
|
||||
func openLink(_ sender: Any?) {
|
||||
guard let password = passwordTableView?.password else {
|
||||
print("Cannot find password of a cell")
|
||||
return;
|
||||
}
|
||||
Utils.copyToPasteboard(textToCopy: password.password)
|
||||
UIApplication.shared.open(URL(string: cellData!.content)!, options: [:], completionHandler: nil)
|
||||
func getNextHOTP(_ sender: Any?) {
|
||||
// if isHOTPCell, passwordTableView should not be nil
|
||||
delegatePasswordTableView!.getNextHOTP()
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -9,15 +9,21 @@
|
|||
|
||||
import UIKit
|
||||
|
||||
protocol PasswordSettingSliderTableViewCellDelegate {
|
||||
func generateAndCopyPassword()
|
||||
}
|
||||
|
||||
class SliderTableViewCell: ContentTableViewCell {
|
||||
|
||||
@IBOutlet weak var titleLabel: UILabel!
|
||||
@IBOutlet weak var valueLabel: UILabel!
|
||||
@IBOutlet weak var slider: UISlider!
|
||||
|
||||
var delegate: UITableViewController?
|
||||
|
||||
var roundedValue: Int {
|
||||
get {
|
||||
return Int(slider.value)
|
||||
return Int(valueLabel.text!)!
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -33,9 +39,17 @@ class SliderTableViewCell: ContentTableViewCell {
|
|||
}
|
||||
|
||||
@IBAction func handleSliderValueChange(_ sender: UISlider) {
|
||||
let roundedValue = round(sender.value)
|
||||
sender.value = roundedValue
|
||||
valueLabel.text = "\(Int(roundedValue))"
|
||||
let oldRoundedValue = self.roundedValue
|
||||
let newRoundedValue = Int(sender.value)
|
||||
// proceed only when the rounded value gets updated
|
||||
guard newRoundedValue != oldRoundedValue else {
|
||||
return;
|
||||
}
|
||||
sender.value = Float(newRoundedValue)
|
||||
valueLabel.text = "\(newRoundedValue)"
|
||||
if let delegate: PasswordSettingSliderTableViewCellDelegate = self.delegate as? PasswordSettingSliderTableViewCellDelegate {
|
||||
delegate.generateAndCopyPassword()
|
||||
}
|
||||
}
|
||||
|
||||
func reset(title: String, minimumValue: Int, maximumValue: Int, defaultValue: Int) {
|
||||
|
|
|
|||
|
|
@ -13,20 +13,20 @@
|
|||
<placeholder placeholderIdentifier="IBFilesOwner" id="-1" userLabel="File's Owner"/>
|
||||
<placeholder placeholderIdentifier="IBFirstResponder" id="-2" customClass="UIResponder"/>
|
||||
<tableViewCell contentMode="scaleToFill" selectionStyle="default" indentationWidth="10" rowHeight="71" id="KGk-i7-Jjw" customClass="SliderTableViewCell" customModule="pass" customModuleProvider="target">
|
||||
<rect key="frame" x="0.0" y="0.0" width="320" height="71"/>
|
||||
<rect key="frame" x="0.0" y="0.0" width="320" height="74"/>
|
||||
<autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMaxY="YES"/>
|
||||
<tableViewCellContentView key="contentView" opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center" tableViewCell="KGk-i7-Jjw" id="H2p-sc-9uM">
|
||||
<rect key="frame" x="0.0" y="0.0" width="320" height="70.5"/>
|
||||
<rect key="frame" x="0.0" y="0.0" width="320" height="73.5"/>
|
||||
<autoresizingMask key="autoresizingMask"/>
|
||||
<subviews>
|
||||
<slider opaque="NO" contentMode="scaleToFill" contentHorizontalAlignment="center" contentVerticalAlignment="center" value="0.5" minValue="0.0" maxValue="1" translatesAutoresizingMaskIntoConstraints="NO" id="MwT-Jl-hhE">
|
||||
<rect key="frame" x="60.5" y="20.5" width="205.5" height="31"/>
|
||||
<rect key="frame" x="60.5" y="22" width="205.5" height="31"/>
|
||||
<connections>
|
||||
<action selector="handleSliderValueChange:" destination="KGk-i7-Jjw" eventType="valueChanged" id="WwM-ZE-yIB"/>
|
||||
</connections>
|
||||
</slider>
|
||||
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="88" textAlignment="right" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="GJP-Fj-VZt" userLabel="Value">
|
||||
<rect key="frame" x="281" y="8" width="24" height="54.5"/>
|
||||
<rect key="frame" x="281" y="8" width="24" height="57.5"/>
|
||||
<constraints>
|
||||
<constraint firstAttribute="width" constant="24" id="tOG-yp-eFw"/>
|
||||
</constraints>
|
||||
|
|
@ -35,7 +35,7 @@
|
|||
<nil key="highlightedColor"/>
|
||||
</label>
|
||||
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="Title" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="t7T-HC-hUd" userLabel="Title">
|
||||
<rect key="frame" x="15" y="8" width="30" height="54.5"/>
|
||||
<rect key="frame" x="15" y="8" width="30" height="57.5"/>
|
||||
<constraints>
|
||||
<constraint firstAttribute="height" relation="greaterThanOrEqual" constant="38" id="8Tz-Qo-Mkg"/>
|
||||
</constraints>
|
||||
|
|
|
|||
|
|
@ -23,7 +23,7 @@ class TextFieldTableViewCell: ContentTableViewCell {
|
|||
override func getContent() -> String? {
|
||||
return contentTextField.text
|
||||
}
|
||||
override func setContent(content: String) {
|
||||
override func setContent(content: String?) {
|
||||
contentTextField.text = content
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -22,7 +22,7 @@ class TextViewTableViewCell: ContentTableViewCell {
|
|||
return contentTextView.text
|
||||
}
|
||||
|
||||
override func setContent(content: String) {
|
||||
override func setContent(content: String?) {
|
||||
contentTextView.text = content
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<document type="com.apple.InterfaceBuilder3.CocoaTouch.XIB" version="3.0" toolsVersion="12106.1" systemVersion="16E154a" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" useTraitCollections="YES" colorMatched="YES">
|
||||
<document type="com.apple.InterfaceBuilder3.CocoaTouch.XIB" version="3.0" toolsVersion="12106.1" systemVersion="16E189a" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" useTraitCollections="YES" colorMatched="YES">
|
||||
<device id="retina4_7" orientation="portrait">
|
||||
<adaptation id="fullscreen"/>
|
||||
</device>
|
||||
|
|
@ -20,10 +20,10 @@
|
|||
<autoresizingMask key="autoresizingMask"/>
|
||||
<subviews>
|
||||
<textView clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="scaleToFill" translatesAutoresizingMaskIntoConstraints="NO" id="xHX-Sh-1pR">
|
||||
<rect key="frame" x="15" y="8" width="297" height="200.5"/>
|
||||
<rect key="frame" x="15" y="15" width="297" height="186.5"/>
|
||||
<color key="backgroundColor" white="1" alpha="1" colorSpace="calibratedWhite"/>
|
||||
<constraints>
|
||||
<constraint firstAttribute="height" relation="greaterThanOrEqual" constant="160" id="Tvq-j8-Nvh"/>
|
||||
<constraint firstAttribute="height" relation="greaterThanOrEqual" constant="120" id="Tvq-j8-Nvh"/>
|
||||
</constraints>
|
||||
<fontDescription key="fontDescription" name="Menlo-Regular" family="Menlo" pointSize="14"/>
|
||||
<textInputTraits key="textInputTraits" autocapitalizationType="sentences" autocorrectionType="no"/>
|
||||
|
|
@ -32,8 +32,8 @@
|
|||
<constraints>
|
||||
<constraint firstAttribute="trailingMargin" secondItem="xHX-Sh-1pR" secondAttribute="trailing" id="LWS-JW-9dS"/>
|
||||
<constraint firstItem="xHX-Sh-1pR" firstAttribute="leading" secondItem="H2p-sc-9uM" secondAttribute="leadingMargin" constant="7" id="SRq-7t-Gyr"/>
|
||||
<constraint firstAttribute="bottomMargin" secondItem="xHX-Sh-1pR" secondAttribute="bottom" id="UPQ-jk-QJR"/>
|
||||
<constraint firstItem="xHX-Sh-1pR" firstAttribute="top" secondItem="H2p-sc-9uM" secondAttribute="topMargin" id="gwb-2C-4wp"/>
|
||||
<constraint firstAttribute="bottomMargin" secondItem="xHX-Sh-1pR" secondAttribute="bottom" constant="7" id="UPQ-jk-QJR"/>
|
||||
<constraint firstItem="xHX-Sh-1pR" firstAttribute="top" secondItem="H2p-sc-9uM" secondAttribute="topMargin" constant="7" id="gwb-2C-4wp"/>
|
||||
</constraints>
|
||||
</tableViewCellContentView>
|
||||
<connections>
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue