Compare commits
17 Commits
2a59a2aa01
...
aff9ac63d1
| Author | SHA1 | Date |
|---|---|---|
|
|
aff9ac63d1 | 3 months ago |
|
|
4c3e4cae68 | 3 months ago |
|
|
795bee106a | 3 months ago |
|
|
7922ccee55 | 3 months ago |
|
|
573add61f1 | 3 months ago |
|
|
b2a4fbb5ed | 3 months ago |
|
|
0089ea3f22 | 3 months ago |
|
|
c871709b17 | 3 months ago |
|
|
dce63cb3f1 | 3 months ago |
|
|
0eb9a179f6 | 3 months ago |
|
|
3277d60c33 | 3 months ago |
|
|
d1dd9362cd | 3 months ago |
|
|
269d287c71 | 3 months ago |
|
|
19721c364f | 3 months ago |
|
|
2e86cf6334 | 3 months ago |
|
|
9ffb066211 | 3 months ago |
|
|
64f7651f2d | 3 months ago |
Binary file not shown.
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -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,137 @@
|
||||
package net.micode.notes.model;
|
||||
|
||||
import android.content.ContentUris;
|
||||
import android.content.ContentValues;
|
||||
import android.content.Context;
|
||||
import android.database.Cursor;
|
||||
import android.net.Uri;
|
||||
import android.util.Log;
|
||||
|
||||
import net.micode.notes.data.Notes;
|
||||
import net.micode.notes.data.Notes.DataColumns;
|
||||
import net.micode.notes.data.Notes.NoteColumns;
|
||||
import net.micode.notes.data.Notes.TextNote;
|
||||
|
||||
public class Task {
|
||||
private static final String TAG = "Task";
|
||||
|
||||
public long id;
|
||||
public String snippet; // Content
|
||||
public long createdDate;
|
||||
public long modifiedDate;
|
||||
public int priority; // 0=Low, 1=Mid, 2=High
|
||||
public long dueDate;
|
||||
public int status; // 0=Active, 1=Completed
|
||||
public long finishedTime;
|
||||
public long alertDate;
|
||||
|
||||
public static final int PRIORITY_LOW = 0;
|
||||
public static final int PRIORITY_NORMAL = 1;
|
||||
public static final int PRIORITY_HIGH = 2;
|
||||
|
||||
public static final int STATUS_ACTIVE = 0;
|
||||
public static final int STATUS_COMPLETED = 1;
|
||||
|
||||
public Task() {
|
||||
id = 0;
|
||||
snippet = "";
|
||||
createdDate = System.currentTimeMillis();
|
||||
modifiedDate = System.currentTimeMillis();
|
||||
priority = PRIORITY_LOW;
|
||||
dueDate = 0;
|
||||
status = STATUS_ACTIVE;
|
||||
finishedTime = 0;
|
||||
alertDate = 0;
|
||||
}
|
||||
|
||||
public static Task fromCursor(Cursor cursor) {
|
||||
Task task = new Task();
|
||||
|
||||
// Use getColumnIndex instead of getColumnIndexOrThrow for safety
|
||||
int idxId = cursor.getColumnIndex(NoteColumns.ID);
|
||||
if (idxId != -1) task.id = cursor.getLong(idxId);
|
||||
|
||||
int idxSnippet = cursor.getColumnIndex(NoteColumns.SNIPPET);
|
||||
if (idxSnippet != -1) task.snippet = cursor.getString(idxSnippet);
|
||||
|
||||
int idxCreated = cursor.getColumnIndex(NoteColumns.CREATED_DATE);
|
||||
if (idxCreated != -1) task.createdDate = cursor.getLong(idxCreated);
|
||||
|
||||
int idxModified = cursor.getColumnIndex(NoteColumns.MODIFIED_DATE);
|
||||
if (idxModified != -1) task.modifiedDate = cursor.getLong(idxModified);
|
||||
|
||||
int idxAlert = cursor.getColumnIndex(NoteColumns.ALERTED_DATE);
|
||||
if (idxAlert != -1) task.alertDate = cursor.getLong(idxAlert);
|
||||
|
||||
int idxPriority = cursor.getColumnIndex(NoteColumns.GTASK_PRIORITY);
|
||||
if (idxPriority != -1) task.priority = cursor.getInt(idxPriority);
|
||||
|
||||
int idxDueDate = cursor.getColumnIndex(NoteColumns.GTASK_DUE_DATE);
|
||||
if (idxDueDate != -1) task.dueDate = cursor.getLong(idxDueDate);
|
||||
|
||||
int idxStatus = cursor.getColumnIndex(NoteColumns.GTASK_STATUS);
|
||||
if (idxStatus != -1) task.status = cursor.getInt(idxStatus);
|
||||
|
||||
int idxFinished = cursor.getColumnIndex(NoteColumns.GTASK_FINISHED_TIME);
|
||||
if (idxFinished != -1) task.finishedTime = cursor.getLong(idxFinished);
|
||||
|
||||
return task;
|
||||
}
|
||||
|
||||
public Uri save(Context context) {
|
||||
ContentValues values = new ContentValues();
|
||||
values.put(NoteColumns.TYPE, Notes.TYPE_TASK);
|
||||
values.put(NoteColumns.MODIFIED_DATE, System.currentTimeMillis());
|
||||
values.put(NoteColumns.ALERTED_DATE, alertDate);
|
||||
values.put(NoteColumns.GTASK_PRIORITY, priority);
|
||||
values.put(NoteColumns.GTASK_DUE_DATE, dueDate);
|
||||
values.put(NoteColumns.GTASK_STATUS, status);
|
||||
values.put(NoteColumns.GTASK_FINISHED_TIME, finishedTime);
|
||||
values.put(NoteColumns.LOCAL_MODIFIED, 1);
|
||||
|
||||
// Ensure snippet is updated in note table too, though trigger might handle it,
|
||||
// explicit update is safer if trigger fails or data table logic changes.
|
||||
values.put(NoteColumns.SNIPPET, snippet);
|
||||
|
||||
if (id == 0) {
|
||||
values.put(NoteColumns.CREATED_DATE, System.currentTimeMillis());
|
||||
values.put(NoteColumns.PARENT_ID, Notes.ID_ROOT_FOLDER);
|
||||
Uri uri = context.getContentResolver().insert(Notes.CONTENT_NOTE_URI, values);
|
||||
if (uri != null) {
|
||||
id = ContentUris.parseId(uri);
|
||||
updateData(context);
|
||||
}
|
||||
return uri;
|
||||
} else {
|
||||
context.getContentResolver().update(
|
||||
ContentUris.withAppendedId(Notes.CONTENT_NOTE_URI, id),
|
||||
values, null, null
|
||||
);
|
||||
updateData(context);
|
||||
return ContentUris.withAppendedId(Notes.CONTENT_NOTE_URI, id);
|
||||
}
|
||||
}
|
||||
|
||||
private void updateData(Context context) {
|
||||
ContentValues values = new ContentValues();
|
||||
values.put(DataColumns.NOTE_ID, id);
|
||||
values.put(DataColumns.MIME_TYPE, TextNote.CONTENT_ITEM_TYPE);
|
||||
values.put(DataColumns.CONTENT, snippet);
|
||||
values.put(DataColumns.MODIFIED_DATE, System.currentTimeMillis());
|
||||
|
||||
Cursor c = context.getContentResolver().query(Notes.CONTENT_DATA_URI, new String[]{DataColumns.ID},
|
||||
DataColumns.NOTE_ID + "=?", new String[]{String.valueOf(id)}, null);
|
||||
|
||||
if (c != null) {
|
||||
if (c.moveToFirst()) {
|
||||
long dataId = c.getLong(0);
|
||||
context.getContentResolver().update(ContentUris.withAppendedId(Notes.CONTENT_DATA_URI, dataId), values, null, null);
|
||||
} else {
|
||||
context.getContentResolver().insert(Notes.CONTENT_DATA_URI, values);
|
||||
}
|
||||
c.close();
|
||||
} else {
|
||||
context.getContentResolver().insert(Notes.CONTENT_DATA_URI, values);
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -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();
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,416 @@
|
||||
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;
|
||||
|
||||
import android.graphics.drawable.Drawable;
|
||||
import android.net.Uri;
|
||||
import android.graphics.Bitmap;
|
||||
import android.graphics.BitmapFactory;
|
||||
import android.graphics.drawable.BitmapDrawable;
|
||||
import android.util.Log;
|
||||
|
||||
public class RichTextHelper {
|
||||
|
||||
private static final String TAG = "RichTextHelper";
|
||||
|
||||
public static class NoteImageGetter implements Html.ImageGetter {
|
||||
private Context mContext;
|
||||
|
||||
public NoteImageGetter(Context context) {
|
||||
mContext = context;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Drawable getDrawable(String source) {
|
||||
if (TextUtils.isEmpty(source)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
try {
|
||||
Uri uri = Uri.parse(source);
|
||||
String path = uri.getPath();
|
||||
if (path == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// Parse dimensions from fragment
|
||||
int targetWidth = -1;
|
||||
int targetHeight = -1;
|
||||
String fragment = uri.getFragment();
|
||||
if (fragment != null) {
|
||||
String[] params = fragment.split("&");
|
||||
for (String param : params) {
|
||||
String[] pair = param.split("=");
|
||||
if (pair.length == 2) {
|
||||
if ("w".equals(pair[0])) {
|
||||
targetWidth = Integer.parseInt(pair[1]);
|
||||
} else if ("h".equals(pair[0])) {
|
||||
targetHeight = Integer.parseInt(pair[1]);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Check if it's a content URI or file path
|
||||
// For simplicity in this project, we assume we saved it as file:// path
|
||||
// But source might come as /data/user/0/...
|
||||
|
||||
// Decode bitmap with resizing
|
||||
// Calculate max width (e.g., screen width - padding)
|
||||
// For simplicity, let's assume a fixed max width or display metrics
|
||||
int maxWidth = mContext.getResources().getDisplayMetrics().widthPixels - 40;
|
||||
|
||||
Bitmap bitmap = decodeSampledBitmapFromFile(path, maxWidth, maxWidth);
|
||||
|
||||
if (bitmap != null) {
|
||||
BitmapDrawable drawable = new BitmapDrawable(mContext.getResources(), bitmap);
|
||||
|
||||
if (targetWidth > 0 && targetHeight > 0) {
|
||||
// Use saved dimensions
|
||||
drawable.setBounds(0, 0, targetWidth, targetHeight);
|
||||
} else {
|
||||
// Use default intrinsic dimensions
|
||||
drawable.setBounds(0, 0, drawable.getIntrinsicWidth(), drawable.getIntrinsicHeight());
|
||||
}
|
||||
return drawable;
|
||||
}
|
||||
} catch (Exception e) {
|
||||
Log.e(TAG, "Failed to load image: " + source, e);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
private Bitmap decodeSampledBitmapFromFile(String pathName, int reqWidth, int reqHeight) {
|
||||
final BitmapFactory.Options options = new BitmapFactory.Options();
|
||||
options.inJustDecodeBounds = true;
|
||||
BitmapFactory.decodeFile(pathName, options);
|
||||
|
||||
options.inSampleSize = calculateInSampleSize(options, reqWidth, reqHeight);
|
||||
|
||||
options.inJustDecodeBounds = false;
|
||||
return BitmapFactory.decodeFile(pathName, options);
|
||||
}
|
||||
|
||||
private int calculateInSampleSize(BitmapFactory.Options options, int reqWidth, int reqHeight) {
|
||||
final int height = options.outHeight;
|
||||
final int width = options.outWidth;
|
||||
int inSampleSize = 1;
|
||||
|
||||
if (height > reqHeight || width > reqWidth) {
|
||||
final int halfHeight = height / 2;
|
||||
final int halfWidth = width / 2;
|
||||
|
||||
while ((halfHeight / inSampleSize) >= reqHeight && (halfWidth / inSampleSize) >= reqWidth) {
|
||||
inSampleSize *= 2;
|
||||
}
|
||||
}
|
||||
return inSampleSize;
|
||||
}
|
||||
}
|
||||
|
||||
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 void insertImage(EditText editText, String imagePath) {
|
||||
// Remove existing fragment if any
|
||||
if (imagePath.contains("#")) {
|
||||
imagePath = imagePath.substring(0, imagePath.indexOf("#"));
|
||||
}
|
||||
// Default: no size specified, use intrinsic
|
||||
String html = "<img src=\"" + imagePath + "\">";
|
||||
int start = editText.getSelectionStart();
|
||||
int end = editText.getSelectionEnd();
|
||||
if (start > end) { int temp = start; start = end; end = temp; }
|
||||
|
||||
Spanned spanned = Html.fromHtml(html, new NoteImageGetter(editText.getContext()), null);
|
||||
editText.getText().replace(start, end, spanned);
|
||||
// Insert a newline after image for easier typing
|
||||
editText.getText().insert(start + spanned.length(), "\n");
|
||||
}
|
||||
|
||||
public static void updateImageSpanSize(EditText editText, android.text.style.ImageSpan span, int width, int height) {
|
||||
Editable editable = editText.getText();
|
||||
int start = editable.getSpanStart(span);
|
||||
int end = editable.getSpanEnd(span);
|
||||
if (start < 0 || end < 0) return; // Span not found
|
||||
|
||||
String source = span.getSource();
|
||||
if (source == null) return;
|
||||
|
||||
// Remove old fragment
|
||||
if (source.contains("#")) {
|
||||
source = source.substring(0, source.indexOf("#"));
|
||||
}
|
||||
|
||||
// Append new dimensions
|
||||
String newSource = source + "#w=" + width + "&h=" + height;
|
||||
String html = "<img src=\"" + newSource + "\">";
|
||||
|
||||
// Create new span with updated source
|
||||
Spanned newSpanned = Html.fromHtml(html, new NoteImageGetter(editText.getContext()), null);
|
||||
|
||||
// We only want the ImageSpan, not the whole Spanned (which might contain newline if insertImage added it, but fromHtml for img usually just one char)
|
||||
// Actually fromHtml returns a Spanned with ImageSpan on a special character.
|
||||
// We can just replace the old span range with new one.
|
||||
|
||||
// Careful: replacing text might reset other spans or move cursor.
|
||||
// Better: get the new ImageSpan from newSpanned and set it on the existing range, removing the old one.
|
||||
android.text.style.ImageSpan[] newSpans = newSpanned.getSpans(0, newSpanned.length(), android.text.style.ImageSpan.class);
|
||||
if (newSpans.length > 0) {
|
||||
editable.removeSpan(span);
|
||||
editable.setSpan(newSpans[0], start, end, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
|
||||
}
|
||||
}
|
||||
|
||||
public static String toHtml(Spanned text) {
|
||||
return Html.toHtml(text);
|
||||
}
|
||||
|
||||
public static Spanned fromHtml(String html, Context context) {
|
||||
return Html.fromHtml(html, new NoteImageGetter(context), null);
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -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,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();
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,205 @@
|
||||
package net.micode.notes.ui;
|
||||
|
||||
import android.app.AlertDialog;
|
||||
import android.app.DatePickerDialog;
|
||||
import android.app.TimePickerDialog;
|
||||
import android.content.Intent;
|
||||
import android.database.Cursor;
|
||||
import android.os.Bundle;
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.View;
|
||||
import android.widget.Button;
|
||||
import android.widget.EditText;
|
||||
import android.widget.ImageView;
|
||||
import android.widget.RadioGroup;
|
||||
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.model.Task;
|
||||
|
||||
import java.util.Calendar;
|
||||
|
||||
public class TaskEditActivity extends AppCompatActivity {
|
||||
|
||||
private EditText contentEdit;
|
||||
private ImageView alarmBtn;
|
||||
private ImageView tagBtn;
|
||||
private Button doneBtn;
|
||||
|
||||
private Task task;
|
||||
private long taskId;
|
||||
|
||||
@Override
|
||||
protected void onCreate(Bundle savedInstanceState) {
|
||||
super.onCreate(savedInstanceState);
|
||||
setContentView(R.layout.activity_task_edit);
|
||||
|
||||
contentEdit = findViewById(R.id.task_edit_content);
|
||||
alarmBtn = findViewById(R.id.btn_alarm);
|
||||
tagBtn = findViewById(R.id.btn_tag);
|
||||
doneBtn = findViewById(R.id.btn_done);
|
||||
|
||||
Intent intent = getIntent();
|
||||
taskId = intent.getLongExtra(Intent.EXTRA_UID, 0);
|
||||
|
||||
if (taskId > 0) {
|
||||
loadTask();
|
||||
} else {
|
||||
task = new Task();
|
||||
}
|
||||
|
||||
setupListeners();
|
||||
}
|
||||
|
||||
private void loadTask() {
|
||||
new Thread(() -> {
|
||||
Cursor cursor = getContentResolver().query(
|
||||
Notes.CONTENT_NOTE_URI,
|
||||
null,
|
||||
NoteColumns.ID + "=?",
|
||||
new String[]{String.valueOf(taskId)},
|
||||
null
|
||||
);
|
||||
|
||||
if (cursor != null) {
|
||||
if (cursor.moveToFirst()) {
|
||||
task = Task.fromCursor(cursor);
|
||||
runOnUiThread(() -> {
|
||||
contentEdit.setText(task.snippet);
|
||||
contentEdit.setSelection(task.snippet.length());
|
||||
});
|
||||
} else {
|
||||
task = new Task();
|
||||
}
|
||||
cursor.close();
|
||||
} else {
|
||||
task = new Task();
|
||||
}
|
||||
}).start();
|
||||
}
|
||||
|
||||
private void setupListeners() {
|
||||
doneBtn.setOnClickListener(v -> {
|
||||
saveTask();
|
||||
setResult(RESULT_OK);
|
||||
finish();
|
||||
});
|
||||
|
||||
alarmBtn.setOnClickListener(v -> {
|
||||
showAlarmDialog();
|
||||
});
|
||||
|
||||
tagBtn.setOnClickListener(v -> {
|
||||
showTagDialog();
|
||||
});
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onBackPressed() {
|
||||
if (saveTask()) {
|
||||
setResult(RESULT_OK);
|
||||
}
|
||||
super.onBackPressed();
|
||||
}
|
||||
|
||||
private boolean saveTask() {
|
||||
String content = contentEdit.getText().toString();
|
||||
if (content.trim().length() == 0) {
|
||||
if (task.id == 0) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
task.snippet = content;
|
||||
task.save(this);
|
||||
|
||||
// Register Alarm if needed.
|
||||
if (task.alertDate > 0 && task.alertDate > System.currentTimeMillis()) {
|
||||
Intent intent = new Intent(this, AlarmReceiver.class);
|
||||
intent.setData(android.content.ContentUris.withAppendedId(Notes.CONTENT_NOTE_URI, task.id));
|
||||
android.app.PendingIntent pendingIntent = android.app.PendingIntent.getBroadcast(this, 0, intent, android.app.PendingIntent.FLAG_UPDATE_CURRENT | android.app.PendingIntent.FLAG_IMMUTABLE);
|
||||
android.app.AlarmManager alarmManager = (android.app.AlarmManager) getSystemService(ALARM_SERVICE);
|
||||
alarmManager.set(android.app.AlarmManager.RTC_WAKEUP, task.alertDate, pendingIntent);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
private void showAlarmDialog() {
|
||||
final Calendar c = Calendar.getInstance();
|
||||
if (task.alertDate > 0) {
|
||||
c.setTimeInMillis(task.alertDate);
|
||||
}
|
||||
|
||||
new DatePickerDialog(this, (view, year, month, dayOfMonth) -> {
|
||||
c.set(Calendar.YEAR, year);
|
||||
c.set(Calendar.MONTH, month);
|
||||
c.set(Calendar.DAY_OF_MONTH, dayOfMonth);
|
||||
|
||||
new TimePickerDialog(this, (view1, hourOfDay, minute) -> {
|
||||
c.set(Calendar.HOUR_OF_DAY, hourOfDay);
|
||||
c.set(Calendar.MINUTE, minute);
|
||||
c.set(Calendar.SECOND, 0);
|
||||
|
||||
task.alertDate = c.getTimeInMillis();
|
||||
Toast.makeText(this, "Alarm set", Toast.LENGTH_SHORT).show();
|
||||
|
||||
}, c.get(Calendar.HOUR_OF_DAY), c.get(Calendar.MINUTE), true).show();
|
||||
|
||||
}, c.get(Calendar.YEAR), c.get(Calendar.MONTH), c.get(Calendar.DAY_OF_MONTH)).show();
|
||||
}
|
||||
|
||||
private void showTagDialog() {
|
||||
View view = LayoutInflater.from(this).inflate(R.layout.dialog_task_tag, null);
|
||||
|
||||
RadioGroup priorityGroup = view.findViewById(R.id.priority_group);
|
||||
TextView dateText = view.findViewById(R.id.date_text);
|
||||
Button dateBtn = view.findViewById(R.id.btn_set_date);
|
||||
|
||||
if (task.priority == Task.PRIORITY_HIGH) priorityGroup.check(R.id.priority_high);
|
||||
else if (task.priority == Task.PRIORITY_NORMAL) priorityGroup.check(R.id.priority_mid);
|
||||
else priorityGroup.check(R.id.priority_low);
|
||||
|
||||
final Calendar c = Calendar.getInstance();
|
||||
if (task.dueDate > 0) {
|
||||
c.setTimeInMillis(task.dueDate);
|
||||
dateText.setText(android.text.format.DateFormat.format("yyyy-MM-dd HH:mm", c));
|
||||
} else {
|
||||
dateText.setText("No Due Date");
|
||||
}
|
||||
|
||||
dateBtn.setOnClickListener(v -> {
|
||||
new DatePickerDialog(this, (dView, year, month, dayOfMonth) -> {
|
||||
c.set(Calendar.YEAR, year);
|
||||
c.set(Calendar.MONTH, month);
|
||||
c.set(Calendar.DAY_OF_MONTH, dayOfMonth);
|
||||
|
||||
new TimePickerDialog(this, (tView, hourOfDay, minute) -> {
|
||||
c.set(Calendar.HOUR_OF_DAY, hourOfDay);
|
||||
c.set(Calendar.MINUTE, minute);
|
||||
|
||||
task.dueDate = c.getTimeInMillis();
|
||||
dateText.setText(android.text.format.DateFormat.format("yyyy-MM-dd HH:mm", c));
|
||||
|
||||
}, c.get(Calendar.HOUR_OF_DAY), c.get(Calendar.MINUTE), true).show();
|
||||
|
||||
}, c.get(Calendar.YEAR), c.get(Calendar.MONTH), c.get(Calendar.DAY_OF_MONTH)).show();
|
||||
});
|
||||
|
||||
new AlertDialog.Builder(this)
|
||||
.setTitle("Set Tag")
|
||||
.setView(view)
|
||||
.setPositiveButton("OK", (dialog, which) -> {
|
||||
int id = priorityGroup.getCheckedRadioButtonId();
|
||||
if (id == R.id.priority_high) task.priority = Task.PRIORITY_HIGH;
|
||||
else if (id == R.id.priority_mid) task.priority = Task.PRIORITY_NORMAL;
|
||||
else task.priority = Task.PRIORITY_LOW;
|
||||
})
|
||||
.setNegativeButton("Cancel", null)
|
||||
.show();
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,156 @@
|
||||
package net.micode.notes.ui;
|
||||
|
||||
import android.app.Activity;
|
||||
import android.content.Intent;
|
||||
import android.database.Cursor;
|
||||
import android.os.Bundle;
|
||||
import android.view.MenuItem;
|
||||
import android.view.View;
|
||||
|
||||
import androidx.appcompat.app.AppCompatActivity;
|
||||
import androidx.appcompat.widget.Toolbar;
|
||||
import androidx.recyclerview.widget.LinearLayoutManager;
|
||||
import androidx.recyclerview.widget.RecyclerView;
|
||||
|
||||
import com.google.android.material.floatingactionbutton.FloatingActionButton;
|
||||
|
||||
import net.micode.notes.R;
|
||||
import net.micode.notes.data.Notes;
|
||||
import net.micode.notes.data.Notes.NoteColumns;
|
||||
import net.micode.notes.model.Task;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
public class TaskListActivity extends AppCompatActivity implements TaskListAdapter.OnTaskItemClickListener {
|
||||
|
||||
private RecyclerView recyclerView;
|
||||
private TaskListAdapter adapter;
|
||||
private FloatingActionButton fab;
|
||||
private static final int REQUEST_EDIT_TASK = 1001;
|
||||
|
||||
@Override
|
||||
protected void onCreate(Bundle savedInstanceState) {
|
||||
super.onCreate(savedInstanceState);
|
||||
setContentView(R.layout.activity_task_list);
|
||||
|
||||
// Initialize Toolbar
|
||||
Toolbar toolbar = findViewById(R.id.toolbar);
|
||||
setSupportActionBar(toolbar);
|
||||
|
||||
// Remove default title
|
||||
if (getSupportActionBar() != null) {
|
||||
getSupportActionBar().setDisplayShowTitleEnabled(false);
|
||||
getSupportActionBar().setDisplayHomeAsUpEnabled(false);
|
||||
}
|
||||
|
||||
// Setup custom "Notes" navigation
|
||||
View notesTitle = findViewById(R.id.tv_toolbar_title_notes);
|
||||
if (notesTitle != null) {
|
||||
notesTitle.setOnClickListener(v -> {
|
||||
finish();
|
||||
overridePendingTransition(R.anim.slide_in_left, R.anim.slide_out_right);
|
||||
});
|
||||
}
|
||||
|
||||
// Setup Navigation Icon (Back Button) - REMOVED as per new requirement
|
||||
// toolbar.setNavigationOnClickListener(v -> {
|
||||
// finish();
|
||||
// overridePendingTransition(R.anim.slide_in_left, R.anim.slide_out_right);
|
||||
// });
|
||||
|
||||
recyclerView = findViewById(R.id.task_list_view);
|
||||
recyclerView.setLayoutManager(new LinearLayoutManager(this));
|
||||
adapter = new TaskListAdapter(this, this);
|
||||
recyclerView.setAdapter(adapter);
|
||||
|
||||
fab = findViewById(R.id.btn_new_task);
|
||||
fab.setOnClickListener(v -> {
|
||||
Intent intent = new Intent(TaskListActivity.this, TaskEditActivity.class);
|
||||
startActivityForResult(intent, REQUEST_EDIT_TASK);
|
||||
});
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onResume() {
|
||||
super.onResume();
|
||||
loadTasks();
|
||||
}
|
||||
|
||||
private void loadTasks() {
|
||||
new Thread(() -> {
|
||||
Cursor cursor = getContentResolver().query(
|
||||
Notes.CONTENT_NOTE_URI,
|
||||
null,
|
||||
NoteColumns.TYPE + "=?",
|
||||
new String[]{String.valueOf(Notes.TYPE_TASK)},
|
||||
null
|
||||
);
|
||||
|
||||
List<Task> tasks = new ArrayList<>();
|
||||
if (cursor != null) {
|
||||
while (cursor.moveToNext()) {
|
||||
tasks.add(Task.fromCursor(cursor));
|
||||
}
|
||||
cursor.close();
|
||||
}
|
||||
|
||||
android.util.Log.d("TaskListActivity", "Loaded tasks count: " + tasks.size());
|
||||
|
||||
runOnUiThread(() -> adapter.setTasks(tasks));
|
||||
}).start();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onItemClick(Task task) {
|
||||
Intent intent = new Intent(this, TaskEditActivity.class);
|
||||
intent.putExtra(Intent.EXTRA_UID, task.id);
|
||||
startActivityForResult(intent, REQUEST_EDIT_TASK);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onCheckBoxClick(Task task) {
|
||||
task.status = (task.status == Task.STATUS_ACTIVE) ? Task.STATUS_COMPLETED : Task.STATUS_ACTIVE;
|
||||
if (task.status == Task.STATUS_COMPLETED) {
|
||||
task.finishedTime = System.currentTimeMillis();
|
||||
} else {
|
||||
task.finishedTime = 0;
|
||||
}
|
||||
|
||||
new Thread(() -> {
|
||||
task.save(this);
|
||||
runOnUiThread(() -> loadTasks()); // Reload to sort
|
||||
}).start();
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onCreateOptionsMenu(android.view.Menu menu) {
|
||||
// Removed menu_notes as per requirement, keeping empty or future menus
|
||||
// getMenuInflater().inflate(R.menu.task_list, menu);
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onOptionsItemSelected(MenuItem item) {
|
||||
if (item.getItemId() == android.R.id.home) {
|
||||
finish();
|
||||
overridePendingTransition(R.anim.slide_in_left, R.anim.slide_out_right);
|
||||
return true;
|
||||
}
|
||||
return super.onOptionsItemSelected(item);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onBackPressed() {
|
||||
super.onBackPressed();
|
||||
overridePendingTransition(R.anim.slide_in_left, R.anim.slide_out_right);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onActivityResult(int requestCode, int resultCode, Intent data) {
|
||||
super.onActivityResult(requestCode, resultCode, data);
|
||||
if (requestCode == REQUEST_EDIT_TASK && resultCode == RESULT_OK) {
|
||||
loadTasks();
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,168 @@
|
||||
package net.micode.notes.ui;
|
||||
|
||||
import android.content.Context;
|
||||
import android.graphics.Paint;
|
||||
import android.text.format.DateFormat;
|
||||
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.model.Task;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.Comparator;
|
||||
import java.util.List;
|
||||
|
||||
public class TaskListAdapter extends RecyclerView.Adapter<TaskListAdapter.TaskViewHolder> {
|
||||
|
||||
private List<Task> tasks = new ArrayList<>();
|
||||
private Context context;
|
||||
private OnTaskItemClickListener listener;
|
||||
|
||||
public interface OnTaskItemClickListener {
|
||||
void onItemClick(Task task);
|
||||
void onCheckBoxClick(Task task);
|
||||
}
|
||||
|
||||
public TaskListAdapter(Context context, OnTaskItemClickListener listener) {
|
||||
this.context = context;
|
||||
this.listener = listener;
|
||||
}
|
||||
|
||||
public void setTasks(List<Task> newTasks) {
|
||||
this.tasks = new ArrayList<>(newTasks);
|
||||
sortTasks();
|
||||
notifyDataSetChanged();
|
||||
}
|
||||
|
||||
private void sortTasks() {
|
||||
Collections.sort(tasks, new Comparator<Task>() {
|
||||
@Override
|
||||
public int compare(Task t1, Task t2) {
|
||||
// 1. Status: Active (0) < Completed (1)
|
||||
if (t1.status != t2.status) {
|
||||
return Integer.compare(t1.status, t2.status);
|
||||
}
|
||||
|
||||
// 2. If both Active
|
||||
if (t1.status == Task.STATUS_ACTIVE) {
|
||||
// Priority: High (2) > Mid (1) > Low (0) -> DESC
|
||||
if (t1.priority != t2.priority) {
|
||||
return Integer.compare(t2.priority, t1.priority);
|
||||
}
|
||||
|
||||
if (t1.dueDate != t2.dueDate) {
|
||||
if (t1.dueDate == 0) return 1; // t1 no date -> bottom
|
||||
if (t2.dueDate == 0) return -1; // t2 no date -> bottom
|
||||
return Long.compare(t1.dueDate, t2.dueDate); // Early date first
|
||||
}
|
||||
|
||||
// Creation Date (Fallback)
|
||||
return Long.compare(t2.createdDate, t1.createdDate);
|
||||
}
|
||||
|
||||
// 3. If both Completed
|
||||
// DESC sort by finishedTime.
|
||||
return Long.compare(t2.finishedTime, t1.finishedTime);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@NonNull
|
||||
@Override
|
||||
public TaskViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
|
||||
View view = LayoutInflater.from(context).inflate(R.layout.task_list_item, parent, false);
|
||||
return new TaskViewHolder(view);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onBindViewHolder(@NonNull TaskViewHolder holder, int position) {
|
||||
Task task = tasks.get(position);
|
||||
holder.bind(task);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getItemCount() {
|
||||
return tasks.size();
|
||||
}
|
||||
|
||||
class TaskViewHolder extends RecyclerView.ViewHolder {
|
||||
CheckBox checkBox;
|
||||
TextView content;
|
||||
TextView priority;
|
||||
TextView date;
|
||||
ImageView alarm;
|
||||
|
||||
public TaskViewHolder(@NonNull View itemView) {
|
||||
super(itemView);
|
||||
checkBox = itemView.findViewById(R.id.task_checkbox);
|
||||
content = itemView.findViewById(R.id.task_content);
|
||||
priority = itemView.findViewById(R.id.task_priority);
|
||||
date = itemView.findViewById(R.id.task_date);
|
||||
alarm = itemView.findViewById(R.id.task_alarm_icon);
|
||||
|
||||
itemView.setOnClickListener(v -> {
|
||||
if (getAdapterPosition() != RecyclerView.NO_POSITION) {
|
||||
listener.onItemClick(tasks.get(getAdapterPosition()));
|
||||
}
|
||||
});
|
||||
|
||||
checkBox.setOnClickListener(v -> {
|
||||
if (getAdapterPosition() != RecyclerView.NO_POSITION) {
|
||||
listener.onCheckBoxClick(tasks.get(getAdapterPosition()));
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
public void bind(Task task) {
|
||||
content.setText(task.snippet);
|
||||
checkBox.setChecked(task.status == Task.STATUS_COMPLETED);
|
||||
|
||||
if (task.status == Task.STATUS_COMPLETED) {
|
||||
content.setPaintFlags(content.getPaintFlags() | Paint.STRIKE_THRU_TEXT_FLAG);
|
||||
content.setAlpha(0.5f);
|
||||
} else {
|
||||
content.setPaintFlags(content.getPaintFlags() & (~Paint.STRIKE_THRU_TEXT_FLAG));
|
||||
content.setAlpha(1.0f);
|
||||
}
|
||||
|
||||
// Priority
|
||||
if (task.priority == Task.PRIORITY_HIGH) {
|
||||
priority.setVisibility(View.VISIBLE);
|
||||
priority.setText("HIGH");
|
||||
priority.setBackgroundColor(0xFFFFCDD2); // Light Red
|
||||
priority.setTextColor(0xFFB71C1C); // Dark Red
|
||||
} else if (task.priority == Task.PRIORITY_NORMAL) {
|
||||
priority.setVisibility(View.VISIBLE);
|
||||
priority.setText("MED");
|
||||
priority.setBackgroundColor(0xFFFFF9C4); // Light Yellow
|
||||
priority.setTextColor(0xFFF57F17); // Dark Yellow
|
||||
} else {
|
||||
priority.setVisibility(View.GONE);
|
||||
}
|
||||
|
||||
// Due Date
|
||||
if (task.dueDate > 0) {
|
||||
date.setVisibility(View.VISIBLE);
|
||||
date.setText(DateFormat.format("MM/dd HH:mm", task.dueDate));
|
||||
} else {
|
||||
date.setVisibility(View.GONE);
|
||||
}
|
||||
|
||||
// Alarm
|
||||
if (task.alertDate > 0) {
|
||||
alarm.setVisibility(View.VISIBLE);
|
||||
} else {
|
||||
alarm.setVisibility(View.GONE);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,8 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<set xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<translate
|
||||
android:duration="@android:integer/config_mediumAnimTime"
|
||||
android:fromXDelta="-100%"
|
||||
android:toXDelta="0%"
|
||||
android:interpolator="@android:anim/decelerate_interpolator"/>
|
||||
</set>
|
||||
@ -0,0 +1,8 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<set xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<translate
|
||||
android:duration="@android:integer/config_mediumAnimTime"
|
||||
android:fromXDelta="100%"
|
||||
android:toXDelta="0%"
|
||||
android:interpolator="@android:anim/decelerate_interpolator"/>
|
||||
</set>
|
||||
@ -0,0 +1,8 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<set xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<translate
|
||||
android:duration="@android:integer/config_mediumAnimTime"
|
||||
android:fromXDelta="0%"
|
||||
android:toXDelta="-100%"
|
||||
android:interpolator="@android:anim/accelerate_interpolator"/>
|
||||
</set>
|
||||
@ -0,0 +1,8 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<set xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<translate
|
||||
android:duration="@android:integer/config_mediumAnimTime"
|
||||
android:fromXDelta="0%"
|
||||
android:toXDelta="100%"
|
||||
android:interpolator="@android:anim/accelerate_interpolator"/>
|
||||
</set>
|
||||
@ -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="#FF000000"
|
||||
android:pathData="M14,2H6c-1.1,0 -1.99,0.9 -1.99,2L4,20c0,1.1 0.89,2 1.99,2H18c1.1,0 2,-0.9 2,-2V8l-6,-6zM16,18H8v-2h8v2zM16,14H8v-2h8v2zM13,9V3.5L18.5,9H13z"/>
|
||||
</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="#FF000000"
|
||||
android:pathData="M19,3h-4.18C14.4,1.84 13.3,1 12,1c-1.3,0 -2.4,0.84 -2.82,2H5c-1.1,0 -2,0.9 -2,2v14c0,1.1 0.9,2 2,2h14c1.1,0 2,-0.9 2,-2V5c0,-1.1 -0.9,-2 -2,-2zM12,3c0.55,0 1,0.45 1,1s-0.45,1 -1,1 -1,-0.45 -1,-1 0.45,-1 1,-1zM14,17H7v-2h7v2zM17,13H7v-2h10v2zM17,9H7V7h10v2z"/>
|
||||
</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,60 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:background="@color/white">
|
||||
|
||||
<LinearLayout
|
||||
android:id="@+id/bottom_bar"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="56dp"
|
||||
android:layout_alignParentBottom="true"
|
||||
android:orientation="horizontal"
|
||||
android:gravity="center_vertical"
|
||||
android:background="#F0F0F0"
|
||||
android:paddingStart="16dp"
|
||||
android:paddingEnd="16dp">
|
||||
|
||||
<ImageView
|
||||
android:id="@+id/btn_alarm"
|
||||
android:layout_width="32dp"
|
||||
android:layout_height="32dp"
|
||||
android:src="@android:drawable/ic_lock_idle_alarm"
|
||||
android:layout_marginEnd="24dp"
|
||||
android:clickable="true"
|
||||
android:background="?android:attr/selectableItemBackgroundBorderless"/>
|
||||
|
||||
<ImageView
|
||||
android:id="@+id/btn_tag"
|
||||
android:layout_width="32dp"
|
||||
android:layout_height="32dp"
|
||||
android:src="@drawable/ic_menu_rich_text"
|
||||
android:layout_marginEnd="24dp"
|
||||
android:clickable="true"
|
||||
android:background="?android:attr/selectableItemBackgroundBorderless"/>
|
||||
|
||||
<View
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="0dp"
|
||||
android:layout_weight="1" />
|
||||
|
||||
<Button
|
||||
android:id="@+id/btn_done"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="Done" />
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
<EditText
|
||||
android:id="@+id/task_edit_content"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:layout_above="@id/bottom_bar"
|
||||
android:gravity="top|start"
|
||||
android:padding="16dp"
|
||||
android:background="@null"
|
||||
android:hint="Enter task..."
|
||||
android:textSize="18sp" />
|
||||
|
||||
</RelativeLayout>
|
||||
@ -0,0 +1,61 @@
|
||||
<?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:background="@color/background_color">
|
||||
|
||||
<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"
|
||||
android:fitsSystemWindows="true">
|
||||
|
||||
<com.google.android.material.appbar.MaterialToolbar
|
||||
android:id="@+id/toolbar"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="?attr/actionBarSize"
|
||||
android:background="?attr/colorPrimary"
|
||||
app:contentInsetStart="0dp"
|
||||
app:contentInsetStartWithNavigation="0dp">
|
||||
|
||||
<TextView
|
||||
android:id="@+id/tv_toolbar_title_notes"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="Notes"
|
||||
android:textSize="20sp"
|
||||
android:textStyle="bold"
|
||||
android:textColor="?android:attr/textColorPrimary"
|
||||
android:layout_gravity="start|center_vertical"
|
||||
android:paddingStart="16dp"
|
||||
android:paddingEnd="16dp"
|
||||
android:background="?android:attr/selectableItemBackground"
|
||||
android:clickable="true"
|
||||
android:focusable="true" />
|
||||
|
||||
</com.google.android.material.appbar.MaterialToolbar>
|
||||
|
||||
</com.google.android.material.appbar.AppBarLayout>
|
||||
|
||||
<androidx.recyclerview.widget.RecyclerView
|
||||
android:id="@+id/task_list_view"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:clipToPadding="false"
|
||||
android:paddingBottom="80dp"
|
||||
app:layout_behavior="@string/appbar_scrolling_view_behavior"/>
|
||||
|
||||
<com.google.android.material.floatingactionbutton.FloatingActionButton
|
||||
android:id="@+id/btn_new_task"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_gravity="bottom|end"
|
||||
android:layout_margin="16dp"
|
||||
android:src="@drawable/ic_add"
|
||||
app:backgroundTint="@color/fab_color"
|
||||
app:tint="@color/text_color_primary"
|
||||
android:contentDescription="New Task" />
|
||||
|
||||
</androidx.coordinatorlayout.widget.CoordinatorLayout>
|
||||
@ -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,71 @@
|
||||
<?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="16dp">
|
||||
|
||||
<TextView
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="Priority"
|
||||
android:textStyle="bold"
|
||||
android:layout_marginBottom="8dp"/>
|
||||
|
||||
<RadioGroup
|
||||
android:id="@+id/priority_group"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="horizontal">
|
||||
|
||||
<RadioButton
|
||||
android:id="@+id/priority_low"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="Low"
|
||||
android:layout_weight="1"/>
|
||||
|
||||
<RadioButton
|
||||
android:id="@+id/priority_mid"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="Mid"
|
||||
android:layout_weight="1"/>
|
||||
|
||||
<RadioButton
|
||||
android:id="@+id/priority_high"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="High"
|
||||
android:layout_weight="1"/>
|
||||
</RadioGroup>
|
||||
|
||||
<TextView
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="Due Date"
|
||||
android:textStyle="bold"
|
||||
android:layout_marginTop="16dp"
|
||||
android:layout_marginBottom="8dp"/>
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="horizontal"
|
||||
android:gravity="center_vertical">
|
||||
|
||||
<TextView
|
||||
android:id="@+id/date_text"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_weight="1"
|
||||
android:text="No Due Date" />
|
||||
|
||||
<Button
|
||||
android:id="@+id/btn_set_date"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="Set" />
|
||||
</LinearLayout>
|
||||
|
||||
</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>
|
||||
@ -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>
|
||||
@ -0,0 +1,70 @@
|
||||
<?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="12dp"
|
||||
android:gravity="center_vertical"
|
||||
android:background="@color/bg_white"
|
||||
android:layout_marginBottom="1dp">
|
||||
|
||||
<CheckBox
|
||||
android:id="@+id/task_checkbox"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content" />
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_weight="1"
|
||||
android:orientation="vertical"
|
||||
android:paddingStart="8dp"
|
||||
android:paddingEnd="8dp">
|
||||
|
||||
<TextView
|
||||
android:id="@+id/task_content"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:textSize="16sp"
|
||||
android:textColor="@color/text_color_primary"
|
||||
android:ellipsize="end"
|
||||
android:maxLines="2" />
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="horizontal"
|
||||
android:layout_marginTop="4dp"
|
||||
android:gravity="center_vertical">
|
||||
|
||||
<TextView
|
||||
android:id="@+id/task_priority"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:textSize="12sp"
|
||||
android:layout_marginEnd="8dp"
|
||||
android:paddingStart="4dp"
|
||||
android:paddingEnd="4dp"
|
||||
android:background="#FFCDD2"
|
||||
android:textColor="#B71C1C"
|
||||
android:text="HIGH"
|
||||
android:visibility="gone"/>
|
||||
|
||||
<TextView
|
||||
android:id="@+id/task_date"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:textSize="12sp"
|
||||
android:textColor="@color/text_color_secondary" />
|
||||
</LinearLayout>
|
||||
</LinearLayout>
|
||||
|
||||
<ImageView
|
||||
android:id="@+id/task_alarm_icon"
|
||||
android:layout_width="16dp"
|
||||
android:layout_height="16dp"
|
||||
android:src="@android:drawable/ic_lock_idle_alarm"
|
||||
android:tint="@color/text_color_secondary"
|
||||
android:visibility="gone" />
|
||||
|
||||
</LinearLayout>
|
||||
@ -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>
|
||||
@ -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>
|
||||
Loading…
Reference in new issue