Various enhancements
This commit is contained in:
parent
9d7f93303f
commit
22281aa7eb
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -11,6 +11,7 @@ struct NotificationsTab: View {
|
|||
NavigationStack(path: $routeurPath.path) {
|
||||
NotificationsListView()
|
||||
.withAppRouteur()
|
||||
.withSheetDestinations(sheetDestinations: $routeurPath.presentedSheet)
|
||||
}
|
||||
.environmentObject(routeurPath)
|
||||
}
|
||||
|
|
|
@ -10,6 +10,7 @@ struct TimelineTab: View {
|
|||
NavigationStack(path: $routeurPath.path) {
|
||||
TimelineView()
|
||||
.withAppRouteur()
|
||||
.withSheetDestinations(sheetDestinations: $routeurPath.presentedSheet)
|
||||
}
|
||||
.environmentObject(routeurPath)
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
|
|
|
@ -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()
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
|
@ -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) {}
|
||||
}
|
|
@ -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() {}
|
||||
|
||||
|
|
|
@ -66,6 +66,7 @@ public struct StatusMediaPreviewView: View {
|
|||
.frame(height: attachements.count > 2 ? 100 : 200)
|
||||
}
|
||||
.cornerRadius(4)
|
||||
.contentShape(Rectangle())
|
||||
.onTapGesture {
|
||||
selectedMediaSheetManager.selectedAttachement = attachement
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue