Various changes and fixes #7
20 changed files with 365 additions and 106 deletions
|
|
@ -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 = "<group>"; };
|
||||
0CA3FB1F28B91D9500FA10A8 /* IndeterminateProgressView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = IndeterminateProgressView.swift; sourceTree = "<group>"; };
|
||||
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 = "<group>"; };
|
||||
0CB6516428C5A5D700DCA721 /* InlinedList.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InlinedList.swift; sourceTree = "<group>"; };
|
||||
0CB6516928C5B4A600DCA721 /* InlineHeader.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InlineHeader.swift; sourceTree = "<group>"; };
|
||||
0CBC76FC288D914F0054BE44 /* BatchChoiceView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BatchChoiceView.swift; sourceTree = "<group>"; };
|
||||
0CBC76FE288DAAD00054BE44 /* NavigationViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NavigationViewModel.swift; sourceTree = "<group>"; };
|
||||
0CBC7704288DE7F40054BE44 /* PersistenceController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PersistenceController.swift; sourceTree = "<group>"; };
|
||||
0CC6E4D428A45BA000AF2BCC /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist; path = Info.plist; sourceTree = "<group>"; };
|
||||
0CDCB91728C662640098B513 /* EmptyInstructionView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EmptyInstructionView.swift; sourceTree = "<group>"; };
|
||||
0CFEFCFC288A006200B3F490 /* GroupBoxStyle.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GroupBoxStyle.swift; sourceTree = "<group>"; };
|
||||
/* 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 = "<group>";
|
||||
|
|
@ -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 */
|
||||
|
|
|
|||
|
|
@ -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: Hashable>(_ id: ID) -> some View {
|
||||
modifier(ConditionalId(id: id))
|
||||
}
|
||||
|
||||
func inlinedList() -> some View {
|
||||
modifier(InlinedList())
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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")
|
||||
}
|
||||
}
|
||||
|
|
|
|||
24
Ferrite/Views/CommonViews/ConditionalId.swift
Normal file
24
Ferrite/Views/CommonViews/ConditionalId.swift
Normal file
|
|
@ -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<ID: Hashable>: ViewModifier {
|
||||
let id: ID
|
||||
|
||||
func body(content: Content) -> some View {
|
||||
if #available(iOS 16, *) {
|
||||
content
|
||||
} else {
|
||||
content
|
||||
.id(id)
|
||||
}
|
||||
}
|
||||
}
|
||||
27
Ferrite/Views/CommonViews/EmptyInstructionView.swift
Normal file
27
Ferrite/Views/CommonViews/EmptyInstructionView.swift
Normal file
|
|
@ -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()
|
||||
}
|
||||
}
|
||||
30
Ferrite/Views/CommonViews/InlineHeader.swift
Normal file
30
Ferrite/Views/CommonViews/InlineHeader.swift
Normal file
|
|
@ -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())
|
||||
}
|
||||
}
|
||||
28
Ferrite/Views/CommonViews/InlinedList.swift
Normal file
28
Ferrite/Views/CommonViews/InlinedList.swift
Normal file
|
|
@ -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))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -88,6 +88,7 @@ struct MagnetChoiceView: View {
|
|||
}
|
||||
}
|
||||
}
|
||||
.dynamicAccentColor(.primary)
|
||||
.sheet(isPresented: $navModel.showLocalActivitySheet) {
|
||||
if #available(iOS 16, *) {
|
||||
AppActivityView(activityItems: navModel.activityItems)
|
||||
|
|
|
|||
|
|
@ -54,6 +54,7 @@ struct SearchResultsView: View {
|
|||
}
|
||||
}
|
||||
.listStyle(.insetGrouped)
|
||||
.inlinedList()
|
||||
.overlay {
|
||||
if scrapingModel.searchResults.isEmpty {
|
||||
if navModel.showSearchProgress {
|
||||
|
|
|
|||
|
|
@ -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())
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -13,12 +13,21 @@ struct SettingsAppVersionView: View {
|
|||
@State private var viewTask: Task<Void, Never>?
|
||||
@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)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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) {
|
||||
|
|
|
|||
|
|
@ -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(
|
||||
|
|
|
|||
|
|
@ -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")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -33,5 +33,6 @@ struct SourceCatalogButtonView: View {
|
|||
}
|
||||
}
|
||||
}
|
||||
.padding(.vertical, 2)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -24,6 +24,7 @@ struct SourceUpdateButtonView: View {
|
|||
Text("by \(updatedSource.author ?? "Unknown")")
|
||||
.foregroundColor(.secondary)
|
||||
}
|
||||
.padding(.vertical, 2)
|
||||
|
||||
Spacer()
|
||||
|
||||
|
|
|
|||
|
|
@ -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<Void, Never>? = 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)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in a new issue