From 86d39b7a950fa28c4ebe897297bdda5e6985d3d1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=8C=85=E5=B0=94=E4=BF=8A?= Date: Sun, 1 Feb 2026 19:45:04 +0800 Subject: [PATCH 1/2] =?UTF-8?q?feat:=20=E6=96=B0=E5=A2=9E=E5=A4=9A?= =?UTF-8?q?=E8=AF=AD=E8=A8=80=E6=94=AF=E6=8C=81=E5=92=8C=E5=9F=BA=E7=A1=80?= =?UTF-8?q?Activity=E9=87=8D=E6=9E=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 引入BaseActivity统一处理多语言切换,所有Activity继承BaseActivity - 添加LocaleHelper工具类实现应用内语言切换功能 - 重构设置页面,移除Google Tasks同步相关功能,新增字体大小、随机背景色和多语言设置 - 优化侧边栏布局,将退出登录按钮移至底部并移除登录按钮 - 修复字体大小设置类型转换问题,支持字符串存储 - 新增笔记导出功能集成到设置页面 - 完善中英文翻译字符串资源,统一界面文本显示 - 更新应用版本至1.1.0 --- .../java/net/micode/notes/MainActivity.java | 3 +- .../net/micode/notes/NotesApplication.java | 8 + .../micode/notes/data/NotesRepository.java | 6 + .../net/micode/notes/tool/LocaleHelper.java | 92 ++++++++ .../micode/notes/ui/AlarmAlertActivity.java | 7 + .../net/micode/notes/ui/BaseActivity.java | 12 ++ .../micode/notes/ui/CapsuleListActivity.java | 2 +- .../net/micode/notes/ui/LoginActivity.java | 2 +- .../net/micode/notes/ui/NoteEditActivity.java | 9 +- .../micode/notes/ui/NoteSearchActivity.java | 2 +- .../micode/notes/ui/NotesListActivity.java | 86 +++++++- .../micode/notes/ui/NotesListFragment.java | 2 +- .../notes/ui/NotesPreferenceActivity.java | 16 +- .../net/micode/notes/ui/SettingsActivity.java | 2 +- .../net/micode/notes/ui/SettingsFragment.java | 91 ++++++-- .../net/micode/notes/ui/SidebarFragment.java | 17 +- .../net/micode/notes/ui/SyncActivity.java | 2 +- .../net/micode/notes/ui/TaskEditActivity.java | 2 +- .../net/micode/notes/ui/TaskListActivity.java | 2 +- .../notes/viewmodel/NotesListViewModel.java | 14 +- .../app/src/main/res/layout/activity_home.xml | 2 +- .../src/main/res/layout/add_account_text.xml | 32 --- .../src/main/res/layout/fragment_sidebar.xml | 93 ++++---- .../src/main/res/values-zh-rCN/strings.xml | 185 ++++++++++------ .../src/main/res/values-zh-rTW/strings.xml | 203 ++++++++++++------ .../app/src/main/res/values/arrays.xml | 28 +++ .../app/src/main/res/values/strings.xml | 18 +- .../app/src/main/res/xml/preferences.xml | 34 ++- 28 files changed, 670 insertions(+), 302 deletions(-) create mode 100644 src/Notesmaster/app/src/main/java/net/micode/notes/tool/LocaleHelper.java create mode 100644 src/Notesmaster/app/src/main/java/net/micode/notes/ui/BaseActivity.java delete mode 100644 src/Notesmaster/app/src/main/res/layout/add_account_text.xml diff --git a/src/Notesmaster/app/src/main/java/net/micode/notes/MainActivity.java b/src/Notesmaster/app/src/main/java/net/micode/notes/MainActivity.java index 0fb0215..98e0c6d 100644 --- a/src/Notesmaster/app/src/main/java/net/micode/notes/MainActivity.java +++ b/src/Notesmaster/app/src/main/java/net/micode/notes/MainActivity.java @@ -15,6 +15,7 @@ import androidx.drawerlayout.widget.DrawerLayout; import net.micode.notes.data.Notes; import net.micode.notes.databinding.ActivityMainBinding; +import net.micode.notes.ui.BaseActivity; import net.micode.notes.ui.SidebarFragment; /** @@ -24,7 +25,7 @@ import net.micode.notes.ui.SidebarFragment; * 支持边到边显示模式,自动适配系统栏的边距。 *

*/ -public class MainActivity extends AppCompatActivity implements SidebarFragment.OnSidebarItemSelectedListener { +public class MainActivity extends BaseActivity implements SidebarFragment.OnSidebarItemSelectedListener { private static final String TAG = "MainActivity"; private ActivityMainBinding binding; diff --git a/src/Notesmaster/app/src/main/java/net/micode/notes/NotesApplication.java b/src/Notesmaster/app/src/main/java/net/micode/notes/NotesApplication.java index 74e1fea..ec92aaa 100644 --- a/src/Notesmaster/app/src/main/java/net/micode/notes/NotesApplication.java +++ b/src/Notesmaster/app/src/main/java/net/micode/notes/NotesApplication.java @@ -15,10 +15,18 @@ import android.provider.Settings; import com.google.android.material.color.DynamicColors; +import net.micode.notes.tool.LocaleHelper; +import android.content.Context; + public class NotesApplication extends Application { private static final String TAG = "NotesApplication"; + @Override + protected void attachBaseContext(Context base) { + super.attachBaseContext(LocaleHelper.onAttach(base)); + } + @Override public void onCreate() { super.onCreate(); diff --git a/src/Notesmaster/app/src/main/java/net/micode/notes/data/NotesRepository.java b/src/Notesmaster/app/src/main/java/net/micode/notes/data/NotesRepository.java index 169d437..7c07d8d 100644 --- a/src/Notesmaster/app/src/main/java/net/micode/notes/data/NotesRepository.java +++ b/src/Notesmaster/app/src/main/java/net/micode/notes/data/NotesRepository.java @@ -32,6 +32,7 @@ import net.micode.notes.data.Notes.TextNote; import net.micode.notes.model.Note; import net.micode.notes.model.WorkingNote; import net.micode.notes.sync.SyncConstants; +import net.micode.notes.tool.ResourceParser; import java.util.ArrayList; import java.util.HashMap; @@ -633,6 +634,11 @@ public class NotesRepository { values.put(NoteColumns.LOCAL_MODIFIED, 1); values.put(NoteColumns.SNIPPET, ""); + // 设置默认背景颜色(支持随机背景设置) + if (context != null) { + values.put(NoteColumns.BG_COLOR_ID, ResourceParser.getDefaultBgId(context)); + } + Uri uri = contentResolver.insert(Notes.CONTENT_NOTE_URI, values); Long noteId = 0L; diff --git a/src/Notesmaster/app/src/main/java/net/micode/notes/tool/LocaleHelper.java b/src/Notesmaster/app/src/main/java/net/micode/notes/tool/LocaleHelper.java new file mode 100644 index 0000000..9f52c15 --- /dev/null +++ b/src/Notesmaster/app/src/main/java/net/micode/notes/tool/LocaleHelper.java @@ -0,0 +1,92 @@ +package net.micode.notes.tool; + +import android.content.Context; +import android.content.SharedPreferences; +import android.content.res.Configuration; +import android.content.res.Resources; +import android.os.Build; +import androidx.preference.PreferenceManager; + +import java.util.Locale; + +public class LocaleHelper { + + private static final String SELECTED_LANGUAGE = "Locale.Helper.Selected.Language"; + + public static Context onAttach(Context context) { + String lang = getPersistedData(context, Locale.getDefault().getLanguage()); + return setLocale(context, lang); + } + + public static Context onAttach(Context context, String defaultLanguage) { + String lang = getPersistedData(context, defaultLanguage); + return setLocale(context, lang); + } + + public static String getLanguage(Context context) { + return getPersistedData(context, Locale.getDefault().getLanguage()); + } + + public static Context setLocale(Context context, String language) { + persist(context, language); + + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) { + return updateResources(context, language); + } + + return updateResourcesLegacy(context, language); + } + + private static String getPersistedData(Context context, String defaultLanguage) { + SharedPreferences preferences = PreferenceManager.getDefaultSharedPreferences(context); + // 默认语言改为中文 (zh-CN) + return preferences.getString(SELECTED_LANGUAGE, "zh-CN"); + } + + private static void persist(Context context, String language) { + SharedPreferences preferences = PreferenceManager.getDefaultSharedPreferences(context); + SharedPreferences.Editor editor = preferences.edit(); + editor.putString(SELECTED_LANGUAGE, language); + editor.apply(); + } + + private static Context updateResources(Context context, String language) { + Locale locale = getLocale(language); + Locale.setDefault(locale); + + Configuration configuration = context.getResources().getConfiguration(); + configuration.setLocale(locale); + configuration.setLayoutDirection(locale); + + return context.createConfigurationContext(configuration); + } + + private static Context updateResourcesLegacy(Context context, String language) { + Locale locale = getLocale(language); + Locale.setDefault(locale); + + Resources resources = context.getResources(); + Configuration configuration = resources.getConfiguration(); + configuration.locale = locale; + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR1) { + configuration.setLayoutDirection(locale); + } + + resources.updateConfiguration(configuration, resources.getDisplayMetrics()); + + return context; + } + + private static Locale getLocale(String language) { + if (language.equals("zh-CN")) { + return Locale.SIMPLIFIED_CHINESE; + } else if (language.equals("zh-TW")) { + return Locale.TRADITIONAL_CHINESE; + } else if (language.equals("en")) { + return Locale.ENGLISH; + } else if (language.equals("system")) { + return Locale.getDefault(); + } + return new Locale(language); + } +} diff --git a/src/Notesmaster/app/src/main/java/net/micode/notes/ui/AlarmAlertActivity.java b/src/Notesmaster/app/src/main/java/net/micode/notes/ui/AlarmAlertActivity.java index 9aec4ef..9211c52 100644 --- a/src/Notesmaster/app/src/main/java/net/micode/notes/ui/AlarmAlertActivity.java +++ b/src/Notesmaster/app/src/main/java/net/micode/notes/ui/AlarmAlertActivity.java @@ -39,6 +39,8 @@ import net.micode.notes.tool.DataUtils; import java.io.IOException; +import net.micode.notes.tool.LocaleHelper; + /** * 闹钟提醒活动 * @@ -55,6 +57,11 @@ import java.io.IOException; * @see net.micode.notes.tool.DataUtils */ public class AlarmAlertActivity extends Activity implements OnClickListener, OnDismissListener { + + @Override + protected void attachBaseContext(Context newBase) { + super.attachBaseContext(LocaleHelper.onAttach(newBase)); + } // 当前提醒的笔记ID private long mNoteId; // 笔记内容摘要 diff --git a/src/Notesmaster/app/src/main/java/net/micode/notes/ui/BaseActivity.java b/src/Notesmaster/app/src/main/java/net/micode/notes/ui/BaseActivity.java new file mode 100644 index 0000000..6eaf636 --- /dev/null +++ b/src/Notesmaster/app/src/main/java/net/micode/notes/ui/BaseActivity.java @@ -0,0 +1,12 @@ +package net.micode.notes.ui; + +import android.content.Context; +import androidx.appcompat.app.AppCompatActivity; +import net.micode.notes.tool.LocaleHelper; + +public class BaseActivity extends AppCompatActivity { + @Override + protected void attachBaseContext(Context newBase) { + super.attachBaseContext(LocaleHelper.onAttach(newBase)); + } +} diff --git a/src/Notesmaster/app/src/main/java/net/micode/notes/ui/CapsuleListActivity.java b/src/Notesmaster/app/src/main/java/net/micode/notes/ui/CapsuleListActivity.java index df5aa30..9f4605e 100644 --- a/src/Notesmaster/app/src/main/java/net/micode/notes/ui/CapsuleListActivity.java +++ b/src/Notesmaster/app/src/main/java/net/micode/notes/ui/CapsuleListActivity.java @@ -8,7 +8,7 @@ import androidx.appcompat.app.AppCompatActivity; import net.micode.notes.R; -public class CapsuleListActivity extends AppCompatActivity { +public class CapsuleListActivity extends BaseActivity { @Override protected void onCreate(@Nullable Bundle savedInstanceState) { diff --git a/src/Notesmaster/app/src/main/java/net/micode/notes/ui/LoginActivity.java b/src/Notesmaster/app/src/main/java/net/micode/notes/ui/LoginActivity.java index b602a4e..d966396 100644 --- a/src/Notesmaster/app/src/main/java/net/micode/notes/ui/LoginActivity.java +++ b/src/Notesmaster/app/src/main/java/net/micode/notes/ui/LoginActivity.java @@ -43,7 +43,7 @@ import net.micode.notes.viewmodel.LoginViewModel; * 遵循 MVVM 架构,所有业务逻辑委托给 {@link LoginViewModel}。 *

*/ -public class LoginActivity extends AppCompatActivity { +public class LoginActivity extends BaseActivity { private static final String TAG = "LoginActivity"; diff --git a/src/Notesmaster/app/src/main/java/net/micode/notes/ui/NoteEditActivity.java b/src/Notesmaster/app/src/main/java/net/micode/notes/ui/NoteEditActivity.java index 5a3b1c0..8cc84ae 100644 --- a/src/Notesmaster/app/src/main/java/net/micode/notes/ui/NoteEditActivity.java +++ b/src/Notesmaster/app/src/main/java/net/micode/notes/ui/NoteEditActivity.java @@ -88,7 +88,7 @@ import net.micode.notes.tool.SmartParser; import net.micode.notes.data.FontManager; -public class NoteEditActivity extends AppCompatActivity implements OnClickListener, +public class NoteEditActivity extends BaseActivity implements OnClickListener, NoteSettingChangedListener, OnTextViewChangeListener { /** * 笔记头部视图持有者 @@ -453,7 +453,12 @@ public class NoteEditActivity extends AppCompatActivity implements OnClickListen } mSharedPrefs = PreferenceManager.getDefaultSharedPreferences(this); - mFontSizeId = mSharedPrefs.getInt(PREFERENCE_FONT_SIZE, ResourceParser.BG_DEFAULT_FONT_SIZE); + try { + String fontSizeStr = mSharedPrefs.getString(PREFERENCE_FONT_SIZE, String.valueOf(ResourceParser.BG_DEFAULT_FONT_SIZE)); + mFontSizeId = Integer.parseInt(fontSizeStr); + } catch (ClassCastException e) { + 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, diff --git a/src/Notesmaster/app/src/main/java/net/micode/notes/ui/NoteSearchActivity.java b/src/Notesmaster/app/src/main/java/net/micode/notes/ui/NoteSearchActivity.java index 9fc2d88..cd1b38f 100644 --- a/src/Notesmaster/app/src/main/java/net/micode/notes/ui/NoteSearchActivity.java +++ b/src/Notesmaster/app/src/main/java/net/micode/notes/ui/NoteSearchActivity.java @@ -21,7 +21,7 @@ import net.micode.notes.tool.SearchHistoryManager; import java.util.ArrayList; import java.util.List; -public class NoteSearchActivity extends AppCompatActivity implements SearchView.OnQueryTextListener, NoteSearchAdapter.OnItemClickListener { +public class NoteSearchActivity extends BaseActivity implements SearchView.OnQueryTextListener, NoteSearchAdapter.OnItemClickListener { private SearchView mSearchView; private RecyclerView mRecyclerView; diff --git a/src/Notesmaster/app/src/main/java/net/micode/notes/ui/NotesListActivity.java b/src/Notesmaster/app/src/main/java/net/micode/notes/ui/NotesListActivity.java index fa8b2de..2be0b80 100644 --- a/src/Notesmaster/app/src/main/java/net/micode/notes/ui/NotesListActivity.java +++ b/src/Notesmaster/app/src/main/java/net/micode/notes/ui/NotesListActivity.java @@ -8,6 +8,7 @@ import android.content.IntentFilter; import android.os.Bundle; import android.util.Log; import android.view.View; +import android.widget.EditText; import android.widget.Toast; import androidx.annotation.NonNull; @@ -33,7 +34,7 @@ import net.micode.notes.viewmodel.NotesListViewModel; * 使用 ViewBinding 访问视图。 *

*/ -public class NotesListActivity extends AppCompatActivity implements SidebarFragment.OnSidebarItemSelectedListener { +public class NotesListActivity extends BaseActivity implements SidebarFragment.OnSidebarItemSelectedListener { private static final String TAG = "NotesListActivity"; private ActivityHomeBinding binding; @@ -200,7 +201,7 @@ public class NotesListActivity extends AppCompatActivity implements SidebarFragm new ViewModelProvider.Factory() { @Override public T create(Class modelClass) { - return (T) new NotesListViewModel(repository); + return (T) new NotesListViewModel(getApplication(), repository); } }).get(NotesListViewModel.class); @@ -250,6 +251,15 @@ public class NotesListActivity extends AppCompatActivity implements SidebarFragm updateSelectionCount(); updateSelectionActionUI(); }); + + viewModel.getSidebarRefreshNeeded().observe(this, needed -> { + if (needed) { + SidebarFragment fragment = (SidebarFragment) getSupportFragmentManager().findFragmentById(R.id.sidebar_fragment); + if (fragment != null) { + fragment.refreshFolderTree(); + } + } + }); } private void updateTrashModeUI(boolean isTrash) { @@ -372,10 +382,76 @@ public class NotesListActivity extends AppCompatActivity implements SidebarFragm binding.drawerLayout.closeDrawer(GravityCompat.START); startActivity(new Intent(this, CapsuleListActivity.class)); } - @Override public void onCreateFolder() { binding.drawerLayout.closeDrawer(GravityCompat.START); /* Show dialog */ } + @Override public void onCreateFolder() { + binding.drawerLayout.closeDrawer(GravityCompat.START); + SidebarFragment fragment = (SidebarFragment) getSupportFragmentManager().findFragmentById(R.id.sidebar_fragment); + if (fragment != null) { + fragment.showCreateFolderDialog(); + } + } @Override public void onCloseSidebar() { binding.drawerLayout.closeDrawer(GravityCompat.START); } - @Override public void onRenameFolder(long folderId) { /* Handle rename */ } - @Override public void onDeleteFolder(long folderId) { /* Handle delete */ } + @Override public void onRenameFolder(long folderId) { + binding.drawerLayout.closeDrawer(GravityCompat.START); + // Show rename dialog + viewModel.getFolderInfo(folderId, new NotesRepository.Callback() { + @Override + public void onSuccess(NotesRepository.NoteInfo folderInfo) { + runOnUiThread(() -> { + if (folderInfo != null) { + showRenameFolderDialog(folderId, folderInfo.snippet); + } + }); + } + + @Override + public void onError(Exception error) { + runOnUiThread(() -> Toast.makeText(NotesListActivity.this, "获取文件夹信息失败", Toast.LENGTH_SHORT).show()); + } + }); + } + + private void showRenameFolderDialog(long folderId, String currentName) { + final EditText input = new EditText(this); + input.setText(currentName); + input.setSelection(currentName.length()); + + new AlertDialog.Builder(this) + .setTitle(R.string.dialog_rename_folder_title) + .setView(input) + .setPositiveButton(R.string.menu_rename, (dialog, which) -> { + String newName = input.getText().toString().trim(); + if (!newName.isEmpty()) { + viewModel.renameFolder(folderId, newName); + } + }) + .setNegativeButton(android.R.string.cancel, null) + .show(); + } + + @Override public void onDeleteFolder(long folderId) { + binding.drawerLayout.closeDrawer(GravityCompat.START); + // Show delete confirmation + viewModel.getFolderInfo(folderId, new NotesRepository.Callback() { + @Override + public void onSuccess(NotesRepository.NoteInfo folderInfo) { + runOnUiThread(() -> { + if (folderInfo != null) { + new AlertDialog.Builder(NotesListActivity.this) + .setTitle(R.string.dialog_delete_folder_title) + .setMessage(String.format(getString(R.string.dialog_delete_folder_with_notes), folderInfo.snippet, folderInfo.notesCount)) + .setPositiveButton(R.string.menu_delete, (dialog, which) -> viewModel.deleteFolder(folderId)) + .setNegativeButton(android.R.string.cancel, null) + .show(); + } + }); + } + + @Override + public void onError(Exception error) { + runOnUiThread(() -> Toast.makeText(NotesListActivity.this, "获取文件夹信息失败", Toast.LENGTH_SHORT).show()); + } + }); + } private class MainPagerAdapter extends FragmentStateAdapter { public MainPagerAdapter(@NonNull AppCompatActivity activity) { super(activity); } diff --git a/src/Notesmaster/app/src/main/java/net/micode/notes/ui/NotesListFragment.java b/src/Notesmaster/app/src/main/java/net/micode/notes/ui/NotesListFragment.java index 2fca2c1..fe66a43 100644 --- a/src/Notesmaster/app/src/main/java/net/micode/notes/ui/NotesListFragment.java +++ b/src/Notesmaster/app/src/main/java/net/micode/notes/ui/NotesListFragment.java @@ -66,7 +66,7 @@ public class NotesListFragment extends Fragment implements new ViewModelProvider.Factory() { @Override public T create(Class modelClass) { - return (T) new NotesListViewModel(repository); + return (T) new NotesListViewModel(requireActivity().getApplication(), repository); } }).get(NotesListViewModel.class); } diff --git a/src/Notesmaster/app/src/main/java/net/micode/notes/ui/NotesPreferenceActivity.java b/src/Notesmaster/app/src/main/java/net/micode/notes/ui/NotesPreferenceActivity.java index de53616..e3b4290 100644 --- a/src/Notesmaster/app/src/main/java/net/micode/notes/ui/NotesPreferenceActivity.java +++ b/src/Notesmaster/app/src/main/java/net/micode/notes/ui/NotesPreferenceActivity.java @@ -11,7 +11,7 @@ import androidx.appcompat.app.AppCompatActivity; import net.micode.notes.R; -public class NotesPreferenceActivity extends AppCompatActivity { +public class NotesPreferenceActivity extends BaseActivity { public static final String PREFERENCE_NAME = "notes_preferences"; public static final String PREFERENCE_SYNC_ACCOUNT_NAME = "pref_key_account_name"; @@ -33,20 +33,6 @@ public class NotesPreferenceActivity extends AppCompatActivity { .replace(R.id.settings_container, new SettingsFragment()) .commit(); } - - loadSyncButton(); - } - - private void loadSyncButton() { - Button syncButton = findViewById(R.id.preference_sync_button); - TextView lastSyncTimeView = findViewById(R.id.prefenerece_sync_status_textview); - - // Google Tasks同步功能已禁用 - syncButton.setEnabled(false); - syncButton.setText("同步功能已禁用"); - - lastSyncTimeView.setText("Google Tasks同步功能已禁用"); - lastSyncTimeView.setVisibility(View.VISIBLE); } public static String getSyncAccountName(android.content.Context context) { diff --git a/src/Notesmaster/app/src/main/java/net/micode/notes/ui/SettingsActivity.java b/src/Notesmaster/app/src/main/java/net/micode/notes/ui/SettingsActivity.java index 8f9b68c..b87bb75 100644 --- a/src/Notesmaster/app/src/main/java/net/micode/notes/ui/SettingsActivity.java +++ b/src/Notesmaster/app/src/main/java/net/micode/notes/ui/SettingsActivity.java @@ -5,7 +5,7 @@ import androidx.appcompat.app.AppCompatActivity; import androidx.preference.PreferenceFragmentCompat; import net.micode.notes.R; -public class SettingsActivity extends AppCompatActivity { +public class SettingsActivity extends BaseActivity { @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); diff --git a/src/Notesmaster/app/src/main/java/net/micode/notes/ui/SettingsFragment.java b/src/Notesmaster/app/src/main/java/net/micode/notes/ui/SettingsFragment.java index b8a9eda..0b4a9db 100644 --- a/src/Notesmaster/app/src/main/java/net/micode/notes/ui/SettingsFragment.java +++ b/src/Notesmaster/app/src/main/java/net/micode/notes/ui/SettingsFragment.java @@ -29,10 +29,15 @@ import net.micode.notes.capsule.ClipboardMonitorService; import static android.app.Activity.RESULT_OK; public class SettingsFragment extends PreferenceFragmentCompat { - public static final String PREFERENCE_SYNC_ACCOUNT_KEY = "pref_sync_account_key"; public static final String PREFERENCE_SECURITY_KEY = "pref_key_security"; public static final String PREFERENCE_THEME_MODE = "pref_theme_mode"; public static final String PREFERENCE_CAPSULE_ENABLE = "pref_key_capsule_enable"; + public static final String PREFERENCE_FONT_SIZE = "pref_font_size"; + public static final String PREFERENCE_BG_RANDOM_APPEAR = "pref_key_bg_random_appear"; + public static final String PREFERENCE_LANGUAGE = "pref_language"; + public static final String PREFERENCE_CLOUD_SYNC_KEY = "pref_key_cloud_sync"; + public static final String PREFERENCE_EXPORT_NOTES_KEY = "pref_key_export_notes"; + public static final int REQUEST_CODE_CHECK_PASSWORD = 104; public static final int REQUEST_CODE_OVERLAY_PERMISSION = 105; @@ -41,10 +46,77 @@ public class SettingsFragment extends PreferenceFragmentCompat { setPreferencesFromResource(R.xml.preferences, rootKey); loadThemePreference(); loadSecurityPreference(); - loadAccountPreference(); loadCapsulePreference(); + loadCloudSyncPreference(); + loadExportPreference(); + loadFontSizePreference(); + loadLanguagePreference(); + } + + private void loadLanguagePreference() { + ListPreference languagePref = findPreference(PREFERENCE_LANGUAGE); + if (languagePref != null) { + languagePref.setOnPreferenceChangeListener((preference, newValue) -> { + String language = (String) newValue; + net.micode.notes.tool.LocaleHelper.setLocale(getContext(), language); + // 重新启动应用或 Activity 以应用更改 + getActivity().recreate(); + return true; + }); + } } + private void loadFontSizePreference() { + ListPreference fontSizePref = findPreference(PREFERENCE_FONT_SIZE); + if (fontSizePref != null) { + fontSizePref.setOnPreferenceChangeListener((preference, newValue) -> { + // ListPreference 默认存储字符串,NoteEditActivity 需要整数 + // 这里我们只需确保它能保存,NoteEditActivity 稍后会修改读取逻辑 + return true; + }); + } + } + + private void loadCloudSyncPreference() { + Preference syncPref = findPreference(PREFERENCE_CLOUD_SYNC_KEY); + if (syncPref != null) { + syncPref.setOnPreferenceClickListener(preference -> { + Intent intent = new Intent(getActivity(), SyncActivity.class); + startActivity(intent); + return true; + }); + } + } + + private void loadExportPreference() { + Preference exportPref = findPreference(PREFERENCE_EXPORT_NOTES_KEY); + if (exportPref != null) { + exportPref.setOnPreferenceClickListener(preference -> { + exportNotes(); + return true; + }); + } + } + + private void exportNotes() { + net.micode.notes.tool.BackupUtils backupUtils = net.micode.notes.tool.BackupUtils.getInstance(getContext()); + int state = backupUtils.exportToText(); + String message; + switch (state) { + case net.micode.notes.tool.BackupUtils.STATE_SUCCESS: + message = getString(R.string.success_sdcard_export) + ": " + backupUtils.getExportedTextFileDir() + backupUtils.getExportedTextFileName(); + break; + case net.micode.notes.tool.BackupUtils.STATE_SD_CARD_UNMOUONTED: + message = getString(R.string.error_sdcard_unmounted); + break; + case net.micode.notes.tool.BackupUtils.STATE_SYSTEM_ERROR: + default: + message = getString(R.string.error_sdcard_export); + break; + } + Toast.makeText(getContext(), message, Toast.LENGTH_LONG).show(); + } + private void loadCapsulePreference() { SwitchPreferenceCompat capsulePref = findPreference(PREFERENCE_CAPSULE_ENABLE); if (capsulePref != null) { @@ -183,21 +255,6 @@ public class SettingsFragment extends PreferenceFragmentCompat { } } - private void loadAccountPreference() { - androidx.preference.PreferenceCategory accountCategory = findPreference(PREFERENCE_SYNC_ACCOUNT_KEY); - if (accountCategory != null) { - accountCategory.removeAll(); - Preference accountPref = new Preference(getContext()); - accountPref.setTitle(getString(R.string.preferences_account_title)); - accountPref.setSummary(getString(R.string.preferences_account_summary)); - accountPref.setOnPreferenceClickListener(preference -> { - Toast.makeText(getActivity(), "Google Tasks同步功能已禁用", Toast.LENGTH_SHORT).show(); - return true; - }); - accountCategory.addPreference(accountPref); - } - } - private void showSetPasswordDialog() { new AlertDialog.Builder(getActivity()) .setTitle("设置密码") diff --git a/src/Notesmaster/app/src/main/java/net/micode/notes/ui/SidebarFragment.java b/src/Notesmaster/app/src/main/java/net/micode/notes/ui/SidebarFragment.java index f61cd7d..faec658 100644 --- a/src/Notesmaster/app/src/main/java/net/micode/notes/ui/SidebarFragment.java +++ b/src/Notesmaster/app/src/main/java/net/micode/notes/ui/SidebarFragment.java @@ -72,8 +72,8 @@ public class SidebarFragment extends Fragment { private LinearLayout menuExport; private LinearLayout menuSettings; private LinearLayout menuCapsule; - private LinearLayout menuLogin; private LinearLayout menuLogout; + private View logoutDivider; // 文件夹树 private LinearLayout folderTreeContainer; @@ -173,8 +173,8 @@ public class SidebarFragment extends Fragment { menuExport = view.findViewById(R.id.menu_export); menuSettings = view.findViewById(R.id.menu_settings); menuCapsule = view.findViewById(R.id.menu_capsule); - menuLogin = view.findViewById(R.id.menu_login); menuLogout = view.findViewById(R.id.menu_logout); + logoutDivider = view.findViewById(R.id.logout_divider); // 文件夹树 folderTreeContainer = view.findViewById(R.id.folder_tree_container); @@ -249,13 +249,6 @@ public class SidebarFragment extends Fragment { }); } - menuLogin.setOnClickListener(v -> { - if (listener != null) { - listener.onLoginSelected(); - listener.onCloseSidebar(); - } - }); - menuLogout.setOnClickListener(v -> showLogoutConfirmDialog()); } @@ -323,10 +316,12 @@ public class SidebarFragment extends Fragment { } // 更新菜单项 - if (menuLogin != null && menuLogout != null) { - menuLogin.setVisibility(isLoggedIn ? View.GONE : View.VISIBLE); + if (menuLogout != null) { menuLogout.setVisibility(isLoggedIn ? View.VISIBLE : View.GONE); } + if (logoutDivider != null) { + logoutDivider.setVisibility(isLoggedIn ? View.VISIBLE : View.GONE); + } } private void showLogoutConfirmDialog() { diff --git a/src/Notesmaster/app/src/main/java/net/micode/notes/ui/SyncActivity.java b/src/Notesmaster/app/src/main/java/net/micode/notes/ui/SyncActivity.java index 63c0391..66282db 100644 --- a/src/Notesmaster/app/src/main/java/net/micode/notes/ui/SyncActivity.java +++ b/src/Notesmaster/app/src/main/java/net/micode/notes/ui/SyncActivity.java @@ -42,7 +42,7 @@ import java.util.Locale; * 显示云同步设置,包括登录状态、同步开关、同步按钮和进度显示。 *

*/ -public class SyncActivity extends AppCompatActivity { +public class SyncActivity extends BaseActivity { private static final String PREFS_SYNC = "sync_settings"; private static final String KEY_AUTO_SYNC = "auto_sync"; diff --git a/src/Notesmaster/app/src/main/java/net/micode/notes/ui/TaskEditActivity.java b/src/Notesmaster/app/src/main/java/net/micode/notes/ui/TaskEditActivity.java index 8960a57..2ff4846 100644 --- a/src/Notesmaster/app/src/main/java/net/micode/notes/ui/TaskEditActivity.java +++ b/src/Notesmaster/app/src/main/java/net/micode/notes/ui/TaskEditActivity.java @@ -24,7 +24,7 @@ import net.micode.notes.model.Task; import java.util.Calendar; -public class TaskEditActivity extends AppCompatActivity { +public class TaskEditActivity extends BaseActivity { private EditText contentEdit; private ImageView alarmBtn; diff --git a/src/Notesmaster/app/src/main/java/net/micode/notes/ui/TaskListActivity.java b/src/Notesmaster/app/src/main/java/net/micode/notes/ui/TaskListActivity.java index 9b965d4..4a940c8 100644 --- a/src/Notesmaster/app/src/main/java/net/micode/notes/ui/TaskListActivity.java +++ b/src/Notesmaster/app/src/main/java/net/micode/notes/ui/TaskListActivity.java @@ -22,7 +22,7 @@ import net.micode.notes.model.Task; import java.util.ArrayList; import java.util.List; -public class TaskListActivity extends AppCompatActivity implements TaskListAdapter.OnTaskItemClickListener { +public class TaskListActivity extends BaseActivity implements TaskListAdapter.OnTaskItemClickListener { private RecyclerView recyclerView; private TaskListAdapter adapter; diff --git a/src/Notesmaster/app/src/main/java/net/micode/notes/viewmodel/NotesListViewModel.java b/src/Notesmaster/app/src/main/java/net/micode/notes/viewmodel/NotesListViewModel.java index 2b051dd..6265f9b 100644 --- a/src/Notesmaster/app/src/main/java/net/micode/notes/viewmodel/NotesListViewModel.java +++ b/src/Notesmaster/app/src/main/java/net/micode/notes/viewmodel/NotesListViewModel.java @@ -16,11 +16,15 @@ package net.micode.notes.viewmodel; +import android.app.Application; import android.util.Log; +import androidx.annotation.NonNull; +import androidx.lifecycle.AndroidViewModel; import androidx.lifecycle.MutableLiveData; import androidx.lifecycle.ViewModel; +import net.micode.notes.R; import net.micode.notes.data.Notes; import net.micode.notes.data.NotesRepository; @@ -38,7 +42,7 @@ import java.util.List; * @see NotesRepository * @see net.micode.notes.model.Note */ -public class NotesListViewModel extends ViewModel { +public class NotesListViewModel extends AndroidViewModel { private static final String TAG = "NotesListViewModel"; private final NotesRepository repository; @@ -80,9 +84,11 @@ public class NotesListViewModel extends ViewModel { /** * 构造函数 * + * @param application 应用程序上下文 * @param repository 笔记数据仓库 */ - public NotesListViewModel(NotesRepository repository) { + public NotesListViewModel(@NonNull Application application, NotesRepository repository) { + super(application); this.repository = repository; Log.d(TAG, "ViewModel created"); } @@ -185,7 +191,7 @@ public class NotesListViewModel extends ViewModel { // 1. "All" / "All Templates" Folder (Virtual) NotesRepository.NoteInfo allFolder = new NotesRepository.NoteInfo(); allFolder.setId(templateMode ? Notes.ID_TEMPLATE_FOLDER : Notes.ID_ALL_NOTES_FOLDER); - allFolder.snippet = templateMode ? "所有模板" : "所有"; // Name + allFolder.snippet = getApplication().getString(templateMode ? R.string.folder_all_templates : R.string.folder_all); // Name displayFolders.add(allFolder); // 2. Real Folders (from DB) @@ -197,7 +203,7 @@ public class NotesListViewModel extends ViewModel { if (!templateMode) { NotesRepository.NoteInfo uncategorizedFolder = new NotesRepository.NoteInfo(); uncategorizedFolder.setId(Notes.ID_ROOT_FOLDER); - uncategorizedFolder.snippet = "未分类"; // Custom Name for Root + uncategorizedFolder.snippet = getApplication().getString(R.string.folder_uncategorized); // Custom Name for Root displayFolders.add(uncategorizedFolder); } diff --git a/src/Notesmaster/app/src/main/res/layout/activity_home.xml b/src/Notesmaster/app/src/main/res/layout/activity_home.xml index 1a3cd5a..3814b7e 100644 --- a/src/Notesmaster/app/src/main/res/layout/activity_home.xml +++ b/src/Notesmaster/app/src/main/res/layout/activity_home.xml @@ -77,7 +77,7 @@ android:gravity="center_vertical" android:paddingStart="16dp" android:paddingEnd="16dp" - android:text="Search notes" + android:text="@string/search_hint" android:textColor="#9E9E9E" android:textSize="16sp" /> diff --git a/src/Notesmaster/app/src/main/res/layout/add_account_text.xml b/src/Notesmaster/app/src/main/res/layout/add_account_text.xml deleted file mode 100644 index c799178..0000000 --- a/src/Notesmaster/app/src/main/res/layout/add_account_text.xml +++ /dev/null @@ -1,32 +0,0 @@ - - - - - - - - \ No newline at end of file diff --git a/src/Notesmaster/app/src/main/res/layout/fragment_sidebar.xml b/src/Notesmaster/app/src/main/res/layout/fragment_sidebar.xml index 34683d4..eb2ab76 100644 --- a/src/Notesmaster/app/src/main/res/layout/fragment_sidebar.xml +++ b/src/Notesmaster/app/src/main/res/layout/fragment_sidebar.xml @@ -303,65 +303,46 @@ - - - - - - - - - - - - - - - + - + + + + + + + + + - + diff --git a/src/Notesmaster/app/src/main/res/values-zh-rCN/strings.xml b/src/Notesmaster/app/src/main/res/values-zh-rCN/strings.xml index ac490e5..27f798c 100644 --- a/src/Notesmaster/app/src/main/res/values-zh-rCN/strings.xml +++ b/src/Notesmaster/app/src/main/res/values-zh-rCN/strings.xml @@ -1,25 +1,9 @@ - - - 便签 - 便签2x2 - 便签4x4 + 便签 2x2 + 便签 4x4 没有关联内容,点击新建便签。 访客模式下,便签内容不可见 ... @@ -35,7 +19,13 @@ 发送邮件 浏览网页 打开地图 - + 创建闹钟 + 查看地图 + + /MIUI/notes/ + notes_%s.txt + + (%d) 新建文件夹 导出文本 同步 @@ -44,6 +34,7 @@ 搜索 删除 移动到文件夹 + 重命名 选中了 %d 项 没有选中项,操作无效 全选 @@ -56,9 +47,17 @@ 进入清单模式 退出清单模式 查看文件夹 - 刪除文件夹 + 删除文件夹 修改文件夹名称 文件夹 %1$s 已存在,请重新命名 + 待办事项 + + + 重命名文件夹 + 删除文件夹 + 删除 \"%1$s\" 及其 %2$d 条笔记? + 删除 \"%1$s\"? + 文件夹名称 分享 发送到桌面 提醒我 @@ -66,17 +65,19 @@ 选择文件夹 上一级文件夹 已添加到桌面 - 删除 + 确认删除文件夹及所包含的便签吗? + 删除所选便签 确认要删除所选的 %d 条便签吗? 确认要删除该条便签吗? - 确认删除文件夹及所包含的便签吗? 已将所选 %1$d 条便签移到 %2$s 文件夹 - + SD卡被占用,不能操作 导出文本时发生错误,请检查SD卡 要查看的便签不存在 不能为空便签设置闹钟提醒 不能将空便签发送到桌面 + 无效的意图 + 不支持的意图操作 导出成功 导出失败 已将文本文件(%1$s)输出至SD卡(%2$s)目录 @@ -91,28 +92,21 @@ 同步已取消 登录%1$s... 正在获取服务器便签列表... - 正在同步本地便签... 设置 - 同步账号 - 与google task同步便签记录 - 上次同步于 %1$s - 添加账号 - 更换账号 - 删除账号 - 取消 + yyyy-MM-dd hh:mm:ss 立即同步 取消同步 - 当前帐号 %1$s - 如更换同步帐号,过去的帐号同步信息将被清空,再次切换的同时可能会造成数据重复 - 同步便签 - 请选择google帐号,便签将与该帐号的google task内容同步。 - 正在同步中,不能修改同步帐号 - 同步帐号已设置为%1$s 新建便签背景颜色随机 + 语言 + 所有 + 所有模板 + 未分类 + 删除 通话便签 请输入名称 + 正在搜索便签 搜索便签 便签中的文字 @@ -123,50 +117,115 @@ %1$s 条符合"%2$s"的搜索结果 - - 我的便签 - %d 个便签 - 创建文件夹 - 文件夹名称 - 文件夹名称不能为空 - 文件夹名称过长(最多50个字符) - 回收站 - 创建文件夹成功 - - 撤回 - 重做 - 清空撤回历史 - 撤回成功 - 重做成功 - 无可撤回 - 无可重做 - - 重命名 - 重命名文件夹 - 删除文件夹 - 删除 "%1$s" 及其 %2$d 条笔记? - 删除 "%1$s"? - 文件夹名称 - 无效的意图 - 不支持的意图操作 暂无便签,点击右下角按钮创建 空便签图标 编辑便签 + 登录 导出 + 模板 设置 + 回收站 + 我的便签 关闭侧边栏 创建文件夹 + %d 个便签 + 创建文件夹 + 文件夹名称 + 文件夹名称不能为空 + 文件夹名称过长(最多50个字符) 文件夹已存在 + 创建文件夹成功 置顶 锁定 确定需要为其上锁? 确定 再想想 + 确定要删除选中的便签吗? 取消置顶 解锁 + + + 未找到结果 + 搜索历史 + 清空 + + + 撤销 + 重做 + 清空撤销历史 + 撤销成功 + 重做成功 + 无可撤销内容 + 无可重做内容 + 保存为模板 + 图片 + 富文本 + + + 小米便签全局速记服务。开启后请勿开启“无障碍快捷方式”,以免出现多余的系统悬浮按钮。 + 需要悬浮窗权限 + 全局速记胶囊需要悬浮窗权限才能显示在其他应用上层。 + 需要无障碍服务权限 + 全局速记胶囊需要无障碍服务权限来监听剪贴板和获取来源应用。 + 全局速记胶囊 + 开启侧边悬浮胶囊,支持跨应用拖拽和剪贴板速记 恢复 永久删除 + + 点击登录/注册 + 用户 + 设备 ID: 未知 + 已同步 + 同步中... + 同步失败 + 笔记 + 云同步 + 其他 + 全部笔记 + 收藏 + 提醒 + 立即同步 + 同步设置 + 帮助与反馈 + 登录 + 退出登录 + + + 云同步 + 登录状态 + 设备 ID: 未初始化 + 自动同步 + 最后同步时间: + 从不 + 同步状态: + 空闲 + 同步中... + 同步成功 + 同步失败 + 立即同步 + 同步已开始 + 同步成功完成 + 同步失败: %1$s + + + 退出登录 + 确定要退出登录吗?退出后本地笔记将保留,但无法同步到云端。 + 退出 + 已退出登录 + + + 文件夹 + 创建文件夹失败 + + + 笔记冲突 + 本地版本 + 云端版本 + 使用本地 + 使用云端 + 合并 + 合并功能即将推出 diff --git a/src/Notesmaster/app/src/main/res/values-zh-rTW/strings.xml b/src/Notesmaster/app/src/main/res/values-zh-rTW/strings.xml index e17bfdd..5efe94d 100644 --- a/src/Notesmaster/app/src/main/res/values-zh-rTW/strings.xml +++ b/src/Notesmaster/app/src/main/res/values-zh-rTW/strings.xml @@ -1,29 +1,13 @@ - - - - + - 便簽 - 便簽2x2 - 便簽4x4 - 沒有關聯內容,點擊新建便簽。 + 便籤 + 便籤 2x2 + 便籤 4x4 + 沒有關聯內容,點擊新建便籤。 訪客模式下,便籤內容不可見 ... - 新建便簽 + 新建便籤 成功刪除提醒 創建提醒 已過期 @@ -33,18 +17,24 @@ 查看 呼叫電話 發送郵件 - 浏覽網頁 + 瀏覽網頁 打開地圖 - 已將所選 %1$d 便籤移到 %2$s 文件夾 - + 創建鬧鐘 + 查看地圖 + + /MIUI/notes/ + notes_%s.txt + + (%d) 新建文件夾 導出文本 同步 取消同步 設置 - 搜尋 + 搜索 刪除 移動到文件夾 + 重命名 選中了 %d 項 沒有選中項,操作無效 全選 @@ -60,6 +50,14 @@ 刪除文件夾 修改文件夾名稱 文件夾 %1$s 已存在,請重新命名 + 待辦事項 + + + 重命名文件夾 + 刪除文件夾 + 刪除 \"%1$s\" 及其 %2$d 條筆記? + 刪除 \"%1$s\"? + 文件夾名稱 分享 發送到桌面 提醒我 @@ -67,20 +65,24 @@ 選擇文件夾 上一級文件夾 已添加到桌面 - 刪除 - 确认要刪除所選的 %d 條便籤嗎? - 确认要删除該條便籤嗎? - 確認刪除檔夾及所包含的便簽嗎? + 確認刪除文件夾及所包含的便籤嗎? + 刪除所選便籤 + 確認要刪除所選的 %d 條便籤嗎? + 確認要刪除該條便籤嗎? + 已將所選 %1$d 條便籤移到 %2$s 文件夾 + SD卡被佔用,不能操作 - 導出TXT時發生錯誤,請檢查SD卡 + 導出文本時發生錯誤,請檢查SD卡 要查看的便籤不存在 - 不能爲空便籤設置鬧鐘提醒 + 不能空便籤設置鬧鐘提醒 不能將空便籤發送到桌面 + 無效的意圖 + 不支持的意圖操作 導出成功 導出失敗 - 已將文本文件(%1$s)導出至SD(%2$s)目錄 + 已將文本文件(%1$s)輸出至SD卡(%2$s)目錄 - 同步便簽... + 同步便籤... 同步成功 同步失敗 同步已取消 @@ -88,27 +90,18 @@ 同步失敗,請檢查網絡和帳號設置 同步失敗,發生內部錯誤 同步已取消 - 登陸%1$s... + 登錄%1$s... 正在獲取服務器便籤列表... - 正在同步本地便籤... 設置 - 同步賬號 - 与google task同步便簽記錄 - 上次同步于 %1$s - 添加賬號 - 更換賬號 - 刪除賬號 - 取消 + yyyy-MM-dd hh:mm:ss 立即同步 取消同步 - 當前帳號 %1$s - 如更換同步帳號,過去的帳號同步信息將被清空,再次切換的同時可能會造成數據重復 - 同步便簽 - 請選擇google帳號,便簽將與該帳號的google task內容同步。 - 正在同步中,不能修改同步帳號 - 同步帳號已設置為%1$s 新建便籤背景顏色隨機 + 語言 + 所有 + 所有模板 + 未分類 刪除 通話便籤 @@ -121,46 +114,118 @@ 設置 取消 - %1$s 條符合"%2$s"的搜尋結果 + %1$s 條符合"%2$s"的搜索結果 - - 我的便籤 - %d 個便籤 - 創建文件夾 - 文件夾名稱 - 文件夾名稱不能為空 - 文件夾名稱過長(最多50個字符) - 回收站 - 創建文件夾成功 - - - 重命名 - 重命名文件夾 - 刪除文件夾 - 刪除 "%1$s" 及其 %2$d 條筆記? - 刪除 "%1$s"? - 文件夾名稱 - 無效的意圖 - 不支持的意圖操作 暫無便籤,點擊右下角按鈕創建 空便籤圖標 編輯便籤 + 登錄 導出 + 模板 設置 + 回收站 + 我的便籤 關閉側邊欄 創建文件夾 + %d 個便籤 + 創建文件夾 + 文件夾名稱 + 文件夾名稱不能為空 + 文件夾名稱過長(最多50個字符) 文件夾已存在 + 創建文件夾成功 置頂 鎖定 確定需要為其上鎖? 確定 再想想 + 確定要刪除選中的便籤嗎? 取消置頂 解鎖 + + + 未找到結果 + 搜索歷史 + 清空 + + + 撤銷 + 重做 + 清空撤銷歷史 + 撤銷成功 + 重做成功 + 無可撤銷內容 + 無可重做內容 + 保存為模板 + 圖片 + 富文本 + + + 小米便籤全局速記服務。開啟後請勿開啟“無障礙快捷方式”,以免出現多餘的系統懸浮按鈕。 + 需要懸浮窗權限 + 全局速記膠囊需要懸浮窗權限才能顯示在其他應用上層。 + 需要無障礙服務權限 + 全局速记胶囊需要无障碍服务权限来监听剪贴板和获取来源应用。 + 全局速記膠囊 + 開啟側邊懸浮膠囊,支持跨應用拖拽和剪貼板速記 恢復 永久刪除 + + 點擊登錄/註冊 + 用戶 + 設備 ID: 未知 + 已同步 + 同步中... + 同步失敗 + 筆記 + 雲同步 + 其他 + 全部筆記 + 收藏 + 提醒 + 立即同步 + 同步設置 + 幫助與反饋 + 登錄 + 退出登錄 + + + 雲同步 + 登錄狀態 + 設備 ID: 未初始化 + 自動同步 + 最後同步時間: + 從不 + 同步狀態: + 空閒 + 同步中... + 同步成功 + 同步失敗 + 立即同步 + 同步已開始 + 同步成功完成 + 同步失敗: %1$s + + + 退出登錄 + 確定要退出登錄嗎?退出後本地筆記將保留,但無法同步到雲端。 + 退出 + 已退出登錄 + + + 文件夾 + 創建文件夾失敗 + + + 筆記衝突 + 本地版本 + 雲端版本 + 使用本地 + 使用雲端 + 合併 + 合併功能即將推出 diff --git a/src/Notesmaster/app/src/main/res/values/arrays.xml b/src/Notesmaster/app/src/main/res/values/arrays.xml index ce7ac3a..0f7f2ca 100644 --- a/src/Notesmaster/app/src/main/res/values/arrays.xml +++ b/src/Notesmaster/app/src/main/res/values/arrays.xml @@ -40,4 +40,32 @@ light dark + + + @string/menu_font_small + @string/menu_font_normal + @string/menu_font_large + @string/menu_font_super + + + + 0 + 1 + 2 + 3 + + + + 简体中文 + 繁體中文 + English + System Default + + + + zh-CN + zh-TW + en + system + \ No newline at end of file diff --git a/src/Notesmaster/app/src/main/res/values/strings.xml b/src/Notesmaster/app/src/main/res/values/strings.xml index 053299e..5d1b869 100644 --- a/src/Notesmaster/app/src/main/res/values/strings.xml +++ b/src/Notesmaster/app/src/main/res/values/strings.xml @@ -108,26 +108,16 @@ Sync is canceled Logging into %1$s... Getting remote note list... - Synchronize local notes with Google Task... Settings - Sync account - Sync notes with google task - Last sync time %1$s yyyy-MM-dd hh:mm:ss - Add account - Change sync account - Remove sync account - Cancel Sync immediately Cancel syncing - Current account %1$s - All sync related information will be deleted, which may result in duplicated items sometime - Sync notes - Please select a google account. Local notes will be synced with google task. - Cannot change the account because sync is in progress - %1$s has been set as the sync account New note background color random + Language + All + All Templates + Uncategorized Delete Call notes diff --git a/src/Notesmaster/app/src/main/res/xml/preferences.xml b/src/Notesmaster/app/src/main/res/xml/preferences.xml index b1a3b16..50c575d 100644 --- a/src/Notesmaster/app/src/main/res/xml/preferences.xml +++ b/src/Notesmaster/app/src/main/res/xml/preferences.xml @@ -6,6 +6,25 @@ android:entries="@array/theme_entries" android:entryValues="@array/theme_values" android:defaultValue="system" /> + + + + + + @@ -23,14 +42,21 @@ android:defaultValue="false" /> - - + + + + + android:summary="1.1.0" /> -- 2.34.1 From f01729f8f8d1f3aa70be529b376d9fd7ef6779f6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=8C=85=E5=B0=94=E4=BF=8A?= Date: Sun, 1 Feb 2026 20:11:14 +0800 Subject: [PATCH 2/2] =?UTF-8?q?feat:=20=E6=B7=BB=E5=8A=A0=E7=AC=94?= =?UTF-8?q?=E8=AE=B0=E5=AF=BC=E5=87=BA=E5=8A=9F=E8=83=BD=E6=94=AF=E6=8C=81?= =?UTF-8?q?=E6=96=87=E6=9C=AC=E3=80=81=E5=9B=BE=E7=89=87=E5=92=8CPDF?= =?UTF-8?q?=E6=A0=BC=E5=BC=8F?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 新增菜单项和按钮用于触发导出功能 - 实现单条笔记导出(文本、图片、PDF)及批量导出 - 添加 FileProvider 配置以支持文件分享 - 新增 PdfExportHelper 和 ImageExportHelper 工具类 - 修改 BackupUtils 支持单条笔记和批量导出 --- .../app/src/main/AndroidManifest.xml | 11 + .../java/net/micode/notes/MainActivity.java | 20 +- .../micode/notes/data/NotesRepository.java | 34 +++ .../net/micode/notes/tool/BackupUtils.java | 165 ++++++++++++++- .../micode/notes/tool/ImageExportHelper.java | 117 ++++++++++ .../micode/notes/tool/PdfExportHelper.java | 98 +++++++++ .../net/micode/notes/ui/NoteEditActivity.java | 154 ++++++++++++++ .../micode/notes/ui/NotesListActivity.java | 199 +++++++++++++++++- .../notes/viewmodel/NotesListViewModel.java | 20 ++ .../app/src/main/res/layout/activity_home.xml | 1 + .../app/src/main/res/menu/note_edit.xml | 4 + .../app/src/main/res/xml/file_paths.xml | 5 + 12 files changed, 815 insertions(+), 13 deletions(-) create mode 100644 src/Notesmaster/app/src/main/java/net/micode/notes/tool/ImageExportHelper.java create mode 100644 src/Notesmaster/app/src/main/java/net/micode/notes/tool/PdfExportHelper.java create mode 100644 src/Notesmaster/app/src/main/res/xml/file_paths.xml diff --git a/src/Notesmaster/app/src/main/AndroidManifest.xml b/src/Notesmaster/app/src/main/AndroidManifest.xml index d858d81..f20cdc7 100644 --- a/src/Notesmaster/app/src/main/AndroidManifest.xml +++ b/src/Notesmaster/app/src/main/AndroidManifest.xml @@ -62,6 +62,7 @@ android:roundIcon="@mipmap/ic_launcher_round" android:supportsRtl="true" android:theme="@style/Theme.Notesmaster" + android:requestLegacyExternalStorage="true" tools:targetApi="31"> @@ -155,6 +156,16 @@ android:authorities="micode_notes" android:multiprocess="true" /> + + + + callback) { + executor.execute(() -> { + try { + NoteInfo noteInfo = getFolderInfo(noteId); + callback.onSuccess(noteInfo); + } catch (Exception e) { + callback.onError(e); + } + }); + } + /** * 查询单个文件夹信息 * @@ -1542,6 +1559,23 @@ public class NotesRepository { }); } + /** + * 获取笔记完整内容(异步版本) + * + * @param noteId 笔记ID + * @param callback 回调接口 + */ + public void getNoteContent(long noteId, Callback callback) { + executor.execute(() -> { + try { + String content = getNoteContent(noteId); + callback.onSuccess(content); + } catch (Exception e) { + callback.onError(e); + } + }); + } + private String getNoteContent(long noteId) { String content = ""; Cursor cursor = contentResolver.query( diff --git a/src/Notesmaster/app/src/main/java/net/micode/notes/tool/BackupUtils.java b/src/Notesmaster/app/src/main/java/net/micode/notes/tool/BackupUtils.java index 10c3994..cf84b56 100644 --- a/src/Notesmaster/app/src/main/java/net/micode/notes/tool/BackupUtils.java +++ b/src/Notesmaster/app/src/main/java/net/micode/notes/tool/BackupUtils.java @@ -116,6 +116,27 @@ public class BackupUtils { return mTextExport.exportToText(); } + /** + * 导出单条笔记为文本文件 + * + * @param noteId 笔记 ID + * @param title 笔记标题(用于文件名) + * @return 导出状态码 + */ + public int exportNoteToText(String noteId, String title) { + return mTextExport.exportNoteToText(noteId, title); + } + + /** + * 批量导出指定笔记为文本文件 + * + * @param noteIds 笔记 ID 列表 + * @return 导出状态码 + */ + public int exportNotesToText(java.util.List noteIds) { + return mTextExport.exportNotesToText(noteIds); + } + /** * 获取导出的文本文件名 * @@ -310,6 +331,72 @@ public class BackupUtils { } } + /** + * 导出单条笔记为文本文件 + * + * @param noteId 笔记 ID + * @param title 笔记标题 + * @return 导出状态码 + */ + public int exportNoteToText(String noteId, String title) { + if (!externalStorageAvailable()) { + Log.d(TAG, "Media was not mounted"); + return STATE_SD_CARD_UNMOUONTED; + } + + PrintStream ps = getExportNotePrintStream(title); + if (ps == null) { + Log.e(TAG, "get print stream error"); + return STATE_SYSTEM_ERROR; + } + + exportNoteToText(noteId, ps); + ps.close(); + + return STATE_SUCCESS; + } + + /** + * 批量导出指定笔记为文本文件 + * + * @param noteIds 笔记 ID 列表 + * @return 导出状态码 + */ + public int exportNotesToText(java.util.List noteIds) { + if (!externalStorageAvailable()) { + Log.d(TAG, "Media was not mounted"); + return STATE_SD_CARD_UNMOUONTED; + } + + PrintStream ps = getExportToTextPrintStream(); + if (ps == null) { + Log.e(TAG, "get print stream error"); + return STATE_SYSTEM_ERROR; + } + + for (Long noteId : noteIds) { + Cursor noteCursor = mContext.getContentResolver().query( + net.micode.notes.data.Notes.CONTENT_NOTE_URI, + NOTE_PROJECTION, + net.micode.notes.data.Notes.NoteColumns.ID + "=?", + new String[]{String.valueOf(noteId)}, + null); + + if (noteCursor != null) { + if (noteCursor.moveToFirst()) { + ps.println(String.format(getFormat(FORMAT_NOTE_DATE), DateFormat.format( + mContext.getString(R.string.format_datetime_mdhm), + noteCursor.getLong(NOTE_COLUMN_MODIFIED_DATE)))); + exportNoteToText(String.valueOf(noteId), ps); + } + noteCursor.close(); + } + } + ps.close(); + + return STATE_SUCCESS; + } + /** * 导出笔记数据为文本文件 *

@@ -383,6 +470,31 @@ public class BackupUtils { return STATE_SUCCESS; } + /** + * 获取导出单条笔记文本文件的输出流 + * + * @param title 笔记标题 + * @return PrintStream 对象,如果创建失败则返回 null + */ + private PrintStream getExportNotePrintStream(String title) { + File file = generateFileWithTitle(mContext, R.string.file_path, title); + if (file == null) { + Log.e(TAG, "create file to exported failed"); + return null; + } + mFileName = file.getName(); + mFileDirectory = Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOWNLOADS).getAbsolutePath(); + PrintStream ps = null; + try { + FileOutputStream fos = new FileOutputStream(file); + ps = new PrintStream(fos); + } catch (FileNotFoundException e) { + e.printStackTrace(); + return null; + } + return ps; + } + /** * 获取导出文本文件的输出流 *

@@ -399,7 +511,7 @@ public class BackupUtils { return null; } mFileName = file.getName(); - mFileDirectory = mContext.getString(R.string.file_path); + mFileDirectory = Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOWNLOADS).getAbsolutePath(); PrintStream ps = null; try { FileOutputStream fos = new FileOutputStream(file); @@ -416,31 +528,62 @@ public class BackupUtils { } /** - * 在 SD 卡上生成导出文本文件 + * 在公用下载目录上生成指定标题的文本文件 + * + * @param context 应用上下文 + * @param filePathResId 文件路径资源 ID (不再使用,改为公用下载目录) + * @param title 笔记标题 + * @return 生成的文件对象,如果创建失败则返回 null + */ + private static File generateFileWithTitle(Context context, int filePathResId, String title) { + // 清理文件名中的非法字符 + String fileName = title.replaceAll("[\\\\/:*?\"<>|]", "_"); + if (TextUtils.isEmpty(fileName)) { + fileName = "untitled"; + } + fileName = fileName + "_" + System.currentTimeMillis() + ".txt"; + + File filedir = Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOWNLOADS); + File file = new File(filedir, fileName); + + try { + if (!filedir.exists()) { + filedir.mkdirs(); + } + if (!file.exists()) { + file.createNewFile(); + } + return file; + } catch (SecurityException | IOException e) { + e.printStackTrace(); + } + + return null; + } + + /** + * 在公用下载目录上生成导出文本文件 *

* 在指定的路径下创建导出文件,如果目录不存在则创建目录。 *

* * @param context 应用上下文 - * @param filePathResId 文件路径资源 ID + * @param filePathResId 文件路径资源 ID (不再使用,改为公用下载目录) * @param fileNameFormatResId 文件名格式资源 ID * @return 生成的文件对象,如果创建失败则返回 null */ private static File generateFileMountedOnSDcard(Context context, int filePathResId, int fileNameFormatResId) { - StringBuilder sb = new StringBuilder(); - sb.append(Environment.getExternalStorageDirectory()); - sb.append(context.getString(filePathResId)); - File filedir = new File(sb.toString()); - sb.append(context.getString( + File filedir = Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOWNLOADS); + String fileName = context.getString( fileNameFormatResId, DateFormat.format(context.getString(R.string.format_date_ymd), - System.currentTimeMillis()))); - File file = new File(sb.toString()); + System.currentTimeMillis())); + File file = new File(filedir, fileName); try { if (!filedir.exists()) { // 创建目录 - filedir.mkdir(); + filedir.mkdirs(); } if (!file.exists()) { // 创建文件 diff --git a/src/Notesmaster/app/src/main/java/net/micode/notes/tool/ImageExportHelper.java b/src/Notesmaster/app/src/main/java/net/micode/notes/tool/ImageExportHelper.java new file mode 100644 index 0000000..be043df --- /dev/null +++ b/src/Notesmaster/app/src/main/java/net/micode/notes/tool/ImageExportHelper.java @@ -0,0 +1,117 @@ +package net.micode.notes.tool; + +import android.content.Context; +import android.graphics.Bitmap; +import android.graphics.Canvas; +import android.graphics.Color; +import android.graphics.Paint; +import android.net.Uri; +import android.view.View; +import android.widget.ScrollView; + +import androidx.core.widget.NestedScrollView; +import androidx.core.content.FileProvider; + +import android.os.Environment; + +import java.io.File; +import java.io.FileOutputStream; +import java.io.IOException; + +/** + * 图片导出工具类 + *

+ * 提供将 View 转换为图片并分享的功能。 + *

+ */ +public class ImageExportHelper { + private static final String TAG = "ImageExportHelper"; + + /** + * 将 View 转换为 Bitmap + * + * @param view 要转换的 View + * @return 转换后的 Bitmap + */ + public static Bitmap viewToBitmap(View view) { + if (view instanceof ScrollView || view instanceof NestedScrollView) { + int height = 0; + if (view instanceof ScrollView) { + ScrollView scrollView = (ScrollView) view; + for (int i = 0; i < scrollView.getChildCount(); i++) { + height += scrollView.getChildAt(i).getHeight(); + } + } else { + NestedScrollView nestedScrollView = (NestedScrollView) view; + for (int i = 0; i < nestedScrollView.getChildCount(); i++) { + height += nestedScrollView.getChildAt(i).getHeight(); + } + } + Bitmap bitmap = Bitmap.createBitmap(view.getWidth(), height, Bitmap.Config.ARGB_8888); + Canvas canvas = new Canvas(bitmap); + canvas.drawColor(Color.WHITE); + view.draw(canvas); + return bitmap; + } else { + Bitmap bitmap = Bitmap.createBitmap(view.getWidth(), view.getHeight(), Bitmap.Config.ARGB_8888); + Canvas canvas = new Canvas(bitmap); + canvas.drawColor(Color.WHITE); + view.draw(canvas); + return bitmap; + } + } + + /** + * 保存 Bitmap 到公用下载目录 + * + * @param context 应用上下文 + * @param bitmap 要保存的 Bitmap + * @param title 笔记标题 + * @return 生成的文件 Uri,如果保存失败则返回 null + */ + public static Uri saveBitmapToExternal(Context context, Bitmap bitmap, String title) { + String fileName = title.replaceAll("[\\\\/:*?\"<>|]", "_"); + if (fileName.isEmpty()) { + fileName = "untitled"; + } + fileName += "_" + System.currentTimeMillis() + ".png"; + + File filedir = Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOWNLOADS); + File file = new File(filedir, fileName); + + try { + if (!filedir.exists()) { + filedir.mkdirs(); + } + FileOutputStream stream = new FileOutputStream(file); + bitmap.compress(Bitmap.CompressFormat.PNG, 100, stream); + stream.close(); + return FileProvider.getUriForFile(context, context.getPackageName() + ".fileprovider", file); + } catch (IOException e) { + e.printStackTrace(); + } + return null; + } + + /** + * 保存 Bitmap 到缓存目录并返回 Uri + * + * @param context 应用上下文 + * @param bitmap 要保存的 Bitmap + * @return 文件的 Uri,如果保存失败则返回 null + */ + public static Uri saveBitmapToCache(Context context, Bitmap bitmap) { + File cachePath = new File(context.getCacheDir(), "images"); + cachePath.mkdirs(); + try { + File file = new File(cachePath, "note_export_" + System.currentTimeMillis() + ".png"); + FileOutputStream stream = new FileOutputStream(file); + bitmap.compress(Bitmap.CompressFormat.PNG, 100, stream); + stream.close(); + return FileProvider.getUriForFile(context, context.getPackageName() + ".fileprovider", file); + } catch (IOException e) { + e.printStackTrace(); + } + return null; + } +} diff --git a/src/Notesmaster/app/src/main/java/net/micode/notes/tool/PdfExportHelper.java b/src/Notesmaster/app/src/main/java/net/micode/notes/tool/PdfExportHelper.java new file mode 100644 index 0000000..d27806c --- /dev/null +++ b/src/Notesmaster/app/src/main/java/net/micode/notes/tool/PdfExportHelper.java @@ -0,0 +1,98 @@ +package net.micode.notes.tool; + +import android.content.Context; +import android.graphics.Canvas; +import android.graphics.Color; +import android.graphics.pdf.PdfDocument; +import android.os.Environment; +import android.text.Layout; +import android.text.StaticLayout; +import android.text.TextPaint; + +import java.io.File; +import java.io.FileOutputStream; +import java.io.IOException; + +/** + * PDF 导出工具类 + *

+ * 使用 Android PdfDocument API 将笔记导出为 PDF 文件。 + *

+ */ +public class PdfExportHelper { + private static final String TAG = "PdfExportHelper"; + + /** + * 将笔记导出为 PDF + * + * @param context 应用上下文 + * @param title 笔记标题 + * @param content 笔记内容 + * @return 生成的 PDF 文件,如果失败则返回 null + */ + public static File exportToPdf(Context context, String title, String content) { + PdfDocument document = new PdfDocument(); + // A4 纸张大小 (595 x 842 points) + PdfDocument.PageInfo pageInfo = new PdfDocument.PageInfo.Builder(595, 842, 1).create(); + PdfDocument.Page page = document.startPage(pageInfo); + + Canvas canvas = page.getCanvas(); + TextPaint titlePaint = new TextPaint(); + titlePaint.setColor(Color.BLACK); + titlePaint.setTextSize(24); + titlePaint.setFakeBoldText(true); + + TextPaint contentPaint = new TextPaint(); + contentPaint.setColor(Color.BLACK); + contentPaint.setTextSize(14); + + int x = 50; + int y = 50; + + // 绘制标题 + canvas.drawText(title, x, y, titlePaint); + y += 40; + + // 绘制正文(自动换行) + StaticLayout staticLayout; + if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.M) { + staticLayout = StaticLayout.Builder.obtain(content, 0, content.length(), contentPaint, pageInfo.getPageWidth() - 100) + .setAlignment(Layout.Alignment.ALIGN_NORMAL) + .setLineSpacing(0.0f, 1.2f) + .setIncludePad(false) + .build(); + } else { + staticLayout = new StaticLayout(content, contentPaint, pageInfo.getPageWidth() - 100, + Layout.Alignment.ALIGN_NORMAL, 1.2f, 0.0f, false); + } + + canvas.save(); + canvas.translate(x, y); + staticLayout.draw(canvas); + canvas.restore(); + + document.finishPage(page); + + // 生成文件名 + String fileName = title.replaceAll("[\\\\/:*?\"<>|]", "_"); + if (fileName.isEmpty()) { + fileName = "untitled"; + } + fileName += "_" + System.currentTimeMillis() + ".pdf"; + + File file = new File(Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOWNLOADS), fileName); + + try { + FileOutputStream fos = new FileOutputStream(file); + document.writeTo(fos); + fos.close(); + } catch (IOException e) { + e.printStackTrace(); + file = null; + } finally { + document.close(); + } + + return file; + } +} diff --git a/src/Notesmaster/app/src/main/java/net/micode/notes/ui/NoteEditActivity.java b/src/Notesmaster/app/src/main/java/net/micode/notes/ui/NoteEditActivity.java index 8cc84ae..4ef73ba 100644 --- a/src/Notesmaster/app/src/main/java/net/micode/notes/ui/NoteEditActivity.java +++ b/src/Notesmaster/app/src/main/java/net/micode/notes/ui/NoteEditActivity.java @@ -53,6 +53,8 @@ import android.widget.ImageView; import android.widget.LinearLayout; import android.widget.TextView; import android.widget.Toast; +import android.graphics.Bitmap; +import android.net.Uri; import net.micode.notes.R; import net.micode.notes.data.Notes; @@ -61,7 +63,10 @@ import net.micode.notes.model.NoteCommand; import net.micode.notes.model.UndoRedoManager; import net.micode.notes.model.WorkingNote; import net.micode.notes.model.WorkingNote.NoteSettingChangedListener; +import net.micode.notes.tool.BackupUtils; import net.micode.notes.tool.DataUtils; +import net.micode.notes.tool.ImageExportHelper; +import net.micode.notes.tool.PdfExportHelper; import net.micode.notes.tool.ResourceParser; import net.micode.notes.tool.ResourceParser.TextAppearanceResources; import net.micode.notes.ui.DateTimePickerDialog.OnDateTimeSetListener; @@ -88,6 +93,12 @@ import net.micode.notes.tool.SmartParser; import net.micode.notes.data.FontManager; +import android.widget.RadioButton; +import android.widget.RadioGroup; + +import java.io.File; +import java.util.ArrayList; + public class NoteEditActivity extends BaseActivity implements OnClickListener, NoteSettingChangedListener, OnTextViewChangeListener { /** @@ -1035,6 +1046,9 @@ public class NoteEditActivity extends BaseActivity implements OnClickListener, getWorkingText(); sendTo(this, mWorkingNote.getContent()); break; + case R.id.menu_export: + showExportDialog(); + break; case R.id.menu_send_to_desktop: sendToDesktop(); break; @@ -1082,6 +1096,135 @@ public class NoteEditActivity extends BaseActivity implements OnClickListener, context.startActivity(intent); } + /** + * 显示导出选项对话框 + */ + private void showExportDialog() { + if (mWorkingNote.getNoteId() == 0) { + saveNote(); + } + getWorkingText(); + String content = mWorkingNote.getContent(); + if (TextUtils.isEmpty(content)) { + showToast(R.string.error_note_empty_for_send_to_desktop); + return; + } + + AlertDialog.Builder builder = new AlertDialog.Builder(this); + builder.setTitle("导出便签"); + + LinearLayout layout = new LinearLayout(this); + layout.setOrientation(LinearLayout.VERTICAL); + layout.setPadding(50, 20, 50, 20); + + final RadioGroup group = new RadioGroup(this); + RadioButton rbText = new RadioButton(this); + rbText.setText("导出为文本 (.txt)"); + rbText.setId(View.generateViewId()); + group.addView(rbText); + + RadioButton rbImage = new RadioButton(this); + rbImage.setText("导出为图片 (.png)"); + rbImage.setId(View.generateViewId()); + group.addView(rbImage); + + RadioButton rbPdf = new RadioButton(this); + rbPdf.setText("导出为 PDF (.pdf)"); + rbPdf.setId(View.generateViewId()); + group.addView(rbPdf); + + group.check(rbText.getId()); + layout.addView(group); + + final CheckBox cbShare = new CheckBox(this); + cbShare.setText("导出后立即分享"); + layout.addView(cbShare); + + builder.setView(layout); + + builder.setPositiveButton("开始导出", (dialog, which) -> { + int checkedId = group.getCheckedRadioButtonId(); + boolean share = cbShare.isChecked(); + + if (checkedId == rbText.getId()) { + performExport(0, share); + } else if (checkedId == rbImage.getId()) { + performExport(1, share); + } else if (checkedId == rbPdf.getId()) { + performExport(2, share); + } + }); + + builder.setNegativeButton("取消", null); + builder.show(); + } + + private void performExport(int format, boolean share) { + String content = mWorkingNote.getContent(); + String title = getTitleFromContent(content); + String noteId = String.valueOf(mWorkingNote.getNoteId()); + + switch (format) { + case 0: // Text + BackupUtils backupUtils = BackupUtils.getInstance(this); + int state = backupUtils.exportNoteToText(noteId, title); + if (state == BackupUtils.STATE_SUCCESS) { + File file = new File(backupUtils.getExportedTextFileDir(), backupUtils.getExportedTextFileName()); + showToast("已导出至下载目录: " + file.getName()); + if (share) shareFile(file, "text/plain"); + } else { + showToast("导出文本失败"); + } + break; + case 1: // Image + binding.svNoteEditScroll.post(() -> { + Bitmap bitmap = ImageExportHelper.viewToBitmap(binding.cvEditorSurface); + Uri uri = ImageExportHelper.saveBitmapToExternal(NoteEditActivity.this, bitmap, title); + if (uri != null) { + showToast("已导出图片至下载目录"); + if (share) shareUri(uri, "image/png"); + } else { + showToast("生成图片失败"); + } + }); + break; + case 2: // PDF + File pdfFile = PdfExportHelper.exportToPdf(this, title, content); + if (pdfFile != null) { + showToast("已导出 PDF 至下载目录: " + pdfFile.getName()); + if (share) shareFile(pdfFile, "application/pdf"); + } else { + showToast("导出 PDF 失败"); + } + break; + } + } + + private String getTitleFromContent(String content) { + String title = content.trim(); + int firstNewLine = title.indexOf('\n'); + if (firstNewLine > 0) { + title = title.substring(0, firstNewLine); + } + if (title.length() > 30) { + title = title.substring(0, 30); + } + return title; + } + + private void shareFile(File file, String mimeType) { + Uri uri = androidx.core.content.FileProvider.getUriForFile(this, getPackageName() + ".fileprovider", file); + shareUri(uri, mimeType); + } + + private void shareUri(Uri uri, String mimeType) { + Intent intent = new Intent(Intent.ACTION_SEND); + intent.setType(mimeType); + intent.putExtra(Intent.EXTRA_STREAM, uri); + intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION); + startActivity(Intent.createChooser(intent, "分享便签")); + } + /** * 创建新笔记 *

@@ -1747,6 +1890,17 @@ public class NoteEditActivity extends BaseActivity implements OnClickListener, } } + /** + * 显示Toast提示 + *

+ * 显示指定文本的Toast提示消息。 + *

+ * @param text 提示文本 + */ + private void showToast(String text) { + Toast.makeText(this, text, Toast.LENGTH_SHORT).show(); + } + /** * 显示Toast提示 *

diff --git a/src/Notesmaster/app/src/main/java/net/micode/notes/ui/NotesListActivity.java b/src/Notesmaster/app/src/main/java/net/micode/notes/ui/NotesListActivity.java index 2be0b80..863997c 100644 --- a/src/Notesmaster/app/src/main/java/net/micode/notes/ui/NotesListActivity.java +++ b/src/Notesmaster/app/src/main/java/net/micode/notes/ui/NotesListActivity.java @@ -3,14 +3,31 @@ package net.micode.notes.ui; import android.app.AlertDialog; import android.content.BroadcastReceiver; import android.content.Context; +import android.content.DialogInterface; import android.content.Intent; import android.content.IntentFilter; +import android.graphics.Bitmap; +import android.graphics.Canvas; +import android.graphics.Color; +import android.net.Uri; import android.os.Bundle; +import android.text.TextUtils; import android.util.Log; import android.view.View; +import android.widget.CheckBox; import android.widget.EditText; +import android.widget.LinearLayout; +import android.widget.RadioButton; +import android.widget.RadioGroup; import android.widget.Toast; +import java.io.File; +import java.util.ArrayList; +import java.util.List; + +import net.micode.notes.tool.ImageExportHelper; +import net.micode.notes.tool.PdfExportHelper; + import androidx.annotation.NonNull; import androidx.appcompat.app.AppCompatActivity; import androidx.core.view.GravityCompat; @@ -24,6 +41,7 @@ import net.micode.notes.data.Notes; import net.micode.notes.data.NotesRepository; import net.micode.notes.databinding.ActivityHomeBinding; import net.micode.notes.sync.SyncManager; +import net.micode.notes.tool.BackupUtils; import net.micode.notes.tool.SecurityManager; import net.micode.notes.viewmodel.NotesListViewModel; @@ -138,6 +156,11 @@ public class NotesListActivity extends BaseActivity implements SidebarFragment.O viewModel.setIsSelectionMode(false); }); + binding.btnActionExport.setOnClickListener(v -> { + exportSelectedNotes(); + viewModel.setIsSelectionMode(false); + }); + binding.btnActionLock.setOnClickListener(v -> { SecurityManager securityManager = SecurityManager.getInstance(this); if (!securityManager.isPasswordSet()) { @@ -375,7 +398,11 @@ public class NotesListActivity extends BaseActivity implements SidebarFragment.O @Override public void onSyncSelected() { binding.drawerLayout.closeDrawer(GravityCompat.START); SyncManager.getInstance().syncNotes(null); } @Override public void onLoginSelected() { binding.drawerLayout.closeDrawer(GravityCompat.START); startActivity(new Intent(this, LoginActivity.class)); } @Override public void onLogoutSelected() { binding.drawerLayout.closeDrawer(GravityCompat.START); viewModel.refreshNotes(); } - @Override public void onExportSelected() { binding.drawerLayout.closeDrawer(GravityCompat.START); Toast.makeText(this, "导出功能待实现", Toast.LENGTH_SHORT).show(); } + @Override public void onExportSelected() { + binding.drawerLayout.closeDrawer(GravityCompat.START); + viewModel.setIsSelectionMode(true); + Toast.makeText(this, "请选择要导出的便签", Toast.LENGTH_SHORT).show(); + } @Override public void onTemplateSelected() { binding.drawerLayout.closeDrawer(GravityCompat.START); viewModel.enterFolder(Notes.ID_TEMPLATE_FOLDER); } @Override public void onSettingsSelected() { binding.drawerLayout.closeDrawer(GravityCompat.START); startActivity(new Intent(this, SettingsActivity.class)); } @Override public void onCapsuleSelected() { @@ -410,6 +437,176 @@ public class NotesListActivity extends BaseActivity implements SidebarFragment.O }); } + private void exportSelectedNotes() { + java.util.List selectedIds = viewModel.getSelectedNoteIds(); + if (selectedIds.isEmpty()) { + Toast.makeText(this, "请先选择要导出的便签", Toast.LENGTH_SHORT).show(); + return; + } + + AlertDialog.Builder builder = new AlertDialog.Builder(this); + builder.setTitle("批量导出便签"); + + LinearLayout layout = new LinearLayout(this); + layout.setOrientation(LinearLayout.VERTICAL); + layout.setPadding(50, 20, 50, 20); + + final RadioGroup group = new RadioGroup(this); + RadioButton rbText = new RadioButton(this); + rbText.setText("导出为文本 (.txt)"); + rbText.setId(View.generateViewId()); + group.addView(rbText); + + RadioButton rbImage = new RadioButton(this); + rbImage.setText("导出为图片 (.png)"); + rbImage.setId(View.generateViewId()); + group.addView(rbImage); + + RadioButton rbPdf = new RadioButton(this); + rbPdf.setText("导出为 PDF (.pdf)"); + rbPdf.setId(View.generateViewId()); + group.addView(rbPdf); + + group.check(rbText.getId()); + layout.addView(group); + + final CheckBox cbShare = new CheckBox(this); + cbShare.setText("导出后立即分享"); + layout.addView(cbShare); + + builder.setView(layout); + + builder.setPositiveButton("开始导出", (dialog, which) -> { + int checkedId = group.getCheckedRadioButtonId(); + boolean share = cbShare.isChecked(); + + if (checkedId == rbText.getId()) { + performBatchExport(0, selectedIds, share); + } else if (checkedId == rbImage.getId()) { + performBatchExport(1, selectedIds, share); + } else if (checkedId == rbPdf.getId()) { + performBatchExport(2, selectedIds, share); + } + }); + + builder.setNegativeButton("取消", null); + builder.show(); + } + + private void performBatchExport(int format, java.util.List selectedIds, boolean share) { + if (format == 0) { // Text (Combined) + BackupUtils backupUtils = BackupUtils.getInstance(this); + int state = backupUtils.exportNotesToText(selectedIds); + if (state == BackupUtils.STATE_SUCCESS) { + File file = new File(backupUtils.getExportedTextFileDir(), backupUtils.getExportedTextFileName()); + Toast.makeText(this, "已导出至下载目录: " + file.getName(), Toast.LENGTH_SHORT).show(); + if (share) shareFile(file, "text/plain"); + } else { + Toast.makeText(this, "导出失败", Toast.LENGTH_SHORT).show(); + } + } else { + final ArrayList uris = new ArrayList<>(); + final int total = selectedIds.size(); + final int[] successCount = {0}; + final String mimeType = (format == 1) ? "image/png" : "application/pdf"; + + for (Long id : selectedIds) { + viewModel.getNoteContent(id, new NotesRepository.Callback() { + @Override + public void onSuccess(String content) { + if (!TextUtils.isEmpty(content)) { + String title = getTitleFromContent(content); + File exportedFile = null; + if (format == 1) { // Image + Bitmap bitmap = renderTextToBitmap(content); + Uri uri = ImageExportHelper.saveBitmapToExternal(NotesListActivity.this, bitmap, title); + if (uri != null) { + successCount[0]++; + uris.add(uri); + } + } else { // PDF + exportedFile = PdfExportHelper.exportToPdf(NotesListActivity.this, title, content); + if (exportedFile != null) { + successCount[0]++; + uris.add(androidx.core.content.FileProvider.getUriForFile(NotesListActivity.this, getPackageName() + ".fileprovider", exportedFile)); + } + } + } + checkBatchFinished(successCount[0], total, uris, mimeType, share); + } + + @Override + public void onError(Exception error) { + checkBatchFinished(successCount[0], total, uris, mimeType, share); + } + }); + } + } + } + + private Bitmap renderTextToBitmap(String text) { + android.widget.TextView textView = new android.widget.TextView(this); + textView.setText(text); + textView.setTextColor(Color.BLACK); + textView.setBackgroundColor(Color.WHITE); + textView.setTextSize(16); + textView.setPadding(40, 40, 40, 40); + textView.setWidth(800); + + int widthMeasureSpec = View.MeasureSpec.makeMeasureSpec(800, View.MeasureSpec.EXACTLY); + int heightMeasureSpec = View.MeasureSpec.makeMeasureSpec(0, View.MeasureSpec.UNSPECIFIED); + textView.measure(widthMeasureSpec, heightMeasureSpec); + textView.layout(0, 0, textView.getMeasuredWidth(), textView.getMeasuredHeight()); + + Bitmap bitmap = Bitmap.createBitmap(textView.getMeasuredWidth(), textView.getMeasuredHeight(), Bitmap.Config.ARGB_8888); + Canvas canvas = new Canvas(bitmap); + textView.draw(canvas); + return bitmap; + } + + private synchronized void checkBatchFinished(int success, int total, ArrayList uris, String mimeType, boolean share) { + // Since we are doing this sequentially or with callbacks, we need to track progress + // For simplicity in this implementation, I'll just check if we have reached total count + if (uris.size() + (total - success) >= total) { + if (success > 0) { + Toast.makeText(this, "成功导出 " + success + " 个文件至下载目录", Toast.LENGTH_SHORT).show(); + if (share) shareUris(uris, mimeType); + } else { + Toast.makeText(this, "导出失败", Toast.LENGTH_SHORT).show(); + } + } + } + + private String getTitleFromContent(String content) { + if (TextUtils.isEmpty(content)) return "untitled"; + String title = content.trim(); + int firstNewLine = title.indexOf('\n'); + if (firstNewLine > 0) { + title = title.substring(0, firstNewLine); + } + if (title.length() > 30) { + title = title.substring(0, 30); + } + return title; + } + + private void shareFile(File file, String mimeType) { + Uri uri = androidx.core.content.FileProvider.getUriForFile(this, getPackageName() + ".fileprovider", file); + Intent intent = new Intent(Intent.ACTION_SEND); + intent.setType(mimeType); + intent.putExtra(Intent.EXTRA_STREAM, uri); + intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION); + startActivity(Intent.createChooser(intent, "分享便签")); + } + + private void shareUris(ArrayList uris, String mimeType) { + Intent intent = new Intent(Intent.ACTION_SEND_MULTIPLE); + intent.setType(mimeType); + intent.putParcelableArrayListExtra(Intent.EXTRA_STREAM, uris); + intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION); + startActivity(Intent.createChooser(intent, "分享便签")); + } + private void showRenameFolderDialog(long folderId, String currentName) { final EditText input = new EditText(this); input.setText(currentName); diff --git a/src/Notesmaster/app/src/main/java/net/micode/notes/viewmodel/NotesListViewModel.java b/src/Notesmaster/app/src/main/java/net/micode/notes/viewmodel/NotesListViewModel.java index 6265f9b..0307c24 100644 --- a/src/Notesmaster/app/src/main/java/net/micode/notes/viewmodel/NotesListViewModel.java +++ b/src/Notesmaster/app/src/main/java/net/micode/notes/viewmodel/NotesListViewModel.java @@ -942,6 +942,26 @@ public class NotesListViewModel extends AndroidViewModel { }); } + /** + * 获取单个笔记的详细信息 + * + * @param noteId 笔记ID + * @param callback 回调接口 + */ + public void getNoteInfo(long noteId, NotesRepository.Callback callback) { + repository.getNoteInfo(noteId, callback); + } + + /** + * 获取单个笔记的完整内容 + * + * @param noteId 笔记ID + * @param callback 回调接口 + */ + public void getNoteContent(long noteId, NotesRepository.Callback callback) { + repository.getNoteContent(noteId, callback); + } + /** * 获取文件夹信息 *

diff --git a/src/Notesmaster/app/src/main/res/layout/activity_home.xml b/src/Notesmaster/app/src/main/res/layout/activity_home.xml index 3814b7e..675d757 100644 --- a/src/Notesmaster/app/src/main/res/layout/activity_home.xml +++ b/src/Notesmaster/app/src/main/res/layout/activity_home.xml @@ -196,6 +196,7 @@ + diff --git a/src/Notesmaster/app/src/main/res/menu/note_edit.xml b/src/Notesmaster/app/src/main/res/menu/note_edit.xml index 64e0fcc..ea281a5 100644 --- a/src/Notesmaster/app/src/main/res/menu/note_edit.xml +++ b/src/Notesmaster/app/src/main/res/menu/note_edit.xml @@ -71,6 +71,10 @@ android:id="@+id/menu_share" android:title="@string/menu_share"/> + + diff --git a/src/Notesmaster/app/src/main/res/xml/file_paths.xml b/src/Notesmaster/app/src/main/res/xml/file_paths.xml new file mode 100644 index 0000000..d68a56c --- /dev/null +++ b/src/Notesmaster/app/src/main/res/xml/file_paths.xml @@ -0,0 +1,5 @@ + + + + + -- 2.34.1