Tracked time (issues/prs) and created by me filter for My Issues (#1421)
All checks were successful
ci/woodpecker/push/locale Pipeline was successful
ci/woodpecker/push/build Pipeline was successful
ci/woodpecker/push/check Pipeline was successful
ci/woodpecker/push/finish Pipeline was successful
ci/woodpecker/cron/check Pipeline was successful
ci/woodpecker/cron/build Pipeline was successful
ci/woodpecker/cron/locale Pipeline was successful

Closes #247 Reviewed-on: #1421 Co-authored-by: M M Arif <mmarif@swatian.com> Co-committed-by: M M Arif <mmarif@swatian.com>
This commit is contained in:
M M Arif 2025-03-18 09:45:46 +00:00 committed by M M Arif
commit 186ed2bb70

View file

@ -70,7 +70,8 @@ import retrofit2.Callback;
*/
@SuppressWarnings("ConstantConditions")
public class MainActivity extends BaseActivity
implements NavigationView.OnNavigationItemSelectedListener, BottomSheetListener {
implements NavigationView.OnNavigationItemSelectedListener,
BottomSheetMyIssuesFilterFragment.BottomSheetListener {
public static boolean refActivity = false;
public static boolean reloadRepos = false;
@ -323,15 +324,14 @@ public class MainActivity extends BaseActivity
mainIntent.removeExtra("launchFragment");
switch (launchFragment) {
case "notifications":
toolbarTitle.setText(getResources().getString(R.string.pageTitleNotifications));
getSupportFragmentManager()
.beginTransaction()
.replace(R.id.fragment_container, new NotificationsFragment())
.commit();
navigationView.setCheckedItem(R.id.nav_notifications);
return;
if (launchFragment.equals("notifications")) {
toolbarTitle.setText(getResources().getString(R.string.pageTitleNotifications));
getSupportFragmentManager()
.beginTransaction()
.replace(R.id.fragment_container, new NotificationsFragment())
.commit();
navigationView.setCheckedItem(R.id.nav_notifications);
return;
}
}
@ -590,22 +590,12 @@ public class MainActivity extends BaseActivity
@Override
public void onButtonClicked(String text) {
switch (text) {
case "openMyIssues":
if (getFragmentRefreshListener() != null) {
getFragmentRefreshListener().onRefresh("open");
}
break;
case "closedMyIssues":
if (getFragmentRefreshListener() != null) {
getFragmentRefreshListener().onRefresh("closed");
}
break;
case "assignedToMe":
if (getFragmentRefreshListener() != null) {
getFragmentRefreshListener().onRefresh("assignedToMe");
}
break;
String[] parts = text.split("_");
String state = parts[0];
String filter = parts[1];
if (getFragmentRefreshListener() != null) {
getFragmentRefreshListener().onRefresh(state + "_" + filter);
}
}
@ -731,8 +721,16 @@ public class MainActivity extends BaseActivity
if (id == R.id.filter) {
Fragment currentFragment =
getSupportFragmentManager().findFragmentById(R.id.fragment_container);
String currentFilter = "open_created_by_me";
if (currentFragment instanceof MyIssuesFragment) {
currentFilter = ((MyIssuesFragment) currentFragment).getCurrentFilter();
}
BottomSheetMyIssuesFilterFragment filterBottomSheet =
new BottomSheetMyIssuesFilterFragment();
BottomSheetMyIssuesFilterFragment.newInstance(currentFilter);
filterBottomSheet.show(getSupportFragmentManager(), "myIssuesFilterMenuBottomSheet");
return true;
}
@ -961,13 +959,15 @@ public class MainActivity extends BaseActivity
this.profileInitListener = profileInitListener;
}
// My issues interface
public FragmentRefreshListener getFragmentRefreshListener() {
return fragmentRefreshListenerMyIssues;
}
public void setFragmentRefreshListenerMyIssues(
FragmentRefreshListener fragmentRefreshListener) {
this.fragmentRefreshListenerMyIssues = fragmentRefreshListener;
public void setFragmentRefreshListenerMyIssues(FragmentRefreshListener listener) {
this.fragmentRefreshListenerMyIssues = listener;
}
public interface FragmentRefreshListener {
void onRefresh(String myIssues);
}
}

View file

@ -0,0 +1,87 @@
package org.mian.gitnex.adapters;
import android.view.LayoutInflater;
import android.view.ViewGroup;
import androidx.annotation.NonNull;
import androidx.recyclerview.widget.RecyclerView;
import com.bumptech.glide.Glide;
import java.util.List;
import java.util.Locale;
import org.gitnex.tea4j.v2.models.TrackedTime;
import org.mian.gitnex.R;
import org.mian.gitnex.databinding.ListTrackedTimeBinding;
/**
* @author mmarif
*/
public class TrackedTimeAdapter
extends RecyclerView.Adapter<TrackedTimeAdapter.TrackedTimeViewHolder> {
private final List<TrackedTime> trackedTimeList;
private OnDeleteClickListener deleteClickListener;
public TrackedTimeAdapter(List<TrackedTime> trackedTimeList) {
this.trackedTimeList = trackedTimeList;
}
public void setOnDeleteClickListener(OnDeleteClickListener listener) {
this.deleteClickListener = listener;
}
@NonNull @Override
public TrackedTimeViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
ListTrackedTimeBinding binding =
ListTrackedTimeBinding.inflate(
LayoutInflater.from(parent.getContext()), parent, false);
return new TrackedTimeViewHolder(binding);
}
@Override
public void onBindViewHolder(@NonNull TrackedTimeViewHolder holder, int position) {
TrackedTime time = trackedTimeList.get(position);
Glide.with(holder.itemView.getContext())
.load(time.getIssue().getUser().getAvatarUrl())
.placeholder(R.drawable.ic_person)
.into(holder.binding.userAvatar);
String userName =
time.getIssue().getUser().getFullName().isEmpty()
? time.getIssue().getUser().getLogin()
: time.getIssue().getUser().getFullName();
holder.binding.userName.setText(userName);
long totalSeconds = time.getTime();
long hours = totalSeconds / 3600;
long minutes = (totalSeconds % 3600) / 60;
long seconds = totalSeconds % 60;
holder.binding.trackedTimeEntry.setText(
String.format(Locale.US, "%dh %dm %ds", hours, minutes, seconds));
holder.binding.deleteTrackedTime.setEnabled(true);
holder.binding.deleteTrackedTime.setOnClickListener(
v -> {
if (deleteClickListener != null) {
deleteClickListener.onDeleteClick(time, position);
}
});
}
@Override
public int getItemCount() {
return trackedTimeList.size();
}
public static class TrackedTimeViewHolder extends RecyclerView.ViewHolder {
private final ListTrackedTimeBinding binding;
TrackedTimeViewHolder(ListTrackedTimeBinding binding) {
super(binding.getRoot());
this.binding = binding;
}
}
public interface OnDeleteClickListener {
void onDeleteClick(TrackedTime time, int position);
}
}

View file

@ -9,7 +9,6 @@ import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import com.google.android.material.bottomsheet.BottomSheetDialogFragment;
import org.mian.gitnex.databinding.BottomSheetMyIssuesFilterBinding;
import org.mian.gitnex.structs.BottomSheetListener;
/**
* @author M M Arif
@ -17,6 +16,32 @@ import org.mian.gitnex.structs.BottomSheetListener;
public class BottomSheetMyIssuesFilterFragment extends BottomSheetDialogFragment {
private BottomSheetListener bmListener;
private String selectedState = "open";
private String selectedFilter = "created_by_me";
private static final String ARG_FILTER = "currentFilter";
public static BottomSheetMyIssuesFilterFragment newInstance(String currentFilter) {
BottomSheetMyIssuesFilterFragment fragment = new BottomSheetMyIssuesFilterFragment();
Bundle args = new Bundle();
args.putString(ARG_FILTER, currentFilter);
fragment.setArguments(args);
return fragment;
}
@Override
public void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
Bundle args = getArguments();
if (args != null && args.containsKey(ARG_FILTER)) {
String currentFilter = args.getString(ARG_FILTER, "open_created_by_me");
String[] parts = currentFilter.split("_");
selectedState = parts[0];
selectedFilter = parts.length > 1 ? parts[1] : "created_by_me";
}
}
@Nullable @Override
public View onCreateView(
@ -24,39 +49,68 @@ public class BottomSheetMyIssuesFilterFragment extends BottomSheetDialogFragment
@Nullable ViewGroup container,
@Nullable Bundle savedInstanceState) {
BottomSheetMyIssuesFilterBinding bottomSheetIssuesFilterBinding =
BottomSheetMyIssuesFilterBinding binding =
BottomSheetMyIssuesFilterBinding.inflate(inflater, container, false);
bottomSheetIssuesFilterBinding.openMyIssues.setOnClickListener(
v1 -> {
bmListener.onButtonClicked("openMyIssues");
dismiss();
// Clear all chip states
binding.chipOpen.setChecked(false);
binding.chipClosed.setChecked(false);
binding.chipCreatedByMe.setChecked(false);
binding.chipAssignedToMe.setChecked(false);
// Set chip states
binding.chipOpen.setChecked(selectedState.equals("open"));
binding.chipClosed.setChecked(selectedState.equals("closed"));
binding.chipCreatedByMe.setChecked(selectedFilter.equals("created_by_me"));
binding.chipAssignedToMe.setChecked(selectedFilter.equals("assignedToMe"));
binding.stateChipGroup.setOnCheckedStateChangeListener(
(group, checkedIds) -> {
if (!checkedIds.isEmpty()) {
int checkedId = checkedIds.get(0);
if (checkedId == binding.chipOpen.getId()) {
selectedState = "open";
} else if (checkedId == binding.chipClosed.getId()) {
selectedState = "closed";
}
applyFilter();
}
});
bottomSheetIssuesFilterBinding.closedMyIssues.setOnClickListener(
v12 -> {
bmListener.onButtonClicked("closedMyIssues");
dismiss();
binding.filterChipGroup.setOnCheckedStateChangeListener(
(group, checkedIds) -> {
if (!checkedIds.isEmpty()) {
int checkedId = checkedIds.get(0);
if (checkedId == binding.chipCreatedByMe.getId()) {
selectedFilter = "created_by_me";
} else if (checkedId == binding.chipAssignedToMe.getId()) {
selectedFilter = "assignedToMe";
}
applyFilter();
}
});
bottomSheetIssuesFilterBinding.assignedToMe.setOnClickListener(
v12 -> {
bmListener.onButtonClicked("assignedToMe");
dismiss();
});
return binding.getRoot();
}
return bottomSheetIssuesFilterBinding.getRoot();
private void applyFilter() {
String result = selectedState + "_" + selectedFilter;
bmListener.onButtonClicked(result);
dismiss();
}
@Override
public void onAttach(@NonNull Context context) {
super.onAttach(context);
try {
bmListener = (BottomSheetListener) context;
} catch (ClassCastException e) {
throw new ClassCastException(context + " must implement BottomSheetListener");
}
}
public interface BottomSheetListener {
void onButtonClicked(String text);
}
}

View file

@ -252,6 +252,14 @@ public class BottomSheetSingleIssueFragment extends BottomSheetDialogFragment {
dismiss();
});
binding.trackedTime.setOnClickListener(
v -> {
BottomSheetTrackedTimeFragment trackedTimeSheet =
BottomSheetTrackedTimeFragment.newInstance(issue);
trackedTimeSheet.show(getParentFragmentManager(), "trackedTimeBottomSheet");
dismiss();
});
binding.subscribeIssue.setOnClickListener(
subscribeToIssue -> {
IssueActions.subscribe(ctx, issue);
@ -286,6 +294,7 @@ public class BottomSheetSingleIssueFragment extends BottomSheetDialogFragment {
binding.mergePullRequest.setVisibility(View.GONE);
binding.updatePullRequest.setVisibility(View.GONE);
binding.manageDependencies.setVisibility(View.GONE);
binding.trackedTime.setVisibility(View.GONE);
if (issue.getIssueType().equalsIgnoreCase("issue")) {
binding.issuePrDivider.setVisibility(View.GONE);
}
@ -297,6 +306,7 @@ public class BottomSheetSingleIssueFragment extends BottomSheetDialogFragment {
binding.addRemoveAssignees.setVisibility(View.GONE);
binding.editLabels.setVisibility(View.GONE);
binding.manageDependencies.setVisibility(View.GONE);
binding.trackedTime.setVisibility(View.GONE);
}
binding.pinIssue.setOnClickListener(

View file

@ -0,0 +1,276 @@
package org.mian.gitnex.fragments;
import android.os.Bundle;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.recyclerview.widget.LinearLayoutManager;
import androidx.recyclerview.widget.RecyclerView;
import com.google.android.material.bottomsheet.BottomSheetDialogFragment;
import java.util.ArrayList;
import java.util.Date;
import java.util.List;
import org.gitnex.tea4j.v2.models.AddTimeOption;
import org.gitnex.tea4j.v2.models.TrackedTime;
import org.mian.gitnex.R;
import org.mian.gitnex.adapters.TrackedTimeAdapter;
import org.mian.gitnex.clients.RetrofitClient;
import org.mian.gitnex.databinding.BottomSheetTrackedTimeBinding;
import org.mian.gitnex.helpers.Constants;
import org.mian.gitnex.helpers.Toasty;
import org.mian.gitnex.helpers.contexts.IssueContext;
import retrofit2.Call;
import retrofit2.Callback;
import retrofit2.Response;
/**
* @author mmarif
*/
public class BottomSheetTrackedTimeFragment extends BottomSheetDialogFragment {
private BottomSheetTrackedTimeBinding binding;
private IssueContext issue;
private TrackedTimeAdapter trackedTimeAdapter;
private List<TrackedTime> trackedTimeList;
private int currentPage = 1;
private boolean isLoading = false;
private boolean hasMore = true;
private int resultLimit;
public static BottomSheetTrackedTimeFragment newInstance(IssueContext issue) {
BottomSheetTrackedTimeFragment fragment = new BottomSheetTrackedTimeFragment();
Bundle args = new Bundle();
args.putSerializable(IssueContext.INTENT_EXTRA, issue);
fragment.setArguments(args);
return fragment;
}
@Nullable @Override
public View onCreateView(
@NonNull LayoutInflater inflater,
@Nullable ViewGroup container,
@Nullable Bundle savedInstanceState) {
binding = BottomSheetTrackedTimeBinding.inflate(inflater, container, false);
if (getArguments() != null) {
issue = (IssueContext) getArguments().getSerializable(IssueContext.INTENT_EXTRA);
}
if (issue == null) {
throw new IllegalStateException("IssueContext is required");
}
resultLimit = Constants.getCurrentResultLimit(requireContext());
trackedTimeList = new ArrayList<>();
trackedTimeAdapter = new TrackedTimeAdapter(trackedTimeList);
LinearLayoutManager layoutManager = new LinearLayoutManager(requireContext());
binding.trackedTimeRecyclerView.setLayoutManager(layoutManager);
binding.trackedTimeRecyclerView.setAdapter(trackedTimeAdapter);
trackedTimeAdapter.setOnDeleteClickListener(this::deleteTrackedTime);
binding.trackedTimeRecyclerView.addOnScrollListener(
new RecyclerView.OnScrollListener() {
@Override
public void onScrolled(@NonNull RecyclerView recyclerView, int dx, int dy) {
super.onScrolled(recyclerView, dx, dy);
int totalItems = layoutManager.getItemCount();
int lastVisibleItem = layoutManager.findLastVisibleItemPosition();
if (!isLoading
&& hasMore
&& totalItems > 0
&& lastVisibleItem >= totalItems - 5) {
currentPage++;
loadTrackedTimes();
}
}
});
binding.addTimeButton.setEnabled(true);
binding.addTimeButton.setOnClickListener(v -> addTrackedTime());
loadTrackedTimes();
return binding.getRoot();
}
private void loadTrackedTimes() {
if (isLoading) return;
isLoading = true;
Call<List<TrackedTime>> call =
RetrofitClient.getApiInterface(requireContext())
.issueTrackedTimes(
issue.getRepository().getOwner(),
issue.getRepository().getName(),
(long) issue.getIssueIndex(),
null,
null,
null,
currentPage,
resultLimit);
call.enqueue(
new Callback<>() {
@Override
public void onResponse(
@NonNull Call<List<TrackedTime>> call,
@NonNull Response<List<TrackedTime>> response) {
isLoading = false;
if (response.isSuccessful() && response.body() != null) {
List<TrackedTime> newTimes = response.body();
int startPosition = trackedTimeList.size();
trackedTimeList.addAll(newTimes);
trackedTimeAdapter.notifyItemRangeInserted(
startPosition, newTimes.size());
hasMore = newTimes.size() >= resultLimit;
updateTotalTime();
updateUI();
} else {
hasMore = false;
updateUI();
}
}
@Override
public void onFailure(
@NonNull Call<List<TrackedTime>> call, @NonNull Throwable t) {
isLoading = false;
hasMore = false;
updateUI();
}
});
}
private void addTrackedTime() {
String hoursStr =
binding.hoursInput.getText() != null ? binding.hoursInput.getText().toString() : "";
String minutesStr =
binding.minutesInput.getText() != null
? binding.minutesInput.getText().toString()
: "";
int hours = hoursStr.isEmpty() ? 0 : Integer.parseInt(hoursStr);
int minutes = minutesStr.isEmpty() ? 0 : Integer.parseInt(minutesStr);
if (hours == 0 && minutes == 0) {
Toasty.warning(requireContext(), getString(R.string.enter_time));
return;
}
long totalSeconds = (hours * 3600L) + (minutes * 60L);
AddTimeOption timeOption = new AddTimeOption();
timeOption.setCreated(new Date());
timeOption.setTime(totalSeconds);
Call<TrackedTime> call =
RetrofitClient.getApiInterface(requireContext())
.issueAddTime(
issue.getRepository().getOwner(),
issue.getRepository().getName(),
(long) issue.getIssueIndex(),
timeOption);
call.enqueue(
new Callback<>() {
@Override
public void onResponse(
@NonNull Call<TrackedTime> call,
@NonNull Response<TrackedTime> response) {
if (response.isSuccessful() && response.body() != null) {
TrackedTime newTime = response.body();
trackedTimeList.add(0, newTime);
trackedTimeAdapter.notifyItemInserted(0);
binding.trackedTimeRecyclerView.scrollToPosition(0);
binding.hoursInput.setText("");
binding.minutesInput.setText("");
updateTotalTime();
updateUI();
Toasty.success(requireContext(), getString(R.string.time_added));
} else {
Toasty.error(requireContext(), getString(R.string.time_add_failed));
}
}
@Override
public void onFailure(@NonNull Call<TrackedTime> call, @NonNull Throwable t) {
Toasty.error(
requireContext(), getString(R.string.genericServerResponseError));
}
});
}
private void deleteTrackedTime(TrackedTime time, int position) {
Call<Void> call =
RetrofitClient.getApiInterface(requireContext())
.issueDeleteTime(
issue.getRepository().getOwner(),
issue.getRepository().getName(),
(long) issue.getIssueIndex(),
time.getId());
call.enqueue(
new Callback<>() {
@Override
public void onResponse(
@NonNull Call<Void> call, @NonNull Response<Void> response) {
if (response.isSuccessful()) {
trackedTimeList.remove(position);
trackedTimeAdapter.notifyItemRemoved(position);
trackedTimeAdapter.notifyItemRangeChanged(
position, trackedTimeList.size());
updateTotalTime();
updateUI();
Toasty.success(requireContext(), getString(R.string.time_removed));
} else {
Toasty.error(requireContext(), getString(R.string.time_delete_failed));
}
}
@Override
public void onFailure(@NonNull Call<Void> call, @NonNull Throwable t) {
Toasty.error(
requireContext(), getString(R.string.genericServerResponseError));
}
});
}
private void updateTotalTime() {
long totalSeconds = 0;
for (TrackedTime time : trackedTimeList) {
totalSeconds += time.getTime();
}
long hours = totalSeconds / 3600;
long minutes = (totalSeconds % 3600) / 60;
long seconds = totalSeconds % 60;
binding.totalTrackedTime.setText(getString(R.string.total_time, hours, minutes, seconds));
}
private void updateUI() {
if (trackedTimeList.isEmpty()) {
binding.trackedTimeRecyclerView.setVisibility(View.GONE);
binding.noTrackedTimeText.setVisibility(View.VISIBLE);
} else {
binding.trackedTimeRecyclerView.setVisibility(View.VISIBLE);
binding.noTrackedTimeText.setVisibility(View.GONE);
}
}
@Override
public void onDestroyView() {
super.onDestroyView();
binding = null;
}
}

View file

@ -364,18 +364,10 @@ public class IssuesFragment extends Fragment {
pinnedIssuesList.clear();
pinnedIssuesList.addAll(response.body());
adapterPinned.notifyDataChanged();
fragmentIssuesBinding.noDataIssues.setVisibility(View.GONE);
} else {
pinnedIssuesList.clear();
adapterPinned.notifyDataChanged();
fragmentIssuesBinding.noDataIssues.setVisibility(View.VISIBLE);
}
fragmentIssuesBinding.progressBar.setVisibility(View.GONE);
} else if (response.code() == 404) {
fragmentIssuesBinding.noDataIssues.setVisibility(View.VISIBLE);
fragmentIssuesBinding.progressBar.setVisibility(View.GONE);
} else {
Toasty.error(context, getString(R.string.genericError));
}
}
@ -397,6 +389,8 @@ public class IssuesFragment extends Fragment {
String labels,
String mentionedBy) {
fragmentIssuesBinding.progressBar.setVisibility(View.VISIBLE);
Call<List<Issue>> call =
RetrofitClient.getApiInterface(context)
.issueListIssues(

View file

@ -11,6 +11,7 @@ import android.view.View;
import android.view.ViewGroup;
import android.view.inputmethod.EditorInfo;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.core.view.MenuProvider;
import androidx.fragment.app.Fragment;
import androidx.lifecycle.Lifecycle;
@ -27,20 +28,55 @@ import org.mian.gitnex.viewmodels.IssuesViewModel;
*/
public class MyIssuesFragment extends Fragment {
public String state = "open";
public boolean assignedToMe = false;
private IssuesViewModel issuesViewModel;
private FragmentIssuesBinding fragmentIssuesBinding;
private IssuesViewModel issuesViewModel;
private ExploreIssuesAdapter adapter;
private int page = 1;
private Menu menu;
private String state = "open";
private boolean assignedToMe = false;
private boolean createdByMe = true;
private int page = 1;
@Override
@Nullable @Override
public View onCreateView(
@NonNull LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
@NonNull LayoutInflater inflater,
@Nullable ViewGroup container,
@Nullable Bundle savedInstanceState) {
fragmentIssuesBinding = FragmentIssuesBinding.inflate(inflater, container, false);
issuesViewModel = new ViewModelProvider(this).get(IssuesViewModel.class);
fragmentIssuesBinding.recyclerView.setHasFixedSize(true);
fragmentIssuesBinding.recyclerView.setLayoutManager(new LinearLayoutManager(getContext()));
fragmentIssuesBinding.createNewIssue.setVisibility(View.GONE);
setupMenu();
((MainActivity) requireActivity())
.setFragmentRefreshListenerMyIssues(
myIssues -> {
updateFilterState(myIssues);
fetchDataAsync(null);
});
fragmentIssuesBinding.pullToRefresh.setOnRefreshListener(
() ->
new Handler(Looper.getMainLooper())
.postDelayed(
() -> {
page = 1;
fragmentIssuesBinding.pullToRefresh.setRefreshing(
false);
fetchDataAsync(null);
},
50));
fetchDataAsync(null);
return fragmentIssuesBinding.getRoot();
}
private void setupMenu() {
requireActivity()
.addMenuProvider(
new MenuProvider() {
@ -65,7 +101,7 @@ public class MyIssuesFragment extends Fragment {
@Override
public boolean onQueryTextSubmit(String query) {
fetchDataAsync(query, state, assignedToMe);
fetchDataAsync(query);
searchView.setQuery(null, false);
searchItem.collapseActionView();
return false;
@ -80,81 +116,74 @@ public class MyIssuesFragment extends Fragment {
@Override
public boolean onMenuItemSelected(@NonNull MenuItem menuItem) {
if (menuItem.getItemId() == R.id.filter) {
String currentFilter =
state
+ "_"
+ (assignedToMe
? "assignedToMe"
: "created_by_me");
BottomSheetMyIssuesFilterFragment bottomSheet =
BottomSheetMyIssuesFilterFragment.newInstance(
currentFilter);
bottomSheet.show(getParentFragmentManager(), "myIssuesFilter");
return true;
}
return false;
}
},
getViewLifecycleOwner(),
Lifecycle.State.RESUMED);
issuesViewModel = new ViewModelProvider(this).get(IssuesViewModel.class);
fragmentIssuesBinding.recyclerView.setHasFixedSize(true);
fragmentIssuesBinding.recyclerView.setLayoutManager(new LinearLayoutManager(getContext()));
fragmentIssuesBinding.createNewIssue.setVisibility(View.GONE);
((MainActivity) requireActivity())
.setFragmentRefreshListenerMyIssues(
myIssues -> {
state = myIssues;
if (state.equals("closed")) {
menu.getItem(1).setIcon(R.drawable.ic_filter_closed);
} else {
menu.getItem(1).setIcon(R.drawable.ic_filter);
}
assignedToMe = state.equals("assignedToMe");
fragmentIssuesBinding.progressBar.setVisibility(View.VISIBLE);
fragmentIssuesBinding.noDataIssues.setVisibility(View.GONE);
fetchDataAsync(null, state, assignedToMe);
});
fragmentIssuesBinding.pullToRefresh.setOnRefreshListener(
() ->
new Handler(Looper.getMainLooper())
.postDelayed(
() -> {
page = 1;
fragmentIssuesBinding.pullToRefresh.setRefreshing(
false);
issuesViewModel.loadIssuesList(
null,
"issues",
true,
state,
assignedToMe,
getContext());
fragmentIssuesBinding.progressBar.setVisibility(
View.VISIBLE);
},
50));
fetchDataAsync(null, state, assignedToMe);
return fragmentIssuesBinding.getRoot();
}
private void fetchDataAsync(String query, String state, boolean assignedToMe) {
public String getCurrentFilter() {
return state + "_" + (assignedToMe ? "assignedToMe" : "created_by_me");
}
private void updateFilterState(String filter) {
String[] parts = filter.split("_");
String stateValue = parts[0];
String filterValue = parts[1];
state = stateValue;
menu.getItem(1)
.setIcon(
state.equals("closed")
? R.drawable.ic_filter_closed
: R.drawable.ic_filter);
if (filterValue.equals("created_by_me")) {
createdByMe = true;
assignedToMe = false;
} else if (filterValue.equals("assignedToMe")) {
createdByMe = false;
assignedToMe = true;
}
}
private void fetchDataAsync(String query) {
fragmentIssuesBinding.progressBar.setVisibility(View.VISIBLE);
fragmentIssuesBinding.noDataIssues.setVisibility(View.GONE);
issuesViewModel
.getIssuesList(query, "issues", true, state, assignedToMe, getContext())
.getIssuesList(query, "issues", createdByMe, state, assignedToMe, getContext())
.observe(
getViewLifecycleOwner(),
issuesListMain -> {
adapter = new ExploreIssuesAdapter(issuesListMain, getContext());
adapter.setLoadMoreListener(
new ExploreIssuesAdapter.OnLoadMoreListener() {
@Override
public void onLoadMore() {
page += 1;
issuesViewModel.loadMoreIssues(
query,
"issues",
true,
createdByMe,
state,
page,
assignedToMe,
@ -166,7 +195,6 @@ public class MyIssuesFragment extends Fragment {
@Override
public void onLoadFinished() {
fragmentIssuesBinding.progressBar.setVisibility(
View.GONE);
}

View file

@ -0,0 +1,34 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:pathData="M5,13a7,7 0,1 0,14 0a7,7 0,0 0,-14 0z"
android:strokeLineJoin="round"
android:strokeWidth="2"
android:fillColor="#00000000"
android:strokeColor="?attr/iconsColor"
android:strokeLineCap="round"/>
<path
android:pathData="M14.5,10.5l-2.5,2.5"
android:strokeLineJoin="round"
android:strokeWidth="2"
android:fillColor="#00000000"
android:strokeColor="?attr/iconsColor"
android:strokeLineCap="round"/>
<path
android:pathData="M17,8l1,-1"
android:strokeLineJoin="round"
android:strokeWidth="2"
android:fillColor="#00000000"
android:strokeColor="?attr/iconsColor"
android:strokeLineCap="round"/>
<path
android:pathData="M14,3h-4"
android:strokeLineJoin="round"
android:strokeWidth="2"
android:fillColor="#00000000"
android:strokeColor="?attr/iconsColor"
android:strokeLineCap="round"/>
</vector>

View file

@ -49,12 +49,11 @@
android:layout_height="wrap_content"
app:singleSelection="true"
android:paddingStart="@dimen/dimen16dp"
android:paddingEnd="@dimen/dimen16dp"
android:paddingBottom="@dimen/dimen8dp">
android:paddingEnd="@dimen/dimen16dp">
<com.google.android.material.chip.Chip
android:id="@+id/openChip"
style="@style/Widget.MaterialComponents.Chip.Filter"
style="@style/Widget.Material3.Chip.Filter"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/isOpen"
@ -62,7 +61,7 @@
<com.google.android.material.chip.Chip
android:id="@+id/closedChip"
style="@style/Widget.MaterialComponents.Chip.Filter"
style="@style/Widget.Material3.Chip.Filter"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/isClosed"/>
@ -78,21 +77,21 @@
<com.google.android.material.chip.Chip
android:id="@+id/mentionsChip"
style="@style/Widget.MaterialComponents.Chip.Filter"
style="@style/Widget.Material3.Chip.Filter"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/im_mentioned"/>
<com.google.android.material.chip.Chip
android:id="@+id/labelsChip"
style="@style/Widget.MaterialComponents.Chip.Filter"
style="@style/Widget.Material3.Chip.Filter"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/newIssueLabelsTitle"/>
<com.google.android.material.chip.Chip
android:id="@+id/milestoneChip"
style="@style/Widget.MaterialComponents.Chip.Filter"
style="@style/Widget.Material3.Chip.Filter"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/tabTextMl"

View file

@ -9,103 +9,90 @@
android:paddingTop="@dimen/dimen6dp"
android:paddingBottom="@dimen/dimen12dp">
<androidx.core.widget.NestedScrollView
<LinearLayout
android:id="@+id/myIssuesFilterHeadFrame"
android:layout_width="match_parent"
android:layout_height="wrap_content">
android:layout_height="wrap_content"
android:orientation="vertical"
android:padding="@dimen/dimen8dp">
<LinearLayout
<TextView
android:id="@+id/bottomSheetHeader"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical">
android:gravity="center"
android:text="@string/strFilter"
android:textColor="?attr/primaryTextColor"
android:textSize="@dimen/dimen16sp"/>
<LinearLayout
android:id="@+id/myIssuesFilterHeadFrame"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
android:padding="@dimen/dimen8dp">
<com.google.android.material.card.MaterialCardView
style="?attr/materialCardViewFilledStyle"
android:layout_width="@dimen/dimen28dp"
android:layout_height="@dimen/dimen4dp"
android:layout_gravity="center_horizontal"
android:layout_marginTop="@dimen/dimen8dp"
android:layout_marginBottom="@dimen/dimen16dp"
app:cardCornerRadius="@dimen/dimen24dp"
app:cardElevation="@dimen/dimen0dp">
<TextView
android:id="@+id/bottomSheetHeader"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:gravity="center"
android:text="@string/strFilter"
android:textColor="?attr/primaryTextColor"
android:textSize="@dimen/dimen16sp"/>
<com.google.android.material.card.MaterialCardView
style="?attr/materialCardViewFilledStyle"
android:layout_width="@dimen/dimen28dp"
android:layout_height="@dimen/dimen4dp"
android:layout_gravity="center_horizontal"
android:layout_marginTop="@dimen/dimen8dp"
android:layout_marginBottom="@dimen/dimen16dp"
app:cardCornerRadius="@dimen/dimen24dp"
app:cardElevation="@dimen/dimen0dp">
<View
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="?attr/fabColor" />
</com.google.android.material.card.MaterialCardView>
</LinearLayout>
<com.google.android.flexbox.FlexboxLayout
android:id="@+id/myIssuesSection"
<View
android:layout_width="match_parent"
android:layout_height="match_parent"
android:padding="@dimen/dimen4dp"
app:alignContent="center"
app:alignItems="flex_start"
app:flexWrap="wrap"
app:justifyContent="center">
android:background="?attr/fabColor" />
<TextView
android:id="@+id/openMyIssues"
android:layout_width="@dimen/dimen132dp"
android:layout_height="@dimen/dimen100dp"
android:background="?android:attr/selectableItemBackgroundBorderless"
android:gravity="center"
android:padding="@dimen/dimen4dp"
android:text="@string/isOpen"
android:textColor="?attr/primaryTextColor"
android:textSize="@dimen/dimen14sp"
app:drawableTopCompat="@drawable/ic_issue"
app:layout_alignSelf="flex_start"/>
</com.google.android.material.card.MaterialCardView>
<TextView
android:id="@+id/closedMyIssues"
android:layout_width="@dimen/dimen132dp"
android:layout_height="@dimen/dimen100dp"
android:background="?android:attr/selectableItemBackgroundBorderless"
android:gravity="center"
android:padding="@dimen/dimen4dp"
android:text="@string/isClosed"
android:textColor="?attr/primaryTextColor"
android:textSize="@dimen/dimen14sp"
app:drawableTopCompat="@drawable/ic_issue_closed"
app:layout_alignSelf="flex_start"/>
</LinearLayout>
<TextView
android:id="@+id/assignedToMe"
android:layout_width="@dimen/dimen132dp"
android:layout_height="@dimen/dimen100dp"
android:background="?android:attr/selectableItemBackgroundBorderless"
android:gravity="center"
android:padding="@dimen/dimen4dp"
android:text="@string/assignedToMe"
android:textColor="?attr/primaryTextColor"
android:textSize="@dimen/dimen14sp"
app:drawableTopCompat="@drawable/ic_person"
app:layout_alignSelf="flex_start"/>
<com.google.android.material.chip.ChipGroup
android:id="@+id/stateChipGroup"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:paddingStart="@dimen/dimen16dp"
android:paddingEnd="@dimen/dimen16dp"
app:singleSelection="true"
app:selectionRequired="true">
</com.google.android.flexbox.FlexboxLayout>
<com.google.android.material.chip.Chip
android:id="@+id/chipOpen"
style="@style/Widget.Material3.Chip.Filter"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/isOpen"
android:checked="true"/>
</LinearLayout>
<com.google.android.material.chip.Chip
android:id="@+id/chipClosed"
style="@style/Widget.Material3.Chip.Filter"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/isClosed"/>
</androidx.core.widget.NestedScrollView>
</com.google.android.material.chip.ChipGroup>
<com.google.android.material.chip.ChipGroup
android:id="@+id/filterChipGroup"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:padding="@dimen/dimen16dp"
app:singleSelection="true"
app:selectionRequired="true">
<com.google.android.material.chip.Chip
android:id="@+id/chipCreatedByMe"
style="@style/Widget.Material3.Chip.Filter"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/created_by_me"
android:checked="true"/>
<com.google.android.material.chip.Chip
android:id="@+id/chipAssignedToMe"
style="@style/Widget.Material3.Chip.Filter"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/assignedToMe"/>
</com.google.android.material.chip.ChipGroup>
</LinearLayout>

View file

@ -176,6 +176,19 @@
app:drawableTopCompat="@drawable/ic_dependencies"
app:layout_alignSelf="flex_start" />
<TextView
android:id="@+id/tracked_time"
android:layout_width="@dimen/dimen132dp"
android:layout_height="@dimen/dimen100dp"
android:background="?android:attr/selectableItemBackgroundBorderless"
android:gravity="center"
android:padding="@dimen/dimen4dp"
android:text="@string/tracked_time"
android:textColor="?attr/primaryTextColor"
android:textSize="@dimen/dimen14sp"
app:drawableTopCompat="@drawable/ic_stopwatch"
app:layout_alignSelf="flex_start" />
<TextView
android:id="@+id/pin_issue"
android:layout_width="@dimen/dimen132dp"

View file

@ -0,0 +1,108 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
android:padding="@dimen/dimen16dp">
<TextView
android:id="@+id/bottomSheetHeader"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="@string/tracked_time"
android:textAppearance="?attr/textAppearanceTitleLarge"
android:textColor="?attr/colorOnSurface"
android:paddingBottom="@dimen/dimen16dp" />
<TextView
android:id="@+id/totalTrackedTime"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:textAppearance="?attr/textAppearanceBodyMedium"
android:textStyle="bold"
android:textColor="?attr/colorOnSurface"
android:paddingBottom="@dimen/dimen12dp" />
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal"
android:gravity="center_vertical">
<com.google.android.material.textfield.TextInputLayout
android:id="@+id/hoursInputLayout"
style="@style/Widget.Material3.TextInputLayout.OutlinedBox.Dense"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:hint="@string/hours"
android:layout_marginEnd="@dimen/dimen12dp">
<com.google.android.material.textfield.TextInputEditText
android:id="@+id/hoursInput"
android:layout_width="match_parent"
android:layout_height="@dimen/dimen40dp"
android:inputType="number"
android:maxLength="3"
android:textAppearance="?attr/textAppearanceBodyMedium" />
</com.google.android.material.textfield.TextInputLayout>
<com.google.android.material.textfield.TextInputLayout
android:id="@+id/minutesInputLayout"
style="@style/Widget.Material3.TextInputLayout.OutlinedBox.Dense"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:hint="@string/minutes"
android:layout_marginEnd="@dimen/dimen12dp">
<com.google.android.material.textfield.TextInputEditText
android:id="@+id/minutesInput"
android:layout_width="match_parent"
android:layout_height="@dimen/dimen40dp"
android:inputType="number"
android:maxLength="2"
android:textAppearance="?attr/textAppearanceBodyMedium" />
</com.google.android.material.textfield.TextInputLayout>
<com.google.android.material.button.MaterialButton
android:id="@+id/addTimeButton"
android:layout_width="@dimen/dimen96dp"
android:layout_height="@dimen/dimen50dp"
android:text="@string/addButton"
android:minWidth="0dp"
android:paddingHorizontal="@dimen/dimen16dp" />
</LinearLayout>
<com.google.android.material.divider.MaterialDivider
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginVertical="@dimen/dimen16dp"
app:dividerColor="?attr/colorOutlineVariant" />
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/trackedTimeRecyclerView"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="@dimen/dimen8dp"
android:overScrollMode="never"
tools:listitem="@layout/list_tracked_time"
tools:itemCount="2" />
<TextView
android:id="@+id/noTrackedTimeText"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="@string/no_tracked_time"
android:textAppearance="?attr/textAppearanceBodyMedium"
android:textColor="?attr/colorOnSurfaceVariant"
android:gravity="center"
android:padding="@dimen/dimen16dp"
android:visibility="visible" />
</LinearLayout>

View file

@ -0,0 +1,82 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal"
android:paddingBottom="@dimen/dimen4dp"
android:gravity="center_vertical">
<com.google.android.material.card.MaterialCardView
android:id="@+id/card_view"
android:layout_width="match_parent"
android:layout_height="wrap_content"
style="?attr/materialCardViewElevatedStyle"
app:cardElevation="@dimen/dimen0dp">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:foreground="?android:attr/selectableItemBackground"
android:background="?attr/materialCardBackgroundColor"
android:padding="@dimen/dimen12dp"
android:gravity="center_vertical"
android:orientation="horizontal">
<!-- User Avatar -->
<com.google.android.material.card.MaterialCardView
android:id="@+id/avatarFrame"
android:layout_width="@dimen/dimen32dp"
android:layout_height="@dimen/dimen32dp"
style="?attr/materialCardViewElevatedStyle"
android:backgroundTint="@android:color/transparent"
app:cardElevation="@dimen/dimen0dp"
android:layout_marginEnd="@dimen/dimen12dp"
app:cardCornerRadius="@dimen/dimen8dp">
<ImageView
android:id="@+id/userAvatar"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:contentDescription="@string/generalImgContentText"
android:scaleType="centerCrop"
android:src="@drawable/ic_person" />
</com.google.android.material.card.MaterialCardView>
<!-- User Name -->
<TextView
android:id="@+id/userName"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:textAppearance="?attr/textAppearanceBodyMedium"
android:textColor="?attr/colorOnSurface"
android:layout_marginEnd="@dimen/dimen4dp"
tools:text="M M Arif - mian" />
<!-- Tracked Time -->
<TextView
android:id="@+id/trackedTimeEntry"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:textAppearance="?attr/textAppearanceBodyMedium"
android:textColor="?attr/colorOnSurface"
android:layout_marginEnd="@dimen/dimen8dp"
tools:text="0h 30m 0s" />
<!-- Delete Icon -->
<ImageView
android:id="@+id/deleteTrackedTime"
android:layout_width="@dimen/dimen22dp"
android:layout_height="@dimen/dimen22dp"
android:src="@drawable/ic_delete"
android:contentDescription="@string/menuDeleteText"
app:tint="?attr/colorOnSurfaceVariant" />
</LinearLayout>
</com.google.android.material.card.MaterialCardView>
</LinearLayout>

View file

@ -155,6 +155,7 @@
<string name="commentButtonText">Comment</string>
<string name="commentEmptyError">Please write your comment</string>
<string name="commentSuccess">Comment posted</string>
<string name="created_by_me">Created by Me</string>
<string name="featureDeprecated">This function will be removed in the future</string>
<string name="screamingInFearEmoticon" translatable="false">😱</string>
@ -554,6 +555,8 @@
<string name="branch">Branch</string>
<string name="branches">Branches</string>
<string name="timeline">Timeline</string>
<string name="hours">Hours</string>
<string name="minutes">Minutes</string>
<!-- generic copy -->
<string name="exploreUsers">Explore users</string>
@ -956,4 +959,13 @@
<string name="dependency_add_failed">Failed to add dependency</string>
<string name="search_failed">Search failed</string>
<string name="no_dependency_search_results">No matching results found</string>
<string name="tracked_time">Tracked Time</string>
<string name="no_tracked_time">No time tracked yet</string>
<string name="total_time">Total: %1$dh %2$dm %3$ds</string>
<string name="enter_time">Please enter some time</string>
<string name="time_added">Time added</string>
<string name="time_removed">Time removed</string>
<string name="time_add_failed">Failed to add time</string>
<string name="time_delete_failed">Failed to delete time</string>
</resources>