From f74e21e7d99aae07af9ee584f3719ee8a989ea94 Mon Sep 17 00:00:00 2001 From: Tom Hicks Date: Mon, 21 Sep 2026 13:24:29 -0700 Subject: [PATCH] Unified Import Source configuration, added concurrency prevention and combined UI --- .../majinnaibu/monstercards/MainActivity.java | 136 ++++++++---------- .../data/GitRepositoryConfig.java | 22 --- .../monstercards/data/ImportConfig.java | 34 +++++ .../importers/GitRepoImporterService.java | 26 ++-- .../models/GitRepositorySource.java | 21 --- .../monstercards/models/ImportSource.java | 30 ++++ .../monstercards/ui/shared/MCFragment.java | 25 ++-- .../app/src/main/res/layout/dialog_import.xml | 11 +- Android/app/src/main/res/values/strings.xml | 20 +-- 9 files changed, 165 insertions(+), 160 deletions(-) delete mode 100644 Android/app/src/main/java/com/majinnaibu/monstercards/data/GitRepositoryConfig.java create mode 100644 Android/app/src/main/java/com/majinnaibu/monstercards/data/ImportConfig.java delete mode 100644 Android/app/src/main/java/com/majinnaibu/monstercards/models/GitRepositorySource.java create mode 100644 Android/app/src/main/java/com/majinnaibu/monstercards/models/ImportSource.java diff --git a/Android/app/src/main/java/com/majinnaibu/monstercards/MainActivity.java b/Android/app/src/main/java/com/majinnaibu/monstercards/MainActivity.java index 970bede..a2a585c 100644 --- a/Android/app/src/main/java/com/majinnaibu/monstercards/MainActivity.java +++ b/Android/app/src/main/java/com/majinnaibu/monstercards/MainActivity.java @@ -10,6 +10,7 @@ import android.view.View; import androidx.annotation.NonNull; import androidx.annotation.Nullable; +import androidx.appcompat.app.AlertDialog; import androidx.appcompat.app.AppCompatActivity; import androidx.core.graphics.Insets; import androidx.core.view.ViewCompat; @@ -33,7 +34,7 @@ import com.majinnaibu.monstercards.importers.GitRepoImporterService; import com.majinnaibu.monstercards.importers.Open5eApiWrapper; import com.majinnaibu.monstercards.init.AppCenterInitializer; import com.majinnaibu.monstercards.models.BinderExport; -import com.majinnaibu.monstercards.models.GitRepositorySource; +import com.majinnaibu.monstercards.models.ImportSource; import com.majinnaibu.monstercards.models.Monster; import com.majinnaibu.monstercards.utils.Logger; import com.majinnaibu.monstercards.utils.SnackbarHelper; @@ -53,10 +54,12 @@ import io.reactivex.rxjava3.core.Single; import io.reactivex.rxjava3.disposables.Disposable; import io.reactivex.rxjava3.schedulers.Schedulers; +import java.util.concurrent.Callable; + public class MainActivity extends AppCompatActivity { - private Disposable mOpen5eImportDisposable; - private Disposable mGitImportDisposable; + private Disposable mImportDisposable; + private ImportSource mCurrentImportSource; @Override public boolean onOptionsItemSelected(@NonNull MenuItem item) { @@ -212,83 +215,73 @@ public class MainActivity extends AppCompatActivity { } } - public void importAllFromOpen5e() { - if (mOpen5eImportDisposable != null && !mOpen5eImportDisposable.isDisposed()) { - ToastHelper.showShort(this, "Open5e import already running"); + public void startImportFromSource(ImportSource source) { + if (mImportDisposable != null && !mImportDisposable.isDisposed()) { + new AlertDialog.Builder(this) + .setTitle(R.string.dialog_import_already_running_title) + .setMessage(getString(R.string.dialog_import_already_running_message, mCurrentImportSource != null ? mCurrentImportSource.projectName : "")) + .setPositiveButton(R.string.action_cancel_current, (dialog, which) -> { + mImportDisposable.dispose(); + executeImportFromSource(source); + }) + .setNegativeButton(R.string.dialog_cancel, null) + .show(); return; } + executeImportFromSource(source); + } + private void executeImportFromSource(ImportSource source) { + mCurrentImportSource = source; View rootView = findViewById(android.R.id.content); - Snackbar snackbar = SnackbarHelper.makeIndefinite(rootView, R.string.snackbar_importing_open5e); + Snackbar snackbar = SnackbarHelper.makeIndefinite(rootView, getString(R.string.snackbar_importing_source, source.projectName)); snackbar.setAction(R.string.action_cancel, v -> { - if (mOpen5eImportDisposable != null && !mOpen5eImportDisposable.isDisposed()) { - mOpen5eImportDisposable.dispose(); - SnackbarHelper.showLong(rootView, R.string.snackbar_open5e_import_cancelled); + if (mImportDisposable != null && !mImportDisposable.isDisposed()) { + mImportDisposable.dispose(); + SnackbarHelper.showLong(rootView, getString(R.string.snackbar_import_source_cancelled, source.projectName)); } }); snackbar.show(); - mOpen5eImportDisposable = Single.fromCallable(() -> { - int totalImported = 0; - String nextUrl = null; - do { - if (mOpen5eImportDisposable != null && mOpen5eImportDisposable.isDisposed()) { - break; - } - Open5eApiWrapper.Open5ePageResult page = Open5eApiWrapper.fetchPage(nextUrl); - for (Monster monster : page.monsters) { - if (mOpen5eImportDisposable != null && mOpen5eImportDisposable.isDisposed()) { - break; + Callable importTask; + if (source.importType == ImportSource.ImportType.OPEN5E_API) { + importTask = () -> { + int totalImported = 0; + String nextUrl = null; + do { + if (mImportDisposable != null && mImportDisposable.isDisposed()) break; + Open5eApiWrapper.Open5ePageResult page = Open5eApiWrapper.fetchPage(nextUrl); + for (Monster monster : page.monsters) { + if (mImportDisposable != null && mImportDisposable.isDisposed()) break; + try { + ((MonsterCardsApplication) getApplication()).getMonsterRepository() + .saveMonster(monster) + .blockingAwait(); + totalImported++; + } catch (Exception e) { + Logger.logError("Failed to save monster to database", e); + runOnUiThread(() -> ToastHelper.showShort(MainActivity.this, "An error occurred while saving a monster.")); + } } - try { - ((MonsterCardsApplication) getApplication()).getMonsterRepository() - .saveMonster(monster) - .blockingAwait(); - totalImported++; - } catch (Exception e) { - Logger.logError("Failed to save monster to database", e); - runOnUiThread(() -> ToastHelper.showShort(MainActivity.this, "An error occurred while saving a monster.")); - } - } - nextUrl = page.nextUrl; - } while (nextUrl != null && !nextUrl.isEmpty()); - return totalImported; - }) - .subscribeOn(Schedulers.io()) - .observeOn(AndroidSchedulers.mainThread()) - .subscribe(result -> { - snackbar.dismiss(); - SnackbarHelper.showLong(rootView, R.string.snackbar_open5e_import_complete); - }, throwable -> { - snackbar.dismiss(); - Logger.logError("Failed to import all Open5e monsters", throwable); - SnackbarHelper.showLong(rootView, R.string.snackbar_open5e_import_failed); - }); - } - - public void importFromGitRepository(@NonNull GitRepositorySource source) { - if (mGitImportDisposable != null && !mGitImportDisposable.isDisposed()) { - ToastHelper.showShort(this, "A GitHub import is already running."); - return; + nextUrl = page.nextUrl; + } while (nextUrl != null && !nextUrl.isEmpty()); + return totalImported; + }; + } else { + importTask = () -> GitRepoImporterService.importFromGitRepository(getApplicationContext(), source, () -> mImportDisposable != null && mImportDisposable.isDisposed()); } - View rootView = findViewById(android.R.id.content); - Snackbar snackbar = SnackbarHelper.makeIndefinite(rootView, R.string.snackbar_importing_github); - snackbar.show(); - - mGitImportDisposable = Single.fromCallable(() -> - GitRepoImporterService.importFromGitRepository(getApplicationContext(), source) - ) - .subscribeOn(Schedulers.io()) - .observeOn(AndroidSchedulers.mainThread()) - .subscribe(resultCount -> { - snackbar.dismiss(); - SnackbarHelper.showLong(rootView, getString(R.string.snackbar_github_import_complete, resultCount, source.projectName)); - }, throwable -> { - snackbar.dismiss(); - Logger.logError("Failed to import from GitHub repository: " + source.projectName, throwable); - SnackbarHelper.showLong(rootView, R.string.snackbar_github_import_failed); - }); + mImportDisposable = Single.fromCallable(importTask) + .subscribeOn(Schedulers.io()) + .observeOn(AndroidSchedulers.mainThread()) + .subscribe(resultCount -> { + snackbar.dismiss(); + SnackbarHelper.showLong(rootView, getString(R.string.snackbar_import_source_complete, resultCount, source.projectName)); + }, throwable -> { + snackbar.dismiss(); + Logger.logError("Failed to import from source: " + source.projectName, throwable); + SnackbarHelper.showLong(rootView, getString(R.string.snackbar_import_source_failed, source.projectName)); + }); } public void importMultipleFilesFromUris(@NonNull List uris) { @@ -464,11 +457,8 @@ public class MainActivity extends AppCompatActivity { @Override protected void onDestroy() { super.onDestroy(); - if (mOpen5eImportDisposable != null && !mOpen5eImportDisposable.isDisposed()) { - mOpen5eImportDisposable.dispose(); - } - if (mGitImportDisposable != null && !mGitImportDisposable.isDisposed()) { - mGitImportDisposable.dispose(); + if (mImportDisposable != null && !mImportDisposable.isDisposed()) { + mImportDisposable.dispose(); } } } diff --git a/Android/app/src/main/java/com/majinnaibu/monstercards/data/GitRepositoryConfig.java b/Android/app/src/main/java/com/majinnaibu/monstercards/data/GitRepositoryConfig.java deleted file mode 100644 index 9bb0d3a..0000000 --- a/Android/app/src/main/java/com/majinnaibu/monstercards/data/GitRepositoryConfig.java +++ /dev/null @@ -1,22 +0,0 @@ -package com.majinnaibu.monstercards.data; - -import com.majinnaibu.monstercards.models.GitRepositorySource; - -import java.util.ArrayList; -import java.util.List; - -public class GitRepositoryConfig { - public static final List SOURCES = new ArrayList<>(); - - static { - SOURCES.add(new GitRepositorySource( - "pf2e_foundry", - "Pathfinder 2e Foundry VTT", - "foundryvtt", - "https://github.com/foundryvtt/pf2e", - "https://github.com/foundryvtt/pf2e/archive/refs/heads/master.zip", - "com.majinnaibu.monstercards.importers.Pf2eImporter", - ".json" - )); - } -} diff --git a/Android/app/src/main/java/com/majinnaibu/monstercards/data/ImportConfig.java b/Android/app/src/main/java/com/majinnaibu/monstercards/data/ImportConfig.java new file mode 100644 index 0000000..37b2c0b --- /dev/null +++ b/Android/app/src/main/java/com/majinnaibu/monstercards/data/ImportConfig.java @@ -0,0 +1,34 @@ +package com.majinnaibu.monstercards.data; + +import com.majinnaibu.monstercards.models.ImportSource; + +import java.util.ArrayList; +import java.util.List; + +public class ImportConfig { + public static final List SOURCES = new ArrayList<>(); + + static { + SOURCES.add(new ImportSource( + "open5e", + "Open5e.com System Reference Document", + "Open5e", + "https://open5e.com", + ImportSource.ImportType.OPEN5E_API, + null, + null, + null + )); + + SOURCES.add(new ImportSource( + "pf2e_foundry", + "Pathfinder 2e Foundry VTT", + "foundryvtt", + "https://github.com/foundryvtt/pf2e", + ImportSource.ImportType.GIT_ARCHIVE, + "https://github.com/foundryvtt/pf2e/archive/refs/heads/master.zip", + "com.majinnaibu.monstercards.importers.Pf2eImporter", + ".json" + )); + } +} diff --git a/Android/app/src/main/java/com/majinnaibu/monstercards/importers/GitRepoImporterService.java b/Android/app/src/main/java/com/majinnaibu/monstercards/importers/GitRepoImporterService.java index 3d969da..0e04cad 100644 --- a/Android/app/src/main/java/com/majinnaibu/monstercards/importers/GitRepoImporterService.java +++ b/Android/app/src/main/java/com/majinnaibu/monstercards/importers/GitRepoImporterService.java @@ -7,7 +7,7 @@ import androidx.annotation.NonNull; import com.majinnaibu.monstercards.MonsterCardsApplication; import com.majinnaibu.monstercards.data.MonsterRepository; -import com.majinnaibu.monstercards.models.GitRepositorySource; +import com.majinnaibu.monstercards.models.ImportSource; import com.majinnaibu.monstercards.models.Monster; import com.majinnaibu.monstercards.utils.Logger; @@ -24,23 +24,28 @@ import java.net.URL; import java.nio.charset.StandardCharsets; import java.util.zip.ZipEntry; import java.util.zip.ZipInputStream; +import java.util.function.BooleanSupplier; public class GitRepoImporterService { - public static int importFromGitRepository(Context context, GitRepositorySource source) throws Exception { + public static int importFromGitRepository(Context context, ImportSource source, BooleanSupplier isCancelled) throws Exception { int totalImported = 0; File tempZip = File.createTempFile("repo_download", ".zip", context.getCacheDir()); File extractDir = new File(context.getCacheDir(), "repo_extracted_" + System.currentTimeMillis()); try { - downloadZip(source.downloadUrl, tempZip); + downloadZip(source.downloadUrl, tempZip, isCancelled); + + if (isCancelled.getAsBoolean()) return 0; if (!extractDir.exists()) { extractDir.mkdirs(); } - unzipAndFilter(tempZip, extractDir, source.fileExtension); + unzipAndFilter(tempZip, extractDir, source.fileExtension, isCancelled); + + if (isCancelled.getAsBoolean()) return 0; // Instantiate Importer dynamically Class clazz = Class.forName(source.importerClassName); @@ -50,7 +55,7 @@ public class GitRepoImporterService { MonsterRepository repository = ((MonsterCardsApplication) context.getApplicationContext()).getMonsterRepository(); // Process files - totalImported = processDirectory(extractDir, importer, repository); + totalImported = processDirectory(extractDir, importer, repository, isCancelled); } finally { deleteFileOrDir(tempZip); @@ -60,7 +65,7 @@ public class GitRepoImporterService { return totalImported; } - private static void downloadZip(String urlStr, File dest) throws IOException { + private static void downloadZip(String urlStr, File dest, BooleanSupplier isCancelled) throws IOException { URL url = new URL(urlStr); HttpURLConnection conn = (HttpURLConnection) url.openConnection(); conn.setRequestMethod("GET"); @@ -86,27 +91,29 @@ public class GitRepoImporterService { byte[] buffer = new byte[8192]; int count; while ((count = in.read(buffer)) != -1) { + if (isCancelled.getAsBoolean()) break; out.write(buffer, 0, count); } } } - private static void unzipAndFilter(File zipFile, File extractDir, String filterExtension) throws IOException { + private static void unzipAndFilter(File zipFile, File extractDir, String filterExtension, BooleanSupplier isCancelled) throws IOException { try (ZipInputStream zis = new ZipInputStream(new FileInputStream(zipFile))) { ZipEntry entry; while ((entry = zis.getNextEntry()) != null) { + if (isCancelled.getAsBoolean()) break; if (!entry.isDirectory()) { String name = entry.getName(); if (filterExtension == null || name.endsWith(filterExtension)) { File outFile = new File(extractDir, new File(name).getName()); if (outFile.exists()) { - // in case there are multiple files with same name in different folders outFile = new File(extractDir, System.currentTimeMillis() + "_" + new File(name).getName()); } try (FileOutputStream fos = new FileOutputStream(outFile)) { byte[] buffer = new byte[8192]; int count; while ((count = zis.read(buffer)) != -1) { + if (isCancelled.getAsBoolean()) break; fos.write(buffer, 0, count); } } @@ -117,12 +124,13 @@ public class GitRepoImporterService { } } - private static int processDirectory(File dir, EntityImporter importer, MonsterRepository repository) { + private static int processDirectory(File dir, EntityImporter importer, MonsterRepository repository, BooleanSupplier isCancelled) { int importedCount = 0; File[] files = dir.listFiles(); if (files == null) return 0; for (File file : files) { + if (isCancelled.getAsBoolean()) break; if (file.isFile()) { String content = readFileContent(file); if (content != null && importer.canImport(content)) { diff --git a/Android/app/src/main/java/com/majinnaibu/monstercards/models/GitRepositorySource.java b/Android/app/src/main/java/com/majinnaibu/monstercards/models/GitRepositorySource.java deleted file mode 100644 index ec02f84..0000000 --- a/Android/app/src/main/java/com/majinnaibu/monstercards/models/GitRepositorySource.java +++ /dev/null @@ -1,21 +0,0 @@ -package com.majinnaibu.monstercards.models; - -public class GitRepositorySource { - public String id; - public String projectName; - public String creatorGithubName; - public String creatorPageLink; - public String downloadUrl; - public String importerClassName; - public String fileExtension; - - public GitRepositorySource(String id, String projectName, String creatorGithubName, String creatorPageLink, String downloadUrl, String importerClassName, String fileExtension) { - this.id = id; - this.projectName = projectName; - this.creatorGithubName = creatorGithubName; - this.creatorPageLink = creatorPageLink; - this.downloadUrl = downloadUrl; - this.importerClassName = importerClassName; - this.fileExtension = fileExtension; - } -} diff --git a/Android/app/src/main/java/com/majinnaibu/monstercards/models/ImportSource.java b/Android/app/src/main/java/com/majinnaibu/monstercards/models/ImportSource.java new file mode 100644 index 0000000..819d19d --- /dev/null +++ b/Android/app/src/main/java/com/majinnaibu/monstercards/models/ImportSource.java @@ -0,0 +1,30 @@ +package com.majinnaibu.monstercards.models; + +public class ImportSource { + public enum ImportType { + GIT_ARCHIVE, + OPEN5E_API + } + + public String id; + public String projectName; + public String creatorName; + public String creatorPageLink; + public ImportType importType; + + // Optional attributes specific to Git archive + public String downloadUrl; + public String importerClassName; + public String fileExtension; + + public ImportSource(String id, String projectName, String creatorName, String creatorPageLink, ImportType importType, String downloadUrl, String importerClassName, String fileExtension) { + this.id = id; + this.projectName = projectName; + this.creatorName = creatorName; + this.creatorPageLink = creatorPageLink; + this.importType = importType; + this.downloadUrl = downloadUrl; + this.importerClassName = importerClassName; + this.fileExtension = fileExtension; + } +} 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 7203e4e..00e490f 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 @@ -36,8 +36,8 @@ import com.majinnaibu.monstercards.utils.Logger; import com.majinnaibu.monstercards.utils.SnackbarHelper; import com.majinnaibu.monstercards.utils.ToastHelper; -import com.majinnaibu.monstercards.data.GitRepositoryConfig; -import com.majinnaibu.monstercards.models.GitRepositorySource; +import com.majinnaibu.monstercards.data.ImportConfig; +import com.majinnaibu.monstercards.models.ImportSource; import java.io.BufferedReader; import java.io.InputStream; @@ -159,24 +159,17 @@ public class MCFragment extends Fragment { dialog.dismiss(); }); - view.findViewById(R.id.button_import_open5e).setOnClickListener(v -> { - if (getActivity() instanceof MainActivity) { - ((MainActivity) getActivity()).importAllFromOpen5e(); - } + view.findViewById(R.id.button_import_source).setOnClickListener(v -> { dialog.dismiss(); - }); - - view.findViewById(R.id.button_import_github).setOnClickListener(v -> { - dialog.dismiss(); - showGithubRepoPicker(); + showSourcePicker(); }); dialog.show(); } - private void showGithubRepoPicker() { + private void showSourcePicker() { Context context = requireContext(); - List sources = GitRepositoryConfig.SOURCES; + List sources = ImportConfig.SOURCES; if (sources.isEmpty()) { return; } @@ -187,11 +180,11 @@ public class MCFragment extends Fragment { } new AlertDialog.Builder(context) - .setTitle(R.string.dialog_select_github_repo) + .setTitle(R.string.dialog_select_import_source) .setItems(repoNames, (d, which) -> { - GitRepositorySource selected = sources.get(which); + ImportSource selected = sources.get(which); if (getActivity() instanceof MainActivity) { - ((MainActivity) getActivity()).importFromGitRepository(selected); + ((MainActivity) getActivity()).startImportFromSource(selected); } }) .setNegativeButton(R.string.dialog_cancel, null) diff --git a/Android/app/src/main/res/layout/dialog_import.xml b/Android/app/src/main/res/layout/dialog_import.xml index 1b74e60..e3b39bf 100644 --- a/Android/app/src/main/res/layout/dialog_import.xml +++ b/Android/app/src/main/res/layout/dialog_import.xml @@ -41,17 +41,10 @@ android:text="@string/action_import_monster_from_file" />