5 Commits

Author SHA1 Message Date
61aa85463c hide home affordance
All checks were successful
TestFlight / Build and upload (push) Successful in 1m11s
2026-07-27 20:55:58 -07:00
fe6ce3a91c MainViewController: bottom safe area inset
All checks were successful
TestFlight / Build and upload (push) Successful in 1m21s
2026-07-27 20:51:39 -07:00
50e5f80a44 adds MOTU audio settings
All checks were successful
TestFlight / Build and upload (push) Successful in 1m20s
2026-07-27 20:11:52 -07:00
cef1770d2f Read archived binary plist with plutil
All checks were successful
TestFlight / Build and upload (push) Successful in 1m29s
2026-07-27 19:38:00 -07:00
4cb775de47 Bake Home Assistant token into release builds
Some checks failed
TestFlight / Build and upload (push) Failing after 25s
2026-07-27 19:28:24 -07:00
10 changed files with 520 additions and 19 deletions

View File

@@ -3,3 +3,4 @@ ASC_ISSUER_ID=
ASC_KEY=
MATCH_PASSWORD=
MATCH_GIT_BASIC_AUTHORIZATION=
HOME_ASSISTANT_ACCESS_TOKEN=

View File

@@ -28,6 +28,7 @@ jobs:
ASC_KEY: ${{ secrets.ASC_KEY }}
MATCH_PASSWORD: ${{ secrets.MATCH_PASSWORD }}
MATCH_GIT_BASIC_AUTHORIZATION: ${{ secrets.MATCH_GIT_BASIC_AUTHORIZATION }}
HOME_ASSISTANT_ACCESS_TOKEN: ${{ secrets.HOME_ASSISTANT_ACCESS_TOKEN }}
CI: "true"
FASTLANE_SKIP_UPDATE_CHECK: "1"
FASTLANE_HIDE_CHANGELOG: "1"

View File

@@ -46,7 +46,10 @@ TestFlight releases
Gitea Actions publishes tags named `release/vX.X.X` to TestFlight. The semantic
version from the tag and the Gitea `GITHUB_RUN_NUMBER` are written into the
app's Info.plist before it is archived. For example:
app's Info.plist before it is archived. The workflow also copies the
`HOME_ASSISTANT_ACCESS_TOKEN` Gitea secret into the app's
`HomeAssistantAccessToken` Info.plist entry and verifies the archived app
contains it. For example:
```sh
git tag -a release/v2.0.0 -m 'Release 2.0.0'
@@ -61,3 +64,4 @@ repository Actions secrets:
- `ASC_KEY`
- `MATCH_PASSWORD`
- `MATCH_GIT_BASIC_AUTHORIZATION`
- `HOME_ASSISTANT_ACCESS_TOKEN`

View File

@@ -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 */,

View File

@@ -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

View 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()
}
}
}

View File

@@ -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

View File

@@ -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>

View 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
)
}
}
}
}

View File

@@ -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
@@ -14,11 +29,17 @@ platform :ios do
build_number = ENV.fetch("GITHUB_RUN_NUMBER")
UI.user_error!("GITHUB_RUN_NUMBER must be a positive integer") unless build_number.match?(/\A[1-9]\d*\z/)
home_assistant_access_token = ENV["HOME_ASSISTANT_ACCESS_TOKEN"]&.strip
if home_assistant_access_token.to_s.empty?
UI.user_error!("HOME_ASSISTANT_ACCESS_TOKEN must be configured for release builds")
end
update_info_plist(
plist_path: "XIONControlPanel/SupportingFiles/Info.plist",
block: proc do |plist|
plist["CFBundleShortVersionString"] = version
plist["CFBundleVersion"] = build_number
plist["HomeAssistantAccessToken"] = home_assistant_access_token
end
)
@@ -31,6 +52,21 @@ platform :ios do
build_app(scheme: "XIONControlPanel")
archived_plist = File.join(
lane_context[SharedValues::XCODEBUILD_ARCHIVE],
"Products",
"Applications",
"XION.app",
"Info.plist"
)
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
upload_to_testflight(
api_key: api_key,
skip_waiting_for_build_processing: true,