Add account switcher feature.
This commit is contained in:
parent
9e21faed2f
commit
ea2eb0a972
|
@ -79,6 +79,8 @@
|
|||
F89992C9296D6DC7005994BF /* CommentBody.swift in Sources */ = {isa = PBXBuildFile; fileRef = F89992C8296D6DC7005994BF /* CommentBody.swift */; };
|
||||
F89992CC296D9231005994BF /* StatusViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = F89992CB296D9231005994BF /* StatusViewModel.swift */; };
|
||||
F89992CE296D92E7005994BF /* AttachmentViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = F89992CD296D92E7005994BF /* AttachmentViewModel.swift */; };
|
||||
F89A46DC296EAACE0062125F /* SettingsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = F89A46DB296EAACE0062125F /* SettingsView.swift */; };
|
||||
F89A46DE296EABA20062125F /* StatusPlaceholder.swift in Sources */ = {isa = PBXBuildFile; fileRef = F89A46DD296EABA20062125F /* StatusPlaceholder.swift */; };
|
||||
F8A93D7E2965FD89001D8331 /* UserProfileView.swift in Sources */ = {isa = PBXBuildFile; fileRef = F8A93D7D2965FD89001D8331 /* UserProfileView.swift */; };
|
||||
F8A93D802965FED4001D8331 /* AccountService.swift in Sources */ = {isa = PBXBuildFile; fileRef = F8A93D7F2965FED4001D8331 /* AccountService.swift */; };
|
||||
F8C14392296AF0B3001FE31D /* String+Exif.swift in Sources */ = {isa = PBXBuildFile; fileRef = F8C14391296AF0B3001FE31D /* String+Exif.swift */; };
|
||||
|
@ -158,6 +160,8 @@
|
|||
F89992C8296D6DC7005994BF /* CommentBody.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CommentBody.swift; sourceTree = "<group>"; };
|
||||
F89992CB296D9231005994BF /* StatusViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StatusViewModel.swift; sourceTree = "<group>"; };
|
||||
F89992CD296D92E7005994BF /* AttachmentViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AttachmentViewModel.swift; sourceTree = "<group>"; };
|
||||
F89A46DB296EAACE0062125F /* SettingsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SettingsView.swift; sourceTree = "<group>"; };
|
||||
F89A46DD296EABA20062125F /* StatusPlaceholder.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StatusPlaceholder.swift; sourceTree = "<group>"; };
|
||||
F8A93D7D2965FD89001D8331 /* UserProfileView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserProfileView.swift; sourceTree = "<group>"; };
|
||||
F8A93D7F2965FED4001D8331 /* AccountService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AccountService.swift; sourceTree = "<group>"; };
|
||||
F8C14391296AF0B3001FE31D /* String+Exif.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "String+Exif.swift"; sourceTree = "<group>"; };
|
||||
|
@ -202,6 +206,7 @@
|
|||
F85DBF902967385F0069BF89 /* FollowingView.swift */,
|
||||
F897978E29684BCB00B22335 /* LoadingView.swift */,
|
||||
F88ABD9329687CA4004EF61E /* ComposeView.swift */,
|
||||
F89A46DB296EAACE0062125F /* SettingsView.swift */,
|
||||
);
|
||||
path = Views;
|
||||
sourceTree = "<group>";
|
||||
|
@ -280,6 +285,7 @@
|
|||
F86B721D296C458700EE59EC /* BlurredImage.swift */,
|
||||
F86B7222296C4BF500EE59EC /* ContentWarning.swift */,
|
||||
F89992C8296D6DC7005994BF /* CommentBody.swift */,
|
||||
F89A46DD296EABA20062125F /* StatusPlaceholder.swift */,
|
||||
);
|
||||
path = Widgets;
|
||||
sourceTree = "<group>";
|
||||
|
@ -509,6 +515,7 @@
|
|||
F88C246E295C37B80006098B /* MainView.swift in Sources */,
|
||||
F86B721E296C458700EE59EC /* BlurredImage.swift in Sources */,
|
||||
F88C2478295C37BB0006098B /* Vernissage.xcdatamodeld in Sources */,
|
||||
F89A46DE296EABA20062125F /* StatusPlaceholder.swift in Sources */,
|
||||
F88C2482295C3A4F0006098B /* StatusView.swift in Sources */,
|
||||
F866F6A329604161002E8F88 /* AccountDataHandler.swift in Sources */,
|
||||
F85D497F296416C800751DF7 /* CommentsSection.swift in Sources */,
|
||||
|
@ -533,6 +540,7 @@
|
|||
F88FAD2D295F4AD7009B20C9 /* ApplicationState.swift in Sources */,
|
||||
F866F6A1296040A8002E8F88 /* ApplicationSettings+CoreDataProperties.swift in Sources */,
|
||||
F8C14394296AF21B001FE31D /* Double+Round.swift in Sources */,
|
||||
F89A46DC296EAACE0062125F /* SettingsView.swift in Sources */,
|
||||
F866F6AE29606367002E8F88 /* ApplicationViewMode.swift in Sources */,
|
||||
F8A93D802965FED4001D8331 /* AccountService.swift in Sources */,
|
||||
F866F6AA29605AFA002E8F88 /* SceneDelegate.swift in Sources */,
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
//
|
||||
// https://mczachurski.dev
|
||||
// Copyright © 2022 Marcin Czachurski and the repository contributors.
|
||||
// Copyright © 2023 Marcin Czachurski and the repository contributors.
|
||||
// Licensed under the MIT License.
|
||||
//
|
||||
|
||||
|
@ -20,6 +20,9 @@ extension AccountData {
|
|||
@NSManaged public var acct: String
|
||||
@NSManaged public var avatar: URL?
|
||||
@NSManaged public var avatarData: Data?
|
||||
@NSManaged public var clientId: String
|
||||
@NSManaged public var clientSecret: String
|
||||
@NSManaged public var clientVapidKey: String
|
||||
@NSManaged public var createdAt: String
|
||||
@NSManaged public var displayName: String?
|
||||
@NSManaged public var followersCount: Int32
|
||||
|
@ -28,13 +31,28 @@ extension AccountData {
|
|||
@NSManaged public var id: String
|
||||
@NSManaged public var locked: Bool
|
||||
@NSManaged public var note: String?
|
||||
@NSManaged public var serverUrl: URL
|
||||
@NSManaged public var statusesCount: Int32
|
||||
@NSManaged public var url: URL?
|
||||
@NSManaged public var username: String
|
||||
@NSManaged public var clientId: String
|
||||
@NSManaged public var clientSecret: String
|
||||
@NSManaged public var clientVapidKey: String
|
||||
@NSManaged public var serverUrl: URL
|
||||
@NSManaged public var statuses: Set<StatusData>?
|
||||
|
||||
}
|
||||
|
||||
// MARK: Generated accessors for statuses
|
||||
extension AccountData {
|
||||
|
||||
@objc(addStatusesObject:)
|
||||
@NSManaged public func addToStatuses(_ value: StatusData)
|
||||
|
||||
@objc(removeStatusesObject:)
|
||||
@NSManaged public func removeFromStatuses(_ value: StatusData)
|
||||
|
||||
@objc(addStatuses:)
|
||||
@NSManaged public func addToStatuses(_ values: NSSet)
|
||||
|
||||
@objc(removeStatuses:)
|
||||
@NSManaged public func removeFromStatuses(_ values: NSSet)
|
||||
|
||||
}
|
||||
|
||||
|
|
|
@ -6,13 +6,14 @@
|
|||
|
||||
|
||||
import Foundation
|
||||
import CoreData
|
||||
|
||||
class AccountDataHandler {
|
||||
public static let shared = AccountDataHandler()
|
||||
private init() { }
|
||||
|
||||
func getAccountsData() -> [AccountData] {
|
||||
let context = CoreDataHandler.shared.container.viewContext
|
||||
func getAccountsData(viewContext: NSManagedObjectContext? = nil) -> [AccountData] {
|
||||
let context = viewContext ?? CoreDataHandler.shared.container.viewContext
|
||||
let fetchRequest = AccountData.fetchRequest()
|
||||
do {
|
||||
return try context.fetch(fetchRequest)
|
||||
|
@ -22,8 +23,8 @@ class AccountDataHandler {
|
|||
}
|
||||
}
|
||||
|
||||
func getCurrentAccountData() -> AccountData? {
|
||||
let accounts = self.getAccountsData()
|
||||
func getCurrentAccountData(viewContext: NSManagedObjectContext? = nil) -> AccountData? {
|
||||
let accounts = self.getAccountsData(viewContext: viewContext)
|
||||
let defaultSettings = ApplicationSettingsHandler.shared.getDefaultSettings()
|
||||
|
||||
let currentAccount = accounts.first { accountData in
|
||||
|
@ -37,6 +38,21 @@ class AccountDataHandler {
|
|||
return accounts.first
|
||||
}
|
||||
|
||||
func getAccountData(accountId: String, viewContext: NSManagedObjectContext? = nil) -> AccountData? {
|
||||
let context = viewContext ?? CoreDataHandler.shared.container.viewContext
|
||||
let fetchRequest = AccountData.fetchRequest()
|
||||
|
||||
fetchRequest.fetchLimit = 1
|
||||
fetchRequest.predicate = NSPredicate(format: "id = %@", accountId)
|
||||
|
||||
do {
|
||||
return try context.fetch(fetchRequest).first
|
||||
} catch {
|
||||
print("Error during fetching status (getAccountData)")
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
func createAccountDataEntity() -> AccountData {
|
||||
let context = CoreDataHandler.shared.container.viewContext
|
||||
return AccountData(context: context)
|
||||
|
|
|
@ -12,17 +12,6 @@ class AttachmentDataHandler {
|
|||
public static let shared = AttachmentDataHandler()
|
||||
private init() { }
|
||||
|
||||
func getAttachmentsData() -> [AttachmentData] {
|
||||
let context = CoreDataHandler.shared.container.viewContext
|
||||
let fetchRequest = AttachmentData.fetchRequest()
|
||||
do {
|
||||
return try context.fetch(fetchRequest)
|
||||
} catch {
|
||||
print("Error during fetching attachmens (getAttachmentsData)")
|
||||
return []
|
||||
}
|
||||
}
|
||||
|
||||
func createAttachmnentDataEntity(viewContext: NSManagedObjectContext? = nil) -> AttachmentData {
|
||||
let context = viewContext ?? CoreDataHandler.shared.container.viewContext
|
||||
return AttachmentData(context: context)
|
||||
|
|
|
@ -4,7 +4,6 @@
|
|||
// Licensed under the MIT License.
|
||||
//
|
||||
|
||||
|
||||
import CoreData
|
||||
|
||||
public class CoreDataHandler {
|
||||
|
|
|
@ -41,6 +41,8 @@ extension StatusData {
|
|||
@NSManaged public var url: URL?
|
||||
@NSManaged public var visibility: String
|
||||
@NSManaged public var attachmentRelation: Set<AttachmentData>?
|
||||
@NSManaged public var pixelfedAccount: AccountData
|
||||
|
||||
}
|
||||
|
||||
// MARK: Generated accessors for attachmentRelation
|
||||
|
|
|
@ -13,23 +13,15 @@ class StatusDataHandler {
|
|||
public static let shared = StatusDataHandler()
|
||||
private init() { }
|
||||
|
||||
func getStatusesData() -> [StatusData] {
|
||||
let context = CoreDataHandler.shared.container.viewContext
|
||||
let fetchRequest = StatusData.fetchRequest()
|
||||
do {
|
||||
return try context.fetch(fetchRequest)
|
||||
} catch {
|
||||
print("Error during fetching statuses (getStatusesData)")
|
||||
return []
|
||||
}
|
||||
}
|
||||
|
||||
func getStatusData(statusId: String) -> StatusData? {
|
||||
func getStatusData(accountId: String, statusId: String) -> StatusData? {
|
||||
let context = CoreDataHandler.shared.container.viewContext
|
||||
let fetchRequest = StatusData.fetchRequest()
|
||||
|
||||
fetchRequest.fetchLimit = 1
|
||||
fetchRequest.predicate = NSPredicate(format: "id = %@", statusId)
|
||||
let predicate1 = NSPredicate(format: "id = %@", statusId)
|
||||
let predicate2 = NSPredicate(format: "pixelfedAccount.id = %@", accountId)
|
||||
|
||||
fetchRequest.predicate = NSCompoundPredicate.init(type: .and, subpredicates: [predicate1,predicate2])
|
||||
|
||||
do {
|
||||
return try context.fetch(fetchRequest).first
|
||||
|
@ -39,13 +31,16 @@ class StatusDataHandler {
|
|||
}
|
||||
}
|
||||
|
||||
func getMaximumStatus(viewContext: NSManagedObjectContext? = nil) -> StatusData? {
|
||||
func getMaximumStatus(accountId: String, viewContext: NSManagedObjectContext? = nil) -> StatusData? {
|
||||
let context = viewContext ?? CoreDataHandler.shared.container.viewContext
|
||||
let fetchRequest = StatusData.fetchRequest()
|
||||
|
||||
fetchRequest.fetchLimit = 1
|
||||
|
||||
let sortDescriptor = NSSortDescriptor(key: "id", ascending: false)
|
||||
fetchRequest.sortDescriptors = [sortDescriptor]
|
||||
fetchRequest.predicate = NSPredicate(format: "pixelfedAccount.id = %@", accountId)
|
||||
|
||||
do {
|
||||
let statuses = try context.fetch(fetchRequest)
|
||||
return statuses.first
|
||||
|
@ -55,13 +50,16 @@ class StatusDataHandler {
|
|||
}
|
||||
}
|
||||
|
||||
func getMinimumtatus(viewContext: NSManagedObjectContext? = nil) -> StatusData? {
|
||||
func getMinimumStatus(accountId: String, viewContext: NSManagedObjectContext? = nil) -> StatusData? {
|
||||
let context = viewContext ?? CoreDataHandler.shared.container.viewContext
|
||||
let fetchRequest = StatusData.fetchRequest()
|
||||
|
||||
fetchRequest.fetchLimit = 1
|
||||
|
||||
let sortDescriptor = NSSortDescriptor(key: "id", ascending: true)
|
||||
fetchRequest.sortDescriptors = [sortDescriptor]
|
||||
fetchRequest.predicate = NSPredicate(format: "pixelfedAccount.id = %@", accountId)
|
||||
|
||||
do {
|
||||
let statuses = try context.fetch(fetchRequest)
|
||||
return statuses.first
|
||||
|
|
|
@ -8,32 +8,36 @@ import Foundation
|
|||
import CoreData
|
||||
import MastodonKit
|
||||
|
||||
public enum DatabaseError: Error {
|
||||
case cannotDownloadAccount
|
||||
}
|
||||
|
||||
public class TimelineService {
|
||||
public static let shared = TimelineService()
|
||||
private init() { }
|
||||
|
||||
public func onBottomOfList(for accountData: AccountData) async throws {
|
||||
public func onBottomOfList(for accountData: AccountData) async throws -> Int {
|
||||
// Load data from API and operate on CoreData on background context.
|
||||
let backgroundContext = CoreDataHandler.shared.newBackgroundContext()
|
||||
|
||||
// Get maximimum downloaded stauts id.
|
||||
let oldestStatus = StatusDataHandler.shared.getMinimumtatus(viewContext: backgroundContext)
|
||||
let oldestStatus = StatusDataHandler.shared.getMinimumStatus(accountId: accountData.id, viewContext: backgroundContext)
|
||||
|
||||
guard let oldestStatus = oldestStatus else {
|
||||
return
|
||||
return 0
|
||||
}
|
||||
|
||||
try await self.loadData(for: accountData, on: backgroundContext, maxId: oldestStatus.id)
|
||||
return try await self.loadData(for: accountData, on: backgroundContext, maxId: oldestStatus.id)
|
||||
}
|
||||
|
||||
public func onTopOfList(for accountData: AccountData) async throws {
|
||||
public func onTopOfList(for accountData: AccountData) async throws -> Int {
|
||||
// Load data from API and operate on CoreData on background context.
|
||||
let backgroundContext = CoreDataHandler.shared.newBackgroundContext()
|
||||
|
||||
// Get maximimum downloaded stauts id.
|
||||
let newestStatus = StatusDataHandler.shared.getMaximumStatus(viewContext: backgroundContext)
|
||||
let newestStatus = StatusDataHandler.shared.getMaximumStatus(accountId: accountData.id, viewContext: backgroundContext)
|
||||
|
||||
try await self.loadData(for: accountData, on: backgroundContext, minId: newestStatus?.id)
|
||||
return try await self.loadData(for: accountData, on: backgroundContext, minId: newestStatus?.id)
|
||||
}
|
||||
|
||||
public func getStatus(withId statusId: String, and accountData: AccountData?) async throws -> Status? {
|
||||
|
@ -50,9 +54,9 @@ public class TimelineService {
|
|||
return try await client.getContext(for: statusId)
|
||||
}
|
||||
|
||||
private func loadData(for accountData: AccountData, on backgroundContext: NSManagedObjectContext, minId: String? = nil, maxId: String? = nil) async throws {
|
||||
private func loadData(for accountData: AccountData, on backgroundContext: NSManagedObjectContext, minId: String? = nil, maxId: String? = nil) async throws -> Int {
|
||||
guard let accessToken = accountData.accessToken else {
|
||||
return
|
||||
return 0
|
||||
}
|
||||
|
||||
// Retrieve statuses from API.
|
||||
|
@ -76,13 +80,22 @@ public class TimelineService {
|
|||
}
|
||||
|
||||
let statusData = StatusDataHandler.shared.createStatusDataEntity(viewContext: backgroundContext)
|
||||
|
||||
guard let dbAccount = AccountDataHandler.shared.getAccountData(accountId: accountData.id, viewContext: backgroundContext) else {
|
||||
throw DatabaseError.cannotDownloadAccount
|
||||
}
|
||||
|
||||
statusData.pixelfedAccount = dbAccount
|
||||
dbAccount.addToStatuses(statusData)
|
||||
|
||||
try await self.copy(from: status, to: statusData, attachmentsData: attachmentsData, on: backgroundContext)
|
||||
}
|
||||
|
||||
try backgroundContext.save()
|
||||
return statuses.count
|
||||
}
|
||||
|
||||
public func updateStatus(_ statusData: StatusData, basedOn status: Status) async throws -> StatusData? {
|
||||
public func updateStatus(_ statusData: StatusData, accountData: AccountData, basedOn status: Status) async throws -> StatusData? {
|
||||
// Load data from API and operate on CoreData on background context.
|
||||
let backgroundContext = CoreDataHandler.shared.newBackgroundContext()
|
||||
|
||||
|
|
|
@ -20,6 +20,7 @@
|
|||
<attribute name="statusesCount" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
|
||||
<attribute name="url" optional="YES" attributeType="URI"/>
|
||||
<attribute name="username" attributeType="String"/>
|
||||
<relationship name="statuses" optional="YES" toMany="YES" deletionRule="Cascade" destinationEntity="StatusData" inverseName="pixelfedAccount" inverseEntity="StatusData"/>
|
||||
</entity>
|
||||
<entity name="ApplicationSettings" representedClassName="ApplicationSettings" syncable="YES">
|
||||
<attribute name="currentAccount" optional="YES" attributeType="String"/>
|
||||
|
@ -68,5 +69,6 @@
|
|||
<attribute name="url" optional="YES" attributeType="URI"/>
|
||||
<attribute name="visibility" attributeType="String"/>
|
||||
<relationship name="attachmentRelation" optional="YES" toMany="YES" deletionRule="Cascade" destinationEntity="AttachmentData" inverseName="statusRelation" inverseEntity="AttachmentData"/>
|
||||
<relationship name="pixelfedAccount" maxCount="1" deletionRule="No Action" destinationEntity="AccountData" inverseName="statuses" inverseEntity="AccountData"/>
|
||||
</entity>
|
||||
</model>
|
|
@ -9,6 +9,7 @@ import MastodonKit
|
|||
|
||||
public class StatusViewModel {
|
||||
|
||||
public let uniqueId: UUID
|
||||
public let id: StatusId
|
||||
public let content: Html
|
||||
|
||||
|
@ -65,6 +66,7 @@ public class StatusViewModel {
|
|||
tags: [Tag] = [],
|
||||
place: Place? = nil
|
||||
) {
|
||||
self.uniqueId = UUID()
|
||||
self.id = id
|
||||
self.content = content
|
||||
self.uri = uri
|
||||
|
@ -94,6 +96,7 @@ public class StatusViewModel {
|
|||
}
|
||||
|
||||
init(status: Status) {
|
||||
self.uniqueId = UUID()
|
||||
self.id = status.id
|
||||
self.content = status.content
|
||||
self.uri = status.uri
|
||||
|
|
|
@ -33,12 +33,10 @@ struct FollowersView: View {
|
|||
|
||||
if allItemsLoaded == false && firstLoadFinished == true {
|
||||
LoadingIndicator()
|
||||
.onAppear {
|
||||
Task {
|
||||
.task {
|
||||
self.page = self.page + 1
|
||||
await self.loadAccounts(page: self.page)
|
||||
}
|
||||
}
|
||||
.frame(idealWidth: .infinity, maxWidth: .infinity, alignment: .center)
|
||||
}
|
||||
}.overlay {
|
||||
|
|
|
@ -33,12 +33,10 @@ struct FollowingView: View {
|
|||
|
||||
if allItemsLoaded == false && firstLoadFinished == true {
|
||||
LoadingIndicator()
|
||||
.onAppear {
|
||||
Task {
|
||||
.task {
|
||||
self.page = self.page + 1
|
||||
await self.loadAccounts(page: self.page)
|
||||
}
|
||||
}
|
||||
.frame(idealWidth: .infinity, maxWidth: .infinity, alignment: .center)
|
||||
}
|
||||
}.overlay {
|
||||
|
|
|
@ -10,15 +10,21 @@ struct HomeFeedView: View {
|
|||
@Environment(\.managedObjectContext) private var viewContext
|
||||
@EnvironmentObject var applicationState: ApplicationState
|
||||
|
||||
@State private var showLoading = false
|
||||
@State private var firstLoadFinished = false
|
||||
@State private var allItemsBottomLoaded = false
|
||||
|
||||
private static let initialColumns = 1
|
||||
@State private var gridColumns = Array(repeating: GridItem(.flexible()), count: initialColumns)
|
||||
|
||||
@FetchRequest(sortDescriptors: [SortDescriptor(\.id, order: .reverse)]) var dbStatuses: FetchedResults<StatusData>
|
||||
@FetchRequest var dbStatuses: FetchedResults<StatusData>
|
||||
|
||||
init(accountId: String) {
|
||||
_dbStatuses = FetchRequest<StatusData>(
|
||||
sortDescriptors: [SortDescriptor(\.id, order: .reverse)],
|
||||
predicate: NSPredicate(format: "pixelfedAccount.id = %@", accountId))
|
||||
}
|
||||
|
||||
var body: some View {
|
||||
ZStack {
|
||||
ScrollView {
|
||||
LazyVGrid(columns: gridColumns) {
|
||||
ForEach(dbStatuses, id: \.self) { item in
|
||||
|
@ -32,12 +38,15 @@ struct HomeFeedView: View {
|
|||
.buttonStyle(EmptyButtonStyle())
|
||||
}
|
||||
|
||||
if allItemsBottomLoaded == false && firstLoadFinished == true {
|
||||
LoadingIndicator()
|
||||
.onAppear {
|
||||
Task {
|
||||
.task {
|
||||
do {
|
||||
if let accountData = self.applicationState.accountData {
|
||||
try await TimelineService.shared.onBottomOfList(for: accountData)
|
||||
let newStatusesCount = try await TimelineService.shared.onBottomOfList(for: accountData)
|
||||
if newStatusesCount == 0 {
|
||||
allItemsBottomLoaded = true
|
||||
}
|
||||
}
|
||||
} catch {
|
||||
print("Error", error)
|
||||
|
@ -46,15 +55,25 @@ struct HomeFeedView: View {
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
if showLoading {
|
||||
.overlay(alignment: .center) {
|
||||
if firstLoadFinished == false {
|
||||
LoadingIndicator()
|
||||
} else {
|
||||
if self.dbStatuses.isEmpty {
|
||||
VStack {
|
||||
Image(systemName: "photo.on.rectangle.angled")
|
||||
.font(.largeTitle)
|
||||
.padding(.bottom, 4)
|
||||
Text("Unfortunately, there are no photos here.")
|
||||
.font(.title3)
|
||||
}.foregroundColor(.lightGrayColor)
|
||||
}
|
||||
}
|
||||
}
|
||||
.refreshable {
|
||||
do {
|
||||
if let accountData = self.applicationState.accountData {
|
||||
try await TimelineService.shared.onTopOfList(for: accountData)
|
||||
_ = try await TimelineService.shared.onTopOfList(for: accountData)
|
||||
}
|
||||
} catch {
|
||||
print("Error", error)
|
||||
|
@ -62,15 +81,20 @@ struct HomeFeedView: View {
|
|||
}
|
||||
.task {
|
||||
do {
|
||||
if self.dbStatuses.isEmpty {
|
||||
self.showLoading = true
|
||||
if let accountData = self.applicationState.accountData {
|
||||
try await TimelineService.shared.onTopOfList(for: accountData)
|
||||
defer {
|
||||
Task { @MainActor in
|
||||
self.firstLoadFinished = true
|
||||
}
|
||||
self.showLoading = false
|
||||
}
|
||||
|
||||
if self.dbStatuses.isEmpty == false {
|
||||
return
|
||||
}
|
||||
|
||||
if let accountData = self.applicationState.accountData {
|
||||
_ = try await TimelineService.shared.onTopOfList(for: accountData)
|
||||
}
|
||||
} catch {
|
||||
self.showLoading = false
|
||||
print("Error", error)
|
||||
}
|
||||
}
|
||||
|
@ -79,6 +103,6 @@ struct HomeFeedView: View {
|
|||
|
||||
struct HomeFeedView_Previews: PreviewProvider {
|
||||
static var previews: some View {
|
||||
HomeFeedView()
|
||||
HomeFeedView(accountId: "")
|
||||
}
|
||||
}
|
||||
|
|
|
@ -13,6 +13,7 @@ struct MainView: View {
|
|||
@Environment(\.managedObjectContext) private var viewContext
|
||||
@EnvironmentObject var applicationState: ApplicationState
|
||||
|
||||
@State private var showSettings = false
|
||||
@State private var navBarTitle: String = "Home"
|
||||
@State private var viewMode: ViewMode = .home {
|
||||
didSet {
|
||||
|
@ -20,6 +21,8 @@ struct MainView: View {
|
|||
}
|
||||
}
|
||||
|
||||
@FetchRequest(sortDescriptors: [SortDescriptor(\.acct, order: .forward)]) var dbAccounts: FetchedResults<AccountData>
|
||||
|
||||
private enum ViewMode {
|
||||
case home, local, federated, profile, notifications
|
||||
}
|
||||
|
@ -28,6 +31,9 @@ struct MainView: View {
|
|||
self.getMainView()
|
||||
.navigationBarTitle(navBarTitle)
|
||||
.navigationBarTitleDisplayMode(.inline)
|
||||
.sheet(isPresented: $showSettings, content: {
|
||||
SettingsView()
|
||||
})
|
||||
.toolbar {
|
||||
self.getLeadingToolbar()
|
||||
self.getPrincipalToolbar()
|
||||
|
@ -38,19 +44,24 @@ struct MainView: View {
|
|||
private func getMainView() -> some View {
|
||||
switch self.viewMode {
|
||||
case .home:
|
||||
HomeFeedView()
|
||||
HomeFeedView(accountId: applicationState.accountData?.id ?? "")
|
||||
.id(applicationState.accountData?.id ?? "")
|
||||
case .local:
|
||||
LocalFeedView()
|
||||
.id(applicationState.accountData?.id ?? "")
|
||||
case .federated:
|
||||
FederatedFeedView()
|
||||
.id(applicationState.accountData?.id ?? "")
|
||||
case .profile:
|
||||
if let accountData = self.applicationState.accountData {
|
||||
UserProfileView(accountId: accountData.id,
|
||||
accountDisplayName: accountData.displayName,
|
||||
accountUserName: accountData.acct)
|
||||
.id(applicationState.accountData?.id ?? "")
|
||||
}
|
||||
case .notifications:
|
||||
NotificationsView()
|
||||
.id(applicationState.accountData?.id ?? "")
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -121,27 +132,24 @@ struct MainView: View {
|
|||
private func getLeadingToolbar() -> some ToolbarContent {
|
||||
ToolbarItem(placement: .navigationBarLeading) {
|
||||
Menu {
|
||||
|
||||
ForEach(self.dbAccounts) { account in
|
||||
Button {
|
||||
// TODO: Switch accounts.
|
||||
self.applicationState.accountData = account
|
||||
} label: {
|
||||
HStack {
|
||||
Text(self.applicationState.accountData?.displayName ?? self.applicationState.accountData?.acct ?? "")
|
||||
Image(systemName: "person.circle.fill")
|
||||
.resizable()
|
||||
.foregroundColor(.mainTextColor)
|
||||
if self.applicationState.accountData?.id == account.id {
|
||||
Label(account.displayName ?? account.acct, systemImage: "checkmark")
|
||||
} else {
|
||||
Text(account.displayName ?? account.acct)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Divider()
|
||||
|
||||
Button {
|
||||
// TODO: Open settings.
|
||||
self.showSettings.toggle()
|
||||
} label: {
|
||||
HStack {
|
||||
Text("Settings")
|
||||
Image(systemName: "gear")
|
||||
}
|
||||
Label("Settings", systemImage: "gear")
|
||||
}
|
||||
} label: {
|
||||
if let avatarData = self.applicationState.accountData?.avatarData, let uiImage = UIImage(data: avatarData) {
|
||||
|
|
|
@ -0,0 +1,64 @@
|
|||
//
|
||||
// https://mczachurski.dev
|
||||
// Copyright © 2023 Marcin Czachurski and the repository contributors.
|
||||
// Licensed under the MIT License.
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
|
||||
struct SettingsView: View {
|
||||
@EnvironmentObject var applicationState: ApplicationState
|
||||
@Environment(\.dismiss) private var dismiss
|
||||
|
||||
@State var accounts: [AccountData] = []
|
||||
|
||||
var body: some View {
|
||||
NavigationView {
|
||||
List {
|
||||
Section("Accounts") {
|
||||
ForEach(self.accounts) { account in
|
||||
UsernameRow(accountAvatar: account.avatar,
|
||||
accountDisplayName: account.displayName,
|
||||
accountUsername: account.username,
|
||||
cachedAvatar: CacheAvatarService.shared.getImage(for: account.id))
|
||||
}
|
||||
|
||||
NavigationLink(destination: SignInView()) {
|
||||
HStack {
|
||||
Text("New account")
|
||||
Spacer()
|
||||
Image(systemName: "person.crop.circle.badge.plus")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Section("General") {
|
||||
Text("Accent")
|
||||
}
|
||||
|
||||
Section("About") {
|
||||
Text("Website")
|
||||
Text("License")
|
||||
}
|
||||
}
|
||||
.frame(alignment: .topLeading)
|
||||
.toolbar {
|
||||
ToolbarItem(placement: .cancellationAction) {
|
||||
Button("Close", role: .cancel) {
|
||||
dismiss()
|
||||
}
|
||||
}
|
||||
}
|
||||
.task {
|
||||
self.accounts = AccountDataHandler.shared.getAccountsData()
|
||||
}
|
||||
.navigationBarTitle(Text("Settings"), displayMode: .inline)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
struct SettingsView_Previews: PreviewProvider {
|
||||
static var previews: some View {
|
||||
SettingsView()
|
||||
}
|
||||
}
|
|
@ -8,10 +8,12 @@ import SwiftUI
|
|||
|
||||
struct SignInView: View {
|
||||
@Environment(\.managedObjectContext) private var viewContext
|
||||
@Environment(\.dismiss) private var dismiss
|
||||
@EnvironmentObject var applicationState: ApplicationState
|
||||
|
||||
@State private var serverAddress: String = ""
|
||||
|
||||
|
||||
var onSignInStateChenge: ((_ applicationViewMode: ApplicationViewMode) -> Void)?
|
||||
|
||||
var body: some View {
|
||||
|
@ -33,6 +35,7 @@ struct SignInView: View {
|
|||
DispatchQueue.main.async {
|
||||
self.applicationState.accountData = accountData
|
||||
onSignInStateChenge?(.mainView)
|
||||
dismiss()
|
||||
}
|
||||
})
|
||||
}
|
||||
|
|
|
@ -88,47 +88,14 @@ struct StatusView: View {
|
|||
}
|
||||
}
|
||||
} else {
|
||||
VStack (alignment: .leading) {
|
||||
if let imageBlurhash, let uiImage = UIImage(blurHash: imageBlurhash, size: CGSize(width: 32, height: 32)) {
|
||||
Image(uiImage: uiImage)
|
||||
.resizable()
|
||||
.frame(width: UIScreen.main.bounds.width, height: self.getImageHeight())
|
||||
} else {
|
||||
Rectangle()
|
||||
.fill(Color.placeholderText)
|
||||
.frame(width: UIScreen.main.bounds.width, height: self.getImageHeight())
|
||||
.redacted(reason: .placeholder)
|
||||
}
|
||||
|
||||
VStack(alignment: .leading) {
|
||||
UsernameRow(accountDisplayName: "Verylong Displayname",
|
||||
accountUsername: "@username")
|
||||
|
||||
Text("Lorem ispum text something")
|
||||
.foregroundColor(.lightGrayColor)
|
||||
.font(.footnote)
|
||||
Text("Lorem ispum text something sdf sdfsdf sdfdsfsdfsdf")
|
||||
.foregroundColor(.lightGrayColor)
|
||||
.font(.footnote)
|
||||
|
||||
LabelIcon(iconName: "mappin.and.ellipse", value: "Wroclaw, Poland")
|
||||
LabelIcon(iconName: "camera", value: "SONY ILCE-7M3")
|
||||
LabelIcon(iconName: "camera.aperture", value: "Viltrox 24mm F1.8 E")
|
||||
LabelIcon(iconName: "timelapse", value: "24.0 mm, f/1.8, 1/640s, ISO 100")
|
||||
LabelIcon(iconName: "calendar", value: "2 Oct 2022")
|
||||
}
|
||||
.padding(8)
|
||||
.redacted(reason: .placeholder)
|
||||
.animatePlaceholder(isLoading: .constant(true))
|
||||
}
|
||||
StatusPlaceholder(imageHeight: self.getImageHeight(), imageBlurhash: self.imageBlurhash)
|
||||
}
|
||||
}
|
||||
.navigationBarTitle("Details")
|
||||
.sheet(isPresented: $showCompose, content: {
|
||||
ComposeView(statusViewModel: $messageForStatus)
|
||||
})
|
||||
.onAppear {
|
||||
Task {
|
||||
.task {
|
||||
do {
|
||||
// Get status from API.
|
||||
if let status = try await TimelineService.shared.getStatus(withId: self.statusId, and: self.applicationState.accountData) {
|
||||
|
@ -144,12 +111,10 @@ struct StatusView: View {
|
|||
|
||||
self.statusViewModel = statusViewModel
|
||||
|
||||
// Get status from database.
|
||||
let statusDataFromDatabase = StatusDataHandler.shared.getStatusData(statusId: self.statusId)
|
||||
|
||||
// If we have status in database then we can update data.
|
||||
if let statusDataFromDatabase {
|
||||
_ = try await TimelineService.shared.updateStatus(statusDataFromDatabase, basedOn: status)
|
||||
if let accountData = self.applicationState.accountData,
|
||||
let statusDataFromDatabase = StatusDataHandler.shared.getStatusData(accountId: accountData.id, statusId: self.statusId) {
|
||||
_ = try await TimelineService.shared.updateStatus(statusDataFromDatabase, accountData: accountData, basedOn: status)
|
||||
}
|
||||
}
|
||||
} catch {
|
||||
|
@ -157,7 +122,6 @@ struct StatusView: View {
|
|||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private func setAttachment(_ attachmentData: AttachmentData) {
|
||||
exifCamera = attachmentData.exifCamera
|
||||
|
|
|
@ -30,8 +30,7 @@ struct UserProfileView: View {
|
|||
}
|
||||
}
|
||||
.navigationBarTitle(self.accountDisplayName ?? self.accountUserName)
|
||||
.onAppear {
|
||||
Task {
|
||||
.task {
|
||||
do {
|
||||
try await self.loadData()
|
||||
} catch {
|
||||
|
@ -39,7 +38,6 @@ struct UserProfileView: View {
|
|||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private func loadData() async throws {
|
||||
async let relationshipTask = AccountService.shared.getRelationship(withId: self.accountId, forUser: self.applicationState.accountData)
|
||||
|
|
|
@ -0,0 +1,55 @@
|
|||
//
|
||||
// https://mczachurski.dev
|
||||
// Copyright © 2023 Marcin Czachurski and the repository contributors.
|
||||
// Licensed under the MIT License.
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
|
||||
struct StatusPlaceholder: View {
|
||||
@State var imageHeight: Double
|
||||
@State var imageBlurhash: String?
|
||||
|
||||
var body: some View {
|
||||
VStack (alignment: .leading) {
|
||||
if let imageBlurhash, let uiImage = UIImage(blurHash: imageBlurhash, size: CGSize(width: 32, height: 32)) {
|
||||
Image(uiImage: uiImage)
|
||||
.resizable()
|
||||
.frame(width: UIScreen.main.bounds.width, height: imageHeight)
|
||||
} else {
|
||||
Rectangle()
|
||||
.fill(Color.placeholderText)
|
||||
.frame(width: UIScreen.main.bounds.width, height: imageHeight)
|
||||
.redacted(reason: .placeholder)
|
||||
}
|
||||
|
||||
VStack(alignment: .leading) {
|
||||
UsernameRow(accountDisplayName: "Verylong Displayname",
|
||||
accountUsername: "@username")
|
||||
|
||||
Text("Lorem ispum text something")
|
||||
.foregroundColor(.lightGrayColor)
|
||||
.font(.footnote)
|
||||
Text("Lorem ispum text something sdf sdfsdf sdfdsfsdfsdf")
|
||||
.foregroundColor(.lightGrayColor)
|
||||
.font(.footnote)
|
||||
|
||||
LabelIcon(iconName: "mappin.and.ellipse", value: "Wroclaw, Poland")
|
||||
LabelIcon(iconName: "camera", value: "SONY ILCE-7M3")
|
||||
LabelIcon(iconName: "camera.aperture", value: "Viltrox 24mm F1.8 E")
|
||||
LabelIcon(iconName: "timelapse", value: "24.0 mm, f/1.8, 1/640s, ISO 100")
|
||||
LabelIcon(iconName: "calendar", value: "2 Oct 2022")
|
||||
}
|
||||
.padding(8)
|
||||
.redacted(reason: .placeholder)
|
||||
.animatePlaceholder(isLoading: .constant(true))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
struct StatusPlaceholder_Previews: PreviewProvider {
|
||||
static var previews: some View {
|
||||
StatusPlaceholder(imageHeight: 100.0)
|
||||
.previewLayout(.fixed(width: 320, height: 320))
|
||||
}
|
||||
}
|
|
@ -20,7 +20,7 @@ struct UserProfileStatuses: View {
|
|||
var body: some View {
|
||||
VStack(alignment: .center) {
|
||||
if firstLoadFinished == true {
|
||||
ForEach(self.statusViewModels, id: \.id) { item in
|
||||
ForEach(self.statusViewModels, id: \.uniqueId) { item in
|
||||
NavigationLink(destination: StatusView(statusId: item.id,
|
||||
imageBlurhash: item.mediaAttachments.first?.blurhash,
|
||||
imageWidth: item.getImageWidth(),
|
||||
|
@ -35,15 +35,13 @@ struct UserProfileStatuses: View {
|
|||
LazyVStack {
|
||||
if allItemsLoaded == false && firstLoadFinished == true {
|
||||
LoadingIndicator()
|
||||
.onAppear {
|
||||
Task {
|
||||
.task {
|
||||
do {
|
||||
try await self.loadMoreStatuses()
|
||||
} catch {
|
||||
print("Error \(error.localizedDescription)")
|
||||
}
|
||||
}
|
||||
}
|
||||
.frame(idealWidth: .infinity, maxWidth: .infinity, alignment: .center)
|
||||
}
|
||||
}
|
||||
|
@ -51,8 +49,7 @@ struct UserProfileStatuses: View {
|
|||
} else {
|
||||
LoadingIndicator()
|
||||
}
|
||||
}.onAppear {
|
||||
Task {
|
||||
}.task {
|
||||
do {
|
||||
try await self.loadStatuses()
|
||||
} catch {
|
||||
|
@ -60,7 +57,6 @@ struct UserProfileStatuses: View {
|
|||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private func loadStatuses() async throws {
|
||||
let statuses = try await AccountService.shared.getStatuses(
|
||||
|
|
Loading…
Reference in New Issue