Files
edgeever/apps/ios/EdgeEver/Features/Workspace/NoteImageFontPickerView.swift
T

154 lines
7.4 KiB
Swift

import SwiftUI
import WebKit
@MainActor
enum NoteImageFontPickerOptions {
static func choices(_ preferences: PreferencesStore) -> [(id: String, label: String)] {
[
("auto", preferences.t("跟随主题", en: "Follow Theme", pl: "Zgodnie z motywem") + " (" + themeLabel(preferences) + ")"),
("kai", preferences.t("文楷手书", en: "Handwritten Kai", pl: "Pismo odręczne Kai")),
("serif", preferences.t("文艺衬线", en: "Serif", pl: "Szeryfowa")),
("sans", preferences.t("现代无衬线", en: "Sans", pl: "Bezszeryfowa")),
("mono", preferences.t("极客等宽", en: "Mono", pl: "Monospace")),
] + NoteImageFontCatalog.choices.map { ($0.id, preferences.t($0.zh, en: $0.en, pl: $0.en)) }
}
private static func themeLabel(_ preferences: PreferencesStore) -> String {
guard let labels = NoteImageFontCatalog.templateLabels[preferences.noteImageShare.theme] else { return "" }
return preferences.t(labels.zh, en: labels.en, ja: labels.ja, pl: labels.pl)
}
static func label(_ preferences: PreferencesStore) -> String {
if preferences.noteImageShare.fontStyle == "auto" { return themeLabel(preferences) }
return choices(preferences).first { $0.id == preferences.noteImageShare.fontStyle }?.label ?? ""
}
}
struct NoteImageFontPickerView: View {
@Environment(AppEnvironment.self) private var env
@Environment(\.dismiss) private var dismiss
@Environment(\.colorScheme) private var colorScheme
@State private var loading = true
@State private var failed = false
@State private var attempt = 0
var body: some View {
ZStack {
if let url = NoteImageFontPickerWebView.packagedHTMLURL() {
NoteImageFontPickerWebView(
url: url,
configuration: configuration,
onReady: { loading = false },
onFailure: { loading = false; failed = true },
onSelect: { id in
env.preferences.noteImageShare.fontStyle = id
dismiss()
}
)
.id(attempt)
.opacity(failed ? 0 : 1)
if loading { ProgressView() }
if failed {
ContentUnavailableView {
Label(env.preferences.t("加载失败", en: "Failed to load", pl: "Nie udało się wczytać"), systemImage: "exclamationmark.triangle")
} actions: {
Button(env.preferences.t("重试", en: "Retry", pl: "Spróbuj ponownie")) {
failed = false
loading = true
attempt += 1
}
}
}
} else {
ContentUnavailableView(env.preferences.t("加载失败", en: "Failed to load", pl: "Nie udało się wczytać"), systemImage: "exclamationmark.triangle")
}
}
.navigationTitle(env.preferences.t("字体风格", en: "Typography", pl: "Typografia"))
.navigationBarTitleDisplayMode(.inline)
}
private var configuration: [String: Any] {
let preferences = env.preferences
return [
"selected": preferences.noteImageShare.fontStyle,
"theme": preferences.noteImageShare.theme,
"dark": colorScheme == .dark,
"title": preferences.t("字体风格", en: "Typography", pl: "Typografia"),
"sample": preferences.t("记录此刻的灵感", en: "Capture a little inspiration", ja: "今この瞬間のひらめきを記録", pl: "Zapisz chwilę inspiracji"),
"latinSample": "Capture a little inspiration",
"loading": preferences.t("正在加载示例…", en: "Loading preview…", ja: "プレビューを読み込み中…", pl: "Ładowanie podglądu…"),
"unavailable": preferences.t("示例暂不可用,仍可选择", en: "Preview unavailable; you can still select this font", ja: "プレビューを表示できませんが、選択できます", pl: "Podgląd niedostępny; nadal możesz wybrać tę czcionkę"),
"options": NoteImageFontPickerOptions.choices(preferences).map { ["id": $0.id, "label": $0.label] },
]
}
}
struct NoteImageFontPickerWebView: UIViewRepresentable {
let url: URL
let configuration: [String: Any]
let onReady: () -> Void
let onFailure: () -> Void
let onSelect: (String) -> Void
static func packagedHTMLURL() -> URL? {
Bundle.main.url(forResource: "font-picker", withExtension: "html", subdirectory: "EditorBundle")
?? Bundle.main.url(forResource: "font-picker", withExtension: "html")
}
func makeCoordinator() -> Coordinator { Coordinator(self) }
func makeUIView(context: Context) -> WKWebView {
let config = WKWebViewConfiguration()
config.preferences.setValue(true, forKey: "allowFileAccessFromFileURLs")
config.mediaTypesRequiringUserActionForPlayback = .all
config.userContentController.add(context.coordinator, name: "fontPicker")
if let data = try? JSONSerialization.data(withJSONObject: configuration),
let json = String(data: data, encoding: .utf8) {
config.userContentController.addUserScript(WKUserScript(
source: "window.EdgeEverFontPickerConfiguration = \(json);",
injectionTime: .atDocumentStart,
forMainFrameOnly: true
))
}
let webView = WKWebView(frame: .zero, configuration: config)
webView.navigationDelegate = context.coordinator
webView.isOpaque = false
webView.backgroundColor = .clear
webView.scrollView.backgroundColor = .clear
webView.loadFileURL(url, allowingReadAccessTo: url.deletingLastPathComponent())
return webView
}
func updateUIView(_ uiView: WKWebView, context: Context) {
context.coordinator.parent = self
}
static func dismantleUIView(_ uiView: WKWebView, coordinator: Coordinator) {
uiView.stopLoading()
uiView.navigationDelegate = nil
uiView.configuration.userContentController.removeScriptMessageHandler(forName: "fontPicker")
}
final class Coordinator: NSObject, WKScriptMessageHandler, WKNavigationDelegate {
var parent: NoteImageFontPickerWebView
init(_ parent: NoteImageFontPickerWebView) { self.parent = parent }
func userContentController(_ userContentController: WKUserContentController, didReceive message: WKScriptMessage) {
guard message.frameInfo.isMainFrame, let body = message.body as? [String: String] else { return }
switch body["type"] {
case "ready": parent.onReady()
case "select":
guard let id = body["id"],
let choices = parent.configuration["options"] as? [[String: String]],
choices.contains(where: { $0["id"] == id }) else { return }
parent.onSelect(id)
default: break
}
}
func webView(_ webView: WKWebView, didFail navigation: WKNavigation!, withError error: Error) { parent.onFailure() }
func webView(_ webView: WKWebView, didFailProvisionalNavigation navigation: WKNavigation!, withError error: Error) { parent.onFailure() }
func webViewWebContentProcessDidTerminate(_ webView: WKWebView) { parent.onFailure() }
}
}