Menu Viewing!

This commit is contained in:
TheBrokenRail 2024-02-17 15:04:08 -05:00
parent 5bfc54ccd8
commit b1b2ab5a60
12 changed files with 388 additions and 30 deletions

View File

@ -3,7 +3,20 @@
<component name="deploymentTargetDropDown"> <component name="deploymentTargetDropDown">
<value> <value>
<entry key="app"> <entry key="app">
<State /> <State>
<runningDeviceTargetSelectedWithDropDown>
<Target>
<type value="RUNNING_DEVICE_TARGET" />
<deviceKey>
<Key>
<type value="SERIAL_NUMBER" />
<value value="R5CRB1GE0RY" />
</Key>
</deviceKey>
</Target>
</runningDeviceTargetSelectedWithDropDown>
<timeTargetWasSelectedWithDropDown value="2024-02-17T18:39:39.847936576Z" />
</State>
</entry> </entry>
</value> </value>
</component> </component>

View File

@ -25,6 +25,7 @@ public class MenuActivity extends AppCompatActivity {
public static final String NAME_EXTRA = "mtu_name"; public static final String NAME_EXTRA = "mtu_name";
public static final String ID_EXTRA = "mtu_id"; public static final String ID_EXTRA = "mtu_id";
private MenuViewModel viewModel;
private MenuAdapter adapter; private MenuAdapter adapter;
@Override @Override
@ -41,13 +42,15 @@ public class MenuActivity extends AppCompatActivity {
// Get View Model // Get View Model
ViewModelProvider viewModelProvider = new ViewModelProvider(this); ViewModelProvider viewModelProvider = new ViewModelProvider(this);
MenuViewModel viewModel = viewModelProvider.get(MenuViewModel.class); viewModel = viewModelProvider.get(MenuViewModel.class);
// Get Info // Get Info
String name = getIntent().getStringExtra(NAME_EXTRA); String name = getIntent().getStringExtra(NAME_EXTRA);
String id = getIntent().getStringExtra(ID_EXTRA);
toolbar.setTitle(name); toolbar.setTitle(name);
String id = getIntent().getStringExtra(ID_EXTRA);
if (!viewModel.task.isSetup()) {
viewModel.task.setup(id, new Date()); viewModel.task.setup(id, new Date());
}
// Setup RecyclerView // Setup RecyclerView
adapter = new MenuAdapter(viewModel.task); adapter = new MenuAdapter(viewModel.task);
@ -55,6 +58,9 @@ public class MenuActivity extends AppCompatActivity {
recyclerView.setLayoutManager(new LinearLayoutManager(this)); recyclerView.setLayoutManager(new LinearLayoutManager(this));
recyclerView.setAdapter(adapter); recyclerView.setAdapter(adapter);
EdgeToEdgeUtil.setup(this, recyclerView); EdgeToEdgeUtil.setup(this, recyclerView);
// Handle Existing Date Picker
MenuDatePicker.setupListeners(getSupportFragmentManager());
} }
@Override @Override
@ -73,4 +79,13 @@ public class MenuActivity extends AppCompatActivity {
return super.onOptionsItemSelected(item); return super.onOptionsItemSelected(item);
} }
} }
/**
* Update selected date.
* @param newDate New date
*/
public void updateDate(Date newDate) {
viewModel.task.setup(null, newDate);
viewModel.task.start();
}
} }

View File

@ -1,6 +1,5 @@
package com.thebrokenrail.mtudining.activity.menu; package com.thebrokenrail.mtudining.activity.menu;
import android.text.InputType;
import android.view.LayoutInflater; import android.view.LayoutInflater;
import android.view.View; import android.view.View;
import android.view.ViewGroup; import android.view.ViewGroup;
@ -9,13 +8,15 @@ import androidx.annotation.NonNull;
import androidx.recyclerview.widget.RecyclerView; import androidx.recyclerview.widget.RecyclerView;
import com.google.android.material.textfield.MaterialAutoCompleteTextView; import com.google.android.material.textfield.MaterialAutoCompleteTextView;
import com.google.android.material.textfield.TextInputEditText;
import com.google.android.material.textfield.TextInputLayout; import com.google.android.material.textfield.TextInputLayout;
import com.thebrokenrail.mtudining.R; import com.thebrokenrail.mtudining.R;
import com.thebrokenrail.mtudining.activity.task.Task; import com.thebrokenrail.mtudining.activity.task.Task;
import com.thebrokenrail.mtudining.activity.task.TaskAdapter; import com.thebrokenrail.mtudining.activity.task.TaskAdapter;
import com.thebrokenrail.mtudining.util.DateUtil; import com.thebrokenrail.mtudining.util.DateUtil;
import com.thebrokenrail.mtudining.widget.CategoryView; import com.thebrokenrail.mtudining.widget.CategoryView;
import com.thebrokenrail.mtudining.widget.CustomDropDownView;
import java.util.Date;
/** /**
* Adapter for listing dining halls. * Adapter for listing dining halls.
@ -34,32 +35,57 @@ class MenuAdapter extends TaskAdapter<MenuData> {
return category; 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 @Override
protected void bindItemView(View view, int position) { 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 // Setup View
CategoryView category = (CategoryView) view; CategoryView category = (CategoryView) view;
category.setup(data.isOpen, data.name, () -> { category.setup(data.isOpen, data.name, () -> {
// Open/Close Category // Open/Close Category
data.isOpen = !data.isOpen; data.isOpen = !data.isOpen;
notifyItemChanged(getResult().categories.indexOf(data)); notifyItemChanged(meal.categories.indexOf(data) + getFirstElementPosition());
}); });
// Add Locations // Add Locations
category.clearItems(); category.clearItems();
for (ListData.Category.Element location : data.locations) { for (MenuData.Meal.Category.Element item : data.items) {
category.addItem(location.name, () -> { category.addItem(item.name, () -> {
// Do Something! // Do Something!
}); });
}*/ }
} }
@Override @Override
protected int getDataSize() { protected int getDataSize() {
MenuData.Meal meal = getMeal();
if (meal != null) {
return meal.categories.size();
} else {
return 0; return 0;
} }
}
@Override @Override
protected View createHeader(@NonNull ViewGroup parent) { protected View createHeader(@NonNull ViewGroup parent) {
// Inflate Header XML
LayoutInflater layoutInflater = LayoutInflater.from(parent.getContext()); LayoutInflater layoutInflater = LayoutInflater.from(parent.getContext());
return layoutInflater.inflate(R.layout.menu_header, parent, false); return layoutInflater.inflate(R.layout.menu_header, parent, false);
} }
@ -67,16 +93,49 @@ class MenuAdapter extends TaskAdapter<MenuData> {
@Override @Override
protected void bindHeader(View view) { protected void bindHeader(View view) {
// Set Date // Set Date
CustomDropDownView dateField = view.findViewById(R.id.menu_date_field);
MaterialAutoCompleteTextView date = view.findViewById(R.id.menu_date); MaterialAutoCompleteTextView date = view.findViewById(R.id.menu_date);
// Retrieve from Task as this must show even when result is not available. // 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 // Set Meal
boolean hasMeal = getResult() != null; boolean hasMeal = getResult() != null;
TextInputLayout meal = view.findViewById(R.id.menu_meal_field); TextInputLayout mealField = view.findViewById(R.id.menu_meal_field);
meal.setEnabled(hasMeal); MaterialAutoCompleteTextView meal = view.findViewById(R.id.menu_meal);
if (hasMeal) { 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);
} }
} }
} }

View File

@ -7,12 +7,10 @@ class MenuData {
public static class Meal { public static class Meal {
public static class Category { public static class Category {
public static class Element { public static class Element {
public final String id;
public final String name; public final String name;
public final String description; public final String description;
public Element(String id, String name, String description) { public Element(String name, String description) {
this.id = id;
this.name = name; this.name = name;
this.description = description; this.description = description;
} }
@ -20,7 +18,7 @@ class MenuData {
public final String name; public final String name;
public boolean isOpen = true; public boolean isOpen = true;
public final List<Element> locations = new ArrayList<>(); public final List<Element> items = new ArrayList<>();
public Category(String name) { public Category(String name) {
this.name = name; this.name = name;
@ -38,5 +36,6 @@ class MenuData {
} }
public final List<Meal> meals = new ArrayList<>(); public final List<Meal> meals = new ArrayList<>();
public String selectedMeal; // This may be changed at runtime.
public String selectedMeal = null;
} }

View File

@ -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<Long> 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<Long> 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<Long> datePicker = (MaterialDatePicker<Long>) 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);
}
}

View File

@ -2,11 +2,16 @@ package com.thebrokenrail.mtudining.activity.menu;
import com.thebrokenrail.mtudining.activity.task.Task; import com.thebrokenrail.mtudining.activity.task.Task;
import com.thebrokenrail.mtudining.api.Connection; import com.thebrokenrail.mtudining.api.Connection;
import com.thebrokenrail.mtudining.api.method.PeriodDetail;
import com.thebrokenrail.mtudining.api.method.Periods; import com.thebrokenrail.mtudining.api.method.Periods;
import com.thebrokenrail.mtudining.util.Constants; import com.thebrokenrail.mtudining.util.Constants;
import java.util.Comparator;
import java.util.Date; import java.util.Date;
/**
* Task for loading menu information.
*/
public class MenuTask extends Task<MenuData> { public class MenuTask extends Task<MenuData> {
private final Connection connection; private final Connection connection;
@ -29,6 +34,14 @@ public class MenuTask extends Task<MenuData> {
this.date = date; this.date = date;
} }
/**
* Check if task is setup.
* @return If the task is setup
*/
boolean isSetup() {
return locationId != null;
}
/** /**
* Get date. * Get date.
* @return The date * @return The date
@ -43,10 +56,61 @@ public class MenuTask extends Task<MenuData> {
Periods periods = new Periods(Constants.PLATFORM, locationId, date); Periods periods = new Periods(Constants.PLATFORM, locationId, date);
connection.send(periods, periodsResponse -> { connection.send(periods, periodsResponse -> {
// Loaded Periods // Loaded Periods
// 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) { for (Periods.Response.Period period : periodsResponse.periods) {
System.out.println("Period: " + period.name); // 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); 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);
}
}, () -> { }, () -> {
// Error // Error
done(id, null); done(id, null);

View File

@ -21,7 +21,7 @@ public abstract class Task<E> {
/** /**
* Last time when {@link #start()} was called. * Last time when {@link #start()} was called.
*/ */
private long lastStart; private Long lastStart = null;
/** /**
* Start task. * Start task.
@ -67,6 +67,8 @@ public abstract class Task<E> {
} }
// Update Listeners // Update Listeners
sendDataToListeners(); sendDataToListeners();
// Ignore Future Responses From This API Call
lastStart = null;
} }
/** /**

View File

@ -198,12 +198,15 @@ public abstract class TaskAdapter<E> extends RecyclerView.Adapter<RecyclerView.V
/** /**
* Reload UI. * Reload UI.
* @param oldItemCount Amount of visible items before UI change
*/ */
private void reloadUI(int oldItemCount) { protected void reloadUI(int oldItemCount) {
// Reload Header Without Animation
notifyItemChanged(0, new Object());
// Remove Existing Items // Remove Existing Items
notifyItemRangeRemoved(0, oldItemCount); notifyItemRangeRemoved(1, oldItemCount - 1);
// Add Items // Add Items
notifyItemRangeInserted(0, getItemCount()); notifyItemRangeInserted(1, getItemCount() - 1);
} }
/** /**

View File

@ -0,0 +1,51 @@
package com.thebrokenrail.mtudining.api.method;
import com.thebrokenrail.mtudining.api.Method;
import com.thebrokenrail.mtudining.util.DateUtil;
import java.util.Date;
import java.util.List;
public class PeriodDetail implements Method<PeriodDetail.Response> {
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<Response> 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<MenuItem> items;
public int sort_order;
}
public List<MenuCategory> categories;
}
public PeriodData periods;
}
public Menu menu;
}
}

View File

@ -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;
});
}
}
}

View File

@ -6,7 +6,7 @@
android:orientation="vertical"> android:orientation="vertical">
<!-- Date --> <!-- Date -->
<com.google.android.material.textfield.TextInputLayout <com.thebrokenrail.mtudining.widget.CustomDropDownView
android:id="@+id/menu_date_field" android:id="@+id/menu_date_field"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
@ -21,7 +21,7 @@
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:inputType="none" /> android:inputType="none" />
</com.google.android.material.textfield.TextInputLayout> </com.thebrokenrail.mtudining.widget.CustomDropDownView>
<!-- Meal --> <!-- Meal -->
<com.google.android.material.textfield.TextInputLayout <com.google.android.material.textfield.TextInputLayout

View File

@ -5,4 +5,6 @@
<string name="load_error">Unable to load data!</string> <string name="load_error">Unable to load data!</string>
<string name="date">Date</string> <string name="date">Date</string>
<string name="meal">Meal</string> <string name="meal">Meal</string>
<string name="select_date">Select Date</string>
<string name="not_available">N/A</string>
</resources> </resources>