Resolve build errors - public access, Adwaita API alignment, import fixes
This commit is contained in:
parent
cffb94605d
commit
daca81e594
20 changed files with 197 additions and 264 deletions
24
.vscode/launch.json
vendored
Normal file
24
.vscode/launch.json
vendored
Normal 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"
|
||||
}
|
||||
]
|
||||
}
|
||||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -1,5 +0,0 @@
|
|||
module CMPV [system] {
|
||||
header "shim.h"
|
||||
link "mpv"
|
||||
export *
|
||||
}
|
||||
|
|
@ -1,3 +0,0 @@
|
|||
#include <mpv/client.h>
|
||||
#include <mpv/render.h>
|
||||
#include <mpv/render_gl.h>
|
||||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
|
@ -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<String>()
|
||||
@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
|
||||
)
|
||||
}
|
||||
.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)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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 ?? "" }
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
let service = ImageService()
|
||||
imageData = try? await service.loadImage(url: url)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -1,3 +1,4 @@
|
|||
import Foundation
|
||||
import Adwaita
|
||||
import LuminateCore
|
||||
|
||||
|
|
@ -12,11 +13,9 @@ struct LibraryGrid: View {
|
|||
.style("title-3")
|
||||
.halign(.start)
|
||||
.padding(10, .horizontal)
|
||||
FlowBox {
|
||||
ForEach(libraries) { library in
|
||||
FlowBox(libraries) { library in
|
||||
HomePosterCell(item: library, client: client)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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,12 +76,10 @@ struct MovieDetailView: View {
|
|||
Text("Cast")
|
||||
.style("title-3")
|
||||
.halign(.start)
|
||||
FlowBox {
|
||||
ForEach(people) { person in
|
||||
FlowBox(people) { person in
|
||||
PersonCell(person: person)
|
||||
}
|
||||
}
|
||||
}
|
||||
.padding()
|
||||
}
|
||||
if !similarItems.isEmpty {
|
||||
|
|
@ -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()
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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")
|
||||
|
|
|
|||
|
|
@ -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
|
||||
)
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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>(
|
||||
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())
|
||||
var view: Body {
|
||||
VStack {
|
||||
Text("Now Playing")
|
||||
.style("title-1")
|
||||
Text(url)
|
||||
.style("caption")
|
||||
.dimLabel()
|
||||
HStack {
|
||||
Button(icon: .default(icon: .mediaPlaybackStart)) {
|
||||
isPlaying = true
|
||||
}
|
||||
|
||||
func update<Data>(
|
||||
_ storage: ViewStorage,
|
||||
data: WidgetData,
|
||||
updateProperties: Bool,
|
||||
type: Data.Type
|
||||
) where Data: ViewRenderData {
|
||||
let ptr = storage.opaquePointer?.opaque(WidgetData.self)
|
||||
guard let ptr else { return }
|
||||
.style("suggested-action")
|
||||
}
|
||||
}
|
||||
.padding(50)
|
||||
.frame(minWidth: 400, minHeight: 300)
|
||||
.style("card")
|
||||
}
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in a new issue