Files
links/mobile/Heygo/Views/LinkListView.swift
T
2025-08-16 11:18:40 +10:00

369 lines
13 KiB
Swift

import SwiftUI
struct LinkListView: View {
@EnvironmentObject var linkViewModel: LinkViewModel
@Environment(\.managedObjectContext) private var viewContext
@State private var showingAddLink = false
@State private var showingFilterSheet = false
@State private var selectedLink: Link?
var body: some View {
NavigationView {
VStack {
// Search bar
SearchBar(text: $linkViewModel.searchText)
.padding(.horizontal)
// Filter and sort bar
HStack {
Button(action: {
showingFilterSheet = true
}) {
HStack {
Image(systemName: "line.horizontal.3.decrease.circle")
Text("filter")
if linkViewModel.selectedTag != nil {
Text("•")
.foregroundColor(.blue)
}
}
}
Spacer()
Menu {
ForEach(LinkViewModel.SortOption.allCases, id: \.self) { option in
Button(action: {
linkViewModel.selectedSortOption = option
}) {
HStack {
Text(option.displayName)
if linkViewModel.selectedSortOption == option {
Image(systemName: "checkmark")
}
}
}
}
} label: {
HStack {
Image(systemName: "arrow.up.arrow.down")
Text(linkViewModel.selectedSortOption.displayName)
}
}
}
.padding(.horizontal)
.padding(.vertical, 8)
// Links list
if linkViewModel.isLoading {
ProgressView()
.frame(maxWidth: .infinity, maxHeight: .infinity)
} else if linkViewModel.filteredLinks.isEmpty {
VStack {
Image(systemName: "link.badge.plus")
.font(.system(size: 60))
.foregroundColor(.secondary)
Text("no_links_found")
.font(.title2)
.foregroundColor(.secondary)
Text("tap_plus_to_add_link")
.font(.caption)
.foregroundColor(.secondary)
}
.frame(maxWidth: .infinity, maxHeight: .infinity)
} else {
List {
ForEach(linkViewModel.filteredLinks) { link in
LinkRowView(link: link)
.onTapGesture {
selectedLink = link
}
.swipeActions(edge: .trailing, allowsFullSwipe: false) {
Button("delete", role: .destructive) {
linkViewModel.deleteLink(link)
}
Button("edit") {
selectedLink = link
showingAddLink = true
}
.tint(.blue)
}
}
}
.refreshable {
linkViewModel.fetchLinks()
}
}
}
.navigationTitle("")
.navigationBarTitleDisplayMode(.inline)
.toolbar {
ToolbarItem(placement: .navigationBarLeading) {
HStack {
Image(systemName: "dog.fill")
.foregroundColor(.black)
Text("Heygo")
.font(.headline)
.foregroundColor(.primary)
}
}
ToolbarItem(placement: .navigationBarTrailing) {
Button(action: {
selectedLink = nil
showingAddLink = true
}) {
Image(systemName: "plus")
}
}
}
.sheet(isPresented: $showingAddLink) {
LinkFormView(link: selectedLink)
.environmentObject(linkViewModel)
}
.sheet(isPresented: $showingFilterSheet) {
FilterView()
.environmentObject(linkViewModel)
}
.sheet(item: $selectedLink) { link in
LinkDetailView(link: link)
.environmentObject(linkViewModel)
}
}
.onAppear {
if linkViewModel.links.isEmpty {
linkViewModel.fetchLinks()
}
}
.alert("Error", isPresented: .constant(linkViewModel.errorMessage != nil)) {
Button("OK") {
linkViewModel.errorMessage = nil
}
} message: {
Text(linkViewModel.errorMessage ?? "")
}
}
}
struct LinkRowView: View {
let link: Link
@EnvironmentObject var linkViewModel: LinkViewModel
var body: some View {
HStack {
VStack(alignment: .leading, spacing: 4) {
// Alias
HStack {
Text(link.alias)
.font(.headline)
.foregroundColor(.primary)
Spacer()
// Link type badge
Text(link.linkTypeEnum.displayName)
.font(.caption)
.padding(.horizontal, 8)
.padding(.vertical, 2)
.background(linkTypeColor(link.linkTypeEnum))
.foregroundColor(.white)
.cornerRadius(4)
}
// URL or description
if let url = link.originalURL, !url.isEmpty {
Text(url)
.font(.caption)
.foregroundColor(.secondary)
.lineLimit(1)
} else if let text = link.text, !text.isEmpty {
Text("custom_content")
.font(.caption)
.foregroundColor(.secondary)
.italic()
}
// Description
if let description = link.linkDescription, !description.isEmpty {
Text(description)
.font(.caption)
.foregroundColor(.secondary)
.lineLimit(2)
}
// Tags and stats
HStack {
// Tags
if !link.sortedTags.isEmpty {
ScrollView(.horizontal, showsIndicators: false) {
HStack(spacing: 4) {
ForEach(link.sortedTags, id: \.id) { tag in
Text(tag.name)
.font(.caption2)
.padding(.horizontal, 6)
.padding(.vertical, 2)
.background(Color.blue.opacity(0.2))
.foregroundColor(.blue)
.cornerRadius(4)
}
}
}
}
Spacer()
// Click count
HStack(spacing: 2) {
Image(systemName: "hand.tap")
Text("\(link.clickCount)")
}
.font(.caption2)
.foregroundColor(.secondary)
}
}
// Action button
Button(action: {
if link.isTemplate {
// Handle template parameters
// For now, just open without parameters
linkViewModel.openLink(link)
} else {
linkViewModel.openLink(link)
}
}) {
Image(systemName: link.isTemplate ? "arrow.right.circle.fill" : "arrow.up.right.circle.fill")
.foregroundColor(.blue)
.font(.title2)
}
}
.padding(.vertical, 4)
}
private func linkTypeColor(_ linkType: LinkType) -> Color {
switch linkType {
case .link:
return .blue
case .custom:
return .green
case .template:
return .orange
}
}
}
struct SearchBar: View {
@Binding var text: String
@State private var isEditing = false
var body: some View {
HStack {
TextField("search_or_enter_url", text: $text)
.padding(7)
.padding(.horizontal, 25)
.background(Color(.systemGray6))
.cornerRadius(8)
.overlay(
HStack {
Image(systemName: "magnifyingglass")
.foregroundColor(.gray)
.frame(minWidth: 0, maxWidth: .infinity, alignment: .leading)
.padding(.leading, 8)
if isEditing {
Button(action: {
self.text = ""
}) {
Image(systemName: "multiply.circle.fill")
.foregroundColor(.gray)
.padding(.trailing, 8)
}
}
}
)
.onTapGesture {
self.isEditing = true
}
if isEditing {
Button(action: {
self.isEditing = false
self.text = ""
UIApplication.shared.sendAction(#selector(UIResponder.resignFirstResponder), to: nil, from: nil, for: nil)
}) {
Text("cancel")
}
.padding(.trailing, 10)
.transition(.move(edge: .trailing))
.animation(.default, value: isEditing)
}
}
}
}
struct FilterView: View {
@EnvironmentObject var linkViewModel: LinkViewModel
@Environment(\.presentationMode) var presentationMode
@FetchRequest(
sortDescriptors: [NSSortDescriptor(keyPath: \Tag.name, ascending: true)],
animation: .default)
private var tags: FetchedResults<Tag>
var body: some View {
NavigationView {
List {
Section("filters") {
HStack {
Text("selected_tag")
Spacer()
if let selectedTag = linkViewModel.selectedTag {
Text(selectedTag.name)
.foregroundColor(.secondary)
} else {
Text("all_tags")
.foregroundColor(.secondary)
}
}
.onTapGesture {
linkViewModel.selectedTag = nil
}
}
Section("tags") {
ForEach(tags) { tag in
HStack {
Text(tag.name)
Spacer()
Text("\(tag.linkCount)")
.foregroundColor(.secondary)
if linkViewModel.selectedTag == tag {
Image(systemName: "checkmark")
.foregroundColor(.blue)
}
}
.onTapGesture {
linkViewModel.selectedTag = tag
presentationMode.wrappedValue.dismiss()
}
}
}
}
.navigationTitle("filter_links")
.navigationBarTitleDisplayMode(.inline)
.toolbar {
ToolbarItem(placement: .navigationBarTrailing) {
Button("done") {
presentationMode.wrappedValue.dismiss()
}
}
}
}
}
}
#Preview {
LinkListView()
.environmentObject(LinkViewModel())
.environment(\.managedObjectContext, PersistenceController.preview.container.viewContext)
}