Move common code to swift package
This commit is contained in:
parent
8547950d80
commit
2b0ab45c12
|
@ -8,20 +8,14 @@
|
|||
|
||||
/* Begin PBXBuildFile section */
|
||||
D6451241276981A40046CCD2 /* WindowController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6451240276981A40046CCD2 /* WindowController.swift */; };
|
||||
D6451243276A408F0046CCD2 /* LocalData.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6451242276A408F0046CCD2 /* LocalData.swift */; };
|
||||
D6559A5228721BAF000EEB4D /* MastoSearchCore in Frameworks */ = {isa = PBXBuildFile; productRef = D6559A5128721BAF000EEB4D /* MastoSearchCore */; };
|
||||
D669039E2769236F00819C4D /* ViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D669039C2769236F00819C4D /* ViewController.swift */; };
|
||||
D66903BE2769250B00819C4D /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = D66903BD2769250B00819C4D /* Main.storyboard */; };
|
||||
D66903C127692EAB00819C4D /* SwiftSoup in Frameworks */ = {isa = PBXBuildFile; productRef = D66903C027692EAB00819C4D /* SwiftSoup */; };
|
||||
D6A4B8A827C1BC5A0016F458 /* APIController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6A4B8A727C1BC5A0016F458 /* APIController.swift */; };
|
||||
D6A4B8B027C2B1770016F458 /* MastoSearchTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6A4B8AF27C2B1770016F458 /* MastoSearchTests.swift */; };
|
||||
D6A4B8B727C2B18C0016F458 /* ImportControllerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6A4B8B627C2B18C0016F458 /* ImportControllerTests.swift */; };
|
||||
D6A4B8BA27C2BE330016F458 /* UInt128.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6A4B8B927C2BE330016F458 /* UInt128.swift */; };
|
||||
D6B24DE927640CE100BA23B8 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6B24DE827640CE100BA23B8 /* AppDelegate.swift */; };
|
||||
D6B24DEB27640CE200BA23B8 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = D6B24DEA27640CE200BA23B8 /* Assets.xcassets */; };
|
||||
D6B24DF727640D2600BA23B8 /* FMDB in Frameworks */ = {isa = PBXBuildFile; productRef = D6B24DF627640D2600BA23B8 /* FMDB */; };
|
||||
D6B24DF927640DD700BA23B8 /* DatabaseController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6B24DF827640DD700BA23B8 /* DatabaseController.swift */; };
|
||||
D6D9CFE82764196E006FE2E7 /* ImportController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6D9CFE72764196E006FE2E7 /* ImportController.swift */; };
|
||||
D6D9CFEA27641D4A006FE2E7 /* Status.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6D9CFE927641D4A006FE2E7 /* Status.swift */; };
|
||||
/* End PBXBuildFile section */
|
||||
|
||||
/* Begin PBXContainerItemProxy section */
|
||||
|
@ -36,21 +30,16 @@
|
|||
|
||||
/* Begin PBXFileReference section */
|
||||
D6451240276981A40046CCD2 /* WindowController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WindowController.swift; sourceTree = "<group>"; };
|
||||
D6451242276A408F0046CCD2 /* LocalData.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LocalData.swift; sourceTree = "<group>"; };
|
||||
D669039C2769236F00819C4D /* ViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ViewController.swift; sourceTree = "<group>"; };
|
||||
D66903BD2769250B00819C4D /* Main.storyboard */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = file.storyboard; path = Main.storyboard; sourceTree = "<group>"; };
|
||||
D6A4B8A727C1BC5A0016F458 /* APIController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = APIController.swift; sourceTree = "<group>"; };
|
||||
D6A4B8AD27C2B1770016F458 /* MastoSearchTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = MastoSearchTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; };
|
||||
D6A4B8AF27C2B1770016F458 /* MastoSearchTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MastoSearchTests.swift; sourceTree = "<group>"; };
|
||||
D6A4B8B627C2B18C0016F458 /* ImportControllerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ImportControllerTests.swift; sourceTree = "<group>"; };
|
||||
D6A4B8B927C2BE330016F458 /* UInt128.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UInt128.swift; sourceTree = "<group>"; };
|
||||
D6B24DE527640CE100BA23B8 /* MastoSearch.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = MastoSearch.app; sourceTree = BUILT_PRODUCTS_DIR; };
|
||||
D6B24DE827640CE100BA23B8 /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = "<group>"; };
|
||||
D6B24DEA27640CE200BA23B8 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = "<group>"; };
|
||||
D6B24DEF27640CE200BA23B8 /* MastoSearch.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = MastoSearch.entitlements; sourceTree = "<group>"; };
|
||||
D6B24DF827640DD700BA23B8 /* DatabaseController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DatabaseController.swift; sourceTree = "<group>"; };
|
||||
D6D9CFE72764196E006FE2E7 /* ImportController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ImportController.swift; sourceTree = "<group>"; };
|
||||
D6D9CFE927641D4A006FE2E7 /* Status.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Status.swift; sourceTree = "<group>"; };
|
||||
D6E77D1428721A7600D8B732 /* MastoSearchCore */ = {isa = PBXFileReference; lastKnownFileType = wrapper; path = MastoSearchCore; sourceTree = "<group>"; };
|
||||
/* End PBXFileReference section */
|
||||
|
||||
/* Begin PBXFrameworksBuildPhase section */
|
||||
|
@ -65,14 +54,21 @@
|
|||
isa = PBXFrameworksBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
D6B24DF727640D2600BA23B8 /* FMDB in Frameworks */,
|
||||
D66903C127692EAB00819C4D /* SwiftSoup in Frameworks */,
|
||||
D6559A5228721BAF000EEB4D /* MastoSearchCore in Frameworks */,
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
};
|
||||
/* End PBXFrameworksBuildPhase section */
|
||||
|
||||
/* Begin PBXGroup section */
|
||||
D6559A5028721BAF000EEB4D /* Frameworks */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
);
|
||||
name = Frameworks;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
D6A4B8AE27C2B1770016F458 /* MastoSearchTests */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
|
@ -82,20 +78,14 @@
|
|||
path = MastoSearchTests;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
D6A4B8B827C2BE250016F458 /* Vendor */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
D6A4B8B927C2BE330016F458 /* UInt128.swift */,
|
||||
);
|
||||
path = Vendor;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
D6B24DDC27640CE100BA23B8 = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
D6E77D1428721A7600D8B732 /* MastoSearchCore */,
|
||||
D6B24DE727640CE100BA23B8 /* MastoSearch */,
|
||||
D6A4B8AE27C2B1770016F458 /* MastoSearchTests */,
|
||||
D6B24DE627640CE100BA23B8 /* Products */,
|
||||
D6559A5028721BAF000EEB4D /* Frameworks */,
|
||||
);
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
|
@ -112,14 +102,8 @@
|
|||
isa = PBXGroup;
|
||||
children = (
|
||||
D6B24DE827640CE100BA23B8 /* AppDelegate.swift */,
|
||||
D6451242276A408F0046CCD2 /* LocalData.swift */,
|
||||
D6B24DF827640DD700BA23B8 /* DatabaseController.swift */,
|
||||
D6D9CFE72764196E006FE2E7 /* ImportController.swift */,
|
||||
D6A4B8A727C1BC5A0016F458 /* APIController.swift */,
|
||||
D6D9CFE927641D4A006FE2E7 /* Status.swift */,
|
||||
D6451240276981A40046CCD2 /* WindowController.swift */,
|
||||
D669039C2769236F00819C4D /* ViewController.swift */,
|
||||
D6A4B8B827C2BE250016F458 /* Vendor */,
|
||||
D6B24DEA27640CE200BA23B8 /* Assets.xcassets */,
|
||||
D66903BD2769250B00819C4D /* Main.storyboard */,
|
||||
D6B24DEF27640CE200BA23B8 /* MastoSearch.entitlements */,
|
||||
|
@ -162,8 +146,8 @@
|
|||
);
|
||||
name = MastoSearch;
|
||||
packageProductDependencies = (
|
||||
D6B24DF627640D2600BA23B8 /* FMDB */,
|
||||
D66903C027692EAB00819C4D /* SwiftSoup */,
|
||||
D6559A5128721BAF000EEB4D /* MastoSearchCore */,
|
||||
);
|
||||
productName = MastoSearch;
|
||||
productReference = D6B24DE527640CE100BA23B8 /* MastoSearch.app */;
|
||||
|
@ -246,13 +230,7 @@
|
|||
files = (
|
||||
D669039E2769236F00819C4D /* ViewController.swift in Sources */,
|
||||
D6451241276981A40046CCD2 /* WindowController.swift in Sources */,
|
||||
D6B24DF927640DD700BA23B8 /* DatabaseController.swift in Sources */,
|
||||
D6B24DE927640CE100BA23B8 /* AppDelegate.swift in Sources */,
|
||||
D6A4B8A827C1BC5A0016F458 /* APIController.swift in Sources */,
|
||||
D6D9CFE82764196E006FE2E7 /* ImportController.swift in Sources */,
|
||||
D6A4B8BA27C2BE330016F458 /* UInt128.swift in Sources */,
|
||||
D6D9CFEA27641D4A006FE2E7 /* Status.swift in Sources */,
|
||||
D6451243276A408F0046CCD2 /* LocalData.swift in Sources */,
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
};
|
||||
|
@ -524,16 +502,15 @@
|
|||
/* End XCRemoteSwiftPackageReference section */
|
||||
|
||||
/* Begin XCSwiftPackageProductDependency section */
|
||||
D6559A5128721BAF000EEB4D /* MastoSearchCore */ = {
|
||||
isa = XCSwiftPackageProductDependency;
|
||||
productName = MastoSearchCore;
|
||||
};
|
||||
D66903C027692EAB00819C4D /* SwiftSoup */ = {
|
||||
isa = XCSwiftPackageProductDependency;
|
||||
package = D66903BF27692EAB00819C4D /* XCRemoteSwiftPackageReference "SwiftSoup" */;
|
||||
productName = SwiftSoup;
|
||||
};
|
||||
D6B24DF627640D2600BA23B8 /* FMDB */ = {
|
||||
isa = XCSwiftPackageProductDependency;
|
||||
package = D6B24DF527640D2600BA23B8 /* XCRemoteSwiftPackageReference "fmdb" */;
|
||||
productName = FMDB;
|
||||
};
|
||||
/* End XCSwiftPackageProductDependency section */
|
||||
};
|
||||
rootObject = D6B24DDD27640CE100BA23B8 /* Project object */;
|
||||
|
|
|
@ -10,18 +10,13 @@ import UniformTypeIdentifiers
|
|||
import AuthenticationServices
|
||||
import Combine
|
||||
import OSLog
|
||||
import MastoSearchCore
|
||||
|
||||
@main
|
||||
class AppDelegate: NSObject, NSApplicationDelegate {
|
||||
|
||||
@IBOutlet weak var accountMenu: NSMenu!
|
||||
|
||||
let onSync = PassthroughSubject<Void, Never>()
|
||||
|
||||
private let syncLogger = Logger(subsystem: Bundle.main.bundleIdentifier!, category: "Sync")
|
||||
private var authSession: ASWebAuthenticationSession?
|
||||
private var syncTotal = 0
|
||||
|
||||
func applicationWillFinishLaunching(_ notification: Notification) {
|
||||
DatabaseController.shared.initialize()
|
||||
|
||||
|
@ -29,7 +24,7 @@ class AppDelegate: NSObject, NSApplicationDelegate {
|
|||
}
|
||||
|
||||
func applicationDidFinishLaunching(_ aNotification: Notification) {
|
||||
syncStatuses()
|
||||
SyncController.shared.syncStatuses(errorHandler: self.handleSyncError(_:))
|
||||
}
|
||||
|
||||
func applicationWillTerminate(_ aNotification: Notification) {
|
||||
|
@ -51,55 +46,6 @@ class AppDelegate: NSObject, NSApplicationDelegate {
|
|||
}
|
||||
}
|
||||
|
||||
private func syncStatuses() {
|
||||
DatabaseController.shared.getNewestStatus { status in
|
||||
guard let status = status else {
|
||||
return
|
||||
}
|
||||
|
||||
self.syncLogger.log("Starting sync...")
|
||||
self.syncTotal = 0
|
||||
self.syncStatuses(range: .after(status.id))
|
||||
}
|
||||
}
|
||||
|
||||
private func syncStatuses(range: APIController.RequestRange) {
|
||||
APIController.shared.getStatuses(range: range) { response in
|
||||
switch response {
|
||||
case .failure(let error):
|
||||
self.syncLogger.error("Erorr syncing statuses: \(String(describing: error), privacy: .public)")
|
||||
DispatchQueue.main.async {
|
||||
let alert = NSAlert()
|
||||
alert.alertStyle = .warning
|
||||
alert.messageText = "Error syncing statuses"
|
||||
alert.informativeText = error.localizedDescription
|
||||
alert.runModal()
|
||||
}
|
||||
|
||||
case .success(let statuses):
|
||||
guard statuses.count > 0 else {
|
||||
DispatchQueue.main.async {
|
||||
self.syncLogger.log("Finished sync of \(self.syncTotal, privacy: .public) statuses")
|
||||
self.onSync.send()
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
DatabaseController.shared.addStatuses(statuses.compactMap {
|
||||
if $0.hasReblog {
|
||||
return nil
|
||||
} else {
|
||||
return Status(id: $0.id, url: $0.url, summary: $0.spoiler_text, content: $0.content, published: $0.created_at)
|
||||
}
|
||||
})
|
||||
|
||||
self.syncTotal += statuses.count
|
||||
|
||||
self.syncStatuses(range: .after(statuses.first!.id))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@IBAction func importFile(_ sender: Any) {
|
||||
let panel = NSOpenPanel()
|
||||
panel.canChooseFiles = true
|
||||
|
@ -111,8 +57,8 @@ class AppDelegate: NSObject, NSApplicationDelegate {
|
|||
return
|
||||
}
|
||||
ImportController.shared.importCSV(url: panel.url!)
|
||||
self.onSync.send()
|
||||
self.syncStatuses()
|
||||
SyncController.shared.onSync.send()
|
||||
SyncController.shared.syncStatuses(errorHandler: self.handleSyncError(_:))
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -131,53 +77,10 @@ class AppDelegate: NSObject, NSApplicationDelegate {
|
|||
return
|
||||
}
|
||||
|
||||
LocalData.account = LocalData.AccountInfo(instanceURL: url, clientID: nil, clientSecret: nil, accessToken: nil)
|
||||
|
||||
APIController.shared.register { response in
|
||||
guard case .success(let registration) = response else {
|
||||
fatalError()
|
||||
}
|
||||
|
||||
LocalData.account!.clientID = registration.client_id
|
||||
LocalData.account!.clientSecret = registration.client_secret
|
||||
|
||||
var authorizeComponents = URLComponents(url: url, resolvingAgainstBaseURL: false)!
|
||||
authorizeComponents.path = "/oauth/authorize"
|
||||
authorizeComponents.queryItems = [
|
||||
URLQueryItem(name: "client_id", value: LocalData.account!.clientID),
|
||||
URLQueryItem(name: "response_type", value: "code"),
|
||||
URLQueryItem(name: "scope", value: APIController.shared.scopes),
|
||||
URLQueryItem(name: "redirect_uri", value: APIController.shared.redirectURI),
|
||||
]
|
||||
|
||||
self.authSession = ASWebAuthenticationSession(url: authorizeComponents.url!, callbackURLScheme: "mastosearch", completionHandler: { url, error in
|
||||
guard error == nil,
|
||||
let url = url,
|
||||
let components = URLComponents(url: url, resolvingAgainstBaseURL: false),
|
||||
let item = components.queryItems?.first(where: { $0.name == "code" }),
|
||||
let authCode = item.value else {
|
||||
fatalError()
|
||||
}
|
||||
|
||||
APIController.shared.getAccessToken(authCode: authCode) { response in
|
||||
guard case .success(let settings) = response else {
|
||||
fatalError()
|
||||
}
|
||||
|
||||
LocalData.account!.accessToken = settings.access_token
|
||||
|
||||
DispatchQueue.main.async {
|
||||
self.updateAccountMenu()
|
||||
self.syncStatuses()
|
||||
}
|
||||
}
|
||||
})
|
||||
DispatchQueue.main.async {
|
||||
self.authSession!.presentationContextProvider = self
|
||||
self.authSession!.start()
|
||||
}
|
||||
LoginController.shared.logIn(with: url, presentationContextProvider: self) {
|
||||
self.updateAccountMenu()
|
||||
SyncController.shared.syncStatuses(errorHandler: self.handleSyncError(_:))
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@objc func logOut() {
|
||||
|
@ -185,6 +88,14 @@ class AppDelegate: NSObject, NSApplicationDelegate {
|
|||
updateAccountMenu()
|
||||
}
|
||||
|
||||
private func handleSyncError(_ error: APIController.Error) {
|
||||
let alert = NSAlert()
|
||||
alert.alertStyle = .warning
|
||||
alert.messageText = "Error syncing statuses"
|
||||
alert.informativeText = error.localizedDescription
|
||||
alert.runModal()
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
extension AppDelegate: ASWebAuthenticationPresentationContextProviding {
|
||||
|
|
|
@ -734,7 +734,7 @@
|
|||
<tableColumnResizingMask key="resizingMask" resizeWithTable="YES" userResizable="YES"/>
|
||||
<prototypeCellViews>
|
||||
<tableCellView identifier="date" id="yUJ-7Q-rvg">
|
||||
<rect key="frame" x="8" y="0.0" width="146" height="24"/>
|
||||
<rect key="frame" x="18" y="0.0" width="146" height="24"/>
|
||||
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
|
||||
<subviews>
|
||||
<textField identifier="dateCell" horizontalHuggingPriority="251" verticalHuggingPriority="750" horizontalCompressionResistancePriority="250" fixedFrame="YES" translatesAutoresizingMaskIntoConstraints="NO" id="kPp-F2-rtb">
|
||||
|
@ -767,7 +767,7 @@
|
|||
<tableColumnResizingMask key="resizingMask" resizeWithTable="YES" userResizable="YES"/>
|
||||
<prototypeCellViews>
|
||||
<tableCellView identifier="contentWarning" id="m4I-Cd-y2k">
|
||||
<rect key="frame" x="171" y="0.0" width="173" height="24"/>
|
||||
<rect key="frame" x="181" y="0.0" width="173" height="24"/>
|
||||
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
|
||||
<subviews>
|
||||
<textField identifier="contentWarningCell" horizontalHuggingPriority="251" verticalHuggingPriority="750" horizontalCompressionResistancePriority="250" fixedFrame="YES" translatesAutoresizingMaskIntoConstraints="NO" id="Zz1-AR-4EW">
|
||||
|
@ -800,7 +800,7 @@
|
|||
<tableColumnResizingMask key="resizingMask" resizeWithTable="YES" userResizable="YES"/>
|
||||
<prototypeCellViews>
|
||||
<tableCellView identifier="content" id="BHM-ku-S45">
|
||||
<rect key="frame" x="361" y="0.0" width="303" height="17"/>
|
||||
<rect key="frame" x="371" y="0.0" width="303" height="17"/>
|
||||
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
|
||||
<subviews>
|
||||
<textField horizontalHuggingPriority="251" verticalHuggingPriority="750" horizontalCompressionResistancePriority="250" fixedFrame="YES" translatesAutoresizingMaskIntoConstraints="NO" id="Hwo-JH-icI">
|
||||
|
|
|
@ -1,16 +0,0 @@
|
|||
//
|
||||
// Status.swift
|
||||
// MastoSearch
|
||||
//
|
||||
// Created by Shadowfacts on 12/10/21.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
struct Status: Identifiable {
|
||||
let id: String
|
||||
let url: String
|
||||
let summary: String?
|
||||
let content: String
|
||||
let published: Date
|
||||
}
|
|
@ -8,6 +8,7 @@
|
|||
import Cocoa
|
||||
import Combine
|
||||
import SwiftSoup
|
||||
import MastoSearchCore
|
||||
|
||||
class ViewController: NSViewController {
|
||||
|
||||
|
@ -65,7 +66,7 @@ class ViewController: NSViewController {
|
|||
.sink { [unowned self] in self.loadAll() }
|
||||
.store(in: &cancellables)
|
||||
|
||||
(NSApp.delegate as! AppDelegate).onSync
|
||||
SyncController.shared.onSync
|
||||
.sink { [unowned self] in
|
||||
self.allStatusesSnapshot = nil
|
||||
self.loadAll()
|
||||
|
|
|
@ -7,6 +7,7 @@
|
|||
|
||||
import Cocoa
|
||||
import Combine
|
||||
import MastoSearchCore
|
||||
|
||||
class WindowController: NSWindowController {
|
||||
|
||||
|
@ -29,7 +30,7 @@ class WindowController: NSWindowController {
|
|||
}
|
||||
.store(in: &cancellables)
|
||||
|
||||
(NSApp.delegate as! AppDelegate).onSync
|
||||
SyncController.shared.onSync
|
||||
.sink { [unowned self] in self.updateSubtitle() }
|
||||
.store(in: &cancellables)
|
||||
|
||||
|
|
|
@ -0,0 +1,9 @@
|
|||
.DS_Store
|
||||
/.build
|
||||
/Packages
|
||||
/*.xcodeproj
|
||||
xcuserdata/
|
||||
DerivedData/
|
||||
.swiftpm/config/registries.json
|
||||
.swiftpm/xcode/package.xcworkspace/contents.xcworkspacedata
|
||||
.netrc
|
|
@ -0,0 +1,14 @@
|
|||
{
|
||||
"pins" : [
|
||||
{
|
||||
"identity" : "fmdb",
|
||||
"kind" : "remoteSourceControl",
|
||||
"location" : "https://github.com/ccgus/fmdb.git",
|
||||
"state" : {
|
||||
"revision" : "61e51fde7f7aab6554f30ab061cc588b28a97d04",
|
||||
"version" : "2.7.7"
|
||||
}
|
||||
}
|
||||
],
|
||||
"version" : 2
|
||||
}
|
|
@ -0,0 +1,34 @@
|
|||
// swift-tools-version: 5.7
|
||||
// The swift-tools-version declares the minimum version of Swift required to build this package.
|
||||
|
||||
import PackageDescription
|
||||
|
||||
let package = Package(
|
||||
name: "MastoSearchCore",
|
||||
platforms: [
|
||||
.macOS(.v12),
|
||||
],
|
||||
products: [
|
||||
// Products define the executables and libraries a package produces, and make them visible to other packages.
|
||||
.library(
|
||||
name: "MastoSearchCore",
|
||||
targets: ["MastoSearchCore"]),
|
||||
],
|
||||
dependencies: [
|
||||
// Dependencies declare other packages that this package depends on.
|
||||
// .package(url: /* package url */, from: "1.0.0"),
|
||||
.package(url: "https://github.com/ccgus/fmdb.git", .upToNextMinor(from: "2.7.7")),
|
||||
],
|
||||
targets: [
|
||||
// Targets are the basic building blocks of a package. A target can define a module or a test suite.
|
||||
// Targets can depend on other targets in this package, and on products in packages this package depends on.
|
||||
.target(
|
||||
name: "MastoSearchCore",
|
||||
dependencies: [
|
||||
.product(name: "FMDB", package: "fmdb"),
|
||||
]),
|
||||
.testTarget(
|
||||
name: "MastoSearchCoreTests",
|
||||
dependencies: ["MastoSearchCore"]),
|
||||
]
|
||||
)
|
|
@ -0,0 +1,3 @@
|
|||
# MastoSearchCore
|
||||
|
||||
A description of this package.
|
|
@ -5,15 +5,15 @@
|
|||
// Created by Shadowfacts on 2/19/22.
|
||||
//
|
||||
|
||||
import Cocoa
|
||||
import Foundation
|
||||
|
||||
struct APIController {
|
||||
public struct APIController {
|
||||
|
||||
static let shared = APIController()
|
||||
public static let shared = APIController()
|
||||
|
||||
let scopes = "read"
|
||||
let redirectScheme = "mastosearch"
|
||||
let redirectURI = "mastosearch://oauth"
|
||||
public let scopes = "read"
|
||||
public let redirectScheme = "mastosearch"
|
||||
public let redirectURI = "mastosearch://oauth"
|
||||
|
||||
private let decoder: JSONDecoder = {
|
||||
let decoder = JSONDecoder()
|
||||
|
@ -71,7 +71,7 @@ struct APIController {
|
|||
task.resume()
|
||||
}
|
||||
|
||||
func register(completion: @escaping (Result<ClientRegistration, Error>) -> Void) {
|
||||
public func register(completion: @escaping (Result<ClientRegistration, Error>) -> Void) {
|
||||
guard let account = LocalData.account else {
|
||||
return
|
||||
}
|
||||
|
@ -88,7 +88,7 @@ struct APIController {
|
|||
run(request: req, completion: completion)
|
||||
}
|
||||
|
||||
func getAccessToken(authCode: String, completion: @escaping (Result<LoginSettings, Error>) -> Void) {
|
||||
public func getAccessToken(authCode: String, completion: @escaping (Result<LoginSettings, Error>) -> Void) {
|
||||
guard let account = LocalData.account else {
|
||||
return
|
||||
}
|
||||
|
@ -107,7 +107,7 @@ struct APIController {
|
|||
run(request: req, completion: completion)
|
||||
}
|
||||
|
||||
func getStatuses(range: RequestRange, completion: @escaping (Result<[Status], Error>) -> Void) {
|
||||
public func getStatuses(range: RequestRange, completion: @escaping (Result<[Status], Error>) -> Void) {
|
||||
guard let account = LocalData.account else {
|
||||
return
|
||||
}
|
||||
|
@ -115,6 +115,7 @@ struct APIController {
|
|||
components.path = "/api/v1/accounts/1/statuses"
|
||||
components.queryItems = range.queryParameters + [
|
||||
URLQueryItem(name: "exclude_replies", value: "false"),
|
||||
URLQueryItem(name: "limit", value: "50"),
|
||||
]
|
||||
run(request: URLRequest(url: components.url!), completion: completion)
|
||||
}
|
||||
|
@ -122,7 +123,7 @@ struct APIController {
|
|||
}
|
||||
|
||||
extension APIController {
|
||||
enum RequestRange {
|
||||
public enum RequestRange {
|
||||
case `default`
|
||||
case after(String)
|
||||
|
||||
|
@ -136,24 +137,24 @@ extension APIController {
|
|||
}
|
||||
}
|
||||
|
||||
struct ClientRegistration: Decodable {
|
||||
let client_id: String
|
||||
let client_secret: String
|
||||
public struct ClientRegistration: Decodable {
|
||||
public let client_id: String
|
||||
public let client_secret: String
|
||||
}
|
||||
|
||||
struct LoginSettings: Decodable {
|
||||
let access_token: String
|
||||
public struct LoginSettings: Decodable {
|
||||
public let access_token: String
|
||||
}
|
||||
|
||||
struct Status: Decodable {
|
||||
let id: String
|
||||
let url: String
|
||||
let spoiler_text: String
|
||||
let content: String
|
||||
let created_at: Date
|
||||
let hasReblog: Bool
|
||||
public struct Status: Decodable {
|
||||
public let id: String
|
||||
public let url: String
|
||||
public let spoiler_text: String
|
||||
public let content: String
|
||||
public let created_at: Date
|
||||
public let hasReblog: Bool
|
||||
|
||||
init(from decoder: Decoder) throws {
|
||||
public init(from decoder: Decoder) throws {
|
||||
let container = try decoder.container(keyedBy: CodingKeys.self)
|
||||
self.id = try container.decode(String.self, forKey: .id)
|
||||
self.url = try container.decode(String.self, forKey: .url)
|
||||
|
@ -174,13 +175,13 @@ extension APIController {
|
|||
}
|
||||
|
||||
extension APIController {
|
||||
enum Error: Swift.Error {
|
||||
public enum Error: Swift.Error {
|
||||
case unexpectedStatusCode(Int)
|
||||
case error(Swift.Error)
|
||||
case noData
|
||||
case decoding(Swift.Error)
|
||||
|
||||
var localizedDescription: String {
|
||||
public var localizedDescription: String {
|
||||
switch self {
|
||||
case .unexpectedStatusCode(let code):
|
||||
return "Unexpected status code \(code)"
|
|
@ -10,9 +10,9 @@ import FMDB
|
|||
import OSLog
|
||||
import Combine
|
||||
|
||||
class DatabaseController {
|
||||
public class DatabaseController {
|
||||
|
||||
static let shared = DatabaseController()
|
||||
public static let shared = DatabaseController()
|
||||
|
||||
private let log = Logger(subsystem: Bundle.main.bundleIdentifier!, category: "DatabaseController")
|
||||
static let dateFormat = Date.ISO8601FormatStyle(includingFractionalSeconds: true)
|
||||
|
@ -22,8 +22,8 @@ class DatabaseController {
|
|||
|
||||
private var queue: FMDatabaseQueue!
|
||||
|
||||
private(set) var isInitialized = false
|
||||
let onInitialize = PassthroughSubject<(), Never>()
|
||||
public private(set) var isInitialized = false
|
||||
public let onInitialize = PassthroughSubject<(), Never>()
|
||||
|
||||
private init() {
|
||||
// this dir will be inside the application sandbox container
|
||||
|
@ -31,7 +31,7 @@ class DatabaseController {
|
|||
databaseURL = applicationSupport.appendingPathComponent("statuses").appendingPathExtension("sqlite")
|
||||
}
|
||||
|
||||
func initialize() {
|
||||
public func initialize() {
|
||||
if !FileManager.default.fileExists(atPath: databaseURL.absoluteString) {
|
||||
FileManager.default.createFile(atPath: databaseURL.absoluteString, contents: nil, attributes: nil)
|
||||
}
|
||||
|
@ -80,12 +80,12 @@ class DatabaseController {
|
|||
onInitialize.send()
|
||||
}
|
||||
|
||||
func close() {
|
||||
public func close() {
|
||||
// db.close()
|
||||
// log.info("Closed database")
|
||||
}
|
||||
|
||||
func addStatuses<S: Sequence>(_ statuses: S) where S.Element == Status {
|
||||
public func addStatuses<S: Sequence>(_ statuses: S) where S.Element == Status {
|
||||
queue.inTransaction { db, rollback in
|
||||
var i = 0
|
||||
for status in statuses {
|
||||
|
@ -117,7 +117,7 @@ class DatabaseController {
|
|||
}
|
||||
}
|
||||
|
||||
func getStatuses(sortDescriptor: NSSortDescriptor?, completion: @escaping (StatusSequence) -> Void) {
|
||||
public func getStatuses(sortDescriptor: NSSortDescriptor?, completion: @escaping (StatusSequence) -> Void) {
|
||||
queue.inDatabase { db in
|
||||
let sortKey = sortDescriptor?.key ?? "published"
|
||||
let asc = sortDescriptor?.ascending == true ? "ASC" : "DESC"
|
||||
|
@ -127,7 +127,7 @@ class DatabaseController {
|
|||
}
|
||||
}
|
||||
|
||||
func getStatuses(query: String, sortDescriptor: NSSortDescriptor?, completion: @escaping (StatusSequence) -> Void) {
|
||||
public func getStatuses(query: String, sortDescriptor: NSSortDescriptor?, completion: @escaping (StatusSequence) -> Void) {
|
||||
queue.inDatabase { db in
|
||||
let sortKey = sortDescriptor?.key ?? "rank"
|
||||
let asc = sortDescriptor?.ascending == false ? "DESC" : "ASC"
|
||||
|
@ -137,14 +137,14 @@ class DatabaseController {
|
|||
}
|
||||
}
|
||||
|
||||
func getNewestStatus(completion: @escaping (Status?) -> Void) {
|
||||
public func getNewestStatus(completion: @escaping (Status?) -> Void) {
|
||||
queue.inDatabase { db in
|
||||
let results = try! db.executeQuery("SELECT * FROM statuses ORDER BY published DESC LIMIT 1", values: nil)
|
||||
completion(StatusSequence(results: results).makeIterator().next())
|
||||
}
|
||||
}
|
||||
|
||||
func countStatuses() -> Int {
|
||||
public func countStatuses() -> Int {
|
||||
var res: Int!
|
||||
queue.inDatabase { db in
|
||||
let results = try! db.executeQuery("SELECT COUNT(*) AS count FROM statuses", values: nil)
|
||||
|
@ -156,17 +156,17 @@ class DatabaseController {
|
|||
|
||||
}
|
||||
|
||||
struct StatusSequence: Sequence {
|
||||
typealias Element = Status
|
||||
public struct StatusSequence: Sequence {
|
||||
public typealias Element = Status
|
||||
|
||||
let results: FMResultSet
|
||||
|
||||
func makeIterator() -> Iterator {
|
||||
public func makeIterator() -> Iterator {
|
||||
return Iterator(results: results)
|
||||
}
|
||||
|
||||
class Iterator: IteratorProtocol {
|
||||
typealias Element = Status
|
||||
public class Iterator: IteratorProtocol {
|
||||
public typealias Element = Status
|
||||
|
||||
let results: FMResultSet
|
||||
|
||||
|
@ -178,7 +178,7 @@ struct StatusSequence: Sequence {
|
|||
results.close()
|
||||
}
|
||||
|
||||
func next() -> Status? {
|
||||
public func next() -> Status? {
|
||||
if results.next() {
|
||||
return Status(
|
||||
id: results.string(forColumn: "api_id")!,
|
|
@ -18,8 +18,8 @@ import OSLog
|
|||
|
||||
*/
|
||||
|
||||
class ImportController {
|
||||
static let shared = ImportController()
|
||||
public class ImportController {
|
||||
public static let shared = ImportController()
|
||||
|
||||
private let logger = Logger(subsystem: Bundle.main.bundleIdentifier!, category: "ImportController")
|
||||
private let dateFormatter: DateFormatter = {
|
||||
|
@ -32,7 +32,7 @@ class ImportController {
|
|||
|
||||
private init() {}
|
||||
|
||||
func importCSV(url: URL) {
|
||||
public func importCSV(url: URL) {
|
||||
var opts = CSVReadingOptions()
|
||||
opts.usesQuoting = true
|
||||
opts.addDateParseStrategy(Date.ISO8601FormatStyle(includingFractionalSeconds: true))
|
|
@ -7,13 +7,13 @@
|
|||
|
||||
import Foundation
|
||||
|
||||
class LocalData {
|
||||
public class LocalData {
|
||||
private init() {}
|
||||
|
||||
private static let encoder = PropertyListEncoder()
|
||||
private static let decoder = PropertyListDecoder()
|
||||
|
||||
static var account: AccountInfo? {
|
||||
public static var account: AccountInfo? {
|
||||
get {
|
||||
guard let data = UserDefaults.standard.data(forKey: "account") else {
|
||||
return nil
|
||||
|
@ -30,11 +30,18 @@ class LocalData {
|
|||
}
|
||||
}
|
||||
|
||||
struct AccountInfo: Codable {
|
||||
let instanceURL: URL
|
||||
var clientID: String!
|
||||
var clientSecret: String!
|
||||
var accessToken: String!
|
||||
public struct AccountInfo: Codable {
|
||||
public let instanceURL: URL
|
||||
public var clientID: String!
|
||||
public var clientSecret: String!
|
||||
public var accessToken: String!
|
||||
|
||||
public init(instanceURL: URL, clientID: String? = nil, clientSecret: String? = nil, accessToken: String? = nil) {
|
||||
self.instanceURL = instanceURL
|
||||
self.clientID = clientID
|
||||
self.clientSecret = clientSecret
|
||||
self.accessToken = accessToken
|
||||
}
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,65 @@
|
|||
//
|
||||
// LoginController.swift
|
||||
// MastoSearchCore
|
||||
//
|
||||
// Created by Shadowfacts on 7/3/22.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
import AuthenticationServices
|
||||
|
||||
public class LoginController {
|
||||
private init() {}
|
||||
|
||||
public static let shared = LoginController()
|
||||
|
||||
private var authSession: ASWebAuthenticationSession?
|
||||
|
||||
public func logIn(with instanceURL: URL, presentationContextProvider: ASWebAuthenticationPresentationContextProviding, completion: @escaping () -> Void) {
|
||||
LocalData.account = LocalData.AccountInfo(instanceURL: instanceURL, clientID: nil, clientSecret: nil, accessToken: nil)
|
||||
|
||||
APIController.shared.register { response in
|
||||
guard case .success(let registration) = response else {
|
||||
fatalError()
|
||||
}
|
||||
|
||||
LocalData.account!.clientID = registration.client_id
|
||||
LocalData.account!.clientSecret = registration.client_secret
|
||||
|
||||
var authorizeComponents = URLComponents(url: instanceURL, resolvingAgainstBaseURL: false)!
|
||||
authorizeComponents.path = "/oauth/authorize"
|
||||
authorizeComponents.queryItems = [
|
||||
URLQueryItem(name: "client_id", value: LocalData.account!.clientID),
|
||||
URLQueryItem(name: "response_type", value: "code"),
|
||||
URLQueryItem(name: "scope", value: APIController.shared.scopes),
|
||||
URLQueryItem(name: "redirect_uri", value: APIController.shared.redirectURI),
|
||||
]
|
||||
|
||||
self.authSession = ASWebAuthenticationSession(url: authorizeComponents.url!, callbackURLScheme: "mastosearch", completionHandler: { url, error in
|
||||
guard error == nil,
|
||||
let url = url,
|
||||
let components = URLComponents(url: url, resolvingAgainstBaseURL: false),
|
||||
let item = components.queryItems?.first(where: { $0.name == "code" }),
|
||||
let authCode = item.value else {
|
||||
fatalError()
|
||||
}
|
||||
|
||||
APIController.shared.getAccessToken(authCode: authCode) { response in
|
||||
guard case .success(let settings) = response else {
|
||||
fatalError()
|
||||
}
|
||||
|
||||
LocalData.account!.accessToken = settings.access_token
|
||||
|
||||
DispatchQueue.main.async {
|
||||
completion()
|
||||
}
|
||||
}
|
||||
})
|
||||
DispatchQueue.main.async {
|
||||
self.authSession!.presentationContextProvider = presentationContextProvider
|
||||
self.authSession!.start()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,24 @@
|
|||
//
|
||||
// Status.swift
|
||||
// MastoSearch
|
||||
//
|
||||
// Created by Shadowfacts on 12/10/21.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
public struct Status: Identifiable {
|
||||
public let id: String
|
||||
public let url: String
|
||||
public let summary: String?
|
||||
public let content: String
|
||||
public let published: Date
|
||||
|
||||
public init(id: String, url: String, summary: String?, content: String, published: Date) {
|
||||
self.id = id
|
||||
self.url = url
|
||||
self.summary = summary
|
||||
self.content = content
|
||||
self.published = published
|
||||
}
|
||||
}
|
|
@ -0,0 +1,70 @@
|
|||
//
|
||||
// SyncController.swift
|
||||
// MastoSearchCore
|
||||
//
|
||||
// Created by Shadowfacts on 7/3/22.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
import Combine
|
||||
import OSLog
|
||||
|
||||
public class SyncController {
|
||||
private init() {}
|
||||
|
||||
public static let shared = SyncController()
|
||||
|
||||
public let onSync = PassthroughSubject<Void, Never>()
|
||||
|
||||
private let logger = Logger(subsystem: Bundle.main.bundleIdentifier!, category: "Sync")
|
||||
private var syncTotal = 0
|
||||
|
||||
public func syncStatuses(errorHandler: @escaping (APIController.Error) -> Void) {
|
||||
DatabaseController.shared.getNewestStatus { status in
|
||||
guard let status else {
|
||||
return
|
||||
}
|
||||
|
||||
self.logger.log("Starting sync...")
|
||||
self.syncTotal = 0
|
||||
self.syncStatuses(range: .after(status.id), errorHandler: errorHandler)
|
||||
}
|
||||
}
|
||||
|
||||
private func syncStatuses(range: APIController.RequestRange, errorHandler: @escaping (APIController.Error) -> Void) {
|
||||
APIController.shared.getStatuses(range: range) { response in
|
||||
switch response {
|
||||
case .failure(let error):
|
||||
self.logger.error("Erorr syncing statuses: \(String(describing: error), privacy: .public)")
|
||||
DispatchQueue.main.async {
|
||||
errorHandler(error)
|
||||
}
|
||||
|
||||
case .success(let statuses):
|
||||
guard statuses.count > 0 else {
|
||||
DispatchQueue.main.async {
|
||||
self.logger.log("Finished sync of \(self.syncTotal, privacy: .public) statuses")
|
||||
self.onSync.send()
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
DatabaseController.shared.addStatuses(statuses.compactMap {
|
||||
if $0.hasReblog {
|
||||
return nil
|
||||
} else {
|
||||
return Status(id: $0.id, url: $0.url, summary: $0.spoiler_text, content: $0.content, published: $0.created_at)
|
||||
}
|
||||
})
|
||||
DispatchQueue.main.async {
|
||||
self.onSync.send()
|
||||
}
|
||||
|
||||
self.syncTotal += statuses.count
|
||||
|
||||
self.syncStatuses(range: .after(statuses.first!.id), errorHandler: errorHandler)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
Loading…
Reference in New Issue