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")
|
.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",
|
||||||
|
|
|
||||||
|
|
@ -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 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)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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",
|
||||||
|
|
|
||||||
|
|
@ -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 ?? "" }
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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 {
|
||||||
|
|
|
||||||
|
|
@ -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)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
|
||||||
|
|
@ -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)
|
||||||
|
|
|
||||||
|
|
@ -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)
|
||||||
|
|
|
||||||
|
|
@ -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 {
|
||||||
|
|
|
||||||
|
|
@ -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()
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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 {
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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")
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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,
|
|
||||||
updateProperties: Bool,
|
|
||||||
type: Data.Type
|
|
||||||
) where Data: ViewRenderData {
|
|
||||||
let ptr = storage.opaquePointer?.opaque(WidgetData.self)
|
|
||||||
guard let ptr else { return }
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
.padding(50)
|
||||||
private func c_realize(widget: UnsafeMutableRawPointer?, data: UnsafeMutableRawPointer?) {
|
.frame(minWidth: 400, minHeight: 300)
|
||||||
guard let widget, let data else { return }
|
.style("card")
|
||||||
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