Add old app/template management screens for now

This commit is contained in:
frknkrc44 2025-08-26 16:25:08 +03:00
commit 2621d656d9
No known key found for this signature in database
GPG key ID: 784AE1CC0DCFD34F
27 changed files with 1233 additions and 87 deletions

View file

@ -0,0 +1,34 @@
package icu.nullptr.hidemyapplist.ui.adapter
import android.view.ViewGroup
import icu.nullptr.hidemyapplist.service.ConfigManager
import icu.nullptr.hidemyapplist.ui.view.AppItemView
class AppManageAdapter(
private val onItemClickListener: (String) -> Unit
) : AppSelectAdapter() {
inner class ViewHolder(view: AppItemView) : AppSelectAdapter.ViewHolder(view) {
init {
view.setOnClickListener {
onItemClickListener.invoke(filteredList[absoluteAdapterPosition])
}
}
override fun bind(packageName: String) {
(itemView as AppItemView).let {
it.load(packageName)
it.showEnabled = ConfigManager.isHideEnabled(packageName)
}
}
}
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {
val view = AppItemView(parent.context, false)
view.layoutParams = ViewGroup.LayoutParams(
ViewGroup.LayoutParams.MATCH_PARENT,
ViewGroup.LayoutParams.WRAP_CONTENT
)
return ViewHolder(view)
}
}

View file

@ -0,0 +1,42 @@
package icu.nullptr.hidemyapplist.ui.adapter
import android.view.ViewGroup
import icu.nullptr.hidemyapplist.ui.view.AppItemView
class AppScopeAdapter(
private val checked: MutableSet<String>,
firstFilter: ((String) -> Boolean)?,
) : AppSelectAdapter(firstFilter) {
private inline var String.isChecked
get() = checked.contains(this)
set(value) {
if (value) checked.add(this) else checked.remove(this)
}
inner class ViewHolder(view: AppItemView) : AppSelectAdapter.ViewHolder(view) {
init {
view.setOnClickListener {
val packageName = filteredList[absoluteAdapterPosition]
packageName.isChecked = !packageName.isChecked
view.isChecked = packageName.isChecked
}
}
override fun bind(packageName: String) {
(itemView as AppItemView).let {
it.load(packageName)
it.isChecked = packageName.isChecked
}
}
}
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {
val view = AppItemView(parent.context, true)
view.layoutParams = ViewGroup.LayoutParams(
ViewGroup.LayoutParams.MATCH_PARENT,
ViewGroup.LayoutParams.WRAP_CONTENT
)
return ViewHolder(view)
}
}

View file

@ -0,0 +1,54 @@
package icu.nullptr.hidemyapplist.ui.adapter
import android.widget.Filter
import android.widget.Filterable
import androidx.recyclerview.widget.RecyclerView
import icu.nullptr.hidemyapplist.service.PrefManager
import icu.nullptr.hidemyapplist.ui.view.AppItemView
import icu.nullptr.hidemyapplist.util.PackageHelper
import kotlinx.coroutines.flow.first
import kotlinx.coroutines.runBlocking
abstract class AppSelectAdapter(
private val firstFilter: ((String) -> Boolean)? = null
) : RecyclerView.Adapter<AppSelectAdapter.ViewHolder>(), Filterable {
abstract class ViewHolder(view: AppItemView) : RecyclerView.ViewHolder(view) {
abstract fun bind(packageName: String)
}
private inner class AppFilter : Filter() {
override fun performFiltering(constraint: CharSequence): FilterResults {
return runBlocking {
val constraintLowered = constraint.toString().lowercase()
val filteredList = PackageHelper.appList.first().filter {
if (firstFilter?.invoke(it) == false) return@filter false
if (!PrefManager.appFilter_showSystem && PackageHelper.isSystem(it)) return@filter false
val label = PackageHelper.loadAppLabel(it)
val packageInfo = PackageHelper.loadPackageInfo(it)
label.lowercase().contains(constraintLowered) || packageInfo.packageName.lowercase().contains(constraintLowered)
}
FilterResults().also { it.values = filteredList }
}
}
@Suppress("UNCHECKED_CAST", "NotifyDataSetChanged")
override fun publishResults(constraint: CharSequence, results: FilterResults) {
filteredList = results.values as List<String>
notifyDataSetChanged()
}
}
private val mFilter = AppFilter()
protected var filteredList: List<String> = listOf()
override fun getItemCount() = filteredList.size
override fun getItemId(position: Int) = filteredList[position].hashCode().toLong()
override fun onBindViewHolder(holder: ViewHolder, position: Int) = holder.bind(filteredList[position])
override fun getFilter(): Filter = mFilter
}

View file

@ -0,0 +1,68 @@
package icu.nullptr.hidemyapplist.ui.adapter
import android.annotation.SuppressLint
import android.view.ViewGroup
import androidx.recyclerview.widget.RecyclerView
import icu.nullptr.hidemyapplist.service.ConfigManager
import icu.nullptr.hidemyapplist.ui.view.ListItemView
import org.frknkrc44.hma_oss.R
import java.text.Collator
import java.util.Locale
class TemplateAdapter(
private val onClickListener: ((ConfigManager.TemplateInfo) -> Unit)?
) : RecyclerView.Adapter<TemplateAdapter.ViewHolder>() {
private lateinit var list: List<ConfigManager.TemplateInfo>
init {
updateList()
}
inner class ViewHolder(view: ListItemView) : RecyclerView.ViewHolder(view) {
init {
view.setOnClickListener {
onClickListener?.invoke(list[absoluteAdapterPosition])
}
}
fun bind(info: ConfigManager.TemplateInfo) {
with(itemView as ListItemView) {
setIcon(
if (info.isWhiteList) R.drawable.outline_assignment_24
else R.drawable.baseline_assignment_24
)
text = info.name
}
}
}
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {
val view = ListItemView(parent.context)
view.layoutParams = ViewGroup.LayoutParams(
ViewGroup.LayoutParams.MATCH_PARENT,
ViewGroup.LayoutParams.WRAP_CONTENT
)
return ViewHolder(view)
}
override fun getItemCount() = list.size
override fun getItemId(position: Int) = list[position].name.hashCode().toLong()
override fun onBindViewHolder(holder: ViewHolder, position: Int) = holder.bind(list[position])
@SuppressLint("NotifyDataSetChanged")
fun updateList() {
list = ConfigManager.getTemplateList().apply {
sortWith { o1, o2 ->
if (o1.isWhiteList != o2.isWhiteList) {
o1.isWhiteList.compareTo(o2.isWhiteList)
} else {
Collator.getInstance(Locale.getDefault()).compare(o1.name, o2.name)
}
}
}
notifyDataSetChanged()
}
}

View file

@ -0,0 +1,24 @@
package icu.nullptr.hidemyapplist.ui.fragment
import android.os.Bundle
import com.google.android.material.transition.MaterialSharedAxis
import icu.nullptr.hidemyapplist.service.ConfigManager
import icu.nullptr.hidemyapplist.ui.adapter.AppManageAdapter
import icu.nullptr.hidemyapplist.ui.util.navController
import org.frknkrc44.hma_oss.R
class AppManageFragment : AppSelectFragment() {
override val firstComparator: Comparator<String> = Comparator.comparing(ConfigManager::isHideEnabled).reversed()
override val adapter = AppManageAdapter {
val args = AppSettingsFragmentArgs(it)
navController.navigate(R.id.nav_app_settings, args.toBundle())
}
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
// enterTransition = MaterialSharedAxis(MaterialSharedAxis.Z, true)
// returnTransition = MaterialSharedAxis(MaterialSharedAxis.Z, false)
}
}

View file

@ -0,0 +1,124 @@
package icu.nullptr.hidemyapplist.ui.fragment
import android.os.Bundle
import android.view.MenuItem
import android.view.View
import androidx.activity.addCallback
import androidx.appcompat.widget.SearchView
import androidx.fragment.app.Fragment
import androidx.lifecycle.flowWithLifecycle
import androidx.lifecycle.lifecycleScope
import androidx.recyclerview.widget.LinearLayoutManager
import by.kirich1409.viewbindingdelegate.viewBinding
import icu.nullptr.hidemyapplist.service.PrefManager
import icu.nullptr.hidemyapplist.ui.adapter.AppSelectAdapter
import icu.nullptr.hidemyapplist.ui.util.navController
import icu.nullptr.hidemyapplist.ui.util.setupToolbar
import icu.nullptr.hidemyapplist.util.PackageHelper
import kotlinx.coroutines.launch
import org.frknkrc44.hma_oss.R
import org.frknkrc44.hma_oss.databinding.FragmentAppSelectBinding
abstract class AppSelectFragment : Fragment(R.layout.fragment_app_select) {
private val binding by viewBinding<FragmentAppSelectBinding>()
protected abstract val firstComparator: Comparator<String>
protected abstract val adapter: AppSelectAdapter
private var search = ""
protected open fun onBack() {
navController.navigateUp()
}
private fun applyFilter() {
adapter.filter.filter(search)
}
private fun sortList() {
lifecycleScope.launch {
PackageHelper.sortList(firstComparator)
applyFilter()
}
}
private fun onMenuOptionSelected(item: MenuItem) {
when (item.itemId) {
R.id.menu_show_system -> {
item.isChecked = !item.isChecked
PrefManager.appFilter_showSystem = item.isChecked
}
R.id.menu_sort_by_label -> {
item.isChecked = true
PrefManager.appFilter_sortMethod = PrefManager.SortMethod.BY_LABEL
}
R.id.menu_sort_by_package_name -> {
item.isChecked = true
PrefManager.appFilter_sortMethod = PrefManager.SortMethod.BY_PACKAGE_NAME
}
R.id.menu_sort_by_install_time -> {
item.isChecked = true
PrefManager.appFilter_sortMethod = PrefManager.SortMethod.BY_INSTALL_TIME
}
R.id.menu_sort_by_update_time -> {
item.isChecked = true
PrefManager.appFilter_sortMethod = PrefManager.SortMethod.BY_UPDATE_TIME
}
R.id.menu_reverse_order -> {
item.isChecked = !item.isChecked
PrefManager.appFilter_reverseOrder = item.isChecked
}
}
sortList()
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
requireActivity().onBackPressedDispatcher.addCallback(viewLifecycleOwner) { onBack() }
setupToolbar(
toolbar = binding.toolbar,
title = getString(R.string.title_app_select),
navigationIcon = R.drawable.baseline_arrow_back_24,
navigationOnClick = { onBack() },
menuRes = R.menu.menu_app_list,
onMenuOptionSelected = this::onMenuOptionSelected
)
with(binding.toolbar.menu) {
val searchView = findItem(R.id.menu_search).actionView as SearchView
searchView.setOnQueryTextListener(object : SearchView.OnQueryTextListener {
override fun onQueryTextSubmit(query: String) = false
override fun onQueryTextChange(newText: String): Boolean {
search = newText
applyFilter()
return true
}
})
findItem(R.id.menu_show_system).isChecked = PrefManager.appFilter_showSystem
when (PrefManager.appFilter_sortMethod) {
PrefManager.SortMethod.BY_LABEL -> findItem(R.id.menu_sort_by_label).isChecked = true
PrefManager.SortMethod.BY_PACKAGE_NAME -> findItem(R.id.menu_sort_by_package_name).isChecked = true
PrefManager.SortMethod.BY_INSTALL_TIME -> findItem(R.id.menu_sort_by_install_time).isChecked = true
PrefManager.SortMethod.BY_UPDATE_TIME -> findItem(R.id.menu_sort_by_update_time).isChecked = true
}
findItem(R.id.menu_reverse_order).isChecked = PrefManager.appFilter_reverseOrder
}
binding.list.layoutManager = LinearLayoutManager(context)
binding.list.adapter = adapter
binding.swipeRefresh.setOnRefreshListener {
PackageHelper.invalidateCache()
}
lifecycleScope.launch {
PackageHelper.isRefreshing
.flowWithLifecycle(lifecycle)
.collect {
binding.swipeRefresh.isRefreshing = it
}
}
sortList()
}
}

View file

@ -0,0 +1,210 @@
package icu.nullptr.hidemyapplist.ui.fragment
import android.annotation.SuppressLint
import android.os.Bundle
import android.view.View
import androidx.activity.addCallback
import androidx.core.graphics.drawable.toDrawable
import androidx.fragment.app.Fragment
import androidx.fragment.app.clearFragmentResultListener
import androidx.fragment.app.setFragmentResultListener
import androidx.fragment.app.viewModels
import androidx.navigation.fragment.navArgs
import androidx.preference.Preference
import androidx.preference.PreferenceDataStore
import androidx.preference.PreferenceFragmentCompat
import androidx.preference.SwitchPreference
import androidx.preference.SwitchPreferenceCompat
import by.kirich1409.viewbindingdelegate.viewBinding
import com.google.android.material.dialog.MaterialAlertDialogBuilder
import icu.nullptr.hidemyapplist.common.JsonConfig
import icu.nullptr.hidemyapplist.common.Presets
import icu.nullptr.hidemyapplist.service.ConfigManager
import icu.nullptr.hidemyapplist.ui.util.navController
import icu.nullptr.hidemyapplist.ui.util.setupToolbar
import icu.nullptr.hidemyapplist.ui.viewmodel.AppSettingsViewModel
import icu.nullptr.hidemyapplist.util.PackageHelper
import org.frknkrc44.hma_oss.BuildConfig
import org.frknkrc44.hma_oss.R
import org.frknkrc44.hma_oss.databinding.FragmentSettingsBinding
class AppSettingsFragment : Fragment(R.layout.fragment_settings) {
private val binding by viewBinding<FragmentSettingsBinding>()
private val viewModel by viewModels<AppSettingsViewModel>() {
val args by navArgs<AppSettingsFragmentArgs>()
val cfg = ConfigManager.getAppConfig(args.packageName)
val pack = if (cfg != null) AppSettingsViewModel.Pack(args.packageName, true, cfg)
else AppSettingsViewModel.Pack(args.packageName, false, JsonConfig.AppConfig())
AppSettingsViewModel.Factory(pack)
}
private fun saveConfig() {
if (!viewModel.pack.enabled) ConfigManager.setAppConfig(viewModel.pack.app, null)
else ConfigManager.setAppConfig(viewModel.pack.app, viewModel.pack.config)
}
private fun onBack() {
saveConfig()
navController.navigateUp()
}
override fun onPause() {
super.onPause()
saveConfig()
}
override fun onDestroyView() {
super.onDestroyView()
saveConfig()
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
requireActivity().onBackPressedDispatcher.addCallback(viewLifecycleOwner) { onBack() }
setupToolbar(
toolbar = binding.toolbar,
title = getString(R.string.title_app_settings),
navigationIcon = R.drawable.baseline_arrow_back_24,
navigationOnClick = { onBack() }
)
if (childFragmentManager.findFragmentById(R.id.settings_container) == null) {
childFragmentManager.beginTransaction()
.replace(R.id.settings_container, AppPreferenceFragment())
.commit()
}
}
class AppPreferenceDataStore(private val pack: AppSettingsViewModel.Pack) : PreferenceDataStore() {
override fun getBoolean(key: String, defValue: Boolean): Boolean {
return when (key) {
"enableHide" -> pack.enabled
"useWhiteList" -> pack.config.useWhitelist
"excludeSystemApps" -> pack.config.excludeSystemApps
else -> throw IllegalArgumentException("Invalid key: $key")
}
}
override fun putBoolean(key: String, value: Boolean) {
when (key) {
"enableHide" -> pack.enabled = value
"useWhiteList" -> pack.config.useWhitelist = value
"excludeSystemApps" -> pack.config.excludeSystemApps = value
else -> throw IllegalArgumentException("Invalid key: $key")
}
}
}
class AppPreferenceFragment : PreferenceFragmentCompat() {
private val parent get() = requireParentFragment() as AppSettingsFragment
private val pack get() = parent.viewModel.pack
private fun updateApplyTemplates() {
findPreference<Preference>("applyTemplates")?.title =
getString(R.string.app_template_using, pack.config.applyTemplates.size)
}
private fun updateApplyPresets() {
findPreference<Preference>("applyPresets")?.title =
getString(R.string.app_preset_using, pack.config.applyPresets.size)
}
private fun updateExtraAppList(useWhiteList: Boolean) {
findPreference<Preference>("extraAppList")?.title =
if (useWhiteList) getString(R.string.app_extra_apps_visible_count, pack.config.extraAppList.size)
else getString(R.string.app_extra_apps_invisible_count, pack.config.extraAppList.size)
}
@SuppressLint("DiscouragedApi")
override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) {
preferenceManager.preferenceDataStore = AppPreferenceDataStore(pack)
setPreferencesFromResource(R.xml.app_settings, rootKey)
findPreference<Preference>("appInfo")?.let {
it.icon = PackageHelper.loadAppIcon(pack.app).toDrawable(resources)
it.title = PackageHelper.loadAppLabel(pack.app)
it.summary = PackageHelper.loadPackageInfo(pack.app).packageName
}
findPreference<SwitchPreferenceCompat>("useWhiteList")?.setOnPreferenceChangeListener { _, newValue ->
pack.config.applyTemplates.clear()
pack.config.extraAppList.clear()
updateApplyTemplates()
updateApplyPresets()
updateExtraAppList(newValue as Boolean)
true
}
findPreference<Preference>("applyTemplates")?.setOnPreferenceClickListener {
val templates = ConfigManager.getTemplateList().mapNotNull {
if (it.isWhiteList == pack.config.useWhitelist) it.name else null
}.toTypedArray()
val checked = templates.map {
pack.config.applyTemplates.contains(it)
}.toBooleanArray()
MaterialAlertDialogBuilder(requireContext())
.setTitle(R.string.app_choose_template)
.setMultiChoiceItems(templates, checked) { _, i, value -> checked[i] = value }
.setNegativeButton(android.R.string.cancel, null)
.setPositiveButton(android.R.string.ok) { _, _ ->
pack.config.applyTemplates = templates.mapIndexedNotNullTo(mutableSetOf()) { i, name ->
if (checked[i]) name else null
}
updateApplyTemplates()
}
.show()
true
}
findPreference<Preference>("applyPresets")?.setOnPreferenceClickListener {
val presetNames = Presets.instance.getAllPresetNames()
val presetTranslations = presetNames.map { name ->
try {
val id = resources.getIdentifier(
"preset_${name}",
"string",
BuildConfig.APPLICATION_ID
)
return@map if (id != 0) { getString(id) } else { name }
} catch (ignore: Throwable) {}
name
}
val presets = presetNames.zip(presetTranslations).toMap().toSortedMap()
val checked = presets.keys.map {
pack.config.applyPresets.contains(it)
}.toBooleanArray()
val presetValues = presets.values.toTypedArray()
MaterialAlertDialogBuilder(requireContext())
.setTitle(R.string.app_choose_preset)
.setMultiChoiceItems(presetValues, checked) { _, i, value -> checked[i] = value }
.setNegativeButton(android.R.string.cancel, null)
.setPositiveButton(android.R.string.ok) { _, _ ->
pack.config.applyPresets = presetValues.mapIndexedNotNullTo(mutableSetOf()) { i, name ->
if (checked[i]) presets.filterValues { v -> v == name }.keys.first() else null
}
updateApplyPresets()
}
.show()
true
}
findPreference<Preference>("extraAppList")?.setOnPreferenceClickListener {
parent.setFragmentResultListener("app_select") { _, bundle ->
pack.config.extraAppList = bundle.getStringArrayList("checked")!!.toMutableSet()
updateExtraAppList(pack.config.useWhitelist)
parent.clearFragmentResultListener("app_select")
}
val args = ScopeFragmentArgs(
filterOnlyEnabled = false,
checked = pack.config.extraAppList.toTypedArray()
)
parent.navController.navigate(R.id.nav_scope, args.toBundle())
true
}
updateApplyTemplates()
updateApplyPresets()
updateExtraAppList(pack.config.useWhitelist)
}
}
}

View file

@ -0,0 +1,29 @@
package icu.nullptr.hidemyapplist.ui.fragment
import android.os.Bundle
import androidx.fragment.app.setFragmentResult
import androidx.navigation.fragment.navArgs
import icu.nullptr.hidemyapplist.service.ConfigManager
import icu.nullptr.hidemyapplist.ui.adapter.AppScopeAdapter
import icu.nullptr.hidemyapplist.ui.util.navController
class ScopeFragment : AppSelectFragment() {
private lateinit var checked: MutableSet<String>
override val firstComparator: Comparator<String> = Comparator.comparing { !checked.contains(it) }
override val adapter by lazy {
val args by navArgs<ScopeFragmentArgs>()
checked = args.checked.toMutableSet()
if (!args.filterOnlyEnabled) AppScopeAdapter(checked, null)
else AppScopeAdapter(checked) { ConfigManager.getAppConfig(it)?.useWhitelist == args.isWhiteList }
}
override fun onBack() {
setFragmentResult("app_select", Bundle().apply {
putStringArrayList("checked", ArrayList(checked))
})
navController.navigateUp()
}
}

View file

@ -0,0 +1,85 @@
package icu.nullptr.hidemyapplist.ui.fragment
import android.graphics.Color
import android.os.Bundle
import android.view.View
import androidx.core.view.doOnPreDraw
import androidx.fragment.app.Fragment
import androidx.fragment.app.clearFragmentResultListener
import androidx.fragment.app.setFragmentResultListener
import androidx.navigation.fragment.FragmentNavigatorExtras
import androidx.recyclerview.widget.LinearLayoutManager
import by.kirich1409.viewbindingdelegate.viewBinding
import com.google.android.material.transition.MaterialContainerTransform
import icu.nullptr.hidemyapplist.common.JsonConfig
import icu.nullptr.hidemyapplist.service.ConfigManager
import icu.nullptr.hidemyapplist.ui.adapter.TemplateAdapter
import icu.nullptr.hidemyapplist.ui.util.navController
import icu.nullptr.hidemyapplist.ui.util.setupToolbar
import org.frknkrc44.hma_oss.R
import org.frknkrc44.hma_oss.databinding.FragmentTemplateManageBinding
class TemplateManageFragment : Fragment(R.layout.fragment_template_manage) {
private val binding by viewBinding<FragmentTemplateManageBinding>()
private val adapter = TemplateAdapter(this::navigateToSettings)
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
sharedElementEnterTransition = MaterialContainerTransform().apply {
drawingViewId = R.id.nav_host_fragment
scrimColor = Color.TRANSPARENT
}
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
setupToolbar(
toolbar = binding.toolbar,
title = getString(R.string.title_template_manage),
navigationIcon = R.drawable.baseline_arrow_back_24,
navigationOnClick = { navController.navigateUp() }
)
postponeEnterTransition()
view.doOnPreDraw { startPostponedEnterTransition() }
binding.newBlacklistTemplate.setOnClickListener {
navigateToSettings(ConfigManager.TemplateInfo(null, false))
}
binding.newWhitelistTemplate.setOnClickListener {
navigateToSettings(ConfigManager.TemplateInfo(null, true))
}
binding.templateList.layoutManager = LinearLayoutManager(context)
binding.templateList.adapter = adapter
}
private fun navigateToSettings(info: ConfigManager.TemplateInfo) {
setFragmentResultListener("template_settings") { _, bundle ->
fun deal() {
var name = bundle.getString("name")
val appliedList = bundle.getStringArrayList("appliedList")!!
val targetList = bundle.getStringArrayList("targetList")!!
if (info.name == null) { // New template
if (name.isNullOrEmpty()) return
ConfigManager.updateTemplate(name, JsonConfig.Template(info.isWhiteList, targetList.toSet()))
ConfigManager.updateTemplateAppliedApps(name, appliedList)
} else { // Existing template
if (name == null) {
ConfigManager.deleteTemplate(info.name)
} else {
if (name.isEmpty()) name = info.name
if (name != info.name) ConfigManager.renameTemplate(info.name, name)
ConfigManager.updateTemplate(name, JsonConfig.Template(info.isWhiteList, targetList.toSet()))
ConfigManager.updateTemplateAppliedApps(name, appliedList)
}
}
}
deal()
adapter.updateList()
clearFragmentResultListener("template_settings")
}
val args = TemplateSettingsFragmentArgs(info.name, info.isWhiteList)
val extras = FragmentNavigatorExtras(binding.hintCard to "transition_manage")
navController.navigate(R.id.nav_template_settings, args.toBundle(), null, extras)
}
}

View file

@ -0,0 +1,122 @@
package icu.nullptr.hidemyapplist.ui.fragment
import android.graphics.Color
import android.os.Bundle
import android.view.View
import androidx.activity.addCallback
import androidx.core.widget.addTextChangedListener
import androidx.fragment.app.Fragment
import androidx.fragment.app.clearFragmentResultListener
import androidx.fragment.app.setFragmentResult
import androidx.fragment.app.setFragmentResultListener
import androidx.fragment.app.viewModels
import androidx.lifecycle.lifecycleScope
import androidx.navigation.fragment.navArgs
import by.kirich1409.viewbindingdelegate.viewBinding
import com.google.android.material.dialog.MaterialAlertDialogBuilder
import com.google.android.material.transition.MaterialContainerTransform
import icu.nullptr.hidemyapplist.service.ConfigManager
import icu.nullptr.hidemyapplist.ui.util.navController
import icu.nullptr.hidemyapplist.ui.util.setupToolbar
import icu.nullptr.hidemyapplist.ui.viewmodel.TemplateSettingsViewModel
import kotlinx.coroutines.launch
import org.frknkrc44.hma_oss.R
import org.frknkrc44.hma_oss.databinding.FragmentTemplateSettingsBinding
class TemplateSettingsFragment : Fragment(R.layout.fragment_template_settings) {
private val binding by viewBinding<FragmentTemplateSettingsBinding>()
private val viewModel by viewModels<TemplateSettingsViewModel> {
val args by navArgs<TemplateSettingsFragmentArgs>()
TemplateSettingsViewModel.Factory(args)
}
private fun onBack(delete: Boolean) {
viewModel.name = viewModel.name?.trim()
if (viewModel.name != viewModel.originalName && (ConfigManager.hasTemplate(viewModel.name) || viewModel.name == null) || delete) {
val builder = MaterialAlertDialogBuilder(requireContext())
.setTitle(if (delete) R.string.template_delete_title else R.string.template_name_invalid)
.setMessage(if (delete) R.string.template_delete else R.string.template_name_already_exist)
.setPositiveButton(android.R.string.ok) { _, _ ->
saveResult(delete)
}
if (delete) builder.setNegativeButton(android.R.string.cancel, null)
builder.show()
} else {
saveResult(false)
}
}
private fun saveResult(delete: Boolean) {
setFragmentResult("template_settings", Bundle().apply {
putString("name",if (delete) null else viewModel.name)
putStringArrayList("appliedList", viewModel.appliedAppList.value)
putStringArrayList("targetList", viewModel.targetAppList.value)
})
navController.navigateUp()
}
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
/*
sharedElementEnterTransition = MaterialContainerTransform().apply {
drawingViewId = R.id.nav_host_fragment
scrimColor = Color.TRANSPARENT
}
*/
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
requireActivity().onBackPressedDispatcher.addCallback(viewLifecycleOwner) { onBack(false) }
setupToolbar(
toolbar = binding.toolbar,
title = getString(R.string.title_template_settings),
navigationIcon = R.drawable.baseline_arrow_back_24,
navigationOnClick = { onBack(false) },
menuRes = R.menu.menu_delete,
onMenuOptionSelected = {
onBack(true)
}
)
binding.templateName.setText(viewModel.name)
binding.templateName.addTextChangedListener { viewModel.name = it.toString() }
binding.targetApps.setOnClickListener {
setFragmentResultListener("app_select") { _, bundle ->
viewModel.targetAppList.value = bundle.getStringArrayList("checked")!!
clearFragmentResultListener("app_select")
}
val args = ScopeFragmentArgs(
filterOnlyEnabled = false,
checked = viewModel.targetAppList.value.toTypedArray()
)
navController.navigate(R.id.nav_scope, args.toBundle())
}
binding.appliedApps.setOnClickListener {
setFragmentResultListener("app_select") { _, bundle ->
viewModel.appliedAppList.value = bundle.getStringArrayList("checked")!!
clearFragmentResultListener("app_select")
}
val args = ScopeFragmentArgs(
filterOnlyEnabled = true,
isWhiteList = viewModel.isWhiteList,
checked = viewModel.appliedAppList.value.toTypedArray()
)
navController.navigate(R.id.nav_scope, args.toBundle())
}
lifecycleScope.launch {
viewModel.targetAppList.collect {
val fmt =
if (viewModel.isWhiteList) R.string.template_apps_visible_count
else R.string.template_apps_invisible_count
binding.targetApps.text = String.format(getString(fmt), it.size)
}
}
lifecycleScope.launch {
viewModel.appliedAppList.collect {
binding.appliedApps.text = String.format(getString(R.string.template_applied_count), it.size)
}
}
}
}

View file

@ -0,0 +1,41 @@
package icu.nullptr.hidemyapplist.ui.view
import android.content.Context
import android.util.AttributeSet
import android.widget.LinearLayout
import by.kirich1409.viewbindingdelegate.CreateMethod
import by.kirich1409.viewbindingdelegate.viewBinding
import icu.nullptr.hidemyapplist.util.PackageHelper
import org.frknkrc44.hma_oss.databinding.AppItemViewBinding
class AppItemView @JvmOverloads constructor(
context: Context,
attrs: AttributeSet? = null,
defStyleAttr: Int = 0,
defStyleRes: Int = 0
) : LinearLayout(context, attrs, defStyleAttr, defStyleRes) {
private val binding by viewBinding<AppItemViewBinding>(createMethod = CreateMethod.INFLATE)
var showEnabled: Boolean
get() = binding.enabled.visibility == VISIBLE
set(value) {
binding.enabled.visibility = if (value) VISIBLE else GONE
}
var isChecked: Boolean
get() = binding.checkbox.isChecked
set(value) {
binding.checkbox.isChecked = value
}
constructor(context: Context, isCheckable: Boolean) : this(context) {
binding.checkbox.visibility = if (isCheckable) VISIBLE else GONE
}
fun load(packageName: String) {
binding.packageName.text = packageName
binding.label.text = PackageHelper.loadAppLabel(packageName)
binding.icon.setImageBitmap(PackageHelper.loadAppIcon(packageName))
}
}

View file

@ -0,0 +1,52 @@
package icu.nullptr.hidemyapplist.ui.view
import android.content.Context
import android.util.AttributeSet
import android.widget.LinearLayout
import androidx.annotation.DrawableRes
import by.kirich1409.viewbindingdelegate.CreateMethod
import by.kirich1409.viewbindingdelegate.viewBinding
import org.frknkrc44.hma_oss.R
import org.frknkrc44.hma_oss.databinding.ListItemViewBinding
class ListItemView @JvmOverloads constructor(
context: Context,
attrs: AttributeSet? = null,
defStyleAttr: Int = 0,
defStyleRes: Int = 0
) : LinearLayout(context, attrs, defStyleAttr, defStyleRes) {
private val binding by viewBinding<ListItemViewBinding>(createMethod = CreateMethod.INFLATE)
init {
val typedArray = context.theme.obtainStyledAttributes(attrs, R.styleable.ListItemView, defStyleAttr, defStyleRes)
val icon = typedArray.getResourceId(R.styleable.ListItemView_icon, 0)
val text = typedArray.getString(R.styleable.ListItemView_text)
val buttonText = typedArray.getText(R.styleable.ListItemView_buttonText)
typedArray.recycle()
binding.icon.setImageResource(icon)
binding.text.text = text
if (buttonText != null) {
binding.button.visibility = VISIBLE
binding.button.text = buttonText
}
}
var text: CharSequence?
get() = binding.text.text
set(value) {
binding.text.text = value
}
fun setIcon(@DrawableRes icon: Int) {
binding.icon.setImageResource(icon)
}
override fun setOnClickListener(l: OnClickListener?) {
if (binding.button.visibility == VISIBLE) {
binding.button.setOnClickListener(l)
} else {
super.setOnClickListener(l)
}
}
}

View file

@ -0,0 +1,23 @@
package icu.nullptr.hidemyapplist.ui.viewmodel
import androidx.lifecycle.ViewModel
import androidx.lifecycle.ViewModelProvider
import icu.nullptr.hidemyapplist.common.JsonConfig
class AppSettingsViewModel(val pack: Pack) : ViewModel() {
class Factory(private val pack: Pack) : ViewModelProvider.Factory {
override fun <T : ViewModel> create(modelClass: Class<T>): T {
if (modelClass.isAssignableFrom(AppSettingsViewModel::class.java)) {
@Suppress("UNCHECKED_CAST")
return AppSettingsViewModel(pack) as T
} else throw IllegalArgumentException("Unknown ViewModel class")
}
}
class Pack(
val app: String,
var enabled: Boolean,
val config: JsonConfig.AppConfig
)
}

View file

@ -0,0 +1,31 @@
package icu.nullptr.hidemyapplist.ui.viewmodel
import androidx.lifecycle.ViewModel
import androidx.lifecycle.ViewModelProvider
import icu.nullptr.hidemyapplist.service.ConfigManager
import icu.nullptr.hidemyapplist.ui.fragment.TemplateSettingsFragmentArgs
import kotlinx.coroutines.flow.MutableStateFlow
class TemplateSettingsViewModel(
val originalName: String?,
val isWhiteList: Boolean,
var name: String?
) : ViewModel() {
class Factory(private val args: TemplateSettingsFragmentArgs) : ViewModelProvider.Factory {
override fun <T : ViewModel> create(modelClass: Class<T>): T {
if (modelClass.isAssignableFrom(TemplateSettingsViewModel::class.java)) {
val viewModel = TemplateSettingsViewModel(args.name, args.isWhiteList, args.name)
args.name?.let {
viewModel.appliedAppList.value = ConfigManager.getTemplateAppliedAppList(it)
viewModel.targetAppList.value = ConfigManager.getTemplateTargetAppList(it)
}
@Suppress("UNCHECKED_CAST")
return viewModel as T
} else throw IllegalArgumentException("Unknown ViewModel class")
}
}
val appliedAppList = MutableStateFlow<ArrayList<String>>(ArrayList())
val targetAppList = MutableStateFlow<ArrayList<String>>(ArrayList())
}

View file

@ -1,4 +0,0 @@
package org.frknkrc44.hma_oss.ui.adapter
class AppListAdapter {
}

View file

@ -87,7 +87,7 @@ class HomeFragment : Fragment(R.layout.fragment_home) {
}
}
@SuppressLint("StringFormatInvalid")
@SuppressLint("StringFormatInvalid", "StringFormatMatches")
override fun onStart() {
super.onStart()
@ -223,7 +223,6 @@ class HomeFragment : Fragment(R.layout.fragment_home) {
with(binding.howToUse) {
text1.text = getString(R.string.about_how_to_use_title)
icon.setImageResource(R.drawable.outline_info_24)
// itemIcon.setCircleBackground(getColor(R.color.info))
root.setOnClickListener {
MaterialAlertDialogBuilder(requireContext())
.setTitle(R.string.about_how_to_use_title)
@ -239,19 +238,22 @@ class HomeFragment : Fragment(R.layout.fragment_home) {
with(binding.manageApps) {
text1.text = getString(R.string.title_app_manage)
icon.setImageResource(R.drawable.outline_android_24)
// itemIcon.setCircleBackground(getColor(R.color.warn))
root.setOnClickListener {
navigate(R.id.nav_app_manage)
}
}
with(binding.manageTemplates) {
text1.text = getString(R.string.title_template_manage)
icon.setImageResource(R.drawable.ic_outline_layers_24)
// itemIcon.setCircleBackground(getColor(R.color.invalid))
root.setOnClickListener {
navigate(R.id.nav_template_manage)
}
}
with(binding.navLogs) {
text1.text = getString(R.string.title_logs)
icon.setImageResource(R.drawable.outline_assignment_24)
// itemIcon.setCircleBackground(getColor(R.color.error))
root.setOnClickListener {
navigate(R.id.nav_logs)
}

View file

@ -1,54 +0,0 @@
package org.frknkrc44.hma_oss.ui.fragment
import android.os.Bundle
import androidx.fragment.app.Fragment
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.lifecycle.lifecycleScope
import by.kirich1409.viewbindingdelegate.viewBinding
import icu.nullptr.hidemyapplist.service.ConfigManager
import icu.nullptr.hidemyapplist.ui.util.navController
import icu.nullptr.hidemyapplist.util.PackageHelper
import kotlinx.coroutines.launch
import org.frknkrc44.hma_oss.R
import org.frknkrc44.hma_oss.databinding.FragmentManageAppsBinding
import org.frknkrc44.hma_oss.ui.adapter.AppListAdapter
/**
* A simple [Fragment] subclass.
* Use the [ManageAppsFragment.newInstance] factory method to
* create an instance of this fragment.
*/
class ManageAppsFragment : Fragment(R.layout.fragment_manage_apps) {
protected val firstComparator: Comparator<String> = Comparator.comparing(ConfigManager::isHideEnabled).reversed()
private val binding by viewBinding<FragmentManageAppsBinding>()
val adapter: AppListAdapter
private var search = ""
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
}
protected open fun onBack() {
navController.navigateUp()
}
private fun applyFilter() {
adapter.filter.filter(search)
}
private fun sortList() {
lifecycleScope.launch {
PackageHelper.sortList(firstComparator)
applyFilter()
}
}
companion object {
@JvmStatic
fun newInstance(param1: String, param2: String) = ManageAppsFragment()
}
}

View file

@ -0,0 +1,56 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="?selectableItemBackground"
android:orientation="horizontal"
android:paddingHorizontal="@dimen/item_padding_horizontal"
android:paddingVertical="@dimen/item_padding_vertical"
tools:context="icu.nullptr.hidemyapplist.ui.view.AppItemView">
<ImageView
android:id="@+id/icon"
style="@style/AppTheme.AppIcon"
tools:ignore="ContentDescription"
tools:src="@tools:sample/avatars" />
<LinearLayout
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:orientation="vertical">
<com.google.android.material.textview.MaterialTextView
android:id="@+id/label"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:textAppearance="?textAppearanceBodyLarge"
tools:text="@tools:sample/full_names" />
<com.google.android.material.textview.MaterialTextView
android:id="@+id/enabled"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:textStyle="bold"
android:text="@string/enabled"
android:textColor="?colorPrimary"
android:visibility="gone"
tools:visibility="visible" />
<com.google.android.material.textview.MaterialTextView
android:id="@+id/package_name"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:fontFamily="monospace"
tools:text="com.example.app" />
</LinearLayout>
<com.google.android.material.checkbox.MaterialCheckBox
android:id="@+id/checkbox"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center_vertical"
android:clickable="false" />
</LinearLayout>

View file

@ -0,0 +1,42 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.coordinatorlayout.widget.CoordinatorLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:transitionGroup="true"
tools:context="icu.nullptr.hidemyapplist.ui.fragment.AppSelectFragment">
<com.google.android.material.appbar.AppBarLayout
android:id="@+id/app_bar"
android:fitsSystemWindows="true"
android:layout_width="match_parent"
android:layout_height="wrap_content">
<com.google.android.material.appbar.MaterialToolbar
android:id="@+id/toolbar"
android:layout_width="match_parent"
android:layout_height="?actionBarSize"
android:elevation="0dp" />
</com.google.android.material.appbar.AppBarLayout>
<LinearLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical"
app:layout_behavior="@string/appbar_scrolling_view_behavior">
<androidx.swiperefreshlayout.widget.SwipeRefreshLayout
android:id="@+id/swipe_refresh"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:clipToPadding="false"
android:scrollbars="vertical">
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/list"
android:layout_width="match_parent"
android:layout_height="match_parent" />
</androidx.swiperefreshlayout.widget.SwipeRefreshLayout>
</LinearLayout>
</androidx.coordinatorlayout.widget.CoordinatorLayout>

View file

@ -1,14 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".ui.fragment.ManageAppsFragment">
<!-- TODO: Update blank fragment layout -->
<TextView
android:layout_width="match_parent"
android:layout_height="match_parent"
android:text="@string/hello_blank_fragment" />
</FrameLayout>

Some files were not shown because too many files have changed in this diff Show more