Adds card and binder export and import.
This commit is contained in:
@@ -22,6 +22,7 @@ import com.google.android.material.bottomnavigation.BottomNavigationView;
|
||||
import com.google.gson.Gson;
|
||||
import com.majinnaibu.monstercards.helpers.MonsterImportHelper;
|
||||
import com.majinnaibu.monstercards.helpers.StringHelper;
|
||||
import com.majinnaibu.monstercards.importers.BinderImporter;
|
||||
import com.majinnaibu.monstercards.importers.DnDBeyondImporter;
|
||||
import com.majinnaibu.monstercards.init.AppCenterInitializer;
|
||||
import com.majinnaibu.monstercards.utils.Logger;
|
||||
@@ -110,6 +111,25 @@ public class MainActivity extends AppCompatActivity {
|
||||
}
|
||||
|
||||
public void importMonsterFromInputAndNavigate(@NonNull String input) {
|
||||
BinderImporter binderImporter = new BinderImporter();
|
||||
if (binderImporter.canImport(input)) {
|
||||
Toast.makeText(this, R.string.toast_importing_url, Toast.LENGTH_SHORT).show();
|
||||
Single.fromCallable(() -> binderImporter.parse(input))
|
||||
.flatMapCompletable(binder -> ((MonsterCardsApplication) getApplication()).getMonsterRepository().importBinder(binder))
|
||||
.subscribeOn(Schedulers.io())
|
||||
.observeOn(AndroidSchedulers.mainThread())
|
||||
.subscribe(() -> {
|
||||
Toast.makeText(this, R.string.snackbar_import_binder_success, Toast.LENGTH_LONG).show();
|
||||
NavHostFragment navHostFragment = Objects.requireNonNull((NavHostFragment) getSupportFragmentManager().findFragmentById(R.id.nav_host_fragment));
|
||||
NavController navController = navHostFragment.getNavController();
|
||||
navController.navigate(R.id.navigation_library);
|
||||
}, throwable -> {
|
||||
Logger.logError("Failed to import binder from input", throwable);
|
||||
Toast.makeText(this, R.string.failed_to_import_url, Toast.LENGTH_LONG).show();
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
Toast.makeText(this, R.string.toast_importing_url, Toast.LENGTH_SHORT).show();
|
||||
Single.fromCallable(() -> MonsterImportHelper.fromJSON(input))
|
||||
.subscribeOn(Schedulers.io())
|
||||
@@ -143,7 +163,7 @@ public class MainActivity extends AppCompatActivity {
|
||||
|
||||
if (uri == null || !isMonsterFile(uri)) {
|
||||
if (uri != null) {
|
||||
Logger.logError("Ignored file because extension is not supported (.monster, .card): " + uri);
|
||||
Logger.logError("Ignored file because extension is not supported (.monster, .card, .binder): " + uri);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
@@ -162,7 +182,8 @@ public class MainActivity extends AppCompatActivity {
|
||||
}
|
||||
String lowerName = fileName.toLowerCase(Locale.ROOT);
|
||||
return lowerName.endsWith(".monster") || lowerName.endsWith(".monster.txt")
|
||||
|| lowerName.endsWith(".card") || lowerName.endsWith(".card.txt");
|
||||
|| lowerName.endsWith(".card") || lowerName.endsWith(".card.txt")
|
||||
|| lowerName.endsWith(".binder") || lowerName.endsWith(".binder.txt");
|
||||
}
|
||||
|
||||
@Nullable
|
||||
|
||||
@@ -4,6 +4,7 @@ import androidx.annotation.NonNull;
|
||||
|
||||
import com.majinnaibu.monstercards.AppDatabase;
|
||||
import com.majinnaibu.monstercards.helpers.StringHelper;
|
||||
import com.majinnaibu.monstercards.models.BinderExport;
|
||||
import com.majinnaibu.monstercards.models.Collection;
|
||||
import com.majinnaibu.monstercards.models.CollectionMonster;
|
||||
import com.majinnaibu.monstercards.models.CollectionWithCount;
|
||||
@@ -12,7 +13,9 @@ import com.majinnaibu.monstercards.models.Monster;
|
||||
import com.majinnaibu.monstercards.models.SearchResultItem;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.HashSet;
|
||||
import java.util.List;
|
||||
import java.util.Set;
|
||||
import java.util.UUID;
|
||||
|
||||
import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers;
|
||||
@@ -260,6 +263,67 @@ public class MonsterRepository {
|
||||
return result;
|
||||
}
|
||||
|
||||
public Completable importBinder(@NonNull BinderExport binder) {
|
||||
return Completable.fromAction(() -> {
|
||||
if (binder.collections == null || binder.collections.isEmpty()) {
|
||||
return;
|
||||
}
|
||||
|
||||
for (BinderExport.CollectionExport colExport : binder.collections) {
|
||||
if (colExport.cards == null || colExport.cards.isEmpty()) {
|
||||
continue;
|
||||
}
|
||||
|
||||
List<Monster> monstersToSave = new ArrayList<>();
|
||||
for (Monster card : colExport.cards) {
|
||||
if (card.id == null) {
|
||||
card.id = UUID.randomUUID();
|
||||
}
|
||||
monstersToSave.add(card);
|
||||
}
|
||||
m_db.monsterDAO().save(monstersToSave.toArray(new Monster[0])).blockingAwait();
|
||||
|
||||
String colName = colExport.name != null ? colExport.name.trim() : "";
|
||||
if (!colName.isEmpty()) {
|
||||
List<Collection> existingCols = m_db.collectionDAO().getAll().first(new ArrayList<>()).blockingGet();
|
||||
Collection targetCollection = null;
|
||||
for (Collection existing : existingCols) {
|
||||
if (existing.name != null && existing.name.equalsIgnoreCase(colName)) {
|
||||
targetCollection = existing;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
UUID targetCollectionId;
|
||||
if (targetCollection != null) {
|
||||
targetCollectionId = targetCollection.id;
|
||||
} else {
|
||||
Collection newCol = new Collection();
|
||||
newCol.id = UUID.randomUUID();
|
||||
newCol.name = colName;
|
||||
m_db.collectionDAO().save(newCol).blockingAwait();
|
||||
targetCollectionId = newCol.id;
|
||||
}
|
||||
|
||||
List<Monster> colMonsters = m_db.collectionDAO().getMonstersForCollection(targetCollectionId.toString())
|
||||
.first(new ArrayList<>()).blockingGet();
|
||||
Set<UUID> existingMonsterIds = new HashSet<>();
|
||||
for (Monster m : colMonsters) {
|
||||
existingMonsterIds.add(m.id);
|
||||
}
|
||||
|
||||
int ordinal = colMonsters.size();
|
||||
for (Monster monster : monstersToSave) {
|
||||
if (!existingMonsterIds.contains(monster.id)) {
|
||||
m_db.collectionDAO().addMonsterToCollection(new CollectionMonster(targetCollectionId, monster.id, ordinal++)).blockingAwait();
|
||||
existingMonsterIds.add(monster.id);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}).subscribeOn(Schedulers.io()).observeOn(AndroidSchedulers.mainThread());
|
||||
}
|
||||
|
||||
private static class Helpers {
|
||||
static boolean monsterMatchesSearch(Monster monster, String searchText) {
|
||||
if (StringHelper.isNullOrEmpty(searchText)) {
|
||||
|
||||
@@ -0,0 +1,34 @@
|
||||
package com.majinnaibu.monstercards.exporters;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
|
||||
import com.google.gson.Gson;
|
||||
import com.google.gson.GsonBuilder;
|
||||
import com.majinnaibu.monstercards.models.BinderExport;
|
||||
import com.majinnaibu.monstercards.models.Monster;
|
||||
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
|
||||
public class BinderExporter {
|
||||
private static final Gson GSON = new GsonBuilder().setPrettyPrinting().disableHtmlEscaping().create();
|
||||
|
||||
@NonNull
|
||||
public String exportBinder(String collectionName, List<Monster> monsters) {
|
||||
BinderExport export = new BinderExport();
|
||||
export.schemaVersion = 1;
|
||||
|
||||
if (monsters != null) {
|
||||
for (Monster monster : monsters) {
|
||||
monster.schemaVersion = 1;
|
||||
}
|
||||
}
|
||||
|
||||
BinderExport.CollectionExport col = new BinderExport.CollectionExport(
|
||||
collectionName != null ? collectionName : "",
|
||||
monsters
|
||||
);
|
||||
export.collections = Collections.singletonList(col);
|
||||
return GSON.toJson(export);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
package com.majinnaibu.monstercards.exporters;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
|
||||
import com.google.gson.Gson;
|
||||
import com.google.gson.GsonBuilder;
|
||||
import com.majinnaibu.monstercards.models.Monster;
|
||||
|
||||
public class MonsterCardExporter {
|
||||
private static final Gson GSON = new GsonBuilder().setPrettyPrinting().disableHtmlEscaping().create();
|
||||
|
||||
@NonNull
|
||||
public String exportCard(@NonNull Monster monster) {
|
||||
monster.schemaVersion = 1;
|
||||
return GSON.toJson(monster);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,37 @@
|
||||
package com.majinnaibu.monstercards.importers;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
|
||||
import com.google.gson.Gson;
|
||||
import com.google.gson.JsonElement;
|
||||
import com.google.gson.JsonObject;
|
||||
import com.google.gson.JsonParser;
|
||||
import com.majinnaibu.monstercards.models.BinderExport;
|
||||
|
||||
public class BinderImporter implements EntityImporter<BinderExport> {
|
||||
|
||||
private static final Gson GSON = new Gson();
|
||||
|
||||
@Override
|
||||
public boolean canImport(@NonNull String input) {
|
||||
try {
|
||||
JsonElement el = JsonParser.parseString(input);
|
||||
if (el.isJsonObject()) {
|
||||
JsonObject obj = el.getAsJsonObject();
|
||||
return obj.has("collections") && obj.get("collections").isJsonArray();
|
||||
}
|
||||
} catch (Exception ignored) {
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
@NonNull
|
||||
@Override
|
||||
public BinderExport parse(@NonNull String input) throws Exception {
|
||||
BinderExport binder = GSON.fromJson(input, BinderExport.class);
|
||||
if (binder == null || binder.collections == null) {
|
||||
throw new IllegalArgumentException("Failed to deserialize Binder JSON");
|
||||
}
|
||||
return binder;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
package com.majinnaibu.monstercards.models;
|
||||
|
||||
import com.google.gson.annotations.SerializedName;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
public class BinderExport {
|
||||
@SerializedName("schemaVersion")
|
||||
public int schemaVersion = 1;
|
||||
|
||||
@SerializedName("collections")
|
||||
public List<CollectionExport> collections = new ArrayList<>();
|
||||
|
||||
public static class CollectionExport {
|
||||
@SerializedName("name")
|
||||
public String name;
|
||||
|
||||
@SerializedName("cards")
|
||||
public List<Monster> cards = new ArrayList<>();
|
||||
|
||||
public CollectionExport() {
|
||||
}
|
||||
|
||||
public CollectionExport(String name, List<Monster> cards) {
|
||||
this.name = name != null ? name : "";
|
||||
this.cards = cards != null ? cards : new ArrayList<>();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -6,6 +6,7 @@ import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.room.ColumnInfo;
|
||||
import androidx.room.Entity;
|
||||
import androidx.room.Ignore;
|
||||
import androidx.room.PrimaryKey;
|
||||
|
||||
import com.majinnaibu.monstercards.data.enums.AbilityScore;
|
||||
@@ -29,6 +30,9 @@ import java.util.UUID;
|
||||
@SuppressWarnings("unused")
|
||||
public class Monster {
|
||||
|
||||
@Ignore
|
||||
public int schemaVersion = 1;
|
||||
|
||||
@PrimaryKey
|
||||
@NonNull
|
||||
public UUID id;
|
||||
|
||||
@@ -187,10 +187,28 @@ public class CollectionDetailFragment extends MCFragment {
|
||||
if (item.getItemId() == R.id.menu_action_add_collection_to_dashboard) {
|
||||
addCollectionToDashboard();
|
||||
return true;
|
||||
} else if (item.getItemId() == R.id.menu_action_export_collection) {
|
||||
exportCollection();
|
||||
return true;
|
||||
}
|
||||
return super.onOptionsItemSelected(item);
|
||||
}
|
||||
|
||||
private void exportCollection() {
|
||||
Collection collection = mViewModel.getCollection().getValue();
|
||||
List<Monster> monsters = mViewModel.getMonsters().getValue();
|
||||
if (collection != null) {
|
||||
String json = new com.majinnaibu.monstercards.exporters.BinderExporter().exportBinder(
|
||||
collection.name,
|
||||
monsters != null ? monsters : new java.util.ArrayList<>()
|
||||
);
|
||||
String collectionName = (collection.name != null && !collection.name.trim().isEmpty())
|
||||
? collection.name.trim()
|
||||
: "collection";
|
||||
exportToFile(collectionName + ".binder", json);
|
||||
}
|
||||
}
|
||||
|
||||
private void addCollectionToDashboard() {
|
||||
MonsterRepository repository = getMonsterRepository();
|
||||
mDisposables.add(repository.addCollectionToDashboard(mCollectionId)
|
||||
|
||||
@@ -274,10 +274,25 @@ public class DashboardFragment extends MCFragment {
|
||||
} else if (item.getItemId() == R.id.menu_action_clear_dashboard) {
|
||||
clearDashboard();
|
||||
return true;
|
||||
} else if (item.getItemId() == R.id.menu_action_export_dashboard) {
|
||||
exportDashboard();
|
||||
return true;
|
||||
}
|
||||
return super.onOptionsItemSelected(item);
|
||||
}
|
||||
|
||||
private void exportDashboard() {
|
||||
mDisposables.add(getMonsterRepository().getDashboardMonsters()
|
||||
.firstOrError()
|
||||
.subscribeOn(Schedulers.io())
|
||||
.observeOn(AndroidSchedulers.mainThread())
|
||||
.subscribe(monsters -> {
|
||||
String json = new com.majinnaibu.monstercards.exporters.BinderExporter().exportBinder("", monsters);
|
||||
String fileName = getString(R.string.default_filename_dashboard) + ".binder";
|
||||
exportToFile(fileName, json);
|
||||
}, Logger::logError));
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onDestroyView() {
|
||||
super.onDestroyView();
|
||||
|
||||
@@ -70,10 +70,25 @@ 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_export_library) {
|
||||
exportLibrary();
|
||||
return true;
|
||||
}
|
||||
return super.onOptionsItemSelected(item);
|
||||
}
|
||||
|
||||
private void exportLibrary() {
|
||||
getMonsterRepository().getMonsters()
|
||||
.firstOrError()
|
||||
.subscribeOn(Schedulers.io())
|
||||
.observeOn(AndroidSchedulers.mainThread())
|
||||
.subscribe(monsters -> {
|
||||
String json = new com.majinnaibu.monstercards.exporters.BinderExporter().exportBinder("", monsters);
|
||||
String fileName = getString(R.string.default_filename_library) + ".binder";
|
||||
exportToFile(fileName, json);
|
||||
}, Logger::logError);
|
||||
}
|
||||
|
||||
private void showImportUrlDialog() {
|
||||
Context context = requireContext();
|
||||
AlertDialog.Builder builder = new AlertDialog.Builder(context);
|
||||
|
||||
@@ -195,10 +195,25 @@ public class MonsterDetailFragment extends MCFragment {
|
||||
} else if (item.getItemId() == R.id.menu_action_share_monster) {
|
||||
shareCurrentMonster();
|
||||
return true;
|
||||
} else if (item.getItemId() == R.id.menu_action_export_card) {
|
||||
exportCurrentMonsterCard();
|
||||
return true;
|
||||
}
|
||||
return super.onOptionsItemSelected(item);
|
||||
}
|
||||
|
||||
private void exportCurrentMonsterCard() {
|
||||
Monster monster = mViewModel.getMonster();
|
||||
if (monster == null) {
|
||||
return;
|
||||
}
|
||||
String jsonCard = new com.majinnaibu.monstercards.exporters.MonsterCardExporter().exportCard(monster);
|
||||
String safeName = (monster.name != null && !monster.name.trim().isEmpty())
|
||||
? monster.name.trim()
|
||||
: "monster";
|
||||
exportToFile(safeName + ".card", jsonCard);
|
||||
}
|
||||
|
||||
private void shareCurrentMonster() {
|
||||
Monster monster = mViewModel.getMonster();
|
||||
if (monster == null) {
|
||||
|
||||
@@ -1,15 +1,53 @@
|
||||
package com.majinnaibu.monstercards.ui.shared;
|
||||
|
||||
import android.app.Activity;
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
import android.database.Cursor;
|
||||
import android.net.Uri;
|
||||
import android.os.Build;
|
||||
import android.os.Bundle;
|
||||
import android.provider.DocumentsContract;
|
||||
import android.provider.OpenableColumns;
|
||||
import android.view.View;
|
||||
|
||||
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.AppCompatActivity;
|
||||
import androidx.fragment.app.Fragment;
|
||||
|
||||
import com.google.android.material.snackbar.Snackbar;
|
||||
import com.majinnaibu.monstercards.MonsterCardsApplication;
|
||||
import com.majinnaibu.monstercards.R;
|
||||
import com.majinnaibu.monstercards.data.MonsterRepository;
|
||||
import com.majinnaibu.monstercards.utils.Logger;
|
||||
|
||||
import java.io.OutputStream;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
|
||||
public class MCFragment extends Fragment {
|
||||
private String mPendingExportContent;
|
||||
private ActivityResultLauncher<Intent> mCreateDocumentLauncher;
|
||||
|
||||
@Override
|
||||
public void onCreate(@Nullable Bundle savedInstanceState) {
|
||||
super.onCreate(savedInstanceState);
|
||||
mCreateDocumentLauncher = registerForActivityResult(
|
||||
new ActivityResultContracts.StartActivityForResult(),
|
||||
result -> {
|
||||
if (result.getResultCode() == Activity.RESULT_OK && result.getData() != null) {
|
||||
Uri uri = result.getData().getData();
|
||||
if (uri != null && mPendingExportContent != null) {
|
||||
writeContentToUri(uri, mPendingExportContent);
|
||||
}
|
||||
}
|
||||
mPendingExportContent = null;
|
||||
});
|
||||
}
|
||||
|
||||
public MonsterCardsApplication getApplication() {
|
||||
return (MonsterCardsApplication) requireActivity().getApplication();
|
||||
}
|
||||
@@ -32,4 +70,76 @@ public class MCFragment extends Fragment {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
protected void exportToFile(@NonNull String defaultFileName, @NonNull String content) {
|
||||
mPendingExportContent = content;
|
||||
Intent intent = new Intent(Intent.ACTION_CREATE_DOCUMENT);
|
||||
intent.addCategory(Intent.CATEGORY_OPENABLE);
|
||||
intent.setType("*/*");
|
||||
intent.putExtra(Intent.EXTRA_TITLE, defaultFileName);
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
||||
Uri downloadsUri = Uri.parse("content://com.android.externalstorage.documents/document/primary%3ADownload");
|
||||
intent.putExtra(DocumentsContract.EXTRA_INITIAL_URI, downloadsUri);
|
||||
}
|
||||
try {
|
||||
mCreateDocumentLauncher.launch(intent);
|
||||
} catch (Exception e) {
|
||||
Logger.logError("Failed to launch document creation picker", e);
|
||||
View view = getView();
|
||||
if (view != null) {
|
||||
Snackbar.make(view, R.string.snackbar_export_failed, Snackbar.LENGTH_LONG).show();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void writeContentToUri(@NonNull Uri uri, @NonNull String content) {
|
||||
Context context = getContext();
|
||||
if (context == null) return;
|
||||
try (OutputStream out = context.getContentResolver().openOutputStream(uri)) {
|
||||
if (out != null) {
|
||||
out.write(content.getBytes(StandardCharsets.UTF_8));
|
||||
out.flush();
|
||||
View view = getView();
|
||||
if (view != null) {
|
||||
String fileName = getFileNameFromUri(context, uri);
|
||||
String message = getString(R.string.snackbar_export_success, fileName);
|
||||
Snackbar.make(view, message, Snackbar.LENGTH_LONG).show();
|
||||
}
|
||||
}
|
||||
} catch (Exception e) {
|
||||
Logger.logError("Failed to write export content to URI", e);
|
||||
View view = getView();
|
||||
if (view != null) {
|
||||
Snackbar.make(view, R.string.snackbar_export_failed, Snackbar.LENGTH_LONG).show();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private String getFileNameFromUri(@NonNull Context context, @NonNull Uri uri) {
|
||||
String displayName = null;
|
||||
if ("content".equals(uri.getScheme())) {
|
||||
try (Cursor cursor = context.getContentResolver().query(uri, new String[]{OpenableColumns.DISPLAY_NAME}, null, null, null)) {
|
||||
if (cursor != null && cursor.moveToFirst()) {
|
||||
int nameIndex = cursor.getColumnIndex(OpenableColumns.DISPLAY_NAME);
|
||||
if (nameIndex != -1) {
|
||||
displayName = cursor.getString(nameIndex);
|
||||
}
|
||||
}
|
||||
} catch (Exception e) {
|
||||
Logger.logError("Error querying display name from content URI", e);
|
||||
}
|
||||
}
|
||||
return displayName != null ? displayName : defaultFileNameFromUri(uri);
|
||||
}
|
||||
|
||||
private String defaultFileNameFromUri(@NonNull Uri uri) {
|
||||
String path = uri.getPath();
|
||||
if (path != null) {
|
||||
int lastSlash = path.lastIndexOf('/');
|
||||
if (lastSlash >= 0 && lastSlash < path.length() - 1) {
|
||||
return path.substring(lastSlash + 1);
|
||||
}
|
||||
}
|
||||
return uri.toString();
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user