Snappy UI - Offloading non UI task to background threads

This gives a huge increase in perceived performance. The UI is now much more responsive since some tasks are run in the background and don't block the UI anymore.
This commit is contained in:
Toni Förster 2024-05-17 16:16:48 +02:00
parent 1fe8a32fb8
commit e35f8b7892
No known key found for this signature in database
GPG Key ID: 292F3E5086C83FC7
4 changed files with 110 additions and 58 deletions

View File

@ -10,7 +10,7 @@ import SwiftUI
final class MPVBackend: PlayerBackend { final class MPVBackend: PlayerBackend {
static var timeUpdateInterval = 0.5 static var timeUpdateInterval = 0.5
static var networkStateUpdateInterval = 1.0 static var networkStateUpdateInterval = 0.1
private var logger = Logger(label: "mpv-backend") private var logger = Logger(label: "mpv-backend")

View File

@ -89,6 +89,9 @@ struct FavoriteItemView: View {
loadCacheAndResource() loadCacheAndResource()
} }
} }
.onDisappear {
resource?.removeObservers(ownedBy: store)
}
.onChange(of: player.currentVideo) { _ in reloadVisibleWatches() } .onChange(of: player.currentVideo) { _ in reloadVisibleWatches() }
.onChange(of: hideShorts) { _ in reloadVisibleWatches() } .onChange(of: hideShorts) { _ in reloadVisibleWatches() }
.onChange(of: hideWatched) { _ in reloadVisibleWatches() } .onChange(of: hideWatched) { _ in reloadVisibleWatches() }
@ -96,13 +99,12 @@ struct FavoriteItemView: View {
} }
.id(watchModel.historyToken) .id(watchModel.historyToken)
.onChange(of: accounts.current) { _ in .onChange(of: accounts.current) { _ in
resource?.removeObservers(ownedBy: store)
resource?.addObserver(store) resource?.addObserver(store)
loadCacheAndResource(force: true) loadCacheAndResource(force: true)
} }
.onChange(of: watchModel.historyToken) { _ in .onChange(of: watchModel.historyToken) { _ in
Delay.by(0.5) { reloadVisibleWatches()
reloadVisibleWatches()
}
} }
} }
@ -164,12 +166,15 @@ struct FavoriteItemView: View {
.prefix(favoritesModel.limit(item)) .prefix(favoritesModel.limit(item))
) )
let last = watches.last let last = watches.last
for watch in watches { for watch in watches {
player.loadHistoryVideoDetails(watch) { player.loadHistoryVideoDetails(watch) {
guard let video = player.historyVideo(watch.videoID), itemVisible(.init(video: video)) else { return } guard let video = player.historyVideo(watch.videoID), itemVisible(.init(video: video)) else { return }
visibleWatches.append(watch) visibleWatches.append(watch)
guard watch == last else { return }
visibleWatches.sort { $0.watchedAt ?? Date() > $1.watchedAt ?? Date() } if watch == last {
visibleWatches.sort { $0.watchedAt ?? Date() > $1.watchedAt ?? Date() }
}
} }
} }
} }

View File

@ -5,18 +5,52 @@ final class FavoriteResourceObserver: ObservableObject, ResourceObserver {
@Published var contentItems = [ContentItem]() @Published var contentItems = [ContentItem]()
func resourceChanged(_ resource: Resource, event _: ResourceEvent) { func resourceChanged(_ resource: Resource, event _: ResourceEvent) {
// swiftlint:disable discouraged_optional_collection
var newVideos: [Video]?
var newItems: [ContentItem]?
// swiftlint:enable discouraged_optional_collection
var newChannel: Channel?
var newChannelPlaylist: ChannelPlaylist?
var newPlaylist: Playlist?
var newPage: SearchPage?
if let videos: [Video] = resource.typedContent() { if let videos: [Video] = resource.typedContent() {
contentItems = videos.map { ContentItem(video: $0) } newVideos = videos
} else if let channel: Channel = resource.typedContent() { } else if let channel: Channel = resource.typedContent() {
contentItems = channel.videos.map { ContentItem(video: $0) } newChannel = channel
} else if let playlist: ChannelPlaylist = resource.typedContent() { } else if let playlist: ChannelPlaylist = resource.typedContent() {
contentItems = playlist.videos.map { ContentItem(video: $0) } newChannelPlaylist = playlist
} else if let playlist: Playlist = resource.typedContent() { } else if let playlist: Playlist = resource.typedContent() {
contentItems = playlist.videos.map { ContentItem(video: $0) } newPlaylist = playlist
} else if let page: SearchPage = resource.typedContent() { } else if let page: SearchPage = resource.typedContent() {
contentItems = page.results newPage = page
} else if let items: [ContentItem] = resource.typedContent() { } else if let items: [ContentItem] = resource.typedContent() {
contentItems = items newItems = items
}
DispatchQueue.global(qos: .userInitiated).async {
var newContentItems: [ContentItem] = []
if let videos = newVideos {
newContentItems = videos.map { ContentItem(video: $0) }
} else if let channel = newChannel {
newContentItems = channel.videos.map { ContentItem(video: $0) }
} else if let playlist = newChannelPlaylist {
newContentItems = playlist.videos.map { ContentItem(video: $0) }
} else if let playlist = newPlaylist {
newContentItems = playlist.videos.map { ContentItem(video: $0) }
} else if let page = newPage {
newContentItems = page.results
} else if let items = newItems {
newContentItems = items
}
DispatchQueue.main.async {
if !newContentItems.isEmpty {
self.contentItems = newContentItems
}
}
} }
} }
} }

View File

@ -150,61 +150,74 @@ struct YatteeApp: App {
} }
configured = true configured = true
#if DEBUG DispatchQueue.main.async {
SiestaLog.Category.enabled = .common #if DEBUG
#endif SiestaLog.Category.enabled = .common
SDImageCodersManager.shared.addCoder(SDImageAWebPCoder.shared) #endif
SDWebImageManager.defaultImageCache = PINCache(name: "stream.yattee.app") SDImageCodersManager.shared.addCoder(SDImageAWebPCoder.shared)
SDWebImageManager.defaultImageCache = PINCache(name: "stream.yattee.app")
if !Defaults[.lastAccountIsPublic] { if !Defaults[.lastAccountIsPublic] {
AccountsModel.shared.configureAccount() AccountsModel.shared.configureAccount()
}
if let countryOfPublicInstances = Defaults[.countryOfPublicInstances] {
InstancesManifest.shared.setPublicAccount(countryOfPublicInstances, asCurrent: AccountsModel.shared.current.isNil)
}
if !AccountsModel.shared.current.isNil {
player.restoreQueue()
}
if !Defaults[.saveRecents] {
recents.clear()
}
let startupSection = Defaults[.startupSection]
var section: TabSelection? = startupSection.tabSelection
#if os(macOS)
if section == .playlists {
section = .search
} }
#endif
NavigationModel.shared.tabSelection = section ?? .search if let countryOfPublicInstances = Defaults[.countryOfPublicInstances] {
InstancesManifest.shared.setPublicAccount(countryOfPublicInstances, asCurrent: AccountsModel.shared.current.isNil)
}
playlists.load() if !AccountsModel.shared.current.isNil {
player.restoreQueue()
}
#if !os(macOS) DispatchQueue.global(qos: .userInitiated).async {
player.updateRemoteCommandCenter() if !Defaults[.saveRecents] {
#endif recents.clear()
if player.presentingPlayer {
player.presentingPlayer = false
}
#if os(iOS)
DispatchQueue.main.asyncAfter(deadline: .now() + 1) {
if Defaults[.lockPortraitWhenBrowsing] {
Orientation.lockOrientation(.portrait, andRotateTo: .portrait)
} }
} }
#endif
URLBookmarkModel.shared.refreshAll() let startupSection = Defaults[.startupSection]
var section: TabSelection? = startupSection.tabSelection
migrateHomeHistoryItems() #if os(macOS)
migrateQualityProfiles() if section == .playlists {
section = .search
}
#endif
NavigationModel.shared.tabSelection = section ?? .search
DispatchQueue.global(qos: .userInitiated).async {
playlists.load()
}
#if !os(macOS)
player.updateRemoteCommandCenter()
#endif
if player.presentingPlayer {
player.presentingPlayer = false
}
#if os(iOS)
DispatchQueue.main.asyncAfter(deadline: .now() + 1) {
if Defaults[.lockPortraitWhenBrowsing] {
Orientation.lockOrientation(.all, andRotateTo: .portrait)
}
}
#endif
DispatchQueue.global(qos: .userInitiated).async {
URLBookmarkModel.shared.refreshAll()
}
DispatchQueue.global(qos: .userInitiated).async {
self.migrateHomeHistoryItems()
}
DispatchQueue.global(qos: .userInitiated).async {
self.migrateQualityProfiles()
}
}
} }
func migrateHomeHistoryItems() { func migrateHomeHistoryItems() {