From 21b8fc3f174293557a1d7f1d07bc1415489e327c Mon Sep 17 00:00:00 2001 From: Nick Alexander Date: Thu, 19 Mar 2015 10:57:40 -0700 Subject: [PATCH 1/3] No bug - Make FxA project App extension compatible to quiet a warning. --- FxA/FxA.xcodeproj/project.pbxproj | 3 +++ 1 file changed, 3 insertions(+) diff --git a/FxA/FxA.xcodeproj/project.pbxproj b/FxA/FxA.xcodeproj/project.pbxproj index 0acdf7cd87f5..c49ead55da5f 100644 --- a/FxA/FxA.xcodeproj/project.pbxproj +++ b/FxA/FxA.xcodeproj/project.pbxproj @@ -794,6 +794,7 @@ 28F9520F19D0F9FB00DCE892 /* Debug */ = { isa = XCBuildConfiguration; buildSettings = { + APPLICATION_EXTENSION_API_ONLY = YES; CODE_SIGN_IDENTITY = "iPhone Developer"; DEFINES_MODULE = YES; DYLIB_COMPATIBILITY_VERSION = 1; @@ -821,6 +822,7 @@ 28F9521019D0F9FB00DCE892 /* FennecAurora */ = { isa = XCBuildConfiguration; buildSettings = { + APPLICATION_EXTENSION_API_ONLY = YES; CODE_SIGN_IDENTITY = "iPhone Developer"; DEFINES_MODULE = YES; DYLIB_COMPATIBILITY_VERSION = 1; @@ -931,6 +933,7 @@ E4D438FC1A8D32B6003FCF55 /* FennecNightly */ = { isa = XCBuildConfiguration; buildSettings = { + APPLICATION_EXTENSION_API_ONLY = YES; CODE_SIGN_IDENTITY = "iPhone Developer"; DEFINES_MODULE = YES; DYLIB_COMPATIBILITY_VERSION = 1; From cbe12765a02fac9218b938b062c0117263a9c0b4 Mon Sep 17 00:00:00 2001 From: Nick Alexander Date: Thu, 19 Mar 2015 11:00:31 -0700 Subject: [PATCH 2/3] Bug 1130495 - Pre: Extract some UX constants for re-use. --- Client/Frontend/Widgets/SiteTableViewController.swift | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/Client/Frontend/Widgets/SiteTableViewController.swift b/Client/Frontend/Widgets/SiteTableViewController.swift index e27cc1e57ee4..7e2efd4c0a36 100644 --- a/Client/Frontend/Widgets/SiteTableViewController.swift +++ b/Client/Frontend/Widgets/SiteTableViewController.swift @@ -5,6 +5,11 @@ import UIKit import Storage +struct SiteTableViewControllerUX { + static let HeaderHeight = CGFloat(25) + static let RowHeight = CGFloat(58) +} + private class SiteTableViewHeader : UITableViewHeaderFooterView { // I can't get drawRect to play nicely with the glass background. As a fallback // we just use views for the top and bottom borders. @@ -102,10 +107,10 @@ class SiteTableViewController: UIViewController, UITableViewDelegate, UITableVie } func tableView(tableView: UITableView, heightForHeaderInSection section: Int) -> CGFloat { - return 25 + return SiteTableViewControllerUX.HeaderHeight } func tableView(tableView: UITableView, heightForRowAtIndexPath indexPath: NSIndexPath) -> CGFloat { - return 58 + return SiteTableViewControllerUX.RowHeight } } From 85eaca7173441e2b3d919640486d6d944c2ef516 Mon Sep 17 00:00:00 2001 From: Nick Alexander Date: Tue, 17 Mar 2015 10:09:06 -0700 Subject: [PATCH 3/3] Bug 1130495 - First version of Synced (Remote) Tabs home panel. There a number of TODO items outstanding: * this needs updated assets; * the timestamp localized and converted to a relative time string; * empty view artwork. --- Client.xcodeproj/project.pbxproj | 8 +- .../deviceTypeDesktop.imageset/Contents.json | 21 +++ .../sync_desktop.png | Bin 0 -> 624 bytes .../deviceTypeMobile.imageset/Contents.json | 21 +++ .../deviceTypeMobile.imageset/sync_mobile.png | Bin 0 -> 602 bytes Client/Frontend/Home/HomePanels.swift | 9 ++ Client/Frontend/Home/RemoteTabsPanel.swift | 130 ++++++++++++++++++ Client/Frontend/Widgets/TwoLineCell.swift | 52 +++++++ Providers/Profile.swift | 9 ++ 9 files changed, 249 insertions(+), 1 deletion(-) create mode 100644 Client/Assets/Images.xcassets/deviceTypeDesktop.imageset/Contents.json create mode 100644 Client/Assets/Images.xcassets/deviceTypeDesktop.imageset/sync_desktop.png create mode 100644 Client/Assets/Images.xcassets/deviceTypeMobile.imageset/Contents.json create mode 100644 Client/Assets/Images.xcassets/deviceTypeMobile.imageset/sync_mobile.png create mode 100644 Client/Frontend/Home/RemoteTabsPanel.swift diff --git a/Client.xcodeproj/project.pbxproj b/Client.xcodeproj/project.pbxproj index cafb2be15cdb..04efeddee271 100644 --- a/Client.xcodeproj/project.pbxproj +++ b/Client.xcodeproj/project.pbxproj @@ -113,6 +113,8 @@ 2FDE87521ABA3EA0005317B1 /* TimeConstants.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2F3444561AB22A4B00FD9731 /* TimeConstants.swift */; }; 2FDE87531ABA3EB4005317B1 /* ColorUtils.swift in Sources */ = {isa = PBXBuildFile; fileRef = D35275151AA8D13B00E9C906 /* ColorUtils.swift */; }; 2FDE87541ABA3EBB005317B1 /* KeyboardHelper.swift in Sources */ = {isa = PBXBuildFile; fileRef = D39999A81AA01D65005AED21 /* KeyboardHelper.swift */; }; + 2FDE87FE1ABB3817005317B1 /* RemoteTabsPanel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2FDE87FD1ABB3817005317B1 /* RemoteTabsPanel.swift */; }; + 2FDE881B1ABB3921005317B1 /* RemoteTabsPanel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2FDE87FD1ABB3817005317B1 /* RemoteTabsPanel.swift */; }; 2FEBABAF1AB3659000DB5728 /* ResultTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2FEBABAE1AB3659000DB5728 /* ResultTests.swift */; }; 39A362D91AAF5ECE00F47390 /* XCGLogger.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 39A362D41AAF5E2C00F47390 /* XCGLogger.framework */; }; 39A362DA1AAF5ECE00F47390 /* XCGLogger.framework in Copy Frameworks */ = {isa = PBXBuildFile; fileRef = 39A362D41AAF5E2C00F47390 /* XCGLogger.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; }; @@ -672,6 +674,7 @@ 2FA0AF801AAFB62E0083E9FA /* HawkHelper.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = HawkHelper.swift; sourceTree = ""; }; 2FA0AFA21AAFF1E20083E9FA /* HawkHelperTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = HawkHelperTests.swift; path = FxATests/HawkHelperTests.swift; sourceTree = ""; }; 2FDB10921A9FBEC5006CF312 /* ProfilePrefsTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ProfilePrefsTests.swift; sourceTree = ""; }; + 2FDE87FD1ABB3817005317B1 /* RemoteTabsPanel.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = RemoteTabsPanel.swift; sourceTree = ""; }; 2FEBABAE1AB3659000DB5728 /* ResultTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = ResultTests.swift; path = SyncTests/ResultTests.swift; sourceTree = ""; }; 39A362B21AAF5E2B00F47390 /* XCGLogger.xcodeproj */ = {isa = PBXFileReference; lastKnownFileType = "wrapper.pb-project"; name = XCGLogger.xcodeproj; path = Carthage/Checkouts/XCGLogger/XCGLogger/Library/XCGLogger.xcodeproj; sourceTree = ""; }; 4A59BF410BBD9B3BE71F4C7C /* TestHistory.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TestHistory.swift; sourceTree = ""; }; @@ -1393,13 +1396,14 @@ F84B22211A09122500AAB793 /* Home */ = { isa = PBXGroup; children = ( - F84B22221A09122500AAB793 /* HomePanelViewController.swift */, F84B22261A09127C00AAB793 /* Home.xcassets */, + F84B22221A09122500AAB793 /* HomePanelViewController.swift */, D30B101D1AA7F9C600C01CA3 /* HomePanels.swift */, 59A6825233896FC846499289 /* HistoryPanel.swift */, 59A6839879D615FC1C0D71CE /* BookmarksPanel.swift */, 0BF648101A9C54E900BA963C /* TopSitesPanel.swift */, 59A685F4EAD19EDEC854BCA4 /* ReaderPanel.swift */, + 2FDE87FD1ABB3817005317B1 /* RemoteTabsPanel.swift */, ); path = Home; sourceTree = ""; @@ -2091,6 +2095,7 @@ E42CCDE31A23A6F900B794D3 /* Clients.swift in Sources */, D30B101E1AA7F9C600C01CA3 /* HomePanels.swift in Sources */, F84B22041A0910F600AAB793 /* AppDelegate.swift in Sources */, + 2FDE87FE1ABB3817005317B1 /* RemoteTabsPanel.swift in Sources */, D31A0FC71A65D6D000DC8C7E /* SearchSuggestClient.swift in Sources */, D38B2D311A8D96D00040E6B5 /* GCDWebServerConnection.m in Sources */, D38B2D401A8D96D00040E6B5 /* GCDWebServerFileRequest.m in Sources */, @@ -2172,6 +2177,7 @@ 2F834D1B1A80629A006A0B7B /* FxAContentViewController.swift in Sources */, D3FA77971A43B5390010CD32 /* OpenSearch.swift in Sources */, 2FA7EAB41AB0BC6F00CE5347 /* FxAClient10.swift in Sources */, + 2FDE881B1ABB3921005317B1 /* RemoteTabsPanel.swift in Sources */, 28786E551AB0F5FA009EA9EF /* DeferredTests.swift in Sources */, D3FA777B1A43B2990010CD32 /* SearchTests.swift in Sources */, D38B2D441A8D96D00040E6B5 /* GCDWebServerMultiPartFormRequest.m in Sources */, diff --git a/Client/Assets/Images.xcassets/deviceTypeDesktop.imageset/Contents.json b/Client/Assets/Images.xcassets/deviceTypeDesktop.imageset/Contents.json new file mode 100644 index 000000000000..180d5a9aa23a --- /dev/null +++ b/Client/Assets/Images.xcassets/deviceTypeDesktop.imageset/Contents.json @@ -0,0 +1,21 @@ +{ + "images" : [ + { + "idiom" : "universal", + "scale" : "1x" + }, + { + "idiom" : "universal", + "scale" : "2x", + "filename" : "sync_desktop.png" + }, + { + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/Client/Assets/Images.xcassets/deviceTypeDesktop.imageset/sync_desktop.png b/Client/Assets/Images.xcassets/deviceTypeDesktop.imageset/sync_desktop.png new file mode 100644 index 0000000000000000000000000000000000000000..2f79d5d698b90f4d423f25f6df756e1cb5c155f1 GIT binary patch literal 624 zcmeAS@N?(olHy`uVBq!ia0vp^5kPFk!3HEvR?bUhU|=%$ba4!+V0=5r*6*-^%+ZtW zD;ks)0+l@D{<5UXT88g_jJo)akeuze zmorp1Zg?5A|6!?J!j{c(>&ssno%cG>aGULhcnhz%qx>s|59yOss`dQ%WE*6-^JWTF zsanRV7xOo^7qZ0X^}Pa^p38* z^Qqg)U2GtOPISEj$bQklhkOzT(3a_9TEUI;tZJ!aB5kiwKwX&KRQ zlI6swq74j_IVU`Qn!u)@*YZHw z>pg{=f4K`A9k_K~D856?QYmJ#Sb|wXjQ0n>cZZ`ZlcgWsYst>%oS@FPE&N;Oi>5Ei zJN4zC$`|}}-@c<%>rmb!mIdD(Ywx;Cot|YZq4uNrn~eld-h#;A%ci9EF8^qCf05+k Vvc4xK7l0{=!PC{xWt~$(69C!U6{P?G literal 0 HcmV?d00001 diff --git a/Client/Assets/Images.xcassets/deviceTypeMobile.imageset/Contents.json b/Client/Assets/Images.xcassets/deviceTypeMobile.imageset/Contents.json new file mode 100644 index 000000000000..ac1b19c676be --- /dev/null +++ b/Client/Assets/Images.xcassets/deviceTypeMobile.imageset/Contents.json @@ -0,0 +1,21 @@ +{ + "images" : [ + { + "idiom" : "universal", + "scale" : "1x" + }, + { + "idiom" : "universal", + "scale" : "2x", + "filename" : "sync_mobile.png" + }, + { + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/Client/Assets/Images.xcassets/deviceTypeMobile.imageset/sync_mobile.png b/Client/Assets/Images.xcassets/deviceTypeMobile.imageset/sync_mobile.png new file mode 100644 index 0000000000000000000000000000000000000000..337b049e5e63dca4156115958530c8ca5c3ca2ab GIT binary patch literal 602 zcmeAS@N?(olHy`uVBq!ia0vp^T0rc?!3HE1Q(STw7?>12T^vI)Sl>q3dLIrD$(s0)T^_*d-v=`3-%xS$G)BCkpjotMgQ2!*=6srm>;})aIO4;bxZlQrmD@8=4nZf zZn8ZhlBZDn{qFawdnYT+`78sB*2TUcxHoXE-ZgHb;B9Z27oxvUt$siiXGA zcr(M*d{21CS$G}nt37@DZCQ;{$|-UEvJ5>9nOx(U*Y^pfzVFdm)wM}9;c;f~szo`h zH!7kQuhNKSG@EyAh1RLChTi3-BuN(sFHXAnlAvYP5`MBh zaH-bzBM})hVsfUv$lX2j#9zxLnq2pfx1LOjIX7qWHvMPf zg*i`u3H2qZS0!#>@jHA(a=P*JB`>*;E8V!K6yf{1HM2ON`*_ggk7dlo@kN*SO)k@U zWMMvS`@w_@?Qh>Q`){BAjdSyy_PHMmXWenhbKk)!eiu-g)H3ub!#v3iQkuJTZOr!u_)O+$D=znYI-R Q0n-eFr>mdKI;Vst09G9rC;$Ke literal 0 HcmV?d00001 diff --git a/Client/Frontend/Home/HomePanels.swift b/Client/Frontend/Home/HomePanels.swift index 4dce5b8b96fa..d8433b4969c3 100644 --- a/Client/Frontend/Home/HomePanels.swift +++ b/Client/Frontend/Home/HomePanels.swift @@ -43,6 +43,15 @@ class HomePanels { imageName: "history", accessibilityLabel: NSLocalizedString("History", comment: "Panel accessibility label")), + HomePanelDescriptor( + makeViewController: { profile in + let controller = RemoteTabsPanel() + controller.profile = profile + return controller + }, + imageName: "tabs", + accessibilityLabel: NSLocalizedString("Synced tabs", comment: "Panel accessibility label")), + HomePanelDescriptor( makeViewController: { profile in let controller = ReadingListPanel() diff --git a/Client/Frontend/Home/RemoteTabsPanel.swift b/Client/Frontend/Home/RemoteTabsPanel.swift new file mode 100644 index 000000000000..9735016149f0 --- /dev/null +++ b/Client/Frontend/Home/RemoteTabsPanel.swift @@ -0,0 +1,130 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +import UIKit +import Snap +import Storage + +private struct RemoteTabsPanelUX { + static let HeaderHeight: CGFloat = SiteTableViewControllerUX.RowHeight // Not HeaderHeight! + static let RowHeight: CGFloat = SiteTableViewControllerUX.RowHeight +} + +private let RemoteClientIdentifier = "RemoteClient" +private let RemoteTabIdentifier = "RemoteTab" + +/** + * Display a tree hierarchy of remote clients and tabs, like: + * client + * tab + * tab + * client + * tab + * tab + * This is not a SiteTableViewController because it is inherently tree-like and not list-like; + * a technical detail is that STVC is backed by a Cursor and this is backed by a richer data + * structure. However, the styling here should agree with STVC where possible. + */ +class RemoteTabsPanel: UITableViewController, HomePanel { + weak var homePanelDelegate: HomePanelDelegate? = nil + var profile: Profile! + + private var clients: [RemoteClient]? + + private func tabAtIndexPath(indexPath: NSIndexPath) -> RemoteTab? { + return clients?[indexPath.section].tabs[indexPath.item] + } + + override func viewDidLoad() { + super.viewDidLoad() + tableView.registerClass(TwoLineHeaderFooterView.self, forHeaderFooterViewReuseIdentifier: RemoteClientIdentifier) + tableView.registerClass(TwoLineTableViewCell.self, forCellReuseIdentifier: RemoteTabIdentifier) + tableView.rowHeight = RemoteTabsPanelUX.RowHeight + tableView.separatorInset = UIEdgeInsetsZero + + refreshControl = UIRefreshControl() + refreshControl?.addTarget(self, action: "SELrefresh", forControlEvents: UIControlEvents.ValueChanged) + } + + override func viewWillAppear(animated: Bool) { + super.viewWillAppear(animated) + self.SELrefresh() + } + + @objc private func SELrefresh() { + self.refreshControl?.beginRefreshing() + profile.remoteClientsAndTabs.getClientsAndTabs { clients in + self.refreshControl?.endRefreshing() + self.clients = clients + // Maybe show a background view. + let tableView = self.tableView + if self.clients == nil || self.clients!.isEmpty { + tableView.backgroundView = UIView() + tableView.backgroundView?.frame = tableView.frame + // TODO: Populate background view with UX-approved content. + tableView.backgroundView?.backgroundColor = UIColor.redColor() + // Hide dividing lines. + tableView.separatorStyle = UITableViewCellSeparatorStyle.None + } else { + tableView.backgroundView = nil + // Show dividing lines. + tableView.separatorStyle = UITableViewCellSeparatorStyle.SingleLine + } + tableView.reloadData() + } + } + + override func numberOfSectionsInTableView(tableView: UITableView) -> Int { + return clients?.count ?? 0 + } + + override func tableView(tableView: UITableView, numberOfRowsInSection section: Int) -> Int { + return clients?[section].tabs.count ?? 0 + } + + override func tableView(tableView: UITableView, heightForHeaderInSection section: Int) -> CGFloat { + return RemoteTabsPanelUX.HeaderHeight + } + + override func tableView(tableView: UITableView, viewForHeaderInSection section: Int) -> UIView? { + if let client = clients?[section] { + let view = tableView.dequeueReusableHeaderFooterViewWithIdentifier(RemoteClientIdentifier) as TwoLineHeaderFooterView + view.frame = CGRect(x: 0, y: 0, width: tableView.frame.width, height: RemoteTabsPanelUX.HeaderHeight) + view.textLabel.text = client.name + // TODO: allow localization; convert timestamp to relative timestring. + view.detailTextLabel.text = "Last synced: \(String(client.lastModified))" + if client.type == "desktop" { + view.imageView.image = UIImage(named: "deviceTypeDesktop") + } else { + view.imageView.image = UIImage(named: "deviceTypeMobile") + } + return view + } else { + return nil + } + } + + override func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell { + let cell = tableView.dequeueReusableCellWithIdentifier(RemoteTabIdentifier, forIndexPath: indexPath) as TwoLineTableViewCell + if let tab = tabAtIndexPath(indexPath) { + // TODO: Populate image with cached favicons. + if let title = tab.title { + cell.textLabel?.text = title + cell.detailTextLabel?.text = tab.URL.absoluteString + } else { + cell.textLabel?.text = tab.URL.absoluteString + cell.detailTextLabel?.text = nil + } + } + return cell + } + + override func tableView(tableView: UITableView, didSelectRowAtIndexPath indexPath: NSIndexPath) { + tableView.deselectRowAtIndexPath(indexPath, animated: false) + + if let tab = tabAtIndexPath(indexPath) { + homePanelDelegate?.homePanel(self, didSelectURL: tab.URL) + } + } +} diff --git a/Client/Frontend/Widgets/TwoLineCell.swift b/Client/Frontend/Widgets/TwoLineCell.swift index 9d5538de8408..cb5dc7e61093 100644 --- a/Client/Frontend/Widgets/TwoLineCell.swift +++ b/Client/Frontend/Widgets/TwoLineCell.swift @@ -64,6 +64,58 @@ class TwoLineCollectionViewCell: UICollectionViewCell { } } +class TwoLineHeaderFooterView: UITableViewHeaderFooterView { + private let twoLineHelper: TwoLineCellHelper! + + // UITableViewHeaderFooterView includes textLabel and detailTextLabel, so we can't override + // them. Unfortunately, they're also used in ways that interfere with us just using them: I get + // hard crashes in layout if I just use them; it seems there's a battle over adding to the + // contentView. So we add our own members, and cover up the other ones. + let _textLabel = UILabel() + let _detailTextLabel = UILabel() + + let imageView = UIImageView() + + // Yes, this is strange. + override var textLabel: UILabel { + return _textLabel + } + + // Yes, this is strange. + override var detailTextLabel: UILabel { + return _detailTextLabel + } + + override init() { + super.init() + } + + override init(reuseIdentifier: String?) { + super.init(reuseIdentifier: reuseIdentifier) + } + + override init(frame: CGRect) { + super.init(frame: frame) + + contentView.addSubview(_textLabel) + contentView.addSubview(_detailTextLabel) + contentView.addSubview(imageView) + + twoLineHelper = TwoLineCellHelper(container: self, textLabel: _textLabel, detailTextLabel: _detailTextLabel, imageView: imageView) + + layoutMargins = UIEdgeInsetsZero + } + + required init(coder aDecoder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + override func layoutSubviews() { + super.layoutSubviews() + twoLineHelper.layoutSubviews() + } +} + private class TwoLineCellHelper { private let container: UIView let textLabel: UILabel diff --git a/Providers/Profile.swift b/Providers/Profile.swift index df61c952e400..b8e903003809 100644 --- a/Providers/Profile.swift +++ b/Providers/Profile.swift @@ -83,6 +83,7 @@ protocol Profile { var history: History { get } var favicons: Favicons { get } var readingList: ReadingList { get } + var remoteClientsAndTabs: RemoteClientsAndTabs { get } var passwords: Passwords { get } // I got really weird EXC_BAD_ACCESS errors on a non-null reference when I made this a getter. @@ -136,6 +137,10 @@ public class MockProfile: Profile { return SQLiteReadingList(files: self.files) }() + lazy var remoteClientsAndTabs: RemoteClientsAndTabs = { + return SQLiteRemoteClientsAndTabs(files: self.files) + }() + lazy var passwords: Passwords = { return MockPasswords(files: self.files) }() @@ -223,6 +228,10 @@ public class BrowserProfile: Profile { return SQLiteReadingList(files: self.files) }() + lazy var remoteClientsAndTabs: RemoteClientsAndTabs = { + return MockRemoteClientsAndTabs() + }() + lazy var passwords: Passwords = { return SQLitePasswords(files: self.files) }()