feat(mmp): 独立红包领取台(手机+PC)、同步、筛选与功能说明
隔离通用消息台;支持设置/手气摘要/电脑同步;忽略临时逆向脚本与运行时 JSON。
This commit is contained in:
@@ -0,0 +1,399 @@
|
||||
package com.miraclegarden.smsmessage.Activity;
|
||||
|
||||
import android.app.DatePickerDialog;
|
||||
import android.content.Intent;
|
||||
import android.graphics.Color;
|
||||
import android.os.Bundle;
|
||||
import android.os.Handler;
|
||||
import android.os.Looper;
|
||||
import android.text.Editable;
|
||||
import android.text.TextWatcher;
|
||||
import android.view.View;
|
||||
import android.widget.TextView;
|
||||
import android.widget.Toast;
|
||||
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.appcompat.app.AlertDialog;
|
||||
import androidx.recyclerview.widget.LinearLayoutManager;
|
||||
|
||||
import com.miraclegarden.library.app.MiracleGardenActivity;
|
||||
import com.miraclegarden.smsmessage.R;
|
||||
import com.miraclegarden.smsmessage.comm.CommonAdapter;
|
||||
import com.miraclegarden.smsmessage.comm.ViewHolder;
|
||||
import com.miraclegarden.smsmessage.databinding.ActivityMmpClaimBinding;
|
||||
import com.miraclegarden.smsmessage.mmp.MmpPacket;
|
||||
import com.miraclegarden.smsmessage.mmp.MmpPacketStore;
|
||||
import com.miraclegarden.smsmessage.mmp.MmpSettingsClient;
|
||||
import com.miraclegarden.smsmessage.mmp.MmpSyncClient;
|
||||
|
||||
import org.json.JSONObject;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Calendar;
|
||||
import java.util.List;
|
||||
import java.util.Locale;
|
||||
|
||||
/**
|
||||
* 手机端红包领取台(独立于通用消息/情况监听)。
|
||||
*/
|
||||
public class MmpClaimActivity extends MiracleGardenActivity<ActivityMmpClaimBinding>
|
||||
implements MmpPacketStore.Listener {
|
||||
|
||||
private final List<MmpPacket> allPackets = new ArrayList<>();
|
||||
private final List<MmpPacket> packets = new ArrayList<>();
|
||||
private CommonAdapter<MmpPacket> adapter;
|
||||
private final Handler handler = new Handler(Looper.getMainLooper());
|
||||
private final Runnable refreshRunnable = this::reload;
|
||||
/** 0全部 1领取中 2已领完 */
|
||||
private int statusFilter = 0;
|
||||
/** 0全部时间 1近1天 2近3天 3近7天 4自定义 */
|
||||
private int timeMode = 0;
|
||||
private long customFromMs;
|
||||
private long customToMs;
|
||||
private String keyword = "";
|
||||
private boolean settingsOpen;
|
||||
|
||||
@Override
|
||||
protected void onCreate(@Nullable Bundle savedInstanceState) {
|
||||
super.onCreate(savedInstanceState);
|
||||
initView();
|
||||
reload();
|
||||
loadSettingsQuiet();
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onResume() {
|
||||
super.onResume();
|
||||
MmpPacketStore.addListener(this);
|
||||
reload();
|
||||
MmpSyncClient.syncIfStale(this, 5000L);
|
||||
handler.removeCallbacks(refreshRunnable);
|
||||
handler.postDelayed(refreshRunnable, 2000);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onPause() {
|
||||
super.onPause();
|
||||
MmpPacketStore.removeListener(this);
|
||||
handler.removeCallbacks(refreshRunnable);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onMmpPacketsChanged() {
|
||||
runOnUiThread(this::reload);
|
||||
}
|
||||
|
||||
private void initView() {
|
||||
binding.backIv.setOnClickListener(v -> finish());
|
||||
binding.btnClear.setOnClickListener(v -> confirmClear());
|
||||
binding.btnHelp.setOnClickListener(v ->
|
||||
startActivity(new android.content.Intent(this, MmpHelpActivity.class)));
|
||||
binding.btnSettings.setOnClickListener(v -> toggleSettings());
|
||||
binding.btnCloseSettings.setOnClickListener(v -> {
|
||||
settingsOpen = false;
|
||||
binding.settingsPanel.setVisibility(View.GONE);
|
||||
});
|
||||
binding.btnSync.setOnClickListener(v -> syncNow());
|
||||
|
||||
binding.chipAll.setOnClickListener(v -> setStatusFilter(0));
|
||||
binding.chipOpen.setOnClickListener(v -> setStatusFilter(1));
|
||||
binding.chipDone.setOnClickListener(v -> setStatusFilter(2));
|
||||
binding.chipTimeAll.setOnClickListener(v -> setTimeMode(0));
|
||||
binding.chipTime1.setOnClickListener(v -> setTimeMode(1));
|
||||
binding.chipTime3.setOnClickListener(v -> setTimeMode(2));
|
||||
binding.chipTime7.setOnClickListener(v -> setTimeMode(3));
|
||||
binding.chipTimeCustom.setOnClickListener(v -> setTimeMode(4));
|
||||
binding.btnDateFrom.setOnClickListener(v -> pickDate(true));
|
||||
binding.btnDateTo.setOnClickListener(v -> pickDate(false));
|
||||
|
||||
binding.etFilter.addTextChangedListener(new TextWatcher() {
|
||||
@Override
|
||||
public void beforeTextChanged(CharSequence s, int start, int count, int after) {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onTextChanged(CharSequence s, int start, int before, int count) {
|
||||
keyword = s != null ? s.toString() : "";
|
||||
applyFilter();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void afterTextChanged(Editable s) {
|
||||
}
|
||||
});
|
||||
|
||||
binding.btnPresetFast.setOnClickListener(v -> applyPreset("fast"));
|
||||
binding.btnPresetNormal.setOnClickListener(v -> applyPreset("normal"));
|
||||
binding.btnPresetSlow.setOnClickListener(v -> applyPreset("slow"));
|
||||
binding.btnSaveSettings.setOnClickListener(v -> saveSettings());
|
||||
|
||||
binding.recyclerview.setLayoutManager(new LinearLayoutManager(this));
|
||||
adapter = new CommonAdapter<MmpPacket>(this, R.layout.item_mmp_packet, packets) {
|
||||
@Override
|
||||
public void convert(ViewHolder holder, MmpPacket packet, int index) {
|
||||
holder.setText(R.id.tv_title, packet.displayTitle());
|
||||
holder.setText(R.id.tv_sum, formatMoney(packet.sumClaimed > 0
|
||||
? packet.sumClaimed : parseDouble(packet.total)));
|
||||
|
||||
android.widget.TextView status = holder.getView(R.id.tv_status);
|
||||
if (packet.finished) {
|
||||
status.setText("已领完");
|
||||
status.setTextColor(Color.parseColor("#047857"));
|
||||
status.setBackgroundColor(Color.parseColor("#ECFDF5"));
|
||||
} else {
|
||||
status.setText("领取中");
|
||||
status.setTextColor(Color.parseColor("#B45309"));
|
||||
status.setBackgroundColor(Color.parseColor("#FFFBEB"));
|
||||
}
|
||||
|
||||
String meta = packet.claimantCount + " 人";
|
||||
if (packet.issuedAt != null && !packet.issuedAt.isEmpty()) {
|
||||
meta += " · 发放 " + packet.issuedAt;
|
||||
}
|
||||
holder.setText(R.id.tv_meta, meta);
|
||||
|
||||
String hi = packet.finished ? "最佳" : "最高";
|
||||
String lo = packet.finished ? "最差" : "最低";
|
||||
com.miraclegarden.smsmessage.mmp.MmpClaim best = packet.bestClaim();
|
||||
com.miraclegarden.smsmessage.mmp.MmpClaim worst = packet.worstClaim();
|
||||
if (best != null) {
|
||||
String amt = best.amountText != null ? best.amountText : formatMoney(best.amount);
|
||||
String nick = best.nickname != null ? best.nickname : "?";
|
||||
holder.setText(R.id.tv_best, hi + " " + nick + " " + amt);
|
||||
} else {
|
||||
holder.setText(R.id.tv_best, hi + " -");
|
||||
}
|
||||
if (worst != null && packet.claimantCount > 1) {
|
||||
String amt = worst.amountText != null ? worst.amountText : formatMoney(worst.amount);
|
||||
String nick = worst.nickname != null ? worst.nickname : "?";
|
||||
holder.setText(R.id.tv_worst, lo + " " + nick + " " + amt);
|
||||
} else {
|
||||
holder.setText(R.id.tv_worst, lo + " -");
|
||||
}
|
||||
holder.setText(R.id.tv_id, packet.shortId());
|
||||
holder.itemView.setOnClickListener(v -> openDetail(packet.packetId));
|
||||
}
|
||||
};
|
||||
binding.recyclerview.setAdapter(adapter);
|
||||
updateChipUi();
|
||||
updateTimeChipUi();
|
||||
}
|
||||
|
||||
private void reload() {
|
||||
allPackets.clear();
|
||||
allPackets.addAll(MmpPacketStore.getPackets(this));
|
||||
applyFilter();
|
||||
handler.removeCallbacks(refreshRunnable);
|
||||
handler.postDelayed(refreshRunnable, 2000);
|
||||
}
|
||||
|
||||
private void applyFilter() {
|
||||
packets.clear();
|
||||
for (MmpPacket p : allPackets) {
|
||||
if (p.matchesFilter(keyword, statusFilter, timeMode, customFromMs, customToMs)) {
|
||||
packets.add(p);
|
||||
}
|
||||
}
|
||||
adapter.notifyDataSetChanged();
|
||||
binding.tvCount.setText(packets.size() + "/" + allPackets.size() + " 个");
|
||||
binding.emptyLy.setVisibility(packets.isEmpty() ? View.VISIBLE : View.GONE);
|
||||
}
|
||||
|
||||
private void setStatusFilter(int filter) {
|
||||
statusFilter = filter;
|
||||
updateChipUi();
|
||||
applyFilter();
|
||||
}
|
||||
|
||||
private void setTimeMode(int mode) {
|
||||
timeMode = mode;
|
||||
binding.customRangeLy.setVisibility(mode == 4 ? View.VISIBLE : View.GONE);
|
||||
updateTimeChipUi();
|
||||
applyFilter();
|
||||
}
|
||||
|
||||
private void pickDate(boolean from) {
|
||||
Calendar cal = Calendar.getInstance();
|
||||
long base = from ? customFromMs : customToMs;
|
||||
if (base > 0) {
|
||||
cal.setTimeInMillis(base);
|
||||
}
|
||||
new DatePickerDialog(this, (view, y, m, d) -> {
|
||||
Calendar c = Calendar.getInstance();
|
||||
if (from) {
|
||||
c.set(y, m, d, 0, 0, 0);
|
||||
c.set(Calendar.MILLISECOND, 0);
|
||||
customFromMs = c.getTimeInMillis();
|
||||
binding.btnDateFrom.setText(String.format(Locale.US, "%04d-%02d-%02d", y, m + 1, d));
|
||||
} else {
|
||||
c.set(y, m, d, 23, 59, 59);
|
||||
c.set(Calendar.MILLISECOND, 999);
|
||||
customToMs = c.getTimeInMillis();
|
||||
binding.btnDateTo.setText(String.format(Locale.US, "%04d-%02d-%02d", y, m + 1, d));
|
||||
}
|
||||
if (timeMode != 4) {
|
||||
setTimeMode(4);
|
||||
} else {
|
||||
applyFilter();
|
||||
}
|
||||
}, cal.get(Calendar.YEAR), cal.get(Calendar.MONTH), cal.get(Calendar.DAY_OF_MONTH)).show();
|
||||
}
|
||||
|
||||
private void updateChipUi() {
|
||||
styleChip(binding.chipAll, statusFilter == 0, true);
|
||||
styleChip(binding.chipOpen, statusFilter == 1, true);
|
||||
styleChip(binding.chipDone, statusFilter == 2, true);
|
||||
}
|
||||
|
||||
private void updateTimeChipUi() {
|
||||
styleChip(binding.chipTimeAll, timeMode == 0, false);
|
||||
styleChip(binding.chipTime1, timeMode == 1, false);
|
||||
styleChip(binding.chipTime3, timeMode == 2, false);
|
||||
styleChip(binding.chipTime7, timeMode == 3, false);
|
||||
styleChip(binding.chipTimeCustom, timeMode == 4, false);
|
||||
}
|
||||
|
||||
private static void styleChip(View chip, boolean active, boolean brand) {
|
||||
if (!(chip instanceof android.widget.TextView)) {
|
||||
return;
|
||||
}
|
||||
android.widget.TextView tv = (android.widget.TextView) chip;
|
||||
if (active) {
|
||||
tv.setBackgroundColor(Color.parseColor(brand ? "#C45C26" : "#1C1410"));
|
||||
tv.setTextColor(Color.WHITE);
|
||||
} else {
|
||||
tv.setBackgroundColor(Color.parseColor("#EEEEEE"));
|
||||
tv.setTextColor(Color.parseColor("#333333"));
|
||||
}
|
||||
}
|
||||
|
||||
private void syncNow() {
|
||||
Toast.makeText(this, "正在同步到电脑…", Toast.LENGTH_SHORT).show();
|
||||
MmpSyncClient.syncToPc(this, (ok, message) -> runOnUiThread(() ->
|
||||
Toast.makeText(this, message != null ? message : (ok ? "同步成功" : "同步失败"),
|
||||
Toast.LENGTH_SHORT).show()));
|
||||
}
|
||||
|
||||
private void toggleSettings() {
|
||||
settingsOpen = !settingsOpen;
|
||||
binding.settingsPanel.setVisibility(settingsOpen ? View.VISIBLE : View.GONE);
|
||||
if (settingsOpen) {
|
||||
loadSettingsQuiet();
|
||||
}
|
||||
}
|
||||
|
||||
private void loadSettingsQuiet() {
|
||||
MmpSettingsClient.load((json, error) -> {
|
||||
if (json != null) {
|
||||
fillSettingsForm(json);
|
||||
binding.tvSettingsStatus.setText("已同步调试台设置");
|
||||
} else if (error != null) {
|
||||
binding.tvSettingsStatus.setText("设置未连上:" + error);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private void fillSettingsForm(JSONObject json) {
|
||||
binding.cfgHistory.setText(String.valueOf(json.optInt("historyCooldownSec", 8)));
|
||||
binding.cfgDetail.setText(String.valueOf(json.optInt("detailCooldownSec", 8)));
|
||||
binding.cfgDedup.setText(String.valueOf(json.optInt("dedupSec", 3)));
|
||||
binding.cfgGap.setText(String.valueOf(json.optInt("detailGapMs", 80)));
|
||||
binding.cfgOpenHistory.setChecked(json.optBoolean("openHistoryIfNoTemplate", true));
|
||||
}
|
||||
|
||||
private void applyPreset(String name) {
|
||||
try {
|
||||
JSONObject o = new JSONObject();
|
||||
if ("fast".equals(name)) {
|
||||
o.put("historyCooldownSec", 5);
|
||||
o.put("detailCooldownSec", 5);
|
||||
o.put("dedupSec", 2);
|
||||
o.put("detailGapMs", 50);
|
||||
o.put("pagePollMs", 800);
|
||||
o.put("openHistoryIfNoTemplate", true);
|
||||
} else if ("slow".equals(name)) {
|
||||
o.put("historyCooldownSec", 30);
|
||||
o.put("detailCooldownSec", 45);
|
||||
o.put("dedupSec", 15);
|
||||
o.put("detailGapMs", 200);
|
||||
o.put("pagePollMs", 3000);
|
||||
o.put("openHistoryIfNoTemplate", true);
|
||||
} else {
|
||||
o.put("historyCooldownSec", 8);
|
||||
o.put("detailCooldownSec", 8);
|
||||
o.put("dedupSec", 3);
|
||||
o.put("detailGapMs", 80);
|
||||
o.put("pagePollMs", 1000);
|
||||
o.put("openHistoryIfNoTemplate", true);
|
||||
}
|
||||
fillSettingsForm(o);
|
||||
binding.tvSettingsStatus.setText("已套用预设,点保存生效");
|
||||
} catch (Exception e) {
|
||||
binding.tvSettingsStatus.setText("预设失败");
|
||||
}
|
||||
}
|
||||
|
||||
private void saveSettings() {
|
||||
try {
|
||||
JSONObject body = new JSONObject();
|
||||
body.put("historyCooldownSec", parseIntSafe(binding.cfgHistory.getText().toString(), 8));
|
||||
body.put("detailCooldownSec", parseIntSafe(binding.cfgDetail.getText().toString(), 8));
|
||||
body.put("dedupSec", parseIntSafe(binding.cfgDedup.getText().toString(), 3));
|
||||
body.put("detailGapMs", parseIntSafe(binding.cfgGap.getText().toString(), 80));
|
||||
body.put("pagePollMs", 1000);
|
||||
body.put("openHistoryIfNoTemplate", binding.cfgOpenHistory.isChecked());
|
||||
binding.tvSettingsStatus.setText("保存中…");
|
||||
MmpSettingsClient.save(body, (json, error) -> {
|
||||
if (json != null) {
|
||||
fillSettingsForm(json);
|
||||
binding.tvSettingsStatus.setText("已保存,Hook 约 10 秒内生效");
|
||||
Toast.makeText(this, "设置已保存", Toast.LENGTH_SHORT).show();
|
||||
} else {
|
||||
binding.tvSettingsStatus.setText("保存失败:" + error);
|
||||
Toast.makeText(this, "保存失败,请确认调试台已开", Toast.LENGTH_SHORT).show();
|
||||
}
|
||||
});
|
||||
} catch (Exception e) {
|
||||
binding.tvSettingsStatus.setText("保存异常");
|
||||
}
|
||||
}
|
||||
|
||||
private void openDetail(String packetId) {
|
||||
Intent intent = new Intent(this, MmpDetailActivity.class);
|
||||
intent.putExtra(MmpDetailActivity.EXTRA_PACKET_ID, packetId);
|
||||
startActivity(intent);
|
||||
}
|
||||
|
||||
private void confirmClear() {
|
||||
new AlertDialog.Builder(this)
|
||||
.setTitle("清空红包台")
|
||||
.setMessage("仅清空本机红包领取记录,不影响通用消息台。")
|
||||
.setPositiveButton("清空", (d, w) -> {
|
||||
MmpPacketStore.clear(this);
|
||||
Toast.makeText(this, "已清空", Toast.LENGTH_SHORT).show();
|
||||
reload();
|
||||
})
|
||||
.setNegativeButton("取消", null)
|
||||
.show();
|
||||
}
|
||||
|
||||
private static String formatMoney(double v) {
|
||||
return String.format(Locale.US, "%.2f", v);
|
||||
}
|
||||
|
||||
private static double parseDouble(String s) {
|
||||
try {
|
||||
return s == null || s.isEmpty() ? 0 : Double.parseDouble(s);
|
||||
} catch (NumberFormatException e) {
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
|
||||
private static int parseIntSafe(String s, int def) {
|
||||
try {
|
||||
return Integer.parseInt(s.trim());
|
||||
} catch (Exception e) {
|
||||
return def;
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user