@ -3,13 +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 ;
@ -23,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 ;
@ -33,7 +52,7 @@ import net.micode.notes.viewmodel.NotesListViewModel;
* 使 用 ViewBinding 访 问 视 图 。
* < / p >
* /
public class NotesListActivity extends AppCompat Activity implements SidebarFragment . OnSidebarItemSelectedListener {
public class NotesListActivity extends Base Activity implements SidebarFragment . OnSidebarItemSelectedListener {
private static final String TAG = "NotesListActivity" ;
private ActivityHomeBinding binding ;
@ -137,6 +156,11 @@ public class NotesListActivity extends AppCompatActivity implements SidebarFragm
viewModel . setIsSelectionMode ( false ) ;
} ) ;
binding . btnActionExport . setOnClickListener ( v - > {
exportSelectedNotes ( ) ;
viewModel . setIsSelectionMode ( false ) ;
} ) ;
binding . btnActionLock . setOnClickListener ( v - > {
SecurityManager securityManager = SecurityManager . getInstance ( this ) ;
if ( ! securityManager . isPasswordSet ( ) ) {
@ -200,7 +224,7 @@ public class NotesListActivity extends AppCompatActivity implements SidebarFragm
new ViewModelProvider . Factory ( ) {
@Override
public < T extends androidx . lifecycle . ViewModel > T create ( Class < T > modelClass ) {
return ( T ) new NotesListViewModel ( repository) ;
return ( T ) new NotesListViewModel ( getApplication( ) , repository) ;
}
} ) . get ( NotesListViewModel . class ) ;
@ -250,6 +274,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 ) {
@ -365,17 +398,257 @@ public class NotesListActivity extends AppCompatActivity implements SidebarFragm
@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 ( ) {
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 < NotesRepository . NoteInfo > ( ) {
@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 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 ) ;
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 < NotesRepository . NoteInfo > ( ) {
@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 ) ; }