mirror of https://github.com/readrops/Readrops.git
Compare commits
4 Commits
bf7ac41d6e
...
d486bd92f9
Author | SHA1 | Date |
---|---|---|
Shinokuni | d486bd92f9 | |
Shinokuni | 7b644cbc97 | |
Shinokuni | 02a3f82b72 | |
Shinokuni | 91378f0a54 |
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
|
@ -1,41 +1,52 @@
|
||||||
package com.readrops.app.compose.item
|
package com.readrops.app.compose.item
|
||||||
|
|
||||||
import android.util.Base64
|
import android.content.Intent
|
||||||
import android.webkit.WebView
|
import android.net.Uri
|
||||||
import android.webkit.WebViewClient
|
import android.widget.RelativeLayout
|
||||||
import androidx.compose.foundation.background
|
import androidx.compose.foundation.layout.Box
|
||||||
import androidx.compose.foundation.layout.Column
|
import androidx.compose.foundation.layout.Column
|
||||||
import androidx.compose.foundation.layout.IntrinsicSize
|
import androidx.compose.foundation.layout.IntrinsicSize
|
||||||
import androidx.compose.foundation.layout.fillMaxSize
|
import androidx.compose.foundation.layout.fillMaxSize
|
||||||
import androidx.compose.foundation.layout.height
|
import androidx.compose.foundation.layout.height
|
||||||
|
import androidx.compose.foundation.layout.offset
|
||||||
import androidx.compose.foundation.layout.padding
|
import androidx.compose.foundation.layout.padding
|
||||||
import androidx.compose.foundation.layout.size
|
import androidx.compose.foundation.layout.size
|
||||||
import androidx.compose.foundation.rememberScrollState
|
|
||||||
import androidx.compose.foundation.shape.CircleShape
|
import androidx.compose.foundation.shape.CircleShape
|
||||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||||
import androidx.compose.foundation.verticalScroll
|
|
||||||
import androidx.compose.material3.MaterialTheme
|
import androidx.compose.material3.MaterialTheme
|
||||||
|
import androidx.compose.material3.Scaffold
|
||||||
import androidx.compose.material3.Surface
|
import androidx.compose.material3.Surface
|
||||||
import androidx.compose.material3.Text
|
import androidx.compose.material3.Text
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.runtime.getValue
|
import androidx.compose.runtime.getValue
|
||||||
|
import androidx.compose.runtime.mutableFloatStateOf
|
||||||
|
import androidx.compose.runtime.mutableStateOf
|
||||||
|
import androidx.compose.runtime.remember
|
||||||
|
import androidx.compose.runtime.setValue
|
||||||
import androidx.compose.ui.Alignment
|
import androidx.compose.ui.Alignment
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.draw.clip
|
import androidx.compose.ui.draw.clip
|
||||||
|
import androidx.compose.ui.geometry.Offset
|
||||||
import androidx.compose.ui.graphics.Color
|
import androidx.compose.ui.graphics.Color
|
||||||
import androidx.compose.ui.graphics.toArgb
|
import androidx.compose.ui.input.nestedscroll.NestedScrollConnection
|
||||||
|
import androidx.compose.ui.input.nestedscroll.NestedScrollSource
|
||||||
|
import androidx.compose.ui.input.nestedscroll.nestedScroll
|
||||||
import androidx.compose.ui.layout.ContentScale
|
import androidx.compose.ui.layout.ContentScale
|
||||||
import androidx.compose.ui.platform.LocalContext
|
import androidx.compose.ui.platform.LocalContext
|
||||||
|
import androidx.compose.ui.platform.LocalDensity
|
||||||
import androidx.compose.ui.res.painterResource
|
import androidx.compose.ui.res.painterResource
|
||||||
import androidx.compose.ui.text.style.TextAlign
|
import androidx.compose.ui.text.style.TextAlign
|
||||||
|
import androidx.compose.ui.unit.IntOffset
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import androidx.compose.ui.viewinterop.AndroidView
|
import androidx.compose.ui.viewinterop.AndroidView
|
||||||
|
import androidx.core.view.children
|
||||||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||||
import cafe.adriel.voyager.koin.getScreenModel
|
import cafe.adriel.voyager.koin.getScreenModel
|
||||||
import coil.compose.AsyncImage
|
import coil.compose.AsyncImage
|
||||||
import com.readrops.api.utils.DateUtils
|
import com.readrops.api.utils.DateUtils
|
||||||
import com.readrops.app.compose.R
|
import com.readrops.app.compose.R
|
||||||
import com.readrops.app.compose.util.Utils
|
import com.readrops.app.compose.item.view.ItemNestedScrollView
|
||||||
|
import com.readrops.app.compose.item.view.ItemWebView
|
||||||
import com.readrops.app.compose.util.components.AndroidScreen
|
import com.readrops.app.compose.util.components.AndroidScreen
|
||||||
import com.readrops.app.compose.util.components.CenteredProgressIndicator
|
import com.readrops.app.compose.util.components.CenteredProgressIndicator
|
||||||
import com.readrops.app.compose.util.components.IconText
|
import com.readrops.app.compose.util.components.IconText
|
||||||
|
@ -47,90 +58,136 @@ import org.koin.core.parameter.parametersOf
|
||||||
import kotlin.math.roundToInt
|
import kotlin.math.roundToInt
|
||||||
|
|
||||||
class ItemScreen(
|
class ItemScreen(
|
||||||
private val itemId: Int,
|
private val itemId: Int
|
||||||
) : AndroidScreen() {
|
) : AndroidScreen() {
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
override fun Content() {
|
override fun Content() {
|
||||||
val context = LocalContext.current
|
val context = LocalContext.current
|
||||||
|
val density = LocalDensity.current
|
||||||
|
|
||||||
val screenModel =
|
val screenModel =
|
||||||
getScreenModel<ItemScreenModel>(parameters = { parametersOf(itemId) })
|
getScreenModel<ItemScreenModel>(parameters = { parametersOf(itemId) })
|
||||||
val scrollState = rememberScrollState()
|
|
||||||
|
|
||||||
val state by screenModel.state.collectAsStateWithLifecycle()
|
val state by screenModel.state.collectAsStateWithLifecycle()
|
||||||
|
|
||||||
val primaryColor = MaterialTheme.colorScheme.primary
|
val primaryColor = MaterialTheme.colorScheme.primary
|
||||||
val backgroundColor = MaterialTheme.colorScheme.background
|
val backgroundColor = MaterialTheme.colorScheme.background
|
||||||
val onBackgroundColor = MaterialTheme.colorScheme.onBackground
|
val onBackgroundColor = MaterialTheme.colorScheme.onBackground
|
||||||
|
|
||||||
Column(
|
var isScrollable by remember { mutableStateOf(true) }
|
||||||
horizontalAlignment = Alignment.CenterHorizontally,
|
|
||||||
modifier = Modifier
|
|
||||||
.fillMaxSize()
|
|
||||||
.background(color = MaterialTheme.colorScheme.background)
|
|
||||||
.verticalScroll(scrollState)
|
|
||||||
) {
|
|
||||||
if (state.itemWithFeed != null) {
|
|
||||||
val itemWithFeed = state.itemWithFeed!!
|
|
||||||
|
|
||||||
if (itemWithFeed.item.imageLink != null) {
|
// https://developer.android.com/develop/ui/compose/touch-input/pointer-input/scroll#parent-compose-child-view
|
||||||
BackgroundTitle(
|
val bottomBarHeight = 64.dp
|
||||||
itemWithFeed = itemWithFeed
|
val bottomBarHeightPx = with(density) { bottomBarHeight.roundToPx().toFloat() }
|
||||||
)
|
val bottomBarOffsetHeightPx = remember { mutableFloatStateOf(0f) }
|
||||||
} else {
|
|
||||||
val tintColor = if (itemWithFeed.bgColor != 0) {
|
|
||||||
Color(itemWithFeed.bgColor)
|
|
||||||
} else {
|
|
||||||
MaterialTheme.colorScheme.onBackground
|
|
||||||
}
|
|
||||||
|
|
||||||
SimpleTitle(
|
val nestedScrollConnection = remember {
|
||||||
itemWithFeed = itemWithFeed,
|
object : NestedScrollConnection {
|
||||||
titleColor = tintColor,
|
override fun onPreScroll(available: Offset, source: NestedScrollSource): Offset {
|
||||||
accentColor = tintColor,
|
val delta = available.y
|
||||||
baseColor = MaterialTheme.colorScheme.onBackground,
|
val newOffset = bottomBarOffsetHeightPx.floatValue + delta
|
||||||
bottomPadding = true
|
bottomBarOffsetHeightPx.floatValue = newOffset.coerceIn(-bottomBarHeightPx, 0f)
|
||||||
|
|
||||||
|
return Offset.Zero
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (state.itemWithFeed != null) {
|
||||||
|
val itemWithFeed = state.itemWithFeed!!
|
||||||
|
val item = itemWithFeed.item
|
||||||
|
|
||||||
|
val accentColor = if (itemWithFeed.bgColor != 0) {
|
||||||
|
Color(itemWithFeed.bgColor)
|
||||||
|
} else {
|
||||||
|
primaryColor
|
||||||
|
}
|
||||||
|
|
||||||
|
fun openUrl(url: String) {
|
||||||
|
val intent = Intent(Intent.ACTION_VIEW, Uri.parse(url))
|
||||||
|
context.startActivity(intent)
|
||||||
|
}
|
||||||
|
|
||||||
|
Scaffold(
|
||||||
|
modifier = Modifier
|
||||||
|
.nestedScroll(nestedScrollConnection),
|
||||||
|
bottomBar = {
|
||||||
|
ItemScreenBottomBar(
|
||||||
|
item = item,
|
||||||
|
accentColor = accentColor,
|
||||||
|
modifier = Modifier
|
||||||
|
.height(bottomBarHeight)
|
||||||
|
.offset {
|
||||||
|
if (isScrollable) {
|
||||||
|
IntOffset(
|
||||||
|
x = 0,
|
||||||
|
y = -bottomBarOffsetHeightPx.floatValue.roundToInt()
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
IntOffset(0, 0)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
onShare = { screenModel.shareItem(item, context) },
|
||||||
|
onOpenUrl = { openUrl(item.link!!) },
|
||||||
|
onChangeReadState = {
|
||||||
|
screenModel.setItemReadState(item.apply { isRead = it })
|
||||||
|
},
|
||||||
|
onChangeStarState = {
|
||||||
|
screenModel.setItemStarState(item.apply { isStarred = it })
|
||||||
|
}
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
) { paddingValues ->
|
||||||
|
Box(
|
||||||
|
modifier = Modifier.padding(paddingValues)
|
||||||
|
) {
|
||||||
|
AndroidView(
|
||||||
|
factory = { context ->
|
||||||
|
ItemNestedScrollView(
|
||||||
|
context = context,
|
||||||
|
onGlobalLayoutListener = { viewHeight, contentHeight ->
|
||||||
|
isScrollable = viewHeight - contentHeight < 0
|
||||||
|
},
|
||||||
|
onUrlClick = { url -> openUrl(url) }
|
||||||
|
) {
|
||||||
|
if (item.imageLink != null) {
|
||||||
|
BackgroundTitle(
|
||||||
|
itemWithFeed = itemWithFeed
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
val tintColor = if (itemWithFeed.bgColor != 0) {
|
||||||
|
Color(itemWithFeed.bgColor)
|
||||||
|
} else {
|
||||||
|
MaterialTheme.colorScheme.onBackground
|
||||||
|
}
|
||||||
|
|
||||||
AndroidView(
|
SimpleTitle(
|
||||||
factory = { context ->
|
itemWithFeed = itemWithFeed,
|
||||||
WebView(context).apply {
|
titleColor = tintColor,
|
||||||
settings.javaScriptEnabled = true
|
accentColor = tintColor,
|
||||||
webViewClient = WebViewClient()
|
baseColor = MaterialTheme.colorScheme.onBackground,
|
||||||
|
bottomPadding = true
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
update = { nestedScrollView ->
|
||||||
|
val relativeLayout =
|
||||||
|
(nestedScrollView.children.toList()[0] as RelativeLayout)
|
||||||
|
val webView = relativeLayout.children.toList()[1] as ItemWebView
|
||||||
|
|
||||||
settings.builtInZoomControls = true
|
webView.loadText(
|
||||||
settings.displayZoomControls = false
|
itemWithFeed = itemWithFeed,
|
||||||
settings.setSupportZoom(false)
|
accentColor = accentColor,
|
||||||
|
backgroundColor = backgroundColor,
|
||||||
isVerticalScrollBarEnabled = false
|
onBackgroundColor = onBackgroundColor
|
||||||
setBackgroundColor(backgroundColor.toArgb())
|
)
|
||||||
}
|
}
|
||||||
},
|
)
|
||||||
update = { webView ->
|
}
|
||||||
val tintColor = if (itemWithFeed.bgColor != 0) {
|
|
||||||
Color(itemWithFeed.bgColor)
|
|
||||||
} else {
|
|
||||||
primaryColor
|
|
||||||
}
|
|
||||||
|
|
||||||
val string = context.getString(
|
|
||||||
R.string.webview_html_template,
|
|
||||||
Utils.getCssColor(tintColor.toArgb()),
|
|
||||||
Utils.getCssColor(onBackgroundColor.toArgb()),
|
|
||||||
Utils.getCssColor(backgroundColor.toArgb()),
|
|
||||||
screenModel.formatText()
|
|
||||||
)
|
|
||||||
val data =
|
|
||||||
Base64.encodeToString(string.encodeToByteArray(), Base64.NO_PADDING)
|
|
||||||
|
|
||||||
webView.loadData(data, "text/html; charset=utf-8", "base64")
|
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
} else {
|
|
||||||
CenteredProgressIndicator()
|
|
||||||
}
|
}
|
||||||
|
} else {
|
||||||
|
CenteredProgressIndicator()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -215,7 +272,8 @@ fun SimpleTitle(
|
||||||
Text(
|
Text(
|
||||||
text = itemWithFeed.feedName,
|
text = itemWithFeed.feedName,
|
||||||
style = MaterialTheme.typography.labelLarge,
|
style = MaterialTheme.typography.labelLarge,
|
||||||
color = baseColor
|
color = baseColor,
|
||||||
|
textAlign = TextAlign.Center
|
||||||
)
|
)
|
||||||
|
|
||||||
ShortSpacer()
|
ShortSpacer()
|
||||||
|
|
|
@ -0,0 +1,84 @@
|
||||||
|
package com.readrops.app.compose.item
|
||||||
|
|
||||||
|
import androidx.compose.foundation.layout.Arrangement
|
||||||
|
import androidx.compose.foundation.layout.Row
|
||||||
|
import androidx.compose.foundation.layout.fillMaxWidth
|
||||||
|
import androidx.compose.foundation.layout.padding
|
||||||
|
import androidx.compose.material.icons.Icons
|
||||||
|
import androidx.compose.material.icons.filled.Share
|
||||||
|
import androidx.compose.material3.Icon
|
||||||
|
import androidx.compose.material3.IconButton
|
||||||
|
import androidx.compose.material3.MaterialTheme
|
||||||
|
import androidx.compose.material3.Surface
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.graphics.Color
|
||||||
|
import androidx.compose.ui.res.painterResource
|
||||||
|
import com.readrops.app.compose.R
|
||||||
|
import com.readrops.app.compose.util.theme.spacing
|
||||||
|
import com.readrops.db.entities.Item
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun ItemScreenBottomBar(
|
||||||
|
item: Item,
|
||||||
|
accentColor: Color,
|
||||||
|
onShare: () -> Unit,
|
||||||
|
onOpenUrl: () -> Unit,
|
||||||
|
onChangeReadState: (Boolean) -> Unit,
|
||||||
|
onChangeStarState: (Boolean) -> Unit,
|
||||||
|
modifier: Modifier = Modifier
|
||||||
|
) {
|
||||||
|
Surface(
|
||||||
|
color = accentColor,
|
||||||
|
modifier = modifier.fillMaxWidth()
|
||||||
|
) {
|
||||||
|
Row(
|
||||||
|
horizontalArrangement = Arrangement.SpaceAround,
|
||||||
|
modifier = Modifier.padding(MaterialTheme.spacing.shortSpacing)
|
||||||
|
) {
|
||||||
|
IconButton(
|
||||||
|
onClick = { onChangeReadState(!item.isRead) }
|
||||||
|
) {
|
||||||
|
Icon(
|
||||||
|
painter = painterResource(
|
||||||
|
id = if (item.isRead)
|
||||||
|
R.drawable.ic_remove_done
|
||||||
|
else R.drawable.ic_done_all
|
||||||
|
),
|
||||||
|
contentDescription = null
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
IconButton(
|
||||||
|
onClick = { onChangeStarState(!item.isStarred) }
|
||||||
|
) {
|
||||||
|
Icon(
|
||||||
|
painter = painterResource(
|
||||||
|
id = if (item.isStarred)
|
||||||
|
R.drawable.ic_star
|
||||||
|
else R.drawable.ic_star_outline
|
||||||
|
),
|
||||||
|
contentDescription = null
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
IconButton(
|
||||||
|
onClick = onShare
|
||||||
|
) {
|
||||||
|
Icon(
|
||||||
|
imageVector = Icons.Default.Share,
|
||||||
|
contentDescription = null
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
IconButton(
|
||||||
|
onClick = onOpenUrl
|
||||||
|
) {
|
||||||
|
Icon(
|
||||||
|
painter = painterResource(id = R.drawable.ic_open_in_browser),
|
||||||
|
contentDescription = null
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,31 +1,55 @@
|
||||||
package com.readrops.app.compose.item
|
package com.readrops.app.compose.item
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
import android.content.Intent
|
||||||
import androidx.compose.runtime.Stable
|
import androidx.compose.runtime.Stable
|
||||||
import cafe.adriel.voyager.core.model.StateScreenModel
|
import cafe.adriel.voyager.core.model.StateScreenModel
|
||||||
import cafe.adriel.voyager.core.model.screenModelScope
|
import cafe.adriel.voyager.core.model.screenModelScope
|
||||||
|
import com.readrops.app.compose.repositories.BaseRepository
|
||||||
import com.readrops.db.Database
|
import com.readrops.db.Database
|
||||||
|
import com.readrops.db.entities.Item
|
||||||
|
import com.readrops.db.entities.account.Account
|
||||||
import com.readrops.db.pojo.ItemWithFeed
|
import com.readrops.db.pojo.ItemWithFeed
|
||||||
import com.readrops.db.queries.ItemSelectionQueryBuilder
|
import com.readrops.db.queries.ItemSelectionQueryBuilder
|
||||||
|
import kotlinx.coroutines.CoroutineDispatcher
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.flow.update
|
import kotlinx.coroutines.flow.update
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import org.jsoup.Jsoup
|
import org.jsoup.Jsoup
|
||||||
import org.jsoup.parser.Parser
|
import org.jsoup.parser.Parser
|
||||||
|
import org.koin.core.component.KoinComponent
|
||||||
|
import org.koin.core.component.get
|
||||||
|
import org.koin.core.parameter.parametersOf
|
||||||
|
|
||||||
class ItemScreenModel(
|
class ItemScreenModel(
|
||||||
private val database: Database,
|
private val database: Database,
|
||||||
private val itemId: Int
|
private val itemId: Int,
|
||||||
) : StateScreenModel<ItemState>(ItemState()) {
|
private val dispatcher: CoroutineDispatcher = Dispatchers.IO
|
||||||
|
) : StateScreenModel<ItemState>(ItemState()), KoinComponent {
|
||||||
|
|
||||||
|
//TODO Is this really the best solution?
|
||||||
|
lateinit var account: Account
|
||||||
|
lateinit var repository: BaseRepository
|
||||||
|
|
||||||
init {
|
init {
|
||||||
screenModelScope.launch(Dispatchers.IO) {
|
screenModelScope.launch(dispatcher) {
|
||||||
mutableState.update {
|
database.newAccountDao().selectCurrentAccount()
|
||||||
val query = ItemSelectionQueryBuilder.buildQuery(itemId, false)
|
.collect { account ->
|
||||||
|
this@ItemScreenModel.account = account!!
|
||||||
|
repository = get { parametersOf(account) }
|
||||||
|
|
||||||
it.copy(
|
val query = ItemSelectionQueryBuilder.buildQuery(
|
||||||
itemWithFeed = database.newItemDao().selectItemById(query)
|
itemId = itemId,
|
||||||
)
|
separateState = account.config.useSeparateState
|
||||||
}
|
)
|
||||||
|
|
||||||
|
database.newItemDao().selectItemById(query)
|
||||||
|
.collect { itemWithFeed ->
|
||||||
|
mutableState.update {
|
||||||
|
it.copy(itemWithFeed = itemWithFeed)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -41,6 +65,30 @@ class ItemScreenModel(
|
||||||
document.select("div,span").forEach { it.clearAttributes() }
|
document.select("div,span").forEach { it.clearAttributes() }
|
||||||
return document.body().html()
|
return document.body().html()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun shareItem(item: Item, context: Context) {
|
||||||
|
Intent().apply {
|
||||||
|
action = Intent.ACTION_SEND
|
||||||
|
type = "text/plain"
|
||||||
|
putExtra(Intent.EXTRA_TEXT, item.link)
|
||||||
|
}.also {
|
||||||
|
context.startActivity(Intent.createChooser(it, null))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun setItemReadState(item: Item) {
|
||||||
|
//TODO support separateState
|
||||||
|
screenModelScope.launch(dispatcher) {
|
||||||
|
repository.setItemReadState(item)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun setItemStarState(item: Item) {
|
||||||
|
//TODO support separateState
|
||||||
|
screenModelScope.launch(dispatcher) {
|
||||||
|
repository.setItemStarState(item)
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Stable
|
@Stable
|
||||||
|
|
|
@ -0,0 +1,67 @@
|
||||||
|
package com.readrops.app.compose.item.view
|
||||||
|
|
||||||
|
import android.annotation.SuppressLint
|
||||||
|
import android.content.Context
|
||||||
|
import android.widget.RelativeLayout
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.ui.platform.ComposeView
|
||||||
|
import androidx.core.view.ViewCompat
|
||||||
|
import androidx.core.widget.NestedScrollView
|
||||||
|
|
||||||
|
@SuppressLint("ResourceType", "ViewConstructor")
|
||||||
|
class ItemNestedScrollView(
|
||||||
|
context: Context,
|
||||||
|
onGlobalLayoutListener: (viewHeight: Int, contentHeight: Int) -> Unit,
|
||||||
|
onUrlClick: (String) -> Unit,
|
||||||
|
composeViewContent: @Composable () -> Unit
|
||||||
|
) : NestedScrollView(context) {
|
||||||
|
|
||||||
|
init {
|
||||||
|
addView(
|
||||||
|
RelativeLayout(context).apply {
|
||||||
|
ViewCompat.setNestedScrollingEnabled(this, true)
|
||||||
|
|
||||||
|
val composeView = ComposeView(context).apply {
|
||||||
|
id = 1
|
||||||
|
|
||||||
|
setContent {
|
||||||
|
composeViewContent()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
val composeViewParams = RelativeLayout.LayoutParams(
|
||||||
|
LayoutParams.MATCH_PARENT,
|
||||||
|
LayoutParams.WRAP_CONTENT
|
||||||
|
)
|
||||||
|
composeViewParams.addRule(RelativeLayout.CENTER_HORIZONTAL)
|
||||||
|
composeView.layoutParams = composeViewParams
|
||||||
|
|
||||||
|
val webView = ItemWebView(
|
||||||
|
context = context,
|
||||||
|
onUrlClick = onUrlClick
|
||||||
|
).apply {
|
||||||
|
id = 2
|
||||||
|
ViewCompat.setNestedScrollingEnabled(this, true)
|
||||||
|
}
|
||||||
|
|
||||||
|
val webViewParams = RelativeLayout.LayoutParams(
|
||||||
|
LayoutParams.MATCH_PARENT,
|
||||||
|
LayoutParams.WRAP_CONTENT
|
||||||
|
)
|
||||||
|
|
||||||
|
webViewParams.addRule(RelativeLayout.BELOW, composeView.id)
|
||||||
|
webView.layoutParams = webViewParams
|
||||||
|
|
||||||
|
addView(composeView)
|
||||||
|
addView(webView)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
viewTreeObserver.addOnGlobalLayoutListener {
|
||||||
|
val viewHeight = this.measuredHeight
|
||||||
|
val contentHeight = getChildAt(0).height
|
||||||
|
|
||||||
|
onGlobalLayoutListener(viewHeight, contentHeight)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,76 @@
|
||||||
|
package com.readrops.app.compose.item.view
|
||||||
|
|
||||||
|
import android.annotation.SuppressLint
|
||||||
|
import android.content.Context
|
||||||
|
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 com.readrops.app.compose.R
|
||||||
|
import com.readrops.app.compose.util.Utils
|
||||||
|
import com.readrops.db.pojo.ItemWithFeed
|
||||||
|
import org.jsoup.Jsoup
|
||||||
|
import org.jsoup.parser.Parser
|
||||||
|
|
||||||
|
@SuppressLint("SetJavaScriptEnabled", "ViewConstructor")
|
||||||
|
class ItemWebView(
|
||||||
|
context: Context,
|
||||||
|
onUrlClick: (String) -> Unit,
|
||||||
|
attrs: AttributeSet? = null,
|
||||||
|
) : WebView(context, attrs) {
|
||||||
|
|
||||||
|
init {
|
||||||
|
settings.javaScriptEnabled = true
|
||||||
|
settings.builtInZoomControls = true
|
||||||
|
settings.displayZoomControls = false
|
||||||
|
settings.setSupportZoom(false)
|
||||||
|
isVerticalScrollBarEnabled = false
|
||||||
|
|
||||||
|
webViewClient = object : WebViewClient() {
|
||||||
|
override fun shouldOverrideUrlLoading(view: WebView?, url: String?): Boolean {
|
||||||
|
url?.let { onUrlClick(it) }
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun loadText(
|
||||||
|
itemWithFeed: ItemWithFeed,
|
||||||
|
accentColor: Color,
|
||||||
|
backgroundColor: Color,
|
||||||
|
onBackgroundColor: Color
|
||||||
|
) {
|
||||||
|
val string = context.getString(
|
||||||
|
R.string.webview_html_template,
|
||||||
|
Utils.getCssColor(accentColor.toArgb()),
|
||||||
|
Utils.getCssColor(onBackgroundColor.toArgb()),
|
||||||
|
Utils.getCssColor(backgroundColor.toArgb()),
|
||||||
|
formatText(itemWithFeed)
|
||||||
|
)
|
||||||
|
|
||||||
|
loadDataWithBaseURL(
|
||||||
|
"file:///android_asset/",
|
||||||
|
string,
|
||||||
|
"text/html; charset=utf-8",
|
||||||
|
"UTF-8",
|
||||||
|
null
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun formatText(itemWithFeed: ItemWithFeed): String {
|
||||||
|
return if (itemWithFeed.item.content != 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()
|
||||||
|
} else {
|
||||||
|
""
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -0,0 +1,5 @@
|
||||||
|
<vector xmlns:android="http://schemas.android.com/apk/res/android" android:height="24dp" android:tint="#000000" android:viewportHeight="24" android:viewportWidth="24" android:width="24dp">
|
||||||
|
|
||||||
|
<path android:fillColor="@android:color/white" android:pathData="M1.79,12l5.58,5.59L5.96,19 0.37,13.41 1.79,12zM2.24,4.22L12.9,14.89l-1.28,1.28L7.44,12l-1.41,1.41L11.62,19l2.69,-2.69 4.89,4.89 1.41,-1.41L3.65,2.81 2.24,4.22zM17.14,13.49L23.62,7 22.2,5.59l-6.48,6.48 1.42,1.42zM17.96,7l-1.41,-1.41 -3.65,3.66 1.41,1.41L17.96,7z"/>
|
||||||
|
|
||||||
|
</vector>
|
|
@ -0,0 +1,5 @@
|
||||||
|
<vector xmlns:android="http://schemas.android.com/apk/res/android" android:height="24dp" android:tint="#000000" android:viewportHeight="24" android:viewportWidth="24" android:width="24dp">
|
||||||
|
|
||||||
|
<path android:fillColor="@android:color/white" android:pathData="M12,17.27L18.18,21l-1.64,-7.03L22,9.24l-7.19,-0.61L12,2 9.19,8.63 2,9.24l5.46,4.73L5.82,21z"/>
|
||||||
|
|
||||||
|
</vector>
|
|
@ -0,0 +1,5 @@
|
||||||
|
<vector xmlns:android="http://schemas.android.com/apk/res/android" android:height="24dp" android:tint="#000000" android:viewportHeight="24" android:viewportWidth="24" android:width="24dp">
|
||||||
|
|
||||||
|
<path android:fillColor="@android:color/white" android:pathData="M22,9.24l-7.19,-0.62L12,2 9.19,8.63 2,9.24l5.46,4.73L5.82,21 12,17.27 18.18,21l-1.63,-7.03L22,9.24zM12,15.4l-3.76,2.27 1,-4.28 -3.32,-2.88 4.38,-0.38L12,6.1l1.71,4.04 4.38,0.38 -3.32,2.88 1,4.28L12,15.4z"/>
|
||||||
|
|
||||||
|
</vector>
|
|
@ -15,6 +15,14 @@
|
||||||
%3$s: background color
|
%3$s: background color
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
@font-face {
|
||||||
|
font-family: Inter;
|
||||||
|
font-style: normal;
|
||||||
|
font-weight: normal;
|
||||||
|
font-display: swap;
|
||||||
|
src: url("fonts/Inter-Regular.woff2") format("woff2");
|
||||||
|
}
|
||||||
|
|
||||||
a:link, a:active, a:hover { color: %1$s }
|
a:link, a:active, a:hover { color: %1$s }
|
||||||
|
|
||||||
* {
|
* {
|
||||||
|
@ -31,6 +39,7 @@
|
||||||
margin-right: 3%%;
|
margin-right: 3%%;
|
||||||
color: %2$s;
|
color: %2$s;
|
||||||
background-color: %3$s;
|
background-color: %3$s;
|
||||||
|
font-family: Inter !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
figure, img, iframe, video {
|
figure, img, iframe, video {
|
||||||
|
|
|
@ -10,6 +10,7 @@ import com.readrops.db.entities.Folder
|
||||||
import com.readrops.db.entities.Item
|
import com.readrops.db.entities.Item
|
||||||
import com.readrops.db.entities.ItemState
|
import com.readrops.db.entities.ItemState
|
||||||
import com.readrops.db.pojo.ItemWithFeed
|
import com.readrops.db.pojo.ItemWithFeed
|
||||||
|
import kotlinx.coroutines.flow.Flow
|
||||||
|
|
||||||
@Dao
|
@Dao
|
||||||
abstract class NewItemDao : NewBaseDao<Item> {
|
abstract class NewItemDao : NewBaseDao<Item> {
|
||||||
|
@ -18,7 +19,7 @@ abstract class NewItemDao : NewBaseDao<Item> {
|
||||||
abstract fun selectAll(query: SupportSQLiteQuery): PagingSource<Int, ItemWithFeed>
|
abstract fun selectAll(query: SupportSQLiteQuery): PagingSource<Int, ItemWithFeed>
|
||||||
|
|
||||||
@RawQuery(observedEntities = [Item::class, ItemState::class])
|
@RawQuery(observedEntities = [Item::class, ItemState::class])
|
||||||
abstract suspend fun selectItemById(query: SupportSQLiteQuery): ItemWithFeed
|
abstract fun selectItemById(query: SupportSQLiteQuery): Flow<ItemWithFeed>
|
||||||
|
|
||||||
@Query("Update Item Set read = :read Where id = :itemId")
|
@Query("Update Item Set read = :read Where id = :itemId")
|
||||||
abstract suspend fun updateReadState(itemId: Int, read: Boolean)
|
abstract suspend fun updateReadState(itemId: Int, read: Boolean)
|
||||||
|
|
Loading…
Reference in New Issue