Add account switcher feature.

This commit is contained in:
Marcin Czachursk 2023-01-11 13:16:43 +01:00
parent 9e21faed2f
commit ea2eb0a972
20 changed files with 339 additions and 183 deletions

View File

@ -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 */,

View File

@ -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)
}

View File

@ -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)

View File

@ -11,17 +11,6 @@ import CoreData
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

View File

@ -3,7 +3,6 @@
// Copyright © 2022 Marcin Czachurski and the repository contributors.
// Licensed under the MIT License.
//
import CoreData

View File

@ -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

View File

@ -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

View File

@ -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()

View File

@ -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>

View File

@ -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

View File

@ -33,11 +33,9 @@ struct FollowersView: View {
if allItemsLoaded == false && firstLoadFinished == true {
LoadingIndicator()
.onAppear {
Task {
self.page = self.page + 1
await self.loadAccounts(page: self.page)
}
.task {
self.page = self.page + 1
await self.loadAccounts(page: self.page)
}
.frame(idealWidth: .infinity, maxWidth: .infinity, alignment: .center)
}

View File

@ -33,11 +33,9 @@ struct FollowingView: View {
if allItemsLoaded == false && firstLoadFinished == true {
LoadingIndicator()
.onAppear {
Task {
self.page = self.page + 1
await self.loadAccounts(page: self.page)
}
.task {
self.page = self.page + 1
await self.loadAccounts(page: self.page)
}
.frame(idealWidth: .infinity, maxWidth: .infinity, alignment: .center)
}

View File

@ -10,51 +10,70 @@ 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 var dbStatuses: FetchedResults<StatusData>
@FetchRequest(sortDescriptors: [SortDescriptor(\.id, order: .reverse)]) 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
NavigationLink(destination: StatusView(statusId: item.id,
imageBlurhash: item.attachments().first?.blurhash,
imageWidth: item.attachments().first?.metaImageWidth,
imageHeight: item.attachments().first?.metaImageHeight)
.environmentObject(applicationState)) {
ImageRow(status: item)
}
.buttonStyle(EmptyButtonStyle())
ScrollView {
LazyVGrid(columns: gridColumns) {
ForEach(dbStatuses, id: \.self) { item in
NavigationLink(destination: StatusView(statusId: item.id,
imageBlurhash: item.attachments().first?.blurhash,
imageWidth: item.attachments().first?.metaImageWidth,
imageHeight: item.attachments().first?.metaImageHeight)
.environmentObject(applicationState)) {
ImageRow(status: item)
}
.buttonStyle(EmptyButtonStyle())
}
if allItemsBottomLoaded == false && firstLoadFinished == true {
LoadingIndicator()
.onAppear {
Task {
do {
if let accountData = self.applicationState.accountData {
try await TimelineService.shared.onBottomOfList(for: accountData)
.task {
do {
if let accountData = self.applicationState.accountData {
let newStatusesCount = try await TimelineService.shared.onBottomOfList(for: accountData)
if newStatusesCount == 0 {
allItemsBottomLoaded = true
}
} catch {
print("Error", error)
}
} catch {
print("Error", error)
}
}
}
}
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: "")
}
}

View File

@ -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 {
Button {
// TODO: Switch accounts.
} label: {
HStack {
Text(self.applicationState.accountData?.displayName ?? self.applicationState.accountData?.acct ?? "")
Image(systemName: "person.circle.fill")
.resizable()
.foregroundColor(.mainTextColor)
ForEach(self.dbAccounts) { account in
Button {
self.applicationState.accountData = account
} label: {
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) {

View File

@ -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()
}
}

View File

@ -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()
}
})
}

View File

@ -88,73 +88,37 @@ 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 {
do {
// Get status from API.
if let status = try await TimelineService.shared.getStatus(withId: self.statusId, and: self.applicationState.accountData) {
let statusViewModel = StatusViewModel(status: status)
// Download images and recalculate exif data.
let allImages = await TimelineService.shared.fetchAllImages(statuses: [status])
for attachment in statusViewModel.mediaAttachments {
if let data = allImages[attachment.id] {
attachment.set(data: data)
}
}
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)
.task {
do {
// Get status from API.
if let status = try await TimelineService.shared.getStatus(withId: self.statusId, and: self.applicationState.accountData) {
let statusViewModel = StatusViewModel(status: status)
// Download images and recalculate exif data.
let allImages = await TimelineService.shared.fetchAllImages(statuses: [status])
for attachment in statusViewModel.mediaAttachments {
if let data = allImages[attachment.id] {
attachment.set(data: data)
}
}
} catch {
print("Error \(error.localizedDescription)")
self.statusViewModel = statusViewModel
// If we have status in database then we can update data.
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 {
print("Error \(error.localizedDescription)")
}
}
}

View File

@ -30,13 +30,11 @@ struct UserProfileView: View {
}
}
.navigationBarTitle(self.accountDisplayName ?? self.accountUserName)
.onAppear {
Task {
do {
try await self.loadData()
} catch {
print("Error \(error.localizedDescription)")
}
.task {
do {
try await self.loadData()
} catch {
print("Error \(error.localizedDescription)")
}
}
}

View File

@ -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))
}
}

View File

@ -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,13 +35,11 @@ struct UserProfileStatuses: View {
LazyVStack {
if allItemsLoaded == false && firstLoadFinished == true {
LoadingIndicator()
.onAppear {
Task {
do {
try await self.loadMoreStatuses()
} catch {
print("Error \(error.localizedDescription)")
}
.task {
do {
try await self.loadMoreStatuses()
} catch {
print("Error \(error.localizedDescription)")
}
}
.frame(idealWidth: .infinity, maxWidth: .infinity, alignment: .center)
@ -51,13 +49,11 @@ struct UserProfileStatuses: View {
} else {
LoadingIndicator()
}
}.onAppear {
Task {
do {
try await self.loadStatuses()
} catch {
print("Error \(error.localizedDescription)")
}
}.task {
do {
try await self.loadStatuses()
} catch {
print("Error \(error.localizedDescription)")
}
}
}