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,201 @@
|
|
|
1
|
+
package com.rubyeverywhere.shell
|
|
2
|
+
|
|
3
|
+
import dev.hotwire.core.config.Hotwire
|
|
4
|
+
import dev.hotwire.navigation.navigator.NavigatorConfiguration
|
|
5
|
+
import dev.hotwire.navigation.tabs.HotwireBottomTab
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Maps the server's `settings.tabs` onto the fixed pool of NavigatorHosts in
|
|
9
|
+
* activity_main.xml.
|
|
10
|
+
*
|
|
11
|
+
* iOS builds its tab bar from the path configuration at runtime, so tabs can be
|
|
12
|
+
* auth-gated server-side and change without a release. Android can't do that
|
|
13
|
+
* directly — a tab needs a real FragmentContainerView id and NavigatorHost
|
|
14
|
+
* resolves its configuration once, in onCreate — so the shell declares five
|
|
15
|
+
* hosts up front and maps whatever the server sends onto them.
|
|
16
|
+
*
|
|
17
|
+
* The list this produces is ALWAYS five long. That is not tidiness, it is a
|
|
18
|
+
* hard requirement of the framework, twice over:
|
|
19
|
+
*
|
|
20
|
+
* * [NavigatorHost] throws `IllegalStateException("No configuration found for
|
|
21
|
+
* NavigatorHost")` in onCreate for any host in the layout that
|
|
22
|
+
* `navigatorConfigurations()` doesn't cover. Three tabs and five hosts
|
|
23
|
+
* would be two crashes at launch.
|
|
24
|
+
* * `HotwireBottomNavigationController.switchTab` only hides hosts present in
|
|
25
|
+
* the *new* tabs list, so a host dropped from a shortened list is never
|
|
26
|
+
* hidden and stays on screen, overlapping the tab that replaced it.
|
|
27
|
+
*
|
|
28
|
+
* "Not a tab right now" is therefore always `isVisible = false`, never a
|
|
29
|
+
* shorter list.
|
|
30
|
+
*/
|
|
31
|
+
object MainTabs {
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* The pool, in layout order. Slot 0 is special:
|
|
35
|
+
* `HotwireActivityDelegate` initialises `currentNavigatorHostId` to the
|
|
36
|
+
* first configuration's host and loads it eagerly regardless of laziness,
|
|
37
|
+
* so slot 0 must always be a real tab (it is — the first resolved tab fills
|
|
38
|
+
* it, and with no tabs at all it holds the app's start URL).
|
|
39
|
+
*/
|
|
40
|
+
val hostIds = listOf(
|
|
41
|
+
R.id.navigator_host_1,
|
|
42
|
+
R.id.navigator_host_2,
|
|
43
|
+
R.id.navigator_host_3,
|
|
44
|
+
R.id.navigator_host_4,
|
|
45
|
+
R.id.navigator_host_5
|
|
46
|
+
)
|
|
47
|
+
|
|
48
|
+
val size = hostIds.size
|
|
49
|
+
|
|
50
|
+
/** The Material icon the More slot wears. Verified present in both fonts. */
|
|
51
|
+
const val MORE_ICON = "more_horiz"
|
|
52
|
+
|
|
53
|
+
/** One tab as the server described it. `icon` is a Material icon name. */
|
|
54
|
+
data class Entry(val title: String, val path: String, val icon: String)
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* One occupied slot in the bar. [entry] is the tab it stands for, or `null`
|
|
58
|
+
* for the synthesised More slot, which stands for all of [Arrangement.overflow]
|
|
59
|
+
* at once and therefore for no single tab.
|
|
60
|
+
*/
|
|
61
|
+
data class Slot(val title: String, val icon: String, val startPath: String, val entry: Entry?)
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* How the resolved tabs were laid out across the five hosts.
|
|
65
|
+
*
|
|
66
|
+
* Android's BottomNavigationView caps at five destinations and, unlike
|
|
67
|
+
* UITabBarController, synthesises nothing past that — a sixth tab simply
|
|
68
|
+
* has no bar item to live in. So the shell hand-builds iOS's answer: with
|
|
69
|
+
* six or more tabs the last slot becomes "More", a container that holds
|
|
70
|
+
* every tab from the fifth on and presents them in a bottom sheet.
|
|
71
|
+
*
|
|
72
|
+
* Five or fewer tabs arrange exactly as they always did. There is no More
|
|
73
|
+
* slot, no sheet, and nothing about the bar changes.
|
|
74
|
+
*/
|
|
75
|
+
data class Arrangement(
|
|
76
|
+
val entries: List<Entry>,
|
|
77
|
+
val slots: List<Slot>,
|
|
78
|
+
val overflow: List<Entry>
|
|
79
|
+
) {
|
|
80
|
+
val hasMore: Boolean get() = overflow.isNotEmpty()
|
|
81
|
+
|
|
82
|
+
/** The More slot's index, or -1 when there is no More slot. */
|
|
83
|
+
val moreIndex: Int get() = if (hasMore) slots.lastIndex else -1
|
|
84
|
+
|
|
85
|
+
fun isMoreIndex(index: Int): Boolean = hasMore && index == slots.lastIndex
|
|
86
|
+
|
|
87
|
+
/** The tab a slot stands for; `null` for More, and for empty slots. */
|
|
88
|
+
fun entryAt(index: Int): Entry? = slots.getOrNull(index)?.entry
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/**
|
|
92
|
+
* `settings.tabs` from the merged path configuration. Parsed defensively:
|
|
93
|
+
* the settings map comes from Gson as plain collections, and the document
|
|
94
|
+
* is fetched from a server that can deploy independently of the app.
|
|
95
|
+
*
|
|
96
|
+
* Deliberately NOT truncated to the host pool. Everything past the fourth
|
|
97
|
+
* tab is still a tab — [arrange] puts it behind More rather than on the
|
|
98
|
+
* floor.
|
|
99
|
+
*/
|
|
100
|
+
fun entries(): List<Entry> {
|
|
101
|
+
val tabs = Hotwire.config.pathConfiguration.settings["tabs"] as? List<*> ?: return emptyList()
|
|
102
|
+
|
|
103
|
+
return tabs.mapNotNull { entry ->
|
|
104
|
+
val map = entry as? Map<*, *> ?: return@mapNotNull null
|
|
105
|
+
val title = map["title"] as? String ?: return@mapNotNull null
|
|
106
|
+
val path = map["path"] as? String ?: return@mapNotNull null
|
|
107
|
+
Entry(title = title, path = path, icon = map["icon"] as? String ?: "circle")
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
/**
|
|
112
|
+
* Lay the resolved tabs out across the pool.
|
|
113
|
+
*
|
|
114
|
+
* Up to five tabs each take a slot of their own. From six, slots 1–4 are the
|
|
115
|
+
* first four tabs and slot 5 becomes More, holding the rest.
|
|
116
|
+
*
|
|
117
|
+
* The More slot's host still needs a start location — every host in the
|
|
118
|
+
* pool does — and it gets the FIRST overflow tab's path. That is load
|
|
119
|
+
* bearing rather than arbitrary: it makes the fifth host's start location
|
|
120
|
+
* identical whether there are exactly five tabs or six, so gaining or
|
|
121
|
+
* losing a More slot never moves a host's start URL and never, by itself,
|
|
122
|
+
* trips the `recreate()` that a path change costs (see [pathsFingerprint]).
|
|
123
|
+
*
|
|
124
|
+
* Nothing fetches it until the user picks a row: More is a menu, not a
|
|
125
|
+
* destination, so tapping it never selects the host (MainActivity's
|
|
126
|
+
* item-selected listener refuses the selection) and `lazyLoadTabs = true`
|
|
127
|
+
* means an unselected host loads nothing.
|
|
128
|
+
*/
|
|
129
|
+
fun arrange(entries: List<Entry>, moreTitle: String): Arrangement {
|
|
130
|
+
if (entries.size <= size) {
|
|
131
|
+
return Arrangement(
|
|
132
|
+
entries = entries,
|
|
133
|
+
slots = entries.map { Slot(it.title, it.icon, it.path, it) },
|
|
134
|
+
overflow = emptyList()
|
|
135
|
+
)
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
val kept = entries.take(size - 1)
|
|
139
|
+
val overflow = entries.drop(size - 1)
|
|
140
|
+
|
|
141
|
+
return Arrangement(
|
|
142
|
+
entries = entries,
|
|
143
|
+
slots = kept.map { Slot(it.title, it.icon, it.path, it) } +
|
|
144
|
+
Slot(moreTitle, MORE_ICON, overflow.first().path, null),
|
|
145
|
+
overflow = overflow
|
|
146
|
+
)
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
/**
|
|
150
|
+
* The five tabs, padded with hidden slots. Hidden slots still need a valid
|
|
151
|
+
* start location — it is what [dev.hotwire.navigation.routing.AppNavigationRouteDecisionHandler]
|
|
152
|
+
* compares hosts against — so they get the app's own start URL. They never
|
|
153
|
+
* fetch it: `lazyLoadTabs = true` means an unselected host loads nothing,
|
|
154
|
+
* and a hidden tab cannot be selected.
|
|
155
|
+
*/
|
|
156
|
+
fun tabs(config: EverywhereConfig, arrangement: Arrangement): List<HotwireBottomTab> =
|
|
157
|
+
hostIds.mapIndexed { index, hostId ->
|
|
158
|
+
val slot = arrangement.slots.getOrNull(index)
|
|
159
|
+
HotwireBottomTab(
|
|
160
|
+
title = slot?.title.orEmpty(),
|
|
161
|
+
// A placeholder, always. `iconResId` is an @DrawableRes Int, so
|
|
162
|
+
// the font-rendered bitmap cannot be passed here; MainActivity
|
|
163
|
+
// overwrites the menu item's icon after every load().
|
|
164
|
+
iconResId = R.drawable.ic_tab_placeholder,
|
|
165
|
+
isVisible = slot != null,
|
|
166
|
+
configuration = NavigatorConfiguration(
|
|
167
|
+
// Stable per slot, not per path: the name keys the tab's
|
|
168
|
+
// Session and SessionViewModel, and a tab that merely
|
|
169
|
+
// changed title must not lose them.
|
|
170
|
+
name = "tab-${index + 1}",
|
|
171
|
+
navigatorHostId = hostId,
|
|
172
|
+
startLocation = slot?.let { config.url(forPath = it.startPath) } ?: config.startUrl
|
|
173
|
+
)
|
|
174
|
+
)
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
/**
|
|
178
|
+
* Identifies a tab set for the cheap-vs-expensive decision.
|
|
179
|
+
*
|
|
180
|
+
* A change in title, icon or visibility is grade one: a second
|
|
181
|
+
* `load()` on the live controller, no fragment teardown, ~96 ms. A change
|
|
182
|
+
* in *paths* is grade two: `NavigatorHost` cached its configuration in
|
|
183
|
+
* onCreate, so nothing short of `Activity.recreate()` re-reads it. Hence
|
|
184
|
+
* two fingerprints — the paths one decides whether the Activity has to go.
|
|
185
|
+
*
|
|
186
|
+
* [fingerprint] covers every entry, overflow included: an overflow tab that
|
|
187
|
+
* changed its title or icon changes nothing in the bar, but the sheet has
|
|
188
|
+
* to be rebuilt from the new entries the next time it opens.
|
|
189
|
+
*/
|
|
190
|
+
fun fingerprint(entries: List<Entry>): String =
|
|
191
|
+
entries.joinToString("\n") { "${it.title}|${it.path}|${it.icon}" }
|
|
192
|
+
|
|
193
|
+
/**
|
|
194
|
+
* The paths the five hosts were actually built against — the slots', not the
|
|
195
|
+
* entries'. An overflow tab's path is deliberately absent: it is not any
|
|
196
|
+
* host's start location, so changing it costs nothing and must not buy a
|
|
197
|
+
* `recreate()`.
|
|
198
|
+
*/
|
|
199
|
+
fun pathsFingerprint(arrangement: Arrangement): String =
|
|
200
|
+
arrangement.slots.joinToString("\n") { it.startPath }
|
|
201
|
+
}
|
data/support/mobile/android/app/src/main/java/com/rubyeverywhere/shell/WebBottomSheetFragment.kt
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
package com.rubyeverywhere.shell
|
|
2
|
+
|
|
3
|
+
import dev.hotwire.navigation.destinations.HotwireDestinationDeepLink
|
|
4
|
+
import dev.hotwire.navigation.fragments.HotwireWebBottomSheetFragment
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* The destination for path rules with `context: modal` and a bottom-sheet
|
|
8
|
+
* presentation. Registered so the path configuration can name it; it needs no
|
|
9
|
+
* behaviour of its own — a sheet is never the first content, so it has no part
|
|
10
|
+
* in dismissing the splash.
|
|
11
|
+
*/
|
|
12
|
+
@HotwireDestinationDeepLink(uri = "hotwire://fragment/web/modal/sheet")
|
|
13
|
+
class WebBottomSheetFragment : HotwireWebBottomSheetFragment()
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
package com.rubyeverywhere.shell
|
|
2
|
+
|
|
3
|
+
import dev.hotwire.core.turbo.errors.VisitError
|
|
4
|
+
import dev.hotwire.navigation.destinations.HotwireDestinationDeepLink
|
|
5
|
+
import dev.hotwire.navigation.fragments.HotwireWebFragment
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* The default destination for every web visit.
|
|
9
|
+
*
|
|
10
|
+
* Deliberately thin — the framework's own fragment already does the work. Its
|
|
11
|
+
* one job is telling [MainActivity] when there is content on screen, which is
|
|
12
|
+
* how the splash comes down. Android has no `NavigatorDelegate.requestDidFinish`
|
|
13
|
+
* the way iOS does; the visit callbacks on the fragment are where that
|
|
14
|
+
* information surfaces.
|
|
15
|
+
*/
|
|
16
|
+
@HotwireDestinationDeepLink(uri = "hotwire://fragment/web")
|
|
17
|
+
open class WebFragment : HotwireWebFragment() {
|
|
18
|
+
|
|
19
|
+
override fun onVisitRequestFinished(location: String) {
|
|
20
|
+
super.onVisitRequestFinished(location)
|
|
21
|
+
settled()
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* An error is still an answer: the framework has swapped in its error view,
|
|
26
|
+
* so leaving the splash up would hide it behind a spinner that never stops.
|
|
27
|
+
*/
|
|
28
|
+
override fun onVisitErrorReceived(location: String, error: VisitError) {
|
|
29
|
+
super.onVisitErrorReceived(location, error)
|
|
30
|
+
settled()
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
private fun settled() {
|
|
34
|
+
(activity as? MainActivity)?.onWebContentSettled()
|
|
35
|
+
}
|
|
36
|
+
}
|