diff --git a/Android/app/src/main/java/com/majinnaibu/monstercards/ui/collections/CollectionDetailFragment.java b/Android/app/src/main/java/com/majinnaibu/monstercards/ui/collections/CollectionDetailFragment.java index e3072df..c8cc731 100644 --- a/Android/app/src/main/java/com/majinnaibu/monstercards/ui/collections/CollectionDetailFragment.java +++ b/Android/app/src/main/java/com/majinnaibu/monstercards/ui/collections/CollectionDetailFragment.java @@ -65,7 +65,7 @@ public class CollectionDetailFragment extends MCFragment { FloatingActionButton fab = root.findViewById(R.id.fab_add_monster_to_collection); if (fab != null) { - fab.setOnClickListener(v -> showAddMonsterDialog()); + fab.setOnClickListener(v -> showCollectionDetailFabOptionsDialog()); } mViewModel.getCollection().observe(getViewLifecycleOwner(), collection -> { @@ -134,6 +134,30 @@ public class CollectionDetailFragment extends MCFragment { recyclerView.setAdapter(mAdapter); } + private void showCollectionDetailFabOptionsDialog() { + String[] options = new String[]{ + getString(R.string.action_add_monster), + getString(R.string.action_create_monster), + getString(R.string.action_import_monster_from_url), + getString(R.string.action_import_monster_from_file) + }; + new AlertDialog.Builder(requireContext()) + .setTitle(R.string.title_collection_actions) + .setItems(options, (dialog, which) -> { + if (which == 0) { + showAddMonsterDialog(); + } else if (which == 1) { + createNewMonster(); + } else if (which == 2) { + showImportUrlDialog(); + } else if (which == 3) { + importMonsterFromFile(); + } + }) + .setNegativeButton(R.string.dialog_cancel, null) + .show(); + } + private void showAddMonsterDialog() { MonsterRepository repository = getMonsterRepository(); mDisposables.add(repository.getMonsters() diff --git a/Android/app/src/main/java/com/majinnaibu/monstercards/ui/collections/CollectionsFragment.java b/Android/app/src/main/java/com/majinnaibu/monstercards/ui/collections/CollectionsFragment.java index 7560d50..4cfae01 100644 --- a/Android/app/src/main/java/com/majinnaibu/monstercards/ui/collections/CollectionsFragment.java +++ b/Android/app/src/main/java/com/majinnaibu/monstercards/ui/collections/CollectionsFragment.java @@ -47,13 +47,13 @@ public class CollectionsFragment extends MCFragment { FloatingActionButton fab = root.findViewById(R.id.fab_add_collection); if (fab != null) { - fab.setOnClickListener(v -> showCreateCollectionDialog()); + fab.setOnClickListener(v -> showCollectionFabOptionsDialog()); } View emptyState = root.findViewById(R.id.empty_state); View emptyStateButton = root.findViewById(R.id.empty_state_button); if (emptyStateButton != null) { - emptyStateButton.setOnClickListener(v -> showCreateCollectionDialog()); + emptyStateButton.setOnClickListener(v -> showCollectionFabOptionsDialog()); } RecyclerView recyclerView = root.findViewById(R.id.collection_list); @@ -109,6 +109,24 @@ public class CollectionsFragment extends MCFragment { itemTouchHelper.attachToRecyclerView(recyclerView); } + private void showCollectionFabOptionsDialog() { + String[] options = new String[]{ + getString(R.string.title_new_collection), + getString(R.string.action_import_collection) + }; + new AlertDialog.Builder(requireContext()) + .setTitle(R.string.title_collection_actions) + .setItems(options, (dialog, which) -> { + if (which == 0) { + showCreateCollectionDialog(); + } else if (which == 1) { + importCollectionFromFile(); + } + }) + .setNegativeButton(R.string.dialog_cancel, null) + .show(); + } + private void showCreateCollectionDialog() { Context context = requireContext(); LinearLayout layout = new LinearLayout(context); diff --git a/Android/app/src/main/java/com/majinnaibu/monstercards/ui/dashboard/DashboardFragment.java b/Android/app/src/main/java/com/majinnaibu/monstercards/ui/dashboard/DashboardFragment.java index 20f3a2f..153ed3f 100644 --- a/Android/app/src/main/java/com/majinnaibu/monstercards/ui/dashboard/DashboardFragment.java +++ b/Android/app/src/main/java/com/majinnaibu/monstercards/ui/dashboard/DashboardFragment.java @@ -148,15 +148,24 @@ public class DashboardFragment extends MCFragment { private void showAddToDashboardOptionsDialog() { String[] options = new String[]{ getString(R.string.action_add_single_monster), - getString(R.string.action_add_collection_option) + getString(R.string.action_add_collection_option), + getString(R.string.action_create_monster), + getString(R.string.action_import_monster_from_url), + getString(R.string.action_import_monster_from_file) }; new AlertDialog.Builder(requireContext()) - .setTitle(R.string.dialog_add_to_dashboard) + .setTitle(R.string.title_dashboard_actions) .setItems(options, (dialog, which) -> { if (which == 0) { showAddMonsterPicker(); } else if (which == 1) { showAddCollectionPicker(); + } else if (which == 2) { + createNewMonster(); + } else if (which == 3) { + showImportUrlDialog(); + } else if (which == 4) { + importMonsterFromFile(); } }) .setNegativeButton(R.string.dialog_cancel, null) diff --git a/Android/app/src/main/java/com/majinnaibu/monstercards/ui/library/LibraryFragment.java b/Android/app/src/main/java/com/majinnaibu/monstercards/ui/library/LibraryFragment.java index 1f716a6..6c6166d 100644 --- a/Android/app/src/main/java/com/majinnaibu/monstercards/ui/library/LibraryFragment.java +++ b/Android/app/src/main/java/com/majinnaibu/monstercards/ui/library/LibraryFragment.java @@ -79,6 +79,9 @@ public class LibraryFragment extends MCFragment { if (item.getItemId() == R.id.menu_action_import_from_url) { showImportUrlDialog(); return true; + } else if (item.getItemId() == R.id.menu_action_import_from_file) { + importMonsterFromFile(); + return true; } else if (item.getItemId() == R.id.menu_action_export_library) { exportLibrary(); return true; @@ -98,47 +101,6 @@ public class LibraryFragment extends MCFragment { }, Logger::logError); } - private void showImportUrlDialog() { - Context context = requireContext(); - AlertDialog.Builder builder = new AlertDialog.Builder(context); - builder.setTitle(R.string.dialog_import_url_title); - builder.setMessage(R.string.dialog_import_url_message); - - final EditText input = new EditText(context); - input.setHint(R.string.dialog_import_url_hint); - input.setSingleLine(true); - - ClipboardManager clipboard = (ClipboardManager) context.getSystemService(Context.CLIPBOARD_SERVICE); - if (clipboard != null && clipboard.hasPrimaryClip()) { - ClipData clip = clipboard.getPrimaryClip(); - if (clip != null && clip.getItemCount() > 0) { - CharSequence clipText = clip.getItemAt(0).getText(); - if (clipText != null) { - String clipStr = clipText.toString().trim(); - if (new DnDBeyondImporter().canImport(clipStr) || new Open5eImporter().canImport(clipStr)) { - input.setText(clipStr); - input.selectAll(); - } - } - } - } - - builder.setView(input); - - builder.setPositiveButton(R.string.dialog_import, (dialog, which) -> { - String urlOrId = input.getText().toString().trim(); - if (!urlOrId.isEmpty()) { - if (getActivity() instanceof MainActivity) { - ((MainActivity) getActivity()).importMonsterFromInputAndNavigate(urlOrId); - } - } - }); - - builder.setNegativeButton(R.string.dialog_cancel, (dialog, which) -> dialog.cancel()); - - builder.show(); - } - private void setupRecyclerView(@NonNull RecyclerView recyclerView, @Nullable View emptyState) { Context context = requireContext(); MonsterRepository repository = this.getMonsterRepository(); @@ -185,31 +147,28 @@ public class LibraryFragment extends MCFragment { } private void setupAddMonsterButton(@NonNull FloatingActionButton fab) { - fab.setOnClickListener(view -> createNewMonster()); + fab.setOnClickListener(view -> showLibraryFabOptionsDialog()); } - private void createNewMonster() { - Monster monster = new Monster(); - monster.name = getString(R.string.default_monster_name); - MonsterRepository repository = this.getMonsterRepository(); - mDisposables.add(repository.addMonster(monster) - .subscribeOn(Schedulers.io()) - .observeOn(AndroidSchedulers.mainThread()) - .subscribeWith(new DisposableCompletableObserver() { - @Override - public void onComplete() { - navigateToEditMonster(monster.id); + private void showLibraryFabOptionsDialog() { + String[] options = new String[]{ + getString(R.string.action_create_monster), + getString(R.string.action_import_monster_from_url), + getString(R.string.action_import_monster_from_file) + }; + new AlertDialog.Builder(requireContext()) + .setTitle(R.string.title_library_actions) + .setItems(options, (dialog, which) -> { + if (which == 0) { + createNewMonster(); + } else if (which == 1) { + showImportUrlDialog(); + } else if (which == 2) { + importMonsterFromFile(); } - - @Override - public void onError(@io.reactivex.rxjava3.annotations.NonNull Throwable e) { - Logger.logError("Error creating monster", e); - View view = getView(); - if (view != null) { - SnackbarHelper.showLong(view, getString(R.string.snackbar_failed_to_create_monster)); - } - } - })); + }) + .setNegativeButton(R.string.dialog_cancel, null) + .show(); } protected void navigateToMonsterDetail(@NonNull UUID monsterId) { diff --git a/Android/app/src/main/java/com/majinnaibu/monstercards/ui/shared/MCFragment.java b/Android/app/src/main/java/com/majinnaibu/monstercards/ui/shared/MCFragment.java index 6835279..bd032cf 100644 --- a/Android/app/src/main/java/com/majinnaibu/monstercards/ui/shared/MCFragment.java +++ b/Android/app/src/main/java/com/majinnaibu/monstercards/ui/shared/MCFragment.java @@ -1,6 +1,8 @@ package com.majinnaibu.monstercards.ui.shared; import android.app.Activity; +import android.content.ClipData; +import android.content.ClipboardManager; import android.content.Context; import android.content.Intent; import android.database.Cursor; @@ -10,27 +12,45 @@ import android.os.Bundle; import android.provider.DocumentsContract; import android.provider.OpenableColumns; import android.view.View; +import android.widget.EditText; import androidx.activity.result.ActivityResultLauncher; import androidx.activity.result.contract.ActivityResultContracts; import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.appcompat.app.ActionBar; +import androidx.appcompat.app.AlertDialog; import androidx.appcompat.app.AppCompatActivity; import androidx.fragment.app.Fragment; +import androidx.navigation.NavController; +import androidx.navigation.Navigation; +import com.majinnaibu.monstercards.MainActivity; import com.majinnaibu.monstercards.MonsterCardsApplication; import com.majinnaibu.monstercards.R; import com.majinnaibu.monstercards.data.MonsterRepository; +import com.majinnaibu.monstercards.importers.DnDBeyondImporter; +import com.majinnaibu.monstercards.importers.Open5eImporter; +import com.majinnaibu.monstercards.models.Monster; import com.majinnaibu.monstercards.utils.Logger; import com.majinnaibu.monstercards.utils.SnackbarHelper; +import java.io.BufferedReader; +import java.io.InputStream; +import java.io.InputStreamReader; import java.io.OutputStream; import java.nio.charset.StandardCharsets; +import java.util.Objects; +import java.util.UUID; + +import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers; +import io.reactivex.rxjava3.observers.DisposableCompletableObserver; +import io.reactivex.rxjava3.schedulers.Schedulers; public class MCFragment extends Fragment { private String mPendingExportContent; private ActivityResultLauncher mCreateDocumentLauncher; + private ActivityResultLauncher mOpenDocumentLauncher; @Override public void onCreate(@Nullable Bundle savedInstanceState) { @@ -46,6 +66,137 @@ public class MCFragment extends Fragment { } mPendingExportContent = null; }); + + mOpenDocumentLauncher = registerForActivityResult( + new ActivityResultContracts.StartActivityForResult(), + result -> { + if (result.getResultCode() == Activity.RESULT_OK && result.getData() != null) { + Uri uri = result.getData().getData(); + if (uri != null && getActivity() instanceof MainActivity) { + String content = readContentsFromUri(uri); + if (content != null && !content.trim().isEmpty()) { + ((MainActivity) getActivity()).importMonsterFromInputAndNavigate(content); + } else { + View view = getView(); + if (view != null) { + SnackbarHelper.showLong(view, R.string.failed_to_import_url); + } + } + } + } + }); + } + + public void importMonsterFromFile() { + Intent intent = new Intent(Intent.ACTION_OPEN_DOCUMENT); + intent.addCategory(Intent.CATEGORY_OPENABLE); + intent.setType("*/*"); + try { + mOpenDocumentLauncher.launch(intent); + } catch (Exception e) { + Logger.logError("Failed to launch file picker", e); + View view = getView(); + if (view != null) { + SnackbarHelper.showLong(view, R.string.failed_to_import_url); + } + } + } + + public void importCollectionFromFile() { + importMonsterFromFile(); + } + + public void showImportUrlDialog() { + Context context = requireContext(); + AlertDialog.Builder builder = new AlertDialog.Builder(context); + builder.setTitle(R.string.dialog_import_url_title); + builder.setMessage(R.string.dialog_import_url_message); + + final EditText input = new EditText(context); + input.setHint(R.string.dialog_import_url_hint); + input.setSingleLine(true); + + ClipboardManager clipboard = (ClipboardManager) context.getSystemService(Context.CLIPBOARD_SERVICE); + if (clipboard != null && clipboard.hasPrimaryClip()) { + ClipData clip = clipboard.getPrimaryClip(); + if (clip != null && clip.getItemCount() > 0) { + CharSequence clipText = clip.getItemAt(0).getText(); + if (clipText != null) { + String clipStr = clipText.toString().trim(); + if (new DnDBeyondImporter().canImport(clipStr) || new Open5eImporter().canImport(clipStr)) { + input.setText(clipStr); + input.selectAll(); + } + } + } + } + + builder.setView(input); + + builder.setPositiveButton(R.string.dialog_import, (dialog, which) -> { + String urlOrId = input.getText().toString().trim(); + if (!urlOrId.isEmpty()) { + if (getActivity() instanceof MainActivity) { + ((MainActivity) getActivity()).importMonsterFromInputAndNavigate(urlOrId); + } + } + }); + + builder.setNegativeButton(R.string.dialog_cancel, (dialog, which) -> dialog.cancel()); + + builder.show(); + } + + public void createNewMonster() { + Monster monster = new Monster(); + monster.name = getString(R.string.default_monster_name); + MonsterRepository repository = getMonsterRepository(); + repository.addMonster(monster) + .subscribeOn(Schedulers.io()) + .observeOn(AndroidSchedulers.mainThread()) + .subscribe(new DisposableCompletableObserver() { + @Override + public void onComplete() { + navigateToEditMonster(monster.id); + } + + @Override + public void onError(@io.reactivex.rxjava3.annotations.NonNull Throwable e) { + Logger.logError("Error creating monster", e); + View view = getView(); + if (view != null) { + SnackbarHelper.showLong(view, getString(R.string.snackbar_failed_to_create_monster)); + } + } + }); + } + + protected void navigateToEditMonster(@NonNull UUID monsterId) { + try { + NavController navController = Navigation.findNavController(requireView()); + Bundle args = new Bundle(); + args.putString("monster_id", monsterId.toString()); + navController.navigate(R.id.edit_monster_navigation, args); + } catch (Exception e) { + Logger.logError("Error navigating to edit monster", e); + } + } + + private String readContentsFromUri(@NonNull Uri uri) { + Context context = getContext(); + if (context == null) return null; + StringBuilder builder = new StringBuilder(); + try (InputStream inputStream = context.getContentResolver().openInputStream(uri); + BufferedReader reader = new BufferedReader(new InputStreamReader(Objects.requireNonNull(inputStream), StandardCharsets.UTF_8))) { + String line; + while ((line = reader.readLine()) != null) { + builder.append(line).append("\n"); + } + } catch (Exception e) { + Logger.logError("Error reading URI contents for import", e); + return null; + } + return builder.toString(); } public MonsterCardsApplication getApplication() { diff --git a/Android/app/src/main/res/menu/library_menu.xml b/Android/app/src/main/res/menu/library_menu.xml index f3b03a5..7772c1f 100644 --- a/Android/app/src/main/res/menu/library_menu.xml +++ b/Android/app/src/main/res/menu/library_menu.xml @@ -7,6 +7,11 @@ android:title="@string/action_import_from_url" app:showAsAction="never" /> + + Dashboard is already empty Remove %1$s? Remove %1$s from your dashboard? + + Create Monster + Import Monster from Url + Import Monster from File + Import Collection + Library Actions + Collection Actions + Dashboard Actions