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 Routeur
import Status
import DesignSystem
extension 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) {
NotificationsListView()
.withAppRouteur()
.withSheetDestinations(sheetDestinations: $routeurPath.presentedSheet)
}
.environmentObject(routeurPath)
}

View file

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

View file

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

View file

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

View file

@ -15,7 +15,9 @@ class AccountDetailViewModel: ObservableObject, StatusesFetcher {
@Published var state: State = .loading
@Published var statusesState: StatusesState = .loading
@Published var title: String = ""
private var account: Account?
private var statuses: [Status] = []
init(accountId: String) {
@ -30,7 +32,9 @@ class AccountDetailViewModel: ObservableObject, StatusesFetcher {
func fetchAccount() async {
guard let client else { return }
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 {
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)
}
public enum SheetDestinations: Identifiable {
public var id: String {
switch self {
case .imageDetail:
return "imageDetail"
}
}
case imageDetail(url: URL)
}
public class RouterPath: ObservableObject {
@Published public var path: [RouteurDestinations] = []
@Published public var presentedSheet: SheetDestinations?
public init() {}

View file

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

View file

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