// ==UserScript==
// @name MWI Watch Market - 奶牛的市场关注监视
// @namespace http://tampermonkey.net/
// @version test0.0.11
// @description 监视下心怡物品的当前价格,还有1day和3day的数据,数据采集于MWIAPI,1day,3day数据为自己生成,没找到获取强化装备市场数据的地方,所以无法监控强化装备市场,如果有误或者有问题可以在MWIItemWatchData的仓库下给我留言
// @author lzy
// @license MIT
// @match https://www.milkywayidle.com/*
// @grant GM_addStyle
// @downloadURL https://update.greasyfork.icu/scripts/535364/MWI%20Watch%20Market%20-%20%E5%A5%B6%E7%89%9B%E7%9A%84%E5%B8%82%E5%9C%BA%E5%85%B3%E6%B3%A8%E7%9B%91%E8%A7%86.user.js
// @updateURL https://update.greasyfork.icu/scripts/535364/MWI%20Watch%20Market%20-%20%E5%A5%B6%E7%89%9B%E7%9A%84%E5%B8%82%E5%9C%BA%E5%85%B3%E6%B3%A8%E7%9B%91%E8%A7%86.meta.js
// ==/UserScript==
(function () {
"use strict";
let market; //当前的市场数据
let itemCNname; //物品的翻译数据
let db; //indexedDB用于保存一些用户的历史数据
const WatchStoreName = "watch";
const LogStoreName = "log";
const isOnline = true; //在线获取数据开关,频繁获取github容易被ban
const ShowButtonId = "mkWatchButton", //显示按钮
CleanButtonId = "mkWatchCleanButton", //清除按钮
RefreshButtonId = "mkWatchRefreshButton", //刷新按钮
HideButtonId = "mkWatchHideButton", //隐藏按钮
BoxClass = "mkWatchBox", //主体盒子
HeaderClass = "mkWatchBox_Header",
HeaderLabelClass = "mkWatchBox_Header_Label", //标题
HeaderActionClass = "mkWatchBox_Header_Action", //操作按钮
ActionClass = "mkWatchBox_Action", //操作按钮
BoxContainerClass = "mkWatchBox_ItemsWatchContainer", //主体容器
ItemsClass = "mkWatchBox_Items", //物品容器
ItemsAddInputId = "mkWatchBox_Items_Input_Add", //物品输入框
ItemsAddSelectId = "mkWatchBox_Items_Select_Add", //物品选择框
ItemsRemoveInputId = "mkWatchBox_Items_Input_Remove", //物品输入框
ItemsRemoveSelectId = "mkWatchBox_Items_Select_Remove", //物品选择框
ItemsAddClass = "mkWatchBox_Items_Add", //物品容器
ItemsRemoveClass = "mkWatchBox_Items_Remove", //物品容器
ItemNameClass = "mkWatchBox_Items_Name", //物品名称
ItemAskClass = "mkWatchBox_Items_Ask", //物品出售价格
ItemBidClass = "mkWatchBox_Items_Bid", //物品收购价格
ItemGroupClass = "mkWatchBox_Items_Group"; //物品收购价格
let saveedItems = []; //保存的物品列表
let data24h, data3day; //24小时和3天的历史价格数据
function init() {
const p1 = getMarkets();
const p2 = initIndexedDb();
const p3 = getItemsList();
const p4 = getItemLogMarkets();
Promise.all([p1, p2, p3, p4]).then((res) => {
console.info("数据获取完毕");
initHtml();
});
}
async function getMarkets() {
//获取当前数据
try {
let res;
if (isOnline) {
res = await fetch(
"https://raw.githubusercontent.com/holychikenz/MWIApi/main/milkyapi.json",
); //在线数据
} else {
res = await fetch("./milkyapi.json"); //本地测试数据
}
const data = await res.json();
market = data.market;
} catch (err) {
market = null;
console.error("获取市场数据失败");
}
return market;
}
async function getItemLogMarkets() {
//获取处理过后的一个历史价格数据
try {
if (isOnline) {
let res24h = await fetch(
"https://happyplum.github.io/MWIItemWatchData/1days.json",
);
data24h = await res24h.json();
let res3day = await fetch(
"https://happyplum.github.io/MWIItemWatchData/3days.json",
);
data3day = await res3day.json();
} else {
let res24h = await fetch("./node-getMWIData/dist/1days.json");
data24h = await res24h.json();
let res3day = await fetch("./node-getMWIData/dist/3days.json");
data3day = await res3day.json();
}
} catch (err) {}
}
function initIndexedDb() {
return new Promise((resolve, reject) => {
const req = indexedDB.open("milk", 1);
req.onerror = (err) => {
console.error("不支持indexedDB?", err);
reject(err);
};
req.onsuccess = (event) => {
db = event.target.result;
db.setData = setData;
db.getData = getData;
db.getAllData = getAllData;
resolve(db);
};
req.onupgradeneeded = (event) => {
const db = event.target.result;
if (!db.objectStoreNames.contains(WatchStoreName)) {
db.createObjectStore(WatchStoreName, { autoIncrement: true });
}
if (!db.objectStoreNames.contains(LogStoreName)) {
db.createObjectStore(LogStoreName, { autoIncrement: true });
}
};
});
}
async function setData(key, value = {}, tableName = WatchStoreName) {
if (!db) await initIndexedDb();
return new Promise((resolve, reject) => {
try {
if (!db.objectStoreNames.contains(tableName)) {
reject(new Error(`Object store "${tableName}" not found`));
return;
}
const transaction = db.transaction(tableName, "readwrite");
const request = transaction
.objectStore(tableName)
.put({ key, ...value }, key);
request.onsuccess = () => {
resolve();
};
request.onerror = (error) => {
reject(error);
};
transaction.onerror = (error) => {
reject(error);
};
} catch (error) {
reject(error);
}
});
}
async function getData(key, tableName = WatchStoreName) {
if (!db) await initIndexedDb();
return new Promise((resolve, reject) => {
const getRequest = db
.transaction(tableName, "readonly")
.objectStore(tableName)
.get(key);
getRequest.onsuccess = (event) => {
resolve(event.target.result);
};
getRequest.onerror = (error) => {
reject(error);
};
});
}
async function delData(key, tableName = WatchStoreName) {
if (!db) await initIndexedDb();
return new Promise((resolve, reject) => {
const getRequest = db
.transaction(tableName, "readwrite")
.objectStore(tableName)
.delete(key);
getRequest.onsuccess = (event) => {
resolve(event.target.result);
};
getRequest.onerror = (error) => {
reject(error);
};
});
}
async function cleanData(tableName = WatchStoreName) {
if (!db) await initIndexedDb();
return new Promise((resolve, reject) => {
try {
if (!db.objectStoreNames.contains(tableName)) {
reject(new Error(`Object store "${tableName}" not found`));
return;
}
const transaction = db.transaction(tableName, "readwrite");
transaction.objectStore(tableName).clear();
transaction.oncomplete = () => {
resolve();
};
transaction.onerror = (error) => {
reject(error);
};
} catch (error) {
reject(error);
}
});
}
async function getAllData(tableName = WatchStoreName) {
if (!db) await initIndexedDb();
return new Promise((resolve, reject) => {
const getRequest = db
.transaction(tableName, "readonly")
.objectStore(tableName)
.getAll();
getRequest.onsuccess = (event) => {
resolve(event.target.result);
};
getRequest.onerror = (error) => {
reject(error);
};
});
}
async function getItemsList() {
let res;
if (isOnline) {
res = await fetch(
"https://happyplum.github.io/MWIItemWatchData/items.json",
);
} else {
res = await fetch("./node-getMWIData/dist/items.json");
}
const data = await res.json();
itemCNname = data;
}
function getCNName(key) {
const itemKey = key
.toLocaleLowerCase()
.replace(/'/g, "")
.replace(/ /g, "_");
return itemCNname[`/items/${itemKey}`];
}
function createHtml() {
const html = `
`;
return html;
}
async function initHtml() {
if (!document.body) {
//如果body不存在,可能html还没绘制完毕,延迟1秒再执行
return setTimeout(initHtml, 1000);
}
//插入主体
const abody = createHtml();
document.body.insertAdjacentHTML("beforeend", abody);
//插入占位按钮,还没想好用什么图标,先放个按钮,用于显示框架
const aicon = ``;
document.body.insertAdjacentHTML("beforeend", aicon);
//添加下拉选框
refreshItems();
//绑定框体事件
bindButtonListener();
}
function bindButtonListener() {
//外框体事件
const showbutton = document.getElementById(ShowButtonId);
showbutton.addEventListener("click", showOrHideBox);
const hidebutton = document.getElementById(HideButtonId);
hidebutton.addEventListener("click", HideBox);
const refreshbutton = document.getElementById(RefreshButtonId);
refreshbutton.addEventListener("click", refresh);
const cleanbutton = document.getElementById(CleanButtonId);
cleanbutton.addEventListener("click", clean);
const headerLabel = document.querySelector(`.${HeaderLabelClass}`);
headerLabel.addEventListener("mousedown", moveBox);
//添加删除事件
const addSearch = document.querySelector(`#${ItemsAddInputId}`);
addSearch.addEventListener("input", searchAddItem);
const addbutton = document.querySelector(`.${ItemsAddClass}`);
addbutton.addEventListener("click", addWatchItem);
const removeSearch = document.querySelector(`#${ItemsRemoveInputId}`);
removeSearch.addEventListener("input", searchRemoveItem);
const removebutton = document.querySelector(`.${ItemsRemoveClass}`);
removebutton.addEventListener("click", removeWatchItem);
}
function moveBox(e) {
//拖动逻辑
const box = document.querySelector(`.${BoxClass}`);
let x = e.clientX;
let y = e.clientY;
document.onmousemove = function (e) {
let nowX = e.clientX;
let nowY = e.clientY;
let disX = nowX - x;
let disY = nowY - y;
box.style.left = `${box.offsetLeft + disX}px`;
box.style.top = `${box.offsetTop + disY}px`;
x = nowX;
y = nowY;
};
document.onmouseup = function () {
document.onmousemove = null;
document.onmouseup = null;
};
}
function showOrHideBox() {
const box = document.querySelector(`.${BoxClass}`);
if (box.style.display === "none") {
ShowBox();
} else {
HideBox();
}
}
function ShowBox() {
const box = document.querySelector(`.${BoxClass}`);
box.style.display = "block";
}
function HideBox() {
const box = document.querySelector(`.${BoxClass}`);
box.style.display = "none";
}
let reing = false;
async function refresh() {
if (reing) {
//给RefreshButtonId增加reloading的class
alert("最近刷新过了,10秒后可再刷新");
return;
}
reing = true;
const refreshbutton = document.getElementById(RefreshButtonId);
refreshbutton.classList.add("reloading");
//清空容器,来增加个过度的感觉,不然感觉反馈不太明显
const container = document.querySelector(`.${BoxContainerClass}`);
container.innerHTML = "";
//刷新逻辑,需要重新获取价格数据,然后重新绘制items
const p1 = getMarkets();
const p2 = getItemLogMarkets();
Promise.all([p1, p2]).then((res) => {
refreshItems();
console.info("刷新完毕");
setTimeout(() => {
reing = false;
refreshbutton.classList.remove("reloading");
}, 10 * 1000);
});
}
function clean() {
cleanData(WatchStoreName);
cleanData(LogStoreName);
refreshItems();
}
let addList = []; //添加的下拉列表
let removeList = []; //删除的下拉列表
async function genSelectOptions() {
//根据market生成select选项,显示需要转换成中文,value为key
//分为3类,添加,删除,未知3类分批,未知类型不需要添加到select中,但是需要打印用来标注
if (!market) return;
saveedItems = await getAllData();
addList = [];
removeList = [];
Object.keys(market).forEach((key) => {
let value = getCNName(key);
if (!value) {
console.log(`没有找到${key}的翻译`);
return;
}
const option = { value: key, text: value };
if (saveedItems.find((item) => item.key === key)) {
removeList.push(option);
} else {
addList.push(option);
}
});
}
let filterAddList = [];
function searchAddItem(e) {
const str = e.target.value;
filterAddList = addList.filter((item) => {
return item.text.includes(str);
});
renderAddSelectOption();
}
let filterRemoveList = [];
function searchRemoveItem(e) {
const str = e.target.value;
filterRemoveList = removeList.filter((item) => {
return item.text.includes(str);
});
renderRemoveSelectOption();
}
function renderAddSelectOption() {
//添加下拉相关
const addSelect = document.querySelector(`#${ItemsAddSelectId}`);
addSelect.innerHTML = "";
const list = filterAddList.length > 0 ? filterAddList : addList;
list.forEach((item) => {
const option = document.createElement("option");
option.value = item.value;
option.text = item.text;
addSelect.add(option);
});
}
function renderRemoveSelectOption() {
//删除下拉相关
const removeSelect = document.querySelector(`#${ItemsRemoveSelectId}`);
removeSelect.innerHTML = "";
const list = filterRemoveList.length > 0 ? filterRemoveList : removeList;
list.forEach((item) => {
const option = document.createElement("option");
option.value = item.value;
option.text = item.text;
removeSelect.add(option);
});
}
async function addWatchItem() {
//获取当前select的值
const select = document.querySelector(`#${ItemsAddSelectId}`);
const key = select.value;
if (!key) return;
await setData(key, { index: 0, ae: -1 });
refreshItems(); //添加完毕后要刷新下
}
async function removeWatchItem() {
const select = document.querySelector(`#${ItemsRemoveSelectId}`);
const key = select.value;
if (!key) return;
await delData(key);
refreshItems(); //添加完毕后要刷新下
}
async function refreshItems() {
await genSelectOptions();
renderAddSelectOption();
renderRemoveSelectOption();
renderItems();
}
async function renderItems() {
//清空容器,可能和refresh阶段有点重复,放着反正也没事
const container = document.querySelector(`.${BoxContainerClass}`);
container.innerHTML = "";
if (!saveedItems) saveedItems = await getAllData();
if (saveedItems.length === 0) return;
//首先获取监听物品
const watchItems = saveedItems
.filter((item) => item.index !== -1)
.sort((a, b) => b.index - a.index);
watchItems.forEach((item) => {
const html = getItemHtml(item);
container.insertAdjacentHTML("beforeend", html);
});
}
function formatNum(num) {
const number = Number(num);
if (number > 1000000) {
return (number / 1000000).toFixed(1) + "M";
}
if (number > 1000) {
return (number / 1000).toFixed(1) + "K";
}
return number.toFixed(0) + "";
}
function getItemHtml(item) {
const key = item.key;
const name = getCNName(key);
return createItem(key, name);
}
function getslope(slope) {
if (slope > 0) {
return `↑`;
} else if (slope < 0) {
return `↓`;
}
return `→`;
}
function createItem(key, name) {
const m = market[key];
const oneDay = data24h[key];
const threeDay = data3day[key];
const html = `
${name}
ask:${formatNum(m.ask)}
bid:${formatNum(m.bid)}
1day ${getslope(oneDay.slope)}
avgCom:${formatNum(
oneDay.avgCombined,
)}
maxAsk:${formatNum(
oneDay.maxAsk,
)}
avgAsk:${formatNum(
oneDay.avgAsk,
)}
minAsk:${formatNum(
oneDay.minAsk,
)}
maxBid:${formatNum(
oneDay.maxBid,
)}
avgBid:${formatNum(
oneDay.avgBid,
)}
minBid:${formatNum(
oneDay.minBid,
)}
3day ${getslope(threeDay.slope)}
avgCom:${formatNum(
threeDay.avgCombined,
)}
maxAsk:${formatNum(
threeDay.maxAsk,
)}
avgAsk:${formatNum(threeDay.avgAsk)}
minAsk:${formatNum(
threeDay.minAsk,
)}
maxBid:${formatNum(
threeDay.maxBid,
)}
avgBid:${formatNum(threeDay.avgBid)}
minBid:${formatNum(
threeDay.minBid,
)}
`;
return html;
}
function addClass() {
let modelStyle = `
#mkWatchButton {
position: absolute;
right: 300px;
top: 40px;
width: 20px;
height: 20px;
cursor: pointer;
}
#mkWatchButton svg{
fill: #faa21e;
width: 20px;
height: 20px;
}
.mkWatchBox {
position: absolute;
z-index:1;
right: 240px;
top: 100px;
width: 380px;
min-height: 200px;
padding: 10px;
background: #033963;
border: #74b9ff solid 1px;
border-radius: 8px;
color: #fff;
}
.mkWatchBox .mkWatchBox_Header {
font-size: 18px;
font-weight: bold;
margin-bottom: 10px;
display: flex;
justify-content: flex-end;
}
.mkWatchBox .mkWatchBox_Header .mkWatchBox_Header_Label {
flex: 1;
user-select: none;
cursor: move;
}
.mkWatchBox .mkWatchBox_Header .mkWatchBox_Header_Action {
display: flex;
}
.mkWatchBox_ItemsWatchContainer {
display: flex;
min-height: 88px;
flex-wrap: wrap;
overflow: auto;
max-height: 440px;
}
.mkWatchBox_ItemsWatchContainer .mkWatchBox_Items {
height: 430px;
width: 100px;
border: #fff solid 1px;
border-radius: 3px;
padding: 4px;
margin: 4px;
font-size: 12px;
}
#mkWatchBox_Items_Input_Add,
#mkWatchBox_Items_Input_Remove {
width: 100px;
}
#mkWatchBox_Items_Select_Add,
#mkWatchBox_Items_Select_Remove {
width: 200px;
margin-left: 4px;
}
.mkWatchBox_Items {
text-align: center;
}
.mkWatchBox_Items_Group {
border: 1px solid #74b9ff;
padding: 4px;
margin: 4px 0px;
}
#mkWatchCleanButton,
#mkWatchRefreshButton,
#mkWatchHideButton {
cursor: pointer;
margin: 0px 4px;
}
#mkWatchCleanButton svg,
#mkWatchRefreshButton svg,
#mkWatchHideButton svg {
fill: #fff;
width: 20px;
height: 20px;
}
.mkWatchBox_Items_Add,
.mkWatchBox_Items_Remove {
cursor: pointer;
}
.mkWatchBox_Items_Add svg,
.mkWatchBox_Items_Remove svg {
width: 20px;
height: 20px;
fill: #fff;
margin: 0px 4px;
}
.mkWatchBox_Action {
display: flex;
}
.reloading#mkWatchRefreshButton svg {
fill: #aaa;
cursor: not-allowed;
}`;
try {
GM_addStyle(modelStyle);
} catch (err) {}
}
addClass();
init();
})();