Skip to content

Commit 1bbebcc

Browse files
committed
feat: add mrpack export to edit instance tab
lets you export an instance as a .mrpack the same way the modrinth app does - pick which folders/files get included (defaults match modrinth: config/mods/resourcepacks/shaderpacks checked, rest unchecked), hash mods against modrinth so matched ones get referenced by url instead of bundled, everything else goes under overrides/. exported file lands in games/.../exports/ and opens the share sheet right after.
1 parent c8ef59e commit 1bbebcc

11 files changed

Lines changed: 1061 additions & 2 deletions

File tree

app_pojavlauncher/src/main/java/net/kdt/pojavlaunch/fragments/ProfileEditorFragment.java

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@
2727
import net.kdt.pojavlaunch.fragments.MainMenuFragment;
2828
import net.kdt.pojavlaunch.extra.ExtraConstants;
2929
import net.kdt.pojavlaunch.extra.ExtraCore;
30+
import net.kdt.pojavlaunch.modloaders.modpacks.ExportMrpackDialog;
3031
import net.kdt.pojavlaunch.multirt.MultiRTUtils;
3132
import net.kdt.pojavlaunch.multirt.RTSpinnerAdapter;
3233
import net.kdt.pojavlaunch.multirt.Runtime;
@@ -53,7 +54,7 @@ public class ProfileEditorFragment extends Fragment implements CropperUtils.Crop
5354
private String mProfileKey;
5455
private MinecraftProfile mTempProfile = null;
5556
private String mValueToConsume = "";
56-
private Button mSaveButton, mDeleteButton, mControlSelectButton, mGameDirButton, mVersionSelectButton;
57+
private Button mSaveButton, mDeleteButton, mControlSelectButton, mGameDirButton, mVersionSelectButton, mExportButton;
5758
private Spinner mDefaultRuntime, mDefaultRenderer;
5859
private EditText mDefaultName, mDefaultJvmArgument;
5960
private TextView mDefaultPath, mDefaultVersion, mDefaultControl;
@@ -141,6 +142,8 @@ public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceStat
141142
// Set up the icon change click listener
142143
mProfileIcon.setOnClickListener(v -> CropperUtils.startCropper(mCropperLauncher));
143144

145+
mExportButton.setOnClickListener(v -> ExportMrpackDialog.show(requireActivity(), mTempProfile));
146+
144147
loadValues(LauncherPreferences.DEFAULT_PREF.getString(LauncherPreferences.PREF_KEY_CURRENT_PROFILE, ""), view.getContext());
145148
}
146149

@@ -262,6 +265,7 @@ private void bindViews(@NonNull View view){
262265
mVersionSelectButton = view.findViewById(R.id.vprof_editor_version_button);
263266
mGameDirButton = view.findViewById(R.id.vprof_editor_path_button);
264267
mProfileIcon = view.findViewById(R.id.vprof_editor_profile_icon);
268+
mExportButton = view.findViewById(R.id.vprof_editor_export_button);
265269
}
266270

267271
private void save(){
Lines changed: 207 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,207 @@
1+
package net.kdt.pojavlaunch.modloaders.modpacks;
2+
3+
import android.view.LayoutInflater;
4+
import android.view.View;
5+
import android.view.ViewGroup;
6+
import android.widget.CheckBox;
7+
import android.widget.ImageView;
8+
import android.widget.TextView;
9+
10+
import androidx.annotation.NonNull;
11+
import androidx.recyclerview.widget.RecyclerView;
12+
13+
import net.kdt.pojavlaunch.R;
14+
15+
import java.io.File;
16+
import java.util.ArrayList;
17+
import java.util.Collections;
18+
import java.util.Comparator;
19+
import java.util.Iterator;
20+
import java.util.List;
21+
import java.util.Map;
22+
23+
/**
24+
* Renders an expandable, checkable file/folder tree for the .mrpack export dialog, the same
25+
* way the official Modrinth app lets you drill into a folder and deselect individual files
26+
* instead of just toggling the whole folder.
27+
* <p/>
28+
* Selection state lives in a single {@code Map<String, Boolean>} (path relative to the
29+
* instance root -> explicit checked state) that is shared with {@code MrpackExporter}: a path
30+
* with no explicit entry simply inherits the state of its nearest ancestor that has one. This
31+
* means the export step doesn't need this adapter at all — it can recompute the same effective
32+
* state by walking the same map.
33+
*/
34+
public class ExportFileTreeAdapter extends RecyclerView.Adapter<ExportFileTreeAdapter.RowHolder> {
35+
36+
private static class Node {
37+
final File file;
38+
final String relPath;
39+
final boolean isDirectory;
40+
final int depth;
41+
boolean expanded = false;
42+
List<Node> children;
43+
44+
Node(File file, String relPath, int depth) {
45+
this.file = file;
46+
this.relPath = relPath;
47+
this.isDirectory = file.isDirectory();
48+
this.depth = depth;
49+
}
50+
}
51+
52+
private final List<Node> mVisibleNodes = new ArrayList<>();
53+
private final Map<String, Boolean> mOverrides;
54+
private final int mIndentPx;
55+
56+
public ExportFileTreeAdapter(File instanceDir, Map<String, Boolean> overrides, int indentPx) {
57+
mOverrides = overrides;
58+
mIndentPx = indentPx;
59+
mVisibleNodes.addAll(buildNodeList(instanceDir, "", 0));
60+
}
61+
62+
private static List<Node> buildNodeList(File parent, String parentRelPath, int depth) {
63+
File[] files = parent.listFiles();
64+
List<Node> nodes = new ArrayList<>();
65+
if (files == null) return nodes;
66+
67+
List<File> directories = new ArrayList<>();
68+
List<File> plainFiles = new ArrayList<>();
69+
for (File f : files) {
70+
if (f.isDirectory()) directories.add(f);
71+
else plainFiles.add(f);
72+
}
73+
Comparator<File> byNameIgnoreCase = (a, b) -> a.getName().compareToIgnoreCase(b.getName());
74+
Collections.sort(directories, byNameIgnoreCase);
75+
Collections.sort(plainFiles, byNameIgnoreCase);
76+
77+
List<File> ordered = new ArrayList<>(directories.size() + plainFiles.size());
78+
ordered.addAll(directories);
79+
ordered.addAll(plainFiles);
80+
81+
for (File f : ordered) {
82+
String relPath = parentRelPath.isEmpty() ? f.getName() : parentRelPath + "/" + f.getName();
83+
nodes.add(new Node(f, relPath, depth));
84+
}
85+
return nodes;
86+
}
87+
88+
private boolean effectiveChecked(String relPath) {
89+
String path = relPath;
90+
while (true) {
91+
Boolean explicit = mOverrides.get(path);
92+
if (explicit != null) return explicit;
93+
int lastSlash = path.lastIndexOf('/');
94+
if (lastSlash < 0) return false;
95+
path = path.substring(0, lastSlash);
96+
}
97+
}
98+
99+
private void setCheckedCascading(Node node, boolean checked) {
100+
mOverrides.put(node.relPath, checked);
101+
// Drop any explicit overrides belonging to already-known descendants so they go back
102+
// to inheriting from this node, instead of conflicting with the new state.
103+
String prefix = node.relPath + "/";
104+
Iterator<String> keys = mOverrides.keySet().iterator();
105+
while (keys.hasNext()) {
106+
if (keys.next().startsWith(prefix)) keys.remove();
107+
}
108+
if (node.children != null) {
109+
for (Node child : node.children) setCheckedCascading(child, checked);
110+
}
111+
}
112+
113+
@NonNull
114+
@Override
115+
public RowHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
116+
View view = LayoutInflater.from(parent.getContext())
117+
.inflate(R.layout.item_export_tree_entry, parent, false);
118+
return new RowHolder(view);
119+
}
120+
121+
@Override
122+
public void onBindViewHolder(@NonNull RowHolder holder, int position) {
123+
holder.bind(mVisibleNodes.get(position));
124+
}
125+
126+
@Override
127+
public int getItemCount() {
128+
return mVisibleNodes.size();
129+
}
130+
131+
private void toggleExpand(Node node) {
132+
int index = mVisibleNodes.indexOf(node);
133+
if (index < 0) return;
134+
135+
if (node.expanded) {
136+
int removeCount = countVisibleDescendants(index, node.depth);
137+
for (int i = 0; i < removeCount; i++) mVisibleNodes.remove(index + 1);
138+
node.expanded = false;
139+
notifyItemChanged(index);
140+
if (removeCount > 0) notifyItemRangeRemoved(index + 1, removeCount);
141+
} else {
142+
if (node.children == null) node.children = buildNodeList(node.file, node.relPath, node.depth + 1);
143+
mVisibleNodes.addAll(index + 1, node.children);
144+
node.expanded = true;
145+
notifyItemChanged(index);
146+
if (!node.children.isEmpty()) notifyItemRangeInserted(index + 1, node.children.size());
147+
}
148+
}
149+
150+
private int countVisibleDescendants(int index, int depth) {
151+
int count = 0;
152+
for (int i = index + 1; i < mVisibleNodes.size(); i++) {
153+
if (mVisibleNodes.get(i).depth <= depth) break;
154+
count++;
155+
}
156+
return count;
157+
}
158+
159+
private void refreshVisibleDescendants(Node node) {
160+
int index = mVisibleNodes.indexOf(node);
161+
if (index < 0) return;
162+
int count = countVisibleDescendants(index, node.depth);
163+
if (count > 0) notifyItemRangeChanged(index + 1, count);
164+
}
165+
166+
class RowHolder extends RecyclerView.ViewHolder {
167+
final View indentSpacer;
168+
final CheckBox checkBox;
169+
final TextView nameView;
170+
final ImageView chevron;
171+
172+
RowHolder(@NonNull View itemView) {
173+
super(itemView);
174+
indentSpacer = itemView.findViewById(R.id.export_entry_indent);
175+
checkBox = itemView.findViewById(R.id.export_entry_checkbox);
176+
nameView = itemView.findViewById(R.id.export_entry_name);
177+
chevron = itemView.findViewById(R.id.export_entry_chevron);
178+
}
179+
180+
void bind(Node node) {
181+
ViewGroup.LayoutParams lp = indentSpacer.getLayoutParams();
182+
lp.width = node.depth * mIndentPx;
183+
indentSpacer.setLayoutParams(lp);
184+
185+
nameView.setText(node.isDirectory ? node.file.getName() + "/" : node.file.getName());
186+
187+
checkBox.setOnCheckedChangeListener(null);
188+
checkBox.setChecked(effectiveChecked(node.relPath));
189+
checkBox.setOnCheckedChangeListener((buttonView, isChecked) -> {
190+
setCheckedCascading(node, isChecked);
191+
refreshVisibleDescendants(node);
192+
});
193+
194+
if (node.isDirectory) {
195+
chevron.setVisibility(View.VISIBLE);
196+
chevron.setRotation(node.expanded ? 0f : 180f);
197+
View.OnClickListener expandListener = v -> toggleExpand(node);
198+
chevron.setOnClickListener(expandListener);
199+
itemView.setOnClickListener(expandListener);
200+
} else {
201+
chevron.setVisibility(View.INVISIBLE);
202+
chevron.setOnClickListener(null);
203+
itemView.setOnClickListener(null);
204+
}
205+
}
206+
}
207+
}

0 commit comments

Comments
 (0)