feat: 添加笔记导出功能支持文本、图片和PDF格式

- 新增菜单项和按钮用于触发导出功能
- 实现单条笔记导出(文本、图片、PDF)及批量导出
- 添加 FileProvider 配置以支持文件分享
- 新增 PdfExportHelper 和 ImageExportHelper 工具类
- 修改 BackupUtils 支持单条笔记和批量导出
baoerjun_branch
包尔俊 3 weeks ago
parent 86d39b7a95
commit f01729f8f8

@ -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" />
<provider
android:name="androidx.core.content.FileProvider"
android:authorities="net.micode.notes.fileprovider"
android:exported="false"
android:grantUriPermissions="true">
<meta-data
android:name="android.support.FILE_PROVIDER_PATHS"
android:resource="@xml/file_paths" />
</provider>
<!-- ==================== 桌面小部件接收器 ==================== -->
<!-- 2x2大小的笔记桌面小部件 -->
<receiver

@ -5,6 +5,7 @@ import android.os.Bundle;
import android.util.Log;
import android.view.Gravity;
import android.view.View;
import android.widget.Toast;
import androidx.activity.EdgeToEdge;
import androidx.appcompat.app.AppCompatActivity;
@ -15,6 +16,7 @@ import androidx.drawerlayout.widget.DrawerLayout;
import net.micode.notes.data.Notes;
import net.micode.notes.databinding.ActivityMainBinding;
import net.micode.notes.tool.BackupUtils;
import net.micode.notes.ui.BaseActivity;
import net.micode.notes.ui.SidebarFragment;
@ -130,7 +132,23 @@ public class MainActivity extends BaseActivity implements SidebarFragment.OnSide
@Override
public void onExportSelected() {
Log.d(TAG, "Export selected");
// TODO: 实现导出功能
BackupUtils backupUtils = BackupUtils.getInstance(this);
int state = backupUtils.exportToText();
switch (state) {
case BackupUtils.STATE_SUCCESS:
Toast.makeText(this, getString(R.string.format_exported_file_location,
backupUtils.getExportedTextFileName(),
backupUtils.getExportedTextFileDir()), Toast.LENGTH_SHORT).show();
break;
case BackupUtils.STATE_SD_CARD_UNMOUONTED:
Toast.makeText(this, R.string.error_sdcard_unmounted, Toast.LENGTH_SHORT).show();
break;
case BackupUtils.STATE_SYSTEM_ERROR:
default:
Toast.makeText(this, R.string.error_sdcard_export, Toast.LENGTH_SHORT).show();
break;
}
closeSidebar();
}
@Override

@ -429,6 +429,23 @@ public class NotesRepository {
return notes;
}
/**
*
*
* @param noteId ID
* @param callback
*/
public void getNoteInfo(long noteId, Callback<NoteInfo> 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<String> 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(

@ -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<Long> 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<Long> 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;
}
/**
*
* <p>
@ -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;
}
/**
*
* <p>
@ -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;
}
/**
*
* <p>
*
* </p>
*
* @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()) {
// 创建文件

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

@ -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
* <p>
* 使 Android PdfDocument API PDF
* </p>
*/
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;
}
}

@ -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, "分享便签"));
}
/**
*
* <p>
@ -1747,6 +1890,17 @@ public class NoteEditActivity extends BaseActivity implements OnClickListener,
}
}
/**
* Toast
* <p>
* Toast
* </p>
* @param text
*/
private void showToast(String text) {
Toast.makeText(this, text, Toast.LENGTH_SHORT).show();
}
/**
* Toast
* <p>

@ -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<Long> 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<Long> 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<Uri> 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<String>() {
@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<Uri> 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<Uri> 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);

@ -942,6 +942,26 @@ public class NotesListViewModel extends AndroidViewModel {
});
}
/**
*
*
* @param noteId ID
* @param callback
*/
public void getNoteInfo(long noteId, NotesRepository.Callback<NotesRepository.NoteInfo> callback) {
repository.getNoteInfo(noteId, callback);
}
/**
*
*
* @param noteId ID
* @param callback
*/
public void getNoteContent(long noteId, NotesRepository.Callback<String> callback) {
repository.getNoteContent(noteId, callback);
}
/**
*
* <p>

@ -196,6 +196,7 @@
<TextView android:id="@+id/btn_action_hide" android:layout_width="0dp" android:layout_height="wrap_content" android:layout_weight="1" android:text="隐藏" android:drawableTop="@android:drawable/ic_menu_close_clear_cancel" android:gravity="center" android:visibility="gone"/>
<TextView android:id="@+id/btn_action_pin" android:layout_width="0dp" android:layout_height="wrap_content" android:layout_weight="1" android:text="置顶" android:drawableTop="@android:drawable/ic_menu_upload" android:gravity="center"/>
<TextView android:id="@+id/btn_action_export" android:layout_width="0dp" android:layout_height="wrap_content" android:layout_weight="1" android:text="导出" android:drawableTop="@android:drawable/ic_menu_save" android:gravity="center"/>
<TextView android:id="@+id/btn_action_move" android:layout_width="0dp" android:layout_height="wrap_content" android:layout_weight="1" android:text="移动到" android:drawableTop="@drawable/ic_folder" android:gravity="center"/>
<TextView android:id="@+id/btn_action_lock" android:layout_width="0dp" android:layout_height="wrap_content" android:layout_weight="1" android:text="加锁" android:drawableTop="@android:drawable/ic_lock_lock" android:gravity="center"/>
<TextView android:id="@+id/btn_action_delete" android:layout_width="0dp" android:layout_height="wrap_content" android:layout_weight="1" android:text="删除" android:drawableTop="@android:drawable/ic_menu_delete" android:gravity="center"/>

@ -71,6 +71,10 @@
android:id="@+id/menu_share"
android:title="@string/menu_share"/>
<item
android:id="@+id/menu_export"
android:title="导出" />
<item
android:id="@+id/menu_send_to_desktop"
android:title="@string/menu_send_to_desktop"/>

@ -0,0 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<paths>
<external-path name="external_files" path="." />
<cache-path name="cache_files" path="." />
</paths>
Loading…
Cancel
Save