mirror of
https://github.com/metabolist/metatext.git
synced 2024-11-22 08:10:59 +00:00
Autoplay
This commit is contained in:
parent
66bd3c78b9
commit
fe6aa0f115
29 changed files with 645 additions and 147 deletions
27
Caches/PlayerCache.swift
Normal file
27
Caches/PlayerCache.swift
Normal file
|
@ -0,0 +1,27 @@
|
||||||
|
// Copyright © 2020 Metabolist. All rights reserved.
|
||||||
|
|
||||||
|
import AVKit
|
||||||
|
|
||||||
|
final class PlayerCache {
|
||||||
|
private let cache = NSCache<NSURL, AVQueuePlayer>()
|
||||||
|
private var allURLsCached = Set<URL>()
|
||||||
|
|
||||||
|
private init() {}
|
||||||
|
}
|
||||||
|
|
||||||
|
extension PlayerCache {
|
||||||
|
static let shared = PlayerCache()
|
||||||
|
|
||||||
|
func player(url: URL) -> AVQueuePlayer {
|
||||||
|
if let player = cache.object(forKey: url as NSURL) {
|
||||||
|
return player
|
||||||
|
}
|
||||||
|
|
||||||
|
let player = AVQueuePlayer(url: url)
|
||||||
|
|
||||||
|
cache.setObject(player, forKey: url as NSURL)
|
||||||
|
allURLsCached.insert(url)
|
||||||
|
|
||||||
|
return player
|
||||||
|
}
|
||||||
|
}
|
|
@ -3,18 +3,18 @@
|
||||||
import UIKit
|
import UIKit
|
||||||
import ViewModels
|
import ViewModels
|
||||||
|
|
||||||
class TableViewDataSource: UITableViewDiffableDataSource<Int, CollectionItemIdentifier> {
|
final class TableViewDataSource: UITableViewDiffableDataSource<Int, CollectionItem> {
|
||||||
private let updateQueue =
|
private let updateQueue =
|
||||||
DispatchQueue(label: "com.metabolist.metatext.collection-data-source.update-queue")
|
DispatchQueue(label: "com.metabolist.metatext.collection-data-source.update-queue")
|
||||||
|
|
||||||
init(tableView: UITableView, viewModelProvider: @escaping (IndexPath) -> CollectionItemViewModel) {
|
init(tableView: UITableView, viewModelProvider: @escaping (IndexPath) -> CollectionItemViewModel) {
|
||||||
for kind in CollectionItemIdentifier.Kind.allCases {
|
for cellClass in CollectionItem.cellClasses {
|
||||||
tableView.register(kind.cellClass, forCellReuseIdentifier: String(describing: kind.cellClass))
|
tableView.register(cellClass, forCellReuseIdentifier: String(describing: cellClass))
|
||||||
}
|
}
|
||||||
|
|
||||||
super.init(tableView: tableView) { tableView, indexPath, identifier in
|
super.init(tableView: tableView) { tableView, indexPath, item in
|
||||||
let cell = tableView.dequeueReusableCell(
|
let cell = tableView.dequeueReusableCell(
|
||||||
withIdentifier: String(describing: identifier.kind.cellClass),
|
withIdentifier: String(describing: item.cellClass),
|
||||||
for: indexPath)
|
for: indexPath)
|
||||||
|
|
||||||
switch (cell, viewModelProvider(indexPath)) {
|
switch (cell, viewModelProvider(indexPath)) {
|
||||||
|
@ -31,4 +31,12 @@ class TableViewDataSource: UITableViewDiffableDataSource<Int, CollectionItemIden
|
||||||
return cell
|
return cell
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override func apply(_ snapshot: NSDiffableDataSourceSnapshot<Int, CollectionItem>,
|
||||||
|
animatingDifferences: Bool = true,
|
||||||
|
completion: (() -> Void)? = nil) {
|
||||||
|
updateQueue.async {
|
||||||
|
super.apply(snapshot, animatingDifferences: animatingDifferences, completion: completion)
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
10
Extensions/AppPreferences+Extensions.swift
Normal file
10
Extensions/AppPreferences+Extensions.swift
Normal file
|
@ -0,0 +1,10 @@
|
||||||
|
// Copyright © 2020 Metabolist. All rights reserved.
|
||||||
|
|
||||||
|
import UIKit
|
||||||
|
import ViewModels
|
||||||
|
|
||||||
|
extension AppPreferences {
|
||||||
|
var shouldReduceMotion: Bool {
|
||||||
|
UIAccessibility.isReduceMotionEnabled && useSystemReduceMotionForMedia
|
||||||
|
}
|
||||||
|
}
|
|
@ -2,7 +2,9 @@
|
||||||
|
|
||||||
import ViewModels
|
import ViewModels
|
||||||
|
|
||||||
extension CollectionItemIdentifier.Kind {
|
extension CollectionItem {
|
||||||
|
static let cellClasses = [StatusListCell.self, AccountListCell.self, LoadMoreCell.self]
|
||||||
|
|
||||||
var cellClass: AnyClass {
|
var cellClass: AnyClass {
|
||||||
switch self {
|
switch self {
|
||||||
case .status:
|
case .status:
|
|
@ -33,6 +33,22 @@
|
||||||
"load-more" = "Load More";
|
"load-more" = "Load More";
|
||||||
"pending.pending-confirmation" = "Your account is pending confirmation";
|
"pending.pending-confirmation" = "Your account is pending confirmation";
|
||||||
"preferences" = "Preferences";
|
"preferences" = "Preferences";
|
||||||
|
"preferences.app" = "App Preferences";
|
||||||
|
"preferences.media" = "Media";
|
||||||
|
"preferences.media.use-system-reduce-motion" = "Use system reduce motion setting";
|
||||||
|
"preferences.media.avatars" = "Avatars";
|
||||||
|
"preferences.media.avatars.animate" = "Animate avatars";
|
||||||
|
"preferences.media.avatars.animate.everywhere" = "Everywhere";
|
||||||
|
"preferences.media.avatars.animate.profiles" = "In profiles";
|
||||||
|
"preferences.media.avatars.animate.never" = "Never";
|
||||||
|
"preferences.media.headers" = "Headers";
|
||||||
|
"preferences.media.headers.animate" = "Animate headers";
|
||||||
|
"preferences.media.autoplay" = "Autoplay";
|
||||||
|
"preferences.media.autoplay.gifs" = "GIFs";
|
||||||
|
"preferences.media.autoplay.videos" = "Videos";
|
||||||
|
"preferences.media.autoplay.always" = "Always";
|
||||||
|
"preferences.media.autoplay.wifi" = "On Wi-Fi";
|
||||||
|
"preferences.media.autoplay.never" = "Never";
|
||||||
"preferences.posting-reading" = "Posting and Reading";
|
"preferences.posting-reading" = "Posting and Reading";
|
||||||
"preferences.posting" = "Posting";
|
"preferences.posting" = "Posting";
|
||||||
"preferences.use-preferences-from-server" = "Use preferences from server";
|
"preferences.use-preferences-from-server" = "Use preferences from server";
|
||||||
|
|
|
@ -14,11 +14,13 @@
|
||||||
D01F41D924F880C400D55A2D /* TouchFallthroughTextView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D01F41D624F880C400D55A2D /* TouchFallthroughTextView.swift */; };
|
D01F41D924F880C400D55A2D /* TouchFallthroughTextView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D01F41D624F880C400D55A2D /* TouchFallthroughTextView.swift */; };
|
||||||
D01F41E424F8889700D55A2D /* StatusAttachmentsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D01F41E224F8889700D55A2D /* StatusAttachmentsView.swift */; };
|
D01F41E424F8889700D55A2D /* StatusAttachmentsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D01F41E224F8889700D55A2D /* StatusAttachmentsView.swift */; };
|
||||||
D02E1F95250B13210071AD56 /* SafariView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D02E1F94250B13210071AD56 /* SafariView.swift */; };
|
D02E1F95250B13210071AD56 /* SafariView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D02E1F94250B13210071AD56 /* SafariView.swift */; };
|
||||||
|
D03B1B2A253818F3008F964B /* MediaPreferencesView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D03B1B29253818F3008F964B /* MediaPreferencesView.swift */; };
|
||||||
D0625E59250F092900502611 /* StatusListCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0625E58250F092900502611 /* StatusListCell.swift */; };
|
D0625E59250F092900502611 /* StatusListCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0625E58250F092900502611 /* StatusListCell.swift */; };
|
||||||
D0625E5D250F0B5C00502611 /* StatusContentConfiguration.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0625E5C250F0B5C00502611 /* StatusContentConfiguration.swift */; };
|
D0625E5D250F0B5C00502611 /* StatusContentConfiguration.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0625E5C250F0B5C00502611 /* StatusContentConfiguration.swift */; };
|
||||||
D06B492324D4611300642749 /* KingfisherSwiftUI in Frameworks */ = {isa = PBXBuildFile; productRef = D06B492224D4611300642749 /* KingfisherSwiftUI */; };
|
D06B492324D4611300642749 /* KingfisherSwiftUI in Frameworks */ = {isa = PBXBuildFile; productRef = D06B492224D4611300642749 /* KingfisherSwiftUI */; };
|
||||||
D06BC5E625202AD90079541D /* ProfileViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D06BC5E525202AD90079541D /* ProfileViewController.swift */; };
|
D06BC5E625202AD90079541D /* ProfileViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D06BC5E525202AD90079541D /* ProfileViewController.swift */; };
|
||||||
D0A1F4F7252E7D4B004435BF /* TableViewDataSource.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0A1F4F6252E7D4B004435BF /* TableViewDataSource.swift */; };
|
D0A1F4F7252E7D4B004435BF /* TableViewDataSource.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0A1F4F6252E7D4B004435BF /* TableViewDataSource.swift */; };
|
||||||
|
D0A3C2F725390A9700739F88 /* AppPreferences+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0A3C2F625390A9700739F88 /* AppPreferences+Extensions.swift */; };
|
||||||
D0B32F50250B373600311912 /* RegistrationView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0B32F4F250B373600311912 /* RegistrationView.swift */; };
|
D0B32F50250B373600311912 /* RegistrationView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0B32F4F250B373600311912 /* RegistrationView.swift */; };
|
||||||
D0B5FE9B251583DB00478838 /* ProfileCollection+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0B5FE9A251583DB00478838 /* ProfileCollection+Extensions.swift */; };
|
D0B5FE9B251583DB00478838 /* ProfileCollection+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0B5FE9A251583DB00478838 /* ProfileCollection+Extensions.swift */; };
|
||||||
D0B8510C25259E56004E0744 /* LoadMoreCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0B8510B25259E56004E0744 /* LoadMoreCell.swift */; };
|
D0B8510C25259E56004E0744 /* LoadMoreCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0B8510B25259E56004E0744 /* LoadMoreCell.swift */; };
|
||||||
|
@ -62,7 +64,9 @@
|
||||||
D0F0B113251A86A000942152 /* AccountContentConfiguration.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0F0B112251A86A000942152 /* AccountContentConfiguration.swift */; };
|
D0F0B113251A86A000942152 /* AccountContentConfiguration.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0F0B112251A86A000942152 /* AccountContentConfiguration.swift */; };
|
||||||
D0F0B126251A90F400942152 /* AccountListCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0F0B125251A90F400942152 /* AccountListCell.swift */; };
|
D0F0B126251A90F400942152 /* AccountListCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0F0B125251A90F400942152 /* AccountListCell.swift */; };
|
||||||
D0F0B12E251A97E400942152 /* TableViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0F0B12D251A97E400942152 /* TableViewController.swift */; };
|
D0F0B12E251A97E400942152 /* TableViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0F0B12D251A97E400942152 /* TableViewController.swift */; };
|
||||||
D0F0B136251AA12700942152 /* CollectionItemKind+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0F0B135251AA12700942152 /* CollectionItemKind+Extensions.swift */; };
|
D0F0B136251AA12700942152 /* CollectionItem+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0F0B135251AA12700942152 /* CollectionItem+Extensions.swift */; };
|
||||||
|
D0FE1C8F253686F9003EF1EB /* PlayerView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0FE1C8E253686F9003EF1EB /* PlayerView.swift */; };
|
||||||
|
D0FE1C9825368A9D003EF1EB /* PlayerCache.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0FE1C9725368A9D003EF1EB /* PlayerCache.swift */; };
|
||||||
/* End PBXBuildFile section */
|
/* End PBXBuildFile section */
|
||||||
|
|
||||||
/* Begin PBXContainerItemProxy section */
|
/* Begin PBXContainerItemProxy section */
|
||||||
|
@ -104,6 +108,7 @@
|
||||||
D01F41D624F880C400D55A2D /* TouchFallthroughTextView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TouchFallthroughTextView.swift; sourceTree = "<group>"; };
|
D01F41D624F880C400D55A2D /* TouchFallthroughTextView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TouchFallthroughTextView.swift; sourceTree = "<group>"; };
|
||||||
D01F41E224F8889700D55A2D /* StatusAttachmentsView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = StatusAttachmentsView.swift; sourceTree = "<group>"; };
|
D01F41E224F8889700D55A2D /* StatusAttachmentsView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = StatusAttachmentsView.swift; sourceTree = "<group>"; };
|
||||||
D02E1F94250B13210071AD56 /* SafariView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SafariView.swift; sourceTree = "<group>"; };
|
D02E1F94250B13210071AD56 /* SafariView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SafariView.swift; sourceTree = "<group>"; };
|
||||||
|
D03B1B29253818F3008F964B /* MediaPreferencesView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MediaPreferencesView.swift; sourceTree = "<group>"; };
|
||||||
D047FA8C24C3E21200AF17C5 /* Metatext.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Metatext.app; sourceTree = BUILT_PRODUCTS_DIR; };
|
D047FA8C24C3E21200AF17C5 /* Metatext.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Metatext.app; sourceTree = BUILT_PRODUCTS_DIR; };
|
||||||
D0625E58250F092900502611 /* StatusListCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StatusListCell.swift; sourceTree = "<group>"; };
|
D0625E58250F092900502611 /* StatusListCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StatusListCell.swift; sourceTree = "<group>"; };
|
||||||
D0625E5C250F0B5C00502611 /* StatusContentConfiguration.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StatusContentConfiguration.swift; sourceTree = "<group>"; };
|
D0625E5C250F0B5C00502611 /* StatusContentConfiguration.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StatusContentConfiguration.swift; sourceTree = "<group>"; };
|
||||||
|
@ -112,6 +117,7 @@
|
||||||
D06BC5E525202AD90079541D /* ProfileViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProfileViewController.swift; sourceTree = "<group>"; };
|
D06BC5E525202AD90079541D /* ProfileViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProfileViewController.swift; sourceTree = "<group>"; };
|
||||||
D085C3BB25008DEC008A6C5E /* DB */ = {isa = PBXFileReference; lastKnownFileType = folder; path = DB; sourceTree = "<group>"; };
|
D085C3BB25008DEC008A6C5E /* DB */ = {isa = PBXFileReference; lastKnownFileType = folder; path = DB; sourceTree = "<group>"; };
|
||||||
D0A1F4F6252E7D4B004435BF /* TableViewDataSource.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TableViewDataSource.swift; sourceTree = "<group>"; };
|
D0A1F4F6252E7D4B004435BF /* TableViewDataSource.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TableViewDataSource.swift; sourceTree = "<group>"; };
|
||||||
|
D0A3C2F625390A9700739F88 /* AppPreferences+Extensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "AppPreferences+Extensions.swift"; sourceTree = "<group>"; };
|
||||||
D0AD03552505814D0085A466 /* Base16 */ = {isa = PBXFileReference; lastKnownFileType = folder; path = Base16; sourceTree = "<group>"; };
|
D0AD03552505814D0085A466 /* Base16 */ = {isa = PBXFileReference; lastKnownFileType = folder; path = Base16; sourceTree = "<group>"; };
|
||||||
D0B32F4F250B373600311912 /* RegistrationView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RegistrationView.swift; sourceTree = "<group>"; };
|
D0B32F4F250B373600311912 /* RegistrationView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RegistrationView.swift; sourceTree = "<group>"; };
|
||||||
D0B5FE9A251583DB00478838 /* ProfileCollection+Extensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "ProfileCollection+Extensions.swift"; sourceTree = "<group>"; };
|
D0B5FE9A251583DB00478838 /* ProfileCollection+Extensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "ProfileCollection+Extensions.swift"; sourceTree = "<group>"; };
|
||||||
|
@ -164,7 +170,9 @@
|
||||||
D0F0B112251A86A000942152 /* AccountContentConfiguration.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AccountContentConfiguration.swift; sourceTree = "<group>"; };
|
D0F0B112251A86A000942152 /* AccountContentConfiguration.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AccountContentConfiguration.swift; sourceTree = "<group>"; };
|
||||||
D0F0B125251A90F400942152 /* AccountListCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AccountListCell.swift; sourceTree = "<group>"; };
|
D0F0B125251A90F400942152 /* AccountListCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AccountListCell.swift; sourceTree = "<group>"; };
|
||||||
D0F0B12D251A97E400942152 /* TableViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TableViewController.swift; sourceTree = "<group>"; };
|
D0F0B12D251A97E400942152 /* TableViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TableViewController.swift; sourceTree = "<group>"; };
|
||||||
D0F0B135251AA12700942152 /* CollectionItemKind+Extensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "CollectionItemKind+Extensions.swift"; sourceTree = "<group>"; };
|
D0F0B135251AA12700942152 /* CollectionItem+Extensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "CollectionItem+Extensions.swift"; sourceTree = "<group>"; };
|
||||||
|
D0FE1C8E253686F9003EF1EB /* PlayerView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PlayerView.swift; sourceTree = "<group>"; };
|
||||||
|
D0FE1C9725368A9D003EF1EB /* PlayerCache.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PlayerCache.swift; sourceTree = "<group>"; };
|
||||||
/* End PBXFileReference section */
|
/* End PBXFileReference section */
|
||||||
|
|
||||||
/* Begin PBXFrameworksBuildPhase section */
|
/* Begin PBXFrameworksBuildPhase section */
|
||||||
|
@ -202,6 +210,7 @@
|
||||||
children = (
|
children = (
|
||||||
D0C7D45224F76169001EBDBB /* Assets.xcassets */,
|
D0C7D45224F76169001EBDBB /* Assets.xcassets */,
|
||||||
D0AD03552505814D0085A466 /* Base16 */,
|
D0AD03552505814D0085A466 /* Base16 */,
|
||||||
|
D0FE1C9625368A15003EF1EB /* Caches */,
|
||||||
D0D7C013250440610039AD6F /* CodableBloomFilter */,
|
D0D7C013250440610039AD6F /* CodableBloomFilter */,
|
||||||
D0A1F4F5252E7D2A004435BF /* Data Sources */,
|
D0A1F4F5252E7D2A004435BF /* Data Sources */,
|
||||||
D085C3BB25008DEC008A6C5E /* DB */,
|
D085C3BB25008DEC008A6C5E /* DB */,
|
||||||
|
@ -288,6 +297,7 @@
|
||||||
D0F0B125251A90F400942152 /* AccountListCell.swift */,
|
D0F0B125251A90F400942152 /* AccountListCell.swift */,
|
||||||
D0F0B10D251A868200942152 /* AccountView.swift */,
|
D0F0B10D251A868200942152 /* AccountView.swift */,
|
||||||
D0C7D42424F76169001EBDBB /* AddIdentityView.swift */,
|
D0C7D42424F76169001EBDBB /* AddIdentityView.swift */,
|
||||||
|
D03B1B29253818F3008F964B /* MediaPreferencesView.swift */,
|
||||||
D0C7D42324F76169001EBDBB /* CustomEmojiText.swift */,
|
D0C7D42324F76169001EBDBB /* CustomEmojiText.swift */,
|
||||||
D0BEB21024FA2A90001B0F04 /* EditFilterView.swift */,
|
D0BEB21024FA2A90001B0F04 /* EditFilterView.swift */,
|
||||||
D0BEB20424FA1107001B0F04 /* FiltersView.swift */,
|
D0BEB20424FA1107001B0F04 /* FiltersView.swift */,
|
||||||
|
@ -298,6 +308,7 @@
|
||||||
D0E569DF252931B100FA1D72 /* LoadMoreContentConfiguration.swift */,
|
D0E569DF252931B100FA1D72 /* LoadMoreContentConfiguration.swift */,
|
||||||
D0E569DA2529319100FA1D72 /* LoadMoreView.swift */,
|
D0E569DA2529319100FA1D72 /* LoadMoreView.swift */,
|
||||||
D0C7D42D24F76169001EBDBB /* NotificationTypesPreferencesView.swift */,
|
D0C7D42D24F76169001EBDBB /* NotificationTypesPreferencesView.swift */,
|
||||||
|
D0FE1C8E253686F9003EF1EB /* PlayerView.swift */,
|
||||||
D0C7D42824F76169001EBDBB /* PostingReadingPreferencesView.swift */,
|
D0C7D42824F76169001EBDBB /* PostingReadingPreferencesView.swift */,
|
||||||
D0C7D42624F76169001EBDBB /* PreferencesView.swift */,
|
D0C7D42624F76169001EBDBB /* PreferencesView.swift */,
|
||||||
D0B32F4F250B373600311912 /* RegistrationView.swift */,
|
D0B32F4F250B373600311912 /* RegistrationView.swift */,
|
||||||
|
@ -344,11 +355,12 @@
|
||||||
D0C7D46824F76169001EBDBB /* Extensions */ = {
|
D0C7D46824F76169001EBDBB /* Extensions */ = {
|
||||||
isa = PBXGroup;
|
isa = PBXGroup;
|
||||||
children = (
|
children = (
|
||||||
D0B5FE9A251583DB00478838 /* ProfileCollection+Extensions.swift */,
|
D0A3C2F625390A9700739F88 /* AppPreferences+Extensions.swift */,
|
||||||
D01C6FAB252024BD003D0300 /* Array+Extensions.swift */,
|
D01C6FAB252024BD003D0300 /* Array+Extensions.swift */,
|
||||||
D0F0B135251AA12700942152 /* CollectionItemKind+Extensions.swift */,
|
D0F0B135251AA12700942152 /* CollectionItem+Extensions.swift */,
|
||||||
D0C7D46E24F76169001EBDBB /* KingfisherOptionsInfo+Extensions.swift */,
|
D0C7D46E24F76169001EBDBB /* KingfisherOptionsInfo+Extensions.swift */,
|
||||||
D0C7D46B24F76169001EBDBB /* NSMutableAttributedString+Extensions.swift */,
|
D0C7D46B24F76169001EBDBB /* NSMutableAttributedString+Extensions.swift */,
|
||||||
|
D0B5FE9A251583DB00478838 /* ProfileCollection+Extensions.swift */,
|
||||||
D0C7D46A24F76169001EBDBB /* String+Extensions.swift */,
|
D0C7D46A24F76169001EBDBB /* String+Extensions.swift */,
|
||||||
D0C7D46C24F76169001EBDBB /* UIColor+Extensions.swift */,
|
D0C7D46C24F76169001EBDBB /* UIColor+Extensions.swift */,
|
||||||
D0030981250C6C8500EACB32 /* URL+Extensions.swift */,
|
D0030981250C6C8500EACB32 /* URL+Extensions.swift */,
|
||||||
|
@ -367,6 +379,14 @@
|
||||||
path = "Notification Service Extension";
|
path = "Notification Service Extension";
|
||||||
sourceTree = "<group>";
|
sourceTree = "<group>";
|
||||||
};
|
};
|
||||||
|
D0FE1C9625368A15003EF1EB /* Caches */ = {
|
||||||
|
isa = PBXGroup;
|
||||||
|
children = (
|
||||||
|
D0FE1C9725368A9D003EF1EB /* PlayerCache.swift */,
|
||||||
|
);
|
||||||
|
path = Caches;
|
||||||
|
sourceTree = "<group>";
|
||||||
|
};
|
||||||
/* End PBXGroup section */
|
/* End PBXGroup section */
|
||||||
|
|
||||||
/* Begin PBXNativeTarget section */
|
/* Begin PBXNativeTarget section */
|
||||||
|
@ -536,15 +556,18 @@
|
||||||
files = (
|
files = (
|
||||||
D0C7D4A324F7616A001EBDBB /* TabNavigationView.swift in Sources */,
|
D0C7D4A324F7616A001EBDBB /* TabNavigationView.swift in Sources */,
|
||||||
D02E1F95250B13210071AD56 /* SafariView.swift in Sources */,
|
D02E1F95250B13210071AD56 /* SafariView.swift in Sources */,
|
||||||
|
D03B1B2A253818F3008F964B /* MediaPreferencesView.swift in Sources */,
|
||||||
D0C7D49C24F7616A001EBDBB /* RootView.swift in Sources */,
|
D0C7D49C24F7616A001EBDBB /* RootView.swift in Sources */,
|
||||||
D0F0B126251A90F400942152 /* AccountListCell.swift in Sources */,
|
D0F0B126251A90F400942152 /* AccountListCell.swift in Sources */,
|
||||||
D0B32F50250B373600311912 /* RegistrationView.swift in Sources */,
|
D0B32F50250B373600311912 /* RegistrationView.swift in Sources */,
|
||||||
D0E569E0252931B100FA1D72 /* LoadMoreContentConfiguration.swift in Sources */,
|
D0E569E0252931B100FA1D72 /* LoadMoreContentConfiguration.swift in Sources */,
|
||||||
D0F0B136251AA12700942152 /* CollectionItemKind+Extensions.swift in Sources */,
|
D0FE1C9825368A9D003EF1EB /* PlayerCache.swift in Sources */,
|
||||||
|
D0F0B136251AA12700942152 /* CollectionItem+Extensions.swift in Sources */,
|
||||||
D0625E5D250F0B5C00502611 /* StatusContentConfiguration.swift in Sources */,
|
D0625E5D250F0B5C00502611 /* StatusContentConfiguration.swift in Sources */,
|
||||||
D0BEB1F324F8EE8C001B0F04 /* StatusAttachmentView.swift in Sources */,
|
D0BEB1F324F8EE8C001B0F04 /* StatusAttachmentView.swift in Sources */,
|
||||||
D0C7D49A24F7616A001EBDBB /* TableView.swift in Sources */,
|
D0C7D49A24F7616A001EBDBB /* TableView.swift in Sources */,
|
||||||
D0F0B12E251A97E400942152 /* TableViewController.swift in Sources */,
|
D0F0B12E251A97E400942152 /* TableViewController.swift in Sources */,
|
||||||
|
D0FE1C8F253686F9003EF1EB /* PlayerView.swift in Sources */,
|
||||||
D0F0B113251A86A000942152 /* AccountContentConfiguration.swift in Sources */,
|
D0F0B113251A86A000942152 /* AccountContentConfiguration.swift in Sources */,
|
||||||
D01F41D924F880C400D55A2D /* TouchFallthroughTextView.swift in Sources */,
|
D01F41D924F880C400D55A2D /* TouchFallthroughTextView.swift in Sources */,
|
||||||
D0C7D4D624F7616A001EBDBB /* NSMutableAttributedString+Extensions.swift in Sources */,
|
D0C7D4D624F7616A001EBDBB /* NSMutableAttributedString+Extensions.swift in Sources */,
|
||||||
|
@ -557,6 +580,7 @@
|
||||||
D0C7D4D524F7616A001EBDBB /* String+Extensions.swift in Sources */,
|
D0C7D4D524F7616A001EBDBB /* String+Extensions.swift in Sources */,
|
||||||
D0C7D4A224F7616A001EBDBB /* NotificationTypesPreferencesView.swift in Sources */,
|
D0C7D4A224F7616A001EBDBB /* NotificationTypesPreferencesView.swift in Sources */,
|
||||||
D0BEB1F724F9A84B001B0F04 /* LoadingTableFooterView.swift in Sources */,
|
D0BEB1F724F9A84B001B0F04 /* LoadingTableFooterView.swift in Sources */,
|
||||||
|
D0A3C2F725390A9700739F88 /* AppPreferences+Extensions.swift in Sources */,
|
||||||
D06BC5E625202AD90079541D /* ProfileViewController.swift in Sources */,
|
D06BC5E625202AD90079541D /* ProfileViewController.swift in Sources */,
|
||||||
D01C6FAC252024BD003D0300 /* Array+Extensions.swift in Sources */,
|
D01C6FAC252024BD003D0300 /* Array+Extensions.swift in Sources */,
|
||||||
D0C7D4D924F7616A001EBDBB /* KingfisherOptionsInfo+Extensions.swift in Sources */,
|
D0C7D4D924F7616A001EBDBB /* KingfisherOptionsInfo+Extensions.swift in Sources */,
|
||||||
|
|
|
@ -9,11 +9,11 @@ import MastodonAPI
|
||||||
|
|
||||||
public struct InstanceURLService {
|
public struct InstanceURLService {
|
||||||
private let httpClient: HTTPClient
|
private let httpClient: HTTPClient
|
||||||
private var userDefaultsClient: UserDefaultsClient
|
private var appPreferences: AppPreferences
|
||||||
|
|
||||||
public init(environment: AppEnvironment) {
|
public init(environment: AppEnvironment) {
|
||||||
httpClient = HTTPClient(session: environment.session, decoder: MastodonDecoder())
|
httpClient = HTTPClient(session: environment.session, decoder: MastodonDecoder())
|
||||||
userDefaultsClient = UserDefaultsClient(userDefaults: environment.userDefaults)
|
appPreferences = AppPreferences(environment: environment)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -61,7 +61,7 @@ public extension InstanceURLService {
|
||||||
|
|
||||||
func updateFilter() -> AnyPublisher<Never, Error> {
|
func updateFilter() -> AnyPublisher<Never, Error> {
|
||||||
httpClient.request(UpdatedFilterTarget())
|
httpClient.request(UpdatedFilterTarget())
|
||||||
.handleEvents(receiveOutput: { userDefaultsClient.updateInstanceFilter($0) })
|
.handleEvents(receiveOutput: { appPreferences.updateInstanceFilter($0) })
|
||||||
.ignoreOutput()
|
.ignoreOutput()
|
||||||
.eraseToAnyPublisher()
|
.eraseToAnyPublisher()
|
||||||
}
|
}
|
||||||
|
@ -119,7 +119,7 @@ private extension InstanceURLService {
|
||||||
]))
|
]))
|
||||||
|
|
||||||
var filter: BloomFilter<String> {
|
var filter: BloomFilter<String> {
|
||||||
userDefaultsClient.updatedInstanceFilter ?? Self.defaultFilter
|
appPreferences.updatedInstanceFilter ?? Self.defaultFilter
|
||||||
}
|
}
|
||||||
|
|
||||||
private func isFiltered(url: URL) -> Bool {
|
private func isFiltered(url: URL) -> Bool {
|
||||||
|
|
106
ServiceLayer/Sources/ServiceLayer/Utilities/AppPreferences.swift
Normal file
106
ServiceLayer/Sources/ServiceLayer/Utilities/AppPreferences.swift
Normal file
|
@ -0,0 +1,106 @@
|
||||||
|
// Copyright © 2020 Metabolist. All rights reserved.
|
||||||
|
|
||||||
|
import CodableBloomFilter
|
||||||
|
import Foundation
|
||||||
|
|
||||||
|
public struct AppPreferences {
|
||||||
|
private let userDefaults: UserDefaults
|
||||||
|
|
||||||
|
public init(environment: AppEnvironment) {
|
||||||
|
self.userDefaults = environment.userDefaults
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public extension AppPreferences {
|
||||||
|
enum AnimateAvatars: String, CaseIterable, Identifiable {
|
||||||
|
case everywhere
|
||||||
|
case profiles
|
||||||
|
case never
|
||||||
|
|
||||||
|
public var id: String { rawValue }
|
||||||
|
}
|
||||||
|
|
||||||
|
enum Autoplay: String, CaseIterable, Identifiable {
|
||||||
|
case always
|
||||||
|
case wifi
|
||||||
|
case never
|
||||||
|
|
||||||
|
public var id: String { rawValue }
|
||||||
|
}
|
||||||
|
|
||||||
|
var useSystemReduceMotionForMedia: Bool {
|
||||||
|
get { self[.useSystemReduceMotionForMedia] ?? true }
|
||||||
|
set { self[.useSystemReduceMotionForMedia] = newValue }
|
||||||
|
}
|
||||||
|
|
||||||
|
var animateAvatars: AnimateAvatars {
|
||||||
|
get {
|
||||||
|
if let rawValue = self[.animateAvatars] as String?,
|
||||||
|
let value = AnimateAvatars(rawValue: rawValue) {
|
||||||
|
return value
|
||||||
|
}
|
||||||
|
|
||||||
|
return .profiles
|
||||||
|
}
|
||||||
|
set { self[.animateAvatars] = newValue.rawValue }
|
||||||
|
}
|
||||||
|
|
||||||
|
var animateHeaders: Bool {
|
||||||
|
get { self[.animateHeaders] ?? true }
|
||||||
|
set { self[.animateHeaders] = newValue }
|
||||||
|
}
|
||||||
|
|
||||||
|
var autoplayGIFs: Autoplay {
|
||||||
|
get {
|
||||||
|
if let rawValue = self[.autoplayGIFs] as String?,
|
||||||
|
let value = Autoplay(rawValue: rawValue) {
|
||||||
|
return value
|
||||||
|
}
|
||||||
|
|
||||||
|
return .always
|
||||||
|
}
|
||||||
|
set { self[.autoplayGIFs] = newValue.rawValue }
|
||||||
|
}
|
||||||
|
|
||||||
|
var autoplayVideos: Autoplay {
|
||||||
|
get {
|
||||||
|
if let rawValue = self[.autoplayVideos] as String?,
|
||||||
|
let value = Autoplay(rawValue: rawValue) {
|
||||||
|
return value
|
||||||
|
}
|
||||||
|
|
||||||
|
return .wifi
|
||||||
|
}
|
||||||
|
set { self[.autoplayVideos] = newValue.rawValue }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
extension AppPreferences {
|
||||||
|
var updatedInstanceFilter: BloomFilter<String>? {
|
||||||
|
guard let data = self[.updatedFilter] as Data? else {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
return try? JSONDecoder().decode(BloomFilter<String>.self, from: data)
|
||||||
|
}
|
||||||
|
|
||||||
|
func updateInstanceFilter( _ filter: BloomFilter<String>) {
|
||||||
|
userDefaults.set(try? JSONEncoder().encode(filter), forKey: Item.updatedFilter.rawValue)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private extension AppPreferences {
|
||||||
|
enum Item: String {
|
||||||
|
case updatedFilter
|
||||||
|
case useSystemReduceMotionForMedia
|
||||||
|
case animateAvatars
|
||||||
|
case animateHeaders
|
||||||
|
case autoplayGIFs
|
||||||
|
case autoplayVideos
|
||||||
|
}
|
||||||
|
|
||||||
|
subscript<T>(index: Item) -> T? {
|
||||||
|
get { userDefaults.value(forKey: index.rawValue) as? T }
|
||||||
|
set { userDefaults.set(newValue, forKey: index.rawValue) }
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,37 +0,0 @@
|
||||||
// Copyright © 2020 Metabolist. All rights reserved.
|
|
||||||
|
|
||||||
import CodableBloomFilter
|
|
||||||
import Foundation
|
|
||||||
|
|
||||||
struct UserDefaultsClient {
|
|
||||||
private let userDefaults: UserDefaults
|
|
||||||
|
|
||||||
init(userDefaults: UserDefaults) {
|
|
||||||
self.userDefaults = userDefaults
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
extension UserDefaultsClient {
|
|
||||||
var updatedInstanceFilter: BloomFilter<String>? {
|
|
||||||
guard let data = self[.updatedFilter] as Data? else {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
return try? JSONDecoder().decode(BloomFilter<String>.self, from: data)
|
|
||||||
}
|
|
||||||
|
|
||||||
func updateInstanceFilter( _ filter: BloomFilter<String>) {
|
|
||||||
userDefaults.set(try? JSONEncoder().encode(filter), forKey: Item.updatedFilter.rawValue)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private extension UserDefaultsClient {
|
|
||||||
enum Item: String {
|
|
||||||
case updatedFilter
|
|
||||||
}
|
|
||||||
|
|
||||||
subscript<T>(index: Item) -> T? {
|
|
||||||
get { userDefaults.value(forKey: index.rawValue) as? T }
|
|
||||||
set { userDefaults.set(newValue, forKey: index.rawValue) }
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -11,7 +11,7 @@ class TableViewController: UITableViewController {
|
||||||
private let loadingTableFooterView = LoadingTableFooterView()
|
private let loadingTableFooterView = LoadingTableFooterView()
|
||||||
private let webfingerIndicatorView = WebfingerIndicatorView()
|
private let webfingerIndicatorView = WebfingerIndicatorView()
|
||||||
private var cancellables = Set<AnyCancellable>()
|
private var cancellables = Set<AnyCancellable>()
|
||||||
private var cellHeightCaches = [CGFloat: [CollectionItemIdentifier: CGFloat]]()
|
private var cellHeightCaches = [CGFloat: [CollectionItem: CGFloat]]()
|
||||||
|
|
||||||
private lazy var dataSource: TableViewDataSource = {
|
private lazy var dataSource: TableViewDataSource = {
|
||||||
.init(tableView: tableView, viewModelProvider: viewModel.viewModel(indexPath:))
|
.init(tableView: tableView, viewModelProvider: viewModel.viewModel(indexPath:))
|
||||||
|
@ -54,6 +54,18 @@ class TableViewController: UITableViewController {
|
||||||
viewModel.request(maxId: nil, minId: nil)
|
viewModel.request(maxId: nil, minId: nil)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override func viewDidAppear(_ animated: Bool) {
|
||||||
|
super.viewDidAppear(animated)
|
||||||
|
|
||||||
|
updateAutoplayViews()
|
||||||
|
}
|
||||||
|
|
||||||
|
override func viewDidDisappear(_ animated: Bool) {
|
||||||
|
super.viewDidDisappear(animated)
|
||||||
|
|
||||||
|
updateAutoplayViews()
|
||||||
|
}
|
||||||
|
|
||||||
override func scrollViewDidScroll(_ scrollView: UIScrollView) {
|
override func scrollViewDidScroll(_ scrollView: UIScrollView) {
|
||||||
guard scrollView.isDragging else { return }
|
guard scrollView.isDragging else { return }
|
||||||
|
|
||||||
|
@ -62,6 +74,8 @@ class TableViewController: UITableViewController {
|
||||||
for loadMoreView in visibleLoadMoreViews {
|
for loadMoreView in visibleLoadMoreViews {
|
||||||
loadMoreView.directionChanged(up: up)
|
loadMoreView.directionChanged(up: up)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
updateAutoplayViews()
|
||||||
}
|
}
|
||||||
|
|
||||||
override func scrollViewDidEndDragging(_ scrollView: UIScrollView, willDecelerate decelerate: Bool) {
|
override func scrollViewDidEndDragging(_ scrollView: UIScrollView, willDecelerate decelerate: Bool) {
|
||||||
|
@ -75,7 +89,7 @@ class TableViewController: UITableViewController {
|
||||||
forRowAt indexPath: IndexPath) {
|
forRowAt indexPath: IndexPath) {
|
||||||
guard let item = dataSource.itemIdentifier(for: indexPath) else { return }
|
guard let item = dataSource.itemIdentifier(for: indexPath) else { return }
|
||||||
|
|
||||||
var heightCache = cellHeightCaches[tableView.frame.width] ?? [CollectionItemIdentifier: CGFloat]()
|
var heightCache = cellHeightCaches[tableView.frame.width] ?? [CollectionItem: CGFloat]()
|
||||||
|
|
||||||
heightCache[item] = cell.frame.height
|
heightCache[item] = cell.frame.height
|
||||||
cellHeightCaches[tableView.frame.width] = heightCache
|
cellHeightCaches[tableView.frame.width] = heightCache
|
||||||
|
@ -104,6 +118,12 @@ class TableViewController: UITableViewController {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
extension TableViewController {
|
||||||
|
static let autoplayableAttachmentsView = PassthroughSubject<StatusAttachmentsView?, Never>()
|
||||||
|
static let autoplayableAttachmentsViewNotification =
|
||||||
|
Notification.Name("com.metabolist.metatext.attachment-view-became-autoplayable")
|
||||||
|
}
|
||||||
|
|
||||||
extension TableViewController: UITableViewDataSourcePrefetching {
|
extension TableViewController: UITableViewDataSourcePrefetching {
|
||||||
func tableView(_ tableView: UITableView, prefetchRowsAt indexPaths: [IndexPath]) {
|
func tableView(_ tableView: UITableView, prefetchRowsAt indexPaths: [IndexPath]) {
|
||||||
guard
|
guard
|
||||||
|
@ -151,6 +171,9 @@ extension TableViewController {
|
||||||
}
|
}
|
||||||
|
|
||||||
private extension TableViewController {
|
private extension TableViewController {
|
||||||
|
static let autoplayViews = [PlayerView](repeating: .init(), count: 4)
|
||||||
|
static var visibleVideoURLs = Set<URL>()
|
||||||
|
|
||||||
var visibleLoadMoreViews: [LoadMoreView] {
|
var visibleLoadMoreViews: [LoadMoreView] {
|
||||||
tableView.visibleCells.compactMap { $0.contentView as? LoadMoreView }
|
tableView.visibleCells.compactMap { $0.contentView as? LoadMoreView }
|
||||||
}
|
}
|
||||||
|
@ -178,9 +201,19 @@ private extension TableViewController {
|
||||||
|
|
||||||
tableView.publisher(for: \.contentOffset)
|
tableView.publisher(for: \.contentOffset)
|
||||||
.compactMap { [weak self] _ in self?.tableView.indexPathsForVisibleRows?.first }
|
.compactMap { [weak self] _ in self?.tableView.indexPathsForVisibleRows?.first }
|
||||||
.removeDuplicates()
|
|
||||||
.sink { [weak self] in self?.viewModel.viewedAtTop(indexPath: $0) }
|
.sink { [weak self] in self?.viewModel.viewedAtTop(indexPath: $0) }
|
||||||
.store(in: &cancellables)
|
.store(in: &cancellables)
|
||||||
|
|
||||||
|
Self.autoplayableAttachmentsView
|
||||||
|
.removeDuplicates()
|
||||||
|
.sink {
|
||||||
|
let notification = Notification(
|
||||||
|
name: Self.autoplayableAttachmentsViewNotification,
|
||||||
|
object: $0,
|
||||||
|
userInfo: nil)
|
||||||
|
NotificationCenter.default.post(notification)
|
||||||
|
}
|
||||||
|
.store(in: &cancellables)
|
||||||
}
|
}
|
||||||
|
|
||||||
func update(_ update: CollectionUpdate) {
|
func update(_ update: CollectionUpdate) {
|
||||||
|
@ -206,6 +239,8 @@ private extension TableViewController {
|
||||||
self.tableView.contentOffset.y -= offsetFromNavigationBar
|
self.tableView.contentOffset.y -= offsetFromNavigationBar
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
self.updateAutoplayViews()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -263,4 +298,21 @@ private extension TableViewController {
|
||||||
|
|
||||||
present(activityViewController, animated: true, completion: nil)
|
present(activityViewController, animated: true, completion: nil)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func updateAutoplayViews() {
|
||||||
|
if let visibleView = navigationController?.visibleViewController?.view,
|
||||||
|
view.isDescendant(of: visibleView),
|
||||||
|
let superview = view.superview,
|
||||||
|
let attachmentsViewClosestToCenter = tableView.visibleCells
|
||||||
|
.compactMap({ ($0.contentView as? StatusView)?.attachmentsView })
|
||||||
|
.filter(\.shouldAutoplay)
|
||||||
|
.min(by: {
|
||||||
|
abs(superview.convert($0.frame, from: $0.superview).midY - view.frame.midY)
|
||||||
|
< abs(superview.convert($1.frame, from: $1.superview).midY - view.frame.midY)
|
||||||
|
}) {
|
||||||
|
Self.autoplayableAttachmentsView.send(attachmentsViewClosestToCenter)
|
||||||
|
} else {
|
||||||
|
Self.autoplayableAttachmentsView.send(nil)
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -7,12 +7,14 @@ import ServiceLayer
|
||||||
|
|
||||||
public struct AccountViewModel: CollectionItemViewModel {
|
public struct AccountViewModel: CollectionItemViewModel {
|
||||||
public let events: AnyPublisher<AnyPublisher<CollectionItemEvent, Error>, Never>
|
public let events: AnyPublisher<AnyPublisher<CollectionItemEvent, Error>, Never>
|
||||||
|
public let identification: Identification
|
||||||
|
|
||||||
private let accountService: AccountService
|
private let accountService: AccountService
|
||||||
private let eventsSubject = PassthroughSubject<AnyPublisher<CollectionItemEvent, Error>, Never>()
|
private let eventsSubject = PassthroughSubject<AnyPublisher<CollectionItemEvent, Error>, Never>()
|
||||||
|
|
||||||
init(accountService: AccountService) {
|
init(accountService: AccountService, identification: Identification) {
|
||||||
self.accountService = accountService
|
self.accountService = accountService
|
||||||
|
self.identification = identification
|
||||||
events = eventsSubject.eraseToAnyPublisher()
|
events = eventsSubject.eraseToAnyPublisher()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -20,8 +22,12 @@ public struct AccountViewModel: CollectionItemViewModel {
|
||||||
public extension AccountViewModel {
|
public extension AccountViewModel {
|
||||||
var avatarURL: URL { accountService.account.avatar }
|
var avatarURL: URL { accountService.account.avatar }
|
||||||
|
|
||||||
|
var avatarStaticURL: URL { accountService.account.avatarStatic }
|
||||||
|
|
||||||
var headerURL: URL { accountService.account.header }
|
var headerURL: URL { accountService.account.header }
|
||||||
|
|
||||||
|
var headerStaticURL: URL { accountService.account.headerStatic }
|
||||||
|
|
||||||
var displayName: String { accountService.account.displayName }
|
var displayName: String { accountService.account.displayName }
|
||||||
|
|
||||||
var accountName: String { "@".appending(accountService.account.acct) }
|
var accountName: String { "@".appending(accountService.account.acct) }
|
||||||
|
|
|
@ -16,7 +16,7 @@ final public class CollectionItemsViewModel: ObservableObject {
|
||||||
private let eventsSubject = PassthroughSubject<CollectionItemEvent, Never>()
|
private let eventsSubject = PassthroughSubject<CollectionItemEvent, Never>()
|
||||||
private let loadingSubject = PassthroughSubject<Bool, Never>()
|
private let loadingSubject = PassthroughSubject<Bool, Never>()
|
||||||
private let expandAllSubject: CurrentValueSubject<ExpandAllState, Never>
|
private let expandAllSubject: CurrentValueSubject<ExpandAllState, Never>
|
||||||
private var maintainScrollPosition: CollectionItemIdentifier?
|
private var maintainScrollPosition: CollectionItem?
|
||||||
private var topVisibleIndexPath = IndexPath(item: 0, section: 0)
|
private var topVisibleIndexPath = IndexPath(item: 0, section: 0)
|
||||||
private var lastSelectedLoadMore: LoadMore?
|
private var lastSelectedLoadMore: LoadMore?
|
||||||
private var cancellables = Set<AnyCancellable>()
|
private var cancellables = Set<AnyCancellable>()
|
||||||
|
@ -44,7 +44,7 @@ final public class CollectionItemsViewModel: ObservableObject {
|
||||||
extension CollectionItemsViewModel: CollectionViewModel {
|
extension CollectionItemsViewModel: CollectionViewModel {
|
||||||
public var updates: AnyPublisher<CollectionUpdate, Never> {
|
public var updates: AnyPublisher<CollectionUpdate, Never> {
|
||||||
items.map { [weak self] in
|
items.map { [weak self] in
|
||||||
CollectionUpdate(items: $0.map { $0.map(CollectionItemIdentifier.init(item:)) },
|
CollectionUpdate(items: $0,
|
||||||
maintainScrollPosition: self?.maintainScrollPosition)
|
maintainScrollPosition: self?.maintainScrollPosition)
|
||||||
}
|
}
|
||||||
.eraseToAnyPublisher()
|
.eraseToAnyPublisher()
|
||||||
|
@ -145,7 +145,8 @@ extension CollectionItemsViewModel: CollectionViewModel {
|
||||||
}
|
}
|
||||||
|
|
||||||
let viewModel = AccountViewModel(
|
let viewModel = AccountViewModel(
|
||||||
accountService: collectionService.navigationService.accountService(account: account))
|
accountService: collectionService.navigationService.accountService(account: account),
|
||||||
|
identification: identification)
|
||||||
|
|
||||||
cache(viewModel: viewModel, forItem: item)
|
cache(viewModel: viewModel, forItem: item)
|
||||||
|
|
||||||
|
@ -187,7 +188,7 @@ private extension CollectionItemsViewModel {
|
||||||
}
|
}
|
||||||
|
|
||||||
func process(items: [[CollectionItem]]) {
|
func process(items: [[CollectionItem]]) {
|
||||||
maintainScrollPosition = identifierForScrollPositionMaintenance(newItems: items)
|
maintainScrollPosition = itemForScrollPositionMaintenance(newItems: items)
|
||||||
self.items.send(items)
|
self.items.send(items)
|
||||||
|
|
||||||
let itemsSet = Set(items.reduce([], +))
|
let itemsSet = Set(items.reduce([], +))
|
||||||
|
@ -195,7 +196,7 @@ private extension CollectionItemsViewModel {
|
||||||
viewModelCache = viewModelCache.filter { itemsSet.contains($0.key) }
|
viewModelCache = viewModelCache.filter { itemsSet.contains($0.key) }
|
||||||
}
|
}
|
||||||
|
|
||||||
func identifierForScrollPositionMaintenance(newItems: [[CollectionItem]]) -> CollectionItemIdentifier? {
|
func itemForScrollPositionMaintenance(newItems: [[CollectionItem]]) -> CollectionItem? {
|
||||||
let flatNewItems = newItems.reduce([], +)
|
let flatNewItems = newItems.reduce([], +)
|
||||||
|
|
||||||
if collectionService is ContextService,
|
if collectionService is ContextService,
|
||||||
|
@ -205,7 +206,7 @@ private extension CollectionItemsViewModel {
|
||||||
|
|
||||||
return configuration.isContextParent // Maintain scroll position of parent after initial load of context
|
return configuration.isContextParent // Maintain scroll position of parent after initial load of context
|
||||||
}) {
|
}) {
|
||||||
return .init(item: contextParent)
|
return contextParent
|
||||||
} else if collectionService is TimelineService {
|
} else if collectionService is TimelineService {
|
||||||
let flatItems = items.value.reduce([], +)
|
let flatItems = items.value.reduce([], +)
|
||||||
let difference = flatNewItems.difference(from: flatItems)
|
let difference = flatNewItems.difference(from: flatItems)
|
||||||
|
@ -222,7 +223,7 @@ private extension CollectionItemsViewModel {
|
||||||
|
|
||||||
return status.id == loadMore.beforeStatusId
|
return status.id == loadMore.beforeStatusId
|
||||||
}) {
|
}) {
|
||||||
return .init(item: statusAfterLoadMore)
|
return statusAfterLoadMore
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -234,7 +235,7 @@ private extension CollectionItemsViewModel {
|
||||||
if newItems.count > topVisibleIndexPath.section,
|
if newItems.count > topVisibleIndexPath.section,
|
||||||
let newIndex = newItems[topVisibleIndexPath.section].firstIndex(of: topVisibleItem),
|
let newIndex = newItems[topVisibleIndexPath.section].firstIndex(of: topVisibleItem),
|
||||||
newIndex > topVisibleIndexPath.item {
|
newIndex > topVisibleIndexPath.item {
|
||||||
return .init(item: topVisibleItem)
|
return topVisibleItem
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,5 @@
|
||||||
|
// Copyright © 2020 Metabolist. All rights reserved.
|
||||||
|
|
||||||
|
import ServiceLayer
|
||||||
|
|
||||||
|
public typealias AppPreferences = ServiceLayer.AppPreferences
|
|
@ -0,0 +1,5 @@
|
||||||
|
// Copyright © 2020 Metabolist. All rights reserved.
|
||||||
|
|
||||||
|
import ServiceLayer
|
||||||
|
|
||||||
|
public typealias CollectionItem = ServiceLayer.CollectionItem
|
|
@ -1,31 +0,0 @@
|
||||||
// Copyright © 2020 Metabolist. All rights reserved.
|
|
||||||
|
|
||||||
import Mastodon
|
|
||||||
import ServiceLayer
|
|
||||||
|
|
||||||
public struct CollectionItemIdentifier: Hashable {
|
|
||||||
private let item: CollectionItem
|
|
||||||
|
|
||||||
init(item: CollectionItem) {
|
|
||||||
self.item = item
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public extension CollectionItemIdentifier {
|
|
||||||
enum Kind: Hashable, CaseIterable {
|
|
||||||
case status
|
|
||||||
case loadMore
|
|
||||||
case account
|
|
||||||
}
|
|
||||||
|
|
||||||
var kind: Kind {
|
|
||||||
switch item {
|
|
||||||
case .status:
|
|
||||||
return .status
|
|
||||||
case .loadMore:
|
|
||||||
return .loadMore
|
|
||||||
case .account:
|
|
||||||
return .account
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,6 +1,6 @@
|
||||||
// Copyright © 2020 Metabolist. All rights reserved.
|
// Copyright © 2020 Metabolist. All rights reserved.
|
||||||
|
|
||||||
public struct CollectionUpdate: Hashable {
|
public struct CollectionUpdate: Hashable {
|
||||||
public let items: [[CollectionItemIdentifier]]
|
public let items: [[CollectionItem]]
|
||||||
public let maintainScrollPosition: CollectionItemIdentifier?
|
public let maintainScrollPosition: CollectionItem?
|
||||||
}
|
}
|
||||||
|
|
|
@ -6,11 +6,16 @@ import ServiceLayer
|
||||||
|
|
||||||
public final class Identification: ObservableObject {
|
public final class Identification: ObservableObject {
|
||||||
@Published private(set) public var identity: Identity
|
@Published private(set) public var identity: Identity
|
||||||
|
@Published public var appPreferences: AppPreferences
|
||||||
let service: IdentityService
|
let service: IdentityService
|
||||||
|
|
||||||
init(identity: Identity, publisher: AnyPublisher<Identity, Never>, service: IdentityService) {
|
init(identity: Identity,
|
||||||
|
publisher: AnyPublisher<Identity, Never>,
|
||||||
|
service: IdentityService,
|
||||||
|
environment: AppEnvironment) {
|
||||||
self.identity = identity
|
self.identity = identity
|
||||||
self.service = service
|
self.service = service
|
||||||
|
appPreferences = AppPreferences(environment: environment)
|
||||||
|
|
||||||
DispatchQueue.main.async {
|
DispatchQueue.main.async {
|
||||||
publisher.dropFirst().assign(to: &self.$identity)
|
publisher.dropFirst().assign(to: &self.$identity)
|
||||||
|
|
|
@ -0,0 +1,13 @@
|
||||||
|
// Copyright © 2020 Metabolist. All rights reserved.
|
||||||
|
|
||||||
|
import Combine
|
||||||
|
import Foundation
|
||||||
|
import ServiceLayer
|
||||||
|
|
||||||
|
public final class MediaPreferencesViewModel: ObservableObject {
|
||||||
|
private let identification: Identification
|
||||||
|
|
||||||
|
public init(identification: Identification) {
|
||||||
|
self.identification = identification
|
||||||
|
}
|
||||||
|
}
|
|
@ -23,7 +23,7 @@ final public class ProfileViewModel {
|
||||||
identification: identification))
|
identification: identification))
|
||||||
|
|
||||||
profileService.accountServicePublisher
|
profileService.accountServicePublisher
|
||||||
.map(AccountViewModel.init(accountService:))
|
.map { AccountViewModel(accountService: $0, identification: identification) }
|
||||||
.assignErrorsToAlertItem(to: \.alertItem, on: self)
|
.assignErrorsToAlertItem(to: \.alertItem, on: self)
|
||||||
.assign(to: &$accountViewModel)
|
.assign(to: &$accountViewModel)
|
||||||
|
|
||||||
|
|
|
@ -84,25 +84,26 @@ private extension RootViewModel {
|
||||||
identityPublisher
|
identityPublisher
|
||||||
.filter { [weak self] in $0.id != self?.navigationViewModel?.identification.identity.id }
|
.filter { [weak self] in $0.id != self?.navigationViewModel?.identification.identity.id }
|
||||||
.map { [weak self] in
|
.map { [weak self] in
|
||||||
|
guard let self = self else { return nil }
|
||||||
|
|
||||||
let identification = Identification(
|
let identification = Identification(
|
||||||
identity: $0,
|
identity: $0,
|
||||||
publisher: identityPublisher.eraseToAnyPublisher(),
|
publisher: identityPublisher.eraseToAnyPublisher(),
|
||||||
service: identityService)
|
service: identityService,
|
||||||
|
environment: self.environment)
|
||||||
|
|
||||||
if let self = self {
|
identification.service.updateLastUse()
|
||||||
identification.service.updateLastUse()
|
.sink { _ in } receiveValue: { _ in }
|
||||||
.sink { _ in } receiveValue: { _ in }
|
.store(in: &self.cancellables)
|
||||||
.store(in: &self.cancellables)
|
|
||||||
|
|
||||||
self.userNotificationService.isAuthorized()
|
self.userNotificationService.isAuthorized()
|
||||||
.filter { $0 }
|
.filter { $0 }
|
||||||
.zip(self.registerForRemoteNotifications())
|
.zip(self.registerForRemoteNotifications())
|
||||||
.filter { identification.identity.lastRegisteredDeviceToken != $1 }
|
.filter { identification.identity.lastRegisteredDeviceToken != $1 }
|
||||||
.map { ($1, identification.identity.pushSubscriptionAlerts) }
|
.map { ($1, identification.identity.pushSubscriptionAlerts) }
|
||||||
.flatMap(identification.service.createPushSubscription(deviceToken:alerts:))
|
.flatMap(identification.service.createPushSubscription(deviceToken:alerts:))
|
||||||
.sink { _ in } receiveValue: { _ in }
|
.sink { _ in } receiveValue: { _ in }
|
||||||
.store(in: &self.cancellables)
|
.store(in: &self.cancellables)
|
||||||
}
|
|
||||||
|
|
||||||
return NavigationViewModel(identification: identification)
|
return NavigationViewModel(identification: identification)
|
||||||
}
|
}
|
||||||
|
|
|
@ -18,11 +18,11 @@ public struct StatusViewModel: CollectionItemViewModel {
|
||||||
public let pollOptionTitles: [String]
|
public let pollOptionTitles: [String]
|
||||||
public let pollEmoji: [Emoji]
|
public let pollEmoji: [Emoji]
|
||||||
public var configuration = CollectionItem.StatusConfiguration.default
|
public var configuration = CollectionItem.StatusConfiguration.default
|
||||||
|
public let identification: Identification
|
||||||
public let events: AnyPublisher<AnyPublisher<CollectionItemEvent, Error>, Never>
|
public let events: AnyPublisher<AnyPublisher<CollectionItemEvent, Error>, Never>
|
||||||
|
|
||||||
private let statusService: StatusService
|
private let statusService: StatusService
|
||||||
private let eventsSubject = PassthroughSubject<AnyPublisher<CollectionItemEvent, Error>, Never>()
|
private let eventsSubject = PassthroughSubject<AnyPublisher<CollectionItemEvent, Error>, Never>()
|
||||||
private let identification: Identification
|
|
||||||
|
|
||||||
init(statusService: StatusService, identification: Identification) {
|
init(statusService: StatusService, identification: Identification) {
|
||||||
self.statusService = statusService
|
self.statusService = statusService
|
||||||
|
@ -77,6 +77,8 @@ public extension StatusViewModel {
|
||||||
|
|
||||||
var avatarURL: URL { statusService.status.displayStatus.account.avatar }
|
var avatarURL: URL { statusService.status.displayStatus.account.avatar }
|
||||||
|
|
||||||
|
var avatarStaticURL: URL { statusService.status.displayStatus.account.avatarStatic }
|
||||||
|
|
||||||
var time: String? { statusService.status.displayStatus.createdAt.timeAgo }
|
var time: String? { statusService.status.displayStatus.createdAt.timeAgo }
|
||||||
|
|
||||||
var contextParentTime: String {
|
var contextParentTime: String {
|
||||||
|
|
|
@ -12,7 +12,16 @@ class AccountHeaderView: UIView {
|
||||||
var viewModel: ProfileViewModel? {
|
var viewModel: ProfileViewModel? {
|
||||||
didSet {
|
didSet {
|
||||||
if let accountViewModel = viewModel?.accountViewModel {
|
if let accountViewModel = viewModel?.accountViewModel {
|
||||||
headerImageView.kf.setImage(with: accountViewModel.headerURL)
|
let appPreferences = accountViewModel.identification.appPreferences
|
||||||
|
let headerURL: URL
|
||||||
|
|
||||||
|
if !appPreferences.shouldReduceMotion, appPreferences.animateHeaders {
|
||||||
|
headerURL = accountViewModel.headerURL
|
||||||
|
} else {
|
||||||
|
headerURL = accountViewModel.headerStaticURL
|
||||||
|
}
|
||||||
|
|
||||||
|
headerImageView.kf.setImage(with: headerURL)
|
||||||
|
|
||||||
let noteFont = UIFont.preferredFont(forTextStyle: .callout)
|
let noteFont = UIFont.preferredFont(forTextStyle: .callout)
|
||||||
let mutableNote = NSMutableAttributedString(attributedString: accountViewModel.note)
|
let mutableNote = NSMutableAttributedString(attributedString: accountViewModel.note)
|
||||||
|
|
|
@ -34,7 +34,6 @@ extension AccountView: UIContentView {
|
||||||
|
|
||||||
self.accountConfiguration = accountConfiguration
|
self.accountConfiguration = accountConfiguration
|
||||||
|
|
||||||
avatarImageView.kf.cancelDownloadTask()
|
|
||||||
applyAccountConfiguration()
|
applyAccountConfiguration()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -97,7 +96,16 @@ private extension AccountView {
|
||||||
}
|
}
|
||||||
|
|
||||||
func applyAccountConfiguration() {
|
func applyAccountConfiguration() {
|
||||||
avatarImageView.kf.setImage(with: accountConfiguration.viewModel.avatarURL)
|
let appPreferences = accountConfiguration.viewModel.identification.appPreferences
|
||||||
|
let avatarURL: URL
|
||||||
|
|
||||||
|
if !appPreferences.shouldReduceMotion && appPreferences.animateAvatars == .everywhere {
|
||||||
|
avatarURL = accountConfiguration.viewModel.avatarURL
|
||||||
|
} else {
|
||||||
|
avatarURL = accountConfiguration.viewModel.avatarStaticURL
|
||||||
|
}
|
||||||
|
|
||||||
|
avatarImageView.kf.setImage(with: avatarURL)
|
||||||
|
|
||||||
if accountConfiguration.viewModel.displayName == "" {
|
if accountConfiguration.viewModel.displayName == "" {
|
||||||
displayNameLabel.isHidden = true
|
displayNameLabel.isHidden = true
|
||||||
|
|
91
Views/MediaPreferencesView.swift
Normal file
91
Views/MediaPreferencesView.swift
Normal file
|
@ -0,0 +1,91 @@
|
||||||
|
// Copyright © 2020 Metabolist. All rights reserved.
|
||||||
|
|
||||||
|
import SwiftUI
|
||||||
|
import ViewModels
|
||||||
|
|
||||||
|
struct MediaPreferencesView: View {
|
||||||
|
@StateObject var viewModel: MediaPreferencesViewModel
|
||||||
|
@EnvironmentObject var identification: Identification
|
||||||
|
@Environment(\.accessibilityReduceMotion) var accessibilityReduceMotion
|
||||||
|
|
||||||
|
var body: some View {
|
||||||
|
Form {
|
||||||
|
if accessibilityReduceMotion {
|
||||||
|
Section {
|
||||||
|
Toggle("preferences.media.use-system-reduce-motion",
|
||||||
|
isOn: $identification.appPreferences.useSystemReduceMotionForMedia)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Section(header: Text("preferences.media.autoplay")) {
|
||||||
|
Picker("preferences.media.autoplay.gifs",
|
||||||
|
selection: reduceMotion ? .constant(.never) : $identification.appPreferences.autoplayGIFs) {
|
||||||
|
ForEach(AppPreferences.Autoplay.allCases) { option in
|
||||||
|
Text(option.localizedStringKey).tag(option)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Picker("preferences.media.autoplay.videos",
|
||||||
|
selection: reduceMotion ? .constant(.never) : $identification.appPreferences.autoplayVideos) {
|
||||||
|
ForEach(AppPreferences.Autoplay.allCases) { option in
|
||||||
|
Text(option.localizedStringKey).tag(option)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.disabled(reduceMotion)
|
||||||
|
Section(header: Text("preferences.media.avatars")) {
|
||||||
|
Picker("preferences.media.avatars.animate",
|
||||||
|
selection: reduceMotion ? .constant(.never) : $identification.appPreferences.animateAvatars) {
|
||||||
|
ForEach(AppPreferences.AnimateAvatars.allCases) { option in
|
||||||
|
Text(option.localizedStringKey).tag(option)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.disabled(reduceMotion)
|
||||||
|
}
|
||||||
|
Section(header: Text("preferences.media.headers")) {
|
||||||
|
Toggle("preferences.media.headers.animate",
|
||||||
|
isOn: reduceMotion ? .constant(false) : $identification.appPreferences.animateHeaders)
|
||||||
|
.disabled(reduceMotion)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.navigationTitle("preferences.media")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private extension MediaPreferencesView {
|
||||||
|
var reduceMotion: Bool {
|
||||||
|
identification.appPreferences.shouldReduceMotion
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
extension AppPreferences.AnimateAvatars {
|
||||||
|
var localizedStringKey: LocalizedStringKey {
|
||||||
|
switch self {
|
||||||
|
case .everywhere:
|
||||||
|
return "preferences.media.avatars.animate.everywhere"
|
||||||
|
case .profiles:
|
||||||
|
return "preferences.media.avatars.animate.profiles"
|
||||||
|
case .never:
|
||||||
|
return "preferences.media.avatars.animate.never"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
extension AppPreferences.Autoplay {
|
||||||
|
var localizedStringKey: LocalizedStringKey {
|
||||||
|
switch self {
|
||||||
|
case .always:
|
||||||
|
return "preferences.media.autoplay.always"
|
||||||
|
case .wifi:
|
||||||
|
return "preferences.media.autoplay.wifi"
|
||||||
|
case .never:
|
||||||
|
return "preferences.media.autoplay.never"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#if DEBUG
|
||||||
|
struct MediaPreferencesView_Previews: PreviewProvider {
|
||||||
|
static var previews: some View {
|
||||||
|
MediaPreferencesView(viewModel: .init(identification: .preview))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
#endif
|
26
Views/PlayerView.swift
Normal file
26
Views/PlayerView.swift
Normal file
|
@ -0,0 +1,26 @@
|
||||||
|
// Copyright © 2020 Metabolist. All rights reserved.
|
||||||
|
|
||||||
|
import AVKit
|
||||||
|
import UIKit
|
||||||
|
|
||||||
|
class PlayerView: UIView {
|
||||||
|
override class var layerClass: AnyClass {
|
||||||
|
AVPlayerLayer.self
|
||||||
|
}
|
||||||
|
|
||||||
|
var player: AVPlayer? {
|
||||||
|
get { (layer as? AVPlayerLayer)?.player }
|
||||||
|
set { (layer as? AVPlayerLayer)?.player = newValue }
|
||||||
|
}
|
||||||
|
|
||||||
|
override init(frame: CGRect) {
|
||||||
|
super.init(frame: frame)
|
||||||
|
|
||||||
|
(layer as? AVPlayerLayer)?.videoGravity = .resizeAspectFill
|
||||||
|
}
|
||||||
|
|
||||||
|
@available(*, unavailable)
|
||||||
|
required init?(coder: NSCoder) {
|
||||||
|
fatalError("init(coder:) has not been implemented")
|
||||||
|
}
|
||||||
|
}
|
|
@ -22,6 +22,11 @@ struct PreferencesView: View {
|
||||||
viewModel: .init(identification: identification)))
|
viewModel: .init(identification: identification)))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
Section(header: Text("preferences.app")) {
|
||||||
|
NavigationLink("preferences.media",
|
||||||
|
destination: MediaPreferencesView(
|
||||||
|
viewModel: .init(identification: identification)))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
.navigationTitle("preferences")
|
.navigationTitle("preferences")
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,46 +1,34 @@
|
||||||
// Copyright © 2020 Metabolist. All rights reserved.
|
// Copyright © 2020 Metabolist. All rights reserved.
|
||||||
|
|
||||||
|
import AVKit
|
||||||
import Kingfisher
|
import Kingfisher
|
||||||
import UIKit
|
import UIKit
|
||||||
import ViewModels
|
import ViewModels
|
||||||
|
|
||||||
final class StatusAttachmentView: UIView {
|
final class StatusAttachmentView: UIView {
|
||||||
|
let playerView = PlayerView()
|
||||||
let imageView = AnimatedImageView()
|
let imageView = AnimatedImageView()
|
||||||
let button = UIButton()
|
let button = UIButton()
|
||||||
let viewModel: AttachmentViewModel
|
|
||||||
|
var playing: Bool = false {
|
||||||
|
didSet {
|
||||||
|
if playing {
|
||||||
|
play()
|
||||||
|
} else {
|
||||||
|
stop()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private let viewModel: AttachmentViewModel
|
||||||
|
private var playerLooper: AVPlayerLooper?
|
||||||
|
|
||||||
init(viewModel: AttachmentViewModel) {
|
init(viewModel: AttachmentViewModel) {
|
||||||
self.viewModel = viewModel
|
self.viewModel = viewModel
|
||||||
|
|
||||||
super.init(frame: .zero)
|
super.init(frame: .zero)
|
||||||
|
|
||||||
layoutMargins = .zero
|
initialSetup()
|
||||||
addSubview(imageView)
|
|
||||||
imageView.translatesAutoresizingMaskIntoConstraints = false
|
|
||||||
imageView.contentMode = .scaleAspectFill
|
|
||||||
imageView.clipsToBounds = true
|
|
||||||
|
|
||||||
addSubview(button)
|
|
||||||
button.translatesAutoresizingMaskIntoConstraints = false
|
|
||||||
button.setBackgroundImage(.highlightedButtonBackground, for: .highlighted)
|
|
||||||
|
|
||||||
switch viewModel.attachment.type {
|
|
||||||
case .image:
|
|
||||||
imageView.kf.setImage(with: viewModel.attachment.previewUrl)
|
|
||||||
default:
|
|
||||||
break
|
|
||||||
}
|
|
||||||
|
|
||||||
NSLayoutConstraint.activate([
|
|
||||||
imageView.leadingAnchor.constraint(equalTo: layoutMarginsGuide.leadingAnchor),
|
|
||||||
imageView.trailingAnchor.constraint(equalTo: layoutMarginsGuide.trailingAnchor),
|
|
||||||
imageView.topAnchor.constraint(equalTo: layoutMarginsGuide.topAnchor),
|
|
||||||
imageView.bottomAnchor.constraint(equalTo: layoutMarginsGuide.bottomAnchor),
|
|
||||||
button.leadingAnchor.constraint(equalTo: layoutMarginsGuide.leadingAnchor),
|
|
||||||
button.trailingAnchor.constraint(equalTo: layoutMarginsGuide.trailingAnchor),
|
|
||||||
button.topAnchor.constraint(equalTo: layoutMarginsGuide.topAnchor),
|
|
||||||
button.bottomAnchor.constraint(equalTo: layoutMarginsGuide.bottomAnchor)
|
|
||||||
])
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@available(*, unavailable)
|
@available(*, unavailable)
|
||||||
|
@ -48,3 +36,112 @@ final class StatusAttachmentView: UIView {
|
||||||
fatalError("init(coder:) has not been implemented")
|
fatalError("init(coder:) has not been implemented")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
extension StatusAttachmentView {
|
||||||
|
func play() {
|
||||||
|
let player = PlayerCache.shared.player(url: viewModel.attachment.url)
|
||||||
|
|
||||||
|
if let cachedPlayerLooper = Self.playerLooperCache[player] {
|
||||||
|
playerLooper = cachedPlayerLooper
|
||||||
|
} else if let item = player.currentItem {
|
||||||
|
playerLooper = AVPlayerLooper(player: player, templateItem: item)
|
||||||
|
Self.playerLooperCache[player] = playerLooper
|
||||||
|
}
|
||||||
|
|
||||||
|
player.isMuted = true
|
||||||
|
player.play()
|
||||||
|
playerView.player = player
|
||||||
|
playerView.isHidden = false
|
||||||
|
}
|
||||||
|
|
||||||
|
func stop() {
|
||||||
|
if let item = playerView.player?.currentItem {
|
||||||
|
let imageGenerator = AVAssetImageGenerator(asset: item.asset)
|
||||||
|
imageGenerator.requestedTimeToleranceAfter = .zero
|
||||||
|
imageGenerator.requestedTimeToleranceBefore = .zero
|
||||||
|
|
||||||
|
if let image = try? imageGenerator.copyCGImage(at: item.currentTime(), actualTime: nil) {
|
||||||
|
imageView.image = .init(cgImage: image)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
playerView.player = nil
|
||||||
|
playerView.isHidden = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private extension StatusAttachmentView {
|
||||||
|
static var playerLooperCache = [AVQueuePlayer: AVPlayerLooper]()
|
||||||
|
|
||||||
|
// swiftlint:disable:next function_body_length
|
||||||
|
func initialSetup() {
|
||||||
|
addSubview(imageView)
|
||||||
|
imageView.translatesAutoresizingMaskIntoConstraints = false
|
||||||
|
imageView.contentMode = .scaleAspectFill
|
||||||
|
imageView.clipsToBounds = true
|
||||||
|
|
||||||
|
let blurEffect = UIBlurEffect(style: .systemUltraThinMaterial)
|
||||||
|
let playView = UIVisualEffectView(effect: blurEffect)
|
||||||
|
let playVibrancyView = UIVisualEffectView(effect: UIVibrancyEffect(blurEffect: blurEffect))
|
||||||
|
let playImageView = UIImageView(
|
||||||
|
image: UIImage(systemName: "play.circle",
|
||||||
|
withConfiguration: UIImage.SymbolConfiguration(textStyle: .largeTitle)))
|
||||||
|
|
||||||
|
playImageView.translatesAutoresizingMaskIntoConstraints = false
|
||||||
|
playVibrancyView.translatesAutoresizingMaskIntoConstraints = false
|
||||||
|
playVibrancyView.contentView.addSubview(playImageView)
|
||||||
|
playView.contentView.addSubview(playVibrancyView)
|
||||||
|
|
||||||
|
addSubview(playView)
|
||||||
|
playView.translatesAutoresizingMaskIntoConstraints = false
|
||||||
|
playView.clipsToBounds = true
|
||||||
|
playView.layer.cornerRadius = .defaultCornerRadius
|
||||||
|
playView.isHidden = viewModel.attachment.type == .image
|
||||||
|
|
||||||
|
addSubview(playerView)
|
||||||
|
playerView.translatesAutoresizingMaskIntoConstraints = false
|
||||||
|
playerView.isHidden = true
|
||||||
|
|
||||||
|
addSubview(button)
|
||||||
|
button.translatesAutoresizingMaskIntoConstraints = false
|
||||||
|
button.setBackgroundImage(.highlightedButtonBackground, for: .highlighted)
|
||||||
|
|
||||||
|
switch viewModel.attachment.type {
|
||||||
|
case .image, .video, .gifv:
|
||||||
|
imageView.kf.setImage(with: viewModel.attachment.previewUrl)
|
||||||
|
default:
|
||||||
|
break
|
||||||
|
}
|
||||||
|
|
||||||
|
NSLayoutConstraint.activate([
|
||||||
|
imageView.leadingAnchor.constraint(equalTo: leadingAnchor),
|
||||||
|
imageView.trailingAnchor.constraint(equalTo: trailingAnchor),
|
||||||
|
imageView.topAnchor.constraint(equalTo: topAnchor),
|
||||||
|
imageView.bottomAnchor.constraint(equalTo: bottomAnchor),
|
||||||
|
playerView.leadingAnchor.constraint(equalTo: leadingAnchor),
|
||||||
|
playerView.trailingAnchor.constraint(equalTo: trailingAnchor),
|
||||||
|
playerView.topAnchor.constraint(equalTo: topAnchor),
|
||||||
|
playerView.bottomAnchor.constraint(equalTo: bottomAnchor),
|
||||||
|
playImageView.centerXAnchor.constraint(equalTo: playView.contentView.centerXAnchor),
|
||||||
|
playImageView.centerYAnchor.constraint(equalTo: playView.contentView.centerYAnchor),
|
||||||
|
playVibrancyView.leadingAnchor.constraint(equalTo: playView.leadingAnchor),
|
||||||
|
playVibrancyView.topAnchor.constraint(equalTo: playView.topAnchor),
|
||||||
|
playVibrancyView.trailingAnchor.constraint(equalTo: playView.trailingAnchor),
|
||||||
|
playVibrancyView.bottomAnchor.constraint(equalTo: playView.bottomAnchor),
|
||||||
|
playView.centerXAnchor.constraint(equalTo: centerXAnchor),
|
||||||
|
playView.centerYAnchor.constraint(equalTo: centerYAnchor),
|
||||||
|
playView.trailingAnchor.constraint(
|
||||||
|
equalTo: playImageView.trailingAnchor, constant: .compactSpacing),
|
||||||
|
playView.bottomAnchor.constraint(
|
||||||
|
equalTo: playImageView.bottomAnchor, constant: .compactSpacing),
|
||||||
|
playImageView.topAnchor.constraint(
|
||||||
|
equalTo: playView.topAnchor, constant: .compactSpacing),
|
||||||
|
playImageView.leadingAnchor.constraint(
|
||||||
|
equalTo: playView.leadingAnchor, constant: .compactSpacing),
|
||||||
|
button.leadingAnchor.constraint(equalTo: leadingAnchor),
|
||||||
|
button.trailingAnchor.constraint(equalTo: trailingAnchor),
|
||||||
|
button.topAnchor.constraint(equalTo: topAnchor),
|
||||||
|
button.bottomAnchor.constraint(equalTo: bottomAnchor)
|
||||||
|
])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -1,5 +1,7 @@
|
||||||
// Copyright © 2020 Metabolist. All rights reserved.
|
// Copyright © 2020 Metabolist. All rights reserved.
|
||||||
|
|
||||||
|
import Combine
|
||||||
|
import Network
|
||||||
import UIKit
|
import UIKit
|
||||||
import ViewModels
|
import ViewModels
|
||||||
|
|
||||||
|
@ -12,6 +14,7 @@ final class StatusAttachmentsView: UIView {
|
||||||
private let hideButtonBackground = UIVisualEffectView(effect: UIBlurEffect(style: .systemMaterial))
|
private let hideButtonBackground = UIVisualEffectView(effect: UIBlurEffect(style: .systemMaterial))
|
||||||
private let hideButton = UIButton()
|
private let hideButton = UIButton()
|
||||||
private var aspectRatioConstraint: NSLayoutConstraint?
|
private var aspectRatioConstraint: NSLayoutConstraint?
|
||||||
|
private var cancellables = Set<AnyCancellable>()
|
||||||
|
|
||||||
var viewModel: StatusViewModel? {
|
var viewModel: StatusViewModel? {
|
||||||
didSet {
|
didSet {
|
||||||
|
@ -75,6 +78,27 @@ final class StatusAttachmentsView: UIView {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
extension StatusAttachmentsView {
|
||||||
|
var shouldAutoplay: Bool {
|
||||||
|
guard !isHidden, let viewModel = viewModel, viewModel.shouldShowAttachments else { return false }
|
||||||
|
|
||||||
|
let appPreferences = viewModel.identification.appPreferences
|
||||||
|
let onWifi = NWPathMonitor(requiredInterfaceType: .wifi).currentPath.status == .satisfied
|
||||||
|
let hasVideoAttachment = viewModel.attachmentViewModels.contains { $0.attachment.type == .video }
|
||||||
|
let shouldAutoplayVideo = appPreferences.autoplayVideos == .always
|
||||||
|
|| appPreferences.autoplayVideos == .wifi && onWifi
|
||||||
|
|
||||||
|
if hasVideoAttachment && shouldAutoplayVideo {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
let hasGIFAttachment = viewModel.attachmentViewModels.contains { $0.attachment.type == .gifv }
|
||||||
|
let shouldAutoplayGIF = appPreferences.autoplayGIFs == .always || appPreferences.autoplayGIFs == .wifi && onWifi
|
||||||
|
|
||||||
|
return hasGIFAttachment && shouldAutoplayGIF
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private extension StatusAttachmentsView {
|
private extension StatusAttachmentsView {
|
||||||
// swiftlint:disable:next function_body_length
|
// swiftlint:disable:next function_body_length
|
||||||
func initialSetup() {
|
func initialSetup() {
|
||||||
|
@ -147,5 +171,20 @@ private extension StatusAttachmentsView {
|
||||||
curtainButton.trailingAnchor.constraint(equalTo: curtain.contentView.trailingAnchor),
|
curtainButton.trailingAnchor.constraint(equalTo: curtain.contentView.trailingAnchor),
|
||||||
curtainButton.bottomAnchor.constraint(equalTo: curtain.contentView.bottomAnchor)
|
curtainButton.bottomAnchor.constraint(equalTo: curtain.contentView.bottomAnchor)
|
||||||
])
|
])
|
||||||
|
|
||||||
|
NotificationCenter.default.publisher(for: TableViewController.autoplayableAttachmentsViewNotification)
|
||||||
|
.sink { [weak self] in
|
||||||
|
guard let self = self else { return }
|
||||||
|
|
||||||
|
for attachmentView in self.attachmentViews {
|
||||||
|
attachmentView.playing = $0.object as? Self === self
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.store(in: &cancellables)
|
||||||
|
}
|
||||||
|
|
||||||
|
var attachmentViews: [StatusAttachmentView] {
|
||||||
|
(leftStackView.arrangedSubviews + rightStackView.arrangedSubviews)
|
||||||
|
.compactMap { $0 as? StatusAttachmentView }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -292,12 +292,22 @@ private extension StatusView {
|
||||||
|
|
||||||
func applyStatusConfiguration() {
|
func applyStatusConfiguration() {
|
||||||
let viewModel = statusConfiguration.viewModel
|
let viewModel = statusConfiguration.viewModel
|
||||||
|
let appPreferences = viewModel.identification.appPreferences
|
||||||
let isContextParent = viewModel.configuration.isContextParent
|
let isContextParent = viewModel.configuration.isContextParent
|
||||||
let mutableContent = NSMutableAttributedString(attributedString: viewModel.content)
|
let mutableContent = NSMutableAttributedString(attributedString: viewModel.content)
|
||||||
let mutableDisplayName = NSMutableAttributedString(string: viewModel.displayName)
|
let mutableDisplayName = NSMutableAttributedString(string: viewModel.displayName)
|
||||||
let mutableSpoilerText = NSMutableAttributedString(string: viewModel.spoilerText)
|
let mutableSpoilerText = NSMutableAttributedString(string: viewModel.spoilerText)
|
||||||
let contentFont = UIFont.preferredFont(forTextStyle: isContextParent ? .title3 : .callout)
|
let contentFont = UIFont.preferredFont(forTextStyle: isContextParent ? .title3 : .callout)
|
||||||
let contentRange = NSRange(location: 0, length: mutableContent.length)
|
let contentRange = NSRange(location: 0, length: mutableContent.length)
|
||||||
|
let avatarURL: URL
|
||||||
|
|
||||||
|
if !appPreferences.shouldReduceMotion && appPreferences.animateAvatars == .everywhere {
|
||||||
|
avatarURL = viewModel.avatarURL
|
||||||
|
} else {
|
||||||
|
avatarURL = viewModel.avatarStaticURL
|
||||||
|
}
|
||||||
|
|
||||||
|
avatarImageView.kf.setImage(with: avatarURL)
|
||||||
|
|
||||||
contentTextView.shouldFallthrough = !isContextParent
|
contentTextView.shouldFallthrough = !isContextParent
|
||||||
sideStackView.isHidden = isContextParent
|
sideStackView.isHidden = isContextParent
|
||||||
|
@ -450,8 +460,6 @@ private extension StatusView {
|
||||||
|
|
||||||
favoriteButton.tintColor = favoriteColor
|
favoriteButton.tintColor = favoriteColor
|
||||||
favoriteButton.setTitleColor(favoriteColor, for: .normal)
|
favoriteButton.setTitleColor(favoriteColor, for: .normal)
|
||||||
|
|
||||||
avatarImageView.kf.setImage(with: viewModel.avatarURL)
|
|
||||||
}
|
}
|
||||||
// swiftlint:enable function_body_length
|
// swiftlint:enable function_body_length
|
||||||
|
|
||||||
|
|
Loading…
Reference in a new issue