mirror of
https://github.com/wahyd4/links.git
synced 2026-08-09 05:06:16 +10:00
369 lines
13 KiB
Swift
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)
|
|
}
|