Skip to content

Commit 975285e

Browse files
committed
feat(mobile): implement native image preview with QuickLook on iOS
- Add ImagePreview helper class for displaying images using QuickLook - Extend HelperModule with previewImage function to load and display images - Update WebView bridge to support image preview from web content - Implement MarkdownImage component with base64 image conversion - Add cross-platform quickLookImage method for native image preview Signed-off-by: Innei <tukon479@gmail.com>
1 parent 64db32e commit 975285e

File tree

12 files changed

+162
-7
lines changed

12 files changed

+162
-7
lines changed
Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,66 @@
1+
//
2+
// Helper+Image.swift
3+
// FollowNative
4+
//
5+
// Created by Innei on 2025/2/7.
6+
//
7+
8+
import ObjectiveC
9+
import QuickLook
10+
import UIKit
11+
12+
class PreviewControllerClass: NSObject, QLPreviewControllerDataSource, QLPreviewControllerDelegate {
13+
private var imageDataArray: [Data] = []
14+
15+
init(images: [Data]) {
16+
self.imageDataArray = images
17+
super.init()
18+
}
19+
20+
func numberOfPreviewItems(in controller: QLPreviewController) -> Int {
21+
return imageDataArray.count
22+
}
23+
24+
func previewController(_ controller: QLPreviewController, previewItemAt index: Int)
25+
-> QLPreviewItem
26+
{
27+
let tempURL = FileManager.default.temporaryDirectory.appendingPathComponent(
28+
"preview_image_\(index).jpg")
29+
try? imageDataArray[index].write(to: tempURL)
30+
return tempURL as QLPreviewItem
31+
}
32+
33+
private func cleanupTempFiles() {
34+
for index in 0..<imageDataArray.count {
35+
let tempURL = FileManager.default.temporaryDirectory.appendingPathComponent(
36+
"preview_image_\(index).jpg")
37+
try? FileManager.default.removeItem(at: tempURL)
38+
}
39+
}
40+
41+
func previewControllerDidDismiss(_ controller: QLPreviewController) {
42+
cleanupTempFiles()
43+
}
44+
45+
}
46+
47+
48+
class ImagePreview: NSObject {
49+
public static func quickLookImage(_ images: [Data]) {
50+
guard let rootViewController = UIApplication.shared.keyWindow?.rootViewController else {
51+
return
52+
}
53+
54+
if images.count == 0 {
55+
print("no preview data")
56+
return
57+
}
58+
let previewController = QLPreviewController()
59+
let previewControllerClass = PreviewControllerClass(images: images)
60+
previewController.view.tintColor = Utils.accentColor
61+
previewController.dataSource = previewControllerClass
62+
previewController.delegate = previewControllerClass
63+
64+
rootViewController.present(previewController, animated: true)
65+
}
66+
}

apps/mobile/native/ios/Helper/HelperModule.swift

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,18 @@ public class HelperModule: Module {
1818
guard let rootVC = UIApplication.shared.windows.first?.rootViewController else { return }
1919
WebViewManager.presentModalWebView(url: url, from: rootVC)
2020
}
21+
}
2122

23+
Function("previewImage") { (images: [String]) in
24+
let imagesData: [Data] =
25+
images.compactMap { image in
26+
let url = URL(string: image)
27+
let data = try? Data(contentsOf: url!)
28+
return data
29+
}
30+
DispatchQueue.main.async {
31+
ImagePreview.quickLookImage(imagesData)
32+
}
2233
}
2334
}
2435
}

apps/mobile/native/ios/SharedWebView/Injected/at_start.js

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@
1111
window.webkit.messageHandlers.message.postMessage?.(JSON.stringify(data))
1212
}
1313

14-
Object.assign(window.webkit, {
14+
window.bridge = {
1515
measure: () => {
1616
send({
1717
type: "measure",
@@ -23,5 +23,11 @@
2323
payload: height,
2424
})
2525
},
26-
})
26+
previewImage: (base64) => {
27+
send({
28+
type: "previewImage",
29+
payload: base64,
30+
})
31+
},
32+
}
2733
})()

apps/mobile/native/ios/SharedWebView/SharedWebView+BridgeData.swift

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,3 +18,8 @@ struct SetContentHeightPayload: Hashable, Codable, BasePayload {
1818
struct BridgeDataBasePayload: Hashable, Codable {
1919
var type: String
2020
}
21+
22+
struct PreviewImagePayload: Hashable, Codable, BasePayload {
23+
var type: String
24+
var payload: [String]
25+
}

apps/mobile/native/ios/SharedWebView/WebViewManager.swift

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -212,6 +212,16 @@ private class WebViewDelegate: NSObject, WKNavigationDelegate, WKScriptMessageHa
212212
case "measure":
213213
self.measureWebView(SharedWebViewModule.sharedWebView!)
214214

215+
case "previewImage":
216+
let data = try? JSONDecoder().decode(
217+
PreviewImagePayload.self, from: decode)
218+
219+
guard let data = data else { return }
220+
DispatchQueue.main.async {
221+
ImagePreview.quickLookImage(
222+
data.payload.compactMap { Data(base64Encoded: $0) })
223+
}
224+
215225
default:
216226
break
217227
}

apps/mobile/src/lib/native/index.ios.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,12 @@ import { requireNativeModule } from "expo"
22

33
interface NativeModule {
44
openLink: (url: string) => void
5+
previewImage: (images: Uint8Array[]) => void
56
}
67
const nativeModule = requireNativeModule("Helper") as NativeModule
78
export const openLink = (url: string) => {
89
nativeModule.openLink(url)
910
}
11+
export const quickLookImage = (images: string[]) => {
12+
nativeModule.previewImage(images)
13+
}

apps/mobile/src/lib/native/index.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,3 +3,5 @@ import { openURL } from "expo-linking"
33
export const openLink = (url: string) => {
44
openURL(url)
55
}
6+
7+
export const quickLookImage = (_images: string[]) => {}

apps/mobile/src/screens/(headless)/debug.tsx

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ import { useSafeAreaInsets } from "react-native-safe-area-context"
2121
import { getDbPath } from "@/src/database"
2222
import { cookieKey, getCookie, sessionTokenKey, signOut } from "@/src/lib/auth"
2323
import { loading } from "@/src/lib/loading"
24+
import { quickLookImage } from "@/src/lib/native"
2425
import { toast } from "@/src/lib/toast"
2526

2627
interface MenuSection {
@@ -107,6 +108,16 @@ export default function DebugPanel() {
107108
})
108109
},
109110
},
111+
{
112+
title: "Quick Look Image",
113+
onPress: () => {
114+
quickLookImage([
115+
"https://picsum.photos/200/300",
116+
"https://picsum.photos/200/300?grayscale",
117+
"https://picsum.photos/200/300?blur",
118+
])
119+
},
120+
},
110121
],
111122
},
112123

apps/mobile/web-app/html-renderer/global.d.ts

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -5,12 +5,11 @@ import "../../../../packages/types/global"
55
interface Bridge {
66
measure: () => void
77
setContentHeight: (height: number) => void
8+
previewImage: (base64: string[]) => void
89
}
910

1011
declare global {
11-
interface Window {
12-
webkit: Bridge
13-
}
12+
export const bridge: Bridge
1413
}
1514

1615
export {}

apps/mobile/web-app/html-renderer/src/App.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -23,11 +23,11 @@ const entryAtom = atom<EntryModel | null>(null)
2323
Object.assign(window, {
2424
setEntry(entry: EntryModel) {
2525
store.set(entryAtom, entry)
26-
window.webkit.measure()
26+
bridge.measure()
2727
},
2828
reset() {
2929
store.set(entryAtom, null)
30-
window.webkit.measure()
30+
bridge.measure()
3131
},
3232
})
3333

0 commit comments

Comments
 (0)