400 lines
16 KiB
Java
400 lines
16 KiB
Java
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;
|
||
}
|
||
}
|
||
}
|