Add infinite scroll for search (fixes #5)

This commit is contained in:
Arkadiusz Fal
2022-01-05 00:18:01 +01:00
parent 3326088081
commit ea6363ba65
14 changed files with 145 additions and 47 deletions

View File

@@ -91,17 +91,19 @@ final class InvidiousAPI: Service, ObservableObject, VideosAPI {
content.json.arrayValue.map(self.extractVideo)
}
configureTransformer(pathPattern("search"), requestMethods: [.get]) { (content: Entity<JSON>) -> [ContentItem] in
content.json.arrayValue.map {
let type = $0.dictionaryValue["type"]?.stringValue
configureTransformer(pathPattern("search"), requestMethods: [.get]) { (content: Entity<JSON>) -> SearchPage in
let results = content.json.arrayValue.compactMap { json -> ContentItem in
let type = json.dictionaryValue["type"]?.stringValue
if type == "channel" {
return ContentItem(channel: self.extractChannel(from: $0))
return ContentItem(channel: self.extractChannel(from: json))
} else if type == "playlist" {
return ContentItem(playlist: self.extractChannelPlaylist(from: $0))
return ContentItem(playlist: self.extractChannelPlaylist(from: json))
}
return ContentItem(video: self.extractVideo(from: $0))
return ContentItem(video: self.extractVideo(from: json))
}
return SearchPage(results: results, last: results.isEmpty)
}
configureTransformer(pathPattern("search/suggestions"), requestMethods: [.get]) { (content: Entity<JSON>) -> [String] in
@@ -238,7 +240,7 @@ final class InvidiousAPI: Service, ObservableObject, VideosAPI {
resource(baseURL: account.url, path: basePathAppending("playlists/\(id)"))
}
func search(_ query: SearchQuery) -> Resource {
func search(_ query: SearchQuery, page: String?) -> Resource {
var resource = resource(baseURL: account.url, path: basePathAppending("search"))
.withParam("q", searchQuery(query.query))
.withParam("sort_by", query.sortBy.parameter)
@@ -252,6 +254,10 @@ final class InvidiousAPI: Service, ObservableObject, VideosAPI {
resource = resource.withParam("duration", duration.rawValue)
}
if let page = page {
resource = resource.withParam("page", page)
}
return resource
}

View File

@@ -51,8 +51,13 @@ final class PipedAPI: Service, ObservableObject, VideosAPI {
self.extractVideos(from: content.json)
}
configureTransformer(pathPattern("search")) { (content: Entity<JSON>) -> [ContentItem] in
self.extractContentItems(from: content.json.dictionaryValue["items"]!)
configureTransformer(pathPattern("search")) { (content: Entity<JSON>) -> SearchPage in
let nextPage = content.json.dictionaryValue["nextpage"]?.stringValue
return SearchPage(
results: self.extractContentItems(from: content.json.dictionaryValue["items"]!),
nextPage: nextPage,
last: nextPage == "null"
)
}
configureTransformer(pathPattern("suggestions")) { (content: Entity<JSON>) -> [String] in
@@ -123,10 +128,18 @@ final class PipedAPI: Service, ObservableObject, VideosAPI {
.withParam("region", country.rawValue)
}
func search(_ query: SearchQuery) -> Resource {
resource(baseURL: account.instance.apiURL, path: "search")
func search(_ query: SearchQuery, page: String?) -> Resource {
let path = page.isNil ? "search" : "nextpage/search"
let resource = resource(baseURL: account.instance.apiURL, path: path)
.withParam("q", query.query)
.withParam("filter", "")
.withParam("filter", "all")
if page.isNil {
return resource
}
return resource.withParam("nextpage", page)
}
func searchSuggestions(query: String) -> Resource {

View File

@@ -9,7 +9,7 @@ protocol VideosAPI {
func channel(_ id: String) -> Resource
func channelVideos(_ id: String) -> Resource
func trending(country: Country, category: TrendingCategory?) -> Resource
func search(_ query: SearchQuery) -> Resource
func search(_ query: SearchQuery, page: String?) -> Resource
func searchSuggestions(query: String) -> Resource
func video(_ id: Video.ID) -> Resource

View File

@@ -42,4 +42,8 @@ enum VideosApp: String, CaseIterable {
var supportsComments: Bool {
self == .piped
}
var searchUsesIndexedPages: Bool {
self == .invidious
}
}

View File

@@ -4,6 +4,7 @@ import SwiftUI
final class SearchModel: ObservableObject {
@Published var store = Store<[ContentItem]>()
@Published var page: SearchPage?
var accounts = AccountsModel()
@Published var query = SearchQuery()
@@ -13,7 +14,6 @@ final class SearchModel: ObservableObject {
@Published var fieldIsFocused = false
private var previousResource: Resource?
private var resource: Resource!
var isLoading: Bool {
@@ -23,60 +23,54 @@ final class SearchModel: ObservableObject {
func changeQuery(_ changeHandler: @escaping (SearchQuery) -> Void = { _ in }) {
changeHandler(query)
let newResource = accounts.api.search(query)
guard newResource != previousResource else {
let newResource = accounts.api.search(query, page: nil)
guard newResource != resource else {
return
}
previousResource?.removeObservers(ownedBy: store)
previousResource = newResource
page = nil
resource = newResource
resource.addObserver(store)
if !query.isEmpty {
loadResourceIfNeededAndReplaceStore()
loadResource()
}
}
func resetQuery(_ query: SearchQuery = SearchQuery()) {
self.query = query
let newResource = accounts.api.search(query)
guard newResource != previousResource else {
let newResource = accounts.api.search(query, page: nil)
guard newResource != resource else {
return
}
page = nil
store.replace([])
previousResource?.removeObservers(ownedBy: store)
previousResource = newResource
resource = newResource
resource.addObserver(store)
if !query.isEmpty {
loadResourceIfNeededAndReplaceStore()
loadResource()
}
}
func loadResourceIfNeededAndReplaceStore() {
func loadResource() {
let currentResource = resource!
if let request = resource.loadIfNeeded() {
request.onSuccess { response in
if let results: [ContentItem] = response.typedContent() {
self.replace(results, for: currentResource)
}
resource.load().onSuccess { response in
if let page: SearchPage = response.typedContent() {
self.page = page
self.replace(page.results, for: currentResource)
}
} else {
replace(store.collection, for: currentResource)
}
}
func replace(_ videos: [ContentItem], for resource: Resource) {
func replace(_ items: [ContentItem], for resource: Resource) {
if self.resource == resource {
store = Store<[ContentItem]>(videos)
store = Store<[ContentItem]>(items)
}
}
@@ -108,4 +102,38 @@ final class SearchModel: ObservableObject {
}
}
}
func loadNextPage() {
guard var pageToLoad = page, !pageToLoad.last else {
return
}
if pageToLoad.nextPage.isNil, accounts.app.searchUsesIndexedPages {
pageToLoad.nextPage = "2"
}
resource?.removeObservers(ownedBy: store)
resource = accounts.api.search(query, page: page?.nextPage)
resource.addObserver(store)
resource
.load()
.onSuccess { response in
if let page: SearchPage = response.typedContent() {
var nextPage: Int?
if self.accounts.app.searchUsesIndexedPages {
nextPage = Int(pageToLoad.nextPage ?? "0")
}
self.page = page
if self.accounts.app.searchUsesIndexedPages {
self.page?.nextPage = String((nextPage ?? 1) + 1)
}
self.replace(self.store.collection + page.results, for: self.resource)
}
}
}
}

View File

@@ -0,0 +1,7 @@
import Foundation
struct SearchPage {
var results = [ContentItem]()
var nextPage: String?
var last = false
}

View File

@@ -61,11 +61,8 @@ final class SearchQuery: ObservableObject {
@Published var date: SearchQuery.Date? = .month
@Published var duration: SearchQuery.Duration?
@Published var page = 1
init(query: String = "", page: Int = 1, sortBy: SearchQuery.SortOrder = .relevance, date: SearchQuery.Date? = nil, duration: SearchQuery.Duration? = nil) {
init(query: String = "", sortBy: SearchQuery.SortOrder = .relevance, date: SearchQuery.Date? = nil, duration: SearchQuery.Duration? = nil) {
self.query = query
self.page = page
self.sortBy = sortBy
self.date = date
self.duration = duration