diff --git a/app/src/main/java/eu/faircode/email/AdapterKeyword.java b/app/src/main/java/eu/faircode/email/AdapterKeyword.java
new file mode 100644
index 0000000000..31a3e44f9f
--- /dev/null
+++ b/app/src/main/java/eu/faircode/email/AdapterKeyword.java
@@ -0,0 +1,308 @@
+package eu.faircode.email;
+
+/*
+ This file is part of FairEmail.
+
+ FairEmail is free software: you can redistribute it and/or modify
+ it under the terms of the GNU General Public License as published by
+ the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ FairEmail 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 for more details.
+
+ You should have received a copy of the GNU General Public License
+ along with FairEmail. If not, see .
+
+ Copyright 2018-2020 by Marcel Bokhorst (M66B)
+*/
+
+import android.content.Context;
+import android.content.DialogInterface;
+import android.content.SharedPreferences;
+import android.os.Bundle;
+import android.text.TextUtils;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.CheckBox;
+import android.widget.CompoundButton;
+
+import androidx.annotation.NonNull;
+import androidx.lifecycle.LifecycleOwner;
+import androidx.preference.PreferenceManager;
+import androidx.recyclerview.widget.DiffUtil;
+import androidx.recyclerview.widget.ListUpdateCallback;
+import androidx.recyclerview.widget.RecyclerView;
+
+import com.flask.colorpicker.ColorPickerView;
+import com.flask.colorpicker.builder.ColorPickerClickListener;
+import com.flask.colorpicker.builder.ColorPickerDialogBuilder;
+
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.List;
+
+public class AdapterKeyword extends RecyclerView.Adapter {
+ private Context context;
+ private LifecycleOwner owner;
+ private LayoutInflater inflater;
+ private boolean pro;
+
+ private long id;
+ private List all = new ArrayList<>();
+
+ public class ViewHolder extends RecyclerView.ViewHolder implements CompoundButton.OnCheckedChangeListener, View.OnClickListener {
+ private View view;
+ private CheckBox cbKeyword;
+ private ViewButtonColor btnColor;
+
+ ViewHolder(View itemView) {
+ super(itemView);
+
+ view = itemView.findViewById(R.id.clItem);
+ cbKeyword = itemView.findViewById(R.id.cbKeyword);
+ btnColor = itemView.findViewById(R.id.btnColor);
+ }
+
+ private void wire() {
+ cbKeyword.setOnCheckedChangeListener(this);
+ btnColor.setOnClickListener(this);
+ }
+
+ private void unwire() {
+ cbKeyword.setOnCheckedChangeListener(null);
+ btnColor.setOnClickListener(null);
+ }
+
+ private void bindTo(TupleKeyword keyword) {
+ cbKeyword.setText(keyword.name);
+ cbKeyword.setChecked(keyword.selected);
+ cbKeyword.setEnabled(pro);
+ btnColor.setColor(keyword.color);
+ btnColor.setEnabled(pro);
+ }
+
+ @Override
+ public void onCheckedChanged(CompoundButton buttonView, boolean isChecked) {
+ int pos = getAdapterPosition();
+ if (pos == RecyclerView.NO_POSITION)
+ return;
+
+ TupleKeyword keyword = all.get(pos);
+ keyword.selected = isChecked;
+
+ Bundle args = new Bundle();
+ args.putLong("id", id);
+ args.putString("keyword", keyword.name);
+ args.putBoolean("selected", keyword.selected);
+
+ new SimpleTask() {
+ @Override
+ protected Void onExecute(Context context, Bundle args) {
+ long id = args.getLong("id");
+ String keyword = args.getString("keyword");
+ boolean selected = args.getBoolean("selected");
+
+ DB db = DB.getInstance(context);
+
+ EntityMessage message = db.message().getMessage(id);
+ if (message == null)
+ return null;
+
+ List keywords = new ArrayList<>(Arrays.asList(message.keywords));
+ if (selected)
+ keywords.add(keyword);
+ else
+ keywords.remove(keyword);
+
+ db.message().setMessageKeywords(message.id, TextUtils.join(" ", keywords));
+
+ return null;
+ }
+
+ @Override
+ protected void onException(Bundle args, Throwable ex) {
+ Log.e(ex);
+ }
+ }.execute(context, owner, args, "keyword:set");
+ }
+
+ @Override
+ public void onClick(View view) {
+ int pos = getAdapterPosition();
+ if (pos == RecyclerView.NO_POSITION)
+ return;
+
+ final TupleKeyword keyword = all.get(pos);
+
+ ColorPickerDialogBuilder builder = ColorPickerDialogBuilder
+ .with(context)
+ .setTitle(context.getString(R.string.title_color))
+ .showColorEdit(true)
+ .wheelType(ColorPickerView.WHEEL_TYPE.FLOWER)
+ .density(6)
+ .lightnessSliderOnly()
+ .setNegativeButton(R.string.title_reset, new DialogInterface.OnClickListener() {
+ @Override
+ public void onClick(DialogInterface dialog, int which) {
+ update(keyword, null);
+ }
+ })
+ .setPositiveButton(android.R.string.ok, new ColorPickerClickListener() {
+ @Override
+ public void onClick(DialogInterface dialog, int selectedColor, Integer[] allColors) {
+ update(keyword, selectedColor);
+ }
+ });
+
+ if (keyword.color != null)
+ builder.initialColor(keyword.color);
+
+ builder.build().show();
+ }
+
+ private void update(TupleKeyword keyword, Integer color) {
+ btnColor.setColor(color);
+ keyword.color = color;
+
+ SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(context);
+ if (color == null)
+ prefs.edit().remove("keyword." + keyword.name).apply();
+ else
+ prefs.edit().putInt("keyword." + keyword.name, keyword.color).apply();
+
+ Bundle args = new Bundle();
+ args.putLong("id", id);
+
+ new SimpleTask() {
+ @Override
+ protected Void onExecute(Context context, Bundle args) {
+ long id = args.getLong("id");
+
+ DB db = DB.getInstance(context);
+
+ EntityMessage message = db.message().getMessage(id);
+ if (message == null)
+ return null;
+
+ // Update keyword colors
+ try {
+ db.beginTransaction();
+
+ db.message().setMessageKeywords(message.id, "");
+ db.message().setMessageKeywords(message.id, TextUtils.join(" ", message.keywords));
+
+ db.setTransactionSuccessful();
+ } finally {
+ db.endTransaction();
+ }
+
+ return null;
+ }
+
+ @Override
+ protected void onException(Bundle args, Throwable ex) {
+ Log.e(ex);
+ }
+ }.execute(context, owner, args, "keyword:set");
+ }
+ }
+
+ AdapterKeyword(Context context, LifecycleOwner owner) {
+ this.context = context;
+ this.owner = owner;
+ this.inflater = LayoutInflater.from(context);
+ this.pro = ActivityBilling.isPro(context);
+
+ setHasStableIds(false);
+ }
+
+ public void set(long id, @NonNull List keywords) {
+ Log.i("Set id=" + id + " keywords=" + keywords.size());
+
+ DiffUtil.DiffResult diff = DiffUtil.calculateDiff(new DiffCallback(all, keywords), false);
+
+ this.id = id;
+ this.all = keywords;
+
+ diff.dispatchUpdatesTo(new ListUpdateCallback() {
+ @Override
+ public void onInserted(int position, int count) {
+ Log.d("Inserted @" + position + " #" + count);
+ }
+
+ @Override
+ public void onRemoved(int position, int count) {
+ Log.d("Removed @" + position + " #" + count);
+ }
+
+ @Override
+ public void onMoved(int fromPosition, int toPosition) {
+ Log.d("Moved " + fromPosition + ">" + toPosition);
+ }
+
+ @Override
+ public void onChanged(int position, int count, Object payload) {
+ Log.d("Changed @" + position + " #" + count);
+ }
+ });
+ diff.dispatchUpdatesTo(this);
+ }
+
+
+ private class DiffCallback extends DiffUtil.Callback {
+ private List prev = new ArrayList<>();
+ private List next = new ArrayList<>();
+
+ DiffCallback(List prev, List next) {
+ this.prev.addAll(prev);
+ this.next.addAll(next);
+ }
+
+ @Override
+ public int getOldListSize() {
+ return prev.size();
+ }
+
+ @Override
+ public int getNewListSize() {
+ return next.size();
+ }
+
+ @Override
+ public boolean areItemsTheSame(int oldItemPosition, int newItemPosition) {
+ TupleKeyword k1 = prev.get(oldItemPosition);
+ TupleKeyword k2 = next.get(newItemPosition);
+ return k1.name.equals(k2.name);
+ }
+
+ @Override
+ public boolean areContentsTheSame(int oldItemPosition, int newItemPosition) {
+ TupleKeyword k1 = prev.get(oldItemPosition);
+ TupleKeyword k2 = next.get(newItemPosition);
+ return k1.equals(k2);
+ }
+ }
+
+ @Override
+ public int getItemCount() {
+ return all.size();
+ }
+
+ @Override
+ @NonNull
+ public ViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
+ return new ViewHolder(inflater.inflate(R.layout.item_keyword, parent, false));
+ }
+
+ @Override
+ public void onBindViewHolder(@NonNull ViewHolder holder, int position) {
+ holder.unwire();
+ TupleKeyword contact = all.get(position);
+ holder.bindTo(contact);
+ holder.wire();
+ }
+}
diff --git a/app/src/main/java/eu/faircode/email/AdapterMessage.java b/app/src/main/java/eu/faircode/email/AdapterMessage.java
index 27863ecaab..df5da34418 100644
--- a/app/src/main/java/eu/faircode/email/AdapterMessage.java
+++ b/app/src/main/java/eu/faircode/email/AdapterMessage.java
@@ -125,6 +125,7 @@ import androidx.recyclerview.widget.StaggeredGridLayoutManager;
import com.github.chrisbanes.photoview.PhotoView;
import com.google.android.material.bottomnavigation.BottomNavigationView;
import com.google.android.material.bottomnavigation.LabelVisibilityMode;
+import com.google.android.material.floatingactionbutton.FloatingActionButton;
import com.google.android.material.snackbar.Snackbar;
import org.jsoup.nodes.Document;
@@ -876,29 +877,31 @@ public class AdapterMessage extends RecyclerView.Adapter 0)
- keywords.append(", ");
- keywords.append(keyword);
-
- String key = "keyword." + keyword;
- if (prefs.contains(key)) {
- int len = keywords.length();
- int color = prefs.getInt(key, textColorSecondary);
- keywords.setSpan(
- new ForegroundColorSpan(color),
- len - keyword.length(), len,
- Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
+ if (keywords_header) {
+ SpannableStringBuilder keywords = new SpannableStringBuilder();
+ for (int i = 0; i < message.keywords.length; i++) {
+ String k = message.keywords[i].toLowerCase();
+ if (IMAP_KEYWORDS_WHITELIST.contains(k) ||
+ !(k.startsWith("$") || IMAP_KEYWORDS_BLACKLIST.contains(k))) {
+ if (keywords.length() > 0)
+ keywords.append(", ");
+
+ keywords.append(message.keywords[i]);
+
+ if (message.keyword_colors[i] != null) {
+ int len = keywords.length();
+ keywords.setSpan(
+ new ForegroundColorSpan(message.keyword_colors[i]),
+ len - message.keywords[i].length(), len,
+ Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
+ }
}
}
- }
- tvKeywords.setVisibility(keywords_header && keywords.length() > 0 ? View.VISIBLE : View.GONE);
- tvKeywords.setText(keywords);
+ tvKeywords.setVisibility(keywords.length() > 0 ? View.VISIBLE : View.GONE);
+ tvKeywords.setText(keywords);
+ } else
+ tvKeywords.setVisibility(View.GONE);
// Line 3
int icon = (message.drafts > 0
@@ -3584,38 +3587,10 @@ public class AdapterMessage extends RecyclerView.Adapter() {
- @Override
- protected EntityFolder onExecute(Context context, Bundle args) {
- long id = args.getLong("id");
-
- DB db = DB.getInstance(context);
- EntityMessage message = db.message().getMessage(id);
- if (message == null)
- return null;
-
- return db.folder().getFolder(message.folder);
- }
-
- @Override
- protected void onExecuted(final Bundle args, EntityFolder folder) {
- if (folder == null)
- return;
- args.putStringArray("fkeywords", folder.keywords);
-
- FragmentKeywordManage fragment = new FragmentKeywordManage();
- fragment.setArguments(args);
- fragment.show(parentFragment.getParentFragmentManager(), "keyword:manage");
- }
-
- @Override
- protected void onException(Bundle args, Throwable ex) {
- Log.unexpectedError(parentFragment.getParentFragmentManager(), ex);
- }
- }.execute(context, owner, args, "message:keywords");
+ FragmentDialogKeywordManage fragment = new FragmentDialogKeywordManage();
+ fragment.setArguments(args);
+ fragment.show(parentFragment.getParentFragmentManager(), "keyword:manage");
}
private void onMenuShare(TupleMessageEx message) {
@@ -4163,6 +4138,11 @@ public class AdapterMessage extends RecyclerView.Adapter list) {
keyPosition.clear();
+ for (int i = 0; i < list.size(); i++) {
+ TupleMessageEx message = list.get(i);
+ if (message != null)
+ message.resolveKeywordColors(context);
+ }
differ.submitList(list);
}
@@ -4516,6 +4496,10 @@ public class AdapterMessage extends RecyclerView.Adapter keywords = Arrays.asList(getArguments().getStringArray("keywords"));
- List fkeywords = Arrays.asList(getArguments().getStringArray("fkeywords"));
- final List items = new ArrayList<>(keywords);
- for (String keyword : fkeywords)
- if (!items.contains(keyword))
- items.add(keyword);
+ final View dview = LayoutInflater.from(getContext()).inflate(R.layout.dialog_keyword_manage, null);
+ final RecyclerView rvKeyword = dview.findViewById(R.id.rvKeyword);
+ final TextView tvPro = dview.findViewById(R.id.tvPro);
+ final FloatingActionButton fabAdd = dview.findViewById(R.id.fabAdd);
+ final ContentLoadingProgressBar pbWait = dview.findViewById(R.id.pbWait);
- Collections.sort(items);
+ rvKeyword.setHasFixedSize(false);
+ final LinearLayoutManager llm = new LinearLayoutManager(getContext());
+ rvKeyword.setLayoutManager(llm);
- final boolean[] selected = new boolean[items.size()];
- final boolean[] dirty = new boolean[items.size()];
- for (int i = 0; i < selected.length; i++) {
- selected[i] = keywords.contains(items.get(i));
- dirty[i] = false;
- }
+ final AdapterKeyword adapter = new AdapterKeyword(getContext(), getViewLifecycleOwner());
+ rvKeyword.setAdapter(adapter);
- return new AlertDialog.Builder(getContext())
- .setTitle(R.string.title_manage_keywords)
- .setMultiChoiceItems(items.toArray(new String[0]), selected, new DialogInterface.OnMultiChoiceClickListener() {
- @Override
- public void onClick(DialogInterface dialog, int which, boolean isChecked) {
- dirty[which] = true;
- }
- })
- .setPositiveButton(android.R.string.ok, new DialogInterface.OnClickListener() {
- @Override
- public void onClick(DialogInterface dialog, int which) {
- if (!ActivityBilling.isPro(getContext())) {
- startActivity(new Intent(getContext(), ActivityBilling.class));
- return;
- }
+ Helper.linkPro(tvPro);
- Bundle args = new Bundle();
- args.putLong("id", id);
- args.putStringArray("keywords", items.toArray(new String[0]));
- args.putBooleanArray("selected", selected);
- args.putBooleanArray("dirty", dirty);
-
- new SimpleTask() {
- @Override
- protected Void onExecute(Context context, Bundle args) {
- long id = args.getLong("id");
- String[] keywords = args.getStringArray("keywords");
- boolean[] selected = args.getBooleanArray("selected");
- boolean[] dirty = args.getBooleanArray("dirty");
-
- DB db = DB.getInstance(context);
- try {
- db.beginTransaction();
-
- EntityMessage message = db.message().getMessage(id);
- if (message == null)
- return null;
-
- for (int i = 0; i < selected.length; i++)
- if (dirty[i])
- EntityOperation.queue(context, message, EntityOperation.KEYWORD, keywords[i], selected[i]);
-
- db.setTransactionSuccessful();
- } finally {
- db.endTransaction();
- }
+ fabAdd.setEnabled(ActivityBilling.isPro(getContext()));
+ fabAdd.setOnClickListener(new View.OnClickListener() {
+ @Override
+ public void onClick(View v) {
+ Bundle args = new Bundle();
+ args.putLong("id", id);
- ServiceSynchronize.eval(context, "keywords");
+ FragmentDialogKeywordAdd fragment = new FragmentDialogKeywordAdd();
+ fragment.setArguments(args);
+ fragment.show(getParentFragmentManager(), "keyword:add");
+ }
+ });
- return null;
- }
+ pbWait.setVisibility(View.VISIBLE);
- @Override
- protected void onException(Bundle args, Throwable ex) {
- Log.unexpectedError(getParentFragmentManager(), ex);
- }
- }.execute(getContext(), getActivity(), args, "message:keywords:manage");
- }
- })
- .setNeutralButton(R.string.title_add, new DialogInterface.OnClickListener() {
- @Override
- public void onClick(DialogInterface dialog, int which) {
- Bundle args = new Bundle();
- args.putLong("id", id);
+ DB db = DB.getInstance(getContext());
+ db.message().liveMessageKeywords(id).observe(getViewLifecycleOwner(), new Observer() {
+ @Override
+ public void onChanged(TupleKeyword.Persisted data) {
+ pbWait.setVisibility(View.GONE);
+ adapter.set(id, TupleKeyword.from(getContext(), data));
+ }
+ });
- FragmentKeywordAdd fragment = new FragmentKeywordAdd();
- fragment.setArguments(args);
- fragment.show(getParentFragmentManager(), "keyword:add");
- }
- })
+ return new AlertDialog.Builder(getContext())
+ .setTitle(R.string.title_manage_keywords)
+ .setView(dview)
.setNegativeButton(android.R.string.cancel, null)
.create();
}
}
- public static class FragmentKeywordAdd extends FragmentDialogBase {
+ public static class FragmentDialogKeywordAdd extends FragmentDialogBase {
@NonNull
@Override
public Dialog onCreateDialog(@Nullable Bundle savedInstanceState) {
final long id = getArguments().getLong("id");
- View view = LayoutInflater.from(getContext()).inflate(R.layout.dialog_keyword, null);
+ View view = LayoutInflater.from(getContext()).inflate(R.layout.dialog_keyword_add, null);
final EditText etKeyword = view.findViewById(R.id.etKeyword);
etKeyword.setText(null);
@@ -5045,11 +4985,6 @@ public class AdapterMessage extends RecyclerView.Adapter liveMessage(long id);
+ @Query("SELECT message.keywords AS selected, folder.keywords AS available" +
+ " FROM message" +
+ " JOIN folder ON folder.id = message.folder" +
+ " WHERE message.id = :id")
+ LiveData liveMessageKeywords(long id);
+
@Transaction
@Query("SELECT account.id AS account, COUNT(message.id) AS unseen, SUM(ABS(notifying)) AS notifying" +
" FROM message" +
diff --git a/app/src/main/java/eu/faircode/email/TupleKeyword.java b/app/src/main/java/eu/faircode/email/TupleKeyword.java
new file mode 100644
index 0000000000..d077547731
--- /dev/null
+++ b/app/src/main/java/eu/faircode/email/TupleKeyword.java
@@ -0,0 +1,86 @@
+package eu.faircode.email;
+
+/*
+ This file is part of FairEmail.
+
+ FairEmail is free software: you can redistribute it and/or modify
+ it under the terms of the GNU General Public License as published by
+ the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ FairEmail 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 for more details.
+
+ You should have received a copy of the GNU General Public License
+ along with FairEmail. If not, see .
+
+ Copyright 2018-2020 by Marcel Bokhorst (M66B)
+*/
+
+import android.content.Context;
+import android.content.SharedPreferences;
+
+import androidx.annotation.Nullable;
+import androidx.preference.PreferenceManager;
+
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.List;
+import java.util.Objects;
+
+public class TupleKeyword {
+ public String name;
+ public boolean selected;
+ public Integer color;
+
+ @Override
+ public boolean equals(@Nullable Object obj) {
+ if (obj instanceof TupleKeyword) {
+ TupleKeyword other = (TupleKeyword) obj;
+ return (this.name.equals(other.name) &&
+ this.selected == other.selected &&
+ Objects.equals(this.color, other.color));
+ } else
+ return false;
+ }
+
+ public static class Persisted {
+ public String[] selected;
+ public String[] available;
+ }
+
+ static List from(Context context, Persisted data) {
+ List result = new ArrayList<>();
+
+ List keywords = new ArrayList<>();
+
+ for (String keyword : data.selected)
+ if (!keywords.contains(keyword))
+ keywords.add(keyword);
+
+ for (String keyword : data.available)
+ if (!keywords.contains(keyword))
+ keywords.add(keyword);
+
+ Collections.sort(keywords);
+
+ SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(context);
+
+ for (String keyword : keywords) {
+ TupleKeyword k = new TupleKeyword();
+ k.name = keyword;
+ k.selected = Arrays.asList(data.selected).contains(keyword);
+
+ String c = "keyword." + keyword;
+ if (prefs.contains(c))
+ k.color = prefs.getInt(c, -1);
+
+ result.add(k);
+ }
+
+ return result;
+ }
+}
diff --git a/app/src/main/java/eu/faircode/email/TupleMessageEx.java b/app/src/main/java/eu/faircode/email/TupleMessageEx.java
index 3d057e0b78..30a8a82035 100644
--- a/app/src/main/java/eu/faircode/email/TupleMessageEx.java
+++ b/app/src/main/java/eu/faircode/email/TupleMessageEx.java
@@ -20,9 +20,14 @@ package eu.faircode.email;
*/
import android.content.Context;
+import android.content.SharedPreferences;
+import android.graphics.Color;
+import androidx.preference.PreferenceManager;
import androidx.room.Ignore;
+import java.util.ArrayList;
+import java.util.List;
import java.util.Objects;
import javax.mail.Address;
@@ -53,12 +58,31 @@ public class TupleMessageEx extends EntityMessage {
@Ignore
boolean duplicate;
+ @Ignore
+ public Integer[] keyword_colors;
+
String getFolderName(Context context) {
return (folderDisplay == null
? Helper.localizeFolderName(context, folderName)
: folderDisplay);
}
+ void resolveKeywordColors(Context context) {
+ List color = new ArrayList<>();
+
+ SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(context);
+ for (int i = 0; i < this.keywords.length; i++) {
+ String key = "keyword." + this.keywords[i];
+ if (prefs.contains(key))
+ color.add(prefs.getInt(key, Color.GRAY));
+ else
+ color.add(null);
+ }
+
+ this.keyword_colors = color.toArray(new Integer[0]);
+ }
+
+
@Override
public boolean equals(Object obj) {
if (obj instanceof TupleMessageEx) {
diff --git a/app/src/main/res/layout/dialog_keyword.xml b/app/src/main/res/layout/dialog_keyword_add.xml
similarity index 92%
rename from app/src/main/res/layout/dialog_keyword.xml
rename to app/src/main/res/layout/dialog_keyword_add.xml
index 6bef3efe52..b8619808fa 100644
--- a/app/src/main/res/layout/dialog_keyword.xml
+++ b/app/src/main/res/layout/dialog_keyword_add.xml
@@ -21,7 +21,7 @@
android:layout_height="wrap_content"
android:layout_marginTop="24dp"
android:imeOptions="actionDone"
- android:inputType="text"
+ android:inputType="textCapSentences"
android:text="Keyword"
android:textAppearance="@style/TextAppearance.AppCompat.Medium"
app:layout_constraintEnd_toEndOf="parent"
diff --git a/app/src/main/res/layout/dialog_keyword_manage.xml b/app/src/main/res/layout/dialog_keyword_manage.xml
new file mode 100644
index 0000000000..be75572cbf
--- /dev/null
+++ b/app/src/main/res/layout/dialog_keyword_manage.xml
@@ -0,0 +1,59 @@
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/layout/item_keyword.xml b/app/src/main/res/layout/item_keyword.xml
new file mode 100644
index 0000000000..21c61b0c02
--- /dev/null
+++ b/app/src/main/res/layout/item_keyword.xml
@@ -0,0 +1,38 @@
+
+
+
+
+
+
+
+
+
+