Timelines with waterfall
This commit is contained in:
parent
085abcbea1
commit
d3aa3b7099
|
@ -9,6 +9,13 @@ import PixelfedKit
|
|||
|
||||
extension Client {
|
||||
public class PublicTimeline: BaseClient {
|
||||
public func getHomeTimeline(maxId: String? = nil,
|
||||
sinceId: String? = nil,
|
||||
minId: String? = nil,
|
||||
limit: Int = 40) async throws -> [Status] {
|
||||
return try await pixelfedClient.getHomeTimeline(maxId: maxId, sinceId: sinceId, minId: minId, limit: limit)
|
||||
}
|
||||
|
||||
public func getStatuses(local: Bool? = nil,
|
||||
remote: Bool? = nil,
|
||||
maxId: String? = nil,
|
||||
|
|
|
@ -21,11 +21,6 @@ struct HomeFeedView: View {
|
|||
@State private var opacity = 0.0
|
||||
@State private var offset = -50.0
|
||||
|
||||
// Gallery parameters.
|
||||
@State private var imageColumns = 3
|
||||
@State private var containerWidth: Double = UIScreen.main.bounds.width
|
||||
@State private var containerHeight: Double = UIScreen.main.bounds.height
|
||||
|
||||
@FetchRequest var dbStatuses: FetchedResults<StatusData>
|
||||
|
||||
init(accountId: String) {
|
||||
|
@ -60,17 +55,6 @@ struct HomeFeedView: View {
|
|||
private func timeline() -> some View {
|
||||
ZStack {
|
||||
ScrollView {
|
||||
if self.imageColumns > 1 {
|
||||
// WaterfallGrid(statusViewModel: $dbStatuses, columns: $imageColumns, hideLoadMore: $allItemsLoaded) { item in
|
||||
// ImageRowAsync(statusViewModel: item, containerWidth: $containerWidth)
|
||||
// } onLoadMore: {
|
||||
// do {
|
||||
// try await self.loadMoreStatuses()
|
||||
// } catch {
|
||||
// ErrorService.shared.handle(error, message: "statuses.error.loadingStatusesFailed", showToastr: !Task.isCancelled)
|
||||
// }
|
||||
// }
|
||||
} else {
|
||||
LazyVStack {
|
||||
ForEach(dbStatuses, id: \.self) { item in
|
||||
if self.shouldUpToDateBeVisible(statusId: item.id) {
|
||||
|
@ -97,12 +81,6 @@ struct HomeFeedView: View {
|
|||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
.gallery { galleryProperties in
|
||||
self.imageColumns = galleryProperties.imageColumns
|
||||
self.containerWidth = galleryProperties.containerWidth
|
||||
self.containerHeight = galleryProperties.containerHeight
|
||||
}
|
||||
|
||||
self.newPhotosView()
|
||||
.offset(y: self.offset)
|
||||
|
|
|
@ -116,8 +116,13 @@ struct MainView: View {
|
|||
private func getMainView() -> some View {
|
||||
switch self.viewMode {
|
||||
case .home:
|
||||
if UIDevice.isIPhone {
|
||||
HomeFeedView(accountId: applicationState.account?.id ?? String.empty())
|
||||
.id(applicationState.account?.id ?? String.empty())
|
||||
} else {
|
||||
StatusesView(listType: .home)
|
||||
.id(applicationState.account?.id ?? String.empty())
|
||||
}
|
||||
case .trendingPhotos:
|
||||
TrendStatusesView(accountId: applicationState.account?.id ?? String.empty())
|
||||
.id(applicationState.account?.id ?? String.empty())
|
||||
|
|
|
@ -38,6 +38,11 @@ struct PaginableStatusesView: View {
|
|||
@State private var state: ViewState = .loading
|
||||
@State private var page = 1
|
||||
|
||||
// Gallery parameters.
|
||||
@State private var imageColumns = 3
|
||||
@State private var containerWidth: Double = UIScreen.main.bounds.width
|
||||
@State private var containerHeight: Double = UIScreen.main.bounds.height
|
||||
|
||||
private let defaultLimit = 10
|
||||
private let imagePrefetcher = ImagePrefetcher(destination: .diskCache)
|
||||
|
||||
|
@ -75,9 +80,20 @@ struct PaginableStatusesView: View {
|
|||
@ViewBuilder
|
||||
private func list() -> some View {
|
||||
ScrollView {
|
||||
if self.imageColumns > 1 {
|
||||
WaterfallGrid($statusViewModels, columns: $imageColumns, hideLoadMore: $allItemsLoaded) { item in
|
||||
ImageRowAsync(statusViewModel: item, containerWidth: $containerWidth)
|
||||
} onLoadMore: {
|
||||
do {
|
||||
try await self.loadMoreStatuses()
|
||||
} catch {
|
||||
ErrorService.shared.handle(error, message: "statuses.error.loadingStatusesFailed", showToastr: !Task.isCancelled)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
LazyVStack(alignment: .center) {
|
||||
ForEach(self.statusViewModels, id: \.id) { item in
|
||||
ImageRowAsync(statusViewModel: item, containerWidth: Binding.constant(UIScreen.main.bounds.width))
|
||||
ImageRowAsync(statusViewModel: item, containerWidth: $containerWidth)
|
||||
}
|
||||
|
||||
if allItemsLoaded == false {
|
||||
|
@ -97,6 +113,12 @@ struct PaginableStatusesView: View {
|
|||
}
|
||||
}
|
||||
}
|
||||
.gallery { galleryProperties in
|
||||
self.imageColumns = galleryProperties.imageColumns
|
||||
self.containerWidth = galleryProperties.containerWidth
|
||||
self.containerHeight = galleryProperties.containerHeight
|
||||
}
|
||||
}
|
||||
|
||||
private func loadData() async {
|
||||
do {
|
||||
|
|
|
@ -14,6 +14,7 @@ import WidgetsKit
|
|||
|
||||
struct StatusesView: View {
|
||||
public enum ListType: Hashable {
|
||||
case home
|
||||
case local
|
||||
case federated
|
||||
case favourites
|
||||
|
@ -22,6 +23,8 @@ struct StatusesView: View {
|
|||
|
||||
public var title: LocalizedStringKey {
|
||||
switch self {
|
||||
case .home:
|
||||
return "mainview.tab.homeTimeline"
|
||||
case .local:
|
||||
return "statuses.navigationBar.localTimeline"
|
||||
case .federated:
|
||||
|
@ -94,10 +97,11 @@ struct StatusesView: View {
|
|||
private func list() -> some View {
|
||||
ScrollView {
|
||||
if self.imageColumns > 1 {
|
||||
WaterfallGrid(statusViewModel: $statusViewModels, columns: $imageColumns, hideLoadMore: $allItemsLoaded) { item in
|
||||
WaterfallGrid($statusViewModels, columns: $imageColumns, hideLoadMore: $allItemsLoaded) { item in
|
||||
ImageRowAsync(statusViewModel: item, containerWidth: $containerWidth)
|
||||
} onLoadMore: {
|
||||
do {
|
||||
print("load more......")
|
||||
try await self.loadMoreStatuses()
|
||||
} catch {
|
||||
ErrorService.shared.handle(error, message: "statuses.error.loadingStatusesFailed", showToastr: !Task.isCancelled)
|
||||
|
@ -232,6 +236,12 @@ struct StatusesView: View {
|
|||
|
||||
private func loadFromApi(maxId: String? = nil, sinceId: String? = nil, minId: String? = nil) async throws -> [Status] {
|
||||
switch self.listType {
|
||||
case .home:
|
||||
return try await self.client.publicTimeline?.getHomeTimeline(
|
||||
maxId: maxId,
|
||||
sinceId: sinceId,
|
||||
minId: minId,
|
||||
limit: self.defaultLimit) ?? []
|
||||
case .local:
|
||||
return try await self.client.publicTimeline?.getStatuses(
|
||||
local: true,
|
||||
|
|
|
@ -21,6 +21,11 @@ struct TrendStatusesView: View {
|
|||
@State private var statusViewModels: [StatusModel] = []
|
||||
@State private var state: ViewState = .loading
|
||||
|
||||
// Gallery parameters.
|
||||
@State private var imageColumns = 3
|
||||
@State private var containerWidth: Double = UIScreen.main.bounds.width
|
||||
@State private var containerHeight: Double = UIScreen.main.bounds.height
|
||||
|
||||
var body: some View {
|
||||
ScrollView {
|
||||
Picker(selection: $tabSelectedValue, label: Text("")) {
|
||||
|
@ -45,6 +50,20 @@ struct TrendStatusesView: View {
|
|||
|
||||
self.mainBody()
|
||||
}
|
||||
.gallery { galleryProperties in
|
||||
self.imageColumns = galleryProperties.imageColumns
|
||||
self.containerWidth = galleryProperties.containerWidth
|
||||
self.containerHeight = galleryProperties.containerHeight
|
||||
}
|
||||
.refreshable {
|
||||
do {
|
||||
HapticService.shared.fireHaptic(of: .dataRefresh(intensity: 0.3))
|
||||
try await self.loadStatuses()
|
||||
HapticService.shared.fireHaptic(of: .dataRefresh(intensity: 0.7))
|
||||
} catch {
|
||||
ErrorService.shared.handle(error, message: "trendingStatuses.error.loadingStatusesFailed", showToastr: !Task.isCancelled)
|
||||
}
|
||||
}
|
||||
.navigationTitle("trendingStatuses.navigationBar.title")
|
||||
}
|
||||
|
||||
|
@ -69,20 +88,17 @@ struct TrendStatusesView: View {
|
|||
case .loaded:
|
||||
if self.statusViewModels.isEmpty {
|
||||
NoDataView(imageSystemName: "photo.on.rectangle.angled", text: "trendingStatuses.title.noPhotos")
|
||||
} else {
|
||||
if self.imageColumns > 1 {
|
||||
WaterfallGrid($statusViewModels, columns: $imageColumns, hideLoadMore: Binding.constant(true)) { item in
|
||||
ImageRowAsync(statusViewModel: item, containerWidth: $containerWidth)
|
||||
} onLoadMore: { }
|
||||
} else {
|
||||
LazyVStack(alignment: .center) {
|
||||
ForEach(self.statusViewModels, id: \.id) { item in
|
||||
ImageRowAsync(statusViewModel: item, containerWidth: Binding.constant(UIScreen.main.bounds.width))
|
||||
}
|
||||
}
|
||||
.refreshable {
|
||||
do {
|
||||
HapticService.shared.fireHaptic(of: .dataRefresh(intensity: 0.3))
|
||||
try await self.loadStatuses()
|
||||
HapticService.shared.fireHaptic(of: .dataRefresh(intensity: 0.7))
|
||||
} catch {
|
||||
ErrorService.shared.handle(error, message: "trendingStatuses.error.loadingStatusesFailed", showToastr: !Task.isCancelled)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -42,7 +42,7 @@ struct UserProfileStatusesView: View {
|
|||
var body: some View {
|
||||
if firstLoadFinished == true {
|
||||
if self.imageColumns > 1 {
|
||||
WaterfallGrid(statusViewModel: $statusViewModels, columns: $imageColumns, hideLoadMore: $allItemsLoaded) { item in
|
||||
WaterfallGrid($statusViewModels, columns: $imageColumns, hideLoadMore: $allItemsLoaded) { item in
|
||||
ImageRowAsync(statusViewModel: item, withAvatar: false, containerWidth: $containerWidth)
|
||||
} onLoadMore: {
|
||||
do {
|
||||
|
|
|
@ -6,33 +6,23 @@
|
|||
|
||||
import SwiftUI
|
||||
import WidgetsKit
|
||||
import ClientKit
|
||||
|
||||
struct WaterfallGrid<Content>: View where Content: View {
|
||||
@Binding private var statusViewModels: [StatusModel]
|
||||
struct WaterfallGrid<Data, ID, Content>: View where Data: RandomAccessCollection, Data: Equatable, Content: View,
|
||||
ID: Hashable, Data.Element: Equatable, Data.Element: Identifiable, Data.Element: Hashable {
|
||||
@Binding private var columns: Int
|
||||
@Binding private var hideLoadMore: Bool
|
||||
|
||||
@State private var data: [[StatusModel]] = []
|
||||
@Binding private var data: Data
|
||||
private let dataId: KeyPath<Data.Element, ID>
|
||||
private let content: (Data.Element) -> Content
|
||||
|
||||
@State private var columnsData: [[Data.Element]] = []
|
||||
|
||||
private let onLoadMore: () async -> Void
|
||||
private let content: (StatusModel) -> Content
|
||||
|
||||
init(statusViewModel: Binding<[StatusModel]>,
|
||||
columns: Binding<Int>,
|
||||
hideLoadMore: Binding<Bool>,
|
||||
content: @escaping (StatusModel) -> Content,
|
||||
onLoadMore: @escaping () async -> Void) {
|
||||
self._statusViewModels = statusViewModel
|
||||
self._columns = columns
|
||||
self._hideLoadMore = hideLoadMore
|
||||
self.content = content
|
||||
self.onLoadMore = onLoadMore
|
||||
}
|
||||
|
||||
var body: some View {
|
||||
HStack(alignment: .top, spacing: 20) {
|
||||
ForEach(self.data, id: \.self) { array in
|
||||
ForEach(self.columnsData, id: \.self) { array in
|
||||
LazyVStack(spacing: 8) {
|
||||
ForEach(array, id: \.id) { item in
|
||||
self.content(item)
|
||||
|
@ -50,7 +40,7 @@ struct WaterfallGrid<Content>: View where Content: View {
|
|||
.onFirstAppear {
|
||||
self.recalculateArrays()
|
||||
}
|
||||
.onChange(of: self.statusViewModels) { _ in
|
||||
.onChange(of: self.data) { _ in
|
||||
self.recalculateArrays()
|
||||
}
|
||||
.onChange(of: self.columns) { _ in
|
||||
|
@ -59,25 +49,52 @@ struct WaterfallGrid<Content>: View where Content: View {
|
|||
}
|
||||
|
||||
private func recalculateArrays() {
|
||||
var internalArray: [[StatusModel]] = []
|
||||
var internalArray: [[Data.Element]] = []
|
||||
|
||||
for _ in 0 ..< self.columns {
|
||||
internalArray.append([])
|
||||
}
|
||||
|
||||
for (index, item) in self.statusViewModels.enumerated() {
|
||||
for (index, item) in self.data.enumerated() {
|
||||
let arrayIndex = index % self.columns
|
||||
internalArray[arrayIndex].append(item)
|
||||
}
|
||||
|
||||
self.data = internalArray
|
||||
self.columnsData = internalArray
|
||||
}
|
||||
|
||||
private func shouldShowSpinner(array: [StatusModel]) -> Bool {
|
||||
private func shouldShowSpinner(array: [Data.Element]) -> Bool {
|
||||
if self.hideLoadMore {
|
||||
return false
|
||||
}
|
||||
|
||||
return self.data[1].first == array.first
|
||||
return self.columnsData[1].first == array.first
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
extension WaterfallGrid {
|
||||
init(_ data: Binding<Data>, id: KeyPath<Data.Element, ID>, columns: Binding<Int>,
|
||||
hideLoadMore: Binding<Bool>, content: @escaping (Data.Element) -> Content, onLoadMore: @escaping () async -> Void) {
|
||||
self._data = data
|
||||
self.dataId = id
|
||||
self.content = content
|
||||
|
||||
self._columns = columns
|
||||
self._hideLoadMore = hideLoadMore
|
||||
self.onLoadMore = onLoadMore
|
||||
}
|
||||
}
|
||||
|
||||
extension WaterfallGrid where ID == Data.Element.ID, Data.Element: Identifiable {
|
||||
init(_ data: Binding<Data>, columns: Binding<Int>,
|
||||
hideLoadMore: Binding<Bool>, content: @escaping (Data.Element) -> Content, onLoadMore: @escaping () async -> Void) {
|
||||
self._data = data
|
||||
self.dataId = \Data.Element.id
|
||||
self.content = content
|
||||
|
||||
self._columns = columns
|
||||
self._hideLoadMore = hideLoadMore
|
||||
self.onLoadMore = onLoadMore
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,18 @@
|
|||
//
|
||||
// https://mczachurski.dev
|
||||
// Copyright © 2023 Marcin Czachurski and the repository contributors.
|
||||
// Licensed under the Apache License 2.0.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
import UIKit
|
||||
|
||||
public extension UIDevice {
|
||||
static var isIPad: Bool {
|
||||
UIDevice.current.userInterfaceIdiom == .pad
|
||||
}
|
||||
|
||||
static var isIPhone: Bool {
|
||||
UIDevice.current.userInterfaceIdiom == .phone
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue