Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 6 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -36,8 +36,8 @@ Copper is a fork of [Amethyst](https://github.com/AngelAuraMC/Amethyst-Android)

## Introduction

* Copper is a Minecraft: Java Edition launcher for Android based on [Boardwalk](https://github.com/zhuowei/Boardwalk) and [PojavLauncher](https://github.com/PojavLauncherTeam/PojavLauncher)
* This launcher can launch almost all available Minecraft versions ranging from rd-132211 to 1.21 snapshots (including Combat Test versions)
* Copper is a Minecraft: Java Edition launcher for Android based on [Boardwalk](https://github.com/zhuowei/Boardwalk), [PojavLauncher](https://github.com/PojavLauncherTeam/PojavLauncher) and [Amethyst Launcher](https://github.com/AngelAuraMC/Amethyst-Android).
* This launcher can launch almost all available Minecraft versions ranging from rd-132211 to 26.x snapshots (including Combat Test versions)
* Modding via Forge and Fabric are also supported.
* This repository contains source code for Android.

Expand Down Expand Up @@ -87,7 +87,9 @@ If you need more control over the build process, follow these steps:

* [x] New UI
* [x] Bug Fixes
* [ ] Fix GL4ES and KW in older versions
* [x] Fix GL4ES and KW in older versions
* [x] Add Modpack export
* [x] Add mclo.gs
* [ ] Add More Renders


Expand Down Expand Up @@ -127,6 +129,7 @@ Copper is licensed under [GNU LGPLv3](https://github.com/CopperLauncher/Copper-A
* [SDL3](https://github.com/libsdl-org/SDL): [zlib License](https://github.com/libsdl-org/SDL/blob/main/LICENSE.txt)
* [sdl2-compat](https://github.com/libsdl-org/sdl2-compat): [zlib License](https://github.com/libsdl-org/sdl2-compat/blob/main/LICENSE.txt)
* Thanks to [MCHeads](https://mc-heads.net) for providing Minecraft avatars.
* Thanks to [Modrinth](https://api.modrinth.com/), [CurseForge](https://docs.curseforge.com/rest-api/) and [McLo.gs](https://api.mclo.gs) for providing us the free API's.

## Roadmap

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@
import net.kdt.pojavlaunch.fragments.MainMenuFragment;
import net.kdt.pojavlaunch.extra.ExtraConstants;
import net.kdt.pojavlaunch.extra.ExtraCore;
import net.kdt.pojavlaunch.modloaders.modpacks.ExportMrpackDialog;
import net.kdt.pojavlaunch.multirt.MultiRTUtils;
import net.kdt.pojavlaunch.multirt.RTSpinnerAdapter;
import net.kdt.pojavlaunch.multirt.Runtime;
Expand All @@ -53,7 +54,7 @@ public class ProfileEditorFragment extends Fragment implements CropperUtils.Crop
private String mProfileKey;
private MinecraftProfile mTempProfile = null;
private String mValueToConsume = "";
private Button mSaveButton, mDeleteButton, mControlSelectButton, mGameDirButton, mVersionSelectButton;
private Button mSaveButton, mDeleteButton, mControlSelectButton, mGameDirButton, mVersionSelectButton, mExportButton;
private Spinner mDefaultRuntime, mDefaultRenderer;
private EditText mDefaultName, mDefaultJvmArgument;
private TextView mDefaultPath, mDefaultVersion, mDefaultControl;
Expand Down Expand Up @@ -141,6 +142,8 @@ public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceStat
// Set up the icon change click listener
mProfileIcon.setOnClickListener(v -> CropperUtils.startCropper(mCropperLauncher));

mExportButton.setOnClickListener(v -> ExportMrpackDialog.show(requireActivity(), mTempProfile));

loadValues(LauncherPreferences.DEFAULT_PREF.getString(LauncherPreferences.PREF_KEY_CURRENT_PROFILE, ""), view.getContext());
}

Expand Down Expand Up @@ -262,6 +265,7 @@ private void bindViews(@NonNull View view){
mVersionSelectButton = view.findViewById(R.id.vprof_editor_version_button);
mGameDirButton = view.findViewById(R.id.vprof_editor_path_button);
mProfileIcon = view.findViewById(R.id.vprof_editor_profile_icon);
mExportButton = view.findViewById(R.id.vprof_editor_export_button);
}

private void save(){
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,207 @@
package net.kdt.pojavlaunch.modloaders.modpacks;

import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.CheckBox;
import android.widget.ImageView;
import android.widget.TextView;

import androidx.annotation.NonNull;
import androidx.recyclerview.widget.RecyclerView;

import net.kdt.pojavlaunch.R;

import java.io.File;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;
import java.util.Iterator;
import java.util.List;
import java.util.Map;

/**
* Renders an expandable, checkable file/folder tree for the .mrpack export dialog, the same
* way the official Modrinth app lets you drill into a folder and deselect individual files
* instead of just toggling the whole folder.
* <p/>
* Selection state lives in a single {@code Map<String, Boolean>} (path relative to the
* instance root -> explicit checked state) that is shared with {@code MrpackExporter}: a path
* with no explicit entry simply inherits the state of its nearest ancestor that has one. This
* means the export step doesn't need this adapter at all — it can recompute the same effective
* state by walking the same map.
*/
public class ExportFileTreeAdapter extends RecyclerView.Adapter<ExportFileTreeAdapter.RowHolder> {

private static class Node {
final File file;
final String relPath;
final boolean isDirectory;
final int depth;
boolean expanded = false;
List<Node> children;

Node(File file, String relPath, int depth) {
this.file = file;
this.relPath = relPath;
this.isDirectory = file.isDirectory();
this.depth = depth;
}
}

private final List<Node> mVisibleNodes = new ArrayList<>();
private final Map<String, Boolean> mOverrides;
private final int mIndentPx;

public ExportFileTreeAdapter(File instanceDir, Map<String, Boolean> overrides, int indentPx) {
mOverrides = overrides;
mIndentPx = indentPx;
mVisibleNodes.addAll(buildNodeList(instanceDir, "", 0));
}

private static List<Node> buildNodeList(File parent, String parentRelPath, int depth) {
File[] files = parent.listFiles();
List<Node> nodes = new ArrayList<>();
if (files == null) return nodes;

List<File> directories = new ArrayList<>();
List<File> plainFiles = new ArrayList<>();
for (File f : files) {
if (f.isDirectory()) directories.add(f);
else plainFiles.add(f);
}
Comparator<File> byNameIgnoreCase = (a, b) -> a.getName().compareToIgnoreCase(b.getName());
Collections.sort(directories, byNameIgnoreCase);
Collections.sort(plainFiles, byNameIgnoreCase);

List<File> ordered = new ArrayList<>(directories.size() + plainFiles.size());
ordered.addAll(directories);
ordered.addAll(plainFiles);

for (File f : ordered) {
String relPath = parentRelPath.isEmpty() ? f.getName() : parentRelPath + "/" + f.getName();
nodes.add(new Node(f, relPath, depth));
}
return nodes;
}

private boolean effectiveChecked(String relPath) {
String path = relPath;
while (true) {
Boolean explicit = mOverrides.get(path);
if (explicit != null) return explicit;
int lastSlash = path.lastIndexOf('/');
if (lastSlash < 0) return false;
path = path.substring(0, lastSlash);
}
}

private void setCheckedCascading(Node node, boolean checked) {
mOverrides.put(node.relPath, checked);
// Drop any explicit overrides belonging to already-known descendants so they go back
// to inheriting from this node, instead of conflicting with the new state.
String prefix = node.relPath + "/";
Iterator<String> keys = mOverrides.keySet().iterator();
while (keys.hasNext()) {
if (keys.next().startsWith(prefix)) keys.remove();
}
if (node.children != null) {
for (Node child : node.children) setCheckedCascading(child, checked);
}
}

@NonNull
@Override
public RowHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
View view = LayoutInflater.from(parent.getContext())
.inflate(R.layout.item_export_tree_entry, parent, false);
return new RowHolder(view);
}

@Override
public void onBindViewHolder(@NonNull RowHolder holder, int position) {
holder.bind(mVisibleNodes.get(position));
}

@Override
public int getItemCount() {
return mVisibleNodes.size();
}

private void toggleExpand(Node node) {
int index = mVisibleNodes.indexOf(node);
if (index < 0) return;

if (node.expanded) {
int removeCount = countVisibleDescendants(index, node.depth);
for (int i = 0; i < removeCount; i++) mVisibleNodes.remove(index + 1);
node.expanded = false;
notifyItemChanged(index);
if (removeCount > 0) notifyItemRangeRemoved(index + 1, removeCount);
} else {
if (node.children == null) node.children = buildNodeList(node.file, node.relPath, node.depth + 1);
mVisibleNodes.addAll(index + 1, node.children);
node.expanded = true;
notifyItemChanged(index);
if (!node.children.isEmpty()) notifyItemRangeInserted(index + 1, node.children.size());
}
}

private int countVisibleDescendants(int index, int depth) {
int count = 0;
for (int i = index + 1; i < mVisibleNodes.size(); i++) {
if (mVisibleNodes.get(i).depth <= depth) break;
count++;
}
return count;
}

private void refreshVisibleDescendants(Node node) {
int index = mVisibleNodes.indexOf(node);
if (index < 0) return;
int count = countVisibleDescendants(index, node.depth);
if (count > 0) notifyItemRangeChanged(index + 1, count);
}

class RowHolder extends RecyclerView.ViewHolder {
final View indentSpacer;
final CheckBox checkBox;
final TextView nameView;
final ImageView chevron;

RowHolder(@NonNull View itemView) {
super(itemView);
indentSpacer = itemView.findViewById(R.id.export_entry_indent);
checkBox = itemView.findViewById(R.id.export_entry_checkbox);
nameView = itemView.findViewById(R.id.export_entry_name);
chevron = itemView.findViewById(R.id.export_entry_chevron);
}

void bind(Node node) {
ViewGroup.LayoutParams lp = indentSpacer.getLayoutParams();
lp.width = node.depth * mIndentPx;
indentSpacer.setLayoutParams(lp);

nameView.setText(node.isDirectory ? node.file.getName() + "/" : node.file.getName());

checkBox.setOnCheckedChangeListener(null);
checkBox.setChecked(effectiveChecked(node.relPath));
checkBox.setOnCheckedChangeListener((buttonView, isChecked) -> {
setCheckedCascading(node, isChecked);
refreshVisibleDescendants(node);
});

if (node.isDirectory) {
chevron.setVisibility(View.VISIBLE);
chevron.setRotation(node.expanded ? 0f : 180f);
View.OnClickListener expandListener = v -> toggleExpand(node);
chevron.setOnClickListener(expandListener);
itemView.setOnClickListener(expandListener);
} else {
chevron.setVisibility(View.INVISIBLE);
chevron.setOnClickListener(null);
itemView.setOnClickListener(null);
}
}
}
}
Loading
Loading