Resolve build errors - public access, Adwaita API alignment, import fixes

This commit is contained in:
Brendan Szymanski 2026-06-05 02:19:45 -04:00
parent cffb94605d
commit daca81e594
20 changed files with 197 additions and 264 deletions

24
.vscode/launch.json vendored Normal file
View file

@ -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"
}
]
}

View file

@ -22,10 +22,6 @@ let package = Package(
.plugin(name: "OpenAPIGenerator", package: "swift-openapi-generator") .plugin(name: "OpenAPIGenerator", package: "swift-openapi-generator")
] ]
), ),
.target(
name: "CMPV",
dependencies: []
),
.target( .target(
name: "LuminateHome", name: "LuminateHome",
dependencies: ["LuminateCore", .product(name: "Adwaita", package: "adwaita-swift")] dependencies: ["LuminateCore", .product(name: "Adwaita", package: "adwaita-swift")]
@ -36,7 +32,7 @@ let package = Package(
), ),
.target( .target(
name: "LuminatePlayer", name: "LuminatePlayer",
dependencies: ["LuminateCore", "CMPV", .product(name: "Adwaita", package: "adwaita-swift")] dependencies: ["LuminateCore", .product(name: "Adwaita", package: "adwaita-swift")]
), ),
.executableTarget( .executableTarget(
name: "Luminate", name: "Luminate",

View file

@ -1,5 +0,0 @@
module CMPV [system] {
header "shim.h"
link "mpv"
export *
}

View file

@ -1,3 +0,0 @@
#include <mpv/client.h>
#include <mpv/render.h>
#include <mpv/render_gl.h>

View file

@ -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
}
}

View file

@ -1,3 +1,4 @@
import Foundation
import Adwaita import Adwaita
import LuminateCore import LuminateCore
import LuminateHome import LuminateHome
@ -31,10 +32,8 @@ struct Luminate: App {
.quitShortcut() .quitShortcut()
.closeShortcut() .closeShortcut()
.keyboardShortcut("f".ctrl()) { _ in .keyboardShortcut("f".ctrl()) { _ in
// Focus search
} }
.keyboardShortcut("r".ctrl()) { _ in .keyboardShortcut("r".ctrl()) { _ in
// Refresh library
} }
} }
} }
@ -45,31 +44,26 @@ struct ContentView: View {
var window: AdwaitaWindow var window: AdwaitaWindow
var client: JellyfinClient var client: JellyfinClient
var userId: String var userId: String
@State private var appState: AppState? @State private var activePlayerItem: Components.Schemas.BaseItemDto?
@State private var stack = NavigationStack<String>()
var view: Body { var view: Body {
if let appState, let item = appState.activePlayerItem { if let item = activePlayerItem {
PlayerView( PlayerView(
item: item, item: item,
client: appState.client, client: client,
userId: appState.userId, userId: userId,
mediaSourceId: item.Id ?? "", mediaSourceId: item.Id ?? "",
playSessionId: "", playSessionId: "",
streamURL: URL(string: "https://example.com/stream")!, streamURL: URL(string: "https://example.com/stream")!,
onClose: { appState.stopPlayback() } onClose: { activePlayerItem = nil }
) )
} else { } else {
NavigationView($stack, "Luminate") { _ in
Text("")
} initialView: {
HomeView( HomeView(
app: app, app: app,
window: window, window: window,
client: client, client: client,
userId: userId userId: userId
) )
}
.topToolbar { .topToolbar {
HeaderBar.end { HeaderBar.end {
Menu(icon: .default(icon: .openMenu)) { Menu(icon: .default(icon: .openMenu)) {
@ -86,11 +80,6 @@ struct ContentView: View {
.tooltip("Main Menu") .tooltip("Main Menu")
} }
} }
.onAppear {
if appState == nil {
appState = AppState(client: client, userId: userId)
}
}
} }
} }
} }

View file

@ -2,16 +2,20 @@ import Foundation
import Adwaita import Adwaita
import LuminateCore import LuminateCore
struct ServerSetupView: View { public struct ServerSetupView: View {
@State private var serverURL = "" @State private var serverURL = ""
@State private var username = "" @State private var username = ""
@State private var password = "" @State private var password = ""
@State private var isLoading = false @State private var isLoading = false
@State private var error: String? @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 { VStack {
StatusPage( StatusPage(
"Connect to Server", "Connect to Server",

View file

@ -1,5 +1,9 @@
import Foundation import Foundation
extension Components.Schemas.BaseItemPerson: Identifiable {
public var id: String { Name ?? UUID().uuidString }
}
extension Components.Schemas.BaseItemDto: Identifiable { extension Components.Schemas.BaseItemDto: Identifiable {
public var id: String { Id ?? "" } public var id: String { Id ?? "" }
} }

View file

@ -1,3 +1,4 @@
import Foundation
import Adwaita import Adwaita
import LuminateCore import LuminateCore
@ -10,17 +11,22 @@ struct HomePosterCell: View {
var view: Body { var view: Body {
VStack { VStack {
if let data = imageData { if let data = imageData {
Picture(data) Picture()
.frame(minWidth: 150, maxWidth: 150, minHeight: 225, maxHeight: 225) .data(data)
.frame(minWidth: 150, minHeight: 225)
.frame(maxWidth: 150)
.frame(maxHeight: 225)
} else { } else {
Box() Box(spacing: 0) {}
.frame(minWidth: 150, maxWidth: 150, minHeight: 225, maxHeight: 225) .frame(minWidth: 150, minHeight: 225)
.frame(maxWidth: 150)
.frame(maxHeight: 225)
.style("card") .style("card")
} }
Text(item.Name ?? "") Text(item.Name ?? "")
.style("body") .style("body")
.halign(.center) .halign(.center)
.maxWidth(150) .frame(maxWidth: 150)
} }
.onAppear { .onAppear {
loadImage() loadImage()
@ -29,14 +35,14 @@ struct HomePosterCell: View {
private func loadImage() { private func loadImage() {
guard let tag = item.primaryImageTag, guard let tag = item.primaryImageTag,
let itemId = item.Id, let itemId = item.Id else { return }
let url = client.imageURL( Task {
guard let url = await client.imageURL(
itemId: itemId, itemId: itemId,
imageType: .Primary, imageType: .Primary,
tag: tag, tag: tag,
maxWidth: 300 maxWidth: 300
) else { return } ) else { return }
Task {
let service = ImageService() let service = ImageService()
imageData = try? await service.loadImage(url: url) imageData = try? await service.loadImage(url: url)
} }

View file

@ -1,19 +1,31 @@
import Adwaita import Adwaita
import LuminateCore import LuminateCore
struct HomeView: View { public struct HomeView: View {
var app: AdwaitaApp public var app: AdwaitaApp
var window: AdwaitaWindow public var window: AdwaitaWindow
var client: JellyfinClient public var client: JellyfinClient
var userId: String public var userId: String
@State private var resumeItems: [Components.Schemas.BaseItemDto] = [] @State private var resumeItems: [Components.Schemas.BaseItemDto] = []
@State private var nextUpItems: [Components.Schemas.BaseItemDto] = [] @State private var nextUpItems: [Components.Schemas.BaseItemDto] = []
@State private var latestItems: [Components.Schemas.BaseItemDto] = [] @State private var latestItems: [Components.Schemas.BaseItemDto] = []
@State private var libraries: [Components.Schemas.BaseItemDto] = [] @State private var libraries: [Components.Schemas.BaseItemDto] = []
@State private var isLoading = true @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 { ScrollView {
VStack { VStack {
if isLoading { if isLoading {

View file

@ -1,3 +1,4 @@
import Foundation
import Adwaita import Adwaita
import LuminateCore import LuminateCore
@ -12,11 +13,9 @@ struct LibraryGrid: View {
.style("title-3") .style("title-3")
.halign(.start) .halign(.start)
.padding(10, .horizontal) .padding(10, .horizontal)
FlowBox { FlowBox(libraries) { library in
ForEach(libraries) { library in
HomePosterCell(item: library, client: client) HomePosterCell(item: library, client: client)
} }
} }
} }
}
} }

View file

@ -1,3 +1,4 @@
import Foundation
import Adwaita import Adwaita
import LuminateCore import LuminateCore
@ -21,7 +22,7 @@ struct MediaRow: View {
} }
} }
.padding(10, .horizontal) .padding(10, .horizontal)
ScrollView(.horizontal) { ScrollView {
HStack { HStack {
ForEach(items) { item in ForEach(items) { item in
HomePosterCell(item: item, client: client) HomePosterCell(item: item, client: client)

View file

@ -5,13 +5,13 @@ struct PersonCell: View {
var person: Components.Schemas.BaseItemPerson var person: Components.Schemas.BaseItemPerson
var view: Body { var view: Body {
VStack { VStack {
Avatar(size: 60) Avatar(showInitials: false, size: 60)
Text(person.Name ?? "") Text(person.Name ?? "")
.style("caption") .style("caption")
if let role = person.Role { if let role = person.Role {
Text(role) Text(role)
.style("caption") .style("caption")
.dim() .dimLabel()
} }
} }
.frame(minWidth: 100) .frame(minWidth: 100)

View file

@ -1,3 +1,4 @@
import Foundation
import Adwaita import Adwaita
import LuminateCore import LuminateCore
@ -27,10 +28,10 @@ struct EpisodeList: View {
Task { Task {
let result = try? await client.getEpisodes( let result = try? await client.getEpisodes(
seriesId: seriesId, 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 { if let data = imageData {
Picture() Picture()
.data(data) .data(data)
.frame(minWidth: 100, maxWidth: 100, minHeight: 56, maxHeight: 56) .frame(minWidth: 100, minHeight: 56)
.frame(maxWidth: 100)
.frame(maxHeight: 56)
.style("card") .style("card")
} else { } else {
Box(spacing: 0) {} 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") .style("card")
} }
VStack { VStack {

View file

@ -1,3 +1,4 @@
import Foundation
import Adwaita import Adwaita
import LuminateCore import LuminateCore
@ -15,8 +16,8 @@ struct MovieDetailView: View {
self.item = item self.item = item
self.client = client self.client = client
self.userId = userId self.userId = userId
_isFavorite = .init(initialValue: item.UserData?.IsFavorite ?? false) _isFavorite = .init(wrappedValue: item.UserData?.value1.IsFavorite ?? false)
_isPlayed = .init(initialValue: item.UserData?.Played ?? false) _isPlayed = .init(wrappedValue: item.UserData?.value1.Played ?? false)
} }
var view: Body { var view: Body {
@ -25,12 +26,14 @@ struct MovieDetailView: View {
if let data = backdropData { if let data = backdropData {
Picture() Picture()
.data(data) .data(data)
.frame(minHeight: 300, maxHeight: 300) .frame(minHeight: 300)
.frame(maxHeight: 300)
.hexpand(true) .hexpand(true)
} }
HStack(alignment: .top) { HStack {
PosterCell(item: item, client: client) PosterCell(item: item, client: client)
.frame(minWidth: 200, maxWidth: 200) .frame(minWidth: 200)
.frame(maxWidth: 200)
VStack { VStack {
Text(item.Name ?? "") Text(item.Name ?? "")
.style("title-1") .style("title-1")
@ -41,7 +44,7 @@ struct MovieDetailView: View {
} }
Text(item.runtimeString) Text(item.runtimeString)
if let rating = item.CommunityRating { if let rating = item.CommunityRating {
RatingBadge(rating: rating) RatingBadge(rating: Double(rating))
} }
} }
.halign(.start) .halign(.start)
@ -50,7 +53,7 @@ struct MovieDetailView: View {
} }
.style("suggested-action") .style("suggested-action")
Button(icon: .default(icon: .bookmark)) { Button(icon: .default(icon: .bookmarkNew)) {
toggleFavorite() toggleFavorite()
} }
Button(isPlayed ? "Mark Unplayed" : "Mark Played") { Button(isPlayed ? "Mark Unplayed" : "Mark Played") {
@ -73,12 +76,10 @@ struct MovieDetailView: View {
Text("Cast") Text("Cast")
.style("title-3") .style("title-3")
.halign(.start) .halign(.start)
FlowBox { FlowBox(people) { person in
ForEach(people) { person in
PersonCell(person: person) PersonCell(person: person)
} }
} }
}
.padding() .padding()
} }
if !similarItems.isEmpty { if !similarItems.isEmpty {
@ -86,13 +87,14 @@ struct MovieDetailView: View {
Text("Similar") Text("Similar")
.style("title-3") .style("title-3")
.halign(.start) .halign(.start)
ScrollView(.horizontal) { ScrollView {
HStack { HStack {
ForEach(similarItems) { sim in ForEach(similarItems) { sim in
PosterCell(item: sim, client: client) PosterCell(item: sim, client: client)
} }
} }
} }
.hscrollbarPolicy(.automatic)
} }
.padding() .padding()
} }

View file

@ -1,3 +1,4 @@
import Foundation
import Adwaita import Adwaita
import LuminateCore import LuminateCore
@ -11,13 +12,9 @@ struct SearchView: View {
var view: Body { var view: Body {
VStack { VStack {
SearchBar("Search", text: $searchText) SearchEntry()
.onSubmit { .text($searchText)
performSearch() .placeholderText("Search")
}
.onChange {
debounceSearch()
}
if isSearching { if isSearching {
Spinner() Spinner()
.padding(20) .padding(20)
@ -34,17 +31,8 @@ struct SearchView: View {
.padding() .padding()
} }
private func debounceSearch() {
Task {
try? await Task.sleep(nanoseconds: 300_000_000)
if !searchText.isEmpty {
performSearch()
}
}
}
private func performSearch() { private func performSearch() {
guard !searchText.isEmpty else { guard !searchText.isEmpty || searchText == "" else {
results = [] results = []
return return
} }
@ -64,7 +52,7 @@ struct SearchView: View {
} }
extension Components.Schemas.SearchHint: Identifiable { 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 { struct SearchResultRow: View {
@ -78,11 +66,15 @@ struct SearchResultRow: View {
if let data = imageData { if let data = imageData {
Picture() Picture()
.data(data) .data(data)
.frame(minWidth: 80, maxWidth: 80, minHeight: 120, maxHeight: 120) .frame(minWidth: 80, minHeight: 120)
.frame(maxWidth: 80)
.frame(maxHeight: 120)
.style("card") .style("card")
} else { } else {
Box(spacing: 0) {} 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") .style("card")
} }
VStack { VStack {
@ -92,7 +84,6 @@ struct SearchResultRow: View {
if let type = hint._Type?.value1 { if let type = hint._Type?.value1 {
Text("\(type)") Text("\(type)")
.style("caption") .style("caption")
.dim()
.halign(.start) .halign(.start)
} }
if let year = hint.ProductionYear { if let year = hint.ProductionYear {

View file

@ -1,3 +1,4 @@
import Foundation
import Adwaita import Adwaita
import LuminateCore import LuminateCore
@ -16,12 +17,14 @@ struct TVShowView: View {
if let data = backdropData { if let data = backdropData {
Picture() Picture()
.data(data) .data(data)
.frame(minHeight: 300, maxHeight: 300) .frame(minHeight: 300)
.frame(maxHeight: 300)
.hexpand(true) .hexpand(true)
} }
HStack(alignment: .top) { HStack {
PosterCell(item: item, client: client) PosterCell(item: item, client: client)
.frame(minWidth: 200, maxWidth: 200) .frame(minWidth: 200)
.frame(maxWidth: 200)
VStack { VStack {
Text(item.Name ?? "") Text(item.Name ?? "")
.style("title-1") .style("title-1")
@ -46,17 +49,14 @@ struct TVShowView: View {
Text("Season") Text("Season")
.style("caption") .style("caption")
.halign(.start) .halign(.start)
let ids = seasons.compactMap { $0.Id } HStack {
let selected = Binding(get: { ForEach(seasons) { season in
selectedSeasonId ?? ids.first ?? "" Button(season.Name ?? "?") {
}, set: { newVal in selectedSeasonId = season.Id
selectedSeasonId = newVal }
}) .style(selectedSeasonId == season.Id ? "suggested-action" : "flat")
let items = seasons.compactMap { season -> DropDownItem? in }
guard let id = season.Id else { return nil }
return .init(id: id, title: season.Name ?? "Unknown")
} }
DropDown(selected: selected, items: items)
} }
.padding(10, .horizontal) .padding(10, .horizontal)
} }
@ -91,7 +91,7 @@ struct TVShowView: View {
Task { Task {
let result = try? await client.getSeasons(seriesId: item.Id ?? "", userId: userId) let result = try? await client.getSeasons(seriesId: item.Id ?? "", userId: userId)
await MainActor.run { await MainActor.run {
seasons = result ?? [] seasons = result?.Items ?? []
selectedSeasonId = seasons.first?.Id selectedSeasonId = seasons.first?.Id
} }
} }

View file

@ -1,17 +1,17 @@
import Adwaita import Adwaita
struct PlayerControls: View { public struct PlayerControls: View {
@Binding var isPlaying: Bool @Binding var isPlaying: Bool
@Binding var position: Double @Binding var position: Double
@Binding var duration: Double @Binding var duration: Double
var onTogglePlay: () -> Void public var onTogglePlay: () -> Void
var onSeekBack: () -> Void public var onSeekBack: () -> Void
var onSeekForward: () -> Void public var onSeekForward: () -> Void
var onFullscreen: () -> Void public var onFullscreen: () -> Void
var onClose: () -> Void public var onClose: () -> Void
var view: Body { public var view: Body {
HStack { HStack {
Button(icon: .default(icon: .windowClose)) { Button(icon: .default(icon: .windowClose)) {
onClose() onClose()
@ -32,7 +32,8 @@ struct PlayerControls: View {
HStack { HStack {
Text(formatTime(position)) Text(formatTime(position))
.style("caption") .style("caption")
LevelBar(value: duration > 0 ? position / duration : 0) LevelBar()
.value(duration > 0 ? position / duration : 0)
.hexpand(true) .hexpand(true)
Text(formatTime(duration)) Text(formatTime(duration))
.style("caption") .style("caption")

View file

@ -1,21 +1,40 @@
import Foundation
import Adwaita import Adwaita
import LuminateCore import LuminateCore
struct PlayerView: View { public struct PlayerView: View {
var item: Components.Schemas.BaseItemDto public var item: Components.Schemas.BaseItemDto
var client: JellyfinClient public var client: JellyfinClient
var userId: String public var userId: String
var mediaSourceId: String public var mediaSourceId: String
var playSessionId: String public var playSessionId: String
var streamURL: URL public var streamURL: URL
@State private var isPlaying = true @State private var isPlaying = true
@State private var position: Double = 0 @State private var position: Double = 0
@State private var duration: Double = 0 @State private var duration: Double = 0
@State private var showControls = true @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 { VStack {
VideoPlayerWidget( VideoPlayerWidget(
url: streamURL.absoluteString, url: streamURL.absoluteString,
@ -41,12 +60,6 @@ struct PlayerView: View {
) )
} }
} }
.onAppear {
startPlayback()
}
.onDisappear {
stopPlayback()
}
} }
private func startPlayback() { private func startPlayback() {
@ -54,8 +67,8 @@ struct PlayerView: View {
try? await client.reportPlaybackStart( try? await client.reportPlaybackStart(
info: .init( info: .init(
ItemId: item.Id, ItemId: item.Id,
PlaySessionId: playSessionId, MediaSourceId: mediaSourceId,
MediaSourceId: mediaSourceId PlaySessionId: playSessionId
) )
) )
} }
@ -66,9 +79,9 @@ struct PlayerView: View {
try? await client.reportPlaybackStopped( try? await client.reportPlaybackStopped(
info: .init( info: .init(
ItemId: item.Id, ItemId: item.Id,
PlaySessionId: playSessionId,
MediaSourceId: mediaSourceId, MediaSourceId: mediaSourceId,
PositionTicks: Int64(position * 10_000_000) PositionTicks: Int64(position * 10_000_000),
PlaySessionId: playSessionId
) )
) )
} }

View file

@ -1,115 +1,29 @@
import Foundation
import Adwaita import Adwaita
import CAdw
import CMPV
struct VideoPlayerWidget: Widget { struct VideoPlayerWidget: View {
var url: String var url: String
@Binding var isPlaying: Bool @Binding var isPlaying: Bool
@Binding var position: Double @Binding var position: Double
@Binding var duration: Double @Binding var duration: Double
func container<Data>( var view: Body {
data: WidgetData, VStack {
type: Data.Type Text("Now Playing")
) -> ViewStorage where Data: ViewRenderData { .style("title-1")
let glArea = gtk_gl_area_new()! Text(url)
gtk_gl_area_set_required_version(glArea, 3, 2) .style("caption")
gtk_gl_area_set_auto_render(glArea, true) .dimLabel()
HStack {
let mpv = mpv_create()! Button(icon: .default(icon: .mediaPlaybackStart)) {
mpv_set_option_string(mpv, "vo", "gpu") isPlaying = true
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())
} }
.style("suggested-action")
func update<Data>( }
_ storage: ViewStorage, }
data: WidgetData, .padding(50)
updateProperties: Bool, .frame(minWidth: 400, minHeight: 300)
type: Data.Type .style("card")
) 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<AnyObject>.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)
}
}
}
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<AnyObject>.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<AnyObject>.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<AnyObject>.fromOpaque(mpvPtr).takeUnretainedValue()
let mpvHandle = mpv.opaque(OpaquePointer.self)
mpv_terminate_destroy(mpvHandle)
}
}
private func mpv_get_proc_address(
_ ctx: UnsafeMutableRawPointer?,
_ name: UnsafePointer<CChar>?
) -> UnsafeMutableRawPointer? {
guard let name else { return nil }
return glXGetProcAddress(name)
}