You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
git/app/src/main/java/net/micode/notes/ui/SidebarFragment.java

518 lines
18 KiB

This file contains ambiguous Unicode characters!

This file contains ambiguous Unicode characters that may be confused with others in your current locale. If your use case is intentional and legitimate, you can safely ignore this warning. Use the Escape button to highlight these characters.

/*
* 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);
adapter.setOnFolderItemClickListener(this::handleFolderItemClick);
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 -> {
android.util.Log.d(TAG, "setupFolderClickListener: folderId=" + folderId);
long currentTime = System.currentTimeMillis();
if (lastClickedView == view && (currentTime - lastClickTime) < DOUBLE_CLICK_INTERVAL) {
android.util.Log.d(TAG, "Double click on root folder, jumping to: " + folderId);
// 这是双击,执行跳转
if (listener != null) {
// 根文件夹也可以跳转(回到根)
listener.onFolderSelected(folderId);
}
// 重置双击状态
lastClickTime = 0;
lastClickedView = null;
} else {
android.util.Log.d(TAG, "Single click on root folder, will toggle expand in " + DOUBLE_CLICK_INTERVAL + "ms");
// 可能是单击,延迟处理
lastClickTime = currentTime;
lastClickedView = view;
view.postDelayed(() -> {
// 如果在延迟期间没有发生双击,则执行单击操作(展开/收起)
if (System.currentTimeMillis() - lastClickTime >= DOUBLE_CLICK_INTERVAL) {
android.util.Log.d(TAG, "Toggling root folder expand");
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) {
android.util.Log.d(TAG, "toggleFolderExpand: folderId=" + folderId);
viewModel.toggleFolderExpand(folderId);
}
/**
* 处理文件夹项点击(单击/双击)
*/
private void handleFolderItemClick(long folderId) {
android.util.Log.d(TAG, "handleFolderItemClick: folderId=" + folderId);
long currentTime = System.currentTimeMillis();
if (lastClickedFolderId == folderId && (currentTime - lastFolderClickTime) < DOUBLE_CLICK_INTERVAL) {
android.util.Log.d(TAG, "Double click detected, jumping to folder: " + folderId);
// 这是双击,执行跳转
if (listener != null) {
listener.onFolderSelected(folderId);
}
// 重置双击状态
lastFolderClickTime = 0;
lastClickedFolderId = -1;
} else {
android.util.Log.d(TAG, "Single click, will toggle expand in " + DOUBLE_CLICK_INTERVAL + "ms");
// 可能是单击,延迟处理
lastFolderClickTime = currentTime;
lastClickedFolderId = folderId;
new android.os.Handler().postDelayed(() -> {
// 如果在延迟期间没有发生双击,则执行单击操作(展开/收起)
if (System.currentTimeMillis() - lastFolderClickTime >= DOUBLE_CLICK_INTERVAL) {
android.util.Log.d(TAG, "Toggling folder expand: " + folderId);
toggleFolderExpand(folderId);
}
}, DOUBLE_CLICK_INTERVAL);
}
}
// 双击检测专用变量(针对文件夹列表项)
private long lastFolderClickTime = 0;
private long lastClickedFolderId = -1;
/**
* 显示创建文件夹对话框
*/
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(), R.string.create_folder_success, Toast.LENGTH_SHORT).show();
// 刷新文件夹列表
viewModel.loadFolderTree();
});
}
}
@Override
public void onError(Exception error) {
if (getActivity() != null) {
getActivity().runOnUiThread(() -> {
Toast.makeText(requireContext(),
getString(R.string.error_folder_name_too_long) + ": " + 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;
private OnFolderItemClickListener folderItemClickListener;
public FolderTreeAdapter(List<FolderTreeItem> folderItems, FolderListViewModel viewModel) {
this.folderItems = folderItems;
this.viewModel = viewModel;
}
public void setData(List<FolderTreeItem> folderItems) {
this.folderItems = folderItems;
}
public void setOnFolderItemClickListener(OnFolderItemClickListener listener) {
this.folderItemClickListener = listener;
}
@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, folderItemClickListener);
}
@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;
private FolderTreeItem currentItem;
private final OnFolderItemClickListener folderItemClickListener;
public FolderViewHolder(@NonNull View itemView, OnFolderItemClickListener listener) {
super(itemView);
this.folderItemClickListener = listener;
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) {
this.currentItem = item;
// 设置缩进
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));
// 设置点击监听器
itemView.setOnClickListener(v -> {
if (folderItemClickListener != null) {
folderItemClickListener.onFolderClick(item.folderId);
}
});
}
}
}
/**
* 文件夹项点击监听器接口
*/
public interface OnFolderItemClickListener {
void onFolderClick(long folderId);
}
/**
* 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;
}
}