7da41a65ad
Add a resizable home-screen widget on iOS and Android showing recent
in-progress books with cover, reading progress, and tap-to-open.
- One responsive widget: Android resizable 1x1 to 4x3 (one book per
column, up to 3); iOS Small/Medium/Large families. Covers are cropped,
rounded, with a percent badge and a progress bar (baked into the bitmap
on Android, SwiftUI overlays on iOS).
- TTS controls (previous, play-pause, next) appear in 2+ row sizes when
TTS is active, wired to the existing media session. Reading progress
stays live during background TTS via a fraction computed from the baked
offline locations.
- Publishes a snapshot plus downsized cover thumbnails to the iOS App
Group and Android SharedPreferences through a new update_reading_widget
native-bridge command; refresh is debounced and driven by library and
progress changes, TTS, and app backgrounding.
- Tapping a cover opens readest://book/{hash}, switching the reader in
place when one is already open.
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
62 lines
2.1 KiB
Swift
62 lines
2.1 KiB
Swift
import Foundation
|
|
import UIKit
|
|
import WidgetKit
|
|
|
|
enum ReadingWidgetWriter {
|
|
static let suiteName = "group.com.bilingify.readest"
|
|
static let snapshotKey = "readingWidgetSnapshot"
|
|
static let thumbMaxPixels: CGFloat = 240
|
|
|
|
struct SnapshotBook: Codable {
|
|
let hash: String
|
|
let title: String
|
|
let author: String
|
|
let percent: Int
|
|
}
|
|
struct Snapshot: Codable {
|
|
let books: [SnapshotBook]
|
|
let sectionTitle: String
|
|
let emptyTitle: String
|
|
}
|
|
|
|
static var containerURL: URL? {
|
|
FileManager.default.containerURL(forSecurityApplicationGroupIdentifier: suiteName)
|
|
}
|
|
|
|
static func coversDir() -> URL? {
|
|
guard let base = containerURL?.appendingPathComponent("widget/covers", isDirectory: true)
|
|
else { return nil }
|
|
try? FileManager.default.createDirectory(at: base, withIntermediateDirectories: true)
|
|
return base
|
|
}
|
|
|
|
/// Downsample a cover file to <= thumbMaxPixels on the long edge and write
|
|
/// it as JPEG into the App Group container. Returns silently on failure.
|
|
static func writeThumbnail(hash: String, sourcePath: String) {
|
|
guard let dir = coversDir() else { return }
|
|
let dst = dir.appendingPathComponent("\(hash).jpg")
|
|
guard let image = UIImage(contentsOfFile: sourcePath) else {
|
|
try? FileManager.default.removeItem(at: dst)
|
|
return
|
|
}
|
|
let longEdge = max(image.size.width, image.size.height)
|
|
let scale = longEdge > thumbMaxPixels ? thumbMaxPixels / longEdge : 1
|
|
let target = CGSize(width: image.size.width * scale, height: image.size.height * scale)
|
|
let format = UIGraphicsImageRendererFormat()
|
|
format.scale = 1.0
|
|
let renderer = UIGraphicsImageRenderer(size: target, format: format)
|
|
let resized = renderer.image { _ in image.draw(in: CGRect(origin: .zero, size: target)) }
|
|
if let data = resized.jpegData(compressionQuality: 0.8) {
|
|
try? data.write(to: dst)
|
|
}
|
|
}
|
|
|
|
static func write(snapshot: Snapshot) {
|
|
guard let data = try? JSONEncoder().encode(snapshot) else { return }
|
|
UserDefaults(suiteName: suiteName)?.set(data, forKey: snapshotKey)
|
|
DispatchQueue.main.async {
|
|
WidgetCenter.shared.reloadAllTimelines()
|
|
}
|
|
}
|
|
}
|