]> git.ktnx.net Git - mobile-ledger.git/commitdiff
rename patterns to templates
authorDamyan Ivanov <dam+mobileledger@ktnx.net>
Tue, 2 Feb 2021 06:35:48 +0000 (08:35 +0200)
committerDamyan Ivanov <dam+mobileledger@ktnx.net>
Tue, 2 Feb 2021 06:35:48 +0000 (08:35 +0200)
"pattern" is the regular expression that is used to match templates

59 files changed:
app/src/main/AndroidManifest.xml
app/src/main/java/net/ktnx/mobileledger/dao/PatternAccountDAO.java
app/src/main/java/net/ktnx/mobileledger/dao/PatternHeaderDAO.java
app/src/main/java/net/ktnx/mobileledger/db/DB.java
app/src/main/java/net/ktnx/mobileledger/db/PatternAccount.java [deleted file]
app/src/main/java/net/ktnx/mobileledger/db/PatternBase.java [deleted file]
app/src/main/java/net/ktnx/mobileledger/db/PatternHeader.java [deleted file]
app/src/main/java/net/ktnx/mobileledger/db/PatternWithAccounts.java
app/src/main/java/net/ktnx/mobileledger/db/TemplateAccount.java [new file with mode: 0644]
app/src/main/java/net/ktnx/mobileledger/db/TemplateBase.java [new file with mode: 0644]
app/src/main/java/net/ktnx/mobileledger/db/TemplateHeader.java [new file with mode: 0644]
app/src/main/java/net/ktnx/mobileledger/model/PatternDetailSource.java [deleted file]
app/src/main/java/net/ktnx/mobileledger/model/PatternDetailsItem.java [deleted file]
app/src/main/java/net/ktnx/mobileledger/model/TemplateDetailSource.java [new file with mode: 0644]
app/src/main/java/net/ktnx/mobileledger/model/TemplateDetailsItem.java [new file with mode: 0644]
app/src/main/java/net/ktnx/mobileledger/ui/PatternDetailSourceSelectorFragment.java [deleted file]
app/src/main/java/net/ktnx/mobileledger/ui/PatternDetailSourceSelectorModel.java [deleted file]
app/src/main/java/net/ktnx/mobileledger/ui/PatternDetailSourceSelectorRecyclerViewAdapter.java [deleted file]
app/src/main/java/net/ktnx/mobileledger/ui/TemplateDetailSourceSelectorFragment.java [new file with mode: 0644]
app/src/main/java/net/ktnx/mobileledger/ui/TemplateDetailSourceSelectorModel.java [new file with mode: 0644]
app/src/main/java/net/ktnx/mobileledger/ui/TemplateDetailSourceSelectorRecyclerViewAdapter.java [new file with mode: 0644]
app/src/main/java/net/ktnx/mobileledger/ui/activity/MainActivity.java
app/src/main/java/net/ktnx/mobileledger/ui/new_transaction/NewTransactionFragment.java
app/src/main/java/net/ktnx/mobileledger/ui/patterns/PatternDetailsAdapter.java [deleted file]
app/src/main/java/net/ktnx/mobileledger/ui/patterns/PatternDetailsFragment.java [deleted file]
app/src/main/java/net/ktnx/mobileledger/ui/patterns/PatternDetailsViewModel.java [deleted file]
app/src/main/java/net/ktnx/mobileledger/ui/patterns/PatternListFragment.java [deleted file]
app/src/main/java/net/ktnx/mobileledger/ui/patterns/PatternViewHolder.java [deleted file]
app/src/main/java/net/ktnx/mobileledger/ui/patterns/PatternsRecyclerViewAdapter.java [deleted file]
app/src/main/java/net/ktnx/mobileledger/ui/templates/TemplateDetailsAdapter.java [new file with mode: 0644]
app/src/main/java/net/ktnx/mobileledger/ui/templates/TemplateDetailsFragment.java [new file with mode: 0644]
app/src/main/java/net/ktnx/mobileledger/ui/templates/TemplateDetailsViewModel.java [new file with mode: 0644]
app/src/main/java/net/ktnx/mobileledger/ui/templates/TemplateListFragment.java [new file with mode: 0644]
app/src/main/java/net/ktnx/mobileledger/ui/templates/TemplateViewHolder.java [new file with mode: 0644]
app/src/main/java/net/ktnx/mobileledger/ui/templates/TemplatesActivity.java
app/src/main/java/net/ktnx/mobileledger/ui/templates/TemplatesRecyclerViewAdapter.java [new file with mode: 0644]
app/src/main/res/layout/activity_patterns.xml [deleted file]
app/src/main/res/layout/activity_templates.xml [new file with mode: 0644]
app/src/main/res/layout/fragment_item_list.xml
app/src/main/res/layout/fragment_pattern_detail_source_selector.xml [deleted file]
app/src/main/res/layout/fragment_pattern_detail_source_selector_list.xml [deleted file]
app/src/main/res/layout/fragment_pattern_list.xml [deleted file]
app/src/main/res/layout/fragment_template_detail_source_selector.xml [new file with mode: 0644]
app/src/main/res/layout/fragment_template_detail_source_selector_list.xml [new file with mode: 0644]
app/src/main/res/layout/fragment_template_list.xml [new file with mode: 0644]
app/src/main/res/layout/pattern_details_account.xml [deleted file]
app/src/main/res/layout/pattern_details_fragment.xml [deleted file]
app/src/main/res/layout/pattern_details_header.xml [deleted file]
app/src/main/res/layout/pattern_layout.xml [deleted file]
app/src/main/res/layout/template_details_account.xml [new file with mode: 0644]
app/src/main/res/layout/template_details_fragment.xml [new file with mode: 0644]
app/src/main/res/layout/template_details_header.xml [new file with mode: 0644]
app/src/main/res/layout/template_list_template_item.xml [new file with mode: 0644]
app/src/main/res/menu/pattern_list_menu.xml [deleted file]
app/src/main/res/menu/template_list_menu.xml [new file with mode: 0644]
app/src/main/res/navigation/pattern_list_navigation.xml [deleted file]
app/src/main/res/navigation/template_list_navigation.xml [new file with mode: 0644]
app/src/main/res/values-bg/strings.xml
app/src/main/res/values/strings.xml

index 21a7e5ad1e6770644286753f1cd66819b61465b1..81183ea66b7ec81d26d746e135863ca3c4112089 100644 (file)
@@ -32,8 +32,8 @@
         android:supportsRtl="true"
         tools:ignore="GoogleAppIndexingWarning">
         <activity
-            android:name=".ui.patterns.PatternsActivity"
-            android:label="@string/title_activity_patterns"
+            android:name=".ui.templates.TemplatesActivity"
+            android:label="@string/title_activity_templates"
             android:theme="@style/AppTheme.default" />
         <activity
             android:name=".ui.activity.SplashActivity"
index f33bdde763bb98be5239e46ef25263d4e1843818..1ee0cd0d9e483c12b999727a32a996e624dad8af 100644 (file)
@@ -24,26 +24,26 @@ import androidx.room.Insert;
 import androidx.room.Query;
 import androidx.room.Update;
 
-import net.ktnx.mobileledger.db.PatternAccount;
+import net.ktnx.mobileledger.db.TemplateAccount;
 
 import java.util.List;
 
 @Dao
 public interface PatternAccountDAO {
     @Insert
-    Long insert(PatternAccount item);
+    Long insert(TemplateAccount item);
 
     @Update
-    void update(PatternAccount... items);
+    void update(TemplateAccount... items);
 
     @Delete
-    void delete(PatternAccount item);
+    void delete(TemplateAccount item);
 
     @Query("SELECT * FROM pattern_accounts WHERE pattern_id=:pattern_id")
-    LiveData<List<PatternAccount>> getPatternAccounts(Long pattern_id);
+    LiveData<List<TemplateAccount>> getPatternAccounts(Long pattern_id);
 
     @Query("SELECT * FROM pattern_accounts WHERE id = :id")
-    LiveData<PatternAccount> getPatternAccountById(Long id);
+    LiveData<TemplateAccount> getPatternAccountById(Long id);
 
 //    not useful for now
 //    @Transaction
index c3f0535aec274393c526765fb0d82d7c47b773ec..b4713d32f343e2a320a0dd08fd79cfe834878ee4 100644 (file)
@@ -25,27 +25,27 @@ import androidx.room.Query;
 import androidx.room.Transaction;
 import androidx.room.Update;
 
-import net.ktnx.mobileledger.db.PatternHeader;
 import net.ktnx.mobileledger.db.PatternWithAccounts;
+import net.ktnx.mobileledger.db.TemplateHeader;
 
 import java.util.List;
 
 @Dao
 public interface PatternHeaderDAO {
     @Insert()
-    long insert(PatternHeader item);
+    long insert(TemplateHeader item);
 
     @Update
-    void update(PatternHeader... items);
+    void update(TemplateHeader... items);
 
     @Delete
-    void delete(PatternHeader item);
+    void delete(TemplateHeader item);
 
     @Query("SELECT * FROM patterns ORDER BY UPPER(name)")
-    LiveData<List<PatternHeader>> getPatterns();
+    LiveData<List<TemplateHeader>> getPatterns();
 
     @Query("SELECT * FROM patterns WHERE id = :id")
-    LiveData<PatternHeader> getPattern(Long id);
+    LiveData<TemplateHeader> getPattern(Long id);
 
     @Transaction
     @Query("SELECT * FROM patterns WHERE id = :id")
index 6bcae84f3a7f4190df70f62d242365ebd911e47a..80652ed1ecb8cf6642b79ff579e73345805dabe6 100644 (file)
@@ -30,7 +30,7 @@ import net.ktnx.mobileledger.dao.PatternAccountDAO;
 import net.ktnx.mobileledger.dao.PatternHeaderDAO;
 import net.ktnx.mobileledger.utils.MobileLedgerDatabase;
 
-@Database(version = 53, entities = {PatternHeader.class, PatternAccount.class, Currency.class})
+@Database(version = 53, entities = {TemplateHeader.class, TemplateAccount.class, Currency.class})
 abstract public class DB extends RoomDatabase {
     private static DB instance;
     public static DB get() {
diff --git a/app/src/main/java/net/ktnx/mobileledger/db/PatternAccount.java b/app/src/main/java/net/ktnx/mobileledger/db/PatternAccount.java
deleted file mode 100644 (file)
index 3ebd51d..0000000
+++ /dev/null
@@ -1,148 +0,0 @@
-/*
- * 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.db;
-
-import androidx.annotation.NonNull;
-import androidx.room.ColumnInfo;
-import androidx.room.Entity;
-import androidx.room.ForeignKey;
-import androidx.room.Index;
-import androidx.room.PrimaryKey;
-
-import org.jetbrains.annotations.NotNull;
-
-@Entity(tableName = "pattern_accounts",
-        indices = {@Index(name = "un_pattern_accounts", unique = true, value = "id"),
-                   @Index(name = "fk_pattern_accounts_pattern", value = "pattern_id"),
-                   @Index(name = "fk_pattern_accounts_currency", value = "currency")
-        }, foreignKeys = {@ForeignKey(childColumns = "pattern_id", parentColumns = "id",
-                                      entity = PatternHeader.class),
-                          @ForeignKey(childColumns = "currency", parentColumns = "id",
-                                      entity = Currency.class)
-})
-public class PatternAccount extends PatternBase {
-    @NonNull
-    @ColumnInfo(name = "pattern_id")
-    private Long patternId;
-    @PrimaryKey(autoGenerate = true)
-    @NotNull
-    private Long id;
-    @ColumnInfo(name = "acc")
-    private String accountName;
-    @ColumnInfo(name = "position")
-    @NonNull
-    private Long position;
-    @ColumnInfo(name = "acc_match_group")
-    private Integer accountNameMatchGroup;
-    @ColumnInfo(name = "currency")
-    private Integer currency;
-    @ColumnInfo(name = "currency_match_group")
-    private Integer currencyMatchGroup;
-    @ColumnInfo(name = "amount")
-    private Float amount;
-    @ColumnInfo(name = "amount_match_group")
-    private Integer amountMatchGroup;
-    @ColumnInfo(name = "comment")
-    private String accountComment;
-    @ColumnInfo(name = "comment_match_group")
-    private Integer accountCommentMatchGroup;
-    @ColumnInfo(name = "negate_amount")
-    private Boolean negateAmount;
-    public PatternAccount(@NotNull Long id, @NonNull Long patternId, @NonNull Long position) {
-        this.id = id;
-        this.patternId = patternId;
-        this.position = position;
-    }
-    public Long getId() {
-        return id;
-    }
-    public void setId(Long id) {
-        this.id = id;
-    }
-    public Boolean getNegateAmount() {
-        return negateAmount;
-    }
-    public void setNegateAmount(Boolean negateAmount) {
-        this.negateAmount = negateAmount;
-    }
-    public @NotNull Long getPatternId() {
-        return patternId;
-    }
-    public void setPatternId(@NonNull Long patternId) {
-        this.patternId = patternId;
-    }
-    @NonNull
-    public String getAccountName() {
-        return accountName;
-    }
-    public void setAccountName(@NonNull String accountName) {
-        this.accountName = accountName;
-    }
-    @NonNull
-    public Long getPosition() {
-        return position;
-    }
-    public void setPosition(@NonNull Long position) {
-        this.position = position;
-    }
-    public void setPosition(int position) {
-        this.position = (long) position;
-    }
-    public Integer getAccountNameMatchGroup() {
-        return accountNameMatchGroup;
-    }
-    public void setAccountNameMatchGroup(Integer accountNameMatchGroup) {
-        this.accountNameMatchGroup = accountNameMatchGroup;
-    }
-    public Integer getCurrency() {
-        return currency;
-    }
-    public void setCurrency(Integer currency) {
-        this.currency = currency;
-    }
-    public Integer getCurrencyMatchGroup() {
-        return currencyMatchGroup;
-    }
-    public void setCurrencyMatchGroup(Integer currencyMatchGroup) {
-        this.currencyMatchGroup = currencyMatchGroup;
-    }
-    public Float getAmount() {
-        return amount;
-    }
-    public void setAmount(Float amount) {
-        this.amount = amount;
-    }
-    public Integer getAmountMatchGroup() {
-        return amountMatchGroup;
-    }
-    public void setAmountMatchGroup(Integer amountMatchGroup) {
-        this.amountMatchGroup = amountMatchGroup;
-    }
-    public String getAccountComment() {
-        return accountComment;
-    }
-    public void setAccountComment(String accountComment) {
-        this.accountComment = accountComment;
-    }
-    public Integer getAccountCommentMatchGroup() {
-        return accountCommentMatchGroup;
-    }
-    public void setAccountCommentMatchGroup(Integer accountCommentMatchGroup) {
-        this.accountCommentMatchGroup = accountCommentMatchGroup;
-    }
-}
diff --git a/app/src/main/java/net/ktnx/mobileledger/db/PatternBase.java b/app/src/main/java/net/ktnx/mobileledger/db/PatternBase.java
deleted file mode 100644 (file)
index 5d048e8..0000000
+++ /dev/null
@@ -1,20 +0,0 @@
-/*
- * 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.db;
-
-public class PatternBase {}
diff --git a/app/src/main/java/net/ktnx/mobileledger/db/PatternHeader.java b/app/src/main/java/net/ktnx/mobileledger/db/PatternHeader.java
deleted file mode 100644 (file)
index c6223c4..0000000
+++ /dev/null
@@ -1,164 +0,0 @@
-package net.ktnx.mobileledger.db;
-
-import androidx.annotation.NonNull;
-import androidx.annotation.Nullable;
-import androidx.room.ColumnInfo;
-import androidx.room.Entity;
-import androidx.room.Index;
-import androidx.room.PrimaryKey;
-
-import net.ktnx.mobileledger.utils.Misc;
-
-import org.jetbrains.annotations.NotNull;
-
-@Entity(tableName = "patterns",
-        indices = {@Index(name = "un_patterns_id", value = "id", unique = true)})
-public class PatternHeader extends PatternBase {
-    @PrimaryKey(autoGenerate = true)
-    @NonNull
-    private Long id;
-    @ColumnInfo(name = "name")
-    @NonNull
-    private String name;
-    @NonNull
-    @ColumnInfo(name = "regular_expression")
-    private String regularExpression;
-    @ColumnInfo(name = "test_text")
-    private String testText;
-    @ColumnInfo(name = "transaction_description")
-    private String transactionDescription;
-    @ColumnInfo(name = "transaction_description_match_group")
-    private Integer transactionDescriptionMatchGroup;
-    @ColumnInfo(name = "transaction_comment")
-    private String transactionComment;
-    @ColumnInfo(name = "transaction_comment_match_group")
-    private Integer transactionCommentMatchGroup;
-    @ColumnInfo(name = "date_year")
-    private Integer dateYear;
-    @ColumnInfo(name = "date_year_match_group")
-    private Integer dateYearMatchGroup;
-    @ColumnInfo(name = "date_month")
-    private Integer dateMonth;
-    @ColumnInfo(name = "date_month_match_group")
-    private Integer dateMonthMatchGroup;
-    @ColumnInfo(name = "date_day")
-    private Integer dateDay;
-    @ColumnInfo(name = "date_day_match_group")
-    private Integer dateDayMatchGroup;
-    public PatternHeader(@NotNull Long id, @NonNull String name,
-                         @NonNull String regularExpression) {
-        this.id = id;
-        this.name = name;
-        this.regularExpression = regularExpression;
-    }
-    public String getTestText() {
-        return testText;
-    }
-    public void setTestText(String testText) {
-        this.testText = testText;
-    }
-    public Integer getTransactionDescriptionMatchGroup() {
-        return transactionDescriptionMatchGroup;
-    }
-    public void setTransactionDescriptionMatchGroup(Integer transactionDescriptionMatchGroup) {
-        this.transactionDescriptionMatchGroup = transactionDescriptionMatchGroup;
-    }
-    public Integer getTransactionCommentMatchGroup() {
-        return transactionCommentMatchGroup;
-    }
-    public void setTransactionCommentMatchGroup(Integer transactionCommentMatchGroup) {
-        this.transactionCommentMatchGroup = transactionCommentMatchGroup;
-    }
-    public Integer getDateYear() {
-        return dateYear;
-    }
-    public void setDateYear(Integer dateYear) {
-        this.dateYear = dateYear;
-    }
-    public Integer getDateMonth() {
-        return dateMonth;
-    }
-    public void setDateMonth(Integer dateMonth) {
-        this.dateMonth = dateMonth;
-    }
-    public Integer getDateDay() {
-        return dateDay;
-    }
-    public void setDateDay(Integer dateDay) {
-        this.dateDay = dateDay;
-    }
-    @NonNull
-    public Long getId() {
-        return id;
-    }
-    public void setId(@NonNull Long id) {
-        this.id = id;
-    }
-    @NonNull
-    public String getName() {
-        return name;
-    }
-    public void setName(@NonNull String name) {
-        this.name = name;
-    }
-    @NonNull
-    public String getRegularExpression() {
-        return regularExpression;
-    }
-    public void setRegularExpression(@NonNull String regularExpression) {
-        this.regularExpression = regularExpression;
-    }
-    public String getTransactionDescription() {
-        return transactionDescription;
-    }
-    public void setTransactionDescription(String transactionDescription) {
-        this.transactionDescription = transactionDescription;
-    }
-    public String getTransactionComment() {
-        return transactionComment;
-    }
-    public void setTransactionComment(String transactionComment) {
-        this.transactionComment = transactionComment;
-    }
-    public Integer getDateYearMatchGroup() {
-        return dateYearMatchGroup;
-    }
-    public void setDateYearMatchGroup(Integer dateYearMatchGroup) {
-        this.dateYearMatchGroup = dateYearMatchGroup;
-    }
-    public Integer getDateMonthMatchGroup() {
-        return dateMonthMatchGroup;
-    }
-    public void setDateMonthMatchGroup(Integer dateMonthMatchGroup) {
-        this.dateMonthMatchGroup = dateMonthMatchGroup;
-    }
-    public Integer getDateDayMatchGroup() {
-        return dateDayMatchGroup;
-    }
-    public void setDateDayMatchGroup(Integer dateDayMatchGroup) {
-        this.dateDayMatchGroup = dateDayMatchGroup;
-    }
-    @Override
-    public boolean equals(@Nullable Object obj) {
-        if (obj == null)
-            return false;
-        if (!(obj instanceof PatternHeader))
-            return false;
-
-        PatternHeader o = (PatternHeader) obj;
-
-        return Misc.equalLongs(id, o.id) && Misc.equalStrings(name, o.name) &&
-               Misc.equalStrings(regularExpression, o.regularExpression) &&
-               Misc.equalStrings(transactionDescription, o.transactionDescription) &&
-               Misc.equalStrings(transactionComment, o.transactionComment) &&
-               Misc.equalIntegers(transactionDescriptionMatchGroup,
-                       o.transactionDescriptionMatchGroup) &&
-               Misc.equalIntegers(transactionCommentMatchGroup, o.transactionCommentMatchGroup) &&
-               Misc.equalIntegers(dateDay, o.dateDay) &&
-               Misc.equalIntegers(dateDayMatchGroup, o.dateDayMatchGroup) &&
-               Misc.equalIntegers(dateMonth, o.dateMonth) &&
-               Misc.equalIntegers(dateMonthMatchGroup, o.dateMonthMatchGroup) &&
-               Misc.equalIntegers(dateYear, o.dateYear) &&
-               Misc.equalIntegers(dateYearMatchGroup, o.dateYearMatchGroup);
-    }
-}
index 8992fe186bae4f05b56cdf59a2320e56628088f9..9f171b1b461315903cf4c65fa0c9b15b9cff60b6 100644 (file)
@@ -24,9 +24,9 @@ import java.util.List;
 
 public class PatternWithAccounts {
     @Embedded
-    public PatternHeader header;
+    public TemplateHeader header;
     @Relation(parentColumn = "id", entityColumn = "pattern_id")
-    public List<PatternAccount> accounts;
+    public List<TemplateAccount> accounts;
 
     public Long getId() {
         return header.getId();
diff --git a/app/src/main/java/net/ktnx/mobileledger/db/TemplateAccount.java b/app/src/main/java/net/ktnx/mobileledger/db/TemplateAccount.java
new file mode 100644 (file)
index 0000000..92b2908
--- /dev/null
@@ -0,0 +1,148 @@
+/*
+ * 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.db;
+
+import androidx.annotation.NonNull;
+import androidx.room.ColumnInfo;
+import androidx.room.Entity;
+import androidx.room.ForeignKey;
+import androidx.room.Index;
+import androidx.room.PrimaryKey;
+
+import org.jetbrains.annotations.NotNull;
+
+@Entity(tableName = "pattern_accounts",
+        indices = {@Index(name = "un_pattern_accounts", unique = true, value = "id"),
+                   @Index(name = "fk_pattern_accounts_pattern", value = "pattern_id"),
+                   @Index(name = "fk_pattern_accounts_currency", value = "currency")
+        }, foreignKeys = {@ForeignKey(childColumns = "pattern_id", parentColumns = "id",
+                                      entity = TemplateHeader.class),
+                          @ForeignKey(childColumns = "currency", parentColumns = "id",
+                                      entity = Currency.class)
+})
+public class TemplateAccount extends TemplateBase {
+    @NonNull
+    @ColumnInfo(name = "pattern_id")
+    private Long patternId;
+    @PrimaryKey(autoGenerate = true)
+    @NotNull
+    private Long id;
+    @ColumnInfo(name = "acc")
+    private String accountName;
+    @ColumnInfo(name = "position")
+    @NonNull
+    private Long position;
+    @ColumnInfo(name = "acc_match_group")
+    private Integer accountNameMatchGroup;
+    @ColumnInfo(name = "currency")
+    private Integer currency;
+    @ColumnInfo(name = "currency_match_group")
+    private Integer currencyMatchGroup;
+    @ColumnInfo(name = "amount")
+    private Float amount;
+    @ColumnInfo(name = "amount_match_group")
+    private Integer amountMatchGroup;
+    @ColumnInfo(name = "comment")
+    private String accountComment;
+    @ColumnInfo(name = "comment_match_group")
+    private Integer accountCommentMatchGroup;
+    @ColumnInfo(name = "negate_amount")
+    private Boolean negateAmount;
+    public TemplateAccount(@NotNull Long id, @NonNull Long patternId, @NonNull Long position) {
+        this.id = id;
+        this.patternId = patternId;
+        this.position = position;
+    }
+    public Long getId() {
+        return id;
+    }
+    public void setId(Long id) {
+        this.id = id;
+    }
+    public Boolean getNegateAmount() {
+        return negateAmount;
+    }
+    public void setNegateAmount(Boolean negateAmount) {
+        this.negateAmount = negateAmount;
+    }
+    public @NotNull Long getPatternId() {
+        return patternId;
+    }
+    public void setPatternId(@NonNull Long patternId) {
+        this.patternId = patternId;
+    }
+    @NonNull
+    public String getAccountName() {
+        return accountName;
+    }
+    public void setAccountName(@NonNull String accountName) {
+        this.accountName = accountName;
+    }
+    @NonNull
+    public Long getPosition() {
+        return position;
+    }
+    public void setPosition(@NonNull Long position) {
+        this.position = position;
+    }
+    public void setPosition(int position) {
+        this.position = (long) position;
+    }
+    public Integer getAccountNameMatchGroup() {
+        return accountNameMatchGroup;
+    }
+    public void setAccountNameMatchGroup(Integer accountNameMatchGroup) {
+        this.accountNameMatchGroup = accountNameMatchGroup;
+    }
+    public Integer getCurrency() {
+        return currency;
+    }
+    public void setCurrency(Integer currency) {
+        this.currency = currency;
+    }
+    public Integer getCurrencyMatchGroup() {
+        return currencyMatchGroup;
+    }
+    public void setCurrencyMatchGroup(Integer currencyMatchGroup) {
+        this.currencyMatchGroup = currencyMatchGroup;
+    }
+    public Float getAmount() {
+        return amount;
+    }
+    public void setAmount(Float amount) {
+        this.amount = amount;
+    }
+    public Integer getAmountMatchGroup() {
+        return amountMatchGroup;
+    }
+    public void setAmountMatchGroup(Integer amountMatchGroup) {
+        this.amountMatchGroup = amountMatchGroup;
+    }
+    public String getAccountComment() {
+        return accountComment;
+    }
+    public void setAccountComment(String accountComment) {
+        this.accountComment = accountComment;
+    }
+    public Integer getAccountCommentMatchGroup() {
+        return accountCommentMatchGroup;
+    }
+    public void setAccountCommentMatchGroup(Integer accountCommentMatchGroup) {
+        this.accountCommentMatchGroup = accountCommentMatchGroup;
+    }
+}
diff --git a/app/src/main/java/net/ktnx/mobileledger/db/TemplateBase.java b/app/src/main/java/net/ktnx/mobileledger/db/TemplateBase.java
new file mode 100644 (file)
index 0000000..83a963a
--- /dev/null
@@ -0,0 +1,20 @@
+/*
+ * 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.db;
+
+public class TemplateBase {}
diff --git a/app/src/main/java/net/ktnx/mobileledger/db/TemplateHeader.java b/app/src/main/java/net/ktnx/mobileledger/db/TemplateHeader.java
new file mode 100644 (file)
index 0000000..301b100
--- /dev/null
@@ -0,0 +1,181 @@
+/*
+ * 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.db;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import androidx.room.ColumnInfo;
+import androidx.room.Entity;
+import androidx.room.Index;
+import androidx.room.PrimaryKey;
+
+import net.ktnx.mobileledger.utils.Misc;
+
+import org.jetbrains.annotations.NotNull;
+
+@Entity(tableName = "patterns",
+        indices = {@Index(name = "un_patterns_id", value = "id", unique = true)})
+public class TemplateHeader extends TemplateBase {
+    @PrimaryKey(autoGenerate = true)
+    @NonNull
+    private Long id;
+    @ColumnInfo(name = "name")
+    @NonNull
+    private String name;
+    @NonNull
+    @ColumnInfo(name = "regular_expression")
+    private String regularExpression;
+    @ColumnInfo(name = "test_text")
+    private String testText;
+    @ColumnInfo(name = "transaction_description")
+    private String transactionDescription;
+    @ColumnInfo(name = "transaction_description_match_group")
+    private Integer transactionDescriptionMatchGroup;
+    @ColumnInfo(name = "transaction_comment")
+    private String transactionComment;
+    @ColumnInfo(name = "transaction_comment_match_group")
+    private Integer transactionCommentMatchGroup;
+    @ColumnInfo(name = "date_year")
+    private Integer dateYear;
+    @ColumnInfo(name = "date_year_match_group")
+    private Integer dateYearMatchGroup;
+    @ColumnInfo(name = "date_month")
+    private Integer dateMonth;
+    @ColumnInfo(name = "date_month_match_group")
+    private Integer dateMonthMatchGroup;
+    @ColumnInfo(name = "date_day")
+    private Integer dateDay;
+    @ColumnInfo(name = "date_day_match_group")
+    private Integer dateDayMatchGroup;
+    public TemplateHeader(@NotNull Long id, @NonNull String name,
+                          @NonNull String regularExpression) {
+        this.id = id;
+        this.name = name;
+        this.regularExpression = regularExpression;
+    }
+    public String getTestText() {
+        return testText;
+    }
+    public void setTestText(String testText) {
+        this.testText = testText;
+    }
+    public Integer getTransactionDescriptionMatchGroup() {
+        return transactionDescriptionMatchGroup;
+    }
+    public void setTransactionDescriptionMatchGroup(Integer transactionDescriptionMatchGroup) {
+        this.transactionDescriptionMatchGroup = transactionDescriptionMatchGroup;
+    }
+    public Integer getTransactionCommentMatchGroup() {
+        return transactionCommentMatchGroup;
+    }
+    public void setTransactionCommentMatchGroup(Integer transactionCommentMatchGroup) {
+        this.transactionCommentMatchGroup = transactionCommentMatchGroup;
+    }
+    public Integer getDateYear() {
+        return dateYear;
+    }
+    public void setDateYear(Integer dateYear) {
+        this.dateYear = dateYear;
+    }
+    public Integer getDateMonth() {
+        return dateMonth;
+    }
+    public void setDateMonth(Integer dateMonth) {
+        this.dateMonth = dateMonth;
+    }
+    public Integer getDateDay() {
+        return dateDay;
+    }
+    public void setDateDay(Integer dateDay) {
+        this.dateDay = dateDay;
+    }
+    @NonNull
+    public Long getId() {
+        return id;
+    }
+    public void setId(@NonNull Long id) {
+        this.id = id;
+    }
+    @NonNull
+    public String getName() {
+        return name;
+    }
+    public void setName(@NonNull String name) {
+        this.name = name;
+    }
+    @NonNull
+    public String getRegularExpression() {
+        return regularExpression;
+    }
+    public void setRegularExpression(@NonNull String regularExpression) {
+        this.regularExpression = regularExpression;
+    }
+    public String getTransactionDescription() {
+        return transactionDescription;
+    }
+    public void setTransactionDescription(String transactionDescription) {
+        this.transactionDescription = transactionDescription;
+    }
+    public String getTransactionComment() {
+        return transactionComment;
+    }
+    public void setTransactionComment(String transactionComment) {
+        this.transactionComment = transactionComment;
+    }
+    public Integer getDateYearMatchGroup() {
+        return dateYearMatchGroup;
+    }
+    public void setDateYearMatchGroup(Integer dateYearMatchGroup) {
+        this.dateYearMatchGroup = dateYearMatchGroup;
+    }
+    public Integer getDateMonthMatchGroup() {
+        return dateMonthMatchGroup;
+    }
+    public void setDateMonthMatchGroup(Integer dateMonthMatchGroup) {
+        this.dateMonthMatchGroup = dateMonthMatchGroup;
+    }
+    public Integer getDateDayMatchGroup() {
+        return dateDayMatchGroup;
+    }
+    public void setDateDayMatchGroup(Integer dateDayMatchGroup) {
+        this.dateDayMatchGroup = dateDayMatchGroup;
+    }
+    @Override
+    public boolean equals(@Nullable Object obj) {
+        if (obj == null)
+            return false;
+        if (!(obj instanceof TemplateHeader))
+            return false;
+
+        TemplateHeader o = (TemplateHeader) obj;
+
+        return Misc.equalLongs(id, o.id) && Misc.equalStrings(name, o.name) &&
+               Misc.equalStrings(regularExpression, o.regularExpression) &&
+               Misc.equalStrings(transactionDescription, o.transactionDescription) &&
+               Misc.equalStrings(transactionComment, o.transactionComment) &&
+               Misc.equalIntegers(transactionDescriptionMatchGroup,
+                       o.transactionDescriptionMatchGroup) &&
+               Misc.equalIntegers(transactionCommentMatchGroup, o.transactionCommentMatchGroup) &&
+               Misc.equalIntegers(dateDay, o.dateDay) &&
+               Misc.equalIntegers(dateDayMatchGroup, o.dateDayMatchGroup) &&
+               Misc.equalIntegers(dateMonth, o.dateMonth) &&
+               Misc.equalIntegers(dateMonthMatchGroup, o.dateMonthMatchGroup) &&
+               Misc.equalIntegers(dateYear, o.dateYear) &&
+               Misc.equalIntegers(dateYearMatchGroup, o.dateYearMatchGroup);
+    }
+}
diff --git a/app/src/main/java/net/ktnx/mobileledger/model/PatternDetailSource.java b/app/src/main/java/net/ktnx/mobileledger/model/PatternDetailSource.java
deleted file mode 100644 (file)
index 5b8f378..0000000
+++ /dev/null
@@ -1,60 +0,0 @@
-/*
- * 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;
-    }
-}
diff --git a/app/src/main/java/net/ktnx/mobileledger/model/PatternDetailsItem.java b/app/src/main/java/net/ktnx/mobileledger/model/PatternDetailsItem.java
deleted file mode 100644 (file)
index eeab37a..0000000
+++ /dev/null
@@ -1,627 +0,0 @@
-/*
- * 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) {
-        this.type = type;
-    }
-    @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() {
-        return new AccountRow();
-    }
-    public static PatternDetailsItem fromRoomObject(PatternBase p) {
-        if (p instanceof PatternHeader) {
-            PatternHeader ph = (PatternHeader) p;
-            Header header = createHeader();
-            header.setId(ph.getId());
-            header.setName(ph.getName());
-            header.setPattern(ph.getRegularExpression());
-            header.setTestText(ph.getTestText());
-
-            if (ph.getTransactionDescriptionMatchGroup() == null)
-                header.setTransactionDescription(ph.getTransactionDescription());
-            else
-                header.setTransactionDescriptionMatchGroup(
-                        ph.getTransactionDescriptionMatchGroup());
-
-            if (ph.getTransactionCommentMatchGroup() == null)
-                header.setTransactionComment(ph.getTransactionComment());
-            else
-                header.setTransactionCommentMatchGroup(ph.getTransactionCommentMatchGroup());
-
-            if (ph.getDateDayMatchGroup() == null)
-                header.setDateDay(ph.getDateDay());
-            else
-                header.setDateDayMatchGroup(ph.getDateDayMatchGroup());
-
-            if (ph.getDateMonthMatchGroup() == null)
-                header.setDateMonth(ph.getDateMonth());
-            else
-                header.setDateMonthMatchGroup(ph.getDateMonthMatchGroup());
-
-            if (ph.getDateYearMatchGroup() == null)
-                header.setDateYear(ph.getDateYear());
-            else
-                header.setDateYearMatchGroup(ph.getDateYearMatchGroup());
-
-            return header;
-        }
-        else if (p instanceof PatternAccount) {
-            PatternAccount pa = (PatternAccount) p;
-            AccountRow acc = createAccountRow();
-            acc.setId(pa.getId());
-
-            if (pa.getAccountNameMatchGroup() == null)
-                acc.setAccountName(Misc.nullIsEmpty(pa.getAccountName()));
-            else
-                acc.setAccountNameMatchGroup(pa.getAccountNameMatchGroup());
-
-            if (pa.getAccountCommentMatchGroup() == null)
-                acc.setAccountComment(Misc.nullIsEmpty(pa.getAccountComment()));
-            else
-                acc.setAccountCommentMatchGroup(pa.getAccountCommentMatchGroup());
-
-            if (pa.getCurrencyMatchGroup() == null) {
-                final Integer currencyId = pa.getCurrency();
-                if (currencyId != null && currencyId > 0)
-                    acc.setCurrency(Currency.loadById(currencyId));
-            }
-            else
-                acc.setCurrencyMatchGroup(pa.getCurrencyMatchGroup());
-
-            final Integer amountMatchGroup = pa.getAmountMatchGroup();
-            if (amountMatchGroup != null && amountMatchGroup > 0) {
-                acc.setAmountMatchGroup(amountMatchGroup);
-                final Boolean negateAmount = pa.getNegateAmount();
-                acc.setNegateAmount(negateAmount != null && negateAmount);
-            }
-            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(Long id) {
-        this.id = id;
-    }
-    public void setId(int id) {
-        this.id = (long) id;
-    }
-    public long getPosition() {
-        return position;
-    }
-    public void setPosition(Long 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(Integer 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) {
-                if (value == null)
-                    return other.value == null;
-                return value.equals(other.value);
-            }
-            else
-                return matchGroup == other.matchGroup;
-        }
-        public void switchToLiteral() {
-            literalValue = true;
-        }
-        public String toString() {
-            if (literalValue)
-                if (value == null)
-                    return "<null>";
-                else
-                    return value.toString();
-            if (matchGroup > 0)
-                return "grp:" + matchGroup;
-            return "<null>";
-        }
-    }
-
-    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 boolean negateAmount;
-        private AccountRow() {
-            super(Type.ACCOUNT_ITEM);
-        }
-        public boolean isNegateAmount() {
-            return negateAmount;
-        }
-        public void setNegateAmount(boolean negateAmount) {
-            this.negateAmount = negateAmount;
-        }
-        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) && negateAmount == o.negateAmount;
-        }
-        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, 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());
-                result.setNegateAmount(null);
-            }
-            else {
-                result.setAmountMatchGroup(amount.getMatchGroup());
-                result.setNegateAmount(negateAmount ? true : null);
-            }
-
-            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<Integer> dateYear = PossiblyMatchedValue.withLiteralInt(null);
-        private PossiblyMatchedValue<Integer> dateMonth = PossiblyMatchedValue.withLiteralInt(null);
-        private PossiblyMatchedValue<Integer> dateDay = PossiblyMatchedValue.withLiteralInt(null);
-        private Header() {
-            super(Type.HEADER);
-        }
-        public Header(Header origin) {
-            this();
-            id = origin.id;
-            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] tran[%s] com[%s]", name, pattern,
-                           testText, transactionDescription, transactionComment);
-        }
-        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 Integer getDateYear() {
-            return dateYear.getValue();
-        }
-        public void setDateYear(Integer dateYear) {
-            this.dateYear.setValue(dateYear);
-        }
-        public Integer getDateMonth() {
-            return dateMonth.getValue();
-        }
-        public void setDateMonth(Integer dateMonth) {
-            this.dateMonth.setValue(dateMonth);
-        }
-        public Integer getDateDay() {
-            return dateDay.getValue();
-        }
-        public void setDateDay(Integer 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(int group) {
-            transactionDescription.setMatchGroup(group);
-        }
-        public int getTransactionCommentMatchGroup() {
-            return transactionComment.getMatchGroup();
-        }
-        public void setTransactionCommentMatchGroup(int 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, name, pattern);
-
-            if (Misc.emptyIsNull(testText) != null)
-                result.setTestText(testText);
-
-            if (transactionDescription.hasLiteralValue())
-                result.setTransactionDescription(transactionDescription.getValue());
-            else
-                result.setTransactionDescriptionMatchGroup(transactionDescription.getMatchGroup());
-
-            if (transactionComment.hasLiteralValue())
-                result.setTransactionComment(transactionComment.getValue());
-            else
-                result.setTransactionCommentMatchGroup(transactionComment.getMatchGroup());
-
-            if (dateYear.hasLiteralValue())
-                result.setDateYear(dateYear.getValue());
-            else
-                result.setDateYearMatchGroup(dateYear.getMatchGroup());
-
-            if (dateMonth.hasLiteralValue())
-                result.setDateMonth(dateMonth.getValue());
-            else
-                result.setDateMonthMatchGroup(dateMonth.getMatchGroup());
-
-            if (dateDay.hasLiteralValue())
-                result.setDateDay(dateDay.getValue());
-            else
-                result.setDateDayMatchGroup(dateDay.getMatchGroup());
-
-            return result;
-        }
-    }
-}
diff --git a/app/src/main/java/net/ktnx/mobileledger/model/TemplateDetailSource.java b/app/src/main/java/net/ktnx/mobileledger/model/TemplateDetailSource.java
new file mode 100644 (file)
index 0000000..611bcfc
--- /dev/null
@@ -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 <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 TemplateDetailSource implements Serializable {
+    public static final DiffUtil.ItemCallback<TemplateDetailSource> DIFF_CALLBACK =
+            new DiffUtil.ItemCallback<TemplateDetailSource>() {
+                @Override
+                public boolean areItemsTheSame(@NonNull TemplateDetailSource oldItem,
+                                               @NonNull TemplateDetailSource newItem) {
+                    return oldItem.groupNumber == newItem.groupNumber;
+                }
+                @Override
+                public boolean areContentsTheSame(@NonNull TemplateDetailSource oldItem,
+                                                  @NonNull TemplateDetailSource newItem) {
+                    return oldItem.matchedText.equals(newItem.matchedText);
+                }
+            };
+
+    private short groupNumber;
+    private String matchedText;
+    public TemplateDetailSource() {
+    }
+    public TemplateDetailSource(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/TemplateDetailsItem.java b/app/src/main/java/net/ktnx/mobileledger/model/TemplateDetailsItem.java
new file mode 100644 (file)
index 0000000..7199227
--- /dev/null
@@ -0,0 +1,627 @@
+/*
+ * 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.TemplateAccount;
+import net.ktnx.mobileledger.db.TemplateBase;
+import net.ktnx.mobileledger.db.TemplateHeader;
+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 TemplateDetailsItem {
+    private final Type type;
+    protected Long id;
+    protected Long position;
+
+    protected TemplateDetailsItem(Type type) {
+        this.type = type;
+    }
+    @Contract(" -> new")
+    public static @NotNull TemplateDetailsItem.Header createHeader() {
+        return new Header();
+    }
+    public static @NotNull TemplateDetailsItem.Header createHeader(Header origin) {
+        return new Header(origin);
+    }
+    @Contract("-> new")
+    public static @NotNull TemplateDetailsItem.AccountRow createAccountRow() {
+        return new AccountRow();
+    }
+    public static TemplateDetailsItem fromRoomObject(TemplateBase p) {
+        if (p instanceof TemplateHeader) {
+            TemplateHeader ph = (TemplateHeader) p;
+            Header header = createHeader();
+            header.setId(ph.getId());
+            header.setName(ph.getName());
+            header.setPattern(ph.getRegularExpression());
+            header.setTestText(ph.getTestText());
+
+            if (ph.getTransactionDescriptionMatchGroup() == null)
+                header.setTransactionDescription(ph.getTransactionDescription());
+            else
+                header.setTransactionDescriptionMatchGroup(
+                        ph.getTransactionDescriptionMatchGroup());
+
+            if (ph.getTransactionCommentMatchGroup() == null)
+                header.setTransactionComment(ph.getTransactionComment());
+            else
+                header.setTransactionCommentMatchGroup(ph.getTransactionCommentMatchGroup());
+
+            if (ph.getDateDayMatchGroup() == null)
+                header.setDateDay(ph.getDateDay());
+            else
+                header.setDateDayMatchGroup(ph.getDateDayMatchGroup());
+
+            if (ph.getDateMonthMatchGroup() == null)
+                header.setDateMonth(ph.getDateMonth());
+            else
+                header.setDateMonthMatchGroup(ph.getDateMonthMatchGroup());
+
+            if (ph.getDateYearMatchGroup() == null)
+                header.setDateYear(ph.getDateYear());
+            else
+                header.setDateYearMatchGroup(ph.getDateYearMatchGroup());
+
+            return header;
+        }
+        else if (p instanceof TemplateAccount) {
+            TemplateAccount pa = (TemplateAccount) p;
+            AccountRow acc = createAccountRow();
+            acc.setId(pa.getId());
+
+            if (pa.getAccountNameMatchGroup() == null)
+                acc.setAccountName(Misc.nullIsEmpty(pa.getAccountName()));
+            else
+                acc.setAccountNameMatchGroup(pa.getAccountNameMatchGroup());
+
+            if (pa.getAccountCommentMatchGroup() == null)
+                acc.setAccountComment(Misc.nullIsEmpty(pa.getAccountComment()));
+            else
+                acc.setAccountCommentMatchGroup(pa.getAccountCommentMatchGroup());
+
+            if (pa.getCurrencyMatchGroup() == null) {
+                final Integer currencyId = pa.getCurrency();
+                if (currencyId != null && currencyId > 0)
+                    acc.setCurrency(Currency.loadById(currencyId));
+            }
+            else
+                acc.setCurrencyMatchGroup(pa.getCurrencyMatchGroup());
+
+            final Integer amountMatchGroup = pa.getAmountMatchGroup();
+            if (amountMatchGroup != null && amountMatchGroup > 0) {
+                acc.setAmountMatchGroup(amountMatchGroup);
+                final Boolean negateAmount = pa.getNegateAmount();
+                acc.setNegateAmount(negateAmount != null && negateAmount);
+            }
+            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(Long id) {
+        this.id = id;
+    }
+    public void setId(int id) {
+        this.id = (long) id;
+    }
+    public long getPosition() {
+        return position;
+    }
+    public void setPosition(Long 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(Integer 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) {
+                if (value == null)
+                    return other.value == null;
+                return value.equals(other.value);
+            }
+            else
+                return matchGroup == other.matchGroup;
+        }
+        public void switchToLiteral() {
+            literalValue = true;
+        }
+        public String toString() {
+            if (literalValue)
+                if (value == null)
+                    return "<null>";
+                else
+                    return value.toString();
+            if (matchGroup > 0)
+                return "grp:" + matchGroup;
+            return "<null>";
+        }
+    }
+
+    public static class TYPE {
+        public static final int header = 0;
+        public static final int accountItem = 1;
+    }
+
+    public static class AccountRow extends TemplateDetailsItem {
+        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 boolean negateAmount;
+        private AccountRow() {
+            super(Type.ACCOUNT_ITEM);
+        }
+        public boolean isNegateAmount() {
+            return negateAmount;
+        }
+        public void setNegateAmount(boolean negateAmount) {
+            this.negateAmount = negateAmount;
+        }
+        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) && negateAmount == o.negateAmount;
+        }
+        public void switchToLiteralAmount() {
+            amount.switchToLiteral();
+        }
+        public void switchToLiteralAccountName() {
+            accountName.switchToLiteral();
+        }
+        public void switchToLiteralAccountComment() {
+            accountComment.switchToLiteral();
+        }
+        public TemplateAccount toDBO(@NonNull Long patternId) {
+            TemplateAccount result = new TemplateAccount(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());
+                result.setNegateAmount(null);
+            }
+            else {
+                result.setAmountMatchGroup(amount.getMatchGroup());
+                result.setNegateAmount(negateAmount ? true : null);
+            }
+
+            return result;
+        }
+    }
+
+    public static class Header extends TemplateDetailsItem {
+        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<Integer> dateYear = PossiblyMatchedValue.withLiteralInt(null);
+        private PossiblyMatchedValue<Integer> dateMonth = PossiblyMatchedValue.withLiteralInt(null);
+        private PossiblyMatchedValue<Integer> dateDay = PossiblyMatchedValue.withLiteralInt(null);
+        private Header() {
+            super(Type.HEADER);
+        }
+        public Header(Header origin) {
+            this();
+            id = origin.id;
+            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] tran[%s] com[%s]", name, pattern,
+                           testText, transactionDescription, transactionComment);
+        }
+        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 Integer getDateYear() {
+            return dateYear.getValue();
+        }
+        public void setDateYear(Integer dateYear) {
+            this.dateYear.setValue(dateYear);
+        }
+        public Integer getDateMonth() {
+            return dateMonth.getValue();
+        }
+        public void setDateMonth(Integer dateMonth) {
+            this.dateMonth.setValue(dateMonth);
+        }
+        public Integer getDateDay() {
+            return dateDay.getValue();
+        }
+        public void setDateDay(Integer 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(int group) {
+            transactionDescription.setMatchGroup(group);
+        }
+        public int getTransactionCommentMatchGroup() {
+            return transactionComment.getMatchGroup();
+        }
+        public void setTransactionCommentMatchGroup(int group) {
+            transactionComment.setMatchGroup(group);
+        }
+        public void switchToLiteralDateYear() {
+            dateYear.switchToLiteral();
+        }
+        public void switchToLiteralDateMonth() {
+            dateMonth.switchToLiteral();
+        }
+        public void switchToLiteralDateDay() { dateDay.switchToLiteral(); }
+        public TemplateHeader toDBO() {
+            TemplateHeader result = new TemplateHeader(id, name, pattern);
+
+            if (Misc.emptyIsNull(testText) != null)
+                result.setTestText(testText);
+
+            if (transactionDescription.hasLiteralValue())
+                result.setTransactionDescription(transactionDescription.getValue());
+            else
+                result.setTransactionDescriptionMatchGroup(transactionDescription.getMatchGroup());
+
+            if (transactionComment.hasLiteralValue())
+                result.setTransactionComment(transactionComment.getValue());
+            else
+                result.setTransactionCommentMatchGroup(transactionComment.getMatchGroup());
+
+            if (dateYear.hasLiteralValue())
+                result.setDateYear(dateYear.getValue());
+            else
+                result.setDateYearMatchGroup(dateYear.getMatchGroup());
+
+            if (dateMonth.hasLiteralValue())
+                result.setDateMonth(dateMonth.getValue());
+            else
+                result.setDateMonthMatchGroup(dateMonth.getMatchGroup());
+
+            if (dateDay.hasLiteralValue())
+                result.setDateDay(dateDay.getValue());
+            else
+                result.setDateDayMatchGroup(dateDay.getMatchGroup());
+
+            return result;
+        }
+    }
+}
diff --git a/app/src/main/java/net/ktnx/mobileledger/ui/PatternDetailSourceSelectorFragment.java b/app/src/main/java/net/ktnx/mobileledger/ui/PatternDetailSourceSelectorFragment.java
deleted file mode 100644 (file)
index 7a8a954..0000000
+++ /dev/null
@@ -1,188 +0,0 @@
-/*
- * 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
diff --git a/app/src/main/java/net/ktnx/mobileledger/ui/PatternDetailSourceSelectorModel.java b/app/src/main/java/net/ktnx/mobileledger/ui/PatternDetailSourceSelectorModel.java
deleted file mode 100644 (file)
index b88c676..0000000
+++ /dev/null
@@ -1,46 +0,0 @@
-/*
- * 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);
-    }
-}
diff --git a/app/src/main/java/net/ktnx/mobileledger/ui/PatternDetailSourceSelectorRecyclerViewAdapter.java b/app/src/main/java/net/ktnx/mobileledger/ui/PatternDetailSourceSelectorRecyclerViewAdapter.java
deleted file mode 100644 (file)
index 91a3c4d..0000000
+++ /dev/null
@@ -1,95 +0,0 @@
-/*
- * 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());
-        }
-    }
-}
diff --git a/app/src/main/java/net/ktnx/mobileledger/ui/TemplateDetailSourceSelectorFragment.java b/app/src/main/java/net/ktnx/mobileledger/ui/TemplateDetailSourceSelectorFragment.java
new file mode 100644 (file)
index 0000000..1bb4840
--- /dev/null
@@ -0,0 +1,188 @@
+/*
+ * 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;
+
+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.FragmentTemplateDetailSourceSelectorListBinding;
+import net.ktnx.mobileledger.model.TemplateDetailSource;
+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 TemplateDetailSourceSelectorFragment 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<TemplateDetailSource> mSources;
+    private TemplateDetailSourceSelectorModel 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 TemplateDetailSourceSelectorFragment() {
+    }
+    @SuppressWarnings("unused")
+    public static TemplateDetailSourceSelectorFragment newInstance() {
+        return newInstance(DEFAULT_COLUMN_COUNT, null, null);
+    }
+    public static TemplateDetailSourceSelectorFragment newInstance(int columnCount,
+                                                                   @Nullable String pattern,
+                                                                   @Nullable String testText) {
+        TemplateDetailSourceSelectorFragment fragment = new TemplateDetailSourceSelectorFragment();
+        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("templates",
+                            String.format("Trying to match pattern '%s' against text '%s'",
+                                    patternText, testText));
+                    if (matcher.matches()) {
+                        if (matcher.groupCount() >= 0) {
+                            ArrayList<TemplateDetailSource> list = new ArrayList<>();
+                            for (short g = 1; g <= matcher.groupCount(); g++) {
+                                list.add(new TemplateDetailSource(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);
+        FragmentTemplateDetailSourceSelectorListBinding b =
+                FragmentTemplateDetailSourceSelectorListBinding.inflate(
+                        LayoutInflater.from(context));
+        csd.setContentView(b.getRoot());
+        csd.setTitle(R.string.choose_template_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(TemplateDetailSourceSelectorModel.class);
+            if (onSourceSelectedListener != null)
+                model.setOnSourceSelectedListener(onSourceSelectedListener);
+            model.setSourcesList(mSources);
+
+            TemplateDetailSourceSelectorRecyclerViewAdapter adapter =
+                    new TemplateDetailSourceSelectorRecyclerViewAdapter();
+            model.groups.observe(this, adapter::submitList);
+
+            recyclerView.setAdapter(adapter);
+            adapter.setSourceSelectedListener(this);
+        }
+        else {
+            b.list.setVisibility(View.GONE);
+            b.templateError.setText(mPatternProblem);
+            b.templateError.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/TemplateDetailSourceSelectorModel.java b/app/src/main/java/net/ktnx/mobileledger/ui/TemplateDetailSourceSelectorModel.java
new file mode 100644 (file)
index 0000000..46f5885
--- /dev/null
@@ -0,0 +1,46 @@
+/*
+ * 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;
+
+import androidx.lifecycle.MutableLiveData;
+import androidx.lifecycle.ViewModel;
+
+import net.ktnx.mobileledger.model.TemplateDetailSource;
+
+import java.util.ArrayList;
+import java.util.List;
+
+public class TemplateDetailSourceSelectorModel extends ViewModel {
+    public final MutableLiveData<List<TemplateDetailSource>> groups = new MutableLiveData<>();
+    private OnSourceSelectedListener selectionListener;
+    public TemplateDetailSourceSelectorModel() {
+    }
+    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<TemplateDetailSource> mSources) {
+        groups.setValue(mSources);
+    }
+}
diff --git a/app/src/main/java/net/ktnx/mobileledger/ui/TemplateDetailSourceSelectorRecyclerViewAdapter.java b/app/src/main/java/net/ktnx/mobileledger/ui/TemplateDetailSourceSelectorRecyclerViewAdapter.java
new file mode 100644 (file)
index 0000000..060bf13
--- /dev/null
@@ -0,0 +1,95 @@
+/*
+ * 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;
+
+import android.view.LayoutInflater;
+import android.view.ViewGroup;
+
+import androidx.recyclerview.widget.ListAdapter;
+import androidx.recyclerview.widget.RecyclerView;
+
+import net.ktnx.mobileledger.databinding.FragmentTemplateDetailSourceSelectorBinding;
+import net.ktnx.mobileledger.model.TemplateDetailSource;
+
+import org.jetbrains.annotations.NotNull;
+
+/**
+ * {@link RecyclerView.Adapter} that can display a {@link TemplateDetailSource} and makes a call
+ * to the
+ * specified {@link OnSourceSelectedListener}.
+ */
+public class TemplateDetailSourceSelectorRecyclerViewAdapter extends
+        ListAdapter<TemplateDetailSource,
+                TemplateDetailSourceSelectorRecyclerViewAdapter.ViewHolder> {
+
+    private OnSourceSelectedListener sourceSelectedListener;
+    public TemplateDetailSourceSelectorRecyclerViewAdapter() {
+        super(TemplateDetailSource.DIFF_CALLBACK);
+    }
+    @NotNull
+    @Override
+    public ViewHolder onCreateViewHolder(@NotNull ViewGroup parent, int viewType) {
+        FragmentTemplateDetailSourceSelectorBinding b =
+                FragmentTemplateDetailSourceSelectorBinding.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(TemplateDetailSource 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 FragmentTemplateDetailSourceSelectorBinding b;
+        private TemplateDetailSource mItem;
+
+        ViewHolder(FragmentTemplateDetailSourceSelectorBinding 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(TemplateDetailSource item) {
+            mItem = item;
+            b.groupNumber.setText(String.valueOf(item.getGroupNumber()));
+            b.matchedText.setText(item.getMatchedText());
+        }
+    }
+}
index 3ed892913f5ffd85b14bda661c14fa031ea227b4..291ad7c4bb459a87fb5e0063530d45c6c3e4d43f 100644 (file)
@@ -55,8 +55,8 @@ import net.ktnx.mobileledger.model.MobileLedgerProfile;
 import net.ktnx.mobileledger.ui.MainModel;
 import net.ktnx.mobileledger.ui.account_summary.AccountSummaryFragment;
 import net.ktnx.mobileledger.ui.new_transaction.NewTransactionActivity;
-import net.ktnx.mobileledger.ui.patterns.PatternsActivity;
 import net.ktnx.mobileledger.ui.profiles.ProfilesRecyclerViewAdapter;
+import net.ktnx.mobileledger.ui.templates.TemplatesActivity;
 import net.ktnx.mobileledger.ui.transaction_list.TransactionListFragment;
 import net.ktnx.mobileledger.utils.Colors;
 import net.ktnx.mobileledger.utils.Logger;
@@ -302,7 +302,7 @@ public class MainActivity extends ProfileThemedActivity {
         b.navPatterns.setOnClickListener(this::onPatternsClick);
     }
     private void onPatternsClick(View view) {
-        Intent intent = new Intent(this, PatternsActivity.class);
+        Intent intent = new Intent(this, TemplatesActivity.class);
         startActivity(intent);
     }
     private void scheduleDataRetrievalIfStale(long lastUpdate) {
index 6be1bd3790bba7984d1874b112def0b6d12f6028..e708c5fe3f2d7a37e88e684fdede50b52f57d3b3 100644 (file)
@@ -49,15 +49,15 @@ import com.google.android.material.snackbar.Snackbar;
 
 import net.ktnx.mobileledger.R;
 import net.ktnx.mobileledger.db.DB;
-import net.ktnx.mobileledger.db.PatternAccount;
-import net.ktnx.mobileledger.db.PatternHeader;
+import net.ktnx.mobileledger.db.TemplateAccount;
+import net.ktnx.mobileledger.db.TemplateHeader;
 import net.ktnx.mobileledger.json.API;
 import net.ktnx.mobileledger.model.Data;
 import net.ktnx.mobileledger.model.LedgerTransaction;
 import net.ktnx.mobileledger.model.LedgerTransactionAccount;
 import net.ktnx.mobileledger.model.MobileLedgerProfile;
 import net.ktnx.mobileledger.ui.QRScanCapableFragment;
-import net.ktnx.mobileledger.ui.patterns.PatternsActivity;
+import net.ktnx.mobileledger.ui.templates.TemplatesActivity;
 import net.ktnx.mobileledger.utils.Logger;
 import net.ktnx.mobileledger.utils.Misc;
 import net.ktnx.mobileledger.utils.SimpleDate;
@@ -90,9 +90,9 @@ public class NewTransactionFragment extends QRScanCapableFragment {
         setHasOptionsMenu(true);
     }
     private void startNewPatternActivity(String scanned) {
-        Intent intent = new Intent(requireContext(), PatternsActivity.class);
+        Intent intent = new Intent(requireContext(), TemplatesActivity.class);
         Bundle args = new Bundle();
-        args.putString(PatternsActivity.ARG_ADD_PATTERN, scanned);
+        args.putString(TemplatesActivity.ARG_ADD_TEMPLATE, scanned);
         requireContext().startActivity(intent, args);
     }
     private void alertNoPatternMatch(String scanned) {
@@ -110,13 +110,13 @@ public class NewTransactionFragment extends QRScanCapableFragment {
         if (Misc.emptyIsNull(text) == null)
             return;
 
-        LiveData<List<PatternHeader>> allPatterns = DB.get()
-                                                      .getPatternDAO()
-                                                      .getPatterns();
+        LiveData<List<TemplateHeader>> allPatterns = DB.get()
+                                                       .getPatternDAO()
+                                                       .getPatterns();
         allPatterns.observe(getViewLifecycleOwner(), patternHeaders -> {
-            ArrayList<PatternHeader> matchingPatterns = new ArrayList<>();
+            ArrayList<TemplateHeader> matchingPatterns = new ArrayList<>();
 
-            for (PatternHeader ph : patternHeaders) {
+            for (TemplateHeader ph : patternHeaders) {
                 String patternSource = ph.getRegularExpression();
                 if (Misc.emptyIsNull(patternSource) == null)
                     continue;
@@ -147,7 +147,7 @@ public class NewTransactionFragment extends QRScanCapableFragment {
                 choosePattern(matchingPatterns, text);
         });
     }
-    private void choosePattern(ArrayList<PatternHeader> matchingPatterns, String matchedText) {
+    private void choosePattern(ArrayList<TemplateHeader> matchingPatterns, String matchedText) {
         final String patternNameColumn = "name";
         AbstractCursor cursor = new AbstractCursor() {
             @Override
@@ -207,7 +207,7 @@ public class NewTransactionFragment extends QRScanCapableFragment {
                .create()
                .show();
     }
-    private void applyPattern(PatternHeader patternHeader, String text) {
+    private void applyPattern(TemplateHeader patternHeader, String text) {
         Pattern pattern = Pattern.compile(patternHeader.getRegularExpression());
 
         Matcher m = pattern.matcher(text);
@@ -262,7 +262,7 @@ public class NewTransactionFragment extends QRScanCapableFragment {
           .observe(getViewLifecycleOwner(), entry -> {
               int rowIndex = 0;
               final boolean accountsInInitialState = viewModel.accountsInInitialState();
-              for (PatternAccount acc : entry.accounts) {
+              for (TemplateAccount acc : entry.accounts) {
                   rowIndex++;
 
                   String accountName = extractStringFromMatches(m, acc.getAccountNameMatchGroup(),
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
deleted file mode 100644 (file)
index 4fd35a5..0000000
+++ /dev/null
@@ -1,624 +0,0 @@
-/*
- * 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.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.QRScanCapableFragment;
-import net.ktnx.mobileledger.utils.Logger;
-import net.ktnx.mobileledger.utils.Misc;
-
-import org.jetbrains.annotations.NotNull;
-
-import java.text.ParseException;
-import java.util.ArrayList;
-import java.util.List;
-import java.util.Locale;
-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()
-                           .equals(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();
-            }
-            @Override
-            public boolean areContentsTheSame(@NonNull PatternDetailsItem oldItem,
-                                              @NonNull PatternDetailsItem newItem) {
-                if (oldItem.getType()
-                           .equals(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) {
-        // header item is always first and IDs id may duplicate some of the account IDs
-        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;
-
-        final String testText = Misc.nullIsEmpty(header.getTestText());
-        Matcher m = p.matcher(testText);
-        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 {
-        ViewHolder(@NonNull View itemView) {
-            super(itemView);
-        }
-        abstract void bind(PatternDetailsItem item);
-    }
-
-    public class Header extends ViewHolder {
-        private final PatternDetailsHeaderBinding b;
-        public Header(@NonNull PatternDetailsHeaderBinding binding) {
-            super(binding.getRoot());
-            b = binding;
-
-            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) {
-                    final PatternDetailsItem.Header header = getItem();
-                    Logger.debug(D_PATTERN_UI,
-                            "Storing changed pattern name " + s + "; header=" + header);
-                    header.setName(String.valueOf(s));
-                }
-            };
-            b.patternName.addTextChangedListener(patternNameWatcher);
-            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) {
-                    final PatternDetailsItem.Header header = getItem();
-                    Logger.debug(D_PATTERN_UI,
-                            "Storing changed pattern " + s + "; header=" + header);
-                    header.setPattern(String.valueOf(s));
-                }
-            };
-            b.pattern.addTextChangedListener(patternWatcher);
-            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) {
-                    final PatternDetailsItem.Header header = getItem();
-                    Logger.debug(D_PATTERN_UI,
-                            "Storing changed test text " + s + "; header=" + header);
-                    header.setTestText(String.valueOf(s));
-                }
-            };
-            b.testText.addTextChangedListener(testTextWatcher);
-            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) {
-                    final PatternDetailsItem.Header header = getItem();
-                    Logger.debug(D_PATTERN_UI,
-                            "Storing changed transaction description " + s + "; header=" + header);
-                    header.setTransactionDescription(String.valueOf(s));
-                }
-            };
-            b.transactionDescription.addTextChangedListener(transactionDescriptionWatcher);
-            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) {
-                    final PatternDetailsItem.Header header = getItem();
-                    Logger.debug(D_PATTERN_UI,
-                            "Storing changed transaction description " + s + "; header=" + header);
-                    header.setTransactionComment(String.valueOf(s));
-                }
-            };
-            b.transactionComment.addTextChangedListener(transactionCommentWatcher);
-        }
-        @NotNull
-        private PatternDetailsItem.Header getItem() {
-            int pos = getAdapterPosition();
-            return differ.getCurrentList()
-                         .get(pos)
-                         .asHeaderItem();
-        }
-        private void selectHeaderDetailSource(View v, HeaderDetail detail) {
-            PatternDetailsItem.Header header = getItem();
-            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();
-            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);
-                final Integer dateYear = header.getDateYear();
-                b.patternDetailsDateYear.setText(
-                        (dateYear == null) ? null : String.valueOf(dateYear));
-                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, HeaderDetail.DATE_YEAR));
-            b.patternDetailsYearSource.setOnClickListener(
-                    v -> selectHeaderDetailSource(v, HeaderDetail.DATE_YEAR));
-
-            if (header.hasLiteralDateMonth()) {
-                b.patternDetailsMonthSource.setText(R.string.pattern_details_source_literal);
-                final Integer dateMonth = header.getDateMonth();
-                b.patternDetailsDateMonth.setText(
-                        (dateMonth == null) ? null : String.valueOf(dateMonth));
-                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, HeaderDetail.DATE_MONTH));
-            b.patternDetailsMonthSource.setOnClickListener(
-                    v -> selectHeaderDetailSource(v, HeaderDetail.DATE_MONTH));
-
-            if (header.hasLiteralDateDay()) {
-                b.patternDetailsDaySource.setText(R.string.pattern_details_source_literal);
-                final Integer dateDay = header.getDateDay();
-                b.patternDetailsDateDay.setText((dateDay == null) ? null : String.valueOf(dateDay));
-                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, HeaderDetail.DATE_DAY));
-            b.patternDetailsDaySource.setOnClickListener(
-                    v -> selectHeaderDetailSource(v, 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, HeaderDetail.DESCRIPTION));
-            b.patternTransactionDescriptionSource.setOnClickListener(
-                    v -> selectHeaderDetailSource(v, 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, HeaderDetail.COMMENT));
-            b.patternTransactionCommentSource.setOnClickListener(
-                    v -> selectHeaderDetailSource(v, HeaderDetail.COMMENT));
-
-            b.patternDetailsHeadScanQrButton.setOnClickListener(this::scanTestQR);
-
-        }
-        private void scanTestQR(View view) {
-            QRScanCapableFragment.triggerQRScan();
-        }
-    }
-
-    public class AccountRow extends ViewHolder {
-        private final PatternDetailsAccountBinding b;
-        public AccountRow(@NonNull PatternDetailsAccountBinding binding) {
-            super(binding.getRoot());
-            b = binding;
-
-            TextWatcher accountNameWatcher = 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.AccountRow accRow = getItem();
-                    Logger.debug(D_PATTERN_UI,
-                            "Storing changed account name " + s + "; accRow=" + accRow);
-                    accRow.setAccountName(String.valueOf(s));
-                }
-            };
-            b.patternDetailsAccountName.addTextChangedListener(accountNameWatcher);
-            TextWatcher accountCommentWatcher = 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.AccountRow accRow = getItem();
-                    Logger.debug(D_PATTERN_UI,
-                            "Storing changed account comment " + s + "; accRow=" + accRow);
-                    accRow.setAccountComment(String.valueOf(s));
-                }
-            };
-            b.patternDetailsAccountComment.addTextChangedListener(accountCommentWatcher);
-
-            b.patternDetailsAccountAmount.addTextChangedListener(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.AccountRow accRow = getItem();
-
-                    String str = String.valueOf(s);
-                    if (Misc.emptyIsNull(str) == null) {
-                        accRow.setAmount(null);
-                    }
-                    else {
-                        try {
-                            final float amount = Data.parseNumber(str);
-                            accRow.setAmount(amount);
-                            b.patternDetailsAccountAmountLayout.setError(null);
-
-                            Logger.debug(D_PATTERN_UI, String.format(Locale.US,
-                                    "Storing changed account amount %s [%4.2f]; accRow=%s", s,
-                                    amount, accRow));
-                        }
-                        catch (NumberFormatException | ParseException e) {
-                            b.patternDetailsAccountAmountLayout.setError("!");
-                        }
-                    }
-                }
-            });
-            b.patternDetailsAccountAmount.setOnFocusChangeListener((v, hasFocus) -> {
-                if (hasFocus)
-                    return;
-
-                PatternDetailsItem.AccountRow accRow = getItem();
-                if (!accRow.hasLiteralAmount())
-                    return;
-                Float amt = accRow.getAmount();
-                if (amt == null)
-                    return;
-
-                b.patternDetailsAccountAmount.setText(Data.formatNumber(amt));
-            });
-
-            b.negateAmountSwitch.setOnCheckedChangeListener(
-                    (buttonView, isChecked) -> getItem().setNegateAmount(isChecked));
-        }
-        @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);
-                Float amt = accRow.getAmount();
-                b.patternDetailsAccountAmount.setText((amt == null) ? null : String.format(
-                        Data.locale.getValue(), "%,4.2f", (accRow.getAmount())));
-                b.negateAmountSwitch.setVisibility(View.GONE);
-            }
-            else {
-                b.patternDetailsAccountAmountSource.setText(
-                        String.format(Locale.US, "Group %d (%s)", accRow.getAmountMatchGroup(),
-                                getMatchGroupText(accRow.getAmountMatchGroup())));
-                b.patternDetailsAccountAmountLayout.setVisibility(View.GONE);
-                b.negateAmountSwitch.setVisibility(View.VISIBLE);
-                b.negateAmountSwitch.setChecked(accRow.isNegateAmount());
-            }
-
-            b.patternAccountNameSourceLabel.setOnClickListener(
-                    v -> selectAccountRowDetailSource(v, AccDetail.ACCOUNT));
-            b.patternDetailsAccountNameSource.setOnClickListener(
-                    v -> selectAccountRowDetailSource(v, AccDetail.ACCOUNT));
-            b.patternAccountCommentSourceLabel.setOnClickListener(
-                    v -> selectAccountRowDetailSource(v, AccDetail.COMMENT));
-            b.patternDetailsAccountCommentSource.setOnClickListener(
-                    v -> selectAccountRowDetailSource(v, AccDetail.COMMENT));
-            b.patternAccountAmountSourceLabel.setOnClickListener(
-                    v -> selectAccountRowDetailSource(v, AccDetail.AMOUNT));
-            b.patternDetailsAccountAmountSource.setOnClickListener(
-                    v -> selectAccountRowDetailSource(v, AccDetail.AMOUNT));
-        }
-        private @NotNull PatternDetailsItem.AccountRow getItem() {
-            return differ.getCurrentList()
-                         .get(getAdapterPosition())
-                         .asAccountRowItem();
-        }
-        private void selectAccountRowDetailSource(View v, AccDetail detail) {
-            PatternDetailsItem.AccountRow accRow = getItem();
-            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
deleted file mode 100644 (file)
index 63bbe9d..0000000
+++ /dev/null
@@ -1,113 +0,0 @@
-/*
- * 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.QRScanCapableFragment;
-import net.ktnx.mobileledger.utils.Logger;
-
-public class PatternDetailsFragment extends QRScanCapableFragment {
-    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 Long mPatternId;
-    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.getLong(ARG_PATTERN_ID, -1);
-            if (mPatternId == -1)
-                mPatternId = null;
-        }
-    }
-    @Override
-    public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container,
-                             @Nullable Bundle savedInstanceState) {
-        NavController controller = ((PatternsActivity) requireActivity()).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.onCreateView(): model=" + mViewModel);
-
-        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(mPatternId)
-                  .observe(getViewLifecycleOwner(), adapter::setItems);
-
-        return b.getRoot();
-    }
-    @Override
-    protected void onQrScanned(String text) {
-        Logger.debug("PatDet_fr", String.format("Got scanned text '%s'", text));
-        if (text != null)
-            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
deleted file mode 100644 (file)
index 982bb47..0000000
+++ /dev/null
@@ -1,179 +0,0 @@
-/*
- * 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.os.AsyncTask;
-
-import androidx.lifecycle.LiveData;
-import androidx.lifecycle.MutableLiveData;
-import androidx.lifecycle.Observer;
-import androidx.lifecycle.ViewModel;
-
-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.db.PatternWithAccounts;
-import net.ktnx.mobileledger.model.PatternDetailsItem;
-import net.ktnx.mobileledger.utils.Logger;
-
-import java.util.ArrayList;
-import java.util.Collections;
-import java.util.List;
-import java.util.Locale;
-import java.util.Objects;
-
-public class PatternDetailsViewModel extends ViewModel {
-    private final MutableLiveData<List<PatternDetailsItem>> items =
-            new MutableLiveData<>(Collections.emptyList());
-    private Long mPatternId;
-    private String mDefaultPatternName;
-    public String getDefaultPatternName() {
-        return mDefaultPatternName;
-    }
-    public void setDefaultPatternName(String name) {
-        mDefaultPatternName = name;
-    }
-
-    public void resetItems() {
-        ArrayList<PatternDetailsItem> newList = new ArrayList<>();
-        final PatternDetailsItem.Header header = PatternDetailsItem.createHeader();
-        header.setName(mDefaultPatternName);
-        header.setId(0);
-        newList.add(header);
-
-        while (newList.size() < 3) {
-            final PatternDetailsItem.AccountRow aRow = PatternDetailsItem.createAccountRow();
-            aRow.setId(newList.size() + 1);
-            newList.add(aRow);
-        }
-
-        items.setValue(newList);
-    }
-    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());
-            changes = true;
-        }
-
-        if (changes)
-            items.setValue(newList);
-    }
-    public LiveData<List<PatternDetailsItem>> getItems(Long patternId) {
-        if (patternId != null && patternId <= 0)
-            throw new IllegalArgumentException("Pattern ID " + patternId + " is invalid");
-
-        mPatternId = patternId;
-
-        if (mPatternId == null) {
-            resetItems();
-            return items;
-        }
-
-        DB db = DB.get();
-        LiveData<PatternWithAccounts> dbList = db.getPatternDAO()
-                                                 .getPatternWithAccounts(mPatternId);
-        Observer<PatternWithAccounts> observer = new Observer<PatternWithAccounts>() {
-            @Override
-            public void onChanged(PatternWithAccounts src) {
-                ArrayList<PatternDetailsItem> l = new ArrayList<>();
-
-                PatternDetailsItem header = PatternDetailsItem.fromRoomObject(src.header);
-                l.add(header);
-                for (PatternAccount acc : src.accounts) {
-                    l.add(PatternDetailsItem.fromRoomObject(acc));
-                }
-
-                for (PatternDetailsItem i : l) {
-                    Logger.debug("patterns-db", "Loaded pattern item " + i);
-                }
-                items.postValue(l);
-
-                dbList.removeObserver(this);
-            }
-        };
-        dbList.observeForever(observer);
-
-        return items;
-    }
-    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 onSavePattern() {
-        Logger.debug("flow", "PatternDetailsViewModel.onSavePattern(); model=" + this);
-        final List<PatternDetailsItem> list = Objects.requireNonNull(items.getValue());
-
-        AsyncTask.execute(() -> {
-            boolean newPattern = mPatternId == null || mPatternId <= 0;
-
-            PatternDetailsItem.Header modelHeader = list.get(0)
-                                                        .asHeaderItem();
-            PatternHeaderDAO headerDAO = DB.get()
-                                           .getPatternDAO();
-            PatternHeader dbHeader = modelHeader.toDBO();
-            if (newPattern) {
-                dbHeader.setId(null);
-                dbHeader.setId(mPatternId = headerDAO.insert(dbHeader));
-            }
-            else
-                headerDAO.update(dbHeader);
-
-            Logger.debug("pattern-db",
-                    String.format(Locale.US, "Stored pattern header %d, item=%s", dbHeader.getId(),
-                            modelHeader));
-
-
-            PatternAccountDAO paDAO = DB.get()
-                                        .getPatternAccountDAO();
-            for (int i = 1; i < list.size(); i++) {
-                final PatternDetailsItem.AccountRow accRowItem = list.get(i)
-                                                                     .asAccountRowItem();
-                PatternAccount dbAccount = accRowItem.toDBO(dbHeader.getId());
-                dbAccount.setPatternId(mPatternId);
-                dbAccount.setPosition(i);
-                if (newPattern) {
-                    dbAccount.setId(null);
-                    dbAccount.setId(paDAO.insert(dbAccount));
-                }
-                else
-                    paDAO.update(dbAccount);
-
-                Logger.debug("pattern-db", String.format(Locale.US,
-                        "Stored pattern account %d, account=%s, comment=%s, neg=%s, item=%s",
-                        dbAccount.getId(), dbAccount.getAccountName(),
-                        dbAccount.getAccountComment(), dbAccount.getNegateAmount(), accRowItem));
-            }
-        });
-    }
-}
\ 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
deleted file mode 100644 (file)
index 3f8dbb1..0000000
+++ /dev/null
@@ -1,135 +0,0 @@
-/*
- * 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.fragment.app.Fragment;
-import androidx.lifecycle.Lifecycle;
-import androidx.lifecycle.LifecycleEventObserver;
-import androidx.lifecycle.LifecycleOwner;
-import androidx.lifecycle.LiveData;
-import androidx.recyclerview.widget.LinearLayoutManager;
-import androidx.recyclerview.widget.RecyclerView;
-
-import net.ktnx.mobileledger.dao.PatternHeaderDAO;
-import net.ktnx.mobileledger.databinding.FragmentPatternListBinding;
-import net.ktnx.mobileledger.db.DB;
-import net.ktnx.mobileledger.db.PatternHeader;
-import net.ktnx.mobileledger.utils.Logger;
-
-import org.jetbrains.annotations.NotNull;
-
-import java.util.List;
-
-/**
- * A simple {@link Fragment} subclass.
- * Use the {@link PatternListFragment#newInstance} factory method to
- * create an instance of this fragment.
- */
-public class PatternListFragment extends Fragment {
-    private FragmentPatternListBinding b;
-    private OnPatternListFragmentInteractionListener mListener;
-
-    public PatternListFragment() {
-        // Required empty public constructor
-    }
-    /**
-     * Use this factory method to create a new instance of
-     * this fragment using the provided parameters.
-     *
-     * @return A new instance of fragment PatternListFragment.
-     */
-    // TODO: Rename and change types and number of parameters
-    public static PatternListFragment newInstance() {
-        PatternListFragment fragment = new PatternListFragment();
-        Bundle args = new Bundle();
-        fragment.setArguments(args);
-        return fragment;
-    }
-    @Override
-    public void onCreate(Bundle savedInstanceState) {
-        super.onCreate(savedInstanceState);
-//        if (getArguments() != null) {
-//            mParam1 = getArguments().getString(ARG_PARAM1);
-//            mParam2 = getArguments().getString(ARG_PARAM2);
-//        }
-    }
-
-    @Override
-    public View onCreateView(@NotNull LayoutInflater inflater, ViewGroup container,
-                             Bundle savedInstanceState) {
-        Logger.debug("flow", "PatternListFragment.onCreateView()");
-        b = FragmentPatternListBinding.inflate(inflater);
-
-        PatternsRecyclerViewAdapter modelAdapter = new PatternsRecyclerViewAdapter();
-
-        b.patternList.setAdapter(modelAdapter);
-        PatternHeaderDAO pDao = DB.get()
-                                  .getPatternDAO();
-        LiveData<List<PatternHeader>> patterns = pDao.getPatterns();
-        patterns.observe(getViewLifecycleOwner(), modelAdapter::setPatterns);
-        LinearLayoutManager llm = new LinearLayoutManager(getContext());
-        llm.setOrientation(RecyclerView.VERTICAL);
-        b.patternList.setLayoutManager(llm);
-        return b.getRoot();
-    }
-    @Override
-    public void onAttach(@NonNull Context context) {
-        super.onAttach(context);
-        if (context instanceof OnPatternListFragmentInteractionListener) {
-            mListener = (OnPatternListFragmentInteractionListener) context;
-        }
-        else {
-            throw new RuntimeException(
-                    context.toString() + " must implement OnFragmentInteractionListener");
-        }
-
-        final LifecycleEventObserver observer = new LifecycleEventObserver() {
-            @Override
-            public void onStateChanged(@NonNull LifecycleOwner source,
-                                       @NonNull Lifecycle.Event event) {
-                if (event.getTargetState() == Lifecycle.State.CREATED) {
-//                    getActivity().setActionBar(b.toolbar);
-                    getLifecycle().removeObserver(this);
-                }
-            }
-        };
-        getLifecycle().addObserver(observer);
-    }
-    /**
-     * This interface must be implemented by activities that contain this
-     * fragment to allow an interaction in this fragment to be communicated
-     * to the activity and potentially other fragments contained in that
-     * activity.
-     * <p>
-     * See the Android Training lesson <a href=
-     * "http://developer.android.com/training/basics/fragments/communicating.html"
-     * >Communicating with Other Fragments</a> for more information.
-     */
-    public interface OnPatternListFragmentInteractionListener {
-        void onSavePattern();
-
-        void onEditPattern(Long 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
deleted file mode 100644 (file)
index 8dc8e32..0000000
+++ /dev/null
@@ -1,38 +0,0 @@
-/*
- * 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 androidx.annotation.NonNull;
-import androidx.recyclerview.widget.RecyclerView;
-
-import net.ktnx.mobileledger.databinding.PatternLayoutBinding;
-import net.ktnx.mobileledger.db.PatternHeader;
-
-class PatternViewHolder extends RecyclerView.ViewHolder {
-    final PatternLayoutBinding b;
-    public PatternViewHolder(@NonNull PatternLayoutBinding binding) {
-        super(binding.getRoot());
-        b = binding;
-    }
-    public void bindToItem(PatternHeader item) {
-        b.patternName.setText(item.getName());
-        b.editButton.setOnClickListener(v -> {
-            ((PatternsActivity) v.getContext()).onEditPattern(item.getId());
-        });
-    }
-}
diff --git a/app/src/main/java/net/ktnx/mobileledger/ui/patterns/PatternsRecyclerViewAdapter.java b/app/src/main/java/net/ktnx/mobileledger/ui/patterns/PatternsRecyclerViewAdapter.java
deleted file mode 100644 (file)
index 642a5ca..0000000
+++ /dev/null
@@ -1,74 +0,0 @@
-/*
- * 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.view.LayoutInflater;
-import android.view.ViewGroup;
-
-import androidx.annotation.NonNull;
-import androidx.recyclerview.widget.AsyncListDiffer;
-import androidx.recyclerview.widget.DiffUtil;
-import androidx.recyclerview.widget.RecyclerView;
-
-import net.ktnx.mobileledger.databinding.PatternLayoutBinding;
-import net.ktnx.mobileledger.db.PatternHeader;
-
-import org.jetbrains.annotations.NotNull;
-
-import java.util.List;
-
-public class PatternsRecyclerViewAdapter extends RecyclerView.Adapter<PatternViewHolder> {
-    private final AsyncListDiffer<PatternHeader> listDiffer;
-    public PatternsRecyclerViewAdapter() {
-        listDiffer = new AsyncListDiffer<>(this, new DiffUtil.ItemCallback<PatternHeader>() {
-            @Override
-            public boolean areItemsTheSame(@NotNull PatternHeader oldItem,
-                                           @NotNull PatternHeader newItem) {
-                return oldItem.getId()
-                              .equals(newItem.getId());
-            }
-            @Override
-            public boolean areContentsTheSame(@NotNull PatternHeader oldItem,
-                                              @NotNull PatternHeader newItem) {
-                return oldItem.equals(newItem);
-            }
-        });
-    }
-    @NonNull
-    @Override
-    public PatternViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
-        PatternLayoutBinding b =
-                PatternLayoutBinding.inflate(LayoutInflater.from(parent.getContext()), parent,
-                        false);
-
-        return new PatternViewHolder(b);
-    }
-    @Override
-    public void onBindViewHolder(@NonNull PatternViewHolder holder, int position) {
-        holder.bindToItem(listDiffer.getCurrentList()
-                                    .get(position));
-    }
-    @Override
-    public int getItemCount() {
-        return listDiffer.getCurrentList()
-                         .size();
-    }
-    public void setPatterns(List<PatternHeader> newList) {
-        listDiffer.submitList(newList);
-    }
-}
diff --git a/app/src/main/java/net/ktnx/mobileledger/ui/templates/TemplateDetailsAdapter.java b/app/src/main/java/net/ktnx/mobileledger/ui/templates/TemplateDetailsAdapter.java
new file mode 100644 (file)
index 0000000..4f70f84
--- /dev/null
@@ -0,0 +1,627 @@
+/*
+ * 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.templates;
+
+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.TemplateDetailsAccountBinding;
+import net.ktnx.mobileledger.databinding.TemplateDetailsHeaderBinding;
+import net.ktnx.mobileledger.db.TemplateBase;
+import net.ktnx.mobileledger.model.Data;
+import net.ktnx.mobileledger.model.TemplateDetailsItem;
+import net.ktnx.mobileledger.ui.QRScanCapableFragment;
+import net.ktnx.mobileledger.ui.TemplateDetailSourceSelectorFragment;
+import net.ktnx.mobileledger.utils.Logger;
+import net.ktnx.mobileledger.utils.Misc;
+
+import org.jetbrains.annotations.NotNull;
+
+import java.text.ParseException;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Locale;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
+class TemplateDetailsAdapter extends RecyclerView.Adapter<TemplateDetailsAdapter.ViewHolder> {
+    private static final String D_TEMPLATE_UI = "template-ui";
+    private final AsyncListDiffer<TemplateDetailsItem> differ;
+    public TemplateDetailsAdapter() {
+        super();
+        setHasStableIds(true);
+        differ = new AsyncListDiffer<>(this, new DiffUtil.ItemCallback<TemplateDetailsItem>() {
+            @Override
+            public boolean areItemsTheSame(@NonNull TemplateDetailsItem oldItem,
+                                           @NonNull TemplateDetailsItem newItem) {
+                if (oldItem.getType() != newItem.getType())
+                    return false;
+                if (oldItem.getType()
+                           .equals(TemplateDetailsItem.Type.HEADER))
+                    return true;    // only one header item, ever
+                // the rest is comparing two account row items
+                return oldItem.asAccountRowItem()
+                              .getId() == newItem.asAccountRowItem()
+                                                 .getId();
+            }
+            @Override
+            public boolean areContentsTheSame(@NonNull TemplateDetailsItem oldItem,
+                                              @NonNull TemplateDetailsItem newItem) {
+                if (oldItem.getType()
+                           .equals(TemplateDetailsItem.Type.HEADER))
+                {
+                    TemplateDetailsItem.Header oldHeader = oldItem.asHeaderItem();
+                    TemplateDetailsItem.Header newHeader = newItem.asHeaderItem();
+
+                    return oldHeader.equalContents(newHeader);
+                }
+                else {
+                    TemplateDetailsItem.AccountRow oldAcc = oldItem.asAccountRowItem();
+                    TemplateDetailsItem.AccountRow newAcc = newItem.asAccountRowItem();
+
+                    return oldAcc.equalContents(newAcc);
+                }
+            }
+        });
+    }
+    @Override
+    public long getItemId(int position) {
+        // header item is always first and IDs id may duplicate some of the account IDs
+        if (position == 0)
+            return -1;
+        TemplateDetailsItem.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 TemplateDetailsAdapter.ViewHolder onCreateViewHolder(@NonNull ViewGroup parent,
+                                                                int viewType) {
+        final LayoutInflater inflater = LayoutInflater.from(parent.getContext());
+        switch (viewType) {
+            case TemplateDetailsItem.TYPE.header:
+                return new Header(TemplateDetailsHeaderBinding.inflate(inflater, parent, false));
+            case TemplateDetailsItem.TYPE.accountItem:
+                return new AccountRow(
+                        TemplateDetailsAccountBinding.inflate(inflater, parent, false));
+            default:
+                throw new IllegalStateException("Unsupported view type " + viewType);
+        }
+    }
+    @Override
+    public void onBindViewHolder(@NonNull TemplateDetailsAdapter.ViewHolder holder, int position) {
+        TemplateDetailsItem item = differ.getCurrentList()
+                                         .get(position);
+        holder.bind(item);
+    }
+    @Override
+    public int getItemCount() {
+        return differ.getCurrentList()
+                     .size();
+    }
+    public void setTemplateItems(List<TemplateBase> items) {
+        ArrayList<TemplateDetailsItem> list = new ArrayList<>();
+        for (TemplateBase p : items) {
+            TemplateDetailsItem item = TemplateDetailsItem.fromRoomObject(p);
+            list.add(item);
+        }
+        setItems(list);
+    }
+    public void setItems(List<TemplateDetailsItem> items) {
+        differ.submitList(items);
+    }
+    public String getMatchGroupText(int groupNumber) {
+        TemplateDetailsItem.Header header = getHeader();
+        Pattern p = header.getCompiledPattern();
+        if (p == null)
+            return null;
+
+        final String testText = Misc.nullIsEmpty(header.getTestText());
+        Matcher m = p.matcher(testText);
+        if (m.matches() && m.groupCount() >= groupNumber)
+            return m.group(groupNumber);
+        else
+            return null;
+    }
+    protected TemplateDetailsItem.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 {
+        ViewHolder(@NonNull View itemView) {
+            super(itemView);
+        }
+        abstract void bind(TemplateDetailsItem item);
+    }
+
+    public class Header extends ViewHolder {
+        private final TemplateDetailsHeaderBinding b;
+        public Header(@NonNull TemplateDetailsHeaderBinding binding) {
+            super(binding.getRoot());
+            b = binding;
+
+            TextWatcher templateNameWatcher = 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) {
+                    final TemplateDetailsItem.Header header = getItem();
+                    Logger.debug(D_TEMPLATE_UI,
+                            "Storing changed template name " + s + "; header=" + header);
+                    header.setName(String.valueOf(s));
+                }
+            };
+            b.templateName.addTextChangedListener(templateNameWatcher);
+            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) {
+                    final TemplateDetailsItem.Header header = getItem();
+                    Logger.debug(D_TEMPLATE_UI,
+                            "Storing changed pattern " + s + "; header=" + header);
+                    header.setPattern(String.valueOf(s));
+                }
+            };
+            b.pattern.addTextChangedListener(patternWatcher);
+            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) {
+                    final TemplateDetailsItem.Header header = getItem();
+                    Logger.debug(D_TEMPLATE_UI,
+                            "Storing changed test text " + s + "; header=" + header);
+                    header.setTestText(String.valueOf(s));
+                }
+            };
+            b.testText.addTextChangedListener(testTextWatcher);
+            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) {
+                    final TemplateDetailsItem.Header header = getItem();
+                    Logger.debug(D_TEMPLATE_UI,
+                            "Storing changed transaction description " + s + "; header=" + header);
+                    header.setTransactionDescription(String.valueOf(s));
+                }
+            };
+            b.transactionDescription.addTextChangedListener(transactionDescriptionWatcher);
+            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) {
+                    final TemplateDetailsItem.Header header = getItem();
+                    Logger.debug(D_TEMPLATE_UI,
+                            "Storing changed transaction description " + s + "; header=" + header);
+                    header.setTransactionComment(String.valueOf(s));
+                }
+            };
+            b.transactionComment.addTextChangedListener(transactionCommentWatcher);
+        }
+        @NotNull
+        private TemplateDetailsItem.Header getItem() {
+            int pos = getAdapterPosition();
+            return differ.getCurrentList()
+                         .get(pos)
+                         .asHeaderItem();
+        }
+        private void selectHeaderDetailSource(View v, HeaderDetail detail) {
+            TemplateDetailsItem.Header header = getItem();
+            Logger.debug(D_TEMPLATE_UI, "header is " + header);
+            TemplateDetailSourceSelectorFragment sel =
+                    TemplateDetailSourceSelectorFragment.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(), "template-details-source-selector");
+        }
+        @Override
+        void bind(TemplateDetailsItem item) {
+            TemplateDetailsItem.Header header = item.asHeaderItem();
+            Logger.debug(D_TEMPLATE_UI, "Binding to header " + header);
+
+            b.templateName.setText(header.getName());
+            b.pattern.setText(header.getPattern());
+            b.testText.setText(header.getTestText());
+
+            if (header.hasLiteralDateYear()) {
+                b.templateDetailsYearSource.setText(R.string.template_details_source_literal);
+                final Integer dateYear = header.getDateYear();
+                b.templateDetailsDateYear.setText(
+                        (dateYear == null) ? null : String.valueOf(dateYear));
+                b.templateDetailsDateYearLayout.setVisibility(View.VISIBLE);
+            }
+            else {
+                b.templateDetailsDateYearLayout.setVisibility(View.GONE);
+                b.templateDetailsYearSource.setText(
+                        String.format(Locale.US, "Group %d (%s)", header.getDateYearMatchGroup(),
+                                getMatchGroupText(header.getDateYearMatchGroup())));
+            }
+            b.templateDetailsYearSourceLabel.setOnClickListener(
+                    v -> selectHeaderDetailSource(v, HeaderDetail.DATE_YEAR));
+            b.templateDetailsYearSource.setOnClickListener(
+                    v -> selectHeaderDetailSource(v, HeaderDetail.DATE_YEAR));
+
+            if (header.hasLiteralDateMonth()) {
+                b.templateDetailsMonthSource.setText(R.string.template_details_source_literal);
+                final Integer dateMonth = header.getDateMonth();
+                b.templateDetailsDateMonth.setText(
+                        (dateMonth == null) ? null : String.valueOf(dateMonth));
+                b.templateDetailsDateMonthLayout.setVisibility(View.VISIBLE);
+            }
+            else {
+                b.templateDetailsDateMonthLayout.setVisibility(View.GONE);
+                b.templateDetailsMonthSource.setText(
+                        String.format(Locale.US, "Group %d (%s)", header.getDateMonthMatchGroup(),
+                                getMatchGroupText(header.getDateMonthMatchGroup())));
+            }
+            b.templateDetailsMonthSourceLabel.setOnClickListener(
+                    v -> selectHeaderDetailSource(v, HeaderDetail.DATE_MONTH));
+            b.templateDetailsMonthSource.setOnClickListener(
+                    v -> selectHeaderDetailSource(v, HeaderDetail.DATE_MONTH));
+
+            if (header.hasLiteralDateDay()) {
+                b.templateDetailsDaySource.setText(R.string.template_details_source_literal);
+                final Integer dateDay = header.getDateDay();
+                b.templateDetailsDateDay.setText(
+                        (dateDay == null) ? null : String.valueOf(dateDay));
+                b.templateDetailsDateDayLayout.setVisibility(View.VISIBLE);
+            }
+            else {
+                b.templateDetailsDateDayLayout.setVisibility(View.GONE);
+                b.templateDetailsDaySource.setText(
+                        String.format(Locale.US, "Group %d (%s)", header.getDateDayMatchGroup(),
+                                getMatchGroupText(header.getDateDayMatchGroup())));
+            }
+            b.templateDetailsDaySourceLabel.setOnClickListener(
+                    v -> selectHeaderDetailSource(v, HeaderDetail.DATE_DAY));
+            b.templateDetailsDaySource.setOnClickListener(
+                    v -> selectHeaderDetailSource(v, HeaderDetail.DATE_DAY));
+
+            if (header.hasLiteralTransactionDescription()) {
+                b.templateTransactionDescriptionSource.setText(
+                        R.string.template_details_source_literal);
+                b.transactionDescription.setText(header.getTransactionDescription());
+                b.transactionDescriptionLayout.setVisibility(View.VISIBLE);
+            }
+            else {
+                b.transactionDescriptionLayout.setVisibility(View.GONE);
+                b.templateTransactionDescriptionSource.setText(
+                        String.format(Locale.US, "Group %d (%s)",
+                                header.getTransactionDescriptionMatchGroup(),
+                                getMatchGroupText(header.getTransactionDescriptionMatchGroup())));
+
+            }
+            b.templateTransactionDescriptionSourceLabel.setOnClickListener(
+                    v -> selectHeaderDetailSource(v, HeaderDetail.DESCRIPTION));
+            b.templateTransactionDescriptionSource.setOnClickListener(
+                    v -> selectHeaderDetailSource(v, HeaderDetail.DESCRIPTION));
+
+            if (header.hasLiteralTransactionComment()) {
+                b.templateTransactionCommentSource.setText(
+                        R.string.template_details_source_literal);
+                b.transactionComment.setText(header.getTransactionComment());
+                b.transactionCommentLayout.setVisibility(View.VISIBLE);
+            }
+            else {
+                b.transactionCommentLayout.setVisibility(View.GONE);
+                b.templateTransactionCommentSource.setText(String.format(Locale.US, "Group %d (%s)",
+                        header.getTransactionCommentMatchGroup(),
+                        getMatchGroupText(header.getTransactionCommentMatchGroup())));
+
+            }
+            b.templateTransactionCommentSourceLabel.setOnClickListener(
+                    v -> selectHeaderDetailSource(v, HeaderDetail.COMMENT));
+            b.templateTransactionCommentSource.setOnClickListener(
+                    v -> selectHeaderDetailSource(v, HeaderDetail.COMMENT));
+
+            b.templateDetailsHeadScanQrButton.setOnClickListener(this::scanTestQR);
+
+        }
+        private void scanTestQR(View view) {
+            QRScanCapableFragment.triggerQRScan();
+        }
+    }
+
+    public class AccountRow extends ViewHolder {
+        private final TemplateDetailsAccountBinding b;
+        public AccountRow(@NonNull TemplateDetailsAccountBinding binding) {
+            super(binding.getRoot());
+            b = binding;
+
+            TextWatcher accountNameWatcher = 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) {
+                    TemplateDetailsItem.AccountRow accRow = getItem();
+                    Logger.debug(D_TEMPLATE_UI,
+                            "Storing changed account name " + s + "; accRow=" + accRow);
+                    accRow.setAccountName(String.valueOf(s));
+                }
+            };
+            b.templateDetailsAccountName.addTextChangedListener(accountNameWatcher);
+            TextWatcher accountCommentWatcher = 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) {
+                    TemplateDetailsItem.AccountRow accRow = getItem();
+                    Logger.debug(D_TEMPLATE_UI,
+                            "Storing changed account comment " + s + "; accRow=" + accRow);
+                    accRow.setAccountComment(String.valueOf(s));
+                }
+            };
+            b.templateDetailsAccountComment.addTextChangedListener(accountCommentWatcher);
+
+            b.templateDetailsAccountAmount.addTextChangedListener(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) {
+                    TemplateDetailsItem.AccountRow accRow = getItem();
+
+                    String str = String.valueOf(s);
+                    if (Misc.emptyIsNull(str) == null) {
+                        accRow.setAmount(null);
+                    }
+                    else {
+                        try {
+                            final float amount = Data.parseNumber(str);
+                            accRow.setAmount(amount);
+                            b.templateDetailsAccountAmountLayout.setError(null);
+
+                            Logger.debug(D_TEMPLATE_UI, String.format(Locale.US,
+                                    "Storing changed account amount %s [%4.2f]; accRow=%s", s,
+                                    amount, accRow));
+                        }
+                        catch (NumberFormatException | ParseException e) {
+                            b.templateDetailsAccountAmountLayout.setError("!");
+                        }
+                    }
+                }
+            });
+            b.templateDetailsAccountAmount.setOnFocusChangeListener((v, hasFocus) -> {
+                if (hasFocus)
+                    return;
+
+                TemplateDetailsItem.AccountRow accRow = getItem();
+                if (!accRow.hasLiteralAmount())
+                    return;
+                Float amt = accRow.getAmount();
+                if (amt == null)
+                    return;
+
+                b.templateDetailsAccountAmount.setText(Data.formatNumber(amt));
+            });
+
+            b.negateAmountSwitch.setOnCheckedChangeListener(
+                    (buttonView, isChecked) -> getItem().setNegateAmount(isChecked));
+        }
+        @Override
+        void bind(TemplateDetailsItem item) {
+            TemplateDetailsItem.AccountRow accRow = item.asAccountRowItem();
+            if (accRow.hasLiteralAccountName()) {
+                b.templateDetailsAccountNameLayout.setVisibility(View.VISIBLE);
+                b.templateDetailsAccountName.setText(accRow.getAccountName());
+                b.templateDetailsAccountNameSource.setText(
+                        R.string.template_details_source_literal);
+            }
+            else {
+                b.templateDetailsAccountNameLayout.setVisibility(View.GONE);
+                b.templateDetailsAccountNameSource.setText(
+                        String.format(Locale.US, "Group %d (%s)", accRow.getAccountNameMatchGroup(),
+                                getMatchGroupText(accRow.getAccountNameMatchGroup())));
+            }
+
+            if (accRow.hasLiteralAccountComment()) {
+                b.templateDetailsAccountCommentLayout.setVisibility(View.VISIBLE);
+                b.templateDetailsAccountComment.setText(accRow.getAccountComment());
+                b.templateDetailsAccountCommentSource.setText(
+                        R.string.template_details_source_literal);
+            }
+            else {
+                b.templateDetailsAccountCommentLayout.setVisibility(View.GONE);
+                b.templateDetailsAccountCommentSource.setText(
+                        String.format(Locale.US, "Group %d (%s)",
+                                accRow.getAccountCommentMatchGroup(),
+                                getMatchGroupText(accRow.getAccountCommentMatchGroup())));
+            }
+
+            if (accRow.hasLiteralAmount()) {
+                b.templateDetailsAccountAmountSource.setText(
+                        R.string.template_details_source_literal);
+                b.templateDetailsAccountAmount.setVisibility(View.VISIBLE);
+                Float amt = accRow.getAmount();
+                b.templateDetailsAccountAmount.setText((amt == null) ? null : String.format(
+                        Data.locale.getValue(), "%,4.2f", (accRow.getAmount())));
+                b.negateAmountSwitch.setVisibility(View.GONE);
+            }
+            else {
+                b.templateDetailsAccountAmountSource.setText(
+                        String.format(Locale.US, "Group %d (%s)", accRow.getAmountMatchGroup(),
+                                getMatchGroupText(accRow.getAmountMatchGroup())));
+                b.templateDetailsAccountAmountLayout.setVisibility(View.GONE);
+                b.negateAmountSwitch.setVisibility(View.VISIBLE);
+                b.negateAmountSwitch.setChecked(accRow.isNegateAmount());
+            }
+
+            b.templateAccountNameSourceLabel.setOnClickListener(
+                    v -> selectAccountRowDetailSource(v, AccDetail.ACCOUNT));
+            b.templateDetailsAccountNameSource.setOnClickListener(
+                    v -> selectAccountRowDetailSource(v, AccDetail.ACCOUNT));
+            b.templateAccountCommentSourceLabel.setOnClickListener(
+                    v -> selectAccountRowDetailSource(v, AccDetail.COMMENT));
+            b.templateDetailsAccountCommentSource.setOnClickListener(
+                    v -> selectAccountRowDetailSource(v, AccDetail.COMMENT));
+            b.templateAccountAmountSourceLabel.setOnClickListener(
+                    v -> selectAccountRowDetailSource(v, AccDetail.AMOUNT));
+            b.templateDetailsAccountAmountSource.setOnClickListener(
+                    v -> selectAccountRowDetailSource(v, AccDetail.AMOUNT));
+        }
+        private @NotNull TemplateDetailsItem.AccountRow getItem() {
+            return differ.getCurrentList()
+                         .get(getAdapterPosition())
+                         .asAccountRowItem();
+        }
+        private void selectAccountRowDetailSource(View v, AccDetail detail) {
+            TemplateDetailsItem.AccountRow accRow = getItem();
+            final TemplateDetailsItem.Header header = getHeader();
+            Logger.debug(D_TEMPLATE_UI, "header is " + header);
+            TemplateDetailSourceSelectorFragment sel =
+                    TemplateDetailSourceSelectorFragment.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(), "template-details-source-selector");
+        }
+    }
+}
diff --git a/app/src/main/java/net/ktnx/mobileledger/ui/templates/TemplateDetailsFragment.java b/app/src/main/java/net/ktnx/mobileledger/ui/templates/TemplateDetailsFragment.java
new file mode 100644 (file)
index 0000000..94e6989
--- /dev/null
@@ -0,0 +1,113 @@
+/*
+ * 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.templates;
+
+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.TemplateDetailsFragmentBinding;
+import net.ktnx.mobileledger.ui.QRScanCapableFragment;
+import net.ktnx.mobileledger.utils.Logger;
+
+public class TemplateDetailsFragment extends QRScanCapableFragment {
+    static final String ARG_TEMPLATE_ID = "pattern-id";
+    private static final String ARG_COLUMN_COUNT = "column-count";
+    TemplateDetailsFragmentBinding b;
+    private TemplateDetailsViewModel mViewModel;
+    private int mColumnCount = 1;
+    private Long mPatternId;
+    public TemplateDetailsFragment() {
+    }
+    public static TemplateDetailsFragment newInstance(int columnCount, int patternId) {
+        final TemplateDetailsFragment fragment = new TemplateDetailsFragment();
+        Bundle args = new Bundle();
+        args.putInt(ARG_COLUMN_COUNT, columnCount);
+        if (patternId > 0)
+            args.putInt(ARG_TEMPLATE_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.getLong(ARG_TEMPLATE_ID, -1);
+            if (mPatternId == -1)
+                mPatternId = null;
+        }
+    }
+    @Override
+    public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container,
+                             @Nullable Bundle savedInstanceState) {
+        NavController controller = ((TemplatesActivity) requireActivity()).getNavController();
+        final ViewModelStoreOwner viewModelStoreOwner =
+                controller.getViewModelStoreOwner(R.id.template_list_navigation);
+        mViewModel = new ViewModelProvider(viewModelStoreOwner).get(TemplateDetailsViewModel.class);
+        mViewModel.setDefaultPatternName(getString(R.string.unnamed_pattern));
+        Logger.debug("flow", "PatternDetailsFragment.onCreateView(): model=" + mViewModel);
+
+        b = TemplateDetailsFragmentBinding.inflate(inflater);
+        Context context = b.patternDetailsRecyclerView.getContext();
+        if (mColumnCount <= 1) {
+            b.patternDetailsRecyclerView.setLayoutManager(new LinearLayoutManager(context));
+        }
+        else {
+            b.patternDetailsRecyclerView.setLayoutManager(
+                    new GridLayoutManager(context, mColumnCount));
+        }
+
+
+        TemplateDetailsAdapter adapter = new TemplateDetailsAdapter();
+        b.patternDetailsRecyclerView.setAdapter(adapter);
+        mViewModel.getItems(mPatternId)
+                  .observe(getViewLifecycleOwner(), adapter::setItems);
+
+        return b.getRoot();
+    }
+    @Override
+    protected void onQrScanned(String text) {
+        Logger.debug("PatDet_fr", String.format("Got scanned text '%s'", text));
+        if (text != null)
+            mViewModel.setTestText(text);
+    }
+    public void onSavePattern() {
+        mViewModel.onSaveTemplate();
+        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/templates/TemplateDetailsViewModel.java b/app/src/main/java/net/ktnx/mobileledger/ui/templates/TemplateDetailsViewModel.java
new file mode 100644 (file)
index 0000000..088b1b7
--- /dev/null
@@ -0,0 +1,179 @@
+/*
+ * 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.templates;
+
+import android.os.AsyncTask;
+
+import androidx.lifecycle.LiveData;
+import androidx.lifecycle.MutableLiveData;
+import androidx.lifecycle.Observer;
+import androidx.lifecycle.ViewModel;
+
+import net.ktnx.mobileledger.dao.PatternAccountDAO;
+import net.ktnx.mobileledger.dao.PatternHeaderDAO;
+import net.ktnx.mobileledger.db.DB;
+import net.ktnx.mobileledger.db.PatternWithAccounts;
+import net.ktnx.mobileledger.db.TemplateAccount;
+import net.ktnx.mobileledger.db.TemplateHeader;
+import net.ktnx.mobileledger.model.TemplateDetailsItem;
+import net.ktnx.mobileledger.utils.Logger;
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+import java.util.Locale;
+import java.util.Objects;
+
+public class TemplateDetailsViewModel extends ViewModel {
+    private final MutableLiveData<List<TemplateDetailsItem>> items =
+            new MutableLiveData<>(Collections.emptyList());
+    private Long mPatternId;
+    private String mDefaultPatternName;
+    public String getDefaultPatternName() {
+        return mDefaultPatternName;
+    }
+    public void setDefaultPatternName(String name) {
+        mDefaultPatternName = name;
+    }
+
+    public void resetItems() {
+        ArrayList<TemplateDetailsItem> newList = new ArrayList<>();
+        final TemplateDetailsItem.Header header = TemplateDetailsItem.createHeader();
+        header.setName(mDefaultPatternName);
+        header.setId(0);
+        newList.add(header);
+
+        while (newList.size() < 3) {
+            final TemplateDetailsItem.AccountRow aRow = TemplateDetailsItem.createAccountRow();
+            aRow.setId(newList.size() + 1);
+            newList.add(aRow);
+        }
+
+        items.setValue(newList);
+    }
+    private void checkItemConsistency() {
+        ArrayList<TemplateDetailsItem> newList = new ArrayList<>(items.getValue());
+        boolean changes = false;
+        if (newList.size() < 1) {
+            final TemplateDetailsItem.Header header = TemplateDetailsItem.createHeader();
+            header.setName(mDefaultPatternName);
+            newList.add(header);
+            changes = true;
+        }
+
+        while (newList.size() < 3) {
+            newList.add(TemplateDetailsItem.createAccountRow());
+            changes = true;
+        }
+
+        if (changes)
+            items.setValue(newList);
+    }
+    public LiveData<List<TemplateDetailsItem>> getItems(Long patternId) {
+        if (patternId != null && patternId <= 0)
+            throw new IllegalArgumentException("Pattern ID " + patternId + " is invalid");
+
+        mPatternId = patternId;
+
+        if (mPatternId == null) {
+            resetItems();
+            return items;
+        }
+
+        DB db = DB.get();
+        LiveData<PatternWithAccounts> dbList = db.getPatternDAO()
+                                                 .getPatternWithAccounts(mPatternId);
+        Observer<PatternWithAccounts> observer = new Observer<PatternWithAccounts>() {
+            @Override
+            public void onChanged(PatternWithAccounts src) {
+                ArrayList<TemplateDetailsItem> l = new ArrayList<>();
+
+                TemplateDetailsItem header = TemplateDetailsItem.fromRoomObject(src.header);
+                l.add(header);
+                for (TemplateAccount acc : src.accounts) {
+                    l.add(TemplateDetailsItem.fromRoomObject(acc));
+                }
+
+                for (TemplateDetailsItem i : l) {
+                    Logger.debug("patterns-db", "Loaded pattern item " + i);
+                }
+                items.postValue(l);
+
+                dbList.removeObserver(this);
+            }
+        };
+        dbList.observeForever(observer);
+
+        return items;
+    }
+    public void setTestText(String text) {
+        List<TemplateDetailsItem> list = new ArrayList<>(items.getValue());
+        TemplateDetailsItem.Header header = new TemplateDetailsItem.Header(list.get(0)
+                                                                               .asHeaderItem());
+        header.setTestText(text);
+        list.set(0, header);
+
+        items.setValue(list);
+    }
+    public void onSaveTemplate() {
+        Logger.debug("flow", "PatternDetailsViewModel.onSavePattern(); model=" + this);
+        final List<TemplateDetailsItem> list = Objects.requireNonNull(items.getValue());
+
+        AsyncTask.execute(() -> {
+            boolean newPattern = mPatternId == null || mPatternId <= 0;
+
+            TemplateDetailsItem.Header modelHeader = list.get(0)
+                                                         .asHeaderItem();
+            PatternHeaderDAO headerDAO = DB.get()
+                                           .getPatternDAO();
+            TemplateHeader dbHeader = modelHeader.toDBO();
+            if (newPattern) {
+                dbHeader.setId(null);
+                dbHeader.setId(mPatternId = headerDAO.insert(dbHeader));
+            }
+            else
+                headerDAO.update(dbHeader);
+
+            Logger.debug("pattern-db",
+                    String.format(Locale.US, "Stored pattern header %d, item=%s", dbHeader.getId(),
+                            modelHeader));
+
+
+            PatternAccountDAO paDAO = DB.get()
+                                        .getPatternAccountDAO();
+            for (int i = 1; i < list.size(); i++) {
+                final TemplateDetailsItem.AccountRow accRowItem = list.get(i)
+                                                                      .asAccountRowItem();
+                TemplateAccount dbAccount = accRowItem.toDBO(dbHeader.getId());
+                dbAccount.setPatternId(mPatternId);
+                dbAccount.setPosition(i);
+                if (newPattern) {
+                    dbAccount.setId(null);
+                    dbAccount.setId(paDAO.insert(dbAccount));
+                }
+                else
+                    paDAO.update(dbAccount);
+
+                Logger.debug("pattern-db", String.format(Locale.US,
+                        "Stored pattern account %d, account=%s, comment=%s, neg=%s, item=%s",
+                        dbAccount.getId(), dbAccount.getAccountName(),
+                        dbAccount.getAccountComment(), dbAccount.getNegateAmount(), accRowItem));
+            }
+        });
+    }
+}
\ No newline at end of file
diff --git a/app/src/main/java/net/ktnx/mobileledger/ui/templates/TemplateListFragment.java b/app/src/main/java/net/ktnx/mobileledger/ui/templates/TemplateListFragment.java
new file mode 100644 (file)
index 0000000..cc5c798
--- /dev/null
@@ -0,0 +1,135 @@
+/*
+ * 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.templates;
+
+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.fragment.app.Fragment;
+import androidx.lifecycle.Lifecycle;
+import androidx.lifecycle.LifecycleEventObserver;
+import androidx.lifecycle.LifecycleOwner;
+import androidx.lifecycle.LiveData;
+import androidx.recyclerview.widget.LinearLayoutManager;
+import androidx.recyclerview.widget.RecyclerView;
+
+import net.ktnx.mobileledger.dao.PatternHeaderDAO;
+import net.ktnx.mobileledger.databinding.FragmentTemplateListBinding;
+import net.ktnx.mobileledger.db.DB;
+import net.ktnx.mobileledger.db.TemplateHeader;
+import net.ktnx.mobileledger.utils.Logger;
+
+import org.jetbrains.annotations.NotNull;
+
+import java.util.List;
+
+/**
+ * A simple {@link Fragment} subclass.
+ * Use the {@link TemplateListFragment#newInstance} factory method to
+ * create an instance of this fragment.
+ */
+public class TemplateListFragment extends Fragment {
+    private FragmentTemplateListBinding b;
+    private OnTemplateListFragmentInteractionListener mListener;
+
+    public TemplateListFragment() {
+        // Required empty public constructor
+    }
+    /**
+     * Use this factory method to create a new instance of
+     * this fragment using the provided parameters.
+     *
+     * @return A new instance of fragment TemplateListFragment.
+     */
+    // TODO: Rename and change types and number of parameters
+    public static TemplateListFragment newInstance() {
+        TemplateListFragment fragment = new TemplateListFragment();
+        Bundle args = new Bundle();
+        fragment.setArguments(args);
+        return fragment;
+    }
+    @Override
+    public void onCreate(Bundle savedInstanceState) {
+        super.onCreate(savedInstanceState);
+//        if (getArguments() != null) {
+//            mParam1 = getArguments().getString(ARG_PARAM1);
+//            mParam2 = getArguments().getString(ARG_PARAM2);
+//        }
+    }
+
+    @Override
+    public View onCreateView(@NotNull LayoutInflater inflater, ViewGroup container,
+                             Bundle savedInstanceState) {
+        Logger.debug("flow", "PatternListFragment.onCreateView()");
+        b = FragmentTemplateListBinding.inflate(inflater);
+
+        TemplatesRecyclerViewAdapter modelAdapter = new TemplatesRecyclerViewAdapter();
+
+        b.templateList.setAdapter(modelAdapter);
+        PatternHeaderDAO pDao = DB.get()
+                                  .getPatternDAO();
+        LiveData<List<TemplateHeader>> templates = pDao.getPatterns();
+        templates.observe(getViewLifecycleOwner(), modelAdapter::setTemplates);
+        LinearLayoutManager llm = new LinearLayoutManager(getContext());
+        llm.setOrientation(RecyclerView.VERTICAL);
+        b.templateList.setLayoutManager(llm);
+        return b.getRoot();
+    }
+    @Override
+    public void onAttach(@NonNull Context context) {
+        super.onAttach(context);
+        if (context instanceof OnTemplateListFragmentInteractionListener) {
+            mListener = (OnTemplateListFragmentInteractionListener) context;
+        }
+        else {
+            throw new RuntimeException(
+                    context.toString() + " must implement OnFragmentInteractionListener");
+        }
+
+        final LifecycleEventObserver observer = new LifecycleEventObserver() {
+            @Override
+            public void onStateChanged(@NonNull LifecycleOwner source,
+                                       @NonNull Lifecycle.Event event) {
+                if (event.getTargetState() == Lifecycle.State.CREATED) {
+//                    getActivity().setActionBar(b.toolbar);
+                    getLifecycle().removeObserver(this);
+                }
+            }
+        };
+        getLifecycle().addObserver(observer);
+    }
+    /**
+     * This interface must be implemented by activities that contain this
+     * fragment to allow an interaction in this fragment to be communicated
+     * to the activity and potentially other fragments contained in that
+     * activity.
+     * <p>
+     * See the Android Training lesson <a href=
+     * "http://developer.android.com/training/basics/fragments/communicating.html"
+     * >Communicating with Other Fragments</a> for more information.
+     */
+    public interface OnTemplateListFragmentInteractionListener {
+        void onSaveTemplate();
+
+        void onEditTemplate(Long id);
+    }
+}
\ No newline at end of file
diff --git a/app/src/main/java/net/ktnx/mobileledger/ui/templates/TemplateViewHolder.java b/app/src/main/java/net/ktnx/mobileledger/ui/templates/TemplateViewHolder.java
new file mode 100644 (file)
index 0000000..e9b9032
--- /dev/null
@@ -0,0 +1,38 @@
+/*
+ * 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.templates;
+
+import androidx.annotation.NonNull;
+import androidx.recyclerview.widget.RecyclerView;
+
+import net.ktnx.mobileledger.databinding.TemplateListTemplateItemBinding;
+import net.ktnx.mobileledger.db.TemplateHeader;
+
+class TemplateViewHolder extends RecyclerView.ViewHolder {
+    final TemplateListTemplateItemBinding b;
+    public TemplateViewHolder(@NonNull TemplateListTemplateItemBinding binding) {
+        super(binding.getRoot());
+        b = binding;
+    }
+    public void bindToItem(TemplateHeader item) {
+        b.templateName.setText(item.getName());
+        b.editButton.setOnClickListener(v -> {
+            ((TemplatesActivity) v.getContext()).onEditTemplate(item.getId());
+        });
+    }
+}
index 0f1d20f451debb3a6d6102dfb19224ecae8638f8..7bcece63d7e17f3e0aa67937e4cc6a23f41b85b5 100644 (file)
@@ -15,7 +15,7 @@
  * along with MoLe. If not, see <https://www.gnu.org/licenses/>.
  */
 
-package net.ktnx.mobileledger.ui.patterns;
+package net.ktnx.mobileledger.ui.templates;
 
 import android.os.Bundle;
 import android.view.Menu;
@@ -25,31 +25,32 @@ 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 net.ktnx.mobileledger.R;
-import net.ktnx.mobileledger.databinding.ActivityPatternsBinding;
+import net.ktnx.mobileledger.databinding.ActivityTemplatesBinding;
 import net.ktnx.mobileledger.ui.activity.CrashReportingActivity;
 import net.ktnx.mobileledger.utils.Logger;
 
 import java.util.Objects;
 
-public class PatternsActivity extends CrashReportingActivity
-        implements PatternListFragment.OnPatternListFragmentInteractionListener {
-    public static final String ARG_ADD_PATTERN = "add-pattern";
-    private ActivityPatternsBinding b;
+public class TemplatesActivity extends CrashReportingActivity
+        implements TemplateListFragment.OnTemplateListFragmentInteractionListener {
+    public static final String ARG_ADD_TEMPLATE = "add-template";
+    private ActivityTemplatesBinding b;
     private NavController navController;
     @Override
     public boolean onCreateOptionsMenu(Menu menu) {
         super.onCreateOptionsMenu(menu);
-        getMenuInflater().inflate(R.menu.pattern_list_menu, menu);
+        getMenuInflater().inflate(R.menu.template_list_menu, menu);
 
         return true;
     }
     @Override
     protected void onCreate(Bundle savedInstanceState) {
         super.onCreate(savedInstanceState);
-        b = ActivityPatternsBinding.inflate(getLayoutInflater());
+        b = ActivityTemplatesBinding.inflate(getLayoutInflater());
         setContentView(b.getRoot());
         setSupportActionBar(b.toolbar);
         // Show the Up button in the action bar.
@@ -63,28 +64,28 @@ public class PatternsActivity extends CrashReportingActivity
         navController = navHostFragment.getNavController();
 
         navController.addOnDestinationChangedListener((controller, destination, arguments) -> {
-            if (destination.getId() == R.id.patternListFragment) {
+            if (destination.getId() == R.id.templateListFragment) {
                 b.fabAdd.show();
                 b.fabSave.hide();
-                b.toolbarLayout.setTitle(getString(R.string.title_activity_patterns));
+                b.toolbarLayout.setTitle(getString(R.string.title_activity_templates));
             }
-            if (destination.getId() == R.id.patternDetailsFragment) {
+            if (destination.getId() == R.id.templateDetailsFragment) {
                 b.fabAdd.hide();
                 b.fabSave.show();
             }
         });
 
-        b.toolbarLayout.setTitle(getString(R.string.title_activity_patterns));
+        b.toolbarLayout.setTitle(getString(R.string.title_activity_templates));
 
-        b.fabAdd.setOnClickListener(v -> onEditPattern(null));
-        b.fabSave.setOnClickListener(v -> onSavePattern());
+        b.fabAdd.setOnClickListener(v -> onEditTemplate(null));
+        b.fabSave.setOnClickListener(v -> onSaveTemplate());
     }
     @Override
     public boolean onOptionsItemSelected(MenuItem item) {
         if (item.getItemId() == android.R.id.home) {
             final NavDestination currentDestination = navController.getCurrentDestination();
             if (currentDestination != null &&
-                currentDestination.getId() == R.id.patternDetailsFragment)
+                currentDestination.getId() == R.id.templateDetailsFragment)
                 navController.popBackStack();
             else
                 finish();
@@ -95,27 +96,27 @@ public class PatternsActivity extends CrashReportingActivity
     }
 
     @Override
-    public void onEditPattern(Long id) {
+    public void onEditTemplate(Long id) {
         if (id == null) {
-            navController.navigate(R.id.action_patternListFragment_to_patternDetailsFragment);
-            b.toolbarLayout.setTitle(getString(R.string.title_new_pattern));
+            navController.navigate(R.id.action_templateListFragment_to_templateDetailsFragment);
+            b.toolbarLayout.setTitle(getString(R.string.title_new_template));
         }
         else {
             Bundle bundle = new Bundle();
-            bundle.putLong(PatternDetailsFragment.ARG_PATTERN_ID, id);
-            navController.navigate(R.id.action_patternListFragment_to_patternDetailsFragment,
+            bundle.putLong(TemplateDetailsFragment.ARG_TEMPLATE_ID, id);
+            navController.navigate(R.id.action_templateListFragment_to_templateDetailsFragment,
                     bundle);
-            b.toolbarLayout.setTitle(getString(R.string.title_edit_pattern));
+            b.toolbarLayout.setTitle(getString(R.string.title_edit_template));
         }
     }
     @Override
-    public void onSavePattern() {
+    public void onSaveTemplate() {
         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.getViewModelStoreOwner(R.id.template_list_navigation);
+        TemplateDetailsViewModel model =
+                new ViewModelProvider(viewModelStoreOwner).get(TemplateDetailsViewModel.class);
+        Logger.debug("flow", "TemplatesActivity.onSavePattern(): model=" + model);
+        model.onSaveTemplate();
         navController.navigateUp();
     }
     public NavController getNavController() {
diff --git a/app/src/main/java/net/ktnx/mobileledger/ui/templates/TemplatesRecyclerViewAdapter.java b/app/src/main/java/net/ktnx/mobileledger/ui/templates/TemplatesRecyclerViewAdapter.java
new file mode 100644 (file)
index 0000000..6c1632e
--- /dev/null
@@ -0,0 +1,74 @@
+/*
+ * 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.templates;
+
+import android.view.LayoutInflater;
+import android.view.ViewGroup;
+
+import androidx.annotation.NonNull;
+import androidx.recyclerview.widget.AsyncListDiffer;
+import androidx.recyclerview.widget.DiffUtil;
+import androidx.recyclerview.widget.RecyclerView;
+
+import net.ktnx.mobileledger.databinding.TemplateListTemplateItemBinding;
+import net.ktnx.mobileledger.db.TemplateHeader;
+
+import org.jetbrains.annotations.NotNull;
+
+import java.util.List;
+
+public class TemplatesRecyclerViewAdapter extends RecyclerView.Adapter<TemplateViewHolder> {
+    private final AsyncListDiffer<TemplateHeader> listDiffer;
+    public TemplatesRecyclerViewAdapter() {
+        listDiffer = new AsyncListDiffer<>(this, new DiffUtil.ItemCallback<TemplateHeader>() {
+            @Override
+            public boolean areItemsTheSame(@NotNull TemplateHeader oldItem,
+                                           @NotNull TemplateHeader newItem) {
+                return oldItem.getId()
+                              .equals(newItem.getId());
+            }
+            @Override
+            public boolean areContentsTheSame(@NotNull TemplateHeader oldItem,
+                                              @NotNull TemplateHeader newItem) {
+                return oldItem.equals(newItem);
+            }
+        });
+    }
+    @NonNull
+    @Override
+    public TemplateViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
+        TemplateListTemplateItemBinding b =
+                TemplateListTemplateItemBinding.inflate(LayoutInflater.from(parent.getContext()),
+                        parent, false);
+
+        return new TemplateViewHolder(b);
+    }
+    @Override
+    public void onBindViewHolder(@NonNull TemplateViewHolder holder, int position) {
+        holder.bindToItem(listDiffer.getCurrentList()
+                                    .get(position));
+    }
+    @Override
+    public int getItemCount() {
+        return listDiffer.getCurrentList()
+                         .size();
+    }
+    public void setTemplates(List<TemplateHeader> newList) {
+        listDiffer.submitList(newList);
+    }
+}
diff --git a/app/src/main/res/layout/activity_patterns.xml b/app/src/main/res/layout/activity_patterns.xml
deleted file mode 100644 (file)
index 47ce9f1..0000000
+++ /dev/null
@@ -1,88 +0,0 @@
-<?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.coordinatorlayout.widget.CoordinatorLayout 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"
-    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>
diff --git a/app/src/main/res/layout/activity_templates.xml b/app/src/main/res/layout/activity_templates.xml
new file mode 100644 (file)
index 0000000..97416f9
--- /dev/null
@@ -0,0 +1,88 @@
+<?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.coordinatorlayout.widget.CoordinatorLayout 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"
+    tools:context=".ui.templates.TemplatesActivity"
+    >
+
+    <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/template_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>
index b2eda57d10ff05307fa69bc47dd4440276468254..9b6014d1602dd4c219b1e4491d180e759852b6d9 100644 (file)
@@ -26,6 +26,6 @@
     android:layout_marginLeft="16dp"
     android:layout_marginRight="16dp"
     app:layoutManager="LinearLayoutManager"
-    tools:context=".ui.patterns.TestItemFragment"
-    tools:listitem="@layout/pattern_details_header"
+    tools:context=".ui.templates.TestItemFragment"
+    tools:listitem="@layout/template_details_header"
     />
\ 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
deleted file mode 100644 (file)
index b36f101..0000000
+++ /dev/null
@@ -1,60 +0,0 @@
-<?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>
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
deleted file mode 100644 (file)
index bfdc5d3..0000000
+++ /dev/null
@@ -1,78 +0,0 @@
-<?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
diff --git a/app/src/main/res/layout/fragment_pattern_list.xml b/app/src/main/res/layout/fragment_pattern_list.xml
deleted file mode 100644 (file)
index f230254..0000000
+++ /dev/null
@@ -1,24 +0,0 @@
-<?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_list"
-    android:layout_width="match_parent"
-    android:layout_height="match_parent"
-    tools:context=".ui.patterns.PatternsActivity"
-    />
diff --git a/app/src/main/res/layout/fragment_template_detail_source_selector.xml b/app/src/main/res/layout/fragment_template_detail_source_selector.xml
new file mode 100644 (file)
index 0000000..d3af166
--- /dev/null
@@ -0,0 +1,60 @@
+<?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"
+    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>
diff --git a/app/src/main/res/layout/fragment_template_detail_source_selector_list.xml b/app/src/main/res/layout/fragment_template_detail_source_selector_list.xml
new file mode 100644 (file)
index 0000000..f21a43d
--- /dev/null
@@ -0,0 +1,78 @@
+<?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"
+    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_template_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/template_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_template_detail_source_selector"
+        />
+    <TextView
+        android:id="@+id/template_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/template_details_source_literal"
+        app:layout_constraintBottom_toBottomOf="parent"
+        app:layout_constraintEnd_toEndOf="parent"
+        />
+</androidx.constraintlayout.widget.ConstraintLayout>
\ No newline at end of file
diff --git a/app/src/main/res/layout/fragment_template_list.xml b/app/src/main/res/layout/fragment_template_list.xml
new file mode 100644 (file)
index 0000000..a81e9fb
--- /dev/null
@@ -0,0 +1,24 @@
+<?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/template_list"
+    android:layout_width="match_parent"
+    android:layout_height="match_parent"
+    tools:context=".ui.templates.TemplatesActivity"
+    />
diff --git a/app/src/main/res/layout/pattern_details_account.xml b/app/src/main/res/layout/pattern_details_account.xml
deleted file mode 100644 (file)
index 4994478..0000000
+++ /dev/null
@@ -1,162 +0,0 @@
-<?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_toStartOf="@id/negate_amount_switch"
-        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_toStartOf="@id/negate_amount_switch"
-        app:layout_constraintStart_toStartOf="parent"
-        app:layout_constraintTop_toBottomOf="@id/pattern_account_amount_source_label"
-        />
-    <com.google.android.material.switchmaterial.SwitchMaterial
-        android:id="@+id/negate_amount_switch"
-        android:layout_width="wrap_content"
-        android:layout_height="wrap_content"
-        app:layout_constraintBottom_toBottomOf="@id/pattern_details_account_amount_source"
-        app:layout_constraintEnd_toEndOf="parent"
-        app:layout_constraintTop_toTopOf="@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
diff --git a/app/src/main/res/layout/pattern_details_fragment.xml b/app/src/main/res/layout/pattern_details_fragment.xml
deleted file mode 100644 (file)
index 01ddc4d..0000000
+++ /dev/null
@@ -1,25 +0,0 @@
-<?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
diff --git a/app/src/main/res/layout/pattern_details_header.xml b/app/src/main/res/layout/pattern_details_header.xml
deleted file mode 100644 (file)
index 245f7e0..0000000
+++ /dev/null
@@ -1,299 +0,0 @@
-<?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="@string/pattern_details_source_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_toEndOf="@id/pattern_details_year_source"
-        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_toEndOf="@id/pattern_details_month_source"
-        app:layout_constraintStart_toStartOf="@id/pattern_details_month_source"
-        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_toStartOf="@id/pattern_details_day_source"
-        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
diff --git a/app/src/main/res/layout/pattern_layout.xml b/app/src/main/res/layout/pattern_layout.xml
deleted file mode 100644 (file)
index 090870f..0000000
+++ /dev/null
@@ -1,45 +0,0 @@
-<?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:layout_width="match_parent"
-    android:layout_height="wrap_content"
-    >
-    <TextView
-        android:id="@+id/pattern_name"
-        android:layout_width="0dp"
-        android:layout_height="wrap_content"
-        android:layout_marginStart="@dimen/text_margin"
-        android:layout_marginEnd="@dimen/text_margin"
-        app:layout_constraintBottom_toBottomOf="parent"
-        app:layout_constraintEnd_toStartOf="@id/edit_button"
-        app:layout_constraintStart_toStartOf="parent"
-        app:layout_constraintTop_toTopOf="parent"
-        />
-    <ImageButton
-        android:id="@+id/edit_button"
-        android:layout_width="@dimen/toolbar_height"
-        android:layout_height="@dimen/toolbar_height"
-        android:backgroundTint="?colorSurface"
-        android:contentDescription="@string/edit_button_description"
-        android:src="@drawable/ic_mode_edit_black_24dp"
-        app:layout_constraintBottom_toBottomOf="parent"
-        app:layout_constraintEnd_toEndOf="parent"
-        app:layout_constraintTop_toTopOf="parent"
-        />
-</androidx.constraintlayout.widget.ConstraintLayout>
\ No newline at end of file
diff --git a/app/src/main/res/layout/template_details_account.xml b/app/src/main/res/layout/template_details_account.xml
new file mode 100644 (file)
index 0000000..0eeb78b
--- /dev/null
@@ -0,0 +1,162 @@
+<?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/template_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/template_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/template_account_name_source_label"
+        />
+    <com.google.android.material.textfield.TextInputLayout
+        android:id="@+id/template_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/template_details_account_name_source"
+        >
+        <com.google.android.material.textfield.TextInputEditText
+            android:id="@+id/template_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/template_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/template_details_account_name_layout"
+        />
+    <TextView
+        android:id="@+id/template_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/template_account_comment_source_label"
+        />
+    <com.google.android.material.textfield.TextInputLayout
+        android:id="@+id/template_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/template_details_account_comment_source"
+        >
+        <com.google.android.material.textfield.TextInputEditText
+            android:id="@+id/template_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/template_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_toStartOf="@id/negate_amount_switch"
+        app:layout_constraintStart_toStartOf="parent"
+        app:layout_constraintTop_toBottomOf="@id/template_details_account_comment_layout"
+        />
+    <TextView
+        android:id="@+id/template_details_account_amount_source"
+        android:layout_width="0dp"
+        android:layout_height="wrap_content"
+        android:textAppearance="?attr/textAppearanceListItemSecondary"
+        app:layout_constraintEnd_toStartOf="@id/negate_amount_switch"
+        app:layout_constraintStart_toStartOf="parent"
+        app:layout_constraintTop_toBottomOf="@id/template_account_amount_source_label"
+        />
+    <com.google.android.material.switchmaterial.SwitchMaterial
+        android:id="@+id/negate_amount_switch"
+        android:layout_width="wrap_content"
+        android:layout_height="wrap_content"
+        app:layout_constraintBottom_toBottomOf="@id/template_details_account_amount_source"
+        app:layout_constraintEnd_toEndOf="parent"
+        app:layout_constraintTop_toTopOf="@id/template_account_amount_source_label"
+        />
+    <com.google.android.material.textfield.TextInputLayout
+        android:id="@+id/template_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/template_details_account_amount_source"
+        >
+        <com.google.android.material.textfield.TextInputEditText
+            android:id="@+id/template_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
diff --git a/app/src/main/res/layout/template_details_fragment.xml b/app/src/main/res/layout/template_details_fragment.xml
new file mode 100644 (file)
index 0000000..2b7d575
--- /dev/null
@@ -0,0 +1,25 @@
+<?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.templates.TemplateDetailsFragment"
+    >
+</androidx.recyclerview.widget.RecyclerView>
\ No newline at end of file
diff --git a/app/src/main/res/layout/template_details_header.xml b/app/src/main/res/layout/template_details_header.xml
new file mode 100644 (file)
index 0000000..bc5b851
--- /dev/null
@@ -0,0 +1,299 @@
+<?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/template_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/template_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/template_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/template_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/template_details_month_source_label"
+        app:layout_constraintStart_toStartOf="parent"
+        app:layout_constraintTop_toBottomOf="@id/pattern_transaction_date_label"
+        />
+    <TextView
+        android:id="@+id/template_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/template_details_day_source_label"
+        app:layout_constraintStart_toEndOf="@id/template_details_year_source_label"
+        app:layout_constraintTop_toBottomOf="@id/pattern_transaction_date_label"
+        />
+    <TextView
+        android:id="@+id/template_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/template_details_month_source_label"
+        app:layout_constraintTop_toBottomOf="@id/pattern_transaction_date_label"
+        />
+    <TextView
+        android:id="@+id/template_details_year_source"
+        android:layout_width="0dp"
+        android:layout_height="wrap_content"
+        android:text="@string/template_details_source_literal"
+        android:textAlignment="center"
+        app:layout_constraintEnd_toStartOf="@id/template_details_month_source"
+        app:layout_constraintStart_toStartOf="parent"
+        app:layout_constraintTop_toBottomOf="@id/template_details_day_source_label"
+        />
+    <TextView
+        android:id="@+id/template_details_month_source"
+        android:layout_width="0dp"
+        android:layout_height="wrap_content"
+        android:text=""
+        android:textAlignment="center"
+        app:layout_constraintEnd_toStartOf="@id/template_details_day_source"
+        app:layout_constraintStart_toEndOf="@id/template_details_year_source"
+        app:layout_constraintTop_toBottomOf="@id/template_details_month_source_label"
+        />
+    <TextView
+        android:id="@+id/template_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/template_details_month_source"
+        app:layout_constraintTop_toBottomOf="@id/template_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="template_details_year_source,template_details_month_source,template_details_day_source"
+        />
+    <com.google.android.material.textfield.TextInputLayout
+        android:id="@+id/template_details_date_year_layout"
+        android:layout_width="0dp"
+        android:layout_height="wrap_content"
+        app:layout_constraintEnd_toEndOf="@id/template_details_year_source"
+        app:layout_constraintStart_toStartOf="parent"
+        app:layout_constraintTop_toBottomOf="@id/barrier_before_date_inputs"
+        >
+        <com.google.android.material.textfield.TextInputEditText
+            android:id="@+id/template_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/template_details_date_month_layout"
+        android:layout_width="0dp"
+        android:layout_height="wrap_content"
+        app:layout_constraintEnd_toEndOf="@id/template_details_month_source"
+        app:layout_constraintStart_toStartOf="@id/template_details_month_source"
+        app:layout_constraintTop_toBottomOf="@id/barrier_before_date_inputs"
+        >
+        <com.google.android.material.textfield.TextInputEditText
+            android:id="@+id/template_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/template_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_toStartOf="@id/template_details_day_source"
+        app:layout_constraintTop_toBottomOf="@id/barrier_before_date_inputs"
+        >
+        <com.google.android.material.textfield.TextInputEditText
+            android:id="@+id/template_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="template_details_date_day_layout,template_details_date_month_layout,template_details_date_year_layout"
+        app:layout_constraintEnd_toEndOf="parent"
+        app:layout_constraintStart_toStartOf="parent"
+        />
+    <TextView
+        android:id="@+id/template_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/template_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/template_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/template_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/template_transaction_description_hint"
+            />
+    </com.google.android.material.textfield.TextInputLayout>
+    <TextView
+        android:id="@+id/template_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/template_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/template_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/template_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/template_transaction_comment_hint"
+            />
+    </com.google.android.material.textfield.TextInputLayout>
+
+</androidx.constraintlayout.widget.ConstraintLayout>
\ No newline at end of file
diff --git a/app/src/main/res/layout/template_list_template_item.xml b/app/src/main/res/layout/template_list_template_item.xml
new file mode 100644 (file)
index 0000000..46d6410
--- /dev/null
@@ -0,0 +1,45 @@
+<?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:layout_width="match_parent"
+    android:layout_height="wrap_content"
+    >
+    <TextView
+        android:id="@+id/template_name"
+        android:layout_width="0dp"
+        android:layout_height="wrap_content"
+        android:layout_marginStart="@dimen/text_margin"
+        android:layout_marginEnd="@dimen/text_margin"
+        app:layout_constraintBottom_toBottomOf="parent"
+        app:layout_constraintEnd_toStartOf="@id/edit_button"
+        app:layout_constraintStart_toStartOf="parent"
+        app:layout_constraintTop_toTopOf="parent"
+        />
+    <ImageButton
+        android:id="@+id/edit_button"
+        android:layout_width="@dimen/toolbar_height"
+        android:layout_height="@dimen/toolbar_height"
+        android:backgroundTint="?colorSurface"
+        android:contentDescription="@string/edit_button_description"
+        android:src="@drawable/ic_mode_edit_black_24dp"
+        app:layout_constraintBottom_toBottomOf="parent"
+        app:layout_constraintEnd_toEndOf="parent"
+        app:layout_constraintTop_toTopOf="parent"
+        />
+</androidx.constraintlayout.widget.ConstraintLayout>
\ No newline at end of file
diff --git a/app/src/main/res/menu/pattern_list_menu.xml b/app/src/main/res/menu/pattern_list_menu.xml
deleted file mode 100644 (file)
index a231f5f..0000000
+++ /dev/null
@@ -1,27 +0,0 @@
-<?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/>.
-  -->
-
-<menu xmlns:android="http://schemas.android.com/apk/res/android"
-    xmlns:app="http://schemas.android.com/apk/res-auto"
-    >
-
-    <item
-        android:icon="@drawable/ic_baseline_help_24_white"
-        android:title="@string/help_menu_item_title"
-        app:showAsAction="ifRoom"
-        />
-</menu>
\ No newline at end of file
diff --git a/app/src/main/res/menu/template_list_menu.xml b/app/src/main/res/menu/template_list_menu.xml
new file mode 100644 (file)
index 0000000..a231f5f
--- /dev/null
@@ -0,0 +1,27 @@
+<?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/>.
+  -->
+
+<menu xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:app="http://schemas.android.com/apk/res-auto"
+    >
+
+    <item
+        android:icon="@drawable/ic_baseline_help_24_white"
+        android:title="@string/help_menu_item_title"
+        app:showAsAction="ifRoom"
+        />
+</menu>
\ 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
deleted file mode 100644 (file)
index 927ec20..0000000
+++ /dev/null
@@ -1,50 +0,0 @@
-<?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/>.
-  -->
-
-<navigation 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/pattern_list_navigation"
-    app:startDestination="@id/patternListFragment"
-    >
-
-    <fragment
-        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
diff --git a/app/src/main/res/navigation/template_list_navigation.xml b/app/src/main/res/navigation/template_list_navigation.xml
new file mode 100644 (file)
index 0000000..cbd32a1
--- /dev/null
@@ -0,0 +1,50 @@
+<?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/>.
+  -->
+
+<navigation 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/template_list_navigation"
+    app:startDestination="@id/templateListFragment"
+    >
+
+    <fragment
+        android:id="@+id/templateListFragment"
+        android:name="net.ktnx.mobileledger.ui.templates.TemplateListFragment"
+        android:label="TemplateListFragment"
+        android:tag="templateListFragment"
+        >
+        <action
+            android:id="@+id/action_templateListFragment_to_templateDetailsFragment"
+            app:destination="@id/templateDetailsFragment"
+            app:enterAnim="@anim/slide_in_left"
+            app:exitAnim="@anim/slide_out_left"
+            />
+    </fragment>
+    <fragment
+        android:id="@+id/templateDetailsFragment"
+        android:name="net.ktnx.mobileledger.ui.templates.TemplateDetailsFragment"
+        android:label="pattern_details_fragment"
+        android:tag="patternDetailsFragment"
+        tools:layout="@layout/template_details_fragment"
+        >
+        <action
+            android:id="@+id/action_templateDetailsFragment_to_templateListFragment"
+            app:destination="@id/templateListFragment"
+            />
+    </fragment>
+</navigation>
\ No newline at end of file
index 98c8b41ce93e8352a5c8e81234341c7e94f24886..0b97052c3710e55c78ffa24416219767e2382ccf 100644 (file)
     <string name="err_json_send_error_unsupported">Възможно е програмния интерфейс на сървъра да не се поддържа от MoLe</string>
     <string name="scan_qr">Сканиране на QR код</string>
     <string name="nav_patterns">Макети</string>
-    <string name="title_activity_patterns">Макети</string>
+    <string name="title_activity_templates">Макети</string>
     <string name="pattern_regex_hint">Шаблон (regex)</string>
     <string name="help_menu_item_title">Помощ</string>
     <string name="edit_button_description">Бутон за промяна</string>
     <string name="pattern_details_account_comment_label">Бележка към сметката</string>
     <string name="pattern_details_account_amount_label">Сума</string>
-    <string name="choose_pattern_detail_source_label">Прихващане от шаблона</string>
+    <string name="choose_template_detail_source_label">Прихващане от шаблона</string>
     <string name="missing_pattern_error">Липсва шаблон</string>
     <string name="missing_test_text">Липсва примерен текст</string>
     <string name="pattern_without_groups">Шаблонът няма прихващания</string>
     <string name="pattern_does_not_match">Шаблонът не съвпада с примерния текст</string>
     <string name="pattern_transaction_parameters_label">Данни за движението</string>
-    <string name="pattern_transaction_description_hint">Описание на движението</string>
-    <string name="pattern_transaction_comment_hint">Бележка към движението</string>
+    <string name="template_transaction_description_hint">Описание на движението</string>
+    <string name="template_transaction_comment_hint">Бележка към движението</string>
     <string name="transaction_description_source_label">Източник на описанието на движението</string>
     <string name="transaction_comment_source_label">Източник на бележката към движението</string>
     <string name="pattern_details_date_label">Дата на движението</string>
     <string name="choose_pattern_to_apply">Избор на макет</string>
     <string name="pattern_name">Име на макет</string>
     <string name="implementation_pending">Функцията още не е готова</string>
-    <string name="title_edit_pattern">Промяна на макет</string>
-    <string name="title_new_pattern">Създаване на макет</string>
+    <string name="title_edit_template">Промяна на макет</string>
+    <string name="title_new_template">Създаване на макет</string>
     <string name="pattern_has_errors">Шаблонът съдържа грешки</string>
     <string name="account_name_is_empty">Липсва сметка</string>
     <string name="pattern_is_empty">Липсва шаблон</string>
     <string name="pattern_details_account_name_label">Сметка</string>
     <string name="pattern_details_account_row_label">Данни за сметката</string>
     <string name="account_name_source_label">Източник на името на сметката</string>
-    <string name="pattern_details_source_literal">ръчно въвеждане</string>
+    <string name="template_details_source_literal">ръчно въвеждане</string>
     <string name="account_comment_source_label">Източник на бележка към сметката</string>
     <string name="account_amount_source_label">Източник на името на сметката</string>
 </resources>
index ea647ba0d19e794ba5b96c77e0c2f89ff12aa84f..8b77d38225afb2a4e75294471336bed4479d7e78 100644 (file)
     <string name="err_json_send_error_unsupported">Perhaps the API of the backend server is not supported by MoLe</string>
     <string name="scan_qr">Scan QR code</string>
     <string name="nav_patterns">Patterns</string>
-    <string name="title_activity_patterns">Patterns</string>
+    <string name="title_activity_templates">Patterns</string>
     <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="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="template_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="choose_template_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="template_transaction_description_hint">Transaction description</string>
+    <string name="template_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="choose_pattern_to_apply">Choose pattern to apply</string>
     <string name="pattern_name">Pattern name</string>
     <string name="implementation_pending">Not implemented yet</string>
-    <string name="title_edit_pattern">Edit pattern</string>
-    <string name="title_new_pattern">New pattern</string>
+    <string name="title_edit_template">Edit pattern</string>
+    <string name="title_new_template">New pattern</string>
 </resources>