Compare commits
25 Commits
jiangtianx
...
master
| Author | SHA1 | Date |
|---|---|---|
|
|
ae90690fd9 | 2 months ago |
|
|
c0aa8be65c | 2 months ago |
|
|
1edd837ed9 | 2 months ago |
|
|
f01729f8f8 | 2 months ago |
|
|
86d39b7a95 | 2 months ago |
|
|
50a82e52bb | 2 months ago |
|
|
394354710b | 2 months ago |
|
|
16e249516a | 2 months ago |
|
|
f8c785f348 | 2 months ago |
|
|
574573cad8 | 2 months ago |
|
|
48344838f1 | 2 months ago |
|
|
a2dc685b07 | 2 months ago |
|
|
87b5daed82 | 2 months ago |
|
|
1b7d196fe6 | 2 months ago |
|
|
70197d0faa | 2 months ago |
|
|
802f828318 | 2 months ago |
|
|
2e7dc5d079 | 2 months ago |
|
|
aff9ac63d1 | 2 months ago |
|
|
4c3e4cae68 | 2 months ago |
|
|
795bee106a | 2 months ago |
|
|
573add61f1 | 2 months ago |
|
|
2a59a2aa01 | 2 months ago |
|
|
b2a4fbb5ed | 2 months ago |
|
|
c871709b17 | 2 months ago |
|
|
0eb9a179f6 | 2 months ago |
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@ -1,6 +0,0 @@
|
||||
{
|
||||
"name": "git",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {}
|
||||
}
|
||||
@ -0,0 +1,3 @@
|
||||
# Default ignored files
|
||||
/shelf/
|
||||
/workspace.xml
|
||||
@ -0,0 +1,6 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="ProjectRootManager">
|
||||
<output url="file://$PROJECT_DIR$/out" />
|
||||
</component>
|
||||
</project>
|
||||
@ -0,0 +1,8 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="ProjectModuleManager">
|
||||
<modules>
|
||||
<module fileurl="file://$PROJECT_DIR$/.idea/src.iml" filepath="$PROJECT_DIR$/.idea/src.iml" />
|
||||
</modules>
|
||||
</component>
|
||||
</project>
|
||||
@ -0,0 +1,6 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="VcsDirectoryMappings">
|
||||
<mapping directory="$PROJECT_DIR$/.." vcs="Git" />
|
||||
</component>
|
||||
</project>
|
||||
@ -0,0 +1,28 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<projectDescription>
|
||||
<name>Notes-master-Notesmaster</name>
|
||||
<comment>Project Notesmaster created by Buildship.</comment>
|
||||
<projects>
|
||||
</projects>
|
||||
<buildSpec>
|
||||
<buildCommand>
|
||||
<name>org.eclipse.buildship.core.gradleprojectbuilder</name>
|
||||
<arguments>
|
||||
</arguments>
|
||||
</buildCommand>
|
||||
</buildSpec>
|
||||
<natures>
|
||||
<nature>org.eclipse.buildship.core.gradleprojectnature</nature>
|
||||
</natures>
|
||||
<filteredResources>
|
||||
<filter>
|
||||
<id>1769218588724</id>
|
||||
<name></name>
|
||||
<type>30</type>
|
||||
<matcher>
|
||||
<id>org.eclipse.core.resources.regexFilterMatcher</id>
|
||||
<arguments>node_modules|\.git|__CREATED_BY_JAVA_LANGUAGE_SERVER__</arguments>
|
||||
</matcher>
|
||||
</filter>
|
||||
</filteredResources>
|
||||
</projectDescription>
|
||||
@ -0,0 +1,6 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<classpath>
|
||||
<classpathentry kind="con" path="org.eclipse.jdt.launching.JRE_CONTAINER/org.eclipse.jdt.internal.debug.ui.launcher.StandardVMType/JavaSE-21/"/>
|
||||
<classpathentry kind="con" path="org.eclipse.buildship.core.gradleclasspathcontainer"/>
|
||||
<classpathentry kind="output" path="bin/default"/>
|
||||
</classpath>
|
||||
@ -1,18 +1,58 @@
|
||||
package net.micode.notes;
|
||||
|
||||
import android.app.Application;
|
||||
import android.util.Log;
|
||||
|
||||
import net.micode.notes.auth.UserAuthManager;
|
||||
import net.micode.notes.data.ThemeRepository;
|
||||
import net.micode.notes.sync.SyncWorker;
|
||||
import net.micode.notes.capsule.CapsuleService;
|
||||
import android.content.Intent;
|
||||
import android.content.SharedPreferences;
|
||||
import android.os.Build;
|
||||
import androidx.preference.PreferenceManager;
|
||||
import android.provider.Settings;
|
||||
|
||||
import com.google.android.material.color.DynamicColors;
|
||||
|
||||
import net.micode.notes.tool.LocaleHelper;
|
||||
import android.content.Context;
|
||||
|
||||
public class NotesApplication extends Application {
|
||||
|
||||
private static final String TAG = "NotesApplication";
|
||||
|
||||
@Override
|
||||
protected void attachBaseContext(Context base) {
|
||||
super.attachBaseContext(LocaleHelper.onAttach(base));
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onCreate() {
|
||||
super.onCreate();
|
||||
// Apply Dynamic Colors (Material You) if available
|
||||
DynamicColors.applyToActivitiesIfAvailable(this);
|
||||
|
||||
// Apply saved theme preference
|
||||
DynamicColors.applyToActivitiesIfAvailable(this);
|
||||
|
||||
ThemeRepository repository = new ThemeRepository(this);
|
||||
ThemeRepository.applyTheme(repository.getThemeMode());
|
||||
|
||||
UserAuthManager authManager = UserAuthManager.getInstance(this);
|
||||
authManager.initialize(this);
|
||||
|
||||
Log.d(TAG, "EMAS Serverless initialized");
|
||||
|
||||
SyncWorker.initialize(this);
|
||||
|
||||
// Start CapsuleService if enabled
|
||||
SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(this);
|
||||
boolean capsuleEnabled = prefs.getBoolean("pref_key_capsule_enable", false);
|
||||
if (capsuleEnabled && Settings.canDrawOverlays(this)) {
|
||||
Intent intent = new Intent(this, CapsuleService.class);
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
||||
startForegroundService(intent);
|
||||
} else {
|
||||
startService(intent);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -0,0 +1,27 @@
|
||||
/*
|
||||
* Copyright (c) 2010-2011, The MiCode Open Source Community (www.micode.net)
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package net.micode.notes.api;
|
||||
|
||||
/**
|
||||
* 云数据库操作回调接口
|
||||
*
|
||||
* @param <T> 返回数据类型
|
||||
*/
|
||||
public interface CloudCallback<T> {
|
||||
void onSuccess(T result);
|
||||
void onError(String error);
|
||||
}
|
||||
@ -0,0 +1,394 @@
|
||||
package net.micode.notes.capsule;
|
||||
|
||||
import android.app.Notification;
|
||||
import android.app.NotificationChannel;
|
||||
import android.app.NotificationManager;
|
||||
import android.app.Service;
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
import android.graphics.PixelFormat;
|
||||
import android.os.Build;
|
||||
import android.os.IBinder;
|
||||
import android.view.Gravity;
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.MotionEvent;
|
||||
import android.view.View;
|
||||
import android.view.WindowManager;
|
||||
import android.widget.Button;
|
||||
import android.widget.EditText;
|
||||
import android.widget.TextView;
|
||||
import android.widget.Toast;
|
||||
import android.os.Handler;
|
||||
import android.util.Log;
|
||||
import android.view.DragEvent;
|
||||
import android.content.ClipData;
|
||||
import android.content.ClipDescription;
|
||||
|
||||
import net.micode.notes.R;
|
||||
import net.micode.notes.model.Note;
|
||||
import net.micode.notes.data.Notes;
|
||||
|
||||
import android.view.GestureDetector;
|
||||
|
||||
public class CapsuleService extends Service {
|
||||
|
||||
private static final String TAG = "CapsuleService";
|
||||
private WindowManager mWindowManager;
|
||||
private View mCollapsedView;
|
||||
private View mExpandedView;
|
||||
private EditText mEtContent;
|
||||
private WindowManager.LayoutParams mCollapsedParams;
|
||||
private WindowManager.LayoutParams mExpandedParams;
|
||||
private GestureDetector mGestureDetector;
|
||||
|
||||
private Handler mHandler = new Handler();
|
||||
public static String currentSourcePackage = "";
|
||||
|
||||
private static final String CHANNEL_ID = "CapsuleServiceChannel";
|
||||
|
||||
public static final String ACTION_SAVE_SUCCESS = "net.micode.notes.capsule.ACTION_SAVE_SUCCESS";
|
||||
|
||||
private final android.content.BroadcastReceiver mSaveReceiver = new android.content.BroadcastReceiver() {
|
||||
@Override
|
||||
public void onReceive(Context context, Intent intent) {
|
||||
if (ACTION_SAVE_SUCCESS.equals(intent.getAction())) {
|
||||
highlightCapsule();
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
public static void setCurrentSourcePackage(String pkg) {
|
||||
currentSourcePackage = pkg;
|
||||
}
|
||||
|
||||
@Override
|
||||
public IBinder onBind(Intent intent) {
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int onStartCommand(Intent intent, int flags, int startId) {
|
||||
return START_STICKY;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onCreate() {
|
||||
super.onCreate();
|
||||
mWindowManager = (WindowManager) getSystemService(WINDOW_SERVICE);
|
||||
createNotificationChannel();
|
||||
startForeground(1, createNotification());
|
||||
|
||||
initViews();
|
||||
}
|
||||
|
||||
private void initViews() {
|
||||
// Collapsed View
|
||||
mCollapsedView = LayoutInflater.from(this).inflate(R.layout.layout_capsule_collapsed, null);
|
||||
|
||||
int layoutFlag;
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
||||
layoutFlag = WindowManager.LayoutParams.TYPE_APPLICATION_OVERLAY;
|
||||
} else {
|
||||
layoutFlag = WindowManager.LayoutParams.TYPE_PHONE;
|
||||
}
|
||||
|
||||
mCollapsedParams = new WindowManager.LayoutParams(
|
||||
WindowManager.LayoutParams.WRAP_CONTENT,
|
||||
WindowManager.LayoutParams.WRAP_CONTENT,
|
||||
layoutFlag,
|
||||
WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE,
|
||||
PixelFormat.TRANSLUCENT);
|
||||
|
||||
mCollapsedParams.gravity = Gravity.TOP | Gravity.START;
|
||||
mCollapsedParams.x = 0;
|
||||
mCollapsedParams.y = 100;
|
||||
|
||||
// Expanded View
|
||||
mExpandedView = LayoutInflater.from(this).inflate(R.layout.layout_capsule_expanded, null);
|
||||
|
||||
mExpandedParams = new WindowManager.LayoutParams(
|
||||
dp2px(320),
|
||||
dp2px(400),
|
||||
layoutFlag,
|
||||
WindowManager.LayoutParams.FLAG_DIM_BEHIND | WindowManager.LayoutParams.FLAG_NOT_TOUCH_MODAL,
|
||||
PixelFormat.TRANSLUCENT);
|
||||
mExpandedParams.dimAmount = 0.5f;
|
||||
mExpandedParams.gravity = Gravity.CENTER;
|
||||
|
||||
// Setup Fields
|
||||
mCollapsedView.setClickable(true);
|
||||
mEtContent = mExpandedView.findViewById(R.id.et_content);
|
||||
|
||||
// Setup Listeners
|
||||
setupCollapsedListener();
|
||||
setupExpandedListener();
|
||||
|
||||
// Add Collapsed View initially
|
||||
try {
|
||||
mWindowManager.addView(mCollapsedView, mCollapsedParams);
|
||||
Log.d(TAG, "initViews: Collapsed view added");
|
||||
} catch (Exception e) {
|
||||
Log.e(TAG, "initViews: Failed to add collapsed view", e);
|
||||
}
|
||||
}
|
||||
|
||||
private void setupCollapsedListener() {
|
||||
mGestureDetector = new GestureDetector(this, new GestureDetector.SimpleOnGestureListener() {
|
||||
@Override
|
||||
public boolean onSingleTapConfirmed(MotionEvent e) {
|
||||
Log.d(TAG, "onSingleTapConfirmed: Triggering showExpandedView");
|
||||
showExpandedView();
|
||||
return true;
|
||||
}
|
||||
});
|
||||
|
||||
mCollapsedView.setOnTouchListener(new View.OnTouchListener() {
|
||||
private int initialX;
|
||||
private int initialY;
|
||||
private float initialTouchX;
|
||||
private float initialTouchY;
|
||||
|
||||
@Override
|
||||
public boolean onTouch(View v, MotionEvent event) {
|
||||
// Let GestureDetector handle taps
|
||||
if (mGestureDetector.onTouchEvent(event)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
switch (event.getAction()) {
|
||||
case MotionEvent.ACTION_DOWN:
|
||||
v.setPressed(true);
|
||||
initialX = mCollapsedParams.x;
|
||||
initialY = mCollapsedParams.y;
|
||||
initialTouchX = event.getRawX();
|
||||
initialTouchY = event.getRawY();
|
||||
return true;
|
||||
case MotionEvent.ACTION_MOVE:
|
||||
int Xdiff = (int) (event.getRawX() - initialTouchX);
|
||||
int Ydiff = (int) (event.getRawY() - initialTouchY);
|
||||
|
||||
// Move if dragged
|
||||
if (Math.abs(Xdiff) > 10 || Math.abs(Ydiff) > 10) {
|
||||
mCollapsedParams.x = initialX + Xdiff;
|
||||
mCollapsedParams.y = initialY + Ydiff;
|
||||
mWindowManager.updateViewLayout(mCollapsedView, mCollapsedParams);
|
||||
}
|
||||
return true;
|
||||
case MotionEvent.ACTION_CANCEL:
|
||||
case MotionEvent.ACTION_UP:
|
||||
v.setPressed(false);
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
});
|
||||
|
||||
mCollapsedView.setOnDragListener((v, event) -> {
|
||||
switch (event.getAction()) {
|
||||
case DragEvent.ACTION_DRAG_STARTED:
|
||||
Log.d(TAG, "onDrag: ACTION_DRAG_STARTED");
|
||||
if (event.getClipDescription().hasMimeType(ClipDescription.MIMETYPE_TEXT_PLAIN) ||
|
||||
event.getClipDescription().hasMimeType(ClipDescription.MIMETYPE_TEXT_HTML)) {
|
||||
v.setAlpha(1.0f);
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
case DragEvent.ACTION_DRAG_ENTERED:
|
||||
Log.d(TAG, "onDrag: ACTION_DRAG_ENTERED");
|
||||
v.animate().scaleX(1.2f).scaleY(1.2f).setDuration(200).start();
|
||||
return true;
|
||||
case DragEvent.ACTION_DRAG_EXITED:
|
||||
Log.d(TAG, "onDrag: ACTION_DRAG_EXITED");
|
||||
v.animate().scaleX(1.0f).scaleY(1.0f).setDuration(200).start();
|
||||
return true;
|
||||
case DragEvent.ACTION_DROP:
|
||||
Log.d(TAG, "onDrag: ACTION_DROP");
|
||||
ClipData.Item item = event.getClipData().getItemAt(0);
|
||||
CharSequence text = item.getText();
|
||||
if (text != null) {
|
||||
saveNote(text.toString());
|
||||
}
|
||||
v.animate().scaleX(1.0f).scaleY(1.0f).setDuration(200).start();
|
||||
return true;
|
||||
case DragEvent.ACTION_DRAG_ENDED:
|
||||
Log.d(TAG, "onDrag: ACTION_DRAG_ENDED");
|
||||
v.setAlpha(0.8f);
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
});
|
||||
}
|
||||
|
||||
private void setupExpandedListener() {
|
||||
Button btnCancel = mExpandedView.findViewById(R.id.btn_cancel);
|
||||
Button btnSave = mExpandedView.findViewById(R.id.btn_save);
|
||||
|
||||
btnCancel.setOnClickListener(v -> showCollapsedView());
|
||||
|
||||
btnSave.setOnClickListener(v -> {
|
||||
String content = mEtContent.getText().toString();
|
||||
if (!content.isEmpty()) {
|
||||
saveNote(content);
|
||||
mEtContent.setText("");
|
||||
showCollapsedView();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private void showExpandedView() {
|
||||
mHandler.post(() -> {
|
||||
try {
|
||||
Log.d(TAG, "showExpandedView: Attempting to show expanded view");
|
||||
if (mCollapsedView != null && mCollapsedView.getParent() != null) {
|
||||
Log.d(TAG, "showExpandedView: Removing collapsed view");
|
||||
mWindowManager.removeViewImmediate(mCollapsedView);
|
||||
}
|
||||
|
||||
if (mExpandedView != null && mExpandedView.getParent() == null) {
|
||||
Log.d(TAG, "showExpandedView: Adding expanded view");
|
||||
mWindowManager.addView(mExpandedView, mExpandedParams);
|
||||
|
||||
TextView tvSource = mExpandedView.findViewById(R.id.tv_source);
|
||||
if (currentSourcePackage != null && !currentSourcePackage.isEmpty()) {
|
||||
tvSource.setText("Source: " + currentSourcePackage);
|
||||
tvSource.setVisibility(View.VISIBLE);
|
||||
} else {
|
||||
tvSource.setVisibility(View.GONE);
|
||||
}
|
||||
|
||||
if (mEtContent != null) {
|
||||
mEtContent.requestFocus();
|
||||
}
|
||||
Log.d(TAG, "showExpandedView: Expanded view added successfully");
|
||||
} else {
|
||||
Log.w(TAG, "showExpandedView: Expanded view already has a parent or is null");
|
||||
}
|
||||
} catch (Exception e) {
|
||||
Log.e(TAG, "showExpandedView: Error", e);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private void showCollapsedView() {
|
||||
mHandler.post(() -> {
|
||||
try {
|
||||
Log.d(TAG, "showCollapsedView: Attempting to show collapsed view");
|
||||
if (mExpandedView != null && mExpandedView.getParent() != null) {
|
||||
Log.d(TAG, "showCollapsedView: Removing expanded view");
|
||||
mWindowManager.removeViewImmediate(mExpandedView);
|
||||
}
|
||||
|
||||
if (mCollapsedView != null && mCollapsedView.getParent() == null) {
|
||||
Log.d(TAG, "showCollapsedView: Adding collapsed view");
|
||||
mWindowManager.addView(mCollapsedView, mCollapsedParams);
|
||||
Log.d(TAG, "showCollapsedView: Collapsed view added successfully");
|
||||
} else {
|
||||
Log.w(TAG, "showCollapsedView: Collapsed view already has a parent or is null");
|
||||
}
|
||||
} catch (Exception e) {
|
||||
Log.e(TAG, "showCollapsedView: Error", e);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private void saveNote(String content) {
|
||||
new Thread(() -> {
|
||||
try {
|
||||
// 1. Create new note in CAPSULE folder
|
||||
long noteId = Note.getNewNoteId(this, Notes.ID_CAPSULE_FOLDER);
|
||||
|
||||
// 2. Create Note object
|
||||
Note note = new Note();
|
||||
note.setTextData(Notes.DataColumns.CONTENT, content);
|
||||
|
||||
// Generate Summary (First 20 chars or first line)
|
||||
String summary = content.length() > 20 ? content.substring(0, 20) + "..." : content;
|
||||
int firstLineEnd = content.indexOf('\n');
|
||||
if (firstLineEnd > 0 && firstLineEnd < 20) {
|
||||
summary = content.substring(0, firstLineEnd);
|
||||
}
|
||||
note.setNoteValue(Notes.NoteColumns.SNIPPET, summary);
|
||||
|
||||
// Add Source Info if available
|
||||
if (currentSourcePackage != null && !currentSourcePackage.isEmpty()) {
|
||||
note.setTextData(Notes.DataColumns.DATA3, currentSourcePackage);
|
||||
}
|
||||
|
||||
boolean success = note.syncNote(this, noteId);
|
||||
|
||||
mHandler.post(() -> {
|
||||
if (success) {
|
||||
Log.d(TAG, "saveNote: Success");
|
||||
Toast.makeText(this, "Saved to Notes", Toast.LENGTH_SHORT).show();
|
||||
} else {
|
||||
Log.e(TAG, "saveNote: Failed");
|
||||
Toast.makeText(this, "Failed to save", Toast.LENGTH_SHORT).show();
|
||||
}
|
||||
});
|
||||
|
||||
} catch (Exception e) {
|
||||
e.printStackTrace();
|
||||
Log.e(TAG, "saveNote: Exception", e);
|
||||
mHandler.post(() -> Toast.makeText(this, "Error: " + e.getMessage(), Toast.LENGTH_SHORT).show());
|
||||
}
|
||||
}).start();
|
||||
}
|
||||
|
||||
private int dp2px(int dp) {
|
||||
return (int) (dp * getResources().getDisplayMetrics().density);
|
||||
}
|
||||
|
||||
private void createNotificationChannel() {
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
||||
NotificationChannel serviceChannel = new NotificationChannel(
|
||||
CHANNEL_ID,
|
||||
"Capsule Service Channel",
|
||||
NotificationManager.IMPORTANCE_DEFAULT
|
||||
);
|
||||
NotificationManager manager = getSystemService(NotificationManager.class);
|
||||
if (manager != null) {
|
||||
manager.createNotificationChannel(serviceChannel);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private Notification createNotification() {
|
||||
Notification.Builder builder;
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
||||
builder = new Notification.Builder(this, CHANNEL_ID);
|
||||
} else {
|
||||
builder = new Notification.Builder(this);
|
||||
}
|
||||
|
||||
return builder.setContentTitle("Global Capsule Running")
|
||||
.setContentText("Tap to configure")
|
||||
.setSmallIcon(R.mipmap.ic_launcher)
|
||||
.build();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onDestroy() {
|
||||
super.onDestroy();
|
||||
try {
|
||||
unregisterReceiver(mSaveReceiver);
|
||||
} catch (Exception e) {
|
||||
Log.e(TAG, "Receiver not registered", e);
|
||||
}
|
||||
if (mCollapsedView != null && mCollapsedView.getParent() != null) {
|
||||
mWindowManager.removeView(mCollapsedView);
|
||||
}
|
||||
if (mExpandedView != null && mExpandedView.getParent() != null) {
|
||||
mWindowManager.removeView(mExpandedView);
|
||||
}
|
||||
}
|
||||
|
||||
private void highlightCapsule() {
|
||||
if (mCollapsedView != null && mCollapsedView.getParent() != null) {
|
||||
mHandler.post(() -> {
|
||||
mCollapsedView.animate().scaleX(1.5f).scaleY(1.5f).setDuration(200).withEndAction(() -> {
|
||||
mCollapsedView.animate().scaleX(1.0f).scaleY(1.0f).setDuration(200).start();
|
||||
}).start();
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,67 @@
|
||||
package net.micode.notes.capsule;
|
||||
|
||||
import android.accessibilityservice.AccessibilityService;
|
||||
import android.content.ClipboardManager;
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
import android.view.accessibility.AccessibilityEvent;
|
||||
import android.widget.Toast;
|
||||
|
||||
public class ClipboardMonitorService extends AccessibilityService {
|
||||
|
||||
private ClipboardManager mClipboardManager;
|
||||
private ClipboardManager.OnPrimaryClipChangedListener mClipListener;
|
||||
private long mLastClipTime = 0;
|
||||
private static final long MERGE_THRESHOLD = 2000; // 2 seconds
|
||||
|
||||
@Override
|
||||
public void onCreate() {
|
||||
super.onCreate();
|
||||
mClipboardManager = (ClipboardManager) getSystemService(Context.CLIPBOARD_SERVICE);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onServiceConnected() {
|
||||
super.onServiceConnected();
|
||||
// Register clipboard listener
|
||||
if (mClipboardManager != null) {
|
||||
mClipListener = () -> {
|
||||
handleClipChanged();
|
||||
};
|
||||
mClipboardManager.addPrimaryClipChangedListener(mClipListener);
|
||||
}
|
||||
}
|
||||
|
||||
private void handleClipChanged() {
|
||||
long now = System.currentTimeMillis();
|
||||
if (now - mLastClipTime < MERGE_THRESHOLD) {
|
||||
// Notify CapsuleService to show "Merge" bubble
|
||||
// For now just show a toast or log
|
||||
// Intent intent = new Intent("net.micode.notes.capsule.ACTION_MERGE_SUGGESTION");
|
||||
// sendBroadcast(intent);
|
||||
}
|
||||
mLastClipTime = now;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onAccessibilityEvent(AccessibilityEvent event) {
|
||||
if (event.getEventType() == AccessibilityEvent.TYPE_WINDOW_STATE_CHANGED) {
|
||||
if (event.getPackageName() != null) {
|
||||
// Store current package name in CapsuleService
|
||||
CapsuleService.setCurrentSourcePackage(event.getPackageName().toString());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onInterrupt() {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onDestroy() {
|
||||
super.onDestroy();
|
||||
if (mClipboardManager != null && mClipListener != null) {
|
||||
mClipboardManager.removePrimaryClipChangedListener(mClipListener);
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,92 @@
|
||||
package net.micode.notes.tool;
|
||||
|
||||
import android.content.Context;
|
||||
import android.content.SharedPreferences;
|
||||
import android.content.res.Configuration;
|
||||
import android.content.res.Resources;
|
||||
import android.os.Build;
|
||||
import androidx.preference.PreferenceManager;
|
||||
|
||||
import java.util.Locale;
|
||||
|
||||
public class LocaleHelper {
|
||||
|
||||
private static final String SELECTED_LANGUAGE = "Locale.Helper.Selected.Language";
|
||||
|
||||
public static Context onAttach(Context context) {
|
||||
String lang = getPersistedData(context, Locale.getDefault().getLanguage());
|
||||
return setLocale(context, lang);
|
||||
}
|
||||
|
||||
public static Context onAttach(Context context, String defaultLanguage) {
|
||||
String lang = getPersistedData(context, defaultLanguage);
|
||||
return setLocale(context, lang);
|
||||
}
|
||||
|
||||
public static String getLanguage(Context context) {
|
||||
return getPersistedData(context, Locale.getDefault().getLanguage());
|
||||
}
|
||||
|
||||
public static Context setLocale(Context context, String language) {
|
||||
persist(context, language);
|
||||
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
|
||||
return updateResources(context, language);
|
||||
}
|
||||
|
||||
return updateResourcesLegacy(context, language);
|
||||
}
|
||||
|
||||
private static String getPersistedData(Context context, String defaultLanguage) {
|
||||
SharedPreferences preferences = PreferenceManager.getDefaultSharedPreferences(context);
|
||||
// 默认语言改为中文 (zh-CN)
|
||||
return preferences.getString(SELECTED_LANGUAGE, "zh-CN");
|
||||
}
|
||||
|
||||
private static void persist(Context context, String language) {
|
||||
SharedPreferences preferences = PreferenceManager.getDefaultSharedPreferences(context);
|
||||
SharedPreferences.Editor editor = preferences.edit();
|
||||
editor.putString(SELECTED_LANGUAGE, language);
|
||||
editor.apply();
|
||||
}
|
||||
|
||||
private static Context updateResources(Context context, String language) {
|
||||
Locale locale = getLocale(language);
|
||||
Locale.setDefault(locale);
|
||||
|
||||
Configuration configuration = context.getResources().getConfiguration();
|
||||
configuration.setLocale(locale);
|
||||
configuration.setLayoutDirection(locale);
|
||||
|
||||
return context.createConfigurationContext(configuration);
|
||||
}
|
||||
|
||||
private static Context updateResourcesLegacy(Context context, String language) {
|
||||
Locale locale = getLocale(language);
|
||||
Locale.setDefault(locale);
|
||||
|
||||
Resources resources = context.getResources();
|
||||
Configuration configuration = resources.getConfiguration();
|
||||
configuration.locale = locale;
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR1) {
|
||||
configuration.setLayoutDirection(locale);
|
||||
}
|
||||
|
||||
resources.updateConfiguration(configuration, resources.getDisplayMetrics());
|
||||
|
||||
return context;
|
||||
}
|
||||
|
||||
private static Locale getLocale(String language) {
|
||||
if (language.equals("zh-CN")) {
|
||||
return Locale.SIMPLIFIED_CHINESE;
|
||||
} else if (language.equals("zh-TW")) {
|
||||
return Locale.TRADITIONAL_CHINESE;
|
||||
} else if (language.equals("en")) {
|
||||
return Locale.ENGLISH;
|
||||
} else if (language.equals("system")) {
|
||||
return Locale.getDefault();
|
||||
}
|
||||
return new Locale(language);
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,12 @@
|
||||
package net.micode.notes.ui;
|
||||
|
||||
import android.content.Context;
|
||||
import androidx.appcompat.app.AppCompatActivity;
|
||||
import net.micode.notes.tool.LocaleHelper;
|
||||
|
||||
public class BaseActivity extends AppCompatActivity {
|
||||
@Override
|
||||
protected void attachBaseContext(Context newBase) {
|
||||
super.attachBaseContext(LocaleHelper.onAttach(newBase));
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,71 @@
|
||||
package net.micode.notes.ui;
|
||||
|
||||
import android.app.Activity;
|
||||
import android.content.Intent;
|
||||
import android.os.Bundle;
|
||||
import android.widget.Toast;
|
||||
import android.util.Log;
|
||||
import net.micode.notes.data.Notes;
|
||||
import net.micode.notes.model.Note;
|
||||
import net.micode.notes.capsule.CapsuleService;
|
||||
|
||||
public class CapsuleActionActivity extends Activity {
|
||||
|
||||
private static final String TAG = "CapsuleActionActivity";
|
||||
|
||||
@Override
|
||||
protected void onCreate(Bundle savedInstanceState) {
|
||||
super.onCreate(savedInstanceState);
|
||||
|
||||
CharSequence text = getIntent().getCharSequenceExtra(Intent.EXTRA_PROCESS_TEXT);
|
||||
String sourcePackage = null;
|
||||
if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.LOLLIPOP_MR1) {
|
||||
if (getReferrer() != null) {
|
||||
sourcePackage = getReferrer().getAuthority(); // or getHost()
|
||||
}
|
||||
}
|
||||
|
||||
if (text != null) {
|
||||
saveNote(text.toString(), sourcePackage);
|
||||
|
||||
// Notify CapsuleService to animate (if running)
|
||||
Intent intent = new Intent("net.micode.notes.capsule.ACTION_SAVE_SUCCESS");
|
||||
sendBroadcast(intent);
|
||||
}
|
||||
|
||||
finish();
|
||||
}
|
||||
|
||||
private void saveNote(String content, String source) {
|
||||
new Thread(() -> {
|
||||
try {
|
||||
long noteId = Note.getNewNoteId(this, Notes.ID_CAPSULE_FOLDER);
|
||||
Note note = new Note();
|
||||
note.setTextData(Notes.DataColumns.CONTENT, content);
|
||||
|
||||
String summary = content.length() > 20 ? content.substring(0, 20) + "..." : content;
|
||||
int firstLineEnd = content.indexOf('\n');
|
||||
if (firstLineEnd > 0 && firstLineEnd < 20) {
|
||||
summary = content.substring(0, firstLineEnd);
|
||||
}
|
||||
note.setNoteValue(Notes.NoteColumns.SNIPPET, summary);
|
||||
|
||||
if (source != null) {
|
||||
note.setTextData(Notes.DataColumns.DATA3, source);
|
||||
}
|
||||
|
||||
boolean success = note.syncNote(this, noteId);
|
||||
|
||||
runOnUiThread(() -> {
|
||||
if (success) {
|
||||
Toast.makeText(this, "已保存到胶囊", Toast.LENGTH_SHORT).show();
|
||||
} else {
|
||||
Toast.makeText(this, "保存失败", Toast.LENGTH_SHORT).show();
|
||||
}
|
||||
});
|
||||
} catch (Exception e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
}).start();
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,33 @@
|
||||
package net.micode.notes.ui;
|
||||
|
||||
import android.os.Bundle;
|
||||
import android.view.MenuItem;
|
||||
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.appcompat.app.AppCompatActivity;
|
||||
|
||||
import net.micode.notes.R;
|
||||
|
||||
public class CapsuleListActivity extends BaseActivity {
|
||||
|
||||
@Override
|
||||
protected void onCreate(@Nullable Bundle savedInstanceState) {
|
||||
super.onCreate(savedInstanceState);
|
||||
setContentView(R.layout.activity_capsule_list);
|
||||
|
||||
if (savedInstanceState == null) {
|
||||
getSupportFragmentManager().beginTransaction()
|
||||
.replace(R.id.container, new CapsuleListFragment())
|
||||
.commit();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onOptionsItemSelected(MenuItem item) {
|
||||
if (item.getItemId() == android.R.id.home) {
|
||||
finish();
|
||||
return true;
|
||||
}
|
||||
return super.onOptionsItemSelected(item);
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,191 @@
|
||||
package net.micode.notes.ui;
|
||||
|
||||
import android.content.Intent;
|
||||
import android.database.Cursor;
|
||||
import android.os.Bundle;
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
import android.widget.TextView;
|
||||
import android.widget.Toast;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.appcompat.app.AppCompatActivity;
|
||||
import androidx.appcompat.widget.Toolbar;
|
||||
import androidx.fragment.app.Fragment;
|
||||
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.model.Note;
|
||||
|
||||
import java.text.SimpleDateFormat;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Date;
|
||||
import java.util.List;
|
||||
import java.util.Locale;
|
||||
|
||||
public class CapsuleListFragment extends Fragment {
|
||||
|
||||
private RecyclerView mRecyclerView;
|
||||
private CapsuleAdapter mAdapter;
|
||||
private TextView mEmptyView;
|
||||
private Toolbar mToolbar;
|
||||
|
||||
@Nullable
|
||||
@Override
|
||||
public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) {
|
||||
View view = inflater.inflate(R.layout.fragment_capsule_list, container, false);
|
||||
mRecyclerView = view.findViewById(R.id.capsule_list);
|
||||
mEmptyView = view.findViewById(R.id.tv_empty);
|
||||
mToolbar = view.findViewById(R.id.toolbar);
|
||||
|
||||
setupToolbar();
|
||||
|
||||
mRecyclerView.setLayoutManager(new LinearLayoutManager(getContext()));
|
||||
mAdapter = new CapsuleAdapter();
|
||||
mRecyclerView.setAdapter(mAdapter);
|
||||
|
||||
return view;
|
||||
}
|
||||
|
||||
private void setupToolbar() {
|
||||
if (mToolbar != null && getActivity() instanceof AppCompatActivity) {
|
||||
AppCompatActivity activity = (AppCompatActivity) getActivity();
|
||||
activity.setSupportActionBar(mToolbar);
|
||||
if (activity.getSupportActionBar() != null) {
|
||||
activity.getSupportActionBar().setDisplayHomeAsUpEnabled(true);
|
||||
activity.getSupportActionBar().setTitle("速记胶囊");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onResume() {
|
||||
super.onResume();
|
||||
loadCapsules();
|
||||
}
|
||||
|
||||
private void loadCapsules() {
|
||||
new Thread(() -> {
|
||||
if (getContext() == null) return;
|
||||
|
||||
// Query notes in CAPSULE folder.
|
||||
// Join with Data table to get DATA3 (source package)
|
||||
Cursor cursor = getContext().getContentResolver().query(
|
||||
Notes.CONTENT_NOTE_URI,
|
||||
null,
|
||||
Notes.NoteColumns.PARENT_ID + "=?",
|
||||
new String[]{String.valueOf(Notes.ID_CAPSULE_FOLDER)},
|
||||
Notes.NoteColumns.MODIFIED_DATE + " DESC"
|
||||
);
|
||||
|
||||
List<CapsuleItem> items = new ArrayList<>();
|
||||
if (cursor != null) {
|
||||
while (cursor.moveToNext()) {
|
||||
long id = cursor.getLong(cursor.getColumnIndexOrThrow(Notes.NoteColumns.ID));
|
||||
String snippet = cursor.getString(cursor.getColumnIndexOrThrow(Notes.NoteColumns.SNIPPET));
|
||||
long modifiedDate = cursor.getLong(cursor.getColumnIndexOrThrow(Notes.NoteColumns.MODIFIED_DATE));
|
||||
|
||||
// Try to get source from projection (if joined) or query separately
|
||||
String source = "";
|
||||
try {
|
||||
int sourceIdx = cursor.getColumnIndex(Notes.DataColumns.DATA3);
|
||||
if (sourceIdx != -1) {
|
||||
source = cursor.getString(sourceIdx);
|
||||
}
|
||||
} catch (Exception e) {
|
||||
// Not joined, ignore for now or lazy load
|
||||
}
|
||||
|
||||
items.add(new CapsuleItem(id, snippet, modifiedDate, source));
|
||||
}
|
||||
cursor.close();
|
||||
}
|
||||
|
||||
// Update UI
|
||||
if (getActivity() != null) {
|
||||
getActivity().runOnUiThread(() -> {
|
||||
mAdapter.setItems(items);
|
||||
mEmptyView.setVisibility(items.isEmpty() ? View.VISIBLE : View.GONE);
|
||||
});
|
||||
}
|
||||
}).start();
|
||||
}
|
||||
|
||||
private void openNoteEditor(long noteId) {
|
||||
Intent intent = new Intent(getActivity(), NoteEditActivity.class);
|
||||
intent.setAction(Intent.ACTION_VIEW);
|
||||
intent.putExtra(Intent.EXTRA_UID, noteId);
|
||||
startActivity(intent);
|
||||
}
|
||||
|
||||
private static class CapsuleItem {
|
||||
long id;
|
||||
String summary;
|
||||
long time;
|
||||
String source;
|
||||
|
||||
public CapsuleItem(long id, String summary, long time, String source) {
|
||||
this.id = id;
|
||||
this.summary = summary;
|
||||
this.time = time;
|
||||
this.source = source;
|
||||
}
|
||||
}
|
||||
|
||||
private class CapsuleAdapter extends RecyclerView.Adapter<CapsuleAdapter.ViewHolder> {
|
||||
private List<CapsuleItem> mItems = new ArrayList<>();
|
||||
|
||||
public void setItems(List<CapsuleItem> items) {
|
||||
mItems = items;
|
||||
notifyDataSetChanged();
|
||||
}
|
||||
|
||||
@NonNull
|
||||
@Override
|
||||
public ViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
|
||||
View v = LayoutInflater.from(parent.getContext()).inflate(R.layout.item_capsule, parent, false);
|
||||
return new ViewHolder(v);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onBindViewHolder(@NonNull ViewHolder holder, int position) {
|
||||
CapsuleItem item = mItems.get(position);
|
||||
holder.tvSummary.setText(item.summary);
|
||||
|
||||
SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm", Locale.getDefault());
|
||||
holder.tvTime.setText(sdf.format(new Date(item.time)));
|
||||
|
||||
if (item.source != null && !item.source.isEmpty()) {
|
||||
holder.tvSource.setText(item.source);
|
||||
holder.tvSource.setVisibility(View.VISIBLE);
|
||||
} else {
|
||||
holder.tvSource.setVisibility(View.GONE);
|
||||
}
|
||||
|
||||
holder.itemView.setOnClickListener(v -> {
|
||||
openNoteEditor(item.id);
|
||||
});
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getItemCount() {
|
||||
return mItems.size();
|
||||
}
|
||||
|
||||
class ViewHolder extends RecyclerView.ViewHolder {
|
||||
TextView tvSummary, tvTime, tvSource;
|
||||
|
||||
public ViewHolder(@NonNull View itemView) {
|
||||
super(itemView);
|
||||
tvSummary = itemView.findViewById(R.id.tv_summary);
|
||||
tvTime = itemView.findViewById(R.id.tv_time);
|
||||
tvSource = itemView.findViewById(R.id.tv_source);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,91 @@
|
||||
package net.micode.notes.ui;
|
||||
|
||||
import android.content.Context;
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
import android.widget.TextView;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.recyclerview.widget.RecyclerView;
|
||||
|
||||
import net.micode.notes.R;
|
||||
import net.micode.notes.data.NotesRepository;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
public class FolderAdapter extends RecyclerView.Adapter<FolderAdapter.FolderViewHolder> {
|
||||
|
||||
private Context context;
|
||||
private List<NotesRepository.NoteInfo> folders;
|
||||
private long selectedFolderId = -1;
|
||||
private OnFolderClickListener listener;
|
||||
|
||||
public interface OnFolderClickListener {
|
||||
void onFolderClick(long folderId);
|
||||
}
|
||||
|
||||
public FolderAdapter(Context context) {
|
||||
this.context = context;
|
||||
this.folders = new ArrayList<>();
|
||||
}
|
||||
|
||||
public void setFolders(List<NotesRepository.NoteInfo> folders) {
|
||||
this.folders = folders != null ? folders : new ArrayList<>();
|
||||
notifyDataSetChanged();
|
||||
}
|
||||
|
||||
public void setSelectedFolderId(long folderId) {
|
||||
this.selectedFolderId = folderId;
|
||||
notifyDataSetChanged();
|
||||
}
|
||||
|
||||
public void setOnFolderClickListener(OnFolderClickListener listener) {
|
||||
this.listener = listener;
|
||||
}
|
||||
|
||||
@NonNull
|
||||
@Override
|
||||
public FolderViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
|
||||
View view = LayoutInflater.from(context).inflate(R.layout.folder_tab_item, parent, false);
|
||||
return new FolderViewHolder(view);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onBindViewHolder(@NonNull FolderViewHolder holder, int position) {
|
||||
NotesRepository.NoteInfo folder = folders.get(position);
|
||||
holder.bind(folder);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getItemCount() {
|
||||
return folders.size();
|
||||
}
|
||||
|
||||
class FolderViewHolder extends RecyclerView.ViewHolder {
|
||||
TextView tvName;
|
||||
|
||||
public FolderViewHolder(View itemView) {
|
||||
super(itemView);
|
||||
tvName = itemView.findViewById(R.id.tv_folder_name);
|
||||
itemView.setOnClickListener(v -> {
|
||||
if (listener != null) {
|
||||
int pos = getAdapterPosition();
|
||||
if (pos != RecyclerView.NO_POSITION) {
|
||||
listener.onFolderClick(folders.get(pos).getId());
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
public void bind(NotesRepository.NoteInfo folder) {
|
||||
String name = folder.snippet; // Folder name is stored in snippet
|
||||
if (name == null || name.isEmpty()) {
|
||||
name = "Folder";
|
||||
}
|
||||
tvName.setText(name);
|
||||
tvName.setSelected(folder.getId() == selectedFolderId);
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,164 @@
|
||||
/*
|
||||
* Copyright (c) 2025, Modern Notes Project
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package net.micode.notes.ui;
|
||||
|
||||
import android.app.AlertDialog;
|
||||
import android.app.Dialog;
|
||||
import android.content.Context;
|
||||
import android.content.DialogInterface;
|
||||
import android.text.InputFilter;
|
||||
import android.text.TextUtils;
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.View;
|
||||
import android.widget.EditText;
|
||||
import android.widget.TextView;
|
||||
|
||||
import net.micode.notes.R;
|
||||
|
||||
/**
|
||||
* 文件夹操作对话框工具类
|
||||
* <p>
|
||||
* 提供重命名和删除文件夹的对话框
|
||||
* </p>
|
||||
*/
|
||||
public class FolderOperationDialogs {
|
||||
|
||||
private static final int MAX_FOLDER_NAME_LENGTH = 50;
|
||||
|
||||
/**
|
||||
* 显示重命名文件夹对话框
|
||||
*
|
||||
* @param activity Activity实例
|
||||
* @param currentName 当前文件夹名称
|
||||
* @param listener 重命名监听器
|
||||
*/
|
||||
public static void showRenameDialog(Context activity, String currentName,
|
||||
OnRenameListener listener) {
|
||||
AlertDialog.Builder builder = new AlertDialog.Builder(activity);
|
||||
builder.setTitle(R.string.dialog_rename_folder_title);
|
||||
|
||||
// 创建输入框
|
||||
final EditText input = new EditText(activity);
|
||||
input.setText(currentName);
|
||||
input.setHint(R.string.dialog_create_folder_hint);
|
||||
input.setFilters(new InputFilter[]{new InputFilter.LengthFilter(MAX_FOLDER_NAME_LENGTH)});
|
||||
input.setSelection(input.getText().length()); // 光标移到末尾
|
||||
|
||||
builder.setView(input);
|
||||
|
||||
builder.setPositiveButton(R.string.menu_rename, (dialog, which) -> {
|
||||
String newName = input.getText().toString().trim();
|
||||
if (TextUtils.isEmpty(newName)) {
|
||||
listener.onError(activity.getString(R.string.error_folder_name_empty));
|
||||
return;
|
||||
}
|
||||
if (newName.length() > MAX_FOLDER_NAME_LENGTH) {
|
||||
listener.onError(activity.getString(R.string.error_folder_name_too_long));
|
||||
return;
|
||||
}
|
||||
|
||||
listener.onRename(newName);
|
||||
});
|
||||
|
||||
builder.setNegativeButton(android.R.string.cancel, (dialog, which) -> {
|
||||
listener.onCancel();
|
||||
});
|
||||
|
||||
builder.show();
|
||||
}
|
||||
|
||||
/**
|
||||
* 显示删除文件夹确认对话框
|
||||
*
|
||||
* @param activity Activity实例
|
||||
* @param folderName 文件夹名称
|
||||
* @param noteCount 文件夹中的笔记数量
|
||||
* @param listener 删除监听器
|
||||
*/
|
||||
public static void showDeleteFolderDialog(Context activity, String folderName,
|
||||
int noteCount, OnDeleteListener listener) {
|
||||
AlertDialog.Builder builder = new AlertDialog.Builder(activity);
|
||||
builder.setTitle(R.string.dialog_delete_folder_title);
|
||||
|
||||
// 创建自定义消息视图
|
||||
LayoutInflater inflater = LayoutInflater.from(activity);
|
||||
View messageView = inflater.inflate(R.layout.dialog_folder_delete, null);
|
||||
TextView messageText = messageView.findViewById(R.id.tv_delete_message);
|
||||
|
||||
String message;
|
||||
if (noteCount > 0) {
|
||||
message = activity.getString(R.string.dialog_delete_folder_with_notes, folderName, noteCount);
|
||||
} else {
|
||||
message = activity.getString(R.string.dialog_delete_folder_empty, folderName);
|
||||
}
|
||||
messageText.setText(message);
|
||||
|
||||
builder.setView(messageView);
|
||||
|
||||
builder.setPositiveButton(R.string.menu_delete, (dialog, which) -> {
|
||||
listener.onDelete();
|
||||
});
|
||||
|
||||
builder.setNegativeButton(android.R.string.cancel, (dialog, which) -> {
|
||||
listener.onCancel();
|
||||
});
|
||||
|
||||
builder.show();
|
||||
}
|
||||
|
||||
/**
|
||||
* 重命名监听器接口
|
||||
*/
|
||||
public interface OnRenameListener {
|
||||
/**
|
||||
* 重命名确认回调
|
||||
*
|
||||
* @param newName 新名称
|
||||
*/
|
||||
void onRename(String newName);
|
||||
|
||||
/**
|
||||
* 取消回调
|
||||
*/
|
||||
default void onCancel() {
|
||||
}
|
||||
|
||||
/**
|
||||
* 错误回调
|
||||
*
|
||||
* @param errorMessage 错误消息
|
||||
*/
|
||||
default void onError(String errorMessage) {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 删除监听器接口
|
||||
*/
|
||||
public interface OnDeleteListener {
|
||||
/**
|
||||
* 删除确认回调
|
||||
*/
|
||||
void onDelete();
|
||||
|
||||
/**
|
||||
* 取消回调
|
||||
*/
|
||||
default void onCancel() {
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,291 @@
|
||||
package net.micode.notes.ui;
|
||||
|
||||
import android.content.Intent;
|
||||
import android.os.Bundle;
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.fragment.app.Fragment;
|
||||
import androidx.lifecycle.ViewModelProvider;
|
||||
import android.app.AlertDialog;
|
||||
import android.text.InputType;
|
||||
import android.widget.EditText;
|
||||
import android.widget.Toast;
|
||||
|
||||
import android.preference.PreferenceManager;
|
||||
import androidx.recyclerview.widget.LinearLayoutManager;
|
||||
import androidx.recyclerview.widget.RecyclerView;
|
||||
import androidx.recyclerview.widget.StaggeredGridLayoutManager;
|
||||
|
||||
import net.micode.notes.R;
|
||||
import net.micode.notes.data.Notes;
|
||||
import net.micode.notes.data.NotesRepository;
|
||||
import net.micode.notes.databinding.NoteListBinding;
|
||||
import net.micode.notes.viewmodel.NotesListViewModel;
|
||||
|
||||
public class NotesListFragment extends Fragment implements
|
||||
NoteInfoAdapter.OnNoteItemClickListener,
|
||||
NoteInfoAdapter.OnNoteItemLongClickListener,
|
||||
NoteInfoAdapter.OnSwipeMenuClickListener {
|
||||
|
||||
private static final String TAG = "NotesListFragment";
|
||||
private static final String PREF_KEY_IS_STAGGERED = "is_staggered";
|
||||
|
||||
private NotesListViewModel viewModel;
|
||||
private NoteListBinding binding;
|
||||
private NoteInfoAdapter adapter;
|
||||
|
||||
private static final int REQUEST_CODE_OPEN_NODE = 102;
|
||||
private static final int REQUEST_CODE_NEW_NODE = 103;
|
||||
private static final int REQUEST_CODE_VERIFY_PASSWORD_FOR_OPEN = 107;
|
||||
|
||||
private NotesRepository.NoteInfo pendingNote;
|
||||
|
||||
@Nullable
|
||||
@Override
|
||||
public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) {
|
||||
binding = NoteListBinding.inflate(inflater, container, false);
|
||||
return binding.getRoot();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) {
|
||||
super.onViewCreated(view, savedInstanceState);
|
||||
initViewModel();
|
||||
initViews(view);
|
||||
observeViewModel();
|
||||
}
|
||||
|
||||
private void initViewModel() {
|
||||
NotesRepository repository = new NotesRepository(requireContext().getContentResolver());
|
||||
// Use requireActivity() to share ViewModel with Activity (for Sidebar filtering)
|
||||
viewModel = new ViewModelProvider(requireActivity(),
|
||||
new ViewModelProvider.Factory() {
|
||||
@Override
|
||||
public <T extends androidx.lifecycle.ViewModel> T create(Class<T> modelClass) {
|
||||
return (T) new NotesListViewModel(requireActivity().getApplication(), repository);
|
||||
}
|
||||
}).get(NotesListViewModel.class);
|
||||
}
|
||||
|
||||
private void initViews(View view) {
|
||||
adapter = new NoteInfoAdapter(requireContext());
|
||||
binding.notesList.setAdapter(adapter);
|
||||
|
||||
// Restore layout preference
|
||||
boolean isStaggered = PreferenceManager.getDefaultSharedPreferences(requireContext())
|
||||
.getBoolean(PREF_KEY_IS_STAGGERED, true);
|
||||
setLayoutManager(isStaggered);
|
||||
|
||||
adapter.setOnNoteItemClickListener(this);
|
||||
adapter.setOnNoteItemLongClickListener(this);
|
||||
adapter.setOnSwipeMenuClickListener(this);
|
||||
|
||||
// Fix FAB: Enable creating new notes
|
||||
binding.btnNewNote.setOnClickListener(v -> {
|
||||
Intent intent = new Intent(getActivity(), NoteEditActivity.class);
|
||||
intent.setAction(Intent.ACTION_INSERT_OR_EDIT);
|
||||
intent.putExtra(Notes.INTENT_EXTRA_FOLDER_ID, viewModel.getCurrentFolderId());
|
||||
startActivityForResult(intent, REQUEST_CODE_NEW_NODE);
|
||||
});
|
||||
}
|
||||
|
||||
private void setLayoutManager(boolean isStaggered) {
|
||||
if (isStaggered) {
|
||||
StaggeredGridLayoutManager layoutManager = new StaggeredGridLayoutManager(2, StaggeredGridLayoutManager.VERTICAL);
|
||||
layoutManager.setGapStrategy(StaggeredGridLayoutManager.GAP_HANDLING_MOVE_ITEMS_BETWEEN_SPANS);
|
||||
binding.notesList.setLayoutManager(layoutManager);
|
||||
} else {
|
||||
binding.notesList.setLayoutManager(new LinearLayoutManager(requireContext()));
|
||||
}
|
||||
}
|
||||
|
||||
public boolean toggleLayout() {
|
||||
boolean isStaggered = binding.notesList.getLayoutManager() instanceof StaggeredGridLayoutManager;
|
||||
boolean newIsStaggered = !isStaggered;
|
||||
|
||||
setLayoutManager(newIsStaggered);
|
||||
|
||||
PreferenceManager.getDefaultSharedPreferences(requireContext())
|
||||
.edit()
|
||||
.putBoolean(PREF_KEY_IS_STAGGERED, newIsStaggered)
|
||||
.apply();
|
||||
|
||||
return newIsStaggered;
|
||||
}
|
||||
|
||||
public boolean isStaggeredLayout() {
|
||||
return binding.notesList.getLayoutManager() instanceof StaggeredGridLayoutManager;
|
||||
}
|
||||
|
||||
private void observeViewModel() {
|
||||
viewModel.getNotesLiveData().observe(getViewLifecycleOwner(), notes -> {
|
||||
adapter.setNotes(notes);
|
||||
});
|
||||
|
||||
viewModel.getIsSelectionMode().observe(getViewLifecycleOwner(), isSelection -> {
|
||||
adapter.setSelectionMode(isSelection);
|
||||
});
|
||||
|
||||
viewModel.getSelectedIdsLiveData().observe(getViewLifecycleOwner(), selectedIds -> {
|
||||
adapter.setSelectedIds(selectedIds);
|
||||
});
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onNoteItemClick(int position, long noteId) {
|
||||
if (Boolean.TRUE.equals(viewModel.getIsSelectionMode().getValue())) {
|
||||
boolean isSelected = viewModel.getSelectedIdsLiveData().getValue() != null &&
|
||||
viewModel.getSelectedIdsLiveData().getValue().contains(noteId);
|
||||
viewModel.toggleNoteSelection(noteId, !isSelected);
|
||||
return;
|
||||
}
|
||||
|
||||
if (viewModel.getNotesLiveData().getValue() != null && position < viewModel.getNotesLiveData().getValue().size()) {
|
||||
NotesRepository.NoteInfo note = viewModel.getNotesLiveData().getValue().get(position);
|
||||
if (note.type == Notes.TYPE_FOLDER) {
|
||||
viewModel.enterFolder(note.getId());
|
||||
} else if (note.type == Notes.TYPE_TEMPLATE) {
|
||||
// Apply template: create a new note based on this template
|
||||
viewModel.applyTemplate(note.getId(), new net.micode.notes.data.NotesRepository.Callback<Long>() {
|
||||
@Override
|
||||
public void onSuccess(Long newNoteId) {
|
||||
// Create a temporary NoteInfo to open the editor
|
||||
net.micode.notes.data.NotesRepository.NoteInfo newNote = new net.micode.notes.data.NotesRepository.NoteInfo();
|
||||
newNote.setId(newNoteId);
|
||||
newNote.setParentId(Notes.ID_ROOT_FOLDER);
|
||||
newNote.type = Notes.TYPE_NOTE;
|
||||
|
||||
requireActivity().runOnUiThread(() -> {
|
||||
openNoteEditor(newNote);
|
||||
Toast.makeText(requireContext(), "已根据模板创建新笔记", Toast.LENGTH_SHORT).show();
|
||||
});
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onError(Exception e) {
|
||||
requireActivity().runOnUiThread(() -> {
|
||||
Toast.makeText(requireContext(), "应用模板失败: " + e.getMessage(), Toast.LENGTH_SHORT).show();
|
||||
});
|
||||
}
|
||||
});
|
||||
} else {
|
||||
if (note.isLocked) {
|
||||
pendingNote = note;
|
||||
Intent intent = new Intent(getActivity(), PasswordActivity.class);
|
||||
intent.setAction(PasswordActivity.ACTION_CHECK_PASSWORD);
|
||||
startActivityForResult(intent, REQUEST_CODE_VERIFY_PASSWORD_FOR_OPEN);
|
||||
} else {
|
||||
openNoteEditor(note);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onActivityResult(int requestCode, int resultCode, @Nullable Intent data) {
|
||||
super.onActivityResult(requestCode, resultCode, data);
|
||||
if (requestCode == REQUEST_CODE_VERIFY_PASSWORD_FOR_OPEN && resultCode == android.app.Activity.RESULT_OK) {
|
||||
if (pendingNote != null) {
|
||||
openNoteEditor(pendingNote);
|
||||
pendingNote = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onNoteItemLongClick(int position, long noteId) {
|
||||
if (!Boolean.TRUE.equals(viewModel.getIsSelectionMode().getValue())) {
|
||||
viewModel.setIsSelectionMode(true);
|
||||
viewModel.toggleNoteSelection(noteId, true);
|
||||
} else {
|
||||
boolean isSelected = viewModel.getSelectedIdsLiveData().getValue() != null &&
|
||||
viewModel.getSelectedIdsLiveData().getValue().contains(noteId);
|
||||
viewModel.toggleNoteSelection(noteId, !isSelected);
|
||||
}
|
||||
}
|
||||
|
||||
// Deprecated Context Menu
|
||||
private void showContextMenu(NotesRepository.NoteInfo note) {
|
||||
// ... kept for reference or removed
|
||||
}
|
||||
|
||||
private void openNoteEditor(NotesRepository.NoteInfo note) {
|
||||
Intent intent = new Intent(getActivity(), NoteEditActivity.class);
|
||||
intent.setAction(Intent.ACTION_VIEW);
|
||||
intent.putExtra(Notes.INTENT_EXTRA_FOLDER_ID, note.getParentId());
|
||||
intent.putExtra(Intent.EXTRA_UID, note.getId());
|
||||
startActivityForResult(intent, REQUEST_CODE_OPEN_NODE);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onResume() {
|
||||
super.onResume();
|
||||
viewModel.refreshNotes();
|
||||
}
|
||||
|
||||
// Swipe Menu Callbacks
|
||||
@Override
|
||||
public void onSwipeEdit(long itemId) {
|
||||
android.util.Log.d(TAG, "onSwipeEdit called for itemId: " + itemId);
|
||||
if (viewModel.getNotesLiveData().getValue() != null) {
|
||||
boolean found = false;
|
||||
for (NotesRepository.NoteInfo note : viewModel.getNotesLiveData().getValue()) {
|
||||
if (note.getId() == itemId) {
|
||||
found = true;
|
||||
if (note.type == Notes.TYPE_FOLDER) {
|
||||
onSwipeRename(itemId);
|
||||
} else {
|
||||
openNoteEditor(note);
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (!found) {
|
||||
Toast.makeText(requireContext(), "未找到笔记 (ID: " + itemId + ")", Toast.LENGTH_SHORT).show();
|
||||
}
|
||||
} else {
|
||||
Toast.makeText(requireContext(), "数据未加载", Toast.LENGTH_SHORT).show();
|
||||
}
|
||||
}
|
||||
|
||||
@Override public void onSwipePin(long itemId) { viewModel.toggleNoteSelection(itemId, true); viewModel.toggleSelectedNotesPin(); viewModel.setIsSelectionMode(false); }
|
||||
@Override public void onSwipeMove(long itemId) { /* Show move dialog */ }
|
||||
@Override public void onSwipeDelete(long itemId) { viewModel.deleteNote(itemId); }
|
||||
|
||||
@Override
|
||||
public void onSwipeRename(long itemId) {
|
||||
// Show rename dialog for folder
|
||||
final EditText input = new EditText(requireContext());
|
||||
input.setInputType(InputType.TYPE_CLASS_TEXT);
|
||||
|
||||
// Find current name
|
||||
String currentName = "";
|
||||
if (viewModel.getNotesLiveData().getValue() != null) {
|
||||
for (NotesRepository.NoteInfo note : viewModel.getNotesLiveData().getValue()) {
|
||||
if (note.getId() == itemId) {
|
||||
currentName = note.snippet;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
input.setText(currentName);
|
||||
|
||||
new AlertDialog.Builder(requireContext())
|
||||
.setTitle("重命名文件夹")
|
||||
.setView(input)
|
||||
.setPositiveButton("确定", (dialog, which) -> {
|
||||
String newName = input.getText().toString().trim();
|
||||
if (!newName.isEmpty()) {
|
||||
viewModel.renameFolder(itemId, newName);
|
||||
}
|
||||
})
|
||||
.setNegativeButton("取消", null)
|
||||
.show();
|
||||
}
|
||||
@Override public void onSwipeRestore(long itemId) { viewModel.toggleNoteSelection(itemId, true); viewModel.restoreSelectedNotes(); viewModel.setIsSelectionMode(false); }
|
||||
@Override public void onSwipePermanentDelete(long itemId) { viewModel.toggleNoteSelection(itemId, true); viewModel.deleteSelectedNotesForever(); viewModel.setIsSelectionMode(false); }
|
||||
}
|
||||
@ -0,0 +1,140 @@
|
||||
package net.micode.notes.ui;
|
||||
|
||||
import android.content.Context;
|
||||
import android.database.Cursor;
|
||||
import android.text.format.DateUtils;
|
||||
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.cardview.widget.CardView;
|
||||
import androidx.recyclerview.widget.RecyclerView;
|
||||
|
||||
import net.micode.notes.R;
|
||||
import net.micode.notes.data.Notes;
|
||||
import net.micode.notes.tool.ResourceParser;
|
||||
|
||||
public class NotesRecyclerAdapter extends RecyclerView.Adapter<NotesRecyclerAdapter.NoteViewHolder> {
|
||||
|
||||
private Context mContext;
|
||||
private Cursor mCursor;
|
||||
private OnNoteItemClickListener mListener;
|
||||
private boolean mChoiceMode;
|
||||
|
||||
public interface OnNoteItemClickListener {
|
||||
void onNoteClick(int position, long noteId);
|
||||
boolean onNoteLongClick(int position, long noteId);
|
||||
}
|
||||
|
||||
public NotesRecyclerAdapter(Context context) {
|
||||
mContext = context;
|
||||
}
|
||||
|
||||
public void setOnNoteItemClickListener(OnNoteItemClickListener listener) {
|
||||
mListener = listener;
|
||||
}
|
||||
|
||||
public void swapCursor(Cursor newCursor) {
|
||||
if (mCursor == newCursor) return;
|
||||
if (mCursor != null) {
|
||||
mCursor.close();
|
||||
}
|
||||
mCursor = newCursor;
|
||||
notifyDataSetChanged();
|
||||
}
|
||||
|
||||
public Cursor getCursor() {
|
||||
return mCursor;
|
||||
}
|
||||
|
||||
@NonNull
|
||||
@Override
|
||||
public NoteViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
|
||||
View view = LayoutInflater.from(mContext).inflate(R.layout.note_item, parent, false);
|
||||
return new NoteViewHolder(view);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onBindViewHolder(@NonNull NoteViewHolder holder, int position) {
|
||||
if (mCursor == null || !mCursor.moveToPosition(position)) {
|
||||
return;
|
||||
}
|
||||
NoteItemData itemData = new NoteItemData(mContext, mCursor);
|
||||
holder.bind(itemData, mChoiceMode, false); // Checked logic omitted for now
|
||||
|
||||
holder.itemView.setOnClickListener(v -> {
|
||||
if (mListener != null) {
|
||||
mListener.onNoteClick(position, itemData.getId());
|
||||
}
|
||||
});
|
||||
|
||||
holder.itemView.setOnLongClickListener(v -> {
|
||||
if (mListener != null) {
|
||||
return mListener.onNoteLongClick(position, itemData.getId());
|
||||
}
|
||||
return false;
|
||||
});
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getItemCount() {
|
||||
return mCursor == null ? 0 : mCursor.getCount();
|
||||
}
|
||||
|
||||
class NoteViewHolder extends RecyclerView.ViewHolder {
|
||||
CardView cardView;
|
||||
TextView title, time, name;
|
||||
ImageView typeIcon, lockIcon, alertIcon;
|
||||
CheckBox checkBox;
|
||||
|
||||
public NoteViewHolder(View itemView) {
|
||||
super(itemView);
|
||||
// Since root is CardView
|
||||
cardView = (CardView) itemView;
|
||||
title = itemView.findViewById(R.id.tv_title);
|
||||
time = itemView.findViewById(R.id.tv_time);
|
||||
name = itemView.findViewById(R.id.tv_name);
|
||||
checkBox = itemView.findViewById(android.R.id.checkbox);
|
||||
typeIcon = itemView.findViewById(R.id.iv_type_icon);
|
||||
lockIcon = itemView.findViewById(R.id.iv_lock_icon);
|
||||
alertIcon = itemView.findViewById(R.id.iv_alert_icon);
|
||||
}
|
||||
|
||||
public void bind(NoteItemData data, boolean choiceMode, boolean checked) {
|
||||
if (choiceMode && (data.getType() == Notes.TYPE_NOTE || data.getType() == Notes.TYPE_TEMPLATE)) {
|
||||
checkBox.setVisibility(View.VISIBLE);
|
||||
checkBox.setChecked(checked);
|
||||
} else {
|
||||
checkBox.setVisibility(View.GONE);
|
||||
}
|
||||
|
||||
if (data.getType() == Notes.TYPE_FOLDER) {
|
||||
String snippet = data.getSnippet();
|
||||
if (snippet == null) snippet = "";
|
||||
title.setText(snippet + " (" + data.getNotesCount() + ")");
|
||||
time.setVisibility(View.GONE);
|
||||
typeIcon.setVisibility(View.VISIBLE);
|
||||
typeIcon.setImageResource(R.drawable.ic_folder);
|
||||
cardView.setCardBackgroundColor(mContext.getColor(R.color.bg_white));
|
||||
} else {
|
||||
typeIcon.setVisibility(View.GONE);
|
||||
time.setVisibility(View.VISIBLE);
|
||||
String titleStr = data.getTitle();
|
||||
if (titleStr == null || titleStr.isEmpty()) {
|
||||
titleStr = data.getSnippet();
|
||||
}
|
||||
title.setText(titleStr);
|
||||
time.setText(DateUtils.getRelativeTimeSpanString(data.getModifiedDate()));
|
||||
|
||||
// Background Color
|
||||
int colorId = data.getBgColorId();
|
||||
int color = ResourceParser.getNoteBgColor(mContext, colorId);
|
||||
cardView.setCardBackgroundColor(color);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,30 @@
|
||||
package net.micode.notes.ui;
|
||||
|
||||
import android.os.Bundle;
|
||||
import androidx.appcompat.app.AppCompatActivity;
|
||||
import androidx.preference.PreferenceFragmentCompat;
|
||||
import net.micode.notes.R;
|
||||
|
||||
public class SettingsActivity extends BaseActivity {
|
||||
@Override
|
||||
protected void onCreate(Bundle savedInstanceState) {
|
||||
super.onCreate(savedInstanceState);
|
||||
setContentView(R.layout.activity_settings);
|
||||
if (savedInstanceState == null) {
|
||||
getSupportFragmentManager()
|
||||
.beginTransaction()
|
||||
.replace(R.id.settings_container, new SettingsFragment())
|
||||
.commit();
|
||||
}
|
||||
if (getSupportActionBar() != null) {
|
||||
getSupportActionBar().setDisplayHomeAsUpEnabled(true);
|
||||
getSupportActionBar().setTitle(R.string.menu_settings);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onSupportNavigateUp() {
|
||||
finish();
|
||||
return true;
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,123 @@
|
||||
package net.micode.notes.ui;
|
||||
|
||||
import android.content.Intent;
|
||||
import android.database.Cursor;
|
||||
import android.os.Bundle;
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.fragment.app.Fragment;
|
||||
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 TaskListFragment extends Fragment implements TaskListAdapter.OnTaskItemClickListener {
|
||||
|
||||
private RecyclerView recyclerView;
|
||||
private TaskListAdapter adapter;
|
||||
private FloatingActionButton fab;
|
||||
private static final int REQUEST_EDIT_TASK = 1001;
|
||||
|
||||
@Nullable
|
||||
@Override
|
||||
public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) {
|
||||
View view = inflater.inflate(R.layout.activity_task_list, container, false);
|
||||
|
||||
// Hide Toolbar in fragment if Activity has one or Tabs
|
||||
// For now, let's keep it but remove navigation logic or hide it if needed
|
||||
View toolbar = view.findViewById(R.id.toolbar);
|
||||
if (toolbar != null) {
|
||||
// toolbar.setVisibility(View.GONE); // Optional: Hide if using main tabs
|
||||
}
|
||||
|
||||
recyclerView = view.findViewById(R.id.task_list_view);
|
||||
recyclerView.setLayoutManager(new LinearLayoutManager(getContext()));
|
||||
adapter = new TaskListAdapter(getContext(), this);
|
||||
recyclerView.setAdapter(adapter);
|
||||
|
||||
fab = view.findViewById(R.id.btn_new_task);
|
||||
fab.setOnClickListener(v -> {
|
||||
Intent intent = new Intent(getActivity(), TaskEditActivity.class);
|
||||
startActivityForResult(intent, REQUEST_EDIT_TASK);
|
||||
});
|
||||
|
||||
return view;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onResume() {
|
||||
super.onResume();
|
||||
loadTasks();
|
||||
}
|
||||
|
||||
private void loadTasks() {
|
||||
new Thread(() -> {
|
||||
if (getContext() == null) return;
|
||||
Cursor cursor = getContext().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();
|
||||
}
|
||||
|
||||
if (getActivity() != null) {
|
||||
getActivity().runOnUiThread(() -> adapter.setTasks(tasks));
|
||||
}
|
||||
}).start();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onItemClick(Task task) {
|
||||
Intent intent = new Intent(getActivity(), 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(() -> {
|
||||
if (getContext() != null) {
|
||||
task.save(getContext());
|
||||
if (getActivity() != null) {
|
||||
getActivity().runOnUiThread(() -> loadTasks());
|
||||
}
|
||||
}
|
||||
}).start();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onActivityResult(int requestCode, int resultCode, Intent data) {
|
||||
super.onActivityResult(requestCode, resultCode, data);
|
||||
if (requestCode == REQUEST_EDIT_TASK && resultCode == android.app.Activity.RESULT_OK) {
|
||||
loadTasks();
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,10 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<set xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:duration="300"
|
||||
android:interpolator="@android:anim/accelerate_interpolator">
|
||||
|
||||
<translate
|
||||
android:fromXDelta="0%"
|
||||
android:toXDelta="100%" />
|
||||
|
||||
</set>
|
||||
@ -0,0 +1,10 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<set xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:duration="300"
|
||||
android:interpolator="@android:anim/decelerate_interpolator">
|
||||
|
||||
<translate
|
||||
android:fromXDelta="100%"
|
||||
android:toXDelta="0%" />
|
||||
|
||||
</set>
|
||||
@ -0,0 +1,5 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<selector xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<item android:state_checked="true" android:color="@color/text_color_primary"/>
|
||||
<item android:color="@color/text_color_secondary"/>
|
||||
</selector>
|
||||
@ -0,0 +1,7 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<shape xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<solid android:color="@color/white" />
|
||||
<corners
|
||||
android:topLeftRadius="24dp"
|
||||
android:topRightRadius="24dp" />
|
||||
</shape>
|
||||
@ -0,0 +1,14 @@
|
||||
<selector xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<item android:state_pressed="true">
|
||||
<shape>
|
||||
<solid android:color="#A0000000"/>
|
||||
<corners android:radius="25dp"/>
|
||||
</shape>
|
||||
</item>
|
||||
<item>
|
||||
<shape>
|
||||
<solid android:color="#80000000"/>
|
||||
<corners android:radius="25dp"/>
|
||||
</shape>
|
||||
</item>
|
||||
</selector>
|
||||
@ -0,0 +1,11 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!--
|
||||
Drawer Header Circle Decorator
|
||||
装饰性圆形,用于增加层次感
|
||||
-->
|
||||
<shape xmlns:android="http://schemas.android.com/res/android"
|
||||
android:shape="oval">
|
||||
|
||||
<solid android:color="@color/white" />
|
||||
|
||||
</shape>
|
||||
@ -0,0 +1,15 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<selector xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<item android:state_selected="true">
|
||||
<shape>
|
||||
<solid android:color="#FFD54F"/> <!-- Yellow -->
|
||||
<corners android:radius="16dp"/>
|
||||
</shape>
|
||||
</item>
|
||||
<item>
|
||||
<shape>
|
||||
<solid android:color="@android:color/transparent"/>
|
||||
<corners android:radius="16dp"/>
|
||||
</shape>
|
||||
</item>
|
||||
</selector>
|
||||
@ -0,0 +1,17 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!--
|
||||
Account Circle Icon - Material Design 3
|
||||
用户头像占位符
|
||||
-->
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="64dp"
|
||||
android:height="64dp"
|
||||
android:viewportWidth="24"
|
||||
android:viewportHeight="24"
|
||||
android:tint="?attr/colorPrimary">
|
||||
|
||||
<path
|
||||
android:fillColor="@android:color/white"
|
||||
android:pathData="M12,2C6.48,2 2,6.48 2,12s4.48,10 10,10 10,-4.48 10,-10S17.52,2 12,2zM12,5c1.66,0 3,1.34 3,3s-1.34,3 -3,3 -3,-1.34 -3,-3 1.34,-3 3,-3zM12,19.2c-2.5,0 -4.71,-1.28 -6,-3.22 0.03,-1.99 4,-3.08 6,-3.08 1.99,0 5.97,1.09 6,3.08 -1.29,1.94 -3.5,3.22 -6,3.22z" />
|
||||
|
||||
</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="#FFC107"
|
||||
android:pathData="M12,2C6.48,2 2,6.48 2,12s4.48,10 10,10 10,-4.48 10,-10S17.52,2 12,2z"/>
|
||||
<path
|
||||
android:fillColor="#FFFFFF"
|
||||
android:pathData="M9,16.17L4.83,12l-1.42,1.41L9,19 21,7l-1.41,-1.41z"/>
|
||||
</vector>
|
||||
@ -0,0 +1,11 @@
|
||||
<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="#00000000"
|
||||
android:strokeColor="#757575"
|
||||
android:strokeWidth="2"
|
||||
android:pathData="M12,2C6.48,2 2,6.48 2,12s4.48,10 10,10 10,-4.48 10,-10S17.52,2 12,2z"/>
|
||||
</vector>
|
||||
@ -0,0 +1,17 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!--
|
||||
Cloud Done Icon - Material Design 3
|
||||
云同步完成图标
|
||||
-->
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
android:viewportWidth="24"
|
||||
android:viewportHeight="24"
|
||||
android:tint="@color/white">
|
||||
|
||||
<path
|
||||
android:fillColor="@android:color/white"
|
||||
android:pathData="M19.35,10.04C18.67,6.59 15.64,4 12,4C9.11,4 6.6,5.64 5.35,8.04C2.34,8.36 0,10.91 0,14c0,3.31 2.69,6 6,6h13c2.76,0 5,-2.24 5,-5C24,12.36 21.95,10.22 19.35,10.04zM10,17l-3.5,-3.5l1.41,-1.41L10,14.17l5.09,-5.09L16.5,10.5L10,17z" />
|
||||
|
||||
</vector>
|
||||
@ -0,0 +1,25 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!--
|
||||
Cloud Settings Icon - Material Design 3
|
||||
云设置图标
|
||||
-->
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
android:viewportWidth="24"
|
||||
android:viewportHeight="24"
|
||||
android:tint="?attr/colorOnSurfaceVariant">
|
||||
|
||||
<path
|
||||
android:fillColor="@android:color/white"
|
||||
android:pathData="M19.35,10.04C18.67,6.59 15.64,4 12,4C9.11,4 6.6,5.64 5.35,8.04C2.34,8.36 0,10.91 0,14c0,3.31 2.69,6 6,6h13c2.76,0 5,-2.24 5,-5C24,12.36 21.95,10.22 19.35,10.04zM19,18H6c-2.21,0 -4,-1.79 -4,-4c0,-2.05 1.53,-3.76 3.56,-3.97l1.07,-0.11l0.5,-0.95C8.08,7.14 9.94,6 12,6c2.62,0 4.88,1.86 5.39,4.43l0.3,1.5l1.53,0.11C20.78,12.14 22,13.45 22,15C22,16.65 20.65,18 19,18z" />
|
||||
|
||||
<path
|
||||
android:fillColor="@android:color/white"
|
||||
android:pathData="M10.59,9.17L5.41,14.34 6.83,15.76 12.01,10.59z" />
|
||||
|
||||
<path
|
||||
android:fillColor="@android:color/white"
|
||||
android:pathData="M13.41,9.17L12,10.59 17.18,15.76 18.59,14.34z" />
|
||||
|
||||
</vector>
|
||||
@ -0,0 +1,17 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!--
|
||||
Delete Icon - Material Design 3
|
||||
删除/回收站图标
|
||||
-->
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
android:viewportWidth="24"
|
||||
android:viewportHeight="24"
|
||||
android:tint="?attr/colorOnSurfaceVariant">
|
||||
|
||||
<path
|
||||
android:fillColor="@android:color/white"
|
||||
android:pathData="M6,19c0,1.1 0.9,2 2,2h8c1.1,0 2,-0.9 2,-2V7H6v12zM19,4h-3.5l-1,-1h-5l-1,1H5v2h14V4z" />
|
||||
|
||||
</vector>
|
||||
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in new issue