diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index b10f2b619..d637034bc 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -148,13 +148,21 @@ android:name="org.koitharu.kotatsu.favourites.ui.categories.FavouriteCategoriesActivity" android:label="@string/manage_categories" /> + + + + + diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/core/prefs/AppWidgetConfig.kt b/app/src/main/kotlin/org/koitharu/kotatsu/core/prefs/AppWidgetConfig.kt index 082266b54..5ba52e13d 100644 --- a/app/src/main/kotlin/org/koitharu/kotatsu/core/prefs/AppWidgetConfig.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/core/prefs/AppWidgetConfig.kt @@ -1,15 +1,38 @@ package org.koitharu.kotatsu.core.prefs +import android.appwidget.AppWidgetProvider import android.content.Context +import android.os.Build import androidx.core.content.edit private const val CATEGORY_ID = "cat_id" +private const val BACKGROUND = "bg" -class AppWidgetConfig(context: Context, val widgetId: Int) { +class AppWidgetConfig( + context: Context, + cls: Class, + val widgetId: Int, +) { - private val prefs = context.getSharedPreferences("appwidget_$widgetId", Context.MODE_PRIVATE) + private val prefs = context.getSharedPreferences("appwidget_${cls.simpleName}_$widgetId", Context.MODE_PRIVATE) var categoryId: Long get() = prefs.getLong(CATEGORY_ID, 0L) set(value) = prefs.edit { putLong(CATEGORY_ID, value) } + + var hasBackground: Boolean + get() = prefs.getBoolean(BACKGROUND, Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) + set(value) = prefs.edit { putBoolean(BACKGROUND, value) } + + fun clear() { + prefs.edit { clear() } + } + + fun copyFrom(other: AppWidgetConfig) { + prefs.edit { + clear() + putLong(CATEGORY_ID, other.categoryId) + putBoolean(BACKGROUND, other.hasBackground) + } + } } diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/core/ui/BaseAppWidgetProvider.kt b/app/src/main/kotlin/org/koitharu/kotatsu/core/ui/BaseAppWidgetProvider.kt new file mode 100644 index 000000000..cddc4af55 --- /dev/null +++ b/app/src/main/kotlin/org/koitharu/kotatsu/core/ui/BaseAppWidgetProvider.kt @@ -0,0 +1,51 @@ +package org.koitharu.kotatsu.core.ui + +import android.appwidget.AppWidgetManager +import android.appwidget.AppWidgetProvider +import android.content.Context +import android.widget.RemoteViews +import androidx.annotation.CallSuper +import org.koitharu.kotatsu.core.prefs.AppWidgetConfig + +abstract class BaseAppWidgetProvider : AppWidgetProvider() { + + @CallSuper + override fun onUpdate( + context: Context, + appWidgetManager: AppWidgetManager, + appWidgetIds: IntArray + ) { + appWidgetIds.forEach { id -> + val config = AppWidgetConfig(context, javaClass, id) + val views = onUpdateWidget(context, config) + appWidgetManager.updateAppWidget(id, views) + } + } + + override fun onDeleted(context: Context, appWidgetIds: IntArray) { + super.onDeleted(context, appWidgetIds) + for (id in appWidgetIds) { + AppWidgetConfig(context, javaClass, id).clear() + } + } + + override fun onRestored(context: Context, oldWidgetIds: IntArray, newWidgetIds: IntArray) { + super.onRestored(context, oldWidgetIds, newWidgetIds) + if (oldWidgetIds.size != newWidgetIds.size) { + return + } + for (i in oldWidgetIds.indices) { + val oldId = oldWidgetIds[i] + val newId = newWidgetIds[i] + val oldConfig = AppWidgetConfig(context, javaClass, oldId) + val newConfig = AppWidgetConfig(context, javaClass, newId) + newConfig.copyFrom(oldConfig) + oldConfig.clear() + } + } + + protected abstract fun onUpdateWidget( + context: Context, + config: AppWidgetConfig, + ): RemoteViews +} diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/core/util/CompositeMutex2.kt b/app/src/main/kotlin/org/koitharu/kotatsu/core/util/CompositeMutex2.kt new file mode 100644 index 000000000..ebcf0e63d --- /dev/null +++ b/app/src/main/kotlin/org/koitharu/kotatsu/core/util/CompositeMutex2.kt @@ -0,0 +1,43 @@ +package org.koitharu.kotatsu.core.util + +import androidx.collection.ArrayMap +import kotlinx.coroutines.sync.Mutex + +class CompositeMutex2 : Set { + + private val delegates = ArrayMap() + + override val size: Int + get() = delegates.size + + override fun contains(element: T): Boolean { + return delegates.containsKey(element) + } + + override fun containsAll(elements: Collection): Boolean { + return elements.all { x -> delegates.containsKey(x) } + } + + override fun isEmpty(): Boolean { + return delegates.isEmpty + } + + override fun iterator(): Iterator { + return delegates.keys.iterator() + } + + suspend fun lock(element: T) { + val mutex = synchronized(delegates) { + delegates.getOrPut(element) { + Mutex() + } + } + mutex.lock() + } + + fun unlock(element: T) { + synchronized(delegates) { + delegates.remove(element)?.unlock() + } + } +} diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/widget/recent/RecentListFactory.kt b/app/src/main/kotlin/org/koitharu/kotatsu/widget/recent/RecentListFactory.kt index 3a317dd07..9dd997814 100644 --- a/app/src/main/kotlin/org/koitharu/kotatsu/widget/recent/RecentListFactory.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/widget/recent/RecentListFactory.kt @@ -55,6 +55,7 @@ class RecentListFactory( .data(item.coverUrl) .size(coverSize) .tag(item.source) + .tag(item) .transformations(transformation) .build(), ).getDrawableOrThrow().toBitmap() diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/widget/recent/RecentWidgetConfigActivity.kt b/app/src/main/kotlin/org/koitharu/kotatsu/widget/recent/RecentWidgetConfigActivity.kt new file mode 100644 index 000000000..aba7d594d --- /dev/null +++ b/app/src/main/kotlin/org/koitharu/kotatsu/widget/recent/RecentWidgetConfigActivity.kt @@ -0,0 +1,74 @@ +package org.koitharu.kotatsu.widget.recent + +import android.app.Activity +import android.appwidget.AppWidgetManager +import android.content.Intent +import android.os.Bundle +import android.view.View +import androidx.core.graphics.Insets +import androidx.core.view.updatePadding +import dagger.hilt.android.AndroidEntryPoint +import org.koitharu.kotatsu.R +import org.koitharu.kotatsu.core.prefs.AppWidgetConfig +import org.koitharu.kotatsu.core.ui.BaseActivity +import org.koitharu.kotatsu.databinding.ActivityAppwidgetRecentBinding +import com.google.android.material.R as materialR + +@AndroidEntryPoint +class RecentWidgetConfigActivity : + BaseActivity(), + View.OnClickListener { + + private lateinit var config: AppWidgetConfig + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + setContentView(ActivityAppwidgetRecentBinding.inflate(layoutInflater)) + supportActionBar?.run { + setDisplayHomeAsUpEnabled(true) + setHomeAsUpIndicator(materialR.drawable.abc_ic_clear_material) + } + viewBinding.buttonDone.setOnClickListener(this) + val appWidgetId = intent?.getIntExtra( + AppWidgetManager.EXTRA_APPWIDGET_ID, + AppWidgetManager.INVALID_APPWIDGET_ID, + ) ?: AppWidgetManager.INVALID_APPWIDGET_ID + if (appWidgetId == AppWidgetManager.INVALID_APPWIDGET_ID) { + finishAfterTransition() + return + } + config = AppWidgetConfig(this, RecentWidgetProvider::class.java, appWidgetId) + viewBinding.switchBackground.isChecked = config.hasBackground + } + + override fun onClick(v: View) { + when (v.id) { + R.id.button_done -> { + config.hasBackground = viewBinding.switchBackground.isChecked + updateWidget() + setResult( + Activity.RESULT_OK, + Intent().putExtra(AppWidgetManager.EXTRA_APPWIDGET_ID, config.widgetId), + ) + finish() + } + } + } + + override fun onWindowInsetsChanged(insets: Insets) { + viewBinding.root.updatePadding( + left = insets.left, + right = insets.right, + bottom = insets.bottom, + top = insets.top, + ) + } + + private fun updateWidget() { + val intent = Intent(this, RecentWidgetProvider::class.java) + intent.action = AppWidgetManager.ACTION_APPWIDGET_UPDATE + val ids = intArrayOf(config.widgetId) + intent.putExtra(AppWidgetManager.EXTRA_APPWIDGET_IDS, ids) + sendBroadcast(intent) + } +} diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/widget/recent/RecentWidgetProvider.kt b/app/src/main/kotlin/org/koitharu/kotatsu/widget/recent/RecentWidgetProvider.kt index dbeec5e2c..c94e449b9 100644 --- a/app/src/main/kotlin/org/koitharu/kotatsu/widget/recent/RecentWidgetProvider.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/widget/recent/RecentWidgetProvider.kt @@ -2,43 +2,52 @@ package org.koitharu.kotatsu.widget.recent import android.app.PendingIntent import android.appwidget.AppWidgetManager -import android.appwidget.AppWidgetProvider import android.content.Context import android.content.Intent +import android.graphics.Color import android.net.Uri import android.widget.RemoteViews import androidx.core.app.PendingIntentCompat import org.koitharu.kotatsu.R +import org.koitharu.kotatsu.core.prefs.AppWidgetConfig +import org.koitharu.kotatsu.core.ui.BaseAppWidgetProvider import org.koitharu.kotatsu.reader.ui.ReaderActivity -class RecentWidgetProvider : AppWidgetProvider() { +class RecentWidgetProvider : BaseAppWidgetProvider() { override fun onUpdate( context: Context, appWidgetManager: AppWidgetManager, appWidgetIds: IntArray ) { - appWidgetIds.forEach { id -> - val views = RemoteViews(context.packageName, R.layout.widget_recent) - val adapter = Intent(context, RecentWidgetService::class.java) - adapter.putExtra(AppWidgetManager.EXTRA_APPWIDGET_ID, id) - adapter.data = Uri.parse(adapter.toUri(Intent.URI_INTENT_SCHEME)) - views.setRemoteAdapter(R.id.stackView, adapter) - val intent = Intent(context, ReaderActivity::class.java) - intent.action = ReaderActivity.ACTION_MANGA_READ - views.setPendingIntentTemplate( - R.id.stackView, - PendingIntentCompat.getActivity( - context, - 0, - intent, - PendingIntent.FLAG_UPDATE_CURRENT, - true, - ), - ) - views.setEmptyView(R.id.stackView, R.id.textView_holder) - appWidgetManager.updateAppWidget(id, views) - } + super.onUpdate(context, appWidgetManager, appWidgetIds) appWidgetManager.notifyAppWidgetViewDataChanged(appWidgetIds, R.id.stackView) } + + override fun onUpdateWidget(context: Context, config: AppWidgetConfig): RemoteViews { + val views = RemoteViews(context.packageName, R.layout.widget_recent) + if (!config.hasBackground) { + views.setInt(R.id.widget_root, "setBackgroundColor", Color.TRANSPARENT) + } else { + views.setInt(R.id.widget_root, "setBackgroundResource", R.drawable.bg_appwidget_root) + } + val adapter = Intent(context, RecentWidgetService::class.java) + adapter.putExtra(AppWidgetManager.EXTRA_APPWIDGET_ID, config.widgetId) + adapter.data = Uri.parse(adapter.toUri(Intent.URI_INTENT_SCHEME)) + views.setRemoteAdapter(R.id.stackView, adapter) + val intent = Intent(context, ReaderActivity::class.java) + intent.action = ReaderActivity.ACTION_MANGA_READ + views.setPendingIntentTemplate( + R.id.stackView, + PendingIntentCompat.getActivity( + context, + 0, + intent, + PendingIntent.FLAG_UPDATE_CURRENT, + true, + ), + ) + views.setEmptyView(R.id.stackView, R.id.textView_holder) + return views + } } diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/widget/shelf/ShelfListFactory.kt b/app/src/main/kotlin/org/koitharu/kotatsu/widget/shelf/ShelfListFactory.kt index 38230a0df..4f66f82b1 100644 --- a/app/src/main/kotlin/org/koitharu/kotatsu/widget/shelf/ShelfListFactory.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/widget/shelf/ShelfListFactory.kt @@ -14,6 +14,7 @@ import kotlinx.coroutines.runBlocking import org.koitharu.kotatsu.R import org.koitharu.kotatsu.core.parser.MangaIntent import org.koitharu.kotatsu.core.prefs.AppWidgetConfig +import org.koitharu.kotatsu.core.ui.image.TrimTransformation import org.koitharu.kotatsu.core.util.ext.getDrawableOrThrow import org.koitharu.kotatsu.favourites.domain.FavouritesRepository import org.koitharu.kotatsu.parsers.model.Manga @@ -27,7 +28,7 @@ class ShelfListFactory( ) : RemoteViewsService.RemoteViewsFactory { private val dataSet = ArrayList() - private val config = AppWidgetConfig(context, widgetId) + private val config = AppWidgetConfig(context, ShelfWidgetProvider::class.java, widgetId) private val transformation = RoundedCornersTransformation( context.resources.getDimension(R.dimen.appwidget_corner_radius_inner), ) @@ -66,7 +67,8 @@ class ShelfListFactory( .data(item.coverUrl) .size(coverSize) .tag(item.source) - .transformations(transformation) + .tag(item) + .transformations(transformation, TrimTransformation()) .build(), ).getDrawableOrThrow().toBitmap() }.onSuccess { cover -> diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/widget/shelf/ShelfConfigActivity.kt b/app/src/main/kotlin/org/koitharu/kotatsu/widget/shelf/ShelfWidgetConfigActivity.kt similarity index 85% rename from app/src/main/kotlin/org/koitharu/kotatsu/widget/shelf/ShelfConfigActivity.kt rename to app/src/main/kotlin/org/koitharu/kotatsu/widget/shelf/ShelfWidgetConfigActivity.kt index e51c3c912..aee292134 100644 --- a/app/src/main/kotlin/org/koitharu/kotatsu/widget/shelf/ShelfConfigActivity.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/widget/shelf/ShelfWidgetConfigActivity.kt @@ -8,7 +8,6 @@ import android.view.View import android.view.ViewGroup import androidx.activity.viewModels import androidx.core.graphics.Insets -import androidx.core.view.isVisible import androidx.core.view.updateLayoutParams import androidx.core.view.updatePadding import dagger.hilt.android.AndroidEntryPoint @@ -19,14 +18,14 @@ import org.koitharu.kotatsu.core.ui.BaseActivity import org.koitharu.kotatsu.core.ui.list.OnListItemClickListener import org.koitharu.kotatsu.core.util.ext.observe import org.koitharu.kotatsu.core.util.ext.observeEvent -import org.koitharu.kotatsu.databinding.ActivityCategoriesBinding +import org.koitharu.kotatsu.databinding.ActivityAppwidgetShelfBinding import org.koitharu.kotatsu.widget.shelf.adapter.CategorySelectAdapter import org.koitharu.kotatsu.widget.shelf.model.CategoryItem import com.google.android.material.R as materialR @AndroidEntryPoint -class ShelfConfigActivity : - BaseActivity(), +class ShelfWidgetConfigActivity : + BaseActivity(), OnListItemClickListener, View.OnClickListener { @@ -37,16 +36,14 @@ class ShelfConfigActivity : override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) - setContentView(ActivityCategoriesBinding.inflate(layoutInflater)) + setContentView(ActivityAppwidgetShelfBinding.inflate(layoutInflater)) supportActionBar?.run { setDisplayHomeAsUpEnabled(true) setHomeAsUpIndicator(materialR.drawable.abc_ic_clear_material) } adapter = CategorySelectAdapter(this) viewBinding.recyclerView.adapter = adapter - viewBinding.buttonDone.isVisible = true viewBinding.buttonDone.setOnClickListener(this) - viewBinding.fabAdd.hide() val appWidgetId = intent?.getIntExtra( AppWidgetManager.EXTRA_APPWIDGET_ID, AppWidgetManager.INVALID_APPWIDGET_ID, @@ -55,8 +52,9 @@ class ShelfConfigActivity : finishAfterTransition() return } - config = AppWidgetConfig(this, appWidgetId) + config = AppWidgetConfig(this, ShelfWidgetProvider::class.java, appWidgetId) viewModel.checkedId = config.categoryId + viewBinding.switchBackground.isChecked = config.hasBackground viewModel.content.observe(this, this::onContentChanged) viewModel.onError.observeEvent(this, SnackbarErrorObserver(viewBinding.recyclerView, null)) @@ -66,6 +64,7 @@ class ShelfConfigActivity : when (v.id) { R.id.button_done -> { config.categoryId = viewModel.checkedId + config.hasBackground = viewBinding.switchBackground.isChecked updateWidget() setResult( Activity.RESULT_OK, @@ -81,11 +80,6 @@ class ShelfConfigActivity : } override fun onWindowInsetsChanged(insets: Insets) { - viewBinding.fabAdd.updateLayoutParams { - rightMargin = topMargin + insets.right - leftMargin = topMargin + insets.left - bottomMargin = topMargin + insets.bottom - } viewBinding.recyclerView.updatePadding( left = insets.left, right = insets.right, diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/widget/shelf/ShelfWidgetProvider.kt b/app/src/main/kotlin/org/koitharu/kotatsu/widget/shelf/ShelfWidgetProvider.kt index 00bcd2651..5246ecd3f 100644 --- a/app/src/main/kotlin/org/koitharu/kotatsu/widget/shelf/ShelfWidgetProvider.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/widget/shelf/ShelfWidgetProvider.kt @@ -2,43 +2,52 @@ package org.koitharu.kotatsu.widget.shelf import android.app.PendingIntent import android.appwidget.AppWidgetManager -import android.appwidget.AppWidgetProvider import android.content.Context import android.content.Intent +import android.graphics.Color import android.net.Uri import android.widget.RemoteViews import androidx.core.app.PendingIntentCompat import org.koitharu.kotatsu.R +import org.koitharu.kotatsu.core.prefs.AppWidgetConfig +import org.koitharu.kotatsu.core.ui.BaseAppWidgetProvider import org.koitharu.kotatsu.reader.ui.ReaderActivity -class ShelfWidgetProvider : AppWidgetProvider() { +class ShelfWidgetProvider : BaseAppWidgetProvider() { override fun onUpdate( context: Context, appWidgetManager: AppWidgetManager, appWidgetIds: IntArray ) { - appWidgetIds.forEach { id -> - val views = RemoteViews(context.packageName, R.layout.widget_shelf) - val adapter = Intent(context, ShelfWidgetService::class.java) - adapter.putExtra(AppWidgetManager.EXTRA_APPWIDGET_ID, id) - adapter.data = Uri.parse(adapter.toUri(Intent.URI_INTENT_SCHEME)) - views.setRemoteAdapter(R.id.gridView, adapter) - val intent = Intent(context, ReaderActivity::class.java) - intent.action = ReaderActivity.ACTION_MANGA_READ - views.setPendingIntentTemplate( - R.id.gridView, - PendingIntentCompat.getActivity( - context, - 0, - intent, - PendingIntent.FLAG_UPDATE_CURRENT, - true, - ), - ) - views.setEmptyView(R.id.gridView, R.id.textView_holder) - appWidgetManager.updateAppWidget(id, views) - } + super.onUpdate(context, appWidgetManager, appWidgetIds) appWidgetManager.notifyAppWidgetViewDataChanged(appWidgetIds, R.id.gridView) } + + override fun onUpdateWidget(context: Context, config: AppWidgetConfig): RemoteViews { + val views = RemoteViews(context.packageName, R.layout.widget_shelf) + if (!config.hasBackground) { + views.setInt(R.id.widget_root, "setBackgroundColor", Color.TRANSPARENT) + } else { + views.setInt(R.id.widget_root, "setBackgroundResource", R.drawable.bg_appwidget_root) + } + val adapter = Intent(context, ShelfWidgetService::class.java) + adapter.putExtra(AppWidgetManager.EXTRA_APPWIDGET_ID, config.widgetId) + adapter.data = Uri.parse(adapter.toUri(Intent.URI_INTENT_SCHEME)) + views.setRemoteAdapter(R.id.gridView, adapter) + val intent = Intent(context, ReaderActivity::class.java) + intent.action = ReaderActivity.ACTION_MANGA_READ + views.setPendingIntentTemplate( + R.id.gridView, + PendingIntentCompat.getActivity( + context, + 0, + intent, + PendingIntent.FLAG_UPDATE_CURRENT, + true, + ), + ) + views.setEmptyView(R.id.gridView, R.id.textView_holder) + return views + } } diff --git a/app/src/main/res/drawable-v31/bg_appwidget_card.xml b/app/src/main/res/drawable-v31/bg_appwidget_card.xml new file mode 100644 index 000000000..4cff15c71 --- /dev/null +++ b/app/src/main/res/drawable-v31/bg_appwidget_card.xml @@ -0,0 +1,7 @@ + + + + + diff --git a/app/src/main/res/drawable-v31/bg_appwidget_root.xml b/app/src/main/res/drawable-v31/bg_appwidget_root.xml new file mode 100644 index 000000000..cb7dbf180 --- /dev/null +++ b/app/src/main/res/drawable-v31/bg_appwidget_root.xml @@ -0,0 +1,7 @@ + + + + + diff --git a/app/src/main/res/drawable/bg_appwidget_card.xml b/app/src/main/res/drawable/bg_appwidget_card.xml index 35a460504..b18a84c01 100644 --- a/app/src/main/res/drawable/bg_appwidget_card.xml +++ b/app/src/main/res/drawable/bg_appwidget_card.xml @@ -3,5 +3,5 @@ xmlns:android="http://schemas.android.com/apk/res/android" android:shape="rectangle"> - - \ No newline at end of file + + diff --git a/app/src/main/res/drawable/bg_appwidget_root.xml b/app/src/main/res/drawable/bg_appwidget_root.xml new file mode 100644 index 000000000..189ce5e53 --- /dev/null +++ b/app/src/main/res/drawable/bg_appwidget_root.xml @@ -0,0 +1,7 @@ + + + + + diff --git a/app/src/main/res/layout/activity_appwidget_recent.xml b/app/src/main/res/layout/activity_appwidget_recent.xml new file mode 100644 index 000000000..7bb6d288e --- /dev/null +++ b/app/src/main/res/layout/activity_appwidget_recent.xml @@ -0,0 +1,39 @@ + + + + + +