forked from akai/readest
912e97cb82
* feat(send): iOS share-extension picker + App Group queue + reliable host launch
Rework the iOS Share Extension to a Zotero-style sheet: URL preview row +
library group picker + "Save & Open". Queues each save into the shared
App Group container and best-effort launches the host app via the
Chrome-style responder-chain trick (IMP cast against
`openURL:options:completionHandler:`). The host plugin drains the queue
on `applicationDidBecomeActive`, so if the launch ever fails the article
still ingests next time Readest is opened.
A `WKScriptMessageHandler` named `readestShareBridge` lets the JS hook
post `{type:'ready'}` on mount, fixing the cold-start race when the
extension wakes the app before the React side has loaded.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* feat(send): cover generator, favicon fetcher, share-extension polish, locale sync
Builds on the previous commit (iOS share-extension picker + App Group queue +
reliable host launch) with three further additions to the send-to-Readest
pipeline:
* **Cover generator** — `services/send/conversion/coverGenerator.ts` renders a
deterministic cover image into clipped EPUBs (favicon + page title + host).
Hooked into `buildEpub.ts` so every clip path (desktop, mobile, browser
extension) produces the same cover for the same source.
* **Favicon fetcher** — `services/send/conversion/faviconFetcher.ts` resolves
the best-available site icon (Open Graph image → apple-touch-icon →
/favicon.ico), with size + format normalization. Feeds the cover generator.
* **Unified page conversion** — `convertToEpub({kind:'page', ...})` replaces
the older `convertPageToEpub(html, url)` so the share extension, /send page,
and browser extension share one entry point. Test: `send-convert-page-unified`.
* **Share-extension project.yml comment** — clarifies why the ShareExtension
target carries no `.lproj` files (system bar buttons + JS-supplied "Default"
label, no per-locale strings to wire).
* **Locale sync** — 33 translation.json files updated with new "Default",
"Saving article…", and cover-generator strings extracted by i18next-scanner.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
75 lines
2.4 KiB
Swift
75 lines
2.4 KiB
Swift
// Mirror of `src-tauri/gen/apple/ShareExtension/AppGroupBridge.swift`. The
|
|
// two targets cannot share Swift source via the Xcode project layout we
|
|
// use (xcodegen `sources:` blocks scope strictly to each target's
|
|
// directory), so the schema is intentionally duplicated. Keep both files
|
|
// byte-aligned when changing field names, keys, or encodings.
|
|
|
|
import Foundation
|
|
|
|
enum AppGroupBridge {
|
|
static let suiteName = "group.com.bilingify.readest"
|
|
static let groupsKey = "shareExtensionGroups"
|
|
static let defaultGroupNameKey = "shareExtensionDefaultGroupName"
|
|
static let pendingSavesKey = "shareExtensionPendingSaves"
|
|
|
|
static var defaults: UserDefaults? {
|
|
UserDefaults(suiteName: suiteName)
|
|
}
|
|
|
|
struct LibraryGroup: Codable, Equatable {
|
|
let id: String
|
|
let name: String
|
|
}
|
|
|
|
struct PendingSave: Codable, Equatable {
|
|
let url: String
|
|
let groupId: String?
|
|
let groupName: String?
|
|
let addedAt: String
|
|
}
|
|
|
|
static func readGroups() -> [LibraryGroup] {
|
|
guard let data = defaults?.data(forKey: groupsKey) else { return [] }
|
|
return (try? JSONDecoder().decode([LibraryGroup].self, from: data)) ?? []
|
|
}
|
|
|
|
static func writeGroups(_ groups: [LibraryGroup]) {
|
|
guard let data = try? JSONEncoder().encode(groups) else { return }
|
|
defaults?.set(data, forKey: groupsKey)
|
|
}
|
|
|
|
/// JS side passes the user-locale-translated "Default" label here so the
|
|
/// Share Extension's no-group row reads in the user's language without
|
|
/// the extension needing its own per-locale strings file.
|
|
static func readDefaultGroupName() -> String? {
|
|
defaults?.string(forKey: defaultGroupNameKey)
|
|
}
|
|
|
|
static func writeDefaultGroupName(_ name: String) {
|
|
defaults?.set(name, forKey: defaultGroupNameKey)
|
|
}
|
|
|
|
static func readPendingSaves() -> [PendingSave] {
|
|
guard let data = defaults?.data(forKey: pendingSavesKey) else { return [] }
|
|
return (try? JSONDecoder().decode([PendingSave].self, from: data)) ?? []
|
|
}
|
|
|
|
static func appendPendingSave(_ save: PendingSave) {
|
|
var saves = readPendingSaves()
|
|
saves.append(save)
|
|
if let data = try? JSONEncoder().encode(saves) {
|
|
defaults?.set(data, forKey: pendingSavesKey)
|
|
}
|
|
}
|
|
|
|
static func clearPendingSaves() {
|
|
defaults?.removeObject(forKey: pendingSavesKey)
|
|
}
|
|
|
|
static func nowIso8601() -> String {
|
|
let formatter = ISO8601DateFormatter()
|
|
formatter.formatOptions = [.withInternetDateTime, .withFractionalSeconds]
|
|
return formatter.string(from: Date())
|
|
}
|
|
}
|