Added Github Repo Import Worker and Framework

This commit is contained in:
2026-09-21 13:17:13 -07:00
committed by Tom
parent 8249c68266
commit cb26cec7bc
8 changed files with 306 additions and 0 deletions

View File

@@ -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<Uri> 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();
}
}
}

View File

@@ -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<GitRepositorySource> 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"
));
}
}

View File

@@ -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<Monster> importer = (EntityImporter<Monster>) 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<Monster> 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();
}
}

View File

@@ -0,0 +1,20 @@
package com.majinnaibu.monstercards.importers;
import androidx.annotation.NonNull;
import com.majinnaibu.monstercards.models.Monster;
public class Pf2eImporter implements EntityImporter<Monster> {
@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();
}
}

View File

@@ -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;
}
}

View File

@@ -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<GitRepositorySource> 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);