From 5528fbdfa3c676dfbebbbbece652458b129211e1 Mon Sep 17 00:00:00 2001 From: SkyD666 Date: Sat, 31 Aug 2024 23:47:30 +0800 Subject: [PATCH] [feature] Support displaying media previews on the reading page; support partial media RSS fields (#49); support displaying category tags on the reading page --- app/build.gradle.kts | 6 +- .../com/skyd/anivu/config/SearchConfig.kt | 4 +- .../java/com/skyd/anivu/di/DatabaseModule.kt | 5 + .../java/com/skyd/anivu/ext/ContextExt.kt | 20 ++ .../com/skyd/anivu/model/bean/FeedViewBean.kt | 2 + .../anivu/model/bean/FeedWithArticleBean.kt | 2 + .../anivu/model/bean/LinkEnclosureBean.kt | 4 +- .../model/bean/{ => article}/ArticleBean.kt | 11 +- .../{ => article}/ArticleWithEnclosureBean.kt | 7 +- .../bean/{ => article}/ArticleWithFeed.kt | 3 +- .../model/bean/{ => article}/EnclosureBean.kt | 23 +- .../anivu/model/bean/article/RssMediaBean.kt | 47 ++++ .../com/skyd/anivu/model/db/AppDatabase.kt | 17 +- .../model/db/converter/CategoriesConverter.kt | 20 ++ .../com/skyd/anivu/model/db/dao/ArticleDao.kt | 38 ++-- .../skyd/anivu/model/db/dao/EnclosureDao.kt | 4 +- .../com/skyd/anivu/model/db/dao/FeedDao.kt | 3 +- .../skyd/anivu/model/db/dao/RssModuleDao.kt | 25 +++ .../model/db/migration/Migration11To12.kt | 26 +++ .../anivu/model/db/migration/Migration4To5.kt | 4 +- .../anivu/model/db/migration/Migration5To6.kt | 4 +- .../anivu/model/db/migration/Migration6To7.kt | 4 +- .../model/repository/ArticleRepository.kt | 6 +- .../anivu/model/repository/ReadRepository.kt | 2 +- .../skyd/anivu/model/repository/RssHelper.kt | 49 ++++- .../model/repository/SearchRepository.kt | 6 +- .../com/skyd/anivu/ui/component/AniVuImage.kt | 15 +- .../adapter/proxy/Article1Proxy.kt | 6 +- .../ui/mpv/controller/button/Screenshot.kt | 4 +- .../article/ArticlePartialStateChange.kt | 2 +- .../anivu/ui/screen/article/ArticleScreen.kt | 2 +- .../anivu/ui/screen/article/ArticleState.kt | 2 +- .../article/enclosure/EnclosureBottomSheet.kt | 10 +- .../ui/screen/read/ReadPartialStateChange.kt | 2 +- .../skyd/anivu/ui/screen/read/ReadScreen.kt | 205 +++++++++++++++++- .../skyd/anivu/ui/screen/read/ReadState.kt | 2 +- .../anivu/ui/screen/search/SearchScreen.kt | 2 +- app/src/main/res/values-zh-rCN/strings.xml | 1 + app/src/main/res/values/strings.xml | 1 + 39 files changed, 523 insertions(+), 73 deletions(-) rename app/src/main/java/com/skyd/anivu/model/bean/{ => article}/ArticleBean.kt (87%) rename app/src/main/java/com/skyd/anivu/model/bean/{ => article}/ArticleWithEnclosureBean.kt (70%) rename app/src/main/java/com/skyd/anivu/model/bean/{ => article}/ArticleWithFeed.kt (85%) rename app/src/main/java/com/skyd/anivu/model/bean/{ => article}/EnclosureBean.kt (72%) create mode 100644 app/src/main/java/com/skyd/anivu/model/bean/article/RssMediaBean.kt create mode 100644 app/src/main/java/com/skyd/anivu/model/db/converter/CategoriesConverter.kt create mode 100644 app/src/main/java/com/skyd/anivu/model/db/dao/RssModuleDao.kt create mode 100644 app/src/main/java/com/skyd/anivu/model/db/migration/Migration11To12.kt diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 663b532b..5c20053e 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -21,8 +21,8 @@ android { applicationId = "com.skyd.anivu" minSdk = 24 targetSdk = 35 - versionCode = 22 - versionName = "2.1-alpha26" + versionCode = 23 + versionName = "2.1-alpha27" testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" @@ -113,6 +113,7 @@ android { "META-INF/jdom-info.xml", "kotlin-tooling-metadata.json", "okhttp3/internal/publicsuffix/NOTICE", + "rome-utils-*.jar", ) jniLibs { excludes += mutableSetOf( @@ -203,6 +204,7 @@ dependencies { implementation("io.coil-kt:coil-video:2.7.0") implementation("com.airbnb.android:lottie-compose:6.5.1") implementation("com.rometools:rome:2.1.0") + implementation("com.rometools:rome-modules:2.1.0") implementation("be.ceau:opml-parser:3.1.0") { exclude(group = "net.sf.kxml", module = "kxml2") } diff --git a/app/src/main/java/com/skyd/anivu/config/SearchConfig.kt b/app/src/main/java/com/skyd/anivu/config/SearchConfig.kt index c2dbbd53..343fbbe9 100644 --- a/app/src/main/java/com/skyd/anivu/config/SearchConfig.kt +++ b/app/src/main/java/com/skyd/anivu/config/SearchConfig.kt @@ -1,7 +1,7 @@ package com.skyd.anivu.config -import com.skyd.anivu.model.bean.ARTICLE_TABLE_NAME -import com.skyd.anivu.model.bean.ArticleBean +import com.skyd.anivu.model.bean.article.ARTICLE_TABLE_NAME +import com.skyd.anivu.model.bean.article.ArticleBean import com.skyd.anivu.model.bean.FEED_TABLE_NAME import com.skyd.anivu.model.bean.FEED_VIEW_NAME import com.skyd.anivu.model.bean.FeedBean diff --git a/app/src/main/java/com/skyd/anivu/di/DatabaseModule.kt b/app/src/main/java/com/skyd/anivu/di/DatabaseModule.kt index 5057b36f..f1e1343c 100644 --- a/app/src/main/java/com/skyd/anivu/di/DatabaseModule.kt +++ b/app/src/main/java/com/skyd/anivu/di/DatabaseModule.kt @@ -9,6 +9,7 @@ import com.skyd.anivu.model.db.dao.EnclosureDao import com.skyd.anivu.model.db.dao.FeedDao import com.skyd.anivu.model.db.dao.GroupDao import com.skyd.anivu.model.db.dao.MediaPlayHistoryDao +import com.skyd.anivu.model.db.dao.RssModuleDao import com.skyd.anivu.model.db.dao.SearchDomainDao import com.skyd.anivu.model.db.dao.SessionParamsDao import com.skyd.anivu.model.db.dao.TorrentFileDao @@ -62,6 +63,10 @@ object DatabaseModule { fun provideMediaPlayHistoryDao(database: AppDatabase): MediaPlayHistoryDao = database.mediaPlayHistoryDao() + @Provides + @Singleton + fun provideRssModuleDao(database: AppDatabase): RssModuleDao = database.rssModuleDao() + @Provides @Singleton diff --git a/app/src/main/java/com/skyd/anivu/ext/ContextExt.kt b/app/src/main/java/com/skyd/anivu/ext/ContextExt.kt index 112705ca..2bccd56f 100644 --- a/app/src/main/java/com/skyd/anivu/ext/ContextExt.kt +++ b/app/src/main/java/com/skyd/anivu/ext/ContextExt.kt @@ -7,6 +7,8 @@ import android.content.pm.PackageInfo import android.content.pm.PackageManager import android.content.res.Configuration import android.graphics.Point +import android.net.ConnectivityManager +import android.net.NetworkCapabilities import android.os.Build import android.os.Build.VERSION.SDK_INT import android.view.Window @@ -17,6 +19,8 @@ import coil.decode.GifDecoder import coil.decode.ImageDecoderDecoder import coil.decode.SvgDecoder import coil.decode.VideoFrameDecoder +import okhttp3.Interceptor +import okhttp3.OkHttpClient val Context.activity: Activity get() { @@ -113,6 +117,13 @@ fun Context.inDarkMode(): Boolean { Configuration.UI_MODE_NIGHT_YES } +fun Context.isWifi(): Boolean { + val connectivityManager = getSystemService(Context.CONNECTIVITY_SERVICE) as ConnectivityManager + val networkCapabilities = + connectivityManager.getNetworkCapabilities(connectivityManager.activeNetwork) + return networkCapabilities?.hasTransport(NetworkCapabilities.TRANSPORT_WIFI) == true +} + fun Context.imageLoaderBuilder(): ImageLoader.Builder { return ImageLoader.Builder(this) .components { @@ -124,4 +135,13 @@ fun Context.imageLoaderBuilder(): ImageLoader.Builder { add(SvgDecoder.Factory()) add(VideoFrameDecoder.Factory()) } + .okHttpClient { + OkHttpClient.Builder() + .addNetworkInterceptor(Interceptor { chain -> + chain.proceed(chain.request()).newBuilder() + .header("Cache-Control", "max-age=31536000,public") + .build() + }) + .build() + } } \ No newline at end of file diff --git a/app/src/main/java/com/skyd/anivu/model/bean/FeedViewBean.kt b/app/src/main/java/com/skyd/anivu/model/bean/FeedViewBean.kt index aa904419..eb6cc84f 100644 --- a/app/src/main/java/com/skyd/anivu/model/bean/FeedViewBean.kt +++ b/app/src/main/java/com/skyd/anivu/model/bean/FeedViewBean.kt @@ -5,6 +5,8 @@ import androidx.room.ColumnInfo import androidx.room.DatabaseView import androidx.room.Embedded import com.skyd.anivu.base.BaseBean +import com.skyd.anivu.model.bean.article.ARTICLE_TABLE_NAME +import com.skyd.anivu.model.bean.article.ArticleBean import kotlinx.parcelize.Parcelize import kotlinx.serialization.Serializable diff --git a/app/src/main/java/com/skyd/anivu/model/bean/FeedWithArticleBean.kt b/app/src/main/java/com/skyd/anivu/model/bean/FeedWithArticleBean.kt index ee143a59..bc98c09e 100644 --- a/app/src/main/java/com/skyd/anivu/model/bean/FeedWithArticleBean.kt +++ b/app/src/main/java/com/skyd/anivu/model/bean/FeedWithArticleBean.kt @@ -2,6 +2,8 @@ package com.skyd.anivu.model.bean import androidx.room.Embedded import androidx.room.Relation +import com.skyd.anivu.model.bean.article.ArticleBean +import com.skyd.anivu.model.bean.article.ArticleWithEnclosureBean /** * A [feed] contains many [articles]. diff --git a/app/src/main/java/com/skyd/anivu/model/bean/LinkEnclosureBean.kt b/app/src/main/java/com/skyd/anivu/model/bean/LinkEnclosureBean.kt index 64d3e814..ae1fafa1 100644 --- a/app/src/main/java/com/skyd/anivu/model/bean/LinkEnclosureBean.kt +++ b/app/src/main/java/com/skyd/anivu/model/bean/LinkEnclosureBean.kt @@ -1,10 +1,12 @@ package com.skyd.anivu.model.bean import com.skyd.anivu.base.BaseBean +import com.skyd.anivu.model.bean.article.EnclosureBean data class LinkEnclosureBean( val link: String, ) : BaseBean { val isMedia: Boolean - get() = EnclosureBean.mediaExtensions.any { link.endsWith(it) } + get() = EnclosureBean.videoExtensions.any { link.endsWith(it) } || + EnclosureBean.audioExtensions.any { link.endsWith(it) } } diff --git a/app/src/main/java/com/skyd/anivu/model/bean/ArticleBean.kt b/app/src/main/java/com/skyd/anivu/model/bean/article/ArticleBean.kt similarity index 87% rename from app/src/main/java/com/skyd/anivu/model/bean/ArticleBean.kt rename to app/src/main/java/com/skyd/anivu/model/bean/article/ArticleBean.kt index 11726009..b0cb002e 100644 --- a/app/src/main/java/com/skyd/anivu/model/bean/ArticleBean.kt +++ b/app/src/main/java/com/skyd/anivu/model/bean/article/ArticleBean.kt @@ -1,4 +1,4 @@ -package com.skyd.anivu.model.bean +package com.skyd.anivu.model.bean.article import android.os.Parcelable import androidx.room.ColumnInfo @@ -7,6 +7,7 @@ import androidx.room.ForeignKey import androidx.room.Index import androidx.room.PrimaryKey import com.skyd.anivu.base.BaseBean +import com.skyd.anivu.model.bean.FeedBean import kotlinx.parcelize.Parcelize import kotlinx.serialization.Serializable @@ -57,7 +58,14 @@ data class ArticleBean( var isRead: Boolean = false, @ColumnInfo(name = IS_FAVORITE_COLUMN) var isFavorite: Boolean = false, + @ColumnInfo(name = CATEGORIES_COLUMN) + var catrgories: Categories? = null, ) : BaseBean, Parcelable { + + @Parcelize + @Serializable + data class Categories(val categories: List) : BaseBean, Parcelable + companion object { const val ARTICLE_ID_COLUMN = "articleId" const val FEED_URL_COLUMN = "feedUrl" @@ -72,5 +80,6 @@ data class ArticleBean( const val UPDATE_AT_COLUMN = "updateAt" const val IS_READ_COLUMN = "isRead" const val IS_FAVORITE_COLUMN = "isFavorite" + const val CATEGORIES_COLUMN = "catrgories" } } \ No newline at end of file diff --git a/app/src/main/java/com/skyd/anivu/model/bean/ArticleWithEnclosureBean.kt b/app/src/main/java/com/skyd/anivu/model/bean/article/ArticleWithEnclosureBean.kt similarity index 70% rename from app/src/main/java/com/skyd/anivu/model/bean/ArticleWithEnclosureBean.kt rename to app/src/main/java/com/skyd/anivu/model/bean/article/ArticleWithEnclosureBean.kt index 3d2eab42..8d10ce41 100644 --- a/app/src/main/java/com/skyd/anivu/model/bean/ArticleWithEnclosureBean.kt +++ b/app/src/main/java/com/skyd/anivu/model/bean/article/ArticleWithEnclosureBean.kt @@ -1,4 +1,4 @@ -package com.skyd.anivu.model.bean +package com.skyd.anivu.model.bean.article import android.os.Parcelable import androidx.room.Embedded @@ -16,4 +16,9 @@ data class ArticleWithEnclosureBean( entityColumn = EnclosureBean.ARTICLE_ID_COLUMN, ) var enclosures: List, + @Relation( + parentColumn = ArticleBean.ARTICLE_ID_COLUMN, + entityColumn = RssMediaBean.ARTICLE_ID_COLUMN, + ) + var media: RssMediaBean?, ) : Serializable, Parcelable diff --git a/app/src/main/java/com/skyd/anivu/model/bean/ArticleWithFeed.kt b/app/src/main/java/com/skyd/anivu/model/bean/article/ArticleWithFeed.kt similarity index 85% rename from app/src/main/java/com/skyd/anivu/model/bean/ArticleWithFeed.kt rename to app/src/main/java/com/skyd/anivu/model/bean/article/ArticleWithFeed.kt index f171c6a2..50b9581a 100644 --- a/app/src/main/java/com/skyd/anivu/model/bean/ArticleWithFeed.kt +++ b/app/src/main/java/com/skyd/anivu/model/bean/article/ArticleWithFeed.kt @@ -1,8 +1,9 @@ -package com.skyd.anivu.model.bean +package com.skyd.anivu.model.bean.article import android.os.Parcelable import androidx.room.Embedded import androidx.room.Relation +import com.skyd.anivu.model.bean.FeedBean import kotlinx.parcelize.Parcelize import java.io.Serializable diff --git a/app/src/main/java/com/skyd/anivu/model/bean/EnclosureBean.kt b/app/src/main/java/com/skyd/anivu/model/bean/article/EnclosureBean.kt similarity index 72% rename from app/src/main/java/com/skyd/anivu/model/bean/EnclosureBean.kt rename to app/src/main/java/com/skyd/anivu/model/bean/article/EnclosureBean.kt index f3c55595..9008c089 100644 --- a/app/src/main/java/com/skyd/anivu/model/bean/EnclosureBean.kt +++ b/app/src/main/java/com/skyd/anivu/model/bean/article/EnclosureBean.kt @@ -1,4 +1,4 @@ -package com.skyd.anivu.model.bean +package com.skyd.anivu.model.bean.article import android.os.Parcelable import androidx.room.ColumnInfo @@ -40,15 +40,24 @@ data class EnclosureBean( const val LENGTH_COLUMN = "length" const val TYPE_COLUMN = "type" - val mediaExtensions = listOf( - ".m3u8", ".ogg", ".mp3", ".flac", ".m4v", ".mov", ".avi", ".webm", - ".mp4", ".mkv", ".m4a", + val videoExtensions = listOf( + ".m3u8", ".m4v", ".mov", ".avi", ".webm", + ".mp4", ".mkv", + ) + val audioExtensions = listOf( + ".ogg", ".mp3", ".flac", ".m4a", ) } val isMedia: Boolean - get() = type?.startsWith("audio/") == true || - type?.startsWith("video/") == true || + get() = isVideo || isAudio + + val isVideo: Boolean + get() = type?.startsWith("video/") == true || type == "application/vnd.apple.mpegurl" || - mediaExtensions.any { url.endsWith(it) } + videoExtensions.any { url.endsWith(it) } + + val isAudio: Boolean + get() = type?.startsWith("audio/") == true || + audioExtensions.any { url.endsWith(it) } } \ No newline at end of file diff --git a/app/src/main/java/com/skyd/anivu/model/bean/article/RssMediaBean.kt b/app/src/main/java/com/skyd/anivu/model/bean/article/RssMediaBean.kt new file mode 100644 index 00000000..6fa2585a --- /dev/null +++ b/app/src/main/java/com/skyd/anivu/model/bean/article/RssMediaBean.kt @@ -0,0 +1,47 @@ +package com.skyd.anivu.model.bean.article + +import android.os.Parcelable +import androidx.room.ColumnInfo +import androidx.room.Entity +import androidx.room.ForeignKey +import androidx.room.PrimaryKey +import com.skyd.anivu.base.BaseBean +import kotlinx.parcelize.Parcelize +import kotlinx.serialization.Serializable + +const val RSS_MEDIA_TABLE_NAME = "RssMedia" + +@Parcelize +@Serializable +@Entity( + tableName = RSS_MEDIA_TABLE_NAME, + foreignKeys = [ + ForeignKey( + entity = ArticleBean::class, + parentColumns = [ArticleBean.ARTICLE_ID_COLUMN], + childColumns = [RssMediaBean.ARTICLE_ID_COLUMN], + onDelete = ForeignKey.CASCADE + ) + ], +) +data class RssMediaBean( + @PrimaryKey + @ColumnInfo(name = ARTICLE_ID_COLUMN) + val articleId: String, + @ColumnInfo(name = DURATION_COLUMN) + val duration: Long? = null, + @ColumnInfo(name = ADULT_COLUMN) + var adult: Boolean = false, + @ColumnInfo(name = IMAGE_COLUMN) + val image: String? = null, + @ColumnInfo(name = EPISODE_COLUMN) + var episode: String? = null, +) : BaseBean, Parcelable { + companion object { + const val ARTICLE_ID_COLUMN = "articleId" + const val DURATION_COLUMN = "duration" + const val ADULT_COLUMN = "adult" + const val IMAGE_COLUMN = "image" + const val EPISODE_COLUMN = "episode" + } +} \ No newline at end of file diff --git a/app/src/main/java/com/skyd/anivu/model/db/AppDatabase.kt b/app/src/main/java/com/skyd/anivu/model/db/AppDatabase.kt index 9ec206ff..a4986285 100644 --- a/app/src/main/java/com/skyd/anivu/model/db/AppDatabase.kt +++ b/app/src/main/java/com/skyd/anivu/model/db/AppDatabase.kt @@ -5,25 +5,29 @@ import androidx.room.Database import androidx.room.Room import androidx.room.RoomDatabase import androidx.room.TypeConverters -import com.skyd.anivu.model.bean.ArticleBean -import com.skyd.anivu.model.bean.EnclosureBean import com.skyd.anivu.model.bean.FeedBean import com.skyd.anivu.model.bean.FeedViewBean import com.skyd.anivu.model.bean.GroupBean import com.skyd.anivu.model.bean.MediaPlayHistoryBean +import com.skyd.anivu.model.bean.article.ArticleBean +import com.skyd.anivu.model.bean.article.EnclosureBean +import com.skyd.anivu.model.bean.article.RssMediaBean import com.skyd.anivu.model.bean.download.DownloadInfoBean import com.skyd.anivu.model.bean.download.DownloadLinkUuidMapBean import com.skyd.anivu.model.bean.download.SessionParamsBean import com.skyd.anivu.model.bean.download.TorrentFileBean +import com.skyd.anivu.model.db.converter.CategoriesConverter import com.skyd.anivu.model.db.dao.ArticleDao import com.skyd.anivu.model.db.dao.DownloadInfoDao import com.skyd.anivu.model.db.dao.EnclosureDao import com.skyd.anivu.model.db.dao.FeedDao import com.skyd.anivu.model.db.dao.GroupDao import com.skyd.anivu.model.db.dao.MediaPlayHistoryDao +import com.skyd.anivu.model.db.dao.RssModuleDao import com.skyd.anivu.model.db.dao.SessionParamsDao import com.skyd.anivu.model.db.dao.TorrentFileDao import com.skyd.anivu.model.db.migration.Migration10To11 +import com.skyd.anivu.model.db.migration.Migration11To12 import com.skyd.anivu.model.db.migration.Migration1To2 import com.skyd.anivu.model.db.migration.Migration2To3 import com.skyd.anivu.model.db.migration.Migration3To4 @@ -47,12 +51,14 @@ const val APP_DATA_BASE_FILE_NAME = "app.db" TorrentFileBean::class, GroupBean::class, MediaPlayHistoryBean::class, + + RssMediaBean::class, ], views = [FeedViewBean::class], - version = 11, + version = 12, ) @TypeConverters( - value = [] + value = [CategoriesConverter::class] ) abstract class AppDatabase : RoomDatabase() { abstract fun groupDao(): GroupDao @@ -63,6 +69,7 @@ abstract class AppDatabase : RoomDatabase() { abstract fun torrentFileDao(): TorrentFileDao abstract fun sessionParamsDao(): SessionParamsDao abstract fun mediaPlayHistoryDao(): MediaPlayHistoryDao + abstract fun rssModuleDao(): RssModuleDao companion object { @Volatile @@ -71,7 +78,7 @@ abstract class AppDatabase : RoomDatabase() { private val migrations = arrayOf( Migration1To2(), Migration2To3(), Migration3To4(), Migration4To5(), Migration5To6(), Migration6To7(), Migration7To8(), Migration8To9(), - Migration9To10(), Migration10To11(), + Migration9To10(), Migration10To11(), Migration11To12(), ) fun getInstance(context: Context): AppDatabase { diff --git a/app/src/main/java/com/skyd/anivu/model/db/converter/CategoriesConverter.kt b/app/src/main/java/com/skyd/anivu/model/db/converter/CategoriesConverter.kt new file mode 100644 index 00000000..22a79d38 --- /dev/null +++ b/app/src/main/java/com/skyd/anivu/model/db/converter/CategoriesConverter.kt @@ -0,0 +1,20 @@ +package com.skyd.anivu.model.db.converter + +import androidx.room.TypeConverter +import com.skyd.anivu.model.bean.article.ArticleBean +import kotlinx.serialization.encodeToString +import kotlinx.serialization.json.Json + +class CategoriesConverter { + @TypeConverter + fun fromString(string: String?): ArticleBean.Categories? { + string ?: return null + return ArticleBean.Categories(categories = Json.decodeFromString(string)) + } + + @TypeConverter + fun categoriesToString(categories: ArticleBean.Categories?): String? { + val list = categories?.categories ?: return null + return Json.encodeToString(list) + } +} \ No newline at end of file diff --git a/app/src/main/java/com/skyd/anivu/model/db/dao/ArticleDao.kt b/app/src/main/java/com/skyd/anivu/model/db/dao/ArticleDao.kt index ae1f8f16..dda55724 100644 --- a/app/src/main/java/com/skyd/anivu/model/db/dao/ArticleDao.kt +++ b/app/src/main/java/com/skyd/anivu/model/db/dao/ArticleDao.kt @@ -11,13 +11,13 @@ import androidx.room.RewriteQueriesToDropUnusedColumns import androidx.room.Transaction import androidx.sqlite.db.SupportSQLiteQuery import com.skyd.anivu.appContext -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.EnclosureBean +import com.skyd.anivu.model.bean.article.EnclosureBean import com.skyd.anivu.model.bean.FEED_TABLE_NAME import com.skyd.anivu.model.bean.FeedBean +import com.skyd.anivu.model.bean.article.ARTICLE_TABLE_NAME +import com.skyd.anivu.model.bean.article.ArticleBean +import com.skyd.anivu.model.bean.article.ArticleWithEnclosureBean +import com.skyd.anivu.model.bean.article.ArticleWithFeed import dagger.hilt.EntryPoint import dagger.hilt.InstallIn import dagger.hilt.android.EntryPointAccessors @@ -30,6 +30,7 @@ interface ArticleDao { @InstallIn(SingletonComponent::class) interface ArticleDaoEntryPoint { val enclosureDao: EnclosureDao + val rssModuleDao: RssModuleDao } // null always compares false in '=' @@ -70,33 +71,42 @@ interface ArticleDao { suspend fun insertListIfNotExist(articleWithEnclosureList: List) { val hiltEntryPoint = EntryPointAccessors.fromApplication(appContext, ArticleDaoEntryPoint::class.java) - articleWithEnclosureList.forEach { + articleWithEnclosureList.forEach { articleWithEnclosure -> + val article = articleWithEnclosure.article // Duplicate article by guid or link - val guid = it.article.guid - val link = it.article.link + val guid = article.guid + val link = article.link var newArticle: ArticleBean? = null if (guid != null) { newArticle = queryArticleByGuid( guid = guid, - feedUrl = it.article.feedUrl, + feedUrl = article.feedUrl, ) } else if (link != null) { newArticle = queryArticleByLink( link = link, - feedUrl = it.article.feedUrl, + feedUrl = article.feedUrl, ) } if (newArticle == null) { - innerUpdateArticle(it.article) - newArticle = it.article + innerUpdateArticle(article) + newArticle = article } else { // Update all fields except articleId - newArticle = it.article.copy(articleId = newArticle.articleId) + newArticle = article.copy(articleId = newArticle.articleId) innerUpdateArticle(newArticle) } + // Update modules + val media = articleWithEnclosure.media + if (media != null) { + hiltEntryPoint.rssModuleDao.insertIfNotExistITunesRssBean(media) + } + hiltEntryPoint.enclosureDao.insertListIfNotExist( - it.enclosures.map { enclosure -> enclosure.copy(articleId = newArticle.articleId) } + articleWithEnclosure.enclosures.map { enclosure -> + enclosure.copy(articleId = newArticle.articleId) + } ) } } diff --git a/app/src/main/java/com/skyd/anivu/model/db/dao/EnclosureDao.kt b/app/src/main/java/com/skyd/anivu/model/db/dao/EnclosureDao.kt index e775dc80..6b7c8814 100644 --- a/app/src/main/java/com/skyd/anivu/model/db/dao/EnclosureDao.kt +++ b/app/src/main/java/com/skyd/anivu/model/db/dao/EnclosureDao.kt @@ -6,8 +6,8 @@ import androidx.room.Insert import androidx.room.OnConflictStrategy import androidx.room.Query import androidx.room.Transaction -import com.skyd.anivu.model.bean.ENCLOSURE_TABLE_NAME -import com.skyd.anivu.model.bean.EnclosureBean +import com.skyd.anivu.model.bean.article.ENCLOSURE_TABLE_NAME +import com.skyd.anivu.model.bean.article.EnclosureBean import kotlinx.coroutines.flow.Flow @Dao diff --git a/app/src/main/java/com/skyd/anivu/model/db/dao/FeedDao.kt b/app/src/main/java/com/skyd/anivu/model/db/dao/FeedDao.kt index bde6022a..c1fe147f 100644 --- a/app/src/main/java/com/skyd/anivu/model/db/dao/FeedDao.kt +++ b/app/src/main/java/com/skyd/anivu/model/db/dao/FeedDao.kt @@ -11,7 +11,6 @@ import androidx.room.Transaction import androidx.room.Update import androidx.sqlite.db.SupportSQLiteQuery import com.skyd.anivu.appContext -import com.skyd.anivu.model.bean.ArticleBean import com.skyd.anivu.model.bean.FEED_TABLE_NAME import com.skyd.anivu.model.bean.FEED_VIEW_NAME import com.skyd.anivu.model.bean.FeedBean @@ -19,6 +18,7 @@ import com.skyd.anivu.model.bean.FeedViewBean import com.skyd.anivu.model.bean.FeedWithArticleBean import com.skyd.anivu.model.bean.GROUP_TABLE_NAME import com.skyd.anivu.model.bean.GroupBean +import com.skyd.anivu.model.bean.article.ArticleBean import dagger.hilt.EntryPoint import dagger.hilt.InstallIn import dagger.hilt.android.EntryPointAccessors @@ -52,7 +52,6 @@ interface FeedDao { val feedUrl = feedWithArticleBean.feed.url hiltEntryPoint.articleDao.insertListIfNotExist( feedWithArticleBean.articles.map { articleWithEnclosure -> - // Enclosure val articleId = articleWithEnclosure.article.articleId // Add ArticleWithEnclosure diff --git a/app/src/main/java/com/skyd/anivu/model/db/dao/RssModuleDao.kt b/app/src/main/java/com/skyd/anivu/model/db/dao/RssModuleDao.kt new file mode 100644 index 00000000..3536a8ed --- /dev/null +++ b/app/src/main/java/com/skyd/anivu/model/db/dao/RssModuleDao.kt @@ -0,0 +1,25 @@ +package com.skyd.anivu.model.db.dao + +import androidx.room.Dao +import androidx.room.Insert +import androidx.room.OnConflictStrategy +import androidx.room.Query +import androidx.room.Transaction +import com.skyd.anivu.model.bean.article.RSS_MEDIA_TABLE_NAME +import com.skyd.anivu.model.bean.article.RssMediaBean + +@Dao +interface RssModuleDao { + @Transaction + @Insert(onConflict = OnConflictStrategy.IGNORE) + fun insertIfNotExistITunesRssBean(rssMediaBean: RssMediaBean) + + @Transaction + @Query( + """ + SELECT * FROM $RSS_MEDIA_TABLE_NAME + WHERE ${RssMediaBean.ARTICLE_ID_COLUMN} = :articleId + """ + ) + fun getRssMediaBean(articleId: String): RssMediaBean +} \ No newline at end of file diff --git a/app/src/main/java/com/skyd/anivu/model/db/migration/Migration11To12.kt b/app/src/main/java/com/skyd/anivu/model/db/migration/Migration11To12.kt new file mode 100644 index 00000000..ba65c453 --- /dev/null +++ b/app/src/main/java/com/skyd/anivu/model/db/migration/Migration11To12.kt @@ -0,0 +1,26 @@ +package com.skyd.anivu.model.db.migration + +import androidx.room.migration.Migration +import androidx.sqlite.db.SupportSQLiteDatabase +import com.skyd.anivu.model.bean.article.ARTICLE_TABLE_NAME +import com.skyd.anivu.model.bean.article.ArticleBean +import com.skyd.anivu.model.bean.article.RSS_MEDIA_TABLE_NAME +import com.skyd.anivu.model.bean.article.RssMediaBean + +class Migration11To12 : Migration(11, 12) { + override fun migrate(db: SupportSQLiteDatabase) { + db.execSQL("ALTER TABLE $ARTICLE_TABLE_NAME ADD ${ArticleBean.CATEGORIES_COLUMN} TEXT") + db.execSQL( + "CREATE TABLE `$RSS_MEDIA_TABLE_NAME` (" + + "${RssMediaBean.ARTICLE_ID_COLUMN} TEXT NOT NULL PRIMARY KEY, " + + "${RssMediaBean.DURATION_COLUMN} INTEGER, " + + "${RssMediaBean.ADULT_COLUMN} INTEGER NOT NULL DEFAULT 0, " + + "${RssMediaBean.IMAGE_COLUMN} TEXT, " + + "${RssMediaBean.EPISODE_COLUMN} TEXT, " + + "FOREIGN KEY (${RssMediaBean.ARTICLE_ID_COLUMN})" + + "REFERENCES $ARTICLE_TABLE_NAME(${ArticleBean.ARTICLE_ID_COLUMN})" + + "ON DELETE CASCADE" + + ")" + ) + } +} \ No newline at end of file diff --git a/app/src/main/java/com/skyd/anivu/model/db/migration/Migration4To5.kt b/app/src/main/java/com/skyd/anivu/model/db/migration/Migration4To5.kt index 7a813570..432cb7de 100644 --- a/app/src/main/java/com/skyd/anivu/model/db/migration/Migration4To5.kt +++ b/app/src/main/java/com/skyd/anivu/model/db/migration/Migration4To5.kt @@ -2,8 +2,8 @@ package com.skyd.anivu.model.db.migration import androidx.room.migration.Migration import androidx.sqlite.db.SupportSQLiteDatabase -import com.skyd.anivu.model.bean.ARTICLE_TABLE_NAME -import com.skyd.anivu.model.bean.ArticleBean +import com.skyd.anivu.model.bean.article.ARTICLE_TABLE_NAME +import com.skyd.anivu.model.bean.article.ArticleBean import com.skyd.anivu.model.bean.FEED_TABLE_NAME import com.skyd.anivu.model.bean.FEED_VIEW_NAME import com.skyd.anivu.model.bean.FeedBean diff --git a/app/src/main/java/com/skyd/anivu/model/db/migration/Migration5To6.kt b/app/src/main/java/com/skyd/anivu/model/db/migration/Migration5To6.kt index a053c94e..e181fdb6 100644 --- a/app/src/main/java/com/skyd/anivu/model/db/migration/Migration5To6.kt +++ b/app/src/main/java/com/skyd/anivu/model/db/migration/Migration5To6.kt @@ -2,8 +2,8 @@ package com.skyd.anivu.model.db.migration import androidx.room.migration.Migration import androidx.sqlite.db.SupportSQLiteDatabase -import com.skyd.anivu.model.bean.ARTICLE_TABLE_NAME -import com.skyd.anivu.model.bean.ArticleBean +import com.skyd.anivu.model.bean.article.ARTICLE_TABLE_NAME +import com.skyd.anivu.model.bean.article.ArticleBean import com.skyd.anivu.model.bean.FEED_TABLE_NAME import com.skyd.anivu.model.bean.FeedBean diff --git a/app/src/main/java/com/skyd/anivu/model/db/migration/Migration6To7.kt b/app/src/main/java/com/skyd/anivu/model/db/migration/Migration6To7.kt index 54f4a588..86117fcd 100644 --- a/app/src/main/java/com/skyd/anivu/model/db/migration/Migration6To7.kt +++ b/app/src/main/java/com/skyd/anivu/model/db/migration/Migration6To7.kt @@ -2,8 +2,8 @@ package com.skyd.anivu.model.db.migration import androidx.room.migration.Migration import androidx.sqlite.db.SupportSQLiteDatabase -import com.skyd.anivu.model.bean.ARTICLE_TABLE_NAME -import com.skyd.anivu.model.bean.ArticleBean +import com.skyd.anivu.model.bean.article.ARTICLE_TABLE_NAME +import com.skyd.anivu.model.bean.article.ArticleBean class Migration6To7 : Migration(6, 7) { override fun migrate(db: SupportSQLiteDatabase) { diff --git a/app/src/main/java/com/skyd/anivu/model/repository/ArticleRepository.kt b/app/src/main/java/com/skyd/anivu/model/repository/ArticleRepository.kt index dee21f1e..ea411246 100644 --- a/app/src/main/java/com/skyd/anivu/model/repository/ArticleRepository.kt +++ b/app/src/main/java/com/skyd/anivu/model/repository/ArticleRepository.kt @@ -7,10 +7,10 @@ import androidx.paging.PagingConfig import androidx.paging.PagingData import androidx.sqlite.db.SimpleSQLiteQuery import com.skyd.anivu.base.BaseRepository -import com.skyd.anivu.model.bean.ARTICLE_TABLE_NAME -import com.skyd.anivu.model.bean.ArticleBean -import com.skyd.anivu.model.bean.ArticleWithFeed import com.skyd.anivu.model.bean.GroupVo +import com.skyd.anivu.model.bean.article.ARTICLE_TABLE_NAME +import com.skyd.anivu.model.bean.article.ArticleBean +import com.skyd.anivu.model.bean.article.ArticleWithFeed import com.skyd.anivu.model.db.dao.ArticleDao import com.skyd.anivu.model.db.dao.FeedDao import kotlinx.coroutines.Deferred diff --git a/app/src/main/java/com/skyd/anivu/model/repository/ReadRepository.kt b/app/src/main/java/com/skyd/anivu/model/repository/ReadRepository.kt index db5e1787..1c508aa2 100644 --- a/app/src/main/java/com/skyd/anivu/model/repository/ReadRepository.kt +++ b/app/src/main/java/com/skyd/anivu/model/repository/ReadRepository.kt @@ -14,7 +14,7 @@ import com.skyd.anivu.ext.savePictureToMediaStore import com.skyd.anivu.ext.share import com.skyd.anivu.ext.toUri import com.skyd.anivu.ext.validateFileName -import com.skyd.anivu.model.bean.ArticleWithEnclosureBean +import com.skyd.anivu.model.bean.article.ArticleWithEnclosureBean import com.skyd.anivu.model.db.dao.ArticleDao import com.skyd.anivu.util.image.ImageFormatChecker import kotlinx.coroutines.Dispatchers diff --git a/app/src/main/java/com/skyd/anivu/model/repository/RssHelper.kt b/app/src/main/java/com/skyd/anivu/model/repository/RssHelper.kt index d3a15d5e..9b5f0609 100644 --- a/app/src/main/java/com/skyd/anivu/model/repository/RssHelper.kt +++ b/app/src/main/java/com/skyd/anivu/model/repository/RssHelper.kt @@ -1,15 +1,20 @@ package com.skyd.anivu.model.repository import android.util.Log +import com.rometools.modules.itunes.EntryInformationImpl +import com.rometools.modules.mediarss.MediaEntryModuleImpl +import com.rometools.modules.mediarss.types.Rating +import com.rometools.rome.feed.module.Module import com.rometools.rome.feed.synd.SyndEntry import com.rometools.rome.io.SyndFeedInput import com.rometools.rome.io.XmlReader import com.skyd.anivu.ext.toEncodedUrl -import com.skyd.anivu.model.bean.ArticleBean -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.bean.article.ArticleBean +import com.skyd.anivu.model.bean.article.ArticleWithEnclosureBean +import com.skyd.anivu.model.bean.article.EnclosureBean +import com.skyd.anivu.model.bean.article.RssMediaBean import com.skyd.anivu.util.favicon.FaviconExtractor import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.async @@ -104,6 +109,7 @@ class RssHelper @Inject constructor( "content: ${content}\n" ) val articleId = UUID.randomUUID().toString() + val rssMedia = getRssMedia(articleId = articleId, modules = syndEntry.modules) return ArticleWithEnclosureBean( article = ArticleBean( articleId = articleId, @@ -117,6 +123,9 @@ class RssHelper @Inject constructor( link = syndEntry.link, guid = syndEntry.uri, updateAt = Date().time, + catrgories = ArticleBean.Categories( + categories = syndEntry.categories.map { it.name }.filter { it.isNotBlank() } + ) ), enclosures = syndEntry.enclosures.map { EnclosureBean( @@ -125,10 +134,42 @@ class RssHelper @Inject constructor( length = it.length, type = it.type, ) - } + }, + media = rssMedia, ) } + private fun getRssMedia(articleId: String, modules: List): RssMediaBean? { + modules.forEach { module -> + val media = when (module) { + is EntryInformationImpl -> { + RssMediaBean( + articleId = articleId, + duration = module.duration?.milliseconds, + adult = module.explicit, + image = module.image?.toString(), + episode = module.episode?.toString(), + ) + } + + is MediaEntryModuleImpl -> { + val content = module.mediaContents.firstOrNull() + RssMediaBean( + articleId = articleId, + duration = content?.duration, + adult = content?.metadata?.ratings?.any { it == Rating.ADULT } ?: false, + image = content?.metadata?.thumbnail?.firstOrNull()?.url?.toString(), + episode = null, + ) + } + + else -> null + } + if (media != null) return media + } + return null + } + fun getRssIcon(url: String): String? { return runCatching { faviconExtractor.extractFavicon(url).apply { Log.e("TAG", "getRssIcon: $this") } diff --git a/app/src/main/java/com/skyd/anivu/model/repository/SearchRepository.kt b/app/src/main/java/com/skyd/anivu/model/repository/SearchRepository.kt index 9fc28e2b..8e8eb4b6 100644 --- a/app/src/main/java/com/skyd/anivu/model/repository/SearchRepository.kt +++ b/app/src/main/java/com/skyd/anivu/model/repository/SearchRepository.kt @@ -10,9 +10,9 @@ import com.skyd.anivu.base.BaseRepository import com.skyd.anivu.config.allSearchDomain 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.ArticleWithFeed +import com.skyd.anivu.model.bean.article.ARTICLE_TABLE_NAME +import com.skyd.anivu.model.bean.article.ArticleBean +import com.skyd.anivu.model.bean.article.ArticleWithFeed import com.skyd.anivu.model.bean.FEED_VIEW_NAME import com.skyd.anivu.model.bean.FeedViewBean import com.skyd.anivu.model.db.dao.ArticleDao diff --git a/app/src/main/java/com/skyd/anivu/ui/component/AniVuImage.kt b/app/src/main/java/com/skyd/anivu/ui/component/AniVuImage.kt index cde1d8c2..bc66eba8 100644 --- a/app/src/main/java/com/skyd/anivu/ui/component/AniVuImage.kt +++ b/app/src/main/java/com/skyd/anivu/ui/component/AniVuImage.kt @@ -3,14 +3,18 @@ package com.skyd.anivu.ui.component import androidx.compose.runtime.Composable import androidx.compose.runtime.remember import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.ColorFilter import androidx.compose.ui.graphics.DefaultAlpha import androidx.compose.ui.layout.ContentScale import androidx.compose.ui.platform.LocalContext import androidx.lifecycle.compose.LocalLifecycleOwner +import coil.ComponentRegistry import coil.EventListener import coil.ImageLoader import coil.compose.AsyncImage +import coil.request.CachePolicy import coil.request.ImageRequest +import coil.util.DebugLogger import com.skyd.anivu.ext.imageLoaderBuilder @@ -22,6 +26,7 @@ fun AniVuImage( imageLoader: ImageLoader = rememberAniVuImageLoader(), contentScale: ContentScale = ContentScale.FillWidth, alpha: Float = DefaultAlpha, + colorFilter: ColorFilter? = null, ) { AsyncImage( model = if (model is ImageRequest) { @@ -32,6 +37,8 @@ fun AniVuImage( remember(model) { ImageRequest.Builder(context) .lifecycle(lifecycleOwner) + .diskCachePolicy(CachePolicy.ENABLED) + .memoryCachePolicy(CachePolicy.ENABLED) .data(model) .crossfade(true) .build() @@ -42,15 +49,21 @@ fun AniVuImage( contentScale = contentScale, imageLoader = imageLoader, alpha = alpha, + colorFilter = colorFilter, ) } @Composable -fun rememberAniVuImageLoader(listener: EventListener? = null): ImageLoader { +fun rememberAniVuImageLoader( + listener: EventListener? = null, + components: ComponentRegistry.Builder.() -> Unit = {}, +): ImageLoader { val context = LocalContext.current return remember(context) { context.imageLoaderBuilder() + .components(components) .run { if (listener != null) eventListener(listener) else this } + .logger(DebugLogger()) .build() } } \ No newline at end of file diff --git a/app/src/main/java/com/skyd/anivu/ui/component/lazyverticalgrid/adapter/proxy/Article1Proxy.kt b/app/src/main/java/com/skyd/anivu/ui/component/lazyverticalgrid/adapter/proxy/Article1Proxy.kt index 9bde80ff..f7722b3f 100644 --- a/app/src/main/java/com/skyd/anivu/ui/component/lazyverticalgrid/adapter/proxy/Article1Proxy.kt +++ b/app/src/main/java/com/skyd/anivu/ui/component/lazyverticalgrid/adapter/proxy/Article1Proxy.kt @@ -68,9 +68,9 @@ import com.skyd.anivu.ext.dataStore import com.skyd.anivu.ext.getOrDefault import com.skyd.anivu.ext.readable import com.skyd.anivu.ext.toDateTimeString -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.article.ArticleBean +import com.skyd.anivu.model.bean.article.ArticleWithEnclosureBean +import com.skyd.anivu.model.bean.article.ArticleWithFeed import com.skyd.anivu.model.bean.FeedBean import com.skyd.anivu.model.preference.behavior.article.ArticleSwipeActionPreference import com.skyd.anivu.model.preference.behavior.article.ArticleSwipeLeftActionPreference diff --git a/app/src/main/java/com/skyd/anivu/ui/mpv/controller/button/Screenshot.kt b/app/src/main/java/com/skyd/anivu/ui/mpv/controller/button/Screenshot.kt index 065079c2..21be7276 100644 --- a/app/src/main/java/com/skyd/anivu/ui/mpv/controller/button/Screenshot.kt +++ b/app/src/main/java/com/skyd/anivu/ui/mpv/controller/button/Screenshot.kt @@ -4,9 +4,9 @@ import androidx.compose.foundation.background import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.WindowInsets import androidx.compose.foundation.layout.WindowInsetsSides +import androidx.compose.foundation.layout.displayCutout import androidx.compose.foundation.layout.only import androidx.compose.foundation.layout.padding -import androidx.compose.foundation.layout.safeDrawing import androidx.compose.foundation.layout.windowInsetsPadding import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.material.icons.Icons @@ -29,7 +29,7 @@ internal fun Screenshot( ) { Icon( modifier = modifier - .windowInsetsPadding(WindowInsets.safeDrawing.only(WindowInsetsSides.End)) + .windowInsetsPadding(WindowInsets.displayCutout.only(WindowInsetsSides.End)) .clip(RoundedCornerShape(6.dp)) .background(color = ControllerLabelGray) .clickable(onClick = onClick) diff --git a/app/src/main/java/com/skyd/anivu/ui/screen/article/ArticlePartialStateChange.kt b/app/src/main/java/com/skyd/anivu/ui/screen/article/ArticlePartialStateChange.kt index 2696cfd3..f4302a9f 100644 --- a/app/src/main/java/com/skyd/anivu/ui/screen/article/ArticlePartialStateChange.kt +++ b/app/src/main/java/com/skyd/anivu/ui/screen/article/ArticlePartialStateChange.kt @@ -1,7 +1,7 @@ package com.skyd.anivu.ui.screen.article import androidx.paging.PagingData -import com.skyd.anivu.model.bean.ArticleWithFeed +import com.skyd.anivu.model.bean.article.ArticleWithFeed import com.skyd.anivu.model.repository.ArticleSort import kotlinx.coroutines.flow.Flow diff --git a/app/src/main/java/com/skyd/anivu/ui/screen/article/ArticleScreen.kt b/app/src/main/java/com/skyd/anivu/ui/screen/article/ArticleScreen.kt index 573ebd79..9c6384cc 100644 --- a/app/src/main/java/com/skyd/anivu/ui/screen/article/ArticleScreen.kt +++ b/app/src/main/java/com/skyd/anivu/ui/screen/article/ArticleScreen.kt @@ -68,7 +68,7 @@ import com.skyd.anivu.base.mvi.getDispatcher import com.skyd.anivu.ext.navigate import com.skyd.anivu.ext.plus import com.skyd.anivu.ext.popBackStackWithLifecycle -import com.skyd.anivu.model.bean.ArticleWithFeed +import com.skyd.anivu.model.bean.article.ArticleWithFeed import com.skyd.anivu.model.repository.ArticleSort import com.skyd.anivu.ui.component.AniVuFloatingActionButton import com.skyd.anivu.ui.component.AniVuIconButton diff --git a/app/src/main/java/com/skyd/anivu/ui/screen/article/ArticleState.kt b/app/src/main/java/com/skyd/anivu/ui/screen/article/ArticleState.kt index 46ebd3be..db2db6f3 100644 --- a/app/src/main/java/com/skyd/anivu/ui/screen/article/ArticleState.kt +++ b/app/src/main/java/com/skyd/anivu/ui/screen/article/ArticleState.kt @@ -2,7 +2,7 @@ package com.skyd.anivu.ui.screen.article import androidx.paging.PagingData import com.skyd.anivu.base.mvi.MviViewState -import com.skyd.anivu.model.bean.ArticleWithFeed +import com.skyd.anivu.model.bean.article.ArticleWithFeed import com.skyd.anivu.model.repository.ArticleSort import kotlinx.coroutines.flow.Flow diff --git a/app/src/main/java/com/skyd/anivu/ui/screen/article/enclosure/EnclosureBottomSheet.kt b/app/src/main/java/com/skyd/anivu/ui/screen/article/enclosure/EnclosureBottomSheet.kt index 911d1517..f9dd8ec3 100644 --- a/app/src/main/java/com/skyd/anivu/ui/screen/article/enclosure/EnclosureBottomSheet.kt +++ b/app/src/main/java/com/skyd/anivu/ui/screen/article/enclosure/EnclosureBottomSheet.kt @@ -6,6 +6,7 @@ import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.width @@ -34,9 +35,9 @@ import com.skyd.anivu.ext.copy import com.skyd.anivu.ext.dataStore import com.skyd.anivu.ext.fileSize import com.skyd.anivu.ext.getOrDefault -import com.skyd.anivu.model.bean.ArticleWithEnclosureBean -import com.skyd.anivu.model.bean.EnclosureBean import com.skyd.anivu.model.bean.LinkEnclosureBean +import com.skyd.anivu.model.bean.article.ArticleWithEnclosureBean +import com.skyd.anivu.model.bean.article.EnclosureBean import com.skyd.anivu.model.preference.rss.ParseLinkTagAsEnclosurePreference import com.skyd.anivu.model.worker.download.DownloadTorrentWorker import com.skyd.anivu.model.worker.download.doIfMagnetOrTorrentLink @@ -87,7 +88,10 @@ fun EnclosureBottomSheet( onDismissRequest = onDismissRequest, sheetState = sheetState ) { - Column(horizontalAlignment = Alignment.CenterHorizontally) { + Column( + modifier = Modifier.fillMaxWidth(), + horizontalAlignment = Alignment.CenterHorizontally, + ) { Text( text = stringResource(id = R.string.bottom_sheet_enclosure_title), style = MaterialTheme.typography.titleLarge, diff --git a/app/src/main/java/com/skyd/anivu/ui/screen/read/ReadPartialStateChange.kt b/app/src/main/java/com/skyd/anivu/ui/screen/read/ReadPartialStateChange.kt index 74a76783..cecc09b9 100644 --- a/app/src/main/java/com/skyd/anivu/ui/screen/read/ReadPartialStateChange.kt +++ b/app/src/main/java/com/skyd/anivu/ui/screen/read/ReadPartialStateChange.kt @@ -1,6 +1,6 @@ package com.skyd.anivu.ui.screen.read -import com.skyd.anivu.model.bean.ArticleWithEnclosureBean +import com.skyd.anivu.model.bean.article.ArticleWithEnclosureBean internal sealed interface ReadPartialStateChange { diff --git a/app/src/main/java/com/skyd/anivu/ui/screen/read/ReadScreen.kt b/app/src/main/java/com/skyd/anivu/ui/screen/read/ReadScreen.kt index 0f9cd205..ddf0a102 100644 --- a/app/src/main/java/com/skyd/anivu/ui/screen/read/ReadScreen.kt +++ b/app/src/main/java/com/skyd/anivu/ui/screen/read/ReadScreen.kt @@ -1,14 +1,27 @@ package com.skyd.anivu.ui.screen.read +import android.net.Uri +import android.text.format.DateUtils import androidx.compose.animation.animateContentSize +import androidx.compose.foundation.background import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.BoxScope import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.FlowRow +import androidx.compose.foundation.layout.PaddingValues import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.fillMaxHeight import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size import androidx.compose.foundation.layout.width +import androidx.compose.foundation.layout.widthIn +import androidx.compose.foundation.lazy.LazyRow +import androidx.compose.foundation.lazy.items import androidx.compose.foundation.rememberScrollState import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.foundation.text.selection.SelectionContainer @@ -17,6 +30,7 @@ import androidx.compose.material.icons.Icons import androidx.compose.material.icons.outlined.AttachFile import androidx.compose.material.icons.outlined.ContentCopy import androidx.compose.material.icons.outlined.Download +import androidx.compose.material.icons.outlined.PlayCircleOutline import androidx.compose.material.icons.outlined.Public import androidx.compose.material.icons.outlined.Share import androidx.compose.material3.Icon @@ -25,6 +39,7 @@ import androidx.compose.material3.ModalBottomSheet import androidx.compose.material3.Scaffold import androidx.compose.material3.SnackbarHost import androidx.compose.material3.SnackbarHostState +import androidx.compose.material3.SuggestionChip import androidx.compose.material3.Text import androidx.compose.material3.TopAppBarDefaults import androidx.compose.runtime.Composable @@ -36,8 +51,12 @@ import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip +import androidx.compose.ui.graphics.BlendMode +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.ColorFilter import androidx.compose.ui.graphics.vector.ImageVector import androidx.compose.ui.input.nestedscroll.nestedScroll +import androidx.compose.ui.layout.ContentScale import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.style.TextOverflow @@ -46,20 +65,33 @@ import androidx.hilt.navigation.compose.hiltViewModel import androidx.lifecycle.compose.collectAsStateWithLifecycle import androidx.navigation.NavController import androidx.navigation.NavOptions +import coil.EventListener +import coil.decode.VideoFrameDecoder +import coil.request.ErrorResult +import coil.request.ImageRequest import com.skyd.anivu.R import com.skyd.anivu.base.mvi.MviEventListener import com.skyd.anivu.base.mvi.getDispatcher +import com.skyd.anivu.ext.activity +import com.skyd.anivu.ext.copy import com.skyd.anivu.ext.ifNullOfBlank +import com.skyd.anivu.ext.isWifi import com.skyd.anivu.ext.openBrowser -import com.skyd.anivu.ext.plus import com.skyd.anivu.ext.toDateTimeString import com.skyd.anivu.ext.toEncodedUrl +import com.skyd.anivu.model.bean.article.ArticleBean +import com.skyd.anivu.model.bean.article.ArticleWithEnclosureBean +import com.skyd.anivu.model.bean.article.EnclosureBean +import com.skyd.anivu.model.bean.article.RssMediaBean +import com.skyd.anivu.ui.activity.PlayActivity import com.skyd.anivu.ui.component.AniVuFloatingActionButton import com.skyd.anivu.ui.component.AniVuIconButton +import com.skyd.anivu.ui.component.AniVuImage import com.skyd.anivu.ui.component.AniVuTopBar import com.skyd.anivu.ui.component.AniVuTopBarStyle import com.skyd.anivu.ui.component.dialog.WaitingDialog import com.skyd.anivu.ui.component.html.HtmlText +import com.skyd.anivu.ui.component.rememberAniVuImageLoader import com.skyd.anivu.ui.screen.article.enclosure.EnclosureBottomSheet import com.skyd.anivu.ui.screen.article.enclosure.getEnclosuresList import com.skyd.anivu.util.ShareUtil @@ -157,7 +189,7 @@ fun ReadScreen(articleId: String, viewModel: ReadViewModel = hiltViewModel()) { .fillMaxHeight() .nestedScroll(scrollBehavior.nestedScrollConnection) .verticalScroll(rememberScrollState()) - .padding(paddingValues + 16.dp) + .padding(paddingValues) .padding(bottom = fabHeight), ) { when (val articleState = uiState.articleState) { @@ -214,6 +246,22 @@ fun ReadScreen(articleId: String, viewModel: ReadViewModel = hiltViewModel()) { } } +@Composable +private fun CategoryArea(categories: ArticleBean.Categories) { + val context = LocalContext.current + FlowRow( + modifier = Modifier.padding(vertical = 16.dp, horizontal = 16.dp), + horizontalArrangement = Arrangement.spacedBy(10.dp), + ) { + categories.categories.forEach { category -> + SuggestionChip( + onClick = { category.copy(context) }, + label = { Text(text = category) }, + ) + } + } +} + @Composable private fun Content( articleState: ArticleState.Success, @@ -226,7 +274,7 @@ private fun Content( var openImageSheet by rememberSaveable { mutableStateOf(null) } SelectionContainer { - Column { + Column(modifier = Modifier.padding(horizontal = 16.dp)) { var expandTitle by rememberSaveable { mutableStateOf(false) } article.article.title?.let { title -> Text( @@ -269,12 +317,19 @@ private fun Content( } } } + MediaRow(articleWithEnclosureBean = article, onPlay = { url -> + PlayActivity.play(context.activity, Uri.parse(url)) + }) HtmlText( + modifier = Modifier.padding(horizontal = 16.dp), text = article.article.content.ifNullOfBlank { article.article.description.orEmpty() }, onImageClick = { imageUrl -> openImageSheet = imageUrl } ) + article.article.catrgories?.let { catrgories -> + CategoryArea(catrgories) + } if (openImageSheet != null) { ImageBottomSheet( @@ -287,6 +342,150 @@ private fun Content( } } +@Composable +private fun RssMediaEpisode(modifier: Modifier = Modifier, rssMedia: RssMediaBean) { + val episode = rssMedia.episode + if (episode != null) { + Text( + modifier = modifier, + text = stringResource(id = R.string.read_screen_episode, episode), + color = Color.White, + ) + } +} + +@Composable +private fun RssMediaDuration(modifier: Modifier = Modifier, rssMedia: RssMediaBean) { + val duration = rssMedia.duration + if (duration != null) { + Text( + modifier = modifier, + text = DateUtils.formatElapsedTime(duration / 1000), + color = Color.White, + ) + } +} + +@Composable +private fun MediaRow(articleWithEnclosureBean: ArticleWithEnclosureBean, onPlay: (String) -> Unit) { + val enclosures = articleWithEnclosureBean.enclosures.filter { it.isMedia } + val cover = articleWithEnclosureBean.media?.image + if (enclosures.size > 1) { + Spacer(modifier = Modifier.height(6.dp)) + LazyRow( + modifier = Modifier.fillMaxWidth(), + horizontalArrangement = Arrangement.spacedBy(12.dp), + contentPadding = PaddingValues(horizontal = 16.dp), + ) { + items(enclosures) { item -> + MediaCover( + modifier = Modifier + .height(160.dp) + .widthIn(min = 200.dp), + cover = cover, + enclosure = item, + onClick = { onPlay(item.url) }, + ) + } + } + articleWithEnclosureBean.media?.let { media -> + Spacer(modifier = Modifier.height(12.dp)) + Row { + RssMediaEpisode(rssMedia = media) + Spacer(modifier = Modifier.width(12.dp)) + RssMediaDuration(rssMedia = media) + } + } + Spacer(modifier = Modifier.height(16.dp)) + } else if (enclosures.size == 1) { + val item = enclosures.first() + Box( + modifier = Modifier + .fillMaxWidth() + .padding(top = 6.dp, bottom = 16.dp) + .padding(horizontal = 16.dp) + ) { + MediaCover( + modifier = Modifier + .height(200.dp) + .widthIn(max = 350.dp) + .align(Alignment.Center), + cover = cover, + enclosure = item, + onClick = { onPlay(item.url) }, + ) { + articleWithEnclosureBean.media?.let { media -> + RssMediaEpisode( + modifier = Modifier + .align(Alignment.TopEnd) + .padding(end = 15.dp, top = 10.dp), + rssMedia = media, + ) + RssMediaDuration( + modifier = Modifier + .align(Alignment.BottomEnd) + .padding(end = 15.dp, bottom = 10.dp), + rssMedia = media, + ) + } + } + } + } +} + +@Composable +private fun MediaCover( + modifier: Modifier = Modifier, + cover: String?, + enclosure: EnclosureBean, + onClick: () -> Unit, + content: @Composable (BoxScope.() -> Unit) = {}, +) { + val context = LocalContext.current + Box( + modifier = modifier + .clip(RoundedCornerShape(12.dp)) + .background(Color.Black.copy(alpha = 0.5f)), + ) { + Box( + modifier = Modifier + .clickable(onClick = onClick) + .align(Alignment.Center), + contentAlignment = Alignment.Center, + ) { + var realImage by rememberSaveable(enclosure) { + mutableStateOf(if (context.isWifi() && enclosure.isVideo) enclosure.url else cover) + } + AniVuImage( + modifier = Modifier.fillMaxHeight(), + imageLoader = rememberAniVuImageLoader( + listener = object : EventListener { + override fun onError(request: ImageRequest, result: ErrorResult) { + if (cover != null && realImage != cover) { + realImage = cover + } + } + }, + components = { add(VideoFrameDecoder.Factory()) }, + ), + model = realImage, + contentScale = ContentScale.FillHeight, + colorFilter = ColorFilter.tint( + Color.Black.copy(alpha = 0.5f), + blendMode = BlendMode.Darken, + ), + ) + Icon( + modifier = Modifier.size(50.dp), + imageVector = Icons.Outlined.PlayCircleOutline, + contentDescription = stringResource(id = R.string.play), + tint = Color.White, + ) + content() + } + } +} + @Composable private fun ImageBottomSheet( imageUrl: String, diff --git a/app/src/main/java/com/skyd/anivu/ui/screen/read/ReadState.kt b/app/src/main/java/com/skyd/anivu/ui/screen/read/ReadState.kt index 62a3673b..93dfa857 100644 --- a/app/src/main/java/com/skyd/anivu/ui/screen/read/ReadState.kt +++ b/app/src/main/java/com/skyd/anivu/ui/screen/read/ReadState.kt @@ -1,7 +1,7 @@ package com.skyd.anivu.ui.screen.read import com.skyd.anivu.base.mvi.MviViewState -import com.skyd.anivu.model.bean.ArticleWithEnclosureBean +import com.skyd.anivu.model.bean.article.ArticleWithEnclosureBean data class ReadState( val articleState: ArticleState, diff --git a/app/src/main/java/com/skyd/anivu/ui/screen/search/SearchScreen.kt b/app/src/main/java/com/skyd/anivu/ui/screen/search/SearchScreen.kt index 776ff242..af05819d 100644 --- a/app/src/main/java/com/skyd/anivu/ui/screen/search/SearchScreen.kt +++ b/app/src/main/java/com/skyd/anivu/ui/screen/search/SearchScreen.kt @@ -68,7 +68,7 @@ import com.skyd.anivu.base.mvi.MviEventListener import com.skyd.anivu.base.mvi.getDispatcher import com.skyd.anivu.ext.navigate import com.skyd.anivu.ext.plus -import com.skyd.anivu.model.bean.ArticleWithFeed +import com.skyd.anivu.model.bean.article.ArticleWithFeed import com.skyd.anivu.model.bean.FeedViewBean import com.skyd.anivu.ui.component.AniVuFloatingActionButton import com.skyd.anivu.ui.component.AniVuIconButton diff --git a/app/src/main/res/values-zh-rCN/strings.xml b/app/src/main/res/values-zh-rCN/strings.xml index 6e928f91..1756e982 100644 --- a/app/src/main/res/values-zh-rCN/strings.xml +++ b/app/src/main/res/values-zh-rCN/strings.xml @@ -328,6 +328,7 @@ 媒体页面 媒体列表 显示预览图 + 第 %s 集 已读 %d 项 diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index aa86e561..ea6a7289 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -335,6 +335,7 @@ Media screen Media list Show thumbnails + Episode %s Read %d item Read %d items