Adds card and binder export and import.
This commit is contained in:
@@ -48,6 +48,8 @@
|
|||||||
<data android:pathPattern=".*\\.monster\\.txt" />
|
<data android:pathPattern=".*\\.monster\\.txt" />
|
||||||
<data android:pathPattern=".*\\.card" />
|
<data android:pathPattern=".*\\.card" />
|
||||||
<data android:pathPattern=".*\\.card\\.txt" />
|
<data android:pathPattern=".*\\.card\\.txt" />
|
||||||
|
<data android:pathPattern=".*\\.binder" />
|
||||||
|
<data android:pathPattern=".*\\.binder\\.txt" />
|
||||||
</intent-filter>
|
</intent-filter>
|
||||||
|
|
||||||
<!-- Intent filter for explicitly sharing files/text to Monster Cards -->
|
<!-- Intent filter for explicitly sharing files/text to Monster Cards -->
|
||||||
|
|||||||
@@ -22,6 +22,7 @@ import com.google.android.material.bottomnavigation.BottomNavigationView;
|
|||||||
import com.google.gson.Gson;
|
import com.google.gson.Gson;
|
||||||
import com.majinnaibu.monstercards.helpers.MonsterImportHelper;
|
import com.majinnaibu.monstercards.helpers.MonsterImportHelper;
|
||||||
import com.majinnaibu.monstercards.helpers.StringHelper;
|
import com.majinnaibu.monstercards.helpers.StringHelper;
|
||||||
|
import com.majinnaibu.monstercards.importers.BinderImporter;
|
||||||
import com.majinnaibu.monstercards.importers.DnDBeyondImporter;
|
import com.majinnaibu.monstercards.importers.DnDBeyondImporter;
|
||||||
import com.majinnaibu.monstercards.init.AppCenterInitializer;
|
import com.majinnaibu.monstercards.init.AppCenterInitializer;
|
||||||
import com.majinnaibu.monstercards.utils.Logger;
|
import com.majinnaibu.monstercards.utils.Logger;
|
||||||
@@ -110,6 +111,25 @@ public class MainActivity extends AppCompatActivity {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public void importMonsterFromInputAndNavigate(@NonNull String input) {
|
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();
|
Toast.makeText(this, R.string.toast_importing_url, Toast.LENGTH_SHORT).show();
|
||||||
Single.fromCallable(() -> MonsterImportHelper.fromJSON(input))
|
Single.fromCallable(() -> MonsterImportHelper.fromJSON(input))
|
||||||
.subscribeOn(Schedulers.io())
|
.subscribeOn(Schedulers.io())
|
||||||
@@ -143,7 +163,7 @@ public class MainActivity extends AppCompatActivity {
|
|||||||
|
|
||||||
if (uri == null || !isMonsterFile(uri)) {
|
if (uri == null || !isMonsterFile(uri)) {
|
||||||
if (uri != null) {
|
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;
|
return null;
|
||||||
}
|
}
|
||||||
@@ -162,7 +182,8 @@ public class MainActivity extends AppCompatActivity {
|
|||||||
}
|
}
|
||||||
String lowerName = fileName.toLowerCase(Locale.ROOT);
|
String lowerName = fileName.toLowerCase(Locale.ROOT);
|
||||||
return lowerName.endsWith(".monster") || lowerName.endsWith(".monster.txt")
|
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
|
@Nullable
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import androidx.annotation.NonNull;
|
|||||||
|
|
||||||
import com.majinnaibu.monstercards.AppDatabase;
|
import com.majinnaibu.monstercards.AppDatabase;
|
||||||
import com.majinnaibu.monstercards.helpers.StringHelper;
|
import com.majinnaibu.monstercards.helpers.StringHelper;
|
||||||
|
import com.majinnaibu.monstercards.models.BinderExport;
|
||||||
import com.majinnaibu.monstercards.models.Collection;
|
import com.majinnaibu.monstercards.models.Collection;
|
||||||
import com.majinnaibu.monstercards.models.CollectionMonster;
|
import com.majinnaibu.monstercards.models.CollectionMonster;
|
||||||
import com.majinnaibu.monstercards.models.CollectionWithCount;
|
import com.majinnaibu.monstercards.models.CollectionWithCount;
|
||||||
@@ -12,7 +13,9 @@ import com.majinnaibu.monstercards.models.Monster;
|
|||||||
import com.majinnaibu.monstercards.models.SearchResultItem;
|
import com.majinnaibu.monstercards.models.SearchResultItem;
|
||||||
|
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
|
import java.util.HashSet;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
import java.util.Set;
|
||||||
import java.util.UUID;
|
import java.util.UUID;
|
||||||
|
|
||||||
import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers;
|
import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers;
|
||||||
@@ -260,6 +263,67 @@ public class MonsterRepository {
|
|||||||
return result;
|
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 {
|
private static class Helpers {
|
||||||
static boolean monsterMatchesSearch(Monster monster, String searchText) {
|
static boolean monsterMatchesSearch(Monster monster, String searchText) {
|
||||||
if (StringHelper.isNullOrEmpty(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.annotation.Nullable;
|
||||||
import androidx.room.ColumnInfo;
|
import androidx.room.ColumnInfo;
|
||||||
import androidx.room.Entity;
|
import androidx.room.Entity;
|
||||||
|
import androidx.room.Ignore;
|
||||||
import androidx.room.PrimaryKey;
|
import androidx.room.PrimaryKey;
|
||||||
|
|
||||||
import com.majinnaibu.monstercards.data.enums.AbilityScore;
|
import com.majinnaibu.monstercards.data.enums.AbilityScore;
|
||||||
@@ -29,6 +30,9 @@ import java.util.UUID;
|
|||||||
@SuppressWarnings("unused")
|
@SuppressWarnings("unused")
|
||||||
public class Monster {
|
public class Monster {
|
||||||
|
|
||||||
|
@Ignore
|
||||||
|
public int schemaVersion = 1;
|
||||||
|
|
||||||
@PrimaryKey
|
@PrimaryKey
|
||||||
@NonNull
|
@NonNull
|
||||||
public UUID id;
|
public UUID id;
|
||||||
|
|||||||
@@ -187,10 +187,28 @@ public class CollectionDetailFragment extends MCFragment {
|
|||||||
if (item.getItemId() == R.id.menu_action_add_collection_to_dashboard) {
|
if (item.getItemId() == R.id.menu_action_add_collection_to_dashboard) {
|
||||||
addCollectionToDashboard();
|
addCollectionToDashboard();
|
||||||
return true;
|
return true;
|
||||||
|
} else if (item.getItemId() == R.id.menu_action_export_collection) {
|
||||||
|
exportCollection();
|
||||||
|
return true;
|
||||||
}
|
}
|
||||||
return super.onOptionsItemSelected(item);
|
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() {
|
private void addCollectionToDashboard() {
|
||||||
MonsterRepository repository = getMonsterRepository();
|
MonsterRepository repository = getMonsterRepository();
|
||||||
mDisposables.add(repository.addCollectionToDashboard(mCollectionId)
|
mDisposables.add(repository.addCollectionToDashboard(mCollectionId)
|
||||||
|
|||||||
@@ -274,10 +274,25 @@ public class DashboardFragment extends MCFragment {
|
|||||||
} else if (item.getItemId() == R.id.menu_action_clear_dashboard) {
|
} else if (item.getItemId() == R.id.menu_action_clear_dashboard) {
|
||||||
clearDashboard();
|
clearDashboard();
|
||||||
return true;
|
return true;
|
||||||
|
} else if (item.getItemId() == R.id.menu_action_export_dashboard) {
|
||||||
|
exportDashboard();
|
||||||
|
return true;
|
||||||
}
|
}
|
||||||
return super.onOptionsItemSelected(item);
|
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
|
@Override
|
||||||
public void onDestroyView() {
|
public void onDestroyView() {
|
||||||
super.onDestroyView();
|
super.onDestroyView();
|
||||||
|
|||||||
@@ -70,10 +70,25 @@ 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_export_library) {
|
||||||
|
exportLibrary();
|
||||||
|
return true;
|
||||||
}
|
}
|
||||||
return super.onOptionsItemSelected(item);
|
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() {
|
private void showImportUrlDialog() {
|
||||||
Context context = requireContext();
|
Context context = requireContext();
|
||||||
AlertDialog.Builder builder = new AlertDialog.Builder(context);
|
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) {
|
} else if (item.getItemId() == R.id.menu_action_share_monster) {
|
||||||
shareCurrentMonster();
|
shareCurrentMonster();
|
||||||
return true;
|
return true;
|
||||||
|
} else if (item.getItemId() == R.id.menu_action_export_card) {
|
||||||
|
exportCurrentMonsterCard();
|
||||||
|
return true;
|
||||||
}
|
}
|
||||||
return super.onOptionsItemSelected(item);
|
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() {
|
private void shareCurrentMonster() {
|
||||||
Monster monster = mViewModel.getMonster();
|
Monster monster = mViewModel.getMonster();
|
||||||
if (monster == null) {
|
if (monster == null) {
|
||||||
|
|||||||
@@ -1,15 +1,53 @@
|
|||||||
package com.majinnaibu.monstercards.ui.shared;
|
package com.majinnaibu.monstercards.ui.shared;
|
||||||
|
|
||||||
import android.app.Activity;
|
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.ActionBar;
|
||||||
import androidx.appcompat.app.AppCompatActivity;
|
import androidx.appcompat.app.AppCompatActivity;
|
||||||
import androidx.fragment.app.Fragment;
|
import androidx.fragment.app.Fragment;
|
||||||
|
|
||||||
|
import com.google.android.material.snackbar.Snackbar;
|
||||||
import com.majinnaibu.monstercards.MonsterCardsApplication;
|
import com.majinnaibu.monstercards.MonsterCardsApplication;
|
||||||
|
import com.majinnaibu.monstercards.R;
|
||||||
import com.majinnaibu.monstercards.data.MonsterRepository;
|
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 {
|
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() {
|
public MonsterCardsApplication getApplication() {
|
||||||
return (MonsterCardsApplication) requireActivity().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();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,4 +6,9 @@
|
|||||||
android:id="@+id/menu_action_add_collection_to_dashboard"
|
android:id="@+id/menu_action_add_collection_to_dashboard"
|
||||||
android:title="@string/action_add_collection_to_dashboard"
|
android:title="@string/action_add_collection_to_dashboard"
|
||||||
app:showAsAction="never" />
|
app:showAsAction="never" />
|
||||||
|
|
||||||
|
<item
|
||||||
|
android:id="@+id/menu_action_export_collection"
|
||||||
|
android:title="@string/action_export_collection"
|
||||||
|
app:showAsAction="never" />
|
||||||
</menu>
|
</menu>
|
||||||
|
|||||||
@@ -16,4 +16,9 @@
|
|||||||
android:id="@+id/menu_action_clear_dashboard"
|
android:id="@+id/menu_action_clear_dashboard"
|
||||||
android:title="@string/action_clear_dashboard"
|
android:title="@string/action_clear_dashboard"
|
||||||
app:showAsAction="never" />
|
app:showAsAction="never" />
|
||||||
|
|
||||||
|
<item
|
||||||
|
android:id="@+id/menu_action_export_dashboard"
|
||||||
|
android:title="@string/action_export_dashboard"
|
||||||
|
app:showAsAction="never" />
|
||||||
</menu>
|
</menu>
|
||||||
|
|||||||
@@ -6,4 +6,9 @@
|
|||||||
android:id="@+id/menu_action_import_from_url"
|
android:id="@+id/menu_action_import_from_url"
|
||||||
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_export_library"
|
||||||
|
android:title="@string/action_export_library"
|
||||||
|
app:showAsAction="never" />
|
||||||
</menu>
|
</menu>
|
||||||
|
|||||||
@@ -23,4 +23,9 @@
|
|||||||
android:id="@+id/menu_action_add_to_dashboard"
|
android:id="@+id/menu_action_add_to_dashboard"
|
||||||
android:title="@string/action_add_to_dashboard"
|
android:title="@string/action_add_to_dashboard"
|
||||||
app:showAsAction="never" />
|
app:showAsAction="never" />
|
||||||
|
|
||||||
|
<item
|
||||||
|
android:id="@+id/menu_action_export_card"
|
||||||
|
android:title="@string/action_export_card"
|
||||||
|
app:showAsAction="never" />
|
||||||
</menu>
|
</menu>
|
||||||
|
|||||||
@@ -171,4 +171,13 @@
|
|||||||
<string name="failed_to_import_url">Failed to import entity. Please check the URL, ID, or JSON format.</string>
|
<string name="failed_to_import_url">Failed to import entity. Please check the URL, ID, or JSON format.</string>
|
||||||
<string name="action_share_monster">Share / Export Monster (.card)</string>
|
<string name="action_share_monster">Share / Export Monster (.card)</string>
|
||||||
<string name="failed_to_share_monster">Failed to share monster</string>
|
<string name="failed_to_share_monster">Failed to share monster</string>
|
||||||
|
<string name="action_export_card">Export Card</string>
|
||||||
|
<string name="action_export_collection">Export Collection</string>
|
||||||
|
<string name="action_export_library">Export Library</string>
|
||||||
|
<string name="action_export_dashboard">Export Dashboard</string>
|
||||||
|
<string name="snackbar_export_success">Exported to %1$s</string>
|
||||||
|
<string name="snackbar_export_failed">Failed to export file</string>
|
||||||
|
<string name="snackbar_import_binder_success">Imported binder successfully</string>
|
||||||
|
<string name="default_filename_library">MonsterCards Library</string>
|
||||||
|
<string name="default_filename_dashboard">MonsterCards Dashboard</string>
|
||||||
</resources>
|
</resources>
|
||||||
|
|||||||
@@ -0,0 +1,59 @@
|
|||||||
|
package com.majinnaibu.monstercards.exporters;
|
||||||
|
|
||||||
|
import static org.junit.Assert.assertEquals;
|
||||||
|
import static org.junit.Assert.assertNotNull;
|
||||||
|
import static org.junit.Assert.assertTrue;
|
||||||
|
|
||||||
|
import com.google.gson.Gson;
|
||||||
|
import com.google.gson.JsonObject;
|
||||||
|
import com.google.gson.JsonParser;
|
||||||
|
import com.majinnaibu.monstercards.importers.BinderImporter;
|
||||||
|
import com.majinnaibu.monstercards.models.BinderExport;
|
||||||
|
import com.majinnaibu.monstercards.models.Monster;
|
||||||
|
|
||||||
|
import org.junit.Test;
|
||||||
|
|
||||||
|
import java.util.Collections;
|
||||||
|
import java.util.UUID;
|
||||||
|
|
||||||
|
public class BinderImportExportTest {
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testExportAndImportBinder() throws Exception {
|
||||||
|
Monster monster = new Monster();
|
||||||
|
monster.id = UUID.randomUUID();
|
||||||
|
monster.name = "Goblin Warrior";
|
||||||
|
monster.size = "Small";
|
||||||
|
monster.type = "humanoid";
|
||||||
|
|
||||||
|
BinderExporter exporter = new BinderExporter();
|
||||||
|
String json = exporter.exportBinder("Goblins", Collections.singletonList(monster));
|
||||||
|
|
||||||
|
BinderImporter importer = new BinderImporter();
|
||||||
|
assertTrue(importer.canImport(json));
|
||||||
|
|
||||||
|
BinderExport binder = importer.parse(json);
|
||||||
|
assertEquals(1, binder.schemaVersion);
|
||||||
|
assertNotNull(binder.collections);
|
||||||
|
assertEquals(1, binder.collections.size());
|
||||||
|
assertEquals("Goblins", binder.collections.get(0).name);
|
||||||
|
assertEquals(1, binder.collections.get(0).cards.size());
|
||||||
|
assertEquals("Goblin Warrior", binder.collections.get(0).cards.get(0).name);
|
||||||
|
assertEquals(1, binder.collections.get(0).cards.get(0).schemaVersion);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testExportSingleCard() {
|
||||||
|
Monster monster = new Monster();
|
||||||
|
monster.id = UUID.randomUUID();
|
||||||
|
monster.name = "Dragon";
|
||||||
|
|
||||||
|
MonsterCardExporter exporter = new MonsterCardExporter();
|
||||||
|
String json = exporter.exportCard(monster);
|
||||||
|
|
||||||
|
JsonObject obj = JsonParser.parseString(json).getAsJsonObject();
|
||||||
|
assertTrue(obj.has("schemaVersion"));
|
||||||
|
assertEquals(1, obj.get("schemaVersion").getAsInt());
|
||||||
|
assertEquals("Dragon", obj.get("name").getAsString());
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user