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
|
@@ -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
|
+
}
|