Replace Flutter app with native SwiftUI rewrite (iOS 26)

Complete ground-up rewrite in SwiftUI targeting iOS 26. Drops the Flutter/Dart
codebase entirely in favour of a lean native app with no third-party dependencies.

Features shipped:
- Bookmark list with pagination, pull-to-refresh, swipe delete/archive
- Add bookmark form + iOS share extension (zero-tap save from any app)
- Tags tab — all tags sorted by count, tap to browse filtered bookmarks
- Native search tab (Tab role: .search) with instant client-side filtering
- AI enrichment via OpenRouter (google/gemini-2.0-flash-lite-001): auto-summary
  and tag generation, semantic search, smart collections
- Settings: linkding server config, OpenRouter API key
- App Groups for credential sharing between main app and share extension
- Swift 6 strict concurrency throughout (@Observable, @MainActor)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Krishna Kumar
2026-05-21 08:33:01 -05:00
parent cd85f9c67f
commit b7ef5c2215
306 changed files with 2337 additions and 17949 deletions

View File

@@ -0,0 +1,129 @@
import UIKit
import UniformTypeIdentifiers
@objc(ShareViewController)
class ShareViewController: UIViewController {
private let appGroupId = "group.com.magicive.marks"
private let configKey = "serverConfig"
override func viewDidLoad() {
super.viewDidLoad()
let blur = UIVisualEffectView(effect: UIBlurEffect(style: .systemMaterial))
blur.frame = view.bounds
blur.autoresizingMask = [.flexibleWidth, .flexibleHeight]
view.addSubview(blur)
extractURLAndSave()
}
private func extractURLAndSave() {
guard let item = extensionContext?.inputItems.first as? NSExtensionItem,
let attachments = item.attachments else {
showResult(success: false, message: "Nothing to save")
return
}
func load(typeId: String) -> Bool {
guard let attachment = attachments.first(where: { $0.hasItemConformingToTypeIdentifier(typeId) }) else {
return false
}
attachment.loadItem(forTypeIdentifier: typeId, options: nil) { [weak self] item, _ in
let urlString = typeId == UTType.url.identifier
? ((item as? URL)?.absoluteString ?? (item as? String))
: (item as? String)
DispatchQueue.main.async {
if let url = urlString { self?.save(url: url) }
else { self?.showResult(success: false, message: "No URL found") }
}
}
return true
}
if !load(typeId: UTType.url.identifier) && !load(typeId: UTType.plainText.identifier) {
showResult(success: false, message: "No URL found")
}
}
private func save(url: String) {
guard let groupDefaults = UserDefaults(suiteName: appGroupId),
let data = groupDefaults.data(forKey: configKey),
let config = try? JSONSerialization.jsonObject(with: data) as? [String: Any],
let host = config["host"] as? String,
let token = config["token"] as? String,
let useHttps = config["useHttps"] as? Bool else {
showResult(success: false, message: "Open Marks first")
return
}
var base = (useHttps ? "https" : "http") + "://" + host
if let port = config["port"] as? Int { base += ":\(port)" }
if let path = config["path"] as? String, !path.isEmpty { base += path }
guard let apiUrl = URL(string: "\(base)/api/bookmarks/") else {
showResult(success: false, message: "Bad server URL")
return
}
var req = URLRequest(url: apiUrl)
req.httpMethod = "POST"
req.setValue("Token \(token)", forHTTPHeaderField: "Authorization")
req.setValue("application/json", forHTTPHeaderField: "Content-Type")
req.httpBody = try? JSONSerialization.data(withJSONObject: [
"url": url, "is_archived": false, "unread": false, "shared": false
])
URLSession.shared.dataTask(with: req) { [weak self] _, response, error in
DispatchQueue.main.async {
let code = (response as? HTTPURLResponse)?.statusCode ?? 0
let ok = error == nil && (code == 200 || code == 201)
self?.showResult(success: ok, message: ok ? "Bookmarked" : "Failed to save")
}
}.resume()
}
private func showResult(success: Bool, message: String) {
let card = UIView()
card.backgroundColor = success ? .systemGreen : .systemRed
card.layer.cornerRadius = 16
card.layer.cornerCurve = .continuous
card.translatesAutoresizingMaskIntoConstraints = false
card.alpha = 0
let stack = UIStackView()
stack.axis = .horizontal
stack.spacing = 10
stack.alignment = .center
stack.translatesAutoresizingMaskIntoConstraints = false
let icon = UIImageView(image: UIImage(systemName: success ? "checkmark.circle.fill" : "xmark.circle.fill"))
icon.tintColor = .white
icon.preferredSymbolConfiguration = .init(pointSize: 22, weight: .semibold)
let label = UILabel()
label.text = message
label.textColor = .white
label.font = .systemFont(ofSize: 17, weight: .semibold)
stack.addArrangedSubview(icon)
stack.addArrangedSubview(label)
card.addSubview(stack)
view.addSubview(card)
NSLayoutConstraint.activate([
stack.centerXAnchor.constraint(equalTo: card.centerXAnchor),
stack.centerYAnchor.constraint(equalTo: card.centerYAnchor),
stack.leadingAnchor.constraint(greaterThanOrEqualTo: card.leadingAnchor, constant: 24),
card.centerXAnchor.constraint(equalTo: view.centerXAnchor),
card.centerYAnchor.constraint(equalTo: view.centerYAnchor),
card.heightAnchor.constraint(equalToConstant: 56),
card.widthAnchor.constraint(greaterThanOrEqualToConstant: 180),
])
UIView.animate(withDuration: 0.2) { card.alpha = 1 }
DispatchQueue.main.asyncAfter(deadline: .now() + (success ? 0.8 : 2.0)) { [weak self] in
UIView.animate(withDuration: 0.15, animations: { card.alpha = 0 }) { _ in
self?.extensionContext?.completeRequest(returningItems: [], completionHandler: nil)
}
}
}
}