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.
Files changed (88) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +255 -0
  3. data/bridge/everywhere/bridge.js +60 -3
  4. data/lib/everywhere/android_resources.rb +217 -0
  5. data/lib/everywhere/android_sdk.rb +330 -0
  6. data/lib/everywhere/asset_catalog.rb +185 -0
  7. data/lib/everywhere/auth_handoff.rb +398 -0
  8. data/lib/everywhere/auth_token.rb +118 -0
  9. data/lib/everywhere/builders/android.rb +844 -0
  10. data/lib/everywhere/builders/ios.rb +40 -3
  11. data/lib/everywhere/commands/build.rb +51 -9
  12. data/lib/everywhere/commands/clean.rb +10 -5
  13. data/lib/everywhere/commands/dev.rb +46 -8
  14. data/lib/everywhere/commands/doctor.rb +89 -3
  15. data/lib/everywhere/commands/install.rb +1 -1
  16. data/lib/everywhere/commands/logs.rb +117 -8
  17. data/lib/everywhere/config.rb +409 -13
  18. data/lib/everywhere/emulator.rb +405 -0
  19. data/lib/everywhere/engine.rb +12 -0
  20. data/lib/everywhere/icon.rb +196 -0
  21. data/lib/everywhere/log_filter.rb +74 -0
  22. data/lib/everywhere/native_helper.rb +2 -5
  23. data/lib/everywhere/omniauth.rb +88 -0
  24. data/lib/everywhere/paths.rb +47 -0
  25. data/lib/everywhere/version.rb +1 -1
  26. data/support/mobile/android/README.md +217 -0
  27. data/support/mobile/android/app/build.gradle.kts +156 -0
  28. data/support/mobile/android/app/everywhere.properties +8 -0
  29. data/support/mobile/android/app/native-packages.gradle.kts +16 -0
  30. data/support/mobile/android/app/proguard-rules.pro +21 -0
  31. data/support/mobile/android/app/src/debug/res/xml/network_security_config.xml +30 -0
  32. data/support/mobile/android/app/src/main/AndroidManifest.xml +42 -0
  33. data/support/mobile/android/app/src/main/assets/everywhere.json +9 -0
  34. data/support/mobile/android/app/src/main/assets/fonts/MaterialIcons-Regular.codepoints +2235 -0
  35. data/support/mobile/android/app/src/main/assets/fonts/MaterialIcons-Regular.ttf +0 -0
  36. data/support/mobile/android/app/src/main/assets/json/path-configuration.json +19 -0
  37. data/support/mobile/android/app/src/main/java/com/rubyeverywhere/shell/AuthFlow.kt +268 -0
  38. data/support/mobile/android/app/src/main/java/com/rubyeverywhere/shell/EverywhereApplication.kt +178 -0
  39. data/support/mobile/android/app/src/main/java/com/rubyeverywhere/shell/EverywhereConfig.kt +409 -0
  40. data/support/mobile/android/app/src/main/java/com/rubyeverywhere/shell/EverywhereEvents.kt +68 -0
  41. data/support/mobile/android/app/src/main/java/com/rubyeverywhere/shell/EverywhereHost.kt +101 -0
  42. data/support/mobile/android/app/src/main/java/com/rubyeverywhere/shell/IconFont.kt +211 -0
  43. data/support/mobile/android/app/src/main/java/com/rubyeverywhere/shell/MainActivity.kt +975 -0
  44. data/support/mobile/android/app/src/main/java/com/rubyeverywhere/shell/MainTabs.kt +201 -0
  45. data/support/mobile/android/app/src/main/java/com/rubyeverywhere/shell/WebBottomSheetFragment.kt +13 -0
  46. data/support/mobile/android/app/src/main/java/com/rubyeverywhere/shell/WebFragment.kt +36 -0
  47. data/support/mobile/android/app/src/main/java/com/rubyeverywhere/shell/bridge/BiometricsComponent.kt +535 -0
  48. data/support/mobile/android/app/src/main/java/com/rubyeverywhere/shell/bridge/HapticsComponent.kt +141 -0
  49. data/support/mobile/android/app/src/main/java/com/rubyeverywhere/shell/bridge/MenuComponent.kt +361 -0
  50. data/support/mobile/android/app/src/main/java/com/rubyeverywhere/shell/bridge/NotificationComponent.kt +151 -0
  51. data/support/mobile/android/app/src/main/java/com/rubyeverywhere/shell/bridge/PermissionsComponent.kt +271 -0
  52. data/support/mobile/android/app/src/main/java/com/rubyeverywhere/shell/bridge/StorageComponent.kt +101 -0
  53. data/support/mobile/android/app/src/main/java/com/rubyeverywhere/shell/bridge/WebControlChannel.kt +204 -0
  54. data/support/mobile/android/app/src/main/java/com/rubyeverywhere/shell/extensions/EverywhereExtensions.kt +51 -0
  55. data/support/mobile/android/app/src/main/res/color/bottom_navigation_active_indicator.xml +4 -0
  56. data/support/mobile/android/app/src/main/res/color/bottom_navigation_item.xml +11 -0
  57. data/support/mobile/android/app/src/main/res/drawable/ic_launcher_foreground.xml +26 -0
  58. data/support/mobile/android/app/src/main/res/drawable/ic_tab_placeholder.xml +28 -0
  59. data/support/mobile/android/app/src/main/res/layout/activity_main.xml +116 -0
  60. data/support/mobile/android/app/src/main/res/layout/bridge_menu_sheet.xml +83 -0
  61. data/support/mobile/android/app/src/main/res/layout/bridge_menu_sheet_item.xml +35 -0
  62. data/support/mobile/android/app/src/main/res/layout/tab_more_sheet.xml +48 -0
  63. data/support/mobile/android/app/src/main/res/layout/tab_more_sheet_item.xml +35 -0
  64. data/support/mobile/android/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml +12 -0
  65. data/support/mobile/android/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml +6 -0
  66. data/support/mobile/android/app/src/main/res/values/colors.xml +21 -0
  67. data/support/mobile/android/app/src/main/res/values/ic_launcher_background.xml +5 -0
  68. data/support/mobile/android/app/src/main/res/values/strings.xml +18 -0
  69. data/support/mobile/android/app/src/main/res/values/styles.xml +32 -0
  70. data/support/mobile/android/app/src/main/res/values/themes.xml +53 -0
  71. data/support/mobile/android/app/src/main/res/values-night/colors.xml +11 -0
  72. data/support/mobile/android/app/src/main/res/values-night/themes.xml +33 -0
  73. data/support/mobile/android/app/src/main/res/xml/network_security_config.xml +12 -0
  74. data/support/mobile/android/app/src/stamped/AndroidManifest.xml +34 -0
  75. data/support/mobile/android/build.gradle.kts +8 -0
  76. data/support/mobile/android/gradle/wrapper/gradle-wrapper.jar +0 -0
  77. data/support/mobile/android/gradle/wrapper/gradle-wrapper.properties +10 -0
  78. data/support/mobile/android/gradle.properties +14 -0
  79. data/support/mobile/android/gradlew +251 -0
  80. data/support/mobile/android/gradlew.bat +94 -0
  81. data/support/mobile/android/settings.gradle.kts +28 -0
  82. data/support/mobile/ios/App/AppDelegate.swift +5 -0
  83. data/support/mobile/ios/App/AuthFlow.swift +101 -0
  84. data/support/mobile/ios/App/EverywhereConfig.swift +84 -0
  85. data/support/mobile/ios/App/SceneDelegate.swift +103 -0
  86. data/support/mobile/ios/App.xcodeproj/project.pbxproj +4 -0
  87. data/support/mobile/ios/README.md +4 -1
  88. metadata +67 -1
@@ -0,0 +1,211 @@
1
+ package com.rubyeverywhere.shell
2
+
3
+ import android.content.Context
4
+ import android.graphics.Bitmap
5
+ import android.graphics.Canvas
6
+ import android.graphics.Color
7
+ import android.graphics.Paint
8
+ import android.graphics.Typeface
9
+ import android.graphics.drawable.BitmapDrawable
10
+ import android.graphics.drawable.Drawable
11
+ import android.util.Log
12
+
13
+ /**
14
+ * Material icons resolved by name, at runtime, from a bundled font.
15
+ *
16
+ * Android has no `UIImage(systemName:)`, but Google ships the Material icons as
17
+ * a font where each icon is a codepoint keyed by its name — so
18
+ * `icons.android: favorite` resolves exactly like an SF Symbol, with no
19
+ * drawable catalog to curate. That matters beyond tab bars: nav-bar buttons and
20
+ * overflow menu items take their icon names from page HTML
21
+ * (`data-everywhere-menu-icon-android="share"`), which only exists at runtime.
22
+ * A curated drawable set could never cover those. A font can.
23
+ *
24
+ * The font in `assets/fonts/` is stamped by the builder — Material Symbols
25
+ * (4,267 icons, variable, with a FILL axis) by default, or the bundled classic
26
+ * MaterialIcons-Regular (2,235 icons, 357 KB) for an offline build. This class
27
+ * discovers whichever one is there rather than naming it, so switching fonts is
28
+ * purely a build-time decision.
29
+ */
30
+ object IconFont {
31
+
32
+ private const val TAG = "Everywhere"
33
+ private const val ASSET_DIR = "fonts"
34
+
35
+ /** Rendered when a name isn't in the font's map. */
36
+ private const val FALLBACK_NAME = "circle"
37
+
38
+ private data class CacheKey(val name: String, val fill: Float, val sizePx: Int)
39
+
40
+ private val bitmaps = mutableMapOf<CacheKey, Bitmap>()
41
+ private val typefaces = mutableMapOf<Float, Typeface?>()
42
+
43
+ private var fontAssetPath: String? = null
44
+ private var codepointsAssetPath: String? = null
45
+ private var codepoints: Map<String, Int>? = null
46
+
47
+ /**
48
+ * A [Drawable] for a Material icon name. Never null: an unknown name falls
49
+ * back to `circle`, and a font that can't be read at all falls back to a
50
+ * blank square of the requested size. Callers put these straight into menu
51
+ * items and toolbars, where a null would mean branching at every call site
52
+ * over a case that should fail visibly, once, in the log — not structurally.
53
+ *
54
+ * @param name the icon name, optionally suffixed `.fill` (`favorite.fill`),
55
+ * mirroring the iOS `heart.fill` spelling. The suffix wins over [fill].
56
+ * @param sizePx the square size to rasterise at. Pass real pixels, not dp:
57
+ * the bitmap is resolution-dependent in a way a vector isn't, and a wrong
58
+ * density reads as blur.
59
+ * @param fill the FILL axis value, 0..1. Silently ignored by the classic
60
+ * font, which has no `fvar` table.
61
+ *
62
+ * The returned bitmap is drawn opaque white. Every visible colour comes
63
+ * from the ColorStateList the host widget applies (see
64
+ * `res/color/bottom_navigation_item.xml`), so the same cached bitmap serves
65
+ * light mode, dark mode and the selected state.
66
+ */
67
+ fun drawable(context: Context, name: String, sizePx: Int, fill: Float = 0f): Drawable {
68
+ val size = sizePx.coerceAtLeast(1)
69
+ val bitmap = bitmap(context, name, size, fill) ?: blank(size)
70
+ return BitmapDrawable(context.resources, bitmap)
71
+ }
72
+
73
+ /**
74
+ * The same thing for callers that only ever want "outlined or filled" — a
75
+ * nav button or a menu row, where the `.fill` suffix has already been split
76
+ * off the name. The axis is continuous, but nothing in the shell has a use
77
+ * for 0.4 of a fill.
78
+ */
79
+ fun drawable(context: Context, name: String, sizePx: Int, fill: Boolean): Drawable =
80
+ drawable(context, name, sizePx, if (fill) 1f else 0f)
81
+
82
+ private fun blank(sizePx: Int): Bitmap =
83
+ Bitmap.createBitmap(sizePx, sizePx, Bitmap.Config.ARGB_8888)
84
+
85
+ private fun bitmap(context: Context, name: String, sizePx: Int, fill: Float): Bitmap? {
86
+ if (sizePx <= 0) return null
87
+
88
+ val requestedFill = if (name.endsWith(".fill")) 1f else fill.coerceIn(0f, 1f)
89
+ val requestedName = name.removeSuffix(".fill")
90
+
91
+ // Keyed by NAME, not codepoint: 217 Symbols codepoints are shared by
92
+ // two or more names, so a codepoint key would collide aliases — and
93
+ // `favorite` and `favorite.fill` are the same codepoint at different
94
+ // fills, which is the collision that would actually be visible.
95
+ val key = CacheKey(requestedName, requestedFill, sizePx)
96
+ bitmaps[key]?.let { return it }
97
+
98
+ val map = codepoints(context) ?: return null
99
+ val codepoint = map[requestedName]
100
+ ?: map[FALLBACK_NAME]
101
+ ?: return null
102
+
103
+ if (map[requestedName] == null) {
104
+ Log.w(TAG, "icon '$requestedName' is not in the bundled icon font; falling back to '$FALLBACK_NAME'")
105
+ }
106
+
107
+ val typeface = typeface(context, requestedFill) ?: return null
108
+ val rendered = render(codepoint, typeface, sizePx)
109
+ bitmaps[key] = rendered
110
+ return rendered
111
+ }
112
+
113
+ private fun render(codepoint: Int, typeface: Typeface, sizePx: Int): Bitmap {
114
+ val paint = Paint(Paint.ANTI_ALIAS_FLAG).apply {
115
+ this.typeface = typeface
116
+ textSize = sizePx.toFloat()
117
+ color = Color.WHITE
118
+ textAlign = Paint.Align.CENTER
119
+ }
120
+
121
+ val bitmap = Bitmap.createBitmap(sizePx, sizePx, Bitmap.Config.ARGB_8888)
122
+ val canvas = Canvas(bitmap)
123
+ val metrics = paint.fontMetrics
124
+ val baseline = sizePx / 2f - (metrics.ascent + metrics.descent) / 2f
125
+
126
+ // Character.toChars, never `codepoint.toChar()`: 105 of the 4,267
127
+ // Material Symbols icons live above U+FFFF (U+FFF03–U+FFFFD) and one
128
+ // classic icon does too. Truncating to 16 bits renders the wrong glyph
129
+ // or a tofu, and does so silently.
130
+ canvas.drawText(String(Character.toChars(codepoint)), sizePx / 2f, baseline, paint)
131
+ return bitmap
132
+ }
133
+
134
+ private fun typeface(context: Context, fill: Float): Typeface? =
135
+ typefaces.getOrPut(fill) {
136
+ val path = fontAsset(context) ?: return@getOrPut null
137
+ runCatching {
138
+ Typeface.Builder(context.assets, path)
139
+ // Ignored by a font with no `fvar` table (the classic set),
140
+ // which is why `icons.android: x.fill` is rejected at build
141
+ // time when `icon_font: classic` — by then it's too late to
142
+ // tell the difference here.
143
+ .setFontVariationSettings("'FILL' $fill, 'wght' 400, 'GRAD' 0, 'opsz' 24")
144
+ .build()
145
+ }.getOrElse {
146
+ Log.w(TAG, "could not build a Typeface from $path", it)
147
+ runCatching { Typeface.createFromAsset(context.assets, path) }.getOrNull()
148
+ }
149
+ }
150
+
151
+ /**
152
+ * `name<space>hexcodepoint`, one per line, no header. The hex field is 4, 5
153
+ * or 6 characters wide depending on the font, so it is parsed rather than
154
+ * sliced.
155
+ */
156
+ private fun codepoints(context: Context): Map<String, Int>? {
157
+ codepoints?.let { return it }
158
+
159
+ val path = codepointsAsset(context) ?: return null
160
+ val parsed = runCatching {
161
+ context.assets.open(path).bufferedReader().useLines { lines ->
162
+ lines.mapNotNull { line ->
163
+ val parts = line.trim().split(' ')
164
+ if (parts.size != 2) return@mapNotNull null
165
+ val cp = parts[1].toIntOrNull(16) ?: return@mapNotNull null
166
+ if (cp <= 0 || cp > 0x10FFFF) return@mapNotNull null
167
+ parts[0] to cp
168
+ }.toMap()
169
+ }
170
+ }.getOrElse {
171
+ Log.e(TAG, "could not read the icon font's codepoints from $path", it)
172
+ null
173
+ }
174
+
175
+ codepoints = parsed
176
+ return parsed
177
+ }
178
+
179
+ /**
180
+ * The `.ttf` and its `.codepoints` are discovered, not named. Which font is
181
+ * present is a build-time decision (`native.android.icon_font`), and the
182
+ * two files must always travel together: 282 names exist in both fonts with
183
+ * *different* codepoints, so a map read from the wrong file renders the
184
+ * wrong icons rather than failing.
185
+ */
186
+ private fun fontAsset(context: Context): String? {
187
+ fontAssetPath?.let { return it }
188
+ val name = list(context).firstOrNull { it.endsWith(".ttf", ignoreCase = true) }
189
+ if (name == null) {
190
+ Log.e(TAG, "no .ttf found in assets/$ASSET_DIR — icons will not render")
191
+ return null
192
+ }
193
+ return "$ASSET_DIR/$name".also { fontAssetPath = it }
194
+ }
195
+
196
+ private fun codepointsAsset(context: Context): String? {
197
+ codepointsAssetPath?.let { return it }
198
+ val font = fontAsset(context)?.substringAfterLast('/')?.substringBeforeLast('.')
199
+ val files = list(context)
200
+ val name = files.firstOrNull { it == "$font.codepoints" }
201
+ ?: files.firstOrNull { it.endsWith(".codepoints", ignoreCase = true) }
202
+ if (name == null) {
203
+ Log.e(TAG, "no .codepoints found in assets/$ASSET_DIR — icons will not render")
204
+ return null
205
+ }
206
+ return "$ASSET_DIR/$name".also { codepointsAssetPath = it }
207
+ }
208
+
209
+ private fun list(context: Context): List<String> =
210
+ runCatching { context.assets.list(ASSET_DIR)?.toList() }.getOrNull() ?: emptyList()
211
+ }