Various enhancements
This commit is contained in:
parent
9d7f93303f
commit
22281aa7eb
@ -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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -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)
|
||||||
}
|
}
|
||||||
|
@ -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)
|
||||||
}
|
}
|
||||||
|
@ -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)
|
||||||
|
@ -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
|
||||||
|
@ -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)
|
||||||
}
|
}
|
||||||
|
@ -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)
|
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() {}
|
||||||
|
|
||||||
|
@ -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
|
||||||
}
|
}
|
||||||
|
@ -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)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
Loading…
x
Reference in New Issue
Block a user