Adds more ways to add monsters.

This commit is contained in:
2026-09-20 17:20:43 -07:00
parent fa25c1bef3
commit 77176a6d4f
7 changed files with 242 additions and 68 deletions

View File

@@ -65,7 +65,7 @@ public class CollectionDetailFragment extends MCFragment {
FloatingActionButton fab = root.findViewById(R.id.fab_add_monster_to_collection); FloatingActionButton fab = root.findViewById(R.id.fab_add_monster_to_collection);
if (fab != null) { if (fab != null) {
fab.setOnClickListener(v -> showAddMonsterDialog()); fab.setOnClickListener(v -> showCollectionDetailFabOptionsDialog());
} }
mViewModel.getCollection().observe(getViewLifecycleOwner(), collection -> { mViewModel.getCollection().observe(getViewLifecycleOwner(), collection -> {
@@ -134,6 +134,30 @@ public class CollectionDetailFragment extends MCFragment {
recyclerView.setAdapter(mAdapter); 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() { private void showAddMonsterDialog() {
MonsterRepository repository = getMonsterRepository(); MonsterRepository repository = getMonsterRepository();
mDisposables.add(repository.getMonsters() mDisposables.add(repository.getMonsters()

View File

@@ -47,13 +47,13 @@ public class CollectionsFragment extends MCFragment {
FloatingActionButton fab = root.findViewById(R.id.fab_add_collection); FloatingActionButton fab = root.findViewById(R.id.fab_add_collection);
if (fab != null) { if (fab != null) {
fab.setOnClickListener(v -> showCreateCollectionDialog()); fab.setOnClickListener(v -> showCollectionFabOptionsDialog());
} }
View emptyState = root.findViewById(R.id.empty_state); View emptyState = root.findViewById(R.id.empty_state);
View emptyStateButton = root.findViewById(R.id.empty_state_button); View emptyStateButton = root.findViewById(R.id.empty_state_button);
if (emptyStateButton != null) { if (emptyStateButton != null) {
emptyStateButton.setOnClickListener(v -> showCreateCollectionDialog()); emptyStateButton.setOnClickListener(v -> showCollectionFabOptionsDialog());
} }
RecyclerView recyclerView = root.findViewById(R.id.collection_list); RecyclerView recyclerView = root.findViewById(R.id.collection_list);
@@ -109,6 +109,24 @@ public class CollectionsFragment extends MCFragment {
itemTouchHelper.attachToRecyclerView(recyclerView); 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() { private void showCreateCollectionDialog() {
Context context = requireContext(); Context context = requireContext();
LinearLayout layout = new LinearLayout(context); LinearLayout layout = new LinearLayout(context);

View File

@@ -148,15 +148,24 @@ public class DashboardFragment extends MCFragment {
private void showAddToDashboardOptionsDialog() { private void showAddToDashboardOptionsDialog() {
String[] options = new String[]{ String[] options = new String[]{
getString(R.string.action_add_single_monster), 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()) new AlertDialog.Builder(requireContext())
.setTitle(R.string.dialog_add_to_dashboard) .setTitle(R.string.title_dashboard_actions)
.setItems(options, (dialog, which) -> { .setItems(options, (dialog, which) -> {
if (which == 0) { if (which == 0) {
showAddMonsterPicker(); showAddMonsterPicker();
} else if (which == 1) { } else if (which == 1) {
showAddCollectionPicker(); showAddCollectionPicker();
} else if (which == 2) {
createNewMonster();
} else if (which == 3) {
showImportUrlDialog();
} else if (which == 4) {
importMonsterFromFile();
} }
}) })
.setNegativeButton(R.string.dialog_cancel, null) .setNegativeButton(R.string.dialog_cancel, null)

View File

@@ -79,6 +79,9 @@ public class LibraryFragment extends MCFragment {
if (item.getItemId() == R.id.menu_action_import_from_url) { if (item.getItemId() == R.id.menu_action_import_from_url) {
showImportUrlDialog(); showImportUrlDialog();
return true; 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) { } else if (item.getItemId() == R.id.menu_action_export_library) {
exportLibrary(); exportLibrary();
return true; return true;
@@ -98,47 +101,6 @@ public class LibraryFragment extends MCFragment {
}, Logger::logError); }, 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) { private void setupRecyclerView(@NonNull RecyclerView recyclerView, @Nullable View emptyState) {
Context context = requireContext(); Context context = requireContext();
MonsterRepository repository = this.getMonsterRepository(); MonsterRepository repository = this.getMonsterRepository();
@@ -185,31 +147,28 @@ public class LibraryFragment extends MCFragment {
} }
private void setupAddMonsterButton(@NonNull FloatingActionButton fab) { private void setupAddMonsterButton(@NonNull FloatingActionButton fab) {
fab.setOnClickListener(view -> createNewMonster()); fab.setOnClickListener(view -> showLibraryFabOptionsDialog());
} }
private void createNewMonster() { private void showLibraryFabOptionsDialog() {
Monster monster = new Monster(); String[] options = new String[]{
monster.name = getString(R.string.default_monster_name); getString(R.string.action_create_monster),
MonsterRepository repository = this.getMonsterRepository(); getString(R.string.action_import_monster_from_url),
mDisposables.add(repository.addMonster(monster) getString(R.string.action_import_monster_from_file)
.subscribeOn(Schedulers.io()) };
.observeOn(AndroidSchedulers.mainThread()) new AlertDialog.Builder(requireContext())
.subscribeWith(new DisposableCompletableObserver() { .setTitle(R.string.title_library_actions)
@Override .setItems(options, (dialog, which) -> {
public void onComplete() { if (which == 0) {
navigateToEditMonster(monster.id); createNewMonster();
} else if (which == 1) {
showImportUrlDialog();
} else if (which == 2) {
importMonsterFromFile();
} }
})
@Override .setNegativeButton(R.string.dialog_cancel, null)
public void onError(@io.reactivex.rxjava3.annotations.NonNull Throwable e) { .show();
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 navigateToMonsterDetail(@NonNull UUID monsterId) { protected void navigateToMonsterDetail(@NonNull UUID monsterId) {

View File

@@ -1,6 +1,8 @@
package com.majinnaibu.monstercards.ui.shared; package com.majinnaibu.monstercards.ui.shared;
import android.app.Activity; import android.app.Activity;
import android.content.ClipData;
import android.content.ClipboardManager;
import android.content.Context; import android.content.Context;
import android.content.Intent; import android.content.Intent;
import android.database.Cursor; import android.database.Cursor;
@@ -10,27 +12,45 @@ import android.os.Bundle;
import android.provider.DocumentsContract; import android.provider.DocumentsContract;
import android.provider.OpenableColumns; import android.provider.OpenableColumns;
import android.view.View; import android.view.View;
import android.widget.EditText;
import androidx.activity.result.ActivityResultLauncher; import androidx.activity.result.ActivityResultLauncher;
import androidx.activity.result.contract.ActivityResultContracts; import androidx.activity.result.contract.ActivityResultContracts;
import androidx.annotation.NonNull; import androidx.annotation.NonNull;
import androidx.annotation.Nullable; import androidx.annotation.Nullable;
import androidx.appcompat.app.ActionBar; import androidx.appcompat.app.ActionBar;
import androidx.appcompat.app.AlertDialog;
import androidx.appcompat.app.AppCompatActivity; import androidx.appcompat.app.AppCompatActivity;
import androidx.fragment.app.Fragment; 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.MonsterCardsApplication;
import com.majinnaibu.monstercards.R; import com.majinnaibu.monstercards.R;
import com.majinnaibu.monstercards.data.MonsterRepository; 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.Logger;
import com.majinnaibu.monstercards.utils.SnackbarHelper; import com.majinnaibu.monstercards.utils.SnackbarHelper;
import java.io.BufferedReader;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.OutputStream; import java.io.OutputStream;
import java.nio.charset.StandardCharsets; 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 { public class MCFragment extends Fragment {
private String mPendingExportContent; private String mPendingExportContent;
private ActivityResultLauncher<Intent> mCreateDocumentLauncher; private ActivityResultLauncher<Intent> mCreateDocumentLauncher;
private ActivityResultLauncher<Intent> mOpenDocumentLauncher;
@Override @Override
public void onCreate(@Nullable Bundle savedInstanceState) { public void onCreate(@Nullable Bundle savedInstanceState) {
@@ -46,6 +66,137 @@ public class MCFragment extends Fragment {
} }
mPendingExportContent = null; 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() { public MonsterCardsApplication getApplication() {

View File

@@ -7,6 +7,11 @@
android:title="@string/action_import_from_url" android:title="@string/action_import_from_url"
app:showAsAction="never" /> app:showAsAction="never" />
<item
android:id="@+id/menu_action_import_from_file"
android:title="@string/action_import_monster_from_file"
app:showAsAction="never" />
<item <item
android:id="@+id/menu_action_export_library" android:id="@+id/menu_action_export_library"
android:title="@string/action_export_library" android:title="@string/action_export_library"

View File

@@ -198,4 +198,12 @@
<string name="snackbar_dashboard_already_empty">Dashboard is already empty</string> <string name="snackbar_dashboard_already_empty">Dashboard is already empty</string>
<string name="dialog_remove_from_dashboard_title">Remove %1$s?</string> <string name="dialog_remove_from_dashboard_title">Remove %1$s?</string>
<string name="dialog_remove_from_dashboard_message">Remove %1$s from your dashboard?</string> <string name="dialog_remove_from_dashboard_message">Remove %1$s from your dashboard?</string>
<string name="action_create_monster">Create Monster</string>
<string name="action_import_monster_from_url">Import Monster from Url</string>
<string name="action_import_monster_from_file">Import Monster from File</string>
<string name="action_import_collection">Import Collection</string>
<string name="title_library_actions">Library Actions</string>
<string name="title_collection_actions">Collection Actions</string>
<string name="title_dashboard_actions">Dashboard Actions</string>
</resources> </resources>