AddAccount sheets done

This commit is contained in:
Stuart Breckenridge
2022-12-18 15:51:42 +08:00
parent 8108f1ab0f
commit 4ed11c0fc6
26 changed files with 536 additions and 1741 deletions

View File

@@ -1,810 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<document type="com.apple.InterfaceBuilder3.CocoaTouch.Storyboard.XIB" version="3.0" toolsVersion="20037" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" useTraitCollections="YES" useSafeAreas="YES" colorMatched="YES">
<device id="retina6_1" orientation="portrait" appearance="light"/>
<dependencies>
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="20020"/>
<capability name="Named colors" minToolsVersion="9.0"/>
<capability name="System colors in document resources" minToolsVersion="11.0"/>
<capability name="documents saved in the Xcode 8 format" minToolsVersion="8.0"/>
</dependencies>
<scenes>
<!--Modal Navigation Controller-->
<scene sceneID="98f-PW-S1C">
<objects>
<navigationController storyboardIdentifier="LocalAccountNavigationViewController" id="TMY-HB-vAu" customClass="ModalNavigationController" customModule="NetNewsWire" customModuleProvider="target" sceneMemberID="viewController">
<navigationBar key="navigationBar" contentMode="scaleToFill" insetsLayoutMarginsFromSafeArea="NO" id="p8g-7e-3f4">
<rect key="frame" x="0.0" y="48" width="414" height="44"/>
<autoresizingMask key="autoresizingMask"/>
</navigationBar>
<connections>
<segue destination="vi3-jb-8XS" kind="relationship" relationship="rootViewController" id="dIe-7d-ZQX"/>
</connections>
</navigationController>
<placeholder placeholderIdentifier="IBFirstResponder" id="6sV-68-OXu" userLabel="First Responder" sceneMemberID="firstResponder"/>
</objects>
<point key="canvasLocation" x="1880" y="-528"/>
</scene>
<!--Modal Navigation Controller-->
<scene sceneID="6i4-ho-e4F">
<objects>
<navigationController storyboardIdentifier="FeedbinAccountNavigationViewController" id="sFg-MZ-PqJ" customClass="ModalNavigationController" customModule="NetNewsWire" customModuleProvider="target" sceneMemberID="viewController">
<navigationBar key="navigationBar" contentMode="scaleToFill" insetsLayoutMarginsFromSafeArea="NO" id="wq6-np-tNn">
<rect key="frame" x="0.0" y="48" width="414" height="44"/>
<autoresizingMask key="autoresizingMask"/>
</navigationBar>
<connections>
<segue destination="ECy-jg-Kyc" kind="relationship" relationship="rootViewController" id="usT-8C-GGf"/>
</connections>
</navigationController>
<placeholder placeholderIdentifier="IBFirstResponder" id="Lfz-4s-0Vn" userLabel="First Responder" sceneMemberID="firstResponder"/>
</objects>
<point key="canvasLocation" x="3177" y="-528"/>
</scene>
<!--On My Device-->
<scene sceneID="J93-FN-Yey">
<objects>
<tableViewController id="vi3-jb-8XS" customClass="LocalAccountViewController" customModule="NetNewsWire" customModuleProvider="target" sceneMemberID="viewController">
<tableView key="view" clipsSubviews="YES" contentMode="scaleToFill" alwaysBounceVertical="YES" dataMode="static" style="insetGrouped" separatorStyle="default" rowHeight="-1" estimatedRowHeight="-1" sectionHeaderHeight="18" sectionFooterHeight="18" id="YLa-nM-G7t">
<rect key="frame" x="0.0" y="0.0" width="414" height="896"/>
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
<view key="tableFooterView" contentMode="scaleToFill" id="Vxr-5V-V6R">
<rect key="frame" x="0.0" y="159" width="414" height="150"/>
<autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMaxY="YES"/>
<subviews>
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="Local accounts do not sync your feeds across devices." textAlignment="center" lineBreakMode="wordWrap" numberOfLines="0" baselineAdjustment="alignBaselines" adjustsFontForContentSizeCategory="YES" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="5ce-ZL-glQ">
<rect key="frame" x="20" y="8" width="373" height="13.5"/>
<fontDescription key="fontDescription" style="UICTFontTextStyleCaption1"/>
<color key="textColor" systemColor="secondaryLabelColor"/>
<nil key="highlightedColor"/>
</label>
</subviews>
<color key="backgroundColor" systemColor="systemGroupedBackgroundColor"/>
<constraints>
<constraint firstAttribute="trailing" secondItem="5ce-ZL-glQ" secondAttribute="trailing" constant="21" id="YLV-d0-1us"/>
<constraint firstItem="5ce-ZL-glQ" firstAttribute="leading" secondItem="Vxr-5V-V6R" secondAttribute="leading" constant="20" symbolic="YES" id="dmE-Zi-5FR"/>
<constraint firstItem="5ce-ZL-glQ" firstAttribute="top" secondItem="Vxr-5V-V6R" secondAttribute="top" constant="8" id="z4G-hO-VUE"/>
</constraints>
</view>
<sections>
<tableViewSection id="TfM-Jc-Fr0">
<cells>
<tableViewCell clipsSubviews="YES" contentMode="scaleToFill" preservesSuperviewLayoutMargins="YES" selectionStyle="none" indentationWidth="10" id="uFU-j6-qP1">
<rect key="frame" x="20" y="18" width="374" height="43.5"/>
<autoresizingMask key="autoresizingMask"/>
<tableViewCellContentView key="contentView" opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center" preservesSuperviewLayoutMargins="YES" insetsLayoutMarginsFromSafeArea="NO" tableViewCell="uFU-j6-qP1" id="fr4-mL-3Yf">
<rect key="frame" x="0.0" y="0.0" width="374" height="43.5"/>
<autoresizingMask key="autoresizingMask"/>
<subviews>
<textField opaque="NO" contentMode="scaleToFill" contentHorizontalAlignment="left" contentVerticalAlignment="center" placeholder="Name" textAlignment="natural" adjustsFontForContentSizeCategory="YES" minimumFontSize="17" translatesAutoresizingMaskIntoConstraints="NO" id="Yl1-R6-xZi">
<rect key="frame" x="20" y="12.5" width="334" height="18.5"/>
<fontDescription key="fontDescription" style="UICTFontTextStyleBody"/>
<textInputTraits key="textInputTraits" autocapitalizationType="words"/>
</textField>
</subviews>
<constraints>
<constraint firstItem="Yl1-R6-xZi" firstAttribute="leading" secondItem="fr4-mL-3Yf" secondAttribute="leading" constant="20" id="HJ4-VN-e9Y"/>
<constraint firstAttribute="trailing" secondItem="Yl1-R6-xZi" secondAttribute="trailing" constant="20" id="vbZ-dD-yZM"/>
<constraint firstItem="Yl1-R6-xZi" firstAttribute="centerY" secondItem="fr4-mL-3Yf" secondAttribute="centerY" id="zsZ-z6-IFh"/>
</constraints>
</tableViewCellContentView>
</tableViewCell>
</cells>
</tableViewSection>
<tableViewSection id="Sgf-NV-3Di">
<cells>
<tableViewCell clipsSubviews="YES" contentMode="scaleToFill" preservesSuperviewLayoutMargins="YES" selectionStyle="none" indentationWidth="10" id="pTk-WJ-j5h" customClass="VibrantTableViewCell" customModule="NetNewsWire" customModuleProvider="target">
<rect key="frame" x="20" y="97.5" width="374" height="43.5"/>
<autoresizingMask key="autoresizingMask"/>
<tableViewCellContentView key="contentView" opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center" preservesSuperviewLayoutMargins="YES" insetsLayoutMarginsFromSafeArea="NO" tableViewCell="pTk-WJ-j5h" id="ahe-yz-PGg">
<rect key="frame" x="0.0" y="0.0" width="374" height="43.5"/>
<autoresizingMask key="autoresizingMask"/>
<subviews>
<button opaque="NO" contentMode="scaleToFill" contentHorizontalAlignment="center" contentVerticalAlignment="center" lineBreakMode="middleTruncation" translatesAutoresizingMaskIntoConstraints="NO" id="mQv-3O-Y2d" customClass="VibrantButton" customModule="NetNewsWire" customModuleProvider="target">
<rect key="frame" x="0.0" y="-0.5" width="374" height="44.5"/>
<constraints>
<constraint firstAttribute="height" constant="44" id="EEL-8n-nHO"/>
</constraints>
<fontDescription key="fontDescription" style="UICTFontTextStyleBody"/>
<state key="normal" title="Add Account">
<color key="titleColor" name="secondaryAccentColor"/>
</state>
<connections>
<action selector="add:" destination="vi3-jb-8XS" eventType="touchUpInside" id="lCb-LW-xZ0"/>
</connections>
</button>
</subviews>
<constraints>
<constraint firstItem="mQv-3O-Y2d" firstAttribute="centerY" secondItem="ahe-yz-PGg" secondAttribute="centerY" id="6bl-bA-qYE"/>
<constraint firstItem="mQv-3O-Y2d" firstAttribute="leading" secondItem="ahe-yz-PGg" secondAttribute="leading" id="7gZ-8n-bWs"/>
<constraint firstAttribute="trailing" secondItem="mQv-3O-Y2d" secondAttribute="trailing" id="FQu-yU-a4k"/>
</constraints>
</tableViewCellContentView>
</tableViewCell>
</cells>
</tableViewSection>
</sections>
<connections>
<outlet property="dataSource" destination="vi3-jb-8XS" id="U1Z-Kw-46j"/>
<outlet property="delegate" destination="vi3-jb-8XS" id="4El-ci-jdg"/>
</connections>
</tableView>
<navigationItem key="navigationItem" title="On My Device" id="AOA-LS-PIB">
<barButtonItem key="leftBarButtonItem" systemItem="cancel" id="b2H-re-cgN">
<connections>
<action selector="cancel:" destination="vi3-jb-8XS" id="gRE-sR-r4Z"/>
</connections>
</barButtonItem>
</navigationItem>
<connections>
<outlet property="footerLabel" destination="5ce-ZL-glQ" id="V50-Yc-hD6"/>
<outlet property="nameTextField" destination="Yl1-R6-xZi" id="jcl-vI-Rde"/>
</connections>
</tableViewController>
<placeholder placeholderIdentifier="IBFirstResponder" id="XJD-sO-MSq" userLabel="First Responder" customClass="UIResponder" sceneMemberID="firstResponder"/>
</objects>
<point key="canvasLocation" x="1879.7101449275365" y="144.64285714285714"/>
</scene>
<!--Feedbin-->
<scene sceneID="IDj-HA-olN">
<objects>
<tableViewController id="ECy-jg-Kyc" customClass="FeedbinAccountViewController" customModule="NetNewsWire" customModuleProvider="target" sceneMemberID="viewController">
<tableView key="view" clipsSubviews="YES" contentMode="scaleToFill" alwaysBounceVertical="YES" dataMode="static" style="insetGrouped" separatorStyle="default" rowHeight="-1" estimatedRowHeight="-1" sectionHeaderHeight="18" sectionFooterHeight="18" id="Y0x-RC-7ln">
<rect key="frame" x="0.0" y="0.0" width="414" height="896"/>
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
<view key="tableFooterView" contentMode="scaleToFill" id="3KO-DU-JXG">
<rect key="frame" x="0.0" y="202.5" width="414" height="150"/>
<autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMaxY="YES"/>
<subviews>
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" textAlignment="center" lineBreakMode="wordWrap" numberOfLines="0" baselineAdjustment="alignBaselines" adjustsFontForContentSizeCategory="YES" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="sgL-0C-JZa">
<rect key="frame" x="20" y="8" width="373" height="65.5"/>
<string key="text">Sign in to your Feedbin account to sync your feeds across your devices. Your username and password will be encrypted and stored in Keychain.
Dont have a Feedbin account?</string>
<fontDescription key="fontDescription" style="UICTFontTextStyleCaption1"/>
<color key="textColor" systemColor="secondaryLabelColor"/>
<nil key="highlightedColor"/>
</label>
<button opaque="NO" contentMode="scaleToFill" contentHorizontalAlignment="center" contentVerticalAlignment="center" buttonType="system" lineBreakMode="middleTruncation" translatesAutoresizingMaskIntoConstraints="NO" id="Xhf-bK-vzm">
<rect key="frame" x="172" y="72" width="70" height="26"/>
<fontDescription key="fontDescription" style="UICTFontTextStyleCaption1"/>
<state key="normal" title="Sign Up Here"/>
<connections>
<action selector="signUpWithProvider:" destination="ECy-jg-Kyc" eventType="touchUpInside" id="fIY-hq-q3H"/>
</connections>
</button>
</subviews>
<color key="backgroundColor" systemColor="systemGroupedBackgroundColor"/>
<constraints>
<constraint firstItem="sgL-0C-JZa" firstAttribute="top" secondItem="3KO-DU-JXG" secondAttribute="top" constant="8" id="BgR-gH-qHf"/>
<constraint firstItem="sgL-0C-JZa" firstAttribute="leading" secondItem="3KO-DU-JXG" secondAttribute="leading" constant="20" symbolic="YES" id="PLI-kz-MMq"/>
<constraint firstItem="Xhf-bK-vzm" firstAttribute="top" secondItem="sgL-0C-JZa" secondAttribute="bottom" constant="-1.5" id="R9l-5y-aMr"/>
<constraint firstAttribute="trailing" secondItem="sgL-0C-JZa" secondAttribute="trailing" constant="21" id="ddS-HE-f1J"/>
<constraint firstItem="Xhf-bK-vzm" firstAttribute="centerX" secondItem="3KO-DU-JXG" secondAttribute="centerX" id="xs6-P4-4Vj"/>
</constraints>
</view>
<sections>
<tableViewSection id="xBN-Pb-KAy">
<cells>
<tableViewCell clipsSubviews="YES" contentMode="scaleToFill" preservesSuperviewLayoutMargins="YES" selectionStyle="none" indentationWidth="10" id="lsa-Fl-Pc7">
<rect key="frame" x="20" y="18" width="374" height="43.5"/>
<autoresizingMask key="autoresizingMask"/>
<tableViewCellContentView key="contentView" opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center" preservesSuperviewLayoutMargins="YES" insetsLayoutMarginsFromSafeArea="NO" tableViewCell="lsa-Fl-Pc7" id="Lpd-D1-1PQ">
<rect key="frame" x="0.0" y="0.0" width="374" height="43.5"/>
<autoresizingMask key="autoresizingMask"/>
<subviews>
<textField opaque="NO" contentMode="scaleToFill" contentHorizontalAlignment="left" contentVerticalAlignment="center" placeholder="Email" textAlignment="natural" adjustsFontForContentSizeCategory="YES" minimumFontSize="17" translatesAutoresizingMaskIntoConstraints="NO" id="vJa-NN-yjR">
<rect key="frame" x="20" y="12.5" width="334" height="18.5"/>
<fontDescription key="fontDescription" style="UICTFontTextStyleBody"/>
<textInputTraits key="textInputTraits" spellCheckingType="no" keyboardType="emailAddress" textContentType="username"/>
</textField>
</subviews>
<constraints>
<constraint firstAttribute="trailing" secondItem="vJa-NN-yjR" secondAttribute="trailing" constant="20" id="7xY-Mz-Szf"/>
<constraint firstItem="vJa-NN-yjR" firstAttribute="centerY" secondItem="Lpd-D1-1PQ" secondAttribute="centerY" id="E8M-nD-KIN"/>
<constraint firstItem="vJa-NN-yjR" firstAttribute="leading" secondItem="Lpd-D1-1PQ" secondAttribute="leading" constant="20" id="Lgm-1L-4xL"/>
</constraints>
</tableViewCellContentView>
</tableViewCell>
<tableViewCell clipsSubviews="YES" contentMode="scaleToFill" preservesSuperviewLayoutMargins="YES" selectionStyle="none" indentationWidth="10" id="Hwv-Q0-zT0">
<rect key="frame" x="20" y="61.5" width="374" height="43.5"/>
<autoresizingMask key="autoresizingMask"/>
<tableViewCellContentView key="contentView" opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center" preservesSuperviewLayoutMargins="YES" insetsLayoutMarginsFromSafeArea="NO" tableViewCell="Hwv-Q0-zT0" id="jIT-5L-d8d">
<rect key="frame" x="0.0" y="0.0" width="374" height="43.5"/>
<autoresizingMask key="autoresizingMask"/>
<subviews>
<textField opaque="NO" contentMode="scaleToFill" contentHorizontalAlignment="left" contentVerticalAlignment="center" placeholder="Password" textAlignment="natural" adjustsFontForContentSizeCategory="YES" minimumFontSize="17" translatesAutoresizingMaskIntoConstraints="NO" id="YC2-RH-QoV">
<rect key="frame" x="20" y="13.5" width="290" height="17"/>
<fontDescription key="fontDescription" style="UICTFontTextStyleBody"/>
<textInputTraits key="textInputTraits" secureTextEntry="YES" textContentType="password"/>
</textField>
<button opaque="NO" contentMode="scaleToFill" horizontalHuggingPriority="251" contentHorizontalAlignment="center" contentVerticalAlignment="center" buttonType="system" lineBreakMode="middleTruncation" pointerInteraction="YES" translatesAutoresizingMaskIntoConstraints="NO" id="TfW-wf-V06">
<rect key="frame" x="318" y="7.5" width="36" height="29"/>
<fontDescription key="fontDescription" style="UICTFontTextStyleBody"/>
<state key="normal" title="Show"/>
<connections>
<action selector="showHidePassword:" destination="ECy-jg-Kyc" eventType="touchUpInside" id="QcS-lr-SPG"/>
</connections>
</button>
</subviews>
<constraints>
<constraint firstItem="TfW-wf-V06" firstAttribute="leading" secondItem="YC2-RH-QoV" secondAttribute="trailing" constant="8" symbolic="YES" id="MHu-Ut-Kox"/>
<constraint firstItem="TfW-wf-V06" firstAttribute="centerY" secondItem="jIT-5L-d8d" secondAttribute="centerY" id="O3Y-Jd-n9t"/>
<constraint firstItem="YC2-RH-QoV" firstAttribute="leading" secondItem="jIT-5L-d8d" secondAttribute="leading" constant="20" id="W79-WW-Buk"/>
<constraint firstItem="YC2-RH-QoV" firstAttribute="centerY" secondItem="jIT-5L-d8d" secondAttribute="centerY" id="iDt-ym-Qjf"/>
<constraint firstAttribute="trailing" secondItem="TfW-wf-V06" secondAttribute="trailing" constant="20" symbolic="YES" id="rMZ-af-tPg"/>
</constraints>
</tableViewCellContentView>
</tableViewCell>
</cells>
</tableViewSection>
<tableViewSection id="Kkf-rn-qdv">
<cells>
<tableViewCell clipsSubviews="YES" contentMode="scaleToFill" preservesSuperviewLayoutMargins="YES" selectionStyle="none" indentationWidth="10" id="hWd-EN-p7e">
<rect key="frame" x="20" y="141" width="374" height="43.5"/>
<autoresizingMask key="autoresizingMask"/>
<tableViewCellContentView key="contentView" opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center" preservesSuperviewLayoutMargins="YES" insetsLayoutMarginsFromSafeArea="NO" tableViewCell="hWd-EN-p7e" id="S8S-1a-vVf">
<rect key="frame" x="0.0" y="0.0" width="374" height="43.5"/>
<autoresizingMask key="autoresizingMask"/>
<subviews>
<button opaque="NO" contentMode="scaleToFill" enabled="NO" contentHorizontalAlignment="center" contentVerticalAlignment="center" lineBreakMode="middleTruncation" translatesAutoresizingMaskIntoConstraints="NO" id="gv7-yG-aE3" customClass="VibrantButton" customModule="NetNewsWire" customModuleProvider="target">
<rect key="frame" x="0.0" y="-0.5" width="374" height="44.5"/>
<constraints>
<constraint firstAttribute="height" constant="44" id="pt0-rn-0JI"/>
</constraints>
<fontDescription key="fontDescription" style="UICTFontTextStyleBody"/>
<state key="normal" title="Action">
<color key="titleColor" name="secondaryAccentColor"/>
</state>
<connections>
<action selector="action:" destination="ECy-jg-Kyc" eventType="touchUpInside" id="79h-R2-s0C"/>
</connections>
</button>
</subviews>
<constraints>
<constraint firstItem="gv7-yG-aE3" firstAttribute="leading" secondItem="S8S-1a-vVf" secondAttribute="leading" id="cbE-1E-Mem"/>
<constraint firstAttribute="trailing" secondItem="gv7-yG-aE3" secondAttribute="trailing" id="tQC-jk-evr"/>
<constraint firstItem="gv7-yG-aE3" firstAttribute="centerY" secondItem="S8S-1a-vVf" secondAttribute="centerY" id="xSU-R7-XQ8"/>
</constraints>
</tableViewCellContentView>
</tableViewCell>
</cells>
</tableViewSection>
</sections>
<connections>
<outlet property="dataSource" destination="ECy-jg-Kyc" id="hUr-Xx-9Ho"/>
<outlet property="delegate" destination="ECy-jg-Kyc" id="DKA-Lp-mNb"/>
</connections>
</tableView>
<navigationItem key="navigationItem" title="Feedbin" id="tYg-9f-kyd">
<barButtonItem key="leftBarButtonItem" systemItem="cancel" id="pfF-Of-5NT">
<connections>
<action selector="cancel:" destination="ECy-jg-Kyc" id="ZKI-gV-ylg"/>
</connections>
</barButtonItem>
<barButtonItem key="rightBarButtonItem" id="Xwp-LO-qff">
<view key="customView" contentMode="scaleToFill" id="cn4-b1-uZa">
<rect key="frame" x="374" y="12" width="20" height="20"/>
<autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMaxY="YES"/>
<subviews>
<activityIndicatorView opaque="NO" contentMode="scaleToFill" horizontalHuggingPriority="750" verticalHuggingPriority="750" fixedFrame="YES" style="medium" translatesAutoresizingMaskIntoConstraints="NO" id="YvV-hB-lzT">
<rect key="frame" x="36" y="6" width="20" height="20"/>
<autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMaxY="YES"/>
</activityIndicatorView>
</subviews>
<color key="backgroundColor" white="0.0" alpha="0.0" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
</view>
</barButtonItem>
</navigationItem>
<connections>
<outlet property="actionButton" destination="gv7-yG-aE3" id="ENc-5A-hQc"/>
<outlet property="activityIndicator" destination="YvV-hB-lzT" id="n1F-tV-5ZV"/>
<outlet property="cancelBarButtonItem" destination="pfF-Of-5NT" id="Zr3-qD-1Yi"/>
<outlet property="emailTextField" destination="vJa-NN-yjR" id="nCF-9W-YsF"/>
<outlet property="footerLabel" destination="sgL-0C-JZa" id="b6I-Mk-2K3"/>
<outlet property="passwordTextField" destination="YC2-RH-QoV" id="qaX-0i-7jq"/>
<outlet property="showHideButton" destination="TfW-wf-V06" id="PbL-67-Nrg"/>
</connections>
</tableViewController>
<placeholder placeholderIdentifier="IBFirstResponder" id="L24-0i-kyr" userLabel="First Responder" customClass="UIResponder" sceneMemberID="firstResponder"/>
</objects>
<point key="canvasLocation" x="3177" y="145"/>
</scene>
<!--Modal Navigation Controller-->
<scene sceneID="j4N-ax-exh">
<objects>
<navigationController storyboardIdentifier="NewsBlurAccountNavigationViewController" id="eE3-pu-HdL" customClass="ModalNavigationController" customModule="NetNewsWire" customModuleProvider="target" sceneMemberID="viewController">
<navigationBar key="navigationBar" contentMode="scaleToFill" insetsLayoutMarginsFromSafeArea="NO" id="Fsp-NG-hoR">
<rect key="frame" x="0.0" y="48" width="414" height="44"/>
<autoresizingMask key="autoresizingMask"/>
</navigationBar>
<connections>
<segue destination="Cge-ND-NpD" kind="relationship" relationship="rootViewController" id="1D5-CN-liN"/>
</connections>
</navigationController>
<placeholder placeholderIdentifier="IBFirstResponder" id="8t3-0U-5vL" userLabel="First Responder" sceneMemberID="firstResponder"/>
</objects>
<point key="canvasLocation" x="4562" y="-528"/>
</scene>
<!--NewsBlur-->
<scene sceneID="tfA-kz-P6O">
<objects>
<tableViewController id="Cge-ND-NpD" customClass="NewsBlurAccountViewController" customModule="NetNewsWire" customModuleProvider="target" sceneMemberID="viewController">
<tableView key="view" clipsSubviews="YES" contentMode="scaleToFill" alwaysBounceVertical="YES" dataMode="static" style="insetGrouped" separatorStyle="default" rowHeight="-1" estimatedRowHeight="-1" sectionHeaderHeight="18" sectionFooterHeight="18" id="fLL-7i-HdK">
<rect key="frame" x="0.0" y="0.0" width="414" height="896"/>
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
<view key="tableFooterView" contentMode="scaleToFill" id="mgO-Iq-dEg">
<rect key="frame" x="0.0" y="202.5" width="414" height="150"/>
<autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMaxY="YES"/>
<subviews>
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" textAlignment="center" lineBreakMode="wordWrap" numberOfLines="0" baselineAdjustment="alignBaselines" adjustsFontForContentSizeCategory="YES" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="fal-e8-3BB">
<rect key="frame" x="20" y="8" width="373" height="65.5"/>
<string key="text">Sign in to your NewsBlur account to sync your feeds across your devices. Your username and password will be encrypted and stored in Keychain.
Dont have a NewsBlur account?</string>
<fontDescription key="fontDescription" style="UICTFontTextStyleCaption1"/>
<color key="textColor" systemColor="secondaryLabelColor"/>
<nil key="highlightedColor"/>
</label>
<button opaque="NO" contentMode="scaleToFill" contentHorizontalAlignment="center" contentVerticalAlignment="center" buttonType="system" lineBreakMode="middleTruncation" translatesAutoresizingMaskIntoConstraints="NO" id="YhB-G0-eeJ">
<rect key="frame" x="172" y="72" width="70" height="26"/>
<fontDescription key="fontDescription" style="UICTFontTextStyleCaption1"/>
<state key="normal" title="Sign Up Here"/>
<connections>
<action selector="signUpWithProvider:" destination="Cge-ND-NpD" eventType="touchUpInside" id="Vfz-DD-Kwm"/>
</connections>
</button>
</subviews>
<color key="backgroundColor" systemColor="systemGroupedBackgroundColor"/>
<constraints>
<constraint firstItem="YhB-G0-eeJ" firstAttribute="centerX" secondItem="mgO-Iq-dEg" secondAttribute="centerX" id="7r5-l6-NNv"/>
<constraint firstItem="fal-e8-3BB" firstAttribute="leading" secondItem="mgO-Iq-dEg" secondAttribute="leading" constant="20" symbolic="YES" id="O4q-GI-2AO"/>
<constraint firstItem="YhB-G0-eeJ" firstAttribute="top" secondItem="fal-e8-3BB" secondAttribute="bottom" constant="-1.5" id="UHc-sh-Xq4"/>
<constraint firstAttribute="trailing" secondItem="fal-e8-3BB" secondAttribute="trailing" constant="21" id="V0d-ny-GRE"/>
<constraint firstItem="fal-e8-3BB" firstAttribute="top" secondItem="mgO-Iq-dEg" secondAttribute="top" constant="8" id="h5B-kg-rZj"/>
</constraints>
</view>
<sections>
<tableViewSection id="I5T-12-2jC">
<cells>
<tableViewCell clipsSubviews="YES" contentMode="scaleToFill" preservesSuperviewLayoutMargins="YES" selectionStyle="none" indentationWidth="10" id="gAY-Bo-c0L">
<rect key="frame" x="20" y="18" width="374" height="43.5"/>
<autoresizingMask key="autoresizingMask"/>
<tableViewCellContentView key="contentView" opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center" preservesSuperviewLayoutMargins="YES" insetsLayoutMarginsFromSafeArea="NO" tableViewCell="gAY-Bo-c0L" id="mqD-6S-DIl">
<rect key="frame" x="0.0" y="0.0" width="374" height="43.5"/>
<autoresizingMask key="autoresizingMask"/>
<subviews>
<textField opaque="NO" contentMode="scaleToFill" contentHorizontalAlignment="left" contentVerticalAlignment="center" placeholder="Username or Email" textAlignment="natural" adjustsFontForContentSizeCategory="YES" minimumFontSize="17" translatesAutoresizingMaskIntoConstraints="NO" id="S4v-fs-DIO">
<rect key="frame" x="20" y="12.5" width="334" height="18.5"/>
<fontDescription key="fontDescription" style="UICTFontTextStyleBody"/>
<textInputTraits key="textInputTraits" spellCheckingType="no" keyboardType="emailAddress" textContentType="username"/>
</textField>
</subviews>
<constraints>
<constraint firstAttribute="trailing" secondItem="S4v-fs-DIO" secondAttribute="trailing" constant="20" id="Upe-dm-4DP"/>
<constraint firstItem="S4v-fs-DIO" firstAttribute="leading" secondItem="mqD-6S-DIl" secondAttribute="leading" constant="20" id="pQc-Fh-6T3"/>
<constraint firstItem="S4v-fs-DIO" firstAttribute="centerY" secondItem="mqD-6S-DIl" secondAttribute="centerY" id="s9a-ew-C5W"/>
</constraints>
</tableViewCellContentView>
</tableViewCell>
<tableViewCell clipsSubviews="YES" contentMode="scaleToFill" preservesSuperviewLayoutMargins="YES" selectionStyle="none" indentationWidth="10" id="iCK-kn-Au6">
<rect key="frame" x="20" y="61.5" width="374" height="43.5"/>
<autoresizingMask key="autoresizingMask"/>
<tableViewCellContentView key="contentView" opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center" preservesSuperviewLayoutMargins="YES" insetsLayoutMarginsFromSafeArea="NO" tableViewCell="iCK-kn-Au6" id="9Ej-wB-9Tr">
<rect key="frame" x="0.0" y="0.0" width="374" height="43.5"/>
<autoresizingMask key="autoresizingMask"/>
<subviews>
<textField opaque="NO" contentMode="scaleToFill" contentHorizontalAlignment="left" contentVerticalAlignment="center" placeholder="Password" textAlignment="natural" adjustsFontForContentSizeCategory="YES" minimumFontSize="17" translatesAutoresizingMaskIntoConstraints="NO" id="fct-XR-fEa">
<rect key="frame" x="20" y="13.5" width="290" height="17"/>
<fontDescription key="fontDescription" style="UICTFontTextStyleBody"/>
<textInputTraits key="textInputTraits" secureTextEntry="YES" textContentType="password"/>
</textField>
<button opaque="NO" contentMode="scaleToFill" horizontalHuggingPriority="251" contentHorizontalAlignment="center" contentVerticalAlignment="center" buttonType="system" lineBreakMode="middleTruncation" translatesAutoresizingMaskIntoConstraints="NO" id="GY9-nr-jFb">
<rect key="frame" x="318" y="7.5" width="36" height="29"/>
<fontDescription key="fontDescription" style="UICTFontTextStyleBody"/>
<state key="normal" title="Show"/>
<connections>
<action selector="showHidePassword:" destination="Cge-ND-NpD" eventType="touchUpInside" id="8JH-LX-URH"/>
</connections>
</button>
</subviews>
<constraints>
<constraint firstItem="GY9-nr-jFb" firstAttribute="centerY" secondItem="9Ej-wB-9Tr" secondAttribute="centerY" id="3jf-KC-nd8"/>
<constraint firstItem="GY9-nr-jFb" firstAttribute="leading" secondItem="fct-XR-fEa" secondAttribute="trailing" constant="8" symbolic="YES" id="Ibr-pt-eGr"/>
<constraint firstAttribute="trailing" secondItem="GY9-nr-jFb" secondAttribute="trailing" constant="20" symbolic="YES" id="mcZ-cl-knP"/>
<constraint firstItem="fct-XR-fEa" firstAttribute="leading" secondItem="9Ej-wB-9Tr" secondAttribute="leading" constant="20" id="u5f-tJ-8ce"/>
<constraint firstItem="fct-XR-fEa" firstAttribute="centerY" secondItem="9Ej-wB-9Tr" secondAttribute="centerY" id="z5e-jg-0nm"/>
</constraints>
</tableViewCellContentView>
</tableViewCell>
</cells>
</tableViewSection>
<tableViewSection id="L37-iZ-GVj">
<cells>
<tableViewCell clipsSubviews="YES" contentMode="scaleToFill" preservesSuperviewLayoutMargins="YES" selectionStyle="none" indentationWidth="10" id="fyQ-K8-byV">
<rect key="frame" x="20" y="141" width="374" height="43.5"/>
<autoresizingMask key="autoresizingMask"/>
<tableViewCellContentView key="contentView" opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center" preservesSuperviewLayoutMargins="YES" insetsLayoutMarginsFromSafeArea="NO" tableViewCell="fyQ-K8-byV" id="CtR-ZJ-FG5">
<rect key="frame" x="0.0" y="0.0" width="374" height="43.5"/>
<autoresizingMask key="autoresizingMask"/>
<subviews>
<button opaque="NO" contentMode="scaleToFill" enabled="NO" contentHorizontalAlignment="center" contentVerticalAlignment="center" lineBreakMode="middleTruncation" translatesAutoresizingMaskIntoConstraints="NO" id="E1I-C4-JdL" customClass="VibrantButton" customModule="NetNewsWire" customModuleProvider="target">
<rect key="frame" x="0.0" y="-0.5" width="374" height="44.5"/>
<constraints>
<constraint firstAttribute="height" constant="44" id="yoo-36-msf"/>
</constraints>
<fontDescription key="fontDescription" style="UICTFontTextStyleBody"/>
<state key="normal" title="Action">
<color key="titleColor" name="secondaryAccentColor"/>
</state>
<connections>
<action selector="action:" destination="Cge-ND-NpD" eventType="touchUpInside" id="YQw-1k-e8G"/>
</connections>
</button>
</subviews>
<constraints>
<constraint firstItem="E1I-C4-JdL" firstAttribute="centerY" secondItem="CtR-ZJ-FG5" secondAttribute="centerY" id="2vc-Ys-4Cj"/>
<constraint firstAttribute="trailing" secondItem="E1I-C4-JdL" secondAttribute="trailing" id="SLX-wc-QR7"/>
<constraint firstItem="E1I-C4-JdL" firstAttribute="leading" secondItem="CtR-ZJ-FG5" secondAttribute="leading" id="Veu-Wo-GSZ"/>
</constraints>
</tableViewCellContentView>
</tableViewCell>
</cells>
</tableViewSection>
</sections>
<connections>
<outlet property="dataSource" destination="Cge-ND-NpD" id="u8B-p4-Vlv"/>
<outlet property="delegate" destination="Cge-ND-NpD" id="RIw-V2-EJC"/>
</connections>
</tableView>
<navigationItem key="navigationItem" title="NewsBlur" id="jCQ-pH-6AD">
<barButtonItem key="leftBarButtonItem" systemItem="cancel" id="bl6-Y1-wQ8">
<connections>
<action selector="cancel:" destination="Cge-ND-NpD" id="9zR-LJ-IWk"/>
</connections>
</barButtonItem>
<barButtonItem key="rightBarButtonItem" id="4yi-H0-B9J">
<view key="customView" contentMode="scaleToFill" id="8DU-L0-P6c">
<rect key="frame" x="374" y="12" width="20" height="20"/>
<autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMaxY="YES"/>
<subviews>
<activityIndicatorView opaque="NO" contentMode="scaleToFill" horizontalHuggingPriority="750" verticalHuggingPriority="750" fixedFrame="YES" style="medium" translatesAutoresizingMaskIntoConstraints="NO" id="HfW-jV-MjK">
<rect key="frame" x="36" y="6" width="20" height="20"/>
<autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMaxY="YES"/>
</activityIndicatorView>
</subviews>
<color key="backgroundColor" white="0.0" alpha="0.0" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
</view>
</barButtonItem>
</navigationItem>
<connections>
<outlet property="actionButton" destination="E1I-C4-JdL" id="q2T-4o-c8i"/>
<outlet property="activityIndicator" destination="HfW-jV-MjK" id="AIV-uG-9uC"/>
<outlet property="cancelBarButtonItem" destination="bl6-Y1-wQ8" id="ohR-gW-5J2"/>
<outlet property="footerLabel" destination="fal-e8-3BB" id="7Fq-Oz-aEx"/>
<outlet property="passwordTextField" destination="fct-XR-fEa" id="fGL-4k-gZ6"/>
<outlet property="showHideButton" destination="GY9-nr-jFb" id="1p9-9F-GMY"/>
<outlet property="usernameTextField" destination="S4v-fs-DIO" id="B7I-yz-M0T"/>
</connections>
</tableViewController>
<placeholder placeholderIdentifier="IBFirstResponder" id="8Ku-6P-yPg" userLabel="First Responder" customClass="UIResponder" sceneMemberID="firstResponder"/>
</objects>
<point key="canvasLocation" x="4561" y="145"/>
</scene>
<!--Reader-->
<scene sceneID="3fU-9I-RDp">
<objects>
<tableViewController id="MzG-hS-TpF" customClass="ReaderAPIAccountViewController" customModule="NetNewsWire" customModuleProvider="target" sceneMemberID="viewController">
<tableView key="view" clipsSubviews="YES" contentMode="scaleToFill" alwaysBounceVertical="YES" dataMode="static" style="insetGrouped" separatorStyle="default" rowHeight="-1" estimatedRowHeight="-1" sectionHeaderHeight="18" sectionFooterHeight="18" id="bQC-XA-xWP">
<rect key="frame" x="0.0" y="0.0" width="414" height="896"/>
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
<view key="tableFooterView" contentMode="scaleToFill" id="6sa-hD-iAT">
<rect key="frame" x="0.0" y="246" width="414" height="150"/>
<autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMaxY="YES"/>
<subviews>
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" textAlignment="center" lineBreakMode="wordWrap" numberOfLines="0" baselineAdjustment="alignBaselines" adjustsFontForContentSizeCategory="YES" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="7Jj-p8-lYw">
<rect key="frame" x="20" y="8" width="373" height="39.5"/>
<string key="text">Use your Reader account to sync your feeds across your devices.
Dont have a Reader account?</string>
<fontDescription key="fontDescription" style="UICTFontTextStyleCaption1"/>
<color key="textColor" systemColor="secondaryLabelColor"/>
<nil key="highlightedColor"/>
</label>
<button opaque="NO" contentMode="scaleToFill" contentHorizontalAlignment="center" contentVerticalAlignment="center" buttonType="system" lineBreakMode="middleTruncation" translatesAutoresizingMaskIntoConstraints="NO" id="3Fq-U4-PS5">
<rect key="frame" x="172" y="46" width="70" height="26"/>
<fontDescription key="fontDescription" style="UICTFontTextStyleCaption1"/>
<state key="normal" title="Sign Up Here"/>
<connections>
<action selector="signUpWithProvider:" destination="MzG-hS-TpF" eventType="touchUpInside" id="pMC-f8-E4G"/>
</connections>
</button>
</subviews>
<color key="backgroundColor" systemColor="systemGroupedBackgroundColor"/>
<constraints>
<constraint firstItem="3Fq-U4-PS5" firstAttribute="top" secondItem="7Jj-p8-lYw" secondAttribute="bottom" constant="-1.5" id="6ma-pw-DSZ"/>
<constraint firstItem="7Jj-p8-lYw" firstAttribute="leading" secondItem="6sa-hD-iAT" secondAttribute="leading" constant="20" symbolic="YES" id="CnY-UA-F3a"/>
<constraint firstItem="3Fq-U4-PS5" firstAttribute="centerX" secondItem="6sa-hD-iAT" secondAttribute="centerX" id="Rgg-oG-KOZ"/>
<constraint firstAttribute="trailing" secondItem="7Jj-p8-lYw" secondAttribute="trailing" constant="21" id="mgT-t6-6c8"/>
<constraint firstItem="7Jj-p8-lYw" firstAttribute="top" secondItem="6sa-hD-iAT" secondAttribute="top" constant="8" id="syN-5x-dbM"/>
</constraints>
</view>
<sections>
<tableViewSection id="Rju-xt-yUY">
<cells>
<tableViewCell clipsSubviews="YES" contentMode="scaleToFill" preservesSuperviewLayoutMargins="YES" selectionStyle="none" indentationWidth="10" id="kW8-SV-Byq">
<rect key="frame" x="20" y="18" width="374" height="43.5"/>
<autoresizingMask key="autoresizingMask"/>
<tableViewCellContentView key="contentView" opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center" preservesSuperviewLayoutMargins="YES" insetsLayoutMarginsFromSafeArea="NO" tableViewCell="kW8-SV-Byq" id="4mV-Au-W6t">
<rect key="frame" x="0.0" y="0.0" width="374" height="43.5"/>
<autoresizingMask key="autoresizingMask"/>
<subviews>
<textField opaque="NO" contentMode="scaleToFill" contentHorizontalAlignment="left" contentVerticalAlignment="center" placeholder="Username or Email" textAlignment="natural" adjustsFontForContentSizeCategory="YES" minimumFontSize="17" translatesAutoresizingMaskIntoConstraints="NO" id="CZg-x8-936">
<rect key="frame" x="14" y="12.5" width="347" height="18.5"/>
<fontDescription key="fontDescription" style="UICTFontTextStyleBody"/>
<textInputTraits key="textInputTraits" spellCheckingType="no" keyboardType="emailAddress" textContentType="username"/>
</textField>
</subviews>
<constraints>
<constraint firstAttribute="trailing" secondItem="CZg-x8-936" secondAttribute="trailing" constant="13" id="7BW-D6-ZAW"/>
<constraint firstItem="CZg-x8-936" firstAttribute="centerY" secondItem="4mV-Au-W6t" secondAttribute="centerY" id="Fii-Qu-oXf"/>
<constraint firstItem="CZg-x8-936" firstAttribute="leading" secondItem="4mV-Au-W6t" secondAttribute="leading" constant="14" id="MKL-Nm-1Po"/>
</constraints>
</tableViewCellContentView>
</tableViewCell>
<tableViewCell clipsSubviews="YES" contentMode="scaleToFill" preservesSuperviewLayoutMargins="YES" selectionStyle="none" indentationWidth="10" id="pNe-n6-tVf">
<rect key="frame" x="20" y="61.5" width="374" height="43.5"/>
<autoresizingMask key="autoresizingMask"/>
<tableViewCellContentView key="contentView" opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center" preservesSuperviewLayoutMargins="YES" insetsLayoutMarginsFromSafeArea="NO" tableViewCell="pNe-n6-tVf" id="yQJ-L0-qVZ">
<rect key="frame" x="0.0" y="0.0" width="374" height="43.5"/>
<autoresizingMask key="autoresizingMask"/>
<subviews>
<textField opaque="NO" contentMode="scaleToFill" contentHorizontalAlignment="left" contentVerticalAlignment="center" placeholder="Password" textAlignment="natural" adjustsFontForContentSizeCategory="YES" minimumFontSize="17" translatesAutoresizingMaskIntoConstraints="NO" id="KgN-kQ-Cyc">
<rect key="frame" x="14" y="13.5" width="300" height="17"/>
<fontDescription key="fontDescription" style="UICTFontTextStyleBody"/>
<textInputTraits key="textInputTraits" secureTextEntry="YES" textContentType="password"/>
</textField>
<button opaque="NO" contentMode="scaleToFill" horizontalHuggingPriority="251" contentHorizontalAlignment="center" contentVerticalAlignment="center" buttonType="system" lineBreakMode="middleTruncation" translatesAutoresizingMaskIntoConstraints="NO" id="cFF-qt-WLs">
<rect key="frame" x="322" y="7.5" width="36" height="29"/>
<fontDescription key="fontDescription" style="UICTFontTextStyleBody"/>
<state key="normal" title="Show"/>
<connections>
<action selector="showHidePassword:" destination="MzG-hS-TpF" eventType="touchUpInside" id="IsF-iJ-oxT"/>
<action selector="showHidePassword:" destination="Cge-ND-NpD" eventType="touchUpInside" id="b9p-LX-Wk7"/>
</connections>
</button>
</subviews>
<constraints>
<constraint firstItem="cFF-qt-WLs" firstAttribute="leading" secondItem="KgN-kQ-Cyc" secondAttribute="trailing" constant="8" symbolic="YES" id="Cwh-XX-m2G"/>
<constraint firstItem="cFF-qt-WLs" firstAttribute="centerY" secondItem="yQJ-L0-qVZ" secondAttribute="centerY" id="GDc-9f-afL"/>
<constraint firstAttribute="trailing" secondItem="cFF-qt-WLs" secondAttribute="trailing" constant="16" id="K93-X3-UuK"/>
<constraint firstItem="KgN-kQ-Cyc" firstAttribute="centerY" secondItem="yQJ-L0-qVZ" secondAttribute="centerY" id="UpQ-pU-DYv"/>
<constraint firstItem="KgN-kQ-Cyc" firstAttribute="leading" secondItem="yQJ-L0-qVZ" secondAttribute="leading" constant="14" id="fam-16-kf6"/>
</constraints>
</tableViewCellContentView>
</tableViewCell>
<tableViewCell clipsSubviews="YES" contentMode="scaleToFill" preservesSuperviewLayoutMargins="YES" selectionStyle="none" indentationWidth="10" id="mCx-af-pd3">
<rect key="frame" x="20" y="105" width="374" height="43.5"/>
<autoresizingMask key="autoresizingMask"/>
<tableViewCellContentView key="contentView" opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center" preservesSuperviewLayoutMargins="YES" insetsLayoutMarginsFromSafeArea="NO" tableViewCell="mCx-af-pd3" id="o1U-Qv-4gz">
<rect key="frame" x="0.0" y="0.0" width="374" height="43.5"/>
<autoresizingMask key="autoresizingMask"/>
<subviews>
<textField opaque="NO" contentMode="scaleToFill" contentHorizontalAlignment="left" contentVerticalAlignment="center" placeholder="API URL" textAlignment="natural" adjustsFontForContentSizeCategory="YES" minimumFontSize="17" translatesAutoresizingMaskIntoConstraints="NO" id="iPv-M2-U8Q">
<rect key="frame" x="14" y="11" width="340" height="18.5"/>
<fontDescription key="fontDescription" style="UICTFontTextStyleBody"/>
<textInputTraits key="textInputTraits" autocorrectionType="no" spellCheckingType="no" keyboardType="URL" smartDashesType="no" smartInsertDeleteType="no" smartQuotesType="no" textContentType="url"/>
</textField>
</subviews>
<constraints>
<constraint firstItem="iPv-M2-U8Q" firstAttribute="leading" secondItem="o1U-Qv-4gz" secondAttribute="leading" constant="14" id="4UP-GO-kmh"/>
<constraint firstItem="iPv-M2-U8Q" firstAttribute="top" secondItem="o1U-Qv-4gz" secondAttribute="topMargin" id="Gap-aN-LP7"/>
<constraint firstItem="iPv-M2-U8Q" firstAttribute="trailing" secondItem="o1U-Qv-4gz" secondAttribute="trailingMargin" id="npR-r8-mpF"/>
</constraints>
</tableViewCellContentView>
</tableViewCell>
</cells>
</tableViewSection>
<tableViewSection id="UWZ-Vu-0Pp">
<cells>
<tableViewCell clipsSubviews="YES" contentMode="scaleToFill" preservesSuperviewLayoutMargins="YES" selectionStyle="none" indentationWidth="10" rowHeight="43.5" id="d3E-Ds-Thm">
<rect key="frame" x="20" y="184.5" width="374" height="43.5"/>
<autoresizingMask key="autoresizingMask"/>
<tableViewCellContentView key="contentView" opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center" preservesSuperviewLayoutMargins="YES" insetsLayoutMarginsFromSafeArea="NO" tableViewCell="d3E-Ds-Thm" id="Frb-uH-Sff">
<rect key="frame" x="0.0" y="0.0" width="374" height="43.5"/>
<autoresizingMask key="autoresizingMask"/>
<subviews>
<button opaque="NO" contentMode="scaleToFill" contentHorizontalAlignment="center" contentVerticalAlignment="center" buttonType="system" lineBreakMode="middleTruncation" translatesAutoresizingMaskIntoConstraints="NO" id="7L9-X7-1Oc">
<rect key="frame" x="0.0" y="-0.5" width="374" height="44.5"/>
<state key="normal" title="Action"/>
<connections>
<action selector="action:" destination="MzG-hS-TpF" eventType="touchUpInside" id="d10-4f-ZUn"/>
</connections>
</button>
</subviews>
<constraints>
<constraint firstItem="7L9-X7-1Oc" firstAttribute="centerY" secondItem="Frb-uH-Sff" secondAttribute="centerY" id="NVm-XD-zND"/>
<constraint firstItem="7L9-X7-1Oc" firstAttribute="centerX" secondItem="Frb-uH-Sff" secondAttribute="centerX" id="YB9-O8-Z15"/>
<constraint firstItem="7L9-X7-1Oc" firstAttribute="height" secondItem="Frb-uH-Sff" secondAttribute="height" multiplier="0.689655" constant="13.999999999999996" id="iNV-NE-jhW"/>
<constraint firstItem="7L9-X7-1Oc" firstAttribute="width" secondItem="Frb-uH-Sff" secondAttribute="width" multiplier="0.893048" constant="40" id="lfQ-KQ-9nR"/>
</constraints>
</tableViewCellContentView>
</tableViewCell>
</cells>
</tableViewSection>
</sections>
<connections>
<outlet property="dataSource" destination="MzG-hS-TpF" id="QvG-cd-Q3P"/>
<outlet property="delegate" destination="MzG-hS-TpF" id="o4Z-RV-8uW"/>
</connections>
</tableView>
<navigationItem key="navigationItem" title="Reader" id="z3N-XM-EXU">
<barButtonItem key="leftBarButtonItem" title="Cancel" id="n8H-ai-4Df">
<connections>
<action selector="cancel:" destination="MzG-hS-TpF" id="a49-Fh-i1S"/>
</connections>
</barButtonItem>
<barButtonItem key="rightBarButtonItem" id="Ih6-jI-jFg">
<view key="customView" contentMode="scaleToFill" id="gSl-PT-7DH">
<rect key="frame" x="374" y="12" width="20" height="20"/>
<autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMaxY="YES"/>
<subviews>
<activityIndicatorView opaque="NO" contentMode="scaleToFill" horizontalHuggingPriority="750" verticalHuggingPriority="750" fixedFrame="YES" style="medium" translatesAutoresizingMaskIntoConstraints="NO" id="pdn-6v-d9a">
<rect key="frame" x="36" y="6" width="20" height="20"/>
<autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMaxY="YES"/>
</activityIndicatorView>
</subviews>
<color key="backgroundColor" white="0.0" alpha="0.0" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
</view>
</barButtonItem>
</navigationItem>
<connections>
<outlet property="actionButton" destination="7L9-X7-1Oc" id="VnP-sl-Cmd"/>
<outlet property="activityIndicator" destination="pdn-6v-d9a" id="vgt-C6-fy6"/>
<outlet property="apiURLTextField" destination="iPv-M2-U8Q" id="8kn-Xk-a8w"/>
<outlet property="cancelBarButtonItem" destination="n8H-ai-4Df" id="u86-HH-HYC"/>
<outlet property="footerLabel" destination="7Jj-p8-lYw" id="Tqv-qR-WBR"/>
<outlet property="passwordTextField" destination="KgN-kQ-Cyc" id="A0K-JL-CEW"/>
<outlet property="showHideButton" destination="cFF-qt-WLs" id="AxI-Gl-NdM"/>
<outlet property="signUpButton" destination="3Fq-U4-PS5" id="Wuj-5g-vDH"/>
<outlet property="usernameTextField" destination="CZg-x8-936" id="nUT-WL-fKD"/>
</connections>
</tableViewController>
<placeholder placeholderIdentifier="IBFirstResponder" id="Fj8-E0-Aeh" userLabel="First Responder" customClass="UIResponder" sceneMemberID="firstResponder"/>
</objects>
<point key="canvasLocation" x="5260.8695652173919" y="144.64285714285714"/>
</scene>
<!--Modal Navigation Controller-->
<scene sceneID="gfi-2F-rht">
<objects>
<navigationController storyboardIdentifier="CloudKitAccountNavigationViewController" id="LhW-Dq-qqj" customClass="ModalNavigationController" customModule="NetNewsWire" customModuleProvider="target" sceneMemberID="viewController">
<navigationBar key="navigationBar" contentMode="scaleToFill" insetsLayoutMarginsFromSafeArea="NO" id="MVG-BZ-ALL">
<rect key="frame" x="0.0" y="48" width="414" height="44"/>
<autoresizingMask key="autoresizingMask"/>
</navigationBar>
<connections>
<segue destination="qj9-Vr-VIU" kind="relationship" relationship="rootViewController" id="n8n-iF-qeC"/>
</connections>
</navigationController>
<placeholder placeholderIdentifier="IBFirstResponder" id="z9f-5I-8GC" userLabel="First Responder" sceneMemberID="firstResponder"/>
</objects>
<point key="canvasLocation" x="2533" y="-528"/>
</scene>
<!--iCloud-->
<scene sceneID="ULt-VE-viU">
<objects>
<tableViewController id="qj9-Vr-VIU" customClass="CloudKitAccountViewController" customModule="NetNewsWire" customModuleProvider="target" sceneMemberID="viewController">
<tableView key="view" clipsSubviews="YES" contentMode="scaleToFill" alwaysBounceVertical="YES" dataMode="static" style="insetGrouped" separatorStyle="default" rowHeight="-1" estimatedRowHeight="-1" sectionHeaderHeight="18" sectionFooterHeight="18" id="j6U-sh-M9y">
<rect key="frame" x="0.0" y="0.0" width="414" height="896"/>
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
<view key="tableFooterView" contentMode="scaleToFill" id="iYz-ri-yys">
<rect key="frame" x="0.0" y="79.5" width="414" height="150"/>
<autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMaxY="YES"/>
<subviews>
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="Your iCloud account syncs your feeds across your Mac and iOS devices" lineBreakMode="wordWrap" numberOfLines="0" baselineAdjustment="alignBaselines" adjustsFontForContentSizeCategory="YES" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="aFS-Y0-2MH">
<rect key="frame" x="20" y="8" width="373" height="36"/>
<fontDescription key="fontDescription" style="UICTFontTextStyleBody"/>
<color key="textColor" systemColor="secondaryLabelColor"/>
<nil key="highlightedColor"/>
</label>
<button opaque="NO" contentMode="scaleToFill" contentHorizontalAlignment="center" contentVerticalAlignment="center" buttonType="system" lineBreakMode="middleTruncation" translatesAutoresizingMaskIntoConstraints="NO" id="vXG-7q-4qg">
<rect key="frame" x="75" y="50.5" width="264" height="30"/>
<fontDescription key="fontDescription" style="UICTFontTextStyleSubhead"/>
<inset key="imageEdgeInsets" minX="0.0" minY="0.0" maxX="2.2250738585072014e-308" maxY="0.0"/>
<state key="normal" title="iCloud Syncing Limitations &amp; Solutions"/>
<connections>
<action selector="openLimitationsAndSolutions:" destination="qj9-Vr-VIU" eventType="touchUpInside" id="JZ5-hQ-PLl"/>
</connections>
</button>
</subviews>
<color key="backgroundColor" systemColor="systemGroupedBackgroundColor"/>
<constraints>
<constraint firstItem="vXG-7q-4qg" firstAttribute="centerX" secondItem="iYz-ri-yys" secondAttribute="centerX" id="8Pg-BU-zIj"/>
<constraint firstItem="aFS-Y0-2MH" firstAttribute="leading" secondItem="iYz-ri-yys" secondAttribute="leading" constant="20" symbolic="YES" id="E97-lo-arw"/>
<constraint firstItem="vXG-7q-4qg" firstAttribute="top" secondItem="aFS-Y0-2MH" secondAttribute="bottom" id="TEh-B3-9Ci"/>
<constraint firstAttribute="trailing" secondItem="aFS-Y0-2MH" secondAttribute="trailing" constant="21" id="XUo-oQ-MbK"/>
<constraint firstItem="aFS-Y0-2MH" firstAttribute="top" secondItem="iYz-ri-yys" secondAttribute="top" constant="8" id="xpj-LW-4l7"/>
</constraints>
</view>
<sections>
<tableViewSection id="bGn-Io-KuQ">
<cells>
<tableViewCell clipsSubviews="YES" contentMode="scaleToFill" preservesSuperviewLayoutMargins="YES" selectionStyle="none" indentationWidth="10" id="FSY-KL-m3i" customClass="VibrantTableViewCell" customModule="NetNewsWire" customModuleProvider="target">
<rect key="frame" x="20" y="18" width="374" height="43.5"/>
<autoresizingMask key="autoresizingMask"/>
<tableViewCellContentView key="contentView" opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center" preservesSuperviewLayoutMargins="YES" insetsLayoutMarginsFromSafeArea="NO" tableViewCell="FSY-KL-m3i" id="ds7-ib-VgJ">
<rect key="frame" x="0.0" y="0.0" width="374" height="43.5"/>
<autoresizingMask key="autoresizingMask"/>
<subviews>
<button opaque="NO" contentMode="scaleToFill" contentHorizontalAlignment="center" contentVerticalAlignment="center" lineBreakMode="middleTruncation" translatesAutoresizingMaskIntoConstraints="NO" id="T1S-zH-rIp" customClass="VibrantButton" customModule="NetNewsWire" customModuleProvider="target">
<rect key="frame" x="0.0" y="-0.5" width="374" height="44.5"/>
<constraints>
<constraint firstAttribute="height" constant="44" id="dOv-Gz-h7s"/>
</constraints>
<fontDescription key="fontDescription" style="UICTFontTextStyleBody"/>
<state key="normal" title="Use iCloud">
<color key="titleColor" name="secondaryAccentColor"/>
</state>
<connections>
<action selector="add:" destination="qj9-Vr-VIU" eventType="touchUpInside" id="kUm-lW-g62"/>
</connections>
</button>
</subviews>
<constraints>
<constraint firstItem="T1S-zH-rIp" firstAttribute="leading" secondItem="ds7-ib-VgJ" secondAttribute="leading" id="7F5-Ym-ew3"/>
<constraint firstAttribute="trailing" secondItem="T1S-zH-rIp" secondAttribute="trailing" id="ON3-nQ-kd8"/>
<constraint firstItem="T1S-zH-rIp" firstAttribute="centerY" secondItem="ds7-ib-VgJ" secondAttribute="centerY" id="dAM-F2-peY"/>
</constraints>
</tableViewCellContentView>
</tableViewCell>
</cells>
</tableViewSection>
</sections>
<connections>
<outlet property="dataSource" destination="qj9-Vr-VIU" id="j7u-Yd-rbe"/>
<outlet property="delegate" destination="qj9-Vr-VIU" id="NhE-Pt-JGp"/>
</connections>
</tableView>
<navigationItem key="navigationItem" title="iCloud" id="idp-kp-cGU">
<barButtonItem key="leftBarButtonItem" systemItem="cancel" id="hKZ-OI-mTV">
<connections>
<action selector="cancel:" destination="qj9-Vr-VIU" id="n5q-9M-3ME"/>
</connections>
</barButtonItem>
</navigationItem>
<connections>
<outlet property="footerLabel" destination="aFS-Y0-2MH" id="gDw-R1-HSK"/>
</connections>
</tableViewController>
<placeholder placeholderIdentifier="IBFirstResponder" id="weY-OS-9NV" userLabel="First Responder" customClass="UIResponder" sceneMemberID="firstResponder"/>
</objects>
<point key="canvasLocation" x="2533" y="145"/>
</scene>
<!--Modal Navigation Controller-->
<scene sceneID="JBz-7C-wEc">
<objects>
<navigationController storyboardIdentifier="ReaderAPIAccountNavigationViewController" id="Son-xT-GLx" customClass="ModalNavigationController" customModule="NetNewsWire" customModuleProvider="target" sceneMemberID="viewController">
<navigationBar key="navigationBar" contentMode="scaleToFill" insetsLayoutMarginsFromSafeArea="NO" id="sdL-X8-E6K">
<rect key="frame" x="0.0" y="48" width="414" height="44"/>
<autoresizingMask key="autoresizingMask"/>
</navigationBar>
<connections>
<segue destination="MzG-hS-TpF" kind="relationship" relationship="rootViewController" id="gsQ-9o-AMJ"/>
</connections>
</navigationController>
<placeholder placeholderIdentifier="IBFirstResponder" id="vls-xO-YVi" userLabel="First Responder" sceneMemberID="firstResponder"/>
</objects>
<point key="canvasLocation" x="5261" y="-528"/>
</scene>
</scenes>
<resources>
<namedColor name="secondaryAccentColor">
<color red="0.031372549019607843" green="0.41568627450980394" blue="0.93333333333333335" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
</namedColor>
<systemColor name="secondaryLabelColor">
<color red="0.23529411764705882" green="0.23529411764705882" blue="0.2627450980392157" alpha="0.59999999999999998" colorSpace="custom" customColorSpace="sRGB"/>
</systemColor>
<systemColor name="systemGroupedBackgroundColor">
<color red="0.94901960784313721" green="0.94901960784313721" blue="0.96862745098039216" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
</systemColor>
</resources>
</document>

View File

@@ -6,11 +6,14 @@
Copyright © 2022 Ranchero Software. All rights reserved.
*/
/* Account Names */
/* Account Section */
"ACCOUNT_NAME" = "Name";
"CLOUDKIT" = "iCloud";
"LOCAL_ACCOUNT_NAME_PHONE" = "On My iPhone";
"LOCAL_ACCOUNT_NAME_PAD" = "On My iPad";
"ACCOUNT_EMAIL_ADDRESS_PROMPT" = "Email Address";
"ACCOUNT_USERNAME_PROMPT" = "Username";
"ACCOUNT_PASSWORD_PROMPT" = "Password";
/* Explainers */
@@ -20,5 +23,7 @@
"OLDREADER_FOOTER_EXPLAINER" = "Sign in to your The Old Reader account and sync your feeds across your devices. Your username and password will be encrypted and stored in Keychain.\n\nDont have a The Old Reader account? [Sign Up Here](https://theoldreader.com)";
"FRESHRSS_FOOTER_EXPLAINER" = "Sign in to your FreshRSS instance and sync your feeds across your devices. Your username and password will be encrypted and stored in Keychain.\n\nDont have an FreshRSS instance? [Sign Up Here](https://freshrss.org)";
"CLOUDKIT_FOOTER_EXPLAINER" = "NetNewsWire will use your iCloud account to sync your subscriptions across your Mac and iOS devices.";
"FEEDBIN_FOOTER_EXPLAINER" = "Sign in to your Feedbin account and sync your feeds across your devices. Your username and password will be encrypted and stored in Keychain.\n\nDont have a Feedbin account? [Sign Up Here](https://feedbin.com/signup)";
"NEWSBLUR_FOOTER_EXPLAINER" = "Sign in to your NewsBlur account and sync your feeds across your devices. Your username and password will be encrypted and stored in Keychain.\n\nDont have a NewsBlur account? [Sign Up Here](https://newsblur.com)";

View File

@@ -1,73 +0,0 @@
//
// CloudKitAccountViewController.swift
// NetNewsWire-iOS
//
// Created by Maurice Parker on 3/28/20.
// Copyright © 2020 Ranchero Software. All rights reserved.
//
import UIKit
import SafariServices
import Account
enum CloudKitAccountViewControllerError: LocalizedError {
case iCloudDriveMissing
var errorDescription: String? {
return NSLocalizedString("Unable to add iCloud Account. Please make sure you have iCloud and iCloud Drive enabled in System Preferences.", comment: "Unable to add iCloud Account.")
}
}
class CloudKitAccountViewController: UITableViewController {
//weak var delegate: AddAccountDismissDelegate?
@IBOutlet weak var footerLabel: UILabel!
override func viewDidLoad() {
super.viewDidLoad()
setupFooter()
tableView.register(ImageHeaderView.self, forHeaderFooterViewReuseIdentifier: "SectionHeader")
}
private func setupFooter() {
footerLabel.text = NSLocalizedString("NetNewsWire will use your iCloud account to sync your subscriptions across your Mac and iOS devices.", comment: "iCloud")
}
@IBAction func cancel(_ sender: Any) {
dismiss(animated: true, completion: nil)
//delegate?.dismiss()
}
@IBAction func add(_ sender: Any) {
guard FileManager.default.ubiquityIdentityToken != nil else {
presentError(CloudKitAccountViewControllerError.iCloudDriveMissing)
return
}
let _ = AccountManager.shared.createAccount(type: .cloudKit)
dismiss(animated: true, completion: nil)
//delegate?.dismiss()
}
override func tableView(_ tableView: UITableView, heightForHeaderInSection section: Int) -> CGFloat {
return section == 0 ? ImageHeaderView.rowHeight : super.tableView(tableView, heightForHeaderInSection: section)
}
override func tableView(_ tableView: UITableView, viewForHeaderInSection section: Int) -> UIView? {
if section == 0 {
let headerView = tableView.dequeueReusableHeaderFooterView(withIdentifier: "SectionHeader") as! ImageHeaderView
headerView.imageView.image = AppAssets.image(for: .cloudKit)
return headerView
} else {
return super.tableView(tableView, viewForHeaderInSection: section)
}
}
@IBAction func openLimitationsAndSolutions(_ sender: Any) {
let vc = SFSafariViewController(url: CloudKitWebDocumentation.limitationsAndSolutionsURL)
vc.modalPresentationStyle = .pageSheet
present(vc, animated: true)
}
}

View File

@@ -1,194 +0,0 @@
//
// FeedbinAccountViewController.swift
// NetNewsWire-iOS
//
// Created by Maurice Parker on 5/19/19.
// Copyright © 2019 Ranchero Software. All rights reserved.
//
import UIKit
import Account
import Secrets
import RSWeb
import SafariServices
import RSCore
class FeedbinAccountViewController: UITableViewController, Logging {
@IBOutlet weak var activityIndicator: UIActivityIndicatorView!
@IBOutlet weak var cancelBarButtonItem: UIBarButtonItem!
@IBOutlet weak var emailTextField: UITextField!
@IBOutlet weak var passwordTextField: UITextField!
@IBOutlet weak var showHideButton: UIButton!
@IBOutlet weak var actionButton: UIButton!
@IBOutlet weak var footerLabel: UILabel!
weak var account: Account?
//weak var delegate: AddAccountDismissDelegate?
override func viewDidLoad() {
super.viewDidLoad()
setupFooter()
activityIndicator.isHidden = true
emailTextField.delegate = self
passwordTextField.delegate = self
if let account = account, let credentials = try? account.retrieveCredentials(type: .basic) {
actionButton.setTitle(NSLocalizedString("Update Credentials", comment: "Update Credentials"), for: .normal)
actionButton.isEnabled = true
emailTextField.text = credentials.username
passwordTextField.text = credentials.secret
} else {
actionButton.setTitle(NSLocalizedString("Add Account", comment: "Add Account"), for: .normal)
}
NotificationCenter.default.addObserver(self, selector: #selector(textDidChange(_:)), name: UITextField.textDidChangeNotification, object: emailTextField)
NotificationCenter.default.addObserver(self, selector: #selector(textDidChange(_:)), name: UITextField.textDidChangeNotification, object: passwordTextField)
tableView.register(ImageHeaderView.self, forHeaderFooterViewReuseIdentifier: "SectionHeader")
}
private func setupFooter() {
footerLabel.text = NSLocalizedString("Sign in to your Feedbin account and sync your feeds across your devices. Your username and password will be encrypted and stored in Keychain.\n\nDont have a Feedbin account?", comment: "Feedbin")
}
override func tableView(_ tableView: UITableView, heightForHeaderInSection section: Int) -> CGFloat {
return section == 0 ? ImageHeaderView.rowHeight : super.tableView(tableView, heightForHeaderInSection: section)
}
override func tableView(_ tableView: UITableView, viewForHeaderInSection section: Int) -> UIView? {
if section == 0 {
let headerView = tableView.dequeueReusableHeaderFooterView(withIdentifier: "SectionHeader") as! ImageHeaderView
headerView.imageView.image = AppAssets.image(for: .feedbin)
return headerView
} else {
return super.tableView(tableView, viewForHeaderInSection: section)
}
}
@IBAction func cancel(_ sender: Any) {
dismiss(animated: true, completion: nil)
}
@IBAction func showHidePassword(_ sender: Any) {
if passwordTextField.isSecureTextEntry {
passwordTextField.isSecureTextEntry = false
showHideButton.setTitle("Hide", for: .normal)
} else {
passwordTextField.isSecureTextEntry = true
showHideButton.setTitle("Show", for: .normal)
}
}
@IBAction func action(_ sender: Any) {
guard let email = emailTextField.text, let password = passwordTextField.text else {
showError(NSLocalizedString("Username & password required.", comment: "Credentials Error"))
return
}
// When you fill in the email address via auto-complete it adds extra whitespace
let trimmedEmail = email.trimmingCharacters(in: .whitespaces)
guard account != nil || !AccountManager.shared.duplicateServiceAccount(type: .feedbin, username: trimmedEmail) else {
showError(NSLocalizedString("There is already a Feedbin account with that username created.", comment: "Duplicate Error"))
return
}
resignFirstResponder()
toggleActivityIndicatorAnimation(visible: true)
setNavigationEnabled(to: false)
let credentials = Credentials(type: .basic, username: trimmedEmail, secret: password)
Account.validateCredentials(type: .feedbin, credentials: credentials) { result in
self.toggleActivityIndicatorAnimation(visible: false)
self.setNavigationEnabled(to: true)
switch result {
case .success(let credentials):
if let credentials = credentials {
if self.account == nil {
self.account = AccountManager.shared.createAccount(type: .feedbin)
}
do {
do {
try self.account?.removeCredentials(type: .basic)
} catch {
self.logger.error("Error removing credentials: \(error.localizedDescription, privacy: .public).")
}
try self.account?.storeCredentials(credentials)
self.account?.refreshAll() { result in
switch result {
case .success:
break
case .failure(let error):
self.presentError(error)
}
}
self.dismiss(animated: true, completion: nil)
//self.delegate?.dismiss()
} catch {
self.showError(NSLocalizedString("Keychain error while storing credentials.", comment: "Credentials Error"))
self.logger.error("Keychain error while storing credentials: \(error.localizedDescription, privacy: .public).")
}
} else {
self.showError(NSLocalizedString("Invalid email/password combination.", comment: "Credentials Error"))
}
case .failure:
self.showError(NSLocalizedString("Network error. Try again later.", comment: "Credentials Error"))
}
}
}
@IBAction func signUpWithProvider(_ sender: Any) {
let url = URL(string: "https://feedbin.com/signup")!
let safari = SFSafariViewController(url: url)
safari.modalPresentationStyle = .currentContext
self.present(safari, animated: true, completion: nil)
}
@objc func textDidChange(_ note: Notification) {
actionButton.isEnabled = !(emailTextField.text?.isEmpty ?? false) && !(passwordTextField.text?.isEmpty ?? false)
}
private func showError(_ message: String) {
presentError(title: NSLocalizedString("Error", comment: "Credentials Error"), message: message)
}
private func setNavigationEnabled(to value:Bool){
cancelBarButtonItem.isEnabled = value
actionButton.isEnabled = value
}
private func toggleActivityIndicatorAnimation(visible value: Bool){
activityIndicator.isHidden = !value
if value {
activityIndicator.startAnimating()
} else {
activityIndicator.stopAnimating()
}
}
}
extension FeedbinAccountViewController: UITextFieldDelegate {
func textFieldShouldReturn(_ textField: UITextField) -> Bool {
if textField == emailTextField {
passwordTextField.becomeFirstResponder()
} else {
textField.resignFirstResponder()
action(self)
}
return true
}
}

View File

@@ -1,197 +0,0 @@
//
// NewsBlurAccountViewController.swift
// NetNewsWire
//
// Created by Anh-Quang Do on 3/9/20.
// Copyright (c) 2020 Ranchero Software. All rights reserved.
//
import UIKit
import Account
import Secrets
import RSWeb
import RSCore
import SafariServices
class NewsBlurAccountViewController: UITableViewController, Logging {
@IBOutlet weak var activityIndicator: UIActivityIndicatorView!
@IBOutlet weak var cancelBarButtonItem: UIBarButtonItem!
@IBOutlet weak var usernameTextField: UITextField!
@IBOutlet weak var passwordTextField: UITextField!
@IBOutlet weak var showHideButton: UIButton!
@IBOutlet weak var actionButton: UIButton!
@IBOutlet weak var footerLabel: UILabel!
weak var account: Account?
//weak var delegate: AddAccountDismissDelegate?
override func viewDidLoad() {
super.viewDidLoad()
setupFooter()
activityIndicator.isHidden = true
usernameTextField.delegate = self
passwordTextField.delegate = self
if let account = account, let credentials = try? account.retrieveCredentials(type: .newsBlurBasic) {
actionButton.setTitle(NSLocalizedString("Update Credentials", comment: "Update Credentials"), for: .normal)
actionButton.isEnabled = true
usernameTextField.text = credentials.username
passwordTextField.text = credentials.secret
} else {
actionButton.setTitle(NSLocalizedString("Add Account", comment: "Add Account"), for: .normal)
}
NotificationCenter.default.addObserver(self, selector: #selector(textDidChange(_:)), name: UITextField.textDidChangeNotification, object: usernameTextField)
NotificationCenter.default.addObserver(self, selector: #selector(textDidChange(_:)), name: UITextField.textDidChangeNotification, object: passwordTextField)
tableView.register(ImageHeaderView.self, forHeaderFooterViewReuseIdentifier: "SectionHeader")
}
private func setupFooter() {
footerLabel.text = NSLocalizedString("Sign in to your NewsBlur account and sync your feeds across your devices. Your username and password will be encrypted and stored in Keychain.\n\nDont have a NewsBlur account?", comment: "NewsBlur")
}
override func tableView(_ tableView: UITableView, heightForHeaderInSection section: Int) -> CGFloat {
return section == 0 ? ImageHeaderView.rowHeight : super.tableView(tableView, heightForHeaderInSection: section)
}
override func tableView(_ tableView: UITableView, viewForHeaderInSection section: Int) -> UIView? {
if section == 0 {
let headerView = tableView.dequeueReusableHeaderFooterView(withIdentifier: "SectionHeader") as! ImageHeaderView
headerView.imageView.image = AppAssets.image(for: .newsBlur)
return headerView
} else {
return super.tableView(tableView, viewForHeaderInSection: section)
}
}
@IBAction func cancel(_ sender: Any) {
dismiss(animated: true, completion: nil)
}
@IBAction func showHidePassword(_ sender: Any) {
if passwordTextField.isSecureTextEntry {
passwordTextField.isSecureTextEntry = false
showHideButton.setTitle("Hide", for: .normal)
} else {
passwordTextField.isSecureTextEntry = true
showHideButton.setTitle("Show", for: .normal)
}
}
@IBAction func action(_ sender: Any) {
guard let username = usernameTextField.text else {
showError(NSLocalizedString("Username required.", comment: "Credentials Error"))
return
}
// When you fill in the email address via auto-complete it adds extra whitespace
let trimmedUsername = username.trimmingCharacters(in: .whitespaces)
guard account != nil || !AccountManager.shared.duplicateServiceAccount(type: .newsBlur, username: trimmedUsername) else {
showError(NSLocalizedString("There is already a NewsBlur account with that username created.", comment: "Duplicate Error"))
return
}
let password = passwordTextField.text ?? ""
startAnimatingActivityIndicator()
disableNavigation()
let basicCredentials = Credentials(type: .newsBlurBasic, username: trimmedUsername, secret: password)
Account.validateCredentials(type: .newsBlur, credentials: basicCredentials) { result in
self.stopAnimatingActivityIndicator()
self.enableNavigation()
switch result {
case .success(let sessionCredentials):
if let sessionCredentials = sessionCredentials {
if self.account == nil {
self.account = AccountManager.shared.createAccount(type: .newsBlur)
}
do {
do {
try self.account?.removeCredentials(type: .newsBlurBasic)
try self.account?.removeCredentials(type: .newsBlurSessionId)
} catch {
self.logger.error("Error removing credentials: \(error.localizedDescription, privacy: .public).")
}
try self.account?.storeCredentials(basicCredentials)
try self.account?.storeCredentials(sessionCredentials)
self.account?.refreshAll() { result in
switch result {
case .success:
break
case .failure(let error):
self.presentError(error)
}
}
self.dismiss(animated: true, completion: nil)
//self.delegate?.dismiss()
} catch {
self.showError(NSLocalizedString("Keychain error while storing credentials.", comment: "Credentials Error"))
self.logger.error("Keychain error while storing credentials: \(error.localizedDescription, privacy: .public).")
}
} else {
self.showError(NSLocalizedString("Invalid username/password combination.", comment: "Credentials Error"))
}
case .failure(let error):
self.showError(error.localizedDescription)
}
}
}
@IBAction func signUpWithProvider(_ sender: Any) {
let url = URL(string: "https://newsblur.com")!
let safari = SFSafariViewController(url: url)
safari.modalPresentationStyle = .currentContext
self.present(safari, animated: true, completion: nil)
}
@objc func textDidChange(_ note: Notification) {
actionButton.isEnabled = !(usernameTextField.text?.isEmpty ?? false)
}
private func showError(_ message: String) {
presentError(title: "Error", message: message)
}
private func enableNavigation() {
self.cancelBarButtonItem.isEnabled = true
self.actionButton.isEnabled = true
}
private func disableNavigation() {
cancelBarButtonItem.isEnabled = false
actionButton.isEnabled = false
}
private func startAnimatingActivityIndicator() {
activityIndicator.isHidden = false
activityIndicator.startAnimating()
}
private func stopAnimatingActivityIndicator() {
self.activityIndicator.isHidden = true
self.activityIndicator.stopAnimating()
}
}
extension NewsBlurAccountViewController: UITextFieldDelegate {
func textFieldShouldReturn(_ textField: UITextField) -> Bool {
textField.resignFirstResponder()
return true
}
}

View File

@@ -1,322 +0,0 @@
//
// ReaderAPIAccountViewController.swift
// NetNewsWire-iOS
//
// Created by Stuart Breckenridge on 25/10/20.
// Copyright © 2020 Ranchero Software. All rights reserved.
//
import UIKit
import Account
import Secrets
import RSWeb
import SafariServices
import RSCore
class ReaderAPIAccountViewController: UITableViewController, Logging {
@IBOutlet weak var activityIndicator: UIActivityIndicatorView!
@IBOutlet weak var cancelBarButtonItem: UIBarButtonItem!
@IBOutlet weak var usernameTextField: UITextField!
@IBOutlet weak var passwordTextField: UITextField!
@IBOutlet weak var apiURLTextField: UITextField!
@IBOutlet weak var showHideButton: UIButton!
@IBOutlet weak var actionButton: UIButton!
@IBOutlet weak var footerLabel: UILabel!
@IBOutlet weak var signUpButton: UIButton!
weak var account: Account?
var accountType: AccountType?
//weak var delegate: AddAccountDismissDelegate?
override func viewDidLoad() {
super.viewDidLoad()
setupFooter()
activityIndicator.isHidden = true
usernameTextField.delegate = self
passwordTextField.delegate = self
if let unwrappedAcount = account,
let credentials = try? retrieveCredentialsForAccount(for: unwrappedAcount) {
actionButton.setTitle(NSLocalizedString("Update Credentials", comment: "Update Credentials"), for: .normal)
actionButton.isEnabled = true
usernameTextField.text = credentials.username
passwordTextField.text = credentials.secret
} else {
actionButton.setTitle(NSLocalizedString("Add Account", comment: "Add Account"), for: .normal)
}
if let unwrappedAccountType = accountType {
switch unwrappedAccountType {
case .freshRSS:
title = NSLocalizedString("FreshRSS", comment: "FreshRSS")
apiURLTextField.placeholder = NSLocalizedString("API URL: fresh.rss.net/api/greader.php", comment: "FreshRSS API Helper")
case .inoreader:
title = NSLocalizedString("InoReader", comment: "InoReader")
case .bazQux:
title = NSLocalizedString("BazQux", comment: "BazQux")
case .theOldReader:
title = NSLocalizedString("The Old Reader", comment: "The Old Reader")
default:
title = ""
}
}
NotificationCenter.default.addObserver(self, selector: #selector(textDidChange(_:)), name: UITextField.textDidChangeNotification, object: usernameTextField)
NotificationCenter.default.addObserver(self, selector: #selector(textDidChange(_:)), name: UITextField.textDidChangeNotification, object: passwordTextField)
tableView.register(ImageHeaderView.self, forHeaderFooterViewReuseIdentifier: "SectionHeader")
}
private func setupFooter() {
switch accountType {
case .bazQux:
footerLabel.text = NSLocalizedString("Sign in to your BazQux account and sync your feeds across your devices. Your username and password will be encrypted and stored in Keychain.\n\nDont have a BazQux account?", comment: "BazQux")
signUpButton.setTitle(NSLocalizedString("Sign Up Here", comment: "BazQux SignUp"), for: .normal)
case .inoreader:
footerLabel.text = NSLocalizedString("Sign in to your InoReader account and sync your feeds across your devices. Your username and password will be encrypted and stored in Keychain.\n\nDont have an InoReader account?", comment: "InoReader")
signUpButton.setTitle(NSLocalizedString("Sign Up Here", comment: "InoReader SignUp"), for: .normal)
case .theOldReader:
footerLabel.text = NSLocalizedString("Sign in to your The Old Reader account and sync your feeds across your devices. Your username and password will be encrypted and stored in Keychain.\n\nDont have a The Old Reader account?", comment: "TOR")
signUpButton.setTitle(NSLocalizedString("Sign Up Here", comment: "TOR SignUp"), for: .normal)
case .freshRSS:
footerLabel.text = NSLocalizedString("Sign in to your FreshRSS instance and sync your feeds across your devices. Your username and password will be encrypted and stored in Keychain.\n\nDont have an FreshRSS instance?", comment: "FreshRSS")
signUpButton.setTitle(NSLocalizedString("Find Out More", comment: "FreshRSS SignUp"), for: .normal)
default:
return
}
}
override func tableView(_ tableView: UITableView, heightForHeaderInSection section: Int) -> CGFloat {
return section == 0 ? ImageHeaderView.rowHeight : super.tableView(tableView, heightForHeaderInSection: section)
}
override func tableView(_ tableView: UITableView, viewForHeaderInSection section: Int) -> UIView? {
if section == 0 {
let headerView = tableView.dequeueReusableHeaderFooterView(withIdentifier: "SectionHeader") as! ImageHeaderView
headerView.imageView.image = headerViewImage()
return headerView
} else {
return super.tableView(tableView, viewForHeaderInSection: section)
}
}
override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
switch section {
case 0:
switch accountType {
case .freshRSS:
return 3
default:
return 2
}
default:
return 1
}
}
@IBAction func cancel(_ sender: Any) {
dismiss(animated: true, completion: nil)
}
@IBAction func showHidePassword(_ sender: Any) {
if passwordTextField.isSecureTextEntry {
passwordTextField.isSecureTextEntry = false
showHideButton.setTitle("Hide", for: .normal)
} else {
passwordTextField.isSecureTextEntry = true
showHideButton.setTitle("Show", for: .normal)
}
}
@IBAction func action(_ sender: Any) {
guard validateDataEntry(), let type = accountType else {
return
}
let username = usernameTextField.text!
let password = passwordTextField.text!
let url = apiURL()!
// When you fill in the email address via auto-complete it adds extra whitespace
let trimmedUsername = username.trimmingCharacters(in: .whitespaces)
guard account != nil || !AccountManager.shared.duplicateServiceAccount(type: type, username: trimmedUsername) else {
showError(NSLocalizedString("There is already an account of that type with that username created.", comment: "Duplicate Error"))
return
}
startAnimatingActivityIndicator()
disableNavigation()
let credentials = Credentials(type: .readerBasic, username: trimmedUsername, secret: password)
Account.validateCredentials(type: type, credentials: credentials, endpoint: url) { result in
self.stopAnimatingActivityIndicator()
self.enableNavigation()
switch result {
case .success(let validatedCredentials):
if let validatedCredentials = validatedCredentials {
if self.account == nil {
self.account = AccountManager.shared.createAccount(type: type)
}
do {
self.account?.endpointURL = url
try? self.account?.removeCredentials(type: .readerBasic)
try? self.account?.removeCredentials(type: .readerAPIKey)
try self.account?.storeCredentials(credentials)
try self.account?.storeCredentials(validatedCredentials)
self.dismiss(animated: true, completion: nil)
self.account?.refreshAll() { result in
switch result {
case .success:
break
case .failure(let error):
self.showError(NSLocalizedString(error.localizedDescription, comment: "Accoount Refresh Error"))
}
}
//self.delegate?.dismiss()
} catch {
self.showError(NSLocalizedString("Keychain error while storing credentials.", comment: "Credentials Error"))
self.logger.error("Keychain error while storing credentials: \(error.localizedDescription, privacy: .public).")
}
} else {
self.showError(NSLocalizedString("Invalid username/password combination.", comment: "Credentials Error"))
}
case .failure(let error):
self.showError(error.localizedDescription)
}
}
}
private func retrieveCredentialsForAccount(for account: Account) throws -> Credentials? {
switch accountType {
case .bazQux, .inoreader, .theOldReader, .freshRSS:
return try account.retrieveCredentials(type: .readerBasic)
default:
return nil
}
}
private func headerViewImage() -> UIImage? {
if let accountType = accountType {
switch accountType {
case .bazQux:
return AppAssets.accountBazQuxImage
case .inoreader:
return AppAssets.accountInoreaderImage
case .theOldReader:
return AppAssets.accountTheOldReaderImage
case .freshRSS:
return AppAssets.accountFreshRSSImage
default:
return nil
}
}
return nil
}
private func validateDataEntry() -> Bool {
switch accountType {
case .freshRSS:
if !usernameTextField.hasText || !passwordTextField.hasText || !apiURLTextField.hasText {
showError(NSLocalizedString("Username, password, and API URL are required.", comment: "Credentials Error"))
return false
}
guard let _ = URL(string: apiURLTextField.text!) else {
showError(NSLocalizedString("Invalid API URL.", comment: "Invalid API URL"))
return false
}
default:
if !usernameTextField.hasText || !passwordTextField.hasText {
showError(NSLocalizedString("Username and password are required.", comment: "Credentials Error"))
return false
}
}
return true
}
@IBAction func signUpWithProvider(_ sender: Any) {
var url: URL!
switch accountType {
case .bazQux:
url = URL(string: "https://bazqux.com")!
case .inoreader:
url = URL(string: "https://www.inoreader.com")!
case .theOldReader:
url = URL(string: "https://theoldreader.com")!
case .freshRSS:
url = URL(string: "https://freshrss.org")!
default:
return
}
let safari = SFSafariViewController(url: url)
safari.modalPresentationStyle = .currentContext
self.present(safari, animated: true, completion: nil)
}
private func apiURL() -> URL? {
switch accountType {
case .freshRSS:
return URL(string: apiURLTextField.text!)!
case .inoreader:
return URL(string: ReaderAPIVariant.inoreader.host)!
case .bazQux:
return URL(string: ReaderAPIVariant.bazQux.host)!
case .theOldReader:
return URL(string: ReaderAPIVariant.theOldReader.host)!
default:
return nil
}
}
@objc func textDidChange(_ note: Notification) {
actionButton.isEnabled = !(usernameTextField.text?.isEmpty ?? false)
}
private func showError(_ message: String) {
presentError(title: "Error", message: message)
}
private func enableNavigation() {
self.cancelBarButtonItem.isEnabled = true
self.actionButton.isEnabled = true
}
private func disableNavigation() {
cancelBarButtonItem.isEnabled = false
actionButton.isEnabled = false
}
private func startAnimatingActivityIndicator() {
activityIndicator.isHidden = false
activityIndicator.startAnimating()
}
private func stopAnimatingActivityIndicator() {
self.activityIndicator.isHidden = true
self.activityIndicator.stopAnimating()
}
}
extension ReaderAPIAccountViewController: UITextFieldDelegate {
func textFieldShouldReturn(_ textField: UITextField) -> Bool {
textField.resignFirstResponder()
return true
}
}

View File

@@ -17,7 +17,7 @@ struct CloudKitAddAccountView: View {
var body: some View {
NavigationView {
Form {
Section(header: cloudKitHeader) {}
AccountSectionHeader(accountType: .cloudKit)
Section { createCloudKitAccount }
Section(footer: cloudKitExplainer) {}
}
@@ -33,23 +33,11 @@ struct CloudKitAddAccountView: View {
} message: {
Text(addAccountError.0?.localizedDescription ?? "Unknown Error")
}
.onReceive(NotificationCenter.default.publisher(for: .UserDidAddAccount)) { _ in
dismiss()
}
.dismissOnExternalContextLaunch()
.dismissOnAccountAdd()
}
}
var cloudKitHeader: some View {
HStack {
Spacer()
Image(uiImage: AppAssets.accountCloudKitImage)
.resizable()
.aspectRatio(contentMode: .fit)
.frame(width: 48, height: 48)
Spacer()
}
}
var createCloudKitAccount: some View {
Button {
guard FileManager.default.ubiquityIdentityToken != nil else {

View File

@@ -0,0 +1,167 @@
//
// FeedbinAddAccountView.swift
// NetNewsWire-iOS
//
// Created by Stuart Breckenridge on 18/12/2022.
// Copyright © 2022 Ranchero Software. All rights reserved.
//
import SwiftUI
import Account
import Secrets
import RSWeb
import SafariServices
import RSCore
struct FeedbinAddAccountView: View {
@Environment(\.dismiss) private var dismiss
@State var account: Account? = nil
@State private var accountEmail: String = ""
@State private var accountPassword: String = ""
@State private var showProgressIndicator: Bool = false
@State private var accountError: (Error?, Bool) = (nil, false)
var body: some View {
NavigationView {
Form {
AccountSectionHeader(accountType: .feedbin)
accountDetails
accountButton
Section(footer: feedbinAccountExplainer) {}
}
.task {
retrieveCredentials()
}
.toolbar {
ToolbarItem(placement: .navigationBarLeading) {
Button(action: { dismiss() }, label: { Text("CANCEL_BUTTON_TITLE", tableName: "Buttons") })
.disabled(showProgressIndicator)
}
ToolbarItem(placement: .navigationBarTrailing) {
if showProgressIndicator { ProgressView() }
}
}
.alert(Text("ERROR_TITLE", tableName: "Errors"), isPresented: $accountError.1) {
Button(role: .cancel) {
//
} label: {
Text("DISMISS_BUTTON_TITLE", tableName: "Buttons")
}
} message: {
Text(accountError.0?.localizedDescription ?? "Error")
}
.navigationTitle(Text(account?.type.localizedAccountName() ?? ""))
.navigationBarTitleDisplayMode(.inline)
.dismissOnExternalContextLaunch()
.dismissOnAccountAdd()
}
}
var accountDetails: some View {
Section {
TextField("Email", text: $accountEmail, prompt: Text("ACCOUNT_EMAIL_ADDRESS_PROMPT", tableName: "Account"))
.autocorrectionDisabled()
.autocapitalization(.none)
SecureField("Password", text: $accountPassword, prompt: Text("ACCOUNT_PASSWORD_PROMPT", tableName: "Account"))
}
}
var accountButton: some View {
Section {
Button {
Task {
do {
try await executeAccountCredentials()
dismiss()
} catch {
accountError = (error, true)
}
}
} label: {
HStack{
Spacer()
if account == nil {
Text("ADD_ACCOUNT_BUTTON_TITLE", tableName: "Buttons")
} else {
Text("UPDATE_CREDENTIALS_BUTTON_TITLE", tableName: "Buttons")
}
Spacer()
}
}
}
}
var feedbinAccountExplainer: some View {
Text("FEEDBIN_FOOTER_EXPLAINER", tableName: "Account").multilineTextAlignment(.center)
}
private func retrieveCredentials() {
if let account = account {
do {
if let creds = try account.retrieveCredentials(type: .basic) {
accountEmail = creds.username
accountPassword = creds.secret
}
} catch {
accountError = (error, true)
}
}
}
private func executeAccountCredentials() async throws {
let trimmedEmailAddress = accountEmail.trimmingWhitespace
guard (account != nil || !AccountManager.shared.duplicateServiceAccount(type: .feedbin, username: trimmedEmailAddress)) else {
throw LocalizedNetNewsWireError.duplicateAccount
}
showProgressIndicator = true
let credentials = Credentials(type: .basic, username: trimmedEmailAddress, secret: accountPassword)
return try await withCheckedThrowingContinuation { continuation in
Account.validateCredentials(type: .feedbin, credentials: credentials) { result in
switch result {
case .success(let credentials):
if let validatedCredentials = credentials {
if self.account == nil {
self.account = AccountManager.shared.createAccount(type: .feedbin)
}
do {
try? self.account?.removeCredentials(type: .basic)
try self.account?.storeCredentials(validatedCredentials)
self.account?.refreshAll(completion: { result in
switch result {
case .success(_):
showProgressIndicator = false
continuation.resume()
case .failure(let failure):
showProgressIndicator = false
continuation.resume(throwing: failure)
}
})
} catch {
showProgressIndicator = false
continuation.resume(throwing: LocalizedNetNewsWireError.keychainError)
}
} else {
showProgressIndicator = false
continuation.resume(throwing: LocalizedNetNewsWireError.invalidUsernameOrPassword)
}
case .failure(let failure):
showProgressIndicator = false
continuation.resume(throwing: failure)
}
}
}
}
}
struct FeedbinAddAccountView_Previews: PreviewProvider {
static var previews: some View {
FeedbinAddAccountView()
}
}

View File

@@ -17,7 +17,7 @@ struct LocalAddAccountView: View {
var body: some View {
NavigationView {
Form {
Section(header: accountHeaderView) {}
AccountSectionHeader(accountType: .onMyMac)
Section { accountNameSection }
Section { addAccountButton }
Section(footer: accountFooterView) {}
@@ -29,9 +29,8 @@ struct LocalAddAccountView: View {
}
.navigationTitle(deviceAccountName())
.navigationBarTitleDisplayMode(.inline)
.onReceive(NotificationCenter.default.publisher(for: .UserDidAddAccount)) { _ in
dismiss()
}
.dismissOnExternalContextLaunch()
.dismissOnAccountAdd()
}
}
@@ -39,6 +38,8 @@ struct LocalAddAccountView: View {
TextField("Name",
text: $accountName,
prompt: Text("ACCOUNT_NAME", tableName: "Account"))
.autocorrectionDisabled()
.autocapitalization(.none)
}
var addAccountButton: some View {
@@ -54,17 +55,6 @@ struct LocalAddAccountView: View {
}
}
var accountHeaderView: some View {
HStack {
Spacer()
Image(uiImage: accountImage())
.resizable()
.aspectRatio(contentMode: .fit)
.frame(width: 48, height: 48)
Spacer()
}
}
var accountFooterView: some View {
HStack {
Spacer()

View File

@@ -0,0 +1,173 @@
//
// NewsBlurAddAccountView.swift
// NetNewsWire-iOS
//
// Created by Stuart Breckenridge on 18/12/2022.
// Copyright © 2022 Ranchero Software. All rights reserved.
//
import SwiftUI
import Account
import Secrets
import RSWeb
import RSCore
struct NewsBlurAddAccountView: View {
@Environment(\.dismiss) private var dismiss
@State var account: Account? = nil
@State private var accountUserName: String = ""
@State private var accountPassword: String = ""
@State private var showProgressIndicator: Bool = false
@State private var accountError: (Error?, Bool) = (nil, false)
var body: some View {
NavigationView {
Form {
AccountSectionHeader(accountType: .newsBlur)
accountDetails
accountButton
Section(footer: newsBlurAccountExplainer) {}
}
.toolbar {
ToolbarItem(placement: .navigationBarLeading) {
Button(action: { dismiss() }, label: { Text("CANCEL_BUTTON_TITLE", tableName: "Buttons") })
.disabled(showProgressIndicator)
}
ToolbarItem(placement: .navigationBarTrailing) {
if showProgressIndicator { ProgressView() }
}
}
.navigationTitle(Text(AccountType.newsBlur.localizedAccountName()))
.navigationBarTitleDisplayMode(.inline)
.task {
retreiveCredentials()
}
.alert(Text("ERROR_TITLE", tableName: "Errors"), isPresented: $accountError.1) {
Button(role: .cancel) {
//
} label: {
Text("DISMISS_BUTTON_TITLE", tableName: "Buttons")
}
} message: {
Text(accountError.0?.localizedDescription ?? "")
}
.dismissOnExternalContextLaunch()
.dismissOnAccountAdd()
}
}
func retreiveCredentials() {
if let account = account {
do {
let credentials = try account.retrieveCredentials(type: .newsBlurBasic)
if let credentials = credentials {
self.accountUserName = credentials.username
self.accountPassword = credentials.secret
} else {
print("No cred")
}
} catch {
print(error.localizedDescription)
}
} else {
print("No account")
}
}
var accountDetails: some View {
Section {
TextField("Email", text: $accountUserName, prompt: Text("ACCOUNT_USERNAME_PROMPT", tableName: "Account"))
.autocorrectionDisabled()
.autocapitalization(.none)
SecureField("Password", text: $accountPassword, prompt: Text("ACCOUNT_PASSWORD_PROMPT", tableName: "Account"))
}
}
var accountButton: some View {
Section {
Button {
Task {
do {
try await executeAccountCredentials()
dismiss()
} catch {
accountError = (error, true)
}
}
} label: {
HStack{
Spacer()
if account == nil {
Text("ADD_ACCOUNT_BUTTON_TITLE", tableName: "Buttons")
} else {
Text("UPDATE_CREDENTIALS_BUTTON_TITLE", tableName: "Buttons")
}
Spacer()
}
}
}
}
var newsBlurAccountExplainer: some View {
Text("NEWSBLUR_FOOTER_EXPLAINER", tableName: "Account").multilineTextAlignment(.center)
}
private func executeAccountCredentials() async throws {
let trimmedEmailAddress = accountUserName.trimmingWhitespace
guard (account != nil || !AccountManager.shared.duplicateServiceAccount(type: .newsBlur, username: trimmedEmailAddress)) else {
throw LocalizedNetNewsWireError.duplicateAccount
}
showProgressIndicator = true
let basicCredentials = Credentials(type: .newsBlurBasic, username: trimmedEmailAddress, secret: accountPassword)
return try await withCheckedThrowingContinuation { continuation in
Account.validateCredentials(type: .newsBlur, credentials: basicCredentials) { result in
switch result {
case .success(let credentials):
if let sessionsCredentials = credentials {
if self.account == nil {
self.account = AccountManager.shared.createAccount(type: .newsBlur)
}
do {
try self.account?.removeCredentials(type: .newsBlurBasic)
try self.account?.removeCredentials(type: .newsBlurSessionId)
try self.account?.storeCredentials(basicCredentials)
try self.account?.storeCredentials(sessionsCredentials)
self.account?.refreshAll(completion: { result in
switch result {
case .success(_):
showProgressIndicator = false
continuation.resume()
case .failure(let failure):
showProgressIndicator = false
continuation.resume(throwing: failure)
}
})
} catch {
showProgressIndicator = false
continuation.resume(throwing: LocalizedNetNewsWireError.keychainError)
}
} else {
showProgressIndicator = false
continuation.resume(throwing: LocalizedNetNewsWireError.invalidUsernameOrPassword)
}
case .failure(let failure):
showProgressIndicator = false
continuation.resume(throwing: failure)
}
}
}
}
}
struct NewsBlurAddAccountView_Previews: PreviewProvider {
static var previews: some View {
NewsBlurAddAccountView()
}
}

View File

@@ -29,14 +29,16 @@ struct ReaderAPIAddAccountView: View {
var body: some View {
NavigationView {
Form {
Section(header: readerAccountImage) {}
if accountType != nil {
AccountSectionHeader(accountType: accountType!)
}
accountDetailsSection
Section(footer: readerAccountExplainer) {}
}
.navigationTitle(Text(accountType?.localizedAccountName() ?? ""))
.navigationBarTitleDisplayMode(.inline)
.task {
try? retrieveAccountCredentials()
retrieveAccountCredentials()
}
.toolbar {
ToolbarItem(placement: .navigationBarLeading) {
@@ -55,10 +57,8 @@ struct ReaderAPIAddAccountView: View {
} message: {
Text(accountSetupError.0?.localizedDescription ?? "")
}
.onReceive(NotificationCenter.default.publisher(for: .UserDidAddAccount)) { _ in
dismiss()
}
.edgesIgnoringSafeArea(.bottom) // Fix to make sure view is not offset from the top when presented
.dismissOnExternalContextLaunch()
.dismissOnAccountAdd()
}
}
@@ -78,47 +78,19 @@ struct ReaderAPIAddAccountView: View {
}
}
var readerAccountImage: some View {
HStack {
Spacer()
if accountType == nil { Text("") }
switch accountType! {
case .bazQux:
Image(uiImage: AppAssets.accountBazQuxImage)
.resizable()
.aspectRatio(contentMode: .fit)
.frame(width: 48, height: 48)
case .inoreader:
Image(uiImage: AppAssets.accountInoreaderImage)
.resizable()
.aspectRatio(contentMode: .fit)
.frame(width: 48, height: 48)
case .theOldReader:
Image(uiImage: AppAssets.accountTheOldReaderImage)
.resizable()
.aspectRatio(contentMode: .fit)
.frame(width: 48, height: 48)
case .freshRSS:
Image(uiImage: AppAssets.accountFreshRSSImage)
.resizable()
.aspectRatio(contentMode: .fit)
.frame(width: 48, height: 48)
default:
Text("")
}
Spacer()
}
}
var accountDetailsSection: some View {
Group {
Section {
TextField("Username", text: $accountUserName)
.autocorrectionDisabled()
.autocapitalization(.none)
SecureField("Password", text: $accountSecret)
if accountType == .freshRSS && accountCredentials == nil {
TextField("FreshRSS URL", text: $accountAPIUrl, prompt: Text("fresh.rss.net/api/greader.php"))
.autocorrectionDisabled()
.autocapitalization(.none)
}
}
@@ -127,6 +99,7 @@ struct ReaderAPIAddAccountView: View {
Task {
do {
try await executeAccountCredentials()
dismiss()
} catch {
accountSetupError = (error, true)
}
@@ -150,7 +123,7 @@ struct ReaderAPIAddAccountView: View {
// MARK: - API
private func retrieveAccountCredentials() throws {
private func retrieveAccountCredentials() {
if let account = account {
do {
if let creds = try account.retrieveCredentials(type: .readerBasic) {

View File

@@ -9,7 +9,7 @@
/* Account Inspector */
"ACCOUNT_NAME_FIELD" = "Account Name";
"ACTIVE" = "Active";
"CLOUDKIT_LIMITATIONS_TITLE" = "[iCloud Syncing Limitations & Solutions](https://netnewswire.com/help/iCloud)";
"CLOUDKIT_LIMITATIONS_LINK" = "[iCloud Syncing Limitations & Solutions](https://netnewswire.com/help/iCloud)";
"REMOVE_ACCOUNT_TITLE" = "Remove Account";
"REMOVE_FEEDLY_MESSAGE" = "Are you sure you want to remove this account? NetNewsWire will no longer be able to access articles and feeds unless the account is added again.";
"REMOVE_ACCOUNT_MESSAGE" = "Are you sure you want to remove this account? This cannot be undone.";

View File

@@ -20,7 +20,7 @@ struct AccountInspectorView: View {
var body: some View {
Form {
Section(header: accountHeaderView){}
AccountSectionHeader(accountType: account.type)
accountNameAndActiveSection
if account.type != .onMyMac &&
@@ -46,13 +46,14 @@ struct AccountInspectorView: View {
case .theOldReader, .bazQux, .inoreader, .freshRSS:
ReaderAPIAddAccountView(accountType: account.type, account: account)
case .feedbin:
Text("FEEDBIN")
FeedbinAddAccountView(account: account)
case .newsBlur:
Text("NEWSBLUR")
NewsBlurAddAccountView(account: account)
default:
EmptyView()
}
}
.dismissOnExternalContextLaunch()
}
var accountHeaderView: some View {
@@ -135,7 +136,7 @@ struct AccountInspectorView: View {
var cloudKitLimitations: some View {
HStack {
Spacer()
Text("CLOUDKIT_LIMITATIONS_TITLE", tableName: "Inspector")
Text("CLOUDKIT_LIMITATIONS_LINK", tableName: "Inspector")
Spacer()
}
}

View File

@@ -50,9 +50,9 @@ struct ExtensionInspectorView: View {
}
.navigationTitle(Text(extensionPoint?.title ?? ""))
.edgesIgnoringSafeArea(.bottom)
.dismissOnExternalContextLaunch()
}
var extensionHeader: some View {
HStack {
Spacer()

View File

@@ -63,6 +63,7 @@ struct WebFeedInspectorView: View {
.sheet(isPresented: $showHomePage, onDismiss: nil) {
SafariView(url: URL(string: webFeed.homePageURL!)!)
}
.dismissOnExternalContextLaunch()
}
var webFeedHeaderView: some View {

View File

@@ -66,7 +66,7 @@ struct AccountsManagementView: View {
Button(role: .destructive) {
AccountManager.shared.deleteAccount(accountToRemove!)
} label: {
Text("REMOVE_BUTTON_TITLE", tableName: "Buttons")
Text("REMOVE_ACCOUNT_BUTTON_TITLE", tableName: "Buttons")
}
Button(role: .cancel) {

View File

@@ -90,15 +90,15 @@ struct AddAccountListView: View {
LocalAddAccountView()
case .cloudKit:
CloudKitAddAccountView()
case .newsBlur:
NewsBlurAddAccountView()
case .freshRSS, .inoreader, .bazQux, .theOldReader:
ReaderAPIAddAccountView(accountType: viewModel.showAddAccountSheet.accountType, account: nil)
default:
Text(viewModel.showAddAccountSheet.accountType.localizedAccountName())
}
}
.onReceive(NotificationCenter.default.publisher(for: .UserDidAddAccount)) { _ in
dismiss()
}
.dismissOnAccountAdd()
}
}

View File

@@ -108,9 +108,7 @@ struct SettingsView: View {
}
}
}
.onReceive(NotificationCenter.default.publisher(for: .LaunchedFromExternalAction), perform: { _ in
dismiss()
})
.dismissOnExternalContextLaunch()
.fileImporter(isPresented: $viewModel.showImportView, allowedContentTypes: OPMLDocument.readableContentTypes) { result in
switch result {
case .success(let url):

View File

@@ -0,0 +1,61 @@
//
// AccountSectionHeader.swift
// NetNewsWire-iOS
//
// Created by Stuart Breckenridge on 18/12/2022.
// Copyright © 2022 Ranchero Software. All rights reserved.
//
import SwiftUI
import Account
struct AccountSectionHeader: View {
var accountType: AccountType
var body: some View {
Section(header: headerImage) {}
}
var headerImage: some View {
HStack {
Spacer()
Image(uiImage: imageToUse())
.resizable()
.aspectRatio(contentMode: .fit)
.frame(width: 48, height: 48)
Spacer()
}
}
private func imageToUse() -> UIImage {
switch accountType {
case .onMyMac:
if UIDevice.current.userInterfaceIdiom == .pad { return AppAssets.accountLocalPadImage }
return AppAssets.accountLocalPhoneImage
case .cloudKit:
return AppAssets.accountCloudKitImage
case .feedly:
return AppAssets.accountFeedlyImage
case .feedbin:
return AppAssets.accountFeedbinImage
case .newsBlur:
return AppAssets.accountNewsBlurImage
case .freshRSS:
return AppAssets.accountFreshRSSImage
case .inoreader:
return AppAssets.accountInoreaderImage
case .bazQux:
return AppAssets.accountBazQuxImage
case .theOldReader:
return AppAssets.accountTheOldReaderImage
}
}
}
struct AccountHeader_Previews: PreviewProvider {
static var previews: some View {
AccountSectionHeader(accountType: .onMyMac)
}
}

View File

@@ -0,0 +1,28 @@
//
// View+DismissOnAccountAdd.swift
// NetNewsWire-iOS
//
// Created by Stuart Breckenridge on 18/12/2022.
// Copyright © 2022 Ranchero Software. All rights reserved.
//
import SwiftUI
struct DismissOnAccountAdd: ViewModifier {
@Environment(\.dismiss) private var dismiss
func body(content: Content) -> some View {
content
.onReceive(NotificationCenter.default.publisher(for: .UserDidAddAccount)) { _ in
dismiss()
}
}
}
extension View {
func dismissOnAccountAdd() -> some View {
modifier(DismissOnAccountAdd())
}
}

View File

@@ -0,0 +1,29 @@
//
// View+DismissOnExternalContext.swift
// NetNewsWire-iOS
//
// Created by Stuart Breckenridge on 18/12/2022.
// Copyright © 2022 Ranchero Software. All rights reserved.
//
import SwiftUI
struct DismissOnExternalContext: ViewModifier {
@Environment(\.dismiss) private var dismiss
func body(content: Content) -> some View {
content
.onReceive(NotificationCenter.default.publisher(for: .LaunchedFromExternalAction)) { _ in
dismiss()
}
}
}
extension View {
func dismissOnExternalContextLaunch() -> some View {
modifier(DismissOnExternalContext())
}
}

View File

@@ -13,9 +13,7 @@ import Account
extension UIViewController {
func presentError(_ error: Error, dismiss: (() -> Void)? = nil) {
if let accountError = error as? AccountError, accountError.isCredentialsError {
presentAccountError(accountError, dismiss: dismiss)
} else if let decodingError = error as? DecodingError {
if let decodingError = error as? DecodingError {
let errorTitle = NSLocalizedString("Error", comment: "Error")
var informativeText: String = ""
switch decodingError {
@@ -53,38 +51,3 @@ extension UIViewController {
}
}
private extension UIViewController {
func presentAccountError(_ error: AccountError, dismiss: (() -> Void)? = nil) {
let title = NSLocalizedString("Account Error", comment: "Account Error")
let alertController = UIAlertController(title: title, message: error.localizedDescription, preferredStyle: .alert)
if error.account?.type == .feedbin {
let credentialsTitle = NSLocalizedString("Update Credentials", comment: "Update Credentials")
let credentialsAction = UIAlertAction(title: credentialsTitle, style: .default) { [weak self] _ in
dismiss?()
let navController = UIStoryboard.account.instantiateViewController(withIdentifier: "FeedbinAccountNavigationViewController") as! UINavigationController
navController.modalPresentationStyle = .formSheet
let addViewController = navController.topViewController as! FeedbinAccountViewController
addViewController.account = error.account
self?.present(navController, animated: true)
}
alertController.addAction(credentialsAction)
alertController.preferredAction = credentialsAction
}
let dismissTitle = NSLocalizedString("OK", comment: "OK")
let dismissAction = UIAlertAction(title: dismissTitle, style: .default) { _ in
dismiss?()
}
alertController.addAction(dismissAction)
self.present(alertController, animated: true, completion: nil)
}
}