mirror of https://github.com/M66B/FairEmail.git
Added saved searches
This commit is contained in:
parent
be1865387d
commit
c15943144b
|
@ -6,7 +6,7 @@
|
|||
|
||||
### Next version
|
||||
|
||||
* Storing folder namespaces
|
||||
* Added saved searches
|
||||
|
||||
### 1.1732
|
||||
|
||||
|
|
|
@ -4,7 +4,7 @@ apply plugin: 'kotlin-android'
|
|||
apply plugin: 'de.undercouch.download'
|
||||
|
||||
def getVersionCode = { -> return 1732 }
|
||||
def getRevision = { -> "\"a\"" }
|
||||
def getRevision = { -> "\"b\"" }
|
||||
def getReleaseName = { -> return "\"Zanabazar\"" }
|
||||
// https://en.wikipedia.org/wiki/List_of_dinosaur_genera
|
||||
|
||||
|
|
File diff suppressed because it is too large
Load Diff
|
@ -6,7 +6,7 @@
|
|||
|
||||
### Next version
|
||||
|
||||
* Storing folder namespaces
|
||||
* Added saved searches
|
||||
|
||||
### 1.1732
|
||||
|
||||
|
|
|
@ -114,19 +114,30 @@ public class ActivityView extends ActivityBilling implements FragmentManager.OnB
|
|||
private ImageButton ibSettings;
|
||||
private ImageButton ibFetchMore;
|
||||
private ImageButton ibForceSync;
|
||||
|
||||
private View vSeparatorOptions;
|
||||
private ImageButton ibExpanderAccount;
|
||||
|
||||
private RecyclerView rvAccount;
|
||||
private ImageButton ibExpanderUnified;
|
||||
|
||||
private ImageButton ibExpanderSearch;
|
||||
private RecyclerView rvSearch;
|
||||
private View vSeparatorSearch;
|
||||
|
||||
private RecyclerView rvUnified;
|
||||
private ImageButton ibExpanderMenu;
|
||||
|
||||
private RecyclerView rvMenu;
|
||||
private ImageButton ibExpanderExtra;
|
||||
|
||||
private RecyclerView rvMenuExtra;
|
||||
|
||||
private Group grpOptions;
|
||||
|
||||
private AdapterNavAccountFolder adapterNavAccount;
|
||||
private AdapterNavUnified adapterNavUnified;
|
||||
private AdapterNavSearch adapterNavSearch;
|
||||
private AdapterNavMenu adapterNavMenu;
|
||||
private AdapterNavMenu adapterNavMenuExtra;
|
||||
|
||||
|
@ -291,12 +302,20 @@ public class ActivityView extends ActivityBilling implements FragmentManager.OnB
|
|||
ibForceSync = drawerContainer.findViewById(R.id.ibForceSync);
|
||||
vSeparatorOptions = drawerContainer.findViewById(R.id.vSeparatorOptions);
|
||||
grpOptions = drawerContainer.findViewById(R.id.grpOptions);
|
||||
|
||||
ibExpanderAccount = drawerContainer.findViewById(R.id.ibExpanderAccount);
|
||||
rvAccount = drawerContainer.findViewById(R.id.rvAccount);
|
||||
|
||||
ibExpanderUnified = drawerContainer.findViewById(R.id.ibExpanderUnified);
|
||||
rvUnified = drawerContainer.findViewById(R.id.rvUnified);
|
||||
|
||||
ibExpanderSearch = drawerContainer.findViewById(R.id.ibExpanderSearch);
|
||||
rvSearch = drawerContainer.findViewById(R.id.rvSearch);
|
||||
vSeparatorSearch = drawerContainer.findViewById(R.id.vSeparatorSearch);
|
||||
|
||||
ibExpanderMenu = drawerContainer.findViewById(R.id.ibExpanderMenu);
|
||||
rvMenu = drawerContainer.findViewById(R.id.rvMenu);
|
||||
|
||||
ibExpanderExtra = drawerContainer.findViewById(R.id.ibExpanderExtra);
|
||||
rvMenuExtra = drawerContainer.findViewById(R.id.rvMenuExtra);
|
||||
|
||||
|
@ -473,6 +492,27 @@ public class ActivityView extends ActivityBilling implements FragmentManager.OnB
|
|||
}
|
||||
});
|
||||
|
||||
// Menus
|
||||
rvSearch.setLayoutManager(new LinearLayoutManager(this));
|
||||
adapterNavSearch = new AdapterNavSearch(this, this, getSupportFragmentManager());
|
||||
rvSearch.setAdapter(adapterNavSearch);
|
||||
|
||||
boolean nav_search = prefs.getBoolean("nav_search", true);
|
||||
ibExpanderSearch.setImageLevel(nav_search ? 0 /* less */ : 1 /* more */);
|
||||
ibExpanderSearch.setVisibility(View.GONE);
|
||||
rvSearch.setVisibility(View.GONE);
|
||||
vSeparatorSearch.setVisibility(View.GONE);
|
||||
|
||||
ibExpanderSearch.setOnClickListener(new View.OnClickListener() {
|
||||
@Override
|
||||
public void onClick(View v) {
|
||||
boolean nav_search = !prefs.getBoolean("nav_search", true);
|
||||
prefs.edit().putBoolean("nav_search", nav_search).apply();
|
||||
ibExpanderSearch.setImageLevel(nav_search ? 0 /* less */ : 1 /* more */);
|
||||
rvSearch.setVisibility(nav_search ? View.VISIBLE : View.GONE);
|
||||
}
|
||||
});
|
||||
|
||||
// Menus
|
||||
rvMenu.setLayoutManager(new LinearLayoutManager(this));
|
||||
adapterNavMenu = new AdapterNavMenu(this, this);
|
||||
|
@ -784,6 +824,20 @@ public class ActivityView extends ActivityBilling implements FragmentManager.OnB
|
|||
}
|
||||
});
|
||||
|
||||
db.search().liveSearch().observe(owner, new Observer<List<EntitySearch>>() {
|
||||
@Override
|
||||
public void onChanged(List<EntitySearch> search) {
|
||||
if (search == null)
|
||||
search = new ArrayList<>();
|
||||
adapterNavSearch.set(search, nav_expanded);
|
||||
|
||||
boolean nav_search = prefs.getBoolean("nav_search", true);
|
||||
ibExpanderSearch.setVisibility(search.size() > 0 ? View.VISIBLE : View.GONE);
|
||||
rvSearch.setVisibility(search.size() > 0 && nav_search ? View.VISIBLE : View.GONE);
|
||||
vSeparatorSearch.setVisibility(search.size() > 0 ? View.VISIBLE : View.GONE);
|
||||
}
|
||||
});
|
||||
|
||||
db.operation().liveStats().observe(owner, new Observer<TupleOperationStats>() {
|
||||
@Override
|
||||
public void onChanged(TupleOperationStats stats) {
|
||||
|
|
|
@ -0,0 +1,267 @@
|
|||
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 <http://www.gnu.org/licenses/>.
|
||||
|
||||
Copyright 2018-2021 by Marcel Bokhorst (M66B)
|
||||
*/
|
||||
|
||||
import android.content.Context;
|
||||
import android.os.Bundle;
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
import android.widget.ImageView;
|
||||
import android.widget.TextView;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.fragment.app.FragmentManager;
|
||||
import androidx.lifecycle.Lifecycle;
|
||||
import androidx.lifecycle.LifecycleObserver;
|
||||
import androidx.lifecycle.LifecycleOwner;
|
||||
import androidx.lifecycle.OnLifecycleEvent;
|
||||
import androidx.recyclerview.widget.DiffUtil;
|
||||
import androidx.recyclerview.widget.ListUpdateCallback;
|
||||
import androidx.recyclerview.widget.RecyclerView;
|
||||
|
||||
import org.json.JSONObject;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
public class AdapterNavSearch extends RecyclerView.Adapter<AdapterNavSearch.ViewHolder> {
|
||||
private Context context;
|
||||
private LifecycleOwner owner;
|
||||
private FragmentManager manager;
|
||||
private LayoutInflater inflater;
|
||||
|
||||
private boolean expanded = true;
|
||||
private List<EntitySearch> items = new ArrayList<>();
|
||||
|
||||
public class ViewHolder extends RecyclerView.ViewHolder implements View.OnClickListener, View.OnLongClickListener {
|
||||
private View view;
|
||||
private ImageView ivItem;
|
||||
private ImageView ivBadge;
|
||||
private TextView tvItem;
|
||||
private TextView tvItemExtra;
|
||||
private ImageView ivExtra;
|
||||
private ImageView ivWarning;
|
||||
|
||||
ViewHolder(View itemView) {
|
||||
super(itemView);
|
||||
|
||||
view = itemView.findViewById(R.id.clItem);
|
||||
ivItem = itemView.findViewById(R.id.ivItem);
|
||||
ivBadge = itemView.findViewById(R.id.ivBadge);
|
||||
tvItem = itemView.findViewById(R.id.tvItem);
|
||||
tvItemExtra = itemView.findViewById(R.id.tvItemExtra);
|
||||
ivExtra = itemView.findViewById(R.id.ivExtra);
|
||||
ivWarning = itemView.findViewById(R.id.ivWarning);
|
||||
}
|
||||
|
||||
private void wire() {
|
||||
view.setOnClickListener(this);
|
||||
view.setOnLongClickListener(this);
|
||||
}
|
||||
|
||||
private void unwire() {
|
||||
view.setOnClickListener(null);
|
||||
view.setOnLongClickListener(null);
|
||||
}
|
||||
|
||||
private void bindTo(EntitySearch search) {
|
||||
ivItem.setImageResource(R.drawable.twotone_search_24);
|
||||
ivBadge.setVisibility(View.GONE);
|
||||
tvItem.setText(search.name);
|
||||
|
||||
|
||||
tvItemExtra.setVisibility(View.GONE);
|
||||
ivExtra.setVisibility(View.GONE);
|
||||
ivWarning.setVisibility(View.GONE);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onClick(View v) {
|
||||
int pos = getAdapterPosition();
|
||||
if (pos == RecyclerView.NO_POSITION)
|
||||
return;
|
||||
|
||||
EntitySearch search = items.get(pos);
|
||||
if (search == null)
|
||||
return;
|
||||
|
||||
try {
|
||||
JSONObject json = new JSONObject(search.data);
|
||||
BoundaryCallbackMessages.SearchCriteria criteria =
|
||||
BoundaryCallbackMessages.SearchCriteria.fromJSON(json);
|
||||
FragmentMessages.search(
|
||||
context, owner, manager,
|
||||
-1L, -1L, false, criteria);
|
||||
} catch (Throwable ex) {
|
||||
Log.e(ex);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onLongClick(View v) {
|
||||
int pos = getAdapterPosition();
|
||||
if (pos == RecyclerView.NO_POSITION)
|
||||
return false;
|
||||
|
||||
EntitySearch search = items.get(pos);
|
||||
if (search == null)
|
||||
return false;
|
||||
|
||||
Bundle args = new Bundle();
|
||||
args.putLong("id", search.id);
|
||||
|
||||
new SimpleTask<Void>() {
|
||||
@Override
|
||||
protected Void onExecute(Context context, Bundle args) throws Throwable {
|
||||
long id = args.getLong("id");
|
||||
|
||||
DB db = DB.getInstance(context);
|
||||
db.search().deleteSearch(id);
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onException(Bundle args, Throwable ex) {
|
||||
Log.unexpectedError(manager, ex);
|
||||
}
|
||||
}.execute(context, owner, args, "search:delete");
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
AdapterNavSearch(Context context, LifecycleOwner owner, FragmentManager manager) {
|
||||
this.context = context;
|
||||
this.owner = owner;
|
||||
this.manager = manager;
|
||||
this.inflater = LayoutInflater.from(context);
|
||||
|
||||
setHasStableIds(true);
|
||||
|
||||
owner.getLifecycle().addObserver(new LifecycleObserver() {
|
||||
@OnLifecycleEvent(Lifecycle.Event.ON_DESTROY)
|
||||
public void onDestroyed() {
|
||||
Log.d(AdapterNavSearch.this + " parent destroyed");
|
||||
AdapterNavSearch.this.manager = null;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
public void set(@NonNull List<EntitySearch> search, boolean expanded) {
|
||||
Log.i("Set nav search=" + search.size() + " expanded=" + expanded);
|
||||
|
||||
DiffUtil.DiffResult diff = DiffUtil.calculateDiff(new DiffCallback(items, search), false);
|
||||
|
||||
this.expanded = expanded;
|
||||
this.items = search;
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
public void setExpanded(boolean expanded) {
|
||||
this.expanded = expanded;
|
||||
notifyDataSetChanged();
|
||||
}
|
||||
|
||||
EntitySearch get(int pos) {
|
||||
return items.get(pos);
|
||||
}
|
||||
|
||||
private static class DiffCallback extends DiffUtil.Callback {
|
||||
private List<EntitySearch> prev = new ArrayList<>();
|
||||
private List<EntitySearch> next = new ArrayList<>();
|
||||
|
||||
DiffCallback(List<EntitySearch> prev, List<EntitySearch> 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) {
|
||||
EntitySearch s1 = prev.get(oldItemPosition);
|
||||
EntitySearch s2 = next.get(newItemPosition);
|
||||
return s1.id.equals(s2.id);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean areContentsTheSame(int oldItemPosition, int newItemPosition) {
|
||||
EntitySearch s1 = prev.get(oldItemPosition);
|
||||
EntitySearch s2 = next.get(newItemPosition);
|
||||
return s1.equals(s2);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public long getItemId(int position) {
|
||||
return items.get(position).id;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getItemCount() {
|
||||
return items.size();
|
||||
}
|
||||
|
||||
@Override
|
||||
@NonNull
|
||||
public ViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
|
||||
return new ViewHolder(inflater.inflate(R.layout.item_nav, parent, false));
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onBindViewHolder(@NonNull ViewHolder holder, int position) {
|
||||
holder.unwire();
|
||||
EntitySearch search = items.get(position);
|
||||
holder.bindTo(search);
|
||||
holder.wire();
|
||||
}
|
||||
}
|
|
@ -39,6 +39,10 @@ import com.sun.mail.imap.protocol.IMAPResponse;
|
|||
import com.sun.mail.imap.protocol.SearchSequence;
|
||||
import com.sun.mail.util.MessageRemovedIOException;
|
||||
|
||||
import org.json.JSONArray;
|
||||
import org.json.JSONException;
|
||||
import org.json.JSONObject;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
import java.io.Serializable;
|
||||
|
@ -953,6 +957,86 @@ public class BoundaryCallbackMessages extends PagedList.BoundaryCallback<TupleMe
|
|||
return false;
|
||||
}
|
||||
|
||||
JSONObject toJson() throws JSONException {
|
||||
JSONObject json = new JSONObject();
|
||||
json.put("query", query);
|
||||
json.put("in_senders", in_senders);
|
||||
json.put("in_recipients", in_recipients);
|
||||
json.put("in_subject", in_subject);
|
||||
json.put("in_keywords", in_keywords);
|
||||
json.put("in_message", in_message);
|
||||
json.put("in_notes", in_notes);
|
||||
json.put("in_headers", in_headers);
|
||||
json.put("in_html", in_html);
|
||||
json.put("with_unseen", with_unseen);
|
||||
json.put("with_flagged", with_flagged);
|
||||
json.put("with_hidden", with_hidden);
|
||||
json.put("with_encrypted", with_encrypted);
|
||||
json.put("with_attachments", with_attachments);
|
||||
json.put("with_notes", with_notes);
|
||||
|
||||
if (with_types != null) {
|
||||
JSONArray jtypes = new JSONArray();
|
||||
for (String type : with_types)
|
||||
jtypes.put(type);
|
||||
json.put("with_types", jtypes);
|
||||
}
|
||||
|
||||
if (with_size != null)
|
||||
json.put("with_size", with_size);
|
||||
|
||||
json.put("in_trash", in_trash);
|
||||
json.put("in_junk", in_junk);
|
||||
|
||||
if (after != null)
|
||||
json.put("after", after);
|
||||
|
||||
if (before != null)
|
||||
json.put("before", before);
|
||||
|
||||
return json;
|
||||
}
|
||||
|
||||
public static SearchCriteria fromJSON(JSONObject json) throws JSONException {
|
||||
SearchCriteria criteria = new SearchCriteria();
|
||||
criteria.query = json.optString("query");
|
||||
criteria.in_senders = json.optBoolean("in_senders");
|
||||
criteria.in_recipients = json.optBoolean("in_recipients");
|
||||
criteria.in_subject = json.optBoolean("in_subject");
|
||||
criteria.in_keywords = json.optBoolean("in_keywords");
|
||||
criteria.in_message = json.optBoolean("in_message");
|
||||
criteria.in_notes = json.optBoolean("in_notes");
|
||||
criteria.in_headers = json.optBoolean("in_headers");
|
||||
criteria.in_html = json.optBoolean("in_html");
|
||||
criteria.with_unseen = json.optBoolean("with_unseen");
|
||||
criteria.with_flagged = json.optBoolean("with_flagged");
|
||||
criteria.with_hidden = json.optBoolean("with_hidden");
|
||||
criteria.with_encrypted = json.optBoolean("with_encrypted");
|
||||
criteria.with_attachments = json.optBoolean("with_attachments");
|
||||
criteria.with_notes = json.optBoolean("with_notes");
|
||||
|
||||
if (json.has("with_types")) {
|
||||
JSONArray jtypes = json.getJSONArray("with_types");
|
||||
criteria.with_types = new String[jtypes.length()];
|
||||
for (int i = 0; i < jtypes.length(); i++)
|
||||
criteria.with_types[i] = jtypes.getString(i);
|
||||
}
|
||||
|
||||
if (json.has("with_size"))
|
||||
criteria.with_size = json.getInt("with_size");
|
||||
|
||||
criteria.in_trash = json.optBoolean("in_trash");
|
||||
criteria.in_junk = json.optBoolean("in_junk");
|
||||
|
||||
if (json.has("after"))
|
||||
criteria.after = json.getLong("after");
|
||||
|
||||
if (json.has("before"))
|
||||
criteria.before = json.getLong("before");
|
||||
|
||||
return criteria;
|
||||
}
|
||||
|
||||
@NonNull
|
||||
@Override
|
||||
public String toString() {
|
||||
|
|
|
@ -68,7 +68,7 @@ import io.requery.android.database.sqlite.SQLiteDatabase;
|
|||
// https://developer.android.com/topic/libraries/architecture/room.html
|
||||
|
||||
@Database(
|
||||
version = 210,
|
||||
version = 211,
|
||||
entities = {
|
||||
EntityIdentity.class,
|
||||
EntityAccount.class,
|
||||
|
@ -80,6 +80,7 @@ import io.requery.android.database.sqlite.SQLiteDatabase;
|
|||
EntityCertificate.class,
|
||||
EntityAnswer.class,
|
||||
EntityRule.class,
|
||||
EntitySearch.class,
|
||||
EntityLog.class
|
||||
},
|
||||
views = {
|
||||
|
@ -111,6 +112,8 @@ public abstract class DB extends RoomDatabase {
|
|||
|
||||
public abstract DaoRule rule();
|
||||
|
||||
public abstract DaoSearch search();
|
||||
|
||||
public abstract DaoLog log();
|
||||
|
||||
private static int sPid;
|
||||
|
@ -124,7 +127,7 @@ public abstract class DB extends RoomDatabase {
|
|||
private static final int DB_CHECKPOINT = 1000; // requery/sqlite-android default
|
||||
|
||||
private static final String[] DB_TABLES = new String[]{
|
||||
"identity", "account", "folder", "message", "attachment", "operation", "contact", "certificate", "answer", "rule", "log"};
|
||||
"identity", "account", "folder", "message", "attachment", "operation", "contact", "certificate", "answer", "rule", "search", "log"};
|
||||
|
||||
@Override
|
||||
public void init(@NonNull DatabaseConfiguration configuration) {
|
||||
|
@ -2143,14 +2146,23 @@ public abstract class DB extends RoomDatabase {
|
|||
db.execSQL("ALTER TABLE `log` ADD COLUMN `message` INTEGER");
|
||||
}
|
||||
}).addMigrations(new Migration(209, 210) {
|
||||
@Override
|
||||
public void migrate(@NonNull SupportSQLiteDatabase db) {
|
||||
Log.i("DB migration from version " + startVersion + " to " + endVersion);
|
||||
db.execSQL("ALTER TABLE `folder` ADD COLUMN `namespace` TEXT");
|
||||
db.execSQL("ALTER TABLE `folder` ADD COLUMN `separator` INTEGER");
|
||||
db.execSQL("UPDATE folder SET separator =" +
|
||||
" (SELECT separator FROM account WHERE account.id = folder.account)");
|
||||
}
|
||||
@Override
|
||||
public void migrate(@NonNull SupportSQLiteDatabase db) {
|
||||
Log.i("DB migration from version " + startVersion + " to " + endVersion);
|
||||
db.execSQL("ALTER TABLE `folder` ADD COLUMN `namespace` TEXT");
|
||||
db.execSQL("ALTER TABLE `folder` ADD COLUMN `separator` INTEGER");
|
||||
db.execSQL("UPDATE folder SET separator =" +
|
||||
" (SELECT separator FROM account WHERE account.id = folder.account)");
|
||||
}
|
||||
}).addMigrations(new Migration(210, 211) {
|
||||
@Override
|
||||
public void migrate(@NonNull SupportSQLiteDatabase db) {
|
||||
Log.i("DB migration from version " + startVersion + " to " + endVersion);
|
||||
db.execSQL("CREATE TABLE `search`" +
|
||||
" (`id` INTEGER PRIMARY KEY AUTOINCREMENT" +
|
||||
", name TEXT NOT NULL" +
|
||||
", `data` TEXT NOT NULL)");
|
||||
}
|
||||
}).addMigrations(new Migration(998, 999) {
|
||||
@Override
|
||||
public void migrate(@NonNull SupportSQLiteDatabase db) {
|
||||
|
|
|
@ -0,0 +1,41 @@
|
|||
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 <http://www.gnu.org/licenses/>.
|
||||
|
||||
Copyright 2018-2021 by Marcel Bokhorst (M66B)
|
||||
*/
|
||||
|
||||
import androidx.lifecycle.LiveData;
|
||||
import androidx.room.Dao;
|
||||
import androidx.room.Insert;
|
||||
import androidx.room.Query;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
@Dao
|
||||
public interface DaoSearch {
|
||||
@Query("SELECT * FROM search" +
|
||||
" ORDER BY name COLLATE NOCASE")
|
||||
LiveData<List<EntitySearch>> liveSearch();
|
||||
|
||||
@Insert
|
||||
long insertSearch(EntitySearch search);
|
||||
|
||||
@Query("DELETE FROM search" +
|
||||
" WHERE id = :id")
|
||||
int deleteSearch(long id);
|
||||
}
|
|
@ -0,0 +1,53 @@
|
|||
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 <http://www.gnu.org/licenses/>.
|
||||
|
||||
Copyright 2018-2021 by Marcel Bokhorst (M66B)
|
||||
*/
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.room.Entity;
|
||||
import androidx.room.PrimaryKey;
|
||||
|
||||
@Entity(
|
||||
tableName = EntitySearch.TABLE_NAME,
|
||||
foreignKeys = {
|
||||
},
|
||||
indices = {
|
||||
}
|
||||
)
|
||||
public class EntitySearch {
|
||||
static final String TABLE_NAME = "search";
|
||||
|
||||
@PrimaryKey(autoGenerate = true)
|
||||
public Long id;
|
||||
@NonNull
|
||||
public String name;
|
||||
@NonNull
|
||||
public String data;
|
||||
|
||||
@Override
|
||||
public boolean equals(Object obj) {
|
||||
if (obj instanceof EntitySearch) {
|
||||
EntitySearch other = (EntitySearch) obj;
|
||||
return (this.id.equals(other.id) &&
|
||||
this.name.equals(other.name) &&
|
||||
this.data.equals(other.data));
|
||||
} else
|
||||
return false;
|
||||
}
|
||||
}
|
|
@ -58,6 +58,44 @@ import java.util.List;
|
|||
import io.requery.android.database.sqlite.SQLiteDatabase;
|
||||
|
||||
public class FragmentDialogSearch extends FragmentDialogBase {
|
||||
private TextViewAutoCompleteAction etQuery;
|
||||
private TextView tvSearch1;
|
||||
private TextView tvSearch2;
|
||||
private TextView tvSearch3;
|
||||
private ImageButton ibResetSearches;
|
||||
|
||||
private ImageButton ibInfo;
|
||||
private ImageButton ibFlagged;
|
||||
private ImageButton ibUnseen;
|
||||
private ImageButton ibInvite;
|
||||
private ImageButton ibAttachment;
|
||||
private ImageButton ibNotes;
|
||||
private ImageButton ibMore;
|
||||
private TextView tvMore;
|
||||
private CheckBox cbSearchIndex;
|
||||
private CheckBox cbSenders;
|
||||
private CheckBox cbRecipients;
|
||||
private CheckBox cbSubject;
|
||||
private CheckBox cbKeywords;
|
||||
private CheckBox cbMessage;
|
||||
private TextView tvSearchTextUnsupported;
|
||||
private CheckBox cbNotes;
|
||||
private CheckBox cbHeaders;
|
||||
private CheckBox cbHtml;
|
||||
private CheckBox cbSearchTrash;
|
||||
private CheckBox cbSearchJunk;
|
||||
private CheckBox cbUnseen;
|
||||
private CheckBox cbFlagged;
|
||||
private CheckBox cbHidden;
|
||||
private CheckBox cbEncrypted;
|
||||
private CheckBox cbAttachments;
|
||||
private Spinner spMessageSize;
|
||||
private Button btnBefore;
|
||||
private Button btnAfter;
|
||||
private TextView tvBefore;
|
||||
private TextView tvAfter;
|
||||
private Group grpMore;
|
||||
|
||||
private static final int MAX_SUGGESTIONS = 3;
|
||||
|
||||
@NonNull
|
||||
|
@ -82,43 +120,43 @@ public class FragmentDialogSearch extends FragmentDialogBase {
|
|||
|
||||
View dview = LayoutInflater.from(context).inflate(R.layout.dialog_search, null);
|
||||
|
||||
final TextViewAutoCompleteAction etQuery = dview.findViewById(R.id.etQuery);
|
||||
final TextView tvSearch1 = dview.findViewById(R.id.tvSearch1);
|
||||
final TextView tvSearch2 = dview.findViewById(R.id.tvSearch2);
|
||||
final TextView tvSearch3 = dview.findViewById(R.id.tvSearch3);
|
||||
final ImageButton ibResetSearches = dview.findViewById(R.id.ibResetSearches);
|
||||
etQuery = dview.findViewById(R.id.etQuery);
|
||||
tvSearch1 = dview.findViewById(R.id.tvSearch1);
|
||||
tvSearch2 = dview.findViewById(R.id.tvSearch2);
|
||||
tvSearch3 = dview.findViewById(R.id.tvSearch3);
|
||||
ibResetSearches = dview.findViewById(R.id.ibResetSearches);
|
||||
|
||||
final ImageButton ibInfo = dview.findViewById(R.id.ibInfo);
|
||||
final ImageButton ibFlagged = dview.findViewById(R.id.ibFlagged);
|
||||
final ImageButton ibUnseen = dview.findViewById(R.id.ibUnseen);
|
||||
final ImageButton ibInvite = dview.findViewById(R.id.ibInvite);
|
||||
final ImageButton ibAttachment = dview.findViewById(R.id.ibAttachment);
|
||||
final ImageButton ibNotes = dview.findViewById(R.id.ibNotes);
|
||||
final ImageButton ibMore = dview.findViewById(R.id.ibMore);
|
||||
final TextView tvMore = dview.findViewById(R.id.tvMore);
|
||||
final CheckBox cbSearchIndex = dview.findViewById(R.id.cbSearchIndex);
|
||||
final CheckBox cbSenders = dview.findViewById(R.id.cbSenders);
|
||||
final CheckBox cbRecipients = dview.findViewById(R.id.cbRecipients);
|
||||
final CheckBox cbSubject = dview.findViewById(R.id.cbSubject);
|
||||
final CheckBox cbKeywords = dview.findViewById(R.id.cbKeywords);
|
||||
final CheckBox cbMessage = dview.findViewById(R.id.cbMessage);
|
||||
final TextView tvSearchTextUnsupported = dview.findViewById(R.id.tvSearchTextUnsupported);
|
||||
final CheckBox cbNotes = dview.findViewById(R.id.cbNotes);
|
||||
final CheckBox cbHeaders = dview.findViewById(R.id.cbHeaders);
|
||||
final CheckBox cbHtml = dview.findViewById(R.id.cbHtml);
|
||||
final CheckBox cbSearchTrash = dview.findViewById(R.id.cbSearchTrash);
|
||||
final CheckBox cbSearchJunk = dview.findViewById(R.id.cbSearchJunk);
|
||||
final CheckBox cbUnseen = dview.findViewById(R.id.cbUnseen);
|
||||
final CheckBox cbFlagged = dview.findViewById(R.id.cbFlagged);
|
||||
final CheckBox cbHidden = dview.findViewById(R.id.cbHidden);
|
||||
final CheckBox cbEncrypted = dview.findViewById(R.id.cbEncrypted);
|
||||
final CheckBox cbAttachments = dview.findViewById(R.id.cbAttachments);
|
||||
final Spinner spMessageSize = dview.findViewById(R.id.spMessageSize);
|
||||
final Button btnBefore = dview.findViewById(R.id.btnBefore);
|
||||
final Button btnAfter = dview.findViewById(R.id.btnAfter);
|
||||
final TextView tvBefore = dview.findViewById(R.id.tvBefore);
|
||||
final TextView tvAfter = dview.findViewById(R.id.tvAfter);
|
||||
final Group grpMore = dview.findViewById(R.id.grpMore);
|
||||
ibInfo = dview.findViewById(R.id.ibInfo);
|
||||
ibFlagged = dview.findViewById(R.id.ibFlagged);
|
||||
ibUnseen = dview.findViewById(R.id.ibUnseen);
|
||||
ibInvite = dview.findViewById(R.id.ibInvite);
|
||||
ibAttachment = dview.findViewById(R.id.ibAttachment);
|
||||
ibNotes = dview.findViewById(R.id.ibNotes);
|
||||
ibMore = dview.findViewById(R.id.ibMore);
|
||||
tvMore = dview.findViewById(R.id.tvMore);
|
||||
cbSearchIndex = dview.findViewById(R.id.cbSearchIndex);
|
||||
cbSenders = dview.findViewById(R.id.cbSenders);
|
||||
cbRecipients = dview.findViewById(R.id.cbRecipients);
|
||||
cbSubject = dview.findViewById(R.id.cbSubject);
|
||||
cbKeywords = dview.findViewById(R.id.cbKeywords);
|
||||
cbMessage = dview.findViewById(R.id.cbMessage);
|
||||
tvSearchTextUnsupported = dview.findViewById(R.id.tvSearchTextUnsupported);
|
||||
cbNotes = dview.findViewById(R.id.cbNotes);
|
||||
cbHeaders = dview.findViewById(R.id.cbHeaders);
|
||||
cbHtml = dview.findViewById(R.id.cbHtml);
|
||||
cbSearchTrash = dview.findViewById(R.id.cbSearchTrash);
|
||||
cbSearchJunk = dview.findViewById(R.id.cbSearchJunk);
|
||||
cbUnseen = dview.findViewById(R.id.cbUnseen);
|
||||
cbFlagged = dview.findViewById(R.id.cbFlagged);
|
||||
cbHidden = dview.findViewById(R.id.cbHidden);
|
||||
cbEncrypted = dview.findViewById(R.id.cbEncrypted);
|
||||
cbAttachments = dview.findViewById(R.id.cbAttachments);
|
||||
spMessageSize = dview.findViewById(R.id.spMessageSize);
|
||||
btnBefore = dview.findViewById(R.id.btnBefore);
|
||||
btnAfter = dview.findViewById(R.id.btnAfter);
|
||||
tvBefore = dview.findViewById(R.id.tvBefore);
|
||||
tvAfter = dview.findViewById(R.id.tvAfter);
|
||||
grpMore = dview.findViewById(R.id.grpMore);
|
||||
|
||||
ibInfo.setOnClickListener(new View.OnClickListener() {
|
||||
@Override
|
||||
|
@ -374,11 +412,9 @@ public class FragmentDialogSearch extends FragmentDialogBase {
|
|||
.setPositiveButton(android.R.string.yes, new DialogInterface.OnClickListener() {
|
||||
@Override
|
||||
public void onClick(DialogInterface dialog, int which) {
|
||||
BoundaryCallbackMessages.SearchCriteria criteria = new BoundaryCallbackMessages.SearchCriteria();
|
||||
BoundaryCallbackMessages.SearchCriteria criteria = getSearchCriteria();
|
||||
|
||||
criteria.query = etQuery.getText().toString().trim();
|
||||
|
||||
if (!TextUtils.isEmpty(criteria.query)) {
|
||||
if (criteria.query != null) {
|
||||
List<String> searches = new ArrayList<>();
|
||||
for (int i = 1; i <= 3; i++)
|
||||
if (prefs.contains("last_search" + i)) {
|
||||
|
@ -395,43 +431,6 @@ public class FragmentDialogSearch extends FragmentDialogBase {
|
|||
editor.apply();
|
||||
}
|
||||
|
||||
if (TextUtils.isEmpty(criteria.query))
|
||||
criteria.query = null;
|
||||
|
||||
criteria.fts = cbSearchIndex.isChecked();
|
||||
if (!criteria.fts) {
|
||||
criteria.in_senders = cbSenders.isChecked();
|
||||
criteria.in_recipients = cbRecipients.isChecked();
|
||||
criteria.in_subject = cbSubject.isChecked();
|
||||
criteria.in_keywords = cbKeywords.isChecked();
|
||||
criteria.in_message = cbMessage.isChecked();
|
||||
criteria.in_notes = cbNotes.isChecked();
|
||||
criteria.in_headers = cbHeaders.isChecked();
|
||||
criteria.in_html = cbHtml.isChecked();
|
||||
criteria.with_unseen = cbUnseen.isChecked();
|
||||
criteria.with_flagged = cbFlagged.isChecked();
|
||||
criteria.with_hidden = cbHidden.isChecked();
|
||||
criteria.with_encrypted = cbEncrypted.isChecked();
|
||||
criteria.with_attachments = cbAttachments.isChecked();
|
||||
|
||||
int pos = spMessageSize.getSelectedItemPosition();
|
||||
if (pos > 0) {
|
||||
int[] sizes = getResources().getIntArray(R.array.sizeValues);
|
||||
criteria.with_size = sizes[pos];
|
||||
}
|
||||
}
|
||||
|
||||
criteria.in_trash = cbSearchTrash.isChecked();
|
||||
criteria.in_junk = cbSearchJunk.isChecked();
|
||||
|
||||
Object after = tvAfter.getTag();
|
||||
Object before = tvBefore.getTag();
|
||||
|
||||
if (after != null)
|
||||
criteria.after = ((Calendar) after).getTimeInMillis();
|
||||
if (before != null)
|
||||
criteria.before = ((Calendar) before).getTimeInMillis();
|
||||
|
||||
Helper.hideKeyboard(etQuery);
|
||||
|
||||
if (criteria.query != null && criteria.query.startsWith("raw:"))
|
||||
|
@ -482,6 +481,38 @@ public class FragmentDialogSearch extends FragmentDialogBase {
|
|||
account, folder, false, criteria);
|
||||
}
|
||||
})
|
||||
.setNeutralButton(R.string.title_save, new DialogInterface.OnClickListener() {
|
||||
@Override
|
||||
public void onClick(DialogInterface dialog, int which) {
|
||||
try {
|
||||
BoundaryCallbackMessages.SearchCriteria criteria = getSearchCriteria();
|
||||
Bundle args = new Bundle();
|
||||
args.putString("name", criteria.getTitle(context));
|
||||
args.putString("data", criteria.toJson().toString());
|
||||
|
||||
new SimpleTask<Void>() {
|
||||
@Override
|
||||
protected Void onExecute(Context context, Bundle args) {
|
||||
EntitySearch search = new EntitySearch();
|
||||
search.name = args.getString("name");
|
||||
search.data = args.getString("data");
|
||||
|
||||
DB db = DB.getInstance(context);
|
||||
db.search().insertSearch(search);
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onException(Bundle args, Throwable ex) {
|
||||
Log.unexpectedError(getParentFragmentManager(), ex);
|
||||
}
|
||||
}.execute(FragmentDialogSearch.this, args, "search:save");
|
||||
} catch (Throwable ex) {
|
||||
Log.e(ex);
|
||||
}
|
||||
}
|
||||
})
|
||||
.setNegativeButton(android.R.string.no, new DialogInterface.OnClickListener() {
|
||||
@Override
|
||||
public void onClick(DialogInterface dialog, int which) {
|
||||
|
@ -547,6 +578,50 @@ public class FragmentDialogSearch extends FragmentDialogBase {
|
|||
getActivity().getWindow().setSoftInputMode(WindowManager.LayoutParams.SOFT_INPUT_STATE_ALWAYS_HIDDEN);
|
||||
}
|
||||
|
||||
private BoundaryCallbackMessages.SearchCriteria getSearchCriteria() {
|
||||
BoundaryCallbackMessages.SearchCriteria criteria = new BoundaryCallbackMessages.SearchCriteria();
|
||||
|
||||
criteria.query = etQuery.getText().toString().trim();
|
||||
if (TextUtils.isEmpty(criteria.query))
|
||||
criteria.query = null;
|
||||
|
||||
criteria.fts = cbSearchIndex.isChecked();
|
||||
if (!criteria.fts) {
|
||||
criteria.in_senders = cbSenders.isChecked();
|
||||
criteria.in_recipients = cbRecipients.isChecked();
|
||||
criteria.in_subject = cbSubject.isChecked();
|
||||
criteria.in_keywords = cbKeywords.isChecked();
|
||||
criteria.in_message = cbMessage.isChecked();
|
||||
criteria.in_notes = cbNotes.isChecked();
|
||||
criteria.in_headers = cbHeaders.isChecked();
|
||||
criteria.in_html = cbHtml.isChecked();
|
||||
criteria.with_unseen = cbUnseen.isChecked();
|
||||
criteria.with_flagged = cbFlagged.isChecked();
|
||||
criteria.with_hidden = cbHidden.isChecked();
|
||||
criteria.with_encrypted = cbEncrypted.isChecked();
|
||||
criteria.with_attachments = cbAttachments.isChecked();
|
||||
|
||||
int pos = spMessageSize.getSelectedItemPosition();
|
||||
if (pos > 0) {
|
||||
int[] sizes = getResources().getIntArray(R.array.sizeValues);
|
||||
criteria.with_size = sizes[pos];
|
||||
}
|
||||
}
|
||||
|
||||
criteria.in_trash = cbSearchTrash.isChecked();
|
||||
criteria.in_junk = cbSearchJunk.isChecked();
|
||||
|
||||
Object after = tvAfter.getTag();
|
||||
Object before = tvBefore.getTag();
|
||||
|
||||
if (after != null)
|
||||
criteria.after = ((Calendar) after).getTimeInMillis();
|
||||
if (before != null)
|
||||
criteria.before = ((Calendar) before).getTimeInMillis();
|
||||
|
||||
return criteria;
|
||||
}
|
||||
|
||||
private void pickDate(TextView tv) {
|
||||
Object tag = tv.getTag();
|
||||
final Calendar cal = (tag == null ? Calendar.getInstance() : (Calendar) tag);
|
||||
|
|
|
@ -159,7 +159,7 @@
|
|||
app:layout_constraintTop_toBottomOf="@id/rvUnified" />
|
||||
|
||||
<ImageButton
|
||||
android:id="@+id/ibExpanderMenu"
|
||||
android:id="@+id/ibExpanderSearch"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:background="?android:attr/selectableItemBackground"
|
||||
|
@ -169,6 +169,36 @@
|
|||
app:layout_constraintTop_toBottomOf="@id/vSeparatorUnified"
|
||||
app:srcCompat="@drawable/expander" />
|
||||
|
||||
<androidx.recyclerview.widget.RecyclerView
|
||||
android:id="@+id/rvSearch"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:animateLayoutChanges="false"
|
||||
android:nestedScrollingEnabled="false"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toBottomOf="@id/ibExpanderSearch" />
|
||||
|
||||
<View
|
||||
android:id="@+id/vSeparatorSearch"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="1dp"
|
||||
android:background="?attr/colorSeparator"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toBottomOf="@id/rvSearch" />
|
||||
|
||||
<ImageButton
|
||||
android:id="@+id/ibExpanderMenu"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:background="?android:attr/selectableItemBackground"
|
||||
android:contentDescription="@string/title_legend_expander"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toBottomOf="@id/vSeparatorSearch"
|
||||
app:srcCompat="@drawable/expander" />
|
||||
|
||||
<androidx.recyclerview.widget.RecyclerView
|
||||
android:id="@+id/rvMenu"
|
||||
android:layout_width="0dp"
|
||||
|
|
|
@ -6,7 +6,7 @@
|
|||
|
||||
### Next version
|
||||
|
||||
* Storing folder namespaces
|
||||
* Added saved searches
|
||||
|
||||
### 1.1732
|
||||
|
||||
|
|
Loading…
Reference in New Issue