--- /dev/null
+/*
+ * Copyright © 2021 Damyan Ivanov.
+ * This file is part of MoLe.
+ * MoLe is free software: you can distribute it and/or modify it
+ * under the term of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your opinion), any later version.
+ *
+ * MoLe is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License terms for details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with MoLe. If not, see <https://www.gnu.org/licenses/>.
+ */
+
+package net.ktnx.mobileledger.model;
+
+import androidx.annotation.NonNull;
+import androidx.recyclerview.widget.DiffUtil;
+
+import java.io.Serializable;
+
+public class PatternDetailSource implements Serializable {
+ public static final DiffUtil.ItemCallback<PatternDetailSource> DIFF_CALLBACK =
+ new DiffUtil.ItemCallback<PatternDetailSource>() {
+ @Override
+ public boolean areItemsTheSame(@NonNull PatternDetailSource oldItem,
+ @NonNull PatternDetailSource newItem) {
+ return oldItem.groupNumber == newItem.groupNumber;
+ }
+ @Override
+ public boolean areContentsTheSame(@NonNull PatternDetailSource oldItem,
+ @NonNull PatternDetailSource newItem) {
+ return oldItem.matchedText.equals(newItem.matchedText);
+ }
+ };
+
+ private short groupNumber;
+ private String matchedText;
+ public PatternDetailSource() {
+ }
+ public PatternDetailSource(short groupNumber, String matchedText) {
+ this.groupNumber = groupNumber;
+ this.matchedText = matchedText;
+ }
+ public short getGroupNumber() {
+ return groupNumber;
+ }
+ public void setGroupNumber(short groupNumber) {
+ this.groupNumber = groupNumber;
+ }
+ public String getMatchedText() {
+ return matchedText;
+ }
+ public void setMatchedText(String matchedText) {
+ this.matchedText = matchedText;
+ }
+}
--- /dev/null
+/*
+ * Copyright © 2021 Damyan Ivanov.
+ * This file is part of MoLe.
+ * MoLe is free software: you can distribute it and/or modify it
+ * under the term of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your opinion), any later version.
+ *
+ * MoLe is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License terms for details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with MoLe. If not, see <https://www.gnu.org/licenses/>.
+ */
+
+package net.ktnx.mobileledger.model;
+
+import android.content.res.Resources;
+
+import androidx.annotation.NonNull;
+
+import net.ktnx.mobileledger.R;
+import net.ktnx.mobileledger.db.PatternAccount;
+import net.ktnx.mobileledger.db.PatternBase;
+import net.ktnx.mobileledger.db.PatternHeader;
+import net.ktnx.mobileledger.utils.Misc;
+
+import org.jetbrains.annotations.Contract;
+import org.jetbrains.annotations.NotNull;
+
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+import java.util.regex.PatternSyntaxException;
+
+abstract public class PatternDetailsItem {
+ private final Type type;
+ protected long id;
+ protected long position;
+
+ protected PatternDetailsItem(Type type, long id, long position) {
+ this.type = type;
+ this.id = (id <= 0) ? -position - 2 : id;
+ this.position = position;
+ }
+ @Contract(" -> new")
+ public static @NotNull PatternDetailsItem.Header createHeader() {
+ return new Header();
+ }
+ public static @NotNull PatternDetailsItem.Header createHeader(Header origin) {
+ return new Header(origin);
+ }
+ @Contract("_ -> new")
+ public static @NotNull PatternDetailsItem.AccountRow createAccountRow(long position) {
+ return new AccountRow(-1, position);
+ }
+ public static PatternDetailsItem fromRoomObject(PatternBase p) {
+ if (p instanceof PatternHeader) {
+ PatternHeader ph = (PatternHeader) p;
+ Header header = createHeader();
+ header.setName(ph.getName());
+ header.setPattern(ph.getRegularExpression());
+ header.setTestText(null);
+ header.setTransactionDescription(ph.getTransactionDescription());
+ header.setTransactionComment(ph.getTransactionComment());
+ header.setDateDayMatchGroup(ph.getDateDayMatchGroup());
+ header.setDateMonthMatchGroup(ph.getDateMonthMatchGroup());
+ header.setDateYearMatchGroup(ph.getDateYearMatchGroup());
+
+ return header;
+ }
+ else if (p instanceof PatternAccount) {
+ PatternAccount pa = (PatternAccount) p;
+ AccountRow acc = createAccountRow(pa.getPosition());
+
+ if (Misc.emptyIsNull(pa.getAccountName()) != null)
+ acc.setAccountName(pa.getAccountName());
+ else
+ acc.setAccountNameMatchGroup(pa.getAccountNameMatchGroup());
+
+ if (Misc.emptyIsNull(pa.getAccountComment()) == null)
+ acc.setAccountCommentMatchGroup(pa.getAccountCommentMatchGroup());
+ else
+ acc.setAccountComment(pa.getAccountComment());
+
+ if (pa.getCurrency() == null) {
+ acc.setCurrencyMatchGroup(pa.getCurrencyMatchGroup());
+ }
+ else {
+ acc.setCurrency(Currency.loadById(pa.getCurrency()));
+ }
+
+ if (pa.getAmount() == null)
+ acc.setAmountMatchGroup(pa.getAmountMatchGroup());
+ else
+ acc.setAmount(pa.getAmount());
+
+ return acc;
+ }
+ else {
+ throw new IllegalStateException("Unexpected item class " + p.getClass());
+ }
+ }
+ public Header asHeaderItem() {
+ ensureType(Type.HEADER);
+ return (Header) this;
+ }
+ public AccountRow asAccountRowItem() {
+ ensureType(Type.ACCOUNT_ITEM);
+ return (AccountRow) this;
+ }
+ private void ensureType(Type type) {
+ if (this.type != type)
+ throw new IllegalStateException(
+ String.format("Type is %s, but %s is required", this.type.toString(),
+ type.toString()));
+ }
+ void ensureTrue(boolean flag) {
+ if (!flag)
+ throw new IllegalStateException(
+ "Literal value requested, but it is matched via a pattern group");
+ }
+ void ensureFalse(boolean flag) {
+ if (flag)
+ throw new IllegalStateException("Matching group requested, but the value is a literal");
+ }
+ public long getId() {
+ return id;
+ }
+ public void setId(int id) {
+ this.id = id;
+ }
+ public long getPosition() {
+ return position;
+ }
+ public void setPosition(int position) {
+ this.position = position;
+ }
+ abstract public String getProblem(@NonNull Resources r, int patternGroupCount);
+ public Type getType() {
+ return type;
+ }
+ public enum Type {
+ HEADER(TYPE.header), ACCOUNT_ITEM(TYPE.accountItem);
+ final int index;
+ Type(int i) {
+ index = i;
+ }
+ public int toInt() {
+ return index;
+ }
+ }
+
+ static class PossiblyMatchedValue<T> {
+ private boolean literalValue;
+ private T value;
+ private int matchGroup;
+ public PossiblyMatchedValue() {
+ literalValue = true;
+ value = null;
+ }
+ public PossiblyMatchedValue(@NonNull PossiblyMatchedValue<T> origin) {
+ literalValue = origin.literalValue;
+ value = origin.value;
+ matchGroup = origin.matchGroup;
+ }
+ @NonNull
+ public static PossiblyMatchedValue<Integer> withLiteralInt(int initialValue) {
+ PossiblyMatchedValue<Integer> result = new PossiblyMatchedValue<>();
+ result.setValue(initialValue);
+ return result;
+ }
+ @NonNull
+ public static PossiblyMatchedValue<Float> withLiteralFloat(float initialValue) {
+ PossiblyMatchedValue<Float> result = new PossiblyMatchedValue<>();
+ result.setValue(initialValue);
+ return result;
+ }
+ public static PossiblyMatchedValue<Short> withLiteralShort(short initialValue) {
+ PossiblyMatchedValue<Short> result = new PossiblyMatchedValue<>();
+ result.setValue(initialValue);
+ return result;
+ }
+ @NonNull
+ public static PossiblyMatchedValue<String> withLiteralString(String initialValue) {
+ PossiblyMatchedValue<String> result = new PossiblyMatchedValue<>();
+ result.setValue(initialValue);
+ return result;
+ }
+ public T getValue() {
+ if (!literalValue)
+ throw new IllegalStateException("Value is not literal");
+ return value;
+ }
+ public void setValue(T newValue) {
+ value = newValue;
+ literalValue = true;
+ }
+ public boolean hasLiteralValue() {
+ return literalValue;
+ }
+ public int getMatchGroup() {
+ if (literalValue)
+ throw new IllegalStateException("Value is literal");
+ return matchGroup;
+ }
+ public void setMatchGroup(int group) {
+ this.matchGroup = group;
+ literalValue = false;
+ }
+ public boolean equals(PossiblyMatchedValue<T> other) {
+ if (!other.literalValue == literalValue)
+ return false;
+ if (literalValue)
+ return value.equals(other.value);
+ else
+ return matchGroup == other.matchGroup;
+ }
+ public void switchToLiteral() {
+ literalValue = true;
+ }
+ }
+
+ public static class TYPE {
+ public static final int header = 0;
+ public static final int accountItem = 1;
+ }
+
+ public static class AccountRow extends PatternDetailsItem {
+ private final PossiblyMatchedValue<String> accountName =
+ PossiblyMatchedValue.withLiteralString("");
+ private final PossiblyMatchedValue<String> accountComment =
+ PossiblyMatchedValue.withLiteralString("");
+ private final PossiblyMatchedValue<Float> amount =
+ PossiblyMatchedValue.withLiteralFloat(0f);
+ private final PossiblyMatchedValue<Currency> currency = new PossiblyMatchedValue<>();
+ private AccountRow(long id, long position) {
+ super(Type.ACCOUNT_ITEM, id, position);
+ }
+ public int getAccountCommentMatchGroup() {
+ return accountComment.getMatchGroup();
+ }
+ public void setAccountCommentMatchGroup(int group) {
+ accountComment.setMatchGroup(group);
+ }
+ public String getAccountComment() {
+ return accountComment.getValue();
+ }
+ public void setAccountComment(String comment) {
+ this.accountComment.setValue(comment);
+ }
+ public int getCurrencyMatchGroup() {
+ return currency.getMatchGroup();
+ }
+ public void setCurrencyMatchGroup(int group) {
+ currency.setMatchGroup(group);
+ }
+ public Currency getCurrency() {
+ return currency.getValue();
+ }
+ public void setCurrency(Currency currency) {
+ this.currency.setValue(currency);
+ }
+ public int getAccountNameMatchGroup() {
+ return accountName.getMatchGroup();
+ }
+ public void setAccountNameMatchGroup(int group) {
+ accountName.setMatchGroup(group);
+ }
+ public String getAccountName() {
+ return accountName.getValue();
+ }
+ public void setAccountName(String accountName) {
+ this.accountName.setValue(accountName);
+ }
+ public boolean hasLiteralAccountName() { return accountName.hasLiteralValue(); }
+ public boolean hasLiteralAmount() {
+ return amount.hasLiteralValue();
+ }
+ public int getAmountMatchGroup() {
+ return amount.getMatchGroup();
+ }
+ public void setAmountMatchGroup(int group) {
+ amount.setMatchGroup(group);
+ }
+ public float getAmount() {
+ return amount.getValue();
+ }
+ public void setAmount(float amount) {
+ this.amount.setValue(amount);
+ }
+ public String getProblem(@NonNull Resources r, int patternGroupCount) {
+ if (Misc.emptyIsNull(accountName.getValue()) == null)
+ return r.getString(R.string.account_name_is_empty);
+ if (!amount.hasLiteralValue() &&
+ (amount.getMatchGroup() < 1 || amount.getMatchGroup() > patternGroupCount))
+ return r.getString(R.string.invalid_matching_group_number);
+
+ return null;
+ }
+ public boolean hasLiteralAccountComment() {
+ return accountComment.hasLiteralValue();
+ }
+ public boolean equalContents(AccountRow o) {
+ return amount.equals(o.amount) && accountName.equals(o.accountName) &&
+ accountComment.equals(o.accountComment);
+ }
+ public void switchToLiteralAmount() {
+ amount.switchToLiteral();
+ }
+ public void switchToLiteralAccountName() {
+ accountName.switchToLiteral();
+ }
+ public void switchToLiteralAccountComment() {
+ accountComment.switchToLiteral();
+ }
+ public PatternAccount toDBO(@NonNull Long patternId) {
+ PatternAccount result = new PatternAccount((id <= 0L) ? null : id, patternId, position);
+
+ if (accountName.hasLiteralValue())
+ result.setAccountName(accountName.getValue());
+ else
+ result.setAccountNameMatchGroup(accountName.getMatchGroup());
+
+ if (accountComment.hasLiteralValue())
+ result.setAccountComment(accountComment.getValue());
+ else
+ result.setAccountCommentMatchGroup(accountComment.getMatchGroup());
+
+ if (amount.hasLiteralValue())
+ result.setAmount(amount.getValue());
+ else
+ result.setAmountMatchGroup(amount.getMatchGroup());
+
+ return result;
+ }
+ }
+
+ public static class Header extends PatternDetailsItem {
+ private String pattern = "";
+ private String testText = "";
+ private Pattern compiledPattern;
+ private String patternError;
+ private String name = "";
+ private PossiblyMatchedValue<String> transactionDescription =
+ PossiblyMatchedValue.withLiteralString("");
+ private PossiblyMatchedValue<String> transactionComment =
+ PossiblyMatchedValue.withLiteralString("");
+ private PossiblyMatchedValue<Short> dateYear =
+ PossiblyMatchedValue.withLiteralShort((short) 0);
+ private PossiblyMatchedValue<Short> dateMonth =
+ PossiblyMatchedValue.withLiteralShort((short) 0);
+ private PossiblyMatchedValue<Short> dateDay =
+ PossiblyMatchedValue.withLiteralShort((short) 0);
+ private Header() {
+ super(Type.HEADER, -1, -1);
+ }
+ public Header(Header origin) {
+ this();
+ name = origin.name;
+ testText = origin.testText;
+ setPattern(origin.pattern);
+
+ transactionDescription = new PossiblyMatchedValue<>(origin.transactionDescription);
+ transactionComment = new PossiblyMatchedValue<>(origin.transactionComment);
+
+ dateYear = new PossiblyMatchedValue<>(origin.dateYear);
+ dateMonth = new PossiblyMatchedValue<>(origin.dateMonth);
+ dateDay = new PossiblyMatchedValue<>(origin.dateDay);
+ }
+ public String getName() {
+ return name;
+ }
+ public void setName(String name) {
+ this.name = name;
+ }
+ public String getPattern() {
+ return pattern;
+ }
+ public void setPattern(String pattern) {
+ this.pattern = pattern;
+ if (pattern != null) {
+ try {
+ this.compiledPattern = Pattern.compile(pattern);
+ this.patternError = null;
+ }
+ catch (PatternSyntaxException e) {
+ this.compiledPattern = null;
+ this.patternError = e.getMessage();
+ }
+ }
+ else {
+ patternError = "Missing pattern";
+ }
+ }
+ @NonNull
+ @Override
+ public String toString() {
+ return super.toString() +
+ String.format(" name[%s] pat[%s] test[%s]", name, pattern, testText);
+ }
+ public String getTestText() {
+ return testText;
+ }
+ public void setTestText(String testText) {
+ this.testText = testText;
+ }
+ public String getTransactionDescription() {
+ return transactionDescription.getValue();
+ }
+ public void setTransactionDescription(String transactionDescription) {
+ this.transactionDescription.setValue(transactionDescription);
+ }
+ public String getTransactionComment() {
+ return transactionComment.getValue();
+ }
+ public void setTransactionComment(String transactionComment) {
+ this.transactionComment.setValue(transactionComment);
+ }
+ public short getDateYear() {
+ return dateYear.getValue();
+ }
+ public void setDateYear(short dateYear) {
+ this.dateYear.setValue(dateYear);
+ }
+ public short getDateMonth() {
+ return dateMonth.getValue();
+ }
+ public void setDateMonth(short dateMonth) {
+ this.dateMonth.setValue(dateMonth);
+ }
+ public short getDateDay() {
+ return dateDay.getValue();
+ }
+ public void setDateDay(short dateDay) {
+ this.dateDay.setValue(dateDay);
+ }
+ public int getDateYearMatchGroup() {
+ return dateYear.getMatchGroup();
+ }
+ public void setDateYearMatchGroup(int dateYearMatchGroup) {
+ this.dateYear.setMatchGroup(dateYearMatchGroup);
+ }
+ public int getDateMonthMatchGroup() {
+ return dateMonth.getMatchGroup();
+ }
+ public void setDateMonthMatchGroup(int dateMonthMatchGroup) {
+ this.dateMonth.setMatchGroup(dateMonthMatchGroup);
+ }
+ public int getDateDayMatchGroup() {
+ return dateDay.getMatchGroup();
+ }
+ public void setDateDayMatchGroup(int dateDayMatchGroup) {
+ this.dateDay.setMatchGroup(dateDayMatchGroup);
+ }
+ public boolean hasLiteralDateYear() {
+ return dateYear.hasLiteralValue();
+ }
+ public boolean hasLiteralDateMonth() {
+ return dateMonth.hasLiteralValue();
+ }
+ public boolean hasLiteralDateDay() {
+ return dateDay.hasLiteralValue();
+ }
+ public boolean hasLiteralTransactionDescription() { return transactionDescription.hasLiteralValue(); }
+ public boolean hasLiteralTransactionComment() { return transactionComment.hasLiteralValue(); }
+ public String getProblem(@NonNull Resources r, int patternGroupCount) {
+ if (patternError != null)
+ return r.getString(R.string.pattern_has_errors) + ": " + patternError;
+ if (Misc.emptyIsNull(pattern) == null)
+ return r.getString(R.string.pattern_is_empty);
+
+ if (!dateYear.hasLiteralValue() && compiledPattern != null &&
+ (dateDay.getMatchGroup() < 1 || dateDay.getMatchGroup() > patternGroupCount))
+ return r.getString(R.string.invalid_matching_group_number);
+
+ if (!dateMonth.hasLiteralValue() && compiledPattern != null &&
+ (dateMonth.getMatchGroup() < 1 || dateMonth.getMatchGroup() > patternGroupCount))
+ return r.getString(R.string.invalid_matching_group_number);
+
+ if (!dateDay.hasLiteralValue() && compiledPattern != null &&
+ (dateDay.getMatchGroup() < 1 || dateDay.getMatchGroup() > patternGroupCount))
+ return r.getString(R.string.invalid_matching_group_number);
+
+ return null;
+ }
+
+ public boolean equalContents(Header o) {
+ if (!dateDay.equals(o.dateDay))
+ return false;
+ if (!dateMonth.equals(o.dateMonth))
+ return false;
+ if (!dateYear.equals(o.dateYear))
+ return false;
+ if (!transactionDescription.equals(o.transactionDescription))
+ return false;
+ if (!transactionComment.equals(o.transactionComment))
+ return true;
+
+ return Misc.equalStrings(name, o.name) && Misc.equalStrings(pattern, o.pattern) &&
+ Misc.equalStrings(testText, o.testText);
+ }
+ public String getMatchGroupText(int group) {
+ if (compiledPattern != null && testText != null) {
+ Matcher m = compiledPattern.matcher(testText);
+ if (m.matches())
+ return m.group(group);
+ }
+
+ return "ø";
+ }
+ public Pattern getCompiledPattern() {
+ return compiledPattern;
+ }
+ public void switchToLiteralTransactionDescription() {
+ transactionDescription.switchToLiteral();
+ }
+ public void switchToLiteralTransactionComment() {
+ transactionComment.switchToLiteral();
+ }
+ public int getTransactionDescriptionMatchGroup() {
+ return transactionDescription.getMatchGroup();
+ }
+ public void setTransactionDescriptionMatchGroup(short group) {
+ transactionDescription.setMatchGroup(group);
+ }
+ public int getTransactionCommentMatchGroup() {
+ return transactionComment.getMatchGroup();
+ }
+ public void setTransactionCommentMatchGroup(short group) {
+ transactionComment.setMatchGroup(group);
+ }
+ public void switchToLiteralDateYear() {
+ dateYear.switchToLiteral();
+ }
+ public void switchToLiteralDateMonth() {
+ dateMonth.switchToLiteral();
+ }
+ public void switchToLiteralDateDay() { dateDay.switchToLiteral(); }
+ public PatternHeader toDBO() {
+ PatternHeader result =
+ new PatternHeader((id <= 0) ? null : id, name, position, pattern);
+ if (transactionDescription.hasLiteralValue())
+ result.setTransactionDescription(transactionDescription.getValue());
+ else
+ result.setTransactionDescriptionMatchGroup(transactionDescription.getMatchGroup());
+
+ if (transactionComment.hasLiteralValue())
+ result.setTransactionComment(transactionComment.getValue());
+ else
+ result.setTransactionCommentMatchGroup(transactionComment.getMatchGroup());
+
+ return result;
+ }
+ }
+}
--- /dev/null
+/*
+ * Copyright © 2021 Damyan Ivanov.
+ * This file is part of MoLe.
+ * MoLe is free software: you can distribute it and/or modify it
+ * under the term of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your opinion), any later version.
+ *
+ * MoLe is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License terms for details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with MoLe. If not, see <https://www.gnu.org/licenses/>.
+ */
+
+package net.ktnx.mobileledger.ui;
+
+public interface OnSourceSelectedListener {
+ void onSourceSelected(boolean literal, short group);
+}
--- /dev/null
+/*
+ * Copyright © 2019 Damyan Ivanov.
+ * This file is part of MoLe.
+ * MoLe is free software: you can distribute it and/or modify it
+ * under the term of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your opinion), any later version.
+ *
+ * MoLe is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License terms for details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with MoLe. If not, see <https://www.gnu.org/licenses/>.
+ */
+
+package net.ktnx.mobileledger.ui;
+
+import android.app.Dialog;
+import android.content.Context;
+import android.os.Bundle;
+import android.view.LayoutInflater;
+import android.view.View;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import androidx.annotation.StringRes;
+import androidx.appcompat.app.AppCompatDialogFragment;
+import androidx.lifecycle.ViewModelProvider;
+import androidx.recyclerview.widget.GridLayoutManager;
+import androidx.recyclerview.widget.LinearLayoutManager;
+import androidx.recyclerview.widget.RecyclerView;
+
+import net.ktnx.mobileledger.R;
+import net.ktnx.mobileledger.databinding.FragmentPatternDetailSourceSelectorListBinding;
+import net.ktnx.mobileledger.model.PatternDetailSource;
+import net.ktnx.mobileledger.utils.Logger;
+import net.ktnx.mobileledger.utils.Misc;
+
+import java.util.ArrayList;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
+/**
+ * A fragment representing a list of Items.
+ * <p/>
+ * Activities containing this fragment MUST implement the {@link OnSourceSelectedListener}
+ * interface.
+ */
+public class PatternDetailSourceSelectorFragment extends AppCompatDialogFragment
+ implements OnSourceSelectedListener {
+
+ public static final int DEFAULT_COLUMN_COUNT = 1;
+ public static final String ARG_COLUMN_COUNT = "column-count";
+ public static final String ARG_PATTERN = "pattern";
+ public static final String ARG_TEST_TEXT = "test-text";
+ private int mColumnCount = DEFAULT_COLUMN_COUNT;
+ private ArrayList<PatternDetailSource> mSources;
+ private PatternDetailSourceSelectorModel model;
+ private OnSourceSelectedListener onSourceSelectedListener;
+ private @StringRes
+ int mPatternProblem;
+
+ /**
+ * Mandatory empty constructor for the fragment manager to instantiate the
+ * fragment (e.g. upon screen orientation changes).
+ */
+ public PatternDetailSourceSelectorFragment() {
+ }
+ @SuppressWarnings("unused")
+ public static PatternDetailSourceSelectorFragment newInstance() {
+ return newInstance(DEFAULT_COLUMN_COUNT, null, null);
+ }
+ public static PatternDetailSourceSelectorFragment newInstance(int columnCount,
+ @Nullable String pattern,
+ @Nullable String testText) {
+ PatternDetailSourceSelectorFragment fragment = new PatternDetailSourceSelectorFragment();
+ Bundle args = new Bundle();
+ args.putInt(ARG_COLUMN_COUNT, columnCount);
+ if (pattern != null)
+ args.putString(ARG_PATTERN, pattern);
+ if (testText != null)
+ args.putString(ARG_TEST_TEXT, testText);
+ fragment.setArguments(args);
+ return fragment;
+ }
+ @Override
+ public void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+
+ if (getArguments() != null) {
+ mColumnCount = getArguments().getInt(ARG_COLUMN_COUNT, DEFAULT_COLUMN_COUNT);
+ final String patternText = getArguments().getString(ARG_PATTERN);
+ final String testText = getArguments().getString(ARG_TEST_TEXT);
+ if (Misc.emptyIsNull(patternText) == null) {
+ mPatternProblem = R.string.missing_pattern_error;
+ }
+ else {
+ if (Misc.emptyIsNull(testText) == null) {
+ mPatternProblem = R.string.missing_test_text;
+ }
+ else {
+ Pattern pattern = Pattern.compile(patternText);
+ Matcher matcher = pattern.matcher(testText);
+ Logger.debug("patterns",
+ String.format("Trying to match pattern '%s' against text '%s'",
+ patternText, testText));
+ if (matcher.matches()) {
+ if (matcher.groupCount() >= 0) {
+ ArrayList<PatternDetailSource> list = new ArrayList<>();
+ for (short g = 1; g <= matcher.groupCount(); g++) {
+ list.add(new PatternDetailSource(g, matcher.group(g)));
+ }
+ mSources = list;
+ }
+ else {
+ mPatternProblem = R.string.pattern_without_groups;
+ }
+ }
+ else {
+ mPatternProblem = R.string.pattern_does_not_match;
+ }
+ }
+ }
+ }
+ }
+ @NonNull
+ @Override
+ public Dialog onCreateDialog(@Nullable Bundle savedInstanceState) {
+ Context context = requireContext();
+ Dialog csd = new Dialog(context);
+ FragmentPatternDetailSourceSelectorListBinding b =
+ FragmentPatternDetailSourceSelectorListBinding.inflate(
+ LayoutInflater.from(context));
+ csd.setContentView(b.getRoot());
+ csd.setTitle(R.string.choose_pattern_detail_source_label);
+
+ if (mSources != null && !mSources.isEmpty()) {
+ RecyclerView recyclerView = b.list;
+
+ if (mColumnCount <= 1) {
+ recyclerView.setLayoutManager(new LinearLayoutManager(context));
+ }
+ else {
+ recyclerView.setLayoutManager(new GridLayoutManager(context, mColumnCount));
+ }
+ model = new ViewModelProvider(this).get(PatternDetailSourceSelectorModel.class);
+ if (onSourceSelectedListener != null)
+ model.setOnSourceSelectedListener(onSourceSelectedListener);
+ model.setSourcesList(mSources);
+
+ PatternDetailSourceSelectorRecyclerViewAdapter adapter =
+ new PatternDetailSourceSelectorRecyclerViewAdapter();
+ model.groups.observe(this, adapter::submitList);
+
+ recyclerView.setAdapter(adapter);
+ adapter.setSourceSelectedListener(this);
+ }
+ else {
+ b.list.setVisibility(View.GONE);
+ b.patternError.setText(mPatternProblem);
+ b.patternError.setVisibility(View.VISIBLE);
+ }
+
+ b.literalButton.setOnClickListener(v -> onSourceSelected(true, (short) -1));
+
+ return csd;
+ }
+ public void setOnSourceSelectedListener(OnSourceSelectedListener listener) {
+ onSourceSelectedListener = listener;
+
+ if (model != null)
+ model.setOnSourceSelectedListener(listener);
+ }
+ public void resetOnSourceSelectedListener() {
+ model.resetOnSourceSelectedListener();
+ }
+ @Override
+ public void onSourceSelected(boolean literal, short group) {
+ if (model != null)
+ model.triggerOnSourceSelectedListener(literal, group);
+ if (onSourceSelectedListener != null)
+ onSourceSelectedListener.onSourceSelected(literal, group);
+
+ dismiss();
+ }
+}
\ No newline at end of file
--- /dev/null
+/*
+ * Copyright © 2020 Damyan Ivanov.
+ * This file is part of MoLe.
+ * MoLe is free software: you can distribute it and/or modify it
+ * under the term of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your opinion), any later version.
+ *
+ * MoLe is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License terms for details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with MoLe. If not, see <https://www.gnu.org/licenses/>.
+ */
+
+package net.ktnx.mobileledger.ui;
+
+import androidx.lifecycle.MutableLiveData;
+import androidx.lifecycle.ViewModel;
+
+import net.ktnx.mobileledger.model.PatternDetailSource;
+
+import java.util.ArrayList;
+import java.util.List;
+
+public class PatternDetailSourceSelectorModel extends ViewModel {
+ public final MutableLiveData<List<PatternDetailSource>> groups = new MutableLiveData<>();
+ private OnSourceSelectedListener selectionListener;
+ public PatternDetailSourceSelectorModel() {
+ }
+ void setOnSourceSelectedListener(OnSourceSelectedListener listener) {
+ selectionListener = listener;
+ }
+ void resetOnSourceSelectedListener() {
+ selectionListener = null;
+ }
+ void triggerOnSourceSelectedListener(boolean literal, short group) {
+ if (selectionListener != null)
+ selectionListener.onSourceSelected(literal, group);
+ }
+ public void setSourcesList(ArrayList<PatternDetailSource> mSources) {
+ groups.setValue(mSources);
+ }
+}
--- /dev/null
+/*
+ * Copyright © 2019 Damyan Ivanov.
+ * This file is part of MoLe.
+ * MoLe is free software: you can distribute it and/or modify it
+ * under the term of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your opinion), any later version.
+ *
+ * MoLe is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License terms for details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with MoLe. If not, see <https://www.gnu.org/licenses/>.
+ */
+
+package net.ktnx.mobileledger.ui;
+
+import android.view.LayoutInflater;
+import android.view.ViewGroup;
+
+import androidx.recyclerview.widget.ListAdapter;
+import androidx.recyclerview.widget.RecyclerView;
+
+import net.ktnx.mobileledger.databinding.FragmentPatternDetailSourceSelectorBinding;
+import net.ktnx.mobileledger.model.PatternDetailSource;
+
+import org.jetbrains.annotations.NotNull;
+
+/**
+ * {@link RecyclerView.Adapter} that can display a {@link PatternDetailSource} and makes a call
+ * to the
+ * specified {@link OnSourceSelectedListener}.
+ */
+public class PatternDetailSourceSelectorRecyclerViewAdapter extends
+ ListAdapter<PatternDetailSource,
+ PatternDetailSourceSelectorRecyclerViewAdapter.ViewHolder> {
+
+ private OnSourceSelectedListener sourceSelectedListener;
+ public PatternDetailSourceSelectorRecyclerViewAdapter() {
+ super(PatternDetailSource.DIFF_CALLBACK);
+ }
+ @NotNull
+ @Override
+ public ViewHolder onCreateViewHolder(@NotNull ViewGroup parent, int viewType) {
+ FragmentPatternDetailSourceSelectorBinding b =
+ FragmentPatternDetailSourceSelectorBinding.inflate(
+ LayoutInflater.from(parent.getContext()), parent, false);
+ return new ViewHolder(b);
+ }
+
+ @Override
+ public void onBindViewHolder(final ViewHolder holder, int position) {
+ holder.bindTo(getItem(position));
+ }
+ public void setSourceSelectedListener(OnSourceSelectedListener listener) {
+ this.sourceSelectedListener = listener;
+ }
+ public void resetSourceSelectedListener() {
+ sourceSelectedListener = null;
+ }
+ public void notifySourceSelected(PatternDetailSource item) {
+ if (null != sourceSelectedListener)
+ sourceSelectedListener.onSourceSelected(false, item.getGroupNumber());
+ }
+ public void notifyLiteralSelected() {
+ if (null != sourceSelectedListener)
+ sourceSelectedListener.onSourceSelected(true, (short) -1);
+ }
+ public class ViewHolder extends RecyclerView.ViewHolder {
+ private final FragmentPatternDetailSourceSelectorBinding b;
+ private PatternDetailSource mItem;
+
+ ViewHolder(FragmentPatternDetailSourceSelectorBinding binding) {
+ super(binding.getRoot());
+ b = binding;
+
+ b.getRoot()
+ .setOnClickListener(v -> notifySourceSelected(mItem));
+ }
+
+ @NotNull
+ @Override
+ public String toString() {
+ return super.toString() + " " + b.groupNumber.getText() + ": '" +
+ b.matchedText.getText() + "'";
+ }
+ void bindTo(PatternDetailSource item) {
+ mItem = item;
+ b.groupNumber.setText(String.valueOf(item.getGroupNumber()));
+ b.matchedText.setText(item.getMatchedText());
+ }
+ }
+}
--- /dev/null
+/*
+ * Copyright © 2021 Damyan Ivanov.
+ * This file is part of MoLe.
+ * MoLe is free software: you can distribute it and/or modify it
+ * under the term of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your opinion), any later version.
+ *
+ * MoLe is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License terms for details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with MoLe. If not, see <https://www.gnu.org/licenses/>.
+ */
+
+package net.ktnx.mobileledger.ui.patterns;
+
+import android.annotation.SuppressLint;
+import android.text.Editable;
+import android.text.TextWatcher;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+
+import androidx.annotation.NonNull;
+import androidx.appcompat.app.AppCompatActivity;
+import androidx.recyclerview.widget.AsyncListDiffer;
+import androidx.recyclerview.widget.DiffUtil;
+import androidx.recyclerview.widget.RecyclerView;
+
+import net.ktnx.mobileledger.R;
+import net.ktnx.mobileledger.databinding.PatternDetailsAccountBinding;
+import net.ktnx.mobileledger.databinding.PatternDetailsHeaderBinding;
+import net.ktnx.mobileledger.db.PatternBase;
+import net.ktnx.mobileledger.model.Data;
+import net.ktnx.mobileledger.model.PatternDetailsItem;
+import net.ktnx.mobileledger.ui.PatternDetailSourceSelectorFragment;
+import net.ktnx.mobileledger.ui.QRScanAbleFragment;
+import net.ktnx.mobileledger.utils.Logger;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Locale;
+import java.util.Objects;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
+class PatternDetailsAdapter extends RecyclerView.Adapter<PatternDetailsAdapter.ViewHolder> {
+ private static final String D_PATTERN_UI = "pattern-ui";
+ private final AsyncListDiffer<PatternDetailsItem> differ;
+ public PatternDetailsAdapter() {
+ super();
+ setHasStableIds(true);
+ differ = new AsyncListDiffer<>(this, new DiffUtil.ItemCallback<PatternDetailsItem>() {
+ @Override
+ public boolean areItemsTheSame(@NonNull PatternDetailsItem oldItem,
+ @NonNull PatternDetailsItem newItem) {
+ if (oldItem.getType() != newItem.getType())
+ return false;
+ if (oldItem.getType() == PatternDetailsItem.Type.HEADER)
+ return true; // only one header item, ever
+ // the rest is comparing two account row items
+ return oldItem.asAccountRowItem()
+ .getId() == newItem.asAccountRowItem()
+ .getId();
+ }
+ @SuppressLint("DiffUtilEquals")
+ @Override
+ public boolean areContentsTheSame(@NonNull PatternDetailsItem oldItem,
+ @NonNull PatternDetailsItem newItem) {
+ if (oldItem.getType() == PatternDetailsItem.Type.HEADER) {
+ PatternDetailsItem.Header oldHeader = oldItem.asHeaderItem();
+ PatternDetailsItem.Header newHeader = newItem.asHeaderItem();
+
+ return oldHeader.equalContents(newHeader);
+ }
+ else {
+ PatternDetailsItem.AccountRow oldAcc = oldItem.asAccountRowItem();
+ PatternDetailsItem.AccountRow newAcc = newItem.asAccountRowItem();
+
+ return oldAcc.equalContents(newAcc);
+ }
+ }
+ });
+ }
+ @Override
+ public long getItemId(int position) {
+ if (position == 0)
+ return -1;
+ PatternDetailsItem.AccountRow accRow = differ.getCurrentList()
+ .get(position)
+ .asAccountRowItem();
+ return accRow.getId();
+ }
+ @Override
+ public int getItemViewType(int position) {
+
+ return differ.getCurrentList()
+ .get(position)
+ .getType()
+ .toInt();
+ }
+ @NonNull
+ @Override
+ public PatternDetailsAdapter.ViewHolder onCreateViewHolder(@NonNull ViewGroup parent,
+ int viewType) {
+ final LayoutInflater inflater = LayoutInflater.from(parent.getContext());
+ switch (viewType) {
+ case PatternDetailsItem.TYPE.header:
+ return new Header(PatternDetailsHeaderBinding.inflate(inflater, parent, false));
+ case PatternDetailsItem.TYPE.accountItem:
+ return new AccountRow(
+ PatternDetailsAccountBinding.inflate(inflater, parent, false));
+ default:
+ throw new IllegalStateException("Unsupported view type " + viewType);
+ }
+ }
+ @Override
+ public void onBindViewHolder(@NonNull PatternDetailsAdapter.ViewHolder holder, int position) {
+ PatternDetailsItem item = differ.getCurrentList()
+ .get(position);
+ holder.bind(item);
+ }
+ @Override
+ public int getItemCount() {
+ return differ.getCurrentList()
+ .size();
+ }
+ public void setPatternItems(List<PatternBase> items) {
+ ArrayList<PatternDetailsItem> list = new ArrayList<>();
+ for (PatternBase p : items) {
+ PatternDetailsItem item = PatternDetailsItem.fromRoomObject(p);
+ list.add(item);
+ }
+ setItems(list);
+ }
+ public void setItems(List<PatternDetailsItem> items) {
+ differ.submitList(items);
+ }
+ public String getMatchGroupText(int groupNumber) {
+ PatternDetailsItem.Header header = getHeader();
+ Pattern p = header.getCompiledPattern();
+ if (p == null) return null;
+
+ Matcher m = p.matcher(header.getTestText());
+ if (m.matches() && m.groupCount() >= groupNumber)
+ return m.group(groupNumber);
+ else
+ return null;
+ }
+ protected PatternDetailsItem.Header getHeader() {
+ return differ.getCurrentList()
+ .get(0)
+ .asHeaderItem();
+ }
+
+ private enum HeaderDetail {DESCRIPTION, COMMENT, DATE_YEAR, DATE_MONTH, DATE_DAY}
+
+ private enum AccDetail {ACCOUNT, COMMENT, AMOUNT}
+
+ public abstract static class ViewHolder extends RecyclerView.ViewHolder {
+ protected int updateInProgress = 0;
+ ViewHolder(@NonNull View itemView) {
+ super(itemView);
+ }
+ protected void startUpdate() {
+ updateInProgress++;
+ }
+ protected void finishUpdate() {
+ if (updateInProgress <= 0)
+ throw new IllegalStateException(
+ "Unexpected updateInProgress value " + updateInProgress);
+
+ updateInProgress--;
+ }
+ abstract void bind(PatternDetailsItem item);
+ }
+
+ public class Header extends ViewHolder {
+ private final PatternDetailsHeaderBinding b;
+ private final TextWatcher patternNameWatcher = new TextWatcher() {
+ @Override
+ public void beforeTextChanged(CharSequence s, int start, int count, int after) {}
+ @Override
+ public void onTextChanged(CharSequence s, int start, int before, int count) {}
+ @Override
+ public void afterTextChanged(Editable s) {
+ Object tag = b.patternDetailsItemHead.getTag();
+ if (tag != null) {
+ final PatternDetailsItem.Header header =
+ ((PatternDetailsItem) tag).asHeaderItem();
+ Logger.debug(D_PATTERN_UI,
+ "Storing changed pattern name " + s + "; header=" + header);
+ header.setName(String.valueOf(s));
+ }
+ }
+ };
+ private final TextWatcher patternWatcher = new TextWatcher() {
+ @Override
+ public void beforeTextChanged(CharSequence s, int start, int count, int after) {}
+ @Override
+ public void onTextChanged(CharSequence s, int start, int before, int count) {}
+ @Override
+ public void afterTextChanged(Editable s) {
+ Object tag = b.patternDetailsItemHead.getTag();
+ if (tag != null) {
+ final PatternDetailsItem.Header header =
+ ((PatternDetailsItem) tag).asHeaderItem();
+ Logger.debug(D_PATTERN_UI,
+ "Storing changed pattern " + s + "; header=" + header);
+ header.setPattern(String.valueOf(s));
+ }
+ }
+ };
+ private final TextWatcher testTextWatcher = new TextWatcher() {
+ @Override
+ public void beforeTextChanged(CharSequence s, int start, int count, int after) {}
+ @Override
+ public void onTextChanged(CharSequence s, int start, int before, int count) {}
+ @Override
+ public void afterTextChanged(Editable s) {
+ Object tag = b.patternDetailsItemHead.getTag();
+ if (tag != null) {
+ final PatternDetailsItem.Header header =
+ ((PatternDetailsItem) tag).asHeaderItem();
+ Logger.debug(D_PATTERN_UI,
+ "Storing changed test text " + s + "; header=" + header);
+ header.setTestText(String.valueOf(s));
+ }
+ }
+ };
+ private final TextWatcher transactionDescriptionWatcher = new TextWatcher() {
+ @Override
+ public void beforeTextChanged(CharSequence s, int start, int count, int after) {
+ }
+ @Override
+ public void onTextChanged(CharSequence s, int start, int before, int count) {
+
+ }
+ @Override
+ public void afterTextChanged(Editable s) {
+ PatternDetailsItem.Header header = ((PatternDetailsItem) Objects.requireNonNull(
+ b.patternDetailsItemHead.getTag())).asHeaderItem();
+ Logger.debug(D_PATTERN_UI,
+ "Storing changed transaction description " + s + "; header=" + header);
+ header.setTransactionDescription(String.valueOf(s));
+ }
+ };
+ private final TextWatcher transactionCommentWatcher = new TextWatcher() {
+ @Override
+ public void beforeTextChanged(CharSequence s, int start, int count, int after) {
+
+ }
+ @Override
+ public void onTextChanged(CharSequence s, int start, int before, int count) {
+
+ }
+ @Override
+ public void afterTextChanged(Editable s) {
+ PatternDetailsItem.Header header = ((PatternDetailsItem) Objects.requireNonNull(
+ b.patternDetailsItemHead.getTag())).asHeaderItem();
+ Logger.debug(D_PATTERN_UI,
+ "Storing changed transaction description " + s + "; header=" + header);
+ header.setTransactionComment(String.valueOf(s));
+ }
+ };
+ public Header(@NonNull PatternDetailsHeaderBinding binding) {
+ super(binding.getRoot());
+ b = binding;
+ }
+ Header(@NonNull View itemView) {
+ super(itemView);
+ throw new IllegalStateException("Should not be used");
+ }
+ private void selectHeaderDetailSource(View v, PatternDetailsItem.Header header,
+ HeaderDetail detail) {
+ Logger.debug(D_PATTERN_UI, "header is " + header);
+ PatternDetailSourceSelectorFragment sel =
+ PatternDetailSourceSelectorFragment.newInstance(1, header.getPattern(),
+ header.getTestText());
+ sel.setOnSourceSelectedListener((literal, group) -> {
+ if (literal) {
+ switch (detail) {
+ case DESCRIPTION:
+ header.switchToLiteralTransactionDescription();
+ break;
+ case COMMENT:
+ header.switchToLiteralTransactionComment();
+ break;
+ case DATE_YEAR:
+ header.switchToLiteralDateYear();
+ break;
+ case DATE_MONTH:
+ header.switchToLiteralDateMonth();
+ break;
+ case DATE_DAY:
+ header.switchToLiteralDateDay();
+ break;
+ default:
+ throw new IllegalStateException("Unexpected detail " + detail);
+ }
+ }
+ else {
+ switch (detail) {
+ case DESCRIPTION:
+ header.setTransactionDescriptionMatchGroup(group);
+ break;
+ case COMMENT:
+ header.setTransactionCommentMatchGroup(group);
+ break;
+ case DATE_YEAR:
+ header.setDateYearMatchGroup(group);
+ break;
+ case DATE_MONTH:
+ header.setDateMonthMatchGroup(group);
+ break;
+ case DATE_DAY:
+ header.setDateDayMatchGroup(group);
+ break;
+ default:
+ throw new IllegalStateException("Unexpected detail " + detail);
+ }
+ }
+
+ notifyItemChanged(getAdapterPosition());
+ });
+ final AppCompatActivity activity = (AppCompatActivity) v.getContext();
+ sel.show(activity.getSupportFragmentManager(), "pattern-details-source-selector");
+ }
+ @Override
+ void bind(PatternDetailsItem item) {
+ PatternDetailsItem.Header header = item.asHeaderItem();
+ startUpdate();
+ try {
+ Logger.debug(D_PATTERN_UI, "Binding to header " + header);
+ b.patternName.setText(header.getName());
+ b.pattern.setText(header.getPattern());
+ b.testText.setText(header.getTestText());
+
+ if (header.hasLiteralDateYear()) {
+ b.patternDetailsYearSource.setText(R.string.pattern_details_source_literal);
+ b.patternDetailsDateYear.setText(String.valueOf(header.getDateYear()));
+ b.patternDetailsDateYearLayout.setVisibility(View.VISIBLE);
+ }
+ else {
+ b.patternDetailsDateYearLayout.setVisibility(View.GONE);
+ b.patternDetailsYearSource.setText(String.format(Locale.US, "Group %d (%s)",
+ header.getDateYearMatchGroup(), getMatchGroupText(
+ header.getDateYearMatchGroup())));
+ }
+ b.patternDetailsYearSourceLabel.setOnClickListener(
+ v -> selectHeaderDetailSource(v, header, HeaderDetail.DATE_YEAR));
+ b.patternDetailsYearSource.setOnClickListener(
+ v -> selectHeaderDetailSource(v, header, HeaderDetail.DATE_YEAR));
+
+ if (header.hasLiteralDateMonth()) {
+ b.patternDetailsMonthSource.setText(R.string.pattern_details_source_literal);
+ b.patternDetailsDateMonth.setText(String.valueOf(header.getDateMonth()));
+ b.patternDetailsDateMonthLayout.setVisibility(View.VISIBLE);
+ }
+ else {
+ b.patternDetailsDateMonthLayout.setVisibility(View.GONE);
+ b.patternDetailsMonthSource.setText(String.format(Locale.US, "Group %d (%s)",
+ header.getDateMonthMatchGroup(), getMatchGroupText(
+ header.getDateMonthMatchGroup())));
+ }
+ b.patternDetailsMonthSourceLabel.setOnClickListener(
+ v -> selectHeaderDetailSource(v, header, HeaderDetail.DATE_MONTH));
+ b.patternDetailsMonthSource.setOnClickListener(
+ v -> selectHeaderDetailSource(v, header, HeaderDetail.DATE_MONTH));
+
+ if (header.hasLiteralDateDay()) {
+ b.patternDetailsDaySource.setText(R.string.pattern_details_source_literal);
+ b.patternDetailsDateDay.setText(String.valueOf(header.getDateDay()));
+ b.patternDetailsDateDayLayout.setVisibility(View.VISIBLE);
+ }
+ else {
+ b.patternDetailsDateDayLayout.setVisibility(View.GONE);
+ b.patternDetailsDaySource.setText(String.format(Locale.US, "Group %d (%s)",
+ header.getDateDayMatchGroup(), getMatchGroupText(
+ header.getDateDayMatchGroup())));
+ }
+ b.patternDetailsDaySourceLabel.setOnClickListener(
+ v -> selectHeaderDetailSource(v, header, HeaderDetail.DATE_DAY));
+ b.patternDetailsDaySource.setOnClickListener(
+ v -> selectHeaderDetailSource(v, header, HeaderDetail.DATE_DAY));
+
+ if (header.hasLiteralTransactionDescription()) {
+ b.patternTransactionDescriptionSource.setText(
+ R.string.pattern_details_source_literal);
+ b.transactionDescription.setText(header.getTransactionDescription());
+ b.transactionDescriptionLayout.setVisibility(View.VISIBLE);
+ }
+ else {
+ b.transactionDescriptionLayout.setVisibility(View.GONE);
+ b.patternTransactionDescriptionSource.setText(
+ String.format(Locale.US, "Group %d (%s)",
+ header.getTransactionDescriptionMatchGroup(), getMatchGroupText(
+ header.getTransactionDescriptionMatchGroup())));
+
+ }
+ b.patternTransactionDescriptionSourceLabel.setOnClickListener(
+ v -> selectHeaderDetailSource(v, header, HeaderDetail.DESCRIPTION));
+ b.patternTransactionDescriptionSource.setOnClickListener(
+ v -> selectHeaderDetailSource(v, header, HeaderDetail.DESCRIPTION));
+
+ if (header.hasLiteralTransactionComment()) {
+ b.patternTransactionCommentSource.setText(
+ R.string.pattern_details_source_literal);
+ b.transactionComment.setText(header.getTransactionComment());
+ b.transactionCommentLayout.setVisibility(View.VISIBLE);
+ }
+ else {
+ b.transactionCommentLayout.setVisibility(View.GONE);
+ b.patternTransactionCommentSource.setText(
+ String.format(Locale.US, "Group %d (%s)",
+ header.getTransactionCommentMatchGroup(),
+ getMatchGroupText(header.getTransactionCommentMatchGroup())));
+
+ }
+ b.patternTransactionCommentSourceLabel.setOnClickListener(
+ v -> selectHeaderDetailSource(v, header, HeaderDetail.COMMENT));
+ b.patternTransactionCommentSource.setOnClickListener(
+ v -> selectHeaderDetailSource(v, header, HeaderDetail.COMMENT));
+
+ b.patternDetailsHeadScanQrButton.setOnClickListener(this::scanTestQR);
+
+ final Object prevTag = b.patternDetailsItemHead.getTag();
+ if (!(prevTag instanceof PatternDetailsItem)) {
+ Logger.debug(D_PATTERN_UI, "Hooked text change listeners");
+
+ b.patternName.addTextChangedListener(patternNameWatcher);
+ b.pattern.addTextChangedListener(patternWatcher);
+ b.testText.addTextChangedListener(testTextWatcher);
+ b.transactionDescription.addTextChangedListener(transactionDescriptionWatcher);
+ b.transactionComment.addTextChangedListener(transactionCommentWatcher);
+ }
+
+ b.patternDetailsItemHead.setTag(item);
+ }
+ finally {
+ finishUpdate();
+ }
+ }
+ private void scanTestQR(View view) {
+ QRScanAbleFragment.triggerQRScan();
+ }
+ }
+
+ public class AccountRow extends ViewHolder {
+ private final PatternDetailsAccountBinding b;
+ public AccountRow(@NonNull PatternDetailsAccountBinding binding) {
+ super(binding.getRoot());
+ b = binding;
+ }
+ AccountRow(@NonNull View itemView) {
+ super(itemView);
+ throw new IllegalStateException("Should not be used");
+ }
+ @Override
+ void bind(PatternDetailsItem item) {
+ PatternDetailsItem.AccountRow accRow = item.asAccountRowItem();
+ if (accRow.hasLiteralAccountName()) {
+ b.patternDetailsAccountNameLayout.setVisibility(View.VISIBLE);
+ b.patternDetailsAccountName.setText(accRow.getAccountName());
+ b.patternDetailsAccountNameSource.setText(R.string.pattern_details_source_literal);
+ }
+ else {
+ b.patternDetailsAccountNameLayout.setVisibility(View.GONE);
+ b.patternDetailsAccountNameSource.setText(
+ String.format(Locale.US, "Group %d (%s)", accRow.getAccountNameMatchGroup(),
+ getMatchGroupText(accRow.getAccountNameMatchGroup())));
+ }
+
+ if (accRow.hasLiteralAccountComment()) {
+ b.patternDetailsAccountCommentLayout.setVisibility(View.VISIBLE);
+ b.patternDetailsAccountComment.setText(accRow.getAccountComment());
+ b.patternDetailsAccountCommentSource.setText(
+ R.string.pattern_details_source_literal);
+ }
+ else {
+ b.patternDetailsAccountCommentLayout.setVisibility(View.GONE);
+ b.patternDetailsAccountCommentSource.setText(
+ String.format(Locale.US, "Group %d (%s)",
+ accRow.getAccountCommentMatchGroup(),
+ getMatchGroupText(accRow.getAccountCommentMatchGroup())));
+ }
+
+ if (accRow.hasLiteralAmount()) {
+ b.patternDetailsAccountAmountSource.setText(
+ R.string.pattern_details_source_literal);
+ b.patternDetailsAccountAmount.setVisibility(View.VISIBLE);
+ b.patternDetailsAccountAmount.setText(Data.formatNumber(accRow.getAmount()));
+ }
+ else {
+ b.patternDetailsAccountAmountSource.setText(
+ String.format(Locale.US, "Group %d (%s)", accRow.getAmountMatchGroup(),
+ getMatchGroupText(accRow.getAmountMatchGroup())));
+ b.patternDetailsAccountAmountLayout.setVisibility(View.GONE);
+ }
+
+ b.patternAccountNameSourceLabel.setOnClickListener(
+ v -> selectAccountRowDetailSource(v, accRow, AccDetail.ACCOUNT));
+ b.patternDetailsAccountNameSource.setOnClickListener(
+ v -> selectAccountRowDetailSource(v, accRow, AccDetail.ACCOUNT));
+ b.patternAccountCommentSourceLabel.setOnClickListener(
+ v -> selectAccountRowDetailSource(v, accRow, AccDetail.COMMENT));
+ b.patternDetailsAccountCommentSource.setOnClickListener(
+ v -> selectAccountRowDetailSource(v, accRow, AccDetail.COMMENT));
+ b.patternAccountAmountSourceLabel.setOnClickListener(
+ v -> selectAccountRowDetailSource(v, accRow, AccDetail.AMOUNT));
+ b.patternDetailsAccountAmountSource.setOnClickListener(
+ v -> selectAccountRowDetailSource(v, accRow, AccDetail.AMOUNT));
+ }
+ private void selectAccountRowDetailSource(View v, PatternDetailsItem.AccountRow accRow,
+ AccDetail detail) {
+ final PatternDetailsItem.Header header = getHeader();
+ Logger.debug(D_PATTERN_UI, "header is " + header);
+ PatternDetailSourceSelectorFragment sel =
+ PatternDetailSourceSelectorFragment.newInstance(1, header.getPattern(),
+ header.getTestText());
+ sel.setOnSourceSelectedListener((literal, group) -> {
+ if (literal) {
+ switch (detail) {
+ case ACCOUNT:
+ accRow.switchToLiteralAccountName();
+ break;
+ case COMMENT:
+ accRow.switchToLiteralAccountComment();
+ break;
+ case AMOUNT:
+ accRow.switchToLiteralAmount();
+ break;
+ default:
+ throw new IllegalStateException("Unexpected detail " + detail);
+ }
+ }
+ else {
+ switch (detail) {
+ case ACCOUNT:
+ accRow.setAccountNameMatchGroup(group);
+ break;
+ case COMMENT:
+ accRow.setAccountCommentMatchGroup(group);
+ break;
+ case AMOUNT:
+ accRow.setAmountMatchGroup(group);
+ break;
+ default:
+ throw new IllegalStateException("Unexpected detail " + detail);
+ }
+ }
+
+ notifyItemChanged(getAdapterPosition());
+ });
+ final AppCompatActivity activity = (AppCompatActivity) v.getContext();
+ sel.show(activity.getSupportFragmentManager(), "pattern-details-source-selector");
+ }
+ }
+}
--- /dev/null
+/*
+ * Copyright © 2021 Damyan Ivanov.
+ * This file is part of MoLe.
+ * MoLe is free software: you can distribute it and/or modify it
+ * under the term of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your opinion), any later version.
+ *
+ * MoLe is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License terms for details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with MoLe. If not, see <https://www.gnu.org/licenses/>.
+ */
+
+package net.ktnx.mobileledger.ui.patterns;
+
+import android.content.Context;
+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.lifecycle.ViewModelProvider;
+import androidx.lifecycle.ViewModelStoreOwner;
+import androidx.navigation.NavController;
+import androidx.recyclerview.widget.GridLayoutManager;
+import androidx.recyclerview.widget.LinearLayoutManager;
+
+import com.google.android.material.snackbar.Snackbar;
+
+import net.ktnx.mobileledger.R;
+import net.ktnx.mobileledger.databinding.PatternDetailsFragmentBinding;
+import net.ktnx.mobileledger.ui.QRScanAbleFragment;
+import net.ktnx.mobileledger.utils.Logger;
+
+public class PatternDetailsFragment extends QRScanAbleFragment {
+ static final String ARG_PATTERN_ID = "pattern-id";
+ private static final String ARG_COLUMN_COUNT = "column-count";
+ PatternDetailsFragmentBinding b;
+ private PatternDetailsViewModel mViewModel;
+ private int mColumnCount = 1;
+ private int mPatternId = PatternDetailsViewModel.NEW_PATTERN;
+ public PatternDetailsFragment() {
+ }
+ public static PatternDetailsFragment newInstance(int columnCount, int patternId) {
+ final PatternDetailsFragment fragment = new PatternDetailsFragment();
+ Bundle args = new Bundle();
+ args.putInt(ARG_COLUMN_COUNT, columnCount);
+ if (patternId > 0)
+ args.putInt(ARG_PATTERN_ID, patternId);
+ fragment.setArguments(args);
+ return fragment;
+ }
+ @Override
+ public void onCreate(@Nullable Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+
+ final Bundle args = getArguments();
+ if (args != null) {
+ mColumnCount = args.getInt(ARG_COLUMN_COUNT, 1);
+ mPatternId = args.getInt(ARG_PATTERN_ID, PatternDetailsViewModel.NEW_PATTERN);
+ }
+ mViewModel.setPatternId(mPatternId);
+ }
+ @Override
+ public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container,
+ @Nullable Bundle savedInstanceState) {
+ b = PatternDetailsFragmentBinding.inflate(inflater);
+ Context context = b.patternDetailsRecyclerView.getContext();
+ if (mColumnCount <= 1) {
+ b.patternDetailsRecyclerView.setLayoutManager(new LinearLayoutManager(context));
+ }
+ else {
+ b.patternDetailsRecyclerView.setLayoutManager(
+ new GridLayoutManager(context, mColumnCount));
+ }
+
+
+ PatternDetailsAdapter adapter = new PatternDetailsAdapter();
+ b.patternDetailsRecyclerView.setAdapter(adapter);
+ mViewModel.getItems()
+ .observe(getViewLifecycleOwner(), adapter::setItems);
+ return b.getRoot();
+ }
+ @Override
+ public void onAttach(@NonNull Context context) {
+ super.onAttach(context);
+ NavController controller = ((PatternsActivity) context).getNavController();
+ final ViewModelStoreOwner viewModelStoreOwner =
+ controller.getViewModelStoreOwner(R.id.pattern_list_navigation);
+ mViewModel = new ViewModelProvider(viewModelStoreOwner).get(PatternDetailsViewModel.class);
+ mViewModel.setDefaultPatternName(getString(R.string.unnamed_pattern));
+ Logger.debug("flow", "PatternDetailsFragment.onAttach(): model=" + mViewModel);
+
+ }
+ @Override
+ protected void onQrScanned(String text) {
+ Logger.debug("PatDet_fr", String.format("Got scanned text '%s'", text));
+ mViewModel.setTestText(text);
+ }
+ public void onSavePattern() {
+ mViewModel.onSavePattern();
+ final Snackbar snackbar = Snackbar.make(b.getRoot(),
+ "One Save pattern action coming up soon in a fragment near you",
+ Snackbar.LENGTH_INDEFINITE);
+// snackbar.setAction("Action", v -> snackbar.dismiss());
+ snackbar.show();
+ }
+}
\ No newline at end of file
--- /dev/null
+/*
+ * Copyright © 2021 Damyan Ivanov.
+ * This file is part of MoLe.
+ * MoLe is free software: you can distribute it and/or modify it
+ * under the term of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your opinion), any later version.
+ *
+ * MoLe is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License terms for details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with MoLe. If not, see <https://www.gnu.org/licenses/>.
+ */
+
+package net.ktnx.mobileledger.ui.patterns;
+
+import android.database.Cursor;
+import android.os.AsyncTask;
+
+import androidx.annotation.NonNull;
+import androidx.lifecycle.LiveData;
+import androidx.lifecycle.MutableLiveData;
+import androidx.lifecycle.ViewModel;
+
+import net.ktnx.mobileledger.App;
+import net.ktnx.mobileledger.dao.PatternAccountDAO;
+import net.ktnx.mobileledger.dao.PatternHeaderDAO;
+import net.ktnx.mobileledger.db.DB;
+import net.ktnx.mobileledger.db.PatternAccount;
+import net.ktnx.mobileledger.db.PatternHeader;
+import net.ktnx.mobileledger.model.Currency;
+import net.ktnx.mobileledger.model.PatternDetailsItem;
+import net.ktnx.mobileledger.utils.Logger;
+import net.ktnx.mobileledger.utils.MLDB;
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+import java.util.Objects;
+
+public class PatternDetailsViewModel extends ViewModel {
+ static final int NEW_PATTERN = -1;
+ private final MutableLiveData<List<PatternDetailsItem>> items = new MutableLiveData<>();
+ private long mPatternId;
+ private String mDefaultPatternName;
+ public String getDefaultPatternName() {
+ return mDefaultPatternName;
+ }
+ public void setDefaultPatternName(String name) {
+ mDefaultPatternName = name;
+ }
+ public LiveData<List<PatternDetailsItem>> getItems() {
+ return items;
+ }
+
+ public void resetItems() {
+ items.setValue(Collections.emptyList());
+ checkItemConsistency();
+ }
+ private void checkItemConsistency() {
+ ArrayList<PatternDetailsItem> newList = new ArrayList<>(items.getValue());
+ boolean changes = false;
+ if (newList.size() < 1) {
+ final PatternDetailsItem.Header header = PatternDetailsItem.createHeader();
+ header.setName(mDefaultPatternName);
+ newList.add(header);
+ changes = true;
+ }
+
+ while (newList.size() < 3) {
+ newList.add(PatternDetailsItem.createAccountRow(newList.size() - 1));
+ changes = true;
+ }
+
+ if (changes)
+ items.setValue(newList);
+ }
+ public void loadItems(long patternId) {
+ DB db = App.getRoomDB();
+ LiveData<PatternHeader> ph = db.getPatternDAO()
+ .getPattern(patternId);
+ ArrayList<PatternDetailsItem> list = new ArrayList<>();
+
+ MLDB.queryInBackground(
+ "SELECT name, regular_expression, transaction_description, transaction_comment, " +
+ "date_year_match_group, date_month_match_group, date_day_match_group FROM " +
+ "patterns WHERE id=?", new String[]{String.valueOf(patternId)},
+ new MLDB.CallbackHelper() {
+ @Override
+ public void onDone() {
+ super.onDone();
+
+ MLDB.queryInBackground(
+ "SELECT id, position, acc, acc_match_group, currency, " +
+ "currency_match_group, amount, amount_match_group," +
+ " comment, comment_match_group FROM " +
+ "pattern_accounts WHERE pattern_id=? ORDER BY " + "position ASC",
+ new String[]{String.valueOf(patternId)}, new MLDB.CallbackHelper() {
+ @Override
+ public void onDone() {
+ super.onDone();
+ items.postValue(list);
+ }
+ @Override
+ public boolean onRow(@NonNull Cursor cursor) {
+ PatternDetailsItem.AccountRow item =
+ PatternDetailsItem.createAccountRow(
+ cursor.getInt(1));
+ list.add(item);
+
+ item.setId(cursor.getInt(0));
+
+ if (cursor.isNull(3)) {
+ item.setAccountName(cursor.getString(2));
+ }
+ else {
+ item.setAccountNameMatchGroup(cursor.getShort(3));
+ }
+
+ if (cursor.isNull(5)) {
+ final int currId = cursor.getInt(4);
+ if (currId > 0)
+ item.setCurrency(Currency.loadById(currId));
+ }
+ else {
+ item.setCurrencyMatchGroup(cursor.getShort(5));
+ }
+
+ if (cursor.isNull(7)) {
+ item.setAmount(cursor.getFloat(6));
+ }
+ else {
+ item.setAmountMatchGroup(cursor.getShort(7));
+ }
+
+ if (cursor.isNull(9)) {
+ item.setAccountComment(cursor.getString(8));
+ }
+ else {
+ item.setAccountCommentMatchGroup(cursor.getShort(9));
+ }
+
+ return true;
+ }
+ });
+ }
+ @Override
+ public boolean onRow(@NonNull Cursor cursor) {
+ PatternDetailsItem.Header header = PatternDetailsItem.createHeader();
+ header.setName(cursor.getString(0));
+ header.setPattern(cursor.getString(1));
+ header.setTransactionDescription(cursor.getString(2));
+ header.setTransactionComment(cursor.getString(3));
+ header.setDateYearMatchGroup(cursor.getShort(4));
+ header.setDateMonthMatchGroup(cursor.getShort(5));
+ header.setDateDayMatchGroup(cursor.getShort(6));
+
+ list.add(header);
+
+ return false;
+ }
+ });
+ }
+ public void setTestText(String text) {
+ List<PatternDetailsItem> list = new ArrayList<>(items.getValue());
+ PatternDetailsItem.Header header = new PatternDetailsItem.Header(list.get(0)
+ .asHeaderItem());
+ header.setTestText(text);
+ list.set(0, header);
+
+ items.setValue(list);
+ }
+ public void setPatternId(int patternId) {
+ if (mPatternId != patternId) {
+ if (patternId == NEW_PATTERN) {
+ resetItems();
+ }
+ else {
+ loadItems(patternId);
+ }
+ mPatternId = patternId;
+ }
+
+ }
+ public void onSavePattern() {
+ Logger.debug("flow", "PatternDetailsViewModel.onSavePattern(); model=" + this);
+ final List<PatternDetailsItem> list = Objects.requireNonNull(items.getValue());
+
+ AsyncTask.execute(() -> {
+ PatternDetailsItem.Header modelHeader = list.get(0)
+ .asHeaderItem();
+ PatternHeaderDAO headerDAO = App.getRoomDB()
+ .getPatternDAO();
+ PatternHeader dbHeader = modelHeader.toDBO();
+ if (mPatternId <= 0) {
+ dbHeader.setId(mPatternId = headerDAO.insert(dbHeader));
+ }
+ else
+ headerDAO.update(dbHeader);
+
+
+ PatternAccountDAO paDAO = App.getRoomDB()
+ .getPatternAccountDAO();
+ for (int i = 1; i < list.size(); i++) {
+ PatternAccount dbAccount = list.get(i)
+ .asAccountRowItem()
+ .toDBO(dbHeader.getId());
+ dbAccount.setPatternId(mPatternId);
+ if (dbAccount.getId() == null || dbAccount.getId() <= 0)
+ dbAccount.setId(paDAO.insert(dbAccount));
+ else
+ paDAO.update(dbAccount);
+ }
+ });
+ }
+}
\ No newline at end of file
import androidx.recyclerview.widget.LinearLayoutManager;
import androidx.recyclerview.widget.RecyclerView;
-import net.ktnx.mobileledger.R;
import net.ktnx.mobileledger.databinding.FragmentPatternListBinding;
+import net.ktnx.mobileledger.utils.Logger;
import org.jetbrains.annotations.NotNull;
@Override
public View onCreateView(@NotNull LayoutInflater inflater, ViewGroup container,
Bundle savedInstanceState) {
+ Logger.debug("flow", "PatternListFragment.onCreateView()");
b = FragmentPatternListBinding.inflate(inflater);
- b.toolbarLayout.setTitle(getString(R.string.title_activity_patterns));
-
- b.fab.setOnClickListener(this::fabClicked);
-
PatternsRecyclerViewAdapter modelAdapter = new PatternsRecyclerViewAdapter();
b.patternList.setAdapter(modelAdapter);
*/
public interface OnPatternListFragmentInteractionListener {
void onNewPattern();
-
+ void onSavePattern();
void onEditPattern(int id);
}
}
\ No newline at end of file
}
public void bindToItem(PatternEntry item) {
b.patternName.setText(item.getName());
+ b.editButon.setOnClickListener(v -> {
+ ((PatternsActivity) v.getContext()).onEditPattern(item.getId());
+ });
}
}
import android.os.Bundle;
import android.view.Menu;
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import androidx.appcompat.app.ActionBar;
+import androidx.lifecycle.ViewModelProvider;
+import androidx.lifecycle.ViewModelStoreOwner;
import androidx.navigation.NavController;
+import androidx.navigation.NavDestination;
import androidx.navigation.fragment.NavHostFragment;
-import com.google.android.material.snackbar.Snackbar;
-
import net.ktnx.mobileledger.R;
import net.ktnx.mobileledger.databinding.ActivityPatternsBinding;
import net.ktnx.mobileledger.ui.activity.CrashReportingActivity;
+import net.ktnx.mobileledger.utils.Logger;
import java.util.Objects;
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
b = ActivityPatternsBinding.inflate(getLayoutInflater());
- setContentView(b.fragmentContainer);
+ setContentView(b.getRoot());
+ setSupportActionBar(b.toolbar);
+ // Show the Up button in the action bar.
+ ActionBar actionBar = getSupportActionBar();
+ if (actionBar != null) {
+ actionBar.setDisplayHomeAsUpEnabled(true);
+ }
NavHostFragment navHostFragment = (NavHostFragment) Objects.requireNonNull(
getSupportFragmentManager().findFragmentById(R.id.fragment_container));
navController = navHostFragment.getNavController();
+
+ navController.addOnDestinationChangedListener(
+ new NavController.OnDestinationChangedListener() {
+ @Override
+ public void onDestinationChanged(@NonNull NavController controller,
+ @NonNull NavDestination destination,
+ @Nullable Bundle arguments) {
+ if (destination.getId() == R.id.patternListFragment) {
+ b.fabAdd.show();
+ b.fabSave.hide();
+ }
+ if (destination.getId() == R.id.patternDetailsFragment) {
+ b.fabAdd.hide();
+ b.fabSave.show();
+ }
+ }
+ });
+
+ b.toolbarLayout.setTitle(getString(R.string.title_activity_patterns));
+
+ b.fabAdd.setOnClickListener(v -> onNewPattern());
+ b.fabSave.setOnClickListener(v -> onSavePattern());
}
@Override
public void onNewPattern() {
-// navController.navigate
- final Snackbar snackbar =
- Snackbar.make(b.fragmentContainer, "New pattern action coming up soon",
- Snackbar.LENGTH_INDEFINITE);
+ navController.navigate(R.id.action_patternListFragment_to_patternDetailsFragment);
+// final Snackbar snackbar =
+// Snackbar.make(b.fragmentContainer, "New pattern action coming up soon",
+// Snackbar.LENGTH_INDEFINITE);
// snackbar.setAction("Action", v -> snackbar.dismiss());
- snackbar.show();
+// snackbar.show();
}
@Override
public void onEditPattern(int id) {
- final Snackbar snackbar =
- Snackbar.make(b.fragmentContainer, "One Edit pattern action coming up soon",
- Snackbar.LENGTH_INDEFINITE);
-// snackbar.setAction("Action", v -> snackbar.dismiss());
- snackbar.show();
+ Bundle bundle = new Bundle();
+ bundle.putInt(PatternDetailsFragment.ARG_PATTERN_ID, id);
+ navController.navigate(R.id.action_patternListFragment_to_patternDetailsFragment, bundle);
+ }
+ @Override
+ public void onSavePattern() {
+ final ViewModelStoreOwner viewModelStoreOwner =
+ navController.getViewModelStoreOwner(R.id.pattern_list_navigation);
+ PatternDetailsViewModel model =
+ new ViewModelProvider(viewModelStoreOwner).get(PatternDetailsViewModel.class);
+ Logger.debug("flow", "PatternsActivity.onSavePattern(): model=" + model);
+ model.onSavePattern();
+ navController.navigate(R.id.patternListFragment);
+ }
+ public NavController getNavController() {
+ return navController;
}
}
\ No newline at end of file
resetDefaultCommodity();
});
- FloatingActionButton fab = context.findViewById(R.id.fab);
+ FloatingActionButton fab = context.findViewById(R.id.fabAdd);
fab.setOnClickListener(v -> onSaveFabClicked());
hookTextChangeSyncRoutine(binding.profileName, model::setProfileName);
~ along with MoLe. If not, see <https://www.gnu.org/licenses/>.
-->
-<androidx.fragment.app.FragmentContainerView xmlns:android="http://schemas.android.com/apk/res/android"
+<androidx.coordinatorlayout.widget.CoordinatorLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
- android:id="@+id/fragment_container"
- android:name="androidx.navigation.fragment.NavHostFragment"
+ xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
- app:defaultNavHost="true"
- app:navGraph="@navigation/pattern_list_navigation"
- />
\ No newline at end of file
+ tools:context=".ui.patterns.PatternsActivity"
+ >
+
+ <com.google.android.material.appbar.AppBarLayout
+ android:id="@+id/appbar"
+ android:layout_width="match_parent"
+ android:layout_height="@dimen/app_bar_height"
+ android:fitsSystemWindows="true"
+ android:theme="@style/ThemeOverlay.AppCompat.Dark.ActionBar"
+ >
+ <com.google.android.material.appbar.CollapsingToolbarLayout
+ android:id="@+id/toolbar_layout"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ android:fitsSystemWindows="true"
+ app:contentScrim="?attr/colorPrimary"
+ app:layout_scrollFlags="scroll|exitUntilCollapsed"
+ app:toolbarId="@+id/toolbar"
+ >
+
+ <androidx.appcompat.widget.Toolbar
+ android:id="@+id/toolbar"
+ android:layout_width="match_parent"
+ android:layout_height="?attr/actionBarSize"
+ app:layout_collapseMode="pin"
+ app:popupTheme="@style/ThemeOverlay.AppCompat.DayNight"
+ />
+ </com.google.android.material.appbar.CollapsingToolbarLayout>
+ </com.google.android.material.appbar.AppBarLayout>
+
+ <androidx.core.widget.NestedScrollView
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ app:layout_behavior="@string/appbar_scrolling_view_behavior"
+ >
+ <androidx.fragment.app.FragmentContainerView
+ android:id="@+id/fragment_container"
+ android:name="androidx.navigation.fragment.NavHostFragment"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ app:defaultNavHost="true"
+ app:navGraph="@navigation/pattern_list_navigation"
+ />
+ </androidx.core.widget.NestedScrollView>
+
+ <com.google.android.material.floatingactionbutton.FloatingActionButton
+ android:id="@+id/fabAdd"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_gravity="bottom|end"
+ android:layout_margin="@dimen/fab_margin"
+ app:srcCompat="@drawable/ic_add_white_24dp"
+ android:contentDescription="@string/add_button_description"
+ />
+
+ <com.google.android.material.floatingactionbutton.FloatingActionButton
+ android:id="@+id/fabSave"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_gravity="bottom|end"
+ android:layout_margin="@dimen/fab_margin"
+ app:srcCompat="@drawable/ic_save_white_24dp"
+ android:contentDescription="@string/save_button_description"
+ />
+
+</androidx.coordinatorlayout.widget.CoordinatorLayout>
app:layout_behavior="@string/appbar_scrolling_view_behavior" />
<com.google.android.material.floatingactionbutton.FloatingActionButton
- android:id="@+id/fab"
+ android:id="@+id/fabAdd"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center_vertical|start"
--- /dev/null
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ ~ Copyright © 2021 Damyan Ivanov.
+ ~ This file is part of MoLe.
+ ~ MoLe is free software: you can distribute it and/or modify it
+ ~ under the term of the GNU General Public License as published by
+ ~ the Free Software Foundation, either version 3 of the License, or
+ ~ (at your opinion), any later version.
+ ~
+ ~ MoLe is distributed in the hope that it will be useful,
+ ~ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ ~ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ ~ GNU General Public License terms for details.
+ ~
+ ~ You should have received a copy of the GNU General Public License
+ ~ along with MoLe. If not, see <https://www.gnu.org/licenses/>.
+ -->
+
+<androidx.recyclerview.widget.RecyclerView 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:id="@+id/list"
+ android:name="net.ktnx.mobileledger.ui.patterns.TestItemFragment"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ android:layout_marginLeft="16dp"
+ android:layout_marginRight="16dp"
+ app:layoutManager="LinearLayoutManager"
+ tools:context=".ui.patterns.TestItemFragment"
+ tools:listitem="@layout/pattern_details_header"
+ />
\ No newline at end of file
--- /dev/null
+<?xml version="1.0" encoding="utf-8"?><!--
+ ~ Copyright © 2019 Damyan Ivanov.
+ ~ This file is part of MoLe.
+ ~ MoLe is free software: you can distribute it and/or modify it
+ ~ under the term of the GNU General Public License as published by
+ ~ the Free Software Foundation, either version 3 of the License, or
+ ~ (at your opinion), any later version.
+ ~
+ ~ MoLe is distributed in the hope that it will be useful,
+ ~ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ ~ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ ~ GNU General Public License terms for details.
+ ~
+ ~ You should have received a copy of the GNU General Public License
+ ~ along with MoLe. If not, see <https://www.gnu.org/licenses/>.
+ -->
+
+<androidx.constraintlayout.widget.ConstraintLayout 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:longClickable="false"
+ >
+
+ <TextView
+ android:id="@+id/group_number"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_margin="@dimen/text_margin"
+ android:gravity="end"
+ android:minWidth="20sp"
+ android:text="1"
+ android:textAppearance="?attr/textAppearanceListItem"
+ app:layout_constraintStart_toStartOf="parent"
+ app:layout_constraintTop_toTopOf="parent"
+ tools:ignore="HardcodedText"
+ />
+ <TextView
+ android:id="@+id/colon"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_marginTop="@dimen/text_margin"
+ android:text=":"
+ android:textAppearance="?attr/textAppearanceListItem"
+ app:layout_constraintStart_toEndOf="@id/group_number"
+ app:layout_constraintTop_toTopOf="parent"
+ tools:ignore="HardcodedText"
+ />
+ <TextView
+ android:id="@+id/matched_text"
+ android:layout_width="0dp"
+ android:layout_height="wrap_content"
+ android:layout_margin="@dimen/text_margin"
+ android:textAppearance="?attr/textAppearanceListItem"
+ app:layout_constraintEnd_toEndOf="parent"
+ app:layout_constraintStart_toEndOf="@id/colon"
+ app:layout_constraintTop_toTopOf="parent"
+ />
+</androidx.constraintlayout.widget.ConstraintLayout>
--- /dev/null
+<?xml version="1.0" encoding="utf-8"?><!--
+ ~ Copyright © 2020 Damyan Ivanov.
+ ~ This file is part of MoLe.
+ ~ MoLe is free software: you can distribute it and/or modify it
+ ~ under the term of the GNU General Public License as published by
+ ~ the Free Software Foundation, either version 3 of the License, or
+ ~ (at your opinion), any later version.
+ ~
+ ~ MoLe is distributed in the hope that it will be useful,
+ ~ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ ~ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ ~ GNU General Public License terms for details.
+ ~
+ ~ You should have received a copy of the GNU General Public License
+ ~ along with MoLe. If not, see <https://www.gnu.org/licenses/>.
+ -->
+
+<androidx.constraintlayout.widget.ConstraintLayout 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="match_parent"
+ android:animateLayoutChanges="true"
+ android:minWidth="60dp"
+ android:padding="@dimen/text_margin"
+ app:layout_constraintWidth_min="60dp"
+ >
+
+ <com.google.android.material.textview.MaterialTextView
+ android:id="@+id/label"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_marginBottom="@dimen/text_margin"
+ android:text="@string/choose_pattern_detail_source_label"
+ android:textSize="18sp"
+ app:layout_constraintBottom_toTopOf="@id/list"
+ app:layout_constraintEnd_toEndOf="parent"
+ app:layout_constraintStart_toStartOf="parent"
+ app:layout_constraintTop_toTopOf="parent"
+ />
+
+ <androidx.recyclerview.widget.RecyclerView
+ android:id="@+id/list"
+ android:name="net.ktnx.mobileledger.ui.PatternDetailSourceSelectorFragment"
+ android:layout_width="0dp"
+ android:layout_height="0dp"
+ app:layout_constraintWidth_min="50dp"
+ app:layout_constraintHeight_min="150dp"
+ android:layout_marginLeft="@dimen/activity_horizontal_margin"
+ android:layout_marginRight="@dimen/activity_horizontal_margin"
+ android:minHeight="100dp"
+ app:layoutManager="LinearLayoutManager"
+ app:layout_constraintBottom_toTopOf="@id/pattern_error"
+ app:layout_constraintEnd_toEndOf="parent"
+ app:layout_constraintStart_toStartOf="parent"
+ app:layout_constraintTop_toBottomOf="@id/label"
+ tools:context="net.ktnx.mobileledger.ui.CurrencySelectorFragment"
+ tools:listitem="@layout/fragment_pattern_detail_source_selector"
+ />
+ <TextView
+ android:id="@+id/pattern_error"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ app:layout_constraintBottom_toTopOf="@id/literal_button"
+ app:layout_constraintEnd_toEndOf="parent"
+ app:layout_constraintStart_toStartOf="parent"
+ app:layout_constraintTop_toBottomOf="@id/list"
+ />
+ <com.google.android.material.button.MaterialButton
+ android:id="@+id/literal_button"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_marginTop="@dimen/text_margin"
+ android:text="@string/pattern_details_source_literal"
+ app:layout_constraintBottom_toBottomOf="parent"
+ app:layout_constraintEnd_toEndOf="parent"
+ />
+</androidx.constraintlayout.widget.ConstraintLayout>
\ No newline at end of file
~ along with MoLe. If not, see <https://www.gnu.org/licenses/>.
-->
-<androidx.coordinatorlayout.widget.CoordinatorLayout xmlns:android="http://schemas.android.com/apk/res/android"
- xmlns:app="http://schemas.android.com/apk/res-auto"
+<androidx.recyclerview.widget.RecyclerView xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
+ android:id="@+id/pattern_list"
android:layout_width="match_parent"
android:layout_height="match_parent"
- android:fitsSystemWindows="true"
+ android:background="#ff808000"
tools:context=".ui.patterns.PatternsActivity"
- >
-
- <com.google.android.material.appbar.AppBarLayout
- android:id="@+id/app_bar"
- android:layout_width="match_parent"
- android:layout_height="@dimen/app_bar_height"
- android:fitsSystemWindows="true"
- >
-
- <com.google.android.material.appbar.CollapsingToolbarLayout
- android:id="@+id/toolbar_layout"
- android:layout_width="match_parent"
- android:layout_height="match_parent"
- android:fitsSystemWindows="true"
- app:contentScrim="?attr/colorPrimary"
- app:layout_scrollFlags="scroll|exitUntilCollapsed"
- app:toolbarId="@+id/toolbar"
- >
-
- <androidx.appcompat.widget.Toolbar
- android:id="@+id/toolbar"
- android:layout_width="match_parent"
- android:layout_height="?attr/actionBarSize"
- app:layout_collapseMode="pin"
- app:popupTheme="@style/AppTheme.PopupOverlay"
- />
-
- </com.google.android.material.appbar.CollapsingToolbarLayout>
- </com.google.android.material.appbar.AppBarLayout>
-
- <androidx.core.widget.NestedScrollView
- android:layout_width="match_parent"
- android:layout_height="match_parent"
- app:layout_behavior="@string/appbar_scrolling_view_behavior"
- tools:context=".ui.patterns.PatternsActivity"
- tools:showIn="@layout/activity_patterns"
- >
-
- <androidx.recyclerview.widget.RecyclerView
- android:id="@+id/pattern_list"
- android:layout_width="match_parent"
- android:layout_height="wrap_content"
- />
-
- </androidx.core.widget.NestedScrollView>
-
- <com.google.android.material.floatingactionbutton.FloatingActionButton
- android:id="@+id/fab"
- android:layout_width="wrap_content"
- android:layout_height="wrap_content"
- android:layout_margin="@dimen/fab_margin"
- app:layout_anchor="@id/pattern_list"
- app:layout_anchorGravity="top|end"
- app:layout_behavior="com.google.android.material.floatingactionbutton.FloatingActionButton$Behavior"
- app:srcCompat="@drawable/ic_add_white_24dp"
- />
-
-</androidx.coordinatorlayout.widget.CoordinatorLayout>
\ No newline at end of file
+ />
--- /dev/null
+<?xml version="1.0" encoding="utf-8"?><!--
+ ~ Copyright © 2021 Damyan Ivanov.
+ ~ This file is part of MoLe.
+ ~ MoLe is free software: you can distribute it and/or modify it
+ ~ under the term of the GNU General Public License as published by
+ ~ the Free Software Foundation, either version 3 of the License, or
+ ~ (at your opinion), any later version.
+ ~
+ ~ MoLe is distributed in the hope that it will be useful,
+ ~ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ ~ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ ~ GNU General Public License terms for details.
+ ~
+ ~ You should have received a copy of the GNU General Public License
+ ~ along with MoLe. If not, see <https://www.gnu.org/licenses/>.
+ -->
+
+<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:app="http://schemas.android.com/apk/res-auto"
+ android:id="@+id/pattern_details_item_account_row"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:paddingHorizontal="@dimen/text_margin"
+ >
+ <TextView
+ android:id="@+id/pattern_account_label"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ android:gravity="end"
+ android:paddingTop="@dimen/text_margin"
+ android:text="@string/pattern_details_account_row_label"
+ app:drawableBottomCompat="@drawable/dashed_border_8dp"
+ />
+ <TextView
+ android:id="@+id/pattern_account_name_source_label"
+ android:layout_width="0dp"
+ android:layout_height="match_parent"
+ android:paddingTop="@dimen/text_margin"
+ android:text="@string/account_name_source_label"
+ android:textAppearance="?attr/textAppearanceListItem"
+ app:layout_constraintEnd_toEndOf="parent"
+ app:layout_constraintStart_toStartOf="parent"
+ app:layout_constraintTop_toBottomOf="@id/pattern_account_label"
+ />
+ <TextView
+ android:id="@+id/pattern_details_account_name_source"
+ android:layout_width="0dp"
+ android:layout_height="wrap_content"
+ android:minWidth="100dp"
+ android:textAppearance="?attr/textAppearanceListItemSecondary"
+ app:layout_constraintEnd_toEndOf="parent"
+ app:layout_constraintStart_toStartOf="parent"
+ app:layout_constraintTop_toBottomOf="@id/pattern_account_name_source_label"
+ />
+ <com.google.android.material.textfield.TextInputLayout
+ android:id="@+id/pattern_details_account_name_layout"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:layout_marginHorizontal="@dimen/text_margin"
+ android:textAppearance="?attr/textAppearanceListItem"
+ app:layout_constraintEnd_toEndOf="parent"
+ app:layout_constraintStart_toStartOf="parent"
+ app:layout_constraintTop_toBottomOf="@id/pattern_details_account_name_source"
+ >
+ <com.google.android.material.textfield.TextInputEditText
+ android:id="@+id/pattern_details_account_name"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:hint="@string/pattern_details_account_name_label"
+ android:inputType="text"
+ />
+ </com.google.android.material.textfield.TextInputLayout>
+
+ <TextView
+ android:id="@+id/pattern_account_comment_source_label"
+ android:layout_width="0dp"
+ android:layout_height="match_parent"
+
+ android:paddingTop="@dimen/text_margin"
+ android:text="@string/account_comment_source_label"
+ android:textAppearance="?attr/textAppearanceListItem"
+ app:layout_constraintEnd_toEndOf="parent"
+ app:layout_constraintStart_toStartOf="parent"
+ app:layout_constraintTop_toBottomOf="@id/pattern_details_account_name_layout"
+ />
+ <TextView
+ android:id="@+id/pattern_details_account_comment_source"
+ android:layout_width="0dp"
+ android:layout_height="wrap_content"
+ android:minWidth="100dp"
+ android:textAppearance="?attr/textAppearanceListItemSecondary"
+ app:layout_constraintEnd_toEndOf="parent"
+ app:layout_constraintStart_toStartOf="parent"
+ app:layout_constraintTop_toBottomOf="@id/pattern_account_comment_source_label"
+ />
+ <com.google.android.material.textfield.TextInputLayout
+ android:id="@+id/pattern_details_account_comment_layout"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:layout_marginHorizontal="@dimen/text_margin"
+ android:textAppearance="?attr/textAppearanceListItem"
+ app:layout_constraintEnd_toEndOf="parent"
+ app:layout_constraintStart_toStartOf="parent"
+ app:layout_constraintTop_toBottomOf="@id/pattern_details_account_comment_source"
+ >
+ <com.google.android.material.textfield.TextInputEditText
+ android:id="@+id/pattern_details_account_comment"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:hint="@string/pattern_details_account_comment_label"
+ android:inputType="text"
+ />
+ </com.google.android.material.textfield.TextInputLayout>
+
+ <TextView
+ android:id="@+id/pattern_account_amount_source_label"
+ android:layout_width="0dp"
+ android:layout_height="match_parent"
+ android:paddingTop="@dimen/text_margin"
+ android:text="@string/account_amount_source_label"
+ android:textAppearance="?attr/textAppearanceListItem"
+ app:layout_constraintEnd_toEndOf="parent"
+ app:layout_constraintStart_toStartOf="parent"
+ app:layout_constraintTop_toBottomOf="@id/pattern_details_account_comment_layout"
+ />
+ <TextView
+ android:id="@+id/pattern_details_account_amount_source"
+ android:layout_width="0dp"
+ android:layout_height="wrap_content"
+ android:textAppearance="?attr/textAppearanceListItemSecondary"
+ app:layout_constraintEnd_toEndOf="parent"
+ app:layout_constraintStart_toStartOf="parent"
+ app:layout_constraintTop_toBottomOf="@id/pattern_account_amount_source_label"
+ />
+ <com.google.android.material.textfield.TextInputLayout
+ android:id="@+id/pattern_details_account_amount_layout"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:layout_marginHorizontal="@dimen/text_margin"
+ android:textAppearance="?attr/textAppearanceListItem"
+ app:layout_constraintEnd_toEndOf="parent"
+ app:layout_constraintStart_toStartOf="parent"
+ app:layout_constraintTop_toBottomOf="@id/pattern_details_account_amount_source"
+ >
+ <com.google.android.material.textfield.TextInputEditText
+ android:id="@+id/pattern_details_account_amount"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:hint="@string/pattern_details_account_amount_label"
+ android:inputType="number|numberDecimal|numberSigned"
+ />
+ </com.google.android.material.textfield.TextInputLayout>
+
+</androidx.constraintlayout.widget.ConstraintLayout>
\ No newline at end of file
--- /dev/null
+<?xml version="1.0" encoding="utf-8"?><!--
+ ~ Copyright © 2021 Damyan Ivanov.
+ ~ This file is part of MoLe.
+ ~ MoLe is free software: you can distribute it and/or modify it
+ ~ under the term of the GNU General Public License as published by
+ ~ the Free Software Foundation, either version 3 of the License, or
+ ~ (at your opinion), any later version.
+ ~
+ ~ MoLe is distributed in the hope that it will be useful,
+ ~ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ ~ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ ~ GNU General Public License terms for details.
+ ~
+ ~ You should have received a copy of the GNU General Public License
+ ~ along with MoLe. If not, see <https://www.gnu.org/licenses/>.
+ -->
+
+<androidx.recyclerview.widget.RecyclerView xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:tools="http://schemas.android.com/tools"
+ android:id="@+id/pattern_details_recycler_view"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ tools:context=".ui.patterns.PatternDetailsFragment"
+ >
+</androidx.recyclerview.widget.RecyclerView>
\ No newline at end of file
--- /dev/null
+<?xml version="1.0" encoding="utf-8"?><!--
+ ~ Copyright © 2021 Damyan Ivanov.
+ ~ This file is part of MoLe.
+ ~ MoLe is free software: you can distribute it and/or modify it
+ ~ under the term of the GNU General Public License as published by
+ ~ the Free Software Foundation, either version 3 of the License, or
+ ~ (at your opinion), any later version.
+ ~
+ ~ MoLe is distributed in the hope that it will be useful,
+ ~ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ ~ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ ~ GNU General Public License terms for details.
+ ~
+ ~ You should have received a copy of the GNU General Public License
+ ~ along with MoLe. If not, see <https://www.gnu.org/licenses/>.
+ -->
+
+<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:app="http://schemas.android.com/apk/res-auto"
+ android:id="@+id/pattern_details_item_head"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:paddingHorizontal="@dimen/text_margin"
+ >
+ <com.google.android.material.textfield.TextInputLayout
+ android:id="@+id/pattern_name_layout"
+ android:layout_width="0dp"
+ android:layout_height="wrap_content"
+ app:layout_constraintEnd_toEndOf="parent"
+ app:layout_constraintStart_toStartOf="parent"
+ app:layout_constraintTop_toTopOf="parent"
+ >
+ <com.google.android.material.textfield.TextInputEditText
+ android:id="@+id/pattern_name"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:hint="@string/pattern_name_label"
+ android:inputType="text"
+ />
+ </com.google.android.material.textfield.TextInputLayout>
+ <com.google.android.material.textfield.TextInputLayout
+ android:id="@+id/pattern_layout"
+ android:layout_width="0dp"
+ android:layout_height="wrap_content"
+ android:textAppearance="?attr/textAppearanceListItem"
+ app:layout_constraintEnd_toEndOf="parent"
+ app:layout_constraintStart_toStartOf="parent"
+ app:layout_constraintTop_toBottomOf="@id/pattern_name_layout"
+ >
+ <com.google.android.material.textfield.TextInputEditText
+ android:id="@+id/pattern"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:hint="@string/pattern_details_pattern_label"
+ android:inputType="text"
+ />
+ </com.google.android.material.textfield.TextInputLayout>
+ <com.google.android.material.textfield.TextInputLayout
+ android:id="@+id/test_text_layout"
+ android:layout_width="0dp"
+ android:layout_height="wrap_content"
+ android:textAppearance="?attr/textAppearanceListItem"
+ app:layout_constraintEnd_toStartOf="@id/pattern_details_head_scan_qr_button"
+ app:layout_constraintStart_toStartOf="parent"
+ app:layout_constraintTop_toBottomOf="@id/pattern_layout"
+ >
+ <com.google.android.material.textfield.TextInputEditText
+ android:id="@+id/test_text"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:hint="@string/pattern_details_test_text_label"
+ android:inputType="text"
+ />
+ </com.google.android.material.textfield.TextInputLayout>
+ <ImageButton
+ android:id="@+id/pattern_details_head_scan_qr_button"
+ android:layout_width="wrap_content"
+ android:layout_height="0dp"
+ android:background="@android:color/transparent"
+ android:contentDescription="@string/scan_qr"
+ android:minWidth="@dimen/thumb_row_height"
+ app:layout_constraintBottom_toBottomOf="@id/test_text_layout"
+ app:layout_constraintEnd_toEndOf="parent"
+ app:layout_constraintTop_toTopOf="@id/test_text_layout"
+ app:srcCompat="@drawable/ic_baseline_qr_code_scanner_24"
+ app:tint="?colorPrimary"
+ />
+ <TextView
+ android:id="@+id/transaction_parameters_label"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:gravity="end"
+ android:paddingTop="@dimen/text_margin"
+ android:text="@string/pattern_transaction_parameters_label"
+ app:layout_constraintTop_toBottomOf="@id/test_text_layout"
+ />
+ <TextView
+ android:id="@+id/pattern_transaction_date_label"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:text="@string/pattern_details_date_label"
+ app:layout_constraintTop_toBottomOf="@id/transaction_parameters_label"
+ />
+ <TextView
+ android:id="@+id/pattern_details_year_source_label"
+ android:layout_width="0dp"
+ android:layout_height="wrap_content"
+ android:text="@string/pattern_details_date_year_source_label"
+ android:textAlignment="center"
+ app:layout_constraintEnd_toStartOf="@id/pattern_details_month_source_label"
+ app:layout_constraintStart_toStartOf="parent"
+ app:layout_constraintTop_toBottomOf="@id/pattern_transaction_date_label"
+ />
+ <TextView
+ android:id="@+id/pattern_details_month_source_label"
+ android:layout_width="0dp"
+ android:layout_height="wrap_content"
+ android:text="@string/pattern_details_date_month_source_label"
+ android:textAlignment="center"
+ app:layout_constraintEnd_toStartOf="@id/pattern_details_day_source_label"
+ app:layout_constraintStart_toEndOf="@id/pattern_details_year_source_label"
+ app:layout_constraintTop_toBottomOf="@id/pattern_transaction_date_label"
+ />
+ <TextView
+ android:id="@+id/pattern_details_day_source_label"
+ android:layout_width="0dp"
+ android:layout_height="wrap_content"
+ android:text="@string/pattern_details_date_day_source_label"
+ android:textAlignment="center"
+ app:layout_constraintEnd_toEndOf="parent"
+ app:layout_constraintStart_toEndOf="@id/pattern_details_month_source_label"
+ app:layout_constraintTop_toBottomOf="@id/pattern_transaction_date_label"
+ />
+ <TextView
+ android:id="@+id/pattern_details_year_source"
+ android:layout_width="0dp"
+ android:layout_height="wrap_content"
+ android:text="literal"
+ android:textAlignment="center"
+ app:layout_constraintEnd_toStartOf="@id/pattern_details_month_source"
+ app:layout_constraintStart_toStartOf="parent"
+ app:layout_constraintTop_toBottomOf="@id/pattern_details_day_source_label"
+ />
+ <TextView
+ android:id="@+id/pattern_details_month_source"
+ android:layout_width="0dp"
+ android:layout_height="wrap_content"
+ android:text=""
+ android:textAlignment="center"
+ app:layout_constraintEnd_toStartOf="@id/pattern_details_day_source"
+ app:layout_constraintStart_toEndOf="@id/pattern_details_year_source"
+ app:layout_constraintTop_toBottomOf="@id/pattern_details_month_source_label"
+ />
+ <TextView
+ android:id="@+id/pattern_details_day_source"
+ android:layout_width="0dp"
+ android:layout_height="wrap_content"
+ android:text=""
+ android:textAlignment="center"
+ app:layout_constraintEnd_toEndOf="parent"
+ app:layout_constraintStart_toEndOf="@id/pattern_details_month_source"
+ app:layout_constraintTop_toBottomOf="@id/pattern_details_day_source_label"
+ />
+ <androidx.constraintlayout.widget.Barrier
+ android:id="@+id/barrier_before_date_inputs"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ app:barrierDirection="bottom"
+ app:constraint_referenced_ids="pattern_details_year_source,pattern_details_month_source,pattern_details_day_source"
+ />
+ <com.google.android.material.textfield.TextInputLayout
+ android:id="@+id/pattern_details_date_year_layout"
+ android:layout_width="0dp"
+ android:layout_height="wrap_content"
+ app:layout_constraintEnd_toStartOf="@id/pattern_details_date_month_layout"
+ app:layout_constraintStart_toStartOf="parent"
+ app:layout_constraintTop_toBottomOf="@id/barrier_before_date_inputs"
+ >
+ <com.google.android.material.textfield.TextInputEditText
+ android:id="@+id/pattern_details_date_year"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:gravity="center_horizontal"
+ android:hint="@string/date_year_hint"
+ />
+ </com.google.android.material.textfield.TextInputLayout>
+ <com.google.android.material.textfield.TextInputLayout
+ android:id="@+id/pattern_details_date_month_layout"
+ android:layout_width="0dp"
+ android:layout_height="wrap_content"
+ app:layout_constraintEnd_toStartOf="@id/pattern_details_date_day_layout"
+ app:layout_constraintStart_toEndOf="@id/pattern_details_date_year_layout"
+ app:layout_constraintTop_toBottomOf="@id/barrier_before_date_inputs"
+ >
+ <com.google.android.material.textfield.TextInputEditText
+ android:id="@+id/pattern_details_date_month"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:gravity="center_horizontal"
+ android:hint="@string/date_month_hint"
+ />
+ </com.google.android.material.textfield.TextInputLayout>
+ <com.google.android.material.textfield.TextInputLayout
+ android:id="@+id/pattern_details_date_day_layout"
+ android:layout_width="0dp"
+ android:layout_height="wrap_content"
+ app:layout_constraintBottom_toTopOf="@id/barrier_before_description"
+ app:layout_constraintEnd_toEndOf="parent"
+ app:layout_constraintStart_toEndOf="@id/pattern_details_date_month_layout"
+ app:layout_constraintTop_toBottomOf="@id/barrier_before_date_inputs"
+ >
+ <com.google.android.material.textfield.TextInputEditText
+ android:id="@+id/pattern_details_date_day"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:gravity="center_horizontal"
+ android:hint="@string/date_day_hint"
+ />
+ </com.google.android.material.textfield.TextInputLayout>
+ <androidx.constraintlayout.widget.Barrier
+ android:id="@+id/barrier_before_description"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:orientation="horizontal"
+ app:barrierDirection="bottom"
+ app:constraint_referenced_ids="pattern_details_date_day_layout,pattern_details_date_month_layout,pattern_details_date_year_layout"
+ app:layout_constraintEnd_toEndOf="parent"
+ app:layout_constraintStart_toStartOf="parent"
+ />
+ <TextView
+ android:id="@+id/pattern_transaction_description_source_label"
+ android:layout_width="0dp"
+ android:layout_height="match_parent"
+ android:paddingTop="@dimen/text_margin"
+ android:text="@string/transaction_description_source_label"
+ android:textAppearance="?attr/textAppearanceListItem"
+ app:layout_constraintEnd_toEndOf="parent"
+ app:layout_constraintStart_toStartOf="parent"
+ app:layout_constraintTop_toBottomOf="@id/barrier_before_description"
+ />
+ <TextView
+ android:id="@+id/pattern_transaction_description_source"
+ android:layout_width="0dp"
+ android:layout_height="wrap_content"
+ android:minWidth="100dp"
+ android:textAppearance="?attr/textAppearanceListItemSecondary"
+ app:layout_constraintEnd_toEndOf="parent"
+ app:layout_constraintStart_toStartOf="parent"
+ app:layout_constraintTop_toBottomOf="@id/pattern_transaction_description_source_label"
+ />
+ <com.google.android.material.textfield.TextInputLayout
+ android:id="@+id/transaction_description_layout"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ app:layout_constraintTop_toBottomOf="@id/pattern_transaction_description_source"
+ >
+ <com.google.android.material.textfield.TextInputEditText
+ android:id="@+id/transaction_description"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:hint="@string/pattern_transaction_description_hint"
+ />
+ </com.google.android.material.textfield.TextInputLayout>
+ <TextView
+ android:id="@+id/pattern_transaction_comment_source_label"
+ android:layout_width="0dp"
+ android:layout_height="match_parent"
+ android:paddingTop="@dimen/text_margin"
+ android:text="@string/transaction_comment_source_label"
+ android:textAppearance="?attr/textAppearanceListItem"
+ app:layout_constraintEnd_toEndOf="parent"
+ app:layout_constraintStart_toStartOf="parent"
+ app:layout_constraintTop_toBottomOf="@id/transaction_description_layout"
+ />
+ <TextView
+ android:id="@+id/pattern_transaction_comment_source"
+ android:layout_width="0dp"
+ android:layout_height="wrap_content"
+ android:minWidth="100dp"
+ android:textAppearance="?attr/textAppearanceListItemSecondary"
+ app:layout_constraintEnd_toEndOf="parent"
+ app:layout_constraintStart_toStartOf="parent"
+ app:layout_constraintTop_toBottomOf="@id/pattern_transaction_comment_source_label"
+ />
+ <com.google.android.material.textfield.TextInputLayout
+ android:id="@+id/transaction_comment_layout"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ app:layout_constraintTop_toBottomOf="@id/pattern_transaction_comment_source"
+ >
+ <com.google.android.material.textfield.TextInputEditText
+ android:id="@+id/transaction_comment"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:hint="@string/pattern_transaction_comment_hint"
+ />
+ </com.google.android.material.textfield.TextInputLayout>
+
+</androidx.constraintlayout.widget.ConstraintLayout>
\ No newline at end of file
<navigation xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
- android:id="@+id/pattern_list_navigation.xml"
+ xmlns:tools="http://schemas.android.com/tools"
+ android:id="@+id/pattern_list_navigation"
app:startDestination="@id/patternListFragment"
>
android:id="@+id/patternListFragment"
android:name="net.ktnx.mobileledger.ui.patterns.PatternListFragment"
android:label="PatternListFragment"
- />
+ android:tag="patternListFragment"
+ >
+ <action
+ android:id="@+id/action_patternListFragment_to_patternDetailsFragment"
+ app:destination="@id/patternDetailsFragment"
+ app:enterAnim="@anim/slide_in_left"
+ app:exitAnim="@anim/slide_out_left"
+ />
+ </fragment>
+ <fragment
+ android:id="@+id/patternDetailsFragment"
+ android:name="net.ktnx.mobileledger.ui.patterns.PatternDetailsFragment"
+ android:label="pattern_details_fragment"
+ android:tag="patternDetailsFragment"
+ tools:layout="@layout/pattern_details_fragment"
+ >
+ <action
+ android:id="@+id/action_patternDetailsFragment_to_patternListFragment"
+ app:destination="@id/patternListFragment"
+ />
+ </fragment>
</navigation>
\ No newline at end of file
<string name="pattern_regex_hint">Pattern (regular expression)</string>
<string name="help_menu_item_title">Help</string>
<string name="edit_button_description">Edit button</string>
+ <string name="large_text">
+ "Material is the metaphor.\n\n"
+
+ "A material metaphor is the unifying theory of a rationalized space and a system of motion."
+ "The material is grounded in tactile reality, inspired by the study of paper and ink, yet "
+ "technologically advanced and open to imagination and magic.\n"
+ "Surfaces and edges of the material provide visual cues that are grounded in reality. The "
+ "use of familiar tactile attributes helps users quickly understand affordances. Yet the "
+ "flexibility of the material creates new affordances that supercede those in the physical "
+ "world, without breaking the rules of physics.\n"
+ "The fundamentals of light, surface, and movement are key to conveying how objects move, "
+ "interact, and exist in space and in relation to each other. Realistic lighting shows "
+ "seams, divides space, and indicates moving parts.\n\n"
+
+ "Bold, graphic, intentional.\n\n"
+
+ "The foundational elements of print based design typography, grids, space, scale, color, "
+ "and use of imagery guide visual treatments. These elements do far more than please the "
+ "eye. They create hierarchy, meaning, and focus. Deliberate color choices, edge to edge "
+ "imagery, large scale typography, and intentional white space create a bold and graphic "
+ "interface that immerse the user in the experience.\n"
+ "An emphasis on user actions makes core functionality immediately apparent and provides "
+ "waypoints for the user.\n\n"
+
+ "Motion provides meaning.\n\n"
+
+ "Motion respects and reinforces the user as the prime mover. Primary user actions are "
+ "inflection points that initiate motion, transforming the whole design.\n"
+ "All action takes place in a single environment. Objects are presented to the user without "
+ "breaking the continuity of experience even as they transform and reorganize.\n"
+ "Motion is meaningful and appropriate, serving to focus attention and maintain continuity. "
+ "Feedback is subtle yet clear. Transitions are efficient yet coherent.\n\n"
+
+ "3D world.\n\n"
+
+ "The material environment is a 3D space, which means all objects have x, y, and z "
+ "dimensions. The z-axis is perpendicularly aligned to the plane of the display, with the "
+ "positive z-axis extending towards the viewer. Every sheet of material occupies a single "
+ "position along the z-axis and has a standard 1dp thickness.\n"
+ "On the web, the z-axis is used for layering and not for perspective. The 3D world is "
+ "emulated by manipulating the y-axis.\n\n"
+
+ "Light and shadow.\n\n"
+
+ "Within the material environment, virtual lights illuminate the scene. Key lights create "
+ "directional shadows, while ambient light creates soft shadows from all angles.\n"
+ "Shadows in the material environment are cast by these two light sources. In Android "
+ "development, shadows occur when light sources are blocked by sheets of material at "
+ "various positions along the z-axis. On the web, shadows are depicted by manipulating the "
+ "y-axis only. The following example shows the card with a height of 6dp.\n\n"
+
+ "Resting elevation.\n\n"
+
+ "All material objects, regardless of size, have a resting elevation, or default elevation "
+ "that does not change. If an object changes elevation, it should return to its resting "
+ "elevation as soon as possible.\n\n"
+
+ "Component elevations.\n\n"
+
+ "The resting elevation for a component type is consistent across apps (e.g., FAB elevation "
+ "does not vary from 6dp in one app to 16dp in another app).\n"
+ "Components may have different resting elevations across platforms, depending on the depth "
+ "of the environment (e.g., TV has a greater depth than mobile or desktop).\n\n"
+
+ "Responsive elevation and dynamic elevation offsets.\n\n"
+
+ "Some component types have responsive elevation, meaning they change elevation in response "
+ "to user input (e.g., normal, focused, and pressed) or system events. These elevation "
+ "changes are consistently implemented using dynamic elevation offsets.\n"
+ "Dynamic elevation offsets are the goal elevation that a component moves towards, relative "
+ "to the component’s resting state. They ensure that elevation changes are consistent "
+ "across actions and component types. For example, all components that lift on press have "
+ "the same elevation change relative to their resting elevation.\n"
+ "Once the input event is completed or cancelled, the component will return to its resting "
+ "elevation.\n\n"
+
+ "Avoiding elevation interference.\n\n"
+
+ "Components with responsive elevations may encounter other components as they move between "
+ "their resting elevations and dynamic elevation offsets. Because material cannot pass "
+ "through other material, components avoid interfering with one another any number of ways, "
+ "whether on a per component basis or using the entire app layout.\n"
+ "On a component level, components can move or be removed before they cause interference. "
+ "For example, a floating action button (FAB) can disappear or move off screen before a "
+ "user picks up a card, or it can move if a snackbar appears.\n"
+ "On the layout level, design your app layout to minimize opportunities for interference. "
+ "For example, position the FAB to one side of stream of a cards so the FAB won’t interfere "
+ "when a user tries to pick up one of cards.\n\n"
+ </string>
+ <string name="pattern_has_errors">Pattern has errors</string>
+ <string name="account_name_is_empty">Account name missing</string>
+ <string name="pattern_is_empty">Pattern missing</string>
+ <string name="invalid_matching_group_number">Invalid matching group number</string>
+ <string name="pattern_name_label">Pattern name</string>
+ <string name="pattern_details_pattern_label">Pattern</string>
+ <string name="pattern_details_amount_label">Amount</string>
+ <string name="pattern_details_test_text_label">Test text</string>
+ <string name="pattern_details_account_name_label">Account name</string>
+ <string name="pattern_details_account_row_label">Transaction account details</string>
+ <string name="account_name_source_label">Account name source</string>
+ <string name="pattern_details_source_literal">literal</string>
+ <string name="account_comment_source_label">Account comment source</string>
+ <string name="account_amount_source_label">Amount source</string>
+ <string name="pattern_details_account_comment_label">Account comment</string>
+ <string name="pattern_details_account_amount_label">Amount</string>
+ <string name="choose_pattern_detail_source_label">Pattern match group</string>
+ <string name="missing_pattern_error">Missing pattern</string>
+ <string name="missing_test_text">Missing test text</string>
+ <string name="pattern_without_groups">Pattern has no capturing groups</string>
+ <string name="pattern_does_not_match">Pattern doesn\'t match the test text</string>
+ <string name="pattern_transaction_parameters_label">Transaction parameters</string>
+ <string name="pattern_transaction_description_hint">Transaction description</string>
+ <string name="pattern_transaction_comment_hint">Transaction comment</string>
+ <string name="transaction_description_source_label">Transaction description source</string>
+ <string name="transaction_comment_source_label">Transaction comment source</string>
+ <string name="pattern_details_date_label">Transaction date</string>
+ <string name="date_year_hint">year</string>
+ <string name="date_month_hint">month</string>
+ <string name="date_day_hint">date</string>
+ <string name="pattern_details_date_year_source_label">year</string>
+ <string name="pattern_details_date_day_source_label">date</string>
+ <string name="pattern_details_date_month_source_label">month</string>
+ <string name="unnamed_pattern">Pattern with no name</string>
+ <string name="add_button_description">Add pattern</string>
+ <string name="save_button_description">Save pattern</string>
</resources>