diff --git a/app/src/main/java/com/majinnaibu/monstercards/ui/editmonster/EditLanguageFragment.java b/app/src/main/java/com/majinnaibu/monstercards/ui/editmonster/EditLanguageFragment.java new file mode 100644 index 0000000..3039241 --- /dev/null +++ b/app/src/main/java/com/majinnaibu/monstercards/ui/editmonster/EditLanguageFragment.java @@ -0,0 +1,85 @@ +package com.majinnaibu.monstercards.ui.editmonster; + +import android.os.Bundle; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.widget.CheckBox; +import android.widget.EditText; + +import androidx.activity.OnBackPressedCallback; +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import androidx.lifecycle.ViewModelProvider; +import androidx.navigation.NavBackStackEntry; +import androidx.navigation.NavController; +import androidx.navigation.Navigation; + +import com.majinnaibu.monstercards.R; +import com.majinnaibu.monstercards.models.Language; +import com.majinnaibu.monstercards.ui.shared.MCFragment; +import com.majinnaibu.monstercards.utils.Logger; +import com.majinnaibu.monstercards.utils.TextChangedListener; + +public class EditLanguageFragment extends MCFragment { + private EditMonsterViewModel mEditMonsterViewModel; + private EditLanguageViewModel mViewModel; + private ViewHolder mHolder; + private Language mOldLanguage; + + @Override + public void onCreate(@Nullable Bundle savedInstanceState) { + mViewModel = new ViewModelProvider(this).get(EditLanguageViewModel.class); + Bundle arguments = getArguments(); + if (arguments != null) { + EditLanguageFragmentArgs args = EditLanguageFragmentArgs.fromBundle(arguments); + mOldLanguage = new Language(args.getName(), args.getCanSpeak()); + mViewModel.copyFromLanguage(mOldLanguage); + } else { + Logger.logWTF("EditLanguageFragment needs arguments"); + mOldLanguage = null; + } + super.onCreate(savedInstanceState); + } + + @Nullable + @Override + public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container, @Nullable @org.jetbrains.annotations.Nullable Bundle savedInstanceState) { + NavController navController = Navigation.findNavController(requireActivity(), R.id.nav_host_fragment); + NavBackStackEntry backStackEntry = navController.getBackStackEntry(R.id.edit_monster_navigation); + mEditMonsterViewModel = new ViewModelProvider(backStackEntry).get(EditMonsterViewModel.class); + + View root = inflater.inflate(R.layout.fragment_edit_language, container, false); + + + mHolder = new ViewHolder(root); + + mHolder.name.setText(mViewModel.getName().getValue()); + mHolder.name.addTextChangedListener(new TextChangedListener((TextChangedListener.OnTextChangedCallback) (s, start, before, count) -> mViewModel.setName(s.toString()))); + + mHolder.canSpeak.setChecked(mViewModel.getCanSpeakValue()); + mHolder.canSpeak.setOnCheckedChangeListener((buttonView, isChecked) -> mViewModel.setCanSpeak(isChecked)); + + requireActivity().getOnBackPressedDispatcher().addCallback(getViewLifecycleOwner(), new OnBackPressedCallback(true) { + @Override + public void handleOnBackPressed() { + if (mViewModel.hasChanges()) { + mEditMonsterViewModel.replaceLanguage(mOldLanguage, mViewModel.getLanguage().getValue()); + } + Navigation.findNavController(requireView()).navigateUp(); + } + }); + + return root; + } + + private static class ViewHolder { + EditText name; + CheckBox canSpeak; + + ViewHolder(View root) { + name = root.findViewById(R.id.name); + canSpeak = root.findViewById(R.id.canSpeak); + } + } +} diff --git a/app/src/main/java/com/majinnaibu/monstercards/ui/editmonster/EditLanguageViewModel.java b/app/src/main/java/com/majinnaibu/monstercards/ui/editmonster/EditLanguageViewModel.java new file mode 100644 index 0000000..4a8632e --- /dev/null +++ b/app/src/main/java/com/majinnaibu/monstercards/ui/editmonster/EditLanguageViewModel.java @@ -0,0 +1,66 @@ +package com.majinnaibu.monstercards.ui.editmonster; + +import androidx.lifecycle.LiveData; + +import com.majinnaibu.monstercards.models.Language; +import com.majinnaibu.monstercards.ui.shared.ChangeTrackedViewModel; +import com.majinnaibu.monstercards.utils.ChangeTrackedLiveData; + +public class EditLanguageViewModel extends ChangeTrackedViewModel { + private final ChangeTrackedLiveData mName; + private final ChangeTrackedLiveData mCanSpeak; + private final ChangeTrackedLiveData mLanguage; + + public EditLanguageViewModel() { + super(); + mName = new ChangeTrackedLiveData<>("New Language", this::makeDirty); + mCanSpeak = new ChangeTrackedLiveData<>(true, this::makeDirty); + mLanguage = new ChangeTrackedLiveData<>(makeLanguage(), this::makeDirty); + } + + public void copyFromLanguage(Language language) { + mName.resetValue(language.getName()); + mCanSpeak.resetValue(language.getSpeaks()); + makeClean(); + } + + public LiveData getLanguage() { + return mLanguage; + } + + public LiveData getName() { + return mName; + } + + public void setName(String name) { + mName.setValue(name); + mLanguage.setValue(makeLanguage()); + } + + public LiveData getCanSpeak() { + return mCanSpeak; + } + + public void setCanSpeak(boolean canSpeak) { + mCanSpeak.setValue(canSpeak); + mLanguage.setValue(makeLanguage()); + } + + public boolean getCanSpeakValue(boolean defaultIfNull) { + Boolean boxedValue = mCanSpeak.getValue(); + if (boxedValue == null) { + return defaultIfNull; + } + return boxedValue; + } + + public boolean getCanSpeakValue() { + return getCanSpeakValue(false); + } + + private Language makeLanguage() { + Boolean boxedValue = mCanSpeak.getValue(); + boolean canSpeak = boxedValue != null && boxedValue; + return new Language(mName.getValue(), canSpeak); + } +} \ No newline at end of file diff --git a/app/src/main/java/com/majinnaibu/monstercards/ui/editmonster/EditLanguagesFragment.java b/app/src/main/java/com/majinnaibu/monstercards/ui/editmonster/EditLanguagesFragment.java new file mode 100644 index 0000000..53740f9 --- /dev/null +++ b/app/src/main/java/com/majinnaibu/monstercards/ui/editmonster/EditLanguagesFragment.java @@ -0,0 +1,98 @@ +package com.majinnaibu.monstercards.ui.editmonster; + +import android.content.Context; +import android.os.Bundle; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; + +import androidx.annotation.NonNull; +import androidx.lifecycle.ViewModelProvider; +import androidx.navigation.NavBackStackEntry; +import androidx.navigation.NavController; +import androidx.navigation.NavDirections; +import androidx.navigation.Navigation; +import androidx.recyclerview.widget.DividerItemDecoration; +import androidx.recyclerview.widget.ItemTouchHelper; +import androidx.recyclerview.widget.LinearLayoutManager; +import androidx.recyclerview.widget.RecyclerView; + +import com.google.android.material.floatingactionbutton.FloatingActionButton; +import com.majinnaibu.monstercards.R; +import com.majinnaibu.monstercards.models.Language; +import com.majinnaibu.monstercards.ui.shared.MCFragment; +import com.majinnaibu.monstercards.ui.shared.SwipeToDeleteCallback; +import com.majinnaibu.monstercards.utils.Logger; +import com.majinnaibu.monstercards.utils.TextChangedListener; + +public class EditLanguagesFragment extends MCFragment { + private EditMonsterViewModel mViewModel; + private ViewHolder mHolder; + + private void navigateToEditLanguage(Language language) { + NavDirections action = EditLanguagesFragmentDirections.actionEditLanguagesFragmentToEditLanguageFragment(language.getName(), language.getSpeaks()); + Navigation.findNavController(requireView()).navigate(action); + } + + @Override + public View onCreateView(LayoutInflater inflater, ViewGroup container, + Bundle savedInstanceState) { + NavController navController = Navigation.findNavController(requireActivity(), R.id.nav_host_fragment); + NavBackStackEntry backStackEntry = navController.getBackStackEntry(R.id.edit_monster_navigation); + mViewModel = new ViewModelProvider(backStackEntry).get(EditMonsterViewModel.class); + + View root = inflater.inflate(R.layout.fragment_edit_languages_list, container, false); + + mHolder = new ViewHolder(root); + setupRecyclerView(mHolder.list); + setupAddLanguageButton(mHolder.addLanguage); + + return root; + } + + private void setupRecyclerView(@NonNull RecyclerView recyclerView) { + Context context = requireContext(); + LinearLayoutManager layoutManager = new LinearLayoutManager(context); + recyclerView.setLayoutManager(layoutManager); + + mViewModel.getLanguages().observe(getViewLifecycleOwner(), languages -> { + EditLanguagesRecyclerViewAdapter adapter = new EditLanguagesRecyclerViewAdapter( + mViewModel.getLanguagesArray(), + language -> { + if (language != null) { + navigateToEditLanguage(language); + } else { + Logger.logError("Can't navigate to EditSkill with a null skill"); + } + }, + mViewModel.getTelepathyRangeUnboxed(), + (value, previousValue) -> mViewModel.setTelepathyRange(value), + mViewModel.getUnderstandsButDescription().getValue(), + new TextChangedListener((TextChangedListener.OnTextChangedCallback) (s, start, before, count) -> mViewModel.setUnderstandsButDescription(s.toString()))); + recyclerView.setAdapter(adapter); + }); + + DividerItemDecoration dividerItemDecoration = new DividerItemDecoration(context, layoutManager.getOrientation()); + recyclerView.addItemDecoration(dividerItemDecoration); + + ItemTouchHelper itemTouchHelper = new ItemTouchHelper(new SwipeToDeleteCallback(context, mViewModel::removeLanguage)); + itemTouchHelper.attachToRecyclerView(recyclerView); + } + + private void setupAddLanguageButton(@NonNull FloatingActionButton fab) { + fab.setOnClickListener(view -> { + Language newLanguage = mViewModel.addNewLanguage(); + navigateToEditLanguage(newLanguage); + }); + } + + private static class ViewHolder { + RecyclerView list; + FloatingActionButton addLanguage; + + ViewHolder(View root) { + this.list = root.findViewById(R.id.list); + this.addLanguage = root.findViewById(R.id.add_language); + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/majinnaibu/monstercards/ui/editmonster/EditLanguagesRecyclerViewAdapter.java b/app/src/main/java/com/majinnaibu/monstercards/ui/editmonster/EditLanguagesRecyclerViewAdapter.java new file mode 100644 index 0000000..8a98b14 --- /dev/null +++ b/app/src/main/java/com/majinnaibu/monstercards/ui/editmonster/EditLanguagesRecyclerViewAdapter.java @@ -0,0 +1,114 @@ +package com.majinnaibu.monstercards.ui.editmonster; + +import android.text.TextWatcher; +import android.view.LayoutInflater; +import android.view.ViewGroup; +import android.widget.EditText; +import android.widget.TextView; + +import androidx.recyclerview.widget.RecyclerView; + +import com.majinnaibu.monstercards.databinding.FragmentEditLanguagesListHeaderBinding; +import com.majinnaibu.monstercards.databinding.FragmentEditLanguagesListItemBinding; +import com.majinnaibu.monstercards.models.Language; +import com.majinnaibu.monstercards.ui.components.Stepper; + +import org.jetbrains.annotations.NotNull; + +import java.util.List; + +public class EditLanguagesRecyclerViewAdapter extends RecyclerView.Adapter { + private final List mValues; + private final ItemCallback mOnClick; + private final int mTelepathyRange; + private final String mUnderstandsBut; + private final Stepper.OnValueChangeListener mOnTelepathyRangeChanged; + private final TextWatcher mOnUnderstandsButChanged; + + private final int HEADER_VIEW_TYPE = 1; + private final int ITEM_VIEW_TYPE = 2; + private final String DISTANCE_IN_FEET_FORMAT = "%d ft."; + + public EditLanguagesRecyclerViewAdapter(List items, ItemCallback onClick, int telepathyRange, Stepper.OnValueChangeListener telepathyRangeChangedListener, String undderstandsBut, TextWatcher understandsButChangedListener) { + mValues = items; + mOnClick = onClick; + mTelepathyRange = telepathyRange; + mOnTelepathyRangeChanged = telepathyRangeChangedListener; + mUnderstandsBut = undderstandsBut; + mOnUnderstandsButChanged = understandsButChangedListener; + } + + @NotNull + @Override + public RecyclerView.ViewHolder onCreateViewHolder(@NotNull ViewGroup parent, int viewType) { + if (viewType == HEADER_VIEW_TYPE) { + return new HeaderViewHolder(FragmentEditLanguagesListHeaderBinding.inflate(LayoutInflater.from(parent.getContext()), parent, false)); + } + return new ItemViewHolder(FragmentEditLanguagesListItemBinding.inflate(LayoutInflater.from(parent.getContext()), parent, false)); + } + + @Override + public void onBindViewHolder(@NotNull final RecyclerView.ViewHolder holder, int position) { + if (holder instanceof HeaderViewHolder) { + HeaderViewHolder headerViewHolder = (HeaderViewHolder) holder; + headerViewHolder.telepathy.setOnFormatValueCallback(value -> String.format(DISTANCE_IN_FEET_FORMAT, value)); + headerViewHolder.telepathy.setValue(mTelepathyRange); + headerViewHolder.telepathy.setOnValueChangeListener(mOnTelepathyRangeChanged); + headerViewHolder.understandsBut.setText(mUnderstandsBut); + headerViewHolder.understandsBut.addTextChangedListener(mOnUnderstandsButChanged); + } else if(holder instanceof ItemViewHolder) { + ItemViewHolder itemViewHolder = (ItemViewHolder)holder; + itemViewHolder.mItem = mValues.get(position-1); + itemViewHolder.mContentView.setText(itemViewHolder.mItem.getName()); + itemViewHolder.itemView.setOnClickListener(view -> { + if (mOnClick != null) { + mOnClick.onItemCallback(itemViewHolder.mItem); + } + }); + } + } + + @Override + public int getItemCount() { + return mValues.size() +1; + } + + @Override + public int getItemViewType(int position) { + if (position == 0) { + return HEADER_VIEW_TYPE; + } + return ITEM_VIEW_TYPE; + } + + public interface ItemCallback { + void onItemCallback(Language language); + } + + public static class HeaderViewHolder extends RecyclerView.ViewHolder { + public final Stepper telepathy; + public final EditText understandsBut; + + public HeaderViewHolder(FragmentEditLanguagesListHeaderBinding binding) { + super(binding.getRoot()); + telepathy = binding.telepathy; + understandsBut = binding.understandsBut; + } + } + + public static class ItemViewHolder extends RecyclerView.ViewHolder { + public final TextView mContentView; + public Language mItem; + + public ItemViewHolder(FragmentEditLanguagesListItemBinding binding) { + super(binding.getRoot()); + mContentView = binding.content; + } + + @NotNull + @Override + public String toString() { + return super.toString() + " '" + mContentView.getText() + "'"; + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/majinnaibu/monstercards/ui/editmonster/EditMonsterFragment.java b/app/src/main/java/com/majinnaibu/monstercards/ui/editmonster/EditMonsterFragment.java index 81d7db7..708ee88 100644 --- a/app/src/main/java/com/majinnaibu/monstercards/ui/editmonster/EditMonsterFragment.java +++ b/app/src/main/java/com/majinnaibu/monstercards/ui/editmonster/EditMonsterFragment.java @@ -140,6 +140,11 @@ public class EditMonsterFragment extends MCFragment { Navigation.findNavController(requireView()).navigate(action); }); + mHolder.languages.setOnClickListener(v -> { + NavDirections action = EditMonsterFragmentDirections.actionEditMonsterFragmentToEditLanguagesFragment(); + Navigation.findNavController(requireView()).navigate(action); + }); + requireActivity().getOnBackPressedDispatcher().addCallback(getViewLifecycleOwner(), new OnBackPressedCallback(true) { @Override public void handleOnBackPressed() { diff --git a/app/src/main/java/com/majinnaibu/monstercards/ui/editmonster/EditMonsterViewModel.java b/app/src/main/java/com/majinnaibu/monstercards/ui/editmonster/EditMonsterViewModel.java index f99910e..58c990c 100644 --- a/app/src/main/java/com/majinnaibu/monstercards/ui/editmonster/EditMonsterViewModel.java +++ b/app/src/main/java/com/majinnaibu/monstercards/ui/editmonster/EditMonsterViewModel.java @@ -740,6 +740,10 @@ public class EditMonsterViewModel extends ChangeTrackedViewModel { return mTelepathyRange; } + public int getTelepathyRangeUnboxed() { + return Helpers.unboxInteger(mTelepathyRange.getValue(), 0); + } + public void setTelepathyRange(int telepathyRange) { mTelepathyRange.setValue(telepathyRange); } @@ -1189,5 +1193,12 @@ public class EditMonsterViewModel extends ChangeTrackedViewModel { static void replaceItemInList(MutableLiveData> listData, T oldItem, T newItem) { replaceItemInList(listData, oldItem, newItem, null); } + + static int unboxInteger(Integer value, int defaultIfNull) { + if (value == null) { + return defaultIfNull; + } + return value; + } } } diff --git a/app/src/main/res/layout/fragment_edit_language.xml b/app/src/main/res/layout/fragment_edit_language.xml new file mode 100644 index 0000000..8d4e30a --- /dev/null +++ b/app/src/main/res/layout/fragment_edit_language.xml @@ -0,0 +1,35 @@ + + + + + + + + + + + + + + diff --git a/app/src/main/res/layout/fragment_edit_languages_list.xml b/app/src/main/res/layout/fragment_edit_languages_list.xml new file mode 100644 index 0000000..e5d8254 --- /dev/null +++ b/app/src/main/res/layout/fragment_edit_languages_list.xml @@ -0,0 +1,33 @@ + + + + + + + diff --git a/app/src/main/res/layout/fragment_edit_languages_list_header.xml b/app/src/main/res/layout/fragment_edit_languages_list_header.xml new file mode 100644 index 0000000..f6775d6 --- /dev/null +++ b/app/src/main/res/layout/fragment_edit_languages_list_header.xml @@ -0,0 +1,36 @@ + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/fragment_edit_languages_list_item.xml b/app/src/main/res/layout/fragment_edit_languages_list_item.xml new file mode 100644 index 0000000..6e9049a --- /dev/null +++ b/app/src/main/res/layout/fragment_edit_languages_list_item.xml @@ -0,0 +1,13 @@ + + + + + \ No newline at end of file diff --git a/app/src/main/res/navigation/mobile_navigation.xml b/app/src/main/res/navigation/mobile_navigation.xml index cfb1c4c..64226ac 100644 --- a/app/src/main/res/navigation/mobile_navigation.xml +++ b/app/src/main/res/navigation/mobile_navigation.xml @@ -108,6 +108,9 @@ + + + + + + + + diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index cd89ea4..9230d5e 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -2,6 +2,7 @@ Add monster Add Sense Add Skill + Add Language Add Condition Edit Actions @@ -52,6 +53,7 @@ Legendary Actions Natural Armor Bonus Name + Understands But Proficiency Expertise None @@ -68,6 +70,7 @@ Strength Subtype Swim Speed + Telepathy Type Wisdom section divider @@ -83,4 +86,5 @@ Description Damage Type Add Damage Type + Can Speak \ No newline at end of file