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