Big progress on persistent data storage
All checks were successful
continuous-integration/drone/push Build is passing

Signed-off-by: Balazs Toldi <balazs@toldi.eu>
This commit is contained in:
Balazs Toldi 2021-10-23 23:54:47 +02:00
parent 90da45609f
commit a4b3e7e80a
Signed by: Bazsalanszky
GPG key ID: 933820884952BE27
10 changed files with 147 additions and 174 deletions

View file

@ -26,6 +26,10 @@
<entry key="../../../../../layout/compose-model-1633889971370.xml" value="0.5796296296296296" />
<entry key="../../../../../layout/compose-model-1633894374457.xml" value="0.575925925925926" />
<entry key="../../../../../layout/compose-model-1633894712445.xml" value="0.5" />
<entry key="../../../../../layout/compose-model-1634049545704.xml" value="0.5722222222222222" />
<entry key="../../../../../layout/compose-model-1634049671897.xml" value="0.2777777777777778" />
<entry key="../../../../../layout/compose-model-1634139145792.xml" value="0.8684834123222749" />
<entry key="../../../../../layout/compose-model-1635019781952.xml" value="0.5435185185185185" />
<entry key="app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml" value="0.21458333333333332" />
</map>
</option>

View file

@ -29,7 +29,6 @@ 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.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import coil.compose.rememberImagePainter
@ -38,16 +37,10 @@ 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.sqlite.Repo
import eu.toldi.balazs.anotherfeedreader.ui.theme.AnotherFeedReaderTheme
import eu.toldi.balazs.anotherfeedreader.viewmodel.FeedViewModel
import kotlinx.coroutines.Dispatchers.IO
import kotlinx.coroutines.GlobalScope
import kotlinx.coroutines.Job
import kotlinx.coroutines.launch
import java.net.URL
import java.time.LocalDateTime
import java.time.ZoneOffset
import java.time.format.DateTimeFormatter
import java.time.format.FormatStyle
@ -104,8 +97,11 @@ class MainActivity : ComponentActivity() {
) {
items(articleList.value.size) { index ->
val article = articleList.value[index]
showArticle(article = article)
val articleAndFeed = articleList.value[index]
articleAndFeed.articles.sortedByDescending { it.pubDate }
.forEach {
showArticle(articleAndFeed.feed, article = it)
}
}
}
}
@ -150,7 +146,7 @@ class MainActivity : ComponentActivity() {
) {
Text(
text = if (selectedIndex == 0) "None" else viewModel.feedsState.value.filterIsInstance<FeedGroup>()[selectedIndex - 1].name.toString(),
text = if (selectedIndex == 0) "None" else viewModel.feedsState.value!!.filterIsInstance<FeedGroup>()[selectedIndex - 1].name.toString(),
modifier = Modifier
.fillMaxWidth()
.clickable(onClick = { expanded = true })
@ -166,13 +162,14 @@ class MainActivity : ComponentActivity() {
}) {
Text(text = "None")
}
viewModel.feedsState.value.filterIsInstance<FeedGroup>()
viewModel.feedsState.value!!.filterIsInstance<FeedGroup>()
.forEachIndexed { index, s ->
DropdownMenuItem(onClick = {
selectedIndex = index + 1
expanded = false
}) {
viewModel.feedsState.value[index].name?.let {
viewModel.feedsState.value!![index].name?.let {
Text(text = it)
}
}
@ -196,12 +193,11 @@ class MainActivity : ComponentActivity() {
onClick = {
try {
val feed = Feed(null,text)
syncJob = GlobalScope.launch(IO) { feed.updateFeed() }
if (selectedIndex == 0)
viewModel.addFeed(feed)
else {
var counter = 0
viewModel.feedsState.value.forEachIndexed { index, feedGroup ->
viewModel.feedsState.value!!.forEachIndexed { index, feedGroup ->
if (feedGroup is FeedGroup) {
counter++
if (counter == selectedIndex) {
@ -260,12 +256,23 @@ class MainActivity : ComponentActivity() {
}
@Composable
fun showArticle(article: Article) {
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 {
@ -290,20 +297,7 @@ class MainActivity : ComponentActivity() {
top = 2.dp,
bottom = 6.dp,
end = 6.dp
)
.clickable {
WebView.webURL = article.url.toString()
WebView.barTitle =
article.feed?.name.toString() ?: ""
startActivity(
Intent(
baseContext,
WebView::class.java
)
)
},
),
elevation = 10.dp,
backgroundColor = MaterialTheme.colors.surface,
shape = RoundedCornerShape(10.dp)
@ -314,8 +308,8 @@ class MainActivity : ComponentActivity() {
horizontalArrangement = Arrangement.Start,
verticalAlignment = Alignment.CenterVertically
) {
val painter = if (article.feed?.faviconURL != null) {
rememberImagePainter(article.feed.faviconURL)
val painter = if (feed.faviconURL != null) {
rememberImagePainter(feed.faviconURL)
} else {
painterResource(id = R.drawable.ic_launcher_background)
}
@ -337,7 +331,7 @@ class MainActivity : ComponentActivity() {
modifier = Modifier.padding(horizontal = 2.dp)
)
}
article.feed?.name?.let {
feed.name?.let {
Text(
text = (fun(): String {
return if (it.length > 25)
@ -476,7 +470,7 @@ class MainActivity : ComponentActivity() {
drawerState: DrawerState,
content: @Composable () -> Unit
) {
val feedList = viewModel.feedsState.value
val feedList = viewModel.feedsState?.value ?: emptyList()
ModalDrawer(drawerState = drawerState, drawerContent = {
LazyColumn(modifier = Modifier.fillMaxHeight()) {
item {
@ -507,7 +501,7 @@ class MainActivity : ComponentActivity() {
}, content = content)
}
/*
@Preview(showBackground = true)
@Composable
fun DefaultPreview() {
@ -539,7 +533,7 @@ class MainActivity : ComponentActivity() {
}
}
}
}*/
}

View file

@ -5,8 +5,6 @@ import androidx.room.Entity
import androidx.room.Ignore
import androidx.room.PrimaryKey
import kotlinx.coroutines.Dispatchers.IO
import kotlinx.coroutines.GlobalScope
import kotlinx.coroutines.launch
import kotlinx.coroutines.runBlocking
import org.jsoup.Jsoup
import org.w3c.dom.Element
@ -74,7 +72,7 @@ data class Article(
}
@ColumnInfo(name = "feedId")
@ColumnInfo(name = "containingFeedId")
var feedId: Long = 0
companion object {
/**

View file

@ -1,23 +1,12 @@
package eu.toldi.balazs.anotherfeedreader.entities
import android.util.Log
import androidx.room.ColumnInfo
import androidx.room.Entity
import androidx.room.Ignore
import androidx.room.PrimaryKey
import eu.toldi.balazs.anotherfeedreader.MainActivity
import eu.toldi.balazs.anotherfeedreader.sqlite.Repo
import kotlinx.coroutines.*
import kotlinx.coroutines.Dispatchers.IO
import net.mm2d.touchicon.TouchIconExtractor
import org.w3c.dom.Document
import org.w3c.dom.Node
import org.w3c.dom.NodeList
import java.net.URL
import javax.xml.parsers.DocumentBuilder
import javax.xml.parsers.DocumentBuilderFactory
@Entity
@Entity(tableName = "feed")
open class Feed
(
@PrimaryKey(autoGenerate = true)
@ -34,7 +23,7 @@ open class Feed
/**
* A hírcsatorna neve
*/
@ColumnInfo(name = "title")
@ColumnInfo(name = "name")
var name: String? = null
@Ignore
val link = URL(feedURL)
@ -46,6 +35,7 @@ open class Feed
/**
* A hírcsatornához tartozó cikkek listája
*/
@Ignore
open var articleList: List<Article> = emptyList()
/**
@ -64,42 +54,6 @@ open class Feed
return articleList[i]
}
/**
* Hírcsatorna frissítése.
* Lekéri a hírcsatorna elérési útvonalán található összes új cikket és hozzáadja a cikkek listájához.
*
*/
open suspend fun updateFeed() {
val factory: DocumentBuilderFactory = DocumentBuilderFactory.newInstance()
val builder: DocumentBuilder = factory.newDocumentBuilder()
var doc: Document = withContext(Dispatchers.IO) {
try {
builder.parse(link.toString())
} catch (e: Exception) {
Log.e(null, e.stackTraceToString())
builder.newDocument()
}
}
if (doc.getElementsByTagName("title").length > 0)
name = doc.getElementsByTagName("title").item(0).getTextContent()
val items: NodeList = doc.getElementsByTagName("item")
for (i in 0 until items.length) {
val article: Node = items.item(i)
val a = withContext(Dispatchers.IO) {Article.createFromNode(article, this@Feed)}
addArticle(a)
}
articleList.sortedByDescending { it.pubDate }
faviconURL = withContext(Dispatchers.IO) {
val extractor = TouchIconExtractor()
val icons = extractor.fromPage("https://" + link.host)
if (icons.size > 0)
icons.maxByOrNull { it.inferArea() }?.url
else null
}
Log.e(null, faviconURL.toString())
}
/**
* Megnézi hogy egy magadott cikk megtalálható-e a hírcsatornán
* @param a a keresett cikk

View file

@ -1,6 +1,5 @@
package eu.toldi.balazs.anotherfeedreader.entities
import java.io.IOException
import java.util.*
@ -45,24 +44,6 @@ open class FeedGroup(name: String) : Feed(name) {
return false
}
/**
* @see Feed.updateFeed
* @throws IOException
*/
override suspend fun updateFeed() {
if (feedList == null) feedList = ArrayList()
if (articleList == null) articleList = ArrayList()
feedList.forEach { f ->
f.updateFeed()
f.articleList.forEach {
if (!hasAricle(it))
articleList += it
}
}
articleList = articleList.sortedByDescending { it.pubDate }
}
/**
* @see Feed.getArticleCount
* @return

View file

@ -1,58 +1,92 @@
package eu.toldi.balazs.anotherfeedreader.repository
import android.util.Log
import androidx.compose.runtime.MutableState
import androidx.compose.runtime.State
import androidx.compose.runtime.mutableStateOf
import androidx.lifecycle.LiveData
import eu.toldi.balazs.anotherfeedreader.entities.Article
import eu.toldi.balazs.anotherfeedreader.entities.Feed
import eu.toldi.balazs.anotherfeedreader.entities.FeedData
import eu.toldi.balazs.anotherfeedreader.entities.FeedGroup
import eu.toldi.balazs.anotherfeedreader.sqlite.ArticleDAO
import eu.toldi.balazs.anotherfeedreader.sqlite.FeedsAndArticles
import eu.toldi.balazs.anotherfeedreader.sqlite.FeedsAndArticlesDAO
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Dispatchers.IO
import kotlinx.coroutines.withContext
import java.net.URL
import net.mm2d.touchicon.TouchIconExtractor
import org.w3c.dom.Document
import org.w3c.dom.Node
import org.w3c.dom.NodeList
import javax.xml.parsers.DocumentBuilder
import javax.xml.parsers.DocumentBuilderFactory
class FeedRepository(private val articleDAO: ArticleDAO) {
class FeedRepository(private val feedsAndArticleDAO: FeedsAndArticlesDAO) {
val feedData = FeedData().apply {
addFeed(FeedGroup("magyar").apply {
addFeed(Feed(feedURL = "https://hvg.hu/rss"))
addFeed(Feed(feedURL = "https://telex.hu/rss"))
addFeed(Feed(feedURL = "https://24.hu/feed"))
addFeed(Feed(feedURL = "https://444.hu/feed"))
})
addFeed(Feed(feedURL = "https://www.theguardian.com/world/rss"))
private val _isRefereshing: MutableState<Boolean> = mutableStateOf(false)
public val isRefeshing: State<Boolean>
get() = _isRefereshing
fun getAllArticles(): LiveData<List<FeedsAndArticles>> {
return feedsAndArticleDAO.getAll()
}
fun getAllArticles(): LiveData<List<Article>> {
return articleDAO.getAll()
suspend fun instert(feed: Feed, article: Article) {
feedsAndArticleDAO.insertArticle(feed, article)
}
suspend fun instert(article: Article){
articleDAO.insertAll(article)
}
suspend fun delete(article: Article){
articleDAO.delete(article)
}
fun getFeedList() : List<Feed> = feedData.feedList
fun getFeedList(): LiveData<List<Feed>> = feedsAndArticleDAO.getFeeds()
suspend fun updateFeeds() {
var done = false
feedData.limit?.let {
it.updateFeed()
done = true
withContext(IO) {
try {
_isRefereshing.value = true
val feeds = feedsAndArticleDAO.getFeedsList()
feeds?.forEach { feed ->
updateFeed(feed)
}
if (!done)
feedData.updateFeed()
_isRefereshing.value = false
} catch (e: Exception) {
feedData.articleList.forEach {
withContext(IO){
instert(it)
}
}
}
fun addFeed(feed: Feed) {
feedData.addFeed(feed)
private suspend fun updateFeed(feed: Feed) {
val factory: DocumentBuilderFactory = DocumentBuilderFactory.newInstance()
val builder: DocumentBuilder = factory.newDocumentBuilder()
var doc: Document = withContext(Dispatchers.IO) {
try {
builder.parse(feed.link.toString())
} catch (e: Exception) {
Log.e(null, e.stackTraceToString())
builder.newDocument()
}
}
if (doc.getElementsByTagName("title").length > 0)
feed.name = doc.getElementsByTagName("title").item(0).getTextContent()
val items: NodeList = doc.getElementsByTagName("item")
for (i in 0 until items.length) {
val article: Node = items.item(i)
val a = withContext(Dispatchers.IO) { Article.createFromNode(article, feed = feed) }
feedsAndArticleDAO.insertArticle(feed, a)
}
feed.faviconURL = withContext(Dispatchers.IO) {
val extractor = TouchIconExtractor()
val icons = extractor.fromPage("https://" + feed.link.host)
if (icons.size > 0)
icons.maxByOrNull { it.inferArea() }?.url
else null
}
feedsAndArticleDAO.updateFeed(feed)
}
suspend fun addFeed(feed: Feed) {
feedsAndArticleDAO.insertFeed(feed)
updateFeeds()
}
}

View file

@ -5,10 +5,12 @@ import androidx.room.Database
import androidx.room.Room
import androidx.room.RoomDatabase
import eu.toldi.balazs.anotherfeedreader.entities.Article
import eu.toldi.balazs.anotherfeedreader.entities.Feed
@Database(entities = [Article::class], version = 1)
@Database(entities = [Article::class, Feed::class], version = 4)
abstract class AppDatabase : RoomDatabase() {
abstract fun articleDao(): ArticleDAO
abstract fun feedsAndArticlesDao(): FeedsAndArticlesDAO
companion object {
@Volatile

View file

@ -9,7 +9,7 @@ data class FeedsAndArticles(
@Embedded val feed: Feed,
@Relation(
parentColumn = "feedId",
entityColumn = "feedId"
entityColumn = "containingFeedId"
)
val articles: List<Article>
)

View file

@ -3,22 +3,35 @@ package eu.toldi.balazs.anotherfeedreader.sqlite
import androidx.lifecycle.LiveData
import androidx.room.*
import eu.toldi.balazs.anotherfeedreader.entities.Article
import eu.toldi.balazs.anotherfeedreader.entities.Feed
@Dao
interface FeedsAndArticlesDAO {
@Transaction
@Query("SELECT * FROM feed")
@Query("SELECT * FROM feed INNER JOIN article ON feed.feedId = article.containingFeedId ORDER BY article.pubDate")
fun getAll(): LiveData<List<FeedsAndArticles>>
@Query("SELECT * FROM article WHERE articleId IN (:articleIds)")
fun loadAllByIds(articleIds: IntArray): List<Article>
@Query("SELECT * FROM feed")
fun getFeeds(): LiveData<List<Feed>>
@Query("SELECT * FROM article WHERE url=:url")
fun findArticleByURL(url: String): Article?
@Query("SELECT * FROM feed")
fun getFeedsList(): List<Feed>
fun insertArticle(feed: Feed, article: Article) {
feed.feedId?.let { article.feedId = it }
_insertArticles(listOf(article))
}
@Insert
fun insertArticles(vararg articles: Article)
fun _insertArticles(articles: List<Article>)
@Update
fun updateFeed(feed: Feed)
@Insert
fun insertFeed(feed: Feed)
@Delete
fun deleteArticle(article: Article)

View file

@ -1,62 +1,55 @@
package eu.toldi.balazs.anotherfeedreader.viewmodel
import android.app.Application
import androidx.compose.runtime.MutableState
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.State
import androidx.lifecycle.AndroidViewModel
import androidx.lifecycle.LiveData
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import eu.toldi.balazs.anotherfeedreader.entities.Article
import eu.toldi.balazs.anotherfeedreader.entities.Feed
import eu.toldi.balazs.anotherfeedreader.entities.FeedData
import eu.toldi.balazs.anotherfeedreader.entities.FeedGroup
import eu.toldi.balazs.anotherfeedreader.repository.FeedRepository
import eu.toldi.balazs.anotherfeedreader.sqlite.AppDatabase
import eu.toldi.balazs.anotherfeedreader.sqlite.FeedsAndArticles
import kotlinx.coroutines.Dispatchers.IO
import kotlinx.coroutines.Job
import kotlinx.coroutines.launch
import java.net.URL
class FeedViewModel(application: Application) : AndroidViewModel(application) {
private val feedRepo: FeedRepository
val articles: LiveData<List<Article>>
val feedsState = mutableStateOf(emptyList<Feed>())
val isRefereshing: MutableState<Boolean> = mutableStateOf(false)
val articles: LiveData<List<FeedsAndArticles>>
val feedsState: LiveData<List<Feed>>
val isRefereshing: State<Boolean>
init {
val articleDAO = AppDatabase.getInstance(application).articleDao()
feedRepo = FeedRepository(articleDAO = articleDAO)
val feedsAndArticlesDao = AppDatabase.getInstance(application).feedsAndArticlesDao()
feedRepo = FeedRepository(feedsAndArticleDAO = feedsAndArticlesDao)
articles = feedRepo.getAllArticles()
viewModelScope.launch {
feedsState.value = feedRepo.getFeedList()
}
feedsState = feedRepo.getFeedList()
isRefereshing = feedRepo.isRefeshing
}
fun updateFeeds(): Job {
return viewModelScope.launch {
isRefereshing.value = true
return viewModelScope.launch(IO) {
feedRepo.updateFeeds()
isRefereshing.value = false
}
}
fun changeFeed() {
viewModelScope.launch {
isRefereshing.value = true
///articles.value = feedRepo.getFeedList()
isRefereshing.value = false
}
}
fun changeFeedList() {
viewModelScope.launch {
feedsState.value = feedRepo.getFeedList()
//feedsState.value = feedRepo.getFeedList().value
}
}
fun addFeed(feed: Feed){
fun addFeed(feed: Feed) {
viewModelScope.launch(IO) {
feedRepo.addFeed(feed)
feedsState.value = feedRepo.getFeedList()
}
}
}