添加侧边栏,实现部分文件夹功能。但是界面存在部分问题

pull/12/head
包尔俊 1 month ago
parent 11217cb843
commit 607af6d090

@ -4,6 +4,14 @@
<selectionStates>
<SelectionState runConfigName="app">
<option name="selectionMode" value="DROPDOWN" />
<DropdownSelection timestamp="2026-01-19T06:52:55.276577900Z">
<Target type="DEFAULT_BOOT">
<handle>
<DeviceId pluginId="LocalEmulator" identifier="path=C:\Users\啊?\.android\avd\Pixel_4a_API_31.avd" />
</handle>
</Target>
</DropdownSelection>
<DialogSelection />
</SelectionState>
</selectionStates>
</component>

@ -4,9 +4,7 @@ plugins {
android {
namespace = "net.micode.notes"
compileSdk {
version = release(36)
}
compileSdk = 36
buildFeatures {
viewBinding = true
}
@ -30,6 +28,13 @@ android {
)
}
}
testOptions {
unitTests {
isIncludeAndroidResources = false
isReturnDefaultValues = true
}
}
compileOptions {
sourceCompatibility = JavaVersion.VERSION_11
targetCompatibility = JavaVersion.VERSION_11
@ -53,6 +58,8 @@ dependencies {
implementation(files("D:\\ke\\software_enginering\\httpcomponents-client-4.5.14-bin\\lib\\httpclient-win-4.5.14.jar"))
implementation(files("D:\\ke\\software_enginering\\httpcomponents-client-4.5.14-bin\\lib\\httpcore-4.4.16.jar"))
testImplementation(libs.junit)
testImplementation(libs.mockito.core)
testImplementation(libs.mockito.junit)
androidTestImplementation(libs.ext.junit)
androidTestImplementation(libs.espresso.core)
}

@ -60,7 +60,7 @@
android:name=".ui.NoteEditActivity"
android:configChanges="keyboardHidden|orientation|screenSize"
android:launchMode="singleTop"
android:theme="@style/NoteTheme"
android:theme="@style/Theme.Notesmaster.Edit"
android:exported="true">
<!-- 意图过滤器:允许查看笔记 -->

@ -8,6 +8,8 @@ import androidx.core.graphics.Insets;
import androidx.core.view.ViewCompat;
import androidx.core.view.WindowInsetsCompat;
import net.micode.notes.ui.SidebarFragment;
/**
*
* <p>
@ -15,14 +17,14 @@ import androidx.core.view.WindowInsetsCompat;
*
* </p>
*/
public class MainActivity extends AppCompatActivity {
public class MainActivity extends AppCompatActivity implements SidebarFragment.OnSidebarItemSelectedListener {
/**
*
* <p>
*
* </p>
*
*
* @param savedInstanceState
*/
@Override
@ -32,7 +34,7 @@ public class MainActivity extends AppCompatActivity {
EdgeToEdge.enable(this);
setContentView(R.layout.activity_main);
// 设置窗口边距监听器,自动适配系统栏
ViewCompat.setOnApplyWindowInsetsListener(findViewById(R.id.main), (v, insets) -> {
ViewCompat.setOnApplyWindowInsetsListener(findViewById(R.id.main_content), (v, insets) -> {
// 获取系统栏边距
Insets systemBars = insets.getInsets(WindowInsetsCompat.Type.systemBars());
// 设置视图内边距以适配系统栏
@ -40,4 +42,59 @@ public class MainActivity extends AppCompatActivity {
return insets;
});
}
// ==================== SidebarFragment.OnSidebarItemSelectedListener 实现 ====================
@Override
public void onFolderSelected(long folderId) {
// TODO: 实现跳转到指定文件夹
// 关闭侧栏
closeSidebar();
}
@Override
public void onTrashSelected() {
// TODO: 实现跳转到回收站
// 关闭侧栏
closeSidebar();
}
@Override
public void onSyncSelected() {
// TODO: 实现同步功能
}
@Override
public void onLoginSelected() {
// TODO: 实现登录功能
}
@Override
public void onExportSelected() {
// TODO: 实现导出功能
}
@Override
public void onSettingsSelected() {
// TODO: 实现设置功能
}
@Override
public void onCreateFolder() {
// TODO: 实现创建文件夹功能
}
@Override
public void onCloseSidebar() {
closeSidebar();
}
// ==================== 私有方法 ====================
/**
*
*/
private void closeSidebar() {
// TODO: 实现侧栏关闭功能
}
}

@ -471,7 +471,7 @@ public class NotesDatabaseHelper extends SQLiteOpenHelper {
* @param context
* @return NotesDatabaseHelper
*/
static synchronized NotesDatabaseHelper getInstance(Context context) {
public static synchronized NotesDatabaseHelper getInstance(Context context) {
if (mInstance == null) {
mInstance = new NotesDatabaseHelper(context);
}

@ -186,22 +186,29 @@ public class NotesRepository {
/**
*
* <p>
* 便便
* </p>
*
* @param folderId ID
* @return
* @return 便
*/
private List<NoteInfo> queryNotes(long folderId) {
List<NoteInfo> notes = new ArrayList<>();
List<NoteInfo> folders = new ArrayList<>();
List<NoteInfo> normalNotes = new ArrayList<>();
String selection;
String[] selectionArgs;
if (folderId == Notes.ID_ROOT_FOLDER) {
// 根文件夹:显示所有非系统笔记和有内容的通话记录文件夹
selection = ROOT_FOLDER_SELECTION;
// 根文件夹:显示所有文件夹和便签
selection = "(" + NoteColumns.TYPE + "<>" + Notes.TYPE_SYSTEM + " AND " + NoteColumns.PARENT_ID + "=?) OR (" +
NoteColumns.ID + "=" + Notes.ID_CALL_RECORD_FOLDER + " AND " + NoteColumns.NOTES_COUNT + ">0)";
selectionArgs = new String[]{String.valueOf(Notes.ID_ROOT_FOLDER)};
} else {
// 子文件夹:只显示该文件夹下的笔记
selection = NORMAL_SELECTION;
// 子文件夹:显示该文件夹下的文件夹和便签
selection = NoteColumns.PARENT_ID + "=? AND " + NoteColumns.TYPE + "<>" + Notes.TYPE_SYSTEM;
selectionArgs = new String[]{String.valueOf(folderId)};
}
@ -216,17 +223,198 @@ public class NotesRepository {
if (cursor != null) {
try {
while (cursor.moveToNext()) {
notes.add(noteFromCursor(cursor));
NoteInfo note = noteFromCursor(cursor);
if (note.type == Notes.TYPE_FOLDER) {
// 文件夹单独收集
folders.add(note);
} else if (note.type == Notes.TYPE_NOTE) {
// 便签收集
normalNotes.add(note);
} else if (note.type == Notes.TYPE_SYSTEM && note.id == Notes.ID_CALL_RECORD_FOLDER) {
// 通话记录文件夹
folders.add(note);
}
}
Log.d(TAG, "Query returned " + cursor.getCount() + " notes");
Log.d(TAG, "Query returned " + folders.size() + " folders and " + normalNotes.size() + " notes");
} finally {
cursor.close();
}
}
// 文件夹按修改时间倒序排列
folders.sort((a, b) -> Long.compare(b.modifiedDate, a.modifiedDate));
// 便签按修改时间倒序排列
normalNotes.sort((a, b) -> Long.compare(b.modifiedDate, a.modifiedDate));
// 合并:文件夹在前,便签在后
notes.addAll(folders);
notes.addAll(normalNotes);
return notes;
}
/**
*
*
* @param folderId ID
* @return null
*/
public NoteInfo getFolderInfo(long folderId) {
if (folderId == Notes.ID_ROOT_FOLDER) {
NoteInfo root = new NoteInfo();
root.id = Notes.ID_ROOT_FOLDER;
root.title = "我的便签";
root.snippet = "我的便签";
root.type = Notes.TYPE_FOLDER;
return root;
}
String selection = NoteColumns.ID + "=?";
String[] selectionArgs = new String[]{String.valueOf(folderId)};
Cursor cursor = contentResolver.query(
Notes.CONTENT_NOTE_URI,
null,
selection,
selectionArgs,
null
);
if (cursor != null) {
try {
if (cursor.moveToFirst()) {
return noteFromCursor(cursor);
}
} finally {
cursor.close();
}
}
return null;
}
/**
* ID
*
* @param folderId ID
* @param callback ID
*/
public void getParentFolderId(long folderId, Callback<Long> callback) {
executor.execute(() -> {
try {
long parentId = getParentFolderId(folderId);
callback.onSuccess(parentId);
} catch (Exception e) {
callback.onError(e);
}
});
}
/**
* ID
*
* @param folderId ID
* @return IDID
*/
public long getParentFolderId(long folderId) {
if (folderId == Notes.ID_ROOT_FOLDER || folderId == Notes.ID_CALL_RECORD_FOLDER) {
return Notes.ID_ROOT_FOLDER;
}
NoteInfo folder = getFolderInfo(folderId);
if (folder != null) {
return folder.parentId;
}
return Notes.ID_ROOT_FOLDER;
}
/**
*
*
* @param folderId ID
* @return
*/
public List<NoteInfo> getFolderPath(long folderId) {
List<NoteInfo> path = new ArrayList<>();
long currentId = folderId;
while (currentId != Notes.ID_ROOT_FOLDER) {
NoteInfo folder = getFolderInfo(currentId);
if (folder == null) {
break;
}
path.add(0, folder); // 添加到列表头部
currentId = folder.parentId;
}
// 添加根文件夹
NoteInfo root = new NoteInfo();
root.id = Notes.ID_ROOT_FOLDER;
root.title = "我的便签";
root.snippet = "我的便签";
root.type = Notes.TYPE_FOLDER;
path.add(0, root);
return path;
}
/**
*
*
* @param folderId ID
* @param callback
*/
public void getFolderPath(long folderId, Callback<List<NoteInfo>> callback) {
executor.execute(() -> {
try {
List<NoteInfo> path = getFolderPath(folderId);
callback.onSuccess(path);
} catch (Exception e) {
callback.onError(e);
}
});
}
/**
*
*
* @param parentId ID
* @param name
* @param callback ID
*/
public void createFolder(long parentId, String name, Callback<Long> callback) {
executor.execute(() -> {
try {
ContentValues values = new ContentValues();
long currentTime = System.currentTimeMillis();
values.put(NoteColumns.PARENT_ID, parentId);
values.put(NoteColumns.TYPE, Notes.TYPE_FOLDER);
values.put(NoteColumns.SNIPPET, name);
values.put(NoteColumns.CREATED_DATE, currentTime);
values.put(NoteColumns.MODIFIED_DATE, currentTime);
values.put(NoteColumns.LOCAL_MODIFIED, 1);
values.put(NoteColumns.NOTES_COUNT, 0);
Uri uri = contentResolver.insert(Notes.CONTENT_NOTE_URI, values);
Long folderId = 0L;
if (uri != null) {
try {
folderId = ContentUris.parseId(uri);
} catch (Exception e) {
Log.e(TAG, "Failed to parse folder ID from URI", e);
}
}
callback.onSuccess(folderId);
Log.d(TAG, "Successfully created folder: " + name + " with ID: " + folderId);
} catch (Exception e) {
Log.e(TAG, "Failed to create folder: " + name, e);
callback.onError(e);
}
});
}
/**
*
* <p>

@ -71,8 +71,11 @@ import java.util.Map;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import androidx.appcompat.app.AppCompatActivity;
import com.google.android.material.appbar.MaterialToolbar;
public class NoteEditActivity extends Activity implements OnClickListener,
public class NoteEditActivity extends AppCompatActivity implements OnClickListener,
NoteSettingChangedListener, OnTextViewChangeListener {
/**
*
@ -143,6 +146,8 @@ public class NoteEditActivity extends Activity implements OnClickListener,
private SharedPreferences mSharedPrefs;
private int mFontSizeId;
private MaterialToolbar toolbar;
private static final String PREFERENCE_FONT_SIZE = "pref_font_size";
private static final int SHORTCUT_ICON_TITLE_MAX_LEN = 10;
@ -160,6 +165,15 @@ public class NoteEditActivity extends Activity implements OnClickListener,
super.onCreate(savedInstanceState);
this.setContentView(R.layout.note_edit);
// 初始化Toolbar使用MaterialToolbar与列表页面一致
MaterialToolbar toolbar = findViewById(R.id.toolbar);
setSupportActionBar(toolbar);
if (getSupportActionBar() != null) {
getSupportActionBar().setDisplayHomeAsUpEnabled(true);
getSupportActionBar().setDisplayShowHomeEnabled(true);
}
toolbar.setNavigationOnClickListener(v -> finish());
if (savedInstanceState == null && !initActivityState(getIntent())) {
finish();
return;
@ -284,6 +298,49 @@ public class NoteEditActivity extends Activity implements OnClickListener,
return true;
}
/**
*
* <p>
* UI
* </p>
*/
private void initResources() {
mHeadViewPanel = findViewById(R.id.note_title);
mNoteHeaderHolder = new HeadViewHolder();
mNoteHeaderHolder.tvModified = findViewById(R.id.tv_modified_date);
mNoteHeaderHolder.ivAlertIcon = findViewById(R.id.iv_alert_icon);
mNoteHeaderHolder.tvAlertDate = findViewById(R.id.tv_alert_date);
mNoteHeaderHolder.ibSetBgColor = findViewById(R.id.btn_set_bg_color);
mNoteHeaderHolder.ibSetBgColor.setOnClickListener(this);
mNoteEditor = findViewById(R.id.note_edit_view);
mNoteEditorPanel = findViewById(R.id.sv_note_edit);
mNoteBgColorSelector = findViewById(R.id.note_bg_color_selector);
// 设置背景颜色选择器的点击事件
for (int id : sBgSelectorBtnsMap.keySet()) {
ImageView iv = findViewById(id);
iv.setOnClickListener(this);
}
mFontSizeSelector = findViewById(R.id.font_size_selector);
for (int id : sFontSizeBtnsMap.keySet()) {
View view = findViewById(id);
view.setOnClickListener(this);
}
mSharedPrefs = PreferenceManager.getDefaultSharedPreferences(this);
mFontSizeId = mSharedPrefs.getInt(PREFERENCE_FONT_SIZE, ResourceParser.BG_DEFAULT_FONT_SIZE);
/**
* HACKME: Fix bug of store the resource id in shared preference.
* The id may larger than the length of resources, in this case,
* return the {@link ResourceParser#BG_DEFAULT_FONT_SIZE}
*/
if (mFontSizeId >= TextAppearanceResources.getResourcesSize()) {
mFontSizeId = ResourceParser.BG_DEFAULT_FONT_SIZE;
}
mEditTextList = findViewById(R.id.note_edit_list);
}
@Override
protected void onResume() {
super.onResume();
@ -430,47 +487,6 @@ public class NoteEditActivity extends Activity implements OnClickListener,
return true;
}
/**
*
* <p>
* UI
* SharedPreferences
* </p>
*/
private void initResources() {
mHeadViewPanel = findViewById(R.id.note_title);
mNoteHeaderHolder = new HeadViewHolder();
mNoteHeaderHolder.tvModified = (TextView) findViewById(R.id.tv_modified_date);
mNoteHeaderHolder.ivAlertIcon = (ImageView) findViewById(R.id.iv_alert_icon);
mNoteHeaderHolder.tvAlertDate = (TextView) findViewById(R.id.tv_alert_date);
mNoteHeaderHolder.ibSetBgColor = (ImageView) findViewById(R.id.btn_set_bg_color);
mNoteHeaderHolder.ibSetBgColor.setOnClickListener(this);
mNoteEditor = (EditText) findViewById(R.id.note_edit_view);
mNoteEditorPanel = findViewById(R.id.sv_note_edit);
mNoteBgColorSelector = findViewById(R.id.note_bg_color_selector);
for (int id : sBgSelectorBtnsMap.keySet()) {
ImageView iv = (ImageView) findViewById(id);
iv.setOnClickListener(this);
}
mFontSizeSelector = findViewById(R.id.font_size_selector);
for (int id : sFontSizeBtnsMap.keySet()) {
View view = findViewById(id);
view.setOnClickListener(this);
};
mSharedPrefs = PreferenceManager.getDefaultSharedPreferences(this);
mFontSizeId = mSharedPrefs.getInt(PREFERENCE_FONT_SIZE, ResourceParser.BG_DEFAULT_FONT_SIZE);
/**
* HACKME: Fix bug of store the resource id in shared preference.
* The id may larger than the length of resources, in this case,
* return the {@link ResourceParser#BG_DEFAULT_FONT_SIZE}
*/
if(mFontSizeId >= TextAppearanceResources.getResourcesSize()) {
mFontSizeId = ResourceParser.BG_DEFAULT_FONT_SIZE;
}
mEditTextList = (LinearLayout) findViewById(R.id.note_edit_list);
}
/**
*
* <p>
@ -528,7 +544,7 @@ public class NoteEditActivity extends Activity implements OnClickListener,
if (id == R.id.btn_set_bg_color) {
mNoteBgColorSelector.setVisibility(View.VISIBLE);
findViewById(sBgSelectorSelectionMap.get(mWorkingNote.getBgColorId())).setVisibility(
- View.VISIBLE);
View.VISIBLE);
} else if (sBgSelectorBtnsMap.containsKey(id)) {
findViewById(sBgSelectorSelectionMap.get(mWorkingNote.getBgColorId())).setVisibility(
View.GONE);

@ -22,18 +22,31 @@ import android.content.Context;
import android.content.DialogInterface;
import android.content.Intent;
import android.os.Bundle;
import android.text.InputFilter;
import android.text.TextUtils;
import android.util.Log;
import androidx.appcompat.view.ActionMode;
import android.view.Menu;
import android.view.MenuItem;
import android.view.View;
import android.view.WindowInsets;
import android.view.WindowInsetsController;
import android.view.WindowManager;
import android.widget.AdapterView;
import android.widget.Button;
import android.widget.EditText;
import android.widget.LinearLayout;
import android.widget.ListView;
import android.widget.PopupMenu;
import android.widget.TextView;
import android.widget.Toast;
import androidx.appcompat.app.AppCompatActivity;
import androidx.core.graphics.Insets;
import androidx.core.view.ViewCompat;
import androidx.core.view.WindowCompat;
import androidx.core.view.WindowInsetsCompat;
import androidx.drawerlayout.widget.DrawerLayout;
import androidx.lifecycle.Observer;
import androidx.lifecycle.ViewModel;
import androidx.lifecycle.ViewModelProvider;
@ -64,7 +77,8 @@ import java.util.List;
public class NotesListActivity extends AppCompatActivity
implements NoteInfoAdapter.OnNoteButtonClickListener,
NoteInfoAdapter.OnNoteItemClickListener,
NoteInfoAdapter.OnNoteItemLongClickListener {
NoteInfoAdapter.OnNoteItemLongClickListener,
SidebarFragment.OnSidebarItemSelectedListener {
private static final String TAG = "NotesListActivity";
private static final int REQUEST_CODE_OPEN_NODE = 102;
private static final int REQUEST_CODE_NEW_NODE = 103;
@ -73,7 +87,13 @@ public class NotesListActivity extends AppCompatActivity
private ListView notesListView;
private androidx.appcompat.widget.Toolbar toolbar;
private NoteInfoAdapter adapter;
private androidx.appcompat.view.ActionMode actionMode;
private DrawerLayout drawerLayout;
private FloatingActionButton fabNewNote;
private LinearLayout breadcrumbContainer;
private LinearLayout breadcrumbItems;
// 多选模式状态
private boolean isMultiSelectMode = false;
/**
*
@ -86,8 +106,21 @@ public class NotesListActivity extends AppCompatActivity
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
// 启用边缘到边缘显示
WindowCompat.setDecorFitsSystemWindows(getWindow(), false);
setContentView(R.layout.note_list);
// 处理窗口insets状态栏和导航栏
View mainView = findViewById(android.R.id.content);
ViewCompat.setOnApplyWindowInsetsListener(mainView, (v, windowInsets) -> {
Insets insets = windowInsets.getInsets(WindowInsetsCompat.Type.systemBars());
// 设置内容区域的padding以避免被状态栏遮挡
v.setPadding(insets.left, insets.top, insets.right, insets.bottom);
return WindowInsetsCompat.CONSUMED;
});
initViewModel();
initViews();
observeViewModel();
@ -129,6 +162,11 @@ public class NotesListActivity extends AppCompatActivity
private void initViews() {
notesListView = findViewById(R.id.notes_list);
toolbar = findViewById(R.id.toolbar);
drawerLayout = findViewById(R.id.drawer_layout);
// 初始化面包屑导航
breadcrumbContainer = findViewById(R.id.breadcrumb_container);
breadcrumbItems = findViewById(R.id.breadcrumb_items);
// 设置适配器
adapter = new NoteInfoAdapter(this);
@ -144,7 +182,7 @@ public class NotesListActivity extends AppCompatActivity
Object item = parent.getItemAtPosition(position);
if (item instanceof NotesRepository.NoteInfo) {
NotesRepository.NoteInfo note = (NotesRepository.NoteInfo) item;
openNoteEditor(note);
handleItemClick(note, position);
}
}
});
@ -156,18 +194,58 @@ public class NotesListActivity extends AppCompatActivity
getSupportActionBar().setTitle(R.string.app_name);
}
// 初始化为普通模式
updateToolbarForNormalMode();
// 设置 Toolbar 的汉堡菜单按钮点击监听器(打开侧栏)
toolbar.setNavigationOnClickListener(v -> {
if (drawerLayout != null) {
drawerLayout.openDrawer(findViewById(R.id.sidebar_fragment));
}
});
// Set FAB click event
FloatingActionButton fabNewNote = findViewById(R.id.btn_new_note);
fabNewNote = findViewById(R.id.btn_new_note);
if (fabNewNote != null) {
fabNewNote.setOnClickListener(v -> {
Intent intent = new Intent(NotesListActivity.this, NoteEditActivity.class);
intent.setAction(Intent.ACTION_INSERT_OR_EDIT);
intent.putExtra(Notes.INTENT_EXTRA_FOLDER_ID, Notes.ID_ROOT_FOLDER);
intent.putExtra(Notes.INTENT_EXTRA_FOLDER_ID, viewModel.getCurrentFolderId());
startActivityForResult(intent, REQUEST_CODE_NEW_NODE);
});
}
}
/**
*
* <p>
* 便
* </p>
*
* @param note
* @param position
*/
private void handleItemClick(NotesRepository.NoteInfo note, int position) {
if (isMultiSelectMode) {
// 多选模式:切换选中状态
boolean isSelected = viewModel.getSelectedNoteIds().contains(note.getId());
viewModel.toggleNoteSelection(note.getId(), !isSelected);
if (adapter != null) {
adapter.setSelectedIds(viewModel.getSelectedNoteIds());
}
updateToolbarForMultiSelectMode();
} else {
// 普通模式
if (note.type == Notes.TYPE_FOLDER) {
// 文件夹:进入该文件夹
viewModel.enterFolder(note.getId());
} else {
// 便签:打开编辑器
openNoteEditor(note);
}
}
}
/**
* ViewModelLiveData
*/
@ -197,6 +275,74 @@ public class NotesListActivity extends AppCompatActivity
}
}
});
// 观察文件夹路径(用于面包屑导航)
viewModel.getFolderPathLiveData().observe(this, new Observer<List<NotesRepository.NoteInfo>>() {
@Override
public void onChanged(List<NotesRepository.NoteInfo> path) {
updateBreadcrumb(path);
}
});
// 观察侧栏刷新通知
viewModel.getSidebarRefreshNeeded().observe(this, new Observer<Boolean>() {
@Override
public void onChanged(Boolean refreshNeeded) {
if (refreshNeeded != null && refreshNeeded) {
// 通知侧栏刷新
SidebarFragment sidebarFragment = (SidebarFragment) getSupportFragmentManager()
.findFragmentById(R.id.sidebar_fragment);
if (sidebarFragment != null) {
sidebarFragment.refreshFolderTree();
}
// 重置刷新状态
viewModel.getSidebarRefreshNeeded().setValue(false);
}
}
});
}
/**
*
*
* @param path
*/
private void updateBreadcrumb(List<NotesRepository.NoteInfo> path) {
if (breadcrumbItems == null || path == null) {
return;
}
breadcrumbItems.removeAllViews();
for (int i = 0; i < path.size(); i++) {
NotesRepository.NoteInfo folder = path.get(i);
// 如果不是第一个,添加分隔符 " > "
if (i > 0) {
TextView separator = new TextView(this);
separator.setText(" > ");
separator.setTextSize(14);
separator.setTextColor(android.R.color.darker_gray);
breadcrumbItems.addView(separator);
}
// 创建面包屑项
TextView breadcrumbItem = (TextView) getLayoutInflater()
.inflate(R.layout.breadcrumb_item, breadcrumbItems, false);
breadcrumbItem.setText(folder.title);
// 如果是当前文件夹(最后一个),高亮显示且不可点击
if (i == path.size() - 1) {
breadcrumbItem.setTextColor(getColor(R.color.primary_color));
breadcrumbItem.setEnabled(false);
} else {
// 其他层级可以点击跳转
final long targetFolderId = folder.id;
breadcrumbItem.setOnClickListener(v -> viewModel.enterFolder(targetFolderId));
}
breadcrumbItems.addView(breadcrumbItem);
}
}
/**
@ -252,26 +398,36 @@ public class NotesListActivity extends AppCompatActivity
public void onNoteItemClick(int position, long noteId) {
Log.d(TAG, "===== onNoteItemClick CALLED =====");
Log.d(TAG, "position: " + position + ", noteId: " + noteId);
if (actionMode != null) {
Log.d(TAG, "ActionMode is active, toggling selection");
if (isMultiSelectMode) {
Log.d(TAG, "Multi-select mode active, toggling selection");
NotesRepository.NoteInfo note = (NotesRepository.NoteInfo) adapter.getItem(position);
if (note != null) {
boolean isSelected = viewModel.getSelectedNoteIds().contains(note.getId());
viewModel.toggleNoteSelection(note.getId(), !isSelected);
if (adapter != null) {
adapter.setSelectedIds(viewModel.getSelectedNoteIds());
}
// 更新toolbar标题
updateToolbarForMultiSelectMode();
}
Log.d(TAG, "===== onNoteItemClick END (multi-select mode) =====");
} else {
Log.d(TAG, "ActionMode is not active, opening editor");
Log.d(TAG, "Normal mode, checking item type");
NotesRepository.NoteInfo note = (NotesRepository.NoteInfo) adapter.getItem(position);
if (note != null) {
openNoteEditor(note);
if (note.type == Notes.TYPE_FOLDER) {
// 文件夹:进入该文件夹
Log.d(TAG, "Folder clicked, entering folder: " + note.getId());
viewModel.enterFolder(note.getId());
} else {
// 便签:打开编辑器
Log.d(TAG, "Note clicked, opening editor");
openNoteEditor(note);
}
}
Log.d(TAG, "===== onNoteItemClick END (editor mode) =====");
Log.d(TAG, "===== onNoteItemClick END =====");
}
}
@ -279,91 +435,114 @@ public class NotesListActivity extends AppCompatActivity
public void onNoteItemLongClick(int position, long noteId) {
Log.d(TAG, "===== onNoteItemLongClick CALLED =====");
Log.d(TAG, "position: " + position + ", noteId: " + noteId);
if (actionMode == null) {
Log.d(TAG, "Starting ActionMode manually");
actionMode = startSupportActionMode(new androidx.appcompat.view.ActionMode.Callback() {
@Override
public boolean onCreateActionMode(androidx.appcompat.view.ActionMode mode, Menu menu) {
Log.d(TAG, "onCreateActionMode called");
mode.getMenuInflater().inflate(R.menu.note_list_options, menu);
return true;
}
@Override
public boolean onPrepareActionMode(androidx.appcompat.view.ActionMode mode, Menu menu) {
return false;
}
@Override
public boolean onActionItemClicked(androidx.appcompat.view.ActionMode mode, MenuItem item) {
Log.d(TAG, "onActionItemClicked: " + item.getTitle());
int itemId = item.getItemId();
if (itemId == R.id.delete) {
showDeleteDialog();
} else if (itemId == R.id.move) {
showMoveMenu();
}
return true;
}
@Override
public void onDestroyActionMode(androidx.appcompat.view.ActionMode mode) {
Log.d(TAG, "onDestroyActionMode called");
actionMode = null;
viewModel.clearSelection();
if (adapter != null) {
adapter.setSelectedIds(new java.util.HashSet<>());
}
adapter.notifyDataSetChanged();
}
});
if (!isMultiSelectMode) {
Log.d(TAG, "Entering multi-select mode");
enterMultiSelectMode();
viewModel.toggleNoteSelection(noteId, true);
if (adapter != null) {
adapter.setSelectedIds(viewModel.getSelectedNoteIds());
}
updateSelectionState(position, true);
Log.d(TAG, "===== onNoteItemLongClick END =====");
} else {
Log.d(TAG, "ActionMode already active, ignoring long click");
Log.d(TAG, "Multi-select mode already active, ignoring long click");
}
}
/**
* ActionMode
*
*/
private void updateActionModeTitle(androidx.appcompat.view.ActionMode mode) {
int selectedCount = viewModel.getSelectedCount();
String title = getString(R.string.menu_select_title, selectedCount);
mode.setTitle(title);
private void enterMultiSelectMode() {
isMultiSelectMode = true;
// 隐藏FAB按钮
if (fabNewNote != null) {
fabNewNote.setVisibility(View.GONE);
}
// 更新toolbar为多选模式
updateToolbarForMultiSelectMode();
}
/**
*
*
* @param mode ActionMode
* @param position
* @param id 便 ID
* @param checked
* 退
*/
public void onItemCheckedStateChanged(ActionMode mode, int position, long id, boolean checked) {
Log.d(TAG, "onItemCheckedStateChanged: id=" + id + ", checked=" + checked);
viewModel.toggleNoteSelection(id, checked);
private void exitMultiSelectMode() {
isMultiSelectMode = false;
// 显示FAB按钮
if (fabNewNote != null) {
fabNewNote.setVisibility(View.VISIBLE);
}
// 清除选中状态
viewModel.clearSelection();
if (adapter != null) {
adapter.setSelectedIds(viewModel.getSelectedNoteIds());
adapter.setSelectedIds(new java.util.HashSet<>());
adapter.notifyDataSetChanged();
}
updateActionModeTitle(mode);
// 更新toolbar为普通模式
updateToolbarForNormalMode();
}
/**
* Toolbar
*/
private void updateToolbarForMultiSelectMode() {
if (toolbar == null) return;
// 设置标题为选中数量
int selectedCount = viewModel.getSelectedCount();
String title = getString(R.string.menu_select_title, selectedCount);
toolbar.setTitle(title);
// 设置导航图标为返回(取消多选)
toolbar.setNavigationIcon(androidx.appcompat.R.drawable.abc_ic_ab_back_material);
toolbar.setNavigationOnClickListener(v -> exitMultiSelectMode());
// 移除普通模式的菜单(如果有)
toolbar.getMenu().clear();
// 直接在toolbar上添加操作按钮不在三点菜单中
Menu menu = toolbar.getMenu();
// 删除按钮
MenuItem deleteItem = menu.add(Menu.NONE, R.id.multi_select_delete, 1, getString(R.string.menu_delete));
deleteItem.setIcon(android.R.drawable.ic_menu_delete);
deleteItem.setShowAsAction(MenuItem.SHOW_AS_ACTION_ALWAYS);
// 移动按钮
MenuItem moveItem = menu.add(Menu.NONE, R.id.multi_select_move, 2, getString(R.string.menu_move));
moveItem.setIcon(android.R.drawable.ic_menu_sort_by_size);
moveItem.setShowAsAction(MenuItem.SHOW_AS_ACTION_ALWAYS);
}
/**
* Toolbar
*/
private void updateToolbarForNormalMode() {
if (toolbar == null) return;
// 设置标题为应用名称
toolbar.setTitle(R.string.app_name);
// 设置导航图标为汉堡菜单
toolbar.setNavigationIcon(android.R.drawable.ic_menu_sort_by_size);
toolbar.setNavigationOnClickListener(v -> {
if (drawerLayout != null) {
drawerLayout.openDrawer(findViewById(R.id.sidebar_fragment));
}
});
// 清除多选模式菜单
toolbar.getMenu().clear();
// 添加普通模式菜单(如果需要)
// getMenuInflater().inflate(R.menu.note_list_options, menu);
}
/**
*
*/
@ -427,8 +606,8 @@ public class NotesListActivity extends AppCompatActivity
Toast.makeText(this, "搜索功能开发中", Toast.LENGTH_SHORT).show();
return true;
case R.id.menu_new_folder:
// TODO: 创建新文件夹
Toast.makeText(this, "创建文件夹功能开发中", Toast.LENGTH_SHORT).show();
// 创建新文件夹
showCreateFolderDialog();
return true;
case R.id.menu_export_text:
// TODO: 导出笔记
@ -442,6 +621,13 @@ public class NotesListActivity extends AppCompatActivity
// TODO: 设置功能
Toast.makeText(this, "设置功能开发中", Toast.LENGTH_SHORT).show();
return true;
// 多选模式菜单项
case R.id.multi_select_delete:
showDeleteDialog();
return true;
case R.id.multi_select_move:
showMoveMenu();
return true;
default:
return super.onOptionsItemSelected(item);
}
@ -500,4 +686,152 @@ public class NotesListActivity extends AppCompatActivity
}
Log.d("NotesListActivity", "===== updateSelectionState END =====");
}
// ==================== SidebarFragment.OnSidebarItemSelectedListener 实现 ====================
@Override
public void onFolderSelected(long folderId) {
// 跳转到指定文件夹
viewModel.enterFolder(folderId);
// 关闭侧栏
if (drawerLayout != null) {
drawerLayout.closeDrawer(findViewById(R.id.sidebar_fragment));
}
}
@Override
public void onTrashSelected() {
// TODO: 实现跳转到回收站
Log.d(TAG, "Trash selected");
// 关闭侧栏
if (drawerLayout != null) {
drawerLayout.closeDrawer(findViewById(R.id.sidebar_fragment));
}
}
@Override
public void onSyncSelected() {
// TODO: 实现同步功能
Log.d(TAG, "Sync selected");
Toast.makeText(this, "同步功能待实现", Toast.LENGTH_SHORT).show();
}
@Override
public void onLoginSelected() {
// TODO: 实现登录功能
Log.d(TAG, "Login selected");
Toast.makeText(this, "登录功能待实现", Toast.LENGTH_SHORT).show();
}
@Override
public void onExportSelected() {
// TODO: 实现导出功能
Log.d(TAG, "Export selected");
Toast.makeText(this, "导出功能待实现", Toast.LENGTH_SHORT).show();
}
@Override
public void onSettingsSelected() {
// TODO: 实现设置功能
Log.d(TAG, "Settings selected");
Toast.makeText(this, "设置功能待实现", Toast.LENGTH_SHORT).show();
}
@Override
public void onCreateFolder() {
// 显示创建文件夹对话框
showCreateFolderDialog();
}
/**
*
*/
private void showCreateFolderDialog() {
AlertDialog.Builder builder = new AlertDialog.Builder(this);
builder.setTitle(R.string.dialog_create_folder_title);
final EditText input = new EditText(this);
input.setHint(R.string.dialog_create_folder_hint);
input.setFilters(new InputFilter[]{new InputFilter.LengthFilter(50)});
builder.setView(input);
builder.setPositiveButton(R.string.menu_create_folder, (dialog, which) -> {
String folderName = input.getText().toString().trim();
if (TextUtils.isEmpty(folderName)) {
Toast.makeText(this, R.string.error_folder_name_empty, Toast.LENGTH_SHORT).show();
return;
}
if (folderName.length() > 50) {
Toast.makeText(this, R.string.error_folder_name_too_long, Toast.LENGTH_SHORT).show();
return;
}
// 创建文件夹
NotesRepository repository = new NotesRepository(getContentResolver());
long parentId = viewModel.getCurrentFolderId();
if (parentId == 0) {
parentId = Notes.ID_ROOT_FOLDER;
}
repository.createFolder(parentId, folderName,
new NotesRepository.Callback<Long>() {
@Override
public void onSuccess(Long folderId) {
runOnUiThread(() -> {
Toast.makeText(NotesListActivity.this, R.string.create_folder_success, Toast.LENGTH_SHORT).show();
// 刷新笔记列表
viewModel.loadNotes(viewModel.getCurrentFolderId());
});
}
@Override
public void onError(Exception error) {
runOnUiThread(() -> {
Toast.makeText(NotesListActivity.this, "创建文件夹失败: " + error.getMessage(), Toast.LENGTH_SHORT).show();
});
}
});
});
builder.setNegativeButton(android.R.string.cancel, null);
builder.show();
}
@Override
public void onCloseSidebar() {
// 关闭侧栏
if (drawerLayout != null) {
drawerLayout.closeDrawer(findViewById(R.id.sidebar_fragment));
}
}
/**
*
*
* <p>
* 退
*
*
* </p>
*/
@Override
public void onBackPressed() {
if (isMultiSelectMode) {
// 多选模式:退出多选模式
exitMultiSelectMode();
} else if (drawerLayout != null && drawerLayout.isDrawerOpen(findViewById(R.id.sidebar_fragment))) {
// 侧栏打开:关闭侧栏
drawerLayout.closeDrawer(findViewById(R.id.sidebar_fragment));
} else if (viewModel.getCurrentFolderId() != Notes.ID_ROOT_FOLDER &&
viewModel.getCurrentFolderId() != Notes.ID_CALL_RECORD_FOLDER) {
// 子文件夹:返回上一级
if (!viewModel.navigateUp()) {
// 如果没有导航历史,返回根文件夹
viewModel.loadNotes(Notes.ID_ROOT_FOLDER);
}
} else {
// 根文件夹:最小化应用
moveTaskToBack(true);
}
}
}

@ -0,0 +1,450 @@
/*
* Copyright (c) 2010-2011, The MiCode Open Source Community (www.micode.net)
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package net.micode.notes.ui;
import android.app.AlertDialog;
import android.content.Context;
import android.os.Bundle;
import android.text.InputFilter;
import android.text.TextUtils;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.view.animation.Animation;
import android.view.animation.TranslateAnimation;
import android.widget.EditText;
import android.widget.ImageView;
import android.widget.LinearLayout;
import android.widget.TextView;
import android.widget.Toast;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.fragment.app.Fragment;
import androidx.lifecycle.ViewModelProvider;
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.viewmodel.FolderListViewModel;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.Set;
/**
* Fragment
* <p>
*
* /
* </p>
*/
public class SidebarFragment extends Fragment {
private static final String TAG = "SidebarFragment";
private static final int MAX_FOLDER_NAME_LENGTH = 50;
// 视图组件
private RecyclerView rvFolderTree;
private TextView tvRootFolder;
private TextView menuSync;
private TextView menuLogin;
private TextView menuExport;
private TextView menuSettings;
private TextView menuTrash;
// 适配器和数据
private FolderTreeAdapter adapter;
private FolderListViewModel viewModel;
// 单击和双击检测
private long lastClickTime = 0;
private View lastClickedView = null;
private static final long DOUBLE_CLICK_INTERVAL = 300; // 毫秒
// 回调接口
private OnSidebarItemSelectedListener listener;
/**
*
*/
public interface OnSidebarItemSelectedListener {
/**
*
* @param folderId ID
*/
void onFolderSelected(long folderId);
/**
*
*/
void onTrashSelected();
/**
*
*/
void onSyncSelected();
/**
*
*/
void onLoginSelected();
/**
*
*/
void onExportSelected();
/**
*
*/
void onSettingsSelected();
/**
*
*/
void onCreateFolder();
/**
*
*/
void onCloseSidebar();
}
@Override
public void onAttach(@NonNull Context context) {
super.onAttach(context);
if (context instanceof OnSidebarItemSelectedListener) {
listener = (OnSidebarItemSelectedListener) context;
} else {
throw new RuntimeException(context.toString() + " must implement OnSidebarItemSelectedListener");
}
}
@Override
public void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
viewModel = new ViewModelProvider(this).get(FolderListViewModel.class);
}
@Nullable
@Override
public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container,
@Nullable Bundle savedInstanceState) {
return inflater.inflate(R.layout.sidebar_layout, container, false);
}
@Override
public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) {
super.onViewCreated(view, savedInstanceState);
initViews(view);
setupListeners();
observeViewModel();
}
/**
*
*/
public void refreshFolderTree() {
if (viewModel != null) {
viewModel.loadFolderTree();
}
}
/**
*
*/
private void initViews(View view) {
rvFolderTree = view.findViewById(R.id.rv_folder_tree);
tvRootFolder = view.findViewById(R.id.tv_root_folder);
menuSync = view.findViewById(R.id.menu_sync);
menuLogin = view.findViewById(R.id.menu_login);
menuExport = view.findViewById(R.id.menu_export);
menuSettings = view.findViewById(R.id.menu_settings);
menuTrash = view.findViewById(R.id.menu_trash);
// 设置RecyclerView
rvFolderTree.setLayoutManager(new LinearLayoutManager(requireContext()));
adapter = new FolderTreeAdapter(new ArrayList<>(), viewModel);
rvFolderTree.setAdapter(adapter);
}
/**
*
*/
private void setupListeners() {
View view = getView();
if (view == null) return;
// 根文件夹(单击展开/收起,双击跳转)
setupFolderClickListener(tvRootFolder, Notes.ID_ROOT_FOLDER);
// 关闭侧栏
view.findViewById(R.id.btn_close_sidebar).setOnClickListener(v -> {
if (listener != null) {
listener.onCloseSidebar();
}
});
// 创建文件夹
view.findViewById(R.id.btn_create_folder).setOnClickListener(v -> showCreateFolderDialog());
// 菜单项
menuSync.setOnClickListener(v -> {
if (listener != null) {
listener.onSyncSelected();
}
});
menuLogin.setOnClickListener(v -> {
if (listener != null) {
listener.onLoginSelected();
}
});
menuExport.setOnClickListener(v -> {
if (listener != null) {
listener.onExportSelected();
}
});
menuSettings.setOnClickListener(v -> {
if (listener != null) {
listener.onSettingsSelected();
}
});
menuTrash.setOnClickListener(v -> {
if (listener != null) {
listener.onTrashSelected();
}
});
}
/**
* /
*/
private void setupFolderClickListener(View view, long folderId) {
view.setOnClickListener(v -> {
long currentTime = System.currentTimeMillis();
if (lastClickedView == view && (currentTime - lastClickTime) < DOUBLE_CLICK_INTERVAL) {
// 这是双击,执行跳转
if (listener != null && folderId != Notes.ID_ROOT_FOLDER) {
listener.onFolderSelected(folderId);
}
// 重置双击状态
lastClickTime = 0;
lastClickedView = null;
} else {
// 可能是单击,延迟处理
lastClickTime = currentTime;
lastClickedView = view;
view.postDelayed(() -> {
// 如果在延迟期间没有发生双击,则执行单击操作(展开/收起)
if (System.currentTimeMillis() - lastClickTime >= DOUBLE_CLICK_INTERVAL) {
toggleFolderExpand(folderId);
}
}, DOUBLE_CLICK_INTERVAL);
}
});
}
/**
* ViewModel
*/
private void observeViewModel() {
viewModel.getFolderTree().observe(getViewLifecycleOwner(), folderItems -> {
if (folderItems != null) {
adapter.setData(folderItems);
adapter.notifyDataSetChanged();
}
});
viewModel.loadFolderTree();
}
/**
* /
*/
private void toggleFolderExpand(long folderId) {
viewModel.toggleFolderExpand(folderId);
}
/**
*
*/
private void showCreateFolderDialog() {
AlertDialog.Builder builder = new AlertDialog.Builder(requireContext());
builder.setTitle(R.string.dialog_create_folder_title);
final EditText input = new EditText(requireContext());
input.setHint(R.string.dialog_create_folder_hint);
input.setFilters(new InputFilter[]{new InputFilter.LengthFilter(MAX_FOLDER_NAME_LENGTH)});
builder.setView(input);
builder.setPositiveButton(R.string.menu_create_folder, (dialog, which) -> {
String folderName = input.getText().toString().trim();
if (TextUtils.isEmpty(folderName)) {
Toast.makeText(requireContext(), R.string.error_folder_name_empty, Toast.LENGTH_SHORT).show();
return;
}
if (folderName.length() > MAX_FOLDER_NAME_LENGTH) {
Toast.makeText(requireContext(), R.string.error_folder_name_too_long, Toast.LENGTH_SHORT).show();
return;
}
// 创建文件夹
NotesRepository repository = new NotesRepository(requireContext().getContentResolver());
long parentId = viewModel.getCurrentFolderId();
if (parentId == 0) {
parentId = Notes.ID_ROOT_FOLDER;
}
repository.createFolder(parentId, folderName,
new NotesRepository.Callback<Long>() {
@Override
public void onSuccess(Long folderId) {
if (getActivity() != null) {
getActivity().runOnUiThread(() -> {
Toast.makeText(requireContext(), "创建文件夹成功", Toast.LENGTH_SHORT).show();
// 刷新文件夹列表
viewModel.loadFolderTree();
});
}
}
@Override
public void onError(Exception error) {
if (getActivity() != null) {
getActivity().runOnUiThread(() -> {
Toast.makeText(requireContext(), "创建文件夹失败: " + error.getMessage(), Toast.LENGTH_SHORT).show();
});
}
}
});
});
builder.setNegativeButton(android.R.string.cancel, null);
builder.show();
}
/**
* FolderTreeAdapter
* /
*/
private static class FolderTreeAdapter extends RecyclerView.Adapter<FolderTreeAdapter.FolderViewHolder> {
private List<FolderTreeItem> folderItems;
private FolderListViewModel viewModel;
public FolderTreeAdapter(List<FolderTreeItem> folderItems, FolderListViewModel viewModel) {
this.folderItems = folderItems;
this.viewModel = viewModel;
}
public void setData(List<FolderTreeItem> folderItems) {
this.folderItems = folderItems;
}
@NonNull
@Override
public FolderViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
View view = LayoutInflater.from(parent.getContext())
.inflate(R.layout.sidebar_folder_item, parent, false);
return new FolderViewHolder(view);
}
@Override
public void onBindViewHolder(@NonNull FolderViewHolder holder, int position) {
FolderTreeItem item = folderItems.get(position);
boolean isExpanded = viewModel != null && viewModel.isFolderExpanded(item.folderId);
holder.bind(item, isExpanded);
}
@Override
public int getItemCount() {
return folderItems.size();
}
static class FolderViewHolder extends RecyclerView.ViewHolder {
private View indentView;
private ImageView ivExpandIcon;
private ImageView ivFolderIcon;
private TextView tvFolderName;
private TextView tvNoteCount;
public FolderViewHolder(@NonNull View itemView) {
super(itemView);
indentView = itemView.findViewById(R.id.indent_view);
ivExpandIcon = itemView.findViewById(R.id.iv_expand_icon);
ivFolderIcon = itemView.findViewById(R.id.iv_folder_icon);
tvFolderName = itemView.findViewById(R.id.tv_folder_name);
tvNoteCount = itemView.findViewById(R.id.tv_note_count);
}
public void bind(FolderTreeItem item, boolean isExpanded) {
// 设置缩进
int indent = item.level * 32;
indentView.setLayoutParams(new LinearLayout.LayoutParams(indent, LinearLayout.LayoutParams.MATCH_PARENT));
// 设置展开/收起图标
if (item.hasChildren) {
ivExpandIcon.setVisibility(View.VISIBLE);
ivExpandIcon.setRotation(isExpanded ? 90 : 0);
} else {
ivExpandIcon.setVisibility(View.INVISIBLE);
}
// 设置文件夹名称
tvFolderName.setText(item.name);
// 设置便签数量
tvNoteCount.setText(String.format(itemView.getContext()
.getString(R.string.folder_note_count), item.noteCount));
}
}
}
/**
* FolderTreeItem
*
*/
public static class FolderTreeItem {
public long folderId;
public String name;
public int level; // 层级0表示顶级
public boolean hasChildren;
public int noteCount;
public FolderTreeItem(long folderId, String name, int level, boolean hasChildren, int noteCount) {
this.folderId = folderId;
this.name = name;
this.level = level;
this.hasChildren = hasChildren;
this.noteCount = noteCount;
}
}
@Override
public void onDetach() {
super.onDetach();
listener = null;
}
}

@ -0,0 +1,269 @@
/*
* 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.viewmodel;
import android.app.Application;
import android.database.Cursor;
import android.net.Uri;
import androidx.annotation.NonNull;
import androidx.lifecycle.AndroidViewModel;
import androidx.lifecycle.LiveData;
import androidx.lifecycle.MutableLiveData;
import net.micode.notes.data.Notes;
import net.micode.notes.data.NotesDatabaseHelper;
import net.micode.notes.data.NotesDatabaseHelper.TABLE;
import net.micode.notes.data.Notes.NoteColumns;
import net.micode.notes.data.NotesRepository;
import net.micode.notes.ui.SidebarFragment.FolderTreeItem;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
/**
* ViewModel
* <p>
*
*
* </p>
*/
public class FolderListViewModel extends AndroidViewModel {
private MutableLiveData<List<FolderTreeItem>> folderTreeLiveData;
private NotesDatabaseHelper dbHelper;
private NotesRepository repository;
private long currentFolderId = Notes.ID_ROOT_FOLDER; // 当前文件夹ID
private Set<Long> expandedFolderIds = new HashSet<>(); // 已展开的文件夹ID集合
public FolderListViewModel(@NonNull Application application) {
super(application);
dbHelper = NotesDatabaseHelper.getInstance(application);
repository = new NotesRepository(application.getContentResolver());
folderTreeLiveData = new MutableLiveData<>();
}
/**
* ID
*/
public long getCurrentFolderId() {
return currentFolderId;
}
/**
* ID
*/
public void setCurrentFolderId(long folderId) {
this.currentFolderId = folderId;
}
/**
* /
* @param folderId ID
*/
public void toggleFolderExpand(long folderId) {
if (expandedFolderIds.contains(folderId)) {
expandedFolderIds.remove(folderId);
} else {
expandedFolderIds.add(folderId);
}
// 重新加载文件夹树
loadFolderTree();
}
/**
*
* @param folderId ID
* @return
*/
public boolean isFolderExpanded(long folderId) {
return expandedFolderIds.contains(folderId);
}
/**
* LiveData
*/
public LiveData<List<FolderTreeItem>> getFolderTree() {
return folderTreeLiveData;
}
/**
*
*/
public void loadFolderTree() {
new Thread(() -> {
List<FolderTreeItem> folderTree = buildFolderTree();
folderTreeLiveData.postValue(folderTree);
}).start();
}
/**
*
* <p>
*
* </p>
* @return
*/
private List<FolderTreeItem> buildFolderTree() {
// 查询所有文件夹(不包括系统文件夹)
List<Map<String, Object>> folders = queryAllFolders();
// 构建文件夹映射表(方便查找父文件夹)
Map<Long, FolderNode> folderMap = new HashMap<>();
List<FolderNode> rootFolders = new ArrayList<>();
// 创建文件夹节点
for (Map<String, Object> folder : folders) {
long id = (Long) folder.get(NoteColumns.ID);
String name = (String) folder.get(NoteColumns.SNIPPET);
long parentId = (Long) folder.get(NoteColumns.PARENT_ID);
int noteCount = ((Number) folder.get(NoteColumns.NOTES_COUNT)).intValue();
FolderNode node = new FolderNode(id, name, parentId, noteCount);
folderMap.put(id, node);
// 如果是顶级文件夹(父文件夹为根),添加到根列表
if (parentId == Notes.ID_ROOT_FOLDER) {
rootFolders.add(node);
}
}
// 构建父子关系
for (FolderNode node : folderMap.values()) {
if (node.parentId != Notes.ID_ROOT_FOLDER) {
FolderNode parent = folderMap.get(node.parentId);
if (parent != null) {
parent.children.add(node);
}
}
}
// 转换为扁平列表用于RecyclerView显示
List<FolderTreeItem> folderTree = new ArrayList<>();
// 检查根文件夹是否展开
boolean rootExpanded = expandedFolderIds.contains(Notes.ID_ROOT_FOLDER);
buildFolderTreeList(rootFolders, folderTree, 0, rootExpanded);
return folderTree;
}
/**
*
*
* (level=0)"我的便签"
* @param nodes
* @param folderTree
* @param level
* @param forceExpandChildren
*/
private void buildFolderTreeList(List<FolderNode> nodes, List<FolderTreeItem> folderTree, int level, boolean forceExpandChildren) {
for (FolderNode node : nodes) {
// 对于顶层文件夹,需要"我的便签"展开后才显示
if (level == 0 && !expandedFolderIds.contains(Notes.ID_ROOT_FOLDER) && !forceExpandChildren) {
continue; // 跳过顶层文件夹,因为根未展开
}
folderTree.add(new FolderTreeItem(
node.id,
node.name,
level,
!node.children.isEmpty(),
node.noteCount
));
// 只有当父文件夹在 expandedFolderIds 中时,才递归处理子文件夹
if (!node.children.isEmpty() && expandedFolderIds.contains(node.id)) {
buildFolderTreeList(node.children, folderTree, level + 1, false);
}
}
}
/**
*
* @return
*/
private List<Map<String, Object>> queryAllFolders() {
List<Map<String, Object>> folders = new ArrayList<>();
// 查询所有文件夹类型的笔记
String selection = NoteColumns.TYPE + " = ?";
String[] selectionArgs = new String[]{
String.valueOf(Notes.TYPE_FOLDER)
};
Cursor cursor = null;
try {
cursor = dbHelper.getReadableDatabase().query(
TABLE.NOTE,
null,
selection,
selectionArgs,
null,
null,
NoteColumns.MODIFIED_DATE + " DESC"
);
if (cursor != null) {
while (cursor.moveToNext()) {
Map<String, Object> folder = new HashMap<>();
long id = cursor.getLong(cursor.getColumnIndexOrThrow(NoteColumns.ID));
String name = cursor.getString(cursor.getColumnIndexOrThrow(NoteColumns.SNIPPET));
long parentId = cursor.getLong(cursor.getColumnIndexOrThrow(NoteColumns.PARENT_ID));
int noteCount = cursor.getInt(cursor.getColumnIndexOrThrow(NoteColumns.NOTES_COUNT));
folder.put(NoteColumns.ID, id);
folder.put(NoteColumns.SNIPPET, name);
folder.put(NoteColumns.PARENT_ID, parentId);
folder.put(NoteColumns.NOTES_COUNT, noteCount);
folders.add(folder);
}
}
} catch (Exception e) {
e.printStackTrace();
} finally {
if (cursor != null) {
cursor.close();
}
}
return folders;
}
/**
* FolderNode
*
*/
private static class FolderNode {
public long id;
public String name;
public long parentId;
public int noteCount;
public List<FolderNode> children = new ArrayList<>();
public FolderNode(long id, String name, long parentId, int noteCount) {
this.id = id;
this.name = name;
this.parentId = parentId;
this.noteCount = noteCount;
}
}
}

@ -58,6 +58,15 @@ public class NotesListViewModel extends ViewModel {
// 当前文件夹ID
private long currentFolderId = Notes.ID_ROOT_FOLDER;
// 文件夹路径LiveData用于面包屑导航
private final MutableLiveData<List<NotesRepository.NoteInfo>> folderPathLiveData = new MutableLiveData<>();
// 侧栏刷新通知LiveData删除等操作后通知侧栏刷新
private final MutableLiveData<Boolean> sidebarRefreshNeeded = new MutableLiveData<>(false);
// 文件夹导航历史(用于返回上一级)
private final List<Long> folderHistory = new ArrayList<>();
/**
*
*
@ -98,7 +107,7 @@ public class NotesListViewModel extends ViewModel {
/**
*
* <p>
*
*
* </p>
*
* @param folderId ID{@link Notes#ID_ROOT_FOLDER}
@ -108,6 +117,20 @@ public class NotesListViewModel extends ViewModel {
isLoading.postValue(true);
errorMessage.postValue(null);
// 加载文件夹路径
repository.getFolderPath(folderId, new NotesRepository.Callback<List<NotesRepository.NoteInfo>>() {
@Override
public void onSuccess(List<NotesRepository.NoteInfo> path) {
folderPathLiveData.postValue(path);
}
@Override
public void onError(Exception error) {
Log.e(TAG, "Failed to load folder path", error);
}
});
// 加载笔记
repository.getNotes(folderId, new NotesRepository.Callback<List<NotesRepository.NoteInfo>>() {
@Override
public void onSuccess(List<NotesRepository.NoteInfo> notes) {
@ -349,6 +372,58 @@ public class NotesListViewModel extends ViewModel {
this.currentFolderId = folderId;
}
/**
* LiveData
*
* @return LiveData
*/
public MutableLiveData<List<NotesRepository.NoteInfo>> getFolderPathLiveData() {
return folderPathLiveData;
}
/**
* LiveData
*
* @return LiveData
*/
public MutableLiveData<Boolean> getSidebarRefreshNeeded() {
return sidebarRefreshNeeded;
}
/**
*
*/
public void triggerSidebarRefresh() {
sidebarRefreshNeeded.postValue(true);
}
/**
*
*
* @param folderId ID
*/
public void enterFolder(long folderId) {
// 将当前文件夹添加到历史记录
if (currentFolderId != Notes.ID_ROOT_FOLDER && currentFolderId != Notes.ID_CALL_RECORD_FOLDER) {
folderHistory.add(currentFolderId);
}
loadNotes(folderId);
}
/**
*
*
* @return
*/
public boolean navigateUp() {
if (!folderHistory.isEmpty()) {
long parentFolderId = folderHistory.remove(folderHistory.size() - 1);
loadNotes(parentFolderId);
return true;
}
return false;
}
/**
*
* <p>

@ -1,19 +1,37 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
<androidx.drawerlayout.widget.DrawerLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/main"
android:id="@+id/drawer_layout"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:fitsSystemWindows="true"
tools:context=".MainActivity">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Hello World!"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<!-- 主内容区域 -->
<androidx.constraintlayout.widget.ConstraintLayout
android:id="@+id/main_content"
android:layout_width="match_parent"
android:layout_height="match_parent">
</androidx.constraintlayout.widget.ConstraintLayout>
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Hello World!"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
</androidx.constraintlayout.widget.ConstraintLayout>
<!-- 侧栏 -->
<fragment
android:id="@+id/sidebar_fragment"
android:name="net.micode.notes.ui.SidebarFragment"
android:layout_width="280dp"
android:layout_height="match_parent"
android:layout_gravity="start"
android:tag="sidebar" />
</androidx.drawerlayout.widget.DrawerLayout>

@ -0,0 +1,16 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
面包屑项布局
显示文件夹名称,支持点击和长按
-->
<TextView xmlns:android="http://schemas.android.com/apk/res/android"
android:id="@+id/tv_breadcrumb_item"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:textSize="14sp"
android:textColor="@color/primary_color"
android:padding="4dp"
android:background="?attr/selectableItemBackground"
android:maxLines="1"
android:ellipsize="end"
android:maxEms="10" />

@ -0,0 +1,35 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
面包屑导航布局
显示当前文件夹路径,支持点击跳转和长按重命名
-->
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:id="@+id/breadcrumb_container"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal"
android:paddingHorizontal="16dp"
android:paddingVertical="8dp"
android:background="@android:color/white"
android:gravity="center_vertical">
<!-- 面包屑项容器(动态添加) -->
<LinearLayout
android:id="@+id/breadcrumb_items"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:orientation="horizontal"
android:gravity="center_vertical" />
<!-- 文件夹图标 -->
<ImageView
android:id="@+id/iv_folder_icon"
android:layout_width="24dp"
android:layout_height="24dp"
android:layout_marginStart="8dp"
android:src="@android:drawable/ic_menu_myplaces"
android:contentDescription="文件夹"
android:visibility="gone" />
</LinearLayout>

@ -1,43 +1,56 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Copyright (c) 2010-2011, The MiCode Open Source Community (www.micode.net)
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
-->
<FrameLayout
android:layout_width="fill_parent"
android:layout_height="fill_parent"
android:background="@drawable/list_background"
xmlns:android="http://schemas.android.com/apk/res/android">
<!-- 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.
-->
<!-- 统一使用Material风格与列表页面一致 -->
<LinearLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical"
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto">
<!-- 统一使用MaterialToolbar与列表页面一致 -->
<com.google.android.material.appbar.MaterialToolbar
android:id="@+id/toolbar"
android:layout_width="match_parent"
android:layout_height="?attr/actionBarSize"
android:background="?attr/colorPrimary"
app:title="@string/menu_edit_note"
app:navigationIcon="@android:drawable/ic_menu_close_clear_cancel" />
<!-- 主内容区域 -->
<LinearLayout
android:layout_width="fill_parent"
android:layout_height="fill_parent"
android:layout_width="match_parent"
android:layout_height="0dp"
android:layout_weight="1"
android:orientation="vertical">
<LinearLayout
android:id="@+id/note_title"
android:layout_width="fill_parent"
android:layout_width="match_parent"
android:layout_height="wrap_content">
<TextView
android:id="@+id/tv_modified_date"
android:layout_width="0dip"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:layout_gravity="left|center_vertical"
android:layout_marginRight="8dip"
android:layout_marginRight="8dp"
android:textAppearance="@style/TextAppearanceSecondaryItem" />
<ImageView
@ -52,8 +65,8 @@
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center_vertical"
android:layout_marginLeft="2dip"
android:layout_marginRight="8dip"
android:layout_marginLeft="2dp"
android:layout_marginRight="8dp"
android:textAppearance="@style/TextAppearanceSecondaryItem" />
<ImageButton
@ -65,31 +78,32 @@
<LinearLayout
android:id="@+id/sv_note_edit"
android:layout_width="fill_parent"
android:layout_height="fill_parent"
android:layout_width="match_parent"
android:layout_height="0dp"
android:layout_weight="1"
android:orientation="vertical">
<ImageView
android:layout_width="fill_parent"
android:layout_height="7dip"
android:layout_width="match_parent"
android:layout_height="7dp"
android:background="@drawable/bg_color_btn_mask" />
<ScrollView
android:layout_width="fill_parent"
android:layout_height="0dip"
android:layout_width="match_parent"
android:layout_height="0dp"
android:layout_weight="1"
android:scrollbars="none"
android:overScrollMode="never"
android:layout_gravity="left|top"
android:fadingEdgeLength="0dip">
android:fadingEdgeLength="0dp">
<LinearLayout
android:layout_width="fill_parent"
android:layout_height="fill_parent">
android:layout_width="match_parent"
android:layout_height="match_parent">
<net.micode.notes.ui.NoteEditText
android:id="@+id/note_edit_view"
android:layout_width="fill_parent"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:gravity="left|top"
android:background="@null"
@ -101,300 +115,302 @@
<LinearLayout
android:id="@+id/note_edit_list"
android:layout_width="fill_parent"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
android:layout_marginLeft="-10dip"
android:layout_marginLeft="-10dp"
android:visibility="gone" />
</LinearLayout>
</ScrollView>
<ImageView
android:layout_width="fill_parent"
android:layout_height="7dip"
android:layout_width="match_parent"
android:layout_height="7dp"
android:background="@drawable/bg_color_btn_mask" />
</LinearLayout>
</LinearLayout>
<ImageView
android:id="@+id/btn_set_bg_color"
android:layout_height="43dip"
android:layout_width="wrap_content"
android:background="@drawable/bg_color_btn_mask"
android:layout_gravity="top|right" />
<ImageView
android:id="@+id/btn_set_bg_color"
android:layout_height="43dp"
android:layout_width="wrap_content"
android:background="@drawable/bg_color_btn_mask"
android:layout_gravity="top|right" />
<LinearLayout
android:id="@+id/note_bg_color_selector"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:background="@drawable/note_edit_color_selector_panel"
android:layout_marginTop="30dip"
android:layout_marginRight="8dip"
android:layout_gravity="top|right"
android:visibility="gone">
<FrameLayout
android:layout_width="0dip"
android:layout_height="match_parent"
android:layout_weight="1">
<LinearLayout
android:id="@+id/note_bg_color_selector"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:background="@drawable/note_edit_color_selector_panel"
android:layout_marginTop="30dp"
android:layout_marginRight="8dp"
android:layout_gravity="top|right"
android:visibility="gone">
<ImageView
android:id="@+id/iv_bg_yellow"
android:layout_width="match_parent"
android:layout_height="match_parent" />
<FrameLayout
android:layout_width="0dp"
android:layout_height="match_parent"
android:layout_weight="1">
<ImageView
android:id="@+id/iv_bg_yellow_select"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="bottom|right"
android:layout_marginRight="5dip"
android:focusable="false"
android:visibility="gone"
android:src="@drawable/selected" />
</FrameLayout>
<FrameLayout
android:layout_width="0dip"
android:layout_height="match_parent"
android:layout_weight="1">
<ImageView
android:id="@+id/iv_bg_yellow"
android:layout_width="match_parent"
android:layout_height="match_parent" />
<ImageView
android:id="@+id/iv_bg_blue"
android:layout_width="match_parent"
android:layout_height="match_parent" />
<ImageView
android:id="@+id/iv_bg_yellow_select"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="bottom|right"
android:layout_marginRight="5dp"
android:focusable="false"
android:visibility="gone"
android:src="@drawable/selected" />
</FrameLayout>
<FrameLayout
android:layout_width="0dp"
android:layout_height="match_parent"
android:layout_weight="1">
<ImageView
android:id="@+id/iv_bg_blue_select"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="bottom|right"
android:focusable="false"
android:visibility="gone"
android:layout_marginRight="3dip"
android:src="@drawable/selected" />
</FrameLayout>
<FrameLayout
android:layout_width="0dip"
android:layout_height="match_parent"
android:layout_weight="1">
<ImageView
android:id="@+id/iv_bg_blue"
android:layout_width="match_parent"
android:layout_height="match_parent" />
<ImageView
android:id="@+id/iv_bg_white"
android:layout_width="match_parent"
android:layout_height="match_parent" />
<ImageView
android:id="@+id/iv_bg_blue_select"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="bottom|right"
android:focusable="false"
android:visibility="gone"
android:layout_marginRight="3dp"
android:src="@drawable/selected" />
</FrameLayout>
<FrameLayout
android:layout_width="0dp"
android:layout_height="match_parent"
android:layout_weight="1">
<ImageView
android:id="@+id/iv_bg_white_select"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="bottom|right"
android:focusable="false"
android:visibility="gone"
android:layout_marginRight="2dip"
android:src="@drawable/selected" />
</FrameLayout>
<FrameLayout
android:layout_width="0dip"
android:layout_height="match_parent"
android:layout_weight="1">
<ImageView
android:id="@+id/iv_bg_white"
android:layout_width="match_parent"
android:layout_height="match_parent" />
<ImageView
android:id="@+id/iv_bg_green"
android:layout_width="match_parent"
android:layout_height="match_parent" />
<ImageView
android:id="@+id/iv_bg_white_select"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="bottom|right"
android:focusable="false"
android:visibility="gone"
android:layout_marginRight="2dp"
android:src="@drawable/selected" />
</FrameLayout>
<FrameLayout
android:layout_width="0dp"
android:layout_height="match_parent"
android:layout_weight="1">
<ImageView
android:id="@+id/iv_bg_green_select"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="bottom|right"
android:focusable="false"
android:visibility="gone"
android:src="@drawable/selected" />
</FrameLayout>
<ImageView
android:id="@+id/iv_bg_green"
android:layout_width="match_parent"
android:layout_height="match_parent" />
<FrameLayout
android:layout_width="0dip"
android:layout_height="match_parent"
android:layout_weight="1">
<ImageView
android:id="@+id/iv_bg_green_select"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="bottom|right"
android:focusable="false"
android:visibility="gone"
android:layout_marginRight="5dp"
android:src="@drawable/selected" />
</FrameLayout>
<FrameLayout
android:layout_width="0dp"
android:layout_height="match_parent"
android:layout_weight="1">
<ImageView
android:id="@+id/iv_bg_red"
android:layout_width="match_parent"
android:layout_height="match_parent" />
<ImageView
android:id="@+id/iv_bg_red"
android:layout_width="match_parent"
android:layout_height="match_parent" />
<ImageView
android:id="@+id/iv_bg_red_select"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="bottom|right"
android:focusable="false"
android:visibility="gone"
android:src="@drawable/selected" />
</FrameLayout>
</LinearLayout>
<ImageView
android:id="@+id/iv_bg_red_select"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="bottom|right"
android:focusable="false"
android:visibility="gone"
android:layout_marginRight="3dp"
android:src="@drawable/selected" />
</FrameLayout>
</LinearLayout>
<LinearLayout
android:id="@+id/font_size_selector"
android:layout_width="fill_parent"
android:layout_height="wrap_content"
android:background="@drawable/font_size_selector_bg"
android:layout_gravity="bottom"
android:visibility="gone">
<FrameLayout
android:id="@+id/ll_font_small"
android:layout_width="0dip"
<LinearLayout
android:id="@+id/font_size_selector"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_weight="1">
android:background="@drawable/font_size_selector_bg"
android:layout_gravity="bottom"
android:visibility="gone">
<LinearLayout
android:layout_width="wrap_content"
<FrameLayout
android:id="@+id/ll_font_small"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:orientation="vertical"
android:layout_gravity="center"
android:gravity="center">
android:layout_weight="1">
<ImageView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:src="@drawable/font_small"
android:layout_marginBottom="5dip" />
<TextView
<LinearLayout
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/menu_font_small"
android:textAppearance="@style/TextAppearanceUnderMenuIcon" />
</LinearLayout>
android:orientation="vertical"
android:layout_gravity="center"
android:gravity="center">
<ImageView
android:id="@+id/iv_small_select"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="bottom|right"
android:layout_marginRight="6dip"
android:layout_marginBottom="-7dip"
android:focusable="false"
android:visibility="gone"
android:src="@drawable/selected" />
</FrameLayout>
<FrameLayout
android:id="@+id/ll_font_normal"
android:layout_width="0dip"
android:layout_height="wrap_content"
android:layout_weight="1">
<ImageView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:src="@drawable/font_small"
android:layout_marginBottom="5dp" />
<LinearLayout
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:orientation="vertical"
android:layout_gravity="center"
android:gravity="center">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/menu_font_small"
android:textAppearance="@style/TextAppearanceUnderMenuIcon" />
</LinearLayout>
<ImageView
android:id="@+id/iv_small_select"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:src="@drawable/font_normal"
android:layout_marginBottom="5dip" />
android:layout_gravity="bottom|right"
android:layout_marginRight="6dp"
android:layout_marginBottom="-7dp"
android:focusable="false"
android:visibility="gone"
android:src="@drawable/selected" />
</FrameLayout>
<FrameLayout
android:id="@+id/ll_font_normal"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1">
<TextView
<LinearLayout
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/menu_font_normal"
android:textAppearance="@style/TextAppearanceUnderMenuIcon" />
</LinearLayout>
android:orientation="vertical"
android:layout_gravity="center"
android:gravity="center">
<ImageView
android:id="@+id/iv_medium_select"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="bottom|right"
android:focusable="false"
android:visibility="gone"
android:layout_marginRight="6dip"
android:layout_marginBottom="-7dip"
android:src="@drawable/selected" />
</FrameLayout>
<FrameLayout
android:id="@+id/ll_font_large"
android:layout_width="0dip"
android:layout_height="wrap_content"
android:layout_weight="1">
<ImageView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:src="@drawable/font_normal"
android:layout_marginBottom="5dp" />
<LinearLayout
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:orientation="vertical"
android:layout_gravity="center"
android:gravity="center">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/menu_font_normal"
android:textAppearance="@style/TextAppearanceUnderMenuIcon" />
</LinearLayout>
<ImageView
android:id="@+id/iv_medium_select"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:src="@drawable/font_large"
android:layout_marginBottom="5dip" />
android:layout_gravity="bottom|right"
android:focusable="false"
android:visibility="gone"
android:layout_marginRight="6dp"
android:layout_marginBottom="-7dp"
android:src="@drawable/selected" />
</FrameLayout>
<FrameLayout
android:id="@+id/ll_font_large"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1">
<TextView
<LinearLayout
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/menu_font_large"
android:textAppearance="@style/TextAppearanceUnderMenuIcon" />
</LinearLayout>
android:orientation="vertical"
android:layout_gravity="center"
android:gravity="center">
<ImageView
android:id="@+id/iv_large_select"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="bottom|right"
android:focusable="false"
android:visibility="gone"
android:layout_marginRight="6dip"
android:layout_marginBottom="-7dip"
android:src="@drawable/selected" />
</FrameLayout>
<FrameLayout
android:id="@+id/ll_font_super"
android:layout_width="0dip"
android:layout_height="wrap_content"
android:layout_weight="1">
<ImageView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:src="@drawable/font_large"
android:layout_marginBottom="5dp" />
<LinearLayout
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:orientation="vertical"
android:layout_gravity="center"
android:gravity="center">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/menu_font_large"
android:textAppearance="@style/TextAppearanceUnderMenuIcon" />
</LinearLayout>
<ImageView
android:id="@+id/iv_large_select"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:src="@drawable/font_super"
android:layout_marginBottom="5dip" />
android:layout_gravity="bottom|right"
android:focusable="false"
android:visibility="gone"
android:layout_marginRight="6dp"
android:layout_marginBottom="-7dp"
android:src="@drawable/selected" />
</FrameLayout>
<FrameLayout
android:id="@+id/ll_font_super"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1">
<TextView
<LinearLayout
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/menu_font_super"
android:textAppearance="@style/TextAppearanceUnderMenuIcon" />
</LinearLayout>
android:orientation="vertical"
android:layout_gravity="center"
android:gravity="center">
<ImageView
android:id="@+id/iv_super_select"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="bottom|right"
android:focusable="false"
android:visibility="gone"
android:layout_marginRight="6dip"
android:layout_marginBottom="-7dip"
android:src="@drawable/selected" />
</FrameLayout>
<ImageView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:src="@drawable/font_super"
android:layout_marginBottom="5dp" />
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/menu_font_super"
android:textAppearance="@style/TextAppearanceUnderMenuIcon" />
</LinearLayout>
<ImageView
android:id="@+id/iv_super_select"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="bottom|right"
android:focusable="false"
android:visibility="gone"
android:layout_marginRight="6dp"
android:layout_marginBottom="-7dp"
android:src="@drawable/selected" />
</FrameLayout>
</LinearLayout>
</LinearLayout>
</FrameLayout>
</LinearLayout>

@ -1,80 +1,101 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Copyright (c) 2010-2011, The MiCode Open Source Community (www.micode.net)
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
-->
<!-- 现代化布局:使用 CoordinatorLayout + AppBarLayout + Toolbar -->
<androidx.coordinatorlayout.widget.CoordinatorLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="@drawable/list_background">
<!-- AppBarLayout替代传统 ActionBar -->
<com.google.android.material.appbar.AppBarLayout
android:id="@+id/appbar"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:theme="@style/ThemeOverlay.Material3.ActionBar">
<!-- Toolbar现代替代 ActionBar 的标准组件 -->
<com.google.android.material.appbar.MaterialToolbar
android:id="@+id/toolbar"
android:layout_width="match_parent"
android:layout_height="?attr/actionBarSize"
app:title="@string/app_name"
app:layout_scrollFlags="scroll|enterAlways|snap" />
</com.google.android.material.appbar.AppBarLayout>
<!-- 便签列表:使用 NestedScrollView 包裹 ListView 以支持滚动 -->
<androidx.core.widget.NestedScrollView
android:layout_width="match_parent"
android:layout_height="match_parent"
android:fillViewport="true"
app:layout_behavior="@string/appbar_scrolling_view_behavior">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical">
<!-- 便签列表 -->
<ListView
android:id="@+id/notes_list"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:minHeight="200dp"
android:cacheColorHint="@null"
android:listSelector="@android:color/transparent"
android:divider="@null"
android:fadingEdge="@null" />
</LinearLayout>
</androidx.core.widget.NestedScrollView>
<!-- 悬浮按钮:替代原来的底部按钮 -->
<com.google.android.material.floatingactionbutton.FloatingActionButton
android:id="@+id/btn_new_note"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="bottom|end"
android:layout_margin="16dp"
android:contentDescription="@string/notelist_menu_new"
app:srcCompat="@android:drawable/ic_input_add" />
</androidx.coordinatorlayout.widget.CoordinatorLayout>
<!-- 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.
-->
<!-- DrawerLayout支持侧栏滑动 -->
<androidx.drawerlayout.widget.DrawerLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:id="@+id/drawer_layout"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="@drawable/list_background">
<!-- 主内容区域 -->
<androidx.coordinatorlayout.widget.CoordinatorLayout
android:layout_width="match_parent"
android:layout_height="match_parent">
<!-- AppBarLayout替代传统 ActionBar -->
<com.google.android.material.appbar.AppBarLayout
android:id="@+id/appbar"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:theme="@style/ThemeOverlay.Material3.ActionBar">
<!-- Toolbar现代替代 ActionBar 的标准组件,带汉堡菜单按钮 -->
<com.google.android.material.appbar.MaterialToolbar
android:id="@+id/toolbar"
android:layout_width="match_parent"
android:layout_height="?attr/actionBarSize"
app:title="@string/app_name"
app:navigationIcon="@android:drawable/ic_menu_sort_by_size"
app:layout_scrollFlags="scroll|enterAlways|snap" />
</com.google.android.material.appbar.AppBarLayout>
<!-- 便签列表:使用 NestedScrollView 包裹 ListView 以支持滚动 -->
<androidx.core.widget.NestedScrollView
android:layout_width="match_parent"
android:layout_height="match_parent"
android:fillViewport="true"
app:layout_behavior="@string/appbar_scrolling_view_behavior">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical">
<!-- 面包屑导航:显示当前文件夹路径(在列表上方) -->
<include layout="@layout/breadcrumb_layout" />
<!-- 便签列表 -->
<ListView
android:id="@+id/notes_list"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:minHeight="200dp"
android:cacheColorHint="@null"
android:listSelector="@android:color/transparent"
android:divider="@null"
android:fadingEdge="@null" />
</LinearLayout>
</androidx.core.widget.NestedScrollView>
<!-- 悬浮按钮:替代原来的底部按钮 -->
<com.google.android.material.floatingactionbutton.FloatingActionButton
android:id="@+id/btn_new_note"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="bottom|end"
android:layout_margin="16dp"
android:contentDescription="@string/notelist_menu_new"
app:srcCompat="@android:drawable/ic_input_add" />
</androidx.coordinatorlayout.widget.CoordinatorLayout>
<!-- 侧栏 -->
<fragment
android:id="@+id/sidebar_fragment"
android:name="net.micode.notes.ui.SidebarFragment"
android:layout_width="280dp"
android:layout_height="match_parent"
android:layout_gravity="start"
android:tag="sidebar" />
</androidx.drawerlayout.widget.DrawerLayout>

@ -0,0 +1,73 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
侧栏文件夹项布局
支持展开/收起图标、文件夹图标、名称和数量
-->
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal"
android:padding="12dp"
android:background="?attr/selectableItemBackground"
android:gravity="center_vertical">
<!-- 缩进(用于子文件夹) -->
<View
android:id="@+id/indent_view"
android:layout_width="0dp"
android:layout_height="match_parent" />
<!-- 展开/收起箭头 -->
<ImageView
android:id="@+id/iv_expand_icon"
android:layout_width="24dp"
android:layout_height="24dp"
android:src="@android:drawable/arrow_down_float"
android:contentDescription="展开/收起"
android:scaleType="centerInside"
android:rotation="0" />
<!-- 文件夹图标 -->
<ImageView
android:id="@+id/iv_folder_icon"
android:layout_width="32dp"
android:layout_height="32dp"
android:layout_marginStart="8dp"
android:src="@android:drawable/ic_menu_myplaces"
android:contentDescription="文件夹"
android:scaleType="centerInside" />
<!-- 文件夹名称和数量 -->
<LinearLayout
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:layout_marginStart="12dp"
android:orientation="vertical"
android:gravity="center_vertical">
<!-- 文件夹名称 -->
<TextView
android:id="@+id/tv_folder_name"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:textSize="16sp"
android:textColor="@android:color/black"
android:ellipsize="end"
android:maxLines="1"
tools:text="工作" />
<!-- 文件夹数量 -->
<TextView
android:id="@+id/tv_note_count"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:textSize="12sp"
android:textColor="@android:color/darker_gray"
android:ellipsize="end"
android:maxLines="1"
tools:text="5个便签" />
</LinearLayout>
</LinearLayout>

@ -0,0 +1,174 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
侧栏布局文件
包含文件夹树、菜单项、操作栏和回收站入口
-->
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:id="@+id/sidebar_container"
android:layout_width="280dp"
android:layout_height="match_parent"
android:layout_gravity="start"
android:orientation="vertical"
android:background="@android:color/white"
android:elevation="8dp">
<!-- 操作栏 -->
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal"
android:padding="16dp"
android:background="@android:color/white"
android:elevation="4dp">
<!-- 关闭按钮 -->
<ImageButton
android:id="@+id/btn_close_sidebar"
android:layout_width="40dp"
android:layout_height="40dp"
android:background="?attr/selectableItemBackgroundBorderless"
android:contentDescription="关闭侧栏"
android:padding="8dp"
android:src="@android:drawable/ic_menu_close_clear_cancel" />
<View
android:layout_width="0dp"
android:layout_height="match_parent"
android:layout_weight="1" />
<!-- 创建文件夹按钮 -->
<ImageButton
android:id="@+id/btn_create_folder"
android:layout_width="40dp"
android:layout_height="40dp"
android:background="?attr/selectableItemBackgroundBorderless"
android:contentDescription="创建文件夹"
android:padding="8dp"
android:src="@android:drawable/ic_input_add" />
</LinearLayout>
<!-- 分隔线 -->
<View
android:layout_width="match_parent"
android:layout_height="1dp"
android:background="@android:color/darker_gray" />
<!-- 根文件夹(始终显示) -->
<TextView
android:id="@+id/tv_root_folder"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:padding="16dp"
android:drawableStart="@android:drawable/ic_menu_myplaces"
android:drawablePadding="12dp"
android:text="@string/root_folder_name"
android:textSize="16sp"
android:textColor="@android:color/black"
android:background="?attr/selectableItemBackground"
android:gravity="center_vertical" />
<!-- 分隔线 -->
<View
android:layout_width="match_parent"
android:layout_height="1dp"
android:background="@android:color/darker_gray" />
<!-- 文件夹树列表 -->
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/rv_folder_tree"
android:layout_width="match_parent"
android:layout_height="0dp"
android:layout_weight="1"
android:padding="8dp"
android:clipToPadding="false" />
<!-- 分隔线 -->
<View
android:layout_width="match_parent"
android:layout_height="1dp"
android:background="@android:color/darker_gray" />
<!-- 菜单项 -->
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical">
<!-- 同步 -->
<TextView
android:id="@+id/menu_sync"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:padding="16dp"
android:drawableStart="@android:drawable/ic_menu_upload"
android:drawablePadding="12dp"
android:text="@string/menu_sync"
android:textSize="16sp"
android:textColor="@android:color/black"
android:background="?attr/selectableItemBackground"
android:gravity="center_vertical" />
<!-- 登录 -->
<TextView
android:id="@+id/menu_login"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:padding="16dp"
android:drawableStart="@android:drawable/ic_lock_lock"
android:drawablePadding="12dp"
android:text="@string/menu_login"
android:textSize="16sp"
android:textColor="@android:color/black"
android:background="?attr/selectableItemBackground"
android:gravity="center_vertical" />
<!-- 导出 -->
<TextView
android:id="@+id/menu_export"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:padding="16dp"
android:drawableStart="@android:drawable/ic_menu_save"
android:drawablePadding="12dp"
android:text="@string/menu_export"
android:textSize="16sp"
android:textColor="@android:color/black"
android:background="?attr/selectableItemBackground"
android:gravity="center_vertical" />
<!-- 设置 -->
<TextView
android:id="@+id/menu_settings"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:padding="16dp"
android:drawableStart="@android:drawable/ic_menu_preferences"
android:drawablePadding="12dp"
android:text="@string/menu_settings"
android:textSize="16sp"
android:textColor="@android:color/black"
android:background="?attr/selectableItemBackground"
android:gravity="center_vertical" />
</LinearLayout>
<!-- 分隔线 -->
<View
android:layout_width="match_parent"
android:layout_height="1dp"
android:background="@android:color/darker_gray" />
<!-- 回收站(固定底部) -->
<TextView
android:id="@+id/menu_trash"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:padding="16dp"
android:drawableStart="@android:drawable/ic_menu_delete"
android:drawablePadding="12dp"
android:text="@string/menu_trash"
android:textSize="16sp"
android:textColor="@android:color/black"
android:background="?attr/selectableItemBackground"
android:gravity="center_vertical" />
</LinearLayout>

@ -0,0 +1,20 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
多选模式的Toolbar菜单
包含删除、移动、取消选项
-->
<menu xmlns:android="http://schemas.android.com/apk/res/android">
<item
android:id="@+id/multi_select_delete"
android:icon="@android:drawable/ic_menu_delete"
android:title="@string/menu_delete"
android:showAsAction="ifRoom" />
<item
android:id="@+id/multi_select_move"
android:icon="@android:drawable/ic_menu_sort_by_size"
android:title="@string/menu_move"
android:showAsAction="ifRoom" />
</menu>

@ -0,0 +1,22 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
多选模式的Toolbar布局
包含删除、移动按钮
-->
<menu xmlns:android="http://schemas.android.com/apk/res/android">
<item
android:id="@+id/action_delete"
android:icon="@android:drawable/ic_menu_delete"
android:title="@string/menu_delete"
android:showAsAction="always"
android:orderInCategory="1" />
<item
android:id="@+id/action_move"
android:icon="@android:drawable/ic_menu_sort_by_size"
android:title="@string/menu_move"
android:showAsAction="always"
android:orderInCategory="2" />
</menu>

@ -137,4 +137,19 @@
<string name="empty_notes_hint">暂无便签,点击右下角按钮创建</string>
<string name="empty_notes_icon">空便签图标</string>
<string name="menu_edit_note">Edit note</string>
<!-- Sidebar strings -->
<string name="root_folder_name">我的便签</string>
<string name="sidebar_close">关闭侧栏</string>
<string name="sidebar_create_folder">创建文件夹</string>
<string name="menu_login">登录</string>
<string name="menu_export">导出</string>
<string name="menu_settings">设置</string>
<string name="menu_trash">回收站</string>
<string name="folder_note_count">%1$d个便签</string>
<string name="dialog_create_folder_title">创建文件夹</string>
<string name="dialog_create_folder_hint">请输入文件夹名称</string>
<string name="error_folder_name_empty">文件夹名称不能为空</string>
<string name="error_folder_name_too_long">文件夹名称不能超过50个字符</string>
<string name="error_folder_name_exists">该文件夹已存在</string>
<string name="create_folder_success">创建文件夹成功</string>
</resources>

@ -7,4 +7,11 @@
</style>
<style name="Theme.Notesmaster" parent="Base.Theme.Notesmaster" />
<!-- NoteEditActivity使用的主题统一使用Material3风格 -->
<style name="Theme.Notesmaster.Edit" parent="Theme.Material3.Light.NoActionBar">
<item name="colorPrimary">@color/primary_color</item>
<item name="colorOnPrimary">@color/on_primary_color</item>
<item name="android:statusBarColor">@color/primary_color</item>
</style>
</resources>

@ -26,6 +26,7 @@ import org.mockito.Mock;
import org.mockito.junit.MockitoJUnitRunner;
import net.micode.notes.model.Note;
import net.micode.notes.data.NotesRepository.NoteInfo;
import java.util.List;
@ -77,9 +78,9 @@ public class NotesRepositoryTest {
long folderId = Notes.ID_ROOT_FOLDER;
// Act
repository.getNotes(folderId, new NotesRepository.Callback<List<Note>>() {
repository.getNotes(folderId, new NotesRepository.Callback<List<NoteInfo>>() {
@Override
public void onSuccess(List<Note> result) {
public void onSuccess(List<NoteInfo> result) {
assertNotNull("Notes list should not be null", result);
// Mock返回空列表实际应用中会有数据
assertTrue("Notes count should be >= 0", result.size() >= 0);
@ -171,9 +172,9 @@ public class NotesRepositoryTest {
String keyword = "测试";
// Act
repository.searchNotes(keyword, new NotesRepository.Callback<List<Note>>() {
repository.searchNotes(keyword, new NotesRepository.Callback<List<NoteInfo>>() {
@Override
public void onSuccess(List<Note> result) {
public void onSuccess(List<NoteInfo> result) {
assertNotNull("Search results should not be null", result);
assertTrue("Search results count should be >= 0", result.size() >= 0);
}
@ -194,9 +195,9 @@ public class NotesRepositoryTest {
String keyword = "";
// Act
repository.searchNotes(keyword, new NotesRepository.Callback<List<Note>>() {
repository.searchNotes(keyword, new NotesRepository.Callback<List<NoteInfo>>() {
@Override
public void onSuccess(List<Note> result) {
public void onSuccess(List<NoteInfo> result) {
assertNotNull("Search results should not be null", result);
// 空关键字应返回空列表
assertEquals("Search results should be empty", 0, result.size());
@ -238,9 +239,9 @@ public class NotesRepositoryTest {
@Test
public void testGetFolders() {
// Act
repository.getFolders(new NotesRepository.Callback<List<Note>>() {
repository.getFolders(new NotesRepository.Callback<List<NoteInfo>>() {
@Override
public void onSuccess(List<Note> result) {
public void onSuccess(List<NoteInfo> result) {
assertNotNull("Folders should not be null", result);
assertTrue("Folders count should be >= 0", result.size() >= 0);
}

@ -7,6 +7,7 @@ appcompat = "1.6.1"
material = "1.10.0"
activity = "1.8.0"
constraintlayout = "2.1.4"
mockito = "4.11.0"
[libraries]
junit = { group = "junit", name = "junit", version.ref = "junit" }
@ -16,6 +17,8 @@ appcompat = { group = "androidx.appcompat", name = "appcompat", version.ref = "a
material = { group = "com.google.android.material", name = "material", version.ref = "material" }
activity = { group = "androidx.activity", name = "activity", version.ref = "activity" }
constraintlayout = { group = "androidx.constraintlayout", name = "constraintlayout", version.ref = "constraintlayout" }
mockito-core = { group = "org.mockito", name = "mockito-core", version.ref = "mockito" }
mockito-junit = { group = "org.mockito", name = "mockito-junit-jupiter", version.ref = "mockito" }
[plugins]
android-application = { id = "com.android.application", version.ref = "agp" }

Loading…
Cancel
Save