From 26bd230041dc4c5bd8c69feafdf5a8a8a8c6b23b Mon Sep 17 00:00:00 2001 From: Derek <3dzguy@outlook.com> Date: Thu, 23 Jul 2026 01:57:16 +0800 Subject: [PATCH 1/2] For privacy --- README.md | 4 +- .../vbrowser/data/prefs/PrefsRepository.kt | 30 +++++ .../com/holeintimes/vbrowser/domain/Models.kt | 8 ++ .../vbrowser/ui/browser/BrowserScreen.kt | 114 +++++++++++++++++- .../vbrowser/ui/browser/BrowserViewModel.kt | 69 ++++++++--- .../ui/settings/PrivacySettingsScreen.kt | 79 ++++++------ .../vbrowser/ui/settings/SettingsScreen.kt | 36 +++--- app/src/main/res/values-zh/strings.xml | 13 +- app/src/main/res/values/strings.xml | 13 +- 9 files changed, 287 insertions(+), 79 deletions(-) diff --git a/README.md b/README.md index 5cfc388..34632f5 100644 --- a/README.md +++ b/README.md @@ -15,9 +15,9 @@ Cachelio keeps your private library under your control on this device. - **Hide media** in Files so it stays out of the normal library view -- **Privacy password** (optional) protects Privacy settings and unlocking hidden files +- **Privacy password** (optional) protects Privacy settings and unlocking the current session - **On-device only** — your password verifier is stored only on this device. There is no cloud account, password reset, or recovery. If you forget it, you must clear the app data -- **Session unlock** — “Show hidden files” lasts for the current app session only. It locks again when the app process restarts +- **Session unlock** — lasts for the current app session only and locks again when the app process restarts. While unlocked, hidden files are shown, and bookmarks added while unlocked are only visible until you unlock again. Bookmark toolbar actions stay available when locked; only private bookmark entries are hidden ## Get the app diff --git a/app/src/main/java/com/holeintimes/vbrowser/data/prefs/PrefsRepository.kt b/app/src/main/java/com/holeintimes/vbrowser/data/prefs/PrefsRepository.kt index cc0fe96..8fe6457 100644 --- a/app/src/main/java/com/holeintimes/vbrowser/data/prefs/PrefsRepository.kt +++ b/app/src/main/java/com/holeintimes/vbrowser/data/prefs/PrefsRepository.kt @@ -11,6 +11,7 @@ import androidx.datastore.preferences.core.stringPreferencesKey import androidx.datastore.preferences.core.stringSetPreferencesKey import androidx.datastore.preferences.preferencesDataStore import com.holeintimes.vbrowser.domain.AppLanguage +import com.holeintimes.vbrowser.domain.BookmarkEntry import com.holeintimes.vbrowser.domain.FilesSort import com.holeintimes.vbrowser.domain.HistoryEntry import com.holeintimes.vbrowser.domain.UserPreferences @@ -46,6 +47,7 @@ class PrefsRepository(private val context: Context) { val lastPlayedPath = stringPreferencesKey("last_played_path") val lastBrightness = floatPreferencesKey("last_brightness") val history = stringPreferencesKey("visit_history") + val bookmarks = stringPreferencesKey("bookmarks") val urlIndex = stringSetPreferencesKey("url_index") } @@ -178,6 +180,34 @@ class PrefsRepository(private val context: Context) { context.dataStore.edit { it.remove(Keys.history) } } + val bookmarks: Flow> = context.dataStore.data.map { p -> + val raw = p[Keys.bookmarks] ?: return@map emptyList() + runCatching { json.decodeFromString>(raw) }.getOrElse { emptyList() } + } + + suspend fun addBookmark(entry: BookmarkEntry) { + context.dataStore.edit { prefs -> + val current = prefs[Keys.bookmarks] + ?.let { runCatching { json.decodeFromString>(it) }.getOrNull() } + .orEmpty() + val updated = listOf(entry) + current.filterNot { + it.url == entry.url && it.isPrivate == entry.isPrivate + } + prefs[Keys.bookmarks] = json.encodeToString(updated.take(200)) + } + } + + suspend fun removeBookmark(url: String, isPrivate: Boolean) { + context.dataStore.edit { prefs -> + val current = prefs[Keys.bookmarks] + ?.let { runCatching { json.decodeFromString>(it) }.getOrNull() } + .orEmpty() + prefs[Keys.bookmarks] = json.encodeToString( + current.filterNot { it.url == url && it.isPrivate == isPrivate } + ) + } + } + val downloadedUrls: Flow> = context.dataStore.data.map { it[Keys.urlIndex] ?: emptySet() } diff --git a/app/src/main/java/com/holeintimes/vbrowser/domain/Models.kt b/app/src/main/java/com/holeintimes/vbrowser/domain/Models.kt index 6353394..1b18892 100644 --- a/app/src/main/java/com/holeintimes/vbrowser/domain/Models.kt +++ b/app/src/main/java/com/holeintimes/vbrowser/domain/Models.kt @@ -113,3 +113,11 @@ data class HistoryEntry( val title: String = "", val visitedAt: Long = System.currentTimeMillis() ) + +@Serializable +data class BookmarkEntry( + val url: String, + val title: String = "", + val addedAt: Long = System.currentTimeMillis(), + val isPrivate: Boolean = false +) diff --git a/app/src/main/java/com/holeintimes/vbrowser/ui/browser/BrowserScreen.kt b/app/src/main/java/com/holeintimes/vbrowser/ui/browser/BrowserScreen.kt index 389b953..9a3f121 100644 --- a/app/src/main/java/com/holeintimes/vbrowser/ui/browser/BrowserScreen.kt +++ b/app/src/main/java/com/holeintimes/vbrowser/ui/browser/BrowserScreen.kt @@ -34,15 +34,20 @@ import androidx.compose.material.icons.automirrored.filled.ArrowBack import androidx.compose.material.icons.automirrored.filled.ArrowForward import androidx.compose.foundation.layout.PaddingValues import androidx.compose.foundation.layout.statusBarsPadding +import androidx.compose.material.icons.filled.Bookmarks +import androidx.compose.material.icons.filled.Delete +import androidx.compose.material.icons.filled.Done +import androidx.compose.material.icons.filled.Download import androidx.compose.material.icons.filled.ExpandLess import androidx.compose.material.icons.filled.ExpandMore -import androidx.compose.material.icons.filled.Done import androidx.compose.material.icons.filled.Lock import androidx.compose.material.icons.filled.Menu +import androidx.compose.material.icons.filled.Refresh +import androidx.compose.material.icons.filled.Star +import androidx.compose.material.icons.filled.StarBorder +import androidx.compose.material3.AlertDialog import androidx.compose.material3.Surface import com.holeintimes.vbrowser.data.sniff.VideoTitleResolver -import androidx.compose.material.icons.filled.Download -import androidx.compose.material.icons.filled.Refresh import androidx.compose.material3.Card import androidx.compose.material3.CardDefaults import androidx.compose.material3.HorizontalDivider @@ -80,6 +85,7 @@ import androidx.lifecycle.compose.collectAsStateWithLifecycle import com.holeintimes.vbrowser.R import com.holeintimes.vbrowser.data.media.MediaLibrary import com.holeintimes.vbrowser.data.sniff.VideoFormatUtil +import com.holeintimes.vbrowser.domain.BookmarkEntry import com.holeintimes.vbrowser.domain.VideoInfo import org.json.JSONArray import org.json.JSONObject @@ -97,6 +103,7 @@ fun BrowserScreen( var webView by remember { mutableStateOf(null) } var foundExpanded by remember { mutableStateOf(false) } var urlEditing by remember { mutableStateOf(false) } + var showBookmarks by remember { mutableStateOf(false) } LaunchedEffect(state.found.size) { if (state.found.isNotEmpty() && !foundExpanded) foundExpanded = true @@ -145,6 +152,25 @@ fun BrowserScreen( IconButton(onClick = { webView?.reload() }) { Icon(Icons.Default.Refresh, contentDescription = stringResource(R.string.refresh)) } + IconButton( + onClick = { viewModel.toggleBookmark() }, + enabled = state.currentUrl.isNotBlank() && state.currentUrl != HOME_URL + ) { + Icon( + imageVector = if (state.isCurrentBookmarked) { + Icons.Filled.Star + } else { + Icons.Filled.StarBorder + }, + contentDescription = stringResource(R.string.bookmark) + ) + } + IconButton(onClick = { showBookmarks = true }) { + Icon( + Icons.Default.Bookmarks, + contentDescription = stringResource(R.string.bookmarks) + ) + } } BrowserUrlBar( currentUrl = state.currentUrl, @@ -222,6 +248,88 @@ fun BrowserScreen( } } } + + if (showBookmarks) { + BookmarksDialog( + bookmarks = state.visibleBookmarks, + onOpen = { entry -> + showBookmarks = false + viewModel.onUrlSubmitted(entry.url) + }, + onRemove = viewModel::removeBookmark, + onDismiss = { showBookmarks = false } + ) + } +} + +@Composable +private fun BookmarksDialog( + bookmarks: List, + onOpen: (BookmarkEntry) -> Unit, + onRemove: (BookmarkEntry) -> Unit, + onDismiss: () -> Unit +) { + AlertDialog( + onDismissRequest = onDismiss, + title = { Text(stringResource(R.string.bookmarks)) }, + text = { + if (bookmarks.isEmpty()) { + Text( + stringResource(R.string.no_bookmarks), + style = MaterialTheme.typography.bodyMedium, + color = MaterialTheme.colorScheme.onSurfaceVariant + ) + } else { + LazyColumn(modifier = Modifier.heightIn(max = 360.dp)) { + items(bookmarks, key = { "${it.isPrivate}:${it.url}" }) { entry -> + Row( + modifier = Modifier + .fillMaxWidth() + .clickable { onOpen(entry) } + .padding(vertical = 8.dp), + verticalAlignment = Alignment.CenterVertically + ) { + Column(modifier = Modifier.weight(1f)) { + Text( + text = entry.title.ifBlank { formatUrlForDisplay(entry.url) }, + style = MaterialTheme.typography.bodyMedium, + maxLines = 1, + overflow = TextOverflow.Ellipsis + ) + Text( + text = formatUrlForDisplay(entry.url), + style = MaterialTheme.typography.bodySmall, + color = MaterialTheme.colorScheme.onSurfaceVariant, + maxLines = 1, + overflow = TextOverflow.Ellipsis + ) + } + if (entry.isPrivate) { + Icon( + Icons.Default.Lock, + contentDescription = null, + tint = MaterialTheme.colorScheme.onSurfaceVariant, + modifier = Modifier.padding(horizontal = 4.dp) + ) + } + IconButton(onClick = { onRemove(entry) }) { + Icon( + Icons.Default.Delete, + contentDescription = stringResource(R.string.remove_bookmark) + ) + } + } + HorizontalDivider() + } + } + } + }, + confirmButton = { + TextButton(onClick = onDismiss) { + Text(stringResource(R.string.cancel)) + } + } + ) } @OptIn(ExperimentalMaterial3Api::class) diff --git a/app/src/main/java/com/holeintimes/vbrowser/ui/browser/BrowserViewModel.kt b/app/src/main/java/com/holeintimes/vbrowser/ui/browser/BrowserViewModel.kt index 6a7bd8f..a91d942 100644 --- a/app/src/main/java/com/holeintimes/vbrowser/ui/browser/BrowserViewModel.kt +++ b/app/src/main/java/com/holeintimes/vbrowser/ui/browser/BrowserViewModel.kt @@ -5,6 +5,7 @@ import androidx.lifecycle.ViewModelProvider import androidx.lifecycle.viewModelScope import com.holeintimes.vbrowser.AppContainer import com.holeintimes.vbrowser.data.sniff.VideoFormatUtil +import com.holeintimes.vbrowser.domain.BookmarkEntry import com.holeintimes.vbrowser.domain.DetectedVideoInfo import com.holeintimes.vbrowser.domain.HistoryEntry import com.holeintimes.vbrowser.domain.UserPreferences @@ -16,7 +17,7 @@ import kotlinx.coroutines.flow.combine import kotlinx.coroutines.flow.stateIn import kotlinx.coroutines.launch -private const val HOME_URL = "file:///android_asset/home.html" +internal const val HOME_URL = "file:///android_asset/home.html" data class BrowserUiState( val currentUrl: String = HOME_URL, @@ -29,25 +30,39 @@ data class BrowserUiState( val downloadedUrls: Set = emptySet(), val prefs: UserPreferences = UserPreferences(), val history: List = emptyList(), + val bookmarks: List = emptyList(), + val privacyUnlocked: Boolean = false, val pendingLoadUrl: String? = null -) +) { + val visibleBookmarks: List + get() = if (privacyUnlocked) bookmarks else bookmarks.filterNot { it.isPrivate } + + val isCurrentBookmarked: Boolean + get() = visibleBookmarks.any { it.url == currentUrl } +} class BrowserViewModel(private val container: AppContainer) : ViewModel() { private val _ui = MutableStateFlow(BrowserUiState()) val uiState: StateFlow = combine( - _ui, - container.sniffer.foundList, - container.prefs.downloadedUrls, - container.prefs.preferences, - container.prefs.history - ) { ui, found, urls, prefs, history -> - ui.copy( - found = found, - downloadedUrls = urls, - prefs = prefs, - history = history - ) + combine( + _ui, + container.sniffer.foundList, + container.prefs.downloadedUrls, + container.prefs.preferences, + container.prefs.history + ) { ui, found, urls, prefs, history -> + ui.copy( + found = found, + downloadedUrls = urls, + prefs = prefs, + history = history + ) + }, + container.prefs.bookmarks, + container.privacySession.isUnlocked + ) { ui, bookmarks, unlocked -> + ui.copy(bookmarks = bookmarks, privacyUnlocked = unlocked) }.stateIn(viewModelScope, SharingStarted.WhileSubscribed(5000), BrowserUiState()) init { @@ -177,6 +192,32 @@ class BrowserViewModel(private val container: AppContainer) : ViewModel() { fun openExternalUrl(url: String) = onUrlSubmitted(url) + fun toggleBookmark() { + val state = uiState.value + val url = state.currentUrl + if (url.isBlank() || url == HOME_URL) return + val existing = state.visibleBookmarks.firstOrNull { it.url == url } + viewModelScope.launch { + if (existing != null) { + container.prefs.removeBookmark(existing.url, existing.isPrivate) + } else { + container.prefs.addBookmark( + BookmarkEntry( + url = url, + title = state.pageTitle, + isPrivate = state.privacyUnlocked + ) + ) + } + } + } + + fun removeBookmark(entry: BookmarkEntry) { + viewModelScope.launch { + container.prefs.removeBookmark(entry.url, entry.isPrivate) + } + } + private fun normalizeUrl(raw: String): String { val t = raw.trim() if (t.isEmpty() || t == HOME_URL) return HOME_URL diff --git a/app/src/main/java/com/holeintimes/vbrowser/ui/settings/PrivacySettingsScreen.kt b/app/src/main/java/com/holeintimes/vbrowser/ui/settings/PrivacySettingsScreen.kt index f29c35c..8047430 100644 --- a/app/src/main/java/com/holeintimes/vbrowser/ui/settings/PrivacySettingsScreen.kt +++ b/app/src/main/java/com/holeintimes/vbrowser/ui/settings/PrivacySettingsScreen.kt @@ -49,6 +49,12 @@ fun PrivacySettingsScreen( .verticalScroll(rememberScrollState()) .padding(16.dp) ) { + Text( + text = stringResource(R.string.privacy_section_session), + style = MaterialTheme.typography.titleSmall, + color = MaterialTheme.colorScheme.primary, + modifier = Modifier.padding(start = 4.dp, bottom = 8.dp) + ) Card( modifier = Modifier.fillMaxWidth(), shape = RoundedCornerShape(12.dp), @@ -56,40 +62,39 @@ fun PrivacySettingsScreen( containerColor = MaterialTheme.colorScheme.surfaceContainerLow ) ) { - Row( - modifier = Modifier - .fillMaxWidth() - .padding(16.dp), - verticalAlignment = Alignment.CenterVertically - ) { - Column(modifier = Modifier.weight(1f)) { + Column(modifier = Modifier.padding(16.dp)) { + Row( + modifier = Modifier.fillMaxWidth(), + verticalAlignment = Alignment.CenterVertically + ) { Text( stringResource(R.string.privacy_session_unlock), - style = MaterialTheme.typography.bodyLarge + style = MaterialTheme.typography.bodyLarge, + modifier = Modifier.weight(1f) ) - Text( - stringResource(R.string.privacy_session_unlock_desc), - style = MaterialTheme.typography.bodySmall, - color = MaterialTheme.colorScheme.onSurfaceVariant + Switch( + checked = unlocked, + onCheckedChange = { + if (it) container.privacySession.unlock() + else container.privacySession.lock() + } ) } - Switch( - checked = unlocked, - onCheckedChange = { - if (it) container.privacySession.unlock() - else container.privacySession.lock() - } + Text( + stringResource(R.string.privacy_session_unlock_desc), + style = MaterialTheme.typography.bodySmall, + color = MaterialTheme.colorScheme.onSurfaceVariant, + modifier = Modifier.padding(top = 12.dp) ) } } Text( - text = stringResource(R.string.privacy_password_warning), - style = MaterialTheme.typography.bodySmall, - color = MaterialTheme.colorScheme.onSurfaceVariant, - modifier = Modifier.padding(horizontal = 4.dp, vertical = 16.dp) + text = stringResource(R.string.privacy_section_password), + style = MaterialTheme.typography.titleSmall, + color = MaterialTheme.colorScheme.primary, + modifier = Modifier.padding(start = 4.dp, top = 20.dp, bottom = 8.dp) ) - Card( modifier = Modifier.fillMaxWidth(), shape = RoundedCornerShape(12.dp), @@ -97,18 +102,24 @@ fun PrivacySettingsScreen( containerColor = MaterialTheme.colorScheme.surfaceContainerLow ) ) { - TextButton( - onClick = { showPasswordEditor = true }, - modifier = Modifier - .fillMaxWidth() - .padding(horizontal = 8.dp, vertical = 4.dp) - ) { + Column(modifier = Modifier.padding(horizontal = 8.dp, vertical = 8.dp)) { + TextButton( + onClick = { showPasswordEditor = true }, + modifier = Modifier.fillMaxWidth() + ) { + Text( + if (hasPassword) { + stringResource(R.string.change_privacy_password) + } else { + stringResource(R.string.set_privacy_password) + } + ) + } Text( - if (hasPassword) { - stringResource(R.string.change_privacy_password) - } else { - stringResource(R.string.set_privacy_password) - } + text = stringResource(R.string.privacy_password_warning), + style = MaterialTheme.typography.bodySmall, + color = MaterialTheme.colorScheme.onSurfaceVariant, + modifier = Modifier.padding(horizontal = 8.dp, vertical = 8.dp) ) } } diff --git a/app/src/main/java/com/holeintimes/vbrowser/ui/settings/SettingsScreen.kt b/app/src/main/java/com/holeintimes/vbrowser/ui/settings/SettingsScreen.kt index 6b4e959..24066aa 100644 --- a/app/src/main/java/com/holeintimes/vbrowser/ui/settings/SettingsScreen.kt +++ b/app/src/main/java/com/holeintimes/vbrowser/ui/settings/SettingsScreen.kt @@ -74,6 +74,24 @@ fun SettingsScreen( checked = prefs.forceWebDarkMode, onChecked = { scope.launch { container.prefs.setForceWebDarkMode(it) } } ) + HorizontalDivider() + PrefRow( + title = stringResource(R.string.privacy_settings_title), + subtitle = stringResource(R.string.privacy_mode_desc), + onClick = { + scope.launch { + if (container.privacySession.isUnlocked.value || + !container.prefs.isPrivacyPasswordSet() + ) { + onOpenPrivacy() + } else { + privacyPassword = "" + privacyPasswordError = false + showPrivacyPassword = true + } + } + } + ) } SettingsSection(title = stringResource(R.string.settings_section_browser)) { @@ -99,24 +117,6 @@ fun SettingsScreen( checked = prefs.autoPlayNext, onChecked = { scope.launch { container.prefs.setAutoPlayNext(it) } } ) - HorizontalDivider() - PrefRow( - title = stringResource(R.string.privacy_mode), - subtitle = stringResource(R.string.privacy_mode_desc), - onClick = { - scope.launch { - if (container.privacySession.isUnlocked.value || - !container.prefs.isPrivacyPasswordSet() - ) { - onOpenPrivacy() - } else { - privacyPassword = "" - privacyPasswordError = false - showPrivacyPassword = true - } - } - } - ) } SettingsSection(title = stringResource(R.string.settings_section_about)) { diff --git a/app/src/main/res/values-zh/strings.xml b/app/src/main/res/values-zh/strings.xml index 6b78473..68bb435 100644 --- a/app/src/main/res/values-zh/strings.xml +++ b/app/src/main/res/values-zh/strings.xml @@ -47,12 +47,17 @@ 未安装邮件应用;地址已复制 Cachelio 下载中 正在进行下载任务 - 隐私模式 - 解锁隐藏文件并管理隐私密码 + 管理会话解锁与隐私密码 隐私设置 - 显示隐藏文件 - 仅在本次应用会话中解锁;应用进程重新启动后会自动锁定。 + 会话 + 密码 + 解锁当前会话 + 打开后:显示隐藏文件;在解锁状态下收藏的网址仅在解锁时可见,锁定状态下收藏的网址在锁定时仍可见。收藏工具栏在锁定与解锁时都可用,仅过滤私密收藏条目。解锁仅在本次应用会话中有效,应用进程重新启动后会自动锁定。 密码验证信息仅保存在本机,无法重置或找回密码。如果忘记密码,只能清除应用数据。 + 收藏 + 收藏 + 取消收藏 + 暂无收藏 需要隐私密码 隐私密码 设置隐私密码 diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 621bcce..b6d8d4d 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -46,12 +46,17 @@ No email app installed; address copied Cachelio downloading Downloads in progress - Privacy mode - Unlock hidden files and manage the privacy password + Manage session unlock and the privacy password Privacy settings - Show hidden files - Unlocked for this app session only. It locks again when the app process restarts. + Session + Password + Unlock this session + When on: show hidden files, and bookmarks added while unlocked are only visible while unlocked. Bookmarks added while locked stay visible when locked. Bookmark toolbar actions stay available either way. Unlock lasts for this app session only and locks again when the app process restarts. Your password verifier is stored only on this device. There is no password reset or recovery. If you forget it, you must clear the app data. + Bookmarks + Bookmark + Remove bookmark + No bookmarks yet Privacy password required Privacy password Set privacy password From 374ae6a5091aaa4918ee69fe51d804a3b0786ebe Mon Sep 17 00:00:00 2001 From: Derek <3dzguy@outlook.com> Date: Thu, 23 Jul 2026 20:17:07 +0800 Subject: [PATCH 2/2] privacy_wf --- .github/workflows/cachelio-debug-apk.yml | 11 +++++++++-- README.md | 4 ++-- 2 files changed, 11 insertions(+), 4 deletions(-) diff --git a/.github/workflows/cachelio-debug-apk.yml b/.github/workflows/cachelio-debug-apk.yml index df6af7c..1bcf958 100644 --- a/.github/workflows/cachelio-debug-apk.yml +++ b/.github/workflows/cachelio-debug-apk.yml @@ -2,7 +2,6 @@ name: Cachelio Debug APK on: push: - branches: [main, master] pull_request: workflow_dispatch: @@ -22,6 +21,14 @@ jobs: - name: Checkout uses: actions/checkout@v4 + - name: Set artifact name + id: meta + run: | + REF="${GITHUB_HEAD_REF:-$GITHUB_REF_NAME}" + SAFE="$(echo "$REF" | tr '/' '-')" + echo "artifact=cachelio-debug-apk-${SAFE}" >> "$GITHUB_OUTPUT" + echo "Building ref: $REF" + - name: Set up JDK 17 uses: actions/setup-java@v4 with: @@ -65,7 +72,7 @@ jobs: - name: Upload debug APK uses: actions/upload-artifact@v4 with: - name: cachelio-debug-apk + name: ${{ steps.meta.outputs.artifact }} path: app/build/outputs/apk/debug/*.apk if-no-files-found: error retention-days: 30 diff --git a/README.md b/README.md index 34632f5..fde1aff 100644 --- a/README.md +++ b/README.md @@ -23,7 +23,7 @@ Cachelio keeps your private library under your control on this device. Official releases are published on [GitHub Releases](https://github.com/NullMargin/z-grab-browser3/releases) as `cachelio-.apk` (for example `cachelio-v2.0.3.apk`). -For development builds, install the latest debug APK from the GitHub Actions artifact [`cachelio-debug-apk`](.github/workflows/vbrowser-debug-apk.yml) (workflow: Cachelio Debug APK). +For development builds, install the latest debug APK from the GitHub Actions artifact `cachelio-debug-apk-` (workflow: [Cachelio Debug APK](.github/workflows/cachelio-debug-apk.yml)). Pushes to any branch build that branch. Cachelio uses package id `com.holeintimes.vbrowser`, so it installs as an update over previous VBrowser builds and keeps your existing downloads and settings. @@ -43,6 +43,6 @@ echo "sdk.dir=$HOME/Android/Sdk" > local.properties Or open the project in Android Studio (Ladybug+) and sync. -CI: [`.github/workflows/vbrowser-debug-apk.yml`](.github/workflows/vbrowser-debug-apk.yml) builds `assembleDebug` on push/PR and `workflow_dispatch`, and uploads `cachelio-debug-apk`. [`.github/workflows/cachelio-release-apk.yml`](.github/workflows/cachelio-release-apk.yml) builds a signed `assembleRelease` APK and attaches it when a GitHub Release is published. +CI: [`.github/workflows/cachelio-debug-apk.yml`](.github/workflows/cachelio-debug-apk.yml) builds `assembleDebug` on push (any branch), PR, and `workflow_dispatch`, and uploads `cachelio-debug-apk-`. [`.github/workflows/cachelio-release-apk.yml`](.github/workflows/cachelio-release-apk.yml) builds a signed `assembleRelease` APK and attaches it when a GitHub Release is published. Reference PRD: [`low-code/docs/BUSINESS_REQUIREMENTS.md`](low-code/docs/BUSINESS_REQUIREMENTS.md)