Ostatnio aktywny 1783455382

Fogest zrewidował ten Gist 1783455382. Przejdź do rewizji

1 file changed, 0 insertions, 0 deletions

batch balance (fixed) zmieniono nazwę na batch balance.user.js

Zmieniono nazwę pliku bez modyfikacji zawartości

Fogest zrewidował ten Gist 1783455354. Przejdź do rewizji

Brak zmian

Fogest zrewidował ten Gist 1778885832. Przejdź do rewizji

1 file changed, 486 insertions

batch balance (fixed) (stworzono plik)

@@ -0,0 +1,486 @@
1 + // ==UserScript==
2 + // @name Batch Balance
3 + // @namespace https://github.com/tobytorn
4 + // @description Distribute money or change balances for multiple faction members (Not supported on mobile)
5 + // @author tobytorn [1617955]
6 + // @match https://www.torn.com/factions.php?step=your*
7 + // @version 2.0.2
8 + // @grant GM_getValue
9 + // @grant GM_setValue
10 + // @grant GM_addStyle
11 + // @supportURL https://github.com/tobytorn/batch-balance
12 + // @license MIT
13 + // @require https://unpkg.com/[email protected]/dist/jquery.min.js
14 + // @downloadURL https://update.greasyfork.org/scripts/536376/Batch%20Balance.user.js
15 + // @updateURL https://update.greasyfork.org/scripts/536376/Batch%20Balance.meta.js
16 + // ==/UserScript==
17 +
18 + // Usage:
19 + // Add the following parameters to the URL of the control page (https://www.torn.com/factions.php?step=your#/tab=controls) to enable this script
20 + // batbal_uids Comma-separated user IDs
21 + // batbal_amounts Comma-separated amounts
22 + // batbal_action [Optional] "add" for adding to balance (default), or "give" for giving money
23 + // batbal_asset [Optional] "money" (default) or "points"
24 + //
25 + // Example: The following URL will add 120 to Leslie, subtract 250 from tobytorn, and add 1.5k to Duke
26 + // https://www.torn.com/factions.php?step=your#/tab=controls&batbal_uids=15,1617955,4&batbal_amounts=120,-250,1500
27 +
28 + 'use strict';
29 +
30 + function batchBalanceWrapper() {
31 + console.log('Batch Balance starts');
32 +
33 + const ACTION_INTERVAL_MS = 1000;
34 + const GM_VALUE_KEY = 'batbal-action';
35 + const PROFILE_HREF_PREFIX = 'profiles.php?XID=';
36 + const ACTION_SPECS = {
37 + give: {
38 + summary: 'Give',
39 + text: 'Give',
40 + waitingText: 'Giving',
41 + bodyParam: 'giveMoney',
42 + },
43 + add: {
44 + summary: 'Add to balance',
45 + text: 'Add',
46 + waitingText: 'Adding',
47 + bodyParam: 'addToBalance',
48 + },
49 + };
50 +
51 + const $ = window.jQuery;
52 +
53 + const LOCAL_STORAGE_PREFIX = 'BATCH_BALANCE_';
54 +
55 + function getLocalStorage(key, defaultValue) {
56 + const value = window.localStorage.getItem(LOCAL_STORAGE_PREFIX + key);
57 + try {
58 + return JSON.parse(value) ?? defaultValue;
59 + } catch (err) {
60 + return defaultValue;
61 + }
62 + }
63 +
64 + function setLocalStorage(key, value) {
65 + window.localStorage.setItem(LOCAL_STORAGE_PREFIX + key, JSON.stringify(value));
66 + }
67 +
68 + const isPda = window.GM_info?.scriptHandler?.toLowerCase().includes('tornpda');
69 + const [getValue, setValue] =
70 + isPda || typeof window.GM_getValue !== 'function' || typeof window.GM_setValue !== 'function'
71 + ? [getLocalStorage, setLocalStorage]
72 + : [window.GM_getValue, window.GM_setValue];
73 +
74 + const STYLE = `
75 + .batbal-overlay {
76 + position: relative;
77 + }
78 + .batbal-overlay:after {
79 + content: '';
80 + position: absolute;
81 + background: repeating-linear-gradient(135deg, #2228, #2228 70px, #0008 70px, #0008 80px);
82 + top: 0;
83 + left: 0;
84 + width: 100%;
85 + height: 100%;
86 + z-index: 900000;
87 + }
88 + #batbal-ctrl {
89 + margin: 10px 0;
90 + padding: 10px;
91 + border-radius: 5px;
92 + background-color: var(--default-bg-panel-color);
93 + text-align: center;
94 + line-height: 16px;
95 + }
96 + #batbal-ctrl-detail > :not(:first-child),
97 + #batbal-ctrl > :not(:first-child) {
98 + margin-top: 10px;
99 + }
100 + #batbal-ctrl-title {
101 + font-size: large;
102 + font-weight: bold;
103 + }
104 + #batbal-ctrl-status {
105 + font-weight: bold;
106 + }
107 + #batbal-ctrl button {
108 + margin: 0 4px;
109 + }
110 + #batbal-ctrl table {
111 + margin: 0 auto;
112 + }
113 + #batbal-ctrl th {
114 + font-weight: bold;
115 + }
116 + #batbal-ctrl th,
117 + #batbal-ctrl td {
118 + color: inherit;
119 + padding: 5px;
120 + border: 1px solid #ccc;
121 + }
122 + #batbal-ctrl td:last-child {
123 + text-align: right;
124 + }
125 + #batbal-ctrl-detail tr.batbal-done:after {
126 + content: '\u2713';
127 + color: green;
128 + padding-left: 6px;
129 + }
130 + `;
131 +
132 + const CONTROLLER_HTML = `
133 + <div id="batbal-ctrl">
134 + <div id="batbal-ctrl-title">Batch Balance</div>
135 + <div>
136 + <table>
137 + <thead>
138 + <tr>
139 + <th colspan="2">Summary</th>
140 + </tr>
141 + </thead>
142 + <tbody>
143 + <tr>
144 + <th>Action</th>
145 + <td id="batbal-ctrl-summary-action-type">-</td>
146 + </tr>
147 + <tr>
148 + <th>Asset Type</th>
149 + <td id="batbal-ctrl-summary-asset-type">-</td>
150 + </tr>
151 + <tr>
152 + <th>Player Count</th>
153 + <td id="batbal-ctrl-summary-player-count">-</td>
154 + </tr>
155 + <tr>
156 + <th>Player Not in Faction</th>
157 + <td><span id="batbal-ctrl-summary-player-not-in-faction">-</span></td>
158 + </tr>
159 + <tr>
160 + <th>Total Amount</th>
161 + <td><span id="batbal-ctrl-summary-total-amount">-</span></td>
162 + </tr>
163 + </tbody>
164 + </table>
165 + </div>
166 + <div>
167 + <button id="batbal-ctrl-start" class="torn-btn" disabled>Start</button>
168 + <button id="batbal-ctrl-show-detail" class="torn-btn">Show details</button>
169 + <button id="batbal-ctrl-hide-detail" class="torn-btn" style="display: none">Hide details</button>
170 + <button id="batbal-ctrl-clear-data" class="torn-btn" disabled>Clear data</button>
171 + </div>
172 + <button id="batbal-ctrl-submit" class="torn-btn" style="display: none" disabled></button>
173 + <div>Status: <span id="batbal-ctrl-status"></span></div>
174 + <div id="batbal-ctrl-detail" style="display: none">
175 + <table>
176 + <thead>
177 + <tr>
178 + <th>ID</th>
179 + <th>Name</th>
180 + <th>Amount</th>
181 + <th>Note</th>
182 + </tr>
183 + </thead>
184 + <tbody></tbody>
185 + </table>
186 + </div>
187 + </div>`;
188 +
189 + function formatAmount(v) {
190 + return (v >= 0 ? '+' : '') + v.toString().replace(/\d{1,3}(?=(\d{3})+$)/g, (s) => s + ',');
191 + }
192 +
193 + async function sleep(t) {
194 + await new Promise((r) => setTimeout(r, t));
195 + }
196 +
197 + // Copied from https://stackoverflow.com/a/25490531
198 + function getCookie(name) {
199 + return document.cookie.match('(^|;)\\s*' + name + '\\s*=\\s*([^;]+)')?.pop() || '';
200 + }
201 +
202 + function getParams() {
203 + const params = new URLSearchParams(location.hash.slice(1));
204 + return params.get('/tab') === 'controls' ? params : null;
205 + }
206 +
207 + function storeAction(action) {
208 + setValue(GM_VALUE_KEY, action);
209 + }
210 +
211 + function parseAction() {
212 + const params = getParams();
213 + if (!params) {
214 + return null;
215 + }
216 + const paramUids = params.get('batbal_uids');
217 + const paramAmounts = params.get('batbal_amounts');
218 + if (paramUids === null || paramAmounts === null) {
219 + return null;
220 + }
221 + const uids = paramUids.split(',');
222 + const amounts = paramAmounts.split(',');
223 + if (amounts.length !== uids.length) {
224 + return { error: 'Param "batbal_uids" and "batbal_amounts" have different lengths' };
225 + }
226 + if (uids.length === 0 || uids.some((x) => !x.match(/^\d+$/))) {
227 + return { error: 'Param "batbal_uids" is invalid' };
228 + }
229 + if (amounts.length === 0 || amounts.some((x) => !x.match(/^[+-]?\d{1,11}$/))) {
230 + return { error: 'Param "batbal_amounts" is invalid' };
231 + }
232 + const actionType = params.get('batbal_action') ?? 'add';
233 + if (!['give', 'add'].includes(actionType)) {
234 + return { error: 'Param "batbal_action" is invalid' };
235 + }
236 + const assetType = params.get('batbal_asset') ?? 'money';
237 + if (!['money', 'points'].includes(assetType)) {
238 + return { error: 'Param "batbal_asset" is invalid' };
239 + }
240 + return {
241 + uidAmounts: uids.map((uid, i) => [uid, parseInt(amounts[i])]).filter(([, amount]) => amount !== 0),
242 + next: 0,
243 + actionType,
244 + assetType,
245 + };
246 + }
247 +
248 + function checkAction(parsedAction, storedAction) {
249 + if (parsedAction && storedAction) {
250 + if (
251 + JSON.stringify(parsedAction.uidAmounts) !== JSON.stringify(storedAction.uidAmounts) ||
252 + parsedAction.actionType !== storedAction.actionType ||
253 + parsedAction.assetType !== storedAction.assetType
254 + ) {
255 + throw new Error(
256 + "An unfinished Batch Balance operation was found that doesn't match the URL parameters. " +
257 + 'Click "Show details" to view the pending operation. ' +
258 + 'To resume it, clear the URL parameters and refresh the page.' +
259 + 'To discard it, click "Clear data" and refresh the page.',
260 + );
261 + }
262 + }
263 + return storedAction ?? parsedAction;
264 + }
265 +
266 + /** @returns Promise<Record<string, { name: string, isInFaction: boolean }>> */
267 + async function getUidMap() {
268 + return new Promise((resolve) => {
269 + const interval = setInterval(function () {
270 + const $depositors = $('.money___DbkSX .userListWrap___AndUA .userInfoWrap___k2sjQ');
271 + if ($depositors.length === 0) {
272 + return;
273 + }
274 + const map = {};
275 + $depositors.each(function () {
276 + const $name = $(this).find(`a[href*="${PROFILE_HREF_PREFIX}"]`).first();
277 + if ($name.length > 0) {
278 + const uid = ($name.attr('href') || '').split(PROFILE_HREF_PREFIX)[1];
279 + map[uid] = {
280 + name: $name.text().trim(),
281 + isInFaction: !$(this).hasClass('inactive___Hd0EQ'),
282 + };
283 + }
284 + });
285 + clearInterval(interval);
286 + resolve(map);
287 + }, 200);
288 + });
289 + }
290 +
291 + function renderController() {
292 + GM_addStyle(STYLE);
293 + const $controlsWrap = $('.faction-controls-wrap');
294 + $controlsWrap.addClass('batbal-overlay');
295 + $controlsWrap.before(CONTROLLER_HTML);
296 + $('#batbal-ctrl-show-detail').on('click', function () {
297 + $('#batbal-ctrl-detail').show();
298 + $('#batbal-ctrl-hide-detail').show();
299 + $(this).hide();
300 + });
301 + $('#batbal-ctrl-hide-detail').on('click', function () {
302 + $('#batbal-ctrl-detail').hide();
303 + $('#batbal-ctrl-show-detail').show();
304 + $(this).hide();
305 + });
306 + $('#batbal-ctrl-clear-data').on('click', function () {
307 + if (
308 + confirm(
309 + 'Are you sure you want to delete the saved Batch Balance data? ' +
310 + 'This will remove any unfinished operations and cannot be undone.',
311 + )
312 + ) {
313 + storeAction(null);
314 + $('#batbal-ctrl').hide();
315 + alert('Saved data has been deleted, please refresh the page');
316 + }
317 + });
318 + }
319 +
320 + function updateStatus(s) {
321 + $('#batbal-ctrl-status').text(String(s));
322 + if (s instanceof Error) {
323 + $('#batbal-ctrl-status').css('color', 'red');
324 + }
325 + }
326 +
327 + function renderDetails(action, uidMap) {
328 + const $tbody = $('#batbal-ctrl-detail tbody');
329 + $tbody.empty();
330 + let outsideCount = 0;
331 + action.uidAmounts.forEach(([uid, amount], i) => {
332 + const amountClass = amount >= 0 ? 't-green' : 't-red';
333 + const trClass = i < action.next ? 'batbal-done' : '';
334 + const uidInfo = uidMap[uid] || {};
335 + const name = uidInfo.name || '';
336 + const isInFaction = uidInfo.isInFaction || false;
337 + if (!isInFaction) {
338 + outsideCount++;
339 + }
340 + $tbody.append(`<tr class="${trClass}">
341 + <td>${uid}</td>
342 + <td>${name}</td>
343 + <td><span class="${amountClass}">${formatAmount(amount)}</span></td>
344 + <td><span class="${!isInFaction ? 't-red' : ''}">${!isInFaction ? 'Not in faction' : ''}</span></td>
345 + </tr>`);
346 + });
347 + const total = action.uidAmounts.reduce((v, [, amount]) => v + amount, 0);
348 + const totalClass = total >= 0 ? 't-green' : 't-red';
349 + const actionSpec = ACTION_SPECS[action.actionType];
350 + $('#batbal-ctrl-summary-action-type').text(actionSpec.summary);
351 + $('#batbal-ctrl-summary-asset-type').text(action.assetType);
352 + $('#batbal-ctrl-summary-player-count').text(action.uidAmounts.length);
353 + $('#batbal-ctrl-summary-player-not-in-faction')
354 + .text(outsideCount)
355 + .toggleClass('t-red', outsideCount > 0);
356 + $('#batbal-ctrl-summary-total-amount').text(formatAmount(total)).addClass(totalClass);
357 + updateStatus(`Progress: ${action.next} / ${action.uidAmounts.length} done`);
358 + }
359 +
360 + async function addMoney({ uid, name, amount, actionType, assetType }) {
361 + const $submit = $('#batbal-ctrl-submit');
362 + $submit.show();
363 + const actionSpec = ACTION_SPECS[actionType];
364 + const textSuffix = ` ${assetType}: ${name} [${uid}] ${formatAmount(amount)}`;
365 + const queryParam = {
366 + money: 'factionsGiveMoney',
367 + points: 'factionsGivePoints',
368 + }[assetType];
369 + $submit.text(`${actionSpec.text} ${textSuffix}`);
370 + $submit.prop('disabled', false);
371 + return new Promise((resolve, reject) => {
372 + $submit.on('click', async () => {
373 + try {
374 + $submit.off('click');
375 + $submit.text(`${actionSpec.waitingText} ${textSuffix}`);
376 + $submit.prop('disabled', true);
377 + const rfcv = getCookie('rfc_v');
378 + const rsp = await fetch(`/page.php?sid=${queryParam}&rfcv=${rfcv}`, {
379 + method: 'POST',
380 + headers: {
381 + 'Content-Type': 'application/json',
382 + 'x-requested-with': 'XMLHttpRequest',
383 + },
384 + body: JSON.stringify({
385 + option: actionSpec.bodyParam,
386 + receiver: parseInt(uid),
387 + amount,
388 + }),
389 + });
390 + const rawData = await rsp.text();
391 + if (!rsp.ok) {
392 + throw new Error(`Network error: ${rsp.status} ${rawData}`);
393 + }
394 + const data = JSON.parse(rawData);
395 + if (data.success === true) {
396 + resolve();
397 + } else {
398 + reject(new Error(`Unexpected server response: ${rawData}`));
399 + }
400 + } catch (err) {
401 + reject(err);
402 + }
403 + });
404 + });
405 + }
406 +
407 + async function start(action, uidMap) {
408 + storeAction(action);
409 + $('#batbal-ctrl-start').prop('disabled', true);
410 + $('#batbal-ctrl-clear-data').prop('disabled', true);
411 +
412 + try {
413 + while (action.next < action.uidAmounts.length) {
414 + updateStatus(`Current progress: ${action.next} / ${action.uidAmounts.length} done`);
415 + const now = Date.now();
416 + const [uid, amount] = action.uidAmounts[action.next];
417 + const uidInfo = uidMap[uid] || {};
418 + const name = uidInfo.name || 'Unknown player';
419 + await addMoney({ uid, name, amount, actionType: action.actionType, assetType: action.assetType });
420 + action.next++;
421 + storeAction(action);
422 + renderDetails(action, uidMap);
423 + const elapsed = Date.now() - now;
424 + if (elapsed < ACTION_INTERVAL_MS) {
425 + await sleep(ACTION_INTERVAL_MS - elapsed);
426 + }
427 + }
428 + storeAction(null);
429 + updateStatus('All done!');
430 + } catch (err) {
431 + updateStatus(err);
432 + }
433 + }
434 +
435 + async function main() {
436 + try {
437 + const parsedAction = parseAction();
438 + const storedAction = getValue(GM_VALUE_KEY, null);
439 + if (storedAction === null && parsedAction === null) {
440 + return;
441 + }
442 +
443 + const uidMap = await getUidMap();
444 + renderController();
445 + if (storedAction) {
446 + renderDetails(storedAction, uidMap);
447 + $('#batbal-ctrl-clear-data').prop('disabled', false);
448 + }
449 + if (parsedAction.error) {
450 + throw new Error(parsedAction.error);
451 + }
452 +
453 + const action = checkAction(parsedAction, storedAction);
454 + if (!storedAction) {
455 + renderDetails(action, uidMap);
456 + }
457 + if (action.actionType === 'give') {
458 + if (action.uidAmounts.some(([uid]) => !uidMap[uid]?.isInFaction)) {
459 + throw new Error('Some players are not in the faction');
460 + }
461 + if (action.uidAmounts.some(([, amount]) => amount <= 0)) {
462 + throw new Error('Amounts to give must be positive');
463 + }
464 + }
465 +
466 + $('#batbal-ctrl-start').prop('disabled', false);
467 + $('#batbal-ctrl-start').on('click', () => start(action, uidMap));
468 + } catch (err) {
469 + updateStatus(err);
470 + console.log('Unhandled exception from Batch Balance:', err);
471 + }
472 + }
473 +
474 + main();
475 + console.log('Batch Balance ends');
476 + }
477 +
478 + if (document.readyState === 'loading') {
479 + document.addEventListener('readystatechange', () => {
480 + if (document.readyState === 'interactive') {
481 + batchBalanceWrapper();
482 + }
483 + });
484 + } else {
485 + batchBalanceWrapper();
486 + }
Nowsze Starsze