+25
-4
@@ -33,6 +33,25 @@ def tvos_flutter_engine_path
|
||||
raise "tvos/Podfile: FLUTTER_LOCAL_ENGINE missing from Flutter/Generated.xcconfig"
|
||||
end
|
||||
|
||||
def tvos_engine_output(engine, *variants)
|
||||
variants.each do |variant|
|
||||
candidate = File.join(engine, 'out', variant)
|
||||
return candidate if Dir.exist?(candidate)
|
||||
end
|
||||
|
||||
raise "tvos/Podfile: none of these Flutter engine outputs exist: #{variants.join(', ')}"
|
||||
end
|
||||
|
||||
def tvos_depends_on_flutter?(target, visited = {})
|
||||
return false if visited[target.uuid]
|
||||
|
||||
visited[target.uuid] = true
|
||||
target.dependencies.any? do |dependency|
|
||||
dependency.name == 'Flutter' ||
|
||||
(dependency.respond_to?(:target) && dependency.target && tvos_depends_on_flutter?(dependency.target, visited))
|
||||
end
|
||||
end
|
||||
|
||||
target 'Runner' do
|
||||
use_frameworks!
|
||||
pod 'Flutter', :path => 'Flutter'
|
||||
@@ -45,22 +64,24 @@ end
|
||||
|
||||
post_install do |installer|
|
||||
engine = tvos_flutter_engine_path
|
||||
simulator_engine = tvos_engine_output(engine, 'tvos_debug_sim_unopt_arm64', 'tvos_debug_sim_unopt')
|
||||
debug_device_engine = tvos_engine_output(engine, 'tvos_debug_unopt', 'tvos_release')
|
||||
release_device_engine = tvos_engine_output(engine, 'tvos_release')
|
||||
installer.pods_project.targets.each do |target|
|
||||
target.build_configurations.each do |config|
|
||||
is_debug = config.name.downcase.include?('debug')
|
||||
sim_variant = is_debug ? 'tvos_debug_sim_unopt_arm64' : 'tvos_release'
|
||||
dev_variant = is_debug ? 'tvos_debug_unopt' : 'tvos_release'
|
||||
config.build_settings['FRAMEWORK_SEARCH_PATHS[sdk=appletvsimulator*]'] = [
|
||||
'$(inherited)',
|
||||
File.join(engine, 'out', sim_variant),
|
||||
simulator_engine,
|
||||
]
|
||||
config.build_settings['FRAMEWORK_SEARCH_PATHS[sdk=appletvos*]'] = [
|
||||
'$(inherited)',
|
||||
File.join(engine, 'out', dev_variant),
|
||||
is_debug ? debug_device_engine : release_device_engine,
|
||||
]
|
||||
# Engine Flutter.framework ships arm64-only for simulator; exclude x86_64
|
||||
# so the link step doesn't look for a slice that doesn't exist.
|
||||
config.build_settings['EXCLUDED_ARCHS[sdk=appletvsimulator*]'] = 'x86_64'
|
||||
config.build_settings['OTHER_LDFLAGS'] = '$(inherited) -framework Flutter' if tvos_depends_on_flutter?(target)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
@@ -5,6 +5,113 @@ import universal_gamepad
|
||||
import os_media_controls
|
||||
import wakelock_plus
|
||||
|
||||
@objc class PlezyFlutterViewController: FlutterViewController {
|
||||
private lazy var tvRemoteChannel = FlutterBasicMessageChannel(
|
||||
name: "flutter/gamepadtouchevent",
|
||||
binaryMessenger: binaryMessenger,
|
||||
codec: FlutterJSONMessageCodec.sharedInstance()
|
||||
)
|
||||
|
||||
override var canBecomeFirstResponder: Bool {
|
||||
true
|
||||
}
|
||||
|
||||
override func viewDidAppear(_ animated: Bool) {
|
||||
super.viewDidAppear(animated)
|
||||
becomeFirstResponder()
|
||||
}
|
||||
|
||||
override func viewWillDisappear(_ animated: Bool) {
|
||||
resignFirstResponder()
|
||||
super.viewWillDisappear(animated)
|
||||
}
|
||||
|
||||
override func pressesBegan(_ presses: Set<UIPress>, with event: UIPressesEvent?) {
|
||||
if handlePlayPausePress(presses) {
|
||||
return
|
||||
}
|
||||
|
||||
super.pressesBegan(presses, with: event)
|
||||
}
|
||||
|
||||
override func pressesEnded(_ presses: Set<UIPress>, with event: UIPressesEvent?) {
|
||||
if containsPlayPausePress(presses) {
|
||||
return
|
||||
}
|
||||
|
||||
super.pressesEnded(presses, with: event)
|
||||
}
|
||||
|
||||
override func pressesCancelled(_ presses: Set<UIPress>, with event: UIPressesEvent?) {
|
||||
if containsPlayPausePress(presses) {
|
||||
return
|
||||
}
|
||||
|
||||
super.pressesCancelled(presses, with: event)
|
||||
}
|
||||
|
||||
override func remoteControlReceived(with event: UIEvent?) {
|
||||
guard let event = event else {
|
||||
super.remoteControlReceived(with: event)
|
||||
return
|
||||
}
|
||||
|
||||
let subtype = event.subtype
|
||||
print("PlezyTvRemote: remote control event subtype=\(remoteControlSubtypeName(subtype))")
|
||||
switch subtype {
|
||||
case .remoteControlPlay, .remoteControlPause, .remoteControlTogglePlayPause:
|
||||
sendPlayPauseEvent(source: "remote_control", detail: remoteControlSubtypeName(subtype))
|
||||
default:
|
||||
super.remoteControlReceived(with: event)
|
||||
}
|
||||
}
|
||||
|
||||
private func handlePlayPausePress(_ presses: Set<UIPress>) -> Bool {
|
||||
guard containsPlayPausePress(presses) else { return false }
|
||||
|
||||
sendPlayPauseEvent(source: "presses", detail: "playPause")
|
||||
return true
|
||||
}
|
||||
|
||||
private func containsPlayPausePress(_ presses: Set<UIPress>) -> Bool {
|
||||
presses.contains { press in
|
||||
press.type == .playPause
|
||||
}
|
||||
}
|
||||
|
||||
private func sendPlayPauseEvent(source: String, detail: String) {
|
||||
print("PlezyTvRemote: intercepted play/pause source=\(source) detail=\(detail)")
|
||||
tvRemoteChannel.sendMessage(["type": "play_pause", "source": source, "detail": detail])
|
||||
}
|
||||
|
||||
private func remoteControlSubtypeName(_ subtype: UIEvent.EventSubtype) -> String {
|
||||
switch subtype {
|
||||
case .remoteControlPlay:
|
||||
return "remoteControlPlay"
|
||||
case .remoteControlPause:
|
||||
return "remoteControlPause"
|
||||
case .remoteControlTogglePlayPause:
|
||||
return "remoteControlTogglePlayPause"
|
||||
case .remoteControlStop:
|
||||
return "remoteControlStop"
|
||||
case .remoteControlNextTrack:
|
||||
return "remoteControlNextTrack"
|
||||
case .remoteControlPreviousTrack:
|
||||
return "remoteControlPreviousTrack"
|
||||
case .remoteControlBeginSeekingForward:
|
||||
return "remoteControlBeginSeekingForward"
|
||||
case .remoteControlEndSeekingForward:
|
||||
return "remoteControlEndSeekingForward"
|
||||
case .remoteControlBeginSeekingBackward:
|
||||
return "remoteControlBeginSeekingBackward"
|
||||
case .remoteControlEndSeekingBackward:
|
||||
return "remoteControlEndSeekingBackward"
|
||||
default:
|
||||
return "unknown(\(subtype.rawValue))"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@main
|
||||
@objc class AppDelegate: FlutterAppDelegate {
|
||||
override func application(
|
||||
|
||||
@@ -10,7 +10,7 @@
|
||||
<!--Flutter View Controller-->
|
||||
<scene sceneID="tne-QT-ifu">
|
||||
<objects>
|
||||
<viewController id="BYZ-38-t0r" customClass="FlutterViewController" sceneMemberID="viewController">
|
||||
<viewController id="BYZ-38-t0r" customClass="PlezyFlutterViewController" customModule="Runner" customModuleProvider="target" sceneMemberID="viewController">
|
||||
<layoutGuides>
|
||||
<viewControllerLayoutGuide type="top" id="y3c-jy-aDJ"/>
|
||||
<viewControllerLayoutGuide type="bottom" id="wfy-db-euE"/>
|
||||
|
||||
@@ -100,16 +100,35 @@ SyncRunnerVersion() {
|
||||
SetPlistString "$plist" CFBundleVersion "$FLUTTER_BUILD_NUMBER"
|
||||
}
|
||||
|
||||
EngineOutputExists() {
|
||||
local variant="$1"
|
||||
[[ -d "$FLUTTER_LOCAL_ENGINE/out/$variant" ]]
|
||||
}
|
||||
|
||||
ResolveEngineOutput() {
|
||||
local variant
|
||||
for variant in "$@"; do
|
||||
local candidate="$FLUTTER_LOCAL_ENGINE/out/$variant"
|
||||
if [[ -d "$candidate" ]]; then
|
||||
printf '%s\n' "$candidate"
|
||||
return 0
|
||||
fi
|
||||
done
|
||||
|
||||
echo " └─ERROR: none of these Flutter engine outputs exist: $*" >&2
|
||||
return 1
|
||||
}
|
||||
|
||||
BuildAppDebug() {
|
||||
# Host tools (frontend_server, patched SDK, dartaotruntime) ship in
|
||||
# host_release for both debug and release consumers — the frontend_server
|
||||
# compiles debug kernels regardless of the host build flavor.
|
||||
HOST_TOOLS=$FLUTTER_LOCAL_ENGINE/out/host_release
|
||||
if [[ "$debug_sim" == "true" ]]; then
|
||||
DEVICE_TOOLS=$FLUTTER_LOCAL_ENGINE/out/tvos_debug_sim_unopt$TARGET_POSTFIX
|
||||
DEVICE_TOOLS=$(ResolveEngineOutput "tvos_debug_sim_unopt$TARGET_POSTFIX" "tvos_debug_sim_unopt_arm64" "tvos_debug_sim_unopt") || return 1
|
||||
else
|
||||
# Device build is always arm64; gn outputs `tvos_debug_unopt` without suffix.
|
||||
DEVICE_TOOLS=$FLUTTER_LOCAL_ENGINE/out/tvos_debug_unopt
|
||||
DEVICE_TOOLS=$(ResolveEngineOutput "tvos_debug_unopt") || return 1
|
||||
fi
|
||||
|
||||
ROOTDIR=$(dirname "$PROJECT_DIR")
|
||||
@@ -420,11 +439,19 @@ BuildApp() {
|
||||
echo " └─engine $FLUTTER_LOCAL_ENGINE"
|
||||
|
||||
|
||||
if [[ "$PLATFORM_NAME" == "appletvsimulator" && "$build_mode" =~ "debug" ]]; then
|
||||
if [[ "$PLATFORM_NAME" == "appletvsimulator" ]]; then
|
||||
debug_sim="true"
|
||||
if [[ ! "$build_mode" =~ "debug" ]]; then
|
||||
echo " └─simulator builds use the debug simulator engine"
|
||||
fi
|
||||
BuildAppDebug
|
||||
elif [[ "$build_mode" =~ "debug" ]]; then
|
||||
BuildAppDebug
|
||||
if EngineOutputExists "tvos_debug_unopt"; then
|
||||
BuildAppDebug
|
||||
else
|
||||
echo " └─debug tvOS device engine not found; building release Flutter app"
|
||||
BuildAppRelease
|
||||
fi
|
||||
elif [[ "$build_mode" =~ "release" ]]; then
|
||||
# release/archive (archive: build mode == "release" && ${ACTION} == "install")
|
||||
BuildAppRelease
|
||||
|
||||
Reference in New Issue
Block a user