Various enhancements

This commit is contained in:
Thomas Ricouard 2022-12-20 09:37:07 +01:00
parent 9d7f93303f
commit 22281aa7eb
11 changed files with 164 additions and 49 deletions

View File

@ -3,6 +3,7 @@ import Timeline
import Account import Account
import Routeur import Routeur
import Status import Status
import DesignSystem
extension View { extension View {
func withAppRouteur() -> some View { func withAppRouteur() -> some View {
@ -17,4 +18,13 @@ extension View {
} }
} }
} }
func withSheetDestinations(sheetDestinations: Binding<SheetDestinations?>) -> some View {
self.sheet(item: sheetDestinations) { destination in
switch destination {
case let .imageDetail(url):
ImageSheetView(url: url)
}
}
}
} }

View File

@ -11,6 +11,7 @@ struct NotificationsTab: View {
NavigationStack(path: $routeurPath.path) { NavigationStack(path: $routeurPath.path) {
NotificationsListView() NotificationsListView()
.withAppRouteur() .withAppRouteur()
.withSheetDestinations(sheetDestinations: $routeurPath.presentedSheet)
} }
.environmentObject(routeurPath) .environmentObject(routeurPath)
} }

View File

@ -10,6 +10,7 @@ struct TimelineTab: View {
NavigationStack(path: $routeurPath.path) { NavigationStack(path: $routeurPath.path) {
TimelineView() TimelineView()
.withAppRouteur() .withAppRouteur()
.withSheetDestinations(sheetDestinations: $routeurPath.presentedSheet)
} }
.environmentObject(routeurPath) .environmentObject(routeurPath)
} }

View File

@ -1,11 +1,14 @@
import SwiftUI import SwiftUI
import Models import Models
import DesignSystem import DesignSystem
import Routeur
struct AccountDetailHeaderView: View { struct AccountDetailHeaderView: View {
@EnvironmentObject private var routeurPath: RouterPath
@Environment(\.redactionReasons) private var reasons @Environment(\.redactionReasons) private var reasons
let account: Account
let account: Account
var body: some View { var body: some View {
VStack(alignment: .leading) { VStack(alignment: .leading) {
headerImageView headerImageView
@ -14,21 +17,28 @@ struct AccountDetailHeaderView: View {
} }
private var headerImageView: some View { private var headerImageView: some View {
AsyncImage( GeometryReader { proxy in
url: account.header, AsyncImage(
content: { image in url: account.header,
image.resizable() content: { image in
.aspectRatio(contentMode: .fill) image.resizable()
.frame(maxHeight: 200) .aspectRatio(contentMode: .fill)
.clipped() .frame(height: 200)
}, .frame(width: proxy.frame(in: .local).width)
placeholder: { .clipped()
Color.gray },
.frame(maxHeight: 20) placeholder: {
} Color.gray
) .frame(height: 200)
.frame(maxHeight: 200) }
.background(Color.gray) )
.background(Color.gray)
}
.frame(height: 200)
.contentShape(Rectangle())
.onTapGesture {
routeurPath.presentedSheet = .imageDetail(url: account.header)
}
} }
private var accountAvatarView: some View { private var accountAvatarView: some View {
@ -50,6 +60,10 @@ struct AccountDetailHeaderView: View {
.frame(maxWidth: 80, maxHeight: 80) .frame(maxWidth: 80, maxHeight: 80)
} }
) )
.contentShape(Rectangle())
.onTapGesture {
routeurPath.presentedSheet = .imageDetail(url: account.avatar)
}
Spacer() Spacer()
Group { Group {
makeCustomInfoLabel(title: "Posts", count: account.statusesCount) makeCustomInfoLabel(title: "Posts", count: account.statusesCount)

View File

@ -8,6 +8,7 @@ import DesignSystem
public struct AccountDetailView: View { public struct AccountDetailView: View {
@EnvironmentObject private var client: Client @EnvironmentObject private var client: Client
@StateObject private var viewModel: AccountDetailViewModel @StateObject private var viewModel: AccountDetailViewModel
@State private var scrollOffset: CGFloat = 0
public init(accountId: String) { public init(accountId: String) {
_viewModel = StateObject(wrappedValue: .init(accountId: accountId)) _viewModel = StateObject(wrappedValue: .init(accountId: accountId))
@ -18,18 +19,23 @@ public struct AccountDetailView: View {
} }
public var body: some View { public var body: some View {
ScrollView { ScrollViewOffsetReader { offset in
self.scrollOffset = offset
} content: {
LazyVStack { LazyVStack {
headerView headerView
Divider()
.offset(y: -20)
StatusesListView(fetcher: viewModel) StatusesListView(fetcher: viewModel)
} }
} }
.edgesIgnoringSafeArea(.top)
.task { .task {
viewModel.client = client viewModel.client = client
await viewModel.fetchAccount() await viewModel.fetchAccount()
await viewModel.fetchStatuses() await viewModel.fetchStatuses()
} }
.edgesIgnoringSafeArea(.top)
.navigationTitle(Text(scrollOffset < -20 ? viewModel.title : ""))
} }
@ViewBuilder @ViewBuilder

View File

@ -15,7 +15,9 @@ class AccountDetailViewModel: ObservableObject, StatusesFetcher {
@Published var state: State = .loading @Published var state: State = .loading
@Published var statusesState: StatusesState = .loading @Published var statusesState: StatusesState = .loading
@Published var title: String = ""
private var account: Account?
private var statuses: [Status] = [] private var statuses: [Status] = []
init(accountId: String) { init(accountId: String) {
@ -30,7 +32,9 @@ class AccountDetailViewModel: ObservableObject, StatusesFetcher {
func fetchAccount() async { func fetchAccount() async {
guard let client else { return } guard let client else { return }
do { do {
state = .data(account: try await client.get(endpoint: Accounts.accounts(id: accountId))) let account: Account = try await client.get(endpoint: Accounts.accounts(id: accountId))
self.title = account.displayName
state = .data(account: account)
} catch { } catch {
state = .error(error: error) state = .error(error: error)
} }

View File

@ -0,0 +1,22 @@
import SwiftUI
public struct ImageSheetView: View {
let url: URL
public init(url: URL) {
self.url = url
}
public var body: some View {
AsyncImage(
url: url,
content: { image in
image.resizable()
.aspectRatio(contentMode: .fit)
},
placeholder: {
ProgressView()
}
)
}
}

View File

@ -0,0 +1,44 @@
/*! @copyright 2021 Medium */
import SwiftUI
// Source: https://www.fivestars.blog/articles/scrollview-offset/
public struct ScrollViewOffsetReader<Content: View>: View {
let onOffsetChange: (CGFloat) -> Void
let content: () -> Content
public init(
onOffsetChange: @escaping (CGFloat) -> Void,
@ViewBuilder content: @escaping () -> Content
) {
self.onOffsetChange = onOffsetChange
self.content = content
}
public var body: some View {
ScrollView {
offsetReader
content()
.padding(.top, -8)
}
.coordinateSpace(name: "frameLayer")
.onPreferenceChange(OffsetPreferenceKey.self, perform: onOffsetChange)
}
var offsetReader: some View {
GeometryReader { proxy in
Color.clear
.preference(
key: OffsetPreferenceKey.self,
value: proxy.frame(in: .named("frameLayer")).minY
)
}
.frame(height: 0)
}
}
private struct OffsetPreferenceKey: PreferenceKey {
static var defaultValue: CGFloat = .zero
static func reduce(value: inout CGFloat, nextValue: () -> CGFloat) {}
}

View File

@ -8,8 +8,20 @@ public enum RouteurDestinations: Hashable {
case statusDetail(id: String) case statusDetail(id: String)
} }
public enum SheetDestinations: Identifiable {
public var id: String {
switch self {
case .imageDetail:
return "imageDetail"
}
}
case imageDetail(url: URL)
}
public class RouterPath: ObservableObject { public class RouterPath: ObservableObject {
@Published public var path: [RouteurDestinations] = [] @Published public var path: [RouteurDestinations] = []
@Published public var presentedSheet: SheetDestinations?
public init() {} public init() {}

View File

@ -66,6 +66,7 @@ public struct StatusMediaPreviewView: View {
.frame(height: attachements.count > 2 ? 100 : 200) .frame(height: attachements.count > 2 ? 100 : 200)
} }
.cornerRadius(4) .cornerRadius(4)
.contentShape(Rectangle())
.onTapGesture { .onTapGesture {
selectedMediaSheetManager.selectedAttachement = attachement selectedMediaSheetManager.selectedAttachement = attachement
} }

View File

@ -39,31 +39,32 @@ public struct StatusRowView: View {
} }
} }
@ViewBuilder
private var statusView: some View { private var statusView: some View {
if let status: AnyStatus = status.reblog ?? status { VStack(alignment: .leading, spacing: 8) {
if !isEmbed { if let status: AnyStatus = status.reblog ?? status {
Button { if !isEmbed {
routeurPath.navigate(to: .accountDetailWithAccount(account: status.account)) Button {
} label: { routeurPath.navigate(to: .accountDetailWithAccount(account: status.account))
makeAccountView(status: status) } label: {
}.buttonStyle(.plain) makeAccountView(status: status)
} }.buttonStyle(.plain)
Text(status.content.asSafeAttributedString)
.font(.body)
.onTapGesture {
routeurPath.navigate(to: .statusDetail(id: status.id))
} }
.environment(\.openURL, OpenURLAction { url in
routeurPath.handleStatus(status: status, url: url) Text(status.content.asSafeAttributedString)
}) .font(.body)
.onTapGesture {
if !status.mediaAttachments.isEmpty { routeurPath.navigate(to: .statusDetail(id: status.id))
StatusMediaPreviewView(attachements: status.mediaAttachments) }
.padding(.vertical, 4) .environment(\.openURL, OpenURLAction { url in
routeurPath.handleStatus(status: status, url: url)
})
if !status.mediaAttachments.isEmpty {
StatusMediaPreviewView(attachements: status.mediaAttachments)
.padding(.vertical, 4)
}
StatusCardView(status: status)
} }
StatusCardView(status: status)
} }
} }
@ -72,16 +73,15 @@ public struct StatusRowView: View {
AvatarView(url: status.account.avatar) AvatarView(url: status.account.avatar)
VStack(alignment: .leading) { VStack(alignment: .leading) {
Text(status.account.displayName) Text(status.account.displayName)
.font(.headline) .font(.subheadline)
HStack { .fontWeight(.semibold)
Text("@\(status.account.acct)") Group {
.font(.footnote) Text("@\(status.account.acct)") +
.foregroundColor(.gray) Text("") +
Spacer()
Text(status.createdAt.formatted) Text(status.createdAt.formatted)
.font(.footnote)
.foregroundColor(.gray)
} }
.font(.footnote)
.foregroundColor(.gray)
} }
} }
} }