offline account

This commit is contained in:
Tungstend 2022-11-08 00:38:24 +08:00
parent 6f87397c9c
commit c562db8cbe
30 changed files with 1682 additions and 38 deletions

View File

@ -0,0 +1,333 @@
package com.tungsten.fcl.game;
import static com.tungsten.fclcore.util.Logging.LOG;
import com.tungsten.fcl.R;
import com.tungsten.fcl.setting.Profile;
import com.tungsten.fcl.setting.VersionSetting;
import com.tungsten.fclauncher.FCLPath;
import com.tungsten.fclauncher.bridge.FCLBridgeCallback;
import com.tungsten.fclcore.auth.Account;
import com.tungsten.fclcore.auth.AuthInfo;
import com.tungsten.fclcore.auth.AuthenticationException;
import com.tungsten.fclcore.auth.CharacterDeletedException;
import com.tungsten.fclcore.auth.CredentialExpiredException;
import com.tungsten.fclcore.auth.authlibinjector.AuthlibInjectorDownloadException;
import com.tungsten.fclcore.download.DefaultDependencyManager;
import com.tungsten.fclcore.download.MaintainTask;
import com.tungsten.fclcore.download.game.GameAssetIndexDownloadTask;
import com.tungsten.fclcore.download.game.GameVerificationFixTask;
import com.tungsten.fclcore.download.game.LibraryDownloadException;
import com.tungsten.fclcore.game.JavaVersion;
import com.tungsten.fclcore.game.LaunchOptions;
import com.tungsten.fclcore.game.Version;
import com.tungsten.fclcore.mod.ModpackCompletionException;
import com.tungsten.fclcore.mod.ModpackConfiguration;
import com.tungsten.fclcore.mod.ModpackProvider;
import com.tungsten.fclcore.task.DownloadException;
import com.tungsten.fclcore.task.Schedulers;
import com.tungsten.fclcore.task.Task;
import com.tungsten.fclcore.task.TaskExecutor;
import com.tungsten.fclcore.task.TaskListener;
import com.tungsten.fclcore.util.Lang;
import com.tungsten.fclcore.util.StringUtils;
import com.tungsten.fclcore.util.io.ResponseCodeException;
import com.tungsten.fclcore.util.platform.CommandBuilder;
import com.tungsten.fcllibrary.component.dialog.FCLAlertDialog;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.net.SocketTimeoutException;
import java.net.URL;
import java.nio.file.AccessDeniedException;
import java.util.*;
import java.util.concurrent.CancellationException;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.atomic.AtomicReference;
import java.util.logging.Level;
public final class LauncherHelper {
/*
private final Profile profile;
private final Account account;
private final String selectedVersion;
private final VersionSetting setting;
private boolean showLogs;
public LauncherHelper(Profile profile, Account account, String selectedVersion) {
this.profile = Objects.requireNonNull(profile);
this.account = Objects.requireNonNull(account);
this.selectedVersion = Objects.requireNonNull(selectedVersion);
this.setting = profile.getVersionSetting(selectedVersion);
this.launchingStepsPane.setTitle(i18n("version.launch"));
}
private final TaskExecutorDialogPane launchingStepsPane = new TaskExecutorDialogPane(TaskCancellationAction.NORMAL);
public void launch() {
LOG.info("Launching game version: " + selectedVersion);
Controllers.dialog(launchingStepsPane);
launch0();
}
private void launch0() {
FCLGameRepository repository = profile.getRepository();
DefaultDependencyManager dependencyManager = profile.getDependency();
AtomicReference<Version> version = new AtomicReference<>(MaintainTask.maintain(repository, repository.getResolvedVersion(selectedVersion)));
Optional<String> gameVersion = repository.getGameVersion(version.get());
boolean integrityCheck = repository.unmarkVersionLaunchedAbnormally(selectedVersion);
CountDownLatch launchingLatch = new CountDownLatch(1);
List<String> javaAgents = new ArrayList<>(0);
AtomicReference<JavaVersion> javaVersionRef = new AtomicReference<>();
TaskExecutor executor = checkGameState(profile, setting, version.get())
.thenComposeAsync(javaVersion -> {
javaVersionRef.set(Objects.requireNonNull(javaVersion));
version.set(version.get());
if (setting.isNotCheckGame())
return null;
return Task.allOf(
dependencyManager.checkGameCompletionAsync(version.get(), integrityCheck),
Task.composeAsync(() -> {
try {
ModpackConfiguration<?> configuration = ModpackHelper.readModpackConfiguration(repository.getModpackConfiguration(selectedVersion));
ModpackProvider provider = ModpackHelper.getProviderByType(configuration.getType());
if (provider == null) return null;
else return provider.createCompletionTask(dependencyManager, selectedVersion);
} catch (IOException e) {
return null;
}
}),
Task.composeAsync(() -> null)
);
}).withStage("launch.state.dependencies")
.thenComposeAsync(() -> {
return gameVersion.map(s -> new GameVerificationFixTask(dependencyManager, s, version.get())).orElse(null);
})
.thenComposeAsync(() -> logIn(account).withStage("launch.state.logging_in"))
.thenComposeAsync(authInfo -> Task.supplyAsync(() -> {
LaunchOptions launchOptions = repository.getLaunchOptions(selectedVersion, javaVersionRef.get(), profile.getGameDir(), javaAgents, scriptFile != null);
return new FCLGameLauncher(
FCLPath.CONTEXT,
repository,
version.get(),
authInfo,
launchOptions,
new FCLProcessListener()
);
}).thenComposeAsync(launcher -> { // launcher is prev task's result
return Task.supplyAsync(launcher::launch);
}).thenAcceptAsync(fclBridge -> { // process is LaunchTask's result
if (scriptFile == null) {
PROCESSES.add(process);
if (launcherVisibility == LauncherVisibility.CLOSE)
Launcher.stopApplication();
else
launchingStepsPane.setCancel(new TaskCancellationAction(it -> {
process.stop();
it.fireEvent(new DialogCloseEvent());
}));
} else {
Platform.runLater(() -> {
launchingStepsPane.fireEvent(new DialogCloseEvent());
Controllers.dialog(i18n("version.launch_script.success", scriptFile.getAbsolutePath()));
});
}
}).thenRunAsync(() -> {
launchingLatch.await();
}).withStage("launch.state.waiting_launching"))
.withStagesHint(Lang.immutableListOf(
"launch.state.java",
"launch.state.dependencies",
"launch.state.logging_in",
"launch.state.waiting_launching"))
.executor();
launchingStepsPane.setExecutor(executor, false);
executor.addTaskListener(new TaskListener() {
@Override
public void onStop(boolean success, TaskExecutor executor) {
launchingStepsPane.fireEvent(new DialogCloseEvent());
if (!success) {
Exception ex = executor.getException();
if (!(ex instanceof CancellationException)) {
String message;
if (ex instanceof ModpackCompletionException) {
if (ex.getCause() instanceof FileNotFoundException)
message = i18n("modpack.type.curse.not_found");
else
message = i18n("modpack.type.curse.error");
} else if (ex instanceof PermissionException) {
message = i18n("launch.failed.executable_permission");
} else if (ex instanceof ProcessCreationException) {
message = i18n("launch.failed.creating_process") + ex.getLocalizedMessage();
} else if (ex instanceof NotDecompressingNativesException) {
message = i18n("launch.failed.decompressing_natives") + ex.getLocalizedMessage();
} else if (ex instanceof LibraryDownloadException) {
message = i18n("launch.failed.download_library", ((LibraryDownloadException) ex).getLibrary().getName()) + "\n";
if (ex.getCause() instanceof ResponseCodeException) {
ResponseCodeException rce = (ResponseCodeException) ex.getCause();
int responseCode = rce.getResponseCode();
URL url = rce.getUrl();
if (responseCode == 404)
message += i18n("download.code.404", url);
else
message += i18n("download.failed", url, responseCode);
} else {
message += StringUtils.getStackTrace(ex.getCause());
}
} else if (ex instanceof DownloadException) {
URL url = ((DownloadException) ex).getUrl();
if (ex.getCause() instanceof SocketTimeoutException) {
message = i18n("install.failed.downloading.timeout", url);
} else if (ex.getCause() instanceof ResponseCodeException) {
ResponseCodeException responseCodeException = (ResponseCodeException) ex.getCause();
if (I18n.hasKey("download.code." + responseCodeException.getResponseCode())) {
message = i18n("download.code." + responseCodeException.getResponseCode(), url);
} else {
message = i18n("install.failed.downloading.detail", url) + "\n" + StringUtils.getStackTrace(ex.getCause());
}
} else {
message = i18n("install.failed.downloading.detail", url) + "\n" + StringUtils.getStackTrace(ex.getCause());
}
} else if (ex instanceof GameAssetIndexDownloadTask.GameAssetIndexMalformedException) {
message = i18n("assets.index.malformed");
} else if (ex instanceof AuthlibInjectorDownloadException) {
message = i18n("account.failed.injector_download_failure");
} else if (ex instanceof CharacterDeletedException) {
message = i18n("account.failed.character_deleted");
} else if (ex instanceof ResponseCodeException) {
ResponseCodeException rce = (ResponseCodeException) ex;
int responseCode = rce.getResponseCode();
URL url = rce.getUrl();
if (responseCode == 404)
message = i18n("download.code.404", url);
else
message = i18n("download.failed", url, responseCode);
} else if (ex instanceof CommandTooLongException) {
message = i18n("launch.failed.command_too_long");
} else if (ex instanceof ExecutionPolicyLimitException) {
Controllers.prompt(new PromptDialogPane.Builder(i18n("launch.failed.execution_policy"),
(result, resolve, reject) -> {
if (CommandBuilder.setExecutionPolicy()) {
LOG.info("Set the ExecutionPolicy for the scope 'CurrentUser' to 'RemoteSigned'");
resolve.run();
} else {
LOG.warning("Failed to set ExecutionPolicy");
reject.accept(i18n("launch.failed.execution_policy.failed_to_set"));
}
})
.addQuestion(new PromptDialogPane.Builder.HintQuestion(i18n("launch.failed.execution_policy.hint")))
);
return;
} else if (ex instanceof AccessDeniedException) {
message = i18n("exception.access_denied", ((AccessDeniedException) ex).getFile());
} else {
message = StringUtils.getStackTrace(ex);
}
Controllers.dialog(message,
scriptFile == null ? i18n("launch.failed") : i18n("version.launch_script.failed"),
MessageType.ERROR);
}
}
}
});
executor.start();
}
private static Task<JavaVersion> checkGameState(Profile profile, VersionSetting setting, Version version) {
if (setting.isNotCheckJVM()) {
return Task.composeAsync(() -> setting.getJavaVersion(version))
.withStage("launch.state.java");
}
return Task.composeAsync(() -> setting.getJavaVersion(version))
.thenComposeAsync(javaVersion -> Task.allOf(Task.completed(javaVersion), Task.supplyAsync(() -> JavaVersion.getSuitableJavaVersion(version))))
.thenComposeAsync(Schedulers.androidUIThread(), javaVersions -> {
JavaVersion javaVersion = (JavaVersion) javaVersions.get(0);
JavaVersion suggestedJavaVersion = (JavaVersion) javaVersions.get(1);
if (setting.getJava() == 0 || javaVersion.getVersion() == suggestedJavaVersion.getVersion()) {
return Task.completed(suggestedJavaVersion);
}
CompletableFuture<JavaVersion> future = new CompletableFuture<>();
Runnable continueAction = () -> future.complete(javaVersion);
FCLAlertDialog.Builder builder = new FCLAlertDialog.Builder(FCLPath.CONTEXT);
builder.setCancelable(false);
builder.setMessage(FCLPath.CONTEXT.getString(R.string.launch_error_java));
builder.setPositiveButton(FCLPath.CONTEXT.getString(R.string.launch_error_java_auto), () -> {
setting.setJava(0);
future.complete(suggestedJavaVersion);
});
builder.setPositiveButton(FCLPath.CONTEXT.getString(R.string.launch_error_java_continue), continueAction::run);
builder.create().show();
return Task.fromCompletableFuture(future);
}).withStage("launch.state.java");
}
private static Task<AuthInfo> logIn(Account account) {
return Task.composeAsync(() -> {
try {
return Task.completed(account.logIn());
} catch (CredentialExpiredException e) {
LOG.log(Level.INFO, "Credential has expired", e);
return Task.completed(DialogController.logIn(account));
} catch (AuthenticationException e) {
LOG.log(Level.WARNING, "Authentication failed, try skipping refresh", e);
CompletableFuture<Task<AuthInfo>> future = new CompletableFuture<>();
runInFX(() -> {
JFXButton loginOfflineButton = new JFXButton(i18n("account.login.skip"));
loginOfflineButton.setOnAction(event -> {
try {
future.complete(Task.completed(account.playOffline()));
} catch (AuthenticationException e2) {
future.completeExceptionally(e2);
}
});
JFXButton retryButton = new JFXButton(i18n("account.login.retry"));
retryButton.setOnAction(event -> {
future.complete(logIn(account));
});
Controllers.dialog(new MessageDialogPane.Builder(i18n("account.failed.server_disconnected"), i18n("account.failed"), MessageType.ERROR)
.addAction(loginOfflineButton)
.addAction(retryButton)
.addCancel(() ->
future.completeExceptionally(new CancellationException()))
.build());
});
return Task.fromCompletableFuture(future).thenComposeAsync(task -> task);
}
});
}
class FCLProcessListener implements FCLBridgeCallback {
@Override
public void onCursorModeChange(int mode) {
// TODO: Handle mouse event
}
@Override
public void onExit(int code) {
if (code != 0) {
// TODO: Show GameCrashWindow here
}
}
}
public static void stopManagedProcesses() {
while (!PROCESSES.isEmpty())
Optional.ofNullable(PROCESSES.poll()).ifPresent(ManagedProcess::stop);
}
*/
}

View File

@ -26,6 +26,7 @@ import android.graphics.Bitmap;
import android.graphics.BitmapFactory; import android.graphics.BitmapFactory;
import android.graphics.Canvas; import android.graphics.Canvas;
import android.graphics.Matrix; import android.graphics.Matrix;
import android.graphics.Paint;
import android.graphics.drawable.BitmapDrawable; import android.graphics.drawable.BitmapDrawable;
import com.tungsten.fcl.util.ResourceNotFoundError; import com.tungsten.fcl.util.ResourceNotFoundError;
@ -33,6 +34,7 @@ import com.tungsten.fclauncher.FCLPath;
import com.tungsten.fclcore.auth.Account; import com.tungsten.fclcore.auth.Account;
import com.tungsten.fclcore.auth.ServerResponseMalformedException; import com.tungsten.fclcore.auth.ServerResponseMalformedException;
import com.tungsten.fclcore.auth.microsoft.MicrosoftAccount; import com.tungsten.fclcore.auth.microsoft.MicrosoftAccount;
import com.tungsten.fclcore.auth.offline.OfflineAccount;
import com.tungsten.fclcore.auth.yggdrasil.Texture; import com.tungsten.fclcore.auth.yggdrasil.Texture;
import com.tungsten.fclcore.auth.yggdrasil.TextureModel; import com.tungsten.fclcore.auth.yggdrasil.TextureModel;
import com.tungsten.fclcore.auth.yggdrasil.TextureType; import com.tungsten.fclcore.auth.yggdrasil.TextureType;
@ -85,7 +87,14 @@ public final class TexturesLoader {
public static LoadedTexture loadTexture(Texture texture) throws IOException { public static LoadedTexture loadTexture(Texture texture) throws IOException {
if (StringUtils.isBlank(texture.getUrl())) { if (StringUtils.isBlank(texture.getUrl())) {
if (texture.getImg() == null)
throw new IOException("Texture url is empty"); throw new IOException("Texture url is empty");
Map<String, String> metadata = texture.getMetadata();
if (metadata == null) {
metadata = emptyMap();
}
return new LoadedTexture(texture.getImg(), metadata);
} }
Path file = getTexturePath(texture); Path file = getTexturePath(texture);
@ -114,6 +123,34 @@ public final class TexturesLoader {
} }
return new LoadedTexture(img, metadata); return new LoadedTexture(img, metadata);
} }
private static Bitmap loadCape(Texture texture) throws IOException {
if (StringUtils.isBlank(texture.getUrl())) {
return texture.getImg();
} else {
Path file = getTexturePath(texture);
if (!Files.isRegularFile(file)) {
// download it
try {
new FileDownloadTask(new URL(texture.getUrl()), file.toFile()).run();
LOG.info("Texture downloaded: " + texture.getUrl());
} catch (Exception e) {
if (Files.isRegularFile(file)) {
// concurrency conflict?
LOG.log(Level.WARNING, "Failed to download texture " + texture.getUrl() + ", but the file is available", e);
} else {
throw new IOException("Failed to download texture " + texture.getUrl());
}
}
}
Bitmap img = BitmapFactory.decodeStream(Files.newInputStream(file));
if (img == null)
throw new IOException("Texture is malformed");
return img;
}
}
// ==== // ====
// ==== Skins ==== // ==== Skins ====
@ -128,7 +165,7 @@ public final class TexturesLoader {
try (InputStream in = ResourceNotFoundError.getResourceAsStream(path)) { try (InputStream in = ResourceNotFoundError.getResourceAsStream(path)) {
DEFAULT_SKINS.put(model, new LoadedTexture(BitmapFactory.decodeStream(in), singletonMap("model", model.modelName))); DEFAULT_SKINS.put(model, new LoadedTexture(BitmapFactory.decodeStream(in), singletonMap("model", model.modelName)));
} catch (Throwable e) { } catch (Throwable e) {
throw new ResourceNotFoundError("Cannoot load default skin from " + path, e); throw new ResourceNotFoundError("Cannot load default skin from " + path, e);
} }
} }
@ -190,6 +227,28 @@ public final class TexturesLoader {
}, uuidFallback); }, uuidFallback);
} }
public static ObjectBinding<Bitmap> capeBinding(Account account) {
return BindingMapping.of(account.getTextures())
.map(textures -> textures
.flatMap(it -> Optional.ofNullable(it.get(TextureType.CAPE)))
.filter(it -> StringUtils.isNotBlank(it.getUrl())))
.asyncMap(it -> {
if (it.isPresent()) {
Texture texture = it.get();
return CompletableFuture.supplyAsync(() -> {
try {
return loadCape(texture);
} catch (IOException e) {
LOG.log(Level.WARNING, "Failed to load texture " + texture.getUrl() + ", using null", e);
return null;
}
}, POOL);
} else {
return CompletableFuture.completedFuture(null);
}
}, null);
}
// ==== // ====
// ==== Avatar ==== // ==== Avatar ====
@ -208,25 +267,25 @@ public final class TexturesLoader {
matrix = new Matrix(); matrix = new Matrix();
matrix.postScale(hatScale, hatScale); matrix.postScale(hatScale, hatScale);
Bitmap newHatBitmap = Bitmap.createBitmap(hatBitmap, 0, 0, 8, 8, matrix, false); Bitmap newHatBitmap = Bitmap.createBitmap(hatBitmap, 0, 0, 8, 8, matrix, false);
canvas.drawBitmap(newFaceBitmap, faceOffset, faceOffset, null); canvas.drawBitmap(newFaceBitmap, faceOffset, faceOffset, new Paint(Paint.ANTI_ALIAS_FLAG));
canvas.drawBitmap(newHatBitmap, 0, 0, null); canvas.drawBitmap(newHatBitmap, 0, 0, new Paint(Paint.ANTI_ALIAS_FLAG));
return avatar; return avatar;
} }
public static ObjectBinding<BitmapDrawable> fxAvatarBinding(YggdrasilService service, UUID uuid, int size) { public static ObjectBinding<BitmapDrawable> avatarBinding(YggdrasilService service, UUID uuid, int size) {
return BindingMapping.of(skinBinding(service, uuid)) return BindingMapping.of(skinBinding(service, uuid))
.map(it -> toAvatar(it.image, size)) .map(it -> toAvatar(it.image, size))
.map(BitmapDrawable::new); .map(BitmapDrawable::new);
} }
public static ObjectBinding<BitmapDrawable> fxAvatarBinding(Account account, int size) { public static ObjectBinding<BitmapDrawable> avatarBinding(Account account, int size) {
if (account instanceof YggdrasilAccount || account instanceof MicrosoftAccount) { if (account instanceof YggdrasilAccount || account instanceof MicrosoftAccount || account instanceof OfflineAccount) {
return BindingMapping.of(skinBinding(account)) return BindingMapping.of(skinBinding(account))
.map(it -> toAvatar(it.image, size)) .map(it -> toAvatar(it.image, size))
.map(BitmapDrawable::new); .map(BitmapDrawable::new);
} else { } else {
return Bindings.createObjectBinding( return Bindings.createObjectBinding(
() -> new BitmapDrawable(toAvatar(getDefaultSkin(TextureModel.detectUUID(account.getUUID())).image, size))); () -> new BitmapDrawable(toAvatar(getDefaultSkin(account == null ? TextureModel.ALEX : TextureModel.detectUUID(account.getUUID())).image, size)));
} }
} }
// ==== // ====

View File

@ -0,0 +1,80 @@
package com.tungsten.fcl.ui.account;
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.game.TexturesLoader;
import com.tungsten.fcl.setting.Accounts;
import com.tungsten.fcl.ui.UIManager;
import com.tungsten.fclcore.auth.Account;
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;
import com.tungsten.fcllibrary.util.ConvertUtils;
public class AccountListAdapter extends FCLAdapter {
private final ObservableList<Account> list;
public AccountListAdapter(Context context, ObservableList<Account> list) {
super(context);
this.list = list;
}
static class ViewHolder {
FCLRadioButton radioButton;
AppCompatImageView avatar;
FCLTextView name;
FCLTextView type;
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_account, null);
viewHolder.radioButton = view.findViewById(R.id.radio);
viewHolder.avatar = view.findViewById(R.id.avatar);
viewHolder.name = view.findViewById(R.id.name);
viewHolder.type = view.findViewById(R.id.type);
viewHolder.delete = view.findViewById(R.id.delete);
view.setTag(viewHolder);
}
else {
viewHolder = (ViewHolder) view.getTag();
}
Account account = list.get(i);
viewHolder.radioButton.setChecked(account == Accounts.getSelectedAccount());
viewHolder.avatar.setBackground(TexturesLoader.avatarBinding(account, ConvertUtils.dip2px(getContext(), 30f)).get());
viewHolder.name.setText(account.getUsername());
viewHolder.type.setText(Accounts.getLocalizedLoginTypeName(getContext(), Accounts.getAccountFactory(account)));
viewHolder.radioButton.setOnClickListener(view1 -> {
Accounts.setSelectedAccount(account);
UIManager.getInstance().getAccountUI().refresh();
});
viewHolder.delete.setOnClickListener(view1 -> {
Accounts.getAccounts().remove(account);
UIManager.getInstance().getAccountUI().refresh();
});
return view;
}
}

View File

@ -1,20 +1,72 @@
package com.tungsten.fcl.ui.account; package com.tungsten.fcl.ui.account;
import static com.tungsten.fclcore.util.Logging.LOG;
import android.content.Context; import android.content.Context;
import android.graphics.BitmapFactory;
import android.graphics.PixelFormat; import android.graphics.PixelFormat;
import android.opengl.GLSurfaceView; import android.opengl.GLSurfaceView;
import android.view.View; import android.view.View;
import android.view.ViewGroup; import android.view.ViewGroup;
import android.widget.ListView;
import android.widget.Toast;
import androidx.appcompat.widget.AppCompatImageView;
import com.tungsten.fcl.R; import com.tungsten.fcl.R;
import com.tungsten.fcl.game.TexturesLoader;
import com.tungsten.fcl.setting.Accounts;
import com.tungsten.fcl.ui.UIManager;
import com.tungsten.fclauncher.FCLPath;
import com.tungsten.fclcore.auth.Account;
import com.tungsten.fclcore.auth.AuthInfo;
import com.tungsten.fclcore.auth.AuthenticationException;
import com.tungsten.fclcore.auth.ClassicAccount;
import com.tungsten.fclcore.auth.CredentialExpiredException;
import com.tungsten.fclcore.auth.OAuthAccount;
import com.tungsten.fclcore.auth.authlibinjector.AuthlibInjectorAccount;
import com.tungsten.fclcore.auth.authlibinjector.AuthlibInjectorServer;
import com.tungsten.fclcore.fakefx.beans.binding.Bindings;
import com.tungsten.fclcore.fakefx.beans.property.SimpleStringProperty;
import com.tungsten.fclcore.fakefx.beans.property.StringProperty;
import com.tungsten.fclcore.task.Schedulers;
import com.tungsten.fclcore.task.Task;
import com.tungsten.fclcore.util.skin.InvalidSkinException;
import com.tungsten.fclcore.util.skin.NormalizedSkin;
import com.tungsten.fcllibrary.component.ui.FCLCommonUI; import com.tungsten.fcllibrary.component.ui.FCLCommonUI;
import com.tungsten.fcllibrary.component.view.FCLButton;
import com.tungsten.fcllibrary.component.view.FCLImageButton;
import com.tungsten.fcllibrary.component.view.FCLProgressBar;
import com.tungsten.fcllibrary.component.view.FCLTextView;
import com.tungsten.fcllibrary.component.view.FCLUILayout; import com.tungsten.fcllibrary.component.view.FCLUILayout;
import com.tungsten.fcllibrary.skin.GameCharacter;
import com.tungsten.fcllibrary.skin.MinecraftSkinRenderer; import com.tungsten.fcllibrary.skin.MinecraftSkinRenderer;
import com.tungsten.fcllibrary.skin.SkinGLSurfaceView; import com.tungsten.fcllibrary.skin.SkinGLSurfaceView;
public class AccountUI extends FCLCommonUI { import java.util.Optional;
import java.util.concurrent.CancellationException;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.atomic.AtomicReference;
import java.util.logging.Level;
public class AccountUI extends FCLCommonUI implements View.OnClickListener {
private SkinGLSurfaceView skinGLSurfaceView; private SkinGLSurfaceView skinGLSurfaceView;
private MinecraftSkinRenderer renderer;
private AppCompatImageView avatarView;
private FCLTextView name;
private FCLTextView description;
private FCLImageButton refresh;
private FCLImageButton editSkin;
private FCLProgressBar refreshProgress;
private FCLProgressBar skinProgress;
private FCLButton addOfflineAccount;
private FCLButton addMicrosoftAccount;
private FCLButton addExternalAccount;
private ListView listView;
private AccountListAdapter accountListAdapter;
public AccountUI(Context context, FCLUILayout parent, int id) { public AccountUI(Context context, FCLUILayout parent, int id) {
super(context, parent, id); super(context, parent, id);
@ -23,12 +75,12 @@ public class AccountUI extends FCLCommonUI {
@Override @Override
public void onCreate() { public void onCreate() {
super.onCreate(); super.onCreate();
MinecraftSkinRenderer renderer = new MinecraftSkinRenderer(getContext(), true); renderer = new MinecraftSkinRenderer(getContext(), true);
skinGLSurfaceView = findViewById(R.id.skin_view); skinGLSurfaceView = findViewById(R.id.skin_view);
ViewGroup.LayoutParams layoutParams = skinGLSurfaceView.getLayoutParams(); ViewGroup.LayoutParams layoutParamsSkin = skinGLSurfaceView.getLayoutParams();
layoutParams.width = (int) (((View) skinGLSurfaceView.getParent().getParent()).getMeasuredWidth() * 0.3f); layoutParamsSkin.width = (int) (((View) skinGLSurfaceView.getParent().getParent()).getMeasuredWidth() * 0.3f);
layoutParams.height = (int) Math.min(((View) skinGLSurfaceView.getParent().getParent()).getMeasuredWidth() * 0.3f, ((View) skinGLSurfaceView.getParent().getParent()).getMeasuredHeight()); layoutParamsSkin.height = (int) Math.min(((View) skinGLSurfaceView.getParent().getParent()).getMeasuredWidth() * 0.3f, ((View) skinGLSurfaceView.getParent().getParent()).getMeasuredHeight());
skinGLSurfaceView.setLayoutParams(layoutParams); skinGLSurfaceView.setLayoutParams(layoutParamsSkin);
skinGLSurfaceView.setEGLConfigChooser(8, 8, 8, 8, 16, 0); skinGLSurfaceView.setEGLConfigChooser(8, 8, 8, 8, 16, 0);
skinGLSurfaceView.getHolder().setFormat(PixelFormat.RGBA_8888); skinGLSurfaceView.getHolder().setFormat(PixelFormat.RGBA_8888);
@ -37,30 +89,152 @@ public class AccountUI extends FCLCommonUI {
skinGLSurfaceView.setRenderer(renderer, 5f); skinGLSurfaceView.setRenderer(renderer, 5f);
skinGLSurfaceView.setRenderMode(GLSurfaceView.RENDERMODE_CONTINUOUSLY); skinGLSurfaceView.setRenderMode(GLSurfaceView.RENDERMODE_CONTINUOUSLY);
skinGLSurfaceView.setPreserveEGLContextOnPause(true); skinGLSurfaceView.setPreserveEGLContextOnPause(true);
avatarView = findViewById(R.id.avatar);
ViewGroup.LayoutParams layoutParamsAvatar = avatarView.getLayoutParams();
layoutParamsAvatar.width = (int) (((View) avatarView.getParent().getParent()).getMeasuredWidth() * 0.08f);
layoutParamsAvatar.height = (int) (((View) avatarView.getParent().getParent()).getMeasuredWidth() * 0.08f);
avatarView.setLayoutParams(layoutParamsAvatar);
refresh = findViewById(R.id.refresh);
name = findViewById(R.id.name);
description = findViewById(R.id.description);
editSkin = findViewById(R.id.skin);
refreshProgress = findViewById(R.id.refresh_progress);
skinProgress = findViewById(R.id.skin_progress);
addOfflineAccount = findViewById(R.id.offline);
addMicrosoftAccount = findViewById(R.id.microsoft);
addExternalAccount = findViewById(R.id.external);
refresh.setOnClickListener(this);
editSkin.setOnClickListener(this);
addOfflineAccount.setOnClickListener(this);
addMicrosoftAccount.setOnClickListener(this);
addExternalAccount.setOnClickListener(this);
listView = findViewById(R.id.list);
} }
@Override @Override
public void onStart() { public void onStart() {
super.onStart(); super.onStart();
} refresh();
@Override
public void onStop() {
super.onStop();
}
@Override
public void onPause() {
super.onPause();
}
@Override
public void onResume() {
super.onResume();
} }
@Override @Override
public void refresh() { public void refresh() {
avatarView.setBackground(TexturesLoader.avatarBinding(Accounts.getSelectedAccount(), (int) (((View) avatarView.getParent().getParent()).getMeasuredWidth() * 0.15f)).get());
try {
NormalizedSkin normalizedSkin = new NormalizedSkin(Accounts.getSelectedAccount() == null ? BitmapFactory.decodeStream(AccountUI.class.getResourceAsStream("/assets/img/alex.png")) : TexturesLoader.skinBinding(Accounts.getSelectedAccount()).get().getImage());
renderer.mCharacter = new GameCharacter(normalizedSkin.isSlim());
renderer.updateTexture(normalizedSkin.isOldFormat() ? normalizedSkin.getNormalizedTexture() : normalizedSkin.getOriginalTexture(), Accounts.getSelectedAccount() == null ? null : TexturesLoader.capeBinding(Accounts.getSelectedAccount()).get());
} catch (InvalidSkinException e) {
e.printStackTrace();
}
if (accountListAdapter == null) {
accountListAdapter = new AccountListAdapter(getContext(), Accounts.getAccounts());
listView.setAdapter(accountListAdapter);
} else {
accountListAdapter.notifyDataSetChanged();
}
name.setText(Accounts.getSelectedAccount() == null ? getContext().getString(R.string.account_state_no_account) : Accounts.getSelectedAccount().getUsername());
description.setVisibility(Accounts.getSelectedAccount() == null ? View.GONE : View.VISIBLE);
if (Accounts.getSelectedAccount() != null) {
String loginTypeName = Accounts.getLocalizedLoginTypeName(getContext(), Accounts.getAccountFactory(Accounts.getSelectedAccount()));
StringProperty subtitle = new SimpleStringProperty();
if (Accounts.getSelectedAccount() instanceof AuthlibInjectorAccount) {
AuthlibInjectorServer server = ((AuthlibInjectorAccount) Accounts.getSelectedAccount()).getServer();
subtitle.bind(Bindings.concat(
loginTypeName, ", ", getContext().getString(R.string.account_injector_server), ": ",
Bindings.createStringBinding(server::getName, server)));
} else {
subtitle.set(loginTypeName);
}
description.setText(subtitle.get());
}
UIManager.getInstance().getMainUI().refresh();
}
@Override
public void onClick(View view) {
if (view == refresh) {
refresh.setVisibility(View.GONE);
refreshProgress.setVisibility(View.VISIBLE);
refreshAsync()
.whenComplete(Schedulers.androidUIThread(), ex -> {
refresh.setVisibility(View.VISIBLE);
refreshProgress.setVisibility(View.GONE);
if (ex != null) {
Toast.makeText(getContext(), Accounts.localizeErrorMessage(getContext(), ex), Toast.LENGTH_SHORT).show();
}
refresh();
})
.start();
}
if (view == editSkin) {
} }
if (view == addOfflineAccount) {
CreateAccountDialog dialog = new CreateAccountDialog(getContext(), Accounts.FACTORY_OFFLINE);
dialog.show();
}
if (view == addMicrosoftAccount) {
CreateAccountDialog dialog = new CreateAccountDialog(getContext(), Accounts.FACTORY_MICROSOFT);
dialog.show();
}
if (view == addExternalAccount) {
CreateAccountDialog dialog = new CreateAccountDialog(getContext(), Accounts.FACTORY_AUTHLIB_INJECTOR);
dialog.show();
}
}
public Task<?> refreshAsync() {
return Task.runAsync(() -> {
if (Accounts.getSelectedAccount() == null)
return;
Accounts.getSelectedAccount().clearCache();
try {
Accounts.getSelectedAccount().logIn();
} catch (CredentialExpiredException e) {
try {
logIn(Accounts.getSelectedAccount());
} catch (CancellationException e1) {
// ignore cancellation
} catch (Exception e1) {
LOG.log(Level.WARNING, "Failed to refresh " + Accounts.getSelectedAccount() + " with password", e1);
throw e1;
}
} catch (AuthenticationException e) {
LOG.log(Level.WARNING, "Failed to refresh " + Accounts.getSelectedAccount() + " with token", e);
throw e;
}
});
}
public static AuthInfo logIn(Account account) throws CancellationException, AuthenticationException, InterruptedException {
if (account instanceof ClassicAccount) {
CountDownLatch latch = new CountDownLatch(1);
AtomicReference<AuthInfo> res = new AtomicReference<>(null);
ClassicAccountLoginDialog dialog = new ClassicAccountLoginDialog(FCLPath.CONTEXT, (ClassicAccount) account, it -> {
res.set(it);
latch.countDown();
}, latch::countDown);
dialog.show();
latch.await();
return Optional.ofNullable(res.get()).orElseThrow(CancellationException::new);
} else if (account instanceof OAuthAccount) {
CountDownLatch latch = new CountDownLatch(1);
AtomicReference<AuthInfo> res = new AtomicReference<>(null);
OAuthAccountLoginDialog dialog = new OAuthAccountLoginDialog(FCLPath.CONTEXT, (OAuthAccount) account, it -> {
res.set(it);
latch.countDown();
}, latch::countDown);
dialog.show();
latch.await();
return Optional.ofNullable(res.get()).orElseThrow(CancellationException::new);
}
return account.logIn();
}
} }

View File

@ -0,0 +1,17 @@
package com.tungsten.fcl.ui.account;
import android.content.Context;
import androidx.annotation.NonNull;
import com.tungsten.fclcore.auth.AuthInfo;
import com.tungsten.fclcore.auth.ClassicAccount;
import com.tungsten.fcllibrary.component.dialog.FCLDialog;
import java.util.function.Consumer;
public class ClassicAccountLoginDialog extends FCLDialog {
public ClassicAccountLoginDialog(@NonNull Context context, ClassicAccount oldAccount, Consumer<AuthInfo> success, Runnable failed) {
super(context);
}
}

View File

@ -0,0 +1,398 @@
package com.tungsten.fcl.ui.account;
import static com.tungsten.fcl.setting.ConfigHolder.config;
import android.content.Context;
import android.os.Handler;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.ListView;
import android.widget.RelativeLayout;
import android.widget.Toast;
import androidx.annotation.NonNull;
import androidx.appcompat.widget.AppCompatImageView;
import androidx.constraintlayout.widget.ConstraintLayout;
import com.tungsten.fcl.R;
import com.tungsten.fcl.game.TexturesLoader;
import com.tungsten.fcl.setting.Accounts;
import com.tungsten.fcl.ui.UIManager;
import com.tungsten.fclcore.auth.AccountFactory;
import com.tungsten.fclcore.auth.CharacterSelector;
import com.tungsten.fclcore.auth.NoSelectedCharacterException;
import com.tungsten.fclcore.auth.authlibinjector.AuthlibInjectorAccountFactory;
import com.tungsten.fclcore.auth.microsoft.MicrosoftAccountFactory;
import com.tungsten.fclcore.auth.offline.OfflineAccountFactory;
import com.tungsten.fclcore.auth.yggdrasil.GameProfile;
import com.tungsten.fclcore.auth.yggdrasil.YggdrasilService;
import com.tungsten.fclcore.task.Schedulers;
import com.tungsten.fclcore.task.Task;
import com.tungsten.fclcore.task.TaskExecutor;
import com.tungsten.fclcore.util.StringUtils;
import com.tungsten.fcllibrary.component.FCLAdapter;
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.FCLTabLayout;
import com.tungsten.fcllibrary.component.view.FCLTextView;
import java.util.List;
import java.util.concurrent.CountDownLatch;
public class CreateAccountDialog extends FCLDialog implements View.OnClickListener {
private FCLTextView title;
private FCLTabLayout tabLayout;
private RelativeLayout detailsContainer;
private FCLButton login;
private FCLButton cancel;
private boolean showMethodSwitcher;
private AccountFactory<?> factory;
private TaskExecutor loginTask;
private Details details;
public CreateAccountDialog(@NonNull Context context, AccountFactory<?> factory) {
super(context);
setContentView(R.layout.dialog_create_account);
setCancelable(false);
title = findViewById(R.id.title);
tabLayout = findViewById(R.id.tab_layout);
detailsContainer = findViewById(R.id.detail_container);
login = findViewById(R.id.login);
cancel = findViewById(R.id.cancel);
login.setOnClickListener(this);
cancel.setOnClickListener(this);
init(factory);
}
private void init(AccountFactory<?> factory) {
if (factory == null) {
showMethodSwitcher = true;
String preferred = config().getPreferredLoginType();
try {
factory = Accounts.getAccountFactory(preferred);
} catch (IllegalArgumentException e) {
factory = Accounts.FACTORY_OFFLINE;
}
} else {
showMethodSwitcher = false;
}
this.factory = factory;
int titleId;
if (showMethodSwitcher) {
titleId = R.string.account_create;
} else {
if (factory instanceof OfflineAccountFactory) {
titleId = R.string.account_create_offline;
} else if (factory instanceof MicrosoftAccountFactory) {
titleId = R.string.account_create_microsoft;
}
else {
titleId = R.string.account_create_external;
}
}
title.setText(getContext().getString(titleId));
tabLayout.setVisibility(showMethodSwitcher ? View.VISIBLE : View.GONE);
initDetails();
}
private void initDetails() {
if (factory instanceof OfflineAccountFactory) {
details = new OfflineDetails(getContext());
}
if (factory instanceof MicrosoftAccountFactory) {
details = new MicrosoftDetails(getContext());
}
if (factory instanceof AuthlibInjectorAccountFactory) {
details = new ExternalDetails(getContext());
}
detailsContainer.removeAllViews();
detailsContainer.addView(details.getView(), ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT);
}
private void login() {
login.setEnabled(false);
cancel.setEnabled(false);
String username;
String password;
Object additionalData;
try {
username = details.getUsername();
password = details.getPassword();
additionalData = details.getAdditionalData();
} catch (IllegalStateException e) {
Toast.makeText(getContext(), e.getMessage(), Toast.LENGTH_SHORT).show();
login.setEnabled(true);
cancel.setEnabled(true);
return;
}
/*
logging.set(true);
deviceCode.set(null);
*/
CharacterSelector selector = new DialogCharacterSelector(getContext());
loginTask = Task.supplyAsync(() -> factory.create(selector, username, password, null, additionalData))
.whenComplete(Schedulers.androidUIThread(), account -> {
int oldIndex = Accounts.getAccounts().indexOf(account);
if (oldIndex == -1) {
Accounts.getAccounts().add(account);
} else {
// adding an already-added account
// instead of discarding the new account, we first remove the existing one then add the new one
Accounts.getAccounts().remove(oldIndex);
Accounts.getAccounts().add(oldIndex, account);
}
// select the new account
Accounts.setSelectedAccount(account);
login.setEnabled(true);
cancel.setEnabled(true);
UIManager.getInstance().getAccountUI().refresh();
dismiss();
}, exception -> {
if (exception instanceof NoSelectedCharacterException) {
dismiss();
} else {
Toast.makeText(getContext(), Accounts.localizeErrorMessage(getContext(), exception), Toast.LENGTH_SHORT).show();
}
login.setEnabled(true);
cancel.setEnabled(true);
}).executor(true);
}
private void onCancel() {
if (loginTask != null) {
loginTask.cancel();
}
dismiss();
}
@Override
public void onClick(View view) {
if (view == login) {
login();
}
if (view == cancel) {
onCancel();
}
}
// details panel
private interface Details {
String getUsername();
String getPassword();
Object getAdditionalData();
View getView();
}
private static class OfflineDetails implements Details {
private final Context context;
private final View view;
private final FCLEditText username;
public OfflineDetails(Context context) {
this.context = context;
this.view = LayoutInflater.from(context).inflate(R.layout.dialog_create_account_offline, null);
username = view.findViewById(R.id.username);
}
@Override
public String getUsername() throws IllegalStateException {
if (StringUtils.isBlank(username.getText().toString())) {
throw new IllegalStateException(context.getString(R.string.account_create_alert));
}
return username.getText().toString();
}
@Override
public String getPassword() throws IllegalStateException {
return null;
}
@Override
public Object getAdditionalData() throws IllegalStateException {
return null;
}
@Override
public View getView() throws IllegalStateException {
return view;
}
}
private static class MicrosoftDetails implements Details {
private final Context context;
public MicrosoftDetails(Context context) {
this.context = context;
}
@Override
public String getUsername() throws IllegalStateException {
return null;
}
@Override
public String getPassword() throws IllegalStateException {
return null;
}
@Override
public Object getAdditionalData() throws IllegalStateException {
return null;
}
@Override
public View getView() throws IllegalStateException {
return null;
}
}
private static class ExternalDetails implements Details {
private final Context context;
public ExternalDetails(Context context) {
this.context = context;
}
@Override
public String getUsername() throws IllegalStateException {
return null;
}
@Override
public String getPassword() throws IllegalStateException {
return null;
}
@Override
public Object getAdditionalData() throws IllegalStateException {
return null;
}
@Override
public View getView() throws IllegalStateException {
return null;
}
}
// character selector
private static class DialogCharacterSelector extends FCLDialog implements CharacterSelector, View.OnClickListener {
private final Handler handler;
private final ListView listView;
private final FCLButton cancel;
private final CountDownLatch latch = new CountDownLatch(1);
private GameProfile selectedProfile = null;
public DialogCharacterSelector(Context context) {
super(context);
setContentView(R.layout.dialog_character_selector);
setCancelable(false);
handler = new Handler();
listView = findViewById(R.id.list);
cancel = findViewById(R.id.negative);
cancel.setOnClickListener(this);
}
public void refresh(YggdrasilService service, List<GameProfile> profiles) {
Adapter adapter = new Adapter(getContext(), service, profiles, profile -> {
selectedProfile = profile;
latch.countDown();
});
listView.setAdapter(adapter);
}
@Override
public GameProfile select(YggdrasilService service, List<GameProfile> profiles) throws NoSelectedCharacterException {
handler.post(() -> {
refresh(service, profiles);
show();
});
try {
latch.await();
if (selectedProfile == null)
throw new NoSelectedCharacterException();
return selectedProfile;
} catch (InterruptedException ignored) {
throw new NoSelectedCharacterException();
} finally {
dismiss();
}
}
@Override
public void onClick(View view) {
if (view == cancel) {
latch.countDown();
dismiss();
}
}
private static class Adapter extends FCLAdapter {
private final YggdrasilService service;
private final List<GameProfile> profiles;
private final Listener listener;
public Adapter(Context context, YggdrasilService service, List<GameProfile> profiles, Listener listener) {
super(context);
this.service = service;
this.profiles = profiles;
this.listener = listener;
}
static class ViewHolder {
ConstraintLayout parent;
AppCompatImageView avatar;
FCLTextView name;
}
interface Listener {
void onSelect(GameProfile profile);
}
@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_character, null);
viewHolder.parent = view.findViewById(R.id.parent);
viewHolder.avatar = view.findViewById(R.id.avatar);
viewHolder.name = view.findViewById(R.id.name);
view.setTag(viewHolder);
}
else {
viewHolder = (ViewHolder) view.getTag();
}
GameProfile gameProfile = profiles.get(i);
viewHolder.name.setText(gameProfile.getName());
viewHolder.avatar.setBackground(TexturesLoader.avatarBinding(service, gameProfile.getId(), 32).get());
viewHolder.parent.setOnClickListener(view1 -> {
listener.onSelect(gameProfile);
});
return view;
}
}
}
}

View File

@ -0,0 +1,17 @@
package com.tungsten.fcl.ui.account;
import android.content.Context;
import androidx.annotation.NonNull;
import com.tungsten.fclcore.auth.AuthInfo;
import com.tungsten.fclcore.auth.OAuthAccount;
import com.tungsten.fcllibrary.component.dialog.FCLDialog;
import java.util.function.Consumer;
public class OAuthAccountLoginDialog extends FCLDialog {
public OAuthAccountLoginDialog(@NonNull Context context, OAuthAccount account, Consumer<AuthInfo> success, Runnable failed) {
super(context);
}
}

View File

@ -0,0 +1,14 @@
<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android">
<corners
android:topLeftRadius="5.0dip"
android:topRightRadius="5.0dip"
android:bottomLeftRadius="5.0dip"
android:bottomRightRadius="5.0dip"/>
<stroke
android:width="1.5dp"
android:color="@android:color/darker_gray"/>
</shape>

View File

@ -0,0 +1,25 @@
<?xml version="1.0" encoding="utf-8"?>
<selector xmlns:android="http://schemas.android.com/apk/res/android">
<item android:state_pressed="true">
<shape>
<solid
android:color="@android:color/darker_gray"/>
<corners
android:radius="5dp"/>
<stroke
android:width="1.5dp"
android:color="@android:color/darker_gray"/>
</shape>
</item>
<item>
<shape>
<solid
android:color="@android:color/transparent"/>
<corners
android:radius="5dp"/>
<stroke
android:width="1.5dp"
android:color="@android:color/darker_gray"/>
</shape>
</item>
</selector>

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="M6,19c0,1.1 0.9,2 2,2h8c1.1,0 2,-0.9 2,-2V7H6v12zM19,4h-3.5l-1,-1h-5l-1,1H5v2h14V4z"/>
</vector>

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="M3,17.25V21h3.75L17.81,9.94l-3.75,-3.75L3,17.25zM20.71,7.04c0.39,-0.39 0.39,-1.02 0,-1.41l-2.34,-2.34c-0.39,-0.39 -1.02,-0.39 -1.41,0l-1.83,1.83 3.75,3.75 1.83,-1.83z"/>
</vector>

View File

@ -0,0 +1,11 @@
<?xml version="1.0" encoding="utf-8"?>
<vector
xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:fillColor="@android:color/white"
android:pathData="M12 4A3.5 3.5 0 0 0 8.5 7.5H10.5A1.5 1.5 0 0 1 12 6A1.5 1.5 0 0 1 13.5 7.5A1.5 1.5 0 0 1 12 9C11.45 9 11 9.45 11 10V11.75L2.4 18.2A1 1 0 0 0 3 20H21A1 1 0 0 0 21.6 18.2L13 11.75V10.85A3.5 3.5 0 0 0 15.5 7.5A3.5 3.5 0 0 0 12 4M12 13.5L18 18H6Z"/>
</vector>

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="M17.65,6.35C16.2,4.9 14.21,4 12,4c-4.42,0 -7.99,3.58 -7.99,8s3.57,8 7.99,8c3.73,0 6.84,-2.55 7.73,-6h-2.08c-0.82,2.33 -3.04,4 -5.65,4 -3.31,0 -6,-2.69 -6,-6s2.69,-6 6,-6c1.66,0 3.14,0.69 4.22,1.78L13,11h7V4l-2.35,2.35z"/>
</vector>

View File

@ -0,0 +1,36 @@
<?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="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:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/account_select_character"
app:layout_constraintHorizontal_bias="0.5"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toTopOf="parent"/>
<ListView
android:layout_marginTop="5dp"
android:divider="@android:color/transparent"
android:dividerHeight="0dp"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:id="@+id/list"
android:layout_marginBottom="5dp"
app:layout_constraintTop_toBottomOf="@+id/title"
app:layout_constraintBottom_toTopOf="@+id/negative"/>
<com.tungsten.fcllibrary.component.view.FCLButton
android:id="@+id/negative"
android:layout_width="match_parent"
android:layout_height="wrap_content"
app:layout_constraintBottom_toBottomOf="parent"
android:text="@string/dialog_negative"/>
</androidx.constraintlayout.widget.ConstraintLayout>

View File

@ -0,0 +1,68 @@
<?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="match_parent"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:padding="10dp">
<com.tungsten.fcllibrary.component.view.FCLTextView
android:id="@+id/title"
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"/>
<com.tungsten.fcllibrary.component.view.FCLTabLayout
android:layout_marginTop="10dp"
android:id="@+id/tab_layout"
app:tabTextAppearance="@style/TabTextAppearance"
android:layout_width="match_parent"
android:layout_height="wrap_content"
app:tabGravity="fill"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toBottomOf="@+id/title">
<com.google.android.material.tabs.TabItem
android:layout_width="wrap_content"
android:layout_height="match_parent"
android:text="@string/account_methods_offline"/>
<com.google.android.material.tabs.TabItem
android:layout_width="wrap_content"
android:layout_height="match_parent"
android:text="@string/account_methods_microsoft"/>
<com.google.android.material.tabs.TabItem
android:layout_width="wrap_content"
android:layout_height="match_parent"
android:text="@string/account_methods_authlib_injector"/>
</com.tungsten.fcllibrary.component.view.FCLTabLayout>
<RelativeLayout
android:id="@+id/detail_container"
android:layout_width="match_parent"
android:layout_height="wrap_content"
app:layout_constraintTop_toBottomOf="@+id/tab_layout"
app:layout_constraintBottom_toTopOf="@+id/login"/>
<com.tungsten.fcllibrary.component.view.FCLButton
android:id="@+id/login"
android:text="@string/account_login"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintBottom_toBottomOf="parent"/>
<com.tungsten.fcllibrary.component.view.FCLButton
android:id="@+id/cancel"
android:text="@string/dialog_negative"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintBottom_toBottomOf="parent"/>
</androidx.constraintlayout.widget.ConstraintLayout>

View File

@ -0,0 +1,17 @@
<?xml version="1.0" encoding="utf-8"?>
<ScrollView xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent">
<androidx.appcompat.widget.LinearLayoutCompat
android:paddingTop="10dp"
android:paddingBottom="10dp"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical">
</androidx.appcompat.widget.LinearLayoutCompat>
</ScrollView>

View File

@ -0,0 +1,17 @@
<?xml version="1.0" encoding="utf-8"?>
<ScrollView xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent">
<androidx.appcompat.widget.LinearLayoutCompat
android:paddingTop="10dp"
android:paddingBottom="10dp"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical">
</androidx.appcompat.widget.LinearLayoutCompat>
</ScrollView>

View File

@ -0,0 +1,36 @@
<?xml version="1.0" encoding="utf-8"?>
<ScrollView xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="wrap_content"
android:layout_height="wrap_content">
<androidx.appcompat.widget.LinearLayoutCompat
android:paddingTop="10dp"
android:paddingBottom="10dp"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:orientation="vertical">
<androidx.appcompat.widget.LinearLayoutCompat
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="match_parent"
android:layout_height="wrap_content"
android:text="@string/account_create_username"
android:layout_gravity="center"/>
<com.tungsten.fcllibrary.component.view.FCLEditText
android:id="@+id/username"
android:layout_width="300dp"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:layout_marginStart="10dp"/>
</androidx.appcompat.widget.LinearLayoutCompat>
</androidx.appcompat.widget.LinearLayoutCompat>
</ScrollView>

View File

@ -0,0 +1,75 @@
<?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">
<androidx.constraintlayout.widget.ConstraintLayout
android:layout_marginTop="8dp"
android:layout_marginStart="5dp"
android:layout_marginEnd="5dp"
app:layout_constraintTop_toTopOf="parent"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="@drawable/bg_item">
<com.tungsten.fcllibrary.component.view.FCLRadioButton
android:id="@+id/radio"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="10dp"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintVertical_bias="0.5"
app:layout_constraintStart_toStartOf="parent"/>
<androidx.appcompat.widget.AppCompatImageView
android:id="@+id/avatar"
android:layout_width="30dp"
android:layout_height="30dp"
app:layout_constraintStart_toEndOf="@+id/radio"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintVertical_bias="0.5"/>
<androidx.appcompat.widget.LinearLayoutCompat
android:orientation="vertical"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginStart="10dp"
app:layout_constraintStart_toEndOf="@+id/avatar"
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/type"/>
</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:layout_marginEnd="10dp"
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,38 @@
<?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="match_parent"
xmlns:app="http://schemas.android.com/apk/res-auto">
<androidx.constraintlayout.widget.ConstraintLayout
android:layout_marginTop="5dp"
android:background="@drawable/bg_item_clickable"
android:id="@+id/parent"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:padding="8dp"
app:layout_constraintTop_toTopOf="parent">
<androidx.appcompat.widget.AppCompatImageView
android:id="@+id/avatar"
android:layout_width="30dp"
android:layout_height="30dp"
app:layout_constraintVertical_bias="0.5"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintTop_toTopOf="parent"/>
<com.tungsten.fcllibrary.component.view.FCLTextView
android:id="@+id/name"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:singleLine="true"
android:layout_marginStart="8dp"
app:layout_constraintVertical_bias="0.5"
app:layout_constraintStart_toStartOf="@+id/avatar"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintTop_toTopOf="parent"/>
</androidx.constraintlayout.widget.ConstraintLayout>
</androidx.constraintlayout.widget.ConstraintLayout>

View File

@ -18,4 +18,138 @@
app:layout_constraintTop_toTopOf="parent" app:layout_constraintTop_toTopOf="parent"
app:layout_constraintBottom_toBottomOf="parent"/> app:layout_constraintBottom_toBottomOf="parent"/>
<androidx.appcompat.widget.AppCompatImageView
android:id="@+id/avatar"
android:layout_width="0dp"
android:layout_height="40dp"
app:layout_constraintHorizontal_bias="0.5"
app:layout_constraintVertical_bias="0.25"
app:layout_constraintWidth_percent="0.08"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toEndOf="parent"/>
<com.tungsten.fcllibrary.component.view.FCLTextView
android:id="@+id/name"
android:singleLine="true"
android:gravity="center"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginBottom="10dp"
app:layout_constraintHorizontal_bias="0.5"
app:layout_constraintWidth_percent="0.4"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintBottom_toTopOf="@+id/description"/>
<com.tungsten.fcllibrary.component.view.FCLTextView
android:id="@+id/description"
android:singleLine="true"
android:gravity="center"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginBottom="10dp"
app:layout_constraintHorizontal_bias="0.5"
app:layout_constraintWidth_percent="0.4"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintBottom_toTopOf="@+id/refresh"/>
<com.tungsten.fcllibrary.component.view.FCLImageButton
android:id="@+id/refresh"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:src="@drawable/ic_baseline_refresh_24"
app:auto_tint="true"
android:layout_marginBottom="10dp"
app:layout_constraintHorizontal_bias="0.44"
app:layout_constraintBottom_toTopOf="@+id/bar"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toEndOf="parent"/>
<com.tungsten.fcllibrary.component.view.FCLProgressBar
android:id="@+id/refresh_progress"
android:visibility="gone"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginBottom="10dp"
app:layout_constraintHorizontal_bias="0.44"
app:layout_constraintBottom_toTopOf="@+id/bar"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toEndOf="parent"/>
<com.tungsten.fcllibrary.component.view.FCLImageButton
android:id="@+id/skin"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:src="@drawable/ic_baseline_hanger_24"
app:auto_tint="true"
android:layout_marginBottom="10dp"
app:layout_constraintHorizontal_bias="0.56"
app:layout_constraintBottom_toTopOf="@+id/bar"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toEndOf="parent"/>
<com.tungsten.fcllibrary.component.view.FCLProgressBar
android:id="@+id/skin_progress"
android:visibility="gone"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginBottom="10dp"
app:layout_constraintHorizontal_bias="0.56"
app:layout_constraintBottom_toTopOf="@+id/bar"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toEndOf="parent"/>
<androidx.appcompat.widget.LinearLayoutCompat
android:id="@+id/bar"
android:layout_width="0dp"
android:layout_height="wrap_content"
app:layout_constraintWidth_percent="0.4"
app:layout_constraintHorizontal_bias="0.5"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toEndOf="parent">
<com.tungsten.fcllibrary.component.view.FCLButton
android:layout_marginStart="5dp"
android:layout_marginEnd="5dp"
android:id="@+id/offline"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:text="@string/account_methods_offline"/>
<com.tungsten.fcllibrary.component.view.FCLButton
android:layout_marginStart="5dp"
android:layout_marginEnd="5dp"
android:id="@+id/microsoft"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:text="@string/account_methods_microsoft"/>
<com.tungsten.fcllibrary.component.view.FCLButton
android:layout_marginStart="5dp"
android:layout_marginEnd="5dp"
android:id="@+id/external"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:text="@string/account_methods_authlib_injector"/>
</androidx.appcompat.widget.LinearLayoutCompat>
<ListView
android:id="@+id/list"
android:divider="@android:color/transparent"
android:dividerHeight="0dp"
android:layout_width="0dp"
android:layout_height="match_parent"
app:layout_constraintHorizontal_bias="1"
app:layout_constraintWidth_percent="0.3"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toEndOf="parent"/>
</androidx.constraintlayout.widget.ConstraintLayout> </androidx.constraintlayout.widget.ConstraintLayout>

View File

@ -15,6 +15,18 @@
<string name="multiplayer">多人联机</string> <string name="multiplayer">多人联机</string>
<string name="setting">全局设置</string> <string name="setting">全局设置</string>
<string name="account_create">创建账户</string>
<string name="account_create_offline">创建离线账户</string>
<string name="account_create_microsoft">创建微软账户</string>
<string name="account_create_external">创建外置账户</string>
<string name="account_create_username">用户名</string>
<string name="account_create_password">密码</string>
<string name="account_create_server">认证服务器</string>
<string name="account_create_home">主页</string>
<string name="account_create_register">注册</string>
<string name="account_create_alert">请先提供足够的账户信息!</string>
<string name="account_injector_server">认证服务器</string>
<string name="account_login">登录</string>
<string name="account_methods_offline">离线账户</string> <string name="account_methods_offline">离线账户</string>
<string name="account_methods_yggdrasil">Mojang 账户</string> <string name="account_methods_yggdrasil">Mojang 账户</string>
<string name="account_methods_authlib_injector">外置账户</string> <string name="account_methods_authlib_injector">外置账户</string>
@ -37,6 +49,8 @@
<string name="account_methods_microsoft_error_no_character">你的账户尚未获取 Minecraft : Java Edition</string> <string name="account_methods_microsoft_error_no_character">你的账户尚未获取 Minecraft : Java Edition</string>
<string name="account_methods_microsoft_error_add_family_probably">请检查并确保年龄设置大于 18 岁。</string> <string name="account_methods_microsoft_error_add_family_probably">请检查并确保年龄设置大于 18 岁。</string>
<string name="account_methods_microsoft_close_page">Microsoft 账户登录完成</string> <string name="account_methods_microsoft_close_page">Microsoft 账户登录完成</string>
<string name="account_select_character">选择角色</string>
<string name="account_state_no_account">没有账户</string>
<string name="download_code_404">未找到文件</string> <string name="download_code_404">未找到文件</string>

View File

@ -23,6 +23,18 @@
<string name="multiplayer">Multiplayer</string> <string name="multiplayer">Multiplayer</string>
<string name="setting">Setting</string> <string name="setting">Setting</string>
<string name="account_create">Create Offline Account</string>
<string name="account_create_offline">Create Offline Account</string>
<string name="account_create_microsoft">Create Microsoft Account</string>
<string name="account_create_external">Create External Account</string>
<string name="account_create_username">Username</string>
<string name="account_create_password">Password</string>
<string name="account_create_server">Authentication server</string>
<string name="account_create_home">Home</string>
<string name="account_create_register">Register</string>
<string name="account_create_alert">Please provide enough account info first!</string>
<string name="account_injector_server">Authentication Server</string>
<string name="account_login">Login</string>
<string name="account_methods_offline">Offline Account</string> <string name="account_methods_offline">Offline Account</string>
<string name="account_methods_yggdrasil">Mojang Account</string> <string name="account_methods_yggdrasil">Mojang Account</string>
<string name="account_methods_authlib_injector">External Account</string> <string name="account_methods_authlib_injector">External Account</string>
@ -45,6 +57,8 @@
<string name="account_methods_microsoft_error_no_character">Your account does not own the Minecraft Java Edition.\nThe game profile may not have been created.</string> <string name="account_methods_microsoft_error_no_character">Your account does not own the Minecraft Java Edition.\nThe game profile may not have been created.</string>
<string name="account_methods_microsoft_error_add_family_probably">Please check if the age indicated in your account settings is at least 18 years old. If not and you believe this is an error, you can go to official website to change it.</string> <string name="account_methods_microsoft_error_add_family_probably">Please check if the age indicated in your account settings is at least 18 years old. If not and you believe this is an error, you can go to official website to change it.</string>
<string name="account_methods_microsoft_close_page">Microsoft account authorization is now completed.</string> <string name="account_methods_microsoft_close_page">Microsoft account authorization is now completed.</string>
<string name="account_select_character">Select character</string>
<string name="account_state_no_account">No account</string>
<string name="download_code_404">File not found</string> <string name="download_code_404">File not found</string>

View File

@ -187,7 +187,7 @@ public class MicrosoftService {
Map<TextureType, Texture> textures = new EnumMap<>(TextureType.class); Map<TextureType, Texture> textures = new EnumMap<>(TextureType.class);
if (!profile.skins.isEmpty()) { if (!profile.skins.isEmpty()) {
textures.put(TextureType.SKIN, new Texture(profile.skins.get(0).url, null)); textures.put(TextureType.SKIN, new Texture(profile.skins.get(0).url, null, null));
} }
// if (!profile.capes.isEmpty()) { // if (!profile.capes.isEmpty()) {
// textures.put(TextureType.CAPE, new Texture(profile.capes.get(0).url, null); // textures.put(TextureType.CAPE, new Texture(profile.capes.get(0).url, null);

View File

@ -4,15 +4,19 @@ import static com.tungsten.fclcore.util.Lang.mapOf;
import static com.tungsten.fclcore.util.Pair.pair; import static com.tungsten.fclcore.util.Pair.pair;
import java.io.IOException; import java.io.IOException;
import java.util.HashMap;
import java.util.Map; import java.util.Map;
import java.util.Optional; import java.util.Optional;
import java.util.UUID; import java.util.UUID;
import java.util.concurrent.CompletableFuture; import java.util.concurrent.CompletableFuture;
import java.util.concurrent.CompletionException; import java.util.concurrent.CompletionException;
import java.util.concurrent.ExecutionException; import java.util.concurrent.ExecutionException;
import java.util.logging.Level;
import static java.util.Objects.requireNonNull; import static java.util.Objects.requireNonNull;
import android.graphics.BitmapFactory;
import com.tungsten.fclcore.auth.Account; import com.tungsten.fclcore.auth.Account;
import com.tungsten.fclcore.auth.AuthInfo; import com.tungsten.fclcore.auth.AuthInfo;
import com.tungsten.fclcore.auth.AuthenticationException; import com.tungsten.fclcore.auth.AuthenticationException;
@ -22,9 +26,11 @@ import com.tungsten.fclcore.auth.authlibinjector.AuthlibInjectorDownloadExceptio
import com.tungsten.fclcore.auth.yggdrasil.Texture; import com.tungsten.fclcore.auth.yggdrasil.Texture;
import com.tungsten.fclcore.auth.yggdrasil.TextureModel; import com.tungsten.fclcore.auth.yggdrasil.TextureModel;
import com.tungsten.fclcore.auth.yggdrasil.TextureType; import com.tungsten.fclcore.auth.yggdrasil.TextureType;
import com.tungsten.fclcore.fakefx.beans.binding.Bindings;
import com.tungsten.fclcore.fakefx.beans.binding.ObjectBinding; import com.tungsten.fclcore.fakefx.beans.binding.ObjectBinding;
import com.tungsten.fclcore.game.Arguments; import com.tungsten.fclcore.game.Arguments;
import com.tungsten.fclcore.game.LaunchOptions; import com.tungsten.fclcore.game.LaunchOptions;
import com.tungsten.fclcore.util.Logging;
import com.tungsten.fclcore.util.StringUtils; import com.tungsten.fclcore.util.StringUtils;
import com.tungsten.fclcore.util.ToStringBuilder; import com.tungsten.fclcore.util.ToStringBuilder;
import com.tungsten.fclcore.util.gson.UUIDTypeAdapter; import com.tungsten.fclcore.util.gson.UUIDTypeAdapter;
@ -174,8 +180,23 @@ public class OfflineAccount extends Account {
@Override @Override
public ObjectBinding<Optional<Map<TextureType, Texture>>> getTextures() { public ObjectBinding<Optional<Map<TextureType, Texture>>> getTextures() {
try {
Skin.LoadedSkin loadedSkin = skin.load(username).run();
Map<TextureType, Texture> map = new HashMap<>();
if (loadedSkin != null) {
map.put(TextureType.SKIN, new Texture(null, null, BitmapFactory.decodeStream(loadedSkin.getSkin().getInputStream())));
if (loadedSkin.getCape() != null) {
map.put(TextureType.CAPE, new Texture(null, null, BitmapFactory.decodeStream(loadedSkin.getCape().getInputStream())));
}
} else {
map.put(TextureType.SKIN, new Texture(null, null, BitmapFactory.decodeStream(OfflineAccount.class.getResourceAsStream(TextureModel.detectUUID(uuid) == TextureModel.ALEX ? "/assets/img/alex.img" : "/assets/img/steve.png"))));
}
return Bindings.createObjectBinding(() -> Optional.of(map));
} catch (Exception e) {
Logging.LOG.log(Level.WARNING, "Failed to load offline account skin, error: " + e);
return super.getTextures(); return super.getTextures();
} }
}
@Override @Override
public String toString() { public String toString() {

View File

@ -1,5 +1,9 @@
package com.tungsten.fclcore.auth.yggdrasil; package com.tungsten.fclcore.auth.yggdrasil;
import android.graphics.Bitmap;
import com.google.gson.annotations.Expose;
import org.jetbrains.annotations.Nullable; import org.jetbrains.annotations.Nullable;
import java.util.Map; import java.util.Map;
@ -8,14 +12,17 @@ public final class Texture {
private final String url; private final String url;
private final Map<String, String> metadata; private final Map<String, String> metadata;
@Expose(serialize = false)
private final Bitmap img;
public Texture() { public Texture() {
this(null, null); this(null, null, null);
} }
public Texture(String url, Map<String, String> metadata) { public Texture(String url, Map<String, String> metadata, Bitmap img) {
this.url = url; this.url = url;
this.metadata = metadata; this.metadata = metadata;
this.img = img;
} }
@Nullable @Nullable
@ -27,4 +34,8 @@ public final class Texture {
public Map<String, String> getMetadata() { public Map<String, String> getMetadata() {
return metadata; return metadata;
} }
public Bitmap getImg() {
return img;
}
} }

View File

@ -19,18 +19,18 @@ public class LocaleUtils {
*/ */
public static boolean isChinese(Context context) { public static boolean isChinese(Context context) {
SharedPreferences sharedPreferences = context.getSharedPreferences("lang", Context.MODE_PRIVATE); SharedPreferences sharedPreferences = context.getSharedPreferences("launcher", Context.MODE_PRIVATE);
int lang = sharedPreferences.getInt("lang", 0); int lang = sharedPreferences.getInt("lang", 0);
return lang == 2 || (lang == 0 && getSystemLocale() == Locale.CHINA); return lang == 2 || (lang == 0 && getSystemLocale() == Locale.CHINA);
} }
public static Context setLanguage(Context context){ public static Context setLanguage(Context context){
SharedPreferences sharedPreferences = context.getSharedPreferences("lang", Context.MODE_PRIVATE); SharedPreferences sharedPreferences = context.getSharedPreferences("launcher", Context.MODE_PRIVATE);
return updateResources(context, sharedPreferences.getInt("lang", 0)); return updateResources(context, sharedPreferences.getInt("lang", 0));
} }
public static void changeLanguage(Context context, int lang) { public static void changeLanguage(Context context, int lang) {
SharedPreferences sharedPreferences = context.getSharedPreferences("lang", Context.MODE_PRIVATE); SharedPreferences sharedPreferences = context.getSharedPreferences("launcher", Context.MODE_PRIVATE);
@SuppressLint("CommitPrefEdits") SharedPreferences.Editor editor = sharedPreferences.edit(); @SuppressLint("CommitPrefEdits") SharedPreferences.Editor editor = sharedPreferences.edit();
editor.putInt("lang", lang); editor.putInt("lang", lang);
editor.apply(); editor.apply();

View File

@ -5,6 +5,7 @@ import android.view.LayoutInflater;
import android.view.View; import android.view.View;
import androidx.annotation.LayoutRes; import androidx.annotation.LayoutRes;
import androidx.annotation.NonNull;
import com.tungsten.fcllibrary.component.FCLActivity; import com.tungsten.fcllibrary.component.FCLActivity;
@ -36,8 +37,9 @@ public abstract class FCLBaseUI implements FCLUILifecycleCallbacks {
return contentView; return contentView;
} }
@NonNull
public final <T extends View> T findViewById(int id) { public final <T extends View> T findViewById(int id) {
return contentView == null ? null : contentView.findViewById(id); return contentView.findViewById(id);
} }
public abstract boolean isShowing(); public abstract boolean isShowing();

View File

@ -6,6 +6,7 @@ import android.graphics.BitmapFactory;
import android.opengl.GLSurfaceView; import android.opengl.GLSurfaceView;
import android.opengl.GLU; import android.opengl.GLU;
import android.os.SystemClock; import android.os.SystemClock;
import android.util.Log;
import javax.microedition.khronos.egl.EGLConfig; import javax.microedition.khronos.egl.EGLConfig;
import javax.microedition.khronos.opengles.GL10; import javax.microedition.khronos.opengles.GL10;
@ -69,6 +70,7 @@ public class MinecraftSkinRenderer implements GLSurfaceView.Renderer {
} }
public void onDrawFrame(final GL10 gl10) { public void onDrawFrame(final GL10 gl10) {
// Log.i("draw", "start");
if (this.changeSkinImage) { if (this.changeSkinImage) {
this.changeSkinImage = false; this.changeSkinImage = false;
} }

View File

@ -17,6 +17,7 @@ public class TextureHelper {
} }
public static int[] loadGLTextureFromBitmap(final Bitmap skin, final Bitmap cape, final GL10 gl10) { public static int[] loadGLTextureFromBitmap(final Bitmap skin, final Bitmap cape, final GL10 gl10) {
// Log.i("loadTex", "start");
final int[] array = { 0 , 0 }; final int[] array = { 0 , 0 };
gl10.glGenTextures(2, array, 0); gl10.glGenTextures(2, array, 0);
gl10.glBindTexture(3553, array[0]); gl10.glBindTexture(3553, array[0]);
@ -33,7 +34,7 @@ public class TextureHelper {
throw new RuntimeException("Error loading texture."); throw new RuntimeException("Error loading texture.");
} }
else { else {
Log.e("loadTex","success"); Log.i("loadTex", "success");
} }
return array; return array;
} }