]> git.ktnx.net Git - mobile-ledger.git/blob - app/src/main/java/net/ktnx/mobileledger/ui/account_summary/AccountSummaryAdapter.java
more pronounced day/month delimiters in the transaction list
[mobile-ledger.git] / app / src / main / java / net / ktnx / mobileledger / ui / account_summary / AccountSummaryAdapter.java
1 /*
2  * Copyright © 2021 Damyan Ivanov.
3  * This file is part of MoLe.
4  * MoLe is free software: you can distribute it and/or modify it
5  * under the term of the GNU General Public License as published by
6  * the Free Software Foundation, either version 3 of the License, or
7  * (at your opinion), any later version.
8  *
9  * MoLe is distributed in the hope that it will be useful,
10  * but WITHOUT ANY WARRANTY; without even the implied warranty of
11  * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12  * GNU General Public License terms for details.
13  *
14  * You should have received a copy of the GNU General Public License
15  * along with MoLe. If not, see <https://www.gnu.org/licenses/>.
16  */
17
18 package net.ktnx.mobileledger.ui.account_summary;
19
20 import android.content.res.Resources;
21 import android.view.LayoutInflater;
22 import android.view.View;
23 import android.view.ViewGroup;
24
25 import androidx.annotation.NonNull;
26 import androidx.annotation.Nullable;
27 import androidx.appcompat.app.AlertDialog;
28 import androidx.constraintlayout.widget.ConstraintLayout;
29 import androidx.lifecycle.LifecycleOwner;
30 import androidx.recyclerview.widget.AsyncListDiffer;
31 import androidx.recyclerview.widget.DiffUtil;
32 import androidx.recyclerview.widget.RecyclerView;
33
34 import net.ktnx.mobileledger.R;
35 import net.ktnx.mobileledger.dao.BaseDAO;
36 import net.ktnx.mobileledger.databinding.AccountListRowBinding;
37 import net.ktnx.mobileledger.databinding.AccountListSummaryRowBinding;
38 import net.ktnx.mobileledger.db.Account;
39 import net.ktnx.mobileledger.db.DB;
40 import net.ktnx.mobileledger.model.AccountListItem;
41 import net.ktnx.mobileledger.model.LedgerAccount;
42 import net.ktnx.mobileledger.ui.activity.MainActivity;
43 import net.ktnx.mobileledger.utils.Logger;
44 import net.ktnx.mobileledger.utils.Misc;
45
46 import org.jetbrains.annotations.NotNull;
47
48 import java.util.List;
49 import java.util.Locale;
50
51 import static net.ktnx.mobileledger.utils.Logger.debug;
52
53 public class AccountSummaryAdapter extends RecyclerView.Adapter<AccountSummaryAdapter.RowHolder> {
54     public static final int AMOUNT_LIMIT = 3;
55     private static final int ITEM_TYPE_HEADER = 1;
56     private static final int ITEM_TYPE_ACCOUNT = 2;
57     private final AsyncListDiffer<AccountListItem> listDiffer;
58
59     AccountSummaryAdapter() {
60         setHasStableIds(true);
61
62         listDiffer = new AsyncListDiffer<>(this, new DiffUtil.ItemCallback<AccountListItem>() {
63             @Nullable
64             @Override
65             public Object getChangePayload(@NonNull AccountListItem oldItem,
66                                            @NonNull AccountListItem newItem) {
67                 Change changes = new Change();
68
69                 final LedgerAccount oldAcc = oldItem.getAccount();
70                 final LedgerAccount newAcc = newItem.getAccount();
71
72                 if (!Misc.equalStrings(oldAcc.getName(), newAcc.getName()))
73                     changes.add(Change.NAME);
74
75                 if (oldAcc.getLevel() != newAcc.getLevel())
76                     changes.add(Change.LEVEL);
77
78                 if (oldAcc.isExpanded() != newAcc.isExpanded())
79                     changes.add(Change.EXPANDED);
80
81                 if (oldAcc.amountsExpanded() != newAcc.amountsExpanded())
82                     changes.add(Change.EXPANDED_AMOUNTS);
83
84                 if (!oldAcc.getAmountsString()
85                            .equals(newAcc.getAmountsString()))
86                     changes.add(Change.AMOUNTS);
87
88                 return changes.toPayload();
89             }
90             @Override
91             public boolean areItemsTheSame(@NotNull AccountListItem oldItem,
92                                            @NotNull AccountListItem newItem) {
93                 final AccountListItem.Type oldType = oldItem.getType();
94                 final AccountListItem.Type newType = newItem.getType();
95                 if (oldType != newType)
96                     return false;
97                 if (oldType == AccountListItem.Type.HEADER)
98                     return true;
99
100                 return oldItem.getAccount()
101                               .getId() == newItem.getAccount()
102                                                  .getId();
103             }
104             @Override
105             public boolean areContentsTheSame(@NotNull AccountListItem oldItem,
106                                               @NotNull AccountListItem newItem) {
107                 return oldItem.sameContent(newItem);
108             }
109         });
110     }
111     @Override
112     public long getItemId(int position) {
113         if (position == 0)
114             return 0;
115         return listDiffer.getCurrentList()
116                          .get(position)
117                          .getAccount()
118                          .getId();
119     }
120     @Override
121     public void onBindViewHolder(@NonNull RowHolder holder, int position,
122                                  @NonNull List<Object> payloads) {
123         holder.bind(listDiffer.getCurrentList()
124                               .get(position), payloads);
125         super.onBindViewHolder(holder, position, payloads);
126     }
127     public void onBindViewHolder(@NonNull RowHolder holder, int position) {
128         holder.bind(listDiffer.getCurrentList()
129                               .get(position), null);
130     }
131     @NonNull
132     @Override
133     public RowHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
134         final LayoutInflater inflater = LayoutInflater.from(parent.getContext());
135
136         final RowHolder result;
137         switch (viewType) {
138             case ITEM_TYPE_HEADER:
139                 result = new HeaderRowHolder(
140                         AccountListSummaryRowBinding.inflate(inflater, parent, false));
141                 break;
142             case ITEM_TYPE_ACCOUNT:
143                 result = new AccountRowHolder(
144                         AccountListRowBinding.inflate(inflater, parent, false));
145                 break;
146             default:
147                 throw new IllegalStateException("Unexpected value: " + viewType);
148         }
149
150 //        Logger.debug("acc-ui", "Creating " + result);
151         return result;
152     }
153     @Override
154     public int getItemCount() {
155         return listDiffer.getCurrentList()
156                          .size();
157     }
158     @Override
159     public int getItemViewType(int position) {
160         return (position == 0) ? ITEM_TYPE_HEADER : ITEM_TYPE_ACCOUNT;
161     }
162     public void setAccounts(List<AccountListItem> newList) {
163         Misc.onMainThread(() -> listDiffer.submitList(newList));
164     }
165     static class Change {
166         static final int NAME = 1;
167         static final int EXPANDED = 1 << 1;
168         static final int LEVEL = 1 << 2;
169         static final int EXPANDED_AMOUNTS = 1 << 3;
170         static final int AMOUNTS = 1 << 4;
171         private int value = 0;
172         public Change() {
173         }
174         public Change(int initialValue) {
175             value = initialValue;
176         }
177         public void add(int bits) {
178             value = value | bits;
179         }
180         public void add(Change change) {
181             value = value | change.value;
182         }
183         public void remove(int bits) {
184             value = value & (~bits);
185         }
186         public void remove(Change change) {
187             value = value & (~change.value);
188         }
189         public Change toPayload() {
190             if (value == 0)
191                 return null;
192             return this;
193         }
194         public boolean has(int bits) {
195             return value == 0 || (value & bits) == bits;
196         }
197     }
198
199     static abstract class RowHolder extends RecyclerView.ViewHolder {
200         public RowHolder(@NonNull View itemView) {
201             super(itemView);
202         }
203         public abstract void bind(AccountListItem accountListItem, @Nullable List<Object> payloads);
204     }
205
206     static class HeaderRowHolder extends RowHolder {
207         private final AccountListSummaryRowBinding b;
208         public HeaderRowHolder(@NonNull AccountListSummaryRowBinding binding) {
209             super(binding.getRoot());
210             b = binding;
211         }
212         @Override
213         public void bind(AccountListItem item, @Nullable List<Object> payloads) {
214             Resources r = itemView.getResources();
215 //            Logger.debug("acc", itemView.getContext()
216 //                                        .toString());
217             ((AccountListItem.Header) item).getText()
218                                            .observe((LifecycleOwner) itemView.getContext(),
219                                                    b.lastUpdateText::setText);
220         }
221     }
222
223     class AccountRowHolder extends AccountSummaryAdapter.RowHolder {
224         private final AccountListRowBinding b;
225         public AccountRowHolder(@NonNull AccountListRowBinding binding) {
226             super(binding.getRoot());
227             b = binding;
228
229             itemView.setOnLongClickListener(this::onItemLongClick);
230             b.accountRowAccName.setOnLongClickListener(this::onItemLongClick);
231             b.accountRowAccAmounts.setOnLongClickListener(this::onItemLongClick);
232             b.accountExpanderContainer.setOnLongClickListener(this::onItemLongClick);
233             b.accountExpander.setOnLongClickListener(this::onItemLongClick);
234
235             b.accountRowAccName.setOnClickListener(v -> toggleAccountExpanded());
236             b.accountExpanderContainer.setOnClickListener(v -> toggleAccountExpanded());
237             b.accountExpander.setOnClickListener(v -> toggleAccountExpanded());
238             b.accountRowAccAmounts.setOnClickListener(v -> toggleAmountsExpanded());
239         }
240         private void toggleAccountExpanded() {
241             LedgerAccount account = getAccount();
242             if (!account.hasSubAccounts())
243                 return;
244             debug("accounts", "Account expander clicked");
245
246             BaseDAO.runAsync(() -> {
247                 Account dbo = account.toDBO();
248                 dbo.setExpanded(!dbo.isExpanded());
249                 Logger.debug("accounts",
250                         String.format(Locale.ROOT, "%s (%d) → %s", account.getName(), dbo.getId(),
251                                 dbo.isExpanded() ? "expanded" : "collapsed"));
252                 DB.get()
253                   .getAccountDAO()
254                   .updateSync(dbo);
255             });
256         }
257         @NotNull
258         private LedgerAccount getAccount() {
259             return listDiffer.getCurrentList()
260                              .get(getBindingAdapterPosition())
261                              .getAccount();
262         }
263         private void toggleAmountsExpanded() {
264             LedgerAccount account = getAccount();
265             if (account.getAmountCount() <= AMOUNT_LIMIT)
266                 return;
267
268             account.toggleAmountsExpanded();
269             if (account.amountsExpanded()) {
270                 b.accountRowAccAmounts.setText(account.getAmountsString());
271                 b.accountRowAmountsExpanderContainer.setVisibility(View.GONE);
272             }
273             else {
274                 b.accountRowAccAmounts.setText(account.getAmountsString(AMOUNT_LIMIT));
275                 b.accountRowAmountsExpanderContainer.setVisibility(View.VISIBLE);
276             }
277
278             BaseDAO.runAsync(() -> {
279                 Account dbo = account.toDBO();
280                 DB.get()
281                   .getAccountDAO()
282                   .updateSync(dbo);
283             });
284         }
285         private boolean onItemLongClick(View v) {
286             MainActivity activity = (MainActivity) v.getContext();
287             AlertDialog.Builder builder = new AlertDialog.Builder(activity);
288             final String accountName = getAccount().getName();
289             builder.setTitle(accountName);
290             builder.setItems(R.array.acc_ctx_menu, (dialog, which) -> {
291                 if (which == 0) {// show transactions
292                     activity.showAccountTransactions(accountName);
293                 }
294                 else {
295                     throw new RuntimeException(String.format("Unknown menu item id (%d)", which));
296                 }
297                 dialog.dismiss();
298             });
299             builder.show();
300             return true;
301         }
302         @Override
303         public void bind(AccountListItem item, @Nullable List<Object> payloads) {
304             LedgerAccount acc = item.getAccount();
305
306             Change changes = new Change();
307             if (payloads != null) {
308                 for (Object p : payloads) {
309                     if (p instanceof Change)
310                         changes.add((Change) p);
311                 }
312             }
313 //            debug("accounts",
314 //                    String.format(Locale.US, "Binding '%s' to %s", acc.getName(), this));
315
316             Resources rm = b.getRoot()
317                             .getContext()
318                             .getResources();
319
320             if (changes.has(Change.NAME))
321                 b.accountRowAccName.setText(acc.getShortName());
322
323             if (changes.has(Change.LEVEL)) {
324                 ConstraintLayout.LayoutParams lp =
325                         (ConstraintLayout.LayoutParams) b.flowWrapper.getLayoutParams();
326                 lp.setMarginStart(
327                         acc.getLevel() * rm.getDimensionPixelSize(R.dimen.thumb_row_height) / 3);
328             }
329
330             if (acc.hasSubAccounts()) {
331                 b.accountExpanderContainer.setVisibility(View.VISIBLE);
332
333                 if (changes.has(Change.EXPANDED)) {
334                     int wantedRotation = acc.isExpanded() ? 0 : 180;
335                     if (b.accountExpanderContainer.getRotation() != wantedRotation) {
336 //                        Logger.debug("acc-ui",
337 //                                String.format(Locale.ROOT, "Rotating %s to %d", acc.getName(),
338 //                                        wantedRotation));
339                         b.accountExpanderContainer.animate()
340                                                   .rotation(wantedRotation);
341                     }
342                 }
343             }
344             else {
345                 b.accountExpanderContainer.setVisibility(View.GONE);
346             }
347
348             if (changes.has(Change.EXPANDED_AMOUNTS)) {
349                 int amounts = acc.getAmountCount();
350                 if ((amounts > AMOUNT_LIMIT) && !acc.amountsExpanded()) {
351                     b.accountRowAccAmounts.setText(acc.getAmountsString(AMOUNT_LIMIT));
352                     b.accountRowAmountsExpanderContainer.setVisibility(View.VISIBLE);
353                 }
354                 else {
355                     b.accountRowAccAmounts.setText(acc.getAmountsString());
356                     b.accountRowAmountsExpanderContainer.setVisibility(View.GONE);
357                 }
358             }
359         }
360     }
361 }