Skip to content

Commit cd0464a

Browse files
committed
refactor: update UI theme and browser navigation
Adjust color palette, refine address bar dimensions, and clean up toolbar elevation to modernize the browser interface. Added support for private mode indicators in the bottom bar and prepared state management for upcoming weather features.
1 parent e34186d commit cd0464a

18 files changed

Lines changed: 1252 additions & 195 deletions

.github/workflows/release.yml

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -62,7 +62,10 @@ jobs:
6262

6363
- name: Setup Android Release Keystore
6464
run: |
65-
if [ ! -f release.keystore ]; then
65+
if [ -n "${{ secrets.RELEASE_KEYSTORE_BASE64 }}" ]; then
66+
echo "Decoding permanent release.keystore from GitHub Secret..."
67+
echo "${{ secrets.RELEASE_KEYSTORE_BASE64 }}" | base64 -d > release.keystore
68+
elif [ ! -f release.keystore ]; then
6669
echo "Generating consistent release.keystore for signing..."
6770
keytool -genkeypair -v -keystore release.keystore -alias feather_release_key -keyalg RSA -keysize 2048 -validity 10000 -storepass feather123 -keypass feather123 -dname "CN=Feather Browser, OU=Mobile Applications, O=Feather Privacy Browser, L=San Francisco, ST=California, C=US"
6871
fi
@@ -84,10 +87,8 @@ jobs:
8487
mkdir -p release-assets
8588
APK_NAME="Feather-Browser-${TAG}.apk"
8689
cp app/build/outputs/apk/release/app-release.apk "release-assets/${APK_NAME}"
87-
cp app/build/outputs/apk/release/app-release.apk "release-assets/Feather-Browser-Release.apk"
8890
cd release-assets
8991
sha256sum "${APK_NAME}" > "${APK_NAME}.sha256"
90-
sha256sum "Feather-Browser-Release.apk" > "Feather-Browser-Release.apk.sha256"
9192
echo "apk_name=${APK_NAME}" >> $GITHUB_OUTPUT
9293
9394
- name: Upload APK as Workflow Artifact

README.md

Lines changed: 63 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@
1616

1717
<br />
1818

19-
[🌟 Core Differentiators](#-core-differentiators)[🛡️ Multi-Profile Isolation](#-isolated-multi-profiles--fingerprint-spoofing)[✨ Feature Highlights](#-feature-highlights)[🛠️ Architecture](#-architecture--tech-stack)[🚀 Building from Source](#-building-from-source)
19+
[🌟 Core Differentiators](#-core-differentiators)[🛡️ Multi-Profile Isolation](#-isolated-multi-profiles--fingerprint-spoofing)[✨ Feature Highlights](#-feature-highlights)[🛠️ Architecture](#-architecture--tech-stack)[🚀 Building from Source](#-building-from-source)[🔑 Release Signing & Updates](#-release-signing--seamless-in-place-updates)
2020

2121
---
2222

@@ -89,6 +89,7 @@ Most mobile browsers share a single global cookie jar and device identity across
8989
- 📑 **Visual Tabs Manager**: Intuitive tab switcher with real-time website favicons, tab counts, and swift swipe-to-dismiss gestures.
9090
- 📱 **Desktop Site Mode**: Instant one-tap switcher between mobile and desktop site layouts.
9191
- 🔎 **In-Page Find & Highlight**: Live text search within web pages featuring match indicators and jump navigation.
92+
- 🌤️ **Live Local Weather Card**: Zero-permission real-time weather forecasts powered by open-meteo using privacy-respecting IP-based geolocation, complete with temperature toggle (°C/°F) and cache efficiency.
9293
- 💾 **Local-Only Persistence**: Fast Room database with SQLite for bookmarks and history. Zero cloud sync or telemetry.
9394
- ⬇️ **Native Download Manager**: Integrated download handler with file opening, progress tracking, and clean directory management.
9495

@@ -140,6 +141,67 @@ cd lightweight_browser
140141

141142
---
142143

144+
## 🔑 Release Signing & Seamless In-Place Updates
145+
146+
### Why Android Shows "App Not Installed" or Google Play Protect Warnings
147+
When downloading consecutive APK builds without a persistent release keystore, each build is signed with an ephemeral debug or temporary key generated on-the-fly. Android's Package Manager enforces cryptographic signature identity:
148+
1. **"App not installed as package appears to be invalid / conflicts with an existing package"**: Triggered when trying to install an APK whose cryptographic certificate signature differs from the currently installed version. Android strictly blocks overwriting app data to prevent unauthorized app hijacking.
149+
2. **Google Play Protect / "Unrecognized App" Warning**: Displayed for any sideloaded APK whose signing certificate has not yet accrued reputation in Google Play Protect's cloud telemetry.
150+
151+
### The Permanent Solution (Used by NewPipe, Tachiyomi, VLC)
152+
Open-source Android applications solve both issues permanently by creating **one persistent Release Keystore**, storing it securely as a GitHub Repository Secret, and letting GitHub Actions automatically sign every tag and push release with this exact same certificate.
153+
154+
#### Step 1: Generate Your Permanent Keystore Locally
155+
Open your terminal (Linux, macOS, or Windows Git Bash / WSL) and run:
156+
```bash
157+
keytool -genkeypair -v \
158+
-keystore release.keystore \
159+
-alias feather_release_key \
160+
-keyalg RSA \
161+
-keysize 2048 \
162+
-validity 10000 \
163+
-storepass feather123 \
164+
-keypass feather123 \
165+
-dname "CN=Feather Browser, OU=Mobile, O=Feather Privacy, L=San Francisco, ST=California, C=US"
166+
```
167+
*(You can customize `-alias`, `-storepass`, and `-dname` as desired. **Back up `release.keystore` safely** — if lost, existing users cannot update without uninstalling!)*
168+
169+
#### Step 2: Convert Keystore to Base64 String
170+
Encode the binary keystore file into a clean string so it can be safely stored in GitHub:
171+
```bash
172+
# On Linux / macOS:
173+
base64 -w 0 release.keystore > keystore_base64.txt
174+
# (On macOS if -w 0 is unsupported: base64 -i release.keystore | tr -d '\n' > keystore_base64.txt)
175+
176+
# On Windows PowerShell:
177+
[Convert]::ToBase64String([IO.File]::ReadAllBytes("release.keystore")) | Out-File -Encoding ASCII keystore_base64.txt
178+
```
179+
180+
#### Step 3: Add to GitHub Repository Secrets
181+
1. Go to your GitHub repository: `https://github.com/YOUR_USERNAME/YOUR_REPO`
182+
2. Navigate to **Settings****Secrets and variables****Actions**.
183+
3. Click **New repository secret**:
184+
- **Name:** `RELEASE_KEYSTORE_BASE64`
185+
- **Secret:** Paste the entire contents of `keystore_base64.txt`.
186+
4. *(Optional)* If you changed the default passwords in Step 1, also add:
187+
- `KEYSTORE_PASSWORD`
188+
- `KEY_ALIAS`
189+
- `KEY_PASSWORD`
190+
191+
#### Step 4: Automated GitHub Actions Workflow
192+
The repository's `.github/workflows/release.yml` is already pre-configured to:
193+
- Automatically decode `RELEASE_KEYSTORE_BASE64` during CI runs.
194+
- Increment the version code monotonically (`APP_VERSION_CODE = 100 + run_number`).
195+
- Compile an R8-optimized release APK (`Feather-Browser-v1.0.x.apk`).
196+
- Generate SHA-256 checksums and publish an official GitHub Release with downloadable APK assets.
197+
198+
#### Step 5: Updating Your Phone In-Place
199+
1. For your very first install of the permanently signed version, uninstall any prior test/debug build to clean out temporary debug signatures.
200+
2. Install the newly signed APK from your GitHub Releases.
201+
3. From this point forward, every subsequent update (e.g. `v1.0.101` -> `v1.0.102`) will **update in-place with a single tap** without losing your bookmarks, history, profiles, or settings, and without signature conflict errors!
202+
203+
---
204+
143205
## 🔒 Permissions & Privacy Guarantee
144206

145207
Feather Browser only requests permissions strictly required for browsing:

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

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,8 @@ import com.example.data.BrowserRepository
1717
import com.example.data.model.*
1818
import com.example.privacy.ContentBlocker
1919
import com.example.privacy.PrivacyManager
20+
import com.example.weather.WeatherRepository
21+
import com.example.weather.WeatherUiState
2022
import kotlinx.coroutines.flow.*
2123
import kotlinx.coroutines.launch
2224
import java.util.UUID
@@ -100,6 +102,28 @@ class BrowserViewModel(application: Application) : AndroidViewModel(application)
100102
val enableWebDarkMode = MutableStateFlow(preferences.isEnableWebDarkMode())
101103
val enableBackgroundPlay = MutableStateFlow(preferences.isEnableBackgroundPlay())
102104
val downloadProvider = MutableStateFlow(preferences.getDownloadProvider())
105+
val isWeatherOnNewTab = MutableStateFlow(preferences.isWeatherOnNewTab())
106+
val isWeatherFahrenheit = MutableStateFlow(preferences.isWeatherFahrenheit())
107+
108+
// Weather Repository & UI State Flow
109+
val weatherRepository = WeatherRepository(application)
110+
val weatherUiState: StateFlow<WeatherUiState> = weatherRepository.weatherState
111+
112+
fun setWeatherOnNewTab(enabled: Boolean) {
113+
isWeatherOnNewTab.value = enabled
114+
preferences.setWeatherOnNewTab(enabled)
115+
}
116+
117+
fun setWeatherFahrenheit(fahrenheit: Boolean) {
118+
isWeatherFahrenheit.value = fahrenheit
119+
preferences.setWeatherFahrenheit(fahrenheit)
120+
}
121+
122+
fun refreshWeather(forceNetwork: Boolean = false) {
123+
viewModelScope.launch {
124+
weatherRepository.refreshWeather(forceNetwork)
125+
}
126+
}
103127

104128
fun setSearchEngine(engine: SearchEngine) {
105129
searchEngine.value = engine
@@ -258,6 +282,9 @@ class BrowserViewModel(application: Application) : AndroidViewModel(application)
258282
repository.initializeDefaultProfilesIfNeeded()
259283
repository.initializeDefaultShortcutsIfNeeded("default_personal")
260284
loadTabsForProfile(_currentProfileId.value)
285+
if (isWeatherOnNewTab.value) {
286+
weatherRepository.refreshWeather(forceNetwork = false)
287+
}
261288
}
262289
}
263290

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

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,24 @@ class BrowserPreferences(context: Context) {
2323
private const val KEY_WEB_DARK_MODE = "pref_web_dark_mode"
2424
private const val KEY_BACKGROUND_PLAY = "pref_background_play"
2525
private const val KEY_DOWNLOAD_PROVIDER = "pref_download_provider"
26+
private const val KEY_WEATHER_ON_NEW_TAB = "pref_weather_on_new_tab"
27+
private const val KEY_WEATHER_FAHRENHEIT = "pref_weather_fahrenheit"
28+
}
29+
30+
fun isWeatherOnNewTab(): Boolean {
31+
return prefs.getBoolean(KEY_WEATHER_ON_NEW_TAB, true)
32+
}
33+
34+
fun setWeatherOnNewTab(enabled: Boolean) {
35+
prefs.edit().putBoolean(KEY_WEATHER_ON_NEW_TAB, enabled).apply()
36+
}
37+
38+
fun isWeatherFahrenheit(): Boolean {
39+
return prefs.getBoolean(KEY_WEATHER_FAHRENHEIT, false)
40+
}
41+
42+
fun setWeatherFahrenheit(fahrenheit: Boolean) {
43+
prefs.edit().putBoolean(KEY_WEATHER_FAHRENHEIT, fahrenheit).apply()
2644
}
2745

2846
fun getSearchEngine(): SearchEngine {

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

Lines changed: 14 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,9 @@ fun BrowserScreen(
5151
val enableWebDarkMode by viewModel.enableWebDarkMode.collectAsStateWithLifecycle()
5252
val enableBackgroundPlay by viewModel.enableBackgroundPlay.collectAsStateWithLifecycle()
5353
val downloadProvider by viewModel.downloadProvider.collectAsStateWithLifecycle()
54+
val isWeatherEnabled by viewModel.isWeatherOnNewTab.collectAsStateWithLifecycle()
55+
val isWeatherFahrenheit by viewModel.isWeatherFahrenheit.collectAsStateWithLifecycle()
56+
val weatherUiState by viewModel.weatherUiState.collectAsStateWithLifecycle()
5457
val adBlockExceptions by viewModel.adBlockExceptions.collectAsStateWithLifecycle()
5558
val quickShortcuts by viewModel.quickShortcuts.collectAsStateWithLifecycle()
5659

@@ -93,8 +96,8 @@ fun BrowserScreen(
9396
topBar = {
9497
Surface(
9598
color = MaterialTheme.colorScheme.surface,
96-
tonalElevation = 2.dp,
97-
shadowElevation = 1.dp,
99+
tonalElevation = 0.dp,
100+
shadowElevation = 0.dp,
98101
modifier = Modifier.fillMaxWidth()
99102
) {
100103
Column(
@@ -140,6 +143,7 @@ fun BrowserScreen(
140143
canGoBack = activeTabState?.canGoBack == true,
141144
canGoForward = activeTabState?.canGoForward == true,
142145
tabCount = currentTabs.size,
146+
isPrivateMode = isPrivateMode,
143147
onGoBack = { viewModel.goBack() },
144148
onGoForward = { viewModel.goForward() },
145149
onGoHome = { viewModel.goHome() },
@@ -222,6 +226,10 @@ fun BrowserScreen(
222226
bookmarks = bookmarks,
223227
shortcuts = quickShortcuts,
224228
newTabStyle = newTabStyle,
229+
weatherState = weatherUiState,
230+
isWeatherEnabled = isWeatherEnabled,
231+
isWeatherFahrenheit = isWeatherFahrenheit,
232+
onRefreshWeather = { viewModel.refreshWeather(forceNetwork = true) },
225233
onNavigate = { viewModel.navigateTo(it) },
226234
onAddShortcut = { title, url -> viewModel.addQuickShortcut(title, url) },
227235
onEditShortcut = { id, title, url -> viewModel.editQuickShortcut(id, title, url) },
@@ -275,6 +283,10 @@ fun BrowserScreen(
275283
onToggleMaterialYou = { viewModel.setUseMaterialYou(it) },
276284
newTabStyle = newTabStyle,
277285
onNewTabStyleChange = { viewModel.setNewTabStyle(it) },
286+
isWeatherEnabled = isWeatherEnabled,
287+
onToggleWeather = { viewModel.setWeatherOnNewTab(it) },
288+
isWeatherFahrenheit = isWeatherFahrenheit,
289+
onToggleWeatherFahrenheit = { viewModel.setWeatherFahrenheit(it) },
278290
isAdBlockEnabled = isAdBlockEnabled,
279291
onToggleAdBlock = { viewModel.setAdBlockEnabled(it) },
280292
blockThirdPartyCookies = blockThirdPartyCookies,

app/src/main/java/com/example/ui/components/AddressBar.kt

Lines changed: 8 additions & 53 deletions
Original file line numberDiff line numberDiff line change
@@ -180,13 +180,14 @@ fun AddressBar(
180180

181181
// Address / Search Bar Input Field with depth
182182
Surface(
183-
shape = RoundedCornerShape(24.dp),
184-
color = MaterialTheme.colorScheme.surfaceVariant.copy(alpha = 0.75f),
185-
border = androidx.compose.foundation.BorderStroke(1.dp, MaterialTheme.colorScheme.outlineVariant.copy(alpha = 0.45f)),
186-
shadowElevation = 1.dp,
183+
shape = RoundedCornerShape(26.dp),
184+
color = MaterialTheme.colorScheme.surfaceVariant.copy(alpha = 0.65f),
185+
border = androidx.compose.foundation.BorderStroke(1.dp, MaterialTheme.colorScheme.outlineVariant.copy(alpha = 0.35f)),
186+
shadowElevation = 0.dp,
187+
tonalElevation = 0.dp,
187188
modifier = Modifier
188189
.weight(1f)
189-
.height(44.dp)
190+
.height(46.dp)
190191
) {
191192
Row(
192193
modifier = Modifier
@@ -372,7 +373,7 @@ fun AddressBar(
372373
if (!isEditing) {
373374
Spacer(modifier = Modifier.width(4.dp))
374375

375-
// Bookmark Icon Button
376+
// Quick Reload or Bookmark on Top Bar
376377
if (hasValidUrl) {
377378
IconButton(
378379
onClick = onToggleBookmark,
@@ -384,56 +385,10 @@ fun AddressBar(
384385
imageVector = if (isBookmarked) Icons.Filled.Bookmark else Icons.Outlined.BookmarkBorder,
385386
contentDescription = if (isBookmarked) "Bookmarked" else "Bookmark this page",
386387
tint = if (isBookmarked) Color(0xFFF59E0B) else MaterialTheme.colorScheme.onSurfaceVariant,
387-
modifier = Modifier.size(22.dp)
388+
modifier = Modifier.size(20.dp)
388389
)
389390
}
390391
}
391-
392-
// Tabs Counter Button
393-
Box(
394-
modifier = Modifier
395-
.size(36.dp)
396-
.clip(RoundedCornerShape(8.dp))
397-
.focusProperties { canFocus = false }
398-
.clickable { onOpenTabs() }
399-
.padding(2.dp)
400-
.testTag("tabs_button"),
401-
contentAlignment = Alignment.Center
402-
) {
403-
Surface(
404-
shape = RoundedCornerShape(6.dp),
405-
color = MaterialTheme.colorScheme.surfaceVariant,
406-
border = androidx.compose.foundation.BorderStroke(1.5.dp, MaterialTheme.colorScheme.outline.copy(alpha = 0.5f)),
407-
modifier = Modifier.defaultMinSize(minWidth = 24.dp, minHeight = 24.dp)
408-
) {
409-
Box(
410-
contentAlignment = Alignment.Center,
411-
modifier = Modifier.padding(horizontal = 4.dp, vertical = 2.dp)
412-
) {
413-
Text(
414-
text = "$tabCount",
415-
fontSize = 11.5.sp,
416-
fontWeight = FontWeight.Bold,
417-
color = MaterialTheme.colorScheme.onSurfaceVariant
418-
)
419-
}
420-
}
421-
}
422-
423-
// Menu 3-dots Button
424-
IconButton(
425-
onClick = onOpenMenu,
426-
modifier = Modifier
427-
.size(36.dp)
428-
.focusProperties { canFocus = false }
429-
.testTag("menu_button")
430-
) {
431-
Icon(
432-
imageVector = Icons.Default.MoreVert,
433-
contentDescription = "Browser Menu",
434-
tint = MaterialTheme.colorScheme.onSurfaceVariant
435-
)
436-
}
437392
}
438393
}
439394

0 commit comments

Comments
 (0)