/* * 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 *

* 显示文件夹树、菜单项和操作按钮 * 提供文件夹导航、创建、展开/收起等功能 *

*/ 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() { @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 { private List folderItems; private FolderListViewModel viewModel; private OnFolderItemClickListener folderItemClickListener; public FolderTreeAdapter(List folderItems, FolderListViewModel viewModel) { this.folderItems = folderItems; this.viewModel = viewModel; } public void setData(List 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; } }