All checks were successful
continuous-integration/drone/push Build is passing
Signed-off-by: Balazs Toldi <balazs@toldi.eu>
539 lines
No EOL
22 KiB
Kotlin
539 lines
No EOL
22 KiB
Kotlin
package eu.toldi.balazs.anotherfeedreader
|
|
|
|
import android.content.Intent
|
|
import android.os.Bundle
|
|
import android.util.Log
|
|
import androidx.activity.ComponentActivity
|
|
import androidx.activity.compose.setContent
|
|
import androidx.activity.viewModels
|
|
import androidx.compose.foundation.Image
|
|
import androidx.compose.foundation.clickable
|
|
import androidx.compose.foundation.gestures.detectTapGestures
|
|
import androidx.compose.foundation.layout.*
|
|
import androidx.compose.foundation.lazy.LazyColumn
|
|
import androidx.compose.foundation.shape.CircleShape
|
|
import androidx.compose.foundation.shape.RoundedCornerShape
|
|
import androidx.compose.material.*
|
|
import androidx.compose.material.icons.Icons
|
|
import androidx.compose.material.icons.filled.*
|
|
import androidx.compose.runtime.*
|
|
import androidx.compose.runtime.livedata.observeAsState
|
|
import androidx.compose.ui.Alignment
|
|
import androidx.compose.ui.Modifier
|
|
import androidx.compose.ui.draw.clip
|
|
import androidx.compose.ui.hapticfeedback.HapticFeedbackType
|
|
import androidx.compose.ui.input.pointer.pointerInput
|
|
import androidx.compose.ui.layout.ContentScale
|
|
import androidx.compose.ui.platform.LocalHapticFeedback
|
|
import androidx.compose.ui.res.painterResource
|
|
import androidx.compose.ui.res.stringResource
|
|
import androidx.compose.ui.text.style.TextAlign
|
|
import androidx.compose.ui.text.style.TextOverflow
|
|
import androidx.compose.ui.unit.dp
|
|
import androidx.compose.ui.unit.sp
|
|
import coil.compose.rememberImagePainter
|
|
import com.google.accompanist.swiperefresh.SwipeRefresh
|
|
import com.google.accompanist.swiperefresh.rememberSwipeRefreshState
|
|
import eu.toldi.balazs.anotherfeedreader.entities.Article
|
|
import eu.toldi.balazs.anotherfeedreader.entities.Feed
|
|
import eu.toldi.balazs.anotherfeedreader.entities.FeedGroup
|
|
import eu.toldi.balazs.anotherfeedreader.ui.theme.AnotherFeedReaderTheme
|
|
import eu.toldi.balazs.anotherfeedreader.viewmodel.FeedViewModel
|
|
import kotlinx.coroutines.Job
|
|
import kotlinx.coroutines.launch
|
|
import java.time.format.DateTimeFormatter
|
|
import java.time.format.FormatStyle
|
|
|
|
class MainActivity : ComponentActivity() {
|
|
|
|
|
|
private val viewModel: FeedViewModel by viewModels()
|
|
var syncJob: Job? = null
|
|
override fun onCreate(savedInstanceState: Bundle?) {
|
|
super.onCreate(savedInstanceState)
|
|
|
|
setContent {
|
|
AnotherFeedReaderTheme {
|
|
// A surface container using the 'background' color from the theme
|
|
Surface(color = MaterialTheme.colors.background) {
|
|
viewModel.updateFeeds()
|
|
|
|
var isAddFeedDialogOpen by remember {
|
|
mutableStateOf(false)
|
|
}
|
|
|
|
val scope = rememberCoroutineScope()
|
|
val drawerState = rememberDrawerState(DrawerValue.Closed)
|
|
val articleList = viewModel.articles.observeAsState(emptyList())
|
|
Scaffold(
|
|
floatingActionButton = {
|
|
FloatingActionButton(
|
|
onClick = { isAddFeedDialogOpen = true },
|
|
backgroundColor = MaterialTheme.colors.primary
|
|
) {
|
|
Icon(Icons.Filled.Add, contentDescription = "Add feed")
|
|
}
|
|
}
|
|
) {
|
|
SideBar(drawerState = drawerState) {
|
|
Column {
|
|
MenuBar {
|
|
scope.launch {
|
|
drawerState.open()
|
|
}
|
|
}
|
|
val isRefreshing = viewModel.isRefereshing.value
|
|
|
|
SwipeRefresh(
|
|
state = rememberSwipeRefreshState(isRefreshing),
|
|
onRefresh = {
|
|
viewModel.updateFeeds()
|
|
},
|
|
) {
|
|
LazyColumn(
|
|
modifier = Modifier
|
|
.fillMaxHeight()
|
|
.fillMaxWidth()
|
|
|
|
) {
|
|
items(articleList.value.size) { index ->
|
|
val articleAndFeed = articleList.value[index]
|
|
articleAndFeed.articles.sortedByDescending { it.pubDate }
|
|
.forEach {
|
|
showArticle(articleAndFeed.feed, article = it)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
if (isAddFeedDialogOpen) {
|
|
showAddFeedDialog { isAddFeedDialogOpen = false }
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
@Composable
|
|
fun showAddFeedDialog(toClose: () -> Unit) {
|
|
var text by remember { mutableStateOf("") }
|
|
var selectedIndex by remember { mutableStateOf(0) }
|
|
AlertDialog(onDismissRequest = { toClose() },
|
|
title = {
|
|
Text(text = "Add feed")
|
|
},
|
|
text = {
|
|
Column {
|
|
OutlinedTextField(
|
|
value = text,
|
|
onValueChange = {
|
|
text = it
|
|
},
|
|
label = { Text("Feed URL") },
|
|
modifier = Modifier.padding(vertical = 4.dp)
|
|
)
|
|
Row() {
|
|
Text("Group:")
|
|
var expanded by remember {
|
|
mutableStateOf(false)
|
|
}
|
|
|
|
Box(
|
|
modifier = Modifier
|
|
.wrapContentSize(Alignment.TopStart)
|
|
) {
|
|
Text(
|
|
|
|
text = if (selectedIndex == 0) "None" else viewModel.feedsState.value!!.filterIsInstance<FeedGroup>()[selectedIndex - 1].name.toString(),
|
|
modifier = Modifier
|
|
.fillMaxWidth()
|
|
.clickable(onClick = { expanded = true })
|
|
)
|
|
}
|
|
DropdownMenu(
|
|
expanded = expanded,
|
|
onDismissRequest = { expanded = false },
|
|
) {
|
|
DropdownMenuItem(onClick = {
|
|
selectedIndex = 0
|
|
expanded = false
|
|
}) {
|
|
Text(text = "None")
|
|
}
|
|
|
|
viewModel.feedsState.value!!.filterIsInstance<FeedGroup>()
|
|
.forEachIndexed { index, s ->
|
|
DropdownMenuItem(onClick = {
|
|
selectedIndex = index + 1
|
|
expanded = false
|
|
}) {
|
|
viewModel.feedsState.value!![index].name?.let {
|
|
Text(text = it)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
},
|
|
buttons = {
|
|
Row(
|
|
modifier = Modifier.padding(all = 8.dp),
|
|
horizontalArrangement = Arrangement.Center
|
|
) {
|
|
Button(
|
|
onClick = toClose,
|
|
modifier = Modifier.padding(all = 8.dp),
|
|
) {
|
|
Text("Cancel")
|
|
}
|
|
Button(
|
|
onClick = {
|
|
try {
|
|
val feed = Feed(null,text)
|
|
if (selectedIndex == 0)
|
|
viewModel.addFeed(feed)
|
|
else {
|
|
var counter = 0
|
|
viewModel.feedsState.value!!.forEachIndexed { index, feedGroup ->
|
|
if (feedGroup is FeedGroup) {
|
|
counter++
|
|
if (counter == selectedIndex) {
|
|
feedGroup.addFeed(feed)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
viewModel.changeFeedList()
|
|
} catch (e: Exception) {
|
|
|
|
} finally {
|
|
toClose()
|
|
}
|
|
},
|
|
modifier = Modifier.padding(all = 8.dp),
|
|
) {
|
|
Text("Add Feed")
|
|
}
|
|
}
|
|
}
|
|
)
|
|
}
|
|
|
|
@Composable
|
|
fun MenuBar(hamburgerClick: () -> Unit) {
|
|
TopAppBar(
|
|
title = {
|
|
Text(stringResource(id = R.string.app_name))
|
|
},
|
|
navigationIcon = {
|
|
IconButton(onClick = hamburgerClick) {
|
|
Icon(
|
|
Icons.Filled.Menu,
|
|
contentDescription = "Menu Hamburger"
|
|
)
|
|
}
|
|
},
|
|
actions = {
|
|
Row {
|
|
IconButton(onClick = {}) {
|
|
Icon(
|
|
Icons.Filled.Search,
|
|
contentDescription = "Search Article"
|
|
)
|
|
}
|
|
IconButton(onClick = {}) {
|
|
Icon(
|
|
Icons.Filled.Settings,
|
|
contentDescription = "Settings"
|
|
)
|
|
}
|
|
}
|
|
}
|
|
)
|
|
}
|
|
|
|
@Composable
|
|
fun showArticle(feed: Feed, article: Article) {
|
|
val haptic = LocalHapticFeedback.current
|
|
Card(
|
|
Modifier
|
|
.pointerInput(Unit) {
|
|
detectTapGestures(
|
|
onTap = {
|
|
WebView.webURL = article.url.toString()
|
|
WebView.barTitle =
|
|
feed.name ?: ""
|
|
startActivity(
|
|
Intent(
|
|
baseContext,
|
|
WebView::class.java
|
|
)
|
|
)
|
|
},
|
|
onLongPress = {
|
|
val share =
|
|
Intent.createChooser(Intent().apply {
|
|
action = Intent.ACTION_SEND
|
|
putExtra(
|
|
Intent.EXTRA_TEXT,
|
|
article.url
|
|
)
|
|
putExtra(
|
|
Intent.EXTRA_TITLE,
|
|
article.title
|
|
)
|
|
type = "text/plain"
|
|
}, null)
|
|
startActivity(share)
|
|
haptic.performHapticFeedback(HapticFeedbackType.LongPress)
|
|
}
|
|
)
|
|
}
|
|
.padding(
|
|
start = 6.dp,
|
|
top = 2.dp,
|
|
bottom = 6.dp,
|
|
end = 6.dp
|
|
),
|
|
elevation = 10.dp,
|
|
backgroundColor = MaterialTheme.colors.surface,
|
|
shape = RoundedCornerShape(10.dp)
|
|
) {
|
|
Column {
|
|
Row(
|
|
modifier = Modifier.padding(horizontal = 3.dp, vertical = 6.dp),
|
|
horizontalArrangement = Arrangement.Start,
|
|
verticalAlignment = Alignment.CenterVertically
|
|
) {
|
|
val painter = if (feed.faviconURL != null) {
|
|
rememberImagePainter(feed.faviconURL)
|
|
} else {
|
|
painterResource(id = R.drawable.ic_launcher_background)
|
|
}
|
|
Image(
|
|
painter = painter,
|
|
contentDescription = null,
|
|
modifier = Modifier
|
|
.size(30.dp)
|
|
.clip(CircleShape)
|
|
)
|
|
article.author?.let {
|
|
Text(
|
|
text = (fun(): String {
|
|
if (it.length > 25)
|
|
return it.dropLast(it.length - 25)
|
|
else return it
|
|
}).invoke(),
|
|
fontSize = 11.sp,
|
|
modifier = Modifier.padding(horizontal = 2.dp)
|
|
)
|
|
}
|
|
feed.name?.let {
|
|
Text(
|
|
text = (fun(): String {
|
|
return if (it.length > 25)
|
|
return it.dropLast(it.length - 25)
|
|
else return it
|
|
}).invoke(),
|
|
fontSize = 11.sp,
|
|
textAlign = TextAlign.Center,
|
|
modifier = Modifier.padding(horizontal = 10.dp),
|
|
maxLines = 1,
|
|
overflow = TextOverflow.Ellipsis
|
|
)
|
|
}
|
|
article.pubDate?.let {
|
|
Text(
|
|
it.format(DateTimeFormatter.ofLocalizedDateTime(FormatStyle.MEDIUM)),
|
|
fontSize = 11.sp,
|
|
textAlign = TextAlign.Right,
|
|
//modifier = Modifier.width(350.dp)
|
|
)
|
|
}
|
|
}
|
|
if (article.imageURL != null) {
|
|
Image(
|
|
painter = rememberImagePainter(article.imageURL),
|
|
contentDescription = null,
|
|
contentScale = ContentScale.Crop,
|
|
alignment = Alignment.Center,
|
|
modifier = Modifier
|
|
.height(200.dp)
|
|
.fillMaxWidth()
|
|
.fillMaxHeight()
|
|
)
|
|
}
|
|
if (article.title != null) {
|
|
Text(
|
|
text = article.title!!.trim(),
|
|
fontSize = 20.sp,
|
|
maxLines = 2,
|
|
overflow = TextOverflow.Ellipsis
|
|
)
|
|
} else Text("")
|
|
if (article.description != null) {
|
|
Text(
|
|
text = article.description!!,
|
|
fontSize = 17.sp,
|
|
maxLines = 2,
|
|
overflow = TextOverflow.Ellipsis
|
|
)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
@Composable
|
|
fun FeedSideBar(feed: Feed) {
|
|
Column() {
|
|
var collapsed by remember {
|
|
mutableStateOf(true)
|
|
}
|
|
Card(
|
|
modifier = Modifier
|
|
.fillMaxWidth()
|
|
.padding(vertical = 5.dp, horizontal = 4.dp)
|
|
.clickable {
|
|
/*feeds.limit = if (feeds.limit == feed) {
|
|
null
|
|
} else feed*/
|
|
viewModel.changeFeed()
|
|
},
|
|
elevation = 10.dp,
|
|
backgroundColor = MaterialTheme.colors.surface,
|
|
) {
|
|
Row {
|
|
if (feed is FeedGroup) {
|
|
IconButton(onClick = {
|
|
collapsed = collapsed.not()
|
|
}) {
|
|
if (collapsed)
|
|
Icon(
|
|
Icons.Filled.KeyboardArrowDown,
|
|
contentDescription = "Show feeds contained"
|
|
)
|
|
else
|
|
Icon(
|
|
Icons.Filled.KeyboardArrowUp,
|
|
contentDescription = "Hide feeds contained"
|
|
)
|
|
}
|
|
}
|
|
feed.name?.let {
|
|
Text(
|
|
text = it,
|
|
fontSize = 26.sp,
|
|
maxLines = 1,
|
|
overflow = TextOverflow.Ellipsis,
|
|
modifier = Modifier.padding(all = 4.dp)
|
|
)
|
|
}
|
|
}
|
|
}
|
|
if (!collapsed) {
|
|
val feedGroup = feed as FeedGroup
|
|
feedGroup.feedList.forEach {
|
|
Log.w(null, it.name.toString())
|
|
Card(
|
|
modifier = Modifier
|
|
.fillMaxWidth()
|
|
.padding(start = 25.dp, top = 5.dp, end = 4.dp, bottom = 5.dp)
|
|
.clickable {
|
|
/*feeds.limit = if (feeds.limit == it) {
|
|
null
|
|
} else it*/
|
|
viewModel.changeFeed()
|
|
},
|
|
elevation = 10.dp,
|
|
backgroundColor = MaterialTheme.colors.surface,
|
|
) {
|
|
it.name?.let {
|
|
Text(
|
|
text = it,
|
|
fontSize = 26.sp,
|
|
maxLines = 1,
|
|
overflow = TextOverflow.Ellipsis,
|
|
modifier = Modifier.padding(all = 4.dp)
|
|
)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
@Composable
|
|
fun SideBar(
|
|
drawerState: DrawerState,
|
|
content: @Composable () -> Unit
|
|
) {
|
|
val feedList = viewModel.feedsState?.value ?: emptyList()
|
|
ModalDrawer(drawerState = drawerState, drawerContent = {
|
|
LazyColumn(modifier = Modifier.fillMaxHeight()) {
|
|
item {
|
|
Card(
|
|
modifier = Modifier
|
|
.fillMaxWidth()
|
|
.padding(vertical = 5.dp, horizontal = 4.dp)
|
|
.clickable {
|
|
//feeds.limit = null
|
|
viewModel.changeFeed()
|
|
},
|
|
elevation = 10.dp,
|
|
backgroundColor = MaterialTheme.colors.surface,
|
|
) {
|
|
Text(
|
|
text = stringResource(id = R.string.all_feeds),
|
|
fontSize = 26.sp,
|
|
maxLines = 1,
|
|
overflow = TextOverflow.Ellipsis,
|
|
modifier = Modifier.padding(all = 4.dp)
|
|
)
|
|
}
|
|
}
|
|
items(feedList.size) { index ->
|
|
FeedSideBar(feed = feedList[index])
|
|
}
|
|
}
|
|
|
|
}, content = content)
|
|
}
|
|
/*
|
|
@Preview(showBackground = true)
|
|
@Composable
|
|
fun DefaultPreview() {
|
|
AnotherFeedReaderTheme {
|
|
Column() {
|
|
MenuBar({})
|
|
val article = Article(null,
|
|
title = "Teszt Article",
|
|
description = "Ez az cikk azért létezik, hogy kipróbájam a cikkek megjelenítését.",
|
|
author = "Toldi Balázs",
|
|
pubDate_stamp = LocalDateTime.now().toEpochSecond(ZoneOffset.UTC),
|
|
feed = Feed("Test 1")
|
|
)
|
|
Column(
|
|
modifier = Modifier.padding(horizontal = 1.dp)
|
|
) {
|
|
showArticle(article)
|
|
showArticle(
|
|
Article(null,
|
|
title = "Teszt Article #2",
|
|
description = "Ez egy másik megjelenítést tesztelő cikk. Ebben egy nagyon hosszú szöveget akarok ábrázolni, aminek nem szabad egyben kiférnie. Legfeljebb 2 sort jeleníthet meg",
|
|
author = "Toldi Balázs",
|
|
pubDate_stamp = LocalDateTime.now().plusDays(-1).toEpochSecond(
|
|
ZoneOffset.UTC),
|
|
feed = Feed("Test 1")
|
|
)
|
|
)
|
|
}
|
|
|
|
}
|
|
}
|
|
}*/
|
|
|
|
|
|
} |