import android.app.AlertDialog; import android.app.Dialog; import android.content.DialogInterface; import android.os.Handler; import android.os.Looper; import android.widget.Toast; import android.widget.CheckBox; import java.util.ArrayList; import java.util.HashMap; import java.util.HashSet; import java.util.List; import java.util.Set; import java.util.LinkedHashMap; import java.util.Map; import java.util.Iterator; import java.util.concurrent.ConcurrentHashMap; import android.text.TextUtils; import android.widget.Button; import android.widget.ListView; import android.view.View; import android.view.Window; import android.widget.EditText; import android.widget.LinearLayout; import android.widget.ProgressBar; import android.text.TextWatcher; import android.text.Editable; import android.widget.ArrayAdapter; import android.widget.AdapterView; import android.view.Gravity; import android.widget.TextView; import android.widget.ScrollView; import android.graphics.Color; import android.graphics.Typeface; import android.view.MotionEvent; import android.content.Context; import android.view.ViewGroup; import org.json.JSONObject; import org.json.JSONArray; import okhttp3.MediaType; import okhttp3.RequestBody; import okhttp3.OkHttpClient; import okhttp3.Request; import okhttp3.Response; import java.time.Duration; import java.io.*; import java.net.*; import me.hd.wauxv.data.bean.info.FriendInfo; import me.hd.wauxv.data.bean.info.GroupInfo; import com.tencent.mm.opensdk.modelmsg.WXMediaMessage; import com.tencent.mm.opensdk.modelmsg.WXMusicObject; class SongItem { String mid; String id; String vid; String name; String singer; String albumPmid; String mediaMid; String source; } class SongSelection { List songs = new ArrayList<>(); long createTime = 0; } private Map pending = new LinkedHashMap<>(); private final long TIMEO = 5 * 60 * 1000; private Map> histories = new LinkedHashMap<>(); private Map history(String cid) { if (cid == null) cid = ""; Map h = histories.get(cid); if (h == null) { h = new LinkedHashMap() { protected boolean removeEldestEntry(Map.Entry e) { return size() > 200; } }; histories.put(cid, h); } return h; } private List fc = null; private List gc = null; private Set qsBlacklist = new HashSet<>(); private final String K_SRC = "music_source", K_SELF = "self_onclick_music_enabled", K_APP = "multi_qq_custom_appid", K_TRIG = "multi_qq_custom_triggers", K_CNT_NETEASE = "search_result_count_netease", K_CNT_QQ = "search_result_count_qq", K_CNT_QISHUI = "search_result_count_qishui", K_CNT_AGGREGATE = "search_result_count_aggregate", K_AUTO = "auto_send_single", K_MODE = "send_mode", K_ISOLATE = "isolate_source"; private final String D_APP = "wx485a97c844086dc9", D_TRIG = "点歌,来首,我想听,放首"; private final int MAX_SR = 30, AGG_LIMIT = 15; private static final String QS_API = "http://www.ffapi.cn/int/v1/dg_qishui"; private static final String NETEASE_API = "http://www.ffapi.cn/int/v1/dg_netease"; private static final String NETEASE_COOKIE = "EVNSM=1.0.0; NMCID=sojdgr.1781216837915.01.4; versioncode=9005030; MUSIC_A_T=1606054002926; URS_APPID=212F2BF76A0D0C49C9FBE381B4753B2DB84DD09BB094D06DDD04C3AC703215CD888BBDD2609A548EFA6B1B3C7AFAE500; buildver=260612102200; resolution=2631x1272; sDeviceId=bnVsbAkwMjowMDowMDowMDowMDowMAliNDBjODM5MjQ4Y2Q4YmJmCWU1NDI2Mjg5NDg0ODdlOTQ%3D; ntes_kaola_ad=1; mobilename=PLK110; __csrf=8ddbd98f796a10bf955757b7aa65c5e4; brand=OnePlus; osver=16; MUSIC_U=00D4B57779592A590FD0E41CEEE16F06175AF2585534A225368E95F2D7C09BD41F4D9ADDE0681E3B2580B7CD58CF345EA2FFFE4B276B4EFA403436804ACA5A1137BAC5CEC69225460402A986603FB0DF8B96CB02A53FEB73EC1E707A719C4CD1717253751E92941316DC8EE1EADA2DF3E809E07F34F6D1969188C9D83011D49D0089C8A6169FA5A35F9959ED76D8517CA6054FF57829352F12CA3D3181D7190F9E60E021ED1636BB49208FA2CF9C03680EF2F6CE27E8DF5E822D91CBDC9C9CC9FB20B1D2F6C399637E71B238FE128BBAECFF810777B8BD3B14A3B9EE9759103BF0B9A371B69A469841611AF5FA5FDEF7AA8BA692064A1FF0012C5EABB12AE896B8E4598CDA3F729272C4CEDD1584A65ACD2E1BDC9D6BF1DBC135874F6EFD795C9B8AB99466095F75A62D612C5F92F2AE480D259ACEAA7CD11C98196AE42C3892B0E0AACE1CA2CFD4B92D55B7C87B581ED5D0B34A470247A55678EAD43BA8854885C2350383B70A076C8C437634C1BDB496457C88FBDA4AA40FEB68EE42DE2A5E252262C3C4C5954166CDF423EB064B6604F013E88BF28F5D12B00DB75C5C36C0B293C05B7FFB1910C372237409167F2D80B89A2B64E307CF098B0E3D0124AD067E615C22101033A40A1965A227D1DCC9AA; channel=tencent; os=android; screenType=other; deviceId=bnVsbAkwMjowMDowMDowMDowMDowMAkxOWNkYjA0MjI3MDdkYzI0CTc5Yjk3M2YwZjUzYjgyMDM%3D; NMDI=Q1NKTQkBDAAY4eBoqkkDFaHXl3c7AAAAHLWqHfFIuKpkahw%2BRsyPSTrcLbqxi4yfPvg54zVWP4%2FInG%2BhcqXbzLMjs%2BRMGZeE07BoBzT4Xg1GWJY%3D; appver=9.5.30; MUSIC_R_T=1607100727619; NMTID=00OQ0GBEFVEOvLqAkFvqzfRcEG5HcgAAAGeuMwEow; packageType=release; minors_mode_age_range=0"; private String blFile() { return JP + "/data/" + pluginId + "/qs_blacklist"; } private final Map> configCache = new ConcurrentHashMap<>(); void onLoad() { try { String bl = Read(blFile()); if (bl != null) for (String id : bl.split(",")) { if (!id.trim().isEmpty()) qsBlacklist.add(id.trim()); } } catch (Exception e) {} new Handler(Looper.getMainLooper()).postDelayed(new Runnable() { public void run() { clean(); new Handler(Looper.getMainLooper()).postDelayed(this, 60000); } }, 60000); toast("🎵 点歌插件已启动"); } private void saveBlacklist() { StringBuilder sb = new StringBuilder(); for (String id : qsBlacklist) { sb.append(id).append(","); } Write(blFile(), sb.toString()); } private String getSource(String tk) { if (getBoolean(K_ISOLATE, true)) { String s = getString(tk, K_SRC, null); if (s != null && (s.equals("netease") || s.equals("qq") || s.equals("qishui"))) return s; } return getString("config", K_SRC, "netease"); } private String srcName(String tk) { String s = getSource(tk); if (s.equals("netease")) return "网易云"; if (s.equals("qq")) return "QQ音乐"; if (s.equals("qishui")) return "汽水音乐"; return "网易云"; } private String srcEmoji(String tk) { String s = getSource(tk); if (s.equals("netease")) return "☁️"; if (s.equals("qq")) return "🎵"; if (s.equals("qishui")) return "🫧"; return "☁️"; } private boolean isGroup(String t) { return t != null && t.endsWith("@chatroom"); } private void setSource(String tk, String val) { if (getBoolean(K_ISOLATE, true)) putString(tk, K_SRC, val); else putString("config", K_SRC, val); } private String sourceTag(String src) { if ("netease".equals(src)) return "☁️"; if ("qq".equals(src)) return "🎵"; if ("qishui".equals(src)) return "🫧"; return ""; } private String sourceTagPlain(String src) { if ("netease".equals(src)) return "网易"; if ("qq".equals(src)) return "QQ"; if ("qishui".equals(src)) return "汽水"; return ""; } public boolean onClickSendBtn(String txt) { if (txt == null) return false; String g = getTargetTalker(), tk = g != null ? g : ""; if (txt.equals("点歌设置")) { settings(); return true; } if (txt.equals("切换网易云")) { if (isGroup(tk)) { sendText(tk, "⚠️ 群聊已开启聚合搜索"); return true; } setSource(tk, "netease"); insertSystemMsg(tk, "☁️ 已切换到 网易云", System.currentTimeMillis()); return true; } if (txt.equals("切换QQ音乐")) { if (isGroup(tk)) { sendText(tk, "⚠️ 群聊已开启聚合搜索"); return true; } setSource(tk, "qq"); insertSystemMsg(tk, "🎵 已切换到 QQ音乐", System.currentTimeMillis()); return true; } if (txt.equals("切换汽水音乐")) { if (isGroup(tk)) { sendText(tk, "⚠️ 群聊已开启聚合搜索"); return true; } setSource(tk, "qishui"); insertSystemMsg(tk, "🫧 已切换到 汽水音乐", System.currentTimeMillis()); return true; } if (txt.equals("开启点歌")) { putInt(tk, "开关", 1); insertSystemMsg(tk, srcEmoji(tk) + " 点歌已开启", System.currentTimeMillis()); return true; } if (txt.equals("关闭点歌")) { putInt(tk, "开关", 0); insertSystemMsg(tk, srcEmoji(tk) + " 点歌已关闭", System.currentTimeMillis()); return true; } if (txt.equals("取消")) { pending.remove(tk); return true; } if (txt.matches("[1-9]\\d*")) return number(tk, txt, getLoginWxid()); if (txt.startsWith("重播")) { String kw = txt.substring(2).trim(); if (kw.isEmpty()) { histShow(tk); return true; } SongItem f = histFind(tk, kw); if (f != null) { final SongItem fs = f; final String ftk = tk; toast(srcEmoji(ftk) + " " + fs.name); new Thread(new Runnable() { public void run() { play(ftk, fs, getLoginWxid()); } }).start(); } else sendText(tk, "❌ 未在历史中找到「" + kw + "」"); return true; } if (getBoolean(K_SELF, false) && isMC(txt)) { final String s = txt; final String ftk = tk; new Thread(new Runnable() { public void run() { cmd(s, ftk, 0, getLoginWxid()); } }).start(); return true; } return false; } public void onHandleMsg(Object d) { String t = d.content, q = d.talker, s = d.sendTalker; if (t == null) return; if (t.equals("切换网易云") || t.equals("切换QQ音乐") || t.equals("切换汽水音乐")) { if (isGroup(q)) { sendText(q, "⚠️ 群聊已开启聚合搜索"); return; } } if (t.equals("切换网易云")) { setSource(q, "netease"); sendText(q, "☁️ 已切换到 网易云"); return; } if (t.equals("切换QQ音乐")) { setSource(q, "qq"); sendText(q, "🎵 已切换到 QQ音乐"); return; } if (t.equals("切换汽水音乐")) { setSource(q, "qishui"); sendText(q, "🫧 已切换到 汽水音乐"); return; } if (t.equals("取消")) { pending.remove(q); return; } if (t.matches("[1-9]\\d*")) { number(q, t, s); return; } if (t.startsWith("重播")) { String kw = t.substring(2).trim(); if (kw.isEmpty()) { histShow(q); return; } SongItem f = histFind(q, kw); if (f != null) { final SongItem fs = f; final String fq = q; final String fsx = s; new Thread(new Runnable() { public void run() { play(fq, fs, fsx); } }).start(); } return; } if (isF(d)) return; if (isMC(t)) { final String ft = t, fq = q, fsx = s; final long fm = d.msgId; new Thread(new Runnable() { public void run() { cmd(ft, fq, fm, fsx); } }).start(); } } private boolean number(String tk, String txt, String sd) { try { SongSelection sl = pending.get(tk); if (sl == null || sl.songs == null || sl.songs.isEmpty()) { pending.remove(tk); return false; } if (System.currentTimeMillis() - sl.createTime > TIMEO) { pending.remove(tk); sendText(tk, "⏰ 选歌已超时"); return true; } int i = Integer.parseInt(txt) - 1; if (i < 0 || i >= sl.songs.size()) return false; SongItem sg = sl.songs.get(i); if (sg == null) { pending.remove(tk); return false; } pending.remove(tk); final String ftk = tk; final SongItem fs = sg; toast(srcEmoji(ftk) + " " + name(sd) + " 选了: " + (fs.name != null ? fs.name : "")); new Thread(new Runnable() { public void run() { try { play(ftk, fs, sd); } catch (Exception e) { sendText(ftk, "❌ 获取失败"); } } }).start(); return true; } catch (Exception e) { pending.remove(tk); return true; } } private SongItem histFind(String cid, String kw) { if (kw == null) return null; for (Map.Entry e : history(cid).entrySet()) { if (e.getKey().toLowerCase().contains(kw.toLowerCase())) return e.getValue(); } return null; } private void histShow(String tk) { Map h = history(tk); if (h.isEmpty()) { sendText(tk, srcEmoji(tk) + " 暂无历史"); return; } List items = new ArrayList<>(h.values()), disp = new ArrayList<>(); Set seen = new HashSet<>(); for (int i = 0; i < Math.min(items.size(), 15); i++) { SongItem s = items.get(i); if (seen.add((s.name + "|" + s.singer + "|" + s.mid + "|" + s.source).toLowerCase())) disp.add(s); } SongSelection sl = new SongSelection(); sl.songs = disp; sl.createTime = System.currentTimeMillis(); pending.put(tk, sl); StringBuilder sb = new StringBuilder("📜 历史(最近15首)\n\n"); for (int i = 0; i < disp.size(); i++) { SongItem si = disp.get(i); sb.append(i + 1).append(". ").append(si.name); if (!TextUtils.isEmpty(si.singer)) sb.append(" - ").append(si.singer); sb.append(sourceTag(si.source)).append("\n"); } sb.append("\n👉 回复数字 1-").append(disp.size()).append(" 重播歌曲"); sendText(tk, sb.toString()); } private void cmd(String txt, String qun, long mid, String sd) { String ti = ""; for (String t : getString("config", K_TRIG, D_TRIG).split(",")) { if (txt.startsWith(t.trim())) { ti = txt.substring(t.trim().length()).trim(); break; } } if (ti.isEmpty()) return; if (isGroup(qun)) { searchAggregate(qun, ti, mid, sd); return; } String source = getSource(qun); if ("netease".equals(source)) searchNetease(qun, ti, mid, sd); else if ("qq".equals(source)) searchQQ(qun, ti, mid, sd); else if ("qishui".equals(source)) searchQishui(qun, ti, mid, sd); else searchNetease(qun, ti, mid, sd); } private List uniqueSongs(List songs) { if (songs == null) return null; List res = new ArrayList<>(); Set seen = new HashSet<>(); for (SongItem s : songs) { String key1 = s.mid; String key2 = (s.name + "|" + s.singer).toLowerCase(); if ((key1 != null && !key1.isEmpty() && !seen.add(key1)) || (key1 == null && !seen.add(key2))) { continue; } res.add(s); } return res; } private List filterSongs(List songs, String kw, int maxCnt) { if (songs == null || songs.isEmpty()) return null; final String k = kw.toLowerCase().trim(); List exact = new ArrayList<>(), singerM = new ArrayList<>(); for (int i = 0; i < songs.size(); i++) { SongItem si = songs.get(i); String snm = si.name != null ? si.name.toLowerCase() : ""; String ssg = si.singer != null ? si.singer.toLowerCase() : ""; if (snm.contains(k)) exact.add(si); else if (ssg.contains(k)) singerM.add(si); } List result = new ArrayList<>(); if (!exact.isEmpty()) result.addAll(exact); else if (!singerM.isEmpty()) result.addAll(singerM); if (result.isEmpty() && !songs.isEmpty()) { result.addAll(songs.subList(0, Math.min(songs.size(), maxCnt))); } if (result.isEmpty()) return null; if (result.size() > maxCnt) result = result.subList(0, maxCnt); return uniqueSongs(result); } private void searchAggregate(String qun, String sn, long mid, String sd) { try { int mode = getInt("config", K_MODE, 1); int neLimit = getNeteaseResultCount(); int qqLimit = getQQResultCount(); int qsLimit = getQishuiResultCount(); int aggLimit = getAggregateResultCount(); List all = new ArrayList<>(); int neCnt = 0, qqCnt = 0, qsCnt = 0; List neF = filterSongs(searchNeteaseInternal(sn, neLimit * 2), sn, aggLimit); if (neF != null) { for (int i = 0; i < neF.size(); i++) { neF.get(i).source = "netease"; all.add(neF.get(i)); neCnt++; } } List qqF = filterSongs(searchQQInternal(sn, qqLimit * 2), sn, aggLimit); if (qqF != null) { for (int i = 0; i < qqF.size(); i++) { qqF.get(i).source = "qq"; all.add(qqF.get(i)); qqCnt++; } } // 汽水音乐搜索时过滤黑名单 List qsRaw = searchQishuiInternal(sn, qsLimit * 3, false); if (qsRaw != null) { List qsFiltered = new ArrayList<>(); for (SongItem si : qsRaw) { // 检查是否在黑名单中(使用歌曲ID) if (!qsBlacklist.contains(si.id)) { qsFiltered.add(si); } } List qsF = filterSongs(qsFiltered, sn, aggLimit); if (qsF != null) { for (int i = 0; i < qsF.size(); i++) { qsF.get(i).source = "qishui"; all.add(qsF.get(i)); qsCnt++; } } } all = uniqueSongs(all); if (all.isEmpty()) { sendReplyMsg(qun, mid, "🔍 三源均未找到「" + sn + "」\n💡 尝试换关键词"); return; } if (mode == 1) { SongItem si = all.get(0); history(qun).put("[" + sourceTagPlain(si.source) + "]" + si.name + "|" + si.singer, si); play(qun, si, sd); return; } if (all.size() == 1 && getBoolean(K_AUTO, false)) { SongItem si = all.get(0); history(qun).put("[" + sourceTagPlain(si.source) + "]" + si.name + "|" + si.singer, si); play(qun, si, sd); return; } SongSelection sel = new SongSelection(); sel.songs = all; sel.createTime = System.currentTimeMillis(); pending.put(qun, sel); StringBuilder sb = new StringBuilder(); sb.append("🔀 聚合搜索「").append(sn).append("」共 ").append(all.size()).append(" 首\n"); sb.append("☁️网易:").append(neCnt).append(" 🎵QQ:").append(qqCnt).append(" 🫧汽水:").append(qsCnt).append("\n\n"); String lastSrc = ""; for (int i = 0; i < all.size(); i++) { SongItem si = all.get(i); if (i > 0 && !si.source.equals(lastSrc)) sb.append("\n\n"); sb.append(i + 1).append(". ").append(sourceTag(si.source)).append(" ").append(si.name); if (!TextUtils.isEmpty(si.singer)) sb.append(" - ").append(si.singer); sb.append("\n"); lastSrc = si.source; } sb.append("───────────────\n👉 回复数字 1~").append(all.size()).append(" 点歌\n👉 回复「取消」放弃歌曲\n⏰ 5分钟自动取消选择的歌曲"); sendReplyMsg(qun, mid, sb.toString()); } catch (Exception e) { sendReplyMsg(qun, mid, "❌ 聚合异常: " + e.getMessage()); } } // ----- 网易云搜索 ----- private int getNeteaseResultCount() { String v = getString("config", K_CNT_NETEASE, "20"); try { int val = Integer.parseInt(v.trim()); if (val < 1) return 1; if (val > 30) return 30; return val; } catch (NumberFormatException e) { return 20; } } private List searchNeteaseInternal(String sn, int limit) { try { int fetch = Math.min(limit, 30); String url = NETEASE_API + "?msg=" + URLEncoder.encode(sn, "UTF-8") + "&limit=" + fetch + "&act=search&format=json"; Map headers = new HashMap<>(); headers.put("Cookie", NETEASE_COOKIE); headers.put("User-Agent", "Mozilla/5.0"); String resp = get(url, headers); if (resp == null || resp.isEmpty()) { log("网易云搜索请求失败,url=" + url); return null; } JSONObject json = new JSONObject(resp); JSONArray listArray = json.optJSONArray("list"); if (listArray == null || listArray.length() == 0) { log("网易云搜索无结果: " + sn); return null; } List songs = new ArrayList<>(); Set seen = new HashSet<>(); for (int i = 0; i < listArray.length() && songs.size() < fetch; i++) { JSONObject item = listArray.optJSONObject(i); if (item == null) continue; String title = item.optString("title", ""); String name = item.optString("name", ""); String singer = item.optString("singer", "未知歌手"); String id = item.optString("id", ""); String pic = item.optString("pic", ""); if (TextUtils.isEmpty(name) && !TextUtils.isEmpty(title)) { int idx = title.lastIndexOf(" - "); if (idx > 0) { name = title.substring(0, idx); if (TextUtils.isEmpty(singer) || "未知歌手".equals(singer)) { singer = title.substring(idx + 3); } } else { name = title; } } if (TextUtils.isEmpty(name) || TextUtils.isEmpty(id)) continue; String key = name + "|" + singer; if (seen.contains(key)) continue; seen.add(key); SongItem si = new SongItem(); si.name = name; si.singer = singer; si.mid = id; si.id = id; if (!TextUtils.isEmpty(pic) && pic.startsWith("http://")) pic = "https://" + pic.substring(7); si.albumPmid = pic; songs.add(si); } if (songs.isEmpty()) { log("网易云解析歌曲列表为空"); return null; } log("网易云搜索成功,共 " + songs.size() + " 首"); return songs; } catch (Exception e) { log("searchNeteaseInternal 异常: " + e.getMessage()); e.printStackTrace(); return null; } } // ----- QQ音乐搜索 ----- private int getQQResultCount() { String v = getString("config", K_CNT_QQ, "10"); try { int val = Integer.parseInt(v.trim()); if (val < 1) return 1; if (val > 30) return 30; return val; } catch (NumberFormatException e) { return 10; } } private List searchQQInternal(String sn, int limit) { try { int fetch = limit > 30 ? 30 : limit; JSONObject sr = CreateQQSearchRequestMulti(sn, fetch); String rp = get("http://u.y.qq.com/cgi-bin/musicu.fcg?data=" + sr.toString(), null); if (rp == null || rp.isEmpty()) return null; JSONObject sl = new JSONObject(rp).optJSONObject("searchMusic").optJSONObject("data").optJSONObject("body").optJSONObject("song"); if (sl == null) return null; JSONArray arr = sl.optJSONArray("list"); if (arr == null || arr.length() == 0) return null; List songs = new ArrayList<>(); Set seen = new HashSet<>(); for (int i = 0; i < Math.min(arr.length(), fetch); i++) { JSONObject it = arr.optJSONObject(i); if (it == null) continue; String nm = it.optString("name", ""), sg = ""; JSONArray sa = it.optJSONArray("singer"); if (sa != null && sa.length() > 0) sg = sa.optJSONObject(0).optString("name", ""); String mid = it.optString("mid", ""); String id = String.valueOf(it.optLong("id", 0)); if (!seen.add(mid)) continue; SongItem si = new SongItem(); si.name = nm; si.singer = sg; si.mid = mid; si.id = id; JSONObject al = it.optJSONObject("album"); si.albumPmid = al != null ? al.optString("pmid", "") : ""; JSONObject fl = it.optJSONObject("file"); si.mediaMid = fl != null ? fl.optString("media_mid", "") : ""; songs.add(si); } return songs.isEmpty() ? null : songs; } catch (Exception e) { return null; } } // ----- 汽水音乐搜索 ----- private int getQishuiResultCount() { String v = getString("config", K_CNT_QISHUI, "10"); try { int val = Integer.parseInt(v.trim()); if (val < 1) return 1; if (val > 30) return 30; return val; } catch (NumberFormatException e) { return 10; } } private int getAggregateResultCount() { String v = getString("config", K_CNT_AGGREGATE, "15"); try { int val = Integer.parseInt(v.trim()); if (val < 1) return 1; if (val > 30) return 30; return val; } catch (NumberFormatException e) { return 15; } } private List searchQishuiInternal(String sn, int limit, boolean forceVid) { if (TextUtils.isEmpty(sn)) return null; List allSongs = new ArrayList<>(); Set seen = new HashSet<>(); try { String url = QS_API + "?msg=" + URLEncoder.encode(sn, "UTF-8") + "&format=json"; String resp = get(url, qsHeaders()); if (resp == null || resp.isEmpty()) return null; JSONObject json = new JSONObject(resp); if (json.optInt("code", -1) != 200) return null; Object dataObj = json.opt("data"); if (dataObj == null) return null; if (dataObj instanceof JSONArray) { JSONArray arr = (JSONArray) dataObj; for (int i = 0; i < arr.length(); i++) { JSONObject item = arr.optJSONObject(i); if (item == null) continue; String nm = item.optString("title", ""); String sid = item.optString("track_id", ""); String cover = item.optString("cover", ""); String singer = item.optString("singer", "未知歌手"); if (nm.isEmpty() || sid.isEmpty()) continue; if (seen.add(sid)) { SongItem si = new SongItem(); si.name = nm; si.singer = singer; si.mid = sid; si.id = sid; si.vid = ""; if (!cover.isEmpty() && cover.startsWith("http://")) cover = "https://" + cover.substring(7); si.albumPmid = cover; si.source = "qishui"; allSongs.add(si); if (allSongs.size() >= limit) break; } } } else if (dataObj instanceof JSONObject) { JSONObject item = (JSONObject) dataObj; String nm = item.optString("title", ""); String sid = item.optString("track_id", ""); String cover = item.optString("cover", ""); String singer = item.optString("singer", "未知歌手"); if (!nm.isEmpty() && !sid.isEmpty()) { SongItem si = new SongItem(); si.name = nm; si.singer = singer; si.mid = sid; si.id = sid; si.vid = ""; if (!cover.isEmpty() && cover.startsWith("http://")) cover = "https://" + cover.substring(7); si.albumPmid = cover; si.source = "qishui"; allSongs.add(si); } } } catch (Exception e) { e.printStackTrace(); } return allSongs.isEmpty() ? null : allSongs; } private void searchNetease(String qun, String sn, long mid, String sd) { try { int cnt = getNeteaseResultCount(); int mode = getInt("config", K_MODE, 1); List raw = searchNeteaseInternal(sn, cnt); if (raw == null || raw.isEmpty()) { sendReplyMsg(qun, mid, "❌ 网易云搜索失败"); return; } for (SongItem si : raw) si.source = "netease"; List filt = filterSongs(raw, sn, cnt); if (filt == null) { sendReplyMsg(qun, mid, "🔍 网易云未精确匹配「" + sn + "」"); return; } filt = uniqueSongs(filt); if (mode == 1) { history(qun).put("[☁]" + filt.get(0).name + "|" + filt.get(0).singer, filt.get(0)); musicItemNetease(qun, filt.get(0), mid, sd); return; } if (filt.size() == 1 && getBoolean(K_AUTO, false)) { history(qun).put("[☁]" + filt.get(0).name + "|" + filt.get(0).singer, filt.get(0)); musicItemNetease(qun, filt.get(0), mid, sd); return; } showList(qun, sn, filt, mid, "☁️ 网易云"); } catch (Exception e) { sendReplyMsg(qun, mid, "❌ 网易云异常: " + e.getMessage()); } } private void musicItemNetease(String qun, SongItem song, long mid, String sd) { try { if (song == null) { sendReplyMsg(qun, mid, "❌ 数据为空"); return; } String nm = nn(song.name), sg = nn(song.singer), id = nn(song.id); if (TextUtils.isEmpty(id)) { sendReplyMsg(qun, mid, "❌ 歌曲ID无效"); return; } String playUrl = ""; String lyric = ""; String pic = nn(song.albumPmid); try { String url = NETEASE_API + "?id=" + id + "&act=musicurl&format=json"; Map headers = new HashMap<>(); headers.put("Cookie", NETEASE_COOKIE); headers.put("User-Agent", "Mozilla/5.0"); String resp = get(url, headers); if (resp != null && !resp.isEmpty()) { try { JSONObject json = new JSONObject(resp); playUrl = json.optString("url", ""); } catch (Exception e) { playUrl = resp.trim(); } if (TextUtils.isEmpty(playUrl)) { playUrl = resp.trim(); } } } catch (Exception e) { log("获取播放链接失败: " + e.getMessage()); } if (TextUtils.isEmpty(playUrl)) { sendReplyMsg(qun, mid, "❌ 获取播放链接失败"); return; } try { String lyricUrl = NETEASE_API + "?id=" + id + "&act=lrcgc&format=json"; Map headers = new HashMap<>(); headers.put("Cookie", NETEASE_COOKIE); headers.put("User-Agent", "Mozilla/5.0"); String resp = get(lyricUrl, headers); if (resp != null && !resp.isEmpty()) { if (resp.startsWith("{") && resp.contains("\"lrc\"")) { try { JSONObject json = new JSONObject(resp); lyric = json.optString("lrc", ""); } catch (Exception e) { lyric = resp; } } else { lyric = resp; } if (!TextUtils.isEmpty(lyric)) { lyric = lyric.trim(); } } } catch (Exception e) { log("获取歌词失败: " + e.getMessage()); } if (TextUtils.isEmpty(lyric)) lyric = "[99:99.99]暂无歌词"; history(qun).put("[☁]" + nm + "|" + sg, song); String finalPic = TextUtils.isEmpty(pic) ? "https://y.gtimg.cn/music/photo_new/T002R300x300M0000000000.jpg" : pic; sendCard(qun, nm, sg, playUrl, lyric, finalPic, mid, sd); } catch (Exception e) { sendReplyMsg(qun, mid, "❌ 网易云异常: " + e.getMessage()); e.printStackTrace(); } } private void searchQQ(String qun, String sn, long mid, String sd) { try { int cnt = getQQResultCount(); int mode = getInt("config", K_MODE, 1); List raw = searchQQInternal(sn, cnt); if (raw == null || raw.isEmpty()) { sendReplyMsg(qun, mid, "❌ QQ音乐搜索失败"); return; } for (SongItem si : raw) si.source = "qq"; List filt = filterSongs(raw, sn, cnt); if (filt == null) { sendReplyMsg(qun, mid, "🔍 QQ音乐未精确匹配「" + sn + "」"); return; } filt = uniqueSongs(filt); if (mode == 1) { history(qun).put("[QQ]" + filt.get(0).name + "|" + filt.get(0).singer, filt.get(0)); musicItemQQ(qun, filt.get(0), mid, sd); return; } if (filt.size() == 1 && getBoolean(K_AUTO, false)) { history(qun).put("[QQ]" + filt.get(0).name + "|" + filt.get(0).singer, filt.get(0)); musicItemQQ(qun, filt.get(0), mid, sd); return; } showList(qun, sn, filt, mid, "🎵 QQ音乐"); } catch (Exception e) { sendReplyMsg(qun, mid, "❌ 异常: " + e.getMessage()); } } private void musicItemQQ(String qun, SongItem song, long mid, String sd) { try { if (song == null) { sendReplyMsg(qun, mid, "❌ 数据为空"); return; } String nm = nn(song.name), m = nn(song.mid), sg = nn(song.singer), pm = nn(song.albumPmid), mm = nn(song.mediaMid), sid = nn(song.id); String pic = "http://y.gtimg.cn/music/photo_new/T002R500x500M000" + pm + ".jpg"; Map hdr = new HashMap<>(); hdr.put("user-agent", "Mozilla/5.0"); hdr.put("referer", "https://y.qq.com/"); String lr = get("http://c.y.qq.com/lyric/fcgi-bin/fcg_query_lyric_new.fcg?format=json&nobase64=1&songmid=" + m, hdr); String lyric = ""; if (lr != null && !lr.isEmpty()) { try { JSONObject lo = new JSONObject(lr); if (lo.has("lyric")) lyric = lo.optString("lyric"); } catch (Exception ie) {} } boolean ok = false; int idInt = -1; try { idInt = Integer.parseInt(sid); } catch (NumberFormatException ie) {} if (idInt > 0) { JSONObject ppReq = CreateQQPpurlRequest(idInt); String ppResp = post("http://u.y.qq.com/cgi-bin/musicu.fcg", ppReq.toString(), null); if (ppResp != null && !ppResp.isEmpty()) { try { String ppurl = exPp(new JSONObject(ppResp)); if (!TextUtils.isEmpty(ppurl)) { JSONObject vkReq = CreateQQVkeyRequest(m, ppurl); String vkResp = post("http://u.y.qq.com/cgi-bin/musicu.fcg", vkReq.toString(), null); if (vkResp != null && !vkResp.isEmpty()) { String url = exVk(new JSONObject(vkResp)); if (!TextUtils.isEmpty(url)) { history(qun).put("[QQ]" + nm + "|" + sg, song); sendCard(qun, nm, sg, url, lyric, pic, mid, sd); ok = true; } } } } catch (Exception ie) {} } } if (!ok) { if (TextUtils.isEmpty(mm) || "null".equals(mm) || TextUtils.isEmpty(m) || "null".equals(m)) { sendReplyMsg(qun, mid, "❌ 歌曲数据不完整"); return; } JSONObject gv = CreateGetVkeyRequest(mm, m); String gvResp = post("http://u.y.qq.com/cgi-bin/musicu.fcg", gv.toString(), null); if (gvResp == null || gvResp.isEmpty()) { sendReplyMsg(qun, mid, "❌ 获取失败"); return; } String fu = ""; try { fu = new JSONObject(gvResp).optJSONObject("request").optJSONObject("data").optJSONArray("midurlinfo").optJSONObject(0).optString("flowurl", ""); } catch (Exception ie) {} if (!TextUtils.isEmpty(fu)) { history(qun).put("[QQ]" + nm + "|" + sg, song); sendCard(qun, nm, sg, "http://sjy.stream.qqmusic.qq.com/" + fu, lyric, pic, mid, sd); } else sendReplyMsg(qun, mid, "❌ 获取失败,可能是数字专辑"); } } catch (Exception e) { sendReplyMsg(qun, mid, "❌ QQ异常: " + e.getMessage()); } } private void searchQishui(String qun, String sn, long mid, String sd) { try { int cnt = getQishuiResultCount(); int mode = getInt("config", K_MODE, 1); List raw = searchQishuiInternal(sn, cnt * 2, false); if (raw == null || raw.isEmpty()) { sendReplyMsg(qun, mid, "🔍 汽水未找到「" + sn + "」\n💡 可切换QQ/网易云或尝试更精确的关键词"); return; } // 过滤黑名单 List filtered = new ArrayList<>(); for (SongItem si : raw) { if (!qsBlacklist.contains(si.id)) { filtered.add(si); } } if (filtered.isEmpty()) { sendReplyMsg(qun, mid, "🚫 汽水搜索到 " + raw.size() + " 首,但全部在黑名单中\n💡 可在设置中管理黑名单"); return; } List filt = filterSongs(filtered, sn, cnt); if (filt == null || filt.isEmpty()) { sendReplyMsg(qun, mid, "🔍 汽水未精确匹配「" + sn + "」\n💡 可切换QQ/网易云"); return; } filt = uniqueSongs(filt); if (mode == 1) { history(qun).put("[🫧]" + filt.get(0).name + "|" + filt.get(0).singer, filt.get(0)); musicItemQishui(qun, filt.get(0), mid, sd); return; } if (filt.size() == 1 && getBoolean(K_AUTO, false)) { history(qun).put("[🫧]" + filt.get(0).name + "|" + filt.get(0).singer, filt.get(0)); musicItemQishui(qun, filt.get(0), mid, sd); return; } showList(qun, sn, filt, mid, "🫧 汽水音乐"); } catch (Exception e) { sendReplyMsg(qun, mid, "❌ 汽水异常: " + e.getMessage()); } } private void musicItemQishui(String qun, SongItem song, long mid, String sd) { try { if (song == null) { sendReplyMsg(qun, mid, "❌ 数据为空"); return; } String nm = nn(song.name); String sg = nn(song.singer); String sid = nn(song.id); String pic = nn(song.albumPmid); String playUrl = "", lyric = ""; String url = QS_API + "?track_id=" + sid + "&format=json"; String resp = get(url, qsHeaders()); if (!TextUtils.isEmpty(resp)) { JSONObject j = new JSONObject(resp); if (j.optInt("code", -1) == 200) { JSONObject data = j.optJSONObject("data"); if (data != null) { playUrl = data.optString("url", ""); lyric = data.optString("lrc", ""); if (TextUtils.isEmpty(pic)) pic = data.optString("cover", ""); String retId = data.optString("track_id", ""); if (!TextUtils.isEmpty(retId) && !retId.equals(sid)) { sendReplyMsg(qun, mid, "⚠️ 汽水返回歌曲ID " + retId + " 与请求 " + sid + " 不符"); return; } } else { playUrl = j.optString("url", ""); lyric = j.optString("lrc", ""); if (TextUtils.isEmpty(pic)) pic = j.optString("cover", ""); String retId = j.optString("track_id", ""); if (!TextUtils.isEmpty(retId) && !retId.equals(sid)) { sendReplyMsg(qun, mid, "⚠️ 汽水返回歌曲ID " + retId + " 与请求 " + sid + " 不符"); return; } } } else { String msg = j.optString("msg", "未知错误"); sendReplyMsg(qun, mid, "⚠️ 汽水获取失败: " + msg); return; } } if (TextUtils.isEmpty(playUrl)) { sendReplyMsg(qun, mid, "🫧 汽水「" + nm + "」播放链接获取失败\n💡 可切换QQ/网易云"); return; } history(qun).put("[🫧]" + nm + "|" + sg, song); sendCard(qun, nm, sg, playUrl, lyric, pic, mid, sd); } catch (Exception e) { sendReplyMsg(qun, mid, "❌ 汽水异常: " + e.getMessage()); } } private Map qsHeaders() { Map h = new HashMap<>(); h.put("User-Agent", "Mozilla/5.0"); return h; } private String nn(String s) { return s != null ? s : ""; } private String exPp(JSONObject o) { try { JSONObject r = o.optJSONObject("request"); if (r == null) return ""; JSONObject d = r.optJSONObject("data"); if (d == null) return ""; JSONArray t = d.optJSONArray("tracks"); if (t == null || t.length() == 0) return ""; return t.optJSONObject(0).optJSONObject("control").optString("ppurl", ""); } catch (Exception e) { return ""; } } private String exVk(JSONObject o) { try { return o.optJSONObject("request").optJSONObject("data").optJSONObject("data").optJSONObject("Yun").optString("purl", ""); } catch (Exception e) { return ""; } } private void clean() { long n = System.currentTimeMillis(); Iterator> it = pending.entrySet().iterator(); while (it.hasNext()) { if (n - it.next().getValue().createTime > TIMEO) it.remove(); } } private String name(String wxid) { try { return getFriendName(wxid); } catch (Exception e) { return wxid; } } private boolean isMC(String t) { if (t == null) return false; for (String tr : getString("config", K_TRIG, D_TRIG).split(",")) if (t.startsWith(tr.trim())) return true; return false; } private void showList(String qun, String sn, List songs, long mid, String tag) { songs = uniqueSongs(songs); SongSelection sel = new SongSelection(); sel.songs = songs; sel.createTime = System.currentTimeMillis(); pending.put(qun, sel); StringBuilder sb = new StringBuilder(tag + " 搜索「" + sn + "」找到 " + songs.size() + " 首:\n\n"); for (int i = 0; i < songs.size(); i++) { SongItem si = songs.get(i); sb.append(i + 1).append(". ").append(si.name); sb.append(" - ").append(TextUtils.isEmpty(si.singer) ? "未知" : si.singer).append("\n"); } sb.append("───────────────\n👉 回复数字 1~").append(songs.size()).append(" 点歌\n👉 回复「取消」放弃歌曲\n⏰ 5分钟后自动取消选择歌曲"); sendReplyMsg(qun, mid, sb.toString()); } private void play(String qun, SongItem song, String sd) { if (song == null) return; if ("netease".equals(song.source)) musicItemNetease(qun, song, 0, sd); else if ("qq".equals(song.source)) musicItemQQ(qun, song, 0, sd); else if ("qishui".equals(song.source)) musicItemQishui(qun, song, 0, sd); else musicItemNetease(qun, song, 0, sd); } private void sendCard(String tk, String ti, String sg, String url, String lrc, String pic, long mid, String sd) { String fs = sg; String fp = pic; if (getBoolean("multi_qq_avatar", false)) { String av = getAvatarUrl(sd, true); if (!TextUtils.isEmpty(av)) fp = av; } if (getBoolean("multi_qq_nickname", false)) { String n = getFriendName(sd, tk); if (!TextUtils.isEmpty(n)) fs = n; } if (TextUtils.isEmpty(fp) || (!fp.startsWith("http://") && !fp.startsWith("https://"))) fp = "https://y.gtimg.cn/music/photo_new/T002R300x300M0000000000.jpg"; WXMusicObject m = new WXMusicObject(); m.musicDataUrl = url; if (lrc == null || lrc.isEmpty()) lrc = "[99:99.99]暂无歌词"; m.songLyric = lrc; m.songAlbumUrl = fp; WXMediaMessage mm = new WXMediaMessage(m); mm.title = ti; mm.description = fs; sendMediaMsg(tk, mm, getString("config", K_APP, D_APP)); } private void sendReplyMsg(String tk, long mid, String ct) { if (mid == 0) sendText(tk, ct); else sendQuoteMsg(tk, mid, ct); } public JSONObject CreateQQSearchRequestMulti(String mn, int cnt) { try { JSONObject j = new JSONObject(); JSONObject c = new JSONObject(); c.put("ct", "19"); c.put("cv", "1882"); c.put("uin", "3449496653"); j.put("comm", c); JSONObject r = new JSONObject(); r.put("method", "DoSearchForQQMusicDesktop"); r.put("module", "music.search.SearchCgiService"); JSONObject p = new JSONObject(); p.put("grp", "1"); p.put("num_per_page", String.valueOf(cnt)); p.put("page_num", "1"); p.put("query", mn); p.put("search_type", "0"); r.put("param", p); j.put("searchMusic", r); return j; } catch (Exception e) { return new JSONObject(); } } public JSONObject CreateQQPpurlRequest(String id) { try { return CreateQQPpurlRequest(Integer.parseInt(id)); } catch (Exception e) { JSONObject j = new JSONObject(); JSONObject c = new JSONObject(); c.put("ct", "11"); c.put("cv", "22060004"); c.put("tmeAppID", "ztelite"); c.put("OpenUDID", "nouid"); c.put("uid", "3449496653"); j.put("comm", c); JSONObject r = new JSONObject(); r.put("module", "music.qqmusiclite.MtLimitFreeSvr"); r.put("method", "Obtain"); JSONObject p = new JSONObject(); JSONArray a = new JSONArray(); a.put(Integer.parseInt(id)); p.put("songid", a); p.put("need_ppurl", true); r.put("param", p); j.put("request", r); return j; } } public JSONObject CreateQQPpurlRequest(int id) { try { JSONObject j = new JSONObject(); JSONObject c = new JSONObject(); c.put("ct", "11"); c.put("cv", "22060004"); c.put("tmeAppID", "ztelite"); c.put("OpenUDID", "nouid"); c.put("uid", "3449496653"); j.put("comm", c); JSONObject r = new JSONObject(); r.put("module", "music.qqmusiclite.MtLimitFreeSvr"); r.put("method", "Obtain"); JSONObject p = new JSONObject(); JSONArray a = new JSONArray(); a.put(id); p.put("songid", a); p.put("need_ppurl", true); r.put("param", p); j.put("request", r); return j; } catch (Exception e) { return new JSONObject(); } } public JSONObject CreateGetVkeyRequest(String fn, String sm) { try { JSONObject j = new JSONObject(); JSONObject c = new JSONObject(); c.put("ct", "11"); c.put("cv", "22060004"); c.put("tmeAppID", "ztelite"); c.put("OpenUDID", "nouid"); c.put("uid", "3449496653"); j.put("comm", c); JSONObject r = new JSONObject(); r.put("module", "music.vkey.GetVkey"); r.put("method", "UrlGetVkey"); JSONObject p = new JSONObject(); p.put("guid", "Yun"); JSONArray ma = new JSONArray(); ma.put(sm); p.put("songmid", ma); JSONArray fa = new JSONArray(); fa.put("M500" + fn + ".mp3"); p.put("filename", fa); r.put("param", p); j.put("request", r); return j; } catch (Exception e) { return new JSONObject(); } } public JSONObject CreateQQVkeyRequest(String mid, String ppurl) { try { JSONObject j = new JSONObject(); JSONObject r = new JSONObject(); r.put("module", "music.vkey.GetVkey"); r.put("method", "CgiGetTempVkey"); JSONObject p = new JSONObject(); p.put("guid", "Yun"); JSONArray a = new JSONArray(); JSONObject o = new JSONObject(); o.put("mediamid", "Yun"); o.put("tempVkey", ppurl); o.put("songMID", mid); a.put(o); p.put("songlist", a); r.put("param", p); j.put("request", r); return j; } catch (Exception e) { return new JSONObject(); } } private final OkHttpClient client = new OkHttpClient.Builder() .connectTimeout(Duration.ofSeconds(30)) .callTimeout(Duration.ofSeconds(30)) .readTimeout(Duration.ofSeconds(30)) .build(); private void addH(Request.Builder b, Map h) { if (h != null) for (Object k : h.keySet()) b.addHeader((String) k, (String) h.get(k)); } private String exec(Request.Builder b) { Response r = null; try { r = client.newCall(b.build()).execute(); if (r.body() == null) return null; return r.body().string(); } catch (IOException e) { return null; } finally { try { if (r != null) r.close(); } catch (Exception e) {} } } public String get(String url, Map h) { Request.Builder b = new Request.Builder().url(url).get(); addH(b, h); return exec(b); } public String post(String url, String data, Map h) { String mt = (h != null && h.containsKey("Content-Type")) ? h.get("Content-Type").toString() : "application/json"; RequestBody body = RequestBody.create(MediaType.parse(mt), data); Request.Builder b = new Request.Builder().url(url).post(body); addH(b, h); return exec(b); } private static final String JP = pluginDir.getAbsolutePath(); private final Object fileLock = new Object(); public void NewFile(String p) { File f = new File(p); if (!f.exists()) { File pd = f.getParentFile(); if (pd != null && !pd.exists()) pd.mkdirs(); try { f.createNewFile(); } catch (IOException e) {} } } public String Read(String p) { synchronized (fileLock) { String t = ""; try { File f = new File(p); if (!f.exists()) return null; java.io.BufferedReader bf = new java.io.BufferedReader(new InputStreamReader(new FileInputStream(f), "UTF-8")); String s; while ((s = bf.readLine()) != null) t += "\n" + s; bf.close(); if (t.isEmpty()) return null; return t.substring(1); } catch (IOException e) { return null; } } } public void Write(String p, String d) { synchronized (fileLock) { try { File f = new File(p); if (!f.exists()) f.createNewFile(); FileOutputStream fos = new FileOutputStream(f); java.io.OutputStreamWriter osw = new java.io.OutputStreamWriter(fos, "UTF-8"); osw.write(d); osw.flush(); osw.close(); } catch (IOException e) {} } } private JSONObject getConfigJson(String name) { synchronized (fileLock) { String fp = JP + "/data/" + pluginId + "/" + name; String ud = Read(fp); try { return new JSONObject(ud == null ? "{}" : ud); } catch (Exception e) { return new JSONObject(); } } } private void saveConfigJson(String name, JSONObject json) { synchronized (fileLock) { String fp = JP + "/data/" + pluginId + "/" + name; Write(fp, json.toString()); } } public void putString(String n, String k, String v) { synchronized (fileLock) { try { JSONObject j = getConfigJson(n); j.put(k, v); saveConfigJson(n, j); Map cache = configCache.get(n); if (cache == null) { cache = new ConcurrentHashMap<>(); configCache.put(n, cache); } cache.put(k, v); } catch (Exception e) {} } } public String getString(String n, String k, String d) { Map cache = configCache.get(n); if (cache != null && cache.containsKey(k)) { Object v = cache.get(k); return v != null ? v.toString() : d; } synchronized (fileLock) { try { JSONObject j = getConfigJson(n); String v = j.optString(k, null); if (v != null) { if (cache == null) { cache = new ConcurrentHashMap<>(); configCache.put(n, cache); } cache.put(k, v); return v; } } catch (Exception e) {} return d; } } public void putInt(String n, String k, int v) { putString(n, k, String.valueOf(v)); } public int getInt(String n, String k, int d) { String s = getString(n, k, null); if (s == null) return d; try { return Integer.parseInt(s); } catch (NumberFormatException e) { return d; } } public void putBoolean(String k, boolean v) { putString("config", k, String.valueOf(v)); } public boolean getBoolean(String k, boolean d) { String s = getString("config", k, null); if (s == null) return d; return "true".equalsIgnoreCase(s); } public boolean isF(Object data) { boolean sw = getInt(data.talker, "开关", 0) == 1; boolean cc = isHalfMinute(data.createTime) || data.isSystem(); return sw ? cc : cc || !data.isSend(); } public boolean isHalfMinute(long ct) { return System.currentTimeMillis() - ct >= 30000; } private final int CP = Color.parseColor("#3F51B5"), CPD = Color.parseColor("#303F9F"), CBG = Color.parseColor("#F5F7FA"), CCARD = Color.parseColor("#FFFFFF"), CT1 = Color.parseColor("#212121"), CT2 = Color.parseColor("#757575"), CTH = Color.parseColor("#BDBDBD"); private int dp(int v) { return (int) (getTopActivity().getResources().getDisplayMetrics().density * v); } private LinearLayout card() { LinearLayout c = new LinearLayout(getTopActivity()); c.setOrientation(LinearLayout.VERTICAL); c.setPadding(dp(20), dp(20), dp(20), dp(20)); c.setBackgroundColor(CCARD); LinearLayout.LayoutParams lp = new LinearLayout.LayoutParams(-1, -2); lp.setMargins(0, 0, 0, dp(16)); c.setLayoutParams(lp); return c; } private TextView sec(String t) { TextView tv = new TextView(getTopActivity()); tv.setText(t); tv.setTextSize(16); tv.setTextColor(CP); tv.setTypeface(Typeface.DEFAULT_BOLD); tv.setPadding(0, 0, 0, dp(12)); return tv; } private EditText edt(String h, String v) { EditText et = new EditText(getTopActivity()); et.setHint(h); et.setText(v); et.setPadding(dp(16), dp(12), dp(16), dp(12)); et.setTextSize(14); et.setTextColor(CT1); et.setHintTextColor(CTH); et.setBackgroundColor(Color.parseColor("#F1F3F4")); et.setSingleLine(true); LinearLayout.LayoutParams lp = new LinearLayout.LayoutParams(-1, -2); lp.setMargins(0, dp(8), 0, dp(8)); et.setLayoutParams(lp); return et; } private Button pbtn(String t) { Button b = new Button(getTopActivity()); b.setText(t); b.setTextSize(14); b.setTextColor(Color.WHITE); b.setAllCaps(false); b.setPadding(dp(24), dp(12), dp(24), dp(12)); b.setBackgroundColor(CP); LinearLayout.LayoutParams lp = new LinearLayout.LayoutParams(-1, -2); lp.setMargins(0, dp(8), 0, dp(8)); b.setLayoutParams(lp); return b; } private Button sbtn(String t) { Button b = new Button(getTopActivity()); b.setText(t); b.setTextSize(13); b.setTextColor(CP); b.setAllCaps(false); b.setPadding(dp(16), dp(8), dp(16), dp(8)); b.setBackgroundColor(Color.parseColor("#E8F0FE")); LinearLayout.LayoutParams lp = new LinearLayout.LayoutParams(-2, -2); lp.setMargins(dp(6), 0, dp(6), 0); b.setLayoutParams(lp); return b; } private void updateSrcBtns(Button bNE, Button bQQ, Button bQS, TextView hint) { String tk = getTargetTalker(); String s = getSource(tk); if ("netease".equals(s)) { bNE.setBackgroundColor(CP); bNE.setTextColor(Color.WHITE); bQQ.setBackgroundColor(Color.parseColor("#E8ECF0")); bQQ.setTextColor(CT1); bQS.setBackgroundColor(Color.parseColor("#E8ECF0")); bQS.setTextColor(CT1); } else if ("qq".equals(s)) { bNE.setBackgroundColor(Color.parseColor("#E8ECF0")); bNE.setTextColor(CT1); bQQ.setBackgroundColor(CP); bQQ.setTextColor(Color.WHITE); bQS.setBackgroundColor(Color.parseColor("#E8ECF0")); bQS.setTextColor(CT1); } else { bNE.setBackgroundColor(Color.parseColor("#E8ECF0")); bNE.setTextColor(CT1); bQQ.setBackgroundColor(Color.parseColor("#E8ECF0")); bQQ.setTextColor(CT1); bQS.setBackgroundColor(CP); bQS.setTextColor(Color.WHITE); } hint.setText("当前: " + srcName(tk)); } private void settings() { try { final android.app.Activity ctx = getTopActivity(); final Dialog dlg = new Dialog(ctx, android.R.style.Theme_Translucent_NoTitleBar); dlg.requestWindowFeature(android.view.Window.FEATURE_NO_TITLE); dlg.setCancelable(true); final String tk = getTargetTalker(); final boolean isGrp = isGroup(tk); LinearLayout out = new LinearLayout(ctx); out.setOrientation(LinearLayout.VERTICAL); out.setBackgroundColor(CBG); out.setPadding(0, dp(40), 0, 0); LinearLayout tb = new LinearLayout(ctx); tb.setOrientation(LinearLayout.HORIZONTAL); tb.setGravity(Gravity.CENTER_VERTICAL); tb.setPadding(dp(24), dp(12), dp(24), dp(16)); tb.setBackgroundColor(CBG); TextView tt = new TextView(ctx); tt.setText("✨ 点歌助手"); tt.setTextSize(18); tt.setTypeface(Typeface.DEFAULT_BOLD); tt.setTextColor(CP); tt.setLayoutParams(new LinearLayout.LayoutParams(0, -2, 1f)); tb.addView(tt); TextView ct = new TextView(ctx); ct.setText("✕"); ct.setTextSize(22); ct.setTextColor(CT2); ct.setPadding(dp(12), dp(4), dp(4), dp(4)); ct.setOnClickListener(new View.OnClickListener() { public void onClick(View v) { dlg.dismiss(); } }); tb.addView(ct); out.addView(tb); ScrollView sv = new ScrollView(ctx); sv.setBackgroundColor(CBG); LinearLayout root = new LinearLayout(ctx); root.setOrientation(LinearLayout.VERTICAL); root.setPadding(dp(20), 0, dp(20), dp(20)); sv.addView(root); // ========== 控制模块 ========== LinearLayout cCtrl = card(); cCtrl.addView(sec("🛠️ 控制模块")); addSw(cCtrl, "音乐源按聊天隔离", K_ISOLATE, "开启后每个聊天独立记忆音乐源,关闭则全局共享"); root.addView(cCtrl); // ========== 音乐源 ========== 顺序:网易云 → QQ音乐 → 汽水音乐 LinearLayout cSrc = card(); if (isGrp) { cSrc.addView(sec("🎶 音乐源 — 群聊聚合")); TextView aggHint = new TextView(ctx); aggHint.setText("🔀 群聊自动聚合三源搜索,按源分段展示"); aggHint.setTextSize(12); aggHint.setTextColor(Color.parseColor("#FF6D00")); aggHint.setPadding(dp(8), dp(8), dp(8), dp(8)); cSrc.addView(aggHint); } else { cSrc.addView(sec("🎶 音乐源")); LinearLayout srcRow = new LinearLayout(ctx); srcRow.setOrientation(LinearLayout.HORIZONTAL); srcRow.setGravity(Gravity.CENTER); final Button btnNE = new Button(ctx); btnNE.setText("☁️ 网易云"); btnNE.setTextSize(13); btnNE.setAllCaps(false); btnNE.setPadding(dp(12), dp(10), dp(12), dp(10)); final Button btnQQ = new Button(ctx); btnQQ.setText("🎵 QQ音乐"); btnQQ.setTextSize(13); btnQQ.setAllCaps(false); btnQQ.setPadding(dp(12), dp(10), dp(12), dp(10)); final Button btnQS = new Button(ctx); btnQS.setText("🫧 汽水音乐"); btnQS.setTextSize(13); btnQS.setAllCaps(false); btnQS.setPadding(dp(12), dp(10), dp(12), dp(10)); LinearLayout.LayoutParams blp = new LinearLayout.LayoutParams(0, -2, 1f); blp.setMargins(dp(4), 0, dp(4), 0); btnNE.setLayoutParams(blp); btnQQ.setLayoutParams(blp); btnQS.setLayoutParams(blp); srcRow.addView(btnNE); srcRow.addView(btnQQ); srcRow.addView(btnQS); final TextView srcHint = new TextView(ctx); srcHint.setTextSize(11); srcHint.setTextColor(CT2); srcHint.setPadding(dp(8), dp(8), dp(8), 0); updateSrcBtns(btnNE, btnQQ, btnQS, srcHint); btnNE.setOnClickListener(new View.OnClickListener() { public void onClick(View v) { setSource(tk, "netease"); updateSrcBtns(btnNE, btnQQ, btnQS, srcHint); toast("☁️ 网易云"); } }); btnQQ.setOnClickListener(new View.OnClickListener() { public void onClick(View v) { setSource(tk, "qq"); updateSrcBtns(btnNE, btnQQ, btnQS, srcHint); toast("🎵 QQ音乐"); } }); btnQS.setOnClickListener(new View.OnClickListener() { public void onClick(View v) { setSource(tk, "qishui"); updateSrcBtns(btnNE, btnQQ, btnQS, srcHint); toast("🫧 汽水音乐"); } }); cSrc.addView(srcRow); cSrc.addView(srcHint); TextView srcDesc = new TextView(ctx); srcDesc.setText("💡 选择默认使用的音乐源(仅对私聊生效,群聊自动聚合)"); srcDesc.setTextSize(11); srcDesc.setTextColor(CT2); srcDesc.setPadding(dp(8), dp(4), dp(8), dp(4)); cSrc.addView(srcDesc); } root.addView(cSrc); // ========== 各源搜索数量 ========== 顺序:网易云 → QQ音乐 → 汽水音乐 LinearLayout cCount = card(); cCount.addView(sec("🔢 各源搜索数量")); TextView countTitle = new TextView(ctx); countTitle.setText("💡 分别控制网易云、QQ音乐、汽水音乐在私聊时的搜索数量"); countTitle.setTextSize(11); countTitle.setTextColor(Color.parseColor("#FF6D00")); countTitle.setPadding(dp(8), dp(4), dp(8), dp(4)); cCount.addView(countTitle); final EditText eNeteaseCount = edt("网易云 (默认10)", getString("config", K_CNT_NETEASE, "10")); cCount.addView(eNeteaseCount); final EditText eQQCount = edt("QQ音乐 (默认10)", getString("config", K_CNT_QQ, "10")); cCount.addView(eQQCount); final EditText eQishuiCount = edt("汽水音乐 (默认10)", getString("config", K_CNT_QISHUI, "10")); cCount.addView(eQishuiCount); TextView countHint = new TextView(ctx); countHint.setText("📌 范围1-30,私聊使用各自数量,群聊聚合使用下方统一数量"); countHint.setTextSize(11); countHint.setTextColor(CT2); countHint.setPadding(dp(8), dp(4), dp(8), dp(4)); cCount.addView(countHint); root.addView(cCount); // ========== 聚合搜索数量 ========== LinearLayout cAgg = card(); cAgg.addView(sec("🔀 聚合搜索数量")); TextView aggTitle = new TextView(ctx); aggTitle.setText("💡 控制群聊三源聚合时,每个源取多少首歌"); aggTitle.setTextSize(11); aggTitle.setTextColor(Color.parseColor("#FF6D00")); aggTitle.setPadding(dp(8), dp(4), dp(8), dp(4)); cAgg.addView(aggTitle); final EditText eAggCount = edt("每源数量 (默认15)", getString("config", K_CNT_AGGREGATE, "15")); cAgg.addView(eAggCount); TextView aggHint = new TextView(ctx); aggHint.setText("📌 范围1-30,仅对群聊聚合搜索生效"); aggHint.setTextSize(11); aggHint.setTextColor(CT2); aggHint.setPadding(dp(8), dp(4), dp(8), dp(4)); cAgg.addView(aggHint); root.addView(cAgg); // ========== 汽水黑名单 ========== LinearLayout cBL = card(); cBL.addView(sec("🚫 汽水黑名单")); TextView blInfo = new TextView(ctx); blInfo.setText("已过滤 " + qsBlacklist.size() + " 首歌曲"); blInfo.setTextSize(12); blInfo.setTextColor(CT2); blInfo.setPadding(dp(8), dp(8), dp(8), dp(4)); cBL.addView(blInfo); TextView blDesc = new TextView(ctx); blDesc.setText("💡 黑名单中的歌曲在汽水音乐搜索时会被自动过滤"); blDesc.setTextSize(11); blDesc.setTextColor(CT2); blDesc.setPadding(dp(8), dp(4), dp(8), dp(4)); cBL.addView(blDesc); Button btnClearBl = pbtn("🗑️ 清空黑名单"); btnClearBl.setOnClickListener(new View.OnClickListener() { public void onClick(View v) { qsBlacklist.clear(); saveBlacklist(); toast("✅ 已清空黑名单"); } }); cBL.addView(btnClearBl); root.addView(cBL); // ========== 发送模式 ========== LinearLayout c0 = card(); c0.addView(sec("📤 发送模式")); addSw(c0, "单发模式", K_MODE, "开启后搜索到歌曲直接发送,关闭后显示列表让您选择"); root.addView(c0); // ========== 搜索 ========== LinearLayout c1 = card(); c1.addView(sec("🔍 搜索")); addSw(c1, "单结果直发", K_AUTO, "仅当搜索结果只有1首时自动发送,无需手动选择"); root.addView(c1); // ========== 历史 ========== LinearLayout c2 = card(); c2.addView(sec("📜 历史")); int hc = history(tk != null ? tk : "").size(); TextView hh = new TextView(ctx); hh.setText("🎶 当前聊天已缓存 " + hc + " 首歌曲"); hh.setTextSize(13); hh.setTextColor(CT2); hh.setPadding(dp(8), dp(8), dp(8), dp(8)); c2.addView(hh); Button cb = pbtn("🗑️ 清空历史"); cb.setOnClickListener(new View.OnClickListener() { public void onClick(View v) { if (tk != null) history(tk).clear(); toast("✅ 已清空"); } }); c2.addView(cb); TextView histDesc = new TextView(ctx); histDesc.setText("💡 历史记录用于「重播」功能,最多保存200首"); histDesc.setTextSize(11); histDesc.setTextColor(CT2); histDesc.setPadding(dp(8), dp(4), dp(8), dp(4)); c2.addView(histDesc); root.addView(c2); // ========== 发送设置 ========== LinearLayout c3 = card(); c3.addView(sec("⚙️ 发送设置")); addSw(c3, "拦截自己", K_SELF, "开启后自己发送的点歌指令不会被触发"); addDiv(c3); addSw(c3, "封面→头像", "multi_qq_avatar", "开启后音乐卡片封面使用点歌人的微信头像"); addDiv(c3); addSw(c3, "歌手→昵称", "multi_qq_nickname", "开启后音乐卡片歌手名显示为点歌人的微信昵称"); root.addView(c3); // ========== 触发词 ========== LinearLayout c5 = card(); c5.addView(sec("📝 触发词")); final EditText eTrig = edt("逗号分隔,如:点歌,来首", getString("config", K_TRIG, D_TRIG)); c5.addView(eTrig); TextView trigDesc = new TextView(ctx); trigDesc.setText("💡 消息以这些词开头时触发点歌,如「点歌 十年」"); trigDesc.setTextSize(11); trigDesc.setTextColor(CT2); trigDesc.setPadding(dp(8), dp(4), dp(8), dp(4)); c5.addView(trigDesc); root.addView(c5); // ========== AppID ========== LinearLayout c6 = card(); c6.addView(sec("🆔 AppID")); final EditText eAppid = edt("留空使用默认", getString("config", K_APP, D_APP)); c6.addView(eAppid); TextView appDesc = new TextView(ctx); appDesc.setText("💡 一般情况下无需修改,用于分享音乐卡片"); appDesc.setTextSize(11); appDesc.setTextColor(CT2); appDesc.setPadding(dp(8), dp(4), dp(8), dp(4)); c6.addView(appDesc); root.addView(c6); // ========== 权限 ========== LinearLayout c7 = card(); c7.addView(sec("🛡️ 权限控制")); TextView permDesc = new TextView(ctx); permDesc.setText("💡 控制哪些好友/群聊可以使用点歌功能"); permDesc.setTextSize(11); permDesc.setTextColor(CT2); permDesc.setPadding(dp(8), dp(8), dp(8), dp(4)); c7.addView(permDesc); Button bf2 = pbtn("👤 好友权限"); Button bg2 = pbtn("🏠 群聊权限"); c7.addView(bf2); c7.addView(bg2); root.addView(c7); root.addView(new View(ctx), new LinearLayout.LayoutParams(-1, dp(8))); out.addView(sv, new LinearLayout.LayoutParams(-1, 0, 1f)); // ========== 底部按钮 ========== LinearLayout btns = new LinearLayout(ctx); btns.setOrientation(LinearLayout.HORIZONTAL); btns.setGravity(Gravity.CENTER); btns.setPadding(dp(20), dp(12), dp(20), dp(20)); btns.setBackgroundColor(CBG); Button cancel = sbtn("取消"); Button save = pbtn("💾 保存"); LinearLayout.LayoutParams blp2 = new LinearLayout.LayoutParams(0, -2, 1f); blp2.setMargins(dp(4), 0, dp(4), 0); cancel.setLayoutParams(blp2); save.setLayoutParams(blp2); btns.addView(cancel); btns.addView(save); out.addView(btns); cancel.setOnClickListener(new View.OnClickListener() { public void onClick(View v) { dlg.dismiss(); } }); save.setOnClickListener(new View.OnClickListener() { public void onClick(View v) { new Thread(new Runnable() { public void run() { try { putString("config", K_APP, TextUtils.isEmpty(eAppid.getText().toString()) ? D_APP : eAppid.getText().toString().trim()); putString("config", K_TRIG, TextUtils.isEmpty(eTrig.getText().toString()) ? D_TRIG : eTrig.getText().toString()); String neCnt = eNeteaseCount.getText().toString().trim(); if (!TextUtils.isEmpty(neCnt)) { try { int val = Integer.parseInt(neCnt); if (val >= 1 && val <= 30) putString("config", K_CNT_NETEASE, String.valueOf(val)); } catch (NumberFormatException e) {} } String qqCnt = eQQCount.getText().toString().trim(); if (!TextUtils.isEmpty(qqCnt)) { try { int val = Integer.parseInt(qqCnt); if (val >= 1 && val <= 30) putString("config", K_CNT_QQ, String.valueOf(val)); } catch (NumberFormatException e) {} } String qsCnt = eQishuiCount.getText().toString().trim(); if (!TextUtils.isEmpty(qsCnt)) { try { int val = Integer.parseInt(qsCnt); if (val >= 1 && val <= 30) putString("config", K_CNT_QISHUI, String.valueOf(val)); } catch (NumberFormatException e) {} } String aggCnt = eAggCount.getText().toString().trim(); if (!TextUtils.isEmpty(aggCnt)) { try { int val = Integer.parseInt(aggCnt); if (val >= 1 && val <= 30) putString("config", K_CNT_AGGREGATE, String.valueOf(val)); } catch (NumberFormatException e) {} } new Handler(Looper.getMainLooper()).post(new Runnable() { public void run() { toast("✅ 已保存"); dlg.dismiss(); } }); } catch (Exception e) { new Handler(Looper.getMainLooper()).post(new Runnable() { public void run() { toast("❌ " + e.getMessage()); } }); } } }).start(); } }); bf2.setOnClickListener(new View.OnClickListener() { public void onClick(View v) { scope(true); } }); bg2.setOnClickListener(new View.OnClickListener() { public void onClick(View v) { scope(false); } }); dlg.setContentView(out); Window win = dlg.getWindow(); if (win != null) { win.setLayout(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT); win.setGravity(Gravity.CENTER); win.setDimAmount(0.25f); win.setBackgroundDrawable(new android.graphics.drawable.ColorDrawable(CBG)); } dlg.show(); } catch (Exception e) { toast("打开设置失败: " + e.getMessage()); } } private void scope(final boolean isF) { String title = isF ? "👤 好友开关" : "🏠 群聊开关"; loadDlg(title, "加载中...", new Runnable() { public void run() { if (isF) { if (fc == null) fc = getFriendList(); } else { if (gc == null) gc = getGroupList(); } new Handler(Looper.getMainLooper()).post(new Runnable() { public void run() { List raw = isF ? fc : gc; if (raw == null || raw.isEmpty()) return; List names = new ArrayList(), ids = new ArrayList(); final Set en = new HashSet(); for (int i = 0; i < raw.size(); i++) { String n = "", id = ""; if (isF) { FriendInfo f = (FriendInfo) raw.get(i); n = TextUtils.isEmpty(f.getNickname()) ? "未知" : f.getNickname(); if (!TextUtils.isEmpty(f.getRemark())) n += " (" + f.getRemark() + ")"; id = f.getWxid(); } else { GroupInfo g = (GroupInfo) raw.get(i); n = TextUtils.isEmpty(g.getName()) ? "未知群聊" : g.getName(); id = g.getRoomId(); } names.add((isF ? "👤 " : "🏠 ") + n + "\nID: " + id); ids.add(id); if (getInt(id, "开关", 0) == 1) en.add(id); } multiDlg(title, names, ids, en, "🔍 搜索...", new Runnable() { public void run() { new Thread(new Runnable() { public void run() { for (int i = 0; i < ids.size(); i++) { String cid = (String) ids.get(i); int ns = en.contains(cid) ? 1 : 0; if (ns != getInt(cid, "开关", 0)) putInt(cid, "开关", ns); } new Handler(Looper.getMainLooper()).post(new Runnable() { public void run() { toast("✅ 权限已保存"); } }); } }).start(); } }, null); } }); } }); } private void multiDlg(String title, List all, List ids, Set sel, String hint, final Runnable onOk, final Runnable onUpd) { try { final Set temp = new HashSet(sel); ScrollView sv = new ScrollView(getTopActivity()); sv.setBackgroundColor(CBG); LinearLayout main = new LinearLayout(getTopActivity()); main.setOrientation(LinearLayout.VERTICAL); main.setPadding(dp(20), dp(20), dp(20), dp(20)); sv.addView(main); final EditText se = edt(hint, ""); se.setSingleLine(true); main.addView(se); final ListView lv = new ListView(getTopActivity()); lv.setOnTouchListener(new View.OnTouchListener() { public boolean onTouch(View v, MotionEvent e) { int a = e.getAction(); if (a == MotionEvent.ACTION_DOWN) v.getParent().requestDisallowInterceptTouchEvent(true); else if (a == MotionEvent.ACTION_UP || a == MotionEvent.ACTION_CANCEL) v.getParent().requestDisallowInterceptTouchEvent(false); return false; } }); lv.setChoiceMode(ListView.CHOICE_MODE_MULTIPLE); lv.setLayoutParams(new LinearLayout.LayoutParams(-1, dp(350))); lv.setDivider(new android.graphics.drawable.ColorDrawable(Color.parseColor("#E0E4E8"))); lv.setDividerHeight(dp(1)); main.addView(lv); final List curI = new ArrayList(), curN = new ArrayList(); final Runnable up = new Runnable() { public void run() { String kw = se.getText().toString().toLowerCase(); curI.clear(); curN.clear(); for (int i = 0; i < all.size(); i++) { String id = (String) ids.get(i), nm = (String) all.get(i); if (kw.isEmpty() || nm.toLowerCase().contains(kw) || id.toLowerCase().contains(kw)) { curI.add(id); curN.add(nm); } } ArrayAdapter ad = new ArrayAdapter(getTopActivity(), android.R.layout.simple_list_item_multiple_choice, curN); lv.setAdapter(ad); lv.clearChoices(); for (int j = 0; j < curI.size(); j++) lv.setItemChecked(j, temp.contains(curI.get(j))); lv.getLayoutParams().height = curI.size() <= 0 ? dp(50) : Math.min(curI.size() * dp(56), dp(400)); lv.requestLayout(); if (onUpd != null) onUpd.run(); } }; lv.setOnItemClickListener(new AdapterView.OnItemClickListener() { public void onItemClick(AdapterView p, View v, int pos, long id) { String s = (String) curI.get(pos); if (lv.isItemChecked(pos)) temp.add(s); else temp.remove(s); if (onUpd != null) onUpd.run(); } }); Handler sh = new Handler(Looper.getMainLooper()); final Runnable sr = new Runnable() { public void run() { up.run(); } }; se.addTextChangedListener(new TextWatcher() { public void beforeTextChanged(CharSequence s, int st, int c, int a) {} public void onTextChanged(CharSequence s, int st, int b, int c) { sh.removeCallbacks(sr); } public void afterTextChanged(Editable s) { sh.postDelayed(sr, 300); } }); AlertDialog.Builder bld = new AlertDialog.Builder(getTopActivity()) .setTitle(title) .setView(sv) .setPositiveButton("✅ 保存", null) .setNegativeButton("❌ 取消", null) .setNeutralButton("全选", null); final AlertDialog dlg = bld.create(); dlg.setOnShowListener(new DialogInterface.OnShowListener() { public void onShow(DialogInterface d) { Button pos = dlg.getButton(AlertDialog.BUTTON_POSITIVE); if (pos != null) pos.setOnClickListener(new View.OnClickListener() { public void onClick(View v) { sel.clear(); sel.addAll(temp); if (onOk != null) onOk.run(); dlg.dismiss(); } }); Button neg = dlg.getButton(AlertDialog.BUTTON_NEGATIVE); if (neg != null) neg.setOnClickListener(new View.OnClickListener() { public void onClick(View v) { dlg.dismiss(); } }); Button nb = dlg.getButton(AlertDialog.BUTTON_NEUTRAL); if (nb != null) nb.setOnClickListener(new View.OnClickListener() { public void onClick(View v) { boolean allC = true; for (int i = 0; i < curI.size(); i++) { if (!temp.contains(curI.get(i))) { allC = false; break; } } if (allC) { for (int i = 0; i < curI.size(); i++) { temp.remove(curI.get(i)); lv.setItemChecked(i, false); } nb.setText("全选"); } else { for (int i = 0; i < curI.size(); i++) { temp.add((String) curI.get(i)); lv.setItemChecked(i, true); } nb.setText("取消全选"); } } }); } }); dlg.show(); up.run(); } catch (Exception e) {} } private void addSw(LinearLayout p, String t, final String k, String d) { boolean en = k.equals(K_MODE) ? (getInt("config", K_MODE, 1) == 1) : getBoolean(k, false); LinearLayout row = new LinearLayout(getTopActivity()); row.setOrientation(LinearLayout.HORIZONTAL); row.setGravity(Gravity.CENTER_VERTICAL); row.setPadding(dp(12), dp(12), dp(12), dp(12)); row.setBackgroundColor(Color.parseColor("#F8F9FA")); TextView tv = new TextView(getTopActivity()); tv.setText(t); tv.setTextSize(14); tv.setTextColor(CT1); tv.setLayoutParams(new LinearLayout.LayoutParams(0, -2, 1f)); CheckBox cb = new CheckBox(getTopActivity()); cb.setChecked(en); cb.setScaleX(0.9f); cb.setScaleY(0.9f); row.addView(tv); row.addView(cb); row.setOnClickListener(new View.OnClickListener() { public void onClick(View v) { cb.setChecked(!cb.isChecked()); if (k.equals(K_MODE)) putInt("config", K_MODE, cb.isChecked() ? 1 : 0); else putBoolean(k, cb.isChecked()); } }); LinearLayout.LayoutParams rlp = new LinearLayout.LayoutParams(-1, -2); rlp.setMargins(0, dp(4), 0, dp(4)); row.setLayoutParams(rlp); p.addView(row); if (d != null) { TextView dv = new TextView(getTopActivity()); dv.setText(d); dv.setTextSize(12); dv.setTextColor(CT2); dv.setPadding(dp(12), 0, dp(12), dp(4)); p.addView(dv); } } private void addDiv(LinearLayout p) { View v = new View(getTopActivity()); v.setBackgroundColor(Color.parseColor("#E8ECF0")); v.setLayoutParams(new LinearLayout.LayoutParams(-1, dp(1))); p.addView(v); } private void loadDlg(String title, String msg, final Runnable task) { LinearLayout lo = new LinearLayout(getTopActivity()); lo.setOrientation(LinearLayout.HORIZONTAL); lo.setPadding(dp(40), dp(40), dp(40), dp(40)); lo.setGravity(Gravity.CENTER_VERTICAL); lo.addView(new ProgressBar(getTopActivity())); TextView tv = new TextView(getTopActivity()); tv.setText(msg); tv.setTextSize(14); tv.setTextColor(CT1); tv.setPadding(dp(16), 0, 0, 0); lo.addView(tv); final AlertDialog ld = new AlertDialog.Builder(getTopActivity()) .setTitle(title) .setView(lo) .setNegativeButton("❌ 取消", new DialogInterface.OnClickListener() { public void onClick(DialogInterface d, int w) { d.dismiss(); } }) .create(); ld.setCancelable(false); ld.show(); new Thread(new Runnable() { public void run() { try { task.run(); } finally { new Handler(Looper.getMainLooper()).post(new Runnable() { public void run() { ld.dismiss(); } }); } } }).start(); }