diff --git a/CHANGELOG.md b/CHANGELOG.md
index fad58f52..8a6fc0af 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,4 +1,6 @@
### v2.1.1
+- Added crash report
+- Refactored with Compose
- Added Component Manager Settings
- Added Portuguese (Brazilian) translation
- Known issue fixes
diff --git a/app/build.gradle.kts b/app/build.gradle.kts
index 82ff51f1..d95077f8 100644
--- a/app/build.gradle.kts
+++ b/app/build.gradle.kts
@@ -50,6 +50,14 @@ android {
}
}
+ buildFeatures {
+ compose = true
+ }
+
+ composeOptions {
+ kotlinCompilerExtensionVersion = libs.versions.compose.kotlin.compiler.get()
+ }
+
signingConfigs {
if (keyprops.isEmpty) return@signingConfigs
create("release") {
@@ -83,7 +91,7 @@ android {
}
lint {
- disable += listOf("NotifyDataSetChanged")
+ disable += listOf("NotifyDataSetChanged", "UsingMaterialAndMaterial3Libraries")
fatal += listOf("NewApi", "InlineApi")
}
@@ -108,13 +116,28 @@ dependencies {
implementation(libs.androidx.window)
implementation(libs.google.material)
implementation(libs.androidx.startup)
+ implementation(libs.hilt.android)
+ kapt(libs.hilt.compiler)
+ implementation(libs.androidx.activity.compose)
+ implementation(libs.androidx.lifecycle.compose)
+ implementation(platform(libs.androidx.compose.bom))
+ implementation(libs.androidx.compose.ui)
+ implementation(libs.androidx.compose.ui.util)
+ implementation(libs.androidx.compose.material3)
+ implementation(libs.androidx.compose.material.icons)
+ implementation(libs.androidx.compose.constraintlayout)
+ implementation(libs.androidx.compose.ui.tooling.preview)
+ debugImplementation(libs.androidx.compose.ui.tooling)
+ implementation(libs.accompanist.drawablepainter)
+
+ implementation(libs.dionsegijn.konfetti)
implementation(libs.io.coil)
+ implementation(libs.io.coil.compose)
implementation(libs.free.reflection)
implementation(libs.viewbinding.delegate)
implementation(libs.blurhash.android)
- implementation(libs.hilt.android)
- kapt(libs.hilt.compiler)
debugImplementation(libs.squareup.leakcanary)
+
implementation(project(":basic"))
implementation(project(":eggs:U"))
implementation(project(":eggs:T"))
diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml
index 06097293..318b0c41 100644
--- a/app/src/main/AndroidManifest.xml
+++ b/app/src/main/AndroidManifest.xml
@@ -63,7 +63,7 @@
@@ -89,7 +89,8 @@
android:name="android.app.shortcuts"
android:resource="@xml/shortcuts" />
-
+
+
\ No newline at end of file
diff --git a/app/src/main/java/com/dede/android_eggs/main/holders/PreviewHolder.kt b/app/src/main/java/com/dede/android_eggs/main/AndroidPreviewHelp.kt
similarity index 55%
rename from app/src/main/java/com/dede/android_eggs/main/holders/PreviewHolder.kt
rename to app/src/main/java/com/dede/android_eggs/main/AndroidPreviewHelp.kt
index db2ce85e..d8311a79 100644
--- a/app/src/main/java/com/dede/android_eggs/main/holders/PreviewHolder.kt
+++ b/app/src/main/java/com/dede/android_eggs/main/AndroidPreviewHelp.kt
@@ -1,99 +1,27 @@
-package com.dede.android_eggs.main.holders
+package com.dede.android_eggs.main
-import android.annotation.SuppressLint
import android.content.Context
-import android.content.res.ColorStateList
-import android.graphics.Color
import android.graphics.ColorMatrix
import android.graphics.ColorMatrixColorFilter
import android.net.Uri
-import android.util.StateSet
import android.view.LayoutInflater
-import android.view.View
-import androidx.annotation.AttrRes
-import androidx.annotation.ColorInt
import androidx.annotation.DrawableRes
import androidx.annotation.StringRes
-import androidx.appcompat.view.ContextThemeWrapper
-import androidx.core.content.withStyledAttributes
import androidx.core.view.doOnPreDraw
import androidx.core.view.isVisible
import androidx.core.view.updatePadding
-import com.dede.android_eggs.R
import com.dede.android_eggs.databinding.DialogAndroidTimelineBinding
-import com.dede.android_eggs.main.entity.Egg
-import com.dede.android_eggs.ui.adapter.VHType
import com.dede.android_eggs.util.CustomTabsBrowser
import com.dede.android_eggs.util.ThemeUtils
-import com.dede.android_eggs.util.resolveColorStateList
-import com.google.android.material.color.MaterialColors
import com.google.android.material.dialog.MaterialAlertDialogBuilder
-import com.google.android.material.resources.MaterialAttributes
-import java.util.*
+import java.util.Calendar
import kotlin.math.roundToInt
-import com.google.android.material.R as M3R
-
-@VHType(viewType = Egg.VIEW_TYPE_PREVIEW)
-class PreviewHolder(view: View) : EggHolder(view) {
+class AndroidPreviewHelp {
companion object {
private const val TIMELINE_YEAR_UDC = 2023// android udc
}
- @Suppress("SameParameterValue")
- private fun createHarmonizeWithPrimaryColorStateList(
- context: Context, @ColorInt color: Int,
- ): ColorStateList {
- val stateSet = intArrayOf(android.R.attr.state_pressed)
- val defaultColor = MaterialColors.harmonizeWithPrimary(context, color)
-
- var pressedColor = context.resolveColorStateList(
- M3R.attr.materialCardViewFilledStyle, M3R.attr.cardBackgroundColor
- )?.getColorForState(stateSet, defaultColor) ?: defaultColor
- pressedColor = MaterialColors.harmonize(color, pressedColor)
-
- return ColorStateList(
- arrayOf(stateSet, StateSet.WILD_CARD),
- intArrayOf(pressedColor, defaultColor)
- )
- }
-
- @SuppressLint("RestrictedApi")
- private fun getLightTextColor(context: Context, @AttrRes textAppearanceAttrRes: Int): Int {
- // always use dark mode color
- val wrapper = ContextThemeWrapper(context, M3R.style.Theme_Material3_DynamicColors_Dark)
- val value = MaterialAttributes.resolve(wrapper, textAppearanceAttrRes)
- var color = Color.WHITE
- if (value != null) {
- wrapper.withStyledAttributes(
- value.resourceId,
- intArrayOf(android.R.attr.textColor)
- ) {
- color = getColor(0, color)
- }
- }
- return color
- }
-
- override fun onBindViewHolder(egg: Egg) {
- super.onBindViewHolder(egg)
- val colorStateList =
- createHarmonizeWithPrimaryColorStateList(context, 0xFF073042.toInt())
- val titleTextColor = getLightTextColor(context, M3R.attr.textAppearanceHeadlineSmall)
- val summaryTextColor = getLightTextColor(context, M3R.attr.textAppearanceBodyMedium)
- binding.tvTitle.setTextColor(titleTextColor)
- binding.tvSummary.setTextColor(summaryTextColor)
- binding.cardView.setCardBackgroundColor(colorStateList)
- binding.tvSummary.text = getTimelineMessage(context)
- binding.cardView.setOnClickListener {
- showTimelineDialog(
- context,
- com.android_u.egg.R.drawable.u_android14_patch_adaptive,
- R.string.nickname_android_u
- )
- }
- }
-
private fun getTimelineMessage(context: Context): CharSequence {
val calendar = Calendar.getInstance()
val year = calendar.get(Calendar.YEAR)
@@ -104,7 +32,7 @@ class PreviewHolder(view: View) : EggHolder(view) {
}
}
- private fun showTimelineDialog(
+ fun showTimelineDialog(
context: Context,
@DrawableRes iconResId: Int,
@StringRes titleRes: Int,
diff --git a/app/src/main/java/com/dede/android_eggs/main/EasterEggHelp.kt b/app/src/main/java/com/dede/android_eggs/main/EasterEggHelp.kt
new file mode 100644
index 00000000..3ae2a4c3
--- /dev/null
+++ b/app/src/main/java/com/dede/android_eggs/main/EasterEggHelp.kt
@@ -0,0 +1,156 @@
+package com.dede.android_eggs.main
+
+import android.content.Context
+import android.graphics.drawable.Drawable
+import android.os.Build
+import android.util.SparseArray
+import androidx.annotation.StringRes
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.platform.LocalInspectionMode
+import com.dede.android_eggs.R
+import com.dede.android_eggs.ui.drawables.AlterableAdaptiveIconDrawable
+import com.dede.android_eggs.views.main.EasterEggModules
+import com.dede.android_eggs.views.settings.prefs.IconShapePref
+import com.dede.basic.provider.EasterEgg
+import com.dede.basic.provider.EasterEggProvider
+import com.dede.basic.requireDrawable
+import dagger.Module
+
+
+object EasterEggHelp {
+
+ @Composable
+ fun previewEasterEggs(): List {
+ if (!LocalInspectionMode.current) {
+ throw IllegalStateException("Only call from Inspection Mode")
+ }
+ val module = EasterEggModules::class.java.getAnnotation(Module::class.java)
+ val baseEasterEggs = module.includes.map {
+ val instance = try {
+ it.java.getField("INSTANCE").get(null)
+ } catch (e: Exception) {
+ it.java.getConstructor().newInstance()
+ }
+ val provider = instance as EasterEggProvider
+ provider.provideEasterEgg()
+ }
+ return EasterEggModules.providePureEasterEggList(baseEasterEggs)
+ }
+
+ class VersionFormatter private constructor(
+ @StringRes val nicknameRes: Int,
+ vararg versionNames: CharSequence,
+ ) {
+
+ companion object {
+ fun create(apiLevel: IntRange, @StringRes nicknameRes: Int = -1): VersionFormatter {
+ return if (apiLevel.first == apiLevel.last) {
+ VersionFormatter(
+ nicknameRes,
+ getVersionNameByApiLevel(apiLevel.first)
+ )
+ } else {
+ VersionFormatter(
+ nicknameRes,
+ getVersionNameByApiLevel(apiLevel.first),
+ getVersionNameByApiLevel(apiLevel.last),
+ )
+ }
+ }
+ }
+
+ private val versionNames: Array = versionNames
+
+ fun format(context: Context): String {
+ val enDash = context.getString(R.string.char_en_dash)
+ val sb = StringBuilder()
+ versionNames.joinTo(sb, separator = enDash)
+ return if (nicknameRes != -1) {
+ val nickname = context.getString(nicknameRes)
+ context.getString(
+ R.string.android_version_nickname_format,
+ sb.toString(), nickname
+ )
+ } else {
+ context.getString(R.string.android_version_format, sb.toString())
+ }
+ }
+ }
+
+ class ApiLevelFormatter constructor(private val apiLevel: IntRange) {
+
+ companion object {
+ fun create(apiLevel: IntRange): ApiLevelFormatter {
+ return ApiLevelFormatter(apiLevel)
+ }
+ }
+
+ fun format(context: Context): String {
+ return if (apiLevel.first == apiLevel.last) {
+ context.getString(R.string.api_version_format, apiLevel.first.toString())
+ } else {
+ val enDash = context.getString(R.string.char_en_dash)
+ context.getString(R.string.api_version_format, apiLevel.join(enDash))
+ }
+ }
+ }
+
+ private fun IntRange.join(s: CharSequence): CharSequence {
+ return StringBuilder()
+ .append(first)
+ .append(s)
+ .append(last)
+ }
+
+ fun EasterEgg.getIcon(context: Context): Drawable {
+ if (supportAdaptiveIcon) {
+ val pathStr = IconShapePref.getMaskPath(context)
+ return AlterableAdaptiveIconDrawable(context, iconRes, pathStr)
+ }
+ return context.requireDrawable(iconRes)
+ }
+
+ fun getVersionNameByApiLevel(level: Int): String {
+ return apiLevelArrays[level]
+ ?: throw IllegalArgumentException("Illegal Api level: $level")
+ }
+
+ private val apiLevelArrays = SparseArray()
+
+ init {
+ apiLevelArrays[Build.VERSION_CODES.UPSIDE_DOWN_CAKE] = "14"
+ apiLevelArrays[Build.VERSION_CODES.TIRAMISU] = "13"
+ apiLevelArrays[Build.VERSION_CODES.S_V2] = "12L"
+ apiLevelArrays[Build.VERSION_CODES.S] = "12"
+ apiLevelArrays[Build.VERSION_CODES.R] = "11"
+ apiLevelArrays[Build.VERSION_CODES.Q] = "10"
+ apiLevelArrays[Build.VERSION_CODES.P] = "9"
+ apiLevelArrays[Build.VERSION_CODES.O_MR1] = "8.1"
+ apiLevelArrays[Build.VERSION_CODES.O] = "8.0"
+ apiLevelArrays[Build.VERSION_CODES.N_MR1] = "7.1"
+ apiLevelArrays[Build.VERSION_CODES.N] = "7.0"
+ apiLevelArrays[Build.VERSION_CODES.M] = "6.0"
+ apiLevelArrays[Build.VERSION_CODES.LOLLIPOP_MR1] = "5.1"
+ apiLevelArrays[Build.VERSION_CODES.LOLLIPOP] = "5.0"
+ apiLevelArrays[Build.VERSION_CODES.KITKAT_WATCH] = "4.4W"
+ apiLevelArrays[Build.VERSION_CODES.KITKAT] = "4.4"
+ apiLevelArrays[Build.VERSION_CODES.JELLY_BEAN_MR2] = "4.3"
+ apiLevelArrays[Build.VERSION_CODES.JELLY_BEAN_MR1] = "4.2"
+ apiLevelArrays[Build.VERSION_CODES.JELLY_BEAN] = "4.1"
+ apiLevelArrays[Build.VERSION_CODES.ICE_CREAM_SANDWICH_MR1] = "4.0.3"
+ apiLevelArrays[Build.VERSION_CODES.ICE_CREAM_SANDWICH] = "4.0"
+ apiLevelArrays[Build.VERSION_CODES.HONEYCOMB_MR2] = "3.2"
+ apiLevelArrays[Build.VERSION_CODES.HONEYCOMB_MR1] = "3.1"
+ apiLevelArrays[Build.VERSION_CODES.HONEYCOMB] = "3.0"
+ apiLevelArrays[Build.VERSION_CODES.GINGERBREAD_MR1] = "2.3.3"
+ apiLevelArrays[Build.VERSION_CODES.GINGERBREAD] = "2.3"
+ apiLevelArrays[Build.VERSION_CODES.FROYO] = "2.2"
+ apiLevelArrays[Build.VERSION_CODES.ECLAIR_MR1] = "2.1"
+ apiLevelArrays[Build.VERSION_CODES.ECLAIR] = "2.0"
+ apiLevelArrays[Build.VERSION_CODES.DONUT] = "1.6"
+ apiLevelArrays[Build.VERSION_CODES.CUPCAKE] = "1.5"
+ apiLevelArrays[Build.VERSION_CODES.BASE_1_1] = "1.1"
+ apiLevelArrays[Build.VERSION_CODES.BASE] = "1.0"
+ }
+
+}
diff --git a/app/src/main/java/com/dede/android_eggs/main/EasterEggsActivity.kt b/app/src/main/java/com/dede/android_eggs/main/EasterEggsActivity.kt
deleted file mode 100644
index f5b395b9..00000000
--- a/app/src/main/java/com/dede/android_eggs/main/EasterEggsActivity.kt
+++ /dev/null
@@ -1,47 +0,0 @@
-package com.dede.android_eggs.main
-
-import android.content.Intent
-import android.os.Bundle
-import androidx.appcompat.app.AppCompatActivity
-import by.kirich1409.viewbindingdelegate.viewBinding
-import com.dede.android_eggs.R
-import com.dede.android_eggs.databinding.ActivityEasterEggsBinding
-import com.dede.android_eggs.util.EdgeUtils
-import com.dede.android_eggs.util.ThemeUtils
-import com.dede.android_eggs.views.main.StartupPage
-import dagger.hilt.android.AndroidEntryPoint
-import dagger.hilt.android.scopes.ActivityScoped
-import javax.inject.Inject
-
-/**
- * Easter Egg Collection
- */
-@AndroidEntryPoint
-class EasterEggsActivity : AppCompatActivity(R.layout.activity_easter_eggs) {
-
- private val binding: ActivityEasterEggsBinding by viewBinding(ActivityEasterEggsBinding::bind)
-
- @Inject
- @ActivityScoped
- lateinit var schemeHandler: SchemeHandler
-
- override fun onCreate(savedInstanceState: Bundle?) {
- ThemeUtils.tryApplyOLEDTheme(this)
- EdgeUtils.applyEdge(window)
- super.onCreate(savedInstanceState)
-
- setSupportActionBar(binding.toolbar)
-
- BackPressedHandler(this).register()
-
- StartupPage.show(this)
-
- schemeHandler.handleIntent(intent)
- }
-
- override fun onNewIntent(intent: Intent?) {
- super.onNewIntent(intent)
- schemeHandler.handleIntent(intent)
- }
-
-}
diff --git a/app/src/main/java/com/dede/android_eggs/main/EggActionHelp.kt b/app/src/main/java/com/dede/android_eggs/main/EggActionHelp.kt
index b5a0b226..07ab9ea5 100644
--- a/app/src/main/java/com/dede/android_eggs/main/EggActionHelp.kt
+++ b/app/src/main/java/com/dede/android_eggs/main/EggActionHelp.kt
@@ -9,7 +9,12 @@ import android.content.Context
import android.content.Intent
import android.content.IntentFilter
import android.content.IntentSender
+import android.graphics.drawable.BitmapDrawable
import android.os.Build
+import android.view.Gravity
+import android.view.View
+import androidx.appcompat.view.menu.MenuPopupAccessor
+import androidx.appcompat.widget.PopupMenu
import androidx.core.app.PendingIntentCompat
import androidx.core.content.getSystemService
import androidx.core.content.pm.ShortcutInfoCompat
@@ -17,14 +22,18 @@ import androidx.core.content.pm.ShortcutManagerCompat
import androidx.core.graphics.drawable.IconCompat
import androidx.core.graphics.drawable.toBitmap
import com.dede.android_eggs.R
-import com.dede.android_eggs.main.entity.Egg
+import com.dede.android_eggs.main.EasterEggHelp.getIcon
import com.dede.android_eggs.ui.drawables.AlterableAdaptiveIconDrawable
import com.dede.android_eggs.util.SplitUtils
import com.dede.android_eggs.util.applyIf
import com.dede.android_eggs.util.toast
+import com.dede.android_eggs.views.main.EasterEggsActivity
import com.dede.basic.cancel
import com.dede.basic.delay
import com.dede.basic.dp
+import com.dede.basic.provider.EasterEgg
+import com.dede.basic.provider.EasterEggGroup
+import kotlin.math.roundToInt
object EggActionHelp {
@@ -34,8 +43,14 @@ object EggActionHelp {
private const val DOCUMENT_LAUNCH_MODE_INTO_EXISTING =
Intent.FLAG_ACTIVITY_NEW_DOCUMENT or Intent.FLAG_ACTIVITY_RETAIN_IN_RECENTS
- private fun createIntent(context: Context, egg: Egg, retainInRecents: Boolean = true): Intent? {
- val targetClass = egg.targetClass ?: return null
+ private fun createIntent(
+ context: Context,
+ targetClass: Class?,
+ retainInRecents: Boolean = true,
+ ): Intent? {
+ if (targetClass == null) {
+ return null
+ }
return Intent(Intent.ACTION_VIEW)
.setClass(context, targetClass)
.applyIf(retainInRecents) {
@@ -43,14 +58,10 @@ object EggActionHelp {
}
}
- fun isSupportedLaunch(egg: Egg): Boolean {
- return egg.targetClass != null
- }
-
- fun launchEgg(context: Context, egg: Egg) {
- val targetClass = egg.targetClass ?: return
+ fun launchEgg(context: Context, egg: EasterEgg) {
+ val targetClass = egg.provideEasterEgg() ?: return
val embedded = SplitUtils.isActivityEmbedded(context)
- val intent = createIntent(context, egg, !embedded)
+ val intent = createIntent(context, targetClass, !embedded)
?: throw IllegalArgumentException("Create Egg launcher intent == null")
val task: AppTask? = findTaskWithTrim(context, targetClass)
if (task != null) {
@@ -101,13 +112,13 @@ object EggActionHelp {
return targetTask
}
- fun isShortcutEnable(egg: Egg): Boolean {
- return egg.targetClass != null
+ fun isSupportShortcut(egg: EasterEgg): Boolean {
+ return egg.provideEasterEgg() != null
}
- fun addShortcut(context: Context, egg: Egg) {
- if (!isShortcutEnable(egg)) return
- val intent = createIntent(context, egg) ?: return
+ fun addShortcut(context: Context, egg: EasterEgg) {
+ if (!isSupportShortcut(egg)) return
+ val intent = createIntent(context, egg.provideEasterEgg()) ?: return
val icon = if (Build.VERSION.SDK_INT < Build.VERSION_CODES.O) {
val bitmap = AlterableAdaptiveIconDrawable(context, egg.iconRes)
@@ -120,7 +131,7 @@ object EggActionHelp {
val shortcut = ShortcutInfoCompat.Builder(context, key)
.setIcon(icon)
.setIntent(intent)
- .setShortLabel(context.getString(egg.eggNameRes))
+ .setShortLabel(context.getString(egg.nameRes))
.build()
val callback = PinShortcutReceiver.registerCallbackWithTimeout(context)
@@ -178,4 +189,50 @@ object EggActionHelp {
cancel(token)
}
}
+
+ fun showEggGroupMenu(
+ context: Context,
+ anchor: View,
+ eggGroup: EasterEggGroup,
+ onSelected: (index: Int) -> Unit,
+ onDismiss: (() -> Unit)? = null,
+ ) {
+ val popupMenu = PopupMenu(
+ context, anchor, Gravity.NO_GRAVITY,
+ 0,
+ R.style.Theme_EggGroup_PopupMenu_ListPopupWindow
+ )
+ popupMenu.setForceShowIcon(true)
+ for ((index, egg) in eggGroup.eggs.withIndex()) {
+ val menuTitle = EasterEggHelp.VersionFormatter.create(egg.apiLevel, egg.nicknameRes)
+ .format(context)
+ popupMenu.menu.add(0, egg.id, index, menuTitle).apply {
+ val drawable = egg.getIcon(context)
+ val drawH = drawable.intrinsicHeight
+ val drawW = drawable.intrinsicWidth
+ val width: Int = 28.dp// Use the width as the basis to align the text
+ val height: Int = (width / drawW.toFloat() * drawH).roundToInt()
+ icon = BitmapDrawable(
+ context.resources,
+ drawable.toBitmap(width, height)
+ )
+ }
+ }
+ popupMenu.setOnDismissListener {
+ onDismiss?.invoke()
+ }
+ MenuPopupAccessor.setApi23Transitions(popupMenu)
+ popupMenu.setOnMenuItemClickListener {
+ val index = eggGroup.eggs.indexOfFirst { egg ->
+ egg.id == it.itemId
+ }
+ if (index != -1) {
+ onSelected.invoke(index)
+ } else {
+ throw IllegalArgumentException("Menu id: ${it.itemId}, title: ${it.title}, child: ${eggGroup.eggs.joinToString()}")
+ }
+ return@setOnMenuItemClickListener true
+ }
+ popupMenu.show()
+ }
}
\ No newline at end of file
diff --git a/app/src/main/java/com/dede/android_eggs/main/EggListFragment.kt b/app/src/main/java/com/dede/android_eggs/main/EggListFragment.kt
deleted file mode 100644
index 8347c14f..00000000
--- a/app/src/main/java/com/dede/android_eggs/main/EggListFragment.kt
+++ /dev/null
@@ -1,142 +0,0 @@
-package com.dede.android_eggs.main
-
-import android.graphics.Rect
-import android.os.Bundle
-import android.view.View
-import androidx.core.view.WindowInsetsCompat
-import androidx.fragment.app.Fragment
-import androidx.recyclerview.widget.LinearLayoutManager
-import androidx.recyclerview.widget.RecyclerView
-import androidx.recyclerview.widget.RecyclerView.ItemDecoration
-import androidx.recyclerview.widget.RecyclerView.OnScrollListener
-import by.kirich1409.viewbindingdelegate.viewBinding
-import com.dede.android_eggs.R
-import com.dede.android_eggs.databinding.FragmentEasterEggListBinding
-import com.dede.android_eggs.util.EasterUtils
-import com.dede.android_eggs.util.EdgeUtils
-import com.dede.android_eggs.util.EdgeUtils.onApplyWindowEdge
-import com.dede.android_eggs.util.LocalEvent
-import com.dede.android_eggs.util.OrientationAngleSensor
-import com.dede.android_eggs.util.toast
-import com.dede.android_eggs.views.main.EggAdapterProvider
-import com.dede.android_eggs.views.settings.ActionBarMenuController
-import com.dede.android_eggs.views.settings.prefs.IconShapePref
-import com.dede.android_eggs.views.settings.prefs.IconVisualEffectsPref
-import com.dede.basic.dp
-import dagger.hilt.android.AndroidEntryPoint
-import dagger.hilt.android.scopes.FragmentScoped
-import javax.inject.Inject
-
-
-@AndroidEntryPoint
-class EggListFragment : Fragment(R.layout.fragment_easter_egg_list),
- OrientationAngleSensor.OnOrientationAnglesUpdate {
-
- private val binding: FragmentEasterEggListBinding by viewBinding(FragmentEasterEggListBinding::bind)
-
- private var isRecyclerViewIdle = true
- private var orientationAngleSensor: OrientationAngleSensor? = null
-
- @Inject
- @FragmentScoped
- lateinit var eggAdapterProvider: EggAdapterProvider
-
- override fun onCreate(savedInstanceState: Bundle?) {
- super.onCreate(savedInstanceState)
- handleOrientationAngleSensor(IconVisualEffectsPref.isEnable(requireContext()))
-
- ActionBarMenuController(requireActivity()).apply {
- onCreate(savedInstanceState)
- onSearchTextChangeListener = eggAdapterProvider
- }
-
- if (EasterUtils.isEaster()) {
- requireContext().toast(R.string.toast_easter)
- }
- }
-
- private fun handleOrientationAngleSensor(enable: Boolean) {
- val orientationAngleSensor = this.orientationAngleSensor
- if (enable && orientationAngleSensor == null) {
- this.orientationAngleSensor = OrientationAngleSensor(
- requireContext(), this, this
- )
- } else if (!enable && orientationAngleSensor != null) {
- updateOrientationAngles(0f, 0f, 0f)
- orientationAngleSensor.destroy()
- this.orientationAngleSensor = null
- }
- }
-
- override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
- super.onViewCreated(view, savedInstanceState)
- binding.recyclerView.adapter = eggAdapterProvider.adapter
- var last: ItemDecoration = EggListDivider(10.dp, 0, 0)
- binding.recyclerView.addItemDecoration(last)
- binding.recyclerView.onApplyWindowEdge(
- EdgeUtils.DEFAULT_EDGE_MASK or WindowInsetsCompat.Type.ime()
- ) {
- removeItemDecoration(last)
- last = EggListDivider(10.dp, 0, it.bottom)
- addItemDecoration(last)
- }
- binding.recyclerView.addOnScrollListener(object : OnScrollListener() {
- override fun onScrollStateChanged(recyclerView: RecyclerView, newState: Int) {
- isRecyclerViewIdle = newState == RecyclerView.SCROLL_STATE_IDLE
- }
- })
- LocalEvent.receiver(this).register(IconShapePref.ACTION_CHANGED) {
- binding.recyclerView.adapter?.notifyDataSetChanged()
- }
- LocalEvent.receiver(this).register(IconVisualEffectsPref.ACTION_CHANGED) {
- val enable = it.getBooleanExtra(IconVisualEffectsPref.EXTRA_VALUE, false)
- handleOrientationAngleSensor(enable)
- }
- }
-
- override fun updateOrientationAngles(zAngle: Float, xAngle: Float, yAngle: Float) {
- if (!isRecyclerViewIdle) return
- val manager = binding.recyclerView.layoutManager as LinearLayoutManager
- val first = manager.findFirstVisibleItemPosition()
- val last = manager.findLastVisibleItemPosition()
- for (i in first..last) {
- val holder = binding.recyclerView.findViewHolderForLayoutPosition(i) ?: continue
- if (holder is OrientationAngleSensor.OnOrientationAnglesUpdate) {
- holder.updateOrientationAngles(zAngle, xAngle, yAngle)
- }
- }
- }
-
- fun smoothScrollToEgg(eggId: Int) {
- val position = eggAdapterProvider.indexOfByEggId(eggId)
- if (position != RecyclerView.NO_POSITION) {
- binding.recyclerView.smoothScrollToPosition(position)
- }
- }
-
- class EggListDivider(
- private val divider: Int,
- private val topInset: Int,
- private val bottomInset: Int,
- ) : ItemDecoration() {
-
- override fun getItemOffsets(
- outRect: Rect,
- view: View,
- parent: RecyclerView,
- state: RecyclerView.State,
- ) {
- val position = parent.getChildAdapterPosition(view)
- if (position == 0) {
- outRect.top = divider + topInset
- }
- outRect.bottom = divider
-
- val adapter = parent.adapter ?: return
- if (position == adapter.itemCount - 1) {
- outRect.bottom = divider + bottomInset
- }
- }
-
- }
-}
\ No newline at end of file
diff --git a/app/src/main/java/com/dede/android_eggs/main/PlaceholderActivity.kt b/app/src/main/java/com/dede/android_eggs/main/PlaceholderActivity.kt
deleted file mode 100644
index 510ddc03..00000000
--- a/app/src/main/java/com/dede/android_eggs/main/PlaceholderActivity.kt
+++ /dev/null
@@ -1,77 +0,0 @@
-package com.dede.android_eggs.main
-
-import android.graphics.drawable.ColorDrawable
-import android.os.Bundle
-import android.view.Gravity
-import android.widget.FrameLayout
-import android.widget.ImageView
-import androidx.appcompat.app.AppCompatActivity
-import com.dede.android_eggs.R
-import com.dede.android_eggs.ui.drawables.AlterableAdaptiveIconDrawable
-import com.dede.android_eggs.util.EdgeUtils
-import com.dede.android_eggs.util.LocalEvent
-import com.dede.android_eggs.util.ThemeUtils
-import com.dede.android_eggs.util.resolveColor
-import com.dede.android_eggs.views.settings.prefs.NightModePref.Companion.ACTION_NIGHT_MODE_CHANGED
-import com.dede.basic.dp
-import dagger.hilt.android.AndroidEntryPoint
-import javax.inject.Inject
-import kotlin.random.Random
-import com.google.android.material.R as M3R
-
-/**
- * Split Placeholder
- *
- * @author shhu
- * @since 2023/5/22
- */
-@AndroidEntryPoint
-class PlaceholderActivity : AppCompatActivity() {
-
- @Inject
- lateinit var iconRes: IntArray
-
- override fun onCreate(savedInstanceState: Bundle?) {
- ThemeUtils.tryApplyOLEDTheme(this)
- EdgeUtils.applyEdge(window)
- super.onCreate(savedInstanceState)
-
- val drawable = AlterableAdaptiveIconDrawable(this, randomRes(), randomPath())
- val imageView = ImageView(this).apply {
- setImageDrawable(drawable)
- scaleX = 0.5f
- scaleY = 0.5f
- alpha = 0f
- animate()
- .scaleX(1f)
- .scaleY(1f)
- .alpha(1f)
- .setDuration(300L)
- .start()
- }
- val params = FrameLayout.LayoutParams(56.dp, 56.dp).apply {
- gravity = Gravity.CENTER
- }
- setContentView(imageView, params)
-
- window.setBackgroundDrawable(ColorDrawable(resolveColor(M3R.attr.colorSurface)))
-
- LocalEvent.receiver(this)
- .register(ACTION_NIGHT_MODE_CHANGED) {
- recreate()
- }
- }
-
- private fun randomRes(): Int {
- val array = iconRes
- val index = Random.nextInt(array.size)
- return array[index]
- }
-
- private fun randomPath(): String {
- val array = resources.getStringArray(R.array.icon_shape_override_paths)
- // 排除第一个
- val index = Random.nextInt(array.size - 1) + 1
- return array[index]
- }
-}
\ No newline at end of file
diff --git a/app/src/main/java/com/dede/android_eggs/main/entity/Egg.kt b/app/src/main/java/com/dede/android_eggs/main/entity/Egg.kt
deleted file mode 100644
index da80bcbd..00000000
--- a/app/src/main/java/com/dede/android_eggs/main/entity/Egg.kt
+++ /dev/null
@@ -1,201 +0,0 @@
-package com.dede.android_eggs.main.entity
-
-import android.app.Activity
-import android.content.Context
-import android.graphics.Typeface
-import android.graphics.drawable.Drawable
-import android.os.Build
-import android.text.SpannableStringBuilder
-import android.text.style.StyleSpan
-import android.util.SparseArray
-import androidx.annotation.DrawableRes
-import androidx.annotation.StringRes
-import com.dede.android_eggs.R
-import com.dede.android_eggs.ui.adapter.VType
-import com.dede.android_eggs.ui.drawables.AlterableAdaptiveIconDrawable
-import com.dede.android_eggs.util.append
-import com.dede.android_eggs.views.settings.prefs.IconShapePref
-import com.dede.basic.provider.BaseEasterEgg
-import com.dede.basic.provider.EasterEgg
-import com.dede.basic.provider.EasterEggGroup
-import com.dede.basic.requireDrawable
-
-
-data class Egg(val easterEgg: EasterEgg) : VType {
-
- class VersionFormatter(
- @StringRes val nicknameRes: Int,
- vararg versionNames: CharSequence,
- ) {
-
- companion object {
- fun create(@StringRes nicknameRes: Int, apiLevel: IntRange): VersionFormatter {
- return if (apiLevel.first == apiLevel.last) {
- VersionFormatter(
- nicknameRes,
- getVersionNameByApiLevel(apiLevel.first)
- )
- } else {
- VersionFormatter(
- nicknameRes,
- getVersionNameByApiLevel(apiLevel.first),
- getVersionNameByApiLevel(apiLevel.last),
- )
- }
- }
- }
-
- private val versionNames: Array = versionNames
-
- fun format(context: Context): CharSequence {
- val enDash = context.getString(R.string.char_en_dash)
- val nickname = context.getString(nicknameRes)
- val sb = StringBuilder()
- versionNames.joinTo(sb, separator = enDash)
- return context.getString(
- R.string.android_version_nickname_format,
- sb.toString(), nickname
- )
- }
- }
-
- class ApiVersionFormatter(
- private val apiRange: IntRange,
- private val versionNameStart: CharSequence,
- private val versionNameLast: CharSequence,
- ) {
-
- companion object {
- fun create(apiLevel: IntRange): ApiVersionFormatter {
- return ApiVersionFormatter(
- apiLevel,
- getVersionNameByApiLevel(apiLevel.first),
- getVersionNameByApiLevel(apiLevel.last),
- )
- }
- }
-
- fun format(context: Context): CharSequence {
- val span = SpannableStringBuilder()
- .append(context.getString(R.string.android_version_format, versionNameStart))
- val italic = StyleSpan(Typeface.ITALIC)
- if (apiRange.first == apiRange.last) {
- span.append("\n")
- .append(
- context.getString(R.string.api_version_format, apiRange.first.toString()),
- italic
- )
- } else {
- val enDash = context.getString(R.string.char_en_dash)
- span.append(enDash)
- .append(versionNameLast)
- .append("\n")
- .append(
- context.getString(R.string.api_version_format, apiRange.join(enDash)),
- italic,
- )
- }
- return span
- }
-
- private fun IntRange.join(s: CharSequence): CharSequence {
- return StringBuilder()
- .append(first)
- .append(s)
- .append(last)
- }
- }
-
- companion object {
- const val VIEW_TYPE_EGG = 0
- const val VIEW_TYPE_WAVY = -1
- const val VIEW_TYPE_PREVIEW = 1
- const val VIEW_TYPE_EGG_GROUP = 2
-
- private val apiLevelArrays = SparseArray()
-
- fun getVersionNameByApiLevel(level: Int): String {
- return apiLevelArrays[level]
- ?: throw IllegalArgumentException("Illegal Api level: $level")
- }
-
- init {
- apiLevelArrays[Build.VERSION_CODES.UPSIDE_DOWN_CAKE] = "14"
- apiLevelArrays[Build.VERSION_CODES.TIRAMISU] = "13"
- apiLevelArrays[Build.VERSION_CODES.S_V2] = "12L"
- apiLevelArrays[Build.VERSION_CODES.S] = "12"
- apiLevelArrays[Build.VERSION_CODES.R] = "11"
- apiLevelArrays[Build.VERSION_CODES.Q] = "10"
- apiLevelArrays[Build.VERSION_CODES.P] = "9"
- apiLevelArrays[Build.VERSION_CODES.O_MR1] = "8.1"
- apiLevelArrays[Build.VERSION_CODES.O] = "8.0"
- apiLevelArrays[Build.VERSION_CODES.N_MR1] = "7.1"
- apiLevelArrays[Build.VERSION_CODES.N] = "7.0"
- apiLevelArrays[Build.VERSION_CODES.M] = "6.0"
- apiLevelArrays[Build.VERSION_CODES.LOLLIPOP_MR1] = "5.1"
- apiLevelArrays[Build.VERSION_CODES.LOLLIPOP] = "5.0"
- apiLevelArrays[Build.VERSION_CODES.KITKAT_WATCH] = "4.4W"
- apiLevelArrays[Build.VERSION_CODES.KITKAT] = "4.4"
- apiLevelArrays[Build.VERSION_CODES.JELLY_BEAN_MR2] = "4.3"
- apiLevelArrays[Build.VERSION_CODES.JELLY_BEAN_MR1] = "4.2"
- apiLevelArrays[Build.VERSION_CODES.JELLY_BEAN] = "4.1"
- apiLevelArrays[Build.VERSION_CODES.ICE_CREAM_SANDWICH_MR1] = "4.0.3"
- apiLevelArrays[Build.VERSION_CODES.ICE_CREAM_SANDWICH] = "4.0"
- apiLevelArrays[Build.VERSION_CODES.HONEYCOMB_MR2] = "3.2"
- apiLevelArrays[Build.VERSION_CODES.HONEYCOMB_MR1] = "3.1"
- apiLevelArrays[Build.VERSION_CODES.HONEYCOMB] = "3.0"
- apiLevelArrays[Build.VERSION_CODES.GINGERBREAD_MR1] = "2.3.3"
- apiLevelArrays[Build.VERSION_CODES.GINGERBREAD] = "2.3"
- apiLevelArrays[Build.VERSION_CODES.FROYO] = "2.2"
- apiLevelArrays[Build.VERSION_CODES.ECLAIR_MR1] = "2.1"
- apiLevelArrays[Build.VERSION_CODES.ECLAIR] = "2.0"
- apiLevelArrays[Build.VERSION_CODES.DONUT] = "1.6"
- apiLevelArrays[Build.VERSION_CODES.CUPCAKE] = "1.5"
- apiLevelArrays[Build.VERSION_CODES.BASE_1_1] = "1.1"
- apiLevelArrays[Build.VERSION_CODES.BASE] = "1.0"
- }
-
- fun Egg.getIcon(context: Context): Drawable {
- if (supportAdaptiveIcon) {
- val pathStr = IconShapePref.getMaskPath(context)
- return AlterableAdaptiveIconDrawable(context, iconRes, pathStr)
- }
- return context.requireDrawable(iconRes)
- }
-
- fun EasterEgg.toEgg(): Egg {
- return Egg(this)
- }
-
- fun BaseEasterEgg.toVTypeEgg(): VType {
- return when (this) {
- is EasterEgg -> toEgg()
- is EasterEggGroup -> EggGroup(eggs.map { it.toEgg() })
- else -> throw UnsupportedOperationException()
- }
- }
- }
-
- val id: Int get() = easterEgg.id
- val iconRes: Int @DrawableRes get() = easterEgg.iconRes
- val eggNameRes: Int @StringRes get() = easterEgg.nameRes
- val supportAdaptiveIcon: Boolean get() = easterEgg.supportAdaptiveIcon
- val targetClass: Class? get() = easterEgg.provideEasterEgg()
-
- val versionFormatter = VersionFormatter.create(easterEgg.nicknameRes, easterEgg.apiLevel)
-
- val apiVersionFormatter = ApiVersionFormatter.create(easterEgg.apiLevel)
-
- override val viewType: Int = VIEW_TYPE_EGG
-}
-
-class EggGroup(val child: List, var selectedIndex: Int = 0) : VType {
-
- val selectedEgg: Egg get() = child[selectedIndex]
-
- override val viewType: Int = Egg.VIEW_TYPE_EGG_GROUP
-}
-
-class Wavy(val wavyRes: Int, val repeat: Boolean = false) : VType {
- override val viewType: Int = Egg.VIEW_TYPE_WAVY
-}
diff --git a/app/src/main/java/com/dede/android_eggs/main/entity/EggFilter.kt b/app/src/main/java/com/dede/android_eggs/main/entity/EggFilter.kt
deleted file mode 100644
index 77c23f68..00000000
--- a/app/src/main/java/com/dede/android_eggs/main/entity/EggFilter.kt
+++ /dev/null
@@ -1,87 +0,0 @@
-package com.dede.android_eggs.main.entity
-
-import android.content.Context
-import android.widget.Filter
-import com.dede.android_eggs.R
-import com.dede.android_eggs.main.entity.Egg.Companion.toVTypeEgg
-import com.dede.android_eggs.ui.adapter.VType
-import com.dede.basic.provider.BaseEasterEgg
-import com.dede.basic.provider.EasterEggGroup
-import dagger.hilt.android.qualifiers.ActivityContext
-import java.util.Locale
-import javax.inject.Inject
-
-/**
- * Egg Search Filter
- *
- * @author shhu
- * @since 2023/8/9
- */
-class EggFilter @Inject constructor(
- @ActivityContext private val context: Context,
- easterEggs: List<@JvmSuppressWildcards BaseEasterEgg>
-) : Filter() {
-
- private fun convertEggList(easterEggs: List): List {
- val list = ArrayList()
- list.add(Wavy(R.drawable.ic_wavy_line))
- for (easterEgg in easterEggs) {
- list.add(easterEgg.toVTypeEgg())
- }
- list.add(Wavy(R.drawable.ic_wavy_line))
- return list
- }
-
- private val allEggList: List = convertEggList(easterEggs)
-
- private val allSearchEggList: List = allEggList.let {
- val newList = ArrayList()
- for (vType in it) {
- if (vType is EggGroup) {
- newList.addAll(vType.child)
- } else {
- newList.add(vType)
- }
- }
- return@let newList
- }
-
- val eggList: MutableList = ArrayList(allEggList)
-
- var onFilterResults: OnFilterResults? = null
-
- interface OnFilterResults {
- fun publishResults(constraint: CharSequence, newList: List)
- }
-
- override fun performFiltering(constraint: CharSequence): FilterResults {
- val resultList = ArrayList()
- if (constraint.isEmpty()) {
- resultList.addAll(allEggList)
- } else {
- for (vType in allSearchEggList) {
- if (vType !is Egg) continue
- val filter = constraint.toString().lowercase(Locale.ROOT)
- if (context.getString(vType.eggNameRes)
- .lowercase(Locale.ROOT)
- .contains(filter) ||
- vType.versionFormatter.format(context).toString()
- .lowercase(Locale.ROOT)
- .contains(filter)
- ) {
- resultList.add(vType)
- }
- }
- }
- return FilterResults().apply { values = resultList }
- }
-
- override fun publishResults(constraint: CharSequence, filterResults: FilterResults) {
- @Suppress("UNCHECKED_CAST")
- val newList = filterResults.values as Collection
- this.eggList.clear()
- this.eggList.addAll(newList)
- onFilterResults?.publishResults(constraint, this.eggList)
- }
-
-}
\ No newline at end of file
diff --git a/app/src/main/java/com/dede/android_eggs/main/entity/TimelineEvent.kt b/app/src/main/java/com/dede/android_eggs/main/entity/TimelineEvent.kt
index 6a198b98..b89ca042 100644
--- a/app/src/main/java/com/dede/android_eggs/main/entity/TimelineEvent.kt
+++ b/app/src/main/java/com/dede/android_eggs/main/entity/TimelineEvent.kt
@@ -7,21 +7,32 @@ import android.text.Spannable
import android.text.SpannableStringBuilder
import android.text.style.StyleSpan
import androidx.annotation.DrawableRes
+import androidx.appcompat.app.AppCompatDelegate
import com.dede.android_eggs.R
import com.dede.android_eggs.views.settings.prefs.LanguagePref
import java.text.Format
import java.util.Calendar
+import java.util.Locale
import java.util.TimeZone
data class TimelineEvent(
val year: String?,
val month: String?,
@DrawableRes val logoRes: Int,
- private val event: CharSequence,
+ val event: CharSequence,
) {
companion object {
+ private fun getApplicationLocale(): Locale {
+ val locales = AppCompatDelegate.getApplicationLocales()
+ return if (locales.isEmpty) {
+ Locale.getDefault()
+ } else {
+ locales.get(0) ?: Locale.getDefault()
+ }
+ }
+
private fun timelineEvent(@DrawableRes logoRes: Int, event: CharSequence): TimelineEvent {
val regex =
Regex("(January|February|March|April|May|June|July|August|September|October|November|December) +(\\d{4,})")
@@ -196,7 +207,7 @@ data class TimelineEvent(
val yearNum = year?.toIntOrNull() ?: return year
val calendar = Calendar.getInstance(TimeZone.getDefault())
calendar.set(Calendar.YEAR, yearNum)
- val locale = LanguagePref.getApplicationLocale()
+ val locale = getApplicationLocale()
val format: Format = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
SimpleDateFormat("yyyy", locale)
} else {
@@ -224,7 +235,7 @@ data class TimelineEvent(
}
val calendar = Calendar.getInstance(TimeZone.getDefault())
calendar.set(Calendar.MONTH, m)
- val locale = LanguagePref.getApplicationLocale()
+ val locale = getApplicationLocale()
val format: Format = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
SimpleDateFormat("MMMM", locale)
} else {
diff --git a/app/src/main/java/com/dede/android_eggs/main/holders/EggHolder.kt b/app/src/main/java/com/dede/android_eggs/main/holders/EggHolder.kt
deleted file mode 100644
index f9b65e37..00000000
--- a/app/src/main/java/com/dede/android_eggs/main/holders/EggHolder.kt
+++ /dev/null
@@ -1,186 +0,0 @@
-package com.dede.android_eggs.main.holders
-
-import android.animation.Animator
-import android.animation.ValueAnimator
-import android.graphics.*
-import android.graphics.drawable.StateListDrawable
-import android.util.StateSet
-import android.view.Gravity
-import android.view.HapticFeedbackConstants
-import android.view.View
-import android.view.animation.LinearInterpolator
-import androidx.core.view.GravityCompat
-import coil.dispose
-import coil.load
-import com.dede.android_eggs.databinding.ItemEasterEggLayoutBinding
-import com.dede.android_eggs.main.EggActionHelp
-import com.dede.android_eggs.main.entity.Egg
-import com.dede.android_eggs.main.entity.Egg.Companion.getIcon
-import com.dede.android_eggs.ui.Icons
-import com.dede.android_eggs.ui.adapter.VHType
-import com.dede.android_eggs.ui.adapter.VHolder
-import com.dede.android_eggs.ui.drawables.AlterableAdaptiveIconDrawable
-import com.dede.android_eggs.ui.drawables.FontIconsDrawable
-import com.dede.android_eggs.ui.views.HorizontalSwipeLayout
-import com.dede.android_eggs.util.OrientationAngleSensor
-import com.dede.android_eggs.util.isRtl
-import com.dede.android_eggs.util.resolveColorStateList
-import com.dede.android_eggs.util.updateCompoundDrawablesRelative
-import com.dede.basic.dp
-import com.dede.basic.dpf
-import kotlin.math.abs
-import kotlin.math.max
-import kotlin.math.min
-import com.google.android.material.R as M3R
-
-@VHType(viewType = Egg.VIEW_TYPE_EGG)
-open class EggHolder(view: View) : VHolder(view),
- OrientationAngleSensor.OnOrientationAnglesUpdate {
-
- val binding: ItemEasterEggLayoutBinding = ItemEasterEggLayoutBinding.bind(view)
-
- private val matrix = Matrix()
- private var lastXDegrees: Float = 0f
- private var lastYDegrees: Float = 0f
- private var animator: Animator? = null
- private val interpolator = LinearInterpolator()
-
- private fun Float.toRoundDegrees(): Float {
- return ((Math.toDegrees(toDouble())) % 90f).toFloat()
- }
-
- private fun calculateAnimDegrees(old: Float, new: Float, fraction: Float): Float {
- return old + (new - old) * fraction
- }
-
- override fun updateOrientationAngles(zAngle: Float, xAngle: Float, yAngle: Float) {
- val iconDrawable = binding.ivIcon.drawable as? AlterableAdaptiveIconDrawable ?: return
- if (!iconDrawable.isAdaptiveIconDrawable) return
-
- val xDegrees = xAngle.toRoundDegrees()// 俯仰角
- val yDegrees = yAngle.toRoundDegrees()// 侧倾角
- if (max(abs(lastXDegrees - xDegrees), abs(lastYDegrees - yDegrees)) < 5f) return
-
- val bounds = iconDrawable.bounds
- val width = bounds.width() / 4f
- val height = bounds.height() / 4f
-
- animator?.cancel()
- val saveXDegrees = lastXDegrees
- val saveYDegrees = lastYDegrees
- animator = ValueAnimator.ofFloat(0f, 1f)
- .setDuration(100)
- .apply {
- interpolator = this@EggHolder.interpolator
- addUpdateListener {
- val fraction = it.animatedFraction
- val cXDegrees = calculateAnimDegrees(saveXDegrees, xDegrees, fraction)
- val cYDegrees = calculateAnimDegrees(saveYDegrees, yDegrees, fraction)
- val dx = cYDegrees / 90f * width * -1f
- val dy = cXDegrees / 90f * height
- matrix.setTranslate(dx, dy)
- iconDrawable.setForegroundMatrix(matrix)
- }
- start()
- }
- lastYDegrees = yDegrees
- lastXDegrees = xDegrees
- }
-
- @Suppress("PARAMETER_NAME_CHANGED_ON_OVERRIDE")
- override fun onBindViewHolder(egg: Egg) {
- binding.tvTitle.setText(egg.eggNameRes)
- binding.tvSummary.text = egg.versionFormatter.format(context)
- binding.cardView.setOnClickListener { EggActionHelp.launchEgg(context, egg) }
- binding.background.tvBgMessage.text = egg.apiVersionFormatter.format(context)
- val enableShortcut = EggActionHelp.isShortcutEnable(egg)
- binding.background.tvAddShortcut.isEnabled = enableShortcut
- binding.root.swipeGravity =
- if (enableShortcut) Gravity.FILL_HORIZONTAL else GravityCompat.END
-
- binding.ivIcon.dispose()
- binding.background.ivBgIcon.dispose()
- if (egg.supportAdaptiveIcon) {
- binding.ivIcon.setImageDrawable(egg.getIcon(context))
- binding.background.ivBgIcon.setImageDrawable(egg.getIcon(context))
- } else {
- binding.ivIcon.load(egg.iconRes)
- binding.background.ivBgIcon.load(egg.iconRes)
- }
-
- val drawable = StateListDrawable().apply {
- val color = context.resolveColorStateList(
- M3R.attr.textAppearanceLabelMedium, android.R.attr.textColor
- )
- addState(
- intArrayOf(android.R.attr.state_selected),
- FontIconsDrawable(context, Icons.Rounded.app_shortcut, color)
- )
- val icon = if (binding.background.tvAddShortcut.isRtl)
- Icons.Rounded.swipe_right else Icons.Rounded.swipe_left
- addState(StateSet.NOTHING, FontIconsDrawable(context, icon, color))
- setBounds(0, 0, 30.dp, 30.dp)
- }
- binding.background.tvAddShortcut.updateCompoundDrawablesRelative(end = drawable)
- binding.root.swipeListener = SwipeAddShortcut(
- onSwipedPositionChanged = { v, _, p ->
- val symbol = if (v.isRtl) 1 else -1
- binding.background.tvAddShortcut.translationX = 12.dpf * symbol * p
- },
- onSwipedStartHalfFeedback = {
- binding.background.tvAddShortcut.isSelected = true
- it.performHapticFeedback(HapticFeedbackConstants.CLOCK_TICK)
- },
- onSwipedRelease = {
- binding.background.tvAddShortcut.isSelected = false
- },
- callback = {
- EggActionHelp.addShortcut(context, egg)
- })
- }
-
- private class SwipeAddShortcut(
- private val onSwipedPositionChanged: (view: View, left: Int, p: Float) -> Unit,
- private val onSwipedStartHalfFeedback: (view: View) -> Unit,
- private val onSwipedRelease: () -> Unit,
- private val callback: () -> Unit,
- ) : HorizontalSwipeLayout.OnSwipeListener {
-
- private var isFeedback: Boolean = false
- private var postInvokeCallback: Boolean = false
-
- override fun onSwipeCaptured(capturedChild: View) {
- isFeedback = false
- }
-
- override fun onSwipePositionChanged(changedView: View, left: Int, dx: Int) {
- val halfWidth = changedView.width / 2
- val rtl = changedView.isRtl
- val isSwipedStartHalf = if (!rtl) {
- left <= -halfWidth
- } else {
- left >= halfWidth
- }
- val p = if (!rtl) {
- -left * 1f / halfWidth
- } else {
- left * 1f / halfWidth
- }
- onSwipedPositionChanged.invoke(changedView, left, max(-1f, min(p, 1f)))
- postInvokeCallback = isSwipedStartHalf
- if (!isFeedback && isSwipedStartHalf) {
- onSwipedStartHalfFeedback.invoke(changedView)
- isFeedback = true
- }
- }
-
- override fun onSwipeReleased(releasedChild: View) {
- if (postInvokeCallback) {
- callback.invoke()
- postInvokeCallback = false
- }
- onSwipedRelease.invoke()
- }
- }
-
-}
\ No newline at end of file
diff --git a/app/src/main/java/com/dede/android_eggs/main/holders/GroupHolder.kt b/app/src/main/java/com/dede/android_eggs/main/holders/GroupHolder.kt
deleted file mode 100644
index 9a743191..00000000
--- a/app/src/main/java/com/dede/android_eggs/main/holders/GroupHolder.kt
+++ /dev/null
@@ -1,107 +0,0 @@
-package com.dede.android_eggs.main.holders
-
-import android.graphics.drawable.BitmapDrawable
-import android.view.Gravity
-import android.view.View
-import androidx.appcompat.view.menu.MenuPopupAccessor
-import androidx.appcompat.widget.PopupMenu
-import androidx.core.graphics.drawable.toBitmap
-import com.dede.android_eggs.R
-import com.dede.android_eggs.databinding.ItemEasterEggLayoutBinding
-import com.dede.android_eggs.main.EggActionHelp
-import com.dede.android_eggs.main.entity.Egg
-import com.dede.android_eggs.main.entity.Egg.Companion.getIcon
-import com.dede.android_eggs.main.entity.EggGroup
-import com.dede.android_eggs.ui.Icons
-import com.dede.android_eggs.ui.adapter.VHType
-import com.dede.android_eggs.ui.adapter.VHolder
-import com.dede.android_eggs.ui.drawables.FontIconsDrawable
-import com.dede.android_eggs.util.OrientationAngleSensor
-import com.dede.android_eggs.util.createOvalRipple
-import com.dede.android_eggs.util.createOvalWrapper
-import com.dede.android_eggs.util.resolveColor
-import com.dede.android_eggs.util.updateCompoundDrawablesRelative
-import com.dede.basic.dp
-import kotlin.math.roundToInt
-import com.google.android.material.R as M3R
-
-@VHType(viewType = Egg.VIEW_TYPE_EGG_GROUP)
-class GroupHolder(view: View) : VHolder(view),
- OrientationAngleSensor.OnOrientationAnglesUpdate {
-
- private val delegate = EggHolder(view)
- private val binding: ItemEasterEggLayoutBinding = delegate.binding
-
- @Suppress("PARAMETER_NAME_CHANGED_ON_OVERRIDE")
- override fun onBindViewHolder(eggGroup: EggGroup) {
- delegate.onBindViewHolder(eggGroup.selectedEgg)
-
- val drawable = createOvalRipple(
- context,
- createOvalWrapper(
- FontIconsDrawable(context, Icons.Rounded.expand_more).apply {
- setColor(binding.tvSummary.currentTextColor)
- },
- context.resolveColor(M3R.attr.colorButtonNormal)
- )
- ).apply {
- setBounds(0, 0, 24.dp, 24.dp)
- }
- binding.tvSummary.updateCompoundDrawablesRelative(end = drawable)
- binding.tvSummary.compoundDrawablePadding = 6.dp
- binding.tvSummary.setOnClickListener {
- showEggGroupMenu(eggGroup)
- }
- if (!EggActionHelp.isSupportedLaunch(eggGroup.selectedEgg)) {
- // override delegate click
- binding.cardView.setOnClickListener {
- showEggGroupMenu(eggGroup)
- }
- }
- }
-
- private fun showEggGroupMenu(eggGroup: EggGroup) {
- val vAdapter = this.vAdapter ?: return
- val popupMenu = PopupMenu(
- context, binding.tvSummary, Gravity.NO_GRAVITY,
- 0,
- R.style.Theme_EggGroup_PopupMenu_ListPopupWindow
- )
- popupMenu.setForceShowIcon(true)
- for ((index, egg) in eggGroup.child.withIndex()) {
- val menuTitle = egg.versionFormatter.format(context)
- popupMenu.menu.add(0, egg.id, index, menuTitle).apply {
- val drawable = egg.getIcon(context)
- val drawH = drawable.intrinsicHeight
- val drawW = drawable.intrinsicWidth
- val width: Int = 28.dp// Use the width as the basis to align the text
- val height: Int = (width / drawW.toFloat() * drawH).roundToInt()
- icon = BitmapDrawable(
- context.resources,
- drawable.toBitmap(width, height)
- )
- }
- }
- MenuPopupAccessor.setApi23Transitions(popupMenu)
- popupMenu.setOnMenuItemClickListener {
- val index = eggGroup.child.indexOfFirst { egg ->
- egg.id == it.itemId
- }
- if (index != -1) {
- if (index != eggGroup.selectedIndex) {
- eggGroup.selectedIndex = index
- vAdapter.notifyItemChanged(bindingAdapterPosition)
- }
- } else {
- throw IllegalArgumentException("Menu id: ${it.itemId}, title: ${it.title}, child: ${eggGroup.child.joinToString()}")
- }
- return@setOnMenuItemClickListener true
- }
- popupMenu.show()
- }
-
- override fun updateOrientationAngles(zAngle: Float, xAngle: Float, yAngle: Float) {
- delegate.updateOrientationAngles(zAngle, xAngle, yAngle)
- }
-
-}
\ No newline at end of file
diff --git a/app/src/main/java/com/dede/android_eggs/main/holders/WavyHolder.kt b/app/src/main/java/com/dede/android_eggs/main/holders/WavyHolder.kt
deleted file mode 100644
index 8a8d1dbc..00000000
--- a/app/src/main/java/com/dede/android_eggs/main/holders/WavyHolder.kt
+++ /dev/null
@@ -1,28 +0,0 @@
-package com.dede.android_eggs.main.holders
-
-import android.view.View
-import android.widget.ImageView
-import com.dede.android_eggs.R
-import com.dede.android_eggs.main.entity.Egg
-import com.dede.android_eggs.main.entity.Wavy
-import com.dede.android_eggs.ui.adapter.VHType
-import com.dede.android_eggs.ui.adapter.VHolder
-import com.dede.android_eggs.util.createRepeatWavyDrawable
-import com.dede.basic.requireDrawable
-
-@VHType(viewType = Egg.VIEW_TYPE_WAVY)
-class WavyHolder(view: View) : VHolder(view) {
-
- private val imageView = itemView.findViewById(R.id.iv_icon)
-
- @Suppress("PARAMETER_NAME_CHANGED_ON_OVERRIDE")
- override fun onBindViewHolder(wavy: Wavy) {
- if (!wavy.repeat) {
- imageView.scaleType = ImageView.ScaleType.CENTER
- imageView.setImageDrawable(context.requireDrawable(wavy.wavyRes))
- } else {
- imageView.scaleType = ImageView.ScaleType.FIT_XY
- imageView.setImageDrawable(createRepeatWavyDrawable(context, wavy.wavyRes))
- }
- }
-}
\ No newline at end of file
diff --git a/app/src/main/java/com/dede/android_eggs/startup/ApplicationInitializer.kt b/app/src/main/java/com/dede/android_eggs/startup/ApplicationInitializer.kt
index ee9f1d58..a8023324 100644
--- a/app/src/main/java/com/dede/android_eggs/startup/ApplicationInitializer.kt
+++ b/app/src/main/java/com/dede/android_eggs/startup/ApplicationInitializer.kt
@@ -3,8 +3,10 @@ package com.dede.android_eggs.startup
import android.app.Application
import android.content.Context
import androidx.startup.Initializer
-import com.dede.android_eggs.views.settings.SettingsPrefs
import com.dede.android_eggs.util.ActivityActionDispatcher
+import com.dede.android_eggs.views.crash.CrashActivity
+import com.dede.android_eggs.views.crash.GlobalExceptionHandler
+import com.dede.android_eggs.views.settings.SettingsPrefs
import com.dede.basic.GlobalContext
class ApplicationInitializer : Initializer {
@@ -13,6 +15,7 @@ class ApplicationInitializer : Initializer {
val application = context.applicationContext as Application
SettingsPrefs.apply(application)
ActivityActionDispatcher.register(application)
+ GlobalExceptionHandler.initialize(application, CrashActivity::class.java)
}
override fun dependencies(): List>> = listOf(
diff --git a/app/src/main/java/com/dede/android_eggs/ui/Icons.kt b/app/src/main/java/com/dede/android_eggs/ui/Icons.kt
index 4ec147ef..5547e515 100644
--- a/app/src/main/java/com/dede/android_eggs/ui/Icons.kt
+++ b/app/src/main/java/com/dede/android_eggs/ui/Icons.kt
@@ -1,7 +1,7 @@
package com.dede.android_eggs.ui
/** Generated automatically via **subset_icons_font.py**, do not modify this file. */
-// 1699349829
+// 1699951651
object Icons {
object Outlined {
@@ -22,9 +22,6 @@ object Icons {
/** app_registration */
const val app_registration = "\uEF40"
- /** app_shortcut */
- const val app_shortcut = "\uEAE4"
-
/** brightness_4 */
const val brightness_4 = "\uE3A9"
@@ -37,21 +34,12 @@ object Icons {
/** brightness_low */
const val brightness_low = "\uE1AD"
- /** expand_more */
- const val expand_more = "\uE5CF"
-
/** palette */
const val palette = "\uE40A"
/** settings */
const val settings = "\uE8B8"
- /** swipe_left */
- const val swipe_left = "\uEB59"
-
- /** swipe_right */
- const val swipe_right = "\uEB52"
-
/** tips_and_updates */
const val tips_and_updates = "\uE79A"
diff --git a/app/src/main/java/com/dede/android_eggs/ui/drawables/AlterableAdaptiveIconDrawable.kt b/app/src/main/java/com/dede/android_eggs/ui/drawables/AlterableAdaptiveIconDrawable.kt
index 748384dd..71a9c363 100644
--- a/app/src/main/java/com/dede/android_eggs/ui/drawables/AlterableAdaptiveIconDrawable.kt
+++ b/app/src/main/java/com/dede/android_eggs/ui/drawables/AlterableAdaptiveIconDrawable.kt
@@ -22,7 +22,7 @@ import kotlin.math.roundToInt
class AlterableAdaptiveIconDrawable(
context: Context,
@DrawableRes res: Int,
- maskPathStr: String? = null
+ maskPathStr: String? = null,
) : Drawable() {
companion object {
@@ -51,6 +51,7 @@ class AlterableAdaptiveIconDrawable(
private val childDrawables: Array
private val mask: Path = Path()
+ private val savedMask: Path = Path()
private val layerCanvas = Canvas()
private var layerBitmap: Bitmap? = null
@@ -70,7 +71,8 @@ class AlterableAdaptiveIconDrawable(
if (TextUtils.isEmpty(pathStr)) {
pathStr = context.resources.getString(R.string.icon_shape_circle_path)
}
- mask.set(PathParser.createPathFromPathData(pathStr))
+ savedMask.set(PathParser.createPathFromPathData(pathStr))
+ mask.set(savedMask)
val drawable = context.requireDrawable(res)
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O && drawable is AdaptiveIconDrawable) {
@@ -94,17 +96,22 @@ class AlterableAdaptiveIconDrawable(
fun setMaskPath(pathStr: String) {
if (TextUtils.isEmpty(pathStr)) return
- mask.set(PathParser.createPathFromPathData(pathStr))
+ savedMask.set(PathParser.createPathFromPathData(pathStr))
+ updateMaskBoundsInternal(bounds)
invalidateSelf()
}
fun setForegroundMatrix(matrix: Matrix) {
if (!isAdaptiveIconDrawable) return
foregroundMatrix.set(matrix)
- layerShader = null
invalidateSelf()
}
+ override fun invalidateSelf() {
+ layerShader = null
+ super.invalidateSelf()
+ }
+
override fun draw(canvas: Canvas) {
val layerBitmap: Bitmap = this.layerBitmap ?: return
@@ -164,10 +171,8 @@ class AlterableAdaptiveIconDrawable(
if (isAdaptiveIconDrawable) {
val cX: Int = bounds.width() / 2
val cY: Int = bounds.height() / 2
- val insetWidth: Int =
- (bounds.width() / (DEFAULT_VIEW_PORT_SCALE * 2)).toInt()
- val insetHeight: Int =
- (bounds.height() / (DEFAULT_VIEW_PORT_SCALE * 2)).toInt()
+ val insetWidth: Int = (bounds.width() / (DEFAULT_VIEW_PORT_SCALE * 2)).toInt()
+ val insetHeight: Int = (bounds.height() / (DEFAULT_VIEW_PORT_SCALE * 2)).toInt()
outRect.set(cX - insetWidth, cY - insetHeight, cX + insetWidth, cY + insetHeight)
} else {
outRect.set(bounds)
@@ -179,13 +184,15 @@ class AlterableAdaptiveIconDrawable(
}
private fun updateMaskBoundsInternal(bounds: Rect) {
+ maskMatrix.reset()
maskMatrix.setScale(bounds.width() / MASK_SIZE, bounds.height() / MASK_SIZE)
- mask.transform(maskMatrix)
+ savedMask.transform(maskMatrix, mask)
// maskMatrix.postTranslate(bounds.left.toFloat(), bounds.top.toFloat())
// mask.transform(maskMatrix)
- if (layerBitmap == null) {
+ val bitmap = layerBitmap
+ if (bitmap == null || bounds.width() != bitmap.width || bounds.height() != bitmap.height) {
layerBitmap = Bitmap.createBitmap(
bounds.width(), bounds.height(), Bitmap.Config.ARGB_8888
)
@@ -220,4 +227,13 @@ class AlterableAdaptiveIconDrawable(
override fun getOpacity(): Int {
return PixelFormat.TRANSLUCENT
}
+
+ override fun applyTheme(t: Resources.Theme) {
+ for (drawable in childDrawables) {
+ val d = drawable.drawable
+ if (d.canApplyTheme()) {
+ d.applyTheme(t)
+ }
+ }
+ }
}
\ No newline at end of file
diff --git a/app/src/main/java/com/dede/android_eggs/ui/views/AppBarExpandedState.kt b/app/src/main/java/com/dede/android_eggs/ui/views/AppBarExpandedState.kt
deleted file mode 100644
index d1e7a3f2..00000000
--- a/app/src/main/java/com/dede/android_eggs/ui/views/AppBarExpandedState.kt
+++ /dev/null
@@ -1,41 +0,0 @@
-package com.dede.android_eggs.ui.views
-
-import android.app.Activity
-import android.os.Bundle
-import androidx.annotation.IdRes
-import com.google.android.material.appbar.AppBarLayout
-
-/**
- * Fix AppBarLayout Expanded state error
- */
-class AppBarExpandedState(
- private val activity: Activity,
- @IdRes private val id: Int,
- private val animate: Boolean = false,
- private val default: Boolean = true,
-) : AppBarLayout.OnOffsetChangedListener {
-
- companion object {
- private const val STATE_KEY = "appbar_expanded_state"
- }
-
- private lateinit var appBarLayout: AppBarLayout
- private var isExpanded: Boolean = default
-
- fun restore(savedInstanceState: Bundle?) {
- appBarLayout = activity.findViewById(id)
- appBarLayout.addOnOffsetChangedListener(this)
- if (savedInstanceState != null) {
- isExpanded = savedInstanceState.getBoolean(STATE_KEY, default)
- }
- appBarLayout.setExpanded(isExpanded, animate)
- }
-
- fun saveState(outState: Bundle) {
- outState.putBoolean(STATE_KEY, isExpanded)
- }
-
- override fun onOffsetChanged(appBarLayout: AppBarLayout?, verticalOffset: Int) {
- isExpanded = verticalOffset >= 0
- }
-}
\ No newline at end of file
diff --git a/app/src/main/java/com/dede/android_eggs/ui/views/EasterEggFooterView.kt b/app/src/main/java/com/dede/android_eggs/ui/views/EasterEggFooterView.kt
deleted file mode 100644
index 7fa7bd6b..00000000
--- a/app/src/main/java/com/dede/android_eggs/ui/views/EasterEggFooterView.kt
+++ /dev/null
@@ -1,119 +0,0 @@
-package com.dede.android_eggs.ui.views
-
-import android.content.Context
-import android.content.Intent
-import android.graphics.Paint
-import android.util.AttributeSet
-import android.view.LayoutInflater
-import android.view.View
-import android.view.ViewGroup
-import androidx.constraintlayout.widget.ConstraintLayout
-import androidx.core.net.toUri
-import androidx.fragment.app.FragmentActivity
-import com.dede.android_eggs.BuildConfig
-import com.dede.android_eggs.R
-import com.dede.android_eggs.databinding.ViewEasterEggFooterBinding
-import com.dede.android_eggs.util.CustomTabsBrowser
-import com.dede.android_eggs.util.createChooser
-import com.dede.android_eggs.util.createRepeatWavyDrawable
-import com.dede.android_eggs.util.getActivity
-import com.dede.android_eggs.views.settings.component.ComponentManagerFragment
-import com.dede.android_eggs.views.timeline.AndroidTimelineFragment
-import com.dede.basic.dp
-
-class EasterEggFooterView @JvmOverloads constructor(
- context: Context,
- attrs: AttributeSet? = null,
- defStyleAttr: Int = 0,
-) : ConstraintLayout(context, attrs, defStyleAttr), View.OnClickListener {
-
- private val binding = ViewEasterEggFooterBinding.inflate(LayoutInflater.from(context), this)
-
- init {
- setPadding(24.dp, 0, 24.dp, 30.dp)
-
- binding.tvVersion.text = binding.root.context.getString(
- R.string.label_version,
- BuildConfig.VERSION_NAME,
- BuildConfig.VERSION_CODE
- )
- binding.tvGitHash.text = BuildConfig.GIT_HASH
- binding.tvGitHash.paintFlags = binding.tvGitHash.paintFlags or Paint.UNDERLINE_TEXT_FLAG
- val views = arrayOf(
- binding.tvGitHash, binding.tvComponentManager,
- binding.tvPrivacy, binding.tvLicense, binding.tvFeedback,
- )
- for (view in views) {
- view.setOnClickListener(this)
- }
- handleFlowLayoutChild()
-
- binding.ivLine.setImageDrawable(
- createRepeatWavyDrawable(context, R.drawable.ic_wavy_line_1)
- )
- }
-
- private fun handleFlowLayoutChild() {
- FlowLayoutSplit.join(binding.flowLayout,
- object : FlowLayoutSplit.FlowSplitViewProvider {
- override fun createSplitView(context: Context, parent: ViewGroup): View {
- return LayoutInflater.from(context)
- .inflate(R.layout.layout_flow_separator, parent, false)
- }
- })
- FlowLayoutSplit.forEachUnwrapChild(binding.flowLayout) {
- it.setOnClickListener(this)
- }
- }
-
- override fun onClick(v: View) {
- val context = v.context
- when (v.id) {
- R.id.tv_git_hash -> {
- val commitId = context.getString(R.string.url_github_commit, BuildConfig.GIT_HASH)
- CustomTabsBrowser.launchUrl(context, commitId.toUri())
- }
-
- R.id.tv_timeline -> {
- val activity = context.getActivity() ?: return
- AndroidTimelineFragment.show(activity.supportFragmentManager)
- }
-
- R.id.tv_component_manager -> {
- val activity = context.getActivity() ?: return
- ComponentManagerFragment.show(activity.supportFragmentManager)
- }
-
- R.id.tv_github, R.id.tv_translation, R.id.tv_donate, R.id.tv_beta,
- R.id.tv_privacy, R.id.tv_license,
- -> {
- CustomTabsBrowser.launchUrl(context, v.tagString.toUri())
- }
-
- R.id.tv_share -> {
- val target = Intent(Intent.ACTION_SEND)
- .putExtra(Intent.EXTRA_TEXT, v.tagString)
- .setType("text/plain")
- val intent = context.createChooser(target)
- context.startActivity(intent)
- }
-
- R.id.tv_star -> {
- launchMarket(context)
- }
-
- R.id.tv_feedback -> {
- CustomTabsBrowser.launchUrlByBrowser(context, v.tagString.toUri())
- }
-
- else -> throw UnsupportedOperationException()
- }
- }
-
- private fun launchMarket(context: Context) {
- val uri = context.getString(R.string.url_market_detail, context.packageName).toUri()
- CustomTabsBrowser.launchUrlByBrowser(context, uri)
- }
-
- private inline val View.tagString get() = tag as String
-}
\ No newline at end of file
diff --git a/app/src/main/java/com/dede/android_eggs/ui/views/FlowLayoutSplit.kt b/app/src/main/java/com/dede/android_eggs/ui/views/FlowLayoutSplit.kt
deleted file mode 100644
index 2fd7e73e..00000000
--- a/app/src/main/java/com/dede/android_eggs/ui/views/FlowLayoutSplit.kt
+++ /dev/null
@@ -1,60 +0,0 @@
-package com.dede.android_eggs.ui.views
-
-import android.content.Context
-import android.view.Gravity
-import android.view.View
-import android.view.ViewGroup
-import android.widget.LinearLayout
-import androidx.core.view.get
-import com.google.android.material.internal.FlowLayout
-
-class FlowLayoutSplit(context: Context) : LinearLayout(context) {
-
- companion object {
-
- fun forEachUnwrapChild(flowLayout: FlowLayout, block: (child: View) -> Unit) {
- for (i in 0.. 0) Gravity.RIGHT else Gravity.LEFT,
- ViewCompat.getLayoutDirection(child)
- )
- val startLeft = getStartLeft(child)
- val endLeft = getEndLeft(child)
- if (absGravity == Gravity.LEFT) {
- if (isTargetGravity(Gravity.LEFT)) {
- return max(-endLeft, left)
- } else if (left >= startLeft) {
- return max(left, startLeft)// slide right after , reverse slide
- }
- } else if (absGravity == Gravity.RIGHT) {
- if (isTargetGravity(Gravity.RIGHT)) {
- return min(left, endLeft)
- } else if (left <= startLeft) {
- return min(left, startLeft)// slide left after , reverse slide
- }
- }
- return child.left
- }
-
- private fun isTargetGravity(abs: Int): Boolean {
- return (gravity and abs) == abs
- }
-
- private fun getEndLeft(child: View): Int {
- return child.width * 3 / 5
- }
-
- private fun getStartLeft(child: View): Int {
- val params = child.layoutParams as MarginLayoutParams
- return width - child.width - params.leftMargin
- }
-
- override fun getViewHorizontalDragRange(child: View): Int {
- return if (isSwipeView(child)) child.width else 0
- }
-
- override fun clampViewPositionVertical(child: View, top: Int, dy: Int): Int {
- return child.top
- }
- }
-}
\ No newline at end of file
diff --git a/app/src/main/java/com/dede/android_eggs/ui/views/SnapshotGroupView.kt b/app/src/main/java/com/dede/android_eggs/ui/views/SnapshotGroupView.kt
index 69f7dc9e..c1a6a201 100644
--- a/app/src/main/java/com/dede/android_eggs/ui/views/SnapshotGroupView.kt
+++ b/app/src/main/java/com/dede/android_eggs/ui/views/SnapshotGroupView.kt
@@ -3,7 +3,6 @@ package com.dede.android_eggs.ui.views
import android.content.Context
import android.graphics.ColorMatrix
import android.graphics.ColorMatrixColorFilter
-import android.net.Uri
import android.util.AttributeSet
import android.view.LayoutInflater
import android.view.ViewGroup
@@ -11,18 +10,12 @@ import android.view.ViewGroup.LayoutParams.MATCH_PARENT
import android.widget.ImageView
import androidx.constraintlayout.widget.ConstraintLayout
import androidx.core.view.isVisible
-import androidx.fragment.app.FragmentActivity
import androidx.recyclerview.widget.RecyclerView
-import coil.dispose
-import coil.load
import com.dede.android_eggs.R
-import com.dede.android_eggs.main.EggListFragment
import com.dede.android_eggs.main.entity.Snapshot
import com.dede.android_eggs.ui.adapter.VAdapter
import com.dede.android_eggs.ui.adapter.VHolder
import com.dede.android_eggs.util.ThemeUtils
-import com.dede.android_eggs.util.findFragmentById
-import com.dede.android_eggs.util.getActivity
import com.dede.basic.provider.SnapshotProvider
import com.dede.blurhash_android.BlurHashDrawable
import com.google.android.material.carousel.CarouselLayoutManager
@@ -86,13 +79,9 @@ class SnapshotGroupView @JvmOverloads constructor(context: Context, attrs: Attri
val background: ImageView = holder.findViewById(R.id.iv_background)
val provider: SnapshotProvider = snapshot.provider
background.isVisible = !provider.includeBackground
- background.dispose()
- if (!provider.includeBackground && background.drawable == null) {
- val placeholder = BlurHashDrawable(context, R.string.hash_snapshot_bg, 54, 32)
- background.load(randomBgUri()) {
- placeholder(placeholder)
- error(placeholder)
- }
+ if (!provider.includeBackground) {
+ val hashDrawable = BlurHashDrawable(randomHash(), 54, 32)// 5:3
+ background.setImageDrawable(hashDrawable)
if (ThemeUtils.isSystemNightMode(context)) {
val matrix = ColorMatrix()
matrix.setScale(0.8f, 0.8f, 0.8f, 0.8f)
@@ -103,18 +92,11 @@ class SnapshotGroupView @JvmOverloads constructor(context: Context, attrs: Attri
}
group.removeAllViewsInLayout()
group.addView(provider.create(group.context), MATCH_PARENT, MATCH_PARENT)
- holder.itemView.setOnClickListener {
- val fragment = it.context.getActivity()
- ?.findFragmentById(R.id.fl_eggs)
- ?: return@setOnClickListener
- fragment.smoothScrollToEgg(snapshot.id)
- }
}
- private fun randomBgUri(): Uri? {
- val list = context.assets.list("gallery")
- if (list.isNullOrEmpty()) return null
- val index = Random.nextInt(list.size)
- return Uri.parse("file:///android_asset/gallery/%s".format(list[index]))
+ private fun randomHash(): String {
+ val strings = context.resources.getStringArray(R.array.hash_gallery)
+ val index = Random.nextInt(strings.size)
+ return strings[index]
}
}
\ No newline at end of file
diff --git a/app/src/main/java/com/dede/android_eggs/ui/views/ViscousFluidInterpolator.kt b/app/src/main/java/com/dede/android_eggs/ui/views/ViscousFluidInterpolator.kt
new file mode 100644
index 00000000..f2fff27d
--- /dev/null
+++ b/app/src/main/java/com/dede/android_eggs/ui/views/ViscousFluidInterpolator.kt
@@ -0,0 +1,46 @@
+package com.dede.android_eggs.ui.views
+
+import android.view.animation.Interpolator
+import kotlin.math.exp
+
+// android.widget.Scroller.ViscousFluidInterpolator
+class ViscousFluidInterpolator : Interpolator {
+
+ override fun getInterpolation(input: Float): Float {
+ val interpolated = VISCOUS_FLUID_NORMALIZE * viscousFluid(input)
+ return if (interpolated > 0) {
+ interpolated + VISCOUS_FLUID_OFFSET
+ } else interpolated
+ }
+
+ companion object {
+
+ fun getInstance(): ViscousFluidInterpolator {
+ return ViscousFluidInterpolator()
+ }
+
+ /**
+ * Controls the viscous fluid effect (how much of it).
+ */
+ private const val VISCOUS_FLUID_SCALE = 8.0f
+
+ // must be set to 1.0 (used in viscousFluid())
+ private val VISCOUS_FLUID_NORMALIZE = 1.0f / viscousFluid(1.0f)
+
+ // account for very small floating-point error
+ private var VISCOUS_FLUID_OFFSET = 1.0f - VISCOUS_FLUID_NORMALIZE * viscousFluid(1.0f)
+
+ private fun viscousFluid(x: Float): Float {
+ var x = x
+ x *= VISCOUS_FLUID_SCALE
+ if (x < 1.0f) {
+ x -= 1.0f - exp(-x.toDouble()).toFloat()
+ } else {
+ val start = 0.36787945f // 1/e == exp(-1)
+ x = 1.0f - exp((1.0f - x).toDouble()).toFloat()
+ x = start + x * (1.0f - start)
+ }
+ return x
+ }
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/dede/android_eggs/util/ContextExt.kt b/app/src/main/java/com/dede/android_eggs/util/ContextExt.kt
index 87750440..8c218224 100644
--- a/app/src/main/java/com/dede/android_eggs/util/ContextExt.kt
+++ b/app/src/main/java/com/dede/android_eggs/util/ContextExt.kt
@@ -2,10 +2,13 @@ package com.dede.android_eggs.util
import android.annotation.SuppressLint
import android.app.Activity
+import android.content.ClipData
+import android.content.ClipboardManager
import android.content.Context
import android.content.Intent
import android.widget.Toast
import androidx.annotation.StringRes
+import androidx.core.content.getSystemService
import com.dede.android_eggs.R
import com.google.android.material.internal.ContextUtils
@@ -28,3 +31,9 @@ fun Context.createChooser(target: Intent): Intent {
return Intent.createChooser(target, getString(R.string.title_open_with))
.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
}
+
+fun Context.copy(text: String) {
+ val service = getSystemService() ?: return
+ service.setPrimaryClip(ClipData.newPlainText(null, text))
+ toast(android.R.string.copy)
+}
diff --git a/app/src/main/java/com/dede/android_eggs/util/LocalEvent.kt b/app/src/main/java/com/dede/android_eggs/util/LocalEvent.kt
index 56cf2b16..660fc007 100644
--- a/app/src/main/java/com/dede/android_eggs/util/LocalEvent.kt
+++ b/app/src/main/java/com/dede/android_eggs/util/LocalEvent.kt
@@ -6,6 +6,10 @@ import android.content.Intent
import android.content.IntentFilter
import android.os.Bundle
import androidx.activity.ComponentActivity
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.DisposableEffect
+import androidx.compose.runtime.remember
+import androidx.compose.ui.platform.LocalContext
import androidx.fragment.app.Fragment
import androidx.lifecycle.DefaultLifecycleObserver
import androidx.lifecycle.LifecycleOwner
@@ -23,6 +27,31 @@ object LocalEvent {
return Receiver(owner)
}
+ @Composable
+ fun receiver(): ComposeReceiver {
+ val context = LocalContext.current
+ return remember { ComposeReceiver(context) }
+ }
+
+ class ComposeReceiver(private val context: Context) {
+
+ private class DisposedReceiver(val callback: EventCallback) : BroadcastReceiver() {
+ override fun onReceive(context: Context, intent: Intent) {
+ callback.invoke(intent)
+ }
+ }
+
+ @Composable
+ fun register(action: String, eventCallback: EventCallback) {
+ DisposableEffect(key1 = action) {
+ val receiver = DisposedReceiver(eventCallback)
+ context.localBroadcastManager.registerReceiver(receiver, IntentFilter(action))
+ onDispose {
+ context.localBroadcastManager.unregisterReceiver(receiver)
+ }
+ }
+ }
+ }
class Receiver(private val owner: LifecycleOwner) {
@@ -60,7 +89,7 @@ private val LifecycleOwner.context: Context
get() = when (this) {
is ComponentActivity -> this
is Fragment -> requireContext()
- else -> throw IllegalArgumentException()
+ else -> throw IllegalArgumentException(this.toString())
}
private val Context.localBroadcastManager: LocalBroadcastManager
diff --git a/app/src/main/java/com/dede/android_eggs/util/OrientationAngleSensor.kt b/app/src/main/java/com/dede/android_eggs/util/OrientationAngleSensor.kt
index f6ffa2a9..1a3fb0b6 100644
--- a/app/src/main/java/com/dede/android_eggs/util/OrientationAngleSensor.kt
+++ b/app/src/main/java/com/dede/android_eggs/util/OrientationAngleSensor.kt
@@ -95,6 +95,8 @@ class OrientationAngleSensor(
}
fun destroy() {
+ // reset default angles
+ onOrientationAnglesUpdate.updateOrientationAngles(0f, 0f, 0f)
stop()
owner?.lifecycle?.removeObserver(this)
}
diff --git a/app/src/main/java/com/dede/android_eggs/util/Pref.kt b/app/src/main/java/com/dede/android_eggs/util/Pref.kt
index 7f16b15d..857883a8 100644
--- a/app/src/main/java/com/dede/android_eggs/util/Pref.kt
+++ b/app/src/main/java/com/dede/android_eggs/util/Pref.kt
@@ -4,15 +4,15 @@ import android.content.Context
import android.content.SharedPreferences
import androidx.preference.Preference
import androidx.preference.PreferenceFragmentCompat
+import androidx.preference.PreferenceManager
-// compileOnly(deps.androidx.preference)
val Context.pref: SharedPreferences
get() {
- return applicationContext.getSharedPreferences(
- applicationContext.packageName + "_preferences",
- Context.MODE_PRIVATE
- )
- //return PreferenceManager.getDefaultSharedPreferences(applicationContext)
+// return applicationContext.getSharedPreferences(
+// applicationContext.packageName + "_preferences",
+// Context.MODE_PRIVATE
+// )
+ return PreferenceManager.getDefaultSharedPreferences(applicationContext)
}
fun PreferenceFragmentCompat.requirePreference(key: String): T {
diff --git a/app/src/main/java/com/dede/android_eggs/views/crash/CrashActivity.kt b/app/src/main/java/com/dede/android_eggs/views/crash/CrashActivity.kt
new file mode 100644
index 00000000..5431808b
--- /dev/null
+++ b/app/src/main/java/com/dede/android_eggs/views/crash/CrashActivity.kt
@@ -0,0 +1,227 @@
+@file:OptIn(ExperimentalMaterial3Api::class)
+
+package com.dede.android_eggs.views.crash
+
+import android.content.ActivityNotFoundException
+import android.content.Intent
+import android.net.Uri
+import android.os.Build
+import android.os.Bundle
+import androidx.activity.compose.setContent
+import androidx.activity.enableEdgeToEdge
+import androidx.appcompat.app.AppCompatActivity
+import androidx.compose.animation.animateContentSize
+import androidx.compose.foundation.layout.Arrangement
+import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.Row
+import androidx.compose.foundation.layout.Spacer
+import androidx.compose.foundation.layout.fillMaxSize
+import androidx.compose.foundation.layout.fillMaxWidth
+import androidx.compose.foundation.layout.height
+import androidx.compose.foundation.layout.navigationBarsPadding
+import androidx.compose.foundation.layout.padding
+import androidx.compose.foundation.layout.size
+import androidx.compose.foundation.layout.statusBarsPadding
+import androidx.compose.foundation.layout.width
+import androidx.compose.foundation.rememberScrollState
+import androidx.compose.foundation.verticalScroll
+import androidx.compose.material.icons.Icons
+import androidx.compose.material.icons.rounded.BugReport
+import androidx.compose.material.icons.rounded.ContentCopy
+import androidx.compose.material.icons.rounded.KeyboardArrowDown
+import androidx.compose.material.icons.rounded.PowerSettingsNew
+import androidx.compose.material.icons.rounded.RestartAlt
+import androidx.compose.material.icons.rounded.SmartToy
+import androidx.compose.material3.Card
+import androidx.compose.material3.ExperimentalMaterial3Api
+import androidx.compose.material3.FloatingActionButton
+import androidx.compose.material3.FloatingActionButtonDefaults
+import androidx.compose.material3.Icon
+import androidx.compose.material3.IconButton
+import androidx.compose.material3.MaterialTheme.colorScheme
+import androidx.compose.material3.MaterialTheme.shapes
+import androidx.compose.material3.MaterialTheme.typography
+import androidx.compose.material3.Surface
+import androidx.compose.material3.Text
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.remember
+import androidx.compose.runtime.setValue
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.platform.LocalContext
+import androidx.compose.ui.res.stringResource
+import androidx.compose.ui.text.font.FontWeight
+import androidx.compose.ui.text.style.TextOverflow
+import androidx.compose.ui.tooling.preview.Preview
+import androidx.compose.ui.unit.dp
+import com.dede.android_eggs.BuildConfig
+import com.dede.android_eggs.R
+import com.dede.android_eggs.util.ThemeUtils
+import com.dede.android_eggs.util.copy
+import com.dede.android_eggs.views.crash.GlobalExceptionHandler.Companion.getCrashReason
+import com.dede.android_eggs.views.main.EasterEggsActivity
+import com.dede.android_eggs.views.theme.AppTheme
+import kotlin.system.exitProcess
+
+class CrashActivity : AppCompatActivity() {
+
+ override fun onCreate(savedInstanceState: Bundle?) {
+ ThemeUtils.tryApplyOLEDTheme(this)
+ enableEdgeToEdge()
+ super.onCreate(savedInstanceState)
+
+ val crashReason = getCrashReason(intent)
+ val split = crashReason.split("\n\n")
+ val exName = split[0].trim()
+ val ex = split.drop(1).joinToString("\n\n")
+
+ val title = "[Bug] App Crash: $exName"
+ val deviceInfo =
+ "Device: ${Build.MODEL} (${Build.BRAND} - ${Build.DEVICE}), SDK: ${Build.VERSION.SDK_INT} (${Build.VERSION.RELEASE}), App: ${BuildConfig.VERSION_NAME} (${BuildConfig.VERSION_CODE})\n\n"
+ val body = "$deviceInfo$ex"
+
+ setContent {
+ AppTheme {
+ Surface {
+ CrashScreen(title, body)
+ }
+ }
+ }
+ }
+}
+
+@Composable
+@Preview(showSystemUi = true)
+private fun CrashScreen(
+ title: String = "[Bug] App Crash: java.lang.IllegalStateException: xxx",
+ body: String = "",
+) {
+ var expanded by remember { mutableStateOf(false) }
+ val context = LocalContext.current
+ Box {
+ Column(
+ modifier = Modifier
+ .fillMaxSize()
+ .verticalScroll(rememberScrollState()),
+ verticalArrangement = Arrangement.Center,
+ horizontalAlignment = Alignment.CenterHorizontally
+ ) {
+ Icon(
+ imageVector = Icons.Rounded.SmartToy,
+ contentDescription = null,
+ modifier = Modifier
+ .statusBarsPadding()
+ .size(42.dp),
+ tint = colorScheme.primary
+ )
+ Spacer(modifier = Modifier.height(6.dp))
+ Text(
+ text = stringResource(id = R.string.app_name),
+ style = typography.titleMedium
+ )
+ Spacer(modifier = Modifier.height(14.dp))
+ Card(
+ onClick = {
+ expanded = !expanded
+ },
+ shape = shapes.extraLarge,
+ modifier = Modifier
+ .fillMaxWidth(fraction = 0.9f)
+ .navigationBarsPadding()
+ .animateContentSize()
+ ) {
+ Row(
+ verticalAlignment = Alignment.CenterVertically
+ ) {
+ Text(
+ text = if (expanded) body else title,
+ modifier = Modifier
+ .weight(1f)
+ .padding(16.dp),
+ maxLines = if (expanded) Int.MAX_VALUE else 1,
+ style = typography.bodySmall,
+ fontWeight = if (expanded) FontWeight.Normal else FontWeight.Bold,
+ overflow = TextOverflow.Ellipsis,
+ )
+ if (!expanded) {
+ Icon(
+ imageVector = Icons.Rounded.KeyboardArrowDown,
+ contentDescription = null
+ )
+ Spacer(modifier = Modifier.width(12.dp))
+ }
+ }
+ }
+ Spacer(modifier = Modifier.height(16.dp))
+ }
+ IconButton(
+ modifier = Modifier
+ .align(Alignment.TopEnd)
+ .padding(top = 12.dp, end = 12.dp)
+ .statusBarsPadding(),
+ onClick = {
+ exitProcess(0)
+ },
+ ) {
+ Icon(
+ imageVector = Icons.Rounded.PowerSettingsNew,
+ contentDescription = null
+ )
+ }
+ Row(
+ modifier = Modifier
+ .align(Alignment.BottomCenter)
+ .padding(bottom = 36.dp)
+ .navigationBarsPadding()
+ .fillMaxWidth(fraction = 0.8f),
+ horizontalArrangement = Arrangement.SpaceAround
+ ) {
+ FloatingActionButton(
+ onClick = {
+ context.startActivity(Intent(context, EasterEggsActivity::class.java))
+ },
+ shape = FloatingActionButtonDefaults.largeShape
+ ) {
+ Icon(
+ imageVector = Icons.Rounded.RestartAlt,
+ contentDescription = null
+ )
+ }
+ FloatingActionButton(
+ onClick = {
+ context.copy(title + "\n\n" + body)
+ },
+ shape = FloatingActionButtonDefaults.largeShape
+ ) {
+ Icon(
+ imageVector = Icons.Rounded.ContentCopy,
+ contentDescription = null
+ )
+ }
+ FloatingActionButton(
+ onClick = {
+ val uri = Uri.parse(context.getString(R.string.url_github_issues))
+ .buildUpon()
+ .appendPath("new")
+ .appendQueryParameter("title", title)
+ .appendQueryParameter("body", "```\n$body\n```")
+ .build()
+ try {
+ context.startActivity(Intent(Intent.ACTION_VIEW, uri))
+ } catch (_: ActivityNotFoundException) {
+ }
+ context.copy(title + "\n\n" + body)
+ },
+ shape = FloatingActionButtonDefaults.largeShape
+ ) {
+ Icon(
+ imageVector = Icons.Rounded.BugReport,
+ contentDescription = null
+ )
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/dede/android_eggs/views/crash/GlobalExceptionHandler.kt b/app/src/main/java/com/dede/android_eggs/views/crash/GlobalExceptionHandler.kt
new file mode 100644
index 00000000..a5f6eec5
--- /dev/null
+++ b/app/src/main/java/com/dede/android_eggs/views/crash/GlobalExceptionHandler.kt
@@ -0,0 +1,61 @@
+package com.dede.android_eggs.views.crash
+
+import android.app.Activity
+import android.content.Context
+import android.content.Intent
+import android.util.Log
+import kotlin.system.exitProcess
+
+class GlobalExceptionHandler private constructor(
+ private val applicationContext: Context,
+ private val defaultHandler: Thread.UncaughtExceptionHandler,
+ private val activityToBeLaunched: Class
+) : Thread.UncaughtExceptionHandler {
+
+ override fun uncaughtException(p0: Thread, p1: Throwable) {
+ kotlin.runCatching {
+ Log.e(this.toString(), p1.stackTraceToString())
+ applicationContext.launchActivity(activityToBeLaunched, p1)
+ exitProcess(0)
+ }.getOrElse {
+ defaultHandler.uncaughtException(p0, p1)
+ }
+ }
+
+ private fun Context.launchActivity(
+ activity: Class,
+ exception: Throwable
+ ) {
+ val crashedIntent = Intent(applicationContext, activity).apply {
+ putExtra(
+ INTENT_DATA_NAME,
+ "${exception::class.java.simpleName}\n\n${Log.getStackTraceString(exception)}"
+ )
+ addFlags(defFlags)
+ }
+ applicationContext.startActivity(crashedIntent)
+ }
+
+ companion object {
+
+ private const val INTENT_DATA_NAME = "GlobalExceptionHandler"
+ private const val defFlags = Intent.FLAG_ACTIVITY_CLEAR_TOP or
+ Intent.FLAG_ACTIVITY_NEW_TASK or
+ Intent.FLAG_ACTIVITY_CLEAR_TASK
+
+ fun getCrashReason(intent: Intent): String {
+ return intent.getStringExtra(INTENT_DATA_NAME) ?: ""
+ }
+
+ fun initialize(
+ applicationContext: Context,
+ activityToBeLaunched: Class,
+ ) = Thread.setDefaultUncaughtExceptionHandler(
+ GlobalExceptionHandler(
+ applicationContext = applicationContext,
+ Thread.getDefaultUncaughtExceptionHandler() as Thread.UncaughtExceptionHandler,
+ activityToBeLaunched
+ )
+ )
+ }
+}
diff --git a/app/src/main/java/com/dede/android_eggs/main/BackPressedHandler.kt b/app/src/main/java/com/dede/android_eggs/views/main/BackPressedHandler.kt
similarity index 97%
rename from app/src/main/java/com/dede/android_eggs/main/BackPressedHandler.kt
rename to app/src/main/java/com/dede/android_eggs/views/main/BackPressedHandler.kt
index a25dfac8..6a41525f 100644
--- a/app/src/main/java/com/dede/android_eggs/main/BackPressedHandler.kt
+++ b/app/src/main/java/com/dede/android_eggs/views/main/BackPressedHandler.kt
@@ -1,4 +1,4 @@
-package com.dede.android_eggs.main
+package com.dede.android_eggs.views.main
import android.animation.ObjectAnimator
import android.annotation.SuppressLint
@@ -12,8 +12,8 @@ import android.view.ViewGroup
import android.view.ViewOutlineProvider
import android.window.BackEvent
import androidx.activity.BackEventCompat
+import androidx.activity.ComponentActivity
import androidx.activity.OnBackPressedCallback
-import androidx.appcompat.app.AppCompatActivity
import androidx.core.animation.doOnEnd
import androidx.core.animation.doOnStart
import androidx.core.content.withStyledAttributes
@@ -31,7 +31,7 @@ import kotlin.math.abs
import kotlin.math.max
import com.google.android.material.R as M3R
-class BackPressedHandler(private val host: AppCompatActivity) :
+class BackPressedHandler(private val host: ComponentActivity) :
OnBackPressedCallback(enabled = true), DefaultLifecycleObserver {
companion object {
diff --git a/app/src/main/java/com/dede/android_eggs/views/main/EasterEggLogoSensorMatrixConvert.kt b/app/src/main/java/com/dede/android_eggs/views/main/EasterEggLogoSensorMatrixConvert.kt
new file mode 100644
index 00000000..dd551a19
--- /dev/null
+++ b/app/src/main/java/com/dede/android_eggs/views/main/EasterEggLogoSensorMatrixConvert.kt
@@ -0,0 +1,83 @@
+package com.dede.android_eggs.views.main
+
+import android.animation.Animator
+import android.animation.ValueAnimator
+import android.graphics.Matrix
+import android.graphics.Rect
+import android.view.animation.LinearInterpolator
+import com.dede.android_eggs.util.OrientationAngleSensor
+import javax.inject.Inject
+import kotlin.math.abs
+import kotlin.math.max
+
+class EasterEggLogoSensorMatrixConvert @Inject constructor() :
+ OrientationAngleSensor.OnOrientationAnglesUpdate {
+
+ private val list = ArrayList()
+
+ fun register(listener: Listener) {
+ list.add(listener)
+ }
+
+ fun unregister(listener: Listener) {
+ list.remove(listener)
+ }
+
+ abstract class Listener(bounds: Rect) {
+
+ private val width = bounds.width() / 6f
+ private val height = bounds.height() / 6f
+
+ private val matrix = Matrix()
+
+ fun updateDegrees(cXDegrees: Float, cYDegrees: Float) {
+ // Swap coordinate systems
+ val dx = cYDegrees / 90f * width * -1f
+ val dy = cXDegrees / 90f * height
+ matrix.setTranslate(dx, dy)
+ onUpdateMatrix(matrix)
+ }
+
+ abstract fun onUpdateMatrix(matrix: Matrix)
+ }
+
+ private var lastXDegrees: Float = 0f
+ private var lastYDegrees: Float = 0f
+ private var animator: Animator? = null
+ private val interpolator = LinearInterpolator()
+
+ private fun Float.toRoundDegrees(): Float {
+ return ((Math.toDegrees(toDouble())) % 90f).toFloat()
+ }
+
+ private fun calculateAnimDegrees(old: Float, new: Float, fraction: Float): Float {
+ return old + (new - old) * fraction
+ }
+
+ override fun updateOrientationAngles(zAngle: Float, xAngle: Float, yAngle: Float) {
+ val xDegrees = xAngle.toRoundDegrees()// 俯仰角
+ val yDegrees = yAngle.toRoundDegrees()// 侧倾角
+ if (max(abs(lastXDegrees - xDegrees), abs(lastYDegrees - yDegrees)) < 5f) return
+
+ animator?.cancel()
+ val saveXDegrees = lastXDegrees
+ val saveYDegrees = lastYDegrees
+ animator = ValueAnimator.ofFloat(0f, 1f)
+ .setDuration(80)
+ .apply {
+ interpolator = this@EasterEggLogoSensorMatrixConvert.interpolator
+ addUpdateListener {
+ val fraction = it.animatedFraction
+ val cXDegrees = calculateAnimDegrees(saveXDegrees, xDegrees, fraction)
+ val cYDegrees = calculateAnimDegrees(saveYDegrees, yDegrees, fraction)
+
+ for (listener in list) {
+ listener.updateDegrees(cXDegrees, cYDegrees)
+ }
+ }
+ start()
+ }
+ lastYDegrees = yDegrees
+ lastXDegrees = xDegrees
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/dede/android_eggs/views/main/EasterEggsActivity.kt b/app/src/main/java/com/dede/android_eggs/views/main/EasterEggsActivity.kt
new file mode 100644
index 00000000..cbf9ab51
--- /dev/null
+++ b/app/src/main/java/com/dede/android_eggs/views/main/EasterEggsActivity.kt
@@ -0,0 +1,119 @@
+@file:OptIn(ExperimentalMaterial3Api::class)
+
+package com.dede.android_eggs.views.main
+
+import android.annotation.SuppressLint
+import android.content.Intent
+import android.os.Bundle
+import androidx.activity.compose.setContent
+import androidx.activity.enableEdgeToEdge
+import androidx.appcompat.app.AppCompatActivity
+import androidx.compose.material3.ExperimentalMaterial3Api
+import androidx.compose.material3.Scaffold
+import androidx.compose.material3.TopAppBarDefaults
+import androidx.compose.runtime.CompositionLocalProvider
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.saveable.rememberSaveable
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.input.nestedscroll.nestedScroll
+import com.dede.android_eggs.util.LocalEvent
+import com.dede.android_eggs.util.OrientationAngleSensor
+import com.dede.android_eggs.util.ThemeUtils
+import com.dede.android_eggs.views.main.compose.BottomSearchBar
+import com.dede.android_eggs.views.main.compose.EasterEggScreen
+import com.dede.android_eggs.views.main.compose.Konfetti
+import com.dede.android_eggs.views.main.compose.LocalEasterEggLogoSensor
+import com.dede.android_eggs.views.main.compose.LocalFragmentManager
+import com.dede.android_eggs.views.main.compose.LocalKonfettiState
+import com.dede.android_eggs.views.main.compose.MainTitleBar
+import com.dede.android_eggs.views.main.compose.Welcome
+import com.dede.android_eggs.views.main.compose.rememberKonfettiState
+import com.dede.android_eggs.views.settings.prefs.IconVisualEffectsPref
+import com.dede.android_eggs.views.theme.AppTheme
+import com.dede.basic.provider.BaseEasterEgg
+import dagger.hilt.android.AndroidEntryPoint
+import dagger.hilt.android.scopes.ActivityScoped
+import javax.inject.Inject
+
+@AndroidEntryPoint
+class EasterEggsActivity : AppCompatActivity() {
+
+ @Inject
+ lateinit var easterEggs: List<@JvmSuppressWildcards BaseEasterEgg>
+
+ @Inject
+ @ActivityScoped
+ lateinit var schemeHandler: SchemeHandler
+
+ private var orientationAngleSensor: OrientationAngleSensor? = null
+
+ @Inject
+ lateinit var sensor: EasterEggLogoSensorMatrixConvert
+
+ override fun onCreate(savedInstanceState: Bundle?) {
+ ThemeUtils.tryApplyOLEDTheme(this)
+ enableEdgeToEdge()
+ super.onCreate(savedInstanceState)
+
+ setContent {
+ val konfettiState = rememberKonfettiState()
+ val searchBarVisibleState = rememberSaveable { mutableStateOf(false) }
+ val searchFieldState = rememberSaveable { mutableStateOf("") }
+
+ val scrollBehavior = TopAppBarDefaults.pinnedScrollBehavior()
+ CompositionLocalProvider(
+ LocalFragmentManager provides supportFragmentManager,
+ LocalEasterEggLogoSensor provides sensor,
+ LocalKonfettiState provides konfettiState
+ ) {
+ AppTheme {
+ Scaffold(
+ topBar = {
+ MainTitleBar(
+ scrollBehavior = scrollBehavior,
+ searchBarVisibleState = searchBarVisibleState,
+ searchFieldState = searchFieldState,
+ )
+ },
+ modifier = Modifier
+ .nestedScroll(scrollBehavior.nestedScrollConnection),
+ bottomBar = {
+ BottomSearchBar(searchBarVisibleState, searchFieldState)
+ }
+ ) { contentPadding ->
+ EasterEggScreen(easterEggs, searchFieldState.value, contentPadding)
+ }
+ Welcome()
+ Konfetti(konfettiState)
+ }
+ }
+ }
+
+ handleOrientationAngleSensor(IconVisualEffectsPref.isEnable(this))
+ LocalEvent.receiver(this).register(IconVisualEffectsPref.ACTION_CHANGED) {
+ val enable = it.getBooleanExtra(IconVisualEffectsPref.EXTRA_VALUE, false)
+ handleOrientationAngleSensor(enable)
+ }
+
+ schemeHandler.handleIntent(intent)
+ }
+
+
+ private fun handleOrientationAngleSensor(enable: Boolean) {
+ val orientationAngleSensor = this.orientationAngleSensor
+ if (enable && orientationAngleSensor == null) {
+ this.orientationAngleSensor = OrientationAngleSensor(
+ this, this, sensor
+ )
+ } else if (!enable && orientationAngleSensor != null) {
+ orientationAngleSensor.destroy()
+ this.orientationAngleSensor = null
+ }
+ }
+
+ @SuppressLint("MissingSuperCall")
+ override fun onNewIntent(intent: Intent?) {
+ super.onNewIntent(intent)
+ schemeHandler.handleIntent(intent)
+ }
+}
diff --git a/app/src/main/java/com/dede/android_eggs/views/main/EggAdapterProvider.kt b/app/src/main/java/com/dede/android_eggs/views/main/EggAdapterProvider.kt
deleted file mode 100644
index 3660cacb..00000000
--- a/app/src/main/java/com/dede/android_eggs/views/main/EggAdapterProvider.kt
+++ /dev/null
@@ -1,80 +0,0 @@
-package com.dede.android_eggs.views.main
-
-import android.content.Context
-import androidx.recyclerview.widget.RecyclerView
-import com.dede.android_eggs.R
-import com.dede.android_eggs.main.entity.Egg
-import com.dede.android_eggs.main.entity.EggFilter
-import com.dede.android_eggs.main.entity.EggGroup
-import com.dede.android_eggs.main.holders.EggHolder
-import com.dede.android_eggs.main.holders.GroupHolder
-import com.dede.android_eggs.main.holders.PreviewHolder
-import com.dede.android_eggs.main.holders.WavyHolder
-import com.dede.android_eggs.ui.adapter.VAdapter
-import com.dede.android_eggs.ui.adapter.VType
-import com.dede.android_eggs.ui.adapter.addFooter
-import com.dede.android_eggs.ui.adapter.addHeader
-import com.dede.android_eggs.ui.adapter.addViewType
-import com.dede.android_eggs.ui.adapter.removeFooter
-import com.dede.android_eggs.ui.adapter.removeHeader
-import com.dede.android_eggs.ui.views.EasterEggFooterView
-import com.dede.android_eggs.ui.views.SnapshotGroupView
-import com.dede.android_eggs.views.settings.ActionBarMenuController
-import dagger.hilt.android.qualifiers.ActivityContext
-import javax.inject.Inject
-
-class EggAdapterProvider @Inject constructor(
- @ActivityContext private val context: Context,
- private val eggFilter: EggFilter
-) : EggFilter.OnFilterResults, ActionBarMenuController.OnSearchTextChangeListener {
-
- val adapter: RecyclerView.Adapter
- get() = vAdapter
-
- private val vAdapter: VAdapter
-
- private val snapshotView = SnapshotGroupView(context)
- private val footerView = EasterEggFooterView(context)
-
- init {
- eggFilter.onFilterResults = this
- vAdapter = VAdapter(eggFilter.eggList) {
- addHeader(snapshotView)
- addViewType(R.layout.item_easter_egg_layout)
- addViewType(R.layout.item_easter_egg_layout)
- addViewType(R.layout.item_easter_egg_layout)
- addViewType(R.layout.item_easter_egg_wavy)
- addFooter(footerView)
- }
- }
-
- fun indexOfByEggId(eggId: Int): Int {
- val position = eggFilter.eggList.indexOfFirst {
- when (it) {
- is Egg -> it.id == eggId
- is EggGroup -> it.child.find { c -> c.id == eggId } != null
- else -> false
- }
- }
- if (position == -1) {
- return -1
- }
- return position + vAdapter.headerFooterExt.headerCount
- }
-
- override fun publishResults(constraint: CharSequence, newList: List) {
- if (constraint.isEmpty()) {
- vAdapter.addHeader(snapshotView)
- vAdapter.addFooter(footerView)
- } else {
- vAdapter.removeHeader(snapshotView)
- vAdapter.removeFooter(footerView)
- }
- vAdapter.notifyDataSetChanged()
- }
-
- override fun onSearchTextChange(newText: String) {
- eggFilter.filter(newText)
- }
-
-}
\ No newline at end of file
diff --git a/app/src/main/java/com/dede/android_eggs/main/SchemeHandler.kt b/app/src/main/java/com/dede/android_eggs/views/main/SchemeHandler.kt
similarity index 90%
rename from app/src/main/java/com/dede/android_eggs/main/SchemeHandler.kt
rename to app/src/main/java/com/dede/android_eggs/views/main/SchemeHandler.kt
index 757326dc..99a246fd 100644
--- a/app/src/main/java/com/dede/android_eggs/main/SchemeHandler.kt
+++ b/app/src/main/java/com/dede/android_eggs/views/main/SchemeHandler.kt
@@ -1,10 +1,10 @@
-package com.dede.android_eggs.main
+package com.dede.android_eggs.views.main
import android.content.Context
import android.content.Intent
import android.net.Uri
import android.util.Log
-import com.dede.android_eggs.main.entity.Egg.Companion.toEgg
+import com.dede.android_eggs.main.EggActionHelp
import com.dede.basic.provider.EasterEgg
import dagger.hilt.android.qualifiers.ActivityContext
import javax.inject.Inject
@@ -49,7 +49,7 @@ class SchemeHandler @Inject constructor(@ActivityContext val context: Context) {
val level = levelStr.toIntOrNull() ?: return false
val egg = easterEggs.find { level in it.apiLevel }
if (egg != null) {
- EggActionHelp.launchEgg(context, egg.toEgg())
+ EggActionHelp.launchEgg(context, egg)
}
return egg != null
}
diff --git a/app/src/main/java/com/dede/android_eggs/views/main/StartupPage.kt b/app/src/main/java/com/dede/android_eggs/views/main/StartupPage.kt
deleted file mode 100644
index d810c8bf..00000000
--- a/app/src/main/java/com/dede/android_eggs/views/main/StartupPage.kt
+++ /dev/null
@@ -1,64 +0,0 @@
-package com.dede.android_eggs.views.main
-
-import android.content.Context
-import android.content.DialogInterface
-import android.widget.ImageView
-import androidx.core.content.edit
-import androidx.core.net.toUri
-import com.dede.android_eggs.R
-import com.dede.android_eggs.util.CustomTabsBrowser
-import com.dede.android_eggs.util.pref
-import com.dede.basic.createVectorDrawableCompat
-import com.google.android.material.dialog.MaterialAlertDialogBuilder
-
-class StartupPage(private val context: Context) : MaterialAlertDialogBuilder(context),
- DialogInterface.OnClickListener {
-
- companion object {
-
- private const val KEY = "key_welcome_status"
-
- fun show(context: Context) {
- val status = context.pref.getBoolean(KEY, false)
- if (status) return
- StartupPage(context)
- .show()
- }
- }
-
- init {
- setTitle(R.string.label_welcome)
- setMessage(R.string.summary_browse_privacy_policy)
- val view = ImageView(context).apply {
- adjustViewBounds = true
- setImageDrawable(context.createVectorDrawableCompat(R.drawable.img_welcome_poster))
- }
- setView(view)
- setNeutralButton(R.string.label_privacy_policy, this)
- setNegativeButton(android.R.string.cancel, this)
- setPositiveButton(R.string.action_agree, this)
- }
-
- override fun onClick(dialog: DialogInterface, which: Int) {
- when (which) {
- DialogInterface.BUTTON_POSITIVE -> {
- context.pref.edit {
- putBoolean(KEY, true)
- }
- }
-
- DialogInterface.BUTTON_NEGATIVE -> {
- }
-
- DialogInterface.BUTTON_NEUTRAL -> {
- CustomTabsBrowser.launchUrl(
- context,
- context.getString(R.string.url_privacy).toUri()
- )
- }
-
- else -> throw UnsupportedOperationException("Dialog which: $which")
- }
- }
-
-}
\ No newline at end of file
diff --git a/app/src/main/java/com/dede/android_eggs/views/main/compose/AndroidViews.kt b/app/src/main/java/com/dede/android_eggs/views/main/compose/AndroidViews.kt
new file mode 100644
index 00000000..fe76a470
--- /dev/null
+++ b/app/src/main/java/com/dede/android_eggs/views/main/compose/AndroidViews.kt
@@ -0,0 +1,43 @@
+package com.dede.android_eggs.views.main.compose
+
+import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.fillMaxWidth
+import androidx.compose.foundation.layout.height
+import androidx.compose.foundation.layout.padding
+import androidx.compose.material3.Card
+import androidx.compose.material3.MaterialTheme.shapes
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.platform.LocalInspectionMode
+import androidx.compose.ui.tooling.preview.Preview
+import androidx.compose.ui.unit.dp
+import androidx.compose.ui.viewinterop.AndroidView
+import com.dede.android_eggs.ui.views.SnapshotGroupView
+
+@Composable
+@Preview
+fun AndroidSnapshotView() {
+ val inspectionMode = LocalInspectionMode.current
+ if (inspectionMode) {
+ Box(
+ modifier = Modifier
+ .padding(horizontal = 12.dp)
+ .padding(top = 12.dp)
+ ) {
+ Card(
+ shape = shapes.extraLarge,
+ modifier = Modifier
+ .fillMaxWidth()
+ .height(200.dp)
+ ) {}
+ }
+ } else {
+ // has inject
+ AndroidView(
+ factory = { SnapshotGroupView(it) },
+ modifier = Modifier
+ .fillMaxWidth()
+ .padding(top = 12.dp)
+ )
+ }
+}
diff --git a/app/src/main/java/com/dede/android_eggs/views/main/compose/BottomSearchBar.kt b/app/src/main/java/com/dede/android_eggs/views/main/compose/BottomSearchBar.kt
new file mode 100644
index 00000000..7743e2da
--- /dev/null
+++ b/app/src/main/java/com/dede/android_eggs/views/main/compose/BottomSearchBar.kt
@@ -0,0 +1,184 @@
+@file:OptIn(ExperimentalAnimationApi::class)
+
+package com.dede.android_eggs.views.main.compose
+
+import androidx.activity.compose.PredictiveBackHandler
+import androidx.compose.animation.AnimatedVisibility
+import androidx.compose.animation.ExperimentalAnimationApi
+import androidx.compose.animation.core.animate
+import androidx.compose.animation.fadeIn
+import androidx.compose.animation.fadeOut
+import androidx.compose.animation.scaleIn
+import androidx.compose.animation.scaleOut
+import androidx.compose.animation.slideInVertically
+import androidx.compose.animation.slideOutVertically
+import androidx.compose.foundation.clickable
+import androidx.compose.foundation.interaction.MutableInteractionSource
+import androidx.compose.foundation.layout.fillMaxWidth
+import androidx.compose.foundation.layout.imePadding
+import androidx.compose.foundation.layout.navigationBarsPadding
+import androidx.compose.foundation.layout.padding
+import androidx.compose.foundation.shape.RoundedCornerShape
+import androidx.compose.foundation.text.KeyboardOptions
+import androidx.compose.material.icons.Icons
+import androidx.compose.material.icons.rounded.ArrowBack
+import androidx.compose.material.icons.rounded.Clear
+import androidx.compose.material.ripple.rememberRipple
+import androidx.compose.material3.Icon
+import androidx.compose.material3.MaterialTheme.colorScheme
+import androidx.compose.material3.Surface
+import androidx.compose.material3.Text
+import androidx.compose.material3.TextField
+import androidx.compose.material3.TextFieldDefaults
+import androidx.compose.material3.surfaceColorAtElevation
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.LaunchedEffect
+import androidx.compose.runtime.MutableState
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.mutableFloatStateOf
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.remember
+import androidx.compose.runtime.setValue
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.focus.FocusRequester
+import androidx.compose.ui.focus.focusRequester
+import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.graphics.Shape
+import androidx.compose.ui.graphics.TransformOrigin
+import androidx.compose.ui.graphics.graphicsLayer
+import androidx.compose.ui.platform.LocalSoftwareKeyboardController
+import androidx.compose.ui.res.stringResource
+import androidx.compose.ui.text.input.ImeAction
+import androidx.compose.ui.text.input.KeyboardType
+import androidx.compose.ui.tooling.preview.Preview
+import androidx.compose.ui.unit.dp
+import com.dede.android_eggs.R
+import kotlinx.coroutines.flow.catch
+
+@Composable
+@Preview
+fun BottomSearchBar(
+ visibleState: MutableState = mutableStateOf(true),
+ searchFieldState: MutableState = mutableStateOf(""),
+) {
+ var visible by visibleState
+ var backProgress by remember { mutableFloatStateOf(0f) }
+ PredictiveBackHandler(enabled = visible) { flow ->
+ flow.catch {
+ animate(backProgress, 0f) { value, _ ->
+ backProgress = value
+ }
+ }.collect { event ->
+ backProgress = event.progress
+ }
+ visible = false
+ }
+ LaunchedEffect(visible) {
+ if (visible) {
+ backProgress = 0f
+ }
+ }
+ AnimatedVisibility(
+ visible = visible,
+ enter = slideInVertically(initialOffsetY = { it }) + fadeIn(),
+ exit = slideOutVertically(targetOffsetY = { it }) + fadeOut(),
+ ) {
+ BottomSearchBar(
+ visible,
+ searchFieldState,
+ modifier = Modifier
+ .graphicsLayer(
+ scaleX = 1F - (0.1F * backProgress),
+ scaleY = 1F - (0.1F * backProgress),
+ transformOrigin = remember { TransformOrigin(0.5f, 1f) },
+ ),
+ shape = RoundedCornerShape(
+ topStart = (28 * backProgress).dp,
+ topEnd = (28 * backProgress).dp
+ ),
+ onClose = { visible = false },
+ )
+ }
+}
+
+@Composable
+private fun BottomSearchBar(
+ visible: Boolean,
+ searchField: MutableState,
+ modifier: Modifier,
+ shape: Shape,
+ onClose: () -> Unit,
+) {
+ var searchText by searchField
+ val keyboardController = LocalSoftwareKeyboardController.current
+ val focusRequester = remember { FocusRequester() }
+ LaunchedEffect(visible) {
+ if (visible) {
+ focusRequester.requestFocus()
+ keyboardController?.show()
+ } else {
+ keyboardController?.hide()
+ }
+ }
+ Surface(
+ modifier = Modifier
+ .then(modifier)
+ .imePadding(),
+ shape = shape,
+ color = colorScheme.surfaceColorAtElevation(4.dp),
+ contentColor = colorScheme.onSurface,
+ tonalElevation = 4.dp,
+ shadowElevation = 4.dp,
+ ) {
+ TextField(modifier = Modifier
+ .fillMaxWidth()
+ .focusRequester(focusRequester)
+ .navigationBarsPadding()
+ .padding(horizontal = 16.dp, vertical = 10.dp),
+ value = searchText,
+ onValueChange = {
+ searchText = it
+ },
+ placeholder = {
+ Text(text = stringResource(R.string.label_search_hint))
+ },
+ keyboardOptions = KeyboardOptions(
+ keyboardType = KeyboardType.Ascii, imeAction = ImeAction.Search
+ ),
+ singleLine = true,
+ shape = RoundedCornerShape(50),
+ colors = TextFieldDefaults.colors(
+ focusedIndicatorColor = Color.Transparent,
+ disabledIndicatorColor = Color.Transparent,
+ unfocusedIndicatorColor = Color.Transparent,
+ ),
+ leadingIcon = {
+ Icon(imageVector = Icons.Rounded.ArrowBack,
+ contentDescription = null,
+ modifier = Modifier.clickable(
+ interactionSource = remember { MutableInteractionSource() },
+ indication = rememberRipple(bounded = false)
+ ) {
+ searchText = ""
+ onClose.invoke()
+ keyboardController?.hide()
+ })
+ },
+ trailingIcon = {
+ AnimatedVisibility(
+ visible = searchText.isNotBlank(),
+ enter = fadeIn() + scaleIn(),
+ exit = fadeOut() + scaleOut(),
+ ) {
+ Icon(imageVector = Icons.Rounded.Clear,
+ contentDescription = null,
+ modifier = Modifier.clickable(
+ interactionSource = remember { MutableInteractionSource() },
+ indication = rememberRipple(bounded = false)
+ ) {
+ searchText = ""
+ })
+ }
+ })
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/dede/android_eggs/views/main/compose/EasterEggGroupSelector.kt b/app/src/main/java/com/dede/android_eggs/views/main/compose/EasterEggGroupSelector.kt
new file mode 100644
index 00000000..e8bc6714
--- /dev/null
+++ b/app/src/main/java/com/dede/android_eggs/views/main/compose/EasterEggGroupSelector.kt
@@ -0,0 +1,109 @@
+package com.dede.android_eggs.views.main.compose
+
+import android.app.Activity
+import android.view.View
+import android.widget.FrameLayout
+import androidx.compose.foundation.clickable
+import androidx.compose.material3.DropdownMenu
+import androidx.compose.material3.DropdownMenuItem
+import androidx.compose.material3.MaterialTheme.shapes
+import androidx.compose.material3.MaterialTheme.typography
+import androidx.compose.material3.Text
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.remember
+import androidx.compose.runtime.setValue
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.draw.clip
+import androidx.compose.ui.geometry.Rect
+import androidx.compose.ui.layout.boundsInParent
+import androidx.compose.ui.layout.onGloballyPositioned
+import androidx.compose.ui.layout.positionInWindow
+import androidx.compose.ui.platform.LocalContext
+import androidx.compose.ui.unit.dp
+import com.dede.android_eggs.main.EasterEggHelp
+import com.dede.android_eggs.main.EggActionHelp
+import com.dede.android_eggs.util.getActivity
+import com.dede.basic.provider.BaseEasterEgg
+import com.dede.basic.provider.EasterEggGroup
+import kotlin.math.roundToInt
+
+
+@Composable
+fun Modifier.withEasterEggGroupSelector(
+ base: BaseEasterEgg,
+ onSelected: (index: Int) -> Unit,
+): Modifier {
+ if (base !is EasterEggGroup) {
+ return this
+ }
+
+ // todo support shape https://issuetracker.google.com/issues/200529605
+// val context = LocalContext.current
+// var expanded by remember { mutableStateOf(false) }
+// DropdownMenu(
+// expanded = expanded,
+// onDismissRequest = {
+// expanded = false
+// },
+// modifier = Modifier.clip(shapes.extraLarge),
+// ) {
+// base.eggs.forEachIndexed { index, egg ->
+// val menuTitle = EasterEggHelp.VersionFormatter.create(egg.apiLevel, egg.nicknameRes)
+// .format(context)
+// DropdownMenuItem(
+// leadingIcon = {
+// EasterEggLogo(egg = egg, 30.dp)
+// },
+// text = {
+// Text(
+// text = menuTitle,
+// style = typography.bodyLarge,
+// )
+// },
+// onClick = {
+// onSelected.invoke(index)
+// expanded = false
+// }
+// )
+// }
+// }
+// return clickable {
+// expanded = true
+// }
+
+ val activity: Activity? = LocalContext.current.getActivity()
+ var popupAnchorBounds by remember { mutableStateOf(Rect.Zero) }
+ return clickable {
+ // DropdownMenu style error, use native popup
+ if (activity == null || popupAnchorBounds.isEmpty) return@clickable
+ val parent = activity.findViewById(android.R.id.content)
+ val fakeView = View(activity).apply {
+ x = popupAnchorBounds.left
+ y = popupAnchorBounds.top
+ }
+ val params = FrameLayout.LayoutParams(
+ popupAnchorBounds.width.roundToInt(),
+ popupAnchorBounds.height.roundToInt()
+ )
+ parent.addView(fakeView, params)
+ EggActionHelp.showEggGroupMenu(
+ activity,
+ fakeView,
+ base,
+ onSelected = onSelected,
+ onDismiss = {
+ parent.removeView(fakeView)
+ })
+ }.onGloballyPositioned {
+ val position = it.positionInWindow()
+ val bounds = it.boundsInParent()
+ popupAnchorBounds = Rect(
+ position.x,
+ position.y,
+ position.x + bounds.width,
+ position.y + bounds.height
+ )
+ }
+}
diff --git a/app/src/main/java/com/dede/android_eggs/views/main/compose/EasterEggItem.kt b/app/src/main/java/com/dede/android_eggs/views/main/compose/EasterEggItem.kt
new file mode 100644
index 00000000..49475928
--- /dev/null
+++ b/app/src/main/java/com/dede/android_eggs/views/main/compose/EasterEggItem.kt
@@ -0,0 +1,352 @@
+@file:OptIn(ExperimentalMaterial3Api::class, ExperimentalFoundationApi::class)
+
+package com.dede.android_eggs.views.main.compose
+
+import android.view.HapticFeedbackConstants
+import androidx.compose.animation.AnimatedContent
+import androidx.compose.animation.animateContentSize
+import androidx.compose.animation.core.animate
+import androidx.compose.animation.core.tween
+import androidx.compose.animation.fadeIn
+import androidx.compose.animation.fadeOut
+import androidx.compose.animation.scaleIn
+import androidx.compose.animation.scaleOut
+import androidx.compose.animation.togetherWith
+import androidx.compose.foundation.ExperimentalFoundationApi
+import androidx.compose.foundation.combinedClickable
+import androidx.compose.foundation.gestures.Orientation
+import androidx.compose.foundation.gestures.draggable
+import androidx.compose.foundation.gestures.rememberDraggableState
+import androidx.compose.foundation.layout.Arrangement
+import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.Row
+import androidx.compose.foundation.layout.fillMaxWidth
+import androidx.compose.foundation.layout.offset
+import androidx.compose.foundation.layout.padding
+import androidx.compose.foundation.layout.size
+import androidx.compose.material.icons.Icons
+import androidx.compose.material.icons.rounded.AppShortcut
+import androidx.compose.material.icons.rounded.KeyboardArrowDown
+import androidx.compose.material.icons.rounded.SwipeLeft
+import androidx.compose.material.icons.rounded.SwipeRight
+import androidx.compose.material3.Card
+import androidx.compose.material3.CardDefaults
+import androidx.compose.material3.ExperimentalMaterial3Api
+import androidx.compose.material3.Icon
+import androidx.compose.material3.MaterialTheme.colorScheme
+import androidx.compose.material3.MaterialTheme.shapes
+import androidx.compose.material3.MaterialTheme.typography
+import androidx.compose.material3.Text
+import androidx.compose.material3.surfaceColorAtElevation
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.LaunchedEffect
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.mutableFloatStateOf
+import androidx.compose.runtime.mutableIntStateOf
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.remember
+import androidx.compose.runtime.setValue
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.draw.clip
+import androidx.compose.ui.layout.onSizeChanged
+import androidx.compose.ui.platform.LocalContext
+import androidx.compose.ui.platform.LocalLayoutDirection
+import androidx.compose.ui.platform.LocalView
+import androidx.compose.ui.res.stringResource
+import androidx.compose.ui.text.SpanStyle
+import androidx.compose.ui.text.buildAnnotatedString
+import androidx.compose.ui.text.font.FontStyle
+import androidx.compose.ui.text.withStyle
+import androidx.compose.ui.tooling.preview.Preview
+import androidx.compose.ui.unit.IntOffset
+import androidx.compose.ui.unit.LayoutDirection
+import androidx.compose.ui.unit.dp
+import com.dede.android_eggs.R
+import com.dede.android_eggs.main.EasterEggHelp
+import com.dede.android_eggs.main.EggActionHelp
+import com.dede.android_eggs.ui.views.ViscousFluidInterpolator
+import com.dede.basic.provider.BaseEasterEgg
+import com.dede.basic.provider.EasterEgg
+import com.dede.basic.provider.EasterEggGroup
+import kotlin.math.abs
+import kotlin.math.min
+import kotlin.math.roundToInt
+
+
+@Composable
+@Preview(showBackground = true)
+fun EasterEggItem(
+ base: BaseEasterEgg = EasterEggHelp.previewEasterEggs().first(),
+ enableItemAnim: Boolean = false,
+) {
+ val context = LocalContext.current
+
+ var groupIndex by remember { mutableIntStateOf(0) }
+ val egg = when (base) {
+ is EasterEgg -> base
+ is EasterEggGroup -> base.eggs[groupIndex]
+ else -> throw UnsupportedOperationException("Unsupported type: ${base.javaClass}")
+ }
+ val supportShortcut = remember(egg) { EggActionHelp.isSupportShortcut(egg) }
+ var swipeProgress by remember { mutableFloatStateOf(0f) }
+
+ EasterEggItemSwipe(
+ floor = {
+ EasterEggItemFloor(egg, supportShortcut, swipeProgress)
+ },
+ content = {
+ EasterEggItemContent(egg, base, supportShortcut, enableItemAnim) {
+ groupIndex = it
+ }
+ },
+ supportShortcut = supportShortcut,
+ onSwipe = {
+ swipeProgress = it
+ },
+ addShortcut = {
+ EggActionHelp.addShortcut(context, egg)
+ },
+ )
+}
+
+@Composable
+fun EasterEggItemSwipe(
+ floor: @Composable () -> Unit,
+ content: @Composable () -> Unit,
+ supportShortcut: Boolean,
+ addShortcut: () -> Unit,
+ onSwipe: (p: Float) -> Unit,
+) {
+ var released by remember { mutableStateOf(false) }
+ var offsetX by remember { mutableFloatStateOf(0f) }
+ var triggerOffsetX by remember { mutableFloatStateOf(0f) }
+ var needTrigger by remember { mutableStateOf(false) }
+
+ val intercept = ViscousFluidInterpolator.getInstance()
+
+ fun callbackSwipeProgress(offsetX: Float) {
+ if (!supportShortcut) return
+ val p = if (triggerOffsetX == 0f) 0f else
+ min(abs(offsetX) / triggerOffsetX, 1f)
+ onSwipe.invoke(p)
+ }
+
+ LaunchedEffect(released) {
+ if (released) {
+ animate(offsetX, 0f, animationSpec = tween(300)) { value, _ ->
+ offsetX = value
+ callbackSwipeProgress(value)
+ }
+ }
+ }
+
+ val view = LocalView.current
+ Box(
+ contentAlignment = Alignment.Center,
+// modifier = Modifier.padding(4.dp)
+ ) {
+ floor()
+ Box(
+ modifier = Modifier
+ .onSizeChanged { triggerOffsetX = it.width / 5f * 2 }
+ .offset { IntOffset(offsetX.roundToInt(), 0) }
+ .draggable(
+ reverseDirection = LocalLayoutDirection.current == LayoutDirection.Rtl,
+ orientation = Orientation.Horizontal,
+ state = rememberDraggableState { delta ->
+ val r = intercept.getInterpolation(
+ 1f - abs(offsetX / (triggerOffsetX * 1.24f))
+ )
+ offsetX += delta * min(r, 1f)
+ if (supportShortcut) {
+ if (!needTrigger && (-offsetX) >= triggerOffsetX) {
+ needTrigger = true
+ view.performHapticFeedback(HapticFeedbackConstants.CLOCK_TICK)
+ }
+ callbackSwipeProgress(offsetX)
+ }
+ },
+ onDragStarted = {
+ released = false
+ },
+ onDragStopped = {
+ if (supportShortcut && needTrigger && abs(offsetX) >= triggerOffsetX) {
+ addShortcut.invoke()
+ }
+ released = true
+ needTrigger = false
+ }
+ ),
+ ) {
+ content()
+ }
+ }
+}
+
+@Composable
+@Preview
+fun EasterEggItemContent(
+ egg: EasterEgg = EasterEggHelp.previewEasterEggs().first(),
+ base: BaseEasterEgg = egg,
+ supportShortcut: Boolean = true,
+ enableItemAnim: Boolean = false,
+ onSelected: ((index: Int) -> Unit)? = null,
+) {
+ val context = LocalContext.current
+ val isGroup = base is EasterEggGroup
+ val androidVersion = remember(egg) {
+ EasterEggHelp.VersionFormatter.create(egg.apiLevel, egg.nicknameRes)
+ .format(context)
+ }
+ Card(
+ colors = CardDefaults.cardColors(containerColor = colorScheme.surfaceColorAtElevation(2.dp)),
+ shape = shapes.extraLarge,
+ modifier = Modifier
+ .padding(horizontal = 12.dp)
+ .fillMaxWidth()
+ ) {
+ Box(
+ modifier = Modifier
+ .clip(shapes.extraLarge)
+ .combinedClickable(
+ onClick = {
+ EggActionHelp.launchEgg(context, egg)
+ },
+ onLongClick = {
+ if (supportShortcut) {
+ EggActionHelp.addShortcut(context, egg)
+ }
+ }
+ )
+ ) {
+ Column(
+ modifier = Modifier.padding(horizontal = 22.dp, vertical = 18.dp)
+ ) {
+ Row(modifier = Modifier.fillMaxWidth()) {
+ Box(
+ modifier = Modifier
+ .padding(end = 10.dp, bottom = 6.dp)
+ .weight(1f, true)
+ ) {
+ Text(
+ text = stringResource(egg.nameRes),
+ style = typography.headlineSmall,
+ modifier = if (enableItemAnim) Modifier.animateContentSize() else Modifier,
+ )
+ }
+ EasterEggLogo(egg, sensor = true)
+ }
+ Row(
+ modifier = Modifier
+ .clip(shapes.extraSmall)
+ .withEasterEggGroupSelector(base) {
+ onSelected?.invoke(it)
+ },
+ verticalAlignment = Alignment.CenterVertically
+ ) {
+ Text(
+ text = androidVersion,
+ style = typography.bodyMedium,
+ modifier = if (enableItemAnim) Modifier.animateContentSize() else Modifier,
+ )
+ if (isGroup) {
+ Icon(
+ modifier = Modifier
+ .padding(start = 4.dp)
+ .size(22.dp),
+ imageVector = Icons.Rounded.KeyboardArrowDown,
+ contentDescription = stringResource(R.string.pref_title_language_more)
+ )
+ }
+ }
+ }
+ }
+ }
+}
+
+@Composable
+@Preview(showBackground = true)
+fun EasterEggItemFloor(
+ egg: EasterEgg = EasterEggHelp.previewEasterEggs().first(),
+ enableShortcut: Boolean = true,
+ swipeProgress: Float = 0f,
+) {
+ val content = LocalContext.current
+ val androidVersion = remember(egg) {
+ EasterEggHelp.VersionFormatter.create(egg.apiLevel)
+ .format(content)
+ }
+ val apiVersion = remember(egg) {
+ EasterEggHelp.ApiLevelFormatter.create(egg.apiLevel)
+ .format(content)
+ }
+ Row(
+ modifier = Modifier
+ .padding(horizontal = 28.dp)
+ .fillMaxWidth(),
+ horizontalArrangement = Arrangement.SpaceBetween,
+ verticalAlignment = Alignment.CenterVertically
+ ) {
+ Row(verticalAlignment = Alignment.CenterVertically) {
+ EasterEggLogo(egg = egg, 36.dp)
+ Text(
+ text = buildAnnotatedString {
+ append(androidVersion)
+ append("\n")
+ withStyle(SpanStyle(fontStyle = FontStyle.Italic)) {
+ append(apiVersion)
+ }
+ },
+ modifier = Modifier.padding(start = 8.dp),
+ maxLines = 2,
+ style = typography.bodyMedium
+ )
+ }
+ if (enableShortcut) {
+ Row(
+ verticalAlignment = Alignment.CenterVertically,
+ modifier = Modifier.offset(x = (swipeProgress * -14).dp)
+ ) {
+ Text(
+ text = stringResource(R.string.label_add_shortcut),
+ modifier = Modifier.padding(end = 4.dp),
+ maxLines = 2,
+ style = typography.bodyMedium
+ )
+ ShortcutIcon(swipeProgress >= 1f)
+ }
+ }
+ }
+}
+
+@Composable
+private fun ShortcutIcon(showShortcut: Boolean = false) {
+ AnimatedContent(
+ targetState = showShortcut,
+ transitionSpec = {
+ scaleIn() + fadeIn() togetherWith scaleOut() + fadeOut()
+ },
+ label = "ShortcutIcon"
+ ) {
+ if (it) {
+ Icon(
+ modifier = Modifier.size(30.dp),
+ imageVector = Icons.Rounded.AppShortcut,
+ contentDescription = stringResource(R.string.label_add_shortcut)
+ )
+ } else {
+ val swipeIcon = if (LocalLayoutDirection.current == LayoutDirection.Rtl) {
+ Icons.Rounded.SwipeRight
+ } else {
+ Icons.Rounded.SwipeLeft
+ }
+ Icon(
+ modifier = Modifier.size(30.dp),
+ imageVector = swipeIcon,
+ contentDescription = stringResource(R.string.label_add_shortcut)
+ )
+ }
+ }
+}
diff --git a/app/src/main/java/com/dede/android_eggs/views/main/compose/EasterEggLogo.kt b/app/src/main/java/com/dede/android_eggs/views/main/compose/EasterEggLogo.kt
new file mode 100644
index 00000000..66711f2b
--- /dev/null
+++ b/app/src/main/java/com/dede/android_eggs/views/main/compose/EasterEggLogo.kt
@@ -0,0 +1,87 @@
+package com.dede.android_eggs.views.main.compose
+
+import android.annotation.SuppressLint
+import android.graphics.Matrix
+import androidx.compose.foundation.Image
+import androidx.compose.foundation.layout.size
+import androidx.compose.foundation.lazy.grid.GridCells
+import androidx.compose.foundation.lazy.grid.LazyVerticalGrid
+import androidx.compose.foundation.lazy.grid.items
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.DisposableEffect
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.remember
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.platform.LocalContext
+import androidx.compose.ui.res.painterResource
+import androidx.compose.ui.res.stringResource
+import androidx.compose.ui.tooling.preview.Preview
+import androidx.compose.ui.unit.Dp
+import androidx.compose.ui.unit.dp
+import com.dede.android_eggs.main.EasterEggHelp
+import com.dede.android_eggs.ui.drawables.AlterableAdaptiveIconDrawable
+import com.dede.android_eggs.util.LocalEvent
+import com.dede.android_eggs.views.main.EasterEggLogoSensorMatrixConvert
+import com.dede.android_eggs.views.settings.prefs.IconShapePref
+import com.dede.basic.provider.EasterEgg
+import com.google.accompanist.drawablepainter.rememberDrawablePainter
+
+
+@Preview(widthDp = 200)
+@Composable
+fun PreviewEasterEggLogo() {
+ val easterEggs = EasterEggHelp.previewEasterEggs()
+ LazyVerticalGrid(columns = GridCells.Fixed(3)) {
+ items(easterEggs) {
+ EasterEggLogo(egg = it)
+ }
+ }
+}
+
+@SuppressLint("NewApi")
+@Composable
+fun EasterEggLogo(egg: EasterEgg, size: Dp = 44.dp, sensor: Boolean = false) {
+ if (egg.supportAdaptiveIcon) {
+ val context = LocalContext.current
+
+ val maskPath: String? by remember { mutableStateOf(IconShapePref.getMaskPath(context)) }
+ val drawable = remember(maskPath, egg.iconRes, context.theme) {
+ AlterableAdaptiveIconDrawable(context, egg.iconRes, maskPath)
+ }
+ LocalEvent.receiver().register(IconShapePref.ACTION_CHANGED) {
+ val newMaskPath = it.getStringExtra(IconShapePref.EXTRA_ICON_SHAPE_PATH)
+ if (newMaskPath != null) {
+ drawable.setMaskPath(newMaskPath)
+ }
+ }
+ val drawablePainter = rememberDrawablePainter(drawable = drawable)
+ val sensorGroup = LocalEasterEggLogoSensor.currentOutInspectionMode
+ if (sensor && sensorGroup != null) {
+ val listener = remember(drawable.bounds) {
+ object : EasterEggLogoSensorMatrixConvert.Listener(drawable.bounds) {
+ override fun onUpdateMatrix(matrix: Matrix) {
+ drawable.setForegroundMatrix(matrix)
+ }
+ }
+ }
+ DisposableEffect(drawable) {
+ sensorGroup.register(listener)
+ onDispose {
+ sensorGroup.unregister(listener)
+ }
+ }
+ }
+ Image(
+ painter = drawablePainter,
+ contentDescription = stringResource(egg.nicknameRes),
+ modifier = Modifier.size(size)
+ )
+ } else {
+ Image(
+ painter = painterResource(egg.iconRes),
+ contentDescription = stringResource(egg.nicknameRes),
+ modifier = Modifier.size(size)
+ )
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/dede/android_eggs/views/main/compose/EasterEggScreen.kt b/app/src/main/java/com/dede/android_eggs/views/main/compose/EasterEggScreen.kt
new file mode 100644
index 00000000..2cdcfc47
--- /dev/null
+++ b/app/src/main/java/com/dede/android_eggs/views/main/compose/EasterEggScreen.kt
@@ -0,0 +1,136 @@
+package com.dede.android_eggs.views.main.compose
+
+import android.content.Context
+import androidx.compose.animation.Crossfade
+import androidx.compose.foundation.layout.Arrangement
+import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.PaddingValues
+import androidx.compose.foundation.layout.fillMaxSize
+import androidx.compose.foundation.layout.fillMaxWidth
+import androidx.compose.foundation.layout.padding
+import androidx.compose.foundation.layout.size
+import androidx.compose.foundation.layout.sizeIn
+import androidx.compose.foundation.lazy.LazyColumn
+import androidx.compose.foundation.lazy.items
+import androidx.compose.material.icons.Icons
+import androidx.compose.material.icons.rounded.SearchOff
+import androidx.compose.material3.Icon
+import androidx.compose.material3.MaterialTheme.colorScheme
+import androidx.compose.material3.Surface
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.remember
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.platform.LocalContext
+import androidx.compose.ui.tooling.preview.Preview
+import androidx.compose.ui.unit.dp
+import com.dede.android_eggs.R
+import com.dede.android_eggs.main.EasterEggHelp
+import com.dede.android_eggs.views.main.EasterEggModules
+import com.dede.basic.provider.BaseEasterEgg
+import com.dede.basic.provider.EasterEgg
+
+@Composable
+@Preview(showBackground = true)
+fun EasterEggScreen(
+ easterEggs: List = EasterEggHelp.previewEasterEggs(),
+ searchFilter: String = "",
+ contentPadding: PaddingValues = PaddingValues(0.dp),
+) {
+ val context = LocalContext.current
+ val pureEasterEggs = remember(easterEggs) {
+ EasterEggModules.providePureEasterEggList(easterEggs)
+ }
+ val searchText = remember(searchFilter) {
+ searchFilter.trim().uppercase()
+ }
+ val searchMode = searchText.isNotBlank()
+ val currentList = remember(searchText, searchMode, easterEggs, pureEasterEggs) {
+ if (searchMode) {
+ filterEasterEggs(context, pureEasterEggs, searchText)
+ } else {
+ easterEggs
+ }
+ }
+ Surface(color = colorScheme.surface) {
+ Box(
+ modifier = Modifier.fillMaxSize(),
+ contentAlignment = Alignment.TopCenter,
+ ) {
+ Crossfade(
+ targetState = currentList.isEmpty(),
+ modifier = Modifier.sizeIn(maxWidth = 560.dp),
+ label = "EasterEggList",
+ ) { isEmpty ->
+ if (isEmpty) {
+ Box(
+ contentAlignment = Alignment.TopCenter,
+ modifier = Modifier
+ .fillMaxWidth()
+ .padding(contentPadding)
+ .padding(top = 32.dp)
+ ) {
+ Icon(
+ imageVector = Icons.Rounded.SearchOff,
+ contentDescription = null,
+ tint = colorScheme.onBackground,
+ modifier = Modifier.size(108.dp)
+ )
+ }
+ } else {
+ LazyColumn(
+ contentPadding = contentPadding,
+ verticalArrangement = Arrangement.spacedBy(12.dp)
+ ) {
+ if (searchMode) {
+ items(items = currentList) {
+ EasterEggItem(it, enableItemAnim = true)
+ }
+ } else {
+ item("snapshot") {
+ AndroidSnapshotView()
+ }
+ item("wavy1") {
+ Wavy(res = R.drawable.ic_wavy_line)
+ }
+ items(items = currentList) {
+ EasterEggItem(it, enableItemAnim = false)
+ }
+ item("wavy2") {
+ Wavy(res = R.drawable.ic_wavy_line)
+ }
+ item("footer") {
+ ProjectDescription()
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+}
+
+private fun filterEasterEggs(
+ context: Context,
+ pureEasterEggs: List,
+ searchText: String,
+): List {
+ val isApiLevel = Regex("^\\d{1,2}$").matches(searchText)
+
+ fun EasterEgg.matchVersionName(version: String): Boolean {
+ val containsStart = EasterEggHelp.getVersionNameByApiLevel(apiLevel.first)
+ .contains(version, true)
+ return if (apiLevel.first == apiLevel.last) {
+ containsStart
+ } else {
+ containsStart || EasterEggHelp.getVersionNameByApiLevel(apiLevel.last)
+ .contains(version, true)
+ }
+ }
+ return pureEasterEggs.filter {
+ context.getString(it.nameRes).contains(searchText, true) ||
+ context.getString(it.nicknameRes).contains(searchText, true) ||
+ it.matchVersionName(searchText) ||
+ (isApiLevel && it.apiLevel.contains(searchText.toIntOrNull() ?: -1))
+ }
+}
diff --git a/app/src/main/java/com/dede/android_eggs/views/main/compose/Image.kt b/app/src/main/java/com/dede/android_eggs/views/main/compose/Image.kt
new file mode 100644
index 00000000..10257bba
--- /dev/null
+++ b/app/src/main/java/com/dede/android_eggs/views/main/compose/Image.kt
@@ -0,0 +1,92 @@
+package com.dede.android_eggs.views.main.compose
+
+import android.graphics.drawable.Drawable
+import androidx.annotation.DrawableRes
+import androidx.compose.foundation.Image
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.size
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.remember
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.layout.ContentScale
+import androidx.compose.ui.platform.LocalContext
+import androidx.compose.ui.res.stringArrayResource
+import androidx.compose.ui.tooling.preview.Preview
+import androidx.compose.ui.unit.dp
+import com.dede.android_eggs.R
+import com.dede.android_eggs.ui.drawables.AlterableAdaptiveIconDrawable
+import com.dede.basic.requireDrawable
+import com.google.accompanist.drawablepainter.rememberDrawablePainter
+
+@Composable
+@Preview
+fun PreviewImage() {
+ val context = LocalContext.current
+ Column(horizontalAlignment = Alignment.CenterHorizontally) {
+ Image(res = R.mipmap.ic_launcher_round, contentDescription = null)
+
+ val maskPath = stringArrayResource(id = R.array.icon_shape_override_paths).last()
+ val drawable = remember(context.theme, maskPath) {
+ AlterableAdaptiveIconDrawable(context, R.mipmap.ic_launcher, maskPath)
+ }
+ Image(
+ drawable = drawable, contentDescription = null,
+ modifier = Modifier.size(56.dp)
+ )
+ }
+}
+
+@Composable
+fun Image(
+ @DrawableRes res: Int,
+ contentDescription: String?,
+ modifier: Modifier = Modifier,
+ alignment: Alignment = Alignment.Center,
+ contentScale: ContentScale = ContentScale.Fit,
+) {
+ val context = LocalContext.current
+ val drawable = remember(res, context.theme) {
+ context.requireDrawable(res)
+ }
+ Image(
+ drawable = drawable,
+ contentDescription = contentDescription,
+ modifier = modifier,
+ alignment = alignment,
+ contentScale = contentScale,
+ )
+}
+
+@Composable
+fun Image(
+ drawable: Drawable,
+ contentDescription: String?,
+ modifier: Modifier = Modifier,
+ alignment: Alignment = Alignment.Center,
+ contentScale: ContentScale = ContentScale.Fit,
+) {
+ val drawablePainter = rememberDrawablePainter(drawable = drawable)
+ Image(
+ painter = drawablePainter,
+ contentDescription = contentDescription,
+ modifier = modifier,
+ alignment = alignment,
+ contentScale = contentScale,
+ )
+}
+
+//private class DrawablePainter(private val drawable: Drawable) : Painter() {
+//
+// override val intrinsicSize: Size
+// get() = Size(
+// drawable.intrinsicWidth.toFloat(),
+// drawable.intrinsicHeight.toFloat()
+// )
+//
+// override fun DrawScope.onDraw() {
+// val rect = size.toRect().toAndroidRectF().toRect()
+// drawable.bounds = rect
+// drawable.draw(drawContext.canvas.nativeCanvas)
+// }
+//}
\ No newline at end of file
diff --git a/app/src/main/java/com/dede/android_eggs/views/main/compose/Konfetti.kt b/app/src/main/java/com/dede/android_eggs/views/main/compose/Konfetti.kt
new file mode 100644
index 00000000..e7787890
--- /dev/null
+++ b/app/src/main/java/com/dede/android_eggs/views/main/compose/Konfetti.kt
@@ -0,0 +1,89 @@
+package com.dede.android_eggs.views.main.compose
+
+import androidx.compose.foundation.layout.fillMaxSize
+import androidx.compose.material3.MaterialTheme
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.MutableState
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.remember
+import androidx.compose.runtime.setValue
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.graphics.toArgb
+import com.dede.android_eggs.views.theme.blend
+import nl.dionsegijn.konfetti.compose.KonfettiView
+import nl.dionsegijn.konfetti.compose.OnParticleSystemUpdateListener
+import nl.dionsegijn.konfetti.core.Angle
+import nl.dionsegijn.konfetti.core.Party
+import nl.dionsegijn.konfetti.core.PartySystem
+import nl.dionsegijn.konfetti.core.Position
+import nl.dionsegijn.konfetti.core.Spread
+import nl.dionsegijn.konfetti.core.emitter.Emitter
+import java.util.concurrent.TimeUnit
+
+@Composable
+fun rememberKonfettiState(visible: Boolean = false): MutableState {
+ return remember { mutableStateOf(visible) }
+}
+
+@Composable
+fun Konfetti(
+ state: MutableState = rememberKonfettiState(),
+ modifier: Modifier = Modifier,
+ primary: Color = MaterialTheme.colorScheme.primary
+) {
+ var visible by state
+ if (!visible) {
+ return
+ }
+ val listener = remember(state) {
+ object : OnParticleSystemUpdateListener {
+ override fun onParticleSystemEnded(system: PartySystem, activeSystems: Int) {
+ visible = false
+ }
+ }
+ }
+ KonfettiView(
+ modifier = Modifier
+ .fillMaxSize()
+ .then(modifier),
+ parties = remember { particles(primary.toArgb()) },
+ updateListener = listener
+ )
+}
+
+private val defaultColors = listOf(0xfce18a, 0xff726d, 0xf4306d, 0xb48def)
+
+private fun particles(primary: Int) = listOf(
+ Party(
+ speed = 5f,
+ maxSpeed = 15f,
+ damping = 0.9f,
+ angle = Angle.BOTTOM,
+ spread = Spread.ROUND,
+ colors = defaultColors.map { it.blend(primary) },
+ emitter = Emitter(duration = 2, TimeUnit.SECONDS).perSecond(100),
+ position = Position.Relative(0.0, 0.0).between(Position.Relative(1.0, 0.0)),
+ ),
+ Party(
+ speed = 10f,
+ maxSpeed = 40f,
+ damping = 0.9f,
+ angle = Angle.RIGHT - 55,
+ spread = 60,
+ colors = defaultColors.map { it.blend(primary) },
+ emitter = Emitter(duration = 2, TimeUnit.SECONDS).perSecond(100),
+ position = Position.Relative(0.0, 1.0)
+ ),
+ Party(
+ speed = 10f,
+ maxSpeed = 40f,
+ damping = 0.9f,
+ angle = Angle.RIGHT - 125,
+ spread = 60,
+ colors = defaultColors.map { it.blend(primary) },
+ emitter = Emitter(duration = 2, TimeUnit.SECONDS).perSecond(100),
+ position = Position.Relative(1.0, 1.0)
+ )
+)
diff --git a/app/src/main/java/com/dede/android_eggs/views/main/compose/LocalProvider.kt b/app/src/main/java/com/dede/android_eggs/views/main/compose/LocalProvider.kt
new file mode 100644
index 00000000..8b3050bc
--- /dev/null
+++ b/app/src/main/java/com/dede/android_eggs/views/main/compose/LocalProvider.kt
@@ -0,0 +1,42 @@
+@file:Suppress("NOTHING_TO_INLINE")
+
+package com.dede.android_eggs.views.main.compose
+
+import android.util.Log
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.ProvidableCompositionLocal
+import androidx.compose.runtime.ReadOnlyComposable
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.staticCompositionLocalOf
+import androidx.compose.ui.platform.LocalInspectionMode
+import androidx.fragment.app.FragmentManager
+import com.dede.android_eggs.views.main.EasterEggLogoSensorMatrixConvert
+
+private inline fun noLocalProvidedFor(name: String): Nothing {
+ throw IllegalStateException("CompositionLocal %s not present".format(name))
+}
+
+private const val TAG = "LocalProvider"
+
+private inline fun noLocalProvidedForLog(name: String) {
+ Log.i(TAG, "CompositionLocal %s not present".format(name))
+}
+
+val ProvidableCompositionLocal.currentOutInspectionMode: T?
+ @ReadOnlyComposable
+ @Composable
+ get() = if (LocalInspectionMode.current) null else current
+
+val LocalFragmentManager = staticCompositionLocalOf {
+ noLocalProvidedFor("LocalFragmentManager")
+}
+
+val LocalEasterEggLogoSensor = staticCompositionLocalOf {
+ noLocalProvidedFor("LocalEasterEggLogoSensor")
+}
+
+val LocalKonfettiState = staticCompositionLocalOf {
+ noLocalProvidedForLog("LocalKonfettiState")
+ mutableStateOf(false)
+}
+
diff --git a/app/src/main/java/com/dede/android_eggs/views/main/compose/MainTitleBar.kt b/app/src/main/java/com/dede/android_eggs/views/main/compose/MainTitleBar.kt
new file mode 100644
index 00000000..f5850ec7
--- /dev/null
+++ b/app/src/main/java/com/dede/android_eggs/views/main/compose/MainTitleBar.kt
@@ -0,0 +1,140 @@
+@file:OptIn(ExperimentalMaterial3Api::class)
+
+package com.dede.android_eggs.views.main.compose
+
+import androidx.compose.animation.AnimatedVisibility
+import androidx.compose.animation.core.animate
+import androidx.compose.animation.core.tween
+import androidx.compose.animation.fadeIn
+import androidx.compose.animation.fadeOut
+import androidx.compose.animation.scaleIn
+import androidx.compose.animation.scaleOut
+import androidx.compose.material.icons.Icons
+import androidx.compose.material.icons.rounded.Search
+import androidx.compose.material.icons.rounded.Settings
+import androidx.compose.material3.CenterAlignedTopAppBar
+import androidx.compose.material3.ExperimentalMaterial3Api
+import androidx.compose.material3.Icon
+import androidx.compose.material3.IconButton
+import androidx.compose.material3.Text
+import androidx.compose.material3.TopAppBarDefaults.pinnedScrollBehavior
+import androidx.compose.material3.TopAppBarScrollBehavior
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.LaunchedEffect
+import androidx.compose.runtime.MutableState
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.mutableFloatStateOf
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.remember
+import androidx.compose.runtime.rememberCoroutineScope
+import androidx.compose.runtime.setValue
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.draw.rotate
+import androidx.compose.ui.res.stringResource
+import androidx.compose.ui.text.style.TextOverflow
+import androidx.compose.ui.tooling.preview.Preview
+import androidx.fragment.app.FragmentManager
+import com.dede.android_eggs.R
+import com.dede.android_eggs.views.settings.SettingsFragment
+import kotlinx.coroutines.delay
+import kotlinx.coroutines.launch
+
+
+private const val TAG_SETTINGS = "Settings"
+
+@Composable
+@Preview
+fun MainTitleBar(
+ scrollBehavior: TopAppBarScrollBehavior = pinnedScrollBehavior(),
+ searchBarVisibleState: MutableState = mutableStateOf(false),
+ searchFieldState: MutableState = mutableStateOf(""),
+) {
+ val fm: FragmentManager? = LocalFragmentManager.currentOutInspectionMode
+ var searchBarVisible by searchBarVisibleState
+ var searchText by searchFieldState
+
+ val fragment = fm?.findFragmentByTag(TAG_SETTINGS) as? SettingsFragment
+ var showSettings by remember { mutableStateOf(fragment != null) }
+ var settingRotate by remember { mutableFloatStateOf(0f) }
+
+ val onSlideCallback = remember {
+ { p: Float -> settingRotate = p * 360f }
+ }
+ val onDismissCallback = remember {
+ { showSettings = false }
+ }
+ if (fragment != null) {
+ fragment.onSlide = onSlideCallback
+ fragment.onDismiss = onDismissCallback
+ }
+
+ LaunchedEffect(showSettings) {
+ if (showSettings) {
+ animate(0f, 360f, animationSpec = tween(500)) { value, _ ->
+ settingRotate = value
+ }
+ }
+ }
+
+ val scope = rememberCoroutineScope()
+
+ fun showSettings() {
+ scope.launch {
+ if (searchBarVisible) {
+ // hide searchBar
+ searchText = ""
+ searchBarVisible = false
+ // await searchBar dismiss
+ delay(200)
+ }
+ if (fm == null) return@launch
+ SettingsFragment().apply {
+ onSlide = onSlideCallback
+ onDismiss = onDismissCallback
+ }.show(fm, TAG_SETTINGS)
+ showSettings = true
+ }
+ }
+
+ CenterAlignedTopAppBar(
+ scrollBehavior = scrollBehavior,
+ title = {
+ Text(
+ text = stringResource(R.string.app_name),
+ maxLines = 1,
+ overflow = TextOverflow.Ellipsis,
+ )
+ },
+ actions = {
+ AnimatedVisibility(
+ visible = !searchBarVisible,
+ enter = fadeIn() + scaleIn(),
+ exit = fadeOut() + scaleOut(),
+ ) {
+ IconButton(
+ onClick = {
+ // show searchBar
+ searchBarVisible = true
+ },
+ ) {
+ Icon(
+ imageVector = Icons.Rounded.Search,
+ contentDescription = stringResource(R.string.label_search_hint),
+ )
+ }
+ }
+
+ IconButton(
+ onClick = {
+ showSettings()
+ },
+ ) {
+ Icon(
+ imageVector = Icons.Rounded.Settings,
+ contentDescription = stringResource(R.string.label_settings),
+ modifier = Modifier.rotate(settingRotate)
+ )
+ }
+ }
+ )
+}
diff --git a/app/src/main/java/com/dede/android_eggs/views/main/compose/ProjectDescription.kt b/app/src/main/java/com/dede/android_eggs/views/main/compose/ProjectDescription.kt
new file mode 100644
index 00000000..01bd131f
--- /dev/null
+++ b/app/src/main/java/com/dede/android_eggs/views/main/compose/ProjectDescription.kt
@@ -0,0 +1,220 @@
+@file:OptIn(ExperimentalLayoutApi::class)
+
+package com.dede.android_eggs.views.main.compose
+
+import android.content.Intent
+import androidx.annotation.StringRes
+import androidx.compose.foundation.clickable
+import androidx.compose.foundation.interaction.MutableInteractionSource
+import androidx.compose.foundation.layout.Arrangement
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.ExperimentalLayoutApi
+import androidx.compose.foundation.layout.FlowRow
+import androidx.compose.foundation.layout.Row
+import androidx.compose.foundation.layout.Spacer
+import androidx.compose.foundation.layout.fillMaxWidth
+import androidx.compose.foundation.layout.height
+import androidx.compose.foundation.layout.padding
+import androidx.compose.foundation.layout.size
+import androidx.compose.material.ripple.rememberRipple
+import androidx.compose.material3.MaterialTheme.colorScheme
+import androidx.compose.material3.MaterialTheme.shapes
+import androidx.compose.material3.MaterialTheme.typography
+import androidx.compose.material3.Text
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.remember
+import androidx.compose.runtime.setValue
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.draw.clip
+import androidx.compose.ui.platform.LocalContext
+import androidx.compose.ui.res.stringResource
+import androidx.compose.ui.text.font.FontStyle
+import androidx.compose.ui.text.style.TextDecoration
+import androidx.compose.ui.tooling.preview.Preview
+import androidx.compose.ui.unit.dp
+import androidx.core.net.toUri
+import androidx.fragment.app.FragmentManager
+import com.dede.android_eggs.BuildConfig
+import com.dede.android_eggs.R
+import com.dede.android_eggs.util.CustomTabsBrowser
+import com.dede.android_eggs.util.createChooser
+import com.dede.android_eggs.views.settings.component.ComponentManagerFragment
+import com.dede.android_eggs.views.timeline.AndroidTimelineFragment
+
+
+@Composable
+private fun ChipItem(
+ @StringRes textRes: Int,
+ separator: Boolean = true,
+ onClick: () -> Unit,
+) {
+ Row(
+ verticalAlignment = Alignment.CenterVertically,
+ ) {
+ Text(
+ text = stringResource(textRes),
+ style = typography.bodyMedium,
+ color = colorScheme.secondary,
+ modifier = Modifier
+ .clip(shapes.extraSmall)
+ .clickable(onClick = onClick)
+ )
+ if (separator) {
+ Text(
+ text = stringResource(id = R.string.char_separator),
+ style = typography.bodyMedium,
+ color = colorScheme.secondary,
+ modifier = Modifier.padding(start = 6.dp)
+ )
+ }
+ }
+}
+
+@Composable
+private fun ChipItem2(
+ @StringRes textRes: Int,
+ onClick: () -> Unit,
+) {
+ Text(
+ text = stringResource(textRes),
+ style = typography.titleSmall,
+ color = colorScheme.secondary,
+ modifier = Modifier
+ .clip(shapes.extraSmall)
+ .clickable(onClick = onClick)
+ )
+}
+
+@Preview(showBackground = true)
+@Composable
+fun ProjectDescription() {
+ val context = LocalContext.current
+ val fm: FragmentManager? = LocalFragmentManager.currentOutInspectionMode
+ var konfettiState by LocalKonfettiState.current
+// var timelineVisible = remember { mutableStateOf(false) }
+
+ fun openCustomTab(@StringRes uri: Int) {
+ CustomTabsBrowser.launchUrl(context, context.getString(uri).toUri())
+ }
+
+ fun openBrowser(uri: String) {
+ CustomTabsBrowser.launchUrlByBrowser(context, uri.toUri())
+ }
+
+ // todo support BackHandler https://issuetracker.google.com/issues/308510945
+// AndroidTimelineSheet(timelineVisible)
+
+ Column(
+ modifier = Modifier
+ .fillMaxWidth()
+ .padding(horizontal = 16.dp)
+ .then(Modifier.padding(bottom = 30.dp))
+ ) {
+ Row(
+ verticalAlignment = Alignment.CenterVertically,
+ modifier = Modifier.padding(horizontal = 4.dp)
+ ) {
+ Image(
+ res = R.mipmap.ic_launcher_round,
+ modifier = Modifier
+ .size(50.dp)
+ .clickable(
+ interactionSource = remember { MutableInteractionSource() },
+ indication = rememberRipple(bounded = false),
+ ) {
+ konfettiState = true
+ },
+ contentDescription = stringResource(id = R.string.app_name)
+ )
+ Column(modifier = Modifier.padding(start = 12.dp)) {
+ Text(
+ text = stringResource(
+ R.string.label_version,
+ BuildConfig.VERSION_NAME,
+ BuildConfig.VERSION_CODE
+ ),
+ style = typography.titleSmall
+ )
+ Spacer(modifier = Modifier.height(4.dp))
+ Text(
+ text = BuildConfig.GIT_HASH,
+ style = typography.bodySmall,
+ fontStyle = FontStyle.Italic,
+ textDecoration = TextDecoration.Underline,
+ modifier = Modifier
+ .clip(shapes.extraSmall)
+ .clickable {
+ val commitId =
+ context.getString(R.string.url_github_commit, BuildConfig.GIT_HASH)
+ CustomTabsBrowser.launchUrl(context, commitId.toUri())
+ }
+ )
+ }
+ }
+ Text(
+ text = stringResource(R.string.label_project_desc),
+ modifier = Modifier.padding(top = 20.dp),
+ style = typography.bodyMedium
+ )
+ FlowRow(
+ modifier = Modifier
+ .padding(top = 20.dp)
+ .fillMaxWidth(),
+ horizontalArrangement = Arrangement.spacedBy(6.dp),
+ verticalArrangement = Arrangement.spacedBy(8.dp)
+ ) {
+ ChipItem(R.string.label_github) {
+ openCustomTab(R.string.url_github)
+ }
+ ChipItem(R.string.label_translation) {
+ openCustomTab(R.string.url_translation)
+ }
+ ChipItem(R.string.label_timeline) {
+ AndroidTimelineFragment.show(fm ?: return@ChipItem)
+// timelineVisible.value = true
+ }
+ ChipItem(R.string.label_component_manager) {
+ ComponentManagerFragment.show(fm ?: return@ChipItem)
+ }
+ ChipItem(R.string.label_star) {
+ val uri = context.getString(R.string.url_market_detail, context.packageName)
+ openBrowser(uri)
+ }
+ ChipItem(R.string.label_donate) {
+ openCustomTab(R.string.url_sponsor)
+ }
+ ChipItem(R.string.label_share) {
+ val target = Intent(Intent.ACTION_SEND)
+ .putExtra(Intent.EXTRA_TEXT, context.getString(R.string.url_share))
+ .setType("text/plain")
+ val intent = context.createChooser(target)
+ context.startActivity(intent)
+ }
+ ChipItem(R.string.label_beta, false) {
+ openCustomTab(R.string.url_beta)
+ }
+ }
+ Wavy(res = R.drawable.ic_wavy_line_1, true, colorScheme.secondaryContainer)
+ FlowRow(
+ modifier = Modifier.fillMaxWidth(),
+ horizontalArrangement = Arrangement.spacedBy(16.dp),
+ verticalArrangement = Arrangement.spacedBy(8.dp)
+ ) {
+ Text(
+ text = stringResource(R.string.app_name),
+ style = typography.titleSmall,
+ )
+ ChipItem2(R.string.label_privacy_policy) {
+ openCustomTab(R.string.url_privacy)
+ }
+ ChipItem2(R.string.label_license) {
+ openCustomTab(R.string.url_license)
+ }
+ ChipItem2(R.string.label_email) {
+ openBrowser(context.getString(R.string.url_mail))
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/dede/android_eggs/views/main/compose/Wavy.kt b/app/src/main/java/com/dede/android_eggs/views/main/compose/Wavy.kt
new file mode 100644
index 00000000..a1cf53f1
--- /dev/null
+++ b/app/src/main/java/com/dede/android_eggs/views/main/compose/Wavy.kt
@@ -0,0 +1,59 @@
+package com.dede.android_eggs.views.main.compose
+
+import androidx.compose.foundation.Image
+import androidx.compose.foundation.layout.fillMaxWidth
+import androidx.compose.foundation.layout.padding
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.remember
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.graphics.toArgb
+import androidx.compose.ui.layout.ContentScale
+import androidx.compose.ui.platform.LocalContext
+import androidx.compose.ui.res.painterResource
+import androidx.compose.ui.tooling.preview.Preview
+import androidx.compose.ui.unit.dp
+import com.dede.android_eggs.R
+import com.dede.android_eggs.util.createRepeatWavyDrawable
+
+@Preview(showBackground = true)
+@Composable
+fun PreviewWavyRepeat() {
+ Wavy(R.drawable.ic_wavy_line_1, true)
+}
+
+@Preview(showBackground = true)
+@Composable
+fun PreviewWavy() {
+ Wavy(R.drawable.ic_wavy_line)
+}
+
+
+@Composable
+fun Wavy(res: Int, repeat: Boolean = false, tint: Color? = null) {
+ val modifier = Modifier
+ .fillMaxWidth()
+ .padding(vertical = 26.dp)
+ if (repeat) {
+ val context = LocalContext.current
+ val drawable = remember(res, context.theme) {
+ createRepeatWavyDrawable(context, res).apply {
+ if (tint != null) {
+ setTint(tint.toArgb())
+ }
+ }
+ }
+ Image(
+ drawable = drawable,
+ contentDescription = null,
+ contentScale = ContentScale.FillBounds,
+ modifier = modifier
+ )
+ } else {
+ Image(
+ painter = painterResource(id = res),
+ contentDescription = null,
+ modifier = modifier
+ )
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/dede/android_eggs/views/main/compose/Welcome.kt b/app/src/main/java/com/dede/android_eggs/views/main/compose/Welcome.kt
new file mode 100644
index 00000000..38d3c497
--- /dev/null
+++ b/app/src/main/java/com/dede/android_eggs/views/main/compose/Welcome.kt
@@ -0,0 +1,87 @@
+package com.dede.android_eggs.views.main.compose
+
+import androidx.compose.foundation.Image
+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.material3.TextButton
+import androidx.compose.material3.AlertDialog
+import androidx.compose.material3.Text
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.remember
+import androidx.compose.runtime.setValue
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.platform.LocalContext
+import androidx.compose.ui.res.painterResource
+import androidx.compose.ui.res.stringResource
+import androidx.compose.ui.tooling.preview.Preview
+import androidx.core.content.edit
+import androidx.core.net.toUri
+import com.dede.android_eggs.R
+import com.dede.android_eggs.util.CustomTabsBrowser
+import com.dede.android_eggs.util.pref
+
+private const val KEY = "key_welcome_status"
+
+@Composable
+@Preview
+fun Welcome() {
+ val context = LocalContext.current
+ var visible by remember {
+ val showed = context.pref.getBoolean(KEY, false)
+ mutableStateOf(!showed)
+// mutableStateOf(true)
+ }
+ if (!visible) {
+ return
+ }
+ var konfettiState by LocalKonfettiState.current
+ AlertDialog(
+ title = {
+ Text(text = stringResource(R.string.label_welcome))
+ },
+ text = {
+ Column {
+ Text(text = stringResource(R.string.summary_browse_privacy_policy))
+ Image(
+ painter = painterResource(R.drawable.img_welcome_poster),
+ contentDescription = null,
+ modifier = Modifier.fillMaxWidth()
+ )
+ }
+ },
+ onDismissRequest = {
+ visible = false
+ },
+ confirmButton = {
+ Row {
+ TextButton(onClick = {
+ CustomTabsBrowser.launchUrl(
+ context,
+ context.getString(R.string.url_privacy).toUri()
+ )
+ }) {
+ Text(text = stringResource(R.string.label_privacy_policy))
+ }
+ Spacer(modifier = Modifier.weight(1f))
+ TextButton(onClick = {
+ visible = false
+ }) {
+ Text(text = stringResource(android.R.string.cancel))
+ }
+ TextButton(onClick = {
+ visible = false
+ context.pref.edit {
+ putBoolean(KEY, true)
+ }
+ konfettiState = true
+ }) {
+ Text(text = stringResource(R.string.action_agree))
+ }
+ }
+ },
+ )
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/dede/android_eggs/views/placeholder/PlaceholderActivity.kt b/app/src/main/java/com/dede/android_eggs/views/placeholder/PlaceholderActivity.kt
new file mode 100644
index 00000000..646f7aec
--- /dev/null
+++ b/app/src/main/java/com/dede/android_eggs/views/placeholder/PlaceholderActivity.kt
@@ -0,0 +1,120 @@
+package com.dede.android_eggs.views.placeholder
+
+import android.os.Bundle
+import androidx.activity.compose.setContent
+import androidx.activity.enableEdgeToEdge
+import androidx.appcompat.app.AppCompatActivity
+import androidx.compose.animation.AnimatedVisibility
+import androidx.compose.animation.animateContentSize
+import androidx.compose.animation.core.MutableTransitionState
+import androidx.compose.animation.core.tween
+import androidx.compose.animation.fadeIn
+import androidx.compose.animation.scaleIn
+import androidx.compose.foundation.Image
+import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.fillMaxSize
+import androidx.compose.foundation.layout.imePadding
+import androidx.compose.foundation.layout.size
+import androidx.compose.material3.Scaffold
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.remember
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.platform.LocalContext
+import androidx.compose.ui.res.stringResource
+import androidx.compose.ui.tooling.preview.Preview
+import androidx.compose.ui.unit.dp
+import com.dede.android_eggs.R
+import com.dede.android_eggs.ui.drawables.AlterableAdaptiveIconDrawable
+import com.dede.android_eggs.util.LocalEvent
+import com.dede.android_eggs.util.ThemeUtils
+import com.dede.android_eggs.views.settings.prefs.DynamicColorPref
+import com.dede.android_eggs.views.settings.prefs.NightModePref
+import com.dede.android_eggs.views.theme.AppTheme
+import com.google.accompanist.drawablepainter.rememberDrawablePainter
+import dagger.hilt.android.AndroidEntryPoint
+import javax.inject.Inject
+import kotlin.random.Random
+
+@AndroidEntryPoint
+class PlaceholderActivity : AppCompatActivity() {
+
+ @Inject
+ lateinit var iconRes: IntArray
+
+ override fun onCreate(savedInstanceState: Bundle?) {
+ ThemeUtils.tryApplyOLEDTheme(this)
+ enableEdgeToEdge()
+ super.onCreate(savedInstanceState)
+
+ setContent {
+ AppTheme {
+ @Suppress("UnusedMaterial3ScaffoldPaddingParameter")
+ Scaffold {
+ Placeholder(randomRes(), randomPath())
+ }
+ }
+ }
+
+ with(LocalEvent.receiver(this)) {
+ register(NightModePref.ACTION_NIGHT_MODE_CHANGED) {
+ recreate()
+ }
+ register(DynamicColorPref.ACTION_DYNAMIC_COLOR_CHANGED) {
+ recreate()
+ }
+ }
+
+ }
+
+ private fun randomRes(): Int {
+ val array = iconRes
+ val index = Random.nextInt(array.size)
+ return array[index]
+ }
+
+ private fun randomPath(): String {
+ val array = resources.getStringArray(R.array.icon_shape_override_paths)
+ // 排除第一个
+ val index = Random.nextInt(array.size - 1) + 1
+ return array[index]
+ }
+
+}
+
+@Composable
+fun Placeholder(res: Int, mask: String? = null) {
+ val context = LocalContext.current
+ val drawable = remember(res, mask, context.theme) {
+ AlterableAdaptiveIconDrawable(context, res, mask)
+ }
+ val painter = rememberDrawablePainter(drawable = drawable)
+ Box(
+ contentAlignment = Alignment.Center,
+ modifier = Modifier
+ .fillMaxSize()
+ .imePadding()
+ ) {
+ AnimatedVisibility(
+ visibleState = remember { MutableTransitionState(false) }
+ .apply { targetState = true },
+ enter = scaleIn(
+ initialScale = 0.3f,
+ animationSpec = tween(500, delayMillis = 100)
+ ) + fadeIn(animationSpec = tween(500, delayMillis = 100)),
+ ) {
+ Image(
+ modifier = Modifier.size(56.dp),
+ painter = painter,
+ contentDescription = stringResource(R.string.app_name),
+ )
+ }
+ }
+}
+
+@Preview
+@Composable
+fun PreviewPlaceholder() {
+ Placeholder(R.mipmap.ic_launcher_round)
+}
+
diff --git a/app/src/main/java/com/dede/android_eggs/views/settings/ActionBarMenuController.kt b/app/src/main/java/com/dede/android_eggs/views/settings/ActionBarMenuController.kt
deleted file mode 100644
index e1c1ba94..00000000
--- a/app/src/main/java/com/dede/android_eggs/views/settings/ActionBarMenuController.kt
+++ /dev/null
@@ -1,88 +0,0 @@
-package com.dede.android_eggs.views.settings
-
-import android.animation.ObjectAnimator
-import android.os.Bundle
-import android.view.Menu
-import android.view.MenuInflater
-import android.view.MenuItem
-import androidx.appcompat.widget.SearchView
-import androidx.core.view.MenuProvider
-import androidx.fragment.app.FragmentActivity
-import com.dede.android_eggs.R
-import com.dede.android_eggs.ui.Icons
-import com.dede.android_eggs.ui.drawables.FontIconsDrawable
-import com.google.android.material.R as M3R
-
-
-class ActionBarMenuController(private val activity: FragmentActivity) : MenuProvider {
-
- companion object {
- private const val FRAGMENT_TAG = "Settings"
- }
-
- interface OnSearchTextChangeListener {
- fun onSearchTextChange(newText: String)
- }
-
- private lateinit var settingsIcon: FontIconsDrawable
-
- var onSearchTextChangeListener: OnSearchTextChangeListener? = null
-
- fun onCreate(savedInstanceState: Bundle?) {
- activity.addMenuProvider(this, activity)
- if (savedInstanceState != null) {
- tryBindSettingsPage()
- }
- }
-
- private fun tryBindSettingsPage() {
- val fm = activity.supportFragmentManager
- val settingsFragment = fm.findFragmentByTag(FRAGMENT_TAG) as? SettingsFragment
- if (settingsFragment != null) {
- settingsFragment.onSlide = this::onSlide
- }
- }
-
- private fun onSlide(offset: Float) {
- if (!::settingsIcon.isInitialized) return
- settingsIcon.setRotate(360f * offset)
- }
-
- override fun onCreateMenu(menu: Menu, menuInflater: MenuInflater) {
- menuInflater.inflate(R.menu.menu_settings, menu)
- settingsIcon =
- FontIconsDrawable(activity, Icons.Rounded.settings, M3R.attr.colorControlNormal, 24f)
- menu.findItem(R.id.menu_settings).icon = settingsIcon
-
- val searchView = menu.findItem(R.id.menu_search).actionView as? SearchView
- if (searchView != null) {// NPE ???
- searchView.queryHint = activity.getText(R.string.label_search_hint)
- searchView.setOnQueryTextListener(object : SearchView.OnQueryTextListener {
- override fun onQueryTextSubmit(query: String): Boolean {
- return true
- }
-
- override fun onQueryTextChange(newText: String): Boolean {
- onSearchTextChangeListener?.onSearchTextChange(newText)
- return true
- }
- })
- }
- }
-
- override fun onMenuItemSelected(menuItem: MenuItem): Boolean {
- when (menuItem.itemId) {
- R.id.menu_settings -> {
- SettingsFragment().apply {
- onSlide = this@ActionBarMenuController::onSlide
- }.show(activity.supportFragmentManager, FRAGMENT_TAG)
- ObjectAnimator.ofFloat(settingsIcon, "rotate", 0f, 360f)
- .setDuration(500)
- .start()
- }
-
- else -> return false
- }
- return true
- }
-}
\ No newline at end of file
diff --git a/app/src/main/java/com/dede/android_eggs/views/settings/SettingsFragment.kt b/app/src/main/java/com/dede/android_eggs/views/settings/SettingsFragment.kt
index 54aeaa69..9e027f1c 100644
--- a/app/src/main/java/com/dede/android_eggs/views/settings/SettingsFragment.kt
+++ b/app/src/main/java/com/dede/android_eggs/views/settings/SettingsFragment.kt
@@ -2,6 +2,7 @@ package com.dede.android_eggs.views.settings
import android.app.Dialog
import android.content.Context
+import android.content.DialogInterface
import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
@@ -25,9 +26,16 @@ class SettingsFragment : BottomSheetDialogFragment(R.layout.fragment_settings) {
var onSlide: ((offset: Float) -> Unit)? = null
+ var onPreDismiss: (() -> Unit)? = null
+
+ var onDismiss: (() -> Unit)? = null
+
private var lastSlideOffset: Float = -1f
private val callback = object : BottomSheetBehavior.BottomSheetCallback() {
override fun onStateChanged(bottomSheet: View, newState: Int) {
+ if (newState == BottomSheetBehavior.STATE_SETTLING) {
+ onPreDismiss?.invoke()
+ }
}
override fun onSlide(bottomSheet: View, slideOffset: Float) {
@@ -39,6 +47,11 @@ class SettingsFragment : BottomSheetDialogFragment(R.layout.fragment_settings) {
private val binding by viewBinding(FragmentSettingsBinding::bind)
+ override fun onDismiss(dialog: DialogInterface) {
+ onDismiss?.invoke()
+ super.onDismiss(dialog)
+ }
+
override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
val dialog = super.onCreateDialog(savedInstanceState) as BottomSheetDialog
EdgeUtils.applyEdge(dialog.window)
diff --git a/app/src/main/java/com/dede/android_eggs/views/settings/component/ComponentManagerFragment.kt b/app/src/main/java/com/dede/android_eggs/views/settings/component/ComponentManagerFragment.kt
index 274b2852..c56915fa 100644
--- a/app/src/main/java/com/dede/android_eggs/views/settings/component/ComponentManagerFragment.kt
+++ b/app/src/main/java/com/dede/android_eggs/views/settings/component/ComponentManagerFragment.kt
@@ -10,7 +10,7 @@ import androidx.preference.SwitchPreferenceCompat
import coil.imageLoader
import coil.request.ImageRequest
import com.dede.android_eggs.R
-import com.dede.android_eggs.main.entity.Egg.VersionFormatter
+import com.dede.android_eggs.main.EasterEggHelp.VersionFormatter
import com.dede.android_eggs.ui.Icons
import com.dede.android_eggs.ui.drawables.FontIconsDrawable
import com.dede.android_eggs.util.EdgeUtils
@@ -68,7 +68,7 @@ class ComponentManagerFragment : BottomSheetDialogFragment(R.layout.fragment_com
.build()
context.imageLoader.enqueue(request)
setTitle(component.nameRes)
- val formatter = VersionFormatter.create(component.nicknameRes, component.apiLevel)
+ val formatter = VersionFormatter.create(component.apiLevel, component.nicknameRes)
summary = formatter.format(context)
isEnabled = component.isSupported()
if (isEnabled) {
diff --git a/app/src/main/java/com/dede/android_eggs/views/settings/prefs/DynamicColorPref.kt b/app/src/main/java/com/dede/android_eggs/views/settings/prefs/DynamicColorPref.kt
index e9c6a36b..7c4ffe7f 100644
--- a/app/src/main/java/com/dede/android_eggs/views/settings/prefs/DynamicColorPref.kt
+++ b/app/src/main/java/com/dede/android_eggs/views/settings/prefs/DynamicColorPref.kt
@@ -3,9 +3,11 @@ package com.dede.android_eggs.views.settings.prefs
import android.app.Activity
import android.app.Application
import android.content.Context
+import androidx.activity.ComponentActivity
import androidx.appcompat.app.AppCompatActivity
import com.dede.android_eggs.R
import com.dede.android_eggs.ui.Icons
+import com.dede.android_eggs.util.LocalEvent
import com.dede.android_eggs.views.settings.SettingPref
import com.dede.android_eggs.views.settings.SettingPref.Op.Companion.isEnable
import com.google.android.material.color.DynamicColors
@@ -20,9 +22,19 @@ class DynamicColorPref : SettingPref(
Op(Op.ON, titleRes = R.string.preference_on, iconUnicode = Icons.Rounded.palette),
Op(Op.OFF, titleRes = R.string.preference_off)
),
- if (DynamicColors.isDynamicColorAvailable()) Op.ON else Op.OFF
+ DEFAULT
), DynamicColors.Precondition, DynamicColors.OnAppliedCallback {
+ companion object {
+
+ const val ACTION_DYNAMIC_COLOR_CHANGED = "ACTION_DYNAMIC_COLOR_CHANGED"
+
+ private val DEFAULT = if (DynamicColors.isDynamicColorAvailable()) Op.ON else Op.OFF
+ fun isDynamicEnable(context: Context): Boolean {
+ return DynamicColorPref().getValue(context, DEFAULT) == Op.ON
+ }
+ }
+
override val titleRes: Int
get() = R.string.pref_title_dynamic_color
override val enable: Boolean
@@ -32,6 +44,7 @@ class DynamicColorPref : SettingPref(
private var isOptionOn = false
override fun apply(context: Context, option: Op) {
+ val old = isOptionOn
isOptionOn = option.isEnable()
if (isOptionOn && !isApplied) {
DynamicColors.applyToActivitiesIfAvailable(
@@ -44,6 +57,9 @@ class DynamicColorPref : SettingPref(
isApplied = true
}
recreateActivityIfPossible(context)
+ if (old != isOptionOn) {
+ LocalEvent.poster(context).post(ACTION_DYNAMIC_COLOR_CHANGED)
+ }
}
override fun shouldApplyDynamicColors(activity: Activity, theme: Int): Boolean {
diff --git a/app/src/main/java/com/dede/android_eggs/views/settings/prefs/IconShapePref.kt b/app/src/main/java/com/dede/android_eggs/views/settings/prefs/IconShapePref.kt
index 01fee6fc..195db2c1 100644
--- a/app/src/main/java/com/dede/android_eggs/views/settings/prefs/IconShapePref.kt
+++ b/app/src/main/java/com/dede/android_eggs/views/settings/prefs/IconShapePref.kt
@@ -8,6 +8,7 @@ import android.graphics.drawable.BitmapDrawable
import android.graphics.drawable.Drawable
import androidx.core.graphics.applyCanvas
import androidx.core.graphics.createBitmap
+import androidx.core.os.bundleOf
import com.dede.android_eggs.R
import com.dede.android_eggs.ui.drawables.AlterableAdaptiveIconDrawable
import com.dede.android_eggs.util.LocalEvent
@@ -32,6 +33,7 @@ class IconShapePref : SettingPref(
) {
companion object {
const val ACTION_CHANGED = "com.dede.easter_eggs.IconShapeChanged"
+ const val EXTRA_ICON_SHAPE_PATH = "extra_icon_shape_path"
private fun iconShapeOp(index: Int): Op {
return Op(index).apply {
@@ -72,8 +74,9 @@ class IconShapePref : SettingPref(
get() = R.string.pref_title_icon_shape_override
override fun apply(context: Context, option: Op) {
+ val extras = bundleOf(EXTRA_ICON_SHAPE_PATH to getMaskPathByIndex(context, option.value))
LocalEvent.poster(context).apply {
- post(ACTION_CHANGED)
+ post(ACTION_CHANGED, extras)
post(SettingsPrefs.ACTION_CLOSE_SETTING)
}
}
diff --git a/app/src/main/java/com/dede/android_eggs/views/settings/prefs/LanguagePref.kt b/app/src/main/java/com/dede/android_eggs/views/settings/prefs/LanguagePref.kt
index f467a720..b4b3a842 100644
--- a/app/src/main/java/com/dede/android_eggs/views/settings/prefs/LanguagePref.kt
+++ b/app/src/main/java/com/dede/android_eggs/views/settings/prefs/LanguagePref.kt
@@ -180,15 +180,6 @@ class LanguagePref : SettingPref(null, getOptions(), SYSTEM) {
return SYSTEM
}
- fun getApplicationLocale(): Locale {
- val locales = AppCompatDelegate.getApplicationLocales()
- return if (locales.isEmpty) {
- Locale.getDefault()
- } else {
- locales.get(0) ?: Locale.getDefault()
- }
- }
-
private fun createLocale(language: String, region: String = ""): Locale {
return Locale.Builder()
.setLanguage(language)
diff --git a/app/src/main/java/com/dede/android_eggs/views/settings/prefs/NightModePref.kt b/app/src/main/java/com/dede/android_eggs/views/settings/prefs/NightModePref.kt
index e02e7367..a7175bb6 100644
--- a/app/src/main/java/com/dede/android_eggs/views/settings/prefs/NightModePref.kt
+++ b/app/src/main/java/com/dede/android_eggs/views/settings/prefs/NightModePref.kt
@@ -27,12 +27,16 @@ class NightModePref : SettingPref(
) {
companion object {
- private const val OLED = -2
+ const val OLED = -2
const val ACTION_NIGHT_MODE_CHANGED = "action_night_mode_changed"
fun isOLEDMode(context: Context): Boolean {
return NightModePref().getValue(context, SYSTEM) == OLED
}
+
+ fun getNightModeValue(context: Context): Int {
+ return NightModePref().getValue(context, SYSTEM)
+ }
}
override val titleRes: Int
diff --git a/app/src/main/java/com/dede/android_eggs/views/theme/Color.kt b/app/src/main/java/com/dede/android_eggs/views/theme/Color.kt
new file mode 100644
index 00000000..8a66844a
--- /dev/null
+++ b/app/src/main/java/com/dede/android_eggs/views/theme/Color.kt
@@ -0,0 +1,65 @@
+package com.dede.android_eggs.views.theme
+import androidx.compose.ui.graphics.Color
+
+val md_theme_light_primary = Color(0xFF006D3C)
+val md_theme_light_onPrimary = Color(0xFFFFFFFF)
+val md_theme_light_primaryContainer = Color(0xFF6FFDA7)
+val md_theme_light_onPrimaryContainer = Color(0xFF00210E)
+val md_theme_light_secondary = Color(0xFF4F6353)
+val md_theme_light_onSecondary = Color(0xFFFFFFFF)
+val md_theme_light_secondaryContainer = Color(0xFFD2E8D4)
+val md_theme_light_onSecondaryContainer = Color(0xFF0D1F13)
+val md_theme_light_tertiary = Color(0xFF3A646F)
+val md_theme_light_onTertiary = Color(0xFFFFFFFF)
+val md_theme_light_tertiaryContainer = Color(0xFFBEEAF7)
+val md_theme_light_onTertiaryContainer = Color(0xFF001F26)
+val md_theme_light_error = Color(0xFFBA1A1A)
+val md_theme_light_errorContainer = Color(0xFFFFDAD6)
+val md_theme_light_onError = Color(0xFFFFFFFF)
+val md_theme_light_onErrorContainer = Color(0xFF410002)
+val md_theme_light_background = Color(0xFFFBFDF8)
+val md_theme_light_onBackground = Color(0xFF191C19)
+val md_theme_light_outline = Color(0xFF717971)
+val md_theme_light_inverseOnSurface = Color(0xFFF0F1EC)
+val md_theme_light_inverseSurface = Color(0xFF2E312E)
+val md_theme_light_inversePrimary = Color(0xFF4EE08D)
+val md_theme_light_surfaceTint = Color(0xFF006D3C)
+val md_theme_light_outlineVariant = Color(0xFFC0C9BF)
+val md_theme_light_scrim = Color(0xFF000000)
+val md_theme_light_surface = Color(0xFFF8FAF5)
+val md_theme_light_onSurface = Color(0xFF191C19)
+val md_theme_light_surfaceVariant = Color(0xFFDDE5DB)
+val md_theme_light_onSurfaceVariant = Color(0xFF414942)
+
+val md_theme_dark_primary = Color(0xFF4EE08D)
+val md_theme_dark_onPrimary = Color(0xFF00391D)
+val md_theme_dark_primaryContainer = Color(0xFF00522C)
+val md_theme_dark_onPrimaryContainer = Color(0xFF6FFDA7)
+val md_theme_dark_secondary = Color(0xFFB6CCB9)
+val md_theme_dark_onSecondary = Color(0xFF223527)
+val md_theme_dark_secondaryContainer = Color(0xFF384B3D)
+val md_theme_dark_onSecondaryContainer = Color(0xFFD2E8D4)
+val md_theme_dark_tertiary = Color(0xFFA2CDDA)
+val md_theme_dark_onTertiary = Color(0xFF023640)
+val md_theme_dark_tertiaryContainer = Color(0xFF214C57)
+val md_theme_dark_onTertiaryContainer = Color(0xFFBEEAF7)
+val md_theme_dark_error = Color(0xFFFFB4AB)
+val md_theme_dark_errorContainer = Color(0xFF93000A)
+val md_theme_dark_onError = Color(0xFF690005)
+val md_theme_dark_onErrorContainer = Color(0xFFFFDAD6)
+val md_theme_dark_background = Color(0xFF191C19)
+val md_theme_dark_onBackground = Color(0xFFE1E3DE)
+val md_theme_dark_outline = Color(0xFF8B938A)
+val md_theme_dark_inverseOnSurface = Color(0xFF191C19)
+val md_theme_dark_inverseSurface = Color(0xFFE1E3DE)
+val md_theme_dark_inversePrimary = Color(0xFF006D3C)
+val md_theme_dark_surfaceTint = Color(0xFF4EE08D)
+val md_theme_dark_outlineVariant = Color(0xFF414942)
+val md_theme_dark_scrim = Color(0xFF000000)
+val md_theme_dark_surface = Color(0xFF111411)
+val md_theme_dark_onSurface = Color(0xFFC5C7C2)
+val md_theme_dark_surfaceVariant = Color(0xFF414942)
+val md_theme_dark_onSurfaceVariant = Color(0xFFC0C9BF)
+
+
+val seed = Color(0xFF4ADC8A)
diff --git a/app/src/main/java/com/dede/android_eggs/views/theme/Theme.kt b/app/src/main/java/com/dede/android_eggs/views/theme/Theme.kt
new file mode 100644
index 00000000..1db1a55a
--- /dev/null
+++ b/app/src/main/java/com/dede/android_eggs/views/theme/Theme.kt
@@ -0,0 +1,154 @@
+@file:Suppress("PrivatePropertyName")
+
+package com.dede.android_eggs.views.theme
+
+import android.content.Context
+import android.os.Build
+import androidx.annotation.FloatRange
+import androidx.appcompat.app.AppCompatDelegate
+import androidx.compose.foundation.isSystemInDarkTheme
+import androidx.compose.material3.ColorScheme
+import androidx.compose.material3.MaterialTheme
+import androidx.compose.material3.darkColorScheme
+import androidx.compose.material3.dynamicDarkColorScheme
+import androidx.compose.material3.dynamicLightColorScheme
+import androidx.compose.material3.lightColorScheme
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.graphics.toArgb
+import androidx.compose.ui.platform.LocalContext
+import androidx.core.graphics.ColorUtils
+import com.dede.android_eggs.views.settings.prefs.DynamicColorPref
+import com.dede.android_eggs.views.settings.prefs.NightModePref
+
+
+fun ColorScheme.toAmoled(): ColorScheme {
+ fun Color.darken(fraction: Float = 0.5f): Color =
+ Color(toArgb().blend(Color.Black.toArgb(), fraction))
+ return copy(
+ primary = primary.darken(0.3f),
+ onPrimary = onPrimary.darken(0.3f),
+ primaryContainer = primaryContainer.darken(0.3f),
+ onPrimaryContainer = onPrimaryContainer.darken(0.3f),
+ inversePrimary = inversePrimary.darken(0.3f),
+ secondary = secondary.darken(0.3f),
+ onSecondary = onSecondary.darken(0.3f),
+ secondaryContainer = secondaryContainer.darken(0.3f),
+ onSecondaryContainer = onSecondaryContainer.darken(0.3f),
+ tertiary = tertiary.darken(0.3f),
+ onTertiary = onTertiary.darken(0.3f),
+ tertiaryContainer = tertiaryContainer.darken(0.3f),
+ onTertiaryContainer = onTertiaryContainer.darken(0.2f),
+ background = Color.Black,
+ onBackground = onBackground.darken(0.15f),
+ surface = Color.Black,
+ onSurface = onSurface.darken(0.15f),
+ surfaceVariant = surfaceVariant,
+ onSurfaceVariant = onSurfaceVariant,
+ surfaceTint = surfaceTint,
+ inverseSurface = inverseSurface.darken(),
+ inverseOnSurface = inverseOnSurface.darken(0.2f),
+ outline = outline.darken(0.2f),
+ outlineVariant = outlineVariant.darken(0.2f)
+ )
+}
+
+fun Int.blend(
+ color: Int,
+ @FloatRange(from = 0.0, to = 1.0) fraction: Float = 0.5f,
+): Int = ColorUtils.blendARGB(this, color, fraction)
+
+private val LightColorScheme = lightColorScheme(
+ primary = md_theme_light_primary,
+ onPrimary = md_theme_light_onPrimary,
+ primaryContainer = md_theme_light_primaryContainer,
+ onPrimaryContainer = md_theme_light_onPrimaryContainer,
+ secondary = md_theme_light_secondary,
+ onSecondary = md_theme_light_onSecondary,
+ secondaryContainer = md_theme_light_secondaryContainer,
+ onSecondaryContainer = md_theme_light_onSecondaryContainer,
+ tertiary = md_theme_light_tertiary,
+ onTertiary = md_theme_light_onTertiary,
+ tertiaryContainer = md_theme_light_tertiaryContainer,
+ onTertiaryContainer = md_theme_light_onTertiaryContainer,
+ error = md_theme_light_error,
+ errorContainer = md_theme_light_errorContainer,
+ onError = md_theme_light_onError,
+ onErrorContainer = md_theme_light_onErrorContainer,
+ background = md_theme_light_background,
+ onBackground = md_theme_light_onBackground,
+ outline = md_theme_light_outline,
+ inverseOnSurface = md_theme_light_inverseOnSurface,
+ inverseSurface = md_theme_light_inverseSurface,
+ inversePrimary = md_theme_light_inversePrimary,
+ surfaceTint = md_theme_light_surfaceTint,
+ outlineVariant = md_theme_light_outlineVariant,
+ scrim = md_theme_light_scrim,
+ surface = md_theme_light_surface,
+ onSurface = md_theme_light_onSurface,
+ surfaceVariant = md_theme_light_surfaceVariant,
+ onSurfaceVariant = md_theme_light_onSurfaceVariant,
+)
+
+private val DarkColorScheme = darkColorScheme(
+ primary = md_theme_dark_primary,
+ onPrimary = md_theme_dark_onPrimary,
+ primaryContainer = md_theme_dark_primaryContainer,
+ onPrimaryContainer = md_theme_dark_onPrimaryContainer,
+ secondary = md_theme_dark_secondary,
+ onSecondary = md_theme_dark_onSecondary,
+ secondaryContainer = md_theme_dark_secondaryContainer,
+ onSecondaryContainer = md_theme_dark_onSecondaryContainer,
+ tertiary = md_theme_dark_tertiary,
+ onTertiary = md_theme_dark_onTertiary,
+ tertiaryContainer = md_theme_dark_tertiaryContainer,
+ onTertiaryContainer = md_theme_dark_onTertiaryContainer,
+ error = md_theme_dark_error,
+ errorContainer = md_theme_dark_errorContainer,
+ onError = md_theme_dark_onError,
+ onErrorContainer = md_theme_dark_onErrorContainer,
+ background = md_theme_dark_background,
+ onBackground = md_theme_dark_onBackground,
+ outline = md_theme_dark_outline,
+ inverseOnSurface = md_theme_dark_inverseOnSurface,
+ inverseSurface = md_theme_dark_inverseSurface,
+ inversePrimary = md_theme_dark_inversePrimary,
+ surfaceTint = md_theme_dark_surfaceTint,
+ outlineVariant = md_theme_dark_outlineVariant,
+ scrim = md_theme_dark_scrim,
+ surface = md_theme_dark_surface,
+ onSurface = md_theme_dark_onSurface,
+ surfaceVariant = md_theme_dark_surfaceVariant,
+ onSurfaceVariant = md_theme_dark_onSurfaceVariant,
+)
+
+@Composable
+fun AppTheme(content: @Composable () -> Unit) {
+ val context: Context = LocalContext.current
+ var nightModeValue = NightModePref.getNightModeValue(context)
+ if (nightModeValue == AppCompatDelegate.MODE_NIGHT_FOLLOW_SYSTEM) {
+ nightModeValue = if (isSystemInDarkTheme())
+ AppCompatDelegate.MODE_NIGHT_YES else AppCompatDelegate.MODE_NIGHT_NO
+ }
+
+ val colors = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S &&
+ DynamicColorPref.isDynamicEnable(context)
+ ) {
+ when (nightModeValue) {
+ NightModePref.OLED -> dynamicDarkColorScheme(context).toAmoled()
+ AppCompatDelegate.MODE_NIGHT_YES -> dynamicDarkColorScheme(context)
+ else -> dynamicLightColorScheme(context)
+ }
+ } else {
+ when (nightModeValue) {
+ NightModePref.OLED -> DarkColorScheme.toAmoled()
+ AppCompatDelegate.MODE_NIGHT_YES -> DarkColorScheme
+ else -> LightColorScheme
+ }
+ }
+
+ MaterialTheme(
+ colorScheme = colors,
+ content = content
+ )
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/dede/android_eggs/views/timeline/AndroidTimeline.kt b/app/src/main/java/com/dede/android_eggs/views/timeline/AndroidTimeline.kt
new file mode 100644
index 00000000..84a8f71f
--- /dev/null
+++ b/app/src/main/java/com/dede/android_eggs/views/timeline/AndroidTimeline.kt
@@ -0,0 +1,207 @@
+@file:OptIn(
+ ExperimentalMaterial3Api::class, ExperimentalLayoutApi::class
+)
+
+package com.dede.android_eggs.views.timeline
+
+import androidx.compose.foundation.background
+import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.ExperimentalLayoutApi
+import androidx.compose.foundation.layout.MutableWindowInsets
+import androidx.compose.foundation.layout.PaddingValues
+import androidx.compose.foundation.layout.WindowInsets
+import androidx.compose.foundation.layout.fillMaxWidth
+import androidx.compose.foundation.layout.offset
+import androidx.compose.foundation.layout.padding
+import androidx.compose.foundation.layout.size
+import androidx.compose.foundation.layout.systemBars
+import androidx.compose.foundation.layout.width
+import androidx.compose.foundation.layout.wrapContentHeight
+import androidx.compose.foundation.lazy.LazyColumn
+import androidx.compose.foundation.lazy.itemsIndexed
+import androidx.compose.foundation.shape.RoundedCornerShape
+import androidx.compose.material.icons.Icons
+import androidx.compose.material.icons.outlined.ArrowLeft
+import androidx.compose.material3.Card
+import androidx.compose.material3.CardDefaults
+import androidx.compose.material3.ExperimentalMaterial3Api
+import androidx.compose.material3.Icon
+import androidx.compose.material3.MaterialTheme.colorScheme
+import androidx.compose.material3.MaterialTheme.shapes
+import androidx.compose.material3.MaterialTheme.typography
+import androidx.compose.material3.ModalBottomSheet
+import androidx.compose.material3.Text
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.MutableState
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.remember
+import androidx.compose.runtime.setValue
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.draw.clip
+import androidx.compose.ui.platform.LocalDensity
+import androidx.compose.ui.text.SpanStyle
+import androidx.compose.ui.text.buildAnnotatedString
+import androidx.compose.ui.text.font.FontWeight
+import androidx.compose.ui.text.withStyle
+import androidx.compose.ui.tooling.preview.Preview
+import androidx.compose.ui.unit.dp
+import androidx.constraintlayout.compose.ConstraintLayout
+import androidx.constraintlayout.compose.Dimension
+import com.dede.android_eggs.main.entity.TimelineEvent
+import com.dede.android_eggs.views.main.compose.Image
+
+@Preview
+@Composable
+fun AndroidTimelineSheet(visible: MutableState = mutableStateOf(true)) {
+ var show by visible
+ if (!show) return
+
+ val top = with(LocalDensity.current) {
+ WindowInsets.systemBars.getTop(this).toDp()
+ }
+
+ ModalBottomSheet(
+ windowInsets = MutableWindowInsets(),
+ modifier = Modifier
+ .offset(y = -top)
+ .padding(top = top),
+ onDismissRequest = {
+ show = false
+ },
+ ) {
+ AndroidTimeline()
+ }
+}
+
+@Composable
+@Preview(showBackground = true)
+fun AndroidTimeline(items: List = TimelineEvent.timelines) {
+ val bottom = with(LocalDensity.current) {
+ WindowInsets.systemBars.getBottom(this).toDp()
+ }
+ LazyColumn(
+ contentPadding = PaddingValues(bottom = bottom),
+ ) {
+ itemsIndexed(items = items) { index, item ->
+ AndroidTimelineItem(item, {
+ return@AndroidTimelineItem index >= items.size - 1
+ }) {
+ if (index <= 0) return@AndroidTimelineItem true
+ return@AndroidTimelineItem items[index - 1].year != it.year
+ }
+ }
+ }
+}
+
+@Composable
+fun AndroidTimelineItem(
+ timeline: TimelineEvent,
+ isLast: (current: TimelineEvent) -> Boolean,
+ isNewYearGroup: (current: TimelineEvent) -> Boolean,
+) {
+ val isNewGroup = remember(timeline) { isNewYearGroup.invoke(timeline) }
+ val isLast = remember(timeline) { isLast.invoke(timeline) }
+
+ ConstraintLayout(
+ modifier = Modifier
+ .fillMaxWidth()
+ .wrapContentHeight()
+ ) {
+ val (logo, card, year, month, arrow, l1, l2) = createRefs()
+ if (isNewGroup) {
+ Text(
+ text = timeline.localYear.toString(),
+ style = typography.titleLarge,
+ modifier = Modifier.constrainAs(year) {
+ end.linkTo(logo.start)
+ start.linkTo(parent.start)
+ top.linkTo(parent.top, margin = 10.dp)
+ }
+ )
+ }
+ Text(
+ text = timeline.localMonth.toString(),
+ style = typography.labelMedium,
+ modifier = Modifier.constrainAs(month) {
+ end.linkTo(logo.start, margin = 10.dp)
+ linkTo(top = logo.top, bottom = logo.bottom)
+ }
+ )
+ Box(
+ modifier = Modifier
+ .width(2.dp)
+ .clip(RoundedCornerShape(bottomEndPercent = 50, bottomStartPercent = 50))
+ .background(colorScheme.primary)
+ .constrainAs(l1) {
+ height = Dimension.fillToConstraints
+ linkTo(start = logo.start, end = logo.end)
+ top.linkTo(parent.top)
+ bottom.linkTo(logo.top, 4.dp)
+ }
+ ) {}
+ Image(
+ res = timeline.logoRes, contentDescription = null,
+ modifier = Modifier
+ .size(24.dp)
+ .constrainAs(logo) {
+ linkTo(start = parent.start, end = parent.end, bias = 0.3f)
+ linkTo(top = card.top, bottom = card.bottom)
+ }
+ )
+ if (!isLast) {
+ Box(
+ modifier = Modifier
+ .width(2.dp)
+ .clip(RoundedCornerShape(topEndPercent = 50, topStartPercent = 50))
+ .background(colorScheme.primary)
+ .constrainAs(l2) {
+ height = Dimension.fillToConstraints
+ linkTo(start = logo.start, end = logo.end)
+ top.linkTo(logo.bottom, 4.dp)
+ bottom.linkTo(parent.bottom)
+ }
+ ) {}
+ }
+ Card(
+ colors = CardDefaults.cardColors(containerColor = colorScheme.secondaryContainer),
+ elevation = CardDefaults.cardElevation(0.dp),
+ shape = shapes.medium,
+ modifier = Modifier
+ .padding(horizontal = 10.dp)
+ .constrainAs(card) {
+ width = Dimension.preferredWrapContent
+ linkTo(start = logo.end, end = parent.end, bias = 0f)
+ top.linkTo(year.bottom)
+ bottom.linkTo(parent.bottom, margin = 10.dp)
+ }
+ ) {
+ Text(
+ text = buildAnnotatedString {
+ val split = timeline.event.split("\n")
+ withStyle(SpanStyle(fontWeight = FontWeight.Bold)) {
+ append(split[0])
+ }
+ if (split.size > 1) {
+ append("\n")
+ append(split[1])
+ }
+ },
+ style = typography.labelMedium,
+ modifier = Modifier.padding(horizontal = 10.dp, vertical = 8.dp)
+ )
+ }
+ // todo fix leak https://issuetracker.google.com/issues/311627066
+ Icon(
+ imageVector = Icons.Outlined.ArrowLeft,
+ tint = colorScheme.secondaryContainer,
+ modifier = Modifier
+ .size(24.dp)
+ .constrainAs(arrow) {
+ linkTo(top = logo.top, bottom = logo.bottom)
+ start.linkTo(card.start, margin = (-3.5).dp)
+ },
+ contentDescription = null
+ )
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/dede/android_eggs/views/timeline/AndroidTimelineFragment.kt b/app/src/main/java/com/dede/android_eggs/views/timeline/AndroidTimelineFragment.kt
index e4585f67..80150b8f 100644
--- a/app/src/main/java/com/dede/android_eggs/views/timeline/AndroidTimelineFragment.kt
+++ b/app/src/main/java/com/dede/android_eggs/views/timeline/AndroidTimelineFragment.kt
@@ -2,6 +2,7 @@ package com.dede.android_eggs.views.timeline
import android.app.Dialog
import android.content.res.ColorStateList
+import android.graphics.Rect
import android.os.Bundle
import android.view.View
import android.widget.ImageView
@@ -9,11 +10,11 @@ import android.widget.TextView
import androidx.core.view.isGone
import androidx.core.view.isVisible
import androidx.fragment.app.FragmentManager
+import androidx.recyclerview.widget.RecyclerView
import by.kirich1409.viewbindingdelegate.viewBinding
import coil.load
import com.dede.android_eggs.R
import com.dede.android_eggs.databinding.FragmentAndroidTimelineBinding
-import com.dede.android_eggs.main.EggListFragment
import com.dede.android_eggs.main.entity.TimelineEvent
import com.dede.android_eggs.main.entity.TimelineEvent.Companion.isLast
import com.dede.android_eggs.main.entity.TimelineEvent.Companion.isNewGroup
@@ -87,12 +88,38 @@ class AndroidTimelineFragment : BottomSheetDialogFragment(R.layout.fragment_andr
holder.findViewById(R.id.iv_logo).load(timelineEvent.logoRes)
holder.findViewById(R.id.line_bottom).isGone = timelineEvent.isLast()
}
- var last = EggListFragment.EggListDivider(0, 0, 0)
+ var last = EggListDivider(0, 0, 0)
binding.recyclerView.addItemDecoration(last)
binding.recyclerView.onApplyWindowEdge {
removeItemDecoration(last)
- last = EggListFragment.EggListDivider(0, 0, it.bottom)
+ last = EggListDivider(0, 0, it.bottom)
addItemDecoration(last)
}
}
+
+ class EggListDivider(
+ private val divider: Int,
+ private val topInset: Int,
+ private val bottomInset: Int,
+ ) : RecyclerView.ItemDecoration() {
+
+ override fun getItemOffsets(
+ outRect: Rect,
+ view: View,
+ parent: RecyclerView,
+ state: RecyclerView.State,
+ ) {
+ val position = parent.getChildAdapterPosition(view)
+ if (position == 0) {
+ outRect.top = divider + topInset
+ }
+ outRect.bottom = divider
+
+ val adapter = parent.adapter ?: return
+ if (position == adapter.itemCount - 1) {
+ outRect.bottom = divider + bottomInset
+ }
+ }
+
+ }
}
\ No newline at end of file
diff --git a/app/src/main/java/com/dede/android_eggs/views/widget/AnalogClockAppWidget.kt b/app/src/main/java/com/dede/android_eggs/views/widget/AnalogClockAppWidget.kt
index b8893ad7..9f237a4f 100644
--- a/app/src/main/java/com/dede/android_eggs/views/widget/AnalogClockAppWidget.kt
+++ b/app/src/main/java/com/dede/android_eggs/views/widget/AnalogClockAppWidget.kt
@@ -9,7 +9,7 @@ import android.os.Bundle
import android.widget.RemoteViews
import androidx.core.app.PendingIntentCompat
import com.dede.android_eggs.R
-import com.dede.android_eggs.main.EasterEggsActivity
+import com.dede.android_eggs.views.main.EasterEggsActivity
/**
* Easter Eggs Analog clock widget.
diff --git a/app/src/main/res/drawable-anydpi/ic_android_snowcone_fg.xml b/app/src/main/res/drawable-anydpi/ic_android_snowcone_fg.xml
deleted file mode 100644
index 1faca7a3..00000000
--- a/app/src/main/res/drawable-anydpi/ic_android_snowcone_fg.xml
+++ /dev/null
@@ -1,26 +0,0 @@
-
-
-
-
-
-
-
-
diff --git a/app/src/main/res/drawable-anydpi/ic_android_tiramisu_fg.xml b/app/src/main/res/drawable-anydpi/ic_android_tiramisu_fg.xml
deleted file mode 100644
index 62680516..00000000
--- a/app/src/main/res/drawable-anydpi/ic_android_tiramisu_fg.xml
+++ /dev/null
@@ -1,46 +0,0 @@
-
-
-
-
-
-
-
-
-
-
diff --git a/app/src/main/res/drawable-ldrtl/popupmenu_background_overlay.xml b/app/src/main/res/drawable-ldrtl/popupmenu_background_overlay.xml
index f0a0f66d..531eb2fc 100644
--- a/app/src/main/res/drawable-ldrtl/popupmenu_background_overlay.xml
+++ b/app/src/main/res/drawable-ldrtl/popupmenu_background_overlay.xml
@@ -1,6 +1,6 @@
-
+
diff --git a/app/src/main/res/drawable/popupmenu_background_overlay.xml b/app/src/main/res/drawable/popupmenu_background_overlay.xml
index 05f758f2..a44c29f7 100644
--- a/app/src/main/res/drawable/popupmenu_background_overlay.xml
+++ b/app/src/main/res/drawable/popupmenu_background_overlay.xml
@@ -1,6 +1,6 @@
-
+
diff --git a/app/src/main/res/font/icons.ttf b/app/src/main/res/font/icons.ttf
index 8172674b..0190ffc1 100644
Binary files a/app/src/main/res/font/icons.ttf and b/app/src/main/res/font/icons.ttf differ
diff --git a/app/src/main/res/layout/activity_easter_eggs.xml b/app/src/main/res/layout/activity_easter_eggs.xml
deleted file mode 100644
index 0c4f5fa5..00000000
--- a/app/src/main/res/layout/activity_easter_eggs.xml
+++ /dev/null
@@ -1,31 +0,0 @@
-
-
-
-
-
-
-
-
-
-
-
-
\ No newline at end of file
diff --git a/app/src/main/res/layout/fragment_easter_egg_list.xml b/app/src/main/res/layout/fragment_easter_egg_list.xml
deleted file mode 100644
index 7afa803b..00000000
--- a/app/src/main/res/layout/fragment_easter_egg_list.xml
+++ /dev/null
@@ -1,22 +0,0 @@
-
-
-
-
-
-
\ No newline at end of file
diff --git a/app/src/main/res/layout/item_easter_egg_layout.xml b/app/src/main/res/layout/item_easter_egg_layout.xml
deleted file mode 100644
index 8e1e1c95..00000000
--- a/app/src/main/res/layout/item_easter_egg_layout.xml
+++ /dev/null
@@ -1,73 +0,0 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
\ No newline at end of file
diff --git a/app/src/main/res/layout/item_easter_egg_wavy.xml b/app/src/main/res/layout/item_easter_egg_wavy.xml
deleted file mode 100644
index cacd3964..00000000
--- a/app/src/main/res/layout/item_easter_egg_wavy.xml
+++ /dev/null
@@ -1,18 +0,0 @@
-
-
-
-
-
\ No newline at end of file
diff --git a/app/src/main/res/layout/layout_flow_separator.xml b/app/src/main/res/layout/layout_flow_separator.xml
deleted file mode 100644
index 5a6fb8ea..00000000
--- a/app/src/main/res/layout/layout_flow_separator.xml
+++ /dev/null
@@ -1,10 +0,0 @@
-
-
\ No newline at end of file
diff --git a/app/src/main/res/layout/layout_item_egg_background.xml b/app/src/main/res/layout/layout_item_egg_background.xml
deleted file mode 100644
index 7760c89e..00000000
--- a/app/src/main/res/layout/layout_item_egg_background.xml
+++ /dev/null
@@ -1,48 +0,0 @@
-
-
-
-
-
-
-
-
-
\ No newline at end of file
diff --git a/app/src/main/res/layout/view_easter_egg_footer.xml b/app/src/main/res/layout/view_easter_egg_footer.xml
deleted file mode 100644
index 0f871434..00000000
--- a/app/src/main/res/layout/view_easter_egg_footer.xml
+++ /dev/null
@@ -1,196 +0,0 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
\ No newline at end of file
diff --git a/app/src/main/res/menu/menu_settings.xml b/app/src/main/res/menu/menu_settings.xml
deleted file mode 100644
index 7d2d8fe8..00000000
--- a/app/src/main/res/menu/menu_settings.xml
+++ /dev/null
@@ -1,14 +0,0 @@
-
-
\ No newline at end of file
diff --git a/app/src/main/res/values-night/hashs.xml b/app/src/main/res/values-night/hashs.xml
index 224efe12..8dbc97cf 100644
--- a/app/src/main/res/values-night/hashs.xml
+++ b/app/src/main/res/values-night/hashs.xml
@@ -1,5 +1,4 @@
- LNFqFcNxsQS6|,oIj@ae=cxFjvfk
LPBpkc1k-SXR$*S#WCsmoLj]R,s.
\ No newline at end of file
diff --git a/app/src/main/res/values/colors.xml b/app/src/main/res/values/colors.xml
index 2797286f..0f550e37 100644
--- a/app/src/main/res/values/colors.xml
+++ b/app/src/main/res/values/colors.xml
@@ -1,58 +1,66 @@
- #6750A4
+ #4ADC8A
+ #006D3C
#FFFFFF
- #EADDFF
- #21005D
- #625B71
+ #6FFDA7
+ #00210E
+ #4F6353
#FFFFFF
- #E8DEF8
- #1D192B
- #7D5260
+ #D2E8D4
+ #0D1F13
+ #3A646F
#FFFFFF
- #FFD8E4
- #31111D
- #B3261E
+ #BEEAF7
+ #001F26
+ #BA1A1A
+ #FFDAD6
#FFFFFF
- #F9DEDC
- #410E0B
- #79747E
- #FFFBFE
- #1C1B1F
- #FFFBFE
- #1C1B1F
- #E7E0EC
- #49454F
- #313033
- #F4EFF4
- #D0BCFF
-
- #D0BCFF
- #381E72
- #4F378B
- #EADDFF
- #CCC2DC
- #332D41
- #4A4458
- #E8DEF8
- #EFB8C8
- #492532
- #633B48
- #FFD8E4
- #F2B8B5
- #601410
- #8C1D18
- #F9DEDC
- #938F99
- #1C1B1F
- #E6E1E5
- #1C1B1F
- #E6E1E5
- #49454F
- #CAC4D0
- #E6E1E5
- #313033
- #6750A4
+ #410002
+ #FBFDF8
+ #191C19
+ #717971
+ #F0F1EC
+ #2E312E
+ #4EE08D
+ #000000
+ #006D3C
+ #C0C9BF
+ #000000
+ #F8FAF5
+ #191C19
+ #DDE5DB
+ #414942
+ #4EE08D
+ #00391D
+ #00522C
+ #6FFDA7
+ #B6CCB9
+ #223527
+ #384B3D
+ #D2E8D4
+ #A2CDDA
+ #023640
+ #214C57
+ #BEEAF7
+ #FFB4AB
+ #93000A
+ #690005
+ #FFDAD6
+ #191C19
+ #E1E3DE
+ #8B938A
+ #191C19
+ #E1E3DE
+ #006D3C
+ #000000
+ #4EE08D
+ #414942
+ #000000
+ #111411
+ #C5C7C2
+ #414942
+ #C0C9BF
@color/md_theme_light_background
diff --git a/app/src/main/res/values/dimens.xml b/app/src/main/res/values/dimens.xml
index 7a827ad9..55344e51 100644
--- a/app/src/main/res/values/dimens.xml
+++ b/app/src/main/res/values/dimens.xml
@@ -1,5 +1,3 @@
- 560dp
- 6dp
\ No newline at end of file
diff --git a/app/src/main/res/values/hashs.xml b/app/src/main/res/values/hashs.xml
index 1467c20c..1f2a671f 100644
--- a/app/src/main/res/values/hashs.xml
+++ b/app/src/main/res/values/hashs.xml
@@ -1,5 +1,31 @@
- LVPO*{9docS$}Nn4R.oy$]${n$bI
LYNnBW3k$%S#$9ofWVoKjZbHayof
+
+
+ - LYOy6uIXXStP}NVtbcbb-f$xj[R+
+ - LCJI-PjcC,XM:GXvC5bYXvfPj=j[
+ - LPM7vPyE5555^SD~~DRi4n$+-psm
+ - LLPaQ8IR9Wx260sq-os+%S-m%2NH
+ - LBG24jS20$,,-oOaEm^K-RNGS5sk
+ - LGT7BOpIQ-ZNUbu4V@m-Z$kCeTjF
+ - LIPsdyrC^ibx1IEJ-o#;E0%2r?$M
+ - LNPQc:.j=HR+?@H[kVX8Z%xtJBrs
+ - LDNK0hNmv,xs:~QpwOtR-9nUs?TK
+ - LYPZom^5|^X7]$I;S$sAiwS#soxG
+ - LbP}Q#xuI1D%0z$RXTOQIIr?X$bu
+ - LPReA_Y$TfxDteR#i|xwpXaOn-Ri
+ - LIQc@M.gbSw5FgrUX+J;=LNWx-Dk
+ - LEGxEDMy58%0~U-:ItIqxUE2NL%0
+ - LOQbzz}$rNWXkoRXWYkkw[N6soR*
+ - LMNU5nabsixm^X9Zoyxu0#In$%je
+ - LHJIxAIBVQtm%}F=#uNWG2Ohw9RX
+ - LJJTFsU[-xH@?IuCx=%NvT9ER*%x
+ - LDR2Vr~BIm5P0r~U,J8w99;2$%9a
+ - LJK-hbXn.mDj-xrX5II;50oaSvt,
+ - LLLr|rNA?$xKtFXWV|ryOh=gRUM#
+ - LLS4wORjL}bIm+e9e-j[bMavpJj^
+ - LkPYet0fM}jZIVM|S_xGRBJ*R*xG
+ - LTQIPCT#ovs*:%*0S*Mx9_NFx]xG
+
\ No newline at end of file
diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml
index ae5e4836..d8a46e8e 100644
--- a/app/src/main/res/values/strings.xml
+++ b/app/src/main/res/values/strings.xml
@@ -8,6 +8,7 @@
https://github.com/hushenghao/AndroidEasterEggs
https://github.com/hushenghao/AndroidEasterEggs/commit/%s
+ https://github.com/hushenghao/AndroidEasterEggs/issues
https://www.pgyer.com/eggs
https://github.com/hushenghao/AndroidEasterEggs/wiki/Privacy-policy
https://www.apache.org/licenses/LICENSE-2.0.txt
diff --git a/app/src/main/res/values/strings_eggs.xml b/app/src/main/res/values/strings_eggs.xml
index 6496dc81..483e79d5 100644
--- a/app/src/main/res/values/strings_eggs.xml
+++ b/app/src/main/res/values/strings_eggs.xml
@@ -1,21 +1,6 @@
- Upside Down Cake
- Tiramisu
- Snow Cone
- Red Velvet Cake
- Quince Tart
- Pie
- Oreo
- Nougat
- Marshmallow
- Lollipop
- KitKat
- Jelly Bean
- Ice Cream Sandwich
- Honeycomb
- Gingerbread
Froyo
Eclair
Donut
diff --git a/app/src/main/res/xml/shortcuts.xml b/app/src/main/res/xml/shortcuts.xml
index 1ffc254a..558e00c5 100644
--- a/app/src/main/res/xml/shortcuts.xml
+++ b/app/src/main/res/xml/shortcuts.xml
@@ -6,10 +6,10 @@
+ android:shortcutLongLabel="@string/u_android_nickname"
+ android:shortcutShortLabel="@string/u_android_nickname">
@@ -56,17 +56,16 @@
not available. -->
-
+
-
-
+
\ No newline at end of file
diff --git a/basic/src/main/java/com/dede/basic/DynamicObjectUtils.kt b/basic/src/main/java/com/dede/basic/DynamicObjectUtils.kt
index adec8bf4..441f1a12 100644
--- a/basic/src/main/java/com/dede/basic/DynamicObjectUtils.kt
+++ b/basic/src/main/java/com/dede/basic/DynamicObjectUtils.kt
@@ -51,13 +51,22 @@ private class ReflectDynamicObject(obj: Any?, clazz: Class) : DynamicOb
private fun pickMethod(name: String, vararg arguments: Any?): Method? {
val types = inferTypes(*arguments)
var method: Method? = null
+ var error: Exception? = null
try {
method = clazz.getMethod(name, *types)
- if (method == null) {
+ } catch (e: Exception) {
+ error = e
+ }
+ if (method == null) {
+ error = null
+ try {
method = clazz.getDeclaredMethod(name, *types)
+ } catch (e: Exception) {
+ error = e
}
- } catch (e: Exception) {
- Log.w(TAG, e)
+ }
+ if (error != null) {
+ Log.w(TAG, error)
}
return method
}
@@ -84,13 +93,22 @@ private class ReflectDynamicObject(obj: Any?, clazz: Class) : DynamicOb
private fun pickField(name: String): Field? {
var field: Field? = null
+ var error: Exception? = null
try {
field = clazz.getField(name)
- if (field == null) {
+ } catch (e: Exception) {
+ error = e
+ }
+ if (field == null) {
+ error = null
+ try {
field = clazz.getDeclaredField(name)
+ } catch (e: Exception) {
+ error = e
}
- } catch (e: Exception) {
- Log.w(TAG, e)
+ }
+ if (error != null) {
+ Log.w(TAG, error)
}
return field
}
diff --git a/basic/src/main/java/com/dede/basic/provider/SnapshotProvider.kt b/basic/src/main/java/com/dede/basic/provider/SnapshotProvider.kt
index e41727e9..ae5262bb 100644
--- a/basic/src/main/java/com/dede/basic/provider/SnapshotProvider.kt
+++ b/basic/src/main/java/com/dede/basic/provider/SnapshotProvider.kt
@@ -2,25 +2,10 @@ package com.dede.basic.provider
import android.content.Context
import android.view.View
-import android.view.ViewGroup
-import java.lang.ref.WeakReference
abstract class SnapshotProvider {
open val includeBackground: Boolean = false
- private var cache: WeakReference? = null
-
- fun get(context: Context): View {
- var view = cache?.get()
- if (view != null) {
- (view.parent as? ViewGroup)?.removeView(view)
- return view
- }
- view = create(context)
- cache = WeakReference(view)
- return view
- }
-
abstract fun create(context: Context): View
}
\ No newline at end of file
diff --git a/buildSrc/build.gradle.kts b/buildSrc/build.gradle.kts
index 54cc83e8..abccff36 100644
--- a/buildSrc/build.gradle.kts
+++ b/buildSrc/build.gradle.kts
@@ -10,5 +10,5 @@ repositories {
dependencies {
implementation(gradleApi())
- implementation("com.android.tools:sdk-common:31.1.3")
+ implementation("com.android.tools:sdk-common:31.1.4")
}
diff --git a/fastlane/metadata/android/en-US/changelogs/33.txt b/fastlane/metadata/android/en-US/changelogs/33.txt
index 89fdf6bd..705dfbc8 100644
--- a/fastlane/metadata/android/en-US/changelogs/33.txt
+++ b/fastlane/metadata/android/en-US/changelogs/33.txt
@@ -1,3 +1,5 @@
+- Added crash report
+- Refactored with Compose
- Added Component Manager Settings
- Added Portuguese (Brazilian) translation
- Known issue fixes
\ No newline at end of file
diff --git a/fastlane/metadata/android/zh-CN/changelogs/33.txt b/fastlane/metadata/android/zh-CN/changelogs/33.txt
index 49af6091..aefff0c8 100644
--- a/fastlane/metadata/android/zh-CN/changelogs/33.txt
+++ b/fastlane/metadata/android/zh-CN/changelogs/33.txt
@@ -1,3 +1,5 @@
+- 新增崩溃上报
+- Compose重构
- 新增组件管理设置
- 新增葡萄牙语(巴西)翻译
- 修复已知问题
\ No newline at end of file
diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml
index 7f3c4a6c..335003b5 100644
--- a/gradle/libs.versions.toml
+++ b/gradle/libs.versions.toml
@@ -3,8 +3,10 @@ kotlin = "1.9.20"
# https://androidx.dev/storage/compose-compiler/repository
# https://maven.google.com/web/index.html?q=compose#androidx.compose.compiler:compiler
compose-kotlin-compiler = "1.5.4"
-agp = "8.1.3"
+agp = "8.1.4"
hilt = "2.48.1"
+# https://google.github.io/accompanist
+accompanist = "0.33.2-alpha"
[plugins]
android-application = { id = "com.android.application", version.ref = "agp" }
@@ -18,7 +20,6 @@ kotlinx-coroutines-android = "org.jetbrains.kotlinx:kotlinx-coroutines-android:1
androidx-appcompat = "androidx.appcompat:appcompat:1.6.1"
androidx-core = "androidx.core:core-ktx:1.12.0"
androidx-activity = "androidx.activity:activity-ktx:1.8.0"
-androidx-activity-compose = "androidx.activity:activity-compose:1.8.0"
androidx-lifecycle-viewmodel = "androidx.lifecycle:lifecycle-viewmodel-ktx:2.6.2"
androidx-lifecycle-runtime = "androidx.lifecycle:lifecycle-runtime-ktx:2.6.2"
androidx-recyclerview = "androidx.recyclerview:recyclerview:1.3.2"
@@ -31,19 +32,28 @@ androidx-window = "androidx.window:window:1.1.0"
google-material = "com.google.android.material:material:1.11.0-beta01"
androidx-startup = "androidx.startup:startup-runtime:1.1.1"
+androidx-activity-compose = "androidx.activity:activity-compose:1.8.0"
+androidx-lifecycle-compose = "androidx.lifecycle:lifecycle-runtime-compose:2.6.2"
androidx-compose-bom = "androidx.compose:compose-bom:2023.10.01"
androidx-compose-ui = { module = "androidx.compose.ui:ui" }
androidx-compose-ui-util = { module = "androidx.compose.ui:ui-util" }
+androidx-compose-ui-tooling = { module = "androidx.compose.ui:ui-tooling" }
androidx-compose-ui-tooling-preview = { module = "androidx.compose.ui:ui-tooling-preview" }
androidx-compose-material = { module = "androidx.compose.material:material" }
+androidx-compose-material3 = { module = "androidx.compose.material3:material3" }
+androidx-compose-material-icons = { module = "androidx.compose.material:material-icons-extended" }
+androidx-compose-constraintlayout = "androidx.constraintlayout:constraintlayout-compose:1.0.1"
+accompanist-drawablepainter = { module = "com.google.accompanist:accompanist-drawablepainter", version.ref = "accompanist" }
hilt-android = { module = "com.google.dagger:hilt-android", version.ref = "hilt" }
hilt-compiler = { module = "com.google.dagger:hilt-compiler", version.ref = "hilt" }
+dionsegijn-konfetti = "nl.dionsegijn:konfetti-compose:2.0.3"
blurhash-android = "io.github.hushenghao:blurhash-android:1.0.1"
viewbinding-delegate = "com.github.kirich1409:viewbindingpropertydelegate-noreflection:1.5.9"
squareup-okio = "com.squareup.okio:okio:3.6.0"
io-coil = "io.coil-kt:coil:2.5.0"
+io-coil-compose = "io.coil-kt:coil-compose:2.5.0"
free-reflection = "com.github.tiann:FreeReflection:3.2.0"
squareup-leakcanary = "com.squareup.leakcanary:leakcanary-android-startup:2.12"
nanohttpd = "org.nanohttpd:nanohttpd:2.3.1"
diff --git a/app/src/main/assets/gallery/1.webp b/script/blurhash/image/gallery/1.webp
similarity index 100%
rename from app/src/main/assets/gallery/1.webp
rename to script/blurhash/image/gallery/1.webp
diff --git a/app/src/main/assets/gallery/10.webp b/script/blurhash/image/gallery/10.webp
similarity index 100%
rename from app/src/main/assets/gallery/10.webp
rename to script/blurhash/image/gallery/10.webp
diff --git a/app/src/main/assets/gallery/11.webp b/script/blurhash/image/gallery/11.webp
similarity index 100%
rename from app/src/main/assets/gallery/11.webp
rename to script/blurhash/image/gallery/11.webp
diff --git a/app/src/main/assets/gallery/12.webp b/script/blurhash/image/gallery/12.webp
similarity index 100%
rename from app/src/main/assets/gallery/12.webp
rename to script/blurhash/image/gallery/12.webp
diff --git a/app/src/main/assets/gallery/13.webp b/script/blurhash/image/gallery/13.webp
similarity index 100%
rename from app/src/main/assets/gallery/13.webp
rename to script/blurhash/image/gallery/13.webp
diff --git a/app/src/main/assets/gallery/14.webp b/script/blurhash/image/gallery/14.webp
similarity index 100%
rename from app/src/main/assets/gallery/14.webp
rename to script/blurhash/image/gallery/14.webp
diff --git a/app/src/main/assets/gallery/15.webp b/script/blurhash/image/gallery/15.webp
similarity index 100%
rename from app/src/main/assets/gallery/15.webp
rename to script/blurhash/image/gallery/15.webp
diff --git a/app/src/main/assets/gallery/16.webp b/script/blurhash/image/gallery/16.webp
similarity index 100%
rename from app/src/main/assets/gallery/16.webp
rename to script/blurhash/image/gallery/16.webp
diff --git a/app/src/main/assets/gallery/17.webp b/script/blurhash/image/gallery/17.webp
similarity index 100%
rename from app/src/main/assets/gallery/17.webp
rename to script/blurhash/image/gallery/17.webp
diff --git a/app/src/main/assets/gallery/18.webp b/script/blurhash/image/gallery/18.webp
similarity index 100%
rename from app/src/main/assets/gallery/18.webp
rename to script/blurhash/image/gallery/18.webp
diff --git a/app/src/main/assets/gallery/19.webp b/script/blurhash/image/gallery/19.webp
similarity index 100%
rename from app/src/main/assets/gallery/19.webp
rename to script/blurhash/image/gallery/19.webp
diff --git a/app/src/main/assets/gallery/2.webp b/script/blurhash/image/gallery/2.webp
similarity index 100%
rename from app/src/main/assets/gallery/2.webp
rename to script/blurhash/image/gallery/2.webp
diff --git a/app/src/main/assets/gallery/20.webp b/script/blurhash/image/gallery/20.webp
similarity index 100%
rename from app/src/main/assets/gallery/20.webp
rename to script/blurhash/image/gallery/20.webp
diff --git a/app/src/main/assets/gallery/21.webp b/script/blurhash/image/gallery/21.webp
similarity index 100%
rename from app/src/main/assets/gallery/21.webp
rename to script/blurhash/image/gallery/21.webp
diff --git a/app/src/main/assets/gallery/22.webp b/script/blurhash/image/gallery/22.webp
similarity index 100%
rename from app/src/main/assets/gallery/22.webp
rename to script/blurhash/image/gallery/22.webp
diff --git a/app/src/main/assets/gallery/23.webp b/script/blurhash/image/gallery/23.webp
similarity index 100%
rename from app/src/main/assets/gallery/23.webp
rename to script/blurhash/image/gallery/23.webp
diff --git a/app/src/main/assets/gallery/24.webp b/script/blurhash/image/gallery/24.webp
similarity index 100%
rename from app/src/main/assets/gallery/24.webp
rename to script/blurhash/image/gallery/24.webp
diff --git a/app/src/main/assets/gallery/3.webp b/script/blurhash/image/gallery/3.webp
similarity index 100%
rename from app/src/main/assets/gallery/3.webp
rename to script/blurhash/image/gallery/3.webp
diff --git a/app/src/main/assets/gallery/4.webp b/script/blurhash/image/gallery/4.webp
similarity index 100%
rename from app/src/main/assets/gallery/4.webp
rename to script/blurhash/image/gallery/4.webp
diff --git a/app/src/main/assets/gallery/5.webp b/script/blurhash/image/gallery/5.webp
similarity index 100%
rename from app/src/main/assets/gallery/5.webp
rename to script/blurhash/image/gallery/5.webp
diff --git a/app/src/main/assets/gallery/6.webp b/script/blurhash/image/gallery/6.webp
similarity index 100%
rename from app/src/main/assets/gallery/6.webp
rename to script/blurhash/image/gallery/6.webp
diff --git a/app/src/main/assets/gallery/7.webp b/script/blurhash/image/gallery/7.webp
similarity index 100%
rename from app/src/main/assets/gallery/7.webp
rename to script/blurhash/image/gallery/7.webp
diff --git a/app/src/main/assets/gallery/8.webp b/script/blurhash/image/gallery/8.webp
similarity index 100%
rename from app/src/main/assets/gallery/8.webp
rename to script/blurhash/image/gallery/8.webp
diff --git a/app/src/main/assets/gallery/9.webp b/script/blurhash/image/gallery/9.webp
similarity index 100%
rename from app/src/main/assets/gallery/9.webp
rename to script/blurhash/image/gallery/9.webp
diff --git a/script/blurhash/image/hash_snapshot_bg.jpg b/script/blurhash/image/hash_snapshot_bg.jpg
deleted file mode 100644
index 33271ae6..00000000
Binary files a/script/blurhash/image/hash_snapshot_bg.jpg and /dev/null differ
diff --git a/script/blurhash/image/night/hash_snapshot_bg.jpg b/script/blurhash/image/night/hash_snapshot_bg.jpg
deleted file mode 100644
index f17fc90a..00000000
Binary files a/script/blurhash/image/night/hash_snapshot_bg.jpg and /dev/null differ
diff --git a/script/blurhash/images.txt b/script/blurhash/images.txt
index 7b359857..dc032b65 100644
--- a/script/blurhash/images.txt
+++ b/script/blurhash/images.txt
@@ -1,5 +1,5 @@
-image/hash_snapshot_bg.jpg
image/hash_back_pressed_bg.jpg
-image/night/hash_snapshot_bg.jpg
-image/night/hash_back_pressed_bg.jpg
\ No newline at end of file
+image/night/hash_back_pressed_bg.jpg
+
+image/gallery
\ No newline at end of file
diff --git a/script/blurhash/multi_blurhash.py b/script/blurhash/multi_blurhash.py
index 3e723f01..d25b69f8 100644
--- a/script/blurhash/multi_blurhash.py
+++ b/script/blurhash/multi_blurhash.py
@@ -4,15 +4,38 @@
import os
with open("images.txt", "r", encoding='utf-8') as f:
- paths = f.readlines()
+ lines = f.readlines()
-xml_format = "{}\n"
+xml_string_format = "{}\n"
+xml_array_format = "\n{}\n"
+xml_array_item_format = "- {}
\n"
-for path in paths:
- path = path.strip()
- if len(path) == 0 or path.startswith("#"):
+def _blurhash(path):
+ return blurhash.encode(path, x_components=4, y_components=3)
+
+def string_name(path):
+ name = os.path.split(path)[1].split(".")[0]
+ if name.startswith("hash"):
+ return name
+ return "hash_{}".format(name)
+
+def blurhash_dir(dir):
+ files = os.listdir(dir)
+ print(files)
+ items = map(lambda path: _blurhash(os.path.join(dir, path)), files)
+ items = map(lambda hash: xml_array_item_format.format(hash), items)
+ print(xml_array_format.format(string_name(dir), "".join(items)))
+
+def blurhash_file(path):
+ hash = _blurhash(path)
+ print(xml_string_format.format(string_name(path), hash))
+
+for line in lines:
+ path = line.strip()
+ if len(path) == 0 or path.startswith("#") or os.path.exists(path) != True:
continue
print(path)
- hash = blurhash.encode(path, x_components=4, y_components=3)
- name = os.path.split(path)[1].split(".")[0]
- print(xml_format.format(name, hash))
+ if os.path.isdir(path):
+ blurhash_dir(path)
+ else:
+ blurhash_file(path)
diff --git a/script/icons/unicodes.json b/script/icons/unicodes.json
index f31c8bc0..1a687982 100644
--- a/script/icons/unicodes.json
+++ b/script/icons/unicodes.json
@@ -6,16 +6,12 @@
],
"rounded": [
"app_registration",
- "app_shortcut",
"brightness_auto",
"brightness_4",
"brightness_7",
"brightness_low",
- "expand_more",
"palette",
"settings",
- "swipe_left",
- "swipe_right",
"tips_and_updates"
],
"filled": []