@@ -108,6 +108,11 @@ extension _VideoPlayerPlaybackServiceMethods on VideoPlayerScreenState {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (PlatformDetector.isAppleTV() && _isPlaybackMediaControlEvent(event)) {
|
||||||
|
appLogger.d('Media control: ${event.runtimeType} ignored on Apple TV; using native remote bridge');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
if (activePlayer == null && event is! NextTrackEvent && event is! PreviousTrackEvent) return;
|
if (activePlayer == null && event is! NextTrackEvent && event is! PreviousTrackEvent) return;
|
||||||
|
|
||||||
if (event is PlayEvent) {
|
if (event is PlayEvent) {
|
||||||
@@ -248,6 +253,9 @@ extension _VideoPlayerPlaybackServiceMethods on VideoPlayerScreenState {
|
|||||||
event is AudioRouteOldDeviceUnavailableEvent ||
|
event is AudioRouteOldDeviceUnavailableEvent ||
|
||||||
event is AudioRouteNewDeviceAvailableEvent;
|
event is AudioRouteNewDeviceAvailableEvent;
|
||||||
|
|
||||||
|
bool _isPlaybackMediaControlEvent(Object event) =>
|
||||||
|
event is PlayEvent || event is PauseEvent || event is TogglePlayPauseEvent;
|
||||||
|
|
||||||
Future<void> _handleAppleAudioSessionEvent(Object event) async {
|
Future<void> _handleAppleAudioSessionEvent(Object event) async {
|
||||||
if (!Platform.isIOS || PlatformDetector.isTV()) return;
|
if (!Platform.isIOS || PlatformDetector.isTV()) return;
|
||||||
|
|
||||||
|
|||||||
@@ -43,6 +43,7 @@ import '../services/trackers/tracker_coordinator.dart';
|
|||||||
import '../services/trakt/trakt_scrobble_service.dart';
|
import '../services/trakt/trakt_scrobble_service.dart';
|
||||||
import '../services/episode_navigation_service.dart';
|
import '../services/episode_navigation_service.dart';
|
||||||
import '../services/app_foreground_service.dart';
|
import '../services/app_foreground_service.dart';
|
||||||
|
import '../services/apple_tv_remote_touch_service.dart';
|
||||||
import '../services/media_controls_manager.dart';
|
import '../services/media_controls_manager.dart';
|
||||||
import '../services/playback_initialization_service.dart';
|
import '../services/playback_initialization_service.dart';
|
||||||
import '../services/playback_context.dart';
|
import '../services/playback_context.dart';
|
||||||
@@ -300,6 +301,7 @@ class VideoPlayerScreenState extends State<VideoPlayerScreen> with WidgetsBindin
|
|||||||
StreamSubscription<bool>? _playingSubscription;
|
StreamSubscription<bool>? _playingSubscription;
|
||||||
StreamSubscription<bool>? _completedSubscription;
|
StreamSubscription<bool>? _completedSubscription;
|
||||||
StreamSubscription<dynamic>? _mediaControlSubscription;
|
StreamSubscription<dynamic>? _mediaControlSubscription;
|
||||||
|
StreamSubscription<AppleTvRemotePlayPauseAction>? _appleTvPlayPauseSubscription;
|
||||||
StreamSubscription<bool>? _bufferingSubscription;
|
StreamSubscription<bool>? _bufferingSubscription;
|
||||||
StreamSubscription<Duration>? _positionSubscription;
|
StreamSubscription<Duration>? _positionSubscription;
|
||||||
StreamSubscription<void>? _playbackRestartSubscription;
|
StreamSubscription<void>? _playbackRestartSubscription;
|
||||||
@@ -541,6 +543,7 @@ class VideoPlayerScreenState extends State<VideoPlayerScreen> with WidgetsBindin
|
|||||||
WidgetsBinding.instance.addObserver(this);
|
WidgetsBinding.instance.addObserver(this);
|
||||||
|
|
||||||
_setupCompanionRemoteCallbacks();
|
_setupCompanionRemoteCallbacks();
|
||||||
|
_setupAppleTvRemotePlaybackActions();
|
||||||
|
|
||||||
_sleepTimerSubscription = SleepTimerService().onPrompt.listen((_) {
|
_sleepTimerSubscription = SleepTimerService().onPrompt.listen((_) {
|
||||||
if (mounted) _showStillWatchingDialog();
|
if (mounted) _showStillWatchingDialog();
|
||||||
@@ -1161,6 +1164,7 @@ class VideoPlayerScreenState extends State<VideoPlayerScreen> with WidgetsBindin
|
|||||||
_completedSubscription?.cancel();
|
_completedSubscription?.cancel();
|
||||||
_errorSubscription?.cancel();
|
_errorSubscription?.cancel();
|
||||||
_mediaControlSubscription?.cancel();
|
_mediaControlSubscription?.cancel();
|
||||||
|
_appleTvPlayPauseSubscription?.cancel();
|
||||||
_bufferingSubscription?.cancel();
|
_bufferingSubscription?.cancel();
|
||||||
_trackManager?.dispose();
|
_trackManager?.dispose();
|
||||||
_positionSubscription?.cancel();
|
_positionSubscription?.cancel();
|
||||||
@@ -1252,6 +1256,53 @@ class VideoPlayerScreenState extends State<VideoPlayerScreen> with WidgetsBindin
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void _setupAppleTvRemotePlaybackActions() {
|
||||||
|
if (!PlatformDetector.isAppleTV()) return;
|
||||||
|
|
||||||
|
_appleTvPlayPauseSubscription = AppleTvRemoteTouchService.instance.playPauseActions.listen((action) {
|
||||||
|
unawaited(_handleAppleTvRemotePlayPause(action));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> _handleAppleTvRemotePlayPause(AppleTvRemotePlayPauseAction action) async {
|
||||||
|
if (!mounted || ModalRoute.of(context)?.isCurrent != true) return;
|
||||||
|
|
||||||
|
final currentPlayer = player;
|
||||||
|
if (!_isPlayerInitialized || currentPlayer == null) {
|
||||||
|
appLogger.d('Apple TV remote play/pause ignored: player not ready');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!_canControlPlaybackFromRemote()) {
|
||||||
|
appLogger.d('Apple TV remote play/pause ignored: playback control unavailable');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
appLogger.d(
|
||||||
|
'Apple TV remote play/pause received source=${action.source}'
|
||||||
|
'${action.detail == null ? '' : ' detail=${action.detail}'}',
|
||||||
|
);
|
||||||
|
|
||||||
|
try {
|
||||||
|
if (!currentPlayer.state.playing) {
|
||||||
|
await _seekBackForRewind(currentPlayer);
|
||||||
|
if (!mounted || player != currentPlayer) return;
|
||||||
|
}
|
||||||
|
await currentPlayer.playOrPause();
|
||||||
|
} catch (e, st) {
|
||||||
|
appLogger.w('Apple TV remote play/pause failed', error: e, stackTrace: st);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
bool _canControlPlaybackFromRemote() {
|
||||||
|
try {
|
||||||
|
final watchTogether = _watchTogetherProvider ?? context.read<WatchTogetherProvider>();
|
||||||
|
return !watchTogether.isInSession || watchTogether.canControl();
|
||||||
|
} catch (e) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
String? _lastLogError;
|
String? _lastLogError;
|
||||||
bool _sawServer500 = false;
|
bool _sawServer500 = false;
|
||||||
|
|
||||||
|
|||||||
@@ -1,3 +1,5 @@
|
|||||||
|
import 'dart:async';
|
||||||
|
|
||||||
import 'package:flutter/services.dart';
|
import 'package:flutter/services.dart';
|
||||||
import 'package:flutter/widgets.dart';
|
import 'package:flutter/widgets.dart';
|
||||||
|
|
||||||
@@ -7,6 +9,13 @@ import 'gamepad_service.dart';
|
|||||||
|
|
||||||
enum _SwipeAxis { horizontal, vertical }
|
enum _SwipeAxis { horizontal, vertical }
|
||||||
|
|
||||||
|
class AppleTvRemotePlayPauseAction {
|
||||||
|
final String source;
|
||||||
|
final String? detail;
|
||||||
|
|
||||||
|
const AppleTvRemotePlayPauseAction({required this.source, this.detail});
|
||||||
|
}
|
||||||
|
|
||||||
/// Bridges tvOS touch-surface events from Apple's iOS Remote app into the
|
/// Bridges tvOS touch-surface events from Apple's iOS Remote app into the
|
||||||
/// focus-tree key events Plezy already handles for D-pad navigation.
|
/// focus-tree key events Plezy already handles for D-pad navigation.
|
||||||
class AppleTvRemoteTouchService {
|
class AppleTvRemoteTouchService {
|
||||||
@@ -25,6 +34,8 @@ class AppleTvRemoteTouchService {
|
|||||||
final VoidCallback _scheduleFrame;
|
final VoidCallback _scheduleFrame;
|
||||||
final DateTime Function() _now;
|
final DateTime Function() _now;
|
||||||
final GamepadDuplicateInputGuard _duplicateInputGuard;
|
final GamepadDuplicateInputGuard _duplicateInputGuard;
|
||||||
|
final StreamController<AppleTvRemotePlayPauseAction> _playPauseController =
|
||||||
|
StreamController<AppleTvRemotePlayPauseAction>.broadcast();
|
||||||
final double swipeThreshold;
|
final double swipeThreshold;
|
||||||
final double axisSwitchDominanceRatio;
|
final double axisSwitchDominanceRatio;
|
||||||
final Duration swipeRepeatInterval;
|
final Duration swipeRepeatInterval;
|
||||||
@@ -66,6 +77,8 @@ class AppleTvRemoteTouchService {
|
|||||||
_duplicateInputGuard =
|
_duplicateInputGuard =
|
||||||
duplicateInputGuard ?? GamepadDuplicateInputGuard(now: now, suppressionWindow: duplicateSuppressionWindow);
|
duplicateInputGuard ?? GamepadDuplicateInputGuard(now: now, suppressionWindow: duplicateSuppressionWindow);
|
||||||
|
|
||||||
|
Stream<AppleTvRemotePlayPauseAction> get playPauseActions => _playPauseController.stream;
|
||||||
|
|
||||||
void start() {
|
void start() {
|
||||||
if (_listening) return;
|
if (_listening) return;
|
||||||
_channel.setMessageHandler(handleMessage);
|
_channel.setMessageHandler(handleMessage);
|
||||||
@@ -86,6 +99,10 @@ class AppleTvRemoteTouchService {
|
|||||||
|
|
||||||
bool handleNativeKeyEvent(KeyEvent event) {
|
bool handleNativeKeyEvent(KeyEvent event) {
|
||||||
_log('native ${_eventTypeName(event)} logical=${_keyName(event.logicalKey)}');
|
_log('native ${_eventTypeName(event)} logical=${_keyName(event.logicalKey)}');
|
||||||
|
if (_isMediaPlaybackKey(event.logicalKey)) {
|
||||||
|
_log('consume native media key reason=direct-playback-action');
|
||||||
|
return true;
|
||||||
|
}
|
||||||
if (event is KeyDownEvent && _isDirectionalKey(event.logicalKey)) {
|
if (event is KeyDownEvent && _isDirectionalKey(event.logicalKey)) {
|
||||||
_lastDirectionalInputAt = _now();
|
_lastDirectionalInputAt = _now();
|
||||||
}
|
}
|
||||||
@@ -128,6 +145,11 @@ class AppleTvRemoteTouchService {
|
|||||||
_releaseSelectFromClick(source: 'click_e');
|
_releaseSelectFromClick(source: 'click_e');
|
||||||
case 'click_s':
|
case 'click_s':
|
||||||
_pressSelectFromClick();
|
_pressSelectFromClick();
|
||||||
|
case 'play_pause':
|
||||||
|
final source = arguments['source'] is String ? arguments['source'] as String : 'native';
|
||||||
|
final detail = arguments['detail'] is String ? arguments['detail'] as String : null;
|
||||||
|
_log('emit action=play_pause source=$source${detail == null ? '' : ' detail=$detail'}');
|
||||||
|
_playPauseController.add(AppleTvRemotePlayPauseAction(source: source, detail: detail));
|
||||||
case 'loc':
|
case 'loc':
|
||||||
break;
|
break;
|
||||||
default:
|
default:
|
||||||
@@ -333,6 +355,9 @@ class AppleTvRemoteTouchService {
|
|||||||
if (key == LogicalKeyboardKey.select) return 'select';
|
if (key == LogicalKeyboardKey.select) return 'select';
|
||||||
if (key == LogicalKeyboardKey.gameButtonA) return 'gameButtonA';
|
if (key == LogicalKeyboardKey.gameButtonA) return 'gameButtonA';
|
||||||
if (key == LogicalKeyboardKey.escape) return 'escape';
|
if (key == LogicalKeyboardKey.escape) return 'escape';
|
||||||
|
if (key == LogicalKeyboardKey.mediaPlay) return 'mediaPlay';
|
||||||
|
if (key == LogicalKeyboardKey.mediaPause) return 'mediaPause';
|
||||||
|
if (key == LogicalKeyboardKey.mediaPlayPause) return 'mediaPlayPause';
|
||||||
return '0x${key.keyId.toRadixString(16)}';
|
return '0x${key.keyId.toRadixString(16)}';
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -343,6 +368,12 @@ class AppleTvRemoteTouchService {
|
|||||||
key == LogicalKeyboardKey.arrowRight;
|
key == LogicalKeyboardKey.arrowRight;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
bool _isMediaPlaybackKey(LogicalKeyboardKey key) {
|
||||||
|
return key == LogicalKeyboardKey.mediaPlayPause ||
|
||||||
|
key == LogicalKeyboardKey.mediaPlay ||
|
||||||
|
key == LogicalKeyboardKey.mediaPause;
|
||||||
|
}
|
||||||
|
|
||||||
String _formatDouble(double? value) {
|
String _formatDouble(double? value) {
|
||||||
if (value == null) return 'n/a';
|
if (value == null) return 'n/a';
|
||||||
return value.toStringAsFixed(1);
|
return value.toStringAsFixed(1);
|
||||||
|
|||||||
+4
-4
@@ -768,11 +768,11 @@ packages:
|
|||||||
dependency: "direct main"
|
dependency: "direct main"
|
||||||
description:
|
description:
|
||||||
path: "."
|
path: "."
|
||||||
ref: d5c1885
|
ref: f51c805ebc15bf7a2f49a74174aeb470d3c4c78e
|
||||||
resolved-ref: d5c18857ada19896cf456b832c143da435a7155d
|
resolved-ref: f51c805ebc15bf7a2f49a74174aeb470d3c4c78e
|
||||||
url: "https://github.com/edde746/os-media-controls"
|
url: "https://github.com/edde746/media_controls"
|
||||||
source: git
|
source: git
|
||||||
version: "0.2.1"
|
version: "0.2.4"
|
||||||
package_config:
|
package_config:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
|
|||||||
+2
-2
@@ -34,8 +34,8 @@ dependencies:
|
|||||||
path: packages/connectivity_plus/connectivity_plus
|
path: packages/connectivity_plus/connectivity_plus
|
||||||
os_media_controls:
|
os_media_controls:
|
||||||
git:
|
git:
|
||||||
url: https://github.com/edde746/os-media-controls
|
url: https://github.com/edde746/media_controls
|
||||||
ref: d5c1885
|
ref: f51c805ebc15bf7a2f49a74174aeb470d3c4c78e
|
||||||
rate_limiter: ^1.0.0
|
rate_limiter: ^1.0.0
|
||||||
wakelock_plus:
|
wakelock_plus:
|
||||||
git:
|
git:
|
||||||
|
|||||||
+25
-4
@@ -33,6 +33,25 @@ def tvos_flutter_engine_path
|
|||||||
raise "tvos/Podfile: FLUTTER_LOCAL_ENGINE missing from Flutter/Generated.xcconfig"
|
raise "tvos/Podfile: FLUTTER_LOCAL_ENGINE missing from Flutter/Generated.xcconfig"
|
||||||
end
|
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
|
target 'Runner' do
|
||||||
use_frameworks!
|
use_frameworks!
|
||||||
pod 'Flutter', :path => 'Flutter'
|
pod 'Flutter', :path => 'Flutter'
|
||||||
@@ -45,22 +64,24 @@ end
|
|||||||
|
|
||||||
post_install do |installer|
|
post_install do |installer|
|
||||||
engine = tvos_flutter_engine_path
|
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|
|
installer.pods_project.targets.each do |target|
|
||||||
target.build_configurations.each do |config|
|
target.build_configurations.each do |config|
|
||||||
is_debug = config.name.downcase.include?('debug')
|
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*]'] = [
|
config.build_settings['FRAMEWORK_SEARCH_PATHS[sdk=appletvsimulator*]'] = [
|
||||||
'$(inherited)',
|
'$(inherited)',
|
||||||
File.join(engine, 'out', sim_variant),
|
simulator_engine,
|
||||||
]
|
]
|
||||||
config.build_settings['FRAMEWORK_SEARCH_PATHS[sdk=appletvos*]'] = [
|
config.build_settings['FRAMEWORK_SEARCH_PATHS[sdk=appletvos*]'] = [
|
||||||
'$(inherited)',
|
'$(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
|
# 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.
|
# 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['EXCLUDED_ARCHS[sdk=appletvsimulator*]'] = 'x86_64'
|
||||||
|
config.build_settings['OTHER_LDFLAGS'] = '$(inherited) -framework Flutter' if tvos_depends_on_flutter?(target)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -5,6 +5,113 @@ import universal_gamepad
|
|||||||
import os_media_controls
|
import os_media_controls
|
||||||
import wakelock_plus
|
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
|
@main
|
||||||
@objc class AppDelegate: FlutterAppDelegate {
|
@objc class AppDelegate: FlutterAppDelegate {
|
||||||
override func application(
|
override func application(
|
||||||
|
|||||||
@@ -10,7 +10,7 @@
|
|||||||
<!--Flutter View Controller-->
|
<!--Flutter View Controller-->
|
||||||
<scene sceneID="tne-QT-ifu">
|
<scene sceneID="tne-QT-ifu">
|
||||||
<objects>
|
<objects>
|
||||||
<viewController id="BYZ-38-t0r" customClass="FlutterViewController" sceneMemberID="viewController">
|
<viewController id="BYZ-38-t0r" customClass="PlezyFlutterViewController" customModule="Runner" customModuleProvider="target" sceneMemberID="viewController">
|
||||||
<layoutGuides>
|
<layoutGuides>
|
||||||
<viewControllerLayoutGuide type="top" id="y3c-jy-aDJ"/>
|
<viewControllerLayoutGuide type="top" id="y3c-jy-aDJ"/>
|
||||||
<viewControllerLayoutGuide type="bottom" id="wfy-db-euE"/>
|
<viewControllerLayoutGuide type="bottom" id="wfy-db-euE"/>
|
||||||
|
|||||||
@@ -100,16 +100,35 @@ SyncRunnerVersion() {
|
|||||||
SetPlistString "$plist" CFBundleVersion "$FLUTTER_BUILD_NUMBER"
|
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() {
|
BuildAppDebug() {
|
||||||
# Host tools (frontend_server, patched SDK, dartaotruntime) ship in
|
# Host tools (frontend_server, patched SDK, dartaotruntime) ship in
|
||||||
# host_release for both debug and release consumers — the frontend_server
|
# host_release for both debug and release consumers — the frontend_server
|
||||||
# compiles debug kernels regardless of the host build flavor.
|
# compiles debug kernels regardless of the host build flavor.
|
||||||
HOST_TOOLS=$FLUTTER_LOCAL_ENGINE/out/host_release
|
HOST_TOOLS=$FLUTTER_LOCAL_ENGINE/out/host_release
|
||||||
if [[ "$debug_sim" == "true" ]]; then
|
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
|
else
|
||||||
# Device build is always arm64; gn outputs `tvos_debug_unopt` without suffix.
|
# 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
|
fi
|
||||||
|
|
||||||
ROOTDIR=$(dirname "$PROJECT_DIR")
|
ROOTDIR=$(dirname "$PROJECT_DIR")
|
||||||
@@ -420,11 +439,19 @@ BuildApp() {
|
|||||||
echo " └─engine $FLUTTER_LOCAL_ENGINE"
|
echo " └─engine $FLUTTER_LOCAL_ENGINE"
|
||||||
|
|
||||||
|
|
||||||
if [[ "$PLATFORM_NAME" == "appletvsimulator" && "$build_mode" =~ "debug" ]]; then
|
if [[ "$PLATFORM_NAME" == "appletvsimulator" ]]; then
|
||||||
debug_sim="true"
|
debug_sim="true"
|
||||||
|
if [[ ! "$build_mode" =~ "debug" ]]; then
|
||||||
|
echo " └─simulator builds use the debug simulator engine"
|
||||||
|
fi
|
||||||
BuildAppDebug
|
BuildAppDebug
|
||||||
elif [[ "$build_mode" =~ "debug" ]]; then
|
elif [[ "$build_mode" =~ "debug" ]]; then
|
||||||
|
if EngineOutputExists "tvos_debug_unopt"; then
|
||||||
BuildAppDebug
|
BuildAppDebug
|
||||||
|
else
|
||||||
|
echo " └─debug tvOS device engine not found; building release Flutter app"
|
||||||
|
BuildAppRelease
|
||||||
|
fi
|
||||||
elif [[ "$build_mode" =~ "release" ]]; then
|
elif [[ "$build_mode" =~ "release" ]]; then
|
||||||
# release/archive (archive: build mode == "release" && ${ACTION} == "install")
|
# release/archive (archive: build mode == "release" && ${ACTION} == "install")
|
||||||
BuildAppRelease
|
BuildAppRelease
|
||||||
|
|||||||
Reference in New Issue
Block a user