diff --git a/.travis.yml b/.travis.yml
index f2ddd737..6329c873 100644
--- a/.travis.yml
+++ b/.travis.yml
@@ -1,4 +1,6 @@
language: android
+jdk:
+ - openjdk11
android:
components:
- tools
diff --git a/app/build.gradle b/app/build.gradle
index 4e4e1600..3ce72cab 100644
--- a/app/build.gradle
+++ b/app/build.gradle
@@ -1,11 +1,10 @@
apply plugin: 'com.android.application'
apply plugin: 'kotlin-android'
apply plugin: 'kotlin-kapt'
-apply plugin: "androidx.navigation.safeargs.kotlin"
+apply plugin: "androidx.navigation.safeargs"
apply plugin: 'com.google.firebase.crashlytics'
apply plugin: 'com.google.gms.google-services'
apply plugin: 'dagger.hilt.android.plugin'
-apply plugin: 'com.mikepenz.aboutlibraries.plugin'
apply plugin: 'com.google.firebase.firebase-perf'
apply plugin: 'com.github.triplet.play'
@@ -28,10 +27,15 @@ android {
// Version info
buildConfigField 'String', 'GIT_SHA', "\"${project.ext.gitHash}\""
+ vectorDrawables {
+ useSupportLibrary true
+ }
+
javaCompileOptions.annotationProcessorOptions.arguments['room.schemaLocation'] = rootProject.file('schemas').toString()
}
buildFeatures {
viewBinding true
+ compose true
}
compileOptions {
sourceCompatibility = JavaVersion.VERSION_1_8
@@ -47,6 +51,7 @@ android {
"-Xopt-in=kotlinx.coroutines.FlowPreview",
"-Xopt-in=kotlinx.coroutines.ExperimentalCoroutinesApi"
]
+ useIR = true
}
testOptions {
unitTests {
@@ -105,6 +110,9 @@ android {
exclude '**/NOTICE.txt'
exclude '**/*.gwt.xml'
}
+ composeOptions {
+ kotlinCompilerExtensionVersion Versions.androidXCompose
+ }
}
dependencies {
@@ -136,10 +144,13 @@ dependencies {
implementation Libs.androidx_fragment
implementation Libs.androidx_hilt_work
implementation Libs.androidx_lifecycle_viewmodel
+ implementation Libs.androidx_lifecycle_livedata
implementation Libs.androidx_lifecycle_java8
+ implementation Libs.androidx_lifecycle_runtime
implementation Libs.androidx_lifecycle_process
implementation Libs.androidx_navigation_fragment
implementation Libs.androidx_navigation_ui
+ implementation "androidx.navigation:navigation-compose:$Versions.androidXNavigation"
implementation Libs.androidx_preference
implementation Libs.androidx_recyclerview
implementation Libs.androidx_recyclerview_selection
@@ -147,6 +158,17 @@ dependencies {
implementation Libs.androidx_room_ktx
implementation Libs.androidx_work_runtime
implementation Libs.androidx_work_gcm
+ implementation 'com.google.android.material:material:1.3.0'
+ implementation 'androidx.activity:activity-compose:1.3.0-alpha08'
+ implementation "androidx.compose.ui:ui:$Versions.androidXCompose"
+ implementation "androidx.compose.foundation:foundation:$Versions.androidXCompose"
+ implementation "androidx.compose.material:material:$Versions.androidXCompose"
+ implementation "androidx.compose.material:material-icons-core:$Versions.androidXCompose"
+ implementation "androidx.compose.material:material-icons-extended:$Versions.androidXCompose"
+ implementation "androidx.compose.ui:ui-tooling:$Versions.androidXCompose"
+ implementation 'androidx.lifecycle:lifecycle-viewmodel-compose:1.0.0-alpha05'
+ implementation 'androidx.hilt:hilt-navigation-compose:1.0.0-alpha02'
+ androidTestImplementation "androidx.compose.ui:ui-test-junit4:$Versions.androidXCompose"
kapt Libs.androidx_room_compiler
kapt Libs.androidx_hilt_compiler
@@ -182,8 +204,8 @@ dependencies {
implementation Libs.kotlinCoroutinesAndroid
// LeakCanary
- debugImplementation Libs.leakCanary
- implementation Libs.leakCanaryPlumberAndroid
+// debugImplementation Libs.leakCanary
+// implementation Libs.leakCanaryPlumberAndroid
// Logging
implementation Libs.slf4jAndroidLogger
diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml
index 78d9bcdf..6da041b0 100644
--- a/app/src/main/AndroidManifest.xml
+++ b/app/src/main/AndroidManifest.xml
@@ -65,21 +65,19 @@
android:name=".receivers.UpdateReceiver"
android:exported="false" />
+
+
-
-
+ android:name="androidx.work.impl.WorkManagerInitializer"
+ android:authorities="${applicationId}.workmanager-init"
+ tools:node="remove" />
: Fragment() {
+open class BaseFragment : Fragment() {
@Inject
lateinit var trackingService: TrackingService
-
- private var _binding: T? = null
- // This property is only valid between onCreateView and onDestroyView.
- protected val binding get() = _binding!!
-
protected var toolbarTitle: CharSequence
get() = requireActivity().title
set(value) {
requireActivity().title = value
}
-
- final override fun onCreateView(
- inflater: LayoutInflater,
- container: ViewGroup?,
- savedInstanceState: Bundle?
- ): View = createViewBinding(inflater, container, savedInstanceState).also { viewBinding ->
- _binding = viewBinding
- }.root
-
- protected abstract fun createViewBinding(
- inflater: LayoutInflater,
- container: ViewGroup?,
- savedInstanceState: Bundle?
- ): T
-
- @CallSuper
- override fun onDestroyView() {
- super.onDestroyView()
- _binding = null
- }
+ val Fragment.viewLifecycleScope: LifecycleCoroutineScope
+ get() = viewLifecycleOwner.lifecycleScope
protected fun updateToolbarTitle(@StringRes resId: Int) {
requireActivity().setTitle(resId)
trackingService.trackScreen(this::class.java, getString(resId))
}
-
- val Fragment.viewLifecycleScope: LifecycleCoroutineScope
- get() = viewLifecycleOwner.lifecycleScope
-
-}
+}
\ No newline at end of file
diff --git a/app/src/main/java/de/psdev/devdrawer/DevDrawerApplication.kt b/app/src/main/java/de/psdev/devdrawer/DevDrawerApplication.kt
index 06ce493d..86e192d1 100644
--- a/app/src/main/java/de/psdev/devdrawer/DevDrawerApplication.kt
+++ b/app/src/main/java/de/psdev/devdrawer/DevDrawerApplication.kt
@@ -29,7 +29,13 @@ class DevDrawerApplication: Application(), Configuration.Provider {
registerAppInstallationReceiver()
setupWorkers()
}.let {
- logger.warn("{} version {} ({}) took {}ms to init", this::class.java.simpleName, BuildConfig.VERSION_NAME, BuildConfig.VERSION_CODE, it)
+ logger.warn(
+ "{} version {} ({}) took {}ms to init",
+ this::class.java.simpleName,
+ BuildConfig.VERSION_NAME,
+ BuildConfig.VERSION_CODE,
+ it
+ )
}
}
@@ -37,12 +43,9 @@ class DevDrawerApplication: Application(), Configuration.Provider {
// Configuration.Provider
// ==========================================================================================================================
- override fun getWorkManagerConfiguration(): Configuration {
- logger.warn { "getWorkManagerConfiguration" }
- return Configuration.Builder()
- .setWorkerFactory(workerFactory)
- .build()
- }
+ override fun getWorkManagerConfiguration(): Configuration = Configuration.Builder()
+ .setWorkerFactory(workerFactory)
+ .build()
// ==========================================================================================================================
// Private API
diff --git a/app/src/main/java/de/psdev/devdrawer/ViewBindingBaseFragment.kt b/app/src/main/java/de/psdev/devdrawer/ViewBindingBaseFragment.kt
new file mode 100644
index 00000000..d5d17130
--- /dev/null
+++ b/app/src/main/java/de/psdev/devdrawer/ViewBindingBaseFragment.kt
@@ -0,0 +1,37 @@
+package de.psdev.devdrawer
+
+import android.os.Bundle
+import android.view.LayoutInflater
+import android.view.View
+import android.view.ViewGroup
+import androidx.annotation.CallSuper
+import androidx.viewbinding.ViewBinding
+
+abstract class ViewBindingBaseFragment : BaseFragment() {
+
+ private var _binding: T? = null
+
+ // This property is only valid between onCreateView and onDestroyView.
+ protected val binding get() = _binding!!
+
+ final override fun onCreateView(
+ inflater: LayoutInflater,
+ container: ViewGroup?,
+ savedInstanceState: Bundle?
+ ): View = createViewBinding(inflater, container, savedInstanceState).also { viewBinding ->
+ _binding = viewBinding
+ }.root
+
+ protected abstract fun createViewBinding(
+ inflater: LayoutInflater,
+ container: ViewGroup?,
+ savedInstanceState: Bundle?
+ ): T
+
+ @CallSuper
+ override fun onDestroyView() {
+ super.onDestroyView()
+ _binding = null
+ }
+
+}
diff --git a/app/src/main/java/de/psdev/devdrawer/about/AboutFragment.kt b/app/src/main/java/de/psdev/devdrawer/about/AboutFragment.kt
index fa33fa0b..19a64538 100644
--- a/app/src/main/java/de/psdev/devdrawer/about/AboutFragment.kt
+++ b/app/src/main/java/de/psdev/devdrawer/about/AboutFragment.kt
@@ -13,13 +13,13 @@ import com.mikepenz.aboutlibraries.Libs
import com.mikepenz.aboutlibraries.LibsBuilder
import com.mikepenz.aboutlibraries.util.LibsListenerImpl
import dagger.hilt.android.AndroidEntryPoint
-import de.psdev.devdrawer.BaseFragment
import de.psdev.devdrawer.R
+import de.psdev.devdrawer.ViewBindingBaseFragment
import de.psdev.devdrawer.databinding.FragmentAboutBinding
import de.psdev.devdrawer.utils.consume
@AndroidEntryPoint
-class AboutFragment : BaseFragment() {
+class AboutFragment : ViewBindingBaseFragment() {
override fun createViewBinding(
inflater: LayoutInflater,
diff --git a/app/src/main/java/de/psdev/devdrawer/appwidget/DDWidgetProvider.kt b/app/src/main/java/de/psdev/devdrawer/appwidget/DDWidgetProvider.kt
index 5e5e9bf7..6c0ec875 100644
--- a/app/src/main/java/de/psdev/devdrawer/appwidget/DDWidgetProvider.kt
+++ b/app/src/main/java/de/psdev/devdrawer/appwidget/DDWidgetProvider.kt
@@ -6,16 +6,13 @@ import android.appwidget.AppWidgetProvider
import android.content.Context
import android.content.Intent
import android.content.Intent.FLAG_ACTIVITY_NEW_TASK
-import android.graphics.Color
import android.net.Uri
import android.widget.RemoteViews
import dagger.hilt.android.AndroidEntryPoint
import de.psdev.devdrawer.R
import de.psdev.devdrawer.database.DevDrawerDatabase
import de.psdev.devdrawer.database.Widget
-import de.psdev.devdrawer.database.WidgetProfile
import de.psdev.devdrawer.receivers.UpdateReceiver
-import de.psdev.devdrawer.utils.Constants
import de.psdev.devdrawer.utils.textColorForBackground
import de.psdev.devdrawer.widgets.WidgetConfigActivity
import kotlinx.coroutines.Dispatchers
@@ -41,34 +38,6 @@ class DDWidgetProvider : AppWidgetProvider() {
// AppWidgetProvider
// ==========================================================================================================================
- override fun onReceive(context: Context, intent: Intent) {
- super.onReceive(context, intent)
- when (intent.action) {
- Constants.ACTION_WIDGET_PINNED -> GlobalScope.launch(Dispatchers.IO) {
- val widgetDao = devDrawerDatabase.widgetDao()
- val widgetProfileDao = devDrawerDatabase.widgetProfileDao()
- val defaultWidgetProfile = widgetProfileDao.findAll().firstOrNull()
- ?: WidgetProfile(name = "Default").also {
- widgetProfileDao.insert(it)
- }
- val widgetId = intent.getIntExtra(
- AppWidgetManager.EXTRA_APPWIDGET_ID,
- AppWidgetManager.INVALID_APPWIDGET_ID
- )
-
- // Create entries in database
- val widget = Widget(
- id = widgetId,
- name = "Widget $widgetId",
- color = Color.BLACK,
- profileId = defaultWidgetProfile.id
- )
- widgetDao.insert(widget)
- UpdateReceiver.send(context)
- }
- }
- }
-
override fun onUpdate(context: Context, appWidgetManager: AppWidgetManager, appWidgetIds: IntArray) {
super.onUpdate(context, appWidgetManager, appWidgetIds)
GlobalScope.launch(Dispatchers.IO) {
@@ -126,8 +95,12 @@ class DDWidgetProvider : AppWidgetProvider() {
val configActivityIntent = WidgetConfigActivity.createStartIntent(context, widget.id)
configActivityIntent.putExtra("from_widget", true)
configActivityIntent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TASK or FLAG_ACTIVITY_NEW_TASK)
- val configActivityPendingIntent =
- PendingIntent.getActivity(context, 0, configActivityIntent, PendingIntent.FLAG_UPDATE_CURRENT)
+ val configActivityPendingIntent = PendingIntent.getActivity(
+ context,
+ 0,
+ configActivityIntent,
+ PendingIntent.FLAG_UPDATE_CURRENT
+ )
widgetView.setOnClickPendingIntent(R.id.btn_settings, configActivityPendingIntent)
// Apps list
diff --git a/app/src/main/java/de/psdev/devdrawer/database/WidgetDao.kt b/app/src/main/java/de/psdev/devdrawer/database/WidgetDao.kt
index 82cdd972..a6712aca 100644
--- a/app/src/main/java/de/psdev/devdrawer/database/WidgetDao.kt
+++ b/app/src/main/java/de/psdev/devdrawer/database/WidgetDao.kt
@@ -2,10 +2,11 @@ package de.psdev.devdrawer.database
import androidx.room.Dao
import androidx.room.Query
+import androidx.room.Transaction
import kotlinx.coroutines.flow.Flow
@Dao
-abstract class WidgetDao : BaseDao() {
+abstract class WidgetDao: BaseDao() {
@Query("SELECT * FROM widgets")
abstract suspend fun findAll(): List
@@ -19,6 +20,10 @@ abstract class WidgetDao : BaseDao() {
@Query("SELECT * FROM widgets WHERE id = :id")
abstract suspend fun findById(id: Int): Widget?
+ @Transaction
+ @Query("SELECT * FROM widgets WHERE id = :id")
+ abstract fun widgetWithIdObservable(id: Int): Flow
+
@Query("DELETE FROM widgets WHERE id IN (:ids)")
abstract suspend fun deleteByIds(ids: List)
diff --git a/app/src/main/java/de/psdev/devdrawer/database/WidgetProfileDao.kt b/app/src/main/java/de/psdev/devdrawer/database/WidgetProfileDao.kt
index b190ac55..78e4c3f2 100644
--- a/app/src/main/java/de/psdev/devdrawer/database/WidgetProfileDao.kt
+++ b/app/src/main/java/de/psdev/devdrawer/database/WidgetProfileDao.kt
@@ -2,10 +2,11 @@ package de.psdev.devdrawer.database
import androidx.room.Dao
import androidx.room.Query
+import androidx.room.Transaction
import kotlinx.coroutines.flow.Flow
@Dao
-abstract class WidgetProfileDao: BaseDao() {
+abstract class WidgetProfileDao : BaseDao() {
@Query("SELECT * FROM widget_profiles")
abstract suspend fun findAll(): List
@@ -15,4 +16,8 @@ abstract class WidgetProfileDao: BaseDao() {
@Query("SELECT * FROM widget_profiles WHERE id = :id")
abstract suspend fun findById(id: String): WidgetProfile?
+ @Transaction
+ @Query("SELECT * FROM widget_profiles WHERE id = :id")
+ abstract fun widgetProfileWithIdObservable(id: String): Flow
+
}
diff --git a/app/src/main/java/de/psdev/devdrawer/profiles/PackageFilterListAdapter.kt b/app/src/main/java/de/psdev/devdrawer/profiles/PackageFilterListAdapter.kt
deleted file mode 100644
index bbd49014..00000000
--- a/app/src/main/java/de/psdev/devdrawer/profiles/PackageFilterListAdapter.kt
+++ /dev/null
@@ -1,101 +0,0 @@
-package de.psdev.devdrawer.profiles
-
-import android.view.ViewGroup
-import androidx.core.view.isVisible
-import androidx.recyclerview.selection.SelectionTracker
-import androidx.recyclerview.widget.ListAdapter
-import androidx.recyclerview.widget.RecyclerView
-import com.google.android.material.dialog.MaterialAlertDialogBuilder
-import de.psdev.devdrawer.R
-import de.psdev.devdrawer.database.FilterType
-import de.psdev.devdrawer.database.PackageFilter
-import de.psdev.devdrawer.databinding.ListItemPackageFilterBinding
-import de.psdev.devdrawer.utils.layoutInflater
-
-class PackageFilterListAdapter(
- private val onDeleteClickListener: PackageFilterActionListener,
- private val onPreviewFilterClickListener: PackageFilterActionListener
-) : ListAdapter(PackageFilter.DIFF_CALLBACK) {
-
- var selectionTracker: SelectionTracker? = null
-
- // ==========================================================================================================================
- // ListAdapter
- // ==========================================================================================================================
-
- override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): PackageFilterViewHolder {
- val onClickListener: PackageFilterActionListener = { packageFilter ->
- selectionTracker?.select(packageFilter.id)
- }
- return PackageFilterViewHolder(
- binding = ListItemPackageFilterBinding.inflate(parent.layoutInflater, parent, false),
- onClickListener = onClickListener,
- onDeleteClickListener = onDeleteClickListener,
- onPreviewFilterClickListener = onPreviewFilterClickListener
- )
- }
-
- override fun onBindViewHolder(holder: PackageFilterViewHolder, position: Int) {
- val packageFilter = getItem(position)
- val isSelected = selectionTracker?.isSelected(packageFilter.id) ?: false
- holder.bindTo(packageFilter, isSelected)
- }
-
- public override fun getItem(position: Int): PackageFilter = super.getItem(position)
-
- class PackageFilterViewHolder(
- private val binding: ListItemPackageFilterBinding,
- private val onClickListener: PackageFilterActionListener,
- private val onDeleteClickListener: PackageFilterActionListener,
- private val onPreviewFilterClickListener: PackageFilterActionListener
- ) : RecyclerView.ViewHolder(binding.root) {
- var currentItem: PackageFilter? = null
- private set
-
- fun bindTo(packageFilter: PackageFilter, isActivated: Boolean = false) {
- currentItem = packageFilter
- with(binding) {
- root.isActivated = isActivated
- root.setOnClickListener {
- onClickListener(packageFilter)
- }
- val iconRes = when (packageFilter.type) {
- FilterType.PACKAGE_NAME -> R.drawable.ic_regex
- FilterType.SIGNATURE -> R.drawable.ic_certificate
- }
- imgIcon.setImageResource(iconRes)
- txtName.text = when (packageFilter.type) {
- FilterType.PACKAGE_NAME -> packageFilter.filter
- FilterType.SIGNATURE -> packageFilter.description
- }
-
- with(btnPreview) {
- setOnClickListener {
- onPreviewFilterClickListener(packageFilter)
- }
- }
- with(btnInfo) {
- isVisible = packageFilter.type == FilterType.SIGNATURE
- setOnClickListener {
- val text = when (packageFilter.type) {
- FilterType.PACKAGE_NAME -> packageFilter.description
- FilterType.SIGNATURE -> "SHA256: ${
- packageFilter.filter.uppercase().chunkedSequence(2)
- .joinToString(separator = ":")
- }"
- }
- MaterialAlertDialogBuilder(itemView.context)
- .setTitle(R.string.info)
- .setMessage(text)
- .setPositiveButton(R.string.close, null)
- .show()
- }
- }
- btnDelete.setOnClickListener {
- onDeleteClickListener(packageFilter)
- }
- }
- }
- }
-
-}
\ No newline at end of file
diff --git a/app/src/main/java/de/psdev/devdrawer/profiles/WidgetEditorViewModel.kt b/app/src/main/java/de/psdev/devdrawer/profiles/WidgetEditorViewModel.kt
new file mode 100644
index 00000000..33dcaa22
--- /dev/null
+++ b/app/src/main/java/de/psdev/devdrawer/profiles/WidgetEditorViewModel.kt
@@ -0,0 +1,60 @@
+package de.psdev.devdrawer.profiles
+
+import androidx.lifecycle.SavedStateHandle
+import androidx.lifecycle.ViewModel
+import androidx.lifecycle.viewModelScope
+import dagger.hilt.android.lifecycle.HiltViewModel
+import de.psdev.devdrawer.database.DevDrawerDatabase
+import de.psdev.devdrawer.database.Widget
+import de.psdev.devdrawer.database.WidgetProfile
+import kotlinx.coroutines.flow.MutableStateFlow
+import kotlinx.coroutines.flow.combine
+import kotlinx.coroutines.flow.distinctUntilChanged
+import kotlinx.coroutines.launch
+import javax.inject.Inject
+
+@HiltViewModel
+class WidgetEditorViewModel @Inject constructor(
+ private val savedStateHandle: SavedStateHandle,
+ private val database: DevDrawerDatabase
+): ViewModel() {
+ private val widgetId: Int = savedStateHandle.get("widgetId")!!
+
+ private val editableWidgetState: MutableStateFlow = MutableStateFlow(null)
+
+ val state = combine(
+ database.widgetDao().widgetWithIdObservable(widgetId),
+ database.widgetProfileDao().findAllFlow().distinctUntilChanged(),
+ editableWidgetState
+
+ ) { persistedWidget, widgetProfiles, editableWidget ->
+ if (editableWidgetState.value == null) {
+ editableWidgetState.value = persistedWidget
+ }
+ WidgetEditorViewState(
+ persistedWidget = persistedWidget,
+ widgetProfiles = widgetProfiles,
+ editableWidget = editableWidget
+ )
+ }
+
+ fun onNameChanged(newName: String) {
+ editableWidgetState.value = editableWidgetState.value?.copy(
+ name = newName
+ )
+ }
+
+ fun onWidgetProfileSelected(widgetProfile: WidgetProfile) {
+ editableWidgetState.value = editableWidgetState.value?.copy(
+ profileId = widgetProfile.id
+ )
+ }
+
+ fun saveChanges() {
+ editableWidgetState.value?.let {
+ viewModelScope.launch {
+ database.widgetDao().update(it)
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/de/psdev/devdrawer/profiles/WidgetEditorViewState.kt b/app/src/main/java/de/psdev/devdrawer/profiles/WidgetEditorViewState.kt
new file mode 100644
index 00000000..48bf58f2
--- /dev/null
+++ b/app/src/main/java/de/psdev/devdrawer/profiles/WidgetEditorViewState.kt
@@ -0,0 +1,16 @@
+package de.psdev.devdrawer.profiles
+
+import androidx.compose.runtime.Immutable
+import de.psdev.devdrawer.database.Widget
+import de.psdev.devdrawer.database.WidgetProfile
+
+@Immutable
+data class WidgetEditorViewState(
+ val persistedWidget: Widget? = null,
+ val editableWidget: Widget? = null,
+ val widgetProfiles: List = emptyList()
+) {
+ companion object {
+ val Empty = WidgetEditorViewState()
+ }
+}
diff --git a/app/src/main/java/de/psdev/devdrawer/profiles/WidgetProfileCard.kt b/app/src/main/java/de/psdev/devdrawer/profiles/WidgetProfileCard.kt
new file mode 100644
index 00000000..a284757f
--- /dev/null
+++ b/app/src/main/java/de/psdev/devdrawer/profiles/WidgetProfileCard.kt
@@ -0,0 +1,61 @@
+package de.psdev.devdrawer.profiles
+
+import android.content.res.Configuration
+import androidx.compose.foundation.clickable
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.fillMaxWidth
+import androidx.compose.foundation.layout.padding
+import androidx.compose.foundation.layout.wrapContentHeight
+import androidx.compose.material.*
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.CompositionLocalProvider
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.res.stringResource
+import androidx.compose.ui.tooling.preview.Preview
+import androidx.compose.ui.unit.dp
+import de.psdev.devdrawer.R
+import de.psdev.devdrawer.database.WidgetProfile
+import de.psdev.devdrawer.ui.theme.DevDrawerTheme
+
+@Composable
+fun WidgetProfileCard(
+ widgetProfile: WidgetProfile,
+ onWidgetProfileClick: (WidgetProfile) -> Unit = {}
+) {
+ Card(
+ modifier = Modifier
+ .fillMaxWidth()
+ .wrapContentHeight()
+ .padding(8.dp)
+ .clickable { onWidgetProfileClick(widgetProfile) }
+ ) {
+ Column(
+ modifier = Modifier
+ .fillMaxWidth()
+ .wrapContentHeight()
+ .padding(16.dp)
+ ) {
+ Text(
+ modifier = Modifier.fillMaxWidth(),
+ style = MaterialTheme.typography.body1,
+ text = widgetProfile.name
+ )
+ CompositionLocalProvider(LocalContentAlpha provides ContentAlpha.medium) {
+ Text(
+ modifier = Modifier.fillMaxWidth(),
+ style = MaterialTheme.typography.body2,
+ text = stringResource(id = R.string.widget_profile_id_template, widgetProfile.id)
+ )
+ }
+ }
+ }
+}
+
+@Preview
+@Preview(uiMode = Configuration.UI_MODE_NIGHT_YES)
+@Composable
+fun Preview_WidgetProfileCard() {
+ DevDrawerTheme {
+ WidgetProfileCard(widgetProfile = WidgetProfile(name = "Test profile"))
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/de/psdev/devdrawer/profiles/WidgetProfileEditFragment.kt b/app/src/main/java/de/psdev/devdrawer/profiles/WidgetProfileEditFragment.kt
index 2d3d1614..781dee96 100644
--- a/app/src/main/java/de/psdev/devdrawer/profiles/WidgetProfileEditFragment.kt
+++ b/app/src/main/java/de/psdev/devdrawer/profiles/WidgetProfileEditFragment.kt
@@ -1,173 +1,157 @@
package de.psdev.devdrawer.profiles
-import android.database.sqlite.SQLiteConstraintException
import android.os.Bundle
-import android.view.*
-import androidx.core.view.isVisible
+import android.view.LayoutInflater
+import android.view.View
+import android.view.ViewGroup
+import androidx.compose.ui.platform.ComposeView
import androidx.lifecycle.lifecycleScope
import androidx.navigation.fragment.findNavController
-import androidx.navigation.fragment.navArgs
-import androidx.recyclerview.widget.LinearLayoutManager
import com.google.android.material.dialog.MaterialAlertDialogBuilder
-import com.google.android.material.snackbar.Snackbar
import dagger.hilt.android.AndroidEntryPoint
import de.psdev.devdrawer.BaseFragment
import de.psdev.devdrawer.R
import de.psdev.devdrawer.database.DevDrawerDatabase
-import de.psdev.devdrawer.database.WidgetProfile
-import de.psdev.devdrawer.databinding.FragmentWidgetProfileEditBinding
+import de.psdev.devdrawer.database.FilterType
import de.psdev.devdrawer.receivers.UpdateReceiver
-import de.psdev.devdrawer.utils.awaitSubmit
-import de.psdev.devdrawer.utils.consume
-import kotlinx.coroutines.flow.*
-import kotlinx.coroutines.launch
+import de.psdev.devdrawer.ui.theme.DevDrawerTheme
import mu.KLogging
-import reactivecircus.flowbinding.android.view.clicks
-import reactivecircus.flowbinding.android.widget.textChanges
import javax.inject.Inject
@AndroidEntryPoint
-class WidgetProfileEditFragment : BaseFragment() {
+class WidgetProfileEditFragment : BaseFragment() {
companion object : KLogging()
@Inject
lateinit var devDrawerDatabase: DevDrawerDatabase
- private val args by navArgs()
-
- private val onDeleteClickListener: PackageFilterActionListener = { packageFilter ->
- MaterialAlertDialogBuilder(requireContext())
- .setTitle("Delete?")
- .setNegativeButton(R.string.no) { _, _ -> }
- .setPositiveButton(R.string.yes) { _, _ ->
- lifecycleScope.launchWhenResumed {
- devDrawerDatabase.packageFilterDao().deleteById(packageFilter.id)
- UpdateReceiver.send(requireContext())
- }
- }
- .show()
- }
- private val onPreviewFilterClickListener: PackageFilterActionListener = { packageFilter ->
- findNavController().navigate(
- WidgetProfileEditFragmentDirections.openFilterPreviewBottomSheetDialogFragment(
- packageFilterId = packageFilter.id
- )
- )
- }
- private val listAdapter: PackageFilterListAdapter = PackageFilterListAdapter(
- onDeleteClickListener = onDeleteClickListener,
- onPreviewFilterClickListener = onPreviewFilterClickListener
- )
- private var widgetProfile: WidgetProfile? = null
-
- private var changedWidgetProfileProperty: MutableStateFlow = MutableStateFlow(false)
-
- override fun onCreate(savedInstanceState: Bundle?) {
- super.onCreate(savedInstanceState)
- setHasOptionsMenu(true)
- }
-
- override fun createViewBinding(
- inflater: LayoutInflater,
- container: ViewGroup?,
- savedInstanceState: Bundle?
- ): FragmentWidgetProfileEditBinding = FragmentWidgetProfileEditBinding.inflate(inflater, container, false)
-
- override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
- super.onViewCreated(view, savedInstanceState)
- with(binding) {
- val context = requireContext()
-
- btnAddFilter.setOnClickListener { _ ->
- widgetProfile?.let {
- val directions =
- WidgetProfileEditFragmentDirections.openAddPackageFilterBottomSheetDialogFragment(
- widgetProfileId = it.id
- )
- findNavController().navigate(directions)
- }
- }
-
- btnAddSignature.setOnClickListener {
- widgetProfile?.let {
- val directions =
- WidgetProfileEditFragmentDirections.openAppSignatureChooserBottomSheetDialogFragment(
- widgetProfileId = it.id
- )
- findNavController().navigate(directions)
+ override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View =
+ ComposeView(requireContext()).apply {
+ setContent {
+ DevDrawerTheme {
+ WidgetProfileEditor(
+ onAddPackageFilterClick = {
+ val directions =
+ WidgetProfileEditFragmentDirections.openAddPackageFilterBottomSheetDialogFragment(it.id)
+ findNavController().navigate(directions)
+ },
+ onAddAppSignatureClick = {
+ val directions =
+ WidgetProfileEditFragmentDirections.openAppSignatureChooserBottomSheetDialogFragment(it.id)
+ findNavController().navigate(directions)
+ },
+ onPackageFilterPreviewClick = { packageFilter ->
+ val directions =
+ WidgetProfileEditFragmentDirections.openFilterPreviewBottomSheetDialogFragment(packageFilter.id)
+ findNavController().navigate(directions)
+ },
+ onPackageFilterInfoClick = { packageFilter ->
+ val text = when (packageFilter.type) {
+ FilterType.PACKAGE_NAME -> packageFilter.description
+ FilterType.SIGNATURE -> "SHA256: ${
+ packageFilter.filter.uppercase().chunkedSequence(2)
+ .joinToString(separator = ":")
+ }"
+ }
+ MaterialAlertDialogBuilder(requireContext())
+ .setTitle(R.string.info)
+ .setMessage(text)
+ .setPositiveButton(R.string.close, null)
+ .show()
+ },
+ onDeletePackageFilterClick = { packageFilter ->
+ MaterialAlertDialogBuilder(requireContext())
+ .setTitle("Delete?")
+ .setNegativeButton(R.string.no) { _, _ -> }
+ .setPositiveButton(R.string.yes) { _, _ ->
+ lifecycleScope.launchWhenResumed {
+ devDrawerDatabase.packageFilterDao().deleteById(packageFilter.id)
+ UpdateReceiver.send(requireContext())
+ }
+ }
+ .show()
+ }
+ )
}
}
- editName.textChanges().skipInitialValue().map { it.toString() }.onEach {
- widgetProfile?.let { widgetProfile ->
- if (widgetProfile.name != it) {
- widgetProfile.name = it
- changedWidgetProfileProperty.value = true
- }
- }
- }.launchIn(viewLifecycleScope)
-
- changedWidgetProfileProperty.onEach {
- btnApply.isVisible = it
- }.launchIn(viewLifecycleScope)
-
- btnApply.clicks().mapNotNull { widgetProfile }.onEach {
- devDrawerDatabase.widgetProfileDao().insertOrUpdate(it)
- editName.clearFocus()
- changedWidgetProfileProperty.value = false
- }.launchIn(viewLifecycleScope)
-
- recyclerPackages.layoutManager = LinearLayoutManager(context, LinearLayoutManager.VERTICAL, false)
- recyclerPackages.adapter = listAdapter
}
- lifecycleScope.launchWhenResumed {
- val profile = devDrawerDatabase.widgetProfileDao().findById(args.profileId)!!
- binding.editName.setText(profile.name)
- widgetProfile = profile
-
- }
- devDrawerDatabase.packageFilterDao().findAllByProfileFlow(args.profileId).onEach {
- listAdapter.awaitSubmit(it)
- }.launchIn(viewLifecycleScope)
- }
-
- override fun onCreateOptionsMenu(menu: Menu, inflater: MenuInflater) {
- super.onCreateOptionsMenu(menu, inflater)
- inflater.inflate(R.menu.menu_fragment_widget_profile_edit, menu)
- }
+// override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
+// super.onViewCreated(view, savedInstanceState)
+// with(binding) {
+// val context = requireContext()
+//
+// editName.textChanges().skipInitialValue().map { it.toString() }.onEach {
+// widgetProfile?.let { widgetProfile ->
+// if (widgetProfile.name != it) {
+// widgetProfile.name = it
+// changedWidgetProfileProperty.value = true
+// }
+// }
+// }.launchIn(viewLifecycleScope)
+//
+// changedWidgetProfileProperty.onEach {
+// btnApply.isVisible = it
+// }.launchIn(viewLifecycleScope)
+//
+// btnApply.clicks().mapNotNull { widgetProfile }.onEach {
+// devDrawerDatabase.widgetProfileDao().insertOrUpdate(it)
+// editName.clearFocus()
+// changedWidgetProfileProperty.value = false
+// }.launchIn(viewLifecycleScope)
+//
+// recyclerPackages.layoutManager = LinearLayoutManager(context, LinearLayoutManager.VERTICAL, false)
+// recyclerPackages.adapter = listAdapter
+// }
+//
+// lifecycleScope.launchWhenResumed {
+// val profile = devDrawerDatabase.widgetProfileDao().findById(args.profileId)!!
+// binding.editName.setText(profile.name)
+// widgetProfile = profile
+//
+// }
+// devDrawerDatabase.packageFilterDao().findAllByProfileFlow(args.profileId).onEach {
+// listAdapter.awaitSubmit(it)
+// }.launchIn(viewLifecycleScope)
+// }
+//
+// override fun onCreateOptionsMenu(menu: Menu, inflater: MenuInflater) {
+// super.onCreateOptionsMenu(menu, inflater)
+// inflater.inflate(R.menu.menu_fragment_widget_profile_edit, menu)
+// }
override fun onResume() {
super.onResume()
updateToolbarTitle(R.string.edit_profile)
}
- override fun onOptionsItemSelected(item: MenuItem): Boolean = when (item.itemId) {
- R.id.action_delete -> consume {
- MaterialAlertDialogBuilder(requireContext())
- .setTitle("Delete profile?")
- .setNegativeButton(R.string.no) { _, _ -> }
- .setPositiveButton(R.string.yes) { _, _ ->
- widgetProfile?.let { widgetProfile ->
- lifecycleScope.launch {
- try {
- devDrawerDatabase.widgetProfileDao().delete(widgetProfile)
- UpdateReceiver.send(requireContext())
- findNavController().popBackStack()
- } catch (e: SQLiteConstraintException) {
- Snackbar.make(binding.root, R.string.error_profile_in_use, Snackbar.LENGTH_LONG).show()
- }
- }
- }
- }
- .show()
- }
- else -> super.onOptionsItemSelected(item)
- }
-
- override fun onDestroyView() {
- binding.recyclerPackages.adapter = null
- super.onDestroyView()
- }
+// override fun onOptionsItemSelected(item: MenuItem): Boolean = when (item.itemId) {
+// R.id.action_delete -> consume {
+// MaterialAlertDialogBuilder(requireContext())
+// .setTitle("Delete profile?")
+// .setNegativeButton(R.string.no) { _, _ -> }
+// .setPositiveButton(R.string.yes) { _, _ ->
+// widgetProfile?.let { widgetProfile ->
+// lifecycleScope.launch {
+// try {
+// devDrawerDatabase.widgetProfileDao().delete(widgetProfile)
+// UpdateReceiver.send(requireContext())
+// findNavController().popBackStack()
+// } catch (e: SQLiteConstraintException) {
+// Snackbar.make(binding.root, R.string.error_profile_in_use, Snackbar.LENGTH_LONG).show()
+// }
+// }
+// }
+// }
+// .show()
+// }
+// else -> super.onOptionsItemSelected(item)
+// }
+
+// override fun onDestroyView() {
+// binding.recyclerPackages.adapter = null
+// super.onDestroyView()
+// }
}
diff --git a/app/src/main/java/de/psdev/devdrawer/profiles/WidgetProfileEditor.kt b/app/src/main/java/de/psdev/devdrawer/profiles/WidgetProfileEditor.kt
new file mode 100644
index 00000000..2c7e90dc
--- /dev/null
+++ b/app/src/main/java/de/psdev/devdrawer/profiles/WidgetProfileEditor.kt
@@ -0,0 +1,285 @@
+package de.psdev.devdrawer.profiles
+
+import android.content.res.Configuration
+import androidx.compose.animation.AnimatedVisibility
+import androidx.compose.animation.ExperimentalAnimationApi
+import androidx.compose.foundation.clickable
+import androidx.compose.foundation.layout.*
+import androidx.compose.foundation.lazy.LazyColumn
+import androidx.compose.foundation.lazy.items
+import androidx.compose.material.*
+import androidx.compose.material.icons.Icons
+import androidx.compose.material.icons.filled.Delete
+import androidx.compose.material.icons.filled.Info
+import androidx.compose.material.icons.filled.Preview
+import androidx.compose.material.icons.outlined.Save
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.collectAsState
+import androidx.compose.runtime.getValue
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.Modifier
+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.hilt.navigation.compose.hiltViewModel
+import de.psdev.devdrawer.R
+import de.psdev.devdrawer.database.FilterType
+import de.psdev.devdrawer.database.PackageFilter
+import de.psdev.devdrawer.database.WidgetProfile
+import de.psdev.devdrawer.ui.theme.DevDrawerTheme
+import de.psdev.devdrawer.utils.rememberFlowWithLifecycle
+import java.util.*
+
+@Composable
+fun WidgetProfileEditor(
+ onAddPackageFilterClick: (WidgetProfile) -> Unit = {},
+ onAddAppSignatureClick: (WidgetProfile) -> Unit = {},
+ onPackageFilterPreviewClick: (PackageFilter) -> Unit = {},
+ onPackageFilterInfoClick: (PackageFilter) -> Unit = {},
+ onDeletePackageFilterClick: (PackageFilter) -> Unit = {}
+) {
+ WidgetProfileEditor(
+ viewModel = hiltViewModel(),
+ onAddPackageFilterClick = onAddPackageFilterClick,
+ onAddAppSignatureClick = onAddAppSignatureClick,
+ onPackageFilterPreviewClick = onPackageFilterPreviewClick,
+ onPackageFilterInfoClick = onPackageFilterInfoClick,
+ onDeletePackageFilterClick = onDeletePackageFilterClick
+ )
+}
+
+@Composable
+fun WidgetProfileEditor(
+ viewModel: WidgetProfileEditorViewModel,
+ onAddPackageFilterClick: (WidgetProfile) -> Unit = {},
+ onAddAppSignatureClick: (WidgetProfile) -> Unit = {},
+ onPackageFilterPreviewClick: (PackageFilter) -> Unit = {},
+ onPackageFilterInfoClick: (PackageFilter) -> Unit = {},
+ onDeletePackageFilterClick: (PackageFilter) -> Unit = {}
+) {
+ val viewState by rememberFlowWithLifecycle(viewModel.state)
+ .collectAsState(initial = WidgetProfileEditorViewState.Empty)
+
+ WidgetProfileEditor(
+ viewState = viewState,
+ onNameChange = {
+ viewModel.onNameChanged(it)
+ },
+ onSaveNameClick = {
+ viewModel.saveChanges(viewState)
+ },
+ onAddPackageFilterClick = onAddPackageFilterClick,
+ onAddAppSignatureClick = onAddAppSignatureClick,
+ onPackageFilterPreviewClick = onPackageFilterPreviewClick,
+ onPackageFilterInfoClick = onPackageFilterInfoClick,
+ onDeletePackageFilterClick = onDeletePackageFilterClick
+ )
+}
+
+@OptIn(ExperimentalAnimationApi::class)
+@Composable
+fun WidgetProfileEditor(
+ viewState: WidgetProfileEditorViewState,
+ onNameChange: (String) -> Unit = {},
+ onSaveNameClick: () -> Unit = {},
+ onAddPackageFilterClick: (WidgetProfile) -> Unit = {},
+ onAddAppSignatureClick: (WidgetProfile) -> Unit = {},
+ onPackageFilterPreviewClick: (PackageFilter) -> Unit = {},
+ onPackageFilterInfoClick: (PackageFilter) -> Unit = {},
+ onDeletePackageFilterClick: (PackageFilter) -> Unit = {}
+) {
+ val widgetProfile = viewState.widgetProfile
+ if (widgetProfile == null) {
+ // Loading
+ Box(modifier = Modifier.fillMaxSize(), contentAlignment = Alignment.Center) {
+ CircularProgressIndicator(modifier = Modifier.size(64.dp))
+ }
+ } else {
+ Column {
+ Surface(modifier = Modifier.wrapContentHeight(), elevation = 2.dp) {
+ Column(
+ modifier = Modifier
+ .wrapContentHeight()
+ .padding(8.dp)
+ ) {
+ WidgetProfileName(
+ widgetName = viewState.widgetName ?: widgetProfile.name,
+ currentName = widgetProfile.name,
+ onNameChange = onNameChange,
+ onSaveNameClick = onSaveNameClick
+ )
+ Row(
+ modifier = Modifier.padding(top = 8.dp),
+ horizontalArrangement = Arrangement.spacedBy(8.dp)
+ ) {
+ Button(
+ modifier = Modifier.weight(1f),
+ onClick = { onAddPackageFilterClick(widgetProfile) }
+ ) {
+ Icon(
+ modifier = Modifier.size(ButtonDefaults.IconSize),
+ painter = painterResource(id = R.drawable.ic_regex),
+ contentDescription = stringResource(id = R.string.add_package_name)
+ )
+ Spacer(Modifier.size(ButtonDefaults.IconSpacing))
+ Text(text = stringResource(id = R.string.add_package_name).uppercase(Locale.getDefault()))
+ }
+ Button(
+ modifier = Modifier.weight(1f),
+ onClick = { onAddAppSignatureClick(widgetProfile) }
+ ) {
+ Icon(
+ modifier = Modifier.size(ButtonDefaults.IconSize),
+ painter = painterResource(id = R.drawable.ic_certificate),
+ contentDescription = stringResource(id = R.string.add_app_signature)
+ )
+ Spacer(Modifier.size(ButtonDefaults.IconSpacing))
+ Text(text = stringResource(id = R.string.add_app_signature).uppercase(Locale.getDefault()))
+ }
+ }
+ }
+ }
+ LazyColumn(
+ modifier = Modifier.weight(1f),
+ contentPadding = PaddingValues(8.dp),
+ verticalArrangement = Arrangement.spacedBy(8.dp)
+ ) {
+ items(viewState.packageFilters) { packageFilter ->
+ Card {
+ Row(
+ modifier = Modifier.padding(8.dp),
+ verticalAlignment = Alignment.CenterVertically
+ ) {
+ val iconRes = when (packageFilter.type) {
+ FilterType.PACKAGE_NAME -> R.drawable.ic_regex
+ FilterType.SIGNATURE -> R.drawable.ic_certificate
+ }
+ Icon(
+ modifier = Modifier.padding(8.dp),
+ painter = painterResource(id = iconRes),
+ contentDescription = null
+ )
+ val text = when (packageFilter.type) {
+ FilterType.PACKAGE_NAME -> packageFilter.filter
+ FilterType.SIGNATURE -> packageFilter.description
+ }
+ Text(modifier = Modifier.weight(1f), text = text)
+ AnimatedVisibility(visible = packageFilter.type == FilterType.SIGNATURE) {
+ Icon(
+ modifier = Modifier
+ .clickable { onPackageFilterInfoClick(packageFilter) }
+ .padding(8.dp),
+ imageVector = Icons.Filled.Info,
+ contentDescription = null
+ )
+ }
+ Icon(
+ modifier = Modifier
+ .clickable { onPackageFilterPreviewClick(packageFilter) }
+ .padding(8.dp),
+ imageVector = Icons.Filled.Preview,
+ contentDescription = null
+ )
+ Icon(
+ modifier = Modifier
+ .clickable { onDeletePackageFilterClick(packageFilter) }
+ .padding(8.dp),
+ imageVector = Icons.Filled.Delete,
+ contentDescription = null
+ )
+ }
+ }
+ }
+ }
+ }
+ }
+}
+
+@OptIn(ExperimentalAnimationApi::class)
+@Composable
+fun WidgetProfileName(
+ widgetName: String,
+ currentName: String,
+ onNameChange: (String) -> Unit = {},
+ onSaveNameClick: () -> Unit = {}
+) {
+ Surface {
+ Row(
+ horizontalArrangement = Arrangement.Center,
+ verticalAlignment = Alignment.CenterVertically
+ ) {
+ OutlinedTextField(
+ modifier = Modifier.weight(1f),
+ singleLine = true,
+ value = widgetName,
+ onValueChange = onNameChange,
+ label = { Text(text = stringResource(id = R.string.name)) }
+ )
+ AnimatedVisibility(visible = widgetName != currentName) {
+ Button(
+ modifier = Modifier.weight(1f),
+ onClick = onSaveNameClick
+ ) {
+ Icon(
+ modifier = Modifier.size(ButtonDefaults.IconSize),
+ imageVector = Icons.Outlined.Save,
+ contentDescription = stringResource(id = R.string.save)
+ )
+ Spacer(Modifier.size(ButtonDefaults.IconSpacing))
+ Text(text = stringResource(id = R.string.save))
+ }
+ }
+ }
+ }
+}
+
+@Preview(showSystemUi = true)
+@Preview(showSystemUi = true, uiMode = Configuration.UI_MODE_NIGHT_YES)
+@Composable
+fun Preview_WidgetProfileEditor_Loading() {
+ DevDrawerTheme {
+ WidgetProfileEditor(
+ viewState = WidgetProfileEditorViewState.Empty
+ )
+ }
+}
+
+@Preview(showSystemUi = true)
+@Preview(showSystemUi = true, uiMode = Configuration.UI_MODE_NIGHT_YES)
+@Composable
+fun Preview_WidgetProfileEditor_Loaded() {
+ val widgetProfile = WidgetProfile(
+ id = UUID.randomUUID().toString(),
+ name = "Test widget profile"
+ )
+ DevDrawerTheme {
+ WidgetProfileEditor(
+ viewState = WidgetProfileEditorViewState(
+ widgetProfile = widgetProfile,
+ widgetName = widgetProfile.name,
+ packageFilters = listOf(
+ PackageFilter(profileId = widgetProfile.id, filter = "01022402020", type = FilterType.SIGNATURE),
+ PackageFilter(profileId = widgetProfile.id, filter = "com.example2.*")
+ )
+ )
+ )
+ }
+}
+
+@Preview(showSystemUi = true)
+@Preview(showSystemUi = true, uiMode = Configuration.UI_MODE_NIGHT_YES)
+@Composable
+fun Preview_WidgetProfileEditor_NameChanged() {
+ DevDrawerTheme {
+ WidgetProfileEditor(
+ viewState = WidgetProfileEditorViewState(
+ widgetProfile = WidgetProfile(
+ id = UUID.randomUUID().toString(),
+ name = "Test widget profile"
+ ),
+ widgetName = "Test widget profile 2"
+ )
+ )
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/de/psdev/devdrawer/profiles/WidgetProfileEditorViewModel.kt b/app/src/main/java/de/psdev/devdrawer/profiles/WidgetProfileEditorViewModel.kt
new file mode 100644
index 00000000..5b3ec359
--- /dev/null
+++ b/app/src/main/java/de/psdev/devdrawer/profiles/WidgetProfileEditorViewModel.kt
@@ -0,0 +1,50 @@
+package de.psdev.devdrawer.profiles
+
+import androidx.lifecycle.SavedStateHandle
+import androidx.lifecycle.ViewModel
+import androidx.lifecycle.viewModelScope
+import dagger.hilt.android.lifecycle.HiltViewModel
+import de.psdev.devdrawer.database.DevDrawerDatabase
+import kotlinx.coroutines.flow.MutableStateFlow
+import kotlinx.coroutines.flow.combine
+import kotlinx.coroutines.flow.distinctUntilChanged
+import kotlinx.coroutines.launch
+import javax.inject.Inject
+
+@HiltViewModel
+class WidgetProfileEditorViewModel @Inject constructor(
+ private val savedStateHandle: SavedStateHandle,
+ private val database: DevDrawerDatabase
+) : ViewModel() {
+
+ private val widgetProfileId: String = savedStateHandle.get("profileId")!!
+ private val widgetNameState: MutableStateFlow = MutableStateFlow(null)
+
+ val state = combine(
+ database.widgetProfileDao().widgetProfileWithIdObservable(widgetProfileId),
+ database.packageFilterDao().findAllByProfileFlow(widgetProfileId).distinctUntilChanged(),
+ widgetNameState
+ ) { widgetProfile, packageFilters, name ->
+ WidgetProfileEditorViewState(
+ widgetProfile = widgetProfile,
+ widgetName = name ?: widgetProfile.name,
+ packageFilters = packageFilters
+ )
+ }
+
+ fun onNameChanged(newName: String) {
+ widgetNameState.value = newName
+ }
+
+ fun saveChanges(viewState: WidgetProfileEditorViewState) {
+ val widgetProfile = viewState.widgetProfile ?: return
+ viewModelScope.launch {
+ database.widgetProfileDao().update(
+ widgetProfile.copy(
+ name = viewState.widgetName ?: widgetProfile.name
+ )
+ )
+ }
+ }
+
+}
\ No newline at end of file
diff --git a/app/src/main/java/de/psdev/devdrawer/profiles/WidgetProfileEditorViewState.kt b/app/src/main/java/de/psdev/devdrawer/profiles/WidgetProfileEditorViewState.kt
new file mode 100644
index 00000000..d1310c36
--- /dev/null
+++ b/app/src/main/java/de/psdev/devdrawer/profiles/WidgetProfileEditorViewState.kt
@@ -0,0 +1,16 @@
+package de.psdev.devdrawer.profiles
+
+import androidx.compose.runtime.Immutable
+import de.psdev.devdrawer.database.PackageFilter
+import de.psdev.devdrawer.database.WidgetProfile
+
+@Immutable
+data class WidgetProfileEditorViewState(
+ val widgetProfile: WidgetProfile? = null,
+ val widgetName: String? = null,
+ val packageFilters: List = emptyList()
+) {
+ companion object {
+ val Empty = WidgetProfileEditorViewState()
+ }
+}
diff --git a/app/src/main/java/de/psdev/devdrawer/profiles/WidgetProfileList.kt b/app/src/main/java/de/psdev/devdrawer/profiles/WidgetProfileList.kt
new file mode 100644
index 00000000..0935fdb5
--- /dev/null
+++ b/app/src/main/java/de/psdev/devdrawer/profiles/WidgetProfileList.kt
@@ -0,0 +1,46 @@
+package de.psdev.devdrawer.profiles
+
+import android.content.res.Configuration
+import androidx.compose.foundation.layout.fillMaxHeight
+import androidx.compose.foundation.layout.fillMaxWidth
+import androidx.compose.foundation.lazy.LazyColumn
+import androidx.compose.foundation.lazy.items
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.tooling.preview.Preview
+import de.psdev.devdrawer.database.WidgetProfile
+import de.psdev.devdrawer.ui.theme.DevDrawerTheme
+
+@Composable
+fun WidgetProfileList(
+ widgetProfiles: List,
+ onWidgetProfileClick: (WidgetProfile) -> Unit = {},
+ modifier: Modifier = Modifier
+) {
+ LazyColumn(
+ modifier = modifier
+ .fillMaxWidth()
+ .fillMaxHeight()
+ ) {
+ items(widgetProfiles, key = { it.id }) { widgetProfile ->
+ WidgetProfileCard(
+ widgetProfile = widgetProfile,
+ onWidgetProfileClick = onWidgetProfileClick
+ )
+ }
+ }
+}
+
+@Preview
+@Preview(uiMode = Configuration.UI_MODE_NIGHT_YES)
+@Composable
+fun Preview_WidgetProfileList() {
+ DevDrawerTheme {
+ WidgetProfileList(
+ listOf(
+ WidgetProfile(name = "Profile 1"),
+ WidgetProfile(name = "Profile 2")
+ )
+ )
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/de/psdev/devdrawer/profiles/WidgetProfileListFragment.kt b/app/src/main/java/de/psdev/devdrawer/profiles/WidgetProfileListFragment.kt
index 769ee55f..2d1201e1 100644
--- a/app/src/main/java/de/psdev/devdrawer/profiles/WidgetProfileListFragment.kt
+++ b/app/src/main/java/de/psdev/devdrawer/profiles/WidgetProfileListFragment.kt
@@ -1,142 +1,62 @@
package de.psdev.devdrawer.profiles
-import android.database.sqlite.SQLiteConstraintException
import android.os.Bundle
-import android.view.*
+import android.view.LayoutInflater
+import android.view.View
+import android.view.ViewGroup
+import androidx.compose.runtime.collectAsState
+import androidx.compose.ui.platform.ComposeView
import androidx.lifecycle.lifecycleScope
-import androidx.navigation.fragment.findNavController
-import androidx.recyclerview.selection.SelectionPredicates
-import androidx.recyclerview.selection.SelectionTracker
-import androidx.recyclerview.selection.StorageStrategy
-import com.google.android.material.snackbar.Snackbar
+import androidx.navigation.findNavController
import dagger.hilt.android.AndroidEntryPoint
import de.psdev.devdrawer.BaseFragment
import de.psdev.devdrawer.R
import de.psdev.devdrawer.database.DevDrawerDatabase
import de.psdev.devdrawer.database.WidgetProfile
-import de.psdev.devdrawer.databinding.FragmentWidgetProfileListBinding
-import de.psdev.devdrawer.utils.awaitSubmit
-import de.psdev.devdrawer.utils.consume
-import kotlinx.coroutines.flow.collect
+import de.psdev.devdrawer.ui.theme.DevDrawerTheme
import kotlinx.coroutines.launch
import mu.KLogging
import javax.inject.Inject
@AndroidEntryPoint
-class WidgetProfileListFragment: BaseFragment() {
+class WidgetProfileListFragment : BaseFragment() {
- companion object: KLogging()
+ companion object : KLogging()
- // Dependencies
@Inject
lateinit var devDrawerDatabase: DevDrawerDatabase
- val listAdapter: WidgetProfilesListAdapter = WidgetProfilesListAdapter()
- var _selectionTracker: SelectionTracker? = null
-
- override fun onCreate(savedInstanceState: Bundle?) {
- super.onCreate(savedInstanceState)
- setHasOptionsMenu(true)
- }
-
- override fun createViewBinding(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): FragmentWidgetProfileListBinding =
- FragmentWidgetProfileListBinding.inflate(inflater, container, false)
-
- override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
- super.onViewCreated(view, savedInstanceState)
- binding.recyclerProfiles.adapter = listAdapter
- val selectionTracker = SelectionTracker.Builder(
- "widgetProfile",
- binding.recyclerProfiles,
- WidgetProfilesItemKeyProvider(listAdapter),
- WidgetProfilesDetailsLookup(binding.recyclerProfiles),
- StorageStrategy.createStringStorage()
- ).withSelectionPredicate(SelectionPredicates.createSelectSingleAnything()).build().also { tracker ->
- tracker.onRestoreInstanceState(savedInstanceState)
- tracker.addObserver(object: SelectionTracker.SelectionObserver() {
- override fun onSelectionChanged() {
- super.onSelectionChanged()
- activity?.invalidateOptionsMenu()
- }
- })
- _selectionTracker = tracker
- }
- listAdapter.selectionTracker = selectionTracker
- viewLifecycleOwner.lifecycleScope.launch {
- val widgetProfileDao = devDrawerDatabase.widgetProfileDao()
- widgetProfileDao.findAllFlow().collect {
- logger.warn { "$it" }
- listAdapter.awaitSubmit(it)
- binding.recyclerProfiles.scrollToPosition(it.indexOfFirst { selectionTracker.isSelected(it.id) })
+ override fun onCreateView(
+ inflater: LayoutInflater,
+ container: ViewGroup?,
+ savedInstanceState: Bundle?
+ ): View = ComposeView(requireContext()).apply {
+ setContent {
+ DevDrawerTheme {
+ val widgetProfiles = devDrawerDatabase.widgetProfileDao().findAllFlow()
+ .collectAsState(initial = emptyList())
+ WidgetProfileListScreen(
+ profiles = widgetProfiles.value,
+ onWidgetProfileClick = { widgetProfile ->
+ findNavController().navigate(WidgetProfileListFragmentDirections.editWidgetProfile(widgetProfile.id))
+ },
+ onCreateWidgetProfileClick = {
+ lifecycleScope.launch {
+ val widgetProfileDao = devDrawerDatabase.widgetProfileDao()
+ val size = widgetProfileDao.findAll().size
+ val widgetProfile = WidgetProfile(name = "Profile ${size + 1}")
+ widgetProfileDao.insert(widgetProfile)
+ }
+ }
+ )
}
}
- childFragmentManager.setFragmentResultListener("createProfile", viewLifecycleOwner) { _, bundle ->
- // We use a String here, but any type that can be put in a Bundle is supported
- val result = bundle.getString("profileId") ?: selectionTracker.selection.firstOrNull() ?: ""
- selectionTracker.select(result)
- }
}
- override fun onCreateOptionsMenu(menu: Menu, inflater: MenuInflater) {
- super.onCreateOptionsMenu(menu, inflater)
- inflater.inflate(R.menu.menu_profiles_list, menu)
- val hasSelection = _selectionTracker?.hasSelection() ?: false
- menu.findItem(R.id.action_create).isVisible = !hasSelection
- menu.findItem(R.id.action_edit).isVisible = hasSelection
- menu.findItem(R.id.action_delete).isVisible = hasSelection
- }
override fun onResume() {
super.onResume()
updateToolbarTitle(R.string.profiles)
}
- override fun onOptionsItemSelected(item: MenuItem): Boolean = when (item.itemId) {
- R.id.action_create -> consume {
- lifecycleScope.launchWhenResumed {
- val widgetProfileDao = devDrawerDatabase.widgetProfileDao()
- val size = widgetProfileDao.findAll().size
- val widgetProfile = WidgetProfile(name = "Profile ${size + 1}")
- widgetProfileDao.insert(widgetProfile)
- findNavController().navigate(WidgetProfileListFragmentDirections.editWidgetProfile(widgetProfile.id))
- }
- }
- R.id.action_edit -> consume {
- val selectedId = _selectionTracker?.selection?.firstOrNull()
- if (selectedId != null) {
- findNavController().navigate(WidgetProfileListFragmentDirections.editWidgetProfile(selectedId))
- }
- }
- R.id.action_delete -> consume {
- lifecycleScope.launchWhenStarted {
- _selectionTracker?.let { tracker ->
- val selectedProfile = tracker.selection.firstOrNull()
- if (selectedProfile != null) {
- val widgetProfile = devDrawerDatabase.widgetProfileDao().findById(selectedProfile)
- if (widgetProfile != null) {
- try {
- devDrawerDatabase.widgetProfileDao().delete(widgetProfile)
- } catch (e: SQLiteConstraintException) {
- Snackbar.make(binding.root, R.string.error_profile_in_use, Snackbar.LENGTH_LONG).show()
- }
- }
- tracker.deselect(selectedProfile)
- }
- }
- }
- }
- else -> super.onOptionsItemSelected(item)
- }
-
- override fun onSaveInstanceState(outState: Bundle) {
- super.onSaveInstanceState(outState)
- _selectionTracker?.onSaveInstanceState(outState)
- }
-
- override fun onDestroyView() {
- _selectionTracker = null
- listAdapter.selectionTracker = null
- binding.recyclerProfiles.adapter = null
- super.onDestroyView()
- }
}
diff --git a/app/src/main/java/de/psdev/devdrawer/profiles/WidgetProfilesListScreen.kt b/app/src/main/java/de/psdev/devdrawer/profiles/WidgetProfilesListScreen.kt
new file mode 100644
index 00000000..669531f9
--- /dev/null
+++ b/app/src/main/java/de/psdev/devdrawer/profiles/WidgetProfilesListScreen.kt
@@ -0,0 +1,67 @@
+package de.psdev.devdrawer.profiles
+
+import android.content.res.Configuration
+import androidx.compose.foundation.layout.*
+import androidx.compose.material.*
+import androidx.compose.material.icons.Icons
+import androidx.compose.material.icons.outlined.Add
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.res.stringResource
+import androidx.compose.ui.tooling.preview.Preview
+import androidx.compose.ui.unit.dp
+import de.psdev.devdrawer.R
+import de.psdev.devdrawer.database.WidgetProfile
+import de.psdev.devdrawer.ui.theme.DevDrawerTheme
+
+@Composable
+fun WidgetProfileListScreen(
+ profiles: List,
+ onWidgetProfileClick: (WidgetProfile) -> Unit = {},
+ onCreateWidgetProfileClick: () -> Unit = {}
+) {
+ if (profiles.isEmpty()) {
+ Column(
+ modifier = Modifier
+ .fillMaxWidth()
+ .fillMaxHeight(),
+ horizontalAlignment = Alignment.CenterHorizontally,
+ verticalArrangement = Arrangement.Center
+ ) {
+ Text(
+ color = MaterialTheme.colors.onBackground,
+ text = stringResource(id = R.string.no_profiles)
+ )
+ Spacer(modifier = Modifier.size(16.dp))
+ Button(onClick = onCreateWidgetProfileClick) {
+ Icon(
+ imageVector = Icons.Outlined.Add,
+ contentDescription = stringResource(id = R.string.widget_profile_list_create_new)
+ )
+ Text(text = stringResource(id = R.string.widget_profile_list_create_new))
+ }
+ }
+ } else {
+ Box(modifier = Modifier.fillMaxSize()) {
+ WidgetProfileList(widgetProfiles = profiles, onWidgetProfileClick = onWidgetProfileClick)
+ FloatingActionButton(
+ onClick = onCreateWidgetProfileClick,
+ modifier = Modifier
+ .align(Alignment.BottomEnd)
+ .padding(end = 16.dp, bottom = 16.dp)
+ ) {
+ Icon(imageVector = Icons.Outlined.Add, contentDescription = stringResource(id = R.string.widget_profile_list_create_new))
+ }
+ }
+ }
+}
+
+@Preview(showSystemUi = true)
+@Preview(showSystemUi = true, uiMode = Configuration.UI_MODE_NIGHT_YES)
+@Composable
+fun Preview_WidgetProfileListScreen_Empty() {
+ DevDrawerTheme {
+ WidgetProfileListScreen(profiles = emptyList())
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/de/psdev/devdrawer/receivers/PinWidgetSuccessReceiver.kt b/app/src/main/java/de/psdev/devdrawer/receivers/PinWidgetSuccessReceiver.kt
new file mode 100644
index 00000000..be09343a
--- /dev/null
+++ b/app/src/main/java/de/psdev/devdrawer/receivers/PinWidgetSuccessReceiver.kt
@@ -0,0 +1,38 @@
+package de.psdev.devdrawer.receivers
+
+import android.appwidget.AppWidgetManager
+import android.content.BroadcastReceiver
+import android.content.Context
+import android.content.Intent
+import androidx.work.Data
+import androidx.work.ExistingWorkPolicy
+import androidx.work.OneTimeWorkRequestBuilder
+import androidx.work.WorkManager
+import de.psdev.devdrawer.widgets.SaveWidgetWorker
+import mu.KLogging
+
+class PinWidgetSuccessReceiver : BroadcastReceiver() {
+
+ companion object : KLogging() {
+ fun intent(context: Context): Intent = Intent(context, PinWidgetSuccessReceiver::class.java)
+ }
+
+ override fun onReceive(context: Context, intent: Intent) {
+ logger.warn { "onReceive[context=$context, intent=$intent]" }
+ val widgetId = intent.getIntExtra(
+ AppWidgetManager.EXTRA_APPWIDGET_ID,
+ AppWidgetManager.INVALID_APPWIDGET_ID
+ )
+ if (widgetId != AppWidgetManager.INVALID_APPWIDGET_ID) {
+ val inputData = Data.Builder().putInt(SaveWidgetWorker.ARG_WIDGET_ID, widgetId).build()
+ val request = OneTimeWorkRequestBuilder()
+ .setInputData(inputData)
+ .build()
+ WorkManager.getInstance(context).enqueueUniqueWork(
+ "SAVE_WIDGET_$widgetId",
+ ExistingWorkPolicy.REPLACE,
+ request
+ )
+ }
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/de/psdev/devdrawer/settings/SettingsFragment.kt b/app/src/main/java/de/psdev/devdrawer/settings/SettingsFragment.kt
index f03d2127..a619a57a 100644
--- a/app/src/main/java/de/psdev/devdrawer/settings/SettingsFragment.kt
+++ b/app/src/main/java/de/psdev/devdrawer/settings/SettingsFragment.kt
@@ -39,8 +39,7 @@ class SettingsFragment: PreferenceFragmentCompat() {
preference.summary = sortOrderLabelFromValue(newValue.toString())
val appWidgetManager = AppWidgetManager.getInstance(context)
- val appWidgetIds =
- appWidgetManager.getAppWidgetIds(ComponentName(context, DDWidgetProvider::class.java))
+ val appWidgetIds = appWidgetManager.getAppWidgetIds(ComponentName(context, DDWidgetProvider::class.java))
appWidgetManager.notifyAppWidgetViewDataChanged(appWidgetIds, R.id.listView)
return@setOnPreferenceChangeListener true
diff --git a/app/src/main/java/de/psdev/devdrawer/ui/theme/Color.kt b/app/src/main/java/de/psdev/devdrawer/ui/theme/Color.kt
new file mode 100644
index 00000000..99e85a57
--- /dev/null
+++ b/app/src/main/java/de/psdev/devdrawer/ui/theme/Color.kt
@@ -0,0 +1,11 @@
+package de.psdev.devdrawer.ui.theme
+
+import androidx.compose.ui.graphics.Color
+
+val LightGreen500 = Color(0xFF8BC34A)
+val LightGreen700 = Color(0xFF689F38)
+val LightGreen200 = Color(0xFFC5E1A5)
+
+val Orange500 = Color(0xFFff9800)
+val Orange700 = Color(0xFFf57c00)
+val Orange200 = Color(0xFFffcc80)
\ No newline at end of file
diff --git a/app/src/main/java/de/psdev/devdrawer/ui/theme/Shape.kt b/app/src/main/java/de/psdev/devdrawer/ui/theme/Shape.kt
new file mode 100644
index 00000000..cfb1dfdd
--- /dev/null
+++ b/app/src/main/java/de/psdev/devdrawer/ui/theme/Shape.kt
@@ -0,0 +1,11 @@
+package de.psdev.devdrawer.ui.theme
+
+import androidx.compose.foundation.shape.RoundedCornerShape
+import androidx.compose.material.Shapes
+import androidx.compose.ui.unit.dp
+
+val Shapes = Shapes(
+ small = RoundedCornerShape(4.dp),
+ medium = RoundedCornerShape(4.dp),
+ large = RoundedCornerShape(0.dp)
+)
\ No newline at end of file
diff --git a/app/src/main/java/de/psdev/devdrawer/ui/theme/Theme.kt b/app/src/main/java/de/psdev/devdrawer/ui/theme/Theme.kt
new file mode 100644
index 00000000..58d1f699
--- /dev/null
+++ b/app/src/main/java/de/psdev/devdrawer/ui/theme/Theme.kt
@@ -0,0 +1,44 @@
+package de.psdev.devdrawer.ui.theme
+
+import androidx.compose.foundation.isSystemInDarkTheme
+import androidx.compose.material.MaterialTheme
+import androidx.compose.material.darkColors
+import androidx.compose.material.lightColors
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.graphics.Color
+
+private val DarkColorPalette = darkColors(
+ primary = LightGreen500,
+ primaryVariant = LightGreen200,
+ secondary = Orange700,
+ onPrimary = Color.White,
+ onSecondary = Color.Black,
+ onBackground = Color.White,
+ onSurface = Color.White,
+ onError = Color.Black
+)
+
+private val LightColorPalette = lightColors(
+ primary = LightGreen500,
+ primaryVariant = LightGreen200,
+ secondary = Orange700
+)
+
+@Composable
+fun DevDrawerTheme(
+ darkTheme: Boolean = isSystemInDarkTheme(),
+ content: @Composable () -> Unit
+) {
+ val colors = if (darkTheme) {
+ DarkColorPalette
+ } else {
+ LightColorPalette
+ }
+
+ MaterialTheme(
+ colors = colors,
+// typography = Typography,
+// shapes = Shapes,
+ content = content
+ )
+}
\ No newline at end of file
diff --git a/app/src/main/java/de/psdev/devdrawer/ui/theme/Type.kt b/app/src/main/java/de/psdev/devdrawer/ui/theme/Type.kt
new file mode 100644
index 00000000..257bab10
--- /dev/null
+++ b/app/src/main/java/de/psdev/devdrawer/ui/theme/Type.kt
@@ -0,0 +1,28 @@
+package de.psdev.devdrawer.ui.theme
+
+import androidx.compose.material.Typography
+import androidx.compose.ui.text.TextStyle
+import androidx.compose.ui.text.font.FontFamily
+import androidx.compose.ui.text.font.FontWeight
+import androidx.compose.ui.unit.sp
+
+// Set of Material typography styles to start with
+val Typography = Typography(
+ body1 = TextStyle(
+ fontFamily = FontFamily.Default,
+ fontWeight = FontWeight.Normal,
+ fontSize = 16.sp
+ )
+ /* Other default text styles to override
+ button = TextStyle(
+ fontFamily = FontFamily.Default,
+ fontWeight = FontWeight.W500,
+ fontSize = 14.sp
+ ),
+ caption = TextStyle(
+ fontFamily = FontFamily.Default,
+ fontWeight = FontWeight.Normal,
+ fontSize = 12.sp
+ )
+ */
+)
\ No newline at end of file
diff --git a/app/src/main/java/de/psdev/devdrawer/utils/Constants.kt b/app/src/main/java/de/psdev/devdrawer/utils/Constants.kt
index 592097a5..db8ec998 100644
--- a/app/src/main/java/de/psdev/devdrawer/utils/Constants.kt
+++ b/app/src/main/java/de/psdev/devdrawer/utils/Constants.kt
@@ -1,8 +1,6 @@
package de.psdev.devdrawer.utils
object Constants {
- const val ACTION_WIDGET_PINNED = "de.psdev.devdrawer.WIDGET_PINNED"
-
const val LAUNCH_APP = 1
const val LAUNCH_APP_DETAILS = 2
const val LAUNCH_UNINSTALL = 3
diff --git a/app/src/main/java/de/psdev/devdrawer/utils/Flow.kt b/app/src/main/java/de/psdev/devdrawer/utils/Flow.kt
new file mode 100644
index 00000000..5d38cd1e
--- /dev/null
+++ b/app/src/main/java/de/psdev/devdrawer/utils/Flow.kt
@@ -0,0 +1,20 @@
+package de.psdev.devdrawer.utils
+
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.remember
+import androidx.compose.ui.platform.LocalLifecycleOwner
+import androidx.lifecycle.Lifecycle
+import androidx.lifecycle.flowWithLifecycle
+import kotlinx.coroutines.flow.Flow
+
+@Composable
+fun rememberFlowWithLifecycle(
+ flow: Flow,
+ lifecycle: Lifecycle = LocalLifecycleOwner.current.lifecycle,
+ minActiveState: Lifecycle.State = Lifecycle.State.STARTED
+): Flow = remember(flow, lifecycle) {
+ flow.flowWithLifecycle(
+ lifecycle = lifecycle,
+ minActiveState = minActiveState
+ )
+}
\ No newline at end of file
diff --git a/app/src/main/java/de/psdev/devdrawer/widgets/CleanupWidgetsWorker.kt b/app/src/main/java/de/psdev/devdrawer/widgets/CleanupWidgetsWorker.kt
index 7e6f5996..aabbf696 100644
--- a/app/src/main/java/de/psdev/devdrawer/widgets/CleanupWidgetsWorker.kt
+++ b/app/src/main/java/de/psdev/devdrawer/widgets/CleanupWidgetsWorker.kt
@@ -28,9 +28,17 @@ class CleanupWidgetsWorker @AssistedInject constructor(
fun enableWorker(application: Application) {
val workManager = WorkManager.getInstance(application)
- val request =
+
+ workManager.enqueueUniqueWork(
+ TAG,
+ ExistingWorkPolicy.APPEND_OR_REPLACE,
+ OneTimeWorkRequestBuilder().build()
+ )
+ workManager.enqueueUniquePeriodicWork(
+ TAG,
+ ExistingPeriodicWorkPolicy.REPLACE,
PeriodicWorkRequestBuilder(30, TimeUnit.MINUTES).build()
- workManager.enqueueUniquePeriodicWork(TAG, ExistingPeriodicWorkPolicy.REPLACE, request)
+ )
}
}
diff --git a/app/src/main/java/de/psdev/devdrawer/widgets/EditWidgetFragment.kt b/app/src/main/java/de/psdev/devdrawer/widgets/EditWidgetFragment.kt
deleted file mode 100644
index 74561804..00000000
--- a/app/src/main/java/de/psdev/devdrawer/widgets/EditWidgetFragment.kt
+++ /dev/null
@@ -1,201 +0,0 @@
-package de.psdev.devdrawer.widgets
-
-import android.app.Activity
-import android.appwidget.AppWidgetManager
-import android.content.Intent
-import android.graphics.Color
-import android.os.Bundle
-import android.view.LayoutInflater
-import android.view.View
-import android.view.ViewGroup
-import androidx.core.view.isVisible
-import androidx.fragment.app.viewModels
-import androidx.lifecycle.lifecycleScope
-import androidx.navigation.fragment.findNavController
-import androidx.navigation.fragment.navArgs
-import androidx.recyclerview.selection.SelectionPredicates
-import androidx.recyclerview.selection.SelectionTracker
-import androidx.recyclerview.selection.StorageStrategy
-import androidx.recyclerview.widget.LinearLayoutManager
-import com.github.dhaval2404.colorpicker.MaterialColorPickerDialog
-import com.github.dhaval2404.colorpicker.model.ColorShape
-import dagger.hilt.android.AndroidEntryPoint
-import de.psdev.devdrawer.BaseFragment
-import de.psdev.devdrawer.R
-import de.psdev.devdrawer.database.DevDrawerDatabase
-import de.psdev.devdrawer.database.WidgetProfile
-import de.psdev.devdrawer.databinding.FragmentWidgetEditBinding
-import de.psdev.devdrawer.profiles.WidgetProfilesDetailsLookup
-import de.psdev.devdrawer.profiles.WidgetProfilesItemKeyProvider
-import de.psdev.devdrawer.profiles.WidgetProfilesListAdapter
-import de.psdev.devdrawer.receivers.UpdateReceiver
-import de.psdev.devdrawer.utils.awaitSubmit
-import de.psdev.devdrawer.utils.receiveClicksFrom
-import de.psdev.devdrawer.utils.receiveTextChangesFrom
-import de.psdev.devdrawer.utils.sortColorList
-import de.psdev.devdrawer.widgets.EditWidgetFragmentViewModel.Selection
-import kotlinx.coroutines.flow.filterNotNull
-import kotlinx.coroutines.flow.first
-import kotlinx.coroutines.flow.launchIn
-import kotlinx.coroutines.flow.onEach
-import kotlinx.coroutines.launch
-import javax.inject.Inject
-
-@AndroidEntryPoint
-class EditWidgetFragment : BaseFragment() {
-
- // Dependencies
- @Inject
- lateinit var devDrawerDatabase: DevDrawerDatabase
-
- @Inject
- lateinit var viewModelViewModelFactory: EditWidgetFragmentViewModel.ViewModelFactory
-
- val args by navArgs()
-
- val viewModel: EditWidgetFragmentViewModel by viewModels {
- EditWidgetFragmentViewModel.factory(viewModelViewModelFactory, args.widgetId)
- }
-
- var _selectionTracker: SelectionTracker? = null
-
- override fun createViewBinding(
- inflater: LayoutInflater,
- container: ViewGroup?,
- savedInstanceState: Bundle?
- ): FragmentWidgetEditBinding =
- FragmentWidgetEditBinding.inflate(inflater, container, false)
-
- override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
- super.onViewCreated(view, savedInstanceState)
- val adapter = WidgetProfilesListAdapter()
- adapter.itemLongClickListener = { widgetProfile ->
- findNavController().navigate(EditWidgetFragmentDirections.createProfileAction(widgetProfile.id))
- }
- // Setup views
- with(binding) {
- with(editName) {
- setText("Widget ${args.widgetId}")
- }
- with(btnColor) {
- setOnClickListener {
- val currentColor = viewModel.savedWidget.value?.color ?: Color.BLACK
- MaterialColorPickerDialog
- .Builder(requireContext())
- .setTitle(R.string.pick_widget_color)
- .setDefaultColor(currentColor)
- .setColorShape(ColorShape.SQAURE)
- .setColorRes(resources.getIntArray(R.array.widget_colors).sortColorList())
- .setPositiveButton(R.string.ok)
- .setNegativeButton(R.string.cancel)
- .setColorListener { color, _ ->
- setBackgroundColor(color)
- viewModel.inputColor.value = color
- }
- .showBottomSheet(childFragmentManager)
- }
- }
- }
- lifecycleScope.launchWhenResumed {
- with(binding) {
- val widget = checkNotNull(viewModel.savedWidget.filterNotNull().first())
- editName.setText(widget.name)
- btnColor.setBackgroundColor(widget.color)
- }
- }
-
- binding.btnNewProfile.setOnClickListener {
- lifecycleScope.launchWhenResumed {
- val widgetProfile = WidgetProfile(name = "Profile for ${viewModel.inputWidgetName.value}")
- devDrawerDatabase.widgetProfileDao().insert(widgetProfile)
- findNavController().navigate(EditWidgetFragmentDirections.createProfileAction(widgetProfile.id))
- }
- }
-
- binding.recyclerProfiles.layoutManager = LinearLayoutManager(view.context, LinearLayoutManager.VERTICAL, false)
- binding.recyclerProfiles.adapter = adapter
- val selectionTracker = SelectionTracker.Builder(
- "widgetProfile",
- binding.recyclerProfiles,
- WidgetProfilesItemKeyProvider(adapter),
- WidgetProfilesDetailsLookup(binding.recyclerProfiles),
- StorageStrategy.createStringStorage()
- ).withSelectionPredicate(
- SelectionPredicates.createSelectSingleAnything()
- ).build().also {
- it.onRestoreInstanceState(savedInstanceState)
- if (savedInstanceState == null) {
- lifecycleScope.launchWhenResumed {
- it.select(devDrawerDatabase.widgetDao().findById(args.widgetId)?.profileId.orEmpty())
- }
- }
- _selectionTracker = it
- }
- adapter.selectionTracker = selectionTracker
-
- viewLifecycleScope.launch {
- viewModel.inputWidgetName.receiveTextChangesFrom(binding.editName).launchIn(this)
-
- selectionTracker.addObserver(object : SelectionTracker.SelectionObserver() {
- override fun onSelectionChanged() {
- super.onSelectionChanged()
- val widgetProfile = selectionTracker.selection.asSequence()
- .map { selectedKey -> adapter.currentList.firstOrNull { it.id == selectedKey } }.firstOrNull()
- if (widgetProfile != null) {
- viewModel.inputSelectedProfile.value = Selection.Profile(widgetProfile)
- } else {
- viewModel.inputSelectedProfile.value = Selection.Nothing
- }
- }
- })
- viewModel.inputSaveTrigger.receiveClicksFrom(binding.btnConfirm).launchIn(this)
- viewModel.outputWidgetProfiles.onEach {
- adapter.awaitSubmit(it)
- binding.txtNoProfiles.isVisible = it.isEmpty()
- }.launchIn(this)
- viewModel.outputFormCompleted.onEach { completed ->
- if (completed) {
- with(binding.btnConfirm) {
- isEnabled = true
- setText(R.string.save)
- }
- } else {
- with(binding.btnConfirm) {
- isEnabled = false
- text = "Select profile"
- }
- }
- }.launchIn(this)
- viewModel.outputCloseTrigger.onEach { widget ->
- val resultValue = Intent().apply {
- putExtra(AppWidgetManager.EXTRA_APPWIDGET_ID, widget.id)
- }
- requireActivity().setResult(Activity.RESULT_OK, resultValue)
- // Will either close the fragment or finish the activity when it's the last activity
- if (!findNavController().popBackStack()) {
- requireActivity().finish()
- }
- UpdateReceiver.send(requireContext())
- }.launchIn(this)
- }
- }
-
- override fun onResume() {
- super.onResume()
- updateToolbarTitle(R.string.edit_widget)
- }
-
- override fun onSaveInstanceState(outState: Bundle) {
- super.onSaveInstanceState(outState)
- _selectionTracker?.onSaveInstanceState(outState)
- }
-
- override fun onDestroyView() {
- binding.recyclerProfiles.adapter = null
- super.onDestroyView()
- }
-
- // TODO Default name: Widget
- // TODO After losing focus of text input update name in viewState
-
-}
diff --git a/app/src/main/java/de/psdev/devdrawer/widgets/SaveWidgetWorker.kt b/app/src/main/java/de/psdev/devdrawer/widgets/SaveWidgetWorker.kt
new file mode 100644
index 00000000..5f8ecec4
--- /dev/null
+++ b/app/src/main/java/de/psdev/devdrawer/widgets/SaveWidgetWorker.kt
@@ -0,0 +1,49 @@
+package de.psdev.devdrawer.widgets
+
+import android.content.Context
+import android.graphics.Color
+import androidx.hilt.work.HiltWorker
+import androidx.work.CoroutineWorker
+import androidx.work.WorkerParameters
+import dagger.assisted.Assisted
+import dagger.assisted.AssistedInject
+import de.psdev.devdrawer.database.DevDrawerDatabase
+import de.psdev.devdrawer.database.Widget
+import de.psdev.devdrawer.database.WidgetProfile
+import de.psdev.devdrawer.receivers.UpdateReceiver
+import mu.KLogging
+
+@HiltWorker
+class SaveWidgetWorker @AssistedInject constructor(
+ @Assisted appContext: Context,
+ @Assisted params: WorkerParameters,
+ private val database: DevDrawerDatabase
+) : CoroutineWorker(appContext, params) {
+
+ companion object : KLogging() {
+ const val ARG_WIDGET_ID = "widgetId"
+ const val INVALID_WIDGET_ID = -1
+ }
+
+ override suspend fun doWork(): Result {
+ val widgetId = inputData.getInt(ARG_WIDGET_ID, INVALID_WIDGET_ID)
+ check(widgetId != INVALID_WIDGET_ID) { "Invalid widget ID" }
+ val widgetDao = database.widgetDao()
+ val widgetProfileDao = database.widgetProfileDao()
+ val defaultWidgetProfile = widgetProfileDao.findAll().firstOrNull()
+ ?: WidgetProfile(name = "Default").also {
+ widgetProfileDao.insert(it)
+ }
+
+ // Create entries in database
+ val widget = Widget(
+ id = widgetId,
+ name = "Widget $widgetId",
+ color = Color.BLACK,
+ profileId = defaultWidgetProfile.id
+ )
+ widgetDao.insert(widget)
+ UpdateReceiver.send(applicationContext)
+ return Result.success()
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/de/psdev/devdrawer/widgets/WidgetCard.kt b/app/src/main/java/de/psdev/devdrawer/widgets/WidgetCard.kt
new file mode 100644
index 00000000..318c214d
--- /dev/null
+++ b/app/src/main/java/de/psdev/devdrawer/widgets/WidgetCard.kt
@@ -0,0 +1,62 @@
+package de.psdev.devdrawer.widgets
+
+import android.content.res.Configuration
+import android.graphics.Color
+import androidx.compose.foundation.clickable
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.fillMaxWidth
+import androidx.compose.foundation.layout.padding
+import androidx.compose.foundation.layout.wrapContentHeight
+import androidx.compose.material.*
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.CompositionLocalProvider
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.res.stringResource
+import androidx.compose.ui.tooling.preview.Preview
+import androidx.compose.ui.unit.dp
+import de.psdev.devdrawer.R
+import de.psdev.devdrawer.database.Widget
+import de.psdev.devdrawer.ui.theme.DevDrawerTheme
+
+@Composable
+fun WidgetCard(
+ widget: Widget,
+ onWidgetClick: (Widget) -> Unit = {}
+) {
+ Card(
+ modifier = Modifier
+ .fillMaxWidth()
+ .wrapContentHeight()
+ .padding(8.dp)
+ .clickable { onWidgetClick(widget) }
+ ) {
+ Column(
+ modifier = Modifier
+ .fillMaxWidth()
+ .wrapContentHeight()
+ .padding(16.dp)
+ ) {
+ Text(
+ modifier = Modifier.fillMaxWidth(),
+ style = MaterialTheme.typography.body1,
+ text = widget.name
+ )
+ CompositionLocalProvider(LocalContentAlpha provides ContentAlpha.medium) {
+ Text(
+ modifier = Modifier.fillMaxWidth(),
+ style = MaterialTheme.typography.body2,
+ text = stringResource(id = R.string.widget_id_template, widget.id)
+ )
+ }
+ }
+ }
+}
+
+@Preview
+@Preview(uiMode = Configuration.UI_MODE_NIGHT_YES)
+@Composable
+fun Preview_WidgetCard() {
+ DevDrawerTheme {
+ WidgetCard(widget = Widget(1, "Test Widget", Color.BLACK, ""))
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/de/psdev/devdrawer/widgets/WidgetConfigActivity.kt b/app/src/main/java/de/psdev/devdrawer/widgets/WidgetConfigActivity.kt
index 278e2157..75a4c5fa 100644
--- a/app/src/main/java/de/psdev/devdrawer/widgets/WidgetConfigActivity.kt
+++ b/app/src/main/java/de/psdev/devdrawer/widgets/WidgetConfigActivity.kt
@@ -57,7 +57,10 @@ class WidgetConfigActivity : BaseActivity() {
setSupportActionBar(binding.toolbar)
val navController = findNavController(R.id.nav_host_fragment)
- navController.setGraph(R.navigation.nav_config_widget, EditWidgetFragmentArgs(widgetId).toBundle())
+ navController.setGraph(
+ R.navigation.nav_config_widget,
+ WidgetEditFragmentArgs.Builder(widgetId).build().toBundle()
+ )
val appBarConfiguration = AppBarConfiguration(navController.graph)
binding.toolbar.setupWithNavController(navController, appBarConfiguration)
}
diff --git a/app/src/main/java/de/psdev/devdrawer/widgets/WidgetEditFragment.kt b/app/src/main/java/de/psdev/devdrawer/widgets/WidgetEditFragment.kt
new file mode 100644
index 00000000..1b9a3b72
--- /dev/null
+++ b/app/src/main/java/de/psdev/devdrawer/widgets/WidgetEditFragment.kt
@@ -0,0 +1,166 @@
+package de.psdev.devdrawer.widgets
+
+import android.os.Bundle
+import android.view.LayoutInflater
+import android.view.View
+import android.view.ViewGroup
+import androidx.compose.ui.platform.ComposeView
+import dagger.hilt.android.AndroidEntryPoint
+import de.psdev.devdrawer.BaseFragment
+import de.psdev.devdrawer.R
+import de.psdev.devdrawer.database.DevDrawerDatabase
+import de.psdev.devdrawer.ui.theme.DevDrawerTheme
+import javax.inject.Inject
+
+@AndroidEntryPoint
+class WidgetEditFragment: BaseFragment() {
+
+ // Dependencies
+ @Inject
+ lateinit var devDrawerDatabase: DevDrawerDatabase
+
+ @Inject
+ lateinit var viewModelViewModelFactory: EditWidgetFragmentViewModel.ViewModelFactory
+
+ override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View =
+ ComposeView(requireContext()).apply {
+ setContent {
+ DevDrawerTheme {
+ WidgetEditor()
+ }
+ }
+ }
+
+// override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
+// super.onViewCreated(view, savedInstanceState)
+// val adapter = WidgetProfilesListAdapter()
+// adapter.itemLongClickListener = { widgetProfile ->
+// findNavController().navigate(EditWidgetFragmentDirections.createProfileAction(widgetProfile.id))
+// }
+// // Setup views
+// with(binding) {
+// with(editName) {
+// setText("Widget ${args.widgetId}")
+// }
+// with(btnColor) {
+// setOnClickListener {
+// val currentColor = viewModel.savedWidget.value?.color ?: Color.BLACK
+// MaterialColorPickerDialog
+// .Builder(requireContext())
+// .setTitle(R.string.pick_widget_color)
+// .setDefaultColor(currentColor)
+// .setColorShape(ColorShape.SQAURE)
+// .setColorRes(resources.getIntArray(R.array.widget_colors).sortColorList())
+// .setPositiveButton(R.string.ok)
+// .setNegativeButton(R.string.cancel)
+// .setColorListener { color, _ ->
+// setBackgroundColor(color)
+// viewModel.inputColor.value = color
+// }
+// .showBottomSheet(childFragmentManager)
+// }
+// }
+// }
+// lifecycleScope.launchWhenResumed {
+// with(binding) {
+// val widget = checkNotNull(viewModel.savedWidget.filterNotNull().first())
+// editName.setText(widget.name)
+// btnColor.setBackgroundColor(widget.color)
+// }
+// }
+//
+// binding.btnNewProfile.setOnClickListener {
+// lifecycleScope.launchWhenResumed {
+// val widgetProfile = WidgetProfile(name = "Profile for ${viewModel.inputWidgetName.value}")
+// devDrawerDatabase.widgetProfileDao().insert(widgetProfile)
+// findNavController().navigate(EditWidgetFragmentDirections.createProfileAction(widgetProfile.id))
+// }
+// }
+//
+// binding.recyclerProfiles.layoutManager = LinearLayoutManager(view.context, LinearLayoutManager.VERTICAL, false)
+// binding.recyclerProfiles.adapter = adapter
+// val selectionTracker = SelectionTracker.Builder(
+// "widgetProfile",
+// binding.recyclerProfiles,
+// WidgetProfilesItemKeyProvider(adapter),
+// WidgetProfilesDetailsLookup(binding.recyclerProfiles),
+// StorageStrategy.createStringStorage()
+// ).withSelectionPredicate(
+// SelectionPredicates.createSelectSingleAnything()
+// ).build().also {
+// it.onRestoreInstanceState(savedInstanceState)
+// if (savedInstanceState == null) {
+// lifecycleScope.launchWhenResumed {
+// it.select(devDrawerDatabase.widgetDao().findById(args.widgetId)?.profileId.orEmpty())
+// }
+// }
+// _selectionTracker = it
+// }
+// adapter.selectionTracker = selectionTracker
+//
+// viewLifecycleScope.launch {
+// viewModel.inputWidgetName.receiveTextChangesFrom(binding.editName).launchIn(this)
+//
+// selectionTracker.addObserver(object : SelectionTracker.SelectionObserver() {
+// override fun onSelectionChanged() {
+// super.onSelectionChanged()
+// val widgetProfile = selectionTracker.selection.asSequence()
+// .map { selectedKey -> adapter.currentList.firstOrNull { it.id == selectedKey } }.firstOrNull()
+// if (widgetProfile != null) {
+// viewModel.inputSelectedProfile.value = Selection.Profile(widgetProfile)
+// } else {
+// viewModel.inputSelectedProfile.value = Selection.Nothing
+// }
+// }
+// })
+// viewModel.inputSaveTrigger.receiveClicksFrom(binding.btnConfirm).launchIn(this)
+// viewModel.outputWidgetProfiles.onEach {
+// adapter.awaitSubmit(it)
+// binding.txtNoProfiles.isVisible = it.isEmpty()
+// }.launchIn(this)
+// viewModel.outputFormCompleted.onEach { completed ->
+// if (completed) {
+// with(binding.btnConfirm) {
+// isEnabled = true
+// setText(R.string.save)
+// }
+// } else {
+// with(binding.btnConfirm) {
+// isEnabled = false
+// text = "Select profile"
+// }
+// }
+// }.launchIn(this)
+// viewModel.outputCloseTrigger.onEach { widget ->
+// val resultValue = Intent().apply {
+// putExtra(AppWidgetManager.EXTRA_APPWIDGET_ID, widget.id)
+// }
+// requireActivity().setResult(Activity.RESULT_OK, resultValue)
+// // Will either close the fragment or finish the activity when it's the last activity
+// if (!findNavController().popBackStack()) {
+// requireActivity().finish()
+// }
+// UpdateReceiver.send(requireContext())
+// }.launchIn(this)
+// }
+// }
+
+ override fun onResume() {
+ super.onResume()
+ updateToolbarTitle(R.string.edit_widget)
+ }
+
+// override fun onSaveInstanceState(outState: Bundle) {
+// super.onSaveInstanceState(outState)
+// _selectionTracker?.onSaveInstanceState(outState)
+// }
+//
+// override fun onDestroyView() {
+// binding.recyclerProfiles.adapter = null
+// super.onDestroyView()
+// }
+
+ // TODO Default name: Widget
+ // TODO After losing focus of text input update name in viewState
+
+}
diff --git a/app/src/main/java/de/psdev/devdrawer/widgets/WidgetEditor.kt b/app/src/main/java/de/psdev/devdrawer/widgets/WidgetEditor.kt
new file mode 100644
index 00000000..07a8007d
--- /dev/null
+++ b/app/src/main/java/de/psdev/devdrawer/widgets/WidgetEditor.kt
@@ -0,0 +1,205 @@
+package de.psdev.devdrawer.widgets
+
+import android.content.res.Configuration
+import androidx.compose.animation.AnimatedVisibility
+import androidx.compose.animation.ExperimentalAnimationApi
+import androidx.compose.animation.fadeIn
+import androidx.compose.animation.fadeOut
+import androidx.compose.foundation.background
+import androidx.compose.foundation.clickable
+import androidx.compose.foundation.layout.*
+import androidx.compose.foundation.lazy.LazyColumn
+import androidx.compose.foundation.lazy.items
+import androidx.compose.material.*
+import androidx.compose.material.icons.Icons
+import androidx.compose.material.icons.outlined.Save
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.collectAsState
+import androidx.compose.runtime.getValue
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.res.stringResource
+import androidx.compose.ui.tooling.preview.Preview
+import androidx.compose.ui.unit.dp
+import androidx.hilt.navigation.compose.hiltViewModel
+import de.psdev.devdrawer.R
+import de.psdev.devdrawer.database.Widget
+import de.psdev.devdrawer.database.WidgetProfile
+import de.psdev.devdrawer.profiles.WidgetEditorViewModel
+import de.psdev.devdrawer.profiles.WidgetEditorViewState
+import de.psdev.devdrawer.ui.theme.DevDrawerTheme
+import de.psdev.devdrawer.utils.rememberFlowWithLifecycle
+import java.util.*
+
+@Composable
+fun WidgetEditor(
+
+) {
+ WidgetEditor(
+ viewModel = hiltViewModel()
+ )
+}
+
+@Composable
+fun WidgetEditor(
+ viewModel: WidgetEditorViewModel
+) {
+ val viewState by rememberFlowWithLifecycle(viewModel.state)
+ .collectAsState(initial = WidgetEditorViewState.Empty)
+
+ WidgetEditor(
+ viewState = viewState,
+ onNameChange = viewModel::onNameChanged,
+ onWidgetProfileSelected = viewModel::onWidgetProfileSelected,
+ onSaveChangesClick = {
+ viewModel.saveChanges()
+ }
+ )
+}
+
+@OptIn(ExperimentalAnimationApi::class)
+@Composable
+fun WidgetEditor(
+ viewState: WidgetEditorViewState,
+ onNameChange: (String) -> Unit = {},
+ onWidgetProfileSelected: (WidgetProfile) -> Unit = {},
+ onSelectWidgetColorClick: () -> Unit = {},
+ onSaveChangesClick: () -> Unit = {}
+) {
+ val widget = viewState.editableWidget
+ if (widget == null) {
+ // Loading
+ Box(modifier = Modifier.fillMaxSize(), contentAlignment = Alignment.Center) {
+ CircularProgressIndicator(modifier = Modifier.size(64.dp))
+ }
+ } else {
+ Box(modifier = Modifier.fillMaxSize()) {
+ Column {
+ Surface(modifier = Modifier.wrapContentHeight(), elevation = 2.dp) {
+ Column(
+ modifier = Modifier
+ .wrapContentHeight()
+ .padding(8.dp)
+ ) {
+ Row(
+ horizontalArrangement = Arrangement.Center,
+ verticalAlignment = Alignment.CenterVertically
+ ) {
+ OutlinedTextField(
+ modifier = Modifier.weight(1f),
+ singleLine = true,
+ value = widget.name,
+ onValueChange = onNameChange,
+ label = { Text(text = stringResource(id = R.string.name)) }
+ )
+ Box(modifier = Modifier
+ .size(48.dp)
+ .background(Color(widget.color))
+ .padding(8.dp)
+ .clickable { onSelectWidgetColorClick() }
+ )
+ }
+ }
+ }
+ LazyColumn(
+ modifier = Modifier.weight(1f),
+ contentPadding = PaddingValues(8.dp),
+ verticalArrangement = Arrangement.spacedBy(8.dp)
+ ) {
+ items(viewState.widgetProfiles) { widgetProfile ->
+ val backgroundColor = if (widget.profileId == widgetProfile.id) {
+ MaterialTheme.colors.secondary
+ } else MaterialTheme.colors.surface
+ Card(backgroundColor = backgroundColor, modifier = Modifier.clickable { onWidgetProfileSelected(widgetProfile) }) {
+ Row(
+ modifier = Modifier.padding(vertical = 16.dp, horizontal = 8.dp),
+ verticalAlignment = Alignment.CenterVertically
+ ) {
+ Text(modifier = Modifier.weight(1f), text = widgetProfile.name)
+ }
+ }
+ }
+ }
+ }
+ AnimatedVisibility(
+ visible = viewState.persistedWidget != viewState.editableWidget,
+ modifier = Modifier.align(Alignment.BottomEnd),
+ enter = fadeIn(),
+ exit = fadeOut()
+ ) {
+ FloatingActionButton(
+ onClick = onSaveChangesClick,
+ modifier = Modifier.padding(end = 16.dp, bottom = 16.dp)
+ ) {
+ Icon(imageVector = Icons.Outlined.Save, contentDescription = stringResource(id = R.string.save))
+ }
+ }
+ }
+ }
+}
+
+@Preview(showSystemUi = true)
+@Preview(showSystemUi = true, uiMode = Configuration.UI_MODE_NIGHT_YES)
+@Composable
+fun Preview_WidgetEditor_Loading() {
+ DevDrawerTheme {
+ WidgetEditor(
+ viewState = WidgetEditorViewState.Empty
+ )
+ }
+}
+
+@Preview(showSystemUi = true)
+@Preview(showSystemUi = true, uiMode = Configuration.UI_MODE_NIGHT_YES)
+@Composable
+fun Preview_WidgetEditor_Loaded() {
+ val widgetProfile = WidgetProfile(
+ id = UUID.randomUUID().toString(),
+ name = "Test widget profile"
+ )
+ val widget = Widget(
+ id = 1,
+ name = "Test widget",
+ color = android.graphics.Color.YELLOW,
+ profileId = widgetProfile.id
+ )
+ DevDrawerTheme {
+ WidgetEditor(
+ viewState = WidgetEditorViewState(
+ persistedWidget = widget,
+ widgetProfiles = listOf(widgetProfile),
+ editableWidget = widget
+ )
+ )
+ }
+}
+
+@Preview(showSystemUi = true)
+@Preview(showSystemUi = true, uiMode = Configuration.UI_MODE_NIGHT_YES)
+@Composable
+fun Preview_WidgetEditor_Loaded_Changed() {
+ val widgetProfile = WidgetProfile(
+ id = UUID.randomUUID().toString(),
+ name = "Test widget profile"
+ )
+ val widgetProfile2 = WidgetProfile(
+ id = UUID.randomUUID().toString(),
+ name = "Test widget profile 2"
+ )
+ val widget = Widget(
+ id = 1,
+ name = "Test widget",
+ color = android.graphics.Color.YELLOW,
+ profileId = widgetProfile.id
+ )
+ DevDrawerTheme {
+ WidgetEditor(
+ viewState = WidgetEditorViewState(
+ persistedWidget = widget,
+ widgetProfiles = listOf(widgetProfile, widgetProfile2),
+ editableWidget = widget.copy(profileId = widgetProfile2.id)
+ )
+ )
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/de/psdev/devdrawer/widgets/WidgetList.kt b/app/src/main/java/de/psdev/devdrawer/widgets/WidgetList.kt
new file mode 100644
index 00000000..fce71478
--- /dev/null
+++ b/app/src/main/java/de/psdev/devdrawer/widgets/WidgetList.kt
@@ -0,0 +1,42 @@
+package de.psdev.devdrawer.widgets
+
+import android.content.res.Configuration
+import androidx.compose.foundation.layout.PaddingValues
+import androidx.compose.foundation.layout.fillMaxHeight
+import androidx.compose.foundation.layout.fillMaxWidth
+import androidx.compose.foundation.lazy.LazyColumn
+import androidx.compose.foundation.lazy.items
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.tooling.preview.Preview
+import androidx.compose.ui.unit.dp
+import de.psdev.devdrawer.database.Widget
+import de.psdev.devdrawer.ui.theme.DevDrawerTheme
+
+@Composable
+fun WidgetList(
+ widgets: List,
+ onWidgetClick: (Widget) -> Unit = {},
+ modifier: Modifier = Modifier,
+ contentPadding: PaddingValues = PaddingValues(0.dp)
+) {
+ LazyColumn(
+ modifier = modifier
+ .fillMaxWidth()
+ .fillMaxHeight(),
+ contentPadding = contentPadding,
+ ) {
+ items(widgets, key = { it.id }) { widget ->
+ WidgetCard(widget = widget, onWidgetClick = onWidgetClick)
+ }
+ }
+}
+
+@Preview(showSystemUi = true)
+@Preview(showSystemUi = true, uiMode = Configuration.UI_MODE_NIGHT_YES)
+@Composable
+fun Preview_WidgetList() {
+ DevDrawerTheme {
+ WidgetList(widgets = testWidgets())
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/de/psdev/devdrawer/widgets/WidgetListFragment.kt b/app/src/main/java/de/psdev/devdrawer/widgets/WidgetListFragment.kt
index e4aafdab..23068904 100644
--- a/app/src/main/java/de/psdev/devdrawer/widgets/WidgetListFragment.kt
+++ b/app/src/main/java/de/psdev/devdrawer/widgets/WidgetListFragment.kt
@@ -3,69 +3,50 @@ package de.psdev.devdrawer.widgets
import android.app.PendingIntent
import android.appwidget.AppWidgetManager
import android.content.ComponentName
-import android.content.Intent
import android.os.Build
import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.annotation.RequiresApi
+import androidx.compose.ui.platform.ComposeView
import androidx.core.content.getSystemService
import androidx.core.os.bundleOf
-import androidx.core.view.isVisible
-import androidx.lifecycle.lifecycleScope
-import androidx.navigation.fragment.findNavController
+import androidx.navigation.findNavController
import dagger.hilt.android.AndroidEntryPoint
import de.psdev.devdrawer.BaseFragment
import de.psdev.devdrawer.R
import de.psdev.devdrawer.appwidget.DDWidgetProvider
import de.psdev.devdrawer.database.DevDrawerDatabase
-import de.psdev.devdrawer.database.Widget
-import de.psdev.devdrawer.databinding.FragmentWidgetListBinding
-import de.psdev.devdrawer.utils.Constants
-import de.psdev.devdrawer.utils.awaitSubmit
-import de.psdev.devdrawer.utils.supportsVersion
-import kotlinx.coroutines.flow.launchIn
-import kotlinx.coroutines.flow.onEach
+import de.psdev.devdrawer.receivers.PinWidgetSuccessReceiver
+import de.psdev.devdrawer.ui.theme.DevDrawerTheme
import mu.KLogging
import javax.inject.Inject
@AndroidEntryPoint
-class WidgetListFragment : BaseFragment() {
+class WidgetListFragment : BaseFragment() {
companion object : KLogging()
@Inject
lateinit var devDrawerDatabase: DevDrawerDatabase
- override fun createViewBinding(
+ override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
- ): FragmentWidgetListBinding = FragmentWidgetListBinding.inflate(inflater, container, false)
-
- override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
- super.onViewCreated(view, savedInstanceState)
- val clickListener: (Widget) -> Unit = { widget ->
- findNavController().navigate(WidgetListFragmentDirections.editWidget(widget.id))
- }
- val listAdapter = WidgetsListAdapter(clickListener)
- with(binding) {
- recyclerWidgets.adapter = listAdapter
- }
- devDrawerDatabase.widgetDao().findAllFlow().onEach {
- listAdapter.awaitSubmit(it)
- binding.containerNoWidgets.isVisible = it.isEmpty()
- supportsVersion(Build.VERSION_CODES.O) {
- with(binding.btnAddWidget) {
- isVisible = true
- setOnClickListener {
- requestAppWidgetPinning()
- }
- }
+ ): View = ComposeView(requireContext()).apply {
+ setContent {
+ DevDrawerTheme {
+ WidgetListScreenWithModel(
+ onWidgetClick = { widget ->
+ findNavController().navigate(WidgetListFragmentDirections.editWidget(widget.id))
+ },
+ onRequestPinWidgetClick = ::requestAppWidgetPinning
+ )
}
- }.launchIn(lifecycleScope)
+ }
}
override fun onResume() {
@@ -73,25 +54,18 @@ class WidgetListFragment : BaseFragment() {
updateToolbarTitle(R.string.widgets)
}
- override fun onDestroyView() {
- binding.recyclerWidgets.adapter = null
- super.onDestroyView()
- }
-
@RequiresApi(Build.VERSION_CODES.O)
private fun requestAppWidgetPinning() {
val activity = requireActivity()
val appWidgetManager: AppWidgetManager = activity.getSystemService() ?: return
val widgetProvider = ComponentName(activity, DDWidgetProvider::class.java)
if (appWidgetManager.isRequestPinAppWidgetSupported) {
- val pinnedWidgetCallbackIntent = Intent(activity, DDWidgetProvider::class.java).apply {
- action = Constants.ACTION_WIDGET_PINNED
- }
+ val intent = PinWidgetSuccessReceiver.intent(activity)
val successCallback = PendingIntent.getBroadcast(
activity,
1,
- pinnedWidgetCallbackIntent,
- PendingIntent.FLAG_UPDATE_CURRENT
+ intent,
+ PendingIntent.FLAG_ONE_SHOT
)
val bundle = bundleOf()
appWidgetManager.requestPinAppWidget(widgetProvider, bundle, successCallback)
diff --git a/app/src/main/java/de/psdev/devdrawer/widgets/WidgetListScreen.kt b/app/src/main/java/de/psdev/devdrawer/widgets/WidgetListScreen.kt
new file mode 100644
index 00000000..360c7123
--- /dev/null
+++ b/app/src/main/java/de/psdev/devdrawer/widgets/WidgetListScreen.kt
@@ -0,0 +1,117 @@
+package de.psdev.devdrawer.widgets
+
+import android.content.res.Configuration
+import android.graphics.Color
+import androidx.compose.foundation.layout.*
+import androidx.compose.material.*
+import androidx.compose.material.icons.Icons
+import androidx.compose.material.icons.outlined.Add
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.collectAsState
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.Modifier
+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.lifecycle.viewmodel.compose.viewModel
+import de.psdev.devdrawer.R
+import de.psdev.devdrawer.database.Widget
+import de.psdev.devdrawer.ui.theme.DevDrawerTheme
+import java.util.*
+
+@Composable
+fun WidgetListScreenWithModel(
+ widgetListScreenViewModel: WidgetListScreenViewModel = viewModel(),
+ onWidgetClick: (Widget) -> Unit = {},
+ onRequestPinWidgetClick: () -> Unit = {}
+) {
+ val widgets = widgetListScreenViewModel.widgets.collectAsState(initial = emptyList()).value
+ WidgetListScreen(
+ widgets = widgets,
+ onWidgetClick = onWidgetClick,
+ onRequestPinWidgetClick = onRequestPinWidgetClick
+ )
+}
+
+@Composable
+fun WidgetListScreen(
+ widgets: List,
+ onWidgetClick: (Widget) -> Unit = {},
+ onRequestPinWidgetClick: () -> Unit = {}
+) {
+ if (widgets.isEmpty()) {
+ Column(
+ horizontalAlignment = Alignment.CenterHorizontally,
+ verticalArrangement = Arrangement.Center,
+ modifier = Modifier
+ .fillMaxWidth()
+ .fillMaxHeight()
+ ) {
+ Text(
+ text = stringResource(id = R.string.no_widgets_created),
+ color = MaterialTheme.colors.onBackground
+ )
+ Spacer(modifier = Modifier.size(16.dp))
+ Button(onClick = onRequestPinWidgetClick) {
+ Icon(
+ painter = painterResource(id = R.drawable.ic_outline_add_box_24),
+ contentDescription = "Add"
+ )
+ Text(
+ modifier = Modifier.padding(start = 8.dp),
+ text = stringResource(id = R.string.add_widget)
+ )
+ }
+ }
+ } else {
+ Box(Modifier.fillMaxSize()) {
+ WidgetList(
+ widgets = widgets,
+ onWidgetClick = onWidgetClick,
+ contentPadding = PaddingValues(bottom = 80.dp)
+ )
+ FloatingActionButton(
+ onClick = onRequestPinWidgetClick,
+ modifier = Modifier
+ .align(Alignment.BottomEnd)
+ .padding(end = 16.dp, bottom = 16.dp)
+ ) {
+ Icon(imageVector = Icons.Outlined.Add, contentDescription = "Pin new widget")
+ }
+ }
+ }
+}
+
+@Preview(showSystemUi = true)
+@Preview(showSystemUi = true, uiMode = Configuration.UI_MODE_NIGHT_YES)
+@Composable
+fun Preview_WidgetListScreen() {
+ DevDrawerTheme {
+ WidgetListScreen(testWidgets())
+ }
+}
+
+@Preview(name = "Empty", showSystemUi = true)
+@Preview(name = "Empty (Dark)", showSystemUi = true, uiMode = Configuration.UI_MODE_NIGHT_YES)
+@Composable
+fun Preview_WidgetListScreen_Empty() {
+ DevDrawerTheme {
+ WidgetListScreen(emptyList())
+ }
+}
+
+fun testWidgets(): List = listOf(
+ Widget(
+ id = 1,
+ name = "Test Widget",
+ color = Color.BLACK,
+ profileId = UUID.randomUUID().toString()
+ ),
+ Widget(
+ id = 2,
+ name = "Test Widget 2",
+ color = Color.BLACK,
+ profileId = UUID.randomUUID().toString()
+ )
+)
diff --git a/app/src/main/java/de/psdev/devdrawer/widgets/WidgetListScreenViewModel.kt b/app/src/main/java/de/psdev/devdrawer/widgets/WidgetListScreenViewModel.kt
new file mode 100644
index 00000000..f76bb830
--- /dev/null
+++ b/app/src/main/java/de/psdev/devdrawer/widgets/WidgetListScreenViewModel.kt
@@ -0,0 +1,21 @@
+package de.psdev.devdrawer.widgets
+
+import androidx.lifecycle.SavedStateHandle
+import androidx.lifecycle.ViewModel
+import androidx.lifecycle.viewModelScope
+import dagger.hilt.android.lifecycle.HiltViewModel
+import de.psdev.devdrawer.database.DevDrawerDatabase
+import de.psdev.devdrawer.database.Widget
+import kotlinx.coroutines.flow.StateFlow
+import kotlinx.coroutines.flow.stateIn
+import javax.inject.Inject
+
+@HiltViewModel
+class WidgetListScreenViewModel @Inject constructor(
+ private val savedStateHandle: SavedStateHandle,
+ private val database: DevDrawerDatabase
+): ViewModel() {
+
+ val widgets = database.widgetDao().findAllFlow()
+
+}
\ No newline at end of file
diff --git a/app/src/main/java/de/psdev/devdrawer/widgets/WidgetsListAdapter.kt b/app/src/main/java/de/psdev/devdrawer/widgets/WidgetsListAdapter.kt
deleted file mode 100644
index 50480379..00000000
--- a/app/src/main/java/de/psdev/devdrawer/widgets/WidgetsListAdapter.kt
+++ /dev/null
@@ -1,46 +0,0 @@
-package de.psdev.devdrawer.widgets
-
-import android.view.ViewGroup
-import androidx.recyclerview.widget.ListAdapter
-import androidx.recyclerview.widget.RecyclerView
-import de.psdev.devdrawer.database.Widget
-import de.psdev.devdrawer.databinding.ListItemWidgetBinding
-import de.psdev.devdrawer.utils.layoutInflater
-import mu.KLogging
-
-class WidgetsListAdapter(
- private val clickListener: (Widget) -> Unit
-): ListAdapter(Widget.DIFF_CALLBACK) {
-
- companion object: KLogging()
-
- // ==========================================================================================================================
- // RecyclerView.Adapter
- // ==========================================================================================================================
-
- override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): WidgetsListViewHolder =
- WidgetsListViewHolder(
- ListItemWidgetBinding.inflate(parent.layoutInflater, parent, false),
- clickListener
- )
-
- override fun onBindViewHolder(holder: WidgetsListViewHolder, position: Int) {
- holder.bindTo(getItem(position))
- }
-
- // ==========================================================================================================================
- // WidgetsListViewHolder
- // ==========================================================================================================================
-
- class WidgetsListViewHolder(
- private val binding: ListItemWidgetBinding,
- private val clickListener: (Widget) -> Unit
- ): RecyclerView.ViewHolder(binding.root) {
-
- fun bindTo(widget: Widget) {
- binding.txtName.text = widget.name
- itemView.setOnClickListener { clickListener(widget) }
- }
- }
-}
-
diff --git a/app/src/main/res/layout/fragment_widget_list.xml b/app/src/main/res/layout/fragment_widget_list.xml
deleted file mode 100644
index 2e0c0e47..00000000
--- a/app/src/main/res/layout/fragment_widget_list.xml
+++ /dev/null
@@ -1,48 +0,0 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
\ No newline at end of file
diff --git a/app/src/main/res/layout/fragment_widget_profile_edit.xml b/app/src/main/res/layout/fragment_widget_profile_edit.xml
deleted file mode 100644
index f767f9e0..00000000
--- a/app/src/main/res/layout/fragment_widget_profile_edit.xml
+++ /dev/null
@@ -1,83 +0,0 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
diff --git a/app/src/main/res/layout/fragment_widget_profile_list.xml b/app/src/main/res/layout/fragment_widget_profile_list.xml
deleted file mode 100644
index 437e06d8..00000000
--- a/app/src/main/res/layout/fragment_widget_profile_list.xml
+++ /dev/null
@@ -1,18 +0,0 @@
-
-
-
-
-
diff --git a/app/src/main/res/layout/list_item_package_filter.xml b/app/src/main/res/layout/list_item_package_filter.xml
deleted file mode 100644
index 942bd821..00000000
--- a/app/src/main/res/layout/list_item_package_filter.xml
+++ /dev/null
@@ -1,78 +0,0 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
diff --git a/app/src/main/res/layout/list_item_widget.xml b/app/src/main/res/layout/list_item_widget.xml
deleted file mode 100644
index 4adbe7d4..00000000
--- a/app/src/main/res/layout/list_item_widget.xml
+++ /dev/null
@@ -1,32 +0,0 @@
-
-
-
-
-
-
-
-
-
\ No newline at end of file
diff --git a/app/src/main/res/navigation/nav_config_widget.xml b/app/src/main/res/navigation/nav_config_widget.xml
index cfda4f96..390adc3d 100644
--- a/app/src/main/res/navigation/nav_config_widget.xml
+++ b/app/src/main/res/navigation/nav_config_widget.xml
@@ -8,7 +8,7 @@
+ android:name="de.psdev.devdrawer.widgets.WidgetListFragment">
+ android:name="de.psdev.devdrawer.profiles.WidgetProfileListFragment">
@@ -50,7 +48,7 @@
Cannot delete profile, still being used by widgets
No
Yes
+ ComposeActivity
+
+ ID: %1$d
+
+
+ Create new profile
+
+
+ ID: %1$s
diff --git a/app/src/main/res/values/themes.xml b/app/src/main/res/values/themes.xml
index 673b4235..abff3ca2 100644
--- a/app/src/main/res/values/themes.xml
+++ b/app/src/main/res/values/themes.xml
@@ -39,8 +39,12 @@
+
+
+
+
+
+
\ No newline at end of file
diff --git a/build.gradle b/build.gradle
index b1c6f1dd..bb2638c4 100644
--- a/build.gradle
+++ b/build.gradle
@@ -5,11 +5,11 @@ buildscript {
repositories {
google()
mavenCentral()
- jcenter()
+// jcenter()
maven { url "https://plugins.gradle.org/m2" }
}
dependencies {
- classpath Plugins.android_gradle
+ classpath 'com.android.tools.build:gradle:7.0.0-beta02'
classpath Plugins.kotlin
classpath Plugins.navigation_safeargs
classpath Plugins.google_services_gradle
@@ -18,7 +18,7 @@ buildscript {
classpath Plugins.firebasePerformancePlugin
classpath Plugins.daggerHiltPlugin
classpath Plugins.aboutLibrariesPlugin
- classpath "com.github.triplet.gradle:play-publisher:3.4.0-agp4.2"
+ classpath "com.github.triplet.gradle:play-publisher:3.4.0-agp7.0"
// NOTE: Do not place your application dependencies here; they belong
// in the individual module build.gradle files
}
diff --git a/buildSrc/build.gradle.kts b/buildSrc/build.gradle.kts
index 24f69106..fc15d752 100644
--- a/buildSrc/build.gradle.kts
+++ b/buildSrc/build.gradle.kts
@@ -5,9 +5,9 @@ plugins {
repositories {
// The org.jetbrains.kotlin.jvm plugin requires a repository
// where to download the Kotlin compiler dependencies from.
+ mavenCentral()
jcenter()
}
kotlinDslPluginOptions {
- experimentalWarning.set(false)
}
\ No newline at end of file
diff --git a/buildSrc/src/main/kotlin/Dependencies.kt b/buildSrc/src/main/kotlin/Dependencies.kt
index 358a7dd8..ac8770b5 100644
--- a/buildSrc/src/main/kotlin/Dependencies.kt
+++ b/buildSrc/src/main/kotlin/Dependencies.kt
@@ -1,6 +1,6 @@
object Versions {
// Plugins
- const val androidGradlePlugin = "4.2.1"
+ const val androidGradlePlugin = "7.0.0-beta02"
const val gradleVersionsPlugin = "0.38.0"
const val googleServicesVersion = "4.3.8"
const val firebaseCrashlyticsPlugin = "2.6.1"
@@ -10,27 +10,28 @@ object Versions {
const val firebasePlatform = "28.0.1"
// Libs
- const val aboutLibraries = "8.8.5"
- const val androidXAppCompat = "1.3.0-rc01"
+ const val aboutLibraries = "8.8.6"
+ const val androidXAppCompat = "1.4.0-alpha01"
const val androidXBrowser = "1.3.0"
+ const val androidXCompose = "1.0.0-beta07"
const val androidXConstraintLayout = "2.1.0-beta02"
- const val androidXCore = "1.6.0-alpha03"
+ const val androidXCore = "1.6.0-beta01"
const val androidXFragment = "1.3.3"
const val androidXHilt = "1.0.0"
const val androidXLifecycle = "2.4.0-alpha01"
- const val androidXNavigation = "2.3.5"
+ const val androidXNavigation = "2.4.0-alpha01"
const val androidXPreference = "1.1.1"
const val androidXRecyclerView = "1.2.0"
const val androidXRecyclerViewSelection = "1.2.0-alpha01"
const val androidXRoom = "2.4.0-alpha02"
const val androidXStartup = "1.0.0"
- const val androidXWorkManager = "2.6.0-alpha02"
+ const val androidXWorkManager = "2.5.0"
const val daggerHilt = "2.35.1"
const val flowBinding = "1.0.0"
const val googlePlayCore = "1.10.0"
const val googlePlayCoreKtx = "1.8.1"
- const val kotlin = "1.5.0"
- const val kotlinCoroutines = "1.5.0"
+ const val kotlin = "1.4.32"
+ const val kotlinCoroutines = "1.4.3"
const val kotlinLogging = "2.0.6"
const val leakCanary = "2.7"
const val materialComponents = "1.4.0-beta01"
@@ -61,8 +62,7 @@ object Plugins {
"com.google.firebase:firebase-crashlytics-gradle:${Versions.firebaseCrashlyticsPlugin}"
const val firebasePerformancePlugin = "com.google.firebase:perf-plugin:${Versions.firebasePerformancePlugin}"
const val daggerHiltPlugin = "com.google.dagger:hilt-android-gradle-plugin:${Versions.daggerHilt}"
- const val aboutLibrariesPlugin =
- "com.mikepenz.aboutlibraries.plugin:aboutlibraries-plugin:${Versions.aboutLibraries}"
+ const val aboutLibrariesPlugin = "com.mikepenz.aboutlibraries.plugin:aboutlibraries-plugin:${Versions.aboutLibraries}"
}
object Libs {
@@ -79,15 +79,18 @@ object Libs {
const val androidx_hilt_work = "androidx.hilt:hilt-work:${Versions.androidXHilt}"
const val androidx_hilt_compiler = "androidx.hilt:hilt-compiler:${Versions.androidXHilt}"
const val androidx_core = "androidx.core:core-ktx:${Versions.androidXCore}"
+
const val androidx_lifecycle_viewmodel = "androidx.lifecycle:lifecycle-viewmodel-ktx:${Versions.androidXLifecycle}"
const val androidx_lifecycle_livedata = "androidx.lifecycle:lifecycle-livedata-ktx:${Versions.androidXLifecycle}"
const val androidx_lifecycle_java8 = "androidx.lifecycle:lifecycle-common-java8:${Versions.androidXLifecycle}"
const val androidx_lifecycle_process = "androidx.lifecycle:lifecycle-process:${Versions.androidXLifecycle}"
- const val androidx_navigation_fragment =
- "androidx.navigation:navigation-fragment-ktx:${Versions.androidXNavigation}"
+ const val androidx_lifecycle_runtime = "androidx.lifecycle:lifecycle-runtime-ktx:${Versions.androidXLifecycle}"
+
+ const val androidx_navigation_fragment = "androidx.navigation:navigation-fragment-ktx:${Versions.androidXNavigation}"
const val androidx_navigation_ui = "androidx.navigation:navigation-ui-ktx:${Versions.androidXNavigation}"
const val androidx_recyclerview = "androidx.recyclerview:recyclerview:${Versions.androidXRecyclerView}"
- const val androidx_recyclerview_selection = "androidx.recyclerview:recyclerview-selection:${Versions.androidXRecyclerViewSelection}"
+ const val androidx_recyclerview_selection =
+ "androidx.recyclerview:recyclerview-selection:${Versions.androidXRecyclerViewSelection}"
const val androidx_room_runtime = "androidx.room:room-runtime:${Versions.androidXRoom}"
const val androidx_room_ktx = "androidx.room:room-ktx:${Versions.androidXRoom}"
const val androidx_room_compiler = "androidx.room:room-compiler:${Versions.androidXRoom}"
diff --git a/gradle.properties b/gradle.properties
index b7d75211..e92ce7e2 100644
--- a/gradle.properties
+++ b/gradle.properties
@@ -17,5 +17,6 @@ org.gradle.jvmargs=-Xmx2048m -Dfile.encoding=UTF-8
# Enable Jetifier
android.useAndroidX=true
android.enableJetifier=true
+android.nonTransitiveRClass=false
# Kotlin
kotlin.code.style=official
\ No newline at end of file