Skip to content

Commit 6786dce

Browse files
committed
feat: add bookmark import support and perf fixes
- Implement bookmark bulk import functionality - Force hardware acceleration in manifest and activity - Refine high refresh rate logic and transition timings
1 parent 4df8a39 commit 6786dce

12 files changed

Lines changed: 351 additions & 46 deletions

File tree

CONTRIBUTING.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,7 @@ By participating in this project, you agree to abide by our [Code of Conduct](CO
2626
## How Can I Contribute?
2727

2828
### Reporting Bugs
29-
- Search existing [GitHub Issues](https://github.com/aanneion/lightweight_browser/issues) to verify if the bug has already been reported.
29+
- Search existing [GitHub Issues](https://github.com/aanneion/feather_browser/issues) to verify if the bug has already been reported.
3030
- If not, open a new issue using the **Bug Report** template.
3131
- Include device specifications (Android version, device model), steps to reproduce, and screenshots or logcat traces if applicable.
3232

README.md

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -6,9 +6,9 @@
66
<strong>An ultra-lightweight (~1.5 MB), high-performance, privacy-first Android browser featuring true isolated multi-profiles with custom device fingerprint spoofing.</strong>
77
</p>
88

9-
[![Releases](https://img.shields.io/badge/Releases-Latest%20APKs-2563EB?style=for-the-badge&logo=github)](https://github.com/aanneion/lightweight_browser/releases)
10-
[![Build Status](https://img.shields.io/github/actions/workflow/status/aanneion/lightweight_browser/release.yml?branch=main&style=for-the-badge&logo=githubactions&logoColor=white)](https://github.com/aanneion/lightweight_browser/actions)
11-
[![APK Size](https://img.shields.io/badge/APK_Size-~1.5_MB-10B981?style=for-the-badge&logo=android&logoColor=white)](https://github.com/aanneion/lightweight_browser/releases)
9+
[![Releases](https://img.shields.io/badge/Releases-Latest%20APKs-2563EB?style=for-the-badge&logo=github)](https://github.com/aanneion/feather_browser/releases)
10+
[![Build Status](https://img.shields.io/github/actions/workflow/status/aanneion/feather_browser/release.yml?branch=main&style=for-the-badge&logo=githubactions&logoColor=white)](https://github.com/aanneion/feather_browser/actions)
11+
[![APK Size](https://img.shields.io/badge/APK_Size-~1.5_MB-10B981?style=for-the-badge&logo=android&logoColor=white)](https://github.com/aanneion/feather_browser/releases)
1212
[![Platform](https://img.shields.io/badge/Platform-Android_8.0+_(API_24+)-3DDC84?style=for-the-badge&logo=android&logoColor=white)](https://www.android.com/)
1313
[![Kotlin](https://img.shields.io/badge/Kotlin-2.2-7F52FF?style=for-the-badge&logo=kotlin&logoColor=white)](https://kotlinlang.org/)
1414
[![Jetpack Compose](https://img.shields.io/badge/UI-Jetpack_Compose_M3-4285F4?style=for-the-badge&logo=jetpackcompose&logoColor=white)](https://developer.android.com/jetpack/compose)
@@ -129,8 +129,8 @@ graph TD
129129
### Clone & Build
130130
```bash
131131
# 1. Clone the repository
132-
git clone https://github.com/aanneion/lightweight_browser.git
133-
cd lightweight_browser
132+
git clone https://github.com/aanneion/feather_browser.git
133+
cd feather_browser
134134

135135
# 2. Run Tests
136136
./gradlew test

SECURITY.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ Security and privacy are the highest priorities for Feather Browser.
1616
If you discover a security vulnerability or privacy leak within this repository, please do **NOT** open a public issue.
1717

1818
Instead, please report it responsibly by:
19-
1. Opening a **Draft Security Advisory** on GitHub via [Security Advisories](https://github.com/aanneion/lightweight_browser/security/advisories/new) (preferred).
19+
1. Opening a **Draft Security Advisory** on GitHub via [Security Advisories](https://github.com/aanneion/feather_browser/security/advisories/new) (preferred).
2020
2. Or contacting the repository maintainer directly through GitHub.
2121

2222
### What to include in your report

app/src/main/AndroidManifest.xml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010

1111
<application
1212
android:allowBackup="true"
13+
android:hardwareAccelerated="true"
1314
android:usesCleartextTraffic="true"
1415
android:dataExtractionRules="@xml/data_extraction_rules"
1516
android:fullBackupContent="@xml/backup_rules"

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

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import android.content.Intent
55
import android.content.pm.PackageManager
66
import android.os.Build
77
import android.os.Bundle
8+
import android.view.WindowManager
89
import androidx.activity.ComponentActivity
910
import androidx.activity.compose.setContent
1011
import androidx.activity.enableEdgeToEdge
@@ -67,6 +68,10 @@ class MainActivity : ComponentActivity() {
6768

6869
private fun enableHighRefreshRate() {
6970
try {
71+
window.setFlags(
72+
WindowManager.LayoutParams.FLAG_HARDWARE_ACCELERATED,
73+
WindowManager.LayoutParams.FLAG_HARDWARE_ACCELERATED
74+
)
7075
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
7176
val currentDisplay = display
7277
val modes = currentDisplay?.supportedModes
@@ -85,6 +90,7 @@ class MainActivity : ComponentActivity() {
8590
if (maxRefreshMode != null && maxRefreshMode.refreshRate > 60f) {
8691
val params = window.attributes
8792
params.preferredDisplayModeId = maxRefreshMode.modeId
93+
params.preferredRefreshRate = maxRefreshMode.refreshRate
8894
window.attributes = params
8995
}
9096
}

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

Lines changed: 18 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ import androidx.lifecycle.AndroidViewModel
1313
import androidx.lifecycle.viewModelScope
1414
import com.example.data.AppDatabase
1515
import com.example.data.BrowserPreferences
16+
import com.example.data.BookmarkExportHelper
1617
import com.example.data.BrowserRepository
1718
import com.example.data.model.*
1819
import com.example.privacy.ContentBlocker
@@ -100,7 +101,7 @@ class BrowserViewModel(application: Application) : AndroidViewModel(application)
100101
val now = System.currentTimeMillis()
101102

102103
// When near page top, always smoothly restore bars
103-
if (scrollY <= 32) {
104+
if (scrollY <= 40) {
104105
accumulatedScrollY = 0
105106
if (!_isBarsVisible.value) {
106107
_isBarsVisible.value = true
@@ -109,8 +110,8 @@ class BrowserViewModel(application: Application) : AndroidViewModel(application)
109110
return
110111
}
111112

112-
// Enforce cooldown so bars don't oscillate during or right after animations
113-
if (now - lastVisibilityToggleTime < 400) {
113+
// Reduced cooldown (150ms) to ensure swift responsiveness without toggle jitter
114+
if (now - lastVisibilityToggleTime < 150) {
114115
accumulatedScrollY = 0
115116
return
116117
}
@@ -125,13 +126,13 @@ class BrowserViewModel(application: Application) : AndroidViewModel(application)
125126
accumulatedScrollY += deltaY
126127

127128
// Deliberate user scroll threshold to trigger hide or show
128-
if (accumulatedScrollY > 70) {
129+
if (accumulatedScrollY > 55) {
129130
if (_isBarsVisible.value) {
130131
_isBarsVisible.value = false
131132
lastVisibilityToggleTime = now
132133
}
133134
accumulatedScrollY = 0
134-
} else if (accumulatedScrollY < -60) {
135+
} else if (accumulatedScrollY < -30) {
135136
if (!_isBarsVisible.value) {
136137
_isBarsVisible.value = true
137138
lastVisibilityToggleTime = now
@@ -879,6 +880,18 @@ class BrowserViewModel(application: Application) : AndroidViewModel(application)
879880
}
880881
}
881882

883+
fun exportBookmarksHtml(profileName: String = "Feather"): String {
884+
return BookmarkExportHelper.exportToNetscapeHtml(bookmarks.value, profileName)
885+
}
886+
887+
fun importBookmarks(rawContent: String, targetProfileId: String = _currentProfileId.value, onComplete: (Int) -> Unit) {
888+
viewModelScope.launch {
889+
val parsed = BookmarkExportHelper.parseBookmarks(rawContent, targetProfileId)
890+
val count = repository.importBookmarks(parsed)
891+
onComplete(count)
892+
}
893+
}
894+
882895
fun deleteHistoryItem(id: Long) {
883896
viewModelScope.launch {
884897
repository.deleteHistoryItem(id)
Lines changed: 140 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,140 @@
1+
package com.example.data
2+
3+
import com.example.data.model.Bookmark
4+
import org.json.JSONArray
5+
import org.json.JSONObject
6+
import java.util.regex.Pattern
7+
8+
object BookmarkExportHelper {
9+
10+
fun exportToNetscapeHtml(bookmarks: List<Bookmark>, profileName: String = "Feather"): String {
11+
val sb = StringBuilder()
12+
val nowSec = System.currentTimeMillis() / 1000
13+
14+
sb.append("<!DOCTYPE NETSCAPE-Bookmark-file-1>\n")
15+
sb.append("<!-- This is an automatically generated file. It will be read and overwritten. Do Not Edit! -->\n")
16+
sb.append("<META HTTP-EQUIV=\"Content-Type\" CONTENT=\"text/html; charset=UTF-8\">\n")
17+
sb.append("<TITLE>Bookmarks</TITLE>\n")
18+
sb.append("<H1>Bookmarks</H1>\n")
19+
sb.append("<DL><p>\n")
20+
sb.append(" <DT><H3 ADD_DATE=\"$nowSec\" LAST_MODIFIED=\"$nowSec\">")
21+
sb.append(escapeHtml(profileName)).append(" Bookmarks</H3>\n")
22+
sb.append(" <DL><p>\n")
23+
24+
for (bm in bookmarks) {
25+
val addDate = if (bm.createdAt > 0) bm.createdAt / 1000 else nowSec
26+
val safeUrl = escapeHtml(bm.url)
27+
val safeTitle = escapeHtml(if (bm.title.isNotBlank()) bm.title else bm.url)
28+
sb.append(" <DT><A HREF=\"").append(safeUrl)
29+
.append("\" ADD_DATE=\"").append(addDate)
30+
.append("\">").append(safeTitle).append("</A>\n")
31+
}
32+
33+
sb.append(" </DL><p>\n")
34+
sb.append("</DL><p>\n")
35+
return sb.toString()
36+
}
37+
38+
fun parseBookmarks(rawContent: String, targetProfileId: String): List<Bookmark> {
39+
val trimmed = rawContent.trim()
40+
if (trimmed.isEmpty()) return emptyList()
41+
42+
// 1. Try parsing JSON if content starts with [ or {
43+
if (trimmed.startsWith("[") || trimmed.startsWith("{")) {
44+
val jsonResults = parseJsonBookmarks(trimmed, targetProfileId)
45+
if (jsonResults.isNotEmpty()) return jsonResults
46+
}
47+
48+
// 2. Parse Netscape HTML / Standard Anchor tags
49+
return parseHtmlBookmarks(trimmed, targetProfileId)
50+
}
51+
52+
private fun parseHtmlBookmarks(html: String, targetProfileId: String): List<Bookmark> {
53+
val results = mutableListOf<Bookmark>()
54+
val pattern = Pattern.compile(
55+
"<a\\s+[^>]*?href=[\"']([^\"']+)[\"'][^>]*>(.*?)<\\/a>",
56+
Pattern.CASE_INSENSITIVE or Pattern.DOTALL
57+
)
58+
val matcher = pattern.matcher(html)
59+
60+
val seenUrls = mutableSetOf<String>()
61+
val now = System.currentTimeMillis()
62+
63+
while (matcher.find()) {
64+
val rawUrl = matcher.group(1)?.trim().orEmpty()
65+
val rawTitle = matcher.group(2)?.trim().orEmpty()
66+
67+
if (rawUrl.isBlank() || rawUrl.startsWith("javascript:", ignoreCase = true)) {
68+
continue
69+
}
70+
71+
val unescapedUrl = unescapeHtml(rawUrl)
72+
val unescapedTitle = unescapeHtml(rawTitle.replace(Regex("<[^>]*>"), "")).ifBlank { unescapedUrl }
73+
74+
if (seenUrls.add(unescapedUrl)) {
75+
results.add(
76+
Bookmark(
77+
profileId = targetProfileId,
78+
title = unescapedTitle,
79+
url = unescapedUrl,
80+
createdAt = now
81+
)
82+
)
83+
}
84+
}
85+
return results
86+
}
87+
88+
private fun parseJsonBookmarks(jsonStr: String, targetProfileId: String): List<Bookmark> {
89+
val results = mutableListOf<Bookmark>()
90+
val seenUrls = mutableSetOf<String>()
91+
val now = System.currentTimeMillis()
92+
93+
try {
94+
val array = if (jsonStr.startsWith("[")) {
95+
JSONArray(jsonStr)
96+
} else {
97+
val obj = JSONObject(jsonStr)
98+
obj.optJSONArray("bookmarks") ?: obj.optJSONArray("items") ?: JSONArray()
99+
}
100+
101+
for (i in 0 until array.length()) {
102+
val item = array.optJSONObject(i) ?: continue
103+
val url = item.optString("url", "").trim()
104+
val title = item.optString("title", "").ifBlank { url }
105+
if (url.isNotBlank() && !url.startsWith("javascript:", ignoreCase = true) && seenUrls.add(url)) {
106+
results.add(
107+
Bookmark(
108+
profileId = targetProfileId,
109+
title = title,
110+
url = url,
111+
createdAt = now
112+
)
113+
)
114+
}
115+
}
116+
} catch (e: Exception) {
117+
// Silently fallback if JSON malformed
118+
}
119+
return results
120+
}
121+
122+
private fun escapeHtml(text: String): String {
123+
return text
124+
.replace("&", "&amp;")
125+
.replace("<", "&lt;")
126+
.replace(">", "&gt;")
127+
.replace("\"", "&quot;")
128+
.replace("'", "&#39;")
129+
}
130+
131+
private fun unescapeHtml(text: String): String {
132+
return text
133+
.replace("&quot;", "\"")
134+
.replace("&#39;", "'")
135+
.replace("&apos;", "'")
136+
.replace("&lt;", "<")
137+
.replace("&gt;", ">")
138+
.replace("&amp;", "&")
139+
}
140+
}

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

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -196,6 +196,12 @@ class BrowserRepository(private val database: AppDatabase) {
196196
database.bookmarkDao().deleteBookmark(bookmark)
197197
}
198198

199+
suspend fun importBookmarks(bookmarks: List<Bookmark>): Int = withContext(Dispatchers.IO) {
200+
if (bookmarks.isEmpty()) return@withContext 0
201+
database.bookmarkDao().insertBookmarks(bookmarks)
202+
bookmarks.size
203+
}
204+
199205
suspend fun isBookmarked(profileId: String, url: String): Boolean = withContext(Dispatchers.IO) {
200206
database.bookmarkDao().getBookmarkByUrl(profileId, url) != null
201207
}

app/src/main/java/com/example/data/dao/Daos.kt

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -63,6 +63,9 @@ interface BookmarkDao {
6363
@Insert(onConflict = OnConflictStrategy.REPLACE)
6464
suspend fun insertBookmark(bookmark: Bookmark)
6565

66+
@Insert(onConflict = OnConflictStrategy.REPLACE)
67+
suspend fun insertBookmarks(bookmarks: List<Bookmark>)
68+
6669
@Update
6770
suspend fun updateBookmark(bookmark: Bookmark)
6871

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

Lines changed: 13 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
package com.example.ui
22

3+
import android.widget.Toast
34
import androidx.activity.compose.BackHandler
45
import androidx.compose.animation.*
56
import androidx.compose.animation.core.FastOutSlowInEasing
@@ -101,15 +102,11 @@ fun BrowserScreen(
101102
visible = isBarsVisible || isHome || isFindInPageActive || isAddressBarEditing,
102103
enter = slideInVertically(
103104
initialOffsetY = { -it },
104-
animationSpec = tween(durationMillis = 200, easing = FastOutSlowInEasing)
105-
) + expandVertically(
106-
animationSpec = tween(durationMillis = 200, easing = FastOutSlowInEasing)
105+
animationSpec = tween(durationMillis = 180, easing = FastOutSlowInEasing)
107106
),
108107
exit = slideOutVertically(
109108
targetOffsetY = { -it },
110-
animationSpec = tween(durationMillis = 200, easing = FastOutSlowInEasing)
111-
) + shrinkVertically(
112-
animationSpec = tween(durationMillis = 200, easing = FastOutSlowInEasing)
109+
animationSpec = tween(durationMillis = 180, easing = FastOutSlowInEasing)
113110
)
114111
) {
115112
Surface(
@@ -163,15 +160,11 @@ fun BrowserScreen(
163160
visible = isBarsVisible || isHome || isFindInPageActive || isAddressBarEditing,
164161
enter = slideInVertically(
165162
initialOffsetY = { it },
166-
animationSpec = tween(durationMillis = 200, easing = FastOutSlowInEasing)
167-
) + expandVertically(
168-
animationSpec = tween(durationMillis = 200, easing = FastOutSlowInEasing)
163+
animationSpec = tween(durationMillis = 180, easing = FastOutSlowInEasing)
169164
),
170165
exit = slideOutVertically(
171166
targetOffsetY = { it },
172-
animationSpec = tween(durationMillis = 200, easing = FastOutSlowInEasing)
173-
) + shrinkVertically(
174-
animationSpec = tween(durationMillis = 200, easing = FastOutSlowInEasing)
167+
animationSpec = tween(durationMillis = 180, easing = FastOutSlowInEasing)
175168
)
176169
) {
177170
BrowserBottomBar(
@@ -361,6 +354,14 @@ fun BrowserScreen(
361354
viewModel.dismissSheet()
362355
},
363356
onDeleteBookmark = { viewModel.deleteBookmark(it) },
357+
onExportBookmarks = {
358+
viewModel.exportBookmarksHtml(currentProfile?.displayName ?: "Feather")
359+
},
360+
onImportBookmarks = { content ->
361+
viewModel.importBookmarks(content) { count ->
362+
Toast.makeText(context, "Imported $count bookmarks successfully", Toast.LENGTH_SHORT).show()
363+
}
364+
},
364365
onDismiss = { viewModel.dismissSheet() },
365366
modifier = Modifier.fillMaxSize()
366367
)

0 commit comments

Comments
 (0)