ruby_everywhere 0.3.0 → 0.4.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.
- checksums.yaml +4 -4
- data/README.md +255 -0
- data/bridge/everywhere/bridge.js +60 -3
- data/lib/everywhere/android_resources.rb +217 -0
- data/lib/everywhere/android_sdk.rb +330 -0
- data/lib/everywhere/asset_catalog.rb +185 -0
- data/lib/everywhere/auth_handoff.rb +398 -0
- data/lib/everywhere/auth_token.rb +118 -0
- data/lib/everywhere/builders/android.rb +844 -0
- data/lib/everywhere/builders/ios.rb +40 -3
- data/lib/everywhere/commands/build.rb +51 -9
- data/lib/everywhere/commands/clean.rb +10 -5
- data/lib/everywhere/commands/dev.rb +46 -8
- data/lib/everywhere/commands/doctor.rb +89 -3
- data/lib/everywhere/commands/install.rb +1 -1
- data/lib/everywhere/commands/logs.rb +117 -8
- data/lib/everywhere/config.rb +409 -13
- data/lib/everywhere/emulator.rb +405 -0
- data/lib/everywhere/engine.rb +12 -0
- data/lib/everywhere/icon.rb +196 -0
- data/lib/everywhere/log_filter.rb +74 -0
- data/lib/everywhere/native_helper.rb +2 -5
- data/lib/everywhere/omniauth.rb +88 -0
- data/lib/everywhere/paths.rb +47 -0
- data/lib/everywhere/version.rb +1 -1
- data/support/mobile/android/README.md +217 -0
- data/support/mobile/android/app/build.gradle.kts +156 -0
- data/support/mobile/android/app/everywhere.properties +8 -0
- data/support/mobile/android/app/native-packages.gradle.kts +16 -0
- data/support/mobile/android/app/proguard-rules.pro +21 -0
- data/support/mobile/android/app/src/debug/res/xml/network_security_config.xml +30 -0
- data/support/mobile/android/app/src/main/AndroidManifest.xml +42 -0
- data/support/mobile/android/app/src/main/assets/everywhere.json +9 -0
- data/support/mobile/android/app/src/main/assets/fonts/MaterialIcons-Regular.codepoints +2235 -0
- data/support/mobile/android/app/src/main/assets/fonts/MaterialIcons-Regular.ttf +0 -0
- data/support/mobile/android/app/src/main/assets/json/path-configuration.json +19 -0
- data/support/mobile/android/app/src/main/java/com/rubyeverywhere/shell/AuthFlow.kt +268 -0
- data/support/mobile/android/app/src/main/java/com/rubyeverywhere/shell/EverywhereApplication.kt +178 -0
- data/support/mobile/android/app/src/main/java/com/rubyeverywhere/shell/EverywhereConfig.kt +409 -0
- data/support/mobile/android/app/src/main/java/com/rubyeverywhere/shell/EverywhereEvents.kt +68 -0
- data/support/mobile/android/app/src/main/java/com/rubyeverywhere/shell/EverywhereHost.kt +101 -0
- data/support/mobile/android/app/src/main/java/com/rubyeverywhere/shell/IconFont.kt +211 -0
- data/support/mobile/android/app/src/main/java/com/rubyeverywhere/shell/MainActivity.kt +975 -0
- data/support/mobile/android/app/src/main/java/com/rubyeverywhere/shell/MainTabs.kt +201 -0
- data/support/mobile/android/app/src/main/java/com/rubyeverywhere/shell/WebBottomSheetFragment.kt +13 -0
- data/support/mobile/android/app/src/main/java/com/rubyeverywhere/shell/WebFragment.kt +36 -0
- data/support/mobile/android/app/src/main/java/com/rubyeverywhere/shell/bridge/BiometricsComponent.kt +535 -0
- data/support/mobile/android/app/src/main/java/com/rubyeverywhere/shell/bridge/HapticsComponent.kt +141 -0
- data/support/mobile/android/app/src/main/java/com/rubyeverywhere/shell/bridge/MenuComponent.kt +361 -0
- data/support/mobile/android/app/src/main/java/com/rubyeverywhere/shell/bridge/NotificationComponent.kt +151 -0
- data/support/mobile/android/app/src/main/java/com/rubyeverywhere/shell/bridge/PermissionsComponent.kt +271 -0
- data/support/mobile/android/app/src/main/java/com/rubyeverywhere/shell/bridge/StorageComponent.kt +101 -0
- data/support/mobile/android/app/src/main/java/com/rubyeverywhere/shell/bridge/WebControlChannel.kt +204 -0
- data/support/mobile/android/app/src/main/java/com/rubyeverywhere/shell/extensions/EverywhereExtensions.kt +51 -0
- data/support/mobile/android/app/src/main/res/color/bottom_navigation_active_indicator.xml +4 -0
- data/support/mobile/android/app/src/main/res/color/bottom_navigation_item.xml +11 -0
- data/support/mobile/android/app/src/main/res/drawable/ic_launcher_foreground.xml +26 -0
- data/support/mobile/android/app/src/main/res/drawable/ic_tab_placeholder.xml +28 -0
- data/support/mobile/android/app/src/main/res/layout/activity_main.xml +116 -0
- data/support/mobile/android/app/src/main/res/layout/bridge_menu_sheet.xml +83 -0
- data/support/mobile/android/app/src/main/res/layout/bridge_menu_sheet_item.xml +35 -0
- data/support/mobile/android/app/src/main/res/layout/tab_more_sheet.xml +48 -0
- data/support/mobile/android/app/src/main/res/layout/tab_more_sheet_item.xml +35 -0
- data/support/mobile/android/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml +12 -0
- data/support/mobile/android/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml +6 -0
- data/support/mobile/android/app/src/main/res/values/colors.xml +21 -0
- data/support/mobile/android/app/src/main/res/values/ic_launcher_background.xml +5 -0
- data/support/mobile/android/app/src/main/res/values/strings.xml +18 -0
- data/support/mobile/android/app/src/main/res/values/styles.xml +32 -0
- data/support/mobile/android/app/src/main/res/values/themes.xml +53 -0
- data/support/mobile/android/app/src/main/res/values-night/colors.xml +11 -0
- data/support/mobile/android/app/src/main/res/values-night/themes.xml +33 -0
- data/support/mobile/android/app/src/main/res/xml/network_security_config.xml +12 -0
- data/support/mobile/android/app/src/stamped/AndroidManifest.xml +34 -0
- data/support/mobile/android/build.gradle.kts +8 -0
- data/support/mobile/android/gradle/wrapper/gradle-wrapper.jar +0 -0
- data/support/mobile/android/gradle/wrapper/gradle-wrapper.properties +10 -0
- data/support/mobile/android/gradle.properties +14 -0
- data/support/mobile/android/gradlew +251 -0
- data/support/mobile/android/gradlew.bat +94 -0
- data/support/mobile/android/settings.gradle.kts +28 -0
- data/support/mobile/ios/App/AppDelegate.swift +5 -0
- data/support/mobile/ios/App/AuthFlow.swift +101 -0
- data/support/mobile/ios/App/EverywhereConfig.swift +84 -0
- data/support/mobile/ios/App/SceneDelegate.swift +103 -0
- data/support/mobile/ios/App.xcodeproj/project.pbxproj +4 -0
- data/support/mobile/ios/README.md +4 -1
- metadata +67 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 7ffb921df597efe047c25240e7d8b92aaad49c00c90d546020e053eac618739a
|
|
4
|
+
data.tar.gz: 17315b4c5d91eced13b44d7147942d7d6357ee791b8622425ab655eb97b1ab27
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 71d9ba1ea63abe51a7262f02f069ef9f068d3192bb20800bdd0fba8bcd50f36f2daa83bb3f758e7273576b69b88be5541428b41f93021579cc1b5054532099ad
|
|
7
|
+
data.tar.gz: 59632bacae03ade28233832d0193d2500e4e3f85c07b7e61a39f1e086cc398b40f66d48091ffc9e336a11e2f91994b666ea60fc7ab2cbef2b76252d6fb0ada60
|
data/README.md
ADDED
|
@@ -0,0 +1,255 @@
|
|
|
1
|
+
# RubyEverywhere
|
|
2
|
+
|
|
3
|
+
**Ship desktop and mobile apps from the Ruby web apps you already know how to build.**
|
|
4
|
+
|
|
5
|
+
[rubyeverywhere.com](https://rubyeverywhere.com) · by [Andrea Fomera](https://afomera.dev)
|
|
6
|
+
|
|
7
|
+
RubyEverywhere takes a Rails (or Sinatra, or Hanami) app and gives it a native
|
|
8
|
+
life on the platforms your users are already on:
|
|
9
|
+
|
|
10
|
+
- **Desktop** — a [Tauri](https://tauri.app)-based shell. In *local* mode your
|
|
11
|
+
whole app is pressed into a single-file binary with
|
|
12
|
+
[Tebako](https://github.com/tamatebako/tebako) and runs on-device, no Ruby
|
|
13
|
+
install required; in *remote* mode the shell is a thin native wrapper around
|
|
14
|
+
your deployed app.
|
|
15
|
+
- **iOS** — a [Hotwire Native](https://native.hotwired.dev) shell with a real
|
|
16
|
+
tab bar, nav-bar buttons, menus, action sheets, notifications, haptics,
|
|
17
|
+
biometrics, deep links, and native sign-in.
|
|
18
|
+
- **Android** — a feature-for-feature Hotwire Native Android port of the same
|
|
19
|
+
shell, stamped from the same `config/everywhere.yml`.
|
|
20
|
+
|
|
21
|
+
One config file, one CLI, one JavaScript API. The web app stays the source of
|
|
22
|
+
truth — most of what you'd normally rebuild natively (tabs, routes, menus,
|
|
23
|
+
badges) deploys with your Rails app rather than through an app-store review.
|
|
24
|
+
|
|
25
|
+
## Install
|
|
26
|
+
|
|
27
|
+
```sh
|
|
28
|
+
gem install ruby_everywhere
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
This gives you two equivalent executables — `every` and `rbe` — plus the
|
|
32
|
+
packaging toolchain (the `rbe-tebako` fork) as a dependency.
|
|
33
|
+
|
|
34
|
+
Or add it to your app's Gemfile, which also wires up the Rails engine (bridge
|
|
35
|
+
asset serving, importmap pin, view helpers, the live mobile config endpoint):
|
|
36
|
+
|
|
37
|
+
```ruby
|
|
38
|
+
gem "ruby_everywhere"
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
Requires Ruby >= 3.2.
|
|
42
|
+
|
|
43
|
+
**The gem is free, and it always will be.** Everything here — the CLI, the
|
|
44
|
+
desktop shell, both mobile shells, the bridge — is MIT licensed and yours to
|
|
45
|
+
use, forever, with no account and no key. Local builds run on a Mac today
|
|
46
|
+
(pressing, code signing, Xcode and the Android SDK all want a macOS host).
|
|
47
|
+
If you don't own one, or you'd rather not keep a build toolchain healthy, the
|
|
48
|
+
[RubyEverywhere Platform](https://rubyeverywhere.com) is a paid service coming
|
|
49
|
+
soon that runs the exact same builds for you — see
|
|
50
|
+
[Hosted builds](#hosted-builds) below. It's a convenience, never a gate: nothing
|
|
51
|
+
in this gem is held back for it.
|
|
52
|
+
|
|
53
|
+
## Quick start
|
|
54
|
+
|
|
55
|
+
```sh
|
|
56
|
+
cd path/to/your/app
|
|
57
|
+
|
|
58
|
+
every install # remote mode: a native shell around your deployed app
|
|
59
|
+
every install --local # local mode: compile and ship the app itself
|
|
60
|
+
|
|
61
|
+
every doctor # check the toolchain before your first build
|
|
62
|
+
|
|
63
|
+
every dev # dev server + native shells on demand, live reload
|
|
64
|
+
every dev --ios # ...straight into the iOS Simulator
|
|
65
|
+
every dev --android # ...straight into the Android emulator
|
|
66
|
+
|
|
67
|
+
every build # package the macOS .app
|
|
68
|
+
every build --ios # build the iOS shell
|
|
69
|
+
every build --android # build the APK/AAB
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
`every install` is idempotent — it adds `config/everywhere.yml`, a boot stub,
|
|
73
|
+
and the framework-specific config tweaks it needs, and skips anything already
|
|
74
|
+
in place. For Rails it does the surgery for you; for Sinatra and Hanami it
|
|
75
|
+
vendors the bridge and prints the remaining wiring rather than guessing at your
|
|
76
|
+
boot files.
|
|
77
|
+
|
|
78
|
+
`every dev` never packages anything: it runs your ordinary dev server and points
|
|
79
|
+
the native shells at it, so the edit-refresh loop is untouched Rails.
|
|
80
|
+
|
|
81
|
+
## Configuration
|
|
82
|
+
|
|
83
|
+
Everything per-app lives in `config/everywhere.yml`. The native shell templates
|
|
84
|
+
are frozen — the CLI stamps a copy per app and only ever writes the identity,
|
|
85
|
+
config, icon, and color files, so a shell upgrade never collides with your app.
|
|
86
|
+
|
|
87
|
+
```yaml
|
|
88
|
+
app:
|
|
89
|
+
name: My Really Awesome App
|
|
90
|
+
bundle_id: com.example.myapp # set it once; renaming moves users' data
|
|
91
|
+
version: 1.2.0
|
|
92
|
+
entry_path: /dashboard # where the app opens
|
|
93
|
+
icon: icon.png
|
|
94
|
+
|
|
95
|
+
appearance:
|
|
96
|
+
tint_color: "#CC342D"
|
|
97
|
+
background_color: # one hex string, or split by system theme
|
|
98
|
+
light: "#FAF9F7"
|
|
99
|
+
dark: "#1C1B1A"
|
|
100
|
+
|
|
101
|
+
remote:
|
|
102
|
+
url: https://app.example.com # remote mode: the deployed app to wrap
|
|
103
|
+
|
|
104
|
+
tabs: # the native mobile tab bar
|
|
105
|
+
- name: Builds
|
|
106
|
+
path: /builds
|
|
107
|
+
icons:
|
|
108
|
+
ios: hammer # SF Symbol
|
|
109
|
+
android: build # Material icon
|
|
110
|
+
|
|
111
|
+
permissions: # only declared permissions can ever prompt
|
|
112
|
+
notifications: true
|
|
113
|
+
camera: "Scan QR codes to pair devices." # the sentence iOS shows
|
|
114
|
+
|
|
115
|
+
rules: # Hotwire Native path configuration
|
|
116
|
+
- patterns: ["/preferences$"]
|
|
117
|
+
properties:
|
|
118
|
+
context: modal
|
|
119
|
+
|
|
120
|
+
build:
|
|
121
|
+
ruby: "4.0.6"
|
|
122
|
+
targets: [macos-arm64, ios-arm64, android-arm64]
|
|
123
|
+
|
|
124
|
+
updates: # self-hosted, signed auto-updates
|
|
125
|
+
url: https://updates.example.com
|
|
126
|
+
public_key: RWQ…
|
|
127
|
+
auto: check # off | check | download | install
|
|
128
|
+
```
|
|
129
|
+
|
|
130
|
+
Also supported: `auth:` (native OAuth sign-in with a callback scheme),
|
|
131
|
+
`deep_linking:` (universal links / App Links — the gem serves the
|
|
132
|
+
`apple-app-site-association` and `assetlinks.json` files for you), and
|
|
133
|
+
`native:` for "supernative" extensions — Swift you keep in your app repo under
|
|
134
|
+
`native/ios/` that `every build` compiles into the shell, including SwiftUI
|
|
135
|
+
screens, bridge components, a custom splash, and pinned SPM packages.
|
|
136
|
+
|
|
137
|
+
Tabs and path rules ship two ways from that one source: baked into the app
|
|
138
|
+
bundle for offline/first launch, and served live from
|
|
139
|
+
`/everywhere/{ios,android}_v1.json`, so changing them deploys with your web app.
|
|
140
|
+
`Everywhere.filter_tabs` lets you vary the tab bar per request — it shares the
|
|
141
|
+
app's session, so signed-out users can get a different bar without a release.
|
|
142
|
+
|
|
143
|
+
## The JavaScript bridge
|
|
144
|
+
|
|
145
|
+
The gem ships `@rubyeverywhere/bridge` — one API for the browser, the desktop
|
|
146
|
+
shell, and the mobile shells. Rails apps get it served and importmap-pinned
|
|
147
|
+
automatically; Sinatra and Hanami get it vendored to `public/bridge.js`.
|
|
148
|
+
|
|
149
|
+
```js
|
|
150
|
+
import Everywhere from "@rubyeverywhere/bridge"
|
|
151
|
+
|
|
152
|
+
Everywhere.platform // "desktop" | "mobile" | "browser"
|
|
153
|
+
Everywhere.native // true inside a RubyEverywhere app
|
|
154
|
+
|
|
155
|
+
Everywhere.notify({ title, body })
|
|
156
|
+
Everywhere.menu({ title, items })
|
|
157
|
+
Everywhere.haptics.impact("medium")
|
|
158
|
+
Everywhere.badge.setTab("/inbox", 3)
|
|
159
|
+
Everywhere.permissions.request("camera")
|
|
160
|
+
Everywhere.storage.set("theme", "dark")
|
|
161
|
+
Everywhere.updates.check()
|
|
162
|
+
```
|
|
163
|
+
|
|
164
|
+
Everything degrades gracefully — the same page works in a plain browser tab and
|
|
165
|
+
gains native powers inside the app. See
|
|
166
|
+
[`bridge/README.md`](bridge/README.md) for the full surface.
|
|
167
|
+
|
|
168
|
+
Most native chrome needs no JavaScript at all. Tag ordinary markup and the
|
|
169
|
+
bridge lifts it into real native controls; tapping the native control just
|
|
170
|
+
`.click()`s the element it mirrors, so behavior stays defined once in the DOM.
|
|
171
|
+
Rails ships helpers that emit exactly that markup:
|
|
172
|
+
|
|
173
|
+
```erb
|
|
174
|
+
<%= everywhere_nav_button "New", new_note_path, icons: { ios: "plus", android: "add" } %>
|
|
175
|
+
<%= everywhere_fab new_note_path %>
|
|
176
|
+
<%= everywhere_nav_menu "More" do %>
|
|
177
|
+
<%= everywhere_menu_item "Delete", note_path(@note), method: :delete, style: "destructive" %>
|
|
178
|
+
<% end %>
|
|
179
|
+
```
|
|
180
|
+
|
|
181
|
+
Plus `native_app?`, `native_platform`, `everywhere_badge`,
|
|
182
|
+
`everywhere_biometric_lock`, and friends.
|
|
183
|
+
|
|
184
|
+
## Commands
|
|
185
|
+
|
|
186
|
+
| Command | What it does |
|
|
187
|
+
| --- | --- |
|
|
188
|
+
| `every install` | Prepare an app (boot stub, `config/everywhere.yml`, config tweaks) |
|
|
189
|
+
| `every doctor` | Check the toolchain — Tebako, Rust, brew deps, Xcode, Android SDK/JDK |
|
|
190
|
+
| `every dev` | Dev server with native shells on demand (`--desktop --ios --android --browser`) |
|
|
191
|
+
| `every logs` | Stream shell logs from the Simulator or Android device |
|
|
192
|
+
| `every build` | Package natively — desktop, `--ios`, or `--android` |
|
|
193
|
+
| `every icon` | Generate `.icns` / `.ico` / Linux icons from a source PNG |
|
|
194
|
+
| `every release` | Sign, notarize, staple, and emit a `release.json` receipt |
|
|
195
|
+
| `every updates keygen` | Generate a minisign-compatible update-signing keypair |
|
|
196
|
+
| `every publish` | Publish a release to your update bucket |
|
|
197
|
+
| `every clean` | Remove the shell build caches |
|
|
198
|
+
| `every shell-dir` | Print the path of the bundled shell template |
|
|
199
|
+
| `every platform …` | Hosted builds: `login`, `logout`, `auth status`, `build` |
|
|
200
|
+
|
|
201
|
+
Run `every` with no arguments for the full list, or `every <command> --help` for
|
|
202
|
+
a command's flags.
|
|
203
|
+
|
|
204
|
+
## Auto-updates
|
|
205
|
+
|
|
206
|
+
Desktop releases can update themselves from a bucket you own, with no service in
|
|
207
|
+
the middle:
|
|
208
|
+
|
|
209
|
+
```sh
|
|
210
|
+
every updates keygen # once, per app
|
|
211
|
+
every release # build, sign, notarize, staple
|
|
212
|
+
every publish --notes-file NOTES.md # sign the manifest, upload, flip latest.json
|
|
213
|
+
```
|
|
214
|
+
|
|
215
|
+
The public key ships in the app; the secret key never leaves your machine. Apps
|
|
216
|
+
can check, install, and switch channels at runtime via `Everywhere.updates`.
|
|
217
|
+
|
|
218
|
+
## Hosted builds
|
|
219
|
+
|
|
220
|
+
Don't have a Mac, or want iOS and Android built without babysitting Xcode, the
|
|
221
|
+
Android SDK, and a signing setup? The **RubyEverywhere Platform** — a paid
|
|
222
|
+
service, coming soon — runs the same builds on managed machines and hands back
|
|
223
|
+
the artifacts. The CLI already speaks to it:
|
|
224
|
+
|
|
225
|
+
```sh
|
|
226
|
+
every platform login
|
|
227
|
+
every platform build --target ios-arm64,android-arm64
|
|
228
|
+
```
|
|
229
|
+
|
|
230
|
+
Same gem, same `config/everywhere.yml`, same output — you're paying for the
|
|
231
|
+
build machines and the toolchain upkeep, not for features. The gem stays free
|
|
232
|
+
and complete on its own.
|
|
233
|
+
|
|
234
|
+
## Project layout
|
|
235
|
+
|
|
236
|
+
```
|
|
237
|
+
lib/everywhere/ the CLI, config, builders, and Rails engine
|
|
238
|
+
exe/every, exe/rbe the executables
|
|
239
|
+
bridge/ @rubyeverywhere/bridge — the JS half
|
|
240
|
+
support/shell/ the Tauri desktop shell template
|
|
241
|
+
support/mobile/ios/ the Hotwire Native iOS shell template
|
|
242
|
+
support/mobile/android/ the Hotwire Native Android shell template
|
|
243
|
+
```
|
|
244
|
+
|
|
245
|
+
The shell templates have their own READMEs documenting the freeze rules — what
|
|
246
|
+
the CLI stamps per app and what it must never touch:
|
|
247
|
+
[iOS](support/mobile/ios/README.md) · [Android](support/mobile/android/README.md).
|
|
248
|
+
|
|
249
|
+
## Contributing
|
|
250
|
+
|
|
251
|
+
Bug reports and pull requests are welcome. Run the test suite with `rake test`.
|
|
252
|
+
|
|
253
|
+
## License
|
|
254
|
+
|
|
255
|
+
Released under the [MIT License](LICENSE.txt). Copyright © 2026 Andrea Fomera.
|
data/bridge/everywhere/bridge.js
CHANGED
|
@@ -16,6 +16,7 @@
|
|
|
16
16
|
// Everywhere.on("menu", handler) // shell events; returns unsubscribe fn
|
|
17
17
|
// Everywhere.visit("/settings") // Turbo.visit with location fallback
|
|
18
18
|
// Everywhere.reloadTabs() // re-fetch native tab/nav config (mobile)
|
|
19
|
+
// Everywhere.auth.signIn("/auth/github") // provider sign-in in the system browser (mobile)
|
|
19
20
|
//
|
|
20
21
|
// Everywhere.haptics.impact("medium") // light | medium | heavy | soft | rigid
|
|
21
22
|
// Everywhere.haptics.notification("success") // success | warning | error
|
|
@@ -532,8 +533,9 @@ const mobile = {
|
|
|
532
533
|
// Ask the shell to re-fetch its native navigation config (tabs + path
|
|
533
534
|
// rules) now, instead of waiting for the next app foreground. Call it after
|
|
534
535
|
// a change that affects which tabs should show — signing in or out — so the
|
|
535
|
-
// tab bar updates immediately. Uses a dedicated
|
|
536
|
-
//
|
|
536
|
+
// tab bar updates immediately. Uses the shell's control channel (a dedicated
|
|
537
|
+
// WKScriptMessageHandler on iOS, a WebMessageListener on Android), so it
|
|
538
|
+
// doesn't depend on a bridge component being mounted on the page.
|
|
537
539
|
reloadTabs() {
|
|
538
540
|
const control = nativeControlChannel()
|
|
539
541
|
if (control) {
|
|
@@ -566,6 +568,16 @@ const mobile = {
|
|
|
566
568
|
if (control) control.postMessage({ action: "clearInstance", ...(to ? { to } : {}) })
|
|
567
569
|
},
|
|
568
570
|
|
|
571
|
+
// Open a provider sign-in in the system browser rather than this web view —
|
|
572
|
+
// where Google refuses to run at all and the rest lose the password manager
|
|
573
|
+
// and the user's existing session. The shell takes it from here: auth
|
|
574
|
+
// session, one-time handoff token, full reset onto the signed-in app.
|
|
575
|
+
authSignIn(path) {
|
|
576
|
+
const control = nativeControlChannel()
|
|
577
|
+
if (control) control.postMessage({ action: "authFlow", to: path })
|
|
578
|
+
else window.location.assign(path)
|
|
579
|
+
},
|
|
580
|
+
|
|
569
581
|
// Real haptics via the shell's "everywhere--haptics" component; vibration
|
|
570
582
|
// fallback while the handshake completes or on an older shell.
|
|
571
583
|
haptic(kind, value) {
|
|
@@ -702,10 +714,43 @@ const mobile = {
|
|
|
702
714
|
}
|
|
703
715
|
}
|
|
704
716
|
|
|
717
|
+
// The shell's out-of-band command line: app-level actions (reset, tabs,
|
|
718
|
+
// badges, instance, auth) that must work on any page, whether or not a bridge
|
|
719
|
+
// component happens to be mounted on it. Each shell injects it its own way,
|
|
720
|
+
// and the two APIs disagree about what a message is:
|
|
721
|
+
//
|
|
722
|
+
// iOS — WKUserContentController.add(_:name:) hands us
|
|
723
|
+
// window.webkit.messageHandlers.everywhereControl, whose
|
|
724
|
+
// postMessage takes an *object* and serializes it across the
|
|
725
|
+
// boundary for us.
|
|
726
|
+
// Android — WebViewCompat.addWebMessageListener("everywhereControl") hands us
|
|
727
|
+
// window.everywhereControl, backed by a WebMessagePort, and a web
|
|
728
|
+
// message is *text*: postMessage(String). (Android has no user
|
|
729
|
+
// scripts either, so the shell injects this with
|
|
730
|
+
// addDocumentStartJavaScript — it's in place before page JS runs.)
|
|
731
|
+
//
|
|
732
|
+
// Normalizing here rather than at each call site is the point: the seven
|
|
733
|
+
// callers below all speak the object shape, and only this function knows which
|
|
734
|
+
// shell is listening. iOS gets its handler back untouched — it already is the
|
|
735
|
+
// normalized shape, so that path stays byte-for-byte what it was.
|
|
705
736
|
function nativeControlChannel() {
|
|
706
|
-
|
|
737
|
+
if (typeof window === "undefined") return null
|
|
738
|
+
|
|
739
|
+
const webkit =
|
|
740
|
+
window.webkit &&
|
|
707
741
|
window.webkit.messageHandlers &&
|
|
708
742
|
window.webkit.messageHandlers.everywhereControl
|
|
743
|
+
if (webkit) return webkit
|
|
744
|
+
|
|
745
|
+
const android = window.everywhereControl
|
|
746
|
+
if (android && typeof android.postMessage === "function") {
|
|
747
|
+
// Built per call, not cached: a fresh document (a Turbo drive visit is
|
|
748
|
+
// not, but a real navigation is) gets a fresh injected object, and there's
|
|
749
|
+
// nothing here worth keeping alive across one.
|
|
750
|
+
return { postMessage: (message) => android.postMessage(JSON.stringify(message)) }
|
|
751
|
+
}
|
|
752
|
+
|
|
753
|
+
return null
|
|
709
754
|
}
|
|
710
755
|
|
|
711
756
|
// --- public surface ----------------------------------------------------------
|
|
@@ -790,6 +835,18 @@ export const Everywhere = {
|
|
|
790
835
|
else this.visit(to || window.location.pathname)
|
|
791
836
|
},
|
|
792
837
|
|
|
838
|
+
// Third-party sign-in (everywhere.yml `auth:`). You rarely need this: the
|
|
839
|
+
// shell already diverts the paths listed there, so a plain link to
|
|
840
|
+
// /auth/github does the right thing everywhere. Call it when the sign-in is
|
|
841
|
+
// driven by something the shell can't see as a visit — a fetch, a custom
|
|
842
|
+
// element, a button that has other work to do first.
|
|
843
|
+
auth: {
|
|
844
|
+
signIn(path) {
|
|
845
|
+
if (adapter.authSignIn) adapter.authSignIn(path)
|
|
846
|
+
else window.location.assign(path)
|
|
847
|
+
}
|
|
848
|
+
},
|
|
849
|
+
|
|
793
850
|
clipboard: {
|
|
794
851
|
write(text) {
|
|
795
852
|
return adapter.clipboardWrite(text)
|
|
@@ -0,0 +1,217 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "fileutils"
|
|
4
|
+
|
|
5
|
+
module Everywhere
|
|
6
|
+
# Compiles an app's native/android/assets/ tree into Android drawable
|
|
7
|
+
# resources so native code can reach them by name — R.drawable.logo in Kotlin,
|
|
8
|
+
# @drawable/logo in a layout.
|
|
9
|
+
#
|
|
10
|
+
# Deliberately the same filename API as the iOS side (see AssetCatalog), so one
|
|
11
|
+
# assets/ tree can be authored once and dropped into both native/ios/ and
|
|
12
|
+
# native/android/: <name>[@2x|@3x][~dark].<ext>. What differs is where the
|
|
13
|
+
# markers land, because Android expresses both as directory qualifiers rather
|
|
14
|
+
# than as catalog metadata:
|
|
15
|
+
#
|
|
16
|
+
# logo.png → res/drawable/logo.png (density-independent baseline)
|
|
17
|
+
# logo@2x.png → res/drawable-xhdpi/logo.png (2x = 320dpi)
|
|
18
|
+
# logo@3x.png → res/drawable-xxhdpi/logo.png (3x = 480dpi)
|
|
19
|
+
# logo~dark.png → res/drawable-night/logo.png
|
|
20
|
+
# logo@2x~dark.png→ res/drawable-night-xhdpi/logo.png
|
|
21
|
+
#
|
|
22
|
+
# Sub-directories organise the source tree but do NOT namespace the result —
|
|
23
|
+
# res/ is flat, and asset names are global on iOS too, so assets/branding/
|
|
24
|
+
# logo.png is R.drawable.logo either way.
|
|
25
|
+
module AndroidResources
|
|
26
|
+
# Raster formats aapt2 accepts as-is. WebP is included (and PDF/SVG are not)
|
|
27
|
+
# because that is the split Android itself draws — the reverse of iOS.
|
|
28
|
+
RASTER = %w[.png .jpg .jpeg .webp].freeze
|
|
29
|
+
|
|
30
|
+
# A drawable written as XML: vectors, shapes, layer lists, selectors. Scale
|
|
31
|
+
# -free, so the density qualifiers above don't apply to them.
|
|
32
|
+
VECTOR = %w[.xml].freeze
|
|
33
|
+
EXTENSIONS = (RASTER + VECTOR).freeze
|
|
34
|
+
|
|
35
|
+
# iOS's @2x/@3x in Android's density buckets. 1x is plain `drawable`, not
|
|
36
|
+
# `drawable-mdpi`: both mean 160dpi, but the unqualified directory is also
|
|
37
|
+
# the fallback every density falls back TO, which is what a single-file
|
|
38
|
+
# asset needs to be.
|
|
39
|
+
DENSITIES = { nil => nil, "2x" => "xhdpi", "3x" => "xxhdpi" }.freeze
|
|
40
|
+
|
|
41
|
+
# Written by the Android builder itself (adaptive launcher icon layers) or
|
|
42
|
+
# shipped in the frozen template's own res/. Both would collide in the
|
|
43
|
+
# merged resource table, so they're refused by name here — a merge conflict
|
|
44
|
+
# from aapt2 arrives minutes into a Gradle run and names no fix.
|
|
45
|
+
RESERVED = %w[
|
|
46
|
+
ic_launcher ic_launcher_round ic_launcher_foreground ic_launcher_background
|
|
47
|
+
ic_tab_placeholder
|
|
48
|
+
].freeze
|
|
49
|
+
|
|
50
|
+
# An Android resource entry is a field on the generated R class, so the name
|
|
51
|
+
# has to be a Java identifier — and aapt2 additionally rejects uppercase in
|
|
52
|
+
# a res/ filename. Hyphens and dots are legal on iOS and never here.
|
|
53
|
+
VALID_NAME = /\A[a-z][a-z0-9_]*\z/
|
|
54
|
+
|
|
55
|
+
# Reserved words can't name a field on R. `val`/`fun` are fine (Kotlin reads
|
|
56
|
+
# R from Java), so this is the Java list, not the Kotlin one.
|
|
57
|
+
JAVA_KEYWORDS = %w[
|
|
58
|
+
abstract assert boolean break byte case catch char class const continue default do double else
|
|
59
|
+
enum extends final finally float for goto if implements import instanceof int interface long
|
|
60
|
+
native new package private protected public return short static strictfp super switch
|
|
61
|
+
synchronized this throw throws transient try void volatile while
|
|
62
|
+
].freeze
|
|
63
|
+
|
|
64
|
+
class << self
|
|
65
|
+
# Copy every image under `source` into `res` as a drawable resource.
|
|
66
|
+
# Returns [names, errors]; nothing is written when errors is non-empty, so
|
|
67
|
+
# a broken assets/ tree can't half-stamp the resource dirs.
|
|
68
|
+
def write!(source, res)
|
|
69
|
+
variants, errors = plan(source)
|
|
70
|
+
return [[], errors] unless errors.empty?
|
|
71
|
+
|
|
72
|
+
variants.each do |variant|
|
|
73
|
+
dir = File.join(res, variant[:dir])
|
|
74
|
+
FileUtils.mkdir_p(dir)
|
|
75
|
+
FileUtils.cp(variant[:path], File.join(dir, "#{variant[:name]}#{variant[:extension]}"))
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
[variants.map { |variant| variant[:name] }.uniq.sort, []]
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
private
|
|
82
|
+
|
|
83
|
+
# Errors are collected (not raised) so one `every build` reports every
|
|
84
|
+
# problem in the tree instead of the first.
|
|
85
|
+
def plan(source)
|
|
86
|
+
errors = []
|
|
87
|
+
files = Dir.glob(File.join(source, "**", "*"))
|
|
88
|
+
.select { |path| File.file?(path) && !File.basename(path).start_with?(".") }
|
|
89
|
+
.sort
|
|
90
|
+
images, other = files.partition { |path| EXTENSIONS.include?(File.extname(path).downcase) }
|
|
91
|
+
|
|
92
|
+
other.each { |path| errors << unsupported(path, source) }
|
|
93
|
+
variants = images.filter_map { |path| variant(path, source, errors) }
|
|
94
|
+
errors.concat(clash_errors(variants))
|
|
95
|
+
|
|
96
|
+
[variants, errors]
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
def unsupported(path, source)
|
|
100
|
+
relative = relative(path, source)
|
|
101
|
+
case File.extname(path).downcase
|
|
102
|
+
when ".svg", ".pdf"
|
|
103
|
+
"#{relative}: Android has no runtime SVG or PDF renderer — export a PNG, or convert it to " \
|
|
104
|
+
"a VectorDrawable (Android Studio: right-click res → New → Vector Asset) and ship the .xml"
|
|
105
|
+
else
|
|
106
|
+
"#{relative}: unsupported image type — use #{EXTENSIONS.join(", ")}"
|
|
107
|
+
end
|
|
108
|
+
end
|
|
109
|
+
|
|
110
|
+
def variant(path, source, errors)
|
|
111
|
+
relative = relative(path, source)
|
|
112
|
+
extension = File.extname(path).downcase
|
|
113
|
+
stem = File.basename(path, File.extname(path))
|
|
114
|
+
|
|
115
|
+
# A nine-patch is `logo.9.png`, whose stem carries a dot that no valid
|
|
116
|
+
# resource name may — so it would otherwise fail with a naming error
|
|
117
|
+
# that says nothing about what the author actually tried to do.
|
|
118
|
+
if stem.end_with?(".9")
|
|
119
|
+
errors << "#{relative}: nine-patch drawables aren't supported yet — ship a plain #{extension} " \
|
|
120
|
+
"(or a VectorDrawable .xml, which stretches without one)"
|
|
121
|
+
return nil
|
|
122
|
+
end
|
|
123
|
+
|
|
124
|
+
name, scale, dark = parse(stem)
|
|
125
|
+
|
|
126
|
+
unless name.match?(VALID_NAME)
|
|
127
|
+
errors << "#{relative}: #{name.inspect} isn't a usable Android resource name — use lowercase " \
|
|
128
|
+
"letters, digits and _, starting with a letter (#{suggestion(name).inspect} would " \
|
|
129
|
+
"work). Unlike iOS, Android has no room for capitals, hyphens or dots here"
|
|
130
|
+
return nil
|
|
131
|
+
end
|
|
132
|
+
if JAVA_KEYWORDS.include?(name)
|
|
133
|
+
errors << "#{relative}: #{name.inspect} is a Java keyword, so it can't name a field on R — " \
|
|
134
|
+
"rename it (#{"#{name}_image".inspect})"
|
|
135
|
+
return nil
|
|
136
|
+
end
|
|
137
|
+
if RESERVED.include?(name)
|
|
138
|
+
errors << "#{relative}: #{name} is reserved by the shell (launcher icon and tab placeholder) — " \
|
|
139
|
+
"rename it"
|
|
140
|
+
return nil
|
|
141
|
+
end
|
|
142
|
+
if scale && VECTOR.include?(extension)
|
|
143
|
+
errors << "#{relative}: an XML drawable is scale-free — drop the @#{scale} suffix"
|
|
144
|
+
return nil
|
|
145
|
+
end
|
|
146
|
+
|
|
147
|
+
{ name: name, extension: extension, dir: directory(scale, dark), source_dir: File.dirname(relative),
|
|
148
|
+
scale: scale, dark: dark, vector: VECTOR.include?(extension), path: path, relative: relative }
|
|
149
|
+
end
|
|
150
|
+
|
|
151
|
+
# Trailing @2x/@3x and ~dark markers, in either order, off the stem —
|
|
152
|
+
# character for character AssetCatalog#parse, because the two platforms
|
|
153
|
+
# read the same filenames.
|
|
154
|
+
def parse(stem)
|
|
155
|
+
name = stem.dup
|
|
156
|
+
scale = nil
|
|
157
|
+
dark = false
|
|
158
|
+
loop do
|
|
159
|
+
if name.sub!(/~dark\z/, "")
|
|
160
|
+
dark = true
|
|
161
|
+
elsif name.sub!(/@([23])x\z/, "")
|
|
162
|
+
scale = "#{Regexp.last_match(1)}x"
|
|
163
|
+
else
|
|
164
|
+
break
|
|
165
|
+
end
|
|
166
|
+
end
|
|
167
|
+
[name, scale, dark]
|
|
168
|
+
end
|
|
169
|
+
|
|
170
|
+
# Qualifier order is fixed by the platform and is not alphabetical: UI mode
|
|
171
|
+
# (night) comes before density. `drawable-xhdpi-night` is not a directory
|
|
172
|
+
# Android will ever match — it's silently ignored, which reads as "my dark
|
|
173
|
+
# asset doesn't work" with nothing in the log.
|
|
174
|
+
def directory(scale, dark)
|
|
175
|
+
["drawable", dark ? "night" : nil, DENSITIES.fetch(scale)].compact.join("-")
|
|
176
|
+
end
|
|
177
|
+
|
|
178
|
+
# What the author probably meant, so the error can show it rather than
|
|
179
|
+
# describe it. Not applied automatically: R.drawable.<name> is written by
|
|
180
|
+
# hand in Kotlin, so a name we invented is a name nobody can guess.
|
|
181
|
+
def suggestion(name)
|
|
182
|
+
candidate = name.gsub(/([a-z0-9])([A-Z])/, '\1_\2').downcase.gsub(/[^a-z0-9]+/, "_")
|
|
183
|
+
.gsub(/_+/, "_").delete_prefix("_").delete_suffix("_")
|
|
184
|
+
candidate.match?(/\A[a-z]/) ? candidate : "image_#{candidate}"
|
|
185
|
+
end
|
|
186
|
+
|
|
187
|
+
def clash_errors(variants)
|
|
188
|
+
errors = []
|
|
189
|
+
variants.group_by { |variant| variant[:name] }.each do |name, group|
|
|
190
|
+
dirs = group.map { |variant| variant[:source_dir] }.uniq
|
|
191
|
+
if dirs.length > 1
|
|
192
|
+
listed = dirs.map { |dir| dir == "." ? "assets/" : "assets/#{dir}/" }.join(" and ")
|
|
193
|
+
errors << "#{name}: same asset name in #{listed} — res/ is flat, so both would become " \
|
|
194
|
+
"R.drawable.#{name}. Rename one"
|
|
195
|
+
end
|
|
196
|
+
if group.map { |variant| variant[:vector] }.uniq.length > 1
|
|
197
|
+
errors << "#{name}: mixes an XML drawable with bitmaps — pick one per asset"
|
|
198
|
+
end
|
|
199
|
+
|
|
200
|
+
# Same destination directory and same name: aapt2 sees two files
|
|
201
|
+
# claiming one resource, whatever their extensions.
|
|
202
|
+
group.group_by { |variant| variant[:dir] }.each_value do |clash|
|
|
203
|
+
next if clash.length < 2
|
|
204
|
+
|
|
205
|
+
errors << "#{name}: #{clash.map { |variant| variant[:relative] }.join(" and ")} both become " \
|
|
206
|
+
"res/#{clash.first[:dir]}/#{name}"
|
|
207
|
+
end
|
|
208
|
+
end
|
|
209
|
+
errors
|
|
210
|
+
end
|
|
211
|
+
|
|
212
|
+
def relative(path, source)
|
|
213
|
+
path.delete_prefix("#{source}/")
|
|
214
|
+
end
|
|
215
|
+
end
|
|
216
|
+
end
|
|
217
|
+
end
|