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 47a7f1b..970bede 100644 --- a/Android/app/src/main/java/com/majinnaibu/monstercards/MainActivity.java +++ b/Android/app/src/main/java/com/majinnaibu/monstercards/MainActivity.java @@ -29,9 +29,11 @@ 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.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.Monster; import com.majinnaibu.monstercards.utils.Logger; import com.majinnaibu.monstercards.utils.SnackbarHelper; @@ -54,6 +56,7 @@ import io.reactivex.rxjava3.schedulers.Schedulers; public class MainActivity extends AppCompatActivity { private Disposable mOpen5eImportDisposable; + private Disposable mGitImportDisposable; @Override public boolean onOptionsItemSelected(@NonNull MenuItem item) { @@ -263,6 +266,31 @@ public class MainActivity extends AppCompatActivity { }); } + public void importFromGitRepository(@NonNull GitRepositorySource source) { + if (mGitImportDisposable != null && !mGitImportDisposable.isDisposed()) { + ToastHelper.showShort(this, "A GitHub import is already running."); + return; + } + + 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); + }); + } + public void importMultipleFilesFromUris(@NonNull List uris) { if (uris.isEmpty()) return; if (uris.size() == 1) { @@ -439,5 +467,8 @@ public class MainActivity extends AppCompatActivity { if (mOpen5eImportDisposable != null && !mOpen5eImportDisposable.isDisposed()) { mOpen5eImportDisposable.dispose(); } + if (mGitImportDisposable != null && !mGitImportDisposable.isDisposed()) { + mGitImportDisposable.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 new file mode 100644 index 0000000..9bb0d3a --- /dev/null +++ b/Android/app/src/main/java/com/majinnaibu/monstercards/data/GitRepositoryConfig.java @@ -0,0 +1,22 @@ +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/importers/GitRepoImporterService.java b/Android/app/src/main/java/com/majinnaibu/monstercards/importers/GitRepoImporterService.java new file mode 100644 index 0000000..3d969da --- /dev/null +++ b/Android/app/src/main/java/com/majinnaibu/monstercards/importers/GitRepoImporterService.java @@ -0,0 +1,168 @@ +package com.majinnaibu.monstercards.importers; + +import android.content.Context; +import android.net.Uri; + +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.Monster; +import com.majinnaibu.monstercards.utils.Logger; + +import java.io.BufferedInputStream; +import java.io.BufferedReader; +import java.io.File; +import java.io.FileInputStream; +import java.io.FileOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.io.InputStreamReader; +import java.net.HttpURLConnection; +import java.net.URL; +import java.nio.charset.StandardCharsets; +import java.util.zip.ZipEntry; +import java.util.zip.ZipInputStream; + +public class GitRepoImporterService { + + public static int importFromGitRepository(Context context, GitRepositorySource source) 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); + + if (!extractDir.exists()) { + extractDir.mkdirs(); + } + + unzipAndFilter(tempZip, extractDir, source.fileExtension); + + // Instantiate Importer dynamically + Class clazz = Class.forName(source.importerClassName); + @SuppressWarnings("unchecked") + EntityImporter importer = (EntityImporter) clazz.newInstance(); + + MonsterRepository repository = ((MonsterCardsApplication) context.getApplicationContext()).getMonsterRepository(); + + // Process files + totalImported = processDirectory(extractDir, importer, repository); + + } finally { + deleteFileOrDir(tempZip); + deleteFileOrDir(extractDir); + } + + return totalImported; + } + + private static void downloadZip(String urlStr, File dest) throws IOException { + URL url = new URL(urlStr); + HttpURLConnection conn = (HttpURLConnection) url.openConnection(); + conn.setRequestMethod("GET"); + conn.setConnectTimeout(15000); + conn.setReadTimeout(30000); + + int code = conn.getResponseCode(); + // GitHub might redirect archive links, so handle redirect + if (code == HttpURLConnection.HTTP_MOVED_TEMP || code == HttpURLConnection.HTTP_MOVED_PERM || code == HttpURLConnection.HTTP_SEE_OTHER) { + String newUrl = conn.getHeaderField("Location"); + conn = (HttpURLConnection) new URL(newUrl).openConnection(); + conn.setConnectTimeout(15000); + conn.setReadTimeout(30000); + code = conn.getResponseCode(); + } + + if (code != 200) { + throw new IllegalStateException("Git repo download failed with HTTP " + code); + } + + try (InputStream in = new BufferedInputStream(conn.getInputStream()); + FileOutputStream out = new FileOutputStream(dest)) { + byte[] buffer = new byte[8192]; + int count; + while ((count = in.read(buffer)) != -1) { + out.write(buffer, 0, count); + } + } + } + + private static void unzipAndFilter(File zipFile, File extractDir, String filterExtension) throws IOException { + try (ZipInputStream zis = new ZipInputStream(new FileInputStream(zipFile))) { + ZipEntry entry; + while ((entry = zis.getNextEntry()) != null) { + 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) { + fos.write(buffer, 0, count); + } + } + } + } + zis.closeEntry(); + } + } + } + + private static int processDirectory(File dir, EntityImporter importer, MonsterRepository repository) { + int importedCount = 0; + File[] files = dir.listFiles(); + if (files == null) return 0; + + for (File file : files) { + if (file.isFile()) { + String content = readFileContent(file); + if (content != null && importer.canImport(content)) { + try { + Monster monster = importer.parse(content); + repository.saveMonster(monster).blockingAwait(); + importedCount++; + } catch (Exception e) { + Logger.logError("Failed to parse/save monster from file: " + file.getName(), e); + } + } + } + } + return importedCount; + } + + private static String readFileContent(File file) { + StringBuilder sb = new StringBuilder(); + try (InputStream in = new FileInputStream(file); + BufferedReader reader = new BufferedReader(new InputStreamReader(in, StandardCharsets.UTF_8))) { + String line; + while ((line = reader.readLine()) != null) { + sb.append(line).append("\n"); + } + } catch (IOException e) { + Logger.logError("Error reading extracted file", e); + return null; + } + return sb.toString(); + } + + private static void deleteFileOrDir(File fileOrDir) { + if (fileOrDir.isDirectory()) { + File[] children = fileOrDir.listFiles(); + if (children != null) { + for (File child : children) { + deleteFileOrDir(child); + } + } + } + fileOrDir.delete(); + } +} diff --git a/Android/app/src/main/java/com/majinnaibu/monstercards/importers/Pf2eImporter.java b/Android/app/src/main/java/com/majinnaibu/monstercards/importers/Pf2eImporter.java new file mode 100644 index 0000000..1dff5e3 --- /dev/null +++ b/Android/app/src/main/java/com/majinnaibu/monstercards/importers/Pf2eImporter.java @@ -0,0 +1,20 @@ +package com.majinnaibu.monstercards.importers; + +import androidx.annotation.NonNull; + +import com.majinnaibu.monstercards.models.Monster; + +public class Pf2eImporter implements EntityImporter { + @Override + public boolean canImport(@NonNull String input) { + // TODO: Implement parsing check for PF2e JSON format + return false; + } + + @NonNull + @Override + public Monster parse(@NonNull String input) throws Exception { + // TODO: Implement actual conversion to Monster cards model + return new Monster(); + } +} 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 new file mode 100644 index 0000000..ec02f84 --- /dev/null +++ b/Android/app/src/main/java/com/majinnaibu/monstercards/models/GitRepositorySource.java @@ -0,0 +1,21 @@ +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/ui/shared/MCFragment.java b/Android/app/src/main/java/com/majinnaibu/monstercards/ui/shared/MCFragment.java index 5ac0647..7203e4e 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,6 +36,9 @@ 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 java.io.BufferedReader; import java.io.InputStream; import java.io.InputStreamReader; @@ -163,9 +166,38 @@ public class MCFragment extends Fragment { dialog.dismiss(); }); + view.findViewById(R.id.button_import_github).setOnClickListener(v -> { + dialog.dismiss(); + showGithubRepoPicker(); + }); + dialog.show(); } + private void showGithubRepoPicker() { + Context context = requireContext(); + List sources = GitRepositoryConfig.SOURCES; + if (sources.isEmpty()) { + return; + } + + String[] repoNames = new String[sources.size()]; + for (int i = 0; i < sources.size(); i++) { + repoNames[i] = sources.get(i).projectName; + } + + new AlertDialog.Builder(context) + .setTitle(R.string.dialog_select_github_repo) + .setItems(repoNames, (d, which) -> { + GitRepositorySource selected = sources.get(which); + if (getActivity() instanceof MainActivity) { + ((MainActivity) getActivity()).importFromGitRepository(selected); + } + }) + .setNegativeButton(R.string.dialog_cancel, null) + .show(); + } + public void createNewMonster() { Monster monster = new Monster(); monster.name = getString(R.string.default_monster_name); diff --git a/Android/app/src/main/res/layout/dialog_import.xml b/Android/app/src/main/res/layout/dialog_import.xml index ae2a9e6..1b74e60 100644 --- a/Android/app/src/main/res/layout/dialog_import.xml +++ b/Android/app/src/main/res/layout/dialog_import.xml @@ -47,4 +47,11 @@ android:layout_marginTop="8dp" android:text="@string/action_import_from_open5e" /> +