commit dea2db8cb872ca85210a2143cdf2d95adbf6d45b
Author: Phoen1x <2284257190@qq.com>
Date: Sat Nov 8 20:29:38 2025 +0800
first
diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..aa724b7
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,15 @@
+*.iml
+.gradle
+/local.properties
+/.idea/caches
+/.idea/libraries
+/.idea/modules.xml
+/.idea/workspace.xml
+/.idea/navEditor.xml
+/.idea/assetWizardSettings.xml
+.DS_Store
+/build
+/captures
+.externalNativeBuild
+.cxx
+local.properties
diff --git a/app/.gitignore b/app/.gitignore
new file mode 100644
index 0000000..42afabf
--- /dev/null
+++ b/app/.gitignore
@@ -0,0 +1 @@
+/build
\ No newline at end of file
diff --git a/app/build.gradle.kts b/app/build.gradle.kts
new file mode 100644
index 0000000..f8e7c7f
--- /dev/null
+++ b/app/build.gradle.kts
@@ -0,0 +1,57 @@
+plugins {
+ alias(libs.plugins.android.application)
+ alias(libs.plugins.kotlin.android)
+}
+
+android {
+ namespace = "com.phoenix.gui"
+ compileSdk = 36
+
+ defaultConfig {
+ applicationId = "com.phoenix.gui"
+ minSdk = 24
+ targetSdk = 36
+ versionCode = 1
+ versionName = "1.0"
+ testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
+ }
+
+ buildTypes {
+ release {
+ isMinifyEnabled = false
+ proguardFiles(
+ getDefaultProguardFile("proguard-android-optimize.txt"),
+ "proguard-rules.pro"
+ )
+ }
+ }
+
+ compileOptions {
+ sourceCompatibility = JavaVersion.VERSION_11
+ targetCompatibility = JavaVersion.VERSION_11
+ }
+
+ kotlinOptions {
+ jvmTarget = "11"
+ }
+
+ buildFeatures {
+ viewBinding = true
+ }
+
+ // for debug
+ packaging {
+ jniLibs.keepDebugSymbols += "**/*.so"
+ }
+}
+
+dependencies {
+ implementation(libs.androidx.core.ktx)
+ implementation(libs.androidx.lifecycle.runtime.ktx)
+ implementation(libs.androidx.appcompat)
+ implementation(libs.androidx.activity)
+ implementation(libs.material)
+ testImplementation(libs.junit)
+ androidTestImplementation(libs.androidx.junit)
+ androidTestImplementation(libs.androidx.espresso.core)
+}
diff --git a/app/proguard-rules.pro b/app/proguard-rules.pro
new file mode 100644
index 0000000..481bb43
--- /dev/null
+++ b/app/proguard-rules.pro
@@ -0,0 +1,21 @@
+# Add project specific ProGuard rules here.
+# You can control the set of applied configuration files using the
+# proguardFiles setting in build.gradle.
+#
+# For more details, see
+# http://developer.android.com/guide/developing/tools/proguard.html
+
+# If your project uses WebView with JS, uncomment the following
+# and specify the fully qualified class name to the JavaScript interface
+# class:
+#-keepclassmembers class fqcn.of.javascript.interface.for.webview {
+# public *;
+#}
+
+# Uncomment this to preserve the line number information for
+# debugging stack traces.
+#-keepattributes SourceFile,LineNumberTable
+
+# If you keep the line number information, uncomment this to
+# hide the original source file name.
+#-renamesourcefileattribute SourceFile
\ No newline at end of file
diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml
new file mode 100644
index 0000000..8d88d8b
--- /dev/null
+++ b/app/src/main/AndroidManifest.xml
@@ -0,0 +1,45 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/java/com/phoenix/gui/MainActivity.java b/app/src/main/java/com/phoenix/gui/MainActivity.java
new file mode 100644
index 0000000..9f74b62
--- /dev/null
+++ b/app/src/main/java/com/phoenix/gui/MainActivity.java
@@ -0,0 +1,424 @@
+package com.phoenix.gui;
+
+import android.content.*;
+import android.graphics.Color;
+import android.os.*;
+import android.view.View;
+import android.view.Gravity;
+import android.view.ViewGroup;
+import android.view.Window;
+import android.widget.*;
+import androidx.activity.ComponentActivity;
+import com.phoenix.gui.config.ConfigManager;
+import com.phoenix.gui.module.Module;
+import com.phoenix.gui.module.ModuleManager;
+import com.phoenix.gui.module.ModuleToggleListener;
+import com.phoenix.gui.module.ShortcutToggleListener;
+import com.phoenix.gui.ui.ArraylistModule;
+import com.phoenix.gui.ui.ArraylistView;
+import com.phoenix.gui.ui.DynamicColorExtractor;
+import com.phoenix.gui.ui.FloatBallView;
+import com.phoenix.gui.ui.ShortcutButton;
+import com.phoenix.gui.ui.dynamic.DynamicIslandWindow;
+import com.phoenix.gui.ui.ThemeManager;
+import com.phoenix.gui.ui.UI;
+
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.Random;
+
+public class MainActivity extends ComponentActivity implements ModuleToggleListener, ShortcutToggleListener {
+
+ private boolean wifiState = true;
+
+ private Button btnStart, btnStop, btnTestProgress, btnTestSwitch, btnHideAll, btnSave, btnToggleDebug, btnToggleArraylist;
+ private Button btnAddRandom, btnRemoveRandom, btnResetArraylist;
+ private EditText etUsername;
+ private SeekBar seekScale;
+ private ScrollView debugPanel;
+ private FrameLayout shortcutsContainer;
+
+ private final Map shortcutButtons = new HashMap<>();
+ private final List allTestModules = new ArrayList<>();
+ private final Random random = new Random();
+
+ @Override
+ protected void onCreate(Bundle b) {
+ super.onCreate(b);
+ requestWindowFeature(Window.FEATURE_NO_TITLE);
+
+ View decorView = getWindow().getDecorView();
+ int uiOptions = View.SYSTEM_UI_FLAG_HIDE_NAVIGATION
+ | View.SYSTEM_UI_FLAG_FULLSCREEN
+ | View.SYSTEM_UI_FLAG_IMMERSIVE_STICKY;
+ decorView.setSystemUiVisibility(uiOptions);
+
+ setContentView(R.layout.activity_main);
+
+ UI.init(this); // 封装
+
+ initAllTestModules();
+ bindViews();
+ loadConfig();
+ setListeners();
+ updateBtnState();
+
+ toast("MainActivity ready");
+ }
+
+ private void initAllTestModules() {
+ // 下面那个随机测试的,可以随便填点啥
+ }
+
+ private void bindViews() {
+ btnStart = findViewById(R.id.btn_start);
+ btnStop = findViewById(R.id.btn_stop);
+ btnTestProgress = findViewById(R.id.btn_test_progress);
+ btnTestSwitch = findViewById(R.id.btn_test_switch);
+ btnHideAll = findViewById(R.id.btn_hide_all);
+ btnSave = findViewById(R.id.btn_save);
+ btnToggleDebug = findViewById(R.id.btn_toggle_debug);
+ btnToggleArraylist = findViewById(R.id.btn_toggle_arraylist);
+ btnAddRandom = findViewById(R.id.btn_add_random_module);
+ btnRemoveRandom = findViewById(R.id.btn_remove_random_module);
+ btnResetArraylist = findViewById(R.id.btn_reset_arraylist);
+
+ etUsername = findViewById(R.id.et_username);
+ seekScale = findViewById(R.id.seek_scale);
+ debugPanel = findViewById(R.id.debug_panel);
+ shortcutsContainer = findViewById(R.id.shortcuts_container);
+ }
+
+ private void loadConfig() {
+ etUsername.setText(ConfigManager.getDynamicIslandUsername());
+ seekScale.setProgress((int) (ConfigManager.getDynamicIslandScale() * 100));
+ }
+
+ private void saveConfig() {
+ String name = etUsername.getText().toString().trim();
+ float scale = seekScale.getProgress() / 100f;
+
+ ConfigManager.setDynamicIslandUsername(name);
+ ConfigManager.setDynamicIslandScale(scale);
+
+ DynamicIslandWindow island = UI.getDynamicIsland();
+ if (island != null && island.isShowing()) {
+ island.updateConfig(scale, name);
+ }
+
+ toast("saved");
+ }
+
+ private void setListeners() {
+ btnStart.setOnClickListener(v -> startGUI());
+ btnStop.setOnClickListener(v -> stopGUI());
+ btnSave.setOnClickListener(v -> saveConfig());
+ btnToggleDebug.setOnClickListener(v -> toggleDebugPanel());
+
+ if (btnToggleArraylist != null) {
+ btnToggleArraylist.setOnClickListener(v -> toggleArraylist());
+ }
+
+ if (btnAddRandom != null) {
+ btnAddRandom.setOnClickListener(v -> addRandomModule());
+ }
+
+ if (btnRemoveRandom != null) {
+ btnRemoveRandom.setOnClickListener(v -> removeRandomModule());
+ }
+
+ if (btnResetArraylist != null) {
+ btnResetArraylist.setOnClickListener(v -> {
+ addDefaultArraylistModules();
+ toast("Arraylist reset");
+ });
+ }
+
+ btnTestProgress.setOnClickListener(v -> {
+ showProgress();
+ toast("test progress");
+ });
+
+ btnTestSwitch.setOnClickListener(v -> showSwitch());
+
+ btnHideAll.setOnClickListener(v -> {
+ hideAll();
+ toast("hide all");
+ });
+ }
+
+ private void updateBtnState() {
+ boolean isRunning = UI.isShowing();
+ btnStart.setEnabled(!isRunning);
+ btnStop.setEnabled(isRunning);
+ btnTestProgress.setEnabled(isRunning);
+ btnTestSwitch.setEnabled(isRunning);
+ btnHideAll.setEnabled(isRunning);
+
+ boolean arraylistControlsEnabled = isRunning && UI.getArraylistView() != null;
+
+ if (btnToggleArraylist != null) {
+ btnToggleArraylist.setEnabled(isRunning);
+ }
+
+ if (btnAddRandom != null) {
+ btnAddRandom.setEnabled(arraylistControlsEnabled);
+ }
+
+ if (btnRemoveRandom != null) {
+ btnRemoveRandom.setEnabled(arraylistControlsEnabled);
+ }
+
+ if (btnResetArraylist != null) {
+ btnResetArraylist.setEnabled(arraylistControlsEnabled);
+ }
+ }
+
+ private void startGUI() {
+ if (UI.isShowing()) return;
+
+ try {
+ UI.show(this);
+ updateBtnState();
+ toast("GUI started");
+ } catch (Exception e) {
+ e.printStackTrace();
+ toast("Failed to start GUI: " + e.getMessage());
+ }
+ }
+
+ private void stopGUI() {
+ if (!UI.isShowing()) return;
+
+ try {
+ if (!shortcutButtons.isEmpty()) {
+ for (ShortcutButton button : shortcutButtons.values()) {
+ try {
+ button.destroy();
+ } catch (Exception e) {
+ e.printStackTrace();
+ }
+ }
+ shortcutButtons.clear();
+ shortcutsContainer.removeAllViews();
+ }
+
+ UI.hide();
+ updateBtnState();
+ toast("GUI stopped");
+ } catch (Exception e) {
+ e.printStackTrace();
+ toast("Failed to stop GUI: " + e.getMessage());
+ }
+ }
+
+ private void addDefaultArraylistModules() {
+ ArraylistView arraylistView = UI.getArraylistView();
+ if (arraylistView == null) return;
+
+ arraylistView.clearModules(); // 是的你没看错这个才是真的能用的
+ // arraylistView.addModule(new ArraylistModule("like this"));
+ }
+
+ private void addRandomModule() {
+ ArraylistView arraylistView = UI.getArraylistView();
+ if (arraylistView == null || allTestModules.isEmpty()) return;
+
+ String moduleName = allTestModules.get(random.nextInt(allTestModules.size()));
+ arraylistView.addModule(new ArraylistModule(moduleName));
+ toast("Added: " + moduleName);
+ }
+
+ private void removeRandomModule() {
+ ArraylistView arraylistView = UI.getArraylistView();
+ if (arraylistView != null && !arraylistView.getModules().isEmpty()) {
+ int indexToRemove = random.nextInt(arraylistView.getModules().size());
+ String moduleName = arraylistView.getModules().get(indexToRemove).getName();
+ arraylistView.removeModule(moduleName);
+ toast("Removed: " + moduleName);
+ } else {
+ toast("Arraylist is empty");
+ }
+ }
+
+ private void toggleArraylist() {
+ ArraylistView arraylist = UI.getArraylistView();
+ if (arraylist == null) {
+ toast("请先启动 GUI");
+ } else {
+ if (arraylist.getVisibility() == View.VISIBLE) {
+ arraylist.hide();
+ toast("Arraylist 已隐藏");
+ } else {
+ arraylist.show();
+ toast("Arraylist 已显示");
+ }
+ }
+ updateBtnState();
+ }
+
+ private void toggleDynamicIsland() {
+ DynamicIslandWindow island = UI.getDynamicIsland();
+ if (island == null) {
+ ConfigManager.setDynamicIslandEnabled(true);
+ toast("请重启 GUI 显示灵动岛");
+ } else {
+ try {
+ island.hide();
+ ConfigManager.setDynamicIslandEnabled(false);
+ toast("灵动岛已隐藏");
+ } catch (Exception e) {
+ e.printStackTrace();
+ }
+ }
+ }
+
+ private void showProgress() {
+ DynamicIslandWindow island = UI.getDynamicIsland();
+ if (island == null || !island.isShowing()) return;
+
+ try {
+ island.addOrUpdateProgress("test_progress", "progress", "im subtitle", null, 0.65f, null);
+ } catch (Exception e) {
+ e.printStackTrace();
+ }
+ }
+
+ private void showSwitch() {
+ DynamicIslandWindow island = UI.getDynamicIsland();
+ if (island == null || !island.isShowing()) return;
+
+ try {
+ wifiState = !wifiState;
+ island.addSwitch("test_switch", "im a function", wifiState);
+ toast("test switch -> " + (wifiState ? "ON" : "OFF"));
+ } catch (Exception e) {
+ e.printStackTrace();
+ }
+ }
+
+ private void hideAll() {
+ DynamicIslandWindow island = UI.getDynamicIsland();
+ if (island != null && island.isShowing()) {
+ try {
+ island.hideAllTasks();
+ } catch (Exception e) {
+ e.printStackTrace();
+ }
+ }
+ }
+
+ private void toggleDebugPanel() {
+ if (debugPanel.getVisibility() == View.VISIBLE) {
+ debugPanel.setVisibility(View.GONE);
+ } else {
+ debugPanel.setVisibility(View.VISIBLE);
+ }
+ }
+
+ @Override
+ public void onModuleToggled(Module module) {
+ DynamicIslandWindow island = UI.getDynamicIsland();
+ if (island != null && island.isShowing()) {
+ try {
+ island.addSwitch("module_" + module.getName(), module.getName(), module.isEnabled());
+ } catch (Exception e) {
+ e.printStackTrace();
+ }
+ }
+
+ ShortcutButton button = shortcutButtons.get(module);
+ if (button != null) {
+ try {
+ button.updateStyle();
+ } catch (Exception e) {
+ e.printStackTrace();
+ }
+ }
+
+ ArraylistView arraylistView = UI.getArraylistView();
+ if (arraylistView != null) {
+ try {
+ if (module.isEnabled()) {
+ arraylistView.addModule(module.getName());
+ } else {
+ arraylistView.removeModule(module.getName());
+ }
+ } catch (Exception e) {
+ e.printStackTrace();
+ }
+ }
+ }
+
+ @Override
+ public void onShortcutToggled(Module module, boolean enabled) {
+ if (enabled) {
+ createShortcut(module);
+ } else {
+ removeShortcut(module);
+ }
+ }
+
+ private void createShortcut(Module module) {
+ if (!shortcutButtons.containsKey(module)) {
+ try {
+ ShortcutButton button = new ShortcutButton(this, module);
+ FrameLayout.LayoutParams params = new FrameLayout.LayoutParams(
+ FrameLayout.LayoutParams.WRAP_CONTENT,
+ FrameLayout.LayoutParams.WRAP_CONTENT,
+ Gravity.CENTER
+ );
+ button.setLayoutParams(params);
+ shortcutsContainer.addView(button);
+ button.show();
+ shortcutButtons.put(module, button);
+ } catch (Exception e) {
+ e.printStackTrace();
+ toast("创建快捷按钮失败: " + e.getMessage());
+ }
+ }
+ }
+
+ private void removeShortcut(Module module) {
+ ShortcutButton button = shortcutButtons.get(module);
+ if (button != null) {
+ try {
+ button.hide();
+ button.postDelayed(() -> {
+ shortcutsContainer.removeView(button);
+ button.destroy();
+ }, 250);
+ } catch (Exception e) {
+ e.printStackTrace();
+ }
+ shortcutButtons.remove(module);
+ }
+ }
+
+ private void toast(String s) {
+ Toast.makeText(this, s, Toast.LENGTH_SHORT).show();
+ android.util.Log.d("PHOENIX", s); // me。
+ }
+
+ @Override
+ protected void onDestroy() {
+ super.onDestroy();
+
+ if (UI.isShowing()) {
+ stopGUI();
+ }
+
+ UI.cleanup();
+ }
+
+ @Override
+ public void onBackPressed() {
+ if (UI.isShowing()) {
+ stopGUI(); // 回家吃饭
+ } else {
+ super.onBackPressed();
+ }
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/phoenix/gui/config/ConfigManager.java b/app/src/main/java/com/phoenix/gui/config/ConfigManager.java
new file mode 100644
index 0000000..01ae585
--- /dev/null
+++ b/app/src/main/java/com/phoenix/gui/config/ConfigManager.java
@@ -0,0 +1,108 @@
+package com.phoenix.gui.config;
+
+import android.content.Context;
+import android.content.SharedPreferences;
+
+public class ConfigManager {
+
+ private static final String PREF_NAME = "phoenix_gui_config";
+ private static SharedPreferences sharedPreferences;
+
+ private static final String KEY_DYNAMIC_ISLAND_ENABLED = "dynamic_island_enabled";
+ private static final String KEY_DYNAMIC_ISLAND_SCALE = "dynamic_island_scale";
+ private static final String KEY_DYNAMIC_ISLAND_USERNAME = "dynamic_island_username";
+
+ // 初始化
+ public static void init(Context context) {
+ if (sharedPreferences == null) {
+ sharedPreferences = context.getSharedPreferences(PREF_NAME, Context.MODE_PRIVATE);
+ }
+ }
+
+ private static String getKey(String moduleName, String configKey) {
+ return moduleName + "_" + configKey;
+ }
+ private static SharedPreferences getPrefs() {
+ if (sharedPreferences == null) {
+ throw new IllegalStateException("ConfigManager not initialized. Call init() first.");
+ }
+ return sharedPreferences;
+ }
+ public static float getFloat(String moduleName, String key, float defaultValue) {
+ try {
+ return getPrefs().getFloat(moduleName + "_" + key, defaultValue);
+ } catch (ClassCastException e) {
+
+ try {
+ return (float) getPrefs().getInt(moduleName + "_" + key, (int) defaultValue);
+ } catch (Exception e2) {
+ return defaultValue;
+ }
+ }
+ }
+
+ public static void saveInt(String moduleName, String configKey, int value) {
+ if (sharedPreferences == null) return;
+ sharedPreferences.edit().putInt(getKey(moduleName, configKey), value).apply();
+ }
+
+ public static int getInt(String moduleName, String configKey, int defaultValue) {
+ if (sharedPreferences == null) return defaultValue;
+ return sharedPreferences.getInt(getKey(moduleName, configKey), defaultValue);
+ }
+ public static void saveBoolean(String moduleName, String configKey, boolean value) {
+ if (sharedPreferences == null) return;
+ sharedPreferences.edit().putBoolean(getKey(moduleName, configKey), value).apply();
+ }
+
+ public static boolean getBoolean(String moduleName, String configKey, boolean defaultValue) {
+ if (sharedPreferences == null) return defaultValue;
+ return sharedPreferences.getBoolean(getKey(moduleName, configKey), defaultValue);
+ }
+
+ public static void saveModuleConfig(String moduleName, String configKey, int value) {
+ saveInt(moduleName, configKey, value);
+ }
+
+ public static void saveModuleConfig(String moduleName, String configKey, boolean value) {
+ saveBoolean(moduleName, configKey, value);
+ }
+
+ public static boolean getDynamicIslandEnabled() {
+ return getPrefs().getBoolean(KEY_DYNAMIC_ISLAND_ENABLED, true);
+ }
+
+ public static void setDynamicIslandEnabled(boolean enabled) {
+ getPrefs().edit()
+ .putBoolean(KEY_DYNAMIC_ISLAND_ENABLED, enabled)
+ .apply();
+ }
+
+ public static float getDynamicIslandScale() {
+ return getPrefs().getFloat(KEY_DYNAMIC_ISLAND_SCALE, 0.7f);
+ }
+
+ public static void setDynamicIslandScale(float scale) {
+ getPrefs().edit()
+ .putFloat(KEY_DYNAMIC_ISLAND_SCALE, scale)
+ .apply();
+ }
+
+ public static String getDynamicIslandUsername() {
+ return getPrefs().getString(KEY_DYNAMIC_ISLAND_USERNAME, "User");
+ }
+
+ public static void setDynamicIslandUsername(String username) {
+ getPrefs().edit()
+ .putString(KEY_DYNAMIC_ISLAND_USERNAME, username)
+ .apply();
+ }
+
+ public static void clearAll() {
+ getPrefs().edit().clear().apply();
+ }
+
+ private ConfigManager() {
+
+ }
+}
diff --git a/app/src/main/java/com/phoenix/gui/module/Module.java b/app/src/main/java/com/phoenix/gui/module/Module.java
new file mode 100644
index 0000000..03e50b0
--- /dev/null
+++ b/app/src/main/java/com/phoenix/gui/module/Module.java
@@ -0,0 +1,91 @@
+package com.phoenix.gui.module;
+
+import com.phoenix.gui.ui.widgets.SubMenuPanel;
+
+public abstract class Module {
+ private final String name;
+ private final ModuleCategory category;
+ private final String description;
+ private boolean isEnabled = false;
+
+ private boolean shortcutEnabled = false;
+
+ public Module(String name, ModuleCategory category, String description) {
+ this.name = name;
+ this.category = category;
+ this.description = description;
+ }
+
+ public Module(String name, ModuleCategory category) {
+ this(name, category, "");
+ }
+
+ public String getName() {
+ return name;
+ }
+
+ public ModuleCategory getCategory() {
+ return category;
+ }
+
+ public String getDescription() {
+ return description;
+ }
+
+ public boolean isEnabled() {
+ return isEnabled;
+ }
+
+ public boolean isShortcutEnabled() {
+ return shortcutEnabled;
+ }
+
+ public boolean supportsShortcut() {
+ return true;
+ }
+
+ public void toggle() {
+ if (isEnabled) {
+ disable();
+ } else {
+ enable();
+ }
+ }
+
+ public void enable() {
+ if (!isEnabled) {
+ isEnabled = true;
+ onEnable();
+ ModuleManager.notifyModuleToggled(this);
+ }
+ }
+
+ public void disable() {
+ if (isEnabled) {
+ isEnabled = false;
+ onDisable();
+ ModuleManager.notifyModuleToggled(this);
+ }
+ }
+
+ protected void onEnable() {}
+
+ protected void onDisable() {}
+
+ public void configureSubMenu(SubMenuPanel subMenu) {
+
+ if (supportsShortcut()) {
+ subMenu.addShortcutSwitch(shortcutEnabled);
+ }
+ }
+
+ protected void onShortcutToggled(boolean enabled) {}
+
+ public void toggleShortcut(boolean enabled) {
+ if (shortcutEnabled != enabled) {
+ shortcutEnabled = enabled;
+ onShortcutToggled(enabled);
+ ModuleManager.notifyShortcutToggled(this, enabled);
+ }
+ }
+}
diff --git a/app/src/main/java/com/phoenix/gui/module/ModuleCategory.java b/app/src/main/java/com/phoenix/gui/module/ModuleCategory.java
new file mode 100644
index 0000000..11cd2a6
--- /dev/null
+++ b/app/src/main/java/com/phoenix/gui/module/ModuleCategory.java
@@ -0,0 +1,19 @@
+package com.phoenix.gui.module;
+
+public enum ModuleCategory {
+ COMBAT("Combat"),
+ MOVEMENT("Movement"),
+ PLAYER("Player"),
+ VISUAL("Visual"),
+ WORLD("World");
+
+ private final String displayName;
+
+ ModuleCategory(String displayName) {
+ this.displayName = displayName;
+ }
+
+ public String getDisplayName() {
+ return displayName;
+ }
+}
diff --git a/app/src/main/java/com/phoenix/gui/module/ModuleManager.java b/app/src/main/java/com/phoenix/gui/module/ModuleManager.java
new file mode 100644
index 0000000..3b38125
--- /dev/null
+++ b/app/src/main/java/com/phoenix/gui/module/ModuleManager.java
@@ -0,0 +1,124 @@
+package com.phoenix.gui.module;
+
+import com.phoenix.gui.module.impl.combat.*;
+import com.phoenix.gui.module.impl.movement.*;
+import com.phoenix.gui.module.impl.player.*;
+import com.phoenix.gui.module.impl.visual.*;
+import com.phoenix.gui.module.impl.world.*;
+
+import java.util.ArrayList;
+import java.util.List;
+
+public class ModuleManager {
+ private static final List modules = new ArrayList<>();
+ private static final List listeners = new ArrayList<>();
+ private static final List shortcutListeners = new ArrayList<>();
+
+ static {
+ registerModules();
+ }
+
+ private static void registerModules() { //注册
+
+ register(new HitBoxModule());
+ register(new InfiniteAuraModule());
+ register(new KillAuraModule());
+ register(new FightBotModule());
+ register(new RideModule());
+ register(new LockBackModule());
+
+ register(new AirJumpModule());
+ register(new SpeedModule());
+ register(new FastStopModule());
+ register(new ScaffoldModule());
+ register(new JekBackModule());
+ register(new FlyModule());
+
+ register(new AntiBotModule());
+ register(new BlinkModule());
+ register(new NameTagModule());
+ register(new PhasesModule());
+ register(new TeleportModule());
+
+ register(new ThemeModule());
+ register(new FovModule());
+ register(new DisableModule());
+ register(new GodModeModule());
+ register(new OnPosModule());
+ register(new InfiniteYModule());
+
+
+ register(new FuckerModule());
+ register(new RemoteStopModule());
+ register(new GameModeModule());
+ register(new AutoClickModule());
+ register(new AimAssetsModule());
+ }
+
+ private static void register(Module module) {
+ modules.add(module);
+ }
+
+ public static List getAllModules() {
+ return new ArrayList<>(modules);
+ }
+
+ public static List getModulesByCategory(ModuleCategory category) {
+ List result = new ArrayList<>();
+ for (Module module : modules) {
+ if (module.getCategory() == category) {
+ result.add(module);
+ }
+ }
+ return result;
+ }
+
+ public static Module getModuleByName(String name) {
+ for (Module module : modules) {
+ if (module.getName().equalsIgnoreCase(name)) {
+ return module;
+ }
+ }
+ return null;
+ }
+
+ public static void addToggleListener(ModuleToggleListener listener) {
+ listeners.add(listener);
+ }
+
+ public static void removeToggleListener(ModuleToggleListener listener) {
+ listeners.remove(listener);
+ }
+
+ public static void addShortcutListener(ShortcutToggleListener listener) {
+ shortcutListeners.add(listener);
+ }
+
+ public static void removeShortcutListener(ShortcutToggleListener listener) {
+ shortcutListeners.remove(listener);
+ }
+
+ public static void notifyModuleToggled(Module module) {
+ for (ModuleToggleListener listener : listeners) {
+ listener.onModuleToggled(module);
+ }
+ }
+
+ public static void notifyShortcutToggled(Module module, boolean enabled) {
+ for (ShortcutToggleListener listener : shortcutListeners) {
+ listener.onShortcutToggled(module, enabled);
+ }
+ }
+
+ public static void disableAll() {
+ for (Module module : modules) {
+ if (module.isEnabled()) {
+ module.disable();
+ }
+ }
+ }
+
+ private ModuleManager() {
+
+ }
+}
diff --git a/app/src/main/java/com/phoenix/gui/module/ModuleToggleListener.java b/app/src/main/java/com/phoenix/gui/module/ModuleToggleListener.java
new file mode 100644
index 0000000..9becf9e
--- /dev/null
+++ b/app/src/main/java/com/phoenix/gui/module/ModuleToggleListener.java
@@ -0,0 +1,5 @@
+package com.phoenix.gui.module;
+
+public interface ModuleToggleListener {
+ void onModuleToggled(Module module);
+}
diff --git a/app/src/main/java/com/phoenix/gui/module/ShortcutToggleListener.java b/app/src/main/java/com/phoenix/gui/module/ShortcutToggleListener.java
new file mode 100644
index 0000000..8b31984
--- /dev/null
+++ b/app/src/main/java/com/phoenix/gui/module/ShortcutToggleListener.java
@@ -0,0 +1,5 @@
+package com.phoenix.gui.module;
+
+public interface ShortcutToggleListener {
+ void onShortcutToggled(Module module, boolean enabled);
+}
diff --git a/app/src/main/java/com/phoenix/gui/module/impl/combat/FightBotModule.java b/app/src/main/java/com/phoenix/gui/module/impl/combat/FightBotModule.java
new file mode 100644
index 0000000..e62923f
--- /dev/null
+++ b/app/src/main/java/com/phoenix/gui/module/impl/combat/FightBotModule.java
@@ -0,0 +1,20 @@
+package com.phoenix.gui.module.impl.combat;
+
+import com.phoenix.gui.module.Module;
+import com.phoenix.gui.module.ModuleCategory;
+
+public class FightBotModule extends Module {
+ public FightBotModule() {
+ super("FightBot", ModuleCategory.COMBAT);
+ }
+
+ @Override
+ protected void onEnable() {
+
+ }
+
+ @Override
+ protected void onDisable() {
+
+ }
+}
diff --git a/app/src/main/java/com/phoenix/gui/module/impl/combat/HitBoxModule.java b/app/src/main/java/com/phoenix/gui/module/impl/combat/HitBoxModule.java
new file mode 100644
index 0000000..912bc04
--- /dev/null
+++ b/app/src/main/java/com/phoenix/gui/module/impl/combat/HitBoxModule.java
@@ -0,0 +1,20 @@
+package com.phoenix.gui.module.impl.combat;
+
+import com.phoenix.gui.module.Module;
+import com.phoenix.gui.module.ModuleCategory;
+
+public class HitBoxModule extends Module {
+ public HitBoxModule() {
+ super("HitBox", ModuleCategory.COMBAT);
+ }
+
+ @Override
+ protected void onEnable() {
+
+ }
+
+ @Override
+ protected void onDisable() {
+
+ }
+}
diff --git a/app/src/main/java/com/phoenix/gui/module/impl/combat/InfiniteAuraModule.java b/app/src/main/java/com/phoenix/gui/module/impl/combat/InfiniteAuraModule.java
new file mode 100644
index 0000000..e6551af
--- /dev/null
+++ b/app/src/main/java/com/phoenix/gui/module/impl/combat/InfiniteAuraModule.java
@@ -0,0 +1,20 @@
+package com.phoenix.gui.module.impl.combat;
+
+import com.phoenix.gui.module.Module;
+import com.phoenix.gui.module.ModuleCategory;
+
+public class InfiniteAuraModule extends Module {
+ public InfiniteAuraModule() {
+ super("InfiniteAura", ModuleCategory.COMBAT);
+ }
+
+ @Override
+ protected void onEnable() {
+
+ }
+
+ @Override
+ protected void onDisable() {
+
+ }
+}
diff --git a/app/src/main/java/com/phoenix/gui/module/impl/combat/KillAuraModule.java b/app/src/main/java/com/phoenix/gui/module/impl/combat/KillAuraModule.java
new file mode 100644
index 0000000..7615cb9
--- /dev/null
+++ b/app/src/main/java/com/phoenix/gui/module/impl/combat/KillAuraModule.java
@@ -0,0 +1,55 @@
+package com.phoenix.gui.module.impl.combat;
+
+import com.phoenix.gui.config.ConfigManager;
+import com.phoenix.gui.module.Module;
+import com.phoenix.gui.module.ModuleCategory;
+import com.phoenix.gui.ui.widgets.SubMenuPanel;
+
+import java.util.Arrays;
+
+public class KillAuraModule extends Module {
+ private float range = 4.0f;
+ private int cps = 10;
+ private int targetMode = 0;
+ private boolean autoBlock = false;
+
+ public KillAuraModule() {
+ super("KillAura", ModuleCategory.COMBAT, "囊囊囊");
+ }
+
+ @Override
+ protected void onEnable() {
+
+ }
+
+ @Override
+ protected void onDisable() {
+
+ }
+
+ @Override
+ public void configureSubMenu(SubMenuPanel subMenu) {
+
+ subMenu.addSlider("Range", 300, 800, (int)(range * 100), "%.2f", value -> {
+ range = value / 100.0f;
+
+ });
+
+ subMenu.addSlider("CPS", 1, 20, cps, "%d", value -> {
+ cps = value;
+
+ });
+
+ subMenu.addMode("Target", Arrays.asList("Single", "Multi"), targetMode, (index, mode) -> {
+ targetMode = index;
+
+ });
+
+ subMenu.addSwitch("Auto Block", autoBlock, enabled -> {
+ autoBlock = enabled;
+
+ });
+
+ super.configureSubMenu(subMenu);
+ }
+}
diff --git a/app/src/main/java/com/phoenix/gui/module/impl/combat/LockBackModule.java b/app/src/main/java/com/phoenix/gui/module/impl/combat/LockBackModule.java
new file mode 100644
index 0000000..645a86c
--- /dev/null
+++ b/app/src/main/java/com/phoenix/gui/module/impl/combat/LockBackModule.java
@@ -0,0 +1,20 @@
+package com.phoenix.gui.module.impl.combat;
+
+import com.phoenix.gui.module.Module;
+import com.phoenix.gui.module.ModuleCategory;
+
+public class LockBackModule extends Module {
+ public LockBackModule() {
+ super("LockBack", ModuleCategory.COMBAT);
+ }
+
+ @Override
+ protected void onEnable() {
+
+ }
+
+ @Override
+ protected void onDisable() {
+
+ }
+}
diff --git a/app/src/main/java/com/phoenix/gui/module/impl/combat/RideModule.java b/app/src/main/java/com/phoenix/gui/module/impl/combat/RideModule.java
new file mode 100644
index 0000000..861c7cc
--- /dev/null
+++ b/app/src/main/java/com/phoenix/gui/module/impl/combat/RideModule.java
@@ -0,0 +1,20 @@
+package com.phoenix.gui.module.impl.combat;
+
+import com.phoenix.gui.module.Module;
+import com.phoenix.gui.module.ModuleCategory;
+
+public class RideModule extends Module {
+ public RideModule() {
+ super("Ride", ModuleCategory.COMBAT);
+ }
+
+ @Override
+ protected void onEnable() {
+
+ }
+
+ @Override
+ protected void onDisable() {
+
+ }
+}
diff --git a/app/src/main/java/com/phoenix/gui/module/impl/movement/AirJumpModule.java b/app/src/main/java/com/phoenix/gui/module/impl/movement/AirJumpModule.java
new file mode 100644
index 0000000..d8e4d6a
--- /dev/null
+++ b/app/src/main/java/com/phoenix/gui/module/impl/movement/AirJumpModule.java
@@ -0,0 +1,20 @@
+package com.phoenix.gui.module.impl.movement;
+
+import com.phoenix.gui.module.Module;
+import com.phoenix.gui.module.ModuleCategory;
+
+public class AirJumpModule extends Module {
+ public AirJumpModule() {
+ super("AirJump", ModuleCategory.MOVEMENT);
+ }
+
+ @Override
+ protected void onEnable() {
+
+ }
+
+ @Override
+ protected void onDisable() {
+
+ }
+}
diff --git a/app/src/main/java/com/phoenix/gui/module/impl/movement/FastStopModule.java b/app/src/main/java/com/phoenix/gui/module/impl/movement/FastStopModule.java
new file mode 100644
index 0000000..608f0d5
--- /dev/null
+++ b/app/src/main/java/com/phoenix/gui/module/impl/movement/FastStopModule.java
@@ -0,0 +1,20 @@
+package com.phoenix.gui.module.impl.movement;
+
+import com.phoenix.gui.module.Module;
+import com.phoenix.gui.module.ModuleCategory;
+
+public class FastStopModule extends Module {
+ public FastStopModule() {
+ super("FastStop", ModuleCategory.MOVEMENT);
+ }
+
+ @Override
+ protected void onEnable() {
+
+ }
+
+ @Override
+ protected void onDisable() {
+
+ }
+}
diff --git a/app/src/main/java/com/phoenix/gui/module/impl/movement/FlyModule.java b/app/src/main/java/com/phoenix/gui/module/impl/movement/FlyModule.java
new file mode 100644
index 0000000..2a48c4e
--- /dev/null
+++ b/app/src/main/java/com/phoenix/gui/module/impl/movement/FlyModule.java
@@ -0,0 +1,20 @@
+package com.phoenix.gui.module.impl.movement;
+
+import com.phoenix.gui.module.Module;
+import com.phoenix.gui.module.ModuleCategory;
+
+public class FlyModule extends Module {
+ public FlyModule() {
+ super("Fly", ModuleCategory.MOVEMENT);
+ }
+
+ @Override
+ protected void onEnable() {
+
+ }
+
+ @Override
+ protected void onDisable() {
+
+ }
+}
diff --git a/app/src/main/java/com/phoenix/gui/module/impl/movement/JekBackModule.java b/app/src/main/java/com/phoenix/gui/module/impl/movement/JekBackModule.java
new file mode 100644
index 0000000..22c1058
--- /dev/null
+++ b/app/src/main/java/com/phoenix/gui/module/impl/movement/JekBackModule.java
@@ -0,0 +1,20 @@
+package com.phoenix.gui.module.impl.movement;
+
+import com.phoenix.gui.module.Module;
+import com.phoenix.gui.module.ModuleCategory;
+
+public class JekBackModule extends Module {
+ public JekBackModule() {
+ super("JekBack", ModuleCategory.MOVEMENT);
+ }
+
+ @Override
+ protected void onEnable() {
+
+ }
+
+ @Override
+ protected void onDisable() {
+
+ }
+}
diff --git a/app/src/main/java/com/phoenix/gui/module/impl/movement/ScaffoldModule.java b/app/src/main/java/com/phoenix/gui/module/impl/movement/ScaffoldModule.java
new file mode 100644
index 0000000..05dad5a
--- /dev/null
+++ b/app/src/main/java/com/phoenix/gui/module/impl/movement/ScaffoldModule.java
@@ -0,0 +1,20 @@
+package com.phoenix.gui.module.impl.movement;
+
+import com.phoenix.gui.module.Module;
+import com.phoenix.gui.module.ModuleCategory;
+
+public class ScaffoldModule extends Module {
+ public ScaffoldModule() {
+ super("Scaffold", ModuleCategory.MOVEMENT);
+ }
+
+ @Override
+ protected void onEnable() {
+
+ }
+
+ @Override
+ protected void onDisable() {
+
+ }
+}
diff --git a/app/src/main/java/com/phoenix/gui/module/impl/movement/SpeedModule.java b/app/src/main/java/com/phoenix/gui/module/impl/movement/SpeedModule.java
new file mode 100644
index 0000000..36bca82
--- /dev/null
+++ b/app/src/main/java/com/phoenix/gui/module/impl/movement/SpeedModule.java
@@ -0,0 +1,20 @@
+package com.phoenix.gui.module.impl.movement;
+
+import com.phoenix.gui.module.Module;
+import com.phoenix.gui.module.ModuleCategory;
+
+public class SpeedModule extends Module {
+ public SpeedModule() {
+ super("Speed", ModuleCategory.MOVEMENT);
+ }
+
+ @Override
+ protected void onEnable() {
+
+ }
+
+ @Override
+ protected void onDisable() {
+
+ }
+}
diff --git a/app/src/main/java/com/phoenix/gui/module/impl/player/AntiBotModule.java b/app/src/main/java/com/phoenix/gui/module/impl/player/AntiBotModule.java
new file mode 100644
index 0000000..31f660f
--- /dev/null
+++ b/app/src/main/java/com/phoenix/gui/module/impl/player/AntiBotModule.java
@@ -0,0 +1,20 @@
+package com.phoenix.gui.module.impl.player;
+
+import com.phoenix.gui.module.Module;
+import com.phoenix.gui.module.ModuleCategory;
+
+public class AntiBotModule extends Module {
+ public AntiBotModule() {
+ super("AntiBot", ModuleCategory.PLAYER);
+ }
+
+ @Override
+ protected void onEnable() {
+
+ }
+
+ @Override
+ protected void onDisable() {
+
+ }
+}
diff --git a/app/src/main/java/com/phoenix/gui/module/impl/player/BlinkModule.java b/app/src/main/java/com/phoenix/gui/module/impl/player/BlinkModule.java
new file mode 100644
index 0000000..da10f2d
--- /dev/null
+++ b/app/src/main/java/com/phoenix/gui/module/impl/player/BlinkModule.java
@@ -0,0 +1,20 @@
+package com.phoenix.gui.module.impl.player;
+
+import com.phoenix.gui.module.Module;
+import com.phoenix.gui.module.ModuleCategory;
+
+public class BlinkModule extends Module {
+ public BlinkModule() {
+ super("Blink", ModuleCategory.PLAYER);
+ }
+
+ @Override
+ protected void onEnable() {
+
+ }
+
+ @Override
+ protected void onDisable() {
+
+ }
+}
diff --git a/app/src/main/java/com/phoenix/gui/module/impl/player/NameTagModule.java b/app/src/main/java/com/phoenix/gui/module/impl/player/NameTagModule.java
new file mode 100644
index 0000000..812f9fa
--- /dev/null
+++ b/app/src/main/java/com/phoenix/gui/module/impl/player/NameTagModule.java
@@ -0,0 +1,20 @@
+package com.phoenix.gui.module.impl.player;
+
+import com.phoenix.gui.module.Module;
+import com.phoenix.gui.module.ModuleCategory;
+
+public class NameTagModule extends Module {
+ public NameTagModule() {
+ super("NameTag", ModuleCategory.PLAYER);
+ }
+
+ @Override
+ protected void onEnable() {
+
+ }
+
+ @Override
+ protected void onDisable() {
+
+ }
+}
diff --git a/app/src/main/java/com/phoenix/gui/module/impl/player/PhasesModule.java b/app/src/main/java/com/phoenix/gui/module/impl/player/PhasesModule.java
new file mode 100644
index 0000000..878fa10
--- /dev/null
+++ b/app/src/main/java/com/phoenix/gui/module/impl/player/PhasesModule.java
@@ -0,0 +1,20 @@
+package com.phoenix.gui.module.impl.player;
+
+import com.phoenix.gui.module.Module;
+import com.phoenix.gui.module.ModuleCategory;
+
+public class PhasesModule extends Module {
+ public PhasesModule() {
+ super("Phases", ModuleCategory.PLAYER);
+ }
+
+ @Override
+ protected void onEnable() {
+
+ }
+
+ @Override
+ protected void onDisable() {
+
+ }
+}
diff --git a/app/src/main/java/com/phoenix/gui/module/impl/player/TeleportModule.java b/app/src/main/java/com/phoenix/gui/module/impl/player/TeleportModule.java
new file mode 100644
index 0000000..386943a
--- /dev/null
+++ b/app/src/main/java/com/phoenix/gui/module/impl/player/TeleportModule.java
@@ -0,0 +1,20 @@
+package com.phoenix.gui.module.impl.player;
+
+import com.phoenix.gui.module.Module;
+import com.phoenix.gui.module.ModuleCategory;
+
+public class TeleportModule extends Module {
+ public TeleportModule() {
+ super("Teleport", ModuleCategory.PLAYER);
+ }
+
+ @Override
+ protected void onEnable() {
+
+ }
+
+ @Override
+ protected void onDisable() {
+
+ }
+}
diff --git a/app/src/main/java/com/phoenix/gui/module/impl/visual/DisableModule.java b/app/src/main/java/com/phoenix/gui/module/impl/visual/DisableModule.java
new file mode 100644
index 0000000..9d3301f
--- /dev/null
+++ b/app/src/main/java/com/phoenix/gui/module/impl/visual/DisableModule.java
@@ -0,0 +1,20 @@
+package com.phoenix.gui.module.impl.visual;
+
+import com.phoenix.gui.module.Module;
+import com.phoenix.gui.module.ModuleCategory;
+
+public class DisableModule extends Module {
+ public DisableModule() {
+ super("Disable", ModuleCategory.VISUAL);
+ }
+
+ @Override
+ protected void onEnable() {
+
+ }
+
+ @Override
+ protected void onDisable() {
+
+ }
+}
diff --git a/app/src/main/java/com/phoenix/gui/module/impl/visual/FovModule.java b/app/src/main/java/com/phoenix/gui/module/impl/visual/FovModule.java
new file mode 100644
index 0000000..329c180
--- /dev/null
+++ b/app/src/main/java/com/phoenix/gui/module/impl/visual/FovModule.java
@@ -0,0 +1,20 @@
+package com.phoenix.gui.module.impl.visual;
+
+import com.phoenix.gui.module.Module;
+import com.phoenix.gui.module.ModuleCategory;
+
+public class FovModule extends Module {
+ public FovModule() {
+ super("Fov", ModuleCategory.VISUAL);
+ }
+
+ @Override
+ protected void onEnable() {
+
+ }
+
+ @Override
+ protected void onDisable() {
+
+ }
+}
diff --git a/app/src/main/java/com/phoenix/gui/module/impl/visual/GodModeModule.java b/app/src/main/java/com/phoenix/gui/module/impl/visual/GodModeModule.java
new file mode 100644
index 0000000..c416ad4
--- /dev/null
+++ b/app/src/main/java/com/phoenix/gui/module/impl/visual/GodModeModule.java
@@ -0,0 +1,20 @@
+package com.phoenix.gui.module.impl.visual;
+
+import com.phoenix.gui.module.Module;
+import com.phoenix.gui.module.ModuleCategory;
+
+public class GodModeModule extends Module {
+ public GodModeModule() {
+ super("GodMode", ModuleCategory.VISUAL);
+ }
+
+ @Override
+ protected void onEnable() {
+
+ }
+
+ @Override
+ protected void onDisable() {
+
+ }
+}
diff --git a/app/src/main/java/com/phoenix/gui/module/impl/visual/InfiniteYModule.java b/app/src/main/java/com/phoenix/gui/module/impl/visual/InfiniteYModule.java
new file mode 100644
index 0000000..33b727b
--- /dev/null
+++ b/app/src/main/java/com/phoenix/gui/module/impl/visual/InfiniteYModule.java
@@ -0,0 +1,20 @@
+package com.phoenix.gui.module.impl.visual;
+
+import com.phoenix.gui.module.Module;
+import com.phoenix.gui.module.ModuleCategory;
+
+public class InfiniteYModule extends Module {
+ public InfiniteYModule() {
+ super("InfiniteY", ModuleCategory.VISUAL);
+ }
+
+ @Override
+ protected void onEnable() {
+
+ }
+
+ @Override
+ protected void onDisable() {
+
+ }
+}
diff --git a/app/src/main/java/com/phoenix/gui/module/impl/visual/OnPosModule.java b/app/src/main/java/com/phoenix/gui/module/impl/visual/OnPosModule.java
new file mode 100644
index 0000000..13184e5
--- /dev/null
+++ b/app/src/main/java/com/phoenix/gui/module/impl/visual/OnPosModule.java
@@ -0,0 +1,20 @@
+package com.phoenix.gui.module.impl.visual;
+
+import com.phoenix.gui.module.Module;
+import com.phoenix.gui.module.ModuleCategory;
+
+public class OnPosModule extends Module {
+ public OnPosModule() {
+ super("OnPos", ModuleCategory.VISUAL);
+ }
+
+ @Override
+ protected void onEnable() {
+
+ }
+
+ @Override
+ protected void onDisable() {
+
+ }
+}
diff --git a/app/src/main/java/com/phoenix/gui/module/impl/visual/ThemeModule.java b/app/src/main/java/com/phoenix/gui/module/impl/visual/ThemeModule.java
new file mode 100644
index 0000000..5892826
--- /dev/null
+++ b/app/src/main/java/com/phoenix/gui/module/impl/visual/ThemeModule.java
@@ -0,0 +1,39 @@
+package com.phoenix.gui.module.impl.visual;
+
+import com.phoenix.gui.module.Module;
+import com.phoenix.gui.module.ModuleCategory;
+import com.phoenix.gui.ui.widgets.ColorPickerView;
+import com.phoenix.gui.ui.widgets.SubMenuPanel;
+
+public class ThemeModule extends Module {
+
+ public ThemeModule() {
+ super("Theme", ModuleCategory.VISUAL, "Customize the UI colors and themes.");
+ }
+
+ // 这个模块没有简单的开关功能,所以 onEnable/onDisable 为空
+ @Override
+ protected void onEnable() {}
+
+ @Override
+ protected void onDisable() {}
+
+ // 这个模块不支持快捷方式
+ @Override
+ public boolean supportsShortcut() {
+ return false;
+ }
+
+ @Override
+ public void configureSubMenu(SubMenuPanel subMenu) {
+ // 添加一行说明文字
+ subMenu.addText("Select a preset theme or create your own color below.");
+
+ // 添加预设主题按钮
+ subMenu.addThemeButtons();
+
+ // 添加自定义颜色选择器
+ ColorPickerView colorPicker = new ColorPickerView(subMenu.getContext());
+ subMenu.addCustomView(colorPicker);
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/phoenix/gui/module/impl/world/AimAssetsModule.java b/app/src/main/java/com/phoenix/gui/module/impl/world/AimAssetsModule.java
new file mode 100644
index 0000000..2b6e5d4
--- /dev/null
+++ b/app/src/main/java/com/phoenix/gui/module/impl/world/AimAssetsModule.java
@@ -0,0 +1,20 @@
+package com.phoenix.gui.module.impl.world;
+
+import com.phoenix.gui.module.Module;
+import com.phoenix.gui.module.ModuleCategory;
+
+public class AimAssetsModule extends Module {
+ public AimAssetsModule() {
+ super("AimAssets", ModuleCategory.WORLD);
+ }
+
+ @Override
+ protected void onEnable() {
+
+ }
+
+ @Override
+ protected void onDisable() {
+
+ }
+}
diff --git a/app/src/main/java/com/phoenix/gui/module/impl/world/AutoClickModule.java b/app/src/main/java/com/phoenix/gui/module/impl/world/AutoClickModule.java
new file mode 100644
index 0000000..3e4e03c
--- /dev/null
+++ b/app/src/main/java/com/phoenix/gui/module/impl/world/AutoClickModule.java
@@ -0,0 +1,20 @@
+package com.phoenix.gui.module.impl.world;
+
+import com.phoenix.gui.module.Module;
+import com.phoenix.gui.module.ModuleCategory;
+
+public class AutoClickModule extends Module {
+ public AutoClickModule() {
+ super("AutoClick", ModuleCategory.WORLD);
+ }
+
+ @Override
+ protected void onEnable() {
+
+ }
+
+ @Override
+ protected void onDisable() {
+
+ }
+}
diff --git a/app/src/main/java/com/phoenix/gui/module/impl/world/FuckerModule.java b/app/src/main/java/com/phoenix/gui/module/impl/world/FuckerModule.java
new file mode 100644
index 0000000..b12b7f9
--- /dev/null
+++ b/app/src/main/java/com/phoenix/gui/module/impl/world/FuckerModule.java
@@ -0,0 +1,20 @@
+package com.phoenix.gui.module.impl.world;
+
+import com.phoenix.gui.module.Module;
+import com.phoenix.gui.module.ModuleCategory;
+
+public class FuckerModule extends Module {
+ public FuckerModule() {
+ super("Fucker", ModuleCategory.WORLD);
+ }
+
+ @Override
+ protected void onEnable() {
+
+ }
+
+ @Override
+ protected void onDisable() {
+
+ }
+}
diff --git a/app/src/main/java/com/phoenix/gui/module/impl/world/GameModeModule.java b/app/src/main/java/com/phoenix/gui/module/impl/world/GameModeModule.java
new file mode 100644
index 0000000..59468b1
--- /dev/null
+++ b/app/src/main/java/com/phoenix/gui/module/impl/world/GameModeModule.java
@@ -0,0 +1,20 @@
+package com.phoenix.gui.module.impl.world;
+
+import com.phoenix.gui.module.Module;
+import com.phoenix.gui.module.ModuleCategory;
+
+public class GameModeModule extends Module {
+ public GameModeModule() {
+ super("GameMode", ModuleCategory.WORLD);
+ }
+
+ @Override
+ protected void onEnable() {
+
+ }
+
+ @Override
+ protected void onDisable() {
+
+ }
+}
diff --git a/app/src/main/java/com/phoenix/gui/module/impl/world/RemoteStopModule.java b/app/src/main/java/com/phoenix/gui/module/impl/world/RemoteStopModule.java
new file mode 100644
index 0000000..26759f8
--- /dev/null
+++ b/app/src/main/java/com/phoenix/gui/module/impl/world/RemoteStopModule.java
@@ -0,0 +1,20 @@
+package com.phoenix.gui.module.impl.world;
+
+import com.phoenix.gui.module.Module;
+import com.phoenix.gui.module.ModuleCategory;
+
+public class RemoteStopModule extends Module {
+ public RemoteStopModule() {
+ super("RemoteStop", ModuleCategory.WORLD);
+ }
+
+ @Override
+ protected void onEnable() {
+
+ }
+
+ @Override
+ protected void onDisable() {
+
+ }
+}
diff --git a/app/src/main/java/com/phoenix/gui/ui/AnimationUtils.java b/app/src/main/java/com/phoenix/gui/ui/AnimationUtils.java
new file mode 100644
index 0000000..754f4c3
--- /dev/null
+++ b/app/src/main/java/com/phoenix/gui/ui/AnimationUtils.java
@@ -0,0 +1,103 @@
+package com.phoenix.gui.ui;
+
+import android.animation.ValueAnimator;
+import android.view.View;
+import android.view.ViewGroup;
+import android.view.animation.DecelerateInterpolator;
+
+public class AnimationUtils {
+
+ public static void expand(View view, long duration) {
+
+ view.measure(
+ ViewGroup.LayoutParams.MATCH_PARENT,
+ ViewGroup.LayoutParams.WRAP_CONTENT
+ );
+ int targetHeight = view.getMeasuredHeight();
+
+ view.getLayoutParams().height = 0;
+ view.setVisibility(View.VISIBLE);
+ view.setAlpha(0f);
+
+ ValueAnimator animator = ValueAnimator.ofInt(0, targetHeight);
+ animator.setDuration(duration);
+ animator.setInterpolator(new DecelerateInterpolator());
+ animator.addUpdateListener(animation -> {
+ int value = (int) animation.getAnimatedValue();
+ ViewGroup.LayoutParams layoutParams = view.getLayoutParams();
+ layoutParams.height = value;
+ view.setLayoutParams(layoutParams);
+ });
+ animator.start();
+
+ view.animate()
+ .alpha(1f)
+ .setDuration(duration)
+ .setInterpolator(new DecelerateInterpolator())
+ .start();
+ }
+
+ public static void expand(View view) {
+ expand(view, 300);
+ }
+
+ public static void collapse(View view, long duration) {
+ int initialHeight = view.getHeight();
+
+ ValueAnimator animator = ValueAnimator.ofInt(initialHeight, 0);
+ animator.setDuration(duration);
+ animator.setInterpolator(new DecelerateInterpolator());
+ animator.addUpdateListener(animation -> {
+ int value = (int) animation.getAnimatedValue();
+ ViewGroup.LayoutParams layoutParams = view.getLayoutParams();
+ layoutParams.height = value;
+ view.setLayoutParams(layoutParams);
+ });
+ animator.start();
+
+ view.animate()
+ .alpha(0f)
+ .setDuration(duration)
+ .setInterpolator(new DecelerateInterpolator())
+ .withEndAction(() -> {
+ view.setVisibility(View.GONE);
+ view.getLayoutParams().height = 0;
+ })
+ .start();
+ }
+
+ public static void collapse(View view) {
+ collapse(view, 300);
+ }
+
+ public static void fadeIn(View view, long duration) {
+ view.setAlpha(0f);
+ view.setVisibility(View.VISIBLE);
+ view.animate()
+ .alpha(1f)
+ .setDuration(duration)
+ .setInterpolator(new DecelerateInterpolator())
+ .start();
+ }
+
+ public static void fadeIn(View view) {
+ fadeIn(view, 300);
+ }
+
+ public static void fadeOut(View view, long duration) {
+ view.animate()
+ .alpha(0f)
+ .setDuration(duration)
+ .setInterpolator(new DecelerateInterpolator())
+ .withEndAction(() -> view.setVisibility(View.GONE))
+ .start();
+ }
+
+ public static void fadeOut(View view) {
+ fadeOut(view, 300);
+ }
+
+ private AnimationUtils() {
+
+ }
+}
diff --git a/app/src/main/java/com/phoenix/gui/ui/ArraylistItemView.java b/app/src/main/java/com/phoenix/gui/ui/ArraylistItemView.java
new file mode 100644
index 0000000..015cd42
--- /dev/null
+++ b/app/src/main/java/com/phoenix/gui/ui/ArraylistItemView.java
@@ -0,0 +1,130 @@
+package com.phoenix.gui.ui;
+
+import android.animation.ValueAnimator;
+import android.content.Context;
+import android.graphics.*;
+import android.graphics.drawable.Drawable;
+import android.text.TextPaint;
+import android.util.AttributeSet;
+import android.view.Gravity;
+import android.view.animation.LinearInterpolator;
+import android.widget.FrameLayout;
+import android.widget.TextView;
+import androidx.annotation.NonNull;
+
+public class ArraylistItemView extends FrameLayout implements ThemeManager.OnThemeColorChangeListener {
+
+ public enum PositionType {
+ SINGLE, FIRST, MIDDLE, LAST
+ }
+
+ private TextView textView;
+ private Paint backgroundPaint;
+ private Path clipPath;
+ private float cornerRadius;
+ private int customTextColor = -1;
+ private float measuredTextWidth = 0;
+ private PositionType positionType = PositionType.MIDDLE;
+
+ private Paint shadowPaint;
+ private Path shadowPath;
+
+ public ArraylistItemView(Context context) {
+ super(context);
+ init();
+ }
+ public ArraylistItemView(Context context, AttributeSet attrs) {
+ super(context, attrs);
+ init();
+ }
+ public ArraylistItemView(Context context, AttributeSet attrs, int defStyleAttr) {
+ super(context, attrs, defStyleAttr);
+ init();
+ }
+
+ private void init() {
+ setWillNotDraw(false);
+ setLayerType(LAYER_TYPE_SOFTWARE, null);
+
+ setClipChildren(false);
+ setClipToPadding(false);
+
+ int shadowSpace = dpToPx(8);
+ setPadding(shadowSpace, shadowSpace, shadowSpace, shadowSpace);
+
+ cornerRadius = dpToPx(3);
+
+ textView = new TextView(getContext());
+ textView.setTextColor(ThemeManager.getThemeColor());
+ textView.setTextSize(10);
+ textView.setTypeface(textView.getTypeface(), Typeface.BOLD);
+ textView.setGravity(Gravity.RIGHT | Gravity.CENTER_VERTICAL);
+ textView.setPadding(dpToPx(6), dpToPx(2), dpToPx(6), dpToPx(2));
+
+ FrameLayout.LayoutParams params = new FrameLayout.LayoutParams(
+ LayoutParams.WRAP_CONTENT,
+ LayoutParams.WRAP_CONTENT
+ );
+ params.gravity = Gravity.END;
+ addView(textView, params);
+
+ backgroundPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
+ backgroundPaint.setColor(0xB2000000);
+ backgroundPaint.setStyle(Paint.Style.FILL);
+
+ // REMOVED: sheenDrawable initialization
+
+ clipPath = new Path();
+ shadowPath = new Path();
+
+ shadowPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
+ shadowPaint.setColor(0xFF000000);
+ shadowPaint.setMaskFilter(new BlurMaskFilter(dpToPx(6), BlurMaskFilter.Blur.NORMAL));
+
+ ThemeManager.addListener(this);
+ }
+
+ @Override
+ protected void onDraw(Canvas canvas) {
+ int width = getWidth();
+ int height = getHeight();
+
+ if (width <= 0 || height <= 0) {
+ super.onDraw(canvas);
+ return;
+ }
+
+ RectF backgroundBounds = new RectF(
+ getPaddingLeft(), getPaddingTop(),
+ width - getPaddingRight(), height - getPaddingBottom()
+ );
+
+ drawSelectiveShadow(canvas, backgroundBounds);
+ drawDynamicBackground(canvas, backgroundBounds);
+
+ super.onDraw(canvas);
+ }
+
+ @Override
+ public void onThemeColorChanged(int newColor) {
+ if (customTextColor == -1) {
+ textView.setTextColor(newColor);
+ }
+ invalidate();
+ }
+
+ public void cleanup() {
+ ThemeManager.removeListener(this);
+ }
+
+
+ // 下面由 gemini 2.5 pro 编写。够屎山。你要是想改你就去找 claude 吧,千万别用 gemini。
+ public void setPositionType(PositionType type) { if (this.positionType != type) { this.positionType = type; invalidate(); } }
+ private void drawSelectiveShadow(Canvas canvas, RectF bounds) { shadowPath.reset(); float spread = dpToPx(0.5f); switch (positionType) { case SINGLE: shadowPath.addRoundRect(bounds, cornerRadius, cornerRadius, Path.Direction.CW); break; case FIRST: shadowPath.moveTo(bounds.left + cornerRadius, bounds.top); shadowPath.lineTo(bounds.right - cornerRadius, bounds.top); shadowPath.quadTo(bounds.right, bounds.top, bounds.right, bounds.top + cornerRadius); shadowPath.lineTo(bounds.right, bounds.bottom + spread); shadowPath.lineTo(bounds.left, bounds.bottom + spread); shadowPath.lineTo(bounds.left, bounds.top + cornerRadius); shadowPath.quadTo(bounds.left, bounds.top, bounds.left + cornerRadius, bounds.top); break; case MIDDLE: shadowPath.moveTo(bounds.right, bounds.top - spread); shadowPath.lineTo(bounds.right, bounds.bottom + spread); shadowPath.lineTo(bounds.left, bounds.bottom + spread); shadowPath.lineTo(bounds.left, bounds.top - spread); break; case LAST: shadowPath.moveTo(bounds.left, bounds.top - spread); shadowPath.lineTo(bounds.right, bounds.top - spread); shadowPath.lineTo(bounds.right, bounds.bottom - cornerRadius); shadowPath.quadTo(bounds.right, bounds.bottom, bounds.right - cornerRadius, bounds.bottom); shadowPath.lineTo(bounds.left + cornerRadius, bounds.bottom); shadowPath.quadTo(bounds.left, bounds.bottom, bounds.left, bounds.bottom - cornerRadius); shadowPath.lineTo(bounds.left, bounds.top - spread); break; } shadowPath.close(); canvas.drawPath(shadowPath, shadowPaint); }
+ private void drawDynamicBackground(Canvas canvas, RectF bounds) { clipPath.reset(); clipPath.moveTo(bounds.left + cornerRadius, bounds.top); if (positionType == PositionType.FIRST || positionType == PositionType.SINGLE) { clipPath.lineTo(bounds.right - cornerRadius, bounds.top); clipPath.quadTo(bounds.right, bounds.top, bounds.right, bounds.top + cornerRadius); } else { clipPath.lineTo(bounds.right, bounds.top); } if (positionType == PositionType.LAST || positionType == PositionType.SINGLE) { clipPath.lineTo(bounds.right, bounds.bottom - cornerRadius); clipPath.quadTo(bounds.right, bounds.bottom, bounds.right - cornerRadius, bounds.bottom); } else { clipPath.lineTo(bounds.right, bounds.bottom); } clipPath.lineTo(bounds.left + cornerRadius, bounds.bottom); clipPath.quadTo(bounds.left, bounds.bottom, bounds.left, bounds.bottom - cornerRadius); clipPath.lineTo(bounds.left, bounds.top + cornerRadius); clipPath.quadTo(bounds.left, bounds.top, bounds.left + cornerRadius, bounds.top); clipPath.close(); canvas.drawPath(clipPath, backgroundPaint); }
+ public void setText(String text) { textView.setText(text); measureTextWidth(); }
+ private void measureTextWidth() { TextPaint paint = textView.getPaint(); String text = textView.getText().toString(); measuredTextWidth = paint.measureText(text) + textView.getPaddingLeft() + textView.getPaddingRight(); }
+ public float getMeasuredTextWidth() { return measuredTextWidth; }
+ public void setTextColor(int color) { customTextColor = color; textView.setTextColor(color); }
+ private int dpToPx(float dp) { return (int) (dp * getContext().getResources().getDisplayMetrics().density); }
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/phoenix/gui/ui/ArraylistModule.java b/app/src/main/java/com/phoenix/gui/ui/ArraylistModule.java
new file mode 100644
index 0000000..371eb20
--- /dev/null
+++ b/app/src/main/java/com/phoenix/gui/ui/ArraylistModule.java
@@ -0,0 +1,47 @@
+package com.phoenix.gui.ui;
+
+public class ArraylistModule {
+ private String name;
+ private int color;
+ private boolean useThemeColor;
+
+ public ArraylistModule(String name) {
+ this.name = name;
+ this.color = ThemeManager.getTextPrimary();
+ this.useThemeColor = true;
+ }
+
+ public ArraylistModule(String name, int color) {
+ this.name = name;
+ this.color = color;
+ this.useThemeColor = false;
+ }
+
+ public String getName() {
+ return name;
+ }
+
+ public void setName(String name) {
+ this.name = name;
+ }
+
+ public int getColor() {
+ if (useThemeColor) {
+ return ThemeManager.getTextPrimary();
+ }
+ return color;
+ }
+
+ public void setColor(int color) {
+ this.color = color;
+ this.useThemeColor = false;
+ }
+
+ public void setUseThemeColor(boolean use) {
+ this.useThemeColor = use;
+ }
+
+ public boolean isUseThemeColor() {
+ return useThemeColor;
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/phoenix/gui/ui/ArraylistView.java b/app/src/main/java/com/phoenix/gui/ui/ArraylistView.java
new file mode 100644
index 0000000..5ac3d43
--- /dev/null
+++ b/app/src/main/java/com/phoenix/gui/ui/ArraylistView.java
@@ -0,0 +1,170 @@
+package com.phoenix.gui.ui;
+
+import android.animation.Animator;
+import android.animation.AnimatorListenerAdapter;
+import android.animation.LayoutTransition;
+import android.animation.ObjectAnimator;
+import android.animation.PropertyValuesHolder;
+import android.animation.ValueAnimator;
+import android.content.Context;
+import android.util.AttributeSet;
+import android.view.Gravity;
+import android.view.View;
+import android.view.ViewGroup;
+import android.view.animation.DecelerateInterpolator;
+import android.widget.LinearLayout;
+import androidx.annotation.NonNull;
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.Comparator;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+public class ArraylistView extends LinearLayout {
+ private List modules;
+ private Map itemViews;
+
+ public ArraylistView(Context context) {
+ super(context);
+ init();
+ }
+ public ArraylistView(Context context, AttributeSet attrs) {
+ super(context, attrs);
+ init();
+ }
+ public ArraylistView(Context context, AttributeSet attrs, int defStyleAttr) {
+ super(context, attrs, defStyleAttr);
+ init();
+ }
+
+ private void init() {
+ setOrientation(VERTICAL);
+ setGravity(Gravity.RIGHT | Gravity.TOP);
+ modules = new ArrayList<>();
+ itemViews = new HashMap<>();
+
+ setClipChildren(false);
+ setClipToPadding(false);
+
+ int paddingTop = dpToPx(16);
+ setPadding(0, paddingTop, 0, 0);
+
+ setupLayoutAnimations();
+ }
+
+ private void setupLayoutAnimations() {
+ LayoutTransition transition = new LayoutTransition();
+
+ transition.setDuration(300);
+
+ transition.setAnimator(LayoutTransition.APPEARING, null);
+ transition.setAnimator(LayoutTransition.DISAPPEARING, null);
+
+ this.setLayoutTransition(transition);
+ }
+
+ private void rebuildViews() {
+ List viewsToShow = new ArrayList<>();
+ int moduleCount = modules.size();
+
+ for (int i = 0; i < moduleCount; i++) {
+ ArraylistModule module = modules.get(i);
+ ArraylistItemView itemView = itemViews.get(module.getName());
+
+ if (itemView != null) {
+ viewsToShow.add(itemView);
+
+ if (moduleCount == 1) {
+ itemView.setPositionType(ArraylistItemView.PositionType.SINGLE);
+ } else if (i == 0) {
+ itemView.setPositionType(ArraylistItemView.PositionType.FIRST);
+ } else if (i == moduleCount - 1) {
+ itemView.setPositionType(ArraylistItemView.PositionType.LAST);
+ } else {
+ itemView.setPositionType(ArraylistItemView.PositionType.MIDDLE);
+ }
+
+ LinearLayout.LayoutParams params = (LinearLayout.LayoutParams) itemView.getLayoutParams();
+ if (params == null) {
+ params = new LinearLayout.LayoutParams(
+ ViewGroup.LayoutParams.WRAP_CONTENT,
+ ViewGroup.LayoutParams.WRAP_CONTENT
+ );
+ }
+ params.gravity = Gravity.RIGHT;
+
+ int shadowSpaceInPx = dpToPx(8);
+ if (i > 0) {
+ params.topMargin = -(shadowSpaceInPx * 2);
+ } else {
+ params.topMargin = 0;
+ }
+ itemView.setLayoutParams(params);
+ }
+ }
+
+ for (int i = getChildCount() - 1; i >= 0; i--) {
+ View child = getChildAt(i);
+ if (!viewsToShow.contains(child)) {
+ removeViewAt(i);
+ }
+ }
+
+ for (int i = 0; i < viewsToShow.size(); i++) {
+ View view = viewsToShow.get(i);
+ if (indexOfChild(view) != i) {
+ if (view.getParent() != null) {
+ removeView(view);
+ }
+ addView(view, i);
+ }
+ }
+ }
+
+ public void addModule(String moduleName) { addModule(new ArraylistModule(moduleName)); }
+ public void addModule(String moduleName, int color) { addModule(new ArraylistModule(moduleName, color)); }
+ public void addModule(ArraylistModule module) {
+ if (itemViews.containsKey(module.getName())) { return; }
+ modules.add(module);
+ ArraylistItemView itemView = new ArraylistItemView(getContext());
+ itemView.setText(module.getName());
+ if (!module.isUseThemeColor()) { itemView.setTextColor(module.getColor()); }
+
+ itemViews.put(module.getName(), itemView);
+
+ itemView.setAlpha(0f);
+ itemView.setTranslationX(dpToPx(20));
+
+ sortModules();
+ rebuildViews();
+
+ animateItemIn(itemView);
+ }
+
+ public void removeModule(String moduleName) {
+ ArraylistModule toRemove = null;
+ for (ArraylistModule module : modules) { if (module.getName().equals(moduleName)) { toRemove = module; break; } }
+ if (toRemove != null) {
+ modules.remove(toRemove);
+ ArraylistItemView itemView = itemViews.get(moduleName);
+ if (itemView != null) {
+ animateItemOut(itemView, () -> {
+ itemViews.remove(moduleName);
+ itemView.cleanup();
+ sortModules();
+ rebuildViews();
+ });
+ }
+ } // 下面也是 gemini 写的,由于我懒得动。所以建议不要动。
+ }
+ private void sortModules() { Collections.sort(modules, (m1, m2) -> { ArraylistItemView v1 = itemViews.get(m1.getName()); ArraylistItemView v2 = itemViews.get(m2.getName()); if (v1 == null || v2 == null) { return Integer.compare(m2.getName().length(), m1.getName().length()); } float width1 = v1.getMeasuredTextWidth(); float width2 = v2.getMeasuredTextWidth(); int widthCompare = Float.compare(width2, width1); if (widthCompare != 0) { return widthCompare; } return m1.getName().compareTo(m2.getName()); }); }
+ private void animateItemIn(ArraylistItemView itemView) { ValueAnimator alphaAnimator = ValueAnimator.ofFloat(0f, 1f); alphaAnimator.setDuration(300); alphaAnimator.setInterpolator(new DecelerateInterpolator()); alphaAnimator.addUpdateListener(animation -> { float value = (float) animation.getAnimatedValue(); itemView.setAlpha(value); }); ValueAnimator translationAnimator = ValueAnimator.ofFloat(dpToPx(20), 0f); translationAnimator.setDuration(300); translationAnimator.setInterpolator(new DecelerateInterpolator()); translationAnimator.addUpdateListener(animation -> { float value = (float) animation.getAnimatedValue(); itemView.setTranslationX(value); }); alphaAnimator.start(); translationAnimator.start(); }
+ private void animateItemOut(ArraylistItemView itemView, Runnable onComplete) { ValueAnimator alphaAnimator = ValueAnimator.ofFloat(itemView.getAlpha(), 0f); alphaAnimator.setDuration(250); alphaAnimator.setInterpolator(new DecelerateInterpolator()); alphaAnimator.addUpdateListener(animation -> { float value = (float) animation.getAnimatedValue(); itemView.setAlpha(value); }); ValueAnimator translationAnimator = ValueAnimator.ofFloat(itemView.getTranslationX(), dpToPx(20)); translationAnimator.setDuration(250); translationAnimator.setInterpolator(new DecelerateInterpolator()); translationAnimator.addUpdateListener(animation -> { float value = (float) animation.getAnimatedValue(); itemView.setTranslationX(value); }); translationAnimator.addListener(new AnimatorListenerAdapter() { @Override public void onAnimationEnd(Animator animation) { if (onComplete != null) { onComplete.run(); } } }); alphaAnimator.start(); translationAnimator.start(); }
+ public void clearModules() { for (ArraylistItemView itemView : itemViews.values()) { itemView.cleanup(); } modules.clear(); itemViews.clear(); removeAllViews(); }
+ public void show() { setVisibility(VISIBLE); }
+ public void hide() { setVisibility(GONE); }
+ private int dpToPx(float dp) { return (int) (dp * getContext().getResources().getDisplayMetrics().density); }
+ public List getModules() { return this.modules; }
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/phoenix/gui/ui/DynamicColorExtractor.java b/app/src/main/java/com/phoenix/gui/ui/DynamicColorExtractor.java
new file mode 100644
index 0000000..29838dd
--- /dev/null
+++ b/app/src/main/java/com/phoenix/gui/ui/DynamicColorExtractor.java
@@ -0,0 +1,159 @@
+package com.phoenix.gui.ui;
+
+import android.app.WallpaperColors;
+import android.app.WallpaperManager;
+import android.content.Context;
+import android.graphics.Color;
+import android.os.Build;
+import android.util.Log;
+
+import androidx.annotation.RequiresApi;
+
+public class DynamicColorExtractor {
+
+ private static final String TAG = "DynamicColorExtractor";
+ private final Context context;
+ private WallpaperManager wallpaperManager;
+ private WallpaperManager.OnColorsChangedListener colorsChangedListener;
+
+ public DynamicColorExtractor(Context context) {
+ this.context = context.getApplicationContext();
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O_MR1) {
+ wallpaperManager = WallpaperManager.getInstance(context);
+ }
+ }
+
+ public void init() {
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O_MR1) {
+ extractAndApplyColors();
+ startListening();
+ } else {
+ Log.w(TAG, "nah i cant use dc");
+ ThemeManager.applyDefaultTheme();
+ }
+ }
+
+ @RequiresApi(api = Build.VERSION_CODES.O_MR1)
+ private void extractAndApplyColors() {
+ try {
+ WallpaperColors wallpaperColors = wallpaperManager.getWallpaperColors(WallpaperManager.FLAG_SYSTEM);
+
+ if (wallpaperColors != null) {
+
+ Color primaryColor = wallpaperColors.getPrimaryColor();
+
+ if (primaryColor != null) {
+ int color = primaryColor.toArgb();
+
+ int adjustedColor = adjustColorForUI(color);
+
+ Log.d(TAG, String.format("color: #%08X -> #%08X", color, adjustedColor));
+
+ ThemeManager.setThemeColor(adjustedColor);
+
+ Color secondaryColor = wallpaperColors.getSecondaryColor();
+ if (secondaryColor != null) {
+ int secColor = secondaryColor.toArgb();
+
+ ThemeManager.setGradientStart(adjustedColor);
+ ThemeManager.setGradientEnd(adjustColorForUI(secColor));
+ }
+
+ Color tertiaryColor = wallpaperColors.getTertiaryColor();
+ if (tertiaryColor != null) {
+ Log.d(TAG, "Tertiary color: #" + Integer.toHexString(tertiaryColor.toArgb()));
+ }
+ } else {
+
+ ThemeManager.applyDefaultTheme();
+ }
+ } else {
+
+ ThemeManager.applyDefaultTheme();
+ }
+ } catch (Exception e) {
+
+ ThemeManager.applyDefaultTheme();
+ }
+ }
+
+ private int adjustColorForUI(int color) {
+ float[] hsv = new float[3];
+ Color.colorToHSV(color, hsv);
+
+ if (hsv[1] < 0.5f) {
+ hsv[1] = Math.min(hsv[1] * 1.5f, 0.7f);
+ }
+
+ if (hsv[2] < 0.6f) {
+ hsv[2] = 0.7f;
+ } else if (hsv[2] > 0.9f) {
+ hsv[2] = 0.85f;
+ }
+
+ return Color.HSVToColor(hsv);
+ }
+
+ @RequiresApi(api = Build.VERSION_CODES.O_MR1)
+ private void startListening() {
+ if (colorsChangedListener == null) {
+ colorsChangedListener = new WallpaperManager.OnColorsChangedListener() {
+ @Override
+ public void onColorsChanged(WallpaperColors colors, int which) {
+ if ((which & WallpaperManager.FLAG_SYSTEM) != 0) {
+
+ extractAndApplyColors();
+ }
+ }
+ };
+
+ wallpaperManager.addOnColorsChangedListener(colorsChangedListener, null);
+
+ }
+ }
+
+ @RequiresApi(api = Build.VERSION_CODES.O_MR1)
+ public void stopListening() {
+ if (colorsChangedListener != null && wallpaperManager != null) {
+ wallpaperManager.removeOnColorsChangedListener(colorsChangedListener);
+ colorsChangedListener = null;
+
+ }
+ }
+
+ public void refresh() {
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O_MR1) {
+ extractAndApplyColors();
+ }
+ }
+
+ @RequiresApi(api = Build.VERSION_CODES.S)
+ public String getColorSchemeInfo() {
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O_MR1) {
+ try {
+ WallpaperColors colors = wallpaperManager.getWallpaperColors(WallpaperManager.FLAG_SYSTEM);
+ if (colors != null) {
+ StringBuilder info = new StringBuilder();
+ info.append("主题色方案: ");
+
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
+ int colorHints = colors.getColorHints();
+ if ((colorHints & WallpaperColors.HINT_SUPPORTS_DARK_TEXT) != 0) {
+ info.append("亮色背景 ");
+ }
+ if ((colorHints & WallpaperColors.HINT_SUPPORTS_DARK_THEME) != 0) {
+ info.append("深色主题 ");
+ }
+ }
+
+ info.append("\n当前主题色: #").append(Integer.toHexString(ThemeManager.getThemeColor()));
+
+ return info.toString();
+ }
+ } catch (Exception e) {
+ Log.e(TAG, "failed", e);
+ }
+ }
+ return "动态取色不可用";
+ }
+}
diff --git a/app/src/main/java/com/phoenix/gui/ui/FloatBallView.java b/app/src/main/java/com/phoenix/gui/ui/FloatBallView.java
new file mode 100644
index 0000000..58ad7a6
--- /dev/null
+++ b/app/src/main/java/com/phoenix/gui/ui/FloatBallView.java
@@ -0,0 +1,232 @@
+package com.phoenix.gui.ui;
+
+import android.content.Context;
+import android.graphics.Color;
+import android.graphics.drawable.GradientDrawable;
+import android.util.DisplayMetrics;
+import android.view.Gravity;
+import android.view.MotionEvent;
+import android.view.View;
+import android.view.WindowManager;
+import android.widget.FrameLayout;
+import android.widget.LinearLayout;
+
+import com.phoenix.gui.config.ConfigManager;
+import com.phoenix.gui.module.ModuleCategory;
+
+import java.util.ArrayList;
+import java.util.List;
+
+public class FloatBallView extends LinearLayout {
+
+ private final List menuViews = new ArrayList<>();
+ private boolean menusVisible = false;
+
+ private final LinearLayout outerBall;
+ private final LinearLayout innerBall;
+
+ private final FrameLayout menusContainer;
+
+ public FloatBallView(Context context, FrameLayout menusContainer) {
+ super(context);
+ this.menusContainer = menusContainer;
+
+ ConfigManager.init(context);
+
+ setLayoutParams(new FrameLayout.LayoutParams(
+ FrameLayout.LayoutParams.WRAP_CONTENT,
+ FrameLayout.LayoutParams.WRAP_CONTENT,
+ Gravity.CENTER
+ ));
+
+ outerBall = new LinearLayout(context);
+ int size = (int) (getScreenHeight(context) * 0.05);
+ outerBall.setLayoutParams(new LayoutParams(size, size));
+
+ GradientDrawable gradient = new GradientDrawable();
+ gradient.setColors(new int[]{ThemeManager.getGradientStart(), ThemeManager.getGradientEnd()});
+ gradient.setGradientType(GradientDrawable.LINEAR_GRADIENT);
+ gradient.setShape(GradientDrawable.OVAL);
+ outerBall.setBackground(gradient);
+ outerBall.setGravity(Gravity.CENTER);
+
+ innerBall = new LinearLayout(context);
+ int innerSize = (int) (getScreenHeight(context) * 0.03);
+ innerBall.setLayoutParams(new LayoutParams(innerSize, innerSize));
+
+ GradientDrawable innerDrawable = new GradientDrawable();
+ innerDrawable.setColor(ThemeManager.getTextPrimary());
+ innerDrawable.setShape(GradientDrawable.OVAL);
+ innerBall.setBackground(innerDrawable);
+
+ outerBall.addView(innerBall);
+
+ LinearLayout controlView = new LinearLayout(context);
+ controlView.setLayoutParams(new LayoutParams(LayoutParams.WRAP_CONTENT, LayoutParams.WRAP_CONTENT));
+ controlView.addView(outerBall);
+ addView(controlView);
+
+ setupTouchListeners();
+
+ createMenus();
+ }
+
+ private void createMenus() {
+ menuViews.add(new MenuView(getContext(), ModuleCategory.PLAYER, MenuView.MenuPosition.POSITION_1));
+ menuViews.add(new MenuView(getContext(), ModuleCategory.WORLD, MenuView.MenuPosition.POSITION_2));
+ menuViews.add(new MenuView(getContext(), ModuleCategory.MOVEMENT, MenuView.MenuPosition.POSITION_3));
+ menuViews.add(new MenuView(getContext(), ModuleCategory.COMBAT, MenuView.MenuPosition.POSITION_4));
+ menuViews.add(new MenuView(getContext(), ModuleCategory.VISUAL, MenuView.MenuPosition.POSITION_5));
+ }
+
+ private void setupTouchListeners() {
+ final float[] downPos = new float[2];
+ final boolean[] hasMoved = {false};
+ final long[] downTime = {0};
+
+ setOnTouchListener((v, event) -> {
+ switch (event.getAction()) {
+ case MotionEvent.ACTION_DOWN:
+ downPos[0] = event.getX();
+ downPos[1] = event.getY();
+ hasMoved[0] = false;
+ downTime[0] = System.currentTimeMillis();
+
+ animate()
+ .scaleX(0.9f)
+ .scaleY(0.9f)
+ .setDuration(100)
+ .start();
+ return true;
+
+ case MotionEvent.ACTION_MOVE:
+ float moveX = event.getX();
+ float moveY = event.getY();
+ float deltaX = moveX - downPos[0];
+ float deltaY = moveY - downPos[1];
+
+ if (Math.abs(deltaX) > 10 || Math.abs(deltaY) > 10) {
+ hasMoved[0] = true;
+ setTranslationX(getTranslationX() + deltaX);
+ setTranslationY(getTranslationY() + deltaY);
+ }
+ return true;
+
+ case MotionEvent.ACTION_UP:
+ case MotionEvent.ACTION_CANCEL:
+ animate()
+ .scaleX(1f)
+ .scaleY(1f)
+ .setDuration(100)
+ .start();
+
+ long clickDuration = System.currentTimeMillis() - downTime[0];
+ if (!hasMoved[0] && clickDuration < 300) {
+ toggleMenus();
+ }
+ return true;
+ }
+ return false;
+ });
+ }
+
+ private void toggleMenus() {
+ menusVisible = !menusVisible;
+
+ if (menusVisible) {
+ showMenus();
+ } else {
+ hideMenus();
+ }
+ }
+
+ private void showMenus() {
+ if (menusContainer == null) return;
+
+ menusContainer.removeAllViews();
+ menusContainer.setVisibility(View.VISIBLE);
+
+ for (MenuView menu : menuViews) {
+ if (menu.getParent() != null) {
+ ((FrameLayout) menu.getParent()).removeView(menu);
+ }
+
+ menusContainer.addView(menu);
+ menu.show();
+ }
+ }
+
+ private void hideMenus() {
+ if (menusContainer == null) return;
+
+ for (MenuView menu : menuViews) {
+ menu.hide();
+ }
+
+ postDelayed(() -> {
+ try {
+ for (MenuView menu : menuViews) {
+ if (menu.getParent() != null) {
+ ((FrameLayout) menu.getParent()).removeView(menu);
+ }
+ }
+ menusContainer.setVisibility(View.GONE);
+ } catch (Exception e) {
+ e.printStackTrace();
+ }
+ }, 300);
+ }
+
+ public void show() {
+ setVisibility(View.VISIBLE);
+ setAlpha(0f);
+ setScaleX(0.8f);
+ setScaleY(0.8f);
+ animate()
+ .alpha(1f)
+ .scaleX(1f)
+ .scaleY(1f)
+ .setDuration(250)
+ .start();
+ }
+
+ public void hide() {
+ if (menusVisible) {
+ hideMenus();
+ menusVisible = false;
+ }
+
+ animate()
+ .alpha(0f)
+ .scaleX(0.8f)
+ .scaleY(0.8f)
+ .setDuration(250)
+ .withEndAction(() -> setVisibility(View.GONE))
+ .start();
+ }
+
+ public void destroy() {
+ try {
+ for (MenuView menu : menuViews) {
+ if (menu.getParent() != null) {
+ ((FrameLayout) menu.getParent()).removeView(menu);
+ }
+ menu.destroy();
+ }
+ menuViews.clear();
+
+ if (menusContainer != null) {
+ menusContainer.removeAllViews();
+ }
+ } catch (Exception e) {
+ e.printStackTrace();
+ }
+ }
+
+ private int getScreenHeight(Context context) {
+ WindowManager windowManager = (WindowManager) context.getSystemService(Context.WINDOW_SERVICE);
+ DisplayMetrics metrics = new DisplayMetrics();
+ windowManager.getDefaultDisplay().getRealMetrics(metrics);
+ return metrics.heightPixels;
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/phoenix/gui/ui/MenuView.java b/app/src/main/java/com/phoenix/gui/ui/MenuView.java
new file mode 100644
index 0000000..7d35657
--- /dev/null
+++ b/app/src/main/java/com/phoenix/gui/ui/MenuView.java
@@ -0,0 +1,516 @@
+package com.phoenix.gui.ui;
+
+import android.animation.ValueAnimator;
+import android.content.Context;
+import android.content.SharedPreferences;
+import android.graphics.Color;
+import android.graphics.Typeface;
+import android.graphics.drawable.GradientDrawable;
+import android.os.Build;
+import android.os.Handler;
+import android.os.Looper;
+import android.util.DisplayMetrics;
+import android.view.Gravity;
+import android.view.MotionEvent;
+import android.view.View;
+import android.view.ViewConfiguration;
+import android.view.ViewOutlineProvider;
+import android.view.WindowManager;
+import android.view.animation.DecelerateInterpolator;
+import android.widget.LinearLayout;
+import android.widget.ScrollView;
+import android.widget.TextView;
+import android.view.ViewGroup;
+import com.phoenix.gui.module.Module;
+import com.phoenix.gui.module.ModuleCategory;
+import com.phoenix.gui.module.ModuleManager;
+
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+public class MenuView extends LinearLayout implements ThemeManager.OnThemeColorChangeListener {
+
+ private static final String PREF_NAME = "menu_view_settings";
+ private static final String KEY_SCALE_FACTOR = "scale_factor";
+
+ private final Context context;
+ private final ModuleCategory category;
+ private final MenuPosition position;
+ private final SharedPreferences preferences;
+
+ private boolean isExpanded = false;
+
+ private final LinearLayout titleBar;
+ private final ScrollView scrollView;
+ private final LinearLayout contentLayout;
+
+ private final Map moduleItems = new HashMap<>();
+ private ValueAnimator expandAnimator;
+ private final Handler handler = new Handler(Looper.getMainLooper());
+
+ private float touchStartX = 0f;
+ private float touchStartY = 0f;
+ private boolean isDragging = false;
+ private final int touchSlop;
+
+ private float scaleFactor = 1.0f;
+
+ private final float baseScaleFactor;
+
+ private static final long ANIMATION_DURATION = 250L;
+ private static final float CORNER_RADIUS_DP = 8f;
+
+ private static int globalMaxWidth = 0;
+
+ private final Runnable statusChecker = new Runnable() {
+ @Override
+ public void run() {
+ if (getVisibility() == View.VISIBLE && isExpanded) {
+ refreshAllModuleStates();
+ handler.postDelayed(this, 500);
+ }
+ }
+ };
+
+ public enum MenuPosition {
+ POSITION_1, POSITION_2, POSITION_3, POSITION_4, POSITION_5
+ }
+
+ public MenuView(Context context, ModuleCategory category, MenuPosition position) {
+ super(context);
+ this.context = context;
+ this.category = category;
+ this.position = position;
+ this.preferences = context.getSharedPreferences(PREF_NAME, Context.MODE_PRIVATE);
+ this.touchSlop = ViewConfiguration.get(context).getScaledTouchSlop();
+
+ this.baseScaleFactor = calculateBaseScaleFactor() * 0.75f;
+
+ this.scaleFactor = 1.0f;
+
+ setOrientation(VERTICAL);
+
+ List modules = ModuleManager.getModulesByCategory(category);
+
+ GradientDrawable bg = new GradientDrawable();
+ bg.setColor(ThemeManager.getBgPrimary());
+ bg.setCornerRadius(scaled(CORNER_RADIUS_DP));
+ setBackground(bg);
+
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
+ setClipToOutline(true);
+ setOutlineProvider(ViewOutlineProvider.BACKGROUND);
+ setElevation(scaled(12));
+ } else {
+ setClipChildren(true);
+ setClipToPadding(true);
+ }
+
+ ViewGroup.LayoutParams layoutParams = new ViewGroup.LayoutParams(
+ ViewGroup.LayoutParams.WRAP_CONTENT,
+ ViewGroup.LayoutParams.WRAP_CONTENT
+ );
+ setLayoutParams(layoutParams);
+
+ int titleHeight = scaled(36);
+ int spacing = scaled(12);
+ setTranslationY(position.ordinal() * (titleHeight + spacing));
+
+ titleBar = createTitleBar();
+ addView(titleBar);
+
+ scrollView = new ScrollView(context);
+ scrollView.setLayoutParams(new LayoutParams(LayoutParams.MATCH_PARENT, 0));
+ scrollView.setVerticalScrollBarEnabled(false);
+ scrollView.setVisibility(View.GONE);
+ scrollView.setClipChildren(false);
+ scrollView.setClipToPadding(false);
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.HONEYCOMB) {
+ scrollView.setLayerType(View.LAYER_TYPE_HARDWARE, null);
+ }
+
+ contentLayout = new LinearLayout(context);
+ contentLayout.setLayoutParams(new LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.WRAP_CONTENT));
+ contentLayout.setOrientation(VERTICAL);
+ contentLayout.setPadding(0, 0, 0, 0);
+ contentLayout.setClipChildren(false);
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.HONEYCOMB) {
+ contentLayout.setLayerType(View.LAYER_TYPE_HARDWARE, null);
+ }
+
+ for (Module module : modules) {
+ ModuleItemView item = new ModuleItemView(context, module, this, baseScaleFactor * scaleFactor);
+ moduleItems.put(module, item);
+ contentLayout.addView(item);
+ }
+
+ scrollView.addView(contentLayout);
+ addView(scrollView);
+
+ setupTouchListeners();
+
+ ThemeManager.addListener(this);
+ }
+
+ private void refreshAllModuleStates() {
+ for (Map.Entry entry : moduleItems.entrySet()) {
+ Module module = entry.getKey();
+ ModuleItemView item = entry.getValue();
+ item.syncWithModuleState(module.isEnabled());
+ }
+ }
+
+ private float calculateBaseScaleFactor() {
+ WindowManager windowManager = (WindowManager) context.getSystemService(Context.WINDOW_SERVICE);
+ DisplayMetrics metrics = new DisplayMetrics();
+ windowManager.getDefaultDisplay().getRealMetrics(metrics);
+ float dpi = metrics.densityDpi;
+
+ if (dpi <= 240) {
+ return 0.7f;
+ } else if (dpi <= 320) {
+ return 0.85f;
+ } else if (dpi <= 480) {
+ return 1.0f;
+ } else if (dpi <= 640) {
+ return 1.15f;
+ } else {
+ return 1.3f;
+ }
+ }
+
+ private int scaled(int baseDp) {
+ return (int) (dpToPx(baseDp) * baseScaleFactor * scaleFactor);
+ }
+
+ private float scaled(float baseDp) {
+ return dpToPx((int) baseDp) * baseScaleFactor * scaleFactor;
+ }
+
+ public float getScaleFactor() {
+ return scaleFactor;
+ }
+
+ public void setScaleFactor(float factor) {
+ factor = Math.max(0.5f, Math.min(2.0f, factor));
+ if (Math.abs(this.scaleFactor - factor) < 0.01f) {
+ return;
+ }
+
+ this.scaleFactor = factor;
+
+ preferences.edit()
+ .putFloat(KEY_SCALE_FACTOR, factor)
+ .apply();
+
+ rebuildUI();
+ }
+
+ public void increaseScale() {
+ setScaleFactor(scaleFactor * 1.1f);
+ }
+
+ public void decreaseScale() {
+ setScaleFactor(scaleFactor * 0.9f);
+ }
+
+ public void resetScale() {
+ setScaleFactor(1.0f);
+ }
+
+ private void rebuildUI() {
+ boolean wasExpanded = isExpanded;
+
+ if (isExpanded) {
+ collapseContent();
+ }
+
+ GradientDrawable bg = (GradientDrawable) getBackground();
+ bg.setCornerRadius(scaled(CORNER_RADIUS_DP));
+
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
+ setElevation(scaled(12));
+ }
+
+ updateTitleBar();
+
+ for (ModuleItemView item : moduleItems.values()) {
+ item.updateScale(baseScaleFactor * scaleFactor);
+ }
+
+ globalMaxWidth = 0;
+ measureInitialSize();
+
+ if (wasExpanded) {
+ handler.postDelayed(this::expandContent, 100);
+ }
+ }
+
+ private void updateTitleBar() {
+ int height = scaled(36);
+ titleBar.setLayoutParams(new LayoutParams(LayoutParams.MATCH_PARENT, height));
+ titleBar.setPadding(scaled(14), 0, scaled(14), 0);
+
+ GradientDrawable bg = new GradientDrawable();
+ bg.setColor(ThemeManager.getBgSecondary());
+ bg.setCornerRadius(scaled(CORNER_RADIUS_DP));
+ titleBar.setBackground(bg);
+
+ TextView textView = (TextView) titleBar.getChildAt(0);
+ textView.setTextSize(11.5f * baseScaleFactor * scaleFactor);
+ }
+
+ private LinearLayout createTitleBar() {
+ LinearLayout bar = new LinearLayout(context);
+ int height = scaled(36);
+ bar.setLayoutParams(new LayoutParams(LayoutParams.MATCH_PARENT, height));
+ bar.setGravity(Gravity.START | Gravity.CENTER_VERTICAL);
+ bar.setPadding(scaled(14), 0, scaled(14), 0);
+
+ GradientDrawable bg = new GradientDrawable();
+ bg.setColor(ThemeManager.getBgSecondary());
+ bg.setCornerRadius(scaled(CORNER_RADIUS_DP));
+ bar.setBackground(bg);
+
+ TextView textView = new TextView(context);
+ textView.setText(category.getDisplayName());
+ textView.setTextSize(11.5f * baseScaleFactor * scaleFactor);
+ textView.setTextColor(ThemeManager.getTextPrimary());
+ textView.setTypeface(Typeface.DEFAULT_BOLD);
+ bar.addView(textView);
+
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.HONEYCOMB) {
+ bar.setLayerType(View.LAYER_TYPE_HARDWARE, null);
+ }
+
+ return bar;
+ }
+
+ private void setupTouchListeners() {
+ final float[] lastRaw = new float[2];
+
+ titleBar.setOnTouchListener((v, event) -> {
+ switch (event.getAction()) {
+ case MotionEvent.ACTION_DOWN:
+ touchStartX = event.getX();
+ touchStartY = event.getY();
+ lastRaw[0] = event.getRawX();
+ lastRaw[1] = event.getRawY();
+ isDragging = false;
+ return true;
+
+ case MotionEvent.ACTION_MOVE:
+ float deltaX = Math.abs(event.getX() - touchStartX);
+ float deltaY = Math.abs(event.getY() - touchStartY);
+
+ if (!isDragging && (deltaX > touchSlop || deltaY > touchSlop)) {
+ isDragging = true;
+ }
+
+ if (isDragging) {
+ float nowRawX = event.getRawX();
+ float nowRawY = event.getRawY();
+ float moveDeltaX = nowRawX - lastRaw[0];
+ float moveDeltaY = nowRawY - lastRaw[1];
+ MenuView.this.setTranslationX(MenuView.this.getTranslationX() + moveDeltaX);
+ MenuView.this.setTranslationY(MenuView.this.getTranslationY() + moveDeltaY);
+ lastRaw[0] = nowRawX;
+ lastRaw[1] = nowRawY;
+ }
+ return true;
+
+ case MotionEvent.ACTION_UP:
+ if (!isDragging) {
+ toggleExpand();
+ }
+ isDragging = false;
+ return true;
+
+ case MotionEvent.ACTION_CANCEL:
+ isDragging = false;
+ return true;
+ }
+ return false;
+ });
+ }
+
+ private void toggleExpand() {
+ if (expandAnimator != null) expandAnimator.cancel();
+
+ if (isExpanded) {
+ collapseContent();
+ } else {
+ expandContent();
+ }
+ }
+
+ private void expandContent() {
+ isExpanded = true;
+ scrollView.setVisibility(View.VISIBLE);
+
+ WindowManager windowManager = (WindowManager) context.getSystemService(Context.WINDOW_SERVICE);
+ DisplayMetrics screenSize = new DisplayMetrics();
+ windowManager.getDefaultDisplay().getRealMetrics(screenSize);
+
+ contentLayout.measure(
+ MeasureSpec.makeMeasureSpec(globalMaxWidth, MeasureSpec.AT_MOST),
+ MeasureSpec.makeMeasureSpec(0, MeasureSpec.UNSPECIFIED)
+ );
+ int maxHeight = (int) (screenSize.heightPixels * 0.5);
+ int targetHeight = Math.min(contentLayout.getMeasuredHeight(), maxHeight);
+
+ scrollView.setAlpha(0f);
+ LayoutParams lp = (LayoutParams) scrollView.getLayoutParams();
+ lp.height = 0;
+ scrollView.setLayoutParams(lp);
+
+ expandAnimator = ValueAnimator.ofInt(0, targetHeight);
+ expandAnimator.setDuration(ANIMATION_DURATION);
+ expandAnimator.setInterpolator(new DecelerateInterpolator());
+ expandAnimator.addUpdateListener(animator -> {
+ int value = (int) animator.getAnimatedValue();
+ LayoutParams params = (LayoutParams) scrollView.getLayoutParams();
+ params.height = value;
+ scrollView.setLayoutParams(params);
+ scrollView.setAlpha((float) value / targetHeight);
+ });
+ expandAnimator.start();
+
+ handler.post(statusChecker);
+ }
+
+ private void collapseContent() {
+ isExpanded = false;
+ int startHeight = scrollView.getHeight();
+
+ closeAllSubMenus();
+
+ handler.removeCallbacks(statusChecker);
+
+ expandAnimator = ValueAnimator.ofInt(startHeight, 0);
+ expandAnimator.setDuration(ANIMATION_DURATION);
+ expandAnimator.setInterpolator(new DecelerateInterpolator());
+ expandAnimator.addUpdateListener(animator -> {
+ int value = (int) animator.getAnimatedValue();
+ LayoutParams params = (LayoutParams) scrollView.getLayoutParams();
+ params.height = value;
+ scrollView.setLayoutParams(params);
+ if (startHeight > 0) {
+ scrollView.setAlpha((float) value / startHeight);
+ }
+ });
+ expandAnimator.addListener(new android.animation.AnimatorListenerAdapter() {
+ @Override
+ public void onAnimationEnd(android.animation.Animator animation) {
+ scrollView.setVisibility(View.GONE);
+ }
+ });
+ expandAnimator.start();
+ }
+
+ public void show() {
+ setVisibility(View.VISIBLE);
+ measureInitialSize();
+
+ setAlpha(0f);
+ setScaleX(0.8f);
+ setScaleY(0.8f);
+ animate()
+ .alpha(1f)
+ .scaleX(1f)
+ .scaleY(1f)
+ .setDuration(ANIMATION_DURATION)
+ .setInterpolator(new DecelerateInterpolator())
+ .start();
+
+ if (isExpanded) {
+ handler.post(statusChecker);
+ }
+ }
+
+ private void measureInitialSize() {
+ int maxWidth = globalMaxWidth;
+
+ titleBar.measure(
+ MeasureSpec.makeMeasureSpec(0, MeasureSpec.UNSPECIFIED),
+ MeasureSpec.makeMeasureSpec(0, MeasureSpec.UNSPECIFIED)
+ );
+ maxWidth = Math.max(maxWidth, titleBar.getMeasuredWidth());
+
+ for (ModuleItemView item : moduleItems.values()) {
+ item.measure(
+ MeasureSpec.makeMeasureSpec(0, MeasureSpec.UNSPECIFIED),
+ MeasureSpec.makeMeasureSpec(0, MeasureSpec.UNSPECIFIED)
+ );
+ maxWidth = Math.max(maxWidth, item.getMeasuredWidth());
+ }
+
+ WindowManager windowManager = (WindowManager) context.getSystemService(Context.WINDOW_SERVICE);
+DisplayMetrics screenSize = new DisplayMetrics();
+windowManager.getDefaultDisplay().getRealMetrics(screenSize);
+int smallerDimension = Math.min(screenSize.widthPixels, screenSize.heightPixels);
+int minWidth = (int) (smallerDimension * 0.3);
+int maxAllowedWidth = (int) (smallerDimension * 0.5);
+maxWidth = Math.max(minWidth, Math.min(maxWidth, maxAllowedWidth)); // 你说屎吧是屎 但是好像还有可读性
+ maxWidth = Math.max(minWidth, Math.min(maxWidth, maxAllowedWidth));
+
+ if (maxWidth > globalMaxWidth) {
+ globalMaxWidth = maxWidth;
+ }
+
+ ViewGroup.LayoutParams params = getLayoutParams();
+ if (params != null) {
+ params.width = globalMaxWidth;
+ setLayoutParams(params);
+ }
+ }
+
+ public void hide() {
+ handler.removeCallbacks(statusChecker);
+
+ if (expandAnimator != null) expandAnimator.cancel();
+ animate().cancel();
+
+ for (ModuleItemView item : moduleItems.values()) {
+ item.cancelAllAnimations();
+ }
+
+ animate()
+ .alpha(0f)
+ .scaleX(0.8f)
+ .scaleY(0.8f)
+ .setDuration(ANIMATION_DURATION)
+ .setInterpolator(new DecelerateInterpolator())
+ .withEndAction(() -> setVisibility(View.GONE))
+ .start();
+ }
+
+ public void closeAllSubMenus() {
+ for (ModuleItemView item : moduleItems.values()) {
+ item.closeSubMenuIfOpen();
+ }
+ }
+
+ public String getScaleInfo() {
+ return String.format("基础缩放: %.2f, 用户缩放: %.2f, 总缩放: %.2f", //debug
+ baseScaleFactor, scaleFactor, baseScaleFactor * scaleFactor);
+ }
+
+ @Override
+ public void onThemeColorChanged(int newColor) {
+ for (ModuleItemView item : moduleItems.values()) {
+ item.updateThemeColor();
+ }
+ }
+
+ public void destroy() {
+ ThemeManager.removeListener(this);
+ if (expandAnimator != null) expandAnimator.cancel();
+ handler.removeCallbacksAndMessages(null);
+ }
+
+ private int dpToPx(int dp) {
+ return (int) (dp * getResources().getDisplayMetrics().density);
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/phoenix/gui/ui/ModuleItemView.java b/app/src/main/java/com/phoenix/gui/ui/ModuleItemView.java
new file mode 100644
index 0000000..c9ee428
--- /dev/null
+++ b/app/src/main/java/com/phoenix/gui/ui/ModuleItemView.java
@@ -0,0 +1,355 @@
+package com.phoenix.gui.ui;
+
+import android.animation.ArgbEvaluator;
+import android.animation.TimeInterpolator;
+import android.animation.ValueAnimator;
+import android.content.Context;
+import android.graphics.Color;
+import android.graphics.Typeface;
+import android.graphics.drawable.GradientDrawable;
+import android.os.Handler;
+import android.os.Looper;
+import android.view.Gravity;
+import android.view.MotionEvent;
+import android.view.ViewConfiguration;
+import android.view.animation.DecelerateInterpolator;
+import android.view.animation.OvershootInterpolator;
+import android.widget.FrameLayout;
+import android.widget.LinearLayout;
+import android.widget.TextView;
+
+import com.phoenix.gui.module.Module;
+import com.phoenix.gui.ui.widgets.SubMenuPanel;
+
+public class ModuleItemView extends LinearLayout {
+
+ private final Module module;
+ private final MenuView parentMenu;
+
+ private final FrameLayout mainContainer;
+ private final TextView normalText;
+ private final TextView boldText;
+ private final SubMenuPanel subMenuPanel;
+
+ private ValueAnimator colorAnimator;
+ private ValueAnimator touchStateAnimator;
+ private final Handler longPressHandler = new Handler(Looper.getMainLooper());
+ private Runnable longPressRunnable;
+ private boolean isLongPressing = false;
+
+ private float touchStartX = 0f;
+ private float touchStartY = 0f;
+ private boolean hasMoved = false;
+ private final int touchSlop;
+
+ private float scaleFactor = 1.0f;
+
+ private boolean lastKnownState = false;
+
+ private static final long LONG_PRESS_DELAY = 500L;
+ private static final float PRESS_SCALE = 0.95f;
+ private static final int PRESS_ANIMATION_DURATION = 150;
+ private static final int RELEASE_ANIMATION_DURATION = 250;
+ private static final int CORNER_RADIUS_DP = 8;
+ private final float cornerRadiusPx;
+ private final TimeInterpolator pressInterpolator = new DecelerateInterpolator();
+ private final TimeInterpolator releaseInterpolator = new OvershootInterpolator(1.5f);
+
+ public ModuleItemView(Context context, Module module, MenuView parentMenu, float scaleFactor) {
+ super(context);
+ this.module = module;
+ this.parentMenu = parentMenu;
+ this.touchSlop = ViewConfiguration.get(context).getScaledTouchSlop();
+ this.lastKnownState = module.isEnabled();
+ this.scaleFactor = scaleFactor;
+ this.cornerRadiusPx = dpToPx(CORNER_RADIUS_DP);
+
+ setOrientation(VERTICAL);
+ setLayoutParams(new LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.WRAP_CONTENT));
+
+ mainContainer = createMainContainer();
+ addView(mainContainer);
+
+ normalText = createNormalText();
+ boldText = createBoldText();
+ mainContainer.addView(normalText);
+ mainContainer.addView(boldText);
+
+ subMenuPanel = new SubMenuPanel(context, module, scaleFactor);
+ addView(subMenuPanel);
+
+ module.configureSubMenu(subMenuPanel);
+
+ subMenuPanel.setOnStateChangedListener(() -> {
+
+ });
+
+ updateVisualState(module.isEnabled(), false);
+
+ setupTouchListeners();
+
+ if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.HONEYCOMB) {
+ setLayerType(LAYER_TYPE_HARDWARE, null);
+ }
+ }
+
+ public void syncWithModuleState(boolean currentState) {
+ if (lastKnownState != currentState) {
+ lastKnownState = currentState;
+ updateVisualState(currentState, true);
+ }
+ }
+
+ public void updateScale(float scaleFactor) {
+ this.scaleFactor = scaleFactor;
+
+ LayoutParams params = (LayoutParams) mainContainer.getLayoutParams();
+ params.setMargins(0, 0, 0, 0);
+ mainContainer.setLayoutParams(params);
+
+ float textSize = 11f * scaleFactor;
+ int paddingV = scaled(7);
+ int paddingH = scaled(12);
+
+ normalText.setTextSize(textSize);
+ normalText.setPadding(paddingH, paddingV, paddingH, paddingV);
+
+ boldText.setTextSize(textSize);
+ boldText.setPadding(paddingH, paddingV, paddingH, paddingV);
+
+ subMenuPanel.updateScale(scaleFactor);
+
+ requestLayout();
+ }
+
+ private int scaled(int baseDp) {
+ return (int) (dpToPx(baseDp) * scaleFactor);
+ }
+
+ private FrameLayout createMainContainer() {
+ FrameLayout container = new FrameLayout(getContext());
+
+ LayoutParams params = new LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.WRAP_CONTENT);
+ params.setMargins(0, 0, 0, 0);
+ container.setLayoutParams(params);
+
+ GradientDrawable initialBackground = new GradientDrawable();
+ initialBackground.setColor(Color.TRANSPARENT);
+ initialBackground.setCornerRadius(0f);
+ container.setBackground(initialBackground);
+
+ return container;
+ }
+
+
+
+ private TextView createNormalText() {
+ TextView textView = new TextView(getContext());
+ textView.setText(module.getName());
+ textView.setTextSize(11f * scaleFactor);
+ textView.setTextColor(ThemeManager.getTextPrimary());
+ textView.setGravity(Gravity.START | Gravity.CENTER_VERTICAL);
+ int paddingV = scaled(7);
+ int paddingH = scaled(12);
+ textView.setPadding(paddingH, paddingV, paddingH, paddingV);
+ textView.setTypeface(Typeface.DEFAULT);
+ textView.setLayoutParams(new FrameLayout.LayoutParams(
+ FrameLayout.LayoutParams.MATCH_PARENT,
+ FrameLayout.LayoutParams.WRAP_CONTENT
+ ));
+ return textView;
+ }
+
+ private TextView createBoldText() {
+ TextView textView = new TextView(getContext());
+ textView.setText(module.getName());
+ textView.setTextSize(11f * scaleFactor);
+ textView.setTextColor(ThemeManager.getTextOnTheme());
+ textView.setGravity(Gravity.START | Gravity.CENTER_VERTICAL);
+ int paddingV = scaled(7);
+ int paddingH = scaled(12);
+ textView.setPadding(paddingH, paddingV, paddingH, paddingV);
+ textView.setTypeface(Typeface.DEFAULT_BOLD);
+ textView.setLayoutParams(new FrameLayout.LayoutParams(
+ FrameLayout.LayoutParams.MATCH_PARENT,
+ FrameLayout.LayoutParams.WRAP_CONTENT
+ ));
+ textView.setAlpha(0f);
+ return textView;
+ }
+
+
+
+ private void setupTouchListeners() {
+ mainContainer.setOnTouchListener((v, event) -> {
+ switch (event.getAction()) {
+ case MotionEvent.ACTION_DOWN:
+ touchStartX = event.getX();
+ touchStartY = event.getY();
+ hasMoved = false;
+ isLongPressing = false;
+
+ longPressRunnable = () -> {
+ if (!hasMoved) {
+ isLongPressing = true;
+ onLongPress();
+ }
+ };
+ longPressHandler.postDelayed(longPressRunnable, LONG_PRESS_DELAY);
+
+
+ animateTouchState(true);
+ return true;
+
+ case MotionEvent.ACTION_MOVE:
+ float deltaX = Math.abs(event.getX() - touchStartX);
+ float deltaY = Math.abs(event.getY() - touchStartY);
+
+ boolean isOutOfBounds = event.getX() < 0 || event.getX() > getWidth() || event.getY() < 0 || event.getY() > getHeight();
+
+ if (!hasMoved && (deltaX > touchSlop || deltaY > touchSlop || isOutOfBounds)) {
+ hasMoved = true;
+ cancelLongPressDetection();
+
+ animateTouchState(false);
+ }
+ return true;
+
+ case MotionEvent.ACTION_UP:
+ case MotionEvent.ACTION_CANCEL:
+ cancelLongPressDetection();
+
+ animateTouchState(false);
+
+ if (!hasMoved && !isLongPressing) {
+ module.toggle();
+ lastKnownState = module.isEnabled();
+ updateVisualState(lastKnownState, true);
+ }
+
+ isLongPressing = false;
+ hasMoved = false;
+ return true;
+ }
+ return false;
+ });
+ }
+
+ private void animateTouchState(boolean pressed) {
+ if (touchStateAnimator != null) {
+ touchStateAnimator.cancel();
+ }
+
+
+ final float startScale = mainContainer.getScaleX();
+ final float endScale = pressed ? PRESS_SCALE : 1.0f;
+
+ final GradientDrawable background = (GradientDrawable) mainContainer.getBackground();
+ final float startRadius = background.getCornerRadii() != null ? background.getCornerRadii()[0] : 0f;
+ final float endRadius = pressed ? cornerRadiusPx : 0f;
+
+
+ touchStateAnimator = ValueAnimator.ofFloat(0f, 1f);
+ touchStateAnimator.setDuration(pressed ? PRESS_ANIMATION_DURATION : RELEASE_ANIMATION_DURATION);
+ touchStateAnimator.setInterpolator(pressed ? pressInterpolator : releaseInterpolator);
+
+ touchStateAnimator.addUpdateListener(animation -> {
+ float fraction = animation.getAnimatedFraction();
+ float currentScale = startScale + (endScale - startScale) * fraction;
+ float currentRadius = startRadius + (endRadius - startRadius) * fraction;
+
+ mainContainer.setScaleX(currentScale);
+ mainContainer.setScaleY(currentScale);
+ background.setCornerRadius(currentRadius);
+ });
+
+ touchStateAnimator.start();
+ }
+
+
+ private void onLongPress() {
+ performHapticFeedback(android.view.HapticFeedbackConstants.LONG_PRESS);
+ subMenuPanel.toggle();
+ }
+
+ private void cancelLongPressDetection() {
+ if (longPressRunnable != null) {
+ longPressHandler.removeCallbacks(longPressRunnable);
+ }
+ longPressRunnable = null;
+ }
+
+
+ public void updateVisualState(boolean isEnabled, boolean animated) {
+ if (animated) {
+ animateToState(isEnabled);
+ } else {
+ setStateImmediately(isEnabled);
+ }
+ }
+
+ private void setStateImmediately(boolean isEnabled) {
+ GradientDrawable bg = (GradientDrawable) mainContainer.getBackground();
+ bg.setColor(isEnabled ? ThemeManager.getThemeColor() : Color.TRANSPARENT);
+ bg.setCornerRadius(0f);
+
+ normalText.setAlpha(isEnabled ? 0f : 1f);
+ boldText.setAlpha(isEnabled ? 1f : 0f);
+ }
+
+ private void animateToState(boolean isEnabled) {
+ if (colorAnimator != null) {
+ colorAnimator.cancel();
+ }
+
+ GradientDrawable bg = (GradientDrawable) mainContainer.getBackground();
+
+ int fromColor = (bg.getColor() != null) ? bg.getColor().getDefaultColor() : Color.TRANSPARENT;
+ int toColor = isEnabled ? ThemeManager.getThemeColor() : Color.TRANSPARENT;
+
+ colorAnimator = ValueAnimator.ofObject(new ArgbEvaluator(), fromColor, toColor);
+ colorAnimator.setDuration(150);
+ colorAnimator.addUpdateListener(animator -> {
+ bg.setColor((int) animator.getAnimatedValue());
+ });
+ colorAnimator.start();
+
+ normalText.animate().alpha(isEnabled ? 0f : 1f).setDuration(150).start();
+ boldText.animate().alpha(isEnabled ? 1f : 0f).setDuration(150).start();
+ }
+
+
+ public void updateThemeColor() {
+ if (module.isEnabled()) {
+ GradientDrawable bg = (GradientDrawable) mainContainer.getBackground();
+ bg.setColor(ThemeManager.getThemeColor());
+ }
+ }
+
+ public void closeSubMenuIfOpen() {
+ if (subMenuPanel.isOpen()) {
+ subMenuPanel.forceCollapse();
+ }
+ }
+
+ public void cancelAllAnimations() {
+ if (colorAnimator != null) colorAnimator.cancel();
+
+ if (touchStateAnimator != null) touchStateAnimator.cancel();
+ mainContainer.animate().cancel(); //保留
+ normalText.animate().cancel();
+ boldText.animate().cancel();
+ cancelLongPressDetection();
+ }
+
+ @Override
+ protected void onDetachedFromWindow() {
+ super.onDetachedFromWindow();
+ cancelAllAnimations();
+ longPressHandler.removeCallbacksAndMessages(null);
+ }
+
+ private int dpToPx(int dp) {
+ return (int) (dp * getResources().getDisplayMetrics().density);
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/phoenix/gui/ui/ShortcutButton.java b/app/src/main/java/com/phoenix/gui/ui/ShortcutButton.java
new file mode 100644
index 0000000..890702a
--- /dev/null
+++ b/app/src/main/java/com/phoenix/gui/ui/ShortcutButton.java
@@ -0,0 +1,299 @@
+package com.phoenix.gui.ui;
+
+import android.animation.ValueAnimator;
+import android.content.Context;
+import android.content.SharedPreferences;
+import android.graphics.Color;
+import android.graphics.Paint;
+import android.graphics.Typeface;
+import android.graphics.drawable.GradientDrawable;
+import android.util.DisplayMetrics;
+import android.view.Gravity;
+import android.view.MotionEvent;
+import android.view.WindowManager;
+import android.widget.FrameLayout;
+import android.widget.LinearLayout;
+import android.widget.TextView;
+
+import com.phoenix.gui.module.Module;
+
+public class ShortcutButton extends LinearLayout implements ThemeManager.OnThemeColorChangeListener {
+
+ private static final String PREF_NAME = "shortcut_button_positions";
+ private static final String KEY_PREFIX_X = "button_x_";
+ private static final String KEY_PREFIX_Y = "button_y_";
+
+ private final Module module;
+ private final SharedPreferences preferences;
+
+ private final TextView textView;
+ private final GradientDrawable background;
+
+ private int currentBackgroundColor;
+ private int fixedTextWidth = -1;
+
+ public ShortcutButton(Context context, Module module) {
+ super(context);
+ this.module = module;
+ this.preferences = context.getSharedPreferences(PREF_NAME, Context.MODE_PRIVATE);
+
+ setOrientation(HORIZONTAL);
+ setGravity(Gravity.CENTER);
+
+ background = new GradientDrawable();
+ currentBackgroundColor = module.isEnabled()
+ ? ThemeManager.getThemeColor()
+ : ThemeManager.getBgDisabled();
+ background.setColor(currentBackgroundColor);
+ background.setCornerRadius(dpToPx(20));
+ setBackground(background);
+
+ int paddingH = dpToPx(12);
+ int paddingV = dpToPx(8);
+ setPadding(paddingH, paddingV, paddingH, paddingV);
+
+ textView = new TextView(context);
+ textView.setText(module.getName());
+ textView.setTextSize(11f);
+ int textColor = module.isEnabled()
+ ? ThemeManager.getTextOnTheme()
+ : ThemeManager.getTextPrimary();
+ textView.setTextColor(textColor);
+ textView.setTypeface(module.isEnabled()
+ ? Typeface.DEFAULT_BOLD
+ : Typeface.DEFAULT);
+ textView.setGravity(Gravity.CENTER);
+
+ fixedTextWidth = calculateMaxTextWidth();
+ LayoutParams textParams = new LayoutParams(fixedTextWidth, LayoutParams.WRAP_CONTENT);
+ textView.setLayoutParams(textParams);
+
+ addView(textView);
+
+
+ loadPosition();
+
+ setupTouchListeners();
+
+ ThemeManager.addListener(this);
+
+ setLayerType(LAYER_TYPE_HARDWARE, null);
+ }
+
+ private int calculateMaxTextWidth() {
+ Paint paint = new Paint();
+ paint.setTextSize(textView.getTextSize());
+
+ paint.setTypeface(Typeface.DEFAULT);
+ float normalWidth = paint.measureText(module.getName());
+
+ paint.setTypeface(Typeface.DEFAULT_BOLD);
+ float boldWidth = paint.measureText(module.getName());
+
+ return (int) Math.ceil(Math.max(normalWidth, boldWidth));
+ }
+
+ private void loadPosition() {
+ String key = getPositionKey();
+
+ boolean hasSavedPosition = preferences.contains(KEY_PREFIX_X + key)
+ && preferences.contains(KEY_PREFIX_Y + key);
+
+ if (hasSavedPosition) {
+ float x = preferences.getInt(KEY_PREFIX_X + key, 0);
+ float y = preferences.getInt(KEY_PREFIX_Y + key, 0);
+ setTranslationX(x);
+ setTranslationY(y);
+ } else {
+ generateRandomPosition();
+ }
+ }
+
+ private void generateRandomPosition() {
+ WindowManager windowManager = (WindowManager) getContext().getSystemService(Context.WINDOW_SERVICE);
+ DisplayMetrics metrics = new DisplayMetrics();
+ windowManager.getDefaultDisplay().getRealMetrics(metrics);
+
+ int estimatedWidth = dpToPx(150);
+ int estimatedHeight = dpToPx(50);
+
+ int margin = dpToPx(30);
+
+ int minX = margin - metrics.widthPixels / 2 + estimatedWidth / 2;
+ int maxX = metrics.widthPixels / 2 - estimatedWidth / 2 - margin;
+ int minY = margin - metrics.heightPixels / 2 + estimatedHeight / 2;
+ int maxY = metrics.heightPixels / 2 - estimatedHeight / 2 - margin;
+
+ if (maxX > minX) {
+ setTranslationX(minX + (int) (Math.random() * (maxX - minX)));
+ }
+
+ if (maxY > minY) {
+ setTranslationY(minY + (int) (Math.random() * (maxY - minY)));
+ }
+ }
+
+ private void savePosition() {
+ String key = getPositionKey();
+ preferences.edit()
+ .putInt(KEY_PREFIX_X + key, (int) getTranslationX())
+ .putInt(KEY_PREFIX_Y + key, (int) getTranslationY())
+ .apply();
+ }
+
+ private String getPositionKey() {
+ return module.getName().replaceAll("[^a-zA-Z0-9]", "_");
+ }
+
+ private void setupTouchListeners() {
+ final float[] downPos = new float[2];
+ final boolean[] hasMoved = {false};
+ final long[] downTime = {0};
+
+ setOnTouchListener((v, event) -> {
+ switch (event.getAction()) {
+ case MotionEvent.ACTION_DOWN:
+ downPos[0] = event.getX();
+ downPos[1] = event.getY();
+ hasMoved[0] = false;
+ downTime[0] = System.currentTimeMillis();
+
+ animate()
+ .scaleX(0.9f)
+ .scaleY(0.9f)
+ .setDuration(100)
+ .start();
+ return true;
+
+ case MotionEvent.ACTION_MOVE:
+ float moveX = event.getX();
+ float moveY = event.getY();
+ float deltaX = moveX - downPos[0];
+ float deltaY = moveY - downPos[1];
+
+ if (Math.abs(deltaX) > 10 || Math.abs(deltaY) > 10) {
+ hasMoved[0] = true;
+ setTranslationX(getTranslationX() + deltaX);
+ setTranslationY(getTranslationY() + deltaY);
+ }
+ return true;
+
+ case MotionEvent.ACTION_UP:
+
+ animate()
+ .scaleX(1f)
+ .scaleY(1f)
+ .setDuration(100)
+ .start();
+
+ long clickDuration = System.currentTimeMillis() - downTime[0];
+
+ if (!hasMoved[0] && clickDuration < 300) {
+ module.toggle();
+ updateStyle();
+ } else if (hasMoved[0]) {
+ savePosition();
+ }
+
+ hasMoved[0] = false;
+ return true;
+
+ case MotionEvent.ACTION_CANCEL:
+
+ animate()
+ .scaleX(1f)
+ .scaleY(1f)
+ .setDuration(100)
+ .start();
+ hasMoved[0] = false;
+ return true;
+ }
+ return false;
+ });
+ }
+
+ public void updateStyle() {
+ boolean isEnabled = module.isEnabled();
+ int targetColor = isEnabled ? ThemeManager.getThemeColor() : ThemeManager.getBgDisabled();
+ int textColor = isEnabled ? ThemeManager.getTextOnTheme() : ThemeManager.getTextPrimary();
+ Typeface targetTypeface = isEnabled ? Typeface.DEFAULT_BOLD : Typeface.DEFAULT;
+
+ ValueAnimator colorAnimator = ValueAnimator.ofArgb(currentBackgroundColor, targetColor);
+ colorAnimator.setDuration(150);
+ colorAnimator.addUpdateListener(animator -> {
+ int color = (int) animator.getAnimatedValue();
+ background.setColor(color);
+ currentBackgroundColor = color;
+ });
+ colorAnimator.start();
+
+ textView.animate()
+ .alpha(0.5f)
+ .setDuration(75)
+ .withEndAction(() -> {
+ textView.setTextColor(textColor);
+ textView.setTypeface(targetTypeface);
+ textView.animate()
+ .alpha(1f)
+ .setDuration(75)
+ .start();
+ })
+ .start();
+ }
+
+ public void show() {
+ setVisibility(VISIBLE);
+ setAlpha(0f);
+ setScaleX(0.5f);
+ setScaleY(0.5f);
+
+ animate()
+ .alpha(1f)
+ .scaleX(1f)
+ .scaleY(1f)
+ .setDuration(200)
+ .start();
+ }
+
+ public void hide() {
+ animate()
+ .alpha(0f)
+ .scaleX(0.5f)
+ .scaleY(0.5f)
+ .setDuration(200)
+ .withEndAction(() -> setVisibility(GONE))
+ .start();
+ }
+
+ @Override
+ public void onThemeColorChanged(int newColor) {
+ if (module.isEnabled()) {
+ ValueAnimator colorAnimator = ValueAnimator.ofArgb(currentBackgroundColor, newColor);
+ colorAnimator.setDuration(150);
+ colorAnimator.addUpdateListener(animator -> {
+ int color = (int) animator.getAnimatedValue();
+ background.setColor(color);
+ currentBackgroundColor = color;
+ });
+ colorAnimator.start();
+ }
+ }
+
+ public void destroy() {
+ clearAnimation();
+ if (animate() != null) {
+ animate().cancel();
+ }
+ if (textView != null && textView.animate() != null) {
+ textView.animate().cancel();
+ }
+
+ ThemeManager.removeListener(this);
+
+ savePosition();
+ }
+
+ private int dpToPx(int dp) {
+ return (int) (dp * getResources().getDisplayMetrics().density);
+ }
+}
diff --git a/app/src/main/java/com/phoenix/gui/ui/ThemeManager.java b/app/src/main/java/com/phoenix/gui/ui/ThemeManager.java
new file mode 100644
index 0000000..5d4dff4
--- /dev/null
+++ b/app/src/main/java/com/phoenix/gui/ui/ThemeManager.java
@@ -0,0 +1,139 @@
+package com.phoenix.gui.ui;
+
+import android.graphics.Color;
+import com.phoenix.gui.config.ConfigManager; // <-- 使用您的 ConfigManager
+
+import java.util.ArrayList;
+import java.util.List;
+
+public class ThemeManager {
+
+ private static final String CONFIG_MODULE_NAME = "GlobalTheme";
+ private static final String CONFIG_KEY_THEME_COLOR = "theme_color";
+ private static final int DEFAULT_THEME_COLOR = 0xFF96CCFF;
+
+ private static int themeColor = DEFAULT_THEME_COLOR;
+
+ private static int bgPrimary = 0xFF292D2C;
+ private static int bgSecondary = 0xFF1F2221;
+ private static int bgDisabled = 0xFF333333;
+
+ private static int textPrimary = 0xFFFFFFFF;
+ private static int textSecondary = 0xFFE0E0E0;
+ private static int textTertiary = 0xFFAAAAAA;
+ private static int textOnTheme = 0xFF1A1A1A;
+
+ private static int stateDisabled = 0xFF555555;
+
+ private static int glowColor = 0x66000000;
+ private static int glassBackground = 0xE6000000;
+
+ private static int gradientStart = 0xFFa7ff8a;
+ private static int gradientEnd = 0xFF82c1fb;
+
+ private static final List listeners = new ArrayList<>();
+
+ public interface OnThemeColorChangeListener {
+ void onThemeColorChanged(int newColor);
+ }
+
+ private static void saveThemeColor() {
+
+ ConfigManager.saveModuleConfig(CONFIG_MODULE_NAME, CONFIG_KEY_THEME_COLOR, themeColor);
+ }
+
+ public static int getThemeColor() {
+ return themeColor;
+ }
+
+ public static void setThemeColor(int color) {
+ if (themeColor != color) {
+ themeColor = color;
+ saveThemeColor();
+ notifyListeners();
+ }
+ }
+
+ public static float[] getThemeColorHSV() {
+ float[] hsv = new float[3];
+ Color.colorToHSV(themeColor, hsv);
+ return hsv;
+ }
+
+ public static int getBgPrimary() { return bgPrimary; }
+ public static void setBgPrimary(int color) { bgPrimary = color; notifyListeners(); }
+ public static int getBgSecondary() { return bgSecondary; }
+ public static void setBgSecondary(int color) { bgSecondary = color; notifyListeners(); }
+ public static int getBgDisabled() { return bgDisabled; }
+ public static void setBgDisabled(int color) { bgDisabled = color; notifyListeners(); }
+ public static int getTextPrimary() { return textPrimary; }
+ public static void setTextPrimary(int color) { textPrimary = color; notifyListeners(); }
+ public static int getTextSecondary() { return textSecondary; }
+ public static void setTextSecondary(int color) { textSecondary = color; notifyListeners(); }
+ public static int getTextTertiary() { return textTertiary; }
+ public static void setTextTertiary(int color) { textTertiary = color; notifyListeners(); }
+ public static int getTextOnTheme() { return textOnTheme; }
+ public static void setTextOnTheme(int color) { textOnTheme = color; notifyListeners(); }
+ public static int getStateDisabled() { return stateDisabled; }
+ public static void setStateDisabled(int color) { stateDisabled = color; notifyListeners(); }
+ public static int getGlowColor() { return glowColor; }
+ public static void setGlowColor(int color) { glowColor = color; notifyListeners(); }
+ public static int getGlassBackground() { return glassBackground; }
+ public static void setGlassBackground(int color) { glassBackground = color; notifyListeners(); }
+ public static int getGradientStart() { return gradientStart; }
+ public static void setGradientStart(int color) { gradientStart = color; notifyListeners(); }
+ public static int getGradientEnd() { return gradientEnd; }
+ public static void setGradientEnd(int color) { gradientEnd = color; notifyListeners(); }
+
+ public static void applyDefaultTheme() {
+ setThemeColor(DEFAULT_THEME_COLOR);
+ }
+
+ public static void applyGreenTheme() {
+ setThemeColor(0xFF4CAF50);
+ }
+
+ public static void applyPurpleTheme() {
+ setThemeColor(0xFFBB86FC);
+ }
+
+ public static void applyOrangeTheme() {
+ setThemeColor(0xFFFF9800);
+ }
+
+ public static void applyPinkTheme() {
+ setThemeColor(0xFFE91E63);
+ }
+
+ public static void setThemeColorRGB(int r, int g, int b) {
+ setThemeColor(Color.rgb(r, g, b));
+ }
+
+ public static void setThemeColorHSV(float hue, float saturation, float value) {
+ setThemeColor(Color.HSVToColor(new float[]{hue, saturation, value}));
+ }
+
+ public static void addListener(OnThemeColorChangeListener listener) {
+ if (!listeners.contains(listener)) {
+ listeners.add(listener);
+ }
+ }
+
+ public static void removeListener(OnThemeColorChangeListener listener) {
+ listeners.remove(listener);
+ }
+
+ private static void notifyListeners() {
+ for (OnThemeColorChangeListener listener : new ArrayList<>(listeners)) {
+ try {
+ listener.onThemeColorChanged(themeColor);
+ } catch (Exception e) {
+ e.printStackTrace();
+ }
+ }
+ }
+
+ private ThemeManager() {
+
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/phoenix/gui/ui/UI.java b/app/src/main/java/com/phoenix/gui/ui/UI.java
new file mode 100644
index 0000000..d359e51
--- /dev/null
+++ b/app/src/main/java/com/phoenix/gui/ui/UI.java
@@ -0,0 +1,256 @@
+package com.phoenix.gui.ui;
+
+import android.app.Activity;
+import android.content.Context;
+import android.os.Build;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.FrameLayout;
+import com.phoenix.gui.R;
+import com.phoenix.gui.config.ConfigManager;
+import com.phoenix.gui.module.ModuleManager;
+import com.phoenix.gui.module.ModuleToggleListener;
+import com.phoenix.gui.module.ShortcutToggleListener;
+import com.phoenix.gui.ui.dynamic.DynamicIslandWindow;
+
+// 封装
+
+public class UI {
+ private static DynamicColorExtractor dynamicColorExtractor;
+ private static boolean initialized = false;
+ private static boolean isShowing = false;
+
+ // UI
+ private static FloatBallView floatBall;
+ private static DynamicIslandWindow dynamicIsland;
+ private static ArraylistView arraylistView;
+
+ // 容器引用
+ private static FrameLayout guiContainer;
+ private static FrameLayout floatBallContainer;
+ private static FrameLayout menusContainer;
+ private static FrameLayout dynamicIslandContainer;
+ private static FrameLayout arraylistContainer;
+
+ // activity 引用
+ private static Activity currentActivity;
+
+
+ public static void init(Context ctx) {
+ if (initialized) {
+ return;
+ }
+
+
+ ConfigManager.init(ctx);
+
+
+ dynamicColorExtractor = new DynamicColorExtractor(ctx);
+ dynamicColorExtractor.init();
+
+ initialized = true;
+ }
+
+ public static void show(Activity activity) {
+ if (isShowing) {
+ return;
+ }
+
+ try {
+ currentActivity = activity;
+
+ // 绑定容器
+ bindContainers(activity);
+
+ // 显示主容器
+ if (guiContainer != null) {
+ guiContainer.setVisibility(View.VISIBLE);
+ }
+
+ // 初始化ppp
+ initFloatBall();
+ initDynamicIsland();
+ initArraylist();
+
+ // 注册
+ if (activity instanceof ModuleToggleListener) {
+ ModuleManager.addToggleListener((ModuleToggleListener) activity);
+ }
+ if (activity instanceof ShortcutToggleListener) {
+ ModuleManager.addShortcutListener((ShortcutToggleListener) activity);
+ }
+
+ isShowing = true;
+ } catch (Exception e) {
+ e.printStackTrace();
+ }
+ }
+
+
+ public static void hide() {
+ if (!isShowing) {
+ return; // 回家吃饭
+ }
+
+ try {
+ // 移除
+ if (currentActivity instanceof ModuleToggleListener) {
+ ModuleManager.removeToggleListener((ModuleToggleListener) currentActivity);
+ }
+ if (currentActivity instanceof ShortcutToggleListener) {
+ ModuleManager.removeShortcutListener((ShortcutToggleListener) currentActivity);
+ }
+
+ // 清理
+ if (arraylistView != null) {
+ arraylistView.clearModules();
+ arraylistView.hide();
+ arraylistView = null;
+ }
+
+ // 清理
+ if (dynamicIsland != null) {
+ dynamicIsland.hide();
+ dynamicIsland = null;
+ }
+
+ // 还是清理
+ if (floatBall != null) {
+ floatBall.hide();
+ floatBall.destroy();
+ floatBall = null;
+ }
+
+ // 清空垃圾桶
+ if (floatBallContainer != null) floatBallContainer.removeAllViews();
+ if (menusContainer != null) menusContainer.removeAllViews();
+ if (arraylistContainer != null) arraylistContainer.removeAllViews();
+
+ // 隐藏
+ if (guiContainer != null) {
+ guiContainer.setVisibility(View.GONE);
+ }
+
+ isShowing = false;
+ } catch (Exception e) {
+ e.printStackTrace();
+ }
+ }
+
+ private static void bindContainers(Activity activity) {
+ guiContainer = activity.findViewById(R.id.gui_container);
+ floatBallContainer = activity.findViewById(R.id.float_ball_container);
+ menusContainer = activity.findViewById(R.id.menus_container);
+ dynamicIslandContainer = activity.findViewById(R.id.dynamic_island_container);
+ arraylistContainer = activity.findViewById(R.id.arraylist_container);
+ }
+
+
+ private static void initFloatBall() {
+ try {
+ if (floatBall != null) {
+ floatBall.destroy();
+ floatBall = null;
+ }
+
+ if (floatBallContainer != null) {
+ floatBallContainer.removeAllViews();
+ floatBall = new FloatBallView(currentActivity, menusContainer);
+ floatBallContainer.addView(floatBall);
+ floatBall.show();
+ }
+ } catch (Exception e) {
+ e.printStackTrace();
+ }
+ }
+
+
+ private static void initDynamicIsland() {
+ boolean enabled = ConfigManager.getDynamicIslandEnabled();
+ if (!enabled) return;
+
+ try {
+ if (dynamicIsland != null) {
+ dynamicIsland.hide();
+ dynamicIsland = null;
+ }
+
+ if (dynamicIslandContainer != null) {
+ dynamicIslandContainer.removeAllViews();
+ dynamicIsland = new DynamicIslandWindow(currentActivity, dynamicIslandContainer);
+ dynamicIsland.show();
+ }
+ } catch (Exception e) {
+ e.printStackTrace();
+ }
+ }
+
+
+ private static void initArraylist() {
+ try {
+ if (arraylistView != null) {
+ arraylistView.clearModules();
+ arraylistView = null;
+ }
+
+ if (arraylistContainer != null) {
+ arraylistContainer.removeAllViews();
+ arraylistView = new ArraylistView(currentActivity);
+ FrameLayout.LayoutParams params = new FrameLayout.LayoutParams(
+ ViewGroup.LayoutParams.MATCH_PARENT,
+ ViewGroup.LayoutParams.MATCH_PARENT
+ );
+ arraylistContainer.addView(arraylistView, params);
+ addDefaultArraylistModules();
+ arraylistView.show();
+ }
+ } catch (Exception e) {
+ e.printStackTrace();
+ }
+ }
+
+
+ private static void addDefaultArraylistModules() {
+ if (arraylistView == null) return;
+
+ arraylistView.clearModules();
+ // arraylistView.addModule(new ArraylistModule("like this"));
+
+ }
+
+
+ public static void cleanup() {
+ hide();
+
+ if (dynamicColorExtractor != null && Build.VERSION.SDK_INT >= Build.VERSION_CODES.O_MR1) {
+ dynamicColorExtractor.stopListening();
+ dynamicColorExtractor = null;
+ }
+
+ currentActivity = null;
+ initialized = false;
+ }
+
+ public static DynamicColorExtractor getDynamicColorExtractor() {
+ return dynamicColorExtractor;
+ }
+
+
+ public static DynamicIslandWindow getDynamicIsland() {
+ return dynamicIsland;
+ }
+
+
+ public static ArraylistView getArraylistView() {
+ return arraylistView;
+ }
+
+
+ public static boolean isShowing() {
+ return isShowing;
+ }
+
+ private UI() {
+ // 工具类
+ }
+}
diff --git a/app/src/main/java/com/phoenix/gui/ui/dynamic/AnimatedTaskItemView.java b/app/src/main/java/com/phoenix/gui/ui/dynamic/AnimatedTaskItemView.java
new file mode 100644
index 0000000..078902c
--- /dev/null
+++ b/app/src/main/java/com/phoenix/gui/ui/dynamic/AnimatedTaskItemView.java
@@ -0,0 +1,60 @@
+package com.phoenix.gui.ui.dynamic;
+
+import android.content.Context;
+import android.os.Handler;
+import android.os.Looper;
+import androidx.annotation.NonNull;
+
+public abstract class AnimatedTaskItemView extends TaskItemView {
+
+ private final Handler updateHandler = new Handler(Looper.getMainLooper());
+ private final Runnable updateRunnable = new Runnable() {
+ @Override
+ public void run() {
+ updateContent();
+ updateHandler.postDelayed(this, 16);
+ }
+ };
+
+ private boolean isUpdating = false;
+
+ public AnimatedTaskItemView(@NonNull Context context, DynamicIslandManager.TaskItem task, float scale) {
+ super(context, task, scale);
+ }
+
+ protected void startUpdating() {
+ if (!isUpdating) {
+ isUpdating = true;
+ updateHandler.post(updateRunnable);
+ }
+ }
+
+ protected void stopUpdating() {
+ if (isUpdating) {
+ isUpdating = false;
+ updateHandler.removeCallbacks(updateRunnable);
+ }
+ }
+
+ @Override
+ protected void onAttachedToWindow() {
+ super.onAttachedToWindow();
+ startUpdating();
+ }
+
+ @Override
+ protected void onDetachedFromWindow() {
+ super.onDetachedFromWindow();
+ stopUpdating();
+ }
+
+ @Override
+ protected void onVisibilityChanged(android.view.View changedView, int visibility) {
+ super.onVisibilityChanged(changedView, visibility);
+ if (visibility == VISIBLE) {
+ startUpdating();
+ } else {
+ stopUpdating();
+ }
+ }
+}
diff --git a/app/src/main/java/com/phoenix/gui/ui/dynamic/CollapsedContentView.java b/app/src/main/java/com/phoenix/gui/ui/dynamic/CollapsedContentView.java
new file mode 100644
index 0000000..1362ecb
--- /dev/null
+++ b/app/src/main/java/com/phoenix/gui/ui/dynamic/CollapsedContentView.java
@@ -0,0 +1,301 @@
+package com.phoenix.gui.ui.dynamic;
+
+import android.animation.ValueAnimator;
+import android.content.Context;
+import android.graphics.Paint;
+import android.graphics.Typeface;
+import android.view.Gravity;
+import android.view.ViewGroup;
+import android.view.animation.OvershootInterpolator;
+import android.widget.FrameLayout;
+import android.widget.ImageView;
+import android.widget.LinearLayout;
+import android.widget.TextView;
+import androidx.annotation.NonNull;
+import com.phoenix.gui.ui.ThemeManager;
+
+public class CollapsedContentView extends LinearLayout implements ThemeManager.OnThemeColorChangeListener {
+
+ private ImageView brandIcon;
+ private TextView brandText;
+
+ private ImageView userIcon;
+ private TextView userText;
+
+ private ImageView fpsIcon;
+ private TextView fpsNumberText;
+ private TextView fpsLabelText;
+ private FrameLayout fpsNumberContainer;
+
+ private DynamicIslandManager manager;
+ private float scale = 0.7f;
+ private int currentFps = 0;
+
+ public CollapsedContentView(@NonNull Context context) {
+ super(context);
+ init(context);
+ }
+
+ private void init(Context context) {
+ setOrientation(HORIZONTAL);
+ setGravity(Gravity.CENTER);
+ setPadding(
+ (int) dpToPx(12 * scale), 0,
+ (int) dpToPx(12 * scale), 0
+ );
+
+ createBrandModule(context);
+ addSeparator(context);
+
+ createUserModule(context);
+ addSeparator(context);
+
+ createFpsModule(context);
+ }
+
+ private void createBrandModule(Context context) {
+ LinearLayout module = new LinearLayout(context);
+ module.setOrientation(HORIZONTAL);
+ module.setGravity(Gravity.CENTER_VERTICAL);
+
+ brandIcon = new ImageView(context);
+ try {
+ int iconId = context.getResources().getIdentifier("icon", "drawable", context.getPackageName());
+ if (iconId != 0) {
+ brandIcon.setImageResource(iconId);
+ } else {
+ brandIcon.setImageResource(android.R.drawable.ic_menu_info_details);
+ }
+ } catch (Exception e) {
+ brandIcon.setImageResource(android.R.drawable.ic_menu_info_details);
+ }
+ // 获取颜色。
+ brandIcon.setColorFilter(ThemeManager.getThemeColor());
+ int iconSize = (int) dpToPx(16 * scale);
+ module.addView(brandIcon, new LayoutParams(iconSize, iconSize));
+
+ module.addView(createSpacer(context, 4 * scale), new LayoutParams(
+ (int) dpToPx(4 * scale), 0
+ ));
+
+ brandText = createStyledText(context, "Phoen1xGUI", ThemeManager.getThemeColor(), true); // 神必小小小opai
+ module.addView(brandText);
+
+ addView(module);
+ }
+
+ private void createUserModule(Context context) {
+ LinearLayout module = new LinearLayout(context);
+ module.setOrientation(HORIZONTAL);
+ module.setGravity(Gravity.CENTER_VERTICAL);
+
+ userIcon = new ImageView(context);
+ userIcon.setImageResource(android.R.drawable.ic_menu_myplaces);
+ userIcon.setColorFilter(com.phoenix.gui.ui.ThemeManager.getTextSecondary());
+ int iconSize = (int) dpToPx(16 * scale);
+ module.addView(userIcon, new LayoutParams(iconSize, iconSize));
+
+ module.addView(createSpacer(context, 4 * scale), new LayoutParams(
+ (int) dpToPx(4 * scale), 0
+ ));
+
+ userText = createStyledText(context, "Test", com.phoenix.gui.ui.ThemeManager.getTextSecondary(), false); // 用户。
+ module.addView(userText);
+
+ addView(module);
+ }
+
+ private void createFpsModule(Context context) {
+ LinearLayout module = new LinearLayout(context);
+ module.setOrientation(HORIZONTAL);
+ module.setGravity(Gravity.CENTER_VERTICAL);
+
+ fpsIcon = new ImageView(context);
+ fpsIcon.setImageResource(android.R.drawable.ic_menu_manage);
+ fpsIcon.setColorFilter(com.phoenix.gui.ui.ThemeManager.getTextSecondary());
+ int iconSize = (int) dpToPx(16 * scale);
+ module.addView(fpsIcon, new LayoutParams(iconSize, iconSize));
+
+ module.addView(createSpacer(context, 4 * scale), new LayoutParams(
+ (int) dpToPx(4 * scale), 0
+ ));
+
+ fpsNumberContainer = new FrameLayout(context);
+ fpsNumberContainer.setClipChildren(false);
+ fpsNumberContainer.setClipToPadding(false);
+ module.addView(fpsNumberContainer, new LayoutParams(
+ LayoutParams.WRAP_CONTENT,
+ LayoutParams.WRAP_CONTENT
+ ));
+
+ fpsNumberText = createStyledText(context, "0", com.phoenix.gui.ui.ThemeManager.getTextSecondary(), false);
+ fpsNumberContainer.addView(fpsNumberText, new FrameLayout.LayoutParams(
+ FrameLayout.LayoutParams.WRAP_CONTENT,
+ FrameLayout.LayoutParams.WRAP_CONTENT
+ ));
+
+ fpsLabelText = createStyledText(context, " FPS", com.phoenix.gui.ui.ThemeManager.getTextSecondary(), false);
+ module.addView(fpsLabelText);
+
+ addView(module);
+ }
+
+ private TextView createStyledText(Context context, String text, int color, boolean bold) {
+ TextView textView = new TextView(context);
+ textView.setText(text);
+ textView.setTextColor(color);
+ textView.setTextSize(13 * scale);
+ textView.setTypeface(null, bold ? Typeface.BOLD : Typeface.NORMAL);
+ textView.setSingleLine(true);
+ textView.setIncludeFontPadding(false);
+ return textView;
+ }
+
+ private TextView createSeparator(Context context) {
+ TextView separator = new TextView(context);
+ separator.setText(" • ");
+
+ int separatorColor = (com.phoenix.gui.ui.ThemeManager.getTextSecondary() & 0x00FFFFFF) | 0x66000000;
+ separator.setTextColor(separatorColor);
+ separator.setTextSize(13 * scale);
+ separator.setIncludeFontPadding(false);
+ LayoutParams params = new LayoutParams(
+ LayoutParams.WRAP_CONTENT,
+ LayoutParams.WRAP_CONTENT
+ );
+ params.setMargins(
+ (int) dpToPx(4 * scale), 0,
+ (int) dpToPx(4 * scale), 0
+ );
+ return separator;
+ }
+
+ private void addSeparator(Context context) {
+ addView(createSeparator(context));
+ }
+
+ private android.view.View createSpacer(Context context, float widthDp) {
+ return new android.view.View(context);
+ }
+
+ public void setManager(DynamicIslandManager manager) {
+ this.manager = manager;
+ updateContent();
+ }
+
+ public void updateContent() {
+ if (manager == null) return;
+
+ userText.setText(manager.getPersistentText());
+ }
+
+ public void updateFps(int fps) {
+ if (currentFps == fps) return;
+
+ final String newFpsText = String.valueOf(fps);
+ currentFps = fps;
+
+ fpsNumberText.animate()
+ .translationY(-dpToPx(20 * scale))
+ .alpha(0f)
+ .scaleX(0.8f)
+ .scaleY(0.8f)
+ .setDuration(150)
+ .setInterpolator(new androidx.interpolator.view.animation.FastOutSlowInInterpolator())
+ .withEndAction(() -> {
+ final int startWidth = fpsNumberContainer.getWidth();
+
+ Paint textPaint = fpsNumberText.getPaint();
+ final int endWidth = (int) Math.ceil(textPaint.measureText(newFpsText));
+
+ fpsNumberText.setText(newFpsText);
+
+ fpsNumberText.setTranslationY(dpToPx(20 * scale));
+ fpsNumberText.setAlpha(0f);
+ fpsNumberText.setScaleX(1.2f);
+ fpsNumberText.setScaleY(1.2f);
+
+ fpsNumberText.animate()
+ .translationY(0f)
+ .alpha(1f)
+ .scaleX(1f)
+ .scaleY(1f)
+ .setDuration(400)
+ .setInterpolator(new OvershootInterpolator(2.5f))
+ .start();
+
+ if (startWidth != endWidth) {
+ ValueAnimator widthAnimator = ValueAnimator.ofInt(startWidth, endWidth);
+ widthAnimator.setDuration(400);
+ widthAnimator.setInterpolator(new OvershootInterpolator(2.5f));
+ widthAnimator.addUpdateListener(animation -> {
+ int animatedWidth = (Integer) animation.getAnimatedValue();
+ ViewGroup.LayoutParams params = fpsNumberContainer.getLayoutParams();
+ params.width = animatedWidth;
+ fpsNumberContainer.setLayoutParams(params);
+ });
+ widthAnimator.start();
+ }
+ })
+ .start();
+ }
+
+ public void updateConfig(float scale, String text) {
+ this.scale = scale;
+ userText.setText(text);
+
+ float textSize = 13 * scale;
+ brandText.setTextSize(textSize);
+ userText.setTextSize(textSize);
+ fpsNumberText.setTextSize(textSize);
+ fpsLabelText.setTextSize(textSize);
+
+ int iconSize = (int) dpToPx(16 * scale);
+ updateIconSize(brandIcon, iconSize);
+ updateIconSize(userIcon, iconSize);
+ updateIconSize(fpsIcon, iconSize);
+
+ setPadding(
+ (int) dpToPx(12 * scale), 0,
+ (int) dpToPx(12 * scale), 0
+ );
+
+ requestLayout();
+ }
+
+ private void updateIconSize(ImageView icon, int size) {
+ if (icon != null) {
+ LayoutParams params = (LayoutParams) icon.getLayoutParams();
+ params.width = size;
+ params.height = size;
+ icon.setLayoutParams(params);
+ }
+ }
+
+ private float dpToPx(float dp) {
+ return dp * getResources().getDisplayMetrics().density;
+ }
+
+ @Override
+ public void onThemeColorChanged(int newColor) {
+ if (brandIcon != null) {
+ brandIcon.setColorFilter(newColor);
+ }
+ if (brandText != null) {
+ brandText.setTextColor(newColor);
+ }
+ }
+
+ @Override
+ protected void onAttachedToWindow() {
+ super.onAttachedToWindow();
+ ThemeManager.addListener(this);
+ onThemeColorChanged(ThemeManager.getThemeColor());
+ }
+
+ @Override
+ protected void onDetachedFromWindow() {
+ super.onDetachedFromWindow();
+ ThemeManager.removeListener(this);
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/phoenix/gui/ui/dynamic/DynamicIslandManager.java b/app/src/main/java/com/phoenix/gui/ui/dynamic/DynamicIslandManager.java
new file mode 100644
index 0000000..2fb28b3
--- /dev/null
+++ b/app/src/main/java/com/phoenix/gui/ui/dynamic/DynamicIslandManager.java
@@ -0,0 +1,357 @@
+package com.phoenix.gui.ui.dynamic;
+
+import android.graphics.drawable.Drawable;
+import android.os.Handler;
+import android.os.Looper;
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import java.util.*;
+import java.util.concurrent.CopyOnWriteArrayList;
+
+public class DynamicIslandManager {
+
+ public static final long VALUE_PROGRESS_TIMEOUT_MS = 1000L;
+ public static final long SWITCH_DISPLAY_DURATION_MS = 500L;
+ public static final long TIME_PROGRESS_GRACE_PERIOD_MS = 1000L;
+
+ public static class TaskItem {
+ public enum Type { SWITCH, PROGRESS }
+
+ public final Type type;
+ public final String identifier;
+ public String text;
+ public String subtitle;
+ public boolean switchState;
+ public Drawable icon;
+ public boolean isTimeBased;
+ public long lastUpdateTime;
+ public boolean isAwaitingData;
+ public boolean removing;
+ public long duration;
+ public float displayProgress;
+ public float targetProgress;
+ public boolean isVisuallyHidden;
+
+ private Runnable hideRunnable;
+ private Runnable progressRunnable;
+
+ public TaskItem(Type type, String identifier, String text, @Nullable String subtitle) {
+ this.type = type;
+ this.identifier = identifier;
+ this.text = text;
+ this.subtitle = subtitle;
+ this.switchState = false;
+ this.icon = null;
+ this.isTimeBased = false;
+ this.lastUpdateTime = System.currentTimeMillis();
+ this.isAwaitingData = false;
+ this.removing = false;
+ this.duration = 0;
+ this.displayProgress = 1.0f;
+ this.targetProgress = 1.0f;
+ this.isVisuallyHidden = false;
+ }
+
+ public void cancelJobs(Handler handler) {
+ if (hideRunnable != null) {
+ handler.removeCallbacks(hideRunnable);
+ hideRunnable = null;
+ }
+ if (progressRunnable != null) {
+ handler.removeCallbacks(progressRunnable);
+ progressRunnable = null;
+ }
+ }
+ }
+
+ public interface StateChangeListener {
+ void onTasksChanged();
+ void onExpandedStateChanged(boolean isExpanded);
+ void onConfigChanged(float scale, String persistentText);
+ }
+
+ private float scale = 0.7f;
+ private String persistentText = "User";
+ private final List tasks = new CopyOnWriteArrayList<>();
+ private final List listeners = new CopyOnWriteArrayList<>();
+ private final Handler handler = new Handler(Looper.getMainLooper());
+ private int currentFps = 0;
+
+ private final Runnable updateRunnable = new Runnable() {
+ @Override
+ public void run() {
+ updateTasks();
+ handler.postDelayed(this, 250);
+ }
+ };
+
+ public DynamicIslandManager(float initialScale, String initialText) {
+ this.scale = Math.max(0.5f, Math.min(2.0f, initialScale));
+ this.persistentText = initialText;
+ handler.post(updateRunnable);
+ }
+
+ public float getScale() { return scale; }
+ public String getPersistentText() { return persistentText; }
+ public List getTasks() { return new ArrayList<>(tasks); }
+ public int getCurrentFps() { return currentFps; }
+ public void setCurrentFps(int fps) { this.currentFps = fps; }
+
+ public boolean isExpanded() {
+ for (TaskItem task : tasks) {
+ if (!task.removing && !task.isVisuallyHidden) return true;
+ }
+ return false;
+ }
+
+ public List getVisibleTasks() {
+ List activeSwitchTasks = new ArrayList<>();
+ List otherTasks = new ArrayList<>();
+
+ for (TaskItem task : tasks) {
+ if (task.removing || task.isVisuallyHidden) continue;
+
+ if (task.type == TaskItem.Type.SWITCH) {
+ activeSwitchTasks.add(task);
+ } else {
+ otherTasks.add(task);
+ }
+ }
+
+ if (!activeSwitchTasks.isEmpty()) {
+ return activeSwitchTasks;
+ } else {
+ return otherTasks;
+ }
+ }
+
+ public void updateConfig(float newScale, String newText) {
+ this.scale = Math.max(0.5f, Math.min(2.0f, newScale));
+ this.persistentText = newText;
+ notifyConfigChanged();
+ }
+
+ public void addSwitch(String identifier, String text, boolean state) {
+ android.util.Log.d("DynamicIsland", String.format("addSwitch: id=%s, text=%s, state=%s", identifier, text, state));
+
+ String mainTitle = "功能开关";
+ String subTitle = text + "|已被" + (state ? "开启" : "关闭");
+
+ int taskIndex = findTaskIndex(identifier);
+ TaskItem task;
+
+ if (taskIndex != -1) {
+ task = tasks.get(taskIndex);
+ task.cancelJobs(handler);
+ task.text = mainTitle;
+ task.subtitle = subTitle;
+ task.switchState = state;
+ task.lastUpdateTime = System.currentTimeMillis();
+ task.duration = SWITCH_DISPLAY_DURATION_MS;
+ task.removing = false;
+ task.isTimeBased = true;
+
+ startTimeBasedAnimation(task);
+ android.util.Log.d("DynamicIsland", "Updated existing switch task");
+ } else {
+ task = new TaskItem(TaskItem.Type.SWITCH, identifier, mainTitle, subTitle);
+ task.switchState = state;
+ task.duration = SWITCH_DISPLAY_DURATION_MS;
+ task.isTimeBased = true;
+
+ startTimeBasedAnimation(task);
+ tasks.add(0, task);
+ android.util.Log.d("DynamicIsland", "Created new switch task");
+ }
+
+ notifyTasksChanged();
+ }
+
+ public void addOrUpdateProgress(String identifier, String text,
+ @Nullable String subtitle, @Nullable Drawable icon,
+ @Nullable Float progress, @Nullable Long duration) {
+ int taskIndex = findTaskIndex(identifier);
+ if (taskIndex != -1) {
+ updateProgressInternal(tasks.get(taskIndex), text, subtitle, progress, duration);
+ } else {
+ addProgressInternal(identifier, text, subtitle, icon, progress, duration);
+ }
+ notifyTasksChanged();
+ }
+
+ public void removeTask(String identifier) {
+ int taskIndex = findTaskIndex(identifier);
+ if (taskIndex != -1) {
+ TaskItem task = tasks.get(taskIndex);
+ if (task.removing) return;
+
+ task.removing = true;
+ task.cancelJobs(handler);
+ notifyTasksChanged();
+
+ handler.postDelayed(() -> {
+ tasks.remove(task);
+ notifyTasksChanged();
+ }, 500);
+ }
+ }
+
+ public void hide() {
+ for (TaskItem task : tasks) {
+ task.cancelJobs(handler);
+ }
+ tasks.clear();
+ notifyTasksChanged();
+ }
+
+ public void destroy() {
+ handler.removeCallbacksAndMessages(null);
+ hide();
+ }
+
+ public void addListener(StateChangeListener listener) { listeners.add(listener); }
+ public void removeListener(StateChangeListener listener) { listeners.remove(listener); }
+
+ private void notifyTasksChanged() {
+ boolean expanded = isExpanded();
+ for (StateChangeListener listener : listeners) {
+ listener.onTasksChanged();
+ listener.onExpandedStateChanged(expanded);
+ }
+ }
+
+ private void notifyConfigChanged() {
+ for (StateChangeListener listener : listeners) {
+ handler.post(() -> listener.onConfigChanged(scale, persistentText));
+ }
+ }
+
+ private int findTaskIndex(String identifier) {
+ for (int i = 0; i < tasks.size(); i++) {
+ if (tasks.get(i).identifier.equals(identifier)) return i;
+ }
+ return -1;
+ }
+
+ private void updateTasks() {
+ if (tasks.isEmpty()) return;
+ long currentTime = System.currentTimeMillis();
+ boolean hasChanged = false;
+
+ Iterator iterator = tasks.iterator();
+ while (iterator.hasNext()) {
+ TaskItem task = iterator.next();
+ if (task.removing) continue;
+
+ boolean shouldBeMarkedForRemoval = false;
+ if (task.isTimeBased) {
+ if (task.displayProgress <= 0.01f && !task.isAwaitingData) {
+ task.isAwaitingData = true;
+ task.lastUpdateTime = currentTime;
+ }
+ if (task.isAwaitingData && (currentTime - task.lastUpdateTime > TIME_PROGRESS_GRACE_PERIOD_MS)) {
+ shouldBeMarkedForRemoval = true;
+ }
+ }
+ else if (task.type == TaskItem.Type.PROGRESS) {
+ if (currentTime - task.lastUpdateTime > VALUE_PROGRESS_TIMEOUT_MS) {
+ shouldBeMarkedForRemoval = true;
+ }
+ }
+
+ if (shouldBeMarkedForRemoval) {
+ task.removing = true;
+ hasChanged = true;
+ handler.postDelayed(() -> {
+ tasks.remove(task);
+ notifyTasksChanged();
+ }, 500);
+ }
+ }
+ if (hasChanged) {
+ notifyTasksChanged();
+ }
+ }
+
+ private void addProgressInternal(String identifier, String text, String subtitle,
+ Drawable icon, Float progressValue, Long duration) {
+ TaskItem newTask = new TaskItem(TaskItem.Type.PROGRESS, identifier, text, subtitle);
+ newTask.icon = icon != null ? icon.mutate() : null;
+ if (progressValue != null) {
+ newTask.isTimeBased = false;
+ newTask.displayProgress = 0f;
+ animateProgressTo(newTask, progressValue, 400);
+ } else {
+ newTask.isTimeBased = true;
+ newTask.duration = duration != null ? duration : 5000L;
+ startTimeBasedAnimation(newTask);
+ }
+ tasks.add(0, newTask);
+ }
+
+ private void updateProgressInternal(TaskItem task, String text, String subtitle,
+ Float progressValue, Long duration) {
+ task.text = text;
+ task.subtitle = subtitle;
+ task.lastUpdateTime = System.currentTimeMillis();
+ task.isAwaitingData = false;
+ task.removing = false;
+ task.cancelJobs(handler);
+
+ if (progressValue != null) {
+ task.isTimeBased = false;
+ animateProgressTo(task, progressValue, 400);
+ } else {
+ task.isTimeBased = true;
+ task.duration = duration != null ? duration : 5000L;
+ startTimeBasedAnimation(task);
+ }
+ }
+
+ private void animateProgressTo(TaskItem task, float targetProgress, long durationMs) {
+ task.cancelJobs(handler);
+ task.targetProgress = Math.max(0f, Math.min(1f, targetProgress));
+ final float startProgress = task.displayProgress;
+ final float delta = task.targetProgress - startProgress;
+
+ if (Math.abs(delta) < 0.001f) return;
+
+ final long startTime = System.currentTimeMillis();
+ final boolean isSwitch = task.type == TaskItem.Type.SWITCH;
+
+ task.progressRunnable = new Runnable() {
+ @Override
+ public void run() {
+ long elapsed = System.currentTimeMillis() - startTime;
+ float fraction = Math.min(1f, elapsed / (float) durationMs);
+ float easedFraction = (float) (1 - Math.pow(1 - fraction, 3));
+ task.displayProgress = startProgress + delta * easedFraction;
+
+ if (!isSwitch) {
+ notifyTasksChanged();
+ }
+
+ if (fraction < 1f) {
+ handler.postDelayed(this, 16);
+ } else {
+ task.displayProgress = task.targetProgress;
+ task.progressRunnable = null;
+
+ notifyTasksChanged();
+ }
+ }
+ };
+ handler.post(task.progressRunnable);
+ }
+
+ private void startTimeBasedAnimation(TaskItem task) {
+ task.cancelJobs(handler);
+
+ if (task.displayProgress < 1.0f) {
+ animateProgressTo(task, 1.0f, 500);
+ handler.postDelayed(() -> animateProgressTo(task, 0f, task.duration), 500);
+ } else {
+ animateProgressTo(task, 0f, task.duration);
+ }
+ }
+}
diff --git a/app/src/main/java/com/phoenix/gui/ui/dynamic/DynamicIslandView.java b/app/src/main/java/com/phoenix/gui/ui/dynamic/DynamicIslandView.java
new file mode 100644
index 0000000..d7bf098
--- /dev/null
+++ b/app/src/main/java/com/phoenix/gui/ui/dynamic/DynamicIslandView.java
@@ -0,0 +1,569 @@
+package com.phoenix.gui.ui.dynamic;
+
+import android.animation.ValueAnimator;
+import android.content.Context;
+import android.graphics.*;
+import android.text.TextPaint;
+import android.util.AttributeSet;
+import android.view.Choreographer;
+import android.view.Gravity;
+import android.view.View;
+import android.view.animation.OvershootInterpolator;
+import android.widget.FrameLayout;
+import android.widget.LinearLayout;
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import androidx.interpolator.view.animation.FastOutSlowInInterpolator;
+
+import com.phoenix.gui.ui.ThemeManager;
+
+import java.util.ArrayList;
+import java.util.List;
+
+public class DynamicIslandView extends FrameLayout implements DynamicIslandManager.StateChangeListener, ThemeManager.OnThemeColorChangeListener {
+
+ private static final float COLLAPSED_HEIGHT_DP = 36f;
+ private static final float EXPANDED_CORNER_RADIUS_DP = 28f;
+ private static final float ITEM_HEIGHT_DP = 52f;
+ private static final float VIEW_PADDING_DP = 12f;
+ private static final float HORIZONTAL_ITEM_PADDING_DP = 24f;
+ private static final float ICON_SIZE_DP = 40f;
+ private static final float ICON_SPACING_DP = 12f;
+ private static final float GLOW_BLUR_RADIUS_DP = 12f;
+ private static final float GLOW_SPREAD_RADIUS_DP = 4f;
+ private static final float SHADOW_SAFETY_MARGIN_DP = 8f;
+ private static final int SIZE_ANIMATION_DURATION_MS = 350;
+ private static final int CONTENT_FADE_OUT_MS = 100;
+ private static final int CONTENT_FADE_IN_MS = 200;
+
+ private DynamicIslandManager manager;
+
+ private Paint backgroundPaint;
+ private Paint glowPaint;
+ private RectF backgroundRect = new RectF();
+ private RectF glowRect = new RectF();
+
+ private float sheenPosition = -1.0f;
+ private ValueAnimator sheenAnimator;
+
+ private CollapsedContentView collapsedContent;
+ private FrameLayout expandedContainer;
+
+ private float currentHeight;
+ private float currentWidth;
+ private float currentCornerRadius;
+ private ValueAnimator sizeAnimator;
+ private boolean wasExpanded = false;
+
+ private TextPaint textPaint;
+
+ private int frameCount = 0;
+ private long lastFpsTime = System.nanoTime();
+
+ public DynamicIslandView(@NonNull Context context) {
+ super(context);
+ init(context);
+ }
+
+ public DynamicIslandView(@NonNull Context context, @Nullable AttributeSet attrs) {
+ super(context, attrs);
+ init(context);
+ }
+
+ private void init(Context context) {
+ setWillNotDraw(false);
+ setLayerType(LAYER_TYPE_SOFTWARE, null);
+ setClipChildren(false);
+ setClipToPadding(false);
+
+ textPaint = new TextPaint(Paint.ANTI_ALIAS_FLAG);
+
+ initPaints();
+ initSheenAnimation();
+ initContentViews(context);
+ startFpsMonitor();
+ }
+
+ private void initPaints() {
+ glowPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
+ glowPaint.setColor(ThemeManager.getGlowColor());
+ glowPaint.setStyle(Paint.Style.FILL);
+ glowPaint.setMaskFilter(new BlurMaskFilter(dpToPx(GLOW_BLUR_RADIUS_DP), BlurMaskFilter.Blur.NORMAL));
+
+ backgroundPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
+ backgroundPaint.setColor(ThemeManager.getGlassBackground());
+ backgroundPaint.setStyle(Paint.Style.FILL);
+ }
+
+ private void initSheenAnimation() {
+ sheenAnimator = ValueAnimator.ofFloat(-1.0f, 2.0f);
+ sheenAnimator.setDuration(2500);
+ sheenAnimator.setStartDelay(500);
+ sheenAnimator.setInterpolator(new android.view.animation.LinearInterpolator());
+ sheenAnimator.setRepeatCount(ValueAnimator.INFINITE);
+ sheenAnimator.setRepeatMode(ValueAnimator.RESTART);
+ sheenAnimator.addUpdateListener(animation -> {
+ sheenPosition = (float) animation.getAnimatedValue();
+ invalidate();
+ });
+ sheenAnimator.start();
+ }
+
+ private void initContentViews(Context context) {
+ collapsedContent = new CollapsedContentView(context);
+ collapsedContent.setVisibility(VISIBLE);
+ LayoutParams collapsedParams = new LayoutParams(LayoutParams.WRAP_CONTENT, LayoutParams.MATCH_PARENT);
+ collapsedParams.gravity = Gravity.CENTER;
+ addView(collapsedContent, collapsedParams);
+
+ expandedContainer = new FrameLayout(context);
+ expandedContainer.setVisibility(GONE);
+ expandedContainer.setAlpha(0f);
+ expandedContainer.setBackgroundColor(Color.TRANSPARENT);
+ LayoutParams expandedParams = new LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.MATCH_PARENT);
+ expandedParams.gravity = Gravity.CENTER;
+ addView(expandedContainer, expandedParams);
+ }
+
+ private void startFpsMonitor() {
+ Choreographer.getInstance().postFrameCallback(new Choreographer.FrameCallback() {
+ @Override
+ public void doFrame(long frameTimeNanos) {
+ frameCount++;
+ long elapsedNanos = frameTimeNanos - lastFpsTime;
+ if (elapsedNanos >= 1_000_000_000L) {
+ if (manager != null) {
+ manager.setCurrentFps(frameCount);
+ if (collapsedContent != null) {
+ collapsedContent.updateFps(frameCount);
+ }
+ }
+ frameCount = 0;
+ lastFpsTime = frameTimeNanos;
+ }
+ Choreographer.getInstance().postFrameCallback(this);
+ }
+ });
+ }
+
+ public void setManager(DynamicIslandManager manager) {
+ if (this.manager != null) {
+ this.manager.removeListener(this);
+ }
+ this.manager = manager;
+ this.manager.addListener(this);
+
+ if (collapsedContent != null) {
+ collapsedContent.setManager(manager);
+ }
+
+ updateContent();
+ }
+
+ @Override
+ protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
+ float scale = manager != null ? manager.getScale() : 0.7f;
+ int glowPadding = (int) (dpToPx(GLOW_BLUR_RADIUS_DP + GLOW_SPREAD_RADIUS_DP) * scale);
+ int safetyMargin = (int) (dpToPx(SHADOW_SAFETY_MARGIN_DP) * scale);
+ int totalPadding = glowPadding + safetyMargin;
+
+ int contentWidth = (int) currentWidth;
+ int contentHeight = (int) currentHeight;
+
+ if (currentWidth == 0 || currentHeight == 0) {
+ boolean isExpanded = manager != null && manager.isExpanded();
+ if (isExpanded) {
+ contentHeight = calculateExpandedHeight(scale);
+ contentWidth = calculateExpandedWidth(scale);
+ } else {
+ contentHeight = (int) (dpToPx(COLLAPSED_HEIGHT_DP) * scale);
+ contentWidth = calculateCollapsedWidth(scale);
+ }
+ currentHeight = contentHeight;
+ currentWidth = contentWidth;
+ currentCornerRadius = isExpanded ? dpToPx(EXPANDED_CORNER_RADIUS_DP) * scale : contentHeight / 2f;
+ }
+
+ int contentWidthSpec = MeasureSpec.makeMeasureSpec(contentWidth, MeasureSpec.AT_MOST);
+ int contentHeightSpec = MeasureSpec.makeMeasureSpec(contentHeight, MeasureSpec.EXACTLY);
+
+ measureChild(collapsedContent, contentWidthSpec, contentHeightSpec);
+ measureChild(expandedContainer, MeasureSpec.makeMeasureSpec(contentWidth, MeasureSpec.EXACTLY), contentHeightSpec);
+
+ setMeasuredDimension(
+ contentWidth + totalPadding * 2,
+ contentHeight + totalPadding * 2
+ );
+ }
+
+ @Override
+ protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
+ float scale = manager != null ? manager.getScale() : 0.7f;
+ int glowPadding = (int) (dpToPx(GLOW_BLUR_RADIUS_DP + GLOW_SPREAD_RADIUS_DP) * scale);
+ int safetyMargin = (int) (dpToPx(SHADOW_SAFETY_MARGIN_DP) * scale);
+ int totalPadding = glowPadding + safetyMargin;
+
+ int contentLeft = totalPadding;
+ int contentTop = totalPadding;
+ int contentRight = getWidth() - totalPadding;
+ int contentBottom = getHeight() - totalPadding;
+
+ int contentWidth = contentRight - contentLeft;
+ int collapsedWidth = collapsedContent.getMeasuredWidth();
+ int collapsedLeft = contentLeft + (contentWidth - collapsedWidth) / 2;
+ collapsedContent.layout(collapsedLeft, contentTop, collapsedLeft + collapsedWidth, contentBottom);
+
+ expandedContainer.layout(contentLeft, contentTop, contentRight, contentBottom);
+ }
+
+ @Override
+ protected void onDraw(Canvas canvas) {
+ super.onDraw(canvas);
+
+ float scale = manager != null ? manager.getScale() : 0.7f;
+ int glowPadding = (int) (dpToPx(GLOW_BLUR_RADIUS_DP + GLOW_SPREAD_RADIUS_DP) * scale);
+ int safetyMargin = (int) (dpToPx(SHADOW_SAFETY_MARGIN_DP) * scale);
+ int totalPadding = glowPadding + safetyMargin;
+
+ float contentLeft = totalPadding;
+ float contentTop = totalPadding;
+ float contentRight = getWidth() - totalPadding;
+ float contentBottom = getHeight() - totalPadding;
+
+ backgroundRect.set(contentLeft, contentTop, contentRight, contentBottom);
+
+ float spreadPx = dpToPx(GLOW_SPREAD_RADIUS_DP) * scale;
+ glowRect.set(
+ contentLeft - spreadPx,
+ contentTop - spreadPx,
+ contentRight + spreadPx,
+ contentBottom + spreadPx
+ );
+
+ canvas.drawRoundRect(glowRect, currentCornerRadius, currentCornerRadius, glowPaint);
+ canvas.drawRoundRect(backgroundRect, currentCornerRadius, currentCornerRadius, backgroundPaint);
+ drawSheen(canvas, backgroundRect, currentCornerRadius);
+ }
+
+ private void drawSheen(Canvas canvas, RectF rect, float cornerRadius) {
+ float gradientWidth = rect.width() * 0.5f;
+ float gradientStart = rect.width() * sheenPosition;
+
+ LinearGradient gradient = new LinearGradient(
+ gradientStart - gradientWidth, rect.top,
+ gradientStart, rect.bottom,
+ new int[]{0x00FFFFFF, 0x14FFFFFF, 0x00FFFFFF},
+ new float[]{0f, 0.5f, 1f},
+ Shader.TileMode.CLAMP
+ );
+
+ Paint sheenPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
+ sheenPaint.setShader(gradient);
+
+ canvas.save();
+ canvas.clipRect(rect);
+ canvas.drawRoundRect(rect, cornerRadius, cornerRadius, sheenPaint);
+ canvas.restore();
+ }
+
+ @Override
+ public void onTasksChanged() {
+ updateContent();
+ post(() -> animateSizeToFitContent());
+ }
+
+ @Override
+ public void onExpandedStateChanged(boolean isExpanded) {
+ if (wasExpanded != isExpanded) {
+ wasExpanded = isExpanded;
+ updateContent();
+ animateToState(isExpanded);
+ }
+ }
+
+ @Override
+ public void onConfigChanged(float scale, String persistentText) {
+ if (collapsedContent != null) {
+ collapsedContent.updateConfig(scale, persistentText);
+ }
+ requestLayout();
+ }
+
+ private void updateContent() {
+ if (manager == null) return;
+ boolean isExpanded = manager.isExpanded();
+
+ if (isExpanded) {
+ LinearLayout taskContainer = null;
+ if (expandedContainer.getChildCount() > 0 && expandedContainer.getChildAt(0) instanceof LinearLayout) {
+ taskContainer = (LinearLayout) expandedContainer.getChildAt(0);
+ }
+
+ if (taskContainer == null) {
+ rebuildExpandedContent();
+ } else {
+ if (!areTasksMatching(taskContainer)) {
+ rebuildExpandedContent();
+ } else {
+ updateExistingContent(taskContainer);
+ }
+ }
+ }
+
+ if (collapsedContent != null) {
+ collapsedContent.updateContent();
+ }
+ }
+
+ private boolean areTasksMatching(LinearLayout taskContainer) {
+ List tasks = manager.getVisibleTasks();
+
+ if (taskContainer.getChildCount() != tasks.size()) {
+ android.util.Log.d("DynamicIsland", String.format(
+ "Tasks not matching: count mismatch (view=%d, task=%d)",
+ taskContainer.getChildCount(), tasks.size()
+ ));
+ return false;
+ }
+
+ for (int i = 0; i < tasks.size(); i++) {
+ View child = taskContainer.getChildAt(i);
+ if (!(child instanceof TaskItemView)) {
+ android.util.Log.d("DynamicIsland", "Tasks not matching: child is not TaskItemView");
+ return false;
+ }
+
+ TaskItemView view = (TaskItemView) child;
+ DynamicIslandManager.TaskItem task = tasks.get(i);
+
+ if (!view.task.identifier.equals(task.identifier)) {
+ android.util.Log.d("DynamicIsland", String.format(
+ "Tasks not matching: identifier mismatch at %d (view=%s, task=%s)",
+ i, view.task.identifier, task.identifier
+ ));
+ return false;
+ }
+
+ if (view.task.type != task.type) {
+ android.util.Log.d("DynamicIsland", String.format(
+ "Tasks not matching: type mismatch at %d (view=%s, task=%s)",
+ i, view.task.type, task.type
+ ));
+ return false;
+ }
+ }
+
+ android.util.Log.d("DynamicIsland", "Tasks matching, using updateExistingContent");
+ return true;
+ }
+
+ private void updateExistingContent(LinearLayout taskContainer) {
+ List tasks = manager.getVisibleTasks();
+ android.util.Log.d("DynamicIsland", String.format("updateExistingContent: %d tasks", tasks.size()));
+
+ for (int i = 0; i < tasks.size(); i++) {
+ if (i >= taskContainer.getChildCount()) break;
+
+ View child = taskContainer.getChildAt(i);
+ if (!(child instanceof TaskItemView)) continue;
+
+ TaskItemView view = (TaskItemView) child;
+ DynamicIslandManager.TaskItem newTask = tasks.get(i);
+
+ view.task = newTask;
+
+ view.updateContent();
+ }
+ }
+
+ private void rebuildExpandedContent() {
+ android.util.Log.d("DynamicIsland", "rebuildExpandedContent called");
+ expandedContainer.removeAllViews();
+ LinearLayout taskContainer = new LinearLayout(getContext());
+ taskContainer.setOrientation(LinearLayout.VERTICAL);
+ taskContainer.setPadding(
+ 0, (int) (dpToPx(VIEW_PADDING_DP) * manager.getScale()),
+ 0, (int) (dpToPx(VIEW_PADDING_DP) * manager.getScale())
+ );
+ for (DynamicIslandManager.TaskItem task : manager.getVisibleTasks()) {
+ TaskItemView itemView = createTaskItemView(task);
+ taskContainer.addView(itemView);
+ }
+ expandedContainer.addView(taskContainer, new LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.MATCH_PARENT));
+ }
+
+ private TaskItemView createTaskItemView(DynamicIslandManager.TaskItem task) {
+ switch (task.type) {
+ case SWITCH:
+ return new SwitchItemView(getContext(), task, manager.getScale());
+ case PROGRESS:
+ return new ProgressItemView(getContext(), task, manager.getScale());
+ default:
+ throw new IllegalArgumentException("Unknown task type: " + task.type);
+ }
+ }
+
+ private void animateSizeToFitContent() {
+ if (manager == null) return;
+ boolean isExpanded = manager.isExpanded();
+ float scale = manager.getScale();
+
+ float targetHeight = isExpanded ? calculateExpandedHeight(scale) : dpToPx(COLLAPSED_HEIGHT_DP) * scale;
+ float targetWidth = isExpanded ? calculateExpandedWidth(scale) : calculateCollapsedWidth(scale);
+ float targetCorner = isExpanded ? dpToPx(EXPANDED_CORNER_RADIUS_DP) * scale : targetHeight / 2f;
+
+ if (Math.abs(targetHeight - currentHeight) > 1f || Math.abs(targetWidth - currentWidth) > 1f) {
+ animateSizeWithSpring(targetHeight, targetWidth, targetCorner);
+ }
+ }
+
+ private void animateToState(boolean expanded) {
+ if (sizeAnimator != null) sizeAnimator.cancel();
+ float scale = manager != null ? manager.getScale() : 0.7f;
+ float targetHeight = expanded ? calculateExpandedHeight(scale) : dpToPx(COLLAPSED_HEIGHT_DP) * scale;
+ float targetWidth = expanded ? calculateExpandedWidth(scale) : calculateCollapsedWidth(scale);
+ float targetCorner = expanded ? dpToPx(EXPANDED_CORNER_RADIUS_DP) * scale : targetHeight / 2f;
+
+ if (expanded) {
+ collapsedContent.animate().alpha(0f).scaleX(0.9f).scaleY(0.9f).setDuration(CONTENT_FADE_OUT_MS)
+ .setInterpolator(new FastOutSlowInInterpolator()).withEndAction(() -> collapsedContent.setVisibility(GONE)).start();
+ expandedContainer.setVisibility(VISIBLE);
+ expandedContainer.setAlpha(0f);
+ expandedContainer.setScaleX(0.9f);
+ expandedContainer.setScaleY(0.9f);
+ expandedContainer.animate().alpha(1f).scaleX(1f).scaleY(1f).setDuration(CONTENT_FADE_IN_MS)
+ .setInterpolator(new FastOutSlowInInterpolator()).start();
+ } else {
+ expandedContainer.animate().alpha(0f).scaleX(0.9f).scaleY(0.9f).setDuration(CONTENT_FADE_OUT_MS)
+ .setInterpolator(new FastOutSlowInInterpolator()).withEndAction(() -> expandedContainer.setVisibility(GONE)).start();
+ collapsedContent.setVisibility(VISIBLE);
+ collapsedContent.setAlpha(0f);
+ collapsedContent.setScaleX(0.9f);
+ collapsedContent.setScaleY(0.9f);
+ collapsedContent.animate().alpha(1f).scaleX(1f).scaleY(1f).setDuration(CONTENT_FADE_IN_MS)
+ .setInterpolator(new FastOutSlowInInterpolator()).start();
+ }
+ animateSizeWithSpring(targetHeight, targetWidth, targetCorner);
+ }
+
+ private void animateSizeWithSpring(float targetHeight, float targetWidth, float targetCorner) {
+ final float startHeight = currentHeight;
+ final float startWidth = currentWidth;
+ final float startCorner = currentCornerRadius;
+
+ if (sizeAnimator != null) sizeAnimator.cancel();
+
+ sizeAnimator = ValueAnimator.ofFloat(0f, 1f);
+ sizeAnimator.setDuration(SIZE_ANIMATION_DURATION_MS);
+ sizeAnimator.setInterpolator(new OvershootInterpolator(0.8f));
+
+ sizeAnimator.addUpdateListener(animation -> {
+ float fraction = animation.getAnimatedFraction();
+ currentHeight = startHeight + (targetHeight - startHeight) * fraction;
+ currentWidth = startWidth + (targetWidth - startWidth) * fraction;
+ currentCornerRadius = startCorner + (targetCorner - startCorner) * fraction;
+ requestLayout();
+ });
+
+ sizeAnimator.start();
+ }
+
+ private int calculateExpandedHeight(float scale) {
+ if (manager == null) return (int) (dpToPx(COLLAPSED_HEIGHT_DP) * scale);
+ int totalHeight = 0;
+ for (DynamicIslandManager.TaskItem task : manager.getVisibleTasks()) {
+ switch (task.type) {
+ case SWITCH:
+ totalHeight += dpToPx(ITEM_HEIGHT_DP) * scale;
+ break;
+ case PROGRESS:
+ totalHeight += dpToPx(ITEM_HEIGHT_DP + 8 + 12) * scale;
+ break;
+ }
+ }
+ return totalHeight + (int) (dpToPx(VIEW_PADDING_DP * 2) * scale);
+ }
+
+ private int calculateExpandedWidth(float scale) {
+ if (manager == null) return (int) (dpToPx(280) * scale);
+
+ int minWidth = (int) (dpToPx(280) * scale);
+ int maxWidth = minWidth;
+
+ for (DynamicIslandManager.TaskItem task : manager.getVisibleTasks()) {
+ int requiredWidth = calculateTaskWidth(task, scale);
+ maxWidth = Math.max(maxWidth, requiredWidth);
+ }
+
+ int absoluteMaxWidth = (int) (dpToPx(450) * scale);
+ return Math.min(maxWidth, absoluteMaxWidth);
+ }
+
+ private int calculateCollapsedWidth(float scale) {
+ if (collapsedContent == null) return (int) (dpToPx(280) * scale);
+
+ int widthSpec = MeasureSpec.makeMeasureSpec(0, MeasureSpec.UNSPECIFIED);
+ int heightSpec = MeasureSpec.makeMeasureSpec((int)(dpToPx(COLLAPSED_HEIGHT_DP) * scale), MeasureSpec.EXACTLY);
+ collapsedContent.measure(widthSpec, heightSpec);
+
+ int contentWidth = collapsedContent.getMeasuredWidth();
+ int padding = (int) (dpToPx(40) * scale);
+ return contentWidth + padding;
+ }
+
+ private int calculateTaskWidth(DynamicIslandManager.TaskItem task, float scale) {
+ float sidePadding = dpToPx(HORIZONTAL_ITEM_PADDING_DP * 2) * scale;
+ float iconWidth = 0;
+ float spacing = 0;
+
+ if (task.icon != null || task.type == DynamicIslandManager.TaskItem.Type.SWITCH) {
+ if (task.type == DynamicIslandManager.TaskItem.Type.SWITCH) iconWidth = dpToPx(52) * scale;
+ else iconWidth = dpToPx(ICON_SIZE_DP) * scale;
+ spacing = dpToPx(ICON_SPACING_DP) * scale;
+ }
+
+ textPaint.setTextSize(spToPx(15 * scale));
+ textPaint.setTypeface(Typeface.DEFAULT_BOLD);
+ float titleWidth = textPaint.measureText(task.text);
+
+ float subtitleWidth = 0;
+ if (task.subtitle != null) {
+ textPaint.setTextSize(spToPx(12 * scale));
+ textPaint.setTypeface(Typeface.DEFAULT);
+ subtitleWidth = textPaint.measureText(task.subtitle.replace("|", " "));
+ }
+
+ float textWidth = Math.max(titleWidth, subtitleWidth);
+ float extraPadding = dpToPx(32) * scale;
+
+ return (int) (sidePadding + iconWidth + spacing + textWidth + extraPadding);
+ }
+
+ private float dpToPx(float dp) {
+ return dp * getResources().getDisplayMetrics().density;
+ }
+
+ private int spToPx(float sp) {
+ return (int) (sp * getResources().getDisplayMetrics().scaledDensity);
+ }
+
+ @Override
+ public void onThemeColorChanged(int newColor) {
+ invalidate();
+ }
+
+ @Override
+ protected void onAttachedToWindow() {
+ super.onAttachedToWindow();
+ ThemeManager.addListener(this);
+ }
+
+ @Override
+ protected void onDetachedFromWindow() {
+ super.onDetachedFromWindow();
+ if (sheenAnimator != null) sheenAnimator.cancel();
+ if (sizeAnimator != null) sizeAnimator.cancel();
+ // 停止监听应该是
+ ThemeManager.removeListener(this);
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/phoenix/gui/ui/dynamic/DynamicIslandWindow.java b/app/src/main/java/com/phoenix/gui/ui/dynamic/DynamicIslandWindow.java
new file mode 100644
index 0000000..05bc3b0
--- /dev/null
+++ b/app/src/main/java/com/phoenix/gui/ui/dynamic/DynamicIslandWindow.java
@@ -0,0 +1,107 @@
+package com.phoenix.gui.ui.dynamic;
+
+import android.content.Context;
+import android.view.View;
+import android.widget.FrameLayout;
+import androidx.annotation.NonNull;
+
+public class DynamicIslandWindow {
+
+ private final Context context;
+ private final FrameLayout container;
+ private DynamicIslandView dynamicIslandView;
+ private DynamicIslandManager manager;
+ private boolean isShowing = false;
+
+ public DynamicIslandWindow(@NonNull Context context, FrameLayout container) {
+ this.context = context.getApplicationContext();
+ this.container = container;
+ }
+
+ public void show() {
+ if (isShowing) return;
+
+ android.content.SharedPreferences prefs = context.getSharedPreferences("SettingsPrefs", Context.MODE_PRIVATE);
+ float scale = prefs.getFloat("dynamicIslandScale", 0.7f);
+ String username = prefs.getString("dynamicIslandUsername", "User");
+ manager = new DynamicIslandManager(scale, username);
+
+ dynamicIslandView = new DynamicIslandView(context);
+ dynamicIslandView.setManager(manager);
+ dynamicIslandView.setAlpha(0.80f);
+
+ if (container != null) {
+ container.removeAllViews();
+ container.addView(dynamicIslandView, new FrameLayout.LayoutParams(
+ FrameLayout.LayoutParams.WRAP_CONTENT,
+ FrameLayout.LayoutParams.WRAP_CONTENT
+ ));
+ container.setVisibility(View.VISIBLE);
+ isShowing = true;
+ }
+ }
+
+ public void hide() {
+ if (!isShowing) return;
+
+ try {
+ if (container != null) {
+ container.removeAllViews();
+ container.setVisibility(View.GONE);
+ }
+
+ if (dynamicIslandView != null) {
+ dynamicIslandView = null;
+ }
+
+ if (manager != null) {
+ manager.destroy();
+ manager = null;
+ }
+
+ isShowing = false;
+ } catch (Exception e) {
+ e.printStackTrace();
+ }
+ }
+
+ public void updateConfig(float scale, String username) {
+ if (manager != null) {
+ manager.updateConfig(scale, username);
+ }
+ }
+
+ public void addSwitch(String identifier, String text, boolean state) {
+ if (manager != null) {
+ manager.addSwitch(identifier, text, state);
+ }
+ }
+
+ public void addOrUpdateProgress(String identifier, String text, String subtitle,
+ android.graphics.drawable.Drawable icon,
+ Float progress, Long duration) {
+ if (manager != null) {
+ manager.addOrUpdateProgress(identifier, text, subtitle, icon, progress, duration);
+ }
+ }
+
+ public void removeTask(String identifier) {
+ if (manager != null) {
+ manager.removeTask(identifier);
+ }
+ }
+
+ public void hideAllTasks() {
+ if (manager != null) {
+ manager.hide();
+ }
+ }
+
+ public boolean isShowing() {
+ return isShowing;
+ }
+
+ public DynamicIslandManager getManager() {
+ return manager;
+ }
+}
diff --git a/app/src/main/java/com/phoenix/gui/ui/dynamic/GlowDrawable.java b/app/src/main/java/com/phoenix/gui/ui/dynamic/GlowDrawable.java
new file mode 100644
index 0000000..1c92768
--- /dev/null
+++ b/app/src/main/java/com/phoenix/gui/ui/dynamic/GlowDrawable.java
@@ -0,0 +1,66 @@
+package com.phoenix.gui.ui.dynamic;
+
+import android.graphics.*;
+import android.graphics.drawable.Drawable;
+import androidx.annotation.NonNull;
+
+public class GlowDrawable extends Drawable {
+
+ private final Paint paint;
+ private float cornerRadius = 0;
+ private float blurRadius = 0;
+ private float spreadRadius = 0;
+
+ public GlowDrawable() {
+ paint = new Paint(Paint.ANTI_ALIAS_FLAG);
+ paint.setColor(0x66000000);
+ paint.setStyle(Paint.Style.FILL);
+ }
+
+ public void setCornerRadius(float radius) {
+ this.cornerRadius = radius;
+ invalidateSelf();
+ }
+
+ public void setBlurRadius(float radius) {
+ this.blurRadius = radius;
+ if (radius > 0) {
+ paint.setMaskFilter(new BlurMaskFilter(radius, BlurMaskFilter.Blur.NORMAL));
+ } else {
+ paint.setMaskFilter(null);
+ }
+ invalidateSelf();
+ }
+
+ public void setSpreadRadius(float radius) {
+ this.spreadRadius = radius;
+ invalidateSelf();
+ }
+
+ @Override
+ public void draw(@NonNull Canvas canvas) {
+ Rect bounds = getBounds();
+ RectF rect = new RectF(
+ bounds.left - spreadRadius,
+ bounds.top - spreadRadius,
+ bounds.right + spreadRadius,
+ bounds.bottom + spreadRadius
+ );
+ canvas.drawRoundRect(rect, cornerRadius, cornerRadius, paint);
+ }
+
+ @Override
+ public void setAlpha(int alpha) {
+ paint.setAlpha(alpha);
+ }
+
+ @Override
+ public void setColorFilter(ColorFilter colorFilter) {
+ paint.setColorFilter(colorFilter);
+ }
+
+ @Override
+ public int getOpacity() {
+ return PixelFormat.TRANSLUCENT;
+ }
+}
diff --git a/app/src/main/java/com/phoenix/gui/ui/dynamic/MaterialProgressBar.java b/app/src/main/java/com/phoenix/gui/ui/dynamic/MaterialProgressBar.java
new file mode 100644
index 0000000..acf9321
--- /dev/null
+++ b/app/src/main/java/com/phoenix/gui/ui/dynamic/MaterialProgressBar.java
@@ -0,0 +1,161 @@
+package com.phoenix.gui.ui.dynamic;
+
+import android.animation.ValueAnimator;
+import android.content.Context;
+import android.graphics.*;
+import android.util.AttributeSet;
+import android.view.View;
+import android.view.animation.LinearInterpolator;
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+
+public class MaterialProgressBar extends View {
+
+ private static final float DEFAULT_HEIGHT_DP = 8f;
+
+ private float progress = 0f;
+
+ private float sheenPosition = 0f;
+ private ValueAnimator sheenAnimator;
+
+ private Paint backgroundPaint;
+ private Paint progressPaint;
+
+ private float barHeight;
+ private float cornerRadius;
+
+ public MaterialProgressBar(@NonNull Context context) {
+ super(context);
+ init(context);
+ }
+
+ public MaterialProgressBar(@NonNull Context context, @Nullable AttributeSet attrs) {
+ super(context, attrs);
+ init(context);
+ }
+
+ private void init(Context context) {
+ float density = context.getResources().getDisplayMetrics().density;
+ barHeight = DEFAULT_HEIGHT_DP * density;
+ cornerRadius = barHeight / 2f;
+
+ backgroundPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
+ backgroundPaint.setStyle(Paint.Style.FILL);
+
+ int backgroundColor = (com.phoenix.gui.ui.ThemeManager.getThemeColor() & 0x00FFFFFF) | 0x26000000;
+ backgroundPaint.setColor(backgroundColor);
+
+ progressPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
+ progressPaint.setStyle(Paint.Style.FILL);
+ progressPaint.setColor(com.phoenix.gui.ui.ThemeManager.getThemeColor());
+
+ startSheenAnimation();
+ }
+
+ private void startSheenAnimation() {
+ sheenAnimator = ValueAnimator.ofFloat(0f, 2f);
+ sheenAnimator.setDuration(1000);
+ sheenAnimator.setInterpolator(new LinearInterpolator());
+ sheenAnimator.setRepeatCount(ValueAnimator.INFINITE);
+ sheenAnimator.setRepeatMode(ValueAnimator.RESTART);
+
+ sheenAnimator.addUpdateListener(animation -> {
+ sheenPosition = (float) animation.getAnimatedValue();
+ invalidate();
+ });
+
+ sheenAnimator.start();
+ }
+
+ @Override
+ protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
+ int width = MeasureSpec.getSize(widthMeasureSpec);
+ int height = (int) (barHeight + getPaddingTop() + getPaddingBottom());
+ setMeasuredDimension(width, height);
+ }
+
+ @Override
+ protected void onDraw(@NonNull Canvas canvas) {
+ super.onDraw(canvas);
+
+ float width = getWidth() - getPaddingLeft() - getPaddingRight();
+ float left = getPaddingLeft();
+ float top = getPaddingTop();
+ float right = left + width;
+ float bottom = top + barHeight;
+
+ RectF backgroundRect = new RectF(left, top, right, bottom);
+ canvas.drawRoundRect(backgroundRect, cornerRadius, cornerRadius, backgroundPaint);
+
+ float progressWidth = Math.max(width * progress, barHeight);
+ float progressRight = left + progressWidth;
+
+ if (progress > 0.001f) {
+
+ canvas.save();
+
+ RectF progressRect = new RectF(left, top, progressRight, bottom);
+ canvas.clipRect(progressRect);
+
+ canvas.drawRoundRect(progressRect, cornerRadius, cornerRadius, progressPaint);
+
+ drawSheen(canvas, progressRect);
+
+ canvas.restore();
+ }
+ }
+
+ private void drawSheen(Canvas canvas, RectF rect) {
+ float gradientWidth = rect.width() * 0.3f;
+ float gradientStart = rect.width() * (sheenPosition - 1f);
+
+ int colorSheen = (com.phoenix.gui.ui.ThemeManager.getTextPrimary() & 0x00FFFFFF) | 0x4D000000;
+
+ LinearGradient gradient = new LinearGradient(
+ rect.left + gradientStart,
+ rect.top,
+ rect.left + gradientStart + gradientWidth,
+ rect.top,
+ new int[]{0x00FFFFFF, colorSheen, 0x00FFFFFF},
+ new float[]{0f, 0.5f, 1f},
+ Shader.TileMode.CLAMP
+ );
+
+ Paint sheenPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
+ sheenPaint.setShader(gradient);
+
+ canvas.drawRoundRect(rect, cornerRadius, cornerRadius, sheenPaint);
+ }
+
+ public void setProgress(float progress) {
+ this.progress = Math.max(0f, Math.min(1f, progress));
+ invalidate();
+ }
+
+ public float getProgress() {
+ return progress;
+ }
+
+ public void setBarHeight(float heightDp) {
+ float density = getResources().getDisplayMetrics().density;
+ this.barHeight = heightDp * density;
+ this.cornerRadius = barHeight / 2f;
+ requestLayout();
+ }
+
+ @Override
+ protected void onDetachedFromWindow() {
+ super.onDetachedFromWindow();
+ if (sheenAnimator != null) {
+ sheenAnimator.cancel();
+ }
+ }
+
+ @Override
+ protected void onAttachedToWindow() {
+ super.onAttachedToWindow();
+ if (sheenAnimator != null && !sheenAnimator.isRunning()) {
+ sheenAnimator.start();
+ }
+ }
+}
diff --git a/app/src/main/java/com/phoenix/gui/ui/dynamic/MaterialSwitchView.java b/app/src/main/java/com/phoenix/gui/ui/dynamic/MaterialSwitchView.java
new file mode 100644
index 0000000..9172901
--- /dev/null
+++ b/app/src/main/java/com/phoenix/gui/ui/dynamic/MaterialSwitchView.java
@@ -0,0 +1,206 @@
+package com.phoenix.gui.ui.dynamic;
+
+import android.animation.ValueAnimator;
+import android.content.Context;
+import android.graphics.*;
+import android.util.AttributeSet;
+import android.util.TypedValue;
+import android.view.View;
+import android.view.animation.OvershootInterpolator;
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import com.phoenix.gui.ui.ThemeManager;
+
+public class MaterialSwitchView extends View implements ThemeManager.OnThemeColorChangeListener {
+
+ private static final float TRACK_WIDTH_DP = 52f;
+ private static final float TRACK_HEIGHT_DP = 32f;
+ private static final float THUMB_SIZE_DP = 24f;
+ private static final float THUMB_PADDING_DP = 4f;
+
+ private boolean isChecked = false;
+ private float thumbPosition = 0f;
+ private float trackColorFraction = 0f;
+
+ private ValueAnimator thumbAnimator;
+ private ValueAnimator colorAnimator;
+
+ private Paint trackPaint;
+ private Paint thumbPaint;
+
+ private float trackWidth;
+ private float trackHeight;
+ private float thumbSize;
+ private float thumbPadding;
+ private float trackRadius;
+ private float thumbRadius;
+
+ public MaterialSwitchView(@NonNull Context context) {
+ super(context);
+ init(context);
+ }
+
+ public MaterialSwitchView(@NonNull Context context, @Nullable AttributeSet attrs) {
+ super(context, attrs);
+ init(context);
+ }
+
+ private void init(Context context) {
+ float density = context.getResources().getDisplayMetrics().density;
+ trackWidth = TRACK_WIDTH_DP * density;
+ trackHeight = TRACK_HEIGHT_DP * density;
+ thumbSize = THUMB_SIZE_DP * density;
+ thumbPadding = THUMB_PADDING_DP * density;
+ trackRadius = trackHeight / 2f;
+ thumbRadius = thumbSize / 2f;
+
+ trackPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
+ trackPaint.setStyle(Paint.Style.FILL);
+
+ thumbPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
+ thumbPaint.setStyle(Paint.Style.FILL);
+ thumbPaint.setColor(com.phoenix.gui.ui.ThemeManager.getTextPrimary());
+ thumbPaint.setShadowLayer(
+ 4f * density,
+ 0f,
+ 2f * density,
+ 0x40000000
+ );
+
+ setLayerType(LAYER_TYPE_SOFTWARE, null);
+
+ setClickable(false);
+ setFocusable(false);
+ }
+
+ @Override
+ protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
+ int width = (int) (trackWidth + getPaddingLeft() + getPaddingRight());
+ int height = (int) (trackHeight + getPaddingTop() + getPaddingBottom());
+ setMeasuredDimension(width, height);
+ }
+
+ @Override
+ protected void onDraw(@NonNull Canvas canvas) {
+ super.onDraw(canvas);
+
+ float centerX = getWidth() / 2f;
+ float centerY = getHeight() / 2f;
+
+ float trackLeft = centerX - trackWidth / 2f;
+ float trackTop = centerY - trackHeight / 2f;
+ float trackRight = centerX + trackWidth / 2f;
+ float trackBottom = centerY + trackHeight / 2f;
+
+ int colorTrackChecked = ThemeManager.getThemeColor();
+ int colorTrackUnchecked = (com.phoenix.gui.ui.ThemeManager.getTextPrimary() & 0x00FFFFFF) | 0x4D000000;
+ int trackColor = interpolateColor(colorTrackUnchecked, colorTrackChecked, trackColorFraction);
+ trackPaint.setColor(trackColor);
+
+ RectF trackRect = new RectF(trackLeft, trackTop, trackRight, trackBottom);
+ canvas.drawRoundRect(trackRect, trackRadius, trackRadius, trackPaint);
+
+ float thumbTravelDistance = trackWidth - thumbSize - 2 * thumbPadding;
+ float thumbCenterX = trackLeft + thumbPadding + thumbRadius + thumbTravelDistance * thumbPosition;
+ float thumbCenterY = centerY;
+
+ canvas.drawCircle(thumbCenterX, thumbCenterY, thumbRadius, thumbPaint);
+ }
+
+ public void setChecked(boolean checked, boolean animate) {
+ if (this.isChecked == checked) return;
+
+ this.isChecked = checked;
+
+ if (animate) {
+ animateToPosition(checked ? 1f : 0f);
+ animateTrackColor(checked ? 1f : 0f);
+ } else {
+ thumbPosition = checked ? 1f : 0f;
+ trackColorFraction = checked ? 1f : 0f;
+ invalidate();
+ }
+ }
+
+ public void setChecked(boolean checked) {
+ setChecked(checked, true);
+ }
+
+ public boolean isChecked() {
+ return isChecked;
+ }
+
+ private void animateToPosition(float targetPosition) {
+ if (thumbAnimator != null) {
+ thumbAnimator.cancel();
+ }
+
+ thumbAnimator = ValueAnimator.ofFloat(thumbPosition, targetPosition);
+ thumbAnimator.setDuration(200);
+ thumbAnimator.setInterpolator(new OvershootInterpolator(1.2f));
+ thumbAnimator.addUpdateListener(animation -> {
+ thumbPosition = (float) animation.getAnimatedValue();
+ invalidate();
+ });
+ thumbAnimator.start();
+ }
+
+ private void animateTrackColor(float targetFraction) {
+ if (colorAnimator != null) {
+ colorAnimator.cancel();
+ }
+
+ colorAnimator = ValueAnimator.ofFloat(trackColorFraction, targetFraction);
+ colorAnimator.setDuration(150);
+ colorAnimator.addUpdateListener(animation -> {
+ trackColorFraction = (float) animation.getAnimatedValue();
+ invalidate();
+ });
+ colorAnimator.start();
+ }
+
+ private int interpolateColor(int colorA, int colorB, float fraction) {
+ int alphaA = Color.alpha(colorA);
+ int redA = Color.red(colorA);
+ int greenA = Color.green(colorA);
+ int blueA = Color.blue(colorA);
+
+ int alphaB = Color.alpha(colorB);
+ int redB = Color.red(colorB);
+ int greenB = Color.green(colorB);
+ int blueB = Color.blue(colorB);
+
+ int alpha = (int) (alphaA + (alphaB - alphaA) * fraction);
+ int red = (int) (redA + (redB - redA) * fraction);
+ int green = (int) (greenA + (greenB - greenA) * fraction);
+ int blue = (int) (blueA + (blueB - blueA) * fraction);
+
+ return Color.argb(alpha, red, green, blue);
+ }
+
+ @Override
+ public void onThemeColorChanged(int newColor) {
+
+ if (isChecked) {
+ invalidate();
+ }
+ }
+
+ @Override
+ protected void onAttachedToWindow() {
+ super.onAttachedToWindow();
+ ThemeManager.addListener(this);
+ }
+
+ @Override
+ protected void onDetachedFromWindow() {
+ super.onDetachedFromWindow();
+ if (thumbAnimator != null) {
+ thumbAnimator.cancel();
+ }
+ if (colorAnimator != null) {
+ colorAnimator.cancel();
+ }
+ ThemeManager.removeListener(this);
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/phoenix/gui/ui/dynamic/ProgressItemView.java b/app/src/main/java/com/phoenix/gui/ui/dynamic/ProgressItemView.java
new file mode 100644
index 0000000..7c5299a
--- /dev/null
+++ b/app/src/main/java/com/phoenix/gui/ui/dynamic/ProgressItemView.java
@@ -0,0 +1,186 @@
+package com.phoenix.gui.ui.dynamic;
+
+import android.content.Context;
+import android.graphics.Color;
+import android.graphics.Typeface;
+import android.graphics.drawable.GradientDrawable;
+import android.util.TypedValue;
+import android.view.Gravity;
+import android.widget.ImageView;
+import android.widget.LinearLayout;
+import android.widget.TextView;
+import androidx.annotation.NonNull;
+import com.phoenix.gui.ui.ThemeManager;
+
+public class ProgressItemView extends AnimatedTaskItemView implements ThemeManager.OnThemeColorChangeListener {
+
+ private int colorPrimary;
+ private int colorOnPrimary;
+ private GradientDrawable iconBackground;
+
+ private static final int ICON_SIZE_DP = 40;
+ private static final int ICON_CORNER_RADIUS_DP = 14;
+ private static final int ICON_INNER_SIZE_DP = 24;
+
+ private ImageView iconView;
+ private TextView titleText;
+ private TextView subtitleText;
+ private MaterialProgressBar progressBar;
+
+ public ProgressItemView(@NonNull Context context, DynamicIslandManager.TaskItem task, float scale) {
+ super(context, task, scale);
+ }
+
+ @Override
+ protected void init(Context context) {
+ colorPrimary = ThemeManager.getThemeColor();
+ colorOnPrimary = ThemeManager.getTextOnTheme();
+
+ setLayoutParams(new LayoutParams(
+ LayoutParams.MATCH_PARENT,
+ (int) (dpToPx(52 + 8 + 12) * scale)
+ ));
+
+ LinearLayout mainContainer = new LinearLayout(context);
+ mainContainer.setOrientation(LinearLayout.VERTICAL);
+ mainContainer.setGravity(Gravity.CENTER_VERTICAL);
+ addView(mainContainer, new LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.MATCH_PARENT));
+
+ LinearLayout topContainer = new LinearLayout(context);
+ topContainer.setOrientation(LinearLayout.HORIZONTAL);
+ topContainer.setGravity(Gravity.CENTER_VERTICAL);
+ topContainer.setPadding(
+ (int) (dpToPx(24) * scale), 0,
+ (int) (dpToPx(24) * scale), 0
+ );
+ mainContainer.addView(topContainer, new LinearLayout.LayoutParams(
+ LinearLayout.LayoutParams.MATCH_PARENT,
+ (int) (dpToPx(52) * scale)
+ ));
+
+ iconView = new ImageView(context);
+ iconView.setScaleType(ImageView.ScaleType.CENTER_INSIDE);
+ int iconSize = (int) (dpToPx(ICON_SIZE_DP) * scale);
+
+ iconBackground = new GradientDrawable();
+ iconBackground.setShape(GradientDrawable.RECTANGLE);
+ iconBackground.setCornerRadius(dpToPx(ICON_CORNER_RADIUS_DP) * scale);
+ iconBackground.setColor(adjustAlpha(colorPrimary, 0.5f));
+ iconView.setBackground(iconBackground);
+
+ int iconPadding = (int) ((dpToPx(ICON_SIZE_DP) - dpToPx(ICON_INNER_SIZE_DP)) / 2 * scale);
+ iconView.setPadding(iconPadding, iconPadding, iconPadding, iconPadding);
+ iconView.setColorFilter(colorOnPrimary);
+
+ LinearLayout.LayoutParams iconParams = new LinearLayout.LayoutParams(iconSize, iconSize);
+ topContainer.addView(iconView, iconParams);
+
+ topContainer.addView(new android.view.View(context), new LinearLayout.LayoutParams(
+ (int) (dpToPx(12) * scale), 0
+ ));
+
+ LinearLayout textContainer = new LinearLayout(context);
+ textContainer.setOrientation(LinearLayout.VERTICAL);
+ textContainer.setGravity(Gravity.CENTER_VERTICAL);
+ topContainer.addView(textContainer, new LinearLayout.LayoutParams(
+ 0, LinearLayout.LayoutParams.MATCH_PARENT, 1f
+ ));
+
+ titleText = new TextView(context);
+ titleText.setTextColor(com.phoenix.gui.ui.ThemeManager.getTextPrimary());
+ titleText.setTextSize(15 * scale);
+ titleText.setTypeface(null, Typeface.BOLD);
+ titleText.setSingleLine(true);
+ titleText.setIncludeFontPadding(false);
+ textContainer.addView(titleText);
+
+ subtitleText = new TextView(context);
+ subtitleText.setTextColor(com.phoenix.gui.ui.ThemeManager.getTextSecondary());
+ subtitleText.setTextSize(12 * scale);
+ subtitleText.setSingleLine(true);
+ subtitleText.setIncludeFontPadding(false);
+ LinearLayout.LayoutParams subtitleParams = new LinearLayout.LayoutParams(
+ LinearLayout.LayoutParams.MATCH_PARENT,
+ LinearLayout.LayoutParams.WRAP_CONTENT
+ );
+ subtitleParams.topMargin = (int) (dpToPx(2) * scale);
+ textContainer.addView(subtitleText, subtitleParams);
+
+ mainContainer.addView(new android.view.View(context), new LinearLayout.LayoutParams(
+ 0, (int) (dpToPx(6) * scale)
+ ));
+
+ progressBar = new MaterialProgressBar(context);
+ progressBar.setBarHeight(8 * scale);
+ LinearLayout.LayoutParams progressParams = new LinearLayout.LayoutParams(
+ LinearLayout.LayoutParams.MATCH_PARENT,
+ (int) (dpToPx(8) * scale)
+ );
+ progressParams.setMargins(
+ (int) (dpToPx(24) * scale),
+ 0,
+ (int) (dpToPx(24) * scale),
+ 0
+ );
+ mainContainer.addView(progressBar, progressParams);
+
+ mainContainer.addView(new android.view.View(context), new LinearLayout.LayoutParams(
+ 0, (int) (dpToPx(6) * scale)
+ ));
+
+ updateContent();
+ }
+
+ @Override
+ protected void updateContent() {
+
+ titleText.setText(task.text);
+
+ if (task.subtitle != null && !task.subtitle.isEmpty()) {
+ subtitleText.setText(task.subtitle);
+ subtitleText.setVisibility(VISIBLE);
+ } else {
+ subtitleText.setVisibility(GONE);
+ }
+
+ if (task.icon != null) {
+ iconView.setImageDrawable(task.icon);
+ iconView.setVisibility(VISIBLE);
+ } else {
+ iconView.setVisibility(GONE);
+ }
+
+ progressBar.setProgress(task.displayProgress);
+ }
+
+ private int adjustAlpha(int color, float alpha) {
+ int alphaInt = Math.round(Color.alpha(color) * alpha);
+ return Color.argb(alphaInt, Color.red(color), Color.green(color), Color.blue(color));
+ }
+
+ @Override
+ public void onThemeColorChanged(int newColor) {
+ this.colorPrimary = newColor;
+ this.colorOnPrimary = ThemeManager.getTextOnTheme();
+
+ if (iconBackground != null) {
+ iconBackground.setColor(adjustAlpha(colorPrimary, 0.5f));
+ }
+ if (iconView != null) {
+ iconView.setColorFilter(colorOnPrimary);
+ }
+ }
+
+ @Override
+ protected void onAttachedToWindow() {
+ super.onAttachedToWindow();
+
+ ThemeManager.addListener(this);
+ }
+
+ @Override
+ protected void onDetachedFromWindow() {
+ super.onDetachedFromWindow();
+ ThemeManager.removeListener(this);
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/phoenix/gui/ui/dynamic/SheenDrawable.java b/app/src/main/java/com/phoenix/gui/ui/dynamic/SheenDrawable.java
new file mode 100644
index 0000000..f63d71b
--- /dev/null
+++ b/app/src/main/java/com/phoenix/gui/ui/dynamic/SheenDrawable.java
@@ -0,0 +1,77 @@
+package com.phoenix.gui.ui.dynamic;
+
+import android.animation.ValueAnimator;
+import android.graphics.*;
+import android.graphics.drawable.Drawable;
+import android.view.animation.LinearInterpolator;
+import androidx.annotation.NonNull;
+
+public class SheenDrawable extends Drawable {
+
+ private final Paint paint;
+ private float cornerRadius = 0;
+ private float sheenPosition = -1.0f;
+ private ValueAnimator animator;
+
+ public SheenDrawable() {
+ paint = new Paint(Paint.ANTI_ALIAS_FLAG);
+ startAnimation();
+ }
+
+ public void setCornerRadius(float radius) {
+ this.cornerRadius = radius;
+ invalidateSelf();
+ }
+
+ private void startAnimation() {
+ animator = ValueAnimator.ofFloat(-1.0f, 2.0f);
+ animator.setDuration(2500);
+ animator.setStartDelay(500);
+ animator.setInterpolator(new LinearInterpolator());
+ animator.setRepeatCount(ValueAnimator.INFINITE);
+ animator.setRepeatMode(ValueAnimator.RESTART);
+
+ animator.addUpdateListener(animation -> {
+ sheenPosition = (float) animation.getAnimatedValue();
+ invalidateSelf();
+ });
+
+ animator.start();
+ }
+
+ @Override
+ public void draw(@NonNull Canvas canvas) {
+ Rect bounds = getBounds();
+
+ float gradientWidth = bounds.width() * 0.5f;
+ float gradientStart = bounds.width() * sheenPosition;
+
+ LinearGradient gradient = new LinearGradient(
+ gradientStart - gradientWidth, 0,
+ gradientStart, bounds.height(),
+ new int[]{0x00FFFFFF, 0x14FFFFFF, 0x00FFFFFF},
+ new float[]{0f, 0.5f, 1f},
+ Shader.TileMode.CLAMP
+ );
+
+ paint.setShader(gradient);
+
+ RectF rect = new RectF(bounds);
+ canvas.drawRoundRect(rect, cornerRadius, cornerRadius, paint);
+ }
+
+ @Override
+ public void setAlpha(int alpha) {
+ paint.setAlpha(alpha);
+ }
+
+ @Override
+ public void setColorFilter(ColorFilter colorFilter) {
+ paint.setColorFilter(colorFilter);
+ }
+
+ @Override
+ public int getOpacity() {
+ return PixelFormat.TRANSLUCENT;
+ }
+}
diff --git a/app/src/main/java/com/phoenix/gui/ui/dynamic/SwitchItemView.java b/app/src/main/java/com/phoenix/gui/ui/dynamic/SwitchItemView.java
new file mode 100644
index 0000000..48ffc29
--- /dev/null
+++ b/app/src/main/java/com/phoenix/gui/ui/dynamic/SwitchItemView.java
@@ -0,0 +1,187 @@
+package com.phoenix.gui.ui.dynamic;
+
+import android.content.Context;
+import android.graphics.Typeface;
+import android.util.TypedValue;
+import android.view.Gravity;
+import android.widget.LinearLayout;
+import android.widget.TextView;
+import androidx.annotation.NonNull;
+import com.phoenix.gui.ui.ThemeManager;
+import java.util.Objects;
+
+public class SwitchItemView extends TaskItemView implements ThemeManager.OnThemeColorChangeListener {
+
+ private int colorPrimary;
+ private MaterialSwitchView switchView;
+ private TextView titleText;
+ private LinearLayout subtitleContainer;
+
+ private String lastRenderedText = null;
+ private String lastRenderedSubtitle = null;
+ private boolean lastRenderedState = false;
+ private boolean isInitialized = false;
+
+ public SwitchItemView(@NonNull Context context, DynamicIslandManager.TaskItem task, float scale) {
+ super(context, task, scale);
+ }
+
+ @Override
+ protected void init(Context context) {
+ colorPrimary = ThemeManager.getThemeColor();
+
+ setLayoutParams(new LayoutParams(
+ LayoutParams.MATCH_PARENT,
+ (int) (dpToPx(52) * scale)
+ ));
+ setPadding(
+ (int) (dpToPx(24) * scale), 0,
+ (int) (dpToPx(24) * scale), 0
+ );
+
+ LinearLayout container = new LinearLayout(context);
+ container.setOrientation(LinearLayout.HORIZONTAL);
+ container.setGravity(Gravity.CENTER_VERTICAL);
+ container.setClickable(false);
+ addView(container, new LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.MATCH_PARENT));
+
+ switchView = new MaterialSwitchView(context);
+ switchView.setScaleX(scale * 1.1f);
+ switchView.setScaleY(scale * 1.1f);
+ switchView.setClickable(false);
+ LinearLayout.LayoutParams switchParams = new LinearLayout.LayoutParams(
+ LayoutParams.WRAP_CONTENT,
+ LinearLayout.LayoutParams.WRAP_CONTENT
+ );
+ container.addView(switchView, switchParams);
+
+ LinearLayout.LayoutParams spacerParams = new LinearLayout.LayoutParams(
+ (int) (dpToPx(12) * scale), 0
+ );
+ container.addView(new android.view.View(context), spacerParams);
+
+ LinearLayout textContainer = new LinearLayout(context);
+ textContainer.setOrientation(LinearLayout.VERTICAL);
+ textContainer.setGravity(Gravity.CENTER_VERTICAL);
+ container.addView(textContainer, new LinearLayout.LayoutParams(
+ 0, LinearLayout.LayoutParams.MATCH_PARENT, 1f
+ ));
+
+ titleText = new TextView(context);
+ titleText.setTextColor(com.phoenix.gui.ui.ThemeManager.getTextPrimary());
+ titleText.setTextSize(15 * scale);
+ titleText.setTypeface(null, Typeface.BOLD);
+ titleText.setSingleLine(true);
+ titleText.setIncludeFontPadding(false);
+ textContainer.addView(titleText, new LinearLayout.LayoutParams(
+ LayoutParams.MATCH_PARENT,
+ LayoutParams.WRAP_CONTENT
+ ));
+
+ subtitleContainer = new LinearLayout(context);
+ subtitleContainer.setOrientation(LinearLayout.HORIZONTAL);
+ subtitleContainer.setGravity(Gravity.CENTER_VERTICAL);
+ LinearLayout.LayoutParams subtitleContainerParams = new LinearLayout.LayoutParams(
+ LayoutParams.MATCH_PARENT,
+ LayoutParams.WRAP_CONTENT
+ );
+ subtitleContainerParams.topMargin = (int) (dpToPx(2) * scale);
+ textContainer.addView(subtitleContainer, subtitleContainerParams);
+
+ updateContent();
+ }
+
+ @Override
+ protected void updateContent() {
+
+ if (!isInitialized) {
+ switchView.setChecked(task.switchState, false);
+ titleText.setText(task.text);
+ updateSubtitle();
+
+ lastRenderedText = task.text;
+ lastRenderedSubtitle = task.subtitle;
+ lastRenderedState = task.switchState;
+ isInitialized = true;
+
+ android.util.Log.d("SwitchItemView", String.format(
+ "First render: id=%s, state=%s", task.identifier, task.switchState
+ ));
+ return;
+ }
+
+ if (lastRenderedState != task.switchState) {
+ switchView.setChecked(task.switchState, true);
+ lastRenderedState = task.switchState;
+
+ android.util.Log.d("SwitchItemView", String.format(
+ "State changed: id=%s, new=%s", task.identifier, task.switchState
+ ));
+ }
+
+ if (!Objects.equals(lastRenderedText, task.text)) {
+ titleText.setText(task.text);
+ lastRenderedText = task.text;
+ }
+
+ if (!Objects.equals(lastRenderedSubtitle, task.subtitle)) {
+ updateSubtitle();
+ lastRenderedSubtitle = task.subtitle;
+ }
+ }
+
+ private void updateSubtitle() {
+ subtitleContainer.removeAllViews();
+
+ if (task.subtitle != null && task.subtitle.contains("|")) {
+ String[] parts = task.subtitle.split("\\|", 2);
+
+ TextView part1 = new TextView(getContext());
+ part1.setText(parts[0]);
+ part1.setTextColor(colorPrimary);
+ part1.setTextSize(12 * scale);
+ part1.setTypeface(null, Typeface.BOLD);
+ part1.setSingleLine(true);
+ part1.setIncludeFontPadding(false);
+ subtitleContainer.addView(part1);
+
+ LinearLayout.LayoutParams spacerParams = new LinearLayout.LayoutParams((int) (dpToPx(4) * scale), 0);
+ subtitleContainer.addView(new android.view.View(getContext()), spacerParams);
+
+ TextView part2 = new TextView(getContext());
+ part2.setText(parts[1]);
+ part2.setTextColor(com.phoenix.gui.ui.ThemeManager.getTextSecondary());
+ part2.setTextSize(12 * scale);
+ part2.setSingleLine(true);
+ part2.setIncludeFontPadding(false);
+ subtitleContainer.addView(part2);
+
+ } else if (task.subtitle != null) {
+ TextView subtitle = new TextView(getContext());
+ subtitle.setText(task.subtitle);
+ subtitle.setTextColor(com.phoenix.gui.ui.ThemeManager.getTextSecondary());
+ subtitle.setTextSize(12 * scale);
+ subtitle.setSingleLine(true);
+ subtitle.setIncludeFontPadding(false);
+ subtitleContainer.addView(subtitle);
+ }
+ }
+
+ @Override
+ public void onThemeColorChanged(int newColor) {
+ this.colorPrimary = newColor;
+ updateSubtitle();
+ }
+
+ @Override
+ protected void onAttachedToWindow() {
+ super.onAttachedToWindow();
+ ThemeManager.addListener(this);
+ }
+
+ @Override
+ protected void onDetachedFromWindow() {
+ super.onDetachedFromWindow();
+ ThemeManager.removeListener(this);
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/phoenix/gui/ui/dynamic/TaskItemView.java b/app/src/main/java/com/phoenix/gui/ui/dynamic/TaskItemView.java
new file mode 100644
index 0000000..d017c31
--- /dev/null
+++ b/app/src/main/java/com/phoenix/gui/ui/dynamic/TaskItemView.java
@@ -0,0 +1,26 @@
+package com.phoenix.gui.ui.dynamic;
+
+import android.content.Context;
+import android.widget.FrameLayout;
+import androidx.annotation.NonNull;
+
+public abstract class TaskItemView extends FrameLayout {
+
+ public DynamicIslandManager.TaskItem task;
+ protected float scale;
+
+ public TaskItemView(@NonNull Context context, DynamicIslandManager.TaskItem task, float scale) {
+ super(context);
+ this.task = task;
+ this.scale = scale;
+ init(context);
+ }
+
+ protected abstract void init(Context context);
+
+ protected abstract void updateContent();
+
+ protected float dpToPx(float dp) {
+ return dp * getResources().getDisplayMetrics().density;
+ }
+}
diff --git a/app/src/main/java/com/phoenix/gui/ui/widgets/ColorPickerView.java b/app/src/main/java/com/phoenix/gui/ui/widgets/ColorPickerView.java
new file mode 100644
index 0000000..6945043
--- /dev/null
+++ b/app/src/main/java/com/phoenix/gui/ui/widgets/ColorPickerView.java
@@ -0,0 +1,139 @@
+package com.phoenix.gui.ui.widgets;
+
+// powered by qp
+
+import android.content.Context;
+import android.graphics.*;
+import android.graphics.drawable.GradientDrawable;
+import android.view.MotionEvent;
+import android.view.View;
+import android.widget.LinearLayout;
+import android.widget.SeekBar;
+import com.phoenix.gui.ui.ThemeManager;
+
+public class ColorPickerView extends LinearLayout {
+
+ private final ColorPaletteView paletteView;
+ private final SeekBar hueSlider;
+ private float currentHue;
+ private float currentSat;
+ private float currentVal;
+
+ public ColorPickerView(Context context) {
+ super(context);
+ setOrientation(VERTICAL);
+ setPadding(0, dpToPx(5), 0, 0);
+
+ float[] initialHsv = ThemeManager.getThemeColorHSV();
+ currentHue = initialHsv[0];
+ currentSat = initialHsv[1];
+ currentVal = initialHsv[2];
+
+ paletteView = new ColorPaletteView(context);
+ addView(paletteView, new LayoutParams(LayoutParams.MATCH_PARENT, dpToPx(100)));
+
+ hueSlider = new SeekBar(context);
+ hueSlider.setMax(360);
+ hueSlider.setProgress((int) currentHue);
+
+ int[] hueColors = new int[361];
+ for (int i = 0; i < hueColors.length; i++) {
+ hueColors[i] = Color.HSVToColor(new float[]{i, 1f, 1f});
+ }
+ GradientDrawable hueGradient = new GradientDrawable(GradientDrawable.Orientation.LEFT_RIGHT, hueColors);
+ hueSlider.setProgressDrawable(hueGradient);
+
+ LayoutParams sliderParams = new LayoutParams(LayoutParams.MATCH_PARENT, dpToPx(10));
+ sliderParams.setMargins(0, dpToPx(12), 0, 0);
+ hueSlider.setLayoutParams(sliderParams);
+ hueSlider.getThumb().setColorFilter(new PorterDuffColorFilter(Color.WHITE, PorterDuff.Mode.SRC_IN));
+
+ hueSlider.setOnSeekBarChangeListener(new SeekBar.OnSeekBarChangeListener() {
+ @Override
+ public void onProgressChanged(SeekBar seekBar, int progress, boolean fromUser) {
+ if (fromUser) {
+ currentHue = progress;
+ paletteView.setHue(currentHue);
+ updateThemeColor();
+ }
+ }
+ @Override public void onStartTrackingTouch(SeekBar seekBar) {}
+ @Override public void onStopTrackingTouch(SeekBar seekBar) {}
+ });
+
+ addView(hueSlider);
+ }
+
+ private void updateThemeColor() {
+ float[] hsv = {currentHue, currentSat, currentVal};
+ int color = Color.HSVToColor(hsv);
+ ThemeManager.setThemeColor(color);
+ }
+
+ private int dpToPx(int dp) {
+ return (int) (dp * getResources().getDisplayMetrics().density);
+ }
+
+ private class ColorPaletteView extends View {
+ private final Paint saturationPaint, valuePaint;
+ private final Paint indicatorPaint;
+ private Shader valueShader, saturationShader;
+ private float hue;
+ private float indicatorX, indicatorY;
+
+ public ColorPaletteView(Context context) {
+ super(context);
+ saturationPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
+ valuePaint = new Paint(Paint.ANTI_ALIAS_FLAG);
+ indicatorPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
+ indicatorPaint.setStyle(Paint.Style.STROKE);
+ indicatorPaint.setStrokeWidth(dpToPx(2));
+ indicatorPaint.setColor(Color.WHITE);
+ indicatorPaint.setShadowLayer(dpToPx(2), 0, 0, Color.BLACK);
+ setHue(currentHue);
+ }
+
+ public void setHue(float hue) {
+ this.hue = hue;
+ if (getWidth() > 0) {
+ int hueColor = Color.HSVToColor(new float[]{hue, 1f, 1f});
+ saturationShader = new LinearGradient(0, 0, getWidth(), 0, Color.WHITE, hueColor, Shader.TileMode.CLAMP);
+ saturationPaint.setShader(saturationShader);
+ }
+ invalidate();
+ }
+
+ @Override
+ protected void onSizeChanged(int w, int h, int oldw, int oldh) {
+ super.onSizeChanged(w, h, oldw, oldh);
+ valueShader = new LinearGradient(0, 0, 0, h, Color.TRANSPARENT, Color.BLACK, Shader.TileMode.CLAMP);
+ valuePaint.setShader(valueShader);
+ setHue(hue);
+ indicatorX = w * currentSat;
+ indicatorY = h * (1 - currentVal);
+ }
+
+ @Override
+ protected void onDraw(Canvas canvas) {
+ canvas.drawRect(0, 0, getWidth(), getHeight(), saturationPaint);
+ canvas.drawRect(0, 0, getWidth(), getHeight(), valuePaint);
+ canvas.drawCircle(indicatorX, indicatorY, dpToPx(6), indicatorPaint);
+ }
+
+ @Override
+ public boolean onTouchEvent(MotionEvent event) {
+ switch (event.getAction()) {
+ case MotionEvent.ACTION_DOWN:
+ case MotionEvent.ACTION_MOVE:
+ indicatorX = Math.max(0, Math.min(getWidth(), event.getX()));
+ indicatorY = Math.max(0, Math.min(getHeight(), event.getY()));
+ currentSat = indicatorX / getWidth();
+ currentVal = 1 - (indicatorY / getHeight());
+ updateThemeColor();
+ invalidate();
+ return true;
+ }
+ return super.onTouchEvent(event);
+ }
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/phoenix/gui/ui/widgets/CustomSlider.java b/app/src/main/java/com/phoenix/gui/ui/widgets/CustomSlider.java
new file mode 100644
index 0000000..aa1bb9c
--- /dev/null
+++ b/app/src/main/java/com/phoenix/gui/ui/widgets/CustomSlider.java
@@ -0,0 +1,130 @@
+package com.phoenix.gui.ui.widgets;
+
+import android.content.Context;
+import android.graphics.Canvas;
+import android.graphics.Color;
+import android.graphics.Paint;
+import android.view.MotionEvent;
+import android.view.View;
+
+import com.phoenix.gui.config.ConfigManager;
+import com.phoenix.gui.ui.ThemeManager;
+
+public class CustomSlider extends View implements ThemeManager.OnThemeColorChangeListener {
+ private final String moduleName;
+ private final String configKey;
+ private final int min;
+ private final int max;
+ private final OnValueChangedListener onValueChanged;
+
+ private float progress = 0f;
+ private float scaleFactor = 1.0f;
+
+ private final Paint trackPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
+ private final Paint thumbFillPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
+ private final Paint thumbStrokePaint = new Paint(Paint.ANTI_ALIAS_FLAG);
+
+ public interface OnValueChangedListener {
+ void onValueChanged(int value);
+ }
+
+ public CustomSlider(Context context, String moduleName, String configKey,
+ int min, int max, int initialValue, OnValueChangedListener listener) {
+ super(context);
+ this.moduleName = moduleName;
+ this.configKey = configKey;
+ this.min = min;
+ this.max = max;
+ this.onValueChanged = listener;
+
+ updatePaints();
+
+ thumbFillPaint.setColor(ThemeManager.getTextPrimary());
+
+ setValue(initialValue, false);
+
+ ThemeManager.addListener(this);
+ }
+
+ public void updateScale(float scaleFactor) {
+ this.scaleFactor = scaleFactor;
+ updatePaints();
+ invalidate();
+ }
+
+ private void updatePaints() {
+ trackPaint.setStrokeCap(Paint.Cap.ROUND);
+ trackPaint.setStrokeWidth(scaledDp(3f));
+ thumbStrokePaint.setStyle(Paint.Style.STROKE);
+ thumbStrokePaint.setStrokeWidth(scaledDp(2f));
+ }
+
+ private float scaledDp(float baseDp) {
+ return dpToPx(baseDp) * scaleFactor;
+ }
+
+ @Override
+ protected void onDraw(Canvas canvas) {
+ super.onDraw(canvas);
+ int themeColor = ThemeManager.getThemeColor();
+ thumbStrokePaint.setColor(themeColor);
+
+ float trackY = getHeight() / 2f;
+ float thumbRadius = getHeight() / 2f - scaledDp(3f);
+ float thumbX = thumbRadius + (getWidth() - 2 * thumbRadius) * progress;
+
+ trackPaint.setColor(ThemeManager.getStateDisabled());
+ canvas.drawLine(thumbX, trackY, getWidth() - thumbRadius, trackY, trackPaint);
+
+ trackPaint.setColor(themeColor);
+ canvas.drawLine(thumbRadius, trackY, thumbX, trackY, trackPaint);
+
+ canvas.drawCircle(thumbX, trackY, thumbRadius, thumbFillPaint);
+ canvas.drawCircle(thumbX, trackY, thumbRadius, thumbStrokePaint);
+ }
+
+ public void setValue(int value, boolean notify) {
+ progress = (float)(value - min) / (max - min);
+ if (notify) {
+ onValueChanged.onValueChanged(value);
+
+ ConfigManager.saveModuleConfig(moduleName, configKey, value);
+ }
+ invalidate();
+ }
+
+ public int getValue() {
+ return min + (int)(progress * (max - min));
+ }
+
+ @Override
+ public boolean onTouchEvent(MotionEvent event) {
+ switch (event.getAction()) {
+ case MotionEvent.ACTION_DOWN:
+ case MotionEvent.ACTION_MOVE:
+ float newProgress = event.getX() / getWidth();
+ progress = Math.max(0f, Math.min(1f, newProgress));
+ int currentValue = getValue();
+ onValueChanged.onValueChanged(currentValue);
+ ConfigManager.saveModuleConfig(moduleName, configKey, currentValue);
+ invalidate();
+ return true;
+ }
+ return super.onTouchEvent(event);
+ }
+
+ @Override
+ public void onThemeColorChanged(int newColor) {
+ invalidate();
+ }
+
+ @Override
+ protected void onDetachedFromWindow() {
+ super.onDetachedFromWindow();
+ ThemeManager.removeListener(this);
+ }
+
+ private float dpToPx(float dp) {
+ return dp * getResources().getDisplayMetrics().density;
+ }
+}
diff --git a/app/src/main/java/com/phoenix/gui/ui/widgets/CustomSwitch.java b/app/src/main/java/com/phoenix/gui/ui/widgets/CustomSwitch.java
new file mode 100644
index 0000000..051c5f4
--- /dev/null
+++ b/app/src/main/java/com/phoenix/gui/ui/widgets/CustomSwitch.java
@@ -0,0 +1,123 @@
+package com.phoenix.gui.ui.widgets;
+
+import android.animation.ValueAnimator;
+import android.content.Context;
+import android.graphics.Canvas;
+import android.graphics.Color;
+import android.graphics.Paint;
+import android.graphics.RectF;
+import android.view.View;
+import android.view.animation.DecelerateInterpolator;
+
+import com.phoenix.gui.config.ConfigManager;
+import com.phoenix.gui.ui.ThemeManager;
+
+public class CustomSwitch extends View implements ThemeManager.OnThemeColorChangeListener {
+ private final String moduleName;
+ private final String configKey;
+ private final OnStateChangedListener onStateChanged;
+
+ private boolean isChecked = false;
+ private float thumbX = 0f;
+ private float scaleFactor = 1.0f;
+
+ private final Paint trackPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
+ private final Paint thumbPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
+ private final RectF trackRect = new RectF();
+ private ValueAnimator thumbAnimator;
+
+ public interface OnStateChangedListener {
+ void onStateChanged(boolean enabled);
+ }
+
+ public CustomSwitch(Context context, String moduleName, String configKey,
+ boolean initialState, OnStateChangedListener listener) {
+ super(context);
+ this.moduleName = moduleName;
+ this.configKey = configKey;
+ this.onStateChanged = listener;
+
+ thumbPaint.setColor(ThemeManager.getTextPrimary());
+ setChecked(initialState, false);
+
+ setOnClickListener(v -> setChecked(!isChecked, true));
+
+ ThemeManager.addListener(this);
+ }
+
+ public void updateScale(float scaleFactor) {
+ this.scaleFactor = scaleFactor;
+
+ setChecked(isChecked, false);
+ invalidate();
+ }
+
+ private float scaledDp(float baseDp) {
+ return dpToPx(baseDp) * scaleFactor;
+ }
+
+ @Override
+ protected void onDraw(Canvas canvas) {
+ super.onDraw(canvas);
+ int themeColor = ThemeManager.getThemeColor();
+
+ trackRect.set(0f, 0f, getWidth(), getHeight());
+ trackPaint.setColor(isChecked ? themeColor : ThemeManager.getStateDisabled());
+ canvas.drawRoundRect(trackRect, getHeight() / 2f, getHeight() / 2f, trackPaint);
+
+ float thumbRadius = getHeight() / 2f - scaledDp(2f);
+ canvas.drawCircle(thumbX, getHeight() / 2f, thumbRadius, thumbPaint);
+ }
+
+ public void setChecked(boolean checked, boolean animate) {
+ if (isChecked == checked && thumbAnimator != null && thumbAnimator.isRunning()) return;
+
+ isChecked = checked;
+ float thumbRadius = getHeight() / 2f - scaledDp(2f);
+ float targetX = isChecked ? getWidth() - thumbRadius - scaledDp(2f) : thumbRadius + scaledDp(2f);
+
+ if (animate) {
+ if (thumbAnimator != null) thumbAnimator.cancel();
+ thumbAnimator = ValueAnimator.ofFloat(thumbX, targetX);
+ thumbAnimator.setDuration(200);
+ thumbAnimator.setInterpolator(new DecelerateInterpolator());
+ thumbAnimator.addUpdateListener(animation -> {
+ thumbX = (float) animation.getAnimatedValue();
+ invalidate();
+ });
+ thumbAnimator.start();
+ } else {
+ thumbX = targetX;
+ invalidate();
+ }
+
+ onStateChanged.onStateChanged(isChecked);
+ ConfigManager.saveModuleConfig(moduleName, configKey, isChecked);
+ }
+
+ public boolean isChecked() {
+ return isChecked;
+ }
+
+ @Override
+ protected void onSizeChanged(int w, int h, int oldw, int oldh) {
+ super.onSizeChanged(w, h, oldw, oldh);
+ setChecked(isChecked, false);
+ }
+
+ @Override
+ public void onThemeColorChanged(int newColor) {
+ invalidate();
+ }
+
+ @Override
+ protected void onDetachedFromWindow() {
+ super.onDetachedFromWindow();
+ ThemeManager.removeListener(this);
+ if (thumbAnimator != null) thumbAnimator.cancel();
+ }
+
+ private float dpToPx(float dp) {
+ return dp * getResources().getDisplayMetrics().density;
+ }
+}
diff --git a/app/src/main/java/com/phoenix/gui/ui/widgets/ModeChips.java b/app/src/main/java/com/phoenix/gui/ui/widgets/ModeChips.java
new file mode 100644
index 0000000..3c3b26c
--- /dev/null
+++ b/app/src/main/java/com/phoenix/gui/ui/widgets/ModeChips.java
@@ -0,0 +1,226 @@
+package com.phoenix.gui.ui.widgets;
+
+import android.content.Context;
+import android.graphics.Color;
+import android.graphics.Typeface;
+import android.graphics.drawable.GradientDrawable;
+import android.view.Gravity;
+import android.view.MotionEvent;
+import android.view.View;
+import android.view.animation.DecelerateInterpolator;
+import android.view.animation.OvershootInterpolator;
+import android.widget.LinearLayout;
+import android.widget.TextView;
+
+import com.phoenix.gui.config.ConfigManager;
+import com.phoenix.gui.ui.ThemeManager;
+
+import java.util.ArrayList;
+import java.util.List;
+
+public class ModeChips extends LinearLayout implements ThemeManager.OnThemeColorChangeListener {
+ private final String moduleName;
+ private final String configKey;
+ private final List modes;
+ private final OnModeChangedListener onModeChanged;
+
+ private int selectedIndex;
+ private final List chips = new ArrayList<>();
+ private final List chipBackgrounds = new ArrayList<>();
+ private float scaleFactor = 1.0f;
+
+ public interface OnModeChangedListener {
+ void onModeChanged(int index, String mode);
+ }
+
+ public ModeChips(Context context, String moduleName, String configKey,
+ List modes, int initialMode, OnModeChangedListener listener) {
+ super(context);
+ this.moduleName = moduleName;
+ this.configKey = configKey;
+ this.modes = modes;
+ this.onModeChanged = listener;
+ this.selectedIndex = initialMode;
+
+ setOrientation(HORIZONTAL);
+
+ for (int i = 0; i < modes.size(); i++) {
+ TextView chip = createChip(modes.get(i), i);
+ chips.add(chip);
+ addView(chip);
+
+ if (i < modes.size() - 1) {
+ addView(createSpacer());
+ }
+ }
+
+ updateSelection(initialMode, false);
+ ThemeManager.addListener(this);
+ }
+
+ public void updateScale(float scaleFactor) {
+ this.scaleFactor = scaleFactor;
+
+ for (TextView chip : chips) {
+ chip.setTextSize(11f * scaleFactor);
+ int paddingH = scaled(12);
+ int paddingV = scaled(6);
+ chip.setPadding(paddingH, paddingV, paddingH, paddingV);
+ }
+
+ for (int i = 0; i < getChildCount(); i++) {
+ if (getChildAt(i) instanceof LinearLayout) {
+ LinearLayout spacer = (LinearLayout) getChildAt(i);
+ if (spacer.getChildCount() == 0) {
+ spacer.setLayoutParams(new LayoutParams(scaled(4), LayoutParams.MATCH_PARENT));
+ }
+ }
+ }
+
+ updateSelection(selectedIndex, false);
+
+ requestLayout();
+ }
+
+ private int scaled(int baseDp) {
+ return (int) (dpToPx(baseDp) * scaleFactor);
+ }
+
+ private TextView createChip(String text, final int index) {
+ TextView chip = new TextView(getContext());
+ chip.setText(text);
+ chip.setTextSize(11f * scaleFactor);
+ chip.setGravity(Gravity.CENTER);
+ chip.setPadding(scaled(12), scaled(6), scaled(12), scaled(6));
+
+ LayoutParams params = new LayoutParams(LayoutParams.WRAP_CONTENT, LayoutParams.WRAP_CONTENT);
+ params.setMargins(0, 0, 0, 0);
+ chip.setLayoutParams(params);
+
+ GradientDrawable bg = new GradientDrawable();
+ bg.setCornerRadius(scaled(8));
+ chipBackgrounds.add(bg);
+ chip.setBackground(bg);
+
+ chip.setOnTouchListener(new OnTouchListener() {
+ @Override
+ public boolean onTouch(View v, MotionEvent event) {
+ switch (event.getAction()) {
+ case MotionEvent.ACTION_DOWN:
+
+ v.animate()
+ .scaleX(0.9f)
+ .scaleY(0.9f)
+ .setDuration(100)
+ .start();
+ return true;
+
+ case MotionEvent.ACTION_UP:
+
+ v.animate()
+ .scaleX(1f)
+ .scaleY(1f)
+ .setDuration(100)
+ .start();
+
+ if (event.getX() >= 0 && event.getX() <= v.getWidth() &&
+ event.getY() >= 0 && event.getY() <= v.getHeight()) {
+
+ if (selectedIndex != index) {
+ updateSelection(index, true);
+ }
+ }
+ return true;
+
+ case MotionEvent.ACTION_CANCEL:
+
+ v.animate()
+ .scaleX(1f)
+ .scaleY(1f)
+ .setDuration(100)
+ .start();
+ return true;
+ }
+ return false;
+ }
+ });
+
+ return chip;
+ }
+
+ private LinearLayout createSpacer() {
+ LinearLayout spacer = new LinearLayout(getContext());
+ spacer.setLayoutParams(new LayoutParams(scaled(4), LayoutParams.MATCH_PARENT));
+ return spacer;
+ }
+
+ private void updateSelection(int newIndex, boolean animate) {
+ int oldIndex = selectedIndex;
+ selectedIndex = newIndex;
+
+ for (int i = 0; i < chips.size(); i++) {
+ applyChipStyle(i, i == newIndex);
+ }
+
+ if (animate && newIndex >= 0 && newIndex < chips.size()) {
+ TextView newChip = chips.get(newIndex);
+ newChip.setScaleX(0.9f);
+ newChip.setScaleY(0.9f);
+ newChip.animate()
+ .scaleX(1f)
+ .scaleY(1f)
+ .setDuration(200)
+ .setInterpolator(new OvershootInterpolator(2f))
+ .start();
+
+ onModeChanged.onModeChanged(newIndex, modes.get(newIndex));
+ ConfigManager.saveModuleConfig(moduleName, configKey, newIndex);
+ }
+ }
+
+ private void applyChipStyle(int index, boolean selected) {
+ if (index < 0 || index >= chips.size()) return;
+
+ TextView chip = chips.get(index);
+ GradientDrawable bg = chipBackgrounds.get(index);
+
+ if (selected) {
+ bg.setColor(ThemeManager.getThemeColor());
+ bg.setCornerRadius(scaled(8));
+ chip.setTextColor(ThemeManager.getTextOnTheme());
+ chip.setTypeface(Typeface.DEFAULT_BOLD);
+ } else {
+ bg.setColor(ThemeManager.getBgDisabled());
+ bg.setCornerRadius(scaled(8));
+ chip.setTextColor(ThemeManager.getTextSecondary());
+ chip.setTypeface(Typeface.DEFAULT);
+ }
+ }
+
+ public String getSelectedMode() {
+ return modes.get(selectedIndex);
+ }
+
+ public int getSelectedIndex() {
+ return selectedIndex;
+ }
+
+ @Override
+ public void onThemeColorChanged(int newColor) {
+ updateSelection(selectedIndex, false);
+ }
+
+ @Override
+ protected void onDetachedFromWindow() {
+ super.onDetachedFromWindow();
+ ThemeManager.removeListener(this);
+
+ for (TextView chip : chips) {
+ chip.animate().cancel();
+ }
+ }
+
+ private int dpToPx(int dp) {
+ return (int) (dp * getResources().getDisplayMetrics().density);
+ }
+}
diff --git a/app/src/main/java/com/phoenix/gui/ui/widgets/SubMenuPanel.java b/app/src/main/java/com/phoenix/gui/ui/widgets/SubMenuPanel.java
new file mode 100644
index 0000000..607982c
--- /dev/null
+++ b/app/src/main/java/com/phoenix/gui/ui/widgets/SubMenuPanel.java
@@ -0,0 +1,498 @@
+package com.phoenix.gui.ui.widgets;
+
+import android.animation.Animator;
+import android.animation.AnimatorListenerAdapter;
+import android.animation.ValueAnimator;
+import android.content.Context;
+import android.graphics.Color;
+import android.graphics.drawable.GradientDrawable;
+import android.view.Gravity;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.HorizontalScrollView;
+import android.widget.LinearLayout;
+import android.widget.TextView;
+
+import com.phoenix.gui.module.Module;
+import com.phoenix.gui.ui.ThemeManager;
+
+import java.util.ArrayList;
+import java.util.List;
+
+public class SubMenuPanel extends LinearLayout implements ThemeManager.OnThemeColorChangeListener {
+ private final Module module;
+ private boolean isOpen = false;
+ private ValueAnimator heightAnimator;
+ private Runnable onStateChangedListener;
+
+ private float scaleFactor = 1.0f;
+
+ private final List scalableWidgets = new ArrayList<>();
+ private final List themeButtons = new ArrayList<>();
+ private final int[] themeColors = {
+ 0xFF96CCFF, // Default
+ 0xFF4CAF50, // Green
+ 0xFFBB86FC, // Purple
+ 0xFFFF9800, // Orange
+ 0xFFE91E63 // Pink
+ };
+
+ public SubMenuPanel(Context context, Module module, float scaleFactor) {
+ super(context);
+ this.module = module;
+ this.scaleFactor = scaleFactor;
+
+ setOrientation(VERTICAL);
+ setBackgroundColor(Color.TRANSPARENT);
+ updatePadding();
+ setVisibility(View.GONE);
+ setAlpha(0f);
+
+ LayoutParams params = new LayoutParams(LayoutParams.MATCH_PARENT, 0);
+ params.setMargins(scaled(6), scaled(2), scaled(6), 0);
+ setLayoutParams(params);
+ }
+
+ public SubMenuPanel(Context context, Module module) {
+ this(context, module, 1.0f);
+ }
+
+ public void updateScale(float scaleFactor) {
+ this.scaleFactor = scaleFactor;
+ updatePadding();
+ LayoutParams params = (LayoutParams) getLayoutParams();
+ params.setMargins(scaled(6), scaled(2), scaled(6), 0);
+ setLayoutParams(params);
+ for (int i = 0; i < getChildCount(); i++) {
+ View child = getChildAt(i);
+ updateChildScale(child);
+ }
+ for (ScalableWidget widget : scalableWidgets) {
+ widget.updateScale(scaleFactor);
+ }
+ requestLayout();
+ }
+
+ private void updatePadding() {
+ int padding = scaled(4);
+ setPadding(padding, 0, padding, scaled(4));
+ }
+
+ private void updateChildScale(View view) {
+ if (view instanceof ViewGroup) {
+ ViewGroup group = (ViewGroup) view;
+ ViewGroup.LayoutParams lp = group.getLayoutParams();
+ if (lp instanceof MarginLayoutParams) {
+ MarginLayoutParams mlp = (MarginLayoutParams) lp;
+ mlp.topMargin = scaled(2);
+ mlp.bottomMargin = scaled(2);
+ }
+ for (int i = 0; i < group.getChildCount(); i++) {
+ updateChildScale(group.getChildAt(i));
+ }
+ } else if (view instanceof TextView) {
+ TextView tv = (TextView) view;
+ float baseSize = 11f;
+ if (tv.getCurrentTextColor() == ThemeManager.getTextTertiary()) {
+ baseSize = 10f;
+ }
+ tv.setTextSize(baseSize * scaleFactor);
+ }
+ }
+
+ private int scaled(int baseDp) {
+ return (int) (dpToPx(baseDp) * scaleFactor);
+ }
+
+ public void addSlider(String label, int min, int max, int defaultValue, String format,
+ CustomSlider.OnValueChangedListener onValueChanged) {
+ LinearLayout container = new LinearLayout(getContext());
+ container.setOrientation(VERTICAL);
+ LayoutParams containerParams = new LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.WRAP_CONTENT);
+ containerParams.setMargins(0, scaled(2), 0, scaled(2));
+ container.setLayoutParams(containerParams);
+
+ LinearLayout labelRow = new LinearLayout(getContext());
+ labelRow.setOrientation(HORIZONTAL);
+
+ TextView labelText = new TextView(getContext());
+ labelText.setText(label);
+ labelText.setTextSize(11f * scaleFactor);
+ labelText.setTextColor(ThemeManager.getTextSecondary());
+ LayoutParams labelParams = new LayoutParams(0, LayoutParams.WRAP_CONTENT, 1f);
+ labelText.setLayoutParams(labelParams);
+ labelRow.addView(labelText);
+
+ TextView valueText = new TextView(getContext());
+ valueText.setTextSize(11f * scaleFactor);
+ valueText.setTextColor(ThemeManager.getTextSecondary());
+ labelRow.addView(valueText);
+
+ container.addView(labelRow);
+
+ CustomSlider slider = new CustomSlider(getContext(), module.getName(), label.toLowerCase(),
+ min, max, defaultValue, value -> {
+ if ("%.2f".equals(format)) {
+ valueText.setText(String.format("%.2f", value / 100.0));
+ } else {
+ valueText.setText(String.valueOf(value));
+ }
+ onValueChanged.onValueChanged(value);
+ });
+
+ slider.updateScale(scaleFactor);
+ LayoutParams sliderParams = new LayoutParams(LayoutParams.MATCH_PARENT, scaled(20));
+ sliderParams.setMargins(0, scaled(2), 0, 0);
+ container.addView(slider, sliderParams);
+ scalableWidgets.add(new ScalableWidget(slider, labelText, valueText, sliderParams, 20));
+ if ("%.2f".equals(format)) {
+ valueText.setText(String.format("%.2f", defaultValue / 100.0));
+ } else {
+ valueText.setText(String.valueOf(defaultValue));
+ }
+ addView(container);
+ }
+
+ public void addShortcutSwitch(boolean defaultValue) {
+ LinearLayout container = new LinearLayout(getContext());
+ container.setOrientation(HORIZONTAL);
+ container.setGravity(Gravity.CENTER_VERTICAL);
+ LayoutParams containerParams = new LayoutParams(LayoutParams.MATCH_PARENT, scaled(28));
+ containerParams.setMargins(0, scaled(2), 0, scaled(2));
+ container.setLayoutParams(containerParams);
+
+ TextView labelText = new TextView(getContext());
+ labelText.setText("Shortcut");
+ labelText.setTextSize(11f * scaleFactor);
+ labelText.setTextColor(ThemeManager.getTextSecondary());
+ LayoutParams labelParams = new LayoutParams(0, LayoutParams.WRAP_CONTENT, 1f);
+ labelText.setLayoutParams(labelParams);
+ container.addView(labelText);
+
+ CustomSwitch switchView = new CustomSwitch(getContext(), module.getName(), "shortcut",
+ defaultValue, enabled -> {
+ module.toggleShortcut(enabled);
+ });
+
+ switchView.updateScale(scaleFactor);
+ LayoutParams switchParams = new LayoutParams(scaled(32), scaled(18));
+ container.addView(switchView, switchParams);
+ scalableWidgets.add(new ScalableWidget(switchView, labelText, containerParams, switchParams, 28, 32, 18));
+ addView(container);
+ }
+
+ public void addSwitch(String label, boolean defaultValue, CustomSwitch.OnStateChangedListener onStateChanged) {
+ LinearLayout container = new LinearLayout(getContext());
+ container.setOrientation(HORIZONTAL);
+ container.setGravity(Gravity.CENTER_VERTICAL);
+ LayoutParams containerParams = new LayoutParams(LayoutParams.MATCH_PARENT, scaled(28));
+ containerParams.setMargins(0, scaled(2), 0, scaled(2));
+ container.setLayoutParams(containerParams);
+
+ TextView labelText = new TextView(getContext());
+ labelText.setText(label);
+ labelText.setTextSize(11f * scaleFactor);
+ labelText.setTextColor(ThemeManager.getTextSecondary());
+ LayoutParams labelParams = new LayoutParams(0, LayoutParams.WRAP_CONTENT, 1f);
+ labelText.setLayoutParams(labelParams);
+ container.addView(labelText);
+
+ CustomSwitch switchView = new CustomSwitch(getContext(), module.getName(), label.toLowerCase(),
+ defaultValue, onStateChanged);
+
+ switchView.updateScale(scaleFactor);
+ LayoutParams switchParams = new LayoutParams(scaled(32), scaled(18));
+ container.addView(switchView, switchParams);
+ scalableWidgets.add(new ScalableWidget(switchView, labelText, containerParams, switchParams, 28, 32, 18));
+ addView(container);
+ }
+
+ public void addMode(String label, List modes, int defaultMode, ModeChips.OnModeChangedListener onModeChanged) {
+ LinearLayout container = new LinearLayout(getContext());
+ container.setOrientation(VERTICAL);
+ LayoutParams containerParams = new LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.WRAP_CONTENT);
+ containerParams.setMargins(0, scaled(2), 0, scaled(2));
+ container.setLayoutParams(containerParams);
+
+ TextView labelText = new TextView(getContext());
+ labelText.setText(label);
+ labelText.setTextSize(11f * scaleFactor);
+ labelText.setTextColor(ThemeManager.getTextSecondary());
+ LayoutParams labelParams = new LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.WRAP_CONTENT);
+ labelParams.setMargins(0, 0, 0, scaled(2));
+ labelText.setLayoutParams(labelParams);
+ container.addView(labelText);
+
+ HorizontalScrollView scrollView = new HorizontalScrollView(getContext());
+ scrollView.setHorizontalScrollBarEnabled(false);
+ scrollView.setLayoutParams(new LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.WRAP_CONTENT));
+ scrollView.setOverScrollMode(View.OVER_SCROLL_NEVER);
+
+ ModeChips chips = new ModeChips(getContext(), module.getName(), label.toLowerCase(),
+ modes, defaultMode, onModeChanged);
+
+ chips.updateScale(scaleFactor);
+ scrollView.addView(chips);
+ container.addView(scrollView);
+ scalableWidgets.add(new ScalableWidget(chips, labelText, null, null, 0));
+ addView(container);
+ }
+
+ public void addText(String text) {
+ TextView textView = new TextView(getContext());
+ textView.setText(text);
+ textView.setTextSize(10f * scaleFactor);
+ textView.setTextColor(ThemeManager.getTextTertiary());
+ LayoutParams params = new LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.WRAP_CONTENT);
+ params.setMargins(0, scaled(2), 0, scaled(2));
+ textView.setLayoutParams(params);
+ addView(textView);
+ }
+
+
+ public void addCustomView(View view) {
+ if (view.getLayoutParams() == null) {
+ LayoutParams params = new LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.WRAP_CONTENT);
+ params.setMargins(0, scaled(4), 0, scaled(4));
+ view.setLayoutParams(params);
+ }
+ addView(view);
+ }
+
+ public void addThemeButtons() {
+ themeButtons.clear();
+ LinearLayout container = new LinearLayout(getContext());
+ container.setOrientation(HORIZONTAL);
+
+ String[] themeNames = {"Default", "Green", "Purple", "Orange", "Pink"};
+ Runnable[] themeActions = {
+ ThemeManager::applyDefaultTheme,
+ ThemeManager::applyGreenTheme,
+ ThemeManager::applyPurpleTheme,
+ ThemeManager::applyOrangeTheme,
+ ThemeManager::applyPinkTheme
+ };
+
+ for(int i = 0; i < themeNames.length; i++) {
+ TextView button = new TextView(getContext());
+ button.setText(themeNames[i]);
+ button.setTextSize(10f * scaleFactor);
+ button.setGravity(Gravity.CENTER);
+ int paddingH = scaled(8);
+ int paddingV = scaled(5);
+ button.setPadding(paddingH, paddingV, paddingH, paddingV);
+
+
+ button.setBackground(new GradientDrawable());
+
+ final Runnable action = themeActions[i];
+ button.setOnClickListener(v -> action.run());
+
+ LayoutParams params = new LayoutParams(LayoutParams.WRAP_CONTENT, LayoutParams.WRAP_CONTENT);
+ if (i > 0) {
+ params.leftMargin = scaled(4);
+ }
+ container.addView(button, params);
+ themeButtons.add(button);
+ }
+
+ HorizontalScrollView scrollView = new HorizontalScrollView(getContext());
+ scrollView.setHorizontalScrollBarEnabled(false);
+ scrollView.addView(container);
+ addCustomView(scrollView);
+
+ updateThemeButtonStyles();
+ }
+
+ private void updateThemeButtonStyles() {
+ int currentThemeColor = ThemeManager.getThemeColor();
+ for (int i = 0; i < themeButtons.size(); i++) {
+ TextView button = themeButtons.get(i);
+ GradientDrawable bg = (GradientDrawable) button.getBackground();
+
+ boolean isSelected = currentThemeColor == themeColors[i];
+
+ if (isSelected) {
+ bg.setColor(ThemeManager.getThemeColor());
+ button.setTextColor(ThemeManager.getTextOnTheme());
+ } else {
+ bg.setColor(ThemeManager.getBgDisabled());
+ button.setTextColor(ThemeManager.getTextPrimary());
+ }
+ bg.setCornerRadius(scaled(6));
+ }
+ }
+
+
+
+ public void toggle() {
+ toggle(null);
+ }
+
+ public void toggle(Runnable onComplete) {
+ if (isOpen) {
+ collapse(onComplete);
+ } else {
+ expand(onComplete);
+ }
+ }
+
+ private void expand(Runnable onComplete) {
+ isOpen = true;
+ setVisibility(View.VISIBLE);
+ measure(
+ MeasureSpec.makeMeasureSpec(((ViewGroup)getParent()).getWidth(), MeasureSpec.EXACTLY),
+ MeasureSpec.makeMeasureSpec(0, MeasureSpec.UNSPECIFIED)
+ );
+ int targetHeight = getMeasuredHeight();
+ if (heightAnimator != null) heightAnimator.cancel();
+ getLayoutParams().height = 0;
+ heightAnimator = ValueAnimator.ofInt(0, targetHeight);
+ heightAnimator.setDuration(250);
+ heightAnimator.addUpdateListener(animation -> {
+ ViewGroup.LayoutParams params = getLayoutParams();
+ params.height = (int) animation.getAnimatedValue();
+ setLayoutParams(params);
+ });
+ heightAnimator.addListener(new AnimatorListenerAdapter() {
+ @Override
+ public void onAnimationEnd(Animator animation) {
+ ViewGroup.LayoutParams params = getLayoutParams();
+ params.height = LayoutParams.WRAP_CONTENT;
+ setLayoutParams(params);
+ if (onComplete != null) onComplete.run();
+ if (onStateChangedListener != null) onStateChangedListener.run();
+ }
+ });
+ heightAnimator.start();
+ setAlpha(0f);
+ animate().alpha(1f).setDuration(250).start();
+ }
+
+ public void collapse(Runnable onComplete) {
+ if (!isOpen) {
+ if (onComplete != null) onComplete.run();
+ return;
+ }
+ isOpen = false;
+ int currentHeight = getHeight();
+ getLayoutParams().height = currentHeight;
+ if (heightAnimator != null) heightAnimator.cancel();
+ animate().alpha(0f).setDuration(250).start();
+ heightAnimator = ValueAnimator.ofInt(currentHeight, 0);
+ heightAnimator.setDuration(250);
+ heightAnimator.addUpdateListener(animation -> {
+ ViewGroup.LayoutParams params = getLayoutParams();
+ params.height = (int) animation.getAnimatedValue();
+ setLayoutParams(params);
+ });
+ heightAnimator.addListener(new AnimatorListenerAdapter() {
+ @Override
+ public void onAnimationEnd(Animator animation) {
+ setVisibility(View.GONE);
+ if (onComplete != null) onComplete.run();
+ if (onStateChangedListener != null) onStateChangedListener.run();
+ }
+ });
+ heightAnimator.start();
+ }
+
+ public void forceCollapse() {
+ if (heightAnimator != null) heightAnimator.cancel();
+ isOpen = false;
+ setVisibility(View.GONE);
+ setAlpha(0f);
+ getLayoutParams().height = 0;
+ }
+
+ public boolean isOpen() {
+ return isOpen;
+ }
+
+ public void setOnStateChangedListener(Runnable listener) {
+ onStateChangedListener = listener;
+ }
+
+ private int dpToPx(int dp) {
+ return (int) (dp * getResources().getDisplayMetrics().density);
+ }
+
+ // 3. 实现 onThemeColorChanged 方法
+ @Override
+ public void onThemeColorChanged(int newColor) {
+ updateThemeButtonStyles();
+ }
+
+ // 4. 注册和注销监听器
+ @Override
+ protected void onAttachedToWindow() {
+ super.onAttachedToWindow();
+ ThemeManager.addListener(this);
+ }
+
+ @Override
+ protected void onDetachedFromWindow() {
+ super.onDetachedFromWindow();
+ ThemeManager.removeListener(this);
+ }
+
+ private class ScalableWidget {
+ View widget;
+ TextView label;
+ TextView valueText;
+ LayoutParams sliderParams;
+ LayoutParams containerParams;
+ LayoutParams switchParams;
+ int baseSliderHeight;
+ int baseContainerHeight;
+ int baseSwitchWidth;
+ int baseSwitchHeight;
+
+ ScalableWidget(View widget, TextView label, TextView valueText, LayoutParams sliderParams, int baseSliderHeight) {
+ this.widget = widget;
+ this.label = label;
+ this.valueText = valueText;
+ this.sliderParams = sliderParams;
+ this.baseSliderHeight = baseSliderHeight;
+ }
+
+ ScalableWidget(View widget, TextView label, LayoutParams containerParams, LayoutParams switchParams,
+ int baseContainerHeight, int baseSwitchWidth, int baseSwitchHeight) {
+ this.widget = widget;
+ this.label = label;
+ this.containerParams = containerParams;
+ this.switchParams = switchParams;
+ this.baseContainerHeight = baseContainerHeight;
+ this.baseSwitchWidth = baseSwitchWidth;
+ this.baseSwitchHeight = baseSwitchHeight;
+ }
+
+ void updateScale(float scale) {
+ if (label != null) {
+ label.setTextSize(11f * scale);
+ }
+ if (valueText != null) {
+ valueText.setTextSize(11f * scale);
+ }
+ if (sliderParams != null) {
+ sliderParams.height = scaled(baseSliderHeight);
+ sliderParams.setMargins(0, scaled(2), 0, 0);
+ }
+ if (containerParams != null) {
+ containerParams.height = scaled(baseContainerHeight);
+ containerParams.setMargins(0, scaled(2), 0, scaled(2));
+ }
+ if (switchParams != null) {
+ switchParams.width = scaled(baseSwitchWidth);
+ switchParams.height = scaled(baseSwitchHeight);
+ }
+ if (widget instanceof CustomSlider) {
+ ((CustomSlider) widget).updateScale(scale);
+ } else if (widget instanceof CustomSwitch) {
+ ((CustomSwitch) widget).updateScale(scale);
+ } else if (widget instanceof ModeChips) {
+ ((ModeChips) widget).updateScale(scale);
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/res/drawable-v24/ic_launcher_foreground.xml b/app/src/main/res/drawable-v24/ic_launcher_foreground.xml
new file mode 100644
index 0000000..2b068d1
--- /dev/null
+++ b/app/src/main/res/drawable-v24/ic_launcher_foreground.xml
@@ -0,0 +1,30 @@
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/drawable/ic_launcher_background.xml b/app/src/main/res/drawable/ic_launcher_background.xml
new file mode 100644
index 0000000..07d5da9
--- /dev/null
+++ b/app/src/main/res/drawable/ic_launcher_background.xml
@@ -0,0 +1,170 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/app/src/main/res/drawable/ic_launcher_foreground.xml b/app/src/main/res/drawable/ic_launcher_foreground.xml
new file mode 100644
index 0000000..2b068d1
--- /dev/null
+++ b/app/src/main/res/drawable/ic_launcher_foreground.xml
@@ -0,0 +1,30 @@
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/drawable/ic_notification.xml b/app/src/main/res/drawable/ic_notification.xml
new file mode 100644
index 0000000..fb69f7b
--- /dev/null
+++ b/app/src/main/res/drawable/ic_notification.xml
@@ -0,0 +1,10 @@
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/layout/activity_main.xml b/app/src/main/res/layout/activity_main.xml
new file mode 100644
index 0000000..58b90a4
--- /dev/null
+++ b/app/src/main/res/layout/activity_main.xml
@@ -0,0 +1,206 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml b/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml
new file mode 100644
index 0000000..6f3b755
--- /dev/null
+++ b/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml
@@ -0,0 +1,6 @@
+
+
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml b/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml
new file mode 100644
index 0000000..6f3b755
--- /dev/null
+++ b/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml
@@ -0,0 +1,6 @@
+
+
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/mipmap-hdpi/ic_launcher.webp b/app/src/main/res/mipmap-hdpi/ic_launcher.webp
new file mode 100644
index 0000000..c209e78
Binary files /dev/null and b/app/src/main/res/mipmap-hdpi/ic_launcher.webp differ
diff --git a/app/src/main/res/mipmap-hdpi/ic_launcher_round.webp b/app/src/main/res/mipmap-hdpi/ic_launcher_round.webp
new file mode 100644
index 0000000..b2dfe3d
Binary files /dev/null and b/app/src/main/res/mipmap-hdpi/ic_launcher_round.webp differ
diff --git a/app/src/main/res/mipmap-mdpi/ic_launcher.webp b/app/src/main/res/mipmap-mdpi/ic_launcher.webp
new file mode 100644
index 0000000..4f0f1d6
Binary files /dev/null and b/app/src/main/res/mipmap-mdpi/ic_launcher.webp differ
diff --git a/app/src/main/res/mipmap-mdpi/ic_launcher_round.webp b/app/src/main/res/mipmap-mdpi/ic_launcher_round.webp
new file mode 100644
index 0000000..62b611d
Binary files /dev/null and b/app/src/main/res/mipmap-mdpi/ic_launcher_round.webp differ
diff --git a/app/src/main/res/mipmap-xhdpi/ic_launcher.webp b/app/src/main/res/mipmap-xhdpi/ic_launcher.webp
new file mode 100644
index 0000000..948a307
Binary files /dev/null and b/app/src/main/res/mipmap-xhdpi/ic_launcher.webp differ
diff --git a/app/src/main/res/mipmap-xhdpi/ic_launcher_round.webp b/app/src/main/res/mipmap-xhdpi/ic_launcher_round.webp
new file mode 100644
index 0000000..1b9a695
Binary files /dev/null and b/app/src/main/res/mipmap-xhdpi/ic_launcher_round.webp differ
diff --git a/app/src/main/res/mipmap-xxhdpi/ic_launcher.webp b/app/src/main/res/mipmap-xxhdpi/ic_launcher.webp
new file mode 100644
index 0000000..28d4b77
Binary files /dev/null and b/app/src/main/res/mipmap-xxhdpi/ic_launcher.webp differ
diff --git a/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp b/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp
new file mode 100644
index 0000000..9287f50
Binary files /dev/null and b/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp differ
diff --git a/app/src/main/res/mipmap-xxxhdpi/ic_launcher.webp b/app/src/main/res/mipmap-xxxhdpi/ic_launcher.webp
new file mode 100644
index 0000000..aa7d642
Binary files /dev/null and b/app/src/main/res/mipmap-xxxhdpi/ic_launcher.webp differ
diff --git a/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp b/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp
new file mode 100644
index 0000000..9126ae3
Binary files /dev/null and b/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp differ
diff --git a/app/src/main/res/values-night/colors.xml b/app/src/main/res/values-night/colors.xml
new file mode 100644
index 0000000..a6b3dae
--- /dev/null
+++ b/app/src/main/res/values-night/colors.xml
@@ -0,0 +1,2 @@
+
+
\ No newline at end of file
diff --git a/app/src/main/res/values-night/themes.xml b/app/src/main/res/values-night/themes.xml
new file mode 100644
index 0000000..e8f677b
--- /dev/null
+++ b/app/src/main/res/values-night/themes.xml
@@ -0,0 +1,9 @@
+
+
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/values/colors.xml b/app/src/main/res/values/colors.xml
new file mode 100644
index 0000000..f8c6127
--- /dev/null
+++ b/app/src/main/res/values/colors.xml
@@ -0,0 +1,10 @@
+
+
+ #FFBB86FC
+ #FF6200EE
+ #FF3700B3
+ #FF03DAC5
+ #FF018786
+ #FF000000
+ #FFFFFFFF
+
\ No newline at end of file
diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml
new file mode 100644
index 0000000..99d454c
--- /dev/null
+++ b/app/src/main/res/values/strings.xml
@@ -0,0 +1,3 @@
+
+ Phoen1xGUI
+
\ No newline at end of file
diff --git a/app/src/main/res/values/themes.xml b/app/src/main/res/values/themes.xml
new file mode 100644
index 0000000..03dc3af
--- /dev/null
+++ b/app/src/main/res/values/themes.xml
@@ -0,0 +1,18 @@
+
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/xml/backup_rules.xml b/app/src/main/res/xml/backup_rules.xml
new file mode 100644
index 0000000..4df9255
--- /dev/null
+++ b/app/src/main/res/xml/backup_rules.xml
@@ -0,0 +1,13 @@
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/xml/data_extraction_rules.xml b/app/src/main/res/xml/data_extraction_rules.xml
new file mode 100644
index 0000000..9ee9997
--- /dev/null
+++ b/app/src/main/res/xml/data_extraction_rules.xml
@@ -0,0 +1,19 @@
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/build.gradle.kts b/build.gradle.kts
new file mode 100644
index 0000000..922f551
--- /dev/null
+++ b/build.gradle.kts
@@ -0,0 +1,5 @@
+// Top-level build file where you can add configuration options common to all sub-projects/modules.
+plugins {
+ alias(libs.plugins.android.application) apply false
+ alias(libs.plugins.kotlin.android) apply false
+}
\ No newline at end of file
diff --git a/gradle.properties b/gradle.properties
new file mode 100644
index 0000000..001aa8d
--- /dev/null
+++ b/gradle.properties
@@ -0,0 +1,24 @@
+# Project-wide Gradle settings.
+# IDE (e.g. Android Studio) users:
+# Gradle settings configured through the IDE *will override*
+# any settings specified in this file.
+# For more details on how to configure your build environment visit
+# http://www.gradle.org/docs/current/userguide/build_environment.html
+# Specifies the JVM arguments used for the daemon process.
+# The setting is particularly useful for tweaking memory settings.
+org.gradle.jvmargs=-Xmx2048m -Dfile.encoding=UTF-8
+# When configured, Gradle will run in incubating parallel mode.
+# This option should only be used with decoupled projects. For more details, visit
+# https://developer.android.com/r/tools/gradle-multi-project-decoupled-projects
+# org.gradle.parallel=true
+# AndroidX package structure to make it clearer which packages are bundled with the
+# Android operating system, and which are packaged with your app's APK
+# https://developer.android.com/topic/libraries/support-library/androidx-rn
+android.useAndroidX=true
+# Kotlin code style for this project: "official" or "obsolete":
+kotlin.code.style=official
+# Enables namespacing of each library's R class so that its R class includes only the
+# resources declared in the library itself and none from the library's dependencies,
+# thereby reducing the size of the R class for that library
+android.nonTransitiveRClass=true
+android.aapt2FromMavenOverride=/home/ph/app/aapt2
\ No newline at end of file
diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml
new file mode 100644
index 0000000..5712302
--- /dev/null
+++ b/gradle/libs.versions.toml
@@ -0,0 +1,26 @@
+[versions]
+agp = "8.13.0"
+kotlin = "2.0.21"
+coreKtx = "1.10.1"
+junit = "4.13.2"
+junitVersion = "1.1.5"
+espressoCore = "3.5.1"
+lifecycleRuntimeKtx = "2.6.1"
+appcompat = "1.6.1"
+activity = "1.8.0"
+material = "1.12.0"
+
+[libraries]
+androidx-core-ktx = { group = "androidx.core", name = "core-ktx", version.ref = "coreKtx" }
+junit = { group = "junit", name = "junit", version.ref = "junit" }
+androidx-junit = { group = "androidx.test.ext", name = "junit", version.ref = "junitVersion" }
+androidx-espresso-core = { group = "androidx.test.espresso", name = "espresso-core", version.ref = "espressoCore" }
+androidx-lifecycle-runtime-ktx = { group = "androidx.lifecycle", name = "lifecycle-runtime-ktx", version.ref = "lifecycleRuntimeKtx" }
+androidx-appcompat = { group = "androidx.appcompat", name = "appcompat", version.ref = "appcompat" }
+androidx-activity = { group = "androidx.activity", name = "activity", version.ref = "activity" }
+material = { group = "com.google.android.material", name = "material", version.ref = "material" }
+
+[plugins]
+android-application = { id = "com.android.application", version.ref = "agp" }
+kotlin-android = { id = "org.jetbrains.kotlin.android", version.ref = "kotlin" }
+
diff --git a/gradle/wrapper/gradle-wrapper.jar b/gradle/wrapper/gradle-wrapper.jar
new file mode 100644
index 0000000..e708b1c
Binary files /dev/null and b/gradle/wrapper/gradle-wrapper.jar differ
diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties
new file mode 100644
index 0000000..cd134a6
--- /dev/null
+++ b/gradle/wrapper/gradle-wrapper.properties
@@ -0,0 +1,6 @@
+#Fri Oct 03 19:51:02 CST 2025
+distributionBase=GRADLE_USER_HOME
+distributionPath=wrapper/dists
+distributionUrl=https\://services.gradle.org/distributions/gradle-8.13-bin.zip
+zipStoreBase=GRADLE_USER_HOME
+zipStorePath=wrapper/dists
diff --git a/gradlew b/gradlew
new file mode 100644
index 0000000..4f906e0
--- /dev/null
+++ b/gradlew
@@ -0,0 +1,185 @@
+#!/usr/bin/env sh
+
+#
+# Copyright 2015 the original author or authors.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# https://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+
+##############################################################################
+##
+## Gradle start up script for UN*X
+##
+##############################################################################
+
+# Attempt to set APP_HOME
+# Resolve links: $0 may be a link
+PRG="$0"
+# Need this for relative symlinks.
+while [ -h "$PRG" ] ; do
+ ls=`ls -ld "$PRG"`
+ link=`expr "$ls" : '.*-> \(.*\)$'`
+ if expr "$link" : '/.*' > /dev/null; then
+ PRG="$link"
+ else
+ PRG=`dirname "$PRG"`"/$link"
+ fi
+done
+SAVED="`pwd`"
+cd "`dirname \"$PRG\"`/" >/dev/null
+APP_HOME="`pwd -P`"
+cd "$SAVED" >/dev/null
+
+APP_NAME="Gradle"
+APP_BASE_NAME=`basename "$0"`
+
+# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
+DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"'
+
+# Use the maximum available, or set MAX_FD != -1 to use that value.
+MAX_FD="maximum"
+
+warn () {
+ echo "$*"
+}
+
+die () {
+ echo
+ echo "$*"
+ echo
+ exit 1
+}
+
+# OS specific support (must be 'true' or 'false').
+cygwin=false
+msys=false
+darwin=false
+nonstop=false
+case "`uname`" in
+ CYGWIN* )
+ cygwin=true
+ ;;
+ Darwin* )
+ darwin=true
+ ;;
+ MINGW* )
+ msys=true
+ ;;
+ NONSTOP* )
+ nonstop=true
+ ;;
+esac
+
+CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar
+
+
+# Determine the Java command to use to start the JVM.
+if [ -n "$JAVA_HOME" ] ; then
+ if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
+ # IBM's JDK on AIX uses strange locations for the executables
+ JAVACMD="$JAVA_HOME/jre/sh/java"
+ else
+ JAVACMD="$JAVA_HOME/bin/java"
+ fi
+ if [ ! -x "$JAVACMD" ] ; then
+ die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME
+
+Please set the JAVA_HOME variable in your environment to match the
+location of your Java installation."
+ fi
+else
+ JAVACMD="java"
+ which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
+
+Please set the JAVA_HOME variable in your environment to match the
+location of your Java installation."
+fi
+
+# Increase the maximum file descriptors if we can.
+if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then
+ MAX_FD_LIMIT=`ulimit -H -n`
+ if [ $? -eq 0 ] ; then
+ if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then
+ MAX_FD="$MAX_FD_LIMIT"
+ fi
+ ulimit -n $MAX_FD
+ if [ $? -ne 0 ] ; then
+ warn "Could not set maximum file descriptor limit: $MAX_FD"
+ fi
+ else
+ warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT"
+ fi
+fi
+
+# For Darwin, add options to specify how the application appears in the dock
+if $darwin; then
+ GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\""
+fi
+
+# For Cygwin or MSYS, switch paths to Windows format before running java
+if [ "$cygwin" = "true" -o "$msys" = "true" ] ; then
+ APP_HOME=`cygpath --path --mixed "$APP_HOME"`
+ CLASSPATH=`cygpath --path --mixed "$CLASSPATH"`
+
+ JAVACMD=`cygpath --unix "$JAVACMD"`
+
+ # We build the pattern for arguments to be converted via cygpath
+ ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null`
+ SEP=""
+ for dir in $ROOTDIRSRAW ; do
+ ROOTDIRS="$ROOTDIRS$SEP$dir"
+ SEP="|"
+ done
+ OURCYGPATTERN="(^($ROOTDIRS))"
+ # Add a user-defined pattern to the cygpath arguments
+ if [ "$GRADLE_CYGPATTERN" != "" ] ; then
+ OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)"
+ fi
+ # Now convert the arguments - kludge to limit ourselves to /bin/sh
+ i=0
+ for arg in "$@" ; do
+ CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -`
+ CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option
+
+ if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition
+ eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"`
+ else
+ eval `echo args$i`="\"$arg\""
+ fi
+ i=`expr $i + 1`
+ done
+ case $i in
+ 0) set -- ;;
+ 1) set -- "$args0" ;;
+ 2) set -- "$args0" "$args1" ;;
+ 3) set -- "$args0" "$args1" "$args2" ;;
+ 4) set -- "$args0" "$args1" "$args2" "$args3" ;;
+ 5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;;
+ 6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;;
+ 7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;;
+ 8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;;
+ 9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;;
+ esac
+fi
+
+# Escape application args
+save () {
+ for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done
+ echo " "
+}
+APP_ARGS=`save "$@"`
+
+# Collect all arguments for the java command, following the shell quoting and substitution rules
+eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS"
+
+exec "$JAVACMD" "$@"
diff --git a/gradlew.bat b/gradlew.bat
new file mode 100644
index 0000000..ac1b06f
--- /dev/null
+++ b/gradlew.bat
@@ -0,0 +1,89 @@
+@rem
+@rem Copyright 2015 the original author or authors.
+@rem
+@rem Licensed under the Apache License, Version 2.0 (the "License");
+@rem you may not use this file except in compliance with the License.
+@rem You may obtain a copy of the License at
+@rem
+@rem https://www.apache.org/licenses/LICENSE-2.0
+@rem
+@rem Unless required by applicable law or agreed to in writing, software
+@rem distributed under the License is distributed on an "AS IS" BASIS,
+@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+@rem See the License for the specific language governing permissions and
+@rem limitations under the License.
+@rem
+
+@if "%DEBUG%" == "" @echo off
+@rem ##########################################################################
+@rem
+@rem Gradle startup script for Windows
+@rem
+@rem ##########################################################################
+
+@rem Set local scope for the variables with windows NT shell
+if "%OS%"=="Windows_NT" setlocal
+
+set DIRNAME=%~dp0
+if "%DIRNAME%" == "" set DIRNAME=.
+set APP_BASE_NAME=%~n0
+set APP_HOME=%DIRNAME%
+
+@rem Resolve any "." and ".." in APP_HOME to make it shorter.
+for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi
+
+@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
+set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m"
+
+@rem Find java.exe
+if defined JAVA_HOME goto findJavaFromJavaHome
+
+set JAVA_EXE=java.exe
+%JAVA_EXE% -version >NUL 2>&1
+if "%ERRORLEVEL%" == "0" goto execute
+
+echo.
+echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
+echo.
+echo Please set the JAVA_HOME variable in your environment to match the
+echo location of your Java installation.
+
+goto fail
+
+:findJavaFromJavaHome
+set JAVA_HOME=%JAVA_HOME:"=%
+set JAVA_EXE=%JAVA_HOME%/bin/java.exe
+
+if exist "%JAVA_EXE%" goto execute
+
+echo.
+echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME%
+echo.
+echo Please set the JAVA_HOME variable in your environment to match the
+echo location of your Java installation.
+
+goto fail
+
+:execute
+@rem Setup the command line
+
+set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar
+
+
+@rem Execute Gradle
+"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %*
+
+:end
+@rem End local scope for the variables with windows NT shell
+if "%ERRORLEVEL%"=="0" goto mainEnd
+
+:fail
+rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
+rem the _cmd.exe /c_ return code!
+if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1
+exit /b 1
+
+:mainEnd
+if "%OS%"=="Windows_NT" endlocal
+
+:omega
diff --git a/readme.md b/readme.md
new file mode 100644
index 0000000..b2addea
--- /dev/null
+++ b/readme.md
@@ -0,0 +1,7 @@
+使用 gradle 8.13 构建
+aide 是构建不了的
+androidide 需要自己迁移 src
+售出不退
+含少量屎山
+
+灵动岛名称为 Phoen1xGUI 的即为 v2.0 版本,该版本优化了性能。
\ No newline at end of file
diff --git a/settings.gradle.kts b/settings.gradle.kts
new file mode 100644
index 0000000..f3b5496
--- /dev/null
+++ b/settings.gradle.kts
@@ -0,0 +1,23 @@
+pluginManagement {
+ repositories {
+ google {
+ content {
+ includeGroupByRegex("com\\.android.*")
+ includeGroupByRegex("com\\.google.*")
+ includeGroupByRegex("androidx.*")
+ }
+ }
+ mavenCentral()
+ gradlePluginPortal()
+ }
+}
+dependencyResolutionManagement {
+ repositoriesMode.set(RepositoriesMode.FAIL_ON_PROJECT_REPOS)
+ repositories {
+ google()
+ mavenCentral()
+ }
+}
+
+rootProject.name = "Phoen1xGUI"
+include(":app")