From b1b2ab5a609906fc8d09bf09bf06f72bfd2f7179 Mon Sep 17 00:00:00 2001 From: TheBrokenRail Date: Sat, 17 Feb 2024 15:04:08 -0500 Subject: [PATCH] Menu Viewing! --- .idea/deploymentTargetDropDown.xml | 15 +++- .../mtudining/activity/menu/MenuActivity.java | 21 ++++- .../mtudining/activity/menu/MenuAdapter.java | 83 +++++++++++++++--- .../mtudining/activity/menu/MenuData.java | 9 +- .../activity/menu/MenuDatePicker.java | 86 +++++++++++++++++++ .../mtudining/activity/menu/MenuTask.java | 70 ++++++++++++++- .../mtudining/activity/task/Task.java | 4 +- .../mtudining/activity/task/TaskAdapter.java | 9 +- .../mtudining/api/method/PeriodDetail.java | 51 +++++++++++ .../mtudining/widget/CustomDropDownView.java | 64 ++++++++++++++ app/src/main/res/layout/menu_header.xml | 4 +- app/src/main/res/values/strings.xml | 2 + 12 files changed, 388 insertions(+), 30 deletions(-) create mode 100644 app/src/main/java/com/thebrokenrail/mtudining/activity/menu/MenuDatePicker.java create mode 100644 app/src/main/java/com/thebrokenrail/mtudining/api/method/PeriodDetail.java create mode 100644 app/src/main/java/com/thebrokenrail/mtudining/widget/CustomDropDownView.java diff --git a/.idea/deploymentTargetDropDown.xml b/.idea/deploymentTargetDropDown.xml index 0c0c338..4c59370 100644 --- a/.idea/deploymentTargetDropDown.xml +++ b/.idea/deploymentTargetDropDown.xml @@ -3,7 +3,20 @@ - + + + + + + + + + + + + + + diff --git a/app/src/main/java/com/thebrokenrail/mtudining/activity/menu/MenuActivity.java b/app/src/main/java/com/thebrokenrail/mtudining/activity/menu/MenuActivity.java index db04bbb..cf74b94 100644 --- a/app/src/main/java/com/thebrokenrail/mtudining/activity/menu/MenuActivity.java +++ b/app/src/main/java/com/thebrokenrail/mtudining/activity/menu/MenuActivity.java @@ -25,6 +25,7 @@ public class MenuActivity extends AppCompatActivity { public static final String NAME_EXTRA = "mtu_name"; public static final String ID_EXTRA = "mtu_id"; + private MenuViewModel viewModel; private MenuAdapter adapter; @Override @@ -41,13 +42,15 @@ public class MenuActivity extends AppCompatActivity { // Get View Model ViewModelProvider viewModelProvider = new ViewModelProvider(this); - MenuViewModel viewModel = viewModelProvider.get(MenuViewModel.class); + viewModel = viewModelProvider.get(MenuViewModel.class); // Get Info String name = getIntent().getStringExtra(NAME_EXTRA); - String id = getIntent().getStringExtra(ID_EXTRA); toolbar.setTitle(name); - viewModel.task.setup(id, new Date()); + String id = getIntent().getStringExtra(ID_EXTRA); + if (!viewModel.task.isSetup()) { + viewModel.task.setup(id, new Date()); + } // Setup RecyclerView adapter = new MenuAdapter(viewModel.task); @@ -55,6 +58,9 @@ public class MenuActivity extends AppCompatActivity { recyclerView.setLayoutManager(new LinearLayoutManager(this)); recyclerView.setAdapter(adapter); EdgeToEdgeUtil.setup(this, recyclerView); + + // Handle Existing Date Picker + MenuDatePicker.setupListeners(getSupportFragmentManager()); } @Override @@ -73,4 +79,13 @@ public class MenuActivity extends AppCompatActivity { return super.onOptionsItemSelected(item); } } + + /** + * Update selected date. + * @param newDate New date + */ + public void updateDate(Date newDate) { + viewModel.task.setup(null, newDate); + viewModel.task.start(); + } } diff --git a/app/src/main/java/com/thebrokenrail/mtudining/activity/menu/MenuAdapter.java b/app/src/main/java/com/thebrokenrail/mtudining/activity/menu/MenuAdapter.java index 53c7aa6..332eb8e 100644 --- a/app/src/main/java/com/thebrokenrail/mtudining/activity/menu/MenuAdapter.java +++ b/app/src/main/java/com/thebrokenrail/mtudining/activity/menu/MenuAdapter.java @@ -1,6 +1,5 @@ package com.thebrokenrail.mtudining.activity.menu; -import android.text.InputType; import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; @@ -9,13 +8,15 @@ import androidx.annotation.NonNull; import androidx.recyclerview.widget.RecyclerView; import com.google.android.material.textfield.MaterialAutoCompleteTextView; -import com.google.android.material.textfield.TextInputEditText; import com.google.android.material.textfield.TextInputLayout; import com.thebrokenrail.mtudining.R; import com.thebrokenrail.mtudining.activity.task.Task; import com.thebrokenrail.mtudining.activity.task.TaskAdapter; import com.thebrokenrail.mtudining.util.DateUtil; import com.thebrokenrail.mtudining.widget.CategoryView; +import com.thebrokenrail.mtudining.widget.CustomDropDownView; + +import java.util.Date; /** * Adapter for listing dining halls. @@ -34,32 +35,57 @@ class MenuAdapter extends TaskAdapter { return category; } + /** + * Get currently selected meal. + * @return A meal menu + */ + private MenuData.Meal getMeal() { + if (getResult() != null) { + for (MenuData.Meal meal : getResult().meals) { + if (meal.name.equals(getResult().selectedMeal)) { + // Found Meal + return meal; + } + } + } + // Could Not Find Meal + return null; + } + @Override protected void bindItemView(View view, int position) { - /*ListData.Category data = getResult().categories.get(position); + MenuData.Meal meal = getMeal(); + assert meal != null; + MenuData.Meal.Category data = meal.categories.get(position - getFirstElementPosition()); // Setup View CategoryView category = (CategoryView) view; category.setup(data.isOpen, data.name, () -> { // Open/Close Category data.isOpen = !data.isOpen; - notifyItemChanged(getResult().categories.indexOf(data)); + notifyItemChanged(meal.categories.indexOf(data) + getFirstElementPosition()); }); // Add Locations category.clearItems(); - for (ListData.Category.Element location : data.locations) { - category.addItem(location.name, () -> { + for (MenuData.Meal.Category.Element item : data.items) { + category.addItem(item.name, () -> { // Do Something! }); - }*/ + } } @Override protected int getDataSize() { - return 0; + MenuData.Meal meal = getMeal(); + if (meal != null) { + return meal.categories.size(); + } else { + return 0; + } } @Override protected View createHeader(@NonNull ViewGroup parent) { + // Inflate Header XML LayoutInflater layoutInflater = LayoutInflater.from(parent.getContext()); return layoutInflater.inflate(R.layout.menu_header, parent, false); } @@ -67,16 +93,49 @@ class MenuAdapter extends TaskAdapter { @Override protected void bindHeader(View view) { // Set Date + CustomDropDownView dateField = view.findViewById(R.id.menu_date_field); MaterialAutoCompleteTextView date = view.findViewById(R.id.menu_date); // Retrieve from Task as this must show even when result is not available. - date.setText(DateUtil.toString(((MenuTask) getTask()).getDate())); + Date current = ((MenuTask) getTask()).getDate(); + date.setText(DateUtil.toString(current)); + // Handle Click + dateField.setup(); + dateField.setOnDropDown(() -> { + // Open Date Picker + MenuDatePicker.show(current, ((MenuActivity) view.getContext()).getSupportFragmentManager()); + }); // Set Meal boolean hasMeal = getResult() != null; - TextInputLayout meal = view.findViewById(R.id.menu_meal_field); - meal.setEnabled(hasMeal); + TextInputLayout mealField = view.findViewById(R.id.menu_meal_field); + MaterialAutoCompleteTextView meal = view.findViewById(R.id.menu_meal); if (hasMeal) { - + // Enable/Disable Field + hasMeal = getResult().meals.size() > 0; + mealField.setEnabled(hasMeal); + if (hasMeal) { + // Set Dropdown Options + meal.setSimpleItems(getResult().meals.stream().map(x -> x.name).toArray(String[]::new)); + // Set Selection + meal.setText(getResult().selectedMeal, false); + // Handle Meal Selection + meal.setOnItemClickListener((parent, view1, position, id) -> { + // Get New Meal + String newMeal = (String) parent.getAdapter().getItem(position); + // Update UI + int oldItemCount = getItemCount(); + getResult().selectedMeal = newMeal; + reloadUI(oldItemCount); + }); + } else { + // Show N/A + meal.setText(meal.getResources().getString(R.string.not_available), false); + } + } else { + // Disable Field + mealField.setEnabled(false); + // Show No Text + meal.setText("", false); } } } diff --git a/app/src/main/java/com/thebrokenrail/mtudining/activity/menu/MenuData.java b/app/src/main/java/com/thebrokenrail/mtudining/activity/menu/MenuData.java index 5028c34..daa0290 100644 --- a/app/src/main/java/com/thebrokenrail/mtudining/activity/menu/MenuData.java +++ b/app/src/main/java/com/thebrokenrail/mtudining/activity/menu/MenuData.java @@ -7,12 +7,10 @@ class MenuData { public static class Meal { public static class Category { public static class Element { - public final String id; public final String name; public final String description; - public Element(String id, String name, String description) { - this.id = id; + public Element(String name, String description) { this.name = name; this.description = description; } @@ -20,7 +18,7 @@ class MenuData { public final String name; public boolean isOpen = true; - public final List locations = new ArrayList<>(); + public final List items = new ArrayList<>(); public Category(String name) { this.name = name; @@ -38,5 +36,6 @@ class MenuData { } public final List meals = new ArrayList<>(); - public String selectedMeal; + // This may be changed at runtime. + public String selectedMeal = null; } diff --git a/app/src/main/java/com/thebrokenrail/mtudining/activity/menu/MenuDatePicker.java b/app/src/main/java/com/thebrokenrail/mtudining/activity/menu/MenuDatePicker.java new file mode 100644 index 0000000..0c71f13 --- /dev/null +++ b/app/src/main/java/com/thebrokenrail/mtudining/activity/menu/MenuDatePicker.java @@ -0,0 +1,86 @@ +package com.thebrokenrail.mtudining.activity.menu; + +import androidx.fragment.app.Fragment; +import androidx.fragment.app.FragmentManager; + +import com.google.android.material.datepicker.MaterialDatePicker; +import com.thebrokenrail.mtudining.R; + +import java.util.Calendar; +import java.util.Date; +import java.util.TimeZone; + +/** + * Date picker for menu selection. + */ +public class MenuDatePicker { + private final static String TAG = "menu_date_picker"; + + /** + * Show date picker. + * @param current Currently selected date + * @param fragmentManager Support fragment manager + */ + public static void show(Date current, FragmentManager fragmentManager) { + // Get Date In UTC + Calendar calendar = Calendar.getInstance(); + calendar.setTime(current); + Calendar utcCalendar = Calendar.getInstance(TimeZone.getTimeZone("UTC")); + copyDateFromCalender(calendar, utcCalendar); + // Build Picker + MaterialDatePicker datePicker = MaterialDatePicker.Builder.datePicker() + .setTitleText(R.string.select_date) + .setSelection(utcCalendar.getTimeInMillis()) + .build(); + // Show + datePicker.show(fragmentManager, TAG); + // Handle Selection + setupListeners(datePicker); + } + + /** + * Setup event listeners on existing date picker. + * @param datePicker The date picker + */ + private static void setupListeners(MaterialDatePicker datePicker) { + datePicker.addOnPositiveButtonClickListener(selection -> { + // Get Selection + if (selection != null) { + // Get New Date + Calendar calendar = Calendar.getInstance(); + Calendar utcCalendar = Calendar.getInstance(TimeZone.getTimeZone("UTC")); + Date selectedDate = new Date(selection); + utcCalendar.setTime(selectedDate); + copyDateFromCalender(utcCalendar, calendar); + Date newDate = calendar.getTime(); + // Get Activity + MenuActivity activity = (MenuActivity) datePicker.requireActivity(); + // Update Date + activity.updateDate(newDate); + } + }); + } + + /** + * Setup event listeners on existing date picker. + * @param fragmentManager Support fragment manager + */ + @SuppressWarnings({"unchecked"}) + public static void setupListeners(FragmentManager fragmentManager) { + // Get Picker + Fragment fragment = fragmentManager.findFragmentByTag(TAG); + if (fragment != null) { + MaterialDatePicker datePicker = (MaterialDatePicker) fragment; + setupListeners(datePicker); + } + } + + private static void copyDateFromCalender(Calendar src, Calendar dst) { + int year = src.get(Calendar.YEAR); + int month = src.get(Calendar.MONTH); + int day = src.get(Calendar.DAY_OF_MONTH); + dst.set(Calendar.YEAR, year); + dst.set(Calendar.MONTH, month); + dst.set(Calendar.DAY_OF_MONTH, day); + } +} diff --git a/app/src/main/java/com/thebrokenrail/mtudining/activity/menu/MenuTask.java b/app/src/main/java/com/thebrokenrail/mtudining/activity/menu/MenuTask.java index 0062db9..c154a3c 100644 --- a/app/src/main/java/com/thebrokenrail/mtudining/activity/menu/MenuTask.java +++ b/app/src/main/java/com/thebrokenrail/mtudining/activity/menu/MenuTask.java @@ -2,11 +2,16 @@ package com.thebrokenrail.mtudining.activity.menu; import com.thebrokenrail.mtudining.activity.task.Task; import com.thebrokenrail.mtudining.api.Connection; +import com.thebrokenrail.mtudining.api.method.PeriodDetail; import com.thebrokenrail.mtudining.api.method.Periods; import com.thebrokenrail.mtudining.util.Constants; +import java.util.Comparator; import java.util.Date; +/** + * Task for loading menu information. + */ public class MenuTask extends Task { private final Connection connection; @@ -29,6 +34,14 @@ public class MenuTask extends Task { this.date = date; } + /** + * Check if task is setup. + * @return If the task is setup + */ + boolean isSetup() { + return locationId != null; + } + /** * Get date. * @return The date @@ -43,10 +56,61 @@ public class MenuTask extends Task { Periods periods = new Periods(Constants.PLATFORM, locationId, date); connection.send(periods, periodsResponse -> { // Loaded Periods - for (Periods.Response.Period period : periodsResponse.periods) { - System.out.println("Period: " + period.name); + + // Sort Periods/Meals + periodsResponse.periods.sort(Comparator.comparingInt(a -> a.sort_order)); + + // Load Menu For Each Period + MenuData data = new MenuData(); + int[] remaining = {periodsResponse.periods.size()}; + if (remaining[0] > 0) { + // Start Loading Menus + for (Periods.Response.Period period : periodsResponse.periods) { + // Load Period Details + MenuData.Meal meal = new MenuData.Meal(period.id, period.name); + PeriodDetail periodDetail = new PeriodDetail(Constants.PLATFORM, locationId, date, period.id); + connection.send(periodDetail, periodDetailResponse -> { + // Loaded Period Details + + // Sort Categories + periodDetailResponse.menu.periods.categories.sort(Comparator.comparingInt(a -> a.sort_order)); + + // Add Data + for (PeriodDetail.Response.Menu.PeriodData.MenuCategory category : periodDetailResponse.menu.periods.categories) { + MenuData.Meal.Category menuCategory = new MenuData.Meal.Category(category.name); + + // Sort Items + category.items.sort(Comparator.comparingInt(a -> a.sort_order)); + + // Add Items To Category + for (PeriodDetail.Response.Menu.PeriodData.MenuCategory.MenuItem item : category.items) { + menuCategory.items.add(new MenuData.Meal.Category.Element(item.name, item.desc)); + } + + // Skip Empty Category + if (menuCategory.items.size() > 0) { + meal.categories.add(menuCategory); + } + } + + // Check If All Data Has Been Loaded + remaining[0]--; + if (remaining[0] == 0) { + // All Data Loaded + done(id, data); + } + }, () -> { + // Error + done(id, null); + }); + data.meals.add(meal); + } + // Set Selected Meal + data.selectedMeal = data.meals.get(0).name; + } else { + // No Periods To Load + done(id, data); } - done(id, null); }, () -> { // Error done(id, null); diff --git a/app/src/main/java/com/thebrokenrail/mtudining/activity/task/Task.java b/app/src/main/java/com/thebrokenrail/mtudining/activity/task/Task.java index bbc6d7e..8923c50 100644 --- a/app/src/main/java/com/thebrokenrail/mtudining/activity/task/Task.java +++ b/app/src/main/java/com/thebrokenrail/mtudining/activity/task/Task.java @@ -21,7 +21,7 @@ public abstract class Task { /** * Last time when {@link #start()} was called. */ - private long lastStart; + private Long lastStart = null; /** * Start task. @@ -67,6 +67,8 @@ public abstract class Task { } // Update Listeners sendDataToListeners(); + // Ignore Future Responses From This API Call + lastStart = null; } /** diff --git a/app/src/main/java/com/thebrokenrail/mtudining/activity/task/TaskAdapter.java b/app/src/main/java/com/thebrokenrail/mtudining/activity/task/TaskAdapter.java index d94e292..6b3151c 100644 --- a/app/src/main/java/com/thebrokenrail/mtudining/activity/task/TaskAdapter.java +++ b/app/src/main/java/com/thebrokenrail/mtudining/activity/task/TaskAdapter.java @@ -198,12 +198,15 @@ public abstract class TaskAdapter extends RecyclerView.Adapter { + private final int platform; + private final String locationId; + private final String date; + private final String period; + + public PeriodDetail(int platform, String locationId, Date date, String period) { + this.platform = platform; + this.locationId = locationId; + this.date = DateUtil.toString(date); + this.period = period; + } + + @Override + public String getPath() { + return "/location/" + locationId + "/periods/" + period + "?platform=" + platform + "&date=" + date; + } + + @Override + public Class getResponseClass() { + return Response.class; + } + + public static class Response { + public static class Menu { + public static class PeriodData { + public static class MenuCategory { + public static class MenuItem { + public String name; + public String desc; + public int sort_order; + } + public String name; + public List items; + public int sort_order; + } + public List categories; + } + public PeriodData periods; + } + public Menu menu; + } +} diff --git a/app/src/main/java/com/thebrokenrail/mtudining/widget/CustomDropDownView.java b/app/src/main/java/com/thebrokenrail/mtudining/widget/CustomDropDownView.java new file mode 100644 index 0000000..4bb9279 --- /dev/null +++ b/app/src/main/java/com/thebrokenrail/mtudining/widget/CustomDropDownView.java @@ -0,0 +1,64 @@ +package com.thebrokenrail.mtudining.widget; + +import static android.view.accessibility.AccessibilityEvent.TYPE_VIEW_CLICKED; + +import android.annotation.SuppressLint; +import android.content.Context; +import android.util.AttributeSet; +import android.view.MotionEvent; +import android.view.accessibility.AccessibilityEvent; +import android.widget.EditText; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; + +import com.google.android.material.textfield.TextInputLayout; + +/** + * {@link TextInputLayout} with support for custom dropdown handling. + */ +public class CustomDropDownView extends TextInputLayout { + public CustomDropDownView(@NonNull Context context, @Nullable AttributeSet attributeSet) { + super(context, attributeSet); + } + + /** + * Current handler. + */ + private Runnable onDropDown = null; + + /** + * Set new dropdown handler. + * @param onDropDown New handler + */ + public void setOnDropDown(Runnable onDropDown) { + this.onDropDown = onDropDown; + } + + @Override + public void onPopulateAccessibilityEvent(AccessibilityEvent event) { + super.onPopulateAccessibilityEvent(event); + // Run Callback + // https://github.com/material-components/material-components-android/blob/dd8f6bebdd45c2875fab6574376a604a7d40061a/lib/java/com/google/android/material/textfield/DropdownMenuEndIconDelegate.java#L249-L252 + if (event.getEventType() == TYPE_VIEW_CLICKED && onDropDown != null) { + onDropDown.run(); + } + } + + /** + * Setup touch handler. + */ + @SuppressLint("ClickableViewAccessibility") + public void setup() { + EditText editText = getEditText(); + if (editText != null) { + // https://github.com/material-components/material-components-android/blob/dd8f6bebdd45c2875fab6574376a604a7d40061a/lib/java/com/google/android/material/textfield/DropdownMenuEndIconDelegate.java#L285-L291 + editText.setOnTouchListener((v, event) -> { + if (event.getAction() == MotionEvent.ACTION_UP && onDropDown != null) { + onDropDown.run(); + } + return false; + }); + } + } +} diff --git a/app/src/main/res/layout/menu_header.xml b/app/src/main/res/layout/menu_header.xml index 6f58736..8b5668c 100644 --- a/app/src/main/res/layout/menu_header.xml +++ b/app/src/main/res/layout/menu_header.xml @@ -6,7 +6,7 @@ android:orientation="vertical"> - - + Unable to load data! Date Meal + Select Date + N/A \ No newline at end of file