diff --git a/Ferrite.xcodeproj/project.pbxproj b/Ferrite.xcodeproj/project.pbxproj index 47c3300..414aeda 100644 --- a/Ferrite.xcodeproj/project.pbxproj +++ b/Ferrite.xcodeproj/project.pbxproj @@ -71,9 +71,14 @@ 0CA148EC288903F000DE2211 /* ContentView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0CA148D4288903F000DE2211 /* ContentView.swift */; }; 0CA3FB2028B91D9500FA10A8 /* IndeterminateProgressView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0CA3FB1F28B91D9500FA10A8 /* IndeterminateProgressView.swift */; }; 0CAF1C7B286F5C8600296F86 /* SwiftSoup in Frameworks */ = {isa = PBXBuildFile; productRef = 0CAF1C7A286F5C8600296F86 /* SwiftSoup */; }; + 0CB6516328C5A57300DCA721 /* ConditionalId.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0CB6516228C5A57300DCA721 /* ConditionalId.swift */; }; + 0CB6516528C5A5D700DCA721 /* InlinedList.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0CB6516428C5A5D700DCA721 /* InlinedList.swift */; }; + 0CB6516828C5A5EC00DCA721 /* Introspect in Frameworks */ = {isa = PBXBuildFile; productRef = 0CB6516728C5A5EC00DCA721 /* Introspect */; }; + 0CB6516A28C5B4A600DCA721 /* InlineHeader.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0CB6516928C5B4A600DCA721 /* InlineHeader.swift */; }; 0CBC76FD288D914F0054BE44 /* BatchChoiceView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0CBC76FC288D914F0054BE44 /* BatchChoiceView.swift */; }; 0CBC76FF288DAAD00054BE44 /* NavigationViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0CBC76FE288DAAD00054BE44 /* NavigationViewModel.swift */; }; 0CBC7705288DE7F40054BE44 /* PersistenceController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0CBC7704288DE7F40054BE44 /* PersistenceController.swift */; }; + 0CDCB91828C662640098B513 /* EmptyInstructionView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0CDCB91728C662640098B513 /* EmptyInstructionView.swift */; }; 0CFEFCFD288A006200B3F490 /* GroupBoxStyle.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0CFEFCFC288A006200B3F490 /* GroupBoxStyle.swift */; }; /* End PBXBuildFile section */ @@ -137,10 +142,14 @@ 0CA148D4288903F000DE2211 /* ContentView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ContentView.swift; sourceTree = ""; }; 0CA3FB1F28B91D9500FA10A8 /* IndeterminateProgressView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = IndeterminateProgressView.swift; sourceTree = ""; }; 0CAF1C68286F5C0E00296F86 /* Ferrite.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Ferrite.app; sourceTree = BUILT_PRODUCTS_DIR; }; + 0CB6516228C5A57300DCA721 /* ConditionalId.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ConditionalId.swift; sourceTree = ""; }; + 0CB6516428C5A5D700DCA721 /* InlinedList.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InlinedList.swift; sourceTree = ""; }; + 0CB6516928C5B4A600DCA721 /* InlineHeader.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InlineHeader.swift; sourceTree = ""; }; 0CBC76FC288D914F0054BE44 /* BatchChoiceView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BatchChoiceView.swift; sourceTree = ""; }; 0CBC76FE288DAAD00054BE44 /* NavigationViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NavigationViewModel.swift; sourceTree = ""; }; 0CBC7704288DE7F40054BE44 /* PersistenceController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PersistenceController.swift; sourceTree = ""; }; 0CC6E4D428A45BA000AF2BCC /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist; path = Info.plist; sourceTree = ""; }; + 0CDCB91728C662640098B513 /* EmptyInstructionView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EmptyInstructionView.swift; sourceTree = ""; }; 0CFEFCFC288A006200B3F490 /* GroupBoxStyle.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GroupBoxStyle.swift; sourceTree = ""; }; /* End PBXFileReference section */ @@ -153,6 +162,7 @@ 0C64A4B4288903680079976D /* Base32 in Frameworks */, 0C4CFC462897030D00AD9FAD /* Regex in Frameworks */, 0C7376F028A97D1400D60918 /* SwiftUIX in Frameworks */, + 0CB6516828C5A5EC00DCA721 /* Introspect in Frameworks */, 0C64A4B7288903880079976D /* KeychainSwift in Frameworks */, 0CAF1C7B286F5C8600296F86 /* SwiftSoup in Frameworks */, ); @@ -241,6 +251,10 @@ 0C32FB562890D1F2002BD219 /* ListRowViews.swift */, 0C7D11FB28AA01E900ED92DB /* DynamicAccentColor.swift */, 0CA3FB1F28B91D9500FA10A8 /* IndeterminateProgressView.swift */, + 0CB6516228C5A57300DCA721 /* ConditionalId.swift */, + 0CB6516428C5A5D700DCA721 /* InlinedList.swift */, + 0CB6516928C5B4A600DCA721 /* InlineHeader.swift */, + 0CDCB91728C662640098B513 /* EmptyInstructionView.swift */, ); path = CommonViews; sourceTree = ""; @@ -366,6 +380,7 @@ 0C4CFC452897030D00AD9FAD /* Regex */, 0C7376EF28A97D1400D60918 /* SwiftUIX */, 0C7506D628B1AC9A008BEE38 /* SwiftyJSON */, + 0CB6516728C5A5EC00DCA721 /* Introspect */, ); productName = Torrenter; productReference = 0CAF1C68286F5C0E00296F86 /* Ferrite.app */; @@ -402,6 +417,7 @@ 0C4CFC442897030D00AD9FAD /* XCRemoteSwiftPackageReference "Regex" */, 0C7376EE28A97D1400D60918 /* XCRemoteSwiftPackageReference "SwiftUIX" */, 0C7506D528B1AC9A008BEE38 /* XCRemoteSwiftPackageReference "SwiftyJSON" */, + 0CB6516628C5A5EC00DCA721 /* XCRemoteSwiftPackageReference "SwiftUI-Introspect" */, ); productRefGroup = 0CAF1C69286F5C0E00296F86 /* Products */; projectDirPath = ""; @@ -431,8 +447,10 @@ buildActionMask = 2147483647; files = ( 0C0D50E5288DFE7F0035ECC8 /* SourceModels.swift in Sources */, + 0CB6516528C5A5D700DCA721 /* InlinedList.swift in Sources */, 0C60B1EF28A1A00000E3FD7E /* SearchProgressView.swift in Sources */, 0C32FB532890D19D002BD219 /* AboutView.swift in Sources */, + 0CB6516328C5A57300DCA721 /* ConditionalId.swift in Sources */, 0C84F4832895BFED0074B7C9 /* Source+CoreDataProperties.swift in Sources */, 0CA148DB288903F000DE2211 /* NavView.swift in Sources */, 0C750745289B003E004B3906 /* SourceRssParser+CoreDataProperties.swift in Sources */, @@ -454,6 +472,7 @@ 0C794B69289DACC800DD1CC8 /* InstalledSourceView.swift in Sources */, 0C79DC082899AF3C003F1C5A /* SourceSeedLeech+CoreDataProperties.swift in Sources */, 0CA148DD288903F000DE2211 /* ScrapingViewModel.swift in Sources */, + 0CB6516A28C5B4A600DCA721 /* InlineHeader.swift in Sources */, 0CA148D8288903F000DE2211 /* MagnetChoiceView.swift in Sources */, 0C84F4862895BFED0074B7C9 /* SourceList+CoreDataClass.swift in Sources */, 0C68135028BC1A2D00FAD890 /* GithubWrapper.swift in Sources */, @@ -466,6 +485,7 @@ 0C794B67289DACB600DD1CC8 /* SourceUpdateButtonView.swift in Sources */, 0CA148E6288903F000DE2211 /* WebView.swift in Sources */, 0C4CFC4E28970C8B00AD9FAD /* SourceComplexQuery+CoreDataProperties.swift in Sources */, + 0CDCB91828C662640098B513 /* EmptyInstructionView.swift in Sources */, 0CA148E2288903F000DE2211 /* Data.swift in Sources */, 0C57D4CC289032ED008534E8 /* SearchResultRDView.swift in Sources */, 0C7D11FC28AA01E900ED92DB /* DynamicAccentColor.swift in Sources */, @@ -747,6 +767,14 @@ minimumVersion = 2.0.0; }; }; + 0CB6516628C5A5EC00DCA721 /* XCRemoteSwiftPackageReference "SwiftUI-Introspect" */ = { + isa = XCRemoteSwiftPackageReference; + repositoryURL = "https://github.com/siteline/SwiftUI-Introspect"; + requirement = { + branch = master; + kind = branch; + }; + }; /* End XCRemoteSwiftPackageReference section */ /* Begin XCSwiftPackageProductDependency section */ @@ -780,6 +808,11 @@ package = 0CAF1C79286F5C8600296F86 /* XCRemoteSwiftPackageReference "SwiftSoup" */; productName = SwiftSoup; }; + 0CB6516728C5A5EC00DCA721 /* Introspect */ = { + isa = XCSwiftPackageProductDependency; + package = 0CB6516628C5A5EC00DCA721 /* XCRemoteSwiftPackageReference "SwiftUI-Introspect" */; + productName = Introspect; + }; /* End XCSwiftPackageProductDependency section */ /* Begin XCVersionGroup section */ diff --git a/Ferrite/Extensions/View.swift b/Ferrite/Extensions/View.swift index 7b7e4cc..d1dc471 100644 --- a/Ferrite/Extensions/View.swift +++ b/Ferrite/Extensions/View.swift @@ -5,12 +5,35 @@ // Created by Brian Dashore on 8/15/22. // +import Introspect import SwiftUI extension View { + // MARK: Custom introspect functions + + func introspectCollectionView(customize: @escaping (UICollectionView) -> Void) -> some View { + inject(UIKitIntrospectionView( + selector: { introspectionView in + guard let viewHost = Introspect.findViewHost(from: introspectionView) else { + return nil + } + return Introspect.previousSibling(containing: UICollectionView.self, from: viewHost) + }, + customize: customize + )) + } + // MARK: Modifiers func dynamicAccentColor(_ color: Color) -> some View { modifier(DynamicAccentColor(color: color)) } + + func conditionalId(_ id: ID) -> some View { + modifier(ConditionalId(id: id)) + } + + func inlinedList() -> some View { + modifier(InlinedList()) + } } diff --git a/Ferrite/Views/AboutView.swift b/Ferrite/Views/AboutView.swift index b2285aa..2933955 100644 --- a/Ferrite/Views/AboutView.swift +++ b/Ferrite/Views/AboutView.swift @@ -9,24 +9,32 @@ import SwiftUI struct AboutView: View { var body: some View { - VStack { - Image("AppImage") - .resizable() - .frame(width: 100, height: 100) - .cornerRadius(25) - - Text("Ferrite is a free and open source application developed by kingbri under the GNU-GPLv3 license.") - .padding() - - List { + List { + Section { ListRowTextView(leftText: "Version", rightText: UIApplication.shared.appVersion) ListRowTextView(leftText: "Build number", rightText: UIApplication.shared.appBuild) ListRowTextView(leftText: "Build type", rightText: UIApplication.shared.buildType) ListRowLinkView(text: "Discord server", link: "https://discord.gg/sYQxnuD7Fj") ListRowLinkView(text: "GitHub repository", link: "https://github.com/bdashore3/Ferrite") + } header: { + VStack(alignment: .center) { + Image("AppImage") + .resizable() + .frame(width: 100, height: 100) + .clipShape(RoundedRectangle(cornerRadius: 100 * 0.225, style: .continuous)) + .padding(.top, 24) + + Text("Ferrite is a free and open source application developed by kingbri under the GNU-GPLv3 license.") + .textCase(.none) + .foregroundColor(.label) + .font(.body) + .padding(.top, 8) + .padding(.bottom, 20) + } + .listRowInsets(EdgeInsets(top: 0, leading: 7, bottom: 0, trailing: 0)) } - .listStyle(.insetGrouped) } + .listStyle(.insetGrouped) .navigationTitle("About") } } diff --git a/Ferrite/Views/CommonViews/ConditionalId.swift b/Ferrite/Views/CommonViews/ConditionalId.swift new file mode 100644 index 0000000..53a033d --- /dev/null +++ b/Ferrite/Views/CommonViews/ConditionalId.swift @@ -0,0 +1,24 @@ +// +// ConditionalId.swift +// Ferrite +// +// Created by Brian Dashore on 9/4/22. +// +// Only applies an ID for below iOS 16 +// This is due to ID workarounds making iOS 16 apps crash +// + +import SwiftUI + +struct ConditionalId: ViewModifier { + let id: ID + + func body(content: Content) -> some View { + if #available(iOS 16, *) { + content + } else { + content + .id(id) + } + } +} diff --git a/Ferrite/Views/CommonViews/EmptyInstructionView.swift b/Ferrite/Views/CommonViews/EmptyInstructionView.swift new file mode 100644 index 0000000..8cd0055 --- /dev/null +++ b/Ferrite/Views/CommonViews/EmptyInstructionView.swift @@ -0,0 +1,27 @@ +// +// EmptyInstructionView.swift +// Ferrite +// +// Created by Brian Dashore on 9/5/22. +// + +import SwiftUI + +struct EmptyInstructionView: View { + let title: String + let message: String + + var body: some View { + VStack(spacing: 5) { + Text(title) + .font(.system(size: 25, weight: .semibold)) + + Text(message) + .padding(.horizontal, 50) + } + .multilineTextAlignment(.center) + .foregroundColor(.secondaryLabel) + .frame(maxWidth: .infinity, maxHeight: .infinity) + .ignoresSafeArea() + } +} diff --git a/Ferrite/Views/CommonViews/InlineHeader.swift b/Ferrite/Views/CommonViews/InlineHeader.swift new file mode 100644 index 0000000..dbb70c4 --- /dev/null +++ b/Ferrite/Views/CommonViews/InlineHeader.swift @@ -0,0 +1,30 @@ +// +// InlineHeader.swift +// Ferrite +// +// Created by Brian Dashore on 9/5/22. +// + +import SwiftUI + +struct InlineHeader: View { + let title: String + + init(_ title: String) { + self.title = title + } + + var body: some View { + Group { + if #available(iOS 16, *) { + Text(title) + .padding(.vertical, 5) + } else { + Text(title) + .padding(.vertical, 10) + } + } + .padding(.horizontal, 20) + .listRowInsets(EdgeInsets()) + } +} diff --git a/Ferrite/Views/CommonViews/InlinedList.swift b/Ferrite/Views/CommonViews/InlinedList.swift new file mode 100644 index 0000000..a0eb034 --- /dev/null +++ b/Ferrite/Views/CommonViews/InlinedList.swift @@ -0,0 +1,28 @@ +// +// InlinedList.swift +// Ferrite +// +// Created by Brian Dashore on 9/4/22. +// +// Removes the top padding on lists for iOS 16 +// Use UITableView.appearance().contentInset.top = -20 for iOS 15 and below in the App file +// + +import Introspect +import SwiftUI + +struct InlinedList: ViewModifier { + func body(content: Content) -> some View { + if #available(iOS 16, *) { + content + .introspectCollectionView { collectionView in + collectionView.contentInset.top = -20 + } + } else { + content + .introspectTableView { tableView in + tableView.tableHeaderView = UIView(frame: CGRect(x: 0, y: 0, width: 0, height: 20)) + } + } + } +} diff --git a/Ferrite/Views/ContentView.swift b/Ferrite/Views/ContentView.swift index 6f92f4f..0c1332f 100644 --- a/Ferrite/Views/ContentView.swift +++ b/Ferrite/Views/ContentView.swift @@ -30,7 +30,7 @@ struct ContentView: View { var body: some View { NavView { VStack(spacing: 10) { - HStack { + HStack(spacing: 6) { Text("Filter") .foregroundColor(.secondary) @@ -50,10 +50,10 @@ struct ContentView: View { Button { selectedSource = source } label: { - Text(name) - if selectedSource == source { - Image(systemName: "checkmark") + Label(name, systemImage: "checkmark") + } else { + Text(name) } } } @@ -94,7 +94,6 @@ struct ContentView: View { } } } - .dynamicAccentColor(.primary) } .navigationTitle("Search") .navigationSearchBar { diff --git a/Ferrite/Views/MagnetChoiceView.swift b/Ferrite/Views/MagnetChoiceView.swift index a9701df..ae142b2 100644 --- a/Ferrite/Views/MagnetChoiceView.swift +++ b/Ferrite/Views/MagnetChoiceView.swift @@ -88,6 +88,7 @@ struct MagnetChoiceView: View { } } } + .dynamicAccentColor(.primary) .sheet(isPresented: $navModel.showLocalActivitySheet) { if #available(iOS 16, *) { AppActivityView(activityItems: navModel.activityItems) diff --git a/Ferrite/Views/SearchResultsView.swift b/Ferrite/Views/SearchResultsView.swift index 4ca3a17..9cf050d 100644 --- a/Ferrite/Views/SearchResultsView.swift +++ b/Ferrite/Views/SearchResultsView.swift @@ -54,6 +54,7 @@ struct SearchResultsView: View { } } .listStyle(.insetGrouped) + .inlinedList() .overlay { if scrapingModel.searchResults.isEmpty { if navModel.showSearchProgress { diff --git a/Ferrite/Views/SettingsView.swift b/Ferrite/Views/SettingsView.swift index 72bfa53..56f8f00 100644 --- a/Ferrite/Views/SettingsView.swift +++ b/Ferrite/Views/SettingsView.swift @@ -5,6 +5,7 @@ // Created by Brian Dashore on 7/11/22. // +import Introspect import SwiftUI struct SettingsView: View { @@ -21,7 +22,7 @@ struct SettingsView: View { var body: some View { NavView { Form { - Section(header: "Debrid services") { + Section(header: InlineHeader("Debrid Services")) { HStack { Text("Real Debrid") Spacer() @@ -40,11 +41,11 @@ struct SettingsView: View { } } - Section(header: "Source management") { + Section(header: InlineHeader("Source management")) { NavigationLink("Source lists", destination: SettingsSourceListView()) } - Section(header: "Default actions") { + Section(header: InlineHeader("Default actions")) { if debridManager.realDebridEnabled { NavigationLink( destination: DebridActionPickerView(), @@ -94,14 +95,14 @@ struct SettingsView: View { ) } - Section(header: Text("Updates")) { + Section(header: InlineHeader("Updates")) { Toggle(isOn: $autoUpdateNotifs) { Text("Show update alerts") } NavigationLink("Version history", destination: SettingsAppVersionView()) } - Section(header: Text("Information")) { + Section(header: InlineHeader("Information")) { ListRowLinkView(text: "Donate", link: "https://ko-fi.com/kingbri") ListRowLinkView(text: "Report issues", link: "https://github.com/bdashore3/Ferrite/issues") NavigationLink("About", destination: AboutView()) diff --git a/Ferrite/Views/SettingsViews/DefaultActionsPickerViews.swift b/Ferrite/Views/SettingsViews/DefaultActionsPickerViews.swift index 8eb1c8d..a03be82 100644 --- a/Ferrite/Views/SettingsViews/DefaultActionsPickerViews.swift +++ b/Ferrite/Views/SettingsViews/DefaultActionsPickerViews.swift @@ -29,6 +29,7 @@ struct MagnetActionPickerView: View { } } .listStyle(.insetGrouped) + .inlinedList() .navigationTitle("Default magnet action") .navigationBarTitleDisplayMode(.inline) } @@ -67,6 +68,7 @@ struct DebridActionPickerView: View { } } .listStyle(.insetGrouped) + .inlinedList() .navigationTitle("Default debrid action") .navigationBarTitleDisplayMode(.inline) } diff --git a/Ferrite/Views/SettingsViews/SettingsAppVersionView.swift b/Ferrite/Views/SettingsViews/SettingsAppVersionView.swift index eaf87eb..f15f15d 100644 --- a/Ferrite/Views/SettingsViews/SettingsAppVersionView.swift +++ b/Ferrite/Views/SettingsViews/SettingsAppVersionView.swift @@ -13,12 +13,21 @@ struct SettingsAppVersionView: View { @State private var viewTask: Task? @State private var releases: [GithubRelease] = [] + @State private var loadedReleases = false + var body: some View { - List { - Section(header: Text("GitHub links")) { - ForEach(releases, id: \.self) { release in - ListRowLinkView(text: release.tagName, link: release.htmlUrl) + ZStack { + if !loadedReleases { + ProgressView() + } else if !releases.isEmpty { + List { + Section(header: InlineHeader("GitHub links")) { + ForEach(releases, id: \.self) { release in + ListRowLinkView(text: release.tagName, link: release.htmlUrl) + } + } } + .listStyle(.insetGrouped) } } .onAppear { @@ -32,13 +41,16 @@ struct SettingsAppVersionView: View { } catch { toastModel.updateToastDescription("Github error: \(error)") } + + withAnimation { + loadedReleases = true + } } } .onDisappear { viewTask?.cancel() } - .listStyle(.insetGrouped) - .navigationTitle("Version history") + .navigationTitle("Version History") .navigationBarTitleDisplayMode(.inline) } } diff --git a/Ferrite/Views/SettingsViews/SettingsSourceListView.swift b/Ferrite/Views/SettingsViews/SettingsSourceListView.swift index 38f3618..778d6d7 100644 --- a/Ferrite/Views/SettingsViews/SettingsSourceListView.swift +++ b/Ferrite/Views/SettingsViews/SettingsSourceListView.swift @@ -21,48 +21,63 @@ struct SettingsSourceListView: View { @State private var selectedSourceList: SourceList? var body: some View { - List { - Section(header: Text("List information")) { - ForEach(sourceLists, id: \.self) { sourceList in - VStack(alignment: .leading, spacing: 5) { - Text(sourceList.name) + ZStack { + if sourceLists.isEmpty { + EmptyInstructionView(title: "No Lists", message: "Add a source list using the + button in the top-right") + } else { + List { + ForEach(sourceLists, id: \.self) { sourceList in + VStack(alignment: .leading, spacing: 5) { + Text(sourceList.name) - Text(sourceList.author) - .foregroundColor(.gray) + Text(sourceList.author) + .foregroundColor(.gray) - Text("ID: \(sourceList.id)") - .font(.caption) - .foregroundColor(.gray) + Text("ID: \(sourceList.id)") + .font(.caption) + .foregroundColor(.gray) + } + .padding(.vertical, 2) + .contextMenu { + Button { + navModel.selectedSourceList = sourceList + presentSourceSheet.toggle() + } label: { + Text("Edit") + Image(systemName: "pencil") + } + + if #available(iOS 15.0, *) { + Button(role: .destructive) { + PersistenceController.shared.delete(sourceList, context: backgroundContext) + } label: { + Text("Remove") + Image(systemName: "trash") + } + } else { + Button { + PersistenceController.shared.delete(sourceList, context: backgroundContext) + } label: { + Text("Remove") + Image(systemName: "trash") + } + } + } } - .contextMenu { - Button { - navModel.selectedSourceList = sourceList - presentSourceSheet.toggle() - } label: { - Text("Edit") - Image(systemName: "pencil") - } - - Button { - PersistenceController.shared.delete(sourceList, context: backgroundContext) - } label: { - Text("Remove") - Image(systemName: "trash") - } + } + .listStyle(.insetGrouped) + .inlinedList() + .sheet(isPresented: $presentSourceSheet) { + if #available(iOS 16, *) { + SourceListEditorView() + .presentationDetents([.medium]) + } else { + SourceListEditorView() } } } } - .listStyle(.insetGrouped) - .sheet(isPresented: $presentSourceSheet) { - if #available(iOS 16, *) { - SourceListEditorView() - .presentationDetents([.medium]) - } else { - SourceListEditorView() - } - } - .navigationTitle("Source lists") + .navigationTitle("Source Lists") .navigationBarTitleDisplayMode(.inline) .toolbar { ToolbarItem(placement: .navigationBarTrailing) { diff --git a/Ferrite/Views/SettingsViews/SourceListEditorView.swift b/Ferrite/Views/SettingsViews/SourceListEditorView.swift index 9db11c7..5ddfae3 100644 --- a/Ferrite/Views/SettingsViews/SourceListEditorView.swift +++ b/Ferrite/Views/SettingsViews/SourceListEditorView.swift @@ -15,20 +15,22 @@ struct SourceListEditorView: View { let backgroundContext = PersistenceController.shared.backgroundContext - @State private var sourceUrl = "" + @State private var sourceUrlSet = false + + @State private var sourceUrl: String = "" var body: some View { NavView { Form { - Section { - TextField("Enter URL", text: $sourceUrl) - .disableAutocorrection(true) - .keyboardType(.URL) - .autocapitalization(.none) - } + TextField("Enter URL", text: $sourceUrl) + .disableAutocorrection(true) + .keyboardType(.URL) + .autocapitalization(.none) + .conditionalId(sourceUrlSet) } .onAppear { sourceUrl = navModel.selectedSourceList?.urlString ?? "" + sourceUrlSet = true } .alert(isPresented: $sourceManager.showUrlErrorAlert) { Alert( diff --git a/Ferrite/Views/SourceViews/InstalledSourceView.swift b/Ferrite/Views/SourceViews/InstalledSourceView.swift index 429ab9e..88065f3 100644 --- a/Ferrite/Views/SourceViews/InstalledSourceView.swift +++ b/Ferrite/Views/SourceViews/InstalledSourceView.swift @@ -32,6 +32,7 @@ struct InstalledSourceView: View { Text("by \(installedSource.author)") .foregroundColor(.secondary) } + .padding(.vertical, 2) } .contextMenu { Button { @@ -42,11 +43,20 @@ struct InstalledSourceView: View { Image(systemName: "gear") } - Button { - PersistenceController.shared.delete(installedSource, context: backgroundContext) - } label: { - Text("Remove") - Image(systemName: "trash") + if #available(iOS 15.0, *) { + Button(role: .destructive) { + PersistenceController.shared.delete(installedSource, context: backgroundContext) + } label: { + Text("Remove") + Image(systemName: "trash") + } + } else { + Button { + PersistenceController.shared.delete(installedSource, context: backgroundContext) + } label: { + Text("Remove") + Image(systemName: "trash") + } } } } diff --git a/Ferrite/Views/SourceViews/SourceCatalogView.swift b/Ferrite/Views/SourceViews/SourceCatalogView.swift index 9566df8..cdfa129 100644 --- a/Ferrite/Views/SourceViews/SourceCatalogView.swift +++ b/Ferrite/Views/SourceViews/SourceCatalogView.swift @@ -33,5 +33,6 @@ struct SourceCatalogButtonView: View { } } } + .padding(.vertical, 2) } } diff --git a/Ferrite/Views/SourceViews/SourceSettingsView.swift b/Ferrite/Views/SourceViews/SourceSettingsView.swift index 7f8f926..6a0daa8 100644 --- a/Ferrite/Views/SourceViews/SourceSettingsView.swift +++ b/Ferrite/Views/SourceViews/SourceSettingsView.swift @@ -16,7 +16,7 @@ struct SourceSettingsView: View { NavView { List { if let selectedSource = navModel.selectedSource { - Section(header: "Info") { + Section(header: InlineHeader("Info")) { VStack(alignment: .leading, spacing: 5) { HStack { Text(selectedSource.name) @@ -40,6 +40,7 @@ struct SourceSettingsView: View { .foregroundColor(.secondary) .font(.caption) } + .padding(.vertical, 2) } if selectedSource.dynamicBaseUrl { @@ -77,7 +78,7 @@ struct SourceSettingsBaseUrlView: View { @State private var tempBaseUrl: String = "" var body: some View { Section( - header: Text("Base URL"), + header: InlineHeader("Base URL"), footer: Text("Enter the base URL of your server.") ) { TextField("https://...", text: $tempBaseUrl, onEditingChanged: { isFocused in @@ -109,7 +110,7 @@ struct SourceSettingsApiView: View { var body: some View { Section( - header: Text("API credentials"), + header: InlineHeader("API credentials"), footer: Text("Grab the required API credentials from the website. A client secret can be an API token.") ) { if let clientId = selectedSourceApi.clientId, clientId.dynamic { @@ -145,7 +146,7 @@ struct SourceSettingsMethodView: View { @ObservedObject var selectedSource: Source var body: some View { - Section(header: Text("Fetch method")) { + Section(header: InlineHeader("Fetch method")) { if selectedSource.api != nil, selectedSource.jsonParser != nil { Button { selectedSource.preferredParser = SourcePreferredParser.siteApi.rawValue diff --git a/Ferrite/Views/SourceViews/SourceUpdateButtonView.swift b/Ferrite/Views/SourceViews/SourceUpdateButtonView.swift index 029788b..2b0f1f2 100644 --- a/Ferrite/Views/SourceViews/SourceUpdateButtonView.swift +++ b/Ferrite/Views/SourceViews/SourceUpdateButtonView.swift @@ -24,6 +24,7 @@ struct SourceUpdateButtonView: View { Text("by \(updatedSource.author ?? "Unknown")") .foregroundColor(.secondary) } + .padding(.vertical, 2) Spacer() diff --git a/Ferrite/Views/SourcesView.swift b/Ferrite/Views/SourcesView.swift index 570c92b..7dab7f5 100644 --- a/Ferrite/Views/SourcesView.swift +++ b/Ferrite/Views/SourcesView.swift @@ -5,9 +5,13 @@ // Created by Brian Dashore on 7/24/22. // +import Introspect import SwiftUI +import SwiftUIX struct SourcesView: View { + @Environment(\.verticalSizeClass) var verticalSizeClass: UserInterfaceSizeClass? + @EnvironmentObject var sourceManager: SourceManager @EnvironmentObject var navModel: NavigationViewModel @@ -36,65 +40,101 @@ struct SourcesView: View { return tempSources } + @State private var checkedForSources = false + @State private var isEditing = false + @State private var viewTask: Task? = nil + @State private var searchText: String = "" + @State private var filteredUpdatedSources: [SourceJson] = [] + @State private var filteredAvailableSources: [SourceJson] = [] var body: some View { NavView { - List { - if !updatedSources.isEmpty { - Section(header: "Updates") { - ForEach(updatedSources, id: \.self) { source in - SourceUpdateButtonView(updatedSource: source) + ZStack { + if !checkedForSources { + ProgressView() + } else if sources.isEmpty, sourceManager.availableSources.isEmpty { + EmptyInstructionView(title: "No Sources", message: "Add a source list in Settings") + } else { + List { + if !filteredUpdatedSources.isEmpty { + Section(header: InlineHeader("Updates")) { + ForEach(filteredUpdatedSources, id: \.self) { source in + SourceUpdateButtonView(updatedSource: source) + } + } } - } - } - if !sources.isEmpty { - Section(header: "Installed") { - ForEach(sources, id: \.self) { source in - InstalledSourceView(installedSource: source) + if !sources.isEmpty { + Section(header: InlineHeader("Installed")) { + ForEach(sources, id: \.self) { source in + InstalledSourceView(installedSource: source) + } + } } - } - } - if sourceManager.availableSources.contains(where: { availableSource in - !sources.contains( - where: { - availableSource.name == $0.name && - availableSource.listId == $0.listId && - availableSource.author == $0.author - } - ) - }) { - Section(header: "Catalog") { - ForEach(sourceManager.availableSources, id: \.self) { availableSource in - if !sources.contains( + if !filteredAvailableSources.isEmpty, sourceManager.availableSources.contains(where: { availableSource in + !sources.contains( where: { availableSource.name == $0.name && availableSource.listId == $0.listId && availableSource.author == $0.author } - ) { - SourceCatalogButtonView(availableSource: availableSource) + ) + }) { + Section(header: InlineHeader("Catalog")) { + ForEach(filteredAvailableSources, id: \.self) { availableSource in + if !sources.contains( + where: { + availableSource.name == $0.name && + availableSource.listId == $0.listId && + availableSource.author == $0.author + } + ) { + SourceCatalogButtonView(availableSource: availableSource) + } + } } } } + .conditionalId(UUID()) + .listStyle(.insetGrouped) } } - .listStyle(.insetGrouped) .sheet(isPresented: $navModel.showSourceSettings) { SourceSettingsView() .environmentObject(navModel) } .onAppear { + filteredUpdatedSources = updatedSources viewTask = Task { await sourceManager.fetchSourcesFromUrl() + filteredAvailableSources = sourceManager.availableSources + checkedForSources = true } } .onDisappear { viewTask?.cancel() } .navigationTitle("Sources") + .navigationSearchBar { + SearchBar("Search", text: $searchText, isEditing: $isEditing) + .showsCancelButton(isEditing) + .onCancel { + searchText = "" + } + } + .onChange(of: searchText) { _ in + filteredAvailableSources = sourceManager.availableSources.filter { searchText.isEmpty ? true : $0.name.contains(searchText) } + filteredUpdatedSources = updatedSources.filter { searchText.isEmpty ? true : $0.name.contains(searchText) } + if #available(iOS 15.0, *) { + if searchText.isEmpty { + sources.nsPredicate = nil + } else { + sources.nsPredicate = NSPredicate(format: "name CONTAINS[cd] %@", searchText) + } + } + } } } }