diff --git a/app_pojavlauncher/src/main/java/net/kdt/pojavlaunch/Tools.java b/app_pojavlauncher/src/main/java/net/kdt/pojavlaunch/Tools.java index 9a5392ee7..792c3deb7 100644 --- a/app_pojavlauncher/src/main/java/net/kdt/pojavlaunch/Tools.java +++ b/app_pojavlauncher/src/main/java/net/kdt/pojavlaunch/Tools.java @@ -79,6 +79,7 @@ import java.util.Map; @SuppressWarnings("IOStreamConstructor") public final class Tools { + public static final float BYTE_TO_MB = 1024 * 1024; public static final Handler MAIN_HANDLER = new Handler(Looper.getMainLooper()); public static String APP_NAME = "null"; diff --git a/app_pojavlauncher/src/main/java/net/kdt/pojavlaunch/fragments/SearchModFragment.java b/app_pojavlauncher/src/main/java/net/kdt/pojavlaunch/fragments/SearchModFragment.java index b767e467c..24af84154 100644 --- a/app_pojavlauncher/src/main/java/net/kdt/pojavlaunch/fragments/SearchModFragment.java +++ b/app_pojavlauncher/src/main/java/net/kdt/pojavlaunch/fragments/SearchModFragment.java @@ -1,5 +1,7 @@ package net.kdt.pojavlaunch.fragments; +import android.content.res.ColorStateList; +import android.graphics.Color; import android.os.Bundle; import android.util.Log; import android.view.View; @@ -37,6 +39,8 @@ public class SearchModFragment extends Fragment { private ModItemAdapter mModItemAdapter; private ProgressBar mSearchProgressBar; private Future mSearchFuture; + private TextView mStatusTextView; + private ColorStateList mDefaultTextColor; private ModpackApi modpackApi; @@ -59,6 +63,9 @@ public class SearchModFragment extends Fragment { mSelectedVersion = view.findViewById(R.id.search_mod_selected_mc_version_textview); mSelectVersionButton = view.findViewById(R.id.search_mod_mc_version_button); mRecyclerview = view.findViewById(R.id.search_mod_list); + mStatusTextView = view.findViewById(R.id.search_mod_status_text); + + mDefaultTextColor = mStatusTextView.getTextColors(); mRecyclerview.setLayoutManager(new LinearLayoutManager(getContext())); mRecyclerview.setAdapter(mModItemAdapter); @@ -81,6 +88,7 @@ public class SearchModFragment extends Fragment { return true; }); } + class SearchModRunnable implements Runnable{ private final Object mFutureLock = new Object(); private final SearchFilters mRunnableFilters; @@ -109,9 +117,22 @@ public class SearchModFragment extends Fragment { ModItem[] items = modpackApi.searchMod(mRunnableFilters); Log.d(SearchModFragment.class.toString(), Arrays.toString(items)); Tools.runOnUiThread(() -> { + ModItem[] localItems = items; if(mMyFuture.isCancelled()) return; mSearchProgressBar.setVisibility(View.GONE); - mModItemAdapter.setModItems(items, mSelectedVersion.getText().toString()); + if(localItems == null) { + mStatusTextView.setVisibility(View.VISIBLE); + mStatusTextView.setTextColor(Color.RED); + mStatusTextView.setText(R.string.search_modpack_error); + }else if(localItems.length == 0) { + mStatusTextView.setVisibility(View.VISIBLE); + mStatusTextView.setTextColor(mDefaultTextColor); + mStatusTextView.setText(R.string.search_modpack_no_result); + localItems = null; + }else{ + mStatusTextView.setVisibility(View.GONE); + } + mModItemAdapter.setModItems(localItems, mSelectedVersion.getText().toString()); }); } } diff --git a/app_pojavlauncher/src/main/java/net/kdt/pojavlaunch/modloaders/modpacks/ModItemAdapter.java b/app_pojavlauncher/src/main/java/net/kdt/pojavlaunch/modloaders/modpacks/ModItemAdapter.java index e46b99520..ad6a5f049 100644 --- a/app_pojavlauncher/src/main/java/net/kdt/pojavlaunch/modloaders/modpacks/ModItemAdapter.java +++ b/app_pojavlauncher/src/main/java/net/kdt/pojavlaunch/modloaders/modpacks/ModItemAdapter.java @@ -1,5 +1,6 @@ package net.kdt.pojavlaunch.modloaders.modpacks; +import android.annotation.SuppressLint; import android.graphics.Bitmap; import android.view.LayoutInflater; import android.view.View; @@ -9,7 +10,6 @@ import android.widget.Button; import android.widget.ImageView; import android.widget.Spinner; import android.widget.TextView; -import android.widget.Toast; import androidx.annotation.NonNull; import androidx.recyclerview.widget.RecyclerView; @@ -28,7 +28,7 @@ import net.kdt.pojavlaunch.modloaders.modpacks.models.ModItem; import java.util.Arrays; public class ModItemAdapter extends RecyclerView.Adapter { - + private static final ModItem[] MOD_ITEMS_EMPTY = new ModItem[0]; private final ModIconCache mIconCache = new ModIconCache(); private ModItem[] mModItems; private final ModpackApi mModpackApi; @@ -44,7 +44,7 @@ public class ModItemAdapter extends RecyclerView.Adapter sThreadLocalBuffer = new ThreadLocal<>(); + private final ThreadPoolExecutor mDownloadPool = new ThreadPoolExecutor(4,4,100, TimeUnit.MILLISECONDS, + new LinkedBlockingQueue<>()); + private final AtomicBoolean mTerminator = new AtomicBoolean(false); + private final AtomicLong mDownloadSize = new AtomicLong(0); + private final Object mExceptionSyncPoint = new Object(); + private final File mDestinationDirectory; + private IOException mFirstIOException; + private long mTotalSize; + + public ModDownloader(File destinationDirectory) { + this.mDestinationDirectory = destinationDirectory; + } + + public void submitDownload(int fileSize, String relativePath, String... url) { + mTotalSize += fileSize; + mDownloadPool.execute(new DownloadTask(url, new File(mDestinationDirectory, relativePath))); + } + + public void awaitFinish(Tools.DownloaderFeedback feedback) throws IOException{ + try { + mDownloadPool.shutdown(); + while(!mDownloadPool.awaitTermination(20, TimeUnit.MILLISECONDS) && !mTerminator.get()) { + feedback.updateProgress((int) mDownloadSize.get(), (int) mTotalSize); + } + + if(mTerminator.get()) { + synchronized (mExceptionSyncPoint) { + if(mFirstIOException == null) mExceptionSyncPoint.wait(); + throw mFirstIOException; + } + } + }catch (InterruptedException e) { + e.printStackTrace(); + } + } + + private static byte[] getThreadLocalBuffer() { + byte[] buffer = sThreadLocalBuffer.get(); + if(buffer != null) return buffer; + buffer = new byte[8192]; + sThreadLocalBuffer.set(buffer); + return buffer; + } + + class DownloadTask implements Runnable, Tools.DownloaderFeedback { + private final String[] mDownloadUrls; + private final File mDestination; + private int last = 0; + + public DownloadTask(String[] downloadurls, + File downloadDestination) { + this.mDownloadUrls = downloadurls; + this.mDestination = downloadDestination; + } + + @Override + public void run() { + IOException exception = null; + for(String sourceUrl : mDownloadUrls) { + try { + exception = tryDownload(sourceUrl); + if(exception == null) return; + }catch (InterruptedException e) { + return; + } + } + if(exception != null) { + synchronized (mExceptionSyncPoint) { + if(mFirstIOException == null) { + mFirstIOException = exception; + mExceptionSyncPoint.notify(); + } + } + } + } + + private IOException tryDownload(String sourceUrl) throws InterruptedException { + IOException exception = null; + for (int i = 0; i < 5; i++) { + try { + DownloadUtils.downloadFileMonitored(sourceUrl, mDestination, getThreadLocalBuffer(), this); + return null; + } catch (InterruptedIOException e) { + throw new InterruptedException(); + } catch (IOException e) { + e.printStackTrace(); + exception = e; + } + mDownloadSize.addAndGet(-last); + last = 0; + } + return exception; + } + + @Override + public void updateProgress(int curr, int max) { + mDownloadSize.addAndGet(curr - last); + last = curr; + } + } +} diff --git a/app_pojavlauncher/src/main/java/net/kdt/pojavlaunch/modloaders/modpacks/api/ModLoaderInfo.java b/app_pojavlauncher/src/main/java/net/kdt/pojavlaunch/modloaders/modpacks/api/ModLoaderInfo.java new file mode 100644 index 000000000..562f6bc34 --- /dev/null +++ b/app_pojavlauncher/src/main/java/net/kdt/pojavlaunch/modloaders/modpacks/api/ModLoaderInfo.java @@ -0,0 +1,29 @@ +package net.kdt.pojavlaunch.modloaders.modpacks.api; + +public class ModLoaderInfo { + public static final int MOD_LOADER_FORGE = 0; + public static final int MOD_LOADER_FABRIC = 1; + public static final int MOD_LOADER_QUILT = 2; + public final int modLoaderType; + public final String modLoaderVersion; + public final String minecraftVersion; + + public ModLoaderInfo(int modLoaderType, String modLoaderVersion, String minecraftVersion) { + this.modLoaderType = modLoaderType; + this.modLoaderVersion = modLoaderVersion; + this.minecraftVersion = minecraftVersion; + } + + public String getVersionId() { + switch (modLoaderType) { + case MOD_LOADER_FORGE: + return minecraftVersion+"-forge-"+modLoaderType; + case MOD_LOADER_FABRIC: + return "fabric-loader-"+modLoaderVersion+"-"+minecraftVersion; + case MOD_LOADER_QUILT: + // TODO + default: + return null; + } + } +} diff --git a/app_pojavlauncher/src/main/java/net/kdt/pojavlaunch/modloaders/modpacks/api/ModpackApi.java b/app_pojavlauncher/src/main/java/net/kdt/pojavlaunch/modloaders/modpacks/api/ModpackApi.java index 5443da46f..c47227e96 100644 --- a/app_pojavlauncher/src/main/java/net/kdt/pojavlaunch/modloaders/modpacks/api/ModpackApi.java +++ b/app_pojavlauncher/src/main/java/net/kdt/pojavlaunch/modloaders/modpacks/api/ModpackApi.java @@ -5,9 +5,6 @@ import net.kdt.pojavlaunch.PojavApplication; import net.kdt.pojavlaunch.modloaders.modpacks.models.ModDetail; import net.kdt.pojavlaunch.modloaders.modpacks.models.ModItem; import net.kdt.pojavlaunch.modloaders.modpacks.models.SearchFilters; -import net.kdt.pojavlaunch.progresskeeper.ProgressKeeper; - -import org.jetbrains.annotations.Nullable; /** * diff --git a/app_pojavlauncher/src/main/java/net/kdt/pojavlaunch/modloaders/modpacks/api/ModrinthApi.java b/app_pojavlauncher/src/main/java/net/kdt/pojavlaunch/modloaders/modpacks/api/ModrinthApi.java index 91b545534..04951f51a 100644 --- a/app_pojavlauncher/src/main/java/net/kdt/pojavlaunch/modloaders/modpacks/api/ModrinthApi.java +++ b/app_pojavlauncher/src/main/java/net/kdt/pojavlaunch/modloaders/modpacks/api/ModrinthApi.java @@ -1,18 +1,20 @@ package net.kdt.pojavlaunch.modloaders.modpacks.api; -import android.util.Log; - import com.google.gson.JsonArray; import com.google.gson.JsonObject; +import com.kdt.mcgui.ProgressLayout; +import net.kdt.pojavlaunch.R; import net.kdt.pojavlaunch.Tools; +import net.kdt.pojavlaunch.modloaders.modpacks.imagecache.ModIconCache; +import net.kdt.pojavlaunch.modloaders.modpacks.models.Constants; import net.kdt.pojavlaunch.modloaders.modpacks.models.ModDetail; import net.kdt.pojavlaunch.modloaders.modpacks.models.ModItem; import net.kdt.pojavlaunch.modloaders.modpacks.models.ModrinthIndex; -import net.kdt.pojavlaunch.modloaders.modpacks.models.Constants; import net.kdt.pojavlaunch.modloaders.modpacks.models.SearchFilters; +import net.kdt.pojavlaunch.progresskeeper.DownloaderProgressWrapper; import net.kdt.pojavlaunch.utils.DownloadUtils; -import net.kdt.pojavlaunch.utils.FileUtils; +import net.kdt.pojavlaunch.utils.ZipUtils; import net.kdt.pojavlaunch.value.launcherprofiles.LauncherProfiles; import net.kdt.pojavlaunch.value.launcherprofiles.MinecraftProfile; @@ -20,9 +22,11 @@ import java.io.File; import java.io.IOException; import java.util.HashMap; import java.util.Locale; +import java.util.Map; +import java.util.zip.ZipFile; -public class ModrinthApi implements ModpackApi { - private ApiHandler mApiHandler; +public class ModrinthApi implements ModpackApi{ + private final ApiHandler mApiHandler; public ModrinthApi(){ mApiHandler = new ApiHandler("https://api.modrinth.com/v2"); } @@ -68,7 +72,7 @@ public class ModrinthApi implements ModpackApi { JsonArray response = mApiHandler.get(String.format("project/%s/version", item.id), JsonArray.class); if(response == null) return null; - System.out.println(response.toString()); + System.out.println(response); String[] names = new String[response.size()]; String[] mcNames = new String[response.size()]; String[] urls = new String[response.size()]; @@ -87,70 +91,80 @@ public class ModrinthApi implements ModpackApi { } @Override - public void installMod(ModDetail modDetail, int selectedVersion){ + public void installMod(ModDetail modDetail, int selectedVersion) { //TODO considering only modpacks for now String versionUrl = modDetail.versionUrls[selectedVersion]; String modpackName = modDetail.title.toLowerCase(Locale.ROOT).trim().replace(" ", "_" ); // Build a new minecraft instance, folder first - File instanceFolder = new File(Tools.DIR_CACHE, modpackName); - instanceFolder.mkdirs(); // Get the mrpack File modpackFile = new File(Tools.DIR_CACHE, modpackName + ".mrpack"); + ModLoaderInfo modLoaderInfo; try { - DownloadUtils.downloadFile(versionUrl, modpackFile); - - FileUtils.uncompressZip(modpackFile, instanceFolder); - - // Get the index - ModrinthIndex index = Tools.GLOBAL_GSON.fromJson(Tools.read(instanceFolder.getAbsolutePath() + "/modrinth.index.json"), ModrinthIndex.class); - System.out.println(index); - // Download mods - for (ModrinthIndex.ModrinthIndexFile file : index.files){ - File destFile = new File(instanceFolder, file.path); - destFile.getParentFile().mkdirs(); - DownloadUtils.downloadFile(file.downloads[0], destFile); - } - - // Apply the overrides - for(String overrideName : new String[]{"overrides", "client-overrides"}) { - File overrideFolder = new File(instanceFolder, overrideName); - if(!overrideFolder.exists() || !overrideFolder.isDirectory()){ - continue; - } - for(File file : overrideFolder.listFiles()){ - // TODO what if overrides + client-overrides have collisions ? - org.apache.commons.io.FileUtils.moveToDirectory(file, instanceFolder, true); - } - overrideFolder.delete(); - } - // Remove server override as it is pointless - org.apache.commons.io.FileUtils.deleteDirectory(new File(instanceFolder, "server-overrides")); - - // Move the instance folder - org.apache.commons.io.FileUtils.moveDirectoryToDirectory(instanceFolder, new File(Tools.DIR_GAME_HOME, "custom_instances"), true); - + byte[] downloadBuffer = new byte[8192]; + DownloadUtils.downloadFileMonitored(versionUrl, modpackFile, downloadBuffer, + new DownloaderProgressWrapper(R.string.modpack_download_downloading_metadata, + ProgressLayout.INSTALL_MODPACK)); + ModrinthIndex modrinthIndex = installMrpack(modpackFile, new File(Tools.DIR_GAME_HOME, "custom_instances/"+modpackName)); + modLoaderInfo = createInfo(modrinthIndex); } catch (IOException e) { throw new RuntimeException(e); } finally { modpackFile.delete(); - try { - org.apache.commons.io.FileUtils.deleteDirectory(instanceFolder); - } catch (IOException e) { - Log.e(ModrinthApi.class.toString(), "Failed to cleanup cache instance folder, if any"); - } + ProgressLayout.clearProgress(ProgressLayout.INSTALL_MODPACK); + } + if(modLoaderInfo == null) { + return; } // Create the instance MinecraftProfile profile = new MinecraftProfile(); profile.gameDir = "./custom_instances/" + modpackName; - profile.name = modpackName; - //FIXME add the proper version ! - profile.lastVersionId = "1.7.10"; + profile.name = modDetail.title; + profile.lastVersionId = modLoaderInfo.getVersionId(); + profile.icon = ModIconCache.getBase64Image(modDetail.getIconCacheTag()); + LauncherProfiles.mainProfileJson.profiles.put(modpackName, profile); LauncherProfiles.update(); } + private static ModLoaderInfo createInfo(ModrinthIndex modrinthIndex) { + if(modrinthIndex == null) return null; + Map dependencies = modrinthIndex.dependencies; + String mcVersion = dependencies.get("minecraft"); + if(mcVersion == null) return null; + String modLoaderVersion; + if((modLoaderVersion = dependencies.get("forge")) != null) { + return new ModLoaderInfo(ModLoaderInfo.MOD_LOADER_FORGE, modLoaderVersion, mcVersion); + } + if((modLoaderVersion = dependencies.get("fabric-loader")) != null) { + return new ModLoaderInfo(ModLoaderInfo.MOD_LOADER_FABRIC, modLoaderVersion, mcVersion); + } + if((modLoaderVersion = dependencies.get("quilt-loader")) != null) { + throw new RuntimeException("Quilt is gay af"); + //return new ModLoaderInfo(ModLoaderInfo.MOD_LOADER_QUILT, modLoaderVersion, mcVersion); + } + return null; + } + + private ModrinthIndex installMrpack(File mrpackFile, File instanceDestination) throws IOException { + try (ZipFile modpackZipFile = new ZipFile(mrpackFile)){ + ModrinthIndex modrinthIndex = Tools.GLOBAL_GSON.fromJson( + Tools.read(ZipUtils.getEntryStream(modpackZipFile, "modrinth.index.json")), + ModrinthIndex.class); + + ModDownloader modDownloader = new ModDownloader(instanceDestination); + for(ModrinthIndex.ModrinthIndexFile indexFile : modrinthIndex.files) { + modDownloader.submitDownload(indexFile.fileSize, indexFile.path, indexFile.downloads); + } + modDownloader.awaitFinish(new DownloaderProgressWrapper(R.string.modpack_download_downloading_mods, ProgressLayout.INSTALL_MODPACK)); + ProgressLayout.setProgress(ProgressLayout.INSTALL_MODPACK, 0, R.string.modpack_download_applying_overrides, 1, 2); + ZipUtils.zipExtract(modpackZipFile, "overrides/", instanceDestination); + ProgressLayout.setProgress(ProgressLayout.INSTALL_MODPACK, 50, R.string.modpack_download_applying_overrides, 2, 2); + ZipUtils.zipExtract(modpackZipFile, "client-overrides/", instanceDestination); + return modrinthIndex; + } + } } diff --git a/app_pojavlauncher/src/main/java/net/kdt/pojavlaunch/modloaders/modpacks/imagecache/ModIconCache.java b/app_pojavlauncher/src/main/java/net/kdt/pojavlaunch/modloaders/modpacks/imagecache/ModIconCache.java index 6e06cbcb5..9efc94fff 100644 --- a/app_pojavlauncher/src/main/java/net/kdt/pojavlaunch/modloaders/modpacks/imagecache/ModIconCache.java +++ b/app_pojavlauncher/src/main/java/net/kdt/pojavlaunch/modloaders/modpacks/imagecache/ModIconCache.java @@ -1,10 +1,15 @@ package net.kdt.pojavlaunch.modloaders.modpacks.imagecache; +import android.util.Base64; import android.util.Log; import net.kdt.pojavlaunch.Tools; +import org.apache.commons.io.IOUtils; + import java.io.File; +import java.io.FileInputStream; +import java.io.IOException; import java.lang.ref.WeakReference; import java.util.ArrayList; import java.util.Iterator; @@ -70,4 +75,32 @@ public class ModIconCache { if(isCanceled) Log.i("IconCache", "checkCancelled("+imageReceiver.hashCode()+") == true"); return isCanceled; } + + /** + * Get the base64-encoded version of a cached icon by its tag. + * Note: this functions performs I/O operations, and should not be called on the UI + * thread. + * @param imageTag the icon tag + * @return the base64 encoded image or null if not cached + */ + + public static String getBase64Image(String imageTag) { + File imagePath = new File(Tools.DIR_CACHE, "mod_icons/"+imageTag+".ca"); + Log.i("IconCache", "Creating base64 version of icon "+imageTag); + if(!imagePath.canRead() || !imagePath.isFile()) { + Log.i("IconCache", "Icon does not exist"); + return null; + } + try { + try(FileInputStream fileInputStream = new FileInputStream(imagePath)) { + byte[] imageBytes = IOUtils.toByteArray(fileInputStream); + // reencode to png? who cares! our profile icon cache is an omnivore! + // if some other launcher parses this and dies it is not our problem :troll: + return "data:image/png;base64,"+ Base64.encodeToString(imageBytes, Base64.DEFAULT); + } + }catch (IOException e) { + e.printStackTrace(); + return null; + } + } } diff --git a/app_pojavlauncher/src/main/java/net/kdt/pojavlaunch/modloaders/modpacks/models/ModDetail.java b/app_pojavlauncher/src/main/java/net/kdt/pojavlaunch/modloaders/modpacks/models/ModDetail.java index 927d5fed5..da571b591 100644 --- a/app_pojavlauncher/src/main/java/net/kdt/pojavlaunch/modloaders/modpacks/models/ModDetail.java +++ b/app_pojavlauncher/src/main/java/net/kdt/pojavlaunch/modloaders/modpacks/models/ModDetail.java @@ -1,6 +1,8 @@ package net.kdt.pojavlaunch.modloaders.modpacks.models; +import androidx.annotation.NonNull; + import java.util.Arrays; public class ModDetail extends ModItem { @@ -15,6 +17,7 @@ public class ModDetail extends ModItem { this.versionUrls = versionUrls; } + @NonNull @Override public String toString() { return "ModDetail{" + diff --git a/app_pojavlauncher/src/main/java/net/kdt/pojavlaunch/modloaders/modpacks/models/ModItem.java b/app_pojavlauncher/src/main/java/net/kdt/pojavlaunch/modloaders/modpacks/models/ModItem.java index d446bb3ad..0ba5f2a89 100644 --- a/app_pojavlauncher/src/main/java/net/kdt/pojavlaunch/modloaders/modpacks/models/ModItem.java +++ b/app_pojavlauncher/src/main/java/net/kdt/pojavlaunch/modloaders/modpacks/models/ModItem.java @@ -1,5 +1,7 @@ package net.kdt.pojavlaunch.modloaders.modpacks.models; +import androidx.annotation.NonNull; + public class ModItem extends ModSource { public String id; @@ -16,6 +18,7 @@ public class ModItem extends ModSource { this.imageUrl = imageUrl; } + @NonNull @Override public String toString() { return "ModItem{" + @@ -27,4 +30,8 @@ public class ModItem extends ModSource { ", isModpack=" + isModpack + '}'; } + + public String getIconCacheTag() { + return apiSource+"_"+id; + } } diff --git a/app_pojavlauncher/src/main/java/net/kdt/pojavlaunch/modloaders/modpacks/models/ModrinthIndex.java b/app_pojavlauncher/src/main/java/net/kdt/pojavlaunch/modloaders/modpacks/models/ModrinthIndex.java index 32f759dbf..52ffca929 100644 --- a/app_pojavlauncher/src/main/java/net/kdt/pojavlaunch/modloaders/modpacks/models/ModrinthIndex.java +++ b/app_pojavlauncher/src/main/java/net/kdt/pojavlaunch/modloaders/modpacks/models/ModrinthIndex.java @@ -1,11 +1,12 @@ package net.kdt.pojavlaunch.modloaders.modpacks.models; -import com.google.gson.annotations.SerializedName; +import androidx.annotation.NonNull; import org.jetbrains.annotations.Nullable; import java.util.Arrays; +import java.util.Map; /** * POJO to represent the modrinth index inside mrpacks @@ -20,6 +21,7 @@ public class ModrinthIndex { public String summary; public ModrinthIndexFile[] files; + public Map dependencies; public static class ModrinthIndexFile { @@ -31,6 +33,7 @@ public class ModrinthIndex { @Nullable public ModrinthIndexFileEnv env; + @NonNull @Override public String toString() { return "ModrinthIndexFile{" + @@ -45,6 +48,7 @@ public class ModrinthIndex { public String sha1; public String sha512; + @NonNull @Override public String toString() { return "ModrinthIndexFileHashes{" + @@ -58,6 +62,7 @@ public class ModrinthIndex { public String client; public String server; + @NonNull @Override public String toString() { return "ModrinthIndexFileEnv{" + @@ -68,27 +73,7 @@ public class ModrinthIndex { } } - - public static class ModrinthIndexDependencies { - @Nullable public String minecraft; - @Nullable public String forge; - @SerializedName("fabric-loader") - @Nullable public String fabricLoader; - @SerializedName("quilt-loader") - @Nullable public String quiltLoader; - - @Override - public String toString() { - return "ModrinthIndexDependencies{" + - "minecraft='" + minecraft + '\'' + - ", forge='" + forge + '\'' + - ", fabricLoader='" + fabricLoader + '\'' + - ", quiltLoader='" + quiltLoader + '\'' + - '}'; - } - - } - + @NonNull @Override public String toString() { return "ModrinthIndex{" + diff --git a/app_pojavlauncher/src/main/java/net/kdt/pojavlaunch/progresskeeper/DownloaderProgressWrapper.java b/app_pojavlauncher/src/main/java/net/kdt/pojavlaunch/progresskeeper/DownloaderProgressWrapper.java new file mode 100644 index 000000000..bf2818186 --- /dev/null +++ b/app_pojavlauncher/src/main/java/net/kdt/pojavlaunch/progresskeeper/DownloaderProgressWrapper.java @@ -0,0 +1,40 @@ +package net.kdt.pojavlaunch.progresskeeper; + +import static net.kdt.pojavlaunch.Tools.BYTE_TO_MB; + +import net.kdt.pojavlaunch.Tools; + +public class DownloaderProgressWrapper implements Tools.DownloaderFeedback { + + private final int mProgressString; + private final String mProgressRecord; + public String extraString = null; + + /** + * A simple wrapper to send the downloader progress to ProgressKeeper + * @param progressString the string that will be used in the progress reporter + * @param progressRecord the record for ProgressKeeper + */ + public DownloaderProgressWrapper(int progressString, String progressRecord) { + this.mProgressString = progressString; + this.mProgressRecord = progressRecord; + } + + @Override + public void updateProgress(int curr, int max) { + Object[] va; + if(extraString != null) { + va = new Object[3]; + va[0] = extraString; + va[1] = curr/BYTE_TO_MB; + va[2] = max/BYTE_TO_MB; + } + else { + va = new Object[2]; + va[0] = curr/BYTE_TO_MB; + va[1] = max/BYTE_TO_MB; + } + // the allocations are fine because thats how java implements variadic arguments in bytecode: an array of whatever + ProgressKeeper.submitProgress(mProgressRecord, (int) Math.max((float)curr/max*100,0), mProgressString, va); + } +} diff --git a/app_pojavlauncher/src/main/java/net/kdt/pojavlaunch/tasks/AsyncMinecraftDownloader.java b/app_pojavlauncher/src/main/java/net/kdt/pojavlaunch/tasks/AsyncMinecraftDownloader.java index 74112fefd..4de6935c3 100644 --- a/app_pojavlauncher/src/main/java/net/kdt/pojavlaunch/tasks/AsyncMinecraftDownloader.java +++ b/app_pojavlauncher/src/main/java/net/kdt/pojavlaunch/tasks/AsyncMinecraftDownloader.java @@ -1,6 +1,7 @@ package net.kdt.pojavlaunch.tasks; import static net.kdt.pojavlaunch.PojavApplication.sExecutorService; +import static net.kdt.pojavlaunch.Tools.BYTE_TO_MB; import static net.kdt.pojavlaunch.utils.DownloadUtils.downloadFileMonitored; import android.app.Activity; @@ -40,7 +41,7 @@ import java.util.concurrent.atomic.AtomicBoolean; import java.util.concurrent.atomic.AtomicInteger; public class AsyncMinecraftDownloader { - private static final float BYTE_TO_MB = 1024 * 1024; + public static final String MINECRAFT_RES = "https://resources.download.minecraft.net/"; /* Allows each downloading thread to have its own RECYCLED buffer */ diff --git a/app_pojavlauncher/src/main/java/net/kdt/pojavlaunch/utils/ZipUtils.java b/app_pojavlauncher/src/main/java/net/kdt/pojavlaunch/utils/ZipUtils.java new file mode 100644 index 000000000..a56fd661b --- /dev/null +++ b/app_pojavlauncher/src/main/java/net/kdt/pojavlaunch/utils/ZipUtils.java @@ -0,0 +1,58 @@ +package net.kdt.pojavlaunch.utils; + +import org.apache.commons.io.IOUtils; + +import java.io.File; +import java.io.FileOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.util.Enumeration; +import java.util.zip.ZipEntry; +import java.util.zip.ZipFile; + +public class ZipUtils { + /** + * Gets an InputStream for a given ZIP entry, throwing an IOException if the ZIP entry does not + * exist. + * @param zipFile The ZipFile to get the entry from + * @param entryPath The full path inside of the ZipFile + * @return The InputStream provided by the ZipFile + * @throws IOException if the entry was not found + */ + public static InputStream getEntryStream(ZipFile zipFile, String entryPath) throws IOException{ + ZipEntry entry = zipFile.getEntry(entryPath); + if(entry == null) throw new IOException("No entry in ZIP file: "+entryPath); + return zipFile.getInputStream(entry); + } + + /** + * Extracts all files in a ZipFile inside of a given directory to a given destination directory + * How to specify dirName: + * If you want to extract all files in the ZipFile, specify "" + * If you want to extract a single directory, specify its full path followed by a trailing / + * @param zipFile The ZipFile to extract files from + * @param dirName The directory to extract the files from + * @param destination The destination directory to extract the files into + * @throws IOException if it was not possible to create a directory or file extraction failed + */ + public static void zipExtract(ZipFile zipFile, String dirName, File destination) throws IOException { + Enumeration zipEntries = zipFile.entries(); + + int dirNameLen = dirName.length(); + while(zipEntries.hasMoreElements()) { + ZipEntry zipEntry = zipEntries.nextElement(); + String entryName = zipEntry.getName(); + if(!entryName.startsWith(dirName) || zipEntry.isDirectory()) continue; + File zipDestination = new File(destination, entryName.substring(dirNameLen)); + File parent = zipDestination.getParentFile(); + if(parent != null && !parent.exists()) + if(!parent.mkdirs()) throw new IOException("Failed to create "+parent.getAbsolutePath()); + try (InputStream inputStream = zipFile.getInputStream(zipEntry); + OutputStream outputStream = + new FileOutputStream(zipDestination)) { + IOUtils.copy(inputStream, outputStream); + } + } + } +} diff --git a/app_pojavlauncher/src/main/res/layout/fragment_mod_search.xml b/app_pojavlauncher/src/main/res/layout/fragment_mod_search.xml index d57ebb069..cb70a72f8 100644 --- a/app_pojavlauncher/src/main/res/layout/fragment_mod_search.xml +++ b/app_pojavlauncher/src/main/res/layout/fragment_mod_search.xml @@ -77,4 +77,15 @@ app:layout_constraintTop_toBottomOf="@id/search_mod_selected_mc_version_textview" /> + + \ No newline at end of file diff --git a/app_pojavlauncher/src/main/res/values/strings.xml b/app_pojavlauncher/src/main/res/values/strings.xml index b8551b202..d9cfb92d6 100644 --- a/app_pojavlauncher/src/main/res/values/strings.xml +++ b/app_pojavlauncher/src/main/res/values/strings.xml @@ -410,5 +410,9 @@ Install - + No modpacks found + Failed to find modpacks + Downloading modpack metadata (%.2f MB / %.2f MB) + Downloading mods (%.2f MB / %.2f MB) + Applying overrides (%d/%d)