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
@ -13,11 +12,13 @@ import SwiftUI
final class BasicAppSettingsCoordinator: NavigationCoordinatable { 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
BasicAppSettingsView(viewModel: BasicAppSettingsViewModel()) func makeStart() -> some View {
} 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
@ -13,16 +12,19 @@ import SwiftUI
final class ConnectToServerCoodinator: NavigationCoordinatable { 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
ConnectToServerView(viewModel: ConnectToServerViewModel()) func makeStart() -> some View {
} 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
@ -15,21 +14,24 @@ typealias FilterCoordinatorParams = (filters: Binding<LibraryFilters>, enabledFi
final class FilterCoordinator: NavigationCoordinatable { 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 enabledFilterType: [FilterType] var filters: LibraryFilters
var parentId: String = "" var enabledFilterType: [FilterType]
var parentId: String = ""
init(filters: Binding<LibraryFilters>, enabledFilterType: [FilterType], parentId: String) { init(filters: Binding<LibraryFilters>, enabledFilterType: [FilterType], parentId: String) {
_filters = filters _filters = filters
self.enabledFilterType = enabledFilterType self.enabledFilterType = enabledFilterType
self.parentId = parentId self.parentId = parentId
} }
@ViewBuilder func makeStart() -> some View { @ViewBuilder
LibraryFilterView(filters: $filters, enabledFilterType: enabledFilterType, parentId: parentId) func makeStart() -> some View {
} 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
@ -14,36 +13,43 @@ import SwiftUI
final class HomeCoordinator: NavigationCoordinatable { 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())
} }
func makeLibrary(params: LibraryCoordinatorParams) -> LibraryCoordinator { func makeLibrary(params: LibraryCoordinatorParams) -> LibraryCoordinator {
LibraryCoordinator(viewModel: params.viewModel, title: params.title) LibraryCoordinator(viewModel: params.viewModel, title: params.title)
} }
func makeItem(item: BaseItemDto) -> ItemCoordinator { func makeItem(item: BaseItemDto) -> ItemCoordinator {
ItemCoordinator(item: item) ItemCoordinator(item: item)
} }
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
HomeView() func makeStart() -> some View {
} 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
@ -14,37 +13,43 @@ import SwiftUI
final class ItemCoordinator: NavigationCoordinatable { 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
init(item: BaseItemDto) { init(item: BaseItemDto) {
self.itemDto = item self.itemDto = item
} }
func makeLibrary(params: LibraryCoordinatorParams) -> LibraryCoordinator { func makeLibrary(params: LibraryCoordinatorParams) -> LibraryCoordinator {
LibraryCoordinator(viewModel: params.viewModel, title: params.title) LibraryCoordinator(viewModel: params.viewModel, title: params.title)
} }
func makeItem(item: BaseItemDto) -> ItemCoordinator { func makeItem(item: BaseItemDto) -> ItemCoordinator {
ItemCoordinator(item: item) ItemCoordinator(item: item)
} }
func makeItemOverview(item: BaseItemDto) -> NavigationViewCoordinator<ItemOverviewCoordinator> {
NavigationViewCoordinator(ItemOverviewCoordinator(item: itemDto))
}
func makeVideoPlayer(viewModel: VideoPlayerViewModel) -> NavigationViewCoordinator<VideoPlayerCoordinator> { func makeItemOverview(item: BaseItemDto) -> NavigationViewCoordinator<ItemOverviewCoordinator> {
NavigationViewCoordinator(VideoPlayerCoordinator(viewModel: viewModel)) NavigationViewCoordinator(ItemOverviewCoordinator(item: itemDto))
} }
@ViewBuilder func makeStart() -> some View { func makeVideoPlayer(viewModel: VideoPlayerViewModel) -> NavigationViewCoordinator<VideoPlayerCoordinator> {
ItemNavigationView(item: itemDto) NavigationViewCoordinator(VideoPlayerCoordinator(viewModel: viewModel))
} }
@ViewBuilder
func makeStart() -> some View {
ItemNavigationView(item: itemDto)
}
} }

View File

@ -1,33 +1,34 @@
// //
/* // 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
init(item: BaseItemDto) {
self.item = item init(item: BaseItemDto) {
} self.item = item
}
@ViewBuilder func makeStart() -> some View {
#if os(tvOS) @ViewBuilder
EmptyView() func makeStart() -> some View {
#else #if os(tvOS)
ItemOverviewView(item: item) EmptyView()
#endif #else
} ItemOverviewView(item: item)
#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
import JellyfinAPI import JellyfinAPI
@ -16,41 +15,47 @@ typealias LibraryCoordinatorParams = (viewModel: LibraryViewModel, title: String
final class LibraryCoordinator: NavigationCoordinatable { 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
init(viewModel: LibraryViewModel, title: String) { init(viewModel: LibraryViewModel, title: String) {
self.viewModel = viewModel self.viewModel = viewModel
self.title = title self.title = title
} }
@ViewBuilder func makeStart() -> some View { @ViewBuilder
LibraryView(viewModel: self.viewModel, title: title) func makeStart() -> some View {
} LibraryView(viewModel: self.viewModel, title: title)
}
func makeSearch(viewModel: LibrarySearchViewModel) -> SearchCoordinator { func makeSearch(viewModel: LibrarySearchViewModel) -> SearchCoordinator {
SearchCoordinator(viewModel: viewModel) SearchCoordinator(viewModel: viewModel)
} }
func makeFilter(params: FilterCoordinatorParams) -> NavigationViewCoordinator<FilterCoordinator> { func makeFilter(params: FilterCoordinatorParams) -> NavigationViewCoordinator<FilterCoordinator> {
NavigationViewCoordinator(FilterCoordinator(filters: params.filters, NavigationViewCoordinator(FilterCoordinator(filters: params.filters,
enabledFilterType: params.enabledFilterType, enabledFilterType: params.enabledFilterType,
parentId: params.parentId)) parentId: params.parentId))
} }
func makeItem(item: BaseItemDto) -> ItemCoordinator { func makeItem(item: BaseItemDto) -> ItemCoordinator {
ItemCoordinator(item: item) ItemCoordinator(item: item)
} }
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
@ -13,28 +12,31 @@ import SwiftUI
final class LibraryListCoordinator: NavigationCoordinatable { 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
let viewModel: LibraryListViewModel @Route(.push)
var library = makeLibrary
init(viewModel: LibraryListViewModel) { let viewModel: LibraryListViewModel
self.viewModel = viewModel
}
func makeLibrary(params: LibraryCoordinatorParams) -> LibraryCoordinator {
LibraryCoordinator(viewModel: params.viewModel, title: params.title)
}
func makeSearch(viewModel: LibrarySearchViewModel) -> SearchCoordinator { init(viewModel: LibraryListViewModel) {
SearchCoordinator(viewModel: viewModel) self.viewModel = viewModel
} }
@ViewBuilder func makeLibrary(params: LibraryCoordinatorParams) -> LibraryCoordinator {
func makeStart() -> some View { LibraryCoordinator(viewModel: params.viewModel, title: params.title)
LibraryListView(viewModel: self.viewModel) }
}
func makeSearch(viewModel: LibrarySearchViewModel) -> SearchCoordinator {
SearchCoordinator(viewModel: viewModel)
}
@ViewBuilder
func makeStart() -> some View {
LibraryListView(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 JellyfinAPI import JellyfinAPI
@ -13,34 +12,38 @@ import Stinsen
import SwiftUI 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
func makeModalItem(item: BaseItemDto) -> NavigationViewCoordinator<ItemCoordinator> { @Route(.fullScreen)
return NavigationViewCoordinator(ItemCoordinator(item: item)) var videoPlayer = makeVideoPlayer
}
func makeModalItem(item: BaseItemDto) -> NavigationViewCoordinator<ItemCoordinator> {
func makeVideoPlayer(item: BaseItemDto) -> NavigationViewCoordinator<EmptyViewCoordinator> { NavigationViewCoordinator(ItemCoordinator(item: item))
// NavigationViewCoordinator(VideoPlayerCoordinator(item: item)) }
NavigationViewCoordinator(EmptyViewCoordinator())
} func makeVideoPlayer(item: BaseItemDto) -> NavigationViewCoordinator<EmptyViewCoordinator> {
// NavigationViewCoordinator(VideoPlayerCoordinator(item: item))
@ViewBuilder NavigationViewCoordinator(EmptyViewCoordinator())
func makeStart() -> some View { }
LiveTVChannelsView()
} @ViewBuilder
func makeStart() -> some View {
LiveTVChannelsView()
}
} }
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
func makeStart() -> some View { @ViewBuilder
Text("Empty") func makeStart() -> some View {
} Text("Empty")
}
} }

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,19 +12,21 @@ import Stinsen
import SwiftUI import SwiftUI
final class LiveTVProgramsCoordinator: NavigationCoordinatable { final class LiveTVProgramsCoordinator: NavigationCoordinatable {
let stack = NavigationStack(initial: \LiveTVProgramsCoordinator.start)
@Root var start = makeStart let stack = NavigationStack(initial: \LiveTVProgramsCoordinator.start)
@Route(.fullScreen) var videoPlayer = makeVideoPlayer
@Root
func makeVideoPlayer(item: BaseItemDto) -> NavigationViewCoordinator<EmptyViewCoordinator> { var start = makeStart
// NavigationViewCoordinator(VideoPlayerCoordinator(item: item)) @Route(.fullScreen)
NavigationViewCoordinator(EmptyViewCoordinator()) var videoPlayer = makeVideoPlayer
}
func makeVideoPlayer(item: BaseItemDto) -> NavigationViewCoordinator<EmptyViewCoordinator> {
@ViewBuilder // NavigationViewCoordinator(VideoPlayerCoordinator(item: item))
func makeStart() -> some View { NavigationViewCoordinator(EmptyViewCoordinator())
LiveTVProgramsView() }
}
@ViewBuilder
func makeStart() -> some View {
LiveTVProgramsView()
}
} }

View File

@ -1,57 +1,62 @@
// //
/* // 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
func makePrograms() -> NavigationViewCoordinator<LiveTVProgramsCoordinator> { @Route(tabItem: makeHomeTab)
return NavigationViewCoordinator(LiveTVProgramsCoordinator()) var home = makeHome
}
func makePrograms() -> NavigationViewCoordinator<LiveTVProgramsCoordinator> {
@ViewBuilder func makeProgramsTab(isActive: Bool) -> some View { NavigationViewCoordinator(LiveTVProgramsCoordinator())
HStack { }
Image(systemName: "tv")
Text("Programs") @ViewBuilder
} func makeProgramsTab(isActive: Bool) -> some View {
} HStack {
Image(systemName: "tv")
func makeChannels() -> NavigationViewCoordinator<LiveTVChannelsCoordinator> { Text("Programs")
return NavigationViewCoordinator(LiveTVChannelsCoordinator()) }
} }
@ViewBuilder func makeChannelsTab(isActive: Bool) -> some View { func makeChannels() -> NavigationViewCoordinator<LiveTVChannelsCoordinator> {
HStack { NavigationViewCoordinator(LiveTVChannelsCoordinator())
Image(systemName: "square.grid.3x3") }
Text("Channels")
} @ViewBuilder
} func makeChannelsTab(isActive: Bool) -> some View {
HStack {
func makeHome() -> LiveTVHomeView { Image(systemName: "square.grid.3x3")
return LiveTVHomeView() Text("Channels")
} }
}
@ViewBuilder func makeHomeTab(isActive: Bool) -> some View {
HStack { func makeHome() -> LiveTVHomeView {
Image(systemName: "house") LiveTVHomeView()
Text("Home") }
}
} @ViewBuilder
func makeHomeTab(isActive: Bool) -> some View {
HStack {
Image(systemName: "house")
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
@ -16,83 +15,91 @@ import SwiftUI
import WidgetKit 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
private var cancellables = Set<AnyCancellable>() var serverList = makeServerList
init() { private var cancellables = Set<AnyCancellable>()
if SessionManager.main.currentLogin != nil {
self.stack = NavigationStack(initial: \MainCoordinator.mainTab)
} else {
self.stack = NavigationStack(initial: \MainCoordinator.serverList)
}
ImageCache.shared.costLimit = 125 * 1024 * 1024 // 125MB memory init() {
DataLoader.sharedUrlCache.diskCapacity = 1000 * 1024 * 1024 // 1000MB disk if SessionManager.main.currentLogin != nil {
self.stack = NavigationStack(initial: \MainCoordinator.mainTab)
} else {
self.stack = NavigationStack(initial: \MainCoordinator.serverList)
}
WidgetCenter.shared.reloadAllTimelines() ImageCache.shared.costLimit = 125 * 1024 * 1024 // 125MB memory
UIScrollView.appearance().keyboardDismissMode = .onDrag DataLoader.sharedUrlCache.diskCapacity = 1000 * 1024 * 1024 // 1000MB disk
// Back bar button item setup WidgetCenter.shared.reloadAllTimelines()
let backButtonBackgroundImage = UIImage(systemName: "chevron.backward.circle.fill") UIScrollView.appearance().keyboardDismissMode = .onDrag
let barAppearance = UINavigationBar.appearance()
barAppearance.backIndicatorImage = backButtonBackgroundImage
barAppearance.backIndicatorTransitionMaskImage = backButtonBackgroundImage
barAppearance.tintColor = UIColor(Color.jellyfinPurple)
// Notification setup for state // Back bar button item setup
let nc = SwiftfinNotificationCenter.main let backButtonBackgroundImage = UIImage(systemName: "chevron.backward.circle.fill")
nc.addObserver(self, selector: #selector(didLogIn), name: SwiftfinNotificationCenter.Keys.didSignIn, object: nil) let barAppearance = UINavigationBar.appearance()
nc.addObserver(self, selector: #selector(didLogOut), name: SwiftfinNotificationCenter.Keys.didSignOut, object: nil) barAppearance.backIndicatorImage = backButtonBackgroundImage
nc.addObserver(self, selector: #selector(processDeepLink), name: SwiftfinNotificationCenter.Keys.processDeepLink, object: nil) barAppearance.backIndicatorTransitionMaskImage = backButtonBackgroundImage
nc.addObserver(self, selector: #selector(didChangeServerCurrentURI), name: SwiftfinNotificationCenter.Keys.didChangeServerCurrentURI, object: nil) barAppearance.tintColor = UIColor(Color.jellyfinPurple)
Defaults.publisher(.appAppearance)
.sink { _ in
JellyfinPlayerApp.setupAppearance()
}
.store(in: &cancellables)
}
@objc func didLogIn() { // Notification setup for state
LogManager.shared.log.info("Received `didSignIn` from SwiftfinNotificationCenter.") let nc = SwiftfinNotificationCenter.main
root(\.mainTab) 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(processDeepLink), name: SwiftfinNotificationCenter.Keys.processDeepLink, object: nil)
nc.addObserver(self, selector: #selector(didChangeServerCurrentURI),
name: SwiftfinNotificationCenter.Keys.didChangeServerCurrentURI, object: nil)
@objc func didLogOut() { Defaults.publisher(.appAppearance)
LogManager.shared.log.info("Received `didSignOut` from SwiftfinNotificationCenter.") .sink { _ in
root(\.serverList) JellyfinPlayerApp.setupAppearance()
} }
.store(in: &cancellables)
}
@objc func processDeepLink(_ notification: Notification) { @objc
guard let deepLink = notification.object as? DeepLink else { return } func didLogIn() {
if let coordinator = hasRoot(\.mainTab) { LogManager.shared.log.info("Received `didSignIn` from SwiftfinNotificationCenter.")
switch deepLink { root(\.mainTab)
case let .item(item): }
coordinator.focusFirst(\.home)
.child
.popToRoot()
.route(to: \.item, item)
}
}
}
@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 didLogOut() {
guard SessionManager.main.currentLogin != nil else { return } LogManager.shared.log.info("Received `didSignOut` from SwiftfinNotificationCenter.")
if newCurrentServerState.id == SessionManager.main.currentLogin.server.id { root(\.serverList)
SessionManager.main.loginUser(server: newCurrentServerState, user: SessionManager.main.currentLogin.user) }
}
}
func makeMainTab() -> MainTabCoordinator { @objc
MainTabCoordinator() func processDeepLink(_ notification: Notification) {
} guard let deepLink = notification.object as? DeepLink else { return }
if let coordinator = hasRoot(\.mainTab) {
switch deepLink {
case let .item(item):
coordinator.focusFirst(\.home)
.child
.popToRoot()
.route(to: \.item, item)
}
}
}
func makeServerList() -> NavigationViewCoordinator<ServerListCoordinator> { @objc
NavigationViewCoordinator(ServerListCoordinator()) 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 }
if newCurrentServerState.id == SessionManager.main.currentLogin.server.id {
SessionManager.main.loginUser(server: newCurrentServerState, user: SessionManager.main.currentLogin.user)
}
}
func makeMainTab() -> MainTabCoordinator {
MainTabCoordinator()
}
func makeServerList() -> NavigationViewCoordinator<ServerListCoordinator> {
NavigationViewCoordinator(ServerListCoordinator())
}
} }

View File

@ -1,50 +1,54 @@
// //
/* // 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
Image(systemName: "house") func makeHomeTab(isActive: Bool) -> some View {
L10n.home.text Image(systemName: "house")
} 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
Image(systemName: "folder") func makeAllMediaTab(isActive: Bool) -> some View {
L10n.allMedia.text Image(systemName: "folder")
} L10n.allMedia.text
}
@ViewBuilder func customize(_ view: AnyView) -> some View { @ViewBuilder
view.onAppear { func customize(_ view: AnyView) -> some View {
AppURLHandler.shared.appURLState = .allowed view.onAppear {
// TODO: todo AppURLHandler.shared.appURLState = .allowed
DispatchQueue.main.asyncAfter(deadline: .now() + 0.25) { // TODO: todo
AppURLHandler.shared.processLaunchedURLIfNeeded() DispatchQueue.main.asyncAfter(deadline: .now() + 0.25) {
} AppURLHandler.shared.processLaunchedURLIfNeeded()
} }
} }
}
} }

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
@ -13,55 +12,60 @@ import Stinsen
import SwiftUI 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 {
view.background { view.background {
Color.black Color.black
.ignoresSafeArea() .ignoresSafeArea()
} }
} }
init() { init() {
if SessionManager.main.currentLogin != nil { if SessionManager.main.currentLogin != nil {
self.stack = NavigationStack(initial: \MainCoordinator.mainTab) self.stack = NavigationStack(initial: \MainCoordinator.mainTab)
} else { } else {
self.stack = NavigationStack(initial: \MainCoordinator.serverList) self.stack = NavigationStack(initial: \MainCoordinator.serverList)
} }
ImageCache.shared.costLimit = 125 * 1024 * 1024 // 125MB memory ImageCache.shared.costLimit = 125 * 1024 * 1024 // 125MB memory
DataLoader.sharedUrlCache.diskCapacity = 1000 * 1024 * 1024 // 1000MB disk DataLoader.sharedUrlCache.diskCapacity = 1000 * 1024 * 1024 // 1000MB disk
// Notification setup for state // Notification setup for state
let nc = SwiftfinNotificationCenter.main let nc = SwiftfinNotificationCenter.main
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)
} }
@objc func didLogIn() { @objc
LogManager.shared.log.info("Received `didSignIn` from NSNotificationCenter.") func didLogIn() {
root(\.mainTab) LogManager.shared.log.info("Received `didSignIn` from NSNotificationCenter.")
} root(\.mainTab)
}
@objc func didLogOut() { @objc
LogManager.shared.log.info("Received `didSignOut` from NSNotificationCenter.") func didLogOut() {
root(\.serverList) LogManager.shared.log.info("Received `didSignOut` from NSNotificationCenter.")
} root(\.serverList)
}
func makeMainTab() -> MainTabCoordinator { func makeMainTab() -> MainTabCoordinator {
MainTabCoordinator() MainTabCoordinator()
} }
func makeServerList() -> NavigationViewCoordinator<ServerListCoordinator> { func makeServerList() -> NavigationViewCoordinator<ServerListCoordinator> {
NavigationViewCoordinator(ServerListCoordinator()) NavigationViewCoordinator(ServerListCoordinator())
} }
func makeLiveTV() -> LiveTVTabCoordinator { func makeLiveTV() -> LiveTVTabCoordinator {
LiveTVTabCoordinator() LiveTVTabCoordinator()
} }
} }

View File

@ -1,80 +1,89 @@
// //
/* // 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.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
HStack { func makeHomeTab(isActive: Bool) -> some View {
Image(systemName: "house") HStack {
L10n.home.text Image(systemName: "house")
} L10n.home.text
} }
}
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
HStack { func makeTvTab(isActive: Bool) -> some View {
Image(systemName: "tv") HStack {
Text("TV Shows") Image(systemName: "tv")
} Text("TV Shows")
} }
}
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
HStack { func makeMoviesTab(isActive: Bool) -> some View {
Image(systemName: "film") HStack {
Text("Movies") Image(systemName: "film")
} Text("Movies")
} }
}
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
HStack { func makeOtherTab(isActive: Bool) -> some View {
Image(systemName: "folder") HStack {
Text("Other") Image(systemName: "folder")
} Text("Other")
} }
}
func makeSettings() -> NavigationViewCoordinator<SettingsCoordinator> { func makeSettings() -> NavigationViewCoordinator<SettingsCoordinator> {
return NavigationViewCoordinator(SettingsCoordinator()) NavigationViewCoordinator(SettingsCoordinator())
} }
@ViewBuilder func makeSettingsTab(isActive: Bool) -> some View { @ViewBuilder
Image(systemName: "gearshape.fill") func makeSettingsTab(isActive: Bool) -> some View {
} 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
@ -14,24 +13,27 @@ import SwiftUI
final class MovieLibrariesCoordinator: NavigationCoordinatable { 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
init(viewModel: MovieLibrariesViewModel, title: String) { init(viewModel: MovieLibrariesViewModel, title: String) {
self.viewModel = viewModel self.viewModel = viewModel
self.title = title self.title = title
} }
@ViewBuilder func makeStart() -> some View { @ViewBuilder
MovieLibrariesView(viewModel: self.viewModel, title: title) func makeStart() -> some View {
} MovieLibrariesView(viewModel: self.viewModel, title: title)
}
func makeLibrary(library: BaseItemDto) -> LibraryCoordinator { func makeLibrary(library: BaseItemDto) -> LibraryCoordinator {
LibraryCoordinator(viewModel: LibraryViewModel(parentID: library.id), title: library.title) LibraryCoordinator(viewModel: LibraryViewModel(parentID: library.id), title: library.title)
} }
} }

View File

@ -1,35 +1,37 @@
// //
/* // 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
init(viewModel: LibrarySearchViewModel) { init(viewModel: LibrarySearchViewModel) {
self.viewModel = viewModel self.viewModel = viewModel
} }
func makeItem(item: BaseItemDto) -> ItemCoordinator { func makeItem(item: BaseItemDto) -> ItemCoordinator {
ItemCoordinator(item: item) ItemCoordinator(item: item)
} }
@ViewBuilder func makeStart() -> some View { @ViewBuilder
LibrarySearchView(viewModel: self.viewModel) func makeStart() -> some View {
} 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
@ -13,17 +12,19 @@ import SwiftUI
final class ServerDetailCoordinator: NavigationCoordinatable { 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
init(viewModel: ServerDetailViewModel) { init(viewModel: ServerDetailViewModel) {
self.viewModel = viewModel self.viewModel = viewModel
} }
@ViewBuilder func makeStart() -> some View { @ViewBuilder
ServerDetailView(viewModel: viewModel) func makeStart() -> some View {
} 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
@ -13,26 +12,31 @@ import SwiftUI
final class ServerListCoordinator: NavigationCoordinatable { 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()
} }
func makeUserList(server: SwiftfinStore.State.Server) -> UserListCoordinator { func makeUserList(server: SwiftfinStore.State.Server) -> UserListCoordinator {
UserListCoordinator(viewModel: .init(server: server)) UserListCoordinator(viewModel: .init(server: server))
} }
func makeBasicAppSettings() -> NavigationViewCoordinator<BasicAppSettingsCoordinator> { func makeBasicAppSettings() -> NavigationViewCoordinator<BasicAppSettingsCoordinator> {
NavigationViewCoordinator(BasicAppSettingsCoordinator()) NavigationViewCoordinator(BasicAppSettingsCoordinator())
} }
@ViewBuilder func makeStart() -> some View { @ViewBuilder
ServerListView(viewModel: ServerListViewModel()) func makeStart() -> some View {
} 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
@ -13,28 +12,36 @@ import SwiftUI
final class SettingsCoordinator: NavigationCoordinatable { 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
let viewModel = ServerDetailViewModel(server: SessionManager.main.currentLogin.server) func makeServerDetail() -> some View {
ServerDetailView(viewModel: viewModel) let viewModel = ServerDetailViewModel(server: SessionManager.main.currentLogin.server)
} ServerDetailView(viewModel: viewModel)
}
@ViewBuilder func makeOverlaySettings() -> some View {
OverlaySettingsView()
}
@ViewBuilder func makeExperimentalSettings() -> some View {
ExperimentalSettingsView()
}
@ViewBuilder func makeStart() -> some View { @ViewBuilder
let viewModel = SettingsViewModel(server: SessionManager.main.currentLogin.server, user: SessionManager.main.currentLogin.user) func makeOverlaySettings() -> some View {
SettingsView(viewModel: viewModel) OverlaySettingsView()
} }
@ViewBuilder
func makeExperimentalSettings() -> some View {
ExperimentalSettingsView()
}
@ViewBuilder
func makeStart() -> some View {
let viewModel = SettingsViewModel(server: SessionManager.main.currentLogin.server, user: SessionManager.main.currentLogin.user)
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
@ -14,24 +13,27 @@ import SwiftUI
final class TVLibrariesCoordinator: NavigationCoordinatable { 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
init(viewModel: TVLibrariesViewModel, title: String) { init(viewModel: TVLibrariesViewModel, title: String) {
self.viewModel = viewModel self.viewModel = viewModel
self.title = title self.title = title
} }
@ViewBuilder func makeStart() -> some View { @ViewBuilder
TVLibrariesView(viewModel: self.viewModel, title: title) func makeStart() -> some View {
} TVLibrariesView(viewModel: self.viewModel, title: title)
}
func makeLibrary(library: BaseItemDto) -> LibraryCoordinator { func makeLibrary(library: BaseItemDto) -> LibraryCoordinator {
LibraryCoordinator(viewModel: LibraryViewModel(parentID: library.id), title: library.title) LibraryCoordinator(viewModel: LibraryViewModel(parentID: library.id), title: library.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
@ -13,27 +12,31 @@ import SwiftUI
final class UserListCoordinator: NavigationCoordinatable { 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
init(viewModel: UserListViewModel) { init(viewModel: UserListViewModel) {
self.viewModel = viewModel self.viewModel = viewModel
} }
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
UserListView(viewModel: viewModel) func makeStart() -> some View {
} 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
@ -13,17 +12,19 @@ import SwiftUI
final class UserSignInCoordinator: NavigationCoordinatable { 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
init(viewModel: UserSignInViewModel) { init(viewModel: UserSignInViewModel) {
self.viewModel = viewModel self.viewModel = viewModel
} }
@ViewBuilder func makeStart() -> some View { @ViewBuilder
UserSignInView(viewModel: viewModel) func makeStart() -> some View {
} 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
@ -15,24 +14,26 @@ import SwiftUI
final class VideoPlayerCoordinator: NavigationCoordinatable { 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
init(viewModel: VideoPlayerViewModel) { init(viewModel: VideoPlayerViewModel) {
self.viewModel = viewModel self.viewModel = viewModel
} }
@ViewBuilder func makeStart() -> some View { @ViewBuilder
PreferenceUIHostingControllerView { func makeStart() -> some View {
VLCPlayerView(viewModel: self.viewModel) PreferenceUIHostingControllerView {
.navigationBarHidden(true) VLCPlayerView(viewModel: self.viewModel)
.statusBar(hidden: true) .navigationBarHidden(true)
.ignoresSafeArea() .statusBar(hidden: true)
.prefersHomeIndicatorAutoHidden(true) .ignoresSafeArea()
.supportedOrientations(UIDevice.current.userInterfaceIdiom == .pad ? .all : .landscape) .prefersHomeIndicatorAutoHidden(true)
}.ignoresSafeArea() .supportedOrientations(UIDevice.current.userInterfaceIdiom == .pad ? .all : .landscape)
} }.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 Defaults import Defaults
import Foundation import Foundation
@ -15,20 +14,21 @@ import SwiftUI
final class VideoPlayerCoordinator: NavigationCoordinatable { 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
init(viewModel: VideoPlayerViewModel) { init(viewModel: VideoPlayerViewModel) {
self.viewModel = viewModel self.viewModel = viewModel
} }
@ViewBuilder func makeStart() -> some View { @ViewBuilder
VLCPlayerView(viewModel: viewModel) func makeStart() -> some View {
.navigationBarHidden(true) VLCPlayerView(viewModel: viewModel)
.ignoresSafeArea() .navigationBarHidden(true)
} .ignoresSafeArea()
}
} }

View File

@ -1,35 +1,34 @@
// //
/* // 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
struct ErrorMessage: Identifiable { struct ErrorMessage: Identifiable {
let code: Int let code: Int
let title: String let title: String
let displayMessage: String let displayMessage: String
let logConstructor: LogConstructor let logConstructor: LogConstructor
// Chosen value such that if an error has this code, don't show the code to the UI // Chosen value such that if an error has this code, don't show the code to the UI
// This was chosen because of its unlikelyhood to ever be used // This was chosen because of its unlikelyhood to ever be used
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
init(code: Int, title: String, displayMessage: String?, logConstructor: LogConstructor) { init(code: Int, title: String, displayMessage: String?, logConstructor: LogConstructor) {
self.code = code self.code = code
self.title = title self.title = title
self.displayMessage = displayMessage ?? logConstructor.message self.displayMessage = displayMessage ?? logConstructor.message
self.logConstructor = logConstructor self.logConstructor = logConstructor
} }
} }

View File

@ -1,20 +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 Foundation
import JellyfinAPI import JellyfinAPI
struct LogConstructor { struct LogConstructor {
var message: String var message: String
let tag: String let tag: String
let level: LogLevel let level: LogLevel
let function: String let function: String
let file: String let file: String
let line: UInt let line: UInt
} }

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,139 +12,146 @@ 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 {
/// For the case that the ErrorResponse object has a code of -1 /// For the case that the ErrorResponse object has a code of -1
case URLError(response: ErrorResponse, displayMessage: String?, logConstructor: LogConstructor) case URLError(response: ErrorResponse, displayMessage: String?, logConstructor: LogConstructor)
/// For the case that the ErrorRespones object has a code of -2 /// For the case that the ErrorRespones object has a code of -2
case HTTPURLError(response: ErrorResponse, displayMessage: String?, logConstructor: LogConstructor) case HTTPURLError(response: ErrorResponse, displayMessage: String?, logConstructor: LogConstructor)
/// For the case that the ErrorResponse object has a positive code /// For the case that the ErrorResponse object has a positive code
case JellyfinError(response: ErrorResponse, displayMessage: String?, logConstructor: LogConstructor) case JellyfinError(response: ErrorResponse, displayMessage: String?, logConstructor: LogConstructor)
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)
} }
} }
func logMessage() { func logMessage() {
let logConstructor = errorMessage.logConstructor let logConstructor = errorMessage.logConstructor
let logFunction: (@autoclosure () -> String, String, String, String, UInt) -> Void let logFunction: (@autoclosure () -> String, String, String, String, UInt) -> Void
switch logConstructor.level { switch logConstructor.level {
case .trace: case .trace:
logFunction = LogManager.shared.log.trace logFunction = LogManager.shared.log.trace
case .debug: case .debug:
logFunction = LogManager.shared.log.debug logFunction = LogManager.shared.log.debug
case .information: case .information:
logFunction = LogManager.shared.log.info logFunction = LogManager.shared.log.info
case .warning: case .warning:
logFunction = LogManager.shared.log.warning logFunction = LogManager.shared.log.warning
case .error: case .error:
logFunction = LogManager.shared.log.error logFunction = LogManager.shared.log.error
case .critical: case .critical:
logFunction = LogManager.shared.log.critical logFunction = LogManager.shared.log.critical
case ._none: case ._none:
logFunction = LogManager.shared.log.debug logFunction = LogManager.shared.log.debug
} }
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
switch err._code { switch err._code {
case -1001: case -1001:
logMessage = "Network timed out." logMessage = "Network timed out."
logConstructor.message = logMessage logConstructor.message = logMessage
errorMessage = ErrorMessage(code: err._code, errorMessage = ErrorMessage(code: err._code,
title: "Timed Out", title: "Timed Out",
displayMessage: displayMessage, displayMessage: displayMessage,
logConstructor: logConstructor) logConstructor: logConstructor)
case -1004: case -1004:
logMessage = "Cannot connect to host." logMessage = "Cannot connect to host."
logConstructor.message = logMessage logConstructor.message = logMessage
errorMessage = ErrorMessage(code: err._code, errorMessage = ErrorMessage(code: err._code,
title: L10n.error, title: L10n.error,
displayMessage: displayMessage, displayMessage: displayMessage,
logConstructor: logConstructor) logConstructor: logConstructor)
default: default:
logConstructor.message = logMessage logConstructor.message = logMessage
errorMessage = ErrorMessage(code: err._code, errorMessage = ErrorMessage(code: err._code,
title: L10n.error, title: L10n.error,
displayMessage: displayMessage, displayMessage: displayMessage,
logConstructor: logConstructor) logConstructor: logConstructor)
} }
} }
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"
var logConstructor = logConstructor var logConstructor = logConstructor
// Not implemented as has not run into one of these errors as time of writing // Not implemented as has not run into one of these errors as time of writing
switch response { switch response {
case .error: case .error:
logConstructor.message = logMessage logConstructor.message = logMessage
errorMessage = ErrorMessage(code: 0, errorMessage = ErrorMessage(code: 0,
title: L10n.error, title: L10n.error,
displayMessage: displayMessage, displayMessage: displayMessage,
logConstructor: logConstructor) logConstructor: logConstructor)
} }
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 {
case 401: case 401:
logMessage = "User is unauthorized." logMessage = "User is unauthorized."
logConstructor.message = logMessage logConstructor.message = logMessage
errorMessage = ErrorMessage(code: code, errorMessage = ErrorMessage(code: code,
title: "Unauthorized", title: "Unauthorized",
displayMessage: displayMessage, displayMessage: displayMessage,
logConstructor: logConstructor) logConstructor: logConstructor)
default: default:
logConstructor.message = logMessage logConstructor.message = logMessage
errorMessage = ErrorMessage(code: code, errorMessage = ErrorMessage(code: code,
title: L10n.error, title: L10n.error,
displayMessage: displayMessage, displayMessage: displayMessage,
logConstructor: logConstructor) logConstructor: logConstructor)
} }
} }
return errorMessage return errorMessage
} }
} }

View File

@ -1,166 +1,151 @@
/* //
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()
let numY = (sizeFlag / 9) + 1 let numY = (sizeFlag / 9) + 1
let numX = (sizeFlag % 9) + 1 let numX = (sizeFlag % 9) + 1
let quantisedMaximumValue = String(blurHash[1]).decode83() let quantisedMaximumValue = String(blurHash[1]).decode83()
let maximumValue = Float(quantisedMaximumValue + 1) / 166 let maximumValue = Float(quantisedMaximumValue + 1) / 166
guard blurHash.count == 4 + 2 * numX * numY else { return nil } guard blurHash.count == 4 + 2 * numX * numY else { return nil }
let colours: [(Float, Float, Float)] = (0 ..< numX * numY).map { i in let colours: [(Float, Float, Float)] = (0 ..< numX * numY).map { i in
if i == 0 { if i == 0 {
let value = String(blurHash[2 ..< 6]).decode83() let value = String(blurHash[2 ..< 6]).decode83()
return decodeDC(value) return decodeDC(value)
} else { } else {
let value = String(blurHash[4 + i * 2 ..< 4 + i * 2 + 2]).decode83() let value = String(blurHash[4 + i * 2 ..< 4 + i * 2 + 2]).decode83()
return decodeAC(value, maximumValue: maximumValue * punch) return decodeAC(value, maximumValue: maximumValue * punch)
} }
} }
let width = Int(size.width) let width = Int(size.width)
let height = Int(size.height) let height = Int(size.height)
let bytesPerRow = width * 3 let bytesPerRow = width * 3
guard let data = CFDataCreateMutable(kCFAllocatorDefault, bytesPerRow * height) else { return nil } guard let data = CFDataCreateMutable(kCFAllocatorDefault, bytesPerRow * height) else { return nil }
CFDataSetLength(data, bytesPerRow * height) CFDataSetLength(data, bytesPerRow * height)
guard let pixels = CFDataGetMutableBytePtr(data) else { return nil } guard let pixels = CFDataGetMutableBytePtr(data) else { return nil }
for y in 0 ..< height { for y in 0 ..< height {
for x in 0 ..< width { for x in 0 ..< width {
var r: Float = 0 var r: Float = 0
var g: Float = 0 var g: Float = 0
var b: Float = 0 var b: Float = 0
for j in 0 ..< numY { for j in 0 ..< numY {
for i in 0 ..< numX { for i in 0 ..< numX {
let basis = cos(Float.pi * Float(x) * Float(i) / Float(width)) * cos(Float.pi * Float(y) * Float(j) / Float(height)) let basis = cos(Float.pi * Float(x) * Float(i) / Float(width)) * cos(Float.pi * Float(y) * Float(j) / Float(height))
let colour = colours[i + j * numX] let colour = colours[i + j * numX]
r += colour.0 * basis r += colour.0 * basis
g += colour.1 * basis g += colour.1 * basis
b += colour.2 * basis b += colour.2 * basis
} }
} }
let intR = UInt8(linearTosRGB(r)) let intR = UInt8(linearTosRGB(r))
let intG = UInt8(linearTosRGB(g)) let intG = UInt8(linearTosRGB(g))
let intB = UInt8(linearTosRGB(b)) let intB = UInt8(linearTosRGB(b))
pixels[3 * x + 0 + y * bytesPerRow] = intR pixels[3 * x + 0 + y * bytesPerRow] = intR
pixels[3 * x + 1 + y * bytesPerRow] = intG pixels[3 * x + 1 + y * bytesPerRow] = intG
pixels[3 * x + 2 + y * bytesPerRow] = intB pixels[3 * x + 2 + y * bytesPerRow] = intB
} }
} }
let bitmapInfo = CGBitmapInfo(rawValue: CGImageAlphaInfo.none.rawValue) let bitmapInfo = CGBitmapInfo(rawValue: CGImageAlphaInfo.none.rawValue)
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)
} }
} }
private func decodeDC(_ value: Int) -> (Float, Float, Float) { private func decodeDC(_ value: Int) -> (Float, Float, Float) {
let intR = value >> 16 let intR = value >> 16
let intG = (value >> 8) & 255 let intG = (value >> 8) & 255
let intB = value & 255 let intB = value & 255
return (sRGBToLinear(intR), sRGBToLinear(intG), sRGBToLinear(intB)) return (sRGBToLinear(intR), sRGBToLinear(intG), sRGBToLinear(intB))
} }
private func decodeAC(_ value: Int, maximumValue: Float) -> (Float, Float, Float) { private func decodeAC(_ value: Int, maximumValue: Float) -> (Float, Float, Float) {
let quantR = value / (19 * 19) let quantR = value / (19 * 19)
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 {
let v = max(0, min(1, value)) let v = max(0, min(1, value))
if v <= 0.0031308 { return Int(v * 12.92 * 255 + 0.5) } else { return Int((1.055 * pow(v, 1 / 2.4) - 0.055) * 255 + 0.5) } if v <= 0.0031308 { return Int(v * 12.92 * 255 + 0.5) } else { return Int((1.055 * pow(v, 1 / 2.4) - 0.055) * 255 + 0.5) }
} }
private func sRGBToLinear<Type: BinaryInteger>(_ value: Type) -> Float { private func sRGBToLinear<Type: BinaryInteger>(_ value: Type) -> Float {
let v = Float(Int64(value)) / 255 let v = Float(Int64(value)) / 255
if v <= 0.04045 { return v / 12.92 } else { return pow((v + 0.055) / 1.055, 2.4) } if v <= 0.04045 { return v / 12.92 } else { return pow((v + 0.055) / 1.055, 2.4) }
} }
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] = {
var dict: [String: Int] = [:] var dict: [String: Int] = [:]
for (index, character) in encodeCharacters.enumerated() { for (index, character) in encodeCharacters.enumerated() {
dict[character] = index dict[character] = index
} }
return dict return dict
}() }()
extension String { extension String {
func decode83() -> Int { func decode83() -> Int {
var value: Int = 0 var value: Int = 0
for character in self { for character in self {
if let digit = decodeCharacters[String(character)] { if let digit = decodeCharacters[String(character)] {
value = value * 83 + digit value = value * 83 + digit
} }
} }
return value return value
} }
} }
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 {
let start = index(startIndex, offsetBy: bounds.lowerBound) let start = index(startIndex, offsetBy: bounds.lowerBound)
let end = index(startIndex, offsetBy: bounds.upperBound) let end = index(startIndex, offsetBy: bounds.upperBound)
return self[start...end] return self[start ... end]
} }
subscript (bounds: CountableRange<Int>) -> Substring { subscript(bounds: CountableRange<Int>) -> Substring {
let start = index(startIndex, offsetBy: bounds.lowerBound) let start = index(startIndex, offsetBy: bounds.lowerBound)
let end = index(startIndex, offsetBy: bounds.upperBound) let end = index(startIndex, offsetBy: bounds.upperBound)
return self[start..<end] return self[start ..< end]
} }
} }

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,22 +1,23 @@
/* 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
public extension Collection { public extension Collection {
/// SwifterSwift: Safe protects the array from out of bounds by use of optional. /// SwifterSwift: Safe protects the array from out of bounds by use of optional.
/// ///
/// let arr = [1, 2, 3, 4, 5] /// let arr = [1, 2, 3, 4, 5]
/// arr[safe: 1] -> 2 /// arr[safe: 1] -> 2
/// arr[safe: 10] -> nil /// arr[safe: 10] -> nil
/// ///
/// - 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,31 +1,30 @@
// //
/* // 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
} }
extension UIColor { extension UIColor {
static let jellyfinPurple = UIColor(red: 172 / 255, green: 92 / 255, blue: 195 / 255, alpha: 1) static let jellyfinPurple = UIColor(red: 172 / 255, green: 92 / 255, blue: 195 / 255, alpha: 1)
} }

View File

@ -1,23 +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 Foundation import Foundation
extension Double { extension Double {
func subtract(_ other: Double, floor: Double) -> Double { func subtract(_ other: Double, floor: Double) -> Double {
var v = self - other var v = self - other
if v < floor { if v < floor {
v += abs(floor - v) v += abs(floor - v)
} }
return v return v
} }
} }

View File

@ -1,21 +1,22 @@
/* 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
extension Image { extension Image {
func centerCropped() -> some View { func centerCropped() -> some View {
GeometryReader { geo in GeometryReader { geo in
self self
.resizable() .resizable()
.scaledToFill() .scaledToFill()
.frame(width: geo.size.width, height: geo.size.height) .frame(width: geo.size.width, height: geo.size.height)
.clipped() .clipped()
} }
} }
} }

View File

@ -1,65 +1,65 @@
// //
/* // 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 {
switch self.itemType {
case .episode:
return getSeriesPrimaryImage(maxWidth: maxWidth)
default:
return self.getPrimaryImage(maxWidth: maxWidth)
}
}
public var title: String { public func imageURLContsructor(maxWidth: Int) -> URL {
switch self.itemType { switch self.itemType {
case .episode: case .episode:
return self.seriesName ?? self.name ?? "" return getSeriesPrimaryImage(maxWidth: maxWidth)
default: default:
return self.name ?? "" return self.getPrimaryImage(maxWidth: maxWidth)
} }
} }
public var subtitle: String? { public var title: String {
switch self.itemType { switch self.itemType {
case .episode: case .episode:
return getEpisodeLocator() return self.seriesName ?? self.name ?? ""
default: default:
return nil return self.name ?? ""
} }
} }
public var blurHash: String { public var subtitle: String? {
return self.getPrimaryImageBlurHash() switch self.itemType {
} case .episode:
return getEpisodeLocator()
default:
return nil
}
}
public var failureInitials: String { public var blurHash: String {
guard let name = self.name else { return "" } self.getPrimaryImageBlurHash()
let initials = name.split(separator: " ").compactMap({ String($0).first }) }
return String(initials)
} public var failureInitials: String {
guard let name = self.name else { return "" }
public var showTitle: Bool { let initials = name.split(separator: " ").compactMap { String($0).first }
switch self.itemType { return String(initials)
case .episode, .series, .movie: }
return Defaults[.showPosterLabels]
default: public var showTitle: Bool {
return true switch self.itemType {
} case .episode, .series, .movie:
} return Defaults[.showPosterLabels]
default:
return 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 Combine import Combine
import Defaults import Defaults
@ -13,97 +12,98 @@ import JellyfinAPI
import UIKit import UIKit
extension BaseItemDto { 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,
maxStreamingBitrate: 60000000,
startTimeTicks: self.userData?.playbackPositionTicks ?? 0,
deviceProfile: profile,
autoOpenLiveStream: true)
return MediaInfoAPI.getPostedPlaybackInfo(itemId: self.id!,
userId: SessionManager.main.currentLogin.user.id,
maxStreamingBitrate: 60000000,
startTimeTicks: self.userData?.playbackPositionTicks ?? 0,
autoOpenLiveStream: true,
playbackInfoDto: playbackInfo)
.map({ response -> VideoPlayerViewModel in
let mediaSource = response.mediaSources!.first!
let audioStreams = mediaSource.mediaStreams?.filter({ $0.type == .audio }) ?? []
let subtitleStreams = mediaSource.mediaStreams?.filter({ $0.type == .subtitle }) ?? []
let defaultAudioStream = audioStreams.first(where: { $0.index! == mediaSource.defaultAudioStreamIndex! })
let defaultSubtitleStream = subtitleStreams.first(where: { $0.index! == mediaSource.defaultSubtitleStreamIndex ?? -1 })
// MARK: Stream
var streamURL = URLComponents(string: SessionManager.main.currentLogin.server.currentURI)!
let streamType: ServerStreamType
if let transcodeURL = mediaSource.transcodingUrl {
streamType = .transcode
streamURL.path = transcodeURL
} else {
streamType = .direct
streamURL.path = "/Videos/\(self.id!)/stream"
}
streamURL.addQueryItem(name: "Static", value: "true") let playbackInfo = PlaybackInfoDto(userId: SessionManager.main.currentLogin.user.id,
streamURL.addQueryItem(name: "MediaSourceId", value: self.id!) maxStreamingBitrate: 60_000_000,
streamURL.addQueryItem(name: "Tag", value: self.etag) startTimeTicks: self.userData?.playbackPositionTicks ?? 0,
streamURL.addQueryItem(name: "MinSegments", value: "6") deviceProfile: profile,
autoOpenLiveStream: true)
// MARK: VidoPlayerViewModel Creation
return MediaInfoAPI.getPostedPlaybackInfo(itemId: self.id!,
var subtitle: String? = nil userId: SessionManager.main.currentLogin.user.id,
maxStreamingBitrate: 60_000_000,
// MARK: Attach media content to self startTimeTicks: self.userData?.playbackPositionTicks ?? 0,
autoOpenLiveStream: true,
var modifiedSelfItem = self playbackInfoDto: playbackInfo)
modifiedSelfItem.mediaStreams = mediaSource.mediaStreams .map { response -> VideoPlayerViewModel in
let mediaSource = response.mediaSources!.first!
// TODO: other forms of media subtitle
if self.itemType == .episode { let audioStreams = mediaSource.mediaStreams?.filter { $0.type == .audio } ?? []
if let seriesName = self.seriesName, let episodeLocator = self.getEpisodeLocator() { let subtitleStreams = mediaSource.mediaStreams?.filter { $0.type == .subtitle } ?? []
subtitle = "\(seriesName) - \(episodeLocator)"
} let defaultAudioStream = audioStreams.first(where: { $0.index! == mediaSource.defaultAudioStreamIndex! })
}
let defaultSubtitleStream = subtitleStreams.first(where: { $0.index! == mediaSource.defaultSubtitleStreamIndex ?? -1 })
let subtitlesEnabled = defaultSubtitleStream != nil
// MARK: Stream
let shouldShowAutoPlay = Defaults[.shouldShowAutoPlay] && itemType == .episode
let autoplayEnabled = Defaults[.autoplayEnabled] && shouldShowAutoPlay var streamURL = URLComponents(string: SessionManager.main.currentLogin.server.currentURI)!
let overlayType = Defaults[.overlayType] let streamType: ServerStreamType
let shouldShowPlayPreviousItem = Defaults[.shouldShowPlayPreviousItem] && itemType == .episode if let transcodeURL = mediaSource.transcodingUrl {
let shouldShowPlayNextItem = Defaults[.shouldShowPlayNextItem] && itemType == .episode streamType = .transcode
streamURL.path = transcodeURL
let videoPlayerViewModel = VideoPlayerViewModel(item: modifiedSelfItem, } else {
title: modifiedSelfItem.name ?? "", streamType = .direct
subtitle: subtitle, streamURL.path = "/Videos/\(self.id!)/stream"
streamURL: streamURL.url!, }
streamType: streamType,
response: response, streamURL.addQueryItem(name: "Static", value: "true")
audioStreams: audioStreams, streamURL.addQueryItem(name: "MediaSourceId", value: self.id!)
subtitleStreams: subtitleStreams, streamURL.addQueryItem(name: "Tag", value: self.etag)
selectedAudioStreamIndex: defaultAudioStream?.index ?? -1, streamURL.addQueryItem(name: "MinSegments", value: "6")
selectedSubtitleStreamIndex: defaultSubtitleStream?.index ?? -1,
subtitlesEnabled: subtitlesEnabled, // MARK: VidoPlayerViewModel Creation
autoplayEnabled: autoplayEnabled,
overlayType: overlayType, var subtitle: String?
shouldShowPlayPreviousItem: shouldShowPlayPreviousItem,
shouldShowPlayNextItem: shouldShowPlayNextItem, // MARK: Attach media content to self
shouldShowAutoPlay: shouldShowAutoPlay)
var modifiedSelfItem = self
return videoPlayerViewModel modifiedSelfItem.mediaStreams = mediaSource.mediaStreams
})
.eraseToAnyPublisher() // TODO: other forms of media subtitle
} if self.itemType == .episode {
if let seriesName = self.seriesName, let episodeLocator = self.getEpisodeLocator() {
subtitle = "\(seriesName) - \(episodeLocator)"
}
}
let subtitlesEnabled = defaultSubtitleStream != nil
let shouldShowAutoPlay = Defaults[.shouldShowAutoPlay] && itemType == .episode
let autoplayEnabled = Defaults[.autoplayEnabled] && shouldShowAutoPlay
let overlayType = Defaults[.overlayType]
let shouldShowPlayPreviousItem = Defaults[.shouldShowPlayPreviousItem] && itemType == .episode
let shouldShowPlayNextItem = Defaults[.shouldShowPlayNextItem] && itemType == .episode
let videoPlayerViewModel = VideoPlayerViewModel(item: modifiedSelfItem,
title: modifiedSelfItem.name ?? "",
subtitle: subtitle,
streamURL: streamURL.url!,
streamType: streamType,
response: response,
audioStreams: audioStreams,
subtitleStreams: subtitleStreams,
selectedAudioStreamIndex: defaultAudioStream?.index ?? -1,
selectedSubtitleStreamIndex: defaultSubtitleStream?.index ?? -1,
subtitlesEnabled: subtitlesEnabled,
autoplayEnabled: autoplayEnabled,
overlayType: overlayType,
shouldShowPlayPreviousItem: shouldShowPlayPreviousItem,
shouldShowPlayNextItem: shouldShowPlayNextItem,
shouldShowAutoPlay: shouldShowAutoPlay)
return videoPlayerViewModel
}
.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
@ -14,282 +13,285 @@ import UIKit
// 001fC^ = dark grey plain blurhash // 001fC^ = dark grey plain blurhash
public extension BaseItemDto { public extension BaseItemDto {
// MARK: Images // MARK: Images
func getSeriesBackdropImageBlurHash() -> String { func getSeriesBackdropImageBlurHash() -> String {
let imgURL = getSeriesBackdropImage(maxWidth: 1) let imgURL = getSeriesBackdropImage(maxWidth: 1)
guard let imgTag = imgURL.queryParameters?["tag"], guard let imgTag = imgURL.queryParameters?["tag"],
let hash = imageBlurHashes?.backdrop?[imgTag] let hash = imageBlurHashes?.backdrop?[imgTag]
else { else {
return "001fC^" return "001fC^"
} }
return hash return hash
} }
func getSeriesPrimaryImageBlurHash() -> String { func getSeriesPrimaryImageBlurHash() -> String {
let imgURL = getSeriesPrimaryImage(maxWidth: 1) let imgURL = getSeriesPrimaryImage(maxWidth: 1)
guard let imgTag = imgURL.queryParameters?["tag"], guard let imgTag = imgURL.queryParameters?["tag"],
let hash = imageBlurHashes?.primary?[imgTag] let hash = imageBlurHashes?.primary?[imgTag]
else { else {
return "001fC^" return "001fC^"
} }
return hash return hash
} }
func getPrimaryImageBlurHash() -> String { func getPrimaryImageBlurHash() -> String {
let imgURL = getPrimaryImage(maxWidth: 1) let imgURL = getPrimaryImage(maxWidth: 1)
guard let imgTag = imgURL.queryParameters?["tag"], guard let imgTag = imgURL.queryParameters?["tag"],
let hash = imageBlurHashes?.primary?[imgTag] let hash = imageBlurHashes?.primary?[imgTag]
else { else {
return "001fC^" return "001fC^"
} }
return hash return hash
} }
func getBackdropImageBlurHash() -> String { func getBackdropImageBlurHash() -> String {
let imgURL = getBackdropImage(maxWidth: 1) let imgURL = getBackdropImage(maxWidth: 1)
guard let imgTag = imgURL.queryParameters?["tag"] else { guard let imgTag = imgURL.queryParameters?["tag"] else {
return "001fC^" return "001fC^"
} }
if imgURL.queryParameters?[ImageType.backdrop.rawValue] == nil { if imgURL.queryParameters?[ImageType.backdrop.rawValue] == nil {
return imageBlurHashes?.backdrop?[imgTag] ?? "001fC^" return imageBlurHashes?.backdrop?[imgTag] ?? "001fC^"
} else { } else {
return imageBlurHashes?.primary?[imgTag] ?? "001fC^" return imageBlurHashes?.primary?[imgTag] ?? "001fC^"
} }
} }
func getBackdropImage(maxWidth: Int) -> URL { func getBackdropImage(maxWidth: Int) -> URL {
var imageType = ImageType.backdrop var imageType = ImageType.backdrop
var imageTag: String? var imageTag: String?
var imageItemId = id ?? "" var imageItemId = id ?? ""
if primaryImageAspectRatio ?? 0.0 < 1.0 { if primaryImageAspectRatio ?? 0.0 < 1.0 {
if !(backdropImageTags?.isEmpty ?? true) { if !(backdropImageTags?.isEmpty ?? true) {
imageTag = backdropImageTags?.first imageTag = backdropImageTags?.first
} }
} else { } else {
imageType = .primary imageType = .primary
imageTag = imageTags?[ImageType.primary.rawValue] ?? "" imageTag = imageTags?[ImageType.primary.rawValue] ?? ""
} }
if imageTag == nil || imageItemId.isEmpty { if imageTag == nil || imageItemId.isEmpty {
if !(parentBackdropImageTags?.isEmpty ?? true) { if !(parentBackdropImageTags?.isEmpty ?? true) {
imageTag = parentBackdropImageTags?.first imageTag = parentBackdropImageTags?.first
imageItemId = parentBackdropItemId ?? "" imageItemId = parentBackdropItemId ?? ""
} }
} }
let x = UIScreen.main.nativeScale * CGFloat(maxWidth) let x = UIScreen.main.nativeScale * CGFloat(maxWidth)
let urlString = ImageAPI.getItemImageWithRequestBuilder(itemId: imageItemId, let urlString = ImageAPI.getItemImageWithRequestBuilder(itemId: imageItemId,
imageType: imageType, imageType: imageType,
maxWidth: Int(x), maxWidth: Int(x),
quality: 96, quality: 96,
tag: imageTag).URLString tag: imageTag).URLString
return URL(string: urlString)! return URL(string: urlString)!
} }
func getEpisodeLocator() -> String? { func getEpisodeLocator() -> String? {
if let seasonNo = parentIndexNumber, let episodeNo = indexNumber { if let seasonNo = parentIndexNumber, let episodeNo = indexNumber {
return L10n.seasonAndEpisode(String(seasonNo), String(episodeNo)) return L10n.seasonAndEpisode(String(seasonNo), String(episodeNo))
} }
return nil return nil
} }
func getSeriesBackdropImage(maxWidth: Int) -> URL { func getSeriesBackdropImage(maxWidth: Int) -> URL {
let x = UIScreen.main.nativeScale * CGFloat(maxWidth) let x = UIScreen.main.nativeScale * CGFloat(maxWidth)
let urlString = ImageAPI.getItemImageWithRequestBuilder(itemId: parentBackdropItemId ?? "", let urlString = ImageAPI.getItemImageWithRequestBuilder(itemId: parentBackdropItemId ?? "",
imageType: .backdrop, imageType: .backdrop,
maxWidth: Int(x), maxWidth: Int(x),
quality: 96, quality: 96,
tag: parentBackdropImageTags?.first).URLString tag: parentBackdropImageTags?.first).URLString
return URL(string: urlString)! return URL(string: urlString)!
} }
func getSeriesPrimaryImage(maxWidth: Int) -> URL { func getSeriesPrimaryImage(maxWidth: Int) -> URL {
let x = UIScreen.main.nativeScale * CGFloat(maxWidth) let x = UIScreen.main.nativeScale * CGFloat(maxWidth)
let urlString = ImageAPI.getItemImageWithRequestBuilder(itemId: seriesId ?? "", let urlString = ImageAPI.getItemImageWithRequestBuilder(itemId: seriesId ?? "",
imageType: .primary, imageType: .primary,
maxWidth: Int(x), maxWidth: Int(x),
quality: 96, quality: 96,
tag: seriesPrimaryImageTag).URLString tag: seriesPrimaryImageTag).URLString
return URL(string: urlString)! return URL(string: urlString)!
} }
func getPrimaryImage(maxWidth: Int) -> URL { func getPrimaryImage(maxWidth: Int) -> URL {
let imageType = ImageType.primary let imageType = ImageType.primary
var imageTag = imageTags?[ImageType.primary.rawValue] ?? "" var imageTag = imageTags?[ImageType.primary.rawValue] ?? ""
var imageItemId = id ?? "" var imageItemId = id ?? ""
if imageTag.isEmpty || imageItemId.isEmpty { if imageTag.isEmpty || imageItemId.isEmpty {
imageTag = seriesPrimaryImageTag ?? "" imageTag = seriesPrimaryImageTag ?? ""
imageItemId = seriesId ?? "" imageItemId = seriesId ?? ""
} }
let x = UIScreen.main.nativeScale * CGFloat(maxWidth) let x = UIScreen.main.nativeScale * CGFloat(maxWidth)
let urlString = ImageAPI.getItemImageWithRequestBuilder(itemId: imageItemId, let urlString = ImageAPI.getItemImageWithRequestBuilder(itemId: imageItemId,
imageType: imageType, imageType: imageType,
maxWidth: Int(x), maxWidth: Int(x),
quality: 96, quality: 96,
tag: imageTag).URLString tag: imageTag).URLString
return URL(string: urlString)! return URL(string: urlString)!
} }
// MARK: Calculations // MARK: Calculations
func getItemRuntime() -> String? { func getItemRuntime() -> String? {
let timeHMSFormatter: DateComponentsFormatter = { let timeHMSFormatter: DateComponentsFormatter = {
let formatter = DateComponentsFormatter() let formatter = DateComponentsFormatter()
formatter.unitsStyle = .abbreviated formatter.unitsStyle = .abbreviated
formatter.allowedUnits = [.hour, .minute] formatter.allowedUnits = [.hour, .minute]
return formatter return formatter
}() }()
guard let runTimeTicks = runTimeTicks, guard let runTimeTicks = runTimeTicks,
let text = timeHMSFormatter.string(from: Double(runTimeTicks / 10_000_000)) else { return nil } let text = timeHMSFormatter.string(from: Double(runTimeTicks / 10_000_000)) else { return nil }
return text return text
} }
func getItemProgressString() -> String? { func getItemProgressString() -> String? {
if userData?.playbackPositionTicks == nil || userData?.playbackPositionTicks == 0 { if userData?.playbackPositionTicks == nil || userData?.playbackPositionTicks == 0 {
return nil return nil
} }
let remainingSecs = ((runTimeTicks ?? 0) - (userData?.playbackPositionTicks ?? 0)) / 10_000_000 let remainingSecs = ((runTimeTicks ?? 0) - (userData?.playbackPositionTicks ?? 0)) / 10_000_000
let proghours = Int(remainingSecs / 3600) let proghours = Int(remainingSecs / 3600)
let progminutes = Int((Int(remainingSecs) - (proghours * 3600)) / 60) let progminutes = Int((Int(remainingSecs) - (proghours * 3600)) / 60)
if proghours != 0 { if proghours != 0 {
return "\(proghours)h \(String(progminutes).leftPad(toWidth: 2, withString: "0"))m" return "\(proghours)h \(String(progminutes).leftPad(toWidth: 2, withString: "0"))m"
} else { } else {
return "\(String(progminutes))m" return "\(String(progminutes))m"
} }
} }
func getLiveStartTimeString(formatter: DateFormatter) -> String {
if let startDate = self.startDate {
return formatter.string(from: startDate)
}
return " "
}
func getLiveEndTimeString(formatter: DateFormatter) -> String {
if let endDate = self.endDate {
return formatter.string(from: endDate)
}
return " "
}
func getLiveProgressPercentage() -> Double {
if let startDate = self.startDate,
let endDate = self.endDate {
let start = startDate.timeIntervalSinceReferenceDate
let end = endDate.timeIntervalSinceReferenceDate
let now = Date().timeIntervalSinceReferenceDate
let length = end - start
let progress = now - start
return progress / length
}
return 0
}
// MARK: ItemType
enum ItemType: String { func getLiveStartTimeString(formatter: DateFormatter) -> String {
case movie = "Movie" if let startDate = self.startDate {
case season = "Season" return formatter.string(from: startDate)
case episode = "Episode" }
case series = "Series" return " "
case boxset = "BoxSet" }
case unknown func getLiveEndTimeString(formatter: DateFormatter) -> String {
if let endDate = self.endDate {
return formatter.string(from: endDate)
}
return " "
}
var showDetails: Bool { func getLiveProgressPercentage() -> Double {
switch self { if let startDate = self.startDate,
case .season, .series: let endDate = self.endDate
return false {
default: let start = startDate.timeIntervalSinceReferenceDate
return true let end = endDate.timeIntervalSinceReferenceDate
} let now = Date().timeIntervalSinceReferenceDate
} let length = end - start
} let progress = now - start
return progress / length
}
return 0
}
var itemType: ItemType { // MARK: ItemType
guard let originalType = type, let knownType = ItemType(rawValue: originalType) else { return .unknown }
return knownType
}
// MARK: PortraitHeaderViewURL enum ItemType: String {
case movie = "Movie"
case season = "Season"
case episode = "Episode"
case series = "Series"
case boxset = "BoxSet"
func portraitHeaderViewURL(maxWidth: Int) -> URL { case unknown
switch itemType {
case .movie, .season, .series, .boxset: var showDetails: Bool {
return getPrimaryImage(maxWidth: maxWidth) switch self {
case .episode: case .season, .series:
return getSeriesPrimaryImage(maxWidth: maxWidth) return false
case .unknown: default:
return getPrimaryImage(maxWidth: maxWidth) return true
} }
} }
}
// MARK: ItemDetail
var itemType: ItemType {
struct ItemDetail { guard let originalType = type, let knownType = ItemType(rawValue: originalType) else { return .unknown }
let title: String return knownType
let content: String }
}
// MARK: PortraitHeaderViewURL
func createInformationItems() -> [ItemDetail] {
var informationItems: [ItemDetail] = [] func portraitHeaderViewURL(maxWidth: Int) -> URL {
switch itemType {
if let productionYear = productionYear { case .movie, .season, .series, .boxset:
informationItems.append(ItemDetail(title: "Released", content: "\(productionYear)")) return getPrimaryImage(maxWidth: maxWidth)
} case .episode:
return getSeriesPrimaryImage(maxWidth: maxWidth)
if let rating = officialRating { case .unknown:
informationItems.append(ItemDetail(title: "Rated", content: "\(rating)")) return getPrimaryImage(maxWidth: maxWidth)
} }
}
if let runtime = getItemRuntime() {
informationItems.append(ItemDetail(title: "Runtime", content: runtime)) // MARK: ItemDetail
}
struct ItemDetail {
return informationItems let title: String
} let content: String
}
func createMediaItems() -> [ItemDetail] {
var mediaItems: [ItemDetail] = [] func createInformationItems() -> [ItemDetail] {
var informationItems: [ItemDetail] = []
if let container = container {
let containerList = container.split(separator: ",").joined(separator: ", ") if let productionYear = productionYear {
informationItems.append(ItemDetail(title: "Released", content: "\(productionYear)"))
if containerList.count > 1 { }
mediaItems.append(ItemDetail(title: "Containers", content: containerList))
} else { if let rating = officialRating {
mediaItems.append(ItemDetail(title: "Container", content: containerList)) informationItems.append(ItemDetail(title: "Rated", content: "\(rating)"))
} }
}
if let runtime = getItemRuntime() {
if let mediaStreams = mediaStreams { informationItems.append(ItemDetail(title: "Runtime", content: runtime))
let audioStreams = mediaStreams.filter({ $0.type == .audio }) }
let subtitleStreams = mediaStreams.filter({ $0.type == .subtitle })
return informationItems
if !audioStreams.isEmpty { }
let audioList = audioStreams.compactMap({ "\($0.displayTitle ?? "No Title") (\($0.codec ?? "No Codec"))" }).joined(separator: ", ")
mediaItems.append(ItemDetail(title: "Audio", content: audioList)) func createMediaItems() -> [ItemDetail] {
} var mediaItems: [ItemDetail] = []
if !subtitleStreams.isEmpty { if let container = container {
let subtitleList = subtitleStreams.compactMap({ "\($0.displayTitle ?? "No Title") (\($0.codec ?? "No Codec"))" }).joined(separator: ", ") let containerList = container.split(separator: ",").joined(separator: ", ")
mediaItems.append(ItemDetail(title: "Subtitles", content: subtitleList))
} if containerList.count > 1 {
} mediaItems.append(ItemDetail(title: "Containers", content: containerList))
} else {
return mediaItems mediaItems.append(ItemDetail(title: "Container", content: containerList))
} }
}
if let mediaStreams = mediaStreams {
let audioStreams = mediaStreams.filter { $0.type == .audio }
let subtitleStreams = mediaStreams.filter { $0.type == .subtitle }
if !audioStreams.isEmpty {
let audioList = audioStreams.compactMap { "\($0.displayTitle ?? "No Title") (\($0.codec ?? "No Codec"))" }
.joined(separator: ", ")
mediaItems.append(ItemDetail(title: "Audio", content: audioList))
}
if !subtitleStreams.isEmpty {
let subtitleList = subtitleStreams.compactMap { "\($0.displayTitle ?? "No Title") (\($0.codec ?? "No Codec"))" }
.joined(separator: ", ")
mediaItems.append(ItemDetail(title: "Subtitles", content: subtitleList))
}
}
return mediaItems
}
} }

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
@ -11,99 +12,103 @@ import UIKit
extension BaseItemPerson { extension BaseItemPerson {
// MARK: Get Image // MARK: Get Image
func getImage(baseURL: String, maxWidth: Int) -> URL {
let x = UIScreen.main.nativeScale * CGFloat(maxWidth)
let urlString = ImageAPI.getItemImageWithRequestBuilder(itemId: id ?? "", func getImage(baseURL: String, maxWidth: Int) -> URL {
imageType: .primary, let x = UIScreen.main.nativeScale * CGFloat(maxWidth)
maxWidth: Int(x),
quality: 96,
tag: primaryImageTag).URLString
return URL(string: urlString)!
}
func getBlurHash() -> String { let urlString = ImageAPI.getItemImageWithRequestBuilder(itemId: id ?? "",
let imgURL = getImage(baseURL: "", maxWidth: 1) imageType: .primary,
guard let imgTag = imgURL.queryParameters?["tag"], maxWidth: Int(x),
let hash = imageBlurHashes?.primary?[imgTag] quality: 96,
else { tag: primaryImageTag).URLString
return "001fC^" return URL(string: urlString)!
} }
return hash func getBlurHash() -> String {
} let imgURL = getImage(baseURL: "", maxWidth: 1)
guard let imgTag = imgURL.queryParameters?["tag"],
let hash = imageBlurHashes?.primary?[imgTag]
else {
return "001fC^"
}
// MARK: First Role return hash
}
// Jellyfin will grab all roles the person played in the show which makes the role // MARK: First Role
// text too long. This will grab the first role which:
// - assumes that the most important role is the first
// - will also grab the last "(<text>)" instance, like "(voice)"
func firstRole() -> String? {
guard let role = self.role else { return nil }
let split = role.split(separator: "/")
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 } // Jellyfin will grab all roles the person played in the show which makes the role
// text too long. This will grab the first role which:
// - assumes that the most important role is the first
// - will also grab the last "(<text>)" instance, like "(voice)"
func firstRole() -> String? {
guard let role = self.role else { return nil }
let split = role.split(separator: "/")
guard split.count > 1 else { return role }
var final = firstRole guard let firstRole = split.first?.trimmingCharacters(in: CharacterSet(charactersIn: " ")),
let lastRole = split.last?.trimmingCharacters(in: CharacterSet(charactersIn: " ")) else { return role }
if let lastOpenIndex = lastRole.lastIndex(of: "("), let lastClosingIndex = lastRole.lastIndex(of: ")") { var final = firstRole
let roleText = lastRole[lastOpenIndex...lastClosingIndex]
final.append(" \(roleText)")
}
return final if let lastOpenIndex = lastRole.lastIndex(of: "("), let lastClosingIndex = lastRole.lastIndex(of: ")") {
} let roleText = lastRole[lastOpenIndex ... lastClosingIndex]
final.append(" \(roleText)")
}
return final
}
} }
// 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 {
return self.getImage(baseURL: SessionManager.main.currentLogin.server.currentURI, maxWidth: maxWidth)
}
public var title: String { public func imageURLContsructor(maxWidth: Int) -> URL {
return self.name ?? "" self.getImage(baseURL: SessionManager.main.currentLogin.server.currentURI, maxWidth: maxWidth)
} }
public var subtitle: String? { public var title: String {
return self.firstRole() self.name ?? ""
} }
public var blurHash: String { public var subtitle: String? {
return self.getBlurHash() self.firstRole()
} }
public var failureInitials: String { public var blurHash: String {
guard let name = self.name else { return "" } self.getBlurHash()
let initials = name.split(separator: " ").compactMap({ String($0).first }) }
return String(initials)
} public var failureInitials: String {
guard let name = self.name else { return "" }
public var showTitle: Bool { let initials = name.split(separator: " ").compactMap { String($0).first }
return true return String(initials)
} }
public var showTitle: Bool {
true
}
} }
// MARK: DiplayedType // MARK: DiplayedType
extension BaseItemPerson { extension BaseItemPerson {
// Only displayed person types. // Only displayed person types.
// Will ignore people like "GuestStar" // Will ignore people like "GuestStar"
enum DisplayedType: String, CaseIterable { enum DisplayedType: String, CaseIterable {
case actor = "Actor" case actor = "Actor"
case director = "Director" case director = "Director"
case writer = "Writer" case writer = "Writer"
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,23 +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 Foundation import Foundation
struct JellyfinAPIError: Error { struct JellyfinAPIError: Error {
private let message: String private let message: String
init(_ message: String) { init(_ message: String) {
self.message = message self.message = message
} }
var localizedDescription: String { var localizedDescription: String {
return message message
} }
} }

View File

@ -1,22 +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
extension MediaStream { extension MediaStream {
func externalURL(base: String) -> URL? { func externalURL(base: String) -> URL? {
guard let deliveryURL = deliveryUrl else { return nil } guard let deliveryURL = deliveryUrl else { return nil }
var baseComponents = URLComponents(string: base) var baseComponents = URLComponents(string: base)
baseComponents?.path += deliveryURL baseComponents?.path += deliveryURL
return baseComponents?.url return baseComponents?.url
} }
} }

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,39 +1,40 @@
/* 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
extension String { extension String {
func removeRegexMatches(pattern: String, replaceWith: String = "") -> String { func removeRegexMatches(pattern: String, replaceWith: String = "") -> String {
do { do {
let regex = try NSRegularExpression(pattern: pattern, options: .caseInsensitive) let regex = try NSRegularExpression(pattern: pattern, options: .caseInsensitive)
let range = NSRange(location: 0, length: count) let range = NSRange(location: 0, length: count)
return regex.stringByReplacingMatches(in: self, options: [], range: range, withTemplate: replaceWith) return regex.stringByReplacingMatches(in: self, options: [], range: range, withTemplate: replaceWith)
} catch { return self } } catch { return self }
} }
func leftPad(toWidth width: Int, withString string: String?) -> String { func leftPad(toWidth width: Int, withString string: String?) -> String {
let paddingString = string ?? " " let paddingString = string ?? " "
if self.count >= width { if self.count >= width {
return self return self
} }
let remainingLength: Int = width - self.count let remainingLength: Int = width - self.count
var padString = String() var padString = String()
for _ in 0 ..< remainingLength { for _ in 0 ..< remainingLength {
padString += paddingString padString += paddingString
} }
return "\(padString)\(self)" return "\(padString)\(self)"
} }
var text: Text { var text: Text {
Text(self) Text(self)
} }
} }

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,22 +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
extension URLComponents { extension URLComponents {
mutating func addQueryItem(name: String, value: String?) { mutating func addQueryItem(name: String, value: String?) {
if let _ = self.queryItems { if let _ = self.queryItems {
self.queryItems?.append(URLQueryItem(name: name, value: value)) self.queryItems?.append(URLQueryItem(name: name, value: value))
} else { } else {
self.queryItems = [] self.queryItems = []
self.queryItems?.append(URLQueryItem(name: name, value: value)) self.queryItems?.append(URLQueryItem(name: name, value: value))
} }
} }
} }

View File

@ -1,26 +1,25 @@
// //
/* // 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
public extension URL { public extension URL {
/// Dictionary of the URL's query parameters /// Dictionary of the URL's query parameters
var queryParameters: [String: String]? { var queryParameters: [String: String]? {
guard let components = URLComponents(url: self, resolvingAgainstBaseURL: false), guard let components = URLComponents(url: self, resolvingAgainstBaseURL: false),
let queryItems = components.queryItems else { return nil } let queryItems = components.queryItems else { return nil }
var items: [String: String] = [:] var items: [String: String] = [:]
for queryItem in queryItems { for queryItem in queryItems {
items[queryItem.name] = queryItem.value items[queryItem.name] = queryItem.value
} }
return items return items
} }
} }

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
@ -10,185 +18,194 @@ import Foundation
// swiftlint:disable explicit_type_interface function_parameter_count identifier_name line_length // swiftlint:disable explicit_type_interface function_parameter_count identifier_name line_length
// swiftlint:disable nesting type_body_length type_name vertical_whitespace_opening_braces // swiftlint:disable nesting type_body_length type_name vertical_whitespace_opening_braces
internal enum L10n { internal enum L10n {
/// Accessibility /// Accessibility
internal static let accessibility = L10n.tr("Localizable", "accessibility") internal static let accessibility = L10n.tr("Localizable", "accessibility")
/// Add URL /// Add URL
internal static let addURL = L10n.tr("Localizable", "addURL") internal static let addURL = L10n.tr("Localizable", "addURL")
/// All Genres /// All Genres
internal static let allGenres = L10n.tr("Localizable", "allGenres") internal static let allGenres = L10n.tr("Localizable", "allGenres")
/// All Media /// All Media
internal static let allMedia = L10n.tr("Localizable", "allMedia") internal static let allMedia = L10n.tr("Localizable", "allMedia")
/// Appearance /// Appearance
internal static let appearance = L10n.tr("Localizable", "appearance") internal static let appearance = L10n.tr("Localizable", "appearance")
/// Apply /// Apply
internal static let apply = L10n.tr("Localizable", "apply") internal static let apply = L10n.tr("Localizable", "apply")
/// Audio & Captions /// Audio & Captions
internal static let audioAndCaptions = L10n.tr("Localizable", "audioAndCaptions") internal static let audioAndCaptions = L10n.tr("Localizable", "audioAndCaptions")
/// Audio Track /// Audio Track
internal static let audioTrack = L10n.tr("Localizable", "audioTrack") internal static let audioTrack = L10n.tr("Localizable", "audioTrack")
/// Back /// Back
internal static let back = L10n.tr("Localizable", "back") internal static let back = L10n.tr("Localizable", "back")
/// CAST /// CAST
internal static let cast = L10n.tr("Localizable", "cast") internal static let cast = L10n.tr("Localizable", "cast")
/// Change Server /// Change Server
internal static let changeServer = L10n.tr("Localizable", "changeServer") internal static let changeServer = L10n.tr("Localizable", "changeServer")
/// Closed Captions /// Closed Captions
internal static let closedCaptions = L10n.tr("Localizable", "closedCaptions") internal static let closedCaptions = L10n.tr("Localizable", "closedCaptions")
/// Connect /// Connect
internal static let connect = L10n.tr("Localizable", "connect") internal static let connect = L10n.tr("Localizable", "connect")
/// Connect Manually /// Connect Manually
internal static let connectManually = L10n.tr("Localizable", "connectManually") internal static let connectManually = L10n.tr("Localizable", "connectManually")
/// Connect to Jellyfin /// Connect to Jellyfin
internal static let connectToJellyfin = L10n.tr("Localizable", "connectToJellyfin") internal static let connectToJellyfin = L10n.tr("Localizable", "connectToJellyfin")
/// Connect to Server /// Connect to Server
internal static let connectToServer = L10n.tr("Localizable", "connectToServer") internal static let connectToServer = L10n.tr("Localizable", "connectToServer")
/// Continue Watching /// Continue Watching
internal static let continueWatching = L10n.tr("Localizable", "continueWatching") internal static let continueWatching = L10n.tr("Localizable", "continueWatching")
/// Dark /// Dark
internal static let dark = L10n.tr("Localizable", "dark") internal static let dark = L10n.tr("Localizable", "dark")
/// DIRECTOR /// DIRECTOR
internal static let director = L10n.tr("Localizable", "director") internal static let director = L10n.tr("Localizable", "director")
/// Discovered Servers /// Discovered Servers
internal static let discoveredServers = L10n.tr("Localizable", "discoveredServers") internal static let discoveredServers = L10n.tr("Localizable", "discoveredServers")
/// Display order /// Display order
internal static let displayOrder = L10n.tr("Localizable", "displayOrder") internal static let displayOrder = L10n.tr("Localizable", "displayOrder")
/// Empty Next Up /// Empty Next Up
internal static let emptyNextUp = L10n.tr("Localizable", "emptyNextUp") internal static let emptyNextUp = L10n.tr("Localizable", "emptyNextUp")
/// Episodes /// Episodes
internal static let episodes = L10n.tr("Localizable", "episodes") internal static let episodes = L10n.tr("Localizable", "episodes")
/// Error /// Error
internal static let error = L10n.tr("Localizable", "error") internal static let error = L10n.tr("Localizable", "error")
/// Existing Server /// Existing Server
internal static let existingServer = L10n.tr("Localizable", "existingServer") internal static let existingServer = L10n.tr("Localizable", "existingServer")
/// Filter Results /// Filter Results
internal static let filterResults = L10n.tr("Localizable", "filterResults") internal static let filterResults = L10n.tr("Localizable", "filterResults")
/// Filters /// Filters
internal static let filters = L10n.tr("Localizable", "filters") internal static let filters = L10n.tr("Localizable", "filters")
/// Genres /// Genres
internal static let genres = L10n.tr("Localizable", "genres") internal static let genres = L10n.tr("Localizable", "genres")
/// Home /// Home
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
internal static let library = L10n.tr("Localizable", "library") /// Library
/// Light internal static let library = L10n.tr("Localizable", "library")
internal static let light = L10n.tr("Localizable", "light") /// Light
/// Loading internal static let light = L10n.tr("Localizable", "light")
internal static let loading = L10n.tr("Localizable", "loading") /// Loading
/// Local Servers internal static let loading = L10n.tr("Localizable", "loading")
internal static let localServers = L10n.tr("Localizable", "localServers") /// Local Servers
/// Login internal static let localServers = L10n.tr("Localizable", "localServers")
internal static let login = L10n.tr("Localizable", "login") /// Login
/// Login to %@ internal static let login = L10n.tr("Localizable", "login")
internal static func loginToWithString(_ p1: Any) -> String { /// Login to %@
return L10n.tr("Localizable", "loginToWithString", String(describing: p1)) internal static func loginToWithString(_ p1: Any) -> String {
} L10n.tr("Localizable", "loginToWithString", String(describing: p1))
/// More Like This }
internal static let moreLikeThis = L10n.tr("Localizable", "moreLikeThis")
/// Next Up /// More Like This
internal static let nextUp = L10n.tr("Localizable", "nextUp") internal static let moreLikeThis = L10n.tr("Localizable", "moreLikeThis")
/// No Cast devices found.. /// Next Up
internal static let noCastdevicesfound = L10n.tr("Localizable", "noCastdevicesfound") internal static let nextUp = L10n.tr("Localizable", "nextUp")
/// No results. /// No Cast devices found..
internal static let noResults = L10n.tr("Localizable", "noResults") internal static let noCastdevicesfound = L10n.tr("Localizable", "noCastdevicesfound")
/// Type: %@ not implemented yet :( /// No results.
internal static func notImplementedYetWithType(_ p1: Any) -> String { internal static let noResults = L10n.tr("Localizable", "noResults")
return L10n.tr("Localizable", "notImplementedYetWithType", String(describing: p1)) /// Type: %@ not implemented yet :(
} internal static func notImplementedYetWithType(_ p1: Any) -> String {
/// Ok L10n.tr("Localizable", "notImplementedYetWithType", String(describing: p1))
internal static let ok = L10n.tr("Localizable", "ok") }
/// Other User
internal static let otherUser = L10n.tr("Localizable", "otherUser") /// Ok
/// Page %1$@ of %2$@ internal static let ok = L10n.tr("Localizable", "ok")
internal static func pageOfWithNumbers(_ p1: Any, _ p2: Any) -> String { /// Other User
return L10n.tr("Localizable", "pageOfWithNumbers", String(describing: p1), String(describing: p2)) internal static let otherUser = L10n.tr("Localizable", "otherUser")
} /// Page %1$@ of %2$@
/// Password internal static func pageOfWithNumbers(_ p1: Any, _ p2: Any) -> String {
internal static let password = L10n.tr("Localizable", "password") L10n.tr("Localizable", "pageOfWithNumbers", String(describing: p1), String(describing: p2))
/// Play }
internal static let play = L10n.tr("Localizable", "play")
/// Playback settings /// Password
internal static let playbackSettings = L10n.tr("Localizable", "playbackSettings") internal static let password = L10n.tr("Localizable", "password")
/// Playback Speed /// Play
internal static let playbackSpeed = L10n.tr("Localizable", "playbackSpeed") internal static let play = L10n.tr("Localizable", "play")
/// Play Next /// Playback settings
internal static let playNext = L10n.tr("Localizable", "playNext") internal static let playbackSettings = L10n.tr("Localizable", "playbackSettings")
/// Reset /// Playback Speed
internal static let reset = L10n.tr("Localizable", "reset") internal static let playbackSpeed = L10n.tr("Localizable", "playbackSpeed")
/// Search /// Play Next
internal static let search = L10n.tr("Localizable", "search") internal static let playNext = L10n.tr("Localizable", "playNext")
/// S%1$@:E%2$@ /// Reset
internal static func seasonAndEpisode(_ p1: Any, _ p2: Any) -> String { internal static let reset = L10n.tr("Localizable", "reset")
return L10n.tr("Localizable", "seasonAndEpisode", String(describing: p1), String(describing: p2)) /// Search
} internal static let search = L10n.tr("Localizable", "search")
/// Seasons /// S%1$@:E%2$@
internal static let seasons = L10n.tr("Localizable", "seasons") internal static func seasonAndEpisode(_ p1: Any, _ p2: Any) -> String {
/// See All L10n.tr("Localizable", "seasonAndEpisode", String(describing: p1), String(describing: p2))
internal static let seeAll = L10n.tr("Localizable", "seeAll") }
/// Select Cast Destination
internal static let selectCastDestination = L10n.tr("Localizable", "selectCastDestination") /// Seasons
/// Server %s already exists. Add new URL? internal static let seasons = L10n.tr("Localizable", "seasons")
internal static func serverAlreadyExistsPrompt(_ p1: UnsafePointer<CChar>) -> String { /// See All
return L10n.tr("Localizable", "serverAlreadyExistsPrompt", p1) internal static let seeAll = L10n.tr("Localizable", "seeAll")
} /// Select Cast Destination
/// Server Information internal static let selectCastDestination = L10n.tr("Localizable", "selectCastDestination")
internal static let serverInformation = L10n.tr("Localizable", "serverInformation") /// Server %s already exists. Add new URL?
/// Server URL internal static func serverAlreadyExistsPrompt(_ p1: UnsafePointer<CChar>) -> String {
internal static let serverURL = L10n.tr("Localizable", "serverURL") L10n.tr("Localizable", "serverAlreadyExistsPrompt", p1)
/// Signed in as %@ }
internal static func signedInAsWithString(_ p1: Any) -> String {
return L10n.tr("Localizable", "signedInAsWithString", String(describing: p1)) /// Server Information
} internal static let serverInformation = L10n.tr("Localizable", "serverInformation")
/// Sort by /// Server URL
internal static let sortBy = L10n.tr("Localizable", "sortBy") internal static let serverURL = L10n.tr("Localizable", "serverURL")
/// STUDIO /// Signed in as %@
internal static let studio = L10n.tr("Localizable", "studio") internal static func signedInAsWithString(_ p1: Any) -> String {
/// Studios L10n.tr("Localizable", "signedInAsWithString", String(describing: p1))
internal static let studios = L10n.tr("Localizable", "studios") }
/// Suggestions
internal static let suggestions = L10n.tr("Localizable", "suggestions") /// Sort by
/// Switch user internal static let sortBy = L10n.tr("Localizable", "sortBy")
internal static let switchUser = L10n.tr("Localizable", "switchUser") /// STUDIO
/// System internal static let studio = L10n.tr("Localizable", "studio")
internal static let system = L10n.tr("Localizable", "system") /// Studios
/// Tags internal static let studios = L10n.tr("Localizable", "studios")
internal static let tags = L10n.tr("Localizable", "tags") /// Suggestions
/// Try again internal static let suggestions = L10n.tr("Localizable", "suggestions")
internal static let tryAgain = L10n.tr("Localizable", "tryAgain") /// Switch user
/// Unknown Error internal static let switchUser = L10n.tr("Localizable", "switchUser")
internal static let unknownError = L10n.tr("Localizable", "unknownError") /// System
/// Username internal static let system = L10n.tr("Localizable", "system")
internal static let username = L10n.tr("Localizable", "username") /// Tags
/// Who's watching? internal static let tags = L10n.tr("Localizable", "tags")
internal static let whosWatching = L10n.tr("Localizable", "WhosWatching") /// Try again
/// WIP internal static let tryAgain = L10n.tr("Localizable", "tryAgain")
internal static let wip = L10n.tr("Localizable", "wip") /// Unknown Error
/// Your Favorites internal static let unknownError = L10n.tr("Localizable", "unknownError")
internal static let yourFavorites = L10n.tr("Localizable", "yourFavorites") /// Username
internal static let username = L10n.tr("Localizable", "username")
/// Who's watching?
internal static let whosWatching = L10n.tr("Localizable", "WhosWatching")
/// WIP
internal static let wip = L10n.tr("Localizable", "wip")
/// Your Favorites
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
// MARK: - Implementation Details // MARK: - Implementation Details
extension L10n { extension L10n {
private static func tr(_ table: String, _ key: String, _ args: CVarArg...) -> String { private static func tr(_ table: String, _ key: String, _ args: CVarArg...) -> String {
let format = BundleToken.bundle.localizedString(forKey: key, value: nil, table: table) let format = BundleToken.bundle.localizedString(forKey: key, value: nil, table: table)
return String(format: format, locale: Locale.current, arguments: args) return String(format: format, locale: Locale.current, arguments: args)
} }
} }
// swiftlint:disable convenience_type // swiftlint:disable convenience_type
private final class BundleToken { private final class BundleToken {
static let bundle: Bundle = { static let bundle: Bundle = {
#if SWIFT_PACKAGE #if SWIFT_PACKAGE
return Bundle.module return Bundle.module
#else #else
return Bundle(for: BundleToken.self) return Bundle(for: BundleToken.self)
#endif #endif
}() }()
} }
// swiftlint:enable convenience_type // swiftlint:enable convenience_type

View File

@ -1,39 +1,38 @@
// //
/* // 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
enum AppAppearance: String, CaseIterable, Defaults.Serializable { enum AppAppearance: String, CaseIterable, Defaults.Serializable {
case system case system
case dark case dark
case light case light
var localizedName: String { var localizedName: String {
switch self { switch self {
case .system: case .system:
return L10n.system return L10n.system
case .dark: case .dark:
return L10n.dark return L10n.dark
case .light: case .light:
return L10n.light return L10n.light
} }
} }
var style: UIUserInterfaceStyle { var style: UIUserInterfaceStyle {
switch self { switch self {
case .system: case .system:
return .unspecified return .unspecified
case .dark: case .dark:
return .dark return .dark
case .light: case .light:
return .light return .light
} }
} }
} }

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
struct Bitrates: Codable, Hashable { struct Bitrates: Codable, Hashable {
public var name: String public var name: String
public var value: Int public var value: Int
} }

View File

@ -1,25 +1,23 @@
// //
/* // 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
enum DetailItemType: String { enum DetailItemType: String {
case movie = "Movie" case movie = "Movie"
case season = "Season" case season = "Season"
case series = "Series" case series = "Series"
case episode = "Episode" case episode = "Episode"
} }
struct DetailItem { 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 :|
@ -11,196 +12,241 @@ import Foundation
import JellyfinAPI import JellyfinAPI
enum CPUModel { enum CPUModel {
case A4 case A4
case A5 case A5
case A5X case A5X
case A6 case A6
case A6X case A6X
case A7 case A7
case A7X case A7X
case A8 case A8
case A8X case A8X
case A9 case A9
case A9X case A9X
case A10 case A10
case A10X case A10X
case A11 case A11
case A12 case A12
case A12X case A12X
case A12Z case A12Z
case A13 case A13
case A14 case A14
case A99 case A99
} }
class DeviceProfileBuilder { class DeviceProfileBuilder {
public var bitrate: Int = 0 public var bitrate: Int = 0
public func setMaxBitrate(bitrate: Int) { public func setMaxBitrate(bitrate: Int) {
self.bitrate = bitrate self.bitrate = bitrate
} }
public func buildProfile() -> DeviceProfile { public func buildProfile() -> DeviceProfile {
let maxStreamingBitrate = bitrate let maxStreamingBitrate = bitrate
let maxStaticBitrate = bitrate let maxStaticBitrate = bitrate
let musicStreamingTranscodingBitrate = bitrate let musicStreamingTranscodingBitrate = bitrate
// 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",
} else { videoCodec: "hevc,h264,hev1,mpeg4,vp9",
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 type: .video)] // HEVC/H.264 with Dolby Digital
} } 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
}
}
// 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
var transcodingProfiles: [TranscodingProfile] = [] var transcodingProfiles: [TranscodingProfile] = []
transcodingProfiles = [TranscodingProfile(container: "ts", type: .video, videoCodec: "h264,mpeg4", audioCodec: "aac,mp3,wav")] transcodingProfiles = [TranscodingProfile(container: "ts", type: .video, videoCodec: "h264,mpeg4", audioCodec: "aac,mp3,wav")]
// 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",
} else { audioCodec: "aac,mp3,wav,eac3,ac3,flac,opus", _protocol: "hls",
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)] context: .streaming, maxAudioChannels: "6", minSegments: 2,
} breakOnNonKeyFrames: true)]
} } 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)]
}
}
// 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",
ProfileCondition(condition: .lessThanEqual, property: .videoLevel, value: "80", isRequired: false), isRequired: false),
ProfileCondition(condition: .notEquals, property: .isInterlaced, value: "true", isRequired: false)] ProfileCondition(condition: .lessThanEqual, property: .videoLevel, value: "80", isRequired: false),
let hevcCodecConditions: [ProfileCondition] = [ ProfileCondition(condition: .notEquals, property: .isInterlaced, 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), let hevcCodecConditions: [ProfileCondition] = [
ProfileCondition(condition: .lessThanEqual, property: .videoLevel, value: "175", isRequired: false), ProfileCondition(condition: .notEquals, property: .isAnamorphic, value: "true", isRequired: false),
ProfileCondition(condition: .notEquals, property: .isInterlaced, value: "true", 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: .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"))
if supportsFeature(minimumSupported: .A9) { if supportsFeature(minimumSupported: .A9) {
codecProfiles.append(CodecProfile(type: .video, applyConditions: hevcCodecConditions, codec: "hevc")) codecProfiles.append(CodecProfile(type: .video, applyConditions: hevcCodecConditions, codec: "hevc"))
} }
var subtitleProfiles: [SubtitleProfile] = [] var subtitleProfiles: [SubtitleProfile] = []
subtitleProfiles.append(SubtitleProfile(format: "ass", method: .embed)) subtitleProfiles.append(SubtitleProfile(format: "ass", method: .embed))
subtitleProfiles.append(SubtitleProfile(format: "ssa", method: .embed)) subtitleProfiles.append(SubtitleProfile(format: "ssa", method: .embed))
subtitleProfiles.append(SubtitleProfile(format: "subrip", method: .embed)) subtitleProfiles.append(SubtitleProfile(format: "subrip", method: .embed))
subtitleProfiles.append(SubtitleProfile(format: "sub", method: .embed)) subtitleProfiles.append(SubtitleProfile(format: "sub", method: .embed))
subtitleProfiles.append(SubtitleProfile(format: "pgssub", method: .embed)) subtitleProfiles.append(SubtitleProfile(format: "pgssub", method: .embed))
// These need to be filtered. Most subrips are embedded. I hate subtitles. // These need to be filtered. Most subrips are embedded. I hate subtitles.
subtitleProfiles.append(SubtitleProfile(format: "subrip", method: .external)) subtitleProfiles.append(SubtitleProfile(format: "subrip", method: .external))
subtitleProfiles.append(SubtitleProfile(format: "sub", method: .external)) subtitleProfiles.append(SubtitleProfile(format: "sub", method: .external))
subtitleProfiles.append(SubtitleProfile(format: "ass", method: .external)) subtitleProfiles.append(SubtitleProfile(format: "ass", method: .external))
subtitleProfiles.append(SubtitleProfile(format: "ssa", method: .external)) subtitleProfiles.append(SubtitleProfile(format: "ssa", method: .external))
subtitleProfiles.append(SubtitleProfile(format: "vtt", method: .external)) subtitleProfiles.append(SubtitleProfile(format: "vtt", method: .external))
subtitleProfiles.append(SubtitleProfile(format: "ass", method: .external)) subtitleProfiles.append(SubtitleProfile(format: "ass", method: .external))
subtitleProfiles.append(SubtitleProfile(format: "ssa", method: .external)) subtitleProfiles.append(SubtitleProfile(format: "ssa", method: .external))
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] = [
return intValues[CPUinfo()] ?? 0 >= intValues[minimumSupported] ?? 0 .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
}
/********************************************** /**********************************************
* CPUInfo(): * CPUInfo():
* Returns a hardcoded value of the current * Returns a hardcoded value of the current
* devices CPU name. * devices CPU name.
***********************************************/ ***********************************************/
private func CPUinfo() -> CPUModel { private func CPUinfo() -> CPUModel {
#if targetEnvironment(simulator) #if targetEnvironment(simulator)
let identifier = ProcessInfo().environment["SIMULATOR_MODEL_IDENTIFIER"]! let identifier = ProcessInfo().environment["SIMULATOR_MODEL_IDENTIFIER"]!
#else #else
var systemInfo = utsname() var systemInfo = utsname()
uname(&systemInfo) uname(&systemInfo)
let machineMirror = Mirror(reflecting: systemInfo.machine) let machineMirror = Mirror(reflecting: systemInfo.machine)
let identifier = machineMirror.children.reduce("") { identifier, element in let identifier = machineMirror.children.reduce("") { identifier, element in
guard let value = element.value as? Int8, value != 0 else { return identifier } guard let value = element.value as? Int8, value != 0 else { return identifier }
return identifier + String(UnicodeScalar(UInt8(value))) return identifier + String(UnicodeScalar(UInt8(value)))
} }
#endif #endif
switch identifier { switch identifier {
case "iPod5,1": return .A5 case "iPod5,1": return .A5
case "iPod7,1": return .A8 case "iPod7,1": return .A8
case "iPod9,1": return .A10 case "iPod9,1": return .A10
case "iPhone3,1", "iPhone3,2", "iPhone3,3": return .A4 case "iPhone3,1", "iPhone3,2", "iPhone3,3": return .A4
case "iPhone4,1": return .A5 case "iPhone4,1": return .A5
case "iPhone5,1", "iPhone5,2": return .A6 case "iPhone5,1", "iPhone5,2": return .A6
case "iPhone5,3", "iPhone5,4": return .A6 case "iPhone5,3", "iPhone5,4": return .A6
case "iPhone6,1", "iPhone6,2": return .A7 case "iPhone6,1", "iPhone6,2": return .A7
case "iPhone7,2": return .A8 case "iPhone7,2": return .A8
case "iPhone7,1": return .A8 case "iPhone7,1": return .A8
case "iPhone8,1": return .A9 case "iPhone8,1": return .A9
case "iPhone8,2", "iPhone8,4": return .A9 case "iPhone8,2", "iPhone8,4": return .A9
case "iPhone9,1", "iPhone9,3": return .A10 case "iPhone9,1", "iPhone9,3": return .A10
case "iPhone9,2", "iPhone9,4": return .A10 case "iPhone9,2", "iPhone9,4": return .A10
case "iPhone10,1", "iPhone10,4": return .A11 case "iPhone10,1", "iPhone10,4": return .A11
case "iPhone10,2", "iPhone10,5": return .A11 case "iPhone10,2", "iPhone10,5": return .A11
case "iPhone10,3", "iPhone10,6": return .A11 case "iPhone10,3", "iPhone10,6": return .A11
case "iPhone11,2", "iPhone11,6", "iPhone11,8": return .A12 case "iPhone11,2", "iPhone11,6", "iPhone11,8": return .A12
case "iPhone12,1", "iPhone12,3", "iPhone12,5", "iPhone12,8": return .A13 case "iPhone12,1", "iPhone12,3", "iPhone12,5", "iPhone12,8": return .A13
case "iPhone13,1", "iPhone13,2", "iPhone13,3", "iPhone13,4": return .A14 case "iPhone13,1", "iPhone13,2", "iPhone13,3", "iPhone13,4": return .A14
case "iPad2,1", "iPad2,2", "iPad2,3", "iPad2,4": return .A5 case "iPad2,1", "iPad2,2", "iPad2,3", "iPad2,4": return .A5
case "iPad3,1", "iPad3,2", "iPad3,3": return .A5X case "iPad3,1", "iPad3,2", "iPad3,3": return .A5X
case "iPad3,4", "iPad3,5", "iPad3,6": return .A6X case "iPad3,4", "iPad3,5", "iPad3,6": return .A6X
case "iPad4,1", "iPad4,2", "iPad4,3": return .A7 case "iPad4,1", "iPad4,2", "iPad4,3": return .A7
case "iPad5,3", "iPad5,4": return .A8X case "iPad5,3", "iPad5,4": return .A8X
case "iPad6,11", "iPad6,12": return .A9 case "iPad6,11", "iPad6,12": return .A9
case "iPad2,5", "iPad2,6", "iPad2,7": return .A5 case "iPad2,5", "iPad2,6", "iPad2,7": return .A5
case "iPad4,4", "iPad4,5", "iPad4,6": return .A7 case "iPad4,4", "iPad4,5", "iPad4,6": return .A7
case "iPad4,7", "iPad4,8", "iPad4,9": return .A7 case "iPad4,7", "iPad4,8", "iPad4,9": return .A7
case "iPad5,1", "iPad5,2": return .A8 case "iPad5,1", "iPad5,2": return .A8
case "iPad11,1", "iPad11,2": return .A12 case "iPad11,1", "iPad11,2": return .A12
case "iPad6,3", "iPad6,4": return .A9X case "iPad6,3", "iPad6,4": return .A9X
case "iPad6,7", "iPad6,8": return .A9X case "iPad6,7", "iPad6,8": return .A9X
case "iPad7,1", "iPad7,2": return .A10X case "iPad7,1", "iPad7,2": return .A10X
case "iPad7,3", "iPad7,4": return .A10X case "iPad7,3", "iPad7,4": return .A10X
case "iPad7,5", "iPad7,6", "iPad7,11", "iPad7,12": return .A10 case "iPad7,5", "iPad7,6", "iPad7,11", "iPad7,12": return .A10
case "iPad8,1", "iPad8,2", "iPad8,3", "iPad8,4": return .A12X case "iPad8,1", "iPad8,2", "iPad8,3", "iPad8,4": return .A12X
case "iPad8,5", "iPad8,6", "iPad8,7", "iPad8,8": return .A12X case "iPad8,5", "iPad8,6", "iPad8,7", "iPad8,8": return .A12X
case "iPad8,9", "iPad8,10", "iPad8,11", "iPad8,12": return .A12Z case "iPad8,9", "iPad8,10", "iPad8,11", "iPad8,12": return .A12Z
case "iPad11,3", "iPad11,4", "iPad11,6", "iPad11,7": return .A12 case "iPad11,3", "iPad11,4", "iPad11,6", "iPad11,7": return .A12
case "iPad13,1", "iPad13,2": return .A14 case "iPad13,1", "iPad13,2": return .A14
case "AppleTV5,3": return .A8 case "AppleTV5,3": return .A8
case "AppleTV6,2": return .A10X case "AppleTV6,2": return .A10X
case "AppleTV11,1": return .A12 case "AppleTV11,1": return .A12
case "AudioAccessory1,1": return .A8 case "AudioAccessory1,1": return .A8
default: return .A99 default: return .A99
} }
} }
} }

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
@ -14,20 +13,20 @@ import SwiftUI
// Our custom view modifier to track rotation and // Our custom view modifier to track rotation and
// call our action // call our action
struct DeviceRotationViewModifier: ViewModifier { struct DeviceRotationViewModifier: ViewModifier {
let action: (UIDeviceOrientation) -> Void let action: (UIDeviceOrientation) -> Void
func body(content: Content) -> some View { func body(content: Content) -> some View {
content content
.onAppear() .onAppear()
.onReceive(NotificationCenter.default.publisher(for: UIDevice.orientationDidChangeNotification)) { _ in .onReceive(NotificationCenter.default.publisher(for: UIDevice.orientationDidChangeNotification)) { _ in
action(UIDevice.current.orientation) action(UIDevice.current.orientation)
} }
} }
} }
// A View wrapper to make the modifier easier to use // A View wrapper to make the modifier easier to use
extension View { extension View {
func onRotate(perform action: @escaping (UIDeviceOrientation) -> Void) -> some View { func onRotate(perform action: @escaping (UIDeviceOrientation) -> Void) -> some View {
self.modifier(DeviceRotationViewModifier(action: action)) self.modifier(DeviceRotationViewModifier(action: action))
} }
} }

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 Defaults import Defaults
import Foundation import Foundation
enum HTTPScheme: String, Defaults.Serializable, CaseIterable { enum HTTPScheme: String, Defaults.Serializable, CaseIterable {
case http case http
case https case https
} }

View File

@ -1,25 +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 Defaults import Defaults
import UIKit import UIKit
enum OverlaySliderColor: String, CaseIterable, DefaultsSerializable { enum OverlaySliderColor: String, CaseIterable, DefaultsSerializable {
case white case white
case jellyfinPurple case jellyfinPurple
var displayLabel: String { var displayLabel: String {
switch self { switch self {
case .white: case .white:
return "White" return "White"
case .jellyfinPurple: case .jellyfinPurple:
return "Jellyfin Purple" return "Jellyfin Purple"
} }
} }
} }

View File

@ -1,25 +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 Defaults import Defaults
import Foundation import Foundation
enum OverlayType: String, CaseIterable, Defaults.Serializable { enum OverlayType: String, CaseIterable, Defaults.Serializable {
case normal case normal
case compact case compact
var label: String { var label: String {
switch self { switch self {
case .normal: case .normal:
return "Normal" return "Normal"
case .compact: case .compact:
return "Compact" return "Compact"
} }
} }
} }

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 Foundation import Foundation
protocol PillStackable { protocol PillStackable {
var title: String { get } var title: String { get }
} }

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,20 +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 Foundation
public protocol PortraitImageStackable { public protocol PortraitImageStackable {
func imageURLContsructor(maxWidth: Int) -> URL func imageURLContsructor(maxWidth: Int) -> URL
var title: String { get } var title: String { get }
var subtitle: String? { get } var subtitle: String? { get }
var blurHash: String { get } var blurHash: String { get }
var failureInitials: String { get } var failureInitials: String { get }
var portraitImageID: String { get } var portraitImageID: String { get }
var showTitle: Bool { get } var showTitle: Bool { get }
} }

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
enum PosterSize { enum PosterSize {
case small case small
case normal case normal
} }

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
struct TrackLanguage: Hashable { struct TrackLanguage: Hashable {
var name: String var name: String
var isoCode: String var isoCode: String
static let auto = TrackLanguage(name: "Auto", isoCode: "Auto") static let auto = TrackLanguage(name: "Auto", isoCode: "Auto")
} }

View File

@ -1,89 +1,90 @@
/* 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
import JellyfinAPI import JellyfinAPI
struct LibraryFilters: Codable, Hashable { struct LibraryFilters: Codable, Hashable {
var filters: [ItemFilter] = [] var filters: [ItemFilter] = []
var sortOrder: [APISortOrder] = [.descending] var sortOrder: [APISortOrder] = [.descending]
var withGenres: [NameGuidPair] = [] var withGenres: [NameGuidPair] = []
var tags: [String] = [] var tags: [String] = []
var sortBy: [SortBy] = [.name] var sortBy: [SortBy] = [.name]
} }
public enum SortBy: String, Codable, CaseIterable { public enum SortBy: String, Codable, CaseIterable {
case premiereDate = "PremiereDate" case premiereDate = "PremiereDate"
case name = "SortName" case name = "SortName"
case dateAdded = "DateCreated" case dateAdded = "DateCreated"
} }
extension SortBy { extension SortBy {
var localized: String { var localized: String {
switch self { switch self {
case .premiereDate: case .premiereDate:
return "Premiere date" return "Premiere date"
case .name: case .name:
return "Name" return "Name"
case .dateAdded: case .dateAdded:
return "Date added" return "Date added"
} }
} }
} }
extension ItemFilter { extension ItemFilter {
static var supportedTypes: [ItemFilter] { static var supportedTypes: [ItemFilter] {
[.isUnplayed, isPlayed, .isFavorite, .likes] [.isUnplayed, isPlayed, .isFavorite, .likes]
} }
var localized: String { var localized: String {
switch self { switch self {
case .isUnplayed: case .isUnplayed:
return "Unplayed" return "Unplayed"
case .isPlayed: case .isPlayed:
return "Played" return "Played"
case .isFavorite: case .isFavorite:
return "Favorites" return "Favorites"
case .likes: case .likes:
return "Liked Items" return "Liked Items"
default: default:
return "" return ""
} }
} }
} }
extension APISortOrder { extension APISortOrder {
var localized: String { var localized: String {
switch self { switch self {
case .ascending: case .ascending:
return "Ascending" return "Ascending"
case .descending: case .descending:
return "Descending" return "Descending"
} }
} }
} }
enum ItemType: String { enum ItemType: String {
case episode = "Episode" case episode = "Episode"
case movie = "Movie" case movie = "Movie"
case series = "Series" case series = "Series"
case season = "Season" case season = "Season"
var localized: String { var localized: String {
switch self { switch self {
case .episode: case .episode:
return L10n.episodes return L10n.episodes
case .movie: case .movie:
return "Movies" return "Movies"
case .series: case .series:
return "Shows" return "Shows"
default: default:
return "" return ""
} }
} }
} }

View File

@ -1,52 +1,51 @@
// //
/* // 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
case fifteen = 15 case fifteen = 15
case ten = 10 case ten = 10
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 {
switch self { switch self {
case .thirty: case .thirty:
return "goforward.30" return "goforward.30"
case .fifteen: case .fifteen:
return "goforward.15" return "goforward.15"
case .ten: case .ten:
return "goforward.10" return "goforward.10"
case .five: case .five:
return "goforward.5" return "goforward.5"
} }
} }
var backwardImageLabel: String { var backwardImageLabel: String {
switch self { switch self {
case .thirty: case .thirty:
return "gobackward.30" return "gobackward.30"
case .fifteen: case .fifteen:
return "gobackward.15" return "gobackward.15"
case .ten: case .ten:
return "gobackward.10" return "gobackward.10"
case .five: case .five:
return "gobackward.5" return "gobackward.5"
} }
} }
} }

View File

@ -1,79 +1,76 @@
// //
/* // 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
public class ServerDiscovery { 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
public let id: String public let id: String
public let name: String public let name: String
public var url: URL { public var url: URL {
URL(string: self.address)! URL(string: self.address)!
} }
public var host: String {
let components = URLComponents(string: self.address)
if let host = components?.host {
return host
}
return self.address
}
public var port: Int { public var host: String {
let components = URLComponents(string: self.address) let components = URLComponents(string: self.address)
if let port = components?.port { if let host = components?.host {
return port return host
} }
return 7359 return self.address
} }
enum CodingKeys: String, CodingKey { public var port: Int {
case address = "Address" let components = URLComponents(string: self.address)
case id = "Id" if let port = components?.port {
case name = "Name" return port
} }
} return 7359
}
private let broadcastConn: UDPBroadcastConnection enum CodingKeys: String, CodingKey {
case address = "Address"
case id = "Id"
case name = "Name"
}
}
public init() { private let broadcastConn: UDPBroadcastConnection
func receiveHandler(_ ipAddress: String, _ port: Int, _ response: Data) {
}
func errorHandler(error: UDPBroadcastConnection.ConnectionError) { public init() {
} func receiveHandler(_ ipAddress: String, _ port: Int, _ response: Data) {}
self.broadcastConn = try! UDPBroadcastConnection(port: 7359, handler: receiveHandler, errorHandler: errorHandler)
}
public func locateServer(completion: @escaping (ServerLookupResponse?) -> Void) { func errorHandler(error: UDPBroadcastConnection.ConnectionError) {}
func receiveHandler(_ ipAddress: String, _ port: Int, _ data: Data) { self.broadcastConn = try! UDPBroadcastConnection(port: 7359, handler: receiveHandler, errorHandler: errorHandler)
do { }
let response = try JSONDecoder().decode(ServerLookupResponse.self, from: data)
LogManager.shared.log.debug("Received JellyfinServer from \"\(response.name)\"", tag: "ServerDiscovery") public func locateServer(completion: @escaping (ServerLookupResponse?) -> Void) {
completion(response) func receiveHandler(_ ipAddress: String, _ port: Int, _ data: Data) {
} catch { do {
completion(nil) let response = try JSONDecoder().decode(ServerLookupResponse.self, from: data)
} LogManager.shared.log.debug("Received JellyfinServer from \"\(response.name)\"", tag: "ServerDiscovery")
} completion(response)
self.broadcastConn.handler = receiveHandler } catch {
do { completion(nil)
try broadcastConn.sendBroadcast("Who is JellyfinServer?") }
LogManager.shared.log.debug("Discovery broadcast sent", tag: "ServerDiscovery") }
} catch { self.broadcastConn.handler = receiveHandler
print(error) do {
} try broadcastConn.sendBroadcast("Who is JellyfinServer?")
} LogManager.shared.log.debug("Discovery broadcast sent", tag: "ServerDiscovery")
} catch {
print(error)
}
}
} }

View File

@ -1,290 +1,289 @@
// //
/* // 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 {
// MARK: Properties // MARK: Properties
/// The address of the UDP socket. /// The address of the UDP socket.
var address: sockaddr_in var address: sockaddr_in
/// Type of a closure that handles incoming UDP packets. /// Type of a closure that handles incoming UDP packets.
public typealias ReceiveHandler = (_ ipAddress: String, _ port: Int, _ response: Data) -> Void public typealias ReceiveHandler = (_ ipAddress: String, _ port: Int, _ response: Data) -> Void
/// Closure that handles incoming UDP packets. /// Closure that handles incoming UDP packets.
var handler: ReceiveHandler? var handler: ReceiveHandler?
/// Type of a closure that handles errors that were encountered during receiving UDP packets. /// Type of a closure that handles errors that were encountered during receiving UDP packets.
public typealias ErrorHandler = (_ error: ConnectionError) -> Void public typealias ErrorHandler = (_ error: ConnectionError) -> Void
/// Closure that handles errors that were encountered during receiving UDP packets. /// Closure that handles errors that were encountered during receiving UDP packets.
var errorHandler: ErrorHandler? var errorHandler: ErrorHandler?
/// A dispatch source for reading data from the UDP socket. /// A dispatch source for reading data from the UDP socket.
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
// MARK: Initializers // MARK: Initializers
/// Initializes the UDP connection with the correct port address. /// Initializes the UDP connection with the correct port address.
/// - Note: This doesn't open a socket! The socket is opened transparently as needed when sending broadcast messages. If you want to open a socket immediately, use the `bindIt` parameter. This will also try to reopen the socket if it gets closed. /// - Note: This doesn't open a socket! The socket is opened transparently as needed when sending broadcast messages. If you want to open a socket immediately, use the `bindIt` parameter. This will also try to reopen the socket if it gets closed.
/// ///
/// - Parameters: /// - Parameters:
/// - port: Number of the UDP port to use. /// - port: Number of the UDP port to use.
/// - bindIt: Opens a port immediately if true, on demand if false. Default is false. /// - bindIt: Opens a port immediately if true, on demand if false. Default is false.
/// - handler: Handler that gets called when data is received. /// - handler: Handler that gets called when data is received.
/// - 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
self.shouldBeBound = bindIt self.shouldBeBound = bindIt
if bindIt { if bindIt {
try createSocket() try createSocket()
} }
} }
deinit { deinit {
if responseSource != nil { if responseSource != nil {
responseSource!.cancel() responseSource!.cancel()
} }
} }
// MARK: Interface // MARK: Interface
/// Create a UDP socket for broadcasting and set up cancel and event handlers /// Create a UDP socket for broadcasting and set up cancel and event handlers
/// ///
/// - Throws: Throws a `ConnectionError` if an error occurs. /// - Throws: Throws a `ConnectionError` if an error occurs.
fileprivate func createSocket() throws { fileprivate func createSocket() throws {
// Create new socket // Create new socket
let newSocket = socket(AF_INET, SOCK_DGRAM, IPPROTO_UDP) let newSocket = socket(AF_INET, SOCK_DGRAM, IPPROTO_UDP)
guard newSocket > 0 else { throw ConnectionError.createSocketFailed } guard newSocket > 0 else { throw ConnectionError.createSocketFailed }
// Enable broadcast on socket // Enable broadcast on socket
var broadcastEnable = Int32(1) var broadcastEnable = Int32(1)
let ret = setsockopt(newSocket, SOL_SOCKET, SO_BROADCAST, &broadcastEnable, socklen_t(MemoryLayout<UInt32>.size)) let ret = setsockopt(newSocket, SOL_SOCKET, SO_BROADCAST, &broadcastEnable, socklen_t(MemoryLayout<UInt32>.size))
if ret == -1 { if ret == -1 {
debugPrint("Couldn't enable broadcast on socket") debugPrint("Couldn't enable broadcast on socket")
close(newSocket) close(newSocket)
throw ConnectionError.enableBroadcastFailed throw ConnectionError.enableBroadcastFailed
} }
// Bind socket if needed // Bind socket if needed
if shouldBeBound { if shouldBeBound {
var saddr = sockaddr(sa_len: 0, sa_family: 0, var saddr = sockaddr(sa_len: 0, sa_family: 0,
sa_data: (0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0)) sa_data: (0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0))
self.address.sin_addr = INADDR_ANY self.address.sin_addr = INADDR_ANY
memcpy(&saddr, &self.address, MemoryLayout<sockaddr_in>.size) memcpy(&saddr, &self.address, MemoryLayout<sockaddr_in>.size)
self.address.sin_addr = INADDR_BROADCAST self.address.sin_addr = INADDR_BROADCAST
let isBound = bind(newSocket, &saddr, socklen_t(MemoryLayout<sockaddr_in>.size)) let isBound = bind(newSocket, &saddr, socklen_t(MemoryLayout<sockaddr_in>.size))
if isBound == -1 { if isBound == -1 {
debugPrint("Couldn't bind socket") debugPrint("Couldn't bind socket")
close(newSocket) close(newSocket)
throw ConnectionError.bindSocketFailed throw ConnectionError.bindSocketFailed
} }
} }
// Disable global SIGPIPE handler so that the app doesn't crash // Disable global SIGPIPE handler so that the app doesn't crash
setNoSigPipe(socket: newSocket) setNoSigPipe(socket: newSocket)
// Set up a dispatch source // Set up a dispatch source
let newResponseSource = DispatchSource.makeReadSource(fileDescriptor: newSocket, queue: dispatchQueue) let newResponseSource = DispatchSource.makeReadSource(fileDescriptor: newSocket, queue: dispatchQueue)
// Set up cancel handler // Set up cancel handler
newResponseSource.setCancelHandler { newResponseSource.setCancelHandler {
// debugPrint("Closing UDP socket") // debugPrint("Closing UDP socket")
let UDPSocket = Int32(newResponseSource.handle) let UDPSocket = Int32(newResponseSource.handle)
shutdown(UDPSocket, SHUT_RDWR) shutdown(UDPSocket, SHUT_RDWR)
close(UDPSocket) close(UDPSocket)
} }
// Set up event handler (gets called when data arrives at the UDP socket) // Set up event handler (gets called when data arrives at the UDP socket)
newResponseSource.setEventHandler { [unowned self] in newResponseSource.setEventHandler { [unowned self] in
guard let source = self.responseSource else { return } guard let source = self.responseSource else { return }
var socketAddress = sockaddr_storage() var socketAddress = sockaddr_storage()
var socketAddressLength = socklen_t(MemoryLayout<sockaddr_storage>.size) var socketAddressLength = socklen_t(MemoryLayout<sockaddr_storage>.size)
let response = [UInt8](repeating: 0, count: 4096) let response = [UInt8](repeating: 0, count: 4096)
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 {
guard bytesRead > 0 else { guard bytesRead > 0 else {
self.closeConnection() self.closeConnection()
if bytesRead == 0 { if bytesRead == 0 {
debugPrint("recvfrom returned EOF") debugPrint("recvfrom returned EOF")
throw ConnectionError.receivedEndOfFile throw ConnectionError.receivedEndOfFile
} else { } else {
if let errorString = String(validatingUTF8: strerror(errno)) { if let errorString = String(validatingUTF8: strerror(errno)) {
debugPrint("recvfrom failed: \(errorString)") debugPrint("recvfrom failed: \(errorString)")
} }
throw ConnectionError.receiveFailed(code: errno) throw ConnectionError.receiveFailed(code: errno)
} }
} }
guard let endpoint = withUnsafePointer(to: &socketAddress, { self.getEndpointFromSocketAddress(socketAddressPointer: UnsafeRawPointer($0).bindMemory(to: sockaddr.self, capacity: 1)) }) guard let endpoint = withUnsafePointer(to: &socketAddress,
else { {
// debugPrint("Failed to get the address and port from the socket address received from recvfrom") self
self.closeConnection() .getEndpointFromSocketAddress(socketAddressPointer: UnsafeRawPointer($0)
return .bindMemory(to: sockaddr.self, capacity: 1)) })
} else {
// debugPrint("Failed to get the address and port from the socket address received from recvfrom")
self.closeConnection()
return
}
// debugPrint("UDP connection received \(bytesRead) bytes from \(endpoint.host):\(endpoint.port)") // debugPrint("UDP connection received \(bytesRead) bytes from \(endpoint.host):\(endpoint.port)")
let responseBytes = Data(response[0..<bytesRead]) let responseBytes = Data(response[0 ..< bytesRead])
// Handle response // Handle response
self.handler?(endpoint.host, endpoint.port, responseBytes) self.handler?(endpoint.host, endpoint.port, responseBytes)
} catch { } catch {
if let error = error as? ConnectionError { if let error = error as? ConnectionError {
self.errorHandler?(error) self.errorHandler?(error)
} else { } else {
self.errorHandler?(ConnectionError.underlying(error: error)) self.errorHandler?(ConnectionError.underlying(error: error))
} }
} }
}
} newResponseSource.resume()
responseSource = newResponseSource
}
newResponseSource.resume() /// Send broadcast message.
responseSource = newResponseSource ///
} /// - Parameter message: Message to send via broadcast.
/// - Throws: Throws a `ConnectionError` if an error occurs.
open func sendBroadcast(_ message: String) throws {
guard let data = message.data(using: .utf8) else { throw ConnectionError.messageEncodingFailed }
try sendBroadcast(data)
}
/// Send broadcast message. /// Send broadcast data.
/// ///
/// - Parameter message: Message to send via broadcast. /// - Parameter data: Data to send via broadcast.
/// - Throws: Throws a `ConnectionError` if an error occurs. /// - Throws: Throws a `ConnectionError` if an error occurs.
open func sendBroadcast(_ message: String) throws { open func sendBroadcast(_ data: Data) throws {
guard let data = message.data(using: .utf8) else { throw ConnectionError.messageEncodingFailed } if responseSource == nil {
try sendBroadcast(data) try createSocket()
} }
/// Send broadcast data. guard let source = responseSource else { return }
/// let UDPSocket = Int32(source.handle)
/// - Parameter data: Data to send via broadcast. let socketLength = socklen_t(address.sin_len)
/// - Throws: Throws a `ConnectionError` if an error occurs. try data.withUnsafeBytes { broadcastMessage in
open func sendBroadcast(_ data: Data) throws { let broadcastMessageLength = data.count
if responseSource == nil { let sent = withUnsafeMutablePointer(to: &address) { pointer -> Int in
try createSocket() let memory = UnsafeRawPointer(pointer).bindMemory(to: sockaddr.self, capacity: 1)
} return sendto(UDPSocket, broadcastMessage.baseAddress, broadcastMessageLength, 0, memory, socketLength)
}
guard let source = responseSource else { return } guard sent > 0 else {
let UDPSocket = Int32(source.handle) closeConnection()
let socketLength = socklen_t(address.sin_len) throw ConnectionError.sendingMessageFailed(code: errno)
try data.withUnsafeBytes { (broadcastMessage) in }
let broadcastMessageLength = data.count }
let sent = withUnsafeMutablePointer(to: &address) { pointer -> Int in }
let memory = UnsafeRawPointer(pointer).bindMemory(to: sockaddr.self, capacity: 1)
return sendto(UDPSocket, broadcastMessage.baseAddress, broadcastMessageLength, 0, memory, socketLength)
}
guard sent > 0 else { /// Close the connection.
closeConnection() ///
throw ConnectionError.sendingMessageFailed(code: errno) /// - Parameter reopen: Automatically reopens the connection if true. Defaults to true.
} open func closeConnection(reopen: Bool = true) {
} if let source = responseSource {
} source.cancel()
responseSource = nil
}
if shouldBeBound && reopen {
dispatchQueue.async {
do {
try self.createSocket()
} catch {
self.errorHandler?(ConnectionError.reopeningSocketFailed(error: error))
}
}
}
}
/// Close the connection. // MARK: - Helper
///
/// - Parameter reopen: Automatically reopens the connection if true. Defaults to true.
open func closeConnection(reopen: Bool = true) {
if let source = responseSource {
source.cancel()
responseSource = nil
}
if shouldBeBound && reopen {
dispatchQueue.async {
do {
try self.createSocket()
} catch {
self.errorHandler?(ConnectionError.reopeningSocketFailed(error: error))
}
}
}
}
// MARK: - Helper /// Convert a sockaddr structure into an IP address string and port.
///
/// - Parameter socketAddressPointer: socketAddressPointer: Pointer to a socket address.
/// - Returns: Returns a tuple of the host IP address and the port in the socket address given.
func getEndpointFromSocketAddress(socketAddressPointer: UnsafePointer<sockaddr>) -> (host: String, port: Int)? {
let socketAddress = UnsafePointer<sockaddr>(socketAddressPointer).pointee
/// Convert a sockaddr structure into an IP address string and port. switch Int32(socketAddress.sa_family) {
/// case AF_INET:
/// - Parameter socketAddressPointer: socketAddressPointer: Pointer to a socket address. var socketAddressInet = UnsafeRawPointer(socketAddressPointer).load(as: sockaddr_in.self)
/// - Returns: Returns a tuple of the host IP address and the port in the socket address given. let length = Int(INET_ADDRSTRLEN) + 2
func getEndpointFromSocketAddress(socketAddressPointer: UnsafePointer<sockaddr>) -> (host: String, port: Int)? { var buffer = [CChar](repeating: 0, count: length)
let socketAddress = UnsafePointer<sockaddr>(socketAddressPointer).pointee let hostCString = inet_ntop(AF_INET, &socketAddressInet.sin_addr, &buffer, socklen_t(length))
let port = Int(UInt16(socketAddressInet.sin_port).byteSwapped)
return (String(cString: hostCString!), port)
switch Int32(socketAddress.sa_family) { case AF_INET6:
case AF_INET: var socketAddressInet6 = UnsafeRawPointer(socketAddressPointer).load(as: sockaddr_in6.self)
var socketAddressInet = UnsafeRawPointer(socketAddressPointer).load(as: sockaddr_in.self) let length = Int(INET6_ADDRSTRLEN) + 2
let length = Int(INET_ADDRSTRLEN) + 2 var buffer = [CChar](repeating: 0, count: length)
var buffer = [CChar](repeating: 0, count: length) let hostCString = inet_ntop(AF_INET6, &socketAddressInet6.sin6_addr, &buffer, socklen_t(length))
let hostCString = inet_ntop(AF_INET, &socketAddressInet.sin_addr, &buffer, socklen_t(length)) let port = Int(UInt16(socketAddressInet6.sin6_port).byteSwapped)
let port = Int(UInt16(socketAddressInet.sin_port).byteSwapped) return (String(cString: hostCString!), port)
return (String(cString: hostCString!), port)
case AF_INET6: default:
var socketAddressInet6 = UnsafeRawPointer(socketAddressPointer).load(as: sockaddr_in6.self) return nil
let length = Int(INET6_ADDRSTRLEN) + 2 }
var buffer = [CChar](repeating: 0, count: length) }
let hostCString = inet_ntop(AF_INET6, &socketAddressInet6.sin6_addr, &buffer, socklen_t(length))
let port = Int(UInt16(socketAddressInet6.sin6_port).byteSwapped)
return (String(cString: hostCString!), port)
default: // MARK: - Private
return nil
}
}
// MARK: - Private /// Prevents crashes when blocking calls are pending and the app is paused (via Home button).
///
/// - Parameter socket: The socket for which the signal should be disabled.
fileprivate func setNoSigPipe(socket: CInt) {
var no_sig_pipe: Int32 = 1
setsockopt(socket, SOL_SOCKET, SO_NOSIGPIPE, &no_sig_pipe, socklen_t(MemoryLayout<Int32>.size))
}
/// Prevents crashes when blocking calls are pending and the app is paused (via Home button). fileprivate class func htonsPort(port: in_port_t) -> in_port_t {
/// let isLittleEndian = Int(OSHostByteOrder()) == OSLittleEndian
/// - Parameter socket: The socket for which the signal should be disabled. return isLittleEndian ? _OSSwapInt16(port) : port
fileprivate func setNoSigPipe(socket: CInt) { }
var no_sig_pipe: Int32 = 1
setsockopt(socket, SOL_SOCKET, SO_NOSIGPIPE, &no_sig_pipe, socklen_t(MemoryLayout<Int32>.size))
}
fileprivate class func htonsPort(port: in_port_t) -> in_port_t {
let isLittleEndian = Int(OSHostByteOrder()) == OSLittleEndian
return isLittleEndian ? _OSSwapInt16(port) : port
}
fileprivate class func ntohs(value: CUnsignedShort) -> CUnsignedShort {
return (value << 8) + (value >> 8)
}
fileprivate class func ntohs(value: CUnsignedShort) -> CUnsignedShort {
(value << 8) + (value >> 8)
}
} }
// Created by Gunter Hager on 25.03.19. // Created by Gunter Hager on 25.03.19.
@ -292,25 +291,24 @@ open class UDPBroadcastConnection {
// //
public extension UDPBroadcastConnection { public extension UDPBroadcastConnection {
enum ConnectionError: Error { enum ConnectionError: Error {
// Creating socket // Creating socket
case createSocketFailed case createSocketFailed
case enableBroadcastFailed case enableBroadcastFailed
case bindSocketFailed case bindSocketFailed
// Sending message // Sending message
case messageEncodingFailed case messageEncodingFailed
case sendingMessageFailed(code: Int32) case sendingMessageFailed(code: Int32)
// Receiving data // Receiving data
case receivedEndOfFile case receivedEndOfFile
case receiveFailed(code: Int32) case receiveFailed(code: Int32)
// Closing socket // Closing socket
case reopeningSocketFailed(error: Error) case reopeningSocketFailed(error: Error)
// Underlying
case underlying(error: Error)
}
// Underlying
case underlying(error: Error)
}
} }

View File

@ -1,36 +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
final class BackgroundManager { final class BackgroundManager {
static let current = BackgroundManager() static let current = BackgroundManager()
fileprivate(set) var backgroundURL: URL? fileprivate(set) var backgroundURL: URL?
fileprivate(set) var blurhash: String = "001fC^" fileprivate(set) var blurhash: String = "001fC^"
init() { init() {
backgroundURL = nil backgroundURL = nil
} }
func setBackground(to: URL, hash: String) { func setBackground(to: URL, hash: String) {
self.backgroundURL = to self.backgroundURL = to
self.blurhash = hash self.blurhash = hash
let nc = NotificationCenter.default let nc = NotificationCenter.default
nc.post(name: Notification.Name("backgroundDidChange"), object: nil) nc.post(name: Notification.Name("backgroundDidChange"), object: nil)
} }
func clearBackground() { func clearBackground() {
self.backgroundURL = nil self.backgroundURL = nil
self.blurhash = "001fC^" self.blurhash = "001fC^"
let nc = NotificationCenter.default let nc = NotificationCenter.default
nc.post(name: Notification.Name("backgroundDidChange"), object: nil) nc.post(name: Notification.Name("backgroundDidChange"), object: nil)
} }
} }

View File

@ -1,56 +1,56 @@
// //
/* // 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
class LogManager { class LogManager {
static let shared = LogManager() static let shared = LogManager()
let log = Puppy() let log = Puppy()
init() { init() {
let console = ConsoleLogger("com.swiftfin.ConsoleLogger") let console = ConsoleLogger("com.swiftfin.ConsoleLogger")
let fileURL = self.getDocumentsDirectory().appendingPathComponent("logs.txt") let fileURL = self.getDocumentsDirectory().appendingPathComponent("logs.txt")
let FM = FileManager() let FM = FileManager()
_ = try? FM.removeItem(at: fileURL) _ = try? FM.removeItem(at: fileURL)
do { do {
let file = try FileLogger("com.swiftfin", fileURL: fileURL) let file = try FileLogger("com.swiftfin", fileURL: fileURL)
file.format = LogFormatter() file.format = LogFormatter()
log.add(file, withLevel: .debug) log.add(file, withLevel: .debug)
} catch let err { } catch let err {
log.error("Couldn't initialize file logger.") log.error("Couldn't initialize file logger.")
print(err) print(err)
} }
console.format = LogFormatter() console.format = LogFormatter()
log.add(console, withLevel: .debug) log.add(console, withLevel: .debug)
log.info("Logger initialized.") log.info("Logger initialized.")
} }
func logFileURL() -> URL { func logFileURL() -> URL {
return self.getDocumentsDirectory().appendingPathComponent("logs.txt") self.getDocumentsDirectory().appendingPathComponent("logs.txt")
} }
func getDocumentsDirectory() -> URL { func getDocumentsDirectory() -> URL {
// find all possible documents directories for this user // find all possible documents directories for this user
let paths = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask) let paths = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask)
// just send back the first one, which ought to be the only one // just send back the first one, which ought to be the only one
return paths[0] return paths[0]
} }
} }
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: "") {
return " [\(level.emoji) \(level)] \(file)#\(line):\(function) \(message)" let file = shortFileName(file).replacingOccurrences(of: ".swift", with: "")
} 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,299 +17,328 @@ 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
private(set) var currentLogin: CurrentLogin!
// MARK: main private(set) var currentLogin: CurrentLogin!
static let main = SessionManager()
// MARK: init // MARK: main
private init() {
if let lastUserID = Defaults[.lastServerUserID],
let user = try? SwiftfinStore.dataStack.fetchOne(From<SwiftfinStore.Models.StoredUser>(),
[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?") } static let main = SessionManager()
guard let existingServer = SwiftfinStore.dataStack.fetchExisting(server) else { return }
JellyfinAPI.basePath = server.currentURI // MARK: init
setAuthHeader(with: accessToken.value)
currentLogin = (server: existingServer.state, user: user.state)
}
}
// MARK: fetchServers private init() {
func fetchServers() -> [SwiftfinStore.State.Server] { if let lastUserID = Defaults[.lastServerUserID],
let servers = try! SwiftfinStore.dataStack.fetchAll(From<SwiftfinStore.Models.StoredServer>()) let user = try? SwiftfinStore.dataStack.fetchOne(From<SwiftfinStore.Models.StoredUser>(),
return servers.map({ $0.state }) [Where<SwiftfinStore.Models.StoredUser>("id == %@", lastUserID)])
} {
// MARK: fetchUsers guard let server = user.server,
func fetchUsers(for server: SwiftfinStore.State.Server) -> [SwiftfinStore.State.User] { let accessToken = user.accessToken else { fatalError("No associated server or access token for last user?") }
guard let storedServer = try? SwiftfinStore.dataStack.fetchOne(From<SwiftfinStore.Models.StoredServer>(), guard let existingServer = SwiftfinStore.dataStack.fetchExisting(server) else { return }
Where<SwiftfinStore.Models.StoredServer>("id == %@", server.id))
else { fatalError("No stored server associated with given state server?") }
return storedServer.users.map({ $0.state }).sorted(by: { $0.username < $1.username })
}
// MARK: connectToServer publisher JellyfinAPI.basePath = server.currentURI
// Connects to a server at the given uri, storing if successful setAuthHeader(with: accessToken.value)
func connectToServer(with uri: String) -> AnyPublisher<SwiftfinStore.State.Server, Error> { currentLogin = (server: existingServer.state, user: user.state)
var uriComponents = URLComponents(string: uri) ?? URLComponents() }
}
if uriComponents.scheme == nil { // MARK: fetchServers
uriComponents.scheme = Defaults[.defaultHTTPScheme].rawValue
}
var uri = uriComponents.string ?? "" func fetchServers() -> [SwiftfinStore.State.Server] {
let servers = try! SwiftfinStore.dataStack.fetchAll(From<SwiftfinStore.Models.StoredServer>())
return servers.map(\.state)
}
if uri.last == "/" { // MARK: fetchUsers
uri = String(uri.dropLast())
}
JellyfinAPI.basePath = uri func fetchUsers(for server: SwiftfinStore.State.Server) -> [SwiftfinStore.State.User] {
guard let storedServer = try? SwiftfinStore.dataStack.fetchOne(From<SwiftfinStore.Models.StoredServer>(),
Where<SwiftfinStore.Models.StoredServer>("id == %@", server.id))
else { fatalError("No stored server associated with given state server?") }
return storedServer.users.map(\.state).sorted(by: { $0.username < $1.username })
}
return SystemAPI.getPublicSystemInfo() // MARK: connectToServer publisher
.tryMap({ response -> (SwiftfinStore.Models.StoredServer, UnsafeDataTransaction) in
let transaction = SwiftfinStore.dataStack.beginUnsafe() // Connects to a server at the given uri, storing if successful
let newServer = transaction.create(Into<SwiftfinStore.Models.StoredServer>()) func connectToServer(with uri: String) -> AnyPublisher<SwiftfinStore.State.Server, Error> {
var uriComponents = URLComponents(string: uri) ?? URLComponents()
guard let name = response.serverName, if uriComponents.scheme == nil {
let id = response.id, uriComponents.scheme = Defaults[.defaultHTTPScheme].rawValue
let os = response.operatingSystem, }
let version = response.version else { throw JellyfinAPIError("Missing server data from network call") }
newServer.uris = [uri] var uri = uriComponents.string ?? ""
newServer.currentURI = uri
newServer.name = name
newServer.id = id
newServer.os = os
newServer.version = version
newServer.users = []
// Check for existing server on device if uri.last == "/" {
if let existingServer = try? SwiftfinStore.dataStack.fetchOne(From<SwiftfinStore.Models.StoredServer>(), uri = String(uri.dropLast())
[Where<SwiftfinStore.Models.StoredServer>("id == %@", newServer.id)]) { }
throw SwiftfinStore.Errors.existingServer(existingServer.state)
}
return (newServer, transaction) JellyfinAPI.basePath = uri
})
.handleEvents(receiveOutput: { (_, transaction) in
try? transaction.commitAndWait()
})
.map({ (server, _) in
return server.state
})
.eraseToAnyPublisher()
}
// MARK: addURIToServer publisher return SystemAPI.getPublicSystemInfo()
func addURIToServer(server: SwiftfinStore.State.Server, uri: String) -> AnyPublisher<SwiftfinStore.State.Server, Error> { .tryMap { response -> (SwiftfinStore.Models.StoredServer, UnsafeDataTransaction) in
return Just(server)
.tryMap { server -> (SwiftfinStore.Models.StoredServer, UnsafeDataTransaction) in
let transaction = SwiftfinStore.dataStack.beginUnsafe() let transaction = SwiftfinStore.dataStack.beginUnsafe()
let newServer = transaction.create(Into<SwiftfinStore.Models.StoredServer>())
guard let existingServer = try? SwiftfinStore.dataStack.fetchOne(From<SwiftfinStore.Models.StoredServer>(), guard let name = response.serverName,
[Where<SwiftfinStore.Models.StoredServer>("id == %@", server.id)]) else { let id = response.id,
fatalError("No stored server associated with given state server?") let os = response.operatingSystem,
} let version = response.version else { throw JellyfinAPIError("Missing server data from network call") }
guard let editServer = transaction.edit(existingServer) else { fatalError("Can't get proxy for existing object?") } newServer.uris = [uri]
editServer.uris.insert(uri) newServer.currentURI = uri
newServer.name = name
newServer.id = id
newServer.os = os
newServer.version = version
newServer.users = []
return (editServer, transaction) // Check for existing server on device
} if let existingServer = try? SwiftfinStore.dataStack.fetchOne(From<SwiftfinStore.Models.StoredServer>(),
.handleEvents(receiveOutput: { (_, transaction) in [Where<SwiftfinStore.Models.StoredServer>("id == %@",
try? transaction.commitAndWait() newServer.id)])
}) {
.map({ (server, _) in throw SwiftfinStore.Errors.existingServer(existingServer.state)
return server.state }
})
.eraseToAnyPublisher()
}
// MARK: setServerCurrentURI publisher return (newServer, transaction)
func setServerCurrentURI(server: SwiftfinStore.State.Server, uri: String) -> AnyPublisher<SwiftfinStore.State.Server, Error> { }
return Just(server) .handleEvents(receiveOutput: { _, transaction in
.tryMap { server -> (SwiftfinStore.Models.StoredServer, UnsafeDataTransaction) in try? transaction.commitAndWait()
})
.map { server, _ in
server.state
}
.eraseToAnyPublisher()
}
let transaction = SwiftfinStore.dataStack.beginUnsafe() // MARK: addURIToServer publisher
guard let existingServer = try? SwiftfinStore.dataStack.fetchOne(From<SwiftfinStore.Models.StoredServer>(), func addURIToServer(server: SwiftfinStore.State.Server, uri: String) -> AnyPublisher<SwiftfinStore.State.Server, Error> {
[Where<SwiftfinStore.Models.StoredServer>("id == %@", server.id)]) else { Just(server)
fatalError("No stored server associated with given state server?") .tryMap { server -> (SwiftfinStore.Models.StoredServer, UnsafeDataTransaction) in
}
if !existingServer.uris.contains(uri) { let transaction = SwiftfinStore.dataStack.beginUnsafe()
fatalError("Attempting to set current uri while server doesn't contain it?")
}
guard let editServer = transaction.edit(existingServer) else { fatalError("Can't get proxy for existing object?") } guard let existingServer = try? SwiftfinStore.dataStack.fetchOne(From<SwiftfinStore.Models.StoredServer>(),
editServer.currentURI = uri [Where<SwiftfinStore.Models.StoredServer>("id == %@",
server.id)])
else {
fatalError("No stored server associated with given state server?")
}
return (editServer, transaction) guard let editServer = transaction.edit(existingServer) else { fatalError("Can't get proxy for existing object?") }
} editServer.uris.insert(uri)
.handleEvents(receiveOutput: { (_, transaction) in
try? transaction.commitAndWait()
})
.map({ (server, _) in
return server.state
})
.eraseToAnyPublisher()
}
// MARK: loginUser publisher return (editServer, transaction)
// 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> { .handleEvents(receiveOutput: { _, transaction in
setAuthHeader(with: "") try? transaction.commitAndWait()
})
.map { server, _ in
server.state
}
.eraseToAnyPublisher()
}
JellyfinAPI.basePath = server.currentURI // MARK: setServerCurrentURI publisher
return UserAPI.authenticateUserByName(authenticateUserByName: AuthenticateUserByName(username: username, pw: password)) func setServerCurrentURI(server: SwiftfinStore.State.Server, uri: String) -> AnyPublisher<SwiftfinStore.State.Server, Error> {
.tryMap({ response -> (SwiftfinStore.Models.StoredServer, SwiftfinStore.Models.StoredUser, UnsafeDataTransaction) in Just(server)
.tryMap { server -> (SwiftfinStore.Models.StoredServer, UnsafeDataTransaction) in
guard let accessToken = response.accessToken else { throw JellyfinAPIError("Access token missing from network call") } let transaction = SwiftfinStore.dataStack.beginUnsafe()
let transaction = SwiftfinStore.dataStack.beginUnsafe() guard let existingServer = try? SwiftfinStore.dataStack.fetchOne(From<SwiftfinStore.Models.StoredServer>(),
let newUser = transaction.create(Into<SwiftfinStore.Models.StoredUser>()) [Where<SwiftfinStore.Models.StoredServer>("id == %@",
server.id)])
else {
fatalError("No stored server associated with given state server?")
}
guard let username = response.user?.name, if !existingServer.uris.contains(uri) {
let id = response.user?.id else { throw JellyfinAPIError("Missing user data from network call") } fatalError("Attempting to set current uri while server doesn't contain it?")
}
newUser.username = username
newUser.id = id
newUser.appleTVID = ""
// Check for existing user on device guard let editServer = transaction.edit(existingServer) else { fatalError("Can't get proxy for existing object?") }
if let existingUser = try? SwiftfinStore.dataStack.fetchOne(From<SwiftfinStore.Models.StoredUser>(), editServer.currentURI = uri
[Where<SwiftfinStore.Models.StoredUser>("id == %@", newUser.id)]) {
throw SwiftfinStore.Errors.existingUser(existingUser.state)
}
let newAccessToken = transaction.create(Into<SwiftfinStore.Models.StoredAccessToken>()) return (editServer, transaction)
newAccessToken.value = accessToken }
newUser.accessToken = newAccessToken .handleEvents(receiveOutput: { _, transaction in
try? transaction.commitAndWait()
})
.map { server, _ in
server.state
}
.eraseToAnyPublisher()
}
guard let userServer = try? SwiftfinStore.dataStack.fetchOne(From<SwiftfinStore.Models.StoredServer>(), // MARK: loginUser publisher
[Where<SwiftfinStore.Models.StoredServer>("id == %@", server.id)])
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?") } // Logs in a user with an associated server, storing if successful
editUserServer.users.insert(newUser) func loginUser(server: SwiftfinStore.State.Server, username: String,
password: String) -> AnyPublisher<SwiftfinStore.State.User, Error>
{
setAuthHeader(with: "")
return (editUserServer, newUser, transaction) JellyfinAPI.basePath = server.currentURI
})
.handleEvents(receiveOutput: { [unowned self] (server, user, transaction) in
setAuthHeader(with: user.accessToken?.value ?? "")
try? transaction.commitAndWait()
// Fetch for the right queue return UserAPI.authenticateUserByName(authenticateUserByName: AuthenticateUserByName(username: username, pw: password))
let currentServer = SwiftfinStore.dataStack.fetchExisting(server)! .tryMap { response -> (SwiftfinStore.Models.StoredServer, SwiftfinStore.Models.StoredUser, UnsafeDataTransaction) in
let currentUser = SwiftfinStore.dataStack.fetchExisting(user)!
Defaults[.lastServerUserID] = user.id guard let accessToken = response.accessToken else { throw JellyfinAPIError("Access token missing from network call") }
currentLogin = (server: currentServer.state, user: currentUser.state) let transaction = SwiftfinStore.dataStack.beginUnsafe()
SwiftfinNotificationCenter.main.post(name: SwiftfinNotificationCenter.Keys.didSignIn, object: nil) let newUser = transaction.create(Into<SwiftfinStore.Models.StoredUser>())
})
.map({ (_, user, _) in
return user.state
})
.eraseToAnyPublisher()
}
// MARK: loginUser guard let username = response.user?.name,
func loginUser(server: SwiftfinStore.State.Server, user: SwiftfinStore.State.User) { let id = response.user?.id else { throw JellyfinAPIError("Missing user data from network call") }
JellyfinAPI.basePath = server.currentURI
Defaults[.lastServerUserID] = user.id
setAuthHeader(with: user.accessToken)
currentLogin = (server: server, user: user)
SwiftfinNotificationCenter.main.post(name: SwiftfinNotificationCenter.Keys.didSignIn, object: nil)
}
// MARK: logout newUser.username = username
func logout() { newUser.id = id
currentLogin = nil newUser.appleTVID = ""
JellyfinAPI.basePath = ""
setAuthHeader(with: "")
Defaults[.lastServerUserID] = nil
SwiftfinNotificationCenter.main.post(name: SwiftfinNotificationCenter.Keys.didSignOut, object: nil)
}
// MARK: purge // Check for existing user on device
func purge() { if let existingUser = try? SwiftfinStore.dataStack.fetchOne(From<SwiftfinStore.Models.StoredUser>(),
// Delete all servers [Where<SwiftfinStore.Models.StoredUser>("id == %@",
let servers = fetchServers() newUser.id)])
{
throw SwiftfinStore.Errors.existingUser(existingUser.state)
}
for server in servers { let newAccessToken = transaction.create(Into<SwiftfinStore.Models.StoredAccessToken>())
delete(server: server) newAccessToken.value = accessToken
} newUser.accessToken = newAccessToken
SwiftfinNotificationCenter.main.post(name: SwiftfinNotificationCenter.Keys.didPurge, object: nil) guard let userServer = try? SwiftfinStore.dataStack.fetchOne(From<SwiftfinStore.Models.StoredServer>(),
} [
Where<SwiftfinStore.Models.StoredServer>("id == %@",
server.id),
])
else { fatalError("No stored server associated with given state server?") }
// MARK: delete user guard let editUserServer = transaction.edit(userServer) else { fatalError("Can't get proxy for existing object?") }
func delete(user: SwiftfinStore.State.User) { editUserServer.users.insert(newUser)
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?")}
_delete(user: storedUser, transaction: nil)
}
// MARK: delete server return (editUserServer, newUser, transaction)
func delete(server: SwiftfinStore.State.Server) { }
guard let storedServer = try? SwiftfinStore.dataStack.fetchOne(From<SwiftfinStore.Models.StoredServer>(), .handleEvents(receiveOutput: { [unowned self] server, user, transaction in
[Where<SwiftfinStore.Models.StoredServer>("id == %@", server.id)]) else { fatalError("No stored server for state server?")} setAuthHeader(with: user.accessToken?.value ?? "")
_delete(server: storedServer, transaction: nil) try? transaction.commitAndWait()
}
private func _delete(user: SwiftfinStore.Models.StoredUser, transaction: UnsafeDataTransaction?) { // Fetch for the right queue
guard let storedAccessToken = user.accessToken else { fatalError("No access token for stored user?")} let currentServer = SwiftfinStore.dataStack.fetchExisting(server)!
let currentUser = SwiftfinStore.dataStack.fetchExisting(user)!
let transaction = transaction == nil ? SwiftfinStore.dataStack.beginUnsafe() : transaction! Defaults[.lastServerUserID] = user.id
transaction.delete(storedAccessToken)
transaction.delete(user)
try? transaction.commitAndWait()
}
private func _delete(server: SwiftfinStore.Models.StoredServer, transaction: UnsafeDataTransaction?) { currentLogin = (server: currentServer.state, user: currentUser.state)
let transaction = transaction == nil ? SwiftfinStore.dataStack.beginUnsafe() : transaction! SwiftfinNotificationCenter.main.post(name: SwiftfinNotificationCenter.Keys.didSignIn, object: nil)
})
.map { _, user, _ in
user.state
}
.eraseToAnyPublisher()
}
for user in server.users { // MARK: loginUser
_delete(user: user, transaction: transaction)
}
transaction.delete(server) func loginUser(server: SwiftfinStore.State.Server, user: SwiftfinStore.State.User) {
try? transaction.commitAndWait() JellyfinAPI.basePath = server.currentURI
} Defaults[.lastServerUserID] = user.id
setAuthHeader(with: user.accessToken)
currentLogin = (server: server, user: user)
SwiftfinNotificationCenter.main.post(name: SwiftfinNotificationCenter.Keys.didSignIn, object: nil)
}
private func setAuthHeader(with accessToken: String) { // MARK: logout
let appVersion = Bundle.main.infoDictionary?["CFBundleShortVersionString"] as? String
var deviceName = UIDevice.current.name
deviceName = deviceName.folding(options: .diacriticInsensitive, locale: .current)
deviceName = String(deviceName.unicodeScalars.filter { CharacterSet.urlQueryAllowed.contains($0) })
let platform: String func logout() {
#if os(tvOS) currentLogin = nil
platform = "tvOS" JellyfinAPI.basePath = ""
#else setAuthHeader(with: "")
platform = "iOS" Defaults[.lastServerUserID] = nil
#endif SwiftfinNotificationCenter.main.post(name: SwiftfinNotificationCenter.Keys.didSignOut, object: nil)
}
var header = "MediaBrowser " // MARK: purge
header.append("Client=\"Jellyfin \(platform)\", ")
header.append("Device=\"\(deviceName)\", ")
header.append("DeviceId=\"\(platform)_\(UIDevice.vendorUUIDString)_\(String(Date().timeIntervalSince1970))\", ")
header.append("Version=\"\(appVersion ?? "0.0.1")\", ")
header.append("Token=\"\(accessToken)\"")
JellyfinAPI.customHeaders["X-Emby-Authorization"] = header func purge() {
} // Delete all servers
let servers = fetchServers()
for server in servers {
delete(server: server)
}
SwiftfinNotificationCenter.main.post(name: SwiftfinNotificationCenter.Keys.didPurge, object: nil)
}
// MARK: delete user
func delete(user: SwiftfinStore.State.User) {
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?") }
_delete(user: storedUser, transaction: nil)
}
// MARK: delete server
func delete(server: SwiftfinStore.State.Server) {
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?") }
_delete(server: storedServer, transaction: nil)
}
private func _delete(user: SwiftfinStore.Models.StoredUser, transaction: UnsafeDataTransaction?) {
guard let storedAccessToken = user.accessToken else { fatalError("No access token for stored user?") }
let transaction = transaction == nil ? SwiftfinStore.dataStack.beginUnsafe() : transaction!
transaction.delete(storedAccessToken)
transaction.delete(user)
try? transaction.commitAndWait()
}
private func _delete(server: SwiftfinStore.Models.StoredServer, transaction: UnsafeDataTransaction?) {
let transaction = transaction == nil ? SwiftfinStore.dataStack.beginUnsafe() : transaction!
for user in server.users {
_delete(user: user, transaction: transaction)
}
transaction.delete(server)
try? transaction.commitAndWait()
}
private func setAuthHeader(with accessToken: String) {
let appVersion = Bundle.main.infoDictionary?["CFBundleShortVersionString"] as? String
var deviceName = UIDevice.current.name
deviceName = deviceName.folding(options: .diacriticInsensitive, locale: .current)
deviceName = String(deviceName.unicodeScalars.filter { CharacterSet.urlQueryAllowed.contains($0) })
let platform: String
#if os(tvOS)
platform = "tvOS"
#else
platform = "iOS"
#endif
var header = "MediaBrowser "
header.append("Client=\"Jellyfin \(platform)\", ")
header.append("Device=\"\(deviceName)\", ")
header.append("DeviceId=\"\(platform)_\(UIDevice.vendorUUIDString)_\(String(Date().timeIntervalSince1970))\", ")
header.append("Version=\"\(appVersion ?? "0.0.1")\", ")
header.append("Token=\"\(accessToken)\"")
JellyfinAPI.customHeaders["X-Emby-Authorization"] = header
}
} }

View File

@ -1,25 +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
enum SwiftfinNotificationCenter { enum SwiftfinNotificationCenter {
static let main: NotificationCenter = { static let main: NotificationCenter = {
return NotificationCenter() NotificationCenter()
}() }()
enum Keys { enum Keys {
static let didSignIn = Notification.Name("didSignIn") static let didSignIn = Notification.Name("didSignIn")
static let didSignOut = Notification.Name("didSignOut") static let didSignOut = Notification.Name("didSignOut")
static let processDeepLink = Notification.Name("processDeepLink") static let processDeepLink = Notification.Name("processDeepLink")
static let didPurge = Notification.Name("didPurge") static let didPurge = Notification.Name("didPurge")
static let didChangeServerCurrentURI = Notification.Name("didChangeCurrentLoginURI") static let didChangeServerCurrentURI = Notification.Name("didChangeCurrentLoginURI")
} }
} }

View File

@ -1,198 +1,215 @@
// //
/* // 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
// Relationships are represented by the related object's IDs or value
enum State {
struct Server { // Safe, copyable representations of their underlying CoreStoredObject
let uris: Set<String> // Relationships are represented by the related object's IDs or value
let currentURI: String enum State {
let name: String
let id: String
let os: String
let version: String
let userIDs: [String]
fileprivate init(uris: Set<String>, currentURI: String, name: String, id: String, os: String, version: String, usersIDs: [String]) { struct Server {
self.uris = uris let uris: Set<String>
self.currentURI = currentURI let currentURI: String
self.name = name let name: String
self.id = id let id: String
self.os = os let os: String
self.version = version let version: String
self.userIDs = usersIDs let userIDs: [String]
}
static var sample: Server { fileprivate init(uris: Set<String>, currentURI: String, name: String, id: String, os: String, version: String,
return Server(uris: ["https://www.notaurl.com", "http://www.maybeaurl.org"], usersIDs: [String])
currentURI: "https://www.notaurl.com", {
name: "Johnny's Tree", self.uris = uris
id: "123abc", self.currentURI = currentURI
os: "macOS", self.name = name
version: "1.1.1", self.id = id
usersIDs: ["1", "2"]) self.os = os
} self.version = version
} self.userIDs = usersIDs
}
struct User { static var sample: Server {
let username: String Server(uris: ["https://www.notaurl.com", "http://www.maybeaurl.org"],
let id: String currentURI: "https://www.notaurl.com",
let serverID: String name: "Johnny's Tree",
let accessToken: String id: "123abc",
os: "macOS",
version: "1.1.1",
usersIDs: ["1", "2"])
}
}
fileprivate init(username: String, id: String, serverID: String, accessToken: String) { struct User {
self.username = username let username: String
self.id = id let id: String
self.serverID = serverID let serverID: String
self.accessToken = accessToken let accessToken: String
}
static var sample: User { fileprivate init(username: String, id: String, serverID: String, accessToken: String) {
return User(username: "JohnnyAppleseed", self.username = username
id: "123abc", self.id = id
serverID: "123abc", self.serverID = serverID
accessToken: "open-sesame") self.accessToken = accessToken
} }
}
}
// MARK: Models static var sample: User {
enum Models { User(username: "JohnnyAppleseed",
id: "123abc",
serverID: "123abc",
accessToken: "open-sesame")
}
}
}
final class StoredServer: CoreStoreObject { // MARK: Models
@Field.Coded("uris", coder: FieldCoders.Json.self) enum Models {
var uris: Set<String> = []
@Field.Stored("currentURI") final class StoredServer: CoreStoreObject {
var currentURI: String = ""
@Field.Stored("name") @Field.Coded("uris", coder: FieldCoders.Json.self)
var name: String = "" var uris: Set<String> = []
@Field.Stored("id") @Field.Stored("currentURI")
var id: String = "" var currentURI: String = ""
@Field.Stored("os") @Field.Stored("name")
var os: String = "" var name: String = ""
@Field.Stored("version") @Field.Stored("id")
var version: String = "" var id: String = ""
@Field.Relationship("users", inverse: \StoredUser.$server) @Field.Stored("os")
var users: Set<StoredUser> var os: String = ""
var state: State.Server { @Field.Stored("version")
return State.Server(uris: uris, var version: String = ""
currentURI: currentURI,
name: name,
id: id,
os: os,
version: version,
usersIDs: users.map({ $0.id }))
}
}
final class StoredUser: CoreStoreObject { @Field.Relationship("users", inverse: \StoredUser.$server)
var users: Set<StoredUser>
@Field.Stored("username") var state: State.Server {
var username: String = "" State.Server(uris: uris,
currentURI: currentURI,
name: name,
id: id,
os: os,
version: version,
usersIDs: users.map(\.id))
}
}
@Field.Stored("id") final class StoredUser: CoreStoreObject {
var id: String = ""
@Field.Stored("appleTVID") @Field.Stored("username")
var appleTVID: String = "" var username: String = ""
@Field.Relationship("server") @Field.Stored("id")
var server: StoredServer? var id: String = ""
@Field.Relationship("accessToken", inverse: \StoredAccessToken.$user) @Field.Stored("appleTVID")
var accessToken: StoredAccessToken? var appleTVID: String = ""
var state: State.User { @Field.Relationship("server")
guard let server = server else { fatalError("No server associated with user") } var server: StoredServer?
guard let accessToken = accessToken else { fatalError("No access token associated with user") }
return State.User(username: username,
id: id,
serverID: server.id,
accessToken: accessToken.value)
}
}
final class StoredAccessToken: CoreStoreObject { @Field.Relationship("accessToken", inverse: \StoredAccessToken.$user)
var accessToken: StoredAccessToken?
@Field.Stored("value") var state: State.User {
var value: String = "" guard let server = server else { fatalError("No server associated with user") }
guard let accessToken = accessToken else { fatalError("No access token associated with user") }
return State.User(username: username,
id: id,
serverID: server.id,
accessToken: accessToken.value)
}
}
@Field.Relationship("user") final class StoredAccessToken: CoreStoreObject {
var user: StoredUser?
}
}
// MARK: Errors @Field.Stored("value")
enum Errors { var value: String = ""
case existingServer(State.Server)
case existingUser(State.User)
}
// MARK: dataStack @Field.Relationship("user")
static let dataStack: DataStack = { var user: StoredUser?
let schema = CoreStoreSchema(modelVersion: "V1", }
entities: [ }
Entity<SwiftfinStore.Models.StoredServer>("Server"),
Entity<SwiftfinStore.Models.StoredUser>("User"),
Entity<SwiftfinStore.Models.StoredAccessToken>("AccessToken")
],
versionLock: [
"AccessToken": [0xa8c475e874494bb1, 0x79486e93449f0b3d, 0xa7dc4a0003541edb, 0x94183fae7580ef72],
"Server": [0x936b46acd8e8f0e3, 0x59890d4d9f3f885f, 0x819cf7a4abf98b22, 0xe16125c5af885a06],
"User": [0x845de08a74bc53ed, 0xe95a406a29f3a5d0, 0x9eda732821a15ea9, 0xb5afa531e41ce8a]
])
let _dataStack = DataStack(schema) // MARK: Errors
try! _dataStack.addStorageAndWait(
SQLiteStore( enum Errors {
fileName: "Swiftfin.sqlite", case existingServer(State.Server)
localStorageOptions: .recreateStoreOnModelMismatch case existingUser(State.User)
) }
)
return _dataStack // MARK: dataStack
}()
static let dataStack: DataStack = {
let schema = CoreStoreSchema(modelVersion: "V1",
entities: [
Entity<SwiftfinStore.Models.StoredServer>("Server"),
Entity<SwiftfinStore.Models.StoredUser>("User"),
Entity<SwiftfinStore.Models.StoredAccessToken>("AccessToken"),
],
versionLock: [
"AccessToken": [
0xA8C4_75E8_7449_4BB1,
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)
try! _dataStack.addStorageAndWait(SQLiteStore(fileName: "Swiftfin.sqlite",
localStorageOptions: .recreateStoreOnModelMismatch))
return _dataStack
}()
} }
// MARK: LocalizedError // MARK: LocalizedError
extension SwiftfinStore.Errors: LocalizedError { extension SwiftfinStore.Errors: LocalizedError {
var title: String { var title: String {
switch self { switch self {
case .existingServer: case .existingServer:
return "Existing Server" return "Existing Server"
case .existingUser: case .existingUser:
return "Existing User" return "Existing User"
} }
} }
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,71 +1,75 @@
// //
/* // 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
extension SwiftfinStore { 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")!
}() }()
} }
} }
extension Defaults.Keys { extension Defaults.Keys {
// Universal settings // Universal settings
static let defaultHTTPScheme = Key<HTTPScheme>("defaultHTTPScheme", default: .http, suite: SwiftfinStore.Defaults.universalSuite) static let defaultHTTPScheme = Key<HTTPScheme>("defaultHTTPScheme", default: .http, suite: SwiftfinStore.Defaults.universalSuite)
static let appAppearance = Key<AppAppearance>("appAppearance", default: .system, suite: SwiftfinStore.Defaults.universalSuite) static let appAppearance = Key<AppAppearance>("appAppearance", default: .system, suite: SwiftfinStore.Defaults.universalSuite)
// General settings // General settings
static let lastServerUserID = Defaults.Key<String?>("lastServerUserID", suite: SwiftfinStore.Defaults.generalSuite) static let lastServerUserID = Defaults.Key<String?>("lastServerUserID", suite: SwiftfinStore.Defaults.generalSuite)
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",
static let autoSelectAudioLangCode = Key<String>("AutoSelectAudioLangCode", default: "Auto", suite: SwiftfinStore.Defaults.generalSuite) suite: SwiftfinStore.Defaults.generalSuite)
static let autoSelectAudioLangCode = Key<String>("AutoSelectAudioLangCode", default: "Auto", suite: SwiftfinStore.Defaults.generalSuite)
// Customize settings
static let showPosterLabels = Key<Bool>("showPosterLabels", default: true, suite: SwiftfinStore.Defaults.generalSuite) // Customize settings
static let showCastAndCrew = Key<Bool>("showCastAndCrew", default: true, suite: SwiftfinStore.Defaults.generalSuite) static let showPosterLabels = Key<Bool>("showPosterLabels", default: true, suite: SwiftfinStore.Defaults.generalSuite)
static let showCastAndCrew = Key<Bool>("showCastAndCrew", default: true, suite: SwiftfinStore.Defaults.generalSuite)
// Video player / overlay settings
static let overlayType = Key<OverlayType>("overlayType", default: .normal, suite: SwiftfinStore.Defaults.generalSuite) // Video player / overlay settings
static let jumpGesturesEnabled = Key<Bool>("gesturesEnabled", default: true, suite: SwiftfinStore.Defaults.generalSuite) static let overlayType = Key<OverlayType>("overlayType", default: .normal, suite: SwiftfinStore.Defaults.generalSuite)
static let videoPlayerJumpForward = Key<VideoPlayerJumpLength>("videoPlayerJumpForward", default: .fifteen, suite: SwiftfinStore.Defaults.generalSuite) static let jumpGesturesEnabled = Key<Bool>("gesturesEnabled", default: true, suite: SwiftfinStore.Defaults.generalSuite)
static let videoPlayerJumpBackward = Key<VideoPlayerJumpLength>("videoPlayerJumpBackward", default: .fifteen, suite: SwiftfinStore.Defaults.generalSuite) static let videoPlayerJumpForward = Key<VideoPlayerJumpLength>("videoPlayerJumpForward", default: .fifteen,
static let autoplayEnabled = Key<Bool>("autoPlayNextItem", default: true, suite: SwiftfinStore.Defaults.generalSuite) suite: SwiftfinStore.Defaults.generalSuite)
static let resumeOffset = Key<Bool>("resumeOffset", default: false, suite: SwiftfinStore.Defaults.generalSuite) static let videoPlayerJumpBackward = Key<VideoPlayerJumpLength>("videoPlayerJumpBackward", default: .fifteen,
suite: SwiftfinStore.Defaults.generalSuite)
// Should show video player items static let autoplayEnabled = Key<Bool>("autoPlayNextItem", default: true, suite: SwiftfinStore.Defaults.generalSuite)
static let shouldShowPlayPreviousItem = Key<Bool>("shouldShowPreviousItem", default: true, suite: SwiftfinStore.Defaults.generalSuite) static let resumeOffset = Key<Bool>("resumeOffset", default: false, suite: SwiftfinStore.Defaults.generalSuite)
static let shouldShowPlayNextItem = Key<Bool>("shouldShowNextItem", default: true, suite: SwiftfinStore.Defaults.generalSuite)
static let shouldShowAutoPlay = Key<Bool>("shouldShowAutoPlayNextItem", default: true, suite: SwiftfinStore.Defaults.generalSuite) // Should show video player items
static let shouldShowPlayPreviousItem = Key<Bool>("shouldShowPreviousItem", default: true, suite: SwiftfinStore.Defaults.generalSuite)
// Should show video player items in overlay menu static let shouldShowPlayNextItem = Key<Bool>("shouldShowNextItem", default: true, suite: SwiftfinStore.Defaults.generalSuite)
static let shouldShowJumpButtonsInOverlayMenu = Key<Bool>("shouldShowJumpButtonsInMenu", default: true, suite: SwiftfinStore.Defaults.generalSuite) static let shouldShowAutoPlay = Key<Bool>("shouldShowAutoPlayNextItem", default: true, suite: SwiftfinStore.Defaults.generalSuite)
// Experimental settings // Should show video player items in overlay menu
struct Experimental { static let shouldShowJumpButtonsInOverlayMenu = Key<Bool>("shouldShowJumpButtonsInMenu", default: true,
static let syncSubtitleStateWithAdjacent = Key<Bool>("experimental.syncSubtitleState", default: false, suite: SwiftfinStore.Defaults.generalSuite) suite: SwiftfinStore.Defaults.generalSuite)
static let liveTVAlphaEnabled = Key<Bool>("liveTVAlphaEnabled", default: false, suite: SwiftfinStore.Defaults.generalSuite)
} // Experimental settings
enum Experimental {
// tvos specific static let syncSubtitleStateWithAdjacent = Key<Bool>("experimental.syncSubtitleState", default: false,
static let downActionShowsMenu = Key<Bool>("downActionShowsMenu", default: true, suite: SwiftfinStore.Defaults.generalSuite) suite: SwiftfinStore.Defaults.generalSuite)
static let confirmClose = Key<Bool>("confirmClose", default: false, suite: SwiftfinStore.Defaults.generalSuite) static let liveTVAlphaEnabled = Key<Bool>("liveTVAlphaEnabled", default: false, suite: SwiftfinStore.Defaults.generalSuite)
static let tvOSCinematicViews = Key<Bool>("tvOSCinematicViews", default: false, suite: SwiftfinStore.Defaults.generalSuite) }
// tvos specific
static let downActionShowsMenu = Key<Bool>("downActionShowsMenu", default: true, suite: SwiftfinStore.Defaults.generalSuite)
static let confirmClose = Key<Bool>("confirmClose", default: false, suite: SwiftfinStore.Defaults.generalSuite)
static let tvOSCinematicViews = Key<Bool>("tvOSCinematicViews", default: false, suite: SwiftfinStore.Defaults.generalSuite)
} }

View File

@ -1,27 +1,26 @@
// //
/* // 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
final class BasicAppSettingsViewModel: ViewModel { final class BasicAppSettingsViewModel: ViewModel {
let appearances = AppAppearance.allCases let appearances = AppAppearance.allCases
func resetUserSettings() { func resetUserSettings() {
SwiftfinStore.Defaults.generalSuite.removeAll() SwiftfinStore.Defaults.generalSuite.removeAll()
} }
func resetAppSettings() { func resetAppSettings() {
SwiftfinStore.Defaults.universalSuite.removeAll() SwiftfinStore.Defaults.universalSuite.removeAll()
} }
func removeAllUsers() { func removeAllUsers() {
SessionManager.main.purge() SessionManager.main.purge()
} }
} }

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,113 +13,120 @@ import Stinsen
struct AddServerURIPayload: Identifiable { struct AddServerURIPayload: Identifiable {
let server: SwiftfinStore.State.Server let server: SwiftfinStore.State.Server
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> = []
var backAddServerURIPayload: AddServerURIPayload? @Published
var searching = false
@Published
var addServerURIPayload: AddServerURIPayload?
var backAddServerURIPayload: AddServerURIPayload?
private let discovery = ServerDiscovery() private let discovery = ServerDiscovery()
var alertTitle: String { var alertTitle: String {
var message: String = "" var message: String = ""
if errorMessage?.code != ErrorMessage.noShowErrorCode { if errorMessage?.code != ErrorMessage.noShowErrorCode {
message.append(contentsOf: "\(errorMessage?.code ?? ErrorMessage.noShowErrorCode)\n") message.append(contentsOf: "\(errorMessage?.code ?? ErrorMessage.noShowErrorCode)\n")
} }
message.append(contentsOf: "\(errorMessage?.title ?? "Unkown Error")") message.append(contentsOf: "\(errorMessage?.title ?? "Unkown Error")")
return message return message
} }
func connectToServer(uri: String) { func connectToServer(uri: String) {
#if targetEnvironment(simulator) #if targetEnvironment(simulator)
var uri = uri var uri = uri
if uri == "localhost" { if uri == "localhost" {
uri = "http://localhost:8096" uri = "http://localhost:8096"
} }
#endif #endif
let trimmedURI = uri.trimmingCharacters(in: .whitespaces)
LogManager.shared.log.debug("Attempting to connect to server at \"\(trimmedURI)\"", tag: "connectToServer") let trimmedURI = uri.trimmingCharacters(in: .whitespaces)
SessionManager.main.connectToServer(with: trimmedURI)
.trackActivity(loading)
.sink(receiveCompletion: { completion in
// This is disgusting. ViewModel Error handling overall needs to be refactored
switch completion {
case .finished: ()
case .failure(let error):
switch error {
case is SwiftfinStore.Errors:
let swiftfinError = error as! SwiftfinStore.Errors
switch swiftfinError {
case .existingServer(let server):
self.addServerURIPayload = AddServerURIPayload(server: server, uri: uri)
self.backAddServerURIPayload = AddServerURIPayload(server: server, uri: uri)
default:
self.handleAPIRequestError(displayMessage: "Unable to connect to server.", logLevel: .critical, tag: "connectToServer",
completion: completion)
}
default:
self.handleAPIRequestError(displayMessage: "Unable to connect to server.", logLevel: .critical, tag: "connectToServer",
completion: completion)
}
}
}, receiveValue: { server in
LogManager.shared.log.debug("Connected to server at \"\(uri)\"", tag: "connectToServer")
self.router?.route(to: \.userSignIn, server)
})
.store(in: &cancellables)
}
func discoverServers() { LogManager.shared.log.debug("Attempting to connect to server at \"\(trimmedURI)\"", tag: "connectToServer")
discoveredServers.removeAll() SessionManager.main.connectToServer(with: trimmedURI)
searching = true .trackActivity(loading)
.sink(receiveCompletion: { completion in
// This is disgusting. ViewModel Error handling overall needs to be refactored
switch completion {
case .finished: ()
case let .failure(error):
switch error {
case is SwiftfinStore.Errors:
let swiftfinError = error as! SwiftfinStore.Errors
switch swiftfinError {
case let .existingServer(server):
self.addServerURIPayload = AddServerURIPayload(server: server, uri: uri)
self.backAddServerURIPayload = AddServerURIPayload(server: server, uri: uri)
default:
self.handleAPIRequestError(displayMessage: "Unable to connect to server.", logLevel: .critical,
tag: "connectToServer",
completion: completion)
}
default:
self.handleAPIRequestError(displayMessage: "Unable to connect to server.", logLevel: .critical,
tag: "connectToServer",
completion: completion)
}
}
}, receiveValue: { server in
LogManager.shared.log.debug("Connected to server at \"\(uri)\"", tag: "connectToServer")
self.router?.route(to: \.userSignIn, server)
})
.store(in: &cancellables)
}
// Timeout after 3 seconds func discoverServers() {
DispatchQueue.main.asyncAfter(deadline: .now() + 3) { discoveredServers.removeAll()
self.searching = false searching = true
}
discovery.locateServer { [self] server in // Timeout after 3 seconds
if let server = server { DispatchQueue.main.asyncAfter(deadline: .now() + 3) {
discoveredServers.insert(server) self.searching = false
} }
}
}
func addURIToServer(addServerURIPayload: AddServerURIPayload) { discovery.locateServer { [self] server in
SessionManager.main.addURIToServer(server: addServerURIPayload.server, uri: addServerURIPayload.uri) if let server = server {
.sink { completion in discoveredServers.insert(server)
self.handleAPIRequestError(displayMessage: "Unable to connect to server.", logLevel: .critical, tag: "connectToServer", }
completion: completion) }
} receiveValue: { server in }
SessionManager.main.setServerCurrentURI(server: server, uri: addServerURIPayload.uri)
.sink { completion in
self.handleAPIRequestError(displayMessage: "Unable to connect to server.", logLevel: .critical, tag: "connectToServer",
completion: completion)
} receiveValue: { _ in
self.router?.dismissCoordinator()
}
.store(in: &self.cancellables)
}
.store(in: &cancellables)
}
func cancelConnection() { func addURIToServer(addServerURIPayload: AddServerURIPayload) {
for cancellable in cancellables { SessionManager.main.addURIToServer(server: addServerURIPayload.server, uri: addServerURIPayload.uri)
cancellable.cancel() .sink { completion in
} self.handleAPIRequestError(displayMessage: "Unable to connect to server.", logLevel: .critical, tag: "connectToServer",
completion: completion)
} receiveValue: { server in
SessionManager.main.setServerCurrentURI(server: server, uri: addServerURIPayload.uri)
.sink { completion in
self.handleAPIRequestError(displayMessage: "Unable to connect to server.", logLevel: .critical,
tag: "connectToServer",
completion: completion)
} receiveValue: { _ in
self.router?.dismissCoordinator()
}
.store(in: &self.cancellables)
}
.store(in: &cancellables)
}
self.isLoading = false func cancelConnection() {
} for cancellable in cancellables {
cancellable.cancel()
}
self.isLoading = false
}
} }

View File

@ -1,81 +1,85 @@
// //
/* // 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
final class EpisodesRowViewModel: ViewModel { 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
willSet { var seasonsEpisodes: [BaseItemDto: [BaseItemDto]] = [:]
if seasonsEpisodes[newValue!]!.isEmpty { @Published
retrieveEpisodesForSeason(newValue!) var selectedSeason: BaseItemDto? {
} willSet {
} if seasonsEpisodes[newValue!]!.isEmpty {
} retrieveEpisodesForSeason(newValue!)
}
init(episodeItemViewModel: EpisodeItemViewModel) { }
self.episodeItemViewModel = episodeItemViewModel }
super.init()
init(episodeItemViewModel: EpisodeItemViewModel) {
retrieveSeasons() self.episodeItemViewModel = episodeItemViewModel
} super.init()
private func retrieveSeasons() { retrieveSeasons()
TvShowsAPI.getSeasons(seriesId: episodeItemViewModel.item.seriesId ?? "", }
userId: SessionManager.main.currentLogin.user.id)
.sink { completion in private func retrieveSeasons() {
self.handleAPIRequestError(completion: completion) TvShowsAPI.getSeasons(seriesId: episodeItemViewModel.item.seriesId ?? "",
} receiveValue: { response in userId: SessionManager.main.currentLogin.user.id)
let seasons = response.items ?? [] .sink { completion in
seasons.forEach { season in self.handleAPIRequestError(completion: completion)
self.seasonsEpisodes[season] = [] } receiveValue: { response in
let seasons = response.items ?? []
if season.id == self.episodeItemViewModel.item.seasonId ?? "" { seasons.forEach { season in
self.selectedSeason = season self.seasonsEpisodes[season] = []
}
} if season.id == self.episodeItemViewModel.item.seasonId ?? "" {
} self.selectedSeason = season
.store(in: &cancellables) }
} }
}
private func retrieveEpisodesForSeason(_ season: BaseItemDto) { .store(in: &cancellables)
guard let seasonID = season.id else { return } }
TvShowsAPI.getEpisodes(seriesId: episodeItemViewModel.item.seriesId ?? "", private func retrieveEpisodesForSeason(_ season: BaseItemDto) {
userId: SessionManager.main.currentLogin.user.id, guard let seasonID = season.id else { return }
fields: [.primaryImageAspectRatio, .seriesPrimaryImage, .seasonUserData, .overview, .genres, .people],
seasonId: seasonID) TvShowsAPI.getEpisodes(seriesId: episodeItemViewModel.item.seriesId ?? "",
.trackActivity(loading) userId: SessionManager.main.currentLogin.user.id,
.sink { completion in fields: [.primaryImageAspectRatio, .seriesPrimaryImage, .seasonUserData, .overview, .genres, .people],
self.handleAPIRequestError(completion: completion) seasonId: seasonID)
} receiveValue: { episodes in .trackActivity(loading)
self.seasonsEpisodes[season] = episodes.items ?? [] .sink { completion in
} self.handleAPIRequestError(completion: completion)
.store(in: &cancellables) } receiveValue: { episodes in
} self.seasonsEpisodes[season] = episodes.items ?? []
}
.store(in: &cancellables)
}
} }
final class SingleSeasonEpisodesRowViewModel: ViewModel { 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
init(seasonItemViewModel: SeasonItemViewModel) { var episodes: [BaseItemDto]
self.seasonItemViewModel = seasonItemViewModel
self.episodes = seasonItemViewModel.episodes init(seasonItemViewModel: SeasonItemViewModel) {
super.init() self.seasonItemViewModel = seasonItemViewModel
} self.episodes = seasonItemViewModel.episodes
super.init()
}
} }

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,168 +13,206 @@ 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()
refresh() refresh()
// Nov. 6, 2021 // Nov. 6, 2021
// This is a workaround since Stinsen doesn't have the ability to rebuild a root at the time of writing. // This is a workaround since Stinsen doesn't have the ability to rebuild a root at the time of writing.
// See ServerDetailViewModel.swift for feature request issue // See ServerDetailViewModel.swift for feature request issue
let nc = SwiftfinNotificationCenter.main let nc = SwiftfinNotificationCenter.main
nc.addObserver(self, selector: #selector(didSignIn), name: SwiftfinNotificationCenter.Keys.didSignIn, object: nil) nc.addObserver(self, selector: #selector(didSignIn), name: SwiftfinNotificationCenter.Keys.didSignIn, object: nil)
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
for cancellable in cancellables { private func didSignIn() {
cancellable.cancel() for cancellable in cancellables {
} cancellable.cancel()
}
librariesShowRecentlyAddedIDs = [] librariesShowRecentlyAddedIDs = []
libraries = [] libraries = []
resumeItems = [] resumeItems = []
nextUpItems = [] nextUpItems = []
refresh() refresh()
} }
@objc private func didSignOut() { @objc
for cancellable in cancellables { private func didSignOut() {
cancellable.cancel() for cancellable in cancellables {
} cancellable.cancel()
}
cancellables.removeAll() cancellables.removeAll()
} }
@objc func refresh() { @objc
LogManager.shared.log.debug("Refresh called.") func refresh() {
LogManager.shared.log.debug("Refresh called.")
refreshLibrariesLatest()
refreshLatestAddedItems()
refreshResumeItems()
refreshNextUpItems()
}
// MARK: Libraries Latest Items
private func refreshLibrariesLatest() {
UserViewsAPI.getUserViews(userId: SessionManager.main.currentLogin.user.id)
.trackActivity(loading)
.sink(receiveCompletion: { completion in
switch completion {
case .finished: ()
case .failure:
self.libraries = []
}
self.handleAPIRequestError(completion: completion)
}, receiveValue: { response in
var newLibraries: [BaseItemDto] = [] refreshLibrariesLatest()
refreshLatestAddedItems()
refreshResumeItems()
refreshNextUpItems()
}
response.items!.forEach { item in // MARK: Libraries Latest Items
LogManager.shared.log.debug("Retrieved user view: \(item.id!) (\(item.name ?? "nil")) with type \(item.collectionType ?? "nil")")
if item.collectionType == "movies" || item.collectionType == "tvshows" {
newLibraries.append(item)
}
}
UserAPI.getCurrentUser() private func refreshLibrariesLatest() {
.trackActivity(self.loading) UserViewsAPI.getUserViews(userId: SessionManager.main.currentLogin.user.id)
.sink(receiveCompletion: { completion in .trackActivity(loading)
switch completion { .sink(receiveCompletion: { completion in
case .finished: () switch completion {
case .failure: case .finished: ()
self.libraries = [] case .failure:
self.handleAPIRequestError(completion: completion) self.libraries = []
} }
}, receiveValue: { response in
let excludeIDs = response.configuration?.latestItemsExcludes != nil ? response.configuration!.latestItemsExcludes! : []
for excludeID in excludeIDs { self.handleAPIRequestError(completion: completion)
newLibraries.removeAll { library in }, receiveValue: { response in
return library.id == excludeID
}
}
self.libraries = newLibraries var newLibraries: [BaseItemDto] = []
})
.store(in: &self.cancellables)
})
.store(in: &cancellables)
}
// MARK: Latest Added Items
private func refreshLatestAddedItems() {
UserLibraryAPI.getLatestMedia(userId: SessionManager.main.currentLogin.user.id,
fields: [.primaryImageAspectRatio, .seriesPrimaryImage, .seasonUserData, .overview, .genres, .people, .chapters],
enableImageTypes: [.primary, .backdrop, .thumb],
enableUserData: true,
limit: 8)
.sink { completion in
switch completion {
case .finished: ()
case .failure:
self.nextUpItems = []
self.handleAPIRequestError(completion: completion)
}
} receiveValue: { items in
LogManager.shared.log.debug("Retrieved \(String(items.count)) resume items")
self.latestAddedItems = items
}
.store(in: &cancellables)
}
// MARK: Resume Items
private func refreshResumeItems() {
ItemsAPI.getResumeItems(userId: SessionManager.main.currentLogin.user.id,
limit: 6,
fields: [.primaryImageAspectRatio, .seriesPrimaryImage, .seasonUserData, .overview, .genres, .people, .chapters],
enableUserData: true)
.trackActivity(loading)
.sink(receiveCompletion: { completion in
switch completion {
case .finished: ()
case .failure:
self.resumeItems = []
self.handleAPIRequestError(completion: completion)
}
}, receiveValue: { response in
LogManager.shared.log.debug("Retrieved \(String(response.items!.count)) resume items")
self.resumeItems = response.items ?? [] response.items!.forEach { item in
}) LogManager.shared.log
.store(in: &cancellables) .debug("Retrieved user view: \(item.id!) (\(item.name ?? "nil")) with type \(item.collectionType ?? "nil")")
} if item.collectionType == "movies" || item.collectionType == "tvshows" {
newLibraries.append(item)
// MARK: Next Up Items }
private func refreshNextUpItems() { }
TvShowsAPI.getNextUp(userId: SessionManager.main.currentLogin.user.id,
limit: 6,
fields: [.primaryImageAspectRatio, .seriesPrimaryImage, .seasonUserData, .overview, .genres, .people, .chapters],
enableUserData: true)
.trackActivity(loading)
.sink(receiveCompletion: { completion in
switch completion {
case .finished: ()
case .failure:
self.nextUpItems = []
self.handleAPIRequestError(completion: completion)
}
}, receiveValue: { response in
LogManager.shared.log.debug("Retrieved \(String(response.items!.count)) nextup items")
self.nextUpItems = response.items ?? [] UserAPI.getCurrentUser()
}) .trackActivity(self.loading)
.store(in: &cancellables) .sink(receiveCompletion: { completion in
} switch completion {
case .finished: ()
case .failure:
self.libraries = []
self.handleAPIRequestError(completion: completion)
}
}, receiveValue: { response in
let excludeIDs = response.configuration?.latestItemsExcludes != nil ? response.configuration!
.latestItemsExcludes! : []
for excludeID in excludeIDs {
newLibraries.removeAll { library in
library.id == excludeID
}
}
self.libraries = newLibraries
})
.store(in: &self.cancellables)
})
.store(in: &cancellables)
}
// MARK: Latest Added Items
private func refreshLatestAddedItems() {
UserLibraryAPI.getLatestMedia(userId: SessionManager.main.currentLogin.user.id,
fields: [
.primaryImageAspectRatio,
.seriesPrimaryImage,
.seasonUserData,
.overview,
.genres,
.people,
.chapters,
],
enableImageTypes: [.primary, .backdrop, .thumb],
enableUserData: true,
limit: 8)
.sink { completion in
switch completion {
case .finished: ()
case .failure:
self.nextUpItems = []
self.handleAPIRequestError(completion: completion)
}
} receiveValue: { items in
LogManager.shared.log.debug("Retrieved \(String(items.count)) resume items")
self.latestAddedItems = items
}
.store(in: &cancellables)
}
// MARK: Resume Items
private func refreshResumeItems() {
ItemsAPI.getResumeItems(userId: SessionManager.main.currentLogin.user.id,
limit: 6,
fields: [
.primaryImageAspectRatio,
.seriesPrimaryImage,
.seasonUserData,
.overview,
.genres,
.people,
.chapters,
],
enableUserData: true)
.trackActivity(loading)
.sink(receiveCompletion: { completion in
switch completion {
case .finished: ()
case .failure:
self.resumeItems = []
self.handleAPIRequestError(completion: completion)
}
}, receiveValue: { response in
LogManager.shared.log.debug("Retrieved \(String(response.items!.count)) resume items")
self.resumeItems = response.items ?? []
})
.store(in: &cancellables)
}
// MARK: Next Up Items
private func refreshNextUpItems() {
TvShowsAPI.getNextUp(userId: SessionManager.main.currentLogin.user.id,
limit: 6,
fields: [
.primaryImageAspectRatio,
.seriesPrimaryImage,
.seasonUserData,
.overview,
.genres,
.people,
.chapters,
],
enableUserData: true)
.trackActivity(loading)
.sink(receiveCompletion: { completion in
switch completion {
case .finished: ()
case .failure:
self.nextUpItems = []
self.handleAPIRequestError(completion: completion)
}
}, receiveValue: { response in
LogManager.shared.log.debug("Retrieved \(String(response.items!.count)) nextup items")
self.nextUpItems = response.items ?? []
})
.store(in: &cancellables)
}
} }

View File

@ -1,36 +1,36 @@
// //
/* // 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 CollectionItemViewModel: ItemViewModel { final class CollectionItemViewModel: ItemViewModel {
@Published var collectionItems: [BaseItemDto] = [] @Published
var collectionItems: [BaseItemDto] = []
override init(item: BaseItemDto) {
super.init(item: item) override init(item: BaseItemDto) {
super.init(item: item)
getCollectionItems()
} getCollectionItems()
}
private func getCollectionItems() {
ItemsAPI.getItems(userId: SessionManager.main.currentLogin.user.id, private func getCollectionItems() {
parentId: item.id, ItemsAPI.getItems(userId: SessionManager.main.currentLogin.user.id,
fields: [.primaryImageAspectRatio, .seriesPrimaryImage, .seasonUserData, .overview, .genres, .people]) parentId: item.id,
.trackActivity(loading) fields: [.primaryImageAspectRatio, .seriesPrimaryImage, .seasonUserData, .overview, .genres, .people])
.sink { [weak self] completion in .trackActivity(loading)
self?.handleAPIRequestError(completion: completion) .sink { [weak self] completion in
} receiveValue: { [weak self] response in self?.handleAPIRequestError(completion: completion)
self?.collectionItems = response.items ?? [] } receiveValue: { [weak self] response in
} self?.collectionItems = response.items ?? []
.store(in: &cancellables) }
} .store(in: &cancellables)
}
} }

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,34 +12,36 @@ import JellyfinAPI
import Stinsen import Stinsen
final class EpisodeItemViewModel: ItemViewModel { final class EpisodeItemViewModel: ItemViewModel {
@RouterObject var itemRouter: ItemCoordinator.Router?
@Published var series: BaseItemDto?
override init(item: BaseItemDto) {
super.init(item: item)
getEpisodeSeries()
}
override func getItemDisplayName() -> String { @RouterObject
guard let episodeLocator = item.getEpisodeLocator() else { return item.name ?? "" } var itemRouter: ItemCoordinator.Router?
return "\(episodeLocator)\n\(item.name ?? "")" @Published
} var series: BaseItemDto?
override func shouldDisplayRuntime() -> Bool { override init(item: BaseItemDto) {
return false super.init(item: item)
}
func getEpisodeSeries() { getEpisodeSeries()
guard let id = item.seriesId else { return } }
UserLibraryAPI.getItem(userId: SessionManager.main.currentLogin.user.id, itemId: id)
.trackActivity(loading) override func getItemDisplayName() -> String {
.sink(receiveCompletion: { [weak self] completion in guard let episodeLocator = item.getEpisodeLocator() else { return item.name ?? "" }
self?.handleAPIRequestError(completion: completion) return "\(episodeLocator)\n\(item.name ?? "")"
}, receiveValue: { [weak self] item in }
self?.series = item
}) override func shouldDisplayRuntime() -> Bool {
.store(in: &cancellables) false
} }
func getEpisodeSeries() {
guard let id = item.seriesId else { return }
UserLibraryAPI.getItem(userId: SessionManager.main.currentLogin.user.id, itemId: id)
.trackActivity(loading)
.sink(receiveCompletion: { [weak self] completion in
self?.handleAPIRequestError(completion: completion)
}, receiveValue: { [weak self] item in
self?.series = item
})
.store(in: &cancellables)
}
} }

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,121 +13,130 @@ import UIKit
class ItemViewModel: ViewModel { class ItemViewModel: ViewModel {
@Published var item: BaseItemDto @Published
@Published var playButtonItem: BaseItemDto? { var item: BaseItemDto
didSet { @Published
if let playButtonItem = playButtonItem { var playButtonItem: BaseItemDto? {
refreshItemVideoPlayerViewModel(for: playButtonItem) didSet {
} if let playButtonItem = playButtonItem {
} refreshItemVideoPlayerViewModel(for: playButtonItem)
} }
@Published var similarItems: [BaseItemDto] = [] }
@Published var isWatched = false }
@Published var isFavorited = false
@Published var informationItems: [BaseItemDto.ItemDetail]
@Published var mediaItems: [BaseItemDto.ItemDetail]
var itemVideoPlayerViewModel: VideoPlayerViewModel?
init(item: BaseItemDto) { @Published
self.item = item var similarItems: [BaseItemDto] = []
@Published
var isWatched = false
@Published
var isFavorited = false
@Published
var informationItems: [BaseItemDto.ItemDetail]
@Published
var mediaItems: [BaseItemDto.ItemDetail]
var itemVideoPlayerViewModel: VideoPlayerViewModel?
switch item.itemType { init(item: BaseItemDto) {
case .episode, .movie: self.item = item
self.playButtonItem = item
default: ()
}
informationItems = item.createInformationItems()
mediaItems = item.createMediaItems()
isFavorited = item.userData?.isFavorite ?? false switch item.itemType {
isWatched = item.userData?.played ?? false case .episode, .movie:
super.init() self.playButtonItem = item
default: ()
}
getSimilarItems() informationItems = item.createInformationItems()
mediaItems = item.createMediaItems()
refreshItemVideoPlayerViewModel(for: item)
}
func refreshItemVideoPlayerViewModel(for item: BaseItemDto) {
item.createVideoPlayerViewModel()
.sink { completion in
self.handleAPIRequestError(completion: completion)
} receiveValue: { videoPlayerViewModel in
self.itemVideoPlayerViewModel = videoPlayerViewModel
self.mediaItems = videoPlayerViewModel.item.createMediaItems()
}
.store(in: &cancellables)
}
func playButtonText() -> String { isFavorited = item.userData?.isFavorite ?? false
if let itemProgressString = item.getItemProgressString() { isWatched = item.userData?.played ?? false
return itemProgressString super.init()
}
return L10n.play
}
func getItemDisplayName() -> String { getSimilarItems()
return item.name ?? ""
}
func shouldDisplayRuntime() -> Bool { refreshItemVideoPlayerViewModel(for: item)
return true }
}
func getSimilarItems() { func refreshItemVideoPlayerViewModel(for item: BaseItemDto) {
LibraryAPI.getSimilarItems(itemId: item.id!, userId: SessionManager.main.currentLogin.user.id, limit: 20, fields: [.primaryImageAspectRatio, .seriesPrimaryImage, .seasonUserData, .overview, .genres, .people]) item.createVideoPlayerViewModel()
.trackActivity(loading) .sink { completion in
.sink(receiveCompletion: { [weak self] completion in self.handleAPIRequestError(completion: completion)
self?.handleAPIRequestError(completion: completion) } receiveValue: { videoPlayerViewModel in
}, receiveValue: { [weak self] response in self.itemVideoPlayerViewModel = videoPlayerViewModel
self?.similarItems = response.items ?? [] self.mediaItems = videoPlayerViewModel.item.createMediaItems()
}) }
.store(in: &cancellables) .store(in: &cancellables)
} }
func updateWatchState() { func playButtonText() -> String {
if isWatched { if let itemProgressString = item.getItemProgressString() {
PlaystateAPI.markUnplayedItem(userId: SessionManager.main.currentLogin.user.id, itemId: item.id!) return itemProgressString
.trackActivity(loading) }
.sink(receiveCompletion: { [weak self] completion in
self?.handleAPIRequestError(completion: completion)
}, receiveValue: { [weak self] _ in
self?.isWatched = false
})
.store(in: &cancellables)
} else {
PlaystateAPI.markPlayedItem(userId: SessionManager.main.currentLogin.user.id, itemId: item.id!)
.trackActivity(loading)
.sink(receiveCompletion: { [weak self] completion in
self?.handleAPIRequestError(completion: completion)
}, receiveValue: { [weak self] _ in
self?.isWatched = true
})
.store(in: &cancellables)
}
}
func updateFavoriteState() { return L10n.play
if isFavorited { }
UserLibraryAPI.unmarkFavoriteItem(userId: SessionManager.main.currentLogin.user.id, itemId: item.id!)
.trackActivity(loading) func getItemDisplayName() -> String {
.sink(receiveCompletion: { [weak self] completion in item.name ?? ""
self?.handleAPIRequestError(completion: completion) }
}, receiveValue: { [weak self] _ in
self?.isFavorited = false func shouldDisplayRuntime() -> Bool {
}) true
.store(in: &cancellables) }
} else {
UserLibraryAPI.markFavoriteItem(userId: SessionManager.main.currentLogin.user.id, itemId: item.id!) func getSimilarItems() {
.trackActivity(loading) LibraryAPI.getSimilarItems(itemId: item.id!, userId: SessionManager.main.currentLogin.user.id, limit: 20,
.sink(receiveCompletion: { [weak self] completion in fields: [.primaryImageAspectRatio, .seriesPrimaryImage, .seasonUserData, .overview, .genres, .people])
self?.handleAPIRequestError(completion: completion) .trackActivity(loading)
}, receiveValue: { [weak self] _ in .sink(receiveCompletion: { [weak self] completion in
self?.isFavorited = true self?.handleAPIRequestError(completion: completion)
}) }, receiveValue: { [weak self] response in
.store(in: &cancellables) self?.similarItems = response.items ?? []
} })
} .store(in: &cancellables)
}
func updateWatchState() {
if isWatched {
PlaystateAPI.markUnplayedItem(userId: SessionManager.main.currentLogin.user.id, itemId: item.id!)
.trackActivity(loading)
.sink(receiveCompletion: { [weak self] completion in
self?.handleAPIRequestError(completion: completion)
}, receiveValue: { [weak self] _ in
self?.isWatched = false
})
.store(in: &cancellables)
} else {
PlaystateAPI.markPlayedItem(userId: SessionManager.main.currentLogin.user.id, itemId: item.id!)
.trackActivity(loading)
.sink(receiveCompletion: { [weak self] completion in
self?.handleAPIRequestError(completion: completion)
}, receiveValue: { [weak self] _ in
self?.isWatched = true
})
.store(in: &cancellables)
}
}
func updateFavoriteState() {
if isFavorited {
UserLibraryAPI.unmarkFavoriteItem(userId: SessionManager.main.currentLogin.user.id, itemId: item.id!)
.trackActivity(loading)
.sink(receiveCompletion: { [weak self] completion in
self?.handleAPIRequestError(completion: completion)
}, receiveValue: { [weak self] _ in
self?.isFavorited = false
})
.store(in: &cancellables)
} else {
UserLibraryAPI.markFavoriteItem(userId: SessionManager.main.currentLogin.user.id, itemId: item.id!)
.trackActivity(loading)
.sink(receiveCompletion: { [weak self] completion in
self?.handleAPIRequestError(completion: completion)
}, receiveValue: { [weak self] _ in
self?.isFavorited = true
})
.store(in: &cancellables)
}
}
} }

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
@ -13,76 +12,79 @@ import JellyfinAPI
import Stinsen import Stinsen
final class SeasonItemViewModel: ItemViewModel { final class SeasonItemViewModel: ItemViewModel {
@RouterObject var itemRouter: ItemCoordinator.Router?
@Published var episodes: [BaseItemDto] = []
@Published var seriesItem: BaseItemDto?
override init(item: BaseItemDto) { @RouterObject
super.init(item: item) var itemRouter: ItemCoordinator.Router?
@Published
var episodes: [BaseItemDto] = []
@Published
var seriesItem: BaseItemDto?
getSeriesItem() override init(item: BaseItemDto) {
requestEpisodes() super.init(item: item)
}
override func playButtonText() -> String { getSeriesItem()
guard let playButtonItem = playButtonItem else { return L10n.play } requestEpisodes()
guard let episodeLocator = playButtonItem.getEpisodeLocator() else { return L10n.play } }
return episodeLocator
}
private func requestEpisodes() { override func playButtonText() -> String {
LogManager.shared.log guard let playButtonItem = playButtonItem else { return L10n.play }
.debug("Getting episodes in season \(item.id!) (\(item.name!)) of show \(item.seriesId!) (\(item.seriesName!))") guard let episodeLocator = playButtonItem.getEpisodeLocator() else { return L10n.play }
TvShowsAPI.getEpisodes(seriesId: item.seriesId ?? "", userId: SessionManager.main.currentLogin.user.id, return episodeLocator
fields: [.primaryImageAspectRatio, .seriesPrimaryImage, .seasonUserData, .overview, .genres, .people], }
seasonId: item.id ?? "")
.trackActivity(loading)
.sink(receiveCompletion: { [weak self] completion in
self?.handleAPIRequestError(completion: completion)
}, receiveValue: { [weak self] response in
self?.episodes = response.items ?? []
LogManager.shared.log.debug("Retrieved \(String(self?.episodes.count ?? 0)) episodes")
self?.setNextUpInSeason() private func requestEpisodes() {
}) LogManager.shared.log
.store(in: &cancellables) .debug("Getting episodes in season \(item.id!) (\(item.name!)) of show \(item.seriesId!) (\(item.seriesName!))")
} TvShowsAPI.getEpisodes(seriesId: item.seriesId ?? "", userId: SessionManager.main.currentLogin.user.id,
fields: [.primaryImageAspectRatio, .seriesPrimaryImage, .seasonUserData, .overview, .genres, .people],
seasonId: item.id ?? "")
.trackActivity(loading)
.sink(receiveCompletion: { [weak self] completion in
self?.handleAPIRequestError(completion: completion)
}, receiveValue: { [weak self] response in
self?.episodes = response.items ?? []
LogManager.shared.log.debug("Retrieved \(String(self?.episodes.count ?? 0)) episodes")
// Sets the play button item to the "Next up" in the season based upon self?.setNextUpInSeason()
// the watched status of episodes in the season. })
// Default to the first episode of the season if all have been watched. .store(in: &cancellables)
private func setNextUpInSeason() { }
guard !episodes.isEmpty else { return }
var firstUnwatchedSearch: BaseItemDto? // Sets the play button item to the "Next up" in the season based upon
// the watched status of episodes in the season.
// Default to the first episode of the season if all have been watched.
private func setNextUpInSeason() {
guard !episodes.isEmpty else { return }
for episode in episodes { var firstUnwatchedSearch: BaseItemDto?
guard let played = episode.userData?.played else { continue }
if !played {
firstUnwatchedSearch = episode
break
}
}
if let firstUnwatched = firstUnwatchedSearch { for episode in episodes {
playButtonItem = firstUnwatched guard let played = episode.userData?.played else { continue }
} else { if !played {
guard let firstEpisode = episodes.first else { return } firstUnwatchedSearch = episode
playButtonItem = firstEpisode break
} }
} }
private func getSeriesItem() { if let firstUnwatched = firstUnwatchedSearch {
guard let seriesID = item.seriesId else { return } playButtonItem = firstUnwatched
UserLibraryAPI.getItem(userId: SessionManager.main.currentLogin.user.id, } else {
itemId: seriesID) guard let firstEpisode = episodes.first else { return }
.trackActivity(loading) playButtonItem = firstEpisode
.sink { [weak self] completion in }
self?.handleAPIRequestError(completion: completion) }
} receiveValue: { [weak self] seriesItem in
self?.seriesItem = seriesItem private func getSeriesItem() {
} guard let seriesID = item.seriesId else { return }
.store(in: &cancellables) UserLibraryAPI.getItem(userId: SessionManager.main.currentLogin.user.id,
} itemId: seriesID)
.trackActivity(loading)
.sink { [weak self] completion in
self?.handleAPIRequestError(completion: completion)
} receiveValue: { [weak self] seriesItem in
self?.seriesItem = seriesItem
}
.store(in: &cancellables)
}
} }

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,68 +12,73 @@ 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)
requestSeasons() requestSeasons()
getNextUp() getNextUp()
} }
override func playButtonText() -> String { override func playButtonText() -> String {
guard let playButtonItem = playButtonItem else { return L10n.play } guard let playButtonItem = playButtonItem else { return L10n.play }
guard let episodeLocator = playButtonItem.getEpisodeLocator() else { return L10n.play } guard let episodeLocator = playButtonItem.getEpisodeLocator() else { return L10n.play }
return episodeLocator return episodeLocator
} }
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,
.trackActivity(loading) fields: [.primaryImageAspectRatio, .seriesPrimaryImage, .seasonUserData, .overview, .genres, .people],
.sink(receiveCompletion: { [weak self] completion in seriesId: self.item.id!, enableUserData: true)
self?.handleAPIRequestError(completion: completion) .trackActivity(loading)
}, receiveValue: { [weak self] response in .sink(receiveCompletion: { [weak self] completion in
if let nextUpItem = response.items?.first { self?.handleAPIRequestError(completion: completion)
self?.playButtonItem = nextUpItem }, receiveValue: { [weak self] response in
} if let nextUpItem = response.items?.first {
}) self?.playButtonItem = nextUpItem
.store(in: &cancellables) }
} })
.store(in: &cancellables)
}
private func getRunYears() -> String { private func getRunYears() -> String {
let dateFormatter = DateFormatter() let dateFormatter = DateFormatter()
dateFormatter.dateFormat = "yyyy" dateFormatter.dateFormat = "yyyy"
var startYear: String? var startYear: String?
var endYear: String? var endYear: String?
if item.premiereDate != nil { if item.premiereDate != nil {
startYear = dateFormatter.string(from: item.premiereDate!) startYear = dateFormatter.string(from: item.premiereDate!)
} }
if item.endDate != nil { if item.endDate != nil {
endYear = dateFormatter.string(from: item.endDate!) endYear = dateFormatter.string(from: item.endDate!)
} }
return "\(startYear ?? "Unknown") - \(endYear ?? "Present")" return "\(startYear ?? "Unknown") - \(endYear ?? "Present")"
} }
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,
.trackActivity(loading) fields: [.primaryImageAspectRatio, .seriesPrimaryImage, .seasonUserData, .overview, .genres, .people],
.sink(receiveCompletion: { [weak self] completion in enableUserData: true)
self?.handleAPIRequestError(completion: completion) .trackActivity(loading)
}, receiveValue: { [weak self] response in .sink(receiveCompletion: { [weak self] completion in
self?.seasons = response.items ?? [] self?.handleAPIRequestError(completion: completion)
LogManager.shared.log.debug("Retrieved \(String(self?.seasons.count ?? 0)) seasons") }, receiveValue: { [weak self] response in
}) self?.seasons = response.items ?? []
.store(in: &cancellables) LogManager.shared.log.debug("Retrieved \(String(self?.seasons.count ?? 0)) seasons")
} })
.store(in: &cancellables)
}
} }

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,38 +12,39 @@ import JellyfinAPI
final class LatestMediaViewModel: ViewModel { final class LatestMediaViewModel: ViewModel {
@Published var items = [BaseItemDto]() @Published
var items = [BaseItemDto]()
let library: BaseItemDto
init(library: BaseItemDto) { let library: BaseItemDto
self.library = library
super.init()
requestLatestMedia() init(library: BaseItemDto) {
} self.library = library
super.init()
func requestLatestMedia() { requestLatestMedia()
LogManager.shared.log.debug("Requesting latest media for user id \(SessionManager.main.currentLogin.user.id)") }
UserLibraryAPI.getLatestMedia(userId: SessionManager.main.currentLogin.user.id,
parentId: library.id ?? "", func requestLatestMedia() {
fields: [ LogManager.shared.log.debug("Requesting latest media for user id \(SessionManager.main.currentLogin.user.id)")
.primaryImageAspectRatio, UserLibraryAPI.getLatestMedia(userId: SessionManager.main.currentLogin.user.id,
.seriesPrimaryImage, parentId: library.id ?? "",
.seasonUserData, fields: [
.overview, .primaryImageAspectRatio,
.genres, .seriesPrimaryImage,
.people .seasonUserData,
], .overview,
includeItemTypes: ["Series", "Movie"], .genres,
enableUserData: true, limit: 12) .people,
.trackActivity(loading) ],
.sink(receiveCompletion: { [weak self] completion in includeItemTypes: ["Series", "Movie"],
self?.handleAPIRequestError(completion: completion) enableUserData: true, limit: 12)
}, receiveValue: { [weak self] response in .trackActivity(loading)
self?.items = response .sink(receiveCompletion: { [weak self] completion in
LogManager.shared.log.debug("Retrieved \(String(self?.items.count ?? 0)) items") self?.handleAPIRequestError(completion: completion)
}) }, receiveValue: { [weak self] response in
.store(in: &cancellables) self?.items = response
} LogManager.shared.log.debug("Retrieved \(String(self?.items.count ?? 0)) items")
})
.store(in: &cancellables)
}
} }

View File

@ -1,72 +1,81 @@
// //
/* // 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
enum FilterType { enum FilterType {
case tag case tag
case genre case genre
case sortOrder case sortOrder
case sortBy case sortBy
case filter case filter
} }
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 = ""
func updateModifiedFilter() { func updateModifiedFilter() {
modifiedFilters.sortOrder = [selectedSortOrder] modifiedFilters.sortOrder = [selectedSortOrder]
modifiedFilters.sortBy = [selectedSortBy] modifiedFilters.sortBy = [selectedSortBy]
} }
func resetFilters() { func resetFilters() {
modifiedFilters = LibraryFilters(filters: [], sortOrder: [.ascending], withGenres: [], tags: [], sortBy: [.name]) modifiedFilters = LibraryFilters(filters: [], sortOrder: [.ascending], withGenres: [], tags: [], sortBy: [.name])
} }
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.selectedSortBy = filters?.sortBy.first ?? .name self.enabledFilterType = enabledFilterType
self.selectedSortOrder = filters?.sortOrder.first ?? .descending self.selectedSortBy = filters?.sortBy.first ?? .name
self.parentId = parentId self.selectedSortOrder = filters?.sortOrder.first ?? .descending
self.parentId = parentId
super.init() super.init()
if let filters = filters { if let filters = filters {
self.modifiedFilters = filters self.modifiedFilters = filters
} }
requestQueryFilters() requestQueryFilters()
} }
func requestQueryFilters() { func requestQueryFilters() {
FilterAPI.getQueryFilters(userId: SessionManager.main.currentLogin.user.id, parentId: self.parentId) FilterAPI.getQueryFilters(userId: SessionManager.main.currentLogin.user.id, parentId: self.parentId)
.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] queryFilters in }, receiveValue: { [weak self] queryFilters in
guard let self = self else { return } guard let self = self else { return }
self.possibleGenres = queryFilters.genres ?? [] self.possibleGenres = queryFilters.genres ?? []
self.possibleTags = queryFilters.tags ?? [] self.possibleTags = queryFilters.tags ?? []
}) })
.store(in: &cancellables) .store(in: &cancellables)
} }
} }

View File

@ -1,36 +1,36 @@
// //
/* // 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: [])
override init() { override init() {
super.init() super.init()
requestLibraries() requestLibraries()
} }
func requestLibraries() { func requestLibraries() {
UserViewsAPI.getUserViews(userId: SessionManager.main.currentLogin.user.id) UserViewsAPI.getUserViews(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)
}, receiveValue: { response in }, receiveValue: { response in
self.libraries = response.items ?? [] self.libraries = response.items ?? []
}) })
.store(in: &cancellables) .store(in: &cancellables)
} }
} }

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,134 +14,140 @@ 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?
init(parentID: String?) { init(parentID: String?) {
self.parentID = parentID self.parentID = parentID
super.init() super.init()
searchQuerySubject searchQuerySubject
.filter { !$0.isEmpty } .filter { !$0.isEmpty }
.debounce(for: 0.25, scheduler: DispatchQueue.main) .debounce(for: 0.25, scheduler: DispatchQueue.main)
.sink(receiveValue: search) .sink(receiveValue: search)
.store(in: &cancellables) .store(in: &cancellables)
setupPublishersForSupportedItemType() setupPublishersForSupportedItemType()
requestSuggestions() requestSuggestions()
} }
func setupPublishersForSupportedItemType() { func setupPublishersForSupportedItemType() {
Publishers.CombineLatest3($movieItems, $showItems, $episodeItems) Publishers.CombineLatest3($movieItems, $showItems, $episodeItems)
.debounce(for: 0.25, scheduler: DispatchQueue.main) .debounce(for: 0.25, scheduler: DispatchQueue.main)
.map { arg -> [ItemType] in .map { arg -> [ItemType] in
var typeList = [ItemType]() var typeList = [ItemType]()
if !arg.0.isEmpty { if !arg.0.isEmpty {
typeList.append(.movie) typeList.append(.movie)
} }
if !arg.1.isEmpty { if !arg.1.isEmpty {
typeList.append(.series) typeList.append(.series)
} }
if !arg.2.isEmpty { if !arg.2.isEmpty {
typeList.append(.episode) typeList.append(.episode)
} }
return typeList return typeList
} }
.receive(on: DispatchQueue.main) .receive(on: DispatchQueue.main)
.sink(receiveValue: { [weak self] typeList in .sink(receiveValue: { [weak self] typeList in
withAnimation { withAnimation {
self?.supportedItemTypeList = typeList self?.supportedItemTypeList = typeList
} }
}) })
.store(in: &cancellables) .store(in: &cancellables)
$supportedItemTypeList $supportedItemTypeList
.receive(on: DispatchQueue.main) .receive(on: DispatchQueue.main)
.withLatestFrom($selectedItemType) .withLatestFrom($selectedItemType)
.compactMap { selectedItemType in .compactMap { selectedItemType in
if self.supportedItemTypeList.contains(selectedItemType) { if self.supportedItemTypeList.contains(selectedItemType) {
return selectedItemType return selectedItemType
} else { } else {
return self.supportedItemTypeList.first return self.supportedItemTypeList.first
} }
} }
.sink(receiveValue: { [weak self] itemType in .sink(receiveValue: { [weak self] itemType in
withAnimation { withAnimation {
self?.selectedItemType = itemType self?.selectedItemType = itemType
} }
}) })
.store(in: &cancellables) .store(in: &cancellables)
} }
func requestSuggestions() { func requestSuggestions() {
ItemsAPI.getItemsByUserId(userId: SessionManager.main.currentLogin.user.id, ItemsAPI.getItemsByUserId(userId: SessionManager.main.currentLogin.user.id,
limit: 20, limit: 20,
recursive: true, recursive: true,
parentId: parentID, parentId: parentID,
includeItemTypes: ["Movie", "Series"], includeItemTypes: ["Movie", "Series"],
sortBy: ["IsFavoriteOrLiked", "Random"], sortBy: ["IsFavoriteOrLiked", "Random"],
imageTypeLimit: 0, imageTypeLimit: 0,
enableTotalRecordCount: false, enableTotalRecordCount: false,
enableImages: false) enableImages: false)
.trackActivity(loading) .trackActivity(loading)
.receive(on: DispatchQueue.main) .receive(on: DispatchQueue.main)
.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] response in
self?.suggestions = response.items ?? [] self?.suggestions = response.items ?? []
}) })
.store(in: &cancellables) .store(in: &cancellables)
} }
func search(with query: String) { func search(with query: String) {
ItemsAPI.getItemsByUserId(userId: SessionManager.main.currentLogin.user.id, limit: 50, recursive: true, searchTerm: query, ItemsAPI.getItemsByUserId(userId: SessionManager.main.currentLogin.user.id, limit: 50, recursive: true, searchTerm: query,
sortOrder: [.ascending], parentId: parentID, sortOrder: [.ascending], parentId: parentID,
fields: [.primaryImageAspectRatio, .seriesPrimaryImage, .seasonUserData, .overview, .genres, .people], fields: [.primaryImageAspectRatio, .seriesPrimaryImage, .seasonUserData, .overview, .genres, .people],
includeItemTypes: [ItemType.movie.rawValue], sortBy: ["SortName"], enableUserData: true, includeItemTypes: [ItemType.movie.rawValue], sortBy: ["SortName"], enableUserData: true,
enableImages: true) enableImages: true)
.trackActivity(loading) .trackActivity(loading)
.receive(on: DispatchQueue.main) .receive(on: DispatchQueue.main)
.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] response in
self?.movieItems = response.items ?? [] self?.movieItems = response.items ?? []
}) })
.store(in: &cancellables) .store(in: &cancellables)
ItemsAPI.getItemsByUserId(userId: SessionManager.main.currentLogin.user.id, limit: 50, recursive: true, searchTerm: query, ItemsAPI.getItemsByUserId(userId: SessionManager.main.currentLogin.user.id, limit: 50, recursive: true, searchTerm: query,
sortOrder: [.ascending], parentId: parentID, sortOrder: [.ascending], parentId: parentID,
fields: [.primaryImageAspectRatio, .seriesPrimaryImage, .seasonUserData, .overview, .genres, .people], fields: [.primaryImageAspectRatio, .seriesPrimaryImage, .seasonUserData, .overview, .genres, .people],
includeItemTypes: [ItemType.series.rawValue], sortBy: ["SortName"], enableUserData: true, includeItemTypes: [ItemType.series.rawValue], sortBy: ["SortName"], enableUserData: true,
enableImages: true) enableImages: true)
.trackActivity(loading) .trackActivity(loading)
.receive(on: DispatchQueue.main) .receive(on: DispatchQueue.main)
.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] response in
self?.showItems = response.items ?? [] self?.showItems = response.items ?? []
}) })
.store(in: &cancellables) .store(in: &cancellables)
ItemsAPI.getItemsByUserId(userId: SessionManager.main.currentLogin.user.id, limit: 50, recursive: true, searchTerm: query, ItemsAPI.getItemsByUserId(userId: SessionManager.main.currentLogin.user.id, limit: 50, recursive: true, searchTerm: query,
sortOrder: [.ascending], parentId: parentID, sortOrder: [.ascending], parentId: parentID,
fields: [.primaryImageAspectRatio, .seriesPrimaryImage, .seasonUserData, .overview, .genres, .people], fields: [.primaryImageAspectRatio, .seriesPrimaryImage, .seasonUserData, .overview, .genres, .people],
includeItemTypes: [ItemType.episode.rawValue], sortBy: ["SortName"], enableUserData: true, includeItemTypes: [ItemType.episode.rawValue], sortBy: ["SortName"], enableUserData: true,
enableImages: true) enableImages: true)
.trackActivity(loading) .trackActivity(loading)
.receive(on: DispatchQueue.main) .receive(on: DispatchQueue.main)
.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] response in
self?.episodeItems = response.items ?? [] self?.episodeItems = response.items ?? []
}) })
.store(in: &cancellables) .store(in: &cancellables)
} }
} }

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,184 +14,209 @@ import SwiftUICollection
typealias LibraryRow = CollectionRow<Int, LibraryRowCell> typealias LibraryRow = CollectionRow<Int, LibraryRowCell>
struct LibraryRowCell: Hashable { struct LibraryRowCell: Hashable {
let id = UUID() let id = UUID()
let item: BaseItemDto? let item: BaseItemDto?
var loadingCell: Bool = false var loadingCell: Bool = false
} }
final class LibraryViewModel: ViewModel { final class LibraryViewModel: ViewModel {
var parentID: String? var parentID: String?
var person: BaseItemPerson? var person: BaseItemPerson?
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]()
var enabledFilterType: [FilterType] { var enabledFilterType: [FilterType] {
if genre == nil { if genre == nil {
return [.tag, .genre, .sortBy, .sortOrder, .filter] return [.tag, .genre, .sortBy, .sortOrder, .filter]
} else { } else {
return [.tag, .sortBy, .sortOrder, .filter] return [.tag, .sortBy, .sortOrder, .filter]
} }
} }
init(parentID: String? = nil, init(parentID: String? = nil,
person: BaseItemPerson? = nil, person: BaseItemPerson? = nil,
genre: NameGuidPair? = nil, genre: NameGuidPair? = nil,
studio: NameGuidPair? = nil, studio: NameGuidPair? = nil,
filters: LibraryFilters = LibraryFilters(filters: [], sortOrder: [.ascending], withGenres: [], sortBy: [.name]), filters: LibraryFilters = LibraryFilters(filters: [], sortOrder: [.ascending], withGenres: [], sortBy: [.name]),
columns: Int = 7) columns: Int = 7)
{ {
self.parentID = parentID self.parentID = parentID
self.person = person self.person = person
self.genre = genre self.genre = genre
self.studio = studio self.studio = studio
self.filters = filters self.filters = filters
self.columns = columns self.columns = columns
super.init() super.init()
$filters $filters
.sink(receiveValue: requestItems(with:)) .sink(receiveValue: requestItems(with:))
.store(in: &cancellables) .store(in: &cancellables)
} }
func requestItems(with filters: LibraryFilters) { func requestItems(with filters: LibraryFilters) {
let personIDs: [String] = [person].compactMap(\.?.id) let personIDs: [String] = [person].compactMap(\.?.id)
let studioIDs: [String] = [studio].compactMap(\.?.id) let studioIDs: [String] = [studio].compactMap(\.?.id)
let genreIDs: [String] let genreIDs: [String]
if filters.withGenres.isEmpty { if filters.withGenres.isEmpty {
genreIDs = [genre].compactMap(\.?.id) genreIDs = [genre].compactMap(\.?.id)
} else { } else {
genreIDs = filters.withGenres.compactMap(\.id) genreIDs = filters.withGenres.compactMap(\.id)
} }
let sortBy = filters.sortBy.map(\.rawValue) let sortBy = filters.sortBy.map(\.rawValue)
ItemsAPI.getItemsByUserId(userId: SessionManager.main.currentLogin.user.id, ItemsAPI.getItemsByUserId(userId: SessionManager.main.currentLogin.user.id,
startIndex: currentPage * 100, startIndex: currentPage * 100,
limit: 100, limit: 100,
recursive: true, recursive: true,
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,
filters: filters.filters, .seriesPrimaryImage,
sortBy: sortBy, .seasonUserData,
tags: filters.tags, .overview,
enableUserData: true, .genres,
personIds: personIDs, .people,
studioIds: studioIDs, .chapters,
genreIds: genreIDs, ],
enableImages: true) includeItemTypes: filters.filters
.trackActivity(loading) .contains(.isFavorite) ? ["Movie", "Series", "Season", "Episode"] : ["Movie", "Series", "BoxSet"],
.sink(receiveCompletion: { [weak self] completion in filters: filters.filters,
self?.handleAPIRequestError(completion: completion) sortBy: sortBy,
}, receiveValue: { [weak self] response in tags: filters.tags,
LogManager.shared.log.debug("Received \(String(response.items?.count ?? 0)) items in library \(self?.parentID ?? "nil")") enableUserData: true,
guard let self = self else { return } personIds: personIDs,
let totalPages = ceil(Double(response.totalRecordCount ?? 0) / 100.0) studioIds: studioIDs,
self.totalPages = Int(totalPages) genreIds: genreIDs,
self.hasPreviousPage = self.currentPage > 0 enableImages: true)
self.hasNextPage = self.currentPage < self.totalPages - 1 .trackActivity(loading)
self.items = response.items ?? [] .sink(receiveCompletion: { [weak self] completion in
self.rows = self.calculateRows(for: self.items) self?.handleAPIRequestError(completion: completion)
}) }, receiveValue: { [weak self] response in
.store(in: &cancellables) LogManager.shared.log.debug("Received \(String(response.items?.count ?? 0)) items in library \(self?.parentID ?? "nil")")
} guard let self = self else { return }
let totalPages = ceil(Double(response.totalRecordCount ?? 0) / 100.0)
self.totalPages = Int(totalPages)
self.hasPreviousPage = self.currentPage > 0
self.hasNextPage = self.currentPage < self.totalPages - 1
self.items = response.items ?? []
self.rows = self.calculateRows(for: self.items)
})
.store(in: &cancellables)
}
func requestItemsAsync(with filters: LibraryFilters) { func requestItemsAsync(with filters: LibraryFilters) {
let personIDs: [String] = [person].compactMap(\.?.id) let personIDs: [String] = [person].compactMap(\.?.id)
let studioIDs: [String] = [studio].compactMap(\.?.id) let studioIDs: [String] = [studio].compactMap(\.?.id)
let genreIDs: [String] let genreIDs: [String]
if filters.withGenres.isEmpty { if filters.withGenres.isEmpty {
genreIDs = [genre].compactMap(\.?.id) genreIDs = [genre].compactMap(\.?.id)
} else { } else {
genreIDs = filters.withGenres.compactMap(\.id) genreIDs = filters.withGenres.compactMap(\.id)
} }
let sortBy = filters.sortBy.map(\.rawValue) let sortBy = filters.sortBy.map(\.rawValue)
ItemsAPI.getItemsByUserId(userId: SessionManager.main.currentLogin.user.id, startIndex: currentPage * 100, ItemsAPI.getItemsByUserId(userId: SessionManager.main.currentLogin.user.id, startIndex: currentPage * 100,
limit: 100, limit: 100,
recursive: true, recursive: true,
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,
filters: filters.filters, .seriesPrimaryImage,
sortBy: sortBy, .seasonUserData,
tags: filters.tags, .overview,
enableUserData: true, .genres,
personIds: personIDs, .people,
studioIds: studioIDs, .chapters,
genreIds: genreIDs, ],
enableImages: true) includeItemTypes: filters.filters
.sink(receiveCompletion: { [weak self] completion in .contains(.isFavorite) ? ["Movie", "Series", "Season", "Episode"] : ["Movie", "Series"],
self?.handleAPIRequestError(completion: completion) filters: filters.filters,
}, receiveValue: { [weak self] response in sortBy: sortBy,
guard let self = self else { return } tags: filters.tags,
let totalPages = ceil(Double(response.totalRecordCount ?? 0) / 100.0) enableUserData: true,
self.totalPages = Int(totalPages) personIds: personIDs,
self.hasPreviousPage = self.currentPage > 0 studioIds: studioIDs,
self.hasNextPage = self.currentPage < self.totalPages - 1 genreIds: genreIDs,
self.items.append(contentsOf: response.items ?? []) enableImages: true)
self.rows = self.calculateRows(for: self.items) .sink(receiveCompletion: { [weak self] completion in
}) self?.handleAPIRequestError(completion: completion)
.store(in: &cancellables) }, receiveValue: { [weak self] response in
} guard let self = self else { return }
let totalPages = ceil(Double(response.totalRecordCount ?? 0) / 100.0)
self.totalPages = Int(totalPages)
self.hasPreviousPage = self.currentPage > 0
self.hasNextPage = self.currentPage < self.totalPages - 1
self.items.append(contentsOf: response.items ?? [])
self.rows = self.calculateRows(for: self.items)
})
.store(in: &cancellables)
}
func requestNextPage() { func requestNextPage() {
currentPage += 1 currentPage += 1
requestItems(with: filters) requestItems(with: filters)
} }
func requestNextPageAsync() { func requestNextPageAsync() {
currentPage += 1 currentPage += 1
requestItemsAsync(with: filters) requestItemsAsync(with: filters)
} }
func requestPreviousPage() { func requestPreviousPage() {
currentPage -= 1 currentPage -= 1
requestItems(with: filters) requestItems(with: filters)
} }
private func calculateRows(for itemList: [BaseItemDto]) -> [LibraryRow] { private func calculateRows(for itemList: [BaseItemDto]) -> [LibraryRow] {
guard !itemList.isEmpty else { return [] } guard !itemList.isEmpty else { return [] }
let rowCount = itemList.count / columns let rowCount = itemList.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 > itemList.count { if lastItemIndex > itemList.count {
lastItemIndex = itemList.count lastItemIndex = itemList.count
} }
var rowCells = [LibraryRowCell]() var rowCells = [LibraryRowCell]()
for item in itemList[firstItemIndex ..< lastItemIndex] { for item in itemList[firstItemIndex ..< lastItemIndex] {
let newCell = LibraryRowCell(item: item) let newCell = LibraryRowCell(item: item)
rowCells.append(newCell) rowCells.append(newCell)
} }
if i == rowCount, hasNextPage { if i == rowCount, hasNextPage {
var loadingCell = LibraryRowCell(item: nil) var loadingCell = LibraryRowCell(item: nil)
loadingCell.loadingCell = true loadingCell.loadingCell = true
rowCells.append(loadingCell) rowCells.append(loadingCell)
} }
calculatedRows.append(LibraryRow(section: i, calculatedRows.append(LibraryRow(section: i,
items: rowCells)) items: rowCells))
} }
return calculatedRows return calculatedRows
} }
} }

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
@ -14,216 +13,217 @@ import SwiftUICollection
typealias LiveTVChannelRow = CollectionRow<Int, LiveTVChannelRowCell> typealias LiveTVChannelRow = CollectionRow<Int, LiveTVChannelRowCell>
struct LiveTVChannelRowCell: Hashable { struct LiveTVChannelRowCell: Hashable {
let id = UUID() let id = UUID()
let item: LiveTVChannelProgram let item: LiveTVChannelProgram
} }
struct LiveTVChannelProgram: Hashable { struct LiveTVChannelProgram: Hashable {
let id = UUID() let id = UUID()
let channel: BaseItemDto let channel: BaseItemDto
let program: BaseItemDto? let program: BaseItemDto?
} }
final class LiveTVChannelsViewModel: ViewModel { final class LiveTVChannelsViewModel: ViewModel {
@Published var channels = [BaseItemDto]() @Published
@Published var channelPrograms = [LiveTVChannelProgram]() { var channels = [BaseItemDto]()
didSet { @Published
rows = [] var channelPrograms = [LiveTVChannelProgram]() {
let rowChannels = channelPrograms.chunked(into: 4) didSet {
for (index, rowChans) in rowChannels.enumerated() { rows = []
rows.append(LiveTVChannelRow(section: index, items: rowChans.map { LiveTVChannelRowCell(item: $0) })) let rowChannels = channelPrograms.chunked(into: 4)
} for (index, rowChans) in rowChannels.enumerated() {
} rows.append(LiveTVChannelRow(section: index, items: rowChans.map { LiveTVChannelRowCell(item: $0) }))
} }
@Published var rows = [LiveTVChannelRow]() }
}
private var programs = [BaseItemDto]()
private var channelProgramsList = [BaseItemDto: [BaseItemDto]]() @Published
private var timer: Timer? var rows = [LiveTVChannelRow]()
var timeFormatter: DateFormatter { private var programs = [BaseItemDto]()
let df = DateFormatter() private var channelProgramsList = [BaseItemDto: [BaseItemDto]]()
df.dateFormat = "h:mm" private var timer: Timer?
return df
} var timeFormatter: DateFormatter {
let df = DateFormatter()
override init() { df.dateFormat = "h:mm"
super.init() return df
}
getChannels()
startScheduleCheckTimer() override init() {
} super.init()
deinit { getChannels()
stopScheduleCheckTimer() startScheduleCheckTimer()
} }
private func getGuideInfo() { deinit {
LiveTvAPI.getGuideInfo() stopScheduleCheckTimer()
.trackActivity(loading) }
.sink(receiveCompletion: { [weak self] completion in
self?.handleAPIRequestError(completion: completion) private func getGuideInfo() {
}, receiveValue: { [weak self] response in LiveTvAPI.getGuideInfo()
LogManager.shared.log.debug("Received Guide Info") .trackActivity(loading)
guard let self = self else { return } .sink(receiveCompletion: { [weak self] completion in
self.getChannels() self?.handleAPIRequestError(completion: completion)
}) }, receiveValue: { [weak self] _ in
.store(in: &cancellables) LogManager.shared.log.debug("Received Guide Info")
} guard let self = self else { return }
self.getChannels()
func getChannels() { })
LiveTvAPI.getLiveTvChannels( .store(in: &cancellables)
userId: SessionManager.main.currentLogin.user.id, }
startIndex: 0,
limit: 1000, func getChannels() {
enableImageTypes: [.primary], LiveTvAPI.getLiveTvChannels(userId: SessionManager.main.currentLogin.user.id,
enableUserData: false, startIndex: 0,
enableFavoriteSorting: true limit: 1000,
) enableImageTypes: [.primary],
.trackActivity(loading) enableUserData: false,
.sink(receiveCompletion: { [weak self] completion in enableFavoriteSorting: true)
self?.handleAPIRequestError(completion: completion) .trackActivity(loading)
}, receiveValue: { [weak self] response in .sink(receiveCompletion: { [weak self] completion in
LogManager.shared.log.debug("Received \(response.items?.count ?? 0) Channels") self?.handleAPIRequestError(completion: completion)
guard let self = self else { return } }, receiveValue: { [weak self] response in
self.channels = response.items ?? [] LogManager.shared.log.debug("Received \(response.items?.count ?? 0) Channels")
self.getPrograms() guard let self = self else { return }
}) self.channels = response.items ?? []
.store(in: &cancellables) self.getPrograms()
} })
.store(in: &cancellables)
private func getPrograms() { }
// http://192.168.1.50:8096/LiveTv/Programs
guard channels.count > 0 else { private func getPrograms() {
LogManager.shared.log.debug("Cannot get programs, channels list empty. ") // http://192.168.1.50:8096/LiveTv/Programs
return guard !channels.isEmpty else {
} LogManager.shared.log.debug("Cannot get programs, channels list empty. ")
let channelIds = channels.compactMap { $0.id } return
}
let minEndDate = Date.now.addComponentsToDate(hours: -1) let channelIds = channels.compactMap(\.id)
let maxStartDate = minEndDate.addComponentsToDate(hours: 6)
let minEndDate = Date.now.addComponentsToDate(hours: -1)
let getProgramsDto = GetProgramsDto( let maxStartDate = minEndDate.addComponentsToDate(hours: 6)
channelIds: channelIds,
userId: SessionManager.main.currentLogin.user.id, let getProgramsDto = GetProgramsDto(channelIds: channelIds,
maxStartDate: maxStartDate, userId: SessionManager.main.currentLogin.user.id,
minEndDate: minEndDate, maxStartDate: maxStartDate,
sortBy: ["StartDate"], minEndDate: minEndDate,
enableImages: true, sortBy: ["StartDate"],
enableTotalRecordCount: false, enableImages: true,
imageTypeLimit: 1, enableTotalRecordCount: false,
enableImageTypes: [.primary], imageTypeLimit: 1,
enableUserData: false enableImageTypes: [.primary],
) enableUserData: false)
LiveTvAPI.getPrograms(getProgramsDto: getProgramsDto) LiveTvAPI.getPrograms(getProgramsDto: getProgramsDto)
.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] response in
LogManager.shared.log.debug("Received \(response.items?.count ?? 0) Programs") LogManager.shared.log.debug("Received \(response.items?.count ?? 0) Programs")
guard let self = self else { return } guard let self = self else { return }
self.programs = response.items ?? [] self.programs = response.items ?? []
self.channelPrograms = self.processChannelPrograms() self.channelPrograms = self.processChannelPrograms()
}) })
.store(in: &cancellables) .store(in: &cancellables)
} }
private func processChannelPrograms() -> [LiveTVChannelProgram] { private func processChannelPrograms() -> [LiveTVChannelProgram] {
var channelPrograms = [LiveTVChannelProgram]() var channelPrograms = [LiveTVChannelProgram]()
let now = Date() let now = Date()
for channel in self.channels { for channel in self.channels {
let prgs = self.programs.filter { item in let prgs = self.programs.filter { item in
item.channelId == channel.id item.channelId == channel.id
} }
DispatchQueue.main.async { DispatchQueue.main.async {
self.channelProgramsList[channel] = prgs self.channelProgramsList[channel] = prgs
} }
var currentPrg: BaseItemDto? var currentPrg: BaseItemDto?
for prg in prgs { for prg in prgs {
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
} }
}
channelPrograms.append(LiveTVChannelProgram(channel: channel, program: currentPrg))
} channelPrograms.append(LiveTVChannelProgram(channel: channel, program: currentPrg))
return channelPrograms }
} return channelPrograms
}
func startScheduleCheckTimer() {
let date = Date() func startScheduleCheckTimer() {
let calendar = Calendar.current let date = Date()
var components = calendar.dateComponents([.era, .year, .month, .day, .hour, .minute], from: date) let calendar = Calendar.current
var components = calendar.dateComponents([.era, .year, .month, .day, .hour, .minute], from: date)
// Run on 10th min of every hour
guard let minute = components.minute else { return } // Run on 10th min of every hour
components.second = 0 guard let minute = components.minute else { return }
components.minute = minute + (10 - (minute % 10)) components.second = 0
components.minute = minute + (10 - (minute % 10))
guard let nextMinute = calendar.date(from: components) else { return }
guard let nextMinute = calendar.date(from: components) else { return }
if let existingTimer = timer {
existingTimer.invalidate() if let existingTimer = timer {
} existingTimer.invalidate()
timer = Timer(fire: nextMinute, interval: 60 * 10, repeats: true) { [weak self] timer in }
guard let self = self else { return } timer = Timer(fire: nextMinute, interval: 60 * 10, repeats: true) { [weak self] _ in
LogManager.shared.log.debug("LiveTVChannels schedule check...") guard let self = self else { return }
DispatchQueue.global(qos: .background).async { LogManager.shared.log.debug("LiveTVChannels schedule check...")
let newChanPrgs = self.processChannelPrograms() DispatchQueue.global(qos: .background).async {
DispatchQueue.main.async { let newChanPrgs = self.processChannelPrograms()
self.channelPrograms = newChanPrgs DispatchQueue.main.async {
} self.channelPrograms = newChanPrgs
} }
} }
if let timer = timer { }
RunLoop.main.add(timer, forMode: .default) if let timer = timer {
} RunLoop.main.add(timer, forMode: .default)
} }
}
func stopScheduleCheckTimer() {
timer?.invalidate() func stopScheduleCheckTimer() {
} timer?.invalidate()
}
} }
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)])
} }
} }
} }
extension Date { extension Date {
func addComponentsToDate(seconds sec: Int? = nil, minutes min: Int? = nil, hours hrs: Int? = nil, days d: Int? = nil) -> Date { func addComponentsToDate(seconds sec: Int? = nil, minutes min: Int? = nil, hours hrs: Int? = nil, days d: Int? = nil) -> Date {
var dc = DateComponents() var dc = DateComponents()
if let sec = sec { if let sec = sec {
dc.second = sec dc.second = sec
} }
if let min = min { if let min = min {
dc.minute = min dc.minute = min
} }
if let hrs = hrs { if let hrs = hrs {
dc.hour = hrs dc.hour = hrs
} }
if let d = d { if let d = d {
dc.day = d dc.day = d
} }
return Calendar.current.date(byAdding: dc, to: self)! return Calendar.current.date(byAdding: dc, to: self)!
} }
func midnightUTCDate() -> Date { func midnightUTCDate() -> Date {
var dc: DateComponents = Calendar.current.dateComponents([.year, .month, .day], from: self) var dc: DateComponents = Calendar.current.dateComponents([.year, .month, .day], from: self)
dc.hour = 0 dc.hour = 0
dc.minute = 0 dc.minute = 0
dc.second = 0 dc.second = 0
dc.nanosecond = 0 dc.nanosecond = 0
dc.timeZone = TimeZone(secondsFromGMT: 0) dc.timeZone = TimeZone(secondsFromGMT: 0)
return Calendar.current.date(from: dc)! return Calendar.current.date(from: dc)!
} }
} }

View File

@ -1,204 +1,200 @@
// //
/* // 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
private var channels = [String:BaseItemDto]() var sportsItems = [BaseItemDto]()
@Published
override init() { var kidsItems = [BaseItemDto]()
super.init() @Published
var newsItems = [BaseItemDto]()
getChannels()
} private var channels = [String: BaseItemDto]()
func findChannel(id: String) -> BaseItemDto? { override init() {
return channels[id] super.init()
}
getChannels()
private func getChannels() { }
LiveTvAPI.getLiveTvChannels(
userId: SessionManager.main.currentLogin.user.id, func findChannel(id: String) -> BaseItemDto? {
startIndex: 0, channels[id]
limit: 1000, }
enableImageTypes: [.primary],
enableUserData: false, private func getChannels() {
enableFavoriteSorting: true LiveTvAPI.getLiveTvChannels(userId: SessionManager.main.currentLogin.user.id,
) startIndex: 0,
.trackActivity(loading) limit: 1000,
.sink(receiveCompletion: { [weak self] completion in enableImageTypes: [.primary],
self?.handleAPIRequestError(completion: completion) enableUserData: false,
}, receiveValue: { [weak self] response in enableFavoriteSorting: true)
LogManager.shared.log.debug("Received \(response.items?.count ?? 0) Channels") .trackActivity(loading)
guard let self = self else { return } .sink(receiveCompletion: { [weak self] completion in
if let chans = response.items { self?.handleAPIRequestError(completion: completion)
for chan in chans { }, receiveValue: { [weak self] response in
if let chanId = chan.id { LogManager.shared.log.debug("Received \(response.items?.count ?? 0) Channels")
self.channels[chanId] = chan guard let self = self else { return }
} if let chans = response.items {
} for chan in chans {
self.getRecommendedPrograms() if let chanId = chan.id {
self.getSeries() self.channels[chanId] = chan
self.getMovies() }
self.getSports() }
self.getKids() self.getRecommendedPrograms()
self.getNews() self.getSeries()
} self.getMovies()
}) self.getSports()
.store(in: &cancellables) self.getKids()
} self.getNews()
}
private func getRecommendedPrograms() { })
LiveTvAPI.getRecommendedPrograms( .store(in: &cancellables)
userId: SessionManager.main.currentLogin.user.id, }
limit: 9,
isAiring: true, private func getRecommendedPrograms() {
imageTypeLimit: 1, LiveTvAPI.getRecommendedPrograms(userId: SessionManager.main.currentLogin.user.id,
enableImageTypes: [.primary, .thumb], limit: 9,
fields: [.channelInfo, .primaryImageAspectRatio], isAiring: true,
enableTotalRecordCount: false imageTypeLimit: 1,
) enableImageTypes: [.primary, .thumb],
.trackActivity(loading) fields: [.channelInfo, .primaryImageAspectRatio],
.sink(receiveCompletion: { [weak self] completion in enableTotalRecordCount: false)
self?.handleAPIRequestError(completion: completion) .trackActivity(loading)
}, receiveValue: { [weak self] response in .sink(receiveCompletion: { [weak self] completion in
LogManager.shared.log.debug("Received \(String(response.items?.count ?? 0)) Recommended Programs") self?.handleAPIRequestError(completion: completion)
guard let self = self else { return } }, receiveValue: { [weak self] response in
self.recommendedItems = response.items ?? [] LogManager.shared.log.debug("Received \(String(response.items?.count ?? 0)) Recommended Programs")
}) guard let self = self else { return }
.store(in: &cancellables) self.recommendedItems = response.items ?? []
} })
.store(in: &cancellables)
private func getSeries() { }
let getProgramsDto = GetProgramsDto(userId: SessionManager.main.currentLogin.user.id,
hasAired: false, private func getSeries() {
isMovie: false, let getProgramsDto = GetProgramsDto(userId: SessionManager.main.currentLogin.user.id,
isSeries: true, hasAired: false,
isNews: false, isMovie: false,
isKids: false, isSeries: true,
isSports: false, isNews: false,
limit: 9, isKids: false,
enableTotalRecordCount: false, isSports: false,
enableImageTypes: [.primary, .thumb], limit: 9,
fields: [.channelInfo, .primaryImageAspectRatio] enableTotalRecordCount: false,
) enableImageTypes: [.primary, .thumb],
fields: [.channelInfo, .primaryImageAspectRatio])
LiveTvAPI.getPrograms(getProgramsDto: getProgramsDto)
.trackActivity(loading) LiveTvAPI.getPrograms(getProgramsDto: getProgramsDto)
.sink(receiveCompletion: { [weak self] completion in .trackActivity(loading)
self?.handleAPIRequestError(completion: completion) .sink(receiveCompletion: { [weak self] completion in
}, receiveValue: { [weak self] response in self?.handleAPIRequestError(completion: completion)
LogManager.shared.log.debug("Received \(String(response.items?.count ?? 0)) Series Items") }, receiveValue: { [weak self] response in
guard let self = self else { return } LogManager.shared.log.debug("Received \(String(response.items?.count ?? 0)) Series Items")
self.seriesItems = response.items ?? [] guard let self = self else { return }
}) self.seriesItems = response.items ?? []
.store(in: &cancellables) })
} .store(in: &cancellables)
}
private func getMovies() {
let getProgramsDto = GetProgramsDto(userId: SessionManager.main.currentLogin.user.id, private func getMovies() {
hasAired: false, let getProgramsDto = GetProgramsDto(userId: SessionManager.main.currentLogin.user.id,
isMovie: true, hasAired: false,
isSeries: false, isMovie: true,
isNews: false, isSeries: false,
isKids: false, isNews: false,
isSports: false, isKids: false,
limit: 9, isSports: false,
enableTotalRecordCount: false, limit: 9,
enableImageTypes: [.primary, .thumb], enableTotalRecordCount: false,
fields: [.channelInfo, .primaryImageAspectRatio] enableImageTypes: [.primary, .thumb],
) fields: [.channelInfo, .primaryImageAspectRatio])
LiveTvAPI.getPrograms(getProgramsDto: getProgramsDto) LiveTvAPI.getPrograms(getProgramsDto: getProgramsDto)
.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] response in
LogManager.shared.log.debug("Received \(String(response.items?.count ?? 0)) Movie Items") LogManager.shared.log.debug("Received \(String(response.items?.count ?? 0)) Movie Items")
guard let self = self else { return } guard let self = self else { return }
self.movieItems = response.items ?? [] self.movieItems = response.items ?? []
}) })
.store(in: &cancellables) .store(in: &cancellables)
} }
private func getSports() { private func getSports() {
let getProgramsDto = GetProgramsDto(userId: SessionManager.main.currentLogin.user.id, let getProgramsDto = GetProgramsDto(userId: SessionManager.main.currentLogin.user.id,
hasAired: false, hasAired: false,
isSports: true, isSports: true,
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) .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] response in LogManager.shared.log.debug("Received \(String(response.items?.count ?? 0)) Sports Items")
LogManager.shared.log.debug("Received \(String(response.items?.count ?? 0)) Sports Items") guard let self = self else { return }
guard let self = self else { return } self.sportsItems = response.items ?? []
self.sportsItems = response.items ?? [] })
}) .store(in: &cancellables)
.store(in: &cancellables) }
}
private func getKids() {
private func getKids() { let getProgramsDto = GetProgramsDto(userId: SessionManager.main.currentLogin.user.id,
let getProgramsDto = GetProgramsDto(userId: SessionManager.main.currentLogin.user.id, hasAired: false,
hasAired: false, isKids: true,
isKids: true, limit: 9,
limit: 9, enableTotalRecordCount: false,
enableTotalRecordCount: false, enableImageTypes: [.primary, .thumb],
enableImageTypes: [.primary, .thumb], fields: [.channelInfo, .primaryImageAspectRatio])
fields: [.channelInfo, .primaryImageAspectRatio]
) LiveTvAPI.getPrograms(getProgramsDto: getProgramsDto)
.trackActivity(loading)
LiveTvAPI.getPrograms(getProgramsDto: getProgramsDto) .sink(receiveCompletion: { [weak self] completion in
.trackActivity(loading) self?.handleAPIRequestError(completion: completion)
.sink(receiveCompletion: { [weak self] completion in }, receiveValue: { [weak self] response in
self?.handleAPIRequestError(completion: completion) LogManager.shared.log.debug("Received \(String(response.items?.count ?? 0)) Kids Items")
}, receiveValue: { [weak self] response in guard let self = self else { return }
LogManager.shared.log.debug("Received \(String(response.items?.count ?? 0)) Kids Items") self.kidsItems = response.items ?? []
guard let self = self else { return } })
self.kidsItems = response.items ?? [] .store(in: &cancellables)
}) }
.store(in: &cancellables)
} private func getNews() {
let getProgramsDto = GetProgramsDto(userId: SessionManager.main.currentLogin.user.id,
private func getNews() { hasAired: false,
let getProgramsDto = GetProgramsDto(userId: SessionManager.main.currentLogin.user.id, isNews: true,
hasAired: false, limit: 9,
isNews: true, enableTotalRecordCount: false,
limit: 9, enableImageTypes: [.primary, .thumb],
enableTotalRecordCount: false, fields: [.channelInfo, .primaryImageAspectRatio])
enableImageTypes: [.primary, .thumb],
fields: [.channelInfo, .primaryImageAspectRatio] LiveTvAPI.getPrograms(getProgramsDto: getProgramsDto)
) .trackActivity(loading)
.sink(receiveCompletion: { [weak self] completion in
LiveTvAPI.getPrograms(getProgramsDto: getProgramsDto) self?.handleAPIRequestError(completion: completion)
.trackActivity(loading) }, receiveValue: { [weak self] response in
.sink(receiveCompletion: { [weak self] completion in LogManager.shared.log.debug("Received \(String(response.items?.count ?? 0)) News Items")
self?.handleAPIRequestError(completion: completion) guard let self = self else { return }
}, receiveValue: { [weak self] response in self.newsItems = response.items ?? []
LogManager.shared.log.debug("Received \(String(response.items?.count ?? 0)) News Items") })
guard let self = self else { return } .store(in: &cancellables)
self.newsItems = response.items ?? [] }
})
.store(in: &cancellables)
}
} }

View File

@ -1,30 +1,33 @@
// //
/* // 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()
let nc = NotificationCenter.default let nc = NotificationCenter.default
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
self.lastBackgroundURL = self.backgroundURL func backgroundDidChange() {
self.backgroundURL = BackgroundManager.current.backgroundURL self.lastBackgroundURL = self.backgroundURL
self.backgroundBlurHash = BackgroundManager.current.blurhash self.backgroundURL = BackgroundManager.current.backgroundURL
} 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,81 +14,79 @@ 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
@RouterObject @RouterObject
var router: MovieLibrariesCoordinator.Router? var router: MovieLibrariesCoordinator.Router?
init( init(columns: Int = 7) {
columns: Int = 7 self.columns = columns
) { super.init()
self.columns = columns
super.init()
requestLibraries() requestLibraries()
} }
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) }, receiveValue: { response in
}, receiveValue: { response in if let responseItems = response.items {
if let responseItems = response.items { self.libraries = []
self.libraries = [] for library in responseItems {
for library in responseItems { if library.collectionType == "movies" {
if library.collectionType == "movies" { self.libraries.append(library)
self.libraries.append(library) }
} }
} self.rows = self.calculateRows()
self.rows = self.calculateRows() if self.libraries.count == 1, let library = self.libraries.first {
if self.libraries.count == 1, let library = self.libraries.first { // show library
// show library self.router?.route(to: \.library, library)
self.router?.route(to: \.library, library) }
} }
} })
}) .store(in: &cancellables)
.store(in: &cancellables) }
}
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 {
lastItemIndex = libraries.count lastItemIndex = libraries.count
} }
var rowCells = [LibraryRowCell]() var rowCells = [LibraryRowCell]()
for item in libraries[firstItemIndex..<lastItemIndex] { for item in libraries[firstItemIndex ..< lastItemIndex] {
let newCell = LibraryRowCell(item: item) let newCell = LibraryRowCell(item: item)
rowCells.append(newCell) rowCells.append(newCell)
} }
if i == rowCount && hasNextPage { if i == rowCount && hasNextPage {
var loadingCell = LibraryRowCell(item: nil) var loadingCell = LibraryRowCell(item: nil)
loadingCell.loadingCell = true loadingCell.loadingCell = true
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,33 +1,33 @@
// //
/* // 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
} }
func setServerCurrentURI(uri: String) { func setServerCurrentURI(uri: String) {
SessionManager.main.setServerCurrentURI(server: server, uri: uri) SessionManager.main.setServerCurrentURI(server: server, uri: uri)
.sink { c in .sink { c in
print(c) print(c)
} receiveValue: { newServerState in } receiveValue: { newServerState in
self.server = newServerState self.server = newServerState
let nc = SwiftfinNotificationCenter.main let nc = SwiftfinNotificationCenter.main
nc.post(name: SwiftfinNotificationCenter.Keys.didChangeServerCurrentURI, object: newServerState) nc.post(name: SwiftfinNotificationCenter.Keys.didChangeServerCurrentURI, object: newServerState)
} }
.store(in: &cancellables) .store(in: &cancellables)
} }
} }

View File

@ -1,47 +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 SwiftUI
class ServerListViewModel: ObservableObject { class ServerListViewModel: ObservableObject {
@Published var servers: [SwiftfinStore.State.Server] = [] @Published
var servers: [SwiftfinStore.State.Server] = []
init() { init() {
// Oct. 15, 2021 // Oct. 15, 2021
// This is a workaround since Stinsen doesn't have the ability to rebuild a root at the time of writing. // This is a workaround since Stinsen doesn't have the ability to rebuild a root at the time of writing.
// Feature request issue: https://github.com/rundfunk47/stinsen/issues/33 // Feature request issue: https://github.com/rundfunk47/stinsen/issues/33
// Go to each MainCoordinator and implement the rebuild of the root when receiving the notification // Go to each MainCoordinator and implement the rebuild of the root when receiving the notification
let nc = SwiftfinNotificationCenter.main let nc = SwiftfinNotificationCenter.main
nc.addObserver(self, selector: #selector(didPurge), name: SwiftfinNotificationCenter.Keys.didPurge, object: nil) nc.addObserver(self, selector: #selector(didPurge), name: SwiftfinNotificationCenter.Keys.didPurge, object: nil)
} }
func fetchServers() { func fetchServers() {
self.servers = SessionManager.main.fetchServers() self.servers = SessionManager.main.fetchServers()
} }
func userTextFor(server: SwiftfinStore.State.Server) -> String { func userTextFor(server: SwiftfinStore.State.Server) -> String {
if server.userIDs.count == 1 { if server.userIDs.count == 1 {
return "1 user" return "1 user"
} else { } else {
return "\(server.userIDs.count) users" return "\(server.userIDs.count) users"
} }
} }
func remove(server: SwiftfinStore.State.Server) { func remove(server: SwiftfinStore.State.Server) {
SessionManager.main.delete(server: server) SessionManager.main.delete(server: server)
fetchServers() fetchServers()
} }
@objc private func didPurge() { @objc
fetchServers() private func didPurge() {
} fetchServers()
}
} }

View File

@ -1,48 +1,47 @@
// //
/* // 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 {
var bitrates: [Bitrates] = []
var langs: [TrackLanguage] = []
let server: SwiftfinStore.State.Server
let user: SwiftfinStore.State.User
init(server: SwiftfinStore.State.Server, user: SwiftfinStore.State.User) { var bitrates: [Bitrates] = []
var langs: [TrackLanguage] = []
self.server = server
self.user = user
// Bitrates
let url = Bundle.main.url(forResource: "bitrates", withExtension: "json")!
do { let server: SwiftfinStore.State.Server
let jsonData = try Data(contentsOf: url, options: .mappedIfSafe) let user: SwiftfinStore.State.User
do {
self.bitrates = try JSONDecoder().decode([Bitrates].self, from: jsonData)
} catch {
LogManager.shared.log.error("Error converting processed JSON into Swift compatible schema.")
}
} catch {
LogManager.shared.log.error("Error processing JSON file `bitrates.json`")
}
// Track languages init(server: SwiftfinStore.State.Server, user: SwiftfinStore.State.User) {
self.langs = Locale.isoLanguageCodes.compactMap {
guard let name = Locale.current.localizedString(forLanguageCode: $0) else { return nil } self.server = server
return TrackLanguage(name: name, isoCode: $0) self.user = user
}.sorted(by: { $0.name < $1.name })
self.langs.insert(.auto, at: 0) // Bitrates
} let url = Bundle.main.url(forResource: "bitrates", withExtension: "json")!
do {
let jsonData = try Data(contentsOf: url, options: .mappedIfSafe)
do {
self.bitrates = try JSONDecoder().decode([Bitrates].self, from: jsonData)
} catch {
LogManager.shared.log.error("Error converting processed JSON into Swift compatible schema.")
}
} catch {
LogManager.shared.log.error("Error processing JSON file `bitrates.json`")
}
// Track languages
self.langs = Locale.isoLanguageCodes.compactMap {
guard let name = Locale.current.localizedString(forLanguageCode: $0) else { return nil }
return TrackLanguage(name: name, isoCode: $0)
}.sorted(by: { $0.name < $1.name })
self.langs.insert(.auto, at: 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 //
*/
import Combine import Combine
import Foundation import Foundation
@ -15,81 +14,79 @@ 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
@RouterObject @RouterObject
var router: TVLibrariesCoordinator.Router? var router: TVLibrariesCoordinator.Router?
init( init(columns: Int = 7) {
columns: Int = 7 self.columns = columns
) { super.init()
self.columns = columns
super.init()
requestLibraries() requestLibraries()
} }
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) }, receiveValue: { response in
}, receiveValue: { response in if let responseItems = response.items {
if let responseItems = response.items { self.libraries = []
self.libraries = [] for library in responseItems {
for library in responseItems { if library.collectionType == "tvshows" {
if library.collectionType == "tvshows" { self.libraries.append(library)
self.libraries.append(library) }
} }
} self.rows = self.calculateRows()
self.rows = self.calculateRows() if self.libraries.count == 1, let library = self.libraries.first {
if self.libraries.count == 1, let library = self.libraries.first { // show library
// show library self.router?.route(to: \.library, library)
self.router?.route(to: \.library, library) }
} }
} })
}) .store(in: &cancellables)
.store(in: &cancellables) }
}
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 {
lastItemIndex = libraries.count lastItemIndex = libraries.count
} }
var rowCells = [LibraryRowCell]() var rowCells = [LibraryRowCell]()
for item in libraries[firstItemIndex..<lastItemIndex] { for item in libraries[firstItemIndex ..< lastItemIndex] {
let newCell = LibraryRowCell(item: item) let newCell = LibraryRowCell(item: item)
rowCells.append(newCell) rowCells.append(newCell)
} }
if i == rowCount && hasNextPage { if i == rowCount && hasNextPage {
var loadingCell = LibraryRowCell(item: nil) var loadingCell = LibraryRowCell(item: nil)
loadingCell.loadingCell = true loadingCell.loadingCell = true
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,47 +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 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
init(server: SwiftfinStore.State.Server) { init(server: SwiftfinStore.State.Server) {
self.server = server self.server = server
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
guard let newServerState = notification.object as? SwiftfinStore.State.Server else { fatalError("Need to have new state server") } func didChangeCurrentLoginURI(_ notification: Notification) {
self.server = newServerState guard let newServerState = notification.object as? SwiftfinStore.State.Server else { fatalError("Need to have new state server") }
} self.server = newServerState
}
func fetchUsers() { func fetchUsers() {
self.users = SessionManager.main.fetchUsers(for: server) self.users = SessionManager.main.fetchUsers(for: server)
} }
func login(user: SwiftfinStore.State.User) { func login(user: SwiftfinStore.State.User) {
self.isLoading = true self.isLoading = true
SessionManager.main.loginUser(server: server, user: user) SessionManager.main.loginUser(server: server, user: user)
} }
func remove(user: SwiftfinStore.State.User) {
SessionManager.main.delete(user: user)
fetchUsers()
}
func remove(user: SwiftfinStore.State.User) {
SessionManager.main.delete(user: user)
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,42 +12,42 @@ import Stinsen
final class UserSignInViewModel: ViewModel { final class UserSignInViewModel: ViewModel {
@RouterObject var router: UserSignInCoordinator.Router? @RouterObject
let server: SwiftfinStore.State.Server var router: UserSignInCoordinator.Router?
let server: SwiftfinStore.State.Server
init(server: SwiftfinStore.State.Server) { init(server: SwiftfinStore.State.Server) {
self.server = server self.server = server
} }
var alertTitle: String { var alertTitle: String {
var message: String = "" var message: String = ""
if errorMessage?.code != ErrorMessage.noShowErrorCode { if errorMessage?.code != ErrorMessage.noShowErrorCode {
message.append(contentsOf: "\(errorMessage?.code ?? ErrorMessage.noShowErrorCode)\n") message.append(contentsOf: "\(errorMessage?.code ?? ErrorMessage.noShowErrorCode)\n")
} }
message.append(contentsOf: "\(errorMessage?.title ?? "Unkown Error")") message.append(contentsOf: "\(errorMessage?.title ?? "Unkown Error")")
return message return message
} }
func login(username: String, password: String) { func login(username: String, password: String) {
LogManager.shared.log.debug("Attempting to login to server at \"\(server.currentURI)\"", tag: "login") LogManager.shared.log.debug("Attempting to login to server at \"\(server.currentURI)\"", tag: "login")
LogManager.shared.log.debug("username: \(username), password: \(password)", tag: "login") LogManager.shared.log.debug("username: \(username), password: \(password)", tag: "login")
SessionManager.main.loginUser(server: server, username: username, password: password) SessionManager.main.loginUser(server: server, username: username, password: password)
.trackActivity(loading) .trackActivity(loading)
.sink { completion in .sink { completion in
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)
}
} func cancelSignIn() {
.store(in: &cancellables) for cancellable in cancellables {
} cancellable.cancel()
}
func cancelSignIn() { self.isLoading = false
for cancellable in cancellables { }
cancellable.cancel()
}
self.isLoading = false
}
} }

View File

@ -1,31 +1,32 @@
// //
/* // 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
var id: Int32 var id: Int32
var url: URL? var url: URL?
var delivery: SubtitleDeliveryMethod var delivery: SubtitleDeliveryMethod
var codec: String var codec: String
var languageCode: String var languageCode: String
} }
struct AudioTrack { struct AudioTrack {
var name: String var name: String
var languageCode: String var languageCode: String
var id: Int32 var id: Int32
} }
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,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
enum ServerStreamType { enum ServerStreamType {
case direct case direct
case transcode case transcode
} }

View File

@ -1,75 +1,82 @@
// //
/* // 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>()
init() { init() {
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,
switch completion { file: String = #file, line: UInt = #line, completion: Subscribers.Completion<Error>)
case .finished: {
self.errorMessage = nil switch completion {
case .failure(let error): case .finished:
let logConstructor = LogConstructor(message: "__NOTHING__", tag: tag, level: logLevel, function: function, file: file, line: line) self.errorMessage = nil
case let .failure(error):
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:
let networkError: NetworkError let networkError: NetworkError
let errorResponse = error as! ErrorResponse let errorResponse = error as! ErrorResponse
switch errorResponse { switch errorResponse {
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
case .error(-2, _, _, _): .error("Request failed: URL request failed with error \(networkError.errorMessage.code): \(errorResponse.localizedDescription)")
networkError = .HTTPURLError(response: errorResponse, displayMessage: displayMessage, logConstructor: logConstructor) case .error(-2, _, _, _):
LogManager.shared.log.error("Request failed: HTTP URL request failed with description: \(errorResponse.localizedDescription)") networkError = .HTTPURLError(response: errorResponse, displayMessage: displayMessage, logConstructor: logConstructor)
default: LogManager.shared.log
networkError = .JellyfinError(response: errorResponse, displayMessage: displayMessage, logConstructor: logConstructor) .error("Request failed: HTTP URL request failed with description: \(errorResponse.localizedDescription)")
// Able to use user-facing friendly description here since just HTTP status codes default:
LogManager.shared.log.error("Request failed: \(networkError.errorMessage.code) - \(networkError.errorMessage.title): \(networkError.errorMessage.logConstructor.message)\n\(error.localizedDescription)") networkError = .JellyfinError(response: errorResponse, displayMessage: displayMessage, logConstructor: logConstructor)
} // 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)")
}
self.errorMessage = networkError.errorMessage self.errorMessage = networkError.errorMessage
networkError.logMessage() networkError.logMessage()
case is SwiftfinStore.Errors: case is SwiftfinStore.Errors:
let swiftfinError = error as! SwiftfinStore.Errors let swiftfinError = error as! SwiftfinStore.Errors
let errorMessage = ErrorMessage(code: ErrorMessage.noShowErrorCode, let errorMessage = ErrorMessage(code: ErrorMessage.noShowErrorCode,
title: swiftfinError.title, title: swiftfinError.title,
displayMessage: swiftfinError.errorDescription ?? "", displayMessage: swiftfinError.errorDescription ?? "",
logConstructor: logConstructor) logConstructor: logConstructor)
self.errorMessage = errorMessage self.errorMessage = errorMessage
LogManager.shared.log.error("Request failed: \(swiftfinError.errorDescription ?? "")") LogManager.shared.log.error("Request failed: \(swiftfinError.errorDescription ?? "")")
default: default:
let genericErrorMessage = ErrorMessage(code: ErrorMessage.noShowErrorCode, let genericErrorMessage = ErrorMessage(code: ErrorMessage.noShowErrorCode,
title: "Generic Error", title: "Generic Error",
displayMessage: error.localizedDescription, displayMessage: error.localizedDescription,
logConstructor: logConstructor) logConstructor: logConstructor)
self.errorMessage = genericErrorMessage self.errorMessage = genericErrorMessage
LogManager.shared.log.error("Request failed: Generic error - \(error.localizedDescription)") LogManager.shared.log.error("Request failed: Generic error - \(error.localizedDescription)")
} }
} }
} }
} }

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