Skip to content

Commit 7585f14

Browse files
committed
refactor: improve browser UI and initialization
- Update Light theme color palette for better contrast - Implement manual WebView directory initialization - Add unit test for navigation logic - Enable configurable toolbar positioning - Add home button to address bar component - Fix emulator-specific high refresh rate issue
1 parent 6786dce commit 7585f14

17 files changed

Lines changed: 2320 additions & 270 deletions

app/src/main/java/com/example/MainActivity.kt

Lines changed: 17 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@ class MainActivity : ComponentActivity() {
3434

3535
override fun onCreate(savedInstanceState: Bundle?) {
3636
super.onCreate(savedInstanceState)
37+
initializeWebViewDirectories()
3738
enableEdgeToEdge()
3839
enableHighRefreshRate()
3940

@@ -66,12 +67,24 @@ class MainActivity : ComponentActivity() {
6667
}
6768
}
6869

70+
private fun initializeWebViewDirectories() {
71+
try {
72+
val cacheDir = applicationContext.cacheDir
73+
val webViewCache = java.io.File(cacheDir, "WebView/Default/HTTP Cache")
74+
val codeCacheJs = java.io.File(webViewCache, "Code Cache/js")
75+
val codeCacheWasm = java.io.File(webViewCache, "Code Cache/wasm")
76+
val indexDir = java.io.File(webViewCache, "index-dir")
77+
codeCacheJs.mkdirs()
78+
codeCacheWasm.mkdirs()
79+
indexDir.mkdirs()
80+
} catch (e: Throwable) {
81+
// Safe fallback
82+
}
83+
}
84+
6985
private fun enableHighRefreshRate() {
86+
if (com.example.browser.DeviceUtils.isEmulator) return
7087
try {
71-
window.setFlags(
72-
WindowManager.LayoutParams.FLAG_HARDWARE_ACCELERATED,
73-
WindowManager.LayoutParams.FLAG_HARDWARE_ACCELERATED
74-
)
7588
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
7689
val currentDisplay = display
7790
val modes = currentDisplay?.supportedModes

app/src/main/java/com/example/browser/BrowserModels.kt

Lines changed: 44 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,27 @@ enum class NewTabStyle(val displayName: String) {
2222
MINIMALIST("Zen Minimal")
2323
}
2424

25+
enum class ToolbarPosition(val displayName: String, val description: String) {
26+
BOTTOM("Bottom (Ergonomic)", "Easy one-handed thumb reachability"),
27+
TOP("Top (Classic)", "Traditional address bar layout at the top")
28+
}
29+
30+
data class ReaderArticle(
31+
val title: String = "",
32+
val byline: String = "",
33+
val domain: String = "",
34+
val contentText: String = "",
35+
val wordCount: Int = 0,
36+
val readingTimeMinutes: Int = 1
37+
)
38+
39+
enum class ReaderTheme(val displayName: String, val bgHex: String, val textHex: String) {
40+
LIGHT("Crisp Light", "#FFFFFF", "#0F172A"),
41+
SEPIA("Warm Paper", "#FBF0D9", "#2C221E"),
42+
NORDIC("Nordic Slate", "#1E293B", "#F8FAFC"),
43+
AMOLED("OLED Black", "#000000", "#E4E4E7")
44+
}
45+
2546
enum class DownloadProvider(val displayName: String, val description: String) {
2647
BUILT_IN("Built-in Manager", "Standard system download manager"),
2748
EXTERNAL_APP("External / Ask Every Time", "Open 1DM, ADM, IDM, or system app chooser")
@@ -51,7 +72,8 @@ enum class ActiveSheet {
5172
SETTINGS,
5273
CLEAR_DATA,
5374
NEW_PROFILE_DIALOG,
54-
EDIT_BOOKMARK_DIALOG
75+
EDIT_BOOKMARK_DIALOG,
76+
READER_MODE
5577
}
5678

5779
enum class ContextMenuType {
@@ -128,3 +150,24 @@ object UrlUtils {
128150
return url.startsWith("https://", ignoreCase = true)
129151
}
130152
}
153+
154+
object DeviceUtils {
155+
val isEmulator: Boolean by lazy {
156+
val fingerprint = android.os.Build.FINGERPRINT.lowercase()
157+
val model = android.os.Build.MODEL.lowercase()
158+
val hardware = android.os.Build.HARDWARE.lowercase()
159+
val brand = android.os.Build.BRAND.lowercase()
160+
val device = android.os.Build.DEVICE.lowercase()
161+
val product = android.os.Build.PRODUCT.lowercase()
162+
163+
fingerprint.startsWith("generic")
164+
|| fingerprint.startsWith("unknown")
165+
|| model.contains("google_sdk")
166+
|| model.contains("emulator")
167+
|| model.contains("android sdk built for")
168+
|| hardware.contains("goldfish")
169+
|| hardware.contains("ranchu")
170+
|| (brand.startsWith("generic") && device.startsWith("generic"))
171+
|| product.contains("sdk")
172+
}
173+
}

app/src/main/java/com/example/browser/BrowserViewModel.kt

Lines changed: 104 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -101,7 +101,7 @@ class BrowserViewModel(application: Application) : AndroidViewModel(application)
101101
val now = System.currentTimeMillis()
102102

103103
// When near page top, always smoothly restore bars
104-
if (scrollY <= 40) {
104+
if (scrollY <= 35) {
105105
accumulatedScrollY = 0
106106
if (!_isBarsVisible.value) {
107107
_isBarsVisible.value = true
@@ -110,29 +110,25 @@ class BrowserViewModel(application: Application) : AndroidViewModel(application)
110110
return
111111
}
112112

113-
// Reduced cooldown (150ms) to ensure swift responsiveness without toggle jitter
114-
if (now - lastVisibilityToggleTime < 150) {
115-
accumulatedScrollY = 0
116-
return
117-
}
118-
119-
// Filter out microscopic finger tremors or resting hand jitter (< 3px)
113+
// Filter out microscopic finger tremors (< 3px)
120114
if (kotlin.math.abs(deltaY) < 3) return
121115

122-
// Direction reversal reset
116+
// Direction reversal: clean reset so sudden scroll direction changes respond immediately
123117
if ((deltaY > 0 && accumulatedScrollY < 0) || (deltaY < 0 && accumulatedScrollY > 0)) {
124118
accumulatedScrollY = 0
125119
}
126120
accumulatedScrollY += deltaY
127121

128122
// Deliberate user scroll threshold to trigger hide or show
129-
if (accumulatedScrollY > 55) {
130-
if (_isBarsVisible.value) {
123+
if (accumulatedScrollY > 60) {
124+
// Scrolling down: hide bars with light debounce to prevent flapping
125+
if (_isBarsVisible.value && now - lastVisibilityToggleTime > 120) {
131126
_isBarsVisible.value = false
132127
lastVisibilityToggleTime = now
133128
}
134129
accumulatedScrollY = 0
135-
} else if (accumulatedScrollY < -30) {
130+
} else if (accumulatedScrollY < -25) {
131+
// Scrolling up: show bars promptly without artificial lag or dropped touch frames
136132
if (!_isBarsVisible.value) {
137133
_isBarsVisible.value = true
138134
lastVisibilityToggleTime = now
@@ -202,6 +198,59 @@ class BrowserViewModel(application: Application) : AndroidViewModel(application)
202198
val downloadProvider = MutableStateFlow(preferences.getDownloadProvider())
203199
val isWeatherOnNewTab = MutableStateFlow(preferences.isWeatherOnNewTab())
204200
val isWeatherFahrenheit = MutableStateFlow(preferences.isWeatherFahrenheit())
201+
val toolbarPosition = MutableStateFlow(preferences.getToolbarPosition())
202+
203+
// Reader Mode State
204+
private val _readerArticle = MutableStateFlow<ReaderArticle?>(null)
205+
val readerArticle: StateFlow<ReaderArticle?> = _readerArticle.asStateFlow()
206+
207+
private val _readerTheme = MutableStateFlow(ReaderTheme.SEPIA)
208+
val readerTheme: StateFlow<ReaderTheme> = _readerTheme.asStateFlow()
209+
210+
private val _readerFontSize = MutableStateFlow(18)
211+
val readerFontSize: StateFlow<Int> = _readerFontSize.asStateFlow()
212+
213+
private val _readerIsSerif = MutableStateFlow(true)
214+
val readerIsSerif: StateFlow<Boolean> = _readerIsSerif.asStateFlow()
215+
216+
fun setToolbarPosition(position: ToolbarPosition) {
217+
toolbarPosition.value = position
218+
preferences.setToolbarPosition(position)
219+
}
220+
221+
fun setReaderTheme(theme: ReaderTheme) { _readerTheme.value = theme }
222+
fun setReaderFontSize(size: Int) { _readerFontSize.value = size.coerceIn(12, 28) }
223+
fun setReaderIsSerif(isSerif: Boolean) { _readerIsSerif.value = isSerif }
224+
fun setReaderArticle(article: ReaderArticle?) { _readerArticle.value = article }
225+
226+
fun openReaderMode() {
227+
val tabId = _activeTabId.value
228+
val currentTab = _activeTabState.value
229+
if (tabId.isBlank() || currentTab == null || currentTab.url.isBlank() || currentTab.url == "about:blank") return
230+
231+
viewModelScope.launch {
232+
_webViewActionEvent.emit(WebViewAction.ExtractReaderContent(
233+
callback = { extracted ->
234+
if (extracted != null && extracted.contentText.isNotBlank()) {
235+
_readerArticle.value = extracted
236+
openSheet(ActiveSheet.READER_MODE)
237+
} else {
238+
// Fallback article if extraction returns sparse text
239+
val fallback = ReaderArticle(
240+
title = currentTab.title,
241+
domain = UrlUtils.extractDomain(currentTab.url),
242+
contentText = "Unable to extract formatted article content for this page. The page may not contain long-form text or may be an interactive web application.",
243+
wordCount = 25,
244+
readingTimeMinutes = 1
245+
)
246+
_readerArticle.value = fallback
247+
openSheet(ActiveSheet.READER_MODE)
248+
}
249+
},
250+
targetTabId = tabId
251+
))
252+
}
253+
}
205254

206255
// Weather Repository & UI State Flow
207256
val weatherRepository = WeatherRepository(application)
@@ -452,6 +501,22 @@ class BrowserViewModel(application: Application) : AndroidViewModel(application)
452501
dismissSheet()
453502
}
454503

504+
fun switchToNextProfile() {
505+
val list = profiles.value
506+
if (list.size <= 1) return
507+
val currentIndex = list.indexOfFirst { it.id == _currentProfileId.value }
508+
val nextIndex = if (currentIndex >= 0) (currentIndex + 1) % list.size else 0
509+
switchProfile(list[nextIndex].id)
510+
}
511+
512+
fun switchToPrevProfile() {
513+
val list = profiles.value
514+
if (list.size <= 1) return
515+
val currentIndex = list.indexOfFirst { it.id == _currentProfileId.value }
516+
val prevIndex = if (currentIndex > 0) currentIndex - 1 else list.size - 1
517+
switchProfile(list[prevIndex].id)
518+
}
519+
455520
fun togglePrivateMode() {
456521
val newPrivate = !_isPrivateMode.value
457522
_isPrivateMode.value = newPrivate
@@ -690,19 +755,41 @@ class BrowserViewModel(application: Application) : AndroidViewModel(application)
690755
setBarsVisible(true)
691756
_isFindInPageActive.value = true
692757
_findQuery.value = ""
758+
_activeTabState.update {
759+
it?.copy(
760+
searchMatchCurrent = 0,
761+
searchMatchCount = 0
762+
)
763+
}
693764
}
694765

695766
fun closeFindInPage() {
696767
val tabId = _activeTabId.value
697768
_isFindInPageActive.value = false
698769
_findQuery.value = ""
770+
_activeTabState.update {
771+
it?.copy(
772+
searchMatchCurrent = 0,
773+
searchMatchCount = 0
774+
)
775+
}
699776
viewModelScope.launch { _webViewActionEvent.emit(WebViewAction.ClearFindMatches(targetTabId = tabId)) }
700777
}
701778

702779
fun setFindQuery(query: String) {
703780
val tabId = _activeTabId.value
704781
_findQuery.value = query
705-
viewModelScope.launch { _webViewActionEvent.emit(WebViewAction.FindAllAsync(query, targetTabId = tabId)) }
782+
if (query.isBlank()) {
783+
_activeTabState.update {
784+
it?.copy(
785+
searchMatchCurrent = 0,
786+
searchMatchCount = 0
787+
)
788+
}
789+
viewModelScope.launch { _webViewActionEvent.emit(WebViewAction.ClearFindMatches(targetTabId = tabId)) }
790+
} else {
791+
viewModelScope.launch { _webViewActionEvent.emit(WebViewAction.FindAllAsync(query, targetTabId = tabId)) }
792+
}
706793
}
707794

708795
fun findNext(forward: Boolean) {
@@ -1034,4 +1121,8 @@ sealed class WebViewAction {
10341121
data class FindAllAsync(val query: String, override val targetTabId: String? = null) : WebViewAction()
10351122
data class FindNext(val forward: Boolean, override val targetTabId: String? = null) : WebViewAction()
10361123
data class ClearFindMatches(override val targetTabId: String? = null) : WebViewAction()
1124+
data class ExtractReaderContent(
1125+
val callback: (ReaderArticle?) -> Unit,
1126+
override val targetTabId: String? = null
1127+
) : WebViewAction()
10371128
}

app/src/main/java/com/example/data/BrowserPreferences.kt

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,20 @@ class BrowserPreferences(context: Context) {
2525
private const val KEY_DOWNLOAD_PROVIDER = "pref_download_provider"
2626
private const val KEY_WEATHER_ON_NEW_TAB = "pref_weather_on_new_tab"
2727
private const val KEY_WEATHER_FAHRENHEIT = "pref_weather_fahrenheit"
28+
private const val KEY_TOOLBAR_POSITION = "pref_toolbar_position"
29+
}
30+
31+
fun getToolbarPosition(): com.example.browser.ToolbarPosition {
32+
val name = prefs.getString(KEY_TOOLBAR_POSITION, com.example.browser.ToolbarPosition.BOTTOM.name)
33+
return try {
34+
com.example.browser.ToolbarPosition.valueOf(name ?: com.example.browser.ToolbarPosition.BOTTOM.name)
35+
} catch (e: Exception) {
36+
com.example.browser.ToolbarPosition.BOTTOM
37+
}
38+
}
39+
40+
fun setToolbarPosition(pos: com.example.browser.ToolbarPosition) {
41+
prefs.edit().putString(KEY_TOOLBAR_POSITION, pos.name).apply()
2842
}
2943

3044
fun isWeatherOnNewTab(): Boolean {

0 commit comments

Comments
 (0)