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": []