Plugins: Add website and about properties

These will serve as descriptions for a plugin which will be displayed
in the Plugin Info screen.

website has also replaced baseUrl and dynamicWebsite has replaced
dynamicBaseUrl

Signed-off-by: kingbri <bdashore3@proton.me>
This commit is contained in:
kingbri 2023-04-02 00:18:18 -04:00
parent 51366f3215
commit d918039810
14 changed files with 162 additions and 73 deletions

View file

@ -137,6 +137,8 @@
0CBC7705288DE7F40054BE44 /* PersistenceController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0CBC7704288DE7F40054BE44 /* PersistenceController.swift */; };
0CC389532970AD900066D06F /* Action+CoreDataClass.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0CC389512970AD900066D06F /* Action+CoreDataClass.swift */; };
0CC389542970AD900066D06F /* Action+CoreDataProperties.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0CC389522970AD900066D06F /* Action+CoreDataProperties.swift */; };
0CD4030A29DA01B6008D9F03 /* PluginInfoMetaView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0CD4030929DA01B6008D9F03 /* PluginInfoMetaView.swift */; };
0CD4030C29DA0222008D9F03 /* PluginInfoAboutView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0CD4030B29DA0222008D9F03 /* PluginInfoAboutView.swift */; };
0CD4CAC628C980EB0046E1DC /* HistoryActionsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0CD4CAC528C980EB0046E1DC /* HistoryActionsView.swift */; };
0CD5E78928CD932B001BF684 /* DisabledAppearance.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0CD5E78828CD932B001BF684 /* DisabledAppearance.swift */; };
0CD5F1FB299BEFBE00476DDB /* CustomScopeBar.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0CD5F1FA299BEFBE00476DDB /* CustomScopeBar.swift */; };
@ -277,6 +279,8 @@
0CC389512970AD900066D06F /* Action+CoreDataClass.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Action+CoreDataClass.swift"; sourceTree = "<group>"; };
0CC389522970AD900066D06F /* Action+CoreDataProperties.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Action+CoreDataProperties.swift"; sourceTree = "<group>"; };
0CC6E4D428A45BA000AF2BCC /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist; path = Info.plist; sourceTree = "<group>"; };
0CD4030929DA01B6008D9F03 /* PluginInfoMetaView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PluginInfoMetaView.swift; sourceTree = "<group>"; };
0CD4030B29DA0222008D9F03 /* PluginInfoAboutView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PluginInfoAboutView.swift; sourceTree = "<group>"; };
0CD4CAC528C980EB0046E1DC /* HistoryActionsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HistoryActionsView.swift; sourceTree = "<group>"; };
0CD5E78828CD932B001BF684 /* DisabledAppearance.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DisabledAppearance.swift; sourceTree = "<group>"; };
0CD5F1FA299BEFBE00476DDB /* CustomScopeBar.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CustomScopeBar.swift; sourceTree = "<group>"; };
@ -420,6 +424,7 @@
0C3E00D4296F560800ECECB2 /* Plugin */ = {
isa = PBXGroup;
children = (
0CD4030829DA01A3008D9F03 /* Info */,
0C44E2AA28D4E09B007711AE /* Buttons */,
0C794B65289DAC9F00DD1CC8 /* Source */,
0C0D50E6288DFF850035ECC8 /* PluginAggregateView.swift */,
@ -665,6 +670,15 @@
path = DataManagement;
sourceTree = "<group>";
};
0CD4030829DA01A3008D9F03 /* Info */ = {
isa = PBXGroup;
children = (
0CD4030929DA01B6008D9F03 /* PluginInfoMetaView.swift */,
0CD4030B29DA0222008D9F03 /* PluginInfoAboutView.swift */,
);
path = Info;
sourceTree = "<group>";
};
/* End PBXGroup section */
/* Begin PBXNativeTarget section */
@ -794,8 +808,10 @@
0CD5E78928CD932B001BF684 /* DisabledAppearance.swift in Sources */,
0CA148DB288903F000DE2211 /* NavView.swift in Sources */,
0CA3B23C28C2AA5600616D3A /* Bookmark+CoreDataClass.swift in Sources */,
0CD4030A29DA01B6008D9F03 /* PluginInfoMetaView.swift in Sources */,
0C750745289B003E004B3906 /* SourceRssParser+CoreDataProperties.swift in Sources */,
0CA3FB2028B91D9500FA10A8 /* IndeterminateProgressView.swift in Sources */,
0CD4030C29DA0222008D9F03 /* PluginInfoAboutView.swift in Sources */,
0C50055A2992BA6A0064606A /* PluginTag+CoreDataClass.swift in Sources */,
0C1A3E5229C8A7F500DA9730 /* SettingsModels.swift in Sources */,
0CBC7705288DE7F40054BE44 /* PersistenceController.swift in Sources */,
@ -1129,7 +1145,7 @@
repositoryURL = "https://github.com/siteline/SwiftUI-Introspect/";
requirement = {
kind = upToNextMajorVersion;
minimumVersion = 0.2.2;
minimumVersion = 0.2.3;
};
};
0C4CFC442897030D00AD9FAD /* XCRemoteSwiftPackageReference "Regex" */ = {

View file

@ -19,6 +19,8 @@ public extension Action {
@NSManaged var name: String
@NSManaged var deeplink: String?
@NSManaged var version: Int16
@NSManaged var about: String?
@NSManaged var website: String?
@NSManaged var requires: [String]
@NSManaged var author: String
@NSManaged var enabled: Bool

View file

@ -15,9 +15,10 @@ public extension Source {
}
@NSManaged var id: UUID
@NSManaged var baseUrl: String?
@NSManaged var about: String?
@NSManaged var website: String?
@NSManaged var dynamicWebsite: Bool
@NSManaged var fallbackUrls: [String]?
@NSManaged var dynamicBaseUrl: Bool
@NSManaged var enabled: Bool
@NSManaged var name: String
@NSManaged var author: String

View file

@ -1,6 +1,7 @@
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<model type="com.apple.IDECoreDataModeler.DataModel" documentVersion="1.0" lastSavedToolsVersion="21513" systemVersion="22D49" minimumToolsVersion="Automatic" sourceLanguage="Swift" userDefinedModelVersionIdentifier="">
<entity name="Action" representedClassName="Action" syncable="YES">
<attribute name="about" optional="YES" attributeType="String"/>
<attribute name="author" attributeType="String" defaultValueString=""/>
<attribute name="deeplink" optional="YES" attributeType="String"/>
<attribute name="enabled" attributeType="Boolean" defaultValueString="NO" usesScalarValueType="YES"/>
@ -9,6 +10,7 @@
<attribute name="name" attributeType="String" defaultValueString=""/>
<attribute name="requires" attributeType="Transformable" valueTransformerName="NSSecureUnarchiveFromData" customClassName="[String]"/>
<attribute name="version" attributeType="Integer 16" defaultValueString="0" usesScalarValueType="YES"/>
<attribute name="website" optional="YES" attributeType="String"/>
<relationship name="tags" optional="YES" toMany="YES" deletionRule="Cascade" ordered="YES" destinationEntity="PluginTag" inverseName="parentAction" inverseEntity="PluginTag"/>
</entity>
<entity name="Bookmark" representedClassName="Bookmark" syncable="YES">
@ -53,9 +55,9 @@
<relationship name="parentSource" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="Source" inverseName="tags" inverseEntity="Source"/>
</entity>
<entity name="Source" representedClassName="Source" syncable="YES">
<attribute name="about" optional="YES" attributeType="String"/>
<attribute name="author" attributeType="String" defaultValueString=""/>
<attribute name="baseUrl" optional="YES" attributeType="String"/>
<attribute name="dynamicBaseUrl" optional="YES" attributeType="Boolean" usesScalarValueType="YES"/>
<attribute name="dynamicWebsite" optional="YES" attributeType="Boolean" usesScalarValueType="YES"/>
<attribute name="enabled" attributeType="Boolean" defaultValueString="NO" usesScalarValueType="YES"/>
<attribute name="fallbackUrls" optional="YES" attributeType="Transformable" valueTransformerName="NSSecureUnarchiveFromData" customClassName="[String]"/>
<attribute name="id" attributeType="UUID" usesScalarValueType="NO"/>
@ -64,6 +66,7 @@
<attribute name="preferredParser" attributeType="Integer 16" defaultValueString="0" usesScalarValueType="YES"/>
<attribute name="trackers" optional="YES" attributeType="Transformable" valueTransformerName="NSSecureUnarchiveFromData" customClassName="[String]"/>
<attribute name="version" attributeType="Integer 16" defaultValueString="0" usesScalarValueType="YES"/>
<attribute name="website" optional="YES" attributeType="String"/>
<relationship name="api" optional="YES" maxCount="1" deletionRule="Cascade" destinationEntity="SourceApi" inverseName="parentSource" inverseEntity="SourceApi"/>
<relationship name="htmlParser" optional="YES" maxCount="1" deletionRule="Cascade" destinationEntity="SourceHtmlParser" inverseName="parentSource" inverseEntity="SourceHtmlParser"/>
<relationship name="jsonParser" optional="YES" maxCount="1" deletionRule="Cascade" destinationEntity="SourceJsonParser" inverseName="parentSource" inverseEntity="SourceJsonParser"/>

View file

@ -11,6 +11,8 @@ public struct ActionJson: Codable, Hashable, PluginJson {
public let name: String
public let version: Int16
let minVersion: String?
let about: String?
let website: String?
let requires: [ActionRequirement]
let deeplink: [DeeplinkActionJson]?
public let author: String?
@ -21,6 +23,8 @@ public struct ActionJson: Codable, Hashable, PluginJson {
public init(name: String,
version: Int16,
minVersion: String?,
about: String?,
website: String?,
requires: [ActionRequirement],
deeplink: [DeeplinkActionJson]?,
author: String?,
@ -31,6 +35,8 @@ public struct ActionJson: Codable, Hashable, PluginJson {
self.name = name
self.version = version
self.minVersion = minVersion
self.about = about
self.website = website
self.requires = requires
self.deeplink = deeplink
self.author = author
@ -44,6 +50,8 @@ public struct ActionJson: Codable, Hashable, PluginJson {
name = try container.decode(String.self, forKey: .name)
version = try container.decode(Int16.self, forKey: .version)
minVersion = try container.decodeIfPresent(String.self, forKey: .minVersion)
about = try container.decodeIfPresent(String.self, forKey: .about)
website = try container.decodeIfPresent(String.self, forKey: .website)
requires = try container.decode([ActionRequirement].self, forKey: .requires)
author = try container.decodeIfPresent(String.self, forKey: .author)
listId = nil

View file

@ -16,9 +16,10 @@ public struct SourceJson: Codable, Hashable, Sendable, PluginJson {
public let name: String
public let version: Int16
let minVersion: String?
let baseUrl: String?
let about: String?
let website: String?
let dynamicWebsite: Bool?
let fallbackUrls: [String]?
let dynamicBaseUrl: Bool?
let trackers: [String]?
let api: SourceApiJson?
let jsonParser: SourceJsonParserJson?

View file

@ -14,6 +14,8 @@ public protocol Plugin: ObservableObject, NSManagedObject {
var name: String { get set }
var version: Int16 { get set }
var author: String { get set }
var about: String? { get set }
var website: String? { get set }
var enabled: Bool { get set }
var tags: NSOrderedSet? { get set }
func getTags() -> [PluginTagJson]

View file

@ -124,9 +124,10 @@ public class PluginManager: ObservableObject {
name: inputJson.name,
version: inputJson.version,
minVersion: inputJson.minVersion,
baseUrl: inputJson.baseUrl,
about: inputJson.about,
website: inputJson.website,
dynamicWebsite: inputJson.dynamicWebsite,
fallbackUrls: inputJson.fallbackUrls,
dynamicBaseUrl: inputJson.dynamicBaseUrl,
trackers: inputJson.trackers,
api: inputJson.api,
jsonParser: inputJson.jsonParser,
@ -154,6 +155,8 @@ public class PluginManager: ObservableObject {
name: inputJson.name,
version: inputJson.version,
minVersion: inputJson.minVersion,
about: inputJson.about,
website: inputJson.website,
requires: inputJson.requires,
deeplink: filteredDeeplinks,
author: pluginList.author,
@ -409,6 +412,8 @@ public class PluginManager: ObservableObject {
newAction.id = UUID()
newAction.name = actionJson.name
newAction.version = actionJson.version
newAction.website = actionJson.website
newAction.about = actionJson.about
newAction.author = actionJson.author ?? "Unknown"
newAction.listId = actionJson.listId
newAction.requires = actionJson.requires.map(\.rawValue)
@ -448,9 +453,9 @@ public class PluginManager: ObservableObject {
let backgroundContext = PersistenceController.shared.backgroundContext
// If there's no base URL and it isn't dynamic, return before any transactions occur
let dynamicBaseUrl = sourceJson.dynamicBaseUrl ?? false
if !dynamicBaseUrl, sourceJson.baseUrl == nil {
await logManager?.error("Not adding this source because base URL parameters are malformed. Please contact the source dev.")
let dynamicWebsite = sourceJson.dynamicWebsite ?? false
if !dynamicWebsite, sourceJson.website == nil {
await logManager?.error("Not adding this source because website parameters are malformed. Please contact the source dev.")
return
}
@ -472,9 +477,10 @@ public class PluginManager: ObservableObject {
newSource.id = UUID()
newSource.name = sourceJson.name
newSource.version = sourceJson.version
newSource.dynamicBaseUrl = dynamicBaseUrl
newSource.baseUrl = sourceJson.baseUrl
newSource.fallbackUrls = dynamicBaseUrl ? nil : sourceJson.fallbackUrls
newSource.about = sourceJson.about
newSource.website = sourceJson.website
newSource.dynamicWebsite = dynamicWebsite
newSource.fallbackUrls = dynamicWebsite ? nil : sourceJson.fallbackUrls
newSource.author = sourceJson.author ?? "Unknown"
newSource.listId = sourceJson.listId
newSource.trackers = sourceJson.trackers

View file

@ -144,7 +144,7 @@ class ScrapingViewModel: ObservableObject {
}
func executeParser(source: Source) async -> SearchRequestResult? {
guard let baseUrl = source.baseUrl else {
guard let website = source.website else {
await logManager?.error("Scraping: The base URL could not be found for source \(source.name)")
return nil
@ -167,7 +167,7 @@ class ScrapingViewModel: ObservableObject {
}
let data = await handleUrls(
baseUrl: baseUrl,
website: website,
replacedSearchUrl: replacedSearchUrl,
fallbackUrls: source.fallbackUrls,
sourceName: source.name
@ -176,7 +176,7 @@ class ScrapingViewModel: ObservableObject {
if let data,
let html = String(data: data, encoding: .utf8)
{
return await scrapeHtml(source: source, baseUrl: baseUrl, html: html)
return await scrapeHtml(source: source, website: website, html: html)
}
}
case .rss:
@ -194,7 +194,7 @@ class ScrapingViewModel: ObservableObject {
)
} else {
data = await handleUrls(
baseUrl: baseUrl,
website: website,
replacedSearchUrl: replacedSearchUrl,
fallbackUrls: source.fallbackUrls,
sourceName: source.name
@ -220,7 +220,7 @@ class ScrapingViewModel: ObservableObject {
replacement: "{clientId}",
searchUrl: replacedSearchUrl,
apiUrl: sourceApi.apiUrl,
baseUrl: baseUrl,
website: website,
sourceName: source.name)
{
replacedSearchUrl = newSearchUrl
@ -233,7 +233,7 @@ class ScrapingViewModel: ObservableObject {
replacement: "{secret}",
searchUrl: replacedSearchUrl,
apiUrl: sourceApi.apiUrl,
baseUrl: baseUrl,
website: website,
sourceName: source.name)
{
replacedSearchUrl = newSearchUrl
@ -241,9 +241,9 @@ class ScrapingViewModel: ObservableObject {
}
}
let passedUrl = source.api?.apiUrl ?? baseUrl
let passedUrl = source.api?.apiUrl ?? website
let data = await handleUrls(
baseUrl: passedUrl,
website: passedUrl,
replacedSearchUrl: replacedSearchUrl,
fallbackUrls: source.fallbackUrls,
sourceName: source.name
@ -261,8 +261,8 @@ class ScrapingViewModel: ObservableObject {
}
// Checks the base URL for any website data then iterates through the fallback URLs
func handleUrls(baseUrl: String, replacedSearchUrl: String?, fallbackUrls: [String]?, sourceName: String) async -> Data? {
let fetchUrl = baseUrl + (replacedSearchUrl.map { $0 } ?? "")
func handleUrls(website: String, replacedSearchUrl: String?, fallbackUrls: [String]?, sourceName: String) async -> Data? {
let fetchUrl = website + (replacedSearchUrl.map { $0 } ?? "")
if let data = await fetchWebsiteData(urlString: fetchUrl, sourceName: sourceName) {
return data
}
@ -283,7 +283,7 @@ class ScrapingViewModel: ObservableObject {
replacement: String,
searchUrl: String,
apiUrl: String?,
baseUrl: String,
website: String,
sourceName: String) async -> String?
{
// Is the credential expired
@ -302,7 +302,7 @@ class ScrapingViewModel: ObservableObject {
credential.value == nil || isExpired,
let credentialUrl = credential.urlString,
let newValue = await fetchApiCredential(
urlString: (apiUrl ?? baseUrl) + credentialUrl,
urlString: (apiUrl ?? website) + credentialUrl,
credential: credential,
sourceName: sourceName
)
@ -733,7 +733,7 @@ class ScrapingViewModel: ObservableObject {
}
// HTML scraper
public func scrapeHtml(source: Source, baseUrl: String, html: String) async -> SearchRequestResult? {
public func scrapeHtml(source: Source, website: String, html: String) async -> SearchRequestResult? {
guard let htmlParser = source.htmlParser else {
return nil
}
@ -783,7 +783,7 @@ class ScrapingViewModel: ObservableObject {
continue
}
let replacedMagnetUrl = externalMagnetUrl.starts(with: "/") ? baseUrl + externalMagnetUrl : externalMagnetUrl
let replacedMagnetUrl = externalMagnetUrl.starts(with: "/") ? website + externalMagnetUrl : externalMagnetUrl
guard
let data = await fetchWebsiteData(urlString: replacedMagnetUrl, sourceName: source.name),
let magnetHtml = String(data: data, encoding: .utf8)

View file

@ -0,0 +1,30 @@
//
// PluginInfoAboutView.swift
// Ferrite
//
// Created by Brian Dashore on 4/2/23.
//
import SwiftUI
struct PluginInfoAboutView<P: Plugin>: View {
@ObservedObject var selectedPlugin: P
var body: some View {
Section(header: InlineHeader("Description")) {
VStack(alignment: .leading, spacing: 10) {
if let pluginAbout = selectedPlugin.about {
if pluginAbout.last == "\n" {
Text(pluginAbout.dropLast())
} else {
Text(pluginAbout)
}
}
if let pluginWebsite = selectedPlugin.website {
Link("Website", destination: URL(string: pluginWebsite) ?? URL(string: "https://kingbri.dev/ferrite")!)
}
}
}
}
}

View file

@ -0,0 +1,54 @@
//
// PluginInfoMetaView.swift
// Ferrite
//
// Created by Brian Dashore on 4/2/23.
//
import SwiftUI
struct PluginInfoMetaView<P: Plugin>: View {
@ObservedObject var selectedPlugin: P
@FetchRequest(
entity: PluginList.entity(),
sortDescriptors: []
) var pluginLists: FetchedResults<PluginList>
var body: some View {
Section(header: InlineHeader("Metadata")) {
VStack(alignment: .leading) {
VStack(alignment: .leading, spacing: 5) {
HStack(spacing: 5) {
Text(selectedPlugin.name)
Text("v\(selectedPlugin.version)")
.foregroundColor(.secondary)
}
Text("by \(selectedPlugin.author)")
.foregroundColor(.secondary)
Group {
Text("ID: \(selectedPlugin.id)")
if let pluginList = pluginLists.first(where: { $0.id == selectedPlugin.listId })
{
Text("List: \(pluginList.name)")
Text("List ID: \(pluginList.id.uuidString)")
} else {
Text("No plugin list found. This source should be removed.")
}
}
.foregroundColor(.secondary)
.font(.caption)
}
if let tags = selectedPlugin.getTags(), !tags.isEmpty {
PluginTagsView(tags: tags)
}
}
.padding(.vertical, 2)
}
}
}

View file

@ -12,48 +12,14 @@ struct PluginInfoView<P: Plugin>: View {
@Binding var selectedPlugin: P?
@FetchRequest(
entity: PluginList.entity(),
sortDescriptors: []
) var pluginLists: FetchedResults<PluginList>
var body: some View {
NavView {
List {
if let selectedPlugin {
Section(header: InlineHeader("Info")) {
VStack(alignment: .leading) {
VStack(alignment: .leading, spacing: 5) {
HStack(spacing: 5) {
Text(selectedPlugin.name)
PluginInfoMetaView(selectedPlugin: selectedPlugin)
Text("v\(selectedPlugin.version)")
.foregroundColor(.secondary)
}
Text("by \(selectedPlugin.author)")
.foregroundColor(.secondary)
Group {
Text("ID: \(selectedPlugin.id)")
if let pluginList = pluginLists.first(where: { $0.id == selectedPlugin.listId })
{
Text("List: \(pluginList.name)")
Text("List ID: \(pluginList.id.uuidString)")
} else {
Text("No plugin list found. This source should be removed.")
}
}
.foregroundColor(.secondary)
.font(.caption)
}
if let tags = selectedPlugin.getTags(), !tags.isEmpty {
PluginTagsView(tags: tags)
}
}
.padding(.vertical, 2)
if selectedPlugin.about != nil || selectedPlugin.website != nil {
PluginInfoAboutView(selectedPlugin: selectedPlugin)
}
if let selectedSource = selectedPlugin as? Source {

View file

@ -10,18 +10,18 @@ import SwiftUI
struct SourceSettingsBaseUrlView: View {
@ObservedObject var selectedSource: Source
@State private var tempBaseUrl: String = ""
@State private var tempSite: String = ""
var body: some View {
Section(
header: InlineHeader("Base URL"),
footer: Text("Enter the base URL of your server.")
) {
TextField("https://...", text: $tempBaseUrl, onEditingChanged: { isFocused in
TextField("https://...", text: $tempSite, onEditingChanged: { isFocused in
if !isFocused {
if tempBaseUrl.last == "/" {
selectedSource.baseUrl = String(tempBaseUrl.dropLast())
if tempSite.last == "/" {
selectedSource.website = String(tempSite.dropLast())
} else {
selectedSource.baseUrl = tempBaseUrl
selectedSource.website = tempSite
}
}
})
@ -29,7 +29,7 @@ struct SourceSettingsBaseUrlView: View {
.autocorrectionDisabled(true)
.autocapitalization(.none)
.onAppear {
tempBaseUrl = selectedSource.baseUrl ?? ""
tempSite = selectedSource.website ?? ""
}
}
}

View file

@ -11,7 +11,7 @@ struct SourceSettingsView: View {
@ObservedObject var selectedSource: Source
var body: some View {
if selectedSource.dynamicBaseUrl {
if selectedSource.dynamicWebsite {
SourceSettingsBaseUrlView(selectedSource: selectedSource)
}