ruby_everywhere 0.3.0 → 0.5.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 +339 -0
- data/bridge/README.md +70 -1
- data/bridge/everywhere/bridge.js +211 -3
- data/bridge/everywhere/native.css +61 -0
- 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 +404 -0
- data/lib/everywhere/auth_token.rb +118 -0
- data/lib/everywhere/builders/android.rb +844 -0
- data/lib/everywhere/builders/desktop.rb +326 -0
- data/lib/everywhere/builders/ios.rb +40 -3
- data/lib/everywhere/child_processes.rb +74 -0
- data/lib/everywhere/commands/build.rb +95 -14
- data/lib/everywhere/commands/clean.rb +10 -5
- data/lib/everywhere/commands/dev.rb +462 -57
- data/lib/everywhere/commands/doctor.rb +89 -3
- data/lib/everywhere/commands/install.rb +21 -1
- data/lib/everywhere/commands/logs.rb +117 -8
- data/lib/everywhere/commands/release.rb +2 -2
- data/lib/everywhere/config.rb +703 -13
- data/lib/everywhere/console.rb +117 -0
- data/lib/everywhere/desktop_assets.rb +150 -0
- data/lib/everywhere/dock/footer.rb +150 -0
- data/lib/everywhere/dock/screen.rb +114 -0
- data/lib/everywhere/dock/state.rb +59 -0
- data/lib/everywhere/dock.rb +238 -0
- data/lib/everywhere/emulator.rb +405 -0
- data/lib/everywhere/engine.rb +12 -0
- data/lib/everywhere/fatal.rb +20 -0
- data/lib/everywhere/icon.rb +196 -0
- data/lib/everywhere/line_pump.rb +89 -0
- data/lib/everywhere/log_filter.rb +111 -0
- data/lib/everywhere/native_helper.rb +39 -9
- data/lib/everywhere/omniauth.rb +88 -0
- data/lib/everywhere/paths.rb +109 -11
- data/lib/everywhere/relay.rb +77 -0
- data/lib/everywhere/shellout.rb +90 -15
- data/lib/everywhere/task_pool.rb +123 -0
- data/lib/everywhere/ui.rb +54 -11
- data/lib/everywhere/version.rb +1 -1
- data/support/desktop/README.md +121 -0
- data/support/{shell → desktop}/src-tauri/Cargo.lock +23 -0
- data/support/{shell → desktop}/src-tauri/Cargo.toml +19 -1
- data/support/{shell → desktop}/src-tauri/capabilities/default.json +7 -0
- data/support/desktop/src-tauri/gen/schemas/capabilities.json +1 -0
- data/support/desktop/src-tauri/src/extension_host.rs +53 -0
- data/support/desktop/src-tauri/src/extensions/mod.rs +39 -0
- data/support/{shell → desktop}/src-tauri/src/main.rs +355 -10
- data/support/{shell → desktop}/src-tauri/tauri.conf.json +2 -2
- 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
- data/support/{macos → release/macos}/notarize.sh +2 -2
- metadata +97 -17
- data/support/github/build.yml +0 -85
- data/support/shell/src-tauri/gen/schemas/capabilities.json +0 -1
- /data/support/{shell → desktop}/splash/index.html +0 -0
- /data/support/{shell → desktop}/src-tauri/build.rs +0 -0
- /data/support/{shell → desktop}/src-tauri/gen/schemas/acl-manifests.json +0 -0
- /data/support/{shell → desktop}/src-tauri/gen/schemas/desktop-schema.json +0 -0
- /data/support/{shell → desktop}/src-tauri/gen/schemas/macOS-schema.json +0 -0
- /data/support/{shell → desktop}/src-tauri/icons/icon.png +0 -0
- /data/support/{shell → desktop}/src-tauri/src/updater.rs +0 -0
- /data/support/{macos → release/macos}/entitlements.plist +0 -0
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
|
|
@@ -177,6 +178,19 @@ function detectPlatform() {
|
|
|
177
178
|
|
|
178
179
|
// --- desktop: the Tauri-based RubyEverywhere shell -------------------------
|
|
179
180
|
|
|
181
|
+
// Distinguishes concurrent desktop command calls. Only has to be unique within
|
|
182
|
+
// one page, so a counter is enough; the random suffix just keeps two rapid
|
|
183
|
+
// calls from colliding if the page is restored from bfcache mid-flight.
|
|
184
|
+
let commandSequence = 0
|
|
185
|
+
const COMMAND_TIMEOUT = 30000
|
|
186
|
+
|
|
187
|
+
// The shell's own window handle. Named so it can't shadow the global `window`
|
|
188
|
+
// inside the adapter below, where every other method still needs the real one.
|
|
189
|
+
function tauriWindow() {
|
|
190
|
+
const api = window.__TAURI__ && window.__TAURI__.window
|
|
191
|
+
return api && api.getCurrentWindow ? api.getCurrentWindow() : null
|
|
192
|
+
}
|
|
193
|
+
|
|
180
194
|
const desktop = {
|
|
181
195
|
notify({ title, body }) {
|
|
182
196
|
return window.__TAURI__.event.emit("everywhere:notify", { title, body })
|
|
@@ -209,6 +223,56 @@ const desktop = {
|
|
|
209
223
|
return window.__TAURI__.clipboardManager.readText()
|
|
210
224
|
},
|
|
211
225
|
|
|
226
|
+
// Window controls, for apps that draw their own title bar (`window.title_bar:
|
|
227
|
+
// frameless`, and `overlay` on Windows/Linux where there's no native overlay
|
|
228
|
+
// style). Without a system title bar there's nothing to drag or click, so the
|
|
229
|
+
// page supplies both and calls these. One entry point rather than six methods
|
|
230
|
+
// because the Tauri window API is already uniform: every one of these is a
|
|
231
|
+
// no-arg call returning a promise.
|
|
232
|
+
//
|
|
233
|
+
// These are ACL-gated. The matching core:window:allow-* permissions have to be
|
|
234
|
+
// in BOTH capability sets (capabilities/default.json for local origins, and
|
|
235
|
+
// the mirrored list in authorize_remote_origin for remote mode) or they reject
|
|
236
|
+
// at runtime.
|
|
237
|
+
windowControl(action) {
|
|
238
|
+
const win = tauriWindow()
|
|
239
|
+
return win ? win[action]() : Promise.resolve(null)
|
|
240
|
+
},
|
|
241
|
+
|
|
242
|
+
// Call a Rust function the app keeps in native/desktop/. Events, not invoke:
|
|
243
|
+
// a RubyEverywhere page is always served over http (127.0.0.1 in local mode,
|
|
244
|
+
// your domain in remote mode) and Tauri v2 refuses invoke from any http
|
|
245
|
+
// origin — even the packaged app's own localhost server. See the comment on
|
|
246
|
+
// install_command_bridge in the shell's extension_host.rs.
|
|
247
|
+
//
|
|
248
|
+
// The id round-trips so concurrent calls can't take each other's answers,
|
|
249
|
+
// which a bare request/response event pair would otherwise allow.
|
|
250
|
+
desktopInvoke(name, payload) {
|
|
251
|
+
return new Promise((resolve, reject) => {
|
|
252
|
+
const id = `${commandSequence++}-${Math.random().toString(36).slice(2)}`
|
|
253
|
+
let off = null
|
|
254
|
+
const settle = (fn, value) => {
|
|
255
|
+
clearTimeout(timer)
|
|
256
|
+
if (off) off()
|
|
257
|
+
fn(value)
|
|
258
|
+
}
|
|
259
|
+
const timer = setTimeout(
|
|
260
|
+
() => settle(reject, new Error(`desktop command ${name} timed out`)),
|
|
261
|
+
COMMAND_TIMEOUT
|
|
262
|
+
)
|
|
263
|
+
off = this.on("command-result", (result) => {
|
|
264
|
+
if (!result || result.id !== id) return
|
|
265
|
+
if (result.ok) settle(resolve, result.value)
|
|
266
|
+
else settle(reject, new Error(result.error || `desktop command ${name} failed`))
|
|
267
|
+
})
|
|
268
|
+
window.__TAURI__.event.emit("everywhere:command", {
|
|
269
|
+
id,
|
|
270
|
+
name,
|
|
271
|
+
payload: payload === undefined ? null : payload
|
|
272
|
+
})
|
|
273
|
+
})
|
|
274
|
+
},
|
|
275
|
+
|
|
212
276
|
// Ask the shell to check its update feed; resolve off whichever update
|
|
213
277
|
// event answers first. The shell always replies to an explicit check
|
|
214
278
|
// (available / none / error), the timeout is just a safety net.
|
|
@@ -294,6 +358,22 @@ const browser = {
|
|
|
294
358
|
return null
|
|
295
359
|
},
|
|
296
360
|
|
|
361
|
+
// A browser tab has no window of its own to drive — the one thing close()
|
|
362
|
+
// could map to (window.close) only works for script-opened windows, which
|
|
363
|
+
// this never is. Resolving null keeps a page that draws a custom title bar
|
|
364
|
+
// from throwing when it's opened in a plain browser.
|
|
365
|
+
windowControl() {
|
|
366
|
+
return Promise.resolve(null)
|
|
367
|
+
},
|
|
368
|
+
|
|
369
|
+
// There is no Rust here. Rejecting (rather than resolving null) is the honest
|
|
370
|
+
// answer: unlike the rest of the bridge, a desktop command has no web
|
|
371
|
+
// equivalent to degrade to, and silently resolving would let a page act on a
|
|
372
|
+
// result it never got. Gate on Everywhere.desktop.supported.
|
|
373
|
+
desktopInvoke(name) {
|
|
374
|
+
return Promise.reject(new Error(`desktop command ${name} is unavailable outside the desktop shell`))
|
|
375
|
+
},
|
|
376
|
+
|
|
297
377
|
// Honest approximation: the Vibration API where it exists (Android
|
|
298
378
|
// browsers); silently nothing elsewhere (iOS Safari has no equivalent).
|
|
299
379
|
haptic(kind, value) {
|
|
@@ -532,8 +612,9 @@ const mobile = {
|
|
|
532
612
|
// Ask the shell to re-fetch its native navigation config (tabs + path
|
|
533
613
|
// rules) now, instead of waiting for the next app foreground. Call it after
|
|
534
614
|
// a change that affects which tabs should show — signing in or out — so the
|
|
535
|
-
// tab bar updates immediately. Uses a dedicated
|
|
536
|
-
//
|
|
615
|
+
// tab bar updates immediately. Uses the shell's control channel (a dedicated
|
|
616
|
+
// WKScriptMessageHandler on iOS, a WebMessageListener on Android), so it
|
|
617
|
+
// doesn't depend on a bridge component being mounted on the page.
|
|
537
618
|
reloadTabs() {
|
|
538
619
|
const control = nativeControlChannel()
|
|
539
620
|
if (control) {
|
|
@@ -566,6 +647,16 @@ const mobile = {
|
|
|
566
647
|
if (control) control.postMessage({ action: "clearInstance", ...(to ? { to } : {}) })
|
|
567
648
|
},
|
|
568
649
|
|
|
650
|
+
// Open a provider sign-in in the system browser rather than this web view —
|
|
651
|
+
// where Google refuses to run at all and the rest lose the password manager
|
|
652
|
+
// and the user's existing session. The shell takes it from here: auth
|
|
653
|
+
// session, one-time handoff token, full reset onto the signed-in app.
|
|
654
|
+
authSignIn(path) {
|
|
655
|
+
const control = nativeControlChannel()
|
|
656
|
+
if (control) control.postMessage({ action: "authFlow", to: path })
|
|
657
|
+
else window.location.assign(path)
|
|
658
|
+
},
|
|
659
|
+
|
|
569
660
|
// Real haptics via the shell's "everywhere--haptics" component; vibration
|
|
570
661
|
// fallback while the handshake completes or on an older shell.
|
|
571
662
|
haptic(kind, value) {
|
|
@@ -702,10 +793,43 @@ const mobile = {
|
|
|
702
793
|
}
|
|
703
794
|
}
|
|
704
795
|
|
|
796
|
+
// The shell's out-of-band command line: app-level actions (reset, tabs,
|
|
797
|
+
// badges, instance, auth) that must work on any page, whether or not a bridge
|
|
798
|
+
// component happens to be mounted on it. Each shell injects it its own way,
|
|
799
|
+
// and the two APIs disagree about what a message is:
|
|
800
|
+
//
|
|
801
|
+
// iOS — WKUserContentController.add(_:name:) hands us
|
|
802
|
+
// window.webkit.messageHandlers.everywhereControl, whose
|
|
803
|
+
// postMessage takes an *object* and serializes it across the
|
|
804
|
+
// boundary for us.
|
|
805
|
+
// Android — WebViewCompat.addWebMessageListener("everywhereControl") hands us
|
|
806
|
+
// window.everywhereControl, backed by a WebMessagePort, and a web
|
|
807
|
+
// message is *text*: postMessage(String). (Android has no user
|
|
808
|
+
// scripts either, so the shell injects this with
|
|
809
|
+
// addDocumentStartJavaScript — it's in place before page JS runs.)
|
|
810
|
+
//
|
|
811
|
+
// Normalizing here rather than at each call site is the point: the seven
|
|
812
|
+
// callers below all speak the object shape, and only this function knows which
|
|
813
|
+
// shell is listening. iOS gets its handler back untouched — it already is the
|
|
814
|
+
// normalized shape, so that path stays byte-for-byte what it was.
|
|
705
815
|
function nativeControlChannel() {
|
|
706
|
-
|
|
816
|
+
if (typeof window === "undefined") return null
|
|
817
|
+
|
|
818
|
+
const webkit =
|
|
819
|
+
window.webkit &&
|
|
707
820
|
window.webkit.messageHandlers &&
|
|
708
821
|
window.webkit.messageHandlers.everywhereControl
|
|
822
|
+
if (webkit) return webkit
|
|
823
|
+
|
|
824
|
+
const android = window.everywhereControl
|
|
825
|
+
if (android && typeof android.postMessage === "function") {
|
|
826
|
+
// Built per call, not cached: a fresh document (a Turbo drive visit is
|
|
827
|
+
// not, but a real navigation is) gets a fresh injected object, and there's
|
|
828
|
+
// nothing here worth keeping alive across one.
|
|
829
|
+
return { postMessage: (message) => android.postMessage(JSON.stringify(message)) }
|
|
830
|
+
}
|
|
831
|
+
|
|
832
|
+
return null
|
|
709
833
|
}
|
|
710
834
|
|
|
711
835
|
// --- public surface ----------------------------------------------------------
|
|
@@ -719,6 +843,17 @@ const adapter = adapters[platform]
|
|
|
719
843
|
// registered by the time app code first calls notify().
|
|
720
844
|
if (platform === "mobile") installWebBridge()
|
|
721
845
|
|
|
846
|
+
// Publish the window chrome mode to CSS. The titlebar utilities in native.css
|
|
847
|
+
// key off this attribute, which is the only way they can know a mode that lives
|
|
848
|
+
// in everywhere.yml — and it keeps them inert everywhere else, since nothing
|
|
849
|
+
// stamps it in a browser tab, on mobile, or in a normally decorated window.
|
|
850
|
+
if (platform === "desktop" && typeof document !== "undefined") {
|
|
851
|
+
const titleBar = config.window && config.window.title_bar
|
|
852
|
+
if (titleBar && titleBar !== "decorated") {
|
|
853
|
+
document.documentElement.dataset.everywhereTitlebar = titleBar
|
|
854
|
+
}
|
|
855
|
+
}
|
|
856
|
+
|
|
722
857
|
// Mutable so setChannel can keep updates.channel truthful without a reload.
|
|
723
858
|
let updatesChannel = (config.updates && config.updates.channel) || null
|
|
724
859
|
|
|
@@ -790,6 +925,18 @@ export const Everywhere = {
|
|
|
790
925
|
else this.visit(to || window.location.pathname)
|
|
791
926
|
},
|
|
792
927
|
|
|
928
|
+
// Third-party sign-in (everywhere.yml `auth:`). You rarely need this: the
|
|
929
|
+
// shell already diverts the paths listed there, so a plain link to
|
|
930
|
+
// /auth/github does the right thing everywhere. Call it when the sign-in is
|
|
931
|
+
// driven by something the shell can't see as a visit — a fetch, a custom
|
|
932
|
+
// element, a button that has other work to do first.
|
|
933
|
+
auth: {
|
|
934
|
+
signIn(path) {
|
|
935
|
+
if (adapter.authSignIn) adapter.authSignIn(path)
|
|
936
|
+
else window.location.assign(path)
|
|
937
|
+
}
|
|
938
|
+
},
|
|
939
|
+
|
|
793
940
|
clipboard: {
|
|
794
941
|
write(text) {
|
|
795
942
|
return adapter.clipboardWrite(text)
|
|
@@ -977,6 +1124,67 @@ export const Everywhere = {
|
|
|
977
1124
|
}
|
|
978
1125
|
},
|
|
979
1126
|
|
|
1127
|
+
// Rust the app keeps in native/desktop/ and declares under
|
|
1128
|
+
// `native.desktop.commands` in everywhere.yml:
|
|
1129
|
+
//
|
|
1130
|
+
// const ports = await Everywhere.desktop.invoke("scan_ports", { baud: 9600 })
|
|
1131
|
+
//
|
|
1132
|
+
// The payload arrives as serde_json::Value and whatever the command returns
|
|
1133
|
+
// comes back as the resolved value. Errors reject with the command's own
|
|
1134
|
+
// message. Rejects immediately off the desktop — there's no web fallback for
|
|
1135
|
+
// "talk to a serial port", so check `supported` first.
|
|
1136
|
+
desktop: {
|
|
1137
|
+
get supported() {
|
|
1138
|
+
return platform === "desktop"
|
|
1139
|
+
},
|
|
1140
|
+
invoke(name, payload) {
|
|
1141
|
+
return adapter.desktopInvoke(name, payload)
|
|
1142
|
+
}
|
|
1143
|
+
},
|
|
1144
|
+
|
|
1145
|
+
// Desktop window controls, for apps that draw their own title bar. Set
|
|
1146
|
+
// `window.title_bar: overlay` (macOS traffic lights float over your content)
|
|
1147
|
+
// or `frameless` (no system chrome at all) in everywhere.yml; frameless is
|
|
1148
|
+
// the one that needs these, since the page then owns dragging and the
|
|
1149
|
+
// minimize/maximize/close buttons:
|
|
1150
|
+
//
|
|
1151
|
+
// <header data-tauri-drag-region>
|
|
1152
|
+
// <button onclick="Everywhere.window.close()">×</button>
|
|
1153
|
+
// </header>
|
|
1154
|
+
//
|
|
1155
|
+
// Everything resolves to null in a browser tab and on mobile, where the OS
|
|
1156
|
+
// owns the frame — so the same markup is safe everywhere.
|
|
1157
|
+
window: {
|
|
1158
|
+
get supported() {
|
|
1159
|
+
return platform === "desktop"
|
|
1160
|
+
},
|
|
1161
|
+
minimize() {
|
|
1162
|
+
return adapter.windowControl("minimize")
|
|
1163
|
+
},
|
|
1164
|
+
maximize() {
|
|
1165
|
+
return adapter.windowControl("maximize")
|
|
1166
|
+
},
|
|
1167
|
+
unmaximize() {
|
|
1168
|
+
return adapter.windowControl("unmaximize")
|
|
1169
|
+
},
|
|
1170
|
+
// What a double-click on a title bar normally does.
|
|
1171
|
+
toggleMaximize() {
|
|
1172
|
+
return adapter.windowControl("toggleMaximize")
|
|
1173
|
+
},
|
|
1174
|
+
close() {
|
|
1175
|
+
return adapter.windowControl("close")
|
|
1176
|
+
},
|
|
1177
|
+
isMaximized() {
|
|
1178
|
+
return adapter.windowControl("isMaximized").then((maximized) => !!maximized)
|
|
1179
|
+
},
|
|
1180
|
+
// Call from a pointerdown handler on your custom title bar. Tauri's
|
|
1181
|
+
// data-tauri-drag-region attribute does this for you; this is the escape
|
|
1182
|
+
// hatch for a drag region you compute yourself.
|
|
1183
|
+
startDragging() {
|
|
1184
|
+
return adapter.windowControl("startDragging")
|
|
1185
|
+
}
|
|
1186
|
+
},
|
|
1187
|
+
|
|
980
1188
|
updates: {
|
|
981
1189
|
// True only when the shell actually has a feed to check (everywhere.yml
|
|
982
1190
|
// updates: url + public_key made it into the shipped config).
|
|
@@ -201,3 +201,64 @@ html[data-bridge-platform] [data-everywhere-nav-menu] {
|
|
|
201
201
|
@keyframes everywhere-sheet-rise {
|
|
202
202
|
from { transform: translateY(1rem); opacity: 0; }
|
|
203
203
|
}
|
|
204
|
+
|
|
205
|
+
/* ---------------------------------------------------------------------------
|
|
206
|
+
* Desktop window chrome — for `window.title_bar: overlay | frameless` in
|
|
207
|
+
* everywhere.yml. The bridge stamps data-everywhere-titlebar on <html>, so
|
|
208
|
+
* every rule here is scoped to the mode that needs it: on mobile, in a plain
|
|
209
|
+
* browser tab, and in a normal decorated window the attribute is absent and
|
|
210
|
+
* these classes do nothing. Safe to apply unconditionally.
|
|
211
|
+
* ------------------------------------------------------------------------- */
|
|
212
|
+
|
|
213
|
+
:root {
|
|
214
|
+
/* macOS traffic lights: a ~78px cluster inside a 28px band at the top left.
|
|
215
|
+
* Override both if you draw a taller bar. */
|
|
216
|
+
--everywhere-titlebar-height: 28px;
|
|
217
|
+
--everywhere-titlebar-controls: 78px;
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
/* Overlay mode: the system still draws (and positions) the traffic lights over
|
|
221
|
+
* your page. Reserve the band so your content doesn't slide underneath them.
|
|
222
|
+
* Frameless draws no controls at all, so it reserves nothing — the page owns
|
|
223
|
+
* that space and decides what goes there. */
|
|
224
|
+
[data-everywhere-titlebar="overlay"] .everywhere-titlebar-inset {
|
|
225
|
+
padding-top: var(--everywhere-titlebar-height);
|
|
226
|
+
}
|
|
227
|
+
[data-everywhere-titlebar="overlay"] .everywhere-titlebar-controls-inset {
|
|
228
|
+
padding-left: var(--everywhere-titlebar-controls);
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
/* A title-bar strip across the top of the window. Purely layout — the shell
|
|
232
|
+
* already makes the top 28px drag the window, so this needs no attribute:
|
|
233
|
+
*
|
|
234
|
+
* <header class="everywhere-titlebar">My App</header>
|
|
235
|
+
*
|
|
236
|
+
* Buttons and links inside it keep their clicks. Mark a region outside the
|
|
237
|
+
* strip with data-tauri-drag-region to make that draggable too, or set
|
|
238
|
+
* window.drag_height: 0 in everywhere.yml to take dragging over entirely.
|
|
239
|
+
*
|
|
240
|
+
* In overlay mode it clears the traffic lights; in frameless it starts at the
|
|
241
|
+
* left edge, because there's nothing there to clear. */
|
|
242
|
+
[data-everywhere-titlebar] .everywhere-titlebar {
|
|
243
|
+
display: flex;
|
|
244
|
+
align-items: center;
|
|
245
|
+
gap: 0.5rem;
|
|
246
|
+
height: var(--everywhere-titlebar-height);
|
|
247
|
+
user-select: none;
|
|
248
|
+
-webkit-user-select: none;
|
|
249
|
+
cursor: default;
|
|
250
|
+
}
|
|
251
|
+
[data-everywhere-titlebar="overlay"] .everywhere-titlebar {
|
|
252
|
+
padding-left: var(--everywhere-titlebar-controls);
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
/* Anything clickable inside the drag strip has to opt out, or the drag region
|
|
256
|
+
* swallows the click before the control ever sees it. */
|
|
257
|
+
[data-everywhere-titlebar] .everywhere-titlebar button,
|
|
258
|
+
[data-everywhere-titlebar] .everywhere-titlebar a,
|
|
259
|
+
[data-everywhere-titlebar] .everywhere-titlebar input,
|
|
260
|
+
[data-everywhere-titlebar] .everywhere-titlebar select,
|
|
261
|
+
[data-everywhere-titlebar] .everywhere-titlebar [data-tauri-drag-region="false"] {
|
|
262
|
+
-webkit-app-region: no-drag;
|
|
263
|
+
cursor: default;
|
|
264
|
+
}
|
|
@@ -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
|