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
--indent 4 #indent
--self init-only # redundantSelf
--semicolons never # semicolons
--stripunusedargs closure-only # unusedArguments
--maxwidth 140 #wrap
--assetliterals visual-width #wrap
--wraparguments after-first # wrapArguments
--wrapparameters after-first # wrapArguments
--wrapcollections before-first # wrapArguments
--wrapconditions after-first # wrapArguments
--funcattributes prev-line # wrapAttributes
--typeattributes prev-line # wrapAttributes
--varattributes prev-line # wrapAttributes
--swiftversion 5.5
--indent tab
--tabwidth 4
--xcodeindentation enabled
--self init-only
--semicolons never
--stripunusedargs closure-only
--maxwidth 140
--assetliterals visual-width
--wraparguments after-first
--wrapparameters after-first
--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
* 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
*/
// 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
import Stinsen
@ -13,11 +12,13 @@ import SwiftUI
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 {
BasicAppSettingsView(viewModel: BasicAppSettingsViewModel())
}
@ViewBuilder
func makeStart() -> some View {
BasicAppSettingsView(viewModel: BasicAppSettingsViewModel())
}
}

View File

@ -1,11 +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
* file, you can obtain one at https://mozilla.org/MPL/2.0/.
*
* Copyright 2021 Aiden Vigue & Jellyfin Contributors
*/
// 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
import Stinsen
@ -13,16 +12,19 @@ import SwiftUI
final class ConnectToServerCoodinator: NavigationCoordinatable {
let stack = NavigationStack(initial: \ConnectToServerCoodinator.start)
let stack = NavigationStack(initial: \ConnectToServerCoodinator.start)
@Root var start = makeStart
@Route(.push) var userSignIn = makeUserSignIn
@Root
var start = makeStart
@Route(.push)
var userSignIn = makeUserSignIn
func makeUserSignIn(server: SwiftfinStore.State.Server) -> UserSignInCoordinator {
return UserSignInCoordinator(viewModel: .init(server: server))
}
func makeUserSignIn(server: SwiftfinStore.State.Server) -> UserSignInCoordinator {
UserSignInCoordinator(viewModel: .init(server: server))
}
@ViewBuilder func makeStart() -> some View {
ConnectToServerView(viewModel: ConnectToServerViewModel())
}
@ViewBuilder
func makeStart() -> some View {
ConnectToServerView(viewModel: ConnectToServerViewModel())
}
}

View File

@ -1,11 +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
* file, you can obtain one at https://mozilla.org/MPL/2.0/.
*
* Copyright 2021 Aiden Vigue & Jellyfin Contributors
*/
// 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
import Stinsen
@ -15,21 +14,24 @@ typealias FilterCoordinatorParams = (filters: Binding<LibraryFilters>, enabledFi
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
var enabledFilterType: [FilterType]
var parentId: String = ""
@Binding
var filters: LibraryFilters
var enabledFilterType: [FilterType]
var parentId: String = ""
init(filters: Binding<LibraryFilters>, enabledFilterType: [FilterType], parentId: String) {
_filters = filters
self.enabledFilterType = enabledFilterType
self.parentId = parentId
}
init(filters: Binding<LibraryFilters>, enabledFilterType: [FilterType], parentId: String) {
_filters = filters
self.enabledFilterType = enabledFilterType
self.parentId = parentId
}
@ViewBuilder func makeStart() -> some View {
LibraryFilterView(filters: $filters, enabledFilterType: enabledFilterType, parentId: parentId)
}
@ViewBuilder
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
* 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
*/
// 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
import JellyfinAPI
@ -14,36 +13,43 @@ import SwiftUI
final class HomeCoordinator: NavigationCoordinatable {
let stack = NavigationStack(initial: \HomeCoordinator.start)
let stack = NavigationStack(initial: \HomeCoordinator.start)
@Root var start = makeStart
@Route(.modal) var settings = makeSettings
@Route(.push) var library = makeLibrary
@Route(.push) var item = makeItem
@Route(.modal) var modalItem = makeModalItem
@Route(.modal) var modalLibrary = makeModalLibrary
@Root
var start = makeStart
@Route(.modal)
var settings = makeSettings
@Route(.push)
var library = makeLibrary
@Route(.push)
var item = makeItem
@Route(.modal)
var modalItem = makeModalItem
@Route(.modal)
var modalLibrary = makeModalLibrary
func makeSettings() -> NavigationViewCoordinator<SettingsCoordinator> {
NavigationViewCoordinator(SettingsCoordinator())
}
func makeSettings() -> NavigationViewCoordinator<SettingsCoordinator> {
NavigationViewCoordinator(SettingsCoordinator())
}
func makeLibrary(params: LibraryCoordinatorParams) -> LibraryCoordinator {
LibraryCoordinator(viewModel: params.viewModel, title: params.title)
}
func makeLibrary(params: LibraryCoordinatorParams) -> LibraryCoordinator {
LibraryCoordinator(viewModel: params.viewModel, title: params.title)
}
func makeItem(item: BaseItemDto) -> ItemCoordinator {
ItemCoordinator(item: item)
}
func makeItem(item: BaseItemDto) -> ItemCoordinator {
ItemCoordinator(item: item)
}
func makeModalItem(item: BaseItemDto) -> NavigationViewCoordinator<ItemCoordinator> {
return NavigationViewCoordinator(ItemCoordinator(item: item))
}
func makeModalItem(item: BaseItemDto) -> NavigationViewCoordinator<ItemCoordinator> {
NavigationViewCoordinator(ItemCoordinator(item: item))
}
func makeModalLibrary(params: LibraryCoordinatorParams) -> NavigationViewCoordinator<LibraryCoordinator> {
return NavigationViewCoordinator(LibraryCoordinator(viewModel: params.viewModel, title: params.title))
}
func makeModalLibrary(params: LibraryCoordinatorParams) -> NavigationViewCoordinator<LibraryCoordinator> {
NavigationViewCoordinator(LibraryCoordinator(viewModel: params.viewModel, title: params.title))
}
@ViewBuilder func makeStart() -> some View {
HomeView()
}
@ViewBuilder
func makeStart() -> some View {
HomeView()
}
}

View File

@ -1,11 +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
* file, you can obtain one at https://mozilla.org/MPL/2.0/.
*
* Copyright 2021 Aiden Vigue & Jellyfin Contributors
*/
// 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
import JellyfinAPI
@ -14,37 +13,43 @@ import SwiftUI
final class ItemCoordinator: NavigationCoordinatable {
let stack = NavigationStack(initial: \ItemCoordinator.start)
let stack = NavigationStack(initial: \ItemCoordinator.start)
@Root var start = makeStart
@Route(.push) var item = makeItem
@Route(.push) var library = makeLibrary
@Route(.modal) var itemOverview = makeItemOverview
@Route(.fullScreen) var videoPlayer = makeVideoPlayer
@Root
var start = makeStart
@Route(.push)
var item = makeItem
@Route(.push)
var library = makeLibrary
@Route(.modal)
var itemOverview = makeItemOverview
@Route(.fullScreen)
var videoPlayer = makeVideoPlayer
let itemDto: BaseItemDto
let itemDto: BaseItemDto
init(item: BaseItemDto) {
self.itemDto = item
}
init(item: BaseItemDto) {
self.itemDto = item
}
func makeLibrary(params: LibraryCoordinatorParams) -> LibraryCoordinator {
LibraryCoordinator(viewModel: params.viewModel, title: params.title)
}
func makeLibrary(params: LibraryCoordinatorParams) -> LibraryCoordinator {
LibraryCoordinator(viewModel: params.viewModel, title: params.title)
}
func makeItem(item: BaseItemDto) -> ItemCoordinator {
ItemCoordinator(item: item)
}
func makeItemOverview(item: BaseItemDto) -> NavigationViewCoordinator<ItemOverviewCoordinator> {
NavigationViewCoordinator(ItemOverviewCoordinator(item: itemDto))
}
func makeItem(item: BaseItemDto) -> ItemCoordinator {
ItemCoordinator(item: item)
}
func makeVideoPlayer(viewModel: VideoPlayerViewModel) -> NavigationViewCoordinator<VideoPlayerCoordinator> {
NavigationViewCoordinator(VideoPlayerCoordinator(viewModel: viewModel))
}
func makeItemOverview(item: BaseItemDto) -> NavigationViewCoordinator<ItemOverviewCoordinator> {
NavigationViewCoordinator(ItemOverviewCoordinator(item: itemDto))
}
@ViewBuilder func makeStart() -> some View {
ItemNavigationView(item: itemDto)
}
func makeVideoPlayer(viewModel: VideoPlayerViewModel) -> NavigationViewCoordinator<VideoPlayerCoordinator> {
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
* 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
*/
// 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 JellyfinAPI
import Stinsen
import SwiftUI
import JellyfinAPI
final class ItemOverviewCoordinator: NavigationCoordinatable {
let stack = NavigationStack(initial: \ItemOverviewCoordinator.start)
@Root var start = makeStart
let item: BaseItemDto
init(item: BaseItemDto) {
self.item = item
}
@ViewBuilder func makeStart() -> some View {
#if os(tvOS)
EmptyView()
#else
ItemOverviewView(item: item)
#endif
}
let stack = NavigationStack(initial: \ItemOverviewCoordinator.start)
@Root
var start = makeStart
let item: BaseItemDto
init(item: BaseItemDto) {
self.item = item
}
@ViewBuilder
func makeStart() -> some View {
#if os(tvOS)
EmptyView()
#else
ItemOverviewView(item: item)
#endif
}
}

View File

@ -1,11 +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
* file, you can obtain one at https://mozilla.org/MPL/2.0/.
*
* Copyright 2021 Aiden Vigue & Jellyfin Contributors
*/
// 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
import JellyfinAPI
@ -16,41 +15,47 @@ typealias LibraryCoordinatorParams = (viewModel: LibraryViewModel, title: String
final class LibraryCoordinator: NavigationCoordinatable {
let stack = NavigationStack(initial: \LibraryCoordinator.start)
let stack = NavigationStack(initial: \LibraryCoordinator.start)
@Root var start = makeStart
@Route(.push) var search = makeSearch
@Route(.modal) var filter = makeFilter
@Route(.push) var item = makeItem
@Route(.modal) var modalItem = makeModalItem
@Root
var start = makeStart
@Route(.push)
var search = makeSearch
@Route(.modal)
var filter = makeFilter
@Route(.push)
var item = makeItem
@Route(.modal)
var modalItem = makeModalItem
let viewModel: LibraryViewModel
let title: String
let viewModel: LibraryViewModel
let title: String
init(viewModel: LibraryViewModel, title: String) {
self.viewModel = viewModel
self.title = title
}
init(viewModel: LibraryViewModel, title: String) {
self.viewModel = viewModel
self.title = title
}
@ViewBuilder func makeStart() -> some View {
LibraryView(viewModel: self.viewModel, title: title)
}
@ViewBuilder
func makeStart() -> some View {
LibraryView(viewModel: self.viewModel, title: title)
}
func makeSearch(viewModel: LibrarySearchViewModel) -> SearchCoordinator {
SearchCoordinator(viewModel: viewModel)
}
func makeSearch(viewModel: LibrarySearchViewModel) -> SearchCoordinator {
SearchCoordinator(viewModel: viewModel)
}
func makeFilter(params: FilterCoordinatorParams) -> NavigationViewCoordinator<FilterCoordinator> {
NavigationViewCoordinator(FilterCoordinator(filters: params.filters,
enabledFilterType: params.enabledFilterType,
parentId: params.parentId))
}
func makeFilter(params: FilterCoordinatorParams) -> NavigationViewCoordinator<FilterCoordinator> {
NavigationViewCoordinator(FilterCoordinator(filters: params.filters,
enabledFilterType: params.enabledFilterType,
parentId: params.parentId))
}
func makeItem(item: BaseItemDto) -> ItemCoordinator {
ItemCoordinator(item: item)
}
func makeItem(item: BaseItemDto) -> ItemCoordinator {
ItemCoordinator(item: item)
}
func makeModalItem(item: BaseItemDto) -> NavigationViewCoordinator<ItemCoordinator> {
return NavigationViewCoordinator(ItemCoordinator(item: item))
}
func makeModalItem(item: BaseItemDto) -> NavigationViewCoordinator<ItemCoordinator> {
NavigationViewCoordinator(ItemCoordinator(item: item))
}
}

View File

@ -1,11 +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
* file, you can obtain one at https://mozilla.org/MPL/2.0/.
*
* Copyright 2021 Aiden Vigue & Jellyfin Contributors
*/
// 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
import Stinsen
@ -13,28 +12,31 @@ import SwiftUI
final class LibraryListCoordinator: NavigationCoordinatable {
let stack = NavigationStack(initial: \LibraryListCoordinator.start)
let stack = NavigationStack(initial: \LibraryListCoordinator.start)
@Root var start = makeStart
@Route(.push) var search = makeSearch
@Route(.push) var library = makeLibrary
let viewModel: LibraryListViewModel
@Root
var start = makeStart
@Route(.push)
var search = makeSearch
@Route(.push)
var library = makeLibrary
init(viewModel: LibraryListViewModel) {
self.viewModel = viewModel
}
func makeLibrary(params: LibraryCoordinatorParams) -> LibraryCoordinator {
LibraryCoordinator(viewModel: params.viewModel, title: params.title)
}
let viewModel: LibraryListViewModel
func makeSearch(viewModel: LibrarySearchViewModel) -> SearchCoordinator {
SearchCoordinator(viewModel: viewModel)
}
init(viewModel: LibraryListViewModel) {
self.viewModel = viewModel
}
@ViewBuilder
func makeStart() -> some View {
LibraryListView(viewModel: self.viewModel)
}
func makeLibrary(params: LibraryCoordinatorParams) -> LibraryCoordinator {
LibraryCoordinator(viewModel: params.viewModel, title: params.title)
}
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
* 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
*/
// 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
import JellyfinAPI
@ -13,34 +12,38 @@ import Stinsen
import SwiftUI
final class LiveTVChannelsCoordinator: NavigationCoordinatable {
let stack = NavigationStack(initial: \LiveTVChannelsCoordinator.start)
let stack = NavigationStack(initial: \LiveTVChannelsCoordinator.start)
@Root var start = makeStart
@Route(.modal) var modalItem = makeModalItem
@Route(.fullScreen) var videoPlayer = makeVideoPlayer
func makeModalItem(item: BaseItemDto) -> NavigationViewCoordinator<ItemCoordinator> {
return NavigationViewCoordinator(ItemCoordinator(item: item))
}
func makeVideoPlayer(item: BaseItemDto) -> NavigationViewCoordinator<EmptyViewCoordinator> {
// NavigationViewCoordinator(VideoPlayerCoordinator(item: item))
NavigationViewCoordinator(EmptyViewCoordinator())
}
@ViewBuilder
func makeStart() -> some View {
LiveTVChannelsView()
}
@Root
var start = makeStart
@Route(.modal)
var modalItem = makeModalItem
@Route(.fullScreen)
var videoPlayer = makeVideoPlayer
func makeModalItem(item: BaseItemDto) -> NavigationViewCoordinator<ItemCoordinator> {
NavigationViewCoordinator(ItemCoordinator(item: item))
}
func makeVideoPlayer(item: BaseItemDto) -> NavigationViewCoordinator<EmptyViewCoordinator> {
// NavigationViewCoordinator(VideoPlayerCoordinator(item: item))
NavigationViewCoordinator(EmptyViewCoordinator())
}
@ViewBuilder
func makeStart() -> some View {
LiveTVChannelsView()
}
}
final class EmptyViewCoordinator: NavigationCoordinatable {
let stack = NavigationStack(initial: \EmptyViewCoordinator.start)
let stack = NavigationStack(initial: \EmptyViewCoordinator.start)
@Root var start = makeStart
@ViewBuilder
func makeStart() -> some View {
Text("Empty")
}
@Root
var start = makeStart
@ViewBuilder
func makeStart() -> some View {
Text("Empty")
}
}

View File

@ -1,11 +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
* file, you can obtain one at https://mozilla.org/MPL/2.0/.
*
* Copyright 2021 Aiden Vigue & Jellyfin Contributors
*/
// 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
import JellyfinAPI
@ -13,19 +12,21 @@ import Stinsen
import SwiftUI
final class LiveTVProgramsCoordinator: NavigationCoordinatable {
let stack = NavigationStack(initial: \LiveTVProgramsCoordinator.start)
@Root var start = makeStart
@Route(.fullScreen) var videoPlayer = makeVideoPlayer
func makeVideoPlayer(item: BaseItemDto) -> NavigationViewCoordinator<EmptyViewCoordinator> {
// NavigationViewCoordinator(VideoPlayerCoordinator(item: item))
NavigationViewCoordinator(EmptyViewCoordinator())
}
@ViewBuilder
func makeStart() -> some View {
LiveTVProgramsView()
}
let stack = NavigationStack(initial: \LiveTVProgramsCoordinator.start)
@Root
var start = makeStart
@Route(.fullScreen)
var videoPlayer = makeVideoPlayer
func makeVideoPlayer(item: BaseItemDto) -> NavigationViewCoordinator<EmptyViewCoordinator> {
// NavigationViewCoordinator(VideoPlayerCoordinator(item: item))
NavigationViewCoordinator(EmptyViewCoordinator())
}
@ViewBuilder
func makeStart() -> some View {
LiveTVProgramsView()
}
}

View File

@ -1,57 +1,62 @@
//
/*
* 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 2021 Aiden Vigue & Jellyfin Contributors
*/
// 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
import SwiftUI
import Stinsen
import SwiftUI
final class LiveTVTabCoordinator: TabCoordinatable {
var child = TabChild(startingItems: [
\LiveTVTabCoordinator.programs,
\LiveTVTabCoordinator.channels,
\LiveTVTabCoordinator.home
])
@Route(tabItem: makeProgramsTab) var programs = makePrograms
@Route(tabItem: makeChannelsTab) var channels = makeChannels
@Route(tabItem: makeHomeTab) var home = makeHome
func makePrograms() -> NavigationViewCoordinator<LiveTVProgramsCoordinator> {
return NavigationViewCoordinator(LiveTVProgramsCoordinator())
}
@ViewBuilder func makeProgramsTab(isActive: Bool) -> some View {
HStack {
Image(systemName: "tv")
Text("Programs")
}
}
func makeChannels() -> NavigationViewCoordinator<LiveTVChannelsCoordinator> {
return NavigationViewCoordinator(LiveTVChannelsCoordinator())
}
@ViewBuilder func makeChannelsTab(isActive: Bool) -> some View {
HStack {
Image(systemName: "square.grid.3x3")
Text("Channels")
}
}
func makeHome() -> LiveTVHomeView {
return LiveTVHomeView()
}
@ViewBuilder func makeHomeTab(isActive: Bool) -> some View {
HStack {
Image(systemName: "house")
Text("Home")
}
}
var child = TabChild(startingItems: [
\LiveTVTabCoordinator.programs,
\LiveTVTabCoordinator.channels,
\LiveTVTabCoordinator.home,
])
@Route(tabItem: makeProgramsTab)
var programs = makePrograms
@Route(tabItem: makeChannelsTab)
var channels = makeChannels
@Route(tabItem: makeHomeTab)
var home = makeHome
func makePrograms() -> NavigationViewCoordinator<LiveTVProgramsCoordinator> {
NavigationViewCoordinator(LiveTVProgramsCoordinator())
}
@ViewBuilder
func makeProgramsTab(isActive: Bool) -> some View {
HStack {
Image(systemName: "tv")
Text("Programs")
}
}
func makeChannels() -> NavigationViewCoordinator<LiveTVChannelsCoordinator> {
NavigationViewCoordinator(LiveTVChannelsCoordinator())
}
@ViewBuilder
func makeChannelsTab(isActive: Bool) -> some View {
HStack {
Image(systemName: "square.grid.3x3")
Text("Channels")
}
}
func makeHome() -> LiveTVHomeView {
LiveTVHomeView()
}
@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
* 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
*/
// 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 Combine
import Defaults
@ -16,83 +15,91 @@ import SwiftUI
import WidgetKit
final class MainCoordinator: NavigationCoordinatable {
var stack: NavigationStack<MainCoordinator>
var stack: NavigationStack<MainCoordinator>
@Root var mainTab = makeMainTab
@Root var serverList = makeServerList
private var cancellables = Set<AnyCancellable>()
@Root
var mainTab = makeMainTab
@Root
var serverList = makeServerList
init() {
if SessionManager.main.currentLogin != nil {
self.stack = NavigationStack(initial: \MainCoordinator.mainTab)
} else {
self.stack = NavigationStack(initial: \MainCoordinator.serverList)
}
private var cancellables = Set<AnyCancellable>()
ImageCache.shared.costLimit = 125 * 1024 * 1024 // 125MB memory
DataLoader.sharedUrlCache.diskCapacity = 1000 * 1024 * 1024 // 1000MB disk
init() {
if SessionManager.main.currentLogin != nil {
self.stack = NavigationStack(initial: \MainCoordinator.mainTab)
} else {
self.stack = NavigationStack(initial: \MainCoordinator.serverList)
}
WidgetCenter.shared.reloadAllTimelines()
UIScrollView.appearance().keyboardDismissMode = .onDrag
ImageCache.shared.costLimit = 125 * 1024 * 1024 // 125MB memory
DataLoader.sharedUrlCache.diskCapacity = 1000 * 1024 * 1024 // 1000MB disk
// Back bar button item setup
let backButtonBackgroundImage = UIImage(systemName: "chevron.backward.circle.fill")
let barAppearance = UINavigationBar.appearance()
barAppearance.backIndicatorImage = backButtonBackgroundImage
barAppearance.backIndicatorTransitionMaskImage = backButtonBackgroundImage
barAppearance.tintColor = UIColor(Color.jellyfinPurple)
WidgetCenter.shared.reloadAllTimelines()
UIScrollView.appearance().keyboardDismissMode = .onDrag
// Notification setup for state
let nc = SwiftfinNotificationCenter.main
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)
Defaults.publisher(.appAppearance)
.sink { _ in
JellyfinPlayerApp.setupAppearance()
}
.store(in: &cancellables)
}
// Back bar button item setup
let backButtonBackgroundImage = UIImage(systemName: "chevron.backward.circle.fill")
let barAppearance = UINavigationBar.appearance()
barAppearance.backIndicatorImage = backButtonBackgroundImage
barAppearance.backIndicatorTransitionMaskImage = backButtonBackgroundImage
barAppearance.tintColor = UIColor(Color.jellyfinPurple)
@objc func didLogIn() {
LogManager.shared.log.info("Received `didSignIn` from SwiftfinNotificationCenter.")
root(\.mainTab)
}
// Notification setup for state
let nc = SwiftfinNotificationCenter.main
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() {
LogManager.shared.log.info("Received `didSignOut` from SwiftfinNotificationCenter.")
root(\.serverList)
}
Defaults.publisher(.appAppearance)
.sink { _ in
JellyfinPlayerApp.setupAppearance()
}
.store(in: &cancellables)
}
@objc 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)
}
}
}
@objc
func didLogIn() {
LogManager.shared.log.info("Received `didSignIn` from SwiftfinNotificationCenter.")
root(\.mainTab)
}
@objc 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)
}
}
@objc
func didLogOut() {
LogManager.shared.log.info("Received `didSignOut` from SwiftfinNotificationCenter.")
root(\.serverList)
}
func makeMainTab() -> MainTabCoordinator {
MainTabCoordinator()
}
@objc
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> {
NavigationViewCoordinator(ServerListCoordinator())
}
@objc
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
* 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
*/
// 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
import SwiftUI
import Stinsen
import SwiftUI
final class MainTabCoordinator: TabCoordinatable {
var child = TabChild(startingItems: [
\MainTabCoordinator.home,
\MainTabCoordinator.allMedia
])
var child = TabChild(startingItems: [
\MainTabCoordinator.home,
\MainTabCoordinator.allMedia,
])
@Route(tabItem: makeHomeTab) var home = makeHome
@Route(tabItem: makeAllMediaTab) var allMedia = makeAllMedia
@Route(tabItem: makeHomeTab)
var home = makeHome
@Route(tabItem: makeAllMediaTab)
var allMedia = makeAllMedia
func makeHome() -> NavigationViewCoordinator<HomeCoordinator> {
return NavigationViewCoordinator(HomeCoordinator())
}
func makeHome() -> NavigationViewCoordinator<HomeCoordinator> {
NavigationViewCoordinator(HomeCoordinator())
}
@ViewBuilder func makeHomeTab(isActive: Bool) -> some View {
Image(systemName: "house")
L10n.home.text
}
@ViewBuilder
func makeHomeTab(isActive: Bool) -> some View {
Image(systemName: "house")
L10n.home.text
}
func makeAllMedia() -> NavigationViewCoordinator<LibraryListCoordinator> {
return NavigationViewCoordinator(LibraryListCoordinator(viewModel: LibraryListViewModel()))
}
func makeAllMedia() -> NavigationViewCoordinator<LibraryListCoordinator> {
NavigationViewCoordinator(LibraryListCoordinator(viewModel: LibraryListViewModel()))
}
@ViewBuilder func makeAllMediaTab(isActive: Bool) -> some View {
Image(systemName: "folder")
L10n.allMedia.text
}
@ViewBuilder
func makeAllMediaTab(isActive: Bool) -> some View {
Image(systemName: "folder")
L10n.allMedia.text
}
@ViewBuilder func customize(_ view: AnyView) -> some View {
view.onAppear {
AppURLHandler.shared.appURLState = .allowed
// TODO: todo
DispatchQueue.main.asyncAfter(deadline: .now() + 0.25) {
AppURLHandler.shared.processLaunchedURLIfNeeded()
}
}
}
@ViewBuilder
func customize(_ view: AnyView) -> some View {
view.onAppear {
AppURLHandler.shared.appURLState = .allowed
// TODO: todo
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
* 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
*/
// 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
import Nuke
@ -13,55 +12,60 @@ import Stinsen
import SwiftUI
final class MainCoordinator: NavigationCoordinatable {
var stack = NavigationStack<MainCoordinator>(initial: \MainCoordinator.mainTab)
var stack = NavigationStack<MainCoordinator>(initial: \MainCoordinator.mainTab)
@Root var mainTab = makeMainTab
@Root var serverList = makeServerList
@Root var liveTV = makeLiveTV
@Root
var mainTab = makeMainTab
@Root
var serverList = makeServerList
@Root
var liveTV = makeLiveTV
@ViewBuilder
func customize(_ view: AnyView) -> some View {
view.background {
Color.black
.ignoresSafeArea()
}
}
@ViewBuilder
func customize(_ view: AnyView) -> some View {
view.background {
Color.black
.ignoresSafeArea()
}
}
init() {
if SessionManager.main.currentLogin != nil {
self.stack = NavigationStack(initial: \MainCoordinator.mainTab)
} else {
self.stack = NavigationStack(initial: \MainCoordinator.serverList)
}
init() {
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
DataLoader.sharedUrlCache.diskCapacity = 1000 * 1024 * 1024 // 1000MB disk
ImageCache.shared.costLimit = 125 * 1024 * 1024 // 125MB memory
DataLoader.sharedUrlCache.diskCapacity = 1000 * 1024 * 1024 // 1000MB disk
// Notification setup for state
let nc = SwiftfinNotificationCenter.main
nc.addObserver(self, selector: #selector(didLogIn), name: SwiftfinNotificationCenter.Keys.didSignIn, object: nil)
nc.addObserver(self, selector: #selector(didLogOut), name: SwiftfinNotificationCenter.Keys.didSignOut, object: nil)
}
// Notification setup for state
let nc = SwiftfinNotificationCenter.main
nc.addObserver(self, selector: #selector(didLogIn), name: SwiftfinNotificationCenter.Keys.didSignIn, object: nil)
nc.addObserver(self, selector: #selector(didLogOut), name: SwiftfinNotificationCenter.Keys.didSignOut, object: nil)
}
@objc func didLogIn() {
LogManager.shared.log.info("Received `didSignIn` from NSNotificationCenter.")
root(\.mainTab)
}
@objc
func didLogIn() {
LogManager.shared.log.info("Received `didSignIn` from NSNotificationCenter.")
root(\.mainTab)
}
@objc func didLogOut() {
LogManager.shared.log.info("Received `didSignOut` from NSNotificationCenter.")
root(\.serverList)
}
@objc
func didLogOut() {
LogManager.shared.log.info("Received `didSignOut` from NSNotificationCenter.")
root(\.serverList)
}
func makeMainTab() -> MainTabCoordinator {
MainTabCoordinator()
}
func makeMainTab() -> MainTabCoordinator {
MainTabCoordinator()
}
func makeServerList() -> NavigationViewCoordinator<ServerListCoordinator> {
NavigationViewCoordinator(ServerListCoordinator())
}
func makeServerList() -> NavigationViewCoordinator<ServerListCoordinator> {
NavigationViewCoordinator(ServerListCoordinator())
}
func makeLiveTV() -> LiveTVTabCoordinator {
LiveTVTabCoordinator()
}
func makeLiveTV() -> LiveTVTabCoordinator {
LiveTVTabCoordinator()
}
}

View File

@ -1,80 +1,89 @@
//
/*
* 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 2021 Aiden Vigue & Jellyfin Contributors
*/
// 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
import SwiftUI
import Stinsen
import SwiftUI
final class MainTabCoordinator: TabCoordinatable {
var child = TabChild(startingItems: [
\MainTabCoordinator.home,
\MainTabCoordinator.tv,
\MainTabCoordinator.movies,
\MainTabCoordinator.other,
\MainTabCoordinator.settings
])
var child = TabChild(startingItems: [
\MainTabCoordinator.home,
\MainTabCoordinator.tv,
\MainTabCoordinator.movies,
\MainTabCoordinator.other,
\MainTabCoordinator.settings,
])
@Route(tabItem: makeHomeTab) var home = makeHome
@Route(tabItem: makeTvTab) var tv = makeTv
@Route(tabItem: makeMoviesTab) var movies = makeMovies
@Route(tabItem: makeOtherTab) var other = makeOther
@Route(tabItem: makeSettingsTab) var settings = makeSettings
@Route(tabItem: makeHomeTab)
var home = makeHome
@Route(tabItem: makeTvTab)
var tv = makeTv
@Route(tabItem: makeMoviesTab)
var movies = makeMovies
@Route(tabItem: makeOtherTab)
var other = makeOther
@Route(tabItem: makeSettingsTab)
var settings = makeSettings
func makeHome() -> NavigationViewCoordinator<HomeCoordinator> {
return NavigationViewCoordinator(HomeCoordinator())
}
func makeHome() -> NavigationViewCoordinator<HomeCoordinator> {
NavigationViewCoordinator(HomeCoordinator())
}
@ViewBuilder func makeHomeTab(isActive: Bool) -> some View {
HStack {
Image(systemName: "house")
L10n.home.text
}
}
@ViewBuilder
func makeHomeTab(isActive: Bool) -> some View {
HStack {
Image(systemName: "house")
L10n.home.text
}
}
func makeTv() -> NavigationViewCoordinator<TVLibrariesCoordinator> {
return NavigationViewCoordinator(TVLibrariesCoordinator(viewModel: TVLibrariesViewModel(), title: "TV Shows"))
}
func makeTv() -> NavigationViewCoordinator<TVLibrariesCoordinator> {
NavigationViewCoordinator(TVLibrariesCoordinator(viewModel: TVLibrariesViewModel(), title: "TV Shows"))
}
@ViewBuilder func makeTvTab(isActive: Bool) -> some View {
HStack {
Image(systemName: "tv")
Text("TV Shows")
}
}
@ViewBuilder
func makeTvTab(isActive: Bool) -> some View {
HStack {
Image(systemName: "tv")
Text("TV Shows")
}
}
func makeMovies() -> NavigationViewCoordinator<MovieLibrariesCoordinator> {
return NavigationViewCoordinator(MovieLibrariesCoordinator(viewModel: MovieLibrariesViewModel(), title: "Movies"))
}
func makeMovies() -> NavigationViewCoordinator<MovieLibrariesCoordinator> {
NavigationViewCoordinator(MovieLibrariesCoordinator(viewModel: MovieLibrariesViewModel(), title: "Movies"))
}
@ViewBuilder func makeMoviesTab(isActive: Bool) -> some View {
HStack {
Image(systemName: "film")
Text("Movies")
}
}
@ViewBuilder
func makeMoviesTab(isActive: Bool) -> some View {
HStack {
Image(systemName: "film")
Text("Movies")
}
}
func makeOther() -> NavigationViewCoordinator<LibraryListCoordinator> {
return NavigationViewCoordinator(LibraryListCoordinator(viewModel: LibraryListViewModel()))
}
func makeOther() -> NavigationViewCoordinator<LibraryListCoordinator> {
NavigationViewCoordinator(LibraryListCoordinator(viewModel: LibraryListViewModel()))
}
@ViewBuilder func makeOtherTab(isActive: Bool) -> some View {
HStack {
Image(systemName: "folder")
Text("Other")
}
}
@ViewBuilder
func makeOtherTab(isActive: Bool) -> some View {
HStack {
Image(systemName: "folder")
Text("Other")
}
}
func makeSettings() -> NavigationViewCoordinator<SettingsCoordinator> {
return NavigationViewCoordinator(SettingsCoordinator())
}
func makeSettings() -> NavigationViewCoordinator<SettingsCoordinator> {
NavigationViewCoordinator(SettingsCoordinator())
}
@ViewBuilder func makeSettingsTab(isActive: Bool) -> some View {
Image(systemName: "gearshape.fill")
}
@ViewBuilder
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
* 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
*/
// 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
import JellyfinAPI
@ -14,24 +13,27 @@ import SwiftUI
final class MovieLibrariesCoordinator: NavigationCoordinatable {
let stack = NavigationStack(initial: \MovieLibrariesCoordinator.start)
let stack = NavigationStack(initial: \MovieLibrariesCoordinator.start)
@Root var start = makeStart
@Route(.push) var library = makeLibrary
@Root
var start = makeStart
@Route(.push)
var library = makeLibrary
let viewModel: MovieLibrariesViewModel
let title: String
let viewModel: MovieLibrariesViewModel
let title: String
init(viewModel: MovieLibrariesViewModel, title: String) {
self.viewModel = viewModel
self.title = title
}
init(viewModel: MovieLibrariesViewModel, title: String) {
self.viewModel = viewModel
self.title = title
}
@ViewBuilder func makeStart() -> some View {
MovieLibrariesView(viewModel: self.viewModel, title: title)
}
@ViewBuilder
func makeStart() -> some View {
MovieLibrariesView(viewModel: self.viewModel, title: title)
}
func makeLibrary(library: BaseItemDto) -> LibraryCoordinator {
LibraryCoordinator(viewModel: LibraryViewModel(parentID: library.id), title: library.title)
}
func makeLibrary(library: BaseItemDto) -> LibraryCoordinator {
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
* 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
*/
// 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
import JellyfinAPI
import Stinsen
import SwiftUI
import JellyfinAPI
final class SearchCoordinator: NavigationCoordinatable {
let stack = NavigationStack(initial: \SearchCoordinator.start)
let stack = NavigationStack(initial: \SearchCoordinator.start)
@Root var start = makeStart
@Route(.push) var item = makeItem
@Root
var start = makeStart
@Route(.push)
var item = makeItem
let viewModel: LibrarySearchViewModel
let viewModel: LibrarySearchViewModel
init(viewModel: LibrarySearchViewModel) {
self.viewModel = viewModel
}
init(viewModel: LibrarySearchViewModel) {
self.viewModel = viewModel
}
func makeItem(item: BaseItemDto) -> ItemCoordinator {
ItemCoordinator(item: item)
}
func makeItem(item: BaseItemDto) -> ItemCoordinator {
ItemCoordinator(item: item)
}
@ViewBuilder func makeStart() -> some View {
LibrarySearchView(viewModel: self.viewModel)
}
@ViewBuilder
func makeStart() -> some View {
LibrarySearchView(viewModel: self.viewModel)
}
}

View File

@ -1,11 +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
* file, you can obtain one at https://mozilla.org/MPL/2.0/.
*
* Copyright 2021 Aiden Vigue & Jellyfin Contributors
*/
// 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
import Stinsen
@ -13,17 +12,19 @@ import SwiftUI
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) {
self.viewModel = viewModel
}
init(viewModel: ServerDetailViewModel) {
self.viewModel = viewModel
}
@ViewBuilder func makeStart() -> some View {
ServerDetailView(viewModel: viewModel)
}
@ViewBuilder
func makeStart() -> some View {
ServerDetailView(viewModel: viewModel)
}
}

View File

@ -1,11 +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
* file, you can obtain one at https://mozilla.org/MPL/2.0/.
*
* Copyright 2021 Aiden Vigue & Jellyfin Contributors
*/
// 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
import Stinsen
@ -13,26 +12,31 @@ import SwiftUI
final class ServerListCoordinator: NavigationCoordinatable {
let stack = NavigationStack(initial: \ServerListCoordinator.start)
let stack = NavigationStack(initial: \ServerListCoordinator.start)
@Root var start = makeStart
@Route(.push) var connectToServer = makeConnectToServer
@Route(.push) var userList = makeUserList
@Route(.modal) var basicAppSettings = makeBasicAppSettings
@Root
var start = makeStart
@Route(.push)
var connectToServer = makeConnectToServer
@Route(.push)
var userList = makeUserList
@Route(.modal)
var basicAppSettings = makeBasicAppSettings
func makeConnectToServer() -> ConnectToServerCoodinator {
ConnectToServerCoodinator()
}
func makeConnectToServer() -> ConnectToServerCoodinator {
ConnectToServerCoodinator()
}
func makeUserList(server: SwiftfinStore.State.Server) -> UserListCoordinator {
UserListCoordinator(viewModel: .init(server: server))
}
func makeUserList(server: SwiftfinStore.State.Server) -> UserListCoordinator {
UserListCoordinator(viewModel: .init(server: server))
}
func makeBasicAppSettings() -> NavigationViewCoordinator<BasicAppSettingsCoordinator> {
NavigationViewCoordinator(BasicAppSettingsCoordinator())
}
func makeBasicAppSettings() -> NavigationViewCoordinator<BasicAppSettingsCoordinator> {
NavigationViewCoordinator(BasicAppSettingsCoordinator())
}
@ViewBuilder func makeStart() -> some View {
ServerListView(viewModel: ServerListViewModel())
}
@ViewBuilder
func makeStart() -> some View {
ServerListView(viewModel: ServerListViewModel())
}
}

View File

@ -1,11 +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
* file, you can obtain one at https://mozilla.org/MPL/2.0/.
*
* Copyright 2021 Aiden Vigue & Jellyfin Contributors
*/
// 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
import Stinsen
@ -13,28 +12,36 @@ import SwiftUI
final class SettingsCoordinator: NavigationCoordinatable {
let stack = NavigationStack(initial: \SettingsCoordinator.start)
let stack = NavigationStack(initial: \SettingsCoordinator.start)
@Root var start = makeStart
@Route(.push) var serverDetail = makeServerDetail
@Route(.push) var overlaySettings = makeOverlaySettings
@Route(.push) var experimentalSettings = makeExperimentalSettings
@Root
var start = makeStart
@Route(.push)
var serverDetail = makeServerDetail
@Route(.push)
var overlaySettings = makeOverlaySettings
@Route(.push)
var experimentalSettings = makeExperimentalSettings
@ViewBuilder func makeServerDetail() -> some View {
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 makeServerDetail() -> some View {
let viewModel = ServerDetailViewModel(server: SessionManager.main.currentLogin.server)
ServerDetailView(viewModel: viewModel)
}
@ViewBuilder func makeStart() -> some View {
let viewModel = SettingsViewModel(server: SessionManager.main.currentLogin.server, user: SessionManager.main.currentLogin.user)
SettingsView(viewModel: viewModel)
}
@ViewBuilder
func makeOverlaySettings() -> some View {
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
* 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
*/
// 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
import JellyfinAPI
@ -14,24 +13,27 @@ import SwiftUI
final class TVLibrariesCoordinator: NavigationCoordinatable {
let stack = NavigationStack(initial: \TVLibrariesCoordinator.start)
let stack = NavigationStack(initial: \TVLibrariesCoordinator.start)
@Root var start = makeStart
@Route(.push) var library = makeLibrary
@Root
var start = makeStart
@Route(.push)
var library = makeLibrary
let viewModel: TVLibrariesViewModel
let title: String
let viewModel: TVLibrariesViewModel
let title: String
init(viewModel: TVLibrariesViewModel, title: String) {
self.viewModel = viewModel
self.title = title
}
init(viewModel: TVLibrariesViewModel, title: String) {
self.viewModel = viewModel
self.title = title
}
@ViewBuilder func makeStart() -> some View {
TVLibrariesView(viewModel: self.viewModel, title: title)
}
@ViewBuilder
func makeStart() -> some View {
TVLibrariesView(viewModel: self.viewModel, title: title)
}
func makeLibrary(library: BaseItemDto) -> LibraryCoordinator {
LibraryCoordinator(viewModel: LibraryViewModel(parentID: library.id), title: library.title)
}
func makeLibrary(library: BaseItemDto) -> LibraryCoordinator {
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
* 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
*/
// 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
import Stinsen
@ -13,27 +12,31 @@ import SwiftUI
final class UserListCoordinator: NavigationCoordinatable {
let stack = NavigationStack(initial: \UserListCoordinator.start)
let stack = NavigationStack(initial: \UserListCoordinator.start)
@Root var start = makeStart
@Route(.push) var userSignIn = makeUserSignIn
@Route(.push) var serverDetail = makeServerDetail
@Root
var start = makeStart
@Route(.push)
var userSignIn = makeUserSignIn
@Route(.push)
var serverDetail = makeServerDetail
let viewModel: UserListViewModel
let viewModel: UserListViewModel
init(viewModel: UserListViewModel) {
self.viewModel = viewModel
}
init(viewModel: UserListViewModel) {
self.viewModel = viewModel
}
func makeUserSignIn(server: SwiftfinStore.State.Server) -> UserSignInCoordinator {
return UserSignInCoordinator(viewModel: .init(server: server))
}
func makeUserSignIn(server: SwiftfinStore.State.Server) -> UserSignInCoordinator {
UserSignInCoordinator(viewModel: .init(server: server))
}
func makeServerDetail(server: SwiftfinStore.State.Server) -> ServerDetailCoordinator {
return ServerDetailCoordinator(viewModel: .init(server: server))
}
func makeServerDetail(server: SwiftfinStore.State.Server) -> ServerDetailCoordinator {
ServerDetailCoordinator(viewModel: .init(server: server))
}
@ViewBuilder func makeStart() -> some View {
UserListView(viewModel: viewModel)
}
@ViewBuilder
func makeStart() -> some View {
UserListView(viewModel: viewModel)
}
}

View File

@ -1,11 +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
* file, you can obtain one at https://mozilla.org/MPL/2.0/.
*
* Copyright 2021 Aiden Vigue & Jellyfin Contributors
*/
// 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
import Stinsen
@ -13,17 +12,19 @@ import SwiftUI
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) {
self.viewModel = viewModel
}
init(viewModel: UserSignInViewModel) {
self.viewModel = viewModel
}
@ViewBuilder func makeStart() -> some View {
UserSignInView(viewModel: viewModel)
}
@ViewBuilder
func makeStart() -> some View {
UserSignInView(viewModel: viewModel)
}
}

View File

@ -1,11 +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
* file, you can obtain one at https://mozilla.org/MPL/2.0/.
*
* Copyright 2021 Aiden Vigue & Jellyfin Contributors
*/
// 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 Defaults
import Foundation
@ -15,24 +14,26 @@ import SwiftUI
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) {
self.viewModel = viewModel
}
init(viewModel: VideoPlayerViewModel) {
self.viewModel = viewModel
}
@ViewBuilder func makeStart() -> some View {
PreferenceUIHostingControllerView {
VLCPlayerView(viewModel: self.viewModel)
.navigationBarHidden(true)
.statusBar(hidden: true)
.ignoresSafeArea()
.prefersHomeIndicatorAutoHidden(true)
.supportedOrientations(UIDevice.current.userInterfaceIdiom == .pad ? .all : .landscape)
}.ignoresSafeArea()
}
@ViewBuilder
func makeStart() -> some View {
PreferenceUIHostingControllerView {
VLCPlayerView(viewModel: self.viewModel)
.navigationBarHidden(true)
.statusBar(hidden: true)
.ignoresSafeArea()
.prefersHomeIndicatorAutoHidden(true)
.supportedOrientations(UIDevice.current.userInterfaceIdiom == .pad ? .all : .landscape)
}.ignoresSafeArea()
}
}

View File

@ -1,11 +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
* file, you can obtain one at https://mozilla.org/MPL/2.0/.
*
* Copyright 2021 Aiden Vigue & Jellyfin Contributors
*/
// 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 Defaults
import Foundation
@ -15,20 +14,21 @@ import SwiftUI
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) {
self.viewModel = viewModel
}
init(viewModel: VideoPlayerViewModel) {
self.viewModel = viewModel
}
@ViewBuilder func makeStart() -> some View {
VLCPlayerView(viewModel: viewModel)
.navigationBarHidden(true)
.ignoresSafeArea()
}
@ViewBuilder
func makeStart() -> some View {
VLCPlayerView(viewModel: viewModel)
.navigationBarHidden(true)
.ignoresSafeArea()
}
}

View File

@ -1,35 +1,34 @@
//
/*
* 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 2021 Aiden Vigue & Jellyfin Contributors
*/
// 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
import JellyfinAPI
struct ErrorMessage: Identifiable {
let code: Int
let title: String
let displayMessage: String
let logConstructor: LogConstructor
let code: Int
let title: String
let displayMessage: String
let logConstructor: LogConstructor
// 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
static let noShowErrorCode = -69420
// 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
static let noShowErrorCode = -69420
var id: String {
return "\(code)\(title)\(logConstructor.message)"
}
var id: String {
"\(code)\(title)\(logConstructor.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) {
self.code = code
self.title = title
self.displayMessage = displayMessage ?? logConstructor.message
self.logConstructor = logConstructor
}
/// 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) {
self.code = code
self.title = title
self.displayMessage = displayMessage ?? logConstructor.message
self.logConstructor = logConstructor
}
}

View File

@ -1,20 +1,19 @@
//
/*
* 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 2021 Aiden Vigue & Jellyfin Contributors
*/
// 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
import JellyfinAPI
struct LogConstructor {
var message: String
let tag: String
let level: LogLevel
let function: String
let file: String
let line: UInt
var message: String
let tag: String
let level: LogLevel
let function: String
let file: String
let line: UInt
}

View File

@ -1,11 +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
* file, you can obtain one at https://mozilla.org/MPL/2.0/.
*
* Copyright 2021 Aiden Vigue & Jellyfin Contributors
*/
// 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
import JellyfinAPI
@ -13,139 +12,146 @@ import JellyfinAPI
/**
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".
The specific kind of errors here should be created and surfaced from within JellyfinAPI on API calls.
Needs to be replaced
*/
enum NetworkError: Error {
/// For the case that the ErrorResponse object has a code of -1
case URLError(response: ErrorResponse, displayMessage: String?, logConstructor: LogConstructor)
/// For the case that the ErrorResponse object has a code of -1
case URLError(response: ErrorResponse, displayMessage: String?, logConstructor: LogConstructor)
/// For the case that the ErrorRespones object has a code of -2
case HTTPURLError(response: ErrorResponse, displayMessage: String?, logConstructor: LogConstructor)
/// For the case that the ErrorRespones object has a code of -2
case HTTPURLError(response: ErrorResponse, displayMessage: String?, logConstructor: LogConstructor)
/// For the case that the ErrorResponse object has a positive code
case JellyfinError(response: ErrorResponse, displayMessage: String?, logConstructor: LogConstructor)
/// For the case that the ErrorResponse object has a positive code
case JellyfinError(response: ErrorResponse, displayMessage: String?, logConstructor: LogConstructor)
var errorMessage: ErrorMessage {
switch self {
case .URLError(let response, let displayMessage, let logConstructor):
return NetworkError.parseURLError(from: response, displayMessage: displayMessage, logConstructor: logConstructor)
case .HTTPURLError(let response, let displayMessage, let logConstructor):
return NetworkError.parseHTTPURLError(from: response, displayMessage: displayMessage, logConstructor: logConstructor)
case .JellyfinError(let response, let displayMessage, let logConstructor):
return NetworkError.parseJellyfinError(from: response, displayMessage: displayMessage, logConstructor: logConstructor)
}
}
var errorMessage: ErrorMessage {
switch self {
case let .URLError(response, displayMessage, logConstructor):
return NetworkError.parseURLError(from: response, displayMessage: displayMessage, logConstructor: logConstructor)
case let .HTTPURLError(response, displayMessage, logConstructor):
return NetworkError.parseHTTPURLError(from: response, displayMessage: displayMessage, logConstructor: logConstructor)
case let .JellyfinError(response, displayMessage, logConstructor):
return NetworkError.parseJellyfinError(from: response, displayMessage: displayMessage, logConstructor: logConstructor)
}
}
func logMessage() {
let logConstructor = errorMessage.logConstructor
let logFunction: (@autoclosure () -> String, String, String, String, UInt) -> Void
func logMessage() {
let logConstructor = errorMessage.logConstructor
let logFunction: (@autoclosure () -> String, String, String, String, UInt) -> Void
switch logConstructor.level {
case .trace:
logFunction = LogManager.shared.log.trace
case .debug:
logFunction = LogManager.shared.log.debug
case .information:
logFunction = LogManager.shared.log.info
case .warning:
logFunction = LogManager.shared.log.warning
case .error:
logFunction = LogManager.shared.log.error
case .critical:
logFunction = LogManager.shared.log.critical
case ._none:
logFunction = LogManager.shared.log.debug
}
switch logConstructor.level {
case .trace:
logFunction = LogManager.shared.log.trace
case .debug:
logFunction = LogManager.shared.log.debug
case .information:
logFunction = LogManager.shared.log.info
case .warning:
logFunction = LogManager.shared.log.warning
case .error:
logFunction = LogManager.shared.log.error
case .critical:
logFunction = LogManager.shared.log.critical
case ._none:
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
var logMessage = "An error has occurred."
var logConstructor = logConstructor
let errorMessage: ErrorMessage
var logMessage = "An error has occurred."
var logConstructor = logConstructor
switch response {
case .error(_, _, _, let err):
switch response {
case let .error(_, _, _, err):
// These codes are currently referenced from:
// https://developer.apple.com/documentation/foundation/1508628-url_loading_system_error_codes
switch err._code {
case -1001:
logMessage = "Network timed out."
logConstructor.message = logMessage
errorMessage = ErrorMessage(code: err._code,
title: "Timed Out",
displayMessage: displayMessage,
logConstructor: logConstructor)
case -1004:
logMessage = "Cannot connect to host."
logConstructor.message = logMessage
errorMessage = ErrorMessage(code: err._code,
title: L10n.error,
displayMessage: displayMessage,
logConstructor: logConstructor)
default:
logConstructor.message = logMessage
errorMessage = ErrorMessage(code: err._code,
title: L10n.error,
displayMessage: displayMessage,
logConstructor: logConstructor)
}
}
// These codes are currently referenced from:
// https://developer.apple.com/documentation/foundation/1508628-url_loading_system_error_codes
switch err._code {
case -1001:
logMessage = "Network timed out."
logConstructor.message = logMessage
errorMessage = ErrorMessage(code: err._code,
title: "Timed Out",
displayMessage: displayMessage,
logConstructor: logConstructor)
case -1004:
logMessage = "Cannot connect to host."
logConstructor.message = logMessage
errorMessage = ErrorMessage(code: err._code,
title: L10n.error,
displayMessage: displayMessage,
logConstructor: logConstructor)
default:
logConstructor.message = logMessage
errorMessage = ErrorMessage(code: err._code,
title: L10n.error,
displayMessage: displayMessage,
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 logMessage = "An HTTP URL error has occurred"
var logConstructor = logConstructor
let errorMessage: ErrorMessage
let logMessage = "An HTTP URL error has occurred"
var logConstructor = logConstructor
// Not implemented as has not run into one of these errors as time of writing
switch response {
case .error:
logConstructor.message = logMessage
errorMessage = ErrorMessage(code: 0,
title: L10n.error,
displayMessage: displayMessage,
logConstructor: logConstructor)
}
// Not implemented as has not run into one of these errors as time of writing
switch response {
case .error:
logConstructor.message = logMessage
errorMessage = ErrorMessage(code: 0,
title: L10n.error,
displayMessage: displayMessage,
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
var logMessage = "An error has occurred."
var logConstructor = logConstructor
let errorMessage: ErrorMessage
var logMessage = "An error has occurred."
var logConstructor = logConstructor
switch response {
case .error(let code, _, _, _):
switch response {
case let .error(code, _, _, _):
// Generic HTTP status codes
switch code {
case 401:
logMessage = "User is unauthorized."
logConstructor.message = logMessage
errorMessage = ErrorMessage(code: code,
title: "Unauthorized",
displayMessage: displayMessage,
logConstructor: logConstructor)
default:
logConstructor.message = logMessage
errorMessage = ErrorMessage(code: code,
title: L10n.error,
displayMessage: displayMessage,
logConstructor: logConstructor)
}
}
// Generic HTTP status codes
switch code {
case 401:
logMessage = "User is unauthorized."
logConstructor.message = logMessage
errorMessage = ErrorMessage(code: code,
title: "Unauthorized",
displayMessage: displayMessage,
logConstructor: logConstructor)
default:
logConstructor.message = logMessage
errorMessage = ErrorMessage(code: code,
title: L10n.error,
displayMessage: displayMessage,
logConstructor: logConstructor)
}
}
return errorMessage
}
return errorMessage
}
}

View File

@ -1,166 +1,151 @@
/*
Copyright (c) 2018 Wolt Enterprises
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
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.
*/
//
// 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 UIKit
extension UIImage {
public convenience init?(blurHash: String, size: CGSize, punch: Float = 1) {
guard blurHash.count >= 6 else { return nil }
public extension UIImage {
convenience init?(blurHash: String, size: CGSize, punch: Float = 1) {
guard blurHash.count >= 6 else { return nil }
let sizeFlag = String(blurHash[0]).decode83()
let numY = (sizeFlag / 9) + 1
let numX = (sizeFlag % 9) + 1
let sizeFlag = String(blurHash[0]).decode83()
let numY = (sizeFlag / 9) + 1
let numX = (sizeFlag % 9) + 1
let quantisedMaximumValue = String(blurHash[1]).decode83()
let maximumValue = Float(quantisedMaximumValue + 1) / 166
let quantisedMaximumValue = String(blurHash[1]).decode83()
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
if i == 0 {
let value = String(blurHash[2 ..< 6]).decode83()
return decodeDC(value)
} else {
let value = String(blurHash[4 + i * 2 ..< 4 + i * 2 + 2]).decode83()
return decodeAC(value, maximumValue: maximumValue * punch)
}
}
let colours: [(Float, Float, Float)] = (0 ..< numX * numY).map { i in
if i == 0 {
let value = String(blurHash[2 ..< 6]).decode83()
return decodeDC(value)
} else {
let value = String(blurHash[4 + i * 2 ..< 4 + i * 2 + 2]).decode83()
return decodeAC(value, maximumValue: maximumValue * punch)
}
}
let width = Int(size.width)
let height = Int(size.height)
let bytesPerRow = width * 3
guard let data = CFDataCreateMutable(kCFAllocatorDefault, bytesPerRow * height) else { return nil }
CFDataSetLength(data, bytesPerRow * height)
guard let pixels = CFDataGetMutableBytePtr(data) else { return nil }
let width = Int(size.width)
let height = Int(size.height)
let bytesPerRow = width * 3
guard let data = CFDataCreateMutable(kCFAllocatorDefault, bytesPerRow * height) else { return nil }
CFDataSetLength(data, bytesPerRow * height)
guard let pixels = CFDataGetMutableBytePtr(data) else { return nil }
for y in 0 ..< height {
for x in 0 ..< width {
var r: Float = 0
var g: Float = 0
var b: Float = 0
for y in 0 ..< height {
for x in 0 ..< width {
var r: Float = 0
var g: Float = 0
var b: Float = 0
for j in 0 ..< numY {
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 colour = colours[i + j * numX]
r += colour.0 * basis
g += colour.1 * basis
b += colour.2 * basis
}
}
for j in 0 ..< numY {
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 colour = colours[i + j * numX]
r += colour.0 * basis
g += colour.1 * basis
b += colour.2 * basis
}
}
let intR = UInt8(linearTosRGB(r))
let intG = UInt8(linearTosRGB(g))
let intB = UInt8(linearTosRGB(b))
let intR = UInt8(linearTosRGB(r))
let intG = UInt8(linearTosRGB(g))
let intB = UInt8(linearTosRGB(b))
pixels[3 * x + 0 + y * bytesPerRow] = intR
pixels[3 * x + 1 + y * bytesPerRow] = intG
pixels[3 * x + 2 + y * bytesPerRow] = intB
}
}
pixels[3 * x + 0 + y * bytesPerRow] = intR
pixels[3 * x + 1 + y * bytesPerRow] = intG
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 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 }
guard let provider = CGDataProvider(data: data) else { return nil }
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 }
self.init(cgImage: cgImage)
}
self.init(cgImage: cgImage)
}
}
private func decodeDC(_ value: Int) -> (Float, Float, Float) {
let intR = value >> 16
let intG = (value >> 8) & 255
let intB = value & 255
return (sRGBToLinear(intR), sRGBToLinear(intG), sRGBToLinear(intB))
let intR = value >> 16
let intG = (value >> 8) & 255
let intB = value & 255
return (sRGBToLinear(intR), sRGBToLinear(intG), sRGBToLinear(intB))
}
private func decodeAC(_ value: Int, maximumValue: Float) -> (Float, Float, Float) {
let quantR = value / (19 * 19)
let quantG = (value / 19) % 19
let quantB = value % 19
let quantR = value / (19 * 19)
let quantG = (value / 19) % 19
let quantB = value % 19
let rgb = (
signPow((Float(quantR) - 9) / 9, 2) * maximumValue,
signPow((Float(quantG) - 9) / 9, 2) * maximumValue,
signPow((Float(quantB) - 9) / 9, 2) * maximumValue
)
let rgb = (signPow((Float(quantR) - 9) / 9, 2) * maximumValue,
signPow((Float(quantG) - 9) / 9, 2) * maximumValue,
signPow((Float(quantB) - 9) / 9, 2) * maximumValue)
return rgb
return rgb
}
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 {
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) }
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) }
}
private func sRGBToLinear<Type: BinaryInteger>(_ value: Type) -> Float {
let v = Float(Int64(value)) / 255
if v <= 0.04045 { return v / 12.92 } else { return pow((v + 0.055) / 1.055, 2.4) }
let v = Float(Int64(value)) / 255
if v <= 0.04045 { return v / 12.92 } else { return pow((v + 0.055) / 1.055, 2.4) }
}
private let encodeCharacters: [String] = {
return "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz#$%*+,-.:;=?@[]^_{|}~".map { String($0) }
"0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz#$%*+,-.:;=?@[]^_{|}~".map { String($0) }
}()
private let decodeCharacters: [String: Int] = {
var dict: [String: Int] = [:]
for (index, character) in encodeCharacters.enumerated() {
dict[character] = index
}
return dict
var dict: [String: Int] = [:]
for (index, character) in encodeCharacters.enumerated() {
dict[character] = index
}
return dict
}()
extension String {
func decode83() -> Int {
var value: Int = 0
for character in self {
if let digit = decodeCharacters[String(character)] {
value = value * 83 + digit
}
}
return value
}
func decode83() -> Int {
var value: Int = 0
for character in self {
if let digit = decodeCharacters[String(character)] {
value = value * 83 + digit
}
}
return value
}
}
private extension String {
subscript (offset: Int) -> Character {
return self[index(startIndex, offsetBy: offset)]
}
subscript(offset: Int) -> Character {
self[index(startIndex, offsetBy: offset)]
}
subscript (bounds: CountableClosedRange<Int>) -> Substring {
let start = index(startIndex, offsetBy: bounds.lowerBound)
let end = index(startIndex, offsetBy: bounds.upperBound)
return self[start...end]
}
subscript(bounds: CountableClosedRange<Int>) -> Substring {
let start = index(startIndex, offsetBy: bounds.lowerBound)
let end = index(startIndex, offsetBy: bounds.upperBound)
return self[start ... end]
}
subscript (bounds: CountableRange<Int>) -> Substring {
let start = index(startIndex, offsetBy: bounds.lowerBound)
let end = index(startIndex, offsetBy: bounds.upperBound)
return self[start..<end]
}
subscript(bounds: CountableRange<Int>) -> Substring {
let start = index(startIndex, offsetBy: bounds.lowerBound)
let end = index(startIndex, offsetBy: bounds.upperBound)
return self[start ..< end]
}
}

View File

@ -1,17 +1,16 @@
//
/*
* 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 2021 Aiden Vigue & Jellyfin Contributors
*/
// 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 UIKit
extension CGSize {
static func Circle(radius: CGFloat) -> CGSize {
return CGSize(width: radius, height: radius)
}
static func Circle(radius: CGFloat) -> CGSize {
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
* file, you can obtain one at https://mozilla.org/MPL/2.0/.
*
* Copyright 2021 Aiden Vigue & Jellyfin Contributors
*/
//
// 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
public extension Collection {
/// SwifterSwift: Safe protects the array from out of bounds by use of optional.
///
/// let arr = [1, 2, 3, 4, 5]
/// arr[safe: 1] -> 2
/// arr[safe: 10] -> nil
///
/// - Parameter index: index of element to access element.
subscript(safe index: Index) -> Element? {
return indices.contains(index) ? self[index] : nil
}
/// SwifterSwift: Safe protects the array from out of bounds by use of optional.
///
/// let arr = [1, 2, 3, 4, 5]
/// arr[safe: 1] -> 2
/// arr[safe: 10] -> nil
///
/// - Parameter index: index of element to access element.
subscript(safe index: Index) -> Element? {
indices.contains(index) ? self[index] : nil
}
}

View File

@ -1,31 +1,30 @@
//
/*
* 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 2021 Aiden Vigue & Jellyfin Contributors
*/
// 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 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
public static let systemFill = Color(UIColor.white)
public static let secondarySystemFill = Color(UIColor.gray)
public static let tertiarySystemFill = Color(UIColor.black)
public static let lightGray = Color(UIColor.lightGray)
#else
public static let systemFill = Color(UIColor.systemFill)
public static let systemBackground = Color(UIColor.systemBackground)
public static let secondarySystemFill = Color(UIColor.secondarySystemBackground)
public static let tertiarySystemFill = Color(UIColor.tertiarySystemBackground)
#endif
#if os(tvOS) // tvOS doesn't have these
static let systemFill = Color(UIColor.white)
static let secondarySystemFill = Color(UIColor.gray)
static let tertiarySystemFill = Color(UIColor.black)
static let lightGray = Color(UIColor.lightGray)
#else
static let systemFill = Color(UIColor.systemFill)
static let systemBackground = Color(UIColor.systemBackground)
static let secondarySystemFill = Color(UIColor.secondarySystemBackground)
static let tertiarySystemFill = Color(UIColor.tertiarySystemBackground)
#endif
}
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
* 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
*/
// 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
extension Double {
func subtract(_ other: Double, floor: Double) -> Double {
var v = self - other
if v < floor {
v += abs(floor - v)
}
return v
}
func subtract(_ other: Double, floor: Double) -> Double {
var v = self - other
if v < floor {
v += abs(floor - 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
* file, you can obtain one at https://mozilla.org/MPL/2.0/.
*
* Copyright 2021 Aiden Vigue & Jellyfin Contributors
*/
//
// 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
import SwiftUI
extension Image {
func centerCropped() -> some View {
GeometryReader { geo in
self
.resizable()
.scaledToFill()
.frame(width: geo.size.width, height: geo.size.height)
.clipped()
}
}
func centerCropped() -> some View {
GeometryReader { geo in
self
.resizable()
.scaledToFill()
.frame(width: geo.size.width, height: geo.size.height)
.clipped()
}
}
}

View File

@ -1,65 +1,65 @@
//
/*
* 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 2021 Aiden Vigue & Jellyfin Contributors
*/
// 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 Defaults
import Foundation
import JellyfinAPI
// MARK: PortraitImageStackable
extension BaseItemDto: PortraitImageStackable {
public var portraitImageID: String {
return 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 portraitImageID: String {
id ?? "no id"
}
public var title: String {
switch self.itemType {
case .episode:
return self.seriesName ?? self.name ?? ""
default:
return self.name ?? ""
}
}
public func imageURLContsructor(maxWidth: Int) -> URL {
switch self.itemType {
case .episode:
return getSeriesPrimaryImage(maxWidth: maxWidth)
default:
return self.getPrimaryImage(maxWidth: maxWidth)
}
}
public var subtitle: String? {
switch self.itemType {
case .episode:
return getEpisodeLocator()
default:
return nil
}
}
public var title: String {
switch self.itemType {
case .episode:
return self.seriesName ?? self.name ?? ""
default:
return self.name ?? ""
}
}
public var blurHash: String {
return self.getPrimaryImageBlurHash()
}
public var subtitle: String? {
switch self.itemType {
case .episode:
return getEpisodeLocator()
default:
return nil
}
}
public var failureInitials: String {
guard let name = self.name else { return "" }
let initials = name.split(separator: " ").compactMap({ String($0).first })
return String(initials)
}
public var showTitle: Bool {
switch self.itemType {
case .episode, .series, .movie:
return Defaults[.showPosterLabels]
default:
return true
}
}
public var blurHash: String {
self.getPrimaryImageBlurHash()
}
public var failureInitials: String {
guard let name = self.name else { return "" }
let initials = name.split(separator: " ").compactMap { String($0).first }
return String(initials)
}
public var showTitle: Bool {
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
* 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
*/
// 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 Combine
import Defaults
@ -13,97 +12,98 @@ import JellyfinAPI
import UIKit
extension BaseItemDto {
func createVideoPlayerViewModel() -> AnyPublisher<VideoPlayerViewModel, Error> {
let builder = DeviceProfileBuilder()
// TODO: fix bitrate settings
builder.setMaxBitrate(bitrate: 60000000)
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"
}
func createVideoPlayerViewModel() -> AnyPublisher<VideoPlayerViewModel, Error> {
let builder = DeviceProfileBuilder()
// TODO: fix bitrate settings
builder.setMaxBitrate(bitrate: 60_000_000)
let profile = builder.buildProfile()
streamURL.addQueryItem(name: "Static", value: "true")
streamURL.addQueryItem(name: "MediaSourceId", value: self.id!)
streamURL.addQueryItem(name: "Tag", value: self.etag)
streamURL.addQueryItem(name: "MinSegments", value: "6")
// MARK: VidoPlayerViewModel Creation
var subtitle: String? = nil
// MARK: Attach media content to self
var modifiedSelfItem = self
modifiedSelfItem.mediaStreams = mediaSource.mediaStreams
// 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()
}
let playbackInfo = PlaybackInfoDto(userId: SessionManager.main.currentLogin.user.id,
maxStreamingBitrate: 60_000_000,
startTimeTicks: self.userData?.playbackPositionTicks ?? 0,
deviceProfile: profile,
autoOpenLiveStream: true)
return MediaInfoAPI.getPostedPlaybackInfo(itemId: self.id!,
userId: SessionManager.main.currentLogin.user.id,
maxStreamingBitrate: 60_000_000,
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")
streamURL.addQueryItem(name: "MediaSourceId", value: self.id!)
streamURL.addQueryItem(name: "Tag", value: self.etag)
streamURL.addQueryItem(name: "MinSegments", value: "6")
// MARK: VidoPlayerViewModel Creation
var subtitle: String?
// MARK: Attach media content to self
var modifiedSelfItem = self
modifiedSelfItem.mediaStreams = mediaSource.mediaStreams
// 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
* 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
*/
// 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
import JellyfinAPI
@ -14,282 +13,285 @@ import UIKit
// 001fC^ = dark grey plain blurhash
public extension BaseItemDto {
// MARK: Images
// MARK: Images
func getSeriesBackdropImageBlurHash() -> String {
let imgURL = getSeriesBackdropImage(maxWidth: 1)
guard let imgTag = imgURL.queryParameters?["tag"],
let hash = imageBlurHashes?.backdrop?[imgTag]
else {
return "001fC^"
}
func getSeriesBackdropImageBlurHash() -> String {
let imgURL = getSeriesBackdropImage(maxWidth: 1)
guard let imgTag = imgURL.queryParameters?["tag"],
let hash = imageBlurHashes?.backdrop?[imgTag]
else {
return "001fC^"
}
return hash
}
return hash
}
func getSeriesPrimaryImageBlurHash() -> String {
let imgURL = getSeriesPrimaryImage(maxWidth: 1)
guard let imgTag = imgURL.queryParameters?["tag"],
let hash = imageBlurHashes?.primary?[imgTag]
else {
return "001fC^"
}
func getSeriesPrimaryImageBlurHash() -> String {
let imgURL = getSeriesPrimaryImage(maxWidth: 1)
guard let imgTag = imgURL.queryParameters?["tag"],
let hash = imageBlurHashes?.primary?[imgTag]
else {
return "001fC^"
}
return hash
}
return hash
}
func getPrimaryImageBlurHash() -> String {
let imgURL = getPrimaryImage(maxWidth: 1)
guard let imgTag = imgURL.queryParameters?["tag"],
let hash = imageBlurHashes?.primary?[imgTag]
else {
return "001fC^"
}
func getPrimaryImageBlurHash() -> String {
let imgURL = getPrimaryImage(maxWidth: 1)
guard let imgTag = imgURL.queryParameters?["tag"],
let hash = imageBlurHashes?.primary?[imgTag]
else {
return "001fC^"
}
return hash
}
return hash
}
func getBackdropImageBlurHash() -> String {
let imgURL = getBackdropImage(maxWidth: 1)
guard let imgTag = imgURL.queryParameters?["tag"] else {
return "001fC^"
}
func getBackdropImageBlurHash() -> String {
let imgURL = getBackdropImage(maxWidth: 1)
guard let imgTag = imgURL.queryParameters?["tag"] else {
return "001fC^"
}
if imgURL.queryParameters?[ImageType.backdrop.rawValue] == nil {
return imageBlurHashes?.backdrop?[imgTag] ?? "001fC^"
} else {
return imageBlurHashes?.primary?[imgTag] ?? "001fC^"
}
}
if imgURL.queryParameters?[ImageType.backdrop.rawValue] == nil {
return imageBlurHashes?.backdrop?[imgTag] ?? "001fC^"
} else {
return imageBlurHashes?.primary?[imgTag] ?? "001fC^"
}
}
func getBackdropImage(maxWidth: Int) -> URL {
var imageType = ImageType.backdrop
var imageTag: String?
var imageItemId = id ?? ""
func getBackdropImage(maxWidth: Int) -> URL {
var imageType = ImageType.backdrop
var imageTag: String?
var imageItemId = id ?? ""
if primaryImageAspectRatio ?? 0.0 < 1.0 {
if !(backdropImageTags?.isEmpty ?? true) {
imageTag = backdropImageTags?.first
}
} else {
imageType = .primary
imageTag = imageTags?[ImageType.primary.rawValue] ?? ""
}
if primaryImageAspectRatio ?? 0.0 < 1.0 {
if !(backdropImageTags?.isEmpty ?? true) {
imageTag = backdropImageTags?.first
}
} else {
imageType = .primary
imageTag = imageTags?[ImageType.primary.rawValue] ?? ""
}
if imageTag == nil || imageItemId.isEmpty {
if !(parentBackdropImageTags?.isEmpty ?? true) {
imageTag = parentBackdropImageTags?.first
imageItemId = parentBackdropItemId ?? ""
}
}
if imageTag == nil || imageItemId.isEmpty {
if !(parentBackdropImageTags?.isEmpty ?? true) {
imageTag = parentBackdropImageTags?.first
imageItemId = parentBackdropItemId ?? ""
}
}
let x = UIScreen.main.nativeScale * CGFloat(maxWidth)
let x = UIScreen.main.nativeScale * CGFloat(maxWidth)
let urlString = ImageAPI.getItemImageWithRequestBuilder(itemId: imageItemId,
imageType: imageType,
maxWidth: Int(x),
quality: 96,
tag: imageTag).URLString
return URL(string: urlString)!
}
let urlString = ImageAPI.getItemImageWithRequestBuilder(itemId: imageItemId,
imageType: imageType,
maxWidth: Int(x),
quality: 96,
tag: imageTag).URLString
return URL(string: urlString)!
}
func getEpisodeLocator() -> String? {
if let seasonNo = parentIndexNumber, let episodeNo = indexNumber {
return L10n.seasonAndEpisode(String(seasonNo), String(episodeNo))
}
return nil
}
func getEpisodeLocator() -> String? {
if let seasonNo = parentIndexNumber, let episodeNo = indexNumber {
return L10n.seasonAndEpisode(String(seasonNo), String(episodeNo))
}
return nil
}
func getSeriesBackdropImage(maxWidth: Int) -> URL {
let x = UIScreen.main.nativeScale * CGFloat(maxWidth)
let urlString = ImageAPI.getItemImageWithRequestBuilder(itemId: parentBackdropItemId ?? "",
imageType: .backdrop,
maxWidth: Int(x),
quality: 96,
tag: parentBackdropImageTags?.first).URLString
return URL(string: urlString)!
}
func getSeriesBackdropImage(maxWidth: Int) -> URL {
let x = UIScreen.main.nativeScale * CGFloat(maxWidth)
let urlString = ImageAPI.getItemImageWithRequestBuilder(itemId: parentBackdropItemId ?? "",
imageType: .backdrop,
maxWidth: Int(x),
quality: 96,
tag: parentBackdropImageTags?.first).URLString
return URL(string: urlString)!
}
func getSeriesPrimaryImage(maxWidth: Int) -> URL {
let x = UIScreen.main.nativeScale * CGFloat(maxWidth)
let urlString = ImageAPI.getItemImageWithRequestBuilder(itemId: seriesId ?? "",
imageType: .primary,
maxWidth: Int(x),
quality: 96,
tag: seriesPrimaryImageTag).URLString
return URL(string: urlString)!
}
func getSeriesPrimaryImage(maxWidth: Int) -> URL {
let x = UIScreen.main.nativeScale * CGFloat(maxWidth)
let urlString = ImageAPI.getItemImageWithRequestBuilder(itemId: seriesId ?? "",
imageType: .primary,
maxWidth: Int(x),
quality: 96,
tag: seriesPrimaryImageTag).URLString
return URL(string: urlString)!
}
func getPrimaryImage(maxWidth: Int) -> URL {
let imageType = ImageType.primary
var imageTag = imageTags?[ImageType.primary.rawValue] ?? ""
var imageItemId = id ?? ""
func getPrimaryImage(maxWidth: Int) -> URL {
let imageType = ImageType.primary
var imageTag = imageTags?[ImageType.primary.rawValue] ?? ""
var imageItemId = id ?? ""
if imageTag.isEmpty || imageItemId.isEmpty {
imageTag = seriesPrimaryImageTag ?? ""
imageItemId = seriesId ?? ""
}
if imageTag.isEmpty || imageItemId.isEmpty {
imageTag = seriesPrimaryImageTag ?? ""
imageItemId = seriesId ?? ""
}
let x = UIScreen.main.nativeScale * CGFloat(maxWidth)
let x = UIScreen.main.nativeScale * CGFloat(maxWidth)
let urlString = ImageAPI.getItemImageWithRequestBuilder(itemId: imageItemId,
imageType: imageType,
maxWidth: Int(x),
quality: 96,
tag: imageTag).URLString
return URL(string: urlString)!
}
let urlString = ImageAPI.getItemImageWithRequestBuilder(itemId: imageItemId,
imageType: imageType,
maxWidth: Int(x),
quality: 96,
tag: imageTag).URLString
return URL(string: urlString)!
}
// MARK: Calculations
// MARK: Calculations
func getItemRuntime() -> String? {
let timeHMSFormatter: DateComponentsFormatter = {
let formatter = DateComponentsFormatter()
formatter.unitsStyle = .abbreviated
formatter.allowedUnits = [.hour, .minute]
return formatter
}()
func getItemRuntime() -> String? {
let timeHMSFormatter: DateComponentsFormatter = {
let formatter = DateComponentsFormatter()
formatter.unitsStyle = .abbreviated
formatter.allowedUnits = [.hour, .minute]
return formatter
}()
guard let runTimeTicks = runTimeTicks,
let text = timeHMSFormatter.string(from: Double(runTimeTicks / 10_000_000)) else { return nil }
guard let runTimeTicks = runTimeTicks,
let text = timeHMSFormatter.string(from: Double(runTimeTicks / 10_000_000)) else { return nil }
return text
}
return text
}
func getItemProgressString() -> String? {
if userData?.playbackPositionTicks == nil || userData?.playbackPositionTicks == 0 {
return nil
}
func getItemProgressString() -> String? {
if userData?.playbackPositionTicks == nil || userData?.playbackPositionTicks == 0 {
return nil
}
let remainingSecs = ((runTimeTicks ?? 0) - (userData?.playbackPositionTicks ?? 0)) / 10_000_000
let proghours = Int(remainingSecs / 3600)
let progminutes = Int((Int(remainingSecs) - (proghours * 3600)) / 60)
if proghours != 0 {
return "\(proghours)h \(String(progminutes).leftPad(toWidth: 2, withString: "0"))m"
} else {
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
let remainingSecs = ((runTimeTicks ?? 0) - (userData?.playbackPositionTicks ?? 0)) / 10_000_000
let proghours = Int(remainingSecs / 3600)
let progminutes = Int((Int(remainingSecs) - (proghours * 3600)) / 60)
if proghours != 0 {
return "\(proghours)h \(String(progminutes).leftPad(toWidth: 2, withString: "0"))m"
} else {
return "\(String(progminutes))m"
}
}
enum ItemType: String {
case movie = "Movie"
case season = "Season"
case episode = "Episode"
case series = "Series"
case boxset = "BoxSet"
func getLiveStartTimeString(formatter: DateFormatter) -> String {
if let startDate = self.startDate {
return formatter.string(from: startDate)
}
return " "
}
case unknown
func getLiveEndTimeString(formatter: DateFormatter) -> String {
if let endDate = self.endDate {
return formatter.string(from: endDate)
}
return " "
}
var showDetails: Bool {
switch self {
case .season, .series:
return false
default:
return true
}
}
}
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
}
var itemType: ItemType {
guard let originalType = type, let knownType = ItemType(rawValue: originalType) else { return .unknown }
return knownType
}
// MARK: ItemType
// 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 {
switch itemType {
case .movie, .season, .series, .boxset:
return getPrimaryImage(maxWidth: maxWidth)
case .episode:
return getSeriesPrimaryImage(maxWidth: maxWidth)
case .unknown:
return getPrimaryImage(maxWidth: maxWidth)
}
}
// MARK: ItemDetail
struct ItemDetail {
let title: String
let content: String
}
func createInformationItems() -> [ItemDetail] {
var informationItems: [ItemDetail] = []
if let productionYear = productionYear {
informationItems.append(ItemDetail(title: "Released", content: "\(productionYear)"))
}
if let rating = officialRating {
informationItems.append(ItemDetail(title: "Rated", content: "\(rating)"))
}
if let runtime = getItemRuntime() {
informationItems.append(ItemDetail(title: "Runtime", content: runtime))
}
return informationItems
}
func createMediaItems() -> [ItemDetail] {
var mediaItems: [ItemDetail] = []
if let container = container {
let containerList = container.split(separator: ",").joined(separator: ", ")
if containerList.count > 1 {
mediaItems.append(ItemDetail(title: "Containers", content: containerList))
} else {
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
}
case unknown
var showDetails: Bool {
switch self {
case .season, .series:
return false
default:
return true
}
}
}
var itemType: ItemType {
guard let originalType = type, let knownType = ItemType(rawValue: originalType) else { return .unknown }
return knownType
}
// MARK: PortraitHeaderViewURL
func portraitHeaderViewURL(maxWidth: Int) -> URL {
switch itemType {
case .movie, .season, .series, .boxset:
return getPrimaryImage(maxWidth: maxWidth)
case .episode:
return getSeriesPrimaryImage(maxWidth: maxWidth)
case .unknown:
return getPrimaryImage(maxWidth: maxWidth)
}
}
// MARK: ItemDetail
struct ItemDetail {
let title: String
let content: String
}
func createInformationItems() -> [ItemDetail] {
var informationItems: [ItemDetail] = []
if let productionYear = productionYear {
informationItems.append(ItemDetail(title: "Released", content: "\(productionYear)"))
}
if let rating = officialRating {
informationItems.append(ItemDetail(title: "Rated", content: "\(rating)"))
}
if let runtime = getItemRuntime() {
informationItems.append(ItemDetail(title: "Runtime", content: runtime))
}
return informationItems
}
func createMediaItems() -> [ItemDetail] {
var mediaItems: [ItemDetail] = []
if let container = container {
let containerList = container.split(separator: ",").joined(separator: ", ")
if containerList.count > 1 {
mediaItems.append(ItemDetail(title: "Containers", content: containerList))
} else {
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
* file, you can obtain one at https://mozilla.org/MPL/2.0/.
*
* Copyright 2021 Aiden Vigue & Jellyfin Contributors
*/
//
// 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
import JellyfinAPI
@ -11,99 +12,103 @@ import UIKit
extension BaseItemPerson {
// MARK: Get Image
func getImage(baseURL: String, maxWidth: Int) -> URL {
let x = UIScreen.main.nativeScale * CGFloat(maxWidth)
// MARK: Get Image
let urlString = ImageAPI.getItemImageWithRequestBuilder(itemId: id ?? "",
imageType: .primary,
maxWidth: Int(x),
quality: 96,
tag: primaryImageTag).URLString
return URL(string: urlString)!
}
func getImage(baseURL: String, maxWidth: Int) -> URL {
let x = UIScreen.main.nativeScale * CGFloat(maxWidth)
func getBlurHash() -> String {
let imgURL = getImage(baseURL: "", maxWidth: 1)
guard let imgTag = imgURL.queryParameters?["tag"],
let hash = imageBlurHashes?.primary?[imgTag]
else {
return "001fC^"
}
let urlString = ImageAPI.getItemImageWithRequestBuilder(itemId: id ?? "",
imageType: .primary,
maxWidth: Int(x),
quality: 96,
tag: primaryImageTag).URLString
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
// 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 }
// MARK: First 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: ")") {
let roleText = lastRole[lastOpenIndex...lastClosingIndex]
final.append(" \(roleText)")
}
var final = firstRole
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
extension BaseItemPerson: PortraitImageStackable {
public var portraitImageID: String {
return (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 portraitImageID: String {
(id ?? "noid") + title + (subtitle ?? "nodescription") + blurHash + failureInitials
}
public var title: String {
return self.name ?? ""
}
public func imageURLContsructor(maxWidth: Int) -> URL {
self.getImage(baseURL: SessionManager.main.currentLogin.server.currentURI, maxWidth: maxWidth)
}
public var subtitle: String? {
return self.firstRole()
}
public var title: String {
self.name ?? ""
}
public var blurHash: String {
return self.getBlurHash()
}
public var subtitle: String? {
self.firstRole()
}
public var failureInitials: String {
guard let name = self.name else { return "" }
let initials = name.split(separator: " ").compactMap({ String($0).first })
return String(initials)
}
public var showTitle: Bool {
return true
}
public var blurHash: String {
self.getBlurHash()
}
public var failureInitials: String {
guard let name = self.name else { return "" }
let initials = name.split(separator: " ").compactMap { String($0).first }
return String(initials)
}
public var showTitle: Bool {
true
}
}
// MARK: DiplayedType
extension BaseItemPerson {
// Only displayed person types.
// Will ignore people like "GuestStar"
enum DisplayedType: String, CaseIterable {
case actor = "Actor"
case director = "Director"
case writer = "Writer"
case producer = "Producer"
// Only displayed person types.
// Will ignore people like "GuestStar"
enum DisplayedType: String, CaseIterable {
case actor = "Actor"
case director = "Director"
case writer = "Writer"
case producer = "Producer"
static var allCasesRaw: [String] {
return self.allCases.map({ $0.rawValue })
}
}
static var allCasesRaw: [String] {
self.allCases.map(\.rawValue)
}
}
}

View File

@ -1,23 +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
* file, you can obtain one at https://mozilla.org/MPL/2.0/.
*
* Copyright 2021 Aiden Vigue & Jellyfin Contributors
*/
// 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
struct JellyfinAPIError: Error {
private let message: String
private let message: String
init(_ message: String) {
self.message = message
}
init(_ message: String) {
self.message = message
}
var localizedDescription: String {
return message
}
var localizedDescription: String {
message
}
}

View File

@ -1,22 +1,21 @@
//
/*
* 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 2021 Aiden Vigue & Jellyfin Contributors
*/
// 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
import JellyfinAPI
extension MediaStream {
func externalURL(base: String) -> URL? {
guard let deliveryURL = deliveryUrl else { return nil }
var baseComponents = URLComponents(string: base)
baseComponents?.path += deliveryURL
return baseComponents?.url
}
func externalURL(base: String) -> URL? {
guard let deliveryURL = deliveryUrl else { return nil }
var baseComponents = URLComponents(string: base)
baseComponents?.path += deliveryURL
return baseComponents?.url
}
}

View File

@ -1,17 +1,16 @@
//
/*
* 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 2021 Aiden Vigue & Jellyfin Contributors
*/
// 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
import JellyfinAPI
extension NameGuidPair: PillStackable {
var title: String {
return self.name ?? ""
}
var title: String {
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
* file, you can obtain one at https://mozilla.org/MPL/2.0/.
*
* Copyright 2021 Aiden Vigue & Jellyfin Contributors
*/
//
// 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
import SwiftUI
extension String {
func removeRegexMatches(pattern: String, replaceWith: String = "") -> String {
do {
let regex = try NSRegularExpression(pattern: pattern, options: .caseInsensitive)
let range = NSRange(location: 0, length: count)
return regex.stringByReplacingMatches(in: self, options: [], range: range, withTemplate: replaceWith)
} catch { return self }
}
func removeRegexMatches(pattern: String, replaceWith: String = "") -> String {
do {
let regex = try NSRegularExpression(pattern: pattern, options: .caseInsensitive)
let range = NSRange(location: 0, length: count)
return regex.stringByReplacingMatches(in: self, options: [], range: range, withTemplate: replaceWith)
} catch { return self }
}
func leftPad(toWidth width: Int, withString string: String?) -> String {
let paddingString = string ?? " "
func leftPad(toWidth width: Int, withString string: String?) -> String {
let paddingString = string ?? " "
if self.count >= width {
return self
}
if self.count >= width {
return self
}
let remainingLength: Int = width - self.count
var padString = String()
for _ in 0 ..< remainingLength {
padString += paddingString
}
let remainingLength: Int = width - self.count
var padString = String()
for _ in 0 ..< remainingLength {
padString += paddingString
}
return "\(padString)\(self)"
}
return "\(padString)\(self)"
}
var text: Text {
Text(self)
}
var text: Text {
Text(self)
}
}

View File

@ -1,16 +1,15 @@
//
/*
* 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 2021 Aiden Vigue & Jellyfin Contributors
*/
// 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 UIKit
extension UIDevice {
static var vendorUUIDString: String {
return current.identifierForVendor!.uuidString
}
static var vendorUUIDString: String {
current.identifierForVendor!.uuidString
}
}

View File

@ -1,22 +1,21 @@
//
/*
* 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 2021 Aiden Vigue & Jellyfin Contributors
*/
// 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
extension URLComponents {
mutating func addQueryItem(name: String, value: String?) {
if let _ = self.queryItems {
self.queryItems?.append(URLQueryItem(name: name, value: value))
} else {
self.queryItems = []
self.queryItems?.append(URLQueryItem(name: name, value: value))
}
}
mutating func addQueryItem(name: String, value: String?) {
if let _ = self.queryItems {
self.queryItems?.append(URLQueryItem(name: name, value: value))
} else {
self.queryItems = []
self.queryItems?.append(URLQueryItem(name: name, value: value))
}
}
}

View File

@ -1,26 +1,25 @@
//
/*
* SwiftFin is subject to the terms of the Mozilla Public
* License, v2.0. If a copy of the MPL was not distributed with this
* file, you can obtain one at https://mozilla.org/MPL/2.0/.
*
* Copyright 2021 Aiden Vigue & Jellyfin Contributors
*/
// 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
public extension URL {
/// Dictionary of the URL's query parameters
var queryParameters: [String: String]? {
guard let components = URLComponents(url: self, resolvingAgainstBaseURL: false),
let queryItems = components.queryItems else { return nil }
/// Dictionary of the URL's query parameters
var queryParameters: [String: String]? {
guard let components = URLComponents(url: self, resolvingAgainstBaseURL: false),
let queryItems = components.queryItems else { return nil }
var items: [String: String] = [:]
var items: [String: String] = [:]
for queryItem in queryItems {
items[queryItem.name] = queryItem.value
}
for queryItem in queryItems {
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
* 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
*/
// 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
import SwiftUI
extension View {
func eraseToAnyView() -> AnyView {
return AnyView(self)
}
func eraseToAnyView() -> AnyView {
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
// 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 nesting type_body_length type_name vertical_whitespace_opening_braces
internal enum L10n {
/// Accessibility
internal static let accessibility = L10n.tr("Localizable", "accessibility")
/// Add URL
internal static let addURL = L10n.tr("Localizable", "addURL")
/// All Genres
internal static let allGenres = L10n.tr("Localizable", "allGenres")
/// All Media
internal static let allMedia = L10n.tr("Localizable", "allMedia")
/// Appearance
internal static let appearance = L10n.tr("Localizable", "appearance")
/// Apply
internal static let apply = L10n.tr("Localizable", "apply")
/// Audio & Captions
internal static let audioAndCaptions = L10n.tr("Localizable", "audioAndCaptions")
/// Audio Track
internal static let audioTrack = L10n.tr("Localizable", "audioTrack")
/// Back
internal static let back = L10n.tr("Localizable", "back")
/// CAST
internal static let cast = L10n.tr("Localizable", "cast")
/// Change Server
internal static let changeServer = L10n.tr("Localizable", "changeServer")
/// Closed Captions
internal static let closedCaptions = L10n.tr("Localizable", "closedCaptions")
/// Connect
internal static let connect = L10n.tr("Localizable", "connect")
/// Connect Manually
internal static let connectManually = L10n.tr("Localizable", "connectManually")
/// Connect to Jellyfin
internal static let connectToJellyfin = L10n.tr("Localizable", "connectToJellyfin")
/// Connect to Server
internal static let connectToServer = L10n.tr("Localizable", "connectToServer")
/// Continue Watching
internal static let continueWatching = L10n.tr("Localizable", "continueWatching")
/// Dark
internal static let dark = L10n.tr("Localizable", "dark")
/// DIRECTOR
internal static let director = L10n.tr("Localizable", "director")
/// Discovered Servers
internal static let discoveredServers = L10n.tr("Localizable", "discoveredServers")
/// Display order
internal static let displayOrder = L10n.tr("Localizable", "displayOrder")
/// Empty Next Up
internal static let emptyNextUp = L10n.tr("Localizable", "emptyNextUp")
/// Episodes
internal static let episodes = L10n.tr("Localizable", "episodes")
/// Error
internal static let error = L10n.tr("Localizable", "error")
/// Existing Server
internal static let existingServer = L10n.tr("Localizable", "existingServer")
/// Filter Results
internal static let filterResults = L10n.tr("Localizable", "filterResults")
/// Filters
internal static let filters = L10n.tr("Localizable", "filters")
/// Genres
internal static let genres = L10n.tr("Localizable", "genres")
/// Home
internal static let home = L10n.tr("Localizable", "home")
/// Latest %@
internal static func latestWithString(_ p1: Any) -> String {
return L10n.tr("Localizable", "latestWithString", String(describing: p1))
}
/// Library
internal static let library = L10n.tr("Localizable", "library")
/// Light
internal static let light = L10n.tr("Localizable", "light")
/// Loading
internal static let loading = L10n.tr("Localizable", "loading")
/// Local Servers
internal static let localServers = L10n.tr("Localizable", "localServers")
/// Login
internal static let login = L10n.tr("Localizable", "login")
/// Login to %@
internal static func loginToWithString(_ p1: Any) -> String {
return L10n.tr("Localizable", "loginToWithString", String(describing: p1))
}
/// More Like This
internal static let moreLikeThis = L10n.tr("Localizable", "moreLikeThis")
/// Next Up
internal static let nextUp = L10n.tr("Localizable", "nextUp")
/// No Cast devices found..
internal static let noCastdevicesfound = L10n.tr("Localizable", "noCastdevicesfound")
/// No results.
internal static let noResults = L10n.tr("Localizable", "noResults")
/// Type: %@ not implemented yet :(
internal static func notImplementedYetWithType(_ p1: Any) -> String {
return L10n.tr("Localizable", "notImplementedYetWithType", String(describing: p1))
}
/// Ok
internal static let ok = L10n.tr("Localizable", "ok")
/// Other User
internal static let otherUser = L10n.tr("Localizable", "otherUser")
/// Page %1$@ of %2$@
internal static func pageOfWithNumbers(_ p1: Any, _ p2: Any) -> String {
return L10n.tr("Localizable", "pageOfWithNumbers", String(describing: p1), String(describing: p2))
}
/// Password
internal static let password = L10n.tr("Localizable", "password")
/// Play
internal static let play = L10n.tr("Localizable", "play")
/// Playback settings
internal static let playbackSettings = L10n.tr("Localizable", "playbackSettings")
/// Playback Speed
internal static let playbackSpeed = L10n.tr("Localizable", "playbackSpeed")
/// Play Next
internal static let playNext = L10n.tr("Localizable", "playNext")
/// Reset
internal static let reset = L10n.tr("Localizable", "reset")
/// Search
internal static let search = L10n.tr("Localizable", "search")
/// S%1$@:E%2$@
internal static func seasonAndEpisode(_ p1: Any, _ p2: Any) -> String {
return L10n.tr("Localizable", "seasonAndEpisode", String(describing: p1), String(describing: p2))
}
/// Seasons
internal static let seasons = L10n.tr("Localizable", "seasons")
/// See All
internal static let seeAll = L10n.tr("Localizable", "seeAll")
/// Select Cast Destination
internal static let selectCastDestination = L10n.tr("Localizable", "selectCastDestination")
/// Server %s already exists. Add new URL?
internal static func serverAlreadyExistsPrompt(_ p1: UnsafePointer<CChar>) -> String {
return L10n.tr("Localizable", "serverAlreadyExistsPrompt", p1)
}
/// Server Information
internal static let serverInformation = L10n.tr("Localizable", "serverInformation")
/// Server URL
internal static let serverURL = L10n.tr("Localizable", "serverURL")
/// Signed in as %@
internal static func signedInAsWithString(_ p1: Any) -> String {
return L10n.tr("Localizable", "signedInAsWithString", String(describing: p1))
}
/// Sort by
internal static let sortBy = L10n.tr("Localizable", "sortBy")
/// STUDIO
internal static let studio = L10n.tr("Localizable", "studio")
/// Studios
internal static let studios = L10n.tr("Localizable", "studios")
/// Suggestions
internal static let suggestions = L10n.tr("Localizable", "suggestions")
/// Switch user
internal static let switchUser = L10n.tr("Localizable", "switchUser")
/// System
internal static let system = L10n.tr("Localizable", "system")
/// Tags
internal static let tags = L10n.tr("Localizable", "tags")
/// Try again
internal static let tryAgain = L10n.tr("Localizable", "tryAgain")
/// Unknown Error
internal static let unknownError = L10n.tr("Localizable", "unknownError")
/// 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")
/// Accessibility
internal static let accessibility = L10n.tr("Localizable", "accessibility")
/// Add URL
internal static let addURL = L10n.tr("Localizable", "addURL")
/// All Genres
internal static let allGenres = L10n.tr("Localizable", "allGenres")
/// All Media
internal static let allMedia = L10n.tr("Localizable", "allMedia")
/// Appearance
internal static let appearance = L10n.tr("Localizable", "appearance")
/// Apply
internal static let apply = L10n.tr("Localizable", "apply")
/// Audio & Captions
internal static let audioAndCaptions = L10n.tr("Localizable", "audioAndCaptions")
/// Audio Track
internal static let audioTrack = L10n.tr("Localizable", "audioTrack")
/// Back
internal static let back = L10n.tr("Localizable", "back")
/// CAST
internal static let cast = L10n.tr("Localizable", "cast")
/// Change Server
internal static let changeServer = L10n.tr("Localizable", "changeServer")
/// Closed Captions
internal static let closedCaptions = L10n.tr("Localizable", "closedCaptions")
/// Connect
internal static let connect = L10n.tr("Localizable", "connect")
/// Connect Manually
internal static let connectManually = L10n.tr("Localizable", "connectManually")
/// Connect to Jellyfin
internal static let connectToJellyfin = L10n.tr("Localizable", "connectToJellyfin")
/// Connect to Server
internal static let connectToServer = L10n.tr("Localizable", "connectToServer")
/// Continue Watching
internal static let continueWatching = L10n.tr("Localizable", "continueWatching")
/// Dark
internal static let dark = L10n.tr("Localizable", "dark")
/// DIRECTOR
internal static let director = L10n.tr("Localizable", "director")
/// Discovered Servers
internal static let discoveredServers = L10n.tr("Localizable", "discoveredServers")
/// Display order
internal static let displayOrder = L10n.tr("Localizable", "displayOrder")
/// Empty Next Up
internal static let emptyNextUp = L10n.tr("Localizable", "emptyNextUp")
/// Episodes
internal static let episodes = L10n.tr("Localizable", "episodes")
/// Error
internal static let error = L10n.tr("Localizable", "error")
/// Existing Server
internal static let existingServer = L10n.tr("Localizable", "existingServer")
/// Filter Results
internal static let filterResults = L10n.tr("Localizable", "filterResults")
/// Filters
internal static let filters = L10n.tr("Localizable", "filters")
/// Genres
internal static let genres = L10n.tr("Localizable", "genres")
/// Home
internal static let home = L10n.tr("Localizable", "home")
/// Latest %@
internal static func latestWithString(_ p1: Any) -> String {
L10n.tr("Localizable", "latestWithString", String(describing: p1))
}
/// Library
internal static let library = L10n.tr("Localizable", "library")
/// Light
internal static let light = L10n.tr("Localizable", "light")
/// Loading
internal static let loading = L10n.tr("Localizable", "loading")
/// Local Servers
internal static let localServers = L10n.tr("Localizable", "localServers")
/// Login
internal static let login = L10n.tr("Localizable", "login")
/// Login to %@
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
internal static let nextUp = L10n.tr("Localizable", "nextUp")
/// No Cast devices found..
internal static let noCastdevicesfound = L10n.tr("Localizable", "noCastdevicesfound")
/// No results.
internal static let noResults = L10n.tr("Localizable", "noResults")
/// Type: %@ not implemented yet :(
internal static func notImplementedYetWithType(_ p1: Any) -> String {
L10n.tr("Localizable", "notImplementedYetWithType", String(describing: p1))
}
/// Ok
internal static let ok = L10n.tr("Localizable", "ok")
/// Other User
internal static let otherUser = L10n.tr("Localizable", "otherUser")
/// Page %1$@ of %2$@
internal static func pageOfWithNumbers(_ p1: Any, _ p2: Any) -> String {
L10n.tr("Localizable", "pageOfWithNumbers", String(describing: p1), String(describing: p2))
}
/// Password
internal static let password = L10n.tr("Localizable", "password")
/// Play
internal static let play = L10n.tr("Localizable", "play")
/// Playback settings
internal static let playbackSettings = L10n.tr("Localizable", "playbackSettings")
/// Playback Speed
internal static let playbackSpeed = L10n.tr("Localizable", "playbackSpeed")
/// Play Next
internal static let playNext = L10n.tr("Localizable", "playNext")
/// Reset
internal static let reset = L10n.tr("Localizable", "reset")
/// Search
internal static let search = L10n.tr("Localizable", "search")
/// S%1$@:E%2$@
internal static func seasonAndEpisode(_ p1: Any, _ p2: Any) -> String {
L10n.tr("Localizable", "seasonAndEpisode", String(describing: p1), String(describing: p2))
}
/// Seasons
internal static let seasons = L10n.tr("Localizable", "seasons")
/// See All
internal static let seeAll = L10n.tr("Localizable", "seeAll")
/// Select Cast Destination
internal static let selectCastDestination = L10n.tr("Localizable", "selectCastDestination")
/// Server %s already exists. Add new URL?
internal static func serverAlreadyExistsPrompt(_ p1: UnsafePointer<CChar>) -> String {
L10n.tr("Localizable", "serverAlreadyExistsPrompt", p1)
}
/// Server Information
internal static let serverInformation = L10n.tr("Localizable", "serverInformation")
/// Server URL
internal static let serverURL = L10n.tr("Localizable", "serverURL")
/// Signed in as %@
internal static func signedInAsWithString(_ p1: Any) -> String {
L10n.tr("Localizable", "signedInAsWithString", String(describing: p1))
}
/// Sort by
internal static let sortBy = L10n.tr("Localizable", "sortBy")
/// STUDIO
internal static let studio = L10n.tr("Localizable", "studio")
/// Studios
internal static let studios = L10n.tr("Localizable", "studios")
/// Suggestions
internal static let suggestions = L10n.tr("Localizable", "suggestions")
/// Switch user
internal static let switchUser = L10n.tr("Localizable", "switchUser")
/// System
internal static let system = L10n.tr("Localizable", "system")
/// Tags
internal static let tags = L10n.tr("Localizable", "tags")
/// Try again
internal static let tryAgain = L10n.tr("Localizable", "tryAgain")
/// Unknown Error
internal static let unknownError = L10n.tr("Localizable", "unknownError")
/// 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 nesting type_body_length type_name vertical_whitespace_opening_braces
// MARK: - Implementation Details
extension L10n {
private static func tr(_ table: String, _ key: String, _ args: CVarArg...) -> String {
let format = BundleToken.bundle.localizedString(forKey: key, value: nil, table: table)
return String(format: format, locale: Locale.current, arguments: args)
}
private static func tr(_ table: String, _ key: String, _ args: CVarArg...) -> String {
let format = BundleToken.bundle.localizedString(forKey: key, value: nil, table: table)
return String(format: format, locale: Locale.current, arguments: args)
}
}
// swiftlint:disable convenience_type
private final class BundleToken {
static let bundle: Bundle = {
#if SWIFT_PACKAGE
return Bundle.module
#else
return Bundle(for: BundleToken.self)
#endif
}()
static let bundle: Bundle = {
#if SWIFT_PACKAGE
return Bundle.module
#else
return Bundle(for: BundleToken.self)
#endif
}()
}
// swiftlint:enable convenience_type

View File

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

View File

@ -1,15 +1,14 @@
//
/*
* 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 2021 Aiden Vigue & Jellyfin Contributors
*/
// 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
struct Bitrates: Codable, Hashable {
public var name: String
public var value: Int
public var name: String
public var value: Int
}

View File

@ -1,25 +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
* file, you can obtain one at https://mozilla.org/MPL/2.0/.
*
* Copyright 2021 Aiden Vigue & Jellyfin Contributors
*/
// 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
import JellyfinAPI
enum DetailItemType: String {
case movie = "Movie"
case season = "Season"
case series = "Series"
case episode = "Episode"
case movie = "Movie"
case season = "Season"
case series = "Series"
case episode = "Episode"
}
struct DetailItem {
let baseItem: BaseItemDto
let type: DetailItemType
let baseItem: BaseItemDto
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
* file, you can obtain one at https://mozilla.org/MPL/2.0/.
*
* Copyright 2021 Aiden Vigue & Jellyfin Contributors
*/
//
// 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
//
// lol can someone buy me a coffee this took forever :|
@ -11,196 +12,241 @@ import Foundation
import JellyfinAPI
enum CPUModel {
case A4
case A5
case A5X
case A6
case A6X
case A7
case A7X
case A8
case A8X
case A9
case A9X
case A10
case A10X
case A11
case A12
case A12X
case A12Z
case A13
case A14
case A99
case A4
case A5
case A5X
case A6
case A6X
case A7
case A7X
case A8
case A8X
case A9
case A9X
case A10
case A10X
case A11
case A12
case A12X
case A12Z
case A13
case A14
case A99
}
class DeviceProfileBuilder {
public var bitrate: Int = 0
public var bitrate: Int = 0
public func setMaxBitrate(bitrate: Int) {
self.bitrate = bitrate
}
public func setMaxBitrate(bitrate: Int) {
self.bitrate = bitrate
}
public func buildProfile() -> DeviceProfile {
let maxStreamingBitrate = bitrate
let maxStaticBitrate = bitrate
let musicStreamingTranscodingBitrate = bitrate
public func buildProfile() -> DeviceProfile {
let maxStreamingBitrate = bitrate
let maxStaticBitrate = bitrate
let musicStreamingTranscodingBitrate = bitrate
// Build direct play profiles
var directPlayProfiles: [DirectPlayProfile] = []
directPlayProfiles = [DirectPlayProfile(container: "mov,mp4,mkv,webm", audioCodec: "aac,mp3,wav", videoCodec: "h264,mpeg4,vp9", type: .video)]
// Build direct play profiles
var directPlayProfiles: [DirectPlayProfile] = []
directPlayProfiles =
[DirectPlayProfile(container: "mov,mp4,mkv,webm", audioCodec: "aac,mp3,wav", videoCodec: "h264,mpeg4,vp9", type: .video)]
// Device supports Dolby Digital (AC3, EAC3)
if supportsFeature(minimumSupported: .A8X) {
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
} 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 Digital (AC3, EAC3)
if supportsFeature(minimumSupported: .A8X) {
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
} 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?
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
}
// Device supports Dolby Vision?
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
}
// Device supports Dolby Atmos?
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
}
// Device supports Dolby Atmos?
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
}
// Build transcoding profiles
var transcodingProfiles: [TranscodingProfile] = []
transcodingProfiles = [TranscodingProfile(container: "ts", type: .video, videoCodec: "h264,mpeg4", audioCodec: "aac,mp3,wav")]
// Build transcoding profiles
var transcodingProfiles: [TranscodingProfile] = []
transcodingProfiles = [TranscodingProfile(container: "ts", type: .video, videoCodec: "h264,mpeg4", audioCodec: "aac,mp3,wav")]
// Device supports Dolby Digital (AC3, EAC3)
if supportsFeature(minimumSupported: .A8X) {
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)]
} 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 Dolby Digital (AC3, EAC3)
if supportsFeature(minimumSupported: .A8X) {
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)]
} 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?
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)]
}
// Device supports FLAC?
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)]
}
var codecProfiles: [CodecProfile] = []
var codecProfiles: [CodecProfile] = []
let h264CodecConditions: [ProfileCondition] = [
ProfileCondition(condition: .notEquals, property: .isAnamorphic, value: "true", isRequired: false),
ProfileCondition(condition: .equalsAny, property: .videoProfile, value: "high|main|baseline|constrained baseline", isRequired: false),
ProfileCondition(condition: .lessThanEqual, property: .videoLevel, value: "80", isRequired: false),
ProfileCondition(condition: .notEquals, property: .isInterlaced, value: "true", isRequired: false)]
let hevcCodecConditions: [ProfileCondition] = [
ProfileCondition(condition: .notEquals, property: .isAnamorphic, 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)]
let h264CodecConditions: [ProfileCondition] = [
ProfileCondition(condition: .notEquals, property: .isAnamorphic, value: "true", isRequired: false),
ProfileCondition(condition: .equalsAny, property: .videoProfile, value: "high|main|baseline|constrained baseline",
isRequired: false),
ProfileCondition(condition: .lessThanEqual, property: .videoLevel, value: "80", isRequired: false),
ProfileCondition(condition: .notEquals, property: .isInterlaced, value: "true", isRequired: false),
]
let hevcCodecConditions: [ProfileCondition] = [
ProfileCondition(condition: .notEquals, property: .isAnamorphic, 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) {
codecProfiles.append(CodecProfile(type: .video, applyConditions: hevcCodecConditions, codec: "hevc"))
}
if supportsFeature(minimumSupported: .A9) {
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: "ssa", method: .embed))
subtitleProfiles.append(SubtitleProfile(format: "subrip", method: .embed))
subtitleProfiles.append(SubtitleProfile(format: "sub", method: .embed))
subtitleProfiles.append(SubtitleProfile(format: "pgssub", method: .embed))
subtitleProfiles.append(SubtitleProfile(format: "ass", method: .embed))
subtitleProfiles.append(SubtitleProfile(format: "ssa", method: .embed))
subtitleProfiles.append(SubtitleProfile(format: "subrip", method: .embed))
subtitleProfiles.append(SubtitleProfile(format: "sub", method: .embed))
subtitleProfiles.append(SubtitleProfile(format: "pgssub", method: .embed))
// These need to be filtered. Most subrips are embedded. I hate subtitles.
subtitleProfiles.append(SubtitleProfile(format: "subrip", method: .external))
subtitleProfiles.append(SubtitleProfile(format: "sub", method: .external))
subtitleProfiles.append(SubtitleProfile(format: "ass", method: .external))
subtitleProfiles.append(SubtitleProfile(format: "ssa", method: .external))
subtitleProfiles.append(SubtitleProfile(format: "vtt", method: .external))
subtitleProfiles.append(SubtitleProfile(format: "ass", method: .external))
subtitleProfiles.append(SubtitleProfile(format: "ssa", method: .external))
// These need to be filtered. Most subrips are embedded. I hate subtitles.
subtitleProfiles.append(SubtitleProfile(format: "subrip", method: .external))
subtitleProfiles.append(SubtitleProfile(format: "sub", method: .external))
subtitleProfiles.append(SubtitleProfile(format: "ass", method: .external))
subtitleProfiles.append(SubtitleProfile(format: "ssa", method: .external))
subtitleProfiles.append(SubtitleProfile(format: "vtt", method: .external))
subtitleProfiles.append(SubtitleProfile(format: "ass", 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 {
let intValues: [CPUModel: Int] = [.A4: 1, .A5: 2, .A5X: 3, .A6: 4, .A6X: 5, .A7: 6, .A7X: 7, .A8: 8, .A8X: 9, .A9: 10, .A9X: 11, .A10: 12, .A10X: 13, .A11: 14, .A12: 15, .A12X: 16, .A12Z: 16, .A13: 17, .A14: 18, .A99: 99]
return intValues[CPUinfo()] ?? 0 >= intValues[minimumSupported] ?? 0
}
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,
]
return intValues[CPUinfo()] ?? 0 >= intValues[minimumSupported] ?? 0
}
/**********************************************
* CPUInfo():
* Returns a hardcoded value of the current
* devices CPU name.
***********************************************/
private func CPUinfo() -> CPUModel {
/**********************************************
* CPUInfo():
* Returns a hardcoded value of the current
* devices CPU name.
***********************************************/
private func CPUinfo() -> CPUModel {
#if targetEnvironment(simulator)
let identifier = ProcessInfo().environment["SIMULATOR_MODEL_IDENTIFIER"]!
#else
#if targetEnvironment(simulator)
let identifier = ProcessInfo().environment["SIMULATOR_MODEL_IDENTIFIER"]!
#else
var systemInfo = utsname()
uname(&systemInfo)
let machineMirror = Mirror(reflecting: systemInfo.machine)
let identifier = machineMirror.children.reduce("") { identifier, element in
guard let value = element.value as? Int8, value != 0 else { return identifier }
return identifier + String(UnicodeScalar(UInt8(value)))
}
#endif
var systemInfo = utsname()
uname(&systemInfo)
let machineMirror = Mirror(reflecting: systemInfo.machine)
let identifier = machineMirror.children.reduce("") { identifier, element in
guard let value = element.value as? Int8, value != 0 else { return identifier }
return identifier + String(UnicodeScalar(UInt8(value)))
}
#endif
switch identifier {
case "iPod5,1": return .A5
case "iPod7,1": return .A8
case "iPod9,1": return .A10
case "iPhone3,1", "iPhone3,2", "iPhone3,3": return .A4
case "iPhone4,1": return .A5
case "iPhone5,1", "iPhone5,2": return .A6
case "iPhone5,3", "iPhone5,4": return .A6
case "iPhone6,1", "iPhone6,2": return .A7
case "iPhone7,2": return .A8
case "iPhone7,1": return .A8
case "iPhone8,1": return .A9
case "iPhone8,2", "iPhone8,4": return .A9
case "iPhone9,1", "iPhone9,3": return .A10
case "iPhone9,2", "iPhone9,4": return .A10
case "iPhone10,1", "iPhone10,4": return .A11
case "iPhone10,2", "iPhone10,5": return .A11
case "iPhone10,3", "iPhone10,6": return .A11
case "iPhone11,2", "iPhone11,6", "iPhone11,8": return .A12
case "iPhone12,1", "iPhone12,3", "iPhone12,5", "iPhone12,8": return .A13
case "iPhone13,1", "iPhone13,2", "iPhone13,3", "iPhone13,4": return .A14
case "iPad2,1", "iPad2,2", "iPad2,3", "iPad2,4": return .A5
case "iPad3,1", "iPad3,2", "iPad3,3": return .A5X
case "iPad3,4", "iPad3,5", "iPad3,6": return .A6X
case "iPad4,1", "iPad4,2", "iPad4,3": return .A7
case "iPad5,3", "iPad5,4": return .A8X
case "iPad6,11", "iPad6,12": return .A9
case "iPad2,5", "iPad2,6", "iPad2,7": return .A5
case "iPad4,4", "iPad4,5", "iPad4,6": return .A7
case "iPad4,7", "iPad4,8", "iPad4,9": return .A7
case "iPad5,1", "iPad5,2": return .A8
case "iPad11,1", "iPad11,2": return .A12
case "iPad6,3", "iPad6,4": return .A9X
case "iPad6,7", "iPad6,8": return .A9X
case "iPad7,1", "iPad7,2": return .A10X
case "iPad7,3", "iPad7,4": return .A10X
case "iPad7,5", "iPad7,6", "iPad7,11", "iPad7,12": return .A10
case "iPad8,1", "iPad8,2", "iPad8,3", "iPad8,4": 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 "iPad11,3", "iPad11,4", "iPad11,6", "iPad11,7": return .A12
case "iPad13,1", "iPad13,2": return .A14
case "AppleTV5,3": return .A8
case "AppleTV6,2": return .A10X
case "AppleTV11,1": return .A12
case "AudioAccessory1,1": return .A8
default: return .A99
}
}
switch identifier {
case "iPod5,1": return .A5
case "iPod7,1": return .A8
case "iPod9,1": return .A10
case "iPhone3,1", "iPhone3,2", "iPhone3,3": return .A4
case "iPhone4,1": return .A5
case "iPhone5,1", "iPhone5,2": return .A6
case "iPhone5,3", "iPhone5,4": return .A6
case "iPhone6,1", "iPhone6,2": return .A7
case "iPhone7,2": return .A8
case "iPhone7,1": return .A8
case "iPhone8,1": return .A9
case "iPhone8,2", "iPhone8,4": return .A9
case "iPhone9,1", "iPhone9,3": return .A10
case "iPhone9,2", "iPhone9,4": return .A10
case "iPhone10,1", "iPhone10,4": return .A11
case "iPhone10,2", "iPhone10,5": return .A11
case "iPhone10,3", "iPhone10,6": return .A11
case "iPhone11,2", "iPhone11,6", "iPhone11,8": return .A12
case "iPhone12,1", "iPhone12,3", "iPhone12,5", "iPhone12,8": return .A13
case "iPhone13,1", "iPhone13,2", "iPhone13,3", "iPhone13,4": return .A14
case "iPad2,1", "iPad2,2", "iPad2,3", "iPad2,4": return .A5
case "iPad3,1", "iPad3,2", "iPad3,3": return .A5X
case "iPad3,4", "iPad3,5", "iPad3,6": return .A6X
case "iPad4,1", "iPad4,2", "iPad4,3": return .A7
case "iPad5,3", "iPad5,4": return .A8X
case "iPad6,11", "iPad6,12": return .A9
case "iPad2,5", "iPad2,6", "iPad2,7": return .A5
case "iPad4,4", "iPad4,5", "iPad4,6": return .A7
case "iPad4,7", "iPad4,8", "iPad4,9": return .A7
case "iPad5,1", "iPad5,2": return .A8
case "iPad11,1", "iPad11,2": return .A12
case "iPad6,3", "iPad6,4": return .A9X
case "iPad6,7", "iPad6,8": return .A9X
case "iPad7,1", "iPad7,2": return .A10X
case "iPad7,3", "iPad7,4": return .A10X
case "iPad7,5", "iPad7,6", "iPad7,11", "iPad7,12": return .A10
case "iPad8,1", "iPad8,2", "iPad8,3", "iPad8,4": 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 "iPad11,3", "iPad11,4", "iPad11,6", "iPad11,7": return .A12
case "iPad13,1", "iPad13,2": return .A14
case "AppleTV5,3": return .A8
case "AppleTV6,2": return .A10X
case "AppleTV11,1": return .A12
case "AudioAccessory1,1": return .A8
default: return .A99
}
}
}

View File

@ -1,11 +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
* file, you can obtain one at https://mozilla.org/MPL/2.0/.
*
* Copyright 2021 Aiden Vigue & Jellyfin Contributors
*/
// 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
//
// https://www.hackingwithswift.com/quick-start/swiftui/how-to-detect-device-rotation
import Foundation
@ -14,20 +13,20 @@ import SwiftUI
// Our custom view modifier to track rotation and
// call our action
struct DeviceRotationViewModifier: ViewModifier {
let action: (UIDeviceOrientation) -> Void
let action: (UIDeviceOrientation) -> Void
func body(content: Content) -> some View {
content
.onAppear()
.onReceive(NotificationCenter.default.publisher(for: UIDevice.orientationDidChangeNotification)) { _ in
action(UIDevice.current.orientation)
}
}
func body(content: Content) -> some View {
content
.onAppear()
.onReceive(NotificationCenter.default.publisher(for: UIDevice.orientationDidChangeNotification)) { _ in
action(UIDevice.current.orientation)
}
}
}
// A View wrapper to make the modifier easier to use
extension View {
func onRotate(perform action: @escaping (UIDeviceOrientation) -> Void) -> some View {
self.modifier(DeviceRotationViewModifier(action: action))
}
func onRotate(perform action: @escaping (UIDeviceOrientation) -> Void) -> some View {
self.modifier(DeviceRotationViewModifier(action: action))
}
}

View File

@ -1,16 +1,15 @@
//
/*
* 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 2021 Aiden Vigue & Jellyfin Contributors
*/
// 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 Defaults
import Foundation
enum HTTPScheme: String, Defaults.Serializable, CaseIterable {
case http
case https
case http
case https
}

View File

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

View File

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

View File

@ -1,14 +1,13 @@
//
/*
* 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 2021 Aiden Vigue & Jellyfin Contributors
*/
// 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
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
* 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
*/
// 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
public protocol PortraitImageStackable {
func imageURLContsructor(maxWidth: Int) -> URL
var title: String { get }
var subtitle: String? { get }
var blurHash: String { get }
var failureInitials: String { get }
var portraitImageID: String { get }
var showTitle: Bool { get }
func imageURLContsructor(maxWidth: Int) -> URL
var title: String { get }
var subtitle: String? { get }
var blurHash: String { get }
var failureInitials: String { get }
var portraitImageID: String { get }
var showTitle: Bool { get }
}

View File

@ -1,15 +1,14 @@
//
/*
* 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 2021 Aiden Vigue & Jellyfin Contributors
*/
// 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 PosterSize {
case small
case normal
case small
case normal
}

View File

@ -1,17 +1,16 @@
//
/*
* 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 2021 Aiden Vigue & Jellyfin Contributors
*/
// 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
struct TrackLanguage: Hashable {
var name: String
var isoCode: String
var name: 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
* file, you can obtain one at https://mozilla.org/MPL/2.0/.
*
* Copyright 2021 Aiden Vigue & Jellyfin Contributors
*/
//
// 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 Combine
import Foundation
import JellyfinAPI
struct LibraryFilters: Codable, Hashable {
var filters: [ItemFilter] = []
var sortOrder: [APISortOrder] = [.descending]
var withGenres: [NameGuidPair] = []
var tags: [String] = []
var sortBy: [SortBy] = [.name]
var filters: [ItemFilter] = []
var sortOrder: [APISortOrder] = [.descending]
var withGenres: [NameGuidPair] = []
var tags: [String] = []
var sortBy: [SortBy] = [.name]
}
public enum SortBy: String, Codable, CaseIterable {
case premiereDate = "PremiereDate"
case name = "SortName"
case dateAdded = "DateCreated"
case premiereDate = "PremiereDate"
case name = "SortName"
case dateAdded = "DateCreated"
}
extension SortBy {
var localized: String {
switch self {
case .premiereDate:
return "Premiere date"
case .name:
return "Name"
case .dateAdded:
return "Date added"
}
}
var localized: String {
switch self {
case .premiereDate:
return "Premiere date"
case .name:
return "Name"
case .dateAdded:
return "Date added"
}
}
}
extension ItemFilter {
static var supportedTypes: [ItemFilter] {
[.isUnplayed, isPlayed, .isFavorite, .likes]
}
static var supportedTypes: [ItemFilter] {
[.isUnplayed, isPlayed, .isFavorite, .likes]
}
var localized: String {
switch self {
case .isUnplayed:
return "Unplayed"
case .isPlayed:
return "Played"
case .isFavorite:
return "Favorites"
case .likes:
return "Liked Items"
default:
return ""
}
}
var localized: String {
switch self {
case .isUnplayed:
return "Unplayed"
case .isPlayed:
return "Played"
case .isFavorite:
return "Favorites"
case .likes:
return "Liked Items"
default:
return ""
}
}
}
extension APISortOrder {
var localized: String {
switch self {
case .ascending:
return "Ascending"
case .descending:
return "Descending"
}
}
var localized: String {
switch self {
case .ascending:
return "Ascending"
case .descending:
return "Descending"
}
}
}
enum ItemType: String {
case episode = "Episode"
case movie = "Movie"
case series = "Series"
case season = "Season"
case episode = "Episode"
case movie = "Movie"
case series = "Series"
case season = "Season"
var localized: String {
switch self {
case .episode:
return L10n.episodes
case .movie:
return "Movies"
case .series:
return "Shows"
default:
return ""
}
}
var localized: String {
switch self {
case .episode:
return L10n.episodes
case .movie:
return "Movies"
case .series:
return "Shows"
default:
return ""
}
}
}

View File

@ -1,52 +1,51 @@
//
/*
* 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 2021 Aiden Vigue & Jellyfin Contributors
*/
// 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 UIKit
import Defaults
import UIKit
enum VideoPlayerJumpLength: Int32, CaseIterable, Defaults.Serializable {
case thirty = 30
case fifteen = 15
case ten = 10
case five = 5
case thirty = 30
case fifteen = 15
case ten = 10
case five = 5
var label: String {
return "\(self.rawValue) seconds"
}
var shortLabel: String {
return "\(self.rawValue)s"
}
var forwardImageLabel: String {
switch self {
case .thirty:
return "goforward.30"
case .fifteen:
return "goforward.15"
case .ten:
return "goforward.10"
case .five:
return "goforward.5"
}
}
var backwardImageLabel: String {
switch self {
case .thirty:
return "gobackward.30"
case .fifteen:
return "gobackward.15"
case .ten:
return "gobackward.10"
case .five:
return "gobackward.5"
}
}
var label: String {
"\(self.rawValue) seconds"
}
var shortLabel: String {
"\(self.rawValue)s"
}
var forwardImageLabel: String {
switch self {
case .thirty:
return "goforward.30"
case .fifteen:
return "goforward.15"
case .ten:
return "goforward.10"
case .five:
return "goforward.5"
}
}
var backwardImageLabel: String {
switch self {
case .thirty:
return "gobackward.30"
case .fifteen:
return "gobackward.15"
case .ten:
return "gobackward.10"
case .five:
return "gobackward.5"
}
}
}

View File

@ -1,79 +1,76 @@
//
/*
* 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/.
*
* Created by Noah Kamara
* Copyright 2021 Aiden Vigue & Jellyfin Contributors
*/
// 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
public class ServerDiscovery {
public struct ServerLookupResponse: Codable, Hashable, Identifiable {
public struct ServerLookupResponse: Codable, Hashable, Identifiable {
public func hash(into hasher: inout Hasher) {
return hasher.combine(id)
}
public func hash(into hasher: inout Hasher) {
hasher.combine(id)
}
private let address: String
public let id: String
public let name: String
private let address: String
public let id: String
public let name: String
public var url: URL {
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 url: URL {
URL(string: self.address)!
}
public var port: Int {
let components = URLComponents(string: self.address)
if let port = components?.port {
return port
}
return 7359
}
public var host: String {
let components = URLComponents(string: self.address)
if let host = components?.host {
return host
}
return self.address
}
enum CodingKeys: String, CodingKey {
case address = "Address"
case id = "Id"
case name = "Name"
}
}
public var port: Int {
let components = URLComponents(string: self.address)
if let port = components?.port {
return port
}
return 7359
}
private let broadcastConn: UDPBroadcastConnection
enum CodingKeys: String, CodingKey {
case address = "Address"
case id = "Id"
case name = "Name"
}
}
public init() {
func receiveHandler(_ ipAddress: String, _ port: Int, _ response: Data) {
}
private let broadcastConn: UDPBroadcastConnection
func errorHandler(error: UDPBroadcastConnection.ConnectionError) {
}
self.broadcastConn = try! UDPBroadcastConnection(port: 7359, handler: receiveHandler, errorHandler: errorHandler)
}
public init() {
func receiveHandler(_ ipAddress: String, _ port: Int, _ response: Data) {}
public func locateServer(completion: @escaping (ServerLookupResponse?) -> Void) {
func receiveHandler(_ ipAddress: String, _ port: Int, _ data: Data) {
do {
let response = try JSONDecoder().decode(ServerLookupResponse.self, from: data)
LogManager.shared.log.debug("Received JellyfinServer from \"\(response.name)\"", tag: "ServerDiscovery")
completion(response)
} catch {
completion(nil)
}
}
self.broadcastConn.handler = receiveHandler
do {
try broadcastConn.sendBroadcast("Who is JellyfinServer?")
LogManager.shared.log.debug("Discovery broadcast sent", tag: "ServerDiscovery")
} catch {
print(error)
}
}
func errorHandler(error: UDPBroadcastConnection.ConnectionError) {}
self.broadcastConn = try! UDPBroadcastConnection(port: 7359, handler: receiveHandler, errorHandler: errorHandler)
}
public func locateServer(completion: @escaping (ServerLookupResponse?) -> Void) {
func receiveHandler(_ ipAddress: String, _ port: Int, _ data: Data) {
do {
let response = try JSONDecoder().decode(ServerLookupResponse.self, from: data)
LogManager.shared.log.debug("Received JellyfinServer from \"\(response.name)\"", tag: "ServerDiscovery")
completion(response)
} catch {
completion(nil)
}
}
self.broadcastConn.handler = receiveHandler
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
* 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/.
*
* Created by Gunter Hager on 10.02.16.
* Copyright © 2016 Gunter Hager. All rights reserved.
*/
// 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
import Darwin
import Foundation
// Addresses
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.
open class UDPBroadcastConnection {
// MARK: Properties
// MARK: Properties
/// The address of the UDP socket.
var address: sockaddr_in
/// The address of the UDP socket.
var address: sockaddr_in
/// Type of a closure that handles incoming UDP packets.
public typealias ReceiveHandler = (_ ipAddress: String, _ port: Int, _ response: Data) -> Void
/// Closure that handles incoming UDP packets.
var handler: ReceiveHandler?
/// Type of a closure that handles incoming UDP packets.
public typealias ReceiveHandler = (_ ipAddress: String, _ port: Int, _ response: Data) -> Void
/// Closure that handles incoming UDP packets.
var handler: ReceiveHandler?
/// Type of a closure that handles errors that were encountered during receiving UDP packets.
public typealias ErrorHandler = (_ error: ConnectionError) -> Void
/// Closure that handles errors that were encountered during receiving UDP packets.
var errorHandler: ErrorHandler?
/// Type of a closure that handles errors that were encountered during receiving UDP packets.
public typealias ErrorHandler = (_ error: ConnectionError) -> Void
/// Closure that handles errors that were encountered during receiving UDP packets.
var errorHandler: ErrorHandler?
/// A dispatch source for reading data from the UDP socket.
var responseSource: DispatchSourceRead?
/// A dispatch source for reading data from the UDP socket.
var responseSource: DispatchSourceRead?
/// The dispatch queue to run responseSource & reconnection on
var dispatchQueue: DispatchQueue = DispatchQueue.main
/// The dispatch queue to run responseSource & reconnection on
var dispatchQueue = DispatchQueue.main
/// Bind to port to start listening without first sending a message
var shouldBeBound: Bool = false
/// Bind to port to start listening without first sending a message
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.
///
/// - Parameters:
/// - port: Number of the UDP port to use.
/// - bindIt: Opens a port immediately if true, on demand if false. Default is false.
/// - handler: Handler that gets called when data is received.
/// - errorHandler: Handler that gets called when an error occurs.
/// - Throws: Throws a `ConnectionError` if an error occurs.
public init(port: UInt16, bindIt: Bool = false, handler: ReceiveHandler?, errorHandler: ErrorHandler?) throws {
self.address = sockaddr_in(
sin_len: __uint8_t(MemoryLayout<sockaddr_in>.size),
sin_family: sa_family_t(AF_INET),
sin_port: UDPBroadcastConnection.htonsPort(port: port),
sin_addr: INADDR_BROADCAST,
sin_zero: ( 0, 0, 0, 0, 0, 0, 0, 0 )
)
/// - 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:
/// - port: Number of the UDP port to use.
/// - bindIt: Opens a port immediately if true, on demand if false. Default is false.
/// - handler: Handler that gets called when data is received.
/// - errorHandler: Handler that gets called when an error occurs.
/// - Throws: Throws a `ConnectionError` if an error occurs.
public init(port: UInt16, bindIt: Bool = false, handler: ReceiveHandler?, errorHandler: ErrorHandler?) throws {
self.address = sockaddr_in(sin_len: __uint8_t(MemoryLayout<sockaddr_in>.size),
sin_family: sa_family_t(AF_INET),
sin_port: UDPBroadcastConnection.htonsPort(port: port),
sin_addr: INADDR_BROADCAST,
sin_zero: (0, 0, 0, 0, 0, 0, 0, 0))
self.handler = handler
self.errorHandler = errorHandler
self.shouldBeBound = bindIt
if bindIt {
try createSocket()
}
}
self.handler = handler
self.errorHandler = errorHandler
self.shouldBeBound = bindIt
if bindIt {
try createSocket()
}
}
deinit {
if responseSource != nil {
responseSource!.cancel()
}
}
deinit {
if responseSource != nil {
responseSource!.cancel()
}
}
// MARK: Interface
// MARK: Interface
/// Create a UDP socket for broadcasting and set up cancel and event handlers
///
/// - Throws: Throws a `ConnectionError` if an error occurs.
fileprivate func createSocket() throws {
/// Create a UDP socket for broadcasting and set up cancel and event handlers
///
/// - Throws: Throws a `ConnectionError` if an error occurs.
fileprivate func createSocket() throws {
// Create new socket
let newSocket = socket(AF_INET, SOCK_DGRAM, IPPROTO_UDP)
guard newSocket > 0 else { throw ConnectionError.createSocketFailed }
// Create new socket
let newSocket = socket(AF_INET, SOCK_DGRAM, IPPROTO_UDP)
guard newSocket > 0 else { throw ConnectionError.createSocketFailed }
// Enable broadcast on socket
var broadcastEnable = Int32(1)
let ret = setsockopt(newSocket, SOL_SOCKET, SO_BROADCAST, &broadcastEnable, socklen_t(MemoryLayout<UInt32>.size))
if ret == -1 {
debugPrint("Couldn't enable broadcast on socket")
close(newSocket)
throw ConnectionError.enableBroadcastFailed
}
// Enable broadcast on socket
var broadcastEnable = Int32(1)
let ret = setsockopt(newSocket, SOL_SOCKET, SO_BROADCAST, &broadcastEnable, socklen_t(MemoryLayout<UInt32>.size))
if ret == -1 {
debugPrint("Couldn't enable broadcast on socket")
close(newSocket)
throw ConnectionError.enableBroadcastFailed
}
// Bind socket if needed
if shouldBeBound {
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))
self.address.sin_addr = INADDR_ANY
memcpy(&saddr, &self.address, MemoryLayout<sockaddr_in>.size)
self.address.sin_addr = INADDR_BROADCAST
let isBound = bind(newSocket, &saddr, socklen_t(MemoryLayout<sockaddr_in>.size))
if isBound == -1 {
debugPrint("Couldn't bind socket")
close(newSocket)
throw ConnectionError.bindSocketFailed
}
}
// Bind socket if needed
if shouldBeBound {
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))
self.address.sin_addr = INADDR_ANY
memcpy(&saddr, &self.address, MemoryLayout<sockaddr_in>.size)
self.address.sin_addr = INADDR_BROADCAST
let isBound = bind(newSocket, &saddr, socklen_t(MemoryLayout<sockaddr_in>.size))
if isBound == -1 {
debugPrint("Couldn't bind socket")
close(newSocket)
throw ConnectionError.bindSocketFailed
}
}
// Disable global SIGPIPE handler so that the app doesn't crash
setNoSigPipe(socket: newSocket)
// Disable global SIGPIPE handler so that the app doesn't crash
setNoSigPipe(socket: newSocket)
// Set up a dispatch source
let newResponseSource = DispatchSource.makeReadSource(fileDescriptor: newSocket, queue: dispatchQueue)
// Set up a dispatch source
let newResponseSource = DispatchSource.makeReadSource(fileDescriptor: newSocket, queue: dispatchQueue)
// Set up cancel handler
newResponseSource.setCancelHandler {
// debugPrint("Closing UDP socket")
let UDPSocket = Int32(newResponseSource.handle)
shutdown(UDPSocket, SHUT_RDWR)
close(UDPSocket)
}
// Set up cancel handler
newResponseSource.setCancelHandler {
// debugPrint("Closing UDP socket")
let UDPSocket = Int32(newResponseSource.handle)
shutdown(UDPSocket, SHUT_RDWR)
close(UDPSocket)
}
// Set up event handler (gets called when data arrives at the UDP socket)
newResponseSource.setEventHandler { [unowned self] in
guard let source = self.responseSource else { return }
// Set up event handler (gets called when data arrives at the UDP socket)
newResponseSource.setEventHandler { [unowned self] in
guard let source = self.responseSource else { return }
var socketAddress = sockaddr_storage()
var socketAddressLength = socklen_t(MemoryLayout<sockaddr_storage>.size)
let response = [UInt8](repeating: 0, count: 4096)
let UDPSocket = Int32(source.handle)
var socketAddress = sockaddr_storage()
var socketAddressLength = socklen_t(MemoryLayout<sockaddr_storage>.size)
let response = [UInt8](repeating: 0, count: 4096)
let UDPSocket = Int32(source.handle)
let bytesRead = withUnsafeMutablePointer(to: &socketAddress) {
recvfrom(UDPSocket, UnsafeMutableRawPointer(mutating: response), response.count, 0, UnsafeMutableRawPointer($0).bindMemory(to: sockaddr.self, capacity: 1), &socketAddressLength)
}
let bytesRead = withUnsafeMutablePointer(to: &socketAddress) {
recvfrom(UDPSocket, UnsafeMutableRawPointer(mutating: response), response.count, 0,
UnsafeMutableRawPointer($0).bindMemory(to: sockaddr.self, capacity: 1), &socketAddressLength)
}
do {
guard bytesRead > 0 else {
self.closeConnection()
if bytesRead == 0 {
debugPrint("recvfrom returned EOF")
throw ConnectionError.receivedEndOfFile
} else {
if let errorString = String(validatingUTF8: strerror(errno)) {
debugPrint("recvfrom failed: \(errorString)")
}
throw ConnectionError.receiveFailed(code: errno)
}
}
do {
guard bytesRead > 0 else {
self.closeConnection()
if bytesRead == 0 {
debugPrint("recvfrom returned EOF")
throw ConnectionError.receivedEndOfFile
} else {
if let errorString = String(validatingUTF8: strerror(errno)) {
debugPrint("recvfrom failed: \(errorString)")
}
throw ConnectionError.receiveFailed(code: errno)
}
}
guard let endpoint = withUnsafePointer(to: &socketAddress, { self.getEndpointFromSocketAddress(socketAddressPointer: UnsafeRawPointer($0).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
}
guard let endpoint = withUnsafePointer(to: &socketAddress,
{
self
.getEndpointFromSocketAddress(socketAddressPointer: UnsafeRawPointer($0)
.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
self.handler?(endpoint.host, endpoint.port, responseBytes)
} catch {
if let error = error as? ConnectionError {
self.errorHandler?(error)
} else {
self.errorHandler?(ConnectionError.underlying(error: error))
}
}
// Handle response
self.handler?(endpoint.host, endpoint.port, responseBytes)
} catch {
if let error = error as? ConnectionError {
self.errorHandler?(error)
} else {
self.errorHandler?(ConnectionError.underlying(error: error))
}
}
}
}
newResponseSource.resume()
responseSource = newResponseSource
}
newResponseSource.resume()
responseSource = newResponseSource
}
/// Send broadcast message.
///
/// - 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.
///
/// - 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 data.
///
/// - Parameter data: Data to send via broadcast.
/// - Throws: Throws a `ConnectionError` if an error occurs.
open func sendBroadcast(_ data: Data) throws {
if responseSource == nil {
try createSocket()
}
/// Send broadcast data.
///
/// - Parameter data: Data to send via broadcast.
/// - Throws: Throws a `ConnectionError` if an error occurs.
open func sendBroadcast(_ data: Data) throws {
if responseSource == nil {
try createSocket()
}
guard let source = responseSource else { return }
let UDPSocket = Int32(source.handle)
let socketLength = socklen_t(address.sin_len)
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 let source = responseSource else { return }
let UDPSocket = Int32(source.handle)
let socketLength = socklen_t(address.sin_len)
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 {
closeConnection()
throw ConnectionError.sendingMessageFailed(code: errno)
}
}
}
guard sent > 0 else {
closeConnection()
throw ConnectionError.sendingMessageFailed(code: errno)
}
}
}
/// Close the connection.
///
/// - 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.
///
/// - 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
// 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.
///
/// - 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
switch Int32(socketAddress.sa_family) {
case AF_INET:
var socketAddressInet = UnsafeRawPointer(socketAddressPointer).load(as: sockaddr_in.self)
let length = Int(INET_ADDRSTRLEN) + 2
var buffer = [CChar](repeating: 0, count: length)
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_INET:
var socketAddressInet = UnsafeRawPointer(socketAddressPointer).load(as: sockaddr_in.self)
let length = Int(INET_ADDRSTRLEN) + 2
var buffer = [CChar](repeating: 0, count: length)
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)
case AF_INET6:
var socketAddressInet6 = UnsafeRawPointer(socketAddressPointer).load(as: sockaddr_in6.self)
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)
case AF_INET6:
var socketAddressInet6 = UnsafeRawPointer(socketAddressPointer).load(as: sockaddr_in6.self)
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:
return nil
}
}
default:
return nil
}
}
// MARK: - Private
// 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).
///
/// - 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))
}
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 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 {
(value << 8) + (value >> 8)
}
}
// Created by Gunter Hager on 25.03.19.
@ -292,25 +291,24 @@ open class UDPBroadcastConnection {
//
public extension UDPBroadcastConnection {
enum ConnectionError: Error {
// Creating socket
case createSocketFailed
case enableBroadcastFailed
case bindSocketFailed
enum ConnectionError: Error {
// Creating socket
case createSocketFailed
case enableBroadcastFailed
case bindSocketFailed
// Sending message
case messageEncodingFailed
case sendingMessageFailed(code: Int32)
// Sending message
case messageEncodingFailed
case sendingMessageFailed(code: Int32)
// Receiving data
case receivedEndOfFile
case receiveFailed(code: Int32)
// Receiving data
case receivedEndOfFile
case receiveFailed(code: Int32)
// Closing socket
case reopeningSocketFailed(error: Error)
// Underlying
case underlying(error: Error)
}
// Closing socket
case reopeningSocketFailed(error: Error)
// Underlying
case underlying(error: Error)
}
}

View File

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

View File

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

View File

@ -1,198 +1,215 @@
//
/*
* 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 2021 Aiden Vigue & Jellyfin Contributors
*/
// 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
import CoreStore
import Defaults
import Foundation
enum SwiftfinStore {
// MARK: State
// Safe, copyable representations of their underlying CoreStoredObject
// Relationships are represented by the related object's IDs or value
enum State {
// MARK: State
struct Server {
let uris: Set<String>
let currentURI: String
let name: String
let id: String
let os: String
let version: String
let userIDs: [String]
// Safe, copyable representations of their underlying CoreStoredObject
// Relationships are represented by the related object's IDs or value
enum State {
fileprivate init(uris: Set<String>, currentURI: String, name: String, id: String, os: String, version: String, usersIDs: [String]) {
self.uris = uris
self.currentURI = currentURI
self.name = name
self.id = id
self.os = os
self.version = version
self.userIDs = usersIDs
}
struct Server {
let uris: Set<String>
let currentURI: String
let name: String
let id: String
let os: String
let version: String
let userIDs: [String]
static var sample: Server {
return Server(uris: ["https://www.notaurl.com", "http://www.maybeaurl.org"],
currentURI: "https://www.notaurl.com",
name: "Johnny's Tree",
id: "123abc",
os: "macOS",
version: "1.1.1",
usersIDs: ["1", "2"])
}
}
fileprivate init(uris: Set<String>, currentURI: String, name: String, id: String, os: String, version: String,
usersIDs: [String])
{
self.uris = uris
self.currentURI = currentURI
self.name = name
self.id = id
self.os = os
self.version = version
self.userIDs = usersIDs
}
struct User {
let username: String
let id: String
let serverID: String
let accessToken: String
static var sample: Server {
Server(uris: ["https://www.notaurl.com", "http://www.maybeaurl.org"],
currentURI: "https://www.notaurl.com",
name: "Johnny's Tree",
id: "123abc",
os: "macOS",
version: "1.1.1",
usersIDs: ["1", "2"])
}
}
fileprivate init(username: String, id: String, serverID: String, accessToken: String) {
self.username = username
self.id = id
self.serverID = serverID
self.accessToken = accessToken
}
struct User {
let username: String
let id: String
let serverID: String
let accessToken: String
static var sample: User {
return User(username: "JohnnyAppleseed",
id: "123abc",
serverID: "123abc",
accessToken: "open-sesame")
}
}
}
fileprivate init(username: String, id: String, serverID: String, accessToken: String) {
self.username = username
self.id = id
self.serverID = serverID
self.accessToken = accessToken
}
// MARK: Models
enum Models {
static var sample: User {
User(username: "JohnnyAppleseed",
id: "123abc",
serverID: "123abc",
accessToken: "open-sesame")
}
}
}
final class StoredServer: CoreStoreObject {
// MARK: Models
@Field.Coded("uris", coder: FieldCoders.Json.self)
var uris: Set<String> = []
enum Models {
@Field.Stored("currentURI")
var currentURI: String = ""
final class StoredServer: CoreStoreObject {
@Field.Stored("name")
var name: String = ""
@Field.Coded("uris", coder: FieldCoders.Json.self)
var uris: Set<String> = []
@Field.Stored("id")
var id: String = ""
@Field.Stored("currentURI")
var currentURI: String = ""
@Field.Stored("os")
var os: String = ""
@Field.Stored("name")
var name: String = ""
@Field.Stored("version")
var version: String = ""
@Field.Stored("id")
var id: String = ""
@Field.Relationship("users", inverse: \StoredUser.$server)
var users: Set<StoredUser>
@Field.Stored("os")
var os: String = ""
var state: State.Server {
return State.Server(uris: uris,
currentURI: currentURI,
name: name,
id: id,
os: os,
version: version,
usersIDs: users.map({ $0.id }))
}
}
@Field.Stored("version")
var version: String = ""
final class StoredUser: CoreStoreObject {
@Field.Relationship("users", inverse: \StoredUser.$server)
var users: Set<StoredUser>
@Field.Stored("username")
var username: String = ""
var state: State.Server {
State.Server(uris: uris,
currentURI: currentURI,
name: name,
id: id,
os: os,
version: version,
usersIDs: users.map(\.id))
}
}
@Field.Stored("id")
var id: String = ""
final class StoredUser: CoreStoreObject {
@Field.Stored("appleTVID")
var appleTVID: String = ""
@Field.Stored("username")
var username: String = ""
@Field.Relationship("server")
var server: StoredServer?
@Field.Stored("id")
var id: String = ""
@Field.Relationship("accessToken", inverse: \StoredAccessToken.$user)
var accessToken: StoredAccessToken?
@Field.Stored("appleTVID")
var appleTVID: String = ""
var state: State.User {
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("server")
var server: StoredServer?
final class StoredAccessToken: CoreStoreObject {
@Field.Relationship("accessToken", inverse: \StoredAccessToken.$user)
var accessToken: StoredAccessToken?
@Field.Stored("value")
var value: String = ""
var state: State.User {
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")
var user: StoredUser?
}
}
final class StoredAccessToken: CoreStoreObject {
// MARK: Errors
enum Errors {
case existingServer(State.Server)
case existingUser(State.User)
}
@Field.Stored("value")
var value: String = ""
// 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": [0xa8c475e874494bb1, 0x79486e93449f0b3d, 0xa7dc4a0003541edb, 0x94183fae7580ef72],
"Server": [0x936b46acd8e8f0e3, 0x59890d4d9f3f885f, 0x819cf7a4abf98b22, 0xe16125c5af885a06],
"User": [0x845de08a74bc53ed, 0xe95a406a29f3a5d0, 0x9eda732821a15ea9, 0xb5afa531e41ce8a]
])
@Field.Relationship("user")
var user: StoredUser?
}
}
let _dataStack = DataStack(schema)
try! _dataStack.addStorageAndWait(
SQLiteStore(
fileName: "Swiftfin.sqlite",
localStorageOptions: .recreateStoreOnModelMismatch
)
)
return _dataStack
}()
// MARK: Errors
enum Errors {
case existingServer(State.Server)
case existingUser(State.User)
}
// 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
extension SwiftfinStore.Errors: LocalizedError {
var title: String {
switch self {
case .existingServer:
return "Existing Server"
case .existingUser:
return "Existing User"
}
}
var title: String {
switch self {
case .existingServer:
return "Existing Server"
case .existingUser:
return "Existing User"
}
}
var errorDescription: String? {
switch self {
case .existingServer(let server):
return "Server \(server.name) already exists with same server ID"
case .existingUser(let user):
return "User \(user.username) already exists with same user ID"
}
}
var errorDescription: String? {
switch self {
case let .existingServer(server):
return "Server \(server.name) already exists with same server ID"
case let .existingUser(user):
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
* 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
*/
// 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 Defaults
import Foundation
extension SwiftfinStore {
enum Defaults {
enum Defaults {
static let generalSuite: UserDefaults = {
return UserDefaults(suiteName: "swiftfinstore-general-defaults")!
}()
static let universalSuite: UserDefaults = {
return UserDefaults(suiteName: "swiftfinstore-universal-defaults")!
}()
}
static let generalSuite: UserDefaults = {
UserDefaults(suiteName: "swiftfinstore-general-defaults")!
}()
static let universalSuite: UserDefaults = {
UserDefaults(suiteName: "swiftfinstore-universal-defaults")!
}()
}
}
extension Defaults.Keys {
// Universal settings
static let defaultHTTPScheme = Key<HTTPScheme>("defaultHTTPScheme", default: .http, suite: SwiftfinStore.Defaults.universalSuite)
static let appAppearance = Key<AppAppearance>("appAppearance", default: .system, suite: SwiftfinStore.Defaults.universalSuite)
// General settings
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 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 autoSelectSubtitlesLangCode = Key<String>("AutoSelectSubtitlesLangCode", default: "Auto", 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)
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)
static let jumpGesturesEnabled = Key<Bool>("gesturesEnabled", default: true, suite: SwiftfinStore.Defaults.generalSuite)
static let videoPlayerJumpForward = Key<VideoPlayerJumpLength>("videoPlayerJumpForward", default: .fifteen, suite: SwiftfinStore.Defaults.generalSuite)
static let videoPlayerJumpBackward = Key<VideoPlayerJumpLength>("videoPlayerJumpBackward", default: .fifteen, suite: SwiftfinStore.Defaults.generalSuite)
static let autoplayEnabled = Key<Bool>("autoPlayNextItem", default: true, suite: SwiftfinStore.Defaults.generalSuite)
static let resumeOffset = Key<Bool>("resumeOffset", default: false, suite: SwiftfinStore.Defaults.generalSuite)
// Should show video player items
static let shouldShowPlayPreviousItem = Key<Bool>("shouldShowPreviousItem", default: true, 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 in overlay menu
static let shouldShowJumpButtonsInOverlayMenu = Key<Bool>("shouldShowJumpButtonsInMenu", default: true, suite: SwiftfinStore.Defaults.generalSuite)
// Experimental settings
struct Experimental {
static let syncSubtitleStateWithAdjacent = Key<Bool>("experimental.syncSubtitleState", default: false, suite: SwiftfinStore.Defaults.generalSuite)
static let liveTVAlphaEnabled = Key<Bool>("liveTVAlphaEnabled", default: false, suite: SwiftfinStore.Defaults.generalSuite)
}
// 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)
// Universal settings
static let defaultHTTPScheme = Key<HTTPScheme>("defaultHTTPScheme", default: .http, suite: SwiftfinStore.Defaults.universalSuite)
static let appAppearance = Key<AppAppearance>("appAppearance", default: .system, suite: SwiftfinStore.Defaults.universalSuite)
// General settings
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 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 autoSelectSubtitlesLangCode = Key<String>("AutoSelectSubtitlesLangCode", default: "Auto",
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)
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)
static let jumpGesturesEnabled = Key<Bool>("gesturesEnabled", default: true, suite: SwiftfinStore.Defaults.generalSuite)
static let videoPlayerJumpForward = Key<VideoPlayerJumpLength>("videoPlayerJumpForward", default: .fifteen,
suite: SwiftfinStore.Defaults.generalSuite)
static let videoPlayerJumpBackward = Key<VideoPlayerJumpLength>("videoPlayerJumpBackward", default: .fifteen,
suite: SwiftfinStore.Defaults.generalSuite)
static let autoplayEnabled = Key<Bool>("autoPlayNextItem", default: true, suite: SwiftfinStore.Defaults.generalSuite)
static let resumeOffset = Key<Bool>("resumeOffset", default: false, suite: SwiftfinStore.Defaults.generalSuite)
// Should show video player items
static let shouldShowPlayPreviousItem = Key<Bool>("shouldShowPreviousItem", default: true, 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 in overlay menu
static let shouldShowJumpButtonsInOverlayMenu = Key<Bool>("shouldShowJumpButtonsInMenu", default: true,
suite: SwiftfinStore.Defaults.generalSuite)
// Experimental settings
enum Experimental {
static let syncSubtitleStateWithAdjacent = Key<Bool>("experimental.syncSubtitleState", default: false,
suite: SwiftfinStore.Defaults.generalSuite)
static let liveTVAlphaEnabled = Key<Bool>("liveTVAlphaEnabled", default: false, suite: SwiftfinStore.Defaults.generalSuite)
}
// 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
* 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
*/
// 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 SwiftUI
final class BasicAppSettingsViewModel: ViewModel {
let appearances = AppAppearance.allCases
let appearances = AppAppearance.allCases
func resetUserSettings() {
SwiftfinStore.Defaults.generalSuite.removeAll()
}
func resetAppSettings() {
SwiftfinStore.Defaults.universalSuite.removeAll()
}
func removeAllUsers() {
SessionManager.main.purge()
}
func resetUserSettings() {
SwiftfinStore.Defaults.generalSuite.removeAll()
}
func resetAppSettings() {
SwiftfinStore.Defaults.universalSuite.removeAll()
}
func removeAllUsers() {
SessionManager.main.purge()
}
}

View File

@ -1,11 +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
* file, you can obtain one at https://mozilla.org/MPL/2.0/.
*
* Copyright 2021 Aiden Vigue & Jellyfin Contributors
*/
// 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 Combine
import Foundation
@ -14,113 +13,120 @@ import Stinsen
struct AddServerURIPayload: Identifiable {
let server: SwiftfinStore.State.Server
let uri: String
let server: SwiftfinStore.State.Server
let uri: String
var id: String {
return server.id.appending(uri)
}
var id: String {
server.id.appending(uri)
}
}
final class ConnectToServerViewModel: ViewModel {
@RouterObject var router: ConnectToServerCoodinator.Router?
@Published var discoveredServers: Set<ServerDiscovery.ServerLookupResponse> = []
@Published var searching = false
@Published var addServerURIPayload: AddServerURIPayload?
var backAddServerURIPayload: AddServerURIPayload?
@RouterObject
var router: ConnectToServerCoodinator.Router?
@Published
var discoveredServers: Set<ServerDiscovery.ServerLookupResponse> = []
@Published
var searching = false
@Published
var addServerURIPayload: AddServerURIPayload?
var backAddServerURIPayload: AddServerURIPayload?
private let discovery = ServerDiscovery()
private let discovery = ServerDiscovery()
var alertTitle: String {
var message: String = ""
if errorMessage?.code != ErrorMessage.noShowErrorCode {
message.append(contentsOf: "\(errorMessage?.code ?? ErrorMessage.noShowErrorCode)\n")
}
message.append(contentsOf: "\(errorMessage?.title ?? "Unkown Error")")
return message
}
var alertTitle: String {
var message: String = ""
if errorMessage?.code != ErrorMessage.noShowErrorCode {
message.append(contentsOf: "\(errorMessage?.code ?? ErrorMessage.noShowErrorCode)\n")
}
message.append(contentsOf: "\(errorMessage?.title ?? "Unkown Error")")
return message
}
func connectToServer(uri: String) {
#if targetEnvironment(simulator)
var uri = uri
if uri == "localhost" {
uri = "http://localhost:8096"
}
#endif
let trimmedURI = uri.trimmingCharacters(in: .whitespaces)
func connectToServer(uri: String) {
#if targetEnvironment(simulator)
var uri = uri
if uri == "localhost" {
uri = "http://localhost:8096"
}
#endif
LogManager.shared.log.debug("Attempting to connect to server at \"\(trimmedURI)\"", tag: "connectToServer")
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)
}
let trimmedURI = uri.trimmingCharacters(in: .whitespaces)
func discoverServers() {
discoveredServers.removeAll()
searching = true
LogManager.shared.log.debug("Attempting to connect to server at \"\(trimmedURI)\"", tag: "connectToServer")
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 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
DispatchQueue.main.asyncAfter(deadline: .now() + 3) {
self.searching = false
}
func discoverServers() {
discoveredServers.removeAll()
searching = true
discovery.locateServer { [self] server in
if let server = server {
discoveredServers.insert(server)
}
}
}
// Timeout after 3 seconds
DispatchQueue.main.asyncAfter(deadline: .now() + 3) {
self.searching = false
}
func addURIToServer(addServerURIPayload: AddServerURIPayload) {
SessionManager.main.addURIToServer(server: addServerURIPayload.server, uri: addServerURIPayload.uri)
.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)
}
discovery.locateServer { [self] server in
if let server = server {
discoveredServers.insert(server)
}
}
}
func cancelConnection() {
for cancellable in cancellables {
cancellable.cancel()
}
func addURIToServer(addServerURIPayload: AddServerURIPayload) {
SessionManager.main.addURIToServer(server: addServerURIPayload.server, uri: addServerURIPayload.uri)
.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
* 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
*/
// 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 JellyfinAPI
import SwiftUI
final class EpisodesRowViewModel: ViewModel {
// TODO: Protocol these viewmodels for generalization instead of Episode
@ObservedObject var episodeItemViewModel: EpisodeItemViewModel
@Published var seasonsEpisodes: [BaseItemDto: [BaseItemDto]] = [:]
@Published var selectedSeason: BaseItemDto? {
willSet {
if seasonsEpisodes[newValue!]!.isEmpty {
retrieveEpisodesForSeason(newValue!)
}
}
}
init(episodeItemViewModel: EpisodeItemViewModel) {
self.episodeItemViewModel = episodeItemViewModel
super.init()
retrieveSeasons()
}
private func retrieveSeasons() {
TvShowsAPI.getSeasons(seriesId: episodeItemViewModel.item.seriesId ?? "",
userId: SessionManager.main.currentLogin.user.id)
.sink { completion in
self.handleAPIRequestError(completion: completion)
} receiveValue: { response in
let seasons = response.items ?? []
seasons.forEach { season in
self.seasonsEpisodes[season] = []
if season.id == self.episodeItemViewModel.item.seasonId ?? "" {
self.selectedSeason = season
}
}
}
.store(in: &cancellables)
}
private func retrieveEpisodesForSeason(_ season: BaseItemDto) {
guard let seasonID = season.id else { return }
TvShowsAPI.getEpisodes(seriesId: episodeItemViewModel.item.seriesId ?? "",
userId: SessionManager.main.currentLogin.user.id,
fields: [.primaryImageAspectRatio, .seriesPrimaryImage, .seasonUserData, .overview, .genres, .people],
seasonId: seasonID)
.trackActivity(loading)
.sink { completion in
self.handleAPIRequestError(completion: completion)
} receiveValue: { episodes in
self.seasonsEpisodes[season] = episodes.items ?? []
}
.store(in: &cancellables)
}
// TODO: Protocol these viewmodels for generalization instead of Episode
@ObservedObject
var episodeItemViewModel: EpisodeItemViewModel
@Published
var seasonsEpisodes: [BaseItemDto: [BaseItemDto]] = [:]
@Published
var selectedSeason: BaseItemDto? {
willSet {
if seasonsEpisodes[newValue!]!.isEmpty {
retrieveEpisodesForSeason(newValue!)
}
}
}
init(episodeItemViewModel: EpisodeItemViewModel) {
self.episodeItemViewModel = episodeItemViewModel
super.init()
retrieveSeasons()
}
private func retrieveSeasons() {
TvShowsAPI.getSeasons(seriesId: episodeItemViewModel.item.seriesId ?? "",
userId: SessionManager.main.currentLogin.user.id)
.sink { completion in
self.handleAPIRequestError(completion: completion)
} receiveValue: { response in
let seasons = response.items ?? []
seasons.forEach { season in
self.seasonsEpisodes[season] = []
if season.id == self.episodeItemViewModel.item.seasonId ?? "" {
self.selectedSeason = season
}
}
}
.store(in: &cancellables)
}
private func retrieveEpisodesForSeason(_ season: BaseItemDto) {
guard let seasonID = season.id else { return }
TvShowsAPI.getEpisodes(seriesId: episodeItemViewModel.item.seriesId ?? "",
userId: SessionManager.main.currentLogin.user.id,
fields: [.primaryImageAspectRatio, .seriesPrimaryImage, .seasonUserData, .overview, .genres, .people],
seasonId: seasonID)
.trackActivity(loading)
.sink { completion in
self.handleAPIRequestError(completion: completion)
} receiveValue: { episodes in
self.seasonsEpisodes[season] = episodes.items ?? []
}
.store(in: &cancellables)
}
}
final class SingleSeasonEpisodesRowViewModel: ViewModel {
// TODO: Protocol these viewmodels for generalization instead of Season
@ObservedObject var seasonItemViewModel: SeasonItemViewModel
@Published var episodes: [BaseItemDto]
init(seasonItemViewModel: SeasonItemViewModel) {
self.seasonItemViewModel = seasonItemViewModel
self.episodes = seasonItemViewModel.episodes
super.init()
}
// TODO: Protocol these viewmodels for generalization instead of Season
@ObservedObject
var seasonItemViewModel: SeasonItemViewModel
@Published
var episodes: [BaseItemDto]
init(seasonItemViewModel: SeasonItemViewModel) {
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
* 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
*/
// 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 ActivityIndicator
import Combine
@ -14,168 +13,206 @@ import JellyfinAPI
final class HomeViewModel: ViewModel {
@Published var latestAddedItems: [BaseItemDto] = []
@Published var resumeItems: [BaseItemDto] = []
@Published var nextUpItems: [BaseItemDto] = []
@Published var librariesShowRecentlyAddedIDs: [String] = []
@Published var libraries: [BaseItemDto] = []
@Published
var latestAddedItems: [BaseItemDto] = []
@Published
var resumeItems: [BaseItemDto] = []
@Published
var nextUpItems: [BaseItemDto] = []
@Published
var librariesShowRecentlyAddedIDs: [String] = []
@Published
var libraries: [BaseItemDto] = []
// temp
var recentFilterSet: LibraryFilters = LibraryFilters(filters: [], sortOrder: [.descending], sortBy: [.dateAdded])
// temp
var recentFilterSet = LibraryFilters(filters: [], sortOrder: [.descending], sortBy: [.dateAdded])
override init() {
super.init()
refresh()
override init() {
super.init()
refresh()
// Nov. 6, 2021
// 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
let nc = SwiftfinNotificationCenter.main
nc.addObserver(self, selector: #selector(didSignIn), name: SwiftfinNotificationCenter.Keys.didSignIn, object: nil)
nc.addObserver(self, selector: #selector(didSignOut), name: SwiftfinNotificationCenter.Keys.didSignOut, object: nil)
}
// Nov. 6, 2021
// 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
let nc = SwiftfinNotificationCenter.main
nc.addObserver(self, selector: #selector(didSignIn), name: SwiftfinNotificationCenter.Keys.didSignIn, object: nil)
nc.addObserver(self, selector: #selector(didSignOut), name: SwiftfinNotificationCenter.Keys.didSignOut, object: nil)
}
@objc private func didSignIn() {
for cancellable in cancellables {
cancellable.cancel()
}
@objc
private func didSignIn() {
for cancellable in cancellables {
cancellable.cancel()
}
librariesShowRecentlyAddedIDs = []
libraries = []
resumeItems = []
nextUpItems = []
librariesShowRecentlyAddedIDs = []
libraries = []
resumeItems = []
nextUpItems = []
refresh()
}
refresh()
}
@objc private func didSignOut() {
for cancellable in cancellables {
cancellable.cancel()
}
@objc
private func didSignOut() {
for cancellable in cancellables {
cancellable.cancel()
}
cancellables.removeAll()
}
cancellables.removeAll()
}
@objc 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
@objc
func refresh() {
LogManager.shared.log.debug("Refresh called.")
var newLibraries: [BaseItemDto] = []
refreshLibrariesLatest()
refreshLatestAddedItems()
refreshResumeItems()
refreshNextUpItems()
}
response.items!.forEach { item in
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)
}
}
// MARK: Libraries Latest Items
UserAPI.getCurrentUser()
.trackActivity(self.loading)
.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! : []
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 = []
}
for excludeID in excludeIDs {
newLibraries.removeAll { library in
return library.id == excludeID
}
}
self.handleAPIRequestError(completion: completion)
}, receiveValue: { response in
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")
var newLibraries: [BaseItemDto] = []
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")
response.items!.forEach { item in
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)
}
}
self.nextUpItems = response.items ?? []
})
.store(in: &cancellables)
}
UserAPI.getCurrentUser()
.trackActivity(self.loading)
.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
* 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
*/
// 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 Combine
import Foundation
import JellyfinAPI
final class CollectionItemViewModel: ItemViewModel {
@Published var collectionItems: [BaseItemDto] = []
override init(item: BaseItemDto) {
super.init(item: item)
getCollectionItems()
}
private func getCollectionItems() {
ItemsAPI.getItems(userId: SessionManager.main.currentLogin.user.id,
parentId: item.id,
fields: [.primaryImageAspectRatio, .seriesPrimaryImage, .seasonUserData, .overview, .genres, .people])
.trackActivity(loading)
.sink { [weak self] completion in
self?.handleAPIRequestError(completion: completion)
} receiveValue: { [weak self] response in
self?.collectionItems = response.items ?? []
}
.store(in: &cancellables)
}
@Published
var collectionItems: [BaseItemDto] = []
override init(item: BaseItemDto) {
super.init(item: item)
getCollectionItems()
}
private func getCollectionItems() {
ItemsAPI.getItems(userId: SessionManager.main.currentLogin.user.id,
parentId: item.id,
fields: [.primaryImageAspectRatio, .seriesPrimaryImage, .seasonUserData, .overview, .genres, .people])
.trackActivity(loading)
.sink { [weak self] completion in
self?.handleAPIRequestError(completion: completion)
} receiveValue: { [weak self] response in
self?.collectionItems = response.items ?? []
}
.store(in: &cancellables)
}
}

View File

@ -1,11 +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
* file, you can obtain one at https://mozilla.org/MPL/2.0/.
*
* Copyright 2021 Aiden Vigue & Jellyfin Contributors
*/
// 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 Combine
import Foundation
@ -13,34 +12,36 @@ import JellyfinAPI
import Stinsen
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 {
guard let episodeLocator = item.getEpisodeLocator() else { return item.name ?? "" }
return "\(episodeLocator)\n\(item.name ?? "")"
}
@RouterObject
var itemRouter: ItemCoordinator.Router?
@Published
var series: BaseItemDto?
override func shouldDisplayRuntime() -> Bool {
return false
}
override init(item: BaseItemDto) {
super.init(item: item)
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)
}
getEpisodeSeries()
}
override func getItemDisplayName() -> String {
guard let episodeLocator = item.getEpisodeLocator() else { return item.name ?? "" }
return "\(episodeLocator)\n\(item.name ?? "")"
}
override func shouldDisplayRuntime() -> Bool {
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
* 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
*/
// 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 Combine
import Foundation
@ -14,121 +13,130 @@ import UIKit
class ItemViewModel: ViewModel {
@Published var item: BaseItemDto
@Published var playButtonItem: BaseItemDto? {
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?
@Published
var item: BaseItemDto
@Published
var playButtonItem: BaseItemDto? {
didSet {
if let playButtonItem = playButtonItem {
refreshItemVideoPlayerViewModel(for: playButtonItem)
}
}
}
init(item: BaseItemDto) {
self.item = item
@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?
switch item.itemType {
case .episode, .movie:
self.playButtonItem = item
default: ()
}
informationItems = item.createInformationItems()
mediaItems = item.createMediaItems()
init(item: BaseItemDto) {
self.item = item
isFavorited = item.userData?.isFavorite ?? false
isWatched = item.userData?.played ?? false
super.init()
switch item.itemType {
case .episode, .movie:
self.playButtonItem = item
default: ()
}
getSimilarItems()
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)
}
informationItems = item.createInformationItems()
mediaItems = item.createMediaItems()
func playButtonText() -> String {
if let itemProgressString = item.getItemProgressString() {
return itemProgressString
}
return L10n.play
}
isFavorited = item.userData?.isFavorite ?? false
isWatched = item.userData?.played ?? false
super.init()
func getItemDisplayName() -> String {
return item.name ?? ""
}
getSimilarItems()
func shouldDisplayRuntime() -> Bool {
return true
}
refreshItemVideoPlayerViewModel(for: item)
}
func getSimilarItems() {
LibraryAPI.getSimilarItems(itemId: item.id!, userId: SessionManager.main.currentLogin.user.id, limit: 20, fields: [.primaryImageAspectRatio, .seriesPrimaryImage, .seasonUserData, .overview, .genres, .people])
.trackActivity(loading)
.sink(receiveCompletion: { [weak self] completion in
self?.handleAPIRequestError(completion: completion)
}, receiveValue: { [weak self] response in
self?.similarItems = response.items ?? []
})
.store(in: &cancellables)
}
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 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 playButtonText() -> String {
if let itemProgressString = item.getItemProgressString() {
return itemProgressString
}
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)
}
}
return L10n.play
}
func getItemDisplayName() -> String {
item.name ?? ""
}
func shouldDisplayRuntime() -> Bool {
true
}
func getSimilarItems() {
LibraryAPI.getSimilarItems(itemId: item.id!, userId: SessionManager.main.currentLogin.user.id, limit: 20,
fields: [.primaryImageAspectRatio, .seriesPrimaryImage, .seasonUserData, .overview, .genres, .people])
.trackActivity(loading)
.sink(receiveCompletion: { [weak self] completion in
self?.handleAPIRequestError(completion: completion)
}, receiveValue: { [weak self] response in
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
* 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
*/
// 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 Combine
import Foundation
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
* 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
*/
// 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 Combine
import Foundation
@ -13,76 +12,79 @@ import JellyfinAPI
import Stinsen
final class SeasonItemViewModel: ItemViewModel {
@RouterObject var itemRouter: ItemCoordinator.Router?
@Published var episodes: [BaseItemDto] = []
@Published var seriesItem: BaseItemDto?
override init(item: BaseItemDto) {
super.init(item: item)
@RouterObject
var itemRouter: ItemCoordinator.Router?
@Published
var episodes: [BaseItemDto] = []
@Published
var seriesItem: BaseItemDto?
getSeriesItem()
requestEpisodes()
}
override init(item: BaseItemDto) {
super.init(item: item)
override func playButtonText() -> String {
guard let playButtonItem = playButtonItem else { return L10n.play }
guard let episodeLocator = playButtonItem.getEpisodeLocator() else { return L10n.play }
return episodeLocator
}
getSeriesItem()
requestEpisodes()
}
private func requestEpisodes() {
LogManager.shared.log
.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")
override func playButtonText() -> String {
guard let playButtonItem = playButtonItem else { return L10n.play }
guard let episodeLocator = playButtonItem.getEpisodeLocator() else { return L10n.play }
return episodeLocator
}
self?.setNextUpInSeason()
})
.store(in: &cancellables)
}
private func requestEpisodes() {
LogManager.shared.log
.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
// 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 }
self?.setNextUpInSeason()
})
.store(in: &cancellables)
}
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 {
guard let played = episode.userData?.played else { continue }
if !played {
firstUnwatchedSearch = episode
break
}
}
var firstUnwatchedSearch: BaseItemDto?
if let firstUnwatched = firstUnwatchedSearch {
playButtonItem = firstUnwatched
} else {
guard let firstEpisode = episodes.first else { return }
playButtonItem = firstEpisode
}
}
private func getSeriesItem() {
guard let seriesID = item.seriesId else { return }
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)
}
for episode in episodes {
guard let played = episode.userData?.played else { continue }
if !played {
firstUnwatchedSearch = episode
break
}
}
if let firstUnwatched = firstUnwatchedSearch {
playButtonItem = firstUnwatched
} else {
guard let firstEpisode = episodes.first else { return }
playButtonItem = firstEpisode
}
}
private func getSeriesItem() {
guard let seriesID = item.seriesId else { return }
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
* 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
*/
// 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 Combine
import Foundation
@ -13,68 +12,73 @@ import JellyfinAPI
final class SeriesItemViewModel: ItemViewModel {
@Published var seasons: [BaseItemDto] = []
@Published
var seasons: [BaseItemDto] = []
override init(item: BaseItemDto) {
super.init(item: item)
override init(item: BaseItemDto) {
super.init(item: item)
requestSeasons()
getNextUp()
}
requestSeasons()
getNextUp()
}
override func playButtonText() -> String {
guard let playButtonItem = playButtonItem else { return L10n.play }
guard let episodeLocator = playButtonItem.getEpisodeLocator() else { return L10n.play }
return episodeLocator
}
override func playButtonText() -> String {
guard let playButtonItem = playButtonItem else { return L10n.play }
guard let episodeLocator = playButtonItem.getEpisodeLocator() else { return L10n.play }
return episodeLocator
}
override func shouldDisplayRuntime() -> Bool {
return false
}
override func shouldDisplayRuntime() -> Bool {
false
}
private func getNextUp() {
private func getNextUp() {
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)
.trackActivity(loading)
.sink(receiveCompletion: { [weak self] completion in
self?.handleAPIRequestError(completion: completion)
}, receiveValue: { [weak self] response in
if let nextUpItem = response.items?.first {
self?.playButtonItem = nextUpItem
}
})
.store(in: &cancellables)
}
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)
.trackActivity(loading)
.sink(receiveCompletion: { [weak self] completion in
self?.handleAPIRequestError(completion: completion)
}, receiveValue: { [weak self] response in
if let nextUpItem = response.items?.first {
self?.playButtonItem = nextUpItem
}
})
.store(in: &cancellables)
}
private func getRunYears() -> String {
let dateFormatter = DateFormatter()
dateFormatter.dateFormat = "yyyy"
private func getRunYears() -> String {
let dateFormatter = DateFormatter()
dateFormatter.dateFormat = "yyyy"
var startYear: String?
var endYear: String?
var startYear: String?
var endYear: String?
if item.premiereDate != nil {
startYear = dateFormatter.string(from: item.premiereDate!)
}
if item.premiereDate != nil {
startYear = dateFormatter.string(from: item.premiereDate!)
}
if item.endDate != nil {
endYear = dateFormatter.string(from: item.endDate!)
}
if item.endDate != nil {
endYear = dateFormatter.string(from: item.endDate!)
}
return "\(startYear ?? "Unknown") - \(endYear ?? "Present")"
}
return "\(startYear ?? "Unknown") - \(endYear ?? "Present")"
}
private func requestSeasons() {
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)
.trackActivity(loading)
.sink(receiveCompletion: { [weak self] completion in
self?.handleAPIRequestError(completion: completion)
}, receiveValue: { [weak self] response in
self?.seasons = response.items ?? []
LogManager.shared.log.debug("Retrieved \(String(self?.seasons.count ?? 0)) seasons")
})
.store(in: &cancellables)
}
private func requestSeasons() {
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)
.trackActivity(loading)
.sink(receiveCompletion: { [weak self] completion in
self?.handleAPIRequestError(completion: completion)
}, receiveValue: { [weak self] response in
self?.seasons = response.items ?? []
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
* 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
*/
// 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 Combine
import Foundation
@ -13,38 +12,39 @@ import JellyfinAPI
final class LatestMediaViewModel: ViewModel {
@Published var items = [BaseItemDto]()
let library: BaseItemDto
@Published
var items = [BaseItemDto]()
init(library: BaseItemDto) {
self.library = library
super.init()
let library: BaseItemDto
requestLatestMedia()
}
init(library: BaseItemDto) {
self.library = library
super.init()
func 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 ?? "",
fields: [
.primaryImageAspectRatio,
.seriesPrimaryImage,
.seasonUserData,
.overview,
.genres,
.people
],
includeItemTypes: ["Series", "Movie"],
enableUserData: true, limit: 12)
.trackActivity(loading)
.sink(receiveCompletion: { [weak self] completion in
self?.handleAPIRequestError(completion: completion)
}, receiveValue: { [weak self] response in
self?.items = response
LogManager.shared.log.debug("Retrieved \(String(self?.items.count ?? 0)) items")
})
.store(in: &cancellables)
}
requestLatestMedia()
}
func 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 ?? "",
fields: [
.primaryImageAspectRatio,
.seriesPrimaryImage,
.seasonUserData,
.overview,
.genres,
.people,
],
includeItemTypes: ["Series", "Movie"],
enableUserData: true, limit: 12)
.trackActivity(loading)
.sink(receiveCompletion: { [weak self] completion in
self?.handleAPIRequestError(completion: completion)
}, receiveValue: { [weak self] response in
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
* 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
*/
// 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 Combine
import Foundation
import JellyfinAPI
enum FilterType {
case tag
case genre
case sortOrder
case sortBy
case filter
case tag
case genre
case sortOrder
case sortBy
case filter
}
final class LibraryFilterViewModel: ViewModel {
@Published var modifiedFilters = LibraryFilters()
@Published
var modifiedFilters = LibraryFilters()
@Published var possibleGenres = [NameGuidPair]()
@Published var possibleTags = [String]()
@Published var possibleSortOrders = APISortOrder.allCases
@Published var possibleSortBys = SortBy.allCases
@Published var possibleItemFilters = ItemFilter.supportedTypes
@Published var enabledFilterType: [FilterType]
@Published var selectedSortOrder: APISortOrder = .descending
@Published var selectedSortBy: SortBy = .name
@Published
var possibleGenres = [NameGuidPair]()
@Published
var possibleTags = [String]()
@Published
var possibleSortOrders = APISortOrder.allCases
@Published
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() {
modifiedFilters.sortOrder = [selectedSortOrder]
modifiedFilters.sortBy = [selectedSortBy]
}
func updateModifiedFilter() {
modifiedFilters.sortOrder = [selectedSortOrder]
modifiedFilters.sortBy = [selectedSortBy]
}
func resetFilters() {
modifiedFilters = LibraryFilters(filters: [], sortOrder: [.ascending], withGenres: [], tags: [], sortBy: [.name])
}
func resetFilters() {
modifiedFilters = LibraryFilters(filters: [], sortOrder: [.ascending], withGenres: [], tags: [], sortBy: [.name])
}
init(filters: LibraryFilters? = nil,
enabledFilterType: [FilterType] = [.tag, .genre, .sortBy, .sortOrder, .filter], parentId: String) {
self.enabledFilterType = enabledFilterType
self.selectedSortBy = filters?.sortBy.first ?? .name
self.selectedSortOrder = filters?.sortOrder.first ?? .descending
self.parentId = parentId
init(filters: LibraryFilters? = nil,
enabledFilterType: [FilterType] = [.tag, .genre, .sortBy, .sortOrder, .filter], parentId: String)
{
self.enabledFilterType = enabledFilterType
self.selectedSortBy = filters?.sortBy.first ?? .name
self.selectedSortOrder = filters?.sortOrder.first ?? .descending
self.parentId = parentId
super.init()
if let filters = filters {
self.modifiedFilters = filters
}
requestQueryFilters()
}
super.init()
if let filters = filters {
self.modifiedFilters = filters
}
requestQueryFilters()
}
func requestQueryFilters() {
FilterAPI.getQueryFilters(userId: SessionManager.main.currentLogin.user.id, parentId: self.parentId)
.trackActivity(loading)
.sink(receiveCompletion: { [weak self] completion in
self?.handleAPIRequestError(completion: completion)
}, receiveValue: { [weak self] queryFilters in
guard let self = self else { return }
self.possibleGenres = queryFilters.genres ?? []
self.possibleTags = queryFilters.tags ?? []
})
.store(in: &cancellables)
}
func requestQueryFilters() {
FilterAPI.getQueryFilters(userId: SessionManager.main.currentLogin.user.id, parentId: self.parentId)
.trackActivity(loading)
.sink(receiveCompletion: { [weak self] completion in
self?.handleAPIRequestError(completion: completion)
}, receiveValue: { [weak self] queryFilters in
guard let self = self else { return }
self.possibleGenres = queryFilters.genres ?? []
self.possibleTags = queryFilters.tags ?? []
})
.store(in: &cancellables)
}
}

View File

@ -1,36 +1,36 @@
//
/*
* 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 2021 Aiden Vigue & Jellyfin Contributors
*/
// 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
import JellyfinAPI
final class LibraryListViewModel: ViewModel {
@Published var libraries: [BaseItemDto] = []
@Published
var libraries: [BaseItemDto] = []
// temp
var withFavorites = LibraryFilters(filters: [.isFavorite], sortOrder: [], withGenres: [], sortBy: [])
// temp
var withFavorites = LibraryFilters(filters: [.isFavorite], sortOrder: [], withGenres: [], sortBy: [])
override init() {
super.init()
override init() {
super.init()
requestLibraries()
}
requestLibraries()
}
func requestLibraries() {
UserViewsAPI.getUserViews(userId: SessionManager.main.currentLogin.user.id)
.trackActivity(loading)
.sink(receiveCompletion: { completion in
self.handleAPIRequestError(completion: completion)
}, receiveValue: { response in
self.libraries = response.items ?? []
})
.store(in: &cancellables)
}
func requestLibraries() {
UserViewsAPI.getUserViews(userId: SessionManager.main.currentLogin.user.id)
.trackActivity(loading)
.sink(receiveCompletion: { completion in
self.handleAPIRequestError(completion: completion)
}, receiveValue: { response in
self.libraries = response.items ?? []
})
.store(in: &cancellables)
}
}

View File

@ -1,11 +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
* file, you can obtain one at https://mozilla.org/MPL/2.0/.
*
* Copyright 2021 Aiden Vigue & Jellyfin Contributors
*/
// 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 Combine
import CombineExt
@ -15,134 +14,140 @@ import SwiftUI
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 var showItems = [BaseItemDto]()
@Published var episodeItems = [BaseItemDto]()
@Published
var movieItems = [BaseItemDto]()
@Published
var showItems = [BaseItemDto]()
@Published
var episodeItems = [BaseItemDto]()
@Published var suggestions = [BaseItemDto]()
@Published
var suggestions = [BaseItemDto]()
var searchQuerySubject = CurrentValueSubject<String, Never>("")
var parentID: String?
var searchQuerySubject = CurrentValueSubject<String, Never>("")
var parentID: String?
init(parentID: String?) {
self.parentID = parentID
super.init()
init(parentID: String?) {
self.parentID = parentID
super.init()
searchQuerySubject
.filter { !$0.isEmpty }
.debounce(for: 0.25, scheduler: DispatchQueue.main)
.sink(receiveValue: search)
.store(in: &cancellables)
setupPublishersForSupportedItemType()
searchQuerySubject
.filter { !$0.isEmpty }
.debounce(for: 0.25, scheduler: DispatchQueue.main)
.sink(receiveValue: search)
.store(in: &cancellables)
setupPublishersForSupportedItemType()
requestSuggestions()
}
requestSuggestions()
}
func setupPublishersForSupportedItemType() {
Publishers.CombineLatest3($movieItems, $showItems, $episodeItems)
.debounce(for: 0.25, scheduler: DispatchQueue.main)
.map { arg -> [ItemType] in
var typeList = [ItemType]()
if !arg.0.isEmpty {
typeList.append(.movie)
}
if !arg.1.isEmpty {
typeList.append(.series)
}
if !arg.2.isEmpty {
typeList.append(.episode)
}
return typeList
}
.receive(on: DispatchQueue.main)
.sink(receiveValue: { [weak self] typeList in
withAnimation {
self?.supportedItemTypeList = typeList
}
})
.store(in: &cancellables)
func setupPublishersForSupportedItemType() {
Publishers.CombineLatest3($movieItems, $showItems, $episodeItems)
.debounce(for: 0.25, scheduler: DispatchQueue.main)
.map { arg -> [ItemType] in
var typeList = [ItemType]()
if !arg.0.isEmpty {
typeList.append(.movie)
}
if !arg.1.isEmpty {
typeList.append(.series)
}
if !arg.2.isEmpty {
typeList.append(.episode)
}
return typeList
}
.receive(on: DispatchQueue.main)
.sink(receiveValue: { [weak self] typeList in
withAnimation {
self?.supportedItemTypeList = typeList
}
})
.store(in: &cancellables)
$supportedItemTypeList
.receive(on: DispatchQueue.main)
.withLatestFrom($selectedItemType)
.compactMap { selectedItemType in
if self.supportedItemTypeList.contains(selectedItemType) {
return selectedItemType
} else {
return self.supportedItemTypeList.first
}
}
.sink(receiveValue: { [weak self] itemType in
withAnimation {
self?.selectedItemType = itemType
}
})
.store(in: &cancellables)
}
$supportedItemTypeList
.receive(on: DispatchQueue.main)
.withLatestFrom($selectedItemType)
.compactMap { selectedItemType in
if self.supportedItemTypeList.contains(selectedItemType) {
return selectedItemType
} else {
return self.supportedItemTypeList.first
}
}
.sink(receiveValue: { [weak self] itemType in
withAnimation {
self?.selectedItemType = itemType
}
})
.store(in: &cancellables)
}
func requestSuggestions() {
ItemsAPI.getItemsByUserId(userId: SessionManager.main.currentLogin.user.id,
limit: 20,
recursive: true,
parentId: parentID,
includeItemTypes: ["Movie", "Series"],
sortBy: ["IsFavoriteOrLiked", "Random"],
imageTypeLimit: 0,
enableTotalRecordCount: false,
enableImages: false)
.trackActivity(loading)
.receive(on: DispatchQueue.main)
.sink(receiveCompletion: { [weak self] completion in
self?.handleAPIRequestError(completion: completion)
}, receiveValue: { [weak self] response in
self?.suggestions = response.items ?? []
})
.store(in: &cancellables)
}
func requestSuggestions() {
ItemsAPI.getItemsByUserId(userId: SessionManager.main.currentLogin.user.id,
limit: 20,
recursive: true,
parentId: parentID,
includeItemTypes: ["Movie", "Series"],
sortBy: ["IsFavoriteOrLiked", "Random"],
imageTypeLimit: 0,
enableTotalRecordCount: false,
enableImages: false)
.trackActivity(loading)
.receive(on: DispatchQueue.main)
.sink(receiveCompletion: { [weak self] completion in
self?.handleAPIRequestError(completion: completion)
}, receiveValue: { [weak self] response in
self?.suggestions = response.items ?? []
})
.store(in: &cancellables)
}
func search(with query: String) {
ItemsAPI.getItemsByUserId(userId: SessionManager.main.currentLogin.user.id, limit: 50, recursive: true, searchTerm: query,
sortOrder: [.ascending], parentId: parentID,
fields: [.primaryImageAspectRatio, .seriesPrimaryImage, .seasonUserData, .overview, .genres, .people],
includeItemTypes: [ItemType.movie.rawValue], sortBy: ["SortName"], enableUserData: true,
enableImages: true)
.trackActivity(loading)
.receive(on: DispatchQueue.main)
.sink(receiveCompletion: { [weak self] completion in
self?.handleAPIRequestError(completion: completion)
}, receiveValue: { [weak self] response in
self?.movieItems = response.items ?? []
})
.store(in: &cancellables)
ItemsAPI.getItemsByUserId(userId: SessionManager.main.currentLogin.user.id, limit: 50, recursive: true, searchTerm: query,
sortOrder: [.ascending], parentId: parentID,
fields: [.primaryImageAspectRatio, .seriesPrimaryImage, .seasonUserData, .overview, .genres, .people],
includeItemTypes: [ItemType.series.rawValue], sortBy: ["SortName"], enableUserData: true,
enableImages: true)
.trackActivity(loading)
.receive(on: DispatchQueue.main)
.sink(receiveCompletion: { [weak self] completion in
self?.handleAPIRequestError(completion: completion)
}, receiveValue: { [weak self] response in
self?.showItems = response.items ?? []
})
.store(in: &cancellables)
ItemsAPI.getItemsByUserId(userId: SessionManager.main.currentLogin.user.id, limit: 50, recursive: true, searchTerm: query,
sortOrder: [.ascending], parentId: parentID,
fields: [.primaryImageAspectRatio, .seriesPrimaryImage, .seasonUserData, .overview, .genres, .people],
includeItemTypes: [ItemType.episode.rawValue], sortBy: ["SortName"], enableUserData: true,
enableImages: true)
.trackActivity(loading)
.receive(on: DispatchQueue.main)
.sink(receiveCompletion: { [weak self] completion in
self?.handleAPIRequestError(completion: completion)
}, receiveValue: { [weak self] response in
self?.episodeItems = response.items ?? []
})
.store(in: &cancellables)
}
func search(with query: String) {
ItemsAPI.getItemsByUserId(userId: SessionManager.main.currentLogin.user.id, limit: 50, recursive: true, searchTerm: query,
sortOrder: [.ascending], parentId: parentID,
fields: [.primaryImageAspectRatio, .seriesPrimaryImage, .seasonUserData, .overview, .genres, .people],
includeItemTypes: [ItemType.movie.rawValue], sortBy: ["SortName"], enableUserData: true,
enableImages: true)
.trackActivity(loading)
.receive(on: DispatchQueue.main)
.sink(receiveCompletion: { [weak self] completion in
self?.handleAPIRequestError(completion: completion)
}, receiveValue: { [weak self] response in
self?.movieItems = response.items ?? []
})
.store(in: &cancellables)
ItemsAPI.getItemsByUserId(userId: SessionManager.main.currentLogin.user.id, limit: 50, recursive: true, searchTerm: query,
sortOrder: [.ascending], parentId: parentID,
fields: [.primaryImageAspectRatio, .seriesPrimaryImage, .seasonUserData, .overview, .genres, .people],
includeItemTypes: [ItemType.series.rawValue], sortBy: ["SortName"], enableUserData: true,
enableImages: true)
.trackActivity(loading)
.receive(on: DispatchQueue.main)
.sink(receiveCompletion: { [weak self] completion in
self?.handleAPIRequestError(completion: completion)
}, receiveValue: { [weak self] response in
self?.showItems = response.items ?? []
})
.store(in: &cancellables)
ItemsAPI.getItemsByUserId(userId: SessionManager.main.currentLogin.user.id, limit: 50, recursive: true, searchTerm: query,
sortOrder: [.ascending], parentId: parentID,
fields: [.primaryImageAspectRatio, .seriesPrimaryImage, .seasonUserData, .overview, .genres, .people],
includeItemTypes: [ItemType.episode.rawValue], sortBy: ["SortName"], enableUserData: true,
enableImages: true)
.trackActivity(loading)
.receive(on: DispatchQueue.main)
.sink(receiveCompletion: { [weak self] completion in
self?.handleAPIRequestError(completion: completion)
}, receiveValue: { [weak self] response in
self?.episodeItems = response.items ?? []
})
.store(in: &cancellables)
}
}

View File

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

View File

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

View File

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

View File

@ -1,30 +1,33 @@
//
/*
* 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 2021 Aiden Vigue & Jellyfin Contributors
*/
// 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
import JellyfinAPI
final class MainTabViewModel: ViewModel {
@Published var backgroundURL: URL?
@Published var lastBackgroundURL: URL?
@Published var backgroundBlurHash: String = "001fC^"
@Published
var backgroundURL: URL?
@Published
var lastBackgroundURL: URL?
@Published
var backgroundBlurHash: String = "001fC^"
override init() {
super.init()
override init() {
super.init()
let nc = NotificationCenter.default
nc.addObserver(self, selector: #selector(backgroundDidChange), name: Notification.Name("backgroundDidChange"), object: nil)
}
let nc = NotificationCenter.default
nc.addObserver(self, selector: #selector(backgroundDidChange), name: Notification.Name("backgroundDidChange"), object: nil)
}
@objc func backgroundDidChange() {
self.lastBackgroundURL = self.backgroundURL
self.backgroundURL = BackgroundManager.current.backgroundURL
self.backgroundBlurHash = BackgroundManager.current.blurhash
}
@objc
func backgroundDidChange() {
self.lastBackgroundURL = self.backgroundURL
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
* 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
*/
// 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 Combine
import Foundation
@ -15,81 +14,79 @@ import SwiftUICollection
final class MovieLibrariesViewModel: ViewModel {
@Published var rows = [LibraryRow]()
@Published var totalPages = 0
@Published var currentPage = 0
@Published var hasNextPage = false
@Published var hasPreviousPage = false
@Published
var rows = [LibraryRow]()
@Published
var totalPages = 0
@Published
var currentPage = 0
@Published
var hasNextPage = false
@Published
var hasPreviousPage = false
private var libraries = [BaseItemDto]()
private let columns: Int
private var libraries = [BaseItemDto]()
private let columns: Int
@RouterObject
var router: MovieLibrariesCoordinator.Router?
@RouterObject
var router: MovieLibrariesCoordinator.Router?
init(
columns: Int = 7
) {
self.columns = columns
super.init()
init(columns: Int = 7) {
self.columns = columns
super.init()
requestLibraries()
}
requestLibraries()
}
func requestLibraries() {
func requestLibraries() {
UserViewsAPI.getUserViews(
userId: SessionManager.main.currentLogin.user.id)
.trackActivity(loading)
.sink(receiveCompletion: { completion in
self.handleAPIRequestError(completion: completion)
}, receiveValue: { response in
if let responseItems = response.items {
self.libraries = []
for library in responseItems {
if library.collectionType == "movies" {
self.libraries.append(library)
}
}
self.rows = self.calculateRows()
if self.libraries.count == 1, let library = self.libraries.first {
// show library
self.router?.route(to: \.library, library)
}
}
})
.store(in: &cancellables)
}
UserViewsAPI.getUserViews(userId: SessionManager.main.currentLogin.user.id)
.trackActivity(loading)
.sink(receiveCompletion: { completion in
self.handleAPIRequestError(completion: completion)
}, receiveValue: { response in
if let responseItems = response.items {
self.libraries = []
for library in responseItems {
if library.collectionType == "movies" {
self.libraries.append(library)
}
}
self.rows = self.calculateRows()
if self.libraries.count == 1, let library = self.libraries.first {
// show library
self.router?.route(to: \.library, library)
}
}
})
.store(in: &cancellables)
}
private func calculateRows() -> [LibraryRow] {
guard libraries.count > 0 else { return [] }
let rowCount = libraries.count / columns
var calculatedRows = [LibraryRow]()
for i in (0...rowCount) {
let firstItemIndex = i * columns
var lastItemIndex = firstItemIndex + columns
if lastItemIndex > libraries.count {
lastItemIndex = libraries.count
}
private func calculateRows() -> [LibraryRow] {
guard !libraries.isEmpty else { return [] }
let rowCount = libraries.count / columns
var calculatedRows = [LibraryRow]()
for i in 0 ... rowCount {
let firstItemIndex = i * columns
var lastItemIndex = firstItemIndex + columns
if lastItemIndex > libraries.count {
lastItemIndex = libraries.count
}
var rowCells = [LibraryRowCell]()
for item in libraries[firstItemIndex..<lastItemIndex] {
let newCell = LibraryRowCell(item: item)
rowCells.append(newCell)
}
if i == rowCount && hasNextPage {
var loadingCell = LibraryRowCell(item: nil)
loadingCell.loadingCell = true
rowCells.append(loadingCell)
}
var rowCells = [LibraryRowCell]()
for item in libraries[firstItemIndex ..< lastItemIndex] {
let newCell = LibraryRowCell(item: item)
rowCells.append(newCell)
}
if i == rowCount && hasNextPage {
var loadingCell = LibraryRowCell(item: nil)
loadingCell.loadingCell = true
rowCells.append(loadingCell)
}
calculatedRows.append(
LibraryRow(
section: i,
items: rowCells
)
)
}
return calculatedRows
}
calculatedRows.append(LibraryRow(section: i,
items: rowCells))
}
return calculatedRows
}
}

View File

@ -1,33 +1,33 @@
//
/*
* 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 2021 Aiden Vigue & Jellyfin Contributors
*/
// 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
import JellyfinAPI
class ServerDetailViewModel: ViewModel {
@Published var server: SwiftfinStore.State.Server
@Published
var server: SwiftfinStore.State.Server
init(server: SwiftfinStore.State.Server) {
self.server = server
}
init(server: SwiftfinStore.State.Server) {
self.server = server
}
func setServerCurrentURI(uri: String) {
SessionManager.main.setServerCurrentURI(server: server, uri: uri)
.sink { c in
print(c)
} receiveValue: { newServerState in
self.server = newServerState
func setServerCurrentURI(uri: String) {
SessionManager.main.setServerCurrentURI(server: server, uri: uri)
.sink { c in
print(c)
} receiveValue: { newServerState in
self.server = newServerState
let nc = SwiftfinNotificationCenter.main
nc.post(name: SwiftfinNotificationCenter.Keys.didChangeServerCurrentURI, object: newServerState)
}
.store(in: &cancellables)
}
let nc = SwiftfinNotificationCenter.main
nc.post(name: SwiftfinNotificationCenter.Keys.didChangeServerCurrentURI, object: newServerState)
}
.store(in: &cancellables)
}
}

View File

@ -1,47 +1,48 @@
//
/*
* 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 2021 Aiden Vigue & Jellyfin Contributors
*/
// 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
import SwiftUI
class ServerListViewModel: ObservableObject {
@Published var servers: [SwiftfinStore.State.Server] = []
@Published
var servers: [SwiftfinStore.State.Server] = []
init() {
init() {
// Oct. 15, 2021
// 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
// Go to each MainCoordinator and implement the rebuild of the root when receiving the notification
let nc = SwiftfinNotificationCenter.main
nc.addObserver(self, selector: #selector(didPurge), name: SwiftfinNotificationCenter.Keys.didPurge, object: nil)
}
// Oct. 15, 2021
// 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
// Go to each MainCoordinator and implement the rebuild of the root when receiving the notification
let nc = SwiftfinNotificationCenter.main
nc.addObserver(self, selector: #selector(didPurge), name: SwiftfinNotificationCenter.Keys.didPurge, object: nil)
}
func fetchServers() {
self.servers = SessionManager.main.fetchServers()
}
func fetchServers() {
self.servers = SessionManager.main.fetchServers()
}
func userTextFor(server: SwiftfinStore.State.Server) -> String {
if server.userIDs.count == 1 {
return "1 user"
} else {
return "\(server.userIDs.count) users"
}
}
func userTextFor(server: SwiftfinStore.State.Server) -> String {
if server.userIDs.count == 1 {
return "1 user"
} else {
return "\(server.userIDs.count) users"
}
}
func remove(server: SwiftfinStore.State.Server) {
SessionManager.main.delete(server: server)
fetchServers()
}
func remove(server: SwiftfinStore.State.Server) {
SessionManager.main.delete(server: server)
fetchServers()
}
@objc private func didPurge() {
fetchServers()
}
@objc
private func didPurge() {
fetchServers()
}
}

View File

@ -1,48 +1,47 @@
//
/*
* 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 2021 Aiden Vigue & Jellyfin Contributors
*/
// 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 Defaults
import Foundation
import SwiftUI
import Defaults
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) {
self.server = server
self.user = user
// Bitrates
let url = Bundle.main.url(forResource: "bitrates", withExtension: "json")!
var bitrates: [Bitrates] = []
var langs: [TrackLanguage] = []
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`")
}
let server: SwiftfinStore.State.Server
let user: SwiftfinStore.State.User
// 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)
}
init(server: SwiftfinStore.State.Server, user: SwiftfinStore.State.User) {
self.server = server
self.user = user
// 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
* 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
*/
// 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 Combine
import Foundation
@ -15,81 +14,79 @@ import SwiftUICollection
final class TVLibrariesViewModel: ViewModel {
@Published var rows = [LibraryRow]()
@Published var totalPages = 0
@Published var currentPage = 0
@Published var hasNextPage = false
@Published var hasPreviousPage = false
@Published
var rows = [LibraryRow]()
@Published
var totalPages = 0
@Published
var currentPage = 0
@Published
var hasNextPage = false
@Published
var hasPreviousPage = false
private var libraries = [BaseItemDto]()
private let columns: Int
private var libraries = [BaseItemDto]()
private let columns: Int
@RouterObject
var router: TVLibrariesCoordinator.Router?
@RouterObject
var router: TVLibrariesCoordinator.Router?
init(
columns: Int = 7
) {
self.columns = columns
super.init()
init(columns: Int = 7) {
self.columns = columns
super.init()
requestLibraries()
}
requestLibraries()
}
func requestLibraries() {
func requestLibraries() {
UserViewsAPI.getUserViews(
userId: SessionManager.main.currentLogin.user.id)
.trackActivity(loading)
.sink(receiveCompletion: { completion in
self.handleAPIRequestError(completion: completion)
}, receiveValue: { response in
if let responseItems = response.items {
self.libraries = []
for library in responseItems {
if library.collectionType == "tvshows" {
self.libraries.append(library)
}
}
self.rows = self.calculateRows()
if self.libraries.count == 1, let library = self.libraries.first {
// show library
self.router?.route(to: \.library, library)
}
}
})
.store(in: &cancellables)
}
UserViewsAPI.getUserViews(userId: SessionManager.main.currentLogin.user.id)
.trackActivity(loading)
.sink(receiveCompletion: { completion in
self.handleAPIRequestError(completion: completion)
}, receiveValue: { response in
if let responseItems = response.items {
self.libraries = []
for library in responseItems {
if library.collectionType == "tvshows" {
self.libraries.append(library)
}
}
self.rows = self.calculateRows()
if self.libraries.count == 1, let library = self.libraries.first {
// show library
self.router?.route(to: \.library, library)
}
}
})
.store(in: &cancellables)
}
private func calculateRows() -> [LibraryRow] {
guard libraries.count > 0 else { return [] }
let rowCount = libraries.count / columns
var calculatedRows = [LibraryRow]()
for i in (0...rowCount) {
let firstItemIndex = i * columns
var lastItemIndex = firstItemIndex + columns
if lastItemIndex > libraries.count {
lastItemIndex = libraries.count
}
private func calculateRows() -> [LibraryRow] {
guard !libraries.isEmpty else { return [] }
let rowCount = libraries.count / columns
var calculatedRows = [LibraryRow]()
for i in 0 ... rowCount {
let firstItemIndex = i * columns
var lastItemIndex = firstItemIndex + columns
if lastItemIndex > libraries.count {
lastItemIndex = libraries.count
}
var rowCells = [LibraryRowCell]()
for item in libraries[firstItemIndex..<lastItemIndex] {
let newCell = LibraryRowCell(item: item)
rowCells.append(newCell)
}
if i == rowCount && hasNextPage {
var loadingCell = LibraryRowCell(item: nil)
loadingCell.loadingCell = true
rowCells.append(loadingCell)
}
var rowCells = [LibraryRowCell]()
for item in libraries[firstItemIndex ..< lastItemIndex] {
let newCell = LibraryRowCell(item: item)
rowCells.append(newCell)
}
if i == rowCount && hasNextPage {
var loadingCell = LibraryRowCell(item: nil)
loadingCell.loadingCell = true
rowCells.append(loadingCell)
}
calculatedRows.append(
LibraryRow(
section: i,
items: rowCells
)
)
}
return calculatedRows
}
calculatedRows.append(LibraryRow(section: i,
items: rowCells))
}
return calculatedRows
}
}

View File

@ -1,47 +1,48 @@
//
/*
* 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 2021 Aiden Vigue & Jellyfin Contributors
*/
// 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
import SwiftUI
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) {
self.server = server
init(server: SwiftfinStore.State.Server) {
self.server = server
super.init()
super.init()
let nc = SwiftfinNotificationCenter.main
nc.addObserver(self, selector: #selector(didChangeCurrentLoginURI), name: SwiftfinNotificationCenter.Keys.didChangeServerCurrentURI, object: nil)
}
let nc = SwiftfinNotificationCenter.main
nc.addObserver(self, selector: #selector(didChangeCurrentLoginURI), name: SwiftfinNotificationCenter.Keys.didChangeServerCurrentURI,
object: nil)
}
@objc func didChangeCurrentLoginURI(_ notification: Notification) {
guard let newServerState = notification.object as? SwiftfinStore.State.Server else { fatalError("Need to have new state server") }
self.server = newServerState
}
@objc
func didChangeCurrentLoginURI(_ notification: Notification) {
guard let newServerState = notification.object as? SwiftfinStore.State.Server else { fatalError("Need to have new state server") }
self.server = newServerState
}
func fetchUsers() {
self.users = SessionManager.main.fetchUsers(for: server)
}
func fetchUsers() {
self.users = SessionManager.main.fetchUsers(for: server)
}
func login(user: SwiftfinStore.State.User) {
self.isLoading = true
SessionManager.main.loginUser(server: server, user: user)
}
func remove(user: SwiftfinStore.State.User) {
SessionManager.main.delete(user: user)
fetchUsers()
}
func login(user: SwiftfinStore.State.User) {
self.isLoading = true
SessionManager.main.loginUser(server: server, user: user)
}
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
* 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
*/
// 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 CoreStore
import Foundation
@ -13,42 +12,42 @@ import Stinsen
final class UserSignInViewModel: ViewModel {
@RouterObject var router: UserSignInCoordinator.Router?
let server: SwiftfinStore.State.Server
@RouterObject
var router: UserSignInCoordinator.Router?
let server: SwiftfinStore.State.Server
init(server: SwiftfinStore.State.Server) {
self.server = server
}
init(server: SwiftfinStore.State.Server) {
self.server = server
}
var alertTitle: String {
var message: String = ""
if errorMessage?.code != ErrorMessage.noShowErrorCode {
message.append(contentsOf: "\(errorMessage?.code ?? ErrorMessage.noShowErrorCode)\n")
}
message.append(contentsOf: "\(errorMessage?.title ?? "Unkown Error")")
return message
}
var alertTitle: String {
var message: String = ""
if errorMessage?.code != ErrorMessage.noShowErrorCode {
message.append(contentsOf: "\(errorMessage?.code ?? ErrorMessage.noShowErrorCode)\n")
}
message.append(contentsOf: "\(errorMessage?.title ?? "Unkown Error")")
return message
}
func login(username: String, password: String) {
LogManager.shared.log.debug("Attempting to login to server at \"\(server.currentURI)\"", tag: "login")
LogManager.shared.log.debug("username: \(username), password: \(password)", tag: "login")
func login(username: String, password: String) {
LogManager.shared.log.debug("Attempting to login to server at \"\(server.currentURI)\"", tag: "login")
LogManager.shared.log.debug("username: \(username), password: \(password)", tag: "login")
SessionManager.main.loginUser(server: server, username: username, password: password)
.trackActivity(loading)
.sink { completion in
self.handleAPIRequestError(displayMessage: "Unable to connect to server.", logLevel: .critical, tag: "login",
completion: completion)
} receiveValue: { _ in
SessionManager.main.loginUser(server: server, username: username, password: password)
.trackActivity(loading)
.sink { completion in
self.handleAPIRequestError(displayMessage: "Unable to connect to server.", logLevel: .critical, tag: "login",
completion: completion)
} receiveValue: { _ in
}
.store(in: &cancellables)
}
}
.store(in: &cancellables)
}
func cancelSignIn() {
for cancellable in cancellables {
cancellable.cancel()
}
func cancelSignIn() {
for cancellable in cancellables {
cancellable.cancel()
}
self.isLoading = false
}
self.isLoading = false
}
}

View File

@ -1,31 +1,32 @@
//
/*
* 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 2021 Aiden Vigue & Jellyfin Contributors
*/
// 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 SwiftUI
import JellyfinAPI
import SwiftUI
struct Subtitle {
var name: String
var id: Int32
var url: URL?
var delivery: SubtitleDeliveryMethod
var codec: String
var languageCode: String
var name: String
var id: Int32
var url: URL?
var delivery: SubtitleDeliveryMethod
var codec: String
var languageCode: String
}
struct AudioTrack {
var name: String
var languageCode: String
var id: Int32
var name: String
var languageCode: String
var id: Int32
}
class PlaybackItem: ObservableObject {
@Published var videoType: PlayMethod = .directPlay
@Published var videoUrl: URL = URL(string: "https://example.com")!
@Published
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
* 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
*/
// 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 ServerStreamType {
case direct
case transcode
case direct
case transcode
}

View File

@ -1,75 +1,82 @@
//
/*
* 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 2021 Aiden Vigue & Jellyfin Contributors
*/
// 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 ActivityIndicator
import Combine
import Foundation
import ActivityIndicator
import JellyfinAPI
class ViewModel: ObservableObject {
@Published var isLoading = false
@Published var errorMessage: ErrorMessage?
@Published
var isLoading = false
@Published
var errorMessage: ErrorMessage?
let loading = ActivityIndicator()
var cancellables = Set<AnyCancellable>()
let loading = ActivityIndicator()
var cancellables = Set<AnyCancellable>()
init() {
loading.loading.assign(to: \.isLoading, on: self).store(in: &cancellables)
}
init() {
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>) {
switch completion {
case .finished:
self.errorMessage = nil
case .failure(let error):
let logConstructor = LogConstructor(message: "__NOTHING__", tag: tag, level: logLevel, function: function, file: file, line: line)
func handleAPIRequestError(displayMessage: String? = nil, logLevel: LogLevel = .error, tag: String = "", function: String = #function,
file: String = #file, line: UInt = #line, completion: Subscribers.Completion<Error>)
{
switch completion {
case .finished:
self.errorMessage = nil
case let .failure(error):
let logConstructor = LogConstructor(message: "__NOTHING__", tag: tag, level: logLevel, function: function, file: file,
line: line)
switch error {
case is ErrorResponse:
let networkError: NetworkError
let errorResponse = error as! ErrorResponse
switch errorResponse {
case .error(-1, _, _, _):
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
LogManager.shared.log.error("Request failed: URL request failed with error \(networkError.errorMessage.code): \(errorResponse.localizedDescription)")
case .error(-2, _, _, _):
networkError = .HTTPURLError(response: errorResponse, displayMessage: displayMessage, logConstructor: logConstructor)
LogManager.shared.log.error("Request failed: HTTP URL request failed with description: \(errorResponse.localizedDescription)")
default:
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)")
}
switch error {
case is ErrorResponse:
let networkError: NetworkError
let errorResponse = error as! ErrorResponse
switch errorResponse {
case .error(-1, _, _, _):
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
LogManager.shared.log
.error("Request failed: URL request failed with error \(networkError.errorMessage.code): \(errorResponse.localizedDescription)")
case .error(-2, _, _, _):
networkError = .HTTPURLError(response: errorResponse, displayMessage: displayMessage, logConstructor: logConstructor)
LogManager.shared.log
.error("Request failed: HTTP URL request failed with description: \(errorResponse.localizedDescription)")
default:
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:
let swiftfinError = error as! SwiftfinStore.Errors
let errorMessage = ErrorMessage(code: ErrorMessage.noShowErrorCode,
title: swiftfinError.title,
displayMessage: swiftfinError.errorDescription ?? "",
logConstructor: logConstructor)
self.errorMessage = errorMessage
LogManager.shared.log.error("Request failed: \(swiftfinError.errorDescription ?? "")")
case is SwiftfinStore.Errors:
let swiftfinError = error as! SwiftfinStore.Errors
let errorMessage = ErrorMessage(code: ErrorMessage.noShowErrorCode,
title: swiftfinError.title,
displayMessage: swiftfinError.errorDescription ?? "",
logConstructor: logConstructor)
self.errorMessage = errorMessage
LogManager.shared.log.error("Request failed: \(swiftfinError.errorDescription ?? "")")
default:
let genericErrorMessage = ErrorMessage(code: ErrorMessage.noShowErrorCode,
title: "Generic Error",
displayMessage: error.localizedDescription,
logConstructor: logConstructor)
self.errorMessage = genericErrorMessage
LogManager.shared.log.error("Request failed: Generic error - \(error.localizedDescription)")
}
}
}
default:
let genericErrorMessage = ErrorMessage(code: ErrorMessage.noShowErrorCode,
title: "Generic Error",
displayMessage: error.localizedDescription,
logConstructor: logConstructor)
self.errorMessage = genericErrorMessage
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