Merge branch 'main' into R.swift
# Conflicts: # JellyfinPlayer.xcodeproj/project.pbxproj # JellyfinPlayer.xcworkspace/xcshareddata/swiftpm/Package.resolved # Translations/en.lproj/Localizable.strings
|
@ -7,15 +7,13 @@
|
|||
|
||||
import SwiftUI
|
||||
import UIKit
|
||||
|
||||
@main
|
||||
struct JellyfinPlayer_tvOSApp: App {
|
||||
let persistenceController = PersistenceController.shared
|
||||
|
||||
var body: some Scene {
|
||||
WindowGroup {
|
||||
SplashView()
|
||||
.environment(\.managedObjectContext, persistenceController.container.viewContext)
|
||||
.ignoresSafeArea(.all, edges: .all)
|
||||
MainCoordinator().view()
|
||||
}
|
||||
}
|
||||
}
|
After Width: | Height: | Size: 18 KiB |
|
@ -0,0 +1,12 @@
|
|||
{
|
||||
"images" : [
|
||||
{
|
||||
"filename" : "1280x768-back.png",
|
||||
"idiom" : "tv"
|
||||
}
|
||||
],
|
||||
"info" : {
|
||||
"author" : "xcode",
|
||||
"version" : 1
|
||||
}
|
||||
}
|
|
@ -0,0 +1,6 @@
|
|||
{
|
||||
"info" : {
|
||||
"author" : "xcode",
|
||||
"version" : 1
|
||||
}
|
||||
}
|
|
@ -0,0 +1,14 @@
|
|||
{
|
||||
"info" : {
|
||||
"author" : "xcode",
|
||||
"version" : 1
|
||||
},
|
||||
"layers" : [
|
||||
{
|
||||
"filename" : "Front.imagestacklayer"
|
||||
},
|
||||
{
|
||||
"filename" : "Back.imagestacklayer"
|
||||
}
|
||||
]
|
||||
}
|
After Width: | Height: | Size: 102 KiB |
|
@ -0,0 +1,12 @@
|
|||
{
|
||||
"images" : [
|
||||
{
|
||||
"filename" : "512.png",
|
||||
"idiom" : "tv"
|
||||
}
|
||||
],
|
||||
"info" : {
|
||||
"author" : "xcode",
|
||||
"version" : 1
|
||||
}
|
||||
}
|
|
@ -0,0 +1,6 @@
|
|||
{
|
||||
"info" : {
|
||||
"author" : "xcode",
|
||||
"version" : 1
|
||||
}
|
||||
}
|
After Width: | Height: | Size: 2.3 KiB |
|
@ -0,0 +1,18 @@
|
|||
{
|
||||
"images" : [
|
||||
{
|
||||
"filename" : "400x240-back.png",
|
||||
"idiom" : "tv",
|
||||
"scale" : "1x"
|
||||
},
|
||||
{
|
||||
"filename" : "Webp.net-resizeimage.png",
|
||||
"idiom" : "tv",
|
||||
"scale" : "2x"
|
||||
}
|
||||
],
|
||||
"info" : {
|
||||
"author" : "xcode",
|
||||
"version" : 1
|
||||
}
|
||||
}
|
After Width: | Height: | Size: 7.7 KiB |
|
@ -0,0 +1,6 @@
|
|||
{
|
||||
"info" : {
|
||||
"author" : "xcode",
|
||||
"version" : 1
|
||||
}
|
||||
}
|
|
@ -0,0 +1,14 @@
|
|||
{
|
||||
"info" : {
|
||||
"author" : "xcode",
|
||||
"version" : 1
|
||||
},
|
||||
"layers" : [
|
||||
{
|
||||
"filename" : "Front.imagestacklayer"
|
||||
},
|
||||
{
|
||||
"filename" : "Back.imagestacklayer"
|
||||
}
|
||||
]
|
||||
}
|
After Width: | Height: | Size: 23 KiB |
|
@ -0,0 +1,18 @@
|
|||
{
|
||||
"images" : [
|
||||
{
|
||||
"filename" : "216.png",
|
||||
"idiom" : "tv",
|
||||
"scale" : "1x"
|
||||
},
|
||||
{
|
||||
"filename" : "Webp.net-resizeimage-2.png",
|
||||
"idiom" : "tv",
|
||||
"scale" : "2x"
|
||||
}
|
||||
],
|
||||
"info" : {
|
||||
"author" : "xcode",
|
||||
"version" : 1
|
||||
}
|
||||
}
|
After Width: | Height: | Size: 76 KiB |
|
@ -0,0 +1,6 @@
|
|||
{
|
||||
"info" : {
|
||||
"author" : "xcode",
|
||||
"version" : 1
|
||||
}
|
||||
}
|
|
@ -0,0 +1,32 @@
|
|||
{
|
||||
"assets" : [
|
||||
{
|
||||
"filename" : "App Icon - App Store.imagestack",
|
||||
"idiom" : "tv",
|
||||
"role" : "primary-app-icon",
|
||||
"size" : "1280x768"
|
||||
},
|
||||
{
|
||||
"filename" : "App Icon.imagestack",
|
||||
"idiom" : "tv",
|
||||
"role" : "primary-app-icon",
|
||||
"size" : "400x240"
|
||||
},
|
||||
{
|
||||
"filename" : "Top Shelf Image Wide.imageset",
|
||||
"idiom" : "tv",
|
||||
"role" : "top-shelf-image-wide",
|
||||
"size" : "2320x720"
|
||||
},
|
||||
{
|
||||
"filename" : "Top Shelf Image.imageset",
|
||||
"idiom" : "tv",
|
||||
"role" : "top-shelf-image",
|
||||
"size" : "1920x720"
|
||||
}
|
||||
],
|
||||
"info" : {
|
||||
"author" : "xcode",
|
||||
"version" : 1
|
||||
}
|
||||
}
|
|
@ -0,0 +1,28 @@
|
|||
{
|
||||
"images" : [
|
||||
{
|
||||
"filename" : "top shelf.png",
|
||||
"idiom" : "tv",
|
||||
"scale" : "1x"
|
||||
},
|
||||
{
|
||||
"filename" : "Untitled-1.png",
|
||||
"idiom" : "tv",
|
||||
"scale" : "2x"
|
||||
},
|
||||
{
|
||||
"filename" : "top shelf-1.png",
|
||||
"idiom" : "tv-marketing",
|
||||
"scale" : "1x"
|
||||
},
|
||||
{
|
||||
"filename" : "Untitled-2.png",
|
||||
"idiom" : "tv-marketing",
|
||||
"scale" : "2x"
|
||||
}
|
||||
],
|
||||
"info" : {
|
||||
"author" : "xcode",
|
||||
"version" : 1
|
||||
}
|
||||
}
|
After Width: | Height: | Size: 355 KiB |
After Width: | Height: | Size: 355 KiB |
After Width: | Height: | Size: 100 KiB |
After Width: | Height: | Size: 100 KiB |
|
@ -0,0 +1,28 @@
|
|||
{
|
||||
"images" : [
|
||||
{
|
||||
"filename" : "top shelf.png",
|
||||
"idiom" : "tv",
|
||||
"scale" : "1x"
|
||||
},
|
||||
{
|
||||
"filename" : "Untitled-2.png",
|
||||
"idiom" : "tv",
|
||||
"scale" : "2x"
|
||||
},
|
||||
{
|
||||
"filename" : "top shelf-1.png",
|
||||
"idiom" : "tv-marketing",
|
||||
"scale" : "1x"
|
||||
},
|
||||
{
|
||||
"filename" : "Untitled-1.png",
|
||||
"idiom" : "tv-marketing",
|
||||
"scale" : "2x"
|
||||
}
|
||||
],
|
||||
"info" : {
|
||||
"author" : "xcode",
|
||||
"version" : 1
|
||||
}
|
||||
}
|
After Width: | Height: | Size: 265 KiB |
After Width: | Height: | Size: 265 KiB |
After Width: | Height: | Size: 76 KiB |
After Width: | Height: | Size: 76 KiB |
|
@ -57,6 +57,22 @@ struct PortraitItemElement: View {
|
|||
.opacity(1), alignment: .topTrailing).opacity(1)
|
||||
Text(item.title)
|
||||
.frame(width: 200, height: 30, alignment: .center)
|
||||
if item.type == "Movie" || item.type == "Series" {
|
||||
Text("\(String(item.productionYear ?? 0)) • \(item.officialRating ?? "N/A")")
|
||||
.foregroundColor(.secondary)
|
||||
.font(.caption)
|
||||
.fontWeight(.medium)
|
||||
} else if item.type == "Season" {
|
||||
Text("\(item.name ?? "") • \(String(item.productionYear ?? 0))")
|
||||
.foregroundColor(.secondary)
|
||||
.font(.caption)
|
||||
.fontWeight(.medium)
|
||||
} else {
|
||||
Text("S\(String(item.parentIndexNumber ?? 0)):E\(String(item.indexNumber ?? 0))")
|
||||
.foregroundColor(.secondary)
|
||||
.font(.caption)
|
||||
.fontWeight(.medium)
|
||||
}
|
||||
}
|
||||
.onChange(of: envFocused) { envFocus in
|
||||
withAnimation(.linear(duration: 0.15)) {
|
||||
|
|
|
@ -19,7 +19,7 @@ struct PublicUserButton: View {
|
|||
var body: some View {
|
||||
VStack {
|
||||
if publicUser.primaryImageTag != nil {
|
||||
ImageView(src: URL(string: "\(ServerEnvironment.current.server.baseURI ?? "")/Users/\(publicUser.id ?? "")/Images/Primary?width=500&quality=80&tag=\(publicUser.primaryImageTag!)")!)
|
||||
ImageView(src: URL(string: "\(SessionManager.main.currentLogin.server.uri)/Users/\(publicUser.id ?? "")/Images/Primary?width=500&quality=80&tag=\(publicUser.primaryImageTag!)")!)
|
||||
.frame(width: 250, height: 250)
|
||||
.cornerRadius(125.0)
|
||||
} else {
|
||||
|
|
|
@ -1,176 +0,0 @@
|
|||
/* 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
|
||||
*/
|
||||
|
||||
import JellyfinAPI
|
||||
import SwiftUI
|
||||
|
||||
struct ConnectToServerView: View {
|
||||
@StateObject var viewModel = ConnectToServerViewModel()
|
||||
@State var username = ""
|
||||
@State var password = ""
|
||||
@State var uri = ""
|
||||
|
||||
var body: some View {
|
||||
VStack(alignment: .leading) {
|
||||
if viewModel.isConnectedServer {
|
||||
if viewModel.publicUsers.isEmpty {
|
||||
Section(header: Text(viewModel.lastPublicUsers.isEmpty || username == "" ? "Login to \(ServerEnvironment.current.server.name ?? "")": "")) {
|
||||
if viewModel.lastPublicUsers.isEmpty || username == "" {
|
||||
TextField(NSLocalizedString("Username", comment: ""), text: $username)
|
||||
.disableAutocorrection(true)
|
||||
.autocapitalization(.none)
|
||||
} else {
|
||||
HStack {
|
||||
Spacer()
|
||||
ImageView(src: URL(string: "\(ServerEnvironment.current.server.baseURI ?? "")/Users/\(viewModel.selectedPublicUser.id ?? "")/Images/Primary?width=500&quality=80&tag=\(viewModel.selectedPublicUser.primaryImageTag ?? "")")!)
|
||||
.frame(width: 250, height: 250)
|
||||
.cornerRadius(125.0)
|
||||
Spacer()
|
||||
}
|
||||
}
|
||||
|
||||
SecureField(NSLocalizedString("Password", comment: ""), text: $password)
|
||||
.disableAutocorrection(true)
|
||||
.autocapitalization(.none)
|
||||
}
|
||||
|
||||
Section {
|
||||
HStack {
|
||||
Button {
|
||||
if !viewModel.lastPublicUsers.isEmpty {
|
||||
username = ""
|
||||
viewModel.showPublicUsers()
|
||||
} else {
|
||||
viewModel.isConnectedServer = false
|
||||
}
|
||||
} label: {
|
||||
Spacer()
|
||||
HStack {
|
||||
Text("Back")
|
||||
}
|
||||
Spacer()
|
||||
}
|
||||
|
||||
Button {
|
||||
viewModel.login()
|
||||
} label: {
|
||||
Spacer()
|
||||
if viewModel.isLoading {
|
||||
ProgressView()
|
||||
} else {
|
||||
Text("Login")
|
||||
}
|
||||
Spacer()
|
||||
}.disabled(viewModel.isLoading || username.isEmpty)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
VStack {
|
||||
HStack {
|
||||
ForEach(viewModel.publicUsers, id: \.id) { publicUser in
|
||||
Button(action: {
|
||||
if SessionManager.current.doesUserHaveSavedSession(userID: publicUser.id!) {
|
||||
let user = SessionManager.current.getSavedSession(userID: publicUser.id!)
|
||||
SessionManager.current.loginWithSavedSession(user: user)
|
||||
} else {
|
||||
username = publicUser.name ?? ""
|
||||
viewModel.selectedPublicUser = publicUser
|
||||
viewModel.hidePublicUsers()
|
||||
if !(publicUser.hasPassword ?? true) {
|
||||
password = ""
|
||||
viewModel.login()
|
||||
}
|
||||
}
|
||||
}) {
|
||||
PublicUserButton(publicUser: publicUser)
|
||||
}
|
||||
.buttonStyle(PlainNavigationLinkButtonStyle())
|
||||
}
|
||||
}.padding(.bottom, 20)
|
||||
HStack {
|
||||
Spacer()
|
||||
Button {
|
||||
viewModel.hidePublicUsers()
|
||||
username = ""
|
||||
} label: {
|
||||
Text("Other User").font(.headline).fontWeight(.semibold)
|
||||
}
|
||||
Spacer()
|
||||
}.padding(.top, 12)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
if !viewModel.isLoading {
|
||||
|
||||
Form {
|
||||
Section(header: Text("Server Information")) {
|
||||
TextField(NSLocalizedString("Server URL", comment: ""), text: $uri)
|
||||
.disableAutocorrection(true)
|
||||
.autocapitalization(.none)
|
||||
.keyboardType(.URL)
|
||||
Button {
|
||||
viewModel.connectToServer()
|
||||
} label: {
|
||||
HStack {
|
||||
Text("Connect")
|
||||
Spacer()
|
||||
}
|
||||
if viewModel.isLoading {
|
||||
ProgressView()
|
||||
}
|
||||
}
|
||||
.disabled(viewModel.isLoading || uri.isEmpty)
|
||||
}
|
||||
Section(header: Text("Local Servers")) {
|
||||
if self.viewModel.searching {
|
||||
ProgressView()
|
||||
}
|
||||
ForEach(self.viewModel.servers, id: \.id) { server in
|
||||
Button(action: {
|
||||
print(server.url)
|
||||
viewModel.connectToServer(at: server.url)
|
||||
}, label: {
|
||||
HStack {
|
||||
VStack(alignment: .leading) {
|
||||
Text(server.name)
|
||||
.font(.headline)
|
||||
Text(server.host)
|
||||
.font(.subheadline)
|
||||
}
|
||||
Spacer()
|
||||
Image(systemName: "chevron.forward")
|
||||
.padding()
|
||||
}
|
||||
|
||||
})
|
||||
.disabled(viewModel.isLoading)
|
||||
}
|
||||
}
|
||||
.onAppear(perform: self.viewModel.discoverServers)
|
||||
}
|
||||
} else {
|
||||
ProgressView()
|
||||
}
|
||||
}
|
||||
}
|
||||
.padding(.leading, 90)
|
||||
.padding(.trailing, 90)
|
||||
.alert(item: $viewModel.errorMessage) { _ in
|
||||
Alert(title: Text("Error"), message: Text(viewModel.errorMessage as? String ?? ""), dismissButton: .default(Text("Ok")))
|
||||
}
|
||||
.onChange(of: uri) { uri in
|
||||
viewModel.uriSubject.send(uri)
|
||||
}
|
||||
.onChange(of: username) { username in
|
||||
viewModel.usernameSubject.send(username)
|
||||
}
|
||||
.onChange(of: password) { password in
|
||||
viewModel.passwordSubject.send(password)
|
||||
}
|
||||
.navigationTitle(viewModel.isConnectedServer ? NSLocalizedString("Who's watching?", comment: "") : NSLocalizedString("Connect to Jellyfin", comment: ""))
|
||||
}
|
||||
}
|
|
@ -1,15 +0,0 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||
<plist version="1.0">
|
||||
<dict>
|
||||
<key>com.apple.developer.user-management</key>
|
||||
<array>
|
||||
<string>get-current-user</string>
|
||||
<string>runs-as-current-user</string>
|
||||
</array>
|
||||
<key>keychain-access-groups</key>
|
||||
<array>
|
||||
<string>$(AppIdentifierPrefix)me.vigue.jellyfin.sharedKeychain</string>
|
||||
</array>
|
||||
</dict>
|
||||
</plist>
|
|
@ -1,5 +0,0 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||
<plist version="1.0">
|
||||
<dict/>
|
||||
</plist>
|
|
@ -1,9 +0,0 @@
|
|||
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
|
||||
<model type="com.apple.IDECoreDataModeler.DataModel" documentVersion="1.0" lastSavedToolsVersion="1" systemVersion="11A491" minimumToolsVersion="Automatic" sourceLanguage="Swift" usedWithCloudKit="false" userDefinedModelVersionIdentifier="">
|
||||
<entity name="Item" representedClassName="Item" syncable="YES" codeGenerationType="class">
|
||||
<attribute name="timestamp" optional="YES" attributeType="Date" usesScalarValueType="NO"/>
|
||||
</entity>
|
||||
<elements>
|
||||
<element name="Item" positionX="-63" positionY="-18" width="128" height="44"/>
|
||||
</elements>
|
||||
</model>
|
|
@ -1,93 +0,0 @@
|
|||
/*
|
||||
* 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
|
||||
*/
|
||||
|
||||
import SwiftUI
|
||||
import SwiftUICollection
|
||||
import JellyfinAPI
|
||||
|
||||
struct LibraryView: View {
|
||||
@StateObject var viewModel: LibraryViewModel
|
||||
var title: String
|
||||
|
||||
// MARK: tracks for grid
|
||||
var defaultFilters = LibraryFilters(filters: [], sortOrder: [.ascending], withGenres: [], tags: [], sortBy: [.name])
|
||||
|
||||
@State var isShowingSearchView = false
|
||||
@State var isShowingFilterView = false
|
||||
|
||||
var body: some View {
|
||||
if viewModel.isLoading == true {
|
||||
ProgressView()
|
||||
} else if !viewModel.items.isEmpty {
|
||||
CollectionView(rows: viewModel.rows) { _, _ in
|
||||
let itemSize = NSCollectionLayoutSize(
|
||||
widthDimension: .fractionalWidth(1),
|
||||
heightDimension: .fractionalHeight(1)
|
||||
)
|
||||
let item = NSCollectionLayoutItem(layoutSize: itemSize)
|
||||
|
||||
let groupSize = NSCollectionLayoutSize(
|
||||
widthDimension: .absolute(200),
|
||||
heightDimension: .absolute(300)
|
||||
)
|
||||
let group = NSCollectionLayoutGroup.horizontal(
|
||||
layoutSize: groupSize,
|
||||
subitems: [item]
|
||||
)
|
||||
|
||||
let header =
|
||||
NSCollectionLayoutBoundarySupplementaryItem(
|
||||
layoutSize: NSCollectionLayoutSize(
|
||||
widthDimension: .fractionalWidth(1),
|
||||
heightDimension: .absolute(44)
|
||||
),
|
||||
elementKind: UICollectionView.elementKindSectionHeader,
|
||||
alignment: .topLeading
|
||||
)
|
||||
|
||||
let section = NSCollectionLayoutSection(group: group)
|
||||
|
||||
section.contentInsets = NSDirectionalEdgeInsets(top: 30, leading: 0, bottom: 80, trailing: 80)
|
||||
section.interGroupSpacing = 48
|
||||
section.orthogonalScrollingBehavior = .continuous
|
||||
section.boundarySupplementaryItems = [header]
|
||||
return section
|
||||
} cell: { _, cell in
|
||||
GeometryReader { _ in
|
||||
if let item = cell.item {
|
||||
if item.type != "Folder" {
|
||||
NavigationLink(destination: LazyView { ItemView(item: item) }) {
|
||||
PortraitItemElement(item: item)
|
||||
}
|
||||
.buttonStyle(PlainNavigationLinkButtonStyle())
|
||||
.onAppear {
|
||||
if item == viewModel.items.last && viewModel.hasNextPage {
|
||||
viewModel.requestNextPageAsync()
|
||||
}
|
||||
}
|
||||
}
|
||||
} else if cell.loadingCell {
|
||||
ProgressView()
|
||||
.frame(minWidth: 0, maxWidth: .infinity, minHeight: 0, maxHeight: .infinity, alignment: .center)
|
||||
}
|
||||
}
|
||||
} supplementaryView: { _, indexPath in
|
||||
HStack {
|
||||
Spacer()
|
||||
}.accessibilityIdentifier("\(indexPath.section).\(indexPath.row)")
|
||||
}
|
||||
.frame(maxWidth: .infinity, maxHeight: .infinity)
|
||||
.ignoresSafeArea(.all)
|
||||
} else {
|
||||
Text("No results.")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// stream BM^S by nicki!
|
||||
//
|
|
@ -1,79 +0,0 @@
|
|||
//
|
||||
/*
|
||||
* SwiftFin is subject to the terms of the Mozilla Public
|
||||
* License, v2.0. 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 Foundation
|
||||
import SwiftUI
|
||||
|
||||
struct MainTabView: View {
|
||||
@State private var tabSelection: Tab = .home
|
||||
@StateObject private var viewModel = MainTabViewModel()
|
||||
@State private var backdropAnim: Bool = true
|
||||
@State private var lastBackdropAnim: Bool = false
|
||||
|
||||
var body: some View {
|
||||
ZStack {
|
||||
// please do not touch my magical crossfading. i will wave my magical github wand and cry
|
||||
if viewModel.lastBackgroundURL != nil {
|
||||
ImageView(src: viewModel.lastBackgroundURL!, bh: viewModel.backgroundBlurHash)
|
||||
.frame(minWidth: 100, maxWidth: .infinity, minHeight: 100, maxHeight: .infinity)
|
||||
.opacity(lastBackdropAnim ? 0.4 : 0)
|
||||
.ignoresSafeArea()
|
||||
}
|
||||
if viewModel.backgroundURL != nil {
|
||||
ImageView(src: viewModel.backgroundURL!, bh: viewModel.backgroundBlurHash)
|
||||
.frame(minWidth: 100, maxWidth: .infinity, minHeight: 100, maxHeight: .infinity)
|
||||
.opacity(backdropAnim ? 0.4 : 0)
|
||||
.onChange(of: viewModel.backgroundURL) { _ in
|
||||
lastBackdropAnim = true
|
||||
backdropAnim = false
|
||||
withAnimation(.linear(duration: 0.33)) {
|
||||
lastBackdropAnim = false
|
||||
backdropAnim = true
|
||||
}
|
||||
}
|
||||
.ignoresSafeArea()
|
||||
}
|
||||
|
||||
TabView(selection: $tabSelection) {
|
||||
HomeView()
|
||||
.offset(y: -1) // don't remove this. it breaks tabview on 4K displays.
|
||||
.tabItem {
|
||||
Text("Home")
|
||||
Image(systemName: "house")
|
||||
}
|
||||
.tag(Tab.home)
|
||||
|
||||
LibraryListView()
|
||||
.tabItem {
|
||||
Text("All Media")
|
||||
Image(systemName: "folder")
|
||||
}
|
||||
.tag(Tab.allMedia)
|
||||
|
||||
SettingsView(viewModel: SettingsViewModel())
|
||||
.offset(y: -1) // don't remove this. it breaks tabview on 4K displays.
|
||||
.tabItem {
|
||||
Text("Settings")
|
||||
Image(systemName: "gear")
|
||||
}
|
||||
.tag(Tab.settings)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
extension MainTabView {
|
||||
enum Tab: String {
|
||||
case home
|
||||
case allMedia
|
||||
case settings
|
||||
}
|
||||
}
|
||||
|
||||
// stream ancient dreams in a modern land by MARINA!
|
|
@ -1,37 +0,0 @@
|
|||
/* 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
|
||||
*/
|
||||
|
||||
import CoreData
|
||||
|
||||
struct PersistenceController {
|
||||
static let shared = PersistenceController()
|
||||
|
||||
let container: NSPersistentContainer
|
||||
|
||||
init(inMemory: Bool = false) {
|
||||
container = NSPersistentContainer(name: "Model")
|
||||
if inMemory {
|
||||
container.persistentStoreDescriptions.first!.url = URL(fileURLWithPath: "/dev/null")
|
||||
}
|
||||
container.loadPersistentStores(completionHandler: { (_, error) in
|
||||
if let error = error as NSError? {
|
||||
// Replace this implementation with code to handle the error appropriately.
|
||||
// fatalError() causes the application to generate a crash log and terminate. You should not use this function in a shipping application, although it may be useful during development.
|
||||
|
||||
/*
|
||||
Typical reasons for an error here include:
|
||||
* The parent directory does not exist, cannot be created, or disallows writing.
|
||||
* The persistent store is not accessible, due to permissions or data protection when the device is locked.
|
||||
* The device is out of space.
|
||||
* The store could not be migrated to the current model version.
|
||||
Check the error message to determine what the actual problem was.
|
||||
*/
|
||||
fatalError("Unresolved error \(error), \(error.userInfo)")
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
|
@ -1,4 +0,0 @@
|
|||
# Design Notes
|
||||
|
||||
tvos is dumb and how I got around the ScrollViews clipping requires ALL interface elements to have a leading and trailing padding of _~~135~~ something else but i forgot_ pt to align with the original "safe area bounds"
|
||||
|
|
@ -1,29 +0,0 @@
|
|||
//
|
||||
/*
|
||||
* SwiftFin is subject to the terms of the Mozilla Public
|
||||
* License, v2.0. 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 SwiftUI
|
||||
|
||||
struct SplashView: View {
|
||||
@StateObject var viewModel = SplashViewModel()
|
||||
|
||||
var body: some View {
|
||||
Group {
|
||||
if viewModel.isLoggedIn {
|
||||
NavigationView {
|
||||
MainTabView()
|
||||
}.padding(.all, -1)
|
||||
} else {
|
||||
NavigationView {
|
||||
ConnectToServerView()
|
||||
}
|
||||
.navigationViewStyle(StackNavigationViewStyle())
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,52 @@
|
|||
//
|
||||
/*
|
||||
* SwiftFin is subject to the terms of the Mozilla Public
|
||||
* License, v2.0. 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 Stinsen
|
||||
import SwiftUI
|
||||
|
||||
struct BasicAppSettingsView: View {
|
||||
|
||||
@EnvironmentObject var basicAppSettingsRouter: BasicAppSettingsCoordinator.Router
|
||||
@ObservedObject var viewModel: BasicAppSettingsViewModel
|
||||
@State var resetTapped: Bool = false
|
||||
|
||||
@Default(.appAppearance) var appAppearance
|
||||
|
||||
var body: some View {
|
||||
Form {
|
||||
Section {
|
||||
Picker(NSLocalizedString("Appearance", comment: ""), selection: $appAppearance) {
|
||||
ForEach(self.viewModel.appearances, id: \.self) { appearance in
|
||||
Text(appearance.localizedName).tag(appearance.rawValue)
|
||||
}
|
||||
}.onChange(of: appAppearance, perform: { _ in
|
||||
UIApplication.shared.windows.first?.overrideUserInterfaceStyle = appAppearance.style
|
||||
})
|
||||
} header: {
|
||||
Text("Accessibility")
|
||||
}
|
||||
|
||||
Button {
|
||||
resetTapped = true
|
||||
} label: {
|
||||
Text("Reset")
|
||||
}
|
||||
}
|
||||
.alert("Reset", isPresented: $resetTapped, actions: {
|
||||
Button(role: .destructive) {
|
||||
viewModel.reset()
|
||||
basicAppSettingsRouter.dismissCoordinator()
|
||||
} label: {
|
||||
Text("Reset")
|
||||
}
|
||||
})
|
||||
.navigationTitle("Settings")
|
||||
}
|
||||
}
|
|
@ -0,0 +1,73 @@
|
|||
/*
|
||||
* 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
|
||||
*/
|
||||
|
||||
import SwiftUI
|
||||
import Stinsen
|
||||
|
||||
struct ConnectToServerView: View {
|
||||
|
||||
@StateObject var viewModel = ConnectToServerViewModel()
|
||||
@State var uri = ""
|
||||
|
||||
var body: some View {
|
||||
List {
|
||||
Section {
|
||||
TextField(NSLocalizedString("Server URL", comment: ""), text: $uri)
|
||||
.disableAutocorrection(true)
|
||||
.autocapitalization(.none)
|
||||
.keyboardType(.URL)
|
||||
Button {
|
||||
viewModel.connectToServer(uri: uri)
|
||||
} label: {
|
||||
HStack {
|
||||
Text("Connect")
|
||||
Spacer()
|
||||
if viewModel.isLoading {
|
||||
ProgressView()
|
||||
}
|
||||
}
|
||||
}
|
||||
.disabled(viewModel.isLoading || uri.isEmpty)
|
||||
} header: {
|
||||
Text("Connect to a Jellyfin server")
|
||||
}
|
||||
|
||||
Section(header: Text("Local Servers")) {
|
||||
if viewModel.searching {
|
||||
ProgressView()
|
||||
}
|
||||
ForEach(viewModel.discoveredServers.sorted(by: { $0.name < $1.name }), id: \.id) { discoveredServer in
|
||||
Button(action: {
|
||||
viewModel.connectToServer(uri: discoveredServer.url.absoluteString)
|
||||
}, label: {
|
||||
HStack {
|
||||
Text(discoveredServer.name)
|
||||
.font(.headline)
|
||||
Text("• \(discoveredServer.host)")
|
||||
.font(.subheadline)
|
||||
.foregroundColor(.secondary)
|
||||
Spacer()
|
||||
if viewModel.isLoading {
|
||||
ProgressView()
|
||||
}
|
||||
}
|
||||
|
||||
})
|
||||
}
|
||||
}
|
||||
.onAppear(perform: self.viewModel.discoverServers)
|
||||
.headerProminence(.increased)
|
||||
}
|
||||
.alert(item: $viewModel.errorMessage) { _ in
|
||||
Alert(title: Text(viewModel.alertTitle),
|
||||
message: Text(viewModel.errorMessage?.displayMessage ?? "Unknown Error"),
|
||||
dismissButton: .cancel())
|
||||
}
|
||||
.navigationTitle("Connect")
|
||||
}
|
||||
}
|
|
@ -9,11 +9,14 @@
|
|||
import SwiftUI
|
||||
import JellyfinAPI
|
||||
import Combine
|
||||
import Stinsen
|
||||
|
||||
struct ContinueWatchingView: View {
|
||||
var items: [BaseItemDto]
|
||||
@Namespace private var namespace
|
||||
|
||||
var homeRouter: HomeCoordinator.Router? = RouterStore.shared.retrieve()
|
||||
|
||||
var body: some View {
|
||||
VStack(alignment: .leading) {
|
||||
if items.count > 0 {
|
||||
|
@ -25,14 +28,16 @@ struct ContinueWatchingView: View {
|
|||
LazyHStack {
|
||||
Spacer().frame(width: 45)
|
||||
ForEach(items, id: \.id) { item in
|
||||
NavigationLink(destination: LazyView { ItemView(item: item) }) {
|
||||
Button {
|
||||
self.homeRouter?.route(to: \.modalItem, item)
|
||||
} label: {
|
||||
LandscapeItemElement(item: item)
|
||||
}
|
||||
.buttonStyle(PlainNavigationLinkButtonStyle())
|
||||
}
|
||||
Spacer().frame(width: 45)
|
||||
}
|
||||
}.frame(height: 330)
|
||||
}.frame(height: 350)
|
||||
} else {
|
||||
EmptyView()
|
||||
}
|
|
@ -11,6 +11,7 @@ import Foundation
|
|||
import SwiftUI
|
||||
|
||||
struct HomeView: View {
|
||||
@EnvironmentObject var homeRouter: HomeCoordinator.Router
|
||||
@StateObject var viewModel = HomeViewModel()
|
||||
|
||||
@State var showingSettings = false
|
||||
|
@ -33,9 +34,9 @@ struct HomeView: View {
|
|||
VStack(alignment: .leading) {
|
||||
let library = viewModel.libraries.first(where: { $0.id == libraryID })
|
||||
|
||||
NavigationLink(destination: LazyView {
|
||||
LibraryView(viewModel: .init(parentID: libraryID, filters: viewModel.recentFilterSet), title: library?.name ?? "")
|
||||
}) {
|
||||
Button {
|
||||
self.homeRouter.route(to: \.modalLibrary, (.init(parentID: libraryID, filters: viewModel.recentFilterSet), title: library?.name ?? ""))
|
||||
} label: {
|
||||
HStack {
|
||||
Text("Latest \(library?.name ?? "")")
|
||||
.font(.headline)
|
|
@ -9,6 +9,19 @@ import SwiftUI
|
|||
import Introspect
|
||||
import JellyfinAPI
|
||||
|
||||
// Useless view necessary in tvOS because of iOS's implementation
|
||||
struct ItemNavigationView: View {
|
||||
private let item: BaseItemDto
|
||||
|
||||
init(item: BaseItemDto) {
|
||||
self.item = item
|
||||
}
|
||||
|
||||
var body: some View {
|
||||
ItemView(item: item)
|
||||
}
|
||||
}
|
||||
|
||||
struct ItemView: View {
|
||||
private var item: BaseItemDto
|
||||
|
|
@ -27,7 +27,7 @@ struct LatestMediaView: View {
|
|||
viewDidLoad = true
|
||||
|
||||
DispatchQueue.global(qos: .userInitiated).async {
|
||||
UserLibraryAPI.getLatestMedia(userId: SessionManager.current.user.user_id!, parentId: library_id, fields: [.primaryImageAspectRatio, .seriesPrimaryImage, .seasonUserData, .overview, .genres, .people], enableUserData: true, limit: 12)
|
||||
UserLibraryAPI.getLatestMedia(userId: SessionManager.main.currentLogin.user.id, parentId: library_id, fields: [.primaryImageAspectRatio, .seriesPrimaryImage, .seasonUserData, .overview, .genres, .people], enableUserData: true, limit: 12)
|
||||
.sink(receiveCompletion: { completion in
|
||||
print(completion)
|
||||
}, receiveValue: { response in
|
||||
|
@ -48,7 +48,7 @@ struct LatestMediaView: View {
|
|||
}
|
||||
Spacer().frame(width: 45)
|
||||
}
|
||||
}.frame(height: 396)
|
||||
}.frame(height: 480)
|
||||
.onAppear(perform: onAppear)
|
||||
}
|
||||
}
|
|
@ -0,0 +1,94 @@
|
|||
/* 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
|
||||
*/
|
||||
|
||||
import JellyfinAPI
|
||||
import Stinsen
|
||||
import SwiftUI
|
||||
|
||||
struct LibraryFilterView: View {
|
||||
|
||||
@EnvironmentObject var filterRouter: FilterCoordinator.Router
|
||||
@Binding var filters: LibraryFilters
|
||||
var parentId: String = ""
|
||||
|
||||
@StateObject var viewModel: LibraryFilterViewModel
|
||||
|
||||
init(filters: Binding<LibraryFilters>, enabledFilterType: [FilterType], parentId: String) {
|
||||
_filters = filters
|
||||
self.parentId = parentId
|
||||
_viewModel =
|
||||
StateObject(wrappedValue: .init(filters: filters.wrappedValue, enabledFilterType: enabledFilterType, parentId: parentId))
|
||||
}
|
||||
|
||||
var body: some View {
|
||||
VStack {
|
||||
if viewModel.isLoading {
|
||||
ProgressView()
|
||||
} else {
|
||||
Form {
|
||||
if viewModel.enabledFilterType.contains(.genre) {
|
||||
MultiSelector(label: NSLocalizedString("Genres", comment: ""),
|
||||
options: viewModel.possibleGenres,
|
||||
optionToString: { $0.name ?? "" },
|
||||
selected: $viewModel.modifiedFilters.withGenres)
|
||||
}
|
||||
if viewModel.enabledFilterType.contains(.filter) {
|
||||
MultiSelector(label: NSLocalizedString("Filters", comment: ""),
|
||||
options: viewModel.possibleItemFilters,
|
||||
optionToString: { $0.localized },
|
||||
selected: $viewModel.modifiedFilters.filters)
|
||||
}
|
||||
if viewModel.enabledFilterType.contains(.tag) {
|
||||
MultiSelector(label: NSLocalizedString("Tags", comment: ""),
|
||||
options: viewModel.possibleTags,
|
||||
optionToString: { $0 },
|
||||
selected: $viewModel.modifiedFilters.tags)
|
||||
}
|
||||
if viewModel.enabledFilterType.contains(.sortBy) {
|
||||
Picker(selection: $viewModel.selectedSortBy, label: Text("Sort by")) {
|
||||
ForEach(viewModel.possibleSortBys, id: \.self) { so in
|
||||
Text(so.localized).tag(so)
|
||||
}
|
||||
}
|
||||
}
|
||||
if viewModel.enabledFilterType.contains(.sortOrder) {
|
||||
Picker(selection: $viewModel.selectedSortOrder, label: Text("Display order")) {
|
||||
ForEach(viewModel.possibleSortOrders, id: \.self) { so in
|
||||
Text(so.rawValue).tag(so)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Button {
|
||||
viewModel.resetFilters()
|
||||
self.filters = viewModel.modifiedFilters
|
||||
filterRouter.dismissCoordinator()
|
||||
} label: {
|
||||
Text("Reset")
|
||||
}
|
||||
}
|
||||
}
|
||||
.toolbar {
|
||||
ToolbarItemGroup(placement: .navigationBarLeading) {
|
||||
Button {
|
||||
filterRouter.dismissCoordinator()
|
||||
} label: {
|
||||
Image(systemName: "xmark")
|
||||
}
|
||||
}
|
||||
ToolbarItemGroup(placement: .navigationBarTrailing) {
|
||||
Button {
|
||||
viewModel.updateModifiedFilter()
|
||||
self.filters = viewModel.modifiedFilters
|
||||
filterRouter.dismissCoordinator()
|
||||
} label: {
|
||||
Text("Apply")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -16,47 +16,11 @@ struct LibraryListView: View {
|
|||
var body: some View {
|
||||
ScrollView {
|
||||
LazyVStack {
|
||||
NavigationLink(destination: LazyView {
|
||||
LibraryView(viewModel: .init(filters: viewModel.withFavorites), title: "Favorites")
|
||||
}) {
|
||||
ZStack {
|
||||
HStack {
|
||||
Spacer()
|
||||
Text("Your Favorites")
|
||||
.font(.subheadline)
|
||||
.fontWeight(.semibold)
|
||||
Spacer()
|
||||
}
|
||||
}
|
||||
.padding(16)
|
||||
.frame(minWidth: 100, maxWidth: .infinity)
|
||||
}
|
||||
.cornerRadius(10)
|
||||
.shadow(radius: 5)
|
||||
.padding(.bottom, 5)
|
||||
|
||||
NavigationLink(destination: LazyView {
|
||||
Text("WIP")
|
||||
}) {
|
||||
ZStack {
|
||||
HStack {
|
||||
Spacer()
|
||||
Text("All Genres")
|
||||
.font(.subheadline)
|
||||
.fontWeight(.semibold)
|
||||
Spacer()
|
||||
}
|
||||
}
|
||||
.padding(16)
|
||||
.frame(minWidth: 100, maxWidth: .infinity)
|
||||
}
|
||||
.cornerRadius(10)
|
||||
.shadow(radius: 5)
|
||||
.padding(.bottom, 15)
|
||||
|
||||
if !viewModel.isLoading {
|
||||
ForEach(viewModel.libraries, id: \.id) { library in
|
||||
if library.collectionType ?? "" == "movies" || library.collectionType ?? "" == "tvshows" {
|
||||
if library.collectionType ?? "" == "movies" || library.collectionType ?? "" == "tvshows" || library.collectionType ?? "" == "music" {
|
||||
EmptyView()
|
||||
} else {
|
||||
NavigationLink(destination: LazyView {
|
||||
LibraryView(viewModel: .init(parentID: library.id), title: library.name ?? "")
|
||||
}) {
|
||||
|
@ -80,8 +44,6 @@ struct LibraryListView: View {
|
|||
.cornerRadius(10)
|
||||
.shadow(radius: 5)
|
||||
.padding(.bottom, 5)
|
||||
} else {
|
||||
EmptyView()
|
||||
}
|
||||
}
|
||||
} else {
|
||||
|
@ -91,15 +53,5 @@ struct LibraryListView: View {
|
|||
.padding(.trailing, 16)
|
||||
.padding(.top, 8)
|
||||
}
|
||||
.navigationTitle(NSLocalizedString("All Media", comment: ""))
|
||||
.toolbar {
|
||||
ToolbarItemGroup(placement: .navigationBarTrailing) {
|
||||
NavigationLink(destination: LazyView {
|
||||
LibrarySearchView(viewModel: .init(parentID: nil))
|
||||
}) {
|
||||
Image(systemName: "magnifyingglass")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,101 @@
|
|||
/*
|
||||
* 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
|
||||
*/
|
||||
|
||||
import SwiftUI
|
||||
import SwiftUICollection
|
||||
import JellyfinAPI
|
||||
|
||||
struct LibraryView: View {
|
||||
@EnvironmentObject var libraryRouter: LibraryCoordinator.Router
|
||||
@StateObject var viewModel: LibraryViewModel
|
||||
var title: String
|
||||
|
||||
// MARK: tracks for grid
|
||||
var defaultFilters = LibraryFilters(filters: [], sortOrder: [.ascending], withGenres: [], tags: [], sortBy: [.name])
|
||||
|
||||
@State var isShowingSearchView = false
|
||||
@State var isShowingFilterView = false
|
||||
|
||||
var body: some View {
|
||||
if viewModel.isLoading == true {
|
||||
ProgressView()
|
||||
} else if !viewModel.rows.isEmpty {
|
||||
CollectionView(rows: viewModel.rows) { _, _ in
|
||||
let itemSize = NSCollectionLayoutSize(
|
||||
widthDimension: .fractionalWidth(1),
|
||||
heightDimension: .fractionalHeight(1)
|
||||
)
|
||||
let item = NSCollectionLayoutItem(layoutSize: itemSize)
|
||||
|
||||
let groupSize = NSCollectionLayoutSize(
|
||||
widthDimension: .absolute(200),
|
||||
heightDimension: .absolute(300)
|
||||
)
|
||||
let group = NSCollectionLayoutGroup.horizontal(
|
||||
layoutSize: groupSize,
|
||||
subitems: [item]
|
||||
)
|
||||
|
||||
let header =
|
||||
NSCollectionLayoutBoundarySupplementaryItem(
|
||||
layoutSize: NSCollectionLayoutSize(
|
||||
widthDimension: .fractionalWidth(1),
|
||||
heightDimension: .absolute(44)
|
||||
),
|
||||
elementKind: UICollectionView.elementKindSectionHeader,
|
||||
alignment: .topLeading
|
||||
)
|
||||
|
||||
let section = NSCollectionLayoutSection(group: group)
|
||||
|
||||
section.contentInsets = NSDirectionalEdgeInsets(top: 30, leading: 0, bottom: 80, trailing: 80)
|
||||
section.interGroupSpacing = 48
|
||||
section.orthogonalScrollingBehavior = .continuous
|
||||
section.boundarySupplementaryItems = [header]
|
||||
return section
|
||||
} cell: { _, cell in
|
||||
GeometryReader { _ in
|
||||
if let item = cell.item {
|
||||
if item.type != "Folder" {
|
||||
Button {
|
||||
libraryRouter.route(to: \.modalItem, item)
|
||||
} label: {
|
||||
PortraitItemElement(item: item)
|
||||
}
|
||||
.buttonStyle(PlainNavigationLinkButtonStyle())
|
||||
.onAppear {
|
||||
if item == viewModel.items.last && viewModel.hasNextPage {
|
||||
viewModel.requestNextPageAsync()
|
||||
}
|
||||
}
|
||||
}
|
||||
} else if cell.loadingCell {
|
||||
ProgressView()
|
||||
.frame(minWidth: 0, maxWidth: .infinity, minHeight: 0, maxHeight: .infinity, alignment: .center)
|
||||
}
|
||||
}
|
||||
} supplementaryView: { _, indexPath in
|
||||
HStack {
|
||||
Spacer()
|
||||
}.accessibilityIdentifier("\(indexPath.section).\(indexPath.row)")
|
||||
}
|
||||
.frame(maxWidth: .infinity, maxHeight: .infinity)
|
||||
.ignoresSafeArea(.all)
|
||||
} else {
|
||||
VStack {
|
||||
Text("No results.")
|
||||
Button { } label: {
|
||||
Text("Reload")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// stream BM^S by nicki!
|
||||
//
|
|
@ -0,0 +1,89 @@
|
|||
/*
|
||||
* 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
|
||||
*/
|
||||
|
||||
import SwiftUI
|
||||
import SwiftUICollection
|
||||
import JellyfinAPI
|
||||
|
||||
struct MovieLibrariesView: View {
|
||||
@EnvironmentObject var movieLibrariesRouter: MovieLibrariesCoordinator.Router
|
||||
@StateObject var viewModel: MovieLibrariesViewModel
|
||||
var title: String
|
||||
|
||||
var body: some View {
|
||||
if viewModel.isLoading == true {
|
||||
ProgressView()
|
||||
} else if !viewModel.rows.isEmpty {
|
||||
CollectionView(rows: viewModel.rows) { _, _ in
|
||||
let itemSize = NSCollectionLayoutSize(
|
||||
widthDimension: .fractionalWidth(1),
|
||||
heightDimension: .fractionalHeight(1)
|
||||
)
|
||||
let item = NSCollectionLayoutItem(layoutSize: itemSize)
|
||||
|
||||
let groupSize = NSCollectionLayoutSize(
|
||||
widthDimension: .absolute(200),
|
||||
heightDimension: .absolute(300)
|
||||
)
|
||||
let group = NSCollectionLayoutGroup.horizontal(
|
||||
layoutSize: groupSize,
|
||||
subitems: [item]
|
||||
)
|
||||
|
||||
let header =
|
||||
NSCollectionLayoutBoundarySupplementaryItem(
|
||||
layoutSize: NSCollectionLayoutSize(
|
||||
widthDimension: .fractionalWidth(1),
|
||||
heightDimension: .absolute(44)
|
||||
),
|
||||
elementKind: UICollectionView.elementKindSectionHeader,
|
||||
alignment: .topLeading
|
||||
)
|
||||
|
||||
let section = NSCollectionLayoutSection(group: group)
|
||||
|
||||
section.contentInsets = NSDirectionalEdgeInsets(top: 30, leading: 0, bottom: 80, trailing: 80)
|
||||
section.interGroupSpacing = 48
|
||||
section.orthogonalScrollingBehavior = .continuous
|
||||
section.boundarySupplementaryItems = [header]
|
||||
return section
|
||||
} cell: { _, cell in
|
||||
GeometryReader { _ in
|
||||
if let item = cell.item {
|
||||
if item.type != "Folder" {
|
||||
Button {
|
||||
self.movieLibrariesRouter.route(to: \.library, item)
|
||||
} label: {
|
||||
PortraitItemElement(item: item)
|
||||
}
|
||||
.buttonStyle(PlainNavigationLinkButtonStyle())
|
||||
}
|
||||
} else if cell.loadingCell {
|
||||
ProgressView()
|
||||
.frame(minWidth: 0, maxWidth: .infinity, minHeight: 0, maxHeight: .infinity, alignment: .center)
|
||||
}
|
||||
}
|
||||
} supplementaryView: { _, indexPath in
|
||||
HStack {
|
||||
Spacer()
|
||||
}.accessibilityIdentifier("\(indexPath.section).\(indexPath.row)")
|
||||
}
|
||||
.frame(maxWidth: .infinity, maxHeight: .infinity)
|
||||
.ignoresSafeArea(.all)
|
||||
} else {
|
||||
VStack {
|
||||
Text("No results.")
|
||||
Button {
|
||||
print("movieLibraries reload")
|
||||
} label: {
|
||||
Text("Reload")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -9,9 +9,12 @@
|
|||
import SwiftUI
|
||||
import JellyfinAPI
|
||||
import Combine
|
||||
import Stinsen
|
||||
|
||||
struct NextUpView: View {
|
||||
var items: [BaseItemDto]
|
||||
|
||||
var homeRouter: HomeCoordinator.Router? = RouterStore.shared.retrieve()
|
||||
|
||||
var body: some View {
|
||||
VStack(alignment: .leading) {
|
||||
|
@ -24,13 +27,15 @@ struct NextUpView: View {
|
|||
LazyHStack {
|
||||
Spacer().frame(width: 45)
|
||||
ForEach(items, id: \.id) { item in
|
||||
NavigationLink(destination: LazyView { ItemView(item: item) }) {
|
||||
Button {
|
||||
self.homeRouter?.route(to: \.modalItem, item)
|
||||
} label: {
|
||||
LandscapeItemElement(item: item)
|
||||
}.buttonStyle(PlainNavigationLinkButtonStyle())
|
||||
}
|
||||
Spacer().frame(width: 45)
|
||||
}
|
||||
}.frame(height: 330)
|
||||
}.frame(height: 350)
|
||||
.offset(y: -10)
|
||||
} else {
|
||||
EmptyView()
|
|
@ -0,0 +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
|
||||
*/
|
||||
|
||||
import SwiftUI
|
||||
|
||||
struct ServerDetailView: View {
|
||||
|
||||
@ObservedObject var viewModel = ServerDetailViewModel()
|
||||
|
||||
var body: some View {
|
||||
Form {
|
||||
Section(header: Text("Server Details")) {
|
||||
HStack {
|
||||
Text("Name")
|
||||
Spacer()
|
||||
Text(SessionManager.main.currentLogin.server.name)
|
||||
.foregroundColor(.secondary)
|
||||
}
|
||||
|
||||
HStack {
|
||||
Text("URI")
|
||||
Spacer()
|
||||
Text(SessionManager.main.currentLogin.server.uri)
|
||||
.foregroundColor(.secondary)
|
||||
}
|
||||
|
||||
HStack {
|
||||
Text("Version")
|
||||
Spacer()
|
||||
Text(SessionManager.main.currentLogin.server.version)
|
||||
.foregroundColor(.secondary)
|
||||
}
|
||||
|
||||
HStack {
|
||||
Text("Operating System")
|
||||
Spacer()
|
||||
Text(SessionManager.main.currentLogin.server.os)
|
||||
.foregroundColor(.secondary)
|
||||
}
|
||||
}
|
||||
|
||||
Button(action: {
|
||||
viewModel.refreshServerLibrary()
|
||||
}, label: {
|
||||
HStack {
|
||||
Text("Refresh Library")
|
||||
.font(.callout)
|
||||
Spacer()
|
||||
if viewModel.isLoading {
|
||||
ProgressView()
|
||||
}
|
||||
}
|
||||
}).disabled(viewModel.isLoading)
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,125 @@
|
|||
//
|
||||
/*
|
||||
* SwiftFin is subject to the terms of the Mozilla Public
|
||||
* License, v2.0. 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 CoreStore
|
||||
import SwiftUI
|
||||
|
||||
struct ServerListView: View {
|
||||
|
||||
@EnvironmentObject var serverListRouter: ServerListCoordinator.Router
|
||||
@ObservedObject var viewModel: ServerListViewModel
|
||||
|
||||
@ViewBuilder
|
||||
private var listView: some View {
|
||||
ScrollView {
|
||||
LazyVStack {
|
||||
ForEach(viewModel.servers, id: \.id) { server in
|
||||
Button {
|
||||
serverListRouter.route(to: \.userList, server)
|
||||
} label: {
|
||||
HStack {
|
||||
Image(systemName: "server.rack")
|
||||
.font(.system(size: 72))
|
||||
.foregroundColor(.primary)
|
||||
|
||||
VStack(alignment: .leading, spacing: 5) {
|
||||
Text(server.name)
|
||||
.font(.title2)
|
||||
.foregroundColor(.primary)
|
||||
|
||||
Text(server.uri)
|
||||
.font(.footnote)
|
||||
.disabled(true)
|
||||
.foregroundColor(.secondary)
|
||||
|
||||
Text(viewModel.userTextFor(server: server))
|
||||
.font(.footnote)
|
||||
.foregroundColor(.primary)
|
||||
}
|
||||
|
||||
Spacer()
|
||||
}
|
||||
}
|
||||
.padding(.horizontal, 100)
|
||||
.contextMenu {
|
||||
Button(role: .destructive) {
|
||||
viewModel.remove(server: server)
|
||||
} label: {
|
||||
Label("Remove", systemImage: "trash")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
.padding(.top, 50)
|
||||
}
|
||||
.padding(.top, 50)
|
||||
}
|
||||
|
||||
@ViewBuilder
|
||||
private var noServerView: some View {
|
||||
VStack {
|
||||
Text("Connect to a Jellyfin server to get started")
|
||||
.frame(minWidth: 50, maxWidth: 500)
|
||||
.multilineTextAlignment(.center)
|
||||
.font(.callout)
|
||||
|
||||
Button {
|
||||
serverListRouter.route(to: \.connectToServer)
|
||||
} label: {
|
||||
Text("Connect")
|
||||
.bold()
|
||||
.font(.callout)
|
||||
}
|
||||
.padding(.top, 40)
|
||||
}
|
||||
}
|
||||
|
||||
@ViewBuilder
|
||||
private var innerBody: some View {
|
||||
if viewModel.servers.isEmpty {
|
||||
noServerView
|
||||
.offset(y: -50)
|
||||
} else {
|
||||
listView
|
||||
}
|
||||
}
|
||||
|
||||
@ViewBuilder
|
||||
private var trailingToolbarContent: some View {
|
||||
if viewModel.servers.isEmpty {
|
||||
EmptyView()
|
||||
} else {
|
||||
Button {
|
||||
serverListRouter.route(to: \.connectToServer)
|
||||
} label: {
|
||||
Image(systemName: "plus.circle.fill")
|
||||
}
|
||||
.contextMenu {
|
||||
Button {
|
||||
serverListRouter.route(to: \.basicAppSettings)
|
||||
} label: {
|
||||
Text("Settings")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var body: some View {
|
||||
innerBody
|
||||
.navigationTitle("Servers")
|
||||
.toolbar {
|
||||
ToolbarItemGroup(placement: .navigationBarTrailing) {
|
||||
trailingToolbarContent
|
||||
}
|
||||
}
|
||||
.onAppear {
|
||||
viewModel.fetchServers()
|
||||
}
|
||||
}
|
||||
}
|
|
@ -8,10 +8,10 @@
|
|||
import CoreData
|
||||
import SwiftUI
|
||||
import Defaults
|
||||
import JellyfinAPI
|
||||
|
||||
struct SettingsView: View {
|
||||
@Environment(\.managedObjectContext) private var viewContext
|
||||
|
||||
|
||||
@ObservedObject var viewModel: SettingsViewModel
|
||||
|
||||
@Default(.inNetworkBandwidth) var inNetworkStreamBitrate
|
||||
|
@ -19,11 +19,6 @@ struct SettingsView: View {
|
|||
@Default(.isAutoSelectSubtitles) var isAutoSelectSubtitles
|
||||
@Default(.autoSelectSubtitlesLangCode) var autoSelectSubtitlesLangcode
|
||||
@Default(.autoSelectAudioLangCode) var autoSelectAudioLangcode
|
||||
@State private var username: String = ""
|
||||
|
||||
func onAppear() {
|
||||
username = SessionManager.current.user?.username ?? ""
|
||||
}
|
||||
|
||||
var body: some View {
|
||||
Form {
|
||||
|
@ -61,30 +56,23 @@ struct SettingsView: View {
|
|||
)
|
||||
}
|
||||
|
||||
Section(header: Text(ServerEnvironment.current.server.name ?? "")) {
|
||||
Section(header: Text(SessionManager.main.currentLogin.server.name)) {
|
||||
HStack {
|
||||
Text("Signed in as \(username)").foregroundColor(.primary)
|
||||
Text("Signed in as \(SessionManager.main.currentLogin.user.username)").foregroundColor(.primary)
|
||||
Spacer()
|
||||
Button {
|
||||
DispatchQueue.main.asyncAfter(deadline: .now() + 0.1) {
|
||||
let nc = NotificationCenter.default
|
||||
nc.post(name: Notification.Name("didSignOut"), object: nil)
|
||||
}
|
||||
SwiftfinNotificationCenter.main.post(name: SwiftfinNotificationCenter.Keys.didSignOut, object: nil)
|
||||
} label: {
|
||||
Text("Switch user").font(.callout)
|
||||
}
|
||||
}
|
||||
Button {
|
||||
DispatchQueue.main.asyncAfter(deadline: .now() + 0.1) {
|
||||
SessionManager.current.logout()
|
||||
let nc = NotificationCenter.default
|
||||
nc.post(name: Notification.Name("didSignOut"), object: nil)
|
||||
}
|
||||
SessionManager.main.logout()
|
||||
} label: {
|
||||
Text("Sign out").font(.callout)
|
||||
}
|
||||
}
|
||||
}.onAppear(perform: onAppear)
|
||||
}
|
||||
.padding(.leading, 90)
|
||||
.padding(.trailing, 90)
|
||||
}
|
|
@ -0,0 +1,89 @@
|
|||
/*
|
||||
* 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
|
||||
*/
|
||||
|
||||
import SwiftUI
|
||||
import SwiftUICollection
|
||||
import JellyfinAPI
|
||||
|
||||
struct TVLibrariesView: View {
|
||||
@EnvironmentObject var tvLibrariesRouter: TVLibrariesCoordinator.Router
|
||||
@StateObject var viewModel: TVLibrariesViewModel
|
||||
var title: String
|
||||
|
||||
var body: some View {
|
||||
if viewModel.isLoading == true {
|
||||
ProgressView()
|
||||
} else if !viewModel.rows.isEmpty {
|
||||
CollectionView(rows: viewModel.rows) { _, _ in
|
||||
let itemSize = NSCollectionLayoutSize(
|
||||
widthDimension: .fractionalWidth(1),
|
||||
heightDimension: .fractionalHeight(1)
|
||||
)
|
||||
let item = NSCollectionLayoutItem(layoutSize: itemSize)
|
||||
|
||||
let groupSize = NSCollectionLayoutSize(
|
||||
widthDimension: .absolute(200),
|
||||
heightDimension: .absolute(300)
|
||||
)
|
||||
let group = NSCollectionLayoutGroup.horizontal(
|
||||
layoutSize: groupSize,
|
||||
subitems: [item]
|
||||
)
|
||||
|
||||
let header =
|
||||
NSCollectionLayoutBoundarySupplementaryItem(
|
||||
layoutSize: NSCollectionLayoutSize(
|
||||
widthDimension: .fractionalWidth(1),
|
||||
heightDimension: .absolute(44)
|
||||
),
|
||||
elementKind: UICollectionView.elementKindSectionHeader,
|
||||
alignment: .topLeading
|
||||
)
|
||||
|
||||
let section = NSCollectionLayoutSection(group: group)
|
||||
|
||||
section.contentInsets = NSDirectionalEdgeInsets(top: 30, leading: 0, bottom: 80, trailing: 80)
|
||||
section.interGroupSpacing = 48
|
||||
section.orthogonalScrollingBehavior = .continuous
|
||||
section.boundarySupplementaryItems = [header]
|
||||
return section
|
||||
} cell: { _, cell in
|
||||
GeometryReader { _ in
|
||||
if let item = cell.item {
|
||||
if item.type != "Folder" {
|
||||
Button {
|
||||
self.tvLibrariesRouter.route(to: \.library, item)
|
||||
} label: {
|
||||
PortraitItemElement(item: item)
|
||||
}
|
||||
.buttonStyle(PlainNavigationLinkButtonStyle())
|
||||
}
|
||||
} else if cell.loadingCell {
|
||||
ProgressView()
|
||||
.frame(minWidth: 0, maxWidth: .infinity, minHeight: 0, maxHeight: .infinity, alignment: .center)
|
||||
}
|
||||
}
|
||||
} supplementaryView: { _, indexPath in
|
||||
HStack {
|
||||
Spacer()
|
||||
}.accessibilityIdentifier("\(indexPath.section).\(indexPath.row)")
|
||||
}
|
||||
.frame(maxWidth: .infinity, maxHeight: .infinity)
|
||||
.ignoresSafeArea(.all)
|
||||
} else {
|
||||
VStack {
|
||||
Text("No results.")
|
||||
Button {
|
||||
print("tvLibraries reload")
|
||||
} label: {
|
||||
Text("Reload")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,107 @@
|
|||
//
|
||||
/*
|
||||
* SwiftFin is subject to the terms of the Mozilla Public
|
||||
* License, v2.0. 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 SwiftUI
|
||||
|
||||
struct UserListView: View {
|
||||
|
||||
@EnvironmentObject var userListRouter: UserListCoordinator.Router
|
||||
@ObservedObject var viewModel: UserListViewModel
|
||||
|
||||
@ViewBuilder
|
||||
private var listView: some View {
|
||||
ScrollView {
|
||||
LazyVStack {
|
||||
ForEach(viewModel.users, id: \.id) { user in
|
||||
Button {
|
||||
viewModel.login(user: user)
|
||||
} label: {
|
||||
HStack {
|
||||
Text(user.username)
|
||||
.font(.title2)
|
||||
|
||||
Spacer()
|
||||
|
||||
if viewModel.isLoading {
|
||||
ProgressView()
|
||||
}
|
||||
}
|
||||
}
|
||||
.padding(.horizontal, 100)
|
||||
.contextMenu {
|
||||
Button(role: .destructive) {
|
||||
viewModel.remove(user: user)
|
||||
} label: {
|
||||
Label("Remove", systemImage: "trash")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
.padding(.top, 50)
|
||||
}
|
||||
.padding(.top, 50)
|
||||
}
|
||||
|
||||
@ViewBuilder
|
||||
private var noUserView: some View {
|
||||
VStack {
|
||||
Text("Sign in to get started")
|
||||
.frame(minWidth: 50, maxWidth: 500)
|
||||
.multilineTextAlignment(.center)
|
||||
.font(.callout)
|
||||
|
||||
Button {
|
||||
userListRouter.route(to: \.userSignIn, viewModel.server)
|
||||
} label: {
|
||||
Text("Sign in")
|
||||
.bold()
|
||||
.font(.callout)
|
||||
}
|
||||
.padding(.top, 40)
|
||||
}
|
||||
}
|
||||
|
||||
@ViewBuilder
|
||||
private var innerBody: some View {
|
||||
if viewModel.users.isEmpty {
|
||||
noUserView
|
||||
.offset(y: -50)
|
||||
} else {
|
||||
listView
|
||||
}
|
||||
}
|
||||
|
||||
@ViewBuilder
|
||||
private var toolbarContent: some View {
|
||||
if viewModel.users.isEmpty {
|
||||
EmptyView()
|
||||
} else {
|
||||
HStack {
|
||||
Button {
|
||||
userListRouter.route(to: \.userSignIn, viewModel.server)
|
||||
} label: {
|
||||
Image(systemName: "person.crop.circle.fill.badge.plus")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var body: some View {
|
||||
innerBody
|
||||
.navigationTitle(viewModel.server.name)
|
||||
.toolbar {
|
||||
ToolbarItem(placement: .navigationBarTrailing) {
|
||||
toolbarContent
|
||||
}
|
||||
}
|
||||
.onAppear {
|
||||
viewModel.fetchUsers()
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,55 @@
|
|||
//
|
||||
/*
|
||||
* SwiftFin is subject to the terms of the Mozilla Public
|
||||
* License, v2.0. 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 SwiftUI
|
||||
import Stinsen
|
||||
|
||||
struct UserSignInView: View {
|
||||
|
||||
@ObservedObject var viewModel: UserSignInViewModel
|
||||
@State private var username: String = ""
|
||||
@State private var password: String = ""
|
||||
|
||||
var body: some View {
|
||||
Form {
|
||||
|
||||
Section {
|
||||
TextField("Username", text: $username)
|
||||
.disableAutocorrection(true)
|
||||
.autocapitalization(.none)
|
||||
|
||||
SecureField("Password", text: $password)
|
||||
.disableAutocorrection(true)
|
||||
.autocapitalization(.none)
|
||||
|
||||
Button {
|
||||
viewModel.login(username: username, password: password)
|
||||
} label: {
|
||||
HStack {
|
||||
Text("Connect")
|
||||
Spacer()
|
||||
if viewModel.isLoading {
|
||||
ProgressView()
|
||||
}
|
||||
}
|
||||
}
|
||||
.disabled(viewModel.isLoading || username.isEmpty)
|
||||
|
||||
} header: {
|
||||
Text("Sign In to \(viewModel.server.name)")
|
||||
}
|
||||
}
|
||||
.alert(item: $viewModel.errorMessage) { _ in
|
||||
Alert(title: Text(viewModel.alertTitle),
|
||||
message: Text(viewModel.errorMessage?.displayMessage ?? "Unknown Error"),
|
||||
dismissButton: .cancel())
|
||||
}
|
||||
.navigationTitle("Sign In")
|
||||
}
|
||||
}
|
|
@ -138,15 +138,13 @@ class VideoPlayerViewController: UIViewController, VideoPlayerSettingsDelegate,
|
|||
let builder = DeviceProfileBuilder()
|
||||
builder.setMaxBitrate(bitrate: maxBitrate)
|
||||
let profile = builder.buildProfile()
|
||||
|
||||
let currentUser = SessionManager.main.currentLogin.user
|
||||
|
||||
guard let currentUser = SessionManager.current.user else {
|
||||
return
|
||||
}
|
||||
|
||||
let playbackInfo = PlaybackInfoDto(userId: currentUser.user_id ?? "", maxStreamingBitrate: Int(maxBitrate), startTimeTicks: manifest.userData?.playbackPositionTicks ?? 0, deviceProfile: profile, autoOpenLiveStream: true)
|
||||
let playbackInfo = PlaybackInfoDto(userId: currentUser.id, maxStreamingBitrate: Int(maxBitrate), startTimeTicks: manifest.userData?.playbackPositionTicks ?? 0, deviceProfile: profile, autoOpenLiveStream: true)
|
||||
|
||||
DispatchQueue.global(qos: .userInitiated).async { [self] in
|
||||
MediaInfoAPI.getPostedPlaybackInfo(itemId: manifest.id!, userId: currentUser.user_id ?? "", maxStreamingBitrate: Int(maxBitrate), startTimeTicks: manifest.userData?.playbackPositionTicks ?? 0, autoOpenLiveStream: true, playbackInfoDto: playbackInfo)
|
||||
MediaInfoAPI.getPostedPlaybackInfo(itemId: manifest.id!, userId: currentUser.id, maxStreamingBitrate: Int(maxBitrate), startTimeTicks: manifest.userData?.playbackPositionTicks ?? 0, autoOpenLiveStream: true, playbackInfoDto: playbackInfo)
|
||||
.sink(receiveCompletion: { result in
|
||||
print(result)
|
||||
}, receiveValue: { [self] response in
|
||||
|
@ -166,12 +164,13 @@ class VideoPlayerViewController: UIViewController, VideoPlayerSettingsDelegate,
|
|||
// Item is being transcoded by request of server
|
||||
if let transcodiungUrl = mediaSource.transcodingUrl {
|
||||
item.videoType = .transcode
|
||||
streamURL = URL(string: "\(ServerEnvironment.current.server.baseURI!)\(transcodiungUrl)")!
|
||||
streamURL = URL(string: "\(SessionManager.main.currentLogin.server.uri)\(transcodiungUrl)")!
|
||||
}
|
||||
// Item will be directly played by the client
|
||||
else {
|
||||
item.videoType = .directPlay
|
||||
streamURL = URL(string: "\(ServerEnvironment.current.server.baseURI!)/Videos/\(manifest.id!)/stream?Static=true&mediaSourceId=\(manifest.id!)&deviceId=\(SessionManager.current.deviceID)&api_key=\(SessionManager.current.accessToken)&Tag=\(mediaSource.eTag!)")!
|
||||
// streamURL = URL(string: "\(SessionManager.main.currentLogin.server.uri)/Videos/\(manifest.id!)/stream?Static=true&mediaSourceId=\(manifest.id!)&deviceId=\(SessionManager.current.deviceID)&api_key=\(SessionManager.current.accessToken)&Tag=\(mediaSource.eTag!)")!
|
||||
streamURL = URL(string: "\(SessionManager.main.currentLogin.server.uri)/Videos/\(manifest.id!)/stream?Static=true&mediaSourceId=\(manifest.id!)&Tag=\(mediaSource.eTag ?? "")")!
|
||||
}
|
||||
|
||||
item.videoUrl = streamURL
|
||||
|
@ -186,7 +185,7 @@ class VideoPlayerViewController: UIViewController, VideoPlayerSettingsDelegate,
|
|||
var deliveryUrl: URL?
|
||||
|
||||
if stream.deliveryMethod == .external {
|
||||
deliveryUrl = URL(string: "\(ServerEnvironment.current.server.baseURI!)\(stream.deliveryUrl!)")!
|
||||
deliveryUrl = URL(string: "\(SessionManager.main.currentLogin.server.uri)\(stream.deliveryUrl!)")!
|
||||
}
|
||||
|
||||
let subtitle = Subtitle(name: stream.displayTitle ?? "Unknown", id: Int32(stream.index!), url: deliveryUrl, delivery: stream.deliveryMethod!, codec: stream.codec ?? "webvtt", languageCode: stream.language ?? "")
|
|
@ -20,39 +20,30 @@
|
|||
}
|
||||
},
|
||||
{
|
||||
"package": "combine-schedulers",
|
||||
"repositoryURL": "https://github.com/pointfreeco/combine-schedulers",
|
||||
"package": "CombineExt",
|
||||
"repositoryURL": "https://github.com/CombineCommunity/CombineExt",
|
||||
"state": {
|
||||
"branch": null,
|
||||
"revision": "4cf088c29a20f52be0f2ca54992b492c54e0076b",
|
||||
"version": "0.5.3"
|
||||
"revision": "0880829102152185190064fd17847a7c681d2127",
|
||||
"version": "1.5.1"
|
||||
}
|
||||
},
|
||||
{
|
||||
"package": "CombineExt",
|
||||
"repositoryURL": "https://github.com/acvigue/CombineExt",
|
||||
"package": "CoreStore",
|
||||
"repositoryURL": "https://github.com/JohnEstropia/CoreStore.git",
|
||||
"state": {
|
||||
"branch": "main",
|
||||
"revision": "f629c5b052d1cb5d03e10890deccc50e4c649e68",
|
||||
"version": null
|
||||
"branch": null,
|
||||
"revision": "496145761ab30e8cf1c44220c0882b95e6b41077",
|
||||
"version": "8.1.0"
|
||||
}
|
||||
},
|
||||
{
|
||||
"package": "Defaults",
|
||||
"repositoryURL": "https://github.com/acvigue/Defaults",
|
||||
"state": {
|
||||
"branch": "main",
|
||||
"revision": "a4153b523ab3df9f5e3f70e9cfe9c54bed98c7e3",
|
||||
"version": null
|
||||
}
|
||||
},
|
||||
{
|
||||
"package": "Gifu",
|
||||
"repositoryURL": "https://github.com/kaishin/Gifu",
|
||||
"repositoryURL": "https://github.com/sindresorhus/Defaults",
|
||||
"state": {
|
||||
"branch": null,
|
||||
"revision": "51f2eab32903e336f590c013267cfa4d7f8b06c4",
|
||||
"version": "3.3.1"
|
||||
"revision": "55f3302c3ab30a8760f10042d0ebc0a6907f865a",
|
||||
"version": "6.1.0"
|
||||
}
|
||||
},
|
||||
{
|
||||
|
@ -64,31 +55,13 @@
|
|||
"version": null
|
||||
}
|
||||
},
|
||||
{
|
||||
"package": "KeychainSwift",
|
||||
"repositoryURL": "https://github.com/evgenyneu/keychain-swift",
|
||||
"state": {
|
||||
"branch": null,
|
||||
"revision": "96fb84f45a96630e7583903bd7e08cf095c7a7ef",
|
||||
"version": "19.0.0"
|
||||
}
|
||||
},
|
||||
{
|
||||
"package": "Nuke",
|
||||
"repositoryURL": "https://github.com/kean/Nuke.git",
|
||||
"repositoryURL": "https://github.com/kean/Nuke",
|
||||
"state": {
|
||||
"branch": null,
|
||||
"revision": "0db18dd34998cca18e9a28bcee136f84518007a0",
|
||||
"version": "10.4.1"
|
||||
}
|
||||
},
|
||||
{
|
||||
"package": "NukeUI",
|
||||
"repositoryURL": "https://github.com/kean/NukeUI",
|
||||
"state": {
|
||||
"branch": null,
|
||||
"revision": "d2580b8d22b29c6244418d8e4b568f3162191460",
|
||||
"version": "0.3.0"
|
||||
"revision": "7f73ceaeacd5df75a7994cd82e165ad9ff1815db",
|
||||
"version": "9.6.1"
|
||||
}
|
||||
},
|
||||
{
|
||||
|
@ -145,31 +118,13 @@
|
|||
"version": null
|
||||
}
|
||||
},
|
||||
{
|
||||
"package": "SwiftUIFocusGuide",
|
||||
"repositoryURL": "https://github.com/rmnblm/SwiftUIFocusGuide",
|
||||
"state": {
|
||||
"branch": null,
|
||||
"revision": "fb8eefaccb2954efedc19a5539241f370baa4a10",
|
||||
"version": "0.1.0"
|
||||
}
|
||||
},
|
||||
{
|
||||
"package": "SwiftyJSON",
|
||||
"repositoryURL": "https://github.com/SwiftyJSON/SwiftyJSON",
|
||||
"state": {
|
||||
"branch": "master",
|
||||
"revision": "b3dcd7dbd0d488e1a7077cb33b00f2083e382f07",
|
||||
"version": null
|
||||
}
|
||||
},
|
||||
{
|
||||
"package": "xctest-dynamic-overlay",
|
||||
"repositoryURL": "https://github.com/pointfreeco/xctest-dynamic-overlay",
|
||||
"state": {
|
||||
"branch": null,
|
||||
"revision": "50a70a9d3583fe228ce672e8923010c8df2deddd",
|
||||
"version": "0.2.1"
|
||||
"revision": "b3dcd7dbd0d488e1a7077cb33b00f2083e382f07",
|
||||
"version": "5.0.1"
|
||||
}
|
||||
}
|
||||
]
|
||||
|
|
|
@ -0,0 +1,27 @@
|
|||
//
|
||||
/*
|
||||
* SwiftFin is subject to the terms of the Mozilla Public
|
||||
* License, v2.0. 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 SwiftUI
|
||||
import UIKit
|
||||
|
||||
class AppDelegate: NSObject, UIApplicationDelegate {
|
||||
static var orientationLock = UIInterfaceOrientationMask.all
|
||||
|
||||
func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey : Any]? = nil) -> Bool {
|
||||
|
||||
// Lazily initialize datastack
|
||||
let _ = SwiftfinStore.dataStack
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
func application(_ application: UIApplication, supportedInterfaceOrientationsFor window: UIWindow?) -> UIInterfaceOrientationMask {
|
||||
AppDelegate.orientationLock
|
||||
}
|
||||
}
|
|
@ -0,0 +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
|
||||
*/
|
||||
|
||||
import SwiftUI
|
||||
import MessageUI
|
||||
|
||||
class EmailHelper: NSObject, MFMailComposeViewControllerDelegate {
|
||||
|
||||
public static let shared = EmailHelper()
|
||||
|
||||
override private init() { }
|
||||
|
||||
func sendLogs(logURL: URL) {
|
||||
if !MFMailComposeViewController.canSendMail() {
|
||||
// Utilities.showErrorBanner(title: "No mail account found", subtitle: "Please setup a mail account")
|
||||
return // EXIT
|
||||
}
|
||||
|
||||
let picker = MFMailComposeViewController()
|
||||
|
||||
let fileManager = FileManager()
|
||||
let data = fileManager.contents(atPath: logURL.path)
|
||||
|
||||
picker.setSubject("[DEV-BUG] SwiftFin")
|
||||
picker
|
||||
.setMessageBody("Please don't edit this email.\n Please don't change the subject. \nUDID: \(UIDevice.current.identifierForVendor?.uuidString ?? "NIL")\n",
|
||||
isHTML: false)
|
||||
picker.setToRecipients(["SwiftFin Bug Reports <swiftfin-bugs@jellyfin.org>"])
|
||||
picker.addAttachmentData(data!, mimeType: "text/plain", fileName: logURL.lastPathComponent)
|
||||
picker.mailComposeDelegate = self
|
||||
|
||||
EmailHelper.getRootViewController()?.present(picker, animated: true, completion: nil)
|
||||
}
|
||||
|
||||
func mailComposeController(_ controller: MFMailComposeViewController, didFinishWith result: MFMailComposeResult, error: Error?) {
|
||||
EmailHelper.getRootViewController()?.dismiss(animated: true, completion: nil)
|
||||
}
|
||||
|
||||
static func getRootViewController() -> UIViewController? {
|
||||
UIApplication.shared.windows.first?.rootViewController
|
||||
}
|
||||
}
|
||||
|
||||
// A view modifier that detects shaking and calls a function of our choosing.
|
||||
struct DeviceShakeViewModifier: ViewModifier {
|
||||
let action: () -> Void
|
||||
|
||||
func body(content: Self.Content) -> some View {
|
||||
content
|
||||
.onAppear()
|
||||
.onReceive(NotificationCenter.default.publisher(for: UIDevice.deviceDidShakeNotification)) { _ in
|
||||
action()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// A View extension to make the modifier easier to use.
|
||||
extension View {
|
||||
func onShake(perform action: @escaping () -> Void) -> some View {
|
||||
modifier(DeviceShakeViewModifier(action: action))
|
||||
}
|
||||
}
|
||||
|
||||
// The notification we'll send when a shake gesture happens.
|
||||
extension UIDevice {
|
||||
static let deviceDidShakeNotification = Notification.Name(rawValue: "deviceDidShakeNotification")
|
||||
}
|
||||
|
||||
// Override the default behavior of shake gestures to send our notification instead.
|
||||
extension UIWindow {
|
||||
override open func motionEnded(_ motion: UIEvent.EventSubtype, with event: UIEvent?) {
|
||||
if motion == .motionShake {
|
||||
NotificationCenter.default.post(name: UIDevice.deviceDidShakeNotification, object: nil)
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,63 @@
|
|||
/* 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
|
||||
*/
|
||||
|
||||
import Defaults
|
||||
import MessageUI
|
||||
import Stinsen
|
||||
import SwiftUI
|
||||
|
||||
// MARK: JellyfinPlayerApp
|
||||
@main
|
||||
struct JellyfinPlayerApp: App {
|
||||
|
||||
@UIApplicationDelegateAdaptor(AppDelegate.self) var appDelegate
|
||||
@Default(.appAppearance) var appAppearance
|
||||
|
||||
var body: some Scene {
|
||||
WindowGroup {
|
||||
EmptyView()
|
||||
.ignoresSafeArea()
|
||||
.onAppear {
|
||||
setupAppearance()
|
||||
}
|
||||
.withHostingWindow { window in
|
||||
window?.rootViewController = PreferenceUIHostingController(wrappedView: MainCoordinator().view())
|
||||
}
|
||||
.onShake {
|
||||
EmailHelper.shared.sendLogs(logURL: LogManager.shared.logFileURL())
|
||||
}
|
||||
.onOpenURL { url in
|
||||
AppURLHandler.shared.processDeepLink(url: url)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private func setupAppearance() {
|
||||
UIApplication.shared.windows.first?.overrideUserInterfaceStyle = appAppearance.style
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: Hosting Window
|
||||
struct HostingWindowFinder: UIViewRepresentable {
|
||||
var callback: (UIWindow?) -> Void
|
||||
|
||||
func makeUIView(context: Context) -> UIView {
|
||||
let view = UIView()
|
||||
DispatchQueue.main.async { [weak view] in
|
||||
callback(view?.window)
|
||||
}
|
||||
return view
|
||||
}
|
||||
|
||||
func updateUIView(_ uiView: UIView, context: Context) {}
|
||||
}
|
||||
|
||||
extension View {
|
||||
func withHostingWindow(_ callback: @escaping (UIWindow?) -> Void) -> some View {
|
||||
background(HostingWindowFinder(callback: callback))
|
||||
}
|
||||
}
|
|
@ -0,0 +1,118 @@
|
|||
//
|
||||
/*
|
||||
* SwiftFin is subject to the terms of the Mozilla Public
|
||||
* License, v2.0. 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
|
||||
import SwiftUI
|
||||
|
||||
// MARK: PreferenceUIHostingController
|
||||
class PreferenceUIHostingController: UIHostingController<AnyView> {
|
||||
init<V: View>(wrappedView: V) {
|
||||
let box = Box()
|
||||
super.init(rootView: AnyView(wrappedView
|
||||
.onPreferenceChange(PrefersHomeIndicatorAutoHiddenPreferenceKey.self) {
|
||||
box.value?._prefersHomeIndicatorAutoHidden = $0
|
||||
}.onPreferenceChange(SupportedOrientationsPreferenceKey.self) {
|
||||
box.value?._orientations = $0
|
||||
}.onPreferenceChange(ViewPreferenceKey.self) {
|
||||
box.value?._viewPreference = $0
|
||||
}))
|
||||
box.value = self
|
||||
}
|
||||
|
||||
@objc dynamic required init?(coder aDecoder: NSCoder) {
|
||||
super.init(coder: aDecoder)
|
||||
super.modalPresentationStyle = .fullScreen
|
||||
}
|
||||
|
||||
private class Box {
|
||||
weak var value: PreferenceUIHostingController?
|
||||
init() {}
|
||||
}
|
||||
|
||||
// MARK: Prefers Home Indicator Auto Hidden
|
||||
|
||||
public var _prefersHomeIndicatorAutoHidden = false {
|
||||
didSet { setNeedsUpdateOfHomeIndicatorAutoHidden() }
|
||||
}
|
||||
|
||||
override var prefersHomeIndicatorAutoHidden: Bool {
|
||||
_prefersHomeIndicatorAutoHidden
|
||||
}
|
||||
|
||||
// MARK: Lock orientation
|
||||
|
||||
public var _orientations: UIInterfaceOrientationMask = .allButUpsideDown {
|
||||
didSet {
|
||||
if _orientations == .landscape {
|
||||
let value = UIInterfaceOrientation.landscapeRight.rawValue
|
||||
UIDevice.current.setValue(value, forKey: "orientation")
|
||||
UIViewController.attemptRotationToDeviceOrientation()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override var supportedInterfaceOrientations: UIInterfaceOrientationMask {
|
||||
_orientations
|
||||
}
|
||||
|
||||
public var _viewPreference: UIUserInterfaceStyle = .unspecified {
|
||||
didSet {
|
||||
overrideUserInterfaceStyle = _viewPreference
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: Preference Keys
|
||||
struct PrefersHomeIndicatorAutoHiddenPreferenceKey: PreferenceKey {
|
||||
typealias Value = Bool
|
||||
|
||||
static var defaultValue: Value = false
|
||||
|
||||
static func reduce(value: inout Value, nextValue: () -> Value) {
|
||||
value = nextValue() || value
|
||||
}
|
||||
}
|
||||
|
||||
struct ViewPreferenceKey: PreferenceKey {
|
||||
typealias Value = UIUserInterfaceStyle
|
||||
|
||||
static var defaultValue: UIUserInterfaceStyle = .unspecified
|
||||
|
||||
static func reduce(value: inout UIUserInterfaceStyle, nextValue: () -> UIUserInterfaceStyle) {
|
||||
value = nextValue()
|
||||
}
|
||||
}
|
||||
|
||||
struct SupportedOrientationsPreferenceKey: PreferenceKey {
|
||||
typealias Value = UIInterfaceOrientationMask
|
||||
static var defaultValue: UIInterfaceOrientationMask = .allButUpsideDown
|
||||
|
||||
static func reduce(value: inout UIInterfaceOrientationMask, nextValue: () -> UIInterfaceOrientationMask) {
|
||||
// use the most restrictive set from the stack
|
||||
value.formIntersection(nextValue())
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: Preference Key View Extension
|
||||
extension View {
|
||||
// Controls the application's preferred home indicator auto-hiding when this view is shown.
|
||||
func prefersHomeIndicatorAutoHidden(_ value: Bool) -> some View {
|
||||
preference(key: PrefersHomeIndicatorAutoHiddenPreferenceKey.self, value: value)
|
||||
}
|
||||
|
||||
func supportedOrientations(_ supportedOrientations: UIInterfaceOrientationMask) -> some View {
|
||||
// When rendered, export the requested orientations upward to Root
|
||||
preference(key: SupportedOrientationsPreferenceKey.self, value: supportedOrientations)
|
||||
}
|
||||
|
||||
func overrideViewPreference(_ viewPreference: UIUserInterfaceStyle) -> some View {
|
||||
// When rendered, export the requested orientations upward to Root
|
||||
preference(key: ViewPreferenceKey.self, value: viewPreference)
|
||||
}
|
||||
}
|
|
@ -82,7 +82,7 @@ extension AppURLHandler {
|
|||
// It would be nice if the ItemViewModel could be initialized to id later.
|
||||
getItem(userID: userID, itemID: itemID) { item in
|
||||
guard let item = item else { return }
|
||||
NotificationCenter.default.post(name: Notification.Name("processDeepLink"), object: DeepLink.item(item))
|
||||
SwiftfinNotificationCenter.main.post(name: SwiftfinNotificationCenter.Keys.processDeepLink, object: DeepLink.item(item))
|
||||
}
|
||||
|
||||
return true
|
After Width: | Height: | Size: 7.7 KiB |
After Width: | Height: | Size: 226 KiB |
After Width: | Height: | Size: 9.2 KiB |
After Width: | Height: | Size: 9.7 KiB |
After Width: | Height: | Size: 12 KiB |
After Width: | Height: | Size: 13 KiB |
After Width: | Height: | Size: 15 KiB |
After Width: | Height: | Size: 17 KiB |
After Width: | Height: | Size: 893 B |
After Width: | Height: | Size: 1.5 KiB |
After Width: | Height: | Size: 2.4 KiB |
After Width: | Height: | Size: 3.4 KiB |
After Width: | Height: | Size: 3.9 KiB |
After Width: | Height: | Size: 4.0 KiB |
After Width: | Height: | Size: 4.3 KiB |
After Width: | Height: | Size: 5.4 KiB |
After Width: | Height: | Size: 5.6 KiB |
After Width: | Height: | Size: 5.9 KiB |
After Width: | Height: | Size: 6.5 KiB |
|
@ -0,0 +1 @@
|
|||
{"images":[{"size":"60x60","expected-size":"180","filename":"180.png","folder":"Assets.xcassets/AppIcon.appiconset/","idiom":"iphone","scale":"3x"},{"size":"40x40","expected-size":"80","filename":"80.png","folder":"Assets.xcassets/AppIcon.appiconset/","idiom":"iphone","scale":"2x"},{"size":"40x40","expected-size":"120","filename":"120.png","folder":"Assets.xcassets/AppIcon.appiconset/","idiom":"iphone","scale":"3x"},{"size":"60x60","expected-size":"120","filename":"120.png","folder":"Assets.xcassets/AppIcon.appiconset/","idiom":"iphone","scale":"2x"},{"size":"57x57","expected-size":"57","filename":"57.png","folder":"Assets.xcassets/AppIcon.appiconset/","idiom":"iphone","scale":"1x"},{"size":"29x29","expected-size":"58","filename":"58.png","folder":"Assets.xcassets/AppIcon.appiconset/","idiom":"iphone","scale":"2x"},{"size":"29x29","expected-size":"29","filename":"29.png","folder":"Assets.xcassets/AppIcon.appiconset/","idiom":"iphone","scale":"1x"},{"size":"29x29","expected-size":"87","filename":"87.png","folder":"Assets.xcassets/AppIcon.appiconset/","idiom":"iphone","scale":"3x"},{"size":"57x57","expected-size":"114","filename":"114.png","folder":"Assets.xcassets/AppIcon.appiconset/","idiom":"iphone","scale":"2x"},{"size":"20x20","expected-size":"40","filename":"40.png","folder":"Assets.xcassets/AppIcon.appiconset/","idiom":"iphone","scale":"2x"},{"size":"20x20","expected-size":"60","filename":"60.png","folder":"Assets.xcassets/AppIcon.appiconset/","idiom":"iphone","scale":"3x"},{"size":"1024x1024","filename":"1024.png","expected-size":"1024","idiom":"ios-marketing","folder":"Assets.xcassets/AppIcon.appiconset/","scale":"1x"},{"size":"40x40","expected-size":"80","filename":"80.png","folder":"Assets.xcassets/AppIcon.appiconset/","idiom":"ipad","scale":"2x"},{"size":"72x72","expected-size":"72","filename":"72.png","folder":"Assets.xcassets/AppIcon.appiconset/","idiom":"ipad","scale":"1x"},{"size":"76x76","expected-size":"152","filename":"152.png","folder":"Assets.xcassets/AppIcon.appiconset/","idiom":"ipad","scale":"2x"},{"size":"50x50","expected-size":"100","filename":"100.png","folder":"Assets.xcassets/AppIcon.appiconset/","idiom":"ipad","scale":"2x"},{"size":"29x29","expected-size":"58","filename":"58.png","folder":"Assets.xcassets/AppIcon.appiconset/","idiom":"ipad","scale":"2x"},{"size":"76x76","expected-size":"76","filename":"76.png","folder":"Assets.xcassets/AppIcon.appiconset/","idiom":"ipad","scale":"1x"},{"size":"29x29","expected-size":"29","filename":"29.png","folder":"Assets.xcassets/AppIcon.appiconset/","idiom":"ipad","scale":"1x"},{"size":"50x50","expected-size":"50","filename":"50.png","folder":"Assets.xcassets/AppIcon.appiconset/","idiom":"ipad","scale":"1x"},{"size":"72x72","expected-size":"144","filename":"144.png","folder":"Assets.xcassets/AppIcon.appiconset/","idiom":"ipad","scale":"2x"},{"size":"40x40","expected-size":"40","filename":"40.png","folder":"Assets.xcassets/AppIcon.appiconset/","idiom":"ipad","scale":"1x"},{"size":"83.5x83.5","expected-size":"167","filename":"167.png","folder":"Assets.xcassets/AppIcon.appiconset/","idiom":"ipad","scale":"2x"},{"size":"20x20","expected-size":"20","filename":"20.png","folder":"Assets.xcassets/AppIcon.appiconset/","idiom":"ipad","scale":"1x"},{"size":"20x20","expected-size":"40","filename":"40.png","folder":"Assets.xcassets/AppIcon.appiconset/","idiom":"ipad","scale":"2x"}]}
|
|
@ -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
|
||||
}
|
||||
}
|
|
@ -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
|
||||
}
|
||||
}
|
After Width: | Height: | Size: 26 KiB |
After Width: | Height: | Size: 26 KiB |