From 269d287c71015260512659c91d20f10c93f7813d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E8=92=8B=E5=A4=A9=E7=BF=94?= Date: Mon, 26 Jan 2026 23:44:54 +0800 Subject: [PATCH] =?UTF-8?q?=E6=90=9C=E7=B4=A2=E5=8A=9F=E8=83=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../notes/tool/SearchHistoryManager.java | 67 +++++++ .../micode/notes/ui/NoteSearchActivity.java | 188 ++++++++++++++++++ .../micode/notes/ui/NoteSearchAdapter.java | 180 +++++++++++++++++ .../main/res/layout/activity_note_search.xml | 68 +++++++ .../main/res/layout/search_history_item.xml | 35 ++++ 5 files changed, 538 insertions(+) create mode 100644 src/Notesmaster/app/src/main/java/net/micode/notes/tool/SearchHistoryManager.java create mode 100644 src/Notesmaster/app/src/main/java/net/micode/notes/ui/NoteSearchActivity.java create mode 100644 src/Notesmaster/app/src/main/java/net/micode/notes/ui/NoteSearchAdapter.java create mode 100644 src/Notesmaster/app/src/main/res/layout/activity_note_search.xml create mode 100644 src/Notesmaster/app/src/main/res/layout/search_history_item.xml diff --git a/src/Notesmaster/app/src/main/java/net/micode/notes/tool/SearchHistoryManager.java b/src/Notesmaster/app/src/main/java/net/micode/notes/tool/SearchHistoryManager.java new file mode 100644 index 0000000..1f3f9b0 --- /dev/null +++ b/src/Notesmaster/app/src/main/java/net/micode/notes/tool/SearchHistoryManager.java @@ -0,0 +1,67 @@ +package net.micode.notes.tool; + +import android.content.Context; +import android.content.SharedPreferences; +import android.text.TextUtils; +import org.json.JSONArray; +import org.json.JSONException; +import java.util.ArrayList; +import java.util.List; + +public class SearchHistoryManager { + private static final String PREF_NAME = "search_history"; + private static final String KEY_HISTORY = "history_list"; + private static final int MAX_HISTORY_SIZE = 10; + + private final SharedPreferences mPrefs; + + public SearchHistoryManager(Context context) { + mPrefs = context.getSharedPreferences(PREF_NAME, Context.MODE_PRIVATE); + } + + public List getHistory() { + String json = mPrefs.getString(KEY_HISTORY, ""); + List list = new ArrayList<>(); + if (TextUtils.isEmpty(json)) { + return list; + } + try { + JSONArray array = new JSONArray(json); + for (int i = 0; i < array.length(); i++) { + list.add(array.getString(i)); + } + } catch (JSONException e) { + e.printStackTrace(); + } + return list; + } + + public void addHistory(String keyword) { + if (TextUtils.isEmpty(keyword)) return; + List history = getHistory(); + // Remove existing to move to top + history.remove(keyword); + history.add(0, keyword); + // Limit size + if (history.size() > MAX_HISTORY_SIZE) { + history = history.subList(0, MAX_HISTORY_SIZE); + } + saveHistory(history); + } + + public void removeHistory(String keyword) { + List history = getHistory(); + if (history.remove(keyword)) { + saveHistory(history); + } + } + + public void clearHistory() { + mPrefs.edit().remove(KEY_HISTORY).apply(); + } + + private void saveHistory(List history) { + JSONArray array = new JSONArray(history); + mPrefs.edit().putString(KEY_HISTORY, array.toString()).apply(); + } +} diff --git a/src/Notesmaster/app/src/main/java/net/micode/notes/ui/NoteSearchActivity.java b/src/Notesmaster/app/src/main/java/net/micode/notes/ui/NoteSearchActivity.java new file mode 100644 index 0000000..523120b --- /dev/null +++ b/src/Notesmaster/app/src/main/java/net/micode/notes/ui/NoteSearchActivity.java @@ -0,0 +1,188 @@ +package net.micode.notes.ui; + +import android.content.Intent; +import android.os.Bundle; +import android.text.TextUtils; +import android.view.View; +import android.widget.TextView; +import android.widget.Toast; + +import androidx.appcompat.app.AppCompatActivity; +import androidx.appcompat.widget.SearchView; +import androidx.appcompat.widget.Toolbar; +import androidx.recyclerview.widget.LinearLayoutManager; +import androidx.recyclerview.widget.RecyclerView; + +import net.micode.notes.R; +import net.micode.notes.data.Notes; +import net.micode.notes.data.NotesRepository; +import net.micode.notes.tool.SearchHistoryManager; + +import java.util.ArrayList; +import java.util.List; + +public class NoteSearchActivity extends AppCompatActivity implements SearchView.OnQueryTextListener, NoteSearchAdapter.OnItemClickListener { + + private SearchView mSearchView; + private RecyclerView mRecyclerView; + private TextView mTvNoResult; + private NoteSearchAdapter mAdapter; + private NotesRepository mRepository; + private SearchHistoryManager mHistoryManager; + + private TextView mBtnShowHistory; + + @Override + protected void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + setContentView(R.layout.activity_note_search); + + mRepository = new NotesRepository(getContentResolver()); + mHistoryManager = new SearchHistoryManager(this); + + initViews(); + // Initial state: search is empty, show history button if there is history, or just show list + // Requirement: "history option below search bar" + showHistoryOption(); + } + + private void initViews() { + Toolbar toolbar = findViewById(R.id.toolbar); + setSupportActionBar(toolbar); + if (getSupportActionBar() != null) { + getSupportActionBar().setDisplayHomeAsUpEnabled(true); + } + toolbar.setNavigationOnClickListener(v -> finish()); + + mSearchView = findViewById(R.id.search_view); + mSearchView.setOnQueryTextListener(this); + mSearchView.setFocusable(true); + mSearchView.setIconified(false); + mSearchView.requestFocusFromTouch(); + + mBtnShowHistory = findViewById(R.id.btn_show_history); + mBtnShowHistory.setOnClickListener(v -> showHistoryList()); + + mRecyclerView = findViewById(R.id.recycler_view); + mRecyclerView.setLayoutManager(new LinearLayoutManager(this)); + mAdapter = new NoteSearchAdapter(this, this); + mRecyclerView.setAdapter(mAdapter); + + mTvNoResult = findViewById(R.id.tv_no_result); + } + + private void showHistoryOption() { + // Show the "History" button, hide the list + mBtnShowHistory.setVisibility(View.VISIBLE); + mRecyclerView.setVisibility(View.GONE); + mTvNoResult.setVisibility(View.GONE); + } + + private void showHistoryList() { + List history = mHistoryManager.getHistory(); + if (history.isEmpty()) { + // If no history, maybe show a toast or empty state? + // But for now, let's just show the empty list which is fine + } + List data = new ArrayList<>(history); + mAdapter.setData(data, null); + + mBtnShowHistory.setVisibility(View.GONE); // Hide button when showing list + mTvNoResult.setVisibility(View.GONE); + mRecyclerView.setVisibility(View.VISIBLE); + } + + private void performSearch(String query) { + if (TextUtils.isEmpty(query)) { + showHistoryOption(); + return; + } + + // Hide history button when searching + mBtnShowHistory.setVisibility(View.GONE); + + mRepository.searchNotes(query, new NotesRepository.Callback>() { + @Override + public void onSuccess(List result) { + runOnUiThread(() -> { + List data = new ArrayList<>(result); + mAdapter.setData(data, query); + if (data.isEmpty()) { + mTvNoResult.setVisibility(View.VISIBLE); + mRecyclerView.setVisibility(View.GONE); + } else { + mTvNoResult.setVisibility(View.GONE); + mRecyclerView.setVisibility(View.VISIBLE); + } + }); + } + + @Override + public void onError(Exception error) { + runOnUiThread(() -> { + Toast.makeText(NoteSearchActivity.this, "Search failed: " + error.getMessage(), Toast.LENGTH_SHORT).show(); + }); + } + }); + } + + @Override + public boolean onQueryTextSubmit(String query) { + if (!TextUtils.isEmpty(query)) { + mHistoryManager.addHistory(query); + performSearch(query); + mSearchView.clearFocus(); // Hide keyboard + } + return true; + } + + @Override + public boolean onQueryTextChange(String newText) { + if (TextUtils.isEmpty(newText)) { + showHistoryOption(); + } else { + performSearch(newText); + } + return true; + } + + @Override + public void onNoteClick(NotesRepository.NoteInfo note) { + // Save history when user clicks a result + String query = mSearchView.getQuery().toString(); + if (!TextUtils.isEmpty(query)) { + mHistoryManager.addHistory(query); + } + + Intent intent = new Intent(this, NoteEditActivity.class); + intent.setAction(Intent.ACTION_VIEW); + intent.putExtra(Intent.EXTRA_UID, note.getId()); + // Pass search keyword for highlighting in editor + // NoteEditActivity uses SearchManager.EXTRA_DATA_KEY for ID and USER_QUERY for keyword + intent.putExtra(android.app.SearchManager.EXTRA_DATA_KEY, String.valueOf(note.getId())); + intent.putExtra(android.app.SearchManager.USER_QUERY, mSearchView.getQuery().toString()); + startActivity(intent); + } + + @Override + public void onHistoryClick(String keyword) { + mSearchView.setQuery(keyword, true); + } + + @Override + public void onHistoryDelete(String keyword) { + mHistoryManager.removeHistory(keyword); + // Refresh history view if we are currently showing history (search box is empty) + if (TextUtils.isEmpty(mSearchView.getQuery())) { + showHistoryList(); + } + } + + @Override + protected void onDestroy() { + super.onDestroy(); + if (mRepository != null) { + mRepository.shutdown(); + } + } +} diff --git a/src/Notesmaster/app/src/main/java/net/micode/notes/ui/NoteSearchAdapter.java b/src/Notesmaster/app/src/main/java/net/micode/notes/ui/NoteSearchAdapter.java new file mode 100644 index 0000000..15fbb80 --- /dev/null +++ b/src/Notesmaster/app/src/main/java/net/micode/notes/ui/NoteSearchAdapter.java @@ -0,0 +1,180 @@ +package net.micode.notes.ui; + +import android.content.Context; +import android.text.Spannable; +import android.text.SpannableString; +import android.text.TextUtils; +import android.text.style.BackgroundColorSpan; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.widget.CheckBox; +import android.widget.ImageView; +import android.widget.TextView; + +import androidx.annotation.NonNull; +import androidx.recyclerview.widget.RecyclerView; + +import net.micode.notes.R; +import net.micode.notes.data.Notes; +import net.micode.notes.data.NotesRepository; +import net.micode.notes.tool.DataUtils; +import net.micode.notes.tool.ResourceParser; + +import java.util.ArrayList; +import java.util.List; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +public class NoteSearchAdapter extends RecyclerView.Adapter { + + private static final int TYPE_HISTORY = 1; + private static final int TYPE_NOTE = 2; + + private Context mContext; + private List mDataList; + private String mSearchKeyword; + private OnItemClickListener mListener; + + public interface OnItemClickListener { + void onNoteClick(NotesRepository.NoteInfo note); + void onHistoryClick(String keyword); + void onHistoryDelete(String keyword); + } + + public NoteSearchAdapter(Context context, OnItemClickListener listener) { + mContext = context; + mListener = listener; + mDataList = new ArrayList<>(); + } + + public void setData(List data, String keyword) { + mDataList = data; + mSearchKeyword = keyword; + notifyDataSetChanged(); + } + + @Override + public int getItemViewType(int position) { + Object item = mDataList.get(position); + if (item instanceof String) { + return TYPE_HISTORY; + } else if (item instanceof NotesRepository.NoteInfo) { + return TYPE_NOTE; + } + return super.getItemViewType(position); + } + + @NonNull + @Override + public RecyclerView.ViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) { + if (viewType == TYPE_HISTORY) { + View view = LayoutInflater.from(mContext).inflate(R.layout.search_history_item, parent, false); + return new HistoryViewHolder(view); + } else { + View view = LayoutInflater.from(mContext).inflate(R.layout.note_item, parent, false); + return new NoteViewHolder(view); + } + } + + @Override + public void onBindViewHolder(@NonNull RecyclerView.ViewHolder holder, int position) { + if (holder instanceof HistoryViewHolder) { + String keyword = (String) mDataList.get(position); + ((HistoryViewHolder) holder).bind(keyword); + } else if (holder instanceof NoteViewHolder) { + NotesRepository.NoteInfo note = (NotesRepository.NoteInfo) mDataList.get(position); + ((NoteViewHolder) holder).bind(note); + } + } + + @Override + public int getItemCount() { + return mDataList.size(); + } + + class HistoryViewHolder extends RecyclerView.ViewHolder { + TextView tvKeyword; + ImageView ivDelete; + + public HistoryViewHolder(View itemView) { + super(itemView); + tvKeyword = itemView.findViewById(R.id.tv_history_keyword); + ivDelete = itemView.findViewById(R.id.iv_delete_history); + } + + public void bind(final String keyword) { + tvKeyword.setText(keyword); + itemView.setOnClickListener(v -> { + if (mListener != null) mListener.onHistoryClick(keyword); + }); + ivDelete.setOnClickListener(v -> { + if (mListener != null) mListener.onHistoryDelete(keyword); + }); + } + } + + class NoteViewHolder extends RecyclerView.ViewHolder { + ImageView ivTypeIcon; + TextView tvTitle; + TextView tvTime; + TextView tvName; + ImageView ivAlertIcon; + CheckBox checkbox; + + public NoteViewHolder(View itemView) { + super(itemView); + ivTypeIcon = itemView.findViewById(R.id.iv_type_icon); + tvTitle = itemView.findViewById(R.id.tv_title); + tvTime = itemView.findViewById(R.id.tv_time); + tvName = itemView.findViewById(R.id.tv_name); + ivAlertIcon = itemView.findViewById(R.id.iv_alert_icon); + checkbox = itemView.findViewById(android.R.id.checkbox); + } + + public void bind(final NotesRepository.NoteInfo note) { + // 设置标题和高亮 + // NoteInfo.title defaults to snippet if title is empty, so it's safe to use title + if (!TextUtils.isEmpty(mSearchKeyword)) { + tvTitle.setText(getHighlightText(note.title, mSearchKeyword)); + } else { + tvTitle.setText(note.title); + } + + // 设置时间 + tvTime.setText(android.text.format.DateUtils.getRelativeTimeSpanString(note.modifiedDate)); + + // 设置背景(如果 NoteInfo 中有背景ID) + // 注意:NoteInfo 中 bgColorId 是整型ID,需要转换为资源ID + // 这里为了简单,暂不设置复杂的背景,或者使用默认背景 + + // 点击事件 + itemView.setOnClickListener(v -> { + if (mListener != null) mListener.onNoteClick(note); + }); + + // 隐藏不需要的视图 + ivTypeIcon.setVisibility(View.GONE); + tvName.setVisibility(View.GONE); + checkbox.setVisibility(View.GONE); + ivAlertIcon.setVisibility(View.GONE); + } + } + + private Spannable getHighlightText(String text, String keyword) { + if (text == null) text = ""; + SpannableString spannable = new SpannableString(text); + if (!TextUtils.isEmpty(keyword)) { + Pattern pattern = Pattern.compile(Pattern.quote(keyword), Pattern.CASE_INSENSITIVE); + Matcher matcher = pattern.matcher(text); + while (matcher.find()) { + spannable.setSpan( + new BackgroundColorSpan(0x40FFFF00), // 半透明黄色 + matcher.start(), + matcher.end(), + Spannable.SPAN_INCLUSIVE_EXCLUSIVE); + } + } + return spannable; + } +} diff --git a/src/Notesmaster/app/src/main/res/layout/activity_note_search.xml b/src/Notesmaster/app/src/main/res/layout/activity_note_search.xml new file mode 100644 index 0000000..6fc7a8a --- /dev/null +++ b/src/Notesmaster/app/src/main/res/layout/activity_note_search.xml @@ -0,0 +1,68 @@ + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/Notesmaster/app/src/main/res/layout/search_history_item.xml b/src/Notesmaster/app/src/main/res/layout/search_history_item.xml new file mode 100644 index 0000000..b506853 --- /dev/null +++ b/src/Notesmaster/app/src/main/res/layout/search_history_item.xml @@ -0,0 +1,35 @@ + + + + + + + + + +