新增功能 #27

Merged
p7tupf26b merged 4 commits from jiangtianxiang_branch into master 4 weeks ago

@ -13,3 +13,8 @@
.externalNativeBuild
.cxx
local.properties
build.gradle.kts
gradle.properties
gradlew
gradlew.bat
settings.gradle.kts

@ -25,6 +25,7 @@
<!-- ==================== 应用配置 ==================== -->
<application
android:name=".NotesApplication"
android:allowBackup="true"
android:dataExtractionRules="@xml/data_extraction_rules"
android:fullBackupContent="@xml/backup_rules"
@ -91,6 +92,15 @@
android:resource="@xml/searchable" />
</activity>
<!-- ==================== 搜索活动 ==================== -->
<activity
android:name=".ui.NoteSearchActivity"
android:label="@string/search"
android:launchMode="singleTop"
android:theme="@style/Theme.Notesmaster"
android:windowSoftInputMode="stateVisible|adjustResize"
android:exported="false" />
<!-- ==================== 内容提供者 ==================== -->
<!-- 提供笔记数据的访问接口,允许其他应用访问笔记数据 -->
<provider
@ -162,7 +172,7 @@
android:name=".ui.AlarmAlertActivity"
android:label="@string/app_name"
android:launchMode="singleInstance"
android:theme="@android:style/Theme.Holo.Wallpaper.NoTitleBar" >
android:theme="@style/Theme.Notesmaster" >
</activity>
<!-- ==================== 设置活动 ==================== -->
@ -171,14 +181,14 @@
android:name="net.micode.notes.ui.NotesPreferenceActivity"
android:label="@string/preferences_title"
android:launchMode="singleTop"
android:theme="@android:style/Theme.Holo.Light" >
android:theme="@style/Theme.Notesmaster" >
</activity>
<!-- ==================== 密码设置/验证活动 ==================== -->
<activity
android:name=".ui.PasswordActivity"
android:label="Password"
android:theme="@android:style/Theme.Holo.Light.NoActionBar"
android:theme="@style/Theme.Notesmaster"
android:windowSoftInputMode="stateVisible|adjustResize">
</activity>

@ -0,0 +1,18 @@
package net.micode.notes;
import android.app.Application;
import net.micode.notes.data.ThemeRepository;
import com.google.android.material.color.DynamicColors;
public class NotesApplication extends Application {
@Override
public void onCreate() {
super.onCreate();
// Apply Dynamic Colors (Material You) if available
DynamicColors.applyToActivitiesIfAvailable(this);
// Apply saved theme preference
ThemeRepository repository = new ThemeRepository(this);
ThemeRepository.applyTheme(repository.getThemeMode());
}
}

@ -0,0 +1,56 @@
package net.micode.notes.data;
import android.content.Context;
import android.content.SharedPreferences;
import android.graphics.Typeface;
import android.widget.TextView;
import androidx.preference.PreferenceManager;
public class FontManager {
public static final String PREF_FONT_FAMILY = "pref_font_family";
private final SharedPreferences mPrefs;
private static FontManager sInstance;
private FontManager(Context context) {
mPrefs = PreferenceManager.getDefaultSharedPreferences(context);
}
public static synchronized FontManager getInstance(Context context) {
if (sInstance == null) {
sInstance = new FontManager(context.getApplicationContext());
}
return sInstance;
}
public void applyFont(TextView textView) {
String fontValue = mPrefs.getString(PREF_FONT_FAMILY, "default");
Typeface typeface = getTypeface(fontValue);
if (typeface != null) {
textView.setTypeface(typeface);
} else {
textView.setTypeface(Typeface.DEFAULT);
}
}
private Typeface getTypeface(String fontValue) {
switch (fontValue) {
case "serif":
return Typeface.SERIF;
case "sans-serif":
return Typeface.SANS_SERIF;
case "monospace":
return Typeface.MONOSPACE;
case "cursive":
// Android doesn't have a built-in cursive typeface constant,
// but we can try to load sans-serif-light or similar as a placeholder,
// or load from assets if we had custom fonts.
// For now, let's map it to serif-italic style if possible or just serif.
return Typeface.create(Typeface.SERIF, Typeface.ITALIC);
case "default":
default:
return Typeface.DEFAULT;
}
}
}

@ -739,13 +739,15 @@ public class NotesRepository {
return;
}
String selection = "(" + NoteColumns.TYPE + " = ?) AND (" +
String selection = "(" + NoteColumns.TYPE + " <> ?) AND (" +
NoteColumns.TITLE + " LIKE ? OR " +
NoteColumns.SNIPPET + " LIKE ? OR " +
NoteColumns.ID + " IN (SELECT " + DataColumns.NOTE_ID +
" FROM data WHERE " + DataColumns.CONTENT + " LIKE ?))";
String[] selectionArgs = new String[]{
String.valueOf(Notes.TYPE_NOTE),
String.valueOf(Notes.TYPE_SYSTEM),
"%" + keyword + "%",
"%" + keyword + "%",
"%" + keyword + "%"
};

@ -0,0 +1,43 @@
package net.micode.notes.data;
import android.content.Context;
import android.content.SharedPreferences;
import androidx.appcompat.app.AppCompatDelegate;
import androidx.preference.PreferenceManager;
public class ThemeRepository {
private static final String PREF_THEME_MODE = "pref_theme_mode";
public static final String THEME_MODE_SYSTEM = "system";
public static final String THEME_MODE_LIGHT = "light";
public static final String THEME_MODE_DARK = "dark";
private final SharedPreferences mPrefs;
public ThemeRepository(Context context) {
mPrefs = PreferenceManager.getDefaultSharedPreferences(context);
}
public String getThemeMode() {
return mPrefs.getString(PREF_THEME_MODE, THEME_MODE_SYSTEM);
}
public void setThemeMode(String mode) {
mPrefs.edit().putString(PREF_THEME_MODE, mode).apply();
applyTheme(mode);
}
public static void applyTheme(String mode) {
switch (mode) {
case THEME_MODE_LIGHT:
AppCompatDelegate.setDefaultNightMode(AppCompatDelegate.MODE_NIGHT_NO);
break;
case THEME_MODE_DARK:
AppCompatDelegate.setDefaultNightMode(AppCompatDelegate.MODE_NIGHT_YES);
break;
case THEME_MODE_SYSTEM:
default:
AppCompatDelegate.setDefaultNightMode(AppCompatDelegate.MODE_NIGHT_FOLLOW_SYSTEM);
break;
}
}
}

@ -0,0 +1,6 @@
package net.micode.notes.model;
public interface Command {
void execute();
void undo();
}

@ -0,0 +1,40 @@
package net.micode.notes.model;
import android.text.Editable;
import android.widget.EditText;
public class NoteCommand implements Command {
private final EditText mEditor;
private final int mStart;
private final CharSequence mBefore;
private final CharSequence mAfter;
public NoteCommand(EditText editor, int start, CharSequence before, CharSequence after) {
mEditor = editor;
mStart = start;
mBefore = before.toString();
mAfter = after.toString();
}
@Override
public void execute() {
// Redo: replace 'before' with 'after'
Editable text = mEditor.getText();
int end = mStart + mBefore.length();
if (end <= text.length()) {
text.replace(mStart, end, mAfter);
mEditor.setSelection(mStart + mAfter.length());
}
}
@Override
public void undo() {
// Undo: replace 'after' with 'before'
Editable text = mEditor.getText();
int end = mStart + mAfter.length();
if (end <= text.length()) {
text.replace(mStart, end, mBefore);
mEditor.setSelection(mStart + mBefore.length());
}
}
}

@ -0,0 +1,49 @@
package net.micode.notes.model;
import java.util.Stack;
public class UndoRedoManager {
private static final int MAX_STACK_SIZE = 20;
private final Stack<Command> mUndoStack = new Stack<>();
private final Stack<Command> mRedoStack = new Stack<>();
public void addCommand(Command command) {
mUndoStack.push(command);
if (mUndoStack.size() > MAX_STACK_SIZE) {
mUndoStack.remove(0);
}
mRedoStack.clear();
}
public void undo() {
if (!mUndoStack.isEmpty()) {
Command command = mUndoStack.pop();
command.undo();
mRedoStack.push(command);
}
}
public void redo() {
if (!mRedoStack.isEmpty()) {
Command command = mRedoStack.pop();
command.execute();
mUndoStack.push(command);
if (mUndoStack.size() > MAX_STACK_SIZE) {
mUndoStack.remove(0);
}
}
}
public boolean canUndo() {
return !mUndoStack.isEmpty();
}
public boolean canRedo() {
return !mRedoStack.isEmpty();
}
public void clear() {
mUndoStack.clear();
mRedoStack.clear();
}
}

@ -427,6 +427,27 @@ public class WorkingNote {
}
}
// Wallpaper Support
private String mWallpaperPath;
public void setWallpaper(String path) {
mWallpaperPath = path;
// Ideally we should save this to DB, but for now we might use shared prefs or a separate table
// Or reuse bg_color_id with a special flag if we want to stick to existing schema strictly?
// Better: store in a new column or reuse a data column if possible.
// Given existing schema, let's use DataColumns.DATA5 if available? No DATA5.
// Let's use a SharedPreference for mapping noteId -> wallpaperPath for now to avoid schema migration complexity in this step.
// Or just use a special negative color ID range for wallpapers?
// Actually, let's use a separate storage for wallpapers map: note_id -> uri string
if (mNoteSettingStatusListener != null) {
mNoteSettingStatusListener.onBackgroundColorChanged(); // Reuse this to trigger refresh
}
}
public String getWallpaperPath() {
return mWallpaperPath;
}
/**
*
* <p>

@ -45,10 +45,40 @@ public class ResourceParser {
/** 红色背景 */
public static final int RED = 4;
// New Presets
public static final int MIDNIGHT_BLACK = 5;
public static final int EYE_CARE_GREEN = 6;
public static final int WARM = 7;
public static final int COOL = 8;
/** 自定义颜色按钮 ID (用于 UI 显示) */
public static final int CUSTOM_COLOR_BUTTON_ID = -100;
/** 壁纸按钮 ID (用于 UI 显示) */
public static final int WALLPAPER_BUTTON_ID = -101;
/** 默认背景颜色 */
public static final int BG_DEFAULT_COLOR = YELLOW;
public static int getNoteBgColor(Context context, int id) {
if (id < 0) {
return id; // Custom color (ARGB)
}
switch (id) {
case YELLOW: return context.getColor(R.color.bg_yellow);
case BLUE: return context.getColor(R.color.bg_blue);
case WHITE: return context.getColor(R.color.bg_white);
case GREEN: return context.getColor(R.color.bg_green);
case RED: return context.getColor(R.color.bg_red);
case MIDNIGHT_BLACK: return context.getColor(R.color.bg_midnight_black);
case EYE_CARE_GREEN: return context.getColor(R.color.bg_eye_care_green);
case WARM: return context.getColor(R.color.bg_warm);
case COOL: return context.getColor(R.color.bg_cool);
default: return context.getColor(R.color.bg_white);
}
}
/** 小号字体 */
public static final int TEXT_SMALL = 0;
@ -97,6 +127,9 @@ public class ResourceParser {
* @return ID
*/
public static int getNoteBgResource(int id) {
if (id >= BG_EDIT_RESOURCES.length || id < 0) {
return R.drawable.edit_white;
}
return BG_EDIT_RESOURCES[id];
}
@ -107,6 +140,9 @@ public class ResourceParser {
* @return ID
*/
public static int getNoteTitleBgResource(int id) {
if (id >= BG_EDIT_TITLE_RESOURCES.length || id < 0) {
return R.drawable.edit_title_white;
}
return BG_EDIT_TITLE_RESOURCES[id];
}
}
@ -182,6 +218,7 @@ public class ResourceParser {
* @return ID
*/
public static int getNoteBgFirstRes(int id) {
if (id >= BG_FIRST_RESOURCES.length || id < 0) return R.drawable.list_white_up;
return BG_FIRST_RESOURCES[id];
}
@ -192,6 +229,7 @@ public class ResourceParser {
* @return ID
*/
public static int getNoteBgLastRes(int id) {
if (id >= BG_LAST_RESOURCES.length || id < 0) return R.drawable.list_white_down;
return BG_LAST_RESOURCES[id];
}
@ -202,6 +240,7 @@ public class ResourceParser {
* @return ID
*/
public static int getNoteBgSingleRes(int id) {
if (id >= BG_SINGLE_RESOURCES.length || id < 0) return R.drawable.list_white_single;
return BG_SINGLE_RESOURCES[id];
}
@ -212,6 +251,7 @@ public class ResourceParser {
* @return ID
*/
public static int getNoteBgNormalRes(int id) {
if (id >= BG_NORMAL_RESOURCES.length || id < 0) return R.drawable.list_white_middle;
return BG_NORMAL_RESOURCES[id];
}

@ -0,0 +1,257 @@
package net.micode.notes.tool;
import android.app.AlertDialog;
import android.content.Context;
import android.content.DialogInterface;
import android.graphics.Color;
import android.graphics.Typeface;
import android.text.Editable;
import android.text.Html;
import android.text.Spannable;
import android.text.Spanned;
import android.text.TextUtils;
import android.text.style.BackgroundColorSpan;
import android.text.style.BulletSpan;
import android.text.style.ForegroundColorSpan;
import android.text.style.QuoteSpan;
import android.text.style.RelativeSizeSpan;
import android.text.style.StrikethroughSpan;
import android.text.style.StyleSpan;
import android.text.style.TypefaceSpan;
import android.text.style.URLSpan;
import android.text.style.UnderlineSpan;
import android.view.LayoutInflater;
import android.view.View;
import android.widget.EditText;
public class RichTextHelper {
public static void applyBold(EditText editText) {
applyStyleSpan(editText, Typeface.BOLD);
}
public static void applyItalic(EditText editText) {
applyStyleSpan(editText, Typeface.ITALIC);
}
public static void applyUnderline(EditText editText) {
int start = editText.getSelectionStart();
int end = editText.getSelectionEnd();
if (start > end) { int temp = start; start = end; end = temp; }
Editable editable = editText.getText();
UnderlineSpan[] spans = editable.getSpans(start, end, UnderlineSpan.class);
if (spans != null && spans.length > 0) {
for (UnderlineSpan span : spans) {
editable.removeSpan(span);
}
} else {
editable.setSpan(new UnderlineSpan(), start, end, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
}
}
public static void applyStrikethrough(EditText editText) {
int start = editText.getSelectionStart();
int end = editText.getSelectionEnd();
if (start > end) { int temp = start; start = end; end = temp; }
Editable editable = editText.getText();
StrikethroughSpan[] spans = editable.getSpans(start, end, StrikethroughSpan.class);
if (spans != null && spans.length > 0) {
for (StrikethroughSpan span : spans) {
editable.removeSpan(span);
}
} else {
editable.setSpan(new StrikethroughSpan(), start, end, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
}
}
private static void applyStyleSpan(EditText editText, int style) {
int start = editText.getSelectionStart();
int end = editText.getSelectionEnd();
if (start > end) { int temp = start; start = end; end = temp; }
Editable editable = editText.getText();
StyleSpan[] spans = editable.getSpans(start, end, StyleSpan.class);
boolean exists = false;
for (StyleSpan span : spans) {
if (span.getStyle() == style) {
editable.removeSpan(span);
exists = true;
}
}
if (!exists) {
editable.setSpan(new StyleSpan(style), start, end, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
}
}
public static void applyHeading(EditText editText, int level) {
int start = editText.getSelectionStart();
int end = editText.getSelectionEnd();
// Expand to full line
Editable text = editText.getText();
String string = text.toString();
// Find line start and end
int lineStart = string.lastIndexOf('\n', start - 1) + 1;
if (lineStart < 0) lineStart = 0;
int lineEnd = string.indexOf('\n', end);
if (lineEnd < 0) lineEnd = string.length();
// Remove existing heading spans
RelativeSizeSpan[] sizeSpans = text.getSpans(lineStart, lineEnd, RelativeSizeSpan.class);
for (RelativeSizeSpan span : sizeSpans) {
text.removeSpan(span);
}
StyleSpan[] styleSpans = text.getSpans(lineStart, lineEnd, StyleSpan.class);
for (StyleSpan span : styleSpans) {
if (span.getStyle() == Typeface.BOLD) {
text.removeSpan(span);
}
}
if (level > 0) {
float scale = 1.0f;
switch (level) {
case 1: scale = 2.0f; break;
case 2: scale = 1.5f; break;
case 3: scale = 1.25f; break;
case 4: scale = 1.1f; break;
case 5: scale = 1.0f; break;
case 6: scale = 0.8f; break;
}
text.setSpan(new RelativeSizeSpan(scale), lineStart, lineEnd, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
text.setSpan(new StyleSpan(Typeface.BOLD), lineStart, lineEnd, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
}
}
public static void applyBullet(EditText editText) {
// Simple bullet implementation for now
int start = editText.getSelectionStart();
Editable text = editText.getText();
String string = text.toString();
int lineStart = string.lastIndexOf('\n', start - 1) + 1;
if (lineStart < 0) lineStart = 0;
// Check if already bulleted
// Note: BulletSpan covers a paragraph.
int lineEnd = string.indexOf('\n', start);
if (lineEnd < 0) lineEnd = string.length();
BulletSpan[] spans = text.getSpans(lineStart, lineEnd, BulletSpan.class);
if (spans != null && spans.length > 0) {
for (BulletSpan span : spans) {
text.removeSpan(span);
}
} else {
text.setSpan(new BulletSpan(20, Color.BLACK), lineStart, lineEnd, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
}
}
public static void applyQuote(EditText editText) {
int start = editText.getSelectionStart();
Editable text = editText.getText();
String string = text.toString();
int lineStart = string.lastIndexOf('\n', start - 1) + 1;
if (lineStart < 0) lineStart = 0;
int lineEnd = string.indexOf('\n', start);
if (lineEnd < 0) lineEnd = string.length();
QuoteSpan[] spans = text.getSpans(lineStart, lineEnd, QuoteSpan.class);
if (spans != null && spans.length > 0) {
for (QuoteSpan span : spans) {
text.removeSpan(span);
}
} else {
text.setSpan(new QuoteSpan(Color.GRAY), lineStart, lineEnd, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
}
}
public static void applyCode(EditText editText) {
int start = editText.getSelectionStart();
int end = editText.getSelectionEnd();
if (start > end) { int temp = start; start = end; end = temp; }
Editable editable = editText.getText();
TypefaceSpan[] spans = editable.getSpans(start, end, TypefaceSpan.class);
boolean exists = false;
for (TypefaceSpan span : spans) {
if ("monospace".equals(span.getFamily())) {
editable.removeSpan(span);
exists = true;
}
}
// Also toggle background color for code block look
BackgroundColorSpan[] bgSpans = editable.getSpans(start, end, BackgroundColorSpan.class);
for (BackgroundColorSpan span : bgSpans) {
if (span.getBackgroundColor() == 0xFFEEEEEE) {
editable.removeSpan(span);
}
}
if (!exists) {
editable.setSpan(new TypefaceSpan("monospace"), start, end, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
editable.setSpan(new BackgroundColorSpan(0xFFEEEEEE), start, end, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
}
}
public static void insertLink(Context context, final EditText editText) {
AlertDialog.Builder builder = new AlertDialog.Builder(context);
builder.setTitle("Insert Link");
final EditText input = new EditText(context);
input.setHint("http://example.com");
builder.setView(input);
builder.setPositiveButton("OK", new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface dialog, int which) {
String url = input.getText().toString();
if (!TextUtils.isEmpty(url)) {
int start = editText.getSelectionStart();
int end = editText.getSelectionEnd();
if (start == end) {
// Insert url as text
editText.getText().insert(start, url);
end = start + url.length();
}
editText.getText().setSpan(new URLSpan(url), start, end, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
}
}
});
builder.setNegativeButton("Cancel", null);
builder.show();
}
public static void applyColor(EditText editText, int color, boolean isBackground) {
int start = editText.getSelectionStart();
int end = editText.getSelectionEnd();
if (start > end) { int temp = start; start = end; end = temp; }
Editable editable = editText.getText();
if (isBackground) {
BackgroundColorSpan[] spans = editable.getSpans(start, end, BackgroundColorSpan.class);
for (BackgroundColorSpan span : spans) editable.removeSpan(span);
editable.setSpan(new BackgroundColorSpan(color), start, end, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
} else {
ForegroundColorSpan[] spans = editable.getSpans(start, end, ForegroundColorSpan.class);
for (ForegroundColorSpan span : spans) editable.removeSpan(span);
editable.setSpan(new ForegroundColorSpan(color), start, end, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
}
}
public static void insertDivider(EditText editText) {
int start = editText.getSelectionStart();
editText.getText().insert(start, "\n-------------------\n");
}
public static String toHtml(Spanned text) {
return Html.toHtml(text);
}
public static Spanned fromHtml(String html) {
return Html.fromHtml(html);
}
}

@ -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<String> getHistory() {
String json = mPrefs.getString(KEY_HISTORY, "");
List<String> 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<String> 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<String> history = getHistory();
if (history.remove(keyword)) {
saveHistory(history);
}
}
public void clearHistory() {
mPrefs.edit().remove(KEY_HISTORY).apply();
}
private void saveHistory(List<String> history) {
JSONArray array = new JSONArray(history);
mPrefs.edit().putString(KEY_HISTORY, array.toString()).apply();
}
}

@ -0,0 +1,111 @@
package net.micode.notes.ui;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.ImageView;
import androidx.annotation.NonNull;
import androidx.recyclerview.widget.RecyclerView;
import net.micode.notes.R;
import net.micode.notes.tool.ResourceParser;
import java.util.List;
public class NoteColorAdapter extends RecyclerView.Adapter<NoteColorAdapter.ViewHolder> {
public interface OnColorClickListener {
void onColorClick(int colorId);
}
private List<Integer> mColorIds;
private int mSelectedColorId;
private OnColorClickListener mListener;
public NoteColorAdapter(List<Integer> colorIds, int selectedColorId, OnColorClickListener listener) {
mColorIds = colorIds;
mSelectedColorId = selectedColorId;
mListener = listener;
}
public void setSelectedColor(int colorId) {
mSelectedColorId = colorId;
notifyDataSetChanged();
}
@NonNull
@Override
public ViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
View view = LayoutInflater.from(parent.getContext())
.inflate(R.layout.note_color_item, parent, false);
return new ViewHolder(view);
}
@Override
public void onBindViewHolder(@NonNull ViewHolder holder, int position) {
int colorId = mColorIds.get(position);
if (colorId == ResourceParser.CUSTOM_COLOR_BUTTON_ID) {
holder.colorView.setScaleType(ImageView.ScaleType.CENTER_INSIDE);
int padding = (int) (12 * holder.itemView.getContext().getResources().getDisplayMetrics().density);
holder.colorView.setPadding(padding, padding, padding, padding);
holder.colorView.setImageResource(R.drawable.ic_palette);
// Apply a dark tint to ensure visibility
holder.colorView.setColorFilter(android.graphics.Color.DKGRAY, android.graphics.PorterDuff.Mode.SRC_IN);
// Optional: Set a background for the icon
holder.colorView.setBackgroundResource(R.drawable.bg_color_btn_mask);
} else if (colorId == ResourceParser.WALLPAPER_BUTTON_ID) {
holder.colorView.setScaleType(ImageView.ScaleType.CENTER_INSIDE);
int padding = (int) (12 * holder.itemView.getContext().getResources().getDisplayMetrics().density);
holder.colorView.setPadding(padding, padding, padding, padding);
holder.colorView.setImageResource(R.drawable.ic_image);
// Apply a dark tint to ensure visibility
holder.colorView.setColorFilter(android.graphics.Color.DKGRAY, android.graphics.PorterDuff.Mode.SRC_IN);
// Optional: Set a background for the icon
holder.colorView.setBackgroundResource(R.drawable.bg_color_btn_mask);
} else {
holder.colorView.setScaleType(ImageView.ScaleType.CENTER_CROP);
holder.colorView.setPadding(0, 0, 0, 0);
// 使用ResourceParser获取背景资源
int bgRes = ResourceParser.NoteBgResources.getNoteBgResource(colorId);
holder.colorView.setImageResource(bgRes);
holder.colorView.setBackground(null); // Clear background if reused
if (colorId >= ResourceParser.MIDNIGHT_BLACK || colorId < 0) {
int color = ResourceParser.getNoteBgColor(holder.itemView.getContext(), colorId);
holder.colorView.setColorFilter(color, android.graphics.PorterDuff.Mode.MULTIPLY);
} else {
holder.colorView.clearColorFilter();
}
}
if (colorId == mSelectedColorId && colorId != ResourceParser.CUSTOM_COLOR_BUTTON_ID) {
holder.checkView.setVisibility(View.VISIBLE);
} else {
holder.checkView.setVisibility(View.GONE);
}
holder.itemView.setOnClickListener(v -> {
if (mListener != null) {
mListener.onColorClick(colorId);
}
});
}
@Override
public int getItemCount() {
return mColorIds.size();
}
static class ViewHolder extends RecyclerView.ViewHolder {
ImageView colorView;
ImageView checkView;
public ViewHolder(@NonNull View itemView) {
super(itemView);
colorView = itemView.findViewById(R.id.color_view);
checkView = itemView.findViewById(R.id.check_view);
}
}
}

@ -57,6 +57,8 @@ import android.widget.Toast;
import net.micode.notes.R;
import net.micode.notes.data.Notes;
import net.micode.notes.data.Notes.TextNote;
import net.micode.notes.model.NoteCommand;
import net.micode.notes.model.UndoRedoManager;
import net.micode.notes.model.WorkingNote;
import net.micode.notes.model.WorkingNote.NoteSettingChangedListener;
import net.micode.notes.tool.DataUtils;
@ -77,8 +79,11 @@ import androidx.appcompat.app.AppCompatActivity;
import com.google.android.material.appbar.MaterialToolbar;
import net.micode.notes.databinding.NoteEditBinding;
import net.micode.notes.tool.RichTextHelper;
import net.micode.notes.data.FontManager;
public class NoteEditActivity extends AppCompatActivity implements OnClickListener,
NoteSettingChangedListener, OnTextViewChangeListener {
/**
@ -101,24 +106,6 @@ public class NoteEditActivity extends AppCompatActivity implements OnClickListen
public EditText etTitle;
}
private static final Map<Integer, Integer> sBgSelectorBtnsMap = new HashMap<Integer, Integer>();
static {
sBgSelectorBtnsMap.put(R.id.iv_bg_yellow, ResourceParser.YELLOW);
sBgSelectorBtnsMap.put(R.id.iv_bg_red, ResourceParser.RED);
sBgSelectorBtnsMap.put(R.id.iv_bg_blue, ResourceParser.BLUE);
sBgSelectorBtnsMap.put(R.id.iv_bg_green, ResourceParser.GREEN);
sBgSelectorBtnsMap.put(R.id.iv_bg_white, ResourceParser.WHITE);
}
private static final Map<Integer, Integer> sBgSelectorSelectionMap = new HashMap<Integer, Integer>();
static {
sBgSelectorSelectionMap.put(ResourceParser.YELLOW, R.id.iv_bg_yellow_select);
sBgSelectorSelectionMap.put(ResourceParser.RED, R.id.iv_bg_red_select);
sBgSelectorSelectionMap.put(ResourceParser.BLUE, R.id.iv_bg_blue_select);
sBgSelectorSelectionMap.put(ResourceParser.GREEN, R.id.iv_bg_green_select);
sBgSelectorSelectionMap.put(ResourceParser.WHITE, R.id.iv_bg_white_select);
}
private static final Map<Integer, Integer> sFontSizeBtnsMap = new HashMap<Integer, Integer>();
static {
sFontSizeBtnsMap.put(R.id.ll_font_large, ResourceParser.TEXT_LARGE);
@ -145,6 +132,8 @@ public class NoteEditActivity extends AppCompatActivity implements OnClickListen
private View mFontSizeSelector;
private View mRichTextSelector;
private EditText mNoteEditor;
private View mNoteEditorPanel;
@ -170,25 +159,11 @@ public class NoteEditActivity extends AppCompatActivity implements OnClickListen
private NoteEditBinding binding;
/**
* ID
*/
private View getBgSelectorView(int viewId) {
switch (viewId) {
case R.id.iv_bg_yellow_select:
return binding.ivBgYellowSelect;
case R.id.iv_bg_red_select:
return binding.ivBgRedSelect;
case R.id.iv_bg_blue_select:
return binding.ivBgBlueSelect;
case R.id.iv_bg_green_select:
return binding.ivBgGreenSelect;
case R.id.iv_bg_white_select:
return binding.ivBgWhiteSelect;
default:
throw new IllegalArgumentException("Unknown view ID: " + viewId);
}
}
private UndoRedoManager mUndoRedoManager;
private boolean mInUndoRedo = false;
private androidx.recyclerview.widget.RecyclerView mColorSelectorRv;
private NoteColorAdapter mColorAdapter;
/**
* ID
@ -215,6 +190,7 @@ public class NoteEditActivity extends AppCompatActivity implements OnClickListen
// 使用ViewBinding设置布局
binding = NoteEditBinding.inflate(getLayoutInflater());
setContentView(binding.getRoot());
mUndoRedoManager = new UndoRedoManager();
// 初始化Toolbar使用MaterialToolbar与列表页面一致
setSupportActionBar(binding.toolbar);
@ -368,10 +344,16 @@ public class NoteEditActivity extends AppCompatActivity implements OnClickListen
mNoteEditor = binding.noteEditView;
mNoteEditorPanel = binding.svNoteEdit;
mNoteBgColorSelector = binding.noteBgColorSelector;
mColorSelectorRv = binding.rvBgColorSelector;
mNoteEditor.addTextChangedListener(new TextWatcher() {
private CharSequence mBeforeText;
@Override
public void beforeTextChanged(CharSequence s, int start, int count, int after) {
if (!mInUndoRedo) {
mBeforeText = s.subSequence(start, start + count).toString();
}
}
@Override
@ -379,6 +361,13 @@ public class NoteEditActivity extends AppCompatActivity implements OnClickListen
if (mNoteHeaderHolder != null && mNoteHeaderHolder.tvCharCount != null) {
mNoteHeaderHolder.tvCharCount.setText(String.valueOf(s.length()) + " 字");
}
if (!mInUndoRedo) {
CharSequence afterText = s.subSequence(start, start + count).toString();
if (!TextUtils.equals(mBeforeText, afterText)) {
mUndoRedoManager.addCommand(new NoteCommand(mNoteEditor, start, mBeforeText, afterText));
invalidateOptionsMenu();
}
}
}
@Override
@ -406,30 +395,34 @@ public class NoteEditActivity extends AppCompatActivity implements OnClickListen
}
});
// 设置背景颜色选择器的点击事件
for (int id : sBgSelectorBtnsMap.keySet()) {
ImageView iv;
switch (id) {
case R.id.iv_bg_yellow:
iv = binding.ivBgYellow;
break;
case R.id.iv_bg_red:
iv = binding.ivBgRed;
break;
case R.id.iv_bg_blue:
iv = binding.ivBgBlue;
break;
case R.id.iv_bg_green:
iv = binding.ivBgGreen;
break;
case R.id.iv_bg_white:
iv = binding.ivBgWhite;
break;
default:
throw new IllegalArgumentException("Unknown view ID: " + id);
// Initialize Color Adapter
java.util.List<Integer> colors = java.util.Arrays.asList(
ResourceParser.YELLOW,
ResourceParser.BLUE,
ResourceParser.WHITE,
ResourceParser.GREEN,
ResourceParser.RED,
ResourceParser.MIDNIGHT_BLACK,
ResourceParser.EYE_CARE_GREEN,
ResourceParser.WARM,
ResourceParser.COOL,
ResourceParser.CUSTOM_COLOR_BUTTON_ID,
ResourceParser.WALLPAPER_BUTTON_ID
);
mColorAdapter = new NoteColorAdapter(colors, ResourceParser.YELLOW, new NoteColorAdapter.OnColorClickListener() {
@Override
public void onColorClick(int colorId) {
if (colorId == ResourceParser.CUSTOM_COLOR_BUTTON_ID) {
showColorPickerDialog();
} else if (colorId == ResourceParser.WALLPAPER_BUTTON_ID) {
pickWallpaper();
} else {
mWorkingNote.setBgColorId(colorId);
mNoteBgColorSelector.setVisibility(View.GONE);
}
}
iv.setOnClickListener(this);
}
});
mColorSelectorRv.setAdapter(mColorAdapter);
mFontSizeSelector = binding.fontSizeSelector;
for (int id : sFontSizeBtnsMap.keySet()) {
@ -464,6 +457,7 @@ public class NoteEditActivity extends AppCompatActivity implements OnClickListen
mFontSizeId = ResourceParser.BG_DEFAULT_FONT_SIZE;
}
mEditTextList = binding.noteEditList;
initRichTextToolbar();
}
@Override
@ -487,21 +481,28 @@ public class NoteEditActivity extends AppCompatActivity implements OnClickListen
private void initNoteScreen() {
mNoteEditor.setTextAppearance(this, TextAppearanceResources
.getTexAppearanceResource(mFontSizeId));
// Apply custom font
FontManager.getInstance(this).applyFont(mNoteEditor);
if (mWorkingNote.getCheckListMode() == TextNote.MODE_CHECK_LIST) {
switchToListMode(mWorkingNote.getContent());
} else {
mNoteEditor.setText(getHighlightQueryResult(mWorkingNote.getContent(), mUserQuery));
String content = mWorkingNote.getContent();
if (content.contains("<") && content.contains(">")) {
mNoteEditor.setText(RichTextHelper.fromHtml(content));
} else {
mNoteEditor.setText(getHighlightQueryResult(content, mUserQuery));
}
mNoteEditor.setSelection(mNoteEditor.getText().length());
}
mNoteHeaderHolder.etTitle.setText(mWorkingNote.getTitle());
for (Integer id : sBgSelectorSelectionMap.keySet()) {
View view = getBgSelectorView(sBgSelectorSelectionMap.get(id));
if (view != null) {
view.setVisibility(View.GONE);
}
// Update Color Adapter selection
if (mColorAdapter != null) {
mColorAdapter.setSelectedColor(mWorkingNote.getBgColorId());
}
mHeadViewPanel.setBackgroundResource(mWorkingNote.getTitleBgResId());
mNoteEditorPanel.setBackgroundResource(mWorkingNote.getBgColorResId());
updateNoteBackgrounds();
mNoteHeaderHolder.tvModified.setText(DateUtils.formatDateTime(this,
mWorkingNote.getModifiedDate(), DateUtils.FORMAT_SHOW_DATE
@ -678,17 +679,7 @@ public class NoteEditActivity extends AppCompatActivity implements OnClickListen
int id = v.getId();
if (id == R.id.btn_set_bg_color) {
mNoteBgColorSelector.setVisibility(View.VISIBLE);
View bgView = getBgSelectorView(sBgSelectorSelectionMap.get(mWorkingNote.getBgColorId()));
if (bgView != null) {
bgView.setVisibility(View.VISIBLE);
}
} else if (sBgSelectorBtnsMap.containsKey(id)) {
View bgView = getBgSelectorView(sBgSelectorSelectionMap.get(mWorkingNote.getBgColorId()));
if (bgView != null) {
bgView.setVisibility(View.GONE);
}
mWorkingNote.setBgColorId(sBgSelectorBtnsMap.get(id));
mNoteBgColorSelector.setVisibility(View.GONE);
// Note: Adapter selection is already set in onBackgroundColorChanged or init
} else if (sFontSizeBtnsMap.containsKey(id)) {
View fontView = getFontSelectorView(sFontSelectorSelectionMap.get(mFontSizeId));
if (fontView != null) {
@ -706,6 +697,8 @@ public class NoteEditActivity extends AppCompatActivity implements OnClickListen
} else {
mNoteEditor.setTextAppearance(this,
TextAppearanceResources.getTexAppearanceResource(mFontSizeId));
// Apply custom font again as setTextAppearance might reset it
FontManager.getInstance(this).applyFont(mNoteEditor);
}
mFontSizeSelector.setVisibility(View.GONE);
}
@ -758,12 +751,143 @@ public class NoteEditActivity extends AppCompatActivity implements OnClickListen
* </p>
*/
public void onBackgroundColorChanged() {
View bgView = getBgSelectorView(sBgSelectorSelectionMap.get(mWorkingNote.getBgColorId()));
if (bgView != null) {
bgView.setVisibility(View.VISIBLE);
if (mColorAdapter != null) {
mColorAdapter.setSelectedColor(mWorkingNote.getBgColorId());
}
updateNoteBackgrounds();
}
private void updateNoteBackgrounds() {
int colorId = mWorkingNote.getBgColorId();
String wallpaperPath = mWorkingNote.getWallpaperPath();
if (wallpaperPath != null) {
// Load wallpaper
android.net.Uri uri = android.net.Uri.parse(wallpaperPath);
try {
java.io.InputStream inputStream = getContentResolver().openInputStream(uri);
android.graphics.Bitmap bitmap = android.graphics.BitmapFactory.decodeStream(inputStream);
android.graphics.drawable.BitmapDrawable drawable = new android.graphics.drawable.BitmapDrawable(getResources(), bitmap);
// Tiling mode (can be configurable later)
drawable.setTileModeXY(android.graphics.Shader.TileMode.REPEAT, android.graphics.Shader.TileMode.REPEAT);
// Add Blur Effect for Android 12+
if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.S) {
mNoteEditorPanel.setBackground(drawable);
mNoteEditorPanel.setRenderEffect(android.graphics.RenderEffect.createBlurEffect(
20f, 20f, android.graphics.Shader.TileMode.CLAMP));
} else {
mNoteEditorPanel.setBackground(drawable);
}
// Header always uses original wallpaper (or maybe slightly darker?)
mHeadViewPanel.setBackground(drawable.getConstantState().newDrawable());
// Dynamic Coloring with Palette
androidx.palette.graphics.Palette.from(bitmap).generate(palette -> {
if (palette != null) {
applyPaletteColors(palette);
}
});
} catch (Exception e) {
Log.e(TAG, "Failed to load wallpaper", e);
// Fallback to color
applyColorBackground(colorId);
}
} else {
applyColorBackground(colorId);
// Reset toolbar colors to default/theme
resetToolbarColors();
}
updateTextColor(colorId);
}
private void applyPaletteColors(androidx.palette.graphics.Palette palette) {
int primaryColor = palette.getDominantColor(getResources().getColor(R.color.primary_color));
int onPrimaryColor = getResources().getColor(R.color.on_primary_color);
// Ensure contrast for onPrimaryColor
if (androidx.core.graphics.ColorUtils.calculateContrast(onPrimaryColor, primaryColor) < 3.0) {
onPrimaryColor = android.graphics.Color.WHITE;
}
binding.toolbar.setBackgroundColor(primaryColor);
binding.toolbar.setTitleTextColor(onPrimaryColor);
if (binding.toolbar.getNavigationIcon() != null) {
binding.toolbar.getNavigationIcon().setTint(onPrimaryColor);
}
getWindow().setStatusBarColor(primaryColor);
}
private void resetToolbarColors() {
int primaryColor = getResources().getColor(R.color.primary_color);
int onPrimaryColor = getResources().getColor(R.color.on_primary_color);
binding.toolbar.setBackgroundColor(primaryColor);
binding.toolbar.setTitleTextColor(onPrimaryColor);
if (binding.toolbar.getNavigationIcon() != null) {
binding.toolbar.getNavigationIcon().setTint(onPrimaryColor);
}
getWindow().setStatusBarColor(primaryColor);
}
private void updateTextColor(int colorId) {
// Default to black for light backgrounds
int textColor = android.graphics.Color.BLACK;
if (colorId == ResourceParser.MIDNIGHT_BLACK) {
textColor = android.graphics.Color.WHITE;
} else if (colorId < 0) {
// Custom color: Calculate luminance
// colorId is the ARGB value for custom colors
if (isColorDark(colorId)) {
textColor = android.graphics.Color.WHITE;
}
}
// For wallpaper, we might want to check palette, but for now default to black or keep current
// If wallpaper is set, this method is called with the underlying colorId.
// We should probably rely on the underlying color or default to white/black.
mNoteEditor.setTextColor(textColor);
// Also update title color if needed
if (mNoteHeaderHolder != null && mNoteHeaderHolder.etTitle != null) {
mNoteHeaderHolder.etTitle.setTextColor(textColor);
}
}
private boolean isColorDark(int color) {
double darkness = 1 - (0.299 * android.graphics.Color.red(color) +
0.587 * android.graphics.Color.green(color) +
0.114 * android.graphics.Color.blue(color)) / 255;
return darkness >= 0.5;
}
private void applyColorBackground(int colorId) {
mNoteEditorPanel.setBackgroundResource(mWorkingNote.getBgColorResId());
mHeadViewPanel.setBackgroundResource(mWorkingNote.getTitleBgResId());
if (colorId >= ResourceParser.MIDNIGHT_BLACK || colorId < 0) {
int color = ResourceParser.getNoteBgColor(this, colorId);
if (mNoteEditorPanel.getBackground() != null) {
mNoteEditorPanel.getBackground().setTint(color);
mNoteEditorPanel.getBackground().setTintMode(android.graphics.PorterDuff.Mode.MULTIPLY);
}
if (mHeadViewPanel.getBackground() != null) {
mHeadViewPanel.getBackground().setTint(color);
mHeadViewPanel.getBackground().setTintMode(android.graphics.PorterDuff.Mode.MULTIPLY);
}
} else {
// Clear tint for legacy resources
if (mNoteEditorPanel.getBackground() != null) {
mNoteEditorPanel.getBackground().clearColorFilter();
}
if (mHeadViewPanel.getBackground() != null) {
mHeadViewPanel.getBackground().clearColorFilter();
}
}
}
/**
@ -790,6 +914,18 @@ public class NoteEditActivity extends AppCompatActivity implements OnClickListen
getMenuInflater().inflate(R.menu.call_note_edit, menu);
} else {
getMenuInflater().inflate(R.menu.note_edit, menu);
MenuItem undoItem = menu.findItem(R.id.menu_undo);
MenuItem redoItem = menu.findItem(R.id.menu_redo);
MenuItem clearItem = menu.findItem(R.id.menu_clear_history);
if (undoItem != null) {
undoItem.setEnabled(mUndoRedoManager.canUndo());
}
if (redoItem != null) {
redoItem.setEnabled(mUndoRedoManager.canRedo());
}
if (clearItem != null) {
clearItem.setEnabled(mUndoRedoManager.canUndo() || mUndoRedoManager.canRedo());
}
}
if (mWorkingNote.getCheckListMode() == TextNote.MODE_CHECK_LIST) {
menu.findItem(R.id.menu_list_mode).setTitle(R.string.menu_normal_mode);
@ -825,6 +961,33 @@ public class NoteEditActivity extends AppCompatActivity implements OnClickListen
@Override
public boolean onOptionsItemSelected(MenuItem item) {
switch (item.getItemId()) {
case R.id.menu_rich_text:
if (mRichTextSelector.getVisibility() == View.VISIBLE) {
mRichTextSelector.setVisibility(View.GONE);
} else {
mRichTextSelector.setVisibility(View.VISIBLE);
mFontSizeSelector.setVisibility(View.GONE);
}
break;
case R.id.menu_undo:
mInUndoRedo = true;
mUndoRedoManager.undo();
mInUndoRedo = false;
invalidateOptionsMenu();
showToast(R.string.undo_success);
break;
case R.id.menu_redo:
mInUndoRedo = true;
mUndoRedoManager.redo();
mInUndoRedo = false;
invalidateOptionsMenu();
showToast(R.string.redo_success);
break;
case R.id.menu_clear_history:
mUndoRedoManager.clear();
invalidateOptionsMenu();
showToast(R.string.menu_clear_history);
break;
case R.id.menu_new_note:
createNewNote();
break;
@ -1143,6 +1306,9 @@ public class NoteEditActivity extends AppCompatActivity implements OnClickListen
View view = LayoutInflater.from(this).inflate(R.layout.note_edit_list_item, null);
final NoteEditText edit = (NoteEditText) view.findViewById(R.id.et_edit_text);
edit.setTextAppearance(this, TextAppearanceResources.getTexAppearanceResource(mFontSizeId));
// Apply custom font
FontManager.getInstance(this).applyFont(edit);
CheckBox cb = ((CheckBox) view.findViewById(R.id.cb_edit_item));
cb.setOnCheckedChangeListener(new OnCheckedChangeListener() {
public void onCheckedChanged(CompoundButton buttonView, boolean isChecked) {
@ -1202,6 +1368,8 @@ public class NoteEditActivity extends AppCompatActivity implements OnClickListen
* @param newMode
*/
public void onCheckListModeChanged(int oldMode, int newMode) {
mUndoRedoManager.clear();
invalidateOptionsMenu();
if (newMode == TextNote.MODE_CHECK_LIST) {
switchToListMode(mNoteEditor.getText().toString());
} else {
@ -1245,7 +1413,7 @@ public class NoteEditActivity extends AppCompatActivity implements OnClickListen
}
mWorkingNote.setWorkingText(sb.toString());
} else {
mWorkingNote.setWorkingText(mNoteEditor.getText().toString());
mWorkingNote.setWorkingText(RichTextHelper.toHtml(mNoteEditor.getText()));
}
return hasChecked;
}
@ -1336,6 +1504,92 @@ public class NoteEditActivity extends AppCompatActivity implements OnClickListen
SHORTCUT_ICON_TITLE_MAX_LEN) : content;
}
private void showColorPickerDialog() {
final View dialogView = LayoutInflater.from(this).inflate(R.layout.dialog_color_picker, null);
final View colorPreview = dialogView.findViewById(R.id.view_color_preview);
android.widget.SeekBar sbRed = dialogView.findViewById(R.id.sb_red);
android.widget.SeekBar sbGreen = dialogView.findViewById(R.id.sb_green);
android.widget.SeekBar sbBlue = dialogView.findViewById(R.id.sb_blue);
int currentColor = android.graphics.Color.WHITE;
if (mWorkingNote.getBgColorId() < 0) {
currentColor = mWorkingNote.getBgColorId();
}
final int[] rgb = new int[]{
android.graphics.Color.red(currentColor),
android.graphics.Color.green(currentColor),
android.graphics.Color.blue(currentColor)
};
colorPreview.setBackgroundColor(android.graphics.Color.rgb(rgb[0], rgb[1], rgb[2]));
sbRed.setProgress(rgb[0]);
sbGreen.setProgress(rgb[1]);
sbBlue.setProgress(rgb[2]);
android.widget.SeekBar.OnSeekBarChangeListener listener = new android.widget.SeekBar.OnSeekBarChangeListener() {
@Override
public void onProgressChanged(android.widget.SeekBar seekBar, int progress, boolean fromUser) {
if (seekBar.getId() == R.id.sb_red) rgb[0] = progress;
else if (seekBar.getId() == R.id.sb_green) rgb[1] = progress;
else if (seekBar.getId() == R.id.sb_blue) rgb[2] = progress;
colorPreview.setBackgroundColor(android.graphics.Color.rgb(rgb[0], rgb[1], rgb[2]));
}
@Override
public void onStartTrackingTouch(android.widget.SeekBar seekBar) {}
@Override
public void onStopTrackingTouch(android.widget.SeekBar seekBar) {}
};
sbRed.setOnSeekBarChangeListener(listener);
sbGreen.setOnSeekBarChangeListener(listener);
sbBlue.setOnSeekBarChangeListener(listener);
new AlertDialog.Builder(this)
.setTitle("Custom Color")
.setView(dialogView)
.setPositiveButton(android.R.string.ok, (dialog, which) -> {
int newColor = android.graphics.Color.rgb(rgb[0], rgb[1], rgb[2]);
// Use negative integer for custom color. Ensure it's negative.
// ARGB color with alpha 255 is negative in Java int.
// If alpha is 0, it might be positive. We assume full opacity.
newColor |= 0xFF000000;
mWorkingNote.setBgColorId(newColor);
mNoteBgColorSelector.setVisibility(View.GONE);
})
.setNegativeButton(android.R.string.cancel, null)
.show();
}
private static final int REQUEST_CODE_PICK_WALLPAPER = 105;
private void pickWallpaper() {
Intent intent = new Intent(Intent.ACTION_GET_CONTENT);
intent.setType("image/*");
startActivityForResult(intent, REQUEST_CODE_PICK_WALLPAPER);
}
@Override
protected void onActivityResult(int requestCode, int resultCode, Intent data) {
super.onActivityResult(requestCode, resultCode, data);
if (requestCode == REQUEST_CODE_PICK_WALLPAPER && resultCode == RESULT_OK && data != null) {
android.net.Uri uri = data.getData();
if (uri != null) {
// Take persistent permissions
try {
getContentResolver().takePersistableUriPermission(uri, Intent.FLAG_GRANT_READ_URI_PERMISSION);
} catch (SecurityException e) {
Log.e(TAG, "Failed to take persistable uri permission", e);
}
mWorkingNote.setWallpaper(uri.toString());
mNoteBgColorSelector.setVisibility(View.GONE);
}
}
}
/**
* Toast
* <p>
@ -1358,4 +1612,78 @@ public class NoteEditActivity extends AppCompatActivity implements OnClickListen
private void showToast(int resId, int duration) {
Toast.makeText(this, resId, duration).show();
}
private void initRichTextToolbar() {
mRichTextSelector = findViewById(R.id.rich_text_selector);
findViewById(R.id.btn_bold).setOnClickListener(new OnClickListener() {
public void onClick(View v) { RichTextHelper.applyBold(mNoteEditor); }
});
findViewById(R.id.btn_italic).setOnClickListener(new OnClickListener() {
public void onClick(View v) { RichTextHelper.applyItalic(mNoteEditor); }
});
findViewById(R.id.btn_underline).setOnClickListener(new OnClickListener() {
public void onClick(View v) { RichTextHelper.applyUnderline(mNoteEditor); }
});
findViewById(R.id.btn_strikethrough).setOnClickListener(new OnClickListener() {
public void onClick(View v) { RichTextHelper.applyStrikethrough(mNoteEditor); }
});
findViewById(R.id.btn_header).setOnClickListener(new OnClickListener() {
public void onClick(View v) {
final CharSequence[] items = {"H1 (Largest)", "H2", "H3", "H4", "H5", "H6 (Smallest)", "Normal"};
AlertDialog.Builder builder = new AlertDialog.Builder(NoteEditActivity.this);
builder.setTitle("Header Level");
builder.setItems(items, new DialogInterface.OnClickListener() {
public void onClick(DialogInterface dialog, int item) {
// item index maps to level: 0->1, 1->2, ..., 5->6, 6->0 (Normal)
int level = (item == 6) ? 0 : (item + 1);
RichTextHelper.applyHeading(mNoteEditor, level);
}
});
builder.show();
}
});
findViewById(R.id.btn_list).setOnClickListener(new OnClickListener() {
public void onClick(View v) { RichTextHelper.applyBullet(mNoteEditor); }
});
findViewById(R.id.btn_quote).setOnClickListener(new OnClickListener() {
public void onClick(View v) { RichTextHelper.applyQuote(mNoteEditor); }
});
findViewById(R.id.btn_code).setOnClickListener(new OnClickListener() {
public void onClick(View v) { RichTextHelper.applyCode(mNoteEditor); }
});
findViewById(R.id.btn_link).setOnClickListener(new OnClickListener() {
public void onClick(View v) { RichTextHelper.insertLink(NoteEditActivity.this, mNoteEditor); }
});
findViewById(R.id.btn_divider).setOnClickListener(new OnClickListener() {
public void onClick(View v) { RichTextHelper.insertDivider(mNoteEditor); }
});
findViewById(R.id.btn_color_text).setOnClickListener(new OnClickListener() {
public void onClick(View v) {
final CharSequence[] items = {"Black", "Red", "Blue"};
final int[] colors = {android.graphics.Color.BLACK, android.graphics.Color.RED, android.graphics.Color.BLUE};
AlertDialog.Builder builder = new AlertDialog.Builder(NoteEditActivity.this);
builder.setTitle("Text Color");
builder.setItems(items, new DialogInterface.OnClickListener() {
public void onClick(DialogInterface dialog, int item) {
RichTextHelper.applyColor(mNoteEditor, colors[item], false);
}
});
builder.show();
}
});
findViewById(R.id.btn_color_fill).setOnClickListener(new OnClickListener() {
public void onClick(View v) {
final CharSequence[] items = {"None", "Yellow", "Green", "Cyan"};
final int[] colors = {android.graphics.Color.TRANSPARENT, android.graphics.Color.YELLOW, android.graphics.Color.GREEN, android.graphics.Color.CYAN};
AlertDialog.Builder builder = new AlertDialog.Builder(NoteEditActivity.this);
builder.setTitle("Background Color");
builder.setItems(items, new DialogInterface.OnClickListener() {
public void onClick(DialogInterface dialog, int item) {
RichTextHelper.applyColor(mNoteEditor, colors[item], true);
}
});
builder.show();
}
});
}
}

@ -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<String> 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<Object> 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<List<NotesRepository.NoteInfo>>() {
@Override
public void onSuccess(List<NotesRepository.NoteInfo> result) {
runOnUiThread(() -> {
List<Object> 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();
}
}
}

@ -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<RecyclerView.ViewHolder> {
private static final int TYPE_HISTORY = 1;
private static final int TYPE_NOTE = 2;
private Context mContext;
private List<Object> 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<Object> 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;
}
}

@ -746,8 +746,8 @@ public class NotesListActivity extends AppCompatActivity
switch (itemId) {
case R.id.menu_search:
// TODO: 打开搜索对话框
Toast.makeText(this, "搜索功能开发中", Toast.LENGTH_SHORT).show();
Intent searchIntent = new Intent(this, NoteSearchActivity.class);
startActivity(searchIntent);
return true;
case R.id.menu_new_folder:
// 创建新文件夹

@ -28,6 +28,7 @@ import net.micode.notes.R;
import net.micode.notes.data.Notes;
import net.micode.notes.tool.DataUtils;
import net.micode.notes.tool.ResourceParser.NoteItemBgResources;
import net.micode.notes.data.FontManager;
/**
@ -80,6 +81,7 @@ public class NotesListItem extends LinearLayout {
mCallName.setVisibility(View.GONE);
mAlert.setVisibility(View.VISIBLE);
mTitle.setTextAppearance(context, R.style.TextAppearancePrimaryItem);
FontManager.getInstance(context).applyFont(mTitle);
mTitle.setText(context.getString(R.string.call_record_folder_name)
+ context.getString(R.string.format_folder_files_count, data.getNotesCount()));
mAlert.setImageResource(R.drawable.call_record);
@ -87,6 +89,7 @@ public class NotesListItem extends LinearLayout {
mCallName.setVisibility(View.VISIBLE);
mCallName.setText(data.getCallName());
mTitle.setTextAppearance(context,R.style.TextAppearanceSecondaryItem);
FontManager.getInstance(context).applyFont(mTitle);
mTitle.setText(DataUtils.getFormattedSnippet(data.getSnippet()));
if (data.hasAlert()) {
mAlert.setImageResource(R.drawable.clock);
@ -97,6 +100,7 @@ public class NotesListItem extends LinearLayout {
} else {
mCallName.setVisibility(View.GONE);
mTitle.setTextAppearance(context, R.style.TextAppearancePrimaryItem);
FontManager.getInstance(context).applyFont(mTitle);
if (data.getType() == Notes.TYPE_FOLDER) {
mTitle.setText(data.getSnippet()
@ -131,18 +135,35 @@ public class NotesListItem extends LinearLayout {
*/
private void setBackground(NoteItemData data) {
int id = data.getBgColorId();
int resId;
if (data.getType() == Notes.TYPE_NOTE) {
if (data.isSingle() || data.isOneFollowingFolder()) {
setBackgroundResource(NoteItemBgResources.getNoteBgSingleRes(id));
resId = NoteItemBgResources.getNoteBgSingleRes(id);
} else if (data.isLast()) {
setBackgroundResource(NoteItemBgResources.getNoteBgLastRes(id));
resId = NoteItemBgResources.getNoteBgLastRes(id);
} else if (data.isFirst() || data.isMultiFollowingFolder()) {
setBackgroundResource(NoteItemBgResources.getNoteBgFirstRes(id));
resId = NoteItemBgResources.getNoteBgFirstRes(id);
} else {
setBackgroundResource(NoteItemBgResources.getNoteBgNormalRes(id));
resId = NoteItemBgResources.getNoteBgNormalRes(id);
}
} else {
setBackgroundResource(NoteItemBgResources.getFolderBgRes());
resId = NoteItemBgResources.getFolderBgRes();
}
setBackgroundResource(resId);
// Apply tint for new colors
if (data.getType() == Notes.TYPE_NOTE && (id >= net.micode.notes.tool.ResourceParser.MIDNIGHT_BLACK || id < 0)) {
int color = net.micode.notes.tool.ResourceParser.getNoteBgColor(getContext(), id);
if (getBackground() != null) {
getBackground().setTint(color);
getBackground().setTintMode(android.graphics.PorterDuff.Mode.MULTIPLY);
}
} else {
// Ensure no tint for legacy colors (if view is recycled)
if (getBackground() != null) {
getBackground().clearColorFilter();
}
}
}

@ -1,659 +1,60 @@
/*
* Copyright (c) 2010-2011, The MiCode Open Source Community (www.micode.net)
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package net.micode.notes.ui;
import android.accounts.Account;
import android.accounts.AccountManager;
import android.app.ActionBar;
import android.app.AlertDialog;
import android.content.BroadcastReceiver;
import android.content.ContentValues;
import android.content.Context;
import android.content.DialogInterface;
import android.content.Intent;
import android.content.IntentFilter;
import android.content.SharedPreferences;
import android.os.Bundle;
import android.preference.Preference;
import android.preference.Preference.OnPreferenceClickListener;
import android.preference.PreferenceActivity;
import android.preference.PreferenceCategory;
import android.text.TextUtils;
import android.text.format.DateFormat;
import android.view.LayoutInflater;
import android.view.Menu;
import android.view.MenuItem;
import android.view.View;
import android.widget.Button;
import android.widget.TextView;
import android.widget.Toast;
import androidx.appcompat.app.AppCompatActivity;
import net.micode.notes.R;
import net.micode.notes.data.Notes;
import net.micode.notes.data.Notes.NoteColumns;
import net.micode.notes.databinding.SettingsHeaderBinding;
// Google Tasks同步功能已禁用
// import net.micode.notes.gtask.remote.GTaskSyncService;
import net.micode.notes.tool.SecurityManager;
import net.micode.notes.ui.PasswordActivity;
public class NotesPreferenceActivity extends AppCompatActivity {
/**
* Activity
* <p>
* Activity
* <ul>
* <li>Google Tasks</li>
* <li></li>
* <li></li>
* </ul>
* </p>
* <p>
* PreferenceActivity使SharedPreferences
* GTaskReceiver广
* </p>
*/
public class NotesPreferenceActivity extends PreferenceActivity {
/**
* SharedPreferences
*/
public static final String PREFERENCE_NAME = "notes_preferences";
/**
* SharedPreferences
*/
public static final String PREFERENCE_SYNC_ACCOUNT_NAME = "pref_key_account_name";
/**
* SharedPreferences
*/
public static final String PREFERENCE_LAST_SYNC_TIME = "pref_last_sync_time";
/**
* SharedPreferences
*/
public static final String PREFERENCE_SET_BG_COLOR_KEY = "pref_key_bg_random_appear";
public static final String PREFERENCE_SECURITY_KEY = "pref_key_security";
public static final int REQUEST_CODE_CHECK_PASSWORD = 104;
/**
* Preference
*/
private static final String PREFERENCE_SYNC_ACCOUNT_KEY = "pref_sync_account_key";
/**
* Intent
*/
private static final String AUTHORITIES_FILTER_KEY = "authorities";
/**
* PreferenceCategory
*/
private PreferenceCategory mAccountCategory;
/**
* 广
*/
private GTaskReceiver mReceiver;
/**
*
*/
private SettingsHeaderBinding mHeaderBinding;
/**
*
*/
private Account[] mOriAccounts;
/**
*
*/
private boolean mHasAddedAccount;
/**
* Activity
* <p>
*
* <ul>
* <li>ActionBar</li>
* <li>preferences.xml</li>
* <li>广</li>
* <li></li>
* </ul>
* </p>
* @param icicle
*/
@Override
protected void onCreate(Bundle icicle) {
super.onCreate(icicle);
/* using the app icon for navigation */
getActionBar().setDisplayHomeAsUpEnabled(true);
addPreferencesFromResource(R.xml.preferences);
mAccountCategory = (PreferenceCategory) findPreference(PREFERENCE_SYNC_ACCOUNT_KEY);
// Google Tasks同步功能已禁用
// mReceiver = new GTaskReceiver();
// IntentFilter filter = new IntentFilter();
// filter.addAction(GTaskSyncService.GTASK_SERVICE_BROADCAST_NAME);
//registerReceiver(mReceiver, filter);
// if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.TIRAMISU) {
// // Android 13 (API 33) 及以上版本需要指定导出标志
// registerReceiver(mReceiver, filter, Context.RECEIVER_NOT_EXPORTED);
// } else {
// // Android 12 及以下版本使用旧方法
// registerReceiver(mReceiver, filter);
// }
mOriAccounts = null;
mHeaderBinding = SettingsHeaderBinding.inflate(getLayoutInflater());
getListView().addHeaderView(mHeaderBinding.getRoot(), null, true);
loadSecurityPreference();
}
/**
* Activity
* <p>
* Google
* UI
* </p>
*/
@Override
protected void onResume() {
super.onResume();
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_settings);
// need to set sync account automatically if user has added a new
// account
if (mHasAddedAccount) {
Account[] accounts = getGoogleAccounts();
if (mOriAccounts != null && accounts.length > mOriAccounts.length) {
for (Account accountNew : accounts) {
boolean found = false;
for (Account accountOld : mOriAccounts) {
if (TextUtils.equals(accountOld.name, accountNew.name)) {
found = true;
break;
}
}
if (!found) {
setSyncAccount(accountNew.name);
break;
}
}
}
if (getSupportActionBar() != null) {
getSupportActionBar().setDisplayHomeAsUpEnabled(true);
getSupportActionBar().setTitle(R.string.preferences_title);
}
refreshUI();
}
/**
* Activity
* <p>
* 广
* </p>
*/
@Override
protected void onDestroy() {
// Google Tasks同步功能已禁用
// if (mReceiver != null) {
// unregisterReceiver(mReceiver);
// }
mHeaderBinding = null;
super.onDestroy();
}
private void loadSecurityPreference() {
Preference securityPref = findPreference(PREFERENCE_SECURITY_KEY);
if (securityPref != null) {
securityPref.setOnPreferenceClickListener(new OnPreferenceClickListener() {
@Override
public boolean onPreferenceClick(Preference preference) {
if (!SecurityManager.getInstance(NotesPreferenceActivity.this).isPasswordSet()) {
showSetPasswordDialog();
} else {
Intent intent = new Intent(NotesPreferenceActivity.this, PasswordActivity.class);
intent.setAction(PasswordActivity.ACTION_CHECK_PASSWORD);
startActivityForResult(intent, REQUEST_CODE_CHECK_PASSWORD);
}
return true;
}
});
}
}
private void showSetPasswordDialog() {
new AlertDialog.Builder(this)
.setTitle("设置密码")
.setItems(new String[]{"数字锁", "手势锁"}, (dialog, which) -> {
int type = (which == 0) ? SecurityManager.TYPE_PIN : SecurityManager.TYPE_PATTERN;
Intent intent = new Intent(this, PasswordActivity.class);
intent.setAction(PasswordActivity.ACTION_SETUP_PASSWORD);
intent.putExtra(PasswordActivity.EXTRA_PASSWORD_TYPE, type);
startActivity(intent);
})
.show();
}
private void showManagePasswordDialog() {
new AlertDialog.Builder(this)
.setTitle("管理密码")
.setItems(new String[]{"更改密码", "取消密码"}, (dialog, which) -> {
if (which == 0) { // Change
showSetPasswordDialog();
} else { // Remove
SecurityManager.getInstance(this).removePassword();
Toast.makeText(this, "密码已取消", Toast.LENGTH_SHORT).show();
}
})
.show();
}
@Override
protected void onActivityResult(int requestCode, int resultCode, Intent data) {
super.onActivityResult(requestCode, resultCode, data);
if (requestCode == REQUEST_CODE_CHECK_PASSWORD && resultCode == RESULT_OK) {
showManagePasswordDialog();
if (savedInstanceState == null) {
getSupportFragmentManager()
.beginTransaction()
.replace(R.id.settings_container, new SettingsFragment())
.commit();
}
}
/**
*
* <p>
* Preference
* Preference
* <ul>
* <li></li>
* <li></li>
* <li></li>
* </ul>
* </p>
*/
private void loadAccountPreference() {
mAccountCategory.removeAll();
Preference accountPref = new Preference(this);
final String defaultAccount = getSyncAccountName(this);
accountPref.setTitle(getString(R.string.preferences_account_title));
accountPref.setSummary(getString(R.string.preferences_account_summary));
accountPref.setOnPreferenceClickListener(new OnPreferenceClickListener() {
public boolean onPreferenceClick(Preference preference) {
// Google Tasks同步功能已禁用
// if (!GTaskSyncService.isSyncing()) {
// if (TextUtils.isEmpty(defaultAccount)) {
// // first time to set account
// showSelectAccountAlertDialog();
// } else {
// // if account has already been set, we need to promp
// // user about risk
// showChangeAccountConfirmAlertDialog();
// }
// } else {
// Toast.makeText(NotesPreferenceActivity.this,
// R.string.preferences_toast_cannot_change_account, Toast.LENGTH_SHORT)
// .show();
// }
Toast.makeText(NotesPreferenceActivity.this,
"Google Tasks同步功能已禁用", Toast.LENGTH_SHORT)
.show();
return true;
}
});
mAccountCategory.addPreference(accountPref);
loadSyncButton();
}
/**
*
* <p>
*
* <ul>
* <li>"取消同步"</li>
* <li>"立即同步"</li>
* </ul>
*
* </p>
*/
private void loadSyncButton() {
Button syncButton = mHeaderBinding.preferenceSyncButton;
TextView lastSyncTimeView = mHeaderBinding.prefenereceSyncStatusTextview;
Button syncButton = findViewById(R.id.preference_sync_button);
TextView lastSyncTimeView = findViewById(R.id.prefenerece_sync_status_textview);
// Google Tasks同步功能已禁用
// set button state
// if (GTaskSyncService.isSyncing()) {
// syncButton.setText(getString(R.string.preferences_button_sync_cancel));
// syncButton.setOnClickListener(new View.OnClickListener() {
// public void onClick(View v) {
// GTaskSyncService.cancelSync(NotesPreferenceActivity.this);
// }
// });
// } else {
// syncButton.setText(getString(R.string.preferences_button_sync_immediately));
// syncButton.setOnClickListener(new View.OnClickListener() {
// public void onClick(View v) {
// GTaskSyncService.startSync(NotesPreferenceActivity.this);
// }
// });
// }
// syncButton.setEnabled(!TextUtils.isEmpty(getSyncAccountName(this)));
// 禁用同步按钮
syncButton.setEnabled(false);
syncButton.setText("同步功能已禁用");
// set last sync time
// if (GTaskSyncService.isSyncing()) {
// lastSyncTimeView.setText(GTaskSyncService.getProgressString());
// lastSyncTimeView.setVisibility(View.VISIBLE);
// } else {
// long lastSyncTime = getLastSyncTime(this);
// if (lastSyncTime != 0) {
// lastSyncTimeView.setText(getString(R.string.preferences_last_sync_time,
// DateFormat.format(getString(R.string.preferences_last_sync_time_format),
// lastSyncTime)));
// lastSyncTimeView.setVisibility(View.VISIBLE);
// } else {
// lastSyncTimeView.setVisibility(View.GONE);
// }
// }
lastSyncTimeView.setText("Google Tasks同步功能已禁用");
lastSyncTimeView.setVisibility(View.VISIBLE);
}
/**
* UI
* <p>
*
* </p>
*/
private void refreshUI() {
loadAccountPreference();
loadSyncButton();
}
/**
*
* <p>
* Google
* "添加账户"
* </p>
*/
private void showSelectAccountAlertDialog() {
AlertDialog.Builder dialogBuilder = new AlertDialog.Builder(this);
View titleView = LayoutInflater.from(this).inflate(R.layout.account_dialog_title, null);
TextView titleTextView = (TextView) titleView.findViewById(R.id.account_dialog_title);
titleTextView.setText(getString(R.string.preferences_dialog_select_account_title));
TextView subtitleTextView = (TextView) titleView.findViewById(R.id.account_dialog_subtitle);
subtitleTextView.setText(getString(R.string.preferences_dialog_select_account_tips));
dialogBuilder.setCustomTitle(titleView);
dialogBuilder.setPositiveButton(null, null);
Account[] accounts = getGoogleAccounts();
String defAccount = getSyncAccountName(this);
mOriAccounts = accounts;
mHasAddedAccount = false;
if (accounts.length > 0) {
CharSequence[] items = new CharSequence[accounts.length];
final CharSequence[] itemMapping = items;
int checkedItem = -1;
int index = 0;
for (Account account : accounts) {
if (TextUtils.equals(account.name, defAccount)) {
checkedItem = index;
}
items[index++] = account.name;
}
dialogBuilder.setSingleChoiceItems(items, checkedItem,
new DialogInterface.OnClickListener() {
public void onClick(DialogInterface dialog, int which) {
setSyncAccount(itemMapping[which].toString());
dialog.dismiss();
refreshUI();
}
});
}
View addAccountView = LayoutInflater.from(this).inflate(R.layout.add_account_text, null);
dialogBuilder.setView(addAccountView);
final AlertDialog dialog = dialogBuilder.show();
addAccountView.setOnClickListener(new View.OnClickListener() {
public void onClick(View v) {
mHasAddedAccount = true;
Intent intent = new Intent("android.settings.ADD_ACCOUNT_SETTINGS");
intent.putExtra(AUTHORITIES_FILTER_KEY, new String[] {
"gmail-ls"
});
startActivityForResult(intent, -1);
dialog.dismiss();
}
});
}
/**
*
* <p>
*
* <ul>
* <li></li>
* <li></li>
* <li></li>
* </ul>
* </p>
*/
private void showChangeAccountConfirmAlertDialog() {
AlertDialog.Builder dialogBuilder = new AlertDialog.Builder(this);
View titleView = LayoutInflater.from(this).inflate(R.layout.account_dialog_title, null);
TextView titleTextView = (TextView) titleView.findViewById(R.id.account_dialog_title);
titleTextView.setText(getString(R.string.preferences_dialog_change_account_title,
getSyncAccountName(this)));
TextView subtitleTextView = (TextView) titleView.findViewById(R.id.account_dialog_subtitle);
subtitleTextView.setText(getString(R.string.preferences_dialog_change_account_warn_msg));
dialogBuilder.setCustomTitle(titleView);
CharSequence[] menuItemArray = new CharSequence[] {
getString(R.string.preferences_menu_change_account),
getString(R.string.preferences_menu_remove_account),
getString(R.string.preferences_menu_cancel)
};
dialogBuilder.setItems(menuItemArray, new DialogInterface.OnClickListener() {
public void onClick(DialogInterface dialog, int which) {
if (which == 0) {
showSelectAccountAlertDialog();
} else if (which == 1) {
removeSyncAccount();
refreshUI();
}
}
});
dialogBuilder.show();
}
/**
* Google
* <p>
* AccountManager"com.google"
* </p>
* @return Google
*/
private Account[] getGoogleAccounts() {
AccountManager accountManager = AccountManager.get(this);
return accountManager.getAccountsByType("com.google");
}
/**
*
* <p>
* SharedPreferences
* <ul>
* <li></li>
* <li>GTASK_IDSYNC_ID</li>
* </ul>
* </p>
* @param account
*/
private void setSyncAccount(String account) {
if (!getSyncAccountName(this).equals(account)) {
SharedPreferences settings = getSharedPreferences(PREFERENCE_NAME, Context.MODE_PRIVATE);
SharedPreferences.Editor editor = settings.edit();
if (account != null) {
editor.putString(PREFERENCE_SYNC_ACCOUNT_NAME, account);
} else {
editor.putString(PREFERENCE_SYNC_ACCOUNT_NAME, "");
}
editor.commit();
// clean up last sync time
setLastSyncTime(this, 0);
// clean up local gtask related info
new Thread(new Runnable() {
public void run() {
ContentValues values = new ContentValues();
values.put(NoteColumns.GTASK_ID, "");
values.put(NoteColumns.SYNC_ID, 0);
getContentResolver().update(Notes.CONTENT_NOTE_URI, values, null, null);
}
}).start();
Toast.makeText(NotesPreferenceActivity.this,
getString(R.string.preferences_toast_success_set_accout, account),
Toast.LENGTH_SHORT).show();
}
}
/**
*
* <p>
* SharedPreferences
* GTASK_IDSYNC_ID
* </p>
*/
private void removeSyncAccount() {
SharedPreferences settings = getSharedPreferences(PREFERENCE_NAME, Context.MODE_PRIVATE);
SharedPreferences.Editor editor = settings.edit();
if (settings.contains(PREFERENCE_SYNC_ACCOUNT_NAME)) {
editor.remove(PREFERENCE_SYNC_ACCOUNT_NAME);
}
if (settings.contains(PREFERENCE_LAST_SYNC_TIME)) {
editor.remove(PREFERENCE_LAST_SYNC_TIME);
}
editor.commit();
// clean up local gtask related info
new Thread(new Runnable() {
public void run() {
ContentValues values = new ContentValues();
values.put(NoteColumns.GTASK_ID, "");
values.put(NoteColumns.SYNC_ID, 0);
getContentResolver().update(Notes.CONTENT_NOTE_URI, values, null, null);
}
}).start();
}
/**
*
* <p>
* SharedPreferences
* </p>
* @param context
* @return
*/
public static String getSyncAccountName(Context context) {
SharedPreferences settings = context.getSharedPreferences(PREFERENCE_NAME,
Context.MODE_PRIVATE);
public static String getSyncAccountName(android.content.Context context) {
android.content.SharedPreferences settings = context.getSharedPreferences(PREFERENCE_NAME, android.content.Context.MODE_PRIVATE);
return settings.getString(PREFERENCE_SYNC_ACCOUNT_NAME, "");
}
/**
*
* <p>
* SharedPreferences
* </p>
* @param context
* @param time
*/
public static void setLastSyncTime(Context context, long time) {
SharedPreferences settings = context.getSharedPreferences(PREFERENCE_NAME,
Context.MODE_PRIVATE);
SharedPreferences.Editor editor = settings.edit();
editor.putLong(PREFERENCE_LAST_SYNC_TIME, time);
editor.commit();
}
/**
*
* <p>
* SharedPreferences
* </p>
* @param context
* @return 0
*/
public static long getLastSyncTime(Context context) {
SharedPreferences settings = context.getSharedPreferences(PREFERENCE_NAME,
Context.MODE_PRIVATE);
return settings.getLong(PREFERENCE_LAST_SYNC_TIME, 0);
}
/**
* 广
* <p>
* GTaskSyncService广UI
* </p>
*/
private class GTaskReceiver extends BroadcastReceiver {
/**
* 广
* <p>
* 广UI
* </p>
* @param context
* @param intent 广Intent
*/
@Override
public void onReceive(Context context, Intent intent) {
refreshUI();
// Google Tasks同步功能已禁用
// if (intent.getBooleanExtra(GTaskSyncService.GTASK_SERVICE_BROADCAST_IS_SYNCING, false)) {
// TextView syncStatus = (TextView) findViewById(R.id.prefenerece_sync_status_textview);
// syncStatus.setText(intent
// .getStringExtra(GTaskSyncService.GTASK_SERVICE_BROADCAST_PROGRESS_MSG));
// }
}
}
/**
*
* <p>
* ActionBar
*
* </p>
* @param item
* @return truefalse
*/
@Override
public boolean onOptionsItemSelected(MenuItem item) {
switch (item.getItemId()) {
case android.R.id.home:

@ -0,0 +1,117 @@
package net.micode.notes.ui;
import android.app.AlertDialog;
import android.content.Context;
import android.content.Intent;
import android.content.SharedPreferences;
import android.os.Bundle;
import android.preference.PreferenceManager;
import android.text.TextUtils;
import android.view.View;
import android.widget.Toast;
import androidx.annotation.Nullable;
import androidx.preference.Preference;
import androidx.preference.PreferenceFragmentCompat;
import net.micode.notes.R;
import net.micode.notes.tool.SecurityManager;
import net.micode.notes.data.ThemeRepository;
import androidx.preference.ListPreference;
import static android.app.Activity.RESULT_OK;
public class SettingsFragment extends PreferenceFragmentCompat {
public static final String PREFERENCE_SYNC_ACCOUNT_KEY = "pref_sync_account_key";
public static final String PREFERENCE_SECURITY_KEY = "pref_key_security";
public static final String PREFERENCE_THEME_MODE = "pref_theme_mode";
public static final int REQUEST_CODE_CHECK_PASSWORD = 104;
@Override
public void onCreatePreferences(Bundle savedInstanceState, String rootKey) {
setPreferencesFromResource(R.xml.preferences, rootKey);
loadThemePreference();
loadSecurityPreference();
loadAccountPreference();
}
private void loadThemePreference() {
ListPreference themePref = findPreference(PREFERENCE_THEME_MODE);
if (themePref != null) {
themePref.setOnPreferenceChangeListener((preference, newValue) -> {
ThemeRepository.applyTheme((String) newValue);
return true;
});
}
}
private void loadSecurityPreference() {
Preference securityPref = findPreference(PREFERENCE_SECURITY_KEY);
if (securityPref != null) {
securityPref.setOnPreferenceClickListener(new Preference.OnPreferenceClickListener() {
@Override
public boolean onPreferenceClick(Preference preference) {
if (!SecurityManager.getInstance(getActivity()).isPasswordSet()) {
showSetPasswordDialog();
} else {
Intent intent = new Intent(getActivity(), PasswordActivity.class);
intent.setAction(PasswordActivity.ACTION_CHECK_PASSWORD);
startActivityForResult(intent, REQUEST_CODE_CHECK_PASSWORD);
}
return true;
}
});
}
}
private void loadAccountPreference() {
androidx.preference.PreferenceCategory accountCategory = findPreference(PREFERENCE_SYNC_ACCOUNT_KEY);
if (accountCategory != null) {
accountCategory.removeAll();
Preference accountPref = new Preference(getContext());
accountPref.setTitle(getString(R.string.preferences_account_title));
accountPref.setSummary(getString(R.string.preferences_account_summary));
accountPref.setOnPreferenceClickListener(preference -> {
Toast.makeText(getActivity(), "Google Tasks同步功能已禁用", Toast.LENGTH_SHORT).show();
return true;
});
accountCategory.addPreference(accountPref);
}
}
private void showSetPasswordDialog() {
new AlertDialog.Builder(getActivity())
.setTitle("设置密码")
.setItems(new String[]{"数字锁", "手势锁"}, (dialog, which) -> {
int type = (which == 0) ? SecurityManager.TYPE_PIN : SecurityManager.TYPE_PATTERN;
Intent intent = new Intent(getActivity(), PasswordActivity.class);
intent.setAction(PasswordActivity.ACTION_SETUP_PASSWORD);
intent.putExtra(PasswordActivity.EXTRA_PASSWORD_TYPE, type);
startActivity(intent);
})
.show();
}
private void showManagePasswordDialog() {
new AlertDialog.Builder(getActivity())
.setTitle("管理密码")
.setItems(new String[]{"更改密码", "取消密码"}, (dialog, which) -> {
if (which == 0) { // Change
showSetPasswordDialog();
} else { // Remove
SecurityManager.getInstance(getActivity()).removePassword();
Toast.makeText(getActivity(), "密码已取消", Toast.LENGTH_SHORT).show();
}
})
.show();
}
@Override
public void onActivityResult(int requestCode, int resultCode, Intent data) {
super.onActivityResult(requestCode, resultCode, data);
if (requestCode == REQUEST_CODE_CHECK_PASSWORD && resultCode == RESULT_OK) {
showManagePasswordDialog();
}
}
}

@ -1,22 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<!-- Copyright (c) 2010-2011, The MiCode Open Source Community (www.micode.net)
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
-->
<selector xmlns:android="http://schemas.android.com/apk/res/android">
<item android:state_pressed="true" android:color="#88555555" />
<item android:state_selected="true" android:color="#ff999999" />
<item android:color="#ff000000" />
</selector>

@ -1,20 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<!-- Copyright (c) 2010-2011, The MiCode Open Source Community (www.micode.net)
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
-->
<selector xmlns:android="http://schemas.android.com/apk/res/android">
<item android:color="#50000000" />
</selector>

@ -0,0 +1,9 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:fillColor="#FF000000"
android:pathData="M15.6,11.79c0.97,-0.67 1.65,-1.77 1.65,-2.79 0,-2.26 -1.75,-4 -4,-4H7v14h7c2.09,0 3.85,-1.75 3.85,-3.75 0,-1.58 -0.95,-2.9 -2.25,-3.46zM10,7.5h3c0.83,0 1.5,0.67 1.5,1.5s-0.67,1.5 -1.5,1.5h-3v-3zM13.5,16.5h-3.5v-3h3.5c0.83,0 1.5,0.67 1.5,1.5s-0.67,1.5 -1.5,1.5z"/>
</vector>

@ -0,0 +1,9 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:fillColor="#FF000000"
android:pathData="M9.4,16.6L4.8,12l4.6,-4.6L8,6l-6,6 6,6 1.4,-1.4zM14.6,16.6l4.6,-4.6 -4.6,-4.6L16,6l6,6 -6,6 -1.4,-1.4z"/>
</vector>

@ -0,0 +1,12 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:fillColor="#FF000000"
android:pathData="M16.56,8.94L7.62,0 6.21,1.41l2.38,2.38 -5.15,5.15c-0.59,0.59 -0.59,1.54 0,2.12l5.5,5.5c0.29,0.29 0.68,0.44 1.06,0.44s0.77,-0.15 1.06,-0.44l5.5,-5.5c0.59,-0.58 0.59,-1.53 0,-2.12zM5.21,10L10,5.21 14.79,10H5.21zM19,11.5s-2,2.17 -2,3.5c0,1.1 0.9,2 2,2s2,-0.9 2,-2c0,-1.33 -2,-3.5 -2,-3.5z"/>
<path
android:fillColor="#FF000000"
android:pathData="M0,20h24v4H0z"/>
</vector>

@ -0,0 +1,12 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:fillColor="#FF000000"
android:pathData="M0,20h24v4H0z"/>
<path
android:fillColor="#FF000000"
android:pathData="M11,3L5.5,17h2.25l1.12,-3h6.25l1.12,3h2.25L13,3h-2zM10,11l2,-5.5 2,5.5h-4z"/>
</vector>

@ -0,0 +1,9 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:fillColor="#FF000000"
android:pathData="M5,4v3h5.5v12h3V7H19V4z"/>
</vector>

@ -0,0 +1,9 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:fillColor="#FF000000"
android:pathData="M10,4v3h2.21l-3.42,8H6v3h8v-3h-2.21l3.42,-8H18V4z"/>
</vector>

@ -0,0 +1,9 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:fillColor="#FF000000"
android:pathData="M4,10.5c-0.83,0 -1.5,0.67 -1.5,1.5s0.67,1.5 1.5,1.5 1.5,-0.67 1.5,-1.5 -0.67,-1.5 -1.5,-1.5zM4,4.5c-0.83,0 -1.5,0.67 -1.5,1.5s0.67,1.5 1.5,1.5 1.5,-0.67 1.5,-1.5 -0.67,-1.5 -1.5,-1.5zM4,16.5c-0.83,0 -1.5,0.67 -1.5,1.5s0.67,1.5 1.5,1.5 1.5,-0.67 1.5,-1.5 -0.67,-1.5 -1.5,-1.5zM8,19h12v-2H8v2zM8,13h12v-2H8v2zM8,5v2h12V5H8z"/>
</vector>

@ -0,0 +1,9 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:fillColor="#FF000000"
android:pathData="M6,17h3l2,-4V7H5v6h3l-2,4zM14,17h3l2,-4V7h-6v6h3l-2,4z"/>
</vector>

@ -0,0 +1,9 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:fillColor="#FF000000"
android:pathData="M10,19h4v-3h-4v3zM5,4v3h5v3h4V7h5V4H5zM3,14h18v-2H3v2z"/>
</vector>

@ -0,0 +1,9 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:fillColor="#FF000000"
android:pathData="M12,17c3.31,0 6,-2.69 6,-6V3h-2.5v8c0,1.93 -1.57,3.5 -3.5,3.5S8.5,12.93 8.5,11V3H6v8c0,3.31 2.69,6 6,6zM5,19v2h14v-2H5z"/>
</vector>

@ -0,0 +1,10 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24"
android:tint="#000000">
<path
android:fillColor="@android:color/white"
android:pathData="M21,19V5c0,-1.1 -0.9,-2 -2,-2H5c-1.1,0 -2,0.9 -2,2v14c0,1.1 0.9,2 2,2h14c1.1,0 2,-0.9 2,-2zM8.5,13.5l2.5,3.01L14.5,12l4.5,6H5l3.5,-4.5z"/>
</vector>

@ -0,0 +1,9 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:fillColor="#FF000000"
android:pathData="M19,13H5v-2h14v2z"/>
</vector>

@ -0,0 +1,9 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:fillColor="#FF000000"
android:pathData="M3.9,12c0,-1.71 1.39,-3.1 3.1,-3.1h4V7H7c-2.76,0 -5,2.24 -5,5s2.24,5 5,5h4v-1.9H7c-1.71,0 -3.1,-1.39 -3.1,-3.1zM8,13h8v-2H8v2zM17,7h-4v1.9h4c1.71,0 3.1,1.39 3.1,3.1s-1.39,3.1 -3.1,3.1h-4V17h4c2.76,0 5,-2.24 5,-5s-2.24,-5 -5,-5z"/>
</vector>

@ -0,0 +1,9 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24.0"
android:viewportHeight="24.0">
<path
android:fillColor="#FFFFFFFF"
android:pathData="M18.4,10.6C16.55,8.99 14.15,8 11.5,8c-4.65,0 -8.58,3.03 -9.96,7.22L3.9,16c1.05,-3.19 4.05,-5.5 7.6,-5.5 1.95,0 3.73,0.72 5.12,1.88L13,16h9V7l-3.6,3.6z"/>
</vector>

@ -0,0 +1,9 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:fillColor="#FF000000"
android:pathData="M2.5,4v3h5v12h3V7h5V4H2.5zM21.5,9h-9v3h3v7h3v-7h3V9z"/>
</vector>

@ -0,0 +1,9 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24.0"
android:viewportHeight="24.0">
<path
android:fillColor="#FFFFFFFF"
android:pathData="M12.5,8c-2.65,0 -5.05,0.99 -6.9,2.6L2,7v9h9l-3.62,-3.62c1.39,-1.16 3.16,-1.88 5.12,-1.88 3.54,0 6.55,2.31 7.6,5.5l2.37,-0.78C21.08,11.03 17.15,8 12.5,8z"/>
</vector>

@ -0,0 +1,10 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24"
android:tint="#000000">
<path
android:fillColor="@android:color/white"
android:pathData="M12,3c-4.97,0 -9,4.03 -9,9s4.03,9 9,9c0.83,0 1.5,-0.67 1.5,-1.5 0,-0.39 -0.15,-0.74 -0.39,-1.01 -0.23,-0.26 -0.38,-0.61 -0.38,-0.99 0,-0.83 0.67,-1.5 1.5,-1.5H16c2.76,0 5,-2.24 5,-5 0,-4.42 -4.03,-8 -9,-8zM6.5,12c-0.83,0 -1.5,-0.67 -1.5,-1.5S5.67,9 6.5,9 8,9.67 8,10.5 7.33,12 6.5,12zM9.5,6c-0.83,0 -1.5,-0.67 -1.5,-1.5S8.67,3 9.5,3s1.5,0.67 1.5,1.5S10.33,6 9.5,6zM14.5,6c-0.83,0 -1.5,-0.67 -1.5,-1.5S13.67,3 14.5,3s1.5,0.67 1.5,1.5S15.33,6 14.5,6zM17.5,12c-0.83,0 -1.5,-0.67 -1.5,-1.5S16.67,9 17.5,9s1.5,0.67 1.5,1.5S18.33,12 17.5,12z"/>
</vector>

@ -0,0 +1,68 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.coordinatorlayout.widget.CoordinatorLayout
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="match_parent"
android:fitsSystemWindows="true">
<com.google.android.material.appbar.AppBarLayout
android:id="@+id/appbar"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:theme="@style/ThemeOverlay.Material3.ActionBar">
<androidx.appcompat.widget.Toolbar
android:id="@+id/toolbar"
android:layout_width="match_parent"
android:layout_height="?attr/actionBarSize"
android:background="?attr/colorPrimary"
app:popupTheme="@style/ThemeOverlay.Material3.Light">
<!-- 搜索框 -->
<androidx.appcompat.widget.SearchView
android:id="@+id/search_view"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:imeOptions="actionSearch|flagNoExtractUi"
app:iconifiedByDefault="false"
app:queryHint="@string/search_hint" />
</androidx.appcompat.widget.Toolbar>
<!-- 历史记录按钮 -->
<TextView
android:id="@+id/btn_show_history"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="?android:attr/selectableItemBackground"
android:padding="16dp"
android:text="@string/search_history_title"
android:textColor="?android:attr/textColorPrimary"
android:textSize="16sp"
android:drawablePadding="8dp"
android:visibility="gone"
app:drawableEndCompat="@android:drawable/arrow_down_float" />
</com.google.android.material.appbar.AppBarLayout>
<!-- 搜索结果列表 -->
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/recycler_view"
android:layout_width="match_parent"
android:layout_height="match_parent"
app:layout_behavior="@string/appbar_scrolling_view_behavior"
android:scrollbars="vertical" />
<!-- 无结果提示 -->
<TextView
android:id="@+id/tv_no_result"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:text="@string/search_no_results"
android:visibility="gone"
android:textSize="16sp"
android:textColor="?android:attr/textColorSecondary" />
</androidx.coordinatorlayout.widget.CoordinatorLayout>

@ -0,0 +1,15 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical">
<include layout="@layout/settings_header" />
<androidx.fragment.app.FragmentContainerView
android:id="@+id/settings_container"
android:layout_width="match_parent"
android:layout_height="0dp"
android:layout_weight="1" />
</LinearLayout>

@ -0,0 +1,69 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
android:padding="24dp">
<com.google.android.material.card.MaterialCardView
android:layout_width="match_parent"
android:layout_height="100dp"
android:layout_marginBottom="24dp"
app:cardCornerRadius="12dp"
app:strokeWidth="1dp"
app:strokeColor="?attr/colorOutline"
xmlns:app="http://schemas.android.com/apk/res-auto">
<View
android:id="@+id/view_color_preview"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="@android:color/white" />
</com.google.android.material.card.MaterialCardView>
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Red"
android:layout_marginBottom="4dp"/>
<SeekBar
android:id="@+id/sb_red"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginBottom="16dp"
android:progressTint="@android:color/holo_red_light"
android:thumbTint="@android:color/holo_red_dark"
android:max="255" />
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Green"
android:layout_marginBottom="4dp"/>
<SeekBar
android:id="@+id/sb_green"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginBottom="16dp"
android:progressTint="@android:color/holo_green_light"
android:thumbTint="@android:color/holo_green_dark"
android:max="255" />
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Blue"
android:layout_marginBottom="4dp"/>
<SeekBar
android:id="@+id/sb_blue"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:progressTint="@android:color/holo_blue_light"
android:thumbTint="@android:color/holo_blue_dark"
android:max="255" />
</LinearLayout>

@ -0,0 +1,26 @@
<?xml version="1.0" encoding="utf-8"?>
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="60dp"
android:layout_height="60dp"
android:layout_margin="4dp"
android:padding="2dp">
<com.google.android.material.imageview.ShapeableImageView
android:id="@+id/color_view"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:scaleType="centerCrop"
app:shapeAppearanceOverlay="@style/ShapeAppearance.Material3.Corner.Full"
app:strokeWidth="1dp"
app:strokeColor="@color/secondary_text_dark" />
<ImageView
android:id="@+id/check_view"
android:layout_width="24dp"
android:layout_height="24dp"
android:layout_gravity="center"
android:src="@drawable/selected"
android:visibility="gone" />
</FrameLayout>

@ -170,112 +170,16 @@
android:layout_marginTop="30dp"
android:layout_marginRight="8dp"
android:layout_gravity="top|right"
android:visibility="gone">
<FrameLayout
android:layout_width="0dp"
android:layout_height="match_parent"
android:layout_weight="1">
<ImageView
android:id="@+id/iv_bg_yellow"
android:layout_width="match_parent"
android:layout_height="match_parent" />
<ImageView
android:id="@+id/iv_bg_yellow_select"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="bottom|right"
android:layout_marginRight="5dp"
android:focusable="false"
android:visibility="gone"
android:src="@drawable/selected" />
</FrameLayout>
<FrameLayout
android:layout_width="0dp"
android:layout_height="match_parent"
android:layout_weight="1">
<ImageView
android:id="@+id/iv_bg_blue"
android:layout_width="match_parent"
android:layout_height="match_parent" />
<ImageView
android:id="@+id/iv_bg_blue_select"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="bottom|right"
android:focusable="false"
android:visibility="gone"
android:layout_marginRight="3dp"
android:src="@drawable/selected" />
</FrameLayout>
<FrameLayout
android:layout_width="0dp"
android:layout_height="match_parent"
android:layout_weight="1">
<ImageView
android:id="@+id/iv_bg_white"
android:layout_width="match_parent"
android:layout_height="match_parent" />
<ImageView
android:id="@+id/iv_bg_white_select"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="bottom|right"
android:focusable="false"
android:visibility="gone"
android:layout_marginRight="2dp"
android:src="@drawable/selected" />
</FrameLayout>
android:visibility="gone"
android:orientation="horizontal">
<FrameLayout
android:layout_width="0dp"
android:layout_height="match_parent"
android:layout_weight="1">
<ImageView
android:id="@+id/iv_bg_green"
android:layout_width="match_parent"
android:layout_height="match_parent" />
<ImageView
android:id="@+id/iv_bg_green_select"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="bottom|right"
android:focusable="false"
android:visibility="gone"
android:layout_marginRight="5dp"
android:src="@drawable/selected" />
</FrameLayout>
<FrameLayout
android:layout_width="0dp"
android:layout_height="match_parent"
android:layout_weight="1">
<ImageView
android:id="@+id/iv_bg_red"
android:layout_width="match_parent"
android:layout_height="match_parent" />
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/rv_bg_color_selector"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:orientation="horizontal"
app:layoutManager="androidx.recyclerview.widget.LinearLayoutManager" />
<ImageView
android:id="@+id/iv_bg_red_select"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="bottom|right"
android:focusable="false"
android:visibility="gone"
android:layout_marginRight="3dp"
android:src="@drawable/selected" />
</FrameLayout>
</LinearLayout>
<LinearLayout
@ -438,5 +342,155 @@
android:src="@drawable/selected" />
</FrameLayout>
</LinearLayout>
<LinearLayout
android:id="@+id/rich_text_selector"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="@drawable/font_size_selector_bg"
android:layout_gravity="bottom"
android:visibility="gone"
android:orientation="vertical">
<HorizontalScrollView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:scrollbars="none">
<LinearLayout
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:orientation="horizontal"
android:padding="8dp">
<ImageButton
android:id="@+id/btn_bold"
android:layout_width="40dp"
android:layout_height="40dp"
android:src="@drawable/ic_format_bold"
android:background="?android:attr/selectableItemBackground"
android:contentDescription="Bold"
android:scaleType="centerInside" />
<ImageButton
android:id="@+id/btn_italic"
android:layout_width="40dp"
android:layout_height="40dp"
android:src="@drawable/ic_format_italic"
android:background="?android:attr/selectableItemBackground"
android:contentDescription="Italic"
android:scaleType="centerInside" />
<ImageButton
android:id="@+id/btn_underline"
android:layout_width="40dp"
android:layout_height="40dp"
android:src="@drawable/ic_format_underline"
android:background="?android:attr/selectableItemBackground"
android:contentDescription="Underline"
android:scaleType="centerInside" />
<ImageButton
android:id="@+id/btn_strikethrough"
android:layout_width="40dp"
android:layout_height="40dp"
android:src="@drawable/ic_format_strikethrough"
android:background="?android:attr/selectableItemBackground"
android:contentDescription="Strikethrough"
android:scaleType="centerInside" />
<View
android:layout_width="1dp"
android:layout_height="match_parent"
android:layout_marginHorizontal="4dp"
android:background="#CCCCCC"/>
<ImageButton
android:id="@+id/btn_header"
android:layout_width="40dp"
android:layout_height="40dp"
android:src="@drawable/ic_format_header"
android:background="?android:attr/selectableItemBackground"
android:contentDescription="Header"
android:scaleType="centerInside" />
<ImageButton
android:id="@+id/btn_list"
android:layout_width="40dp"
android:layout_height="40dp"
android:src="@drawable/ic_format_list_bulleted"
android:background="?android:attr/selectableItemBackground"
android:contentDescription="List"
android:scaleType="centerInside" />
<ImageButton
android:id="@+id/btn_quote"
android:layout_width="40dp"
android:layout_height="40dp"
android:src="@drawable/ic_format_quote"
android:background="?android:attr/selectableItemBackground"
android:contentDescription="Quote"
android:scaleType="centerInside" />
<ImageButton
android:id="@+id/btn_code"
android:layout_width="40dp"
android:layout_height="40dp"
android:src="@drawable/ic_format_code"
android:background="?android:attr/selectableItemBackground"
android:contentDescription="Code"
android:scaleType="centerInside" />
<View
android:layout_width="1dp"
android:layout_height="match_parent"
android:layout_marginHorizontal="4dp"
android:background="#CCCCCC"/>
<ImageButton
android:id="@+id/btn_link"
android:layout_width="40dp"
android:layout_height="40dp"
android:src="@drawable/ic_insert_link"
android:background="?android:attr/selectableItemBackground"
android:contentDescription="Link"
android:scaleType="centerInside" />
<ImageButton
android:id="@+id/btn_divider"
android:layout_width="40dp"
android:layout_height="40dp"
android:src="@drawable/ic_insert_divider"
android:background="?android:attr/selectableItemBackground"
android:contentDescription="Divider"
android:scaleType="centerInside" />
<View
android:layout_width="1dp"
android:layout_height="match_parent"
android:layout_marginHorizontal="4dp"
android:background="#CCCCCC"/>
<ImageButton
android:id="@+id/btn_color_text"
android:layout_width="40dp"
android:layout_height="40dp"
android:src="@drawable/ic_format_color_text"
android:background="?android:attr/selectableItemBackground"
android:contentDescription="Text Color"
android:scaleType="centerInside" />
<ImageButton
android:id="@+id/btn_color_fill"
android:layout_width="40dp"
android:layout_height="40dp"
android:src="@drawable/ic_format_color_fill"
android:background="?android:attr/selectableItemBackground"
android:contentDescription="Background Color"
android:scaleType="centerInside" />
</LinearLayout>
</HorizontalScrollView>
</LinearLayout>
</LinearLayout>
</LinearLayout>

@ -22,7 +22,7 @@
android:id="@+id/drawer_layout"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="@drawable/list_background">
android:background="@color/background_color">
<!-- 主内容区域 -->
<androidx.coordinatorlayout.widget.CoordinatorLayout
@ -89,6 +89,7 @@
android:layout_gravity="bottom|end"
android:layout_margin="16dp"
android:contentDescription="@string/notelist_menu_new"
app:backgroundTint="@color/fab_color"
app:srcCompat="@android:drawable/ic_input_add" />
</androidx.coordinatorlayout.widget.CoordinatorLayout>

@ -0,0 +1,35 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal"
android:padding="16dp"
android:gravity="center_vertical"
android:background="?android:attr/selectableItemBackground">
<ImageView
android:layout_width="24dp"
android:layout_height="24dp"
android:src="@android:drawable/ic_menu_recent_history"
android:tint="?android:attr/textColorSecondary"
android:contentDescription="@null" />
<TextView
android:id="@+id/tv_history_keyword"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:layout_marginStart="16dp"
android:textSize="16sp"
android:textColor="?android:attr/textColorPrimary" />
<ImageView
android:id="@+id/iv_delete_history"
android:layout_width="24dp"
android:layout_height="24dp"
android:padding="4dp"
android:src="@android:drawable/ic_menu_close_clear_cancel"
android:tint="?android:attr/textColorSecondary"
android:contentDescription="@string/menu_delete" />
</LinearLayout>

@ -16,12 +16,35 @@
-->
<menu
xmlns:android="http://schemas.android.com/apk/res/android">
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto">
<item
android:id="@+id/menu_new_note"
android:title="@string/notelist_menu_new"/>
<item
android:id="@+id/menu_rich_text"
android:title="@string/menu_rich_text"
android:icon="@drawable/ic_menu_rich_text"
app:showAsAction="always"/>
<item
android:id="@+id/menu_undo"
android:title="@string/menu_undo"
android:icon="@drawable/ic_menu_undo"
app:showAsAction="always"/>
<item
android:id="@+id/menu_redo"
android:title="@string/menu_redo"
android:icon="@drawable/ic_menu_redo"
app:showAsAction="always"/>
<item
android:id="@+id/menu_clear_history"
android:title="@string/menu_clear_history"/>
<item
android:id="@+id/menu_delete"
android:title="@string/menu_delete"/>

@ -0,0 +1,15 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<color name="background_color">#121212</color>
<color name="text_color_primary">#DEFFFFFF</color>
<color name="text_color_secondary">#99FFFFFF</color>
<!-- In Night mode, keeping note backgrounds desaturated/dark is better,
but for now we keep them linked to original resources unless we want to override them.
We override bg_white to be dark grey for general UI usage (if any). -->
<color name="bg_white">#2C2C2C</color>
<!-- Overriding primary_text_dark to ensure legacy styles also switch if they are not updated -->
<color name="primary_text_dark">#DEFFFFFF</color>
<color name="secondary_text_dark">#99FFFFFF</color>
</resources>

@ -133,4 +133,11 @@
<string name="menu_trash">回收站</string>
<string name="create_folder_success">创建文件夹成功</string>
<string name="menu_undo">撤回</string>
<string name="menu_redo">重做</string>
<string name="menu_clear_history">清空撤回历史</string>
<string name="undo_success">撤回成功</string>
<string name="redo_success">重做成功</string>
<string name="undo_fail">无可撤回</string>
<string name="redo_fail">无可重做</string>
</resources>

@ -28,4 +28,16 @@
<item>Messaging</item>
<item>Email</item>
</string-array>
<string-array name="theme_entries">
<item>Follow System</item>
<item>Light</item>
<item>Dark</item>
</string-array>
<string-array name="theme_values">
<item>system</item>
<item>light</item>
<item>dark</item>
</string-array>
</resources>

@ -17,7 +17,27 @@
<resources>
<color name="user_query_highlight">#335b5b5b</color>
<color name="primary_color">#1976D2</color>
<color name="primary_color">#263238</color>
<color name="on_primary_color">#FFFFFF</color>
<color name="background_color">#FAFAFA</color>
<color name="background_color">#E8E8E8</color>
<color name="primary_text_dark">#000000</color>
<color name="secondary_text_dark">#808080</color>
<color name="fab_color">#FFC107</color>
<!-- Semantic Text Colors -->
<color name="text_color_primary">#000000</color>
<color name="text_color_secondary">#808080</color>
<!-- Note Background Colors -->
<color name="bg_yellow">#FFF9C4</color>
<color name="bg_blue">#B3E5FC</color>
<color name="bg_white">#FFFFFF</color>
<color name="bg_green">#C8E6C9</color>
<color name="bg_red">#FFCDD2</color>
<!-- New Presets -->
<color name="bg_midnight_black">#212121</color>
<color name="bg_eye_care_green">#C7EDCC</color>
<color name="bg_warm">#FFE0B2</color>
<color name="bg_cool">#E1BEE7</color>
</resources>

@ -0,0 +1,17 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<string-array name="font_family_entries">
<item>System Default</item>
<item>Serif</item>
<item>Sans Serif</item>
<item>Monospace</item>
<item>Cursive</item>
</string-array>
<string-array name="font_family_values">
<item>default</item>
<item>serif</item>
<item>sans-serif</item>
<item>monospace</item>
<item>cursive</item>
</string-array>
</resources>

@ -161,4 +161,19 @@
<string name="delete_confirmation">Are you sure you want to delete selected notes?</string>
<string name="menu_unpin">Unpin</string>
<string name="menu_unlock">Unlock</string>
<!-- Search related -->
<string name="search_no_results">No results found</string>
<string name="search_history_title">Search History</string>
<string name="search_history_clear">Clear</string>
<!-- Undo/Redo -->
<string name="menu_undo">Undo</string>
<string name="menu_redo">Redo</string>
<string name="menu_clear_history">Clear Undo History</string>
<string name="undo_success">Undo successful</string>
<string name="redo_success">Redo successful</string>
<string name="undo_fail">Nothing to undo</string>
<string name="redo_fail">Nothing to redo</string>
<string name="menu_rich_text">Rich Text</string>
</resources>

@ -35,27 +35,27 @@
<style name="TextAppearancePrimaryItem">
<item name="android:textSize">@dimen/text_font_size_normal</item>
<item name="android:textColor">@color/primary_text_dark</item>
<item name="android:textColor">@color/text_color_primary</item>
</style>
<style name="TextAppearanceSecondaryItem">
<item name="android:textSize">@dimen/text_font_size_small</item>
<item name="android:textColor">@color/secondary_text_dark</item>
<item name="android:textColor">@color/text_color_secondary</item>
</style>
<style name="TextAppearanceUnderMenuIcon">
<item name="android:textSize">@dimen/text_font_size_normal</item>
<item name="android:textColor">@android:color/black</item>
<item name="android:textColor">@color/text_color_primary</item>
</style>
<style name="HighlightTextAppearancePrimary">
<item name="android:textSize">@dimen/text_font_size_normal</item>
<item name="android:textColor">@color/primary_text_dark</item>
<item name="android:textColor">@color/text_color_primary</item>
</style>
<style name="HighlightTextAppearanceSecondary">
<item name="android:textSize">@dimen/text_font_size_small</item>
<item name="android:textColor">@color/secondary_text_dark</item>
<item name="android:textColor">@color/text_color_secondary</item>
</style>
<style name="NoteTheme" parent="@android:style/Theme.Holo.Light">

@ -8,14 +8,19 @@
<item name="android:navigationBarColor">@android:color/transparent</item>
<!-- 根据内容自动调整状态栏图标颜色(深色背景=浅色图标) -->
<item name="android:windowLightStatusBar">false</item>
<!-- Preference Theme for SettingsFragment -->
<item name="preferenceTheme">@style/PreferenceThemeOverlay</item>
</style>
<style name="Theme.Notesmaster" parent="Base.Theme.Notesmaster" />
<!-- NoteEditActivity使用的主题统一使用Material3风格 -->
<style name="Theme.Notesmaster.Edit" parent="Theme.Material3.Light.NoActionBar">
<style name="Theme.Notesmaster.Edit" parent="Theme.Material3.DayNight.NoActionBar">
<item name="colorPrimary">@color/primary_color</item>
<item name="colorOnPrimary">@color/on_primary_color</item>
<item name="android:statusBarColor">@color/primary_color</item>
<!-- 确保背景色适配深色模式,虽然会被便签背景覆盖 -->
<item name="android:colorBackground">@color/background_color</item>
<item name="android:windowBackground">@color/background_color</item>
</style>
</resources>

@ -21,6 +21,24 @@
android:key="pref_sync_account_key">
</PreferenceCategory>
<PreferenceCategory android:title="Appearance">
<ListPreference
android:key="pref_theme_mode"
android:title="Theme"
android:summary="%s"
android:entries="@array/theme_entries"
android:entryValues="@array/theme_values"
android:defaultValue="system" />
<ListPreference
android:key="pref_font_family"
android:title="Font Family"
android:summary="%s"
android:entries="@array/font_family_entries"
android:entryValues="@array/font_family_values"
android:defaultValue="default" />
</PreferenceCategory>
<PreferenceCategory>
<CheckBoxPreference
android:key="pref_key_bg_random_appear"

Loading…
Cancel
Save