Yattee v2 rewrite

This commit is contained in:
Arkadiusz Fal
2026-02-08 18:31:16 +01:00
parent d94a50f8c3
commit 100df744d9
1043 changed files with 163886 additions and 68471 deletions

View File

@@ -1,38 +0,0 @@
import AVFoundation
import Defaults
import Foundation
import Logging
import UIKit
final class AppDelegate: UIResponder, UIApplicationDelegate {
var orientationLock = UIInterfaceOrientationMask.all
private var logger = Logger(label: "stream.yattee.app.delegate")
private(set) static var instance: AppDelegate!
func application(_: UIApplication, supportedInterfaceOrientationsFor _: UIWindow?) -> UIInterfaceOrientationMask {
return orientationLock
}
func application(_: UIApplication, didFinishLaunchingWithOptions _: [UIApplication.LaunchOptionsKey: Any]? = nil) -> Bool { // swiftlint:disable:this discouraged_optional_collection
Self.instance = self
#if !os(macOS)
UIViewController.swizzleHomeIndicatorProperty()
OrientationTracker.shared.startDeviceOrientationTracking()
OrientationModel.shared.startOrientationUpdates()
UIApplication.shared.beginReceivingRemoteControlEvents()
#endif
return true
}
func application(_: UIApplication, open url: URL, options _: [UIApplication.OpenURLOptionsKey: Any] = [:]) -> Bool {
if url.scheme == "yattee" {
OpenURLHandler(navigationStyle: Constants.defaultNavigationStyle).handle(url)
return true
}
return false
}
}

View File

@@ -1,2 +0,0 @@
#import <ifaddrs.h>
#import <CoreFoundation/CoreFoundation.h>

View File

@@ -1,106 +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>CFBundleDocumentTypes</key>
<array>
<dict>
<key>CFBundleTypeName</key>
<string>mpeg4Movie</string>
<key>LSHandlerRank</key>
<string>Default</string>
<key>LSItemContentTypes</key>
<array>
<string>public.mpeg-4</string>
</array>
</dict>
<dict>
<key>CFBundleTypeName</key>
<string>url</string>
<key>LSHandlerRank</key>
<string>Default</string>
<key>LSItemContentTypes</key>
<array>
<string>public.url</string>
</array>
</dict>
<dict>
<key>CFBundleTypeName</key>
<string>fileURL</string>
<key>LSHandlerRank</key>
<string>Default</string>
<key>LSItemContentTypes</key>
<array>
<string>public.file-url</string>
</array>
</dict>
<dict>
<key>CFBundleTypeName</key>
<string>Settings text</string>
<key>LSHandlerRank</key>
<string>Default</string>
<key>LSItemContentTypes</key>
<array>
<string>public.json</string>
</array>
</dict>
</array>
<key>CFBundleURLTypes</key>
<array>
<dict>
<key>CFBundleTypeRole</key>
<string>Editor</string>
<key>CFBundleURLName</key>
<string>net.yattee.app.watch</string>
<key>CFBundleURLSchemes</key>
<array>
<string>yattee</string>
</array>
</dict>
</array>
<key>NSAppTransportSecurity</key>
<dict>
<key>NSAllowsArbitraryLoads</key>
<true/>
</dict>
<key>UIApplicationSceneManifest</key>
<dict>
<key>UIApplicationSupportsMultipleScenes</key>
<false/>
<key>UISceneConfigurations</key>
<dict/>
</dict>
<key>UIBackgroundModes</key>
<array>
<string>audio</string>
</array>
<key>UIFileSharingEnabled</key>
<true/>
<key>UTExportedTypeDeclarations</key>
<array>
<dict>
<key>UTTypeConformsTo</key>
<array>
<string>public.json</string>
</array>
<key>UTTypeDescription</key>
<string>Yattee Settings</string>
<key>UTTypeIconFiles</key>
<array/>
<key>UTTypeIdentifier</key>
<string>stream.yattee.app-settings</string>
<key>UTTypeTagSpecification</key>
<dict>
<key>public.filename-extension</key>
<array>
<string>yatteesettings</string>
</array>
<key>public.mime-type</key>
<array>
<string>application/json</string>
</array>
</dict>
</dict>
</array>
</dict>
</plist>

View File

@@ -1,68 +0,0 @@
import CoreMotion
import Logging
import UIKit
enum Orientation {
static var logger = Logger(label: "stream.yattee.orientation")
static func lockOrientation(_ orientation: UIInterfaceOrientationMask) {
// Orientation locking is only for iPhone, not iPad
guard Constants.isIPhone else {
logger.info("skipping orientation lock on iPad")
return
}
if let delegate = AppDelegate.instance {
delegate.orientationLock = orientation
let orientationString = orientation == .portrait ? "portrait" : orientation == .landscapeLeft ? "landscapeLeft" :
orientation == .landscapeRight ? "landscapeRight" : orientation == .portraitUpsideDown ? "portraitUpsideDown" :
orientation == .landscape ? "landscape" : orientation == .all ? "all" : "allButUpsideDown"
logger.info("locking \(orientationString)")
}
}
static func lockOrientation(_ orientation: UIInterfaceOrientationMask, andRotateTo rotateOrientation: UIInterfaceOrientation? = nil) {
// Orientation locking and rotation is only for iPhone, not iPad
guard Constants.isIPhone else {
logger.info("skipping orientation lock and rotation on iPad")
return
}
lockOrientation(orientation)
guard let rotateOrientation else {
return
}
let orientationString = rotateOrientation == .portrait ? "portrait" : rotateOrientation == .landscapeLeft ? "landscapeLeft" :
rotateOrientation == .landscapeRight ? "landscapeRight" : rotateOrientation == .portraitUpsideDown ? "portraitUpsideDown" : "allButUpsideDown"
logger.info("rotating to \(orientationString)")
// swiftlint:disable:next deployment_target
if #available(iOS 16, *) {
guard let windowScene = Self.scene else { return }
let rotateOrientationMask = rotateOrientation == .portrait ? UIInterfaceOrientationMask.portrait :
rotateOrientation == .landscapeLeft ? .landscapeLeft :
rotateOrientation == .landscapeRight ? .landscapeRight :
.all
windowScene.requestGeometryUpdate(.iOS(interfaceOrientations: rotateOrientationMask)) { error in
print("denied rotation \(error)")
}
} else {
UIDevice.current.setValue(rotateOrientation.rawValue, forKey: "orientation")
}
UINavigationController.attemptRotationToDeviceOrientation()
}
private static var scene: UIWindowScene? {
UIApplication.shared.connectedScenes
.filter { $0.activationState == .foregroundActive }
.compactMap { $0 as? UIWindowScene }
.first
}
}

View File

@@ -1,90 +0,0 @@
import Defaults
import Foundation
import Logging
import Repeat
import SwiftUI
final class OrientationModel {
static var shared = OrientationModel()
let logger = Logger(label: "stream.yattee.orientation.model")
var orientation = UIInterfaceOrientation.portrait
var lastOrientation: UIInterfaceOrientation?
var orientationDebouncer = Debouncer(.milliseconds(300))
var orientationObserver: Any?
@Default(.lockPortraitWhenBrowsing) private var lockPortraitWhenBrowsing
@Default(.enterFullscreenInLandscape) private var enterFullscreenInLandscape
private var player = PlayerModel.shared
func startOrientationUpdates() {
// Ensure the orientation observer is active
orientationObserver = NotificationCenter.default.addObserver(
forName: OrientationTracker.deviceOrientationChangedNotification,
object: nil,
queue: .main
) { _ in
self.logger.info("Notification received: Device orientation changed.")
// We only allow .portrait and are not showing the player
guard (!self.player.presentingPlayer && !self.lockPortraitWhenBrowsing) || self.player.presentingPlayer
else {
return
}
let orientation = OrientationTracker.shared.currentInterfaceOrientation
self.logger.info("Current interface orientation: \(orientation)")
// Always update lastOrientation to keep track of the latest state
if self.lastOrientation != orientation {
self.lastOrientation = orientation
self.logger.info("Orientation changed to: \(orientation)")
} else {
self.logger.info("Orientation has not changed.")
}
// Only take action if the player is active and presenting
guard (!self.player.isOrientationLocked && !self.player.playingInPictureInPicture) || (!self.lockPortraitWhenBrowsing && !self.player.presentingPlayer) || (!self.lockPortraitWhenBrowsing && self.player.presentingPlayer && !self.player.isOrientationLocked)
else {
self.logger.info("Only updating orientation without actions.")
return
}
DispatchQueue.main.async {
self.orientationDebouncer.callback = {
DispatchQueue.main.async {
if orientation.isLandscape {
if self.enterFullscreenInLandscape, self.player.presentingPlayer {
self.logger.info("Entering fullscreen because orientation is landscape.")
self.player.controls.presentingControls = false
self.player.enterFullScreen(showControls: false)
}
Orientation.lockOrientation(OrientationTracker.shared.currentInterfaceOrientationMask, andRotateTo: orientation)
} else {
self.logger.info("Exiting fullscreen because orientation is portrait.")
if self.player.playingFullScreen {
self.player.exitFullScreen(showControls: false)
}
if self.lockPortraitWhenBrowsing {
Orientation.lockOrientation(.portrait, andRotateTo: .portrait)
} else {
Orientation.lockOrientation(OrientationTracker.shared.currentInterfaceOrientationMask, andRotateTo: orientation)
}
}
}
}
self.orientationDebouncer.call()
}
}
}
func lockOrientation(_ orientation: UIInterfaceOrientationMask, andRotateTo rotateOrientation: UIInterfaceOrientation? = nil) {
logger.info("Locking orientation to: \(orientation), rotating to: \(String(describing: rotateOrientation)).")
if let rotateOrientation {
self.orientation = rotateOrientation
lastOrientation = rotateOrientation
}
Orientation.lockOrientation(orientation, andRotateTo: rotateOrientation)
}
}

View File

@@ -1,98 +0,0 @@
import CoreMotion
import UIKit
public class OrientationTracker {
public static let shared = OrientationTracker()
public static let deviceOrientationChangedNotification = NSNotification.Name("DeviceOrientationChangedNotification")
public var currentDeviceOrientation: UIDeviceOrientation = .portrait
public var currentInterfaceOrientation: UIInterfaceOrientation {
switch currentDeviceOrientation {
case .landscapeLeft:
return .landscapeLeft
case .landscapeRight:
return .landscapeRight
default:
return .portrait
}
}
public var currentInterfaceOrientationMask: UIInterfaceOrientationMask {
switch currentInterfaceOrientation {
case .landscapeLeft:
return .landscapeLeft
case .landscapeRight:
return .landscapeRight
default:
return .portrait
}
}
public var affineTransform: CGAffineTransform {
var angleRadians: Double
switch currentDeviceOrientation {
case .portrait:
angleRadians = 0
case .landscapeLeft:
angleRadians = -0.5 * .pi
case .landscapeRight:
angleRadians = 0.5 * .pi
case .portraitUpsideDown:
angleRadians = .pi
default:
return .identity
}
return CGAffineTransform(rotationAngle: angleRadians)
}
private let motionManager: CMMotionManager
private let queue: OperationQueue
private init() {
motionManager = CMMotionManager()
motionManager.accelerometerUpdateInterval = 0.1
queue = OperationQueue()
}
public func startDeviceOrientationTracking() {
motionManager.startAccelerometerUpdates(to: queue) { accelerometerData, error in
guard error == nil else { return }
guard let accelerometerData else { return }
let newDeviceOrientation = self.deviceOrientation(forAccelerometerData: accelerometerData)
guard newDeviceOrientation != self.currentDeviceOrientation else { return }
self.currentDeviceOrientation = newDeviceOrientation
NotificationCenter.default.post(
name: Self.deviceOrientationChangedNotification,
object: nil,
userInfo: nil
)
}
}
public func stopDeviceOrientationTracking() {
motionManager.stopAccelerometerUpdates()
}
private func deviceOrientation(forAccelerometerData accelerometerData: CMAccelerometerData) -> UIDeviceOrientation {
let threshold = 0.55
if accelerometerData.acceleration.x >= threshold {
return .landscapeLeft
}
if accelerometerData.acceleration.x <= -threshold {
return .landscapeRight
}
if accelerometerData.acceleration.y <= -threshold {
return .portrait
}
if UIDevice.current.userInterfaceIdiom == .pad && accelerometerData.acceleration.y >= threshold {
return .portraitUpsideDown
}
return currentDeviceOrientation
}
}

View File

@@ -1,15 +0,0 @@
import Foundation
import SwiftUI
final class SafeAreaModel: ObservableObject {
static var shared = SafeAreaModel()
@Published var safeArea = EdgeInsets()
var horizontalInsets: Double {
safeArea.leading + safeArea.trailing
}
var verticalInsets: Double {
safeArea.top + safeArea.bottom
}
}

View File

@@ -1,31 +0,0 @@
import Foundation
import SwiftUI
struct ShareSheet: UIViewControllerRepresentable {
typealias Callback = (_ activityType: UIActivity.ActivityType?,
_ completed: Bool,
_ returnedItems: [Any],
_ error: Error?) -> Void
let activityItems: [Any]
let applicationActivities = [UIActivity]()
let excludedActivityTypes = [UIActivity.ActivityType]()
let callback: Callback? = nil
func makeUIViewController(context _: Context) -> UIActivityViewController {
let controller = UIActivityViewController(
activityItems: activityItems,
applicationActivities: applicationActivities
)
controller.excludedActivityTypes = excludedActivityTypes
controller.completionWithItemsHandler = { activityType, completed, returnedItems, error in
callback?(activityType, completed, returnedItems ?? [], error)
}
return controller
}
func updateUIViewController(_: UIActivityViewController, context _: Context) {}
}

View File

@@ -1,10 +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.security.application-groups</key>
<array>
<string>group.78Z5H3M6RJ.stream.yattee.app.urlbookmarks</string>
</array>
</dict>
</plist>