= listOf()
+)
+
+@Stable
+data class StateChange(
+ val item: Item,
+ val starChange: Boolean = false,
+ val readChange: Boolean = false
+) {
+
+ val itemId: Int
+ get() = item.id
+}
diff --git a/app/src/main/java/com/readrops/app/item/ItemScreenPage.kt b/app/src/main/java/com/readrops/app/item/ItemScreenPage.kt
new file mode 100644
index 000000000..7582101ac
--- /dev/null
+++ b/app/src/main/java/com/readrops/app/item/ItemScreenPage.kt
@@ -0,0 +1,157 @@
+package com.readrops.app.item
+
+import android.widget.RelativeLayout
+import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.height
+import androidx.compose.foundation.layout.navigationBarsPadding
+import androidx.compose.foundation.layout.offset
+import androidx.compose.foundation.layout.padding
+import androidx.compose.foundation.layout.statusBarsPadding
+import androidx.compose.material.icons.Icons
+import androidx.compose.material.icons.automirrored.filled.ArrowBack
+import androidx.compose.material3.Icon
+import androidx.compose.material3.IconButton
+import androidx.compose.material3.MaterialTheme
+import androidx.compose.material3.Scaffold
+import androidx.compose.material3.SnackbarHost
+import androidx.compose.material3.SnackbarHostState
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.remember
+import androidx.compose.runtime.setValue
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.graphics.toArgb
+import androidx.compose.ui.input.nestedscroll.nestedScroll
+import androidx.compose.ui.unit.IntOffset
+import androidx.compose.ui.viewinterop.AndroidView
+import androidx.core.view.children
+import com.readrops.app.item.components.BackgroundTitle
+import com.readrops.app.item.components.BottomBarState
+import com.readrops.app.item.components.ItemScreenBottomBar
+import com.readrops.app.item.components.SimpleTitle
+import com.readrops.app.item.components.rememberBottomBarNestedScrollConnection
+import com.readrops.app.item.view.ItemNestedScrollView
+import com.readrops.app.item.view.ItemWebView
+import com.readrops.app.util.extensions.displayColor
+import com.readrops.db.pojo.ItemWithFeed
+
+@Composable
+fun ItemScreenPage(
+ itemWithFeed: ItemWithFeed,
+ snackbarHostState: SnackbarHostState,
+ onOpenUrl: (String) -> Unit,
+ onShareItem: () -> Unit,
+ onSetReadState: (Boolean) -> Unit,
+ onSetStarState: (Boolean) -> Unit,
+ onOpenImageDialog: (String) -> Unit,
+ onPop: () -> Unit,
+ modifier: Modifier = Modifier
+) {
+ val item = itemWithFeed.item
+
+ val backgroundColor = MaterialTheme.colorScheme.background
+ val onBackgroundColor = MaterialTheme.colorScheme.onBackground
+
+ val accentColor = itemWithFeed.displayColor(MaterialTheme.colorScheme.background.toArgb())
+
+ val nestedScrollConnection = rememberBottomBarNestedScrollConnection()
+ var refreshAndroidView by remember { mutableStateOf(true) }
+ var isScrollable by remember { mutableStateOf(true) }
+
+ Scaffold(
+ modifier = modifier.nestedScroll(nestedScrollConnection)
+ .navigationBarsPadding(),
+ snackbarHost = { SnackbarHost(snackbarHostState) },
+ bottomBar = {
+ ItemScreenBottomBar(
+ state = BottomBarState(
+ isRead = itemWithFeed.isRead,
+ isStarred = itemWithFeed.isStarred,
+ isOpenUrlVisible = !item.link.isNullOrEmpty()
+ ),
+ accentColor = accentColor,
+ modifier = Modifier
+ .navigationBarsPadding()
+ .height(nestedScrollConnection.bottomBarHeight)
+ .offset {
+ if (isScrollable) {
+ IntOffset(
+ x = 0,
+ y = -nestedScrollConnection.bottomBarOffset
+ )
+ } else {
+ IntOffset(0, 0)
+ }
+ },
+ onShare = onShareItem,
+ onOpenUrl = { onOpenUrl(item.link!!) },
+ onChangeReadState = onSetReadState,
+ onChangeStarState = onSetStarState
+ )
+ }
+ ) { paddingValues ->
+ Box(
+ modifier = Modifier.padding(paddingValues)
+ ) {
+ AndroidView(
+ factory = { context ->
+ ItemNestedScrollView(
+ context = context,
+ useBackgroundTitle = item.imageLink != null,
+ onGlobalLayoutListener = { viewHeight, contentHeight ->
+ isScrollable = viewHeight - contentHeight < 0
+ },
+ onUrlClick = { url -> onOpenUrl(url) },
+ onImageLongPress = { url -> onOpenImageDialog(url) }
+ ) {
+ if (item.imageLink != null) {
+ BackgroundTitle(
+ itemWithFeed = itemWithFeed,
+ onClickBack = onPop
+ )
+ } else {
+ Box {
+ IconButton(
+ onClick = onPop,
+ modifier = Modifier
+ .statusBarsPadding()
+ .align(Alignment.TopStart)
+ ) {
+ Icon(
+ imageVector = Icons.AutoMirrored.Default.ArrowBack,
+ contentDescription = null,
+ )
+ }
+
+ SimpleTitle(
+ itemWithFeed = itemWithFeed,
+ titleColor = accentColor,
+ onBackgroundColor = MaterialTheme.colorScheme.onBackground,
+ bottomPadding = true
+ )
+ }
+ }
+ }
+ },
+ update = { nestedScrollView ->
+ if (refreshAndroidView) {
+ val relativeLayout =
+ (nestedScrollView.children.toList().first() as RelativeLayout)
+ val webView = relativeLayout.children.toList()[1] as ItemWebView
+
+ webView.loadText(
+ itemWithFeed = itemWithFeed,
+ accentColor = accentColor,
+ backgroundColor = backgroundColor,
+ onBackgroundColor = onBackgroundColor
+ )
+
+ refreshAndroidView = false
+ }
+ }
+ )
+ }
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/readrops/app/item/components/BackgroundTitle.kt b/app/src/main/java/com/readrops/app/item/components/BackgroundTitle.kt
new file mode 100644
index 000000000..f75f41481
--- /dev/null
+++ b/app/src/main/java/com/readrops/app/item/components/BackgroundTitle.kt
@@ -0,0 +1,93 @@
+package com.readrops.app.item.components
+
+import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.IntrinsicSize
+import androidx.compose.foundation.layout.fillMaxSize
+import androidx.compose.foundation.layout.height
+import androidx.compose.foundation.layout.statusBarsPadding
+import androidx.compose.foundation.shape.RoundedCornerShape
+import androidx.compose.material.icons.Icons
+import androidx.compose.material.icons.automirrored.filled.ArrowBack
+import androidx.compose.material3.Icon
+import androidx.compose.material3.IconButton
+import androidx.compose.material3.Surface
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.layout.ContentScale
+import androidx.compose.ui.res.painterResource
+import androidx.compose.ui.unit.dp
+import coil3.compose.AsyncImage
+import com.readrops.app.R
+import com.readrops.app.timelime.components.itemWithFeed
+import com.readrops.app.util.DefaultPreview
+import com.readrops.app.util.theme.MediumSpacer
+import com.readrops.app.util.theme.ReadropsTheme
+import com.readrops.db.pojo.ItemWithFeed
+
+@Composable
+fun BackgroundTitle(
+ itemWithFeed: ItemWithFeed,
+ onClickBack: () -> Unit,
+) {
+ val onScrimColor = Color.White.copy(alpha = 0.85f)
+
+ Surface(
+ shape = RoundedCornerShape(
+ bottomStart = 24.dp,
+ bottomEnd = 24.dp
+ ),
+ modifier = Modifier.height(IntrinsicSize.Max)
+ ) {
+ AsyncImage(
+ model = itemWithFeed.item.imageLink,
+ contentDescription = null,
+ contentScale = ContentScale.Crop,
+ error = painterResource(id = R.drawable.ic_broken_image),
+ modifier = Modifier
+ .fillMaxSize()
+ )
+
+ Surface(
+ color = Color.Black.copy(alpha = 0.6f),
+ modifier = Modifier
+ .fillMaxSize()
+ ) {
+ Box {
+ IconButton(
+ onClick = onClickBack,
+ modifier = Modifier
+ .statusBarsPadding()
+ .align(Alignment.TopStart)
+ ) {
+ Icon(
+ imageVector = Icons.AutoMirrored.Default.ArrowBack,
+ contentDescription = null,
+ tint = Color.White
+ )
+ }
+
+ SimpleTitle(
+ itemWithFeed = itemWithFeed,
+ titleColor = onScrimColor,
+ onBackgroundColor = onScrimColor,
+ bottomPadding = true
+ )
+ }
+ }
+ }
+
+ MediumSpacer()
+}
+
+@DefaultPreview
+@Composable
+private fun BackgroundTitlePreview() {
+ ReadropsTheme {
+ BackgroundTitle(
+ itemWithFeed = itemWithFeed,
+ onClickBack = {}
+ )
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/readrops/app/item/components/BottomBarNestedScrollConnection.kt b/app/src/main/java/com/readrops/app/item/components/BottomBarNestedScrollConnection.kt
new file mode 100644
index 000000000..319143ff0
--- /dev/null
+++ b/app/src/main/java/com/readrops/app/item/components/BottomBarNestedScrollConnection.kt
@@ -0,0 +1,38 @@
+package com.readrops.app.item.components
+
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.mutableIntStateOf
+import androidx.compose.runtime.remember
+import androidx.compose.runtime.setValue
+import androidx.compose.ui.geometry.Offset
+import androidx.compose.ui.input.nestedscroll.NestedScrollConnection
+import androidx.compose.ui.input.nestedscroll.NestedScrollSource
+import androidx.compose.ui.platform.LocalDensity
+import androidx.compose.ui.unit.Density
+import androidx.compose.ui.unit.Dp
+import androidx.compose.ui.unit.dp
+import kotlin.math.roundToInt
+
+@Composable
+fun rememberBottomBarNestedScrollConnection(density: Density = LocalDensity.current) =
+ remember { BottomBarNestedScrollConnection(density) }
+
+class BottomBarNestedScrollConnection(
+ density: Density,
+ val bottomBarHeight: Dp = 64.dp,
+) : NestedScrollConnection {
+
+ private val bottomBarHeightPx = with(density) { bottomBarHeight.roundToPx().toFloat() }
+
+ var bottomBarOffset: Int by mutableIntStateOf(0)
+ private set
+
+ override fun onPreScroll(available: Offset, source: NestedScrollSource): Offset {
+ val delta = available.y
+ val newOffset = bottomBarOffset.toFloat() + delta
+ bottomBarOffset = newOffset.coerceIn(-bottomBarHeightPx, 0f).roundToInt()
+
+ return Offset.Zero
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/readrops/app/item/ItemScreenBottomBar.kt b/app/src/main/java/com/readrops/app/item/components/ItemScreenBottomBar.kt
similarity index 64%
rename from app/src/main/java/com/readrops/app/item/ItemScreenBottomBar.kt
rename to app/src/main/java/com/readrops/app/item/components/ItemScreenBottomBar.kt
index c15af3e0a..b60a96452 100644
--- a/app/src/main/java/com/readrops/app/item/ItemScreenBottomBar.kt
+++ b/app/src/main/java/com/readrops/app/item/components/ItemScreenBottomBar.kt
@@ -1,4 +1,4 @@
-package com.readrops.app.item
+package com.readrops.app.item.components
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Row
@@ -16,12 +16,15 @@ import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.toArgb
import androidx.compose.ui.res.painterResource
import com.readrops.app.R
-import com.readrops.app.util.FeedColors
+import com.readrops.app.util.DefaultPreview
+import com.readrops.app.util.extensions.canDisplayOnBackground
+import com.readrops.app.util.theme.ReadropsTheme
import com.readrops.app.util.theme.spacing
data class BottomBarState(
val isRead: Boolean = false,
- val isStarred: Boolean = false
+ val isStarred: Boolean = false,
+ val isOpenUrlVisible: Boolean = true
)
@Composable
@@ -34,10 +37,9 @@ fun ItemScreenBottomBar(
onChangeStarState: (Boolean) -> Unit,
modifier: Modifier = Modifier
) {
- val tint = if (FeedColors.isColorDark(accentColor.toArgb()))
- Color.White
- else
- Color.Black
+ val onAccentColor =
+ if (Color.White.toArgb().canDisplayOnBackground(accentColor.toArgb(), threshold = 2.5f))
+ Color.White else Color.Black
Surface(
color = accentColor,
@@ -56,7 +58,7 @@ fun ItemScreenBottomBar(
R.drawable.ic_remove_done
else R.drawable.ic_done_all
),
- tint = tint,
+ tint = onAccentColor,
contentDescription = null
)
}
@@ -70,7 +72,7 @@ fun ItemScreenBottomBar(
R.drawable.ic_star
else R.drawable.ic_star_outline
),
- tint = tint,
+ tint = onAccentColor,
contentDescription = null
)
}
@@ -80,20 +82,40 @@ fun ItemScreenBottomBar(
) {
Icon(
imageVector = Icons.Default.Share,
- tint = tint,
+ tint = onAccentColor,
contentDescription = null
)
}
- IconButton(
- onClick = onOpenUrl
- ) {
- Icon(
- painter = painterResource(id = R.drawable.ic_open_in_browser),
- tint = tint,
- contentDescription = null
- )
+ if (state.isOpenUrlVisible) {
+ IconButton(
+ onClick = onOpenUrl
+ ) {
+ Icon(
+ painter = painterResource(id = R.drawable.ic_open_in_browser),
+ tint = onAccentColor,
+ contentDescription = null
+ )
+ }
}
}
}
}
+
+@DefaultPreview
+@Composable
+private fun ItemScreenBottomBarPreview() {
+ ReadropsTheme {
+ ItemScreenBottomBar(
+ state = BottomBarState(
+ isRead = false,
+ isStarred = false
+ ),
+ accentColor = MaterialTheme.colorScheme.primary,
+ onShare = {},
+ onOpenUrl = {},
+ onChangeReadState = {},
+ onChangeStarState = {},
+ )
+ }
+}
diff --git a/app/src/main/java/com/readrops/app/item/components/SimpleTitle.kt b/app/src/main/java/com/readrops/app/item/components/SimpleTitle.kt
new file mode 100644
index 000000000..c23b67c4a
--- /dev/null
+++ b/app/src/main/java/com/readrops/app/item/components/SimpleTitle.kt
@@ -0,0 +1,117 @@
+package com.readrops.app.item.components
+
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.fillMaxWidth
+import androidx.compose.foundation.layout.padding
+import androidx.compose.foundation.shape.CircleShape
+import androidx.compose.material3.MaterialTheme
+import androidx.compose.material3.Text
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.draw.clip
+import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.graphics.toArgb
+import androidx.compose.ui.res.painterResource
+import androidx.compose.ui.res.stringResource
+import androidx.compose.ui.text.style.TextAlign
+import androidx.compose.ui.unit.dp
+import com.readrops.app.R
+import com.readrops.app.timelime.components.itemWithFeed
+import com.readrops.app.util.DefaultPreview
+import com.readrops.app.util.components.FeedIcon
+import com.readrops.app.util.components.IconText
+import com.readrops.app.util.extensions.displayColor
+import com.readrops.app.util.theme.ReadropsTheme
+import com.readrops.app.util.theme.ShortSpacer
+import com.readrops.app.util.theme.spacing
+import com.readrops.db.pojo.ItemWithFeed
+import com.readrops.db.util.DateUtils
+import kotlin.math.roundToInt
+
+@Composable
+fun SimpleTitle(
+ itemWithFeed: ItemWithFeed,
+ titleColor: Color,
+ onBackgroundColor: Color,
+ bottomPadding: Boolean,
+) {
+ val item = itemWithFeed.item
+ val spacing = MaterialTheme.spacing.mediumSpacing
+
+ Column(
+ horizontalAlignment = Alignment.CenterHorizontally,
+ modifier = Modifier
+ .fillMaxWidth()
+ .padding(
+ start = spacing,
+ end = spacing,
+ top = spacing,
+ bottom = if (bottomPadding) spacing else 0.dp
+ )
+ ) {
+ FeedIcon(
+ iconUrl = itemWithFeed.feedIconUrl,
+ name = itemWithFeed.feedName,
+ size = 48.dp,
+ modifier = Modifier.clip(CircleShape)
+ )
+
+ ShortSpacer()
+
+ Text(
+ text = itemWithFeed.feedName,
+ style = MaterialTheme.typography.labelLarge,
+ color = onBackgroundColor,
+ textAlign = TextAlign.Center
+ )
+
+ ShortSpacer()
+
+ Text(
+ text = item.title!!,
+ style = MaterialTheme.typography.headlineMedium,
+ color = titleColor,
+ textAlign = TextAlign.Center,
+ modifier = Modifier.align(Alignment.CenterHorizontally)
+ )
+
+ if (item.author != null) {
+ ShortSpacer()
+
+ IconText(
+ icon = painterResource(id = R.drawable.ic_person),
+ text = itemWithFeed.item.author!!,
+ style = MaterialTheme.typography.labelMedium,
+ color = onBackgroundColor,
+ tint = itemWithFeed.displayColor(MaterialTheme.colorScheme.background.toArgb())
+ )
+ }
+
+ ShortSpacer()
+
+ val readTime = if (item.readTime > 1) {
+ stringResource(id = R.string.read_time, item.readTime.roundToInt())
+ } else {
+ stringResource(id = R.string.read_time_lower_than_1)
+ }
+ Text(
+ text = "${DateUtils.formattedDateByLocal(item.pubDate!!)} ${stringResource(id = R.string.interpoint)} $readTime",
+ style = MaterialTheme.typography.labelMedium,
+ color = onBackgroundColor
+ )
+ }
+}
+
+@DefaultPreview
+@Composable
+private fun SimpleTitlePreview() {
+ ReadropsTheme {
+ SimpleTitle(
+ itemWithFeed = itemWithFeed,
+ titleColor = MaterialTheme.colorScheme.primary,
+ onBackgroundColor = MaterialTheme.colorScheme.onBackground,
+ bottomPadding = true
+ )
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/readrops/app/item/view/ItemNestedScrollView.kt b/app/src/main/java/com/readrops/app/item/view/ItemNestedScrollView.kt
index f3755adf9..c6ece9937 100644
--- a/app/src/main/java/com/readrops/app/item/view/ItemNestedScrollView.kt
+++ b/app/src/main/java/com/readrops/app/item/view/ItemNestedScrollView.kt
@@ -22,6 +22,7 @@ class ItemNestedScrollView(
addView(
RelativeLayout(context).apply {
ViewCompat.setNestedScrollingEnabled(this, true)
+ descendantFocusability = FOCUS_BLOCK_DESCENDANTS
val composeView = ComposeView(context).apply {
id = 1
diff --git a/app/src/main/java/com/readrops/app/item/view/ItemWebView.kt b/app/src/main/java/com/readrops/app/item/view/ItemWebView.kt
index edc5f7333..bae220721 100644
--- a/app/src/main/java/com/readrops/app/item/view/ItemWebView.kt
+++ b/app/src/main/java/com/readrops/app/item/view/ItemWebView.kt
@@ -2,11 +2,13 @@ package com.readrops.app.item.view
import android.annotation.SuppressLint
import android.content.Context
+import android.text.SpannedString
import android.util.AttributeSet
import android.webkit.WebView
import android.webkit.WebViewClient
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.toArgb
+import androidx.core.text.HtmlCompat
import androidx.core.text.layoutDirection
import com.readrops.app.R
import com.readrops.app.util.Utils
@@ -78,17 +80,22 @@ class ItemWebView(
}
private fun formatText(itemWithFeed: ItemWithFeed): String {
- return if (itemWithFeed.item.text != null) {
- val document = if (itemWithFeed.websiteUrl != null) Jsoup.parse(
- Parser.unescapeEntities(itemWithFeed.item.text, false), itemWithFeed.websiteUrl
- ) else Jsoup.parse(
- Parser.unescapeEntities(itemWithFeed.item.text, false)
- )
-
- document.select("div,span").forEach { it.clearAttributes() }
- return document.body().html()
+ val text = itemWithFeed.item.text ?: return ""
+ val unescapedText = Parser.unescapeEntities(text, false)
+ val document = if (itemWithFeed.websiteUrl != null) {
+ Jsoup.parse(unescapedText, itemWithFeed.websiteUrl!!)
+ } else {
+ Jsoup.parse(unescapedText)
+ }
+ // If body has no tags or all tags are unknown (and therefore likely not HTML tags at all),
+ // treat the whole thing as plain text and convert it to HTML turning newlines into
/ tags
+ val body = document.body()
+ val isPlainText = body.stream().skip(1).allMatch { !it.tag().isKnownTag }
+ return if (isPlainText) {
+ HtmlCompat.toHtml(SpannedString(unescapedText), HtmlCompat.TO_HTML_PARAGRAPH_LINES_CONSECUTIVE)
} else {
- ""
+ body.select("div,span").forEach { it.clearAttributes() }
+ body.html()
}
}
}
\ No newline at end of file
diff --git a/app/src/main/java/com/readrops/app/more/MoreTab.kt b/app/src/main/java/com/readrops/app/more/MoreTab.kt
index 4a345a6ff..f5e40db8f 100644
--- a/app/src/main/java/com/readrops/app/more/MoreTab.kt
+++ b/app/src/main/java/com/readrops/app/more/MoreTab.kt
@@ -11,6 +11,7 @@ import androidx.compose.foundation.layout.statusBarsPadding
import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton
import androidx.compose.material3.MaterialTheme
+import androidx.compose.material3.Surface
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
@@ -31,10 +32,11 @@ import cafe.adriel.voyager.navigator.tab.TabOptions
import com.readrops.app.BuildConfig
import com.readrops.app.R
import com.readrops.app.account.selection.adaptiveIconPainterResource
+import com.readrops.app.more.debug.DebugScreen
import com.readrops.app.more.preferences.PreferencesScreen
import com.readrops.app.util.components.IconText
import com.readrops.app.util.components.SelectableIconText
-import com.readrops.app.util.openUrl
+import com.readrops.app.util.extensions.openUrl
import com.readrops.app.util.theme.LargeSpacer
import com.readrops.app.util.theme.MediumSpacer
import com.readrops.app.util.theme.ShortSpacer
@@ -64,115 +66,130 @@ object MoreTab : Tab, KoinComponent {
)
}
- Column(
- horizontalAlignment = Alignment.CenterHorizontally,
- modifier = Modifier
- .fillMaxSize()
- .statusBarsPadding()
- ) {
- LargeSpacer()
-
- Image(
- painter = adaptiveIconPainterResource(id = R.mipmap.ic_launcher),
- contentDescription = null,
- modifier = Modifier.size(64.dp)
- )
-
- MediumSpacer()
-
- Text(
- text = stringResource(R.string.app_name),
- style = MaterialTheme.typography.titleLarge
- )
-
- ShortSpacer()
-
- IconText(
- text = if (BuildConfig.DEBUG) {
- "v${BuildConfig.VERSION_NAME} (${BuildConfig.VERSION_CODE})"
- } else {
- "v${BuildConfig.VERSION_NAME}"
- },
- icon = painterResource(id = R.drawable.ic_version),
- style = MaterialTheme.typography.labelLarge
- )
-
- ShortSpacer()
-
- Text(
- text = stringResource(id = R.string.app_licence),
- style = MaterialTheme.typography.labelSmall,
- color = MaterialTheme.colorScheme.onSurfaceVariant
- )
-
- ShortSpacer()
-
- Row(
- verticalAlignment = Alignment.CenterVertically,
- horizontalArrangement = Arrangement.Center,
- modifier = Modifier.fillMaxWidth()
+ Surface(color = MaterialTheme.colorScheme.background) {
+ Column(
+ horizontalAlignment = Alignment.CenterHorizontally,
+ modifier = Modifier
+ .fillMaxSize()
+ .statusBarsPadding()
) {
- IconButton(
- onClick = { context.openUrl(context.getString(R.string.app_url)) }
+ LargeSpacer()
+
+ Image(
+ painter = adaptiveIconPainterResource(id = R.mipmap.ic_launcher),
+ contentDescription = null,
+ modifier = Modifier.size(64.dp)
+ )
+
+ MediumSpacer()
+
+ Text(
+ text = stringResource(R.string.app_name),
+ style = MaterialTheme.typography.titleLarge,
+ color = MaterialTheme.colorScheme.onBackground
+ )
+
+ ShortSpacer()
+
+ IconText(
+ text = if (BuildConfig.DEBUG) {
+ "v${BuildConfig.VERSION_NAME} (${BuildConfig.VERSION_CODE})"
+ } else {
+ "v${BuildConfig.VERSION_NAME}"
+ },
+ icon = painterResource(id = R.drawable.ic_version),
+ style = MaterialTheme.typography.labelLarge
+ )
+
+ ShortSpacer()
+
+ Text(
+ text = stringResource(id = R.string.app_licence),
+ style = MaterialTheme.typography.labelSmall,
+ color = MaterialTheme.colorScheme.onSurfaceVariant
+ )
+
+ ShortSpacer()
+
+ Row(
+ verticalAlignment = Alignment.CenterVertically,
+ horizontalArrangement = Arrangement.Center,
+ modifier = Modifier.fillMaxWidth()
) {
- Icon(
- painter = painterResource(id = R.drawable.ic_github),
- contentDescription = null,
- modifier = Modifier.size(24.dp)
- )
+ IconButton(
+ onClick = { context.openUrl(context.getString(R.string.app_url)) }
+ ) {
+ Icon(
+ painter = painterResource(id = R.drawable.ic_github),
+ contentDescription = null,
+ modifier = Modifier.size(24.dp)
+ )
+ }
+
+ IconButton(
+ onClick = { context.openUrl(context.getString(R.string.changelog_url)) }
+ ) {
+ Icon(
+ painter = painterResource(id = R.drawable.ic_changelog),
+ contentDescription = null,
+ modifier = Modifier.size(24.dp)
+ )
+ }
+
+ IconButton(
+ onClick = { context.openUrl(context.getString(R.string.app_issues_url)) }
+ ) {
+ Icon(
+ painter = painterResource(id = R.drawable.ic_bug_report),
+ contentDescription = null
+ )
+ }
}
- IconButton(
- onClick = { context.openUrl(context.getString(R.string.changelog_url)) }
- ) {
- Icon(
- painter = painterResource(id = R.drawable.ic_changelog),
- contentDescription = null,
- modifier = Modifier.size(24.dp)
- )
- }
-
- IconButton(
- onClick = { context.openUrl(context.getString(R.string.app_issues_url)) }
- ) {
- Icon(
- painter = painterResource(id = R.drawable.ic_bug_report),
- contentDescription = null
+ MediumSpacer()
+
+ SelectableIconText(
+ icon = painterResource(id = R.drawable.ic_settings),
+ text = stringResource(R.string.settings),
+ style = MaterialTheme.typography.titleMedium.copy(fontWeight = FontWeight.Normal),
+ spacing = MaterialTheme.spacing.largeSpacing,
+ padding = MaterialTheme.spacing.mediumSpacing,
+ tint = MaterialTheme.colorScheme.primary,
+ onClick = { navigator.push(PreferencesScreen()) }
+ )
+
+ SelectableIconText(
+ icon = painterResource(id = R.drawable.ic_library),
+ text = stringResource(id = R.string.open_source_libraries),
+ style = MaterialTheme.typography.titleMedium.copy(fontWeight = FontWeight.Normal),
+ spacing = MaterialTheme.spacing.largeSpacing,
+ padding = MaterialTheme.spacing.mediumSpacing,
+ tint = MaterialTheme.colorScheme.primary,
+ onClick = { navigator.push(AboutLibrariesScreen()) }
+ )
+
+ SelectableIconText(
+ icon = painterResource(id = R.drawable.ic_donation),
+ text = stringResource(id = R.string.make_donation),
+ style = MaterialTheme.typography.titleMedium.copy(fontWeight = FontWeight.Normal),
+ spacing = MaterialTheme.spacing.largeSpacing,
+ padding = MaterialTheme.spacing.mediumSpacing,
+ tint = MaterialTheme.colorScheme.primary,
+ onClick = { showDonationDialog = true }
+ )
+
+ if (BuildConfig.DEBUG) {
+ SelectableIconText(
+ icon = painterResource(id = R.drawable.ic_bug),
+ text = "Debug",
+ style = MaterialTheme.typography.titleMedium.copy(fontWeight = FontWeight.Normal),
+ spacing = MaterialTheme.spacing.largeSpacing,
+ padding = MaterialTheme.spacing.mediumSpacing,
+ tint = MaterialTheme.colorScheme.primary,
+ onClick = { navigator.push(DebugScreen()) }
)
}
}
-
- MediumSpacer()
-
- SelectableIconText(
- icon = painterResource(id = R.drawable.ic_settings),
- text = stringResource(R.string.settings),
- style = MaterialTheme.typography.titleMedium.copy(fontWeight = FontWeight.Normal),
- spacing = MaterialTheme.spacing.largeSpacing,
- padding = MaterialTheme.spacing.mediumSpacing,
- tint = MaterialTheme.colorScheme.primary,
- onClick = { navigator.push(PreferencesScreen()) }
- )
-
- SelectableIconText(
- icon = painterResource(id = R.drawable.ic_library),
- text = stringResource(id = R.string.open_source_libraries),
- style = MaterialTheme.typography.titleMedium.copy(fontWeight = FontWeight.Normal),
- spacing = MaterialTheme.spacing.largeSpacing,
- padding = MaterialTheme.spacing.mediumSpacing,
- tint = MaterialTheme.colorScheme.primary,
- onClick = { navigator.push(AboutLibrariesScreen()) }
- )
-
- SelectableIconText(
- icon = painterResource(id = R.drawable.ic_donation),
- text = stringResource(id = R.string.make_donation),
- style = MaterialTheme.typography.titleMedium.copy(fontWeight = FontWeight.Normal),
- spacing = MaterialTheme.spacing.largeSpacing,
- padding = MaterialTheme.spacing.mediumSpacing,
- tint = MaterialTheme.colorScheme.primary,
- onClick = { showDonationDialog = true }
- )
}
}
}
\ No newline at end of file
diff --git a/app/src/main/java/com/readrops/app/more/debug/DebugScreen.kt b/app/src/main/java/com/readrops/app/more/debug/DebugScreen.kt
new file mode 100644
index 000000000..1defd5413
--- /dev/null
+++ b/app/src/main/java/com/readrops/app/more/debug/DebugScreen.kt
@@ -0,0 +1,115 @@
+package com.readrops.app.more.debug
+
+import android.annotation.SuppressLint
+import android.app.PendingIntent
+import android.content.Intent
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.padding
+import androidx.compose.material.icons.Icons
+import androidx.compose.material.icons.automirrored.filled.ArrowBack
+import androidx.compose.material3.ExperimentalMaterial3Api
+import androidx.compose.material3.Icon
+import androidx.compose.material3.IconButton
+import androidx.compose.material3.Scaffold
+import androidx.compose.material3.Text
+import androidx.compose.material3.TopAppBar
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.rememberCoroutineScope
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.platform.LocalContext
+import androidx.compose.ui.res.stringResource
+import androidx.core.app.NotificationCompat
+import androidx.core.app.NotificationCompat.Builder
+import androidx.core.app.NotificationManagerCompat
+import cafe.adriel.voyager.navigator.LocalNavigator
+import cafe.adriel.voyager.navigator.currentOrThrow
+import com.readrops.app.MainActivity
+import com.readrops.app.R
+import com.readrops.app.ReadropsApp
+import com.readrops.app.more.preferences.components.BasePreference
+import com.readrops.app.more.preferences.components.PreferenceHeader
+import com.readrops.app.sync.SyncWorker.Companion.ACCOUNT_ID_KEY
+import com.readrops.app.sync.SyncWorker.Companion.ITEM_ID_KEY
+import com.readrops.app.sync.SyncWorker.Companion.SYNC_RESULT_NOTIFICATION_ID
+import com.readrops.app.util.components.AndroidScreen
+import com.readrops.db.Database
+import kotlinx.coroutines.flow.first
+import kotlinx.coroutines.launch
+import org.koin.core.component.KoinComponent
+import org.koin.core.component.get
+
+class DebugScreen : AndroidScreen(), KoinComponent {
+
+ @OptIn(ExperimentalMaterial3Api::class)
+ @SuppressLint("MissingPermission")
+ @Composable
+ override fun Content() {
+ val context = LocalContext.current
+ val navigator = LocalNavigator.currentOrThrow
+ val coroutineScope = rememberCoroutineScope()
+
+ Scaffold(
+ topBar = {
+ TopAppBar(
+ title = { Text("Debug") },
+ navigationIcon = {
+ IconButton(
+ onClick = { navigator.pop() }
+ ) {
+ Icon(
+ imageVector = Icons.AutoMirrored.Default.ArrowBack,
+ contentDescription = null
+ )
+ }
+ }
+ )
+ }
+ ) { paddingValues ->
+ Column(
+ modifier = Modifier.padding(paddingValues)
+ ) {
+ PreferenceHeader(stringResource(R.string.notifications))
+
+ BasePreference(
+ title = "Send notification: Single item from single feed",
+ onClick = {
+ coroutineScope.launch {
+ val database = get()
+
+ val item = database.itemDao().selectFirst()
+ val account = database.accountDao().selectCurrentAccount().first()
+
+ val intent = Intent(context, MainActivity::class.java).apply {
+ putExtra(ACCOUNT_ID_KEY, account!!.id)
+ putExtra(ITEM_ID_KEY, item.id)
+ }
+
+ val notificationBuilder = Builder(context, ReadropsApp.SYNC_CHANNEL_ID)
+ .setContentTitle(item.title)
+ .setContentText("Test notification")
+ .setStyle(
+ NotificationCompat.BigTextStyle().bigText("Test notification")
+ )
+ .setSmallIcon(R.drawable.ic_notifications)
+ .setContentIntent(
+ PendingIntent.getActivity(
+ context,
+ 0,
+ intent,
+ PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE
+ )
+ )
+ .setAutoCancel(true)
+
+ get().notify(
+ SYNC_RESULT_NOTIFICATION_ID,
+ notificationBuilder.build()
+ )
+ }
+ }
+ )
+ }
+ }
+
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/readrops/app/more/preferences/PreferencesScreen.kt b/app/src/main/java/com/readrops/app/more/preferences/PreferencesScreen.kt
index 5bd1e6a9f..a198e9f23 100644
--- a/app/src/main/java/com/readrops/app/more/preferences/PreferencesScreen.kt
+++ b/app/src/main/java/com/readrops/app/more/preferences/PreferencesScreen.kt
@@ -8,7 +8,10 @@ import android.os.PowerManager
import android.provider.Settings
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
+import androidx.compose.foundation.rememberScrollState
+import androidx.compose.foundation.verticalScroll
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.automirrored.filled.ArrowBack
import androidx.compose.material3.ExperimentalMaterial3Api
@@ -27,15 +30,17 @@ import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.stringResource
import androidx.lifecycle.compose.collectAsStateWithLifecycle
-import cafe.adriel.voyager.koin.getScreenModel
+import cafe.adriel.voyager.koin.koinScreenModel
import cafe.adriel.voyager.navigator.LocalNavigator
import cafe.adriel.voyager.navigator.currentOrThrow
import com.readrops.app.R
import com.readrops.app.more.preferences.components.BasePreference
+import com.readrops.app.more.preferences.components.CustomShareIntentTextWidget
import com.readrops.app.more.preferences.components.ListPreferenceWidget
import com.readrops.app.more.preferences.components.PreferenceHeader
import com.readrops.app.more.preferences.components.SwitchPreferenceWidget
import com.readrops.app.sync.SyncWorker
+import com.readrops.app.timelime.components.SwipeAction
import com.readrops.app.util.components.AndroidScreen
import com.readrops.app.util.components.CenteredProgressIndicator
import kotlinx.coroutines.launch
@@ -48,8 +53,9 @@ class PreferencesScreen : AndroidScreen() {
override fun Content() {
val navigator = LocalNavigator.currentOrThrow
val context = LocalContext.current
- val screenModel = getScreenModel()
+ val screenModel = koinScreenModel()
+ val scrollState = rememberScrollState()
val coroutineScope = rememberCoroutineScope()
val snackbarHostState = remember { SnackbarHostState() }
@@ -84,7 +90,12 @@ class PreferencesScreen : AndroidScreen() {
else -> {
val loadedState = (state as PreferencesScreenState.Loaded)
- Column {
+ // a lazyColumn might be necessary in the future
+ Column(
+ modifier = Modifier
+ .fillMaxWidth()
+ .verticalScroll(scrollState)
+ ) {
PreferenceHeader(text = stringResource(id = R.string.global))
ListPreferenceWidget(
@@ -100,8 +111,8 @@ class PreferencesScreen : AndroidScreen() {
)
ListPreferenceWidget(
- preference = loadedState.themeColourScheme.second,
- selectedKey = loadedState.themeColourScheme.first,
+ preference = loadedState.themeColorScheme.second,
+ selectedKey = loadedState.themeColorScheme.first,
entries = mapOf(
"readrops" to stringResource(id = R.string.theme_readrops),
"blackwhite" to stringResource(id = R.string.theme_blackwhite),
@@ -157,6 +168,24 @@ class PreferencesScreen : AndroidScreen() {
PreferenceHeader(text = stringResource(id = R.string.timeline))
+ SwitchPreferenceWidget(
+ preference = loadedState.syncAtLaunchPref.second,
+ isChecked = loadedState.syncAtLaunchPref.first,
+ title = stringResource(R.string.synchronize_at_launch)
+ )
+
+ ListPreferenceWidget(
+ preference = loadedState.mainFilterPref.second,
+ selectedKey = loadedState.mainFilterPref.first,
+ entries = mapOf(
+ "ALL" to stringResource(R.string.articles),
+ "NEW" to stringResource(R.string.new_articles),
+ "STARS" to stringResource(R.string.favorites)
+ ),
+ title = stringResource(R.string.default_category),
+ onValueChange = {}
+ )
+
ListPreferenceWidget(
preference = loadedState.timelineItemSize.second,
selectedKey = loadedState.timelineItemSize.first,
@@ -182,6 +211,30 @@ class PreferencesScreen : AndroidScreen() {
title = stringResource(id = R.string.mark_items_read_on_scroll)
)
+ ListPreferenceWidget(
+ preference = loadedState.swipeToLeft.second,
+ selectedKey = loadedState.swipeToLeft.first,
+ entries = mapOf(
+ SwipeAction.DISABLED.name to stringResource(R.string.disabled),
+ SwipeAction.READ.name to stringResource(R.string.mark_read),
+ SwipeAction.FAVORITE.name to stringResource(R.string.add_to_favorite)
+ ),
+ title = stringResource(R.string.swipe_to_left_action),
+ onValueChange = {}
+ )
+
+ ListPreferenceWidget(
+ preference = loadedState.swipeToRight.second,
+ selectedKey = loadedState.swipeToRight.first,
+ entries = mapOf(
+ SwipeAction.DISABLED.name to stringResource(R.string.disabled),
+ SwipeAction.READ.name to stringResource(R.string.mark_read),
+ SwipeAction.FAVORITE.name to stringResource(R.string.add_to_favorite)
+ ),
+ title = stringResource(R.string.swipe_to_right_action),
+ onValueChange = {}
+ )
+
PreferenceHeader(text = stringResource(id = R.string.item_view))
ListPreferenceWidget(
@@ -194,6 +247,22 @@ class PreferencesScreen : AndroidScreen() {
title = stringResource(id = R.string.open_items_in),
onValueChange = {}
)
+
+ SwitchPreferenceWidget(
+ preference = loadedState.useCustomShareIntentTpl.second,
+ isChecked = loadedState.useCustomShareIntentTpl.first,
+ title = stringResource(id = R.string.use_custom_share_intent_tpl),
+ onValueChanged = screenModel::updateDialog
+ )
+
+ if (loadedState.showDialog) {
+ CustomShareIntentTextWidget(
+ preference = loadedState.customShareIntentTpl.second,
+ template = loadedState.customShareIntentTpl.first,
+ exampleItem = loadedState.exampleItem,
+ onDismiss = { screenModel.updateDialog(false) },
+ )
+ }
}
}
}
diff --git a/app/src/main/java/com/readrops/app/more/preferences/PreferencesScreenModel.kt b/app/src/main/java/com/readrops/app/more/preferences/PreferencesScreenModel.kt
index d488a4309..34a130d47 100644
--- a/app/src/main/java/com/readrops/app/more/preferences/PreferencesScreenModel.kt
+++ b/app/src/main/java/com/readrops/app/more/preferences/PreferencesScreenModel.kt
@@ -1,9 +1,13 @@
package com.readrops.app.more.preferences
+import android.content.Context
import cafe.adriel.voyager.core.model.StateScreenModel
import cafe.adriel.voyager.core.model.screenModelScope
+import com.readrops.app.R
import com.readrops.app.util.Preference
import com.readrops.app.util.Preferences
+import com.readrops.db.Database
+import com.readrops.db.entities.Item
import kotlinx.coroutines.CoroutineDispatcher
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.combine
@@ -13,40 +17,78 @@ import kotlinx.coroutines.launch
typealias PreferenceState = Pair>
class PreferencesScreenModel(
+ database: Database,
+ context: Context,
preferences: Preferences,
dispatcher: CoroutineDispatcher = Dispatchers.IO
) : StateScreenModel(PreferencesScreenState.Loading) {
-
init {
screenModelScope.launch(dispatcher) {
- val flows = listOf(
- preferences.theme.flow,
- preferences.backgroundSynchronization.flow,
- preferences.scrollRead.flow,
- preferences.hideReadFeeds.flow,
- preferences.openLinksWith.flow,
- preferences.timelineItemSize.flow,
- preferences.themeColourScheme.flow
- )
-
- combine(
- flows
- ) { list ->
- PreferencesScreenState.Loaded(
- themePref = (list[0] as String) to preferences.theme,
- backgroundSyncPref = (list[1] as String) to preferences.backgroundSynchronization,
- scrollReadPref = (list[2] as Boolean) to preferences.scrollRead,
- hideReadFeeds = (list[3] as Boolean) to preferences.hideReadFeeds,
- openLinksWith = (list[4] as String) to preferences.openLinksWith,
- timelineItemSize = (list[5] as String) to preferences.timelineItemSize,
- themeColourScheme = (list[6] as String) to preferences.themeColourScheme
+ with(preferences) {
+ val flows = listOf(
+ theme.flow,
+ backgroundSynchronization.flow,
+ scrollRead.flow,
+ hideReadFeeds.flow,
+ openLinksWith.flow,
+ timelineItemSize.flow,
+ mainFilter.flow,
+ synchAtLaunch.flow,
+ useCustomShareIntentTpl.flow,
+ customShareIntentTpl.flow,
+ swipeToLeft.flow,
+ swipeToRight.flow,
+ themeColorScheme.flow
)
- }.collect { theme ->
- mutableState.update { theme }
+
+ combine(
+ flows
+ ) { list ->
+ PreferencesScreenState.Loaded(
+ themePref = (list[0] as String) to theme,
+ backgroundSyncPref = (list[1] as String) to backgroundSynchronization,
+ scrollReadPref = (list[2] as Boolean) to scrollRead,
+ hideReadFeeds = (list[3] as Boolean) to hideReadFeeds,
+ openLinksWith = (list[4] as String) to openLinksWith,
+ timelineItemSize = (list[5] as String) to timelineItemSize,
+ mainFilterPref = (list[6] as String) to mainFilter,
+ syncAtLaunchPref = (list[7] as Boolean) to synchAtLaunch,
+ useCustomShareIntentTpl = (list[8] as Boolean) to useCustomShareIntentTpl,
+ customShareIntentTpl = (list[9] as String) to customShareIntentTpl,
+ swipeToLeft = (list[10] as String) to swipeToLeft,
+ swipeToRight = (list[11] as String) to swipeToRight,
+ themeColorScheme = (list[12] as String) to themeColorScheme,
+ exampleItem = if (database.itemDao().count() > 0) {
+ database.itemDao().selectFirst()
+ } else {
+ Item(
+ title = context.getString(R.string.example_item_title),
+ author = context.getString(R.string.example_item_author),
+ content = context.getString(R.string.example_item_content),
+ link = "https://example.org"
+ )
+ }
+ )
+ }.collect { theme ->
+ mutableState.update { previous ->
+ (previous as? PreferencesScreenState.Loaded)?.let {
+ theme.copy(showDialog = previous.showDialog)
+ } ?: theme
+ }
+ }
}
}
}
+ fun updateDialog(isVisible: Boolean) {
+ if (mutableState.value is PreferencesScreenState.Loaded) {
+ mutableState.update {
+ (mutableState.value as PreferencesScreenState.Loaded).copy(
+ showDialog = isVisible
+ )
+ }
+ }
+ }
}
sealed class PreferencesScreenState {
@@ -60,7 +102,15 @@ sealed class PreferencesScreenState {
val hideReadFeeds: PreferenceState,
val openLinksWith: PreferenceState,
val timelineItemSize: PreferenceState,
- val themeColourScheme: PreferenceState
+ val mainFilterPref: PreferenceState,
+ val syncAtLaunchPref: PreferenceState,
+ val useCustomShareIntentTpl: PreferenceState,
+ val customShareIntentTpl: PreferenceState,
+ val swipeToLeft: PreferenceState,
+ val swipeToRight: PreferenceState,
+ val themeColorScheme: PreferenceState,
+ val exampleItem: Item,
+ val showDialog: Boolean = false
) : PreferencesScreenState()
}
\ No newline at end of file
diff --git a/app/src/main/java/com/readrops/app/more/preferences/components/BasePreference.kt b/app/src/main/java/com/readrops/app/more/preferences/components/BasePreference.kt
index 288f4e26e..1d9afe601 100644
--- a/app/src/main/java/com/readrops/app/more/preferences/components/BasePreference.kt
+++ b/app/src/main/java/com/readrops/app/more/preferences/components/BasePreference.kt
@@ -4,6 +4,7 @@ import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.padding
import androidx.compose.material3.MaterialTheme
@@ -22,7 +23,8 @@ fun BasePreference(
onClick: () -> Unit,
modifier: Modifier = Modifier,
subtitle: String? = null,
- rightComponent: (@Composable () -> Unit)? = null
+ rightComponent: (@Composable () -> Unit)? = null,
+ paddingValues: PaddingValues = PaddingValues(MaterialTheme.spacing.mediumSpacing)
) {
Box(
modifier = modifier.clickable { onClick() }
@@ -30,7 +32,7 @@ fun BasePreference(
Row(
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.SpaceBetween,
- modifier = Modifier.padding(MaterialTheme.spacing.mediumSpacing)
+ modifier = Modifier.padding(paddingValues)
) {
Column(
modifier = Modifier.weight(1f)
diff --git a/app/src/main/java/com/readrops/app/more/preferences/components/CustomShareIntentTextWidget.kt b/app/src/main/java/com/readrops/app/more/preferences/components/CustomShareIntentTextWidget.kt
new file mode 100644
index 000000000..ab77b10b9
--- /dev/null
+++ b/app/src/main/java/com/readrops/app/more/preferences/components/CustomShareIntentTextWidget.kt
@@ -0,0 +1,166 @@
+package com.readrops.app.more.preferences.components
+
+import androidx.compose.foundation.layout.Arrangement
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.Row
+import androidx.compose.material.icons.Icons
+import androidx.compose.material.icons.filled.KeyboardArrowDown
+import androidx.compose.material.icons.filled.KeyboardArrowUp
+import androidx.compose.material3.AlertDialogDefaults
+import androidx.compose.material3.Icon
+import androidx.compose.material3.IconButton
+import androidx.compose.material3.MaterialTheme
+import androidx.compose.material3.Text
+import androidx.compose.material3.TextButton
+import androidx.compose.material3.TextField
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.LaunchedEffect
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.remember
+import androidx.compose.runtime.rememberCoroutineScope
+import androidx.compose.runtime.setValue
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.focus.FocusRequester
+import androidx.compose.ui.focus.focusRequester
+import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.platform.LocalContext
+import androidx.compose.ui.res.stringResource
+import androidx.compose.ui.text.AnnotatedString
+import androidx.compose.ui.text.SpanStyle
+import androidx.compose.ui.text.TextLinkStyles
+import androidx.compose.ui.text.font.FontStyle
+import androidx.compose.ui.text.fromHtml
+import androidx.compose.ui.text.style.TextDecoration
+import com.readrops.app.R
+import com.readrops.app.util.Preference
+import com.readrops.app.util.ShareIntentTextRenderer
+import com.readrops.app.util.theme.MediumSpacer
+import com.readrops.app.util.theme.ShortSpacer
+import com.readrops.db.entities.Item
+import kotlinx.coroutines.launch
+
+@Composable
+fun CustomShareIntentTextWidget(
+ preference: Preference,
+ template: String,
+ exampleItem: Item,
+ onDismiss: () -> Unit,
+) {
+ var localTemplate by remember { mutableStateOf(template) }
+ var generateTemplate by remember { mutableStateOf(false) }
+ val coroutineScope = rememberCoroutineScope()
+ val focusRequester = remember { FocusRequester() }
+ val renderer = remember { ShareIntentTextRenderer(exampleItem) }
+
+ PreferenceBaseDialog(
+ title = stringResource(R.string.use_custom_share_intent_tpl),
+ onDismiss = onDismiss
+ ) {
+ Column(
+ horizontalAlignment = Alignment.Start
+ ) {
+ Text(
+ text = AnnotatedString.fromHtml(
+ stringResource(R.string.use_custom_share_intent_tpl_explenation),
+ linkStyles = TextLinkStyles(
+ style = SpanStyle(
+ textDecoration = TextDecoration.Underline,
+ fontStyle = FontStyle.Italic,
+ color = Color.Blue
+ )
+ )
+ ),
+ style = MaterialTheme.typography.bodyMedium,
+ color = AlertDialogDefaults.textContentColor
+ )
+
+ MediumSpacer()
+
+ TextField(
+ value = (
+ if (generateTemplate) renderer.renderOrError(localTemplate)
+ else localTemplate
+ ),
+ onValueChange = { localTemplate = it },
+ readOnly = generateTemplate,
+ minLines = 3,
+ modifier = Modifier.focusRequester(focusRequester),
+ )
+
+ LaunchedEffect(Unit) {
+ focusRequester.requestFocus()
+ }
+
+ ShortSpacer()
+
+ Row(horizontalArrangement = Arrangement.SpaceEvenly) {
+ TextButton(
+ modifier = Modifier.weight(1f),
+ onClick = {
+ localTemplate = """
+ {{ title|remove_author|capitalize }} — {{ author }}
+
+ {{ url }}
+ """.trimIndent()
+ },
+ ) {
+ Text(text = stringResource(R.string.try_the_default_template))
+ }
+
+ TextButton(
+ modifier = Modifier.weight(1f),
+ onClick = { generateTemplate = !generateTemplate },
+ ) {
+ Text(
+ text = (
+ if (generateTemplate) stringResource(R.string.edit_template)
+ else stringResource(R.string.render_template)
+ )
+ )
+ }
+ }
+
+ MediumSpacer()
+
+ Text(
+ AnnotatedString.fromHtml(
+ stringResource(
+ R.string.example_item_explanation,
+ renderer.context.keys.joinToString(transform = { "$it" }),
+ renderer.documentation
+ ),
+ linkStyles = TextLinkStyles(
+ style = SpanStyle(
+ textDecoration = TextDecoration.Underline,
+ fontStyle = FontStyle.Italic,
+ color = Color.Blue
+ )
+ )
+ ),
+ style = MaterialTheme.typography.bodyMedium,
+ color = AlertDialogDefaults.textContentColor
+ )
+
+ MediumSpacer()
+
+ Row(modifier = Modifier.align(Alignment.End)) {
+ TextButton(onClick = onDismiss) {
+ Text(text = stringResource(id = R.string.back))
+ }
+
+ TextButton(
+ onClick = {
+ coroutineScope.launch {
+ preference.write(localTemplate)
+ onDismiss()
+ }
+ },
+ ) {
+ Text(text = stringResource(id = R.string.save))
+ }
+ }
+ }
+ }
+}
diff --git a/app/src/main/java/com/readrops/app/more/preferences/components/RadioButtonPreferenceDialog.kt b/app/src/main/java/com/readrops/app/more/preferences/components/RadioButtonPreferenceDialog.kt
index 669af0880..482243a60 100644
--- a/app/src/main/java/com/readrops/app/more/preferences/components/RadioButtonPreferenceDialog.kt
+++ b/app/src/main/java/com/readrops/app/more/preferences/components/RadioButtonPreferenceDialog.kt
@@ -6,7 +6,10 @@ import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.fillMaxWidth
+import androidx.compose.foundation.layout.imePadding
import androidx.compose.foundation.layout.padding
+import androidx.compose.foundation.rememberScrollState
+import androidx.compose.foundation.verticalScroll
import androidx.compose.material3.AlertDialogDefaults
import androidx.compose.material3.BasicAlertDialog
import androidx.compose.material3.ExperimentalMaterial3Api
@@ -19,6 +22,7 @@ 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.window.DialogProperties
import com.readrops.app.R
import com.readrops.app.util.theme.LargeSpacer
import com.readrops.app.util.theme.MediumSpacer
@@ -32,8 +36,12 @@ fun PreferenceBaseDialog(
modifier: Modifier = Modifier,
content: @Composable () -> Unit
) {
+ val scrollState = rememberScrollState()
+
BasicAlertDialog(
- onDismissRequest = onDismiss
+ onDismissRequest = onDismiss,
+ modifier = Modifier.imePadding(),
+ properties = DialogProperties(decorFitsSystemWindows = false),
) {
Surface(
tonalElevation = AlertDialogDefaults.TonalElevation,
@@ -44,6 +52,7 @@ fun PreferenceBaseDialog(
horizontalAlignment = Alignment.Start,
modifier = modifier
.padding(MaterialTheme.spacing.largeSpacing)
+ .verticalScroll(scrollState)
) {
Text(
text = title,
@@ -93,7 +102,7 @@ fun RadioButtonPreferenceDialog(
onClick = onDismiss,
modifier = Modifier.align(Alignment.End)
) {
- Text(text = stringResource(id = R.string.cancel))
+ Text(text = stringResource(id = R.string.back))
}
}
}
diff --git a/app/src/main/java/com/readrops/app/more/preferences/components/SwitchPreferenceWidget.kt b/app/src/main/java/com/readrops/app/more/preferences/components/SwitchPreferenceWidget.kt
index 1e36b5c0a..48c398bb7 100644
--- a/app/src/main/java/com/readrops/app/more/preferences/components/SwitchPreferenceWidget.kt
+++ b/app/src/main/java/com/readrops/app/more/preferences/components/SwitchPreferenceWidget.kt
@@ -14,25 +14,25 @@ fun SwitchPreferenceWidget(
title: String,
modifier: Modifier = Modifier,
subtitle: String? = null,
+ onValueChanged: ((Boolean) -> Unit)? = null
) {
val coroutineScope = rememberCoroutineScope()
+ fun changeValue(newValue: Boolean) {
+ coroutineScope.launch {
+ preference.write(newValue)
+ onValueChanged?.let { it(newValue) }
+ }
+ }
+
BasePreference(
title = title,
subtitle = subtitle,
- onClick = {
- coroutineScope.launch {
- preference.write(!isChecked)
- }
- },
+ onClick = { changeValue(!isChecked) },
rightComponent = {
Switch(
checked = isChecked,
- onCheckedChange = {
- coroutineScope.launch {
- preference.write(!isChecked)
- }
- }
+ onCheckedChange = ::changeValue
)
},
modifier = modifier
diff --git a/app/src/main/java/com/readrops/app/notifications/NotificationsScreen.kt b/app/src/main/java/com/readrops/app/notifications/NotificationsScreen.kt
index de74cbf7b..9d12d7856 100644
--- a/app/src/main/java/com/readrops/app/notifications/NotificationsScreen.kt
+++ b/app/src/main/java/com/readrops/app/notifications/NotificationsScreen.kt
@@ -31,7 +31,7 @@ import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource
import androidx.lifecycle.compose.collectAsStateWithLifecycle
-import cafe.adriel.voyager.koin.getScreenModel
+import cafe.adriel.voyager.koin.koinScreenModel
import cafe.adriel.voyager.navigator.LocalNavigator
import cafe.adriel.voyager.navigator.currentOrThrow
import com.google.accompanist.permissions.ExperimentalPermissionsApi
@@ -60,7 +60,7 @@ class NotificationsScreen(val account: Account) : AndroidScreen() {
override fun Content() {
val navigator = LocalNavigator.currentOrThrow
val context = LocalContext.current
- val screenModel = getScreenModel { parametersOf(account) }
+ val screenModel = koinScreenModel { parametersOf(account) }
val state by screenModel.state.collectAsStateWithLifecycle()
diff --git a/app/src/main/java/com/readrops/app/repositories/FeverRepository.kt b/app/src/main/java/com/readrops/app/repositories/FeverRepository.kt
index 62c1a2402..37d15cf1d 100644
--- a/app/src/main/java/com/readrops/app/repositories/FeverRepository.kt
+++ b/app/src/main/java/com/readrops/app/repositories/FeverRepository.kt
@@ -25,7 +25,7 @@ class FeverRepository(
val authenticated = feverDataSource.login(account.login!!, account.password!!)
if (authenticated) {
- account.displayedName = account.accountType!!.name
+ account.displayedName = account.type!!.name
} else {
throw LoginFailedException()
}
diff --git a/app/src/main/java/com/readrops/app/repositories/FreshRSSRepository.kt b/app/src/main/java/com/readrops/app/repositories/GReaderRepository.kt
similarity index 91%
rename from app/src/main/java/com/readrops/app/repositories/FreshRSSRepository.kt
rename to app/src/main/java/com/readrops/app/repositories/GReaderRepository.kt
index eb269488c..ceef02fe4 100644
--- a/app/src/main/java/com/readrops/app/repositories/FreshRSSRepository.kt
+++ b/app/src/main/java/com/readrops/app/repositories/GReaderRepository.kt
@@ -2,8 +2,8 @@ package com.readrops.app.repositories
import com.readrops.api.services.Credentials
import com.readrops.api.services.SyncType
-import com.readrops.api.services.freshrss.FreshRSSDataSource
-import com.readrops.api.services.freshrss.FreshRSSSyncData
+import com.readrops.api.services.greader.GReaderDataSource
+import com.readrops.api.services.greader.GReaderSyncData
import com.readrops.api.utils.AuthInterceptor
import com.readrops.app.util.Utils
import com.readrops.db.Database
@@ -13,16 +13,18 @@ import com.readrops.db.entities.Item
import com.readrops.db.entities.ItemState
import com.readrops.db.entities.account.Account
import org.koin.core.component.KoinComponent
+import org.koin.core.component.get
-class FreshRSSRepository(
+class GReaderRepository(
database: Database,
account: Account,
- private val dataSource: FreshRSSDataSource,
+ private val dataSource: GReaderDataSource,
) : BaseRepository(database, account), KoinComponent {
override suspend fun login(account: Account) {
- val authInterceptor = getKoin().get()
- authInterceptor.credentials = Credentials.toCredentials(account)
+ val authInterceptor = get().apply {
+ credentials = Credentials.toCredentials(account)
+ }
account.token = dataSource.login(account.login!!, account.password!!)
// we got the authToken, time to provide it to make real calls
@@ -43,7 +45,7 @@ class FreshRSSRepository(
val itemStateChanges = database.itemStateChangeDao()
.selectItemStateChanges(account.id)
- val syncData = FreshRSSSyncData(
+ val syncData = GReaderSyncData(
readIds = itemStateChanges.filter { it.readChange && it.read }
.map { it.remoteId },
unreadIds = itemStateChanges.filter { it.readChange && !it.read }
@@ -95,7 +97,7 @@ class FreshRSSRepository(
onUpdate(newFeed)
try {
- dataSource.createFeed(account.writeToken!!, newFeed.url!!)
+ dataSource.createFeed(account.writeToken!!, newFeed.url!!, newFeed.remoteFolderId)
} catch (e: Exception) {
errors[newFeed] = e
}
@@ -116,7 +118,7 @@ class FreshRSSRepository(
override suspend fun updateFolder(folder: Folder) {
dataSource.updateFolder(account.writeToken!!, folder.remoteId!!, folder.name!!)
- folder.remoteId = FreshRSSDataSource.FOLDER_PREFIX + folder.name
+ folder.remoteId = GReaderDataSource.FOLDER_PREFIX + folder.name
super.updateFolder(folder)
}
@@ -185,7 +187,7 @@ class FreshRSSRepository(
database.itemStateDao().deleteItemStates(account.id)
database.itemStateDao().insert(unreadIds.map { id ->
- val starred = starredIds.count { starredId -> starredId == id } == 1
+ val starred = starredIds.any { starredId -> starredId == id }
if (starred) {
starredIds.remove(id)
@@ -201,7 +203,7 @@ class FreshRSSRepository(
})
database.itemStateDao().insert(readIds.map { id ->
- val starred = starredIds.count { starredId -> starredId == id } == 1
+ val starred = starredIds.any { starredId -> starredId == id }
if (starred) {
starredIds.remove(id)
}
diff --git a/app/src/main/java/com/readrops/app/repositories/GetFoldersWithFeeds.kt b/app/src/main/java/com/readrops/app/repositories/GetFoldersWithFeeds.kt
index 4127e2ec8..c70d38039 100644
--- a/app/src/main/java/com/readrops/app/repositories/GetFoldersWithFeeds.kt
+++ b/app/src/main/java/com/readrops/app/repositories/GetFoldersWithFeeds.kt
@@ -3,6 +3,7 @@ package com.readrops.app.repositories
import com.readrops.db.Database
import com.readrops.db.entities.Feed
import com.readrops.db.entities.Folder
+import com.readrops.db.entities.OpenIn
import com.readrops.db.filters.MainFilter
import com.readrops.db.queries.FeedUnreadCountQueryBuilder
import com.readrops.db.queries.FoldersAndFeedsQueryBuilder
@@ -19,7 +20,7 @@ class GetFoldersWithFeeds(
useSeparateState: Boolean,
hideReadFeeds: Boolean = false
): Flow