mirror of
https://repo.dactyloidae.xyz/Dactyloidae/UXP.git
synced 2026-09-21 15:57:31 +09:00
Dactyloidae iOS initial commit
This commit is contained in:
parent
daa6179d22
commit
7154a0497e
2123 changed files with 197052 additions and 0 deletions
26
mobile/ios/ReadingList/Info.plist
Normal file
26
mobile/ios/ReadingList/Info.plist
Normal file
|
|
@ -0,0 +1,26 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||
<plist version="1.0">
|
||||
<dict>
|
||||
<key>CFBundleDevelopmentRegion</key>
|
||||
<string>en</string>
|
||||
<key>CFBundleExecutable</key>
|
||||
<string>$(EXECUTABLE_NAME)</string>
|
||||
<key>CFBundleIdentifier</key>
|
||||
<string>$(PRODUCT_BUNDLE_IDENTIFIER)</string>
|
||||
<key>CFBundleInfoDictionaryVersion</key>
|
||||
<string>6.0</string>
|
||||
<key>CFBundleName</key>
|
||||
<string>$(PRODUCT_NAME)</string>
|
||||
<key>CFBundlePackageType</key>
|
||||
<string>FMWK</string>
|
||||
<key>CFBundleShortVersionString</key>
|
||||
<string>10.6</string>
|
||||
<key>CFBundleSignature</key>
|
||||
<string>????</string>
|
||||
<key>CFBundleVersion</key>
|
||||
<string>1</string>
|
||||
<key>NSPrincipalClass</key>
|
||||
<string></string>
|
||||
</dict>
|
||||
</plist>
|
||||
13
mobile/ios/ReadingList/ReadingList-Bridging-Header.h
Normal file
13
mobile/ios/ReadingList/ReadingList-Bridging-Header.h
Normal file
|
|
@ -0,0 +1,13 @@
|
|||
//
|
||||
// ReadingList-Bridging-Header.h
|
||||
// Client
|
||||
//
|
||||
// Created by Emily Toop on 1/8/16.
|
||||
// Copyright © 2016 Mozilla. All rights reserved.
|
||||
//
|
||||
|
||||
#ifndef ReadingList_Bridging_Header_h
|
||||
#define ReadingList_Bridging_Header_h
|
||||
|
||||
|
||||
#endif /* ReadingList_Bridging_Header_h */
|
||||
9
mobile/ios/ReadingList/ReadingListAuthenticator.swift
Normal file
9
mobile/ios/ReadingList/ReadingListAuthenticator.swift
Normal file
|
|
@ -0,0 +1,9 @@
|
|||
/* 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 Foundation
|
||||
|
||||
protocol ReadingListAuthenticator {
|
||||
var headers: [String: String] { get }
|
||||
}
|
||||
|
|
@ -0,0 +1,16 @@
|
|||
/* 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 Foundation
|
||||
|
||||
class ReadingListBasicAuthAuthenticator: ReadingListAuthenticator {
|
||||
var headers: [String: String]
|
||||
|
||||
init(username: String, password: String) {
|
||||
let credentials = "\(username):\(password)"
|
||||
let credentialsData = credentials.data(using: String.Encoding.utf8)!
|
||||
let encodedCredentials = credentialsData.base64EncodedString(options: NSData.Base64EncodingOptions())
|
||||
self.headers = ["Authorization": "Basic \(encodedCredentials)"]
|
||||
}
|
||||
}
|
||||
35
mobile/ios/ReadingList/ReadingListBatchRecordResponse.swift
Normal file
35
mobile/ios/ReadingList/ReadingListBatchRecordResponse.swift
Normal file
|
|
@ -0,0 +1,35 @@
|
|||
/* 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 Foundation
|
||||
|
||||
class ReadingListBatchRecordResponse: ReadingListResponse {
|
||||
var responses: [ReadingListRecordResponse] = [ReadingListRecordResponse]()
|
||||
|
||||
override init?(response: HTTPURLResponse, json: [String: Any]) {
|
||||
super.init(response: response, json: json)
|
||||
guard let responses = json["responses"] as? [[String: Any]] else {
|
||||
return nil
|
||||
}
|
||||
|
||||
for resp in responses {
|
||||
guard let body = resp["body"] as? [String: Any],
|
||||
let statusCode = resp["status"] as? Int,
|
||||
let path = resp["path"] as? String,
|
||||
let url = URL(string: path, relativeTo: self.response.url),
|
||||
let headers = resp["headers"] as? [String: String],
|
||||
let r = HTTPURLResponse(url: url, statusCode: statusCode, httpVersion: "1.1", headerFields: headers),
|
||||
let recordResponse = ReadingListRecordResponse(response: r, json: body) else {
|
||||
return nil
|
||||
}
|
||||
self.responses.append(recordResponse)
|
||||
}
|
||||
}
|
||||
|
||||
var wasSuccessful: Bool {
|
||||
get {
|
||||
return response.statusCode == 200
|
||||
}
|
||||
}
|
||||
}
|
||||
14
mobile/ios/ReadingList/ReadingListChangeAccumulator.swift
Normal file
14
mobile/ios/ReadingList/ReadingListChangeAccumulator.swift
Normal file
|
|
@ -0,0 +1,14 @@
|
|||
/* 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 Foundation
|
||||
|
||||
protocol ReadingListChangeAccumulator {
|
||||
func addDeletedClientRecord(_ deletedRecord: ReadingListClientRecord)
|
||||
func addDeletedServerRecord(_ deletedRecord: ReadingListServerRecord)
|
||||
func addChangedRecord(_ changedRecord: ReadingListClientRecord)
|
||||
func addUploadedRecord(_ uploadedRecord: ReadingListClientRecord, down: ReadingListServerRecord)
|
||||
func addDownloadedRecord(_ downloadedRecord: ReadingListServerRecord)
|
||||
func applyAccumulatedChanges()
|
||||
}
|
||||
211
mobile/ios/ReadingList/ReadingListClient.swift
Normal file
211
mobile/ios/ReadingList/ReadingListClient.swift
Normal file
|
|
@ -0,0 +1,211 @@
|
|||
/* 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 Foundation
|
||||
import Alamofire
|
||||
|
||||
enum ReadingListDeleteRecordResult {
|
||||
case success(ReadingListRecordResponse)
|
||||
case preconditionFailed(ReadingListResponse)
|
||||
case notFound(ReadingListResponse)
|
||||
case failure(ReadingListResponse)
|
||||
case error(NSError)
|
||||
}
|
||||
|
||||
enum ReadingListGetRecordResult {
|
||||
case success(ReadingListRecordResponse)
|
||||
case notModified(ReadingListResponse) // TODO Should really call this NotModified for clarity
|
||||
case notFound(ReadingListResponse)
|
||||
case failure(ReadingListResponse)
|
||||
case error(NSError)
|
||||
}
|
||||
|
||||
enum ReadingListGetAllRecordsResult {
|
||||
case success(ReadingListRecordsResponse)
|
||||
case notModified(ReadingListResponse) // TODO Should really call this NotModified for clarity
|
||||
case failure(ReadingListResponse)
|
||||
case error(NSError)
|
||||
}
|
||||
|
||||
enum ReadingListPatchRecordResult {
|
||||
}
|
||||
|
||||
enum ReadingListAddRecordResult {
|
||||
case success(ReadingListRecordResponse)
|
||||
case failure(ReadingListResponse)
|
||||
case conflict(ReadingListResponse)
|
||||
case error(NSError)
|
||||
}
|
||||
|
||||
enum ReadingListBatchAddRecordsResult {
|
||||
case success(ReadingListBatchRecordResponse)
|
||||
case failure(ReadingListResponse)
|
||||
case error(NSError)
|
||||
}
|
||||
|
||||
private let ReadingListClientUnknownError = NSError(domain: "org.mozilla.ios.Fennec.ReadingListClient", code: -1, userInfo: nil)
|
||||
|
||||
class ReadingListClient {
|
||||
var serviceURL: URL
|
||||
var authenticator: ReadingListAuthenticator
|
||||
|
||||
var articlesURL: URL!
|
||||
var articlesBaseURL: URL!
|
||||
var batchURL: URL!
|
||||
|
||||
func getRecordWithGuid(_ guid: String, ifModifiedSince: ReadingListTimestamp?, completion: @escaping (ReadingListGetRecordResult) -> Void) {
|
||||
if let url = URL(string: guid, relativeTo: articlesBaseURL) {
|
||||
SessionManager.default.request(createRequest("GET", url, ifModifiedSince: ifModifiedSince)).responseJSON(options: [], completionHandler: { response -> Void in
|
||||
if let json = response.result.value as? [String: Any], let response = response.response {
|
||||
switch response.statusCode {
|
||||
case 200:
|
||||
completion(.success(ReadingListRecordResponse(response: response, json: json)!))
|
||||
case 304:
|
||||
completion(.notModified(ReadingListResponse(response: response, json: json)!))
|
||||
case 404:
|
||||
completion(.notFound(ReadingListResponse(response: response, json: json)!))
|
||||
default:
|
||||
completion(.failure(ReadingListResponse(response: response, json: json)!))
|
||||
}
|
||||
} else {
|
||||
completion(.error(response.result.error as NSError? ?? ReadingListClientUnknownError))
|
||||
}
|
||||
})
|
||||
} else {
|
||||
// TODO ???
|
||||
}
|
||||
}
|
||||
|
||||
func getRecordWithGuid(_ guid: String, completion: @escaping (ReadingListGetRecordResult) -> Void) {
|
||||
getRecordWithGuid(guid, ifModifiedSince: nil, completion: completion)
|
||||
}
|
||||
|
||||
func getAllRecordsWithFetchSpec(_ fetchSpec: ReadingListFetchSpec, ifModifiedSince: ReadingListTimestamp?, completion: @escaping (ReadingListGetAllRecordsResult) -> Void) {
|
||||
if let url = fetchSpec.getURL(serviceURL: serviceURL, path: "/v1/articles") {
|
||||
SessionManager.default.request(createRequest("GET", url)).responseJSON(options: [], completionHandler: { response -> Void in
|
||||
if let json = response.result.value as? [String: Any], let response = response.response {
|
||||
switch response.statusCode {
|
||||
case 200:
|
||||
completion(.success(ReadingListRecordsResponse(response: response, json: json)!))
|
||||
case 304:
|
||||
completion(.notModified(ReadingListResponse(response: response, json: json)!))
|
||||
default:
|
||||
completion(.failure(ReadingListResponse(response: response, json: json)!))
|
||||
}
|
||||
} else {
|
||||
completion(.error(response.result.error as NSError? ?? ReadingListClientUnknownError))
|
||||
}
|
||||
})
|
||||
} else {
|
||||
// TODO ???
|
||||
}
|
||||
}
|
||||
|
||||
func getAllRecordsWithFetchSpec(_ fetchSpec: ReadingListFetchSpec, completion: @escaping (ReadingListGetAllRecordsResult) -> Void) {
|
||||
getAllRecordsWithFetchSpec(fetchSpec, ifModifiedSince: nil, completion: completion)
|
||||
}
|
||||
|
||||
func patchRecord(_ record: ReadingListClientRecord, completion: (ReadingListPatchRecordResult) -> Void) {
|
||||
}
|
||||
|
||||
func addRecord(_ record: ReadingListClientRecord, completion: @escaping (ReadingListAddRecordResult) -> Void) {
|
||||
SessionManager.default.request(createRequest("POST", articlesURL, json: record.json)).responseJSON(options: [], completionHandler: { response in
|
||||
if let json = response.result.value as? [String: Any], let response = response.response {
|
||||
switch response.statusCode {
|
||||
case 200, 201: // TODO Should we have different results for these? Do we care about 200 vs 201?
|
||||
completion(.success(ReadingListRecordResponse(response: response, json: json)!))
|
||||
case 303:
|
||||
completion(.conflict(ReadingListResponse(response: response, json: json)!))
|
||||
default:
|
||||
completion(.failure(ReadingListResponse(response: response, json: json)!))
|
||||
}
|
||||
} else {
|
||||
completion(.error(response.result.error as NSError? ?? ReadingListClientUnknownError))
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
/// Build the JSON body for POST /v1/batch { defaults: {}, request: [ {body: {} } ] }
|
||||
fileprivate func recordsToBatchJSON(_ records: [ReadingListClientRecord]) -> AnyObject {
|
||||
return [
|
||||
"defaults": ["method": "POST", "path": "/v1/articles", "headers": ["Content-Type": "application/json"]],
|
||||
"requests": records.map { ["body": $0.json] }
|
||||
] as NSDictionary
|
||||
}
|
||||
|
||||
func batchAddRecords(_ records: [ReadingListClientRecord], completion: @escaping (ReadingListBatchAddRecordsResult) -> Void) {
|
||||
SessionManager.default.request(createRequest("POST", batchURL, json: recordsToBatchJSON(records))).responseJSON(options: [], completionHandler: { response in
|
||||
if let json = response.result.value as? [String: Any], let response = response.response {
|
||||
switch response.statusCode {
|
||||
case 200:
|
||||
completion(.success(ReadingListBatchRecordResponse(response: response, json: json)!))
|
||||
default:
|
||||
completion(.failure(ReadingListResponse(response: response, json: json)!))
|
||||
}
|
||||
} else {
|
||||
completion(.error(response.result.error as NSError? ?? ReadingListClientUnknownError))
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func deleteRecordWithGuid(_ guid: String, ifUnmodifiedSince: ReadingListTimestamp?, completion: @escaping (ReadingListDeleteRecordResult) -> Void) {
|
||||
if let url = URL(string: guid, relativeTo: articlesBaseURL) {
|
||||
SessionManager.default.request(createRequest("DELETE", url, ifUnmodifiedSince: ifUnmodifiedSince)).responseJSON(options: [], completionHandler: { response in
|
||||
if let json = response.result.value as? [String: Any], let response = response.response {
|
||||
switch response.statusCode {
|
||||
case 200:
|
||||
completion(.success(ReadingListRecordResponse(response: response, json: json)!))
|
||||
case 412:
|
||||
completion(.preconditionFailed(ReadingListResponse(response: response, json: json)!))
|
||||
case 404:
|
||||
completion(.notFound(ReadingListResponse(response: response, json: json)!))
|
||||
default:
|
||||
completion(.failure(ReadingListResponse(response: response, json: json)!))
|
||||
}
|
||||
} else {
|
||||
completion(.error(response.result.error as NSError? ?? ReadingListClientUnknownError))
|
||||
}
|
||||
})
|
||||
} else {
|
||||
// TODO ???
|
||||
}
|
||||
}
|
||||
|
||||
func deleteRecordWithGuid(_ guid: String, completion: @escaping (ReadingListDeleteRecordResult) -> Void) {
|
||||
deleteRecordWithGuid(guid, ifUnmodifiedSince: nil, completion: completion)
|
||||
}
|
||||
|
||||
func createRequest(_ method: String, _ url: URL, ifUnmodifiedSince: ReadingListTimestamp? = nil, ifModifiedSince: ReadingListTimestamp? = nil, json: AnyObject? = nil) -> URLRequest {
|
||||
let request = NSMutableURLRequest(url: url)
|
||||
request.httpMethod = method
|
||||
if let ifUnmodifiedSince = ifUnmodifiedSince {
|
||||
request.setValue(String(ifUnmodifiedSince), forHTTPHeaderField: "If-Unmodified-Since")
|
||||
}
|
||||
if let ifModifiedSince = ifModifiedSince {
|
||||
request.setValue(String(ifModifiedSince), forHTTPHeaderField: "If-Modified-Since")
|
||||
}
|
||||
for (headerField, value) in authenticator.headers {
|
||||
request.setValue(value, forHTTPHeaderField: headerField)
|
||||
}
|
||||
request.addValue("application/json", forHTTPHeaderField: "Accept")
|
||||
if let json = json {
|
||||
request.addValue("application/json", forHTTPHeaderField: "Content-Type")
|
||||
do {
|
||||
request.httpBody = try JSONSerialization.data(withJSONObject: json, options: JSONSerialization.WritingOptions.prettyPrinted)
|
||||
} catch _ {
|
||||
request.httpBody = nil
|
||||
} // TODO Handle errors here
|
||||
}
|
||||
return request as URLRequest
|
||||
}
|
||||
|
||||
init(serviceURL: URL, authenticator: ReadingListAuthenticator) {
|
||||
self.serviceURL = serviceURL
|
||||
self.authenticator = authenticator
|
||||
|
||||
self.articlesURL = URL(string: "/v1/articles", relativeTo: self.serviceURL)
|
||||
self.articlesBaseURL = URL(string: "/v1/articles/", relativeTo: self.serviceURL)
|
||||
self.batchURL = URL(string: "/v1/batch", relativeTo: self.serviceURL)
|
||||
}
|
||||
}
|
||||
25
mobile/ios/ReadingList/ReadingListClientMetadata.swift
Normal file
25
mobile/ios/ReadingList/ReadingListClientMetadata.swift
Normal file
|
|
@ -0,0 +1,25 @@
|
|||
/* 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 Foundation
|
||||
|
||||
public struct ReadingListClientMetadata: Equatable {
|
||||
/// The id of the record in the database
|
||||
public var id: Int64
|
||||
/// A client timestamp
|
||||
public var lastModified: ReadingListTimestamp
|
||||
|
||||
public init?(row: [String: Any]) {
|
||||
guard let id = row["client_id"] as? Int64,
|
||||
let lastModified = row["client_last_modified"] as? Int64 else {
|
||||
return nil
|
||||
}
|
||||
self.id = id
|
||||
self.lastModified = lastModified
|
||||
}
|
||||
}
|
||||
|
||||
public func ==(lhs: ReadingListClientMetadata, rhs: ReadingListClientMetadata) -> Bool {
|
||||
return lhs.id == rhs.id && lhs.lastModified == rhs.lastModified
|
||||
}
|
||||
67
mobile/ios/ReadingList/ReadingListClientRecord.swift
Normal file
67
mobile/ios/ReadingList/ReadingListClientRecord.swift
Normal file
|
|
@ -0,0 +1,67 @@
|
|||
/* 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 Foundation
|
||||
|
||||
public struct ReadingListClientRecord: Equatable {
|
||||
public let clientMetadata: ReadingListClientMetadata
|
||||
public let serverMetadata: ReadingListServerMetadata?
|
||||
|
||||
public let url: String
|
||||
public let title: String
|
||||
public let addedBy: String
|
||||
public let unread: Bool
|
||||
public let archived: Bool
|
||||
public let favorite: Bool
|
||||
|
||||
/// Initializer for when a record is loaded from a database row
|
||||
public init?(row: [String: Any]) {
|
||||
guard let clientMetadata = ReadingListClientMetadata(row: row) else {
|
||||
return nil
|
||||
}
|
||||
|
||||
guard let url = row["url"] as? String,
|
||||
let title = row["title"] as? String,
|
||||
let addedBy = row["added_by"] as? String,
|
||||
let unread = row["unread"] as? Bool else {
|
||||
return nil
|
||||
}
|
||||
|
||||
self.clientMetadata = clientMetadata
|
||||
self.serverMetadata = ReadingListServerMetadata(row: row)
|
||||
|
||||
self.url = url
|
||||
self.title = title
|
||||
self.addedBy = addedBy
|
||||
|
||||
self.unread = unread
|
||||
|
||||
self.archived = row["archived"] as? Bool ?? false
|
||||
self.favorite = row["favorite"] as? Bool ?? false
|
||||
}
|
||||
|
||||
public var json: AnyObject {
|
||||
get {
|
||||
let json = NSMutableDictionary()
|
||||
json["url"] = url
|
||||
json["title"] = title
|
||||
json["added_by"] = addedBy
|
||||
json["unread"] = unread
|
||||
json["archived"] = archived
|
||||
json["favorite"] = favorite
|
||||
return json
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public func ==(lhs: ReadingListClientRecord, rhs: ReadingListClientRecord) -> Bool {
|
||||
return lhs.clientMetadata == rhs.clientMetadata
|
||||
&& lhs.serverMetadata == rhs.serverMetadata
|
||||
&& lhs.url == rhs.url
|
||||
&& lhs.title == rhs.title
|
||||
&& lhs.addedBy == rhs.addedBy
|
||||
&& lhs.unread == rhs.unread
|
||||
&& lhs.archived == rhs.archived
|
||||
&& lhs.favorite == rhs.favorite
|
||||
}
|
||||
17
mobile/ios/ReadingList/ReadingListError.swift
Normal file
17
mobile/ios/ReadingList/ReadingListError.swift
Normal file
|
|
@ -0,0 +1,17 @@
|
|||
/* 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 Foundation
|
||||
import Shared
|
||||
|
||||
// TODO This needs to encapsulate an NSError eventually
|
||||
open class ReadingListError: MaybeErrorType {
|
||||
var message: String
|
||||
init(_ message: String) {
|
||||
self.message = message
|
||||
}
|
||||
open var description: String {
|
||||
return message
|
||||
}
|
||||
}
|
||||
87
mobile/ios/ReadingList/ReadingListFetchSpec.swift
Normal file
87
mobile/ios/ReadingList/ReadingListFetchSpec.swift
Normal file
|
|
@ -0,0 +1,87 @@
|
|||
/* 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 Foundation
|
||||
|
||||
// TODO This should really use NSURLComponents to build up the query part. And not string ops.
|
||||
|
||||
class ReadingListFetchSpec {
|
||||
var queryString: String
|
||||
|
||||
init(queryString: String) {
|
||||
self.queryString = queryString
|
||||
}
|
||||
|
||||
func getURL(serviceURL: URL) -> URL? {
|
||||
if var components = URLComponents(url: serviceURL, resolvingAgainstBaseURL: true) {
|
||||
components.query = queryString
|
||||
return components.url
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func getURL(serviceURL: URL, path: String) -> URL? {
|
||||
if var components = URLComponents(url: serviceURL, resolvingAgainstBaseURL: true) {
|
||||
components.path = path
|
||||
components.query = queryString
|
||||
return components.url
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// This should really generate a dictionary of values that we can pass to NSURLComponents instead of building the query string by hand
|
||||
|
||||
class Builder {
|
||||
var buffer: String = ""
|
||||
var first = true
|
||||
|
||||
func build() -> ReadingListFetchSpec {
|
||||
return ReadingListFetchSpec(queryString: buffer)
|
||||
}
|
||||
|
||||
fileprivate func ampersand() {
|
||||
if first {
|
||||
first = false
|
||||
return
|
||||
}
|
||||
buffer += "&"
|
||||
}
|
||||
|
||||
func setUnread(_ unread: Bool) -> Builder {
|
||||
ampersand()
|
||||
buffer += "unread="
|
||||
buffer += unread ? "true" : "false"
|
||||
return self
|
||||
}
|
||||
|
||||
func setStatus(_ status: String, not: Bool) -> Builder {
|
||||
ampersand()
|
||||
if not {
|
||||
buffer += "not_"
|
||||
}
|
||||
buffer += "status="
|
||||
buffer += status
|
||||
return self
|
||||
}
|
||||
|
||||
fileprivate func qualifyAttribute(_ attribute: String, withQualifier qualifier: String, value: String) -> Builder {
|
||||
ampersand()
|
||||
buffer += qualifier
|
||||
buffer += attribute
|
||||
buffer += "="
|
||||
buffer += value
|
||||
return self
|
||||
}
|
||||
|
||||
func setMinAttribute(_ attribute: String, value: String) -> Builder {
|
||||
_ = qualifyAttribute(attribute, withQualifier: "min_", value: value)
|
||||
return self
|
||||
}
|
||||
|
||||
func setMaxAttribute(_ attribute: String, value: String) -> Builder {
|
||||
_ = qualifyAttribute(attribute, withQualifier: "max_", value: value)
|
||||
return self
|
||||
}
|
||||
}
|
||||
}
|
||||
15
mobile/ios/ReadingList/ReadingListOAuthAuthenticator.swift
Normal file
15
mobile/ios/ReadingList/ReadingListOAuthAuthenticator.swift
Normal file
|
|
@ -0,0 +1,15 @@
|
|||
/* 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 Foundation
|
||||
|
||||
class ReadingListOAuthAuthAuthenticator: ReadingListAuthenticator {
|
||||
var token: String
|
||||
var headers: [String: String]
|
||||
|
||||
init(token: String) {
|
||||
self.token = token
|
||||
self.headers = ["Authorization": "Bearer \(token)"]
|
||||
}
|
||||
}
|
||||
44
mobile/ios/ReadingList/ReadingListRecord.swift
Normal file
44
mobile/ios/ReadingList/ReadingListRecord.swift
Normal file
|
|
@ -0,0 +1,44 @@
|
|||
/* 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 Foundation
|
||||
|
||||
// TODO This is not used anymore. Decide if we need to turn this into a protocol that the ClientRecord and ServerRecord implement. Not sure if we actually need that though.
|
||||
class ReadingListRecord {
|
||||
let serverMetadata: ReadingListServerMetadata?
|
||||
|
||||
init(serverMetadata: ReadingListServerMetadata?) {
|
||||
self.serverMetadata = serverMetadata
|
||||
}
|
||||
|
||||
var guid: String? {
|
||||
get {
|
||||
return serverMetadata?.guid
|
||||
}
|
||||
}
|
||||
|
||||
var serverLastModified: Int64? {
|
||||
get {
|
||||
return serverMetadata?.lastModified
|
||||
}
|
||||
}
|
||||
|
||||
var url: String {
|
||||
get {
|
||||
fatalError("Subclass Responsibility")
|
||||
}
|
||||
}
|
||||
|
||||
var title: String {
|
||||
get {
|
||||
fatalError("Subclass Responsibility")
|
||||
}
|
||||
}
|
||||
|
||||
var addedBy: String {
|
||||
get {
|
||||
fatalError("Subclass Responsibility")
|
||||
}
|
||||
}
|
||||
}
|
||||
28
mobile/ios/ReadingList/ReadingListRecordResponse.swift
Normal file
28
mobile/ios/ReadingList/ReadingListRecordResponse.swift
Normal file
|
|
@ -0,0 +1,28 @@
|
|||
/* 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 Foundation
|
||||
|
||||
class ReadingListRecordResponse: ReadingListResponse {
|
||||
override init?(response: HTTPURLResponse, json: [String: Any]) {
|
||||
super.init(response: response, json: json)
|
||||
}
|
||||
|
||||
var wasSuccessful: Bool {
|
||||
get {
|
||||
return response.statusCode == 200 || response.statusCode == 201 || response.statusCode == 204
|
||||
// TODO On Android we call super.wasSuccessful() .. is there another value that we consider a success?
|
||||
}
|
||||
}
|
||||
|
||||
var record: ReadingListServerRecord? {
|
||||
get {
|
||||
if let json = self.json {
|
||||
return ReadingListServerRecord(json: json)
|
||||
} else {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
36
mobile/ios/ReadingList/ReadingListRecordsResponse.swift
Normal file
36
mobile/ios/ReadingList/ReadingListRecordsResponse.swift
Normal file
|
|
@ -0,0 +1,36 @@
|
|||
/* 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 Foundation
|
||||
|
||||
class ReadingListRecordsResponse: ReadingListResponse {
|
||||
override init?(response: HTTPURLResponse, json: [String: Any]) {
|
||||
super.init(response: response, json: json)
|
||||
}
|
||||
|
||||
var wasSuccessful: Bool {
|
||||
get {
|
||||
return response.statusCode == 200 || response.statusCode == 201 || response.statusCode == 204
|
||||
// TODO On Android we call super.wasSuccessful() .. is there another value that we consider a success?
|
||||
}
|
||||
}
|
||||
|
||||
var records: [ReadingListServerRecord]? {
|
||||
get {
|
||||
if let json = self.json {
|
||||
var records = [ReadingListServerRecord]()
|
||||
if let items = json["items"] as? [[String: Any]] {
|
||||
for item in items {
|
||||
if let record = ReadingListServerRecord(json: item) {
|
||||
records.append(record)
|
||||
}
|
||||
}
|
||||
}
|
||||
return records
|
||||
} else {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
28
mobile/ios/ReadingList/ReadingListResponse.swift
Normal file
28
mobile/ios/ReadingList/ReadingListResponse.swift
Normal file
|
|
@ -0,0 +1,28 @@
|
|||
/* 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 Foundation
|
||||
|
||||
/// Wrapper around NSURLResponse and the response json object to easily pass this to delegates. Also contains some higher level functions to access service specific headers.
|
||||
///
|
||||
/// TODO: In the Android code this is a subclass of MozResponse - Which has a bunch of other useful shortcuts. Maybe we should do that too? Or for the sake of simplicity, move some of those functions (which ones do we need?) into this class
|
||||
class ReadingListResponse {
|
||||
var response: HTTPURLResponse
|
||||
var json: [String: Any]?
|
||||
|
||||
init?(response: HTTPURLResponse, json: [String: Any]) {
|
||||
self.response = response
|
||||
self.json = json
|
||||
}
|
||||
|
||||
var lastModified: Int64? {
|
||||
get {
|
||||
if let lastModified = response.allHeaderFields["Last-Modified"] as? String {
|
||||
return Int64(lastModified)
|
||||
} else {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
180
mobile/ios/ReadingList/ReadingListSQLStorage.swift
Normal file
180
mobile/ios/ReadingList/ReadingListSQLStorage.swift
Normal file
|
|
@ -0,0 +1,180 @@
|
|||
/* 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 Foundation
|
||||
|
||||
import SQLite
|
||||
import Shared
|
||||
|
||||
class ReadingListSQLStorage: ReadingListStorage {
|
||||
var db: Connection!
|
||||
let items: Table!
|
||||
struct ItemColumns {
|
||||
// Client Metadata
|
||||
static let ClientId = Expression<Int64>("client_id")
|
||||
static let ClientLastModified = Expression<Int64>("client_last_modified")
|
||||
// Server Metadata
|
||||
static let Id = Expression<String?>("id")
|
||||
static let LastModified = Expression<Int64?>("last_modified")
|
||||
// Properties
|
||||
static let Url = Expression<String>("url")
|
||||
static let Title = Expression<String>("title")
|
||||
static let AddedBy = Expression<String>("added_by")
|
||||
static let Archived = Expression<Bool>("archived")
|
||||
static let Favorite = Expression<Bool>("favorite")
|
||||
static let Unread = Expression<Bool>("unread")
|
||||
}
|
||||
|
||||
init(path: String) {
|
||||
db = try! Connection(path)
|
||||
|
||||
items = Table("items")
|
||||
do {
|
||||
try db.run(items.create(temporary: false, ifNotExists: true, block: { (t: SQLite.TableBuilder) in
|
||||
// Client Metadata
|
||||
t.column(ItemColumns.ClientId, primaryKey: .autoincrement)
|
||||
t.column(ItemColumns.ClientLastModified)
|
||||
// Server Metadata
|
||||
t.column(ItemColumns.Id) // TODO Unique but may be null?
|
||||
t.column(ItemColumns.LastModified)
|
||||
// Properties
|
||||
t.column(ItemColumns.Url, unique: true)
|
||||
t.column(ItemColumns.Title)
|
||||
t.column(ItemColumns.AddedBy)
|
||||
t.column(ItemColumns.Archived, defaultValue: false)
|
||||
t.column(ItemColumns.Favorite, defaultValue: false)
|
||||
t.column(ItemColumns.Unread, defaultValue: true)
|
||||
}))
|
||||
} catch {
|
||||
print("Unable to create items database '\(error)")
|
||||
}
|
||||
}
|
||||
|
||||
func getAllRecords() -> Maybe<[ReadingListClientRecord]> {
|
||||
do {
|
||||
let preparedItems = try db.prepare(items)
|
||||
return Maybe(success: Array(preparedItems).map {ReadingListClientRecord(row: self.rowToDictionary($0))!})
|
||||
} catch {
|
||||
return Maybe(failure: ReadingListStorageError("Can't fetch all records: \(error)"))
|
||||
}
|
||||
}
|
||||
|
||||
func getNewRecords() -> Maybe<[ReadingListClientRecord]> {
|
||||
do {
|
||||
let preparedItems = try db.prepare(items.filter(ItemColumns.Id == nil))
|
||||
return Maybe(success: Array(preparedItems).map {ReadingListClientRecord(row: self.rowToDictionary($0))!})
|
||||
} catch {
|
||||
return Maybe(failure: ReadingListStorageError("Can't fetch all records: \(error)"))
|
||||
}
|
||||
}
|
||||
|
||||
func getUnreadRecords() -> Maybe<[ReadingListClientRecord]> {
|
||||
do {
|
||||
let preparedItems = try db.prepare(items.filter(ItemColumns.Unread == true))
|
||||
return Maybe(success: Array(preparedItems).map {ReadingListClientRecord(row: self.rowToDictionary($0))!})
|
||||
} catch {
|
||||
return Maybe(failure: ReadingListStorageError("Can't fetch all records: \(error)"))
|
||||
}
|
||||
}
|
||||
|
||||
func getAvailableRecords() -> Maybe<[ReadingListClientRecord]> {
|
||||
do {
|
||||
let preparedItems = try db.prepare(items.order(ItemColumns.ClientLastModified.desc))
|
||||
return Maybe(success: Array(preparedItems).map {ReadingListClientRecord(row: self.rowToDictionary($0))!})
|
||||
} catch {
|
||||
return Maybe(failure: ReadingListStorageError("Can't fetch all records: \(error)"))
|
||||
}
|
||||
}
|
||||
|
||||
func deleteRecord(_ record: ReadingListClientRecord) -> Maybe<Void> {
|
||||
print("Trying to delete record with id \(record.clientMetadata.id)\n")
|
||||
let query = items.filter(ItemColumns.ClientId == record.clientMetadata.id)
|
||||
do {
|
||||
try db.run(query.delete())
|
||||
return Maybe(success: Void())
|
||||
} catch {
|
||||
return Maybe(failure: ReadingListStorageError("Failed to delete"))
|
||||
}
|
||||
}
|
||||
|
||||
func deleteAllRecords() -> Maybe<Void> {
|
||||
do {
|
||||
try db.run(self.items.delete())
|
||||
return Maybe(success: Void())
|
||||
} catch {
|
||||
return Maybe(failure: ReadingListStorageError("Failed to delete"))
|
||||
}
|
||||
}
|
||||
|
||||
func createRecordWithURL(_ url: String, title: String, addedBy: String) -> Maybe<ReadingListClientRecord> {
|
||||
let insert = items.insert(ItemColumns.ClientLastModified <- ReadingListNow(), ItemColumns.Url <- url, ItemColumns.Title <- title, ItemColumns.AddedBy <- addedBy)
|
||||
|
||||
do {
|
||||
let id = try db.run(insert)
|
||||
let preparedItems = try db.prepare(items.filter(ItemColumns.ClientId == id))
|
||||
if let item = Array(preparedItems).first {
|
||||
if let record = ReadingListClientRecord(row: rowToDictionary(item)) {
|
||||
return Maybe(success: record)
|
||||
} else {
|
||||
return Maybe(failure: ReadingListStorageError("Can't create RLCR from row"))
|
||||
}
|
||||
} else {
|
||||
return Maybe(failure: ReadingListStorageError("Can't get first item from results"))
|
||||
}
|
||||
} catch {
|
||||
return Maybe(failure: ReadingListStorageError("Can't insert: \(error)"))
|
||||
}
|
||||
}
|
||||
|
||||
func getRecordWithURL(_ url: String) -> Maybe<ReadingListClientRecord?> {
|
||||
do {
|
||||
let preparedItems = try db.prepare(items.filter(ItemColumns.Url == url))
|
||||
if let item = Array(preparedItems).first {
|
||||
if let record = ReadingListClientRecord(row: rowToDictionary(item)) {
|
||||
return Maybe(success: record)
|
||||
} else {
|
||||
return Maybe(failure: ReadingListStorageError("Can't create RLCR from row"))
|
||||
}
|
||||
} else {
|
||||
return Maybe(success: nil)
|
||||
}
|
||||
} catch {
|
||||
return Maybe(failure: ReadingListStorageError("Can't fetch: \(error)"))
|
||||
}
|
||||
}
|
||||
|
||||
func updateRecord(_ record: ReadingListClientRecord, unread: Bool) -> Maybe<ReadingListClientRecord?> {
|
||||
let query = items.filter(ItemColumns.ClientId == record.clientMetadata.id).update(ItemColumns.Unread <- unread)
|
||||
do {
|
||||
try db.run(query)
|
||||
let preparedItems = try db.prepare(items.filter(ItemColumns.ClientId == record.clientMetadata.id))
|
||||
if let item = Array(preparedItems).first {
|
||||
if let record = ReadingListClientRecord(row: rowToDictionary(item)) {
|
||||
return Maybe(success: record)
|
||||
} else {
|
||||
return Maybe(failure: ReadingListStorageError("Can't create RLCR from row"))
|
||||
}
|
||||
} else {
|
||||
return Maybe(success: nil)
|
||||
}
|
||||
} catch {
|
||||
return Maybe(success: nil)
|
||||
}
|
||||
}
|
||||
|
||||
fileprivate func rowToDictionary(_ row: Row) -> [String: Any] {
|
||||
var result: [String: Any] = [:]
|
||||
result["client_id"] = NSNumber(value: row.get(ItemColumns.ClientId))
|
||||
result["client_last_modified"] = NSNumber(value: row.get(ItemColumns.ClientLastModified))
|
||||
result["id"] = row.get(ItemColumns.Id)
|
||||
result["last_modified"] = NSNumber(value: row.get(ItemColumns.LastModified) ?? 0)
|
||||
result["url"] = row.get(ItemColumns.Url)
|
||||
result["title"] = row.get(ItemColumns.Title)
|
||||
result["added_by"] = row.get(ItemColumns.AddedBy)
|
||||
result["archived"] = row.get(ItemColumns.Archived)
|
||||
result["favorite"] = row.get(ItemColumns.Favorite)
|
||||
result["unread"] = row.get(ItemColumns.Unread)
|
||||
return result
|
||||
}
|
||||
}
|
||||
37
mobile/ios/ReadingList/ReadingListServerMetadata.swift
Normal file
37
mobile/ios/ReadingList/ReadingListServerMetadata.swift
Normal file
|
|
@ -0,0 +1,37 @@
|
|||
/* 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 Foundation
|
||||
|
||||
public struct ReadingListServerMetadata: Equatable {
|
||||
public var guid: String
|
||||
public var lastModified: ReadingListTimestamp
|
||||
|
||||
init(guid: String, lastModified: ReadingListTimestamp) {
|
||||
self.guid = guid
|
||||
self.lastModified = lastModified
|
||||
}
|
||||
|
||||
/// Initialize from server record.
|
||||
init?(json: [String: Any]) {
|
||||
self.init(data: json)
|
||||
}
|
||||
|
||||
init?(row: [String: Any]) {
|
||||
self.init(data: row)
|
||||
}
|
||||
|
||||
private init?(data: [String: Any]) {
|
||||
guard let guid = data["id"] as? String,
|
||||
let lastModified = data["last_modified"] as? Int64 else {
|
||||
return nil
|
||||
}
|
||||
self.guid = guid
|
||||
self.lastModified = lastModified
|
||||
}
|
||||
}
|
||||
|
||||
public func ==(lhs: ReadingListServerMetadata, rhs: ReadingListServerMetadata) -> Bool {
|
||||
return lhs.guid == rhs.guid && lhs.lastModified == rhs.lastModified
|
||||
}
|
||||
41
mobile/ios/ReadingList/ReadingListServerRecord.swift
Normal file
41
mobile/ios/ReadingList/ReadingListServerRecord.swift
Normal file
|
|
@ -0,0 +1,41 @@
|
|||
/* 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 Foundation
|
||||
|
||||
struct ReadingListServerRecord {
|
||||
let serverMetadata: ReadingListServerMetadata?
|
||||
|
||||
let url: String
|
||||
let title: String
|
||||
let addedBy: String
|
||||
let unread: Bool
|
||||
let archived: Bool
|
||||
let favorite: Bool
|
||||
|
||||
/// Initializer for when a record is loaded from server-sent json
|
||||
init?(json: [String: Any]?) {
|
||||
guard let json = json,
|
||||
let serverMetadata = ReadingListServerMetadata(json: json),
|
||||
let url = json["url"] as? String,
|
||||
let title = json["title"] as? String,
|
||||
let addedBy = json["added_by"] as? String,
|
||||
let unread = json["unread"] as? Bool,
|
||||
let archived = json["archived"] as? Bool,
|
||||
let favorite = json["favorite"] as? Bool else {
|
||||
return nil
|
||||
}
|
||||
|
||||
self.serverMetadata = serverMetadata
|
||||
|
||||
self.url = url
|
||||
self.title = title
|
||||
self.addedBy = addedBy
|
||||
|
||||
self.unread = unread
|
||||
self.archived = archived
|
||||
self.favorite = favorite
|
||||
}
|
||||
|
||||
}
|
||||
42
mobile/ios/ReadingList/ReadingListService.swift
Normal file
42
mobile/ios/ReadingList/ReadingListService.swift
Normal file
|
|
@ -0,0 +1,42 @@
|
|||
/* 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 Foundation
|
||||
import Shared
|
||||
|
||||
/// This is the public API that the application and extension talk to. It exposes the bare minimum
|
||||
/// functions that need to be public and hides details like storage and syncing.
|
||||
open class ReadingListService {
|
||||
var databasePath: String
|
||||
var storage: ReadingListStorage
|
||||
|
||||
public init?(profileStoragePath: String) {
|
||||
databasePath = (profileStoragePath as NSString).appendingPathComponent("ReadingList.db")
|
||||
storage = ReadingListSQLStorage(path: "\(profileStoragePath)/ReadingList.db")
|
||||
}
|
||||
|
||||
open func getAvailableRecords() -> Maybe<[ReadingListClientRecord]> {
|
||||
return storage.getAvailableRecords()
|
||||
}
|
||||
|
||||
@discardableResult open func deleteRecord(_ record: ReadingListClientRecord) -> Maybe<Void> {
|
||||
return storage.deleteRecord(record)
|
||||
}
|
||||
|
||||
open func deleteAllRecords() -> Maybe<Void> {
|
||||
return storage.deleteAllRecords()
|
||||
}
|
||||
|
||||
@discardableResult open func createRecordWithURL(_ url: String, title: String, addedBy: String) -> Maybe<ReadingListClientRecord> {
|
||||
return storage.createRecordWithURL(url, title: title, addedBy: addedBy)
|
||||
}
|
||||
|
||||
open func getRecordWithURL(_ url: String) -> Maybe<ReadingListClientRecord?> {
|
||||
return storage.getRecordWithURL(url)
|
||||
}
|
||||
|
||||
@discardableResult open func updateRecord(_ record: ReadingListClientRecord, unread: Bool) -> Maybe<ReadingListClientRecord?> {
|
||||
return storage.updateRecord(record, unread: unread)
|
||||
}
|
||||
}
|
||||
31
mobile/ios/ReadingList/ReadingListStorage.swift
Normal file
31
mobile/ios/ReadingList/ReadingListStorage.swift
Normal file
|
|
@ -0,0 +1,31 @@
|
|||
/* 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 Foundation
|
||||
import Shared
|
||||
|
||||
class ReadingListStorageError: MaybeErrorType {
|
||||
var message: String
|
||||
init(_ message: String) {
|
||||
self.message = message
|
||||
}
|
||||
var description: String {
|
||||
return message
|
||||
}
|
||||
}
|
||||
|
||||
/// Storage protocol. The only thing the client (application) communicates with is the storage. Adding, removing and updating items.
|
||||
protocol ReadingListStorage {
|
||||
func getAllRecords() -> Maybe<[ReadingListClientRecord]>
|
||||
func getNewRecords() -> Maybe<[ReadingListClientRecord]>
|
||||
|
||||
// These are the used by the application
|
||||
func getUnreadRecords() -> Maybe<[ReadingListClientRecord]>
|
||||
func getAvailableRecords() -> Maybe<[ReadingListClientRecord]>
|
||||
func deleteRecord(_ record: ReadingListClientRecord) -> Maybe<Void>
|
||||
func deleteAllRecords() -> Maybe<Void>
|
||||
func createRecordWithURL(_ url: String, title: String, addedBy: String) -> Maybe<ReadingListClientRecord>
|
||||
func getRecordWithURL(_ url: String) -> Maybe<ReadingListClientRecord?>
|
||||
func updateRecord(_ record: ReadingListClientRecord, unread: Bool) -> Maybe<ReadingListClientRecord?>
|
||||
}
|
||||
25
mobile/ios/ReadingList/ReadingListSyncMetadata.swift
Normal file
25
mobile/ios/ReadingList/ReadingListSyncMetadata.swift
Normal file
|
|
@ -0,0 +1,25 @@
|
|||
/* 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 Foundation
|
||||
|
||||
enum ReadingListSyncChanges {
|
||||
|
||||
}
|
||||
|
||||
enum ReadingListSyncStatus {
|
||||
case synced
|
||||
case new
|
||||
case deleted
|
||||
case modified
|
||||
}
|
||||
|
||||
enum ReadingListSyncChange {
|
||||
case none, unread, favorite, resolved
|
||||
}
|
||||
|
||||
struct ReadingListSyncMetadata {
|
||||
var changes: ReadingListSyncChanges
|
||||
var status: ReadingListSyncStatus
|
||||
}
|
||||
69
mobile/ios/ReadingList/ReadingListSynchronizer.swift
Normal file
69
mobile/ios/ReadingList/ReadingListSynchronizer.swift
Normal file
|
|
@ -0,0 +1,69 @@
|
|||
/* 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 Foundation
|
||||
|
||||
enum ReadingListSynchronizerResult {
|
||||
case success
|
||||
case failure
|
||||
case error(NSError)
|
||||
}
|
||||
|
||||
enum ReadingListSyncType {
|
||||
case uploadOnly
|
||||
case full
|
||||
}
|
||||
|
||||
class ReadingListSynchronizer {
|
||||
var storage: ReadingListStorage
|
||||
var client: ReadingListClient
|
||||
|
||||
init(storage: ReadingListStorage, client: ReadingListClient) {
|
||||
self.storage = storage
|
||||
self.client = client
|
||||
}
|
||||
|
||||
func synchronize(type: ReadingListSyncType, completion: (ReadingListSynchronizerResult) -> Void) {
|
||||
// TODO Check if we are already syncing - Keep state somewhere
|
||||
// TODO If this is a first time sync then we want to remember the account and server in storage, so that:
|
||||
// TODO Check if the client is configured to use a different account or server
|
||||
switch type {
|
||||
case .uploadOnly:
|
||||
let synchronizer = ReadingListUploadOnlySynchronizer(storage: storage, client: client)
|
||||
synchronizer.synchronizeWithCompletion(completion)
|
||||
case .full:
|
||||
let synchronizer = ReadingListFullSynchronizer(storage: storage, client: client)
|
||||
synchronizer.synchronizeWithCompletion(completion)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// This is implemented in two different classes to make the design simpler. There will be some duplicate
|
||||
// code but I prefer that instead of having two implementations in one class.
|
||||
|
||||
private class ReadingListUploadOnlySynchronizer {
|
||||
var storage: ReadingListStorage
|
||||
var client: ReadingListClient
|
||||
|
||||
init(storage: ReadingListStorage, client: ReadingListClient) {
|
||||
self.storage = storage
|
||||
self.client = client
|
||||
}
|
||||
|
||||
func synchronizeWithCompletion(_ completion: (ReadingListSynchronizerResult) -> Void) {
|
||||
}
|
||||
}
|
||||
|
||||
private class ReadingListFullSynchronizer {
|
||||
var storage: ReadingListStorage
|
||||
var client: ReadingListClient
|
||||
|
||||
init(storage: ReadingListStorage, client: ReadingListClient) {
|
||||
self.storage = storage
|
||||
self.client = client
|
||||
}
|
||||
|
||||
func synchronizeWithCompletion(_ completion: (ReadingListSynchronizerResult) -> Void) {
|
||||
}
|
||||
}
|
||||
16
mobile/ios/ReadingList/ReadingListUtils.swift
Normal file
16
mobile/ios/ReadingList/ReadingListUtils.swift
Normal file
|
|
@ -0,0 +1,16 @@
|
|||
/* 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 Foundation
|
||||
|
||||
public typealias ReadingListRecordId = Int64
|
||||
public typealias ReadingListTimestamp = Int64
|
||||
|
||||
func ReadingListNow() -> ReadingListTimestamp {
|
||||
return ReadingListTimestamp(Date.timeIntervalSinceReferenceDate * 1000.0)
|
||||
}
|
||||
|
||||
let ReadingListDefaultUnread: Bool = true
|
||||
let ReadingListDefaultFavorite: Bool = false
|
||||
let ReadingListDefaultArchived: Bool = false
|
||||
Loading…
Add table
Add a link
Reference in a new issue