mirror of
https://github.com/jellyfin/Swiftfin.git
synced 2024-11-23 05:59:51 +00:00
swiftformat
This commit is contained in:
parent
961e639970
commit
4298062ca3
61
.swiftformat
61
.swiftformat
@ -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
|
@ -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())
|
||||
}
|
||||
}
|
||||
|
@ -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())
|
||||
}
|
||||
}
|
||||
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
@ -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()
|
||||
}
|
||||
}
|
||||
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
@ -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
|
||||
}
|
||||
}
|
||||
|
@ -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))
|
||||
}
|
||||
}
|
||||
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
@ -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")
|
||||
}
|
||||
}
|
||||
|
@ -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()
|
||||
}
|
||||
}
|
||||
|
@ -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")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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())
|
||||
}
|
||||
}
|
||||
|
@ -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()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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()
|
||||
}
|
||||
}
|
||||
|
@ -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")
|
||||
}
|
||||
}
|
||||
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
@ -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())
|
||||
}
|
||||
}
|
||||
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
@ -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()
|
||||
}
|
||||
}
|
||||
|
@ -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()
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -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
|
||||
}
|
||||
}
|
||||
|
@ -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
|
||||
}
|
||||
|
@ -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
|
||||
}
|
||||
}
|
||||
|
@ -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]
|
||||
}
|
||||
}
|
||||
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
@ -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
|
||||
}
|
||||
}
|
||||
|
@ -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)
|
||||
}
|
||||
|
@ -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
|
||||
}
|
||||
}
|
||||
|
@ -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()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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()
|
||||
}
|
||||
}
|
||||
|
@ -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
|
||||
}
|
||||
}
|
||||
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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
|
||||
}
|
||||
}
|
||||
|
@ -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
|
||||
}
|
||||
}
|
||||
|
@ -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 ?? ""
|
||||
}
|
||||
}
|
||||
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
@ -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
|
||||
}
|
||||
}
|
||||
|
@ -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))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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
|
||||
}
|
||||
}
|
||||
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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
|
||||
}
|
||||
|
@ -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
|
||||
}
|
||||
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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))
|
||||
}
|
||||
}
|
||||
|
@ -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
|
||||
}
|
||||
|
@ -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"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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 }
|
||||
}
|
||||
|
41
Shared/Objects/PlaybackSpeed.swift
Normal file
41
Shared/Objects/PlaybackSpeed.swift
Normal 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"
|
||||
}
|
||||
}
|
||||
}
|
@ -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 }
|
||||
}
|
||||
|
@ -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
|
||||
}
|
||||
|
@ -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")
|
||||
}
|
||||
|
@ -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 ""
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
@ -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)"
|
||||
}
|
||||
}
|
||||
|
@ -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
|
||||
}
|
||||
}
|
||||
|
@ -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")
|
||||
}
|
||||
}
|
||||
|
@ -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"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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)
|
||||
}
|
||||
|
@ -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()
|
||||
}
|
||||
}
|
||||
|
@ -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
|
||||
}
|
||||
}
|
||||
|
@ -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()
|
||||
}
|
||||
}
|
||||
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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 {}
|
||||
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
@ -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
|
||||
}
|
||||
}
|
||||
|
@ -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)!
|
||||
}
|
||||
}
|
||||
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
@ -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
|
||||
}
|
||||
}
|
||||
|
@ -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
|
||||
}
|
||||
}
|
||||
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
@ -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()
|
||||
}
|
||||
}
|
||||
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
@ -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
|
||||
}
|
||||
}
|
||||
|
@ -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()
|
||||
}
|
||||
}
|
||||
|
@ -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
|
||||
}
|
||||
}
|
||||
|
@ -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")!
|
||||
}
|
||||
|
@ -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
|
||||
}
|
||||
|
File diff suppressed because it is too large
Load Diff
@ -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
Loading…
Reference in New Issue
Block a user