Skip to content

Commit

Permalink
[feature|optimize] Support show RSS icon; support directly download m…
Browse files Browse the repository at this point in the history
…agnet/torrent link; support show all articles; replaced Filled icon with Outlined icon
  • Loading branch information
SkyD666 committed Apr 27, 2024
1 parent 21c3c62 commit b6634cd
Show file tree
Hide file tree
Showing 49 changed files with 614 additions and 485 deletions.
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -70,7 +70,7 @@

## 🌏 Translation

如果你有興趣,請幫我們翻譯,謝謝。
If you are interested, please help us **translate**, thank you.

<a title="Crowdin" target="_blank" href="https://crowdin.com/project/anivu"><img src="https://badges.crowdin.net/anivu/localized.svg"></a>

Expand Down
4 changes: 2 additions & 2 deletions app/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -20,8 +20,8 @@ android {
applicationId = "com.skyd.anivu"
minSdk = 24
targetSdk = 34
versionCode = 14
versionName = "1.1-beta15"
versionCode = 15
versionName = "1.1-beta16"

testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"

Expand Down
34 changes: 34 additions & 0 deletions app/src/main/java/com/skyd/anivu/model/bean/FaviconBean.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
package com.skyd.anivu.model.bean

import androidx.annotation.Keep
import com.skyd.anivu.base.BaseBean
import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable

@Keep
@Serializable
data class FaviconBean(
@SerialName("url")
val url: String?,
@SerialName("icons")
val icons: List<FaviconItem>?,
) : BaseBean {
@Keep
@Serializable
data class FaviconItem(
@SerialName("url")
val url: String?,
@SerialName("width")
val width: Int?,
@SerialName("height")
val height: Int?,
@SerialName("format")
val format: String?,
@SerialName("bytes")
val bytes: Long?,
@SerialName("error")
val error: String?,
@SerialName("sha1sum")
val sha1sum: String?,
) : BaseBean
}
3 changes: 3 additions & 0 deletions app/src/main/java/com/skyd/anivu/model/bean/FeedBean.kt
Original file line number Diff line number Diff line change
Expand Up @@ -38,5 +38,8 @@ data class FeedBean(
const val ICON_COLUMN = "icon"
const val GROUP_ID_COLUMN = "groupId"
const val NICKNAME_COLUMN = "nickname"

fun FeedBean.isDefaultGroup(): Boolean =
this.groupId == null || this.groupId == GroupBean.DEFAULT_GROUP_ID
}
}
17 changes: 10 additions & 7 deletions app/src/main/java/com/skyd/anivu/model/bean/GroupBean.kt
Original file line number Diff line number Diff line change
Expand Up @@ -15,12 +15,12 @@ const val GROUP_TABLE_NAME = "Group"
@Parcelize
@Serializable
@Entity(tableName = GROUP_TABLE_NAME)
data class GroupBean(
open class GroupBean(
@PrimaryKey
@ColumnInfo(name = GROUP_ID_COLUMN)
val groupId: String,
@ColumnInfo(name = NAME_COLUMN)
val name: String,
open val name: String,
) : BaseBean, Parcelable {
override fun equals(other: Any?): Boolean {
if (this === other) return true
Expand All @@ -32,16 +32,19 @@ data class GroupBean(
return groupId.hashCode()
}

object DefaultGroup :
GroupBean(DEFAULT_GROUP_ID, appContext.getString(R.string.default_feed_group)) {
private fun readResolve(): Any = DefaultGroup
override val name: String
get() = appContext.getString(R.string.default_feed_group)
}

companion object {
const val DEFAULT_GROUP_ID = "default"

const val NAME_COLUMN = "name"
const val GROUP_ID_COLUMN = "groupId"

val defaultGroup
get() = GroupBean(
groupId = DEFAULT_GROUP_ID,
name = appContext.getString(R.string.default_feed_group)
)
fun GroupBean.isDefaultGroup(): Boolean = this.groupId == DEFAULT_GROUP_ID
}
}
4 changes: 2 additions & 2 deletions app/src/main/java/com/skyd/anivu/model/db/dao/ArticleDao.kt
Original file line number Diff line number Diff line change
Expand Up @@ -98,11 +98,11 @@ interface ArticleDao {
@Query(
"""
SELECT * FROM $ARTICLE_TABLE_NAME
WHERE ${ArticleBean.FEED_URL_COLUMN} = :feedUrl
WHERE ${ArticleBean.FEED_URL_COLUMN} IN (:feedUrls)
ORDER BY ${ArticleBean.DATE_COLUMN} DESC
"""
)
fun getArticlePagingSource(feedUrl: String): PagingSource<Int, ArticleWithFeed>
fun getArticlePagingSource(feedUrls: List<String>): PagingSource<Int, ArticleWithFeed>

@Transaction
@RawQuery(observedEntities = [ArticleBean::class])
Expand Down
10 changes: 10 additions & 0 deletions app/src/main/java/com/skyd/anivu/model/db/dao/FeedDao.kt
Original file line number Diff line number Diff line change
Expand Up @@ -93,6 +93,16 @@ interface FeedDao {
)
suspend fun moveFeedToGroup(fromGroupId: String?, toGroupId: String?): Int

@Transaction
@Query(
"""
UPDATE $FEED_TABLE_NAME
SET ${FeedBean.ICON_COLUMN} = :icon
WHERE ${FeedBean.URL_COLUMN} = :feedUrl
"""
)
suspend fun updateFeedIcon(feedUrl: String, icon: String?): Int

@Transaction
@Query("SELECT * FROM $FEED_TABLE_NAME")
fun getFeedPagingSource(): PagingSource<Int, FeedBean>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,11 +4,13 @@ import androidx.paging.Pager
import androidx.paging.PagingConfig
import androidx.paging.PagingData
import com.skyd.anivu.base.BaseRepository
import com.skyd.anivu.model.bean.ArticleWithEnclosureBean
import com.skyd.anivu.model.bean.ArticleWithFeed
import com.skyd.anivu.model.db.dao.ArticleDao
import com.skyd.anivu.model.db.dao.FeedDao
import kotlinx.coroutines.Deferred
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.async
import kotlinx.coroutines.coroutineScope
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.flow
import kotlinx.coroutines.flow.flowOn
Expand All @@ -20,28 +22,43 @@ class ArticleRepository @Inject constructor(
private val rssHelper: RssHelper,
private val pagingConfig: PagingConfig,
) : BaseRepository() {
fun requestArticleList(feedUrl: String): Flow<PagingData<ArticleWithFeed>> {
fun requestArticleList(feedUrls: List<String>): Flow<PagingData<ArticleWithFeed>> {
return Pager(pagingConfig) {
articleDao.getArticlePagingSource(feedUrl)
articleDao.getArticlePagingSource(feedUrls)
}.flow.flowOn(Dispatchers.IO)
}

fun refreshArticleList(feedUrl: String): Flow<Unit> {
fun refreshArticleList(feedUrls: List<String>): Flow<Unit> {
return flow {
val articleBeanList = rssHelper.queryRssXml(
feed = feedDao.getFeed(feedUrl),
latestLink = articleDao.queryLatestByFeedUrl(feedUrl)?.link
).ifEmpty {
emit(Unit)
return@flow
}
emit(articleDao.insertListIfNotExist(articleBeanList.map { articleWithEnclosure ->
if (articleWithEnclosure.article.feedUrl != feedUrl) {
articleWithEnclosure.copy(
article = articleWithEnclosure.article.copy(feedUrl = feedUrl)
)
} else articleWithEnclosure
}))
emit(coroutineScope {
val requests = mutableListOf<Deferred<Unit>>()
feedUrls.forEach { feedUrl ->
requests += async {
val articleBeanListAsync = async {
rssHelper.queryRssXml(
feed = feedDao.getFeed(feedUrl),
latestLink = articleDao.queryLatestByFeedUrl(feedUrl)?.link
)
}
val iconAsync = async { rssHelper.getRssIcon(feedUrl) }
val articleBeanList = articleBeanListAsync.await()

if (articleBeanList.isEmpty()) return@async

val icon = iconAsync.await()
if (icon != null) feedDao.updateFeedIcon(feedUrl, icon)

articleDao.insertListIfNotExist(articleBeanList.map { articleWithEnclosure ->
if (articleWithEnclosure.article.feedUrl != feedUrl) {
articleWithEnclosure.copy(
article = articleWithEnclosure.article.copy(feedUrl = feedUrl)
)
} else articleWithEnclosure
})
}
}
requests.forEach { it.await() }
})
}.flowOn(Dispatchers.IO)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ class FeedRepository @Inject constructor(
groupList to feedDao.getFeedsNotIn(groupIds)
}.map { (groupList, defaultFeeds) ->
val dataList = mutableListOf<Any>()
dataList.add(GroupBean.defaultGroup)
dataList.add(GroupBean.DefaultGroup)
dataList.addAll(defaultFeeds)
groupList.forEach { group ->
dataList.add(group.group)
Expand Down
58 changes: 14 additions & 44 deletions app/src/main/java/com/skyd/anivu/model/repository/RssHelper.kt
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
package com.skyd.anivu.model.repository

import android.content.Context
import android.util.Log
import com.rometools.rome.feed.synd.SyndEntry
import com.rometools.rome.io.SyndFeedInput
Expand All @@ -11,13 +10,14 @@ import com.skyd.anivu.model.bean.ArticleWithEnclosureBean
import com.skyd.anivu.model.bean.EnclosureBean
import com.skyd.anivu.model.bean.FeedBean
import com.skyd.anivu.model.bean.FeedWithArticleBean
import com.skyd.anivu.model.db.dao.FeedDao
import dagger.hilt.android.qualifiers.ApplicationContext
import com.skyd.anivu.model.service.HttpService
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.async
import kotlinx.coroutines.withContext
import okhttp3.OkHttpClient
import okhttp3.Request
import okhttp3.executeAsync
import retrofit2.Retrofit
import java.io.InputStream
import java.util.*
import javax.inject.Inject
Expand All @@ -26,20 +26,21 @@ import javax.inject.Inject
* Some operations on RSS.
*/
class RssHelper @Inject constructor(
@ApplicationContext
private val context: Context,
private val retrofit: Retrofit,
private val okHttpClient: OkHttpClient,
) {

@Throws(Exception::class)
suspend fun searchFeed(url: String): FeedWithArticleBean {
return withContext(Dispatchers.IO) {
val iconAsync = async { getRssIcon(url) }
val syndFeed = SyndFeedInput().build(XmlReader(inputStream(okHttpClient, url)))
val feed = FeedBean(
url = url,
title = syndFeed.title,
description = syndFeed.description,
link = syndFeed.link,
icon = syndFeed.icon?.link ?: iconAsync.await(),
)
val list = syndFeed.entries.map { article(feed, it) }
FeedWithArticleBean(feed, list)
Expand Down Expand Up @@ -109,6 +110,14 @@ class RssHelper @Inject constructor(
)
}

suspend fun getRssIcon(url: String): String? {
return runCatching {
retrofit.create(HttpService::class.java)
.requestFavicon(url)
.icons?.firstOrNull { it.width != null && it.width >= 20 }?.url
}.onFailure { it.printStackTrace() }.getOrNull()
}

fun findImg(rawDescription: String): String? {
// From: https://gitlab.com/spacecowboy/Feeder
// Using negative lookahead to skip data: urls, being inline base64
Expand All @@ -118,45 +127,6 @@ class RssHelper @Inject constructor(
return regex.find(rawDescription)?.groupValues?.get(2)?.takeIf { !it.startsWith("data:") }
}

@Throws(Exception::class)
suspend fun queryRssIcon(
feedDao: FeedDao,
feed: FeedBean,
articleLink: String,
) {
withContext(Dispatchers.IO) {
val domainRegex = Regex("(http|https)://(www.)?(\\w+(\\.)?)+")
val request = response(okHttpClient, articleLink)
val content = request.body.string()
val regex = Regex("""<link(.+?)rel="shortcut icon"(.+?)href="(.+?)"""")
var iconLink = regex
.find(content)
?.groups?.get(3)
?.value
Log.i("rlog", "queryRssIcon: $iconLink")
if (iconLink != null) {
if (iconLink.startsWith("//")) {
iconLink = "http:$iconLink"
}
if (iconLink.startsWith("/")) {
iconLink = "${domainRegex.find(articleLink)?.value}$iconLink"
}
saveRssIcon(feedDao, feed, iconLink)
} else {
domainRegex.find(articleLink)?.value?.let {
Log.i("RLog", "favicon: $it")
if (response(okHttpClient, "$it/favicon.ico").isSuccessful) {
saveRssIcon(feedDao, feed, it)
}
}
}
}
}

private suspend fun saveRssIcon(feedDao: FeedDao, feed: FeedBean, iconLink: String) {
feedDao.setFeed(feed.copy(icon = iconLink))
}

private suspend fun inputStream(
client: OkHttpClient,
url: String,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,6 @@ import com.skyd.anivu.ext.dataStore
import com.skyd.anivu.ext.getOrDefault
import com.skyd.anivu.model.bean.ARTICLE_TABLE_NAME
import com.skyd.anivu.model.bean.ArticleBean
import com.skyd.anivu.model.bean.ArticleWithEnclosureBean
import com.skyd.anivu.model.bean.ArticleWithFeed
import com.skyd.anivu.model.bean.FEED_TABLE_NAME
import com.skyd.anivu.model.bean.FeedBean
Expand Down Expand Up @@ -92,16 +91,18 @@ class SearchRepository @Inject constructor(
}

fun requestSearchArticle(
feedUrl: String? = null,
feedUrls: List<String>,
query: String,
): Flow<PagingData<ArticleWithFeed>> {
return flow {
emit(
genSql(
tableName = ARTICLE_TABLE_NAME,
k = query,
leadingFilter = if (feedUrl == null) "1"
else "${ArticleBean.FEED_URL_COLUMN} = ${DatabaseUtils.sqlEscapeString(feedUrl)}",
leadingFilter = if (feedUrls.isEmpty()) "1"
else "${ArticleBean.FEED_URL_COLUMN} IN (${
feedUrls.joinToString(", ") { DatabaseUtils.sqlEscapeString(it) }
})",
)
)
}.flatMapConcat { sql ->
Expand Down
5 changes: 5 additions & 0 deletions app/src/main/java/com/skyd/anivu/model/service/HttpService.kt
Original file line number Diff line number Diff line change
@@ -1,11 +1,16 @@
package com.skyd.anivu.model.service

import com.skyd.anivu.model.bean.FaviconBean
import okhttp3.ResponseBody
import retrofit2.Call
import retrofit2.http.GET
import retrofit2.http.Query
import retrofit2.http.Url

interface HttpService {
@GET
fun requestGetResponseBody(@Url url: String): Call<ResponseBody>

@GET("https://besticon-demo.herokuapp.com/allicons.json")
suspend fun requestFavicon(@Query("url") url: String): FaviconBean
}
Loading

0 comments on commit b6634cd

Please sign in to comment.