@erkamyaman/capacitor-foldable 8.4.1 → 9.0.0-beta.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/ErkamyamanCapacitorFoldable.podspec +1 -1
- package/Package.swift +3 -4
- package/README.md +27 -560
- package/android/build.gradle +3 -7
- package/ios/Sources/FoldablePlugin/FoldablePlugin.swift +22 -4
- package/package.json +6 -6
- package/skills/capacitor-foldable/references/ionic.md +11 -6
- package/skills/capacitor-foldable/references/iphone-duo.md +12 -3
|
@@ -12,7 +12,7 @@ Pod::Spec.new do |s|
|
|
|
12
12
|
s.source = { :git => package['repository']['url'], :tag => s.version.to_s }
|
|
13
13
|
s.source_files = 'ios/Sources/**/*.{swift,h,m,c,cc,mm,cpp}'
|
|
14
14
|
s.resource_bundles = { 'ErkamyamanCapacitorFoldablePrivacy' => ['ios/Sources/FoldablePlugin/Resources/PrivacyInfo.xcprivacy'] }
|
|
15
|
-
s.ios.deployment_target = '
|
|
15
|
+
s.ios.deployment_target = '16.0'
|
|
16
16
|
s.dependency 'Capacitor'
|
|
17
17
|
s.swift_version = '5.1'
|
|
18
18
|
end
|
package/Package.swift
CHANGED
|
@@ -3,21 +3,20 @@ import PackageDescription
|
|
|
3
3
|
|
|
4
4
|
let package = Package(
|
|
5
5
|
name: "ErkamyamanCapacitorFoldable",
|
|
6
|
-
platforms: [.iOS(.
|
|
6
|
+
platforms: [.iOS(.v16)],
|
|
7
7
|
products: [
|
|
8
8
|
.library(
|
|
9
9
|
name: "ErkamyamanCapacitorFoldable",
|
|
10
10
|
targets: ["FoldablePlugin"])
|
|
11
11
|
],
|
|
12
12
|
dependencies: [
|
|
13
|
-
.package(
|
|
13
|
+
.package(name: "capacitor", path: "example-app/node_modules/@capacitor/ios")
|
|
14
14
|
],
|
|
15
15
|
targets: [
|
|
16
16
|
.target(
|
|
17
17
|
name: "FoldablePlugin",
|
|
18
18
|
dependencies: [
|
|
19
|
-
.product(name: "Capacitor", package: "capacitor
|
|
20
|
-
.product(name: "Cordova", package: "capacitor-swift-pm")
|
|
19
|
+
.product(name: "Capacitor", package: "capacitor")
|
|
21
20
|
],
|
|
22
21
|
path: "ios/Sources/FoldablePlugin",
|
|
23
22
|
resources: [.copy("Resources/PrivacyInfo.xcprivacy")]),
|
package/README.md
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
<p align="center"><img src="https://raw.githubusercontent.com/erkamyaman/capacitor-foldable/main/docs/images/logo/logo.svg" width="168" height="129" alt="Capacitor Foldable logo"></p>
|
|
1
2
|
<h1 align="center">Capacitor Foldable</h1>
|
|
2
3
|
<p align="center"><strong><code>@erkamyaman/capacitor-foldable</code></strong></p>
|
|
3
4
|
<p align="center">
|
|
@@ -11,58 +12,25 @@
|
|
|
11
12
|
Fold state, posture, hinge angle, size classes and bar placement for foldable phones such as Galaxy Z Fold and Flip, Pixel Fold and iPhone Duo, in Capacitor apps.
|
|
12
13
|
</p>
|
|
13
14
|
|
|
14
|
-
<p align="center">
|
|
15
|
-
<img height="240" alt="iPhone Duo closed, on the outer display" src="https://raw.githubusercontent.com/erkamyaman/capacitor-foldable/main/docs/images/iphone-duo-closed.png">
|
|
16
|
-
<img height="240" alt="iPhone Duo half-open in book posture" src="https://raw.githubusercontent.com/erkamyaman/capacitor-foldable/main/docs/images/iphone-duo-book.png">
|
|
17
|
-
<img height="240" alt="iPhone Duo fully open" src="https://raw.githubusercontent.com/erkamyaman/capacitor-foldable/main/docs/images/iphone-duo-open.png">
|
|
18
|
-
</p>
|
|
19
|
-
<p align="center">
|
|
20
|
-
<img height="300" alt="Pixel Fold in tabletop posture" src="https://raw.githubusercontent.com/erkamyaman/capacitor-foldable/main/docs/images/android-tabletop.jpg">
|
|
21
|
-
<img height="300" alt="Pixel Fold half-open in book posture" src="https://raw.githubusercontent.com/erkamyaman/capacitor-foldable/main/docs/images/android-book.jpg">
|
|
22
|
-
</p>
|
|
23
|
-
|
|
24
|
-
```typescript
|
|
25
|
-
const { state, hingeOrientation } = await Foldable.getFoldState();
|
|
26
|
-
// { state: 'half-opened', hingeOrientation: 'horizontal' }
|
|
27
|
-
```
|
|
28
|
-
|
|
29
|
-
### Versions
|
|
30
|
-
|
|
31
|
-
| Capacitor | Plugin | iOS |
|
|
32
|
-
| --------- | ------ | --- |
|
|
33
|
-
| v7.x | v7.x | Size classes only, build with Xcode 26 |
|
|
34
|
-
| v8.x | v8.x | Size classes, plus iPhone Duo on Capacitor 8.5+ |
|
|
35
|
-
|
|
36
|
-
### Supported Platforms
|
|
37
|
-
|
|
38
|
-
| Platform | Status | Minimum |
|
|
39
|
-
| -------- | ------ | ------- |
|
|
40
|
-
| Android | Supported | API 24, `compileSdk` 36 |
|
|
41
|
-
| iOS | Size classes everywhere; fold, hinge and bar placement on iPhone Duo with Capacitor 8.5+ and iOS 27.1+, built with any Xcode | iOS 15 |
|
|
42
|
-
| Web | Stub, returns `flat` | |
|
|
43
|
-
|
|
44
|
-
## Do you need this plugin?
|
|
45
|
-
|
|
46
|
-
Web views already resize with the window, so a responsive layout that pads with every safe-area inset adapts to foldables without a plugin. Use this one when your app needs to know what CSS can't tell it:
|
|
47
|
-
|
|
48
|
-
- **Whether the device folds at all**, and whether it can stand half-open like a laptop.
|
|
49
|
-
- **Where the fold is**, and whether it splits the screen.
|
|
50
|
-
- **How the device is held**: flat, tabletop or book.
|
|
51
|
-
- **The hinge angle**, for effects and interactions.
|
|
52
|
-
- **Outer or inner display**, and Material window size classes.
|
|
53
|
-
- **Where iPhone Duo puts native bars**, so your HTML tab bar can follow. For Ionic's `ion-tabs` there's a ready-made stylesheet, plus a fix for the tab bar disappearing when you fold.
|
|
54
|
-
- **The outer display on Android foldables**: rear display and dual screen.
|
|
15
|
+
<p align="center"><a href="https://erkamyaman.github.io/capacitor-foldable/"><strong>Documentation</strong></a> · <a href="https://erkamyaman.github.io/capacitor-foldable/api.html">API</a> · <a href="https://erkamyaman.github.io/capacitor-foldable/iphone-duo.html">iPhone Duo guide</a> · <a href="https://github.com/erkamyaman/hinge-guess">Hinge Guess demo</a></p>
|
|
55
16
|
|
|
56
|
-
|
|
17
|
+
| Ionic tabs on iPhone Duo without the plugin | With the plugin |
|
|
18
|
+
| --- | --- |
|
|
19
|
+
| <img src="https://raw.githubusercontent.com/erkamyaman/capacitor-foldable/main/docs/images/ionic-before-book.png" width="360" alt="Ionic tab bar across the fold in book posture"> | <img src="https://raw.githubusercontent.com/erkamyaman/capacitor-foldable/main/docs/images/ionic-after-book.png" width="360" alt="Ionic tab bar moved to the side in book posture"> |
|
|
57
20
|
|
|
58
|
-
##
|
|
21
|
+
## Install
|
|
59
22
|
|
|
60
23
|
```bash
|
|
61
24
|
npm install @erkamyaman/capacitor-foldable
|
|
62
25
|
npx cap sync
|
|
63
26
|
```
|
|
64
27
|
|
|
65
|
-
|
|
28
|
+
| Capacitor | Plugin | iOS |
|
|
29
|
+
| --------- | ------ | --- |
|
|
30
|
+
| v8.x | v8.x | Size classes, plus iPhone Duo on Capacitor 8.5+ and iOS 27.1, built with any Xcode |
|
|
31
|
+
| v7.x | v7.x | Size classes only |
|
|
32
|
+
|
|
33
|
+
Android works on API 24+ with any foldable Jetpack WindowManager supports. The web build is a stub that reports `flat`.
|
|
66
34
|
|
|
67
35
|
## Usage
|
|
68
36
|
|
|
@@ -76,533 +44,32 @@ Foldable.addListener('foldStateChange', ({ posture }) => {
|
|
|
76
44
|
});
|
|
77
45
|
```
|
|
78
46
|
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
## Web standard APIs
|
|
82
|
-
|
|
83
|
-
Chrome ships the [Device Posture API](https://developer.mozilla.org/docs/Web/API/Device_Posture_API) and the [Viewport Segments API](https://developer.mozilla.org/docs/Web/API/Viewport_Segments_API), but Android's WebView turns both off. Safari 27.1 has them behind a feature flag that is off by default and belongs to Safari, so a `WKWebView` inside your app cannot switch it on. Neither works in a Capacitor app today. `installFoldablePolyfill()` fills them in from the native fold state, and mirrors the CSS features as classes and variables on `<html>`:
|
|
84
|
-
|
|
85
|
-
```typescript
|
|
86
|
-
import { installFoldablePolyfill } from '@erkamyaman/capacitor-foldable';
|
|
87
|
-
|
|
88
|
-
await installFoldablePolyfill();
|
|
89
|
-
|
|
90
|
-
navigator.devicePosture.type; // 'continuous' | 'folded'
|
|
91
|
-
window.viewport.segments; // two DOMRects when the fold splits the web view
|
|
92
|
-
```
|
|
93
|
-
|
|
94
|
-
| Standard CSS | With the polyfill |
|
|
95
|
-
| --- | --- |
|
|
96
|
-
| `@media (device-posture: folded)` | `.device-posture-folded` |
|
|
97
|
-
| `@media (horizontal-viewport-segments: 2)` | `.horizontal-viewport-segments-2` |
|
|
98
|
-
| `@media (vertical-viewport-segments: 2)` | `.vertical-viewport-segments-2` |
|
|
99
|
-
| `env(viewport-segment-width 0 0)` | `var(--viewport-segment-width-0-0)` |
|
|
100
|
-
| Native bars moved to the side (iPhone Duo) | `.vertical-bars-leading`, `.vertical-bars-trailing` |
|
|
101
|
-
| The phone is being folded right now | `.folding` |
|
|
102
|
-
|
|
103
|
-
The segment variables only exist while the device is half-open. To lay out along the fold whether it's flat or half-open, such as a game on one side and its controls on the other, the polyfill also sets the fold's position whenever there is one:
|
|
104
|
-
|
|
105
|
-
| Fold | With the polyfill |
|
|
106
|
-
| --- | --- |
|
|
107
|
-
| Direction | `.fold-vertical`, `.fold-horizontal` |
|
|
108
|
-
| Position and size | `var(--fold-left)`, `var(--fold-top)`, `var(--fold-width)`, `var(--fold-height)` |
|
|
109
|
-
|
|
110
|
-
```css
|
|
111
|
-
.fold-vertical .game { width: var(--fold-left); }
|
|
112
|
-
.fold-vertical .controls { left: calc(var(--fold-left) + var(--fold-width)); }
|
|
113
|
-
```
|
|
114
|
-
|
|
115
|
-
Using Ionic's `ion-tabs`? `import '@erkamyaman/capacitor-foldable/ionic-tabs.css'` moves it to the side the way native tab bars do on iPhone Duo. See [Ionic tabs](docs/iphone-duo.md#ionic-tabs).
|
|
116
|
-
|
|
117
|
-
| Ionic tabs without the plugin | With `ionic-tabs.css` |
|
|
118
|
-
| --- | --- |
|
|
119
|
-
| <img src="https://raw.githubusercontent.com/erkamyaman/capacitor-foldable/main/docs/images/ionic-before-book.png" width="300" alt="Ionic tab bar across the fold in book posture"> | <img src="https://raw.githubusercontent.com/erkamyaman/capacitor-foldable/main/docs/images/ionic-after-book.png" width="300" alt="Ionic tab bar as a pill on the side in book posture"> |
|
|
120
|
-
| <img src="https://raw.githubusercontent.com/erkamyaman/capacitor-foldable/main/docs/images/ionic-before-closed.png" width="220" alt="Ionic tab bar at the bottom of the closed iPhone Duo"> | <img src="https://raw.githubusercontent.com/erkamyaman/capacitor-foldable/main/docs/images/ionic-after-closed.png" width="220" alt="Ionic tab bar as a pill under the clock on the closed iPhone Duo"> |
|
|
121
|
-
|
|
122
|
-
## Apps built with it
|
|
123
|
-
|
|
124
|
-
- **[Hinge Guess](https://github.com/erkamyaman/hinge-guess)** is a small game: fold the phone to a target angle and score how close you got. Ionic Angular, native tabs, and the plugin behind the dial.
|
|
125
|
-
- **[examples](examples)** in this repo is a catalogue of one-file demos, one per API, laid out as two pages around the crease on iPhone Duo.
|
|
126
|
-
- **[example-app](example-app)** is the plain single-screen app that shows every value the plugin reports at once.
|
|
47
|
+
Prefer CSS? `installFoldablePolyfill()` fills in the Device Posture and Viewport Segments APIs that Capacitor's web views turn off, and sets `--fold-*` variables and classes on `<html>`. Using `ion-tabs`? `import '@erkamyaman/capacitor-foldable/ionic-tabs.css'`.
|
|
127
48
|
|
|
128
|
-
|
|
49
|
+
**Everything else is in the [documentation](https://erkamyaman.github.io/capacitor-foldable/).**
|
|
129
50
|
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
**iOS.** Public APIs only, UIKit and Capacitor, with no private symbols. An app built against an SDK older than 27.1 reaches the iOS 27.1 APIs through the Objective-C runtime, since the compiler cannot see them, and they are the same public APIs either way. It touches none of Apple's [required reason APIs](https://developer.apple.com/documentation/bundleresources/describing-use-of-required-reason-api), collects nothing and tracks nobody, and it ships a `PrivacyInfo.xcprivacy` saying exactly that, which Xcode folds into your app's privacy report. Nothing it uses needs a purpose string in `Info.plist`. Any Xcode that Capacitor 8 supports builds fine, and reports the fold on a 27.1 device either way.
|
|
133
|
-
|
|
134
|
-
**Android.** The plugin adds no permissions and no `uses-feature` to your app, so it changes nothing about which devices can install it. Its own manifest is empty; Jetpack WindowManager merges two optional `<uses-library>` entries, which are how it binds to the OEM window extensions and filter nothing. The hinge angle comes from `TYPE_HINGE_ANGLE`, which needs no permission, and rear display and dual screen go through `WindowAreaController`, also permission free. `minSdk` is 24, the same as Capacitor's own default, and `targetSdk` follows your project.
|
|
135
|
-
|
|
136
|
-
**Data safety and privacy labels.** Everything stays on the device: the plugin has no network code and stores nothing. There is no entry for it to add to Play's Data safety form or to your App Store privacy label.
|
|
51
|
+
## Guides
|
|
137
52
|
|
|
138
|
-
|
|
53
|
+
- [API reference](https://erkamyaman.github.io/capacitor-foldable/api.html)
|
|
54
|
+
- [iPhone Duo](https://erkamyaman.github.io/capacitor-foldable/iphone-duo.html)
|
|
55
|
+
- [Layout tips](https://erkamyaman.github.io/capacitor-foldable/layout-tips.html)
|
|
56
|
+
- [Examples](https://erkamyaman.github.io/capacitor-foldable/examples.html)
|
|
57
|
+
- [Testing on Android](https://erkamyaman.github.io/capacitor-foldable/testing-android.html)
|
|
58
|
+
- [Store review](https://erkamyaman.github.io/capacitor-foldable/store-review.html)
|
|
139
59
|
|
|
140
|
-
|
|
60
|
+
## Agent skill
|
|
141
61
|
|
|
142
62
|
```bash
|
|
143
63
|
npx skills add erkamyaman/capacitor-foldable@capacitor-foldable
|
|
144
64
|
```
|
|
145
65
|
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
The same skill is published on its own at [iphone-duo-capacitor-skills](https://github.com/erkamyaman/iphone-duo-capacitor-skills), which is where it sits next to the iPhone Duo skills for SwiftUI and UIKit.
|
|
149
|
-
|
|
150
|
-
## Guides
|
|
151
|
-
|
|
152
|
-
- [Examples](docs/examples.md)
|
|
153
|
-
- [Layout tips](docs/layout-tips.md)
|
|
154
|
-
- [Testing on Android](docs/testing-android.md)
|
|
155
|
-
- [iPhone Duo](docs/iphone-duo.md)
|
|
156
|
-
|
|
157
|
-
## API
|
|
158
|
-
|
|
159
|
-
<docgen-index>
|
|
160
|
-
|
|
161
|
-
* [`isDeviceFoldable()`](#isdevicefoldable)
|
|
162
|
-
* [`getFoldState()`](#getfoldstate)
|
|
163
|
-
* [`getHingeAngle()`](#gethingeangle)
|
|
164
|
-
* [`getReservedRegions()`](#getreservedregions)
|
|
165
|
-
* [`setVerticalBarBehavior(...)`](#setverticalbarbehavior)
|
|
166
|
-
* [`getSizeClass()`](#getsizeclass)
|
|
167
|
-
* [`getDisplayModes()`](#getdisplaymodes)
|
|
168
|
-
* [`getBarPlacement()`](#getbarplacement)
|
|
169
|
-
* [`startRearDisplay()`](#startreardisplay)
|
|
170
|
-
* [`stopRearDisplay()`](#stopreardisplay)
|
|
171
|
-
* [`startDualScreen(...)`](#startdualscreen)
|
|
172
|
-
* [`stopDualScreen()`](#stopdualscreen)
|
|
173
|
-
* [`addListener('foldStateChange', ...)`](#addlistenerfoldstatechange-)
|
|
174
|
-
* [`addListener('hingeAngleChange', ...)`](#addlistenerhingeanglechange-)
|
|
175
|
-
* [`addListener('sizeClassChange', ...)`](#addlistenersizeclasschange-)
|
|
176
|
-
* [`addListener('displayModeChange', ...)`](#addlistenerdisplaymodechange-)
|
|
177
|
-
* [`addListener('foldingChange', ...)`](#addlistenerfoldingchange-)
|
|
178
|
-
* [`addListener('barPlacementChange', ...)`](#addlistenerbarplacementchange-)
|
|
179
|
-
* [Interfaces](#interfaces)
|
|
180
|
-
* [Type Aliases](#type-aliases)
|
|
181
|
-
|
|
182
|
-
</docgen-index>
|
|
183
|
-
|
|
184
|
-
<docgen-api>
|
|
185
|
-
<!--Update the source file JSDoc comments and rerun docgen to update the docs below-->
|
|
186
|
-
|
|
187
|
-
### isDeviceFoldable()
|
|
188
|
-
|
|
189
|
-
```typescript
|
|
190
|
-
isDeviceFoldable() => Promise<{ foldable: boolean; supportsTabletop: boolean; }>
|
|
191
|
-
```
|
|
192
|
-
|
|
193
|
-
Whether the device has a fold at all, and whether it can be propped half
|
|
194
|
-
open like a laptop. Both `false` on web, and on iOS except on iPhone Duo
|
|
195
|
-
(iOS 27.1 or later).
|
|
196
|
-
|
|
197
|
-
**Returns:** <code>Promise<{ foldable: boolean; supportsTabletop: boolean; }></code>
|
|
198
|
-
|
|
199
|
-
**Since:** 7.0.0
|
|
200
|
-
|
|
201
|
-
--------------------
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
### getFoldState()
|
|
205
|
-
|
|
206
|
-
```typescript
|
|
207
|
-
getFoldState() => Promise<FoldState>
|
|
208
|
-
```
|
|
209
|
-
|
|
210
|
-
Read the current fold state. Resolves to
|
|
211
|
-
`{ state: 'flat', isSeparating: false, posture: 'flat' }` when there is no
|
|
212
|
-
fold information.
|
|
213
|
-
|
|
214
|
-
**Returns:** <code>Promise<<a href="#foldstate">FoldState</a>></code>
|
|
215
|
-
|
|
216
|
-
**Since:** 7.0.0
|
|
217
|
-
|
|
218
|
-
--------------------
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
### getHingeAngle()
|
|
222
|
-
|
|
223
|
-
```typescript
|
|
224
|
-
getHingeAngle() => Promise<{ angle: number | null; status: HingeStatus | null; }>
|
|
225
|
-
```
|
|
226
|
-
|
|
227
|
-
Read the angle between the two halves of the device, in degrees: `0` when
|
|
228
|
-
closed, `180` when flat. Resolves to `{ angle: null }` on devices without a
|
|
229
|
-
hinge angle sensor, on web, and on iOS before 27.1.
|
|
230
|
-
|
|
231
|
-
**Returns:** <code>Promise<{ angle: number | null; status: <a href="#hingestatus">HingeStatus</a> | null; }></code>
|
|
232
|
-
|
|
233
|
-
**Since:** 7.0.0
|
|
234
|
-
|
|
235
|
-
--------------------
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
### getReservedRegions()
|
|
239
|
-
|
|
240
|
-
```typescript
|
|
241
|
-
getReservedRegions() => Promise<{ regions: ReservedRegion[]; }>
|
|
242
|
-
```
|
|
243
|
-
|
|
244
|
-
Read every region the system reserves on this display: the fold and
|
|
245
|
-
anything covering the screen, active or not. iPhone Duo reports the fold,
|
|
246
|
-
the vertical status bar area and the under-display camera. Resolves to an
|
|
247
|
-
empty list on Android, on web and on iOS before 27.1.
|
|
248
|
-
|
|
249
|
-
**Returns:** <code>Promise<{ regions: ReservedRegion[]; }></code>
|
|
250
|
-
|
|
251
|
-
**Since:** 8.2.0
|
|
252
|
-
|
|
253
|
-
--------------------
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
### setVerticalBarBehavior(...)
|
|
257
|
-
|
|
258
|
-
```typescript
|
|
259
|
-
setVerticalBarBehavior(options: { behavior: 'automatic' | 'disabled'; }) => Promise<{ applied: boolean; }>
|
|
260
|
-
```
|
|
261
|
-
|
|
262
|
-
Choose whether iPhone Duo may move this app's bars to the side of the
|
|
263
|
-
display. `'disabled'` keeps everything horizontal, including the status
|
|
264
|
-
bar. Resolves to `{ applied: false }` when the app does not use
|
|
265
|
-
`FoldableBridgeViewController`, on Android, on web and on iOS before 27.1.
|
|
266
|
-
See the iPhone Duo guide for the one-line storyboard change.
|
|
267
|
-
|
|
268
|
-
| Param | Type |
|
|
269
|
-
| ------------- | ----------------------------------------------------- |
|
|
270
|
-
| **`options`** | <code>{ behavior: 'automatic' \| 'disabled'; }</code> |
|
|
271
|
-
|
|
272
|
-
**Returns:** <code>Promise<{ applied: boolean; }></code>
|
|
273
|
-
|
|
274
|
-
**Since:** 8.2.0
|
|
275
|
-
|
|
276
|
-
--------------------
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
### getSizeClass()
|
|
280
|
-
|
|
281
|
-
```typescript
|
|
282
|
-
getSizeClass() => Promise<SizeClass>
|
|
283
|
-
```
|
|
284
|
-
|
|
285
|
-
Read the window's size classes: Apple's compact and regular, the signal its
|
|
286
|
-
iPhone Duo guidelines recommend for telling the outer display from the inner
|
|
287
|
-
one, and Material's width and height classes. On iOS `horizontal` and
|
|
288
|
-
`vertical` are UIKit's size classes; everything else comes from the window
|
|
289
|
-
size.
|
|
290
|
-
|
|
291
|
-
**Returns:** <code>Promise<<a href="#sizeclass">SizeClass</a>></code>
|
|
292
|
-
|
|
293
|
-
**Since:** 7.0.0
|
|
294
|
-
|
|
295
|
-
--------------------
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
### getDisplayModes()
|
|
299
|
-
|
|
300
|
-
```typescript
|
|
301
|
-
getDisplayModes() => Promise<DisplayModes>
|
|
302
|
-
```
|
|
303
|
-
|
|
304
|
-
Read which of the foldable display modes the device offers right now. Both
|
|
305
|
-
are `'unsupported'` on iOS and web.
|
|
306
|
-
|
|
307
|
-
**Returns:** <code>Promise<<a href="#displaymodes">DisplayModes</a>></code>
|
|
308
|
-
|
|
309
|
-
**Since:** 7.0.0
|
|
310
|
-
|
|
311
|
-
--------------------
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
### getBarPlacement()
|
|
315
|
-
|
|
316
|
-
```typescript
|
|
317
|
-
getBarPlacement() => Promise<BarPlacement>
|
|
318
|
-
```
|
|
319
|
-
|
|
320
|
-
Read where native bars go. Native tab bars and toolbars move to the side of
|
|
321
|
-
the display on iPhone Duo, but HTML ones stay put, so use this to move your
|
|
322
|
-
own tab bar too.
|
|
323
|
-
|
|
324
|
-
**Returns:** <code>Promise<<a href="#barplacement">BarPlacement</a>></code>
|
|
325
|
-
|
|
326
|
-
**Since:** 7.0.0
|
|
327
|
-
|
|
328
|
-
--------------------
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
### startRearDisplay()
|
|
332
|
-
|
|
333
|
-
```typescript
|
|
334
|
-
startRearDisplay() => Promise<void>
|
|
335
|
-
```
|
|
336
|
-
|
|
337
|
-
Move the app to the outer display. Android asks the user to confirm first,
|
|
338
|
-
and the promise resolves once the app has moved. Rejects when rear display
|
|
339
|
-
mode is not `'available'`. Only on Android.
|
|
340
|
-
|
|
341
|
-
**Since:** 7.0.0
|
|
342
|
-
|
|
343
|
-
--------------------
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
### stopRearDisplay()
|
|
347
|
-
|
|
348
|
-
```typescript
|
|
349
|
-
stopRearDisplay() => Promise<void>
|
|
350
|
-
```
|
|
351
|
-
|
|
352
|
-
Move the app back to the inner display.
|
|
353
|
-
|
|
354
|
-
**Since:** 7.0.0
|
|
355
|
-
|
|
356
|
-
--------------------
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
### startDualScreen(...)
|
|
360
|
-
|
|
361
|
-
```typescript
|
|
362
|
-
startDualScreen(options: { url: string; }) => Promise<void>
|
|
363
|
-
```
|
|
364
|
-
|
|
365
|
-
Show a page on the outer display while the app stays on the inner one. A
|
|
366
|
-
relative `url` resolves against the app's own URL, so `'cover.html'` loads a
|
|
367
|
-
page bundled with the app. The page runs in its own web view without access
|
|
368
|
-
to Capacitor plugins. Calling this again while dual-screen mode is active
|
|
369
|
-
replaces the page. Only on Android.
|
|
370
|
-
|
|
371
|
-
| Param | Type |
|
|
372
|
-
| ------------- | ----------------------------- |
|
|
373
|
-
| **`options`** | <code>{ url: string; }</code> |
|
|
374
|
-
|
|
375
|
-
**Since:** 7.0.0
|
|
376
|
-
|
|
377
|
-
--------------------
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
### stopDualScreen()
|
|
381
|
-
|
|
382
|
-
```typescript
|
|
383
|
-
stopDualScreen() => Promise<void>
|
|
384
|
-
```
|
|
385
|
-
|
|
386
|
-
Close the page on the outer display.
|
|
387
|
-
|
|
388
|
-
**Since:** 7.0.0
|
|
389
|
-
|
|
390
|
-
--------------------
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
### addListener('foldStateChange', ...)
|
|
394
|
-
|
|
395
|
-
```typescript
|
|
396
|
-
addListener(eventName: 'foldStateChange', listenerFunc: (state: FoldState) => void) => Promise<PluginListenerHandle>
|
|
397
|
-
```
|
|
398
|
-
|
|
399
|
-
Listen for fold state changes. On a foldable this also fires when the
|
|
400
|
-
device rotates, because `hingeOrientation` and `hingeBounds` rotate with
|
|
401
|
-
the window.
|
|
402
|
-
|
|
403
|
-
| Param | Type |
|
|
404
|
-
| ------------------ | ------------------------------------------------------------------- |
|
|
405
|
-
| **`eventName`** | <code>'foldStateChange'</code> |
|
|
406
|
-
| **`listenerFunc`** | <code>(state: <a href="#foldstate">FoldState</a>) => void</code> |
|
|
407
|
-
|
|
408
|
-
**Returns:** <code>Promise<<a href="#pluginlistenerhandle">PluginListenerHandle</a>></code>
|
|
409
|
-
|
|
410
|
-
**Since:** 7.0.0
|
|
411
|
-
|
|
412
|
-
--------------------
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
### addListener('hingeAngleChange', ...)
|
|
416
|
-
|
|
417
|
-
```typescript
|
|
418
|
-
addListener(eventName: 'hingeAngleChange', listenerFunc: (event: { angle: number; }) => void) => Promise<PluginListenerHandle>
|
|
419
|
-
```
|
|
420
|
-
|
|
421
|
-
Listen for hinge angle changes. On Android the hinge sensor only runs while
|
|
422
|
-
at least one of these listeners is registered. On iOS it fires on iPhone
|
|
423
|
-
Duo (iOS 27.1 or later). Never fires on web.
|
|
424
|
-
|
|
425
|
-
| Param | Type |
|
|
426
|
-
| ------------------ | --------------------------------------------------- |
|
|
427
|
-
| **`eventName`** | <code>'hingeAngleChange'</code> |
|
|
428
|
-
| **`listenerFunc`** | <code>(event: { angle: number; }) => void</code> |
|
|
429
|
-
|
|
430
|
-
**Returns:** <code>Promise<<a href="#pluginlistenerhandle">PluginListenerHandle</a>></code>
|
|
431
|
-
|
|
432
|
-
**Since:** 7.0.0
|
|
433
|
-
|
|
434
|
-
--------------------
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
### addListener('sizeClassChange', ...)
|
|
438
|
-
|
|
439
|
-
```typescript
|
|
440
|
-
addListener(eventName: 'sizeClassChange', listenerFunc: (sizeClass: SizeClass) => void) => Promise<PluginListenerHandle>
|
|
441
|
-
```
|
|
442
|
-
|
|
443
|
-
Listen for size class changes, such as unfolding the device, rotating it or
|
|
444
|
-
resizing the window. On iOS this fires when UIKit's size classes change
|
|
445
|
-
(iOS 17 or later) or the device rotates, so a resize that keeps the same
|
|
446
|
-
size classes may not update `widthClass` and `heightClass` until then.
|
|
447
|
-
|
|
448
|
-
| Param | Type |
|
|
449
|
-
| ------------------ | ----------------------------------------------------------------------- |
|
|
450
|
-
| **`eventName`** | <code>'sizeClassChange'</code> |
|
|
451
|
-
| **`listenerFunc`** | <code>(sizeClass: <a href="#sizeclass">SizeClass</a>) => void</code> |
|
|
452
|
-
|
|
453
|
-
**Returns:** <code>Promise<<a href="#pluginlistenerhandle">PluginListenerHandle</a>></code>
|
|
454
|
-
|
|
455
|
-
**Since:** 7.0.0
|
|
456
|
-
|
|
457
|
-
--------------------
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
### addListener('displayModeChange', ...)
|
|
461
|
-
|
|
462
|
-
```typescript
|
|
463
|
-
addListener(eventName: 'displayModeChange', listenerFunc: (modes: DisplayModes) => void) => Promise<PluginListenerHandle>
|
|
464
|
-
```
|
|
465
|
-
|
|
466
|
-
Listen for changes to the display modes, including a mode ending because
|
|
467
|
-
the user folded or unfolded the device. Never fires on iOS and web.
|
|
468
|
-
|
|
469
|
-
| Param | Type |
|
|
470
|
-
| ------------------ | ------------------------------------------------------------------------- |
|
|
471
|
-
| **`eventName`** | <code>'displayModeChange'</code> |
|
|
472
|
-
| **`listenerFunc`** | <code>(modes: <a href="#displaymodes">DisplayModes</a>) => void</code> |
|
|
473
|
-
|
|
474
|
-
**Returns:** <code>Promise<<a href="#pluginlistenerhandle">PluginListenerHandle</a>></code>
|
|
475
|
-
|
|
476
|
-
**Since:** 7.0.0
|
|
477
|
-
|
|
478
|
-
--------------------
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
### addListener('foldingChange', ...)
|
|
482
|
-
|
|
483
|
-
```typescript
|
|
484
|
-
addListener(eventName: 'foldingChange', listenerFunc: (event: { folding: boolean; }) => void) => Promise<PluginListenerHandle>
|
|
485
|
-
```
|
|
486
|
-
|
|
487
|
-
Listen for the device being folded or unfolded. `folding` turns `true` as
|
|
488
|
-
soon as the hinge starts moving and `false` about half a second after it
|
|
489
|
-
stops, so an app can pause animations or heavy work while the screen is in
|
|
490
|
-
motion. Only fires where a hinge angle is available: Android foldables with
|
|
491
|
-
a hinge sensor, and iPhone Duo on iOS 27.1 or later.
|
|
492
|
-
|
|
493
|
-
| Param | Type |
|
|
494
|
-
| ------------------ | ------------------------------------------------------ |
|
|
495
|
-
| **`eventName`** | <code>'foldingChange'</code> |
|
|
496
|
-
| **`listenerFunc`** | <code>(event: { folding: boolean; }) => void</code> |
|
|
497
|
-
|
|
498
|
-
**Returns:** <code>Promise<<a href="#pluginlistenerhandle">PluginListenerHandle</a>></code>
|
|
499
|
-
|
|
500
|
-
**Since:** 8.2.0
|
|
501
|
-
|
|
502
|
-
--------------------
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
### addListener('barPlacementChange', ...)
|
|
506
|
-
|
|
507
|
-
```typescript
|
|
508
|
-
addListener(eventName: 'barPlacementChange', listenerFunc: (placement: BarPlacement) => void) => Promise<PluginListenerHandle>
|
|
509
|
-
```
|
|
510
|
-
|
|
511
|
-
Listen for bar placement changes, such as opening or rotating iPhone Duo.
|
|
512
|
-
Only fires on iOS 27.1 or later.
|
|
513
|
-
|
|
514
|
-
| Param | Type |
|
|
515
|
-
| ------------------ | ----------------------------------------------------------------------------- |
|
|
516
|
-
| **`eventName`** | <code>'barPlacementChange'</code> |
|
|
517
|
-
| **`listenerFunc`** | <code>(placement: <a href="#barplacement">BarPlacement</a>) => void</code> |
|
|
518
|
-
|
|
519
|
-
**Returns:** <code>Promise<<a href="#pluginlistenerhandle">PluginListenerHandle</a>></code>
|
|
520
|
-
|
|
521
|
-
**Since:** 7.0.0
|
|
522
|
-
|
|
523
|
-
--------------------
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
### Interfaces
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
#### FoldState
|
|
530
|
-
|
|
531
|
-
| Prop | Type | Description | Since |
|
|
532
|
-
| ---------------------- | -------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ----- |
|
|
533
|
-
| **`state`** | <code>'flat' \| 'half-opened' \| 'closed'</code> | Posture of the fold. `'closed'` is never reported today: a device shut on its cover display reports `'flat'`. | 7.0.0 |
|
|
534
|
-
| **`isSeparating`** | <code>boolean</code> | Whether the fold splits the web view into two areas: `true` when half-opened, or when the hinge has a physical gap. | 7.0.0 |
|
|
535
|
-
| **`posture`** | <code>'flat' \| 'tabletop' \| 'book'</code> | How the device is held: `'tabletop'` when half-opened with a horizontal hinge, like a laptop, `'book'` when half-opened with a vertical hinge, and `'flat'` otherwise. | 7.0.0 |
|
|
536
|
-
| **`hingeOrientation`** | <code>'horizontal' \| 'vertical'</code> | Direction of the hinge relative to the window, so it flips when the device rotates. Omitted when there is no fold. | 7.0.0 |
|
|
537
|
-
| **`hingeBounds`** | <code>{ x: number; y: number; width: number; height: number; }</code> | Position of the fold in CSS pixels, relative to the web view. Zero wide (or zero tall) on a seamless fold. Omitted when there is no fold. | 7.0.0 |
|
|
538
|
-
| **`occludedBounds`** | <code>{ x: number; y: number; width: number; height: number; }</code> | Area of the web view the hinge covers, in CSS pixels. Only present on devices with a physical gap. | 7.0.0 |
|
|
539
|
-
| **`activeDisplay`** | <code>'inner' \| 'outer'</code> | Which display is showing the app, on a device with an inner and an outer display such as iPhone Duo. Reported on iPhone Duo (iOS 27.1 or later) and omitted elsewhere. | 7.0.0 |
|
|
540
|
-
| **`cameraBounds`** | <code>{ x: number; y: number; width: number; height: number; }[]</code> | Areas of the web view something covers, in CSS pixels. On Android these are the display cutouts. On iPhone Duo these are the active occlusion regions: the camera in use, and the strip the system keeps for the vertical status bar. Use `getReservedRegions()` when you need to tell them apart. Omitted when there are none. | 7.0.0 |
|
|
541
|
-
| **`hingeMargins`** | <code>{ top: number; right: number; bottom: number; left: number; }</code> | Space the system asks you to keep clear around the fold, in CSS pixels. `hingeBounds` covers the crease plus these margins, so the crease itself is `hingeBounds` shrunk by them. iPhone Duo reports 20 on each side of a vertical fold. Omitted on Android and where there is no fold. | 8.2.0 |
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
#### ReservedRegion
|
|
545
|
-
|
|
546
|
-
| Prop | Type | Description | Since |
|
|
547
|
-
| -------------- | -------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------ | ----- |
|
|
548
|
-
| **`kind`** | <code>'division' \| 'occlusion'</code> | `'division'` is the fold itself. `'occlusion'` is something covering the display, such as a camera or the vertical status bar area. | 8.2.0 |
|
|
549
|
-
| **`isActive`** | <code>boolean</code> | Whether the region applies right now. An inactive division is a flat fold, and an inactive occlusion is a camera that is not in use. | 8.2.0 |
|
|
550
|
-
| **`x`** | <code>number</code> | | |
|
|
551
|
-
| **`y`** | <code>number</code> | | |
|
|
552
|
-
| **`width`** | <code>number</code> | | |
|
|
553
|
-
| **`height`** | <code>number</code> | | |
|
|
554
|
-
| **`margins`** | <code>{ top: number; right: number; bottom: number; left: number; }</code> | Space to keep clear around the region, in CSS pixels. | 8.2.0 |
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
#### SizeClass
|
|
558
|
-
|
|
559
|
-
| Prop | Type | Description | Since |
|
|
560
|
-
| ----------------- | --------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ----- |
|
|
561
|
-
| **`horizontal`** | <code>'compact' \| 'regular'</code> | Width size class of the window: `'compact'` on a phone and on the outer display of a foldable, `'regular'` on the inner display, tablets and wide windows. On Android and web `'regular'` starts at 600 CSS pixels. | 7.0.0 |
|
|
562
|
-
| **`vertical`** | <code>'compact' \| 'regular'</code> | Height size class of the window: `'compact'` on a phone in landscape. On Android and web `'regular'` starts at 480 CSS pixels. | 7.0.0 |
|
|
563
|
-
| **`widthClass`** | <code>'compact' \| 'medium' \| 'expanded' \| 'large' \| 'extraLarge'</code> | Material window width class, from the window width in CSS pixels: `'compact'` below 600, `'medium'` below 840, `'expanded'` below 1200, `'large'` below 1600 and `'extraLarge'` from 1600. | 7.0.0 |
|
|
564
|
-
| **`heightClass`** | <code>'compact' \| 'medium' \| 'expanded'</code> | Material window height class, from the window height in CSS pixels: `'compact'` below 480, `'medium'` below 900 and `'expanded'` from 900. | 7.0.0 |
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
#### DisplayModes
|
|
568
|
-
|
|
569
|
-
| Prop | Type | Description | Since |
|
|
570
|
-
| ----------------- | --------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------- | ----- |
|
|
571
|
-
| **`rearDisplay`** | <code><a href="#displaymodestatus">DisplayModeStatus</a></code> | Rear display mode moves the app to the outer display, so people can frame a photo with the rear cameras. Only on Android foldables that offer it. | 7.0.0 |
|
|
572
|
-
| **`dualScreen`** | <code><a href="#displaymodestatus">DisplayModeStatus</a></code> | Dual-screen mode shows a second page on the outer display while the app stays on the inner one. Only on Android foldables that offer it. | 7.0.0 |
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
#### BarPlacement
|
|
576
|
-
|
|
577
|
-
| Prop | Type | Description | Since |
|
|
578
|
-
| --------------------- | -------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ----- |
|
|
579
|
-
| **`verticalBarEdge`** | <code>'leading' \| 'trailing' \| null</code> | The edge iPhone Duo moves tab bars and toolbars to when it lays them out vertically: `'leading'` or `'trailing'` in the reading direction, so `'leading'` is the left edge in left-to-right languages. `null` when bars stay horizontal, and always on Android, web and iOS before 27.1. An app built with an older Xcode still gets the edge: it is inferred from the safe-area inset the bar leaves, since the trait that names it is missing. | 7.0.0 |
|
|
580
|
-
| **`inset`** | <code>number</code> | How much room the vertical bar takes on its edge, in points, rounded to whole points. It is the same value the web view reports as a safe-area inset, so `env(safe-area-inset-*)` already covers simple layouts. Read it when you place your own chrome and need the number itself, and do not assume it stays at 84: it differs when the bar is disabled, and Apple can change it. `0` when no vertical bar is reported, and always on Android and web. | 8.3.3 |
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
#### PluginListenerHandle
|
|
584
|
-
|
|
585
|
-
| Prop | Type |
|
|
586
|
-
| ------------ | ----------------------------------------- |
|
|
587
|
-
| **`remove`** | <code>() => Promise<void></code> |
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
### Type Aliases
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
#### HingeStatus
|
|
594
|
-
|
|
595
|
-
How far open the hinge is, as the system sees it. Note that it can lag the
|
|
596
|
-
angle: iOS keeps reporting `'closed'` for a moment after the phone opens.
|
|
597
|
-
|
|
598
|
-
<code>'closed' | 'partiallyOpen' | 'fullyOpen'</code>
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
#### DisplayModeStatus
|
|
66
|
+
Also ships in the npm package under [`skills/`](skills/capacitor-foldable), and on its own at [iphone-duo-capacitor-skills](https://github.com/erkamyaman/iphone-duo-capacitor-skills).
|
|
602
67
|
|
|
603
|
-
|
|
68
|
+
## Used by
|
|
604
69
|
|
|
605
|
-
|
|
70
|
+
- [Capacitor Native UI](https://github.com/rdlabo-dev/ionic-theme-ios27/tree/main/packages/capacitor-native-ui) and [Ionic Theme iOS 27](https://github.com/rdlabo-dev/ionic-theme-ios27) by rdlabo
|
|
71
|
+
- [RecipeSage](https://github.com/julianpoy/RecipeSage), a collaborative recipe keeper and meal planner, moves its native tab bar to the side on iPhone Duo with it
|
|
72
|
+
- [Hinge Guess](https://github.com/erkamyaman/hinge-guess), a game where the phone's hinge is the controller
|
|
606
73
|
|
|
607
74
|
## Trademarks and licence
|
|
608
75
|
|
package/android/build.gradle
CHANGED
|
@@ -9,26 +9,22 @@ ext {
|
|
|
9
9
|
}
|
|
10
10
|
|
|
11
11
|
buildscript {
|
|
12
|
-
ext.kotlin_version = project.hasProperty("kotlin_version") ? rootProject.ext.kotlin_version : '2.1.21'
|
|
13
12
|
repositories {
|
|
14
13
|
google()
|
|
15
14
|
mavenCentral()
|
|
16
15
|
}
|
|
17
16
|
dependencies {
|
|
18
|
-
classpath 'com.android.tools.build:gradle:
|
|
19
|
-
classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"
|
|
17
|
+
classpath 'com.android.tools.build:gradle:9.4.1'
|
|
20
18
|
}
|
|
21
19
|
}
|
|
22
20
|
|
|
23
21
|
apply plugin: 'com.android.library'
|
|
24
|
-
apply plugin: 'kotlin-android'
|
|
25
22
|
|
|
26
23
|
android {
|
|
27
24
|
namespace = "io.github.erkamyaman.foldable"
|
|
28
|
-
compileSdk = project.hasProperty('compileSdkVersion') ? rootProject.ext.compileSdkVersion :
|
|
25
|
+
compileSdk = project.hasProperty('compileSdkVersion') ? rootProject.ext.compileSdkVersion : 37
|
|
29
26
|
defaultConfig {
|
|
30
|
-
minSdkVersion project.hasProperty('minSdkVersion') ? rootProject.ext.minSdkVersion :
|
|
31
|
-
targetSdkVersion project.hasProperty('targetSdkVersion') ? rootProject.ext.targetSdkVersion : 36
|
|
27
|
+
minSdkVersion project.hasProperty('minSdkVersion') ? rootProject.ext.minSdkVersion : 26
|
|
32
28
|
versionCode 1
|
|
33
29
|
versionName "1.0"
|
|
34
30
|
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
|
|
@@ -31,6 +31,8 @@ public class FoldablePlugin: CAPPlugin, CAPBridgedPlugin {
|
|
|
31
31
|
private var lastHingeAngle: Double?
|
|
32
32
|
private var isFolding = false
|
|
33
33
|
private var foldingWorkItem: DispatchWorkItem?
|
|
34
|
+
private var barPlacementWorkItems: [DispatchWorkItem] = []
|
|
35
|
+
private var barPlacementNeedsResend = false
|
|
34
36
|
private var orientationObserver: NSObjectProtocol?
|
|
35
37
|
|
|
36
38
|
@objc override public func load() {
|
|
@@ -103,7 +105,12 @@ public class FoldablePlugin: CAPPlugin, CAPBridgedPlugin {
|
|
|
103
105
|
@objc func getBarPlacement(_ call: CAPPluginCall) {
|
|
104
106
|
DispatchQueue.main.async { [weak self] in
|
|
105
107
|
guard let self = self else { return }
|
|
106
|
-
|
|
108
|
+
let placement = self.currentBarPlacement()
|
|
109
|
+
if let last = self.lastBarPlacement, !(last as NSDictionary).isEqual(to: placement) {
|
|
110
|
+
self.barPlacementNeedsResend = true
|
|
111
|
+
self.notifyBarPlacementAfterSettling()
|
|
112
|
+
}
|
|
113
|
+
call.resolve(placement)
|
|
107
114
|
}
|
|
108
115
|
}
|
|
109
116
|
|
|
@@ -153,6 +160,7 @@ public class FoldablePlugin: CAPPlugin, CAPBridgedPlugin {
|
|
|
153
160
|
implementation.observeChanges(in: view) { [weak self] in
|
|
154
161
|
self?.notifyFoldStateNowAndAfterSettling()
|
|
155
162
|
self?.notifyHingeAngleIfChanged()
|
|
163
|
+
self?.notifyBarPlacementAfterSettling()
|
|
156
164
|
}
|
|
157
165
|
|
|
158
166
|
UIDevice.current.beginGeneratingDeviceOrientationNotifications()
|
|
@@ -163,7 +171,7 @@ public class FoldablePlugin: CAPPlugin, CAPBridgedPlugin {
|
|
|
163
171
|
) { [weak self] _ in
|
|
164
172
|
self?.notifyFoldStateNowAndAfterSettling()
|
|
165
173
|
self?.notifySizeClassIfChanged()
|
|
166
|
-
self?.
|
|
174
|
+
self?.notifyBarPlacementAfterSettling()
|
|
167
175
|
}
|
|
168
176
|
|
|
169
177
|
guard #available(iOS 17.0, *) else { return }
|
|
@@ -179,7 +187,7 @@ public class FoldablePlugin: CAPPlugin, CAPBridgedPlugin {
|
|
|
179
187
|
_ = view.registerForTraitChanges(traits) { [weak self] (_: UIView, _: UITraitCollection) in
|
|
180
188
|
self?.notifySizeClassIfChanged()
|
|
181
189
|
self?.notifyFoldStateNowAndAfterSettling()
|
|
182
|
-
self?.
|
|
190
|
+
self?.notifyBarPlacementAfterSettling()
|
|
183
191
|
}
|
|
184
192
|
}
|
|
185
193
|
}
|
|
@@ -211,12 +219,22 @@ public class FoldablePlugin: CAPPlugin, CAPBridgedPlugin {
|
|
|
211
219
|
|
|
212
220
|
private func notifyBarPlacementIfChanged() {
|
|
213
221
|
let placement = currentBarPlacement()
|
|
214
|
-
if let last = lastBarPlacement, (last as NSDictionary).isEqual(to: placement) { return }
|
|
222
|
+
if !barPlacementNeedsResend, let last = lastBarPlacement, (last as NSDictionary).isEqual(to: placement) { return }
|
|
215
223
|
|
|
224
|
+
barPlacementNeedsResend = false
|
|
216
225
|
lastBarPlacement = placement
|
|
217
226
|
notifyListeners("barPlacementChange", data: placement)
|
|
218
227
|
}
|
|
219
228
|
|
|
229
|
+
private func notifyBarPlacementAfterSettling() {
|
|
230
|
+
barPlacementWorkItems.forEach { $0.cancel() }
|
|
231
|
+
barPlacementWorkItems = [0.25, 0.8].map { delay in
|
|
232
|
+
let check = DispatchWorkItem { [weak self] in self?.notifyBarPlacementIfChanged() }
|
|
233
|
+
DispatchQueue.main.asyncAfter(deadline: .now() + delay, execute: check)
|
|
234
|
+
return check
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
|
|
220
238
|
private func notifyFoldStateNowAndAfterSettling() {
|
|
221
239
|
notifyFoldStateIfChanged()
|
|
222
240
|
DispatchQueue.main.asyncAfter(deadline: .now() + 0.3) { [weak self] in
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@erkamyaman/capacitor-foldable",
|
|
3
|
-
"version": "
|
|
3
|
+
"version": "9.0.0-beta.0",
|
|
4
4
|
"description": "Fold-state and dual-display APIs for foldable devices in Capacitor apps",
|
|
5
5
|
"main": "dist/plugin.cjs.js",
|
|
6
6
|
"module": "dist/esm/index.js",
|
|
@@ -75,17 +75,17 @@
|
|
|
75
75
|
"eslint": "eslint . --ext ts",
|
|
76
76
|
"prettier": "prettier \"**/*.{css,html,ts,js,java}\" --plugin=prettier-plugin-java",
|
|
77
77
|
"swiftlint": "node-swiftlint",
|
|
78
|
-
"docgen": "docgen --api FoldablePlugin --output-readme
|
|
78
|
+
"docgen": "node scripts/docs-api.mjs open && docgen --api FoldablePlugin --output-readme docs/api.md --output-json dist/docs.json && node scripts/docs-api.mjs close",
|
|
79
79
|
"build": "npm run clean && npm run docgen && tsc && rollup -c rollup.config.mjs",
|
|
80
80
|
"clean": "rimraf ./dist",
|
|
81
81
|
"watch": "tsc --watch",
|
|
82
82
|
"prepublishOnly": "npm run build"
|
|
83
83
|
},
|
|
84
84
|
"devDependencies": {
|
|
85
|
-
"@capacitor/android": "
|
|
86
|
-
"@capacitor/core": "
|
|
85
|
+
"@capacitor/android": "9.0.0-beta.0",
|
|
86
|
+
"@capacitor/core": "9.0.0-beta.0",
|
|
87
87
|
"@capacitor/docgen": "^0.3.1",
|
|
88
|
-
"@capacitor/ios": "
|
|
88
|
+
"@capacitor/ios": "9.0.0-beta.0",
|
|
89
89
|
"@ionic/eslint-config": "^0.4.0",
|
|
90
90
|
"@ionic/prettier-config": "^4.0.0",
|
|
91
91
|
"@ionic/swiftlint-config": "^2.0.0",
|
|
@@ -98,7 +98,7 @@
|
|
|
98
98
|
"typescript": "^5.9.3"
|
|
99
99
|
},
|
|
100
100
|
"peerDependencies": {
|
|
101
|
-
"@capacitor/core": "^
|
|
101
|
+
"@capacitor/core": "^9.0.0-beta.0"
|
|
102
102
|
},
|
|
103
103
|
"prettier": "@ionic/prettier-config",
|
|
104
104
|
"swiftlint": "@ionic/swiftlint-config",
|
|
@@ -23,16 +23,21 @@ React and Vue import it from the entry file as usual, since their bundlers ship
|
|
|
23
23
|
|
|
24
24
|
## Projecting Ionic controls into native UI
|
|
25
25
|
|
|
26
|
-
Moving the tab bar with CSS is one answer. The other is [`@rdlabo/
|
|
26
|
+
Moving the tab bar with CSS is one answer. The other is [`@rdlabo/capacitor-native-ui`](https://github.com/rdlabo-dev/ionic-theme-ios27/tree/main/packages/capacitor-native-ui), which renders existing `ion-back-button`, `ion-buttons`, `ion-button`, `ion-tab-bar` and segment controls with UIKit and SwiftUI while the original elements, state and handlers stay in the WebView. It can put them in the system's vertical rail on iPhone Duo, and it works with an app's existing theme. The optional [`@rdlabo/ionic-theme-ios27`](https://github.com/rdlabo-dev/ionic-theme-ios27) adds iOS 27 styling on top.
|
|
27
27
|
|
|
28
|
-
The split is clean: this plugin provides device state, the theme handles
|
|
28
|
+
The split is clean: this plugin provides device state, Native UI projects the controls, and the theme handles Web styling. Both rdlabo packages list this plugin in their standard setup, installed by the app as a direct dependency:
|
|
29
29
|
|
|
30
|
-
|
|
30
|
+
```sh
|
|
31
|
+
npm install @rdlabo/capacitor-native-ui @erkamyaman/capacitor-foldable
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
For setup, send the reader to rdlabo's own documentation rather than duplicating it here:
|
|
31
35
|
|
|
32
|
-
- [
|
|
33
|
-
- [Vertical Bars
|
|
36
|
+
- [Native UI guide](https://github.com/rdlabo-dev/ionic-theme-ios27/tree/main/packages/capacitor-native-ui)
|
|
37
|
+
- [Vertical Bars with an existing Ionic theme](https://github.com/rdlabo-dev/ionic-theme-ios27/blob/main/packages/capacitor-native-ui/docs/ionic-vertical-bars.md)
|
|
38
|
+
- [Migrating from theme 1.2.x](https://github.com/rdlabo-dev/ionic-theme-ios27/blob/main/packages/capacitor-native-ui/docs/migration.md), where the native APIs were still bundled in the theme
|
|
34
39
|
|
|
35
|
-
Use the CSS stylesheet above when you want Ionic's own tab bar moved, and
|
|
40
|
+
Use the CSS stylesheet above when you want Ionic's own tab bar moved, and Native UI when you want the controls rendered natively.
|
|
36
41
|
|
|
37
42
|
## A fold fires a keyboard event
|
|
38
43
|
|
|
@@ -30,16 +30,25 @@ The system moves bars to the side, and the web view's safe areas follow. Measure
|
|
|
30
30
|
|
|
31
31
|
So for most layouts, padding with the safe-area insets is enough, and `getBarPlacement()` is for when you are positioning your own chrome to match the system's.
|
|
32
32
|
|
|
33
|
-
The opt-out needs the plugin's view controller
|
|
33
|
+
The opt-out needs the plugin's view controller. Capacitor 9 starts the app from SwiftUI, so return it from `ios/App/App/CapacitorView.swift`:
|
|
34
34
|
|
|
35
35
|
```swift
|
|
36
|
+
import SwiftUI
|
|
36
37
|
import Capacitor
|
|
37
38
|
import FoldablePlugin
|
|
38
39
|
|
|
39
|
-
|
|
40
|
+
public struct CapacitorView: UIViewControllerRepresentable {
|
|
41
|
+
public func makeUIViewController(context: Context) -> CAPBridgeViewController {
|
|
42
|
+
FoldableBridgeViewController()
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
public func updateUIViewController(_ vc: CAPBridgeViewController, context: Context) {}
|
|
46
|
+
}
|
|
40
47
|
```
|
|
41
48
|
|
|
42
|
-
|
|
49
|
+
On Capacitor 8, set it in `ios/App/App/SceneDelegate.swift` instead: `window?.rootViewController = FoldableBridgeViewController()`. After `npx cap migrate` to Capacitor 9, delete that window setup, because the SwiftUI app owns the window and a second one would cover it.
|
|
50
|
+
|
|
51
|
+
`FoldablePlugin` is the module name for a Swift Package install, which is what `npx cap add ios` sets up for a new app. On a CocoaPods install the module is `ErkamyamanCapacitorFoldable`. Older apps that build the controller in `Main.storyboard` set the class there instead.
|
|
43
52
|
|
|
44
53
|
## The web view pauses between displays
|
|
45
54
|
|