Skip to content

Commit

Permalink
[iOS] Admin Dashboard (jellyfin#1230)
Browse files Browse the repository at this point in the history
  • Loading branch information
JPKribs authored Oct 5, 2024
1 parent 4cba762 commit bc9eaca
Show file tree
Hide file tree
Showing 54 changed files with 2,659 additions and 77 deletions.
82 changes: 69 additions & 13 deletions Shared/Components/ProgressBar.swift
Original file line number Diff line number Diff line change
Expand Up @@ -8,24 +8,80 @@

import SwiftUI

// TODO: see if animation is correct here or should be in caller views

// TODO: remove and replace with below
struct ProgressBar: View {

@State
private var contentSize: CGSize = .zero

let progress: CGFloat

var body: some View {
ZStack(alignment: .leading) {
Capsule()
.foregroundColor(.secondary)
.opacity(0.2)

Capsule()
.mask(alignment: .leading) {
Rectangle()
.scaleEffect(x: progress, anchor: .leading)
Capsule()
.foregroundStyle(.secondary)
.opacity(0.2)
.overlay(alignment: .leading) {
Capsule()
.mask(alignment: .leading) {
Rectangle()
}
.frame(width: contentSize.width * progress)
.foregroundStyle(.primary)
}
.trackingSize($contentSize)
}
}

// TODO: fix capsule with low progress

extension ProgressViewStyle where Self == PlaybackProgressViewStyle {

static var playback: Self { .init(secondaryProgress: nil) }

static func playback(secondaryProgress: Double?) -> Self {
.init(secondaryProgress: secondaryProgress)
}
}

struct PlaybackProgressViewStyle: ProgressViewStyle {

@State
private var contentSize: CGSize = .zero

let secondaryProgress: Double?

func makeBody(configuration: Configuration) -> some View {
Capsule()
.foregroundStyle(.secondary)
.opacity(0.2)
.overlay(alignment: .leading) {
ZStack(alignment: .leading) {

if let secondaryProgress {
Capsule()
.mask(alignment: .leading) {
Rectangle()
}
.frame(width: contentSize.width * clamp(secondaryProgress, min: 0, max: 1))
.foregroundStyle(.tertiary)
}

Capsule()
.mask(alignment: .leading) {
Rectangle()
}
.frame(width: contentSize.width * (configuration.fractionCompleted ?? 0))
.foregroundStyle(.primary)
}
}
.animation(.linear(duration: 0.1), value: progress)
}
.trackingSize($contentSize)
}
}

// #Preview {
// ProgressView(value: 0.3)
// .progressViewStyle(.SwiftfinLinear(secondaryProgress: 0.3))
// .frame(height: 8)
// .padding(.horizontal, 10)
// .foregroundStyle(.primary, .secondary, .orange)
// }
26 changes: 20 additions & 6 deletions Shared/Components/TextPairView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -13,17 +13,17 @@ import SwiftUI

struct TextPairView: View {

let leading: String
let trailing: String
private let leading: Text
private let trailing: Text

var body: some View {
HStack {
Text(leading)
leading
.foregroundColor(.primary)

Spacer()

Text(trailing)
trailing
.foregroundColor(.secondary)
}
}
Expand All @@ -33,8 +33,22 @@ extension TextPairView {

init(_ textPair: TextPair) {
self.init(
leading: textPair.title,
trailing: textPair.subtitle
leading: Text(textPair.title),
trailing: Text(textPair.subtitle)
)
}

init(leading: String, trailing: String) {
self.init(
leading: Text(leading),
trailing: Text(trailing)
)
}

init(_ title: String, value: @autoclosure () -> Text) {
self.init(
leading: Text(title),
trailing: value()
)
}
}
56 changes: 54 additions & 2 deletions Shared/Coordinators/SettingsCoordinator.swift
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
// Copyright (c) 2024 Jellyfin & Jellyfin Contributors
//

import JellyfinAPI
import PulseUI
import Stinsen
import SwiftUI
Expand Down Expand Up @@ -43,12 +44,27 @@ final class SettingsCoordinator: NavigationCoordinatable {
@Route(.push)
var indicatorSettings = makeIndicatorSettings
@Route(.push)
var serverDetail = makeServerDetail
var serverConnection = makeServerConnection
@Route(.push)
var videoPlayerSettings = makeVideoPlayerSettings
@Route(.push)
var customDeviceProfileSettings = makeCustomDeviceProfileSettings

@Route(.push)
var userDashboard = makeUserDashboard
@Route(.push)
var activeSessions = makeActiveSessions
@Route(.push)
var activeDeviceDetails = makeActiveDeviceDetails
@Route(.modal)
var itemOverviewView = makeItemOverviewView
@Route(.push)
var tasks = makeTasks
@Route(.push)
var editScheduledTask = makeEditScheduledTask
@Route(.push)
var serverLogs = makeServerLogs

@Route(.modal)
var editCustomDeviceProfile = makeEditCustomDeviceProfile
@Route(.modal)
Expand Down Expand Up @@ -142,10 +158,46 @@ final class SettingsCoordinator: NavigationCoordinatable {
}

@ViewBuilder
func makeServerDetail(server: ServerState) -> some View {
func makeServerConnection(server: ServerState) -> some View {
EditServerView(server: server)
}

@ViewBuilder
func makeUserDashboard() -> some View {
UserDashboardView()
}

@ViewBuilder
func makeActiveSessions() -> some View {
ActiveSessionsView()
}

@ViewBuilder
func makeActiveDeviceDetails(box: BindingBox<SessionInfo?>) -> some View {
ActiveSessionDetailView(box: box)
}

func makeItemOverviewView(item: BaseItemDto) -> NavigationViewCoordinator<BasicNavigationViewCoordinator> {
NavigationViewCoordinator {
ItemOverviewView(item: item)
}
}

@ViewBuilder
func makeTasks() -> some View {
ScheduledTasksView()
}

@ViewBuilder
func makeEditScheduledTask(observer: ServerTaskObserver) -> some View {
EditScheduledTaskView(observer: observer)
}

@ViewBuilder
func makeServerLogs() -> some View {
ServerLogsView()
}

func makeItemFilterDrawerSelector(selection: Binding<[ItemFilterType]>) -> some View {
OrderedSectionSelectorView(selection: selection, sources: ItemFilterType.allCases)
.navigationTitle(L10n.filters)
Expand Down
30 changes: 30 additions & 0 deletions Shared/Extensions/FormatStyle.swift
Original file line number Diff line number Diff line change
Expand Up @@ -22,3 +22,33 @@ extension FormatStyle where Self == HourMinuteFormatStyle {

static var hourMinute: HourMinuteFormatStyle { HourMinuteFormatStyle() }
}

struct RunTimeFormatStyle: FormatStyle {

private var negate: Bool = false

var negated: RunTimeFormatStyle {
mutating(\.negate, with: true)
}

func format(_ value: Int) -> String {
let hours = value / 3600
let minutes = (value % 3600) / 60
let seconds = value % 3600 % 60

let hourText = hours > 0 ? String(hours).appending(":") : ""
let minutesText = hours > 0 ? String(minutes).leftPad(maxWidth: 2, with: "0").appending(":") : String(minutes)
.appending(":")
let secondsText = String(seconds).leftPad(maxWidth: 2, with: "0")

return hourText
.appending(minutesText)
.appending(secondsText)
.prepending("-", if: negate)
}
}

extension FormatStyle where Self == RunTimeFormatStyle {

static var runtime: RunTimeFormatStyle { RunTimeFormatStyle() }
}
11 changes: 11 additions & 0 deletions Shared/Extensions/JellyfinAPI/BaseItemDto/BaseItemDto+Poster.swift
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,8 @@ extension BaseItemDto: Poster {

var systemImage: String {
switch type {
case .audio, .musicAlbum:
"music.note"
case .boxSet:
"film.stack"
case .channel, .tvChannel, .liveTvChannel, .program:
Expand Down Expand Up @@ -93,4 +95,13 @@ extension BaseItemDto: Poster {
[imageSource(.backdrop, maxWidth: maxWidth)]
}
}

func squareImageSources(maxWidth: CGFloat?) -> [ImageSource] {
switch type {
case .audio, .musicAlbum:
[imageSource(.primary, maxWidth: maxWidth)]
default:
[]
}
}
}
12 changes: 12 additions & 0 deletions Shared/Extensions/JellyfinAPI/BaseItemDto/BaseItemDto.swift
Original file line number Diff line number Diff line change
Expand Up @@ -253,4 +253,16 @@ extension BaseItemDto {

return L10n.play
}

var parentTitle: String? {
switch type {
case .audio:
album
case .episode:
seriesName
case .program: nil
default:
nil
}
}
}
6 changes: 5 additions & 1 deletion Shared/Extensions/JellyfinAPI/JellyfinClient.swift
Original file line number Diff line number Diff line change
Expand Up @@ -13,14 +13,18 @@ import UIKit

extension JellyfinClient {

func fullURL<T>(with request: Request<T>) -> URL? {
func fullURL<T>(with request: Request<T>, queryAPIKey: Bool = false) -> URL? {

guard let path = request.url?.path else { return configuration.url }
guard let fullPath = fullURL(with: path) else { return nil }
guard var components = URLComponents(string: fullPath.absoluteString) else { return nil }

components.queryItems = request.query?.map { URLQueryItem(name: $0.0, value: $0.1) } ?? []

if queryAPIKey, let accessToken {
components.queryItems?.append(.init(name: "api_key", value: accessToken))
}

return components.url ?? fullPath
}

Expand Down
25 changes: 25 additions & 0 deletions Shared/Extensions/JellyfinAPI/PlayMethod.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
//
// Swiftfin is subject to the terms of the Mozilla Public
// License, v2.0. If a copy of the MPL was not distributed with this
// file, you can obtain one at https://mozilla.org/MPL/2.0/.
//
// Copyright (c) 2024 Jellyfin & Jellyfin Contributors
//

import Foundation
import JellyfinAPI
import SwiftUI

extension PlayMethod: Displayable {

var displayTitle: String {
switch self {
case .transcode:
return L10n.transcode
case .directStream:
return L10n.directStream
case .directPlay:
return L10n.directPlay
}
}
}
18 changes: 18 additions & 0 deletions Shared/Extensions/JellyfinAPI/PlayerStateInfo.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
//
// Swiftfin is subject to the terms of the Mozilla Public
// License, v2.0. If a copy of the MPL was not distributed with this
// file, you can obtain one at https://mozilla.org/MPL/2.0/.
//
// Copyright (c) 2024 Jellyfin & Jellyfin Contributors
//

import Foundation
import JellyfinAPI

extension PlayerStateInfo {

var positionSeconds: Int? {
guard let positionTicks else { return nil }
return positionTicks / 10_000_000
}
}
26 changes: 26 additions & 0 deletions Shared/Extensions/JellyfinAPI/TaskCompletionStatus.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
//
// Swiftfin is subject to the terms of the Mozilla Public
// License, v2.0. If a copy of the MPL was not distributed with this
// file, you can obtain one at https://mozilla.org/MPL/2.0/.
//
// Copyright (c) 2024 Jellyfin & Jellyfin Contributors
//

import Foundation
import JellyfinAPI

extension TaskCompletionStatus: Displayable {

var displayTitle: String {
switch self {
case .completed:
return L10n.taskCompleted
case .failed:
return L10n.taskFailed
case .cancelled:
return L10n.taskCancelled
case .aborted:
return L10n.taskAborted
}
}
}
Loading

0 comments on commit bc9eaca

Please sign in to comment.