Skip to content

Commit 4df8a39

Browse files
committed
feat: implement browser context menu and improve scrolling
- Add long-press context menu for links and images - Improve auto-hiding navigation bar logic to prevent jitter - Refactor application ID and CI build configuration
1 parent 9aa7bba commit 4df8a39

7 files changed

Lines changed: 488 additions & 7 deletions

File tree

.github/workflows/release.yml

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -58,7 +58,11 @@ jobs:
5858
with:
5959
java-version: '21'
6060
distribution: 'microsoft'
61-
cache: gradle
61+
62+
- name: Setup Gradle Build Cache
63+
uses: gradle/actions/setup-gradle@v4
64+
with:
65+
cache-read-only: false
6266

6367
- name: Setup Android Release Keystore
6468
run: |
@@ -77,7 +81,7 @@ jobs:
7781
run: chmod +x gradlew
7882

7983
- name: Build Release APK
80-
run: ./gradlew assembleRelease --no-daemon -Dkotlin.compiler.execution.strategy=daemon --stacktrace
84+
run: ./gradlew assembleRelease --build-cache --parallel -Dorg.gradle.jvmargs="-Xmx4096m -XX:+UseParallelGC" --stacktrace
8185

8286
- name: Prepare Release Assets
8387
id: prepare_asset

app/build.gradle.kts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ android {
1313
val envVersionName = System.getenv("APP_VERSION_NAME") ?: "1.0.108"
1414

1515
defaultConfig {
16-
applicationId = "com.aistudio.browser.lwbxbz"
16+
applicationId = "apps.feather.browser"
1717
minSdk = 24
1818
targetSdk = 36
1919
versionCode = envVersionCode

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

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,19 @@ enum class ActiveSheet {
5454
EDIT_BOOKMARK_DIALOG
5555
}
5656

57+
enum class ContextMenuType {
58+
LINK,
59+
IMAGE,
60+
IMAGE_LINK
61+
}
62+
63+
data class ContextMenuData(
64+
val url: String? = null,
65+
val title: String? = null,
66+
val imageUrl: String? = null,
67+
val type: ContextMenuType = ContextMenuType.LINK
68+
)
69+
5770
data class ActiveTabState(
5871
val id: String,
5972
val profileId: String,

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

Lines changed: 60 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -86,42 +86,99 @@ class BrowserViewModel(application: Application) : AndroidViewModel(application)
8686
private val _isBarsVisible = MutableStateFlow(true)
8787
val isBarsVisible: StateFlow<Boolean> = _isBarsVisible.asStateFlow()
8888
private var accumulatedScrollY = 0
89+
private var lastVisibilityToggleTime = 0L
8990

9091
fun setBarsVisible(visible: Boolean) {
9192
if (_isBarsVisible.value != visible) {
9293
_isBarsVisible.value = visible
94+
lastVisibilityToggleTime = System.currentTimeMillis()
9395
}
9496
accumulatedScrollY = 0
9597
}
9698

9799
fun onWebScroll(deltaY: Int, scrollY: Int) {
98-
if (scrollY <= 24) {
100+
val now = System.currentTimeMillis()
101+
102+
// When near page top, always smoothly restore bars
103+
if (scrollY <= 32) {
99104
accumulatedScrollY = 0
100105
if (!_isBarsVisible.value) {
101106
_isBarsVisible.value = true
107+
lastVisibilityToggleTime = now
102108
}
103109
return
104110
}
105111

112+
// Enforce cooldown so bars don't oscillate during or right after animations
113+
if (now - lastVisibilityToggleTime < 400) {
114+
accumulatedScrollY = 0
115+
return
116+
}
117+
118+
// Filter out microscopic finger tremors or resting hand jitter (< 3px)
119+
if (kotlin.math.abs(deltaY) < 3) return
120+
106121
// Direction reversal reset
107122
if ((deltaY > 0 && accumulatedScrollY < 0) || (deltaY < 0 && accumulatedScrollY > 0)) {
108123
accumulatedScrollY = 0
109124
}
110125
accumulatedScrollY += deltaY
111126

112-
if (accumulatedScrollY > 28) {
127+
// Deliberate user scroll threshold to trigger hide or show
128+
if (accumulatedScrollY > 70) {
113129
if (_isBarsVisible.value) {
114130
_isBarsVisible.value = false
131+
lastVisibilityToggleTime = now
115132
}
116133
accumulatedScrollY = 0
117-
} else if (accumulatedScrollY < -24) {
134+
} else if (accumulatedScrollY < -60) {
118135
if (!_isBarsVisible.value) {
119136
_isBarsVisible.value = true
137+
lastVisibilityToggleTime = now
120138
}
121139
accumulatedScrollY = 0
122140
}
123141
}
124142

143+
// Link / Image Long-Press Context Menu
144+
private val _contextMenuData = MutableStateFlow<ContextMenuData?>(null)
145+
val contextMenuData: StateFlow<ContextMenuData?> = _contextMenuData.asStateFlow()
146+
147+
fun showContextMenu(data: ContextMenuData) {
148+
_contextMenuData.value = data
149+
}
150+
151+
fun dismissContextMenu() {
152+
_contextMenuData.value = null
153+
}
154+
155+
fun openLinkInNewTab(
156+
url: String,
157+
openInBackground: Boolean = false,
158+
isPrivate: Boolean = _isPrivateMode.value
159+
) {
160+
viewModelScope.launch {
161+
val tabId = UUID.randomUUID().toString()
162+
val targetProfileId = if (isPrivate) "private_session" else _currentProfileId.value
163+
val newTab = BrowserTab(
164+
id = tabId,
165+
profileId = targetProfileId,
166+
url = url,
167+
title = url,
168+
isPrivate = isPrivate
169+
)
170+
repository.saveTab(newTab)
171+
if (!openInBackground) {
172+
if (isPrivate && !_isPrivateMode.value) {
173+
_isPrivateMode.value = true
174+
} else if (!isPrivate && _isPrivateMode.value) {
175+
_isPrivateMode.value = false
176+
}
177+
selectTab(tabId, autoDismiss = true)
178+
}
179+
}
180+
}
181+
125182
// Find in Page state
126183
private val _isFindInPageActive = MutableStateFlow(false)
127184
val isFindInPageActive: StateFlow<Boolean> = _isFindInPageActive.asStateFlow()

app/src/main/java/com/example/ui/BrowserScreen.kt

Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,7 @@ fun BrowserScreen(
4040
val isFindInPageActive by viewModel.isFindInPageActive.collectAsStateWithLifecycle()
4141
val findQuery by viewModel.findQuery.collectAsStateWithLifecycle()
4242
val isBarsVisible by viewModel.isBarsVisible.collectAsStateWithLifecycle()
43+
val contextMenuData by viewModel.contextMenuData.collectAsStateWithLifecycle()
4344
var isAddressBarEditing by remember { mutableStateOf(false) }
4445

4546
// Settings
@@ -427,6 +428,65 @@ fun BrowserScreen(
427428
}
428429
}
429430

431+
// Context Menu for Link / Image Long-Press
432+
contextMenuData?.let { menuData ->
433+
ContextMenuSheet(
434+
data = menuData,
435+
onOpenInNewTab = { url ->
436+
viewModel.openLinkInNewTab(url = url, openInBackground = false)
437+
},
438+
onOpenInBackground = { url ->
439+
viewModel.openLinkInNewTab(url = url, openInBackground = true)
440+
android.widget.Toast.makeText(context, "Opened in background tab", android.widget.Toast.LENGTH_SHORT).show()
441+
},
442+
onOpenInPrivateTab = { url ->
443+
viewModel.openLinkInNewTab(url = url, openInBackground = false, isPrivate = true)
444+
},
445+
onCopyLinkAddress = { url ->
446+
val clipboard = context.getSystemService(android.content.Context.CLIPBOARD_SERVICE) as android.content.ClipboardManager
447+
clipboard.setPrimaryClip(android.content.ClipData.newPlainText("URL", url))
448+
android.widget.Toast.makeText(context, "Link address copied", android.widget.Toast.LENGTH_SHORT).show()
449+
},
450+
onCopyLinkText = { text ->
451+
val clipboard = context.getSystemService(android.content.Context.CLIPBOARD_SERVICE) as android.content.ClipboardManager
452+
clipboard.setPrimaryClip(android.content.ClipData.newPlainText("Link Text", text))
453+
android.widget.Toast.makeText(context, "Link text copied", android.widget.Toast.LENGTH_SHORT).show()
454+
},
455+
onShareLink = { url ->
456+
val shareIntent = android.content.Intent(android.content.Intent.ACTION_SEND).apply {
457+
type = "text/plain"
458+
putExtra(android.content.Intent.EXTRA_TEXT, url)
459+
}
460+
context.startActivity(android.content.Intent.createChooser(shareIntent, "Share link"))
461+
},
462+
onOpenImageInNewTab = { imageUrl ->
463+
viewModel.openLinkInNewTab(url = imageUrl, openInBackground = false)
464+
},
465+
onDownloadImage = { imageUrl ->
466+
viewModel.handleDownloadRequest(
467+
url = imageUrl,
468+
userAgent = "",
469+
contentDisposition = "",
470+
mimetype = "image/*",
471+
contentLength = 0L
472+
)
473+
},
474+
onCopyImageAddress = { imageUrl ->
475+
val clipboard = context.getSystemService(android.content.Context.CLIPBOARD_SERVICE) as android.content.ClipboardManager
476+
clipboard.setPrimaryClip(android.content.ClipData.newPlainText("Image URL", imageUrl))
477+
android.widget.Toast.makeText(context, "Image address copied", android.widget.Toast.LENGTH_SHORT).show()
478+
},
479+
onShareImage = { imageUrl ->
480+
val shareIntent = android.content.Intent(android.content.Intent.ACTION_SEND).apply {
481+
type = "text/plain"
482+
putExtra(android.content.Intent.EXTRA_TEXT, imageUrl)
483+
}
484+
context.startActivity(android.content.Intent.createChooser(shareIntent, "Share image"))
485+
},
486+
onDismiss = { viewModel.dismissContextMenu() }
487+
)
488+
}
489+
430490
// Overlays / Sheets
431491
if (showMenuSheet) {
432492
BrowserMenuSheet(

0 commit comments

Comments
 (0)