mirror of
https://github.com/yattee/yattee.git
synced 2026-10-16 01:24:14 +00:00
Yattee v2 rewrite
This commit is contained in:
@@ -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
|
||||
}
|
||||
}
|
||||
@@ -1,2 +0,0 @@
|
||||
#import <ifaddrs.h>
|
||||
#import <CoreFoundation/CoreFoundation.h>
|
||||
106
iOS/Info.plist
106
iOS/Info.plist
@@ -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>
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
@@ -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) {}
|
||||
}
|
||||
@@ -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>
|
||||
Reference in New Issue
Block a user