feat: add "Disable avatar animation" preference
This commit is contained in:
parent
553402bee5
commit
13a1194a59
|
@ -189,6 +189,7 @@
|
||||||
<attribute name="appearanceRaw" attributeType="String"/>
|
<attribute name="appearanceRaw" attributeType="String"/>
|
||||||
<attribute name="createdAt" attributeType="Date" usesScalarValueType="NO"/>
|
<attribute name="createdAt" attributeType="Date" usesScalarValueType="NO"/>
|
||||||
<attribute name="domain" attributeType="String"/>
|
<attribute name="domain" attributeType="String"/>
|
||||||
|
<attribute name="preferredStaticAvatar" attributeType="Boolean" defaultValueString="NO" usesScalarValueType="YES"/>
|
||||||
<attribute name="preferredTrueBlackDarkMode" attributeType="Boolean" defaultValueString="NO" usesScalarValueType="YES"/>
|
<attribute name="preferredTrueBlackDarkMode" attributeType="Boolean" defaultValueString="NO" usesScalarValueType="YES"/>
|
||||||
<attribute name="updatedAt" attributeType="Date" usesScalarValueType="NO"/>
|
<attribute name="updatedAt" attributeType="Date" usesScalarValueType="NO"/>
|
||||||
<attribute name="userID" attributeType="String"/>
|
<attribute name="userID" attributeType="String"/>
|
||||||
|
@ -281,7 +282,7 @@
|
||||||
<element name="PollOption" positionX="0" positionY="0" width="128" height="134"/>
|
<element name="PollOption" positionX="0" positionY="0" width="128" height="134"/>
|
||||||
<element name="PrivateNote" positionX="0" positionY="0" width="128" height="89"/>
|
<element name="PrivateNote" positionX="0" positionY="0" width="128" height="89"/>
|
||||||
<element name="SearchHistory" positionX="0" positionY="0" width="128" height="104"/>
|
<element name="SearchHistory" positionX="0" positionY="0" width="128" height="104"/>
|
||||||
<element name="Setting" positionX="72" positionY="162" width="128" height="134"/>
|
<element name="Setting" positionX="72" positionY="162" width="128" height="149"/>
|
||||||
<element name="Status" positionX="0" positionY="0" width="128" height="599"/>
|
<element name="Status" positionX="0" positionY="0" width="128" height="599"/>
|
||||||
<element name="Subscription" positionX="81" positionY="171" width="128" height="179"/>
|
<element name="Subscription" positionX="81" positionY="171" width="128" height="179"/>
|
||||||
<element name="SubscriptionAlerts" positionX="72" positionY="162" width="128" height="14"/>
|
<element name="SubscriptionAlerts" positionX="72" positionY="162" width="128" height="14"/>
|
||||||
|
|
|
@ -10,10 +10,12 @@ import Foundation
|
||||||
|
|
||||||
public final class Setting: NSManagedObject {
|
public final class Setting: NSManagedObject {
|
||||||
|
|
||||||
@NSManaged public var appearanceRaw: String
|
|
||||||
@NSManaged public var preferredTrueBlackDarkMode: Bool
|
|
||||||
@NSManaged public var domain: String
|
@NSManaged public var domain: String
|
||||||
@NSManaged public var userID: String
|
@NSManaged public var userID: String
|
||||||
|
|
||||||
|
@NSManaged public var appearanceRaw: String
|
||||||
|
@NSManaged public var preferredTrueBlackDarkMode: Bool
|
||||||
|
@NSManaged public var preferredStaticAvatar: Bool
|
||||||
|
|
||||||
@NSManaged public private(set) var createdAt: Date
|
@NSManaged public private(set) var createdAt: Date
|
||||||
@NSManaged public private(set) var updatedAt: Date
|
@NSManaged public private(set) var updatedAt: Date
|
||||||
|
@ -54,6 +56,12 @@ extension Setting {
|
||||||
self.preferredTrueBlackDarkMode = preferredTrueBlackDarkMode
|
self.preferredTrueBlackDarkMode = preferredTrueBlackDarkMode
|
||||||
didUpdate(at: Date())
|
didUpdate(at: Date())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public func update(preferredStaticAvatar: Bool) {
|
||||||
|
guard preferredStaticAvatar != self.preferredStaticAvatar else { return }
|
||||||
|
self.preferredStaticAvatar = preferredStaticAvatar
|
||||||
|
didUpdate(at: Date())
|
||||||
|
}
|
||||||
|
|
||||||
public func didUpdate(at networkDate: Date) {
|
public func didUpdate(at networkDate: Date) {
|
||||||
self.updatedAt = networkDate
|
self.updatedAt = networkDate
|
||||||
|
|
|
@ -497,6 +497,9 @@
|
||||||
"appearance_settings": {
|
"appearance_settings": {
|
||||||
"dark_mode": {
|
"dark_mode": {
|
||||||
"title": "True black Dark Mode"
|
"title": "True black Dark Mode"
|
||||||
|
},
|
||||||
|
"avatar_animation": {
|
||||||
|
"title": "Disable avatar animation"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"notifications": {
|
"notifications": {
|
||||||
|
|
|
@ -411,6 +411,7 @@
|
||||||
DB9D6C2E25E504AC0051B173 /* Attachment.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB9D6C2D25E504AC0051B173 /* Attachment.swift */; };
|
DB9D6C2E25E504AC0051B173 /* Attachment.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB9D6C2D25E504AC0051B173 /* Attachment.swift */; };
|
||||||
DB9D6C3825E508BE0051B173 /* Attachment.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB9D6C3725E508BE0051B173 /* Attachment.swift */; };
|
DB9D6C3825E508BE0051B173 /* Attachment.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB9D6C3725E508BE0051B173 /* Attachment.swift */; };
|
||||||
DB9E0D6F25EE008500CFDD76 /* UIInterpolatingMotionEffect.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB9E0D6E25EE008500CFDD76 /* UIInterpolatingMotionEffect.swift */; };
|
DB9E0D6F25EE008500CFDD76 /* UIInterpolatingMotionEffect.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB9E0D6E25EE008500CFDD76 /* UIInterpolatingMotionEffect.swift */; };
|
||||||
|
DBA088DF26958164003EB4B2 /* UserFetchedResultsController.swift in Sources */ = {isa = PBXBuildFile; fileRef = DBA088DE26958164003EB4B2 /* UserFetchedResultsController.swift */; };
|
||||||
DBA0A10925FB3C2B0079C110 /* RoundedEdgesButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = DBA0A10825FB3C2B0079C110 /* RoundedEdgesButton.swift */; };
|
DBA0A10925FB3C2B0079C110 /* RoundedEdgesButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = DBA0A10825FB3C2B0079C110 /* RoundedEdgesButton.swift */; };
|
||||||
DBA0A11325FB3FC10079C110 /* ComposeToolbarView.swift in Sources */ = {isa = PBXBuildFile; fileRef = DBA0A11225FB3FC10079C110 /* ComposeToolbarView.swift */; };
|
DBA0A11325FB3FC10079C110 /* ComposeToolbarView.swift in Sources */ = {isa = PBXBuildFile; fileRef = DBA0A11225FB3FC10079C110 /* ComposeToolbarView.swift */; };
|
||||||
DBA1DB80268F84F80052DB59 /* NotificationType.swift in Sources */ = {isa = PBXBuildFile; fileRef = DBA1DB7F268F84F80052DB59 /* NotificationType.swift */; };
|
DBA1DB80268F84F80052DB59 /* NotificationType.swift in Sources */ = {isa = PBXBuildFile; fileRef = DBA1DB7F268F84F80052DB59 /* NotificationType.swift */; };
|
||||||
|
@ -1039,6 +1040,7 @@
|
||||||
DB9D6C2D25E504AC0051B173 /* Attachment.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Attachment.swift; sourceTree = "<group>"; };
|
DB9D6C2D25E504AC0051B173 /* Attachment.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Attachment.swift; sourceTree = "<group>"; };
|
||||||
DB9D6C3725E508BE0051B173 /* Attachment.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Attachment.swift; sourceTree = "<group>"; };
|
DB9D6C3725E508BE0051B173 /* Attachment.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Attachment.swift; sourceTree = "<group>"; };
|
||||||
DB9E0D6E25EE008500CFDD76 /* UIInterpolatingMotionEffect.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UIInterpolatingMotionEffect.swift; sourceTree = "<group>"; };
|
DB9E0D6E25EE008500CFDD76 /* UIInterpolatingMotionEffect.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UIInterpolatingMotionEffect.swift; sourceTree = "<group>"; };
|
||||||
|
DBA088DE26958164003EB4B2 /* UserFetchedResultsController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserFetchedResultsController.swift; sourceTree = "<group>"; };
|
||||||
DBA0A10825FB3C2B0079C110 /* RoundedEdgesButton.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RoundedEdgesButton.swift; sourceTree = "<group>"; };
|
DBA0A10825FB3C2B0079C110 /* RoundedEdgesButton.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RoundedEdgesButton.swift; sourceTree = "<group>"; };
|
||||||
DBA0A11225FB3FC10079C110 /* ComposeToolbarView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ComposeToolbarView.swift; sourceTree = "<group>"; };
|
DBA0A11225FB3FC10079C110 /* ComposeToolbarView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ComposeToolbarView.swift; sourceTree = "<group>"; };
|
||||||
DBA1DB7F268F84F80052DB59 /* NotificationType.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NotificationType.swift; sourceTree = "<group>"; };
|
DBA1DB7F268F84F80052DB59 /* NotificationType.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NotificationType.swift; sourceTree = "<group>"; };
|
||||||
|
@ -2607,6 +2609,7 @@
|
||||||
isa = PBXGroup;
|
isa = PBXGroup;
|
||||||
children = (
|
children = (
|
||||||
DBCBED1C26132E1A00B49291 /* StatusFetchedResultsController.swift */,
|
DBCBED1C26132E1A00B49291 /* StatusFetchedResultsController.swift */,
|
||||||
|
DBA088DE26958164003EB4B2 /* UserFetchedResultsController.swift */,
|
||||||
DB6D9F75263587C7008423CD /* SettingFetchedResultController.swift */,
|
DB6D9F75263587C7008423CD /* SettingFetchedResultController.swift */,
|
||||||
);
|
);
|
||||||
path = FetchedResultsController;
|
path = FetchedResultsController;
|
||||||
|
@ -3256,6 +3259,7 @@
|
||||||
DBA9443E265CFA6400C537E1 /* ProfileFieldCollectionViewCell.swift in Sources */,
|
DBA9443E265CFA6400C537E1 /* ProfileFieldCollectionViewCell.swift in Sources */,
|
||||||
2D24E1232626ED9D00A59D4F /* UIView+Gesture.swift in Sources */,
|
2D24E1232626ED9D00A59D4F /* UIView+Gesture.swift in Sources */,
|
||||||
DB45FAE325CA7181005A8AC7 /* MastodonUser.swift in Sources */,
|
DB45FAE325CA7181005A8AC7 /* MastodonUser.swift in Sources */,
|
||||||
|
DBA088DF26958164003EB4B2 /* UserFetchedResultsController.swift in Sources */,
|
||||||
DB2FF510260B113300ADA9FE /* ComposeStatusPollExpiresOptionCollectionViewCell.swift in Sources */,
|
DB2FF510260B113300ADA9FE /* ComposeStatusPollExpiresOptionCollectionViewCell.swift in Sources */,
|
||||||
0F202213261351F5000C64BF /* APIService+HashtagTimeline.swift in Sources */,
|
0F202213261351F5000C64BF /* APIService+HashtagTimeline.swift in Sources */,
|
||||||
DB0AC6FC25CD02E600D75117 /* APIService+Instance.swift in Sources */,
|
DB0AC6FC25CD02E600D75117 /* APIService+Instance.swift in Sources */,
|
||||||
|
|
|
@ -12,7 +12,7 @@
|
||||||
<key>CoreDataStack.xcscheme_^#shared#^_</key>
|
<key>CoreDataStack.xcscheme_^#shared#^_</key>
|
||||||
<dict>
|
<dict>
|
||||||
<key>orderHint</key>
|
<key>orderHint</key>
|
||||||
<integer>21</integer>
|
<integer>20</integer>
|
||||||
</dict>
|
</dict>
|
||||||
<key>Mastodon - ASDK.xcscheme_^#shared#^_</key>
|
<key>Mastodon - ASDK.xcscheme_^#shared#^_</key>
|
||||||
<dict>
|
<dict>
|
||||||
|
@ -37,7 +37,7 @@
|
||||||
<key>NotificationService.xcscheme_^#shared#^_</key>
|
<key>NotificationService.xcscheme_^#shared#^_</key>
|
||||||
<dict>
|
<dict>
|
||||||
<key>orderHint</key>
|
<key>orderHint</key>
|
||||||
<integer>20</integer>
|
<integer>21</integer>
|
||||||
</dict>
|
</dict>
|
||||||
</dict>
|
</dict>
|
||||||
<key>SuppressBuildableAutocreation</key>
|
<key>SuppressBuildableAutocreation</key>
|
||||||
|
|
|
@ -0,0 +1,87 @@
|
||||||
|
//
|
||||||
|
// UserFetchedResultsController.swift
|
||||||
|
// Mastodon
|
||||||
|
//
|
||||||
|
// Created by MainasuK Cirno on 2021-7-7.
|
||||||
|
//
|
||||||
|
|
||||||
|
import os.log
|
||||||
|
import UIKit
|
||||||
|
import Combine
|
||||||
|
import CoreData
|
||||||
|
import CoreDataStack
|
||||||
|
import MastodonSDK
|
||||||
|
|
||||||
|
final class UserFetchedResultsController: NSObject {
|
||||||
|
|
||||||
|
var disposeBag = Set<AnyCancellable>()
|
||||||
|
|
||||||
|
let fetchedResultsController: NSFetchedResultsController<MastodonUser>
|
||||||
|
|
||||||
|
// input
|
||||||
|
let domain = CurrentValueSubject<String?, Never>(nil)
|
||||||
|
let userIDs = CurrentValueSubject<[Mastodon.Entity.Account.ID], Never>([])
|
||||||
|
|
||||||
|
// output
|
||||||
|
let objectIDs = CurrentValueSubject<[NSManagedObjectID], Never>([])
|
||||||
|
|
||||||
|
init(managedObjectContext: NSManagedObjectContext, domain: String?, additionalTweetPredicate: NSPredicate?) {
|
||||||
|
self.domain.value = domain ?? ""
|
||||||
|
self.fetchedResultsController = {
|
||||||
|
let fetchRequest = MastodonUser.sortedFetchRequest
|
||||||
|
fetchRequest.predicate = MastodonUser.predicate(domain: domain ?? "", ids: [])
|
||||||
|
fetchRequest.returnsObjectsAsFaults = false
|
||||||
|
fetchRequest.fetchBatchSize = 20
|
||||||
|
let controller = NSFetchedResultsController(
|
||||||
|
fetchRequest: fetchRequest,
|
||||||
|
managedObjectContext: managedObjectContext,
|
||||||
|
sectionNameKeyPath: nil,
|
||||||
|
cacheName: nil
|
||||||
|
)
|
||||||
|
|
||||||
|
return controller
|
||||||
|
}()
|
||||||
|
super.init()
|
||||||
|
|
||||||
|
fetchedResultsController.delegate = self
|
||||||
|
|
||||||
|
Publishers.CombineLatest(
|
||||||
|
self.domain.removeDuplicates().eraseToAnyPublisher(),
|
||||||
|
self.userIDs.removeDuplicates().eraseToAnyPublisher()
|
||||||
|
)
|
||||||
|
.receive(on: DispatchQueue.main)
|
||||||
|
.sink { [weak self] domain, ids in
|
||||||
|
guard let self = self else { return }
|
||||||
|
var predicates = [MastodonUser.predicate(domain: domain ?? "", ids: ids)]
|
||||||
|
if let additionalPredicate = additionalTweetPredicate {
|
||||||
|
predicates.append(additionalPredicate)
|
||||||
|
}
|
||||||
|
self.fetchedResultsController.fetchRequest.predicate = NSCompoundPredicate(andPredicateWithSubpredicates: predicates)
|
||||||
|
do {
|
||||||
|
try self.fetchedResultsController.performFetch()
|
||||||
|
} catch {
|
||||||
|
assertionFailure(error.localizedDescription)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.store(in: &disposeBag)
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
// MARK: - NSFetchedResultsControllerDelegate
|
||||||
|
extension UserFetchedResultsController: NSFetchedResultsControllerDelegate {
|
||||||
|
func controller(_ controller: NSFetchedResultsController<NSFetchRequestResult>, didChangeContentWith snapshot: NSDiffableDataSourceSnapshotReference) {
|
||||||
|
os_log("%{public}s[%{public}ld], %{public}s", ((#file as NSString).lastPathComponent), #line, #function)
|
||||||
|
|
||||||
|
let indexes = userIDs.value
|
||||||
|
let objects = fetchedResultsController.fetchedObjects ?? []
|
||||||
|
|
||||||
|
let items: [NSManagedObjectID] = objects
|
||||||
|
.compactMap { object in
|
||||||
|
indexes.firstIndex(of: object.id).map { index in (index, object) }
|
||||||
|
}
|
||||||
|
.sorted { $0.0 < $1.0 }
|
||||||
|
.map { $0.1.objectID }
|
||||||
|
self.objectIDs.value = items
|
||||||
|
}
|
||||||
|
}
|
|
@ -11,6 +11,7 @@ import CoreData
|
||||||
enum SettingsItem: Hashable {
|
enum SettingsItem: Hashable {
|
||||||
case appearance(settingObjectID: NSManagedObjectID)
|
case appearance(settingObjectID: NSManagedObjectID)
|
||||||
case appearanceDarkMode(settingObjectID: NSManagedObjectID)
|
case appearanceDarkMode(settingObjectID: NSManagedObjectID)
|
||||||
|
case appearanceDisableAvatarAnimation(settingObjectID: NSManagedObjectID)
|
||||||
case notification(settingObjectID: NSManagedObjectID, switchMode: NotificationSwitchMode)
|
case notification(settingObjectID: NSManagedObjectID, switchMode: NotificationSwitchMode)
|
||||||
case boringZone(item: Link)
|
case boringZone(item: Link)
|
||||||
case spicyZone(item: Link)
|
case spicyZone(item: Link)
|
||||||
|
|
|
@ -74,11 +74,12 @@ extension MastodonUser {
|
||||||
}
|
}
|
||||||
|
|
||||||
public func avatarImageURL() -> URL? {
|
public func avatarImageURL() -> URL? {
|
||||||
return URL(string: avatar)
|
let string = UserDefaults.shared.preferredStaticAvatar ? avatarStatic ?? avatar : avatar
|
||||||
|
return URL(string: string)
|
||||||
}
|
}
|
||||||
|
|
||||||
public func avatarImageURLWithFallback(domain: String) -> URL {
|
public func avatarImageURLWithFallback(domain: String) -> URL {
|
||||||
return URL(string: avatar) ?? URL(string: "https://\(domain)/avatars/original/missing.png")!
|
return avatarImageURL() ?? URL(string: "https://\(domain)/avatars/original/missing.png")!
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -5,6 +5,7 @@
|
||||||
// Created by xiaojian sun on 2021/4/2.
|
// Created by xiaojian sun on 2021/4/2.
|
||||||
//
|
//
|
||||||
|
|
||||||
|
import UIKit
|
||||||
import MastodonSDK
|
import MastodonSDK
|
||||||
|
|
||||||
extension Mastodon.Entity.Account: Hashable {
|
extension Mastodon.Entity.Account: Hashable {
|
||||||
|
@ -16,3 +17,14 @@ extension Mastodon.Entity.Account: Hashable {
|
||||||
return lhs.id == rhs.id
|
return lhs.id == rhs.id
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
extension Mastodon.Entity.Account {
|
||||||
|
public func avatarImageURL() -> URL? {
|
||||||
|
let string = UserDefaults.shared.preferredStaticAvatar ? avatarStatic ?? avatar : avatar
|
||||||
|
return URL(string: string)
|
||||||
|
}
|
||||||
|
|
||||||
|
public func avatarImageURLWithFallback(domain: String) -> URL {
|
||||||
|
return avatarImageURL() ?? URL(string: "https://\(domain)/avatars/original/missing.png")!
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -934,6 +934,10 @@ internal enum L10n {
|
||||||
internal static let title = L10n.tr("Localizable", "Scene.Settings.Section.Appearance.Title")
|
internal static let title = L10n.tr("Localizable", "Scene.Settings.Section.Appearance.Title")
|
||||||
}
|
}
|
||||||
internal enum AppearanceSettings {
|
internal enum AppearanceSettings {
|
||||||
|
internal enum AvatarAnimation {
|
||||||
|
/// Disable avatar animation
|
||||||
|
internal static let title = L10n.tr("Localizable", "Scene.Settings.Section.AppearanceSettings.AvatarAnimation.Title")
|
||||||
|
}
|
||||||
internal enum DarkMode {
|
internal enum DarkMode {
|
||||||
/// True black Dark Mode
|
/// True black Dark Mode
|
||||||
internal static let title = L10n.tr("Localizable", "Scene.Settings.Section.AppearanceSettings.DarkMode.Title")
|
internal static let title = L10n.tr("Localizable", "Scene.Settings.Section.AppearanceSettings.DarkMode.Title")
|
||||||
|
|
|
@ -17,4 +17,12 @@ extension UserDefaults {
|
||||||
set { self[#function] = newValue.rawValue }
|
set { self[#function] = newValue.rawValue }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@objc dynamic var preferredStaticAvatar: Bool {
|
||||||
|
get {
|
||||||
|
register(defaults: [#function: false])
|
||||||
|
return bool(forKey: #function)
|
||||||
|
}
|
||||||
|
set { self[#function] = newValue }
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -317,6 +317,7 @@ any server.";
|
||||||
"Scene.Settings.Section.Appearance.Dark" = "Always Dark";
|
"Scene.Settings.Section.Appearance.Dark" = "Always Dark";
|
||||||
"Scene.Settings.Section.Appearance.Light" = "Always Light";
|
"Scene.Settings.Section.Appearance.Light" = "Always Light";
|
||||||
"Scene.Settings.Section.Appearance.Title" = "Appearance";
|
"Scene.Settings.Section.Appearance.Title" = "Appearance";
|
||||||
|
"Scene.Settings.Section.AppearanceSettings.AvatarAnimation.Title" = "Disable avatar animation";
|
||||||
"Scene.Settings.Section.AppearanceSettings.DarkMode.Title" = "True black Dark Mode";
|
"Scene.Settings.Section.AppearanceSettings.DarkMode.Title" = "True black Dark Mode";
|
||||||
"Scene.Settings.Section.Boringzone.Privacy" = "Privacy Policy";
|
"Scene.Settings.Section.Boringzone.Privacy" = "Privacy Policy";
|
||||||
"Scene.Settings.Section.Boringzone.Terms" = "Terms of Service";
|
"Scene.Settings.Section.Boringzone.Terms" = "Terms of Service";
|
||||||
|
|
|
@ -317,6 +317,7 @@ any server.";
|
||||||
"Scene.Settings.Section.Appearance.Dark" = "Always Dark";
|
"Scene.Settings.Section.Appearance.Dark" = "Always Dark";
|
||||||
"Scene.Settings.Section.Appearance.Light" = "Always Light";
|
"Scene.Settings.Section.Appearance.Light" = "Always Light";
|
||||||
"Scene.Settings.Section.Appearance.Title" = "Appearance";
|
"Scene.Settings.Section.Appearance.Title" = "Appearance";
|
||||||
|
"Scene.Settings.Section.AppearanceSettings.AvatarAnimation.Title" = "Disable avatar animation";
|
||||||
"Scene.Settings.Section.AppearanceSettings.DarkMode.Title" = "True black Dark Mode";
|
"Scene.Settings.Section.AppearanceSettings.DarkMode.Title" = "True black Dark Mode";
|
||||||
"Scene.Settings.Section.Boringzone.Privacy" = "Privacy Policy";
|
"Scene.Settings.Section.Boringzone.Privacy" = "Privacy Policy";
|
||||||
"Scene.Settings.Section.Boringzone.Terms" = "Terms of Service";
|
"Scene.Settings.Section.Boringzone.Terms" = "Terms of Service";
|
||||||
|
|
|
@ -40,6 +40,8 @@ final class SearchViewModel: NSObject {
|
||||||
var accountDiffableDataSource: UICollectionViewDiffableDataSource<RecommendAccountSection, NSManagedObjectID>?
|
var accountDiffableDataSource: UICollectionViewDiffableDataSource<RecommendAccountSection, NSManagedObjectID>?
|
||||||
var searchResultDiffableDataSource: UITableViewDiffableDataSource<SearchResultSection, SearchResultItem>?
|
var searchResultDiffableDataSource: UITableViewDiffableDataSource<SearchResultSection, SearchResultItem>?
|
||||||
|
|
||||||
|
let statusFetchedResultsController: StatusFetchedResultsController
|
||||||
|
|
||||||
// bottom loader
|
// bottom loader
|
||||||
private(set) lazy var loadoldestStateMachine: GKStateMachine = {
|
private(set) lazy var loadoldestStateMachine: GKStateMachine = {
|
||||||
// exclude timeline middle fetcher state
|
// exclude timeline middle fetcher state
|
||||||
|
@ -59,6 +61,11 @@ final class SearchViewModel: NSObject {
|
||||||
init(context: AppContext, coordinator: SceneCoordinator) {
|
init(context: AppContext, coordinator: SceneCoordinator) {
|
||||||
self.coordinator = coordinator
|
self.coordinator = coordinator
|
||||||
self.context = context
|
self.context = context
|
||||||
|
self.statusFetchedResultsController = StatusFetchedResultsController(
|
||||||
|
managedObjectContext: context.managedObjectContext,
|
||||||
|
domain: nil,
|
||||||
|
additionalTweetPredicate: nil
|
||||||
|
)
|
||||||
super.init()
|
super.init()
|
||||||
|
|
||||||
// bind active authentication
|
// bind active authentication
|
||||||
|
@ -70,6 +77,7 @@ final class SearchViewModel: NSObject {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
self.currentMastodonUser.value = activeMastodonAuthentication.user
|
self.currentMastodonUser.value = activeMastodonAuthentication.user
|
||||||
|
self.statusFetchedResultsController.domain.value = activeMastodonAuthentication.domain
|
||||||
}
|
}
|
||||||
.store(in: &disposeBag)
|
.store(in: &disposeBag)
|
||||||
|
|
||||||
|
@ -224,7 +232,7 @@ final class SearchViewModel: NSObject {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.store(in: &disposeBag)
|
.store(in: &disposeBag)
|
||||||
|
|
||||||
searchResult
|
searchResult
|
||||||
.receive(on: DispatchQueue.main)
|
.receive(on: DispatchQueue.main)
|
||||||
.sink { [weak self] searchResult in
|
.sink { [weak self] searchResult in
|
||||||
|
@ -343,7 +351,7 @@ final class SearchViewModel: NSObject {
|
||||||
DispatchQueue.main.async {
|
DispatchQueue.main.async {
|
||||||
self.coordinator.present(scene: .profile(viewModel: viewModel), from: from, transition: .show)
|
self.coordinator.present(scene: .profile(viewModel: viewModel), from: from, transition: .show)
|
||||||
}
|
}
|
||||||
|
|
||||||
case .hashtag(let tag):
|
case .hashtag(let tag):
|
||||||
let (tagInCoreData, _) = APIService.CoreData.createOrMergeTag(into: self.context.managedObjectContext, entity: tag)
|
let (tagInCoreData, _) = APIService.CoreData.createOrMergeTag(into: self.context.managedObjectContext, entity: tag)
|
||||||
if let searchHistories = searchHistories {
|
if let searchHistories = searchHistories {
|
||||||
|
|
|
@ -10,10 +10,13 @@ import CoreDataStack
|
||||||
import Foundation
|
import Foundation
|
||||||
import MastodonSDK
|
import MastodonSDK
|
||||||
import UIKit
|
import UIKit
|
||||||
|
import FLAnimatedImage
|
||||||
|
import Nuke
|
||||||
|
|
||||||
final class SearchingTableViewCell: UITableViewCell {
|
final class SearchingTableViewCell: UITableViewCell {
|
||||||
|
|
||||||
let _imageView: UIImageView = {
|
let _imageView: UIImageView = {
|
||||||
let imageView = UIImageView()
|
let imageView = FLAnimatedImageView()
|
||||||
imageView.tintColor = Asset.Colors.Label.primary.color
|
imageView.tintColor = Asset.Colors.Label.primary.color
|
||||||
imageView.layer.cornerRadius = 4
|
imageView.layer.cornerRadius = 4
|
||||||
imageView.clipsToBounds = true
|
imageView.clipsToBounds = true
|
||||||
|
@ -37,8 +40,7 @@ final class SearchingTableViewCell: UITableViewCell {
|
||||||
|
|
||||||
override func prepareForReuse() {
|
override func prepareForReuse() {
|
||||||
super.prepareForReuse()
|
super.prepareForReuse()
|
||||||
_imageView.af.cancelImageRequest()
|
Nuke.cancelRequest(for: _imageView)
|
||||||
_imageView.image = nil
|
|
||||||
}
|
}
|
||||||
|
|
||||||
override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) {
|
override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) {
|
||||||
|
@ -90,22 +92,28 @@ extension SearchingTableViewCell {
|
||||||
}
|
}
|
||||||
|
|
||||||
func config(with account: Mastodon.Entity.Account) {
|
func config(with account: Mastodon.Entity.Account) {
|
||||||
_imageView.af.setImage(
|
Nuke.loadImage(
|
||||||
withURL: URL(string: account.avatar)!,
|
with: account.avatarImageURL(),
|
||||||
placeholderImage: UIImage.placeholder(color: .systemFill),
|
options: ImageLoadingOptions(
|
||||||
imageTransition: .crossDissolve(0.2)
|
placeholder: UIImage.placeholder(color: .systemFill),
|
||||||
|
transition: .fadeIn(duration: 0.2)
|
||||||
|
),
|
||||||
|
into: _imageView
|
||||||
)
|
)
|
||||||
_titleLabel.text = account.displayName.isEmpty ? account.username : account.displayName
|
_titleLabel.text = account.displayName.isEmpty ? account.username : account.displayName
|
||||||
_subTitleLabel.text = account.acct
|
_subTitleLabel.text = account.acct
|
||||||
}
|
}
|
||||||
|
|
||||||
func config(with account: MastodonUser) {
|
func config(with account: MastodonUser) {
|
||||||
_imageView.af.setImage(
|
Nuke.loadImage(
|
||||||
withURL: URL(string: account.avatar)!,
|
with: account.avatarImageURL(),
|
||||||
placeholderImage: UIImage.placeholder(color: .systemFill),
|
options: ImageLoadingOptions(
|
||||||
imageTransition: .crossDissolve(0.2)
|
placeholder: UIImage.placeholder(color: .systemFill),
|
||||||
|
transition: .fadeIn(duration: 0.2)
|
||||||
|
),
|
||||||
|
into: _imageView
|
||||||
)
|
)
|
||||||
_titleLabel.text = account.displayName.isEmpty ? account.username : account.displayName
|
_titleLabel.text = account.displayNameWithFallback
|
||||||
_subTitleLabel.text = account.acct
|
_subTitleLabel.text = account.acct
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -122,7 +130,7 @@ extension SearchingTableViewCell {
|
||||||
let string = L10n.Scene.Search.Recommend.HashTag.peopleTalking(String(peopleAreTalking))
|
let string = L10n.Scene.Search.Recommend.HashTag.peopleTalking(String(peopleAreTalking))
|
||||||
_subTitleLabel.text = string
|
_subTitleLabel.text = string
|
||||||
}
|
}
|
||||||
|
|
||||||
func config(with tag: Tag) {
|
func config(with tag: Tag) {
|
||||||
let image = UIImage(systemName: "number.circle.fill", withConfiguration: UIImage.SymbolConfiguration(pointSize: 34, weight: .regular))!.withRenderingMode(.alwaysTemplate)
|
let image = UIImage(systemName: "number.circle.fill", withConfiguration: UIImage.SymbolConfiguration(pointSize: 34, weight: .regular))!.withRenderingMode(.alwaysTemplate)
|
||||||
_imageView.image = image
|
_imageView.image = image
|
||||||
|
|
|
@ -356,7 +356,7 @@ extension SettingsViewController: UITableViewDelegate {
|
||||||
case .appearance:
|
case .appearance:
|
||||||
// do nothing
|
// do nothing
|
||||||
break
|
break
|
||||||
case .appearanceDarkMode:
|
case .appearanceDarkMode, .appearanceDisableAvatarAnimation:
|
||||||
// do nothing
|
// do nothing
|
||||||
break
|
break
|
||||||
case .notification:
|
case .notification:
|
||||||
|
@ -443,10 +443,12 @@ extension SettingsViewController: SettingsToggleCellDelegate {
|
||||||
func settingsToggleCell(_ cell: SettingsToggleTableViewCell, switchValueDidChange switch: UISwitch) {
|
func settingsToggleCell(_ cell: SettingsToggleTableViewCell, switchValueDidChange switch: UISwitch) {
|
||||||
guard let dataSource = viewModel.dataSource else { return }
|
guard let dataSource = viewModel.dataSource else { return }
|
||||||
guard let indexPath = tableView.indexPath(for: cell) else { return }
|
guard let indexPath = tableView.indexPath(for: cell) else { return }
|
||||||
|
|
||||||
|
let isOn = `switch`.isOn
|
||||||
let item = dataSource.itemIdentifier(for: indexPath)
|
let item = dataSource.itemIdentifier(for: indexPath)
|
||||||
|
|
||||||
switch item {
|
switch item {
|
||||||
case .appearanceDarkMode(let settingObjectID):
|
case .appearanceDarkMode(let settingObjectID):
|
||||||
let isOn = `switch`.isOn
|
|
||||||
let managedObjectContext = context.backgroundManagedObjectContext
|
let managedObjectContext = context.backgroundManagedObjectContext
|
||||||
managedObjectContext.performChanges {
|
managedObjectContext.performChanges {
|
||||||
let setting = managedObjectContext.object(with: settingObjectID) as! Setting
|
let setting = managedObjectContext.object(with: settingObjectID) as! Setting
|
||||||
|
@ -462,8 +464,23 @@ extension SettingsViewController: SettingsToggleCellDelegate {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.store(in: &disposeBag)
|
.store(in: &disposeBag)
|
||||||
|
case .appearanceDisableAvatarAnimation(let settingObjectID):
|
||||||
|
let managedObjectContext = context.backgroundManagedObjectContext
|
||||||
|
managedObjectContext.performChanges {
|
||||||
|
let setting = managedObjectContext.object(with: settingObjectID) as! Setting
|
||||||
|
setting.update(preferredStaticAvatar: isOn)
|
||||||
|
}
|
||||||
|
.sink { result in
|
||||||
|
switch result {
|
||||||
|
case .success:
|
||||||
|
UserDefaults.shared.preferredStaticAvatar = isOn
|
||||||
|
case .failure(let error):
|
||||||
|
assertionFailure(error.localizedDescription)
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.store(in: &disposeBag)
|
||||||
case .notification(let settingObjectID, let switchMode):
|
case .notification(let settingObjectID, let switchMode):
|
||||||
let isOn = `switch`.isOn
|
|
||||||
let managedObjectContext = context.backgroundManagedObjectContext
|
let managedObjectContext = context.backgroundManagedObjectContext
|
||||||
managedObjectContext.performChanges {
|
managedObjectContext.performChanges {
|
||||||
let setting = managedObjectContext.object(with: settingObjectID) as! Setting
|
let setting = managedObjectContext.object(with: settingObjectID) as! Setting
|
||||||
|
|
|
@ -96,7 +96,10 @@ extension SettingsViewModel {
|
||||||
snapshot.appendSections([.appearance])
|
snapshot.appendSections([.appearance])
|
||||||
snapshot.appendItems(appearanceItems, toSection: .appearance)
|
snapshot.appendItems(appearanceItems, toSection: .appearance)
|
||||||
|
|
||||||
let appearanceSettingItems = [SettingsItem.appearanceDarkMode(settingObjectID: setting.objectID)]
|
let appearanceSettingItems = [
|
||||||
|
SettingsItem.appearanceDarkMode(settingObjectID: setting.objectID),
|
||||||
|
SettingsItem.appearanceDisableAvatarAnimation(settingObjectID: setting.objectID)
|
||||||
|
]
|
||||||
snapshot.appendSections([.appearanceSettings])
|
snapshot.appendSections([.appearanceSettings])
|
||||||
snapshot.appendItems(appearanceSettingItems, toSection: .appearanceSettings)
|
snapshot.appendItems(appearanceSettingItems, toSection: .appearanceSettings)
|
||||||
|
|
||||||
|
@ -146,7 +149,6 @@ extension SettingsViewModel {
|
||||||
weak settingsToggleCellDelegate
|
weak settingsToggleCellDelegate
|
||||||
] tableView, indexPath, item -> UITableViewCell? in
|
] tableView, indexPath, item -> UITableViewCell? in
|
||||||
guard let self = self else { return nil }
|
guard let self = self else { return nil }
|
||||||
|
|
||||||
switch item {
|
switch item {
|
||||||
case .appearance(let objectID):
|
case .appearance(let objectID):
|
||||||
let cell = tableView.dequeueReusableCell(withIdentifier: String(describing: SettingsAppearanceTableViewCell.self), for: indexPath) as! SettingsAppearanceTableViewCell
|
let cell = tableView.dequeueReusableCell(withIdentifier: String(describing: SettingsAppearanceTableViewCell.self), for: indexPath) as! SettingsAppearanceTableViewCell
|
||||||
|
@ -167,13 +169,14 @@ extension SettingsViewModel {
|
||||||
}
|
}
|
||||||
cell.delegate = settingsAppearanceTableViewCellDelegate
|
cell.delegate = settingsAppearanceTableViewCellDelegate
|
||||||
return cell
|
return cell
|
||||||
case .appearanceDarkMode(let objectID):
|
case .appearanceDarkMode(let objectID),
|
||||||
|
.appearanceDisableAvatarAnimation(let objectID):
|
||||||
let cell = tableView.dequeueReusableCell(withIdentifier: String(describing: SettingsToggleTableViewCell.self), for: indexPath) as! SettingsToggleTableViewCell
|
let cell = tableView.dequeueReusableCell(withIdentifier: String(describing: SettingsToggleTableViewCell.self), for: indexPath) as! SettingsToggleTableViewCell
|
||||||
cell.delegate = settingsToggleCellDelegate
|
cell.delegate = settingsToggleCellDelegate
|
||||||
cell.textLabel?.text = L10n.Scene.Settings.Section.AppearanceSettings.DarkMode.title
|
|
||||||
self.context.managedObjectContext.performAndWait {
|
self.context.managedObjectContext.performAndWait {
|
||||||
let setting = self.context.managedObjectContext.object(with: objectID) as! Setting
|
let setting = self.context.managedObjectContext.object(with: objectID) as! Setting
|
||||||
cell.switchButton.isOn = setting.preferredTrueBlackDarkMode
|
SettingsViewModel.configureSettingToggle(cell: cell, item: item, setting: setting)
|
||||||
|
|
||||||
ManagedObjectObserver.observe(object: setting)
|
ManagedObjectObserver.observe(object: setting)
|
||||||
.receive(on: DispatchQueue.main)
|
.receive(on: DispatchQueue.main)
|
||||||
.sink(receiveCompletion: { _ in
|
.sink(receiveCompletion: { _ in
|
||||||
|
@ -182,7 +185,7 @@ extension SettingsViewModel {
|
||||||
guard let cell = cell else { return }
|
guard let cell = cell else { return }
|
||||||
guard case .update(let object) = change.changeType,
|
guard case .update(let object) = change.changeType,
|
||||||
let setting = object as? Setting else { return }
|
let setting = object as? Setting else { return }
|
||||||
cell.switchButton.isOn = setting.preferredTrueBlackDarkMode
|
SettingsViewModel.configureSettingToggle(cell: cell, item: item, setting: setting)
|
||||||
})
|
})
|
||||||
.store(in: &cell.disposeBag)
|
.store(in: &cell.disposeBag)
|
||||||
}
|
}
|
||||||
|
@ -220,6 +223,23 @@ extension SettingsViewModel {
|
||||||
}
|
}
|
||||||
|
|
||||||
extension SettingsViewModel {
|
extension SettingsViewModel {
|
||||||
|
|
||||||
|
static func configureSettingToggle(
|
||||||
|
cell: SettingsToggleTableViewCell,
|
||||||
|
item: SettingsItem,
|
||||||
|
setting: Setting
|
||||||
|
) {
|
||||||
|
switch item {
|
||||||
|
case .appearanceDarkMode:
|
||||||
|
cell.textLabel?.text = L10n.Scene.Settings.Section.AppearanceSettings.DarkMode.title
|
||||||
|
cell.switchButton.isOn = setting.preferredTrueBlackDarkMode
|
||||||
|
case .appearanceDisableAvatarAnimation:
|
||||||
|
cell.textLabel?.text = L10n.Scene.Settings.Section.AppearanceSettings.AvatarAnimation.title
|
||||||
|
cell.switchButton.isOn = setting.preferredStaticAvatar
|
||||||
|
default:
|
||||||
|
assertionFailure()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
static func configureSettingToggle(
|
static func configureSettingToggle(
|
||||||
cell: SettingsToggleTableViewCell,
|
cell: SettingsToggleTableViewCell,
|
||||||
|
|
|
@ -95,10 +95,8 @@ final class StatusView: UIView {
|
||||||
view.accessibilityLabel = L10n.Common.Controls.Status.showUserProfile
|
view.accessibilityLabel = L10n.Common.Controls.Status.showUserProfile
|
||||||
return view
|
return view
|
||||||
}()
|
}()
|
||||||
let avatarImageView: UIImageView = {
|
let avatarImageView: FLAnimatedImageView = {
|
||||||
let imageView = FLAnimatedImageView()
|
let imageView = FLAnimatedImageView()
|
||||||
// imageView.layer.shouldRasterize = true
|
|
||||||
// imageView.layer.rasterizationScale = UIScreen.main.scale
|
|
||||||
return imageView
|
return imageView
|
||||||
}()
|
}()
|
||||||
let avatarStackedContainerButton: AvatarStackContainerButton = AvatarStackContainerButton()
|
let avatarStackedContainerButton: AvatarStackContainerButton = AvatarStackContainerButton()
|
||||||
|
@ -480,6 +478,9 @@ extension StatusView {
|
||||||
avatarStackedContainerButton.addTarget(self, action: #selector(StatusView.avatarStackedContainerButtonDidPressed(_:)), for: .touchUpInside)
|
avatarStackedContainerButton.addTarget(self, action: #selector(StatusView.avatarStackedContainerButtonDidPressed(_:)), for: .touchUpInside)
|
||||||
revealContentWarningButton.addTarget(self, action: #selector(StatusView.revealContentWarningButtonDidPressed(_:)), for: .touchUpInside)
|
revealContentWarningButton.addTarget(self, action: #selector(StatusView.revealContentWarningButtonDidPressed(_:)), for: .touchUpInside)
|
||||||
pollVoteButton.addTarget(self, action: #selector(StatusView.pollVoteButtonPressed(_:)), for: .touchUpInside)
|
pollVoteButton.addTarget(self, action: #selector(StatusView.pollVoteButtonPressed(_:)), for: .touchUpInside)
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -81,6 +81,26 @@ final class AuthenticationService: NSObject {
|
||||||
.assign(to: \.value, on: activeMastodonAuthenticationBox)
|
.assign(to: \.value, on: activeMastodonAuthenticationBox)
|
||||||
.store(in: &disposeBag)
|
.store(in: &disposeBag)
|
||||||
|
|
||||||
|
activeMastodonAuthenticationBox
|
||||||
|
.receive(on: RunLoop.main)
|
||||||
|
.sink { [weak self] authenticationBox in
|
||||||
|
guard let _ = self else { return }
|
||||||
|
guard let authenticationBox = authenticationBox else { return }
|
||||||
|
let request = Setting.sortedFetchRequest
|
||||||
|
request.predicate = Setting.predicate(domain: authenticationBox.domain, userID: authenticationBox.userID)
|
||||||
|
guard let setting = managedObjectContext.safeFetch(request).first else { return }
|
||||||
|
|
||||||
|
let themeName: ThemeName = setting.preferredTrueBlackDarkMode ? .system : .mastodon
|
||||||
|
if UserDefaults.shared.currentThemeNameRawValue != themeName.rawValue {
|
||||||
|
ThemeService.shared.set(themeName: themeName)
|
||||||
|
os_log(.info, log: .debug, "%{public}s[%{public}ld], %{public}s: update theme style", ((#file as NSString).lastPathComponent), #line, #function)
|
||||||
|
}
|
||||||
|
if UserDefaults.shared.preferredStaticAvatar != setting.preferredStaticAvatar {
|
||||||
|
UserDefaults.shared.preferredStaticAvatar = setting.preferredStaticAvatar
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.store(in: &disposeBag)
|
||||||
|
|
||||||
do {
|
do {
|
||||||
try mastodonAuthenticationFetchedResultsController.performFetch()
|
try mastodonAuthenticationFetchedResultsController.performFetch()
|
||||||
mastodonAuthentications.value = mastodonAuthenticationFetchedResultsController.fetchedObjects ?? []
|
mastodonAuthentications.value = mastodonAuthenticationFetchedResultsController.fetchedObjects ?? []
|
||||||
|
|
Loading…
Reference in New Issue