version list page

This commit is contained in:
Tungstend 2022-11-09 23:08:37 +08:00
parent e09ff00da5
commit 384c7fdb64
23 changed files with 1093 additions and 12 deletions

View File

@ -258,8 +258,6 @@ public class FCLGameRepository extends DefaultGameRepository {
if (id == null || !isLoaded())
return FCLPath.CONTEXT.getDrawable(R.drawable.img_grass);
VersionSetting vs = getLocalVersionSettingOrCreate(id);
Version version = getVersion(id).resolve(this);
File iconFile = getVersionIconFile(id);
if (iconFile.exists())

View File

@ -7,6 +7,7 @@ import com.google.gson.annotations.JsonAdapter;
import com.tungsten.fcl.game.FCLCacheRepository;
import com.tungsten.fcl.game.FCLGameRepository;
import com.tungsten.fcl.util.WeakListenerHolder;
import com.tungsten.fclauncher.FCLPath;
import com.tungsten.fclcore.download.DefaultDependencyManager;
import com.tungsten.fclcore.download.DownloadProvider;
import com.tungsten.fclcore.event.EventBus;
@ -86,7 +87,7 @@ public final class Profile implements Observable {
}
public Profile(String name) {
this(name, new File(".minecraft"));
this(name, new File(FCLPath.SHARED_COMMON_DIR));
}
public Profile(String name, File initialGameDir) {

View File

@ -0,0 +1,75 @@
package com.tungsten.fcl.ui.version;
import android.app.Activity;
import android.content.Context;
import android.view.View;
import android.widget.Toast;
import androidx.annotation.NonNull;
import com.tungsten.fcl.R;
import com.tungsten.fcl.setting.Profile;
import com.tungsten.fcl.setting.Profiles;
import com.tungsten.fcl.ui.UIManager;
import com.tungsten.fcl.util.RequestCodes;
import com.tungsten.fclcore.util.StringUtils;
import com.tungsten.fcllibrary.browser.FileBrowser;
import com.tungsten.fcllibrary.browser.options.LibMode;
import com.tungsten.fcllibrary.component.dialog.FCLDialog;
import com.tungsten.fcllibrary.component.view.FCLButton;
import com.tungsten.fcllibrary.component.view.FCLEditText;
import com.tungsten.fcllibrary.component.view.FCLImageButton;
import com.tungsten.fcllibrary.component.view.FCLTextView;
import java.io.File;
import java.util.ArrayList;
public class AddProfileDialog extends FCLDialog implements View.OnClickListener {
private FCLEditText editText;
private FCLTextView pathText;
private FCLImageButton editPath;
private FCLButton positive;
private FCLButton negative;
public AddProfileDialog(@NonNull Context context) {
super(context);
setContentView(R.layout.dialog_add_profile);
setCancelable(false);
editText = findViewById(R.id.name);
pathText = findViewById(R.id.path);
editPath = findViewById(R.id.edit);
positive = findViewById(R.id.positive);
negative = findViewById(R.id.negative);
editPath.setOnClickListener(this);
positive.setOnClickListener(this);
negative.setOnClickListener(this);
}
@Override
public void onClick(View view) {
if (view == editPath) {
FileBrowser.Builder builder = new FileBrowser.Builder(getContext());
builder.setLibMode(LibMode.FOLDER_CHOOSER);
builder.setTitle(getContext().getString(R.string.profile_select));
builder.create().browse(UIManager.getInstance().getVersionUI().getActivity(), RequestCodes.SELECT_PROFILE_CODE, (requestCode, resultCode, data) -> {
if (requestCode == RequestCodes.SELECT_PROFILE_CODE && resultCode == Activity.RESULT_OK && data != null) {
ArrayList<String> strings = FileBrowser.getSelectedFiles(data);
pathText.setText(strings.get(0));
}
});
}
if (view == positive) {
if (StringUtils.isBlank(editText.getText().toString()) || StringUtils.isBlank(pathText.getText().toString())) {
Toast.makeText(getContext(), getContext().getString(R.string.profile_add_alert), Toast.LENGTH_SHORT).show();
} else {
Profiles.getProfiles().add(new Profile(editText.getText().toString(), new File(pathText.getText().toString())));
UIManager.getInstance().getVersionUI().refreshProfile();
dismiss();
}
}
if (view == negative) {
dismiss();
}
}
}

View File

@ -0,0 +1,75 @@
package com.tungsten.fcl.ui.version;
import android.content.Context;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import com.tungsten.fcl.R;
import com.tungsten.fcl.setting.Profile;
import com.tungsten.fcl.setting.Profiles;
import com.tungsten.fcl.ui.UIManager;
import com.tungsten.fclcore.fakefx.collections.ObservableList;
import com.tungsten.fcllibrary.component.FCLAdapter;
import com.tungsten.fcllibrary.component.view.FCLImageButton;
import com.tungsten.fcllibrary.component.view.FCLRadioButton;
import com.tungsten.fcllibrary.component.view.FCLTextView;
public class ProfileListAdapter extends FCLAdapter {
private ObservableList<Profile> list;
public ProfileListAdapter(Context context, ObservableList<Profile> list) {
super(context);
this.list = list;
}
static class ViewHolder {
FCLRadioButton radioButton;
FCLTextView name;
FCLTextView path;
FCLImageButton delete;
}
@Override
public int getCount() {
return list.size();
}
@Override
public Object getItem(int i) {
return list.get(i);
}
@Override
public View getView(int i, View view, ViewGroup viewGroup) {
final ViewHolder viewHolder;
if (view == null) {
viewHolder = new ViewHolder();
view = LayoutInflater.from(getContext()).inflate(R.layout.item_profile, null);
viewHolder.radioButton = view.findViewById(R.id.radio);
viewHolder.name = view.findViewById(R.id.name);
viewHolder.path = view.findViewById(R.id.path);
viewHolder.delete = view.findViewById(R.id.delete);
view.setTag(viewHolder);
}
else {
viewHolder = (ViewHolder) view.getTag();
}
Profile profile = list.get(i);
viewHolder.radioButton.setChecked(profile == Profiles.getSelectedProfile());
viewHolder.name.setText(profile.getName());
viewHolder.path.setText(profile.getGameDir().getAbsolutePath());
viewHolder.radioButton.setOnClickListener(view1 -> {
Profiles.setSelectedProfile(profile);
notifyDataSetChanged();
UIManager.getInstance().getVersionUI().refresh();
});
viewHolder.delete.setOnClickListener(view1 -> {
Profiles.getProfiles().remove(profile);
UIManager.getInstance().getVersionUI().refresh();
UIManager.getInstance().getVersionUI().refreshProfile();
});
return view;
}
}

View File

@ -0,0 +1,63 @@
package com.tungsten.fcl.ui.version;
import android.content.Context;
import android.view.View;
import android.widget.Toast;
import androidx.annotation.NonNull;
import com.tungsten.fcl.R;
import com.tungsten.fclcore.util.FutureCallback;
import com.tungsten.fcllibrary.component.dialog.FCLDialog;
import com.tungsten.fcllibrary.component.view.FCLButton;
import com.tungsten.fcllibrary.component.view.FCLEditText;
import java.util.concurrent.CompletableFuture;
public class RenameVersionDialog extends FCLDialog implements View.OnClickListener {
private final FutureCallback<String> callback;
private final CompletableFuture<String> future = new CompletableFuture<>();
private FCLEditText editText;
private FCLButton positive;
private FCLButton negative;
public RenameVersionDialog(@NonNull Context context, String oldName, FutureCallback<String> callback) {
super(context);
setContentView(R.layout.dialog_rename_version);
setCancelable(false);
this.callback = callback;
editText = findViewById(R.id.new_name);
positive = findViewById(R.id.positive);
negative = findViewById(R.id.negative);
positive.setOnClickListener(this);
negative.setOnClickListener(this);
editText.setText(oldName);
}
@Override
public void onClick(View view) {
if (view == positive) {
positive.setEnabled(false);
negative.setEnabled(false);
callback.call(editText.getText().toString(), () -> {
positive.setEnabled(true);
negative.setEnabled(true);
future.complete(editText.getText().toString());
dismiss();
}, msg -> {
positive.setEnabled(true);
negative.setEnabled(true);
Toast.makeText(getContext(), msg, Toast.LENGTH_SHORT).show();
});
}
if (view == negative) {
dismiss();
}
}
public CompletableFuture<String> getFuture() {
return future;
}
}

View File

@ -0,0 +1,94 @@
package com.tungsten.fcl.ui.version;
import static com.tungsten.fclcore.util.Lang.threadPool;
import android.content.Context;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import androidx.appcompat.widget.AppCompatImageView;
import com.tungsten.fcl.R;
import com.tungsten.fcl.ui.UIManager;
import com.tungsten.fcllibrary.component.FCLAdapter;
import com.tungsten.fcllibrary.component.view.FCLImageButton;
import com.tungsten.fcllibrary.component.view.FCLRadioButton;
import com.tungsten.fcllibrary.component.view.FCLTextView;
import java.util.ArrayList;
public class VersionListAdapter extends FCLAdapter {
private final ArrayList<VersionListItem> list;
public VersionListAdapter(Context context, ArrayList<VersionListItem> list) {
super(context);
this.list = list;
}
static class ViewHolder {
FCLRadioButton radioButton;
AppCompatImageView icon;
FCLTextView title;
FCLTextView subtitle;
FCLImageButton rename;
FCLImageButton copy;
FCLImageButton browse;
FCLImageButton delete;
}
@Override
public int getCount() {
return list.size();
}
@Override
public Object getItem(int i) {
return list.get(i);
}
@Override
public View getView(int i, View view, ViewGroup viewGroup) {
final ViewHolder viewHolder;
if (view == null) {
viewHolder = new ViewHolder();
view = LayoutInflater.from(getContext()).inflate(R.layout.item_version, null);
viewHolder.radioButton = view.findViewById(R.id.radio);
viewHolder.icon = view.findViewById(R.id.icon);
viewHolder.title = view.findViewById(R.id.title);
viewHolder.subtitle = view.findViewById(R.id.subtitle);
viewHolder.rename = view.findViewById(R.id.rename);
viewHolder.copy = view.findViewById(R.id.copy);
viewHolder.browse = view.findViewById(R.id.browse);
viewHolder.delete = view.findViewById(R.id.delete);
view.setTag(viewHolder);
}
else {
viewHolder = (ViewHolder) view.getTag();
}
VersionListItem versionListItem = list.get(i);
versionListItem.checkSelection();
viewHolder.radioButton.setChecked(versionListItem.selectedProperty().get());
viewHolder.icon.setBackground(versionListItem.getDrawable());
viewHolder.title.setText(versionListItem.getVersion());
viewHolder.subtitle.setText(versionListItem.getLibraries());
viewHolder.radioButton.setOnClickListener(view1 -> {
versionListItem.getProfile().setSelectedVersion(versionListItem.getVersion());
notifyDataSetChanged();
});
viewHolder.rename.setOnClickListener(view1 -> {
Versions.renameVersion(getContext(), versionListItem.getProfile(), versionListItem.getVersion());
});
viewHolder.copy.setOnClickListener(view1 -> {
});
viewHolder.browse.setOnClickListener(view1 -> {
Versions.openFolder(UIManager.getInstance().getVersionUI().getActivity(), versionListItem.getProfile(), versionListItem.getVersion());
});
viewHolder.delete.setOnClickListener(view1 -> {
Versions.deleteVersion(getContext(), versionListItem.getProfile(), versionListItem.getVersion());
});
return view;
}
}

View File

@ -0,0 +1,56 @@
package com.tungsten.fcl.ui.version;
import android.graphics.drawable.Drawable;
import com.tungsten.fcl.setting.Profile;
import com.tungsten.fclcore.fakefx.beans.property.BooleanProperty;
import com.tungsten.fclcore.fakefx.beans.property.SimpleBooleanProperty;
public class VersionListItem {
private final Profile profile;
private final String version;
private final boolean isModpack;
private final BooleanProperty selected = new SimpleBooleanProperty();
private final String libraries;
private final Drawable drawable;
public VersionListItem(Profile profile, String id, String libraries, Drawable drawable) {
this.profile = profile;
this.version = id;
this.libraries = libraries;
this.drawable = drawable;
this.isModpack = profile.getRepository().isModpack(id);
selected.set(id.equals(profile.getSelectedVersion()));
}
public Profile getProfile() {
return profile;
}
public String getVersion() {
return version;
}
public String getLibraries() {
return libraries;
}
public Drawable getDrawable() {
return drawable;
}
public BooleanProperty selectedProperty() {
return selected;
}
public void checkSelection() {
selected.set(version.equals(profile.getSelectedVersion()));
}
public boolean canUpdate() {
return isModpack;
}
}

View File

@ -1,17 +1,97 @@
package com.tungsten.fcl.ui.version;
import android.content.Context;
import static com.tungsten.fclcore.download.LibraryAnalyzer.LibraryType.MINECRAFT;
import android.content.Context;
import android.view.View;
import android.widget.ListView;
import com.tungsten.fcl.R;
import com.tungsten.fcl.setting.Profiles;
import com.tungsten.fclcore.download.LibraryAnalyzer;
import com.tungsten.fclcore.task.Schedulers;
import com.tungsten.fcllibrary.component.ui.FCLCommonUI;
import com.tungsten.fcllibrary.component.view.FCLButton;
import com.tungsten.fcllibrary.component.view.FCLProgressBar;
import com.tungsten.fcllibrary.component.view.FCLUILayout;
public class VersionUI extends FCLCommonUI {
import java.util.ArrayList;
import java.util.stream.Collectors;
public class VersionUI extends FCLCommonUI implements View.OnClickListener {
private FCLButton refresh;
private FCLButton newProfile;
private FCLProgressBar progressBar;
private ListView profileListView;
private ListView versionListView;
public VersionUI(Context context, FCLUILayout parent, int id) {
super(context, parent, id);
}
@Override
public void refresh() {
public void onCreate() {
super.onCreate();
refresh = findViewById(R.id.refresh);
newProfile = findViewById(R.id.new_profile);
progressBar = findViewById(R.id.progress);
profileListView = findViewById(R.id.profile_list);
versionListView = findViewById(R.id.version_list);
refresh.setOnClickListener(this);
newProfile.setOnClickListener(this);
refresh();
}
@Override
public void refresh() {
refresh.setEnabled(false);
versionListView.setVisibility(View.GONE);
progressBar.setVisibility(View.VISIBLE);
refreshProfile();
Profiles.getSelectedProfile().getRepository().refreshVersionsAsync().whenComplete(Schedulers.androidUIThread(), exception -> {
ArrayList<VersionListItem> children = (ArrayList<VersionListItem>) Profiles.getSelectedProfile().getRepository().getDisplayVersions()
.map(version -> {
String game = Profiles.getSelectedProfile().getRepository().getGameVersion(version.getId()).orElse(getContext().getString(R.string.message_unknown));
StringBuilder libraries = new StringBuilder(game);
LibraryAnalyzer analyzer = LibraryAnalyzer.analyze(Profiles.getSelectedProfile().getRepository().getResolvedPreservingPatchesVersion(version.getId()));
for (LibraryAnalyzer.LibraryMark mark : analyzer) {
String libraryId = mark.getLibraryId();
String libraryVersion = mark.getLibraryVersion();
if (libraryId.equals(MINECRAFT.getPatchId())) continue;
int resId = getContext().getResources().getIdentifier("install_installer_" + libraryId.replace("-", "_"), "string", getContext().getPackageName());
if (resId != 0 && getContext().getString(resId) != null) {
libraries.append(", ").append(getContext().getString(resId));
if (libraryVersion != null)
libraries.append(": ").append(libraryVersion.replaceAll("(?i)" + libraryId, ""));
}
}
return new VersionListItem(Profiles.getSelectedProfile(), version.getId(), libraries.toString(), Profiles.getSelectedProfile().getRepository().getVersionIconImage(version.getId()));
})
.collect(Collectors.toList());
VersionListAdapter adapter = new VersionListAdapter(getContext(), children);
versionListView.setAdapter(adapter);
refresh.setEnabled(true);
versionListView.setVisibility(View.VISIBLE);
progressBar.setVisibility(View.GONE);
}).start();
}
public void refreshProfile() {
ProfileListAdapter adapter = new ProfileListAdapter(getContext(), Profiles.getProfiles());
profileListView.setAdapter(adapter);
}
@Override
public void onClick(View view) {
if (view == refresh) {
refresh();
}
if (view == newProfile) {
AddProfileDialog dialog = new AddProfileDialog(getContext());
dialog.show();
}
}
}

View File

@ -0,0 +1,124 @@
package com.tungsten.fcl.ui.version;
import android.app.Activity;
import android.content.Context;
import com.tungsten.fcl.R;
import com.tungsten.fcl.setting.Profile;
import com.tungsten.fcl.ui.UIManager;
import com.tungsten.fcl.util.RequestCodes;
import com.tungsten.fclcore.download.game.GameAssetDownloadTask;
import com.tungsten.fclcore.game.GameDirectoryType;
import com.tungsten.fclcore.task.Schedulers;
import com.tungsten.fclcore.task.Task;
import com.tungsten.fclcore.task.TaskExecutor;
import com.tungsten.fclcore.util.Logging;
import com.tungsten.fclcore.util.StringUtils;
import com.tungsten.fclcore.util.platform.OperatingSystem;
import com.tungsten.fcllibrary.browser.FileBrowser;
import com.tungsten.fcllibrary.browser.options.LibMode;
import com.tungsten.fcllibrary.component.dialog.FCLAlertDialog;
import java.io.IOException;
import java.util.concurrent.CompletableFuture;
import java.util.logging.Level;
public class Versions {
public static void deleteVersion(Context context, Profile profile, String version) {
boolean isIndependent = profile.getVersionSetting(version).getGameDirType() == GameDirectoryType.VERSION_FOLDER;
String message = isIndependent ? String.format(context.getString(R.string.version_manage_remove_confirm_independent), version) : String.format(context.getString(R.string.version_manage_remove_confirm), version);
FCLAlertDialog.Builder builder = new FCLAlertDialog.Builder(context);
builder.setAlertLevel(FCLAlertDialog.AlertLevel.ALERT);
builder.setMessage(message);
builder.setPositiveButton(() -> {
profile.getRepository().removeVersionFromDisk(version);
UIManager.getInstance().getVersionUI().refresh();
});
builder.setNegativeButton(null);
builder.create().show();
}
public static CompletableFuture<String> renameVersion(Context context, Profile profile, String version) {
RenameVersionDialog dialog = new RenameVersionDialog(context, version, (newName, resolve, reject) -> {
if (!OperatingSystem.isNameValid(newName)) {
reject.accept(context.getString(R.string.install_new_game_malformed));
return;
}
if (profile.getRepository().renameVersion(version, newName)) {
resolve.run();
profile.getRepository().refreshVersionsAsync()
.thenRunAsync(Schedulers.androidUIThread(), () -> {
if (profile.getRepository().hasVersion(newName)) {
profile.setSelectedVersion(newName);
}
UIManager.getInstance().getVersionUI().refresh();
}).start();
} else {
reject.accept(context.getString(R.string.version_manage_rename_fail));
}
});
dialog.show();
return dialog.getFuture();
}
/*
public static void exportVersion(Profile profile, String version) {
Controllers.getDecorator().startWizard(new ExportWizardProvider(profile, version), i18n("modpack.wizard"));
}
*/
public static void openFolder(Activity context, Profile profile, String version) {
FileBrowser.Builder builder = new FileBrowser.Builder(context);
builder.setLibMode(LibMode.FILE_BROWSER);
builder.setInitDir(profile.getRepository().getRunDirectory(version).getAbsolutePath());
builder.create().browse(context, RequestCodes.BROWSE_VERSION_DIR_CODE, null);
}
/*
public static void duplicateVersion(Profile profile, String version) {
Controllers.prompt(
new PromptDialogPane.Builder(i18n("version.manage.duplicate.prompt"), (res, resolve, reject) -> {
String newVersionName = ((PromptDialogPane.Builder.StringQuestion) res.get(1)).getValue();
boolean copySaves = ((PromptDialogPane.Builder.BooleanQuestion) res.get(2)).getValue();
Task.runAsync(() -> profile.getRepository().duplicateVersion(version, newVersionName, copySaves))
.thenComposeAsync(profile.getRepository().refreshVersionsAsync())
.whenComplete(Schedulers.androidUIThread(), (result, exception) -> {
if (exception == null) {
resolve.run();
} else {
reject.accept(StringUtils.getStackTrace(exception));
profile.getRepository().removeVersionFromDisk(newVersionName);
}
}).start();
})
.addQuestion(new PromptDialogPane.Builder.HintQuestion(i18n("version.manage.duplicate.confirm")))
.addQuestion(new PromptDialogPane.Builder.StringQuestion(null, version,
new Validator(i18n("install.new_game.already_exists"), newVersionName -> !profile.getRepository().hasVersion(newVersionName))))
.addQuestion(new PromptDialogPane.Builder.BooleanQuestion(i18n("version.manage.duplicate.duplicate_save"), false)));
}
public static void updateVersion(Profile profile, String version) {
Controllers.getDecorator().startWizard(new ModpackInstallWizardProvider(profile, version));
}
public static void updateGameAssets(Profile profile, String version) {
TaskExecutor executor = new GameAssetDownloadTask(profile.getDependency(), profile.getRepository().getVersion(version), GameAssetDownloadTask.DOWNLOAD_INDEX_FORCIBLY, true)
.executor();
Controllers.taskDialog(executor, i18n("version.manage.redownload_assets_index"), TaskCancellationAction.NO_CANCEL);
executor.start();
}
*/
public static void cleanVersion(Profile profile, String id) {
try {
profile.getRepository().clean(id);
} catch (IOException e) {
Logging.LOG.log(Level.WARNING, "Unable to clean game directory", e);
}
}
}

View File

@ -3,4 +3,8 @@ package com.tungsten.fcl.util;
public class RequestCodes {
public static final int PERMISSION_REQUEST_CODE = 0;
public static final int BROWSE_VERSION_DIR_CODE = 50;
public static final int SELECT_PROFILE_CODE = 100;
}

View File

@ -0,0 +1,10 @@
<vector
android:height="24dp"
android:viewportHeight="24"
android:viewportWidth="24"
android:width="24dp"
xmlns:android="http://schemas.android.com/apk/res/android">
<path
android:fillColor="@android:color/white"
android:pathData="M16,1L4,1c-1.1,0 -2,0.9 -2,2v14h2L4,3h12L16,1zM19,5L8,5c-1.1,0 -2,0.9 -2,2v14c0,1.1 0.9,2 2,2h11c1.1,0 2,-0.9 2,-2L21,7c0,-1.1 -0.9,-2 -2,-2zM19,21L8,21L8,7h11v14z"/>
</vector>

View File

@ -0,0 +1,93 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="400dp"
android:layout_height="match_parent"
android:padding="10dp"
xmlns:app="http://schemas.android.com/apk/res-auto">
<com.tungsten.fcllibrary.component.view.FCLTextView
android:id="@+id/title"
android:text="@string/version_new_profile"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintHorizontal_bias="0.5"/>
<androidx.appcompat.widget.LinearLayoutCompat
android:id="@+id/name_layout"
app:layout_constraintTop_toBottomOf="@+id/title"
android:layout_marginTop="10dp"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal">
<com.tungsten.fcllibrary.component.view.FCLTextView
android:singleLine="true"
android:layout_width="80dp"
android:layout_height="wrap_content"
android:text="@string/profile_name"
android:layout_gravity="center"/>
<com.tungsten.fcllibrary.component.view.FCLEditText
android:singleLine="true"
android:id="@+id/name"
android:layout_width="300dp"
android:layout_height="wrap_content"
android:layout_gravity="center"/>
</androidx.appcompat.widget.LinearLayoutCompat>
<androidx.appcompat.widget.LinearLayoutCompat
android:id="@+id/path_layout"
app:layout_constraintTop_toBottomOf="@+id/name_layout"
android:layout_marginTop="10dp"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal">
<com.tungsten.fcllibrary.component.view.FCLTextView
android:singleLine="true"
android:layout_width="80dp"
android:layout_height="wrap_content"
android:text="@string/profile_path"
android:layout_gravity="center"/>
<com.tungsten.fcllibrary.component.view.FCLTextView
android:singleLine="true"
android:id="@+id/path"
android:layout_width="250dp"
android:layout_height="wrap_content"
android:layout_gravity="center"/>
<com.tungsten.fcllibrary.component.view.FCLImageButton
app:auto_tint="true"
android:layout_marginStart="10dp"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:src="@drawable/ic_baseline_edit_24"
android:id="@+id/edit"/>
</androidx.appcompat.widget.LinearLayoutCompat>
<com.tungsten.fcllibrary.component.view.FCLButton
android:layout_marginTop="10dp"
android:id="@+id/positive"
android:text="@string/dialog_positive"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/path_layout"/>
<com.tungsten.fcllibrary.component.view.FCLButton
android:layout_marginTop="10dp"
android:id="@+id/negative"
android:text="@string/dialog_negative"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toBottomOf="@id/path_layout"/>
</androidx.constraintlayout.widget.ConstraintLayout>

View File

@ -0,0 +1,62 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:padding="10dp"
android:layout_width="match_parent"
android:layout_height="match_parent"
xmlns:app="http://schemas.android.com/apk/res-auto">
<com.tungsten.fcllibrary.component.view.FCLTextView
android:id="@+id/title"
android:text="@string/version_manage_rename_message"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintHorizontal_bias="0.5"/>
<androidx.appcompat.widget.LinearLayoutCompat
android:id="@+id/details"
android:layout_marginTop="10dp"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/title"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:orientation="horizontal">
<com.tungsten.fcllibrary.component.view.FCLTextView
android:singleLine="true"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/version_manage_rename_new"
android:layout_gravity="center"/>
<com.tungsten.fcllibrary.component.view.FCLEditText
android:layout_marginStart="10dp"
android:singleLine="true"
android:id="@+id/new_name"
android:layout_width="250dp"
android:layout_height="wrap_content"
android:layout_gravity="center"/>
</androidx.appcompat.widget.LinearLayoutCompat>
<com.tungsten.fcllibrary.component.view.FCLButton
android:layout_marginTop="10dp"
android:id="@+id/positive"
android:text="@string/dialog_positive"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/details"/>
<com.tungsten.fcllibrary.component.view.FCLButton
android:layout_marginTop="10dp"
android:id="@+id/negative"
android:text="@string/dialog_negative"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toBottomOf="@+id/details"/>
</androidx.constraintlayout.widget.ConstraintLayout>

View File

@ -0,0 +1,64 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:paddingBottom="10dp"
xmlns:app="http://schemas.android.com/apk/res-auto">
<androidx.constraintlayout.widget.ConstraintLayout
app:layout_constraintTop_toTopOf="parent"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="@drawable/bg_item"
android:padding="5dp">
<com.tungsten.fcllibrary.component.view.FCLRadioButton
android:id="@+id/radio"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintVertical_bias="0.5"/>
<androidx.appcompat.widget.LinearLayoutCompat
android:layout_width="0dp"
android:layout_height="wrap_content"
android:orientation="vertical"
android:layout_marginStart="10dp"
android:layout_marginEnd="10dp"
app:layout_constraintStart_toEndOf="@+id/radio"
app:layout_constraintEnd_toStartOf="@+id/delete"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintVertical_bias="0.5">
<com.tungsten.fcllibrary.component.view.FCLTextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:singleLine="true"
android:id="@+id/name"/>
<com.tungsten.fcllibrary.component.view.FCLTextView
android:textSize="11sp"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:singleLine="true"
android:id="@+id/path"/>
</androidx.appcompat.widget.LinearLayoutCompat>
<com.tungsten.fcllibrary.component.view.FCLImageButton
android:id="@+id/delete"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:src="@drawable/ic_baseline_delete_24"
app:auto_tint="true"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintVertical_bias="0.5"/>
</androidx.constraintlayout.widget.ConstraintLayout>
</androidx.constraintlayout.widget.ConstraintLayout>

View File

@ -0,0 +1,106 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="wrap_content"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:paddingBottom="10dp">
<androidx.constraintlayout.widget.ConstraintLayout
android:background="@drawable/bg_item"
android:layout_width="match_parent"
android:layout_height="wrap_content"
app:layout_constraintTop_toTopOf="parent"
android:padding="5dp">
<com.tungsten.fcllibrary.component.view.FCLRadioButton
android:id="@+id/radio"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintVertical_bias="0.5"/>
<androidx.appcompat.widget.AppCompatImageView
android:id="@+id/icon"
android:layout_width="30dp"
android:layout_height="30dp"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintStart_toEndOf="@+id/radio"
app:layout_constraintVertical_bias="0.5"/>
<androidx.appcompat.widget.LinearLayoutCompat
android:layout_width="0dp"
android:layout_height="wrap_content"
android:orientation="vertical"
android:layout_marginStart="10dp"
android:layout_marginEnd="10dp"
app:layout_constraintStart_toEndOf="@+id/icon"
app:layout_constraintEnd_toStartOf="@+id/rename"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintVertical_bias="0.5">
<com.tungsten.fcllibrary.component.view.FCLTextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:singleLine="true"
android:id="@+id/title"/>
<com.tungsten.fcllibrary.component.view.FCLTextView
android:textSize="11sp"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:singleLine="true"
android:id="@+id/subtitle"/>
</androidx.appcompat.widget.LinearLayoutCompat>
<com.tungsten.fcllibrary.component.view.FCLImageButton
android:id="@+id/rename"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:src="@drawable/ic_baseline_edit_24"
app:auto_tint="true"
app:layout_constraintEnd_toStartOf="@+id/copy"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintVertical_bias="0.5"/>
<com.tungsten.fcllibrary.component.view.FCLImageButton
android:id="@+id/copy"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:src="@drawable/ic_baseline_content_copy_24"
app:auto_tint="true"
app:layout_constraintEnd_toStartOf="@+id/browse"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintVertical_bias="0.5"/>
<com.tungsten.fcllibrary.component.view.FCLImageButton
android:id="@+id/browse"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:src="@drawable/ic_baseline_folder_24"
app:auto_tint="true"
app:layout_constraintEnd_toStartOf="@+id/delete"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintVertical_bias="0.5"/>
<com.tungsten.fcllibrary.component.view.FCLImageButton
android:id="@+id/delete"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:src="@drawable/ic_baseline_delete_24"
app:auto_tint="true"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintVertical_bias="0.5"/>
</androidx.constraintlayout.widget.ConstraintLayout>
</androidx.constraintlayout.widget.ConstraintLayout>

View File

@ -1,7 +1,65 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:background="@color/ui_bg_color"
android:padding="10dp"
android:layout_width="match_parent"
android:layout_height="match_parent">
<androidx.appcompat.widget.LinearLayoutCompat
android:id="@+id/left"
android:orientation="vertical"
android:layout_width="0dp"
android:layout_height="match_parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintHorizontal_bias="0"
app:layout_constraintWidth_percent="0.3">
<ListView
android:id="@+id/profile_list"
android:layout_width="match_parent"
android:layout_height="0dp"
android:layout_weight="1"
android:divider="@android:color/transparent"
android:dividerHeight="0dp"/>
<com.tungsten.fcllibrary.component.view.FCLButton
android:id="@+id/refresh"
android:layout_marginTop="10dp"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="@string/message_refresh"/>
<com.tungsten.fcllibrary.component.view.FCLButton
android:id="@+id/new_profile"
android:layout_marginTop="10dp"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="@string/version_new_profile"/>
</androidx.appcompat.widget.LinearLayoutCompat>
<RelativeLayout
android:layout_width="0dp"
android:layout_height="match_parent"
android:layout_marginStart="10dp"
app:layout_constraintStart_toEndOf="@+id/left"
app:layout_constraintEnd_toEndOf="parent">
<ListView
android:id="@+id/version_list"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:divider="@android:color/transparent"
android:dividerHeight="0dp" />
<com.tungsten.fcllibrary.component.view.FCLProgressBar
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_centerInParent="true"
android:id="@+id/progress"/>
</RelativeLayout>
</androidx.constraintlayout.widget.ConstraintLayout>

View File

@ -59,6 +59,7 @@
<string name="exception_artifact_malformed">无法校验文件。</string>
<string name="exception_ssl_handshake">缺少 SSL 证书。</string>
<string name="install_new_game_malformed">无效的名称</string>
<string name="install_failed_downloading_timeout">下载超时</string>
<string name="install_failed_downloading_detail">无法下载</string>
@ -73,8 +74,21 @@
<string name="launch_state_waiting_launching">等待游戏启动</string>
<string name="launch_state_done">完成</string>
<string name="message_refresh">刷新</string>
<string name="message_cancelled">操作已取消</string>
<string name="message_unknown">未知</string>
<string name="profile_shared">公有目录</string>
<string name="profile_private">私有目录</string>
<string name="profile_name">名称</string>
<string name="profile_path">路径</string>
<string name="profile_select">选择目录</string>
<string name="profile_add_alert">请先填写名称与路径!</string>
<string name="version_manage_remove_confirm">你确定要删除 %s 吗? 该操作无法撤回!</string>
<string name="version_manage_remove_confirm_independent">该版本启用了版本隔离,删除该版本将会一并删除存档及其他数据。你仍要删除 %s 吗?</string>
<string name="version_manage_rename_message">重命名版本</string>
<string name="version_manage_rename_new">新名称</string>
<string name="version_manage_rename_fail">重命名失败</string>
<string name="version_new_profile">新建目录</string>
</resources>

View File

@ -66,7 +66,16 @@
<string name="exception_access_denied">Unable to access the file.</string>
<string name="exception_artifact_malformed">Cannot verify the integrity of the downloaded files.</string>
<string name="exception_ssl_handshake">Unable to establish SSL connection due to missing SSL certificates in current Java installation.</string>
<string name="install_installer_fabric" translatable="false">Fabric</string>
<string name="install_installer_fabric_api" translatable="false">Fabric API</string>
<string name="install_installer_forge" translatable="false">Forge</string>
<string name="install_installer_game" translatable="false">Minecraft</string>
<string name="install_installer_liteloader" translatable="false">LiteLoader</string>
<string name="install_installer_optifine" translatable="false">OptiFine</string>
<string name="install_installer_quilt" translatable="false">Quilt</string>
<string name="install_installer_quilt_api" translatable="false">QSL/QFAPI</string>
<string name="install_new_game_malformed">Invalid Name</string>
<string name="install_failed_downloading_timeout">Download timeout</string>
<string name="install_failed_downloading_detail">Unable to download</string>
@ -80,9 +89,22 @@
<string name="launch_state_logging_in">Logging in</string>
<string name="launch_state_waiting_launching">Waiting for the game to launch</string>
<string name="launch_state_done">Completing launch</string>
<string name="message_refresh">Refresh</string>
<string name="message_cancelled">Operation was cancelled</string>
<string name="message_unknown">Unknown</string>
<string name="profile_shared">Shared Directory</string>
<string name="profile_private">Private Directory</string>
<string name="profile_name">Name</string>
<string name="profile_path">Path</string>
<string name="profile_select">Select Directory</string>
<string name="profile_add_alert">Please fill the name and path first!</string>
<string name="version_manage_remove_confirm">Are you sure you want to permanently remove the version %s? This action cannot be undone!</string>
<string name="version_manage_remove_confirm_independent">Since this instance is stored in an isolated directory, deleting it will also delete its saves and other data. Do you still want to delete instance %s?</string>
<string name="version_manage_rename_message">Rename version</string>
<string name="version_manage_rename_new">New name</string>
<string name="version_manage_rename_fail">Rename failed</string>
<string name="version_new_profile">New Directory</string>
</resources>

View File

@ -1,6 +1,11 @@
package com.tungsten.fclcore.task;
import com.tungsten.fclcore.event.EventManager;
import com.tungsten.fclcore.fakefx.beans.property.ReadOnlyDoubleProperty;
import com.tungsten.fclcore.fakefx.beans.property.ReadOnlyDoubleWrapper;
import com.tungsten.fclcore.fakefx.beans.property.ReadOnlyStringProperty;
import com.tungsten.fclcore.fakefx.beans.property.ReadOnlyStringWrapper;
import com.tungsten.fclcore.util.InvocationDispatcher;
import com.tungsten.fclcore.util.Logging;
import com.tungsten.fclcore.util.ReflectionHelper;
import com.tungsten.fclcore.util.function.ExceptionalConsumer;
@ -296,6 +301,12 @@ public abstract class Task<T> {
}
private long lastTime = Long.MIN_VALUE;
private final ReadOnlyDoubleWrapper progress = new ReadOnlyDoubleWrapper(this, "progress", -1);
private final InvocationDispatcher<Double> progressUpdate = InvocationDispatcher.runOn(Schedulers.androidUIThread(), progress::set);
public ReadOnlyDoubleProperty progressProperty() {
return progress.getReadOnlyProperty();
}
protected void updateProgress(long progress, long total) {
updateProgress(1.0 * progress / total);
@ -312,11 +323,18 @@ public abstract class Task<T> {
}
protected void updateProgressImmediately(double progress) {
// TODO: update progress
progressUpdate.accept(progress);
}
private final ReadOnlyStringWrapper message = new ReadOnlyStringWrapper(this, "message", null);
private final InvocationDispatcher<String> messageUpdate = InvocationDispatcher.runOn(Schedulers.androidUIThread(), message::set);
public final ReadOnlyStringProperty messageProperty() {
return message.getReadOnlyProperty();
}
protected final void updateMessage(String newMessage) {
// TODO: update message
messageUpdate.accept(newMessage);
}
public final T run() throws Exception {
@ -334,7 +352,11 @@ public abstract class Task<T> {
}
private void doSubTask(Task<?> task) throws Exception {
message.bind(task.message);
progress.bind(task.progress);
task.run();
message.unbind();
progress.unbind();
}
public final TaskExecutor executor() {

View File

@ -0,0 +1,17 @@
package com.tungsten.fclcore.util;
import java.util.function.Consumer;
@FunctionalInterface
public interface FutureCallback<T> {
/**
* Callback of future, called after future finishes.
* This callback gives the feedback whether the result of future is acceptable or not,
* if not, giving the reason, and future will be relaunched when necessary.
* @param result result of the future
* @param resolve accept the result
* @param reject reject the result with failure reason
*/
void call(T result, Runnable resolve, Consumer<String> reject);
}

View File

@ -3,6 +3,8 @@ package com.tungsten.fcllibrary.anim;
import android.animation.Animator;
import android.animation.AnimatorListenerAdapter;
import android.animation.ObjectAnimator;
import android.os.Handler;
import android.view.View;
import com.tungsten.fcllibrary.component.view.FCLDynamicIsland;
@ -10,6 +12,8 @@ public class DynamicIslandAnim {
private final FCLDynamicIsland view;
private Thread thread;
private Handler handler;
private ObjectAnimator expandScaleAnimatorX;
private ObjectAnimator shrinkScaleAnimatorX;
private ObjectAnimator expandScaleAnimatorY;
@ -18,12 +22,17 @@ public class DynamicIslandAnim {
private ObjectAnimator shrinkAdjustAnimatorX;
private ObjectAnimator expandAdjustAnimatorY;
private ObjectAnimator shrinkAdjustAnimatorY;
private ObjectAnimator hideAnimator;
public DynamicIslandAnim(FCLDynamicIsland view) {
this.view = view;
this.handler = new Handler();
}
public void refresh(float scale) {
if (thread != null) {
thread.interrupt();
}
if (expandScaleAnimatorX != null && expandScaleAnimatorX.isRunning()) {
expandScaleAnimatorX.cancel();
}
@ -48,6 +57,9 @@ public class DynamicIslandAnim {
if (shrinkAdjustAnimatorY != null && shrinkAdjustAnimatorY.isRunning()) {
shrinkAdjustAnimatorY.cancel();
}
if (hideAnimator != null && hideAnimator.isRunning()) {
hideAnimator.cancel();
}
expandScaleAnimatorX = ObjectAnimator.ofFloat(view, "scaleX", scale / 2f, 0.95f).setDuration(300);
shrinkScaleAnimatorX = ObjectAnimator.ofFloat(view, "scaleX", 0.95f, scale / 2f).setDuration(300);
expandScaleAnimatorY = ObjectAnimator.ofFloat(view, "scaleY", 0.5f, 0.95f).setDuration(300);
@ -56,9 +68,12 @@ public class DynamicIslandAnim {
shrinkAdjustAnimatorX = ObjectAnimator.ofFloat(view, "scaleX", 1f, 0.95f).setDuration(200);
expandAdjustAnimatorY = ObjectAnimator.ofFloat(view, "scaleY", 0.95f, 1f).setDuration(200);
shrinkAdjustAnimatorY = ObjectAnimator.ofFloat(view, "scaleY", 1f, 0.95f).setDuration(200);
hideAnimator = ObjectAnimator.ofFloat(view, "alpha", 1f, 0f).setDuration(2000);
}
public void run(String text) {
view.setVisibility(View.VISIBLE);
view.setAlpha(1f);
shrinkScaleAnimatorX.addListener(new AnimatorListenerAdapter() {
@Override
public void onAnimationEnd(Animator animation) {
@ -73,6 +88,32 @@ public class DynamicIslandAnim {
@Override
public void onAnimationEnd(Animator animation) {
super.onAnimationEnd(animation);
shrinkAdjustAnimatorX.addListener(new AnimatorListenerAdapter() {
@Override
public void onAnimationEnd(Animator animation) {
super.onAnimationEnd(animation);
thread = new Thread(() -> {
try {
Thread.sleep(2000);
if (!thread.isInterrupted()) {
handler.post(() -> {
hideAnimator.addListener(new AnimatorListenerAdapter() {
@Override
public void onAnimationEnd(Animator animation) {
super.onAnimationEnd(animation);
view.setVisibility(View.GONE);
}
});
hideAnimator.start();
});
}
} catch (InterruptedException e) {
e.printStackTrace();
}
});
thread.start();
}
});
shrinkAdjustAnimatorX.start();
shrinkAdjustAnimatorY.start();
}

View File

@ -120,16 +120,18 @@ public class FileBrowserActivity extends FCLActivity implements View.OnClickList
selectedFiles.remove(path);
}
else {
if (fileBrowser.getLibMode() == LibMode.FOLDER_CHOOSER || fileBrowser.getSelectionMode() == SelectionMode.SINGLE_SELECTION) {
if (fileBrowser.getSelectionMode() == SelectionMode.SINGLE_SELECTION) {
selectedFiles = new ArrayList<>();
}
selectedFiles.add(path);
}
adapter1.setSelectedFiles(selectedFiles);
adapter1.notifyDataSetChanged();
System.out.println(selectedFiles);
}
});
listView.setAdapter(adapter);
System.out.println(selectedFiles);
}
@Override

View File

@ -134,7 +134,7 @@ public class FileBrowserAdapter extends FCLAdapter {
if (file.isDirectory()) {
listener.onEnterDir(file.getAbsolutePath());
}
if (fileBrowser.getLibMode() != LibMode.FILE_BROWSER && !(fileBrowser.getLibMode() == LibMode.FILE_CHOOSER && file.isDirectory()) && !(fileBrowser.getLibMode() == LibMode.FOLDER_CHOOSER && file.isFile())) {
if (fileBrowser.getLibMode() != LibMode.FILE_BROWSER && fileBrowser.getLibMode() != LibMode.FOLDER_CHOOSER && !(fileBrowser.getLibMode() == LibMode.FILE_CHOOSER)) {
listener.onSelect(this, file.getAbsolutePath());
}
});