swiftformat

This commit is contained in:
Ethan Pippin 2022-01-10 12:28:03 -07:00
parent 961e639970
commit 4298062ca3
223 changed files with 16548 additions and 15983 deletions

View File

@ -1,18 +1,49 @@
# version: 0.47.5 # version: 0.47.5
--indent 4 #indent --swiftversion 5.5
--self init-only # redundantSelf
--semicolons never # semicolons --indent tab
--stripunusedargs closure-only # unusedArguments --tabwidth 4
--maxwidth 140 #wrap --xcodeindentation enabled
--assetliterals visual-width #wrap --self init-only
--wraparguments after-first # wrapArguments --semicolons never
--wrapparameters after-first # wrapArguments --stripunusedargs closure-only
--wrapcollections before-first # wrapArguments --maxwidth 140
--wrapconditions after-first # wrapArguments --assetliterals visual-width
--funcattributes prev-line # wrapAttributes --wraparguments after-first
--typeattributes prev-line # wrapAttributes --wrapparameters after-first
--varattributes prev-line # wrapAttributes --wrapcollections before-first
--wrapconditions after-first
--funcattributes prev-line
--typeattributes prev-line
--varattributes prev-line
--trailingclosures
--shortoptionals "always"
--enable isEmpty, \
leadingDelimiters, \
wrapEnumCases, \
typeSugar, \
void, \
trailingSpace, \
spaceInsideParens, \
spaceInsideGenerics, \
spaceInsideComments, \
spaceInsideBrackets, \
spaceInsideBraces, \
blankLinesAroundMark, \
redundantLet, \
redundantInit, \
blankLinesAroundMark
--disable strongOutlets, \
yodaConditions, \
blankLinesAtStartOfScope,\
andOperator, \
redundantFileprivate, \
redundantSelf
--exclude Pods
--header "\nSwiftfin is subject to the terms of the Mozilla Public\nLicense, v2.0. If a copy of the MPL was not distributed with this\nfile, you can obtain one at https://mozilla.org/MPL/2.0/.\n\nCopyright (c) {year} Jellyfin & Jellyfin Contributors\n"
--enable isEmpty
--disable strongOutlets,yodaConditions

View File

@ -1,11 +1,10 @@
// //
/* // Swiftfin is subject to the terms of the Mozilla Public
* SwiftFin is subject to the terms of the Mozilla Public // License, v2.0. If a copy of the MPL was not distributed with this
* 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/.
* file, you can obtain one at https://mozilla.org/MPL/2.0/. //
* // Copyright (c) 2022 Jellyfin & Jellyfin Contributors
* Copyright 2021 Aiden Vigue & Jellyfin Contributors //
*/
import Foundation import Foundation
import Stinsen import Stinsen
@ -15,9 +14,11 @@ final class BasicAppSettingsCoordinator: NavigationCoordinatable {
let stack = NavigationStack(initial: \BasicAppSettingsCoordinator.start) let stack = NavigationStack(initial: \BasicAppSettingsCoordinator.start)
@Root var start = makeStart @Root
var start = makeStart
@ViewBuilder func makeStart() -> some View { @ViewBuilder
func makeStart() -> some View {
BasicAppSettingsView(viewModel: BasicAppSettingsViewModel()) BasicAppSettingsView(viewModel: BasicAppSettingsViewModel())
} }
} }

View File

@ -1,11 +1,10 @@
// //
/* // Swiftfin is subject to the terms of the Mozilla Public
* SwiftFin is subject to the terms of the Mozilla Public // License, v2.0. If a copy of the MPL was not distributed with this
* 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/.
* file, you can obtain one at https://mozilla.org/MPL/2.0/. //
* // Copyright (c) 2022 Jellyfin & Jellyfin Contributors
* Copyright 2021 Aiden Vigue & Jellyfin Contributors //
*/
import Foundation import Foundation
import Stinsen import Stinsen
@ -15,14 +14,17 @@ final class ConnectToServerCoodinator: NavigationCoordinatable {
let stack = NavigationStack(initial: \ConnectToServerCoodinator.start) let stack = NavigationStack(initial: \ConnectToServerCoodinator.start)
@Root var start = makeStart @Root
@Route(.push) var userSignIn = makeUserSignIn var start = makeStart
@Route(.push)
var userSignIn = makeUserSignIn
func makeUserSignIn(server: SwiftfinStore.State.Server) -> UserSignInCoordinator { func makeUserSignIn(server: SwiftfinStore.State.Server) -> UserSignInCoordinator {
return UserSignInCoordinator(viewModel: .init(server: server)) UserSignInCoordinator(viewModel: .init(server: server))
} }
@ViewBuilder func makeStart() -> some View { @ViewBuilder
func makeStart() -> some View {
ConnectToServerView(viewModel: ConnectToServerViewModel()) ConnectToServerView(viewModel: ConnectToServerViewModel())
} }
} }

View File

@ -1,11 +1,10 @@
// //
/* // Swiftfin is subject to the terms of the Mozilla Public
* SwiftFin is subject to the terms of the Mozilla Public // License, v2.0. If a copy of the MPL was not distributed with this
* 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/.
* file, you can obtain one at https://mozilla.org/MPL/2.0/. //
* // Copyright (c) 2022 Jellyfin & Jellyfin Contributors
* Copyright 2021 Aiden Vigue & Jellyfin Contributors //
*/
import Foundation import Foundation
import Stinsen import Stinsen
@ -17,9 +16,11 @@ final class FilterCoordinator: NavigationCoordinatable {
let stack = NavigationStack(initial: \FilterCoordinator.start) let stack = NavigationStack(initial: \FilterCoordinator.start)
@Root var start = makeStart @Root
var start = makeStart
@Binding var filters: LibraryFilters @Binding
var filters: LibraryFilters
var enabledFilterType: [FilterType] var enabledFilterType: [FilterType]
var parentId: String = "" var parentId: String = ""
@ -29,7 +30,8 @@ final class FilterCoordinator: NavigationCoordinatable {
self.parentId = parentId self.parentId = parentId
} }
@ViewBuilder func makeStart() -> some View { @ViewBuilder
func makeStart() -> some View {
LibraryFilterView(filters: $filters, enabledFilterType: enabledFilterType, parentId: parentId) LibraryFilterView(filters: $filters, enabledFilterType: enabledFilterType, parentId: parentId)
} }
} }

View File

@ -1,11 +1,10 @@
// //
/* // Swiftfin is subject to the terms of the Mozilla Public
* SwiftFin is subject to the terms of the Mozilla Public // License, v2.0. If a copy of the MPL was not distributed with this
* 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/.
* file, you can obtain one at https://mozilla.org/MPL/2.0/. //
* // Copyright (c) 2022 Jellyfin & Jellyfin Contributors
* Copyright 2021 Aiden Vigue & Jellyfin Contributors //
*/
import Foundation import Foundation
import JellyfinAPI import JellyfinAPI
@ -16,12 +15,18 @@ final class HomeCoordinator: NavigationCoordinatable {
let stack = NavigationStack(initial: \HomeCoordinator.start) let stack = NavigationStack(initial: \HomeCoordinator.start)
@Root var start = makeStart @Root
@Route(.modal) var settings = makeSettings var start = makeStart
@Route(.push) var library = makeLibrary @Route(.modal)
@Route(.push) var item = makeItem var settings = makeSettings
@Route(.modal) var modalItem = makeModalItem @Route(.push)
@Route(.modal) var modalLibrary = makeModalLibrary var library = makeLibrary
@Route(.push)
var item = makeItem
@Route(.modal)
var modalItem = makeModalItem
@Route(.modal)
var modalLibrary = makeModalLibrary
func makeSettings() -> NavigationViewCoordinator<SettingsCoordinator> { func makeSettings() -> NavigationViewCoordinator<SettingsCoordinator> {
NavigationViewCoordinator(SettingsCoordinator()) NavigationViewCoordinator(SettingsCoordinator())
@ -36,14 +41,15 @@ final class HomeCoordinator: NavigationCoordinatable {
} }
func makeModalItem(item: BaseItemDto) -> NavigationViewCoordinator<ItemCoordinator> { func makeModalItem(item: BaseItemDto) -> NavigationViewCoordinator<ItemCoordinator> {
return NavigationViewCoordinator(ItemCoordinator(item: item)) NavigationViewCoordinator(ItemCoordinator(item: item))
} }
func makeModalLibrary(params: LibraryCoordinatorParams) -> NavigationViewCoordinator<LibraryCoordinator> { func makeModalLibrary(params: LibraryCoordinatorParams) -> NavigationViewCoordinator<LibraryCoordinator> {
return NavigationViewCoordinator(LibraryCoordinator(viewModel: params.viewModel, title: params.title)) NavigationViewCoordinator(LibraryCoordinator(viewModel: params.viewModel, title: params.title))
} }
@ViewBuilder func makeStart() -> some View { @ViewBuilder
func makeStart() -> some View {
HomeView() HomeView()
} }
} }

View File

@ -1,11 +1,10 @@
// //
/* // Swiftfin is subject to the terms of the Mozilla Public
* SwiftFin is subject to the terms of the Mozilla Public // License, v2.0. If a copy of the MPL was not distributed with this
* 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/.
* file, you can obtain one at https://mozilla.org/MPL/2.0/. //
* // Copyright (c) 2022 Jellyfin & Jellyfin Contributors
* Copyright 2021 Aiden Vigue & Jellyfin Contributors //
*/
import Foundation import Foundation
import JellyfinAPI import JellyfinAPI
@ -16,11 +15,16 @@ final class ItemCoordinator: NavigationCoordinatable {
let stack = NavigationStack(initial: \ItemCoordinator.start) let stack = NavigationStack(initial: \ItemCoordinator.start)
@Root var start = makeStart @Root
@Route(.push) var item = makeItem var start = makeStart
@Route(.push) var library = makeLibrary @Route(.push)
@Route(.modal) var itemOverview = makeItemOverview var item = makeItem
@Route(.fullScreen) var videoPlayer = makeVideoPlayer @Route(.push)
var library = makeLibrary
@Route(.modal)
var itemOverview = makeItemOverview
@Route(.fullScreen)
var videoPlayer = makeVideoPlayer
let itemDto: BaseItemDto let itemDto: BaseItemDto
@ -44,7 +48,8 @@ final class ItemCoordinator: NavigationCoordinatable {
NavigationViewCoordinator(VideoPlayerCoordinator(viewModel: viewModel)) NavigationViewCoordinator(VideoPlayerCoordinator(viewModel: viewModel))
} }
@ViewBuilder func makeStart() -> some View { @ViewBuilder
func makeStart() -> some View {
ItemNavigationView(item: itemDto) ItemNavigationView(item: itemDto)
} }
} }

View File

@ -1,21 +1,21 @@
// //
/* // Swiftfin is subject to the terms of the Mozilla Public
* SwiftFin is subject to the terms of the Mozilla Public // License, v2.0. If a copy of the MPL was not distributed with this
* 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/.
* file, you can obtain one at https://mozilla.org/MPL/2.0/. //
* // Copyright (c) 2022 Jellyfin & Jellyfin Contributors
* Copyright 2021 Aiden Vigue & Jellyfin Contributors //
*/
import JellyfinAPI
import Stinsen import Stinsen
import SwiftUI import SwiftUI
import JellyfinAPI
final class ItemOverviewCoordinator: NavigationCoordinatable { final class ItemOverviewCoordinator: NavigationCoordinatable {
let stack = NavigationStack(initial: \ItemOverviewCoordinator.start) let stack = NavigationStack(initial: \ItemOverviewCoordinator.start)
@Root var start = makeStart @Root
var start = makeStart
let item: BaseItemDto let item: BaseItemDto
@ -23,7 +23,8 @@ final class ItemOverviewCoordinator: NavigationCoordinatable {
self.item = item self.item = item
} }
@ViewBuilder func makeStart() -> some View { @ViewBuilder
func makeStart() -> some View {
#if os(tvOS) #if os(tvOS)
EmptyView() EmptyView()
#else #else

View File

@ -1,11 +1,10 @@
// //
/* // Swiftfin is subject to the terms of the Mozilla Public
* SwiftFin is subject to the terms of the Mozilla Public // License, v2.0. If a copy of the MPL was not distributed with this
* 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/.
* file, you can obtain one at https://mozilla.org/MPL/2.0/. //
* // Copyright (c) 2022 Jellyfin & Jellyfin Contributors
* Copyright 2021 Aiden Vigue & Jellyfin Contributors //
*/
import Foundation import Foundation
import JellyfinAPI import JellyfinAPI
@ -18,11 +17,16 @@ final class LibraryCoordinator: NavigationCoordinatable {
let stack = NavigationStack(initial: \LibraryCoordinator.start) let stack = NavigationStack(initial: \LibraryCoordinator.start)
@Root var start = makeStart @Root
@Route(.push) var search = makeSearch var start = makeStart
@Route(.modal) var filter = makeFilter @Route(.push)
@Route(.push) var item = makeItem var search = makeSearch
@Route(.modal) var modalItem = makeModalItem @Route(.modal)
var filter = makeFilter
@Route(.push)
var item = makeItem
@Route(.modal)
var modalItem = makeModalItem
let viewModel: LibraryViewModel let viewModel: LibraryViewModel
let title: String let title: String
@ -32,7 +36,8 @@ final class LibraryCoordinator: NavigationCoordinatable {
self.title = title self.title = title
} }
@ViewBuilder func makeStart() -> some View { @ViewBuilder
func makeStart() -> some View {
LibraryView(viewModel: self.viewModel, title: title) LibraryView(viewModel: self.viewModel, title: title)
} }
@ -51,6 +56,6 @@ final class LibraryCoordinator: NavigationCoordinatable {
} }
func makeModalItem(item: BaseItemDto) -> NavigationViewCoordinator<ItemCoordinator> { func makeModalItem(item: BaseItemDto) -> NavigationViewCoordinator<ItemCoordinator> {
return NavigationViewCoordinator(ItemCoordinator(item: item)) NavigationViewCoordinator(ItemCoordinator(item: item))
} }
} }

View File

@ -1,11 +1,10 @@
// //
/* // Swiftfin is subject to the terms of the Mozilla Public
* SwiftFin is subject to the terms of the Mozilla Public // License, v2.0. If a copy of the MPL was not distributed with this
* 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/.
* file, you can obtain one at https://mozilla.org/MPL/2.0/. //
* // Copyright (c) 2022 Jellyfin & Jellyfin Contributors
* Copyright 2021 Aiden Vigue & Jellyfin Contributors //
*/
import Foundation import Foundation
import Stinsen import Stinsen
@ -15,9 +14,12 @@ final class LibraryListCoordinator: NavigationCoordinatable {
let stack = NavigationStack(initial: \LibraryListCoordinator.start) let stack = NavigationStack(initial: \LibraryListCoordinator.start)
@Root var start = makeStart @Root
@Route(.push) var search = makeSearch var start = makeStart
@Route(.push) var library = makeLibrary @Route(.push)
var search = makeSearch
@Route(.push)
var library = makeLibrary
let viewModel: LibraryListViewModel let viewModel: LibraryListViewModel

View File

@ -1,11 +1,10 @@
// //
/* // Swiftfin is subject to the terms of the Mozilla Public
* SwiftFin is subject to the terms of the Mozilla Public // License, v2.0. If a copy of the MPL was not distributed with this
* 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/.
* file, you can obtain one at https://mozilla.org/MPL/2.0/. //
* // Copyright (c) 2022 Jellyfin & Jellyfin Contributors
* Copyright 2021 Aiden Vigue & Jellyfin Contributors //
*/
import Foundation import Foundation
import JellyfinAPI import JellyfinAPI
@ -15,12 +14,15 @@ import SwiftUI
final class LiveTVChannelsCoordinator: NavigationCoordinatable { final class LiveTVChannelsCoordinator: NavigationCoordinatable {
let stack = NavigationStack(initial: \LiveTVChannelsCoordinator.start) let stack = NavigationStack(initial: \LiveTVChannelsCoordinator.start)
@Root var start = makeStart @Root
@Route(.modal) var modalItem = makeModalItem var start = makeStart
@Route(.fullScreen) var videoPlayer = makeVideoPlayer @Route(.modal)
var modalItem = makeModalItem
@Route(.fullScreen)
var videoPlayer = makeVideoPlayer
func makeModalItem(item: BaseItemDto) -> NavigationViewCoordinator<ItemCoordinator> { func makeModalItem(item: BaseItemDto) -> NavigationViewCoordinator<ItemCoordinator> {
return NavigationViewCoordinator(ItemCoordinator(item: item)) NavigationViewCoordinator(ItemCoordinator(item: item))
} }
func makeVideoPlayer(item: BaseItemDto) -> NavigationViewCoordinator<EmptyViewCoordinator> { func makeVideoPlayer(item: BaseItemDto) -> NavigationViewCoordinator<EmptyViewCoordinator> {
@ -37,7 +39,8 @@ final class LiveTVChannelsCoordinator: NavigationCoordinatable {
final class EmptyViewCoordinator: NavigationCoordinatable { final class EmptyViewCoordinator: NavigationCoordinatable {
let stack = NavigationStack(initial: \EmptyViewCoordinator.start) let stack = NavigationStack(initial: \EmptyViewCoordinator.start)
@Root var start = makeStart @Root
var start = makeStart
@ViewBuilder @ViewBuilder
func makeStart() -> some View { func makeStart() -> some View {

View File

@ -1,11 +1,10 @@
// //
/* // Swiftfin is subject to the terms of the Mozilla Public
* SwiftFin is subject to the terms of the Mozilla Public // License, v2.0. If a copy of the MPL was not distributed with this
* 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/.
* file, you can obtain one at https://mozilla.org/MPL/2.0/. //
* // Copyright (c) 2022 Jellyfin & Jellyfin Contributors
* Copyright 2021 Aiden Vigue & Jellyfin Contributors //
*/
import Foundation import Foundation
import JellyfinAPI import JellyfinAPI
@ -16,8 +15,10 @@ final class LiveTVProgramsCoordinator: NavigationCoordinatable {
let stack = NavigationStack(initial: \LiveTVProgramsCoordinator.start) let stack = NavigationStack(initial: \LiveTVProgramsCoordinator.start)
@Root var start = makeStart @Root
@Route(.fullScreen) var videoPlayer = makeVideoPlayer var start = makeStart
@Route(.fullScreen)
var videoPlayer = makeVideoPlayer
func makeVideoPlayer(item: BaseItemDto) -> NavigationViewCoordinator<EmptyViewCoordinator> { func makeVideoPlayer(item: BaseItemDto) -> NavigationViewCoordinator<EmptyViewCoordinator> {
// NavigationViewCoordinator(VideoPlayerCoordinator(item: item)) // NavigationViewCoordinator(VideoPlayerCoordinator(item: item))

View File

@ -1,32 +1,35 @@
// //
/* // Swiftfin is subject to the terms of the Mozilla Public
* SwiftFin is subject to the terms of the Mozilla Public // License, v2.0. If a copy of the MPL was not distributed with this
* 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/.
* file, you can obtain one at https://mozilla.org/MPL/2.0/. //
* // Copyright (c) 2022 Jellyfin & Jellyfin Contributors
* Copyright 2021 Aiden Vigue & Jellyfin Contributors //
*/
import Foundation import Foundation
import SwiftUI
import Stinsen import Stinsen
import SwiftUI
final class LiveTVTabCoordinator: TabCoordinatable { final class LiveTVTabCoordinator: TabCoordinatable {
var child = TabChild(startingItems: [ var child = TabChild(startingItems: [
\LiveTVTabCoordinator.programs, \LiveTVTabCoordinator.programs,
\LiveTVTabCoordinator.channels, \LiveTVTabCoordinator.channels,
\LiveTVTabCoordinator.home \LiveTVTabCoordinator.home,
]) ])
@Route(tabItem: makeProgramsTab) var programs = makePrograms @Route(tabItem: makeProgramsTab)
@Route(tabItem: makeChannelsTab) var channels = makeChannels var programs = makePrograms
@Route(tabItem: makeHomeTab) var home = makeHome @Route(tabItem: makeChannelsTab)
var channels = makeChannels
@Route(tabItem: makeHomeTab)
var home = makeHome
func makePrograms() -> NavigationViewCoordinator<LiveTVProgramsCoordinator> { func makePrograms() -> NavigationViewCoordinator<LiveTVProgramsCoordinator> {
return NavigationViewCoordinator(LiveTVProgramsCoordinator()) NavigationViewCoordinator(LiveTVProgramsCoordinator())
} }
@ViewBuilder func makeProgramsTab(isActive: Bool) -> some View { @ViewBuilder
func makeProgramsTab(isActive: Bool) -> some View {
HStack { HStack {
Image(systemName: "tv") Image(systemName: "tv")
Text("Programs") Text("Programs")
@ -34,10 +37,11 @@ final class LiveTVTabCoordinator: TabCoordinatable {
} }
func makeChannels() -> NavigationViewCoordinator<LiveTVChannelsCoordinator> { func makeChannels() -> NavigationViewCoordinator<LiveTVChannelsCoordinator> {
return NavigationViewCoordinator(LiveTVChannelsCoordinator()) NavigationViewCoordinator(LiveTVChannelsCoordinator())
} }
@ViewBuilder func makeChannelsTab(isActive: Bool) -> some View { @ViewBuilder
func makeChannelsTab(isActive: Bool) -> some View {
HStack { HStack {
Image(systemName: "square.grid.3x3") Image(systemName: "square.grid.3x3")
Text("Channels") Text("Channels")
@ -45,10 +49,11 @@ final class LiveTVTabCoordinator: TabCoordinatable {
} }
func makeHome() -> LiveTVHomeView { func makeHome() -> LiveTVHomeView {
return LiveTVHomeView() LiveTVHomeView()
} }
@ViewBuilder func makeHomeTab(isActive: Bool) -> some View { @ViewBuilder
func makeHomeTab(isActive: Bool) -> some View {
HStack { HStack {
Image(systemName: "house") Image(systemName: "house")
Text("Home") Text("Home")

View File

@ -1,11 +1,10 @@
// //
/* // Swiftfin is subject to the terms of the Mozilla Public
* SwiftFin is subject to the terms of the Mozilla Public // License, v2.0. If a copy of the MPL was not distributed with this
* 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/.
* file, you can obtain one at https://mozilla.org/MPL/2.0/. //
* // Copyright (c) 2022 Jellyfin & Jellyfin Contributors
* Copyright 2021 Aiden Vigue & Jellyfin Contributors //
*/
import Combine import Combine
import Defaults import Defaults
@ -18,8 +17,10 @@ import WidgetKit
final class MainCoordinator: NavigationCoordinatable { final class MainCoordinator: NavigationCoordinatable {
var stack: NavigationStack<MainCoordinator> var stack: NavigationStack<MainCoordinator>
@Root var mainTab = makeMainTab @Root
@Root var serverList = makeServerList var mainTab = makeMainTab
@Root
var serverList = makeServerList
private var cancellables = Set<AnyCancellable>() private var cancellables = Set<AnyCancellable>()
@ -48,7 +49,8 @@ final class MainCoordinator: NavigationCoordinatable {
nc.addObserver(self, selector: #selector(didLogIn), name: SwiftfinNotificationCenter.Keys.didSignIn, object: nil) nc.addObserver(self, selector: #selector(didLogIn), name: SwiftfinNotificationCenter.Keys.didSignIn, object: nil)
nc.addObserver(self, selector: #selector(didLogOut), name: SwiftfinNotificationCenter.Keys.didSignOut, object: nil) nc.addObserver(self, selector: #selector(didLogOut), name: SwiftfinNotificationCenter.Keys.didSignOut, object: nil)
nc.addObserver(self, selector: #selector(processDeepLink), name: SwiftfinNotificationCenter.Keys.processDeepLink, object: nil) nc.addObserver(self, selector: #selector(processDeepLink), name: SwiftfinNotificationCenter.Keys.processDeepLink, object: nil)
nc.addObserver(self, selector: #selector(didChangeServerCurrentURI), name: SwiftfinNotificationCenter.Keys.didChangeServerCurrentURI, object: nil) nc.addObserver(self, selector: #selector(didChangeServerCurrentURI),
name: SwiftfinNotificationCenter.Keys.didChangeServerCurrentURI, object: nil)
Defaults.publisher(.appAppearance) Defaults.publisher(.appAppearance)
.sink { _ in .sink { _ in
@ -57,17 +59,20 @@ final class MainCoordinator: NavigationCoordinatable {
.store(in: &cancellables) .store(in: &cancellables)
} }
@objc func didLogIn() { @objc
func didLogIn() {
LogManager.shared.log.info("Received `didSignIn` from SwiftfinNotificationCenter.") LogManager.shared.log.info("Received `didSignIn` from SwiftfinNotificationCenter.")
root(\.mainTab) root(\.mainTab)
} }
@objc func didLogOut() { @objc
func didLogOut() {
LogManager.shared.log.info("Received `didSignOut` from SwiftfinNotificationCenter.") LogManager.shared.log.info("Received `didSignOut` from SwiftfinNotificationCenter.")
root(\.serverList) root(\.serverList)
} }
@objc func processDeepLink(_ notification: Notification) { @objc
func processDeepLink(_ notification: Notification) {
guard let deepLink = notification.object as? DeepLink else { return } guard let deepLink = notification.object as? DeepLink else { return }
if let coordinator = hasRoot(\.mainTab) { if let coordinator = hasRoot(\.mainTab) {
switch deepLink { switch deepLink {
@ -80,8 +85,10 @@ final class MainCoordinator: NavigationCoordinatable {
} }
} }
@objc func didChangeServerCurrentURI(_ notification: Notification) { @objc
guard let newCurrentServerState = notification.object as? SwiftfinStore.State.Server else { fatalError("Need to have new current login state server") } func didChangeServerCurrentURI(_ notification: Notification) {
guard let newCurrentServerState = notification.object as? SwiftfinStore.State.Server
else { fatalError("Need to have new current login state server") }
guard SessionManager.main.currentLogin != nil else { return } guard SessionManager.main.currentLogin != nil else { return }
if newCurrentServerState.id == SessionManager.main.currentLogin.server.id { if newCurrentServerState.id == SessionManager.main.currentLogin.server.id {
SessionManager.main.loginUser(server: newCurrentServerState, user: SessionManager.main.currentLogin.user) SessionManager.main.loginUser(server: newCurrentServerState, user: SessionManager.main.currentLogin.user)

View File

@ -1,44 +1,48 @@
// //
/* // Swiftfin is subject to the terms of the Mozilla Public
* SwiftFin is subject to the terms of the Mozilla Public // License, v2.0. If a copy of the MPL was not distributed with this
* 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/.
* file, you can obtain one at https://mozilla.org/MPL/2.0/. //
* // Copyright (c) 2022 Jellyfin & Jellyfin Contributors
* Copyright 2021 Aiden Vigue & Jellyfin Contributors //
*/
import Foundation import Foundation
import SwiftUI
import Stinsen import Stinsen
import SwiftUI
final class MainTabCoordinator: TabCoordinatable { final class MainTabCoordinator: TabCoordinatable {
var child = TabChild(startingItems: [ var child = TabChild(startingItems: [
\MainTabCoordinator.home, \MainTabCoordinator.home,
\MainTabCoordinator.allMedia \MainTabCoordinator.allMedia,
]) ])
@Route(tabItem: makeHomeTab) var home = makeHome @Route(tabItem: makeHomeTab)
@Route(tabItem: makeAllMediaTab) var allMedia = makeAllMedia var home = makeHome
@Route(tabItem: makeAllMediaTab)
var allMedia = makeAllMedia
func makeHome() -> NavigationViewCoordinator<HomeCoordinator> { func makeHome() -> NavigationViewCoordinator<HomeCoordinator> {
return NavigationViewCoordinator(HomeCoordinator()) NavigationViewCoordinator(HomeCoordinator())
} }
@ViewBuilder func makeHomeTab(isActive: Bool) -> some View { @ViewBuilder
func makeHomeTab(isActive: Bool) -> some View {
Image(systemName: "house") Image(systemName: "house")
L10n.home.text L10n.home.text
} }
func makeAllMedia() -> NavigationViewCoordinator<LibraryListCoordinator> { func makeAllMedia() -> NavigationViewCoordinator<LibraryListCoordinator> {
return NavigationViewCoordinator(LibraryListCoordinator(viewModel: LibraryListViewModel())) NavigationViewCoordinator(LibraryListCoordinator(viewModel: LibraryListViewModel()))
} }
@ViewBuilder func makeAllMediaTab(isActive: Bool) -> some View { @ViewBuilder
func makeAllMediaTab(isActive: Bool) -> some View {
Image(systemName: "folder") Image(systemName: "folder")
L10n.allMedia.text L10n.allMedia.text
} }
@ViewBuilder func customize(_ view: AnyView) -> some View { @ViewBuilder
func customize(_ view: AnyView) -> some View {
view.onAppear { view.onAppear {
AppURLHandler.shared.appURLState = .allowed AppURLHandler.shared.appURLState = .allowed
// TODO: todo // TODO: todo

View File

@ -1,11 +1,10 @@
// //
/* // Swiftfin is subject to the terms of the Mozilla Public
* SwiftFin is subject to the terms of the Mozilla Public // License, v2.0. If a copy of the MPL was not distributed with this
* 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/.
* file, you can obtain one at https://mozilla.org/MPL/2.0/. //
* // Copyright (c) 2022 Jellyfin & Jellyfin Contributors
* Copyright 2021 Aiden Vigue & Jellyfin Contributors //
*/
import Foundation import Foundation
import Nuke import Nuke
@ -15,9 +14,12 @@ import SwiftUI
final class MainCoordinator: NavigationCoordinatable { final class MainCoordinator: NavigationCoordinatable {
var stack = NavigationStack<MainCoordinator>(initial: \MainCoordinator.mainTab) var stack = NavigationStack<MainCoordinator>(initial: \MainCoordinator.mainTab)
@Root var mainTab = makeMainTab @Root
@Root var serverList = makeServerList var mainTab = makeMainTab
@Root var liveTV = makeLiveTV @Root
var serverList = makeServerList
@Root
var liveTV = makeLiveTV
@ViewBuilder @ViewBuilder
func customize(_ view: AnyView) -> some View { func customize(_ view: AnyView) -> some View {
@ -43,12 +45,14 @@ final class MainCoordinator: NavigationCoordinatable {
nc.addObserver(self, selector: #selector(didLogOut), name: SwiftfinNotificationCenter.Keys.didSignOut, object: nil) nc.addObserver(self, selector: #selector(didLogOut), name: SwiftfinNotificationCenter.Keys.didSignOut, object: nil)
} }
@objc func didLogIn() { @objc
func didLogIn() {
LogManager.shared.log.info("Received `didSignIn` from NSNotificationCenter.") LogManager.shared.log.info("Received `didSignIn` from NSNotificationCenter.")
root(\.mainTab) root(\.mainTab)
} }
@objc func didLogOut() { @objc
func didLogOut() {
LogManager.shared.log.info("Received `didSignOut` from NSNotificationCenter.") LogManager.shared.log.info("Received `didSignOut` from NSNotificationCenter.")
root(\.serverList) root(\.serverList)
} }

View File

@ -1,15 +1,14 @@
// //
/* // Swiftfin is subject to the terms of the Mozilla Public
* SwiftFin is subject to the terms of the Mozilla Public // License, v2.0. If a copy of the MPL was not distributed with this
* 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/.
* file, you can obtain one at https://mozilla.org/MPL/2.0/. //
* // Copyright (c) 2022 Jellyfin & Jellyfin Contributors
* Copyright 2021 Aiden Vigue & Jellyfin Contributors //
*/
import Foundation import Foundation
import SwiftUI
import Stinsen import Stinsen
import SwiftUI
final class MainTabCoordinator: TabCoordinatable { final class MainTabCoordinator: TabCoordinatable {
var child = TabChild(startingItems: [ var child = TabChild(startingItems: [
@ -17,20 +16,26 @@ final class MainTabCoordinator: TabCoordinatable {
\MainTabCoordinator.tv, \MainTabCoordinator.tv,
\MainTabCoordinator.movies, \MainTabCoordinator.movies,
\MainTabCoordinator.other, \MainTabCoordinator.other,
\MainTabCoordinator.settings \MainTabCoordinator.settings,
]) ])
@Route(tabItem: makeHomeTab) var home = makeHome @Route(tabItem: makeHomeTab)
@Route(tabItem: makeTvTab) var tv = makeTv var home = makeHome
@Route(tabItem: makeMoviesTab) var movies = makeMovies @Route(tabItem: makeTvTab)
@Route(tabItem: makeOtherTab) var other = makeOther var tv = makeTv
@Route(tabItem: makeSettingsTab) var settings = makeSettings @Route(tabItem: makeMoviesTab)
var movies = makeMovies
@Route(tabItem: makeOtherTab)
var other = makeOther
@Route(tabItem: makeSettingsTab)
var settings = makeSettings
func makeHome() -> NavigationViewCoordinator<HomeCoordinator> { func makeHome() -> NavigationViewCoordinator<HomeCoordinator> {
return NavigationViewCoordinator(HomeCoordinator()) NavigationViewCoordinator(HomeCoordinator())
} }
@ViewBuilder func makeHomeTab(isActive: Bool) -> some View { @ViewBuilder
func makeHomeTab(isActive: Bool) -> some View {
HStack { HStack {
Image(systemName: "house") Image(systemName: "house")
L10n.home.text L10n.home.text
@ -38,10 +43,11 @@ final class MainTabCoordinator: TabCoordinatable {
} }
func makeTv() -> NavigationViewCoordinator<TVLibrariesCoordinator> { func makeTv() -> NavigationViewCoordinator<TVLibrariesCoordinator> {
return NavigationViewCoordinator(TVLibrariesCoordinator(viewModel: TVLibrariesViewModel(), title: "TV Shows")) NavigationViewCoordinator(TVLibrariesCoordinator(viewModel: TVLibrariesViewModel(), title: "TV Shows"))
} }
@ViewBuilder func makeTvTab(isActive: Bool) -> some View { @ViewBuilder
func makeTvTab(isActive: Bool) -> some View {
HStack { HStack {
Image(systemName: "tv") Image(systemName: "tv")
Text("TV Shows") Text("TV Shows")
@ -49,10 +55,11 @@ final class MainTabCoordinator: TabCoordinatable {
} }
func makeMovies() -> NavigationViewCoordinator<MovieLibrariesCoordinator> { func makeMovies() -> NavigationViewCoordinator<MovieLibrariesCoordinator> {
return NavigationViewCoordinator(MovieLibrariesCoordinator(viewModel: MovieLibrariesViewModel(), title: "Movies")) NavigationViewCoordinator(MovieLibrariesCoordinator(viewModel: MovieLibrariesViewModel(), title: "Movies"))
} }
@ViewBuilder func makeMoviesTab(isActive: Bool) -> some View { @ViewBuilder
func makeMoviesTab(isActive: Bool) -> some View {
HStack { HStack {
Image(systemName: "film") Image(systemName: "film")
Text("Movies") Text("Movies")
@ -60,10 +67,11 @@ final class MainTabCoordinator: TabCoordinatable {
} }
func makeOther() -> NavigationViewCoordinator<LibraryListCoordinator> { func makeOther() -> NavigationViewCoordinator<LibraryListCoordinator> {
return NavigationViewCoordinator(LibraryListCoordinator(viewModel: LibraryListViewModel())) NavigationViewCoordinator(LibraryListCoordinator(viewModel: LibraryListViewModel()))
} }
@ViewBuilder func makeOtherTab(isActive: Bool) -> some View { @ViewBuilder
func makeOtherTab(isActive: Bool) -> some View {
HStack { HStack {
Image(systemName: "folder") Image(systemName: "folder")
Text("Other") Text("Other")
@ -71,10 +79,11 @@ final class MainTabCoordinator: TabCoordinatable {
} }
func makeSettings() -> NavigationViewCoordinator<SettingsCoordinator> { func makeSettings() -> NavigationViewCoordinator<SettingsCoordinator> {
return NavigationViewCoordinator(SettingsCoordinator()) NavigationViewCoordinator(SettingsCoordinator())
} }
@ViewBuilder func makeSettingsTab(isActive: Bool) -> some View { @ViewBuilder
func makeSettingsTab(isActive: Bool) -> some View {
Image(systemName: "gearshape.fill") Image(systemName: "gearshape.fill")
} }
} }

View File

@ -1,11 +1,10 @@
// //
/* // Swiftfin is subject to the terms of the Mozilla Public
* SwiftFin is subject to the terms of the Mozilla Public // License, v2.0. If a copy of the MPL was not distributed with this
* 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/.
* file, you can obtain one at https://mozilla.org/MPL/2.0/. //
* // Copyright (c) 2022 Jellyfin & Jellyfin Contributors
* Copyright 2021 Aiden Vigue & Jellyfin Contributors //
*/
import Foundation import Foundation
import JellyfinAPI import JellyfinAPI
@ -16,8 +15,10 @@ final class MovieLibrariesCoordinator: NavigationCoordinatable {
let stack = NavigationStack(initial: \MovieLibrariesCoordinator.start) let stack = NavigationStack(initial: \MovieLibrariesCoordinator.start)
@Root var start = makeStart @Root
@Route(.push) var library = makeLibrary var start = makeStart
@Route(.push)
var library = makeLibrary
let viewModel: MovieLibrariesViewModel let viewModel: MovieLibrariesViewModel
let title: String let title: String
@ -27,7 +28,8 @@ final class MovieLibrariesCoordinator: NavigationCoordinatable {
self.title = title self.title = title
} }
@ViewBuilder func makeStart() -> some View { @ViewBuilder
func makeStart() -> some View {
MovieLibrariesView(viewModel: self.viewModel, title: title) MovieLibrariesView(viewModel: self.viewModel, title: title)
} }

View File

@ -1,23 +1,24 @@
// //
/* // Swiftfin is subject to the terms of the Mozilla Public
* SwiftFin is subject to the terms of the Mozilla Public // License, v2.0. If a copy of the MPL was not distributed with this
* 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/.
* file, you can obtain one at https://mozilla.org/MPL/2.0/. //
* // Copyright (c) 2022 Jellyfin & Jellyfin Contributors
* Copyright 2021 Aiden Vigue & Jellyfin Contributors //
*/
import Foundation import Foundation
import JellyfinAPI
import Stinsen import Stinsen
import SwiftUI import SwiftUI
import JellyfinAPI
final class SearchCoordinator: NavigationCoordinatable { final class SearchCoordinator: NavigationCoordinatable {
let stack = NavigationStack(initial: \SearchCoordinator.start) let stack = NavigationStack(initial: \SearchCoordinator.start)
@Root var start = makeStart @Root
@Route(.push) var item = makeItem var start = makeStart
@Route(.push)
var item = makeItem
let viewModel: LibrarySearchViewModel let viewModel: LibrarySearchViewModel
@ -29,7 +30,8 @@ final class SearchCoordinator: NavigationCoordinatable {
ItemCoordinator(item: item) ItemCoordinator(item: item)
} }
@ViewBuilder func makeStart() -> some View { @ViewBuilder
func makeStart() -> some View {
LibrarySearchView(viewModel: self.viewModel) LibrarySearchView(viewModel: self.viewModel)
} }
} }

View File

@ -1,11 +1,10 @@
// //
/* // Swiftfin is subject to the terms of the Mozilla Public
* SwiftFin is subject to the terms of the Mozilla Public // License, v2.0. If a copy of the MPL was not distributed with this
* 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/.
* file, you can obtain one at https://mozilla.org/MPL/2.0/. //
* // Copyright (c) 2022 Jellyfin & Jellyfin Contributors
* Copyright 2021 Aiden Vigue & Jellyfin Contributors //
*/
import Foundation import Foundation
import Stinsen import Stinsen
@ -15,7 +14,8 @@ final class ServerDetailCoordinator: NavigationCoordinatable {
let stack = NavigationStack(initial: \ServerDetailCoordinator.start) let stack = NavigationStack(initial: \ServerDetailCoordinator.start)
@Root var start = makeStart @Root
var start = makeStart
let viewModel: ServerDetailViewModel let viewModel: ServerDetailViewModel
@ -23,7 +23,8 @@ final class ServerDetailCoordinator: NavigationCoordinatable {
self.viewModel = viewModel self.viewModel = viewModel
} }
@ViewBuilder func makeStart() -> some View { @ViewBuilder
func makeStart() -> some View {
ServerDetailView(viewModel: viewModel) ServerDetailView(viewModel: viewModel)
} }
} }

View File

@ -1,11 +1,10 @@
// //
/* // Swiftfin is subject to the terms of the Mozilla Public
* SwiftFin is subject to the terms of the Mozilla Public // License, v2.0. If a copy of the MPL was not distributed with this
* 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/.
* file, you can obtain one at https://mozilla.org/MPL/2.0/. //
* // Copyright (c) 2022 Jellyfin & Jellyfin Contributors
* Copyright 2021 Aiden Vigue & Jellyfin Contributors //
*/
import Foundation import Foundation
import Stinsen import Stinsen
@ -15,10 +14,14 @@ final class ServerListCoordinator: NavigationCoordinatable {
let stack = NavigationStack(initial: \ServerListCoordinator.start) let stack = NavigationStack(initial: \ServerListCoordinator.start)
@Root var start = makeStart @Root
@Route(.push) var connectToServer = makeConnectToServer var start = makeStart
@Route(.push) var userList = makeUserList @Route(.push)
@Route(.modal) var basicAppSettings = makeBasicAppSettings var connectToServer = makeConnectToServer
@Route(.push)
var userList = makeUserList
@Route(.modal)
var basicAppSettings = makeBasicAppSettings
func makeConnectToServer() -> ConnectToServerCoodinator { func makeConnectToServer() -> ConnectToServerCoodinator {
ConnectToServerCoodinator() ConnectToServerCoodinator()
@ -32,7 +35,8 @@ final class ServerListCoordinator: NavigationCoordinatable {
NavigationViewCoordinator(BasicAppSettingsCoordinator()) NavigationViewCoordinator(BasicAppSettingsCoordinator())
} }
@ViewBuilder func makeStart() -> some View { @ViewBuilder
func makeStart() -> some View {
ServerListView(viewModel: ServerListViewModel()) ServerListView(viewModel: ServerListViewModel())
} }
} }

View File

@ -1,11 +1,10 @@
// //
/* // Swiftfin is subject to the terms of the Mozilla Public
* SwiftFin is subject to the terms of the Mozilla Public // License, v2.0. If a copy of the MPL was not distributed with this
* 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/.
* file, you can obtain one at https://mozilla.org/MPL/2.0/. //
* // Copyright (c) 2022 Jellyfin & Jellyfin Contributors
* Copyright 2021 Aiden Vigue & Jellyfin Contributors //
*/
import Foundation import Foundation
import Stinsen import Stinsen
@ -15,25 +14,33 @@ final class SettingsCoordinator: NavigationCoordinatable {
let stack = NavigationStack(initial: \SettingsCoordinator.start) let stack = NavigationStack(initial: \SettingsCoordinator.start)
@Root var start = makeStart @Root
@Route(.push) var serverDetail = makeServerDetail var start = makeStart
@Route(.push) var overlaySettings = makeOverlaySettings @Route(.push)
@Route(.push) var experimentalSettings = makeExperimentalSettings var serverDetail = makeServerDetail
@Route(.push)
var overlaySettings = makeOverlaySettings
@Route(.push)
var experimentalSettings = makeExperimentalSettings
@ViewBuilder func makeServerDetail() -> some View { @ViewBuilder
func makeServerDetail() -> some View {
let viewModel = ServerDetailViewModel(server: SessionManager.main.currentLogin.server) let viewModel = ServerDetailViewModel(server: SessionManager.main.currentLogin.server)
ServerDetailView(viewModel: viewModel) ServerDetailView(viewModel: viewModel)
} }
@ViewBuilder func makeOverlaySettings() -> some View { @ViewBuilder
func makeOverlaySettings() -> some View {
OverlaySettingsView() OverlaySettingsView()
} }
@ViewBuilder func makeExperimentalSettings() -> some View { @ViewBuilder
func makeExperimentalSettings() -> some View {
ExperimentalSettingsView() ExperimentalSettingsView()
} }
@ViewBuilder func makeStart() -> some View { @ViewBuilder
func makeStart() -> some View {
let viewModel = SettingsViewModel(server: SessionManager.main.currentLogin.server, user: SessionManager.main.currentLogin.user) let viewModel = SettingsViewModel(server: SessionManager.main.currentLogin.server, user: SessionManager.main.currentLogin.user)
SettingsView(viewModel: viewModel) SettingsView(viewModel: viewModel)
} }

View File

@ -1,11 +1,10 @@
// //
/* // Swiftfin is subject to the terms of the Mozilla Public
* SwiftFin is subject to the terms of the Mozilla Public // License, v2.0. If a copy of the MPL was not distributed with this
* 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/.
* file, you can obtain one at https://mozilla.org/MPL/2.0/. //
* // Copyright (c) 2022 Jellyfin & Jellyfin Contributors
* Copyright 2021 Aiden Vigue & Jellyfin Contributors //
*/
import Foundation import Foundation
import JellyfinAPI import JellyfinAPI
@ -16,8 +15,10 @@ final class TVLibrariesCoordinator: NavigationCoordinatable {
let stack = NavigationStack(initial: \TVLibrariesCoordinator.start) let stack = NavigationStack(initial: \TVLibrariesCoordinator.start)
@Root var start = makeStart @Root
@Route(.push) var library = makeLibrary var start = makeStart
@Route(.push)
var library = makeLibrary
let viewModel: TVLibrariesViewModel let viewModel: TVLibrariesViewModel
let title: String let title: String
@ -27,7 +28,8 @@ final class TVLibrariesCoordinator: NavigationCoordinatable {
self.title = title self.title = title
} }
@ViewBuilder func makeStart() -> some View { @ViewBuilder
func makeStart() -> some View {
TVLibrariesView(viewModel: self.viewModel, title: title) TVLibrariesView(viewModel: self.viewModel, title: title)
} }

View File

@ -1,11 +1,10 @@
// //
/* // Swiftfin is subject to the terms of the Mozilla Public
* SwiftFin is subject to the terms of the Mozilla Public // License, v2.0. If a copy of the MPL was not distributed with this
* 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/.
* file, you can obtain one at https://mozilla.org/MPL/2.0/. //
* // Copyright (c) 2022 Jellyfin & Jellyfin Contributors
* Copyright 2021 Aiden Vigue & Jellyfin Contributors //
*/
import Foundation import Foundation
import Stinsen import Stinsen
@ -15,9 +14,12 @@ final class UserListCoordinator: NavigationCoordinatable {
let stack = NavigationStack(initial: \UserListCoordinator.start) let stack = NavigationStack(initial: \UserListCoordinator.start)
@Root var start = makeStart @Root
@Route(.push) var userSignIn = makeUserSignIn var start = makeStart
@Route(.push) var serverDetail = makeServerDetail @Route(.push)
var userSignIn = makeUserSignIn
@Route(.push)
var serverDetail = makeServerDetail
let viewModel: UserListViewModel let viewModel: UserListViewModel
@ -26,14 +28,15 @@ final class UserListCoordinator: NavigationCoordinatable {
} }
func makeUserSignIn(server: SwiftfinStore.State.Server) -> UserSignInCoordinator { func makeUserSignIn(server: SwiftfinStore.State.Server) -> UserSignInCoordinator {
return UserSignInCoordinator(viewModel: .init(server: server)) UserSignInCoordinator(viewModel: .init(server: server))
} }
func makeServerDetail(server: SwiftfinStore.State.Server) -> ServerDetailCoordinator { func makeServerDetail(server: SwiftfinStore.State.Server) -> ServerDetailCoordinator {
return ServerDetailCoordinator(viewModel: .init(server: server)) ServerDetailCoordinator(viewModel: .init(server: server))
} }
@ViewBuilder func makeStart() -> some View { @ViewBuilder
func makeStart() -> some View {
UserListView(viewModel: viewModel) UserListView(viewModel: viewModel)
} }
} }

View File

@ -1,11 +1,10 @@
// //
/* // Swiftfin is subject to the terms of the Mozilla Public
* SwiftFin is subject to the terms of the Mozilla Public // License, v2.0. If a copy of the MPL was not distributed with this
* 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/.
* file, you can obtain one at https://mozilla.org/MPL/2.0/. //
* // Copyright (c) 2022 Jellyfin & Jellyfin Contributors
* Copyright 2021 Aiden Vigue & Jellyfin Contributors //
*/
import Foundation import Foundation
import Stinsen import Stinsen
@ -15,7 +14,8 @@ final class UserSignInCoordinator: NavigationCoordinatable {
let stack = NavigationStack(initial: \UserSignInCoordinator.start) let stack = NavigationStack(initial: \UserSignInCoordinator.start)
@Root var start = makeStart @Root
var start = makeStart
let viewModel: UserSignInViewModel let viewModel: UserSignInViewModel
@ -23,7 +23,8 @@ final class UserSignInCoordinator: NavigationCoordinatable {
self.viewModel = viewModel self.viewModel = viewModel
} }
@ViewBuilder func makeStart() -> some View { @ViewBuilder
func makeStart() -> some View {
UserSignInView(viewModel: viewModel) UserSignInView(viewModel: viewModel)
} }
} }

View File

@ -1,11 +1,10 @@
// //
/* // Swiftfin is subject to the terms of the Mozilla Public
* SwiftFin is subject to the terms of the Mozilla Public // License, v2.0. If a copy of the MPL was not distributed with this
* 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/.
* file, you can obtain one at https://mozilla.org/MPL/2.0/. //
* // Copyright (c) 2022 Jellyfin & Jellyfin Contributors
* Copyright 2021 Aiden Vigue & Jellyfin Contributors //
*/
import Defaults import Defaults
import Foundation import Foundation
@ -17,7 +16,8 @@ final class VideoPlayerCoordinator: NavigationCoordinatable {
let stack = NavigationStack(initial: \VideoPlayerCoordinator.start) let stack = NavigationStack(initial: \VideoPlayerCoordinator.start)
@Root var start = makeStart @Root
var start = makeStart
let viewModel: VideoPlayerViewModel let viewModel: VideoPlayerViewModel
@ -25,7 +25,8 @@ final class VideoPlayerCoordinator: NavigationCoordinatable {
self.viewModel = viewModel self.viewModel = viewModel
} }
@ViewBuilder func makeStart() -> some View { @ViewBuilder
func makeStart() -> some View {
PreferenceUIHostingControllerView { PreferenceUIHostingControllerView {
VLCPlayerView(viewModel: self.viewModel) VLCPlayerView(viewModel: self.viewModel)
.navigationBarHidden(true) .navigationBarHidden(true)

View File

@ -1,11 +1,10 @@
// //
/* // Swiftfin is subject to the terms of the Mozilla Public
* SwiftFin is subject to the terms of the Mozilla Public // License, v2.0. If a copy of the MPL was not distributed with this
* 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/.
* file, you can obtain one at https://mozilla.org/MPL/2.0/. //
* // Copyright (c) 2022 Jellyfin & Jellyfin Contributors
* Copyright 2021 Aiden Vigue & Jellyfin Contributors //
*/
import Defaults import Defaults
import Foundation import Foundation
@ -17,7 +16,8 @@ final class VideoPlayerCoordinator: NavigationCoordinatable {
let stack = NavigationStack(initial: \VideoPlayerCoordinator.start) let stack = NavigationStack(initial: \VideoPlayerCoordinator.start)
@Root var start = makeStart @Root
var start = makeStart
let viewModel: VideoPlayerViewModel let viewModel: VideoPlayerViewModel
@ -25,10 +25,10 @@ final class VideoPlayerCoordinator: NavigationCoordinatable {
self.viewModel = viewModel self.viewModel = viewModel
} }
@ViewBuilder func makeStart() -> some View { @ViewBuilder
func makeStart() -> some View {
VLCPlayerView(viewModel: viewModel) VLCPlayerView(viewModel: viewModel)
.navigationBarHidden(true) .navigationBarHidden(true)
.ignoresSafeArea() .ignoresSafeArea()
} }
} }

View File

@ -1,11 +1,10 @@
// //
/* // Swiftfin is subject to the terms of the Mozilla Public
* SwiftFin is subject to the terms of the Mozilla Public // License, v2.0. If a copy of the MPL was not distributed with this
* 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/.
* file, you can obtain one at https://mozilla.org/MPL/2.0/. //
* // Copyright (c) 2022 Jellyfin & Jellyfin Contributors
* Copyright 2021 Aiden Vigue & Jellyfin Contributors //
*/
import Foundation import Foundation
import JellyfinAPI import JellyfinAPI
@ -22,7 +21,7 @@ struct ErrorMessage: Identifiable {
static let noShowErrorCode = -69420 static let noShowErrorCode = -69420
var id: String { var id: String {
return "\(code)\(title)\(logConstructor.message)" "\(code)\(title)\(logConstructor.message)"
} }
/// If the custom displayMessage is `nil`, it will be set to the given logConstructor's message /// If the custom displayMessage is `nil`, it will be set to the given logConstructor's message

View File

@ -1,11 +1,10 @@
// //
/* // Swiftfin is subject to the terms of the Mozilla Public
* SwiftFin is subject to the terms of the Mozilla Public // License, v2.0. If a copy of the MPL was not distributed with this
* 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/.
* file, you can obtain one at https://mozilla.org/MPL/2.0/. //
* // Copyright (c) 2022 Jellyfin & Jellyfin Contributors
* Copyright 2021 Aiden Vigue & Jellyfin Contributors //
*/
import Foundation import Foundation
import JellyfinAPI import JellyfinAPI

View File

@ -1,11 +1,10 @@
// //
/* // Swiftfin is subject to the terms of the Mozilla Public
* SwiftFin is subject to the terms of the Mozilla Public // License, v2.0. If a copy of the MPL was not distributed with this
* 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/.
* file, you can obtain one at https://mozilla.org/MPL/2.0/. //
* // Copyright (c) 2022 Jellyfin & Jellyfin Contributors
* Copyright 2021 Aiden Vigue & Jellyfin Contributors //
*/
import Foundation import Foundation
import JellyfinAPI import JellyfinAPI
@ -13,7 +12,8 @@ import JellyfinAPI
/** /**
The implementation of the network errors here are a temporary measure. The implementation of the network errors here are a temporary measure.
It is very repetitive, messy, and doesn't fulfill the entire specification of "error reporting". It is very repetitive, messy, and doesn't fulfill the entire specification of "error reporting".
The specific kind of errors here should be created and surfaced from within JellyfinAPI on API calls.
Needs to be replaced
*/ */
enum NetworkError: Error { enum NetworkError: Error {
@ -29,11 +29,11 @@ enum NetworkError: Error {
var errorMessage: ErrorMessage { var errorMessage: ErrorMessage {
switch self { switch self {
case .URLError(let response, let displayMessage, let logConstructor): case let .URLError(response, displayMessage, logConstructor):
return NetworkError.parseURLError(from: response, displayMessage: displayMessage, logConstructor: logConstructor) return NetworkError.parseURLError(from: response, displayMessage: displayMessage, logConstructor: logConstructor)
case .HTTPURLError(let response, let displayMessage, let logConstructor): case let .HTTPURLError(response, displayMessage, logConstructor):
return NetworkError.parseHTTPURLError(from: response, displayMessage: displayMessage, logConstructor: logConstructor) return NetworkError.parseHTTPURLError(from: response, displayMessage: displayMessage, logConstructor: logConstructor)
case .JellyfinError(let response, let displayMessage, let logConstructor): case let .JellyfinError(response, displayMessage, logConstructor):
return NetworkError.parseJellyfinError(from: response, displayMessage: displayMessage, logConstructor: logConstructor) return NetworkError.parseJellyfinError(from: response, displayMessage: displayMessage, logConstructor: logConstructor)
} }
} }
@ -62,14 +62,16 @@ enum NetworkError: Error {
logFunction(logConstructor.message, logConstructor.tag, logConstructor.function, logConstructor.file, logConstructor.line) logFunction(logConstructor.message, logConstructor.tag, logConstructor.function, logConstructor.file, logConstructor.line)
} }
private static func parseURLError(from response: ErrorResponse, displayMessage: String?, logConstructor: LogConstructor) -> ErrorMessage { private static func parseURLError(from response: ErrorResponse, displayMessage: String?,
logConstructor: LogConstructor) -> ErrorMessage
{
let errorMessage: ErrorMessage let errorMessage: ErrorMessage
var logMessage = "An error has occurred." var logMessage = "An error has occurred."
var logConstructor = logConstructor var logConstructor = logConstructor
switch response { switch response {
case .error(_, _, _, let err): case let .error(_, _, _, err):
// These codes are currently referenced from: // These codes are currently referenced from:
// https://developer.apple.com/documentation/foundation/1508628-url_loading_system_error_codes // https://developer.apple.com/documentation/foundation/1508628-url_loading_system_error_codes
@ -100,7 +102,9 @@ enum NetworkError: Error {
return errorMessage return errorMessage
} }
private static func parseHTTPURLError(from response: ErrorResponse, displayMessage: String?, logConstructor: LogConstructor) -> ErrorMessage { private static func parseHTTPURLError(from response: ErrorResponse, displayMessage: String?,
logConstructor: LogConstructor) -> ErrorMessage
{
let errorMessage: ErrorMessage let errorMessage: ErrorMessage
let logMessage = "An HTTP URL error has occurred" let logMessage = "An HTTP URL error has occurred"
@ -119,14 +123,16 @@ enum NetworkError: Error {
return errorMessage return errorMessage
} }
private static func parseJellyfinError(from response: ErrorResponse, displayMessage: String?, logConstructor: LogConstructor) -> ErrorMessage { private static func parseJellyfinError(from response: ErrorResponse, displayMessage: String?,
logConstructor: LogConstructor) -> ErrorMessage
{
let errorMessage: ErrorMessage let errorMessage: ErrorMessage
var logMessage = "An error has occurred." var logMessage = "An error has occurred."
var logConstructor = logConstructor var logConstructor = logConstructor
switch response { switch response {
case .error(let code, _, _, _): case let .error(code, _, _, _):
// Generic HTTP status codes // Generic HTTP status codes
switch code { switch code {

View File

@ -1,29 +1,15 @@
/* //
Copyright (c) 2018 Wolt Enterprises // Swiftfin is subject to the terms of the Mozilla Public
// License, v2.0. If a copy of the MPL was not distributed with this
Permission is hereby granted, free of charge, to any person obtaining a copy // file, you can obtain one at https://mozilla.org/MPL/2.0/.
of this software and associated documentation files (the "Software"), to deal //
in the Software without restriction, including without limitation the rights // Copyright (c) 2022 Jellyfin & Jellyfin Contributors
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell //
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
*/
import UIKit import UIKit
extension UIImage { public extension UIImage {
public convenience init?(blurHash: String, size: CGSize, punch: Float = 1) { convenience init?(blurHash: String, size: CGSize, punch: Float = 1) {
guard blurHash.count >= 6 else { return nil } guard blurHash.count >= 6 else { return nil }
let sizeFlag = String(blurHash[0]).decode83() let sizeFlag = String(blurHash[0]).decode83()
@ -82,7 +68,8 @@ extension UIImage {
guard let provider = CGDataProvider(data: data) else { return nil } guard let provider = CGDataProvider(data: data) else { return nil }
guard let cgImage = CGImage(width: width, height: height, bitsPerComponent: 8, bitsPerPixel: 24, bytesPerRow: bytesPerRow, guard let cgImage = CGImage(width: width, height: height, bitsPerComponent: 8, bitsPerPixel: 24, bytesPerRow: bytesPerRow,
space: CGColorSpaceCreateDeviceRGB(), bitmapInfo: bitmapInfo, provider: provider, decode: nil, shouldInterpolate: true, intent: .defaultIntent) else { return nil } space: CGColorSpaceCreateDeviceRGB(), bitmapInfo: bitmapInfo, provider: provider, decode: nil,
shouldInterpolate: true, intent: .defaultIntent) else { return nil }
self.init(cgImage: cgImage) self.init(cgImage: cgImage)
} }
@ -100,17 +87,15 @@ private func decodeAC(_ value: Int, maximumValue: Float) -> (Float, Float, Float
let quantG = (value / 19) % 19 let quantG = (value / 19) % 19
let quantB = value % 19 let quantB = value % 19
let rgb = ( let rgb = (signPow((Float(quantR) - 9) / 9, 2) * maximumValue,
signPow((Float(quantR) - 9) / 9, 2) * maximumValue,
signPow((Float(quantG) - 9) / 9, 2) * maximumValue, signPow((Float(quantG) - 9) / 9, 2) * maximumValue,
signPow((Float(quantB) - 9) / 9, 2) * maximumValue signPow((Float(quantB) - 9) / 9, 2) * maximumValue)
)
return rgb return rgb
} }
private func signPow(_ value: Float, _ exp: Float) -> Float { private func signPow(_ value: Float, _ exp: Float) -> Float {
return copysign(pow(abs(value), exp), value) copysign(pow(abs(value), exp), value)
} }
private func linearTosRGB(_ value: Float) -> Int { private func linearTosRGB(_ value: Float) -> Int {
@ -124,7 +109,7 @@ private func sRGBToLinear<Type: BinaryInteger>(_ value: Type) -> Float {
} }
private let encodeCharacters: [String] = { private let encodeCharacters: [String] = {
return "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz#$%*+,-.:;=?@[]^_{|}~".map { String($0) } "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz#$%*+,-.:;=?@[]^_{|}~".map { String($0) }
}() }()
private let decodeCharacters: [String: Int] = { private let decodeCharacters: [String: Int] = {
@ -149,7 +134,7 @@ extension String {
private extension String { private extension String {
subscript(offset: Int) -> Character { subscript(offset: Int) -> Character {
return self[index(startIndex, offsetBy: offset)] self[index(startIndex, offsetBy: offset)]
} }
subscript(bounds: CountableClosedRange<Int>) -> Substring { subscript(bounds: CountableClosedRange<Int>) -> Substring {

View File

@ -1,17 +1,16 @@
// //
/* // Swiftfin is subject to the terms of the Mozilla Public
* SwiftFin is subject to the terms of the Mozilla Public // License, v2.0. If a copy of the MPL was not distributed with this
* 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/.
* file, you can obtain one at https://mozilla.org/MPL/2.0/. //
* // Copyright (c) 2022 Jellyfin & Jellyfin Contributors
* Copyright 2021 Aiden Vigue & Jellyfin Contributors //
*/
import UIKit import UIKit
extension CGSize { extension CGSize {
static func Circle(radius: CGFloat) -> CGSize { static func Circle(radius: CGFloat) -> CGSize {
return CGSize(width: radius, height: radius) CGSize(width: radius, height: radius)
} }
} }

View File

@ -1,9 +1,10 @@
/* SwiftFin is subject to the terms of the Mozilla Public //
* License, v2.0. If a copy of the MPL was not distributed with this // Swiftfin is subject to the terms of the Mozilla Public
* file, you can obtain one at https://mozilla.org/MPL/2.0/. // 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 2021 Aiden Vigue & Jellyfin Contributors //
*/ // Copyright (c) 2022 Jellyfin & Jellyfin Contributors
//
import Foundation import Foundation
@ -17,6 +18,6 @@ public extension Collection {
/// ///
/// - Parameter index: index of element to access element. /// - Parameter index: index of element to access element.
subscript(safe index: Index) -> Element? { subscript(safe index: Index) -> Element? {
return indices.contains(index) ? self[index] : nil indices.contains(index) ? self[index] : nil
} }
} }

View File

@ -1,28 +1,27 @@
// //
/* // Swiftfin is subject to the terms of the Mozilla Public
* SwiftFin is subject to the terms of the Mozilla Public // License, v2.0. If a copy of the MPL was not distributed with this
* 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/.
* file, you can obtain one at https://mozilla.org/MPL/2.0/. //
* // Copyright (c) 2022 Jellyfin & Jellyfin Contributors
* Copyright 2021 Aiden Vigue & Jellyfin Contributors //
*/
import SwiftUI import SwiftUI
extension Color { public extension Color {
static let jellyfinPurple = Color(uiColor: .jellyfinPurple) internal static let jellyfinPurple = Color(uiColor: .jellyfinPurple)
#if os(tvOS) // tvOS doesn't have these #if os(tvOS) // tvOS doesn't have these
public static let systemFill = Color(UIColor.white) static let systemFill = Color(UIColor.white)
public static let secondarySystemFill = Color(UIColor.gray) static let secondarySystemFill = Color(UIColor.gray)
public static let tertiarySystemFill = Color(UIColor.black) static let tertiarySystemFill = Color(UIColor.black)
public static let lightGray = Color(UIColor.lightGray) static let lightGray = Color(UIColor.lightGray)
#else #else
public static let systemFill = Color(UIColor.systemFill) static let systemFill = Color(UIColor.systemFill)
public static let systemBackground = Color(UIColor.systemBackground) static let systemBackground = Color(UIColor.systemBackground)
public static let secondarySystemFill = Color(UIColor.secondarySystemBackground) static let secondarySystemFill = Color(UIColor.secondarySystemBackground)
public static let tertiarySystemFill = Color(UIColor.tertiarySystemBackground) static let tertiarySystemFill = Color(UIColor.tertiarySystemBackground)
#endif #endif
} }

View File

@ -1,11 +1,10 @@
// //
/* // Swiftfin is subject to the terms of the Mozilla Public
* SwiftFin is subject to the terms of the Mozilla Public // License, v2.0. If a copy of the MPL was not distributed with this
* 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/.
* file, you can obtain one at https://mozilla.org/MPL/2.0/. //
* // Copyright (c) 2022 Jellyfin & Jellyfin Contributors
* Copyright 2021 Aiden Vigue & Jellyfin Contributors //
*/
import Foundation import Foundation

View File

@ -1,9 +1,10 @@
/* SwiftFin is subject to the terms of the Mozilla Public //
* License, v2.0. If a copy of the MPL was not distributed with this // Swiftfin is subject to the terms of the Mozilla Public
* file, you can obtain one at https://mozilla.org/MPL/2.0/. // 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 2021 Aiden Vigue & Jellyfin Contributors //
*/ // Copyright (c) 2022 Jellyfin & Jellyfin Contributors
//
import Foundation import Foundation
import SwiftUI import SwiftUI

View File

@ -1,20 +1,20 @@
// //
/* // Swiftfin is subject to the terms of the Mozilla Public
* SwiftFin is subject to the terms of the Mozilla Public // License, v2.0. If a copy of the MPL was not distributed with this
* 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/.
* file, you can obtain one at https://mozilla.org/MPL/2.0/. //
* // Copyright (c) 2022 Jellyfin & Jellyfin Contributors
* Copyright 2021 Aiden Vigue & Jellyfin Contributors //
*/
import Defaults import Defaults
import Foundation import Foundation
import JellyfinAPI import JellyfinAPI
// MARK: PortraitImageStackable // MARK: PortraitImageStackable
extension BaseItemDto: PortraitImageStackable { extension BaseItemDto: PortraitImageStackable {
public var portraitImageID: String { public var portraitImageID: String {
return id ?? "no id" id ?? "no id"
} }
public func imageURLContsructor(maxWidth: Int) -> URL { public func imageURLContsructor(maxWidth: Int) -> URL {
@ -45,12 +45,12 @@ extension BaseItemDto: PortraitImageStackable {
} }
public var blurHash: String { public var blurHash: String {
return self.getPrimaryImageBlurHash() self.getPrimaryImageBlurHash()
} }
public var failureInitials: String { public var failureInitials: String {
guard let name = self.name else { return "" } guard let name = self.name else { return "" }
let initials = name.split(separator: " ").compactMap({ String($0).first }) let initials = name.split(separator: " ").compactMap { String($0).first }
return String(initials) return String(initials)
} }

View File

@ -1,11 +1,10 @@
// //
/* // Swiftfin is subject to the terms of the Mozilla Public
* SwiftFin is subject to the terms of the Mozilla Public // License, v2.0. If a copy of the MPL was not distributed with this
* 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/.
* file, you can obtain one at https://mozilla.org/MPL/2.0/. //
* // Copyright (c) 2022 Jellyfin & Jellyfin Contributors
* Copyright 2021 Aiden Vigue & Jellyfin Contributors //
*/
import Combine import Combine
import Defaults import Defaults
@ -16,32 +15,33 @@ extension BaseItemDto {
func createVideoPlayerViewModel() -> AnyPublisher<VideoPlayerViewModel, Error> { func createVideoPlayerViewModel() -> AnyPublisher<VideoPlayerViewModel, Error> {
let builder = DeviceProfileBuilder() let builder = DeviceProfileBuilder()
// TODO: fix bitrate settings // TODO: fix bitrate settings
builder.setMaxBitrate(bitrate: 60000000) builder.setMaxBitrate(bitrate: 60_000_000)
let profile = builder.buildProfile() let profile = builder.buildProfile()
let playbackInfo = PlaybackInfoDto(userId: SessionManager.main.currentLogin.user.id, let playbackInfo = PlaybackInfoDto(userId: SessionManager.main.currentLogin.user.id,
maxStreamingBitrate: 60000000, maxStreamingBitrate: 60_000_000,
startTimeTicks: self.userData?.playbackPositionTicks ?? 0, startTimeTicks: self.userData?.playbackPositionTicks ?? 0,
deviceProfile: profile, deviceProfile: profile,
autoOpenLiveStream: true) autoOpenLiveStream: true)
return MediaInfoAPI.getPostedPlaybackInfo(itemId: self.id!, return MediaInfoAPI.getPostedPlaybackInfo(itemId: self.id!,
userId: SessionManager.main.currentLogin.user.id, userId: SessionManager.main.currentLogin.user.id,
maxStreamingBitrate: 60000000, maxStreamingBitrate: 60_000_000,
startTimeTicks: self.userData?.playbackPositionTicks ?? 0, startTimeTicks: self.userData?.playbackPositionTicks ?? 0,
autoOpenLiveStream: true, autoOpenLiveStream: true,
playbackInfoDto: playbackInfo) playbackInfoDto: playbackInfo)
.map({ response -> VideoPlayerViewModel in .map { response -> VideoPlayerViewModel in
let mediaSource = response.mediaSources!.first! let mediaSource = response.mediaSources!.first!
let audioStreams = mediaSource.mediaStreams?.filter({ $0.type == .audio }) ?? [] let audioStreams = mediaSource.mediaStreams?.filter { $0.type == .audio } ?? []
let subtitleStreams = mediaSource.mediaStreams?.filter({ $0.type == .subtitle }) ?? [] let subtitleStreams = mediaSource.mediaStreams?.filter { $0.type == .subtitle } ?? []
let defaultAudioStream = audioStreams.first(where: { $0.index! == mediaSource.defaultAudioStreamIndex! }) let defaultAudioStream = audioStreams.first(where: { $0.index! == mediaSource.defaultAudioStreamIndex! })
let defaultSubtitleStream = subtitleStreams.first(where: { $0.index! == mediaSource.defaultSubtitleStreamIndex ?? -1 }) let defaultSubtitleStream = subtitleStreams.first(where: { $0.index! == mediaSource.defaultSubtitleStreamIndex ?? -1 })
// MARK: Stream // MARK: Stream
var streamURL = URLComponents(string: SessionManager.main.currentLogin.server.currentURI)! var streamURL = URLComponents(string: SessionManager.main.currentLogin.server.currentURI)!
let streamType: ServerStreamType let streamType: ServerStreamType
@ -61,7 +61,7 @@ extension BaseItemDto {
// MARK: VidoPlayerViewModel Creation // MARK: VidoPlayerViewModel Creation
var subtitle: String? = nil var subtitle: String?
// MARK: Attach media content to self // MARK: Attach media content to self
@ -103,7 +103,7 @@ extension BaseItemDto {
shouldShowAutoPlay: shouldShowAutoPlay) shouldShowAutoPlay: shouldShowAutoPlay)
return videoPlayerViewModel return videoPlayerViewModel
}) }
.eraseToAnyPublisher() .eraseToAnyPublisher()
} }
} }

View File

@ -1,11 +1,10 @@
// //
/* // Swiftfin is subject to the terms of the Mozilla Public
* SwiftFin is subject to the terms of the Mozilla Public // License, v2.0. If a copy of the MPL was not distributed with this
* 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/.
* file, you can obtain one at https://mozilla.org/MPL/2.0/. //
* // Copyright (c) 2022 Jellyfin & Jellyfin Contributors
* Copyright 2021 Aiden Vigue & Jellyfin Contributors //
*/
import Foundation import Foundation
import JellyfinAPI import JellyfinAPI
@ -187,7 +186,8 @@ public extension BaseItemDto {
func getLiveProgressPercentage() -> Double { func getLiveProgressPercentage() -> Double {
if let startDate = self.startDate, if let startDate = self.startDate,
let endDate = self.endDate { let endDate = self.endDate
{
let start = startDate.timeIntervalSinceReferenceDate let start = startDate.timeIntervalSinceReferenceDate
let end = endDate.timeIntervalSinceReferenceDate let end = endDate.timeIntervalSinceReferenceDate
let now = Date().timeIntervalSinceReferenceDate let now = Date().timeIntervalSinceReferenceDate
@ -276,16 +276,18 @@ public extension BaseItemDto {
} }
if let mediaStreams = mediaStreams { if let mediaStreams = mediaStreams {
let audioStreams = mediaStreams.filter({ $0.type == .audio }) let audioStreams = mediaStreams.filter { $0.type == .audio }
let subtitleStreams = mediaStreams.filter({ $0.type == .subtitle }) let subtitleStreams = mediaStreams.filter { $0.type == .subtitle }
if !audioStreams.isEmpty { if !audioStreams.isEmpty {
let audioList = audioStreams.compactMap({ "\($0.displayTitle ?? "No Title") (\($0.codec ?? "No Codec"))" }).joined(separator: ", ") let audioList = audioStreams.compactMap { "\($0.displayTitle ?? "No Title") (\($0.codec ?? "No Codec"))" }
.joined(separator: ", ")
mediaItems.append(ItemDetail(title: "Audio", content: audioList)) mediaItems.append(ItemDetail(title: "Audio", content: audioList))
} }
if !subtitleStreams.isEmpty { if !subtitleStreams.isEmpty {
let subtitleList = subtitleStreams.compactMap({ "\($0.displayTitle ?? "No Title") (\($0.codec ?? "No Codec"))" }).joined(separator: ", ") let subtitleList = subtitleStreams.compactMap { "\($0.displayTitle ?? "No Title") (\($0.codec ?? "No Codec"))" }
.joined(separator: ", ")
mediaItems.append(ItemDetail(title: "Subtitles", content: subtitleList)) mediaItems.append(ItemDetail(title: "Subtitles", content: subtitleList))
} }
} }

View File

@ -1,9 +1,10 @@
/* SwiftFin is subject to the terms of the Mozilla Public //
* License, v2.0. If a copy of the MPL was not distributed with this // Swiftfin is subject to the terms of the Mozilla Public
* file, you can obtain one at https://mozilla.org/MPL/2.0/. // 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 2021 Aiden Vigue & Jellyfin Contributors //
*/ // Copyright (c) 2022 Jellyfin & Jellyfin Contributors
//
import Foundation import Foundation
import JellyfinAPI import JellyfinAPI
@ -12,6 +13,7 @@ import UIKit
extension BaseItemPerson { extension BaseItemPerson {
// MARK: Get Image // MARK: Get Image
func getImage(baseURL: String, maxWidth: Int) -> URL { func getImage(baseURL: String, maxWidth: Int) -> URL {
let x = UIScreen.main.nativeScale * CGFloat(maxWidth) let x = UIScreen.main.nativeScale * CGFloat(maxWidth)
@ -45,7 +47,8 @@ extension BaseItemPerson {
let split = role.split(separator: "/") let split = role.split(separator: "/")
guard split.count > 1 else { return role } guard split.count > 1 else { return role }
guard let firstRole = split.first?.trimmingCharacters(in: CharacterSet(charactersIn: " ")), let lastRole = split.last?.trimmingCharacters(in: CharacterSet(charactersIn: " ")) else { return role } guard let firstRole = split.first?.trimmingCharacters(in: CharacterSet(charactersIn: " ")),
let lastRole = split.last?.trimmingCharacters(in: CharacterSet(charactersIn: " ")) else { return role }
var final = firstRole var final = firstRole
@ -59,39 +62,41 @@ extension BaseItemPerson {
} }
// MARK: PortraitImageStackable // MARK: PortraitImageStackable
extension BaseItemPerson: PortraitImageStackable { extension BaseItemPerson: PortraitImageStackable {
public var portraitImageID: String { public var portraitImageID: String {
return (id ?? "noid") + title + (subtitle ?? "nodescription") + blurHash + failureInitials (id ?? "noid") + title + (subtitle ?? "nodescription") + blurHash + failureInitials
} }
public func imageURLContsructor(maxWidth: Int) -> URL { public func imageURLContsructor(maxWidth: Int) -> URL {
return self.getImage(baseURL: SessionManager.main.currentLogin.server.currentURI, maxWidth: maxWidth) self.getImage(baseURL: SessionManager.main.currentLogin.server.currentURI, maxWidth: maxWidth)
} }
public var title: String { public var title: String {
return self.name ?? "" self.name ?? ""
} }
public var subtitle: String? { public var subtitle: String? {
return self.firstRole() self.firstRole()
} }
public var blurHash: String { public var blurHash: String {
return self.getBlurHash() self.getBlurHash()
} }
public var failureInitials: String { public var failureInitials: String {
guard let name = self.name else { return "" } guard let name = self.name else { return "" }
let initials = name.split(separator: " ").compactMap({ String($0).first }) let initials = name.split(separator: " ").compactMap { String($0).first }
return String(initials) return String(initials)
} }
public var showTitle: Bool { public var showTitle: Bool {
return true true
} }
} }
// MARK: DiplayedType // MARK: DiplayedType
extension BaseItemPerson { extension BaseItemPerson {
// Only displayed person types. // Only displayed person types.
@ -103,7 +108,7 @@ extension BaseItemPerson {
case producer = "Producer" case producer = "Producer"
static var allCasesRaw: [String] { static var allCasesRaw: [String] {
return self.allCases.map({ $0.rawValue }) self.allCases.map(\.rawValue)
} }
} }
} }

View File

@ -1,11 +1,10 @@
// //
/* // Swiftfin is subject to the terms of the Mozilla Public
* SwiftFin is subject to the terms of the Mozilla Public // License, v2.0. If a copy of the MPL was not distributed with this
* 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/.
* file, you can obtain one at https://mozilla.org/MPL/2.0/. //
* // Copyright (c) 2022 Jellyfin & Jellyfin Contributors
* Copyright 2021 Aiden Vigue & Jellyfin Contributors //
*/
import Foundation import Foundation
@ -18,6 +17,6 @@ struct JellyfinAPIError: Error {
} }
var localizedDescription: String { var localizedDescription: String {
return message message
} }
} }

View File

@ -1,11 +1,10 @@
// //
/* // Swiftfin is subject to the terms of the Mozilla Public
* SwiftFin is subject to the terms of the Mozilla Public // License, v2.0. If a copy of the MPL was not distributed with this
* 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/.
* file, you can obtain one at https://mozilla.org/MPL/2.0/. //
* // Copyright (c) 2022 Jellyfin & Jellyfin Contributors
* Copyright 2021 Aiden Vigue & Jellyfin Contributors //
*/
import Foundation import Foundation
import JellyfinAPI import JellyfinAPI

View File

@ -1,17 +1,16 @@
// //
/* // Swiftfin is subject to the terms of the Mozilla Public
* SwiftFin is subject to the terms of the Mozilla Public // License, v2.0. If a copy of the MPL was not distributed with this
* 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/.
* file, you can obtain one at https://mozilla.org/MPL/2.0/. //
* // Copyright (c) 2022 Jellyfin & Jellyfin Contributors
* Copyright 2021 Aiden Vigue & Jellyfin Contributors //
*/
import Foundation import Foundation
import JellyfinAPI import JellyfinAPI
extension NameGuidPair: PillStackable { extension NameGuidPair: PillStackable {
var title: String { var title: String {
return self.name ?? "" self.name ?? ""
} }
} }

View File

@ -1,9 +1,10 @@
/* JellyfinPlayer/Swiftfin is subject to the terms of the Mozilla Public //
* License, v2.0. If a copy of the MPL was not distributed with this // Swiftfin is subject to the terms of the Mozilla Public
* file, you can obtain one at https://mozilla.org/MPL/2.0/. // 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 2021 Aiden Vigue & Jellyfin Contributors //
*/ // Copyright (c) 2022 Jellyfin & Jellyfin Contributors
//
import Foundation import Foundation
import SwiftUI import SwiftUI

View File

@ -1,16 +1,15 @@
// //
/* // Swiftfin is subject to the terms of the Mozilla Public
* SwiftFin is subject to the terms of the Mozilla Public // License, v2.0. If a copy of the MPL was not distributed with this
* 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/.
* file, you can obtain one at https://mozilla.org/MPL/2.0/. //
* // Copyright (c) 2022 Jellyfin & Jellyfin Contributors
* Copyright 2021 Aiden Vigue & Jellyfin Contributors //
*/
import UIKit import UIKit
extension UIDevice { extension UIDevice {
static var vendorUUIDString: String { static var vendorUUIDString: String {
return current.identifierForVendor!.uuidString current.identifierForVendor!.uuidString
} }
} }

View File

@ -1,11 +1,10 @@
// //
/* // Swiftfin is subject to the terms of the Mozilla Public
* SwiftFin is subject to the terms of the Mozilla Public // License, v2.0. If a copy of the MPL was not distributed with this
* 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/.
* file, you can obtain one at https://mozilla.org/MPL/2.0/. //
* // Copyright (c) 2022 Jellyfin & Jellyfin Contributors
* Copyright 2021 Aiden Vigue & Jellyfin Contributors //
*/
import Foundation import Foundation

View File

@ -1,11 +1,10 @@
// //
/* // Swiftfin is subject to the terms of the Mozilla Public
* SwiftFin is subject to the terms of the Mozilla Public // License, v2.0. If a copy of the MPL was not distributed with this
* 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/.
* file, you can obtain one at https://mozilla.org/MPL/2.0/. //
* // Copyright (c) 2022 Jellyfin & Jellyfin Contributors
* Copyright 2021 Aiden Vigue & Jellyfin Contributors //
*/
import Foundation import Foundation

View File

@ -1,17 +1,16 @@
// //
/* // Swiftfin is subject to the terms of the Mozilla Public
* SwiftFin is subject to the terms of the Mozilla Public // License, v2.0. If a copy of the MPL was not distributed with this
* 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/.
* file, you can obtain one at https://mozilla.org/MPL/2.0/. //
* // Copyright (c) 2022 Jellyfin & Jellyfin Contributors
* Copyright 2021 Aiden Vigue & Jellyfin Contributors //
*/
import Foundation import Foundation
import SwiftUI import SwiftUI
extension View { extension View {
func eraseToAnyView() -> AnyView { func eraseToAnyView() -> AnyView {
return AnyView(self) AnyView(self)
} }
} }

View File

@ -1,3 +1,11 @@
//
// 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) 2022 Jellyfin & Jellyfin Contributors
//
// swiftlint:disable all // swiftlint:disable all
// Generated using SwiftGen https://github.com/SwiftGen/SwiftGen // Generated using SwiftGen https://github.com/SwiftGen/SwiftGen
@ -70,8 +78,9 @@ internal enum L10n {
internal static let home = L10n.tr("Localizable", "home") internal static let home = L10n.tr("Localizable", "home")
/// Latest %@ /// Latest %@
internal static func latestWithString(_ p1: Any) -> String { internal static func latestWithString(_ p1: Any) -> String {
return L10n.tr("Localizable", "latestWithString", String(describing: p1)) L10n.tr("Localizable", "latestWithString", String(describing: p1))
} }
/// Library /// Library
internal static let library = L10n.tr("Localizable", "library") internal static let library = L10n.tr("Localizable", "library")
/// Light /// Light
@ -84,8 +93,9 @@ internal enum L10n {
internal static let login = L10n.tr("Localizable", "login") internal static let login = L10n.tr("Localizable", "login")
/// Login to %@ /// Login to %@
internal static func loginToWithString(_ p1: Any) -> String { internal static func loginToWithString(_ p1: Any) -> String {
return L10n.tr("Localizable", "loginToWithString", String(describing: p1)) L10n.tr("Localizable", "loginToWithString", String(describing: p1))
} }
/// More Like This /// More Like This
internal static let moreLikeThis = L10n.tr("Localizable", "moreLikeThis") internal static let moreLikeThis = L10n.tr("Localizable", "moreLikeThis")
/// Next Up /// Next Up
@ -96,16 +106,18 @@ internal enum L10n {
internal static let noResults = L10n.tr("Localizable", "noResults") internal static let noResults = L10n.tr("Localizable", "noResults")
/// Type: %@ not implemented yet :( /// Type: %@ not implemented yet :(
internal static func notImplementedYetWithType(_ p1: Any) -> String { internal static func notImplementedYetWithType(_ p1: Any) -> String {
return L10n.tr("Localizable", "notImplementedYetWithType", String(describing: p1)) L10n.tr("Localizable", "notImplementedYetWithType", String(describing: p1))
} }
/// Ok /// Ok
internal static let ok = L10n.tr("Localizable", "ok") internal static let ok = L10n.tr("Localizable", "ok")
/// Other User /// Other User
internal static let otherUser = L10n.tr("Localizable", "otherUser") internal static let otherUser = L10n.tr("Localizable", "otherUser")
/// Page %1$@ of %2$@ /// Page %1$@ of %2$@
internal static func pageOfWithNumbers(_ p1: Any, _ p2: Any) -> String { internal static func pageOfWithNumbers(_ p1: Any, _ p2: Any) -> String {
return L10n.tr("Localizable", "pageOfWithNumbers", String(describing: p1), String(describing: p2)) L10n.tr("Localizable", "pageOfWithNumbers", String(describing: p1), String(describing: p2))
} }
/// Password /// Password
internal static let password = L10n.tr("Localizable", "password") internal static let password = L10n.tr("Localizable", "password")
/// Play /// Play
@ -122,8 +134,9 @@ internal enum L10n {
internal static let search = L10n.tr("Localizable", "search") internal static let search = L10n.tr("Localizable", "search")
/// S%1$@:E%2$@ /// S%1$@:E%2$@
internal static func seasonAndEpisode(_ p1: Any, _ p2: Any) -> String { internal static func seasonAndEpisode(_ p1: Any, _ p2: Any) -> String {
return L10n.tr("Localizable", "seasonAndEpisode", String(describing: p1), String(describing: p2)) L10n.tr("Localizable", "seasonAndEpisode", String(describing: p1), String(describing: p2))
} }
/// Seasons /// Seasons
internal static let seasons = L10n.tr("Localizable", "seasons") internal static let seasons = L10n.tr("Localizable", "seasons")
/// See All /// See All
@ -132,16 +145,18 @@ internal enum L10n {
internal static let selectCastDestination = L10n.tr("Localizable", "selectCastDestination") internal static let selectCastDestination = L10n.tr("Localizable", "selectCastDestination")
/// Server %s already exists. Add new URL? /// Server %s already exists. Add new URL?
internal static func serverAlreadyExistsPrompt(_ p1: UnsafePointer<CChar>) -> String { internal static func serverAlreadyExistsPrompt(_ p1: UnsafePointer<CChar>) -> String {
return L10n.tr("Localizable", "serverAlreadyExistsPrompt", p1) L10n.tr("Localizable", "serverAlreadyExistsPrompt", p1)
} }
/// Server Information /// Server Information
internal static let serverInformation = L10n.tr("Localizable", "serverInformation") internal static let serverInformation = L10n.tr("Localizable", "serverInformation")
/// Server URL /// Server URL
internal static let serverURL = L10n.tr("Localizable", "serverURL") internal static let serverURL = L10n.tr("Localizable", "serverURL")
/// Signed in as %@ /// Signed in as %@
internal static func signedInAsWithString(_ p1: Any) -> String { internal static func signedInAsWithString(_ p1: Any) -> String {
return L10n.tr("Localizable", "signedInAsWithString", String(describing: p1)) L10n.tr("Localizable", "signedInAsWithString", String(describing: p1))
} }
/// Sort by /// Sort by
internal static let sortBy = L10n.tr("Localizable", "sortBy") internal static let sortBy = L10n.tr("Localizable", "sortBy")
/// STUDIO /// STUDIO
@ -169,6 +184,7 @@ internal enum L10n {
/// Your Favorites /// Your Favorites
internal static let yourFavorites = L10n.tr("Localizable", "yourFavorites") internal static let yourFavorites = L10n.tr("Localizable", "yourFavorites")
} }
// swiftlint:enable explicit_type_interface function_parameter_count identifier_name line_length // swiftlint:enable explicit_type_interface function_parameter_count identifier_name line_length
// swiftlint:enable nesting type_body_length type_name vertical_whitespace_opening_braces // swiftlint:enable nesting type_body_length type_name vertical_whitespace_opening_braces
@ -191,4 +207,5 @@ private final class BundleToken {
#endif #endif
}() }()
} }
// swiftlint:enable convenience_type // swiftlint:enable convenience_type

View File

@ -1,11 +1,10 @@
// //
/* // Swiftfin is subject to the terms of the Mozilla Public
* SwiftFin is subject to the terms of the Mozilla Public // License, v2.0. If a copy of the MPL was not distributed with this
* 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/.
* file, you can obtain one at https://mozilla.org/MPL/2.0/. //
* // Copyright (c) 2022 Jellyfin & Jellyfin Contributors
* Copyright 2021 Aiden Vigue & Jellyfin Contributors //
*/
import Defaults import Defaults
import SwiftUI import SwiftUI

View File

@ -1,11 +1,10 @@
// //
/* // Swiftfin is subject to the terms of the Mozilla Public
* SwiftFin is subject to the terms of the Mozilla Public // License, v2.0. If a copy of the MPL was not distributed with this
* 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/.
* file, you can obtain one at https://mozilla.org/MPL/2.0/. //
* // Copyright (c) 2022 Jellyfin & Jellyfin Contributors
* Copyright 2021 Aiden Vigue & Jellyfin Contributors //
*/
import Foundation import Foundation

View File

@ -1,11 +1,10 @@
// //
/* // Swiftfin is subject to the terms of the Mozilla Public
* SwiftFin is subject to the terms of the Mozilla Public // License, v2.0. If a copy of the MPL was not distributed with this
* 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/.
* file, you can obtain one at https://mozilla.org/MPL/2.0/. //
* // Copyright (c) 2022 Jellyfin & Jellyfin Contributors
* Copyright 2021 Aiden Vigue & Jellyfin Contributors //
*/
import Foundation import Foundation
import JellyfinAPI import JellyfinAPI
@ -21,5 +20,4 @@ struct DetailItem {
let baseItem: BaseItemDto let baseItem: BaseItemDto
let type: DetailItemType let type: DetailItemType
} }

View File

@ -1,9 +1,10 @@
/* JellyfinPlayer/Swiftfin is subject to the terms of the Mozilla Public //
* License, v2.0. If a copy of the MPL was not distributed with this // Swiftfin is subject to the terms of the Mozilla Public
* file, you can obtain one at https://mozilla.org/MPL/2.0/. // 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 2021 Aiden Vigue & Jellyfin Contributors //
*/ // Copyright (c) 2022 Jellyfin & Jellyfin Contributors
//
// lol can someone buy me a coffee this took forever :| // lol can someone buy me a coffee this took forever :|
@ -47,25 +48,34 @@ class DeviceProfileBuilder {
// Build direct play profiles // Build direct play profiles
var directPlayProfiles: [DirectPlayProfile] = [] var directPlayProfiles: [DirectPlayProfile] = []
directPlayProfiles = [DirectPlayProfile(container: "mov,mp4,mkv,webm", audioCodec: "aac,mp3,wav", videoCodec: "h264,mpeg4,vp9", type: .video)] directPlayProfiles =
[DirectPlayProfile(container: "mov,mp4,mkv,webm", audioCodec: "aac,mp3,wav", videoCodec: "h264,mpeg4,vp9", type: .video)]
// Device supports Dolby Digital (AC3, EAC3) // Device supports Dolby Digital (AC3, EAC3)
if supportsFeature(minimumSupported: .A8X) { if supportsFeature(minimumSupported: .A8X) {
if supportsFeature(minimumSupported: .A9) { if supportsFeature(minimumSupported: .A9) {
directPlayProfiles = [DirectPlayProfile(container: "mov,mp4,mkv,webm", audioCodec: "aac,mp3,wav,ac3,eac3,flac,opus", videoCodec: "hevc,h264,hev1,mpeg4,vp9", type: .video)] // HEVC/H.264 with Dolby Digital directPlayProfiles = [DirectPlayProfile(container: "mov,mp4,mkv,webm", audioCodec: "aac,mp3,wav,ac3,eac3,flac,opus",
videoCodec: "hevc,h264,hev1,mpeg4,vp9",
type: .video)] // HEVC/H.264 with Dolby Digital
} else { } else {
directPlayProfiles = [DirectPlayProfile(container: "mov,mp4,mkv,webm", audioCodec: "ac3,eac3,aac,mp3,wav,opus", videoCodec: "h264,mpeg4,vp9", type: .video)] // H.264 with Dolby Digital directPlayProfiles = [DirectPlayProfile(container: "mov,mp4,mkv,webm", audioCodec: "ac3,eac3,aac,mp3,wav,opus",
videoCodec: "h264,mpeg4,vp9", type: .video)] // H.264 with Dolby Digital
} }
} }
// Device supports Dolby Vision? // Device supports Dolby Vision?
if supportsFeature(minimumSupported: .A10X) { if supportsFeature(minimumSupported: .A10X) {
directPlayProfiles = [DirectPlayProfile(container: "mov,mp4,mkv,webm", audioCodec: "aac,mp3,wav,ac3,eac3,flac,opus", videoCodec: "dvhe,dvh1,h264,hevc,hev1,mpeg4,vp9", type: .video)] // H.264/HEVC with Dolby Digital - No Atmos - Vision directPlayProfiles = [DirectPlayProfile(container: "mov,mp4,mkv,webm", audioCodec: "aac,mp3,wav,ac3,eac3,flac,opus",
videoCodec: "dvhe,dvh1,h264,hevc,hev1,mpeg4,vp9",
type: .video)] // H.264/HEVC with Dolby Digital - No Atmos - Vision
} }
// Device supports Dolby Atmos? // Device supports Dolby Atmos?
if supportsFeature(minimumSupported: .A12) { if supportsFeature(minimumSupported: .A12) {
directPlayProfiles = [DirectPlayProfile(container: "mov,mp4,mkv,webm", audioCodec: "aac,mp3,wav,ac3,eac3,flac,truehd,dts,dca,opus", videoCodec: "h264,hevc,dvhe,dvh1,h264,hevc,hev1,mpeg4,vp9", type: .video)] // H.264/HEVC with Dolby Digital & Atmos - Vision directPlayProfiles = [DirectPlayProfile(container: "mov,mp4,mkv,webm",
audioCodec: "aac,mp3,wav,ac3,eac3,flac,truehd,dts,dca,opus",
videoCodec: "h264,hevc,dvhe,dvh1,h264,hevc,hev1,mpeg4,vp9",
type: .video)] // H.264/HEVC with Dolby Digital & Atmos - Vision
} }
// Build transcoding profiles // Build transcoding profiles
@ -75,29 +85,41 @@ class DeviceProfileBuilder {
// Device supports Dolby Digital (AC3, EAC3) // Device supports Dolby Digital (AC3, EAC3)
if supportsFeature(minimumSupported: .A8X) { if supportsFeature(minimumSupported: .A8X) {
if supportsFeature(minimumSupported: .A9) { if supportsFeature(minimumSupported: .A9) {
transcodingProfiles = [TranscodingProfile(container: "ts", type: .video, videoCodec: "h264,hevc,mpeg4", audioCodec: "aac,mp3,wav,eac3,ac3,flac,opus", _protocol: "hls", context: .streaming, maxAudioChannels: "6", minSegments: 2, breakOnNonKeyFrames: true)] transcodingProfiles = [TranscodingProfile(container: "ts", type: .video, videoCodec: "h264,hevc,mpeg4",
audioCodec: "aac,mp3,wav,eac3,ac3,flac,opus", _protocol: "hls",
context: .streaming, maxAudioChannels: "6", minSegments: 2,
breakOnNonKeyFrames: true)]
} else { } else {
transcodingProfiles = [TranscodingProfile(container: "ts", type: .video, videoCodec: "h264,mpeg4", audioCodec: "aac,mp3,wav,eac3,ac3,opus", _protocol: "hls", context: .streaming, maxAudioChannels: "6", minSegments: 2, breakOnNonKeyFrames: true)] transcodingProfiles = [TranscodingProfile(container: "ts", type: .video, videoCodec: "h264,mpeg4",
audioCodec: "aac,mp3,wav,eac3,ac3,opus", _protocol: "hls",
context: .streaming, maxAudioChannels: "6", minSegments: 2,
breakOnNonKeyFrames: true)]
} }
} }
// Device supports FLAC? // Device supports FLAC?
if supportsFeature(minimumSupported: .A10X) { if supportsFeature(minimumSupported: .A10X) {
transcodingProfiles = [TranscodingProfile(container: "ts", type: .video, videoCodec: "hevc,h264,mpeg4", audioCodec: "aac,mp3,wav,ac3,eac3,flac,opus", _protocol: "hls", context: .streaming, maxAudioChannels: "6", minSegments: 2, breakOnNonKeyFrames: true)] transcodingProfiles = [TranscodingProfile(container: "ts", type: .video, videoCodec: "hevc,h264,mpeg4",
audioCodec: "aac,mp3,wav,ac3,eac3,flac,opus", _protocol: "hls",
context: .streaming, maxAudioChannels: "6", minSegments: 2,
breakOnNonKeyFrames: true)]
} }
var codecProfiles: [CodecProfile] = [] var codecProfiles: [CodecProfile] = []
let h264CodecConditions: [ProfileCondition] = [ let h264CodecConditions: [ProfileCondition] = [
ProfileCondition(condition: .notEquals, property: .isAnamorphic, value: "true", isRequired: false), ProfileCondition(condition: .notEquals, property: .isAnamorphic, value: "true", isRequired: false),
ProfileCondition(condition: .equalsAny, property: .videoProfile, value: "high|main|baseline|constrained baseline", isRequired: false), ProfileCondition(condition: .equalsAny, property: .videoProfile, value: "high|main|baseline|constrained baseline",
isRequired: false),
ProfileCondition(condition: .lessThanEqual, property: .videoLevel, value: "80", isRequired: false), ProfileCondition(condition: .lessThanEqual, property: .videoLevel, value: "80", isRequired: false),
ProfileCondition(condition: .notEquals, property: .isInterlaced, value: "true", isRequired: false)] ProfileCondition(condition: .notEquals, property: .isInterlaced, value: "true", isRequired: false),
]
let hevcCodecConditions: [ProfileCondition] = [ let hevcCodecConditions: [ProfileCondition] = [
ProfileCondition(condition: .notEquals, property: .isAnamorphic, value: "true", isRequired: false), ProfileCondition(condition: .notEquals, property: .isAnamorphic, value: "true", isRequired: false),
ProfileCondition(condition: .equalsAny, property: .videoProfile, value: "high|main|main 10", isRequired: false), ProfileCondition(condition: .equalsAny, property: .videoProfile, value: "high|main|main 10", isRequired: false),
ProfileCondition(condition: .lessThanEqual, property: .videoLevel, value: "175", isRequired: false), ProfileCondition(condition: .lessThanEqual, property: .videoLevel, value: "175", isRequired: false),
ProfileCondition(condition: .notEquals, property: .isInterlaced, value: "true", isRequired: false)] ProfileCondition(condition: .notEquals, property: .isInterlaced, value: "true", isRequired: false),
]
codecProfiles.append(CodecProfile(type: .video, applyConditions: h264CodecConditions, codec: "h264")) codecProfiles.append(CodecProfile(type: .video, applyConditions: h264CodecConditions, codec: "h264"))
@ -124,13 +146,37 @@ class DeviceProfileBuilder {
let responseProfiles: [ResponseProfile] = [ResponseProfile(container: "m4v", type: .video, mimeType: "video/mp4")] let responseProfiles: [ResponseProfile] = [ResponseProfile(container: "m4v", type: .video, mimeType: "video/mp4")]
let profile = DeviceProfile(maxStreamingBitrate: maxStreamingBitrate, maxStaticBitrate: maxStaticBitrate, musicStreamingTranscodingBitrate: musicStreamingTranscodingBitrate, directPlayProfiles: directPlayProfiles, transcodingProfiles: transcodingProfiles, containerProfiles: [], codecProfiles: codecProfiles, responseProfiles: responseProfiles, subtitleProfiles: subtitleProfiles) let profile = DeviceProfile(maxStreamingBitrate: maxStreamingBitrate, maxStaticBitrate: maxStaticBitrate,
musicStreamingTranscodingBitrate: musicStreamingTranscodingBitrate,
directPlayProfiles: directPlayProfiles, transcodingProfiles: transcodingProfiles, containerProfiles: [],
codecProfiles: codecProfiles, responseProfiles: responseProfiles, subtitleProfiles: subtitleProfiles)
return profile return profile
} }
private func supportsFeature(minimumSupported: CPUModel) -> Bool { private func supportsFeature(minimumSupported: CPUModel) -> Bool {
let intValues: [CPUModel: Int] = [.A4: 1, .A5: 2, .A5X: 3, .A6: 4, .A6X: 5, .A7: 6, .A7X: 7, .A8: 8, .A8X: 9, .A9: 10, .A9X: 11, .A10: 12, .A10X: 13, .A11: 14, .A12: 15, .A12X: 16, .A12Z: 16, .A13: 17, .A14: 18, .A99: 99] let intValues: [CPUModel: Int] = [
.A4: 1,
.A5: 2,
.A5X: 3,
.A6: 4,
.A6X: 5,
.A7: 6,
.A7X: 7,
.A8: 8,
.A8X: 9,
.A9: 10,
.A9X: 11,
.A10: 12,
.A10X: 13,
.A11: 14,
.A12: 15,
.A12X: 16,
.A12Z: 16,
.A13: 17,
.A14: 18,
.A99: 99,
]
return intValues[CPUinfo()] ?? 0 >= intValues[minimumSupported] ?? 0 return intValues[CPUinfo()] ?? 0 >= intValues[minimumSupported] ?? 0
} }

View File

@ -1,11 +1,10 @@
// //
/* // Swiftfin is subject to the terms of the Mozilla Public
* SwiftFin is subject to the terms of the Mozilla Public // License, v2.0. If a copy of the MPL was not distributed with this
* 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/.
* file, you can obtain one at https://mozilla.org/MPL/2.0/. //
* // Copyright (c) 2022 Jellyfin & Jellyfin Contributors
* Copyright 2021 Aiden Vigue & Jellyfin Contributors //
*/
// https://www.hackingwithswift.com/quick-start/swiftui/how-to-detect-device-rotation // https://www.hackingwithswift.com/quick-start/swiftui/how-to-detect-device-rotation
import Foundation import Foundation

View File

@ -1,11 +1,10 @@
// //
/* // Swiftfin is subject to the terms of the Mozilla Public
* SwiftFin is subject to the terms of the Mozilla Public // License, v2.0. If a copy of the MPL was not distributed with this
* 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/.
* file, you can obtain one at https://mozilla.org/MPL/2.0/. //
* // Copyright (c) 2022 Jellyfin & Jellyfin Contributors
* Copyright 2021 Aiden Vigue & Jellyfin Contributors //
*/
import Defaults import Defaults
import Foundation import Foundation

View File

@ -1,11 +1,10 @@
// //
/* // Swiftfin is subject to the terms of the Mozilla Public
* SwiftFin is subject to the terms of the Mozilla Public // License, v2.0. If a copy of the MPL was not distributed with this
* 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/.
* file, you can obtain one at https://mozilla.org/MPL/2.0/. //
* // Copyright (c) 2022 Jellyfin & Jellyfin Contributors
* Copyright 2021 Aiden Vigue & Jellyfin Contributors //
*/
import Defaults import Defaults
import UIKit import UIKit

View File

@ -1,11 +1,10 @@
// //
/* // Swiftfin is subject to the terms of the Mozilla Public
* SwiftFin is subject to the terms of the Mozilla Public // License, v2.0. If a copy of the MPL was not distributed with this
* 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/.
* file, you can obtain one at https://mozilla.org/MPL/2.0/. //
* // Copyright (c) 2022 Jellyfin & Jellyfin Contributors
* Copyright 2021 Aiden Vigue & Jellyfin Contributors //
*/
import Defaults import Defaults
import Foundation import Foundation

View File

@ -1,11 +1,10 @@
// //
/* // Swiftfin is subject to the terms of the Mozilla Public
* SwiftFin is subject to the terms of the Mozilla Public // License, v2.0. If a copy of the MPL was not distributed with this
* 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/.
* file, you can obtain one at https://mozilla.org/MPL/2.0/. //
* // Copyright (c) 2022 Jellyfin & Jellyfin Contributors
* Copyright 2021 Aiden Vigue & Jellyfin Contributors //
*/
import Foundation import Foundation

View File

@ -0,0 +1,41 @@
//
// 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) 2022 Jellyfin & Jellyfin Contributors
//
import Foundation
enum PlaybackSpeed: Double, CaseIterable {
case quarter = 0.25
case half = 0.5
case threeQuarter = 0.75
case one = 1.0
case oneQuarter = 1.25
case oneHalf = 1.5
case oneThreeQuarter = 1.75
case two = 2.0
var displayTitle: String {
switch self {
case .quarter:
return "0.25x"
case .half:
return "0.5x"
case .threeQuarter:
return "0.75x"
case .one:
return "1x"
case .oneQuarter:
return "1.25x"
case .oneHalf:
return "1.5x"
case .oneThreeQuarter:
return "1.75x"
case .two:
return "2x"
}
}
}

View File

@ -1,11 +1,10 @@
// //
/* // Swiftfin is subject to the terms of the Mozilla Public
* SwiftFin is subject to the terms of the Mozilla Public // License, v2.0. If a copy of the MPL was not distributed with this
* 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/.
* file, you can obtain one at https://mozilla.org/MPL/2.0/. //
* // Copyright (c) 2022 Jellyfin & Jellyfin Contributors
* Copyright 2021 Aiden Vigue & Jellyfin Contributors //
*/
import Foundation import Foundation

View File

@ -1,11 +1,10 @@
// //
/* // Swiftfin is subject to the terms of the Mozilla Public
* SwiftFin is subject to the terms of the Mozilla Public // License, v2.0. If a copy of the MPL was not distributed with this
* 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/.
* file, you can obtain one at https://mozilla.org/MPL/2.0/. //
* // Copyright (c) 2022 Jellyfin & Jellyfin Contributors
* Copyright 2021 Aiden Vigue & Jellyfin Contributors //
*/
import Foundation import Foundation

View File

@ -1,11 +1,10 @@
// //
/* // Swiftfin is subject to the terms of the Mozilla Public
* SwiftFin is subject to the terms of the Mozilla Public // License, v2.0. If a copy of the MPL was not distributed with this
* 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/.
* file, you can obtain one at https://mozilla.org/MPL/2.0/. //
* // Copyright (c) 2022 Jellyfin & Jellyfin Contributors
* Copyright 2021 Aiden Vigue & Jellyfin Contributors //
*/
import Foundation import Foundation

View File

@ -1,9 +1,10 @@
/* JellyfinPlayer/Swiftfin is subject to the terms of the Mozilla Public //
* License, v2.0. If a copy of the MPL was not distributed with this // Swiftfin is subject to the terms of the Mozilla Public
* file, you can obtain one at https://mozilla.org/MPL/2.0/. // 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 2021 Aiden Vigue & Jellyfin Contributors //
*/ // Copyright (c) 2022 Jellyfin & Jellyfin Contributors
//
import Combine import Combine
import Foundation import Foundation

View File

@ -1,14 +1,13 @@
// //
/* // Swiftfin is subject to the terms of the Mozilla Public
* SwiftFin is subject to the terms of the Mozilla Public // License, v2.0. If a copy of the MPL was not distributed with this
* 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/.
* file, you can obtain one at https://mozilla.org/MPL/2.0/. //
* // Copyright (c) 2022 Jellyfin & Jellyfin Contributors
* Copyright 2021 Aiden Vigue & Jellyfin Contributors //
*/
import UIKit
import Defaults import Defaults
import UIKit
enum VideoPlayerJumpLength: Int32, CaseIterable, Defaults.Serializable { enum VideoPlayerJumpLength: Int32, CaseIterable, Defaults.Serializable {
case thirty = 30 case thirty = 30
@ -17,11 +16,11 @@ enum VideoPlayerJumpLength: Int32, CaseIterable, Defaults.Serializable {
case five = 5 case five = 5
var label: String { var label: String {
return "\(self.rawValue) seconds" "\(self.rawValue) seconds"
} }
var shortLabel: String { var shortLabel: String {
return "\(self.rawValue)s" "\(self.rawValue)s"
} }
var forwardImageLabel: String { var forwardImageLabel: String {

View File

@ -1,12 +1,10 @@
// //
/* // Swiftfin is subject to the terms of the Mozilla Public
* SwiftFin is subject to the terms of the Mozilla Public // License, v2.0. If a copy of the MPL was not distributed with this
* 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/.
* file, you can obtain one at https://mozilla.org/MPL/2.0/. //
* // Copyright (c) 2022 Jellyfin & Jellyfin Contributors
* Created by Noah Kamara //
* Copyright 2021 Aiden Vigue & Jellyfin Contributors
*/
import Foundation import Foundation
@ -14,7 +12,7 @@ public class ServerDiscovery {
public struct ServerLookupResponse: Codable, Hashable, Identifiable { public struct ServerLookupResponse: Codable, Hashable, Identifiable {
public func hash(into hasher: inout Hasher) { public func hash(into hasher: inout Hasher) {
return hasher.combine(id) hasher.combine(id)
} }
private let address: String private let address: String
@ -24,6 +22,7 @@ public class ServerDiscovery {
public var url: URL { public var url: URL {
URL(string: self.address)! URL(string: self.address)!
} }
public var host: String { public var host: String {
let components = URLComponents(string: self.address) let components = URLComponents(string: self.address)
if let host = components?.host { if let host = components?.host {
@ -50,11 +49,9 @@ public class ServerDiscovery {
private let broadcastConn: UDPBroadcastConnection private let broadcastConn: UDPBroadcastConnection
public init() { public init() {
func receiveHandler(_ ipAddress: String, _ port: Int, _ response: Data) { func receiveHandler(_ ipAddress: String, _ port: Int, _ response: Data) {}
}
func errorHandler(error: UDPBroadcastConnection.ConnectionError) { func errorHandler(error: UDPBroadcastConnection.ConnectionError) {}
}
self.broadcastConn = try! UDPBroadcastConnection(port: 7359, handler: receiveHandler, errorHandler: errorHandler) self.broadcastConn = try! UDPBroadcastConnection(port: 7359, handler: receiveHandler, errorHandler: errorHandler)
} }

View File

@ -1,20 +1,18 @@
// //
/* // Swiftfin is subject to the terms of the Mozilla Public
* SwiftFin is subject to the terms of the Mozilla Public // License, v2.0. If a copy of the MPL was not distributed with this
* 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/.
* file, you can obtain one at https://mozilla.org/MPL/2.0/. //
* // Copyright (c) 2022 Jellyfin & Jellyfin Contributors
* Created by Gunter Hager on 10.02.16. //
* Copyright © 2016 Gunter Hager. All rights reserved.
*/
import Foundation
import Darwin import Darwin
import Foundation
// Addresses // Addresses
let INADDR_ANY = in_addr(s_addr: 0) let INADDR_ANY = in_addr(s_addr: 0)
let INADDR_BROADCAST = in_addr(s_addr: 0xffffffff) let INADDR_BROADCAST = in_addr(s_addr: 0xFFFF_FFFF)
/// An object representing the UDP broadcast connection. Uses a dispatch source to handle the incoming traffic on the UDP socket. /// An object representing the UDP broadcast connection. Uses a dispatch source to handle the incoming traffic on the UDP socket.
open class UDPBroadcastConnection { open class UDPBroadcastConnection {
@ -38,7 +36,7 @@ open class UDPBroadcastConnection {
var responseSource: DispatchSourceRead? var responseSource: DispatchSourceRead?
/// The dispatch queue to run responseSource & reconnection on /// The dispatch queue to run responseSource & reconnection on
var dispatchQueue: DispatchQueue = DispatchQueue.main var dispatchQueue = DispatchQueue.main
/// Bind to port to start listening without first sending a message /// Bind to port to start listening without first sending a message
var shouldBeBound: Bool = false var shouldBeBound: Bool = false
@ -56,13 +54,11 @@ open class UDPBroadcastConnection {
/// - errorHandler: Handler that gets called when an error occurs. /// - errorHandler: Handler that gets called when an error occurs.
/// - Throws: Throws a `ConnectionError` if an error occurs. /// - Throws: Throws a `ConnectionError` if an error occurs.
public init(port: UInt16, bindIt: Bool = false, handler: ReceiveHandler?, errorHandler: ErrorHandler?) throws { public init(port: UInt16, bindIt: Bool = false, handler: ReceiveHandler?, errorHandler: ErrorHandler?) throws {
self.address = sockaddr_in( self.address = sockaddr_in(sin_len: __uint8_t(MemoryLayout<sockaddr_in>.size),
sin_len: __uint8_t(MemoryLayout<sockaddr_in>.size),
sin_family: sa_family_t(AF_INET), sin_family: sa_family_t(AF_INET),
sin_port: UDPBroadcastConnection.htonsPort(port: port), sin_port: UDPBroadcastConnection.htonsPort(port: port),
sin_addr: INADDR_BROADCAST, sin_addr: INADDR_BROADCAST,
sin_zero: ( 0, 0, 0, 0, 0, 0, 0, 0 ) sin_zero: (0, 0, 0, 0, 0, 0, 0, 0))
)
self.handler = handler self.handler = handler
self.errorHandler = errorHandler self.errorHandler = errorHandler
@ -137,7 +133,8 @@ open class UDPBroadcastConnection {
let UDPSocket = Int32(source.handle) let UDPSocket = Int32(source.handle)
let bytesRead = withUnsafeMutablePointer(to: &socketAddress) { let bytesRead = withUnsafeMutablePointer(to: &socketAddress) {
recvfrom(UDPSocket, UnsafeMutableRawPointer(mutating: response), response.count, 0, UnsafeMutableRawPointer($0).bindMemory(to: sockaddr.self, capacity: 1), &socketAddressLength) recvfrom(UDPSocket, UnsafeMutableRawPointer(mutating: response), response.count, 0,
UnsafeMutableRawPointer($0).bindMemory(to: sockaddr.self, capacity: 1), &socketAddressLength)
} }
do { do {
@ -154,7 +151,11 @@ open class UDPBroadcastConnection {
} }
} }
guard let endpoint = withUnsafePointer(to: &socketAddress, { self.getEndpointFromSocketAddress(socketAddressPointer: UnsafeRawPointer($0).bindMemory(to: sockaddr.self, capacity: 1)) }) guard let endpoint = withUnsafePointer(to: &socketAddress,
{
self
.getEndpointFromSocketAddress(socketAddressPointer: UnsafeRawPointer($0)
.bindMemory(to: sockaddr.self, capacity: 1)) })
else { else {
// debugPrint("Failed to get the address and port from the socket address received from recvfrom") // debugPrint("Failed to get the address and port from the socket address received from recvfrom")
self.closeConnection() self.closeConnection()
@ -174,7 +175,6 @@ open class UDPBroadcastConnection {
self.errorHandler?(ConnectionError.underlying(error: error)) self.errorHandler?(ConnectionError.underlying(error: error))
} }
} }
} }
newResponseSource.resume() newResponseSource.resume()
@ -202,7 +202,7 @@ open class UDPBroadcastConnection {
guard let source = responseSource else { return } guard let source = responseSource else { return }
let UDPSocket = Int32(source.handle) let UDPSocket = Int32(source.handle)
let socketLength = socklen_t(address.sin_len) let socketLength = socklen_t(address.sin_len)
try data.withUnsafeBytes { (broadcastMessage) in try data.withUnsafeBytes { broadcastMessage in
let broadcastMessageLength = data.count let broadcastMessageLength = data.count
let sent = withUnsafeMutablePointer(to: &address) { pointer -> Int in let sent = withUnsafeMutablePointer(to: &address) { pointer -> Int in
let memory = UnsafeRawPointer(pointer).bindMemory(to: sockaddr.self, capacity: 1) let memory = UnsafeRawPointer(pointer).bindMemory(to: sockaddr.self, capacity: 1)
@ -282,9 +282,8 @@ open class UDPBroadcastConnection {
} }
fileprivate class func ntohs(value: CUnsignedShort) -> CUnsignedShort { fileprivate class func ntohs(value: CUnsignedShort) -> CUnsignedShort {
return (value << 8) + (value >> 8) (value << 8) + (value >> 8)
} }
} }
// Created by Gunter Hager on 25.03.19. // Created by Gunter Hager on 25.03.19.
@ -312,5 +311,4 @@ public extension UDPBroadcastConnection {
// Underlying // Underlying
case underlying(error: Error) case underlying(error: Error)
} }
} }

View File

@ -1,11 +1,10 @@
// //
/* // Swiftfin is subject to the terms of the Mozilla Public
* SwiftFin is subject to the terms of the Mozilla Public // License, v2.0. If a copy of the MPL was not distributed with this
* 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/.
* file, you can obtain one at https://mozilla.org/MPL/2.0/. //
* // Copyright (c) 2022 Jellyfin & Jellyfin Contributors
* Copyright 2021 Aiden Vigue & Jellyfin Contributors //
*/
import Foundation import Foundation

View File

@ -1,11 +1,10 @@
// //
/* // Swiftfin is subject to the terms of the Mozilla Public
* SwiftFin is subject to the terms of the Mozilla Public // License, v2.0. If a copy of the MPL was not distributed with this
* 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/.
* file, you can obtain one at https://mozilla.org/MPL/2.0/. //
* // Copyright (c) 2022 Jellyfin & Jellyfin Contributors
* Copyright 2021 Aiden Vigue & Jellyfin Contributors //
*/
import Foundation import Foundation
import Puppy import Puppy
@ -34,7 +33,7 @@ class LogManager {
} }
func logFileURL() -> URL { func logFileURL() -> URL {
return self.getDocumentsDirectory().appendingPathComponent("logs.txt") self.getDocumentsDirectory().appendingPathComponent("logs.txt")
} }
func getDocumentsDirectory() -> URL { func getDocumentsDirectory() -> URL {
@ -49,7 +48,8 @@ class LogManager {
class LogFormatter: LogFormattable { class LogFormatter: LogFormattable {
func formatMessage(_ level: LogLevel, message: String, tag: String, function: String, func formatMessage(_ level: LogLevel, message: String, tag: String, function: String,
file: String, line: UInt, swiftLogInfo: [String: String], file: String, line: UInt, swiftLogInfo: [String: String],
label: String, date: Date, threadID: UInt64) -> String { label: String, date: Date, threadID: UInt64) -> String
{
let file = shortFileName(file).replacingOccurrences(of: ".swift", with: "") let file = shortFileName(file).replacingOccurrences(of: ".swift", with: "")
return " [\(level.emoji) \(level)] \(file)#\(line):\(function) \(message)" return " [\(level.emoji) \(level)] \(file)#\(line):\(function) \(message)"
} }

View File

@ -1,11 +1,10 @@
// //
/* // Swiftfin is subject to the terms of the Mozilla Public
* SwiftFin is subject to the terms of the Mozilla Public // License, v2.0. If a copy of the MPL was not distributed with this
* 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/.
* file, you can obtain one at https://mozilla.org/MPL/2.0/. //
* // Copyright (c) 2022 Jellyfin & Jellyfin Contributors
* Copyright 2021 Aiden Vigue & Jellyfin Contributors //
*/
import Combine import Combine
import CoreData import CoreData
@ -18,8 +17,8 @@ import UIKit
typealias CurrentLogin = (server: SwiftfinStore.State.Server, user: SwiftfinStore.State.User) typealias CurrentLogin = (server: SwiftfinStore.State.Server, user: SwiftfinStore.State.User)
// MARK: NewSessionManager // MARK: NewSessionManager
final class SessionManager {
final class SessionManager {
// MARK: currentLogin // MARK: currentLogin
@ -30,12 +29,15 @@ final class SessionManager {
static let main = SessionManager() static let main = SessionManager()
// MARK: init // MARK: init
private init() { private init() {
if let lastUserID = Defaults[.lastServerUserID], if let lastUserID = Defaults[.lastServerUserID],
let user = try? SwiftfinStore.dataStack.fetchOne(From<SwiftfinStore.Models.StoredUser>(), let user = try? SwiftfinStore.dataStack.fetchOne(From<SwiftfinStore.Models.StoredUser>(),
[Where<SwiftfinStore.Models.StoredUser>("id == %@", lastUserID)]) { [Where<SwiftfinStore.Models.StoredUser>("id == %@", lastUserID)])
{
guard let server = user.server, let accessToken = user.accessToken else { fatalError("No associated server or access token for last user?") } guard let server = user.server,
let accessToken = user.accessToken else { fatalError("No associated server or access token for last user?") }
guard let existingServer = SwiftfinStore.dataStack.fetchExisting(server) else { return } guard let existingServer = SwiftfinStore.dataStack.fetchExisting(server) else { return }
JellyfinAPI.basePath = server.currentURI JellyfinAPI.basePath = server.currentURI
@ -45,20 +47,23 @@ final class SessionManager {
} }
// MARK: fetchServers // MARK: fetchServers
func fetchServers() -> [SwiftfinStore.State.Server] { func fetchServers() -> [SwiftfinStore.State.Server] {
let servers = try! SwiftfinStore.dataStack.fetchAll(From<SwiftfinStore.Models.StoredServer>()) let servers = try! SwiftfinStore.dataStack.fetchAll(From<SwiftfinStore.Models.StoredServer>())
return servers.map({ $0.state }) return servers.map(\.state)
} }
// MARK: fetchUsers // MARK: fetchUsers
func fetchUsers(for server: SwiftfinStore.State.Server) -> [SwiftfinStore.State.User] { func fetchUsers(for server: SwiftfinStore.State.Server) -> [SwiftfinStore.State.User] {
guard let storedServer = try? SwiftfinStore.dataStack.fetchOne(From<SwiftfinStore.Models.StoredServer>(), guard let storedServer = try? SwiftfinStore.dataStack.fetchOne(From<SwiftfinStore.Models.StoredServer>(),
Where<SwiftfinStore.Models.StoredServer>("id == %@", server.id)) Where<SwiftfinStore.Models.StoredServer>("id == %@", server.id))
else { fatalError("No stored server associated with given state server?") } else { fatalError("No stored server associated with given state server?") }
return storedServer.users.map({ $0.state }).sorted(by: { $0.username < $1.username }) return storedServer.users.map(\.state).sorted(by: { $0.username < $1.username })
} }
// MARK: connectToServer publisher // MARK: connectToServer publisher
// Connects to a server at the given uri, storing if successful // Connects to a server at the given uri, storing if successful
func connectToServer(with uri: String) -> AnyPublisher<SwiftfinStore.State.Server, Error> { func connectToServer(with uri: String) -> AnyPublisher<SwiftfinStore.State.Server, Error> {
var uriComponents = URLComponents(string: uri) ?? URLComponents() var uriComponents = URLComponents(string: uri) ?? URLComponents()
@ -76,7 +81,7 @@ final class SessionManager {
JellyfinAPI.basePath = uri JellyfinAPI.basePath = uri
return SystemAPI.getPublicSystemInfo() return SystemAPI.getPublicSystemInfo()
.tryMap({ response -> (SwiftfinStore.Models.StoredServer, UnsafeDataTransaction) in .tryMap { response -> (SwiftfinStore.Models.StoredServer, UnsafeDataTransaction) in
let transaction = SwiftfinStore.dataStack.beginUnsafe() let transaction = SwiftfinStore.dataStack.beginUnsafe()
let newServer = transaction.create(Into<SwiftfinStore.Models.StoredServer>()) let newServer = transaction.create(Into<SwiftfinStore.Models.StoredServer>())
@ -96,30 +101,35 @@ final class SessionManager {
// Check for existing server on device // Check for existing server on device
if let existingServer = try? SwiftfinStore.dataStack.fetchOne(From<SwiftfinStore.Models.StoredServer>(), if let existingServer = try? SwiftfinStore.dataStack.fetchOne(From<SwiftfinStore.Models.StoredServer>(),
[Where<SwiftfinStore.Models.StoredServer>("id == %@", newServer.id)]) { [Where<SwiftfinStore.Models.StoredServer>("id == %@",
newServer.id)])
{
throw SwiftfinStore.Errors.existingServer(existingServer.state) throw SwiftfinStore.Errors.existingServer(existingServer.state)
} }
return (newServer, transaction) return (newServer, transaction)
}) }
.handleEvents(receiveOutput: { (_, transaction) in .handleEvents(receiveOutput: { _, transaction in
try? transaction.commitAndWait() try? transaction.commitAndWait()
}) })
.map({ (server, _) in .map { server, _ in
return server.state server.state
}) }
.eraseToAnyPublisher() .eraseToAnyPublisher()
} }
// MARK: addURIToServer publisher // MARK: addURIToServer publisher
func addURIToServer(server: SwiftfinStore.State.Server, uri: String) -> AnyPublisher<SwiftfinStore.State.Server, Error> { func addURIToServer(server: SwiftfinStore.State.Server, uri: String) -> AnyPublisher<SwiftfinStore.State.Server, Error> {
return Just(server) Just(server)
.tryMap { server -> (SwiftfinStore.Models.StoredServer, UnsafeDataTransaction) in .tryMap { server -> (SwiftfinStore.Models.StoredServer, UnsafeDataTransaction) in
let transaction = SwiftfinStore.dataStack.beginUnsafe() let transaction = SwiftfinStore.dataStack.beginUnsafe()
guard let existingServer = try? SwiftfinStore.dataStack.fetchOne(From<SwiftfinStore.Models.StoredServer>(), guard let existingServer = try? SwiftfinStore.dataStack.fetchOne(From<SwiftfinStore.Models.StoredServer>(),
[Where<SwiftfinStore.Models.StoredServer>("id == %@", server.id)]) else { [Where<SwiftfinStore.Models.StoredServer>("id == %@",
server.id)])
else {
fatalError("No stored server associated with given state server?") fatalError("No stored server associated with given state server?")
} }
@ -128,24 +138,27 @@ final class SessionManager {
return (editServer, transaction) return (editServer, transaction)
} }
.handleEvents(receiveOutput: { (_, transaction) in .handleEvents(receiveOutput: { _, transaction in
try? transaction.commitAndWait() try? transaction.commitAndWait()
}) })
.map({ (server, _) in .map { server, _ in
return server.state server.state
}) }
.eraseToAnyPublisher() .eraseToAnyPublisher()
} }
// MARK: setServerCurrentURI publisher // MARK: setServerCurrentURI publisher
func setServerCurrentURI(server: SwiftfinStore.State.Server, uri: String) -> AnyPublisher<SwiftfinStore.State.Server, Error> { func setServerCurrentURI(server: SwiftfinStore.State.Server, uri: String) -> AnyPublisher<SwiftfinStore.State.Server, Error> {
return Just(server) Just(server)
.tryMap { server -> (SwiftfinStore.Models.StoredServer, UnsafeDataTransaction) in .tryMap { server -> (SwiftfinStore.Models.StoredServer, UnsafeDataTransaction) in
let transaction = SwiftfinStore.dataStack.beginUnsafe() let transaction = SwiftfinStore.dataStack.beginUnsafe()
guard let existingServer = try? SwiftfinStore.dataStack.fetchOne(From<SwiftfinStore.Models.StoredServer>(), guard let existingServer = try? SwiftfinStore.dataStack.fetchOne(From<SwiftfinStore.Models.StoredServer>(),
[Where<SwiftfinStore.Models.StoredServer>("id == %@", server.id)]) else { [Where<SwiftfinStore.Models.StoredServer>("id == %@",
server.id)])
else {
fatalError("No stored server associated with given state server?") fatalError("No stored server associated with given state server?")
} }
@ -158,24 +171,27 @@ final class SessionManager {
return (editServer, transaction) return (editServer, transaction)
} }
.handleEvents(receiveOutput: { (_, transaction) in .handleEvents(receiveOutput: { _, transaction in
try? transaction.commitAndWait() try? transaction.commitAndWait()
}) })
.map({ (server, _) in .map { server, _ in
return server.state server.state
}) }
.eraseToAnyPublisher() .eraseToAnyPublisher()
} }
// MARK: loginUser publisher // MARK: loginUser publisher
// Logs in a user with an associated server, storing if successful // Logs in a user with an associated server, storing if successful
func loginUser(server: SwiftfinStore.State.Server, username: String, password: String) -> AnyPublisher<SwiftfinStore.State.User, Error> { func loginUser(server: SwiftfinStore.State.Server, username: String,
password: String) -> AnyPublisher<SwiftfinStore.State.User, Error>
{
setAuthHeader(with: "") setAuthHeader(with: "")
JellyfinAPI.basePath = server.currentURI JellyfinAPI.basePath = server.currentURI
return UserAPI.authenticateUserByName(authenticateUserByName: AuthenticateUserByName(username: username, pw: password)) return UserAPI.authenticateUserByName(authenticateUserByName: AuthenticateUserByName(username: username, pw: password))
.tryMap({ response -> (SwiftfinStore.Models.StoredServer, SwiftfinStore.Models.StoredUser, UnsafeDataTransaction) in .tryMap { response -> (SwiftfinStore.Models.StoredServer, SwiftfinStore.Models.StoredUser, UnsafeDataTransaction) in
guard let accessToken = response.accessToken else { throw JellyfinAPIError("Access token missing from network call") } guard let accessToken = response.accessToken else { throw JellyfinAPIError("Access token missing from network call") }
@ -191,7 +207,9 @@ final class SessionManager {
// Check for existing user on device // Check for existing user on device
if let existingUser = try? SwiftfinStore.dataStack.fetchOne(From<SwiftfinStore.Models.StoredUser>(), if let existingUser = try? SwiftfinStore.dataStack.fetchOne(From<SwiftfinStore.Models.StoredUser>(),
[Where<SwiftfinStore.Models.StoredUser>("id == %@", newUser.id)]) { [Where<SwiftfinStore.Models.StoredUser>("id == %@",
newUser.id)])
{
throw SwiftfinStore.Errors.existingUser(existingUser.state) throw SwiftfinStore.Errors.existingUser(existingUser.state)
} }
@ -200,15 +218,18 @@ final class SessionManager {
newUser.accessToken = newAccessToken newUser.accessToken = newAccessToken
guard let userServer = try? SwiftfinStore.dataStack.fetchOne(From<SwiftfinStore.Models.StoredServer>(), guard let userServer = try? SwiftfinStore.dataStack.fetchOne(From<SwiftfinStore.Models.StoredServer>(),
[Where<SwiftfinStore.Models.StoredServer>("id == %@", server.id)]) [
Where<SwiftfinStore.Models.StoredServer>("id == %@",
server.id),
])
else { fatalError("No stored server associated with given state server?") } else { fatalError("No stored server associated with given state server?") }
guard let editUserServer = transaction.edit(userServer) else { fatalError("Can't get proxy for existing object?") } guard let editUserServer = transaction.edit(userServer) else { fatalError("Can't get proxy for existing object?") }
editUserServer.users.insert(newUser) editUserServer.users.insert(newUser)
return (editUserServer, newUser, transaction) return (editUserServer, newUser, transaction)
}) }
.handleEvents(receiveOutput: { [unowned self] (server, user, transaction) in .handleEvents(receiveOutput: { [unowned self] server, user, transaction in
setAuthHeader(with: user.accessToken?.value ?? "") setAuthHeader(with: user.accessToken?.value ?? "")
try? transaction.commitAndWait() try? transaction.commitAndWait()
@ -221,13 +242,14 @@ final class SessionManager {
currentLogin = (server: currentServer.state, user: currentUser.state) currentLogin = (server: currentServer.state, user: currentUser.state)
SwiftfinNotificationCenter.main.post(name: SwiftfinNotificationCenter.Keys.didSignIn, object: nil) SwiftfinNotificationCenter.main.post(name: SwiftfinNotificationCenter.Keys.didSignIn, object: nil)
}) })
.map({ (_, user, _) in .map { _, user, _ in
return user.state user.state
}) }
.eraseToAnyPublisher() .eraseToAnyPublisher()
} }
// MARK: loginUser // MARK: loginUser
func loginUser(server: SwiftfinStore.State.Server, user: SwiftfinStore.State.User) { func loginUser(server: SwiftfinStore.State.Server, user: SwiftfinStore.State.User) {
JellyfinAPI.basePath = server.currentURI JellyfinAPI.basePath = server.currentURI
Defaults[.lastServerUserID] = user.id Defaults[.lastServerUserID] = user.id
@ -237,6 +259,7 @@ final class SessionManager {
} }
// MARK: logout // MARK: logout
func logout() { func logout() {
currentLogin = nil currentLogin = nil
JellyfinAPI.basePath = "" JellyfinAPI.basePath = ""
@ -246,6 +269,7 @@ final class SessionManager {
} }
// MARK: purge // MARK: purge
func purge() { func purge() {
// Delete all servers // Delete all servers
let servers = fetchServers() let servers = fetchServers()
@ -258,16 +282,20 @@ final class SessionManager {
} }
// MARK: delete user // MARK: delete user
func delete(user: SwiftfinStore.State.User) { func delete(user: SwiftfinStore.State.User) {
guard let storedUser = try? SwiftfinStore.dataStack.fetchOne(From<SwiftfinStore.Models.StoredUser>(), guard let storedUser = try? SwiftfinStore.dataStack.fetchOne(From<SwiftfinStore.Models.StoredUser>(),
[Where<SwiftfinStore.Models.StoredUser>("id == %@", user.id)]) else { fatalError("No stored user for state user?")} [Where<SwiftfinStore.Models.StoredUser>("id == %@", user.id)])
else { fatalError("No stored user for state user?") }
_delete(user: storedUser, transaction: nil) _delete(user: storedUser, transaction: nil)
} }
// MARK: delete server // MARK: delete server
func delete(server: SwiftfinStore.State.Server) { func delete(server: SwiftfinStore.State.Server) {
guard let storedServer = try? SwiftfinStore.dataStack.fetchOne(From<SwiftfinStore.Models.StoredServer>(), guard let storedServer = try? SwiftfinStore.dataStack.fetchOne(From<SwiftfinStore.Models.StoredServer>(),
[Where<SwiftfinStore.Models.StoredServer>("id == %@", server.id)]) else { fatalError("No stored server for state server?")} [Where<SwiftfinStore.Models.StoredServer>("id == %@", server.id)])
else { fatalError("No stored server for state server?") }
_delete(server: storedServer, transaction: nil) _delete(server: storedServer, transaction: nil)
} }

View File

@ -1,18 +1,17 @@
// //
/* // Swiftfin is subject to the terms of the Mozilla Public
* SwiftFin is subject to the terms of the Mozilla Public // License, v2.0. If a copy of the MPL was not distributed with this
* 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/.
* file, you can obtain one at https://mozilla.org/MPL/2.0/. //
* // Copyright (c) 2022 Jellyfin & Jellyfin Contributors
* Copyright 2021 Aiden Vigue & Jellyfin Contributors //
*/
import Foundation import Foundation
enum SwiftfinNotificationCenter { enum SwiftfinNotificationCenter {
static let main: NotificationCenter = { static let main: NotificationCenter = {
return NotificationCenter() NotificationCenter()
}() }()
enum Keys { enum Keys {

View File

@ -1,19 +1,19 @@
// //
/* // Swiftfin is subject to the terms of the Mozilla Public
* SwiftFin is subject to the terms of the Mozilla Public // License, v2.0. If a copy of the MPL was not distributed with this
* 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/.
* file, you can obtain one at https://mozilla.org/MPL/2.0/. //
* // Copyright (c) 2022 Jellyfin & Jellyfin Contributors
* Copyright 2021 Aiden Vigue & Jellyfin Contributors //
*/
import Foundation
import CoreStore import CoreStore
import Defaults import Defaults
import Foundation
enum SwiftfinStore { enum SwiftfinStore {
// MARK: State // MARK: State
// Safe, copyable representations of their underlying CoreStoredObject // Safe, copyable representations of their underlying CoreStoredObject
// Relationships are represented by the related object's IDs or value // Relationships are represented by the related object's IDs or value
enum State { enum State {
@ -27,7 +27,9 @@ enum SwiftfinStore {
let version: String let version: String
let userIDs: [String] let userIDs: [String]
fileprivate init(uris: Set<String>, currentURI: String, name: String, id: String, os: String, version: String, usersIDs: [String]) { fileprivate init(uris: Set<String>, currentURI: String, name: String, id: String, os: String, version: String,
usersIDs: [String])
{
self.uris = uris self.uris = uris
self.currentURI = currentURI self.currentURI = currentURI
self.name = name self.name = name
@ -38,7 +40,7 @@ enum SwiftfinStore {
} }
static var sample: Server { static var sample: Server {
return Server(uris: ["https://www.notaurl.com", "http://www.maybeaurl.org"], Server(uris: ["https://www.notaurl.com", "http://www.maybeaurl.org"],
currentURI: "https://www.notaurl.com", currentURI: "https://www.notaurl.com",
name: "Johnny's Tree", name: "Johnny's Tree",
id: "123abc", id: "123abc",
@ -62,7 +64,7 @@ enum SwiftfinStore {
} }
static var sample: User { static var sample: User {
return User(username: "JohnnyAppleseed", User(username: "JohnnyAppleseed",
id: "123abc", id: "123abc",
serverID: "123abc", serverID: "123abc",
accessToken: "open-sesame") accessToken: "open-sesame")
@ -71,6 +73,7 @@ enum SwiftfinStore {
} }
// MARK: Models // MARK: Models
enum Models { enum Models {
final class StoredServer: CoreStoreObject { final class StoredServer: CoreStoreObject {
@ -97,13 +100,13 @@ enum SwiftfinStore {
var users: Set<StoredUser> var users: Set<StoredUser>
var state: State.Server { var state: State.Server {
return State.Server(uris: uris, State.Server(uris: uris,
currentURI: currentURI, currentURI: currentURI,
name: name, name: name,
id: id, id: id,
os: os, os: os,
version: version, version: version,
usersIDs: users.map({ $0.id })) usersIDs: users.map(\.id))
} }
} }
@ -145,37 +148,51 @@ enum SwiftfinStore {
} }
// MARK: Errors // MARK: Errors
enum Errors { enum Errors {
case existingServer(State.Server) case existingServer(State.Server)
case existingUser(State.User) case existingUser(State.User)
} }
// MARK: dataStack // MARK: dataStack
static let dataStack: DataStack = { static let dataStack: DataStack = {
let schema = CoreStoreSchema(modelVersion: "V1", let schema = CoreStoreSchema(modelVersion: "V1",
entities: [ entities: [
Entity<SwiftfinStore.Models.StoredServer>("Server"), Entity<SwiftfinStore.Models.StoredServer>("Server"),
Entity<SwiftfinStore.Models.StoredUser>("User"), Entity<SwiftfinStore.Models.StoredUser>("User"),
Entity<SwiftfinStore.Models.StoredAccessToken>("AccessToken") Entity<SwiftfinStore.Models.StoredAccessToken>("AccessToken"),
], ],
versionLock: [ versionLock: [
"AccessToken": [0xa8c475e874494bb1, 0x79486e93449f0b3d, 0xa7dc4a0003541edb, 0x94183fae7580ef72], "AccessToken": [
"Server": [0x936b46acd8e8f0e3, 0x59890d4d9f3f885f, 0x819cf7a4abf98b22, 0xe16125c5af885a06], 0xA8C4_75E8_7449_4BB1,
"User": [0x845de08a74bc53ed, 0xe95a406a29f3a5d0, 0x9eda732821a15ea9, 0xb5afa531e41ce8a] 0x7948_6E93_449F_0B3D,
0xA7DC_4A00_0354_1EDB,
0x9418_3FAE_7580_EF72,
],
"Server": [
0x936B_46AC_D8E8_F0E3,
0x5989_0D4D_9F3F_885F,
0x819C_F7A4_ABF9_8B22,
0xE161_25C5_AF88_5A06,
],
"User": [
0x845D_E08A_74BC_53ED,
0xE95A_406A_29F3_A5D0,
0x9EDA_7328_21A1_5EA9,
0xB5A_FA53_1E41_CE8A,
],
]) ])
let _dataStack = DataStack(schema) let _dataStack = DataStack(schema)
try! _dataStack.addStorageAndWait( try! _dataStack.addStorageAndWait(SQLiteStore(fileName: "Swiftfin.sqlite",
SQLiteStore( localStorageOptions: .recreateStoreOnModelMismatch))
fileName: "Swiftfin.sqlite",
localStorageOptions: .recreateStoreOnModelMismatch
)
)
return _dataStack return _dataStack
}() }()
} }
// MARK: LocalizedError // MARK: LocalizedError
extension SwiftfinStore.Errors: LocalizedError { extension SwiftfinStore.Errors: LocalizedError {
var title: String { var title: String {
@ -189,9 +206,9 @@ extension SwiftfinStore.Errors: LocalizedError {
var errorDescription: String? { var errorDescription: String? {
switch self { switch self {
case .existingServer(let server): case let .existingServer(server):
return "Server \(server.name) already exists with same server ID" return "Server \(server.name) already exists with same server ID"
case .existingUser(let user): case let .existingUser(user):
return "User \(user.username) already exists with same user ID" return "User \(user.username) already exists with same user ID"
} }
} }

View File

@ -1,11 +1,10 @@
// //
/* // Swiftfin is subject to the terms of the Mozilla Public
* SwiftFin is subject to the terms of the Mozilla Public // License, v2.0. If a copy of the MPL was not distributed with this
* 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/.
* file, you can obtain one at https://mozilla.org/MPL/2.0/. //
* // Copyright (c) 2022 Jellyfin & Jellyfin Contributors
* Copyright 2021 Aiden Vigue & Jellyfin Contributors //
*/
import Defaults import Defaults
import Foundation import Foundation
@ -15,11 +14,11 @@ extension SwiftfinStore {
enum Defaults { enum Defaults {
static let generalSuite: UserDefaults = { static let generalSuite: UserDefaults = {
return UserDefaults(suiteName: "swiftfinstore-general-defaults")! UserDefaults(suiteName: "swiftfinstore-general-defaults")!
}() }()
static let universalSuite: UserDefaults = { static let universalSuite: UserDefaults = {
return UserDefaults(suiteName: "swiftfinstore-universal-defaults")! UserDefaults(suiteName: "swiftfinstore-universal-defaults")!
}() }()
} }
} }
@ -35,7 +34,8 @@ extension Defaults.Keys {
static let inNetworkBandwidth = Key<Int>("InNetworkBandwidth", default: 40_000_000, suite: SwiftfinStore.Defaults.generalSuite) static let inNetworkBandwidth = Key<Int>("InNetworkBandwidth", default: 40_000_000, suite: SwiftfinStore.Defaults.generalSuite)
static let outOfNetworkBandwidth = Key<Int>("OutOfNetworkBandwidth", default: 40_000_000, suite: SwiftfinStore.Defaults.generalSuite) static let outOfNetworkBandwidth = Key<Int>("OutOfNetworkBandwidth", default: 40_000_000, suite: SwiftfinStore.Defaults.generalSuite)
static let isAutoSelectSubtitles = Key<Bool>("isAutoSelectSubtitles", default: false, suite: SwiftfinStore.Defaults.generalSuite) static let isAutoSelectSubtitles = Key<Bool>("isAutoSelectSubtitles", default: false, suite: SwiftfinStore.Defaults.generalSuite)
static let autoSelectSubtitlesLangCode = Key<String>("AutoSelectSubtitlesLangCode", default: "Auto", suite: SwiftfinStore.Defaults.generalSuite) static let autoSelectSubtitlesLangCode = Key<String>("AutoSelectSubtitlesLangCode", default: "Auto",
suite: SwiftfinStore.Defaults.generalSuite)
static let autoSelectAudioLangCode = Key<String>("AutoSelectAudioLangCode", default: "Auto", suite: SwiftfinStore.Defaults.generalSuite) static let autoSelectAudioLangCode = Key<String>("AutoSelectAudioLangCode", default: "Auto", suite: SwiftfinStore.Defaults.generalSuite)
// Customize settings // Customize settings
@ -45,8 +45,10 @@ extension Defaults.Keys {
// Video player / overlay settings // Video player / overlay settings
static let overlayType = Key<OverlayType>("overlayType", default: .normal, suite: SwiftfinStore.Defaults.generalSuite) static let overlayType = Key<OverlayType>("overlayType", default: .normal, suite: SwiftfinStore.Defaults.generalSuite)
static let jumpGesturesEnabled = Key<Bool>("gesturesEnabled", default: true, suite: SwiftfinStore.Defaults.generalSuite) static let jumpGesturesEnabled = Key<Bool>("gesturesEnabled", default: true, suite: SwiftfinStore.Defaults.generalSuite)
static let videoPlayerJumpForward = Key<VideoPlayerJumpLength>("videoPlayerJumpForward", default: .fifteen, suite: SwiftfinStore.Defaults.generalSuite) static let videoPlayerJumpForward = Key<VideoPlayerJumpLength>("videoPlayerJumpForward", default: .fifteen,
static let videoPlayerJumpBackward = Key<VideoPlayerJumpLength>("videoPlayerJumpBackward", default: .fifteen, suite: SwiftfinStore.Defaults.generalSuite) suite: SwiftfinStore.Defaults.generalSuite)
static let videoPlayerJumpBackward = Key<VideoPlayerJumpLength>("videoPlayerJumpBackward", default: .fifteen,
suite: SwiftfinStore.Defaults.generalSuite)
static let autoplayEnabled = Key<Bool>("autoPlayNextItem", default: true, suite: SwiftfinStore.Defaults.generalSuite) static let autoplayEnabled = Key<Bool>("autoPlayNextItem", default: true, suite: SwiftfinStore.Defaults.generalSuite)
static let resumeOffset = Key<Bool>("resumeOffset", default: false, suite: SwiftfinStore.Defaults.generalSuite) static let resumeOffset = Key<Bool>("resumeOffset", default: false, suite: SwiftfinStore.Defaults.generalSuite)
@ -56,11 +58,13 @@ extension Defaults.Keys {
static let shouldShowAutoPlay = Key<Bool>("shouldShowAutoPlayNextItem", default: true, suite: SwiftfinStore.Defaults.generalSuite) static let shouldShowAutoPlay = Key<Bool>("shouldShowAutoPlayNextItem", default: true, suite: SwiftfinStore.Defaults.generalSuite)
// Should show video player items in overlay menu // Should show video player items in overlay menu
static let shouldShowJumpButtonsInOverlayMenu = Key<Bool>("shouldShowJumpButtonsInMenu", default: true, suite: SwiftfinStore.Defaults.generalSuite) static let shouldShowJumpButtonsInOverlayMenu = Key<Bool>("shouldShowJumpButtonsInMenu", default: true,
suite: SwiftfinStore.Defaults.generalSuite)
// Experimental settings // Experimental settings
struct Experimental { enum Experimental {
static let syncSubtitleStateWithAdjacent = Key<Bool>("experimental.syncSubtitleState", default: false, suite: SwiftfinStore.Defaults.generalSuite) static let syncSubtitleStateWithAdjacent = Key<Bool>("experimental.syncSubtitleState", default: false,
suite: SwiftfinStore.Defaults.generalSuite)
static let liveTVAlphaEnabled = Key<Bool>("liveTVAlphaEnabled", default: false, suite: SwiftfinStore.Defaults.generalSuite) static let liveTVAlphaEnabled = Key<Bool>("liveTVAlphaEnabled", default: false, suite: SwiftfinStore.Defaults.generalSuite)
} }

View File

@ -1,11 +1,10 @@
// //
/* // Swiftfin is subject to the terms of the Mozilla Public
* SwiftFin is subject to the terms of the Mozilla Public // License, v2.0. If a copy of the MPL was not distributed with this
* 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/.
* file, you can obtain one at https://mozilla.org/MPL/2.0/. //
* // Copyright (c) 2022 Jellyfin & Jellyfin Contributors
* Copyright 2021 Aiden Vigue & Jellyfin Contributors //
*/
import SwiftUI import SwiftUI

View File

@ -1,11 +1,10 @@
// //
/* // Swiftfin is subject to the terms of the Mozilla Public
* SwiftFin is subject to the terms of the Mozilla Public // License, v2.0. If a copy of the MPL was not distributed with this
* 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/.
* file, you can obtain one at https://mozilla.org/MPL/2.0/. //
* // Copyright (c) 2022 Jellyfin & Jellyfin Contributors
* Copyright 2021 Aiden Vigue & Jellyfin Contributors //
*/
import Combine import Combine
import Foundation import Foundation
@ -18,16 +17,20 @@ struct AddServerURIPayload: Identifiable {
let uri: String let uri: String
var id: String { var id: String {
return server.id.appending(uri) server.id.appending(uri)
} }
} }
final class ConnectToServerViewModel: ViewModel { final class ConnectToServerViewModel: ViewModel {
@RouterObject var router: ConnectToServerCoodinator.Router? @RouterObject
@Published var discoveredServers: Set<ServerDiscovery.ServerLookupResponse> = [] var router: ConnectToServerCoodinator.Router?
@Published var searching = false @Published
@Published var addServerURIPayload: AddServerURIPayload? var discoveredServers: Set<ServerDiscovery.ServerLookupResponse> = []
@Published
var searching = false
@Published
var addServerURIPayload: AddServerURIPayload?
var backAddServerURIPayload: AddServerURIPayload? var backAddServerURIPayload: AddServerURIPayload?
private let discovery = ServerDiscovery() private let discovery = ServerDiscovery()
@ -58,20 +61,22 @@ final class ConnectToServerViewModel: ViewModel {
// This is disgusting. ViewModel Error handling overall needs to be refactored // This is disgusting. ViewModel Error handling overall needs to be refactored
switch completion { switch completion {
case .finished: () case .finished: ()
case .failure(let error): case let .failure(error):
switch error { switch error {
case is SwiftfinStore.Errors: case is SwiftfinStore.Errors:
let swiftfinError = error as! SwiftfinStore.Errors let swiftfinError = error as! SwiftfinStore.Errors
switch swiftfinError { switch swiftfinError {
case .existingServer(let server): case let .existingServer(server):
self.addServerURIPayload = AddServerURIPayload(server: server, uri: uri) self.addServerURIPayload = AddServerURIPayload(server: server, uri: uri)
self.backAddServerURIPayload = AddServerURIPayload(server: server, uri: uri) self.backAddServerURIPayload = AddServerURIPayload(server: server, uri: uri)
default: default:
self.handleAPIRequestError(displayMessage: "Unable to connect to server.", logLevel: .critical, tag: "connectToServer", self.handleAPIRequestError(displayMessage: "Unable to connect to server.", logLevel: .critical,
tag: "connectToServer",
completion: completion) completion: completion)
} }
default: default:
self.handleAPIRequestError(displayMessage: "Unable to connect to server.", logLevel: .critical, tag: "connectToServer", self.handleAPIRequestError(displayMessage: "Unable to connect to server.", logLevel: .critical,
tag: "connectToServer",
completion: completion) completion: completion)
} }
} }
@ -106,7 +111,8 @@ final class ConnectToServerViewModel: ViewModel {
} receiveValue: { server in } receiveValue: { server in
SessionManager.main.setServerCurrentURI(server: server, uri: addServerURIPayload.uri) SessionManager.main.setServerCurrentURI(server: server, uri: addServerURIPayload.uri)
.sink { completion in .sink { completion in
self.handleAPIRequestError(displayMessage: "Unable to connect to server.", logLevel: .critical, tag: "connectToServer", self.handleAPIRequestError(displayMessage: "Unable to connect to server.", logLevel: .critical,
tag: "connectToServer",
completion: completion) completion: completion)
} receiveValue: { _ in } receiveValue: { _ in
self.router?.dismissCoordinator() self.router?.dismissCoordinator()

View File

@ -1,11 +1,10 @@
// //
/* // Swiftfin is subject to the terms of the Mozilla Public
* SwiftFin is subject to the terms of the Mozilla Public // License, v2.0. If a copy of the MPL was not distributed with this
* 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/.
* file, you can obtain one at https://mozilla.org/MPL/2.0/. //
* // Copyright (c) 2022 Jellyfin & Jellyfin Contributors
* Copyright 2021 Aiden Vigue & Jellyfin Contributors //
*/
import JellyfinAPI import JellyfinAPI
import SwiftUI import SwiftUI
@ -14,9 +13,12 @@ final class EpisodesRowViewModel: ViewModel {
// TODO: Protocol these viewmodels for generalization instead of Episode // TODO: Protocol these viewmodels for generalization instead of Episode
@ObservedObject var episodeItemViewModel: EpisodeItemViewModel @ObservedObject
@Published var seasonsEpisodes: [BaseItemDto: [BaseItemDto]] = [:] var episodeItemViewModel: EpisodeItemViewModel
@Published var selectedSeason: BaseItemDto? { @Published
var seasonsEpisodes: [BaseItemDto: [BaseItemDto]] = [:]
@Published
var selectedSeason: BaseItemDto? {
willSet { willSet {
if seasonsEpisodes[newValue!]!.isEmpty { if seasonsEpisodes[newValue!]!.isEmpty {
retrieveEpisodesForSeason(newValue!) retrieveEpisodesForSeason(newValue!)
@ -70,8 +72,10 @@ final class SingleSeasonEpisodesRowViewModel: ViewModel {
// TODO: Protocol these viewmodels for generalization instead of Season // TODO: Protocol these viewmodels for generalization instead of Season
@ObservedObject var seasonItemViewModel: SeasonItemViewModel @ObservedObject
@Published var episodes: [BaseItemDto] var seasonItemViewModel: SeasonItemViewModel
@Published
var episodes: [BaseItemDto]
init(seasonItemViewModel: SeasonItemViewModel) { init(seasonItemViewModel: SeasonItemViewModel) {
self.seasonItemViewModel = seasonItemViewModel self.seasonItemViewModel = seasonItemViewModel

View File

@ -1,11 +1,10 @@
// //
/* // Swiftfin is subject to the terms of the Mozilla Public
* SwiftFin is subject to the terms of the Mozilla Public // License, v2.0. If a copy of the MPL was not distributed with this
* 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/.
* file, you can obtain one at https://mozilla.org/MPL/2.0/. //
* // Copyright (c) 2022 Jellyfin & Jellyfin Contributors
* Copyright 2021 Aiden Vigue & Jellyfin Contributors //
*/
import ActivityIndicator import ActivityIndicator
import Combine import Combine
@ -14,14 +13,19 @@ import JellyfinAPI
final class HomeViewModel: ViewModel { final class HomeViewModel: ViewModel {
@Published var latestAddedItems: [BaseItemDto] = [] @Published
@Published var resumeItems: [BaseItemDto] = [] var latestAddedItems: [BaseItemDto] = []
@Published var nextUpItems: [BaseItemDto] = [] @Published
@Published var librariesShowRecentlyAddedIDs: [String] = [] var resumeItems: [BaseItemDto] = []
@Published var libraries: [BaseItemDto] = [] @Published
var nextUpItems: [BaseItemDto] = []
@Published
var librariesShowRecentlyAddedIDs: [String] = []
@Published
var libraries: [BaseItemDto] = []
// temp // temp
var recentFilterSet: LibraryFilters = LibraryFilters(filters: [], sortOrder: [.descending], sortBy: [.dateAdded]) var recentFilterSet = LibraryFilters(filters: [], sortOrder: [.descending], sortBy: [.dateAdded])
override init() { override init() {
super.init() super.init()
@ -35,7 +39,8 @@ final class HomeViewModel: ViewModel {
nc.addObserver(self, selector: #selector(didSignOut), name: SwiftfinNotificationCenter.Keys.didSignOut, object: nil) nc.addObserver(self, selector: #selector(didSignOut), name: SwiftfinNotificationCenter.Keys.didSignOut, object: nil)
} }
@objc private func didSignIn() { @objc
private func didSignIn() {
for cancellable in cancellables { for cancellable in cancellables {
cancellable.cancel() cancellable.cancel()
} }
@ -48,7 +53,8 @@ final class HomeViewModel: ViewModel {
refresh() refresh()
} }
@objc private func didSignOut() { @objc
private func didSignOut() {
for cancellable in cancellables { for cancellable in cancellables {
cancellable.cancel() cancellable.cancel()
} }
@ -56,7 +62,8 @@ final class HomeViewModel: ViewModel {
cancellables.removeAll() cancellables.removeAll()
} }
@objc func refresh() { @objc
func refresh() {
LogManager.shared.log.debug("Refresh called.") LogManager.shared.log.debug("Refresh called.")
refreshLibrariesLatest() refreshLibrariesLatest()
@ -66,6 +73,7 @@ final class HomeViewModel: ViewModel {
} }
// MARK: Libraries Latest Items // MARK: Libraries Latest Items
private func refreshLibrariesLatest() { private func refreshLibrariesLatest() {
UserViewsAPI.getUserViews(userId: SessionManager.main.currentLogin.user.id) UserViewsAPI.getUserViews(userId: SessionManager.main.currentLogin.user.id)
.trackActivity(loading) .trackActivity(loading)
@ -82,7 +90,8 @@ final class HomeViewModel: ViewModel {
var newLibraries: [BaseItemDto] = [] var newLibraries: [BaseItemDto] = []
response.items!.forEach { item in response.items!.forEach { item in
LogManager.shared.log.debug("Retrieved user view: \(item.id!) (\(item.name ?? "nil")) with type \(item.collectionType ?? "nil")") LogManager.shared.log
.debug("Retrieved user view: \(item.id!) (\(item.name ?? "nil")) with type \(item.collectionType ?? "nil")")
if item.collectionType == "movies" || item.collectionType == "tvshows" { if item.collectionType == "movies" || item.collectionType == "tvshows" {
newLibraries.append(item) newLibraries.append(item)
} }
@ -98,11 +107,12 @@ final class HomeViewModel: ViewModel {
self.handleAPIRequestError(completion: completion) self.handleAPIRequestError(completion: completion)
} }
}, receiveValue: { response in }, receiveValue: { response in
let excludeIDs = response.configuration?.latestItemsExcludes != nil ? response.configuration!.latestItemsExcludes! : [] let excludeIDs = response.configuration?.latestItemsExcludes != nil ? response.configuration!
.latestItemsExcludes! : []
for excludeID in excludeIDs { for excludeID in excludeIDs {
newLibraries.removeAll { library in newLibraries.removeAll { library in
return library.id == excludeID library.id == excludeID
} }
} }
@ -114,9 +124,18 @@ final class HomeViewModel: ViewModel {
} }
// MARK: Latest Added Items // MARK: Latest Added Items
private func refreshLatestAddedItems() { private func refreshLatestAddedItems() {
UserLibraryAPI.getLatestMedia(userId: SessionManager.main.currentLogin.user.id, UserLibraryAPI.getLatestMedia(userId: SessionManager.main.currentLogin.user.id,
fields: [.primaryImageAspectRatio, .seriesPrimaryImage, .seasonUserData, .overview, .genres, .people, .chapters], fields: [
.primaryImageAspectRatio,
.seriesPrimaryImage,
.seasonUserData,
.overview,
.genres,
.people,
.chapters,
],
enableImageTypes: [.primary, .backdrop, .thumb], enableImageTypes: [.primary, .backdrop, .thumb],
enableUserData: true, enableUserData: true,
limit: 8) limit: 8)
@ -136,10 +155,19 @@ final class HomeViewModel: ViewModel {
} }
// MARK: Resume Items // MARK: Resume Items
private func refreshResumeItems() { private func refreshResumeItems() {
ItemsAPI.getResumeItems(userId: SessionManager.main.currentLogin.user.id, ItemsAPI.getResumeItems(userId: SessionManager.main.currentLogin.user.id,
limit: 6, limit: 6,
fields: [.primaryImageAspectRatio, .seriesPrimaryImage, .seasonUserData, .overview, .genres, .people, .chapters], fields: [
.primaryImageAspectRatio,
.seriesPrimaryImage,
.seasonUserData,
.overview,
.genres,
.people,
.chapters,
],
enableUserData: true) enableUserData: true)
.trackActivity(loading) .trackActivity(loading)
.sink(receiveCompletion: { completion in .sink(receiveCompletion: { completion in
@ -158,10 +186,19 @@ final class HomeViewModel: ViewModel {
} }
// MARK: Next Up Items // MARK: Next Up Items
private func refreshNextUpItems() { private func refreshNextUpItems() {
TvShowsAPI.getNextUp(userId: SessionManager.main.currentLogin.user.id, TvShowsAPI.getNextUp(userId: SessionManager.main.currentLogin.user.id,
limit: 6, limit: 6,
fields: [.primaryImageAspectRatio, .seriesPrimaryImage, .seasonUserData, .overview, .genres, .people, .chapters], fields: [
.primaryImageAspectRatio,
.seriesPrimaryImage,
.seasonUserData,
.overview,
.genres,
.people,
.chapters,
],
enableUserData: true) enableUserData: true)
.trackActivity(loading) .trackActivity(loading)
.sink(receiveCompletion: { completion in .sink(receiveCompletion: { completion in

View File

@ -1,11 +1,10 @@
// //
/* // Swiftfin is subject to the terms of the Mozilla Public
* SwiftFin is subject to the terms of the Mozilla Public // License, v2.0. If a copy of the MPL was not distributed with this
* 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/.
* file, you can obtain one at https://mozilla.org/MPL/2.0/. //
* // Copyright (c) 2022 Jellyfin & Jellyfin Contributors
* Copyright 2021 Aiden Vigue & Jellyfin Contributors //
*/
import Combine import Combine
import Foundation import Foundation
@ -13,7 +12,8 @@ import JellyfinAPI
final class CollectionItemViewModel: ItemViewModel { final class CollectionItemViewModel: ItemViewModel {
@Published var collectionItems: [BaseItemDto] = [] @Published
var collectionItems: [BaseItemDto] = []
override init(item: BaseItemDto) { override init(item: BaseItemDto) {
super.init(item: item) super.init(item: item)

View File

@ -1,11 +1,10 @@
// //
/* // Swiftfin is subject to the terms of the Mozilla Public
* SwiftFin is subject to the terms of the Mozilla Public // License, v2.0. If a copy of the MPL was not distributed with this
* 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/.
* file, you can obtain one at https://mozilla.org/MPL/2.0/. //
* // Copyright (c) 2022 Jellyfin & Jellyfin Contributors
* Copyright 2021 Aiden Vigue & Jellyfin Contributors //
*/
import Combine import Combine
import Foundation import Foundation
@ -14,8 +13,10 @@ import Stinsen
final class EpisodeItemViewModel: ItemViewModel { final class EpisodeItemViewModel: ItemViewModel {
@RouterObject var itemRouter: ItemCoordinator.Router? @RouterObject
@Published var series: BaseItemDto? var itemRouter: ItemCoordinator.Router?
@Published
var series: BaseItemDto?
override init(item: BaseItemDto) { override init(item: BaseItemDto) {
super.init(item: item) super.init(item: item)
@ -29,7 +30,7 @@ final class EpisodeItemViewModel: ItemViewModel {
} }
override func shouldDisplayRuntime() -> Bool { override func shouldDisplayRuntime() -> Bool {
return false false
} }
func getEpisodeSeries() { func getEpisodeSeries() {

View File

@ -1,11 +1,10 @@
// //
/* // Swiftfin is subject to the terms of the Mozilla Public
* SwiftFin is subject to the terms of the Mozilla Public // License, v2.0. If a copy of the MPL was not distributed with this
* 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/.
* file, you can obtain one at https://mozilla.org/MPL/2.0/. //
* // Copyright (c) 2022 Jellyfin & Jellyfin Contributors
* Copyright 2021 Aiden Vigue & Jellyfin Contributors //
*/
import Combine import Combine
import Foundation import Foundation
@ -14,19 +13,27 @@ import UIKit
class ItemViewModel: ViewModel { class ItemViewModel: ViewModel {
@Published var item: BaseItemDto @Published
@Published var playButtonItem: BaseItemDto? { var item: BaseItemDto
@Published
var playButtonItem: BaseItemDto? {
didSet { didSet {
if let playButtonItem = playButtonItem { if let playButtonItem = playButtonItem {
refreshItemVideoPlayerViewModel(for: playButtonItem) refreshItemVideoPlayerViewModel(for: playButtonItem)
} }
} }
} }
@Published var similarItems: [BaseItemDto] = []
@Published var isWatched = false @Published
@Published var isFavorited = false var similarItems: [BaseItemDto] = []
@Published var informationItems: [BaseItemDto.ItemDetail] @Published
@Published var mediaItems: [BaseItemDto.ItemDetail] var isWatched = false
@Published
var isFavorited = false
@Published
var informationItems: [BaseItemDto.ItemDetail]
@Published
var mediaItems: [BaseItemDto.ItemDetail]
var itemVideoPlayerViewModel: VideoPlayerViewModel? var itemVideoPlayerViewModel: VideoPlayerViewModel?
init(item: BaseItemDto) { init(item: BaseItemDto) {
@ -70,15 +77,16 @@ class ItemViewModel: ViewModel {
} }
func getItemDisplayName() -> String { func getItemDisplayName() -> String {
return item.name ?? "" item.name ?? ""
} }
func shouldDisplayRuntime() -> Bool { func shouldDisplayRuntime() -> Bool {
return true true
} }
func getSimilarItems() { func getSimilarItems() {
LibraryAPI.getSimilarItems(itemId: item.id!, userId: SessionManager.main.currentLogin.user.id, limit: 20, fields: [.primaryImageAspectRatio, .seriesPrimaryImage, .seasonUserData, .overview, .genres, .people]) LibraryAPI.getSimilarItems(itemId: item.id!, userId: SessionManager.main.currentLogin.user.id, limit: 20,
fields: [.primaryImageAspectRatio, .seriesPrimaryImage, .seasonUserData, .overview, .genres, .people])
.trackActivity(loading) .trackActivity(loading)
.sink(receiveCompletion: { [weak self] completion in .sink(receiveCompletion: { [weak self] completion in
self?.handleAPIRequestError(completion: completion) self?.handleAPIRequestError(completion: completion)

View File

@ -1,15 +1,13 @@
// //
/* // Swiftfin is subject to the terms of the Mozilla Public
* SwiftFin is subject to the terms of the Mozilla Public // License, v2.0. If a copy of the MPL was not distributed with this
* 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/.
* file, you can obtain one at https://mozilla.org/MPL/2.0/. //
* // Copyright (c) 2022 Jellyfin & Jellyfin Contributors
* Copyright 2021 Aiden Vigue & Jellyfin Contributors //
*/
import Combine import Combine
import Foundation import Foundation
import JellyfinAPI import JellyfinAPI
final class MovieItemViewModel: ItemViewModel { final class MovieItemViewModel: ItemViewModel {}
}

View File

@ -1,11 +1,10 @@
// //
/* // Swiftfin is subject to the terms of the Mozilla Public
* SwiftFin is subject to the terms of the Mozilla Public // License, v2.0. If a copy of the MPL was not distributed with this
* 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/.
* file, you can obtain one at https://mozilla.org/MPL/2.0/. //
* // Copyright (c) 2022 Jellyfin & Jellyfin Contributors
* Copyright 2021 Aiden Vigue & Jellyfin Contributors //
*/
import Combine import Combine
import Foundation import Foundation
@ -14,9 +13,12 @@ import Stinsen
final class SeasonItemViewModel: ItemViewModel { final class SeasonItemViewModel: ItemViewModel {
@RouterObject var itemRouter: ItemCoordinator.Router? @RouterObject
@Published var episodes: [BaseItemDto] = [] var itemRouter: ItemCoordinator.Router?
@Published var seriesItem: BaseItemDto? @Published
var episodes: [BaseItemDto] = []
@Published
var seriesItem: BaseItemDto?
override init(item: BaseItemDto) { override init(item: BaseItemDto) {
super.init(item: item) super.init(item: item)

View File

@ -1,11 +1,10 @@
// //
/* // Swiftfin is subject to the terms of the Mozilla Public
* SwiftFin is subject to the terms of the Mozilla Public // License, v2.0. If a copy of the MPL was not distributed with this
* 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/.
* file, you can obtain one at https://mozilla.org/MPL/2.0/. //
* // Copyright (c) 2022 Jellyfin & Jellyfin Contributors
* Copyright 2021 Aiden Vigue & Jellyfin Contributors //
*/
import Combine import Combine
import Foundation import Foundation
@ -13,7 +12,8 @@ import JellyfinAPI
final class SeriesItemViewModel: ItemViewModel { final class SeriesItemViewModel: ItemViewModel {
@Published var seasons: [BaseItemDto] = [] @Published
var seasons: [BaseItemDto] = []
override init(item: BaseItemDto) { override init(item: BaseItemDto) {
super.init(item: item) super.init(item: item)
@ -29,13 +29,15 @@ final class SeriesItemViewModel: ItemViewModel {
} }
override func shouldDisplayRuntime() -> Bool { override func shouldDisplayRuntime() -> Bool {
return false false
} }
private func getNextUp() { private func getNextUp() {
LogManager.shared.log.debug("Getting next up for show \(self.item.id!) (\(self.item.name!))") LogManager.shared.log.debug("Getting next up for show \(self.item.id!) (\(self.item.name!))")
TvShowsAPI.getNextUp(userId: SessionManager.main.currentLogin.user.id, fields: [.primaryImageAspectRatio, .seriesPrimaryImage, .seasonUserData, .overview, .genres, .people], seriesId: self.item.id!, enableUserData: true) TvShowsAPI.getNextUp(userId: SessionManager.main.currentLogin.user.id,
fields: [.primaryImageAspectRatio, .seriesPrimaryImage, .seasonUserData, .overview, .genres, .people],
seriesId: self.item.id!, enableUserData: true)
.trackActivity(loading) .trackActivity(loading)
.sink(receiveCompletion: { [weak self] completion in .sink(receiveCompletion: { [weak self] completion in
self?.handleAPIRequestError(completion: completion) self?.handleAPIRequestError(completion: completion)
@ -67,7 +69,9 @@ final class SeriesItemViewModel: ItemViewModel {
private func requestSeasons() { private func requestSeasons() {
LogManager.shared.log.debug("Getting seasons of show \(self.item.id!) (\(self.item.name!))") LogManager.shared.log.debug("Getting seasons of show \(self.item.id!) (\(self.item.name!))")
TvShowsAPI.getSeasons(seriesId: item.id ?? "", userId: SessionManager.main.currentLogin.user.id, fields: [.primaryImageAspectRatio, .seriesPrimaryImage, .seasonUserData, .overview, .genres, .people], enableUserData: true) TvShowsAPI.getSeasons(seriesId: item.id ?? "", userId: SessionManager.main.currentLogin.user.id,
fields: [.primaryImageAspectRatio, .seriesPrimaryImage, .seasonUserData, .overview, .genres, .people],
enableUserData: true)
.trackActivity(loading) .trackActivity(loading)
.sink(receiveCompletion: { [weak self] completion in .sink(receiveCompletion: { [weak self] completion in
self?.handleAPIRequestError(completion: completion) self?.handleAPIRequestError(completion: completion)

View File

@ -1,11 +1,10 @@
// //
/* // Swiftfin is subject to the terms of the Mozilla Public
* SwiftFin is subject to the terms of the Mozilla Public // License, v2.0. If a copy of the MPL was not distributed with this
* 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/.
* file, you can obtain one at https://mozilla.org/MPL/2.0/. //
* // Copyright (c) 2022 Jellyfin & Jellyfin Contributors
* Copyright 2021 Aiden Vigue & Jellyfin Contributors //
*/
import Combine import Combine
import Foundation import Foundation
@ -13,7 +12,8 @@ import JellyfinAPI
final class LatestMediaViewModel: ViewModel { final class LatestMediaViewModel: ViewModel {
@Published var items = [BaseItemDto]() @Published
var items = [BaseItemDto]()
let library: BaseItemDto let library: BaseItemDto
@ -34,7 +34,7 @@ final class LatestMediaViewModel: ViewModel {
.seasonUserData, .seasonUserData,
.overview, .overview,
.genres, .genres,
.people .people,
], ],
includeItemTypes: ["Series", "Movie"], includeItemTypes: ["Series", "Movie"],
enableUserData: true, limit: 12) enableUserData: true, limit: 12)

View File

@ -1,11 +1,10 @@
// //
/* // Swiftfin is subject to the terms of the Mozilla Public
* SwiftFin is subject to the terms of the Mozilla Public // License, v2.0. If a copy of the MPL was not distributed with this
* 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/.
* file, you can obtain one at https://mozilla.org/MPL/2.0/. //
* // Copyright (c) 2022 Jellyfin & Jellyfin Contributors
* Copyright 2021 Aiden Vigue & Jellyfin Contributors //
*/
import Combine import Combine
import Foundation import Foundation
@ -21,16 +20,25 @@ enum FilterType {
final class LibraryFilterViewModel: ViewModel { final class LibraryFilterViewModel: ViewModel {
@Published var modifiedFilters = LibraryFilters() @Published
var modifiedFilters = LibraryFilters()
@Published var possibleGenres = [NameGuidPair]() @Published
@Published var possibleTags = [String]() var possibleGenres = [NameGuidPair]()
@Published var possibleSortOrders = APISortOrder.allCases @Published
@Published var possibleSortBys = SortBy.allCases var possibleTags = [String]()
@Published var possibleItemFilters = ItemFilter.supportedTypes @Published
@Published var enabledFilterType: [FilterType] var possibleSortOrders = APISortOrder.allCases
@Published var selectedSortOrder: APISortOrder = .descending @Published
@Published var selectedSortBy: SortBy = .name var possibleSortBys = SortBy.allCases
@Published
var possibleItemFilters = ItemFilter.supportedTypes
@Published
var enabledFilterType: [FilterType]
@Published
var selectedSortOrder: APISortOrder = .descending
@Published
var selectedSortBy: SortBy = .name
var parentId: String = "" var parentId: String = ""
@ -44,7 +52,8 @@ final class LibraryFilterViewModel: ViewModel {
} }
init(filters: LibraryFilters? = nil, init(filters: LibraryFilters? = nil,
enabledFilterType: [FilterType] = [.tag, .genre, .sortBy, .sortOrder, .filter], parentId: String) { enabledFilterType: [FilterType] = [.tag, .genre, .sortBy, .sortOrder, .filter], parentId: String)
{
self.enabledFilterType = enabledFilterType self.enabledFilterType = enabledFilterType
self.selectedSortBy = filters?.sortBy.first ?? .name self.selectedSortBy = filters?.sortBy.first ?? .name
self.selectedSortOrder = filters?.sortOrder.first ?? .descending self.selectedSortOrder = filters?.sortOrder.first ?? .descending

View File

@ -1,18 +1,18 @@
// //
/* // Swiftfin is subject to the terms of the Mozilla Public
* SwiftFin is subject to the terms of the Mozilla Public // License, v2.0. If a copy of the MPL was not distributed with this
* 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/.
* file, you can obtain one at https://mozilla.org/MPL/2.0/. //
* // Copyright (c) 2022 Jellyfin & Jellyfin Contributors
* Copyright 2021 Aiden Vigue & Jellyfin Contributors //
*/
import Foundation import Foundation
import JellyfinAPI import JellyfinAPI
final class LibraryListViewModel: ViewModel { final class LibraryListViewModel: ViewModel {
@Published var libraries: [BaseItemDto] = [] @Published
var libraries: [BaseItemDto] = []
// temp // temp
var withFavorites = LibraryFilters(filters: [.isFavorite], sortOrder: [], withGenres: [], sortBy: []) var withFavorites = LibraryFilters(filters: [.isFavorite], sortOrder: [], withGenres: [], sortBy: [])

View File

@ -1,11 +1,10 @@
// //
/* // Swiftfin is subject to the terms of the Mozilla Public
* SwiftFin is subject to the terms of the Mozilla Public // License, v2.0. If a copy of the MPL was not distributed with this
* 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/.
* file, you can obtain one at https://mozilla.org/MPL/2.0/. //
* // Copyright (c) 2022 Jellyfin & Jellyfin Contributors
* Copyright 2021 Aiden Vigue & Jellyfin Contributors //
*/
import Combine import Combine
import CombineExt import CombineExt
@ -15,15 +14,21 @@ import SwiftUI
final class LibrarySearchViewModel: ViewModel { final class LibrarySearchViewModel: ViewModel {
@Published var supportedItemTypeList = [ItemType]() @Published
var supportedItemTypeList = [ItemType]()
@Published var selectedItemType: ItemType = .movie @Published
var selectedItemType: ItemType = .movie
@Published var movieItems = [BaseItemDto]() @Published
@Published var showItems = [BaseItemDto]() var movieItems = [BaseItemDto]()
@Published var episodeItems = [BaseItemDto]() @Published
var showItems = [BaseItemDto]()
@Published
var episodeItems = [BaseItemDto]()
@Published var suggestions = [BaseItemDto]() @Published
var suggestions = [BaseItemDto]()
var searchQuerySubject = CurrentValueSubject<String, Never>("") var searchQuerySubject = CurrentValueSubject<String, Never>("")
var parentID: String? var parentID: String?

View File

@ -1,11 +1,10 @@
// //
/* // Swiftfin is subject to the terms of the Mozilla Public
* SwiftFin is subject to the terms of the Mozilla Public // License, v2.0. If a copy of the MPL was not distributed with this
* 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/.
* file, you can obtain one at https://mozilla.org/MPL/2.0/. //
* // Copyright (c) 2022 Jellyfin & Jellyfin Contributors
* Copyright 2021 Aiden Vigue & Jellyfin Contributors //
*/
import Combine import Combine
import Foundation import Foundation
@ -26,16 +25,23 @@ final class LibraryViewModel: ViewModel {
var genre: NameGuidPair? var genre: NameGuidPair?
var studio: NameGuidPair? var studio: NameGuidPair?
@Published var items = [BaseItemDto]() @Published
@Published var rows = [LibraryRow]() var items = [BaseItemDto]()
@Published
var rows = [LibraryRow]()
@Published var totalPages = 0 @Published
@Published var currentPage = 0 var totalPages = 0
@Published var hasNextPage = false @Published
@Published var hasPreviousPage = false var currentPage = 0
@Published
var hasNextPage = false
@Published
var hasPreviousPage = false
// temp // temp
@Published var filters: LibraryFilters @Published
var filters: LibraryFilters
private let columns: Int private let columns: Int
private var libraries = [BaseItemDto]() private var libraries = [BaseItemDto]()
@ -86,8 +92,17 @@ final class LibraryViewModel: ViewModel {
searchTerm: nil, searchTerm: nil,
sortOrder: filters.sortOrder, sortOrder: filters.sortOrder,
parentId: parentID, parentId: parentID,
fields: [.primaryImageAspectRatio, .seriesPrimaryImage, .seasonUserData, .overview, .genres, .people, .chapters], fields: [
includeItemTypes: filters.filters.contains(.isFavorite) ? ["Movie", "Series", "Season", "Episode"] : ["Movie", "Series", "BoxSet"], .primaryImageAspectRatio,
.seriesPrimaryImage,
.seasonUserData,
.overview,
.genres,
.people,
.chapters,
],
includeItemTypes: filters.filters
.contains(.isFavorite) ? ["Movie", "Series", "Season", "Episode"] : ["Movie", "Series", "BoxSet"],
filters: filters.filters, filters: filters.filters,
sortBy: sortBy, sortBy: sortBy,
tags: filters.tags, tags: filters.tags,
@ -129,8 +144,17 @@ final class LibraryViewModel: ViewModel {
searchTerm: nil, searchTerm: nil,
sortOrder: filters.sortOrder, sortOrder: filters.sortOrder,
parentId: parentID, parentId: parentID,
fields: [.primaryImageAspectRatio, .seriesPrimaryImage, .seasonUserData, .overview, .genres, .people, .chapters], fields: [
includeItemTypes: filters.filters.contains(.isFavorite) ? ["Movie", "Series", "Season", "Episode"] : ["Movie", "Series"], .primaryImageAspectRatio,
.seriesPrimaryImage,
.seasonUserData,
.overview,
.genres,
.people,
.chapters,
],
includeItemTypes: filters.filters
.contains(.isFavorite) ? ["Movie", "Series", "Season", "Episode"] : ["Movie", "Series"],
filters: filters.filters, filters: filters.filters,
sortBy: sortBy, sortBy: sortBy,
tags: filters.tags, tags: filters.tags,

View File

@ -1,11 +1,10 @@
// //
/* // Swiftfin is subject to the terms of the Mozilla Public
* SwiftFin is subject to the terms of the Mozilla Public // License, v2.0. If a copy of the MPL was not distributed with this
* 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/.
* file, you can obtain one at https://mozilla.org/MPL/2.0/. //
* // Copyright (c) 2022 Jellyfin & Jellyfin Contributors
* Copyright 2021 Aiden Vigue & Jellyfin Contributors //
*/
import Foundation import Foundation
import JellyfinAPI import JellyfinAPI
@ -26,8 +25,10 @@ struct LiveTVChannelProgram: Hashable {
final class LiveTVChannelsViewModel: ViewModel { final class LiveTVChannelsViewModel: ViewModel {
@Published var channels = [BaseItemDto]() @Published
@Published var channelPrograms = [LiveTVChannelProgram]() { var channels = [BaseItemDto]()
@Published
var channelPrograms = [LiveTVChannelProgram]() {
didSet { didSet {
rows = [] rows = []
let rowChannels = channelPrograms.chunked(into: 4) let rowChannels = channelPrograms.chunked(into: 4)
@ -36,7 +37,9 @@ final class LiveTVChannelsViewModel: ViewModel {
} }
} }
} }
@Published var rows = [LiveTVChannelRow]()
@Published
var rows = [LiveTVChannelRow]()
private var programs = [BaseItemDto]() private var programs = [BaseItemDto]()
private var channelProgramsList = [BaseItemDto: [BaseItemDto]]() private var channelProgramsList = [BaseItemDto: [BaseItemDto]]()
@ -64,7 +67,7 @@ final class LiveTVChannelsViewModel: ViewModel {
.trackActivity(loading) .trackActivity(loading)
.sink(receiveCompletion: { [weak self] completion in .sink(receiveCompletion: { [weak self] completion in
self?.handleAPIRequestError(completion: completion) self?.handleAPIRequestError(completion: completion)
}, receiveValue: { [weak self] response in }, receiveValue: { [weak self] _ in
LogManager.shared.log.debug("Received Guide Info") LogManager.shared.log.debug("Received Guide Info")
guard let self = self else { return } guard let self = self else { return }
self.getChannels() self.getChannels()
@ -73,14 +76,12 @@ final class LiveTVChannelsViewModel: ViewModel {
} }
func getChannels() { func getChannels() {
LiveTvAPI.getLiveTvChannels( LiveTvAPI.getLiveTvChannels(userId: SessionManager.main.currentLogin.user.id,
userId: SessionManager.main.currentLogin.user.id,
startIndex: 0, startIndex: 0,
limit: 1000, limit: 1000,
enableImageTypes: [.primary], enableImageTypes: [.primary],
enableUserData: false, enableUserData: false,
enableFavoriteSorting: true enableFavoriteSorting: true)
)
.trackActivity(loading) .trackActivity(loading)
.sink(receiveCompletion: { [weak self] completion in .sink(receiveCompletion: { [weak self] completion in
self?.handleAPIRequestError(completion: completion) self?.handleAPIRequestError(completion: completion)
@ -95,17 +96,16 @@ final class LiveTVChannelsViewModel: ViewModel {
private func getPrograms() { private func getPrograms() {
// http://192.168.1.50:8096/LiveTv/Programs // http://192.168.1.50:8096/LiveTv/Programs
guard channels.count > 0 else { guard !channels.isEmpty else {
LogManager.shared.log.debug("Cannot get programs, channels list empty. ") LogManager.shared.log.debug("Cannot get programs, channels list empty. ")
return return
} }
let channelIds = channels.compactMap { $0.id } let channelIds = channels.compactMap(\.id)
let minEndDate = Date.now.addComponentsToDate(hours: -1) let minEndDate = Date.now.addComponentsToDate(hours: -1)
let maxStartDate = minEndDate.addComponentsToDate(hours: 6) let maxStartDate = minEndDate.addComponentsToDate(hours: 6)
let getProgramsDto = GetProgramsDto( let getProgramsDto = GetProgramsDto(channelIds: channelIds,
channelIds: channelIds,
userId: SessionManager.main.currentLogin.user.id, userId: SessionManager.main.currentLogin.user.id,
maxStartDate: maxStartDate, maxStartDate: maxStartDate,
minEndDate: minEndDate, minEndDate: minEndDate,
@ -114,8 +114,7 @@ final class LiveTVChannelsViewModel: ViewModel {
enableTotalRecordCount: false, enableTotalRecordCount: false,
imageTypeLimit: 1, imageTypeLimit: 1,
enableImageTypes: [.primary], enableImageTypes: [.primary],
enableUserData: false enableUserData: false)
)
LiveTvAPI.getPrograms(getProgramsDto: getProgramsDto) LiveTvAPI.getPrograms(getProgramsDto: getProgramsDto)
.trackActivity(loading) .trackActivity(loading)
@ -146,7 +145,8 @@ final class LiveTVChannelsViewModel: ViewModel {
if let startDate = prg.startDate, if let startDate = prg.startDate,
let endDate = prg.endDate, let endDate = prg.endDate,
now.timeIntervalSinceReferenceDate > startDate.timeIntervalSinceReferenceDate && now.timeIntervalSinceReferenceDate > startDate.timeIntervalSinceReferenceDate &&
now.timeIntervalSinceReferenceDate < endDate.timeIntervalSinceReferenceDate { now.timeIntervalSinceReferenceDate < endDate.timeIntervalSinceReferenceDate
{
currentPrg = prg currentPrg = prg
} }
} }
@ -171,7 +171,7 @@ final class LiveTVChannelsViewModel: ViewModel {
if let existingTimer = timer { if let existingTimer = timer {
existingTimer.invalidate() existingTimer.invalidate()
} }
timer = Timer(fire: nextMinute, interval: 60 * 10, repeats: true) { [weak self] timer in timer = Timer(fire: nextMinute, interval: 60 * 10, repeats: true) { [weak self] _ in
guard let self = self else { return } guard let self = self else { return }
LogManager.shared.log.debug("LiveTVChannels schedule check...") LogManager.shared.log.debug("LiveTVChannels schedule check...")
DispatchQueue.global(qos: .background).async { DispatchQueue.global(qos: .background).async {
@ -193,7 +193,7 @@ final class LiveTVChannelsViewModel: ViewModel {
extension Array { extension Array {
func chunked(into size: Int) -> [[Element]] { func chunked(into size: Int) -> [[Element]] {
return stride(from: 0, to: count, by: size).map { stride(from: 0, to: count, by: size).map {
Array(self[$0 ..< Swift.min($0 + size, count)]) Array(self[$0 ..< Swift.min($0 + size, count)])
} }
} }

View File

@ -1,23 +1,28 @@
// //
/* // Swiftfin is subject to the terms of the Mozilla Public
* SwiftFin is subject to the terms of the Mozilla Public // License, v2.0. If a copy of the MPL was not distributed with this
* 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/.
* file, you can obtain one at https://mozilla.org/MPL/2.0/. //
* // Copyright (c) 2022 Jellyfin & Jellyfin Contributors
* Copyright 2021 Aiden Vigue & Jellyfin Contributors //
*/
import Foundation import Foundation
import JellyfinAPI import JellyfinAPI
final class LiveTVProgramsViewModel: ViewModel { final class LiveTVProgramsViewModel: ViewModel {
@Published var recommendedItems = [BaseItemDto]() @Published
@Published var seriesItems = [BaseItemDto]() var recommendedItems = [BaseItemDto]()
@Published var movieItems = [BaseItemDto]() @Published
@Published var sportsItems = [BaseItemDto]() var seriesItems = [BaseItemDto]()
@Published var kidsItems = [BaseItemDto]() @Published
@Published var newsItems = [BaseItemDto]() var movieItems = [BaseItemDto]()
@Published
var sportsItems = [BaseItemDto]()
@Published
var kidsItems = [BaseItemDto]()
@Published
var newsItems = [BaseItemDto]()
private var channels = [String: BaseItemDto]() private var channels = [String: BaseItemDto]()
@ -28,18 +33,16 @@ final class LiveTVProgramsViewModel: ViewModel {
} }
func findChannel(id: String) -> BaseItemDto? { func findChannel(id: String) -> BaseItemDto? {
return channels[id] channels[id]
} }
private func getChannels() { private func getChannels() {
LiveTvAPI.getLiveTvChannels( LiveTvAPI.getLiveTvChannels(userId: SessionManager.main.currentLogin.user.id,
userId: SessionManager.main.currentLogin.user.id,
startIndex: 0, startIndex: 0,
limit: 1000, limit: 1000,
enableImageTypes: [.primary], enableImageTypes: [.primary],
enableUserData: false, enableUserData: false,
enableFavoriteSorting: true enableFavoriteSorting: true)
)
.trackActivity(loading) .trackActivity(loading)
.sink(receiveCompletion: { [weak self] completion in .sink(receiveCompletion: { [weak self] completion in
self?.handleAPIRequestError(completion: completion) self?.handleAPIRequestError(completion: completion)
@ -64,15 +67,13 @@ final class LiveTVProgramsViewModel: ViewModel {
} }
private func getRecommendedPrograms() { private func getRecommendedPrograms() {
LiveTvAPI.getRecommendedPrograms( LiveTvAPI.getRecommendedPrograms(userId: SessionManager.main.currentLogin.user.id,
userId: SessionManager.main.currentLogin.user.id,
limit: 9, limit: 9,
isAiring: true, isAiring: true,
imageTypeLimit: 1, imageTypeLimit: 1,
enableImageTypes: [.primary, .thumb], enableImageTypes: [.primary, .thumb],
fields: [.channelInfo, .primaryImageAspectRatio], fields: [.channelInfo, .primaryImageAspectRatio],
enableTotalRecordCount: false enableTotalRecordCount: false)
)
.trackActivity(loading) .trackActivity(loading)
.sink(receiveCompletion: { [weak self] completion in .sink(receiveCompletion: { [weak self] completion in
self?.handleAPIRequestError(completion: completion) self?.handleAPIRequestError(completion: completion)
@ -95,8 +96,7 @@ final class LiveTVProgramsViewModel: ViewModel {
limit: 9, limit: 9,
enableTotalRecordCount: false, enableTotalRecordCount: false,
enableImageTypes: [.primary, .thumb], enableImageTypes: [.primary, .thumb],
fields: [.channelInfo, .primaryImageAspectRatio] fields: [.channelInfo, .primaryImageAspectRatio])
)
LiveTvAPI.getPrograms(getProgramsDto: getProgramsDto) LiveTvAPI.getPrograms(getProgramsDto: getProgramsDto)
.trackActivity(loading) .trackActivity(loading)
@ -121,8 +121,7 @@ final class LiveTVProgramsViewModel: ViewModel {
limit: 9, limit: 9,
enableTotalRecordCount: false, enableTotalRecordCount: false,
enableImageTypes: [.primary, .thumb], enableImageTypes: [.primary, .thumb],
fields: [.channelInfo, .primaryImageAspectRatio] fields: [.channelInfo, .primaryImageAspectRatio])
)
LiveTvAPI.getPrograms(getProgramsDto: getProgramsDto) LiveTvAPI.getPrograms(getProgramsDto: getProgramsDto)
.trackActivity(loading) .trackActivity(loading)
@ -143,8 +142,7 @@ final class LiveTVProgramsViewModel: ViewModel {
limit: 9, limit: 9,
enableTotalRecordCount: false, enableTotalRecordCount: false,
enableImageTypes: [.primary, .thumb], enableImageTypes: [.primary, .thumb],
fields: [.channelInfo, .primaryImageAspectRatio] fields: [.channelInfo, .primaryImageAspectRatio])
)
LiveTvAPI.getPrograms(getProgramsDto: getProgramsDto) LiveTvAPI.getPrograms(getProgramsDto: getProgramsDto)
.trackActivity(loading) .trackActivity(loading)
@ -165,8 +163,7 @@ final class LiveTVProgramsViewModel: ViewModel {
limit: 9, limit: 9,
enableTotalRecordCount: false, enableTotalRecordCount: false,
enableImageTypes: [.primary, .thumb], enableImageTypes: [.primary, .thumb],
fields: [.channelInfo, .primaryImageAspectRatio] fields: [.channelInfo, .primaryImageAspectRatio])
)
LiveTvAPI.getPrograms(getProgramsDto: getProgramsDto) LiveTvAPI.getPrograms(getProgramsDto: getProgramsDto)
.trackActivity(loading) .trackActivity(loading)
@ -187,8 +184,7 @@ final class LiveTVProgramsViewModel: ViewModel {
limit: 9, limit: 9,
enableTotalRecordCount: false, enableTotalRecordCount: false,
enableImageTypes: [.primary, .thumb], enableImageTypes: [.primary, .thumb],
fields: [.channelInfo, .primaryImageAspectRatio] fields: [.channelInfo, .primaryImageAspectRatio])
)
LiveTvAPI.getPrograms(getProgramsDto: getProgramsDto) LiveTvAPI.getPrograms(getProgramsDto: getProgramsDto)
.trackActivity(loading) .trackActivity(loading)

View File

@ -1,19 +1,21 @@
// //
/* // Swiftfin is subject to the terms of the Mozilla Public
* SwiftFin is subject to the terms of the Mozilla Public // License, v2.0. If a copy of the MPL was not distributed with this
* 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/.
* file, you can obtain one at https://mozilla.org/MPL/2.0/. //
* // Copyright (c) 2022 Jellyfin & Jellyfin Contributors
* Copyright 2021 Aiden Vigue & Jellyfin Contributors //
*/
import Foundation import Foundation
import JellyfinAPI import JellyfinAPI
final class MainTabViewModel: ViewModel { final class MainTabViewModel: ViewModel {
@Published var backgroundURL: URL? @Published
@Published var lastBackgroundURL: URL? var backgroundURL: URL?
@Published var backgroundBlurHash: String = "001fC^" @Published
var lastBackgroundURL: URL?
@Published
var backgroundBlurHash: String = "001fC^"
override init() { override init() {
super.init() super.init()
@ -22,7 +24,8 @@ final class MainTabViewModel: ViewModel {
nc.addObserver(self, selector: #selector(backgroundDidChange), name: Notification.Name("backgroundDidChange"), object: nil) nc.addObserver(self, selector: #selector(backgroundDidChange), name: Notification.Name("backgroundDidChange"), object: nil)
} }
@objc func backgroundDidChange() { @objc
func backgroundDidChange() {
self.lastBackgroundURL = self.backgroundURL self.lastBackgroundURL = self.backgroundURL
self.backgroundURL = BackgroundManager.current.backgroundURL self.backgroundURL = BackgroundManager.current.backgroundURL
self.backgroundBlurHash = BackgroundManager.current.blurhash self.backgroundBlurHash = BackgroundManager.current.blurhash

View File

@ -1,11 +1,10 @@
// //
/* // Swiftfin is subject to the terms of the Mozilla Public
* SwiftFin is subject to the terms of the Mozilla Public // License, v2.0. If a copy of the MPL was not distributed with this
* 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/.
* file, you can obtain one at https://mozilla.org/MPL/2.0/. //
* // Copyright (c) 2022 Jellyfin & Jellyfin Contributors
* Copyright 2021 Aiden Vigue & Jellyfin Contributors //
*/
import Combine import Combine
import Foundation import Foundation
@ -15,11 +14,16 @@ import SwiftUICollection
final class MovieLibrariesViewModel: ViewModel { final class MovieLibrariesViewModel: ViewModel {
@Published var rows = [LibraryRow]() @Published
@Published var totalPages = 0 var rows = [LibraryRow]()
@Published var currentPage = 0 @Published
@Published var hasNextPage = false var totalPages = 0
@Published var hasPreviousPage = false @Published
var currentPage = 0
@Published
var hasNextPage = false
@Published
var hasPreviousPage = false
private var libraries = [BaseItemDto]() private var libraries = [BaseItemDto]()
private let columns: Int private let columns: Int
@ -27,9 +31,7 @@ final class MovieLibrariesViewModel: ViewModel {
@RouterObject @RouterObject
var router: MovieLibrariesCoordinator.Router? var router: MovieLibrariesCoordinator.Router?
init( init(columns: Int = 7) {
columns: Int = 7
) {
self.columns = columns self.columns = columns
super.init() super.init()
@ -38,8 +40,7 @@ final class MovieLibrariesViewModel: ViewModel {
func requestLibraries() { func requestLibraries() {
UserViewsAPI.getUserViews( UserViewsAPI.getUserViews(userId: SessionManager.main.currentLogin.user.id)
userId: SessionManager.main.currentLogin.user.id)
.trackActivity(loading) .trackActivity(loading)
.sink(receiveCompletion: { completion in .sink(receiveCompletion: { completion in
self.handleAPIRequestError(completion: completion) self.handleAPIRequestError(completion: completion)
@ -62,10 +63,10 @@ final class MovieLibrariesViewModel: ViewModel {
} }
private func calculateRows() -> [LibraryRow] { private func calculateRows() -> [LibraryRow] {
guard libraries.count > 0 else { return [] } guard !libraries.isEmpty else { return [] }
let rowCount = libraries.count / columns let rowCount = libraries.count / columns
var calculatedRows = [LibraryRow]() var calculatedRows = [LibraryRow]()
for i in (0...rowCount) { for i in 0 ... rowCount {
let firstItemIndex = i * columns let firstItemIndex = i * columns
var lastItemIndex = firstItemIndex + columns var lastItemIndex = firstItemIndex + columns
if lastItemIndex > libraries.count { if lastItemIndex > libraries.count {
@ -83,12 +84,8 @@ final class MovieLibrariesViewModel: ViewModel {
rowCells.append(loadingCell) rowCells.append(loadingCell)
} }
calculatedRows.append( calculatedRows.append(LibraryRow(section: i,
LibraryRow( items: rowCells))
section: i,
items: rowCells
)
)
} }
return calculatedRows return calculatedRows
} }

View File

@ -1,18 +1,18 @@
// //
/* // Swiftfin is subject to the terms of the Mozilla Public
* SwiftFin is subject to the terms of the Mozilla Public // License, v2.0. If a copy of the MPL was not distributed with this
* 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/.
* file, you can obtain one at https://mozilla.org/MPL/2.0/. //
* // Copyright (c) 2022 Jellyfin & Jellyfin Contributors
* Copyright 2021 Aiden Vigue & Jellyfin Contributors //
*/
import Foundation import Foundation
import JellyfinAPI import JellyfinAPI
class ServerDetailViewModel: ViewModel { class ServerDetailViewModel: ViewModel {
@Published var server: SwiftfinStore.State.Server @Published
var server: SwiftfinStore.State.Server
init(server: SwiftfinStore.State.Server) { init(server: SwiftfinStore.State.Server) {
self.server = server self.server = server

View File

@ -1,18 +1,18 @@
// //
/* // Swiftfin is subject to the terms of the Mozilla Public
* SwiftFin is subject to the terms of the Mozilla Public // License, v2.0. If a copy of the MPL was not distributed with this
* 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/.
* file, you can obtain one at https://mozilla.org/MPL/2.0/. //
* // Copyright (c) 2022 Jellyfin & Jellyfin Contributors
* Copyright 2021 Aiden Vigue & Jellyfin Contributors //
*/
import Foundation import Foundation
import SwiftUI import SwiftUI
class ServerListViewModel: ObservableObject { class ServerListViewModel: ObservableObject {
@Published var servers: [SwiftfinStore.State.Server] = [] @Published
var servers: [SwiftfinStore.State.Server] = []
init() { init() {
@ -41,7 +41,8 @@ class ServerListViewModel: ObservableObject {
fetchServers() fetchServers()
} }
@objc private func didPurge() { @objc
private func didPurge() {
fetchServers() fetchServers()
} }
} }

View File

@ -1,15 +1,14 @@
// //
/* // Swiftfin is subject to the terms of the Mozilla Public
* SwiftFin is subject to the terms of the Mozilla Public // License, v2.0. If a copy of the MPL was not distributed with this
* 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/.
* file, you can obtain one at https://mozilla.org/MPL/2.0/. //
* // Copyright (c) 2022 Jellyfin & Jellyfin Contributors
* Copyright 2021 Aiden Vigue & Jellyfin Contributors //
*/
import Defaults
import Foundation import Foundation
import SwiftUI import SwiftUI
import Defaults
final class SettingsViewModel: ObservableObject { final class SettingsViewModel: ObservableObject {

View File

@ -1,11 +1,10 @@
// //
/* // Swiftfin is subject to the terms of the Mozilla Public
* SwiftFin is subject to the terms of the Mozilla Public // License, v2.0. If a copy of the MPL was not distributed with this
* 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/.
* file, you can obtain one at https://mozilla.org/MPL/2.0/. //
* // Copyright (c) 2022 Jellyfin & Jellyfin Contributors
* Copyright 2021 Aiden Vigue & Jellyfin Contributors //
*/
import Combine import Combine
import Foundation import Foundation
@ -15,11 +14,16 @@ import SwiftUICollection
final class TVLibrariesViewModel: ViewModel { final class TVLibrariesViewModel: ViewModel {
@Published var rows = [LibraryRow]() @Published
@Published var totalPages = 0 var rows = [LibraryRow]()
@Published var currentPage = 0 @Published
@Published var hasNextPage = false var totalPages = 0
@Published var hasPreviousPage = false @Published
var currentPage = 0
@Published
var hasNextPage = false
@Published
var hasPreviousPage = false
private var libraries = [BaseItemDto]() private var libraries = [BaseItemDto]()
private let columns: Int private let columns: Int
@ -27,9 +31,7 @@ final class TVLibrariesViewModel: ViewModel {
@RouterObject @RouterObject
var router: TVLibrariesCoordinator.Router? var router: TVLibrariesCoordinator.Router?
init( init(columns: Int = 7) {
columns: Int = 7
) {
self.columns = columns self.columns = columns
super.init() super.init()
@ -38,8 +40,7 @@ final class TVLibrariesViewModel: ViewModel {
func requestLibraries() { func requestLibraries() {
UserViewsAPI.getUserViews( UserViewsAPI.getUserViews(userId: SessionManager.main.currentLogin.user.id)
userId: SessionManager.main.currentLogin.user.id)
.trackActivity(loading) .trackActivity(loading)
.sink(receiveCompletion: { completion in .sink(receiveCompletion: { completion in
self.handleAPIRequestError(completion: completion) self.handleAPIRequestError(completion: completion)
@ -62,10 +63,10 @@ final class TVLibrariesViewModel: ViewModel {
} }
private func calculateRows() -> [LibraryRow] { private func calculateRows() -> [LibraryRow] {
guard libraries.count > 0 else { return [] } guard !libraries.isEmpty else { return [] }
let rowCount = libraries.count / columns let rowCount = libraries.count / columns
var calculatedRows = [LibraryRow]() var calculatedRows = [LibraryRow]()
for i in (0...rowCount) { for i in 0 ... rowCount {
let firstItemIndex = i * columns let firstItemIndex = i * columns
var lastItemIndex = firstItemIndex + columns var lastItemIndex = firstItemIndex + columns
if lastItemIndex > libraries.count { if lastItemIndex > libraries.count {
@ -83,12 +84,8 @@ final class TVLibrariesViewModel: ViewModel {
rowCells.append(loadingCell) rowCells.append(loadingCell)
} }
calculatedRows.append( calculatedRows.append(LibraryRow(section: i,
LibraryRow( items: rowCells))
section: i,
items: rowCells
)
)
} }
return calculatedRows return calculatedRows
} }

View File

@ -1,18 +1,18 @@
// //
/* // Swiftfin is subject to the terms of the Mozilla Public
* SwiftFin is subject to the terms of the Mozilla Public // License, v2.0. If a copy of the MPL was not distributed with this
* 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/.
* file, you can obtain one at https://mozilla.org/MPL/2.0/. //
* // Copyright (c) 2022 Jellyfin & Jellyfin Contributors
* Copyright 2021 Aiden Vigue & Jellyfin Contributors //
*/
import Foundation import Foundation
import SwiftUI import SwiftUI
class UserListViewModel: ViewModel { class UserListViewModel: ViewModel {
@Published var users: [SwiftfinStore.State.User] = [] @Published
var users: [SwiftfinStore.State.User] = []
var server: SwiftfinStore.State.Server var server: SwiftfinStore.State.Server
@ -22,10 +22,12 @@ class UserListViewModel: ViewModel {
super.init() super.init()
let nc = SwiftfinNotificationCenter.main let nc = SwiftfinNotificationCenter.main
nc.addObserver(self, selector: #selector(didChangeCurrentLoginURI), name: SwiftfinNotificationCenter.Keys.didChangeServerCurrentURI, object: nil) nc.addObserver(self, selector: #selector(didChangeCurrentLoginURI), name: SwiftfinNotificationCenter.Keys.didChangeServerCurrentURI,
object: nil)
} }
@objc func didChangeCurrentLoginURI(_ notification: Notification) { @objc
func didChangeCurrentLoginURI(_ notification: Notification) {
guard let newServerState = notification.object as? SwiftfinStore.State.Server else { fatalError("Need to have new state server") } guard let newServerState = notification.object as? SwiftfinStore.State.Server else { fatalError("Need to have new state server") }
self.server = newServerState self.server = newServerState
} }
@ -43,5 +45,4 @@ class UserListViewModel: ViewModel {
SessionManager.main.delete(user: user) SessionManager.main.delete(user: user)
fetchUsers() fetchUsers()
} }
} }

View File

@ -1,11 +1,10 @@
// //
/* // Swiftfin is subject to the terms of the Mozilla Public
* SwiftFin is subject to the terms of the Mozilla Public // License, v2.0. If a copy of the MPL was not distributed with this
* 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/.
* file, you can obtain one at https://mozilla.org/MPL/2.0/. //
* // Copyright (c) 2022 Jellyfin & Jellyfin Contributors
* Copyright 2021 Aiden Vigue & Jellyfin Contributors //
*/
import CoreStore import CoreStore
import Foundation import Foundation
@ -13,7 +12,8 @@ import Stinsen
final class UserSignInViewModel: ViewModel { final class UserSignInViewModel: ViewModel {
@RouterObject var router: UserSignInCoordinator.Router? @RouterObject
var router: UserSignInCoordinator.Router?
let server: SwiftfinStore.State.Server let server: SwiftfinStore.State.Server
init(server: SwiftfinStore.State.Server) { init(server: SwiftfinStore.State.Server) {
@ -39,7 +39,6 @@ final class UserSignInViewModel: ViewModel {
self.handleAPIRequestError(displayMessage: "Unable to connect to server.", logLevel: .critical, tag: "login", self.handleAPIRequestError(displayMessage: "Unable to connect to server.", logLevel: .critical, tag: "login",
completion: completion) completion: completion)
} receiveValue: { _ in } receiveValue: { _ in
} }
.store(in: &cancellables) .store(in: &cancellables)
} }

View File

@ -1,14 +1,13 @@
// //
/* // Swiftfin is subject to the terms of the Mozilla Public
* SwiftFin is subject to the terms of the Mozilla Public // License, v2.0. If a copy of the MPL was not distributed with this
* 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/.
* file, you can obtain one at https://mozilla.org/MPL/2.0/. //
* // Copyright (c) 2022 Jellyfin & Jellyfin Contributors
* Copyright 2021 Aiden Vigue & Jellyfin Contributors //
*/
import SwiftUI
import JellyfinAPI import JellyfinAPI
import SwiftUI
struct Subtitle { struct Subtitle {
var name: String var name: String
@ -26,6 +25,8 @@ struct AudioTrack {
} }
class PlaybackItem: ObservableObject { class PlaybackItem: ObservableObject {
@Published var videoType: PlayMethod = .directPlay @Published
@Published var videoUrl: URL = URL(string: "https://example.com")! var videoType: PlayMethod = .directPlay
@Published
var videoUrl = URL(string: "https://example.com")!
} }

View File

@ -1,11 +1,10 @@
// //
/* // Swiftfin is subject to the terms of the Mozilla Public
* SwiftFin is subject to the terms of the Mozilla Public // License, v2.0. If a copy of the MPL was not distributed with this
* 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/.
* file, you can obtain one at https://mozilla.org/MPL/2.0/. //
* // Copyright (c) 2022 Jellyfin & Jellyfin Contributors
* Copyright 2021 Aiden Vigue & Jellyfin Contributors //
*/
import Foundation import Foundation

View File

@ -1,11 +1,10 @@
// //
/* // Swiftfin is subject to the terms of the Mozilla Public
* SwiftFin is subject to the terms of the Mozilla Public // License, v2.0. If a copy of the MPL was not distributed with this
* 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/.
* file, you can obtain one at https://mozilla.org/MPL/2.0/. //
* // Copyright (c) 2022 Jellyfin & Jellyfin Contributors
* Copyright 2021 Aiden Vigue & Jellyfin Contributors //
*/
import Combine import Combine
import Defaults import Defaults
@ -25,11 +24,16 @@ final class VideoPlayerViewModel: ViewModel {
// Manually kept state because VLCKit doesn't properly set "played" // Manually kept state because VLCKit doesn't properly set "played"
// on the VLCMediaPlayer object // on the VLCMediaPlayer object
@Published var playerState: VLCMediaPlayerState = .buffering @Published
@Published var leftLabelText: String = "--:--" var playerState: VLCMediaPlayerState = .buffering
@Published var rightLabelText: String = "--:--" @Published
@Published var playbackSpeed: PlaybackSpeed = .one var leftLabelText: String = "--:--"
@Published var subtitlesEnabled: Bool { @Published
var rightLabelText: String = "--:--"
@Published
var playbackSpeed: PlaybackSpeed = .one
@Published
var subtitlesEnabled: Bool {
didSet { didSet {
if syncSubtitleStateWithAdjacent { if syncSubtitleStateWithAdjacent {
previousItemVideoPlayerViewModel?.matchSubtitlesEnabled(with: self) previousItemVideoPlayerViewModel?.matchSubtitlesEnabled(with: self)
@ -37,8 +41,11 @@ final class VideoPlayerViewModel: ViewModel {
} }
} }
} }
@Published var selectedAudioStreamIndex: Int
@Published var selectedSubtitleStreamIndex: Int { @Published
var selectedAudioStreamIndex: Int
@Published
var selectedSubtitleStreamIndex: Int {
didSet { didSet {
if syncSubtitleStateWithAdjacent { if syncSubtitleStateWithAdjacent {
previousItemVideoPlayerViewModel?.matchSubtitleStream(with: self) previousItemVideoPlayerViewModel?.matchSubtitleStream(with: self)
@ -46,26 +53,37 @@ final class VideoPlayerViewModel: ViewModel {
} }
} }
} }
@Published var previousItemVideoPlayerViewModel: VideoPlayerViewModel?
@Published var nextItemVideoPlayerViewModel: VideoPlayerViewModel? @Published
@Published var jumpBackwardLength: VideoPlayerJumpLength { var previousItemVideoPlayerViewModel: VideoPlayerViewModel?
@Published
var nextItemVideoPlayerViewModel: VideoPlayerViewModel?
@Published
var jumpBackwardLength: VideoPlayerJumpLength {
willSet { willSet {
Defaults[.videoPlayerJumpBackward] = newValue Defaults[.videoPlayerJumpBackward] = newValue
} }
} }
@Published var jumpForwardLength: VideoPlayerJumpLength {
@Published
var jumpForwardLength: VideoPlayerJumpLength {
willSet { willSet {
Defaults[.videoPlayerJumpForward] = newValue Defaults[.videoPlayerJumpForward] = newValue
} }
} }
@Published var sliderIsScrubbing: Bool = false
@Published var sliderPercentage: Double = 0 { @Published
var sliderIsScrubbing: Bool = false
@Published
var sliderPercentage: Double = 0 {
willSet { willSet {
sliderScrubbingSubject.send(self) sliderScrubbingSubject.send(self)
sliderPercentageChanged(newValue: newValue) sliderPercentageChanged(newValue: newValue)
} }
} }
@Published var autoplayEnabled: Bool {
@Published
var autoplayEnabled: Bool {
willSet { willSet {
previousItemVideoPlayerViewModel?.autoplayEnabled = newValue previousItemVideoPlayerViewModel?.autoplayEnabled = newValue
nextItemVideoPlayerViewModel?.autoplayEnabled = newValue nextItemVideoPlayerViewModel?.autoplayEnabled = newValue
@ -81,6 +99,7 @@ final class VideoPlayerViewModel: ViewModel {
let shouldShowJumpButtonsInOverlayMenu: Bool let shouldShowJumpButtonsInOverlayMenu: Bool
// MARK: General // MARK: General
let item: BaseItemDto let item: BaseItemDto
let title: String let title: String
let subtitle: String? let subtitle: String?
@ -93,9 +112,11 @@ final class VideoPlayerViewModel: ViewModel {
let streamType: ServerStreamType let streamType: ServerStreamType
// MARK: Experimental // MARK: Experimental
let syncSubtitleStateWithAdjacent: Bool let syncSubtitleStateWithAdjacent: Bool
// MARK: tvOS // MARK: tvOS
let confirmClose: Bool let confirmClose: Bool
// Full response kept for convenience // Full response kept for convenience
@ -114,17 +135,17 @@ final class VideoPlayerViewModel: ViewModel {
} }
var currentSecondTicks: Int64 { var currentSecondTicks: Int64 {
return Int64(currentSeconds) * 10_000_000 Int64(currentSeconds) * 10_000_000
} }
// MARK: Helpers // MARK: Helpers
var currentAudioStream: MediaStream? { var currentAudioStream: MediaStream? {
return audioStreams.first(where: { $0.index == selectedAudioStreamIndex }) audioStreams.first(where: { $0.index == selectedAudioStreamIndex })
} }
var currentSubtitleStream: MediaStream? { var currentSubtitleStream: MediaStream? {
return subtitleStreams.first(where: { $0.index == selectedSubtitleStreamIndex }) subtitleStreams.first(where: { $0.index == selectedSubtitleStreamIndex })
} }
// Necessary PassthroughSubject to capture manual scrubbing from sliders // Necessary PassthroughSubject to capture manual scrubbing from sliders
@ -152,7 +173,8 @@ final class VideoPlayerViewModel: ViewModel {
overlayType: OverlayType, overlayType: OverlayType,
shouldShowPlayPreviousItem: Bool, shouldShowPlayPreviousItem: Bool,
shouldShowPlayNextItem: Bool, shouldShowPlayNextItem: Bool,
shouldShowAutoPlay: Bool) { shouldShowAutoPlay: Bool)
{
self.item = item self.item = item
self.title = title self.title = title
self.subtitle = subtitle self.subtitle = subtitle
@ -214,6 +236,7 @@ final class VideoPlayerViewModel: ViewModel {
} }
// MARK: Adjacent Items // MARK: Adjacent Items
extension VideoPlayerViewModel { extension VideoPlayerViewModel {
func getAdjacentEpisodes() { func getAdjacentEpisodes() {
@ -306,7 +329,8 @@ extension VideoPlayerViewModel {
matchSubtitlesEnabled(with: masterViewModel) matchSubtitlesEnabled(with: masterViewModel)
} }
guard let masterSubtitleStream = masterViewModel.subtitleStreams.first(where: { $0.index == masterViewModel.selectedSubtitleStreamIndex }), guard let masterSubtitleStream = masterViewModel.subtitleStreams
.first(where: { $0.index == masterViewModel.selectedSubtitleStreamIndex }),
let matchingSubtitleStream = self.subtitleStreams.first(where: { mediaStreamAboutEqual($0, masterSubtitleStream) }), let matchingSubtitleStream = self.subtitleStreams.first(where: { mediaStreamAboutEqual($0, masterSubtitleStream) }),
let matchingSubtitleStreamIndex = matchingSubtitleStream.index else { return } let matchingSubtitleStreamIndex = matchingSubtitleStream.index else { return }
@ -325,24 +349,27 @@ extension VideoPlayerViewModel {
} }
private func mediaStreamAboutEqual(_ lhs: MediaStream, _ rhs: MediaStream) -> Bool { private func mediaStreamAboutEqual(_ lhs: MediaStream, _ rhs: MediaStream) -> Bool {
return lhs.displayTitle == rhs.displayTitle && lhs.language == rhs.language lhs.displayTitle == rhs.displayTitle && lhs.language == rhs.language
} }
} }
// MARK: Progress Report Timer // MARK: Progress Report Timer
extension VideoPlayerViewModel { extension VideoPlayerViewModel {
private func sendNewProgressReportWithTimer() { private func sendNewProgressReportWithTimer() {
self.progressReportTimer?.invalidate() self.progressReportTimer?.invalidate()
self.progressReportTimer = Timer.scheduledTimer(timeInterval: 0.7, target: self, selector: #selector(_sendProgressReport), userInfo: nil, repeats: false) self.progressReportTimer = Timer.scheduledTimer(timeInterval: 0.7, target: self, selector: #selector(_sendProgressReport),
userInfo: nil, repeats: false)
} }
} }
// MARK: Updates // MARK: Updates
extension VideoPlayerViewModel { extension VideoPlayerViewModel {
// MARK: sendPlayReport // MARK: sendPlayReport
func sendPlayReport() { func sendPlayReport() {
self.startTimeTicks = Int64(Date().timeIntervalSince1970) * 10_000_000 self.startTimeTicks = Int64(Date().timeIntervalSince1970) * 10_000_000
@ -368,8 +395,7 @@ extension VideoPlayerViewModel {
playSessionId: response.playSessionId, playSessionId: response.playSessionId,
repeatMode: .repeatNone, repeatMode: .repeatNone,
nowPlayingQueue: nil, nowPlayingQueue: nil,
playlistItemId: "playlistItem0" playlistItemId: "playlistItem0")
)
PlaystateAPI.reportPlaybackStart(playbackStartInfo: startInfo) PlaystateAPI.reportPlaybackStart(playbackStartInfo: startInfo)
.sink { completion in .sink { completion in
@ -381,6 +407,7 @@ extension VideoPlayerViewModel {
} }
// MARK: sendPauseReport // MARK: sendPauseReport
func sendPauseReport(paused: Bool) { func sendPauseReport(paused: Bool) {
let subtitleStreamIndex = subtitlesEnabled ? selectedSubtitleStreamIndex : nil let subtitleStreamIndex = subtitlesEnabled ? selectedSubtitleStreamIndex : nil
@ -404,8 +431,7 @@ extension VideoPlayerViewModel {
playSessionId: response.playSessionId, playSessionId: response.playSessionId,
repeatMode: .repeatNone, repeatMode: .repeatNone,
nowPlayingQueue: nil, nowPlayingQueue: nil,
playlistItemId: "playlistItem0" playlistItemId: "playlistItem0")
)
PlaystateAPI.reportPlaybackStart(playbackStartInfo: pauseInfo) PlaystateAPI.reportPlaybackStart(playbackStartInfo: pauseInfo)
.sink { completion in .sink { completion in
@ -417,6 +443,7 @@ extension VideoPlayerViewModel {
} }
// MARK: sendProgressReport // MARK: sendProgressReport
func sendProgressReport() { func sendProgressReport() {
let subtitleStreamIndex = subtitlesEnabled ? selectedSubtitleStreamIndex : nil let subtitleStreamIndex = subtitlesEnabled ? selectedSubtitleStreamIndex : nil
@ -447,7 +474,8 @@ extension VideoPlayerViewModel {
self.sendNewProgressReportWithTimer() self.sendNewProgressReportWithTimer()
} }
@objc private func _sendProgressReport() { @objc
private func _sendProgressReport() {
guard let lastProgressReport = lastProgressReport else { return } guard let lastProgressReport = lastProgressReport else { return }
PlaystateAPI.reportPlaybackProgress(playbackProgressInfo: lastProgressReport) PlaystateAPI.reportPlaybackProgress(playbackProgressInfo: lastProgressReport)
@ -462,6 +490,7 @@ extension VideoPlayerViewModel {
} }
// MARK: sendStopReport // MARK: sendStopReport
func sendStopReport() { func sendStopReport() {
let stopInfo = PlaybackStopInfo(item: item, let stopInfo = PlaybackStopInfo(item: item,
@ -487,6 +516,7 @@ extension VideoPlayerViewModel {
} }
// MARK: Embedded/Normal Subtitle Streams // MARK: Embedded/Normal Subtitle Streams
extension VideoPlayerViewModel { extension VideoPlayerViewModel {
func createEmbeddedSubtitleStream(with subtitleStream: MediaStream) -> URL { func createEmbeddedSubtitleStream(with subtitleStream: MediaStream) -> URL {

View File

@ -1,21 +1,22 @@
// //
/* // Swiftfin is subject to the terms of the Mozilla Public
* SwiftFin is subject to the terms of the Mozilla Public // License, v2.0. If a copy of the MPL was not distributed with this
* 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/.
* file, you can obtain one at https://mozilla.org/MPL/2.0/. //
* // Copyright (c) 2022 Jellyfin & Jellyfin Contributors
* Copyright 2021 Aiden Vigue & Jellyfin Contributors //
*/
import ActivityIndicator
import Combine import Combine
import Foundation import Foundation
import ActivityIndicator
import JellyfinAPI import JellyfinAPI
class ViewModel: ObservableObject { class ViewModel: ObservableObject {
@Published var isLoading = false @Published
@Published var errorMessage: ErrorMessage? var isLoading = false
@Published
var errorMessage: ErrorMessage?
let loading = ActivityIndicator() let loading = ActivityIndicator()
var cancellables = Set<AnyCancellable>() var cancellables = Set<AnyCancellable>()
@ -24,12 +25,15 @@ class ViewModel: ObservableObject {
loading.loading.assign(to: \.isLoading, on: self).store(in: &cancellables) loading.loading.assign(to: \.isLoading, on: self).store(in: &cancellables)
} }
func handleAPIRequestError(displayMessage: String? = nil, logLevel: LogLevel = .error, tag: String = "", function: String = #function, file: String = #file, line: UInt = #line, completion: Subscribers.Completion<Error>) { func handleAPIRequestError(displayMessage: String? = nil, logLevel: LogLevel = .error, tag: String = "", function: String = #function,
file: String = #file, line: UInt = #line, completion: Subscribers.Completion<Error>)
{
switch completion { switch completion {
case .finished: case .finished:
self.errorMessage = nil self.errorMessage = nil
case .failure(let error): case let .failure(error):
let logConstructor = LogConstructor(message: "__NOTHING__", tag: tag, level: logLevel, function: function, file: file, line: line) let logConstructor = LogConstructor(message: "__NOTHING__", tag: tag, level: logLevel, function: function, file: file,
line: line)
switch error { switch error {
case is ErrorResponse: case is ErrorResponse:
@ -39,14 +43,17 @@ class ViewModel: ObservableObject {
case .error(-1, _, _, _): case .error(-1, _, _, _):
networkError = .URLError(response: errorResponse, displayMessage: displayMessage, logConstructor: logConstructor) networkError = .URLError(response: errorResponse, displayMessage: displayMessage, logConstructor: logConstructor)
// Use the errorResponse description for debugging, rather than the user-facing friendly description which may not be implemented // Use the errorResponse description for debugging, rather than the user-facing friendly description which may not be implemented
LogManager.shared.log.error("Request failed: URL request failed with error \(networkError.errorMessage.code): \(errorResponse.localizedDescription)") LogManager.shared.log
.error("Request failed: URL request failed with error \(networkError.errorMessage.code): \(errorResponse.localizedDescription)")
case .error(-2, _, _, _): case .error(-2, _, _, _):
networkError = .HTTPURLError(response: errorResponse, displayMessage: displayMessage, logConstructor: logConstructor) networkError = .HTTPURLError(response: errorResponse, displayMessage: displayMessage, logConstructor: logConstructor)
LogManager.shared.log.error("Request failed: HTTP URL request failed with description: \(errorResponse.localizedDescription)") LogManager.shared.log
.error("Request failed: HTTP URL request failed with description: \(errorResponse.localizedDescription)")
default: default:
networkError = .JellyfinError(response: errorResponse, displayMessage: displayMessage, logConstructor: logConstructor) networkError = .JellyfinError(response: errorResponse, displayMessage: displayMessage, logConstructor: logConstructor)
// Able to use user-facing friendly description here since just HTTP status codes // Able to use user-facing friendly description here since just HTTP status codes
LogManager.shared.log.error("Request failed: \(networkError.errorMessage.code) - \(networkError.errorMessage.title): \(networkError.errorMessage.logConstructor.message)\n\(error.localizedDescription)") LogManager.shared.log
.error("Request failed: \(networkError.errorMessage.code) - \(networkError.errorMessage.title): \(networkError.errorMessage.logConstructor.message)\n\(error.localizedDescription)")
} }
self.errorMessage = networkError.errorMessage self.errorMessage = networkError.errorMessage

Some files were not shown because too many files have changed in this diff Show More