Skip to content

Commit

Permalink
OPML export
Browse files Browse the repository at this point in the history
  • Loading branch information
Giorgio Calderolla committed May 22, 2021
1 parent f373c8a commit 863759d
Show file tree
Hide file tree
Showing 17 changed files with 181 additions and 71 deletions.
12 changes: 6 additions & 6 deletions Catch.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,7 @@
44BA5D1A1DB81A8E00A9CE9A /* Application.swift in Sources */ = {isa = PBXBuildFile; fileRef = 44BA5D191DB81A8E00A9CE9A /* Application.swift */; };
44BA5D1C1DB8220100A9CE9A /* MenuController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 44BA5D1B1DB8220100A9CE9A /* MenuController.swift */; };
44C81989220D6B7700D9DAAD /* Feed.swift in Sources */ = {isa = PBXBuildFile; fileRef = 44C81988220D6B7700D9DAAD /* Feed.swift */; };
44C8198B220D73DC00D9DAAD /* OPMLParser.swift in Sources */ = {isa = PBXBuildFile; fileRef = 44C8198A220D73DC00D9DAAD /* OPMLParser.swift */; };
44C8198B220D73DC00D9DAAD /* OPML.swift in Sources */ = {isa = PBXBuildFile; fileRef = 44C8198A220D73DC00D9DAAD /* OPML.swift */; };
44C8926F1D1814A7008F8543 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 44C8926E1D1814A7008F8543 /* AppDelegate.swift */; };
44E2CEA31DBC0B8F00ED7A8D /* PreferencesController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 44E2CEA21DBC0B8F00ED7A8D /* PreferencesController.swift */; };
44E2CEA51DBC134E00ED7A8D /* FeedParser.swift in Sources */ = {isa = PBXBuildFile; fileRef = 44E2CEA41DBC134E00ED7A8D /* FeedParser.swift */; };
Expand Down Expand Up @@ -163,7 +163,7 @@
44BA5D191DB81A8E00A9CE9A /* Application.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = Application.swift; path = Sources/App/Application.swift; sourceTree = "<group>"; };
44BA5D1B1DB8220100A9CE9A /* MenuController.swift */ = {isa = PBXFileReference; fileEncoding = 4; indentWidth = 2; lastKnownFileType = sourcecode.swift; name = MenuController.swift; path = Sources/App/MenuController.swift; sourceTree = "<group>"; tabWidth = 2; };
44C81988220D6B7700D9DAAD /* Feed.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = Feed.swift; path = Sources/Shared/Feed.swift; sourceTree = "<group>"; };
44C8198A220D73DC00D9DAAD /* OPMLParser.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = OPMLParser.swift; path = Sources/App/OPMLParser.swift; sourceTree = "<group>"; };
44C8198A220D73DC00D9DAAD /* OPML.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = OPML.swift; path = Sources/App/OPML.swift; sourceTree = "<group>"; };
44C8926E1D1814A7008F8543 /* AppDelegate.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = AppDelegate.swift; path = Sources/App/AppDelegate.swift; sourceTree = "<group>"; };
44E2CEA21DBC0B8F00ED7A8D /* PreferencesController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = PreferencesController.swift; path = Sources/App/PreferencesController.swift; sourceTree = "<group>"; };
44E2CEA41DBC134E00ED7A8D /* FeedParser.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = FeedParser.swift; path = "Sources/Feed Helper/FeedParser.swift"; sourceTree = SOURCE_ROOT; };
Expand Down Expand Up @@ -314,7 +314,7 @@
44A6FA8A1DE0B85C005303DF /* FeedHelperProxy.swift */,
44AB5B5D1DCE794A00AE6EB6 /* HistoryItem.swift */,
44A6FA8C1DE1A5F1005303DF /* IntervalTimer.swift */,
44C8198A220D73DC00D9DAAD /* OPMLParser.swift */,
44C8198A220D73DC00D9DAAD /* OPML.swift */,
4453A66D1DE60D6200383E40 /* PowerManager.swift */,
4441F59324308B77001AEC1E /* ServiceURLs.swift */,
);
Expand Down Expand Up @@ -566,7 +566,7 @@
44AB5B821DD5532C00AE6EB6 /* BundleInfo.swift in Sources */,
4441F59624308C8D001AEC1E /* Scripts.swift in Sources */,
44B363461DC99D1900128259 /* FeedChecker.swift in Sources */,
44C8198B220D73DC00D9DAAD /* OPMLParser.swift in Sources */,
44C8198B220D73DC00D9DAAD /* OPML.swift in Sources */,
A7FA8FE226072836002FC21E /* AddFeedView.swift in Sources */,
44BA5D121DAEC0C400A9CE9A /* WindowShakeAnimation.swift in Sources */,
447E0F731DDAB24C001048AB /* FileUtils.swift in Sources */,
Expand Down Expand Up @@ -725,7 +725,7 @@
CODE_SIGN_IDENTITY = "";
COMBINE_HIDPI_IMAGES = YES;
COPY_PHASE_STRIP = NO;
CURRENT_PROJECT_VERSION = 768;
CURRENT_PROJECT_VERSION = 780;
ENABLE_STRICT_OBJC_MSGSEND = YES;
ENABLE_TESTABILITY = YES;
FRAMEWORK_SEARCH_PATHS = (
Expand Down Expand Up @@ -790,7 +790,7 @@
CODE_SIGN_IDENTITY = "Mac Developer";
COMBINE_HIDPI_IMAGES = YES;
COPY_PHASE_STRIP = NO;
CURRENT_PROJECT_VERSION = 768;
CURRENT_PROJECT_VERSION = 780;
DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
ENABLE_NS_ASSERTIONS = NO;
ENABLE_STRICT_OBJC_MSGSEND = YES;
Expand Down
45 changes: 35 additions & 10 deletions Resources/App/Base.lproj/UI.xib
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,7 @@
<outlet property="addFeedSheetController" destination="cFd-Mt-3tR" id="Pw2-C9-f2M"/>
<outlet property="automaticallyCheckForUpdatesCheckbox" destination="806" id="HZV-bf-GPz"/>
<outlet property="downloadScriptCheckbox" destination="GJR-jo-BJL" id="yz5-UX-AF1"/>
<outlet property="exportToOPMLButton" destination="FK7-xt-fUo" id="shm-Gx-9ZA"/>
<outlet property="feedContentsController" destination="q07-cJ-rHw" id="exC-H7-tIX"/>
<outlet property="feedsTableView" destination="ZyI-YF-l8d" id="SiE-no-MEx"/>
<outlet property="removeFeedButton" destination="AnU-XT-Qai" id="Dk3-s6-LhI"/>
Expand Down Expand Up @@ -312,16 +313,39 @@
<constraint firstAttribute="height" constant="4" id="qAj-tV-K5o"/>
</constraints>
</customView>
<button verticalHuggingPriority="750" translatesAutoresizingMaskIntoConstraints="NO" id="SIo-Vm-tN9">
<rect key="frame" x="-7" y="57" width="184" height="32"/>
<buttonCell key="cell" type="push" title="Import From OPML File…" bezelStyle="rounded" alignment="center" borderStyle="border" imageScaling="proportionallyDown" inset="2" id="6pY-YU-cuQ">
<behavior key="behavior" pushIn="YES" lightByBackground="YES" lightByGray="YES"/>
<font key="font" metaFont="system"/>
</buttonCell>
<connections>
<action selector="importFromOPMLFile:" target="vyd-92-YXh" id="JrH-bO-49c"/>
</connections>
</button>
<stackView distribution="fill" orientation="horizontal" alignment="top" horizontalStackHuggingPriority="249.99998474121094" verticalStackHuggingPriority="249.99998474121094" detachesHiddenViews="YES" translatesAutoresizingMaskIntoConstraints="NO" id="3TO-Gy-mwS">
<rect key="frame" x="0.0" y="64" width="330" height="20"/>
<subviews>
<button verticalHuggingPriority="750" translatesAutoresizingMaskIntoConstraints="NO" id="SIo-Vm-tN9">
<rect key="frame" x="-7" y="-7" width="184" height="32"/>
<buttonCell key="cell" type="push" title="Import From OPML File…" bezelStyle="rounded" alignment="center" borderStyle="border" imageScaling="proportionallyDown" inset="2" id="6pY-YU-cuQ">
<behavior key="behavior" pushIn="YES" lightByBackground="YES" lightByGray="YES"/>
<font key="font" metaFont="system"/>
</buttonCell>
<connections>
<action selector="importFromOPMLFile:" target="vyd-92-YXh" id="JrH-bO-49c"/>
</connections>
</button>
<button verticalHuggingPriority="750" translatesAutoresizingMaskIntoConstraints="NO" id="FK7-xt-fUo">
<rect key="frame" x="171" y="-7" width="166" height="32"/>
<buttonCell key="cell" type="push" title="Export to OPML File…" bezelStyle="rounded" alignment="center" borderStyle="border" imageScaling="proportionallyDown" inset="2" id="uCv-tU-gSa">
<behavior key="behavior" pushIn="YES" lightByBackground="YES" lightByGray="YES"/>
<font key="font" metaFont="system"/>
</buttonCell>
<connections>
<action selector="exportToOPMLFile:" target="vyd-92-YXh" id="awe-YL-ieR"/>
</connections>
</button>
</subviews>
<visibilityPriorities>
<integer value="1000"/>
<integer value="1000"/>
</visibilityPriorities>
<customSpacing>
<real value="3.4028234663852886e+38"/>
<real value="3.4028234663852886e+38"/>
</customSpacing>
</stackView>
<customView translatesAutoresizingMaskIntoConstraints="NO" id="dvj-ac-7iB" userLabel="Spacer">
<rect key="frame" x="0.0" y="52" width="163" height="4"/>
<constraints>
Expand Down Expand Up @@ -651,6 +675,7 @@ DQ
<outlet property="andLabel" destination="636" id="0mE-a5-Orr"/>
<outlet property="checkForUpdatesCheckbox" destination="806" id="a9t-HZ-aSN"/>
<outlet property="downloadScriptCheckbox" destination="GJR-jo-BJL" id="z5g-lf-uhF"/>
<outlet property="exportToOPMLButton" destination="FK7-xt-fUo" id="XiN-RD-Af8"/>
<outlet property="helpButton" destination="555" id="7G6-RV-1PQ"/>
<outlet property="importFromOPMLButton" destination="SIo-Vm-tN9" id="lme-NT-tYN"/>
<outlet property="onlyCheckBetweenCheckbox" destination="638" id="Pvk-2b-YCo"/>
Expand Down
Binary file modified Resources/App/ca.lproj/Localizable.strings
Binary file not shown.
Binary file modified Resources/App/de.lproj/Localizable.strings
Binary file not shown.
Binary file modified Resources/App/en.lproj/Localizable.strings
Binary file not shown.
Binary file modified Resources/App/es.lproj/Localizable.strings
Binary file not shown.
Binary file modified Resources/App/fi.lproj/Localizable.strings
Binary file not shown.
Binary file modified Resources/App/fr.lproj/Localizable.strings
Binary file not shown.
Binary file modified Resources/App/it.lproj/Localizable.strings
Binary file not shown.
Binary file modified Resources/App/nl.lproj/Localizable.strings
Binary file not shown.
Binary file modified Resources/App/pl.lproj/Localizable.strings
Binary file not shown.
Binary file modified Resources/App/zh-Hans.lproj/Localizable.strings
Binary file not shown.
Binary file modified Resources/App/zh-Hant.lproj/Localizable.strings
Binary file not shown.
108 changes: 108 additions & 0 deletions Sources/App/OPML.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,108 @@
import Foundation


private extension XMLNode {
subscript(xPath: String) -> String? {
return (try? nodes(forXPath: xPath))?.last?.stringValue
}
}


private extension Feed {
/// Try to initialize an episode with the data found in an RSS "item" or Atom "entry" element
init?(itemNode: XMLNode) {
// Get the item URL
guard let urlString = itemNode["@xmlUrl"] else {
NSLog("Missing feed URL")
return nil
}

guard let url = URL(string: urlString) else {
NSLog("Invalid feed URL: \(urlString)")
return nil
}

// Get the name
guard let name = itemNode["@title"] ?? itemNode["@text"], name != "" else {
NSLog("Missing or empty feed title")
return nil
}

self.name = name
self.url = url
}

var outlineElement: XMLElement {
let outlineElement = XMLElement(name: "outline")

let titleAttribute = XMLNode(kind: .attribute)
titleAttribute.name = "title"
titleAttribute.stringValue = name

let textAttribute = XMLNode(kind: .attribute)
textAttribute.name = "text"
textAttribute.stringValue = name

let typeAttribute = XMLNode(kind: .attribute)
typeAttribute.name = "type"
typeAttribute.stringValue = "rss"

let xmlURLAttribute = XMLNode(kind: .attribute)
xmlURLAttribute.name = "xmlUrl"
xmlURLAttribute.stringValue = url.absoluteString

outlineElement.addAttribute(titleAttribute)
outlineElement.addAttribute(textAttribute)
outlineElement.addAttribute(typeAttribute)
outlineElement.addAttribute(xmlURLAttribute)

return outlineElement
}
}

// Parses feeds out of an OPML file.
struct OPMLParser {
func parse(opml: Data) throws -> [Feed] {
NSLog("Parsing OPML file")

// Parse xml
let xml = try XMLDocument(data: opml)

// Extract feed item nodes
let outlineNodes = try xml.nodes(forXPath: "//opml/body//outline")

// Extract episodes from NSXMLNodes
let feeds = outlineNodes.compactMap(Feed.init(itemNode:))

NSLog("Parsed \(feeds.count) feeds")

return feeds
}
}

// Creates an OPML file from feeds.
struct OPMLSerializer {
func serialize(feeds: [Feed]) throws -> Data {
NSLog("Serializing OPML file")

let xml = XMLDocument()
xml.characterEncoding = "utf-8"

let opmlElement = XMLElement(name: "opml")

let headElement = XMLElement(name: "head")
let bodyElement = XMLElement(name: "body")

let outlineElements = feeds.map(\.outlineElement)
for outlineElement in outlineElements {
bodyElement.addChild(outlineElement)
}

opmlElement.addChild(headElement)
opmlElement.addChild(bodyElement)

xml.addChild(opmlElement)

return xml.xmlData(options: [.nodeCompactEmptyElement, .nodePrettyPrint])
}
}
54 changes: 0 additions & 54 deletions Sources/App/OPMLParser.swift

This file was deleted.

31 changes: 30 additions & 1 deletion Sources/App/PreferencesController.swift
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ private extension NSUserInterfaceItemIdentifier {
class PreferencesController: NSWindowController {
@IBOutlet private weak var feedsTableView: NSTableView!
@IBOutlet private weak var removeFeedButton: NSButton!
@IBOutlet private weak var exportToOPMLButton: NSButton!
@IBOutlet private weak var torrentsSavePathWarningImageView: NSImageView!
@IBOutlet private weak var automaticallyCheckForUpdatesCheckbox: NSButton!
@IBOutlet private weak var addFeedSheetController: AddFeedController!
Expand Down Expand Up @@ -122,6 +123,8 @@ class PreferencesController: NSWindowController {
reloadFeedList()

refreshRemoveButton()

exportToOPMLButton.isEnabled = Defaults.shared.hasValidFeeds
}

private func reloadFeedList() {
Expand Down Expand Up @@ -173,14 +176,40 @@ extension PreferencesController {

do {
let data = try Data(contentsOf: url)
let parsedFeeds = try OPMLParser.parse(opml: data)
let parsedFeeds = try OPMLParser().parse(opml: data)
Defaults.shared.feeds += parsedFeeds
} catch {
NSLog("Couldn't parse OPML: \(error)")
}
}
}

@IBAction private func exportToOPMLFile(_: Any?) {
guard let window = self.window else { return }

let data: Data
do {
data = try OPMLSerializer().serialize(feeds: Defaults.shared.feeds)
} catch {
NSLog("Couldn't serialize OPML: \(error)")
return
}

let savePanel = NSSavePanel()
savePanel.nameFieldStringValue = "Catch.xml"

savePanel.beginSheetModal(for: window) { response in
guard response == .OK, let url = savePanel.url else { return }

do {
try data.write(to: url)
}
catch {
NSLog("Couldn't write OPML: \(error)")
}
}
}

@IBAction private func savePreferences(_: Any?) {
Defaults.shared.save()

Expand Down
2 changes: 2 additions & 0 deletions Sources/App/PreferencesView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ class PreferencesView: NSView {
@IBOutlet weak var acceptButton: NSButton!

@IBOutlet weak var importFromOPMLButton: NSButton!
@IBOutlet weak var exportToOPMLButton: NSButton!
@IBOutlet weak var onlyCheckBetweenCheckbox: NSButton!
@IBOutlet weak var andLabel: NSTextField!

Expand All @@ -29,6 +30,7 @@ class PreferencesView: NSView {
acceptButton.title = NSLocalizedString("OK", comment: "")

importFromOPMLButton.title = NSLocalizedString("Import From OPML File…", comment: "")
exportToOPMLButton.title = NSLocalizedString("Export to OPML File…", comment: "")
onlyCheckBetweenCheckbox.title = NSLocalizedString("Only check feed between", comment: "")
andLabel.stringValue = NSLocalizedString("and", comment: "")

Expand Down

0 comments on commit 863759d

Please sign in to comment.