Merge pull request #96 from VernissageApp/develop

Merge version 2.0.0 into main
This commit is contained in:
Marcin Czachurski 2023-10-28 17:29:51 +02:00 committed by GitHub
commit 60a69e6236
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
171 changed files with 15612 additions and 4742 deletions

View File

@ -1,14 +1,13 @@
// swift-tools-version: 5.8
// swift-tools-version: 5.9
// The swift-tools-version declares the minimum version of Swift required to build this package.
import PackageDescription
let package = Package(
name: "ClientKit",
defaultLocalization: "en",
platforms: [
.iOS(.v16),
.macOS(.v12),
.watchOS(.v8)
.iOS(.v17)
],
products: [
// Products define the executables and libraries a package produces, and make them visible to other packages.

View File

@ -7,7 +7,7 @@
import Foundation
import PixelfedKit
public class Client: ObservableObject {
@Observable public class Client {
public static let shared = Client()
private init() { }

View File

@ -0,0 +1,5 @@
{
"sourceLanguage" : "en",
"strings" : {},
"version" : "1.0"
}

View File

@ -6,7 +6,7 @@
import Foundation
public class AccountModel: ObservableObject, Identifiable {
@Observable public class AccountModel: Identifiable {
public let id: String
public let accessToken: String?
public let refreshToken: String?
@ -27,8 +27,9 @@ public class AccountModel: ObservableObject, Identifiable {
public let url: URL?
public let username: String
public let lastSeenStatusId: String?
@Published public var avatarData: Data?
public let lastSeenNotificationId: String?
public var avatarData: Data?
public init(id: String,
accessToken: String?,
@ -50,6 +51,7 @@ public class AccountModel: ObservableObject, Identifiable {
url: URL?,
username: String,
lastSeenStatusId: String?,
lastSeenNotificationId: String?,
avatarData: Data? = nil) {
self.id = id
self.accessToken = accessToken
@ -72,6 +74,7 @@ public class AccountModel: ObservableObject, Identifiable {
self.username = username
self.lastSeenStatusId = lastSeenStatusId
self.avatarData = avatarData
self.lastSeenNotificationId = lastSeenNotificationId
}
}

View File

@ -7,7 +7,7 @@
import Foundation
import PixelfedKit
public class AttachmentModel: ObservableObject, Identifiable {
@Observable public class AttachmentModel: Identifiable {
public let id: String
public let type: MediaAttachment.MediaAttachmentType
public let url: URL
@ -21,11 +21,11 @@ public class AttachmentModel: ObservableObject, Identifiable {
public let metaImageWidth: Int32?
public let metaImageHeight: Int32?
@Published public var exifCamera: String?
@Published public var exifCreatedDate: String?
@Published public var exifExposure: String?
@Published public var exifLens: String?
@Published public var data: Data?
public var exifCamera: String?
public var exifCreatedDate: String?
public var exifExposure: String?
public var exifLens: String?
public var data: Data?
public init(id: String,
type: MediaAttachment.MediaAttachmentType,

View File

@ -7,7 +7,7 @@
import Foundation
import PixelfedKit
public class StatusModel: ObservableObject {
@Observable public class StatusModel {
public let id: EntityId
public let rebloggedStatusId: EntityId?
public let content: Html
@ -38,8 +38,8 @@ public class StatusModel: ObservableObject {
public let reblogStatus: Status?
@Published public var favourited: Bool
@Published public var mediaAttachments: [AttachmentModel]
public var favourited: Bool
public var mediaAttachments: [AttachmentModel]
public init(status: Status) {
self.id = status.id

View File

@ -1,35 +0,0 @@
//
// https://mczachurski.dev
// Copyright © 2023 Marcin Czachurski and the repository contributors.
// Licensed under the Apache License 2.0.
//
import Foundation
import ClientKit
extension AccountData {
func toAccountModel() -> AccountModel {
let accountModel = AccountModel(id: self.id,
accessToken: self.accessToken,
refreshToken: self.refreshToken,
acct: self.acct,
avatar: self.avatar,
clientId: self.clientId,
clientSecret: self.clientSecret,
clientVapidKey: self.clientVapidKey,
createdAt: self.createdAt,
displayName: self.displayName,
followersCount: self.followersCount,
followingCount: self.followingCount,
header: self.header,
locked: self.locked,
note: self.note,
serverUrl: self.serverUrl,
statusesCount: self.statusesCount,
url: self.url,
username: self.username,
lastSeenStatusId: self.lastSeenStatusId,
avatarData: self.avatarData)
return accountModel
}
}

View File

@ -1,12 +0,0 @@
//
// https://mczachurski.dev
// Copyright © 2023 Marcin Czachurski and the repository contributors.
// Licensed under the Apache License 2.0.
//
import Foundation
import CoreData
@objc(AccountData)
public class AccountData: NSManagedObject {
}

View File

@ -1,84 +0,0 @@
//
// https://mczachurski.dev
// Copyright © 2023 Marcin Czachurski and the repository contributors.
// Licensed under the Apache License 2.0.
//
import Foundation
import CoreData
extension AccountData {
@nonobjc public class func fetchRequest() -> NSFetchRequest<AccountData> {
return NSFetchRequest<AccountData>(entityName: "AccountData")
}
@NSManaged public var accessToken: String?
@NSManaged public var refreshToken: String?
@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
@NSManaged public var followingCount: Int32
@NSManaged public var header: URL?
@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 statuses: Set<StatusData>?
@NSManaged public var viewedStatuses: Set<ViewedStatus>?
@NSManaged public var accountRelationships: Set<AccountRelationship>?
@NSManaged public var lastSeenStatusId: String?
}
// 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)
@objc(addViewedStatusesObject:)
@NSManaged public func addToViewedStatuses(_ value: ViewedStatus)
@objc(removeViewedStatusesObject:)
@NSManaged public func removeFromViewedStatuses(_ value: ViewedStatus)
@objc(addViewedStatuses:)
@NSManaged public func addToViewedStatuses(_ values: NSSet)
@objc(removeViewedStatuses:)
@NSManaged public func removeFromViewedStatuses(_ values: NSSet)
@objc(addAccountRelationshipsObject:)
@NSManaged public func addToAccountRelationships(_ value: AccountRelationship)
@objc(removeAccountRelationshipsObject:)
@NSManaged public func removeFromVAccountRelationships(_ value: AccountRelationship)
@objc(addAccountRelationships:)
@NSManaged public func addToAccountRelationships(_ values: NSSet)
@objc(removeAccountRelationships:)
@NSManaged public func removeFromAccountRelationships(_ values: NSSet)
}
extension AccountData: Identifiable {
}

166
CoreData/AccountData.swift Normal file
View File

@ -0,0 +1,166 @@
//
// https://mczachurski.dev
// Copyright © 2023 Marcin Czachurski and the repository contributors.
// Licensed under the Apache License 2.0.
//
import Foundation
import SwiftData
import ClientKit
@Model final public class AccountData {
@Attribute(.unique) public var id: String
/// Access token to the server API.
public var accessToken: String?
/// Refresh token which can be used to download new access token.
public var refreshToken: String?
/// Full user name (user name with server address).
public var acct: String
/// URL to user avatar.
public var avatar: URL?
/// Avatar downloaded from server (visible mainly in top navigation bar).
@Attribute(.externalStorage) public var avatarData: Data?
/// Id of OAuth client.
public var clientId: String
/// Secret of OAutch client.
public var clientSecret: String
/// Vapid key of OAuth client.
public var clientVapidKey: String
/// Date of creating user.
public var createdAt: String
/// Human readable user name.
public var displayName: String?
/// Number of followers.
public var followersCount: Int32
/// Number of following users.
public var followingCount: Int32
/// URL to header image visible on user profile.
public var header: URL?
/// User profile is locked.
public var locked: Bool
/// Description on user profile.
public var note: String?
/// Address to server.
public var serverUrl: URL
/// NUmber of statuses added by the user.
public var statusesCount: Int32
/// Url to user profile.
public var url: URL?
/// User name (without server address).
public var username: String
/// Last status seen on home timeline by the user.
public var lastSeenStatusId: String?
/// Last status loaded on home timeline.
public var lastLoadedStatusId: String?
/// JSON string with last objects loaded into home timeline.
public var timelineCache: String?
/// Last notification seen by the user.
public var lastSeenNotificationId: String?
@Relationship(deleteRule: .cascade, inverse: \ViewedStatus.pixelfedAccount) public var viewedStatuses: [ViewedStatus]
@Relationship(deleteRule: .cascade, inverse: \AccountRelationship.pixelfedAccount) public var accountRelationships: [AccountRelationship]
init(
accessToken: String? = nil,
refreshToken: String? = nil,
acct: String = "",
avatar: URL? = nil,
avatarData: Data? = nil,
clientId: String = "",
clientSecret: String = "",
clientVapidKey: String = "",
createdAt: String = "",
displayName: String? = nil,
followersCount: Int32 = .zero,
followingCount: Int32 = .zero,
header: URL? = nil,
id: String = "",
locked: Bool = false,
note: String? = nil,
serverUrl: URL,
statusesCount: Int32 = .zero,
url: URL? = nil,
username: String = "",
viewedStatuses: [ViewedStatus] = [],
accountRelationships: [AccountRelationship] = [],
lastSeenStatusId: String? = nil
) {
self.accessToken = accessToken
self.refreshToken = refreshToken
self.acct = acct
self.avatar = avatar
self.avatarData = avatarData
self.clientId = clientId
self.clientSecret = clientSecret
self.clientVapidKey = clientVapidKey
self.createdAt = createdAt
self.displayName = displayName
self.followersCount = followersCount
self.followingCount = followingCount
self.header = header
self.id = id
self.locked = locked
self.note = note
self.serverUrl = serverUrl
self.statusesCount = statusesCount
self.url = url
self.username = username
self.viewedStatuses = viewedStatuses
self.accountRelationships = accountRelationships
self.lastSeenStatusId = lastSeenStatusId
}
}
extension AccountData: Identifiable {
}
extension AccountData {
func toAccountModel() -> AccountModel {
let accountModel = AccountModel(id: self.id,
accessToken: self.accessToken,
refreshToken: self.refreshToken,
acct: self.acct,
avatar: self.avatar,
clientId: self.clientId,
clientSecret: self.clientSecret,
clientVapidKey: self.clientVapidKey,
createdAt: self.createdAt,
displayName: self.displayName,
followersCount: self.followersCount,
followingCount: self.followingCount,
header: self.header,
locked: self.locked,
note: self.note,
serverUrl: self.serverUrl,
statusesCount: self.statusesCount,
url: self.url,
username: self.username,
lastSeenStatusId: self.lastSeenStatusId,
lastSeenNotificationId: self.lastSeenNotificationId,
avatarData: self.avatarData)
return accountModel
}
}

View File

@ -5,26 +5,29 @@
//
import Foundation
import CoreData
import SwiftData
import PixelfedKit
import EnvironmentKit
class AccountDataHandler {
public static let shared = AccountDataHandler()
private init() { }
func getAccountsData(viewContext: NSManagedObjectContext? = nil) -> [AccountData] {
let context = viewContext ?? CoreDataHandler.shared.container.viewContext
let fetchRequest = AccountData.fetchRequest()
func getAccountsData(modelContext: ModelContext) -> [AccountData] {
do {
return try context.fetch(fetchRequest)
var fetchDescriptor = FetchDescriptor<AccountData>()
fetchDescriptor.includePendingChanges = true
return try modelContext.fetch(fetchDescriptor)
} catch {
CoreDataError.shared.handle(error, message: "Accounts cannot be retrieved (getAccountsData).")
return []
}
}
func getCurrentAccountData(viewContext: NSManagedObjectContext? = nil) -> AccountData? {
let accounts = self.getAccountsData(viewContext: viewContext)
let defaultSettings = ApplicationSettingsHandler.shared.get()
func getCurrentAccountData(modelContext: ModelContext) -> AccountData? {
let accounts = self.getAccountsData(modelContext: modelContext)
let defaultSettings = ApplicationSettingsHandler.shared.get(modelContext: modelContext)
let currentAccount = accounts.first { accountData in
accountData.id == defaultSettings.currentAccount
@ -37,34 +40,69 @@ 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)
func getAccountData(accountId: String, modelContext: ModelContext) -> AccountData? {
do {
return try context.fetch(fetchRequest).first
var fetchDescriptor = FetchDescriptor<AccountData>(
predicate: #Predicate { $0.id == accountId}
)
fetchDescriptor.fetchLimit = 1
fetchDescriptor.includePendingChanges = true
return try modelContext.fetch(fetchDescriptor).first
} catch {
CoreDataError.shared.handle(error, message: "Error during fetching status (getAccountData).")
return nil
}
}
func remove(accountData: AccountData) {
let context = CoreDataHandler.shared.container.viewContext
context.delete(accountData)
func remove(accountData: AccountData, modelContext: ModelContext) {
do {
try context.save()
modelContext.delete(accountData)
try modelContext.save()
} catch {
CoreDataError.shared.handle(error, message: "Error during deleting account data (remove).")
}
}
func update(lastSeenStatusId: String?, lastLoadedStatusId: String?, statuses: [Status]? = nil, applicationState: ApplicationState, modelContext: ModelContext) throws {
guard let accountId = applicationState.account?.id else {
return
}
guard let accountDataFromDb = self.getAccountData(accountId: accountId, modelContext: modelContext) else {
return
}
if (accountDataFromDb.lastSeenStatusId ?? "0") < (lastSeenStatusId ?? "0") {
accountDataFromDb.lastSeenStatusId = lastSeenStatusId
applicationState.lastSeenStatusId = lastSeenStatusId
}
func createAccountDataEntity(viewContext: NSManagedObjectContext? = nil) -> AccountData {
let context = viewContext ?? CoreDataHandler.shared.container.viewContext
return AccountData(context: context)
if (accountDataFromDb.lastLoadedStatusId ?? "0") < (lastLoadedStatusId ?? "0") {
accountDataFromDb.lastLoadedStatusId = lastLoadedStatusId
}
if let statuses, let statusesJsonData = try? JSONEncoder().encode(statuses) {
accountDataFromDb.timelineCache = String(data: statusesJsonData, encoding: .utf8)
}
try modelContext.save()
}
func update(lastSeenNotificationId: String?, applicationState: ApplicationState, modelContext: ModelContext) throws {
guard let accountId = applicationState.account?.id else {
return
}
guard let accountDataFromDb = self.getAccountData(accountId: accountId, modelContext: modelContext) else {
return
}
if (accountDataFromDb.lastSeenNotificationId ?? "0") < (lastSeenNotificationId ?? "0") {
accountDataFromDb.lastSeenNotificationId = lastSeenNotificationId
applicationState.lastSeenNotificationId = lastSeenNotificationId
}
try modelContext.save()
}
}

View File

@ -1,12 +0,0 @@
//
// https://mczachurski.dev
// Copyright © 2023 Marcin Czachurski and the repository contributors.
// Licensed under the Apache License 2.0.
//
import Foundation
import CoreData
@objc(AccountRelationship)
public class AccountRelationship: NSManagedObject {
}

View File

@ -1,22 +0,0 @@
//
// https://mczachurski.dev
// Copyright © 2023 Marcin Czachurski and the repository contributors.
// Licensed under the Apache License 2.0.
//
import Foundation
import CoreData
extension AccountRelationship {
@nonobjc public class func fetchRequest() -> NSFetchRequest<AccountRelationship> {
return NSFetchRequest<AccountRelationship>(entityName: "AccountRelationship")
}
@NSManaged public var accountId: String
@NSManaged public var boostedStatusesMuted: Bool
@NSManaged public var pixelfedAccount: AccountData
}
extension AccountRelationship: Identifiable {
}

View File

@ -0,0 +1,23 @@
//
// https://mczachurski.dev
// Copyright © 2023 Marcin Czachurski and the repository contributors.
// Licensed under the Apache License 2.0.
//
import Foundation
import SwiftData
@Model final public class AccountRelationship {
@Attribute(.unique) public var accountId: String
public var boostedStatusesMuted: Bool
public var pixelfedAccount: AccountData?
init(accountId: String, boostedStatusesMuted: Bool, pixelfedAccount: AccountData? = nil) {
self.accountId = accountId
self.boostedStatusesMuted = boostedStatusesMuted
self.pixelfedAccount = pixelfedAccount
}
}
extension AccountRelationship: Identifiable {
}

View File

@ -5,65 +5,74 @@
//
import Foundation
import CoreData
import PixelfedKit
import SwiftData
class AccountRelationshipHandler {
public static let shared = AccountRelationshipHandler()
private init() { }
func createAccountRelationshipEntity(viewContext: NSManagedObjectContext? = nil) -> AccountRelationship {
let context = viewContext ?? CoreDataHandler.shared.container.viewContext
return AccountRelationship(context: context)
func getAccountRelationships(for accountId: String, modelContext: ModelContext) -> [AccountRelationship] {
do {
var fetchDescriptor = FetchDescriptor<AccountRelationship>(
predicate: #Predicate { $0.pixelfedAccount?.id == accountId }
)
fetchDescriptor.includePendingChanges = true
return try modelContext.fetch(fetchDescriptor)
} catch {
CoreDataError.shared.handle(error, message: "Error during fetching account relationship (isBoostedMutedForAccount).")
return []
}
}
/// Check if boosted statuses from given account are muted.
func isBoostedStatusesMuted(accountId: String, status: Status, viewContext: NSManagedObjectContext? = nil) -> Bool {
func isBoostedStatusesMuted(accountId: String, status: Status, modelContext: ModelContext) -> Bool {
if status.reblog == nil {
return false
}
let accountRelationship = self.getAccountRelationship(for: accountId, relation: status.account.id, viewContext: viewContext)
let accountRelationship = self.getAccountRelationship(for: accountId, relation: status.account.id, modelContext: modelContext)
return accountRelationship?.boostedStatusesMuted ?? false
}
func isBoostedStatusesMuted(for accountId: String, relation relationAccountId: String, viewContext: NSManagedObjectContext? = nil) -> Bool {
let accountRelationship = self.getAccountRelationship(for: accountId, relation: relationAccountId, viewContext: viewContext)
func isBoostedStatusesMuted(for accountId: String, relation relationAccountId: String, modelContext: ModelContext) -> Bool {
let accountRelationship = self.getAccountRelationship(for: accountId, relation: relationAccountId, modelContext: modelContext)
return accountRelationship?.boostedStatusesMuted ?? false
}
func setBoostedStatusesMuted(for accountId: String, relation relationAccountId: String, boostedStatusesMuted: Bool, viewContext: NSManagedObjectContext? = nil) {
let context = viewContext ?? CoreDataHandler.shared.container.viewContext
var accountRelationship = self.getAccountRelationship(for: accountId, relation: relationAccountId, viewContext: context)
func setBoostedStatusesMuted(for accountId: String, relation relationAccountId: String, boostedStatusesMuted: Bool, modelContext: ModelContext) {
var accountRelationship = self.getAccountRelationship(for: accountId, relation: relationAccountId, modelContext: modelContext)
if accountRelationship == nil {
guard let accountDataFromDb = AccountDataHandler.shared.getAccountData(accountId: accountId, viewContext: context) else {
guard let accountDataFromDb = AccountDataHandler.shared.getAccountData(accountId: accountId, modelContext: modelContext) else {
return
}
let newAccountRelationship = AccountRelationshipHandler.shared.createAccountRelationshipEntity(viewContext: context)
newAccountRelationship.accountId = relationAccountId
newAccountRelationship.pixelfedAccount = accountDataFromDb
accountDataFromDb.addToAccountRelationships(newAccountRelationship)
let newAccountRelationship = AccountRelationship(accountId: relationAccountId, boostedStatusesMuted: false, pixelfedAccount: accountDataFromDb)
modelContext.insert(newAccountRelationship)
accountDataFromDb.accountRelationships.append(newAccountRelationship)
accountRelationship = newAccountRelationship
}
accountRelationship?.boostedStatusesMuted = boostedStatusesMuted
CoreDataHandler.shared.save(viewContext: context)
do {
try modelContext.save()
} catch {
CoreDataError.shared.handle(error, message: "Error during saving boosted muted statuses.")
}
}
private func getAccountRelationship(for accountId: String, relation relationAccountId: String, viewContext: NSManagedObjectContext? = nil) -> AccountRelationship? {
let context = viewContext ?? CoreDataHandler.shared.container.viewContext
let fetchRequest = AccountRelationship.fetchRequest()
fetchRequest.fetchLimit = 1
let statusAccountIddPredicate = NSPredicate(format: "accountId = %@", relationAccountId)
let accountPredicate = NSPredicate(format: "pixelfedAccount.id = %@", accountId)
fetchRequest.predicate = NSCompoundPredicate.init(type: .and, subpredicates: [statusAccountIddPredicate, accountPredicate])
private func getAccountRelationship(for accountId: String, relation relationAccountId: String, modelContext: ModelContext) -> AccountRelationship? {
do {
return try context.fetch(fetchRequest).first
var fetchDescriptor = FetchDescriptor<AccountRelationship>(
predicate: #Predicate { $0.accountId == relationAccountId && $0.pixelfedAccount?.id == accountId }
)
fetchDescriptor.fetchLimit = 1
fetchDescriptor.includePendingChanges = true
return try modelContext.fetch(fetchDescriptor).first
} catch {
CoreDataError.shared.handle(error, message: "Error during fetching account relationship (isBoostedMutedForAccount).")
return nil

View File

@ -1,12 +0,0 @@
//
// https://mczachurski.dev
// Copyright © 2023 Marcin Czachurski and the repository contributors.
// Licensed under the Apache License 2.0.
//
import Foundation
import CoreData
@objc(ApplicationSettings)
public class ApplicationSettings: NSManagedObject {
}

View File

@ -1,46 +0,0 @@
//
// https://mczachurski.dev
// Copyright © 2023 Marcin Czachurski and the repository contributors.
// Licensed under the Apache License 2.0.
//
import Foundation
import CoreData
extension ApplicationSettings {
@nonobjc public class func fetchRequest() -> NSFetchRequest<ApplicationSettings> {
return NSFetchRequest<ApplicationSettings>(entityName: "ApplicationSettings")
}
@NSManaged public var currentAccount: String?
@NSManaged public var theme: Int32
@NSManaged public var tintColor: Int32
@NSManaged public var avatarShape: Int32
@NSManaged public var activeIcon: String
@NSManaged public var lastRefreshTokens: Date
@NSManaged public var hapticTabSelectionEnabled: Bool
@NSManaged public var hapticRefreshEnabled: Bool
@NSManaged public var hapticButtonPressEnabled: Bool
@NSManaged public var hapticAnimationEnabled: Bool
@NSManaged public var hapticNotificationEnabled: Bool
@NSManaged public var showSensitive: Bool
@NSManaged public var showPhotoDescription: Bool
@NSManaged public var menuPosition: Int32
@NSManaged public var showAvatarsOnTimeline: Bool
@NSManaged public var showFavouritesOnTimeline: Bool
@NSManaged public var showAltIconOnTimeline: Bool
@NSManaged public var warnAboutMissingAlt: Bool
@NSManaged public var showGridOnUserProfile: Bool
@NSManaged public var showReboostedStatuses: Bool
@NSManaged public var hideStatusesWithoutAlt: Bool
@NSManaged public var customNavigationMenuItem1: Int32
@NSManaged public var customNavigationMenuItem2: Int32
@NSManaged public var customNavigationMenuItem3: Int32
}
extension ApplicationSettings: Identifiable {
}

View File

@ -0,0 +1,97 @@
//
// https://mczachurski.dev
// Copyright © 2023 Marcin Czachurski and the repository contributors.
// Licensed under the Apache License 2.0.
//
import Foundation
import SwiftData
import EnvironmentKit
@Model final public class ApplicationSettings {
public var currentAccount: String? = nil
public var theme: Int32 = Int32(Theme.system.rawValue)
public var tintColor: Int32 = Int32(TintColor.accentColor2.rawValue)
public var avatarShape: Int32 = Int32(AvatarShape.circle.rawValue)
public var activeIcon: String = "Default"
public var lastRefreshTokens: Date = Date.distantPast
public var hapticTabSelectionEnabled: Bool = true
public var hapticRefreshEnabled: Bool = true
public var hapticButtonPressEnabled: Bool = true
public var hapticAnimationEnabled: Bool = true
public var hapticNotificationEnabled: Bool = true
public var showSensitive: Bool = false
public var showApplicationBadge: Bool = false
public var showPhotoDescription: Bool = false
public var menuPosition: Int32 = Int32(MenuPosition.top.rawValue)
public var showAvatarsOnTimeline: Bool = false
public var showFavouritesOnTimeline: Bool = false
public var showAltIconOnTimeline: Bool = false
public var warnAboutMissingAlt: Bool = true
public var showGridOnUserProfile: Bool = false
public var showReboostedStatuses: Bool = false
public var hideStatusesWithoutAlt: Bool = false
public var customNavigationMenuItem1: Int32 = 1
public var customNavigationMenuItem2: Int32 = 2
public var customNavigationMenuItem3: Int32 = 5
init(
currentAccount: String? = nil,
theme: Int32 = Int32(Theme.system.rawValue),
tintColor: Int32 = Int32(TintColor.accentColor2.rawValue),
avatarShape: Int32 = Int32(AvatarShape.circle.rawValue),
activeIcon: String = "Default",
lastRefreshTokens: Date = Date.distantPast,
hapticTabSelectionEnabled: Bool = true,
hapticRefreshEnabled: Bool = true,
hapticButtonPressEnabled: Bool = true,
hapticAnimationEnabled: Bool = true,
hapticNotificationEnabled: Bool = true,
showSensitive: Bool = false,
showApplicationBadge: Bool = false,
showPhotoDescription: Bool = false,
menuPosition: Int32 = Int32(MenuPosition.top.rawValue),
showAvatarsOnTimeline: Bool = false,
showFavouritesOnTimeline: Bool = false,
showAltIconOnTimeline: Bool = false,
warnAboutMissingAlt: Bool = true,
showGridOnUserProfile: Bool = false,
showReboostedStatuses: Bool = false,
hideStatusesWithoutAlt: Bool = false,
customNavigationMenuItem1: Int32 = 1,
customNavigationMenuItem2: Int32 = 2,
customNavigationMenuItem3: Int32 = 5
) {
self.currentAccount = currentAccount
self.theme = theme
self.tintColor = tintColor
self.avatarShape = avatarShape
self.activeIcon = activeIcon
self.lastRefreshTokens = lastRefreshTokens
self.hapticTabSelectionEnabled = hapticTabSelectionEnabled
self.hapticRefreshEnabled = hapticRefreshEnabled
self.hapticButtonPressEnabled = hapticButtonPressEnabled
self.hapticAnimationEnabled = hapticAnimationEnabled
self.hapticNotificationEnabled = hapticNotificationEnabled
self.showSensitive = showSensitive
self.showApplicationBadge = showApplicationBadge
self.showPhotoDescription = showPhotoDescription
self.menuPosition = menuPosition
self.showAvatarsOnTimeline = showAvatarsOnTimeline
self.showFavouritesOnTimeline = showFavouritesOnTimeline
self.showAltIconOnTimeline = showAltIconOnTimeline
self.warnAboutMissingAlt = warnAboutMissingAlt
self.showGridOnUserProfile = showGridOnUserProfile
self.showReboostedStatuses = showReboostedStatuses
self.hideStatusesWithoutAlt = hideStatusesWithoutAlt
self.customNavigationMenuItem1 = customNavigationMenuItem1
self.customNavigationMenuItem2 = customNavigationMenuItem2
self.customNavigationMenuItem3 = customNavigationMenuItem3
}
}
extension ApplicationSettings: Identifiable {
}

View File

@ -5,20 +5,21 @@
//
import Foundation
import CoreData
import EnvironmentKit
import SwiftData
class ApplicationSettingsHandler {
public static let shared = ApplicationSettingsHandler()
private init() { }
func get(viewContext: NSManagedObjectContext? = nil) -> ApplicationSettings {
func get(modelContext: ModelContext) -> ApplicationSettings {
var settingsList: [ApplicationSettings] = []
let context = viewContext ?? CoreDataHandler.shared.container.viewContext
let fetchRequest = ApplicationSettings.fetchRequest()
do {
settingsList = try context.fetch(fetchRequest)
var fetchDescriptor = FetchDescriptor<ApplicationSettings>()
fetchDescriptor.includePendingChanges = true
settingsList = try modelContext.fetch(fetchDescriptor)
} catch {
CoreDataError.shared.handle(error, message: "Error during fetching application settings.")
}
@ -26,18 +27,23 @@ class ApplicationSettingsHandler {
if let settings = settingsList.first {
return settings
} else {
let settings = self.createApplicationSettingsEntity(viewContext: context)
settings.avatarShape = Int32(AvatarShape.circle.rawValue)
settings.theme = Int32(Theme.system.rawValue)
settings.tintColor = Int32(TintColor.accentColor2.rawValue)
CoreDataHandler.shared.save(viewContext: context)
do {
let settings = ApplicationSettings()
modelContext.insert(settings)
return settings
try modelContext.save()
return settings
} catch {
CoreDataError.shared.handle(error, message: "Error during saving new application settings.")
let settings = ApplicationSettings()
return settings
}
}
}
func update(applicationState: ApplicationState) {
let defaultSettings = ApplicationSettingsHandler.shared.get()
func update(applicationState: ApplicationState, modelContext: ModelContext) {
let defaultSettings = ApplicationSettingsHandler.shared.get(modelContext: modelContext)
if let tintColor = TintColor(rawValue: Int(defaultSettings.tintColor)) {
applicationState.tintColor = tintColor
@ -61,6 +67,7 @@ class ApplicationSettingsHandler {
applicationState.showGridOnUserProfile = defaultSettings.showGridOnUserProfile
applicationState.showReboostedStatuses = defaultSettings.showReboostedStatuses
applicationState.hideStatusesWithoutAlt = defaultSettings.hideStatusesWithoutAlt
applicationState.showApplicationBadge = defaultSettings.showApplicationBadge
if let menuPosition = MenuPosition(rawValue: Int(defaultSettings.menuPosition)) {
applicationState.menuPosition = menuPosition
@ -73,146 +80,147 @@ class ApplicationSettingsHandler {
applicationState.hapticNotificationEnabled = defaultSettings.hapticNotificationEnabled
}
func set(accountId: String?) {
let defaultSettings = self.get()
func set(accountId: String?, modelContext: ModelContext) {
let defaultSettings = self.get(modelContext: modelContext)
defaultSettings.currentAccount = accountId
CoreDataHandler.shared.save()
try? modelContext.save()
}
func set(tintColor: TintColor) {
let defaultSettings = self.get()
func set(tintColor: TintColor, modelContext: ModelContext) {
let defaultSettings = self.get(modelContext: modelContext)
defaultSettings.tintColor = Int32(tintColor.rawValue)
CoreDataHandler.shared.save()
try? modelContext.save()
}
func set(theme: Theme) {
let defaultSettings = self.get()
func set(theme: Theme, modelContext: ModelContext) {
let defaultSettings = self.get(modelContext: modelContext)
defaultSettings.theme = Int32(theme.rawValue)
CoreDataHandler.shared.save()
try? modelContext.save()
}
func set(avatarShape: AvatarShape) {
let defaultSettings = self.get()
func set(avatarShape: AvatarShape, modelContext: ModelContext) {
let defaultSettings = self.get(modelContext: modelContext)
defaultSettings.avatarShape = Int32(avatarShape.rawValue)
CoreDataHandler.shared.save()
try? modelContext.save()
}
func set(hapticTabSelectionEnabled: Bool) {
let defaultSettings = self.get()
func set(hapticTabSelectionEnabled: Bool, modelContext: ModelContext) {
let defaultSettings = self.get(modelContext: modelContext)
defaultSettings.hapticTabSelectionEnabled = hapticTabSelectionEnabled
CoreDataHandler.shared.save()
try? modelContext.save()
}
func set(hapticRefreshEnabled: Bool) {
let defaultSettings = self.get()
func set(hapticRefreshEnabled: Bool, modelContext: ModelContext) {
let defaultSettings = self.get(modelContext: modelContext)
defaultSettings.hapticRefreshEnabled = hapticRefreshEnabled
CoreDataHandler.shared.save()
try? modelContext.save()
}
func set(hapticAnimationEnabled: Bool) {
let defaultSettings = self.get()
func set(hapticAnimationEnabled: Bool, modelContext: ModelContext) {
let defaultSettings = self.get(modelContext: modelContext)
defaultSettings.hapticAnimationEnabled = hapticAnimationEnabled
CoreDataHandler.shared.save()
try? modelContext.save()
}
func set(hapticNotificationEnabled: Bool) {
let defaultSettings = self.get()
func set(hapticNotificationEnabled: Bool, modelContext: ModelContext) {
let defaultSettings = self.get(modelContext: modelContext)
defaultSettings.hapticNotificationEnabled = hapticNotificationEnabled
CoreDataHandler.shared.save()
try? modelContext.save()
}
func set(hapticButtonPressEnabled: Bool) {
let defaultSettings = self.get()
func set(hapticButtonPressEnabled: Bool, modelContext: ModelContext) {
let defaultSettings = self.get(modelContext: modelContext)
defaultSettings.hapticButtonPressEnabled = hapticButtonPressEnabled
CoreDataHandler.shared.save()
try? modelContext.save()
}
func set(showSensitive: Bool) {
let defaultSettings = self.get()
func set(showSensitive: Bool, modelContext: ModelContext) {
let defaultSettings = self.get(modelContext: modelContext)
defaultSettings.showSensitive = showSensitive
CoreDataHandler.shared.save()
}
func set(showPhotoDescription: Bool) {
let defaultSettings = self.get()
defaultSettings.showPhotoDescription = showPhotoDescription
CoreDataHandler.shared.save()
}
func set(activeIcon: String) {
let defaultSettings = self.get()
defaultSettings.activeIcon = activeIcon
CoreDataHandler.shared.save()
}
func set(menuPosition: MenuPosition) {
let defaultSettings = self.get()
defaultSettings.menuPosition = Int32(menuPosition.rawValue)
CoreDataHandler.shared.save()
}
func set(showAvatarsOnTimeline: Bool) {
let defaultSettings = self.get()
defaultSettings.showAvatarsOnTimeline = showAvatarsOnTimeline
CoreDataHandler.shared.save()
}
func set(showFavouritesOnTimeline: Bool) {
let defaultSettings = self.get()
defaultSettings.showFavouritesOnTimeline = showFavouritesOnTimeline
CoreDataHandler.shared.save()
}
func set(showAltIconOnTimeline: Bool) {
let defaultSettings = self.get()
defaultSettings.showAltIconOnTimeline = showAltIconOnTimeline
CoreDataHandler.shared.save()
}
func set(warnAboutMissingAlt: Bool) {
let defaultSettings = self.get()
defaultSettings.warnAboutMissingAlt = warnAboutMissingAlt
CoreDataHandler.shared.save()
}
func set(customNavigationMenuItem1: Int) {
let defaultSettings = self.get()
defaultSettings.customNavigationMenuItem1 = Int32(customNavigationMenuItem1)
CoreDataHandler.shared.save()
}
func set(customNavigationMenuItem2: Int) {
let defaultSettings = self.get()
defaultSettings.customNavigationMenuItem2 = Int32(customNavigationMenuItem2)
CoreDataHandler.shared.save()
}
func set(customNavigationMenuItem3: Int) {
let defaultSettings = self.get()
defaultSettings.customNavigationMenuItem3 = Int32(customNavigationMenuItem3)
CoreDataHandler.shared.save()
}
func set(showGridOnUserProfile: Bool) {
let defaultSettings = self.get()
defaultSettings.showGridOnUserProfile = showGridOnUserProfile
CoreDataHandler.shared.save()
}
func set(showReboostedStatuses: Bool) {
let defaultSettings = self.get()
defaultSettings.showReboostedStatuses = showReboostedStatuses
CoreDataHandler.shared.save()
}
func set(hideStatusesWithoutAlt: Bool) {
let defaultSettings = self.get()
defaultSettings.hideStatusesWithoutAlt = hideStatusesWithoutAlt
CoreDataHandler.shared.save()
try? modelContext.save()
}
private func createApplicationSettingsEntity(viewContext: NSManagedObjectContext? = nil) -> ApplicationSettings {
let context = viewContext ?? CoreDataHandler.shared.container.viewContext
return ApplicationSettings(context: context)
func set(showApplicationBadge: Bool, modelContext: ModelContext) {
let defaultSettings = self.get(modelContext: modelContext)
defaultSettings.showApplicationBadge = showApplicationBadge
try? modelContext.save()
}
func set(showPhotoDescription: Bool, modelContext: ModelContext) {
let defaultSettings = self.get(modelContext: modelContext)
defaultSettings.showPhotoDescription = showPhotoDescription
try? modelContext.save()
}
func set(activeIcon: String, modelContext: ModelContext) {
let defaultSettings = self.get(modelContext: modelContext)
defaultSettings.activeIcon = activeIcon
try? modelContext.save()
}
func set(menuPosition: MenuPosition, modelContext: ModelContext) {
let defaultSettings = self.get(modelContext: modelContext)
defaultSettings.menuPosition = Int32(menuPosition.rawValue)
try? modelContext.save()
}
func set(showAvatarsOnTimeline: Bool, modelContext: ModelContext) {
let defaultSettings = self.get(modelContext: modelContext)
defaultSettings.showAvatarsOnTimeline = showAvatarsOnTimeline
try? modelContext.save()
}
func set(showFavouritesOnTimeline: Bool, modelContext: ModelContext) {
let defaultSettings = self.get(modelContext: modelContext)
defaultSettings.showFavouritesOnTimeline = showFavouritesOnTimeline
try? modelContext.save()
}
func set(showAltIconOnTimeline: Bool, modelContext: ModelContext) {
let defaultSettings = self.get(modelContext: modelContext)
defaultSettings.showAltIconOnTimeline = showAltIconOnTimeline
try? modelContext.save()
}
func set(warnAboutMissingAlt: Bool, modelContext: ModelContext) {
let defaultSettings = self.get(modelContext: modelContext)
defaultSettings.warnAboutMissingAlt = warnAboutMissingAlt
try? modelContext.save()
}
func set(customNavigationMenuItem1: Int, modelContext: ModelContext) {
let defaultSettings = self.get(modelContext: modelContext)
defaultSettings.customNavigationMenuItem1 = Int32(customNavigationMenuItem1)
try? modelContext.save()
}
func set(customNavigationMenuItem2: Int, modelContext: ModelContext) {
let defaultSettings = self.get(modelContext: modelContext)
defaultSettings.customNavigationMenuItem2 = Int32(customNavigationMenuItem2)
try? modelContext.save()
}
func set(customNavigationMenuItem3: Int, modelContext: ModelContext) {
let defaultSettings = self.get(modelContext: modelContext)
defaultSettings.customNavigationMenuItem3 = Int32(customNavigationMenuItem3)
try? modelContext.save()
}
func set(showGridOnUserProfile: Bool, modelContext: ModelContext) {
let defaultSettings = self.get(modelContext: modelContext)
defaultSettings.showGridOnUserProfile = showGridOnUserProfile
try? modelContext.save()
}
func set(showReboostedStatuses: Bool, modelContext: ModelContext) {
let defaultSettings = self.get(modelContext: modelContext)
defaultSettings.showReboostedStatuses = showReboostedStatuses
try? modelContext.save()
}
func set(hideStatusesWithoutAlt: Bool, modelContext: ModelContext) {
let defaultSettings = self.get(modelContext: modelContext)
defaultSettings.hideStatusesWithoutAlt = hideStatusesWithoutAlt
try? modelContext.save()
}
}

View File

@ -1,30 +0,0 @@
//
// https://mczachurski.dev
// Copyright © 2023 Marcin Czachurski and the repository contributors.
// Licensed under the Apache License 2.0.
//
import Foundation
import PixelfedKit
extension AttachmentData {
func copyFrom(_ attachment: MediaAttachment) {
self.id = attachment.id
self.url = attachment.url
self.blurhash = attachment.blurhash
self.previewUrl = attachment.previewUrl
self.remoteUrl = attachment.remoteUrl
self.text = attachment.description
self.type = attachment.type.rawValue
// We can set image width only when it wasn't previusly recalculated.
if let width = (attachment.meta as? ImageMetadata)?.original?.width, self.metaImageWidth <= 0 && width > 0 {
self.metaImageWidth = Int32(width)
}
// We can set image height only when it wasn't previusly recalculated.
if let height = (attachment.meta as? ImageMetadata)?.original?.height, self.metaImageHeight <= 0 && height > 0 {
self.metaImageHeight = Int32(height)
}
}
}

View File

@ -1,13 +0,0 @@
//
// https://mczachurski.dev
// Copyright © 2023 Marcin Czachurski and the repository contributors.
// Licensed under the Apache License 2.0.
//
import Foundation
extension AttachmentData: Comparable {
public static func < (lhs: AttachmentData, rhs: AttachmentData) -> Bool {
lhs.id < rhs.id
}
}

View File

@ -1,12 +0,0 @@
//
// https://mczachurski.dev
// Copyright © 2023 Marcin Czachurski and the repository contributors.
// Licensed under the Apache License 2.0.
//
import Foundation
import CoreData
@objc(AttachmentData)
public class AttachmentData: NSManagedObject {
}

View File

@ -1,36 +0,0 @@
//
// https://mczachurski.dev
// Copyright © 2023 Marcin Czachurski and the repository contributors.
// Licensed under the Apache License 2.0.
//
import Foundation
import CoreData
extension AttachmentData {
@nonobjc public class func fetchRequest() -> NSFetchRequest<AttachmentData> {
return NSFetchRequest<AttachmentData>(entityName: "AttachmentData")
}
@NSManaged public var blurhash: String?
@NSManaged public var data: Data?
@NSManaged public var exifCamera: String?
@NSManaged public var exifCreatedDate: String?
@NSManaged public var exifExposure: String?
@NSManaged public var exifLens: String?
@NSManaged public var id: String
@NSManaged public var previewUrl: URL?
@NSManaged public var remoteUrl: URL?
@NSManaged public var statusId: String
@NSManaged public var text: String?
@NSManaged public var type: String
@NSManaged public var url: URL
@NSManaged public var metaImageWidth: Int32
@NSManaged public var metaImageHeight: Int32
@NSManaged public var order: Int32
@NSManaged public var statusRelation: StatusData?
}
extension AttachmentData: Identifiable {
}

View File

@ -1,13 +0,0 @@
//
// https://mczachurski.dev
// Copyright © 2023 Marcin Czachurski and the repository contributors.
// Licensed under the Apache License 2.0.
//
import Foundation
extension AttachmentData {
func isFaulty() -> Bool {
return self.isDeleted || self.isFault
}
}

View File

@ -1,24 +0,0 @@
//
// https://mczachurski.dev
// Copyright © 2023 Marcin Czachurski and the repository contributors.
// Licensed under the Apache License 2.0.
//
import Foundation
extension [AttachmentData] {
func getHighestImage() -> AttachmentData? {
var attachment = self.first
var imgHeight = 0.0
for item in self {
let attachmentheight = Double(item.metaImageHeight)
if attachmentheight > imgHeight {
attachment = item
imgHeight = attachmentheight
}
}
return attachment
}
}

View File

@ -1,38 +0,0 @@
//
// https://mczachurski.dev
// Copyright © 2023 Marcin Czachurski and the repository contributors.
// Licensed under the Apache License 2.0.
//
import Foundation
import CoreData
class AttachmentDataHandler {
public static let shared = AttachmentDataHandler()
private init() { }
func createAttachmnentDataEntity(viewContext: NSManagedObjectContext? = nil) -> AttachmentData {
let context = viewContext ?? CoreDataHandler.shared.container.viewContext
return AttachmentData(context: context)
}
func getDownloadedAttachmentData(accountId: String, length: Int, viewContext: NSManagedObjectContext? = nil) -> [AttachmentData] {
let context = viewContext ?? CoreDataHandler.shared.container.viewContext
let fetchRequest = AttachmentData.fetchRequest()
fetchRequest.fetchLimit = length
let sortDescriptor = NSSortDescriptor(key: "statusRelation.id", ascending: true)
fetchRequest.sortDescriptors = [sortDescriptor]
let predicate1 = NSPredicate(format: "statusRelation.pixelfedAccount.id = %@", accountId)
let predicate2 = NSPredicate(format: "data != nil")
fetchRequest.predicate = NSCompoundPredicate.init(type: .and, subpredicates: [predicate1, predicate2])
do {
return try context.fetch(fetchRequest)
} catch {
CoreDataError.shared.handle(error, message: "Error during fetching attachment data (getDownloadedAttachmentData).")
return []
}
}
}

View File

@ -1,99 +0,0 @@
//
// https://mczachurski.dev
// Copyright © 2023 Marcin Czachurski and the repository contributors.
// Licensed under the Apache License 2.0.
//
import CoreData
import OSLog
import EnvironmentKit
public class CoreDataHandler {
public static let shared = CoreDataHandler()
lazy var container: NSPersistentContainer = {
let container = NSPersistentContainer(name: AppConstants.coreDataPersistantContainerName)
let storeURL = FileManager.default.containerURL(forSecurityApplicationGroupIdentifier: "group.dev.mczachurski.vernissage")!
.appendingPathComponent("Data.sqlite")
var defaultURL: URL?
if let storeDescription = container.persistentStoreDescriptions.first, let url = storeDescription.url {
defaultURL = FileManager.default.fileExists(atPath: url.path) ? url : nil
}
if defaultURL == nil {
container.persistentStoreDescriptions = [NSPersistentStoreDescription(url: storeURL)]
}
container.loadPersistentStores(completionHandler: { [unowned container] (_, error) in
if let error = error as NSError? {
// Replace this implementation with code to handle the error appropriately.
// fatalError() causes the application to generate a crash log and terminate.
// You should not use this function in a shipping application, although it may be useful during development.
/*
Typical reasons for an error here include:
* The parent directory does not exist, cannot be created, or disallows writing.
* The persistent store is not accessible, due to permissions or data protection when the device is locked.
* The device is out of space.
* The store could not be migrated to the current model version.
Check the error message to determine what the actual problem was.
*/
fatalError("Unresolved error \(error), \(error.userInfo)")
}
// Migrate old store do current (shared between app and widget)
if let url = defaultURL, url.absoluteString != storeURL.absoluteString {
let coordinator = container.persistentStoreCoordinator
if let oldStore = coordinator.persistentStore(for: url) {
// Migration process.
do {
try coordinator.migratePersistentStore(oldStore, to: storeURL, options: nil, withType: NSSQLiteStoreType)
} catch {
Logger.main.error("\(error.localizedDescription)")
}
// Delete old store.
let fileCoordinator = NSFileCoordinator(filePresenter: nil)
fileCoordinator.coordinate(writingItemAt: url, options: .forDeleting, error: nil, byAccessor: { url in
do {
try FileManager.default.removeItem(at: url)
} catch {
Logger.main.error("\(error.localizedDescription)")
}
})
}
}
})
container.viewContext.automaticallyMergesChangesFromParent = true
return container
}()
public func newBackgroundContext() -> NSManagedObjectContext {
self.container.newBackgroundContext()
}
public func save(viewContext: NSManagedObjectContext? = nil) {
let context = viewContext ?? CoreDataHandler.shared.container.viewContext
if context.hasChanges {
context.performAndWait {
do {
try context.save()
} catch {
// Replace this implementation with code to handle the error appropriately.
// fatalError() causes the application to generate a crash log and terminate.
// You should not use this function in a shipping application, although it may be useful during development.
#if DEBUG
let nserror = error as NSError
fatalError("Unresolved error \(nserror), \(nserror.userInfo)")
#else
CoreDataError.shared.handle(error, message: "An error occurred while writing the data.")
#endif
}
}
}
}
}

View File

@ -1,19 +0,0 @@
//
// https://mczachurski.dev
// Copyright © 2023 Marcin Czachurski and the repository contributors.
// Licensed under the Apache License 2.0.
//
import Foundation
extension StatusData {
func attachments() -> [AttachmentData] {
guard let attachments = self.attachmentsRelation else {
return []
}
return attachments.sorted(by: { lhs, rhs in
lhs.order < rhs.order
})
}
}

View File

@ -1,12 +0,0 @@
//
// https://mczachurski.dev
// Copyright © 2023 Marcin Czachurski and the repository contributors.
// Licensed under the Apache License 2.0.
//
import Foundation
import CoreData
@objc(StatusData)
public class StatusData: NSManagedObject {
}

View File

@ -1,68 +0,0 @@
//
// https://mczachurski.dev
// Copyright © 2023 Marcin Czachurski and the repository contributors.
// Licensed under the Apache License 2.0.
//
import Foundation
import CoreData
extension StatusData {
@nonobjc public class func fetchRequest() -> NSFetchRequest<StatusData> {
return NSFetchRequest<StatusData>(entityName: "StatusData")
}
@NSManaged public var accountAvatar: URL?
@NSManaged public var accountDisplayName: String?
@NSManaged public var accountId: String
@NSManaged public var accountUsername: String
@NSManaged public var applicationName: String?
@NSManaged public var applicationWebsite: URL?
@NSManaged public var bookmarked: Bool
@NSManaged public var content: String
@NSManaged public var createdAt: String
@NSManaged public var favourited: Bool
@NSManaged public var favouritesCount: Int32
@NSManaged public var id: String
@NSManaged public var inReplyToAccount: String?
@NSManaged public var inReplyToId: String?
@NSManaged public var muted: Bool
@NSManaged public var pinned: Bool
@NSManaged public var reblogged: Bool
@NSManaged public var reblogsCount: Int32
@NSManaged public var repliesCount: Int32
@NSManaged public var sensitive: Bool
@NSManaged public var spoilerText: String?
@NSManaged public var uri: String?
@NSManaged public var url: URL?
@NSManaged public var visibility: String
@NSManaged public var attachmentsRelation: Set<AttachmentData>?
@NSManaged public var pixelfedAccount: AccountData
@NSManaged public var rebloggedStatusId: String?
@NSManaged public var rebloggedAccountAvatar: URL?
@NSManaged public var rebloggedAccountDisplayName: String?
@NSManaged public var rebloggedAccountId: String?
@NSManaged public var rebloggedAccountUsername: String?
}
// MARK: Generated accessors for attachmentRelation
extension StatusData {
@objc(addAttachmentsRelationObject:)
@NSManaged public func addToAttachmentsRelation(_ value: AttachmentData)
@objc(removeAttachmentsRelationObject:)
@NSManaged public func removeFromAttachmentsRelation(_ value: AttachmentData)
@objc(addAttachmentsRelation:)
@NSManaged public func addToAttachmentsRelation(_ values: NSSet)
@objc(removeAttachmentsRelation:)
@NSManaged public func removeFromAttachmentsRelation(_ values: NSSet)
}
extension StatusData: Identifiable {
}

View File

@ -1,13 +0,0 @@
//
// https://mczachurski.dev
// Copyright © 2023 Marcin Czachurski and the repository contributors.
// Licensed under the Apache License 2.0.
//
import Foundation
extension StatusData {
func isFaulty() -> Bool {
return self.isDeleted || self.isFault
}
}

View File

@ -1,88 +0,0 @@
//
// https://mczachurski.dev
// Copyright © 2023 Marcin Czachurski and the repository contributors.
// Licensed under the Apache License 2.0.
//
import Foundation
import PixelfedKit
extension StatusData {
func copyFrom(_ status: Status) {
if let reblog = status.reblog {
self.copyFrom(reblog)
self.id = status.id
self.rebloggedStatusId = reblog.id
self.rebloggedAccountAvatar = status.account.avatar
self.rebloggedAccountDisplayName = status.account.displayName
self.rebloggedAccountId = status.account.id
self.rebloggedAccountUsername = status.account.acct
} else {
self.id = status.id
self.createdAt = status.createdAt
self.accountAvatar = status.account.avatar
self.accountDisplayName = status.account.displayName
self.accountId = status.account.id
self.accountUsername = status.account.acct
self.applicationName = status.application?.name
self.applicationWebsite = status.application?.website
self.bookmarked = status.bookmarked
self.content = status.content.htmlValue
self.favourited = status.favourited
self.favouritesCount = Int32(status.favouritesCount)
self.inReplyToAccount = status.inReplyToAccount
self.inReplyToId = status.inReplyToId
self.muted = status.muted
self.pinned = status.pinned
self.reblogged = status.reblogged
self.reblogsCount = Int32(status.reblogsCount)
self.repliesCount = Int32(status.repliesCount)
self.sensitive = status.sensitive
self.spoilerText = status.spoilerText
self.uri = status.uri
self.url = status.url
self.visibility = status.visibility.rawValue
}
}
func updateFrom(_ status: Status) {
if let reblog = status.reblog {
self.updateFrom(reblog)
self.rebloggedAccountAvatar = status.account.avatar
self.rebloggedAccountDisplayName = status.account.displayName
self.rebloggedAccountId = status.account.id
self.rebloggedAccountUsername = status.account.acct
} else {
self.accountAvatar = status.account.avatar
self.accountDisplayName = status.account.displayName
self.accountUsername = status.account.acct
self.applicationName = status.application?.name
self.applicationWebsite = status.application?.website
self.bookmarked = status.bookmarked
self.content = status.content.htmlValue
self.favourited = status.favourited
self.favouritesCount = Int32(status.favouritesCount)
self.inReplyToAccount = status.inReplyToAccount
self.inReplyToId = status.inReplyToId
self.muted = status.muted
self.pinned = status.pinned
self.reblogged = status.reblogged
self.reblogsCount = Int32(status.reblogsCount)
self.repliesCount = Int32(status.repliesCount)
self.sensitive = status.sensitive
self.spoilerText = status.spoilerText
self.uri = status.uri
self.url = status.url
self.visibility = status.visibility.rawValue
}
}
}
public extension StatusData {
func getOrginalStatusId() -> String {
return self.rebloggedStatusId ?? self.id
}
}

View File

@ -1,136 +0,0 @@
//
// https://mczachurski.dev
// Copyright © 2023 Marcin Czachurski and the repository contributors.
// Licensed under the Apache License 2.0.
//
import Foundation
import CoreData
import PixelfedKit
class StatusDataHandler {
public static let shared = StatusDataHandler()
private init() { }
func getAllStatuses(accountId: String, viewContext: NSManagedObjectContext? = nil) -> [StatusData] {
let context = viewContext ?? CoreDataHandler.shared.container.viewContext
let fetchRequest = StatusData.fetchRequest()
let sortDescriptor = NSSortDescriptor(key: "id", ascending: true)
fetchRequest.sortDescriptors = [sortDescriptor]
fetchRequest.predicate = NSPredicate(format: "pixelfedAccount.id = %@", accountId)
do {
return try context.fetch(fetchRequest)
} catch {
CoreDataError.shared.handle(error, message: "Error during fetching status (getStatusData).")
return []
}
}
func getAllOlderStatuses(accountId: String, statusId: String, viewContext: NSManagedObjectContext? = nil) -> [StatusData] {
let context = viewContext ?? CoreDataHandler.shared.container.viewContext
let fetchRequest = StatusData.fetchRequest()
let sortDescriptor = NSSortDescriptor(key: "id", ascending: true)
fetchRequest.sortDescriptors = [sortDescriptor]
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)
} catch {
CoreDataError.shared.handle(error, message: "Error during fetching status (getStatusData).")
return []
}
}
func getStatusData(accountId: String, statusId: String, viewContext: NSManagedObjectContext? = nil) -> StatusData? {
let context = viewContext ?? CoreDataHandler.shared.container.viewContext
let fetchRequest = StatusData.fetchRequest()
fetchRequest.fetchLimit = 1
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
} catch {
CoreDataError.shared.handle(error, message: "Error during fetching status (getStatusData).")
return nil
}
}
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
} catch {
CoreDataError.shared.handle(error, message: "Error during fetching maximum status (getMaximumStatus).")
return nil
}
}
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
} catch {
CoreDataError.shared.handle(error, message: "Error during fetching minimum status (getMinimumtatus).")
return nil
}
}
func remove(accountId: String, statusId: String, viewContext: NSManagedObjectContext? = nil) {
let status = self.getStatusData(accountId: accountId, statusId: statusId)
guard let status else {
return
}
let context = viewContext ?? CoreDataHandler.shared.container.viewContext
context.delete(status)
do {
try context.save()
} catch {
CoreDataError.shared.handle(error, message: "Error during deleting status (remove).")
}
}
func setFavourited(accountId: String, statusId: String) {
let backgroundContext = CoreDataHandler.shared.newBackgroundContext()
if let statusData = self.getStatusData(accountId: accountId, statusId: statusId, viewContext: backgroundContext) {
statusData.favourited = true
CoreDataHandler.shared.save(viewContext: backgroundContext)
}
}
func createStatusDataEntity(viewContext: NSManagedObjectContext? = nil) -> StatusData {
let context = viewContext ?? CoreDataHandler.shared.container.viewContext
return StatusData(context: context)
}
}

View File

@ -0,0 +1,30 @@
//
// https://mczachurski.dev
// Copyright © 2023 Marcin Czachurski and the repository contributors.
// Licensed under the Apache License 2.0.
//
import OSLog
import EnvironmentKit
import SwiftData
public class SwiftDataHandler {
public static let shared = SwiftDataHandler()
lazy var sharedModelContainer: ModelContainer = {
let schema = Schema([
ApplicationSettings.self,
AccountData.self,
ViewedStatus.self,
AccountRelationship.self
])
let modelConfiguration = ModelConfiguration(schema: schema, isStoredInMemoryOnly: false)
do {
return try ModelContainer(for: schema, configurations: [modelConfiguration])
} catch {
fatalError("Could not create ModelContainer: \(error)")
}
}()
}

View File

@ -1,8 +1,5 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>_XCCurrentVersionName</key>
<string>Vernissage-019.xcdatamodel</string>
</dict>
<dict/>
</plist>

View File

@ -1,5 +1,5 @@
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<model type="com.apple.IDECoreDataModeler.DataModel" documentVersion="1.0" lastSavedToolsVersion="22222" systemVersion="23A344" minimumToolsVersion="Automatic" sourceLanguage="Swift" userDefinedModelVersionIdentifier="">
<model type="com.apple.IDECoreDataModeler.DataModel" documentVersion="1.0" lastSavedToolsVersion="22222" systemVersion="23A344" minimumToolsVersion="Automatic" sourceLanguage="Swift" usedWithSwiftData="YES" userDefinedModelVersionIdentifier="">
<entity name="AccountData" representedClassName="AccountData" syncable="YES">
<attribute name="accessToken" optional="YES" attributeType="String"/>
<attribute name="acct" attributeType="String"/>

View File

@ -1,12 +0,0 @@
//
// https://mczachurski.dev
// Copyright © 2023 Marcin Czachurski and the repository contributors.
// Licensed under the Apache License 2.0.
//
import Foundation
import CoreData
@objc(ViewedStatus)
public class ViewedStatus: NSManagedObject {
}

View File

@ -1,23 +0,0 @@
//
// https://mczachurski.dev
// Copyright © 2023 Marcin Czachurski and the repository contributors.
// Licensed under the Apache License 2.0.
//
import Foundation
import CoreData
extension ViewedStatus {
@nonobjc public class func fetchRequest() -> NSFetchRequest<ViewedStatus> {
return NSFetchRequest<ViewedStatus>(entityName: "ViewedStatus")
}
@NSManaged public var id: String
@NSManaged public var reblogId: String?
@NSManaged public var date: Date
@NSManaged public var pixelfedAccount: AccountData
}
extension ViewedStatus: Identifiable {
}

View File

@ -0,0 +1,26 @@
//
// https://mczachurski.dev
// Copyright © 2023 Marcin Czachurski and the repository contributors.
// Licensed under the Apache License 2.0.
//
import Foundation
import SwiftData
@Model final public class ViewedStatus {
@Attribute(.unique) public var id: String
public var reblogId: String?
public var date: Date
public var pixelfedAccount: AccountData?
init(id: String, reblogId: String? = nil, date: Date, pixelfedAccount: AccountData? = nil) {
self.id = id
self.reblogId = reblogId
self.date = date
self.pixelfedAccount = pixelfedAccount
}
}
extension ViewedStatus: Identifiable {
}

View File

@ -5,49 +5,56 @@
//
import Foundation
import CoreData
import SwiftData
import PixelfedKit
class ViewedStatusHandler {
public static let shared = ViewedStatusHandler()
private init() { }
/// Append new visible statuses to database.
func append(contentsOf statuses: [Status], accountId: String, modelContext: ModelContext) throws {
guard let accountDataFromDb = AccountDataHandler.shared.getAccountData(accountId: accountId, modelContext: modelContext) else {
return
}
for status in statuses {
guard self.getViewedStatus(accountId: accountId, statusId: status.id, modelContext: modelContext) == nil else {
continue
}
let viewedStatus = ViewedStatus(id: status.id, reblogId: status.reblog?.id, date: Date())
modelContext.insert(viewedStatus)
func createViewedStatusEntity(viewContext: NSManagedObjectContext? = nil) -> ViewedStatus {
let context = viewContext ?? CoreDataHandler.shared.container.viewContext
return ViewedStatus(context: context)
viewedStatus.pixelfedAccount = accountDataFromDb
accountDataFromDb.viewedStatuses.append(viewedStatus)
}
try modelContext.save()
}
/// Check if given status (real picture) has been already visible on the timeline (during last month).
func hasBeenAlreadyOnTimeline(accountId: String, status: Status, viewContext: NSManagedObjectContext? = nil) -> Bool {
func hasBeenAlreadyOnTimeline(accountId: String, status: Status, modelContext: ModelContext) -> Bool {
guard let reblog = status.reblog else {
return false
}
let context = viewContext ?? CoreDataHandler.shared.container.viewContext
let fetchRequest = ViewedStatus.fetchRequest()
fetchRequest.fetchLimit = 1
let statusIdPredicate = NSPredicate(format: "id = %@", reblog.id)
let reblogIdPredicate = NSPredicate(format: "reblogId = %@", reblog.id)
let idPredicates = NSCompoundPredicate.init(type: .or, subpredicates: [statusIdPredicate, reblogIdPredicate])
let accountPredicate = NSPredicate(format: "pixelfedAccount.id = %@", accountId)
fetchRequest.predicate = NSCompoundPredicate.init(type: .and, subpredicates: [idPredicates, accountPredicate])
do {
guard let first = try context.fetch(fetchRequest).first else {
let reblogId = reblog.id
let statusId = status.id
var fetchDescriptor = FetchDescriptor<ViewedStatus>(
// Here we are finding status which is other then checked status AND orginal status has been visible OR same reblogged by different user status has been visible.
predicate: #Predicate { $0.pixelfedAccount?.id == accountId && $0.id != statusId && ($0.id == reblogId || $0.reblogId == reblogId) }
)
fetchDescriptor.fetchLimit = 1
fetchDescriptor.includePendingChanges = true
guard let first = try modelContext.fetch(fetchDescriptor).first else {
return false
}
if first.reblogId == nil {
return true
}
if first.id != status.id {
return true
}
return false
return true
} catch {
CoreDataError.shared.handle(error, message: "Error during fetching viewed statuses (hasBeenAlreadyOnTimeline).")
return false
@ -55,25 +62,42 @@ class ViewedStatusHandler {
}
/// Mark to delete statuses older then one month.
func deleteOldViewedStatuses(viewContext: NSManagedObjectContext? = nil) {
let oldViewedStatuses = self.getOldViewedStatuses(viewContext: viewContext)
func deleteOldViewedStatuses(modelContext: ModelContext) throws {
let oldViewedStatuses = self.getOldViewedStatuses(modelContext: modelContext)
for status in oldViewedStatuses {
viewContext?.delete(status)
modelContext.delete(status)
}
try modelContext.save()
}
private func getViewedStatus(accountId: String, statusId: String, modelContext: ModelContext) -> ViewedStatus? {
do {
var fetchDescriptor = FetchDescriptor<ViewedStatus>(
predicate: #Predicate { $0.id == statusId && $0.pixelfedAccount?.id == accountId }
)
fetchDescriptor.fetchLimit = 1
fetchDescriptor.includePendingChanges = true
return try modelContext.fetch(fetchDescriptor).first
} catch {
CoreDataError.shared.handle(error, message: "Error during fetching viewed statuses (getOldViewedStatuses).")
return nil
}
}
private func getOldViewedStatuses(viewContext: NSManagedObjectContext? = nil) -> [ViewedStatus] {
let context = viewContext ?? CoreDataHandler.shared.container.viewContext
private func getOldViewedStatuses(modelContext: ModelContext) -> [ViewedStatus] {
guard let date = Calendar.current.date(byAdding: .month, value: -1, to: Date()) else {
return []
}
do {
let fetchRequest = ViewedStatus.fetchRequest()
fetchRequest.predicate = NSPredicate(format: "date < %@", date as NSDate)
return try context.fetch(fetchRequest)
var fetchDescriptor = FetchDescriptor<ViewedStatus>(
predicate: #Predicate { $0.date < date }
)
fetchDescriptor.includePendingChanges = true
return try modelContext.fetch(fetchDescriptor)
} catch {
CoreDataError.shared.handle(error, message: "Error during fetching viewed statuses (getOldViewedStatuses).")
return []

View File

@ -1,14 +1,13 @@
// swift-tools-version: 5.8
// swift-tools-version: 5.9
// The swift-tools-version declares the minimum version of Swift required to build this package.
import PackageDescription
let package = Package(
name: "EnvironmentKit",
defaultLocalization: "en",
platforms: [
.iOS(.v16),
.macOS(.v12),
.watchOS(.v8)
.iOS(.v17)
],
products: [
// Products define the executables and libraries a package produces, and make them visible to other packages.

View File

@ -9,7 +9,7 @@ import SwiftUI
import PixelfedKit
import ClientKit
public class ApplicationState: ObservableObject {
@Observable public class ApplicationState {
public static let shared = ApplicationState()
private init() { }
@ -24,99 +24,110 @@ public class ApplicationState: ObservableObject {
private static let defaults = Defaults()
/// Actual signed in account.
@Published public private(set) var account: AccountModel?
public private(set) var account: AccountModel?
/// The maximum number of allowed characters per status.
@Published public private(set) var statusMaxCharacters = defaults.statusMaxCharacters
public private(set) var statusMaxCharacters = defaults.statusMaxCharacters
/// The maximum number of media attachments that can be added to a status.
@Published public private(set) var statusMaxMediaAttachments = defaults.statusMaxMediaAttachments
public private(set) var statusMaxMediaAttachments = defaults.statusMaxMediaAttachments
/// Each URL in a status will be assumed to be exactly this many characters.
@Published public private(set) var statusCharactersReservedPerUrl = defaults.statusCharactersReservedPerUrl
public private(set) var statusCharactersReservedPerUrl = defaults.statusCharactersReservedPerUrl
/// Last notification seen by the user.
public var lastSeenNotificationId: String?
/// Amount of new notifications.
public var amountOfNewNotifications = 0
/// Last status seen by the user.
@Published public var lastSeenStatusId: String?
public var lastSeenStatusId: String?
/// Amount of new statuses which are not displayed yet to the user.
@Published public var amountOfNewStatuses = 0
public var amountOfNewStatuses = 0
/// Model for newly created comment.
@Published public var newComment: CommentModel?
/// Id of latest published status by the user.
public var latestPublishedStatusId: String?
/// Active icon name.
@Published public var activeIcon = "Default"
public var activeIcon = "Default"
/// Tint color in whole application.
@Published public var tintColor = TintColor.accentColor2
public var tintColor = TintColor.accentColor2
/// Application theme.
@Published public var theme = Theme.system
public var theme = Theme.system
/// Avatar shape.
@Published public var avatarShape = AvatarShape.circle
public var avatarShape = AvatarShape.circle
/// Status id for showed interaction row.
@Published public var showInteractionStatusId = ""
public var showInteractionStatusId = ""
/// Should we fire haptic when user change tabs.
@Published public var hapticTabSelectionEnabled = true
public var hapticTabSelectionEnabled = true
/// Should we fire haptic when user refresh list.
@Published public var hapticRefreshEnabled = true
public var hapticRefreshEnabled = true
/// Should we fire haptic when user tap button.
@Published public var hapticButtonPressEnabled = true
public var hapticButtonPressEnabled = true
/// Should we fire haptic when animation is finished.
@Published public var hapticAnimationEnabled = true
public var hapticAnimationEnabled = true
/// Should we fire haptic when notification occures.
@Published public var hapticNotificationEnabled = true
public var hapticNotificationEnabled = true
/// Should sensitive photos without mask.
@Published public var showSensitive = false
public var showSensitive = false
/// Should photo description for visually impaired be displayed.
@Published public var showPhotoDescription = false
public var showPhotoDescription = false
/// Status which should be shown from URL.
@Published public var showStatusId: String?
public var showStatusId: String?
/// Account which should be shown from URL.
@Published public var showAccountId: String?
public var showAccountId: String?
/// Updated user profile.
@Published public var updatedProfile: Account?
public var updatedProfile: Account?
/// Information which menu should be shown (top or bottom).
@Published public var menuPosition = MenuPosition.top
public var menuPosition = MenuPosition.top
/// Should avatars be visible on timelines.
@Published public var showAvatarsOnTimeline = false
public var showAvatarsOnTimeline = false
/// Should favourites be visible on timelines.
@Published public var showFavouritesOnTimeline = false
public var showFavouritesOnTimeline = false
/// Should ALT icon be visible on timelines.
@Published public var showAltIconOnTimeline = false
public var showAltIconOnTimeline = false
/// Show warning about missing ALT texts on compose screen.
@Published public var warnAboutMissingAlt = true
public var warnAboutMissingAlt = true
/// Show grid of photos on user profile.
@Published public var showGridOnUserProfile = false
public var showGridOnUserProfile = false
/// Show reboosted statuses on home timeline.
@Published public var showReboostedStatuses = false
public var showReboostedStatuses = false
/// Hide statuses without ALT text.
@Published public var hideStatusesWithoutAlt = false
public var hideStatusesWithoutAlt = false
public func changeApplicationState(accountModel: AccountModel, instance: Instance?, lastSeenStatusId: String?) {
/// Should show application badge.
public var showApplicationBadge = false
public func changeApplicationState(accountModel: AccountModel, instance: Instance?, lastSeenStatusId: String?, lastSeenNotificationId: String?) {
self.account = accountModel
self.lastSeenNotificationId = lastSeenNotificationId
self.lastSeenStatusId = lastSeenStatusId
self.amountOfNewStatuses = 0
self.amountOfNewNotifications = 0
if let statusesConfiguration = instance?.configuration?.statuses {
self.statusMaxCharacters = statusesConfiguration.maxCharacters
@ -132,7 +143,9 @@ public class ApplicationState: ObservableObject {
public func clearApplicationState() {
self.account = nil
self.lastSeenStatusId = nil
self.lastSeenNotificationId = nil
self.amountOfNewStatuses = 0
self.amountOfNewNotifications = 0
self.statusMaxCharacters = ApplicationState.defaults.statusMaxCharacters
self.statusMaxMediaAttachments = ApplicationState.defaults.statusMaxMediaAttachments

View File

@ -0,0 +1,5 @@
{
"sourceLanguage" : "en",
"strings" : {},
"version" : "1.0"
}

View File

@ -22,5 +22,6 @@ public struct AppConstants {
public static let accountUri = "\(AppConstants.accountScheme)://\(accountCallbackPart)"
public static let imagePipelineCacheName = "dev.mczachurski.Vernissage.DataCache"
public static let backgroundFetcherName = "dev.mczachurski.Vernissage.NotificationFetcher"
public static let coreDataPersistantContainerName = "Vernissage"
}

File diff suppressed because it is too large Load Diff

View File

@ -1,381 +0,0 @@
// MARK: Common strings.
"global.title.contentWarning" = "Sensitive content";
"global.title.seePost" = "See post";
"global.title.refresh" = "Refresh";
"global.title.momentsAgo" = "moments ago";
"global.title.success" = "Success";
"global.title.photoSaved" = "Photo has been saved.";
"global.title.ok" = "OK";
"global.title.showMore" = "Show more";
"global.title.showLess" = "Show less";
"global.title.close" = "Close";
"global.error.refreshingCredentialsTitle" = "Refreshing credentials error.";
"global.error.refreshingCredentialsSubtitle" = "Please sign in again to Pixelfed.";
// MARK: Global errors.
"global.error.unexpected" = "Unexpected error.";
"global.error.statusesNotRetrieved" = "Statuses not retrieved.";
"global.error.errorDuringDownloadStatuses" = "Error during download statuses from server.";
"global.error.errorDuringDownloadHashtag" = "Error during download tag from server.";
"global.error.hashtagNotExists" = "Hashtag does not exists.";
"global.error.errorDuringImageDownload" = "Cannot download image.";
"global.error.canceledImageDownload" = "Download image has been canceled.";
"global.error.errorDuringDataLoad" = "Loading data failed.";
"global.error.errorDuringUserRead" = "Cannot retrieve user account.";
"global.error.badUrlServer" = "Bad url to server.";
"global.error.accessTokenNotFound" = "Access token not found.";
"global.error.errorDuringDownloadStatus" = "Error during download status from server.";
"global.error.errorDuringPurchaseVerification" = "Purchase verification failed.";
// MARK: Main view (main navigation bar).
"mainview.tab.homeTimeline" = "Home";
"mainview.tab.localTimeline" = "Local";
"mainview.tab.federatedTimeline" = "Federated";
"mainview.tab.trendingPhotos" = "Photos";
"mainview.tab.trendingTags" = "Tags";
"mainview.tab.trendingAccounts" = "Accounts";
"mainview.tab.userProfile" = "Profile";
"mainview.tab.notifications" = "Notifications";
"mainview.tab.search" = "Search";
"mainview.tab.trending" = "Trending";
// MARK: Main view (leading navigation bar).
"mainview.menu.settings" = "Settings";
// MARK: Main view (error notifications).
"mainview.error.switchAccounts" = "Cannot switch accounts.";
// MARK: Home timeline.
"home.title.allCaughtUp" = "You're all caught up";
"home.title.noPhotos" = "Unfortunately, there are no photos here.";
// MARK: Statuses timeline (local/federated/favourite/bookmarks etc.).
"statuses.navigationBar.localTimeline" = "Local";
"statuses.navigationBar.federatedTimeline" = "Federated";
"statuses.navigationBar.favourites" = "Favourites";
"statuses.navigationBar.bookmarks" = "Bookmarks";
"statuses.title.noPhotos" = "Unfortunately, there are no photos here.";
"statuses.title.tagFollowed" = "You are following the tag.";
"statuses.title.tagUnfollowed" = "Tag has been unfollowed.";
"statuses.error.loadingStatusesFailed" = "Loading statuses failed.";
"statuses.error.tagFollowFailed" = "Follow tag failed.";
"statuses.error.tagUnfollowFailed" = "Unfollow tag failed.";
// Mark: Search view.
"search.navigationBar.title" = "Search";
"search.title.placeholder" = "Search...";
"search.title.usersWith" = "Users with %@";
"search.title.goToUser" = "Go to user %@";
"search.title.hashtagWith" = "Hashtags with %@";
"search.title.goToHashtag" = "Go to hashtag %@";
// Mark: Trending statuses.
"trendingStatuses.navigationBar.title" = "Photos";
"trendingStatuses.title.daily" = "Daily";
"trendingStatuses.title.monthly" = "Monthly";
"trendingStatuses.title.yearly" = "Yearly";
"trendingStatuses.error.loadingStatusesFailed" = "Loading statuses failed.";
"trendingStatuses.title.noPhotos" = "Unfortunately, there are no photos here.";
// Mark: Trending tags.
"tags.navigationBar.trendingTitle" = "Tags";
"tags.navigationBar.searchTitle" = "Tags";
"tags.navigationBar.followedTitle" = "Followed tags";
"tags.title.noTags" = "Unfortunately, there are no tags here.";
"tags.title.amountOfPosts" = "%d posts";
"tags.error.loadingTagsFailed" = "Loading tags failed.";
// Mark: Trending accounts.
"trendingAccounts.navigationBar.title" = "Accounts";
"trendingAccounts.title.noAccounts" = "Unfortunately, there is no one here.";
"trendingAccounts.error.loadingAccountsFailed" = "Loading accounts failed.";
// Mark: User profile view.
"userProfile.title.openInBrowser" = "Open in browser";
"userProfile.title.share" = "Share";
"userProfile.title.unmute" = "Unmute";
"userProfile.title.mute" = "Mute";
"userProfile.title.unblock" = "Unblock";
"userProfile.title.block" = "Block";
"userProfile.title.favourites" = "Favourites";
"userProfile.title.bookmarks" = "Bookmarks";
"userProfile.title.followedTags" = "Followed tags";
"userProfile.title.posts" = "Posts";
"userProfile.title.followers" = "Followers";
"userProfile.title.following" = "Following";
"userProfile.title.joined" = "Joined %@";
"userProfile.title.unfollow" = "Unfollow";
"userProfile.title.follow" = "Follow";
"userProfile.title.instance" = "Instance information";
"userProfile.title.blocks" = "Blocked accounts";
"userProfile.title.mutes" = "Muted accounts";
"userProfile.title.muted" = "Account muted";
"userProfile.title.unmuted" = "Account unmuted";
"userProfile.title.blocked" = "Account blocked";
"userProfile.title.unblocked" = "Account unblocked";
"userProfile.title.report" = "Report";
"userProfile.title.followsYou" = "Follows you";
"userProfile.title.requestFollow" = "Request follow";
"userProfile.title.cancelRequestFollow" = "Cancel request";
"userProfile.title.followRequests" = "Follow requests";
"userProfile.title.privateProfileTitle" = "This profile is private.";
"userProfile.title.privateProfileSubtitle" = "Only approved followers can see photos.";
"userProfile.error.notExists" = "Account does not exists.";
"userProfile.error.loadingAccountFailed" = "Error during download account from server.";
"userProfile.error.muting" = "Muting/unmuting action failed.";
"userProfile.error.block" = "Block/unblock action failed.";
"userProfile.error.relationship" = "Relationship action failed.";
"userProfile.title.edit" = "Edit";
"userProfile.title.muted" = "Muted";
"userProfile.title.blocked" = "Blocked";
"userProfile.title.enableBoosts" = "Enable boosts";
"userProfile.title.disableBoosts" = "Disable boosts";
"userProfile.title.boostedStatusesMuted" = "Boosts muted";
// Mark: Notifications view.
"notifications.navigationBar.title" = "Notifications";
"notifications.title.noNotifications" = "Unfortunately, there is nothing here.";
"notifications.title.followedYou" = "followed you";
"notifications.title.mentionedYou" = "mentioned you";
"notifications.title.boosted" = "boosted";
"notifications.title.favourited" = "favourited";
"notifications.title.postedStatus" = "posted status";
"notifications.title.followRequest" = "follow request";
"notifications.title.poll" = "poll";
"notifications.title.updatedStatus" = "updated status";
"notifications.title.signedUp" = "signed up";
"notifications.title.newReport" = "new report";
"notifications.error.loadingNotificationsFailed" = "Loading notifications failed.";
// Mark: Compose view.
"compose.navigationBar.title" = "Compose";
"compose.title.everyone" = "Everyone";
"compose.title.unlisted" = "Unlisted";
"compose.title.followers" = "Followers";
"compose.title.attachPhotoFull" = "Attach a photo and type what's on your mind";
"compose.title.attachPhotoMini" = "Type what's on your mind";
"compose.title.publish" = "Publish";
"compose.title.cancel" = "Cancel";
"compose.title.writeContentWarning" = "Write content warning";
"compose.title.commentsWillBeDisabled" = "Comments will be disabled";
"compose.title.statusPublished" = "Status published";
"compose.title.tryToUpload" = "Try to upload";
"compose.title.delete" = "Delete";
"compose.title.edit" = "Edit";
"compose.title.photos" = "Photos library";
"compose.title.camera" = "Take photo";
"compose.title.files" = "Browse files";
"compose.title.missingAltTexts" = "Missing ALT texts";
"compose.title.missingAltTextsWarning" = "Not all images have been described for the visually impaired. Would you like to send photos anyway?";
"compose.error.loadingPhotosFailed" = "Cannot retreive image from library.";
"compose.error.postingPhotoFailed" = "Error during posting photo.";
"compose.error.postingStatusFailed" = "Error during posting status.";
// Mark: Photo editor view.
"photoEdit.navigationBar.title" = "Photo details";
"photoEdit.title.photo" = "Photo";
"photoEdit.title.accessibility" = "Accessibility";
"photoEdit.title.accessibilityDescription" = "Description for the visually impaired";
"photoEdit.title.save" = "Save";
"photoEdit.title.cancel" = "Cancel";
"photoEdit.error.updatePhotoFailed" = "Error during updating photo.";
// Mark: Place selector view.
"placeSelector.navigationBar.title" = "Places";
"placeSelector.title.search" = "Search...";
"placeSelector.title.buttonSearch" = "Search";
"placeSelector.title.cancel" = "Cancel";
"placeSelector.error.loadingPlacesFailed" = "Loading notifications failed.";
// Mark: Settings view.
"settings.navigationBar.title" = "Settings";
"settings.title.close" = "Close";
"settings.title.version" = "Version";
"settings.title.accounts" = "Accounts";
"settings.title.newAccount" = "New account";
"settings.title.accent" = "Accent";
"settings.title.theme" = "Theme";
"settings.title.system" = "System";
"settings.title.light" = "Light";
"settings.title.dark" = "Dark";
"settings.title.avatar" = "Avatar";
"settings.title.circle" = "Circle";
"settings.title.rounderRectangle" = "Rounded rectangle";
"settings.title.other" = "Other";
"settings.title.thirdParty" = "Third party";
"settings.title.reportBug" = "Report a bug";
"settings.title.githubIssues" = "Issues on Github";
"settings.title.follow" = "Follow me";
"settings.title.support" = "Support";
"settings.title.thankYouTitle" = "Thank you 💕";
"settings.title.thankYouMessage" = "Thanks for your purchase. Purchases both big and small help us keep our dream of providing the best quality products to our customers. We hope youre loving Vernissage.";
"settings.title.thankYouClose" = "Close";
"settings.title.haptics" = "Haptics";
"settings.title.hapticsTabSelection" = "Tab selection";
"settings.title.hapticsButtonPress" = "Button press";
"settings.title.hapticsListRefresh" = "List refresh";
"settings.title.hapticsAnimationFinished" = "Animation finished";
"settings.title.mediaSettings" = "Media settings";
"settings.title.alwaysShowSensitiveTitle" = "Always show NSFW";
"settings.title.alwaysShowSensitiveDescription" = "Force show all NFSW (sensitive) media without warnings";
"settings.title.alwaysShowAltTitle" = "Show alternative text";
"settings.title.alwaysShowAltDescription" = "Show alternative text if present on status details screen";
"settings.title.general" = "General";
"settings.title.applicationIcon" = "Application icon";
"settings.title.followVernissage" = "Follow Vernissage";
"settings.title.mastodonAccount" = "Mastodon account";
"settings.title.pixelfedAccount" = "Pixelfed account";
"settings.title.openPage" = "Open";
"settings.title.privacyPolicy" = "Privacy policy";
"settings.title.terms" = "Terms & Conditions";
"settings.title.sourceCode" = "Source code";
"settings.title.rate" = "Rate Vernissage";
"settings.title.socials" = "Socials";
"settings.title.menuPosition" = "Menu position";
"settings.title.topMenu" = "Navigation bar";
"settings.title.bottomRightMenu" = "Bottom right";
"settings.title.bottomLeftMenu" = "Bottom left";
"settings.title.showAvatars" = "Show avatars";
"settings.title.showAvatarsOnTimeline" = "Avatars will be displayed on timelines";
"settings.title.showFavourite" = "Show favourites";
"settings.title.showFavouriteOnTimeline" = "Favourites will be displayed on timelines";
"settings.title.showAltText" = "Show ALT icon";
"settings.title.showAltTextOnTimeline" = "ALT icon will be displayed on timelines";
"settings.title.warnAboutMissingAltTitle" = "Warn of missing ALT text";
"settings.title.warnAboutMissingAltDescription" = "A warning about missing ALT texts will be displayed before publishing new post.";
"settings.title.enableReboostOnTimeline" = "Show boosted statuses";
"settings.title.enableReboostOnTimelineDescription" = "Boosted statuses will be visible on your home timeline.";
"settings.title.hideStatusesWithoutAlt" = "Hide statuses without ALT text";
"settings.title.hideStatusesWithoutAltDescription" = "Statuses without ALT text will not be visible on your home timeline.";
// Mark: Signin view.
"signin.navigationBar.title" = "Sign in to Pixelfed";
"signin.title.serverAddress" = "Server address";
"signin.title.signIn" = "Sign in";
"signin.title.enterServerAddress" = "Enter server address";
"signin.title.howToJoinLink" = "How to join Pixelfed";
"signin.title.chooseServer" = "Or choose Pixelfed server";
"signin.title.amountOfUsers" = "%d users";
"signin.title.amountOStatuses" = "%d statuses";
"signin.error.communicationFailed" = "Communication with server failed.";
// Mark: Status view.
"status.navigationBar.title" = "Details";
"status.title.uploaded" = "Uploaded";
"status.title.via" = "via %@";
"status.title.reboostedBy" = "Boosted by";
"status.title.favouritedBy" = "Favourited by";
"status.title.openInBrowser" = "Open in browser";
"status.title.shareStatus" = "Share status";
"status.title.yourStatus" = "Your status";
"status.title.delete" = "Delete";
"status.title.reboosted" = "Boosted";
"status.title.unreboosted" = "Unboosted";
"status.title.favourited" = "Favourited";
"status.title.unfavourited" = "Unfavourited";
"status.title.bookmarked" = "Bookmarked";
"status.title.unbookmarked" = "Unbookmarked";
"status.title.statusDeleted" = "Status deleted";
"status.title.reboost" = "Boost";
"status.title.unreboost" = "Unboost";
"status.title.favourite" = "Favourite";
"status.title.unfavourite" = "Unfavourite";
"status.title.bookmark" = "Bookmark";
"status.title.unbookmark" = "Unbookmark";
"status.title.comment" = "Comment";
"status.title.report" = "Report";
"status.title.saveImage" = "Save image";
"status.title.showMediaDescription" = "Show media description";
"status.title.mediaDescription" = "Media description";
"status.title.shareImage" = "Share image";
"status.title.altText" = "ALT";
"status.error.loadingStatusFailed" = "Loading status failed.";
"status.error.notFound" = "Status not existing anymore.";
"status.error.loadingCommentsFailed" = "Comments cannot be downloaded.";
"status.error.reboostFailed" = "Boost action failed.";
"status.error.favouriteFailed" = "Favourite action failed.";
"status.error.bookmarkFailed" = "Bookmark action failed.";
"status.error.deleteFailed" = "Delete action failed.";
// Mark: Accounts view.
"accounts.navigationBar.followers" = "Followers";
"accounts.navigationBar.following" = "Following";
"accounts.navigationBar.favouritedBy" = "Favourited by";
"accounts.navigationBar.reboostedBy" = "Boosted by";
"accounts.navigationBar.blocked" = "Blocked accounts";
"accounts.navigationBar.mutes" = "Muted accounts";
"accounts.title.noAccounts" = "Unfortunately, there is no one here.";
"accounts.error.loadingAccountsFailed" = "Loading accounts failed.";
// Mark: Third party view.
"thirdParty.navigationBar.title" = "Third party";
// Mark: Widget view.
"widget.title.photoDescription" = "Widget with photos from Pixelfed.";
"widget.title.qrCodeDescription" = "Widget with QR Code to your Pixelfed profile.";
// Mark: In-app purchases.
"purchase.donut.title" = "Donut";
"purchase.donut.description" = "Treat me to a doughnut.";
"purchase.coffee.title" = "Coffee";
"purchase.coffee.description" = "Treat me to a coffee.";
"purchase.cake.title" = "Coffee & cake";
"purchase.cake.description" = "Treat me to a coffee and cake.";
// Mark: Edit profile.
"editProfile.navigationBar.title" = "Edit profile";
"editProfile.title.displayName" = "Display name";
"editProfile.title.bio" = "Bio";
"editProfile.title.website" = "Website";
"editProfile.title.save" = "Save";
"editProfile.title.accountSaved" = "Profile has been updated.";
"editProfile.title.photoInfo" = "The changed photo will be visible in the app and on the website with a small delay.";
"editProfile.title.privateAccount" = "Private account";
"editProfile.title.privateAccountInfo" = "When your account is private, only people you approve can see your photos and videos on Pixelfed. Your existing followers won't be affected.";
"editProfile.error.saveAccountFailed" = "Saving profile failed.";
"editProfile.error.loadingAvatarFailed" = "Loading avatar failed.";
"editProfile.error.noProfileData" = "Profile data cannot be displayed.";
"editProfile.error.loadingAccountFailed" = "Error during download account from server.";
// Mark: Instance information.
"instance.navigationBar.title" = "Instance";
"instance.title.instanceInfo" = "Instance info";
"instance.title.name" = "Name";
"instance.title.address" = "Address";
"instance.title.email" = "Email";
"instance.title.version" = "Version";
"instance.title.users" = "Users";
"instance.title.posts" = "Posts";
"instance.title.domains" = "Domains";
"instance.title.registrations" = "Registrations";
"instance.title.approvalRequired" = "Approval required";
"instance.title.rules" = "Instance rules";
"instance.title.contact" = "Contact";
"instance.title.pixelfedAccount" = "Pixelfed account";
"instance.error.noInstanceData" = "Instance data cannot be displayed.";
"instance.error.loadingDataFailed" = "Error during download instance data from server.";
// Mark: Report screen.
"report.navigationBar.title" = "Report";
"report.title.close" = "Close";
"report.title.send" = "Send";
"report.title.userReported" = "User has been reported";
"report.title.postReported" = "Post has been reported";
"report.title.reportType" = "Type of abuse";
"report.title.spam" = "It's a spam";
"report.title.sensitive" = "Nudity or sexual activity";
"report.title.abusive" = "Hate speech or symbols";
"report.title.underage" = "Underage account";
"report.title.violence" = "Violence or dangerous organisations";
"report.title.copyright" = "Copyright infringement";
"report.title.impersonation" = "Impersonation";
"report.title.scam" = "Bullying or harassment";
"report.title.terrorism" = "Terrorism";
"report.error.notReported" = "Error during sending report.";
// Mark: Following requests.
"followingRequests.navigationBar.title" = "Following requests";
"followingRequests.title.approve" = "Approve";
"followingRequests.title.reject" = "Reject";
"followingRequests.error.approve" = "Error during approving request.";
"followingRequests.error.reject" = "Error during rejecting request.";

View File

@ -1,381 +0,0 @@
// MARK: Common strings.
"global.title.contentWarning" = "Eduki hunkigarria";
"global.title.seePost" = "Ikusi bidalketa";
"global.title.refresh" = "Freskatu";
"global.title.momentsAgo" = "oraintxe bertan";
"global.title.success" = "Primeran";
"global.title.photoSaved" = "Argazkia gorde da.";
"global.title.ok" = "Ados";
"global.title.showMore" = "Erakutsi gehiago";
"global.title.showLess" = "Erakutsi gutxiago";
"global.title.close" = "Itxi";
"global.error.refreshingCredentialsTitle" = "Egiaztagirien freskatzeak huts egin du.";
"global.error.refreshingCredentialsSubtitle" = "Hasi saioa berriro Pixelfeden.";
// MARK: Global errors.
"global.error.unexpected" = "Espero ez zen errorea.";
"global.error.statusesNotRetrieved" = "Ez dira egoerak eskuratu.";
"global.error.errorDuringDownloadStatuses" = "Errorea zerbitzaritik egoerak eskuratzean.";
"global.error.errorDuringDownloadHashtag" = "Errorea zerbitzaritik traolak eskuratzean.";
"global.error.hashtagNotExists" = "Traola ez da lehendik existitzen.";
"global.error.errorDuringImageDownload" = "Ezin da irudia eskuratu.";
"global.error.canceledImageDownload" = "Irudiaren deskarga bertan behera utzi da.";
"global.error.errorDuringDataLoad" = "Datuen kargak huts egin du.";
"global.error.errorDuringUserRead" = "Ezin izan da erabiltzailearen kontua eskuratu.";
"global.error.badUrlServer" = "Zerbitzariaren URL okerra.";
"global.error.accessTokenNotFound" = "Ez da sarbide-tokena aurkitu.";
"global.error.errorDuringDownloadStatus" = "Errorea zerbitzaritik egoera eskuratzean.";
"global.error.errorDuringPurchaseVerification" = "Erosketaren egiaztaketak huts egin du.";
// MARK: Main view (main navigation bar).
"mainview.tab.homeTimeline" = "Hasiera";
"mainview.tab.localTimeline" = "Lokala";
"mainview.tab.federatedTimeline" = "Federatua";
"mainview.tab.trendingPhotos" = "Argazkiak";
"mainview.tab.trendingTags" = "Traolak";
"mainview.tab.trendingAccounts" = "Kontuak";
"mainview.tab.userProfile" = "Profila";
"mainview.tab.notifications" = "Jakinarazpenak";
"mainview.tab.search" = "Bilatu";
"mainview.tab.trending" = "Bogan";
// MARK: Main view (leading navigation bar).
"mainview.menu.settings" = "Ezarpenak";
// MARK: Main view (error notifications).
"mainview.error.switchAccounts" = "Ezin da kontua aldatu.";
// MARK: Home timeline.
"home.title.allCaughtUp" = "Egunean zaude";
"home.title.noPhotos" = "Argazkirik ez.";
// MARK: Statuses timeline (local/federated/favourite/bookmarks etc.).
"statuses.navigationBar.localTimeline" = "Lokala";
"statuses.navigationBar.federatedTimeline" = "Federatua";
"statuses.navigationBar.favourites" = "Gogokoak";
"statuses.navigationBar.bookmarks" = "Laster-markak";
"statuses.title.noPhotos" = "Argazkirik ez.";
"statuses.title.tagFollowed" = "Traolari jarraitzen diozu.";
"statuses.title.tagUnfollowed" = "Traola jarraitzeari utzi diozu.";
"statuses.error.loadingStatusesFailed" = "Egoerak kargatzeak huts egin du.";
"statuses.error.tagFollowFailed" = "Traolari jarraitzeak huts egin du.";
"statuses.error.tagUnfollowFailed" = "Traolari jarraitzeari uzteak huts egin du.";
// Mark: Search view.
"search.navigationBar.title" = "Bilatu";
"search.title.placeholder" = "Bilatu...";
"search.title.usersWith" = "%@ duten erabiltzaileak";
"search.title.goToUser" = "Joan %@ erabiltzailera";
"search.title.hashtagWith" = "%@ duten traolak";
"search.title.goToHashtag" = "Joan %@ traolara";
// Mark: Trending statuses.
"trendingStatuses.navigationBar.title" = "Argazkiak";
"trendingStatuses.title.daily" = "Egunekoak";
"trendingStatuses.title.monthly" = "Hilabetekoak";
"trendingStatuses.title.yearly" = "Urtekoak";
"trendingStatuses.error.loadingStatusesFailed" = "Egoerak kargatzeak huts egin du.";
"trendingStatuses.title.noPhotos" = "Argazkirik ez.";
// Mark: Trending tags.
"tags.navigationBar.trendingTitle" = "Traolak";
"tags.navigationBar.searchTitle" = "Traolak";
"tags.navigationBar.followedTitle" = "Jarraitzen dituzun traolak";
"tags.title.noTags" = "Traolarik ez.";
"tags.title.amountOfPosts" = "%d bidalketa";
"tags.error.loadingTagsFailed" = "Traolak kargatzeak huts egin du.";
// Mark: Trending accounts.
"trendingAccounts.navigationBar.title" = "Kontuak";
"trendingAccounts.title.noAccounts" = "Inor ez.";
"trendingAccounts.error.loadingAccountsFailed" = "Kontuak kargatzeak huts egin du.";
// Mark: User profile view.
"userProfile.title.openInBrowser" = "Ireki nabigatzailean";
"userProfile.title.share" = "Partekatu";
"userProfile.title.unmute" = "Utzi mututzeari";
"userProfile.title.mute" = "Mututu";
"userProfile.title.unblock" = "Utzi blokeatzeari";
"userProfile.title.block" = "Blokeatu";
"userProfile.title.favourites" = "Gogokoak";
"userProfile.title.bookmarks" = "Laster-markak";
"userProfile.title.followedTags" = "Traolak";
"userProfile.title.posts" = "Bidalketa";
"userProfile.title.followers" = "Jarraitzaile";
"userProfile.title.following" = "Jarraitzen";
"userProfile.title.joined" = "%@ egin zuen bat";
"userProfile.title.unfollow" = "Utzi jarraitzeari";
"userProfile.title.follow" = "Jarraitu";
"userProfile.title.instance" = "Instantziari buruzko informazioa";
"userProfile.title.blocks" = "Blokeatutako kontuak";
"userProfile.title.mutes" = "Mutututako kontuak";
"userProfile.title.muted" = "Kontua mututu da";
"userProfile.title.unmuted" = "Kontua mututzeari utzi zaio";
"userProfile.title.blocked" = "Kontua blokeatu da";
"userProfile.title.unblocked" = "Kontua blokeatzeari utzi zaio";
"userProfile.title.report" = "Salatu";
"userProfile.title.followsYou" = "Jarraitzen dizu";
"userProfile.title.requestFollow" = "Egin jarraitzeko eskaera";
"userProfile.title.cancelRequestFollow" = "Utzi bertan behera jarraitzeko eskaera";
"userProfile.title.followRequests" = "Jarraipen-eskaerak";
"userProfile.title.privateProfileTitle" = "Profil hau pribatua da.";
"userProfile.title.privateProfileSubtitle" = "Baimendutako jarraitzaileek soilik ikus ditzakete argazkiak.";
"userProfile.error.notExists" = "Kontua ez da existitzen.";
"userProfile.error.loadingAccountFailed" = "Errorea zerbitzaritik kontua eskuratzean.";
"userProfile.error.muting" = "Mututu/Mututzeari uzteak huts egin du.";
"userProfile.error.block" = "Blokeatu/Blokeatzeari uzteak huts egin du.";
"userProfile.error.relationship" = "Harreman ekintzak huts egin du.";
"userProfile.title.edit" = "Editatu";
"userProfile.title.muted" = "Mutututa";
"userProfile.title.blocked" = "Blokeatuta";
"userProfile.title.enableBoosts" = "Ikusi bultzadak";
"userProfile.title.disableBoosts" = "Ezkutatu bultzadak";
"userProfile.title.boostedStatusesMuted" = "Bultzadak mututu dira";
// Mark: Notifications view.
"notifications.navigationBar.title" = "Jakinarazpenak";
"notifications.title.noNotifications" = "Ez dago jakinarazpenik.";
"notifications.title.followedYou" = "jarraitu dizu";
"notifications.title.mentionedYou" = "aipatu zaitu";
"notifications.title.boosted" = "bultzatu du";
"notifications.title.favourited" = "gogoko du";
"notifications.title.postedStatus" = "argitaratu du";
"notifications.title.followRequest" = "jarraipen-eskaera bidali dizu";
"notifications.title.poll" = "bozketa";
"notifications.title.updatedStatus" = "egoera eguneratu du";
"notifications.title.signedUp" = "izena eman du";
"notifications.title.newReport" = "txosten berria";
"notifications.error.loadingNotificationsFailed" = "Jakinarazpenak kargatzeak huts egin du.";
// Mark: Compose view.
"compose.navigationBar.title" = "Idatzi";
"compose.title.everyone" = "Edonorentzat ikusgai";
"compose.title.unlisted" = "Zerrendatu gabea";
"compose.title.followers" = "Jarraitzaileentzat soilik";
"compose.title.attachPhotoFull" = "Erantsi argazkia eta idatzi buruan duzuna";
"compose.title.attachPhotoMini" = "Idatzi buruan duzuna";
"compose.title.publish" = "Argitaratu";
"compose.title.cancel" = "Utzi";
"compose.title.writeContentWarning" = "Idatzi edukiari buruzko oharra";
"compose.title.commentsWillBeDisabled" = "Iruzkinak ezgaituko dira";
"compose.title.statusPublished" = "Egoera argitaratu da";
"compose.title.tryToUpload" = "Saiatu igotzen";
"compose.title.delete" = "Ezabatu";
"compose.title.edit" = "Editatu";
"compose.title.photos" = "Argazki-liburutegia";
"compose.title.camera" = "Egin argazkia";
"compose.title.files" = "Arakatu fitxategiak";
"compose.title.missingAltTexts" = "ALT testurik ez";
"compose.title.missingAltTextsWarning" = "Irudiren bat ez da ikusmen urritasuna dutenentzat deskribatu. Argazkiok argitaratu nahi dituzu hala ere?";
"compose.error.loadingPhotosFailed" = "Ezin da liburutegiko irudia eskuratu.";
"compose.error.postingPhotoFailed" = "Errorea argazkia argitaratzean.";
"compose.error.postingStatusFailed" = "Errorea egoera argitaratzean.";
// Mark: Photo editor view.
"photoEdit.navigationBar.title" = "Argazkiaren xehetasunak";
"photoEdit.title.photo" = "Argazkia";
"photoEdit.title.accessibility" = "Irisgarritasuna";
"photoEdit.title.accessibilityDescription" = "Ikusmen urritasuna dutenentzat deskribapena";
"photoEdit.title.save" = "Gorde";
"photoEdit.title.cancel" = "Utzi";
"photoEdit.error.updatePhotoFailed" = "Errorea argazkia eguneratzean.";
// Mark: Place selector view.
"placeSelector.navigationBar.title" = "Tokiak";
"placeSelector.title.search" = "Bilatu...";
"placeSelector.title.buttonSearch" = "Bilatu";
"placeSelector.title.cancel" = "Utzi";
"placeSelector.error.loadingPlacesFailed" = "Jakinarazpenak kargatzeak huts egin du.";
// Mark: Settings view.
"settings.navigationBar.title" = "Ezarpenak";
"settings.title.close" = "Itxi";
"settings.title.version" = "Bertsioa";
"settings.title.accounts" = "Kontuak";
"settings.title.newAccount" = "Gehitu kontua";
"settings.title.accent" = "Kolore nagusia";
"settings.title.theme" = "Gaia";
"settings.title.system" = "Sistemak darabilena";
"settings.title.light" = "Argia";
"settings.title.dark" = "Iluna";
"settings.title.avatar" = "Abatarra";
"settings.title.circle" = "Biribila";
"settings.title.rounderRectangle" = "Biribildutako ertzak";
"settings.title.other" = "Beste batzuk";
"settings.title.thirdParty" = "Hirugarrenak";
"settings.title.reportBug" = "Eman errore baten berri";
"settings.title.githubIssues" = "Erroreak Github-en";
"settings.title.follow" = "Jarraitu niri";
"settings.title.support" = "Eman babesa";
"settings.title.thankYouTitle" = "Eskerrik asko 💕";
"settings.title.thankYouMessage" = "Mila esker erosketagatik. Erosketa handi eta txikiek gure bezeroei kalitatezko produkturik onenak eskaintzeko ametsari eusten laguntzen digute. Espero dugu Vernissage gustuko izatea.";
"settings.title.thankYouClose" = "Itxi";
"settings.title.haptics" = "Hobespen haptikoak";
"settings.title.hapticsTabSelection" = "Fitxak hautatzean";
"settings.title.hapticsButtonPress" = "Botoietan tap egitean";
"settings.title.hapticsListRefresh" = "Zerrendak freskatzean";
"settings.title.hapticsAnimationFinished" = "Animazioak amaitzean";
"settings.title.mediaSettings" = "Multimedia hobespenak";
"settings.title.alwaysShowSensitiveTitle" = "Erakutsi beti NSFW edukia";
"settings.title.alwaysShowSensitiveDescription" = "NSFW (Lantokirako egokia ez den edukia) gisa markatutako multimedia edukia ohartarazpenik gabe erakutsiko da";
"settings.title.alwaysShowAltTitle" = "Erakutsi testu alternatiboa";
"settings.title.alwaysShowAltDescription" = "Testu alternatiboa xehetasunen pantailan erakutsiko da, baldin badago";
"settings.title.general" = "Orokorra";
"settings.title.applicationIcon" = "Aplikazioaren ikonoa";
"settings.title.followVernissage" = "Jarraitu Vernissage-ri";
"settings.title.mastodonAccount" = "Mastodon kontua";
"settings.title.pixelfedAccount" = "Pixelfed kontua";
"settings.title.openPage" = "Ireki";
"settings.title.privacyPolicy" = "Pribatutasun politika";
"settings.title.terms" = "Erabilera baldintzak";
"settings.title.sourceCode" = "Iturburu kodea";
"settings.title.rate" = "Baloratu Vernissage";
"settings.title.socials" = "Gizarte-sareak";
"settings.title.menuPosition" = "Menuaren kokapena";
"settings.title.topMenu" = "Nabigazio barra";
"settings.title.bottomRightMenu" = "Behe eskumaldean";
"settings.title.bottomLeftMenu" = "Behe ezkerraldean";
"settings.title.showAvatars" = "Erakutsi abatarrak";
"settings.title.showAvatarsOnTimeline" = "Abatarrak denbora-lerroan erakutsiko dira";
"settings.title.showFavourite" = "Erakutsi gogokoak";
"settings.title.showFavouriteOnTimeline" = "Gogokoak denbora-lerroan erakutsiko dira";
"settings.title.showAltText" = "Erakutsi ALT ikurra";
"settings.title.showAltTextOnTimeline" = "ALT ikurra (deskribapena edo testu alternatiboa dagoenaren seinale) denbora-lerroan erakutsiko da";
"settings.title.warnAboutMissingAltTitle" = "Abisatu ALT ahaztu bazait";
"settings.title.warnAboutMissingAltDescription" = "Irudiren batek deskribapenik ez badu, argitaratu baino lehen abisua erakutsiko da.";
"settings.title.enableReboostOnTimeline" = "Erakutsi bultzatutako egoerak";
"settings.title.enableReboostOnTimelineDescription" = "Besteek bultzatu dituzten egoerak denbora-lerroan erakutsiko dira.";
"settings.title.hideStatusesWithoutAlt" = "Ezkutatu ALT gabeko egoerak";
"settings.title.hideStatusesWithoutAltDescription" = "Deskribapen edo testu alternatiborik ez duten egoerak ez dira denbora-lerroan erakutsiko.";
// Mark: Signin view.
"signin.navigationBar.title" = "Hasi saioa Pixelfed-en";
"signin.title.serverAddress" = "Zerbitzariaren helbidea";
"signin.title.signIn" = "Hasi saioa";
"signin.title.enterServerAddress" = "Sartu zerbitzariaren helbidea";
"signin.title.howToJoinLink" = "Nola batu Pixelfed-era";
"signin.title.chooseServer" = "Edo aukeratu Pixelfed zerbitzaria";
"signin.title.amountOfUsers" = "%d erabiltzaile";
"signin.title.amountOStatuses" = "%d egoera";
"signin.error.communicationFailed" = "Zerbitzariarekin komunikazioak huts egin du.";
// Mark: Status view.
"status.navigationBar.title" = "Xehetasunak";
"status.title.uploaded" = ">";
"status.title.via" = "%@ bidez";
"status.title.reboostedBy" = "Bultzatu dutenak";
"status.title.favouritedBy" = "Gogoko egin dutenak";
"status.title.openInBrowser" = "Ireki nabigatzailean";
"status.title.shareStatus" = "Partekatu egoera";
"status.title.yourStatus" = "Zure egoera";
"status.title.delete" = "Ezabatu";
"status.title.reboosted" = "Bultzatua";
"status.title.unreboosted" = "Bultzada kendua";
"status.title.favourited" = "Gogoko egina";
"status.title.unfavourited" = "Gogoko egiteari utzia";
"status.title.bookmarked" = "Laster-marka jarria";
"status.title.unbookmarked" = "Laster-marka kendua";
"status.title.statusDeleted" = "Egoera ezabatua";
"status.title.reboost" = "Bultzatu";
"status.title.unreboost" = "Kendu bultzada";
"status.title.favourite" = "Egin gogoko";
"status.title.unfavourite" = "Kendu gogokoa";
"status.title.bookmark" = "Jarri laster-marka";
"status.title.unbookmark" = "Kendu laster-marka";
"status.title.comment" = "Egin iruzkina";
"status.title.report" = "Salatu";
"status.title.saveImage" = "Gorde irudia";
"status.title.showMediaDescription" = "Erakutsi multimediaren deskribapena";
"status.title.mediaDescription" = "Multimediaren deskribapena";
"status.title.shareImage" = "Partekatu irudia";
"status.title.altText" = "ALT";
"status.error.loadingStatusFailed" = "Egoera kargatzeak huts egin du.";
"status.error.notFound" = "Egoera ez da dagoeneko existitzen.";
"status.error.loadingCommentsFailed" = "Ezin dira iruzkinak eskuratu.";
"status.error.reboostFailed" = "Bultzadak huts egin du.";
"status.error.favouriteFailed" = "Gogokoak huts egin du.";
"status.error.bookmarkFailed" = "Laster-markak huts egin du.";
"status.error.deleteFailed" = "Ezabatzeak huts egin du.";
// Mark: Accounts view.
"accounts.navigationBar.followers" = "Jarraitzaile";
"accounts.navigationBar.following" = "Jarraitzen";
"accounts.navigationBar.favouritedBy" = "Honek gogoko egina";
"accounts.navigationBar.reboostedBy" = "Honek bultzatua";
"accounts.navigationBar.blocked" = "Blokeatutako kontuak";
"accounts.navigationBar.mutes" = "Mutututako kontuak";
"accounts.title.noAccounts" = "Inor ez.";
"accounts.error.loadingAccountsFailed" = "Kontuak kargatzeak huts egin du.";
// Mark: Third party view.
"thirdParty.navigationBar.title" = "Hirugarrenak";
// Mark: Widget view.
"widget.title.photoDescription" = "Widgeta Pixelfed-eko argazkiekin.";
"widget.title.qrCodeDescription" = "Widgeta zure Pixelfed-eko profilaren QR kodearekin.";
// Mark: In-app purchases.
"purchase.donut.title" = "Opila";
"purchase.donut.description" = "Eros diezadazu opil bat.";
"purchase.coffee.title" = "Kafea";
"purchase.coffee.description" = "Gonbida nazazu kafe bat hartzera.";
"purchase.cake.title" = "Kafea eta tarta";
"purchase.cake.description" = "Kafea eta tarta erosiko?";
// Mark: Edit profile.
"editProfile.navigationBar.title" = "Editatu profila";
"editProfile.title.displayName" = "Pantaila izena";
"editProfile.title.bio" = "Biografia";
"editProfile.title.website" = "Webgunea";
"editProfile.title.save" = "Gorde";
"editProfile.title.accountSaved" = "Profila eguneratu da.";
"editProfile.title.photoInfo" = "Aldatutako argazkia atzerapen txiki batekin ikusiko da aplikazioan eta web gunean.";
"editProfile.title.privateAccount" = "Babestutako kontua";
"editProfile.title.privateAccountInfo" = "Zure kontua babestuta dagoenean baimendutako pertsonek bakarrik ikus ditzakete zure argazkiak eta bideoak Pixelfed-en. Ez du eraginik izango dagoeneko jarraitzen dizutenengan.";
"editProfile.error.saveAccountFailed" = "Profila gordetzeak huts egin du.";
"editProfile.error.loadingAvatarFailed" = "Abatarra kargatzeak huts egin du.";
"editProfile.error.noProfileData" = "Ezin dira profileko datuak erakutsi.";
"editProfile.error.loadingAccountFailed" = "Errorea zerbitzaritik kontua eskuratzean.";
// Mark: Instance information.
"instance.navigationBar.title" = "Instantzia";
"instance.title.instanceInfo" = "Instantziari buruzko informazioa";
"instance.title.name" = "Izena";
"instance.title.address" = "Helbidea";
"instance.title.email" = "ePosta";
"instance.title.version" = "Bertsioa";
"instance.title.users" = "Erabiltzaileak";
"instance.title.posts" = "Bidalketak";
"instance.title.domains" = "Domeinuak";
"instance.title.registrations" = "Izen emateak";
"instance.title.approvalRequired" = "Onespena behar da";
"instance.title.rules" = "Instantziaren arauak";
"instance.title.contact" = "Harremana";
"instance.title.pixelfedAccount" = "Pixelfed kontua";
"instance.error.noInstanceData" = "Ezin dira instantziaren datuak erakutsi.";
"instance.error.loadingDataFailed" = "Errorea zerbitzaritik instantziaren datuak eskuratzean.";
// Mark: Report screen.
"report.navigationBar.title" = "Salatu";
"report.title.close" = "Itxi";
"report.title.send" = "Bidali";
"report.title.userReported" = "Erabiltzailea salatu da";
"report.title.postReported" = "Bidalketa salatu da";
"report.title.reportType" = "Urraketa mota";
"report.title.spam" = "Spama da";
"report.title.sensitive" = "Biluzia edo sexu-ekintza";
"report.title.abusive" = "Gorroto sustatzen duten hitzaldiak edo ikurrak";
"report.title.underage" = "Adingabea";
"report.title.violence" = "Bortizkeria edo erakunde arriskutsua";
"report.title.copyright" = "Egile-eskubideen urraketa";
"report.title.impersonation" = "Imitatzailea";
"report.title.scam" = "Bullyinga edo jazarpena";
"report.title.terrorism" = "Terrorismoa";
"report.error.notReported" = "Errorea salaketa bidaltzerakoan.";
// Mark: Following requests.
"followingRequests.navigationBar.title" = "Jarraipen-eskaerak";
"followingRequests.title.approve" = "Baimendu";
"followingRequests.title.reject" = "Baztertu";
"followingRequests.error.approve" = "Errorea eskaera baimentzean.";
"followingRequests.error.reject" = "Errorea eskaera baztertzean.";

View File

@ -1,381 +0,0 @@
// MARK: Common strings.
"global.title.contentWarning" = "Contenu sensible";
"global.title.seePost" = "Voir le post";
"global.title.refresh" = "Rafraîchir";
"global.title.momentsAgo" = "Il y a quelques instants";
"global.title.success" = "Succès";
"global.title.photoSaved" = "La photo a été sauvegardée.";
"global.title.ok" = "OK";
"global.title.showMore" = "Montrer plus";
"global.title.showLess" = "Montrer moins";
"global.title.close" = "Fermer";
"global.error.refreshingCredentialsTitle" = "Erreur d'actualisation des données d'identification.";
"global.error.refreshingCredentialsSubtitle" = "Veuillez vous connecter à nouveau à Pixelfed.";
// MARK: Global errors.
"global.error.unexpected" = "Erreur inattendue.";
"global.error.statusesNotRetrieved" = "Statuts non récupérés.";
"global.error.errorDuringDownloadStatuses" = "Erreur pendant le téléchargerment des statuts du serveur.";
"global.error.errorDuringDownloadHashtag" = "Erreur pendant le téléchargement des tags depuis le serveur.";
"global.error.hashtagNotExists" = "Le hashtag n'existe pas.";
"global.error.errorDuringImageDownload" = "Impossible de télécharger l'image.";
"global.error.canceledImageDownload" = "Le téléchargement de l'image a été annulé.";
"global.error.errorDuringDataLoad" = "Le chargement des données a échoué.";
"global.error.errorDuringUserRead" = "Impossible de récupérer les données de l'utilisateur.";
"global.error.badUrlServer" = "Mauvaise URL pour le serveur.";
"global.error.accessTokenNotFound" = "Le jeton d'accès n'est pas trouvé.";
"global.error.errorDuringDownloadStatus" = "Erreur durant le téléchargement du statut depuis le serveur.";
"global.error.errorDuringPurchaseVerification" = "Vérification d'achat échoué.";
// MARK: Main view (main navigation bar).
"mainview.tab.homeTimeline" = "Accueil";
"mainview.tab.localTimeline" = "Local";
"mainview.tab.federatedTimeline" = "Fédéré";
"mainview.tab.trendingPhotos" = "Photos";
"mainview.tab.trendingTags" = "Tags";
"mainview.tab.trendingAccounts" = "Utilisateurs";
"mainview.tab.userProfile" = "Profil";
"mainview.tab.notifications" = "Notifications";
"mainview.tab.search" = "Rechercher";
"mainview.tab.trending" = "Tendance";
// MARK: Main view (leading navigation bar).
"mainview.menu.settings" = "Paramètres";
// MARK: Main view (error notifications).
"mainview.error.switchAccounts" = "Impossible de changer de compte.";
// MARK: Home timeline.
"home.title.allCaughtUp" = "Tout est à jour";
"home.title.noPhotos" = "Malheureusement, il n'y a pas de photos ici.";
// MARK: Statuses timeline (local/federated/favourite/bookmarks etc.).
"statuses.navigationBar.localTimeline" = "Local";
"statuses.navigationBar.federatedTimeline" = "Fédéré";
"statuses.navigationBar.favourites" = "Favoris";
"statuses.navigationBar.bookmarks" = "Marque-pages";
"statuses.title.noPhotos" = "Malheureusement, il n'y a pas de photos ici.";
"statuses.title.tagFollowed" = "Vous suivez ce tag.";
"statuses.title.tagUnfollowed" = "Vous ne suivez plus ce tag.";
"statuses.error.loadingStatusesFailed" = "Chargement des statuts impossible.";
"statuses.error.tagFollowFailed" = "Suivi de tag échoué.";
"statuses.error.tagUnfollowFailed" = "Ne plus suivre le tag a échoué.";
// Mark: Search view.
"search.navigationBar.title" = "Rechercher";
"search.title.placeholder" = "Rechercher...";
"search.title.usersWith" = "Utilisateurs avec %@";
"search.title.goToUser" = "Voir l'utilisateur %@";
"search.title.hashtagWith" = "Hashtags avec %@";
"search.title.goToHashtag" = "Voir le hashtag %@";
// Mark: Trending statuses.
"trendingStatuses.navigationBar.title" = "Photos";
"trendingStatuses.title.daily" = "Quotidien";
"trendingStatuses.title.monthly" = "Mensuel";
"trendingStatuses.title.yearly" = "Annuel";
"trendingStatuses.error.loadingStatusesFailed" = "Chargement des statuts échoué.";
"trendingStatuses.title.noPhotos" = "Malheureusement, il n'y a pas de photos ici.";
// Mark: Trending tags.
"tags.navigationBar.trendingTitle" = "Tags";
"tags.navigationBar.searchTitle" = "Tags";
"tags.navigationBar.followedTitle" = "Tags";
"tags.title.noTags" = "Malheureusement, il n'y a pas de tags ici.";
"tags.title.amountOfPosts" = "%d posts";
"tags.error.loadingTagsFailed" = "Chargement des tags échoué.";
// Mark: Trending accounts.
"trendingAccounts.navigationBar.title" = "Utilisateurs";
"trendingAccounts.title.noAccounts" = "Malheureusement, il n'y a personne ici.";
"trendingAccounts.error.loadingAccountsFailed" = "Chargement des comptes échoué.";
// Mark: User profile view.
"userProfile.title.openInBrowser" = "Ouvrir dans un navigateur";
"userProfile.title.share" = "Partager";
"userProfile.title.unmute" = "Désactiver";
"userProfile.title.mute" = "Sourdine";
"userProfile.title.unblock" = "Déblouer";
"userProfile.title.block" = "Bloquer";
"userProfile.title.favourites" = "Favoris";
"userProfile.title.bookmarks" = "Marque-pages";
"userProfile.title.followedTags" = "Tags";
"userProfile.title.posts" = "Posts";
"userProfile.title.followers" = "Abonnés";
"userProfile.title.following" = "Abonnements";
"userProfile.title.joined" = "Joint %@";
"userProfile.title.unfollow" = "Ne plus suivre";
"userProfile.title.follow" = "Suivre";
"userProfile.title.instance" = "Information sur l'instance";
"userProfile.title.blocks" = "Comptes bloqués";
"userProfile.title.mutes" = "Comptes en sourdine";
"userProfile.title.muted" = "Compte mis en sourdine";
"userProfile.title.unmuted" = "Compte remis en actif";
"userProfile.title.blocked" = "Compte bloqué";
"userProfile.title.unblocked" = "Compte débloqué";
"userProfile.title.report" = "Rapport";
"userProfile.title.followsYou" = "Vous suit";
"userProfile.title.requestFollow" = "Demande de suivi";
"userProfile.title.cancelRequestFollow" = "Annuler la demande";
"userProfile.title.followRequests" = "Suivre les demandes";
"userProfile.title.privateProfileTitle" = "Ce profil est privé.";
"userProfile.title.privateProfileSubtitle" = "Seules les personnes approuvées peuvent voir les photos.";
"userProfile.error.notExists" = "Le compte n'existe pas.";
"userProfile.error.loadingAccountFailed" = "Erreur pendant le téléchargement du compte depuis le serveur.";
"userProfile.error.muting" = "L'action sourdine / réactivation a échoué.";
"userProfile.error.block" = "L'action bloquer / déblouquer a échoué.";
"userProfile.error.relationship" = "L'action de relation a échoué.";
"userProfile.title.edit" = "Editer";
"userProfile.title.muted" = "Sourdine";
"userProfile.title.blocked" = "Bloquer";
"userProfile.title.enableBoosts" = "Enable boosts";
"userProfile.title.disableBoosts" = "Disable boosts";
"userProfile.title.boostedStatusesMuted" = "Boosts muted";
// Mark: Notifications view.
"notifications.navigationBar.title" = "Notifications";
"notifications.title.noNotifications" = "Malheureusement, il n'y a rien ici.";
"notifications.title.followedYou" = "vous a suivi";
"notifications.title.mentionedYou" = "vous a mentionné";
"notifications.title.boosted" = "partagé";
"notifications.title.favourited" = "favori";
"notifications.title.postedStatus" = "statut posté";
"notifications.title.followRequest" = "demande de suivi";
"notifications.title.poll" = "sondage";
"notifications.title.updatedStatus" = "statut mis à jour";
"notifications.title.signedUp" = "s'inscrire";
"notifications.title.newReport" = "nouveau rapport";
"notifications.error.loadingNotificationsFailed" = "Chargement des notifications échoué.";
// Mark: Compose view.
"compose.navigationBar.title" = "Composer";
"compose.title.everyone" = "Tout le monde";
"compose.title.unlisted" = "Non listé";
"compose.title.followers" = "Abonnés";
"compose.title.attachPhotoFull" = "Joignez une photo et écrivez ce qui vous convient";
"compose.title.attachPhotoMini" = "Ecrivez ce qui vous convient";
"compose.title.publish" = "Publier";
"compose.title.cancel" = "Annuler";
"compose.title.writeContentWarning" = "Rédaction d'un avertissement sur le contenu";
"compose.title.commentsWillBeDisabled" = "Les commentaires seront désactivés";
"compose.title.statusPublished" = "Statuts publiés";
"compose.title.tryToUpload" = "Essayer de télécharger";
"compose.title.delete" = "Supprimer";
"compose.title.edit" = "Editer";
"compose.title.photos" = "Albums photos";
"compose.title.camera" = "Prendre une photo";
"compose.title.files" = "Parcourir les fichiers";
"compose.title.missingAltTexts" = "Manque un texte ALT";
"compose.title.missingAltTextsWarning" = "Toutes les images n'ont pas été décrites pour les malvoyants. Souhaitez-vous tout de même envoyer des photos ?";
"compose.error.loadingPhotosFailed" = "Impossible de récupérer l'image depuis la bibliothèque.";
"compose.error.postingPhotoFailed" = "Erreur pendant le post de la photo.";
"compose.error.postingStatusFailed" = "Erreur pendant le post du statut.";
// Mark: Photo editor view.
"photoEdit.navigationBar.title" = "Détails sur la photo";
"photoEdit.title.photo" = "Photo";
"photoEdit.title.accessibility" = "Accessibilité";
"photoEdit.title.accessibilityDescription" = "Description pour les malvoyants";
"photoEdit.title.save" = "Enregistrer";
"photoEdit.title.cancel" = "Annuler";
"photoEdit.error.updatePhotoFailed" = "Erreur pendant la mise à jour de la photo.";
// Mark: Place selector view.
"placeSelector.navigationBar.title" = "Lieux";
"placeSelector.title.search" = "Rechercher...";
"placeSelector.title.buttonSearch" = "Rechercher";
"placeSelector.title.cancel" = "Annuler";
"placeSelector.error.loadingPlacesFailed" = "Chargement des notifications échoué.";
// Mark: Settings view.
"settings.navigationBar.title" = "Paramètres";
"settings.title.close" = "Fermer";
"settings.title.version" = "Version";
"settings.title.accounts" = "Compte";
"settings.title.newAccount" = "Nouveau compte";
"settings.title.accent" = "Accent";
"settings.title.theme" = "Thème";
"settings.title.system" = "Système";
"settings.title.light" = "Clair";
"settings.title.dark" = "Sombre";
"settings.title.avatar" = "Avatar";
"settings.title.circle" = "Cercle";
"settings.title.rounderRectangle" = "Rectangle arrondi";
"settings.title.other" = "Autre";
"settings.title.thirdParty" = "Tiers";
"settings.title.reportBug" = "Rapporter un bogue";
"settings.title.githubIssues" = "Problèmes sur Github";
"settings.title.follow" = "Me suivre";
"settings.title.support" = "Support";
"settings.title.thankYouTitle" = "Merci 💕";
"settings.title.thankYouMessage" = "Merci pour votre achat. Les achats, petits et grands, nous aident à réaliser notre rêve de fournir des produits de la meilleure qualité à nos clients. Nous espérons que vous aimez Vernissage.";
"settings.title.thankYouClose" = "Fermer";
"settings.title.haptics" = "Haptique";
"settings.title.hapticsTabSelection" = "Sélection de l'onglet";
"settings.title.hapticsButtonPress" = "Appui sur un bouton";
"settings.title.hapticsListRefresh" = "Rafraîchir la liste";
"settings.title.hapticsAnimationFinished" = "Animation finie";
"settings.title.mediaSettings" = "Paramètres du media";
"settings.title.alwaysShowSensitiveTitle" = "Toujours montrer les NSFW";
"settings.title.alwaysShowSensitiveDescription" = "Forcer l'affichage de tous les media NFSW (contenu sensible) sans avertissement";
"settings.title.alwaysShowAltTitle" = "Afficher le texte alternatif";
"settings.title.alwaysShowAltDescription" = "Afficher le texte alternatif si présent sur l'écran des détails des statuts";
"settings.title.general" = "Général";
"settings.title.applicationIcon" = "Icône de l'application";
"settings.title.followVernissage" = "Suivre Vernissage";
"settings.title.mastodonAccount" = "Compte Mastodon";
"settings.title.pixelfedAccount" = "Compte Pixelfed";
"settings.title.openPage" = "Ouvrir";
"settings.title.privacyPolicy" = "Politique de confidentialité";
"settings.title.terms" = "Conditions générales d'utilisation";
"settings.title.sourceCode" = "Code source";
"settings.title.rate" = "Noter Vernissage";
"settings.title.socials" = "Social";
"settings.title.menuPosition" = "Position du menu";
"settings.title.topMenu" = "Barre de navigation";
"settings.title.bottomRightMenu" = "En bas à droite";
"settings.title.bottomLeftMenu" = "En bas à gauche";
"settings.title.showAvatars" = "Afficher les avatars";
"settings.title.showAvatarsOnTimeline" = "Les avatars sont affichés sur la timeline";
"settings.title.showFavourite" = "Afficher les favoris";
"settings.title.showFavouriteOnTimeline" = "Les favoris sont affichés sur la timeline";
"settings.title.showAltText" = "Afficher l'icône ALT";
"settings.title.showAltTextOnTimeline" = "L'icône ALT sera affichée sur la timeline";
"settings.title.warnAboutMissingAltTitle" = "Avertir de l'absence de texte ALT";
"settings.title.warnAboutMissingAltDescription" = "Un avertissement concernant les textes ALT manquants sera affiché avant la publication d'un nouveau message.";
"settings.title.enableReboostOnTimeline" = "Show boosted statuses";
"settings.title.enableReboostOnTimelineDescription" = "Boosted statuses will be visible on your home timeline.";
"settings.title.hideStatusesWithoutAlt" = "Hide statuses without ALT text";
"settings.title.hideStatusesWithoutAltDescription" = "Statuses without ALT text will not be visible on your home timeline.";
// Mark: Signin view.
"signin.navigationBar.title" = "Se connecter à Pixelfed";
"signin.title.serverAddress" = "Adresse du serveur";
"signin.title.signIn" = "Connecter";
"signin.title.enterServerAddress" = "Entrer l'adresse du server";
"signin.title.howToJoinLink" = "Comment rejoindre Pixelfed";
"signin.title.chooseServer" = "Ou choisissez un sereveur Pixelfed";
"signin.title.amountOfUsers" = "%d Utilisateurs";
"signin.title.amountOStatuses" = "%d statuts";
"signin.error.communicationFailed" = "La communication avec le server a échoué.";
// Mark: Status view.
"status.navigationBar.title" = "Détails";
"status.title.uploaded" = "Envoyé";
"status.title.via" = "via %@";
"status.title.reboostedBy" = "Partagé par";
"status.title.favouritedBy" = "Favoris par";
"status.title.openInBrowser" = "Ouvrir dans un navigateur";
"status.title.shareStatus" = "Partger le statut";
"status.title.yourStatus" = "Votre statut";
"status.title.delete" = "Supprimer";
"status.title.reboosted" = "Partagé";
"status.title.unreboosted" = "Enlever le partage";
"status.title.favourited" = "Favorisé";
"status.title.unfavourited" = "Enlever le favoris";
"status.title.bookmarked" = "Marque-pages effectué";
"status.title.unbookmarked" = "Marque-pages enlevé";
"status.title.statusDeleted" = "Statut supprimé";
"status.title.reboost" = "Partagé";
"status.title.unreboost" = "Enlever le partage";
"status.title.favourite" = "Favoris";
"status.title.unfavourite" = "Enlever le favoris";
"status.title.bookmark" = "Marque-pages";
"status.title.unbookmark" = "Marque-pages enlevé";
"status.title.comment" = "Commenter";
"status.title.report" = "Rapport";
"status.title.saveImage" = "Enregistrer l'image";
"status.title.showMediaDescription" = "Afficher la description du media";
"status.title.mediaDescription" = "Description du media";
"status.title.shareImage" = "Partager l'image";
"status.title.altText" = "ALT";
"status.error.loadingStatusFailed" = "Chargement du statut échoué.";
"status.error.notFound" = "Le statut n'existe plus.";
"status.error.loadingCommentsFailed" = "Les commentaires ne peuvent être téléchargés.";
"status.error.reboostFailed" = "L'action de partage a échoué.";
"status.error.favouriteFailed" = "L'action de favoris a échoué.";
"status.error.bookmarkFailed" = "L'action de marque-pages a échoué.";
"status.error.deleteFailed" = "L'action de suppression a échoué.";
// Mark: Accounts view.
"accounts.navigationBar.followers" = "Abonnés";
"accounts.navigationBar.following" = "Abonnements";
"accounts.navigationBar.favouritedBy" = "Favorisé par";
"accounts.navigationBar.reboostedBy" = "Partagé par";
"accounts.navigationBar.blocked" = "Comptes bloqués";
"accounts.navigationBar.mutes" = "Comptes mis en sourdine";
"accounts.title.noAccounts" = "Malheureusement, il n'y a personne ici.";
"accounts.error.loadingAccountsFailed" = "Le chargement des comptes a échoué.";
// Mark: Third party view.
"thirdParty.navigationBar.title" = "Tiers";
// Mark: Widget view.
"widget.title.photoDescription" = "Widget avec des photos de Pixelfed.";
"widget.title.qrCodeDescription" = "Widget avec QR Code vers votre profil Pixelfed.";
// Mark: In-app purchases.
"purchase.donut.title" = "Beignet";
"purchase.donut.description" = "Offrez-moi un beignet.";
"purchase.coffee.title" = "Café";
"purchase.coffee.description" = "Offrez-moi un café.";
"purchase.cake.title" = "Café et gâteau";
"purchase.cake.description" = "Offrez-moi un café et un gâteau.";
// Mark: Edit profile.
"editProfile.navigationBar.title" = "Editer le profil";
"editProfile.title.displayName" = "Afficher le nom";
"editProfile.title.bio" = "Bio";
"editProfile.title.website" = "Site web";
"editProfile.title.save" = "Enregistrer";
"editProfile.title.accountSaved" = "Le profil a été mis à jour.";
"editProfile.title.photoInfo" = "La photo modifiée sera visible dans l'application et sur le site web avec un petit délai.";
"editProfile.title.privateAccount" = "Compte privé";
"editProfile.title.privateAccountInfo" = "Lorsque votre compte est privé, seules les personnes que vous autorisez peuvent voir vos photos et vidéos sur Pixelfed. Les personnes qui vous suivent déjà ne seront pas affectées.";
"editProfile.error.saveAccountFailed" = "Enregistrement du profil échoué.";
"editProfile.error.loadingAvatarFailed" = "Chargement de l'avatar échoué.";
"editProfile.error.noProfileData" = "Les données du profil ne peuvent pas être affichées.";
"editProfile.error.loadingAccountFailed" = "Erreur lors du téléchargement du compte depuis le serveur.";
// Mark: Instance information.
"instance.navigationBar.title" = "Instance";
"instance.title.instanceInfo" = "Information sur l'instance";
"instance.title.name" = "Nom";
"instance.title.address" = "Addresse";
"instance.title.email" = "Email";
"instance.title.version" = "Version";
"instance.title.users" = "Utilisateurs";
"instance.title.posts" = "Posts";
"instance.title.domains" = "Domaines";
"instance.title.registrations" = "Inscriptions";
"instance.title.approvalRequired" = "Approbation requise";
"instance.title.rules" = "Règles de l'instance";
"instance.title.contact" = "Contact";
"instance.title.pixelfedAccount" = "Compte Pixelfed";
"instance.error.noInstanceData" = "Les données d'instance ne peuvent pas être affichées.";
"instance.error.loadingDataFailed" = "Erreur lors du téléchargement des données d'instance depuis le serveur.";
// Mark: Report screen.
"report.navigationBar.title" = "Rapport";
"report.title.close" = "Fermer";
"report.title.send" = "Envoyer";
"report.title.userReported" = "L'utilisateur a été signalé";
"report.title.postReported" = "Le post a été signalé";
"report.title.reportType" = "Type d'abus";
"report.title.spam" = "C'est un spam";
"report.title.sensitive" = "Nudité ou activité sexuelle";
"report.title.abusive" = "Discours ou symboles haineux";
"report.title.underage" = "Compte mineur";
"report.title.violence" = "Violence ou organisations dangereuses";
"report.title.copyright" = "Violation des droits d'auteur";
"report.title.impersonation" = "Usurpation d'identité";
"report.title.scam" = "Intimidation ou harcèlement";
"report.title.terrorism" = "Le terrorisme";
"report.error.notReported" = "Erreur lors de l'envoi du rapport.";
// Mark: Following requests.
"followingRequests.navigationBar.title" = "Suivre les demandes";
"followingRequests.title.approve" = "Approuver";
"followingRequests.title.reject" = "Rejeter";
"followingRequests.error.approve" = "Erreur lors de l'approbation de la demande.";
"followingRequests.error.reject" = "Erreur lors du rejet de la demande.";

View File

@ -1,381 +0,0 @@
// MARK: Common strings.
"global.title.contentWarning" = "Wrażliwe treści";
"global.title.seePost" = "Pokaż zdjęcie";
"global.title.refresh" = "Odśwież";
"global.title.momentsAgo" = "chwilę temu";
"global.title.success" = "Sukces";
"global.title.photoSaved" = "Zdjęcie zostało zapisane.";
"global.title.ok" = "OK";
"global.title.showMore" = "Pokaż więcej";
"global.title.showLess" = "Pokaż mniej";
"global.title.close" = "Zamknij";
"global.error.refreshingCredentialsTitle" = "Błąd odświeżania danych uwierzytelniających.";
"global.error.refreshingCredentialsSubtitle" = "Prosimy o ponowne zalogowanie się do Pixelfed.";
// MARK: Global errors.
"global.error.unexpected" = "Wystąpił nieoczekiwany błąd.";
"global.error.statusesNotRetrieved" = "Statusy nie zostały pobrane.";
"global.error.errorDuringDownloadStatuses" = "Błąd podczas pobierania statusów.";
"global.error.errorDuringDownloadHashtag" = "Błąd podczas pobierania taga.";
"global.error.hashtagNotExists" = "Tag nie istnieje.";
"global.error.errorDuringImageDownload" = "Błąd podczas pobierania zdjęcia.";
"global.error.canceledImageDownload" = "Pobieranie zdjęcia zostało anulowane.";
"global.error.errorDuringDataLoad" = "Błąd podczas pobierania danych.";
"global.error.errorDuringUserRead" = "Błąd podczas odczytu danych użytkownika.";
"global.error.badUrlServer" = "Niepoprawny adres serwera.";
"global.error.accessTokenNotFound" = "Brak tokenu dostępu.";
"global.error.errorDuringDownloadStatus" = "Błąd podczas pobierania statusu.";
"global.error.errorDuringPurchaseVerification" = "Błąd podczas weryfikacji płatności.";
// MARK: Main view (main navigation bar).
"mainview.tab.homeTimeline" = "Główna";
"mainview.tab.localTimeline" = "Lokalne";
"mainview.tab.federatedTimeline" = "Globalne";
"mainview.tab.trendingPhotos" = "Zdjęcia";
"mainview.tab.trendingTags" = "Tagi";
"mainview.tab.trendingAccounts" = "Użytkownicy";
"mainview.tab.userProfile" = "Profil";
"mainview.tab.notifications" = "Powiadomienia";
"mainview.tab.search" = "Wyszukaj";
"mainview.tab.trending" = "Popularne";
// MARK: Main view (leading navigation bar).
"mainview.menu.settings" = "Ustawienia";
// MARK: Main view (error notifications).
"mainview.error.switchAccounts" = "Błąd podczas przełączania kont.";
// MARK: Home timeline.
"home.title.allCaughtUp" = "Jesteś na bieżąco";
"home.title.noPhotos" = "Niestety nie ma jeszcze żadnych zdjęć.";
// MARK: Statuses timeline (local/federated/favourite/bookmarks etc.).
"statuses.navigationBar.localTimeline" = "Lokalne";
"statuses.navigationBar.federatedTimeline" = "Globalne";
"statuses.navigationBar.favourites" = "Polubione";
"statuses.navigationBar.bookmarks" = "Zakładki";
"statuses.title.noPhotos" = "Niestety nie ma jeszcze żadnych zdjęć.";
"statuses.title.tagFollowed" = "Od teraz śledzisz taga.";
"statuses.title.tagUnfollowed" = "Nie śledzisz już taga.";
"statuses.error.loadingStatusesFailed" = "Błąd podczas wczytywania statusów.";
"statuses.error.tagFollowFailed" = "Błąd podczas żądania śledzenia taga.";
"statuses.error.tagUnfollowFailed" = "Błąd podczas wyłączenia śledzenia taga.";
// Mark: Search view.
"search.navigationBar.title" = "Wyszukaj";
"search.title.placeholder" = "Wyszukaj...";
"search.title.usersWith" = "Użytkownicy zawierający %@";
"search.title.goToUser" = "Przejdź do użytkownika %@";
"search.title.hashtagWith" = "Tagi zawierające %@";
"search.title.goToHashtag" = "Przejdź do taga %@";
// Mark: Trending statuses.
"trendingStatuses.navigationBar.title" = "Zdjęcia";
"trendingStatuses.title.daily" = "Dzień";
"trendingStatuses.title.monthly" = "Miesiąc";
"trendingStatuses.title.yearly" = "Rok";
"trendingStatuses.error.loadingStatusesFailed" = "Błąd podczas wczytywania statusów.";
"trendingStatuses.title.noPhotos" = "Niestety nie ma jeszcze żadnych zdjęć.";
// Mark: Trending tags.
"tags.navigationBar.trendingTitle" = "Tagi";
"tags.navigationBar.searchTitle" = "Tagi";
"tags.navigationBar.followedTitle" = "Obserwowane tagi";
"tags.title.noTags" = "Niestety nie ma jeszcze żadnych tagów.";
"tags.title.amountOfPosts" = "%d statusów";
"tags.error.loadingTagsFailed" = "Błąd podczas wczytywania tagów.";
// Mark: Trending accounts.
"trendingAccounts.navigationBar.title" = "Użytkownicy";
"trendingAccounts.title.noAccounts" = "Niestety nie ma tutaj nikogo.";
"trendingAccounts.error.loadingAccountsFailed" = "Błąd podczas wczytywania użytkownikow.";
// Mark: User profile view.
"userProfile.title.openInBrowser" = "Otwórz w przeglądarce";
"userProfile.title.share" = "Udostępnij";
"userProfile.title.unmute" = "Wyłącz wyciszenie";
"userProfile.title.mute" = "Wycisz";
"userProfile.title.unblock" = "Odblokuj";
"userProfile.title.block" = "Zablokuj";
"userProfile.title.favourites" = "Polubione";
"userProfile.title.bookmarks" = "Zakładki";
"userProfile.title.followedTags" = "Obserwowane tagi";
"userProfile.title.posts" = "Statusy";
"userProfile.title.followers" = "Obserwujący";
"userProfile.title.following" = "Obserwowani";
"userProfile.title.joined" = "Dołączył(a) %@";
"userProfile.title.unfollow" = "Przestań obserwować";
"userProfile.title.follow" = "Obserwuj";
"userProfile.title.instance" = "Informacje o instancji";
"userProfile.title.blocks" = "Zablokowane konta";
"userProfile.title.mutes" = "Wyciszone konta";
"userProfile.title.muted" = "Konto wyciszone";
"userProfile.title.unmuted" = "Wyciszenie wyłączone";
"userProfile.title.blocked" = "Konto zablokowane";
"userProfile.title.unblocked" = "Konto odblokowane";
"userProfile.title.report" = "Zgłoś";
"userProfile.title.followsYou" = "Obserwuje ciebie";
"userProfile.title.requestFollow" = "Poproś o obserwowanie";
"userProfile.title.cancelRequestFollow" = "Anuluj prośbę";
"userProfile.title.followRequests" = "Prośby o obserwowanie";
"userProfile.title.privateProfileTitle" = "To konto jest prywatne.";
"userProfile.title.privateProfileSubtitle" = "Tylko zaakceptowani użytkownicy mogą przeglądać zdjęcia.";
"userProfile.error.notExists" = "Konto nie istnieje.";
"userProfile.error.notExists" = "Błąd podczas pobierania danych użytkownika.";
"userProfile.error.mute" = "Błąd podczas wyciszania użytkownika.";
"userProfile.error.block" = "Błąd podczas blokowania/odblokowywania użytkownika.";
"userProfile.error.relationship" = "Błąd podczas zmiany relacji z użytkownikiem.";
"userProfile.title.edit" = "Edytuj";
"userProfile.title.muted" = "Wyciszony";
"userProfile.title.blocked" = "Zablokowany";
"userProfile.title.enableBoosts" = "Wyświetl podbicia";
"userProfile.title.disableBoosts" = "Ukryj podbicia";
"userProfile.title.boostedStatusesMuted" = "Podbicia ukryte";
// Mark: Notifications view.
"notifications.navigationBar.title" = "Powiadomienia";
"notifications.title.noNotifications" = "Niestety nic tutaj nie ma.";
"notifications.title.followedYou" = "obserwuje ciebie";
"notifications.title.mentionedYou" = "wspomniał ciebie";
"notifications.title.boosted" = "podbił";
"notifications.title.favourited" = "polubił";
"notifications.title.postedStatus" = "stworzył status";
"notifications.title.followRequest" = "chce obserwować";
"notifications.title.poll" = "ankieta";
"notifications.title.updatedStatus" = "zaktualizował status";
"notifications.title.signedUp" = "zalogował się";
"notifications.title.newReport" = "nowy raport";
"notifications.error.loadingNotificationsFailed" = "Błąd podczas wczytywania powiadomień.";
// Mark: Compose view.
"compose.navigationBar.title" = "Utwórz";
"compose.title.everyone" = "Publiczny";
"compose.title.unlisted" = "Publiczny (niewidoczny)";
"compose.title.followers" = "Tylko obserwujący";
"compose.title.attachPhotoFull" = "Dołącz zdjęcie i napisz, co myślisz";
"compose.title.attachPhotoMini" = "Wpisz, co masz na myśli";
"compose.title.publish" = "Wyślij";
"compose.title.cancel" = "Anuluj";
"compose.title.writeContentWarning" = "Napisz ostrzeżenie o treści";
"compose.title.commentsWillBeDisabled" = "Komentarze zostaną wyłączone";
"compose.title.statusPublished" = "Stan opublikowany";
"compose.title.tryToUpload" = "Ponów";
"compose.title.delete" = "Usuń";
"compose.title.edit" = "Edytuj";
"compose.title.photos" = "Biblioteka zdjęć";
"compose.title.camera" = "Zrób zdjęcie";
"compose.title.files" = "Przeglądaj pliki";
"compose.title.missingAltTexts" = "Brakuje tekstów ALT";
"compose.title.missingAltTextsWarning" = "Nie wszystkie zdjęcia zostały opisane dla niedowidzących. Czy pomimo tego chcesz je wysłać?";
"compose.error.loadingPhotosFailed" = "Nie można pobrać zdjęcia z biblioteki.";
"compose.error.postingPhotoFailed" = "Błąd podczas publikowania zdjęcia.";
"compose.error.postingStatusFailed" = "Błąd podczas wysyłania statusu.";
// Mark: Photo editor view.
"photoEdit.navigationBar.title" = "Szczegóły zdjęcia";
"photoEdit.title.photo" = "Zdjęcie";
"photoEdit.title.accessibility" = "Dostępność";
"photoEdit.title.accessibilityDescription" = "Opis dla osób niedowidzących";
"photoEdit.title.save" = "Zapisz";
"photoEdit.title.cancel" = "Anuluj";
"photoEdit.error.updatePhotoFailed" = "Błąd podczas aktualizowania zdjęcia.";
// Mark: Place selector view.
"placeSelector.navigationBar.title" = "Lokalizacja";
"placeSelector.title.search" = "Wyszukaj...";
"placeSelector.title.buttonSearch" = "Szukaj";
"placeSelector.title.cancel" = "Anuluj";
"placeSelector.error.loadingPlacesFailed" = "Błąd podczas wczytywanie lokalizacji.";
// Mark: Settings view.
"settings.navigationBar.title" = "Ustawienia";
"settings.title.close" = "Zamknij";
"settings.title.version" = "Wersja";
"settings.title.accounts" = "Konta";
"settings.title.newAccount" = "Dodaj konto";
"settings.title.accent" = "Akcent";
"settings.title.theme" = "Wygląd";
"settings.title.system" = "Systemowy";
"settings.title.light" = "Jasny";
"settings.title.dark" = "Ciemny";
"settings.title.avatar" = "Awatar";
"settings.title.circle" = "Okrągły";
"settings.title.rounderRectangle" = "Zaokrąglony kwadratowy";
"settings.title.other" = "Inne";
"settings.title.thirdParty" = "Zewnętrzne biblioteki";
"settings.title.reportBug" = "Zgłoś błąd";
"settings.title.githubIssues" = "Błędy na Github";
"settings.title.follow" = "Obserwuj mnie";
"settings.title.support" = "Wsparcie";
"settings.title.thankYouTitle" = "Dziękuję 💕";
"settings.title.thankYouMessage" = "Dziękujemy za twój zakup. Zakupy zarówno te duże, jak i te małe pomagają nam w realizacji marzenia o dostarczaniu naszym klientom produktów najwyższej jakości. Mamy nadzieję, że Vernissage spełnia Twoje oczekiwania.";
"settings.title.thankYouClose" = "Zamknij";
"settings.title.haptics" = "Haptyka";
"settings.title.hapticsTabSelection" = "Wybór zakładki";
"settings.title.hapticsButtonPress" = "Naciśnięcie przycisku";
"settings.title.hapticsListRefresh" = "Odświeżanie listy";
"settings.title.hapticsAnimationFinished" = "Zakończenie animacji";
"settings.title.mediaSettings" = "Ustawienia mediów";
"settings.title.alwaysShowSensitiveTitle" = "Zawsze pokazuj statusy NSFW";
"settings.title.alwaysShowSensitiveDescription" = "Wymuś pokazywanie statusów NFSW (czułych) bez ostrzeżeń";
"settings.title.alwaysShowAltTitle" = "Pokaż tekst alternatywny";
"settings.title.alwaysShowAltDescription" = "Pokaż alternatywny tekst, jeśli jest obecny na szczegółach statusu";
"settings.title.general" = "Ogólne";
"settings.title.applicationIcon" = "Ikona aplikacji";
"settings.title.followVernissage" = "Obserwuj Vernissage";
"settings.title.mastodonAccount" = "Konto Mastodon";
"settings.title.pixelfedAccount" = "Konto Pixelfed";
"settings.title.openPage" = "Otwórz";
"settings.title.privacyPolicy" = "Polityka prywatności";
"settings.title.terms" = "Zasady i warunki";
"settings.title.sourceCode" = "Kod źródłowy";
"settings.title.rate" = "Oceń Vernissage";
"settings.title.socials" = "Społeczności";
"settings.title.menuPosition" = "Pozycja menu";
"settings.title.topMenu" = "Panel tytułowy";
"settings.title.bottomRightMenu" = "Dolny prawy";
"settings.title.bottomLeftMenu" = "Dolny lewy";
"settings.title.showAvatars" = "Wyświetlaj awatary";
"settings.title.showAvatarsOnTimeline" = "Awatary będą widoczne na osiach zdjęć";
"settings.title.showFavourite" = "Wyświetlaj polubienia";
"settings.title.showFavouriteOnTimeline" = "Polubienia będą widoczne na osiach zdjęć";
"settings.title.showAltText" = "Wyświetlaj ikonę ALT";
"settings.title.showAltTextOnTimeline" = "Ikony ALT będą widonczne na osiach zdjęć";
"settings.title.warnAboutMissingAltTitle" = "Ostrzeganie o brakującym tekście ALT";
"settings.title.warnAboutMissingAltDescription" = "Ostrzeżenie o brakujących tekstach ALT będzie wyświetlane przed opublikowaniem nowego statusu.";
"settings.title.enableReboostOnTimeline" = "Wyświetl podbite statusy";
"settings.title.enableReboostOnTimelineDescription" = "Podbite statusy będą widoczne na twojej osi czasu.";
"settings.title.hideStatusesWithoutAlt" = "Ukryj statusy bez tekstu ALT";
"settings.title.hideStatusesWithoutAltDescription" = "Statusy bez tekstu ALT nie będą wyświetlane na twojej osi czasu.";
// Mark: Signin view.
"signin.navigationBar.title" = "Zaloguj się do Pixelfed";
"signin.title.serverAddress" = "Adres serwera";
"signin.title.signIn" = "Zaloguj się";
"signin.title.enterServerAddress" = "Wpisz adres serwera";
"signin.title.howToJoinLink" = "Jak przyłączyć się do Pixelfed";
"signin.title.chooseServer" = "Lub wybierz serwer Pixelfed";
"signin.title.amountOfUsers" = "%d użytkowników";
"signin.title.amountOStatuses" = "%d statusów";
"signin.error.communicationFailed" = "Błąd podczas komunikacji z serwerem.";
// Mark: Status view.
"status.navigationBar.title" = "Szczegóły";
"status.title.uploaded" = "Wysłano";
"status.title.via" = "przez %@";
"status.title.reboostedBy" = "Podbite przez";
"status.title.favouritedBy" = "Polubione przez";
"status.title.openInBrowser" = "Otwórz w przeglądarce";
"status.title.shareStatus" = "Udostępnij status";
"status.title.yourStatus" = "Twój status";
"status.title.delete" = "Usuń";
"status.title.reboosted" = "Podbite";
"status.title.unreboosted" = "Podbicie wycofane";
"status.title.favourited" = "Polubione";
"status.title.unfavourited" = "Polubienie wycofane";
"status.title.bookmarked" = "Dodane do zakładek";
"status.title.unbookmarked" = "Usunięte z zakładek";
"status.title.statusDeleted" = "Status usunięty";
"status.title.reboost" = "Podbij";
"status.title.unreboost" = "Cofnij podbicie";
"status.title.favourite" = "Polub";
"status.title.unfavourite" = "Cofnij polubienie";
"status.title.bookmark" = "Dodaj do zakładek";
"status.title.unbookmark" = "Usuń z zakładek";
"status.title.comment" = "Skomentuj";
"status.title.report" = "Zgłoś";
"status.title.saveImage" = "Zapisz zdjęcie";
"status.title.showMediaDescription" = "Pokaż opis zdjęcia";
"status.title.mediaDescription" = "Opis zdjęcia";
"status.title.shareImage" = "Udostępnij zdjęcie";
"status.title.altText" = "ALT";
"status.error.loadingStatusFailed" = "Błąd podczas wczytywanie statusu.";
"status.error.notFound" = "Status już nie istnieje.";
"status.error.loadingCommentsFailed" =" Błąd podczas wczytywanie komentarzy.";
"status.error.reboostFailed" = "Błąd podczas podbijania.";
"status.error.favouriteFailed" = "Błąd podczas polubiania.";
"status.error.bookmarkFailed" = "Błąd podczas dodawania/usuwania z zakładek.";
"status.error.deleteFailed" = "Błąd podczas usuwania.";
// Mark: Accounts view.
"accounts.navigationBar.followers" = "Obserwujący";
"accounts.navigationBar.following" = "Obserwowani";
"accounts.navigationBar.favouritedBy" = "Polubione przez";
"accounts.navigationBar.reboostedBy" = "Podbite przez";
"accounts.navigationBar.blocked" = "Zablokowani";
"accounts.navigationBar.mutes" = "Wyciszeni";
"accounts.title.noAccounts" = "Niestety nie ma tutaj nikogo.";
"accounts.error.loadingAccountsFailed" = "Błąd podczas wczytywania użytkownikow.";
// Mark: Third party view.
"thirdParty.navigationBar.title" = "Zewnętrzne biblioteki";
// Mark: Widget view.
"widget.title.photoDescription" = "Widget ze zdjęciami z Pixelfed.";
"widget.title.qrCodeDescription" = "Widget z QR kodem do profilu na Pixelfed.";
// Mark: In-app purchases.
"purchase.donut.title" = "Pączek";
"purchase.donut.description" = "Poczęstuj mnie pączkiem.";
"purchase.coffee.title" = "Kawa";
"purchase.coffee.description" = "Poczęstuj mnie kawą.";
"purchase.cake.title" = "Kawa z ciastkiem";
"purchase.cake.description" = "Poczęstuj mnie kawą i ciastkiem.";
// Mark: Edit profile.
"editProfile.navigationBar.title" = "Edutuj profil";
"editProfile.title.displayName" = "Wyświetlana nazwa";
"editProfile.title.bio" = "Bio";
"editProfile.title.website" = "Strona";
"editProfile.title.save" = "Zapisz";
"editProfile.title.accountSaved" = "Profil zaktualizowano.";
"editProfile.title.photoInfo" = "Zmienione zdjęcie będzie widoczne w aplikacji oraz na stronie z małym opóźnieniem.";
"editProfile.title.privateAccount" = "Konto prywatne";
"editProfile.title.privateAccountInfo" = "Kiedy Twoje konto jest prywatne, tylko osoby, które zaakceptujesz mogą oglądać Twoje zdjęcia i filmy na Pixelfed. Nie wpłynie to na Twoich obecnych obserwujących.";
"editProfile.error.saveAccountFailed" = "Błąd podczas aktualizacji profilu.";
"editProfile.error.loadingAvatarFailed" = "Błąd podczas wczytywania zdjęcia.";
"editProfile.error.noProfileData" = "Dane profilu nie mogą zostać wyświetlone.";
"editProfile.error.loadingAccountFailed" = "Błąd podczas pobierania profilu użytkownika.";
// Mark: Instance information.
"instance.navigationBar.title" = "Instancja";
"instance.title.instanceInfo" = "Informacja o instancji";
"instance.title.name" = "Nazwa";
"instance.title.address" = "Adres";
"instance.title.email" = "Email";
"instance.title.version" = "Wersja";
"instance.title.users" = "Użytkownicy";
"instance.title.posts" = "Statusów";
"instance.title.domains" = "Domen";
"instance.title.registrations" = "Rejestracja";
"instance.title.approvalRequired" = "Akeptowanie rejestracji";
"instance.title.rules" = "Reguły instancji";
"instance.title.contact" = "Kontakt";
"instance.title.pixelfedAccount" = "Konto Pixelfed";
"instance.error.noInstanceData" = "Dane instancji nie mogą zostać wyświetlone.";
"instance.error.loadingDataFailed" = "Błąd podczas pobierania danych instancji.";
// Mark: Report screen.
"report.navigationBar.title" = "Zgłoś";
"report.title.close" = "Zamknij";
"report.title.send" = "Wyślij";
"report.title.userReported" = "Użytkownik został zgłoszony.";
"report.title.postReported" = "Status został zgłoszony.";
"report.title.reportType" = "Typ nadużycia";
"report.title.spam" = "Spam";
"report.title.sensitive" = "Nagość lub aktywność seksualna";
"report.title.abusive" = "Mowa lub symbole nienawiści";
"report.title.underage" = "Konto niepełnoletniego";
"report.title.violence" = "Przemoc lub niebezpieczne organizacje";
"report.title.copyright" = "Naruszenie praw autorskich";
"report.title.impersonation" = "Podszywanie się";
"report.title.scam" = "Znęcanie się lub nękanie";
"report.title.terrorism" = "Terroryzm";
"report.error.notReported" = "Błąd podczas wysyłania zgłoszenia.";
// Mark: Following requests.
"followingRequests.navigationBar.title" = "Prośby o obserwowanie";
"followingRequests.title.approve" = "Zaakceptuj";
"followingRequests.title.reject" = "Odrzuć";
"followingRequests.error.approve" = "Błąd podczas akceptowania prośby.";
"followingRequests.error.reject" = "Błąd podczas odrzucania prośby.";

View File

@ -1,4 +1,4 @@
// swift-tools-version: 5.7
// swift-tools-version: 5.9
// The swift-tools-version declares the minimum version of Swift required to build this package.
import PackageDescription
@ -7,9 +7,7 @@ let package = Package(
name: "PixelfedKit",
defaultLocalization: "en",
platforms: [
.iOS(.v16),
.macOS(.v12),
.watchOS(.v8)
.iOS(.v17)
],
products: [
// Products define the executables and libraries a package produces, and make them visible to other packages.

View File

@ -16,7 +16,7 @@ extension NetworkError: LocalizedError {
switch self {
case .notSuccessResponse(let response):
let statusCode = response.statusCode()
let localizedString = NSLocalizedString("global.error.notSuccessResponse",
bundle: Bundle.module,
comment: "It's error returned from remote server. Request URL: '\(response.url?.string ?? "unknown")'.")

View File

@ -0,0 +1,286 @@
{
"sourceLanguage" : "en",
"strings" : {
"global.error.notSuccessResponse" : {
"comment" : "It's error returned from remote server. Request URL: '(response.url?.string ?? \"unknown\")'.",
"localizations" : {
"en" : {
"stringUnit" : {
"state" : "translated",
"value" : "Server response: %@."
}
},
"es" : {
"stringUnit" : {
"state" : "translated",
"value" : "Respuesta del servidor: %@."
}
},
"eu" : {
"stringUnit" : {
"state" : "translated",
"value" : "Zerbitzariaren erantzuna: %@."
}
},
"fr" : {
"stringUnit" : {
"state" : "translated",
"value" : "Réponse du serveur : %@."
}
},
"pl" : {
"stringUnit" : {
"state" : "translated",
"value" : "Odpowiedź serwera: %@."
}
}
}
},
"global.error.unknownError" : {
"comment" : "Response doesn't contains any information about request status.",
"localizations" : {
"en" : {
"stringUnit" : {
"state" : "translated",
"value" : "Unexpected error."
}
},
"es" : {
"stringUnit" : {
"state" : "translated",
"value" : "Error inesperado."
}
},
"eu" : {
"stringUnit" : {
"state" : "translated",
"value" : "Espero ez zen errorea."
}
},
"fr" : {
"stringUnit" : {
"state" : "translated",
"value" : "Erreur inattendue."
}
},
"pl" : {
"stringUnit" : {
"state" : "translated",
"value" : "Nieznany błąd serwera."
}
}
}
},
"report.error.duplicate" : {
"comment" : "The report has already been sent.",
"localizations" : {
"en" : {
"stringUnit" : {
"state" : "translated",
"value" : "The report has already been sent."
}
},
"es" : {
"stringUnit" : {
"state" : "translated",
"value" : "El informe ya ha sido enviado."
}
},
"eu" : {
"stringUnit" : {
"state" : "translated",
"value" : "Txostena bidali da dagoeneko."
}
},
"fr" : {
"stringUnit" : {
"state" : "translated",
"value" : "Le rapport a déjà été envoyé."
}
},
"pl" : {
"stringUnit" : {
"state" : "translated",
"value" : "Zgłoszenie zostało już wysłane."
}
}
}
},
"report.error.invalidObject" : {
"comment" : "Invalid object type.",
"localizations" : {
"en" : {
"stringUnit" : {
"state" : "translated",
"value" : "Invalid object type."
}
},
"es" : {
"stringUnit" : {
"state" : "translated",
"value" : "Tipo de objeto no válido."
}
},
"eu" : {
"stringUnit" : {
"state" : "translated",
"value" : "Elementu-mota ez da baliozkoa."
}
},
"fr" : {
"stringUnit" : {
"state" : "translated",
"value" : "Type d'objet non valide."
}
},
"pl" : {
"stringUnit" : {
"state" : "translated",
"value" : "Niepoprawny typ obiektu."
}
}
}
},
"report.error.invalidObjectId" : {
"comment" : "Incorrect object Id.",
"localizations" : {
"en" : {
"stringUnit" : {
"state" : "translated",
"value" : "Incorrect object Id."
}
},
"es" : {
"stringUnit" : {
"state" : "translated",
"value" : "Identificador de objeto incorrecto."
}
},
"eu" : {
"stringUnit" : {
"state" : "translated",
"value" : "Elementuaren IDa ez da zuzena."
}
},
"fr" : {
"stringUnit" : {
"state" : "translated",
"value" : "Identifiant d'object incorrect."
}
},
"pl" : {
"stringUnit" : {
"state" : "translated",
"value" : "Niepoprawny Id obiektu."
}
}
}
},
"report.error.invalidParameters" : {
"comment" : "Invalid report parameters.",
"localizations" : {
"en" : {
"stringUnit" : {
"state" : "translated",
"value" : "Invalid report parameters."
}
},
"es" : {
"stringUnit" : {
"state" : "translated",
"value" : "Parámetros de informe no válidos."
}
},
"eu" : {
"stringUnit" : {
"state" : "translated",
"value" : "Txostenaren parametroak ez dira baliozkoak."
}
},
"fr" : {
"stringUnit" : {
"state" : "translated",
"value" : "Paramètres de rapport non valides."
}
},
"pl" : {
"stringUnit" : {
"state" : "translated",
"value" : "Niepoprawne parametry zgłoszenia."
}
}
}
},
"report.error.invalidType" : {
"comment" : "Invalid report type.",
"localizations" : {
"en" : {
"stringUnit" : {
"state" : "translated",
"value" : "Invalid report type."
}
},
"es" : {
"stringUnit" : {
"state" : "translated",
"value" : "Tipo de informe no válido."
}
},
"eu" : {
"stringUnit" : {
"state" : "translated",
"value" : "Txosten-mota ez da baliozkoa."
}
},
"fr" : {
"stringUnit" : {
"state" : "translated",
"value" : "Type de rapport non valide."
}
},
"pl" : {
"stringUnit" : {
"state" : "translated",
"value" : "Niepoprawny typ raportu."
}
}
}
},
"report.error.noSelfReports" : {
"comment" : "Self-reporting is not allowed.",
"localizations" : {
"en" : {
"stringUnit" : {
"state" : "translated",
"value" : "Self-reporting is not allowed."
}
},
"es" : {
"stringUnit" : {
"state" : "translated",
"value" : "No se permite el autoinforme."
}
},
"eu" : {
"stringUnit" : {
"state" : "translated",
"value" : "Ezin duzu zure burua salatu."
}
},
"fr" : {
"stringUnit" : {
"state" : "translated",
"value" : "L'autodéclaration n'est pas autorisée."
}
},
"pl" : {
"stringUnit" : {
"state" : "translated",
"value" : "Zgłaszanie siebie jest niedozwolone."
}
}
}
}
},
"version" : "1.0"
}

View File

@ -12,12 +12,14 @@ public extension PixelfedClientAuthenticated {
sinceId: EntityId? = nil,
minId: EntityId? = nil,
limit: Int? = nil,
includeReblogs: Bool? = nil) async throws -> [Status] {
includeReblogs: Bool? = nil,
timeoutInterval: Double? = nil) async throws -> [Status] {
let request = try Self.request(
for: baseURL,
target: Pixelfed.Timelines.home(maxId, sinceId, minId, limit, includeReblogs),
withBearerToken: token
withBearerToken: token,
timeoutInterval: timeoutInterval
)
return try await downloadJson([Status].self, request: request)

View File

@ -1,11 +0,0 @@
// MARK: Network errors.
"global.error.notSuccessResponse" = "Server response: %@.";
"global.error.unknownError" = "Unexpected error.";
// Mark: Report errors.
"report.error.noSelfReports" = "Self-reporting is not allowed.";
"report.error.invalidObjectId" = "Incorrect object Id.";
"report.error.duplicate" = "The report has already been sent.";
"report.error.invalidParameters" = "Invalid report parameters.";
"report.error.invalidType" = "Invalid report type.";
"report.error.invalidObject" = "Invalid object type.";

View File

@ -1,11 +0,0 @@
// MARK: Network errors.
"global.error.notSuccessResponse" = "Respuesta del servidor: %@.";
"global.error.unknownError" = "Error inesperado.";
// Mark: Report errors.
"report.error.noSelfReports" = "No se permite el autoinforme.";
"report.error.invalidObjectId" = "Identificador de objeto incorrecto.";
"report.error.duplicate" = "El informe ya ha sido enviado.";
"report.error.invalidParameters" = "Parámetros de informe no válidos.";
"report.error.invalidType" = "Tipo de informe no válido.";
"report.error.invalidObject" = "Tipo de objeto no válido.";

View File

@ -1,11 +0,0 @@
// MARK: Network errors.
"global.error.notSuccessResponse" = "Zerbitzariaren erantzuna: %@.";
"global.error.unknownError" = "Espero ez zen errorea.";
// Mark: Report errors.
"report.error.noSelfReports" = "Ezin duzu zure burua salatu.";
"report.error.invalidObjectId" = "Elementuaren IDa ez da zuzena.";
"report.error.duplicate" = "Txostena bidali da dagoeneko.";
"report.error.invalidParameters" = "Txostenaren parametroak ez dira baliozkoak.";
"report.error.invalidType" = "Txosten-mota ez da baliozkoa.";
"report.error.invalidObject" = "Elementu-mota ez da baliozkoa.";

View File

@ -1,11 +0,0 @@
// MARK: Network errors.
"global.error.notSuccessResponse" = "Réponse du serveur : %@.";
"global.error.unknownError" = "Erreur inattendue.";
// Mark: Report errors.
"report.error.noSelfReports" = "L'autodéclaration n'est pas autorisée.";
"report.error.invalidObjectId" = "Identifiant d'object incorrect.";
"report.error.duplicate" = "Le rapport a déjà été envoyé.";
"report.error.invalidParameters" = "Paramètres de rapport non valides.";
"report.error.invalidType" = "Type de rapport non valide.";
"report.error.invalidObject" = "Type d'objet non valide.";

View File

@ -1,11 +0,0 @@
// MARK: Network errors.
"global.error.notSuccessResponse" = "Odpowiedź serwera: %@.";
"global.error.unknownError" = "Nieznany błąd serwera.";
// Mark: Report errors.
"report.error.noSelfReports" = "Zgłaszanie siebie jest niedozwolone.";
"report.error.invalidObjectId" = "Niepoprawny Id obiektu.";
"report.error.duplicate" = "Zgłoszenie zostało już wysłane.";
"report.error.invalidParameters" = "Niepoprawne parametry zgłoszenia.";
"report.error.invalidType" = "Niepoprawny typ raportu.";
"report.error.invalidObject" = "Niepoprawny typ obiektu.";

View File

@ -24,12 +24,20 @@ Thank you in advance for any, even the smallest help, with the development of th
## Translations
Creating new translation is pretty easy, all you need to do is to copy two folders:
- `Vernissage/Localization/en.lproj`
- `Vernissage/PixelfedKit/Sources/PixelfedKit/Resources/en.lproj`
Application is using new translation mechanism introduced in XCode 15 (xcstring). Here you can find description how this mechanism is working: [https://www.youtube.com/watch?v=jNbnwwLrJE8](https://www.youtube.com/watch?v=jNbnwwLrJE8).
In the name of the folders you have to put the code of the new language ([here](https://stackoverflow.com/a/13360348) you can find the languages codes).
Then you have to open files in these folders and translate them 🇯🇵🇫🇷🇨🇮🇧🇪. After translation create a Pull Request 👍.
In the applications we have several string catalogs:
- Localization/Localizable
- EnvironmentKit/Source/EnvironmentKit/Localizable
- WidgetKit/Source/WidgetKit/Localizable
- ServicesKit/Source/ServicesKit/Localizable
- PixelfedKit/Source/PixelfedKit/Localizable
- ClientKit/Source/ClientKit/Localizable
Right now it's very easy to find new (not translated yet) titles. Also you can mark titles which need some review.
However you need to have XCode 15 installed. There isn't right now good external tool that have similar features.
![translations](Resources/translations.png)
From time to time you have to come back and translate lines which has been added since the last translation.
@ -38,9 +46,5 @@ From time to time you have to come back and translate lines which has been added
Things that should be implemented in version 2.0:
- [ ] Use auto generated resources (Color/Images) instead static extensions (how to do this in separete Swift Packages?)
- [ ] Move to xcstring (new Xcode transaction system)
- [ ] Move to new Observable macro (iOS 17)
- [ ] Migrate to SwiftData (iOS 17)
- [ ] Use ViewModels
- [ ] Add tips (new TipKit framework in iOS 17)
- [ ] Use ViewModels?
- [ ] Enable swiftlint (https://github.com/realm/SwiftLint/issues/5053)

BIN
Resources/translations.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.5 MiB

View File

@ -1,14 +1,13 @@
// swift-tools-version: 5.8
// swift-tools-version: 5.9
// The swift-tools-version declares the minimum version of Swift required to build this package.
import PackageDescription
let package = Package(
name: "ServicesKit",
defaultLocalization: "en",
platforms: [
.iOS(.v16),
.macOS(.v12),
.watchOS(.v8)
.iOS(.v17)
],
products: [
// Products define the executables and libraries a package produces, and make them visible to other packages.

View File

@ -28,7 +28,7 @@ public class CacheImageService {
self.add(data: imageData, for: url)
}
} catch {
ErrorService.shared.handle(error, message: "Downloading image into cache failed.")
ErrorService.shared.handle(error, message: "global.error.downloadingImageFailed")
}
}

View File

@ -13,15 +13,29 @@ public class ErrorService {
public static let shared = ErrorService()
private init() { }
public func handle(_ error: Error, message: String, showToastr: Bool = false) {
let localizedMessage = NSLocalizedString(message, comment: "Error message")
public func handle(_ error: Error, message: LocalizedStringResource, showToastr: Bool = false) {
let localizedMessage = NSLocalizedString(message.key, comment: "Error message")
if showToastr {
switch error {
case is LocalizedError:
ToastrService.shared.showError(title: message, subtitle: error.localizedDescription)
default:
ToastrService.shared.showError(subtitle: localizedMessage)
ToastrService.shared.showError(title: "", subtitle: localizedMessage)
}
}
Logger.main.error("Error ['\(localizedMessage)']: \(error.localizedDescription)")
Logger.main.error("Error ['\(localizedMessage)']: \(error)")
}
public func handle(_ error: Error, localizedMessage: String, showToastr: Bool = false) {
if showToastr {
switch error {
case is LocalizedError:
ToastrService.shared.showError(localizedMessage: localizedMessage, subtitle: error.localizedDescription)
default:
ToastrService.shared.showError(title: "", subtitle: localizedMessage)
}
}

View File

@ -28,7 +28,6 @@ public class HapticService {
impactGenerator.prepare()
}
@MainActor
public func fireHaptic(of type: HapticType) {
guard supportsHaptics else { return }

View File

@ -0,0 +1,66 @@
{
"sourceLanguage" : "en",
"strings" : {
"" : {
},
"global.error.downloadingImageFailed" : {
"localizations" : {
"en" : {
"stringUnit" : {
"state" : "translated",
"value" : "Downloading image into cache failed."
}
},
"es" : {
"stringUnit" : {
"state" : "translated",
"value" : "Error al descargar la imagen en la caché."
}
},
"pl" : {
"stringUnit" : {
"state" : "translated",
"value" : "Błąd podczas pobieranie obrazków do pamięci podręcznej."
}
}
}
},
"global.error.unexpected" : {
"extractionState" : "stale",
"localizations" : {
"en" : {
"stringUnit" : {
"state" : "translated",
"value" : "Unexpected error."
}
},
"es" : {
"stringUnit" : {
"state" : "translated",
"value" : "Error inesperado."
}
},
"eu" : {
"stringUnit" : {
"state" : "translated",
"value" : "Espero ez zen errorea."
}
},
"fr" : {
"stringUnit" : {
"state" : "translated",
"value" : "Erreur inattendue."
}
},
"pl" : {
"stringUnit" : {
"state" : "translated",
"value" : "Wystąpił nieoczekiwany błąd."
}
}
}
}
},
"version" : "1.0"
}

View File

@ -12,14 +12,14 @@ public class ToastrService {
public static let shared = ToastrService()
private init() { }
public func showSuccess(_ title: String, imageSystemName: String, subtitle: String? = nil) {
public func showSuccess(_ title: LocalizedStringResource, imageSystemName: String, subtitle: String? = nil) {
let image = self.createImage(systemName: imageSystemName, color: UIColor(Color.accentColor))
self.showSuccess(title, image: image, subtitle: subtitle)
self.showSuccess(title.key, image: image, subtitle: subtitle)
}
public func showSuccess(_ title: String, imageName: String, subtitle: String? = nil) {
public func showSuccess(_ title: LocalizedStringResource, imageName: String, subtitle: String? = nil) {
let image = self.createImage(name: imageName, color: UIColor(Color.accentColor))
self.showSuccess(title, image: image, subtitle: subtitle)
self.showSuccess(title.key, image: image, subtitle: subtitle)
}
private func showSuccess(_ title: String, image: UIImage?, subtitle: String? = nil) {
@ -39,17 +39,22 @@ public class ToastrService {
Drops.show(drop)
}
public func showError(title: String = "global.error.unexpected", imageSystemName: String = "ant.circle.fill", subtitle: String? = nil) {
public func showError(title: LocalizedStringResource, imageSystemName: String = "ant.circle.fill", subtitle: String? = nil) {
let image = self.createImage(systemName: imageSystemName, color: UIColor(Color.accentColor))
self.showError(title: title, image: image, subtitle: subtitle)
self.showError(title: title.key, image: image, subtitle: subtitle)
}
public func showError(localizedMessage: String, imageSystemName: String = "ant.circle.fill", subtitle: String? = nil) {
let image = self.createImage(systemName: imageSystemName, color: UIColor(Color.accentColor))
self.showError(title: localizedMessage, image: image, subtitle: subtitle)
}
public func showError(title: String = "global.error.unexpected", imageName: String, subtitle: String? = nil) {
public func showError(title: LocalizedStringResource, imageName: String, subtitle: String? = nil) {
let image = self.createImage(name: imageName, color: UIColor(Color.accentColor))
self.showError(title: title, image: image, subtitle: subtitle)
self.showError(title: title.key, image: image, subtitle: subtitle)
}
private func showError(title: String = "global.error.unexpected", image: UIImage?, subtitle: String? = nil) {
private func showError(title: String, image: UIImage?, subtitle: String? = nil) {
let drop = Drop(
title: NSLocalizedString(title, comment: "Error displayed to the user."),
subtitle: subtitle,

View File

@ -7,12 +7,6 @@
objects = {
/* Begin PBXBuildFile section */
F80048032961850500E6868A /* AttachmentData+CoreDataClass.swift in Sources */ = {isa = PBXBuildFile; fileRef = F80047FF2961850500E6868A /* AttachmentData+CoreDataClass.swift */; };
F80048042961850500E6868A /* AttachmentData+CoreDataProperties.swift in Sources */ = {isa = PBXBuildFile; fileRef = F80048002961850500E6868A /* AttachmentData+CoreDataProperties.swift */; };
F80048052961850500E6868A /* StatusData+CoreDataClass.swift in Sources */ = {isa = PBXBuildFile; fileRef = F80048012961850500E6868A /* StatusData+CoreDataClass.swift */; };
F80048062961850500E6868A /* StatusData+CoreDataProperties.swift in Sources */ = {isa = PBXBuildFile; fileRef = F80048022961850500E6868A /* StatusData+CoreDataProperties.swift */; };
F80048082961E6DE00E6868A /* StatusDataHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = F80048072961E6DE00E6868A /* StatusDataHandler.swift */; };
F800480A2961EA1900E6868A /* AttachmentDataHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = F80048092961EA1900E6868A /* AttachmentDataHandler.swift */; };
F802884F297AEED5000BDD51 /* DatabaseError.swift in Sources */ = {isa = PBXBuildFile; fileRef = F802884E297AEED5000BDD51 /* DatabaseError.swift */; };
F805DCF129DBEF83006A1FD9 /* ReportView.swift in Sources */ = {isa = PBXBuildFile; fileRef = F805DCF029DBEF83006A1FD9 /* ReportView.swift */; };
F808641429756666009F035C /* NotificationRowView.swift in Sources */ = {isa = PBXBuildFile; fileRef = F808641329756666009F035C /* NotificationRowView.swift */; };
@ -22,13 +16,9 @@
F8210DD52966BB7E001D9973 /* Nuke in Frameworks */ = {isa = PBXBuildFile; productRef = F8210DD42966BB7E001D9973 /* Nuke */; };
F8210DD72966BB7E001D9973 /* NukeExtensions in Frameworks */ = {isa = PBXBuildFile; productRef = F8210DD62966BB7E001D9973 /* NukeExtensions */; };
F8210DD92966BB7E001D9973 /* NukeUI in Frameworks */ = {isa = PBXBuildFile; productRef = F8210DD82966BB7E001D9973 /* NukeUI */; };
F8210DDD2966CF17001D9973 /* StatusData+Status.swift in Sources */ = {isa = PBXBuildFile; fileRef = F8210DDC2966CF17001D9973 /* StatusData+Status.swift */; };
F8210DDF2966CFC7001D9973 /* AttachmentData+Attachment.swift in Sources */ = {isa = PBXBuildFile; fileRef = F8210DDE2966CFC7001D9973 /* AttachmentData+Attachment.swift */; };
F8210DEA2966E4F9001D9973 /* AnimatePlaceholderModifier.swift in Sources */ = {isa = PBXBuildFile; fileRef = F8210DE92966E4F9001D9973 /* AnimatePlaceholderModifier.swift */; };
F825F0C929F7A562008BD204 /* UserProfilePrivateAccountView.swift in Sources */ = {isa = PBXBuildFile; fileRef = F825F0C829F7A562008BD204 /* UserProfilePrivateAccountView.swift */; };
F825F0CB29F7CFC4008BD204 /* FollowRequestsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = F825F0CA29F7CFC4008BD204 /* FollowRequestsView.swift */; };
F835082329BEF9C400DE3247 /* Localizable.strings in Resources */ = {isa = PBXBuildFile; fileRef = F835082629BEF9C400DE3247 /* Localizable.strings */; };
F835082429BEF9C400DE3247 /* Localizable.strings in Resources */ = {isa = PBXBuildFile; fileRef = F835082629BEF9C400DE3247 /* Localizable.strings */; };
F83CBEFB298298A1002972C8 /* ImageCarouselPicture.swift in Sources */ = {isa = PBXBuildFile; fileRef = F83CBEFA298298A1002972C8 /* ImageCarouselPicture.swift */; };
F84625E929FE2788002D3AF4 /* QRCodeWidget.swift in Sources */ = {isa = PBXBuildFile; fileRef = F84625E829FE2788002D3AF4 /* QRCodeWidget.swift */; };
F84625EB29FE28D4002D3AF4 /* QRCodeWidgetEntryView.swift in Sources */ = {isa = PBXBuildFile; fileRef = F84625EA29FE28D4002D3AF4 /* QRCodeWidgetEntryView.swift */; };
@ -40,13 +30,9 @@
F858906B29E1CC7A00D4BDED /* UIApplication+Window.swift in Sources */ = {isa = PBXBuildFile; fileRef = F858906A29E1CC7A00D4BDED /* UIApplication+Window.swift */; };
F85D0C652ABA08F9002B3577 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = F85D0C642ABA08F9002B3577 /* Assets.xcassets */; };
F85D4971296402DC00751DF7 /* AuthorizationService.swift in Sources */ = {isa = PBXBuildFile; fileRef = F85D4970296402DC00751DF7 /* AuthorizationService.swift */; };
F85D4975296407F100751DF7 /* HomeTimelineService.swift in Sources */ = {isa = PBXBuildFile; fileRef = F85D4974296407F100751DF7 /* HomeTimelineService.swift */; };
F85D497729640A5200751DF7 /* ImageRow.swift in Sources */ = {isa = PBXBuildFile; fileRef = F85D497629640A5200751DF7 /* ImageRow.swift */; };
F85D497929640B9D00751DF7 /* ImagesCarousel.swift in Sources */ = {isa = PBXBuildFile; fileRef = F85D497829640B9D00751DF7 /* ImagesCarousel.swift */; };
F85D497D29640D5900751DF7 /* InteractionRow.swift in Sources */ = {isa = PBXBuildFile; fileRef = F85D497C29640D5900751DF7 /* InteractionRow.swift */; };
F85D497F296416C800751DF7 /* CommentsSectionView.swift in Sources */ = {isa = PBXBuildFile; fileRef = F85D497E296416C800751DF7 /* CommentsSectionView.swift */; };
F85D498329642FAC00751DF7 /* AttachmentData+Comperable.swift in Sources */ = {isa = PBXBuildFile; fileRef = F85D498229642FAC00751DF7 /* AttachmentData+Comperable.swift */; };
F85D49852964301800751DF7 /* StatusData+Attachments.swift in Sources */ = {isa = PBXBuildFile; fileRef = F85D49842964301800751DF7 /* StatusData+Attachments.swift */; };
F85D4DFE29B78C8400345267 /* HashtagModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = F85D4DFD29B78C8400345267 /* HashtagModel.swift */; };
F85DBF8F296732E20069BF89 /* AccountsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = F85DBF8E296732E20069BF89 /* AccountsView.swift */; };
F86167C6297FE6CC004D1F67 /* AvatarShapesSectionView.swift in Sources */ = {isa = PBXBuildFile; fileRef = F86167C5297FE6CC004D1F67 /* AvatarShapesSectionView.swift */; };
@ -61,29 +47,14 @@
F864F77829BB930000B13921 /* PhotoWidgetEntry.swift in Sources */ = {isa = PBXBuildFile; fileRef = F864F77729BB930000B13921 /* PhotoWidgetEntry.swift */; };
F864F77A29BB94A800B13921 /* PixelfedKit in Frameworks */ = {isa = PBXBuildFile; productRef = F864F77929BB94A800B13921 /* PixelfedKit */; };
F864F77C29BB982100B13921 /* StatusFetcher.swift in Sources */ = {isa = PBXBuildFile; fileRef = F864F77B29BB982100B13921 /* StatusFetcher.swift */; };
F864F77D29BB9A4600B13921 /* AttachmentData+CoreDataClass.swift in Sources */ = {isa = PBXBuildFile; fileRef = F80047FF2961850500E6868A /* AttachmentData+CoreDataClass.swift */; };
F864F77E29BB9A4900B13921 /* AttachmentData+CoreDataProperties.swift in Sources */ = {isa = PBXBuildFile; fileRef = F80048002961850500E6868A /* AttachmentData+CoreDataProperties.swift */; };
F864F78229BB9A6500B13921 /* StatusData+CoreDataClass.swift in Sources */ = {isa = PBXBuildFile; fileRef = F80048012961850500E6868A /* StatusData+CoreDataClass.swift */; };
F864F78329BB9A6800B13921 /* StatusData+CoreDataProperties.swift in Sources */ = {isa = PBXBuildFile; fileRef = F80048022961850500E6868A /* StatusData+CoreDataProperties.swift */; };
F864F78429BB9A6E00B13921 /* ApplicationSettings+CoreDataClass.swift in Sources */ = {isa = PBXBuildFile; fileRef = F866F69E296040A8002E8F88 /* ApplicationSettings+CoreDataClass.swift */; };
F864F78529BB9A7100B13921 /* ApplicationSettings+CoreDataProperties.swift in Sources */ = {isa = PBXBuildFile; fileRef = F866F69F296040A8002E8F88 /* ApplicationSettings+CoreDataProperties.swift */; };
F864F78629BB9A7400B13921 /* AccountData+CoreDataClass.swift in Sources */ = {isa = PBXBuildFile; fileRef = F88FAD28295F43B8009B20C9 /* AccountData+CoreDataClass.swift */; };
F864F78729BB9A7700B13921 /* AccountData+CoreDataProperties.swift in Sources */ = {isa = PBXBuildFile; fileRef = F88FAD29295F43B8009B20C9 /* AccountData+CoreDataProperties.swift */; };
F864F78829BB9A7B00B13921 /* CoreDataHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = F88C2474295C37BB0006098B /* CoreDataHandler.swift */; };
F864F78529BB9A7100B13921 /* ApplicationSettings.swift in Sources */ = {isa = PBXBuildFile; fileRef = F866F69F296040A8002E8F88 /* ApplicationSettings.swift */; };
F864F78729BB9A7700B13921 /* AccountData.swift in Sources */ = {isa = PBXBuildFile; fileRef = F88FAD29295F43B8009B20C9 /* AccountData.swift */; };
F864F78829BB9A7B00B13921 /* SwiftDataHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = F88C2474295C37BB0006098B /* SwiftDataHandler.swift */; };
F864F78929BB9A7D00B13921 /* AccountDataHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = F866F6A229604161002E8F88 /* AccountDataHandler.swift */; };
F864F78A29BB9A8000B13921 /* ApplicationSettingsHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = F866F6A429604194002E8F88 /* ApplicationSettingsHandler.swift */; };
F864F78B29BB9A8300B13921 /* StatusDataHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = F80048072961E6DE00E6868A /* StatusDataHandler.swift */; };
F864F78C29BB9A8500B13921 /* AttachmentDataHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = F80048092961EA1900E6868A /* AttachmentDataHandler.swift */; };
F864F7A529BBA01D00B13921 /* CoreDataError.swift in Sources */ = {isa = PBXBuildFile; fileRef = F864F7A429BBA01D00B13921 /* CoreDataError.swift */; };
F864F7A629BBA01D00B13921 /* CoreDataError.swift in Sources */ = {isa = PBXBuildFile; fileRef = F864F7A429BBA01D00B13921 /* CoreDataError.swift */; };
F865B4CD2A024AD8008ACDFC /* StatusData+Faulty.swift in Sources */ = {isa = PBXBuildFile; fileRef = F865B4CC2A024AD8008ACDFC /* StatusData+Faulty.swift */; };
F865B4CE2A024AD8008ACDFC /* StatusData+Faulty.swift in Sources */ = {isa = PBXBuildFile; fileRef = F865B4CC2A024AD8008ACDFC /* StatusData+Faulty.swift */; };
F865B4CF2A024AD8008ACDFC /* StatusData+Faulty.swift in Sources */ = {isa = PBXBuildFile; fileRef = F865B4CC2A024AD8008ACDFC /* StatusData+Faulty.swift */; };
F865B4D12A024AFE008ACDFC /* AttachmentData+Faulty.swift in Sources */ = {isa = PBXBuildFile; fileRef = F865B4D02A024AFE008ACDFC /* AttachmentData+Faulty.swift */; };
F865B4D22A024AFE008ACDFC /* AttachmentData+Faulty.swift in Sources */ = {isa = PBXBuildFile; fileRef = F865B4D02A024AFE008ACDFC /* AttachmentData+Faulty.swift */; };
F865B4D32A024AFE008ACDFC /* AttachmentData+Faulty.swift in Sources */ = {isa = PBXBuildFile; fileRef = F865B4D02A024AFE008ACDFC /* AttachmentData+Faulty.swift */; };
F866F6A0296040A8002E8F88 /* ApplicationSettings+CoreDataClass.swift in Sources */ = {isa = PBXBuildFile; fileRef = F866F69E296040A8002E8F88 /* ApplicationSettings+CoreDataClass.swift */; };
F866F6A1296040A8002E8F88 /* ApplicationSettings+CoreDataProperties.swift in Sources */ = {isa = PBXBuildFile; fileRef = F866F69F296040A8002E8F88 /* ApplicationSettings+CoreDataProperties.swift */; };
F866F6A1296040A8002E8F88 /* ApplicationSettings.swift in Sources */ = {isa = PBXBuildFile; fileRef = F866F69F296040A8002E8F88 /* ApplicationSettings.swift */; };
F866F6A329604161002E8F88 /* AccountDataHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = F866F6A229604161002E8F88 /* AccountDataHandler.swift */; };
F866F6A529604194002E8F88 /* ApplicationSettingsHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = F866F6A429604194002E8F88 /* ApplicationSettingsHandler.swift */; };
F866F6A729604629002E8F88 /* SignInView.swift in Sources */ = {isa = PBXBuildFile; fileRef = F866F6A629604629002E8F88 /* SignInView.swift */; };
@ -106,7 +77,6 @@
F8705A7B29FF872F00DA818A /* QRCodeGenerator.swift in Sources */ = {isa = PBXBuildFile; fileRef = F8705A7A29FF872F00DA818A /* QRCodeGenerator.swift */; };
F8705A7E29FF880600DA818A /* FileFetcher.swift in Sources */ = {isa = PBXBuildFile; fileRef = F8705A7D29FF880600DA818A /* FileFetcher.swift */; };
F870EE5229F1645C00A2D43B /* MainNavigationOptions.swift in Sources */ = {isa = PBXBuildFile; fileRef = F870EE5129F1645C00A2D43B /* MainNavigationOptions.swift */; };
F871F21D29EF0D7000A351EF /* NavigationMenuItemDetails.swift in Sources */ = {isa = PBXBuildFile; fileRef = F871F21C29EF0D7000A351EF /* NavigationMenuItemDetails.swift */; };
F8742FC429990AFB00E9642B /* ClientError.swift in Sources */ = {isa = PBXBuildFile; fileRef = F8742FC329990AFB00E9642B /* ClientError.swift */; };
F8764187298ABB520057D362 /* ViewState.swift in Sources */ = {isa = PBXBuildFile; fileRef = F8764186298ABB520057D362 /* ViewState.swift */; };
F876418D298AE5020057D362 /* PaginableStatusesView.swift in Sources */ = {isa = PBXBuildFile; fileRef = F876418C298AE5020057D362 /* PaginableStatusesView.swift */; };
@ -114,6 +84,7 @@
F87AEB922986C44E00434FB6 /* AuthorizationSession.swift in Sources */ = {isa = PBXBuildFile; fileRef = F87AEB912986C44E00434FB6 /* AuthorizationSession.swift */; };
F87AEB972986D16D00434FB6 /* AuthorisationError.swift in Sources */ = {isa = PBXBuildFile; fileRef = F87AEB962986D16D00434FB6 /* AuthorisationError.swift */; };
F883402029B62AE900C3E096 /* SearchView.swift in Sources */ = {isa = PBXBuildFile; fileRef = F883401F29B62AE900C3E096 /* SearchView.swift */; };
F886BBAC2AE7CF510083152B /* NotificationView.swift in Sources */ = {isa = PBXBuildFile; fileRef = F886BBAB2AE7CF510083152B /* NotificationView.swift */; };
F88AB05329B3613900345EDE /* PhotoUrl.swift in Sources */ = {isa = PBXBuildFile; fileRef = F88AB05229B3613900345EDE /* PhotoUrl.swift */; };
F88AB05529B3626300345EDE /* ImageGrid.swift in Sources */ = {isa = PBXBuildFile; fileRef = F88AB05429B3626300345EDE /* ImageGrid.swift */; };
F88AB05829B36B8200345EDE /* AccountsPhotoView.swift in Sources */ = {isa = PBXBuildFile; fileRef = F88AB05729B36B8200345EDE /* AccountsPhotoView.swift */; };
@ -123,53 +94,40 @@
F88BC51329E02FD800CE6141 /* ComposeView.swift in Sources */ = {isa = PBXBuildFile; fileRef = F88BC51229E02FD800CE6141 /* ComposeView.swift */; };
F88BC51629E0307F00CE6141 /* NotificationsName.swift in Sources */ = {isa = PBXBuildFile; fileRef = F88BC51529E0307F00CE6141 /* NotificationsName.swift */; };
F88BC51B29E0350300CE6141 /* ClientKit in Frameworks */ = {isa = PBXBuildFile; productRef = F88BC51A29E0350300CE6141 /* ClientKit */; };
F88BC51D29E0377B00CE6141 /* AccountData+AccountModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = F88BC51C29E0377B00CE6141 /* AccountData+AccountModel.swift */; };
F88BC51F29E03ED300CE6141 /* ClientKit in Frameworks */ = {isa = PBXBuildFile; productRef = F88BC51E29E03ED300CE6141 /* ClientKit */; };
F88BC52729E0431D00CE6141 /* ServicesKit in Frameworks */ = {isa = PBXBuildFile; productRef = F88BC52629E0431D00CE6141 /* ServicesKit */; };
F88BC52A29E046D700CE6141 /* WidgetsKit in Frameworks */ = {isa = PBXBuildFile; productRef = F88BC52929E046D700CE6141 /* WidgetsKit */; };
F88BC52D29E04BB600CE6141 /* EnvironmentKit in Frameworks */ = {isa = PBXBuildFile; productRef = F88BC52C29E04BB600CE6141 /* EnvironmentKit */; };
F88BC52F29E04C5F00CE6141 /* EnvironmentKit in Frameworks */ = {isa = PBXBuildFile; productRef = F88BC52E29E04C5F00CE6141 /* EnvironmentKit */; };
F88BC53029E0672000CE6141 /* Localizable.strings in Resources */ = {isa = PBXBuildFile; fileRef = F835082629BEF9C400DE3247 /* Localizable.strings */; };
F88BC53229E0677000CE6141 /* EnvironmentKit in Frameworks */ = {isa = PBXBuildFile; productRef = F88BC53129E0677000CE6141 /* EnvironmentKit */; };
F88BC53B29E06A5100CE6141 /* ImageContextMenu.swift in Sources */ = {isa = PBXBuildFile; fileRef = F88BC53A29E06A5100CE6141 /* ImageContextMenu.swift */; };
F88BC53D29E06EAD00CE6141 /* ServicesKit in Frameworks */ = {isa = PBXBuildFile; productRef = F88BC53C29E06EAD00CE6141 /* ServicesKit */; };
F88BC53F29E06EB100CE6141 /* WidgetsKit in Frameworks */ = {isa = PBXBuildFile; productRef = F88BC53E29E06EB100CE6141 /* WidgetsKit */; };
F88BC54129E072A600CE6141 /* CoreDataError.swift in Sources */ = {isa = PBXBuildFile; fileRef = F864F7A429BBA01D00B13921 /* CoreDataError.swift */; };
F88BC54229E072A900CE6141 /* AttachmentDataHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = F80048092961EA1900E6868A /* AttachmentDataHandler.swift */; };
F88BC54329E072AC00CE6141 /* StatusDataHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = F80048072961E6DE00E6868A /* StatusDataHandler.swift */; };
F88BC54429E072AF00CE6141 /* ApplicationSettingsHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = F866F6A429604194002E8F88 /* ApplicationSettingsHandler.swift */; };
F88BC54529E072B200CE6141 /* AccountDataHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = F866F6A229604161002E8F88 /* AccountDataHandler.swift */; };
F88BC54629E072B500CE6141 /* CoreDataHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = F88C2474295C37BB0006098B /* CoreDataHandler.swift */; };
F88BC54729E072B800CE6141 /* AccountData+CoreDataProperties.swift in Sources */ = {isa = PBXBuildFile; fileRef = F88FAD29295F43B8009B20C9 /* AccountData+CoreDataProperties.swift */; };
F88BC54829E072BC00CE6141 /* AccountData+CoreDataClass.swift in Sources */ = {isa = PBXBuildFile; fileRef = F88FAD28295F43B8009B20C9 /* AccountData+CoreDataClass.swift */; };
F88BC54929E072C000CE6141 /* ApplicationSettings+CoreDataProperties.swift in Sources */ = {isa = PBXBuildFile; fileRef = F866F69F296040A8002E8F88 /* ApplicationSettings+CoreDataProperties.swift */; };
F88BC54A29E072C400CE6141 /* ApplicationSettings+CoreDataClass.swift in Sources */ = {isa = PBXBuildFile; fileRef = F866F69E296040A8002E8F88 /* ApplicationSettings+CoreDataClass.swift */; };
F88BC54B29E072CA00CE6141 /* StatusData+CoreDataProperties.swift in Sources */ = {isa = PBXBuildFile; fileRef = F80048022961850500E6868A /* StatusData+CoreDataProperties.swift */; };
F88BC54C29E072CD00CE6141 /* StatusData+CoreDataClass.swift in Sources */ = {isa = PBXBuildFile; fileRef = F80048012961850500E6868A /* StatusData+CoreDataClass.swift */; };
F88BC54D29E072D600CE6141 /* AttachmentData+CoreDataProperties.swift in Sources */ = {isa = PBXBuildFile; fileRef = F80048002961850500E6868A /* AttachmentData+CoreDataProperties.swift */; };
F88BC54E29E072D900CE6141 /* AttachmentData+CoreDataClass.swift in Sources */ = {isa = PBXBuildFile; fileRef = F80047FF2961850500E6868A /* AttachmentData+CoreDataClass.swift */; };
F88BC54F29E073BC00CE6141 /* AccountData+AccountModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = F88BC51C29E0377B00CE6141 /* AccountData+AccountModel.swift */; };
F88BC55029E074EB00CE6141 /* Vernissage.xcdatamodeld in Sources */ = {isa = PBXBuildFile; fileRef = F88C2476295C37BB0006098B /* Vernissage.xcdatamodeld */; };
F88BC54629E072B500CE6141 /* SwiftDataHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = F88C2474295C37BB0006098B /* SwiftDataHandler.swift */; };
F88BC54729E072B800CE6141 /* AccountData.swift in Sources */ = {isa = PBXBuildFile; fileRef = F88FAD29295F43B8009B20C9 /* AccountData.swift */; };
F88BC54929E072C000CE6141 /* ApplicationSettings.swift in Sources */ = {isa = PBXBuildFile; fileRef = F866F69F296040A8002E8F88 /* ApplicationSettings.swift */; };
F88BC55229E0798900CE6141 /* SharedAssets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = F88BC55129E0798900CE6141 /* SharedAssets.xcassets */; };
F88BC55429E0798900CE6141 /* SharedAssets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = F88BC55129E0798900CE6141 /* SharedAssets.xcassets */; };
F88C246C295C37B80006098B /* VernissageApp.swift in Sources */ = {isa = PBXBuildFile; fileRef = F88C246B295C37B80006098B /* VernissageApp.swift */; };
F88C246E295C37B80006098B /* MainView.swift in Sources */ = {isa = PBXBuildFile; fileRef = F88C246D295C37B80006098B /* MainView.swift */; };
F88C2470295C37BB0006098B /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = F88C246F295C37BB0006098B /* Assets.xcassets */; };
F88C2473295C37BB0006098B /* Preview Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = F88C2472295C37BB0006098B /* Preview Assets.xcassets */; };
F88C2475295C37BB0006098B /* CoreDataHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = F88C2474295C37BB0006098B /* CoreDataHandler.swift */; };
F88C2478295C37BB0006098B /* Vernissage.xcdatamodeld in Sources */ = {isa = PBXBuildFile; fileRef = F88C2476295C37BB0006098B /* Vernissage.xcdatamodeld */; };
F88C2475295C37BB0006098B /* SwiftDataHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = F88C2474295C37BB0006098B /* SwiftDataHandler.swift */; };
F88C2482295C3A4F0006098B /* StatusView.swift in Sources */ = {isa = PBXBuildFile; fileRef = F88C2481295C3A4F0006098B /* StatusView.swift */; };
F88E4D42297E69FD0057491A /* StatusesView.swift in Sources */ = {isa = PBXBuildFile; fileRef = F88E4D41297E69FD0057491A /* StatusesView.swift */; };
F88E4D48297E90CD0057491A /* TrendStatusesView.swift in Sources */ = {isa = PBXBuildFile; fileRef = F88E4D47297E90CD0057491A /* TrendStatusesView.swift */; };
F88E4D4A297EA0490057491A /* RouterPath.swift in Sources */ = {isa = PBXBuildFile; fileRef = F88E4D49297EA0490057491A /* RouterPath.swift */; };
F88E4D4D297EA4290057491A /* EmojiText in Frameworks */ = {isa = PBXBuildFile; productRef = F88E4D4C297EA4290057491A /* EmojiText */; };
F88E4D56297EAD6E0057491A /* AppRouteur.swift in Sources */ = {isa = PBXBuildFile; fileRef = F88E4D55297EAD6E0057491A /* AppRouteur.swift */; };
F88FAD21295F3944009B20C9 /* HomeFeedView.swift in Sources */ = {isa = PBXBuildFile; fileRef = F88FAD20295F3944009B20C9 /* HomeFeedView.swift */; };
F88FAD27295F400E009B20C9 /* NotificationsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = F88FAD26295F400E009B20C9 /* NotificationsView.swift */; };
F88FAD2A295F43B8009B20C9 /* AccountData+CoreDataClass.swift in Sources */ = {isa = PBXBuildFile; fileRef = F88FAD28295F43B8009B20C9 /* AccountData+CoreDataClass.swift */; };
F88FAD2B295F43B8009B20C9 /* AccountData+CoreDataProperties.swift in Sources */ = {isa = PBXBuildFile; fileRef = F88FAD29295F43B8009B20C9 /* AccountData+CoreDataProperties.swift */; };
F891E7CE29C35BF50022C449 /* ImageRowItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = F891E7CD29C35BF50022C449 /* ImageRowItem.swift */; };
F88FAD2B295F43B8009B20C9 /* AccountData.swift in Sources */ = {isa = PBXBuildFile; fileRef = F88FAD29295F43B8009B20C9 /* AccountData.swift */; };
F891E7D029C368750022C449 /* ImageRowItemAsync.swift in Sources */ = {isa = PBXBuildFile; fileRef = F891E7CF29C368750022C449 /* ImageRowItemAsync.swift */; };
F89229EF2ADA63620040C964 /* Localizable.xcstrings in Resources */ = {isa = PBXBuildFile; fileRef = F89229EE2ADA63620040C964 /* Localizable.xcstrings */; };
F89229F02ADA63620040C964 /* Localizable.xcstrings in Resources */ = {isa = PBXBuildFile; fileRef = F89229EE2ADA63620040C964 /* Localizable.xcstrings */; };
F89229F12ADA63620040C964 /* Localizable.xcstrings in Resources */ = {isa = PBXBuildFile; fileRef = F89229EE2ADA63620040C964 /* Localizable.xcstrings */; };
F897978F29684BCB00B22335 /* LoadingView.swift in Sources */ = {isa = PBXBuildFile; fileRef = F897978E29684BCB00B22335 /* LoadingView.swift */; };
F89992C9296D6DC7005994BF /* CommentBodyView.swift in Sources */ = {isa = PBXBuildFile; fileRef = F89992C8296D6DC7005994BF /* CommentBodyView.swift */; };
F89A46DC296EAACE0062125F /* SettingsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = F89A46DB296EAACE0062125F /* SettingsView.swift */; };
@ -177,7 +135,6 @@
F89AC00529A1F9B500F4159F /* AppMetadata.swift in Sources */ = {isa = PBXBuildFile; fileRef = F89AC00429A1F9B500F4159F /* AppMetadata.swift */; };
F89B5CC029D019B600549F2F /* HTMLString in Frameworks */ = {isa = PBXBuildFile; productRef = F89B5CBF29D019B600549F2F /* HTMLString */; };
F89B5CC229D01BF700549F2F /* InstanceView.swift in Sources */ = {isa = PBXBuildFile; fileRef = F89B5CC129D01BF700549F2F /* InstanceView.swift */; };
F89CEB802984198600A1376F /* AttachmentData+HighestImage.swift in Sources */ = {isa = PBXBuildFile; fileRef = F89CEB7F2984198600A1376F /* AttachmentData+HighestImage.swift */; };
F89D6C4229717FDC001DA3D4 /* AccountsSectionView.swift in Sources */ = {isa = PBXBuildFile; fileRef = F89D6C4129717FDC001DA3D4 /* AccountsSectionView.swift */; };
F89D6C4429718092001DA3D4 /* AccentsSectionView.swift in Sources */ = {isa = PBXBuildFile; fileRef = F89D6C4329718092001DA3D4 /* AccentsSectionView.swift */; };
F89D6C4629718193001DA3D4 /* GeneralSectionView.swift in Sources */ = {isa = PBXBuildFile; fileRef = F89D6C4529718193001DA3D4 /* GeneralSectionView.swift */; };
@ -193,31 +150,27 @@
F8B0886029943498002AB40A /* OtherSectionView.swift in Sources */ = {isa = PBXBuildFile; fileRef = F8B0885F29943498002AB40A /* OtherSectionView.swift */; };
F8B08862299435C9002AB40A /* SupportView.swift in Sources */ = {isa = PBXBuildFile; fileRef = F8B08861299435C9002AB40A /* SupportView.swift */; };
F8B758DE2AB9DD85000C8068 /* ColumnData.swift in Sources */ = {isa = PBXBuildFile; fileRef = F8B758DD2AB9DD85000C8068 /* ColumnData.swift */; };
F8D0E5222AE2A2630061C561 /* HomeTimelineView.swift in Sources */ = {isa = PBXBuildFile; fileRef = F8D0E5212AE2A2630061C561 /* HomeTimelineView.swift */; };
F8D0E5242AE2A88A0061C561 /* HomeTimelineService.swift in Sources */ = {isa = PBXBuildFile; fileRef = F8D0E5232AE2A88A0061C561 /* HomeTimelineService.swift */; };
F8D5444329D4066C002225D6 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = F8D5444229D4066C002225D6 /* AppDelegate.swift */; };
F8D8E0C72ACC234A00AA1374 /* ViewedStatus+CoreDataClass.swift in Sources */ = {isa = PBXBuildFile; fileRef = F8D8E0C62ACC234A00AA1374 /* ViewedStatus+CoreDataClass.swift */; };
F8D8E0C82ACC234A00AA1374 /* ViewedStatus+CoreDataClass.swift in Sources */ = {isa = PBXBuildFile; fileRef = F8D8E0C62ACC234A00AA1374 /* ViewedStatus+CoreDataClass.swift */; };
F8D8E0C92ACC234A00AA1374 /* ViewedStatus+CoreDataClass.swift in Sources */ = {isa = PBXBuildFile; fileRef = F8D8E0C62ACC234A00AA1374 /* ViewedStatus+CoreDataClass.swift */; };
F8D8E0CB2ACC237000AA1374 /* ViewedStatus+CoreDataProperties.swift in Sources */ = {isa = PBXBuildFile; fileRef = F8D8E0CA2ACC237000AA1374 /* ViewedStatus+CoreDataProperties.swift */; };
F8D8E0CC2ACC237000AA1374 /* ViewedStatus+CoreDataProperties.swift in Sources */ = {isa = PBXBuildFile; fileRef = F8D8E0CA2ACC237000AA1374 /* ViewedStatus+CoreDataProperties.swift */; };
F8D8E0CD2ACC237000AA1374 /* ViewedStatus+CoreDataProperties.swift in Sources */ = {isa = PBXBuildFile; fileRef = F8D8E0CA2ACC237000AA1374 /* ViewedStatus+CoreDataProperties.swift */; };
F8D8E0CB2ACC237000AA1374 /* ViewedStatus.swift in Sources */ = {isa = PBXBuildFile; fileRef = F8D8E0CA2ACC237000AA1374 /* ViewedStatus.swift */; };
F8D8E0CC2ACC237000AA1374 /* ViewedStatus.swift in Sources */ = {isa = PBXBuildFile; fileRef = F8D8E0CA2ACC237000AA1374 /* ViewedStatus.swift */; };
F8D8E0CD2ACC237000AA1374 /* ViewedStatus.swift in Sources */ = {isa = PBXBuildFile; fileRef = F8D8E0CA2ACC237000AA1374 /* ViewedStatus.swift */; };
F8D8E0CF2ACC23B300AA1374 /* ViewedStatusHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = F8D8E0CE2ACC23B300AA1374 /* ViewedStatusHandler.swift */; };
F8D8E0D02ACC23B300AA1374 /* ViewedStatusHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = F8D8E0CE2ACC23B300AA1374 /* ViewedStatusHandler.swift */; };
F8D8E0D12ACC23B300AA1374 /* ViewedStatusHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = F8D8E0CE2ACC23B300AA1374 /* ViewedStatusHandler.swift */; };
F8DE749F2AE4F7B500ACD188 /* NotificationsService.swift in Sources */ = {isa = PBXBuildFile; fileRef = F8DE749E2AE4F7B500ACD188 /* NotificationsService.swift */; };
F8DF38E429DD68820047F1AA /* ViewOffsetKey.swift in Sources */ = {isa = PBXBuildFile; fileRef = F8DF38E329DD68820047F1AA /* ViewOffsetKey.swift */; };
F8DF38E629DDB98A0047F1AA /* SocialsSectionView.swift in Sources */ = {isa = PBXBuildFile; fileRef = F8DF38E529DDB98A0047F1AA /* SocialsSectionView.swift */; };
F8E36E462AB8745300769C55 /* Sizable.swift in Sources */ = {isa = PBXBuildFile; fileRef = F8E36E452AB8745300769C55 /* Sizable.swift */; };
F8E36E482AB874A500769C55 /* StatusModel+Sizeable.swift in Sources */ = {isa = PBXBuildFile; fileRef = F8E36E472AB874A500769C55 /* StatusModel+Sizeable.swift */; };
F8E6D03329CDD52500416CCA /* EditProfileView.swift in Sources */ = {isa = PBXBuildFile; fileRef = F8E6D03229CDD52500416CCA /* EditProfileView.swift */; };
F8E7ADFA2AD44CC00038FFFD /* AccountRelationship+CoreDataClass.swift in Sources */ = {isa = PBXBuildFile; fileRef = F8E7ADF92AD44CC00038FFFD /* AccountRelationship+CoreDataClass.swift */; };
F8E7ADFB2AD44CC00038FFFD /* AccountRelationship+CoreDataClass.swift in Sources */ = {isa = PBXBuildFile; fileRef = F8E7ADF92AD44CC00038FFFD /* AccountRelationship+CoreDataClass.swift */; };
F8E7ADFC2AD44CC00038FFFD /* AccountRelationship+CoreDataClass.swift in Sources */ = {isa = PBXBuildFile; fileRef = F8E7ADF92AD44CC00038FFFD /* AccountRelationship+CoreDataClass.swift */; };
F8E7ADFE2AD44CEB0038FFFD /* AccountRelationship+CoreDataProperties.swift in Sources */ = {isa = PBXBuildFile; fileRef = F8E7ADFD2AD44CEB0038FFFD /* AccountRelationship+CoreDataProperties.swift */; };
F8E7ADFF2AD44CEB0038FFFD /* AccountRelationship+CoreDataProperties.swift in Sources */ = {isa = PBXBuildFile; fileRef = F8E7ADFD2AD44CEB0038FFFD /* AccountRelationship+CoreDataProperties.swift */; };
F8E7AE002AD44CEB0038FFFD /* AccountRelationship+CoreDataProperties.swift in Sources */ = {isa = PBXBuildFile; fileRef = F8E7ADFD2AD44CEB0038FFFD /* AccountRelationship+CoreDataProperties.swift */; };
F8E7ADFE2AD44CEB0038FFFD /* AccountRelationship.swift in Sources */ = {isa = PBXBuildFile; fileRef = F8E7ADFD2AD44CEB0038FFFD /* AccountRelationship.swift */; };
F8E7ADFF2AD44CEB0038FFFD /* AccountRelationship.swift in Sources */ = {isa = PBXBuildFile; fileRef = F8E7ADFD2AD44CEB0038FFFD /* AccountRelationship.swift */; };
F8E7AE002AD44CEB0038FFFD /* AccountRelationship.swift in Sources */ = {isa = PBXBuildFile; fileRef = F8E7ADFD2AD44CEB0038FFFD /* AccountRelationship.swift */; };
F8E7AE022AD44D600038FFFD /* AccountRelationshipHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = F8E7AE012AD44D600038FFFD /* AccountRelationshipHandler.swift */; };
F8E7AE032AD44D600038FFFD /* AccountRelationshipHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = F8E7AE012AD44D600038FFFD /* AccountRelationshipHandler.swift */; };
F8E7AE042AD44D600038FFFD /* AccountRelationshipHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = F8E7AE012AD44D600038FFFD /* AccountRelationshipHandler.swift */; };
F8F6E44229BC58F20004795E /* Vernissage.xcdatamodeld in Sources */ = {isa = PBXBuildFile; fileRef = F88C2476295C37BB0006098B /* Vernissage.xcdatamodeld */; };
F8F6E44C29BCC1F70004795E /* PhotoSmallWidgetView.swift in Sources */ = {isa = PBXBuildFile; fileRef = F8F6E44629BCC0DC0004795E /* PhotoSmallWidgetView.swift */; };
F8F6E44D29BCC1F90004795E /* PhotoMediumWidgetView.swift in Sources */ = {isa = PBXBuildFile; fileRef = F8F6E44829BCC0F00004795E /* PhotoMediumWidgetView.swift */; };
F8F6E44E29BCC1FB0004795E /* PhotoLargeWidgetView.swift in Sources */ = {isa = PBXBuildFile; fileRef = F8F6E44A29BCC0FF0004795E /* PhotoLargeWidgetView.swift */; };
@ -259,26 +212,15 @@
/* End PBXCopyFilesBuildPhase section */
/* Begin PBXFileReference section */
F80047FF2961850500E6868A /* AttachmentData+CoreDataClass.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "AttachmentData+CoreDataClass.swift"; sourceTree = "<group>"; };
F80048002961850500E6868A /* AttachmentData+CoreDataProperties.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "AttachmentData+CoreDataProperties.swift"; sourceTree = "<group>"; };
F80048012961850500E6868A /* StatusData+CoreDataClass.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "StatusData+CoreDataClass.swift"; sourceTree = "<group>"; };
F80048022961850500E6868A /* StatusData+CoreDataProperties.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "StatusData+CoreDataProperties.swift"; sourceTree = "<group>"; };
F80048072961E6DE00E6868A /* StatusDataHandler.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StatusDataHandler.swift; sourceTree = "<group>"; };
F80048092961EA1900E6868A /* AttachmentDataHandler.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AttachmentDataHandler.swift; sourceTree = "<group>"; };
F802884E297AEED5000BDD51 /* DatabaseError.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DatabaseError.swift; sourceTree = "<group>"; };
F805DCF029DBEF83006A1FD9 /* ReportView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ReportView.swift; sourceTree = "<group>"; };
F808641329756666009F035C /* NotificationRowView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NotificationRowView.swift; sourceTree = "<group>"; };
F8121CA7298A86D600B466C7 /* InstanceRowView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InstanceRowView.swift; sourceTree = "<group>"; };
F815F60B29E49CF20044566B /* Avatar.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Avatar.swift; sourceTree = "<group>"; };
F8206A032A06547600E19412 /* Vernissage-014.xcdatamodel */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcdatamodel; path = "Vernissage-014.xcdatamodel"; sourceTree = "<group>"; };
F8210DCE2966B600001D9973 /* ImageRowAsync.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ImageRowAsync.swift; sourceTree = "<group>"; };
F8210DDC2966CF17001D9973 /* StatusData+Status.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "StatusData+Status.swift"; sourceTree = "<group>"; };
F8210DDE2966CFC7001D9973 /* AttachmentData+Attachment.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "AttachmentData+Attachment.swift"; sourceTree = "<group>"; };
F8210DE92966E4F9001D9973 /* AnimatePlaceholderModifier.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AnimatePlaceholderModifier.swift; sourceTree = "<group>"; };
F825F0C829F7A562008BD204 /* UserProfilePrivateAccountView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserProfilePrivateAccountView.swift; sourceTree = "<group>"; };
F825F0CA29F7CFC4008BD204 /* FollowRequestsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FollowRequestsView.swift; sourceTree = "<group>"; };
F835082529BEF9C400DE3247 /* en */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = en; path = en.lproj/Localizable.strings; sourceTree = "<group>"; };
F835082729BEFA1E00DE3247 /* pl */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = pl; path = pl.lproj/Localizable.strings; sourceTree = "<group>"; };
F837269429A221420098D3C4 /* PixelfedKit */ = {isa = PBXFileReference; lastKnownFileType = wrapper; path = PixelfedKit; sourceTree = "<group>"; };
F83CBEFA298298A1002972C8 /* ImageCarouselPicture.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ImageCarouselPicture.swift; sourceTree = "<group>"; };
F844F42429D2DC39000DD896 /* LICENSE */ = {isa = PBXFileReference; lastKnownFileType = text; path = LICENSE; sourceTree = "<group>"; };
@ -289,16 +231,11 @@
F84625F329FE2BF9002D3AF4 /* QRCodeProvider.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = QRCodeProvider.swift; sourceTree = "<group>"; };
F84625F729FE2C2F002D3AF4 /* AccountFetcher.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AccountFetcher.swift; sourceTree = "<group>"; };
F858906A29E1CC7A00D4BDED /* UIApplication+Window.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "UIApplication+Window.swift"; sourceTree = "<group>"; };
F85B586C29ED169B00A16D12 /* Vernissage-010.xcdatamodel */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcdatamodel; path = "Vernissage-010.xcdatamodel"; sourceTree = "<group>"; };
F85D0C642ABA08F9002B3577 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = "<group>"; };
F85D4970296402DC00751DF7 /* AuthorizationService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AuthorizationService.swift; sourceTree = "<group>"; };
F85D4974296407F100751DF7 /* HomeTimelineService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HomeTimelineService.swift; sourceTree = "<group>"; };
F85D497629640A5200751DF7 /* ImageRow.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ImageRow.swift; sourceTree = "<group>"; };
F85D497829640B9D00751DF7 /* ImagesCarousel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ImagesCarousel.swift; sourceTree = "<group>"; };
F85D497C29640D5900751DF7 /* InteractionRow.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InteractionRow.swift; sourceTree = "<group>"; };
F85D497E296416C800751DF7 /* CommentsSectionView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CommentsSectionView.swift; sourceTree = "<group>"; };
F85D498229642FAC00751DF7 /* AttachmentData+Comperable.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "AttachmentData+Comperable.swift"; sourceTree = "<group>"; };
F85D49842964301800751DF7 /* StatusData+Attachments.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "StatusData+Attachments.swift"; sourceTree = "<group>"; };
F85D4DFD29B78C8400345267 /* HashtagModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HashtagModel.swift; sourceTree = "<group>"; };
F85DBF8E296732E20069BF89 /* AccountsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AccountsView.swift; sourceTree = "<group>"; };
F86167C5297FE6CC004D1F67 /* AvatarShapesSectionView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AvatarShapesSectionView.swift; sourceTree = "<group>"; };
@ -314,11 +251,7 @@
F864F77729BB930000B13921 /* PhotoWidgetEntry.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PhotoWidgetEntry.swift; sourceTree = "<group>"; };
F864F77B29BB982100B13921 /* StatusFetcher.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StatusFetcher.swift; sourceTree = "<group>"; };
F864F7A429BBA01D00B13921 /* CoreDataError.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CoreDataError.swift; sourceTree = "<group>"; };
F865B4CC2A024AD8008ACDFC /* StatusData+Faulty.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "StatusData+Faulty.swift"; sourceTree = "<group>"; };
F865B4D02A024AFE008ACDFC /* AttachmentData+Faulty.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "AttachmentData+Faulty.swift"; sourceTree = "<group>"; };
F865B4D42A0252FB008ACDFC /* Vernissage-013.xcdatamodel */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcdatamodel; path = "Vernissage-013.xcdatamodel"; sourceTree = "<group>"; };
F866F69E296040A8002E8F88 /* ApplicationSettings+CoreDataClass.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "ApplicationSettings+CoreDataClass.swift"; sourceTree = "<group>"; };
F866F69F296040A8002E8F88 /* ApplicationSettings+CoreDataProperties.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "ApplicationSettings+CoreDataProperties.swift"; sourceTree = "<group>"; };
F866F69F296040A8002E8F88 /* ApplicationSettings.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ApplicationSettings.swift; sourceTree = "<group>"; };
F866F6A229604161002E8F88 /* AccountDataHandler.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AccountDataHandler.swift; sourceTree = "<group>"; };
F866F6A429604194002E8F88 /* ApplicationSettingsHandler.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ApplicationSettingsHandler.swift; sourceTree = "<group>"; };
F866F6A629604629002E8F88 /* SignInView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SignInView.swift; sourceTree = "<group>"; };
@ -342,17 +275,14 @@
F8705A7A29FF872F00DA818A /* QRCodeGenerator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = QRCodeGenerator.swift; sourceTree = "<group>"; };
F8705A7D29FF880600DA818A /* FileFetcher.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FileFetcher.swift; sourceTree = "<group>"; };
F870EE5129F1645C00A2D43B /* MainNavigationOptions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MainNavigationOptions.swift; sourceTree = "<group>"; };
F871F21C29EF0D7000A351EF /* NavigationMenuItemDetails.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NavigationMenuItemDetails.swift; sourceTree = "<group>"; };
F871F21F29EF0FEC00A351EF /* Vernissage-011.xcdatamodel */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcdatamodel; path = "Vernissage-011.xcdatamodel"; sourceTree = "<group>"; };
F87425F62AD5402700A119D7 /* Vernissage-019.xcdatamodel */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcdatamodel; path = "Vernissage-019.xcdatamodel"; sourceTree = "<group>"; };
F8742FC329990AFB00E9642B /* ClientError.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ClientError.swift; sourceTree = "<group>"; };
F8764186298ABB520057D362 /* ViewState.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ViewState.swift; sourceTree = "<group>"; };
F876418C298AE5020057D362 /* PaginableStatusesView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PaginableStatusesView.swift; sourceTree = "<group>"; };
F878842129A4A4E3003CFAD2 /* AppMetadataService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppMetadataService.swift; sourceTree = "<group>"; };
F87AEB912986C44E00434FB6 /* AuthorizationSession.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AuthorizationSession.swift; sourceTree = "<group>"; };
F87AEB962986D16D00434FB6 /* AuthorisationError.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AuthorisationError.swift; sourceTree = "<group>"; };
F880EECE2AC70A2B00C09C31 /* Vernissage-015.xcdatamodel */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcdatamodel; path = "Vernissage-015.xcdatamodel"; sourceTree = "<group>"; };
F883401F29B62AE900C3E096 /* SearchView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SearchView.swift; sourceTree = "<group>"; };
F886BBAB2AE7CF510083152B /* NotificationView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NotificationView.swift; sourceTree = "<group>"; };
F88AB05229B3613900345EDE /* PhotoUrl.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PhotoUrl.swift; sourceTree = "<group>"; };
F88AB05429B3626300345EDE /* ImageGrid.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ImageGrid.swift; sourceTree = "<group>"; };
F88AB05729B36B8200345EDE /* AccountsPhotoView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AccountsPhotoView.swift; sourceTree = "<group>"; };
@ -365,7 +295,6 @@
F88BC51429E02FEB00CE6141 /* VernissageShareExtension.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = VernissageShareExtension.entitlements; sourceTree = "<group>"; };
F88BC51529E0307F00CE6141 /* NotificationsName.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NotificationsName.swift; sourceTree = "<group>"; };
F88BC51929E0344000CE6141 /* ClientKit */ = {isa = PBXFileReference; lastKnownFileType = wrapper; path = ClientKit; sourceTree = "<group>"; };
F88BC51C29E0377B00CE6141 /* AccountData+AccountModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "AccountData+AccountModel.swift"; sourceTree = "<group>"; };
F88BC52529E0421F00CE6141 /* ServicesKit */ = {isa = PBXFileReference; lastKnownFileType = wrapper; path = ServicesKit; sourceTree = "<group>"; };
F88BC52829E0467400CE6141 /* WidgetsKit */ = {isa = PBXFileReference; lastKnownFileType = wrapper; path = WidgetsKit; sourceTree = "<group>"; };
F88BC52B29E04AC200CE6141 /* EnvironmentKit */ = {isa = PBXFileReference; lastKnownFileType = wrapper; path = EnvironmentKit; sourceTree = "<group>"; };
@ -376,41 +305,31 @@
F88C246D295C37B80006098B /* MainView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MainView.swift; sourceTree = "<group>"; };
F88C246F295C37BB0006098B /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = "<group>"; };
F88C2472295C37BB0006098B /* Preview Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = "Preview Assets.xcassets"; sourceTree = "<group>"; };
F88C2474295C37BB0006098B /* CoreDataHandler.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CoreDataHandler.swift; sourceTree = "<group>"; };
F88C2477295C37BB0006098B /* Vernissage.xcdatamodel */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcdatamodel; path = Vernissage.xcdatamodel; sourceTree = "<group>"; };
F88C2474295C37BB0006098B /* SwiftDataHandler.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SwiftDataHandler.swift; sourceTree = "<group>"; };
F88C2481295C3A4F0006098B /* StatusView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StatusView.swift; sourceTree = "<group>"; };
F88E4D41297E69FD0057491A /* StatusesView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StatusesView.swift; sourceTree = "<group>"; };
F88E4D47297E90CD0057491A /* TrendStatusesView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TrendStatusesView.swift; sourceTree = "<group>"; };
F88E4D49297EA0490057491A /* RouterPath.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RouterPath.swift; sourceTree = "<group>"; };
F88E4D55297EAD6E0057491A /* AppRouteur.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppRouteur.swift; sourceTree = "<group>"; };
F88FAD20295F3944009B20C9 /* HomeFeedView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HomeFeedView.swift; sourceTree = "<group>"; };
F88FAD26295F400E009B20C9 /* NotificationsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NotificationsView.swift; sourceTree = "<group>"; };
F88FAD28295F43B8009B20C9 /* AccountData+CoreDataClass.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "AccountData+CoreDataClass.swift"; sourceTree = "<group>"; };
F88FAD29295F43B8009B20C9 /* AccountData+CoreDataProperties.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "AccountData+CoreDataProperties.swift"; sourceTree = "<group>"; };
F8911A1829DE9E5500770F44 /* Vernissage-007.xcdatamodel */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcdatamodel; path = "Vernissage-007.xcdatamodel"; sourceTree = "<group>"; };
F891E7CD29C35BF50022C449 /* ImageRowItem.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ImageRowItem.swift; sourceTree = "<group>"; };
F88FAD29295F43B8009B20C9 /* AccountData.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AccountData.swift; sourceTree = "<group>"; };
F891E7CF29C368750022C449 /* ImageRowItemAsync.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ImageRowItemAsync.swift; sourceTree = "<group>"; };
F89229EE2ADA63620040C964 /* Localizable.xcstrings */ = {isa = PBXFileReference; lastKnownFileType = text.json.xcstrings; path = Localizable.xcstrings; sourceTree = "<group>"; };
F897978E29684BCB00B22335 /* LoadingView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LoadingView.swift; sourceTree = "<group>"; };
F89992C8296D6DC7005994BF /* CommentBodyView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CommentBodyView.swift; sourceTree = "<group>"; };
F89A46DB296EAACE0062125F /* SettingsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SettingsView.swift; sourceTree = "<group>"; };
F89A46DD296EABA20062125F /* StatusPlaceholderView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StatusPlaceholderView.swift; sourceTree = "<group>"; };
F89AC00429A1F9B500F4159F /* AppMetadata.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppMetadata.swift; sourceTree = "<group>"; };
F89B5CC129D01BF700549F2F /* InstanceView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InstanceView.swift; sourceTree = "<group>"; };
F89CEB7F2984198600A1376F /* AttachmentData+HighestImage.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "AttachmentData+HighestImage.swift"; sourceTree = "<group>"; };
F89D6C4129717FDC001DA3D4 /* AccountsSectionView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AccountsSectionView.swift; sourceTree = "<group>"; };
F89D6C4329718092001DA3D4 /* AccentsSectionView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AccentsSectionView.swift; sourceTree = "<group>"; };
F89D6C4529718193001DA3D4 /* GeneralSectionView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GeneralSectionView.swift; sourceTree = "<group>"; };
F89D6C49297196FF001DA3D4 /* ImageViewer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ImageViewer.swift; sourceTree = "<group>"; };
F89F0605299139F6003DC875 /* Vernissage-002.xcdatamodel */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcdatamodel; path = "Vernissage-002.xcdatamodel"; sourceTree = "<group>"; };
F89F57AF29D1C11200001EE3 /* RelationshipModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RelationshipModel.swift; sourceTree = "<group>"; };
F8A270DA29F500860062D275 /* fr */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = fr; path = fr.lproj/Localizable.strings; sourceTree = "<group>"; };
F8A4A88429E4099900267E36 /* Vernissage-008.xcdatamodel */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcdatamodel; path = "Vernissage-008.xcdatamodel"; sourceTree = "<group>"; };
F8A93D7D2965FD89001D8331 /* UserProfileView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserProfileView.swift; sourceTree = "<group>"; };
F8AFF7C029B259150087D083 /* HashtagsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HashtagsView.swift; sourceTree = "<group>"; };
F8AFF7C329B25EF40087D083 /* ImagesGrid.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ImagesGrid.swift; sourceTree = "<group>"; };
F8B05AC929B488C600857221 /* Vernissage-003.xcdatamodel */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcdatamodel; path = "Vernissage-003.xcdatamodel"; sourceTree = "<group>"; };
F8B05ACA29B489B100857221 /* HapticsSectionView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HapticsSectionView.swift; sourceTree = "<group>"; };
F8B05ACC29B48DD000857221 /* Vernissage-004.xcdatamodel */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcdatamodel; path = "Vernissage-004.xcdatamodel"; sourceTree = "<group>"; };
F8B05ACD29B48E2F00857221 /* MediaSettingsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MediaSettingsView.swift; sourceTree = "<group>"; };
F8B0885D29942E31002AB40A /* ThirdPartyView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ThirdPartyView.swift; sourceTree = "<group>"; };
F8B0885F29943498002AB40A /* OtherSectionView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OtherSectionView.swift; sourceTree = "<group>"; };
@ -418,26 +337,19 @@
F8B3699A29D86EB600BE3808 /* .swiftlint.yml */ = {isa = PBXFileReference; lastKnownFileType = text.yaml; path = .swiftlint.yml; sourceTree = "<group>"; };
F8B3699B29D86EBD00BE3808 /* .gitignore */ = {isa = PBXFileReference; lastKnownFileType = text; path = .gitignore; sourceTree = "<group>"; };
F8B758DD2AB9DD85000C8068 /* ColumnData.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ColumnData.swift; sourceTree = "<group>"; };
F8BD04192ACC2280004B8E2C /* Vernissage-016.xcdatamodel */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcdatamodel; path = "Vernissage-016.xcdatamodel"; sourceTree = "<group>"; };
F8C937A929882CA90004D782 /* Vernissage-001.xcdatamodel */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcdatamodel; path = "Vernissage-001.xcdatamodel"; sourceTree = "<group>"; };
F8CAE64129B8F1AF001E0372 /* Vernissage-005.xcdatamodel */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcdatamodel; path = "Vernissage-005.xcdatamodel"; sourceTree = "<group>"; };
F8D0E5212AE2A2630061C561 /* HomeTimelineView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HomeTimelineView.swift; sourceTree = "<group>"; };
F8D0E5232AE2A88A0061C561 /* HomeTimelineService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HomeTimelineService.swift; sourceTree = "<group>"; };
F8D5444229D4066C002225D6 /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = "<group>"; };
F8D8E0C62ACC234A00AA1374 /* ViewedStatus+CoreDataClass.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "ViewedStatus+CoreDataClass.swift"; sourceTree = "<group>"; };
F8D8E0CA2ACC237000AA1374 /* ViewedStatus+CoreDataProperties.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "ViewedStatus+CoreDataProperties.swift"; sourceTree = "<group>"; };
F8D8E0CA2ACC237000AA1374 /* ViewedStatus.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ViewedStatus.swift; sourceTree = "<group>"; };
F8D8E0CE2ACC23B300AA1374 /* ViewedStatusHandler.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ViewedStatusHandler.swift; sourceTree = "<group>"; };
F8D8E0D22ACC89CB00AA1374 /* Vernissage-017.xcdatamodel */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcdatamodel; path = "Vernissage-017.xcdatamodel"; sourceTree = "<group>"; };
F8DE749E2AE4F7B500ACD188 /* NotificationsService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NotificationsService.swift; sourceTree = "<group>"; };
F8DF38E329DD68820047F1AA /* ViewOffsetKey.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ViewOffsetKey.swift; sourceTree = "<group>"; };
F8DF38E529DDB98A0047F1AA /* SocialsSectionView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SocialsSectionView.swift; sourceTree = "<group>"; };
F8DF38E729DDC3D20047F1AA /* eu */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = eu; path = eu.lproj/Localizable.strings; sourceTree = "<group>"; };
F8E36E452AB8745300769C55 /* Sizable.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Sizable.swift; sourceTree = "<group>"; };
F8E36E472AB874A500769C55 /* StatusModel+Sizeable.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "StatusModel+Sizeable.swift"; sourceTree = "<group>"; };
F8E6D03229CDD52500416CCA /* EditProfileView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EditProfileView.swift; sourceTree = "<group>"; };
F8E7ADF82AD44AFD0038FFFD /* Vernissage-018.xcdatamodel */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcdatamodel; path = "Vernissage-018.xcdatamodel"; sourceTree = "<group>"; };
F8E7ADF92AD44CC00038FFFD /* AccountRelationship+CoreDataClass.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "AccountRelationship+CoreDataClass.swift"; sourceTree = "<group>"; };
F8E7ADFD2AD44CEB0038FFFD /* AccountRelationship+CoreDataProperties.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "AccountRelationship+CoreDataProperties.swift"; sourceTree = "<group>"; };
F8E7ADFD2AD44CEB0038FFFD /* AccountRelationship.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AccountRelationship.swift; sourceTree = "<group>"; };
F8E7AE012AD44D600038FFFD /* AccountRelationshipHandler.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AccountRelationshipHandler.swift; sourceTree = "<group>"; };
F8EF371429C624DA00669F45 /* Vernissage-006.xcdatamodel */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcdatamodel; path = "Vernissage-006.xcdatamodel"; sourceTree = "<group>"; };
F8EF3C8B29FC3A5F00CBFF7C /* Vernissage-012.xcdatamodel */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcdatamodel; path = "Vernissage-012.xcdatamodel"; sourceTree = "<group>"; };
F8F6E44329BC5CAA0004795E /* VernissageWidgetExtension.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = VernissageWidgetExtension.entitlements; sourceTree = "<group>"; };
F8F6E44429BC5CC50004795E /* Vernissage.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = Vernissage.entitlements; sourceTree = "<group>"; };
F8F6E44629BCC0DC0004795E /* PhotoSmallWidgetView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PhotoSmallWidgetView.swift; sourceTree = "<group>"; };
@ -446,7 +358,6 @@
F8F6E45029BCE9190004795E /* UIImage+Resize.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UIImage+Resize.swift"; sourceTree = "<group>"; };
F8FAA0AC2AB0BCB400FD78BD /* View+ContainerBackground.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "View+ContainerBackground.swift"; sourceTree = "<group>"; };
F8FB8AB929EB2ED400342C04 /* NavigationMenuButtons.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NavigationMenuButtons.swift; sourceTree = "<group>"; };
F8FFBD4929E99BEE0047EE80 /* Vernissage-009.xcdatamodel */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcdatamodel; path = "Vernissage-009.xcdatamodel"; sourceTree = "<group>"; };
/* End PBXFileReference section */
/* Begin PBXFrameworksBuildPhase section */
@ -541,7 +452,7 @@
F89D6C4829718868001DA3D4 /* StatusView */,
F88C246D295C37B80006098B /* MainView.swift */,
F88ABD9329687CA4004EF61E /* ComposeView.swift */,
F88FAD20295F3944009B20C9 /* HomeFeedView.swift */,
F8D0E5212AE2A2630061C561 /* HomeTimelineView.swift */,
F88AB05729B36B8200345EDE /* AccountsPhotoView.swift */,
F88E4D47297E90CD0057491A /* TrendStatusesView.swift */,
F883401F29B62AE900C3E096 /* SearchView.swift */,
@ -570,7 +481,6 @@
F85D4DFD29B78C8400345267 /* HashtagModel.swift */,
F89F57AF29D1C11200001EE3 /* RelationshipModel.swift */,
F8DF38E329DD68820047F1AA /* ViewOffsetKey.swift */,
F871F21C29EF0D7000A351EF /* NavigationMenuItemDetails.swift */,
F8624D3C29F2D3AC00204986 /* SelectedMenuItemDetails.swift */,
F8E36E452AB8745300769C55 /* Sizable.swift */,
F8B758DD2AB9DD85000C8068 /* ColumnData.swift */,
@ -581,32 +491,13 @@
F8341F96295C6427009C8EE6 /* CoreData */ = {
isa = PBXGroup;
children = (
F88C2476295C37BB0006098B /* Vernissage.xcdatamodeld */,
F80047FF2961850500E6868A /* AttachmentData+CoreDataClass.swift */,
F80048002961850500E6868A /* AttachmentData+CoreDataProperties.swift */,
F85D498229642FAC00751DF7 /* AttachmentData+Comperable.swift */,
F8210DDE2966CFC7001D9973 /* AttachmentData+Attachment.swift */,
F89CEB7F2984198600A1376F /* AttachmentData+HighestImage.swift */,
F865B4D02A024AFE008ACDFC /* AttachmentData+Faulty.swift */,
F80048012961850500E6868A /* StatusData+CoreDataClass.swift */,
F80048022961850500E6868A /* StatusData+CoreDataProperties.swift */,
F85D49842964301800751DF7 /* StatusData+Attachments.swift */,
F8210DDC2966CF17001D9973 /* StatusData+Status.swift */,
F865B4CC2A024AD8008ACDFC /* StatusData+Faulty.swift */,
F866F69E296040A8002E8F88 /* ApplicationSettings+CoreDataClass.swift */,
F866F69F296040A8002E8F88 /* ApplicationSettings+CoreDataProperties.swift */,
F88FAD28295F43B8009B20C9 /* AccountData+CoreDataClass.swift */,
F88FAD29295F43B8009B20C9 /* AccountData+CoreDataProperties.swift */,
F88BC51C29E0377B00CE6141 /* AccountData+AccountModel.swift */,
F8D8E0C62ACC234A00AA1374 /* ViewedStatus+CoreDataClass.swift */,
F8D8E0CA2ACC237000AA1374 /* ViewedStatus+CoreDataProperties.swift */,
F8E7ADF92AD44CC00038FFFD /* AccountRelationship+CoreDataClass.swift */,
F8E7ADFD2AD44CEB0038FFFD /* AccountRelationship+CoreDataProperties.swift */,
F88C2474295C37BB0006098B /* CoreDataHandler.swift */,
F866F69F296040A8002E8F88 /* ApplicationSettings.swift */,
F88FAD29295F43B8009B20C9 /* AccountData.swift */,
F8D8E0CA2ACC237000AA1374 /* ViewedStatus.swift */,
F8E7ADFD2AD44CEB0038FFFD /* AccountRelationship.swift */,
F88C2474295C37BB0006098B /* SwiftDataHandler.swift */,
F866F6A229604161002E8F88 /* AccountDataHandler.swift */,
F866F6A429604194002E8F88 /* ApplicationSettingsHandler.swift */,
F80048072961E6DE00E6868A /* StatusDataHandler.swift */,
F80048092961EA1900E6868A /* AttachmentDataHandler.swift */,
F8D8E0CE2ACC23B300AA1374 /* ViewedStatusHandler.swift */,
F8E7AE012AD44D600038FFFD /* AccountRelationshipHandler.swift */,
F864F7A429BBA01D00B13921 /* CoreDataError.swift */,
@ -617,7 +508,7 @@
F835081F29BEF88600DE3247 /* Localization */ = {
isa = PBXGroup;
children = (
F835082629BEF9C400DE3247 /* Localizable.strings */,
F89229EE2ADA63620040C964 /* Localizable.xcstrings */,
);
path = Localization;
sourceTree = "<group>";
@ -625,8 +516,6 @@
F83901A2295D863B00456AE2 /* Widgets */ = {
isa = PBXGroup;
children = (
F85D497629640A5200751DF7 /* ImageRow.swift */,
F891E7CD29C35BF50022C449 /* ImageRowItem.swift */,
F8210DCE2966B600001D9973 /* ImageRowAsync.swift */,
F891E7CF29C368750022C449 /* ImageRowItemAsync.swift */,
F85D497829640B9D00751DF7 /* ImagesCarousel.swift */,
@ -747,6 +636,7 @@
F8B05ACA29B489B100857221 /* HapticsSectionView.swift */,
F8B05ACD29B48E2F00857221 /* MediaSettingsView.swift */,
F8DF38E529DDB98A0047F1AA /* SocialsSectionView.swift */,
F886BBAB2AE7CF510083152B /* NotificationView.swift */,
);
path = Subviews;
sourceTree = "<group>";
@ -881,10 +771,11 @@
children = (
F85D4970296402DC00751DF7 /* AuthorizationService.swift */,
F87AEB912986C44E00434FB6 /* AuthorizationSession.swift */,
F85D4974296407F100751DF7 /* HomeTimelineService.swift */,
F88E4D49297EA0490057491A /* RouterPath.swift */,
F878842129A4A4E3003CFAD2 /* AppMetadataService.swift */,
F86BC9E829EBBB66009415EC /* ImageSaver.swift */,
F8D0E5232AE2A88A0061C561 /* HomeTimelineService.swift */,
F8DE749E2AE4F7B500ACD188 /* NotificationsService.swift */,
);
path = Services;
sourceTree = "<group>";
@ -1065,6 +956,7 @@
pl,
eu,
fr,
es,
);
mainGroup = F88C245F295C37B80006098B;
packageReferences = (
@ -1091,7 +983,7 @@
buildActionMask = 2147483647;
files = (
F85D0C652ABA08F9002B3577 /* Assets.xcassets in Resources */,
F835082429BEF9C400DE3247 /* Localizable.strings in Resources */,
F89229F02ADA63620040C964 /* Localizable.xcstrings in Resources */,
);
runOnlyForDeploymentPostprocessing = 0;
};
@ -1099,8 +991,8 @@
isa = PBXResourcesBuildPhase;
buildActionMask = 2147483647;
files = (
F88BC53029E0672000CE6141 /* Localizable.strings in Resources */,
F88BC55429E0798900CE6141 /* SharedAssets.xcassets in Resources */,
F89229F12ADA63620040C964 /* Localizable.xcstrings in Resources */,
);
runOnlyForDeploymentPostprocessing = 0;
};
@ -1110,7 +1002,7 @@
files = (
F88BC55229E0798900CE6141 /* SharedAssets.xcassets in Resources */,
F88C2473295C37BB0006098B /* Preview Assets.xcassets in Resources */,
F835082329BEF9C400DE3247 /* Localizable.strings in Resources */,
F89229EF2ADA63620040C964 /* Localizable.xcstrings in Resources */,
F88C2470295C37BB0006098B /* Assets.xcassets in Resources */,
F86A42FF299A8C5500DF7645 /* InAppPurchaseStoreKitConfiguration.storekit in Resources */,
);
@ -1130,43 +1022,30 @@
F8705A7729FF7ABD00DA818A /* QRCodeSmallWidgetView.swift in Sources */,
F864F77C29BB982100B13921 /* StatusFetcher.swift in Sources */,
F84625ED29FE295B002D3AF4 /* QRCodeLargeWidgetView.swift in Sources */,
F8F6E44229BC58F20004795E /* Vernissage.xcdatamodeld in Sources */,
F8F6E44C29BCC1F70004795E /* PhotoSmallWidgetView.swift in Sources */,
F864F76629BB91B400B13921 /* PhotoWidget.swift in Sources */,
F8E7ADFB2AD44CC00038FFFD /* AccountRelationship+CoreDataClass.swift in Sources */,
F8F6E44D29BCC1F90004795E /* PhotoMediumWidgetView.swift in Sources */,
F815F60C29E49CF20044566B /* Avatar.swift in Sources */,
F8F6E44E29BCC1FB0004795E /* PhotoLargeWidgetView.swift in Sources */,
F864F76429BB91B400B13921 /* VernissageWidgetBundle.swift in Sources */,
F864F77D29BB9A4600B13921 /* AttachmentData+CoreDataClass.swift in Sources */,
F8D8E0C82ACC234A00AA1374 /* ViewedStatus+CoreDataClass.swift in Sources */,
F8E7ADFF2AD44CEB0038FFFD /* AccountRelationship+CoreDataProperties.swift in Sources */,
F8D8E0CC2ACC237000AA1374 /* ViewedStatus+CoreDataProperties.swift in Sources */,
F8E7ADFF2AD44CEB0038FFFD /* AccountRelationship.swift in Sources */,
F8D8E0CC2ACC237000AA1374 /* ViewedStatus.swift in Sources */,
F864F7A629BBA01D00B13921 /* CoreDataError.swift in Sources */,
F864F77E29BB9A4900B13921 /* AttachmentData+CoreDataProperties.swift in Sources */,
F864F78229BB9A6500B13921 /* StatusData+CoreDataClass.swift in Sources */,
F864F78329BB9A6800B13921 /* StatusData+CoreDataProperties.swift in Sources */,
F864F78429BB9A6E00B13921 /* ApplicationSettings+CoreDataClass.swift in Sources */,
F8FAA0AD2AB0BCB400FD78BD /* View+ContainerBackground.swift in Sources */,
F864F78629BB9A7400B13921 /* AccountData+CoreDataClass.swift in Sources */,
F8705A7B29FF872F00DA818A /* QRCodeGenerator.swift in Sources */,
F865B4CE2A024AD8008ACDFC /* StatusData+Faulty.swift in Sources */,
F8705A7E29FF880600DA818A /* FileFetcher.swift in Sources */,
F865B4D22A024AFE008ACDFC /* AttachmentData+Faulty.swift in Sources */,
F864F78529BB9A7100B13921 /* ApplicationSettings+CoreDataProperties.swift in Sources */,
F864F78529BB9A7100B13921 /* ApplicationSettings.swift in Sources */,
F84625E929FE2788002D3AF4 /* QRCodeWidget.swift in Sources */,
F864F78729BB9A7700B13921 /* AccountData+CoreDataProperties.swift in Sources */,
F864F78729BB9A7700B13921 /* AccountData.swift in Sources */,
F8E7AE032AD44D600038FFFD /* AccountRelationshipHandler.swift in Sources */,
F864F78829BB9A7B00B13921 /* CoreDataHandler.swift in Sources */,
F864F78829BB9A7B00B13921 /* SwiftDataHandler.swift in Sources */,
F8F6E45129BCE9190004795E /* UIImage+Resize.swift in Sources */,
F864F78929BB9A7D00B13921 /* AccountDataHandler.swift in Sources */,
F864F78A29BB9A8000B13921 /* ApplicationSettingsHandler.swift in Sources */,
F84625F229FE2B6B002D3AF4 /* QRCodeWidgetEntry.swift in Sources */,
F84625EB29FE28D4002D3AF4 /* QRCodeWidgetEntryView.swift in Sources */,
F864F78C29BB9A8500B13921 /* AttachmentDataHandler.swift in Sources */,
F84625F829FE2C2F002D3AF4 /* AccountFetcher.swift in Sources */,
F84625F429FE2BF9002D3AF4 /* QRCodeProvider.swift in Sources */,
F864F78B29BB9A8300B13921 /* StatusDataHandler.swift in Sources */,
F8705A7929FF7CCB00DA818A /* QRCodeMediumWidgetView.swift in Sources */,
);
runOnlyForDeploymentPostprocessing = 0;
@ -1176,32 +1055,18 @@
buildActionMask = 2147483647;
files = (
F88BC54529E072B200CE6141 /* AccountDataHandler.swift in Sources */,
F8E7ADFC2AD44CC00038FFFD /* AccountRelationship+CoreDataClass.swift in Sources */,
F88BC54729E072B800CE6141 /* AccountData+CoreDataProperties.swift in Sources */,
F88BC54729E072B800CE6141 /* AccountData.swift in Sources */,
F8D8E0D12ACC23B300AA1374 /* ViewedStatusHandler.swift in Sources */,
F88BC54D29E072D600CE6141 /* AttachmentData+CoreDataProperties.swift in Sources */,
F8E7AE002AD44CEB0038FFFD /* AccountRelationship+CoreDataProperties.swift in Sources */,
F88BC54F29E073BC00CE6141 /* AccountData+AccountModel.swift in Sources */,
F865B4D32A024AFE008ACDFC /* AttachmentData+Faulty.swift in Sources */,
F88BC54629E072B500CE6141 /* CoreDataHandler.swift in Sources */,
F8E7AE002AD44CEB0038FFFD /* AccountRelationship.swift in Sources */,
F88BC54629E072B500CE6141 /* SwiftDataHandler.swift in Sources */,
F88BC50529E02F3900CE6141 /* ShareViewController.swift in Sources */,
F88BC54129E072A600CE6141 /* CoreDataError.swift in Sources */,
F88BC54229E072A900CE6141 /* AttachmentDataHandler.swift in Sources */,
F88BC54429E072AF00CE6141 /* ApplicationSettingsHandler.swift in Sources */,
F8D8E0CD2ACC237000AA1374 /* ViewedStatus+CoreDataProperties.swift in Sources */,
F8D8E0CD2ACC237000AA1374 /* ViewedStatus.swift in Sources */,
F88BC51629E0307F00CE6141 /* NotificationsName.swift in Sources */,
F8E7AE042AD44D600038FFFD /* AccountRelationshipHandler.swift in Sources */,
F88BC54829E072BC00CE6141 /* AccountData+CoreDataClass.swift in Sources */,
F88BC51329E02FD800CE6141 /* ComposeView.swift in Sources */,
F88BC54E29E072D900CE6141 /* AttachmentData+CoreDataClass.swift in Sources */,
F8D8E0C92ACC234A00AA1374 /* ViewedStatus+CoreDataClass.swift in Sources */,
F88BC54C29E072CD00CE6141 /* StatusData+CoreDataClass.swift in Sources */,
F88BC54B29E072CA00CE6141 /* StatusData+CoreDataProperties.swift in Sources */,
F88BC54A29E072C400CE6141 /* ApplicationSettings+CoreDataClass.swift in Sources */,
F865B4CF2A024AD8008ACDFC /* StatusData+Faulty.swift in Sources */,
F88BC54929E072C000CE6141 /* ApplicationSettings+CoreDataProperties.swift in Sources */,
F88BC55029E074EB00CE6141 /* Vernissage.xcdatamodeld in Sources */,
F88BC54329E072AC00CE6141 /* StatusDataHandler.swift in Sources */,
F88BC54929E072C000CE6141 /* ApplicationSettings.swift in Sources */,
);
runOnlyForDeploymentPostprocessing = 0;
};
@ -1209,62 +1074,44 @@
isa = PBXSourcesBuildPhase;
buildActionMask = 2147483647;
files = (
F85D497729640A5200751DF7 /* ImageRow.swift in Sources */,
F8210DDF2966CFC7001D9973 /* AttachmentData+Attachment.swift in Sources */,
F88AB05529B3626300345EDE /* ImageGrid.swift in Sources */,
F87AEB922986C44E00434FB6 /* AuthorizationSession.swift in Sources */,
F86A4301299A97F500DF7645 /* ProductIdentifiers.swift in Sources */,
F89D6C4229717FDC001DA3D4 /* AccountsSectionView.swift in Sources */,
F8E36E482AB874A500769C55 /* StatusModel+Sizeable.swift in Sources */,
F80048082961E6DE00E6868A /* StatusDataHandler.swift in Sources */,
F866F6A0296040A8002E8F88 /* ApplicationSettings+CoreDataClass.swift in Sources */,
F8210DEA2966E4F9001D9973 /* AnimatePlaceholderModifier.swift in Sources */,
F865B4D12A024AFE008ACDFC /* AttachmentData+Faulty.swift in Sources */,
F8B08862299435C9002AB40A /* SupportView.swift in Sources */,
F8B05ACB29B489B100857221 /* HapticsSectionView.swift in Sources */,
F88FAD2B295F43B8009B20C9 /* AccountData+CoreDataProperties.swift in Sources */,
F85D4975296407F100751DF7 /* HomeTimelineService.swift in Sources */,
F80048062961850500E6868A /* StatusData+CoreDataProperties.swift in Sources */,
F88FAD21295F3944009B20C9 /* HomeFeedView.swift in Sources */,
F88C2475295C37BB0006098B /* CoreDataHandler.swift in Sources */,
F88FAD2B295F43B8009B20C9 /* AccountData.swift in Sources */,
F88C2475295C37BB0006098B /* SwiftDataHandler.swift in Sources */,
F87AEB972986D16D00434FB6 /* AuthorisationError.swift in Sources */,
F8742FC429990AFB00E9642B /* ClientError.swift in Sources */,
F883402029B62AE900C3E096 /* SearchView.swift in Sources */,
F88FAD2A295F43B8009B20C9 /* AccountData+CoreDataClass.swift in Sources */,
F871F21D29EF0D7000A351EF /* NavigationMenuItemDetails.swift in Sources */,
F8E36E462AB8745300769C55 /* Sizable.swift in Sources */,
F85DBF8F296732E20069BF89 /* AccountsView.swift in Sources */,
F805DCF129DBEF83006A1FD9 /* ReportView.swift in Sources */,
F8B0886029943498002AB40A /* OtherSectionView.swift in Sources */,
F808641429756666009F035C /* NotificationRowView.swift in Sources */,
F8D8E0C72ACC234A00AA1374 /* ViewedStatus+CoreDataClass.swift in Sources */,
F8624D3D29F2D3AC00204986 /* SelectedMenuItemDetails.swift in Sources */,
F8210DDD2966CF17001D9973 /* StatusData+Status.swift in Sources */,
F8210DCF2966B600001D9973 /* ImageRowAsync.swift in Sources */,
F85D498329642FAC00751DF7 /* AttachmentData+Comperable.swift in Sources */,
F86A4305299AA12800DF7645 /* PurchaseError.swift in Sources */,
F8B05ACE29B48E2F00857221 /* MediaSettingsView.swift in Sources */,
F89D6C4429718092001DA3D4 /* AccentsSectionView.swift in Sources */,
F88E4D42297E69FD0057491A /* StatusesView.swift in Sources */,
F878842229A4A4E3003CFAD2 /* AppMetadataService.swift in Sources */,
F85D497929640B9D00751DF7 /* ImagesCarousel.swift in Sources */,
F8D0E5222AE2A2630061C561 /* HomeTimelineView.swift in Sources */,
F89AC00529A1F9B500F4159F /* AppMetadata.swift in Sources */,
F80048052961850500E6868A /* StatusData+CoreDataClass.swift in Sources */,
F891E7CE29C35BF50022C449 /* ImageRowItem.swift in Sources */,
F86B7221296C49A300EE59EC /* EmptyButtonStyle.swift in Sources */,
F80048042961850500E6868A /* AttachmentData+CoreDataProperties.swift in Sources */,
F8B758DE2AB9DD85000C8068 /* ColumnData.swift in Sources */,
F88E4D4A297EA0490057491A /* RouterPath.swift in Sources */,
F88E4D48297E90CD0057491A /* TrendStatusesView.swift in Sources */,
F800480A2961EA1900E6868A /* AttachmentDataHandler.swift in Sources */,
F80048032961850500E6868A /* AttachmentData+CoreDataClass.swift in Sources */,
F891E7D029C368750022C449 /* ImageRowItemAsync.swift in Sources */,
F89D6C4A297196FF001DA3D4 /* ImageViewer.swift in Sources */,
F8A93D7E2965FD89001D8331 /* UserProfileView.swift in Sources */,
F88C246E295C37B80006098B /* MainView.swift in Sources */,
F8E7ADFA2AD44CC00038FFFD /* AccountRelationship+CoreDataClass.swift in Sources */,
F8AFF7C429B25EF40087D083 /* ImagesGrid.swift in Sources */,
F88C2478295C37BB0006098B /* Vernissage.xcdatamodeld in Sources */,
F8DE749F2AE4F7B500ACD188 /* NotificationsService.swift in Sources */,
F8AFF7C129B259150087D083 /* HashtagsView.swift in Sources */,
F8DF38E429DD68820047F1AA /* ViewOffsetKey.swift in Sources */,
F89A46DE296EABA20062125F /* StatusPlaceholderView.swift in Sources */,
@ -1276,13 +1123,10 @@
F8B0885E29942E31002AB40A /* ThirdPartyView.swift in Sources */,
F8E6D03329CDD52500416CCA /* EditProfileView.swift in Sources */,
F858906B29E1CC7A00D4BDED /* UIApplication+Window.swift in Sources */,
F865B4CD2A024AD8008ACDFC /* StatusData+Faulty.swift in Sources */,
F876418D298AE5020057D362 /* PaginableStatusesView.swift in Sources */,
F85D49852964301800751DF7 /* StatusData+Attachments.swift in Sources */,
F8764187298ABB520057D362 /* ViewState.swift in Sources */,
F870EE5229F1645C00A2D43B /* MainNavigationOptions.swift in Sources */,
F88ABD9429687CA4004EF61E /* ComposeView.swift in Sources */,
F89CEB802984198600A1376F /* AttachmentData+HighestImage.swift in Sources */,
F86B7214296BFDCE00EE59EC /* UserProfileHeaderView.swift in Sources */,
F85D497D29640D5900751DF7 /* InteractionRow.swift in Sources */,
F86167C6297FE6CC004D1F67 /* AvatarShapesSectionView.swift in Sources */,
@ -1294,10 +1138,10 @@
F86A4307299AA5E900DF7645 /* ThanksView.swift in Sources */,
F8FB8ABA29EB2ED400342C04 /* NavigationMenuButtons.swift in Sources */,
F8D8E0CF2ACC23B300AA1374 /* ViewedStatusHandler.swift in Sources */,
F88BC51D29E0377B00CE6141 /* AccountData+AccountModel.swift in Sources */,
F89B5CC229D01BF700549F2F /* InstanceView.swift in Sources */,
F825F0CB29F7CFC4008BD204 /* FollowRequestsView.swift in Sources */,
F825F0C929F7A562008BD204 /* UserProfilePrivateAccountView.swift in Sources */,
F8D0E5242AE2A88A0061C561 /* HomeTimelineService.swift in Sources */,
F89F57B029D1C11200001EE3 /* RelationshipModel.swift in Sources */,
F88AB05829B36B8200345EDE /* AccountsPhotoView.swift in Sources */,
F85D4971296402DC00751DF7 /* AuthorizationService.swift in Sources */,
@ -1306,11 +1150,11 @@
F88E4D56297EAD6E0057491A /* AppRouteur.swift in Sources */,
F88FAD27295F400E009B20C9 /* NotificationsView.swift in Sources */,
F86B7216296BFFDA00EE59EC /* UserProfileStatusesView.swift in Sources */,
F8D8E0CB2ACC237000AA1374 /* ViewedStatus+CoreDataProperties.swift in Sources */,
F8D8E0CB2ACC237000AA1374 /* ViewedStatus.swift in Sources */,
F897978F29684BCB00B22335 /* LoadingView.swift in Sources */,
F8E7ADFE2AD44CEB0038FFFD /* AccountRelationship+CoreDataProperties.swift in Sources */,
F8E7ADFE2AD44CEB0038FFFD /* AccountRelationship.swift in Sources */,
F89992C9296D6DC7005994BF /* CommentBodyView.swift in Sources */,
F866F6A1296040A8002E8F88 /* ApplicationSettings+CoreDataProperties.swift in Sources */,
F866F6A1296040A8002E8F88 /* ApplicationSettings.swift in Sources */,
F8E7AE022AD44D600038FFFD /* AccountRelationshipHandler.swift in Sources */,
F86A4303299A9AF500DF7645 /* TipsStore.swift in Sources */,
F8DF38E629DDB98A0047F1AA /* SocialsSectionView.swift in Sources */,
@ -1321,6 +1165,7 @@
F89A46DC296EAACE0062125F /* SettingsView.swift in Sources */,
F866F6AE29606367002E8F88 /* ApplicationViewMode.swift in Sources */,
F8675DD02A1FA40500A89959 /* WaterfallGrid.swift in Sources */,
F886BBAC2AE7CF510083152B /* NotificationView.swift in Sources */,
F85D4DFE29B78C8400345267 /* HashtagModel.swift in Sources */,
F866F6AA29605AFA002E8F88 /* SceneDelegate.swift in Sources */,
);
@ -1341,20 +1186,6 @@
};
/* End PBXTargetDependency section */
/* Begin PBXVariantGroup section */
F835082629BEF9C400DE3247 /* Localizable.strings */ = {
isa = PBXVariantGroup;
children = (
F835082529BEF9C400DE3247 /* en */,
F835082729BEFA1E00DE3247 /* pl */,
F8DF38E729DDC3D20047F1AA /* eu */,
F8A270DA29F500860062D275 /* fr */,
);
name = Localizable.strings;
sourceTree = "<group>";
};
/* End PBXVariantGroup section */
/* Begin XCBuildConfiguration section */
F864F76E29BB91B600B13921 /* Debug */ = {
isa = XCBuildConfiguration;
@ -1372,16 +1203,19 @@
INFOPLIST_FILE = VernissageWidget/Info.plist;
INFOPLIST_KEY_CFBundleDisplayName = VernissageWidget;
INFOPLIST_KEY_NSHumanReadableCopyright = "";
IPHONEOS_DEPLOYMENT_TARGET = 16.6;
IPHONEOS_DEPLOYMENT_TARGET = 17.0;
LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)",
"@executable_path/Frameworks",
"@executable_path/../../Frameworks",
);
MARKETING_VERSION = 1.13.0;
MARKETING_VERSION = 2.0.0;
PRODUCT_BUNDLE_IDENTIFIER = dev.mczachurski.vernissage.widget;
PRODUCT_NAME = "$(TARGET_NAME)";
SKIP_INSTALL = YES;
SUPPORTED_PLATFORMS = "iphoneos iphonesimulator";
SUPPORTS_MACCATALYST = NO;
SUPPORTS_MAC_DESIGNED_FOR_IPHONE_IPAD = NO;
SWIFT_EMIT_LOC_STRINGS = YES;
SWIFT_VERSION = 5.0;
TARGETED_DEVICE_FAMILY = "1,2";
@ -1403,16 +1237,19 @@
INFOPLIST_FILE = VernissageWidget/Info.plist;
INFOPLIST_KEY_CFBundleDisplayName = VernissageWidget;
INFOPLIST_KEY_NSHumanReadableCopyright = "";
IPHONEOS_DEPLOYMENT_TARGET = 16.6;
IPHONEOS_DEPLOYMENT_TARGET = 17.0;
LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)",
"@executable_path/Frameworks",
"@executable_path/../../Frameworks",
);
MARKETING_VERSION = 1.13.0;
MARKETING_VERSION = 2.0.0;
PRODUCT_BUNDLE_IDENTIFIER = dev.mczachurski.vernissage.widget;
PRODUCT_NAME = "$(TARGET_NAME)";
SKIP_INSTALL = YES;
SUPPORTED_PLATFORMS = "iphoneos iphonesimulator";
SUPPORTS_MACCATALYST = NO;
SUPPORTS_MAC_DESIGNED_FOR_IPHONE_IPAD = NO;
SWIFT_EMIT_LOC_STRINGS = YES;
SWIFT_VERSION = 5.0;
TARGETED_DEVICE_FAMILY = "1,2";
@ -1433,16 +1270,19 @@
INFOPLIST_FILE = VernissageShare/Info.plist;
INFOPLIST_KEY_CFBundleDisplayName = VernissageShareExtension;
INFOPLIST_KEY_NSHumanReadableCopyright = "";
IPHONEOS_DEPLOYMENT_TARGET = 16.6;
IPHONEOS_DEPLOYMENT_TARGET = 17.0;
LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)",
"@executable_path/Frameworks",
"@executable_path/../../Frameworks",
);
MARKETING_VERSION = 1.13.0;
MARKETING_VERSION = 2.0.0;
PRODUCT_BUNDLE_IDENTIFIER = dev.mczachurski.vernissage.share;
PRODUCT_NAME = "$(TARGET_NAME)";
SKIP_INSTALL = YES;
SUPPORTED_PLATFORMS = "iphoneos iphonesimulator";
SUPPORTS_MACCATALYST = NO;
SUPPORTS_MAC_DESIGNED_FOR_IPHONE_IPAD = NO;
SWIFT_EMIT_LOC_STRINGS = YES;
SWIFT_VERSION = 5.0;
TARGETED_DEVICE_FAMILY = "1,2";
@ -1462,16 +1302,19 @@
INFOPLIST_FILE = VernissageShare/Info.plist;
INFOPLIST_KEY_CFBundleDisplayName = VernissageShareExtension;
INFOPLIST_KEY_NSHumanReadableCopyright = "";
IPHONEOS_DEPLOYMENT_TARGET = 16.6;
IPHONEOS_DEPLOYMENT_TARGET = 17.0;
LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)",
"@executable_path/Frameworks",
"@executable_path/../../Frameworks",
);
MARKETING_VERSION = 1.13.0;
MARKETING_VERSION = 2.0.0;
PRODUCT_BUNDLE_IDENTIFIER = dev.mczachurski.vernissage.share;
PRODUCT_NAME = "$(TARGET_NAME)";
SKIP_INSTALL = YES;
SUPPORTED_PLATFORMS = "iphoneos iphonesimulator";
SUPPORTS_MACCATALYST = NO;
SUPPORTS_MAC_DESIGNED_FOR_IPHONE_IPAD = NO;
SWIFT_EMIT_LOC_STRINGS = YES;
SWIFT_VERSION = 5.0;
TARGETED_DEVICE_FAMILY = "1,2";
@ -1531,7 +1374,8 @@
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
GCC_WARN_UNUSED_FUNCTION = YES;
GCC_WARN_UNUSED_VARIABLE = YES;
IPHONEOS_DEPLOYMENT_TARGET = 16.6;
IPHONEOS_DEPLOYMENT_TARGET = 17.0;
LOCALIZATION_PREFERS_STRING_CATALOGS = YES;
MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE;
MTL_FAST_MATH = YES;
ONLY_ACTIVE_ARCH = YES;
@ -1588,7 +1432,8 @@
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
GCC_WARN_UNUSED_FUNCTION = YES;
GCC_WARN_UNUSED_VARIABLE = YES;
IPHONEOS_DEPLOYMENT_TARGET = 16.6;
IPHONEOS_DEPLOYMENT_TARGET = 17.0;
LOCALIZATION_PREFERS_STRING_CATALOGS = YES;
MTL_ENABLE_DEBUG_INFO = NO;
MTL_FAST_MATH = YES;
SDKROOT = iphoneos;
@ -1624,12 +1469,12 @@
INFOPLIST_KEY_UILaunchScreen_Generation = YES;
INFOPLIST_KEY_UISupportedInterfaceOrientations = UIInterfaceOrientationPortrait;
INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown";
IPHONEOS_DEPLOYMENT_TARGET = 16.6;
IPHONEOS_DEPLOYMENT_TARGET = 17.0;
LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)",
"@executable_path/Frameworks",
);
MARKETING_VERSION = 1.13.0;
MARKETING_VERSION = 2.0.0;
PRODUCT_BUNDLE_IDENTIFIER = dev.mczachurski.vernissage;
PRODUCT_NAME = "$(TARGET_NAME)";
PROVISIONING_PROFILE_SPECIFIER = "";
@ -1667,12 +1512,12 @@
INFOPLIST_KEY_UILaunchScreen_Generation = YES;
INFOPLIST_KEY_UISupportedInterfaceOrientations = UIInterfaceOrientationPortrait;
INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown";
IPHONEOS_DEPLOYMENT_TARGET = 16.6;
IPHONEOS_DEPLOYMENT_TARGET = 17.0;
LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)",
"@executable_path/Frameworks",
);
MARKETING_VERSION = 1.13.0;
MARKETING_VERSION = 2.0.0;
PRODUCT_BUNDLE_IDENTIFIER = dev.mczachurski.vernissage;
PRODUCT_NAME = "$(TARGET_NAME)";
SUPPORTED_PLATFORMS = "iphoneos iphonesimulator";
@ -1845,38 +1690,6 @@
productName = Semaphore;
};
/* End XCSwiftPackageProductDependency section */
/* Begin XCVersionGroup section */
F88C2476295C37BB0006098B /* Vernissage.xcdatamodeld */ = {
isa = XCVersionGroup;
children = (
F87425F62AD5402700A119D7 /* Vernissage-019.xcdatamodel */,
F8E7ADF82AD44AFD0038FFFD /* Vernissage-018.xcdatamodel */,
F8D8E0D22ACC89CB00AA1374 /* Vernissage-017.xcdatamodel */,
F8BD04192ACC2280004B8E2C /* Vernissage-016.xcdatamodel */,
F880EECE2AC70A2B00C09C31 /* Vernissage-015.xcdatamodel */,
F8206A032A06547600E19412 /* Vernissage-014.xcdatamodel */,
F865B4D42A0252FB008ACDFC /* Vernissage-013.xcdatamodel */,
F8EF3C8B29FC3A5F00CBFF7C /* Vernissage-012.xcdatamodel */,
F871F21F29EF0FEC00A351EF /* Vernissage-011.xcdatamodel */,
F85B586C29ED169B00A16D12 /* Vernissage-010.xcdatamodel */,
F8FFBD4929E99BEE0047EE80 /* Vernissage-009.xcdatamodel */,
F8A4A88429E4099900267E36 /* Vernissage-008.xcdatamodel */,
F8911A1829DE9E5500770F44 /* Vernissage-007.xcdatamodel */,
F8EF371429C624DA00669F45 /* Vernissage-006.xcdatamodel */,
F8CAE64129B8F1AF001E0372 /* Vernissage-005.xcdatamodel */,
F8B05ACC29B48DD000857221 /* Vernissage-004.xcdatamodel */,
F8B05AC929B488C600857221 /* Vernissage-003.xcdatamodel */,
F89F0605299139F6003DC875 /* Vernissage-002.xcdatamodel */,
F8C937A929882CA90004D782 /* Vernissage-001.xcdatamodel */,
F88C2477295C37BB0006098B /* Vernissage.xcdatamodel */,
);
currentVersion = F87425F62AD5402700A119D7 /* Vernissage-019.xcdatamodel */;
path = Vernissage.xcdatamodeld;
sourceTree = "<group>";
versionGroupType = wrapper.xcdatamodel;
};
/* End XCVersionGroup section */
};
rootObject = F88C2460295C37B80006098B /* Project object */;
}

View File

@ -1,14 +1,5 @@
{
"pins" : [
{
"identity" : "activityindicatorview",
"kind" : "remoteSourceControl",
"location" : "https://github.com/exyte/ActivityIndicatorView.git",
"state" : {
"revision" : "9970fd0bb7a05dad0b6566ae1f56937716686b24",
"version" : "1.1.1"
}
},
{
"identity" : "brightfutures",
"kind" : "remoteSourceControl",

View File

@ -15,5 +15,5 @@ class AppDelegate: NSObject, UIApplicationDelegate {
let sceneConfig: UISceneConfiguration = UISceneConfiguration(name: nil, sessionRole: connectingSceneSession.role)
sceneConfig.delegateClass = SceneDelegate.self
return sceneConfig
}
}
}

View File

@ -8,8 +8,8 @@ import Foundation
import SwiftUI
import WidgetsKit
@MainActor
extension View {
func withAppRouteur() -> some View {
self.navigationDestination(for: RouteurDestinations.self) { destination in
switch destination {

View File

@ -10,14 +10,13 @@ import ServicesKit
import OSLog
import EnvironmentKit
@MainActor
final class TipsStore: ObservableObject {
@Observable final class TipsStore {
/// Products are registered in AppStore connect (and for development in InAppPurchaseStoreKitConfiguration.storekit file).
@Published private(set) var items = [Product]()
private(set) var items = [Product]()
/// Status of the purchase.
@Published private(set) var status: ActionStatus? {
private(set) var status: ActionStatus? {
didSet {
switch status {
case .failed:
@ -29,7 +28,7 @@ final class TipsStore: ObservableObject {
}
/// True when error during purchase occures.
@Published var hasError = false
var hasError = false
/// Error during purchase.
var error: PurchaseError? {
@ -62,7 +61,7 @@ final class TipsStore: ObservableObject {
try await self.handlePurchase(from: result)
} catch {
self.status = .failed(.system(error))
ErrorService.shared.handle(error, message: "Purchase failed.", showToastr: false)
ErrorService.shared.handle(error, message: "global.error.purchaseFailed", showToastr: false)
}
}
@ -109,7 +108,7 @@ final class TipsStore: ObservableObject {
}
} catch {
self?.status = .failed(.system(error))
ErrorService.shared.handle(error, message: "Cannot configure transaction listener.", showToastr: false)
ErrorService.shared.handle(error, message: "global.error.cannotConfigureTransactionListener", showToastr: false)
}
}
}
@ -123,7 +122,7 @@ final class TipsStore: ObservableObject {
self.items = products
} catch {
self.status = .failed(.system(error))
ErrorService.shared.handle(error, message: "Cannot download in-app products.", showToastr: false)
ErrorService.shared.handle(error, message: "global.error.cannotDownloadInAppProducts", showToastr: false)
}
}
}

View File

@ -2,10 +2,10 @@
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>ITSAppUsesNonExemptEncryption</key>
<false/>
<key>LSMinimumSystemVersion</key>
<string>14.0</string>
<key>BGTaskSchedulerPermittedIdentifiers</key>
<array>
<string>dev.mczachurski.Vernissage.NotificationFetcher</string>
</array>
<key>CFBundleURLTypes</key>
<array>
<dict>
@ -25,10 +25,18 @@
</array>
</dict>
</array>
<key>ITSAppUsesNonExemptEncryption</key>
<false/>
<key>LSMinimumSystemVersion</key>
<string>14.0</string>
<key>UIApplicationSceneManifest</key>
<dict>
<key>UISceneConfigurations</key>
<dict/>
</dict>
<key>UIBackgroundModes</key>
<array>
<string>fetch</string>
</array>
</dict>
</plist>

View File

@ -1,26 +0,0 @@
//
// https://mczachurski.dev
// Copyright © 2023 Marcin Czachurski and the repository contributors.
// Licensed under the Apache License 2.0.
//
import Foundation
import SwiftUI
class NavigationMenuItemDetails: ObservableObject, Identifiable {
@Published var title: LocalizedStringKey
@Published var image: String
@Published var viewMode: MainView.ViewMode {
didSet {
self.title = viewMode.title
self.image = viewMode.image
}
}
init(viewMode: MainView.ViewMode) {
self.viewMode = viewMode
self.title = viewMode.title
self.image = viewMode.image
}
}

View File

@ -6,13 +6,13 @@
import Foundation
public class PhotoUrl: ObservableObject, Identifiable {
@Observable public class PhotoUrl: Identifiable {
public var id: String
@Published public var statusId: String?
@Published public var url: URL?
@Published public var blurhash: String?
@Published public var sensitive = false
public var statusId: String?
public var url: URL?
public var blurhash: String?
public var sensitive = false
init(id: String) {
self.id = id

View File

@ -8,7 +8,7 @@ import SwiftUI
import Foundation
import PixelfedKit
public class RelationshipModel: ObservableObject {
@Observable public class RelationshipModel {
enum RelationshipAction {
case follow
case unfollow
@ -17,46 +17,46 @@ public class RelationshipModel: ObservableObject {
}
/// The account ID.
@Published public var id: EntityId
public var id: EntityId
/// Are you followed by this user?
@Published public var followedBy: Bool
public var followedBy: Bool
/// Is this user blocking you?
@Published public var blockedBy: Bool
public var blockedBy: Bool
/// Are you muting notifications from this user?
@Published public var mutingNotifications: Bool
public var mutingNotifications: Bool
/// Do you have a pending follow request for this user?
@Published public var requested: Bool
public var requested: Bool
/// Are you receiving this users boosts in your home timeline?
@Published public var showingReblogs: Bool
public var showingReblogs: Bool
/// Have you enabled notifications for this user?
@Published public var notifying: Bool
public var notifying: Bool
/// Are you blocking this users domain?
@Published public var domainBlocking: Bool
public var domainBlocking: Bool
/// Are you featuring this user on your profile?
@Published public var endorsed: Bool
public var endorsed: Bool
/// Which languages are you following from this user? Array of String (ISO 639-1 language two-letter code).
@Published public var languages: [String]?
public var languages: [String]?
/// This users profile bio.
@Published public var note: String?
public var note: String?
/// Are you following this user?
@Published public var following: Bool
public var following: Bool
/// Are you blocking this user?
@Published public var blocking: Bool
public var blocking: Bool
/// Are you muting this user?
@Published public var muting: Bool
public var muting: Bool
public init() {
self.id = ""

View File

@ -6,11 +6,12 @@
import Foundation
class SelectedMenuItemDetails: NavigationMenuItemDetails {
class SelectedMenuItemDetails: Identifiable {
public let position: Int
public var viewMode: MainView.ViewMode
init(position: Int, viewMode: MainView.ViewMode) {
self.position = position
super.init(viewMode: viewMode)
self.viewMode = viewMode
}
}

View File

@ -33,7 +33,7 @@ struct AnimatePlaceholderModifier: AnimatableModifier {
guard isLoading else { return }
isAnim.toggle()
}
.onChange(of: isLoading) { _ in
.onChange(of: isLoading) {
isAnim.toggle()
}
}

View File

@ -8,6 +8,7 @@ import SwiftUI
import PixelfedKit
import OAuthSwift
import EnvironmentKit
import BackgroundTasks
class SceneDelegate: NSObject, UISceneDelegate {
func scene(_ scene: UIScene, openURLContexts URLContexts: Set<UIOpenURLContext>) {

View File

@ -33,7 +33,7 @@ public class AppMetadataService {
self.memoryCacheData[metadataCacheKey] = metadata
return metadata
} catch {
ErrorService.shared.handle(error, message: "Error during downloading metadata.")
ErrorService.shared.handle(error, message: "global.error.errorDuringDownloadingMetadata")
return AppMetadata()
}
}

View File

@ -7,18 +7,19 @@
import Foundation
import PixelfedKit
import ClientKit
import CoreData
import SwiftData
import AuthenticationServices
import ServicesKit
import EnvironmentKit
/// Srvice responsible for login user into the Pixelfed account.
@MainActor
public class AuthorizationService {
public static let shared = AuthorizationService()
private init() { }
/// Access token verification.
public func verifyAccount(session: AuthorizationSession, accountModel: AccountModel, _ result: @escaping (AccountModel?) -> Void) async {
public func verifyAccount(session: AuthorizationSession, accountModel: AccountModel, modelContext: ModelContext, _ result: @escaping (AccountModel?) -> Void) async {
// When we dont have even one account stored in database then we have to ask user to enter server and sign in.
guard let accessToken = accountModel.accessToken else {
result(nil)
@ -33,15 +34,18 @@ public class AuthorizationService {
let signedInAccountModel = await self.update(accountId: accountModel.id,
basedOn: account,
accessToken: accessToken,
refreshToken: accountModel.refreshToken)
refreshToken: accountModel.refreshToken,
modelContext: modelContext)
result(signedInAccountModel)
} catch {
do {
let signedInAccountModel = try await self.refreshCredentials(for: accountModel, presentationContextProvider: session)
let signedInAccountModel = try await self.refreshCredentials(for: accountModel,
presentationContextProvider: session,
modelContext: modelContext)
result(signedInAccountModel)
} catch {
ErrorService.shared.handle(error, message: "Issues during refreshing credentials.")
ErrorService.shared.handle(error, message: "global.error.refreshingCredentialsTitle")
ToastrService.shared.showError(title: "global.error.refreshingCredentialsTitle",
subtitle: NSLocalizedString("global.error.refreshingCredentialsSubtitle", comment: ""))
result(nil)
@ -50,7 +54,10 @@ public class AuthorizationService {
}
/// Sign in to the Pixelfed server.
public func sign(in serverAddress: String, session: AuthorizationSession, _ result: @escaping (AccountModel) -> Void) async throws {
public func sign(in serverAddress: String,
session:AuthorizationSession,
modelContext: ModelContext,
_ result: @escaping (AccountModel) -> Void) async throws {
guard let baseUrl = URL(string: serverAddress) else {
throw AuthorisationError.badServerUrl
@ -82,8 +89,7 @@ public class AuthorizationService {
let account = try await authenticatedClient.verifyCredentials()
// Get/create account object in database.
let backgroundContext = CoreDataHandler.shared.newBackgroundContext()
let accountData = self.getAccountData(account: account, backgroundContext: backgroundContext)
let accountData = self.getAccountData(account: account, serverUrl: baseUrl, modelContext: modelContext)
accountData.id = account.id
accountData.username = account.username
@ -115,39 +121,39 @@ public class AuthorizationService {
let avatarData = try await RemoteFileService.shared.fetchData(url: avatarUrl)
accountData.avatarData = avatarData
} catch {
ErrorService.shared.handle(error, message: "Avatar has not been downloaded.")
ErrorService.shared.handle(error, message: "global.error.avatarHasNotBeenDownloaded")
}
}
// Set newly created account as current (only when we create a first account).
let defaultSettings = ApplicationSettingsHandler.shared.get(viewContext: backgroundContext)
let defaultSettings = ApplicationSettingsHandler.shared.get(modelContext: modelContext)
if defaultSettings.currentAccount == nil {
defaultSettings.currentAccount = accountData.id
}
// Save account/settings data in database.
CoreDataHandler.shared.save(viewContext: backgroundContext)
try modelContext.save()
// Return account data.
let accountModel = accountData.toAccountModel()
result(accountModel)
}
public func refreshAccessTokens() async {
let accounts = AccountDataHandler.shared.getAccountsData()
public func refreshAccessTokens(modelContext: ModelContext) async {
let accounts = AccountDataHandler.shared.getAccountsData(modelContext: modelContext)
await withTaskGroup(of: Void.self) { group in
for account in accounts {
group.addTask {
group.addTask { @MainActor in
do {
_ = try await self.refreshAccessToken(accountData: account)
_ = try await self.refreshAccessToken(accountData: account, modelContext: modelContext)
#if DEBUG
ToastrService.shared.showSuccess("New access tokens has been retrieved.", imageSystemName: "key.fill")
ToastrService.shared.showSuccess("global.title.newAccessTokenRetrieved", imageSystemName: "key.fill")
#endif
} catch {
#if DEBUG
ErrorService.shared.handle(error, message: "Refresh token failed: '\(account.acct)'.", showToastr: true)
ErrorService.shared.handle(error, message: "global.error.refreshTokenFailed", showToastr: true)
#else
ErrorService.shared.handle(error, message: "Error during refreshing access token for account '\(account.acct)'.")
#endif
@ -157,7 +163,7 @@ public class AuthorizationService {
}
}
private func refreshAccessToken(accountData: AccountData) async throws -> AccountModel? {
private func refreshAccessToken(accountData: AccountData, modelContext: ModelContext) async throws -> AccountModel? {
let client = PixelfedClient(baseURL: accountData.serverUrl)
guard let refreshToken = accountData.refreshToken else {
@ -177,11 +183,13 @@ public class AuthorizationService {
return await self.update(accountId: accountData.id,
basedOn: account,
accessToken: oAuthSwiftCredential.oauthToken,
refreshToken: oAuthSwiftCredential.oauthRefreshToken)
refreshToken: oAuthSwiftCredential.oauthRefreshToken,
modelContext: modelContext)
}
private func refreshCredentials(for accountModel: AccountModel,
presentationContextProvider: ASWebAuthenticationPresentationContextProviding
presentationContextProvider: ASWebAuthenticationPresentationContextProviding,
modelContext: ModelContext
) async throws -> AccountModel? {
let client = PixelfedClient(baseURL: accountModel.serverUrl)
@ -206,16 +214,17 @@ public class AuthorizationService {
return await self.update(accountId: accountModel.id,
basedOn: account,
accessToken: oAuthSwiftCredential.oauthToken,
refreshToken: oAuthSwiftCredential.oauthRefreshToken)
refreshToken: oAuthSwiftCredential.oauthRefreshToken,
modelContext: modelContext)
}
private func update(accountId: String,
basedOn account: Account,
accessToken: String,
refreshToken: String?
refreshToken: String?,
modelContext: ModelContext
) async -> AccountModel? {
let backgroundContext = CoreDataHandler.shared.newBackgroundContext()
guard let dbAccount = AccountDataHandler.shared.getAccountData(accountId: accountId, viewContext: backgroundContext) else {
guard let dbAccount = AccountDataHandler.shared.getAccountData(accountId: accountId, modelContext: modelContext) else {
return nil
}
@ -242,21 +251,24 @@ public class AuthorizationService {
let avatarData = try await RemoteFileService.shared.fetchData(url: avatarUrl)
dbAccount.avatarData = avatarData
} catch {
ErrorService.shared.handle(error, message: "Avatar has not been downloaded.")
ErrorService.shared.handle(error, message: "global.error.avatarHasNotBeenDownloaded")
}
}
// Save account data in database and in application state.
CoreDataHandler.shared.save(viewContext: backgroundContext)
try? modelContext.save()
return dbAccount.toAccountModel()
}
private func getAccountData(account: Account, backgroundContext: NSManagedObjectContext) -> AccountData {
if let accountFromDb = AccountDataHandler.shared.getAccountData(accountId: account.id, viewContext: backgroundContext) {
private func getAccountData(account: Account, serverUrl: URL, modelContext: ModelContext) -> AccountData {
if let accountFromDb = AccountDataHandler.shared.getAccountData(accountId: account.id, modelContext: modelContext) {
return accountFromDb
}
return AccountDataHandler.shared.createAccountDataEntity(viewContext: backgroundContext)
let accountData = AccountData(serverUrl: serverUrl)
modelContext.insert(accountData)
return accountData
}
}

View File

@ -5,7 +5,7 @@
//
import Foundation
import CoreData
import SwiftData
import PixelfedKit
import ClientKit
import ServicesKit
@ -15,427 +15,119 @@ import EnvironmentKit
import Semaphore
/// Service responsible for managing home timeline.
@MainActor
public class HomeTimelineService {
public static let shared = HomeTimelineService()
private init() { }
private let defaultAmountOfDownloadedStatuses = 40
private let maximumAmountOfDownloadedStatuses = 80
private let imagePrefetcher = ImagePrefetcher(destination: .diskCache)
private let semaphore = AsyncSemaphore(value: 1)
@MainActor
public func loadOnBottom(for account: AccountModel, includeReblogs: Bool, hideStatusesWithoutAlt: Bool) async throws -> Int {
// Load data from API and operate on CoreData on background context.
let backgroundContext = CoreDataHandler.shared.newBackgroundContext()
// Get minimum downloaded stauts id.
let oldestStatus = StatusDataHandler.shared.getMinimumStatus(accountId: account.id, viewContext: backgroundContext)
guard let oldestStatus = oldestStatus else {
return 0
}
// Load data on bottom of the list.
let allStatusesFromApi = try await self.load(for: account,
includeReblogs: includeReblogs,
hideStatusesWithoutAlt: hideStatusesWithoutAlt,
on: backgroundContext,
maxId: oldestStatus.id)
// Save data into database.
CoreDataHandler.shared.save(viewContext: backgroundContext)
// Start prefetching images.
self.prefetch(statuses: allStatusesFromApi)
// Return amount of newly downloaded statuses.
return allStatusesFromApi.count
}
@MainActor
public func refreshTimeline(for account: AccountModel, includeReblogs: Bool, hideStatusesWithoutAlt: Bool, updateLastSeenStatus: Bool = false) async throws -> String? {
await semaphore.wait()
defer { semaphore.signal() }
// Load data from API and operate on CoreData on background context.
let backgroundContext = CoreDataHandler.shared.newBackgroundContext()
// Retrieve newest visible status (last visible by user).
let dbNewestStatus = StatusDataHandler.shared.getMaximumStatus(accountId: account.id, viewContext: backgroundContext)
let lastSeenStatusId = dbNewestStatus?.id
// Refresh/load home timeline (refreshing on top downloads always first 40 items).
// When Apple introduce good way to show new items without scroll to top then we can change that method.
let allStatusesFromApi = try await self.refresh(for: account,
includeReblogs: includeReblogs,
hideStatusesWithoutAlt: hideStatusesWithoutAlt,
on: backgroundContext)
// Update last seen status.
if let lastSeenStatusId, updateLastSeenStatus == true {
try self.update(lastSeenStatusId: lastSeenStatusId, for: account, on: backgroundContext)
}
// Delete old viewed statuses from database.
ViewedStatusHandler.shared.deleteOldViewedStatuses(viewContext: backgroundContext)
// Start prefetching images.
self.prefetch(statuses: allStatusesFromApi)
// Save data into database.
CoreDataHandler.shared.save(viewContext: backgroundContext)
// Return id of last seen status.
return lastSeenStatusId
}
@MainActor
public func update(attachment: AttachmentData, withData imageData: Data, imageWidth: Double, imageHeight: Double) {
attachment.data = imageData
attachment.metaImageWidth = Int32(imageWidth)
attachment.metaImageHeight = Int32(imageHeight)
// TODO: Uncomment/remove when exif metadata will be supported.
// self.setExifProperties(in: attachment, from: imageData)
// Save data into database.
CoreDataHandler.shared.save()
}
public func amountOfNewStatuses(for account: AccountModel, includeReblogs: Bool, hideStatusesWithoutAlt: Bool) async -> Int {
public func amountOfNewStatuses(includeReblogs: Bool, hideStatusesWithoutAlt: Bool, modelContext: ModelContext) async -> Int {
await semaphore.wait()
defer { semaphore.signal() }
guard let accessToken = account.accessToken else {
guard let accountData = AccountDataHandler.shared.getCurrentAccountData(modelContext: modelContext) else {
return 0
}
// Load data from API and operate on CoreData on background context.
let backgroundContext = CoreDataHandler.shared.newBackgroundContext()
guard let accessToken = accountData.accessToken else {
return 0
}
// Get maximimum downloaded stauts id.
let newestStatus = StatusDataHandler.shared.getMaximumStatus(accountId: account.id, viewContext: backgroundContext)
guard let newestStatus else {
guard let lastSeenStatusId = self.getLastLoadedStatusId(accountId: accountData.id, modelContext: modelContext) else {
return 0
}
let client = PixelfedClient(baseURL: account.serverUrl).getAuthenticated(token: accessToken)
let client = PixelfedClient(baseURL: accountData.serverUrl).getAuthenticated(token: accessToken)
var statuses: [Status] = []
var newestStatusId = newestStatus.id
var newestStatusId = lastSeenStatusId
// There can be more then 80 newest statuses, that's why we have to sometimes send more then one request.
while true {
do {
let downloadedStatuses = try await client.getHomeTimeline(minId: newestStatusId,
limit: self.maximumAmountOfDownloadedStatuses,
includeReblogs: includeReblogs)
guard let firstStatus = downloadedStatuses.first else {
break
}
let visibleStatuses = self.getVisibleStatuses(accountId: accountData.id,
statuses: downloadedStatuses,
hideStatusesWithoutAlt: hideStatusesWithoutAlt,
modelContext: modelContext)
// We have to include in the counter only statuses with images.
let statusesWithImagesOnly = downloadedStatuses.getStatusesWithImagesOnly()
for status in statusesWithImagesOnly {
// We have to hide statuses without ALT text.
if hideStatusesWithoutAlt && status.statusContainsAltText() == false {
continue
}
// We shouldn't add statuses that are boosted by muted accounts.
if AccountRelationshipHandler.shared.isBoostedStatusesMuted(accountId: account.id, status: status, viewContext: backgroundContext) {
continue
}
// We should add to timeline only statuses that has not been showned to the user already.
guard self.hasBeenAlreadyOnTimeline(accountId: account.id, status: status, on: backgroundContext) == false else {
continue
}
// Same rebloged status has been already visible in current portion of data.
if let reblog = status.reblog, statuses.contains(where: { $0.reblog?.id == reblog.id }) {
continue
}
// Same status has been already visible in current portion of data.
if let reblog = status.reblog, statusesWithImagesOnly.contains(where: { $0.id == reblog.id }) {
continue
}
statuses.append(status)
}
statuses.append(contentsOf: visibleStatuses)
newestStatusId = firstStatus.id
} catch {
ErrorService.shared.handle(error, message: "Error during downloading new statuses for amount of new statuses.")
ErrorService.shared.handle(error, message: "global.error.errorDuringDownloadingNewStatuses")
break
}
}
// Start prefetching images.
self.prefetch(statuses: statuses)
// Return number of new statuses not visible yet on the timeline.
return statuses.count
}
private func update(lastSeenStatusId: String, for account: AccountModel, on backgroundContext: NSManagedObjectContext) throws {
// Save information about last seen status.
guard let accountDataFromDb = AccountDataHandler.shared.getAccountData(accountId: account.id, viewContext: backgroundContext) else {
throw DatabaseError.cannotDownloadAccount
public func getVisibleStatuses(accountId: String, statuses: [Status], hideStatusesWithoutAlt: Bool, modelContext: ModelContext) -> [Status] {
// We have to include in the counter only statuses with images.
let statusesWithImagesOnly = statuses.getStatusesWithImagesOnly()
var visibleStatuses: [Status] = []
for status in statusesWithImagesOnly {
// We have to hide statuses without ALT text.
if hideStatusesWithoutAlt && status.statusContainsAltText() == false {
continue
}
// We shouldn't add statuses that are boosted by muted accounts.
if AccountRelationshipHandler.shared.isBoostedStatusesMuted(accountId: accountId, status: status, modelContext: modelContext) {
continue
}
// We should add to timeline only statuses that has not been showned to the user already.
guard self.hasBeenAlreadyOnTimeline(accountId: accountId, status: status, modelContext: modelContext) == false else {
continue
}
// Same rebloged status has been already visible in already processed (visible) portion of data.
if let reblog = status.reblog, visibleStatuses.contains(where: { $0.reblog?.id == reblog.id || $0.id == reblog.id }) {
continue
}
// Same rebloged (orginal) status will be added to visible in same portion of data.
if let reblog = status.reblog, statusesWithImagesOnly.contains(where: { $0.id == reblog.id }) {
continue
}
visibleStatuses.append(status)
}
accountDataFromDb.lastSeenStatusId = lastSeenStatusId
return visibleStatuses
}
private func hasBeenAlreadyOnTimeline(accountId: String, status: Status, modelContext: ModelContext) -> Bool {
return ViewedStatusHandler.shared.hasBeenAlreadyOnTimeline(accountId: accountId, status: status, modelContext: modelContext)
}
private func getLastLoadedStatusId(accountId: String, modelContext: ModelContext) -> String? {
let accountData = AccountDataHandler.shared.getAccountData(accountId: accountId, modelContext: modelContext)
return accountData?.lastLoadedStatusId
}
private func update(status statusData: StatusData, basedOn status: Status, for account: AccountModel) async throws -> StatusData? {
// Load data from API and operate on CoreData on background context.
let backgroundContext = CoreDataHandler.shared.newBackgroundContext()
// Update status data in database.
self.copy(from: status, to: statusData, on: backgroundContext)
// Save data into database.
CoreDataHandler.shared.save(viewContext: backgroundContext)
return statusData
}
private func refresh(for account: AccountModel, includeReblogs: Bool, hideStatusesWithoutAlt: Bool, on backgroundContext: NSManagedObjectContext) async throws -> [Status] {
// Retrieve statuses from API.
let statuses = try await self.getUniqueStatusesForHomeTimeline(account: account,
includeReblogs: includeReblogs,
hideStatusesWithoutAlt: hideStatusesWithoutAlt,
on: backgroundContext)
// Update all existing statuses in database.
for status in statuses {
if let dbStatus = StatusDataHandler.shared.getStatusData(accountId: account.id, statusId: status.id, viewContext: backgroundContext) {
dbStatus.updateFrom(status)
}
}
// Add statuses which are not existing in database, but has been downloaded via API.
var statusesToAdd: [Status] = []
for status in statuses where StatusDataHandler.shared.getStatusData(accountId: account.id,
statusId: status.id,
viewContext: backgroundContext) == nil {
statusesToAdd.append(status)
}
// Collection with statuses to remove from database.
var dbStatusesToRemove: [StatusData] = []
let allDbStatuses = StatusDataHandler.shared.getAllStatuses(accountId: account.id, viewContext: backgroundContext)
// Find statuses to delete (not exiting in the API results).
for dbStatus in allDbStatuses where !statuses.contains(where: { status in status.id == dbStatus.id }) {
dbStatusesToRemove.append(dbStatus)
}
// Find statuses to delete (duplicates).
var existingStatusIds: [String] = []
for dbStatus in allDbStatuses {
if existingStatusIds.contains(where: { $0 == dbStatus.id }) {
dbStatusesToRemove.append(dbStatus)
} else {
existingStatusIds.append(dbStatus.id)
}
}
// Delete statuses from database.
if !dbStatusesToRemove.isEmpty {
for dbStatusToRemove in dbStatusesToRemove {
backgroundContext.delete(dbStatusToRemove)
}
}
// Save statuses in database.
if !statusesToAdd.isEmpty {
_ = try await self.add(statusesToAdd, for: account, on: backgroundContext)
}
// Return all statuses downloaded from API.
return statuses
}
private func load(for account: AccountModel,
includeReblogs: Bool,
hideStatusesWithoutAlt: Bool,
on backgroundContext: NSManagedObjectContext,
maxId: String? = nil
) async throws -> [Status] {
// Retrieve statuses from API.
let statuses = try await self.getUniqueStatusesForHomeTimeline(account: account,
maxId: maxId,
includeReblogs: includeReblogs,
hideStatusesWithoutAlt: hideStatusesWithoutAlt,
on: backgroundContext)
// Save statuses in database.
try await self.add(statuses, for: account, on: backgroundContext)
// Return all statuses downloaded from API.
return statuses
}
private func add(_ statuses: [Status],
for account: AccountModel,
on backgroundContext: NSManagedObjectContext
) async throws {
guard let accountDataFromDb = AccountDataHandler.shared.getAccountData(accountId: account.id, viewContext: backgroundContext) else {
throw DatabaseError.cannotDownloadAccount
}
// Proceed statuses with images only.
let statusesWithImages = statuses.getStatusesWithImagesOnly()
// Save all data to database.
for status in statusesWithImages {
// Save status to database.
let statusData = StatusDataHandler.shared.createStatusDataEntity(viewContext: backgroundContext)
self.copy(from: status, to: statusData, on: backgroundContext)
statusData.pixelfedAccount = accountDataFromDb
accountDataFromDb.addToStatuses(statusData)
// Save statusId to viewed statuses.
let viewedStatus = ViewedStatusHandler.shared.createViewedStatusEntity(viewContext: backgroundContext)
viewedStatus.id = status.id
viewedStatus.reblogId = status.reblog?.id
viewedStatus.date = Date()
viewedStatus.pixelfedAccount = accountDataFromDb
accountDataFromDb.addToViewedStatuses(viewedStatus)
}
}
private func copy(from status: Status,
to statusData: StatusData,
on backgroundContext: NSManagedObjectContext
) {
statusData.copyFrom(status)
for (index, attachment) in status.getAllImageMediaAttachments().enumerated() {
// Save attachment in database.
let attachmentData = statusData.attachments().first { item in item.id == attachment.id }
?? AttachmentDataHandler.shared.createAttachmnentDataEntity(viewContext: backgroundContext)
attachmentData.copyFrom(attachment)
attachmentData.statusId = statusData.id
attachmentData.order = Int32(index)
if attachmentData.isInserted {
attachmentData.statusRelation = statusData
statusData.addToAttachmentsRelation(attachmentData)
}
}
}
private func setExifProperties(in attachmentData: AttachmentData, from imageData: Data) {
// Read exif information.
if let exifProperties = imageData.getExifData() {
if let make = exifProperties.getExifValue("Make"), let model = exifProperties.getExifValue("Model") {
attachmentData.exifCamera = "\(make) \(model)"
}
// "Lens" or "Lens Model"
if let lens = exifProperties.getExifValue("Lens") {
attachmentData.exifLens = lens
}
if let createData = exifProperties.getExifValue("CreateDate") {
attachmentData.exifCreatedDate = createData
}
if let focalLenIn35mmFilm = exifProperties.getExifValue("FocalLenIn35mmFilm"),
let fNumber = exifProperties.getExifValue("FNumber")?.calculateExifNumber(),
let exposureTime = exifProperties.getExifValue("ExposureTime"),
let photographicSensitivity = exifProperties.getExifValue("PhotographicSensitivity") {
attachmentData.exifExposure = "\(focalLenIn35mmFilm)mm, f/\(fNumber), \(exposureTime)s, ISO \(photographicSensitivity)"
}
}
}
private func prefetch(statuses: [Status]) {
let statusModels = statuses.toStatusModels()
imagePrefetcher.startPrefetching(with: statusModels.getAllImagesUrls())
}
private func hasBeenAlreadyOnTimeline(accountId: String, status: Status, on backgroundContext: NSManagedObjectContext) -> Bool {
return ViewedStatusHandler.shared.hasBeenAlreadyOnTimeline(accountId: accountId, status: status, viewContext: backgroundContext)
}
private func getUniqueStatusesForHomeTimeline(account: AccountModel,
maxId: EntityId? = nil,
includeReblogs: Bool? = nil,
hideStatusesWithoutAlt: Bool = false,
on backgroundContext: NSManagedObjectContext) async throws -> [Status] {
guard let accessToken = account.accessToken else {
return []
}
let client = PixelfedClient(baseURL: account.serverUrl).getAuthenticated(token: accessToken)
var lastStatusId = maxId
var statuses: [Status] = []
while true {
let downloadedStatuses = try await client.getHomeTimeline(maxId: lastStatusId,
limit: self.maximumAmountOfDownloadedStatuses,
includeReblogs: includeReblogs)
// When there is not any older statuses we have to finish.
guard let lastStatus = downloadedStatuses.last else {
break
}
// We have to include in the counter only statuses with images.
let statusesWithImagesOnly = downloadedStatuses.getStatusesWithImagesOnly()
for status in statusesWithImagesOnly {
// When we process default amount of statuses to show we can stop adding another ones to the list.
if statuses.count == self.defaultAmountOfDownloadedStatuses {
break
}
// We have to hide statuses without ALT text.
if hideStatusesWithoutAlt && status.statusContainsAltText() == false {
continue
}
// We shouldn't add statuses that are boosted by muted accounts.
if AccountRelationshipHandler.shared.isBoostedStatusesMuted(accountId: account.id, status: status, viewContext: backgroundContext) {
continue
}
// We should add to timeline only statuses that has not been showned to the user already.
guard self.hasBeenAlreadyOnTimeline(accountId: account.id, status: status, on: backgroundContext) == false else {
continue
}
// Same rebloged status has been already visible in current portion of data.
if let reblog = status.reblog, statuses.contains(where: { $0.reblog?.id == reblog.id }) {
continue
}
// Same status has been already visible in current portion of data.
if let reblog = status.reblog, statusesWithImagesOnly.contains(where: { $0.id == reblog.id }) {
continue
}
statuses.append(status)
}
if statuses.count >= self.defaultAmountOfDownloadedStatuses {
break
}
lastStatusId = lastStatus.id
}
return statuses
}
}

View File

@ -0,0 +1,141 @@
//
// https://mczachurski.dev
// Copyright © 2023 Marcin Czachurski and the repository contributors.
// Licensed under the Apache License 2.0.
//
import Foundation
import SwiftData
import PixelfedKit
import ClientKit
import ServicesKit
import Nuke
import OSLog
import EnvironmentKit
import Semaphore
import UserNotifications
/// Service responsible for managing notifications.
@MainActor
public class NotificationsService {
public static let shared = NotificationsService()
private init() { }
private let semaphore = AsyncSemaphore(value: 1)
public func newNotificationsHasBeenAdded(for account: AccountModel, modelContext: ModelContext) async -> Bool {
await semaphore.wait()
defer { semaphore.signal() }
guard let accessToken = account.accessToken else {
return false
}
// Get maximimum downloaded stauts id.
guard let lastSeenNotificationId = self.getLastSeenNotificationId(accountId: account.id, modelContext: modelContext) else {
return false
}
let client = PixelfedClient(baseURL: account.serverUrl).getAuthenticated(token: accessToken)
do {
let linkableNotifications = try await client.notifications(minId: lastSeenNotificationId, limit: 5)
return linkableNotifications.data.first(where: { $0.id != lastSeenNotificationId }) != nil
} catch {
ErrorService.shared.handle(error, message: "notifications.error.loadingNotificationsFailed")
return false
}
}
public func amountOfNewNotifications(modelContext: ModelContext) async -> Int {
await semaphore.wait()
defer { semaphore.signal() }
guard let accountData = AccountDataHandler.shared.getCurrentAccountData(modelContext: modelContext) else {
return 0
}
guard let accessToken = accountData.accessToken else {
return 0
}
// Get maximimum downloaded stauts id.
guard let lastSeenNotificationId = self.getLastSeenNotificationId(accountId: accountData.id, modelContext: modelContext) else {
return 0
}
let client = PixelfedClient(baseURL: accountData.serverUrl).getAuthenticated(token: accessToken)
var notifications: [PixelfedKit.Notification] = []
var maxId: String? = nil
var allItemsProcessed = false
// There can be more then 40 newest notifications, that's why we have to sometimes send more then one request.
while true {
do {
let linkable = try await client.notifications(maxId: maxId, limit: 40)
guard linkable.data.first != nil else {
break
}
for item in linkable.data {
// We have to stop when we go to already seen notification.
if item.id == lastSeenNotificationId {
allItemsProcessed = true
break
}
notifications.append(item)
// We have to stop when we already count 99 notifications (it's maximum number which we want to show in the badge).
if notifications.count == 99 {
allItemsProcessed = true
break
}
}
if allItemsProcessed {
break
}
guard let linkedMaxId = linkable.link?.maxId else {
break
}
maxId = linkedMaxId
} catch {
ErrorService.shared.handle(error, message: "global.error.errorDuringDownloadingNewStatuses")
break
}
}
// Return number of new notifications not visible yet on the timeline.
return notifications.count
}
/// Function sets application badge counts when notifications (and badge) are enabled.
public func setBadgeCount(_ count: Int, modelContext: ModelContext) async throws {
// Badge have to enabled in system settings.
let applicationSettings = ApplicationSettingsHandler.shared.get(modelContext: modelContext)
guard applicationSettings.showApplicationBadge else {
return
}
// Notifications have to be enabled.
let center = UNUserNotificationCenter.current()
let settings = await center.notificationSettings()
guard (settings.authorizationStatus == .authorized) || (settings.authorizationStatus == .provisional) else {
return
}
// Badge notification have to be enabled.
if settings.badgeSetting == .enabled {
try await center.setBadgeCount(count)
}
}
private func getLastSeenNotificationId(accountId: String, modelContext: ModelContext) -> String? {
let accountData = AccountDataHandler.shared.getAccountData(accountId: accountId, modelContext: modelContext)
return accountData?.lastSeenNotificationId
}
}

View File

@ -69,13 +69,13 @@ enum AlertDestinations: Identifiable {
}
@MainActor
class RouterPath: ObservableObject {
@Observable class RouterPath {
public var urlHandler: ((URL) -> OpenURLAction.Result)?
@Published public var path: [RouteurDestinations] = []
@Published public var presentedSheet: SheetDestinations?
@Published public var presentedOverlay: OverlayDestinations?
@Published public var presentedAlert: AlertDestinations?
public var path: [RouteurDestinations] = []
public var presentedSheet: SheetDestinations?
public var presentedOverlay: OverlayDestinations?
public var presentedAlert: AlertDestinations?
public init() {}

View File

@ -10,27 +10,31 @@ import NukeUI
import ClientKit
import EnvironmentKit
import WidgetKit
import SwiftData
import TipKit
import OSLog
import BackgroundTasks
@main
struct VernissageApp: App {
@UIApplicationDelegateAdaptor(AppDelegate.self) var appDelegate
@Environment(\.scenePhase) private var phase
let coreDataHandler = CoreDataHandler.shared
@StateObject var applicationState = ApplicationState.shared
@StateObject var client = Client.shared
@StateObject var routerPath = RouterPath()
@StateObject var tipsStore = TipsStore()
@State var applicationState = ApplicationState.shared
@State var client = Client.shared
@State var routerPath = RouterPath()
@State var tipsStore = TipsStore()
@State var applicationViewMode: ApplicationViewMode = .loading
@State var tintColor = ApplicationState.shared.tintColor.color()
@State var theme = ApplicationState.shared.theme.colorScheme()
let modelContainer = SwiftDataHandler.shared.sharedModelContainer
let timer = Timer.publish(every: 120, on: .main, in: .common).autoconnect()
var body: some Scene {
WindowGroup {
NavigationStack(path: $routerPath.path) {
NavigationStack {
switch applicationViewMode {
case .loading:
LoadingView()
@ -50,58 +54,66 @@ struct VernissageApp: App {
.withAlertDestinations(alertDestinations: $routerPath.presentedAlert)
}
}
.environment(\.managedObjectContext, coreDataHandler.container.viewContext)
.environmentObject(applicationState)
.environmentObject(client)
.environmentObject(routerPath)
.environmentObject(tipsStore)
.modelContainer(modelContainer)
.environment(applicationState)
.environment(client)
.environment(routerPath)
.environment(tipsStore)
.tint(self.tintColor)
.preferredColorScheme(self.theme)
.task {
await self.onApplicationStart()
}
.navigationViewStyle(.stack)
.onReceive(NotificationCenter.default.publisher(for: UIApplication.didBecomeActiveNotification)) { _ in
DispatchQueue.main.asyncAfter(deadline: .now() + 5) {
Task {
// Refresh indicator of new photos when application is become active.
await self.calculateNewPhotosInBackground()
}
}
// Reload widget content when application become active.
WidgetCenter.shared.reloadAllTimelines()
}
.onReceive(timer) { _ in
Task {
// Refresh indicator of new photos each two minutes (when application is in the foreground)..
await self.calculateNewPhotosInBackground()
// Refresh indicator of new photos and new notifications each two minutes (when application is in the foreground)..
_ = await (self.calculateNewPhotosInBackground(), self.calculateNewNotificationsInBackground())
}
}
.onChange(of: applicationState.theme) { newValue in
.onChange(of: applicationState.theme) { oldValue, newValue in
self.theme = newValue.colorScheme()
}
.onChange(of: applicationState.tintColor) { newValue in
.onChange(of: applicationState.tintColor) { oldValue, newValue in
self.tintColor = newValue.color()
}
.onChange(of: applicationState.account) { newValue in
.onChange(of: applicationState.account) { oldValue, newValue in
if newValue == nil {
self.applicationViewMode = .signIn
}
}
.onChange(of: applicationState.showStatusId) { newValue in
.onChange(of: applicationState.showStatusId) { oldValue, newValue in
if let statusId = newValue {
self.routerPath.navigate(to: .status(id: statusId))
self.applicationState.showStatusId = nil
}
}
.onChange(of: applicationState.showAccountId) { newValue in
.onChange(of: applicationState.showAccountId) { oldValue, newValue in
if let accountId = newValue {
self.routerPath.navigate(to: .userProfile(accountId: accountId, accountDisplayName: nil, accountUserName: ""))
self.applicationState.showAccountId = nil
}
}
}
.onChange(of: phase) { oldValue, newValue in
switch newValue {
case .background:
scheduleAppRefresh()
case .active:
DispatchQueue.main.asyncAfter(deadline: .now() + 5) {
Task {
// Refresh indicator of new photos and new statuses when application is become active.
_ = await (self.calculateNewPhotosInBackground(), self.calculateNewNotificationsInBackground())
}
}
// Reload widget content when application become active.
WidgetCenter.shared.reloadAllTimelines()
default: break
}
}
.backgroundTask(.appRefresh(AppConstants.backgroundFetcherName)) {
await self.setBadgeCount()
}
}
@MainActor
@ -109,6 +121,9 @@ struct VernissageApp: App {
UIPageControl.appearance().currentPageIndicatorTintColor = UIColor.white.withAlphaComponent(0.7)
UIPageControl.appearance().pageIndicatorTintColor = UIColor.white.withAlphaComponent(0.4)
// Configure TipKit.
try? Tips.configure([.displayFrequency(.daily), .datastoreLocation(.applicationDefault)])
// Set custom configurations for Nuke image/data loaders.
self.setImagePipelines()
@ -119,17 +134,20 @@ struct VernissageApp: App {
await self.refreshAccessTokens()
// When user doesn't exists then we have to open sign in view.
guard let currentAccount = AccountDataHandler.shared.getCurrentAccountData() else {
let modelContext = self.modelContainer.mainContext
guard let currentAccount = AccountDataHandler.shared.getCurrentAccountData(modelContext: modelContext) else {
self.applicationViewMode = .signIn
return
}
// Create model based on core data entity.
let accountModel = currentAccount.toAccountModel()
// Verify access token correctness.
let authorizationSession = AuthorizationSession()
await AuthorizationService.shared.verifyAccount(session: authorizationSession, accountModel: accountModel) { signedInAccountModel in
await AuthorizationService.shared.verifyAccount(session: authorizationSession,
accountModel: accountModel,
modelContext: modelContext) { signedInAccountModel in
guard let signedInAccountModel else {
self.applicationViewMode = .signIn
return
@ -149,20 +167,22 @@ struct VernissageApp: App {
// Refresh application state.
self.applicationState.changeApplicationState(accountModel: accountModel,
instance: instance,
lastSeenStatusId: accountModel.lastSeenStatusId)
lastSeenStatusId: accountModel.lastSeenStatusId,
lastSeenNotificationId: accountModel.lastSeenNotificationId)
// Change view displayed by application.
self.applicationViewMode = .mainView
// Check amount of newly added photos.
if checkNewPhotos {
await self.calculateNewPhotosInBackground()
_ = await (self.calculateNewPhotosInBackground(), self.calculateNewNotificationsInBackground())
}
}
}
private func loadUserPreferences() {
ApplicationSettingsHandler.shared.update(applicationState: self.applicationState)
let modelContext = self.modelContainer.mainContext
ApplicationSettingsHandler.shared.update(applicationState: self.applicationState, modelContext: modelContext)
self.tintColor = self.applicationState.tintColor.color()
self.theme = self.applicationState.theme.colorScheme()
@ -187,7 +207,8 @@ struct VernissageApp: App {
}
private func refreshAccessTokens() async {
let defaultSettings = ApplicationSettingsHandler.shared.get()
let modelContext = self.modelContainer.mainContext
let defaultSettings = ApplicationSettingsHandler.shared.get(modelContext: modelContext)
// Run refreshing access tokens once per day.
guard let refreshTokenDate = Calendar.current.date(byAdding: .day, value: 1, to: defaultSettings.lastRefreshTokens), refreshTokenDate < Date.now else {
@ -195,18 +216,52 @@ struct VernissageApp: App {
}
// Refresh access tokens.
await AuthorizationService.shared.refreshAccessTokens()
await AuthorizationService.shared.refreshAccessTokens(modelContext: modelContext)
// Update time when refresh tokens has been updated.
defaultSettings.lastRefreshTokens = Date.now
CoreDataHandler.shared.save()
try? modelContext.save()
}
private func calculateNewPhotosInBackground() async {
if let account = self.applicationState.account {
self.applicationState.amountOfNewStatuses = await HomeTimelineService.shared.amountOfNewStatuses(for: account,
includeReblogs: self.applicationState.showReboostedStatuses,
hideStatusesWithoutAlt: self.applicationState.hideStatusesWithoutAlt)
let modelContext = self.modelContainer.mainContext
self.applicationState.amountOfNewStatuses = await HomeTimelineService.shared.amountOfNewStatuses(
includeReblogs: self.applicationState.showReboostedStatuses,
hideStatusesWithoutAlt: self.applicationState.hideStatusesWithoutAlt,
modelContext: modelContext
)
}
private func calculateNewNotificationsInBackground() async {
Logger.main.info("Calculating new notifications started.")
let modelContext = self.modelContainer.mainContext
let amountOfNewNotifications = await NotificationsService.shared.amountOfNewNotifications(modelContext: modelContext)
self.applicationState.amountOfNewNotifications = amountOfNewNotifications
do {
try await NotificationsService.shared.setBadgeCount(amountOfNewNotifications, modelContext: modelContext)
Logger.main.info("New notifications (\(amountOfNewNotifications)) calculated successfully.")
} catch {
Logger.main.error("Error ['Set badge count failed']: \(error.localizedDescription)")
}
}
private func scheduleAppRefresh() {
let request = BGAppRefreshTaskRequest(identifier: AppConstants.backgroundFetcherName)
request.earliestBeginDate = .now.addingTimeInterval(20 * 60)
do {
try BGTaskScheduler.shared.submit(request)
Logger.main.info("Background task scheduled successfully.")
} catch {
Logger.main.error("Error ['Registering background task failed']: \(error.localizedDescription)")
}
}
private func setBadgeCount() async {
scheduleAppRefresh()
await self.calculateNewNotificationsInBackground()
}
}

View File

@ -20,22 +20,11 @@ public extension View {
)
)
}
func imageContextMenu(statusData: StatusData, attachmentData: AttachmentData, uiImage: UIImage?) -> some View {
modifier(
ImageContextMenu(
id: statusData.getOrginalStatusId(),
url: statusData.url,
altText: attachmentData.text,
uiImage: uiImage
)
)
}
}
private struct ImageContextMenu: ViewModifier {
@EnvironmentObject var client: Client
@EnvironmentObject var routerPath: RouterPath
@Environment(Client.self) var client
@Environment(RouterPath.self) var routerPath
private let id: String
private let url: URL?
@ -125,7 +114,7 @@ private struct ImageContextMenu: ViewModifier {
private func reboost() async {
do {
_ = try await self.client.statuses?.boost(statusId: self.id)
ToastrService.shared.showSuccess(NSLocalizedString("status.title.reboosted", comment: "Reboosted"), imageName: "custom.rocket.fill")
ToastrService.shared.showSuccess("status.title.reboosted", imageName: "custom.rocket.fill")
} catch {
ErrorService.shared.handle(error, message: "status.error.reboostFailed", showToastr: true)
}
@ -134,7 +123,7 @@ private struct ImageContextMenu: ViewModifier {
private func favourite() async {
do {
_ = try await self.client.statuses?.favourite(statusId: self.id)
ToastrService.shared.showSuccess(NSLocalizedString("status.title.favourited", comment: "Favourited"), imageSystemName: "star.fill")
ToastrService.shared.showSuccess("status.title.favourited", imageSystemName: "star.fill")
} catch {
ErrorService.shared.handle(error, message: "status.error.favouriteFailed", showToastr: true)
}
@ -143,7 +132,7 @@ private struct ImageContextMenu: ViewModifier {
private func bookmark() async {
do {
_ = try await self.client.statuses?.bookmark(statusId: self.id)
ToastrService.shared.showSuccess(NSLocalizedString("status.title.bookmarked", comment: "Bookmarked"), imageSystemName: "bookmark.fill")
ToastrService.shared.showSuccess("status.title.bookmarked", imageSystemName: "bookmark.fill")
} catch {
ErrorService.shared.handle(error, message: "status.error.bookmarkFailed", showToastr: true)
}

View File

@ -8,7 +8,10 @@ import Foundation
import SwiftUI
import EnvironmentKit
import ServicesKit
import TipKit
import WidgetsKit
@MainActor
extension View {
func navigationMenuButtons(menuPosition: Binding<MenuPosition>,
onViewModeIconTap: @escaping (MainView.ViewMode) -> Void) -> some View {
@ -16,21 +19,16 @@ extension View {
}
}
@MainActor
private struct NavigationMenuButtons: ViewModifier {
@EnvironmentObject var routerPath: RouterPath
@Environment(ApplicationState.self) var applicationState
@Environment(RouterPath.self) var routerPath
@Environment(\.modelContext) private var modelContext
private let menuCustomizableTip = MenuCustomizableTip()
private let onViewModeIconTap: (MainView.ViewMode) -> Void
private let imageFontSize = 20.0
private let customMenuItems = [
NavigationMenuItemDetails(viewMode: .home),
NavigationMenuItemDetails(viewMode: .local),
NavigationMenuItemDetails(viewMode: .federated),
NavigationMenuItemDetails(viewMode: .search),
NavigationMenuItemDetails(viewMode: .profile),
NavigationMenuItemDetails(viewMode: .notifications)
]
@State private var displayedCustomMenuItems = [
SelectedMenuItemDetails(position: 1, viewMode: .home),
SelectedMenuItemDetails(position: 2, viewMode: .local),
@ -99,6 +97,7 @@ private struct NavigationMenuButtons: ViewModifier {
.background(.ultraThinMaterial)
.clipShape(Circle())
}
.popoverTip(menuCustomizableTip, arrowEdge: .bottom)
} else {
HStack(alignment: .center) {
self.composeImageView()
@ -116,6 +115,7 @@ private struct NavigationMenuButtons: ViewModifier {
.background(.ultraThinMaterial)
.clipShape(Capsule())
}
.popoverTip(menuCustomizableTip, arrowEdge: .bottom)
}
}
@ -132,6 +132,7 @@ private struct NavigationMenuButtons: ViewModifier {
.padding(.vertical, 10)
.padding(.horizontal, 8)
}
.environment(\.menuOrder, .fixed)
}
@ViewBuilder
@ -160,45 +161,37 @@ private struct NavigationMenuButtons: ViewModifier {
Button {
self.onViewModeIconTap(displayedCustomMenuItem.viewMode)
} label: {
Image(systemName: displayedCustomMenuItem.image)
displayedCustomMenuItem.viewMode.getImage(applicationState: applicationState)
.font(.system(size: self.imageFontSize))
.foregroundColor(.mainTextColor.opacity(0.75))
.padding(.vertical, 10)
.padding(.horizontal, 8)
}.contextMenu {
self.listOfIconsView(displayedCustomMenuItem)
}
}
@ViewBuilder
private func listOfIconsView(_ displayedCustomMenuItem: SelectedMenuItemDetails) -> some View {
ForEach(self.customMenuItems) { item in
Button {
MainNavigationOptions(hiddenMenuItems: Binding.constant([])) { viewMode in
withAnimation {
displayedCustomMenuItem.viewMode = item.viewMode
displayedCustomMenuItem.viewMode = viewMode
}
// Saving in core data.
// Saving in database.
switch displayedCustomMenuItem.position {
case 1:
ApplicationSettingsHandler.shared.set(customNavigationMenuItem1: item.viewMode.rawValue)
ApplicationSettingsHandler.shared.set(customNavigationMenuItem1: viewMode.rawValue, modelContext: modelContext)
case 2:
ApplicationSettingsHandler.shared.set(customNavigationMenuItem2: item.viewMode.rawValue)
ApplicationSettingsHandler.shared.set(customNavigationMenuItem2: viewMode.rawValue, modelContext: modelContext)
case 3:
ApplicationSettingsHandler.shared.set(customNavigationMenuItem3: item.viewMode.rawValue)
ApplicationSettingsHandler.shared.set(customNavigationMenuItem3: viewMode.rawValue, modelContext: modelContext)
default:
break
}
self.hiddenMenuItems = self.displayedCustomMenuItems.map({ $0.viewMode })
} label: {
Label(item.title, systemImage: item.image)
MenuCustomizableTip().invalidate(reason: .actionPerformed)
}
}
}
private func loadCustomMenuItems() {
let applicationSettings = ApplicationSettingsHandler.shared.get()
let applicationSettings = ApplicationSettingsHandler.shared.get(modelContext: modelContext)
self.setCustomMenuItem(position: 1, viewMode: MainView.ViewMode(rawValue: Int(applicationSettings.customNavigationMenuItem1)) ?? .home)
self.setCustomMenuItem(position: 2, viewMode: MainView.ViewMode(rawValue: Int(applicationSettings.customNavigationMenuItem2)) ?? .local)
@ -208,11 +201,8 @@ private struct NavigationMenuButtons: ViewModifier {
}
private func setCustomMenuItem(position: Int, viewMode: MainView.ViewMode) {
if let displayedCustomMenuItem = self.displayedCustomMenuItems.first(where: { $0.position == position }),
let customMenuItem = self.customMenuItems.first(where: { $0.viewMode == viewMode }) {
displayedCustomMenuItem.title = customMenuItem.title
displayedCustomMenuItem.viewMode = customMenuItem.viewMode
displayedCustomMenuItem.image = customMenuItem.image
if let displayedCustomMenuItem = self.displayedCustomMenuItems.first(where: { $0.position == position }) {
displayedCustomMenuItem.viewMode = viewMode
}
}
}

View File

@ -12,6 +12,7 @@ import ServicesKit
import EnvironmentKit
import WidgetsKit
@MainActor
struct AccountsPhotoView: View {
public enum ListType: Hashable {
case trending
@ -27,9 +28,9 @@ struct AccountsPhotoView: View {
}
}
@EnvironmentObject var applicationState: ApplicationState
@EnvironmentObject var client: Client
@EnvironmentObject var routerPath: RouterPath
@Environment(ApplicationState.self) var applicationState
@Environment(Client.self) var client
@Environment(RouterPath.self) var routerPath
@State public var listType: ListType
@ -99,7 +100,10 @@ struct AccountsPhotoView: View {
private func loadData() async {
do {
self.accounts = try await self.loadAccounts()
self.state = .loaded
withAnimation {
self.state = .loaded
}
} catch {
if !Task.isCancelled {
ErrorService.shared.handle(error, message: "trendingAccounts.error.loadingAccountsFailed", showToastr: true)

View File

@ -12,6 +12,7 @@ import ServicesKit
import EnvironmentKit
import WidgetsKit
@MainActor
struct AccountsView: View {
public enum ListType: Hashable {
case followers(entityId: String)
@ -21,6 +22,7 @@ struct AccountsView: View {
case blocks
case mutes
case search(query: String)
case disabledBoosts
public var title: String {
switch self {
@ -36,14 +38,17 @@ struct AccountsView: View {
return NSLocalizedString("accounts.navigationBar.blocked", comment: "Blocked")
case .mutes:
return NSLocalizedString("accounts.navigationBar.mutes", comment: "Mutes")
case .disabledBoosts:
return NSLocalizedString("accounts.navigationBar.disabledBoosts", comment: "Disabled boosts")
case .search(let query):
return query
}
}
}
@EnvironmentObject var applicationState: ApplicationState
@EnvironmentObject var client: Client
@Environment(ApplicationState.self) var applicationState
@Environment(Client.self) var client
@Environment(\.modelContext) private var modelContext
@State var listType: ListType
@ -119,7 +124,10 @@ struct AccountsView: View {
private func loadData(page: Int) async {
do {
try await self.loadAccounts(page: page)
self.state = .loaded
withAnimation {
self.state = .loaded
}
} catch {
if !Task.isCancelled {
ErrorService.shared.handle(error, message: "accounts.error.loadingAccountsFailed", showToastr: true)
@ -184,6 +192,25 @@ struct AccountsView: View {
} else {
return []
}
case .disabledBoosts:
if let accountId = self.applicationState.account?.id, self.downloadedPage == 1 {
let accountRelationships = AccountRelationshipHandler.shared.getAccountRelationships(for: accountId, modelContext: modelContext)
var downloadedAccounts: [Account] = []
for accountRelationship in accountRelationships {
do {
if let account = try await self.client.accounts?.account(withId: accountRelationship.accountId) {
downloadedAccounts.append(account)
}
} catch {
ErrorService.shared.handle(error, message: "accounts.error.loadingAccountsFailed", showToastr: false)
}
}
return downloadedAccounts
} else {
return []
}
}
}

View File

@ -8,6 +8,7 @@ import Foundation
import UIKit
import SwiftUI
@MainActor
struct ActivityView: UIViewControllerRepresentable {
let image: UIImage

View File

@ -13,9 +13,10 @@ import ServicesKit
import EnvironmentKit
import WidgetsKit
@MainActor
struct ComposeView: View {
@EnvironmentObject var routerPath: RouterPath
@EnvironmentObject var client: Client
@Environment(RouterPath.self) var routerPath
@Environment(Client.self) var client
@Environment(\.dismiss) private var dismiss
@ -26,6 +27,8 @@ struct ComposeView: View {
}
var body: some View {
@Bindable var routerPath = routerPath
NavigationView {
BaseComposeView(statusViewModel: self.statusViewModel) {
dismiss()

View File

@ -13,9 +13,12 @@ import ServicesKit
import WidgetsKit
import EnvironmentKit
@MainActor
struct EditProfileView: View {
@EnvironmentObject private var applicationState: ApplicationState
@EnvironmentObject private var client: Client
@Environment(ApplicationState.self) var applicationState
@Environment(Client.self) var client
@Environment(\.modelContext) private var modelContext
@Environment(\.dismiss) private var dismiss
@State private var account: Account?
@ -93,7 +96,7 @@ struct EditProfileView: View {
}
}
}
.onChange(of: self.selectedItems) { _ in
.onChange(of: self.selectedItems) {
Task {
await self.getAvatar()
}
@ -120,7 +123,9 @@ struct EditProfileView: View {
UserAvatar(accountAvatar: account.avatar, size: .large)
}
LoadingIndicator(isVisible: $saveDisabled)
if saveDisabled {
LoadingIndicator()
}
BottomRight {
Button {
@ -168,9 +173,9 @@ struct EditProfileView: View {
private func formView() -> some View {
Section {
TextField("", text: $displayName)
.onChange(of: self.displayName, perform: { _ in
.onChange(of: self.displayName) {
self.displayName = String(self.displayName.prefix(self.displayNameMaxLength))
})
}
} header: {
Text("editProfile.title.displayName", comment: "Display name")
} footer: {
@ -183,9 +188,9 @@ struct EditProfileView: View {
Section {
TextField("", text: $bio, axis: .vertical)
.lineLimit(5, reservesSpace: true)
.onChange(of: self.bio, perform: { _ in
.onChange(of: self.bio) {
self.bio = String(self.bio.prefix(self.bioMaxLength))
})
}
} header: {
Text("editProfile.title.bio", comment: "Bio")
} footer: {
@ -200,9 +205,9 @@ struct EditProfileView: View {
.autocapitalization(.none)
.keyboardType(.URL)
.autocorrectionDisabled()
.onChange(of: self.website, perform: { _ in
.onChange(of: self.website) {
self.website = String(self.website.prefix(self.websiteMaxLength))
})
}
} header: {
Text("editProfile.title.website", comment: "Website")
} footer: {
@ -245,10 +250,10 @@ struct EditProfileView: View {
if let avatarData = self.avatarData {
_ = try await self.client.accounts?.avatar(image: avatarData)
if let accountData = AccountDataHandler.shared.getAccountData(accountId: account.id) {
if let accountData = AccountDataHandler.shared.getAccountData(accountId: account.id, modelContext: modelContext) {
accountData.avatarData = avatarData
self.applicationState.account?.avatarData = avatarData
CoreDataHandler.shared.save()
try modelContext.save()
}
}

View File

@ -12,10 +12,11 @@ import ServicesKit
import EnvironmentKit
import WidgetsKit
@MainActor
struct FollowRequestsView: View {
@EnvironmentObject var applicationState: ApplicationState
@EnvironmentObject var routerPath: RouterPath
@EnvironmentObject var client: Client
@Environment(ApplicationState.self) var applicationState
@Environment(RouterPath.self) var routerPath
@Environment(Client.self) var client
@State private var accounts: [Account] = []
@State private var downloadedPage = 1
@ -126,7 +127,10 @@ struct FollowRequestsView: View {
private func loadData(page: Int) async {
do {
try await self.loadAccounts(page: page)
self.state = .loaded
withAnimation {
self.state = .loaded
}
} catch {
if !Task.isCancelled {
ErrorService.shared.handle(error, message: "accounts.error.loadingAccountsFailed", showToastr: true)

View File

@ -12,6 +12,7 @@ import ServicesKit
import EnvironmentKit
import WidgetsKit
@MainActor
struct HashtagsView: View {
public enum ListType: Hashable {
case trending
@ -30,9 +31,9 @@ struct HashtagsView: View {
}
}
@EnvironmentObject var applicationState: ApplicationState
@EnvironmentObject var client: Client
@EnvironmentObject var routerPath: RouterPath
@Environment(ApplicationState.self) var applicationState
@Environment(Client.self) var client
@Environment(RouterPath.self) var routerPath
@State public var listType: ListType
@ -98,7 +99,10 @@ struct HashtagsView: View {
private func loadData() async {
do {
self.tags = try await self.loadTags()
self.state = .loaded
withAnimation {
self.state = .loaded
}
} catch {
if !Task.isCancelled {
ErrorService.shared.handle(error, message: "tags.error.loadingTagsFailed", showToastr: true)

View File

@ -1,222 +0,0 @@
//
// https://mczachurski.dev
// Copyright © 2023 Marcin Czachurski and the repository contributors.
// Licensed under the Apache License 2.0.
//
import SwiftUI
import ServicesKit
import EnvironmentKit
import WidgetsKit
import OSLog
import Semaphore
struct HomeFeedView: View {
@Environment(\.managedObjectContext) private var viewContext
@EnvironmentObject var applicationState: ApplicationState
@EnvironmentObject var routerPath: RouterPath
@State private var allItemsLoaded = false
@State private var state: ViewState = .loading
@State private var opacity = 0.0
@State private var offset = -50.0
@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 {
switch state {
case .loading:
LoadingIndicator()
.task {
await self.loadData()
}
case .loaded:
if self.dbStatuses.isEmpty {
NoDataView(imageSystemName: "photo.on.rectangle.angled", text: "home.title.noPhotos")
} else {
self.timeline()
}
case .error(let error):
ErrorView(error: error) {
self.state = .loading
await self.loadData()
}
.padding()
}
}
@ViewBuilder
private func timeline() -> some View {
ZStack {
ScrollView {
LazyVStack {
ForEach(dbStatuses, id: \.self) { item in
if self.shouldUpToDateBeVisible(statusId: item.id) {
self.upToDatePlaceholder()
}
ImageRow(statusData: item)
}
if allItemsLoaded == false {
LoadingIndicator()
.task {
do {
if let account = self.applicationState.account {
let newStatusesCount = try await HomeTimelineService.shared.loadOnBottom(
for: account,
includeReblogs: self.applicationState.showReboostedStatuses,
hideStatusesWithoutAlt: self.applicationState.hideStatusesWithoutAlt
)
if newStatusesCount == 0 {
allItemsLoaded = true
}
}
} catch {
ErrorService.shared.handle(error, message: "global.error.errorDuringDownloadStatuses", showToastr: !Task.isCancelled)
}
}
}
}
}
self.newPhotosView()
.offset(y: self.offset)
.opacity(self.opacity)
}
.refreshable {
HapticService.shared.fireHaptic(of: .dataRefresh(intensity: 0.3))
await self.refreshData()
HapticService.shared.fireHaptic(of: .dataRefresh(intensity: 0.7))
}
.onChange(of: self.applicationState.amountOfNewStatuses) { _ in
self.calculateOffset()
}.onAppear {
self.calculateOffset()
}
}
private func refreshData() async {
do {
if let account = self.applicationState.account {
let lastSeenStatusId = try await HomeTimelineService.shared.refreshTimeline(for: account,
includeReblogs: self.applicationState.showReboostedStatuses,
hideStatusesWithoutAlt: self.applicationState.hideStatusesWithoutAlt,
updateLastSeenStatus: true)
asyncAfter(0.75) {
self.applicationState.lastSeenStatusId = lastSeenStatusId
self.applicationState.amountOfNewStatuses = 0
}
}
} catch {
ErrorService.shared.handle(error, message: "global.error.errorDuringDownloadStatuses", showToastr: !Task.isCancelled)
}
}
private func loadData() async {
do {
// We have to load data automatically only when the database is empty.
guard self.dbStatuses.isEmpty else {
withAnimation {
self.state = .loaded
}
return
}
if let account = self.applicationState.account {
_ = try await HomeTimelineService.shared.refreshTimeline(for: account,
includeReblogs: self.applicationState.showReboostedStatuses,
hideStatusesWithoutAlt: self.applicationState.hideStatusesWithoutAlt)
}
self.applicationState.amountOfNewStatuses = 0
self.state = .loaded
} catch {
if !Task.isCancelled {
ErrorService.shared.handle(error, message: "global.error.statusesNotRetrieved", showToastr: true)
self.state = .error(error)
} else {
ErrorService.shared.handle(error, message: "global.error.statusesNotRetrieved", showToastr: false)
}
}
}
private func calculateOffset() {
if self.applicationState.amountOfNewStatuses > 0 {
withAnimation(.easeIn) {
self.showNewStatusesView()
}
} else {
withAnimation(.easeOut) {
self.hideNewStatusesView()
}
}
}
private func showNewStatusesView() {
self.offset = 0.0
self.opacity = 1.0
}
private func hideNewStatusesView() {
self.offset = -50.0
self.opacity = 0.0
}
private func shouldUpToDateBeVisible(statusId: String) -> Bool {
return self.applicationState.lastSeenStatusId != dbStatuses.first?.id && self.applicationState.lastSeenStatusId == statusId
}
@ViewBuilder
private func upToDatePlaceholder() -> some View {
VStack(alignment: .center) {
Image(systemName: "checkmark.seal")
.resizable()
.frame(width: 64, height: 64)
.fontWeight(.ultraLight)
.foregroundColor(self.applicationState.tintColor.color().opacity(0.6))
Text("home.title.allCaughtUp", comment: "You're all caught up")
.font(.title2)
.fontWeight(.thin)
.foregroundColor(Color.mainTextColor.opacity(0.6))
}
.padding(.vertical, 8)
.frame(width: UIScreen.main.bounds.width, height: UIScreen.main.bounds.width * 0.75)
}
@ViewBuilder
private func newPhotosView() -> some View {
VStack(alignment: .trailing, spacing: 4) {
HStack {
Spacer()
HStack {
Image(systemName: "arrow.up")
.fontWeight(.light)
Text("\(self.applicationState.amountOfNewStatuses)")
.fontWeight(.semibold)
}
.padding(.vertical, 12)
.padding(.horizontal, 18)
.font(.callout)
.foregroundColor(Color.mainTextColor)
.background(.ultraThinMaterial)
.clipShape(Capsule())
}
Spacer()
}
.padding(.top, 10)
.padding(.trailing, 6)
}
}

View File

@ -0,0 +1,357 @@
//
// https://mczachurski.dev
// Copyright © 2023 Marcin Czachurski and the repository contributors.
// Licensed under the Apache License 2.0.
//
import SwiftUI
import Nuke
import PixelfedKit
import ClientKit
import ServicesKit
import EnvironmentKit
import WidgetsKit
import TipKit
@MainActor
struct HomeTimelineView: View {
@Environment(ApplicationState.self) var applicationState
@Environment(Client.self) var client
@Environment(\.modelContext) private var modelContext
@State private var allItemsLoaded = false
@State private var statusViewModels: [StatusModel] = []
@State private var state: ViewState = .loading
@State private var lastStatusId: String?
@State private var opacity = 0.0
@State private var offset = -50.0
private let defaultLimit = 80
private let imagePrefetcher = ImagePrefetcher(destination: .diskCache)
private let timelineDoubleTapTip = TimelineDoubleTapTip()
var body: some View {
switch state {
case .loading:
LoadingIndicator()
.task {
await self.loadData()
}
case .loaded:
if self.statusViewModels.isEmpty {
NoDataView(imageSystemName: "photo.on.rectangle.angled", text: "statuses.title.noPhotos")
} else {
self.list()
}
case .error(let error):
ErrorView(error: error) {
self.state = .loading
await self.loadData()
}
.padding()
}
}
@ViewBuilder
private func list() -> some View {
ZStack {
ScrollView {
LazyVStack(alignment: .center) {
TipView(timelineDoubleTapTip)
.padding(8)
ForEach(self.statusViewModels, id: \.id) { item in
if self.shouldUpToDateBeVisible(statusId: item.id) {
self.upToDatePlaceholder()
}
ImageRowAsync(statusViewModel: item, containerWidth: Binding.constant(UIScreen.main.bounds.width))
}
if allItemsLoaded == false {
HStack {
Spacer()
LoadingIndicator()
.task {
do {
try await self.loadMoreStatuses()
} catch {
ErrorService.shared.handle(error, message: "statuses.error.loadingStatusesFailed", showToastr: !Task.isCancelled)
}
}
Spacer()
}
}
}
}
self.newPhotosView()
.offset(y: self.offset)
.opacity(self.opacity)
}
.refreshable {
do {
HapticService.shared.fireHaptic(of: .dataRefresh(intensity: 0.3))
try await self.refreshStatuses()
HapticService.shared.fireHaptic(of: .dataRefresh(intensity: 0.7))
} catch {
ErrorService.shared.handle(error, message: "statuses.error.loadingStatusesFailed", showToastr: !Task.isCancelled)
}
}
.onChange(of: self.applicationState.showReboostedStatuses) {
Task {
do {
HapticService.shared.fireHaptic(of: .dataRefresh(intensity: 0.3))
try await self.refreshStatuses()
HapticService.shared.fireHaptic(of: .dataRefresh(intensity: 0.7))
} catch {
ErrorService.shared.handle(error, message: "statuses.error.loadingStatusesFailed", showToastr: !Task.isCancelled)
}
}
}
.onChange(of: self.applicationState.amountOfNewStatuses) {
self.calculateOffset()
}.onAppear {
self.calculateOffset()
}
}
@ViewBuilder
private func upToDatePlaceholder() -> some View {
VStack(alignment: .center) {
Image(systemName: "checkmark.seal")
.resizable()
.frame(width: 64, height: 64)
.fontWeight(.ultraLight)
.foregroundColor(self.applicationState.tintColor.color().opacity(0.6))
Text("home.title.allCaughtUp", comment: "You're all caught up")
.font(.title2)
.fontWeight(.thin)
.foregroundColor(Color.mainTextColor.opacity(0.6))
}
.padding(.vertical, 8)
.frame(width: UIScreen.main.bounds.width, height: UIScreen.main.bounds.width * 0.75)
}
@ViewBuilder
private func newPhotosView() -> some View {
VStack(alignment: .trailing, spacing: 4) {
HStack {
Spacer()
HStack {
Image(systemName: "arrow.up")
.fontWeight(.light)
Text("\(self.applicationState.amountOfNewStatuses)")
.fontWeight(.semibold)
}
.padding(.vertical, 12)
.padding(.horizontal, 18)
.font(.callout)
.foregroundColor(Color.mainTextColor)
.background(.ultraThinMaterial)
.clipShape(Capsule())
}
Spacer()
}
.padding(.top, 10)
.padding(.trailing, 6)
}
private func loadData() async {
do {
try await self.loadFirstStatuses()
try ViewedStatusHandler.shared.deleteOldViewedStatuses(modelContext: modelContext)
withAnimation {
self.state = .loaded
}
} catch {
ErrorService.shared.handle(error, message: "statuses.error.loadingStatusesFailed", showToastr: !Task.isCancelled)
self.state = .error(error)
}
}
private func loadFirstStatuses() async throws {
guard let accountId = self.applicationState.account?.id,
let accountData = AccountDataHandler.shared.getAccountData(accountId: accountId, modelContext: modelContext) else {
return
}
// Download statuses from API (which are older then last visible status).
let statuses = try await self.loadFromCacheOrApi(timelineCache: accountData.timelineCache)
if statuses.isEmpty {
self.allItemsLoaded = true
return
}
// Remember last status id returned by API.
self.lastStatusId = statuses.last?.id
// Get only visible statuses.
let visibleStatuses = HomeTimelineService.shared.getVisibleStatuses(accountId: accountId,
statuses: statuses,
hideStatusesWithoutAlt: self.applicationState.hideStatusesWithoutAlt,
modelContext: modelContext)
// Remeber first status returned by API in user context (when it's newer then remembered).
try AccountDataHandler.shared.update(lastSeenStatusId: nil,
lastLoadedStatusId: statuses.first?.id,
applicationState: self.applicationState,
modelContext: modelContext)
// Append statuses to viewed.
try ViewedStatusHandler.shared.append(contentsOf: statuses, accountId: accountId, modelContext: modelContext)
// Map to view models.
let statusModels = visibleStatuses.map({ StatusModel(status: $0) })
// Prefetch images.
self.prefetch(statusModels: statusModels)
// Append to empty list.
self.statusViewModels.append(contentsOf: statusModels)
}
private func loadMoreStatuses() async throws {
if let lastStatusId = self.lastStatusId, let accountId = self.applicationState.account?.id {
// Download statuses from API.
let statuses = try await self.loadFromApi(maxId: lastStatusId)
if statuses.isEmpty {
self.allItemsLoaded = true
return
}
// Now we have new last status.
if let lastStatusId = statuses.last?.id {
self.lastStatusId = lastStatusId
}
// Get only visible statuses.
let visibleStatuses = HomeTimelineService.shared.getVisibleStatuses(accountId: accountId,
statuses: statuses,
hideStatusesWithoutAlt: self.applicationState.hideStatusesWithoutAlt,
modelContext: modelContext)
// Append statuses to viewed.
try ViewedStatusHandler.shared.append(contentsOf: statuses, accountId: accountId, modelContext: modelContext)
// Map to view models.
let statusModels = visibleStatuses.map({ StatusModel(status: $0) })
// Prefetch images.
self.prefetch(statusModels: statusModels)
// Append statuses to existing array of statuses (at the end).
self.statusViewModels.append(contentsOf: statusModels)
}
}
private func refreshStatuses() async throws {
guard let accountId = self.applicationState.account?.id else {
return
}
// Download statuses from API.
let statuses = try await self.loadFromApi()
if statuses.isEmpty {
self.allItemsLoaded = true
return
}
// Remember last status id returned by API.
self.lastStatusId = statuses.last?.id
// Get only visible statuses.
let visibleStatuses = HomeTimelineService.shared.getVisibleStatuses(accountId: accountId,
statuses: statuses,
hideStatusesWithoutAlt: self.applicationState.hideStatusesWithoutAlt,
modelContext: modelContext)
// Remeber first status returned by API in user context (when it's newer then remembered).
try AccountDataHandler.shared.update(lastSeenStatusId: self.statusViewModels.first?.id,
lastLoadedStatusId: statuses.first?.id,
statuses: statuses,
applicationState: self.applicationState,
modelContext: modelContext)
// Append statuses to viewed.
try ViewedStatusHandler.shared.append(contentsOf: statuses, accountId: accountId, modelContext: modelContext)
// Map to view models.
let statusModels = visibleStatuses.map({ StatusModel(status: $0) })
// Prefetch images.
self.prefetch(statusModels: statusModels)
// Replace old collection with new one.
self.statusViewModels = statusModels
// Set that all statuses has been downloaded.
self.applicationState.amountOfNewStatuses = 0
}
private func loadFromCacheOrApi(timelineCache: String?) async throws -> [Status] {
if let timelineCache, let timelineCacheData = timelineCache.data(using: .utf8) {
let statusesFromCache = try? JSONDecoder().decode([Status].self, from: timelineCacheData)
if let statusesFromCache {
return statusesFromCache
}
}
return try await self.loadFromApi()
}
private func loadFromApi(maxId: String? = nil, sinceId: String? = nil, minId: String? = nil) async throws -> [Status] {
return try await self.client.publicTimeline?.getHomeTimeline(
maxId: maxId,
sinceId: sinceId,
minId: minId,
limit: self.defaultLimit,
includeReblogs: self.applicationState.showReboostedStatuses) ?? []
}
private func calculateOffset() {
if self.applicationState.amountOfNewStatuses > 0 {
withAnimation(.easeIn) {
self.showNewStatusesView()
}
} else {
withAnimation(.easeOut) {
self.hideNewStatusesView()
}
}
}
private func showNewStatusesView() {
self.offset = 0.0
self.opacity = 1.0
}
private func hideNewStatusesView() {
self.offset = -50.0
self.opacity = 0.0
}
private func prefetch(statusModels: [StatusModel]) {
imagePrefetcher.startPrefetching(with: statusModels.getAllImagesUrls())
}
private func shouldHideStatusWithoutAlt(status: Status) -> Bool {
if self.applicationState.hideStatusesWithoutAlt == false {
return false
}
return status.statusContainsAltText() == false
}
private func shouldUpToDateBeVisible(statusId: String) -> Bool {
return self.applicationState.lastSeenStatusId != statusViewModels.first?.id && self.applicationState.lastSeenStatusId == statusId
}
}

View File

@ -12,10 +12,11 @@ import ServicesKit
import EnvironmentKit
import WidgetsKit
@MainActor
struct InstanceView: View {
@EnvironmentObject private var applicationState: ApplicationState
@EnvironmentObject private var routerPath: RouterPath
@EnvironmentObject private var client: Client
@Environment(ApplicationState.self) var applicationState
@Environment(RouterPath.self) var routerPath
@Environment(Client.self) var client
@State private var state: ViewState = .loading
@State private var instance: Instance?
@ -130,7 +131,9 @@ struct InstanceView: View {
self.instance = try await self.client.instances.instance(url: serverUrl)
}
self.state = .loaded
withAnimation {
self.state = .loaded
}
} catch {
if !Task.isCancelled {
ErrorService.shared.handle(error, message: "instance.error.loadingDataFailed", showToastr: true)

View File

@ -6,6 +6,7 @@
import SwiftUI
@MainActor
struct LoadingView: View {
var body: some View {
VStack(alignment: .center) {

View File

@ -6,19 +6,21 @@
import SwiftUI
import UIKit
import CoreData
import SwiftData
import PixelfedKit
import ClientKit
import ServicesKit
import EnvironmentKit
import WidgetsKit
@MainActor
struct MainView: View {
@Environment(\.managedObjectContext) private var viewContext
@Environment(\.modelContext) private var modelContext
@EnvironmentObject var applicationState: ApplicationState
@EnvironmentObject var client: Client
@EnvironmentObject var routerPath: RouterPath
@EnvironmentObject var tipsStore: TipsStore
@Environment(ApplicationState.self) var applicationState
@Environment(Client.self) var client
@Environment(RouterPath.self) var routerPath
@Environment(TipsStore.self) var tipsStore
@State private var navBarTitle: LocalizedStringKey = ViewMode.home.title
@State private var viewMode: ViewMode = .home {
@ -27,9 +29,11 @@ struct MainView: View {
}
}
@FetchRequest(sortDescriptors: [SortDescriptor(\.acct, order: .forward)]) var dbAccounts: FetchedResults<AccountData>
private let mainNavigationTip = MainNavigationTip()
@Query(sort: \AccountData.acct, order: .forward) var dbAccounts: [AccountData]
public enum ViewMode: Int {
public enum ViewMode: Int, Identifiable {
case home = 1
case local = 2
case federated = 3
@ -40,6 +44,10 @@ struct MainView: View {
case trendingTags = 8
case trendingAccounts = 9
var id: Self {
return self
}
public var title: LocalizedStringKey {
switch self {
case .home:
@ -63,53 +71,68 @@ struct MainView: View {
}
}
public var image: String {
@ViewBuilder
public func getImage(applicationState: ApplicationState) -> some View {
switch self {
case .home:
return "house"
Image(systemName: "house")
case .trendingPhotos:
return "photo.stack"
Image(systemName: "photo.stack")
case .trendingTags:
return "tag"
Image(systemName: "tag")
case .trendingAccounts:
return "person.3"
Image(systemName: "person.crop.rectangle.stack")
case .local:
return "building"
Image(systemName: "building")
case .federated:
return "globe.europe.africa"
Image(systemName: "globe.europe.africa")
case .profile:
return "person.crop.circle"
Image(systemName: "person.crop.circle")
case .notifications:
return "bell.badge"
if applicationState.menuPosition == .top {
applicationState.amountOfNewNotifications > 0 ? Image(systemName: "bell.badge") : Image(systemName: "bell")
} else {
applicationState.amountOfNewNotifications > 0
? AnyView(
Image(systemName: "bell.badge")
.symbolRenderingMode(.palette)
.foregroundStyle(applicationState.tintColor.color().opacity(0.75), Color.mainTextColor.opacity(0.75)))
: AnyView(Image(systemName: "bell"))
}
case .search:
return "magnifyingglass"
Image(systemName: "magnifyingglass")
}
}
}
var body: some View {
self.getMainView()
.navigationMenuButtons(menuPosition: $applicationState.menuPosition) { viewMode in
self.switchView(to: viewMode)
}
.navigationTitle(navBarTitle)
.navigationBarTitleDisplayMode(.inline)
.toolbar {
self.getLeadingToolbar()
@Bindable var applicationState = applicationState
@Bindable var routerPath = routerPath
if self.applicationState.menuPosition == .top {
self.getPrincipalToolbar()
self.getTrailingToolbar()
NavigationStack(path: $routerPath.path) {
self.getMainView()
.navigationMenuButtons(menuPosition: $applicationState.menuPosition) { viewMode in
self.switchView(to: viewMode)
}
}
.onChange(of: tipsStore.status) { status in
if status == .successful {
withAnimation(.spring()) {
self.routerPath.presentedOverlay = .successPayment
self.tipsStore.reset()
.navigationTitle(navBarTitle)
.navigationBarTitleDisplayMode(.inline)
.toolbar {
self.getLeadingToolbar()
if self.applicationState.menuPosition == .top {
self.getPrincipalToolbar()
self.getTrailingToolbar()
}
}
}
.onChange(of: tipsStore.status) { oldStatus, newStatus in
if newStatus == .successful {
withAnimation(.spring()) {
self.routerPath.presentedOverlay = .successPayment
self.tipsStore.reset()
}
}
}
}
}
@ViewBuilder
@ -117,7 +140,7 @@ struct MainView: View {
switch self.viewMode {
case .home:
if UIDevice.isIPhone {
HomeFeedView(accountId: applicationState.account?.id ?? String.empty())
HomeTimelineView()
.id(applicationState.account?.id ?? String.empty())
} else {
StatusesView(listType: .home)
@ -173,6 +196,7 @@ struct MainView: View {
}
.frame(width: 150)
.foregroundColor(.mainTextColor)
.popoverTip(self.mainNavigationTip)
}
}
}
@ -282,9 +306,11 @@ struct MainView: View {
let authorizationSession = AuthorizationSession()
let accountModel = account.toAccountModel()
await AuthorizationService.shared.verifyAccount(session: authorizationSession, accountModel: accountModel) { signedInAccountModel in
await AuthorizationService.shared.verifyAccount(session: authorizationSession,
accountModel: accountModel,
modelContext: modelContext) { signedInAccountModel in
guard let signedInAccountModel else {
ToastrService.shared.showError(subtitle: NSLocalizedString("mainview.error.switchAccounts", comment: "Cannot switch accounts."))
ToastrService.shared.showError(title: "", subtitle: NSLocalizedString("mainview.error.switchAccounts", comment: "Cannot switch accounts."))
return
}
@ -297,12 +323,28 @@ struct MainView: View {
// Refresh application state.
self.applicationState.changeApplicationState(accountModel: signedInAccountModel,
instance: instance,
lastSeenStatusId: signedInAccountModel.lastSeenStatusId)
lastSeenStatusId: signedInAccountModel.lastSeenStatusId,
lastSeenNotificationId: signedInAccountModel.lastSeenNotificationId)
// Set account as default (application will open this account after restart).
ApplicationSettingsHandler.shared.set(accountId: signedInAccountModel.id)
ApplicationSettingsHandler.shared.set(accountId: signedInAccountModel.id, modelContext: modelContext)
// Refresh new photos and notifications.
_ = await (self.calculateNewPhotosInBackground(), self.calculateNewNotificationsInBackground())
}
}
}
}
private func calculateNewPhotosInBackground() async {
self.applicationState.amountOfNewStatuses = await HomeTimelineService.shared.amountOfNewStatuses(
includeReblogs: self.applicationState.showReboostedStatuses,
hideStatusesWithoutAlt: self.applicationState.hideStatusesWithoutAlt,
modelContext: modelContext
)
}
private func calculateNewNotificationsInBackground() async {
self.applicationState.amountOfNewNotifications = await NotificationsService.shared.amountOfNewNotifications(modelContext: modelContext)
}
}

View File

@ -11,9 +11,11 @@ import ServicesKit
import EnvironmentKit
import WidgetsKit
@MainActor
struct NotificationsView: View {
@EnvironmentObject var applicationState: ApplicationState
@EnvironmentObject var client: Client
@Environment(ApplicationState.self) var applicationState
@Environment(Client.self) var client
@Environment(\.modelContext) private var modelContext
@State var accountId: String
@State private var notifications: [PixelfedKit.Notification] = []
@ -22,6 +24,7 @@ struct NotificationsView: View {
@State private var minId: String?
@State private var maxId: String?
@State private var lastSeenNotificationId: String?
private let defaultPageSize = 40
@ -57,7 +60,7 @@ struct NotificationsView: View {
private func list() -> some View {
List {
ForEach(notifications, id: \.id) { notification in
NotificationRowView(notification: notification)
NotificationRowView(notification: notification, isNewNotification: self.isNewNotification(notification: notification))
}
if allItemsLoaded == false {
@ -75,13 +78,18 @@ struct NotificationsView: View {
.listStyle(PlainListStyle())
.refreshable {
HapticService.shared.fireHaptic(of: .dataRefresh(intensity: 0.3))
await self.loadNewNotifications()
await self.refreshNotifications()
HapticService.shared.fireHaptic(of: .dataRefresh(intensity: 0.7))
}
}
func loadNotifications() async {
do {
if let accountId = applicationState.account?.id {
let accountData = AccountDataHandler.shared.getAccountData(accountId: accountId, modelContext: modelContext)
self.lastSeenNotificationId = accountData?.lastSeenNotificationId
}
if let linkable = try await self.client.notifications?.notifications(maxId: maxId, minId: minId, limit: 5) {
self.minId = linkable.link?.minId
self.maxId = linkable.link?.maxId
@ -91,7 +99,15 @@ struct NotificationsView: View {
self.allItemsLoaded = true
}
self.state = .loaded
withAnimation {
self.state = .loaded
}
try AccountDataHandler.shared.update(lastSeenNotificationId: linkable.data.first?.id, applicationState: self.applicationState, modelContext: modelContext)
// Refresh infomation about viewed notifications.
self.applicationState.amountOfNewNotifications = 0
try? await NotificationsService.shared.setBadgeCount(0, modelContext: modelContext)
}
} catch {
if !Task.isCancelled {
@ -119,14 +135,25 @@ struct NotificationsView: View {
}
}
private func loadNewNotifications() async {
private func refreshNotifications() async {
do {
if let accountId = applicationState.account?.id {
let accountData = AccountDataHandler.shared.getAccountData(accountId: accountId, modelContext: modelContext)
self.lastSeenNotificationId = accountData?.lastSeenNotificationId
}
if let linkable = try await self.client.notifications?.notifications(minId: self.minId, limit: self.defaultPageSize) {
if let first = linkable.data.first, self.notifications.contains(where: { notification in notification.id == first.id }) {
// We have all notifications, we don't have to do anything.
return
}
try AccountDataHandler.shared.update(lastSeenNotificationId: linkable.data.first?.id, applicationState: self.applicationState, modelContext: modelContext)
// Refresh infomation about viewed notifications.
self.applicationState.amountOfNewNotifications = 0
try? await NotificationsService.shared.setBadgeCount(0, modelContext: modelContext)
self.minId = linkable.link?.minId
self.notifications.insert(contentsOf: linkable.data, at: 0)
}
@ -134,4 +161,12 @@ struct NotificationsView: View {
ErrorService.shared.handle(error, message: "notifications.error.loadingNotificationsFailed", showToastr: !Task.isCancelled)
}
}
private func isNewNotification(notification: PixelfedKit.Notification) -> Bool {
guard let lastSeenNotificationId = self.lastSeenNotificationId else {
return false
}
return notification.id > lastSeenNotificationId
}
}

View File

@ -13,17 +13,19 @@ import EnvironmentKit
import WidgetsKit
struct NotificationRowView: View {
@EnvironmentObject var applicationState: ApplicationState
@EnvironmentObject var routerPath: RouterPath
@EnvironmentObject var client: Client
@Environment(ApplicationState.self) var applicationState
@Environment(RouterPath.self) var routerPath
@Environment(Client.self) var client
@State private var image: SwiftUI.Image?
private var attachment: MediaAttachment?
private var notification: PixelfedKit.Notification
private let isNewNotification: Bool
private let notification: PixelfedKit.Notification
public init(notification: PixelfedKit.Notification) {
public init(notification: PixelfedKit.Notification, isNewNotification: Bool) {
self.notification = notification
self.isNewNotification = isNewNotification
self.attachment = notification.status?.getAllImageMediaAttachments().first
if let attachment, let previewUrl = attachment.previewUrl, let imageFromCache = CacheImageService.shared.get(for: previewUrl) {
@ -48,7 +50,7 @@ struct NotificationRowView: View {
.frame(width: 56, height: 56)
VStack(alignment: .leading, spacing: 0) {
HStack(alignment: .top) {
HStack(alignment: .center) {
Text(self.notification.account.displayNameWithoutEmojis)
.foregroundColor(.mainTextColor)
.font(.footnote)
@ -56,6 +58,12 @@ struct NotificationRowView: View {
Spacer()
if self.isNewNotification {
Circle()
.foregroundStyle(self.applicationState.tintColor.color())
.frame(width: 8.0, height: 8.0)
}
if let createdAt = self.notification.createdAt.toDate(.isoDateTimeMilliSec) {
RelativeTime(date: createdAt)
.foregroundColor(.customGrayColor)

Some files were not shown because too many files have changed in this diff Show More