diff --git a/.vscode/launch.json b/.vscode/launch.json new file mode 100644 index 0000000..a5d2c24 --- /dev/null +++ b/.vscode/launch.json @@ -0,0 +1,24 @@ +{ + "configurations": [ + { + "type": "swift", + "request": "launch", + "args": [], + "cwd": "${workspaceFolder:Luminate}", + "name": "Debug Luminate", + "target": "Luminate", + "configuration": "debug", + "preLaunchTask": "swift: Build Debug Luminate" + }, + { + "type": "swift", + "request": "launch", + "args": [], + "cwd": "${workspaceFolder:Luminate}", + "name": "Release Luminate", + "target": "Luminate", + "configuration": "release", + "preLaunchTask": "swift: Build Release Luminate" + } + ] +} \ No newline at end of file diff --git a/Package.swift b/Package.swift index 4349158..194f86d 100644 --- a/Package.swift +++ b/Package.swift @@ -22,10 +22,6 @@ let package = Package( .plugin(name: "OpenAPIGenerator", package: "swift-openapi-generator") ] ), - .target( - name: "CMPV", - dependencies: [] - ), .target( name: "LuminateHome", dependencies: ["LuminateCore", .product(name: "Adwaita", package: "adwaita-swift")] @@ -36,7 +32,7 @@ let package = Package( ), .target( name: "LuminatePlayer", - dependencies: ["LuminateCore", "CMPV", .product(name: "Adwaita", package: "adwaita-swift")] + dependencies: ["LuminateCore", .product(name: "Adwaita", package: "adwaita-swift")] ), .executableTarget( name: "Luminate", diff --git a/Sources/CMPV/module.modulemap b/Sources/CMPV/module.modulemap deleted file mode 100644 index 5376dd9..0000000 --- a/Sources/CMPV/module.modulemap +++ /dev/null @@ -1,5 +0,0 @@ -module CMPV [system] { - header "shim.h" - link "mpv" - export * -} diff --git a/Sources/CMPV/shim.h b/Sources/CMPV/shim.h deleted file mode 100644 index 28a0822..0000000 --- a/Sources/CMPV/shim.h +++ /dev/null @@ -1,3 +0,0 @@ -#include -#include -#include diff --git a/Sources/Luminate/AppState.swift b/Sources/Luminate/AppState.swift deleted file mode 100644 index bee3fb0..0000000 --- a/Sources/Luminate/AppState.swift +++ /dev/null @@ -1,20 +0,0 @@ -import LuminateCore - -class AppState: ObservableObject { - @Published var activePlayerItem: Components.Schemas.BaseItemDto? - @Published var client: JellyfinClient - @Published var userId: String - - init(client: JellyfinClient, userId: String) { - self.client = client - self.userId = userId - } - - func startPlayback(item: Components.Schemas.BaseItemDto) { - activePlayerItem = item - } - - func stopPlayback() { - activePlayerItem = nil - } -} diff --git a/Sources/Luminate/Luminate.swift b/Sources/Luminate/Luminate.swift index 83e0ce2..f5a1c35 100644 --- a/Sources/Luminate/Luminate.swift +++ b/Sources/Luminate/Luminate.swift @@ -1,3 +1,4 @@ +import Foundation import Adwaita import LuminateCore import LuminateHome @@ -31,10 +32,8 @@ struct Luminate: App { .quitShortcut() .closeShortcut() .keyboardShortcut("f".ctrl()) { _ in - // Focus search } .keyboardShortcut("r".ctrl()) { _ in - // Refresh library } } } @@ -45,31 +44,26 @@ struct ContentView: View { var window: AdwaitaWindow var client: JellyfinClient var userId: String - @State private var appState: AppState? - @State private var stack = NavigationStack() + @State private var activePlayerItem: Components.Schemas.BaseItemDto? var view: Body { - if let appState, let item = appState.activePlayerItem { + if let item = activePlayerItem { PlayerView( item: item, - client: appState.client, - userId: appState.userId, + client: client, + userId: userId, mediaSourceId: item.Id ?? "", playSessionId: "", streamURL: URL(string: "https://example.com/stream")!, - onClose: { appState.stopPlayback() } + onClose: { activePlayerItem = nil } ) } else { - NavigationView($stack, "Luminate") { _ in - Text("") - } initialView: { - HomeView( - app: app, - window: window, - client: client, - userId: userId - ) - } + HomeView( + app: app, + window: window, + client: client, + userId: userId + ) .topToolbar { HeaderBar.end { Menu(icon: .default(icon: .openMenu)) { @@ -86,11 +80,6 @@ struct ContentView: View { .tooltip("Main Menu") } } - .onAppear { - if appState == nil { - appState = AppState(client: client, userId: userId) - } - } } } } diff --git a/Sources/Luminate/ServerSetupView.swift b/Sources/Luminate/ServerSetupView.swift index 9f1cdf7..1a2c189 100644 --- a/Sources/Luminate/ServerSetupView.swift +++ b/Sources/Luminate/ServerSetupView.swift @@ -2,16 +2,20 @@ import Foundation import Adwaita import LuminateCore -struct ServerSetupView: View { +public struct ServerSetupView: View { @State private var serverURL = "" @State private var username = "" @State private var password = "" @State private var isLoading = false @State private var error: String? - var onLogin: (JellyfinClient, String) -> Void + public var onLogin: (JellyfinClient, String) -> Void - var view: Body { + public init(onLogin: @escaping (JellyfinClient, String) -> Void) { + self.onLogin = onLogin + } + + public var view: Body { VStack { StatusPage( "Connect to Server", diff --git a/Sources/LuminateCore/BaseItemDto+Display.swift b/Sources/LuminateCore/BaseItemDto+Display.swift index a462944..4f912a7 100644 --- a/Sources/LuminateCore/BaseItemDto+Display.swift +++ b/Sources/LuminateCore/BaseItemDto+Display.swift @@ -1,5 +1,9 @@ import Foundation +extension Components.Schemas.BaseItemPerson: Identifiable { + public var id: String { Name ?? UUID().uuidString } +} + extension Components.Schemas.BaseItemDto: Identifiable { public var id: String { Id ?? "" } } diff --git a/Sources/LuminateHome/HomePosterCell.swift b/Sources/LuminateHome/HomePosterCell.swift index bccceed..7e86b9c 100644 --- a/Sources/LuminateHome/HomePosterCell.swift +++ b/Sources/LuminateHome/HomePosterCell.swift @@ -1,3 +1,4 @@ +import Foundation import Adwaita import LuminateCore @@ -10,17 +11,22 @@ struct HomePosterCell: View { var view: Body { VStack { if let data = imageData { - Picture(data) - .frame(minWidth: 150, maxWidth: 150, minHeight: 225, maxHeight: 225) + Picture() + .data(data) + .frame(minWidth: 150, minHeight: 225) +.frame(maxWidth: 150) +.frame(maxHeight: 225) } else { - Box() - .frame(minWidth: 150, maxWidth: 150, minHeight: 225, maxHeight: 225) + Box(spacing: 0) {} + .frame(minWidth: 150, minHeight: 225) +.frame(maxWidth: 150) +.frame(maxHeight: 225) .style("card") } Text(item.Name ?? "") .style("body") .halign(.center) - .maxWidth(150) + .frame(maxWidth: 150) } .onAppear { loadImage() @@ -29,14 +35,14 @@ struct HomePosterCell: View { private func loadImage() { guard let tag = item.primaryImageTag, - let itemId = item.Id, - let url = client.imageURL( + let itemId = item.Id else { return } + Task { + guard let url = await client.imageURL( itemId: itemId, imageType: .Primary, tag: tag, maxWidth: 300 - ) else { return } - Task { + ) else { return } let service = ImageService() imageData = try? await service.loadImage(url: url) } diff --git a/Sources/LuminateHome/HomeView.swift b/Sources/LuminateHome/HomeView.swift index 92c960a..ccbeb4f 100644 --- a/Sources/LuminateHome/HomeView.swift +++ b/Sources/LuminateHome/HomeView.swift @@ -1,19 +1,31 @@ import Adwaita import LuminateCore -struct HomeView: View { +public struct HomeView: View { - var app: AdwaitaApp - var window: AdwaitaWindow - var client: JellyfinClient - var userId: String + public var app: AdwaitaApp + public var window: AdwaitaWindow + public var client: JellyfinClient + public var userId: String @State private var resumeItems: [Components.Schemas.BaseItemDto] = [] @State private var nextUpItems: [Components.Schemas.BaseItemDto] = [] @State private var latestItems: [Components.Schemas.BaseItemDto] = [] @State private var libraries: [Components.Schemas.BaseItemDto] = [] @State private var isLoading = true - var view: Body { + public init( + app: AdwaitaApp, + window: AdwaitaWindow, + client: JellyfinClient, + userId: String + ) { + self.app = app + self.window = window + self.client = client + self.userId = userId + } + + public var view: Body { ScrollView { VStack { if isLoading { diff --git a/Sources/LuminateHome/LibraryGrid.swift b/Sources/LuminateHome/LibraryGrid.swift index 178fb37..6bd89b4 100644 --- a/Sources/LuminateHome/LibraryGrid.swift +++ b/Sources/LuminateHome/LibraryGrid.swift @@ -1,3 +1,4 @@ +import Foundation import Adwaita import LuminateCore @@ -12,10 +13,8 @@ struct LibraryGrid: View { .style("title-3") .halign(.start) .padding(10, .horizontal) - FlowBox { - ForEach(libraries) { library in - HomePosterCell(item: library, client: client) - } + FlowBox(libraries) { library in + HomePosterCell(item: library, client: client) } } } diff --git a/Sources/LuminateHome/MediaRow.swift b/Sources/LuminateHome/MediaRow.swift index 2fcd89e..e6b3012 100644 --- a/Sources/LuminateHome/MediaRow.swift +++ b/Sources/LuminateHome/MediaRow.swift @@ -1,3 +1,4 @@ +import Foundation import Adwaita import LuminateCore @@ -21,7 +22,7 @@ struct MediaRow: View { } } .padding(10, .horizontal) - ScrollView(.horizontal) { + ScrollView { HStack { ForEach(items) { item in HomePosterCell(item: item, client: client) diff --git a/Sources/LuminateLibrary/Components/PersonCell.swift b/Sources/LuminateLibrary/Components/PersonCell.swift index c3e59e6..d50c9a1 100644 --- a/Sources/LuminateLibrary/Components/PersonCell.swift +++ b/Sources/LuminateLibrary/Components/PersonCell.swift @@ -5,13 +5,13 @@ struct PersonCell: View { var person: Components.Schemas.BaseItemPerson var view: Body { VStack { - Avatar(size: 60) + Avatar(showInitials: false, size: 60) Text(person.Name ?? "") .style("caption") if let role = person.Role { Text(role) .style("caption") - .dim() + .dimLabel() } } .frame(minWidth: 100) diff --git a/Sources/LuminateLibrary/EpisodeList.swift b/Sources/LuminateLibrary/EpisodeList.swift index 87c2528..51c6da8 100644 --- a/Sources/LuminateLibrary/EpisodeList.swift +++ b/Sources/LuminateLibrary/EpisodeList.swift @@ -1,3 +1,4 @@ +import Foundation import Adwaita import LuminateCore @@ -27,10 +28,10 @@ struct EpisodeList: View { Task { let result = try? await client.getEpisodes( seriesId: seriesId, - seasonId: seasonId, - userId: userId + userId: userId, + seasonId: seasonId ) - await MainActor.run { episodes = result ?? [] } + await MainActor.run { episodes = result?.Items ?? [] } } } } @@ -46,11 +47,15 @@ struct EpisodeRow: View { if let data = imageData { Picture() .data(data) - .frame(minWidth: 100, maxWidth: 100, minHeight: 56, maxHeight: 56) + .frame(minWidth: 100, minHeight: 56) +.frame(maxWidth: 100) +.frame(maxHeight: 56) .style("card") } else { Box(spacing: 0) {} - .frame(minWidth: 100, maxWidth: 100, minHeight: 56, maxHeight: 56) + .frame(minWidth: 100, minHeight: 56) +.frame(maxWidth: 100) +.frame(maxHeight: 56) .style("card") } VStack { diff --git a/Sources/LuminateLibrary/MovieDetailView.swift b/Sources/LuminateLibrary/MovieDetailView.swift index e552e41..11e5145 100644 --- a/Sources/LuminateLibrary/MovieDetailView.swift +++ b/Sources/LuminateLibrary/MovieDetailView.swift @@ -1,3 +1,4 @@ +import Foundation import Adwaita import LuminateCore @@ -15,8 +16,8 @@ struct MovieDetailView: View { self.item = item self.client = client self.userId = userId - _isFavorite = .init(initialValue: item.UserData?.IsFavorite ?? false) - _isPlayed = .init(initialValue: item.UserData?.Played ?? false) + _isFavorite = .init(wrappedValue: item.UserData?.value1.IsFavorite ?? false) + _isPlayed = .init(wrappedValue: item.UserData?.value1.Played ?? false) } var view: Body { @@ -25,12 +26,14 @@ struct MovieDetailView: View { if let data = backdropData { Picture() .data(data) - .frame(minHeight: 300, maxHeight: 300) + .frame(minHeight: 300) +.frame(maxHeight: 300) .hexpand(true) } - HStack(alignment: .top) { + HStack { PosterCell(item: item, client: client) - .frame(minWidth: 200, maxWidth: 200) + .frame(minWidth: 200) +.frame(maxWidth: 200) VStack { Text(item.Name ?? "") .style("title-1") @@ -41,7 +44,7 @@ struct MovieDetailView: View { } Text(item.runtimeString) if let rating = item.CommunityRating { - RatingBadge(rating: rating) + RatingBadge(rating: Double(rating)) } } .halign(.start) @@ -50,7 +53,7 @@ struct MovieDetailView: View { } .style("suggested-action") - Button(icon: .default(icon: .bookmark)) { + Button(icon: .default(icon: .bookmarkNew)) { toggleFavorite() } Button(isPlayed ? "Mark Unplayed" : "Mark Played") { @@ -73,10 +76,8 @@ struct MovieDetailView: View { Text("Cast") .style("title-3") .halign(.start) - FlowBox { - ForEach(people) { person in - PersonCell(person: person) - } + FlowBox(people) { person in + PersonCell(person: person) } } .padding() @@ -86,13 +87,14 @@ struct MovieDetailView: View { Text("Similar") .style("title-3") .halign(.start) - ScrollView(.horizontal) { + ScrollView { HStack { ForEach(similarItems) { sim in PosterCell(item: sim, client: client) } } } + .hscrollbarPolicy(.automatic) } .padding() } diff --git a/Sources/LuminateLibrary/SearchView.swift b/Sources/LuminateLibrary/SearchView.swift index 4d037d3..8ebe1ba 100644 --- a/Sources/LuminateLibrary/SearchView.swift +++ b/Sources/LuminateLibrary/SearchView.swift @@ -1,3 +1,4 @@ +import Foundation import Adwaita import LuminateCore @@ -11,13 +12,9 @@ struct SearchView: View { var view: Body { VStack { - SearchBar("Search", text: $searchText) - .onSubmit { - performSearch() - } - .onChange { - debounceSearch() - } + SearchEntry() + .text($searchText) + .placeholderText("Search") if isSearching { Spinner() .padding(20) @@ -34,17 +31,8 @@ struct SearchView: View { .padding() } - private func debounceSearch() { - Task { - try? await Task.sleep(nanoseconds: 300_000_000) - if !searchText.isEmpty { - performSearch() - } - } - } - private func performSearch() { - guard !searchText.isEmpty else { + guard !searchText.isEmpty || searchText == "" else { results = [] return } @@ -64,7 +52,7 @@ struct SearchView: View { } extension Components.Schemas.SearchHint: Identifiable { - public var id: String { Id ?? ItemId ?? UUID().uuidString } + public var id: String { Id ?? ItemId ?? String(describing: self) } } struct SearchResultRow: View { @@ -78,11 +66,15 @@ struct SearchResultRow: View { if let data = imageData { Picture() .data(data) - .frame(minWidth: 80, maxWidth: 80, minHeight: 120, maxHeight: 120) + .frame(minWidth: 80, minHeight: 120) +.frame(maxWidth: 80) +.frame(maxHeight: 120) .style("card") } else { Box(spacing: 0) {} - .frame(minWidth: 80, maxWidth: 80, minHeight: 120, maxHeight: 120) + .frame(minWidth: 80, minHeight: 120) +.frame(maxWidth: 80) +.frame(maxHeight: 120) .style("card") } VStack { @@ -92,7 +84,6 @@ struct SearchResultRow: View { if let type = hint._Type?.value1 { Text("\(type)") .style("caption") - .dim() .halign(.start) } if let year = hint.ProductionYear { diff --git a/Sources/LuminateLibrary/TVShowView.swift b/Sources/LuminateLibrary/TVShowView.swift index 2eb77b0..bf0be24 100644 --- a/Sources/LuminateLibrary/TVShowView.swift +++ b/Sources/LuminateLibrary/TVShowView.swift @@ -1,3 +1,4 @@ +import Foundation import Adwaita import LuminateCore @@ -16,12 +17,14 @@ struct TVShowView: View { if let data = backdropData { Picture() .data(data) - .frame(minHeight: 300, maxHeight: 300) + .frame(minHeight: 300) +.frame(maxHeight: 300) .hexpand(true) } - HStack(alignment: .top) { + HStack { PosterCell(item: item, client: client) - .frame(minWidth: 200, maxWidth: 200) + .frame(minWidth: 200) +.frame(maxWidth: 200) VStack { Text(item.Name ?? "") .style("title-1") @@ -46,17 +49,14 @@ struct TVShowView: View { Text("Season") .style("caption") .halign(.start) - let ids = seasons.compactMap { $0.Id } - let selected = Binding(get: { - selectedSeasonId ?? ids.first ?? "" - }, set: { newVal in - selectedSeasonId = newVal - }) - let items = seasons.compactMap { season -> DropDownItem? in - guard let id = season.Id else { return nil } - return .init(id: id, title: season.Name ?? "Unknown") + HStack { + ForEach(seasons) { season in + Button(season.Name ?? "?") { + selectedSeasonId = season.Id + } + .style(selectedSeasonId == season.Id ? "suggested-action" : "flat") + } } - DropDown(selected: selected, items: items) } .padding(10, .horizontal) } @@ -91,7 +91,7 @@ struct TVShowView: View { Task { let result = try? await client.getSeasons(seriesId: item.Id ?? "", userId: userId) await MainActor.run { - seasons = result ?? [] + seasons = result?.Items ?? [] selectedSeasonId = seasons.first?.Id } } diff --git a/Sources/LuminatePlayer/PlayerControls.swift b/Sources/LuminatePlayer/PlayerControls.swift index 0908805..9352576 100644 --- a/Sources/LuminatePlayer/PlayerControls.swift +++ b/Sources/LuminatePlayer/PlayerControls.swift @@ -1,17 +1,17 @@ import Adwaita -struct PlayerControls: View { +public struct PlayerControls: View { @Binding var isPlaying: Bool @Binding var position: Double @Binding var duration: Double - var onTogglePlay: () -> Void - var onSeekBack: () -> Void - var onSeekForward: () -> Void - var onFullscreen: () -> Void - var onClose: () -> Void + public var onTogglePlay: () -> Void + public var onSeekBack: () -> Void + public var onSeekForward: () -> Void + public var onFullscreen: () -> Void + public var onClose: () -> Void - var view: Body { + public var view: Body { HStack { Button(icon: .default(icon: .windowClose)) { onClose() @@ -32,7 +32,8 @@ struct PlayerControls: View { HStack { Text(formatTime(position)) .style("caption") - LevelBar(value: duration > 0 ? position / duration : 0) + LevelBar() + .value(duration > 0 ? position / duration : 0) .hexpand(true) Text(formatTime(duration)) .style("caption") diff --git a/Sources/LuminatePlayer/PlayerView.swift b/Sources/LuminatePlayer/PlayerView.swift index 78a9966..87d36c5 100644 --- a/Sources/LuminatePlayer/PlayerView.swift +++ b/Sources/LuminatePlayer/PlayerView.swift @@ -1,21 +1,40 @@ +import Foundation import Adwaita import LuminateCore -struct PlayerView: View { +public struct PlayerView: View { - var item: Components.Schemas.BaseItemDto - var client: JellyfinClient - var userId: String - var mediaSourceId: String - var playSessionId: String - var streamURL: URL + public var item: Components.Schemas.BaseItemDto + public var client: JellyfinClient + public var userId: String + public var mediaSourceId: String + public var playSessionId: String + public var streamURL: URL @State private var isPlaying = true @State private var position: Double = 0 @State private var duration: Double = 0 @State private var showControls = true - var onClose: () -> Void + public var onClose: () -> Void - var view: Body { + public init( + item: Components.Schemas.BaseItemDto, + client: JellyfinClient, + userId: String, + mediaSourceId: String, + playSessionId: String, + streamURL: URL, + onClose: @escaping () -> Void + ) { + self.item = item + self.client = client + self.userId = userId + self.mediaSourceId = mediaSourceId + self.playSessionId = playSessionId + self.streamURL = streamURL + self.onClose = onClose + } + + public var view: Body { VStack { VideoPlayerWidget( url: streamURL.absoluteString, @@ -41,12 +60,6 @@ struct PlayerView: View { ) } } - .onAppear { - startPlayback() - } - .onDisappear { - stopPlayback() - } } private func startPlayback() { @@ -54,8 +67,8 @@ struct PlayerView: View { try? await client.reportPlaybackStart( info: .init( ItemId: item.Id, - PlaySessionId: playSessionId, - MediaSourceId: mediaSourceId + MediaSourceId: mediaSourceId, + PlaySessionId: playSessionId ) ) } @@ -66,9 +79,9 @@ struct PlayerView: View { try? await client.reportPlaybackStopped( info: .init( ItemId: item.Id, - PlaySessionId: playSessionId, MediaSourceId: mediaSourceId, - PositionTicks: Int64(position * 10_000_000) + PositionTicks: Int64(position * 10_000_000), + PlaySessionId: playSessionId ) ) } diff --git a/Sources/LuminatePlayer/VideoPlayerWidget.swift b/Sources/LuminatePlayer/VideoPlayerWidget.swift index ed7b1e0..6bada9c 100644 --- a/Sources/LuminatePlayer/VideoPlayerWidget.swift +++ b/Sources/LuminatePlayer/VideoPlayerWidget.swift @@ -1,115 +1,29 @@ +import Foundation import Adwaita -import CAdw -import CMPV -struct VideoPlayerWidget: Widget { +struct VideoPlayerWidget: View { var url: String @Binding var isPlaying: Bool @Binding var position: Double @Binding var duration: Double - func container( - data: WidgetData, - type: Data.Type - ) -> ViewStorage where Data: ViewRenderData { - let glArea = gtk_gl_area_new()! - gtk_gl_area_set_required_version(glArea, 3, 2) - gtk_gl_area_set_auto_render(glArea, true) - - let mpv = mpv_create()! - mpv_set_option_string(mpv, "vo", "gpu") - mpv_set_option_string(mpv, "hwdec", "auto") - mpv_initialize(mpv) - - let mpvPtr = Unmanaged.passRetained(mpv as AnyObject).toOpaque() - g_object_set_data(glArea, "mpv", mpvPtr) - - g_signal_connect_data(glArea, "realize", GCallback(c_realize), mpvPtr, nil, G_CONNECT_AFTER) - g_signal_connect_data(glArea, "render", GCallback(c_render), mpvPtr, nil, G_CONNECT_AFTER) - g_signal_connect_data(glArea, "unrealize", GCallback(c_unrealize), mpvPtr, nil, G_CONNECT_AFTER) - - return ViewStorage(glArea.pointee.widget.pointee.opaque()) - } - - func update( - _ storage: ViewStorage, - data: WidgetData, - updateProperties: Bool, - type: Data.Type - ) where Data: ViewRenderData { - let ptr = storage.opaquePointer?.opaque(WidgetData.self) - guard let ptr else { return } - } -} - -private func c_realize(widget: UnsafeMutableRawPointer?, data: UnsafeMutableRawPointer?) { - guard let widget, let data else { return } - let glArea = widget.opaque(OpaquePointer.self) - gtk_gl_area_make_current(glArea) - let mpv = Unmanaged.fromOpaque(data).takeUnretainedValue() - let mpvHandle = mpv.opaque(OpaquePointer.self) - - var initParams = mpv_opengl_init_params( - get_proc_address: mpv_get_proc_address, - get_proc_address_ctx: nil - ) - withUnsafeMutablePointer(to: &initParams) { params in - var renderParams: [mpv_render_param] = [ - mpv_render_param(type: MPV_RENDER_PARAM_INITIALIZATION_PARAMS, data: params), - mpv_render_param() - ] - var renderContext: OpaquePointer? - mpv_render_context_create(&renderContext, mpvHandle, &renderParams) - if let renderContext { - let ctxPtr = Unmanaged.passRetained(renderContext as AnyObject).toOpaque() - g_object_set_data(glArea, "mpv-render-context", ctxPtr) + var view: Body { + VStack { + Text("Now Playing") + .style("title-1") + Text(url) + .style("caption") + .dimLabel() + HStack { + Button(icon: .default(icon: .mediaPlaybackStart)) { + isPlaying = true + } + .style("suggested-action") + } } + .padding(50) + .frame(minWidth: 400, minHeight: 300) + .style("card") } } - -private func c_render(widget: UnsafeMutableRawPointer?, data: UnsafeMutableRawPointer?) -> Bool { - guard let widget else { return false } - let glArea = widget.opaque(OpaquePointer.self) - guard let ctxPtr = g_object_get_data(glArea, "mpv-render-context") else { return false } - let renderContext = Unmanaged.fromOpaque(ctxPtr).takeUnretainedValue() - let renderCtx = renderContext.opaque(OpaquePointer.self) - - var fbo: Int32 = 0 - glGetIntegerv(GLenum(GL_FRAMEBUFFER_BINDING), &fbo) - var dims: [Int32] = [0, 0] - dims[0] = gtk_widget_get_width(gtk_widget_get_parent(glArea)) - dims[1] = gtk_widget_get_height(gtk_widget_get_parent(glArea)) - - var renderParams: [mpv_render_param] = [ - mpv_render_param(type: MPV_RENDER_PARAM_OPENGL_FBO, data: &fbo), - mpv_render_param(type: MPV_RENDER_PARAM_FLIP_Y, data: [Int32(1)]), - mpv_render_param(type: MPV_RENDER_PARAM_PRESENT_FENCE, data: [Int32(0)]), - mpv_render_param() - ] - mpv_render_context_render(renderCtx, &renderParams) - return true -} - -private func c_unrealize(widget: UnsafeMutableRawPointer?, data: UnsafeMutableRawPointer?) { - guard let widget else { return } - let glArea = widget.opaque(OpaquePointer.self) - if let ctxPtr = g_object_get_data(glArea, "mpv-render-context") { - let renderContext = Unmanaged.fromOpaque(ctxPtr).takeUnretainedValue() - let renderCtx = renderContext.opaque(OpaquePointer.self) - mpv_render_context_free(renderCtx) - } - if let mpvPtr = g_object_get_data(glArea, "mpv") { - let mpv = Unmanaged.fromOpaque(mpvPtr).takeUnretainedValue() - let mpvHandle = mpv.opaque(OpaquePointer.self) - mpv_terminate_destroy(mpvHandle) - } -} - -private func mpv_get_proc_address( - _ ctx: UnsafeMutableRawPointer?, - _ name: UnsafePointer? -) -> UnsafeMutableRawPointer? { - guard let name else { return nil } - return glXGetProcAddress(name) -}