Skip to content

Commit 7767f27

Browse files
committed
add open_status_image_viewer for macos
1 parent 7628d7a commit 7767f27

File tree

4 files changed

+115
-63
lines changed

4 files changed

+115
-63
lines changed

desktopApp/build.gradle.kts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -55,7 +55,7 @@ compose.desktop {
5555
val hasSigningProps = file.exists()
5656
packageBuildVersion = System.getenv("BUILD_NUMBER") ?: "21"
5757
bundleID = "dev.dimension.flare"
58-
minimumSystemVersion = "12.0"
58+
minimumSystemVersion = "14.0"
5959
appStore = hasSigningProps
6060

6161
jvmArgs(

desktopApp/src/main/kotlin/dev/dimension/flare/common/macos/MacosBridge.kt

Lines changed: 7 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,9 @@ import kotlinx.serialization.Serializable
88

99
internal object MacosBridge {
1010
private interface Bridge : Library {
11+
@Suppress("FunctionName")
12+
fun open_status_image_viewer(modelJson: String)
13+
1114
@Suppress("FunctionName")
1215
fun open_video_viewer(modelJson: String)
1316

@@ -122,18 +125,10 @@ internal object MacosBridge {
122125
index = selectedIndex,
123126
medias = medias,
124127
)
125-
126-
val selectedItem = data.getOrNull(selectedIndex)
127-
when (selectedItem) {
128-
is UiMedia.Audio -> Unit
129-
is UiMedia.Gif ->
130-
lib.open_img_viewer(selectedItem.url)
131-
is UiMedia.Image ->
132-
lib.open_img_viewer(selectedItem.url)
133-
is UiMedia.Video ->
134-
lib.open_video_viewer(selectedItem.url)
135-
null -> Unit
136-
}
128+
val modelJson =
129+
kotlinx.serialization.json.Json
130+
.encodeToString(OpenStatusImageModel.serializer(), model)
131+
lib.open_status_image_viewer(modelJson)
137132
}
138133

139134
@Serializable

desktopApp/src/main/swift/macosBridge.xcodeproj/project.pbxproj

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -268,7 +268,7 @@
268268
"$(PROJECT_DIR)/macosBridge/build/macosBridge.build/Release/macosBridge.build/Objects-normal/arm64/Binary",
269269
"$(PROJECT_DIR)/macosBridge/build/macosBridge.build/Release/macosBridge.build/Objects-normal/x86_64/Binary",
270270
);
271-
MACOSX_DEPLOYMENT_TARGET = 12.0;
271+
MACOSX_DEPLOYMENT_TARGET = 14.0;
272272
PRODUCT_NAME = "$(TARGET_NAME)";
273273
SKIP_INSTALL = YES;
274274
SWIFT_OPTIMIZATION_LEVEL = "-Onone";
@@ -293,7 +293,7 @@
293293
"$(PROJECT_DIR)/macosBridge/build/macosBridge.build/Release/macosBridge.build/Objects-normal/arm64/Binary",
294294
"$(PROJECT_DIR)/macosBridge/build/macosBridge.build/Release/macosBridge.build/Objects-normal/x86_64/Binary",
295295
);
296-
MACOSX_DEPLOYMENT_TARGET = 12.0;
296+
MACOSX_DEPLOYMENT_TARGET = 14.0;
297297
PRODUCT_NAME = "$(TARGET_NAME)";
298298
SKIP_INSTALL = YES;
299299
SWIFT_VERSION = 6.0;

desktopApp/src/main/swift/macosBridge/ImageViewerBridge.swift

Lines changed: 105 additions & 48 deletions
Original file line numberDiff line numberDiff line change
@@ -4,20 +4,27 @@ import Kingfisher
44
import SwiftUI
55
import AVKit
66

7+
class ClosableWindow : NSWindow {
8+
override func cancelOperation(_ sender: Any?) {
9+
self.close()
10+
}
11+
}
12+
713
@_cdecl("open_img_viewer")
814
public func open_img_viewer(urlCString: UnsafePointer<CChar>?) {
915
let urlStr = urlCString.flatMap { String(cString: $0) } ?? "about:blank"
1016
let targetURL = URL(string: urlStr)!
1117
DispatchQueue.main.async {
1218
if NSApp == nil { _ = NSApplication.shared }
1319

14-
let win = NSWindow(contentRect: NSRect(x: 200, y: 200, width: 1000, height: 700),
20+
let win = ClosableWindow(contentRect: NSRect(x: 200, y: 200, width: 1000, height: 700),
1521
styleMask: [.resizable, .titled, .closable, .miniaturizable, .fullSizeContentView],
1622
backing: .buffered, defer: false)
1723
win.titlebarAppearsTransparent = true
1824
win.titleVisibility = .hidden
1925
win.isMovableByWindowBackground = true
2026
win.appearance = .init(named: .darkAqua)
27+
win.setFrameOriginToPositionWindowInCenterOfScreen()
2128

2229
win.contentView = makeZoomingScrollView(targetURL: targetURL)
2330
win.isReleasedWhenClosed = false
@@ -35,13 +42,14 @@ public func open_video_viewer(urlCString: UnsafePointer<CChar>?) {
3542
DispatchQueue.main.async {
3643
if NSApp == nil { _ = NSApplication.shared }
3744

38-
let win = NSWindow(contentRect: NSRect(x: 200, y: 200, width: 1000, height: 700),
45+
let win = ClosableWindow(contentRect: NSRect(x: 200, y: 200, width: 1000, height: 700),
3946
styleMask: [.resizable, .titled, .closable, .miniaturizable, .fullSizeContentView],
4047
backing: .buffered, defer: false)
4148
win.titlebarAppearsTransparent = true
4249
win.titleVisibility = .hidden
4350
win.isMovableByWindowBackground = true
4451
win.appearance = .init(named: .darkAqua)
52+
win.setFrameOriginToPositionWindowInCenterOfScreen()
4553
let playerView = AVPlayerView()
4654
let videoPlayer = AVPlayer(url: targetURL)
4755
playerView.player = videoPlayer
@@ -56,53 +64,94 @@ public func open_video_viewer(urlCString: UnsafePointer<CChar>?) {
5664

5765

5866

59-
//struct OpenStatusImageModel: Decodable {
60-
// let index: Int
61-
// let medias: [StatusMediaItem]
62-
//}
63-
//
64-
//struct StatusMediaItem: Decodable {
65-
// let url: String
66-
// let type: String
67-
// let placeholder: String?
68-
//}
69-
//
70-
//@_cdecl("open_status_image_viewer")
71-
//public func open_status_image_viewer(_ json: UnsafePointer<CChar>?) {
72-
// guard let json = json else { return }
73-
// let s = String(cString: json)
74-
// if let data = s.data(using: .utf8),
75-
// let model = try? JSONDecoder().decode(OpenStatusImageModel.self, from: data) {
76-
// DispatchQueue.main.sync {
77-
// if NSApp == nil { _ = NSApplication.shared }
78-
// let win = NSWindow(contentRect: NSRect(x: 200, y: 200, width: 1000, height: 700),
79-
// styleMask: [.resizable, .titled, .closable, .miniaturizable, .fullSizeContentView],
80-
// backing: .buffered, defer: false)
81-
//// let win = NSWindow(contentViewController: OpenStatusPagerViewController(model: model))
82-
//// win.contentMinSize = .init(width: 100, height: 100)
83-
//// win.styleMask = [.resizable, .titled, .closable, .miniaturizable, .fullSizeContentView]
84-
// win.titlebarAppearsTransparent = true
85-
// win.titleVisibility = .hidden
86-
// win.isMovableByWindowBackground = true
87-
// win.appearance = .init(named: .darkAqua)
88-
//
89-
//// let pagerVC = OpenStatusPagerViewController(model: model)
90-
//// win.contentViewController = pagerVC
91-
// win.contentView = NSHostingView(rootView: MediaPagerView(model: model))
92-
// win.isReleasedWhenClosed = false
93-
// win.delegate = windowDelegate
94-
// openedWindows.insert(win)
95-
// swiftLog(2, "Window opened. Total windows: \(openedWindows.count)") // 调试日志
96-
//
97-
// win.makeKeyAndOrderFront(nil)
98-
// NSApp.activate(ignoringOtherApps: true)
99-
// swiftLog(2, "8. Window should be visible. Window count: \(openedWindows.count)")
100-
// swiftLog(2, "9. Window is visible: \(win.isVisible), is key: \(win.isKeyWindow)")
101-
//
102-
// }
103-
// }
104-
//}
67+
struct OpenStatusImageModel: Decodable {
68+
let index: Int
69+
let medias: [StatusMediaItem]
70+
}
71+
72+
struct StatusMediaItem: Decodable {
73+
let url: String
74+
let type: String
75+
let placeholder: String?
76+
}
77+
78+
@_cdecl("open_status_image_viewer")
79+
public func open_status_image_viewer(_ json: UnsafePointer<CChar>?) {
80+
guard let json = json else { return }
81+
let s = String(cString: json)
82+
if let data = s.data(using: .utf8),
83+
let model = try? JSONDecoder().decode(OpenStatusImageModel.self, from: data) {
84+
DispatchQueue.main.sync {
85+
if NSApp == nil { _ = NSApplication.shared }
86+
let win = ClosableWindow(contentRect: NSRect(x: 200, y: 200, width: 1000, height: 700),
87+
styleMask: [.resizable, .titled, .closable, .miniaturizable, .fullSizeContentView],
88+
backing: .buffered, defer: false)
89+
win.titlebarAppearsTransparent = true
90+
win.titleVisibility = .hidden
91+
win.isMovableByWindowBackground = true
92+
win.appearance = .init(named: .darkAqua)
93+
win.setFrameOriginToPositionWindowInCenterOfScreen()
94+
95+
win.contentView = NSHostingView(rootView: StatusMediaView(medias: model.medias, page: model.index))
96+
win.isReleasedWhenClosed = false
97+
98+
win.makeKeyAndOrderFront(nil)
99+
NSApp.activate(ignoringOtherApps: true)
100+
101+
}
102+
}
103+
}
104+
105105

106+
struct StatusMediaView: View {
107+
let medias: [StatusMediaItem]
108+
@State var page: Int? = 0
109+
var body: some View {
110+
ScrollView(.horizontal) {
111+
HStack(spacing: 0) {
112+
ForEach(0..<medias.count, id: \.self) { i in
113+
let media = medias[i]
114+
ZStack {
115+
if (media.type == "gif" || media.type == "image") {
116+
KFImage(.init(string: media.url)!)
117+
.placeholder({
118+
if let placeholder = media.placeholder {
119+
KFImage
120+
.url(.init(string: placeholder)!)
121+
.resizable()
122+
.scaledToFit()
123+
} else {
124+
ProgressView()
125+
}
126+
})
127+
.resizable()
128+
.scaledToFit()
129+
} else if (media.type == "video") {
130+
if (page == i) {
131+
let player = AVPlayer(url: .init(string: media.url)!)
132+
VideoPlayer(player: player)
133+
.onAppear {
134+
player.play()
135+
}
136+
} else if let placeholder = media.placeholder {
137+
KFImage(.init(string: placeholder)!)
138+
} else {
139+
EmptyView()
140+
}
141+
}
142+
}
143+
.containerRelativeFrame(.horizontal, count: 1, spacing: 0)
144+
.id(i)
145+
}
146+
}
147+
.scrollTargetLayout()
148+
}
149+
.scrollIndicators(.hidden)
150+
.scrollTargetBehavior(.paging)
151+
.scrollPosition(id: $page)
152+
.ignoresSafeArea()
153+
}
154+
}
106155

107156

108157

@@ -165,3 +214,11 @@ final class CenteringClipView: NSClipView {
165214
return rect
166215
}
167216
}
217+
218+
extension NSWindow {
219+
public func setFrameOriginToPositionWindowInCenterOfScreen() {
220+
if let screenSize = screen?.frame.size {
221+
self.setFrameOrigin(NSPoint(x: (screenSize.width-frame.size.width)/2, y: (screenSize.height-frame.size.height)/2))
222+
}
223+
}
224+
}

0 commit comments

Comments
 (0)