Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions Mac/Preferences/Accounts/AccountsDetailViewController.swift
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,13 @@ final class AccountsDetailViewController: NSViewController {
accountsFeedbinWindowController.account = account
accountsFeedbinWindowController.runSheetOnWindow(window)

case .miniflux:
let accountsMinifluxWindowController = AccountsMinifluxWindowController()
accountsWindowController = accountsMinifluxWindowController
accountsMinifluxWindowController.accountType = account.type
accountsMinifluxWindowController.account = account
accountsMinifluxWindowController.runSheetOnWindow(window)

case .inoreader, .bazQux, .theOldReader, .freshRSS:
let accountsReaderAPIWindowController = AccountsReaderAPIWindowController()
accountsWindowController = accountsReaderAPIWindowController
Expand Down
235 changes: 235 additions & 0 deletions Mac/Preferences/Accounts/AccountsMiniflux.xib

Large diffs are not rendered by default.

149 changes: 149 additions & 0 deletions Mac/Preferences/Accounts/AccountsMinifluxWindowController.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,149 @@
//
// AccountsMinifluxWindowController.swift
// NetNewsWire
//
// Created by Ingmar Stein on 6/18/26.
// Copyright © 2026 Ranchero Software. All rights reserved.
//

import AppKit
import Account
import RSWeb
import Secrets

final class AccountsMinifluxWindowController: NSWindowController {

@IBOutlet var titleImageView: NSImageView!
@IBOutlet var titleLabel: NSTextField!

@IBOutlet var gridView: NSGridView!
@IBOutlet var progressIndicator: NSProgressIndicator!
@IBOutlet var apiURLTextField: NSTextField!
@IBOutlet var apiKeyTextField: NSSecureTextField!
@IBOutlet var createAccountButton: NSButton!
@IBOutlet var errorMessageLabel: NSTextField!
@IBOutlet var actionButton: NSButton!
@IBOutlet var noAccountTextField: NSTextField!

var account: Account?
var accountType: AccountType?

private weak var hostWindow: NSWindow?

convenience init() {
self.init(windowNibName: NSNib.Name("AccountsMiniflux"))
}

override func windowDidLoad() {
titleImageView.image = Assets.Images.accountMiniflux
titleLabel.stringValue = NSLocalizedString("Sign in to your Miniflux instance.", comment: "Miniflux")
noAccountTextField.stringValue = NSLocalizedString("Don't have a Miniflux instance?", comment: "No Miniflux")
createAccountButton.title = NSLocalizedString("Find out more", comment: "No Miniflux Button")
apiURLTextField.placeholderString = NSLocalizedString("https://miniflux.example.com", comment: "Miniflux API URL Helper")

if let account = account, let credentials = try? account.retrieveCredentials(type: .minifluxAPIKey) {
apiURLTextField.stringValue = account.endpointURL?.absoluteString ?? ""
apiKeyTextField.stringValue = credentials.secret
actionButton.title = NSLocalizedString("Update", comment: "Update")
} else {
actionButton.title = NSLocalizedString("Create", comment: "Create")
}

apiURLTextField.becomeFirstResponder()
}

// MARK: API

func runSheetOnWindow(_ hostWindow: NSWindow, completion: ((NSApplication.ModalResponse) -> Void)? = nil) {
guard let window else {
return
}

self.hostWindow = hostWindow
hostWindow.beginSheet(window, completionHandler: completion)
}

// MARK: Actions

@IBAction func cancel(_ sender: Any) {
hostWindow!.endSheet(window!, returnCode: NSApplication.ModalResponse.cancel)
}

@IBAction func action(_ sender: Any) {
self.errorMessageLabel.stringValue = ""

guard !apiURLTextField.stringValue.isEmpty && !apiKeyTextField.stringValue.isEmpty else {
self.errorMessageLabel.stringValue = NSLocalizedString("API URL and API Key are required.", comment: "Credentials Error")
return
}

let accountType: AccountType = .miniflux

guard let inputURL = URL(string: apiURLTextField.stringValue.trimmingWhitespace) else {
self.errorMessageLabel.stringValue = NSLocalizedString("Invalid API URL.", comment: "Invalid API URL")
return
}
let apiURL = inputURL

guard account != nil || !AccountManager.shared.duplicateServiceAccount(type: accountType, endpointURL: apiURL) else {
self.errorMessageLabel.stringValue = NSLocalizedString("There is already a Miniflux account with that URL created.", comment: "Duplicate Error")
return
}

Task { @MainActor in
actionButton.isEnabled = false
progressIndicator.isHidden = false
progressIndicator.startAnimation(self)

@MainActor func stopAnimation() {
actionButton.isEnabled = true
progressIndicator.isHidden = true
progressIndicator.stopAnimation(self)
}

let trimmedAPIKey = apiKeyTextField.stringValue.trimmingWhitespace
let credentials = Credentials(type: .minifluxAPIKey, username: trimmedAPIKey, secret: trimmedAPIKey)
do {
let validatedCredentials = try await Account.validateCredentials(type: accountType, credentials: credentials, endpoint: apiURL)
stopAnimation()

guard let validatedCredentials else {
errorMessageLabel.stringValue = NSLocalizedString("Invalid API key.", comment: "Credentials Error")
return
}

if account == nil {
account = AccountManager.shared.createAccount(type: accountType)
}

do {
account?.endpointURL = apiURL

try account?.storeCredentials(validatedCredentials)

do {
try await account?.refreshAll()
} catch {
NSApplication.shared.presentError(error)
}

hostWindow?.endSheet(window!, returnCode: NSApplication.ModalResponse.OK)
} catch {
errorMessageLabel.stringValue = NSLocalizedString("Keychain error while storing credentials.", comment: "Credentials Error")
}

} catch {
stopAnimation()
if case AccountError.urlNotFound = error {
errorMessageLabel.stringValue = NSLocalizedString("The API URL couldn't be found. Please check the URL.", comment: "API URL not found")
} else {
errorMessageLabel.stringValue = error.localizedDescription
}
}
}
}

@IBAction func createAccountWithProvider(_ sender: Any) {
NSWorkspace.shared.open(URL(string: "https://miniflux.app")!)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -185,6 +185,11 @@ extension AccountsPreferencesViewController: AccountsPreferencesAddAccountDelega
accountsReaderAPIWindowController.accountType = accountType
accountsReaderAPIWindowController.runSheetOnWindow(window)

case .miniflux:
let accountsMinifluxWindowController = AccountsMinifluxWindowController()
addAccountWindowController = accountsMinifluxWindowController
accountsMinifluxWindowController.runSheetOnWindow(window)

case .feedly:
let addAccount = OAuthAccountAuthorizationOperation(accountType: .feedly)
addAccount.delegate = self
Expand Down
2 changes: 1 addition & 1 deletion Mac/Preferences/Accounts/AddAccountsView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -60,7 +60,7 @@ enum AddAccountSections: Int, CaseIterable {
return [.bazQux, .feedbin, .feedly, .inoreader, .newsBlur, .theOldReader]
}
case .selfhosted:
return [.freshRSS]
return [.freshRSS, .miniflux]
case .allOrdered:
return AddAccountSections.local.sectionContent +
AddAccountSections.icloud.sectionContent +
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
{
"images" : [
{
"filename" : "accountMiniflux.pdf",
"idiom" : "universal"
}
],
"info" : {
"author" : "xcode",
"version" : 1
},
"properties" : {
"preserves-vector-representation" : true,
"template-rendering-intent" : "original"
}
}
Binary file not shown.
2 changes: 2 additions & 0 deletions Mac/Scripting/Account+Scriptability.swift
Original file line number Diff line number Diff line change
Expand Up @@ -230,6 +230,8 @@ import RSCore
osType = "Bzqx"
case .theOldReader:
osType = "Tord"
case .miniflux:
osType = "Mnfl"
}
return osType.fourCharCode
}
Expand Down
7 changes: 7 additions & 0 deletions Modules/Account/Sources/Account/Account.swift
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,7 @@ nonisolated public enum AccountType: Int, Codable, Sendable {
case inoreader = 21
case bazQux = 22
case theOldReader = 23
case miniflux = 24

public var isDeveloperRestricted: Bool {
return self == .cloudKit || self == .feedbin || self == .feedly || self == .inoreader
Expand All @@ -74,6 +75,8 @@ nonisolated public enum AccountType: Int, Codable, Sendable {
return NSLocalizedString("BazQux", comment: "Account name")
case .theOldReader:
return NSLocalizedString("The Old Reader", comment: "Account name")
case .miniflux:
return "Miniflux"
}
}
}
Expand Down Expand Up @@ -312,6 +315,8 @@ public enum FetchType {
self.delegate = ReaderAPIAccountDelegate(dataFolder: dataFolder, variant: .bazQux)
case .theOldReader:
self.delegate = ReaderAPIAccountDelegate(dataFolder: dataFolder, variant: .theOldReader)
case .miniflux:
self.delegate = MinifluxAccountDelegate(dataFolder: dataFolder)
}

self.accountID = accountID
Expand Down Expand Up @@ -410,6 +415,8 @@ public enum FetchType {
return try await NewsBlurAccountDelegate.validateCredentials(credentials: credentials, endpoint: endpoint)
case .freshRSS, .inoreader, .bazQux, .theOldReader:
return try await ReaderAPIAccountDelegate.validateCredentials(credentials: credentials, endpoint: endpoint)
case .miniflux:
return try await MinifluxAccountDelegate.validateCredentials(credentials: credentials, endpoint: endpoint)
default:
return nil
}
Expand Down
12 changes: 12 additions & 0 deletions Modules/Account/Sources/Account/AccountManager.swift
Original file line number Diff line number Diff line change
Expand Up @@ -233,6 +233,18 @@ import ActivityLog
return false
}

public func duplicateServiceAccount(type: AccountType, endpointURL: URL?) -> Bool {
guard type != .onMyMac else {
return false
}
for account in accounts {
if account.type == type && account.endpointURL == endpointURL {
return true
}
}
return false
}

public func existingAccount(accountID: String) -> Account? {
return accountsDictionary[accountID]
}
Expand Down
Loading