From 86d39b7a950fa28c4ebe897297bdda5e6985d3d1 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?=E5=8C=85=E5=B0=94=E4=BF=8A?=
@@ -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
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 @@