From: Damyan Ivanov Date: Fri, 29 Jan 2021 05:26:14 +0000 (+0000) Subject: somewhat working pattern list/editor X-Git-Tag: v0.17.0~206 X-Git-Url: https://git.ktnx.net/?a=commitdiff_plain;h=4d2ce14d526978de65113314cc50ab5ecf9c7d09;p=mobile-ledger.git somewhat working pattern list/editor --- diff --git a/app/src/main/java/net/ktnx/mobileledger/model/PatternDetailSource.java b/app/src/main/java/net/ktnx/mobileledger/model/PatternDetailSource.java new file mode 100644 index 00000000..5b8f378d --- /dev/null +++ b/app/src/main/java/net/ktnx/mobileledger/model/PatternDetailSource.java @@ -0,0 +1,60 @@ +/* + * 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 . + */ + +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 DIFF_CALLBACK = + new DiffUtil.ItemCallback() { + @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; + } +} diff --git a/app/src/main/java/net/ktnx/mobileledger/model/PatternDetailsItem.java b/app/src/main/java/net/ktnx/mobileledger/model/PatternDetailsItem.java new file mode 100644 index 00000000..82b1bb45 --- /dev/null +++ b/app/src/main/java/net/ktnx/mobileledger/model/PatternDetailsItem.java @@ -0,0 +1,557 @@ +/* + * 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 . + */ + +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 { + private boolean literalValue; + private T value; + private int matchGroup; + public PossiblyMatchedValue() { + literalValue = true; + value = null; + } + public PossiblyMatchedValue(@NonNull PossiblyMatchedValue origin) { + literalValue = origin.literalValue; + value = origin.value; + matchGroup = origin.matchGroup; + } + @NonNull + public static PossiblyMatchedValue withLiteralInt(int initialValue) { + PossiblyMatchedValue result = new PossiblyMatchedValue<>(); + result.setValue(initialValue); + return result; + } + @NonNull + public static PossiblyMatchedValue withLiteralFloat(float initialValue) { + PossiblyMatchedValue result = new PossiblyMatchedValue<>(); + result.setValue(initialValue); + return result; + } + public static PossiblyMatchedValue withLiteralShort(short initialValue) { + PossiblyMatchedValue result = new PossiblyMatchedValue<>(); + result.setValue(initialValue); + return result; + } + @NonNull + public static PossiblyMatchedValue withLiteralString(String initialValue) { + PossiblyMatchedValue 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 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 accountName = + PossiblyMatchedValue.withLiteralString(""); + private final PossiblyMatchedValue accountComment = + PossiblyMatchedValue.withLiteralString(""); + private final PossiblyMatchedValue amount = + PossiblyMatchedValue.withLiteralFloat(0f); + private final PossiblyMatchedValue 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 transactionDescription = + PossiblyMatchedValue.withLiteralString(""); + private PossiblyMatchedValue transactionComment = + PossiblyMatchedValue.withLiteralString(""); + private PossiblyMatchedValue dateYear = + PossiblyMatchedValue.withLiteralShort((short) 0); + private PossiblyMatchedValue dateMonth = + PossiblyMatchedValue.withLiteralShort((short) 0); + private PossiblyMatchedValue 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; + } + } +} diff --git a/app/src/main/java/net/ktnx/mobileledger/ui/OnSourceSelectedListener.java b/app/src/main/java/net/ktnx/mobileledger/ui/OnSourceSelectedListener.java new file mode 100644 index 00000000..a294adbd --- /dev/null +++ b/app/src/main/java/net/ktnx/mobileledger/ui/OnSourceSelectedListener.java @@ -0,0 +1,22 @@ +/* + * 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 . + */ + +package net.ktnx.mobileledger.ui; + +public interface OnSourceSelectedListener { + void onSourceSelected(boolean literal, short group); +} diff --git a/app/src/main/java/net/ktnx/mobileledger/ui/PatternDetailSourceSelectorFragment.java b/app/src/main/java/net/ktnx/mobileledger/ui/PatternDetailSourceSelectorFragment.java new file mode 100644 index 00000000..7a8a954d --- /dev/null +++ b/app/src/main/java/net/ktnx/mobileledger/ui/PatternDetailSourceSelectorFragment.java @@ -0,0 +1,188 @@ +/* + * 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 . + */ + +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. + *

+ * 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 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 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 diff --git a/app/src/main/java/net/ktnx/mobileledger/ui/PatternDetailSourceSelectorModel.java b/app/src/main/java/net/ktnx/mobileledger/ui/PatternDetailSourceSelectorModel.java new file mode 100644 index 00000000..b88c6765 --- /dev/null +++ b/app/src/main/java/net/ktnx/mobileledger/ui/PatternDetailSourceSelectorModel.java @@ -0,0 +1,46 @@ +/* + * 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 . + */ + +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> 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 mSources) { + groups.setValue(mSources); + } +} diff --git a/app/src/main/java/net/ktnx/mobileledger/ui/PatternDetailSourceSelectorRecyclerViewAdapter.java b/app/src/main/java/net/ktnx/mobileledger/ui/PatternDetailSourceSelectorRecyclerViewAdapter.java new file mode 100644 index 00000000..91a3c4d7 --- /dev/null +++ b/app/src/main/java/net/ktnx/mobileledger/ui/PatternDetailSourceSelectorRecyclerViewAdapter.java @@ -0,0 +1,95 @@ +/* + * 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 . + */ + +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 { + + 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()); + } + } +} diff --git a/app/src/main/java/net/ktnx/mobileledger/ui/patterns/PatternDetailsAdapter.java b/app/src/main/java/net/ktnx/mobileledger/ui/patterns/PatternDetailsAdapter.java new file mode 100644 index 00000000..5b6ab814 --- /dev/null +++ b/app/src/main/java/net/ktnx/mobileledger/ui/patterns/PatternDetailsAdapter.java @@ -0,0 +1,562 @@ +/* + * 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 . + */ + +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 { + private static final String D_PATTERN_UI = "pattern-ui"; + private final AsyncListDiffer differ; + public PatternDetailsAdapter() { + super(); + setHasStableIds(true); + differ = new AsyncListDiffer<>(this, new DiffUtil.ItemCallback() { + @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 items) { + ArrayList list = new ArrayList<>(); + for (PatternBase p : items) { + PatternDetailsItem item = PatternDetailsItem.fromRoomObject(p); + list.add(item); + } + setItems(list); + } + public void setItems(List 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"); + } + } +} diff --git a/app/src/main/java/net/ktnx/mobileledger/ui/patterns/PatternDetailsFragment.java b/app/src/main/java/net/ktnx/mobileledger/ui/patterns/PatternDetailsFragment.java new file mode 100644 index 00000000..83aa17d2 --- /dev/null +++ b/app/src/main/java/net/ktnx/mobileledger/ui/patterns/PatternDetailsFragment.java @@ -0,0 +1,114 @@ +/* + * 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 . + */ + +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 diff --git a/app/src/main/java/net/ktnx/mobileledger/ui/patterns/PatternDetailsViewModel.java b/app/src/main/java/net/ktnx/mobileledger/ui/patterns/PatternDetailsViewModel.java new file mode 100644 index 00000000..1c6e0359 --- /dev/null +++ b/app/src/main/java/net/ktnx/mobileledger/ui/patterns/PatternDetailsViewModel.java @@ -0,0 +1,219 @@ +/* + * 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 . + */ + +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> items = new MutableLiveData<>(); + private long mPatternId; + private String mDefaultPatternName; + public String getDefaultPatternName() { + return mDefaultPatternName; + } + public void setDefaultPatternName(String name) { + mDefaultPatternName = name; + } + public LiveData> getItems() { + return items; + } + + public void resetItems() { + items.setValue(Collections.emptyList()); + checkItemConsistency(); + } + private void checkItemConsistency() { + ArrayList 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 ph = db.getPatternDAO() + .getPattern(patternId); + ArrayList 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 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 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 diff --git a/app/src/main/java/net/ktnx/mobileledger/ui/patterns/PatternListFragment.java b/app/src/main/java/net/ktnx/mobileledger/ui/patterns/PatternListFragment.java index ad356456..52aab60e 100644 --- a/app/src/main/java/net/ktnx/mobileledger/ui/patterns/PatternListFragment.java +++ b/app/src/main/java/net/ktnx/mobileledger/ui/patterns/PatternListFragment.java @@ -31,8 +31,8 @@ import androidx.lifecycle.LifecycleOwner; 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; @@ -73,12 +73,9 @@ public class PatternListFragment extends Fragment { @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); @@ -129,7 +126,7 @@ public class PatternListFragment extends Fragment { */ public interface OnPatternListFragmentInteractionListener { void onNewPattern(); - + void onSavePattern(); void onEditPattern(int id); } } \ No newline at end of file diff --git a/app/src/main/java/net/ktnx/mobileledger/ui/patterns/PatternViewHolder.java b/app/src/main/java/net/ktnx/mobileledger/ui/patterns/PatternViewHolder.java index 0a60ab3b..829260b9 100644 --- a/app/src/main/java/net/ktnx/mobileledger/ui/patterns/PatternViewHolder.java +++ b/app/src/main/java/net/ktnx/mobileledger/ui/patterns/PatternViewHolder.java @@ -31,5 +31,8 @@ class PatternViewHolder extends RecyclerView.ViewHolder { } public void bindToItem(PatternEntry item) { b.patternName.setText(item.getName()); + b.editButon.setOnClickListener(v -> { + ((PatternsActivity) v.getContext()).onEditPattern(item.getId()); + }); } } diff --git a/app/src/main/java/net/ktnx/mobileledger/ui/patterns/PatternsActivity.java b/app/src/main/java/net/ktnx/mobileledger/ui/patterns/PatternsActivity.java index 43a6227d..da258d48 100644 --- a/app/src/main/java/net/ktnx/mobileledger/ui/patterns/PatternsActivity.java +++ b/app/src/main/java/net/ktnx/mobileledger/ui/patterns/PatternsActivity.java @@ -20,14 +20,19 @@ package net.ktnx.mobileledger.ui.patterns; 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; @@ -46,27 +51,66 @@ public class PatternsActivity extends CrashReportingActivity 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 diff --git a/app/src/main/java/net/ktnx/mobileledger/ui/profiles/ProfileDetailFragment.java b/app/src/main/java/net/ktnx/mobileledger/ui/profiles/ProfileDetailFragment.java index e531809a..ed73190e 100644 --- a/app/src/main/java/net/ktnx/mobileledger/ui/profiles/ProfileDetailFragment.java +++ b/app/src/main/java/net/ktnx/mobileledger/ui/profiles/ProfileDetailFragment.java @@ -205,7 +205,7 @@ public class ProfileDetailFragment extends Fragment { resetDefaultCommodity(); }); - FloatingActionButton fab = context.findViewById(R.id.fab); + FloatingActionButton fab = context.findViewById(R.id.fabAdd); fab.setOnClickListener(v -> onSaveFabClicked()); hookTextChangeSyncRoutine(binding.profileName, model::setProfileName); diff --git a/app/src/main/res/layout/activity_patterns.xml b/app/src/main/res/layout/activity_patterns.xml index 69f5aac1..47ce9f1d 100644 --- a/app/src/main/res/layout/activity_patterns.xml +++ b/app/src/main/res/layout/activity_patterns.xml @@ -15,12 +15,74 @@ ~ along with MoLe. If not, see . --> - \ No newline at end of file + tools:context=".ui.patterns.PatternsActivity" + > + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/layout/activity_profile_detail.xml b/app/src/main/res/layout/activity_profile_detail.xml index 282af250..4184146a 100644 --- a/app/src/main/res/layout/activity_profile_detail.xml +++ b/app/src/main/res/layout/activity_profile_detail.xml @@ -60,7 +60,7 @@ app:layout_behavior="@string/appbar_scrolling_view_behavior" /> + + + \ No newline at end of file diff --git a/app/src/main/res/layout/fragment_pattern_detail_source_selector.xml b/app/src/main/res/layout/fragment_pattern_detail_source_selector.xml new file mode 100644 index 00000000..b36f101c --- /dev/null +++ b/app/src/main/res/layout/fragment_pattern_detail_source_selector.xml @@ -0,0 +1,60 @@ + + + + + + + + diff --git a/app/src/main/res/layout/fragment_pattern_detail_source_selector_list.xml b/app/src/main/res/layout/fragment_pattern_detail_source_selector_list.xml new file mode 100644 index 00000000..bfdc5d3b --- /dev/null +++ b/app/src/main/res/layout/fragment_pattern_detail_source_selector_list.xml @@ -0,0 +1,78 @@ + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/fragment_pattern_list.xml b/app/src/main/res/layout/fragment_pattern_list.xml index e476b0cc..f90abdea 100644 --- a/app/src/main/res/layout/fragment_pattern_list.xml +++ b/app/src/main/res/layout/fragment_pattern_list.xml @@ -15,68 +15,11 @@ ~ along with MoLe. If not, see . --> - - - - - - - - - - - - - - - - - - - - \ No newline at end of file + /> diff --git a/app/src/main/res/layout/pattern_details_account.xml b/app/src/main/res/layout/pattern_details_account.xml new file mode 100644 index 00000000..6a40b0cb --- /dev/null +++ b/app/src/main/res/layout/pattern_details_account.xml @@ -0,0 +1,154 @@ + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/pattern_details_fragment.xml b/app/src/main/res/layout/pattern_details_fragment.xml new file mode 100644 index 00000000..01ddc4d5 --- /dev/null +++ b/app/src/main/res/layout/pattern_details_fragment.xml @@ -0,0 +1,25 @@ + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/pattern_details_header.xml b/app/src/main/res/layout/pattern_details_header.xml new file mode 100644 index 00000000..1e9e35f9 --- /dev/null +++ b/app/src/main/res/layout/pattern_details_header.xml @@ -0,0 +1,299 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/navigation/pattern_list_navigation.xml b/app/src/main/res/navigation/pattern_list_navigation.xml index ba60d02b..927ec201 100644 --- a/app/src/main/res/navigation/pattern_list_navigation.xml +++ b/app/src/main/res/navigation/pattern_list_navigation.xml @@ -17,7 +17,8 @@ @@ -25,5 +26,25 @@ android:id="@+id/patternListFragment" android:name="net.ktnx.mobileledger.ui.patterns.PatternListFragment" android:label="PatternListFragment" - /> + android:tag="patternListFragment" + > + + + + + \ No newline at end of file diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 0d1a17a0..69ef1d83 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -183,4 +183,129 @@ Pattern (regular expression) Help Edit button + + "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" + + Pattern has errors + Account name missing + Pattern missing + Invalid matching group number + Pattern name + Pattern + Amount + Test text + Account name + Transaction account details + Account name source + literal + Account comment source + Amount source + Account comment + Amount + Pattern match group + Missing pattern + Missing test text + Pattern has no capturing groups + Pattern doesn\'t match the test text + Transaction parameters + Transaction description + Transaction comment + Transaction description source + Transaction comment source + Transaction date + year + month + date + year + date + month + Pattern with no name + Add pattern + Save pattern