功能添加

This commit is contained in:
lihongwei 2025-06-27 17:59:56 +08:00
parent 452f1d6377
commit 27727eaff0
26 changed files with 1399 additions and 371 deletions

View File

@ -50,4 +50,6 @@ dependencies {
implementation ("androidx.room:room-runtime:2.7.1")
annotationProcessor ("androidx.room:room-compiler:2.7.1")
implementation("com.google.code.gson:gson:2.12.1")
}

View File

@ -13,6 +13,7 @@ public class MyApplication extends Application {
super.onCreate();
application = this;
}
public static Context getContext() {

View File

@ -1,6 +1,7 @@
package com.auto.clicker.autoclicker.room.repository;
import android.content.Context;
import android.util.Log;
import com.auto.clicker.autoclicker.data.Event;
import com.auto.clicker.autoclicker.data.EventWrapper;
@ -9,34 +10,26 @@ import com.auto.clicker.autoclicker.data.SlideEvent;
import com.auto.clicker.autoclicker.room.AppDatabase;
import com.auto.clicker.autoclicker.room.SolutionWithEventsAndPoints;
import com.auto.clicker.autoclicker.room.dao.EventEntityDao;
import com.auto.clicker.autoclicker.room.dao.TouchPointDao;
import com.auto.clicker.autoclicker.room.dao.SolutionDao;
import com.auto.clicker.autoclicker.room.dao.TouchPointDao;
import com.auto.clicker.autoclicker.room.entity.EventEntity;
import com.auto.clicker.autoclicker.room.entity.TouchPoint;
import com.auto.clicker.autoclicker.room.entity.Solution;
import com.auto.clicker.autoclicker.room.entity.TouchPoint;
import java.util.List;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import android.content.Context;
import java.util.List;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import android.util.Log; // Add Log import for debugging
public class EventRepository {
private final SolutionDao solutionDao;
private final TouchPointDao touchPointDao; // Renamed from PointDao
private final TouchPointDao touchPointDao;
private final EventEntityDao eventEntityDao;
private final ExecutorService databaseWriteExecutor;
public EventRepository(Context context) {
AppDatabase db = AppDatabase.getInstance(context); // Updated to getInstance
this.solutionDao = db.solutionDao();
this.touchPointDao = db.pointDao(); // Assuming db.pointDao() returns TouchPointDao now
this.touchPointDao = db.pointDao();
this.eventEntityDao = db.eventEntityDao();
this.databaseWriteExecutor = Executors.newFixedThreadPool(2);
}
@ -147,20 +140,36 @@ public class EventRepository {
});
}
// --- 删除操作 ---
public void deleteSolution(Solution solution, RepositoryCallback<Integer> callback) {
databaseWriteExecutor.execute(() -> {
try {
int rowsAffected = solutionDao.deleteSolution(solution);
if (callback != null) callback.onComplete(rowsAffected);
} catch (Exception e) {
Log.e("EventRepository", "Error deleting solution: " + e.getMessage(), e);
if (callback != null) callback.onError(e);
}
});
// public void deleteSolution(Solution solution, RepositoryCallback<Integer> callback) {
// databaseWriteExecutor.execute(() -> {
// try {
// int rowsAffected = solutionDao.deleteSolution(solution);
// if (callback != null) callback.onComplete(rowsAffected);
// } catch (Exception e) {
// Log.e("EventRepository", "Error deleting solution: " + e.getMessage(), e);
// if (callback != null) callback.onError(e);
// }
// });
// }
public void updateSolutionName(long solutionId, String newName, RepositoryCallback<Void> callback) {
}
public void deleteSolution(long solutionId, RepositoryCallback<Void> callback) {
}
// 用于复制方案需要保存一个新的方案以及其关联的事件和点
public void saveSolutionWithEventsAndPoints(Solution solution, List<SolutionWithEventsAndPoints.EventWithPoints> eventsWithPoints, RepositoryCallback<Void> callback) {
}
// 获取某个方案及其所有事件和点的数据
public void getSolutionWithEventsAndPoints(String solutionId, RepositoryCallback<SolutionWithEventsAndPoints> callback) {
}
// 假设的回调接口可以根据需要自定义
public interface RepositoryCallback<T> {
void onComplete(T result);

View File

@ -48,6 +48,11 @@ public class AutoClickService extends AccessibilityService {
private int slideDuration = 500;
private int currentEventIndex = 0;
private int loopCount = 1;
private long loopTimeMillis = 0;
private long startTimeMillis;
private int currentLoop = 0;
@Override
public void onCreate() {
super.onCreate();
@ -122,6 +127,46 @@ public class AutoClickService extends AccessibilityService {
logDebug("设置滑动时长: " + duration + "ms");
}
/**
* 设置事件队列的循环次数
*
* @param count 循环次数设置为0表示无限循环设置为1表示不循环执行一次
*/
public void setLoopCount(int count) {
if (count < 0) {
Log.w(TAG, "循环次数不能为负数,已设置为默认值 1");
this.loopCount = 1;
} else {
this.loopCount = count;
}
logDebug("设置循环次数: " + loopCount + "");
// 如果设置了循环次数则重置循环时间为0确保两种模式不冲突
if (this.loopCount > 0) {
this.loopTimeMillis = 0;
logDebug("已重置循环时间为 0ms");
}
}
/**
* 设置事件队列的循环总时间
*
* @param timeMillis 循环总时间单位毫秒设置为0表示不按时间循环
*/
public void setLoopTime(long timeMillis) {
if (timeMillis < 0) {
Log.w(TAG, "循环时间不能为负数,已设置为默认值 0ms");
this.loopTimeMillis = 0;
} else {
this.loopTimeMillis = timeMillis;
}
logDebug("设置循环时间: " + loopTimeMillis + "ms");
// 如果设置了循环时间则重置循环次数为0确保两种模式不冲突
if (this.loopTimeMillis > 0) {
this.loopCount = 0;
logDebug("已重置循环次数为 0");
}
}
public void startClicking() {
if (!isRunning) {
if (runtimeEvents.isEmpty()) {
@ -131,10 +176,22 @@ public class AutoClickService extends AccessibilityService {
isRunning = true;
currentEventIndex = 0;
currentLoop = 0; // 重置循环计数
startTimeMillis = System.currentTimeMillis(); // 记录开始时间
EventViewBinder.setAllTouchPointsDraggable(false);
logDebug("开始执行事件队列 - 共 " + runtimeEvents.size() + " 个事件");
// 根据循环设置调整日志输出
String loopInfo;
if (loopCount > 0) {
loopInfo = "循环 " + (loopCount == 1 ? "1 次" : loopCount + "");
} else if (loopTimeMillis > 0) {
loopInfo = "循环 " + (loopTimeMillis / 1000) + "";
} else {
loopInfo = "无限循环"; // 如果两者都为0
}
logDebug("开始执行事件队列 - 共 " + runtimeEvents.size() + " 个事件. " + loopInfo);
executeEventsByType();
}
}
@ -146,6 +203,11 @@ public class AutoClickService extends AccessibilityService {
EventViewBinder.setAllTouchPointsDraggable(true);
// 重置循环状态
currentEventIndex = 0; // 停止时重置事件索引
currentLoop = 0; // 停止时重置循环计数
startTimeMillis = 0; // 停止时重置开始时间
logDebug("停止执行事件");
}
}
@ -155,15 +217,52 @@ public class AutoClickService extends AccessibilityService {
}
private void executeEventsByType() {
if (!isRunning || runtimeEvents.isEmpty()) {
logDebug("跳过执行:服务未运行或事件队列为空");
if (!isRunning) {
logDebug("跳过执行:服务未运行");
return;
}
if (runtimeEvents.isEmpty()) {
logDebug("跳过执行:事件队列为空");
stopClicking(); // 如果事件队列为空则停止服务
return;
}
// 在每一轮循环开始前排序确保顺序正确
runtimeEvents.sort(Comparator.comparingInt(EventWrapper::getOrder));
// 检查是否完成了一轮事件列表
if (currentEventIndex >= runtimeEvents.size()) {
currentEventIndex = 0;
currentEventIndex = 0; // 重置事件索引准备开始新一轮循环
currentLoop++; // 完成一轮增加循环计数
logDebug("完成第 " + currentLoop + " 轮事件执行。");
// 判断是否继续循环
boolean continueLoop = false;
if (loopCount > 0) { // 按次数循环模式
if (currentLoop < loopCount) {
continueLoop = true;
logDebug("继续第 " + (currentLoop + 1) + " 轮循环 (共 " + loopCount + " 轮)");
} else {
logDebug("已达到设定的循环次数 (" + loopCount + " 次),停止循环。");
}
} else if (loopTimeMillis > 0) { // 按时间循环模式
long elapsedTime = System.currentTimeMillis() - startTimeMillis;
if (elapsedTime < loopTimeMillis) {
continueLoop = true;
logDebug("继续循环 (已用时 " + (elapsedTime / 1000) + "s, 总时长 " + (loopTimeMillis / 1000) + "s)");
} else {
logDebug("已达到设定的循环时间 (" + (loopTimeMillis / 1000) + "s),停止循环。");
}
} else { // 无限循环模式 (loopCount == 0 && loopTimeMillis == 0)
continueLoop = true;
logDebug("无限循环模式,继续下一轮循环。");
}
if (!continueLoop) {
stopClicking(); // 不再继续循环则停止服务
return;
}
}
EventWrapper wrapper = runtimeEvents.get(currentEventIndex);
@ -175,8 +274,8 @@ public class AutoClickService extends AccessibilityService {
performSlide((SlideEvent) event);
} else {
logDebug("未知或不匹配的事件类型,跳过执行: " + wrapper.getType());
currentEventIndex++;
handler.postDelayed(this::executeEventsByType, clickInterval);
currentEventIndex++; // 跳过当前事件
handler.postDelayed(this::executeEventsByType, clickInterval); // 立即尝试下一个事件
}
}
@ -206,7 +305,8 @@ public class AutoClickService extends AccessibilityService {
logDebug("点击完成");
if (isRunning) {
int feedbackIndex = currentEventIndex;
currentEventIndex++;
currentEventIndex++; // 事件索引递增
// 延迟执行下一个事件循环逻辑在executeEventsByType中处理
handler.postDelayed(() -> executeEventsByType(), clickInterval);
flashTouchFeedback(feedbackIndex);
}
@ -216,7 +316,8 @@ public class AutoClickService extends AccessibilityService {
public void onCancelled(GestureDescription gestureDescription) {
Log.e(TAG, "点击被取消");
if (isRunning) {
currentEventIndex++;
currentEventIndex++; // 事件索引递增
// 延迟执行下一个事件
handler.postDelayed(() -> executeEventsByType(), clickInterval + 300);
}
}
@ -265,12 +366,12 @@ public class AutoClickService extends AccessibilityService {
logDebug("滑动完成");
if (isRunning) {
int feedbackIndex = currentEventIndex;
currentEventIndex++;
currentEventIndex++; // 事件索引递增
// 延迟执行下一个事件
handler.postDelayed(() -> executeEventsByType(), clickInterval);
flashTouchFeedback(feedbackIndex);
}
logDebug("--- onCompleted: 下一个事件将是 index " + currentEventIndex + " ---");
// 再次打印确认 slideEvent.getStartPoint() getEndPoint() 是否被意外修改
logDebug("onCompleted 后SlideEvent 起点: (" + slideEvent.getStartPoint().getX() + ", " + slideEvent.getStartPoint().getY() + ")");
logDebug("onCompleted 后SlideEvent 终点: (" + slideEvent.getEndPoint().getX() + ", " + slideEvent.getEndPoint().getY() + ")");
}
@ -279,11 +380,11 @@ public class AutoClickService extends AccessibilityService {
public void onCancelled(GestureDescription gestureDescription) {
Log.e(TAG, "滑动被取消");
if (isRunning) {
currentEventIndex++;
currentEventIndex++; // 事件索引递增
// 延迟执行下一个事件
handler.postDelayed(() -> executeEventsByType(), clickInterval + 300);
}
logDebug("--- onCancelled: 下一个事件将是 index " + currentEventIndex + " ---");
// 再次打印确认 slideEvent.getStartPoint() getEndPoint() 是否被意外修改
logDebug("onCancelled 后SlideEvent 起点: (" + slideEvent.getStartPoint().getX() + ", " + slideEvent.getStartPoint().getY() + ")");
logDebug("onCancelled 后SlideEvent 终点: (" + slideEvent.getEndPoint().getX() + ", " + slideEvent.getEndPoint().getY() + ")");
}

View File

@ -20,6 +20,7 @@ import android.widget.VideoView;
import androidx.activity.EdgeToEdge;
import androidx.activity.result.ActivityResultLauncher;
import androidx.activity.result.contract.ActivityResultContracts;
import androidx.annotation.NonNull;
import androidx.appcompat.app.AppCompatActivity;
import androidx.core.content.ContextCompat;
import androidx.core.graphics.Insets;
@ -57,12 +58,14 @@ public class MainActivity extends AppCompatActivity {
private int isFloatingShown;
private int selectedFloatingMode = FLOATING_SINGLE;
public static final String EXTRA_FLOATING_MODE = "extra_floating_mode";
private final BroadcastReceiver floatingReceiver = new BroadcastReceiver() {
@Override
public void onReceive(Context context, Intent intent) {
isFloatingShown = intent.getIntExtra("isShown", FLOATING_NONE);
logDebug("接收到浮窗状态广播: isShown = " + isFloatingShown);
if (isFloatingShown == FLOATING_NONE) {
} else {
selectedFloatingMode = isFloatingShown;
@ -160,6 +163,29 @@ public class MainActivity extends AppCompatActivity {
logDebug("选择多点模式. selectedFloatingMode=" + selectedFloatingMode);
});
handleIncomingIntent(getIntent());
}
@Override
protected void onNewIntent(@NonNull Intent intent) {
super.onNewIntent(intent);
// 如果 MainActivity 已经在运行并且再次通过 Intent 启动会调用 onNewIntent
setIntent(intent); // 更新当前的 Intent
handleIncomingIntent(intent);
}
private void handleIncomingIntent(Intent intent) {
int mode = intent.getIntExtra(EXTRA_FLOATING_MODE, FLOATING_NONE);
Log.d(TAG, "mode: " + mode);
if (mode != FLOATING_NONE) {
if (mode == FLOATING_SINGLE) {
binding.single.performClick();
binding.animatedView.performClick();
} else if (mode == FLOATING_MULTI) {
binding.multi.performClick();
binding.animatedView.performClick();
}
}
}
private void checkPermissions() {
@ -173,7 +199,7 @@ public class MainActivity extends AppCompatActivity {
logDebug("无障碍服务未启用,请求权限");
showPermissionRequest(Settings.ACTION_ACCESSIBILITY_SETTINGS, "请启用无障碍服务以便应用正常工作", "无法打开无障碍设置");
setStartButtonEnabled(false);
return;
}
@ -182,7 +208,7 @@ public class MainActivity extends AppCompatActivity {
Intent intent = new Intent(Settings.ACTION_MANAGE_OVERLAY_PERMISSION, Uri.parse("package:" + getPackageName()));
showPermissionRequest(intent, "请授予悬浮窗权限,否则无法显示浮窗", "无法打开悬浮窗设置");
setStartButtonEnabled(false);
return;
}
@ -192,13 +218,13 @@ public class MainActivity extends AppCompatActivity {
intent.setData(Uri.parse("package:" + getPackageName()));
showPermissionRequest(intent, "请禁用电池优化,以确保应用在后台稳定运行", "无法打开电池优化设置");
setStartButtonEnabled(false);
return;
}
logDebug("所有权限均已授予");
setStartButtonEnabled(true);
}
private void showPermissionRequest(String action, String toastMessage, String errorMessage) {
@ -253,7 +279,7 @@ public class MainActivity extends AppCompatActivity {
logDebug("同步服务状态: 当前浮窗模式 = " + currentFloatingMode);
isFloatingShown = currentFloatingMode;
PrefUtils.setFloatingShown(this, isFloatingShown);
updateSelectionButtons();
}
@ -286,14 +312,14 @@ public class MainActivity extends AppCompatActivity {
}
lastClickTime = currentTime;
logDebug("点击启动/隐藏浮窗按钮. 当前 isFloatingShown=" + isFloatingShown + ", selectedFloatingMode=" + selectedFloatingMode);
if (!isAccessibilityServiceEnabled() || !Settings.canDrawOverlays(this) || !isIgnoringBatteryOptimizations()) {
Toast.makeText(this, "请授予所有必要权限", Toast.LENGTH_LONG).show();
checkPermissions();
logDebug("权限不足,无法启动浮窗,重新检查权限");
return;
}
if (isFloatingShown != FLOATING_NONE && isFloatingShown != selectedFloatingMode) {
Toast.makeText(this, "已显示其他悬浮窗,请先关闭", Toast.LENGTH_SHORT).show();
logDebug("已显示其他模式的浮窗,请先关闭");

View File

@ -1,19 +1,48 @@
package com.auto.clicker.autoclicker.ui.activity.menu;
import android.app.AlertDialog;
import android.content.Intent;
import android.net.Uri;
import android.os.Bundle;
import android.os.Handler;
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.appcompat.app.AppCompatActivity;
import androidx.core.graphics.Insets;
import androidx.core.view.ViewCompat;
import androidx.core.view.WindowInsetsCompat;
import androidx.recyclerview.widget.LinearLayoutManager;
import com.auto.clicker.autoclicker.R;
import com.auto.clicker.autoclicker.databinding.ActivityFeedbackBinding;
import com.auto.clicker.autoclicker.databinding.ActivityScriptsBinding;
import com.auto.clicker.autoclicker.room.SolutionWithEventsAndPoints;
import com.auto.clicker.autoclicker.room.entity.EventEntity;
import com.auto.clicker.autoclicker.room.entity.Solution;
import com.auto.clicker.autoclicker.room.entity.TouchPoint;
import com.auto.clicker.autoclicker.room.repository.EventRepository;
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 com.google.gson.Gson;
import com.google.gson.GsonBuilder;
public class ScriptsActivity extends AppCompatActivity {
import java.io.OutputStream;
import java.util.ArrayList;
import java.util.List;
import java.util.UUID;
public class ScriptsActivity extends AppCompatActivity implements SolutionAdapter.OnOptionMenuClickListener {
private ActivityScriptsBinding binding;
private SolutionAdapter solutionsAdapter;
private FloatingViewManager floatingViewManager;
private static final String TAG = "ScriptsActivity";
private static final int CREATE_FILE_REQUEST_CODE = 1; // 用于导出文件的请求码
@Override
protected void onCreate(Bundle savedInstanceState) {
@ -27,6 +56,336 @@ public class ScriptsActivity extends AppCompatActivity {
return insets;
});
binding.scriptsRecyclerView.setLayoutManager(new LinearLayoutManager(this));
// 传入 this 作为 OnOptionMenuClickListener 的实现
solutionsAdapter = new SolutionAdapter(new ArrayList<>(), this::onSolutionSelected, this);
binding.scriptsRecyclerView.setAdapter(solutionsAdapter);
floatingViewManager = new FloatingViewManager(this);
binding.btnBack.setOnClickListener(v -> finish());
binding.create.setOnClickListener(v -> {
CreateModeSelectionDialog dialog = new CreateModeSelectionDialog();
dialog.show(getSupportFragmentManager(), "CreateModeSelectionDialog");
});
binding.importScripts.setOnClickListener(v -> {
Toast.makeText(this, "导入方案功能待实现", Toast.LENGTH_SHORT).show();
// TODO: 实现导入方案的逻辑
});
binding.fabImportScripts.setOnClickListener(v -> {
Toast.makeText(this, "右下角导入方案功能待实现", Toast.LENGTH_SHORT).show();
// TODO: 实现右下角导入方案的逻辑可能与上面的importScripts按钮功能相同或类似
});
loadSolutions();
}
@Override
protected void onResume() {
super.onResume();
loadSolutions();
}
private void loadSolutions() {
if (floatingViewManager == null) {
Log.e(TAG, "FloatingViewManager 未设置,无法加载方案列表。");
Toast.makeText(this, "加载方案列表功能不可用,请联系开发者。", Toast.LENGTH_SHORT).show();
updateLayout(true);
return;
}
floatingViewManager.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();
}
});
}
@Override
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); // 加载失败也显示无方案视图
});
}
});
}
private void updateLayout(boolean isEmpty) {
if (isEmpty) {
binding.noScriptsGroup.setVisibility(View.VISIBLE);
binding.scriptsRecyclerView.setVisibility(View.GONE);
binding.fabImportScripts.setVisibility(View.GONE);
} else {
binding.noScriptsGroup.setVisibility(View.GONE);
binding.scriptsRecyclerView.setVisibility(View.VISIBLE);
binding.fabImportScripts.setVisibility(View.VISIBLE);
}
}
private void onSolutionSelected(Solution solution) {
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();
}
}
// --- 实现 SolutionAdapter.OnOptionMenuClickListener 接口的方法 ---
@Override
public void onRenameClick(Solution solution) {
// 实现重命名逻辑
showRenameDialog(solution);
}
@Override
public void onDuplicateClick(Solution solution) {
// 实现复制逻辑
duplicateSolution(solution);
}
@Override
public void onDeleteClick(Solution solution) {
// 实现删除逻辑
showDeleteConfirmationDialog(solution);
}
@Override
public void onExportClick(Solution solution) {
// 实现导出逻辑
exportSolution(solution);
}
private void showRenameDialog(final Solution solution) {
AlertDialog.Builder builder = new AlertDialog.Builder(this);
builder.setTitle("重命名方案");
final EditText input = new EditText(this);
input.setText(solution.getSolutionName());
builder.setView(input);
builder.setPositiveButton("确定", (dialog, which) -> {
String newName = input.getText().toString().trim();
if (!newName.isEmpty() && !newName.equals(solution.getSolutionName())) {
// 调用 EventRepository 更新方案名称
floatingViewManager.getEventRepository().updateSolutionName(solution.getSolutionId(), newName, new EventRepository.RepositoryCallback<Void>() {
@Override
public void onComplete(Void result) {
new Handler(Looper.getMainLooper()).post(() -> {
Toast.makeText(ScriptsActivity.this, "方案已重命名为: " + newName, Toast.LENGTH_SHORT).show();
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 if (newName.isEmpty()) {
Toast.makeText(ScriptsActivity.this, "方案名称不能为空。", Toast.LENGTH_SHORT).show();
}
});
builder.setNegativeButton("取消", (dialog, which) -> dialog.cancel());
builder.show();
}
private void duplicateSolution(final Solution originalSolution) {
floatingViewManager.getEventRepository().getSolutionWithEventsAndPoints(originalSolution.getSolutionId(), new EventRepository.RepositoryCallback<SolutionWithEventsAndPoints>() {
@Override
public void onComplete(SolutionWithEventsAndPoints originalData) {
if (originalData != null && originalData.solution != null) {
// 创建新的 Solution 对象
Solution newSolution = new Solution();
newSolution.setSolutionId(UUID.randomUUID().toString()); // 生成新的ID
newSolution.setSolutionName(originalSolution.getSolutionName() + " (1)");
newSolution.setMode(originalSolution.getMode());
// 复制事件和点深拷贝
List<SolutionWithEventsAndPoints.EventWithPoints> newEventsWithPoints = new ArrayList<>();
if (originalData.events != null) {
for (SolutionWithEventsAndPoints.EventWithPoints originalEvent : originalData.events) {
SolutionWithEventsAndPoints.EventWithPoints newEvent = new SolutionWithEventsAndPoints.EventWithPoints();
// 复制 EventEntity
newEvent.eventEntity = new EventEntity();
newEvent.eventEntity.eventId = UUID.randomUUID().toString(); // 新的事件ID
newEvent.eventEntity.solutionId = newSolution.getSolutionId(); // 关联新的方案ID
newEvent.eventEntity.eventType = originalEvent.eventEntity.eventType;
newEvent.eventEntity.orderInSolution = originalEvent.eventEntity.orderInSolution;
// 复制 TouchPoint
if (originalEvent.clickTouchPoint != null) {
newEvent.clickTouchPoint = new TouchPoint();
newEvent.clickTouchPoint.pointId = UUID.randomUUID().toString(); // 新的点ID
newEvent.clickTouchPoint.eventId = newEvent.eventEntity.eventId; // 关联新的事件ID
newEvent.clickTouchPoint.x = originalEvent.clickTouchPoint.x;
newEvent.clickTouchPoint.y = originalEvent.clickTouchPoint.y;
newEvent.clickTouchPoint.pointOrder = originalEvent.clickTouchPoint.pointOrder;
}
if (originalEvent.slideStartTouchPoint != null) {
newEvent.slideStartTouchPoint = new TouchPoint();
newEvent.slideStartTouchPoint.pointId = UUID.randomUUID().toString(); // 新的点ID
newEvent.slideStartTouchPoint.eventId = newEvent.eventEntity.eventId; // 关联新的事件ID
newEvent.slideStartTouchPoint.x = originalEvent.slideStartTouchPoint.x;
newEvent.slideStartTouchPoint.y = originalEvent.slideStartTouchPoint.y;
newEvent.slideStartTouchPoint.pointOrder = originalEvent.slideStartTouchPoint.pointOrder;
}
if (originalEvent.slideEndTouchPoint != null) {
newEvent.slideEndTouchPoint = new TouchPoint();
newEvent.slideEndTouchPoint.pointId = UUID.randomUUID().toString(); // 新的点ID
newEvent.slideEndTouchPoint.eventId = newEvent.eventEntity.eventId; // 关联新的事件ID
newEvent.slideEndTouchPoint.x = originalEvent.slideEndTouchPoint.x;
newEvent.slideEndTouchPoint.y = originalEvent.slideEndTouchPoint.y;
newEvent.slideEndTouchPoint.pointOrder = originalEvent.slideEndTouchPoint.pointOrder;
}
newEventsWithPoints.add(newEvent);
}
}
// 保存新的方案及其事件和点
floatingViewManager.getEventRepository().saveSolutionWithEventsAndPoints(newSolution, newEventsWithPoints, new EventRepository.RepositoryCallback<Void>() {
@Override
public void onComplete(Void result) {
new Handler(Looper.getMainLooper()).post(() -> {
Toast.makeText(ScriptsActivity.this, "方案 '" + originalSolution.getSolutionName() + "' 已复制为 '" + newSolution.getSolutionName() + "'", Toast.LENGTH_SHORT).show();
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()
);
}
}
@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();
});
}
});
}
private void showDeleteConfirmationDialog(final Solution solution) {
new AlertDialog.Builder(this)
.setTitle("删除方案")
.setMessage("确定要删除方案 '" + solution.getSolutionName() + "' 吗?")
.setPositiveButton("删除", (dialog, which) -> deleteSolution(solution))
.setNegativeButton("取消", null)
.show();
}
private void deleteSolution(final Solution solution) {
floatingViewManager.getEventRepository().deleteSolution(solution.getSolutionId(), new EventRepository.RepositoryCallback<Void>() {
@Override
public void onComplete(Void result) {
new Handler(Looper.getMainLooper()).post(() -> {
Toast.makeText(ScriptsActivity.this, "方案 '" + solution.getSolutionName() + "' 已删除。", Toast.LENGTH_SHORT).show();
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();
});
}
});
}
private void exportSolution(final Solution solution) {
floatingViewManager.getEventRepository().getSolutionWithEventsAndPoints(solution.getSolutionId(), new EventRepository.RepositoryCallback<SolutionWithEventsAndPoints>() {
@Override
public void onComplete(SolutionWithEventsAndPoints solutionData) {
if (solutionData != null) {
new Handler(Looper.getMainLooper()).post(() -> {
Gson gson = new GsonBuilder().setPrettyPrinting().create();
String jsonString = gson.toJson(solutionData);
Intent intent = new Intent(Intent.ACTION_CREATE_DOCUMENT);
intent.addCategory(Intent.CATEGORY_OPENABLE);
intent.setType("application/json"); // 导出为 JSON 文件
intent.putExtra(Intent.EXTRA_TITLE, solution.getSolutionName() + ".json");
startActivityForResult(intent, CREATE_FILE_REQUEST_CODE);
// JSON 字符串保存到临时变量或直接传递
// onActivityResult 中处理保存
tempExportJson = jsonString; // 临时保存待导出的JSON数据
});
} else {
new Handler(Looper.getMainLooper()).post(() ->
Toast.makeText(ScriptsActivity.this, "获取方案数据失败,无法导出。", Toast.LENGTH_SHORT).show()
);
}
}
@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();
});
}
});
}
// 临时变量用于存储待导出的JSON数据
private String tempExportJson;
@Override
protected void onActivityResult(int requestCode, int resultCode, Intent data) {
super.onActivityResult(requestCode, resultCode, data);
if (requestCode == CREATE_FILE_REQUEST_CODE && resultCode == RESULT_OK) {
if (data != null && data.getData() != null) {
Uri uri = data.getData();
if (tempExportJson != null) {
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; // 清除临时数据
}
} else {
Toast.makeText(this, "没有可导出的数据。", Toast.LENGTH_SHORT).show();
}
}
}
}
}

View File

@ -1,10 +1,13 @@
package com.auto.clicker.autoclicker.ui.adapter.floating;
import android.content.Context;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.ImageView;
import android.widget.PopupWindow;
import android.widget.TextView;
import androidx.annotation.NonNull;
import androidx.recyclerview.widget.RecyclerView;
@ -13,18 +16,32 @@ import com.auto.clicker.autoclicker.room.entity.Solution;
import java.util.List;
public class SolutionAdapter extends RecyclerView.Adapter<SolutionAdapter.SolutionViewHolder> {
private List<Solution> solutionList;
private OnSolutionClickListener listener;
private OnOptionMenuClickListener optionMenuListener;
// 新增接口定义
public interface OnOptionMenuClickListener {
void onRenameClick(Solution solution);
void onDuplicateClick(Solution solution);
void onDeleteClick(Solution solution);
void onExportClick(Solution solution);
}
public interface OnSolutionClickListener {
void onSolutionClick(Solution solution);
}
public SolutionAdapter(List<Solution> solutionList, OnSolutionClickListener listener) {
public SolutionAdapter(List<Solution> solutionList, OnSolutionClickListener listener, OnOptionMenuClickListener optionMenuListener) {
this.solutionList = solutionList;
this.listener = listener;
this.optionMenuListener = optionMenuListener; // 初始化
}
@NonNull
@ -32,25 +49,31 @@ public class SolutionAdapter extends RecyclerView.Adapter<SolutionAdapter.Soluti
public SolutionViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
View view = LayoutInflater.from(parent.getContext())
.inflate(R.layout.item_solution, parent, false);
return new SolutionViewHolder(view);
return new SolutionViewHolder(view, parent.getContext()); // 传递 Context
}
@Override
public void onBindViewHolder(@NonNull SolutionViewHolder holder, int position) {
Solution solution = solutionList.get(position);
holder.solutionNameTextView.setText(solution.getSolutionName());
if(solution.getMode() == 1){
holder.solutionModeNameTextView.setText("1-point Mode");
}else {
holder.solutionModeNameTextView.setText("Multi-Point Mode");
holder.name.setText(solution.getSolutionName());
if (solution.getMode() == 1) {
holder.mode.setText("1-point Mode");
} else {
holder.mode.setText("Multi-Point Mode");
}
holder.solutionPlayImageView.setOnClickListener(v -> {
holder.play.setOnClickListener(v -> {
if (listener != null) {
listener.onSolutionClick(solution);
}
});
// option 按钮设置点击监听器
holder.option.setOnClickListener(v -> {
if (optionMenuListener != null) {
holder.showOptionMenu(solution, optionMenuListener); // 调用方法显示菜单
}
});
}
@Override
@ -67,15 +90,62 @@ public class SolutionAdapter extends RecyclerView.Adapter<SolutionAdapter.Soluti
}
static class SolutionViewHolder extends RecyclerView.ViewHolder {
TextView solutionNameTextView;
TextView solutionModeNameTextView;
ImageView solutionPlayImageView;
TextView name;
TextView mode;
ImageView play;
ImageView option;
Context context; // 保存 Context
SolutionViewHolder(View itemView) {
SolutionViewHolder(View itemView, Context context) {
super(itemView);
solutionNameTextView = itemView.findViewById(R.id.textViewTopLeft);
solutionModeNameTextView = itemView.findViewById(R.id.textViewBottomLeft);
solutionPlayImageView = itemView.findViewById(R.id.imageViewRight);
this.context = context; // 初始化 Context
name = itemView.findViewById(R.id.name);
mode = itemView.findViewById(R.id.mode);
play = itemView.findViewById(R.id.play);
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允许点击外部关闭
);
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(); // 关闭弹窗
});
tvDuplicate.setOnClickListener(v -> {
menuListener.onDuplicateClick(solution);
popupWindow.dismiss();
});
tvDelete.setOnClickListener(v -> {
menuListener.onDeleteClick(solution);
popupWindow.dismiss();
});
tvExport.setOnClickListener(v -> {
menuListener.onExportClick(solution);
popupWindow.dismiss();
});
// 显示 PopupWindow锚定在 option 按钮下方
popupWindow.showAsDropDown(option);
}
}
}
}

View File

@ -1,47 +0,0 @@
package com.auto.clicker.autoclicker.ui.adapter.script;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import androidx.annotation.NonNull;
import androidx.recyclerview.widget.RecyclerView;
import com.auto.clicker.autoclicker.R;
import com.auto.clicker.autoclicker.room.entity.Solution;
import java.util.List;
public class ScriptListAdapter extends RecyclerView.Adapter<ScriptListAdapter.ViewHolder> {
private List<Solution> solutionList;
public ScriptListAdapter(List<Solution> solutionList) {
this.solutionList = solutionList;
}
@NonNull
@Override
public ViewHolder onCreateViewHolder(@NonNull ViewGroup parentGroup, int viewType) {
LayoutInflater inflater = LayoutInflater.from(parentGroup.getContext());
View itemLayout = inflater.inflate(R.layout.item_script, parentGroup, false);
return new ViewHolder(itemLayout);
}
@Override
public void onBindViewHolder(@NonNull ViewHolder itemHolder, int pos) {
}
@Override
public int getItemCount() {
return solutionList.size();
}
static class ViewHolder extends RecyclerView.ViewHolder {
public ViewHolder(@NonNull View itemView) {
super(itemView);
}
}
}

View File

@ -0,0 +1,101 @@
package com.auto.clicker.autoclicker.ui.dialog;
import android.content.Intent;
import android.os.Bundle;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.Button;
import android.widget.ImageView;
import android.widget.Toast;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.constraintlayout.widget.ConstraintLayout;
import com.auto.clicker.autoclicker.R;
import com.auto.clicker.autoclicker.ui.activity.main.MainActivity;
import com.google.android.material.bottomsheet.BottomSheetDialogFragment;
public class CreateModeSelectionDialog extends BottomSheetDialogFragment {
public static final String EXTRA_FLOATING_MODE = "extra_floating_mode";
public static final int FLOATING_SINGLE = 1;
public static final int FLOATING_MULTI = 2;
private int selectedMode = FLOATING_SINGLE;
private ConstraintLayout layoutSingleMode;
private ConstraintLayout layoutMultiMode;
private ImageView iconSingle;
private ImageView iconMulti;
public CreateModeSelectionDialog() {
}
@Nullable
@Override
public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) {
View view = inflater.inflate(R.layout.dialog_create_mode_selection, container, false);
ImageView btnClose = view.findViewById(R.id.btn_close_dialog);
layoutSingleMode = view.findViewById(R.id.layout_single_mode);
layoutMultiMode = view.findViewById(R.id.layout_multi_mode);
Button btnStartCreate = view.findViewById(R.id.btn_start_create);
iconSingle = view.findViewById(R.id.icon_single);
iconMulti = view.findViewById(R.id.icon_multi);
btnClose.setOnClickListener(v -> dismiss());
layoutSingleMode.setOnClickListener(v -> {
setSelectedMode(FLOATING_SINGLE);
});
layoutMultiMode.setOnClickListener(v -> {
setSelectedMode(FLOATING_MULTI);
});
btnStartCreate.setOnClickListener(v -> {
if (selectedMode == 0) {
Toast.makeText(getContext(), "请选择模式", Toast.LENGTH_SHORT).show();
return;
}
navigateToMainActivity(selectedMode);
});
updateModeSelectionUI();
return view;
}
private void setSelectedMode(int mode) {
if (this.selectedMode != mode) {
this.selectedMode = mode;
updateModeSelectionUI();
}
}
private void updateModeSelectionUI() {
if (selectedMode == FLOATING_SINGLE) {
layoutSingleMode.setSelected(true);
iconSingle.setImageResource(R.drawable.ic_circle_filled_check);
layoutMultiMode.setSelected(false);
iconMulti.setImageResource(R.drawable.ic_circle_hollow);
} else if (selectedMode == FLOATING_MULTI) {
layoutMultiMode.setSelected(true);
iconMulti.setImageResource(R.drawable.ic_circle_filled_check);
layoutSingleMode.setSelected(false);
iconSingle.setImageResource(R.drawable.ic_circle_hollow);
}
}
private void navigateToMainActivity(int mode) {
Intent intent = new Intent(getActivity(), MainActivity.class);
intent.putExtra(EXTRA_FLOATING_MODE, mode);
startActivity(intent);
dismiss();
}
}

View File

@ -1,66 +0,0 @@
package com.auto.clicker.autoclicker.ui.fragment.floating;
import android.os.Bundle;
import androidx.fragment.app.Fragment;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import com.auto.clicker.autoclicker.R;
/**
* A simple {@link Fragment} subclass.
* Use the {@link LoadScriptFragment#newInstance} factory method to
* create an instance of this fragment.
*/
public class LoadScriptFragment extends Fragment {
// TODO: Rename parameter arguments, choose names that match
// the fragment initialization parameters, e.g. ARG_ITEM_NUMBER
private static final String ARG_PARAM1 = "param1";
private static final String ARG_PARAM2 = "param2";
// TODO: Rename and change types of parameters
private String mParam1;
private String mParam2;
public LoadScriptFragment() {
// Required empty public constructor
}
/**
* Use this factory method to create a new instance of
* this fragment using the provided parameters.
*
* @param param1 Parameter 1.
* @param param2 Parameter 2.
* @return A new instance of fragment LoadScriptFragment.
*/
// TODO: Rename and change types and number of parameters
public static LoadScriptFragment newInstance(String param1, String param2) {
LoadScriptFragment fragment = new LoadScriptFragment();
Bundle args = new Bundle();
args.putString(ARG_PARAM1, param1);
args.putString(ARG_PARAM2, param2);
fragment.setArguments(args);
return fragment;
}
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
if (getArguments() != null) {
mParam1 = getArguments().getString(ARG_PARAM1);
mParam2 = getArguments().getString(ARG_PARAM2);
}
}
@Override
public View onCreateView(LayoutInflater inflater, ViewGroup container,
Bundle savedInstanceState) {
// Inflate the layout for this fragment
return inflater.inflate(R.layout.fragment_load_script, container, false);
}
}

View File

@ -1,66 +0,0 @@
package com.auto.clicker.autoclicker.ui.fragment.floating;
import android.os.Bundle;
import androidx.fragment.app.Fragment;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import com.auto.clicker.autoclicker.R;
/**
* A simple {@link Fragment} subclass.
* Use the {@link SaveScriptFragment#newInstance} factory method to
* create an instance of this fragment.
*/
public class SaveScriptFragment extends Fragment {
// TODO: Rename parameter arguments, choose names that match
// the fragment initialization parameters, e.g. ARG_ITEM_NUMBER
private static final String ARG_PARAM1 = "param1";
private static final String ARG_PARAM2 = "param2";
// TODO: Rename and change types of parameters
private String mParam1;
private String mParam2;
public SaveScriptFragment() {
// Required empty public constructor
}
/**
* Use this factory method to create a new instance of
* this fragment using the provided parameters.
*
* @param param1 Parameter 1.
* @param param2 Parameter 2.
* @return A new instance of fragment SaveScriptFragment.
*/
// TODO: Rename and change types and number of parameters
public static SaveScriptFragment newInstance(String param1, String param2) {
SaveScriptFragment fragment = new SaveScriptFragment();
Bundle args = new Bundle();
args.putString(ARG_PARAM1, param1);
args.putString(ARG_PARAM2, param2);
fragment.setArguments(args);
return fragment;
}
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
if (getArguments() != null) {
mParam1 = getArguments().getString(ARG_PARAM1);
mParam2 = getArguments().getString(ARG_PARAM2);
}
}
@Override
public View onCreateView(LayoutInflater inflater, ViewGroup container,
Bundle savedInstanceState) {
// Inflate the layout for this fragment
return inflater.inflate(R.layout.fragment_save_script, container, false);
}
}

View File

@ -12,7 +12,6 @@ import android.util.Log;
import android.view.ContextThemeWrapper;
import android.view.Gravity;
import android.view.LayoutInflater;
import android.view.MotionEvent;
import android.view.View;
import android.view.ViewGroup;
import android.view.WindowManager;
@ -22,6 +21,7 @@ import android.widget.PopupWindow;
import android.widget.TextView;
import android.widget.Toast;
import androidx.appcompat.widget.AppCompatButton;
import androidx.appcompat.widget.SwitchCompat;
import androidx.constraintlayout.widget.ConstraintLayout;
@ -36,8 +36,6 @@ public class FloatingSettingDialogManager {
private ConstraintLayout floatingDialogView;
private WindowManager.LayoutParams floatingDialogParams;
private FloatingViewManager floatingViewManager;
private static final int TYPE_INTERVAL = 0;
private static final int TYPE_SWIPE_DURATION = 1;
private static final int TYPE_REPEAT = 2;
@ -53,16 +51,22 @@ public class FloatingSettingDialogManager {
private TextView tvSelectedUnit;
private EditText etRepeatValue;
private TextView repeatSelectedUnit;
private AppCompatButton saveButton;
private AppCompatButton closeButton;
private String currentIntervalUnit = "";
private String currentSwipeDurationUnit = "";
private String currentRepeatUnit = "";
private String currentIntervalValue = "";
private String currentDurationValue = "";
private String currentRepeatValue = "";
private boolean isRepeatSwitchChecked = false;
public FloatingSettingDialogManager(Context context) {
this.context = context;
this.windowManager = (WindowManager) context.getSystemService(Context.WINDOW_SERVICE);
}
public void setFloatingViewManager(FloatingViewManager manager) {
this.floatingViewManager = manager;
}
public void showFloatingTabDialog() {
if (floatingDialogView != null) {
removeFloatingTabDialog();
@ -85,20 +89,46 @@ public class FloatingSettingDialogManager {
etRepeatValue = floatingDialogView.findViewById(R.id.dialog_et_repeat_value);
repeatSelectedUnit = floatingDialogView.findViewById(R.id.dialog_repeat_selected_unit);
saveButton = floatingDialogView.findViewById(R.id.save);
closeButton = floatingDialogView.findViewById(R.id.close);
intervalSelectorContainer.setOnClickListener(v -> showUnitSelection(v, TYPE_INTERVAL));
unitSelectorContainer.setOnClickListener(v -> showUnitSelection(v, TYPE_SWIPE_DURATION));
setupRepeatMode();
// --- 恢复UI状态 ---
restoreUIState();
floatingDialogView.setOnTouchListener((v, event) -> {
if (event.getAction() == MotionEvent.ACTION_OUTSIDE) {
// 如果是外部点击事件就关闭对话框
// floatingDialogView.setOnTouchListener((v, event) -> {
// if (event.getAction() == MotionEvent.ACTION_OUTSIDE) {
// // 如果是外部点击事件就关闭对话框
// saveCurrentSettings();
// removeFloatingTabDialog();
// Log.d(TAG, "检测到外部点击,浮动标签页弹窗已关闭。");
// return true; // 消费此事件
// }
// return false; // 其他事件不处理
// });
if (saveButton != null) {
saveButton.setOnClickListener(v -> {
saveCurrentSettings();
applySettingsToService();
removeFloatingTabDialog();
Log.d(TAG, "检测到外部点击,浮动标签页弹窗已关闭。");
return true; // 消费此事件
}
return false; // 其他事件不处理
});
showToastOnUi(context, "设置已保存!");
});
} else {
Log.e(TAG, "Save button not found!");
}
if (closeButton != null) {
closeButton.setOnClickListener(v -> {
removeFloatingTabDialog();
showToastOnUi(context, "设置已关闭,未保存更改。");
});
} else {
Log.e(TAG, "Close button not found!");
}
try {
windowManager.addView(floatingDialogView, floatingDialogParams);
@ -128,6 +158,8 @@ public class FloatingSettingDialogManager {
tvSelectedUnit = null;
etRepeatValue = null;
repeatSelectedUnit = null;
saveButton = null;
closeButton = null;
Log.d(TAG, "浮动标签页弹窗已移除");
} catch (IllegalArgumentException e) {
@ -163,31 +195,35 @@ public class FloatingSettingDialogManager {
}
repeatSwitch.setOnCheckedChangeListener((buttonView, isChecked) -> {
if (isChecked) {
repeatInput.setVisibility(View.VISIBLE);
repeatSelectorContainer.setVisibility(View.VISIBLE);
repeatSelectedUnit.setText(context.getString(R.string.infinitely_unit));
isRepeatSwitchChecked = isChecked; // 保存开关状态
updateRepeatUI(isChecked);
});
repeatSelectorContainer.setOnClickListener(v -> showUnitSelection(v, TYPE_REPEAT));
}
private void updateRepeatUI(boolean isChecked) {
if (isChecked) {
repeatInput.setVisibility(View.VISIBLE);
repeatSelectorContainer.setVisibility(View.VISIBLE);
// 只有当单位是无限次时才禁用输入框
if (currentRepeatUnit.equals(context.getString(R.string.infinitely_unit)) || repeatSelectedUnit.getText().toString().equals(context.getString(R.string.infinitely_unit))) {
etRepeatValue.setText("");
etRepeatValue.setEnabled(false);
etRepeatValue.setInputType(InputType.TYPE_NULL);
repeatSelectedUnit.setText(context.getString(R.string.infinitely_unit)); // 确保显示无限次
} else {
repeatInput.setVisibility(View.GONE);
repeatSelectorContainer.setVisibility(View.GONE);
etRepeatValue.setEnabled(true);
etRepeatValue.setInputType(InputType.TYPE_CLASS_NUMBER); // 默认设置为数字输入
// 恢复之前的输入值和单位
etRepeatValue.setText(currentRepeatValue);
repeatSelectedUnit.setText(currentRepeatUnit.isEmpty() ? context.getString(R.string.cycle_unit) : currentRepeatUnit);
}
});
// 根据初始状态设置可见性
if (!repeatSwitch.isChecked()) {
} else {
repeatInput.setVisibility(View.GONE);
repeatSelectorContainer.setVisibility(View.GONE);
} else {
repeatSelectedUnit.setText(context.getString(R.string.infinitely_unit));
etRepeatValue.setText("");
etRepeatValue.setEnabled(false);
etRepeatValue.setInputType(InputType.TYPE_NULL);
}
repeatSelectorContainer.setOnClickListener(v -> showUnitSelection(v, TYPE_REPEAT));
}
private void showUnitSelection(View anchorView, int type) {
@ -242,10 +278,10 @@ public class FloatingSettingDialogManager {
String unitText = context.getString(R.string.milliseconds_unit);
if (type == TYPE_INTERVAL) {
intervalSelectedUnit.setText(unitText);
showInputValue("Interval", etIntervalValue.getText().toString(), unitText);
currentIntervalUnit = unitText;
} else {
tvSelectedUnit.setText(unitText);
showInputValue("Swipe Duration", etDurationValue.getText().toString(), unitText);
currentSwipeDurationUnit = unitText;
}
popupWindow.dismiss();
});
@ -254,10 +290,10 @@ public class FloatingSettingDialogManager {
String unitText = context.getString(R.string.seconds_unit);
if (type == TYPE_INTERVAL) {
intervalSelectedUnit.setText(unitText);
showInputValue("Interval", etIntervalValue.getText().toString(), unitText);
currentIntervalUnit = unitText;
} else {
tvSelectedUnit.setText(unitText);
showInputValue("Swipe Duration", etDurationValue.getText().toString(), unitText);
currentSwipeDurationUnit = unitText;
}
popupWindow.dismiss();
});
@ -266,38 +302,41 @@ public class FloatingSettingDialogManager {
tvMin.setOnClickListener(v -> {
String unitText = context.getString(R.string.minutes_unit);
intervalSelectedUnit.setText(unitText);
currentIntervalUnit = unitText;
popupWindow.dismiss();
showInputValue("Interval", etIntervalValue.getText().toString(), unitText);
});
}
}
if (type == TYPE_REPEAT) {
tvInfinitely.setOnClickListener(v -> {
repeatSelectedUnit.setText(context.getString(R.string.infinitely_unit));
String unitText = context.getString(R.string.infinitely_unit);
repeatSelectedUnit.setText(unitText);
currentRepeatUnit = unitText;
etRepeatValue.setText("");
etRepeatValue.setEnabled(false);
etRepeatValue.setInputType(InputType.TYPE_NULL);
popupWindow.dismiss();
showInputValue("Repeat", etRepeatValue.getText().toString(), context.getString(R.string.infinitely_unit));
});
tvDuration.setOnClickListener(v -> {
repeatSelectedUnit.setText(context.getString(R.string.duration_unit));
etRepeatValue.setText("");
String unitText = context.getString(R.string.duration_unit);
repeatSelectedUnit.setText(unitText);
currentRepeatUnit = unitText;
etRepeatValue.setText(""); // 清空以便用户输入新值
etRepeatValue.setEnabled(true);
etRepeatValue.setInputType(InputType.TYPE_CLASS_NUMBER);
popupWindow.dismiss();
showInputValue("Repeat", etRepeatValue.getText().toString(), context.getString(R.string.duration_unit));
});
tvCycle.setOnClickListener(v -> {
repeatSelectedUnit.setText(context.getString(R.string.cycle_unit));
String unitText = context.getString(R.string.cycle_unit);
repeatSelectedUnit.setText(unitText);
currentRepeatUnit = unitText;
etRepeatValue.setText("");
etRepeatValue.setEnabled(true);
etRepeatValue.setInputType(InputType.TYPE_CLASS_NUMBER);
popupWindow.dismiss();
showInputValue("Repeat", etRepeatValue.getText().toString(), context.getString(R.string.cycle_unit));
});
}
@ -306,98 +345,164 @@ public class FloatingSettingDialogManager {
} else {
popupWindow.showAsDropDown(anchorView);
}
}
private void showInputValue(String type, String value, String unit) {
String message = type + " 值: " + (value.isEmpty() ? "未输入" : value) + ", 单位: " + unit;
showToastOnUi(context, message);
private void saveCurrentSettings() {
currentIntervalValue = etIntervalValue.getText().toString();
currentDurationValue = etDurationValue.getText().toString();
currentRepeatValue = etRepeatValue.getText().toString();
Log.d(TAG, "UI设置已保存到Manager: Interval=" + currentIntervalValue + " " + currentIntervalUnit +
", Duration=" + currentDurationValue + " " + currentSwipeDurationUnit +
", Repeat=" + currentRepeatValue + " " + currentRepeatUnit +
", RepeatSwitch=" + isRepeatSwitchChecked);
}
private void applySettingsToService() {
AutoClickService service = AutoClickService.getInstance();
if (service == null) {
Log.e(TAG, "AutoClickService 实例为空,无法应用设置。");
showToastOnUi(context, "自动点击服务未运行,无法应用设置。");
return;
}
switch (type) {
case "Interval":
long intervalValue = 0;
if (!value.isEmpty()) {
try {
intervalValue = Long.parseLong(value);
// 根据 unit 进行单位转换
if (unit.equals(context.getString(R.string.seconds_unit))) {
intervalValue *= 1000; // 如果单位是秒转换为毫秒
}
if (service != null) {
service.setClickInterval(intervalValue);
} else {
Log.e(TAG, "AutoClickService 实例为空,无法设置点击间隔。");
}
} catch (NumberFormatException e) {
Log.e(TAG, "点击间隔值解析错误: " + value, e);
showToastOnUi(context, "点击间隔值无效,请输入有效数字。");
}
// 应用点击间隔
long intervalValue = 0;
if (!currentIntervalValue.isEmpty()) {
try {
intervalValue = Long.parseLong(currentIntervalValue);
if (currentIntervalUnit.equals(context.getString(R.string.seconds_unit))) {
intervalValue *= 1000;
} else if (currentIntervalUnit.equals(context.getString(R.string.minutes_unit))) {
intervalValue *= 60 * 1000;
}
// 你可以在这里将 intervalValue 保存到你的数据模型或 SharedPreferences
break;
service.setClickInterval(intervalValue);
Log.d(TAG, "应用点击间隔到服务: " + intervalValue + "ms");
} catch (NumberFormatException e) {
Log.e(TAG, "点击间隔值解析错误: " + currentIntervalValue, e);
showToastOnUi(context, "点击间隔值无效,请输入有效数字。");
}
} else {
// 如果为空设置一个默认值或不设置这取决于你的业务逻辑
// service.setClickInterval(默认值);
Log.d(TAG, "点击间隔值为空,未应用到服务。");
}
case "Swipe Duration":
int slideDurationValue = 0;
if (!value.isEmpty()) {
try {
slideDurationValue = Integer.parseInt(value);
if (unit.equals(context.getString(R.string.seconds_unit))) {
slideDurationValue *= 1000; // 如果单位是秒转换为毫秒
}
if (service != null) {
service.setSlideDuration(slideDurationValue);
} else {
Log.e(TAG, "AutoClickService 实例为空,无法设置滑动时长。");
}
} catch (NumberFormatException e) {
Log.e(TAG, "滑动时长值解析错误: " + value, e);
showToastOnUi(context, "滑动时长值无效,请输入有效数字。");
}
int slideDurationValue = 0;
if (!currentDurationValue.isEmpty()) {
try {
slideDurationValue = Integer.parseInt(currentDurationValue);
if (currentSwipeDurationUnit.equals(context.getString(R.string.seconds_unit))) {
slideDurationValue *= 1000;
}
// 你可以在这里将 slideDurationValue 保存到你的数据模型或 SharedPreferences
break;
service.setSlideDuration(slideDurationValue);
Log.d(TAG, "应用滑动时长到服务: " + slideDurationValue + "ms");
} catch (NumberFormatException e) {
Log.e(TAG, "滑动时长值解析错误: " + currentDurationValue, e);
showToastOnUi(context, "滑动时长值无效,请输入有效数字。");
}
} else {
// 如果为空设置一个默认值或不设置
// service.setSlideDuration(默认值);
Log.d(TAG, "滑动时长值为空,未应用到服务。");
}
case "Repeat":
if (unit.equals(context.getString(R.string.infinitely_unit))) {
// Repeat 为无限次不需要处理输入值
// 确保 etRepeatValue 是禁用的
// 应用重复设置
if (isRepeatSwitchChecked) {
if (currentRepeatUnit.equals(context.getString(R.string.infinitely_unit))) {
service.setLoopCount(0); // 无限循环
Log.d(TAG, "应用重复模式: 无限次。");
} else if (currentRepeatUnit.equals(context.getString(R.string.duration_unit))) {
long repeatDurationMillis = 0;
if (!currentRepeatValue.isEmpty()) {
try {
repeatDurationMillis = Long.parseLong(currentRepeatValue);
// 根据你的UI单位这里可能需要进一步转换例如用户输入的是秒
// repeatDurationMillis *= 1000L;
service.setLoopTime(repeatDurationMillis);
Log.d(TAG, "应用重复时长: " + repeatDurationMillis + "ms。");
} catch (NumberFormatException e) {
Log.e(TAG, "重复时长值解析错误: " + currentRepeatValue, e);
showToastOnUi(context, "重复时长值无效,请输入有效数字。");
service.setLoopTime(0); // 错误时取消时间循环
}
} else {
service.setLoopTime(0); // 如果值为空取消时间循环
Log.d(TAG, "重复时长值为空,取消时间循环。");
}
service.setLoopCount(0); // 确保与时间循环互斥
} else if (currentRepeatUnit.equals(context.getString(R.string.cycle_unit))) {
int repeatCount = 0;
if (!currentRepeatValue.isEmpty()) {
try {
repeatCount = Integer.parseInt(currentRepeatValue);
service.setLoopCount(repeatCount);
Log.d(TAG, "应用重复次数: " + repeatCount + "次。");
} catch (NumberFormatException e) {
Log.e(TAG, "重复次数值解析错误: " + currentRepeatValue, e);
showToastOnUi(context, "重复次数值无效,请输入有效数字。");
service.setLoopCount(1); // 错误时设置为执行一次
}
} else {
service.setLoopCount(1); // 如果值为空设置为执行一次
Log.d(TAG, "重复次数值为空,设置为执行一次。");
}
service.setLoopTime(0); // 确保与次数循环互斥
}
} else {
service.setLoopCount(1);
service.setLoopTime(0);
Log.d(TAG, "重复开关关闭,服务设置为执行一次。");
}
}
private void restoreUIState() {
// 恢复点击间隔
if (!currentIntervalValue.isEmpty()) {
etIntervalValue.setText(currentIntervalValue);
}
if (!currentIntervalUnit.isEmpty()) {
intervalSelectedUnit.setText(currentIntervalUnit);
} else {
intervalSelectedUnit.setText(context.getString(R.string.milliseconds_unit)); // 默认单位
currentIntervalUnit = context.getString(R.string.milliseconds_unit);
}
// 恢复滑动时长
if (!currentDurationValue.isEmpty()) {
etDurationValue.setText(currentDurationValue);
}
if (!currentSwipeDurationUnit.isEmpty()) {
tvSelectedUnit.setText(currentSwipeDurationUnit);
} else {
tvSelectedUnit.setText(context.getString(R.string.milliseconds_unit)); // 默认单位
currentSwipeDurationUnit = context.getString(R.string.milliseconds_unit);
}
// 恢复重复设置
repeatSwitch.setChecked(isRepeatSwitchChecked);
updateRepeatUI(isRepeatSwitchChecked); // 根据开关状态更新UI
if (isRepeatSwitchChecked) {
if (currentRepeatUnit.isEmpty()) {
// 如果没有保存的单位默认为无限次
repeatSelectedUnit.setText(context.getString(R.string.infinitely_unit));
currentRepeatUnit = context.getString(R.string.infinitely_unit);
etRepeatValue.setText("");
etRepeatValue.setEnabled(false);
etRepeatValue.setInputType(InputType.TYPE_NULL);
} else {
repeatSelectedUnit.setText(currentRepeatUnit);
if (currentRepeatUnit.equals(context.getString(R.string.infinitely_unit))) {
etRepeatValue.setText("");
etRepeatValue.setEnabled(false);
etRepeatValue.setInputType(InputType.TYPE_NULL);
showToastOnUi(context, "重复模式: 无限次");
// AutoClickService 内部已是循环执行无限次意味着一直运行直到用户停止
// 如果需要更明确的无限次模式可以在 AutoClickService 中设置一个标志位
} else if (unit.equals(context.getString(R.string.duration_unit))) {
// Repeat 为时长处理时间格式的输入 (例如 "HH:mm:ss" -> 毫秒)
String durationValueStr = value.isEmpty() ? "0" : value;
// TODO: 你需要在这里添加解析时间格式的逻辑例如
// long parsedDuration = parseDurationString(durationValueStr); // 自定义方法解析时间
// 如果 AutoClickService 需要总运行时间限制你需要修改 AutoClickService 来支持
// 否则你可能需要在悬浮窗管理类中实现一个计时器当达到设定时长时调用 service.stopClicking()
etRepeatValue.setEnabled(true);
etRepeatValue.setInputType(InputType.TYPE_CLASS_NUMBER); // 这里暂时设置为数字如果需要特定时间输入需要更复杂的实现
showToastOnUi(context, "重复时长: " + durationValueStr);
} else if (unit.equals(context.getString(R.string.cycle_unit))) {
// Repeat 为次数处理整数输入
int repeatCount = 0;
if (!value.isEmpty()) {
try {
repeatCount = Integer.parseInt(value);
// TODO: 如果 AutoClickService 需要总循环次数限制你需要修改 AutoClickService 来支持
// 可以在 AutoClickService 中添加一个变量来跟踪剩余循环次数并在每次事件队列执行完毕后递减
// 当次数为 0 调用 stopClicking()
} catch (NumberFormatException e) {
Log.e(TAG, "重复次数值解析错误: " + value, e);
showToastOnUi(context, "重复次数值无效,请输入有效数字。");
}
}
} else {
etRepeatValue.setText(currentRepeatValue);
etRepeatValue.setEnabled(true);
etRepeatValue.setInputType(InputType.TYPE_CLASS_NUMBER);
showToastOnUi(context, "重复次数: " + repeatCount);
}
break;
}
}
}

View File

@ -97,7 +97,6 @@ public class FloatingViewManager {
this.floatingTabDialogManager = new FloatingTabDialogManager(context);
this.floatingSettingDialogManager = new FloatingSettingDialogManager(context);
this.floatingTabDialogManager.setFloatingViewManager(this);
this.floatingSettingDialogManager.setFloatingViewManager(this);
this.eventRepository = new EventRepository(context.getApplicationContext());
this.touchPointSize = 100;
@ -926,4 +925,8 @@ public class FloatingViewManager {
playButton.setImageResource(R.drawable.play);
}
}
public EventRepository getEventRepository() {
return eventRepository;
}
}

View File

@ -0,0 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<selector xmlns:android="http://schemas.android.com/apk/res/android">
<item android:state_selected="true" android:drawable="@drawable/bg_selected" />
<item android:drawable="@drawable/bg_rounded_rect" />
</selector>

View File

@ -0,0 +1,12 @@
<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
<item>
<shape android:shape="rectangle">
<solid android:color="@android:color/transparent" />
<corners android:radius="8dp" />
<stroke android:width="2dp" android:color="@color/blue" />
</shape>
</item>
<item android:drawable="@drawable/bg_rounded_rect"
android:left="2dp" android:top="2dp"
android:right="2dp" android:bottom="2dp" />
</layer-list>

View File

@ -0,0 +1,9 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="17dp"
android:height="17dp"
android:viewportWidth="17"
android:viewportHeight="17">
<path
android:pathData="M8.5,0C13.194,0 17,3.806 17,8.5C17,13.194 13.194,17 8.5,17C3.806,17 0,13.194 0,8.5C0,3.806 3.806,0 8.5,0ZM7.412,10.442L4.413,7.449L3,8.864L7.247,13.104L7.248,13.102L7.41,13.265L14.298,6.418L12.888,5L7.412,10.442Z"
android:fillColor="#0BC4FC"/>
</vector>

View File

@ -0,0 +1,9 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="17dp"
android:height="17dp"
android:viewportWidth="17"
android:viewportHeight="17">
<path
android:pathData="M8.5,8.5m-8.5,0a8.5,8.5 0,1 1,17 0a8.5,8.5 0,1 1,-17 0"
android:fillColor="#3A6876"/>
</vector>

View File

@ -0,0 +1,42 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="17dp"
android:height="17dp"
android:viewportWidth="17"
android:viewportHeight="17">
<path
android:pathData="M0,0h17v17h-17z"
android:fillColor="#B3B3B3"/>
<group>
<clip-path
android:pathData="M-222,-703h360v800h-360z"/>
<path
android:pathData="M-222,-703h360v800h-360z"
android:fillColor="#ffffff"/>
<path
android:pathData="M138,-703H-222V97H138V-703Z"
android:fillColor="#171717"/>
<path
android:pathData="M0,-14L81,-14A23,23 0,0 1,104 9L104,9A23,23 0,0 1,81 32L0,32A23,23 0,0 1,-23 9L-23,9A23,23 0,0 1,0 -14z"
android:fillColor="#0BC4FC"/>
<group>
<clip-path
android:pathData="M0,0h17v17h-17z"/>
<path
android:pathData="M15.637,15.619H1.36V1.361H7.699V0.003H1.02C0.459,0.003 0,0.462 0,1.022V15.958C0,16.518 0.459,16.977 1.02,16.977H15.977C16.538,16.977 16.997,16.518 16.997,15.958V9.059H15.637V15.619V15.619Z"
android:fillColor="#ffffff"/>
<path
android:pathData="M16.317,13.497C15.943,13.497 15.637,13.191 15.637,12.818V8.829C15.637,8.456 15.943,8.15 16.317,8.15C16.691,8.15 16.997,8.456 16.997,8.829V12.818C16.997,13.191 16.699,13.497 16.317,13.497ZM8.158,1.361H6.374C6,1.361 5.694,1.056 5.694,0.682C5.694,0.309 6,0.003 6.374,0.003H8.158C8.532,0.003 8.838,0.309 8.838,0.682C8.838,1.055 8.541,1.361 8.158,1.361V1.361ZM11.821,10.255H7.436C7.054,10.255 6.739,9.941 6.739,9.559C6.739,9.177 7.054,8.863 7.436,8.863H11.821C12.203,8.863 12.518,9.177 12.518,9.559C12.518,9.941 12.212,10.255 11.821,10.255Z"
android:fillColor="#ffffff"/>
<group>
<clip-path
android:pathData="M17,0H0V17H17V0Z"/>
<path
android:pathData="M6.739,9.559V5.18C6.739,4.798 7.054,4.484 7.436,4.484C7.819,4.484 8.133,4.798 8.133,5.18V9.559C8.133,9.941 7.819,10.255 7.436,10.255C7.054,10.255 6.739,9.941 6.739,9.559Z"
android:fillColor="#ffffff"/>
<path
android:pathData="M6.952,10.051L6.943,10.043C6.68,9.78 6.68,9.355 6.943,9.092L15.841,0.199C16.104,-0.064 16.529,-0.064 16.793,0.199L16.801,0.207C17.065,0.47 17.065,0.895 16.801,1.158L7.895,10.051C7.632,10.314 7.207,10.314 6.952,10.051Z"
android:fillColor="#ffffff"/>
</group>
</group>
</group>
</vector>

View File

@ -0,0 +1,45 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="41dp"
android:height="48dp"
android:viewportWidth="41"
android:viewportHeight="48">
<path
android:pathData="M33.969,22.647C33.721,22.75 33.043,22.69 32.086,21.816C30.403,20.282 25.605,20.639 24.026,20.8C23.775,20.348 23.478,19.576 23.9,19.171C24.031,19.06 24.185,18.968 24.345,18.909C25.394,18.476 27.308,18.386 27.954,18.386L29.089,18.386L25.385,9.407C25.347,9.264 25.35,8.895 26.096,8.587C26.842,8.28 27.273,8.498 27.419,8.611L29.252,13.055L29.843,12.905C32.874,12.152 37.43,11.656 38.045,12.888L40.524,18.897C40.524,18.897 40.684,19.249 40.513,19.608C40.383,19.878 40.075,20.113 39.596,20.311L33.981,22.642"
android:fillColor="#ffffff"/>
<path
android:strokeWidth="1"
android:pathData="M10.966,22.294L20.294,12.034"
android:fillColor="#00000000"
android:strokeColor="#9DB9C2"
android:strokeLineCap="round"/>
<path
android:strokeWidth="1"
android:pathData="M11.691,30.85L24.85,39.309"
android:fillColor="#00000000"
android:strokeColor="#9DB9C2"
android:strokeLineCap="round"/>
<path
android:strokeWidth="1"
android:pathData="M24.5,6.5m-6,0a6,6 0,1 1,12 0a6,6 0,1 1,-12 0"
android:fillColor="#00000000"
android:strokeColor="#ffffff"/>
<path
android:strokeWidth="1"
android:pathData="M6.5,26.5m-6,0a6,6 0,1 1,12 0a6,6 0,1 1,-12 0"
android:fillColor="#00000000"
android:strokeColor="#9DB9C2"/>
<path
android:strokeWidth="1"
android:pathData="M30.5,41.5m-6,0a6,6 0,1 1,12 0a6,6 0,1 1,-12 0"
android:fillColor="#00000000"
android:strokeColor="#436873"/>
<path
android:pathData="M24.946,9H24.295V4.905L23.287,5.374L23.098,4.905L24.393,4.324H24.946V9Z"
android:fillColor="#ffffff"/>
<path
android:pathData="M5.196,29V28.433C5.695,27.896 6.127,27.409 6.491,26.97C6.855,26.527 7.077,26.233 7.156,26.088C7.24,25.943 7.282,25.761 7.282,25.542C7.282,25.318 7.212,25.143 7.072,25.017C6.932,24.891 6.727,24.828 6.456,24.828C6.185,24.828 5.903,24.886 5.609,25.003L5.455,24.499C5.838,24.322 6.225,24.233 6.617,24.233C7.009,24.233 7.319,24.347 7.548,24.576C7.781,24.805 7.898,25.122 7.898,25.528C7.898,25.934 7.7,26.382 7.303,26.872C6.738,27.577 6.304,28.097 6.001,28.433H8.066L8.017,29H5.196Z"
android:fillColor="#9DB9C2"/>
<path
android:pathData="M30.267,41.725H29.931V41.249H30.26C30.535,41.216 30.748,41.123 30.897,40.969C31.051,40.815 31.128,40.635 31.128,40.43C31.128,40.22 31.058,40.066 30.918,39.968C30.783,39.87 30.584,39.821 30.323,39.821C30.066,39.821 29.791,39.879 29.497,39.996L29.343,39.492C29.74,39.319 30.127,39.233 30.505,39.233C30.888,39.233 31.191,39.338 31.415,39.548C31.644,39.753 31.758,40.031 31.758,40.381C31.758,40.596 31.707,40.799 31.604,40.99C31.506,41.177 31.364,41.321 31.177,41.424C31.69,41.615 31.947,42.003 31.947,42.586C31.947,43.029 31.809,43.391 31.534,43.671C31.263,43.951 30.904,44.091 30.456,44.091C30.013,44.091 29.593,43.97 29.196,43.727L29.427,43.202C29.819,43.398 30.15,43.496 30.421,43.496C30.696,43.496 30.913,43.412 31.072,43.244C31.231,43.071 31.31,42.854 31.31,42.593C31.31,42.332 31.219,42.122 31.037,41.963C30.855,41.804 30.598,41.725 30.267,41.725Z"
android:fillColor="#436773"/>
</vector>

View File

@ -0,0 +1,14 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="16dp"
android:height="20dp"
android:viewportWidth="16"
android:viewportHeight="20">
<path
android:pathData="M8.51,20C8.241,20 7.638,19.687 7.087,18.513C6.116,16.453 1.544,14.953 0.023,14.5C-0.036,13.987 -0.017,13.16 0.528,12.947C0.692,12.893 0.869,12.867 1.039,12.873C2.174,12.873 3.978,13.52 4.575,13.767L5.624,14.2V4.487C5.644,4.34 5.788,4 6.595,4C7.401,4 7.716,4.367 7.808,4.527V9.333L8.411,9.42C11.501,9.88 15.901,11.16 16,12.533V19.033C16,19.033 16.013,19.42 15.718,19.687C15.495,19.887 15.121,19.987 14.603,19.987L8.523,20"
android:fillColor="#ffffff"/>
<path
android:strokeWidth="1"
android:pathData="M6.5,6.5m-6,0a6,6 0,1 1,12 0a6,6 0,1 1,-12 0"
android:fillColor="#00000000"
android:strokeColor="#ffffff"/>
</vector>

View File

@ -30,6 +30,12 @@
app:layout_constraintStart_toEndOf="@+id/btn_back"
app:layout_constraintTop_toTopOf="@+id/btn_back" />
<androidx.constraintlayout.widget.Group
android:id="@+id/no_scripts_group"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
app:constraint_referenced_ids="no_scripts, no_scripts_text, create, import_scripts" />
<ImageView
android:id="@+id/no_scripts"
android:layout_width="wrap_content"
@ -43,6 +49,7 @@
app:layout_constraintVertical_bias="0.38" />
<TextView
android:id="@+id/no_scripts_text"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/no_scripts_text"
@ -58,11 +65,11 @@
android:id="@+id/create"
android:layout_width="223dp"
android:layout_height="48dp"
android:layout_marginTop="32dp"
android:background="@drawable/btn_border_background"
android:text="@string/create_now"
android:textAllCaps="false"
android:textColor="@color/white"
android:layout_marginTop="32dp"
android:textSize="22sp"
android:textStyle="bold"
app:layout_constraintEnd_toEndOf="parent"
@ -83,4 +90,34 @@
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/create" />
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/scripts_recycler_view"
android:layout_width="0dp"
android:layout_height="0dp"
android:layout_marginTop="16dp"
android:visibility="gone"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintHorizontal_bias="0.0"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/title_scripts"
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"
android:layout_marginEnd="24dp"
android:layout_marginBottom="24dp"
android:clickable="true"
android:focusable="true"
android:src="@drawable/import_script"
android:visibility="gone"
app:backgroundTint="#0BC4FC"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:tint="@color/white" />
</androidx.constraintlayout.widget.ConstraintLayout>

View File

@ -0,0 +1,167 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="@drawable/bg_rounded_rect"
android:orientation="vertical"
android:padding="18dp">
<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginBottom="16dp">
<TextView
android:id="@+id/dialog_title"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Start New Script"
android:textColor="@color/white"
android:textSize="25sp"
android:textStyle="bold"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<ImageView
android:id="@+id/btn_close_dialog"
android:layout_width="24dp"
android:layout_height="24dp"
android:backgroundTint="@color/close_blue"
android:clickable="true"
android:focusable="true"
android:src="@drawable/cancel"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:tint="@color/gray" />
</androidx.constraintlayout.widget.ConstraintLayout>
<androidx.constraintlayout.widget.ConstraintLayout
android:id="@+id/layout_single_mode"
android:layout_width="match_parent"
android:layout_height="120dp"
android:layout_marginBottom="16dp"
android:background="@drawable/bg_mode_item_selector"
android:clickable="true"
android:focusable="true"
android:padding="16dp">
<ImageView
android:id="@+id/icon_single"
android:layout_width="24dp"
android:layout_height="24dp"
android:layout_marginStart="8dp"
android:src="@drawable/ic_circle_hollow"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<TextView
android:id="@+id/text_single_title"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="16dp"
android:text="Single Point Mode"
android:textColor="@color/white"
android:textSize="19sp"
android:textStyle="bold"
app:layout_constraintStart_toEndOf="@+id/icon_single"
app:layout_constraintTop_toTopOf="parent" />
<TextView
android:id="@+id/text_single_desc"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginTop="4dp"
android:text="Tap a single position repeatedly at\nspecified intervals"
android:textColor="@color/text_gray"
android:textSize="15sp"
app:layout_constraintEnd_toStartOf="@+id/iv_single_arrow"
app:layout_constraintStart_toStartOf="@+id/text_single_title"
app:layout_constraintTop_toBottomOf="@+id/text_single_title" />
<ImageView
android:id="@+id/iv_single_arrow"
android:layout_width="24dp"
android:layout_height="24dp"
android:layout_marginEnd="8dp"
android:src="@drawable/single_dialog"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toTopOf="parent" />
</androidx.constraintlayout.widget.ConstraintLayout>
<androidx.constraintlayout.widget.ConstraintLayout
android:id="@+id/layout_multi_mode"
android:layout_width="match_parent"
android:layout_height="120dp"
android:layout_marginBottom="24dp"
android:background="@drawable/bg_mode_item_selector"
android:clickable="true"
android:focusable="true"
android:padding="16dp">
<ImageView
android:id="@+id/icon_multi"
android:layout_width="24dp"
android:layout_height="24dp"
android:layout_marginStart="8dp"
android:src="@drawable/ic_circle_hollow"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<TextView
android:id="@+id/text_multi_title"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="16dp"
android:text="Multi-Point Mode"
android:textColor="@color/white"
android:textSize="19sp"
android:textStyle="bold"
app:layout_constraintStart_toEndOf="@+id/icon_multi"
app:layout_constraintTop_toTopOf="parent" />
<TextView
android:id="@+id/text_multi_desc"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginTop="4dp"
android:text="Set a series of taps/swipes in\ndifferent positions"
android:textColor="@color/text_gray"
android:textSize="15sp"
app:layout_constraintEnd_toStartOf="@+id/iv_multi_arrow"
app:layout_constraintStart_toStartOf="@+id/text_multi_title"
app:layout_constraintTop_toBottomOf="@+id/text_multi_title" />
<ImageView
android:id="@+id/iv_multi_arrow"
android:layout_width="41dp"
android:layout_height="48dp"
android:layout_marginEnd="8dp"
android:src="@drawable/multi_dialog"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toTopOf="parent" />
</androidx.constraintlayout.widget.ConstraintLayout>
<androidx.appcompat.widget.AppCompatButton
android:id="@+id/btn_start_create"
android:layout_width="257dp"
android:layout_height="48dp"
android:layout_gravity="center"
android:background="@drawable/bg_rounded_rect"
android:backgroundTint="@color/blue"
android:text="Start"
android:textAllCaps="false"
android:textColor="@color/white"
android:textSize="21sp"
android:textStyle="bold" />
</LinearLayout>

View File

@ -2,7 +2,7 @@
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="321dp"
android:layout_height="386dp"
android:layout_height="420dp"
android:background="@drawable/bg_rounded_rect"
android:paddingBottom="25dp">
@ -285,4 +285,33 @@
</LinearLayout>
</LinearLayout>
<androidx.appcompat.widget.AppCompatButton
android:id="@+id/save"
android:layout_width="100dp"
android:layout_height="36dp"
android:background="@drawable/btn_border_background"
android:text="Save"
android:textSize="14sp"
android:layout_marginTop="18dp"
app:layout_constraintStart_toStartOf="parent"
android:backgroundTint="#0BC4FC"
android:textAllCaps="false"
app:layout_constraintTop_toBottomOf="@+id/dialog_repeat_input"
app:layout_constraintEnd_toStartOf="@+id/close"
android:textColor="@color/white" />
<androidx.appcompat.widget.AppCompatButton
android:id="@+id/close"
android:layout_width="100dp"
android:layout_height="36dp"
android:background="@drawable/close_bg"
android:text="Close"
android:textAllCaps="false"
android:textSize="14sp"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toEndOf="@+id/save"
app:layout_constraintTop_toTopOf="@+id/save"
app:layout_constraintBottom_toBottomOf="@id/save"
android:textColor="@color/white" />
</androidx.constraintlayout.widget.ConstraintLayout>

View File

@ -1,28 +1,28 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="250dp"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginBottom="11dp"
android:background="@drawable/bg_rounded_rect"
android:backgroundTint="#164958"
android:layout_marginBottom="11dp"
android:padding="16dp">
<TextView
android:id="@+id/textViewTopLeft"
android:id="@+id/name"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="My Script 1"
android:textColor="@android:color/white"
android:textSize="20sp"
android:textStyle="bold"
app:layout_constraintBottom_toTopOf="@+id/textViewBottomLeft"
app:layout_constraintBottom_toTopOf="@+id/mode"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintVertical_chainStyle="packed" />
<TextView
android:id="@+id/textViewBottomLeft"
android:id="@+id/mode"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="4dp"
@ -31,14 +31,24 @@
android:textSize="14sp"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/textViewTopLeft" />
app:layout_constraintTop_toBottomOf="@+id/name" />
<ImageView
android:id="@+id/imageViewRight"
android:id="@+id/play"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginEnd="32dp"
android:src="@drawable/solution_play"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toStartOf="@+id/option"
app:layout_constraintTop_toTopOf="parent" />
<ImageView
android:id="@+id/option"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:src="@drawable/script_option"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toTopOf="parent" />

View File

@ -0,0 +1,50 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:background="@color/bg_blue"
android:elevation="8dp"
android:orientation="vertical"
android:padding="8dp">
<TextView
android:id="@+id/tv_rename"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="?attr/selectableItemBackground"
android:padding="12dp"
android:text="Rename"
android:textColor="@color/black"
android:textSize="16sp" />
<TextView
android:id="@+id/tv_duplicate"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="?attr/selectableItemBackground"
android:padding="12dp"
android:text="Duplicate"
android:textColor="@color/black"
android:textSize="16sp" />
<TextView
android:id="@+id/tv_delete"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="?attr/selectableItemBackground"
android:padding="12dp"
android:text="Delete"
android:textColor="@color/black"
android:textSize="16sp" />
<TextView
android:id="@+id/tv_export"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="?attr/selectableItemBackground"
android:padding="12dp"
android:text="Export"
android:textColor="@color/black"
android:textSize="16sp" />
</LinearLayout>

View File

@ -10,4 +10,5 @@
<color name="button_gray">#446672</color>
<color name="button_selected">#1A546B</color>
<color name="button_unselected">#171717</color>
<color name="close_blue">#368298</color>
</resources>