Compare commits
6 Commits
release/v2
...
master
| Author | SHA1 | Date | |
|---|---|---|---|
| 61aa85463c | |||
| fe6ce3a91c | |||
| 50e5f80a44 | |||
| cef1770d2f | |||
| 4cb775de47 | |||
| be30286414 |
@@ -3,3 +3,4 @@ ASC_ISSUER_ID=
|
|||||||
ASC_KEY=
|
ASC_KEY=
|
||||||
MATCH_PASSWORD=
|
MATCH_PASSWORD=
|
||||||
MATCH_GIT_BASIC_AUTHORIZATION=
|
MATCH_GIT_BASIC_AUTHORIZATION=
|
||||||
|
HOME_ASSISTANT_ACCESS_TOKEN=
|
||||||
|
|||||||
@@ -28,6 +28,7 @@ jobs:
|
|||||||
ASC_KEY: ${{ secrets.ASC_KEY }}
|
ASC_KEY: ${{ secrets.ASC_KEY }}
|
||||||
MATCH_PASSWORD: ${{ secrets.MATCH_PASSWORD }}
|
MATCH_PASSWORD: ${{ secrets.MATCH_PASSWORD }}
|
||||||
MATCH_GIT_BASIC_AUTHORIZATION: ${{ secrets.MATCH_GIT_BASIC_AUTHORIZATION }}
|
MATCH_GIT_BASIC_AUTHORIZATION: ${{ secrets.MATCH_GIT_BASIC_AUTHORIZATION }}
|
||||||
|
HOME_ASSISTANT_ACCESS_TOKEN: ${{ secrets.HOME_ASSISTANT_ACCESS_TOKEN }}
|
||||||
CI: "true"
|
CI: "true"
|
||||||
FASTLANE_SKIP_UPDATE_CHECK: "1"
|
FASTLANE_SKIP_UPDATE_CHECK: "1"
|
||||||
FASTLANE_HIDE_CHANGELOG: "1"
|
FASTLANE_HIDE_CHANGELOG: "1"
|
||||||
|
|||||||
@@ -46,7 +46,10 @@ TestFlight releases
|
|||||||
|
|
||||||
Gitea Actions publishes tags named `release/vX.X.X` to TestFlight. The semantic
|
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
|
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
|
```sh
|
||||||
git tag -a release/v2.0.0 -m 'Release 2.0.0'
|
git tag -a release/v2.0.0 -m 'Release 2.0.0'
|
||||||
@@ -61,3 +64,4 @@ repository Actions secrets:
|
|||||||
- `ASC_KEY`
|
- `ASC_KEY`
|
||||||
- `MATCH_PASSWORD`
|
- `MATCH_PASSWORD`
|
||||||
- `MATCH_GIT_BASIC_AUTHORIZATION`
|
- `MATCH_GIT_BASIC_AUTHORIZATION`
|
||||||
|
- `HOME_ASSISTANT_ACCESS_TOKEN`
|
||||||
|
|||||||
@@ -14,6 +14,8 @@
|
|||||||
0C3CAEAF1C3350C800B856AD /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 0C3CAEAE1C3350C800B856AD /* Assets.xcassets */; };
|
0C3CAEAF1C3350C800B856AD /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 0C3CAEAE1C3350C800B856AD /* Assets.xcassets */; };
|
||||||
0C3CAEB21C3350C800B856AD /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 0C3CAEB01C3350C800B856AD /* LaunchScreen.storyboard */; };
|
0C3CAEB21C3350C800B856AD /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 0C3CAEB01C3350C800B856AD /* LaunchScreen.storyboard */; };
|
||||||
0C41EF4A1C34B51500AF847F /* SwitchesViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C41EF491C34B51500AF847F /* SwitchesViewController.swift */; };
|
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 */; };
|
0C41EF4E1C34B81F00AF847F /* KSIRingView.m in Sources */ = {isa = PBXBuildFile; fileRef = 0C41EF4D1C34B81F00AF847F /* KSIRingView.m */; };
|
||||||
0C41EF501C34B97700AF847F /* HeaderView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C41EF4F1C34B97700AF847F /* HeaderView.swift */; };
|
0C41EF501C34B97700AF847F /* HeaderView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C41EF4F1C34B97700AF847F /* HeaderView.swift */; };
|
||||||
0C41EF521C34BA4E00AF847F /* Orbitron-Medium.ttf in Resources */ = {isa = PBXBuildFile; fileRef = 0C41EF511C34BA4E00AF847F /* Orbitron-Medium.ttf */; };
|
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>"; };
|
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>"; };
|
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>"; };
|
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>"; };
|
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>"; };
|
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>"; };
|
0C41EF4F1C34B97700AF847F /* HeaderView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = HeaderView.swift; sourceTree = "<group>"; };
|
||||||
@@ -146,6 +150,7 @@
|
|||||||
0C20E2B61C33514100BA789B /* Controllers */ = {
|
0C20E2B61C33514100BA789B /* Controllers */ = {
|
||||||
isa = PBXGroup;
|
isa = PBXGroup;
|
||||||
children = (
|
children = (
|
||||||
|
B10000010000000000000002 /* AudioMixerViewController.swift */,
|
||||||
0C20E2BA1C33529700BA789B /* MainViewController.swift */,
|
0C20E2BA1C33529700BA789B /* MainViewController.swift */,
|
||||||
0C41EF491C34B51500AF847F /* SwitchesViewController.swift */,
|
0C41EF491C34B51500AF847F /* SwitchesViewController.swift */,
|
||||||
0C20E2BC1C33534F00BA789B /* VisualizationViewController.swift */,
|
0C20E2BC1C33534F00BA789B /* VisualizationViewController.swift */,
|
||||||
@@ -167,6 +172,7 @@
|
|||||||
0C20E2B81C33514100BA789B /* Views */ = {
|
0C20E2B81C33514100BA789B /* Views */ = {
|
||||||
isa = PBXGroup;
|
isa = PBXGroup;
|
||||||
children = (
|
children = (
|
||||||
|
B10000010000000000000004 /* AudioSectionView.swift */,
|
||||||
0C41EF551C35ABCD00AF847F /* ConnectionStatusView.swift */,
|
0C41EF551C35ABCD00AF847F /* ConnectionStatusView.swift */,
|
||||||
0C41EF4F1C34B97700AF847F /* HeaderView.swift */,
|
0C41EF4F1C34B97700AF847F /* HeaderView.swift */,
|
||||||
0CF9A48D1C35E9C800276F13 /* SwitchIndicatorView.swift */,
|
0CF9A48D1C35E9C800276F13 /* SwitchIndicatorView.swift */,
|
||||||
@@ -393,6 +399,8 @@
|
|||||||
isa = PBXSourcesBuildPhase;
|
isa = PBXSourcesBuildPhase;
|
||||||
buildActionMask = 2147483647;
|
buildActionMask = 2147483647;
|
||||||
files = (
|
files = (
|
||||||
|
B10000010000000000000001 /* AudioMixerViewController.swift in Sources */,
|
||||||
|
B10000010000000000000003 /* AudioSectionView.swift in Sources */,
|
||||||
0C41EF4E1C34B81F00AF847F /* KSIRingView.m in Sources */,
|
0C41EF4E1C34B81F00AF847F /* KSIRingView.m in Sources */,
|
||||||
0CAA327B1C361CBE00B353CF /* NSErrorAdditions.swift in Sources */,
|
0CAA327B1C361CBE00B353CF /* NSErrorAdditions.swift in Sources */,
|
||||||
CDA64C242E03AE6400E9B07E /* HomeAssistantServer.swift in Sources */,
|
CDA64C242E03AE6400E9B07E /* HomeAssistantServer.swift in Sources */,
|
||||||
|
|||||||
@@ -52,6 +52,18 @@
|
|||||||
ReferencedContainer = "container:XIONControlPanel.xcodeproj">
|
ReferencedContainer = "container:XIONControlPanel.xcodeproj">
|
||||||
</BuildableReference>
|
</BuildableReference>
|
||||||
</MacroExpansion>
|
</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>
|
<Testables>
|
||||||
<TestableReference
|
<TestableReference
|
||||||
skipped = "NO"
|
skipped = "NO"
|
||||||
@@ -65,18 +77,6 @@
|
|||||||
</BuildableReference>
|
</BuildableReference>
|
||||||
</TestableReference>
|
</TestableReference>
|
||||||
</Testables>
|
</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>
|
</TestAction>
|
||||||
<LaunchAction
|
<LaunchAction
|
||||||
buildConfiguration = "Debug"
|
buildConfiguration = "Debug"
|
||||||
@@ -101,7 +101,7 @@
|
|||||||
<EnvironmentVariables>
|
<EnvironmentVariables>
|
||||||
<EnvironmentVariable
|
<EnvironmentVariable
|
||||||
key = "HOME_ASSISTANT_ACCESS_TOKEN"
|
key = "HOME_ASSISTANT_ACCESS_TOKEN"
|
||||||
value = "$(HOME_ASSISTANT_ACCESS_TOKEN)"
|
value = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiI2NzJmZDIyZWE4NmI0ZTc4YTYwNjc2NjJhZjRlZDlmNSIsImlhdCI6MTc4NTIwNjg4OCwiZXhwIjoyMTAwNTY2ODg4fQ.mtL05642nRgDw8ybdxCpRbA9e7rlCzFLIFhO_xAAigc"
|
||||||
isEnabled = "YES">
|
isEnabled = "YES">
|
||||||
</EnvironmentVariable>
|
</EnvironmentVariable>
|
||||||
<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 _switchesController: SwitchesViewController = SwitchesViewController()
|
||||||
fileprivate var _lightsController: 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?)
|
override init(nibName nibNameOrNil: String?, bundle nibBundleOrNil: Bundle?)
|
||||||
{
|
{
|
||||||
@@ -57,8 +62,30 @@ class MainViewController: UIViewController, SwitchesViewControllerDelegate, Serv
|
|||||||
self.addChild(_lightsController)
|
self.addChild(_lightsController)
|
||||||
self.view.addSubview(_lightsController.view)
|
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)
|
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)
|
_updateConnectivityStatus(.disconnected)
|
||||||
_updateSizeClassPresentation()
|
_updateSizeClassPresentation()
|
||||||
}
|
}
|
||||||
@@ -82,8 +109,10 @@ class MainViewController: UIViewController, SwitchesViewControllerDelegate, Serv
|
|||||||
)
|
)
|
||||||
|
|
||||||
_headerView.frame = headerBounds
|
_headerView.frame = headerBounds
|
||||||
|
_audioMixerDimmingView.frame = bounds
|
||||||
|
|
||||||
if _visualizationShouldBeVisible() {
|
if _visualizationShouldBeVisible() {
|
||||||
|
let sectionSpacing: CGFloat = 8.0
|
||||||
let visualizationFrame = CGRect(
|
let visualizationFrame = CGRect(
|
||||||
x: bodyBounds.origin.x,
|
x: bodyBounds.origin.x,
|
||||||
y: bodyBounds.origin.y,
|
y: bodyBounds.origin.y,
|
||||||
@@ -103,31 +132,68 @@ class MainViewController: UIViewController, SwitchesViewControllerDelegate, Serv
|
|||||||
dy: 0.0
|
dy: 0.0
|
||||||
)
|
)
|
||||||
|
|
||||||
_switchesController.view.frame = CGRect(
|
let rightColumnFrame = CGRect(
|
||||||
x: visualizationFrame.maxX,
|
x: visualizationFrame.maxX,
|
||||||
y: bodyBounds.origin.y,
|
y: bodyBounds.origin.y,
|
||||||
width: bodyBounds.width - visualizationFrame.width,
|
width: bodyBounds.width - visualizationFrame.width,
|
||||||
height: bodyBounds.height
|
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 {
|
} else {
|
||||||
let sectionSpacing: CGFloat = 8
|
let sectionSpacing: CGFloat = 8.0
|
||||||
let switchesHeight = rint(bodyBounds.height * 0.68)
|
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
|
_visualizationController.view.frame = .zero
|
||||||
|
_audioSectionView.bottomContentInset = 0.0
|
||||||
_switchesController.view.frame = CGRect(
|
_switchesController.view.frame = CGRect(
|
||||||
x: bodyBounds.minX,
|
x: bodyBounds.minX,
|
||||||
y: bodyBounds.minY,
|
y: bodyBounds.minY,
|
||||||
width: bodyBounds.width,
|
width: bodyBounds.width,
|
||||||
height: switchesHeight
|
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(
|
_lightsController.view.frame = CGRect(
|
||||||
x: bodyBounds.minX + sectionSpacing,
|
x: bodyBounds.minX + sectionSpacing,
|
||||||
y: bodyBounds.minY + switchesHeight + sectionSpacing,
|
y: _audioSectionView.frame.maxY + sectionSpacing,
|
||||||
width: bodyBounds.width - (sectionSpacing * 2),
|
width: bodyBounds.width - (sectionSpacing * 2),
|
||||||
height: bodyBounds.height - switchesHeight - sectionSpacing
|
height: lightsHeight
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if _audioMixerController.parent === self, !_isAudioMixerAnimating {
|
||||||
|
_audioMixerController.view.frame = _isAudioMixerPresented
|
||||||
|
? _audioMixerPresentedFrame()
|
||||||
|
: _audioMixerHiddenFrame()
|
||||||
|
}
|
||||||
|
|
||||||
_updateSizeClassPresentation()
|
_updateSizeClassPresentation()
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -158,6 +224,11 @@ class MainViewController: UIViewController, SwitchesViewControllerDelegate, Serv
|
|||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override var prefersHomeIndicatorAutoHidden: Bool
|
||||||
|
{
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
// MARK: SwitchesViewControllerDelegate
|
// MARK: SwitchesViewControllerDelegate
|
||||||
|
|
||||||
func switchesViewControllerDidToggleDevices(_ controller: SwitchesViewController, devices: [AnyDevice])
|
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)
|
internal func _updateConnectivityStatus(_ status: ConnectionStatus)
|
||||||
{
|
{
|
||||||
DispatchQueue.main.async { () -> Void in
|
DispatchQueue.main.async { () -> Void in
|
||||||
|
|||||||
@@ -22,6 +22,8 @@
|
|||||||
<string>$(CURRENT_PROJECT_VERSION)</string>
|
<string>$(CURRENT_PROJECT_VERSION)</string>
|
||||||
<key>HomeAssistantAccessToken</key>
|
<key>HomeAssistantAccessToken</key>
|
||||||
<string>$(HOME_ASSISTANT_ACCESS_TOKEN)</string>
|
<string>$(HOME_ASSISTANT_ACCESS_TOKEN)</string>
|
||||||
|
<key>ITSAppUsesNonExemptEncryption</key>
|
||||||
|
<false/>
|
||||||
<key>LSRequiresIPhoneOS</key>
|
<key>LSRequiresIPhoneOS</key>
|
||||||
<true/>
|
<true/>
|
||||||
<key>NSAppTransportSecurity</key>
|
<key>NSAppTransportSecurity</key>
|
||||||
@@ -29,6 +31,8 @@
|
|||||||
<key>NSAllowsArbitraryLoads</key>
|
<key>NSAllowsArbitraryLoads</key>
|
||||||
<true/>
|
<true/>
|
||||||
</dict>
|
</dict>
|
||||||
|
<key>NSLocalNetworkUsageDescription</key>
|
||||||
|
<string>Connect to the local MOTU audio mixer touch console.</string>
|
||||||
<key>UIAppFonts</key>
|
<key>UIAppFonts</key>
|
||||||
<array>
|
<array>
|
||||||
<string>Orbitron-Medium.ttf</string>
|
<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)
|
default_platform(:ios)
|
||||||
|
|
||||||
platform :ios do
|
platform :ios do
|
||||||
@@ -14,11 +29,17 @@ platform :ios do
|
|||||||
build_number = ENV.fetch("GITHUB_RUN_NUMBER")
|
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/)
|
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(
|
update_info_plist(
|
||||||
plist_path: "XIONControlPanel/SupportingFiles/Info.plist",
|
plist_path: "XIONControlPanel/SupportingFiles/Info.plist",
|
||||||
block: proc do |plist|
|
block: proc do |plist|
|
||||||
plist["CFBundleShortVersionString"] = version
|
plist["CFBundleShortVersionString"] = version
|
||||||
plist["CFBundleVersion"] = build_number
|
plist["CFBundleVersion"] = build_number
|
||||||
|
plist["HomeAssistantAccessToken"] = home_assistant_access_token
|
||||||
end
|
end
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -31,6 +52,21 @@ platform :ios do
|
|||||||
|
|
||||||
build_app(scheme: "XIONControlPanel")
|
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(
|
upload_to_testflight(
|
||||||
api_key: api_key,
|
api_key: api_key,
|
||||||
skip_waiting_for_build_processing: true,
|
skip_waiting_for_build_processing: true,
|
||||||
|
|||||||
Reference in New Issue
Block a user