diff --git a/README.md b/README.md index 0b38ad0..670e2cf 100644 --- a/README.md +++ b/README.md @@ -9,18 +9,18 @@ repository. ## What is this? An unofficial Android client for [Mealie](https://hay-kot.github.io/mealie/). It enables you to -easily access your recipes using via Android phone. The main goal is to have everything stored -locally to access it without Internet connection which is impossible with web site version. +easily access your recipes using an Android device. The main advantage over website is that +recipe data is stored locally and can be accessed without the Internet connection. ## Status -Current version is a very early alpha which supports a tiny portion of the Mealie capabilites. There -is a lot of work ahead, do not expect much. Having said that, you can list your recipes and read the -instructions/ingredients for each of them. +Current version is a very early alpha which supports a small subset of the Mealie capabilities. +It supports both API of older Mealie 0.5.6 and newer 1.0.0. Displays the list of recipes, +information about each of the recipes. Moreover, you can create a recipe from the app! ## Screenshots - + ## How to install @@ -28,6 +28,4 @@ Download the latest apk from the releases page. ## Contribution -Any contribution is greatly appreciated. Including translations, new issues, feature requests and -typo corrections. I won't specify any rules until I have to ask the same thing multiple times. Just -use the common sense. \ No newline at end of file +Any contribution is greatly appreciated: translations, bug reports, feature requests and any PR. \ No newline at end of file diff --git a/app/src/main/java/gq/kirmanak/mealient/App.kt b/app/src/main/java/gq/kirmanak/mealient/App.kt index 24332c9..fbd1c92 100644 --- a/app/src/main/java/gq/kirmanak/mealient/App.kt +++ b/app/src/main/java/gq/kirmanak/mealient/App.kt @@ -1,6 +1,7 @@ package gq.kirmanak.mealient import android.app.Application +import com.google.android.material.color.DynamicColors import dagger.hilt.android.HiltAndroidApp import gq.kirmanak.mealient.architecture.configuration.BuildConfiguration import gq.kirmanak.mealient.data.analytics.Analytics @@ -23,5 +24,6 @@ class App : Application() { super.onCreate() logger.v { "onCreate() called" } analytics.setIsEnabled(!buildConfiguration.isDebug()) + DynamicColors.applyToActivitiesIfAvailable(this) } } diff --git a/app/src/main/java/gq/kirmanak/mealient/extensions/FragmentExtensions.kt b/app/src/main/java/gq/kirmanak/mealient/extensions/FragmentExtensions.kt index f6ffa7c..0ab4993 100644 --- a/app/src/main/java/gq/kirmanak/mealient/extensions/FragmentExtensions.kt +++ b/app/src/main/java/gq/kirmanak/mealient/extensions/FragmentExtensions.kt @@ -2,19 +2,11 @@ package gq.kirmanak.mealient.extensions import androidx.annotation.StringRes import androidx.fragment.app.Fragment -import androidx.lifecycle.Lifecycle -import androidx.lifecycle.lifecycleScope -import androidx.lifecycle.repeatOnLifecycle import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.FlowCollector -import kotlinx.coroutines.launch fun Fragment.collectWhenViewResumed(flow: Flow, collector: FlowCollector) { - viewLifecycleOwner.lifecycleScope.launch { - viewLifecycleOwner.repeatOnLifecycle(Lifecycle.State.RESUMED) { - flow.collect(collector) - } - } + viewLifecycleOwner.collectWhenResumed(flow, collector) } fun Fragment.showLongToast(@StringRes text: Int) = context?.showLongToast(text) != null diff --git a/app/src/main/java/gq/kirmanak/mealient/extensions/ViewExtensions.kt b/app/src/main/java/gq/kirmanak/mealient/extensions/ViewExtensions.kt index 452b2c9..08ede24 100644 --- a/app/src/main/java/gq/kirmanak/mealient/extensions/ViewExtensions.kt +++ b/app/src/main/java/gq/kirmanak/mealient/extensions/ViewExtensions.kt @@ -2,6 +2,9 @@ package gq.kirmanak.mealient.extensions import android.content.Context import android.content.SharedPreferences +import android.content.res.Configuration.UI_MODE_NIGHT_MASK +import android.content.res.Configuration.UI_MODE_NIGHT_YES +import android.os.Build import android.view.View import android.view.inputmethod.InputMethodManager import android.widget.EditText @@ -10,10 +13,7 @@ import android.widget.Toast import androidx.annotation.StringRes import androidx.core.content.getSystemService import androidx.core.widget.doAfterTextChanged -import androidx.lifecycle.LifecycleOwner -import androidx.lifecycle.LiveData -import androidx.lifecycle.Observer -import androidx.lifecycle.lifecycleScope +import androidx.lifecycle.* import androidx.swiperefreshlayout.widget.SwipeRefreshLayout import com.google.android.material.textfield.TextInputLayout import gq.kirmanak.mealient.logging.Logger @@ -21,10 +21,7 @@ import kotlinx.coroutines.channels.ChannelResult import kotlinx.coroutines.channels.awaitClose import kotlinx.coroutines.channels.onClosed import kotlinx.coroutines.channels.onFailure -import kotlinx.coroutines.flow.Flow -import kotlinx.coroutines.flow.callbackFlow -import kotlinx.coroutines.flow.filterNotNull -import kotlinx.coroutines.flow.first +import kotlinx.coroutines.flow.* import kotlinx.coroutines.launch fun SwipeRefreshLayout.refreshRequestFlow(logger: Logger): Flow = callbackFlow { @@ -114,4 +111,19 @@ private fun Context.showToast(text: String, length: Int) { fun View.hideKeyboard() { val imm = context.getSystemService() imm?.hideSoftInputFromWindow(windowToken, 0) +} + +fun Context.isDarkThemeOn(): Boolean { + return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) + resources.configuration.isNightModeActive + else + resources.configuration.uiMode and UI_MODE_NIGHT_MASK == UI_MODE_NIGHT_YES +} + +fun LifecycleOwner.collectWhenResumed(flow: Flow, collector: FlowCollector) { + lifecycleScope.launch { + repeatOnLifecycle(Lifecycle.State.RESUMED) { + flow.collect(collector) + } + } } \ No newline at end of file diff --git a/app/src/main/java/gq/kirmanak/mealient/ui/activity/MainActivity.kt b/app/src/main/java/gq/kirmanak/mealient/ui/activity/MainActivity.kt index 63270ce..434c8ce 100644 --- a/app/src/main/java/gq/kirmanak/mealient/ui/activity/MainActivity.kt +++ b/app/src/main/java/gq/kirmanak/mealient/ui/activity/MainActivity.kt @@ -1,20 +1,18 @@ package gq.kirmanak.mealient.ui.activity import android.os.Bundle -import android.view.Menu import android.view.MenuItem import androidx.activity.viewModels import androidx.appcompat.app.AppCompatActivity -import androidx.appcompat.widget.SearchView -import androidx.appcompat.widget.SearchView.OnQueryTextListener import androidx.core.splashscreen.SplashScreen.Companion.installSplashScreen +import androidx.core.view.WindowInsetsControllerCompat import androidx.core.view.isVisible +import androidx.core.view.iterator +import androidx.drawerlayout.widget.DrawerLayout import androidx.navigation.NavController import androidx.navigation.NavDirections import androidx.navigation.fragment.NavHostFragment import by.kirich1409.viewbindingdelegate.viewBinding -import com.google.android.material.shape.CornerFamily -import com.google.android.material.shape.MaterialShapeDrawable import dagger.hilt.android.AndroidEntryPoint import gq.kirmanak.mealient.NavGraphDirections.Companion.actionGlobalAddRecipeFragment import gq.kirmanak.mealient.NavGraphDirections.Companion.actionGlobalAuthenticationFragment @@ -22,6 +20,8 @@ import gq.kirmanak.mealient.NavGraphDirections.Companion.actionGlobalBaseURLFrag import gq.kirmanak.mealient.NavGraphDirections.Companion.actionGlobalRecipesListFragment import gq.kirmanak.mealient.R import gq.kirmanak.mealient.databinding.MainActivityBinding +import gq.kirmanak.mealient.extensions.collectWhenResumed +import gq.kirmanak.mealient.extensions.isDarkThemeOn import gq.kirmanak.mealient.extensions.observeOnce import gq.kirmanak.mealient.logging.Logger import javax.inject.Inject @@ -31,8 +31,6 @@ class MainActivity : AppCompatActivity(R.layout.main_activity) { private val binding: MainActivityBinding by viewBinding(MainActivityBinding::bind, R.id.drawer) private val viewModel by viewModels() - private val title: String by lazy { getString(R.string.app_name) } - private val uiState: MainActivityUiState get() = viewModel.uiState private val navController: NavController get() = binding.navHost.getFragment().navController @@ -45,126 +43,98 @@ class MainActivity : AppCompatActivity(R.layout.main_activity) { logger.v { "onCreate() called with: savedInstanceState = $savedInstanceState" } splashScreen.setKeepOnScreenCondition { viewModel.startDestination.value == null } setContentView(binding.root) - configureToolbar() + setupUi() configureNavGraph() - viewModel.uiStateLive.observe(this, ::onUiStateChange) - binding.navigationView.setNavigationItemSelectedListener(::onNavigationItemSelected) } private fun configureNavGraph() { logger.v { "configureNavGraph() called" } viewModel.startDestination.observeOnce(this) { logger.d { "configureNavGraph: received destination" } - val graph = navController.navInflater.inflate(R.navigation.nav_graph) + val controller = navController + val graph = controller.navInflater.inflate(R.navigation.nav_graph) graph.setStartDestination(it) - navController.setGraph(graph, intent.extras) + controller.setGraph(graph, intent.extras) } } - private fun configureToolbar() { - setSupportActionBar(binding.toolbar) - binding.toolbar.setNavigationIcon(R.drawable.ic_toolbar) - binding.toolbar.setNavigationOnClickListener { binding.drawer.open() } - setToolbarRoundCorner() + private fun setupUi() { + binding.toolbar.setNavigationOnClickListener { + binding.toolbar.clearSearchFocus() + binding.drawer.open() + } + binding.toolbar.onSearchQueryChanged { query -> + viewModel.onSearchQuery(query.trim().takeUnless { it.isEmpty() }) + } + binding.navigationView.setNavigationItemSelectedListener(::onNavigationItemSelected) + with(WindowInsetsControllerCompat(window, window.decorView)) { + val isAppearanceLightBars = !isDarkThemeOn() + isAppearanceLightNavigationBars = isAppearanceLightBars + isAppearanceLightStatusBars = isAppearanceLightBars + } + viewModel.uiStateLive.observe(this, ::onUiStateChange) + collectWhenResumed(viewModel.clearSearchViewFocus) { + logger.d { "clearSearchViewFocus(): received event" } + binding.toolbar.clearSearchFocus() + } } private fun onNavigationItemSelected(menuItem: MenuItem): Boolean { logger.v { "onNavigationItemSelected() called with: menuItem = $menuItem" } - menuItem.isChecked = true + if (menuItem.isChecked) { + logger.d { "Not navigating because it is the current destination" } + binding.drawer.close() + return true + } val directions = when (menuItem.itemId) { R.id.add_recipe -> actionGlobalAddRecipeFragment() R.id.recipes_list -> actionGlobalRecipesListFragment() - R.id.change_url -> actionGlobalBaseURLFragment() + R.id.change_url -> actionGlobalBaseURLFragment(false) + R.id.login -> actionGlobalAuthenticationFragment() + R.id.logout -> { + viewModel.logout() + return true + } else -> throw IllegalArgumentException("Unknown menu item id: ${menuItem.itemId}") } + menuItem.isChecked = true navigateTo(directions) - binding.drawer.close() return true } private fun onUiStateChange(uiState: MainActivityUiState) { logger.v { "onUiStateChange() called with: uiState = $uiState" } - supportActionBar?.title = if (uiState.titleVisible) title else null - binding.navigationView.isVisible = uiState.navigationVisible - invalidateOptionsMenu() - } + for (menuItem in binding.navigationView.menu.iterator()) { + val itemId = menuItem.itemId + when (itemId) { + R.id.logout -> menuItem.isVisible = uiState.canShowLogout + R.id.login -> menuItem.isVisible = uiState.canShowLogin + } + menuItem.isChecked = itemId == uiState.checkedMenuItemId + } - private fun setToolbarRoundCorner() { - logger.v { "setToolbarRoundCorner() called" } - val drawables = listOf( - binding.toolbarHolder.background as? MaterialShapeDrawable, - binding.toolbar.background as? MaterialShapeDrawable, + binding.toolbar.isVisible = uiState.navigationVisible + binding.root.setDrawerLockMode( + if (uiState.navigationVisible) { + DrawerLayout.LOCK_MODE_UNLOCKED + } else { + DrawerLayout.LOCK_MODE_LOCKED_CLOSED + } ) - logger.d { "setToolbarRoundCorner: drawables = $drawables" } - val radius = resources.getDimension(R.dimen.main_activity_toolbar_corner_radius) - for (drawable in drawables) { - drawable?.apply { - shapeAppearanceModel = shapeAppearanceModel.toBuilder() - .setBottomLeftCorner(CornerFamily.ROUNDED, radius).build() - } + + binding.toolbar.isSearchVisible = uiState.searchVisible + + if (uiState.searchVisible) { + binding.toolbarHolder.setBackgroundResource(R.drawable.bg_toolbar) + } else { + binding.toolbarHolder.background = null } } - override fun onCreateOptionsMenu(menu: Menu): Boolean { - logger.v { "onCreateOptionsMenu() called with: menu = $menu" } - menuInflater.inflate(R.menu.main_toolbar, menu) - menu.findItem(R.id.logout).isVisible = uiState.canShowLogout - menu.findItem(R.id.login).isVisible = uiState.canShowLogin - val searchItem = menu.findItem(R.id.search_recipe_action) - searchItem.isVisible = uiState.searchVisible - setupSearchItem(searchItem) - return true - } - - private fun setupSearchItem(searchItem: MenuItem) { - logger.v { "setupSearchItem() called with: searchItem = $searchItem" } - val searchView = searchItem.actionView as? SearchView - if (searchView == null) { - logger.e { "setupSearchItem: search item's actionView is null or not SearchView" } - return - } - - searchView.queryHint = getString(R.string.search_recipes_hint) - searchView.isSubmitButtonEnabled = false - - searchView.setQuery(viewModel.lastSearchQuery, false) - searchView.isIconified = viewModel.lastSearchQuery.isNullOrBlank() - - searchView.setOnCloseListener { - logger.v { "onClose() called" } - viewModel.onSearchQuery(null) - false - } - - searchView.setOnQueryTextListener(object : OnQueryTextListener { - override fun onQueryTextSubmit(query: String?): Boolean = true - - override fun onQueryTextChange(newText: String?): Boolean { - logger.v { "onQueryTextChange() called with: newText = $newText" } - viewModel.onSearchQuery(newText?.trim()?.takeUnless { it.isEmpty() }) - return true - } - }) - } - - override fun onOptionsItemSelected(item: MenuItem): Boolean { - logger.v { "onOptionsItemSelected() called with: item = $item" } - val result = when (item.itemId) { - R.id.login -> { - navigateTo(actionGlobalAuthenticationFragment()) - true - } - R.id.logout -> { - viewModel.logout() - true - } - else -> super.onOptionsItemSelected(item) - } - return result - } - private fun navigateTo(directions: NavDirections) { logger.v { "navigateTo() called with: directions = $directions" } + binding.toolbarHolder.setExpanded(true) + binding.drawer.close() navController.navigate(directions) } } \ No newline at end of file diff --git a/app/src/main/java/gq/kirmanak/mealient/ui/activity/MainActivityUiState.kt b/app/src/main/java/gq/kirmanak/mealient/ui/activity/MainActivityUiState.kt index 3317d18..eda6f14 100644 --- a/app/src/main/java/gq/kirmanak/mealient/ui/activity/MainActivityUiState.kt +++ b/app/src/main/java/gq/kirmanak/mealient/ui/activity/MainActivityUiState.kt @@ -1,15 +1,14 @@ package gq.kirmanak.mealient.ui.activity +import androidx.annotation.IdRes + data class MainActivityUiState( - val loginButtonVisible: Boolean = false, - val titleVisible: Boolean = true, val isAuthorized: Boolean = false, val navigationVisible: Boolean = false, val searchVisible: Boolean = false, + @IdRes val checkedMenuItemId: Int? = null, ) { - val canShowLogin: Boolean - get() = !isAuthorized && loginButtonVisible + val canShowLogin: Boolean get() = !isAuthorized - val canShowLogout: Boolean - get() = isAuthorized && loginButtonVisible + val canShowLogout: Boolean get() = isAuthorized } diff --git a/app/src/main/java/gq/kirmanak/mealient/ui/activity/MainActivityViewModel.kt b/app/src/main/java/gq/kirmanak/mealient/ui/activity/MainActivityViewModel.kt index cb910bb..8022d30 100644 --- a/app/src/main/java/gq/kirmanak/mealient/ui/activity/MainActivityViewModel.kt +++ b/app/src/main/java/gq/kirmanak/mealient/ui/activity/MainActivityViewModel.kt @@ -8,8 +8,11 @@ import gq.kirmanak.mealient.data.baseurl.ServerInfoRepo import gq.kirmanak.mealient.data.disclaimer.DisclaimerStorage import gq.kirmanak.mealient.data.recipes.RecipeRepo import gq.kirmanak.mealient.logging.Logger +import kotlinx.coroutines.channels.Channel +import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.launchIn import kotlinx.coroutines.flow.onEach +import kotlinx.coroutines.flow.receiveAsFlow import kotlinx.coroutines.launch import javax.inject.Inject @@ -32,8 +35,8 @@ class MainActivityViewModel @Inject constructor( private val _startDestination = MutableLiveData() val startDestination: LiveData = _startDestination - var lastSearchQuery: String? = null - private set + private val _clearSearchViewFocusChannel = Channel() + val clearSearchViewFocus: Flow = _clearSearchViewFocusChannel.receiveAsFlow() init { authRepo.isAuthorizedFlow @@ -60,9 +63,11 @@ class MainActivityViewModel @Inject constructor( fun onSearchQuery(query: String?) { logger.v { "onSearchQuery() called with: query = $query" } - if (lastSearchQuery != query) { - lastSearchQuery = query - recipeRepo.updateNameQuery(query) - } + recipeRepo.updateNameQuery(query) + } + + fun clearSearchViewFocus() { + logger.v { "clearSearchViewFocus() called" } + _clearSearchViewFocusChannel.trySend(Unit) } } \ No newline at end of file diff --git a/app/src/main/java/gq/kirmanak/mealient/ui/activity/ToolbarView.kt b/app/src/main/java/gq/kirmanak/mealient/ui/activity/ToolbarView.kt new file mode 100644 index 0000000..71215e2 --- /dev/null +++ b/app/src/main/java/gq/kirmanak/mealient/ui/activity/ToolbarView.kt @@ -0,0 +1,47 @@ +package gq.kirmanak.mealient.ui.activity + +import android.content.Context +import android.util.AttributeSet +import android.view.LayoutInflater +import androidx.annotation.AttrRes +import androidx.annotation.StyleRes +import androidx.constraintlayout.widget.ConstraintLayout +import androidx.core.view.isVisible +import androidx.core.widget.doAfterTextChanged +import gq.kirmanak.mealient.databinding.ViewToolbarBinding +import gq.kirmanak.mealient.extensions.hideKeyboard + +class ToolbarView @JvmOverloads constructor( + context: Context, + attributeSet: AttributeSet? = null, + @AttrRes defStyleAttr: Int = 0, + @StyleRes defStyleRes: Int = 0, +) : ConstraintLayout(context, attributeSet, defStyleAttr, defStyleRes) { + + private lateinit var binding: ViewToolbarBinding + + var isSearchVisible: Boolean + get() = binding.searchEdit.isVisible + set(value) { + binding.searchEdit.isVisible = value + } + + override fun onFinishInflate() { + super.onFinishInflate() + val inflater = LayoutInflater.from(context) + binding = ViewToolbarBinding.inflate(inflater, this) + } + + fun setNavigationOnClickListener(listener: OnClickListener?) { + binding.navigationIcon.setOnClickListener(listener) + } + + fun onSearchQueryChanged(block: (String) -> Unit) { + binding.searchEdit.doAfterTextChanged { block(it.toString()) } + } + + fun clearSearchFocus() { + binding.searchEdit.clearFocus() + hideKeyboard() + } +} \ No newline at end of file diff --git a/app/src/main/java/gq/kirmanak/mealient/ui/add/AddRecipeFragment.kt b/app/src/main/java/gq/kirmanak/mealient/ui/add/AddRecipeFragment.kt index d311d68..c9cfeb0 100644 --- a/app/src/main/java/gq/kirmanak/mealient/ui/add/AddRecipeFragment.kt +++ b/app/src/main/java/gq/kirmanak/mealient/ui/add/AddRecipeFragment.kt @@ -39,10 +39,9 @@ class AddRecipeFragment : Fragment(R.layout.fragment_add_recipe) { logger.v { "onViewCreated() called with: view = $view, savedInstanceState = $savedInstanceState" } activityViewModel.updateUiState { it.copy( - loginButtonVisible = true, - titleVisible = false, navigationVisible = true, searchVisible = false, + checkedMenuItemId = R.id.add_recipe, ) } viewModel.loadPreservedRequest() diff --git a/app/src/main/java/gq/kirmanak/mealient/ui/auth/AuthenticationFragment.kt b/app/src/main/java/gq/kirmanak/mealient/ui/auth/AuthenticationFragment.kt index bd108f7..f28ae2d 100644 --- a/app/src/main/java/gq/kirmanak/mealient/ui/auth/AuthenticationFragment.kt +++ b/app/src/main/java/gq/kirmanak/mealient/ui/auth/AuthenticationFragment.kt @@ -32,12 +32,7 @@ class AuthenticationFragment : Fragment(R.layout.fragment_authentication) { logger.v { "onViewCreated() called with: view = $view, savedInstanceState = $savedInstanceState" } binding.button.setOnClickListener { onLoginClicked() } activityViewModel.updateUiState { - it.copy( - loginButtonVisible = false, - titleVisible = true, - navigationVisible = false, - searchVisible = false - ) + it.copy(navigationVisible = true, searchVisible = false, checkedMenuItemId = R.id.login) } viewModel.uiState.observe(viewLifecycleOwner, ::onUiStateChange) } @@ -66,7 +61,7 @@ class AuthenticationFragment : Fragment(R.layout.fragment_authentication) { private fun onUiStateChange(uiState: OperationUiState) = with(binding) { logger.v { "onUiStateChange() called with: authUiState = $uiState" } if (uiState.isSuccess) { - findNavController().popBackStack() + findNavController().navigateUp() return } diff --git a/app/src/main/java/gq/kirmanak/mealient/ui/baseurl/BaseURLFragment.kt b/app/src/main/java/gq/kirmanak/mealient/ui/baseurl/BaseURLFragment.kt index 31b1bfa..5313d56 100644 --- a/app/src/main/java/gq/kirmanak/mealient/ui/baseurl/BaseURLFragment.kt +++ b/app/src/main/java/gq/kirmanak/mealient/ui/baseurl/BaseURLFragment.kt @@ -6,6 +6,7 @@ import androidx.fragment.app.Fragment import androidx.fragment.app.activityViewModels import androidx.fragment.app.viewModels import androidx.navigation.fragment.findNavController +import androidx.navigation.fragment.navArgs import by.kirich1409.viewbindingdelegate.viewBinding import dagger.hilt.android.AndroidEntryPoint import gq.kirmanak.mealient.R @@ -24,6 +25,7 @@ class BaseURLFragment : Fragment(R.layout.fragment_base_url) { private val binding by viewBinding(FragmentBaseUrlBinding::bind) private val viewModel by viewModels() private val activityViewModel by activityViewModels() + private val args by navArgs() @Inject lateinit var logger: Logger @@ -35,10 +37,9 @@ class BaseURLFragment : Fragment(R.layout.fragment_base_url) { viewModel.uiState.observe(viewLifecycleOwner, ::onUiStateChange) activityViewModel.updateUiState { it.copy( - loginButtonVisible = false, - titleVisible = true, - navigationVisible = false, - searchVisible = false + navigationVisible = !args.isOnboarding, + searchVisible = false, + checkedMenuItemId = R.id.change_url, ) } } diff --git a/app/src/main/java/gq/kirmanak/mealient/ui/disclaimer/DisclaimerFragment.kt b/app/src/main/java/gq/kirmanak/mealient/ui/disclaimer/DisclaimerFragment.kt index bd024e4..d7db583 100644 --- a/app/src/main/java/gq/kirmanak/mealient/ui/disclaimer/DisclaimerFragment.kt +++ b/app/src/main/java/gq/kirmanak/mealient/ui/disclaimer/DisclaimerFragment.kt @@ -38,7 +38,7 @@ class DisclaimerFragment : Fragment(R.layout.fragment_disclaimer) { private fun navigateNext() { logger.v { "navigateNext() called" } - findNavController().navigate(actionDisclaimerFragmentToBaseURLFragment()) + findNavController().navigate(actionDisclaimerFragmentToBaseURLFragment(true)) } override fun onViewCreated(view: View, savedInstanceState: Bundle?) { @@ -58,12 +58,7 @@ class DisclaimerFragment : Fragment(R.layout.fragment_disclaimer) { } viewModel.startCountDown() activityViewModel.updateUiState { - it.copy( - loginButtonVisible = false, - titleVisible = true, - navigationVisible = false, - searchVisible = false - ) + it.copy(navigationVisible = false, searchVisible = false, checkedMenuItemId = null) } } } \ No newline at end of file diff --git a/app/src/main/java/gq/kirmanak/mealient/ui/recipes/RecipesListFragment.kt b/app/src/main/java/gq/kirmanak/mealient/ui/recipes/RecipesListFragment.kt index 4752876..79d1452 100644 --- a/app/src/main/java/gq/kirmanak/mealient/ui/recipes/RecipesListFragment.kt +++ b/app/src/main/java/gq/kirmanak/mealient/ui/recipes/RecipesListFragment.kt @@ -50,10 +50,9 @@ class RecipesListFragment : Fragment(R.layout.fragment_recipes_list) { logger.v { "onViewCreated() called with: view = $view, savedInstanceState = $savedInstanceState" } activityViewModel.updateUiState { it.copy( - loginButtonVisible = true, - titleVisible = false, navigationVisible = true, searchVisible = true, + checkedMenuItemId = R.id.recipes_list ) } setupRecipeAdapter() @@ -62,8 +61,8 @@ class RecipesListFragment : Fragment(R.layout.fragment_recipes_list) { @SuppressLint("ClickableViewAccessibility") private fun hideKeyboardOnScroll() { - binding.recipes.setOnTouchListener { view, _ -> - view?.hideKeyboard() + binding.recipes.setOnTouchListener { _, _ -> + activityViewModel.clearSearchViewFocus() false } } @@ -85,9 +84,7 @@ class RecipesListFragment : Fragment(R.layout.fragment_recipes_list) { private fun isNavigatingSomewhere(): Boolean { logger.v { "isNavigatingSomewhere() called" } - val label = findNavController().currentDestination?.label - logger.d { "isNavigatingSomewhere: current destination is $label" } - return label != "fragment_recipes" + return findNavController().currentDestination?.id != R.id.recipesListFragment } private fun setupRecipeAdapter() { @@ -158,18 +155,12 @@ private fun Throwable.toLoadErrorReasonText(): Int? = when (this) { } private fun PagingDataAdapter.refreshErrors(): Flow { - return loadStateFlow - .map { it.refresh } - .valueUpdatesOnly() - .filterIsInstance() + return loadStateFlow.map { it.refresh }.valueUpdatesOnly().filterIsInstance() .map { it.error } } private fun PagingDataAdapter.appendPaginationEnd(): Flow { - return loadStateFlow - .map { it.append.endOfPaginationReached } - .valueUpdatesOnly() - .filter { it } + return loadStateFlow.map { it.append.endOfPaginationReached }.valueUpdatesOnly().filter { it } .map { } } diff --git a/app/src/main/java/gq/kirmanak/mealient/ui/recipes/info/RecipeInfoFragment.kt b/app/src/main/java/gq/kirmanak/mealient/ui/recipes/info/RecipeInfoFragment.kt index 92868be..aac4ad7 100644 --- a/app/src/main/java/gq/kirmanak/mealient/ui/recipes/info/RecipeInfoFragment.kt +++ b/app/src/main/java/gq/kirmanak/mealient/ui/recipes/info/RecipeInfoFragment.kt @@ -1,6 +1,5 @@ package gq.kirmanak.mealient.ui.recipes.info -import android.app.Dialog import android.os.Bundle import android.view.LayoutInflater import android.view.View @@ -8,10 +7,8 @@ import android.view.ViewGroup import androidx.core.view.isVisible import androidx.fragment.app.viewModels import by.kirich1409.viewbindingdelegate.viewBinding -import com.google.android.material.bottomsheet.BottomSheetDialog import com.google.android.material.bottomsheet.BottomSheetDialogFragment import dagger.hilt.android.AndroidEntryPoint -import gq.kirmanak.mealient.R import gq.kirmanak.mealient.databinding.FragmentRecipeInfoBinding import gq.kirmanak.mealient.logging.Logger import gq.kirmanak.mealient.ui.recipes.images.RecipeImageLoader @@ -71,9 +68,6 @@ class RecipeInfoFragment : BottomSheetDialogFragment() { instructionsAdapter.submitList(uiState.recipeInstructions) } - override fun onCreateDialog(savedInstanceState: Bundle?): Dialog = - BottomSheetDialog(requireContext(), R.style.NoShapeBottomSheetDialog) - override fun onDestroyView() { super.onDestroyView() logger.v { "onDestroyView() called" } diff --git a/app/src/main/res/drawable-night/ic_splash_screen_background.xml b/app/src/main/res/drawable-night/ic_splash_screen_background.xml new file mode 100644 index 0000000..ce6d767 --- /dev/null +++ b/app/src/main/res/drawable-night/ic_splash_screen_background.xml @@ -0,0 +1,13 @@ + + + + + diff --git a/app/src/main/res/drawable-night/ic_splash_screen_foreground.xml b/app/src/main/res/drawable-night/ic_splash_screen_foreground.xml new file mode 100644 index 0000000..575a54f --- /dev/null +++ b/app/src/main/res/drawable-night/ic_splash_screen_foreground.xml @@ -0,0 +1,18 @@ + + + + + + diff --git a/app/src/main/res/drawable/bg_toolbar.xml b/app/src/main/res/drawable/bg_toolbar.xml new file mode 100644 index 0000000..b123a8a --- /dev/null +++ b/app/src/main/res/drawable/bg_toolbar.xml @@ -0,0 +1,7 @@ + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/ic_add.xml b/app/src/main/res/drawable/ic_add.xml new file mode 100644 index 0000000..f2bce58 --- /dev/null +++ b/app/src/main/res/drawable/ic_add.xml @@ -0,0 +1,10 @@ + + + diff --git a/app/src/main/res/drawable/ic_change.xml b/app/src/main/res/drawable/ic_change.xml new file mode 100644 index 0000000..4039c66 --- /dev/null +++ b/app/src/main/res/drawable/ic_change.xml @@ -0,0 +1,10 @@ + + + diff --git a/app/src/main/res/drawable/ic_launcher_background.xml b/app/src/main/res/drawable/ic_launcher_background.xml index bb1400d..edd4c77 100644 --- a/app/src/main/res/drawable/ic_launcher_background.xml +++ b/app/src/main/res/drawable/ic_launcher_background.xml @@ -1,31 +1,13 @@ - - - - - - - - - - - + android:width="108dp" + android:height="108dp" + android:viewportWidth="108" + android:viewportHeight="108"> + + + diff --git a/app/src/main/res/drawable/ic_launcher_foreground.xml b/app/src/main/res/drawable/ic_launcher_foreground.xml index a135f3f..89108aa 100644 --- a/app/src/main/res/drawable/ic_launcher_foreground.xml +++ b/app/src/main/res/drawable/ic_launcher_foreground.xml @@ -1,35 +1,18 @@ - - - - - - + android:width="108dp" + android:height="108dp" + android:viewportWidth="108" + android:viewportHeight="108"> + + + + diff --git a/app/src/main/res/drawable/ic_launcher_monochrome.xml b/app/src/main/res/drawable/ic_launcher_monochrome.xml new file mode 100644 index 0000000..424afd8 --- /dev/null +++ b/app/src/main/res/drawable/ic_launcher_monochrome.xml @@ -0,0 +1,15 @@ + + + + + + diff --git a/app/src/main/res/drawable/ic_list.xml b/app/src/main/res/drawable/ic_list.xml new file mode 100644 index 0000000..69c90d2 --- /dev/null +++ b/app/src/main/res/drawable/ic_list.xml @@ -0,0 +1,11 @@ + + + diff --git a/app/src/main/res/drawable/ic_login.xml b/app/src/main/res/drawable/ic_login.xml new file mode 100644 index 0000000..9c9dedc --- /dev/null +++ b/app/src/main/res/drawable/ic_login.xml @@ -0,0 +1,11 @@ + + + diff --git a/app/src/main/res/drawable/ic_logout.xml b/app/src/main/res/drawable/ic_logout.xml new file mode 100644 index 0000000..46542bc --- /dev/null +++ b/app/src/main/res/drawable/ic_logout.xml @@ -0,0 +1,11 @@ + + + diff --git a/app/src/main/res/drawable/ic_menu.xml b/app/src/main/res/drawable/ic_menu.xml new file mode 100644 index 0000000..d632557 --- /dev/null +++ b/app/src/main/res/drawable/ic_menu.xml @@ -0,0 +1,10 @@ + + + diff --git a/app/src/main/res/drawable/ic_splash_screen_background.xml b/app/src/main/res/drawable/ic_splash_screen_background.xml index dcd8800..6f25d0f 100644 --- a/app/src/main/res/drawable/ic_splash_screen_background.xml +++ b/app/src/main/res/drawable/ic_splash_screen_background.xml @@ -1,31 +1,13 @@ - - - - - - - - - + android:scaleX="7.1" + android:scaleY="7.1"> + diff --git a/app/src/main/res/drawable/ic_splash_screen_foreground.xml b/app/src/main/res/drawable/ic_splash_screen_foreground.xml index 98a9788..a8efbdb 100644 --- a/app/src/main/res/drawable/ic_splash_screen_foreground.xml +++ b/app/src/main/res/drawable/ic_splash_screen_foreground.xml @@ -1,35 +1,18 @@ - - - - - - + android:width="288dp" + android:height="288dp" + android:viewportWidth="288" + android:viewportHeight="288"> + + + + diff --git a/app/src/main/res/drawable/ic_toolbar.xml b/app/src/main/res/drawable/ic_toolbar.xml deleted file mode 100644 index 7c9f16f..0000000 --- a/app/src/main/res/drawable/ic_toolbar.xml +++ /dev/null @@ -1,35 +0,0 @@ - - - - - - - - diff --git a/app/src/main/res/drawable/placeholder_recipe.xml b/app/src/main/res/drawable/placeholder_recipe.xml index 3097227..5d47c6b 100644 --- a/app/src/main/res/drawable/placeholder_recipe.xml +++ b/app/src/main/res/drawable/placeholder_recipe.xml @@ -1,19 +1,19 @@ - - - - + android:width="200dp" + android:height="122dp" + android:viewportWidth="300" + android:viewportHeight="182"> + + + + diff --git a/app/src/main/res/drawable/recipe_info_background.xml b/app/src/main/res/drawable/recipe_info_background.xml deleted file mode 100644 index a700ba3..0000000 --- a/app/src/main/res/drawable/recipe_info_background.xml +++ /dev/null @@ -1,8 +0,0 @@ - - - - - \ No newline at end of file diff --git a/app/src/main/res/layout/fragment_disclaimer.xml b/app/src/main/res/layout/fragment_disclaimer.xml index 15a2098..14267c2 100644 --- a/app/src/main/res/layout/fragment_disclaimer.xml +++ b/app/src/main/res/layout/fragment_disclaimer.xml @@ -2,6 +2,7 @@ @@ -12,12 +13,10 @@ android:layout_height="wrap_content" android:layout_marginHorizontal="20dp" android:layout_marginTop="40dp" - app:cardElevation="8dp" - app:layout_constraintTop_toTopOf="parent" app:layout_constraintBottom_toTopOf="@+id/okay" app:layout_constraintEnd_toEndOf="parent" app:layout_constraintStart_toStartOf="parent" - app:shapeAppearance="@style/ShapeAppearance.AllCornersRounded"> + app:layout_constraintTop_toTopOf="parent"> + app:layout_constraintTop_toBottomOf="@+id/main_text_holder" + tools:text="Okay (3 seconds)" /> \ No newline at end of file diff --git a/app/src/main/res/layout/fragment_recipe_info.xml b/app/src/main/res/layout/fragment_recipe_info.xml index e564fa7..7f5e8b8 100644 --- a/app/src/main/res/layout/fragment_recipe_info.xml +++ b/app/src/main/res/layout/fragment_recipe_info.xml @@ -33,22 +33,23 @@ @@ -102,7 +102,7 @@ android:id="@+id/ingredients_list" android:layout_width="match_parent" android:layout_height="wrap_content" - android:layout_marginHorizontal="10dp" + android:layout_marginHorizontal="@dimen/margin_small" app:layoutManager="androidx.recyclerview.widget.LinearLayoutManager" tools:itemCount="3" tools:listitem="@layout/view_holder_ingredient" /> @@ -121,7 +121,7 @@ android:layout_width="0dp" android:layout_height="wrap_content" android:layout_marginStart="35dp" - android:layout_marginEnd="8dp" + android:layout_marginEnd="@dimen/margin_small" android:text="@string/fragment_recipe_info_instructions_header" android:textAppearance="?textAppearanceHeadline6" app:layout_constraintBottom_toTopOf="@+id/instructions_list" @@ -133,11 +133,11 @@ android:id="@+id/instructions_list" android:layout_width="0dp" android:layout_height="wrap_content" + app:layoutManager="androidx.recyclerview.widget.LinearLayoutManager" app:layout_constraintBottom_toBottomOf="parent" app:layout_constraintEnd_toStartOf="@+id/end_guide" - app:layoutManager="androidx.recyclerview.widget.LinearLayoutManager" - app:layout_constraintTop_toBottomOf="@+id/instructions_header" app:layout_constraintStart_toEndOf="@+id/start_guide" + app:layout_constraintTop_toBottomOf="@+id/instructions_header" tools:itemCount="2" tools:listitem="@layout/view_holder_instruction" /> diff --git a/app/src/main/res/layout/main_activity.xml b/app/src/main/res/layout/main_activity.xml index 2706507..684e7ba 100644 --- a/app/src/main/res/layout/main_activity.xml +++ b/app/src/main/res/layout/main_activity.xml @@ -3,6 +3,7 @@ xmlns:app="http://schemas.android.com/apk/res-auto" xmlns:tools="http://schemas.android.com/tools" android:id="@+id/drawer" + style="?drawerLayoutStyle" android:layout_width="match_parent" android:layout_height="match_parent" tools:context=".ui.activity.MainActivity" @@ -10,21 +11,20 @@ + android:layout_height="match_parent"> - @@ -33,6 +33,7 @@ android:name="androidx.navigation.fragment.NavHostFragment" android:layout_width="match_parent" android:layout_height="match_parent" + android:layout_marginTop="@dimen/margin_small" app:defaultNavHost="true" app:layout_behavior="@string/appbar_scrolling_view_behavior" /> @@ -42,5 +43,6 @@ android:layout_width="wrap_content" android:layout_height="match_parent" android:layout_gravity="start" + app:headerLayout="@layout/view_navigation_drawer_header" app:menu="@menu/navigation_menu" /> diff --git a/app/src/main/res/layout/view_holder_instruction.xml b/app/src/main/res/layout/view_holder_instruction.xml index cee4dda..e633e30 100644 --- a/app/src/main/res/layout/view_holder_instruction.xml +++ b/app/src/main/res/layout/view_holder_instruction.xml @@ -2,11 +2,10 @@ + android:layout_height="wrap_content" + android:layout_margin="@dimen/margin_small"> + android:layout_marginVertical="@dimen/margin_small" + android:layout_marginStart="@dimen/margin_medium" + android:layout_marginEnd="@dimen/margin_medium"> \ No newline at end of file diff --git a/app/src/main/res/layout/view_navigation_drawer_header.xml b/app/src/main/res/layout/view_navigation_drawer_header.xml new file mode 100644 index 0000000..cf380d8 --- /dev/null +++ b/app/src/main/res/layout/view_navigation_drawer_header.xml @@ -0,0 +1,19 @@ + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/view_toolbar.xml b/app/src/main/res/layout/view_toolbar.xml new file mode 100644 index 0000000..dbb6bc7 --- /dev/null +++ b/app/src/main/res/layout/view_toolbar.xml @@ -0,0 +1,36 @@ + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/menu/main_toolbar.xml b/app/src/main/res/menu/main_toolbar.xml index 242a16e..1f07637 100644 --- a/app/src/main/res/menu/main_toolbar.xml +++ b/app/src/main/res/menu/main_toolbar.xml @@ -2,23 +2,10 @@ - - - - + app:showAsAction="always" /> \ No newline at end of file diff --git a/app/src/main/res/menu/navigation_menu.xml b/app/src/main/res/menu/navigation_menu.xml index c5b3ea1..e896a64 100644 --- a/app/src/main/res/menu/navigation_menu.xml +++ b/app/src/main/res/menu/navigation_menu.xml @@ -2,13 +2,31 @@ + android:checkable="true" + android:icon="@drawable/ic_list" + android:title="@string/menu_navigation_drawer_recipes_list" /> + android:checkable="true" + android:icon="@drawable/ic_add" + android:title="@string/menu_navigation_drawer_add_recipe" /> + android:checkable="true" + android:icon="@drawable/ic_change" + android:title="@string/menu_navigation_drawer_change_url" /> + + + + + \ No newline at end of file diff --git a/app/src/main/res/mipmap/ic_launcher.xml b/app/src/main/res/mipmap/ic_launcher.xml index eca70cf..b070c76 100644 --- a/app/src/main/res/mipmap/ic_launcher.xml +++ b/app/src/main/res/mipmap/ic_launcher.xml @@ -2,4 +2,5 @@ + \ No newline at end of file diff --git a/app/src/main/res/navigation/nav_graph.xml b/app/src/main/res/navigation/nav_graph.xml index cb5ca08..39e13e5 100644 --- a/app/src/main/res/navigation/nav_graph.xml +++ b/app/src/main/res/navigation/nav_graph.xml @@ -51,8 +51,10 @@ + app:popUpTo="@id/nav_graph" /> + + app:destination="@id/authenticationFragment" /> + app:destination="@id/recipesListFragment" + app:popUpTo="@id/nav_graph" /> - \ No newline at end of file diff --git a/app/src/main/res/values-ru/strings.xml b/app/src/main/res/values-ru/strings.xml index fea499a..36e0879 100644 --- a/app/src/main/res/values-ru/strings.xml +++ b/app/src/main/res/values-ru/strings.xml @@ -5,7 +5,7 @@ URL сервера Войти Изображение готового блюда - Выйти + Выйти Загрузка Ингредиенты Инструкции @@ -21,11 +21,11 @@ Что-то пошло не так, попробуйте еще раз. Проверьте формат URL: %s Продолжить - Войти + Войти Название рецепта Описание - Рецепты - Добавить рецепт + Рецепты + Добавить рецепт Количество порций Сохранить рецепт Добавить шаг @@ -47,6 +47,7 @@ неожиданный ответ нет соединения Ошибка загрузки. - Сменить URL + Сменить URL Найти рецепты + Открыть меню навигации \ No newline at end of file diff --git a/app/src/main/res/values/colors.xml b/app/src/main/res/values/colors.xml deleted file mode 100644 index 904f3be..0000000 --- a/app/src/main/res/values/colors.xml +++ /dev/null @@ -1,58 +0,0 @@ - - #7743B5 - #FFFFFF - #EFDBFF - #290054 - #655A70 - #FFFFFF - #ECDDF7 - #201829 - #805159 - #FFFFFF - #FFD9DF - #321118 - #BA1B1B - #FFDAD4 - #FFFFFF - #410001 - #FFFBFC - #1D1B1E - #FFFBFC - #1D1B1E - #E8DFEB - #4A454E - #7B757E - #F5EFF3 - #322F33 - #DBB8FF - #000000 - #DBB8FF - #DBB8FF - #460283 - #5E289B - #EFDBFF - #CFC1DA - #362D40 - #4D4357 - #ECDDF7 - #F2B7C0 - #4B252C - #653A42 - #FFD9DF - #FFB4A9 - #930006 - #680003 - #FFDAD4 - #1D1B1E - #E7E1E5 - #1D1B1E - #E7E1E5 - #4A454E - #CCC4CF - #958E98 - #1D1B1E - #E7E1E5 - #7743B5 - #000000 - #7743B5 - \ No newline at end of file diff --git a/app/src/main/res/values/dimens.xml b/app/src/main/res/values/dimens.xml index 01d1f49..3b149f6 100644 --- a/app/src/main/res/values/dimens.xml +++ b/app/src/main/res/values/dimens.xml @@ -1,8 +1,5 @@ 8dp - 182dp - @dimen/height_view_holder_recipe_image - 32dp - 15dp + 16dp \ No newline at end of file diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 457b90e..6c8778f 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -5,8 +5,7 @@ Server URL Login Picture of the cooked meal - @string/menu_main_toolbar_logout - Logout + Logout Loading… @string/content_description_view_holder_recipe_image Ingredients @@ -18,8 +17,7 @@ Check URL format: %s Proceed @string/fragment_authentication_unknown_error - @string/menu_main_toolbar_login - Login + Login Okay Step: %d E-mail can\'t be empty @@ -28,8 +26,8 @@ Something went wrong, please try again. Recipe name Description - Add recipe - Recipes + Add recipe + Recipes Recipe yield Save recipe New step @@ -51,6 +49,8 @@ unauthorized unexpected response no connection - Change URL + Change URL Search recipes + @string/app_name + Open navigation drawer \ No newline at end of file diff --git a/app/src/main/res/values/styles.xml b/app/src/main/res/values/styles.xml index 2add8a8..3e79bdf 100644 --- a/app/src/main/res/values/styles.xml +++ b/app/src/main/res/values/styles.xml @@ -13,22 +13,6 @@ @dimen/margin_small - - - - - - - - - - - \ No newline at end of file +