Compare commits
4 Commits
release/v2
...
release/v2
| Author | SHA1 | Date | |
|---|---|---|---|
| 61aa85463c | |||
| fe6ce3a91c | |||
| 50e5f80a44 | |||
| cef1770d2f |
@@ -14,6 +14,8 @@
|
||||
0C3CAEAF1C3350C800B856AD /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 0C3CAEAE1C3350C800B856AD /* Assets.xcassets */; };
|
||||
0C3CAEB21C3350C800B856AD /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 0C3CAEB01C3350C800B856AD /* LaunchScreen.storyboard */; };
|
||||
0C41EF4A1C34B51500AF847F /* SwitchesViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C41EF491C34B51500AF847F /* SwitchesViewController.swift */; };
|
||||
B10000010000000000000001 /* AudioMixerViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = B10000010000000000000002 /* AudioMixerViewController.swift */; };
|
||||
B10000010000000000000003 /* AudioSectionView.swift in Sources */ = {isa = PBXBuildFile; fileRef = B10000010000000000000004 /* AudioSectionView.swift */; };
|
||||
0C41EF4E1C34B81F00AF847F /* KSIRingView.m in Sources */ = {isa = PBXBuildFile; fileRef = 0C41EF4D1C34B81F00AF847F /* KSIRingView.m */; };
|
||||
0C41EF501C34B97700AF847F /* HeaderView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C41EF4F1C34B97700AF847F /* HeaderView.swift */; };
|
||||
0C41EF521C34BA4E00AF847F /* Orbitron-Medium.ttf in Resources */ = {isa = PBXBuildFile; fileRef = 0C41EF511C34BA4E00AF847F /* Orbitron-Medium.ttf */; };
|
||||
@@ -67,6 +69,8 @@
|
||||
0C3CAEB11C3350C800B856AD /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = "<group>"; };
|
||||
0C3CAEB31C3350C800B856AD /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; };
|
||||
0C41EF491C34B51500AF847F /* SwitchesViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SwitchesViewController.swift; sourceTree = "<group>"; };
|
||||
B10000010000000000000002 /* AudioMixerViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AudioMixerViewController.swift; sourceTree = "<group>"; };
|
||||
B10000010000000000000004 /* AudioSectionView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AudioSectionView.swift; sourceTree = "<group>"; };
|
||||
0C41EF4C1C34B81F00AF847F /* KSIRingView.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = KSIRingView.h; sourceTree = "<group>"; };
|
||||
0C41EF4D1C34B81F00AF847F /* KSIRingView.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = KSIRingView.m; sourceTree = "<group>"; };
|
||||
0C41EF4F1C34B97700AF847F /* HeaderView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = HeaderView.swift; sourceTree = "<group>"; };
|
||||
@@ -146,6 +150,7 @@
|
||||
0C20E2B61C33514100BA789B /* Controllers */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
B10000010000000000000002 /* AudioMixerViewController.swift */,
|
||||
0C20E2BA1C33529700BA789B /* MainViewController.swift */,
|
||||
0C41EF491C34B51500AF847F /* SwitchesViewController.swift */,
|
||||
0C20E2BC1C33534F00BA789B /* VisualizationViewController.swift */,
|
||||
@@ -167,6 +172,7 @@
|
||||
0C20E2B81C33514100BA789B /* Views */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
B10000010000000000000004 /* AudioSectionView.swift */,
|
||||
0C41EF551C35ABCD00AF847F /* ConnectionStatusView.swift */,
|
||||
0C41EF4F1C34B97700AF847F /* HeaderView.swift */,
|
||||
0CF9A48D1C35E9C800276F13 /* SwitchIndicatorView.swift */,
|
||||
@@ -393,6 +399,8 @@
|
||||
isa = PBXSourcesBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
B10000010000000000000001 /* AudioMixerViewController.swift in Sources */,
|
||||
B10000010000000000000003 /* AudioSectionView.swift in Sources */,
|
||||
0C41EF4E1C34B81F00AF847F /* KSIRingView.m in Sources */,
|
||||
0CAA327B1C361CBE00B353CF /* NSErrorAdditions.swift in Sources */,
|
||||
CDA64C242E03AE6400E9B07E /* HomeAssistantServer.swift in Sources */,
|
||||
|
||||
@@ -52,6 +52,18 @@
|
||||
ReferencedContainer = "container:XIONControlPanel.xcodeproj">
|
||||
</BuildableReference>
|
||||
</MacroExpansion>
|
||||
<EnvironmentVariables>
|
||||
<EnvironmentVariable
|
||||
key = "HOME_ASSISTANT_ACCESS_TOKEN"
|
||||
value = "$(HOME_ASSISTANT_ACCESS_TOKEN)"
|
||||
isEnabled = "YES">
|
||||
</EnvironmentVariable>
|
||||
<EnvironmentVariable
|
||||
key = "RUN_LIVE_HOME_ASSISTANT_TESTS"
|
||||
value = "$(RUN_LIVE_HOME_ASSISTANT_TESTS)"
|
||||
isEnabled = "YES">
|
||||
</EnvironmentVariable>
|
||||
</EnvironmentVariables>
|
||||
<Testables>
|
||||
<TestableReference
|
||||
skipped = "NO"
|
||||
@@ -65,18 +77,6 @@
|
||||
</BuildableReference>
|
||||
</TestableReference>
|
||||
</Testables>
|
||||
<EnvironmentVariables>
|
||||
<EnvironmentVariable
|
||||
key = "HOME_ASSISTANT_ACCESS_TOKEN"
|
||||
value = "$(HOME_ASSISTANT_ACCESS_TOKEN)"
|
||||
isEnabled = "YES">
|
||||
</EnvironmentVariable>
|
||||
<EnvironmentVariable
|
||||
key = "RUN_LIVE_HOME_ASSISTANT_TESTS"
|
||||
value = "$(RUN_LIVE_HOME_ASSISTANT_TESTS)"
|
||||
isEnabled = "YES">
|
||||
</EnvironmentVariable>
|
||||
</EnvironmentVariables>
|
||||
</TestAction>
|
||||
<LaunchAction
|
||||
buildConfiguration = "Debug"
|
||||
@@ -101,7 +101,7 @@
|
||||
<EnvironmentVariables>
|
||||
<EnvironmentVariable
|
||||
key = "HOME_ASSISTANT_ACCESS_TOKEN"
|
||||
value = "$(HOME_ASSISTANT_ACCESS_TOKEN)"
|
||||
value = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiI2NzJmZDIyZWE4NmI0ZTc4YTYwNjc2NjJhZjRlZDlmNSIsImlhdCI6MTc4NTIwNjg4OCwiZXhwIjoyMTAwNTY2ODg4fQ.mtL05642nRgDw8ybdxCpRbA9e7rlCzFLIFhO_xAAigc"
|
||||
isEnabled = "YES">
|
||||
</EnvironmentVariable>
|
||||
<EnvironmentVariable
|
||||
|
||||
162
XIONControlPanel/Controllers/AudioMixerViewController.swift
Normal file
162
XIONControlPanel/Controllers/AudioMixerViewController.swift
Normal file
@@ -0,0 +1,162 @@
|
||||
//
|
||||
// AudioMixerViewController.swift
|
||||
// XIONControlPanel
|
||||
//
|
||||
|
||||
import UIKit
|
||||
import WebKit
|
||||
|
||||
final class AudioMixerViewController: UIViewController, WKNavigationDelegate
|
||||
{
|
||||
static let controlsWidth: CGFloat = 54.0
|
||||
|
||||
var closeHandler: (() -> Void)?
|
||||
|
||||
private static let mixerURL = URL(string: "http://avmotu.localdomain/#touch-console")!
|
||||
private static let accentColor = UIColor(red: 0.0, green: 0.95, blue: 0.95, alpha: 1.0)
|
||||
|
||||
private let _panelView = UIView(frame: .zero)
|
||||
private let _webView = WKWebView(frame: .zero, configuration: WKWebViewConfiguration())
|
||||
private let _closeButton = UIButton(type: .custom)
|
||||
private let _refreshButton = UIButton(type: .custom)
|
||||
private let _loadingProgressView = UIProgressView(progressViewStyle: .bar)
|
||||
private var _progressObservation: NSKeyValueObservation?
|
||||
|
||||
override func loadView()
|
||||
{
|
||||
view = UIView(frame: .zero)
|
||||
view.backgroundColor = .clear
|
||||
view.clipsToBounds = false
|
||||
}
|
||||
|
||||
override func viewDidLoad()
|
||||
{
|
||||
super.viewDidLoad()
|
||||
|
||||
_panelView.backgroundColor = .black
|
||||
_panelView.layer.borderColor = Self.accentColor.withAlphaComponent(0.65).cgColor
|
||||
_panelView.layer.borderWidth = 1.0
|
||||
_panelView.layer.shadowColor = UIColor.black.cgColor
|
||||
_panelView.layer.shadowOpacity = 0.95
|
||||
_panelView.layer.shadowRadius = 116.0
|
||||
_panelView.layer.shadowOffset = CGSize(width: -10.0, height: 0.0)
|
||||
view.addSubview(_panelView)
|
||||
|
||||
_webView.navigationDelegate = self
|
||||
_webView.isOpaque = false
|
||||
_webView.backgroundColor = .black
|
||||
_webView.scrollView.backgroundColor = .black
|
||||
_webView.allowsBackForwardNavigationGestures = false
|
||||
_panelView.addSubview(_webView)
|
||||
|
||||
_configureControlTab(_closeButton, accessibilityLabel: "Close Mixer")
|
||||
_closeButton.setTitle("X", for: .normal)
|
||||
_closeButton.titleLabel?.font = UIFont(name: "Orbitron-Medium", size: 18.0)
|
||||
_closeButton.accessibilityIdentifier = "audio.close"
|
||||
_closeButton.addTarget(self, action: #selector(_close), for: .touchUpInside)
|
||||
view.addSubview(_closeButton)
|
||||
|
||||
_configureControlTab(_refreshButton, accessibilityLabel: "Refresh Mixer")
|
||||
let refreshImage = UIImage(
|
||||
systemName: "arrow.clockwise",
|
||||
withConfiguration: UIImage.SymbolConfiguration(pointSize: 20.0, weight: .medium)
|
||||
)
|
||||
_refreshButton.setImage(refreshImage, for: .normal)
|
||||
_refreshButton.tintColor = Self.accentColor
|
||||
_refreshButton.accessibilityIdentifier = "audio.refresh"
|
||||
_refreshButton.addTarget(self, action: #selector(_refresh), for: .touchUpInside)
|
||||
view.addSubview(_refreshButton)
|
||||
|
||||
_loadingProgressView.progressTintColor = Self.accentColor
|
||||
_loadingProgressView.trackTintColor = UIColor(white: 0.15, alpha: 1.0)
|
||||
_panelView.addSubview(_loadingProgressView)
|
||||
|
||||
_progressObservation = _webView.observe(
|
||||
\.estimatedProgress,
|
||||
options: [.new]
|
||||
) { [weak self] webView, _ in
|
||||
guard let self else { return }
|
||||
let progress = Float(webView.estimatedProgress)
|
||||
self._loadingProgressView.setProgress(progress, animated: true)
|
||||
self._loadingProgressView.isHidden = progress >= 1.0
|
||||
}
|
||||
|
||||
_loadMixer()
|
||||
}
|
||||
|
||||
override func viewDidLayoutSubviews()
|
||||
{
|
||||
super.viewDidLayoutSubviews()
|
||||
|
||||
let contentFrame = CGRect(
|
||||
x: Self.controlsWidth,
|
||||
y: 0.0,
|
||||
width: max(0.0, view.bounds.width - Self.controlsWidth),
|
||||
height: view.bounds.height
|
||||
)
|
||||
_panelView.frame = contentFrame
|
||||
_panelView.layer.shadowPath = UIBezierPath(rect: _panelView.bounds).cgPath
|
||||
_webView.frame = _panelView.bounds
|
||||
_loadingProgressView.frame = CGRect(
|
||||
x: 1.0,
|
||||
y: 1.0,
|
||||
width: max(0.0, _panelView.bounds.width - 2.0),
|
||||
height: 2.0
|
||||
)
|
||||
|
||||
let tabHeight: CGFloat = 54.0
|
||||
let tabSpacing: CGFloat = 5.0
|
||||
let tabTop: CGFloat = 16.0
|
||||
_closeButton.frame = CGRect(
|
||||
x: 0.0,
|
||||
y: tabTop,
|
||||
width: Self.controlsWidth + 1.0,
|
||||
height: tabHeight
|
||||
)
|
||||
_refreshButton.frame = CGRect(
|
||||
x: 0.0,
|
||||
y: _closeButton.frame.maxY + tabSpacing,
|
||||
width: Self.controlsWidth + 1.0,
|
||||
height: tabHeight
|
||||
)
|
||||
|
||||
view.bringSubviewToFront(_closeButton)
|
||||
view.bringSubviewToFront(_refreshButton)
|
||||
}
|
||||
|
||||
private func _configureControlTab(_ button: UIButton, accessibilityLabel: String)
|
||||
{
|
||||
button.backgroundColor = UIColor(white: 0.2, alpha: 1.0)
|
||||
button.setTitleColor(Self.accentColor, for: .normal)
|
||||
button.accessibilityTraits = .button
|
||||
button.accessibilityLabel = accessibilityLabel
|
||||
button.layer.borderColor = Self.accentColor.withAlphaComponent(0.65).cgColor
|
||||
button.layer.borderWidth = 1.0
|
||||
}
|
||||
|
||||
private func _loadMixer()
|
||||
{
|
||||
_loadingProgressView.isHidden = false
|
||||
_loadingProgressView.progress = 0.0
|
||||
let request = URLRequest(
|
||||
url: Self.mixerURL,
|
||||
cachePolicy: .reloadIgnoringLocalCacheData,
|
||||
timeoutInterval: 30.0
|
||||
)
|
||||
_webView.load(request)
|
||||
}
|
||||
|
||||
@objc private func _close()
|
||||
{
|
||||
closeHandler?()
|
||||
}
|
||||
|
||||
@objc private func _refresh()
|
||||
{
|
||||
if _webView.url == nil {
|
||||
_loadMixer()
|
||||
} else {
|
||||
_webView.reload()
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -19,6 +19,11 @@ class MainViewController: UIViewController, SwitchesViewControllerDelegate, Serv
|
||||
|
||||
fileprivate var _switchesController: SwitchesViewController = SwitchesViewController()
|
||||
fileprivate var _lightsController: SwitchesViewController = SwitchesViewController()
|
||||
fileprivate var _audioSectionView: AudioSectionView = AudioSectionView()
|
||||
fileprivate var _audioMixerController: AudioMixerViewController = AudioMixerViewController()
|
||||
fileprivate var _audioMixerDimmingView: UIControl = UIControl(frame: .zero)
|
||||
fileprivate var _isAudioMixerPresented: Bool = false
|
||||
fileprivate var _isAudioMixerAnimating: Bool = false
|
||||
|
||||
override init(nibName nibNameOrNil: String?, bundle nibBundleOrNil: Bundle?)
|
||||
{
|
||||
@@ -57,8 +62,30 @@ class MainViewController: UIViewController, SwitchesViewControllerDelegate, Serv
|
||||
self.addChild(_lightsController)
|
||||
self.view.addSubview(_lightsController.view)
|
||||
|
||||
_audioSectionView.mixerSettingsHandler = { [weak self] in
|
||||
self?._presentAudioMixer()
|
||||
}
|
||||
self.view.addSubview(_audioSectionView)
|
||||
|
||||
_audioMixerController.closeHandler = { [weak self] in
|
||||
self?._dismissAudioMixer()
|
||||
}
|
||||
|
||||
self.view.addSubview(_headerView)
|
||||
|
||||
_audioMixerDimmingView.backgroundColor = UIColor.black
|
||||
_audioMixerDimmingView.alpha = 0.0
|
||||
_audioMixerDimmingView.isHidden = true
|
||||
_audioMixerDimmingView.accessibilityIdentifier = "audio.dimmingView"
|
||||
_audioMixerDimmingView.accessibilityLabel = "Dismiss Mixer"
|
||||
_audioMixerDimmingView.accessibilityTraits = .button
|
||||
_audioMixerDimmingView.addTarget(
|
||||
self,
|
||||
action: #selector(_audioMixerDimmingViewTapped),
|
||||
for: .touchUpInside
|
||||
)
|
||||
self.view.addSubview(_audioMixerDimmingView)
|
||||
|
||||
_updateConnectivityStatus(.disconnected)
|
||||
_updateSizeClassPresentation()
|
||||
}
|
||||
@@ -82,8 +109,10 @@ class MainViewController: UIViewController, SwitchesViewControllerDelegate, Serv
|
||||
)
|
||||
|
||||
_headerView.frame = headerBounds
|
||||
_audioMixerDimmingView.frame = bounds
|
||||
|
||||
if _visualizationShouldBeVisible() {
|
||||
let sectionSpacing: CGFloat = 8.0
|
||||
let visualizationFrame = CGRect(
|
||||
x: bodyBounds.origin.x,
|
||||
y: bodyBounds.origin.y,
|
||||
@@ -103,31 +132,68 @@ class MainViewController: UIViewController, SwitchesViewControllerDelegate, Serv
|
||||
dy: 0.0
|
||||
)
|
||||
|
||||
_switchesController.view.frame = CGRect(
|
||||
let rightColumnFrame = CGRect(
|
||||
x: visualizationFrame.maxX,
|
||||
y: bodyBounds.origin.y,
|
||||
width: bodyBounds.width - visualizationFrame.width,
|
||||
height: bodyBounds.height
|
||||
)
|
||||
let audioContentHeight = min(
|
||||
AudioSectionView.preferredHeight(forWidth: rightColumnFrame.width),
|
||||
bodyBounds.height * 0.22
|
||||
)
|
||||
let audioBottomInset = view.safeAreaInsets.bottom
|
||||
let audioHeight = audioContentHeight + audioBottomInset
|
||||
_audioSectionView.bottomContentInset = audioBottomInset
|
||||
_switchesController.view.frame = CGRect(
|
||||
x: rightColumnFrame.minX,
|
||||
y: rightColumnFrame.minY,
|
||||
width: rightColumnFrame.width,
|
||||
height: rightColumnFrame.height - audioHeight - sectionSpacing
|
||||
)
|
||||
_audioSectionView.frame = CGRect(
|
||||
x: rightColumnFrame.minX,
|
||||
y: _switchesController.view.frame.maxY + sectionSpacing,
|
||||
width: rightColumnFrame.width,
|
||||
height: audioHeight
|
||||
)
|
||||
} else {
|
||||
let sectionSpacing: CGFloat = 8
|
||||
let switchesHeight = rint(bodyBounds.height * 0.68)
|
||||
let sectionSpacing: CGFloat = 8.0
|
||||
let audioHeight = min(
|
||||
AudioSectionView.preferredHeight(forWidth: bodyBounds.width),
|
||||
bodyBounds.height * 0.18
|
||||
)
|
||||
let lightsHeight = rint(bodyBounds.height * 0.27)
|
||||
let switchesHeight = bodyBounds.height - audioHeight - lightsHeight - (sectionSpacing * 2.0)
|
||||
|
||||
_visualizationController.view.frame = .zero
|
||||
_audioSectionView.bottomContentInset = 0.0
|
||||
_switchesController.view.frame = CGRect(
|
||||
x: bodyBounds.minX,
|
||||
y: bodyBounds.minY,
|
||||
width: bodyBounds.width,
|
||||
height: switchesHeight
|
||||
)
|
||||
_audioSectionView.frame = CGRect(
|
||||
x: bodyBounds.minX + sectionSpacing,
|
||||
y: _switchesController.view.frame.maxY + sectionSpacing,
|
||||
width: bodyBounds.width - (sectionSpacing * 2.0),
|
||||
height: audioHeight
|
||||
)
|
||||
_lightsController.view.frame = CGRect(
|
||||
x: bodyBounds.minX + sectionSpacing,
|
||||
y: bodyBounds.minY + switchesHeight + sectionSpacing,
|
||||
y: _audioSectionView.frame.maxY + sectionSpacing,
|
||||
width: bodyBounds.width - (sectionSpacing * 2),
|
||||
height: bodyBounds.height - switchesHeight - sectionSpacing
|
||||
height: lightsHeight
|
||||
)
|
||||
}
|
||||
|
||||
if _audioMixerController.parent === self, !_isAudioMixerAnimating {
|
||||
_audioMixerController.view.frame = _isAudioMixerPresented
|
||||
? _audioMixerPresentedFrame()
|
||||
: _audioMixerHiddenFrame()
|
||||
}
|
||||
|
||||
_updateSizeClassPresentation()
|
||||
}
|
||||
|
||||
@@ -158,6 +224,11 @@ class MainViewController: UIViewController, SwitchesViewControllerDelegate, Serv
|
||||
return true
|
||||
}
|
||||
|
||||
override var prefersHomeIndicatorAutoHidden: Bool
|
||||
{
|
||||
return true
|
||||
}
|
||||
|
||||
// MARK: SwitchesViewControllerDelegate
|
||||
|
||||
func switchesViewControllerDidToggleDevices(_ controller: SwitchesViewController, devices: [AnyDevice])
|
||||
@@ -215,6 +286,90 @@ class MainViewController: UIViewController, SwitchesViewControllerDelegate, Serv
|
||||
}
|
||||
}
|
||||
|
||||
internal func _presentAudioMixer()
|
||||
{
|
||||
guard !_isAudioMixerPresented, !_isAudioMixerAnimating else { return }
|
||||
|
||||
if _audioMixerController.parent == nil {
|
||||
addChild(_audioMixerController)
|
||||
view.addSubview(_audioMixerController.view)
|
||||
_audioMixerController.didMove(toParent: self)
|
||||
}
|
||||
|
||||
_isAudioMixerPresented = true
|
||||
_isAudioMixerAnimating = true
|
||||
_audioMixerDimmingView.alpha = 0.0
|
||||
_audioMixerDimmingView.isHidden = false
|
||||
_audioMixerController.view.isHidden = false
|
||||
_audioMixerController.view.frame = _audioMixerHiddenFrame()
|
||||
_audioMixerController.view.setNeedsLayout()
|
||||
_audioMixerController.view.layoutIfNeeded()
|
||||
view.bringSubviewToFront(_audioMixerDimmingView)
|
||||
view.bringSubviewToFront(_audioMixerController.view)
|
||||
|
||||
UIView.animate(
|
||||
withDuration: 0.45,
|
||||
delay: 0.0,
|
||||
usingSpringWithDamping: 0.88,
|
||||
initialSpringVelocity: 0.25,
|
||||
options: [.allowUserInteraction, .beginFromCurrentState],
|
||||
animations: {
|
||||
self._audioMixerDimmingView.alpha = 0.55
|
||||
self._audioMixerController.view.frame = self._audioMixerPresentedFrame()
|
||||
},
|
||||
completion: { _ in
|
||||
self._isAudioMixerAnimating = false
|
||||
self._audioMixerController.view.frame = self._audioMixerPresentedFrame()
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
internal func _dismissAudioMixer()
|
||||
{
|
||||
guard _isAudioMixerPresented, !_isAudioMixerAnimating else { return }
|
||||
|
||||
_isAudioMixerPresented = false
|
||||
_isAudioMixerAnimating = true
|
||||
UIView.animate(
|
||||
withDuration: 0.35,
|
||||
delay: 0.0,
|
||||
options: [.curveEaseIn, .allowUserInteraction, .beginFromCurrentState],
|
||||
animations: {
|
||||
self._audioMixerDimmingView.alpha = 0.0
|
||||
self._audioMixerController.view.frame = self._audioMixerHiddenFrame()
|
||||
},
|
||||
completion: { _ in
|
||||
self._isAudioMixerAnimating = false
|
||||
self._audioMixerController.view.isHidden = true
|
||||
self._audioMixerDimmingView.isHidden = true
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
@objc private func _audioMixerDimmingViewTapped()
|
||||
{
|
||||
_dismissAudioMixer()
|
||||
}
|
||||
|
||||
private func _audioMixerPresentedFrame() -> CGRect
|
||||
{
|
||||
let switchesFrame = _switchesController.view.frame
|
||||
return CGRect(
|
||||
x: switchesFrame.minX - AudioMixerViewController.controlsWidth,
|
||||
y: switchesFrame.minY,
|
||||
width: switchesFrame.width + AudioMixerViewController.controlsWidth,
|
||||
height: _audioSectionView.frame.maxY - switchesFrame.minY
|
||||
)
|
||||
}
|
||||
|
||||
private func _audioMixerHiddenFrame() -> CGRect
|
||||
{
|
||||
return _audioMixerPresentedFrame().offsetBy(
|
||||
dx: UIScreen.main.bounds.width,
|
||||
dy: 0.0
|
||||
)
|
||||
}
|
||||
|
||||
internal func _updateConnectivityStatus(_ status: ConnectionStatus)
|
||||
{
|
||||
DispatchQueue.main.async { () -> Void in
|
||||
|
||||
@@ -31,6 +31,8 @@
|
||||
<key>NSAllowsArbitraryLoads</key>
|
||||
<true/>
|
||||
</dict>
|
||||
<key>NSLocalNetworkUsageDescription</key>
|
||||
<string>Connect to the local MOTU audio mixer touch console.</string>
|
||||
<key>UIAppFonts</key>
|
||||
<array>
|
||||
<string>Orbitron-Medium.ttf</string>
|
||||
|
||||
132
XIONControlPanel/Views/AudioSectionView.swift
Normal file
132
XIONControlPanel/Views/AudioSectionView.swift
Normal file
@@ -0,0 +1,132 @@
|
||||
//
|
||||
// AudioSectionView.swift
|
||||
// XIONControlPanel
|
||||
//
|
||||
|
||||
import UIKit
|
||||
|
||||
final class AudioSectionView: UIView
|
||||
{
|
||||
var mixerSettingsHandler: (() -> Void)?
|
||||
var bottomContentInset: CGFloat = 0.0
|
||||
{
|
||||
didSet
|
||||
{
|
||||
if oldValue != bottomContentInset {
|
||||
setNeedsLayout()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private let _label = UILabel(frame: .zero)
|
||||
private let _mixerSettingsButton = AudioActionButton(type: .custom)
|
||||
|
||||
override init(frame: CGRect)
|
||||
{
|
||||
super.init(frame: frame)
|
||||
|
||||
_label.font = UIFont(name: "Orbitron-Medium", size: 14.0)
|
||||
_label.text = "audio"
|
||||
_label.textColor = .white.withAlphaComponent(0.8)
|
||||
addSubview(_label)
|
||||
|
||||
_mixerSettingsButton.setTitle("MIXER SETTINGS", for: .normal)
|
||||
_mixerSettingsButton.titleLabel?.font = UIFont(name: "Orbitron-Medium", size: 21.0)
|
||||
_mixerSettingsButton.setTitleColor(.white, for: .normal)
|
||||
_mixerSettingsButton.accessibilityIdentifier = "audio.mixerSettings"
|
||||
_mixerSettingsButton.accessibilityLabel = "Mixer Settings"
|
||||
_mixerSettingsButton.addTarget(self, action: #selector(_showMixerSettings), for: .touchUpInside)
|
||||
addSubview(_mixerSettingsButton)
|
||||
}
|
||||
|
||||
required init?(coder: NSCoder)
|
||||
{
|
||||
fatalError("unsupported")
|
||||
}
|
||||
|
||||
override func layoutSubviews()
|
||||
{
|
||||
super.layoutSubviews()
|
||||
|
||||
let labelSpacing: CGFloat = 8.0
|
||||
let labelSize = _label.sizeThatFits(bounds.size)
|
||||
_label.frame = CGRect(
|
||||
x: 0.0,
|
||||
y: 0.0,
|
||||
width: labelSize.width,
|
||||
height: labelSize.height
|
||||
)
|
||||
_mixerSettingsButton.frame = CGRect(
|
||||
x: 0.0,
|
||||
y: _label.frame.maxY + labelSpacing,
|
||||
width: bounds.width,
|
||||
height: max(
|
||||
0.0,
|
||||
bounds.height - _label.frame.height - labelSpacing - bottomContentInset
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
static func preferredHeight(forWidth width: CGFloat) -> CGFloat
|
||||
{
|
||||
let cellsPerRow: CGFloat
|
||||
switch width {
|
||||
case 500.0...:
|
||||
cellsPerRow = 4.0
|
||||
case 400.0..<500.0:
|
||||
cellsPerRow = 3.0
|
||||
default:
|
||||
cellsPerRow = 2.0
|
||||
}
|
||||
|
||||
let cellSpacing: CGFloat = 5.0
|
||||
let cellDimension = floor(
|
||||
(width / cellsPerRow) - ((cellSpacing * (cellsPerRow - 1.0)) / cellsPerRow)
|
||||
)
|
||||
let buttonHeight = rint(cellDimension / 1.5)
|
||||
return 17.0 + 8.0 + buttonHeight
|
||||
}
|
||||
|
||||
@objc private func _showMixerSettings()
|
||||
{
|
||||
mixerSettingsHandler?()
|
||||
}
|
||||
}
|
||||
|
||||
private final class AudioActionButton: UIButton
|
||||
{
|
||||
private static let normalBackgroundColor = UIColor(white: 0.2, alpha: 1.0)
|
||||
private static let highlightedBackgroundColor = UIColor(white: 0.8, alpha: 1.0)
|
||||
|
||||
override init(frame: CGRect)
|
||||
{
|
||||
super.init(frame: frame)
|
||||
backgroundColor = Self.normalBackgroundColor
|
||||
accessibilityTraits = .button
|
||||
}
|
||||
|
||||
required init?(coder: NSCoder)
|
||||
{
|
||||
fatalError("unsupported")
|
||||
}
|
||||
|
||||
override var isHighlighted: Bool
|
||||
{
|
||||
didSet
|
||||
{
|
||||
if isHighlighted {
|
||||
backgroundColor = Self.highlightedBackgroundColor
|
||||
} else {
|
||||
UIView.animate(
|
||||
withDuration: 0.35,
|
||||
delay: 0.0,
|
||||
options: [.allowUserInteraction, .beginFromCurrentState],
|
||||
animations: {
|
||||
self.backgroundColor = Self.normalBackgroundColor
|
||||
},
|
||||
completion: nil
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,3 +1,18 @@
|
||||
require "open3"
|
||||
|
||||
def read_plist_value(path, key)
|
||||
value, _error, status = Open3.capture3(
|
||||
"/usr/bin/plutil",
|
||||
"-extract",
|
||||
key,
|
||||
"raw",
|
||||
path
|
||||
)
|
||||
UI.user_error!("Unable to read #{key} from the archived app") unless status.success?
|
||||
|
||||
value.strip
|
||||
end
|
||||
|
||||
default_platform(:ios)
|
||||
|
||||
platform :ios do
|
||||
@@ -44,10 +59,10 @@ platform :ios do
|
||||
"XION.app",
|
||||
"Info.plist"
|
||||
)
|
||||
archived_access_token = get_info_plist_value(
|
||||
path: archived_plist,
|
||||
key: "HomeAssistantAccessToken"
|
||||
).to_s
|
||||
archived_access_token = read_plist_value(
|
||||
archived_plist,
|
||||
"HomeAssistantAccessToken"
|
||||
)
|
||||
unless archived_access_token == home_assistant_access_token
|
||||
UI.user_error!("The archived app does not contain the configured Home Assistant access token")
|
||||
end
|
||||
|
||||
Reference in New Issue
Block a user