]> git.ktnx.net Git - mobile-ledger.git/blob - app/src/main/java/net/ktnx/mobileledger/ui/activity/NewTransactionFragment.java
use indefinite snackbar showing interval
[mobile-ledger.git] / app / src / main / java / net / ktnx / mobileledger / ui / activity / NewTransactionFragment.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.activity;
19
20 import android.app.Activity;
21 import android.content.Context;
22 import android.content.Intent;
23 import android.content.res.Resources;
24 import android.os.Bundle;
25 import android.renderscript.RSInvalidStateException;
26 import android.view.LayoutInflater;
27 import android.view.Menu;
28 import android.view.MenuInflater;
29 import android.view.MenuItem;
30 import android.view.View;
31 import android.view.ViewGroup;
32 import android.widget.ProgressBar;
33
34 import androidx.activity.result.ActivityResultLauncher;
35 import androidx.activity.result.contract.ActivityResultContract;
36 import androidx.annotation.NonNull;
37 import androidx.annotation.Nullable;
38 import androidx.appcompat.app.AlertDialog;
39 import androidx.fragment.app.Fragment;
40 import androidx.fragment.app.FragmentActivity;
41 import androidx.lifecycle.ViewModelProvider;
42 import androidx.recyclerview.widget.LinearLayoutManager;
43 import androidx.recyclerview.widget.RecyclerView;
44
45 import com.google.android.material.floatingactionbutton.FloatingActionButton;
46 import com.google.android.material.snackbar.Snackbar;
47
48 import net.ktnx.mobileledger.R;
49 import net.ktnx.mobileledger.json.API;
50 import net.ktnx.mobileledger.model.Data;
51 import net.ktnx.mobileledger.model.LedgerTransaction;
52 import net.ktnx.mobileledger.model.LedgerTransactionAccount;
53 import net.ktnx.mobileledger.model.MobileLedgerProfile;
54 import net.ktnx.mobileledger.utils.Logger;
55 import net.ktnx.mobileledger.utils.Misc;
56 import net.ktnx.mobileledger.utils.SimpleDate;
57
58 import org.jetbrains.annotations.NotNull;
59
60 import java.util.regex.Matcher;
61 import java.util.regex.Pattern;
62
63 /**
64  * A simple {@link Fragment} subclass.
65  * Activities that contain this fragment must implement the
66  * {@link OnNewTransactionFragmentInteractionListener} interface
67  * to handle interaction events.
68  */
69
70 // TODO: offer to undo account remove-on-swipe
71
72 public class NewTransactionFragment extends Fragment {
73     private NewTransactionItemsAdapter listAdapter;
74     private NewTransactionModel viewModel;
75     final ActivityResultLauncher<Void> scanQrLauncher =
76             registerForActivityResult(new ActivityResultContract<Void, String>() {
77                 @NonNull
78                 @Override
79                 public Intent createIntent(@NonNull Context context, Void input) {
80                     final Intent intent = new Intent("com.google.zxing.client.android.SCAN");
81                     intent.putExtra("SCAN_MODE", "QR_CODE_MODE");
82                     return intent;
83                 }
84                 @Override
85                 public String parseResult(int resultCode, @Nullable Intent intent) {
86                     if (resultCode == Activity.RESULT_CANCELED)
87                         return null;
88                     return intent.getStringExtra("SCAN_RESULT");
89                 }
90             }, this::onQrScanned);
91     private FloatingActionButton fab;
92     private OnNewTransactionFragmentInteractionListener mListener;
93     private MobileLedgerProfile mProfile;
94     public NewTransactionFragment() {
95         // Required empty public constructor
96         setHasOptionsMenu(true);
97     }
98     private void onQrScanned(String text) {
99         Logger.debug("qr", String.format("Got QR scan result [%s]", text));
100         Pattern p =
101                 Pattern.compile("^(\\d+)\\*(\\d+)\\*(\\d+)-(\\d+)-(\\d+)\\*([:\\d]+)\\*([\\d.]+)$");
102         Matcher m = p.matcher(text);
103         if (m.matches()) {
104             float amount = Float.parseFloat(m.group(7));
105             viewModel.setDate(
106                     new SimpleDate(Integer.parseInt(m.group(3)), Integer.parseInt(m.group(4)),
107                             Integer.parseInt(m.group(5))));
108
109             if (viewModel.accountsInInitialState()) {
110                 {
111                     NewTransactionModel.Item firstItem = viewModel.getItem(1);
112                     if (firstItem == null) {
113                         viewModel.addAccount(new LedgerTransactionAccount("разход:пазар"));
114                         listAdapter.notifyItemInserted(viewModel.items.size() - 1);
115                     }
116                     else {
117                         firstItem.setAccountName("разход:пазар");
118                         firstItem.getAccount()
119                                  .resetAmount();
120                         listAdapter.notifyItemChanged(1);
121                     }
122                 }
123                 {
124                     NewTransactionModel.Item secondItem = viewModel.getItem(2);
125                     if (secondItem == null) {
126                         viewModel.addAccount(
127                                 new LedgerTransactionAccount("актив:кеш:дам", -amount, null, null));
128                         listAdapter.notifyItemInserted(viewModel.items.size() - 1);
129                     }
130                     else {
131                         secondItem.setAccountName("актив:кеш:дам");
132                         secondItem.getAccount()
133                                   .setAmount(-amount);
134                         listAdapter.notifyItemChanged(2);
135                     }
136                 }
137             }
138             else {
139                 viewModel.addAccount(new LedgerTransactionAccount("разход:пазар"));
140                 viewModel.addAccount(
141                         new LedgerTransactionAccount("актив:кеш:дам", -amount, null, null));
142                 listAdapter.notifyItemRangeInserted(viewModel.items.size() - 1, 2);
143             }
144
145             listAdapter.checkTransactionSubmittable();
146         }
147     }
148     @Override
149     public void onCreateOptionsMenu(@NonNull Menu menu, @NonNull MenuInflater inflater) {
150         super.onCreateOptionsMenu(menu, inflater);
151         final FragmentActivity activity = getActivity();
152
153         inflater.inflate(R.menu.new_transaction_fragment, menu);
154
155         menu.findItem(R.id.scan_qr)
156             .setOnMenuItemClickListener(this::onScanQrAction);
157
158         menu.findItem(R.id.action_reset_new_transaction_activity)
159             .setOnMenuItemClickListener(item -> {
160                 listAdapter.reset();
161                 return true;
162             });
163
164         final MenuItem toggleCurrencyItem = menu.findItem(R.id.toggle_currency);
165         toggleCurrencyItem.setOnMenuItemClickListener(item -> {
166             viewModel.toggleCurrencyVisible();
167             return true;
168         });
169         if (activity != null)
170             viewModel.showCurrency.observe(activity, toggleCurrencyItem::setChecked);
171
172         final MenuItem toggleCommentsItem = menu.findItem(R.id.toggle_comments);
173         toggleCommentsItem.setOnMenuItemClickListener(item -> {
174             viewModel.toggleShowComments();
175             return true;
176         });
177         if (activity != null)
178             viewModel.showComments.observe(activity, toggleCommentsItem::setChecked);
179     }
180     private boolean onScanQrAction(MenuItem item) {
181         try {
182             scanQrLauncher.launch(null);
183         }
184         catch (Exception e) {
185             Logger.debug("qr", "Error launching QR scanner", e);
186         }
187
188         return true;
189     }
190     @Override
191     public View onCreateView(LayoutInflater inflater, ViewGroup container,
192                              Bundle savedInstanceState) {
193         // Inflate the layout for this fragment
194         return inflater.inflate(R.layout.fragment_new_transaction, container, false);
195     }
196
197     @Override
198     public void onViewCreated(@NotNull View view, @Nullable Bundle savedInstanceState) {
199         super.onViewCreated(view, savedInstanceState);
200         FragmentActivity activity = getActivity();
201         if (activity == null)
202             throw new RSInvalidStateException(
203                     "getActivity() returned null within onActivityCreated()");
204
205         viewModel = new ViewModelProvider(activity).get(NewTransactionModel.class);
206         viewModel.observeDataProfile(this);
207         mProfile = Data.getProfile();
208         listAdapter = new NewTransactionItemsAdapter(viewModel, mProfile);
209
210         RecyclerView list = activity.findViewById(R.id.new_transaction_accounts);
211         list.setAdapter(listAdapter);
212         list.setLayoutManager(new LinearLayoutManager(activity));
213
214         Data.observeProfile(getViewLifecycleOwner(), profile -> {
215             mProfile = profile;
216             listAdapter.setProfile(profile);
217         });
218         listAdapter.notifyDataSetChanged();
219         viewModel.isSubmittable()
220                  .observe(getViewLifecycleOwner(), isSubmittable -> {
221                      if (isSubmittable) {
222                          if (fab != null) {
223                              fab.show();
224                          }
225                      }
226                      else {
227                          if (fab != null) {
228                              fab.hide();
229                          }
230                      }
231                  });
232 //        viewModel.checkTransactionSubmittable(listAdapter);
233
234         fab = activity.findViewById(R.id.fab);
235         fab.setOnClickListener(v -> onFabPressed());
236
237         boolean keep = false;
238
239         Bundle args = getArguments();
240         if (args != null) {
241             String error = args.getString("error");
242             if (error != null) {
243                 Logger.debug("new-trans-f", String.format("Got error: %s", error));
244
245                 Context context = getContext();
246                 if (context != null) {
247                     AlertDialog.Builder builder = new AlertDialog.Builder(context);
248                     final Resources resources = context.getResources();
249                     final StringBuilder message = new StringBuilder();
250                     message.append(resources.getString(R.string.err_json_send_error_head));
251                     message.append("\n\n");
252                     message.append(error);
253                     if (mProfile.getApiVersion()
254                                 .equals(API.auto))
255                         message.append(
256                                 resources.getString(R.string.err_json_send_error_unsupported));
257                     else {
258                         message.append(resources.getString(R.string.err_json_send_error_tail));
259                         builder.setPositiveButton(R.string.btn_profile_options, (dialog, which) -> {
260                             Logger.debug("error", "will start profile editor");
261                             MobileLedgerProfile.startEditProfileActivity(context, mProfile);
262                         });
263                     }
264                     builder.setMessage(message);
265                     builder.create()
266                            .show();
267                 }
268                 else {
269                     Snackbar.make(list, error, Snackbar.LENGTH_INDEFINITE)
270                             .show();
271                 }
272                 keep = true;
273             }
274         }
275
276         int focused = 0;
277         if (savedInstanceState != null) {
278             keep |= savedInstanceState.getBoolean("keep", true);
279             focused = savedInstanceState.getInt("focused", 0);
280         }
281
282         if (!keep)
283             viewModel.reset();
284         else {
285             viewModel.setFocusedItem(focused);
286         }
287
288         ProgressBar p = activity.findViewById(R.id.progressBar);
289         viewModel.observeBusyFlag(getViewLifecycleOwner(), isBusy -> {
290             if (isBusy) {
291 //                Handler h = new Handler();
292 //                h.postDelayed(() -> {
293 //                    if (viewModel.getBusyFlag())
294 //                        p.setVisibility(View.VISIBLE);
295 //
296 //                }, 10);
297                 p.setVisibility(View.VISIBLE);
298             }
299             else
300                 p.setVisibility(View.INVISIBLE);
301         });
302     }
303     @Override
304     public void onSaveInstanceState(@NonNull Bundle outState) {
305         super.onSaveInstanceState(outState);
306         outState.putBoolean("keep", true);
307         final int focusedItem = viewModel.getFocusedItem();
308         outState.putInt("focused", focusedItem);
309     }
310     private void onFabPressed() {
311         fab.hide();
312         Misc.hideSoftKeyboard(this);
313         if (mListener != null) {
314             SimpleDate date = viewModel.getDate();
315             LedgerTransaction tr =
316                     new LedgerTransaction(null, date, viewModel.getDescription(), mProfile);
317
318             tr.setComment(viewModel.getComment());
319             LedgerTransactionAccount emptyAmountAccount = null;
320             float emptyAmountAccountBalance = 0;
321             for (int i = 0; i < viewModel.getAccountCount(); i++) {
322                 LedgerTransactionAccount acc =
323                         new LedgerTransactionAccount(viewModel.getAccount(i));
324                 if (acc.getAccountName()
325                        .trim()
326                        .isEmpty())
327                     continue;
328
329                 if (acc.isAmountSet()) {
330                     emptyAmountAccountBalance += acc.getAmount();
331                 }
332                 else {
333                     emptyAmountAccount = acc;
334                 }
335
336                 tr.addAccount(acc);
337             }
338
339             if (emptyAmountAccount != null)
340                 emptyAmountAccount.setAmount(-emptyAmountAccountBalance);
341
342             mListener.onTransactionSave(tr);
343         }
344     }
345
346     @Override
347     public void onAttach(@NotNull Context context) {
348         super.onAttach(context);
349         if (context instanceof OnNewTransactionFragmentInteractionListener) {
350             mListener = (OnNewTransactionFragmentInteractionListener) context;
351         }
352         else {
353             throw new RuntimeException(
354                     context.toString() + " must implement OnFragmentInteractionListener");
355         }
356     }
357
358     @Override
359     public void onDetach() {
360         super.onDetach();
361         mListener = null;
362     }
363
364     /**
365      * This interface must be implemented by activities that contain this
366      * fragment to allow an interaction in this fragment to be communicated
367      * to the activity and potentially other fragments contained in that
368      * activity.
369      * <p>
370      * See the Android Training lesson <a href=
371      * "http://developer.android.com/training/basics/fragments/communicating.html"
372      * >Communicating with Other Fragments</a> for more information.
373      */
374     public interface OnNewTransactionFragmentInteractionListener {
375         void onTransactionSave(LedgerTransaction tr);
376     }
377 }