Compare commits
No commits in common. 'master' and 'jiangtianxiang_branch' have entirely different histories.
master
...
jiangtianx
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@ -0,0 +1,6 @@
|
||||
{
|
||||
"name": "git",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {}
|
||||
}
|
||||
@ -1,3 +0,0 @@
|
||||
# Default ignored files
|
||||
/shelf/
|
||||
/workspace.xml
|
||||
@ -1,6 +0,0 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="ProjectRootManager">
|
||||
<output url="file://$PROJECT_DIR$/out" />
|
||||
</component>
|
||||
</project>
|
||||
@ -1,8 +0,0 @@
|
||||
<?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>
|
||||
@ -1,6 +0,0 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="VcsDirectoryMappings">
|
||||
<mapping directory="$PROJECT_DIR$/.." vcs="Git" />
|
||||
</component>
|
||||
</project>
|
||||
@ -1,28 +0,0 @@
|
||||
<?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>
|
||||
@ -1,6 +0,0 @@
|
||||
<?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,58 +1,18 @@
|
||||
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
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,27 +0,0 @@
|
||||
/*
|
||||
* 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);
|
||||
}
|
||||
@ -1,394 +0,0 @@
|
||||
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();
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -1,67 +0,0 @@
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -1,92 +0,0 @@
|
||||
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);
|
||||
}
|
||||
}
|
||||
@ -1,12 +0,0 @@
|
||||
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));
|
||||
}
|
||||
}
|
||||
@ -1,71 +0,0 @@
|
||||
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();
|
||||
}
|
||||
}
|
||||
@ -1,33 +0,0 @@
|
||||
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);
|
||||
}
|
||||
}
|
||||
@ -1,191 +0,0 @@
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -1,91 +0,0 @@
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -1,164 +0,0 @@
|
||||
/*
|
||||
* 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() {
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -1,291 +0,0 @@
|
||||
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); }
|
||||
}
|
||||
@ -1,140 +0,0 @@
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -1,30 +0,0 @@
|
||||
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;
|
||||
}
|
||||
}
|
||||
@ -1,123 +0,0 @@
|
||||
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();
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -1,10 +0,0 @@
|
||||
<?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>
|
||||
@ -1,10 +0,0 @@
|
||||
<?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>
|
||||
@ -1,5 +0,0 @@
|
||||
<?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>
|
||||
@ -1,7 +0,0 @@
|
||||
<?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>
|
||||
@ -1,14 +0,0 @@
|
||||
<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>
|
||||
@ -1,11 +0,0 @@
|
||||
<?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>
|
||||
@ -1,15 +0,0 @@
|
||||
<?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>
|
||||
@ -1,17 +0,0 @@
|
||||
<?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>
|
||||
@ -1,12 +0,0 @@
|
||||
<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>
|
||||
@ -1,11 +0,0 @@
|
||||
<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>
|
||||
@ -1,17 +0,0 @@
|
||||
<?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>
|
||||
@ -1,25 +0,0 @@
|
||||
<?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>
|
||||
@ -1,17 +0,0 @@
|
||||
<?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