Added Github Repo Import Worker and Framework
This commit is contained in:
@@ -29,9 +29,11 @@ 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.BinderImporter;
|
||||||
import com.majinnaibu.monstercards.importers.DnDBeyondImporter;
|
import com.majinnaibu.monstercards.importers.DnDBeyondImporter;
|
||||||
|
import com.majinnaibu.monstercards.importers.GitRepoImporterService;
|
||||||
import com.majinnaibu.monstercards.importers.Open5eApiWrapper;
|
import com.majinnaibu.monstercards.importers.Open5eApiWrapper;
|
||||||
import com.majinnaibu.monstercards.init.AppCenterInitializer;
|
import com.majinnaibu.monstercards.init.AppCenterInitializer;
|
||||||
import com.majinnaibu.monstercards.models.BinderExport;
|
import com.majinnaibu.monstercards.models.BinderExport;
|
||||||
|
import com.majinnaibu.monstercards.models.GitRepositorySource;
|
||||||
import com.majinnaibu.monstercards.models.Monster;
|
import com.majinnaibu.monstercards.models.Monster;
|
||||||
import com.majinnaibu.monstercards.utils.Logger;
|
import com.majinnaibu.monstercards.utils.Logger;
|
||||||
import com.majinnaibu.monstercards.utils.SnackbarHelper;
|
import com.majinnaibu.monstercards.utils.SnackbarHelper;
|
||||||
@@ -54,6 +56,7 @@ import io.reactivex.rxjava3.schedulers.Schedulers;
|
|||||||
public class MainActivity extends AppCompatActivity {
|
public class MainActivity extends AppCompatActivity {
|
||||||
|
|
||||||
private Disposable mOpen5eImportDisposable;
|
private Disposable mOpen5eImportDisposable;
|
||||||
|
private Disposable mGitImportDisposable;
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public boolean onOptionsItemSelected(@NonNull MenuItem item) {
|
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) {
|
public void importMultipleFilesFromUris(@NonNull List<Uri> uris) {
|
||||||
if (uris.isEmpty()) return;
|
if (uris.isEmpty()) return;
|
||||||
if (uris.size() == 1) {
|
if (uris.size() == 1) {
|
||||||
@@ -439,5 +467,8 @@ public class MainActivity extends AppCompatActivity {
|
|||||||
if (mOpen5eImportDisposable != null && !mOpen5eImportDisposable.isDisposed()) {
|
if (mOpen5eImportDisposable != null && !mOpen5eImportDisposable.isDisposed()) {
|
||||||
mOpen5eImportDisposable.dispose();
|
mOpen5eImportDisposable.dispose();
|
||||||
}
|
}
|
||||||
|
if (mGitImportDisposable != null && !mGitImportDisposable.isDisposed()) {
|
||||||
|
mGitImportDisposable.dispose();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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"
|
||||||
|
));
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -36,6 +36,9 @@ import com.majinnaibu.monstercards.utils.Logger;
|
|||||||
import com.majinnaibu.monstercards.utils.SnackbarHelper;
|
import com.majinnaibu.monstercards.utils.SnackbarHelper;
|
||||||
import com.majinnaibu.monstercards.utils.ToastHelper;
|
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.BufferedReader;
|
||||||
import java.io.InputStream;
|
import java.io.InputStream;
|
||||||
import java.io.InputStreamReader;
|
import java.io.InputStreamReader;
|
||||||
@@ -163,9 +166,38 @@ public class MCFragment extends Fragment {
|
|||||||
dialog.dismiss();
|
dialog.dismiss();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
view.findViewById(R.id.button_import_github).setOnClickListener(v -> {
|
||||||
|
dialog.dismiss();
|
||||||
|
showGithubRepoPicker();
|
||||||
|
});
|
||||||
|
|
||||||
dialog.show();
|
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() {
|
public void createNewMonster() {
|
||||||
Monster monster = new Monster();
|
Monster monster = new Monster();
|
||||||
monster.name = getString(R.string.default_monster_name);
|
monster.name = getString(R.string.default_monster_name);
|
||||||
|
|||||||
@@ -47,4 +47,11 @@
|
|||||||
android:layout_marginTop="8dp"
|
android:layout_marginTop="8dp"
|
||||||
android:text="@string/action_import_from_open5e" />
|
android:text="@string/action_import_from_open5e" />
|
||||||
|
|
||||||
|
<Button
|
||||||
|
android:id="@+id/button_import_github"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_marginTop="8dp"
|
||||||
|
android:text="@string/action_import_from_github" />
|
||||||
|
|
||||||
</LinearLayout>
|
</LinearLayout>
|
||||||
|
|||||||
@@ -231,4 +231,9 @@
|
|||||||
<string name="snackbar_open5e_import_complete">Finished importing Open5e monsters.</string>
|
<string name="snackbar_open5e_import_complete">Finished importing Open5e monsters.</string>
|
||||||
<string name="snackbar_open5e_import_cancelled">Cancelled Open5e import.</string>
|
<string name="snackbar_open5e_import_cancelled">Cancelled Open5e import.</string>
|
||||||
<string name="snackbar_open5e_import_failed">Failed to import from Open5e.</string>
|
<string name="snackbar_open5e_import_failed">Failed to import from Open5e.</string>
|
||||||
|
<string name="action_import_from_github">From GitHub Repository</string>
|
||||||
|
<string name="dialog_select_github_repo">Select Repository</string>
|
||||||
|
<string name="snackbar_importing_github">Downloading and importing repository…</string>
|
||||||
|
<string name="snackbar_github_import_complete">Imported %1$d monsters from %2$s.</string>
|
||||||
|
<string name="snackbar_github_import_failed">Failed to import from GitHub repository.</string>
|
||||||
</resources>
|
</resources>
|
||||||
|
|||||||
Reference in New Issue
Block a user