154 lines
7.4 KiB
Swift
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() }
|
|
}
|
|
}
|