Folo/apps/mobile/native/ios/Views/ProfileView.swift

181 lines
5.3 KiB
Swift

//
// ProfileView.swift
//
// Created by Innei on 2025/3/24.
//
import ExpoModulesCore
import SwiftUI
struct ProfileView: View {
@Binding var profile: ProfileData
@Binding var user: UserData
let onPress: EventDispatcher
var hasAnyFeeds: Bool {
!profile.feeds.isEmpty || !profile.groupedFeeds.isEmpty
}
var body: some View {
List {
VStack {
VStack {
ProfileAvatar(image: user.image, name: user.name)
Text(user.name)
.font(.title)
.fontWeight(.bold)
.lineLimit(1)
if !user.handle.isEmpty {
Text("@\(user.handle)")
.foregroundColor(.gray)
}
}
.frame(maxWidth: .infinity)
.padding(.top, 30)
.padding(.bottom, 20)
.listRowInsets(EdgeInsets())
.listRowSeparator(.hidden)
}.listRowInsets(.none)
if !profile.lists.isEmpty {
Section(header: SectionHeaderText(text: "Lists")) {
ForEach($profile.lists, id: \.id) { $list in
Button {
onPress(["type": "list", "id": list.id])
} label: {
HStack {
ProfileListImage(
image: list.image, title: list.title,
customTitle: list.customTitle)
ProfileItemTitle(title: list.title, customTitle: list.customTitle)
}
}.foregroundStyle(.foreground)
}
}
}
if hasAnyFeeds {
Section(header: SectionHeaderText(text: "Feeds")) {
}
}
ForEach(profile.groupedFeeds.sorted(by: { $0.key < $1.key }), id: \.key) { key, value in
Section(
header:
Text(key).textCase(.none)
) {
ForEach(value, id: \.id) { feed in
Button {
onPress(["type": "feed", "id": feed.id])
} label: {
HStack {
FeedIconView(
feed: FeedIconFeedData.transform(feed), size: 24,
fallback: true,
siteUrl: feed.siteUrl
)
ProfileItemTitle(title: feed.title, customTitle: feed.customTitle)
}
}.foregroundStyle(.foreground)
}
}
}
if !profile.feeds.isEmpty {
Section(header: Text("Uncategorized Feeds").textCase(.none)) {
ForEach($profile.feeds, id: \.id) { $feed in
Button {
onPress(["type": "feed", "id": feed.id])
} label: {
HStack {
FeedIconView(
feed: FeedIconFeedData.transform(feed), size: 24,
fallback: true,
siteUrl: feed.siteUrl
)
ProfileItemTitle(title: feed.title, customTitle: feed.customTitle)
}
}.foregroundStyle(.foreground)
}
}
}
}.listStyle(.insetGrouped)
}
}
private struct ProfileListImage: View {
var image: String?
var title: String
var customTitle: String?
var body: some View {
if let image = image, !image.isEmpty {
Image(systemName: image)
.frame(width: 24, height: 24)
} else {
// Fallback icon with gradient background based on title
FallbackIcon(title: customTitle ?? title, size: 24)
}
}
}
private struct SectionHeaderText: View {
let text: String
var body: some View {
Text(text).font(.headline).foregroundStyle(.black).textCase(.none).padding(.top, 20)
}
}
private struct ProfileItemTitle: View {
let title: String
let customTitle: String?
var body: some View {
if let customTitle = customTitle, !customTitle.isEmpty {
Text(customTitle).lineLimit(1)
} else {
Text(title).lineLimit(1)
}
}
}
private struct ProfileAvatar: View {
let image: String?
let name: String
var body: some View {
AsyncImage(url: URL(string: image ?? "")) { phase in
switch phase {
case .empty:
ProgressView()
case .success(let image):
image
.resizable()
.scaledToFill()
case .failure:
FallbackIcon(title: name, size: 80)
@unknown default:
FallbackIcon(title: name, size: 80)
}
}
.scaledToFill()
.frame(width: 80, height: 80)
.clipShape(Circle())
}
}