Merge branch 'main' into multi-server-url

This commit is contained in:
Ethan Pippin 2021-11-06 20:02:39 -06:00
commit eb901da824
33 changed files with 234 additions and 46 deletions

View File

@ -290,6 +290,9 @@
E18845F826DEA9C900B0C5B7 /* ItemViewBody.swift in Sources */ = {isa = PBXBuildFile; fileRef = E18845F726DEA9C900B0C5B7 /* ItemViewBody.swift */; };
E188460026DECB9E00B0C5B7 /* ItemLandscapeTopBarView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E18845FF26DECB9E00B0C5B7 /* ItemLandscapeTopBarView.swift */; };
E188460426DEF04800B0C5B7 /* EpisodeCardVStackView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E188460326DEF04800B0C5B7 /* EpisodeCardVStackView.swift */; };
E19169CE272514760085832A /* HTTPScheme.swift in Sources */ = {isa = PBXBuildFile; fileRef = E19169CD272514760085832A /* HTTPScheme.swift */; };
E19169CF272514760085832A /* HTTPScheme.swift in Sources */ = {isa = PBXBuildFile; fileRef = E19169CD272514760085832A /* HTTPScheme.swift */; };
E19169D0272514760085832A /* HTTPScheme.swift in Sources */ = {isa = PBXBuildFile; fileRef = E19169CD272514760085832A /* HTTPScheme.swift */; };
E193D4D827193CAC00900D82 /* PortraitImageStackable.swift in Sources */ = {isa = PBXBuildFile; fileRef = E193D4D727193CAC00900D82 /* PortraitImageStackable.swift */; };
E193D4D927193CAC00900D82 /* PortraitImageStackable.swift in Sources */ = {isa = PBXBuildFile; fileRef = E193D4D727193CAC00900D82 /* PortraitImageStackable.swift */; };
E193D4DB27193CCA00900D82 /* PillStackable.swift in Sources */ = {isa = PBXBuildFile; fileRef = E193D4DA27193CCA00900D82 /* PillStackable.swift */; };
@ -342,6 +345,7 @@
E1D4BF8C2719F39F00A11E64 /* AppAppearance.swift in Sources */ = {isa = PBXBuildFile; fileRef = E1D4BF802719D22800A11E64 /* AppAppearance.swift */; };
E1D4BF8D2719F3A300A11E64 /* VideoPlayerJumpLength.swift in Sources */ = {isa = PBXBuildFile; fileRef = E1F0204D26CCCA74001C1C3B /* VideoPlayerJumpLength.swift */; };
E1D4BF8F271A079A00A11E64 /* BasicAppSettingsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E1D4BF8E271A079A00A11E64 /* BasicAppSettingsView.swift */; };
E1E48CC9271E6D410021A2F9 /* RefreshHelper.swift in Sources */ = {isa = PBXBuildFile; fileRef = E1E48CC8271E6D410021A2F9 /* RefreshHelper.swift */; };
E1F0204E26CCCA74001C1C3B /* VideoPlayerJumpLength.swift in Sources */ = {isa = PBXBuildFile; fileRef = E1F0204D26CCCA74001C1C3B /* VideoPlayerJumpLength.swift */; };
E1F0204F26CCCA74001C1C3B /* VideoPlayerJumpLength.swift in Sources */ = {isa = PBXBuildFile; fileRef = E1F0204D26CCCA74001C1C3B /* VideoPlayerJumpLength.swift */; };
E1FCD08826C35A0D007C8DCF /* NetworkError.swift in Sources */ = {isa = PBXBuildFile; fileRef = E1FCD08726C35A0D007C8DCF /* NetworkError.swift */; };
@ -575,6 +579,7 @@
E18845F726DEA9C900B0C5B7 /* ItemViewBody.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ItemViewBody.swift; sourceTree = "<group>"; };
E18845FF26DECB9E00B0C5B7 /* ItemLandscapeTopBarView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ItemLandscapeTopBarView.swift; sourceTree = "<group>"; };
E188460326DEF04800B0C5B7 /* EpisodeCardVStackView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EpisodeCardVStackView.swift; sourceTree = "<group>"; };
E19169CD272514760085832A /* HTTPScheme.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HTTPScheme.swift; sourceTree = "<group>"; };
E193D4D727193CAC00900D82 /* PortraitImageStackable.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PortraitImageStackable.swift; sourceTree = "<group>"; };
E193D4DA27193CCA00900D82 /* PillStackable.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PillStackable.swift; sourceTree = "<group>"; };
E193D5422719407E00900D82 /* tvOSMainCoordinator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = tvOSMainCoordinator.swift; sourceTree = "<group>"; };
@ -597,6 +602,7 @@
E1D4BF862719D27100A11E64 /* Bitrates.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Bitrates.swift; sourceTree = "<group>"; };
E1D4BF892719D3D000A11E64 /* BasicAppSettingsCoordinator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BasicAppSettingsCoordinator.swift; sourceTree = "<group>"; };
E1D4BF8E271A079A00A11E64 /* BasicAppSettingsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BasicAppSettingsView.swift; sourceTree = "<group>"; };
E1E48CC8271E6D410021A2F9 /* RefreshHelper.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RefreshHelper.swift; sourceTree = "<group>"; };
E1F0204D26CCCA74001C1C3B /* VideoPlayerJumpLength.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VideoPlayerJumpLength.swift; sourceTree = "<group>"; };
E1FCD08726C35A0D007C8DCF /* NetworkError.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NetworkError.swift; sourceTree = "<group>"; };
E1FCD09526C47118007C8DCF /* ErrorMessage.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ErrorMessage.swift; sourceTree = "<group>"; };
@ -807,6 +813,7 @@
E1AD104926D94822003E4A08 /* DetailItem.swift */,
53192D5C265AA78A008A4215 /* DeviceProfileBuilder.swift */,
62EC353326766B03000E9F2D /* DeviceRotationViewModifier.swift */,
E19169CD272514760085832A /* HTTPScheme.swift */,
E193D4DA27193CCA00900D82 /* PillStackable.swift */,
E193D4D727193CAC00900D82 /* PortraitImageStackable.swift */,
E1D4BF832719D25A00A11E64 /* TrackLanguage.swift */,
@ -856,6 +863,7 @@
5377CBF3263B596A003A4E83 /* JellyfinPlayer */ = {
isa = PBXGroup;
children = (
E1DD1127271E7D15005BE12F /* Objects */,
E13DD3BB27163C3E009D4DAF /* App */,
62ECA01926FA6D6900E8EBB7 /* AppURLHandler */,
5377CBF8263B596B003A4E83 /* Assets.xcassets */,
@ -1293,6 +1301,14 @@
path = Views;
sourceTree = "<group>";
};
E1DD1127271E7D15005BE12F /* Objects */ = {
isa = PBXGroup;
children = (
E1E48CC8271E6D410021A2F9 /* RefreshHelper.swift */,
);
path = Objects;
sourceTree = "<group>";
};
E1FCD08E26C466F3007C8DCF /* Errors */ = {
isa = PBXGroup;
children = (
@ -1734,6 +1750,7 @@
E193D549271941CC00900D82 /* UserSignInView.swift in Sources */,
535870AA2669D8AE00D05A09 /* BlurHashDecode.swift in Sources */,
53ABFDE5267974EF00886593 /* ViewModel.swift in Sources */,
E19169CF272514760085832A /* HTTPScheme.swift in Sources */,
C45B29BB26FAC5B600CEF5E0 /* ColorExtension.swift in Sources */,
531069582684E7EE00CFFDBA /* MediaInfoView.swift in Sources */,
E1D4BF822719D22800A11E64 /* AppAppearance.swift in Sources */,
@ -1808,6 +1825,7 @@
E173DA5426D050F500CC4EB7 /* ServerDetailViewModel.swift in Sources */,
E188460426DEF04800B0C5B7 /* EpisodeCardVStackView.swift in Sources */,
53F8377D265FF67C00F456B3 /* VideoPlayerSettingsView.swift in Sources */,
E19169CE272514760085832A /* HTTPScheme.swift in Sources */,
53192D5D265AA78A008A4215 /* DeviceProfileBuilder.swift in Sources */,
62133890265F83A900A81A2A /* LibraryListView.swift in Sources */,
62C29EA326D1030F00C1D2E7 /* ConnectToServerCoodinator.swift in Sources */,
@ -1841,6 +1859,7 @@
6220D0CC26D640C400B8E046 /* AppURLHandler.swift in Sources */,
62E632F3267D54030063E547 /* ItemViewModel.swift in Sources */,
53DE4BD2267098F300739748 /* SearchBarView.swift in Sources */,
E1E48CC9271E6D410021A2F9 /* RefreshHelper.swift in Sources */,
E1D4BF842719D25A00A11E64 /* TrackLanguage.swift in Sources */,
E14F7D0726DB36EF007C3AE6 /* ItemPortraitMainView.swift in Sources */,
E1AD106226D9B7CD003E4A08 /* ItemPortraitHeaderOverlayView.swift in Sources */,
@ -1904,6 +1923,7 @@
files = (
53649AB3269D3F5B00A2D8B7 /* LogManager.swift in Sources */,
E13DD3CB27164BA8009D4DAF /* UIDeviceExtensions.swift in Sources */,
E19169D0272514760085832A /* HTTPScheme.swift in Sources */,
6267B3D726710B9700A7371D /* CollectionExtensions.swift in Sources */,
628B953C2670D2430091AF3B /* StringExtensions.swift in Sources */,
6267B3DB2671139400A7371D /* ImageExtensions.swift in Sources */,

View File

@ -19,8 +19,7 @@ struct JellyfinPlayerApp: App {
var body: some Scene {
WindowGroup {
// TODO: Replace with a SplashView
Color(appAppearance.style == .light ? UIColor.white : UIColor.black)
EmptyView()
.ignoresSafeArea()
.onAppear {
setupAppearance()

View File

@ -0,0 +1,38 @@
{
"colors" : [
{
"color" : {
"color-space" : "srgb",
"components" : {
"alpha" : "1.000",
"blue" : "1.000",
"green" : "1.000",
"red" : "1.000"
}
},
"idiom" : "universal"
},
{
"appearances" : [
{
"appearance" : "luminosity",
"value" : "dark"
}
],
"color" : {
"color-space" : "srgb",
"components" : {
"alpha" : "1.000",
"blue" : "0.000",
"green" : "0.000",
"red" : "0.000"
}
},
"idiom" : "universal"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}

View File

@ -0,0 +1,23 @@
{
"images" : [
{
"filename" : "swiftfin-logo.png",
"idiom" : "universal",
"scale" : "1x"
},
{
"filename" : "swiftfin-logo-1.png",
"idiom" : "universal",
"scale" : "2x"
},
{
"filename" : "swiftfin-logo-2.png",
"idiom" : "universal",
"scale" : "3x"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 26 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 26 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 26 KiB

View File

@ -62,9 +62,14 @@ network.</string>
<key>UIApplicationSupportsIndirectInputEvents</key>
<true/>
<key>UILaunchScreen</key>
<dict/>
<key>UILaunchStoryboardName</key>
<string>VideoPlayer</string>
<dict>
<key>UIImageRespectsSafeAreaInsets</key>
<true/>
<key>UIImageName</key>
<string>swiftfin-logo</string>
<key>UIColorName</key>
<string>LaunchScreenBackground</string>
</dict>
<key>UIRequiredDeviceCapabilities</key>
<array>
<string>armv7</string>

View File

@ -0,0 +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
*/
import UIKit
// A more general derivative of
// https://stackoverflow.com/questions/65812080/introspect-library-uirefreshcontrol-with-swiftui-not-working
class RefreshHelper {
var refreshControl: UIRefreshControl?
var refreshAction: (() -> Void)?
@objc func didRefresh() {
guard let refreshControl = refreshControl else { return }
refreshAction?()
refreshControl.endRefreshing()
}
}

View File

@ -18,6 +18,7 @@ struct BasicAppSettingsView: View {
@State var resetTapped: Bool = false
@Default(.appAppearance) var appAppearance
@Default(.defaultHTTPScheme) var defaultHTTPScheme
var body: some View {
Form {
@ -33,6 +34,16 @@ struct BasicAppSettingsView: View {
Text("Accessibility")
}
Section {
Picker("Default Scheme", selection: $defaultHTTPScheme) {
ForEach(HTTPScheme.allCases, id: \.self) { scheme in
Text("\(scheme.rawValue)")
}
}
} header: {
Text("Networking")
}
Button {
resetTapped = true
} label: {

View File

@ -6,14 +6,17 @@
* Copyright 2021 Aiden Vigue & Jellyfin Contributors
*/
import SwiftUI
import Defaults
import Stinsen
import SwiftUI
struct ConnectToServerView: View {
@ObservedObject var viewModel: ConnectToServerViewModel
@State var uri = ""
@Default(.defaultHTTPScheme) var defaultHTTPScheme
var body: some View {
List {
Section {
@ -21,6 +24,11 @@ struct ConnectToServerView: View {
.disableAutocorrection(true)
.autocapitalization(.none)
.keyboardType(.URL)
.onAppear {
if uri == "" {
uri = "\(defaultHTTPScheme.rawValue)://"
}
}
if viewModel.isLoading {
Button(role: .destructive) {

View File

@ -8,12 +8,15 @@
*/
import Foundation
import Introspect
import SwiftUI
struct HomeView: View {
@EnvironmentObject var homeRouter: HomeCoordinator.Router
@StateObject var viewModel = HomeViewModel()
private let refreshHelper = RefreshHelper()
@ViewBuilder
var innerBody: some View {
@ -28,33 +31,40 @@ struct HomeView: View {
if !viewModel.nextUpItems.isEmpty {
NextUpView(items: viewModel.nextUpItems)
}
if !viewModel.librariesShowRecentlyAddedIDs.isEmpty {
ForEach(viewModel.librariesShowRecentlyAddedIDs, id: \.self) { libraryID in
let library = viewModel.libraries.first(where: { $0.id == libraryID })
HStack {
Text("Latest \(library?.name ?? "")")
.font(.title2)
.fontWeight(.bold)
Spacer()
Button {
homeRouter
.route(to: \.library, (viewModel: .init(parentID: libraryID,
filters: viewModel.recentFilterSet),
title: library?.name ?? ""))
} label: {
HStack {
Text("See All").font(.subheadline).fontWeight(.bold)
Image(systemName: "chevron.right").font(Font.subheadline.bold())
}
ForEach(viewModel.libraries, id: \.self) { library in
HStack {
Text("Latest \(library.name ?? "")")
.font(.title2)
.fontWeight(.bold)
Spacer()
Button {
homeRouter
.route(to: \.library, (viewModel: .init(parentID: library.id!,
filters: viewModel.recentFilterSet),
title: library.name ?? ""))
} label: {
HStack {
Text("See All").font(.subheadline).fontWeight(.bold)
Image(systemName: "chevron.right").font(Font.subheadline.bold())
}
}.padding(.leading, 16)
.padding(.trailing, 16)
LatestMediaView(viewModel: .init(libraryID: libraryID))
}
}
}.padding(.leading, 16)
.padding(.trailing, 16)
LatestMediaView(viewModel: .init(libraryID: library.id!))
}
}
.padding(.bottom, UIDevice.current.userInterfaceIdiom == .phone ? 20 : 30)
}
.introspectScrollView { scrollView in
let control = UIRefreshControl()
refreshHelper.refreshControl = control
refreshHelper.refreshAction = viewModel.refresh
control.addTarget(refreshHelper, action: #selector(RefreshHelper.didRefresh), for: .valueChanged)
scrollView.refreshControl = control
}
}
}

View File

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

View File

@ -37,7 +37,7 @@ public class ServerDiscovery {
if let port = components?.port {
return port
}
return 8096
return 7359
}
enum CodingKeys: String, CodingKey {

View File

@ -54,10 +54,14 @@ final class SessionManager {
// Connects to a server at the given uri, storing if successful
func connectToServer(with uri: String) -> AnyPublisher<SwiftfinStore.State.Server, Error> {
var uri = uri
if !uri.contains("http") {
uri = "https://" + uri
var uriComponents = URLComponents(string: uri) ?? URLComponents()
if uriComponents.scheme == nil {
uriComponents.scheme = SwiftfinStore.Defaults.suite[.defaultHTTPScheme].rawValue
}
var uri = uriComponents.string ?? ""
if uri.last == "/" {
uri = String(uri.dropLast())
}

View File

@ -23,6 +23,7 @@ extension SwiftfinStore {
extension Defaults.Keys {
static let lastServerUserID = Defaults.Key<String?>("lastServerUserID", suite: SwiftfinStore.Defaults.suite)
static let defaultHTTPScheme = Key<HTTPScheme>("defaultHTTPScheme", default: .http, suite: SwiftfinStore.Defaults.suite)
static let inNetworkBandwidth = Key<Int>("InNetworkBandwidth", default: 40_000_000, suite: SwiftfinStore.Defaults.suite)
static let outOfNetworkBandwidth = Key<Int>("OutOfNetworkBandwidth", default: 40_000_000, suite: SwiftfinStore.Defaults.suite)
static let isAutoSelectSubtitles = Key<Bool>("isAutoSelectSubtitles", default: false, suite: SwiftfinStore.Defaults.suite)

View File

@ -14,10 +14,10 @@ import JellyfinAPI
final class HomeViewModel: ViewModel {
@Published var librariesShowRecentlyAddedIDs = [String]()
@Published var libraries = [BaseItemDto]()
@Published var resumeItems = [BaseItemDto]()
@Published var nextUpItems = [BaseItemDto]()
@Published var librariesShowRecentlyAddedIDs: [String] = []
@Published var libraries: [BaseItemDto] = []
@Published var resumeItems: [BaseItemDto] = []
@Published var nextUpItems: [BaseItemDto] = []
// temp
var recentFilterSet: LibraryFilters = LibraryFilters(filters: [], sortOrder: [.descending], sortBy: [.dateAdded])
@ -32,26 +32,42 @@ final class HomeViewModel: ViewModel {
UserViewsAPI.getUserViews(userId: SessionManager.main.currentLogin.user.id)
.trackActivity(loading)
.sink(receiveCompletion: { completion in
self.handleAPIRequestError(completion: completion)
switch completion {
case .finished: ()
case .failure(_):
self.libraries = []
self.handleAPIRequestError(completion: completion)
}
}, receiveValue: { response in
var newLibraries: [BaseItemDto] = []
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" {
self.libraries.append(item)
newLibraries.append(item)
}
}
UserAPI.getCurrentUser()
.trackActivity(self.loading)
.sink(receiveCompletion: { completion in
self.handleAPIRequestError(completion: completion)
switch completion {
case .finished: ()
case .failure(_):
self.libraries = []
self.handleAPIRequestError(completion: completion)
}
}, receiveValue: { response in
self.libraries.forEach { library in
if !(response.configuration?.latestItemsExcludes?.contains(library.id!))! {
LogManager.shared.log.debug("Adding library \(library.id!) (\(library.name ?? "nil")) to recently added list")
self.librariesShowRecentlyAddedIDs.append(library.id!)
let excludeIDs = response.configuration?.latestItemsExcludes != nil ? response.configuration!.latestItemsExcludes! : []
for excludeID in excludeIDs {
newLibraries.removeAll { library in
return library.id == excludeID
}
}
self.libraries = newLibraries
})
.store(in: &self.cancellables)
})
@ -59,12 +75,20 @@ final class HomeViewModel: ViewModel {
ItemsAPI.getResumeItems(userId: SessionManager.main.currentLogin.user.id, limit: 12,
fields: [.primaryImageAspectRatio, .seriesPrimaryImage, .seasonUserData, .overview, .genres, .people],
mediaTypes: ["Video"], imageTypeLimit: 1, enableImageTypes: [.primary, .backdrop, .thumb])
mediaTypes: ["Video"],
imageTypeLimit: 1,
enableImageTypes: [.primary, .backdrop, .thumb])
.trackActivity(loading)
.sink(receiveCompletion: { completion in
self.handleAPIRequestError(completion: completion)
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)
@ -73,9 +97,15 @@ final class HomeViewModel: ViewModel {
fields: [.primaryImageAspectRatio, .seriesPrimaryImage, .seasonUserData, .overview, .genres, .people])
.trackActivity(loading)
.sink(receiveCompletion: { completion in
self.handleAPIRequestError(completion: completion)
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)

Binary file not shown.

Binary file not shown.