Skip to content
Merged

6.2 #370

Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
109 commits
Select commit Hold shift + click to select a range
4c23c5f
iOS: fix bluetooth permission
jonasbark Jun 19, 2026
3b2f87b
Translate 21 missing keys into de/es/fr/it/pl
jonasbark Jun 21, 2026
d82fcd3
Bump prop submodule: add AdvertisedServiceRegistry for mDNS diagnostics
jonasbark Jun 22, 2026
7123489
Bump prop submodule: expose multicast-lock status
jonasbark Jun 22, 2026
17671f3
Bump prop submodule: record advertised services; expose hostLabel + lock
jonasbark Jun 22, 2026
7fb9186
Bump prop submodule: AdvertisedAddressPicker.report()
jonasbark Jun 22, 2026
84b9a69
Bump prop submodule: track active ResilientTcpServers
jonasbark Jun 22, 2026
d8e8930
feat: add mDNS discovery scan for diagnostics (openbikecontrol + wahoo)
jonasbark Jun 22, 2026
54e90b4
feat: add DebugDiagnostics aggregator and text formatter
jonasbark Jun 22, 2026
4dc226a
feat: embed diagnostics block in debugText; skip discovery on crash path
jonasbark Jun 22, 2026
0db4191
feat: attach full debugText (diagnostics + logs) to device feedback
jonasbark Jun 22, 2026
0d2e8b7
feat: show diagnostics block above the Logs page with refresh + share
jonasbark Jun 22, 2026
91ade72
Bump prop submodule: fix dircon integration test for multicast-lock g…
jonasbark Jun 22, 2026
91b362b
Bump prop submodule: drop redundant import in advertised_service_regi…
jonasbark Jun 22, 2026
4e035d0
feat: label OpenBikeControl TCP server; gate iOS local-network infere…
jonasbark Jun 22, 2026
0f5ff8a
Bump prop submodule: label DirCon/Click TCP servers
jonasbark Jun 22, 2026
8e6966b
refactor: drop Bluetooth/Location from diagnostics permissions; keep …
jonasbark Jun 22, 2026
74d78f7
fix: guard crash-logger debugText call; migrate widget test to new pe…
jonasbark Jun 22, 2026
16007d4
style: tidy diagnostics Permissions line; omit when nothing to show
jonasbark Jun 22, 2026
07ae51a
perf: open support chat instantly; gather diagnostics in background
jonasbark Jun 22, 2026
ccc9180
style: sort diagnostics TXT entries alphabetically by key
jonasbark Jun 22, 2026
37bc7cc
fix UI issue
jonasbark Jun 24, 2026
d3f72db
fix OpenBikeControl failed to advertise: Advertising has already started
jonasbark Jun 24, 2026
14467bc
feat: front-shift config fields on ShiftingConfig
jonasbark Jun 24, 2026
c9c74d4
feat: add frontShift in-game action + l10n
jonasbark Jun 24, 2026
b990a6c
feat: seed chainring config into FitnessBikeDefinition
jonasbark Jun 24, 2026
935c074
feat: route frontShift to FitnessBikeDefinition (direct path)
jonasbark Jun 24, 2026
464d909
feat: supportsNativeFrontShift capability flag (Zwift=true)
jonasbark Jun 24, 2026
8f81535
chore(6.2): scaffold screen recording deps and in-repo plugin
jonasbark Jun 24, 2026
7184f89
feat(6.2): add ScreenRecordingService with testable backend seam
jonasbark Jun 24, 2026
11b48d1
fix(6.2): screen recording service never-throws contract + startedRec…
jonasbark Jun 24, 2026
21744dc
feat(zwift): forward frontShift as both-shifters combo frame
jonasbark Jun 24, 2026
24dc1bd
feat(6.2): add screenRecording action enum + localized strings
jonasbark Jun 24, 2026
61c02d4
feat(6.2): register core.screenRecording with platform backend factory
jonasbark Jun 24, 2026
6435780
feat: both-shifters combo (coincidence window) + BaseActions helpers
jonasbark Jun 24, 2026
2491fa1
feat(6.2): handle screenRecording action in base_actions
jonasbark Jun 24, 2026
3bd3619
feat: same-frame both-shifters combo → frontShift (Ride/Click)
jonasbark Jun 24, 2026
cda77d1
feat(6.2): add Record Screen editor card and recording indicator
jonasbark Jun 24, 2026
825d70b
feat: front-shift config UI (enable + chainring teeth)
jonasbark Jun 24, 2026
48fbb47
feat(6.2): Android screen recording backend (MediaProjection + galler…
jonasbark Jun 24, 2026
4a657ac
feat: show active front chainring on gear hero card
jonasbark Jun 24, 2026
985dc7c
docs: changelog for SRAM-style front shifting
jonasbark Jun 24, 2026
53faca8
Bump prop submodule: front chainring toggle
jonasbark Jun 24, 2026
08f0848
fix(6.2): screen_recorder plugin declares no native platforms yet (un…
jonasbark Jun 24, 2026
f051c4c
chore(6.2): lock new deps + regenerate plugin registrants (gal; no sc…
jonasbark Jun 24, 2026
91b63b8
feat(6.2): macOS screen recording backend (ScreenCaptureKit)
jonasbark Jun 24, 2026
5f8fdf8
fix(6.2): macOS capture robustness (native-res, finalize check, main-…
jonasbark Jun 24, 2026
c17be4c
fix(6.2): macOS capture at native (Retina) pixel resolution via CGDis…
jonasbark Jun 24, 2026
43723f8
feat(6.2): Windows screen recording backend (WGC + Media Foundation) …
jonasbark Jun 24, 2026
e90ed89
feat(6.2): iOS screen recording backend (ReplayKit broadcast extensio…
jonasbark Jun 24, 2026
520bd91
docs(6.2): native backend setup + status (macОS/iOS/Windows manual st…
jonasbark Jun 24, 2026
625695e
refactor(ios): extract shared GearSnapshot + add GearReadoutView for PiP
jonasbark Jun 24, 2026
898b0e8
feat(ios): add DeviceCapabilities (Dynamic Island detection, PiP elig…
jonasbark Jun 24, 2026
e6927be
feat(ios): add PipGearController and Picture in Picture background mode
jonasbark Jun 24, 2026
38c6c70
fix(ios): check pixel buffer pool creation; document main-actor pump …
jonasbark Jun 24, 2026
0a1f485
feat(ios): wire bike_control/pip_ios method channel
jonasbark Jun 24, 2026
be63340
feat(overlay): add IosPipController + shared overlayStateToActivityMap
jonasbark Jun 24, 2026
52252bf
feat(overlay): drive PiP gear window from IosOverlayController on eli…
jonasbark Jun 24, 2026
5dd0a38
fix(overlay): guard PiP stop() in hide() so _pipActive always resets
jonasbark Jun 24, 2026
adaef40
fix(ios): move fromLiveActivity to extension-only file so Runner comp…
jonasbark Jun 24, 2026
9b070f0
fix(ios): run PiP render path on the main actor (ImageRenderer is @Ma…
jonasbark Jun 24, 2026
2d15f80
fix(ios): render PiP frames — IOSurface-backed pixel buffers + Displa…
jonasbark Jun 24, 2026
b1f0e25
docs(pip): changelog entry + overlay subtitle reflects floating PiP w…
jonasbark Jun 24, 2026
b43a7fd
feat(6.2): move Record Screen under Weitere Aktionen (below screensho…
jonasbark Jun 24, 2026
f403964
feat(pip): opt-in on Dynamic-Island iPhones + immediate floating wind…
jonasbark Jun 24, 2026
4befad7
feat(6.2): show reveal-recording action on mobile too (opens gallery …
jonasbark Jun 24, 2026
756a4c8
fix(6.2): iOS broadcast stop — reuse existing App Group + register st…
jonasbark Jun 24, 2026
ed99c08
fix(6.2): iOS broadcast stop via App Group stop-flag poll + heartbeat…
jonasbark Jun 24, 2026
bc6867e
cleanup
jonasbark Jun 24, 2026
46570d5
Merge feat/screen-recording into 6.2: screen recording action (Androi…
jonasbark Jun 24, 2026
29edc83
refactor: remove dead supportsNativeFrontShift getter
jonasbark Jun 24, 2026
4bdd0bb
feat(6.2): make Record Screen a Pro-only action (proGuard + editor ba…
jonasbark Jun 24, 2026
157d9d1
update changelog
jonasbark Jun 25, 2026
9300138
fix(obc): resolve "Address already in use" on the OpenBikeControl server
jonasbark Jun 25, 2026
38ea171
fix(ui): guard async setState/context against disposed widgets
jonasbark Jun 25, 2026
b619d94
fix(zwift): put Rouvy advertisement service UUIDs in the scan response
jonasbark Jun 25, 2026
2d73fa8
Merge branch '6.1' into 6.2
jonasbark Jun 25, 2026
1488406
refactor: share BLE advertising "already started" recovery via mixin
jonasbark Jun 25, 2026
7c2a504
Bump prop submodule: fix Zwift Sync grade double sign-decode
jonasbark Jun 25, 2026
06dcef0
Merge branch '6.1' into 6.2
jonasbark Jun 25, 2026
5e7eff0
feat(proxy): show transport icon to disambiguate same-named trainers
jonasbark Jun 25, 2026
4602ba9
fix(screen_recorder): qualify IDirect3DDxgiInterfaceAccess to fix Win…
jonasbark Jun 25, 2026
9e60e53
fix(screen_recorder): correct upside-down Windows recording
jonasbark Jun 25, 2026
f22ab69
chore(deps): point universal_ble fork at rebased 2.1.0 + iOS patch
jonasbark Jun 25, 2026
c9c1710
test(obc): cover PeripheralAdvertisingRecovery advertising recovery
jonasbark Jun 25, 2026
1cadf45
test(obc): cover app-info fragment reassembly; extract AppInfoReassem…
jonasbark Jun 25, 2026
acb32a2
feat(shifting): rename "SRAM-style front shift" to "Virtual front der…
jonasbark Jun 26, 2026
c5a7fcd
feat(shifting): show 2×N gear position when the front derailleur is a…
jonasbark Jun 26, 2026
e905591
docs(changelog): reword front shift as "Virtual front derailleur"
jonasbark Jun 26, 2026
7cfc8fa
fix(overlay): centre the gear value and even out padding
jonasbark Jun 26, 2026
eaf972f
test(screenshot): blog widget snapshots for the front derailleur
jonasbark Jun 26, 2026
dacdec5
test: golden scenes for MyWhoosh setup widgets
jonasbark Jun 26, 2026
b24a6c5
test: generate MyWhoosh setup widgets per locale (en,de,es,fr,it)
jonasbark Jun 26, 2026
83e6ca5
test: full connection-card goldens (with action badges) for 8 control…
jonasbark Jun 26, 2026
bbb663b
test: localized Rouvy & TrainingPeaks setup widget goldens
jonasbark Jun 26, 2026
5fd2a7a
test: render real per-app connection-method tiles for Rouvy & Trainin…
jonasbark Jun 26, 2026
364eecb
test: localized overview/main-screen goldens (real device names) for …
jonasbark Jun 26, 2026
24948d0
adjust changelog
jonasbark Jun 26, 2026
2ffde87
fix(shifting): apply front-derailleur settings live; bound chainrings…
jonasbark Jun 26, 2026
de7f914
fix(obc): reset app-info reassembler on disconnect and bound the buffer
jonasbark Jun 26, 2026
2bc2489
fix(screen-recording): guard re-entrant toggle; recover native start …
jonasbark Jun 26, 2026
73b62b8
fix(overlay): widen drag hit area; fix PiP show/hide race; orientatio…
jonasbark Jun 26, 2026
57207e3
changelog date
jonasbark Jun 26, 2026
7978a12
feat(help): link to the bikecontrol.app setup guide for the controlle…
jonasbark Jun 26, 2026
71d04dc
test(screenshots): refresh Customization goldens for the setup-guide …
jonasbark Jun 26, 2026
478c43b
version++
jonasbark Jun 26, 2026
955e1f7
version++
jonasbark Jun 26, 2026
c4a540a
fix(ios): drop invalid 'picture-in-picture' UIBackgroundMode
jonasbark Jun 26, 2026
26d136b
chore: bump build to 6.2.0+142 for a new iOS build
jonasbark Jun 26, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
26 changes: 13 additions & 13 deletions .github/workflows/build.yml
Original file line number Diff line number Diff line change
Expand Up @@ -80,19 +80,6 @@ jobs:
with:
flutter-version: ${{ env.FLUTTER_VERSION }}

- name: 🚀 Shorebird Release macOS
if: inputs.build_mac && false
uses: shorebirdtech/shorebird-release@v1
with:
flutter-version: ${{ env.FLUTTER_VERSION }}
platform: macos
args: "--obfuscate --split-debug-info=symbols -- --dart-define=VERIFYING_SHARED_SECRET=${{ secrets.VERIFYING_SHARED_SECRET }} --dart-define=REVENUECAT_API_KEY_IOS=${{ secrets.REVENUECAT_API_KEY_IOS }} --dart-define=SUPABASE_ANON_KEY=${{ secrets.SUPABASE_ANON_KEY }}"

- name: Flutter Release macOS
if: inputs.build_mac
run:
flutter build macos --release --obfuscate --split-debug-info=symbols --dart-define=VERIFYING_SHARED_SECRET=${{ secrets.VERIFYING_SHARED_SECRET }} --dart-define=REVENUECAT_API_KEY_IOS=${{ secrets.REVENUECAT_API_KEY_IOS }} --dart-define=SUPABASE_ANON_KEY=${{ secrets.SUPABASE_ANON_KEY }}

- name: Decode Keystore
if: inputs.build_android
uses: ./.github/actions/decode-keystore
Expand All @@ -108,6 +95,19 @@ jobs:
platform: android
args: "--obfuscate --split-debug-info=symbols -- --dart-define=REVENUECAT_API_KEY_ANDROID=${{ secrets.REVENUECAT_API_KEY_ANDROID }} --dart-define=SUPABASE_ANON_KEY=${{ secrets.SUPABASE_ANON_KEY }}"

- name: 🚀 Shorebird Release macOS
if: inputs.build_mac && false
uses: shorebirdtech/shorebird-release@v1
with:
flutter-version: ${{ env.FLUTTER_VERSION }}
platform: macos
args: "--obfuscate --split-debug-info=symbols -- --dart-define=VERIFYING_SHARED_SECRET=${{ secrets.VERIFYING_SHARED_SECRET }} --dart-define=REVENUECAT_API_KEY_IOS=${{ secrets.REVENUECAT_API_KEY_IOS }} --dart-define=SUPABASE_ANON_KEY=${{ secrets.SUPABASE_ANON_KEY }}"

- name: Flutter Release macOS
if: inputs.build_mac
run:
flutter build macos --release --obfuscate --split-debug-info=symbols --dart-define=VERIFYING_SHARED_SECRET=${{ secrets.VERIFYING_SHARED_SECRET }} --dart-define=REVENUECAT_API_KEY_IOS=${{ secrets.REVENUECAT_API_KEY_IOS }} --dart-define=SUPABASE_ANON_KEY=${{ secrets.SUPABASE_ANON_KEY }}

- name: Extract latest changelog
id: changelog
run: |
Expand Down
7 changes: 7 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,10 @@
### 6.2.0 (26-06-2026)
**Features**:
- Virtual front derailleur: adds a second chainring (2× drivetrain) for more realism. The new assignable "Front Shift (Chainring)" action — or pressing both shifters together, SRAM-AXS style — toggles between your small and large rings, changing resistance by the exact chainring ratio while staying on the same rear cog. While it's active your current gear reads as head-unit-style position notation (e.g. 2×14) in the app, the gear overlay, and the iOS Live Activity. Set your chainring sizes per trainer in the gear settings.
- SRAM style shifting: when you click shift up & down at the same time, the front chainring switches
- iOS / iPadOS: Floating gear overlay (Picture-in-Picture): your current gear can now show in a floating window over your trainer app during a ride.
- Screen recording: a new assignable "Record Screen" action starts a screen recording from a controller button.

### 6.1.0 (19-06-2026)
**Features**:
- WiFi enabled Smart Trainers are now supported
Expand Down
4 changes: 4 additions & 0 deletions android/app/src/main/AndroidManifest.xml
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,10 @@

<!-- Required for trainer overlay display -->
<uses-permission android:name="android.permission.SYSTEM_ALERT_WINDOW" />
<!-- Strip flutter_screen_recording library permissions not needed by this app:
capture is video-only (no microphone), and we have no boot receiver. -->
<uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED" tools:node="remove" />
<uses-permission android:name="android.permission.RECORD_AUDIO" tools:node="remove" />
<!-- Android 14+: foreground service type declaration for overlay window service -->
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_SPECIAL_USE" />

Expand Down
18 changes: 18 additions & 0 deletions ios/Podfile.lock
Original file line number Diff line number Diff line change
@@ -1,5 +1,9 @@
PODS:
- Flutter (1.0.0)
- flutter_foreground_task (0.0.1):
- Flutter
- flutter_screen_recording (0.0.1):
- Flutter
- flutter_volume_controller (0.0.1):
- Flutter
- gamepads_ios (0.1.1):
Expand All @@ -12,22 +16,31 @@ PODS:
- Flutter
- restart_app (0.0.1):
- Flutter
- screen_recorder (0.0.1):
- Flutter
- sign_in_with_apple (0.0.1):
- Flutter

DEPENDENCIES:
- Flutter (from `Flutter`)
- flutter_foreground_task (from `.symlinks/plugins/flutter_foreground_task/ios`)
- flutter_screen_recording (from `.symlinks/plugins/flutter_screen_recording/ios`)
- flutter_volume_controller (from `.symlinks/plugins/flutter_volume_controller/ios`)
- gamepads_ios (from `.symlinks/plugins/gamepads_ios/ios`)
- media_key_detector_ios (from `.symlinks/plugins/media_key_detector_ios/ios`)
- nsd_ios (from `.symlinks/plugins/nsd_ios/ios`)
- permission_handler_apple (from `.symlinks/plugins/permission_handler_apple/ios`)
- restart_app (from `.symlinks/plugins/restart_app/ios`)
- screen_recorder (from `.symlinks/plugins/screen_recorder/ios`)
- sign_in_with_apple (from `.symlinks/plugins/sign_in_with_apple/ios`)

EXTERNAL SOURCES:
Flutter:
:path: Flutter
flutter_foreground_task:
:path: ".symlinks/plugins/flutter_foreground_task/ios"
flutter_screen_recording:
:path: ".symlinks/plugins/flutter_screen_recording/ios"
flutter_volume_controller:
:path: ".symlinks/plugins/flutter_volume_controller/ios"
gamepads_ios:
Expand All @@ -40,17 +53,22 @@ EXTERNAL SOURCES:
:path: ".symlinks/plugins/permission_handler_apple/ios"
restart_app:
:path: ".symlinks/plugins/restart_app/ios"
screen_recorder:
:path: ".symlinks/plugins/screen_recorder/ios"
sign_in_with_apple:
:path: ".symlinks/plugins/sign_in_with_apple/ios"

SPEC CHECKSUMS:
Flutter: cabc95a1d2626b1b06e7179b784ebcf0c0cde467
flutter_foreground_task: a159d2c2173b33699ddb3e6c2a067045d7cebb89
flutter_screen_recording: d27b9073d122b618a07ef0e407470d051b265b26
flutter_volume_controller: c2be490cb0487e8b88d0d9fc2b7e1c139a4ebccb
gamepads_ios: c75c6d31377d275b0effb9174c619e2705678c09
media_key_detector_ios: eb43ab957d4ef9084d079216b6dbac96c91d42eb
nsd_ios: 596ad79109ddd3e52d665f650f36428049e3653e
permission_handler_apple: 4ed2196e43d0651e8ff7ca3483a069d469701f2d
restart_app: 9cda5378aacc5000e3f66ee76a9201534e7d3ecf
screen_recorder: 192d2308d545ace5676318ff0c1f2ee53231dbc1
sign_in_with_apple: c5dcc141574c8c54d5ac99dd2163c0c72ad22418

PODFILE CHECKSUM: 7ebd5c9b932b3af79d5c67e3af873118b74e970f
Expand Down
36 changes: 26 additions & 10 deletions ios/Runner.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,12 @@
97C146FE1CF9000F007C117D /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FD1CF9000F007C117D /* Assets.xcassets */; };
97C147011CF9000F007C117D /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */; };
9DEFD285994D09CFCE400F36 /* Pods_Runner.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 5CE7ADD07A99710C0FB974A8 /* Pods_Runner.framework */; };
F02276C12FEC277800C2B6F1 /* GearSnapshot.swift in Sources */ = {isa = PBXBuildFile; fileRef = F02276BF2FEC277800C2B6F1 /* GearSnapshot.swift */; };
F02276C22FEC277800C2B6F1 /* GearReadoutView.swift in Sources */ = {isa = PBXBuildFile; fileRef = F02276C02FEC277800C2B6F1 /* GearReadoutView.swift */; };
F02276C52FEC278C00C2B6F1 /* DeviceCapabilities.swift in Sources */ = {isa = PBXBuildFile; fileRef = F02276C32FEC278C00C2B6F1 /* DeviceCapabilities.swift */; };
F02276C62FEC278C00C2B6F1 /* PipGearController.swift in Sources */ = {isa = PBXBuildFile; fileRef = F02276C42FEC278C00C2B6F1 /* PipGearController.swift */; };
F02276F12FEC2ADC00C2B6F1 /* GearSnapshot+LiveActivity.swift in Sources */ = {isa = PBXBuildFile; fileRef = F02276F02FEC2ADC00C2B6F1 /* GearSnapshot+LiveActivity.swift */; };
F02277062FEC2B6500C2B6F1 /* GearSnapshot.swift in Sources */ = {isa = PBXBuildFile; fileRef = F02276BF2FEC277800C2B6F1 /* GearSnapshot.swift */; };
F068794F2FAF1F5700395E5C /* WidgetKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = F068794E2FAF1F5700395E5C /* WidgetKit.framework */; };
F06879512FAF1F5700395E5C /* SwiftUI.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = F06879502FAF1F5700395E5C /* SwiftUI.framework */; };
F06879602FAF1F5900395E5C /* TrainerActivityExtension.appex in Embed Foundation Extensions */ = {isa = PBXBuildFile; fileRef = F068794D2FAF1F5700395E5C /* TrainerActivityExtension.appex */; settings = {ATTRIBUTES = (RemoveHeadersOnCopy, ); }; };
Expand Down Expand Up @@ -88,6 +94,11 @@
97C147021CF9000F007C117D /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; };
DFFDC4B9C4D6EF6A3BDE2E73 /* Pods-RunnerTests.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-RunnerTests.debug.xcconfig"; path = "Target Support Files/Pods-RunnerTests/Pods-RunnerTests.debug.xcconfig"; sourceTree = "<group>"; };
EFDECED99A47773C293F8819 /* Pods-RunnerTests.profile.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-RunnerTests.profile.xcconfig"; path = "Target Support Files/Pods-RunnerTests/Pods-RunnerTests.profile.xcconfig"; sourceTree = "<group>"; };
F02276BF2FEC277800C2B6F1 /* GearSnapshot.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = GearSnapshot.swift; path = Shared/GearSnapshot.swift; sourceTree = "<group>"; };
F02276C02FEC277800C2B6F1 /* GearReadoutView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = GearReadoutView.swift; path = Shared/GearReadoutView.swift; sourceTree = "<group>"; };
F02276C32FEC278C00C2B6F1 /* DeviceCapabilities.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = DeviceCapabilities.swift; path = Runner/DeviceCapabilities.swift; sourceTree = "<group>"; };
F02276C42FEC278C00C2B6F1 /* PipGearController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = PipGearController.swift; path = Runner/PipGearController.swift; sourceTree = "<group>"; };
F02276F02FEC2ADC00C2B6F1 /* GearSnapshot+LiveActivity.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = "GearSnapshot+LiveActivity.swift"; path = "Shared/GearSnapshot+LiveActivity.swift"; sourceTree = "<group>"; };
F068794D2FAF1F5700395E5C /* TrainerActivityExtension.appex */ = {isa = PBXFileReference; explicitFileType = "wrapper.app-extension"; includeInIndex = 0; path = TrainerActivityExtension.appex; sourceTree = BUILT_PRODUCTS_DIR; };
F068794E2FAF1F5700395E5C /* WidgetKit.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = WidgetKit.framework; path = System/Library/Frameworks/WidgetKit.framework; sourceTree = SDKROOT; };
F06879502FAF1F5700395E5C /* SwiftUI.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = SwiftUI.framework; path = System/Library/Frameworks/SwiftUI.framework; sourceTree = SDKROOT; };
Expand Down Expand Up @@ -197,6 +208,11 @@
97C146E51CF9000F007C117D = {
isa = PBXGroup;
children = (
F02276F02FEC2ADC00C2B6F1 /* GearSnapshot+LiveActivity.swift */,
F02276C32FEC278C00C2B6F1 /* DeviceCapabilities.swift */,
F02276C42FEC278C00C2B6F1 /* PipGearController.swift */,
F02276C02FEC277800C2B6F1 /* GearReadoutView.swift */,
F02276BF2FEC277800C2B6F1 /* GearSnapshot.swift */,
F068796C2FAF203C00395E5C /* TrainerActivityExtension.entitlements */,
9740EEB11CF90186004384FC /* Flutter */,
97C146F01CF9000F007C117D /* Runner */,
Expand Down Expand Up @@ -504,6 +520,10 @@
isa = PBXSourcesBuildPhase;
buildActionMask = 2147483647;
files = (
F02276C52FEC278C00C2B6F1 /* DeviceCapabilities.swift in Sources */,
F02276C62FEC278C00C2B6F1 /* PipGearController.swift in Sources */,
F02276C12FEC277800C2B6F1 /* GearSnapshot.swift in Sources */,
F02276C22FEC277800C2B6F1 /* GearReadoutView.swift in Sources */,
74858FAF1ED2DC5600515810 /* AppDelegate.swift in Sources */,
1498D2341E8E89220040F4C2 /* GeneratedPluginRegistrant.m in Sources */,
);
Expand All @@ -513,6 +533,8 @@
isa = PBXSourcesBuildPhase;
buildActionMask = 2147483647;
files = (
F02277062FEC2B6500C2B6F1 /* GearSnapshot.swift in Sources */,
F02276F12FEC2ADC00C2B6F1 /* GearSnapshot+LiveActivity.swift in Sources */,
);
runOnlyForDeploymentPostprocessing = 0;
};
Expand Down Expand Up @@ -804,11 +826,9 @@
CLANG_ENABLE_MODULES = YES;
CODE_SIGN_ENTITLEMENTS = Runner/Runner.entitlements;
CODE_SIGN_IDENTITY = "Apple Development";
"CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer";
CODE_SIGN_STYLE = Manual;
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)";
DEVELOPMENT_TEAM = "";
"DEVELOPMENT_TEAM[sdk=iphoneos*]" = UZRHKPVWN9;
DEVELOPMENT_TEAM = UZRHKPVWN9;
ENABLE_BITCODE = NO;
INFOPLIST_FILE = Runner/Info.plist;
IPHONEOS_DEPLOYMENT_TARGET = 15.6;
Expand All @@ -819,7 +839,6 @@
PRODUCT_BUNDLE_IDENTIFIER = de.jonasbark.swiftcontrol.darwin;
PRODUCT_NAME = "$(TARGET_NAME)";
PROVISIONING_PROFILE_SPECIFIER = "";
"PROVISIONING_PROFILE_SPECIFIER[sdk=iphoneos*]" = "BikeControl Dev";
SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h";
SWIFT_OPTIMIZATION_LEVEL = "-Onone";
SWIFT_VERSION = 5.0;
Expand Down Expand Up @@ -872,11 +891,9 @@
CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE;
CODE_SIGN_ENTITLEMENTS = TrainerActivityExtension.entitlements;
CODE_SIGN_IDENTITY = "Apple Development";
"CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer";
CODE_SIGN_STYLE = Manual;
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 1;
DEVELOPMENT_TEAM = "";
"DEVELOPMENT_TEAM[sdk=iphoneos*]" = UZRHKPVWN9;
DEVELOPMENT_TEAM = UZRHKPVWN9;
ENABLE_USER_SCRIPT_SANDBOXING = YES;
GCC_C_LANGUAGE_STANDARD = gnu17;
GENERATE_INFOPLIST_FILE = YES;
Expand All @@ -897,7 +914,6 @@
PRODUCT_BUNDLE_IDENTIFIER = de.jonasbark.swiftcontrol.darwin.TrainerActivity;
PRODUCT_NAME = "$(TARGET_NAME)";
PROVISIONING_PROFILE_SPECIFIER = "";
"PROVISIONING_PROFILE_SPECIFIER[sdk=iphoneos*]" = "LiveActivity Debug";
SKIP_INSTALL = YES;
STRING_CATALOG_GENERATE_SYMBOLS = YES;
SWIFT_ACTIVE_COMPILATION_CONDITIONS = "DEBUG $(inherited)";
Expand Down
44 changes: 44 additions & 0 deletions ios/Runner/AppDelegate.swift
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,10 @@ import UIKit
/// a MethodCallHandler on this channel; native code below forwards Darwin
/// notifications posted by the extension's `AppIntent`s.
private static let overlayActionsChannel = "bike_control/overlay_actions_ios"
private static let pipChannelName = "bike_control/pip_ios"

private var actionChannel: FlutterMethodChannel?
private var pipChannel: FlutterMethodChannel?

override func application(
_ application: UIApplication,
Expand Down Expand Up @@ -39,6 +41,48 @@ import UIKit
binaryMessenger: registrar.messenger()
)
}

if pipChannel == nil,
let registrar = engineBridge.pluginRegistry.registrar(forPlugin: "BikeControlPip") {
let channel = FlutterMethodChannel(
name: AppDelegate.pipChannelName,
binaryMessenger: registrar.messenger()
)
channel.setMethodCallHandler { call, result in
switch call.method {
case "isSupported":
if #available(iOS 16.0, *) {
result(DeviceCapabilities.pipEligible)
} else {
result(false)
}
case "isCapable":
if #available(iOS 16.0, *) {
result(DeviceCapabilities.isPipCapable)
} else {
result(false)
}
case "start":
if #available(iOS 16.0, *) {
PipGearController.shared.start(initial: call.arguments as? [String: Any] ?? [:])
}
result(nil)
case "update":
if #available(iOS 16.0, *) {
PipGearController.shared.update(call.arguments as? [String: Any] ?? [:])
}
result(nil)
case "stop":
if #available(iOS 16.0, *) {
PipGearController.shared.stop()
}
result(nil)
default:
result(FlutterMethodNotImplemented)
}
}
pipChannel = channel
}
}

// MARK: - Live Activity Darwin notification bridge
Expand Down
51 changes: 51 additions & 0 deletions ios/Runner/DeviceCapabilities.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
import AVKit
import UIKit

enum DeviceCapabilities {
/// No public API exposes "has Dynamic Island". Heuristic: Dynamic Island
/// iPhones report a larger safe-area inset (~59pt) on the cutout edge than
/// notch devices (≤48pt). iPads have no Dynamic Island.
///
/// We use the MAX of all four insets, not just `top`: in landscape the
/// cutout moves to a side edge and the top inset collapses to ~0, so a
/// top-only check would misread a Dynamic-Island iPhone as non-DI and
/// auto-start a redundant PiP. The max inset stays orientation-independent.
static var hasDynamicIsland: Bool {
guard UIDevice.current.userInterfaceIdiom == .phone else { return false }
return keyWindowMaxInset() >= 51
}

/// Whether PiP is technically possible at all: iOS 16+ (ImageRenderer) and
/// the device supports PiP — regardless of the Dynamic Island. Drives the
/// opt-in toggle and honoring it on Dynamic-Island iPhones.
static var isPipCapable: Bool {
guard #available(iOS 16.0, *) else { return false }
return AVPictureInPictureController.isPictureInPictureSupported()
}

/// PiP is the AUTOMATIC floating display on iPad and on iPhones WITHOUT a
/// Dynamic Island (Dynamic-Island iPhones default to the Live Activity, but
/// can opt into PiP via settings).
static var pipEligible: Bool {
guard isPipCapable else { return false }
if UIDevice.current.userInterfaceIdiom == .pad { return true }
return !hasDynamicIsland
}

/// iPad has room to show the floating window immediately (foreground), rather
/// than only once BikeControl is backgrounded.
static var prefersForegroundPip: Bool {
UIDevice.current.userInterfaceIdiom == .pad
}

private static func keyWindowMaxInset() -> CGFloat {
for scene in UIApplication.shared.connectedScenes {
guard let ws = scene as? UIWindowScene else { continue }
if let w = ws.windows.first(where: { $0.isKeyWindow }) ?? ws.windows.first {
let i = w.safeAreaInsets
return max(max(i.top, i.bottom), max(i.left, i.right))
}
}
return 0
}
}
2 changes: 2 additions & 0 deletions ios/Runner/Info.plist
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,8 @@
<string>BikeControl does not use the camera. This entry is required because the image picker plugin links camera APIs.</string>
<key>NSPhotoLibraryUsageDescription</key>
<string>Access your photo library to store screenshots.</string>
<key>NSPhotoLibraryAddUsageDescription</key>
<string>BikeControl saves your screen recordings to your photo library.</string>
<key>NSSupportsLiveActivities</key>
<true/>
<key>UIApplicationSceneManifest</key>
Expand Down
Loading
Loading