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>
88 lines
3.0 KiB
Swift
88 lines
3.0 KiB
Swift
// Shared App Group container schema between the Readest Share Extension and
|
|
// the host app. Keep this file in sync with the mirror at
|
|
// `src-tauri/plugins/tauri-plugin-native-bridge/ios/Sources/AppGroupBridge.swift`.
|
|
// Two NSUserDefaults keys form the contract:
|
|
//
|
|
// shareExtensionGroups (host → extension)
|
|
// JSON array of { id: String, name: String }. Library groups the user
|
|
// can pick when saving. Refreshed by the host every time it foregrounds.
|
|
//
|
|
// shareExtensionDefaultGroupName (host → extension)
|
|
// User-locale-translated label for the "no group" row at the top of
|
|
// the picker. JS supplies `t('Default')` so the extension doesn't
|
|
// need its own per-locale strings file.
|
|
//
|
|
// shareExtensionPendingSaves (extension → host)
|
|
// JSON array of { url, groupId?, groupName?, addedAt } (ISO-8601 string).
|
|
// The extension appends here on every Save. The host drains + clears on
|
|
// foreground and feeds each entry through the same clip-and-import path
|
|
// the in-app "From Web URL" entry uses.
|
|
|
|
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)
|
|
}
|
|
|
|
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)
|
|
}
|
|
|
|
// ISO-8601 with fractional seconds — round-trips cleanly through
|
|
// JavaScript's Date constructor on the JS side.
|
|
static func nowIso8601() -> String {
|
|
let formatter = ISO8601DateFormatter()
|
|
formatter.formatOptions = [.withInternetDateTime, .withFractionalSeconds]
|
|
return formatter.string(from: Date())
|
|
}
|
|
}
|