diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 76259c27..006c8dd8 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -20,8 +20,8 @@ android { applicationId = "com.skyd.anivu" minSdk = 24 targetSdk = 34 - versionCode = 11 - versionName = "1.1-beta08" + versionCode = 12 + versionName = "1.1-beta09" testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" @@ -144,9 +144,11 @@ dependencies { implementation("androidx.constraintlayout:constraintlayout:2.1.4") implementation("androidx.navigation:navigation-fragment-ktx:2.7.7") implementation("androidx.navigation:navigation-ui-ktx:2.7.7") + implementation("androidx.lifecycle:lifecycle-runtime-compose:2.7.0") implementation("androidx.compose.ui:ui:1.6.5") implementation("androidx.compose.material3:material3:1.2.1") implementation("androidx.compose.material3:material3-window-size-class:1.2.1") + implementation("androidx.compose.material:material-icons-extended:1.6.5") implementation("com.materialkolor:material-kolor:1.4.4") implementation("androidx.room:room-runtime:2.6.1") implementation("androidx.room:room-ktx:2.6.1") @@ -167,6 +169,7 @@ dependencies { implementation("com.google.dagger:hilt-android:2.51") ksp("com.google.dagger:hilt-android-compiler:2.51") + implementation("androidx.hilt:hilt-navigation-compose:1.2.0") implementation("com.squareup.okhttp3:okhttp:4.12.0") implementation("com.squareup.okhttp3:okhttp-coroutines-jvm:5.0.0-alpha.12") @@ -176,6 +179,7 @@ dependencies { implementation("org.jetbrains.kotlinx:kotlinx-serialization-json:1.6.3") implementation("io.coil-kt:coil:2.6.0") + implementation("io.coil-kt:coil-compose:2.6.0") implementation("com.rometools:rome:2.1.0") implementation("net.dankito.readability4j:readability4j:1.0.8") diff --git a/app/proguard-rules.pro b/app/proguard-rules.pro index fd32b171..5b5ff472 100644 --- a/app/proguard-rules.pro +++ b/app/proguard-rules.pro @@ -103,5 +103,8 @@ public static final ** CREATOR; -keep class com.skyd.anivu.ui.adapter.variety.VarietyAdapter$Proxy { *; } -keep class com.skyd.anivu.ui.adapter.variety.AsyncListDiffer { *; } +-keep class * extends com.skyd.anivu.ui.component.lazyverticalgrid.adapter.LazyGridAdapter$Proxy +-keep class com.skyd.anivu.ui.component.lazyverticalgrid.adapter.LazyGridAdapter$Proxy { *; } + # Retrofit -keep, allowobfuscation, allowshrinking interface retrofit2.Call \ No newline at end of file diff --git a/app/src/main/java/com/skyd/anivu/base/BaseComposeFragment.kt b/app/src/main/java/com/skyd/anivu/base/BaseComposeFragment.kt index d4557168..fe781677 100644 --- a/app/src/main/java/com/skyd/anivu/base/BaseComposeFragment.kt +++ b/app/src/main/java/com/skyd/anivu/base/BaseComposeFragment.kt @@ -1,9 +1,11 @@ package com.skyd.anivu.base +import android.os.Bundle import androidx.compose.material3.windowsizeclass.calculateWindowSizeClass import androidx.compose.runtime.Composable import androidx.compose.runtime.CompositionLocalProvider import androidx.compose.ui.platform.ComposeView +import androidx.compose.ui.platform.ViewCompositionStrategy import androidx.fragment.app.Fragment import androidx.navigation.NavHostController import com.skyd.anivu.ext.findMainNavController @@ -19,6 +21,8 @@ abstract class BaseComposeFragment : Fragment() { fun setContentBase(content: @Composable () -> Unit): ComposeView { navController = findMainNavController() as NavHostController return ComposeView(requireContext()).apply { + // Dispose of the Composition when the view's LifecycleOwner is destroyed + setViewCompositionStrategy(ViewCompositionStrategy.DisposeOnViewTreeLifecycleDestroyed) setContent { CompositionLocalProvider( LocalNavController provides navController, @@ -29,4 +33,21 @@ abstract class BaseComposeFragment : Fragment() { } } } + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + transitionProvider?.let { provider -> + enterTransition = provider.enterTransition + returnTransition = provider.returnTransition + exitTransition = provider.exitTransition + reenterTransition = provider.reenterTransition + } + } + + private val defaultTransitionProvider = BaseFragment.TransitionProvider() + + protected val nullTransitionProvider: BaseFragment.TransitionProvider? = null + + protected open val transitionProvider: BaseFragment.TransitionProvider? = + defaultTransitionProvider } diff --git a/app/src/main/java/com/skyd/anivu/base/mvi/MviIntent.kt b/app/src/main/java/com/skyd/anivu/base/mvi/MviIntent.kt index ec7d0aa8..6e419b31 100644 --- a/app/src/main/java/com/skyd/anivu/base/mvi/MviIntent.kt +++ b/app/src/main/java/com/skyd/anivu/base/mvi/MviIntent.kt @@ -1,6 +1,37 @@ package com.skyd.anivu.base.mvi +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.remember +import com.skyd.anivu.ext.startWith +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.channels.Channel +import kotlinx.coroutines.flow.collect +import kotlinx.coroutines.flow.consumeAsFlow +import kotlinx.coroutines.flow.onEach +import kotlinx.coroutines.withContext + /** * Immutable object which represent an view's intent. */ -interface MviIntent \ No newline at end of file +interface MviIntent + +@Composable +fun + AbstractMviViewModel.getDispatcher(startWith: I): (I) -> Unit { + val intentChannel = remember { Channel(Channel.UNLIMITED) } + LaunchedEffect(Unit) { + withContext(Dispatchers.Main.immediate) { + intentChannel + .consumeAsFlow() + .startWith(startWith) + .onEach(this@getDispatcher::processIntent) + .collect() + } + } + return remember { + { intent: I -> + intentChannel.trySend(intent).getOrThrow() + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/skyd/anivu/config/Const.kt b/app/src/main/java/com/skyd/anivu/config/Const.kt index 5b2465e8..15188fe1 100644 --- a/app/src/main/java/com/skyd/anivu/config/Const.kt +++ b/app/src/main/java/com/skyd/anivu/config/Const.kt @@ -5,9 +5,12 @@ import java.io.File object Const { const val GITHUB_REPO = "https://github.com/SkyD666/AniVu" + const val GITHUB_LATEST_RELEASE = "https://api.github.com/repos/SkyD666/AniVu/releases/latest" const val GITHUB_NEW_ISSUE_URL = "https://github.com/SkyD666/AniVu/issues/new" const val TELEGRAM_GROUP = "https://t.me/SkyD666Chat" const val DISCORD_SERVER = "https://discord.gg/pEWEjeJTa3" + const val AFADIAN_LINK = "https://afdian.net/a/SkyD666" + const val BUY_ME_A_COFFEE_LINK = "https://www.buymeacoffee.com/SkyD666" const val RAYS_ANDROID_URL = "https://github.com/SkyD666/Rays-Android" const val RACA_ANDROID_URL = "https://github.com/SkyD666/Raca-Android" 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 f48cecd5..91d7df7f 100644 --- a/app/src/main/java/com/skyd/anivu/ext/ContextExt.kt +++ b/app/src/main/java/com/skyd/anivu/ext/ContextExt.kt @@ -9,6 +9,7 @@ import android.content.res.Configuration import android.graphics.Point import android.os.Build import androidx.core.content.ContextCompat +import androidx.core.content.pm.PackageInfoCompat val Context.activity: Activity get() { @@ -64,6 +65,19 @@ fun Context.getAppVersionName(): String { return appVersionName } +fun Context.getAppVersionCode(): Long { + var appVersionCode: Long = 0 + try { + val packageInfo = applicationContext + .packageManager + .getPackageInfo(packageName, 0) + appVersionCode = PackageInfoCompat.getLongVersionCode(packageInfo) + } catch (e: PackageManager.NameNotFoundException) { + e.printStackTrace() + } + return appVersionCode +} + fun Context.getAppName(): String? { return try { val packageInfo: PackageInfo = packageManager.getPackageInfo(packageName, 0) diff --git a/app/src/main/java/com/skyd/anivu/ext/PreferenceExt.kt b/app/src/main/java/com/skyd/anivu/ext/PreferenceExt.kt index 3f57e2bc..76b90e15 100644 --- a/app/src/main/java/com/skyd/anivu/ext/PreferenceExt.kt +++ b/app/src/main/java/com/skyd/anivu/ext/PreferenceExt.kt @@ -1,6 +1,7 @@ package com.skyd.anivu.ext import androidx.datastore.preferences.core.Preferences +import com.skyd.anivu.model.preference.IgnoreUpdateVersionPreference import com.skyd.anivu.model.preference.Settings import com.skyd.anivu.model.preference.appearance.DarkModePreference import com.skyd.anivu.model.preference.appearance.ThemePreference @@ -10,5 +11,8 @@ fun Preferences.toSettings(): Settings { // Theme theme = ThemePreference.fromPreferences(this), darkMode = DarkModePreference.fromPreferences(this), + + // Update + ignoreUpdateVersion = IgnoreUpdateVersionPreference.fromPreferences(this), ) } diff --git a/app/src/main/java/com/skyd/anivu/ext/SnackbarExt.kt b/app/src/main/java/com/skyd/anivu/ext/SnackbarExt.kt new file mode 100644 index 00000000..11212301 --- /dev/null +++ b/app/src/main/java/com/skyd/anivu/ext/SnackbarExt.kt @@ -0,0 +1,46 @@ +package com.skyd.anivu.ext + +import androidx.compose.material3.SnackbarDuration +import androidx.compose.material3.SnackbarHostState +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.launch + +fun SnackbarHostState.showSnackbar( + scope: CoroutineScope, + message: String, + actionLabel: String? = null, + withDismissAction: Boolean = true, + duration: SnackbarDuration = if (actionLabel == null) SnackbarDuration.Short else SnackbarDuration.Indefinite +) { + scope.launch { + showSnackbar( + message = message, + actionLabel = actionLabel, + withDismissAction = withDismissAction, + duration = duration, + ) + } +} + +@Composable +fun SnackbarHostState.showSnackbarWithLaunchedEffect( + message: String, + key1: Any? = this, + key2: Any? = null, + key3: Any? = null, + actionLabel: String? = null, + withDismissAction: Boolean = true, + duration: SnackbarDuration = if (actionLabel == null) SnackbarDuration.Short else SnackbarDuration.Indefinite +): SnackbarHostState { + LaunchedEffect(key1, key2, key3) { + showSnackbar( + message = message, + actionLabel = actionLabel, + withDismissAction = withDismissAction, + duration = duration, + ) + } + return this +} \ No newline at end of file diff --git a/app/src/main/java/com/skyd/anivu/ext/WindowExt.kt b/app/src/main/java/com/skyd/anivu/ext/WindowExt.kt new file mode 100644 index 00000000..a1161c11 --- /dev/null +++ b/app/src/main/java/com/skyd/anivu/ext/WindowExt.kt @@ -0,0 +1,13 @@ +package com.skyd.anivu.ext + +import androidx.compose.material3.windowsizeclass.WindowSizeClass +import androidx.compose.material3.windowsizeclass.WindowWidthSizeClass + +val WindowSizeClass.isCompact: Boolean + get() = widthSizeClass == WindowWidthSizeClass.Compact + +val WindowSizeClass.isMedium: Boolean + get() = widthSizeClass == WindowWidthSizeClass.Medium + +val WindowSizeClass.isExpanded: Boolean + get() = widthSizeClass == WindowWidthSizeClass.Expanded \ No newline at end of file diff --git a/app/src/main/java/com/skyd/anivu/model/bean/MoreBean.kt b/app/src/main/java/com/skyd/anivu/model/bean/MoreBean.kt index 7f17c8fb..f8893c9c 100644 --- a/app/src/main/java/com/skyd/anivu/model/bean/MoreBean.kt +++ b/app/src/main/java/com/skyd/anivu/model/bean/MoreBean.kt @@ -1,15 +1,15 @@ package com.skyd.anivu.model.bean -import android.graphics.drawable.Drawable -import androidx.annotation.ColorInt -import androidx.annotation.IdRes +import androidx.annotation.DrawableRes +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.Shape import com.skyd.anivu.base.BaseBean data class MoreBean( - val icon: Drawable, - @ColorInt val iconTint: Int, val title: String, - @IdRes val navigateId: Int, - val background: Drawable, - @ColorInt val backgroundTint: Int, + @DrawableRes val icon: Int, + val iconTint: Color, + val shape: Shape, + val shapeColor: Color, + val action: () -> Unit, ) : BaseBean diff --git a/app/src/main/java/com/skyd/anivu/model/bean/OtherWorksBean.kt b/app/src/main/java/com/skyd/anivu/model/bean/OtherWorksBean.kt index 99a05279..402a2411 100644 --- a/app/src/main/java/com/skyd/anivu/model/bean/OtherWorksBean.kt +++ b/app/src/main/java/com/skyd/anivu/model/bean/OtherWorksBean.kt @@ -1,11 +1,11 @@ package com.skyd.anivu.model.bean -import android.graphics.drawable.Drawable +import androidx.annotation.DrawableRes import com.skyd.anivu.base.BaseBean data class OtherWorksBean( val name: String, - val icon: Drawable, + @DrawableRes val icon: Int, val description: String, val url: String, ) : BaseBean \ No newline at end of file diff --git a/app/src/main/java/com/skyd/anivu/model/bean/UpdateBean.kt b/app/src/main/java/com/skyd/anivu/model/bean/UpdateBean.kt new file mode 100644 index 00000000..0944b647 --- /dev/null +++ b/app/src/main/java/com/skyd/anivu/model/bean/UpdateBean.kt @@ -0,0 +1,42 @@ +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 UpdateBean( + @SerialName("tag_name") + var tagName: String, + @SerialName("name") + var name: String, + @SerialName("html_url") + var htmlUrl: String, + @SerialName("published_at") + var publishedAt: String, + @SerialName("assets") + var assets: List, + @SerialName("body") + var body: String +) : BaseBean { + + @Keep + @Serializable + class AssetsBean( + @SerialName("name") + var name: String, + @SerialName("size") + var size: Long, + @SerialName("download_count") + var downloadCount: Long?, + @SerialName("browser_download_url") + var browserDownloadUrl: String, + @SerialName("created_at") + var createdAt: String?, + @SerialName("updated_at") + var updatedAt: String? + ) : BaseBean + +} \ No newline at end of file diff --git a/app/src/main/java/com/skyd/anivu/model/preference/IgnoreUpdateVersionPreference.kt b/app/src/main/java/com/skyd/anivu/model/preference/IgnoreUpdateVersionPreference.kt new file mode 100644 index 00000000..864d8797 --- /dev/null +++ b/app/src/main/java/com/skyd/anivu/model/preference/IgnoreUpdateVersionPreference.kt @@ -0,0 +1,26 @@ +package com.skyd.anivu.model.preference + +import android.content.Context +import androidx.datastore.preferences.core.Preferences +import androidx.datastore.preferences.core.longPreferencesKey +import com.skyd.anivu.base.BasePreference +import com.skyd.anivu.ext.dataStore +import com.skyd.anivu.ext.put +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.launch + +object IgnoreUpdateVersionPreference : BasePreference { + private const val IGNORE_UPDATE_VERSION = "ignoreUpdateVersion" + override val default = 0L + + val key = longPreferencesKey(IGNORE_UPDATE_VERSION) + + fun put(context: Context, scope: CoroutineScope, value: Long) { + scope.launch(Dispatchers.IO) { + context.dataStore.put(key, value) + } + } + + override fun fromPreferences(preferences: Preferences): Long = preferences[key] ?: default +} \ No newline at end of file diff --git a/app/src/main/java/com/skyd/anivu/model/preference/Settings.kt b/app/src/main/java/com/skyd/anivu/model/preference/Settings.kt index 816762c7..7aeda933 100644 --- a/app/src/main/java/com/skyd/anivu/model/preference/Settings.kt +++ b/app/src/main/java/com/skyd/anivu/model/preference/Settings.kt @@ -11,6 +11,7 @@ import com.skyd.anivu.ext.toSettings import com.skyd.anivu.model.preference.appearance.DarkModePreference import com.skyd.anivu.model.preference.appearance.ThemePreference import com.skyd.anivu.ui.local.LocalDarkMode +import com.skyd.anivu.ui.local.LocalIgnoreUpdateVersion import com.skyd.anivu.ui.local.LocalTheme import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.flow.map @@ -19,6 +20,8 @@ data class Settings( // Theme val theme: String = ThemePreference.default, val darkMode: Int = DarkModePreference.default, + // Update + val ignoreUpdateVersion: Long = IgnoreUpdateVersionPreference.default, ) @Composable @@ -33,6 +36,8 @@ fun SettingsProvider( // Theme LocalTheme provides settings.theme, LocalDarkMode provides settings.darkMode, + // Update + LocalIgnoreUpdateVersion provides settings.ignoreUpdateVersion, ) { content() } diff --git a/app/src/main/java/com/skyd/anivu/model/repository/UpdateRepository.kt b/app/src/main/java/com/skyd/anivu/model/repository/UpdateRepository.kt new file mode 100644 index 00000000..05fa47f4 --- /dev/null +++ b/app/src/main/java/com/skyd/anivu/model/repository/UpdateRepository.kt @@ -0,0 +1,19 @@ +package com.skyd.anivu.model.repository + +import com.skyd.anivu.base.BaseRepository +import com.skyd.anivu.model.bean.UpdateBean +import com.skyd.anivu.model.service.UpdateService +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.flow +import kotlinx.coroutines.flow.flowOn +import retrofit2.Retrofit +import javax.inject.Inject + +class UpdateRepository @Inject constructor(private val retrofit: Retrofit) : BaseRepository() { + suspend fun checkUpdate(): Flow { + return flow { + emit(retrofit.create(UpdateService::class.java).checkUpdate()) + }.flowOn(Dispatchers.IO) + } +} \ No newline at end of file diff --git a/app/src/main/java/com/skyd/anivu/model/service/UpdateService.kt b/app/src/main/java/com/skyd/anivu/model/service/UpdateService.kt new file mode 100644 index 00000000..2b8049da --- /dev/null +++ b/app/src/main/java/com/skyd/anivu/model/service/UpdateService.kt @@ -0,0 +1,10 @@ +package com.skyd.anivu.model.service + +import com.skyd.anivu.config.Const +import com.skyd.anivu.model.bean.UpdateBean +import retrofit2.http.GET + +interface UpdateService { + @GET(Const.GITHUB_LATEST_RELEASE) + suspend fun checkUpdate(): UpdateBean +} \ No newline at end of file diff --git a/app/src/main/java/com/skyd/anivu/ui/adapter/variety/AniSpanSize.kt b/app/src/main/java/com/skyd/anivu/ui/adapter/variety/AniSpanSize.kt index 21bd006d..85ac615d 100644 --- a/app/src/main/java/com/skyd/anivu/ui/adapter/variety/AniSpanSize.kt +++ b/app/src/main/java/com/skyd/anivu/ui/adapter/variety/AniSpanSize.kt @@ -3,10 +3,6 @@ package com.skyd.anivu.ui.adapter.variety import androidx.recyclerview.widget.GridLayoutManager import com.skyd.anivu.appContext import com.skyd.anivu.ext.screenIsLand -import com.skyd.anivu.model.bean.FeedBean -import com.skyd.anivu.model.bean.LicenseBean -import com.skyd.anivu.model.bean.MoreBean -import com.skyd.anivu.model.bean.OtherWorksBean class AniSpanSize( private val adapter: VarietyAdapter, @@ -18,14 +14,11 @@ class AniSpanSize( fun getSpanSize(data: Any?, enableLandScape: Boolean): Int { return if (enableLandScape && appContext.screenIsLand) { when (data) { - is MoreBean -> MAX_SPAN_SIZE / 3 - is OtherWorksBean -> MAX_SPAN_SIZE / 2 - is LicenseBean -> MAX_SPAN_SIZE / 2 +// is OtherWorksBean -> MAX_SPAN_SIZE / 2 else -> MAX_SPAN_SIZE } } else { when (data) { - is MoreBean -> MAX_SPAN_SIZE / 2 else -> MAX_SPAN_SIZE } } diff --git a/app/src/main/java/com/skyd/anivu/ui/adapter/variety/ViewHolder.kt b/app/src/main/java/com/skyd/anivu/ui/adapter/variety/ViewHolder.kt index dbf2e74c..00392b34 100644 --- a/app/src/main/java/com/skyd/anivu/ui/adapter/variety/ViewHolder.kt +++ b/app/src/main/java/com/skyd/anivu/ui/adapter/variety/ViewHolder.kt @@ -10,8 +10,6 @@ import com.skyd.anivu.databinding.ItemEnclosure1Binding import com.skyd.anivu.databinding.ItemFeed1Binding import com.skyd.anivu.databinding.ItemLinkEnclosure1Binding import com.skyd.anivu.databinding.ItemMedia1Binding -import com.skyd.anivu.databinding.ItemMore1Binding -import com.skyd.anivu.databinding.ItemOtherWorks1Binding import com.skyd.anivu.databinding.ItemParentDir1Binding abstract class BaseViewHolder(val binding: V) : @@ -39,11 +37,5 @@ class Media1ViewHolder(binding: ItemMedia1Binding) : class ParentDir1ViewHolder(binding: ItemParentDir1Binding) : BaseViewHolder(binding) -class More1ViewHolder(binding: ItemMore1Binding) : - BaseViewHolder(binding) - class ColorPalette1ViewHolder(binding: ItemColorPalette1Binding) : BaseViewHolder(binding) - -class OtherWorks1ViewHolder(binding: ItemOtherWorks1Binding) : - BaseViewHolder(binding) diff --git a/app/src/main/java/com/skyd/anivu/ui/adapter/variety/proxy/More1Proxy.kt b/app/src/main/java/com/skyd/anivu/ui/adapter/variety/proxy/More1Proxy.kt deleted file mode 100644 index fcbb8101..00000000 --- a/app/src/main/java/com/skyd/anivu/ui/adapter/variety/proxy/More1Proxy.kt +++ /dev/null @@ -1,42 +0,0 @@ -package com.skyd.anivu.ui.adapter.variety.proxy - - -import android.content.res.ColorStateList -import android.view.LayoutInflater -import android.view.ViewGroup -import com.skyd.anivu.databinding.ItemMore1Binding -import com.skyd.anivu.model.bean.MoreBean -import com.skyd.anivu.ui.adapter.variety.More1ViewHolder -import com.skyd.anivu.ui.adapter.variety.VarietyAdapter - - -class More1Proxy( - private val onClick: (Int) -> Unit, -) : VarietyAdapter.Proxy() { - override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): More1ViewHolder { - val holder = More1ViewHolder( - ItemMore1Binding - .inflate(LayoutInflater.from(parent.context), parent, false), - ) - holder.itemView.setOnClickListener { - onClick(holder.bindingAdapterPosition) - } - return holder - } - - override fun onBindViewHolder( - holder: More1ViewHolder, - data: MoreBean, - index: Int, - action: ((Any?) -> Unit)? - ) { - holder.binding.ivMore1Icon.apply { - imageTintList = ColorStateList.valueOf(data.iconTint) - setImageDrawable(data.icon) - - backgroundTintList = ColorStateList.valueOf(data.backgroundTint) - background = data.background - } - holder.binding.tvMore1Title.text = data.title - } -} \ No newline at end of file diff --git a/app/src/main/java/com/skyd/anivu/ui/adapter/variety/proxy/OtherWorks1Proxy.kt b/app/src/main/java/com/skyd/anivu/ui/adapter/variety/proxy/OtherWorks1Proxy.kt deleted file mode 100644 index 688a192c..00000000 --- a/app/src/main/java/com/skyd/anivu/ui/adapter/variety/proxy/OtherWorks1Proxy.kt +++ /dev/null @@ -1,42 +0,0 @@ -package com.skyd.anivu.ui.adapter.variety.proxy - - -import android.view.LayoutInflater -import android.view.ViewGroup -import com.skyd.anivu.databinding.ItemOtherWorks1Binding -import com.skyd.anivu.ext.openBrowser -import com.skyd.anivu.model.bean.OtherWorksBean -import com.skyd.anivu.ui.adapter.variety.OtherWorks1ViewHolder -import com.skyd.anivu.ui.adapter.variety.VarietyAdapter - - -class OtherWorks1Proxy( - private val adapter: VarietyAdapter, -) : - VarietyAdapter.Proxy() { - override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): OtherWorks1ViewHolder { - val holder = OtherWorks1ViewHolder( - ItemOtherWorks1Binding - .inflate(LayoutInflater.from(parent.context), parent, false), - ) - holder.itemView.setOnClickListener { - val data = adapter.dataList.getOrNull(holder.bindingAdapterPosition) - if (data !is OtherWorksBean) return@setOnClickListener - data.url.openBrowser(it.context) - } - return holder - } - - override fun onBindViewHolder( - holder: OtherWorks1ViewHolder, - data: OtherWorksBean, - index: Int, - action: ((Any?) -> Unit)? - ) { - holder.binding.apply { - ivOtherWorks1Icon.setImageDrawable(data.icon) - tvOtherWorks1Title.text = data.name - tvOtherWorks1Description.text = data.description - } - } -} \ No newline at end of file diff --git a/app/src/main/java/com/skyd/anivu/ui/component/dialog/AniVuDialog.kt b/app/src/main/java/com/skyd/anivu/ui/component/dialog/AniVuDialog.kt new file mode 100644 index 00000000..b74190fc --- /dev/null +++ b/app/src/main/java/com/skyd/anivu/ui/component/dialog/AniVuDialog.kt @@ -0,0 +1,52 @@ +package com.skyd.anivu.ui.component.dialog + +import androidx.compose.foundation.rememberScrollState +import androidx.compose.foundation.text.selection.SelectionContainer +import androidx.compose.foundation.verticalScroll +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.Info +import androidx.compose.material3.AlertDialog +import androidx.compose.material3.Icon +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.window.DialogProperties + +@Composable +fun AniVuDialog( + modifier: Modifier = Modifier, + visible: Boolean, + properties: DialogProperties = DialogProperties(), + onDismissRequest: () -> Unit = {}, + icon: @Composable (() -> Unit)? = { + Icon( + imageVector = Icons.Default.Info, + contentDescription = null, + ) + }, + title: @Composable (() -> Unit)? = null, + text: @Composable (() -> Unit)? = null, + selectable: Boolean = true, + confirmButton: @Composable () -> Unit, + dismissButton: @Composable (() -> Unit)? = null, +) { + if (visible) { + AlertDialog( + properties = properties, + modifier = modifier, + onDismissRequest = onDismissRequest, + icon = icon, + title = title, + text = { + if (selectable) { + SelectionContainer(modifier = Modifier.verticalScroll(rememberScrollState())) { + text?.invoke() + } + } else { + text?.invoke() + } + }, + confirmButton = confirmButton, + dismissButton = dismissButton, + ) + } +} \ No newline at end of file diff --git a/app/src/main/java/com/skyd/anivu/ui/component/dialog/WaitingDialog.kt b/app/src/main/java/com/skyd/anivu/ui/component/dialog/WaitingDialog.kt new file mode 100644 index 00000000..66a152ad --- /dev/null +++ b/app/src/main/java/com/skyd/anivu/ui/component/dialog/WaitingDialog.kt @@ -0,0 +1,97 @@ +package com.skyd.anivu.ui.component.dialog + +import androidx.compose.animation.core.animateFloatAsState +import androidx.compose.foundation.layout.Column +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.material.icons.Icons +import androidx.compose.material.icons.filled.HourglassEmpty +import androidx.compose.material3.Icon +import androidx.compose.material3.LinearProgressIndicator +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.ProgressIndicatorDefaults +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.semantics.semantics +import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.unit.dp +import com.skyd.anivu.R + +@Composable +fun WaitingDialog( + visible: Boolean, + currentValue: Int? = null, + totalValue: Int? = null, + msg: String? = null, + title: String = stringResource(R.string.warning) +) { + if (currentValue == null || totalValue == null) { + WaitingDialog( + visible = visible, + title = title, + text = if (msg == null) null else { + { + Text( + text = msg, + style = MaterialTheme.typography.labelLarge, + textAlign = TextAlign.Center, + ) + } + } + ) + } else { + WaitingDialog(visible = visible, title = title) { + Column( + modifier = Modifier + .fillMaxWidth() + .padding(10.dp), + horizontalAlignment = Alignment.CenterHorizontally + ) { + val animatedProgress by animateFloatAsState( + targetValue = currentValue.toFloat() / totalValue, + animationSpec = ProgressIndicatorDefaults.ProgressAnimationSpec, + label = "waitingDialogAnimatedProgress" + ) + LinearProgressIndicator( + progress = { animatedProgress }, + modifier = Modifier.semantics(mergeDescendants = true) {}, + ) + Spacer(modifier = Modifier.height(10.dp)) + Text( + text = "$currentValue / $totalValue", + style = MaterialTheme.typography.labelLarge + ) + if (msg != null) { + Spacer(modifier = Modifier.height(10.dp)) + Text( + text = msg, + style = MaterialTheme.typography.labelLarge, + textAlign = TextAlign.Center, + ) + } + } + } + } +} + +@Composable +fun WaitingDialog( + visible: Boolean, + title: String = stringResource(R.string.waiting), + text: @Composable (() -> Unit)? = null, +) { + AniVuDialog( + visible = visible, + onDismissRequest = { }, + icon = { Icon(imageVector = Icons.Default.HourglassEmpty, contentDescription = null) }, + title = { Text(text = title) }, + text = text, + confirmButton = {} + ) +} \ No newline at end of file diff --git a/app/src/main/java/com/skyd/anivu/ui/component/lazyverticalgrid/RaysLazyVerticalGrid.kt b/app/src/main/java/com/skyd/anivu/ui/component/lazyverticalgrid/RaysLazyVerticalGrid.kt new file mode 100644 index 00000000..3e8b5067 --- /dev/null +++ b/app/src/main/java/com/skyd/anivu/ui/component/lazyverticalgrid/RaysLazyVerticalGrid.kt @@ -0,0 +1,65 @@ +package com.skyd.anivu.ui.component.lazyverticalgrid + +import androidx.compose.foundation.layout.PaddingValues +import androidx.compose.foundation.lazy.grid.GridCells +import androidx.compose.foundation.lazy.grid.GridItemSpan +import androidx.compose.foundation.lazy.grid.LazyVerticalGrid +import androidx.compose.foundation.lazy.grid.itemsIndexed +import androidx.compose.foundation.lazy.grid.rememberLazyGridState +import androidx.compose.runtime.Composable +import androidx.compose.runtime.remember +import androidx.compose.ui.Modifier +import androidx.compose.ui.platform.LocalConfiguration +import androidx.compose.ui.platform.LocalContext +import com.skyd.anivu.ui.component.lazyverticalgrid.adapter.AniVuItemSpace.anivuItemSpace +import com.skyd.anivu.ui.component.lazyverticalgrid.adapter.LazyGridAdapter +import com.skyd.anivu.ui.component.lazyverticalgrid.adapter.MAX_SPAN_SIZE +import com.skyd.anivu.ui.component.lazyverticalgrid.adapter.anivuShowSpan + +@Composable +fun AniVuLazyVerticalGrid( + modifier: Modifier = Modifier, + contentPadding: PaddingValues = PaddingValues(), + dataList: List, + adapter: LazyGridAdapter, + enableLandScape: Boolean = true, // 是否启用横屏使用另一套布局方案 + key: ((index: Int, item: Any) -> Any)? = null +) { + val context = LocalContext.current + val listState = rememberLazyGridState() + val spanIndexArray: MutableList = remember { mutableListOf() } + val configuration = LocalConfiguration.current + LazyVerticalGrid( + modifier = modifier, + columns = GridCells.Fixed(MAX_SPAN_SIZE), + state = listState, + contentPadding = contentPadding + ) { + itemsIndexed( + items = dataList, + key = key, + span = { index, item -> + val spanIndex = maxLineSpan - maxCurrentLineSpan + if (spanIndexArray.size > index) spanIndexArray[index] = spanIndex + else spanIndexArray.add(spanIndex) + GridItemSpan( + anivuShowSpan( + data = item, + enableLandScape = enableLandScape, + context = context, + ) + ) + } + ) { index, item -> + adapter.Draw( + modifier = Modifier.anivuItemSpace( + item = item, + spanSize = anivuShowSpan(data = item, context = context), + spanIndex = spanIndexArray[index] + ), + index = index, + data = item + ) + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/skyd/anivu/ui/component/lazyverticalgrid/adapter/AniVuShowSpan.kt b/app/src/main/java/com/skyd/anivu/ui/component/lazyverticalgrid/adapter/AniVuShowSpan.kt new file mode 100644 index 00000000..da5963f1 --- /dev/null +++ b/app/src/main/java/com/skyd/anivu/ui/component/lazyverticalgrid/adapter/AniVuShowSpan.kt @@ -0,0 +1,22 @@ +package com.skyd.anivu.ui.component.lazyverticalgrid.adapter + +import android.content.Context +import com.skyd.anivu.ext.screenIsLand +import com.skyd.anivu.model.bean.MoreBean + +const val MAX_SPAN_SIZE = 60 +fun anivuShowSpan( + data: Any, + enableLandScape: Boolean = true, + context: Context +): Int = if (enableLandScape && context.screenIsLand) { + when (data) { + is MoreBean -> MAX_SPAN_SIZE / 3 + else -> MAX_SPAN_SIZE / 3 + } +} else { + when (data) { + is MoreBean -> MAX_SPAN_SIZE / 2 + else -> MAX_SPAN_SIZE / 1 + } +} \ No newline at end of file diff --git a/app/src/main/java/com/skyd/anivu/ui/component/lazyverticalgrid/adapter/LazyGridAdapter.kt b/app/src/main/java/com/skyd/anivu/ui/component/lazyverticalgrid/adapter/LazyGridAdapter.kt new file mode 100644 index 00000000..7f4b2a3f --- /dev/null +++ b/app/src/main/java/com/skyd/anivu/ui/component/lazyverticalgrid/adapter/LazyGridAdapter.kt @@ -0,0 +1,41 @@ +package com.skyd.anivu.ui.component.lazyverticalgrid.adapter + +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import java.lang.reflect.ParameterizedType + +class LazyGridAdapter( + private var proxyList: MutableList> = mutableListOf(), +) { + @Suppress("UNCHECKED_CAST") + @Composable + fun Draw(modifier: Modifier, index: Int, data: Any) { + val type: Int = getProxyIndex(data) + if (type != -1) (proxyList[type] as Proxy).Draw(modifier, index, data) + } + + // 获取策略在列表中的索引,可能返回-1 + private fun getProxyIndex(data: Any): Int = proxyList.indexOfFirst { + // 如果Proxy中的第一个类型参数T和数据的类型相同,则返回对应策略的索引 + (it.javaClass.genericSuperclass as ParameterizedType).actualTypeArguments[0].let { argument -> + if (argument.toString() == data.javaClass.toString()) + true // 正常情况 + else if (((argument as? ParameterizedType)?.rawType as? Class<*>) + ?.isAssignableFrom(data.javaClass) == true + ) { + true // data是T的子类的情况 + } else { + // Proxy第一个泛型是类似List,又嵌套了个泛型 + if (argument is ParameterizedType) + argument.rawType.toString() == data.javaClass.toString() + else false + } + } + } + + // 抽象策略类 + abstract class Proxy { + @Composable + abstract fun Draw(modifier: Modifier, index: Int, data: T) + } +} diff --git a/app/src/main/java/com/skyd/anivu/ui/component/lazyverticalgrid/adapter/RaysItemSpace.kt b/app/src/main/java/com/skyd/anivu/ui/component/lazyverticalgrid/adapter/RaysItemSpace.kt new file mode 100644 index 00000000..695632b5 --- /dev/null +++ b/app/src/main/java/com/skyd/anivu/ui/component/lazyverticalgrid/adapter/RaysItemSpace.kt @@ -0,0 +1,152 @@ +package com.skyd.anivu.ui.component.lazyverticalgrid.adapter + +import androidx.compose.foundation.layout.PaddingValues +import androidx.compose.foundation.layout.padding +import androidx.compose.ui.Modifier +import androidx.compose.ui.unit.dp + +object AniVuItemSpace { + private val ITEM_SPACING = 12.dp + private val HORIZONTAL_PADDING = 16.dp + + fun Modifier.anivuItemSpace(item: Any, spanSize: Int, spanIndex: Int) = + this.padding(getItemSpace(item, spanSize, spanIndex)) + + private fun getItemSpace(item: Any, spanSize: Int, spanIndex: Int): PaddingValues { + var top = 0.dp + var bottom = 0.dp + var start = 0.dp + var end = 0.dp + if (needVerticalMargin(item.javaClass)) { + top = 10.dp + bottom = 2.dp + } + if (spanSize == MAX_SPAN_SIZE) { + /** + * 只有一列 + */ + if (noHorizontalMargin(item.javaClass)) { + return PaddingValues(top = top, bottom = bottom, start = start, end = end) + } + start = HORIZONTAL_PADDING + end = HORIZONTAL_PADDING + } else if (spanSize == MAX_SPAN_SIZE / 2) { + /** + * 只有两列,没有在中间的item + * 2x = ITEM_SPACING + */ + val x = ITEM_SPACING / 2f + if (spanIndex == 0) { + start = HORIZONTAL_PADDING + end = x + } else { + start = x + end = HORIZONTAL_PADDING + } + } else if (spanSize == MAX_SPAN_SIZE / 3) { + /** + * 只有三列,一个在中间的item + * HORIZONTAL_PADDING + x = 2y + * x + y = ITEM_SPACING + */ + val y = (HORIZONTAL_PADDING + ITEM_SPACING) / 3f + val x = ITEM_SPACING - y + if (spanIndex == 0) { + start = HORIZONTAL_PADDING + end = x + } else if (spanIndex + spanSize == MAX_SPAN_SIZE) { + // 最右侧最后一个 + start = x + end = HORIZONTAL_PADDING + } else { + start = y + end = y + } + } else if (spanSize == MAX_SPAN_SIZE / 5) { + /** + * 只有五列 + * HORIZONTAL_PADDING + x = y + z + * x + y = ITEM_SPACING + * z + (HORIZONTAL_PADDING + x) / 2 = ITEM_SPACING + */ + val x = (ITEM_SPACING * 4 - HORIZONTAL_PADDING * 3) / 5f + val y = ITEM_SPACING - x + val z = HORIZONTAL_PADDING + x - y + if (spanIndex == 0) { + // 最左侧第一个 + start = HORIZONTAL_PADDING + end = x + } else if (spanIndex + spanSize == MAX_SPAN_SIZE) { + // 最右侧最后一个 + start = x + end = HORIZONTAL_PADDING + } else if (spanIndex == spanSize) { + // 第二个 + start = y + end = z + } else if (spanIndex == MAX_SPAN_SIZE - 2 * spanSize) { + // 倒数第二个 + start = z + end = y + } else { + // 最中间的 + start = (HORIZONTAL_PADDING + x) / 2f + end = (HORIZONTAL_PADDING + x) / 2f + } + } else { + /** + * 多于三列(不包括五列),有在中间的item + */ + if ((MAX_SPAN_SIZE / spanSize) % 2 == 0) { + /** + * 偶数个item + * HORIZONTAL_PADDING + x = y + ITEM_SPACING / 2 + * x + y = ITEM_SPACING + */ + val y = (HORIZONTAL_PADDING + ITEM_SPACING / 2f) / 2f + val x = ITEM_SPACING - y + if (spanIndex == 0) { + // 最左侧第一个 + start = HORIZONTAL_PADDING + end = x + } else if (spanIndex + spanSize == MAX_SPAN_SIZE) { + // 最右侧最后一个 + start = x + end = HORIZONTAL_PADDING + } else { + // 中间的项目 + if (spanIndex < MAX_SPAN_SIZE / 2) { + // 左侧部分 + start = y + end = ITEM_SPACING / 2 + } else { + // 右侧部分 + start = ITEM_SPACING / 2 + end = y + } + } + } else { + /** + * 奇数个item,严格大于5的奇数(暂无需求,未实现) + */ + } + } + return PaddingValues(top = top, bottom = bottom, start = start, end = end) + } + + private val noHorizontalMarginType: Set> = setOf( + ) + + private fun noHorizontalMargin(clz: Class<*>?): Boolean { + clz ?: return true + return clz in noHorizontalMarginType + } + + private val needVerticalMarginType: Set> = setOf( + ) + + private fun needVerticalMargin(clz: Class<*>?): Boolean { + clz ?: return false + return clz in needVerticalMarginType + } +} diff --git a/app/src/main/java/com/skyd/anivu/ui/component/lazyverticalgrid/adapter/proxy/MoreProxy.kt b/app/src/main/java/com/skyd/anivu/ui/component/lazyverticalgrid/adapter/proxy/MoreProxy.kt new file mode 100644 index 00000000..63bd45dc --- /dev/null +++ b/app/src/main/java/com/skyd/anivu/ui/component/lazyverticalgrid/adapter/proxy/MoreProxy.kt @@ -0,0 +1,83 @@ +package com.skyd.anivu.ui.component.lazyverticalgrid.adapter.proxy + +import androidx.compose.foundation.background +import androidx.compose.foundation.basicMarquee +import androidx.compose.foundation.combinedClickable +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material3.Icon +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.OutlinedCard +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.unit.dp +import com.skyd.anivu.model.bean.MoreBean +import com.skyd.anivu.ui.component.lazyverticalgrid.adapter.LazyGridAdapter + +class MoreProxy( + private val onClickListener: ((data: MoreBean) -> Unit)? = null +) : LazyGridAdapter.Proxy() { + @Composable + override fun Draw(modifier: Modifier, index: Int, data: MoreBean) { + More1Item(modifier = modifier, data = data, onClickListener = onClickListener) + } +} + +@Composable +fun More1Item( + modifier: Modifier, + data: MoreBean, + onClickListener: ((data: MoreBean) -> Unit)? = null +) { + OutlinedCard( + modifier = modifier.padding(vertical = 6.dp), + shape = RoundedCornerShape(16) + ) { + Column( + modifier = Modifier + .fillMaxWidth() + .combinedClickable( + onClick = { + onClickListener?.invoke(data) + } + ) + .padding(25.dp), + horizontalAlignment = Alignment.CenterHorizontally + ) { + Box( + modifier = Modifier + .padding(5.dp) + .background( + color = data.shapeColor, + shape = data.shape + ) + .padding(16.dp) + ) { + Icon( + modifier = Modifier.size(35.dp), + painter = painterResource(id = data.icon), + contentDescription = null, + tint = data.iconTint + ) + } + Text( + modifier = Modifier + .padding(horizontal = 5.dp) + .padding(top = 15.dp) + .basicMarquee(iterations = Int.MAX_VALUE), + text = data.title, + style = MaterialTheme.typography.titleMedium, + maxLines = 1, + textAlign = TextAlign.Center + ) + } + } +} diff --git a/app/src/main/java/com/skyd/anivu/ui/component/shape/CloverShape.kt b/app/src/main/java/com/skyd/anivu/ui/component/shape/CloverShape.kt new file mode 100644 index 00000000..fdfdb508 --- /dev/null +++ b/app/src/main/java/com/skyd/anivu/ui/component/shape/CloverShape.kt @@ -0,0 +1,50 @@ +package com.skyd.anivu.ui.component.shape + +import android.graphics.Matrix +import androidx.compose.ui.geometry.Size +import androidx.compose.ui.graphics.Outline +import androidx.compose.ui.graphics.Path +import androidx.compose.ui.graphics.Shape +import androidx.compose.ui.graphics.asAndroidPath +import androidx.compose.ui.graphics.asComposePath +import androidx.compose.ui.unit.Density +import androidx.compose.ui.unit.LayoutDirection + +val CloverShape: Shape = object : Shape { + override fun createOutline( + size: Size, + layoutDirection: LayoutDirection, + density: Density + ): Outline { + val baseWidth = 200f + val baseHeight = 200f + + val path = Path().apply { + moveTo(12f, 100f) + cubicTo(12f, 76f, 0f, 77.6142f, 0f, 50f) + cubicTo(0f, 22.3858f, 22.3858f, 0f, 50f, 0f) + cubicTo(77.6142f, 0f, 76f, 12f, 100f, 12f) + cubicTo(124f, 12f, 122.3858f, 0f, 150f, 0f) + cubicTo(177.6142f, 0f, 200f, 22.3858f, 200f, 50f) + cubicTo(200f, 77.6142f, 188f, 76f, 188f, 100f) + cubicTo(188f, 124f, 200f, 122.3858f, 200f, 150f) + cubicTo(200f, 177.6142f, 177.6142f, 200f, 150f, 200f) + cubicTo(122.3858f, 200f, 124f, 188f, 100f, 188f) + cubicTo(76f, 188f, 77.6142f, 200f, 50f, 200f) + cubicTo(22.3858f, 200f, 0f, 177.6142f, 0f, 150f) + cubicTo(0f, 122.3858f, 12f, 124f, 12f, 100f) + close() + } + + return Outline.Generic( + path + .asAndroidPath() + .apply { + transform(Matrix().apply { + setScale(size.width / baseWidth, size.height / baseHeight) + }) + } + .asComposePath() + ) + } +} \ No newline at end of file diff --git a/app/src/main/java/com/skyd/anivu/ui/component/shape/CurlyCornerShape.kt b/app/src/main/java/com/skyd/anivu/ui/component/shape/CurlyCornerShape.kt new file mode 100644 index 00000000..48246520 --- /dev/null +++ b/app/src/main/java/com/skyd/anivu/ui/component/shape/CurlyCornerShape.kt @@ -0,0 +1,80 @@ +package com.skyd.anivu.ui.component.shape + +import androidx.compose.foundation.shape.CornerBasedShape +import androidx.compose.foundation.shape.CornerSize +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.foundation.shape.ZeroCornerSize +import androidx.compose.ui.geometry.Size +import androidx.compose.ui.graphics.Outline +import androidx.compose.ui.graphics.Path +import androidx.compose.ui.unit.LayoutDirection +import kotlin.math.cos +import kotlin.math.sin + +class CurlyCornerShape( + private val amp: Float = 16f, + private val count: Int = 12, +) : CornerBasedShape( + topStart = ZeroCornerSize, + topEnd = ZeroCornerSize, + bottomEnd = ZeroCornerSize, + bottomStart = ZeroCornerSize +) { + + private fun sineCircleXYatAngle( + d1: Float, + d2: Float, + d3: Float, + d4: Float, + d5: Float, + i: Int, + ): List = (i * d5).run { + listOf( + (sin(this) * d4 + d3) * cos(d5) + d1, + (sin(this) * d4 + d3) * sin(d5) + d2 + ) + } + + override fun createOutline( + size: Size, + topStart: Float, + topEnd: Float, + bottomEnd: Float, + bottomStart: Float, + layoutDirection: LayoutDirection, + ): Outline { + val d = 2f + val r2: Float = size.width / d + var r13: Float = size.height / d + val r18: Float = size.width / 2f - amp + val path = Path() + path.moveTo((d * r2 - amp), r13) + var i = 0 + while (true) { + val i2 = i + 1 + val d3 = r13 + val r5: List = sineCircleXYatAngle( + r2, r13, r18, amp, Math.toRadians(i.toDouble()).toFloat(), count + ) + path.lineTo(r5[0], r5[1]) + if (i2 >= 360) { + path.close() + return Outline.Generic(path) + } + i = i2 + r13 = d3 + } + } + + override fun copy( + topStart: CornerSize, + topEnd: CornerSize, + bottomEnd: CornerSize, + bottomStart: CornerSize, + ) = RoundedCornerShape( + topStart = topStart, + topEnd = topEnd, + bottomEnd = bottomEnd, + bottomStart = bottomStart + ) +} \ No newline at end of file diff --git a/app/src/main/java/com/skyd/anivu/ui/component/shape/PolygonShape.kt b/app/src/main/java/com/skyd/anivu/ui/component/shape/PolygonShape.kt new file mode 100644 index 00000000..74176537 --- /dev/null +++ b/app/src/main/java/com/skyd/anivu/ui/component/shape/PolygonShape.kt @@ -0,0 +1,48 @@ +package com.skyd.anivu.ui.component.shape + +import androidx.compose.ui.geometry.Size +import androidx.compose.ui.graphics.Outline +import androidx.compose.ui.graphics.Path +import androidx.compose.ui.graphics.Shape +import androidx.compose.ui.unit.Density +import androidx.compose.ui.unit.LayoutDirection +import kotlin.math.PI +import kotlin.math.cos +import kotlin.math.min +import kotlin.math.sin + + +/** + * Shape describing Polygons + * + * Note: The shape draws within the minimum of provided width and height so can't be used to create stretched shape. + * + * @param sides number of sides. + * @param rotation value between 0 - 360 + */ +class PolygonShape(sides: Int, private val rotation: Float = 0f) : Shape { + private val stepCount = 2 * PI / sides + private val rotationDegree = PI / 180 * rotation + + override fun createOutline( + size: Size, + layoutDirection: LayoutDirection, + density: Density + ): Outline = Outline.Generic(Path().apply { + val r = min(size.height, size.width) * 0.5f + + val xCenter = size.width * 0.5f + val yCenter = size.height * 0.5f + + moveTo(xCenter, yCenter) + + var t = -rotationDegree + + while (t <= 2 * PI) { + lineTo((r * cos(t) + xCenter).toFloat(), (r * sin(t) + yCenter).toFloat()) + t += stepCount + } + + lineTo((r * cos(t) + xCenter).toFloat(), (r * sin(t) + yCenter).toFloat()) + }) +} \ No newline at end of file diff --git a/app/src/main/java/com/skyd/anivu/ui/component/shape/RoundedCornerStarShape.kt b/app/src/main/java/com/skyd/anivu/ui/component/shape/RoundedCornerStarShape.kt new file mode 100644 index 00000000..d7df32c7 --- /dev/null +++ b/app/src/main/java/com/skyd/anivu/ui/component/shape/RoundedCornerStarShape.kt @@ -0,0 +1,62 @@ +package com.skyd.anivu.ui.component.shape + +import android.graphics.Matrix +import androidx.compose.ui.geometry.Size +import androidx.compose.ui.graphics.Outline +import androidx.compose.ui.graphics.Path +import androidx.compose.ui.graphics.Shape +import androidx.compose.ui.graphics.asAndroidPath +import androidx.compose.ui.graphics.asComposePath +import androidx.compose.ui.unit.Density +import androidx.compose.ui.unit.LayoutDirection + +val RoundedCornerStarShape: Shape = object : Shape { + override fun createOutline( + size: Size, + layoutDirection: LayoutDirection, + density: Density + ): Outline { + val baseWidth = 183.51f + val baseHeight = 183.51f + + val path = Path().apply { + relativeMoveTo(91.76f, 6.76f) + relativeLineTo(0f, 0f) + relativeCubicTo(23.16f, -15.72f, 54.85f, -2.6f, 60.1f, 24.9f) + relativeLineTo(0f, 0f) + relativeCubicTo(0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f) + relativeCubicTo(27.49f, 5.26f, 40.62f, 36.95f, 24.9f, 60.1f) + relativeLineTo(0f, 0f) + relativeCubicTo(0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f) + relativeCubicTo(15.72f, 23.16f, 2.6f, 54.85f, -24.9f, 60.1f) + relativeLineTo(0f, 0f) + relativeCubicTo(0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f) + relativeCubicTo(-5.26f, 27.49f, -36.95f, 40.62f, -60.1f, 24.9f) + relativeLineTo(0f, 0f) + relativeCubicTo(0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f) + relativeCubicTo(-23.16f, 15.72f, -54.85f, 2.6f, -60.1f, -24.9f) + relativeLineTo(0f, 0f) + relativeCubicTo(0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f) + relativeCubicTo(-27.49f, -5.26f, -40.62f, -36.95f, -24.9f, -60.1f) + relativeLineTo(0f, 0f) + relativeCubicTo(0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f) + relativeCubicTo(-15.72f, -23.16f, -2.6f, -54.85f, 24.9f, -60.1f) + relativeLineTo(0f, 0f) + relativeCubicTo(0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f) + cubicTo(36.91f, 4.16f, 68.6f, -8.97f, 91.76f, 6.76f) + relativeLineTo(0f, 0f) + close() + } + + return Outline.Generic( + path + .asAndroidPath() + .apply { + transform(Matrix().apply { + setScale(size.width / baseWidth, size.height / baseHeight) + }) + } + .asComposePath() + ) + } +} \ No newline at end of file diff --git a/app/src/main/java/com/skyd/anivu/ui/component/shape/SquircleShape.kt b/app/src/main/java/com/skyd/anivu/ui/component/shape/SquircleShape.kt new file mode 100644 index 00000000..0a0ae35b --- /dev/null +++ b/app/src/main/java/com/skyd/anivu/ui/component/shape/SquircleShape.kt @@ -0,0 +1,42 @@ +package com.skyd.anivu.ui.component.shape + +import android.graphics.Matrix +import androidx.compose.ui.geometry.Size +import androidx.compose.ui.graphics.Outline +import androidx.compose.ui.graphics.Path +import androidx.compose.ui.graphics.Shape +import androidx.compose.ui.graphics.asAndroidPath +import androidx.compose.ui.graphics.asComposePath +import androidx.compose.ui.unit.Density +import androidx.compose.ui.unit.LayoutDirection + +val SquircleShape: Shape = object : Shape { + override fun createOutline( + size: Size, + layoutDirection: LayoutDirection, + density: Density + ): Outline { + val baseWidth = 1000f + val baseHeight = 1000f + + val path = Path().apply { + moveTo(0f, 500f) + cubicTo(0f, 88.25f, 88.25f, 0f, 500f, 0f) + cubicTo(911.75f, 0f, 1000f, 88.25f, 1000f, 500f) + cubicTo(1000f, 911.75f, 911.75f, 1000f, 500f, 1000f) + cubicTo(88.25f, 1000f, 0f, 911.75f, 0f, 500f) + close() + } + + return Outline.Generic( + path + .asAndroidPath() + .apply { + transform(Matrix().apply { + setScale(size.width / baseWidth, size.height / baseHeight) + }) + } + .asComposePath() + ) + } +} \ No newline at end of file diff --git a/app/src/main/java/com/skyd/anivu/ui/fragment/about/AboutFragment.kt b/app/src/main/java/com/skyd/anivu/ui/fragment/about/AboutFragment.kt index 77452f96..d625b5c5 100644 --- a/app/src/main/java/com/skyd/anivu/ui/fragment/about/AboutFragment.kt +++ b/app/src/main/java/com/skyd/anivu/ui/fragment/about/AboutFragment.kt @@ -2,112 +2,503 @@ package com.skyd.anivu.ui.fragment.about import android.os.Bundle import android.view.LayoutInflater +import android.view.View import android.view.ViewGroup -import androidx.appcompat.content.res.AppCompatResources -import androidx.core.view.ViewCompat -import androidx.navigation.fragment.findNavController -import androidx.recyclerview.widget.GridLayoutManager +import androidx.compose.foundation.Image +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.Column +import androidx.compose.foundation.layout.IntrinsicSize +import androidx.compose.foundation.layout.PaddingValues +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.aspectRatio +import androidx.compose.foundation.layout.fillMaxHeight +import androidx.compose.foundation.layout.fillMaxSize +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.lazy.LazyColumn +import androidx.compose.foundation.lazy.itemsIndexed +import androidx.compose.foundation.rememberScrollState +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.foundation.verticalScroll +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.Balance +import androidx.compose.material.icons.filled.Coffee +import androidx.compose.material.icons.filled.Lightbulb +import androidx.compose.material.icons.filled.Translate +import androidx.compose.material.icons.filled.Update +import androidx.compose.material3.Badge +import androidx.compose.material3.BadgedBox +import androidx.compose.material3.Button +import androidx.compose.material3.Card +import androidx.compose.material3.HorizontalDivider +import androidx.compose.material3.Icon +import androidx.compose.material3.ListItem +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Scaffold +import androidx.compose.material3.SnackbarHost +import androidx.compose.material3.SnackbarHostState +import androidx.compose.material3.Text +import androidx.compose.material3.TextButton +import androidx.compose.material3.TopAppBarDefaults +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.rememberCoroutineScope +import androidx.compose.runtime.saveable.rememberSaveable +import androidx.compose.runtime.setValue +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.rotate +import androidx.compose.ui.graphics.ColorFilter +import androidx.compose.ui.input.nestedscroll.nestedScroll +import androidx.compose.ui.layout.ContentScale +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.platform.LocalDensity +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.semantics.contentDescription +import androidx.compose.ui.semantics.semantics +import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.unit.dp +import coil.compose.AsyncImage import com.skyd.anivu.R -import com.skyd.anivu.base.BaseFragment +import com.skyd.anivu.base.BaseComposeFragment import com.skyd.anivu.config.Const -import com.skyd.anivu.databinding.FragmentAboutBinding -import com.skyd.anivu.ext.addBadge -import com.skyd.anivu.ext.addInsetsByPadding -import com.skyd.anivu.ext.dp import com.skyd.anivu.ext.getAppVersionName +import com.skyd.anivu.ext.isCompact import com.skyd.anivu.ext.openBrowser -import com.skyd.anivu.ext.popBackStackWithLifecycle +import com.skyd.anivu.ext.plus +import com.skyd.anivu.ext.showSnackbar import com.skyd.anivu.model.bean.OtherWorksBean -import com.skyd.anivu.ui.adapter.decoration.AniVuItemDecoration -import com.skyd.anivu.ui.adapter.variety.AniSpanSize -import com.skyd.anivu.ui.adapter.variety.VarietyAdapter -import com.skyd.anivu.ui.adapter.variety.proxy.OtherWorks1Proxy +import com.skyd.anivu.ui.component.AniVuIconButton +import com.skyd.anivu.ui.component.AniVuTopBar +import com.skyd.anivu.ui.component.AniVuTopBarStyle +import com.skyd.anivu.ui.component.dialog.AniVuDialog +import com.skyd.anivu.ui.component.shape.CloverShape +import com.skyd.anivu.ui.component.shape.CurlyCornerShape +import com.skyd.anivu.ui.component.shape.SquircleShape +import com.skyd.anivu.ui.fragment.about.update.UpdateDialog +import com.skyd.anivu.ui.local.LocalNavController +import com.skyd.anivu.ui.local.LocalWindowSizeClass import dagger.hilt.android.AndroidEntryPoint +import kotlinx.coroutines.launch +import java.util.Calendar @AndroidEntryPoint -class AboutFragment : BaseFragment() { - private val adapter = VarietyAdapter( - mutableListOf() - ).apply { - addProxy(OtherWorks1Proxy(adapter = this)) - } +class AboutFragment : BaseComposeFragment() { + override fun onCreateView( + inflater: LayoutInflater, + container: ViewGroup?, + savedInstanceState: Bundle? + ): View = setContentBase { AboutScreen() } +} - override fun onCreate(savedInstanceState: Bundle?) { - super.onCreate(savedInstanceState) +@Composable +fun AboutScreen() { + val scrollBehavior = TopAppBarDefaults.exitUntilCollapsedScrollBehavior() + val navController = LocalNavController.current + val context = LocalContext.current + val scope = rememberCoroutineScope() + val snackbarHostState = remember { SnackbarHostState() } + var openUpdateDialog by rememberSaveable { mutableStateOf(false) } + var openSponsorDialog by rememberSaveable { mutableStateOf(false) } - adapter.dataList += mutableListOf( - OtherWorksBean( - name = getString(R.string.about_fragment_other_works_rays_name), - icon = AppCompatResources.getDrawable(requireContext(), R.drawable.ic_rays)!!, - description = getString(R.string.about_fragment_other_works_rays_description), - url = Const.RAYS_ANDROID_URL, - ), - OtherWorksBean( - name = getString(R.string.about_fragment_other_works_raca_name), - icon = AppCompatResources.getDrawable(requireContext(), R.drawable.ic_raca)!!, - description = getString(R.string.about_fragment_other_works_raca_description), - url = Const.RACA_ANDROID_URL, - ), - OtherWorksBean( - name = getString(R.string.about_fragment_other_works_night_screen_name), - icon = AppCompatResources.getDrawable( - requireContext(), - R.drawable.ic_night_screen - )!!, - description = getString(R.string.about_fragment_other_works_night_screen_description), - url = Const.NIGHT_SCREEN_URL, - ), - ) - } + Scaffold( + snackbarHost = { SnackbarHost(hostState = snackbarHostState) }, + topBar = { + AniVuTopBar( + style = AniVuTopBarStyle.Large, + scrollBehavior = scrollBehavior, + title = { Text(text = stringResource(R.string.about_screen_name)) }, + actions = { + AniVuIconButton( + imageVector = Icons.Default.Balance, + contentDescription = stringResource(id = R.string.license_screen_name), + onClick = { navController.navigate(R.id.action_to_license_fragment) } + ) + AniVuIconButton( + onClick = { openUpdateDialog = true }, + imageVector = Icons.Default.Update, + contentDescription = stringResource(id = R.string.update_check) + ) + }, + ) + } + ) { paddingValues -> + val windowSizeClass = LocalWindowSizeClass.current + val otherWorksList = rememberOtherWorksList() - override fun FragmentAboutBinding.initView() { - topAppBar.setNavigationOnClickListener { findNavController().popBackStackWithLifecycle() } - topAppBar.setOnMenuItemClickListener { - when (it.itemId) { - R.id.action_about_fragment_license -> { - findNavController().navigate(R.id.action_to_license_fragment) - true + LazyColumn( + modifier = Modifier + .fillMaxSize() + .nestedScroll(scrollBehavior.nestedScrollConnection), + contentPadding = paddingValues + PaddingValues(16.dp), + horizontalAlignment = Alignment.CenterHorizontally + ) { + if (windowSizeClass.isCompact) { + item { IconArea() } + item { TextArea() } + item { + HelpArea( + openSponsorDialog = openSponsorDialog, + onTranslateClick = { + snackbarHostState.showSnackbar( + message = "Coming soon...", + scope = scope, + withDismissAction = true, + ) + }, + onSponsorDialogVisibleChange = { openSponsorDialog = it } + ) + ButtonArea() } + } else { + item { + Row(modifier = Modifier.fillMaxWidth()) { + Column( + modifier = Modifier.weight(0.95f), + horizontalAlignment = Alignment.CenterHorizontally, + ) { + IconArea() + ButtonArea() + } + Spacer(modifier = Modifier.width(16.dp)) + Column(modifier = Modifier.weight(1f)) { + TextArea() + HelpArea( + openSponsorDialog = openSponsorDialog, + onTranslateClick = { + snackbarHostState.showSnackbar( + message = "Coming soon...", + scope = scope, + withDismissAction = true, + ) + }, + onSponsorDialogVisibleChange = { openSponsorDialog = it } + ) + } + } + Spacer(modifier = Modifier.height(10.dp)) + } + } - else -> false + item { + Text( + text = stringResource(R.string.about_screen_other_works), + style = MaterialTheme.typography.titleMedium, + ) + } + itemsIndexed(items = otherWorksList) { _, item -> + OtherWorksItem(data = item) } } - tvAboutFragmentAppName.addBadge { - isVisible = true - verticalOffset = 7.dp - horizontalOffset = 10.dp - text = requireContext().getAppVersionName() + var isRetry by rememberSaveable { mutableStateOf(false) } + + if (openUpdateDialog) { + UpdateDialog( + isRetry = isRetry, + onClosed = { openUpdateDialog = false }, + onSuccess = { isRetry = false }, + onError = { msg -> + isRetry = true + openUpdateDialog = false + scope.launch { + snackbarHostState.showSnackbar( + message = context.getString(R.string.update_check_failed, msg), + withDismissAction = true, + ) + } + } + ) } + } +} - btnAboutFragmentGithub.setOnClickListener { - Const.GITHUB_REPO.openBrowser(it.context) +@Composable +private fun IconArea() { + Box( + modifier = Modifier + .padding(16.dp) + .size(120.dp) + ) { + Image( + modifier = Modifier.aspectRatio(1f), + contentScale = ContentScale.Crop, + alignment = Alignment.Center, + painter = painterResource(id = R.drawable.ic_icon_2_24), + colorFilter = ColorFilter.tint(MaterialTheme.colorScheme.primary), + contentDescription = null + ) + val c = Calendar.getInstance() + val month = c.get(Calendar.MONTH) + val day = c.get(Calendar.DAY_OF_MONTH) + if (month == Calendar.DECEMBER && (day in 22..28)) { // Xmas + Image( + modifier = Modifier + .fillMaxWidth(0.67f) + .aspectRatio(1f) + .rotate(20f) + .padding(start = 17.dp) + .align(Alignment.TopStart), + painter = painterResource(R.drawable.ic_santa_hat), + contentDescription = null, + ) } - btnAboutFragmentTelegram.setOnClickListener { - Const.TELEGRAM_GROUP.openBrowser(it.context) + } +} + +@Composable +private fun TextArea(modifier: Modifier = Modifier) { + val context = LocalContext.current + Column( + modifier = modifier + .padding(top = 12.dp) + .fillMaxWidth(1f), + horizontalAlignment = Alignment.CenterHorizontally + ) { + BadgedBox( + badge = { + Badge { + val badgeNumber = rememberSaveable { context.getAppVersionName() } + Text( + text = badgeNumber, + modifier = Modifier.semantics { contentDescription = badgeNumber } + ) + } + } + ) { + Text( + text = stringResource(id = R.string.app_name), + style = MaterialTheme.typography.titleLarge, + textAlign = TextAlign.Center + ) } - btnAboutFragmentDiscord.setOnClickListener { - Const.DISCORD_SERVER.openBrowser(it.context) + Card( + modifier = Modifier.padding(top = 16.dp), + shape = RoundedCornerShape(10) + ) { + Column( + modifier = Modifier.padding(20.dp), + horizontalAlignment = Alignment.CenterHorizontally + ) { + Text( + text = stringResource(id = R.string.app_short_description), + style = MaterialTheme.typography.bodyLarge, + textAlign = TextAlign.Center + ) + Text( + modifier = Modifier.padding(top = 12.dp), + text = stringResource(id = R.string.app_tech_stack_description), + style = MaterialTheme.typography.bodyMedium, + textAlign = TextAlign.Center + ) + } } + } +} - rvAboutFragment.layoutManager = GridLayoutManager( - requireContext(), - AniSpanSize.MAX_SPAN_SIZE - ).apply { - spanSizeLookup = AniSpanSize(adapter) +@Composable +private fun HelpArea( + openSponsorDialog: Boolean, + onTranslateClick: () -> Unit, + onSponsorDialogVisibleChange: (Boolean) -> Unit, +) { + Spacer(modifier = Modifier.height(16.dp)) + Row(modifier = Modifier.height(IntrinsicSize.Min)) { + Button( + onClick = onTranslateClick, + modifier = Modifier + .weight(1f) + .fillMaxHeight() + ) { + Icon(imageVector = Icons.Default.Translate, contentDescription = null) + Spacer(modifier = Modifier.width(6.dp)) + Text(text = stringResource(id = R.string.help_translate), textAlign = TextAlign.Center) + } + Spacer(modifier = Modifier.width(12.dp)) + Button( + onClick = { onSponsorDialogVisibleChange(true) }, + modifier = Modifier + .weight(1f) + .fillMaxHeight() + ) { + Icon(imageVector = Icons.Default.Coffee, contentDescription = null) + Spacer(modifier = Modifier.width(6.dp)) + Text(text = stringResource(id = R.string.sponsor), textAlign = TextAlign.Center) } - rvAboutFragment.addItemDecoration(AniVuItemDecoration(hItemSpace = 20.dp)) - rvAboutFragment.adapter = adapter } + SponsorDialog(visible = openSponsorDialog, onClose = { onSponsorDialogVisibleChange(false) }) +} + +@Composable +private fun SponsorDialog(visible: Boolean, onClose: () -> Unit) { + val context = LocalContext.current + AniVuDialog( + visible = visible, + onDismissRequest = onClose, + icon = { Icon(imageVector = Icons.Default.Coffee, contentDescription = null) }, + title = { Text(text = stringResource(id = R.string.sponsor)) }, + selectable = false, + text = { + Column(modifier = Modifier.verticalScroll(rememberScrollState())) { + Text(text = stringResource(id = R.string.sponsor_description)) + Spacer(modifier = Modifier.height(6.dp)) + ListItem( + modifier = Modifier.clickable { + Const.AFADIAN_LINK.openBrowser(context) + onClose() + }, + headlineContent = { Text(text = stringResource(R.string.sponsor_afadian)) }, + leadingContent = { + Icon(imageVector = Icons.Default.Lightbulb, contentDescription = null) + } + ) + HorizontalDivider() + ListItem( + modifier = Modifier.clickable { + Const.BUY_ME_A_COFFEE_LINK.openBrowser(context) + onClose() + }, + headlineContent = { Text(text = stringResource(R.string.sponsor_buy_me_a_coffee)) }, + leadingContent = { + Icon(imageVector = Icons.Default.Coffee, contentDescription = null) + } + ) + } + }, + confirmButton = { + TextButton(onClick = onClose) { + Text(text = stringResource(R.string.close)) + } + }, + ) +} - override fun FragmentAboutBinding.setWindowInsets() { - ablAboutFragment.addInsetsByPadding(top = true, left = true, right = true) - // Fix: https://github.com/material-components/material-components-android/issues/1310 - ViewCompat.setOnApplyWindowInsetsListener(ctlAboutFragment, null) - nsvAboutFragment.addInsetsByPadding(bottom = true, left = true, right = true) +@Composable +private fun ButtonArea() { + Row( + modifier = Modifier.padding(horizontal = 16.dp), + horizontalArrangement = Arrangement.Center + ) { + val boxModifier = Modifier.padding(vertical = 16.dp, horizontal = 6.dp) + val context = LocalContext.current + Box( + modifier = boxModifier.background( + color = MaterialTheme.colorScheme.primaryContainer, + shape = CurlyCornerShape( + amp = with(LocalDensity.current) { 1.dp.toPx() }, + count = 10 + ), + ), + contentAlignment = Alignment.Center + ) { + AniVuIconButton( + painter = painterResource(id = R.drawable.ic_github_24), + contentDescription = stringResource(id = R.string.about_screen_visit_github), + onClick = { Const.GITHUB_REPO.openBrowser(context) } + ) + } + Box( + modifier = boxModifier.background( + color = MaterialTheme.colorScheme.secondaryContainer, + shape = SquircleShape, + ), + contentAlignment = Alignment.Center + ) { + AniVuIconButton( + painter = painterResource(id = R.drawable.ic_telegram_24), + contentDescription = stringResource(id = R.string.about_screen_join_telegram), + onClick = { Const.TELEGRAM_GROUP.openBrowser(context) } + ) + } + Box( + modifier = boxModifier.background( + color = MaterialTheme.colorScheme.tertiaryContainer, + shape = CloverShape, + ), + contentAlignment = Alignment.Center + ) { + AniVuIconButton( + painter = painterResource(id = R.drawable.ic_discord_24), + contentDescription = stringResource(id = R.string.about_screen_join_discord), + onClick = { Const.DISCORD_SERVER.openBrowser(context) } + ) + } } +} - override fun getViewBinding(inflater: LayoutInflater, container: ViewGroup?) = - FragmentAboutBinding.inflate(inflater, container, false) +@Composable +private fun rememberOtherWorksList(): List { + val context = LocalContext.current + return remember { + listOf( + OtherWorksBean( + name = context.getString(R.string.about_screen_other_works_rays_name), + icon = R.drawable.ic_rays, + description = context.getString(R.string.about_screen_other_works_rays_description), + url = Const.RAYS_ANDROID_URL, + ), + OtherWorksBean( + name = context.getString(R.string.about_screen_other_works_raca_name), + icon = R.drawable.ic_raca, + description = context.getString(R.string.about_screen_other_works_raca_description), + url = Const.RACA_ANDROID_URL, + ), + OtherWorksBean( + name = context.getString(R.string.about_screen_other_works_night_screen_name), + icon = R.drawable.ic_night_screen, + description = context.getString(R.string.about_screen_other_works_night_screen_description), + url = Const.NIGHT_SCREEN_URL, + ), + ) + } +} + +@Composable +private fun OtherWorksItem( + modifier: Modifier = Modifier, + data: OtherWorksBean, +) { + val context = LocalContext.current + Card( + modifier = modifier + .padding(vertical = 10.dp) + .fillMaxWidth() + ) { + Column( + modifier = Modifier + .clickable { data.url.openBrowser(context) } + .padding(15.dp) + ) { + Row(verticalAlignment = Alignment.CenterVertically) { + AsyncImage( + modifier = Modifier + .size(30.dp) + .aspectRatio(1f), + model = data.icon, + contentDescription = data.name + ) + Spacer(modifier = Modifier.width(16.dp)) + Text( + text = data.name, + style = MaterialTheme.typography.titleLarge + ) + } + Text( + modifier = Modifier.padding(top = 6.dp), + text = data.description, + style = MaterialTheme.typography.bodyMedium + ) + } + } } \ No newline at end of file diff --git a/app/src/main/java/com/skyd/anivu/ui/fragment/about/update/UpdateDialog.kt b/app/src/main/java/com/skyd/anivu/ui/fragment/about/update/UpdateDialog.kt new file mode 100644 index 00000000..c891c63f --- /dev/null +++ b/app/src/main/java/com/skyd/anivu/ui/fragment/about/update/UpdateDialog.kt @@ -0,0 +1,238 @@ +package com.skyd.anivu.ui.fragment.about.update + +import android.text.Html +import android.text.method.LinkMovementMethod +import android.widget.TextView +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.rememberScrollState +import androidx.compose.foundation.selection.toggleable +import androidx.compose.foundation.text.selection.SelectionContainer +import androidx.compose.foundation.verticalScroll +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.Update +import androidx.compose.material3.Card +import androidx.compose.material3.CardDefaults +import androidx.compose.material3.Checkbox +import androidx.compose.material3.Icon +import androidx.compose.material3.LocalContentColor +import androidx.compose.material3.Text +import androidx.compose.material3.TextButton +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.getValue +import androidx.compose.runtime.rememberCoroutineScope +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.toArgb +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.semantics.Role +import androidx.compose.ui.unit.dp +import androidx.compose.ui.viewinterop.AndroidView +import androidx.hilt.navigation.compose.hiltViewModel +import androidx.lifecycle.compose.collectAsStateWithLifecycle +import com.skyd.anivu.R +import com.skyd.anivu.base.mvi.getDispatcher +import com.skyd.anivu.model.bean.UpdateBean +import com.skyd.anivu.model.preference.IgnoreUpdateVersionPreference +import com.skyd.anivu.ui.component.dialog.AniVuDialog +import com.skyd.anivu.ui.component.dialog.WaitingDialog +import com.skyd.anivu.ui.local.LocalIgnoreUpdateVersion +import okhttp3.internal.toLongOrDefault + + +@Composable +fun UpdateDialog( + silence: Boolean = false, + isRetry: Boolean = false, + onSuccess: () -> Unit = {}, + onClosed: () -> Unit = {}, + onError: (String) -> Unit = {}, + viewModel: UpdateViewModel = hiltViewModel() +) { + val uiState by viewModel.viewState.collectAsStateWithLifecycle() + val uiEvent by viewModel.singleEvent.collectAsStateWithLifecycle(initialValue = null) + + val dispatch = viewModel.getDispatcher(startWith = UpdateIntent.CheckUpdate(isRetry = false)) + + LaunchedEffect(Unit) { + if (isRetry) { + dispatch(UpdateIntent.CheckUpdate(isRetry = true)) + } + } + + WaitingDialog(visible = uiState.loadingDialog && !silence) + + when (val updateUiState = uiState.updateUiState) { + UpdateUiState.Init -> Unit + is UpdateUiState.OpenNewerDialog -> { + NewerDialog( + updateBean = updateUiState.data, + silence = silence, + onDismissRequest = { + onClosed() + dispatch(UpdateIntent.CloseDialog) + }, + onDownloadClick = { updateBean -> + dispatch( + UpdateIntent.Update(updateBean?.htmlUrl) + ) + } + ) + } + + UpdateUiState.OpenNoUpdateDialog -> { + NoUpdateDialog( + visible = !silence, + onDismissRequest = { + onClosed() + dispatch(UpdateIntent.CloseDialog) + } + ) + } + } + + when (val event = uiEvent) { + is UpdateEvent.CheckError -> LaunchedEffect(event) { + onError(event.msg) + } + + is UpdateEvent.CheckSuccess -> onSuccess() + null -> Unit + } +} + +@Composable +private fun NewerDialog( + updateBean: UpdateBean?, + silence: Boolean, + onDismissRequest: () -> Unit, + onDownloadClick: (UpdateBean?) -> Unit, +) { + val context = LocalContext.current + val ignoreUpdateVersion = LocalIgnoreUpdateVersion.current + val scope = rememberCoroutineScope() + + val visible = updateBean != null && + (!silence || ignoreUpdateVersion < updateBean.tagName.toLongOrDefault(0L)) + + if (!visible) { + onDismissRequest() + } + + AniVuDialog( + onDismissRequest = onDismissRequest, + visible = visible, + icon = { Icon(imageVector = Icons.Default.Update, contentDescription = null) }, + title = { Text(text = stringResource(R.string.update_newer)) }, + selectable = false, + text = { + Column { + Column( + modifier = Modifier + .weight(weight = 1f, fill = false) + .verticalScroll(rememberScrollState()) + ) { + SelectionContainer { + Text( + text = stringResource( + R.string.update_newer_text, + updateBean!!.name, + updateBean.publishedAt, + updateBean.assets.firstOrNull()?.downloadCount.toString(), + ) + ) + } + val textColor = LocalContentColor.current + AndroidView( + factory = { context -> + TextView(context).apply { + setTextColor(textColor.toArgb()) + setTextIsSelectable(true) + movementMethod = LinkMovementMethod.getInstance() + isSingleLine = false + text = Html.fromHtml(updateBean!!.body, Html.FROM_HTML_MODE_COMPACT) + } + } + ) + } + val checked = ignoreUpdateVersion == (updateBean!!.tagName.toLongOrNull() ?: 0L) + Spacer(modifier = Modifier.height(5.dp)) + Card(colors = CardDefaults.cardColors(containerColor = Color.Transparent)) { + Row( + Modifier + .fillMaxWidth() + .toggleable( + value = checked, + onValueChange = { + IgnoreUpdateVersionPreference.put( + context = context, + scope = scope, + value = if (it) { + onDismissRequest() + updateBean.tagName.toLongOrNull() ?: 0L + } else { + 0L + } + ) + }, + role = Role.Checkbox + ) + .padding(horizontal = 6.dp), + verticalAlignment = Alignment.CenterVertically + ) { + Checkbox( + checked = checked, + onCheckedChange = null, + ) + Text( + modifier = Modifier + .padding(start = 16.dp) + .padding(vertical = 6.dp), + text = stringResource(R.string.update_ignore), + ) + } + } + } + }, + confirmButton = { + TextButton(onClick = { onDownloadClick(updateBean) }) { + Text(text = stringResource(id = R.string.download_update)) + } + }, + dismissButton = { + TextButton(onClick = onDismissRequest) { + Text(text = stringResource(id = R.string.cancel)) + } + } + ) +} + +@Composable +private fun NoUpdateDialog( + visible: Boolean, + onDismissRequest: () -> Unit, +) { + if (!visible) { + onDismissRequest() + } + + AniVuDialog( + onDismissRequest = onDismissRequest, + visible = visible, + icon = { Icon(imageVector = Icons.Default.Update, contentDescription = null) }, + title = { Text(text = stringResource(R.string.update_check)) }, + text = { Text(text = stringResource(R.string.update_no_update)) }, + confirmButton = { + TextButton(onClick = onDismissRequest) { + Text(text = stringResource(id = R.string.ok)) + } + } + ) +} \ No newline at end of file diff --git a/app/src/main/java/com/skyd/anivu/ui/fragment/about/update/UpdateEvent.kt b/app/src/main/java/com/skyd/anivu/ui/fragment/about/update/UpdateEvent.kt new file mode 100644 index 00000000..bde3a369 --- /dev/null +++ b/app/src/main/java/com/skyd/anivu/ui/fragment/about/update/UpdateEvent.kt @@ -0,0 +1,14 @@ +package com.skyd.anivu.ui.fragment.about.update + +import com.skyd.anivu.base.mvi.MviSingleEvent +import kotlin.random.Random + +sealed interface UpdateEvent : MviSingleEvent { + data class CheckError( + val msg: String, + private val random: Long = Random.nextLong() + System.currentTimeMillis(), + ) : UpdateEvent + + data class CheckSuccess(private val random: Long = Random.nextLong() + System.currentTimeMillis()) : + UpdateEvent +} diff --git a/app/src/main/java/com/skyd/anivu/ui/fragment/about/update/UpdateIntent.kt b/app/src/main/java/com/skyd/anivu/ui/fragment/about/update/UpdateIntent.kt new file mode 100644 index 00000000..1a6e6051 --- /dev/null +++ b/app/src/main/java/com/skyd/anivu/ui/fragment/about/update/UpdateIntent.kt @@ -0,0 +1,9 @@ +package com.skyd.anivu.ui.fragment.about.update + +import com.skyd.anivu.base.mvi.MviIntent + +sealed interface UpdateIntent : MviIntent { + data object CloseDialog : UpdateIntent + data class CheckUpdate(val isRetry: Boolean) : UpdateIntent + data class Update(val url: String?) : UpdateIntent +} \ No newline at end of file diff --git a/app/src/main/java/com/skyd/anivu/ui/fragment/about/update/UpdatePartialStateChange.kt b/app/src/main/java/com/skyd/anivu/ui/fragment/about/update/UpdatePartialStateChange.kt new file mode 100644 index 00000000..dc4825b6 --- /dev/null +++ b/app/src/main/java/com/skyd/anivu/ui/fragment/about/update/UpdatePartialStateChange.kt @@ -0,0 +1,40 @@ +package com.skyd.anivu.ui.fragment.about.update + +import com.skyd.anivu.model.bean.UpdateBean + +internal sealed interface UpdatePartialStateChange { + fun reduce(oldState: UpdateState): UpdateState + + data class Error(val msg: String) : UpdatePartialStateChange { + override fun reduce(oldState: UpdateState) = oldState.copy(loadingDialog = false) + } + + data object LoadingDialog : UpdatePartialStateChange { + override fun reduce(oldState: UpdateState) = oldState.copy(loadingDialog = true) + } + + data object RequestUpdate : UpdatePartialStateChange { + override fun reduce(oldState: UpdateState): UpdateState = oldState.copy( + loadingDialog = false, + ) + } + + sealed interface CheckUpdate : UpdatePartialStateChange { + override fun reduce(oldState: UpdateState): UpdateState { + return when (this) { + is HasUpdate -> oldState.copy( + updateUiState = UpdateUiState.OpenNewerDialog(data), + loadingDialog = false, + ) + + NoUpdate -> oldState.copy( + updateUiState = UpdateUiState.OpenNoUpdateDialog, + loadingDialog = false, + ) + } + } + + data object NoUpdate : CheckUpdate + data class HasUpdate(val data: UpdateBean) : CheckUpdate + } +} diff --git a/app/src/main/java/com/skyd/anivu/ui/fragment/about/update/UpdateState.kt b/app/src/main/java/com/skyd/anivu/ui/fragment/about/update/UpdateState.kt new file mode 100644 index 00000000..2360b24d --- /dev/null +++ b/app/src/main/java/com/skyd/anivu/ui/fragment/about/update/UpdateState.kt @@ -0,0 +1,23 @@ +package com.skyd.anivu.ui.fragment.about.update + +import com.skyd.anivu.base.mvi.MviViewState +import com.skyd.anivu.model.bean.UpdateBean + + +data class UpdateState( + var updateUiState: UpdateUiState, + var loadingDialog: Boolean, +) : MviViewState { + companion object { + fun initial() = UpdateState( + updateUiState = UpdateUiState.Init, + loadingDialog = false, + ) + } +} + +sealed class UpdateUiState { + data class OpenNewerDialog(val data: UpdateBean) : UpdateUiState() + data object OpenNoUpdateDialog : UpdateUiState() + data object Init : UpdateUiState() +} \ No newline at end of file diff --git a/app/src/main/java/com/skyd/anivu/ui/fragment/about/update/UpdateViewModel.kt b/app/src/main/java/com/skyd/anivu/ui/fragment/about/update/UpdateViewModel.kt new file mode 100644 index 00000000..7ed8c1b1 --- /dev/null +++ b/app/src/main/java/com/skyd/anivu/ui/fragment/about/update/UpdateViewModel.kt @@ -0,0 +1,103 @@ +package com.skyd.anivu.ui.fragment.about.update + +import androidx.lifecycle.viewModelScope +import com.skyd.anivu.appContext +import com.skyd.anivu.base.mvi.AbstractMviViewModel +import com.skyd.anivu.config.Const +import com.skyd.anivu.ext.catchMap +import com.skyd.anivu.ext.getAppVersionCode +import com.skyd.anivu.ext.openBrowser +import com.skyd.anivu.ext.startWith +import com.skyd.anivu.ext.toDateTimeString +import com.skyd.anivu.model.repository.UpdateRepository +import dagger.hilt.android.lifecycle.HiltViewModel +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.SharedFlow +import kotlinx.coroutines.flow.SharingStarted +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.filter +import kotlinx.coroutines.flow.filterIsInstance +import kotlinx.coroutines.flow.filterNot +import kotlinx.coroutines.flow.flatMapConcat +import kotlinx.coroutines.flow.map +import kotlinx.coroutines.flow.merge +import kotlinx.coroutines.flow.onEach +import kotlinx.coroutines.flow.scan +import kotlinx.coroutines.flow.stateIn +import kotlinx.coroutines.flow.take +import okhttp3.internal.toLongOrDefault +import java.text.SimpleDateFormat +import java.util.Locale +import java.util.TimeZone +import javax.inject.Inject + +@HiltViewModel +class UpdateViewModel @Inject constructor(private var updateRepo: UpdateRepository) : + AbstractMviViewModel() { + + override val viewState: StateFlow + + init { + val initialVS = UpdateState.initial() + + viewState = merge( + intentSharedFlow.filter { it is UpdateIntent.CheckUpdate && !it.isRetry }.take(1), + intentSharedFlow.filter { it is UpdateIntent.CheckUpdate && it.isRetry }, + intentSharedFlow.filterNot { it is UpdateIntent.CheckUpdate } + ) + .shareWhileSubscribed() + .toUpdatePartialStateChangeFlow() + .debugLog("UpdatePartialStateChange") + .sendSingleEvent() + .scan(initialVS) { vs, change -> change.reduce(vs) } + .debugLog("ViewState") + .stateIn( + viewModelScope, + SharingStarted.Eagerly, + initialVS + ) + } + + private fun Flow.sendSingleEvent(): Flow { + return onEach { change -> + val event = when (change) { + is UpdatePartialStateChange.Error -> UpdateEvent.CheckError(change.msg) + is UpdatePartialStateChange.CheckUpdate.NoUpdate, + is UpdatePartialStateChange.CheckUpdate.HasUpdate -> UpdateEvent.CheckSuccess() + + else -> return@onEach + } + sendEvent(event) + } + } + + private fun SharedFlow.toUpdatePartialStateChangeFlow(): Flow { + return merge( + filterIsInstance().flatMapConcat { + updateRepo.checkUpdate().map { data -> + if (appContext.getAppVersionCode() < data.tagName.toLongOrDefault(0L)) { + val date = SimpleDateFormat( + "yyyy-MM-dd'T'HH:mm:ss'Z'", + Locale.getDefault() + ).apply { + timeZone = TimeZone.getTimeZone("UTC") + }.parse(data.publishedAt) + val publishedAt: String = date?.toDateTimeString() ?: data.publishedAt + + UpdatePartialStateChange.CheckUpdate.HasUpdate( + data.copy(publishedAt = publishedAt) + ) + } else { + UpdatePartialStateChange.CheckUpdate.NoUpdate + } + }.startWith(UpdatePartialStateChange.LoadingDialog) + .catchMap { UpdatePartialStateChange.Error(it.message.orEmpty()) } + }, + + filterIsInstance().map { intent -> + (intent.url ?: Const.GITHUB_REPO).openBrowser(appContext) + UpdatePartialStateChange.RequestUpdate + }, + ) + } +} \ No newline at end of file diff --git a/app/src/main/java/com/skyd/anivu/ui/fragment/license/LicenseFragment.kt b/app/src/main/java/com/skyd/anivu/ui/fragment/license/LicenseFragment.kt index 845881f9..df26e67f 100644 --- a/app/src/main/java/com/skyd/anivu/ui/fragment/license/LicenseFragment.kt +++ b/app/src/main/java/com/skyd/anivu/ui/fragment/license/LicenseFragment.kt @@ -145,6 +145,12 @@ private fun getLicenseList(): List { license = "Apache-2.0", link = "https://github.com/Kotlin/kotlinx.serialization", ), + + LicenseBean( + name = "MaterialKolor", + license = "MIT", + link = "https://github.com/jordond/MaterialKolor" + ), LicenseBean( name = "Retrofit", license = "Apache-2.0", diff --git a/app/src/main/java/com/skyd/anivu/ui/fragment/more/MoreFragment.kt b/app/src/main/java/com/skyd/anivu/ui/fragment/more/MoreFragment.kt index aa4071ce..228eeec4 100644 --- a/app/src/main/java/com/skyd/anivu/ui/fragment/more/MoreFragment.kt +++ b/app/src/main/java/com/skyd/anivu/ui/fragment/more/MoreFragment.kt @@ -1,95 +1,127 @@ package com.skyd.anivu.ui.fragment.more +import android.content.Context +import android.os.Bundle import android.view.LayoutInflater +import android.view.View import android.view.ViewGroup -import androidx.appcompat.content.res.AppCompatResources -import androidx.recyclerview.widget.GridLayoutManager -import com.google.android.material.color.MaterialColors +import androidx.compose.foundation.layout.PaddingValues +import androidx.compose.foundation.layout.WindowInsets +import androidx.compose.foundation.layout.WindowInsetsSides +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.only +import androidx.compose.foundation.layout.safeDrawing +import androidx.compose.material3.ColorScheme +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Scaffold +import androidx.compose.material3.SnackbarHost +import androidx.compose.material3.SnackbarHostState +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.remember +import androidx.compose.ui.Modifier +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.platform.LocalDensity +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.unit.Density +import androidx.compose.ui.unit.dp +import androidx.navigation.NavController import com.skyd.anivu.R -import com.skyd.anivu.base.BaseFragment -import com.skyd.anivu.databinding.FragmentMoreBinding -import com.skyd.anivu.ext.addInsetsByPadding -import com.skyd.anivu.ext.findMainNavController -import com.skyd.anivu.ext.screenIsLand +import com.skyd.anivu.base.BaseComposeFragment +import com.skyd.anivu.ext.isCompact +import com.skyd.anivu.ext.plus import com.skyd.anivu.model.bean.MoreBean -import com.skyd.anivu.ui.adapter.decoration.AniVuItemDecoration -import com.skyd.anivu.ui.adapter.variety.AniSpanSize -import com.skyd.anivu.ui.adapter.variety.VarietyAdapter -import com.skyd.anivu.ui.adapter.variety.proxy.More1Proxy +import com.skyd.anivu.ui.component.AniVuTopBar +import com.skyd.anivu.ui.component.AniVuTopBarStyle +import com.skyd.anivu.ui.component.lazyverticalgrid.AniVuLazyVerticalGrid +import com.skyd.anivu.ui.component.lazyverticalgrid.adapter.LazyGridAdapter +import com.skyd.anivu.ui.component.lazyverticalgrid.adapter.proxy.MoreProxy +import com.skyd.anivu.ui.component.shape.CloverShape +import com.skyd.anivu.ui.component.shape.CurlyCornerShape +import com.skyd.anivu.ui.local.LocalNavController +import com.skyd.anivu.ui.local.LocalWindowSizeClass import dagger.hilt.android.AndroidEntryPoint @AndroidEntryPoint -class MoreFragment : BaseFragment() { +class MoreFragment : BaseComposeFragment() { override val transitionProvider = nullTransitionProvider - override fun FragmentMoreBinding.initView() { - val adapter = VarietyAdapter(mutableListOf()).apply { - dataList = getMoreBeanList() - addProxy(More1Proxy(onClick = { - val data = dataList[it] - if (data is MoreBean) { - findMainNavController().navigate(data.navigateId) - } - })) - } + override fun onCreateView( + inflater: LayoutInflater, + container: ViewGroup?, + savedInstanceState: Bundle? + ): View = setContentBase { MoreScreen() } +} - rvMoreFragment.layoutManager = GridLayoutManager( - requireContext(), - AniSpanSize.MAX_SPAN_SIZE - ).apply { - spanSizeLookup = AniSpanSize(adapter) - } - rvMoreFragment.addItemDecoration(AniVuItemDecoration()) - rvMoreFragment.adapter = adapter - } +@Composable +fun MoreScreen() { + val navController = LocalNavController.current + val snackbarHostState = remember { SnackbarHostState() } + val context = LocalContext.current + val density = LocalDensity.current + val windowSizeClass = LocalWindowSizeClass.current - override fun FragmentMoreBinding.setWindowInsets() { - val isLand = requireContext().screenIsLand - ablMoreFragment.addInsetsByPadding(top = true, left = !isLand, right = true) - rvMoreFragment.addInsetsByPadding(left = !isLand, right = true) - } - - private fun getMoreBeanList(): MutableList { - return mutableListOf( - MoreBean( - title = getString(R.string.settings_fragment_name), - icon = AppCompatResources.getDrawable( - requireContext(), R.drawable.ic_settings_24 - )!!, - iconTint = MaterialColors.getColor( - requireView(), - com.google.android.material.R.attr.colorOnPrimary - ), - navigateId = R.id.action_to_settings_fragment, - background = AppCompatResources.getDrawable( - requireContext(), R.drawable.shape_curly_corner - )!!, - backgroundTint = MaterialColors.getColor( - requireView(), - com.google.android.material.R.attr.colorPrimary - ), - ), - MoreBean( - title = getString(R.string.about_fragment_name), - icon = AppCompatResources.getDrawable( - requireContext(), R.drawable.ic_info_24 - )!!, - iconTint = MaterialColors.getColor( - requireView(), - com.google.android.material.R.attr.colorOnSecondary - ), - navigateId = R.id.action_to_about_fragment, - background = AppCompatResources.getDrawable( - requireContext(), R.drawable.shape_clover - )!!, - backgroundTint = MaterialColors.getColor( - requireView(), - com.google.android.material.R.attr.colorSecondary - ), - ), + Scaffold( + snackbarHost = { SnackbarHost(hostState = snackbarHostState) }, + topBar = { + AniVuTopBar( + style = AniVuTopBarStyle.CenterAligned, + title = { Text(text = stringResource(id = R.string.more_screen_name)) }, + navigationIcon = {}, + windowInsets = WindowInsets.safeDrawing.only( + (WindowInsetsSides.Top + WindowInsetsSides.Right).run { + if (windowSizeClass.isCompact) plus(WindowInsetsSides.Left) else this + } + ) + ) + }, + contentWindowInsets = WindowInsets.safeDrawing.only( + (WindowInsetsSides.Top + WindowInsetsSides.Right).run { + if (windowSizeClass.isCompact) plus(WindowInsetsSides.Left) else this + } + ) + ) { + val colorScheme: ColorScheme = MaterialTheme.colorScheme + val adapter = remember { + LazyGridAdapter( + mutableListOf( + MoreProxy(onClickListener = { data -> data.action.invoke() }) + ) + ) + } + AniVuLazyVerticalGrid( + modifier = Modifier.fillMaxSize(), + dataList = remember(context, colorScheme, density, navController) { + getMoreBeanList(context, colorScheme, density, navController) + }, + adapter = adapter, + contentPadding = it + PaddingValues(vertical = 10.dp), ) } +} - override fun getViewBinding(inflater: LayoutInflater, container: ViewGroup?) = - FragmentMoreBinding.inflate(inflater, container, false) +private fun getMoreBeanList( + context: Context, + colorScheme: ColorScheme, + density: Density, + navController: NavController, +): MutableList { + return mutableListOf( + MoreBean( + title = context.getString(R.string.settings_fragment_name), + icon = R.drawable.ic_settings_24, + iconTint = colorScheme.onPrimary, + shape = CloverShape, + shapeColor = colorScheme.primary, + action = { navController.navigate(R.id.action_to_settings_fragment) }, + ), + MoreBean( + title = context.getString(R.string.about_screen_name), + icon = R.drawable.ic_info_24, + iconTint = colorScheme.onSecondary, + shape = CurlyCornerShape(amp = with(density) { 1.6.dp.toPx() }, count = 10), + shapeColor = colorScheme.secondary, + action = { navController.navigate(R.id.action_to_about_fragment) } + ), + ) } \ No newline at end of file diff --git a/app/src/main/java/com/skyd/anivu/ui/local/LocalValue.kt b/app/src/main/java/com/skyd/anivu/ui/local/LocalValue.kt index 6a2c106a..a954e8e0 100644 --- a/app/src/main/java/com/skyd/anivu/ui/local/LocalValue.kt +++ b/app/src/main/java/com/skyd/anivu/ui/local/LocalValue.kt @@ -3,6 +3,7 @@ package com.skyd.anivu.ui.local import androidx.compose.material3.windowsizeclass.WindowSizeClass import androidx.compose.runtime.compositionLocalOf import androidx.navigation.NavHostController +import com.skyd.anivu.model.preference.IgnoreUpdateVersionPreference import com.skyd.anivu.model.preference.appearance.DarkModePreference import com.skyd.anivu.model.preference.appearance.ThemePreference @@ -14,5 +15,9 @@ val LocalWindowSizeClass = compositionLocalOf { error("LocalWindowSizeClass not initialized!") } +// Theme val LocalTheme = compositionLocalOf { ThemePreference.default } val LocalDarkMode = compositionLocalOf { DarkModePreference.default } + +// Update +val LocalIgnoreUpdateVersion = compositionLocalOf { IgnoreUpdateVersionPreference.default } \ No newline at end of file diff --git a/app/src/main/java/com/skyd/anivu/ui/theme/SystemTonalPalettes.kt b/app/src/main/java/com/skyd/anivu/ui/theme/SystemTonalPalettes.kt index bc92c503..a764631b 100644 --- a/app/src/main/java/com/skyd/anivu/ui/theme/SystemTonalPalettes.kt +++ b/app/src/main/java/com/skyd/anivu/ui/theme/SystemTonalPalettes.kt @@ -23,7 +23,7 @@ private fun getColorFromTheme(context: Context, @ColorRes id: Int): Color { } @RequiresApi(Build.VERSION_CODES.S) -private fun primarySystem(context: Context, tone: Int = 50): Color = when (tone) { +fun primarySystem(context: Context, tone: Int = 50): Color = when (tone) { 0 -> getColorFromTheme(context, android.R.color.system_accent1_1000) 10 -> getColorFromTheme(context, android.R.color.system_accent1_900) 20 -> getColorFromTheme(context, android.R.color.system_accent1_800) diff --git a/app/src/main/java/com/skyd/anivu/ui/theme/Theme.kt b/app/src/main/java/com/skyd/anivu/ui/theme/Theme.kt index 6c874c03..5f57a1bd 100644 --- a/app/src/main/java/com/skyd/anivu/ui/theme/Theme.kt +++ b/app/src/main/java/com/skyd/anivu/ui/theme/Theme.kt @@ -83,20 +83,21 @@ private fun setSystemBarsColor(view: View, darkMode: Boolean) { @Composable fun extractAllColors(darkTheme: Boolean): Map { - return extractColors(darkTheme) + extractColorsFromWallpaper(darkTheme) + return extractColors(darkTheme) + extractDynamicColor(darkTheme) } @Composable fun extractColors(darkTheme: Boolean): Map { return ThemePreference.values.associateWith { rememberDynamicColorScheme( - ThemePreference.toSeedColor(LocalContext.current, it), isDark = darkTheme + seedColor = ThemePreference.toSeedColor(LocalContext.current, it), + isDark = darkTheme, ) }.toMutableMap() } @Composable -fun extractColorsFromWallpaper(darkTheme: Boolean): Map { +fun extractDynamicColor(darkTheme: Boolean): Map { val context = LocalContext.current val preset = mutableMapOf() @@ -104,27 +105,13 @@ fun extractColorsFromWallpaper(darkTheme: Boolean): Map { val colors = WallpaperManager.getInstance(context) .getWallpaperColors(WallpaperManager.FLAG_SYSTEM) val primary = colors?.primaryColor?.toArgb() - val secondary = colors?.secondaryColor?.toArgb() - val tertiary = colors?.tertiaryColor?.toArgb() if (primary != null) { - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) { - preset["WallpaperPrimary"] = rememberSystemDynamicColorScheme(isDark = darkTheme) + preset[ThemePreference.DYNAMIC] = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) { + rememberSystemDynamicColorScheme(isDark = darkTheme) } else { - preset["WallpaperPrimary"] = rememberDynamicColorScheme( - seedColor = Color(primary), isDark = darkTheme, - ) + rememberDynamicColorScheme(seedColor = Color(primary), isDark = darkTheme) } } - if (secondary != null) { - preset["WallpaperSecondary"] = rememberDynamicColorScheme( - seedColor = Color(secondary), isDark = darkTheme, - ) - } - if (tertiary != null) { - preset["WallpaperTertiary"] = rememberDynamicColorScheme( - seedColor = Color(tertiary), isDark = darkTheme, - ) - } } return preset } \ No newline at end of file diff --git a/app/src/main/res/drawable/ic_santa_hat.xml b/app/src/main/res/drawable/ic_santa_hat.xml new file mode 100644 index 00000000..0aaceb40 --- /dev/null +++ b/app/src/main/res/drawable/ic_santa_hat.xml @@ -0,0 +1,33 @@ + + + + + + + + + + + diff --git a/app/src/main/res/layout-land/fragment_about.xml b/app/src/main/res/layout-land/fragment_about.xml deleted file mode 100644 index b9d46098..00000000 --- a/app/src/main/res/layout-land/fragment_about.xml +++ /dev/null @@ -1,199 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/app/src/main/res/layout/fragment_about.xml b/app/src/main/res/layout/fragment_about.xml deleted file mode 100644 index 43ecc171..00000000 --- a/app/src/main/res/layout/fragment_about.xml +++ /dev/null @@ -1,174 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/app/src/main/res/layout/fragment_more.xml b/app/src/main/res/layout/fragment_more.xml deleted file mode 100644 index d1511894..00000000 --- a/app/src/main/res/layout/fragment_more.xml +++ /dev/null @@ -1,34 +0,0 @@ - - - - - - - - - - - \ No newline at end of file diff --git a/app/src/main/res/layout/item_more_1.xml b/app/src/main/res/layout/item_more_1.xml deleted file mode 100644 index 335e5fbe..00000000 --- a/app/src/main/res/layout/item_more_1.xml +++ /dev/null @@ -1,36 +0,0 @@ - - - - - - - - - - \ No newline at end of file diff --git a/app/src/main/res/layout/item_other_works_1.xml b/app/src/main/res/layout/item_other_works_1.xml deleted file mode 100644 index fb09484b..00000000 --- a/app/src/main/res/layout/item_other_works_1.xml +++ /dev/null @@ -1,46 +0,0 @@ - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/app/src/main/res/menu/menu_bottom_navigation.xml b/app/src/main/res/menu/menu_bottom_navigation.xml index e403fc3c..03ac9047 100644 --- a/app/src/main/res/menu/menu_bottom_navigation.xml +++ b/app/src/main/res/menu/menu_bottom_navigation.xml @@ -14,5 +14,5 @@ android:id="@+id/item_bottom_navigation_more" android:enabled="true" android:icon="@drawable/selector_bottom_navigation_more" - android:title="@string/more_fragment_name" /> + android:title="@string/more_screen_name" /> \ No newline at end of file diff --git a/app/src/main/res/navigation/nav_graph.xml b/app/src/main/res/navigation/nav_graph.xml index a75b8d72..3d3bb8ba 100644 --- a/app/src/main/res/navigation/nav_graph.xml +++ b/app/src/main/res/navigation/nav_graph.xml @@ -72,8 +72,7 @@ + android:label="@string/about_screen_name" /> diff --git a/app/src/main/res/navigation/nav_graph_bottom_navigation.xml b/app/src/main/res/navigation/nav_graph_bottom_navigation.xml index c10bd71f..c8bbd432 100644 --- a/app/src/main/res/navigation/nav_graph_bottom_navigation.xml +++ b/app/src/main/res/navigation/nav_graph_bottom_navigation.xml @@ -18,8 +18,7 @@ + android:label="@string/more_screen_name" /> 主页 订阅 媒体 - 更多 + 更多 添加 RSS 订阅链接 粘贴 @@ -46,16 +46,16 @@ 视频 还原屏幕 倍速播放中… - 关于 + 关于 一个集 RSS 番剧订阅与更新、比特洪流下载、视频播放为一体的工具。 - AniVu 使用 MVI 架构,完全采用 Material You 设计风格。所有页面均使用 Android View 开发。 - 访问 GitHub 仓库 - 加入 Telegram 群组 - 加入 Discord 服务器 - 其他作品 - 一个在本地记录、查找、管理表情包的工具。您还在为手机中的表情包太多,找不到想要的表情包而苦恼吗?使用这款工具将帮助您管理您存储的表情包,再也不因为找不到表情包而烦恼!\uD83D\uDE0B - 一个在本地记录、查找抽象段落/评论区小作文的工具。🤗 您还在为记不住小作文内容,面临前面、中间、后面都忘了的尴尬处境吗?使用这款工具将帮助您记录您所遇到的小作文,再也不因为忘记而烦恼!😋 - 当您在夜间🌙使用手机时,Night Screen 可以帮助您减少屏幕亮度,减少对眼睛的伤害。 + AniVu 使用 MVI 架构,完全采用 Material You 设计风格。使用 Android View 和 Compose 混合开发。 + 访问 GitHub 仓库 + 加入 Telegram 群组 + 加入 Discord 服务器 + 其他作品 + 一个在本地记录、查找、管理表情包的工具。您还在为手机中的表情包太多,找不到想要的表情包而苦恼吗?使用这款工具将帮助您管理您存储的表情包,再也不因为找不到表情包而烦恼!\uD83D\uDE0B + 一个在本地记录、查找抽象段落/评论区小作文的工具。🤗 您还在为记不住小作文内容,面临前面、中间、后面都忘了的尴尬处境吗?使用这款工具将帮助您记录您所遇到的小作文,再也不因为忘记而烦恼!😋 + 当您在夜间🌙使用手机时,Night Screen 可以帮助您减少屏幕亮度,减少对眼睛的伤害。 找不到浏览器!网址:%s 开源许可证 搜索 @@ -148,6 +148,17 @@ 跟随系统 动态主题 将壁纸颜色应用于主题 + 翻译 + 赞助 + 如果您喜欢我开发的软件,可以请我喝一杯咖啡,谢谢! + 爱发电 + 检查更新 + 当前已是最新版! + 发现新版本 + 新版本:%1$s\n发布时间:%2$s\n下载数:%3$s\n + 下载 + 忽略本次更新 + 检查更新失败:%s 每 %d 分钟 diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index aff3ebf3..a7c51554 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -3,7 +3,7 @@ Main Fragment Feed Media - More + More Add RSS URL Paste @@ -48,19 +48,19 @@ Video Reset Fast forwarding… - About + About An all-in-one tool for RSS anime subscription and updates, bit torrent downloads, and video playback. - AniVu utilizes the MVI architecture and fully adopts the Material You design style. All pages are developed using Android View. - GitHub repository - Telegram group - Discord server - Other works - Rays (Record All Your Stickers) - A tool to record, search and manage stickers on your phone. Are you still struggling with too many stickers on your phone and having trouble finding the ones you want? This tool will help you manage your stickers! \uD83D\uDE0B - Raca (Record All Classic Articles) - A tool to record and search abstract passages and mini-essays in the comments section locally. 🤗 Are you still having trouble remembering the content of your mini-essay and facing the embarrassing situation of forgetting the front, middle and back? Using this tool will help you record the mini-essays you come across and never worry about forgetting them again! 😋 - Night Screen - When you use your phone at night 🌙, Night Screen can help you reduce the brightness of the screen and reduce the damage to your eyes. + AniVu utilizes the MVI architecture and fully adopts the Material You design style. All pages are developed using Android View and Compose. + GitHub repository + Telegram group + Discord server + Other works + Rays (Record All Your Stickers) + A tool to record, search and manage stickers on your phone. Are you still struggling with too many stickers on your phone and having trouble finding the ones you want? This tool will help you manage your stickers! \uD83D\uDE0B + Raca (Record All Classic Articles) + A tool to record and search abstract passages and mini-essays in the comments section locally. 🤗 Are you still having trouble remembering the content of your mini-essay and facing the embarrassing situation of forgetting the front, middle and back? Using this tool will help you record the mini-essays you come across and never worry about forgetting them again! 😋 + Night Screen + When you use your phone at night 🌙, Night Screen can help you reduce the brightness of the screen and reduce the damage to your eyes. Can\'t find the browser! Link: %s License Search @@ -155,6 +155,18 @@ System Dynamic theme Apply wallpaper colors to the app + Translate + Sponsor + If you like the app, you can buy me a cup of coffee, thanks! + Afadian + Buy me a coffee + Check updates + Already the latest version! + New version + New version: %1$s\nPublished on: %2$s\nDownloads: %3$s\n + Download + Ignore this update + Check for update failed: %s Every %d minute Every %d minutes