功能优化

This commit is contained in:
lihongwei 2025-06-30 14:18:51 +08:00
parent 73e2478168
commit 5a78b467fc
8 changed files with 197 additions and 78 deletions

View File

@ -9,8 +9,11 @@ import com.auto.clicker.autoclicker.room.entity.Solution;
import java.util.List;
import com.google.gson.annotations.Expose; // 确保导入 @Expose
public class SolutionWithEventsAndPoints {
@Embedded
@Expose
public Solution solution;
@Relation(
@ -18,10 +21,12 @@ public class SolutionWithEventsAndPoints {
entityColumn = "solution_id",
entity = EventEntity.class
)
@Expose
public List<EventWithPoints> events;
public static class EventWithPoints {
@Embedded
@Expose
public EventEntity eventEntity;
@Relation(
@ -29,6 +34,7 @@ public class SolutionWithEventsAndPoints {
entityColumn = "pointId",
entity = TouchPoint.class
)
@Expose
public TouchPoint clickTouchPoint;
@Relation(
@ -36,6 +42,7 @@ public class SolutionWithEventsAndPoints {
entityColumn = "pointId",
entity = TouchPoint.class
)
@Expose
public TouchPoint slideStartTouchPoint;
@Relation(
@ -43,6 +50,7 @@ public class SolutionWithEventsAndPoints {
entityColumn = "pointId",
entity = TouchPoint.class
)
@Expose
public TouchPoint slideEndTouchPoint;
}
}
}

View File

@ -164,6 +164,73 @@ public class EventRepository {
});
}
@Transaction
public void importSolutionFromData(SolutionWithEventsAndPoints importedData, RepositoryCallback<Void> callback) {
databaseWriteExecutor.execute(() -> {
try {
Solution newSolution = new Solution(importedData.solution.solutionName, importedData.solution.mode);
long newSolutionId = solutionDao.insertSolution(newSolution);
if (newSolutionId == -1) {
throw new RuntimeException("Failed to import solution: Could not insert new Solution.");
}
for (SolutionWithEventsAndPoints.EventWithPoints eventWithPoints : importedData.events) {
Long newClickPointId = null;
if (eventWithPoints.clickTouchPoint != null) {
TouchPoint newClickPoint = new TouchPoint(eventWithPoints.clickTouchPoint.x, eventWithPoints.clickTouchPoint.y);
newClickPointId = touchPointDao.insertPoint(newClickPoint);
if (newClickPointId == -1) throw new RuntimeException("Failed to import click point.");
}
Long newSlideStartPointId = null;
if (eventWithPoints.slideStartTouchPoint != null) {
TouchPoint newSlideStartPoint = new TouchPoint(eventWithPoints.slideStartTouchPoint.x, eventWithPoints.slideStartTouchPoint.y);
newSlideStartPointId = touchPointDao.insertPoint(newSlideStartPoint);
if (newSlideStartPointId == -1) throw new RuntimeException("Failed to import slide start point.");
}
Long newSlideEndPointId = null;
if (eventWithPoints.slideEndTouchPoint != null) {
TouchPoint newSlideEndPoint = new TouchPoint(eventWithPoints.slideEndTouchPoint.x, eventWithPoints.slideEndTouchPoint.y);
newSlideEndPointId = touchPointDao.insertPoint(newSlideEndPoint);
if (newSlideEndPointId == -1) throw new RuntimeException("Failed to import slide end point.");
}
EventEntity newEventEntity = new EventEntity(
newSolutionId, // 关联到新生成的方案ID
eventWithPoints.eventEntity.eventType,
eventWithPoints.eventEntity.orderInSolution,
newClickPointId,
newSlideStartPointId,
newSlideEndPointId,
eventWithPoints.eventEntity.duration
);
long newEventEntityId = eventEntityDao.insertEventEntity(newEventEntity);
if (newEventEntityId == -1) {
throw new RuntimeException("Failed to import event entity.");
}
}
if (callback != null) callback.onComplete(null);
} catch (Exception e) {
Log.e("EventRepository", "Error importing solution: " + e.getMessage(), e);
if (callback != null) {
callback.onError(e);
}
}
});
}
public SolutionWithEventsAndPoints importSolutionFromJson(String jsonString) {
try {
Gson gson = new GsonBuilder().excludeFieldsWithoutExposeAnnotation().create();
return gson.fromJson(jsonString, SolutionWithEventsAndPoints.class);
} catch (Exception e) {
Log.e("EventRepository", "Error parsing JSON for import: " + e.getMessage(), e);
return null;
}
}
@Transaction
public void saveDuplicateSolution(SolutionWithEventsAndPoints originalData, RepositoryCallback<Void> callback) {
databaseWriteExecutor.execute(() -> {

View File

@ -8,7 +8,6 @@ import android.os.Looper;
import android.util.Log;
import android.view.View;
import android.widget.EditText;
import android.widget.Toast;
import androidx.activity.EdgeToEdge;
import androidx.annotation.Nullable;
@ -28,6 +27,9 @@ import com.auto.clicker.autoclicker.ui.adapter.floating.SolutionAdapter;
import com.auto.clicker.autoclicker.ui.dialog.CreateModeSelectionDialog;
import com.auto.clicker.autoclicker.view.FloatingViewManager;
import java.io.BufferedReader;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.OutputStream;
import java.util.ArrayList;
import java.util.List;
@ -40,6 +42,7 @@ public class ScriptsActivity extends AppCompatActivity implements SolutionAdapte
private static final String TAG = "ScriptsActivity";
private static final int CREATE_FILE_REQUEST_CODE = 1;
private static final int PICK_FILE_REQUEST_CODE = 2;
@Override
protected void onCreate(Bundle savedInstanceState) {
@ -54,12 +57,10 @@ public class ScriptsActivity extends AppCompatActivity implements SolutionAdapte
});
binding.scriptsRecyclerView.setLayoutManager(new LinearLayoutManager(this));
// this 作为 OnSolutionClickListener OnOptionMenuClickListener 传入
solutionsAdapter = new SolutionAdapter(new ArrayList<>(), this::onSolutionSelected, this);
binding.scriptsRecyclerView.setAdapter(solutionsAdapter);
// 确保 FloatingViewManager Activity 创建时被初始化
floatingViewManager = new FloatingViewManager(this); // 如果 FloatingViewManager 依赖 Context这里需要传递
floatingViewManager = new FloatingViewManager(this);
binding.btnBack.setOnClickListener(v -> finish());
@ -69,13 +70,11 @@ public class ScriptsActivity extends AppCompatActivity implements SolutionAdapte
});
binding.importScripts.setOnClickListener(v -> {
Toast.makeText(this, "导入方案功能待实现", Toast.LENGTH_SHORT).show();
// TODO: 实现导入方案的逻辑
pickJsonFileForImport();
});
binding.fabImportScripts.setOnClickListener(v -> {
Toast.makeText(this, "右下角导入方案功能待实现", Toast.LENGTH_SHORT).show();
// TODO: 实现右下角导入方案的逻辑可能与上面的importScripts按钮功能相同或类似
binding.customImportView.setOnClickListener(v -> {
pickJsonFileForImport();
});
loadSolutions();
@ -90,24 +89,16 @@ public class ScriptsActivity extends AppCompatActivity implements SolutionAdapte
private void loadSolutions() {
if (floatingViewManager == null) {
Log.e(TAG, "FloatingViewManager 未设置,无法加载方案列表。");
Toast.makeText(this, "加载方案列表功能不可用,请联系开发者。", Toast.LENGTH_SHORT).show();
updateLayout(true);
return;
}
// 确保 FloatingViewManager 能够提供 EventRepository 实例
// 假设 FloatingViewManager 内部管理着 EventRepository
floatingViewManager.getEventRepository().getAllSolutions(new EventRepository.RepositoryCallback<List<Solution>>() {
@Override
public void onComplete(List<Solution> solutions) {
new Handler(Looper.getMainLooper()).post(() -> {
solutionsAdapter.setSolutions(solutions);
updateLayout(solutions.isEmpty());
if (solutions.isEmpty()) {
Toast.makeText(ScriptsActivity.this, "没有可加载的方案。", Toast.LENGTH_SHORT).show();
} else {
Toast.makeText(ScriptsActivity.this, "方案加载成功!", Toast.LENGTH_SHORT).show();
}
});
}
@ -115,7 +106,6 @@ public class ScriptsActivity extends AppCompatActivity implements SolutionAdapte
public void onError(Exception e) {
new Handler(Looper.getMainLooper()).post(() -> {
Log.e(TAG, "加载方案列表失败: " + e.getMessage(), e);
Toast.makeText(ScriptsActivity.this, "加载方案列表失败。", Toast.LENGTH_SHORT).show();
updateLayout(true);
});
}
@ -126,11 +116,11 @@ public class ScriptsActivity extends AppCompatActivity implements SolutionAdapte
if (isEmpty) {
binding.noScriptsGroup.setVisibility(View.VISIBLE);
binding.scriptsRecyclerView.setVisibility(View.GONE);
binding.fabImportScripts.setVisibility(View.GONE);
binding.customImportView.setVisibility(View.GONE);
} else {
binding.noScriptsGroup.setVisibility(View.GONE);
binding.scriptsRecyclerView.setVisibility(View.VISIBLE);
binding.fabImportScripts.setVisibility(View.VISIBLE);
binding.customImportView.setVisibility(View.VISIBLE);
}
}
@ -138,10 +128,8 @@ public class ScriptsActivity extends AppCompatActivity implements SolutionAdapte
if (floatingViewManager != null) {
floatingViewManager.showFloatingViews(solution.getMode());
floatingViewManager.loadSolution(solution.getSolutionId());
Toast.makeText(this, "正在加载方案: " + solution.getSolutionName(), Toast.LENGTH_SHORT).show();
} else {
Log.e(TAG, "FloatingViewManager 未设置!无法加载方案。");
Toast.makeText(this, "加载功能不可用,请联系开发者。", Toast.LENGTH_SHORT).show();
}
}
@ -180,8 +168,7 @@ public class ScriptsActivity extends AppCompatActivity implements SolutionAdapte
@Override
public void onComplete(Void result) {
new Handler(Looper.getMainLooper()).post(() -> {
Toast.makeText(ScriptsActivity.this, "方案已重命名为: " + newName, Toast.LENGTH_SHORT).show();
loadSolutions(); // 刷新列表
loadSolutions();
});
}
@ -189,14 +176,9 @@ public class ScriptsActivity extends AppCompatActivity implements SolutionAdapte
public void onError(Exception e) {
new Handler(Looper.getMainLooper()).post(() -> {
Log.e(TAG, "重命名失败: " + e.getMessage(), e);
Toast.makeText(ScriptsActivity.this, "重命名失败: " + e.getMessage(), Toast.LENGTH_SHORT).show();
});
}
});
} else if (newName.isEmpty()) {
Toast.makeText(ScriptsActivity.this, "方案名称不能为空。", Toast.LENGTH_SHORT).show();
} else {
Toast.makeText(ScriptsActivity.this, "新名称与原名称相同。", Toast.LENGTH_SHORT).show();
}
});
builder.setNegativeButton("取消", (dialog, which) -> dialog.cancel());
@ -211,23 +193,18 @@ public class ScriptsActivity extends AppCompatActivity implements SolutionAdapte
floatingViewManager.getEventRepository().saveDuplicateSolution(originalData, new EventRepository.RepositoryCallback<Void>() {
@Override
public void onComplete(Void result) {
new Handler(Looper.getMainLooper()).post(() -> {
Toast.makeText(ScriptsActivity.this, "方案 '" + originalSolution.getSolutionName() + "' 已复制成功!", Toast.LENGTH_SHORT).show();
loadSolutions(); // 刷新列表
});
new Handler(Looper.getMainLooper()).post(ScriptsActivity.this::loadSolutions);
}
@Override
public void onError(Exception e) {
new Handler(Looper.getMainLooper()).post(() -> {
Log.e(TAG, "复制方案失败: " + e.getMessage(), e);
Toast.makeText(ScriptsActivity.this, "复制方案失败: " + e.getMessage(), Toast.LENGTH_SHORT).show();
});
}
});
} else {
new Handler(Looper.getMainLooper()).post(() -> {
Toast.makeText(ScriptsActivity.this, "无法找到原始方案数据进行复制。", Toast.LENGTH_SHORT).show();
});
}
}
@ -236,7 +213,6 @@ public class ScriptsActivity extends AppCompatActivity implements SolutionAdapte
public void onError(Exception e) {
new Handler(Looper.getMainLooper()).post(() -> {
Log.e(TAG, "获取原始方案数据失败: " + e.getMessage(), e);
Toast.makeText(ScriptsActivity.this, "获取原始方案数据失败: " + e.getMessage(), Toast.LENGTH_SHORT).show();
});
}
});
@ -256,8 +232,7 @@ public class ScriptsActivity extends AppCompatActivity implements SolutionAdapte
@Override
public void onComplete(Void result) {
new Handler(Looper.getMainLooper()).post(() -> {
Toast.makeText(ScriptsActivity.this, "方案 '" + solution.getSolutionName() + "' 已删除。", Toast.LENGTH_SHORT).show();
loadSolutions(); // 刷新列表
loadSolutions();
});
}
@ -265,12 +240,18 @@ public class ScriptsActivity extends AppCompatActivity implements SolutionAdapte
public void onError(Exception e) {
new Handler(Looper.getMainLooper()).post(() -> {
Log.e(TAG, "删除方案失败: " + e.getMessage(), e);
Toast.makeText(ScriptsActivity.this, "删除方案失败: " + e.getMessage(), Toast.LENGTH_SHORT).show();
});
}
});
}
private void pickJsonFileForImport() {
Intent intent = new Intent(Intent.ACTION_OPEN_DOCUMENT);
intent.addCategory(Intent.CATEGORY_OPENABLE);
intent.setType("application/json");
startActivityForResult(intent, PICK_FILE_REQUEST_CODE);
}
private void exportSolution(final Solution solution) {
floatingViewManager.getEventRepository().getSolutionWithEventsAndPoints(solution.getSolutionId(), new EventRepository.RepositoryCallback<SolutionWithEventsAndPoints>() {
@Override
@ -283,11 +264,7 @@ public class ScriptsActivity extends AppCompatActivity implements SolutionAdapte
intent.setType("application/json");
intent.putExtra(Intent.EXTRA_TITLE, solution.getSolutionName() + ".json");
startActivityForResult(intent, CREATE_FILE_REQUEST_CODE);
} else {
Toast.makeText(ScriptsActivity.this, "无法生成导出数据。", Toast.LENGTH_SHORT).show();
}
} else {
Toast.makeText(ScriptsActivity.this, "无法获取方案数据进行导出。", Toast.LENGTH_SHORT).show();
}
}
@ -295,12 +272,57 @@ public class ScriptsActivity extends AppCompatActivity implements SolutionAdapte
public void onError(Exception e) {
new Handler(Looper.getMainLooper()).post(() -> {
Log.e(TAG, "获取方案数据失败: " + e.getMessage(), e);
Toast.makeText(ScriptsActivity.this, "获取方案数据失败: " + e.getMessage(), Toast.LENGTH_SHORT).show();
});
}
});
}
private void importSolutionFromFile(Uri uri) {
try {
InputStream inputStream = getContentResolver().openInputStream(uri);
if (inputStream == null) {
return;
}
StringBuilder stringBuilder = new StringBuilder();
try (BufferedReader reader = new BufferedReader(new InputStreamReader(inputStream))) {
String line;
while ((line = reader.readLine()) != null) {
stringBuilder.append(line);
}
}
String jsonString = stringBuilder.toString();
if (jsonString.isEmpty()) {
return;
}
SolutionWithEventsAndPoints importedData = floatingViewManager.getEventRepository().importSolutionFromJson(jsonString);
if (importedData != null && importedData.solution != null) {
// 保存到数据库
floatingViewManager.getEventRepository().importSolutionFromData(importedData, new EventRepository.RepositoryCallback<Void>() {
@Override
public void onComplete(Void result) {
new Handler(Looper.getMainLooper()).post(() -> {
loadSolutions(); // 刷新列表
});
}
@Override
public void onError(Exception e) {
new Handler(Looper.getMainLooper()).post(() -> {
Log.e(TAG, "导入方案失败: " + e.getMessage(), e);
});
}
});
}
} catch (Exception e) {
Log.e(TAG, "读取或解析导入文件失败: " + e.getMessage(), e);
}
}
@Override
protected void onActivityResult(int requestCode, int resultCode, @Nullable Intent data) {
super.onActivityResult(requestCode, resultCode, data);
@ -311,20 +333,20 @@ public class ScriptsActivity extends AppCompatActivity implements SolutionAdapte
try (OutputStream outputStream = getContentResolver().openOutputStream(uri)) {
if (outputStream != null) {
outputStream.write(tempExportJson.getBytes());
Toast.makeText(this, "方案已成功导出!", Toast.LENGTH_SHORT).show();
}
} catch (Exception e) {
Log.e(TAG, "保存文件失败: " + e.getMessage(), e);
Toast.makeText(this, "保存文件失败: " + e.getMessage(), Toast.LENGTH_SHORT).show();
} finally {
tempExportJson = null; // 清除临时数据
tempExportJson = null;
}
} else {
Toast.makeText(this, "没有可导出的数据。", Toast.LENGTH_SHORT).show();
}
} else {
Toast.makeText(this, "文件创建操作被取消或数据无效。", Toast.LENGTH_SHORT).show();
}
} else if (requestCode == PICK_FILE_REQUEST_CODE && resultCode == RESULT_OK) {
if (data != null && data.getData() != null) {
Uri uri = data.getData();
importSolutionFromFile(uri);
}
}
}
}

View File

@ -76,12 +76,12 @@ public class SolutionAdapter extends RecyclerView.Adapter<SolutionAdapter.Soluti
});
if (optionMenuListener != null) {
holder.option.setVisibility(View.VISIBLE); // 显示按钮
holder.option.setVisibility(View.VISIBLE);
holder.option.setOnClickListener(v -> {
holder.showOptionMenu(solution, optionMenuListener); // 调用方法显示菜单
holder.showOptionMenu(solution, optionMenuListener);
});
} else {
holder.option.setVisibility(View.GONE); // 隐藏按钮
holder.option.setVisibility(View.GONE);
}
}
@ -114,28 +114,25 @@ public class SolutionAdapter extends RecyclerView.Adapter<SolutionAdapter.Soluti
option = itemView.findViewById(R.id.option);
}
// 显示选项菜单的私有方法
private void showOptionMenu(Solution solution, OnOptionMenuClickListener menuListener) {
View popupView = LayoutInflater.from(context).inflate(R.layout.option_menu_layout, null);
PopupWindow popupWindow = new PopupWindow(
popupView,
ViewGroup.LayoutParams.WRAP_CONTENT,
ViewGroup.LayoutParams.WRAP_CONTENT,
true // 设置为 true允许点击外部关闭
true
);
popupWindow.setElevation(8f); // 添加阴影
popupWindow.setBackgroundDrawable(context.getDrawable(android.R.color.transparent)); // 设置透明背景以便显示阴影
popupWindow.setElevation(8f);
popupWindow.setBackgroundDrawable(context.getDrawable(android.R.color.transparent));
// 查找 popupView 中的 TextView
TextView tvRename = popupView.findViewById(R.id.tv_rename);
TextView tvDuplicate = popupView.findViewById(R.id.tv_duplicate);
TextView tvDelete = popupView.findViewById(R.id.tv_delete);
TextView tvExport = popupView.findViewById(R.id.tv_export);
// 设置点击监听器
tvRename.setOnClickListener(v -> {
menuListener.onRenameClick(solution);
popupWindow.dismiss(); // 关闭弹窗
popupWindow.dismiss();
});
tvDuplicate.setOnClickListener(v -> {
@ -153,7 +150,6 @@ public class SolutionAdapter extends RecyclerView.Adapter<SolutionAdapter.Soluti
popupWindow.dismiss();
});
// 显示 PopupWindow锚定在 option 按钮下方
popupWindow.showAsDropDown(option);
}
}

View File

@ -84,7 +84,7 @@ public class FloatingViewManager {
private static final String KEY_CONTROL_BAR_WIDTH = "control_bar_width";
private static final int DEFAULT_TOUCH_POINT_SIZE = 100;
private static final int DEFAULT_CONTROL_BAR_WIDTH_DP = 38;
private static final int DEFAULT_CONTROL_BAR_WIDTH_DP = 50;
private ImageView playButton;

View File

@ -96,8 +96,9 @@
android:layout_width="0dp"
android:layout_height="0dp"
android:layout_marginTop="16dp"
android:layout_marginBottom="15dp"
android:visibility="gone"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintBottom_toTopOf="@+id/custom_import_view"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintHorizontal_bias="0.0"
app:layout_constraintStart_toStartOf="parent"
@ -105,19 +106,42 @@
app:layout_constraintVertical_bias="1.0"
tools:visibility="visible" />
<com.google.android.material.floatingactionbutton.FloatingActionButton
android:id="@+id/fab_import_scripts"
android:layout_width="127dp"
android:layout_height="46dp"
<LinearLayout
android:id="@+id/custom_import_view"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginEnd="24dp"
android:layout_marginBottom="24dp"
android:background="@drawable/rounded_edittext_background"
android:backgroundTint="@color/blue"
android:clickable="true"
android:focusable="true"
android:src="@drawable/import_script"
android:gravity="center_vertical"
android:orientation="horizontal"
android:paddingStart="16dp"
android:paddingTop="10dp"
android:paddingEnd="24dp"
android:paddingBottom="10dp"
android:visibility="gone"
app:backgroundTint="#0BC4FC"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:tint="@color/white" />
app:layout_constraintEnd_toEndOf="parent">
<ImageView
android:id="@+id/icon_import"
android:layout_width="24dp"
android:layout_height="24dp"
android:src="@drawable/import_script" />
<TextView
android:id="@+id/text_import"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="8dp"
android:text="Import"
android:textColor="@color/white"
android:textSize="18sp"
android:textStyle="bold" />
</LinearLayout>
</androidx.constraintlayout.widget.ConstraintLayout>

View File

@ -48,7 +48,9 @@
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:src="@drawable/script_option"
android:visibility="gone"
android:paddingEnd="10dp"
android:paddingStart="10dp"
android:visibility="visible"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toTopOf="parent" />

View File

@ -14,7 +14,7 @@
android:background="?attr/selectableItemBackground"
android:padding="12dp"
android:text="Rename"
android:textColor="@color/black"
android:textColor="@color/white"
android:textSize="16sp" />
<TextView
@ -24,7 +24,7 @@
android:background="?attr/selectableItemBackground"
android:padding="12dp"
android:text="Duplicate"
android:textColor="@color/black"
android:textColor="@color/white"
android:textSize="16sp" />
<TextView
@ -34,7 +34,7 @@
android:background="?attr/selectableItemBackground"
android:padding="12dp"
android:text="Delete"
android:textColor="@color/black"
android:textColor="@color/white"
android:textSize="16sp" />
<TextView
@ -44,7 +44,7 @@
android:background="?attr/selectableItemBackground"
android:padding="12dp"
android:text="Export"
android:textColor="@color/black"
android:textColor="@color/white"
android:textSize="16sp" />
</LinearLayout>