Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 3 additions & 3 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -14,11 +14,11 @@ check: ## Type-check JavaScript files

.PHONY: bump-release-dev
bump-release-dev: ## Bump GitHub version
./script/bumpver update --tag=dev --push
./scripts/bumpver update --tag=dev --push

.PHONY: bump-release-production
bump-release-production: ## Bump Google Play version
./script/bumpver update --tag=final
./scripts/bumpver update --tag=final

.PHONY: changelog
changelog: ## Prep next release notes
Expand Down Expand Up @@ -56,7 +56,7 @@ bench-reset: ## Delete bench data

.PHONY: install-tailscale
install-tailscale: ## Install Tailscale on emulator
./script/install-tailscale
./scripts/install-tailscale

.PHONY: help
help:
Expand Down
103 changes: 44 additions & 59 deletions app/src/main/java/com/capyreader/app/ui/articles/ArticleList.kt
Original file line number Diff line number Diff line change
Expand Up @@ -5,24 +5,14 @@ import androidx.compose.animation.fadeIn
import androidx.compose.animation.fadeOut
import androidx.compose.animation.slideInVertically
import androidx.compose.animation.slideOutVertically
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.LazyListState
import androidx.compose.foundation.shape.CircleShape
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.ArrowUpward
import androidx.compose.material3.Icon
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Surface
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
Expand All @@ -49,7 +39,9 @@ import com.capyreader.app.preferences.AppPreferences
import com.jocmp.capy.Article
import com.jocmp.capy.MarkRead
import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.distinctUntilChanged
import kotlinx.coroutines.flow.collectLatest
import kotlinx.coroutines.flow.filter
import kotlinx.coroutines.flow.first
import org.koin.compose.koinInject
import java.time.LocalDateTime

Expand All @@ -63,38 +55,24 @@ fun ArticleList(
enableMarkReadOnScroll: Boolean = false,
dimReadArticles: Boolean = true,
scrollToTop: () -> Unit = {},
isRefreshing: Boolean = false,
) {
val articleOptions = rememberArticleOptions().copy(
dim = dimReadArticles,
)
val currentTime = rememberCurrentTime()
val localDensity = LocalDensity.current
val coroutineScope = rememberCoroutineScope()
var listHeight by remember { mutableStateOf(0.dp) }
var hasNewArticles by remember { mutableStateOf(false) }

if (!enableMarkReadOnScroll) {
LaunchedEffect(listState) {
var lastCount = listState.layoutInfo.totalItemsCount

snapshotFlow { listState.layoutInfo.totalItemsCount }
.distinctUntilChanged()
.collect { count ->
if (count > lastCount && listState.firstVisibleItemIndex > 0) {
hasNewArticles = true
}
lastCount = count
}
}

LaunchedEffect(listState) {
snapshotFlow { listState.firstVisibleItemIndex }
.collect { index ->
if (index == 0) {
hasNewArticles = false
}
}
}
NewArticleObserver(
articles = articles,
listState = listState,
isRefreshing = isRefreshing,
onNewArticles = { hasNewArticles = true },
onScrollToTop = { hasNewArticles = false },
)
}

Box(Modifier.fillMaxSize()) {
Expand Down Expand Up @@ -161,32 +139,6 @@ fun ArticleList(
}
}

@Composable
private fun NewArticlesPill(onClick: () -> Unit) {
Surface(
onClick = onClick,
shape = CircleShape,
color = MaterialTheme.colorScheme.primary,
contentColor = MaterialTheme.colorScheme.onPrimary,
shadowElevation = 4.dp,
) {
Row(
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.spacedBy(4.dp),
modifier = Modifier.padding(
PaddingValues(start = 12.dp, end = 16.dp, top = 8.dp, bottom = 8.dp)
)
) {
Icon(
imageVector = Icons.Filled.ArrowUpward,
contentDescription = null,
modifier = Modifier.size(18.dp),
)
Text(stringResource(R.string.article_list_new_articles_pill))
}
}
}

@Composable
fun FeedOverScrollBox(height: Dp) {
Box(
Expand All @@ -204,6 +156,39 @@ fun FeedOverScrollBox(height: Dp) {
}
}

@Composable
private fun NewArticleObserver(
articles: LazyPagingItems<Article>,
listState: LazyListState,
isRefreshing: Boolean,
onNewArticles: () -> Unit,
onScrollToTop: () -> Unit,
) {
LaunchedEffect(Unit) {
snapshotFlow { isRefreshing }
.filter { it }
.collectLatest {
val baseline = articles.itemCount

snapshotFlow { articles.itemCount }
.first { it > baseline }

if (listState.firstVisibleItemIndex > 0) {
onNewArticles()
}
}
}

LaunchedEffect(listState) {
snapshotFlow { listState.firstVisibleItemIndex }
.collect { index ->
if (index == 0) {
onScrollToTop()
}
}
}
}

@Composable
fun rememberCurrentTime(): LocalDateTime {
var currentTime by remember { mutableStateOf(LocalDateTime.now()) }
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -600,6 +600,7 @@ fun ArticleScreen(
enableMarkReadOnScroll = viewModel.markReadOnScrollEnabled,
dimReadArticles = filter.status != ArticleStatus.STARRED,
scrollToTop = { scrollToTop() },
isRefreshing = isPullToRefreshing,
onMarkAllRead = { range ->
onMarkAllRead(range)
},
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
package com.capyreader.app.ui.articles

import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.shape.CircleShape
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.ArrowUpward
import androidx.compose.material3.Icon
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Surface
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.tooling.preview.PreviewLightDark
import androidx.compose.ui.unit.dp
import com.capyreader.app.R
import com.capyreader.app.ui.theme.CapyTheme

@Composable
fun NewArticlesPill(onClick: () -> Unit) {
Surface(
onClick = onClick,
shape = CircleShape,
color = MaterialTheme.colorScheme.primary,
contentColor = MaterialTheme.colorScheme.onPrimary,
shadowElevation = 4.dp,
) {
Row(
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.spacedBy(6.dp),
modifier = Modifier.padding(
PaddingValues(start = 12.dp, end = 16.dp, top = 8.dp, bottom = 8.dp)
)
) {
Icon(
imageVector = Icons.Filled.ArrowUpward,
contentDescription = null,
modifier = Modifier.size(16.dp),
)
Text(stringResource(R.string.article_list_new_articles_pill))
}
}
}

@PreviewLightDark
@Composable
private fun NewArticlesPillPreview() {
CapyTheme {
NewArticlesPill {}
}
}
File renamed without changes.
86 changes: 86 additions & 0 deletions scripts/delete-newest
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
#!/usr/bin/env bash
# Deletes the N newest articles (by published_at) from the on-device DB so a
# subsequent refresh re-fetches them. Useful for exercising the sync path.
#
# Force-stops the app to release the DB, pulls it via run-as, deletes locally
# with sqlite3, writes back via stdin into run-as, and clears WAL/SHM so the
# app reopens cleanly.
#
# Usage:
# scripts/delete-newest # default count: 5
# scripts/delete-newest 20
# scripts/delete-newest 5 -p com.capyreader.app.nightly

set -euo pipefail

CANDIDATE_PACKAGES=(
com.capyreader.app.debug
com.capyreader.app.nightly
com.capyreader.app
)

count=5
package=""

while [[ $# -gt 0 ]]; do
case "$1" in
-p|--package) package="$2"; shift 2 ;;
-h|--help)
sed -n '2,/^$/p' "$0" | sed 's/^# \?//'
exit 0
;;
-*) echo "unknown option: $1" >&2; exit 2 ;;
*) count="$1"; shift ;;
esac
done

[[ "$count" =~ ^[0-9]+$ ]] || { echo "count must be a non-negative integer, got: $count" >&2; exit 2; }

command -v adb >/dev/null 2>&1 || { echo "adb not found on PATH" >&2; exit 1; }
command -v sqlite3 >/dev/null 2>&1 || { echo "sqlite3 not found on PATH" >&2; exit 1; }

if [[ -z "$package" ]]; then
installed=$(adb shell pm list packages | tr -d '\r' | sed 's/^package://')
for candidate in "${CANDIDATE_PACKAGES[@]}"; do
if grep -qx "$candidate" <<<"$installed"; then
package="$candidate"
break
fi
done
[[ -n "$package" ]] || { echo "No capyreader build found. Tried: ${CANDIDATE_PACKAGES[*]}" >&2; exit 1; }
fi

remote_dir="/data/data/$package/databases"
db_name=$(adb shell run-as "$package" ls "$remote_dir" \
| tr -d '\r' | tr ' ' '\n' \
| grep '^articles_' | grep -v -E -- '-(journal|wal|shm)$' \
| head -1)
[[ -n "$db_name" ]] || { echo "No articles_* database found for $package" >&2; exit 1; }

work_dir=$(mktemp -d)
trap 'rm -rf "$work_dir"' EXIT
local_db="$work_dir/$db_name"

echo "Force-stopping $package..."
adb shell am force-stop "$package"

echo "Pulling $db_name..."
adb exec-out run-as "$package" cat "$remote_dir/$db_name" > "$local_db"

before=$(sqlite3 "$local_db" "SELECT COUNT(*) FROM articles;")
sqlite3 "$local_db" <<SQL
DELETE FROM articles
WHERE id IN (
SELECT id FROM articles ORDER BY published_at DESC LIMIT $count
);
SQL
after=$(sqlite3 "$local_db" "SELECT COUNT(*) FROM articles;")
echo "Deleted $((before - after)) of $count requested (articles: $before -> $after)"

echo "Writing DB back..."
adb shell "run-as $package sh -c 'cat > $remote_dir/$db_name'" < "$local_db"

echo "Clearing WAL/SHM..."
adb shell "run-as $package sh -c 'rm -f $remote_dir/$db_name-wal $remote_dir/$db_name-shm'"

echo "Done. Reopen the app and refresh to re-fetch."
54 changes: 54 additions & 0 deletions scripts/force-refresh
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
#!/usr/bin/env bash
# Force-stops capyreader and relaunches it, which drains the sync outbox via
# the on-launch refreshAll() path.
#
# WorkManager's PeriodicWorkRequest gate refuses to run RefreshFeedsWorker out
# of schedule even with `cmd jobscheduler run -f`, so we cheat: kill the
# process and restart it. Same code path as a periodic refresh, just sooner.
#
# Usage:
# scripts/force-refresh # auto-detect debug build
# scripts/force-refresh -p com.capyreader.app.nightly

set -euo pipefail

CANDIDATE_PACKAGES=(
com.capyreader.app.debug
com.capyreader.app.nightly
com.capyreader.app
)

package=""

while [[ $# -gt 0 ]]; do
case "$1" in
-p|--package) package="$2"; shift 2 ;;
-h|--help)
sed -n '2,/^$/p' "$0" | sed 's/^# \?//'
exit 0
;;
*) echo "unknown option: $1" >&2; exit 2 ;;
esac
done

command -v adb >/dev/null 2>&1 || { echo "adb not found on PATH" >&2; exit 1; }

if [[ -z "$package" ]]; then
installed=$(adb shell pm list packages | tr -d '\r' | sed 's/^package://')
for candidate in "${CANDIDATE_PACKAGES[@]}"; do
if grep -qx "$candidate" <<<"$installed"; then
package="$candidate"
break
fi
done
[[ -n "$package" ]] || { echo "No capyreader build found. Tried: ${CANDIDATE_PACKAGES[*]}" >&2; exit 1; }
fi

activity=$(adb shell cmd package resolve-activity --brief "$package" | tr -d '\r' | tail -1)
[[ "$activity" == */* ]] || { echo "Could not resolve launcher activity for $package: $activity" >&2; exit 1; }

echo "Force-stopping $package..."
adb shell am force-stop "$package"

echo "Launching $activity to trigger refreshAll on startup..."
adb shell am start -n "$activity"
File renamed without changes.
Loading