Merge branch 'main' into theme-setting
This commit is contained in:
commit
1398d6cac0
|
@ -111,6 +111,7 @@ struct ConnectToServerView: View {
|
||||||
TextField(NSLocalizedString("Server URL", comment: ""), text: $uri)
|
TextField(NSLocalizedString("Server URL", comment: ""), text: $uri)
|
||||||
.disableAutocorrection(true)
|
.disableAutocorrection(true)
|
||||||
.autocapitalization(.none)
|
.autocapitalization(.none)
|
||||||
|
.keyboardType(.URL)
|
||||||
Button {
|
Button {
|
||||||
viewModel.connectToServer()
|
viewModel.connectToServer()
|
||||||
} label: {
|
} label: {
|
||||||
|
|
|
@ -22,6 +22,11 @@
|
||||||
<string>$(CURRENT_PROJECT_VERSION)</string>
|
<string>$(CURRENT_PROJECT_VERSION)</string>
|
||||||
<key>LSRequiresIPhoneOS</key>
|
<key>LSRequiresIPhoneOS</key>
|
||||||
<true/>
|
<true/>
|
||||||
|
<key>NSAppTransportSecurity</key>
|
||||||
|
<dict>
|
||||||
|
<key>NSAllowsArbitraryLoads</key>
|
||||||
|
<true/>
|
||||||
|
</dict>
|
||||||
<key>UILaunchScreen</key>
|
<key>UILaunchScreen</key>
|
||||||
<dict/>
|
<dict/>
|
||||||
<key>UIRequiredDeviceCapabilities</key>
|
<key>UIRequiredDeviceCapabilities</key>
|
||||||
|
|
|
@ -109,6 +109,7 @@ struct ConnectToServerView: View {
|
||||||
TextField(NSLocalizedString("Server URL", comment: ""), text: $uri)
|
TextField(NSLocalizedString("Server URL", comment: ""), text: $uri)
|
||||||
.disableAutocorrection(true)
|
.disableAutocorrection(true)
|
||||||
.autocapitalization(.none)
|
.autocapitalization(.none)
|
||||||
|
.keyboardType(.URL)
|
||||||
Button {
|
Button {
|
||||||
viewModel.connectToServer()
|
viewModel.connectToServer()
|
||||||
} label: {
|
} label: {
|
||||||
|
|
|
@ -30,8 +30,6 @@
|
||||||
<dict>
|
<dict>
|
||||||
<key>NSAllowsArbitraryLoads</key>
|
<key>NSAllowsArbitraryLoads</key>
|
||||||
<true/>
|
<true/>
|
||||||
<key>NSAllowsLocalNetworking</key>
|
|
||||||
<true/>
|
|
||||||
</dict>
|
</dict>
|
||||||
<key>NSBluetoothAlwaysUsageDescription</key>
|
<key>NSBluetoothAlwaysUsageDescription</key>
|
||||||
<string>${PRODUCT_NAME} uses Bluetooth to discover nearby Cast devices.</string>
|
<string>${PRODUCT_NAME} uses Bluetooth to discover nearby Cast devices.</string>
|
||||||
|
|
|
@ -34,7 +34,7 @@ struct ItemView: View {
|
||||||
.statusBar(hidden: true)
|
.statusBar(hidden: true)
|
||||||
.edgesIgnoringSafeArea(.all)
|
.edgesIgnoringSafeArea(.all)
|
||||||
.prefersHomeIndicatorAutoHidden(true)
|
.prefersHomeIndicatorAutoHidden(true)
|
||||||
}.supportedOrientations(.landscape), isActive: $videoPlayerItem.shouldShowPlayer) {
|
}, isActive: $videoPlayerItem.shouldShowPlayer) {
|
||||||
EmptyView()
|
EmptyView()
|
||||||
}
|
}
|
||||||
VStack {
|
VStack {
|
||||||
|
@ -56,7 +56,6 @@ struct ItemView: View {
|
||||||
.navigationBarHidden(false)
|
.navigationBarHidden(false)
|
||||||
.navigationBarBackButtonHidden(false)
|
.navigationBarBackButtonHidden(false)
|
||||||
.environmentObject(videoPlayerItem)
|
.environmentObject(videoPlayerItem)
|
||||||
.supportedOrientations(.all)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -209,6 +209,8 @@ class EmailHelper: NSObject, MFMailComposeViewControllerDelegate {
|
||||||
|
|
||||||
@main
|
@main
|
||||||
struct JellyfinPlayerApp: App {
|
struct JellyfinPlayerApp: App {
|
||||||
|
@UIApplicationDelegateAdaptor(AppDelegate.self) var appDelegate
|
||||||
|
|
||||||
let persistenceController = PersistenceController.shared
|
let persistenceController = PersistenceController.shared
|
||||||
|
|
||||||
var body: some Scene {
|
var body: some Scene {
|
||||||
|
@ -224,3 +226,12 @@ struct JellyfinPlayerApp: App {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
class AppDelegate: NSObject, UIApplicationDelegate {
|
||||||
|
|
||||||
|
static var orientationLock = UIInterfaceOrientationMask.all
|
||||||
|
|
||||||
|
func application(_ application: UIApplication, supportedInterfaceOrientationsFor window: UIWindow?) -> UIInterfaceOrientationMask {
|
||||||
|
AppDelegate.orientationLock
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -12,13 +12,13 @@ import SwiftUI
|
||||||
struct LibrarySearchView: View {
|
struct LibrarySearchView: View {
|
||||||
@StateObject var viewModel: LibrarySearchViewModel
|
@StateObject var viewModel: LibrarySearchViewModel
|
||||||
@State var searchQuery = ""
|
@State var searchQuery = ""
|
||||||
|
|
||||||
@State private var tracks: [GridItem] = Array(repeating: .init(.flexible()), count: Int(UIScreen.main.bounds.size.width) / 125)
|
@State private var tracks: [GridItem] = Array(repeating: .init(.flexible()), count: Int(UIScreen.main.bounds.size.width) / 125)
|
||||||
|
|
||||||
func recalcTracks() {
|
func recalcTracks() {
|
||||||
tracks = Array(repeating: .init(.flexible()), count: Int(UIScreen.main.bounds.size.width) / 125)
|
tracks = Array(repeating: .init(.flexible()), count: Int(UIScreen.main.bounds.size.width) / 125)
|
||||||
}
|
}
|
||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
ZStack {
|
ZStack {
|
||||||
VStack {
|
VStack {
|
||||||
|
@ -40,7 +40,7 @@ struct LibrarySearchView: View {
|
||||||
}
|
}
|
||||||
.navigationBarTitle("Search", displayMode: .inline)
|
.navigationBarTitle("Search", displayMode: .inline)
|
||||||
}
|
}
|
||||||
|
|
||||||
var suggestionsListView: some View {
|
var suggestionsListView: some View {
|
||||||
ScrollView {
|
ScrollView {
|
||||||
LazyVStack(spacing: 8) {
|
LazyVStack(spacing: 8) {
|
||||||
|
@ -61,7 +61,7 @@ struct LibrarySearchView: View {
|
||||||
.padding(.horizontal, 16)
|
.padding(.horizontal, 16)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
var resultView: some View {
|
var resultView: some View {
|
||||||
let items = items(for: viewModel.selectedItemType)
|
let items = items(for: viewModel.selectedItemType)
|
||||||
return VStack(alignment: .leading, spacing: 16) {
|
return VStack(alignment: .leading, spacing: 16) {
|
||||||
|
@ -90,7 +90,7 @@ struct LibrarySearchView: View {
|
||||||
recalcTracks()
|
recalcTracks()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func items(for type: ItemType) -> [BaseItemDto] {
|
func items(for type: ItemType) -> [BaseItemDto] {
|
||||||
switch type {
|
switch type {
|
||||||
case .episode:
|
case .episode:
|
||||||
|
@ -106,7 +106,7 @@ struct LibrarySearchView: View {
|
||||||
}
|
}
|
||||||
|
|
||||||
private extension ItemType {
|
private extension ItemType {
|
||||||
|
|
||||||
var localized: String {
|
var localized: String {
|
||||||
switch self {
|
switch self {
|
||||||
case .episode:
|
case .episode:
|
||||||
|
|
|
@ -73,7 +73,7 @@ struct SettingsView: View {
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
Section {
|
Section(header: Text(ServerEnvironment.current.server.name ?? "")) {
|
||||||
HStack {
|
HStack {
|
||||||
Text("Signed in as \(username)").foregroundColor(.primary)
|
Text("Signed in as \(username)").foregroundColor(.primary)
|
||||||
Spacer()
|
Spacer()
|
||||||
|
|
|
@ -81,7 +81,7 @@ class PlayerViewController: UIViewController, GCKDiscoveryManagerListener, GCKRe
|
||||||
var playbackItem = PlaybackItem()
|
var playbackItem = PlaybackItem()
|
||||||
var remoteTimeUpdateTimer: Timer?
|
var remoteTimeUpdateTimer: Timer?
|
||||||
var upNextViewModel: UpNextViewModel = UpNextViewModel()
|
var upNextViewModel: UpNextViewModel = UpNextViewModel()
|
||||||
var lastOri: UIDeviceOrientation!
|
var lastOri: UIInterfaceOrientation!
|
||||||
|
|
||||||
// MARK: IBActions
|
// MARK: IBActions
|
||||||
@IBAction func seekSliderStart(_ sender: Any) {
|
@IBAction func seekSliderStart(_ sender: Any) {
|
||||||
|
@ -387,6 +387,7 @@ class PlayerViewController: UIViewController, GCKDiscoveryManagerListener, GCKRe
|
||||||
|
|
||||||
// MARK: viewDidLoad
|
// MARK: viewDidLoad
|
||||||
override func viewDidLoad() {
|
override func viewDidLoad() {
|
||||||
|
super.viewDidLoad()
|
||||||
if manifest.type == "Movie" {
|
if manifest.type == "Movie" {
|
||||||
titleLabel.text = manifest.name ?? ""
|
titleLabel.text = manifest.name ?? ""
|
||||||
} else {
|
} else {
|
||||||
|
@ -396,14 +397,21 @@ class PlayerViewController: UIViewController, GCKDiscoveryManagerListener, GCKRe
|
||||||
upNextViewModel.delegate = self
|
upNextViewModel.delegate = self
|
||||||
}
|
}
|
||||||
|
|
||||||
lastOri = UIDevice.current.orientation
|
DispatchQueue.main.async {
|
||||||
|
self.lastOri = UIApplication.shared.windows.first?.windowScene?.interfaceOrientation
|
||||||
|
AppDelegate.orientationLock = .landscape
|
||||||
|
|
||||||
if !UIDevice.current.orientation.isLandscape || UIDevice.current.orientation.isFlat {
|
if !self.lastOri.isLandscape {
|
||||||
let value = UIInterfaceOrientation.landscapeRight.rawValue
|
UIDevice.current.setValue(UIInterfaceOrientation.landscapeRight.rawValue, forKey: "orientation")
|
||||||
UIDevice.current.setValue(value, forKey: "orientation")
|
UIViewController.attemptRotationToDeviceOrientation()
|
||||||
UIViewController.attemptRotationToDeviceOrientation()
|
}
|
||||||
}
|
}
|
||||||
super.viewDidLoad()
|
|
||||||
|
NotificationCenter.default.addObserver(self, selector: #selector(didChangedOrientation), name: UIDevice.orientationDidChangeNotification, object: nil)
|
||||||
|
}
|
||||||
|
|
||||||
|
@objc func didChangedOrientation() {
|
||||||
|
lastOri = UIApplication.shared.windows.first?.windowScene?.interfaceOrientation
|
||||||
}
|
}
|
||||||
|
|
||||||
func mediaHasStartedPlaying() {
|
func mediaHasStartedPlaying() {
|
||||||
|
@ -438,12 +446,15 @@ class PlayerViewController: UIViewController, GCKDiscoveryManagerListener, GCKRe
|
||||||
}
|
}
|
||||||
|
|
||||||
override func viewWillDisappear(_ animated: Bool) {
|
override func viewWillDisappear(_ animated: Bool) {
|
||||||
|
super.viewWillDisappear(animated)
|
||||||
self.tabBarController?.tabBar.isHidden = false
|
self.tabBarController?.tabBar.isHidden = false
|
||||||
self.navigationController?.isNavigationBarHidden = false
|
self.navigationController?.isNavigationBarHidden = false
|
||||||
overrideUserInterfaceStyle = .unspecified
|
overrideUserInterfaceStyle = .unspecified
|
||||||
UIDevice.current.setValue(lastOri.rawValue, forKey: "orientation")
|
DispatchQueue.main.async {
|
||||||
UIViewController.attemptRotationToDeviceOrientation()
|
AppDelegate.orientationLock = .all
|
||||||
super.viewWillDisappear(animated)
|
UIDevice.current.setValue(self.lastOri.rawValue, forKey: "orientation")
|
||||||
|
UIViewController.attemptRotationToDeviceOrientation()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// MARK: viewDidAppear
|
// MARK: viewDidAppear
|
||||||
|
@ -479,19 +490,19 @@ class PlayerViewController: UIViewController, GCKDiscoveryManagerListener, GCKRe
|
||||||
MediaInfoAPI.getPostedPlaybackInfo(itemId: manifest.id!, userId: SessionManager.current.user.user_id!, maxStreamingBitrate: Int(maxBitrate), startTimeTicks: manifest.userData?.playbackPositionTicks ?? 0, autoOpenLiveStream: true, playbackInfoDto: playbackInfo)
|
MediaInfoAPI.getPostedPlaybackInfo(itemId: manifest.id!, userId: SessionManager.current.user.user_id!, maxStreamingBitrate: Int(maxBitrate), startTimeTicks: manifest.userData?.playbackPositionTicks ?? 0, autoOpenLiveStream: true, playbackInfoDto: playbackInfo)
|
||||||
.sink(receiveCompletion: { completion in
|
.sink(receiveCompletion: { completion in
|
||||||
switch completion {
|
switch completion {
|
||||||
case .finished:
|
case .finished:
|
||||||
break
|
break
|
||||||
case .failure(let error):
|
case .failure(let error):
|
||||||
if let err = error as? ErrorResponse {
|
if let err = error as? ErrorResponse {
|
||||||
switch err {
|
switch err {
|
||||||
case .error(401, _, _, _):
|
case .error(401, _, _, _):
|
||||||
self.delegate?.exitPlayer(self)
|
self.delegate?.exitPlayer(self)
|
||||||
SessionManager.current.logout()
|
SessionManager.current.logout()
|
||||||
case .error:
|
case .error:
|
||||||
self.delegate?.exitPlayer(self)
|
self.delegate?.exitPlayer(self)
|
||||||
}
|
|
||||||
}
|
}
|
||||||
break
|
}
|
||||||
|
break
|
||||||
}
|
}
|
||||||
}, receiveValue: { [self] response in
|
}, receiveValue: { [self] response in
|
||||||
playSessionId = response.playSessionId ?? ""
|
playSessionId = response.playSessionId ?? ""
|
||||||
|
@ -906,35 +917,35 @@ extension PlayerViewController: GCKSessionManagerListener {
|
||||||
// MARK: - VLCMediaPlayer Delegates
|
// MARK: - VLCMediaPlayer Delegates
|
||||||
extension PlayerViewController: VLCMediaPlayerDelegate {
|
extension PlayerViewController: VLCMediaPlayerDelegate {
|
||||||
func mediaPlayerStateChanged(_ aNotification: Notification!) {
|
func mediaPlayerStateChanged(_ aNotification: Notification!) {
|
||||||
let currentState: VLCMediaPlayerState = mediaPlayer.state
|
let currentState: VLCMediaPlayerState = mediaPlayer.state
|
||||||
switch currentState {
|
switch currentState {
|
||||||
case .stopped :
|
case .stopped :
|
||||||
LogManager.shared.log.debug("Player state changed: STOPPED")
|
LogManager.shared.log.debug("Player state changed: STOPPED")
|
||||||
break
|
break
|
||||||
case .ended :
|
case .ended :
|
||||||
LogManager.shared.log.debug("Player state changed: ENDED")
|
LogManager.shared.log.debug("Player state changed: ENDED")
|
||||||
break
|
break
|
||||||
case .playing :
|
case .playing :
|
||||||
LogManager.shared.log.debug("Player state changed: PLAYING")
|
LogManager.shared.log.debug("Player state changed: PLAYING")
|
||||||
sendProgressReport(eventName: "unpause")
|
sendProgressReport(eventName: "unpause")
|
||||||
delegate?.hideLoadingView(self)
|
delegate?.hideLoadingView(self)
|
||||||
paused = false
|
paused = false
|
||||||
case .paused :
|
case .paused :
|
||||||
LogManager.shared.log.debug("Player state changed: PAUSED")
|
LogManager.shared.log.debug("Player state changed: PAUSED")
|
||||||
paused = true
|
paused = true
|
||||||
case .opening :
|
case .opening :
|
||||||
LogManager.shared.log.debug("Player state changed: OPENING")
|
LogManager.shared.log.debug("Player state changed: OPENING")
|
||||||
case .buffering :
|
case .buffering :
|
||||||
LogManager.shared.log.debug("Player state changed: BUFFERING")
|
LogManager.shared.log.debug("Player state changed: BUFFERING")
|
||||||
delegate?.showLoadingView(self)
|
delegate?.showLoadingView(self)
|
||||||
case .error :
|
case .error :
|
||||||
LogManager.shared.log.error("Video had error.")
|
LogManager.shared.log.error("Video had error.")
|
||||||
sendStopReport()
|
sendStopReport()
|
||||||
case .esAdded:
|
case .esAdded:
|
||||||
mainActionButton.setImage(UIImage(systemName: "pause"), for: .normal)
|
mainActionButton.setImage(UIImage(systemName: "pause"), for: .normal)
|
||||||
@unknown default:
|
@unknown default:
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func mediaPlayerTimeChanged(_ aNotification: Notification!) {
|
func mediaPlayerTimeChanged(_ aNotification: Notification!) {
|
||||||
|
|
24
README.md
24
README.md
|
@ -1,6 +1,9 @@
|
||||||
<p align="center">
|
<p align="center">
|
||||||
<img alt="SwiftFin" height="125" src="https://github.com/jellyfin/SwiftFin/raw/main/JellyfinPlayer/Assets.xcassets/AppIcon.appiconset/152.png">
|
<img alt="SwiftFin" height="125" src="https://github.com/jellyfin/SwiftFin/raw/main/JellyfinPlayer/Assets.xcassets/AppIcon.appiconset/152.png">
|
||||||
<h2 align="center">SwiftFin</h2>
|
<h2 align="center">SwiftFin</h2>
|
||||||
|
<a href="https://translate.jellyfin.org/engage/swiftfin/">
|
||||||
|
<img src="https://translate.jellyfin.org/widgets/swiftfin/-/svg-badge.svg"/>
|
||||||
|
</a>
|
||||||
<a href="https://matrix.to/#/+jellyfin:matrix.org">
|
<a href="https://matrix.to/#/+jellyfin:matrix.org">
|
||||||
<img src="https://img.shields.io/matrix/jellyfin:matrix.org">
|
<img src="https://img.shields.io/matrix/jellyfin:matrix.org">
|
||||||
</a>
|
</a>
|
||||||
|
@ -19,6 +22,27 @@
|
||||||
|
|
||||||
<a href='https://testflight.apple.com/join/WiN0G62Q'><img height='70' alt='Join the Beta on TestFlight' src='https://anotherlens.app/testflight-badge.png'/></a>
|
<a href='https://testflight.apple.com/join/WiN0G62Q'><img height='70' alt='Join the Beta on TestFlight' src='https://anotherlens.app/testflight-badge.png'/></a>
|
||||||
|
|
||||||
|
**Don't see SwiftFin in your language?**
|
||||||
|
|
||||||
|
Check out our [Weblate instance](https://translate.jellyfin.org/projects/swiftfin/) to help translate SwiftFin and other projects.
|
||||||
|
|
||||||
|
<a href="https://translate.jellyfin.org/engage/swiftfin/">
|
||||||
|
<img src="https://translate.jellyfin.org/widgets/swiftfin/-/multi-auto.svg"/>
|
||||||
|
</a>
|
||||||
|
|
||||||
## ⚙️ Development
|
## ⚙️ Development
|
||||||
|
|
||||||
Xcode 13.0 with command line tools.
|
Xcode 13.0 with command line tools.
|
||||||
|
|
||||||
|
### Build Process
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# install Cocoapods (if not installed)
|
||||||
|
$ sudo gem install cocoapods
|
||||||
|
|
||||||
|
# install dependencies
|
||||||
|
$ pod install
|
||||||
|
|
||||||
|
# open workspace and build it
|
||||||
|
$ open JellyfinPlayer.xcworkspace
|
||||||
|
```
|
Binary file not shown.
Loading…
Reference in New Issue