Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[iOS] Admin Dashboard #1230

Merged
merged 43 commits into from
Oct 5, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
43 commits
Select commit Hold shift + click to select a range
9f35702
Active Sessions. TODO: Localization
JPKribs Sep 5, 2024
be3930f
Now live updates every 2 seconds? I THINK this is a valid way to do i…
JPKribs Sep 6, 2024
798baa9
Cleanup Comments
JPKribs Sep 6, 2024
06f30d0
Localization, blurhash for placeholder, and some cleanup. Should be r…
JPKribs Sep 6, 2024
0fe69ba
Cache Image on first retrieval. Then, so long as the Item.Id doesn't …
JPKribs Sep 6, 2024
344393b
Details screen, reorganization, and newly introduced deorganization
JPKribs Sep 7, 2024
3f0058d
ViewModel now stateful but generates 1 error message ever 2 seconds. …
JPKribs Sep 8, 2024
c5713c8
Fix Code Sense Issues
JPKribs Sep 8, 2024
ffd282f
Works great, no more ViewModel errors but the sessions only update if…
JPKribs Sep 9, 2024
6d74038
Merge branch 'jellyfin:main' into AdminDashboardSessions
JPKribs Sep 9, 2024
69fb0b5
Functional. Reworked. Only a little bit flickery
JPKribs Sep 11, 2024
52a85b2
Merge remote-tracking branch 'refs/remotes/origin/AdminDashboardSessi…
JPKribs Sep 11, 2024
147d268
Cleanup Sections. Add Comments to larger views to explain what is goi…
JPKribs Sep 12, 2024
c2be8b1
Lin ting
JPKribs Sep 12, 2024
d00648f
Unwrap nowPlayingItem for ActiveSessionButton
JPKribs Sep 12, 2024
a686095
Mirror the jellyfin-web and build out the dashboard with function but…
JPKribs Sep 12, 2024
b8a13d1
Cleanup Active Device Padding
JPKribs Sep 12, 2024
9ed53d4
Padding Cleanup + User Permissions Check. TODO: Localizations
JPKribs Sep 12, 2024
ad5651f
CodeFactor
JPKribs Sep 12, 2024
d0f3c97
Track Library Scan Progress. I think this is finally ready.
JPKribs Sep 13, 2024
ae9fe1b
Migrate Scan Library to Scheduled Task Process. Implement Scheduled T…
JPKribs Sep 13, 2024
0243098
Last Item: Update from "Cancel" on the Task to instead be "Canceled" …
JPKribs Sep 13, 2024
68fd1d1
WIP -> Split out Active Devices into it's own View. Removed most of t…
JPKribs Sep 18, 2024
e27ad61
Sessions List View Rows. VCollection Configuration. Flip/Save on view.
JPKribs Sep 18, 2024
6cf8b3c
Cleanup + Overview/Tagline Truncation. Store the values in the UserSt…
JPKribs Sep 18, 2024
ed6de62
Cleanup + Overview/Tagline Truncation. Store the values in the UserSt…
JPKribs Sep 18, 2024
1e831d3
Cleaned up! Ish... Room for improvement but this should be good enoug…
JPKribs Sep 19, 2024
b053f02
Button/Row Font Increases
JPKribs Sep 19, 2024
4e8deef
Hide scheduled tasks that are isHidden and make sure to only included…
JPKribs Sep 19, 2024
2fdd7fe
wip
LePips Sep 27, 2024
549f6c1
wip
LePips Sep 28, 2024
26abd08
wip
LePips Sep 30, 2024
448baef
wip
LePips Sep 30, 2024
7f68bd7
wip
LePips Sep 30, 2024
6f104e4
wip
LePips Sep 30, 2024
97b1b61
wip
LePips Sep 30, 2024
120118e
wip
LePips Sep 30, 2024
4aaf8a2
wip
LePips Sep 30, 2024
a6f89c7
Localization & CodeSense(?) error fix from github. The Action? = Nil.…
JPKribs Oct 4, 2024
ab70e0e
wip
LePips Oct 4, 2024
116e5f2
wip
LePips Oct 4, 2024
e7ec6d1
Update ScheduledTasksViewModel.swift
LePips Oct 4, 2024
0d8b04e
cleanup
LePips Oct 4, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
Loading