Add wish auto import
parent
439ca8fa71
commit
5e96df8aa9
|
@ -0,0 +1,39 @@
|
||||||
|
<script>
|
||||||
|
export let className = '';
|
||||||
|
export let placeholder = '';
|
||||||
|
export let step = undefined;
|
||||||
|
export let min = Math.min();
|
||||||
|
export let max = Math.max();
|
||||||
|
|
||||||
|
export let value = '';
|
||||||
|
|
||||||
|
const handleInput = (event) => {
|
||||||
|
value = event.target.value;
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<div
|
||||||
|
class={`container overflow-hidden flex flex-1 relative items-center bg-background rounded-2xl focus-within:border-primary border-2 border-transparent ease-in duration-100 ${className}`}
|
||||||
|
>
|
||||||
|
<textarea
|
||||||
|
{placeholder}
|
||||||
|
{value}
|
||||||
|
{min}
|
||||||
|
{max}
|
||||||
|
{step}
|
||||||
|
on:change
|
||||||
|
on:input={handleInput}
|
||||||
|
class={`w-full min-h-full pr-4 text-white placeholder-gray-500 leading-none bg-transparent border-none focus:outline-none`}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
.container {
|
||||||
|
height: fit-content;
|
||||||
|
}
|
||||||
|
|
||||||
|
textarea {
|
||||||
|
min-height: 100px;
|
||||||
|
@apply p-4;
|
||||||
|
}
|
||||||
|
</style>
|
|
@ -9,7 +9,7 @@
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div class="fixed left-0 right-0 bottom-0 text-center px-4 z-50 md:left-auto md:max-w-screen-sm">
|
<div class="fixed left-0 right-0 bottom-0 text-center px-4 md:left-auto md:max-w-screen-sm" style="z-index:99999;">
|
||||||
{#each $toasts as toast (toast._id)}
|
{#each $toasts as toast (toast._id)}
|
||||||
<div
|
<div
|
||||||
class={`rounded-xl px-4 py-2 mb-4 w-full bg-black bg-opacity-75 ${types[toast.type]}`}
|
class={`rounded-xl px-4 py-2 mb-4 w-full bg-black bg-opacity-75 ${types[toast.type]}`}
|
||||||
|
|
|
@ -0,0 +1,615 @@
|
||||||
|
<script>
|
||||||
|
import { mdiClose, mdiDownload, mdiHelpCircle, mdiLoading } from '@mdi/js';
|
||||||
|
import { onMount, tick } from 'svelte';
|
||||||
|
import dayjs from 'dayjs';
|
||||||
|
|
||||||
|
import { pushToast } from '../stores/toast';
|
||||||
|
import Button from './Button.svelte';
|
||||||
|
import Icon from './Icon.svelte';
|
||||||
|
import Input from './Input.svelte';
|
||||||
|
import Textarea from './Textarea.svelte';
|
||||||
|
import { weaponList } from '../data/weaponList';
|
||||||
|
import { characters } from '../data/characters';
|
||||||
|
import { readSave, updateSave } from '../stores/saveManager';
|
||||||
|
|
||||||
|
export let closeModal;
|
||||||
|
|
||||||
|
const fetchController = new AbortController();
|
||||||
|
const fetchSignal = fetchController.signal;
|
||||||
|
|
||||||
|
let numberFormat = Intl.NumberFormat();
|
||||||
|
|
||||||
|
let showFaq = false;
|
||||||
|
let selectedType = 'pc';
|
||||||
|
|
||||||
|
let generatedTextInput = '';
|
||||||
|
let genshinLink = '';
|
||||||
|
|
||||||
|
let types = {
|
||||||
|
100: {
|
||||||
|
name: "Beginners' Wish",
|
||||||
|
id: 'beginners',
|
||||||
|
},
|
||||||
|
200: {
|
||||||
|
name: 'Standard',
|
||||||
|
id: 'standard',
|
||||||
|
},
|
||||||
|
301: {
|
||||||
|
name: 'Character Event',
|
||||||
|
id: 'character-event',
|
||||||
|
},
|
||||||
|
302: {
|
||||||
|
name: 'Weapon Event',
|
||||||
|
id: 'weapon-event',
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
let wishes = {};
|
||||||
|
|
||||||
|
let url;
|
||||||
|
|
||||||
|
let processingLog = false;
|
||||||
|
let fetchingWishes = false;
|
||||||
|
let finishedProcessingLog = false;
|
||||||
|
let calculatingPity = false;
|
||||||
|
|
||||||
|
let cancelled = false;
|
||||||
|
|
||||||
|
let region = '';
|
||||||
|
let currentBanner = '';
|
||||||
|
let currentPage = 1;
|
||||||
|
|
||||||
|
function cancel() {
|
||||||
|
fetchController.abort();
|
||||||
|
cancelled = true;
|
||||||
|
|
||||||
|
setTimeout(() => {
|
||||||
|
closeModal();
|
||||||
|
}, 2000);
|
||||||
|
}
|
||||||
|
|
||||||
|
function getDeviceType() {
|
||||||
|
switch (selectedType) {
|
||||||
|
case 'pc':
|
||||||
|
return 'pc';
|
||||||
|
case 'android':
|
||||||
|
case 'ios':
|
||||||
|
return 'mobile';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function startImport() {
|
||||||
|
if (selectedType === 'pclocal') {
|
||||||
|
importFromGeneratedText();
|
||||||
|
} else {
|
||||||
|
processLogs();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function processLogs() {
|
||||||
|
processingLog = true;
|
||||||
|
|
||||||
|
try {
|
||||||
|
url = new URL(genshinLink);
|
||||||
|
} catch (err) {
|
||||||
|
pushToast('Invalid link, please check it again', 'error');
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
for (const [wishNumber, type] of Object.entries(types)) {
|
||||||
|
await getLog(wishNumber, type);
|
||||||
|
if (cancelled) return;
|
||||||
|
await sleep(2000);
|
||||||
|
}
|
||||||
|
finishedProcessingLog = true;
|
||||||
|
} catch (err) {
|
||||||
|
wishes = {};
|
||||||
|
processingLog = false;
|
||||||
|
fetchingWishes = false;
|
||||||
|
finishedProcessingLog = false;
|
||||||
|
calculatingPity = false;
|
||||||
|
|
||||||
|
region = '';
|
||||||
|
currentBanner = '';
|
||||||
|
currentPage = 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function getLog(wishNumber, type) {
|
||||||
|
fetchingWishes = true;
|
||||||
|
|
||||||
|
console.log(wishNumber, type);
|
||||||
|
url.searchParams.set('auth_appid', 'webview_gacha');
|
||||||
|
url.searchParams.set('init_type', '301');
|
||||||
|
url.searchParams.set('gacha_id', 'd610857102f9256ba143ccf2e03b964c76a6ed');
|
||||||
|
url.searchParams.set('lang', 'en');
|
||||||
|
url.searchParams.set('device_type', getDeviceType());
|
||||||
|
if (region !== '') url.searchParams.set('region', region);
|
||||||
|
url.searchParams.set('gacha_type', wishNumber);
|
||||||
|
url.searchParams.set('size', 20);
|
||||||
|
url.searchParams.append('lang', 'en-us');
|
||||||
|
url.hash = '';
|
||||||
|
url.host = 'hk4e-api-os.mihoyo.com';
|
||||||
|
url.pathname = 'event/gacha_info/api/getGachaLog';
|
||||||
|
|
||||||
|
currentBanner = type.name;
|
||||||
|
|
||||||
|
const weapons = Object.values(weaponList);
|
||||||
|
const chars = Object.values(characters);
|
||||||
|
|
||||||
|
let page = 1;
|
||||||
|
let result = [];
|
||||||
|
do {
|
||||||
|
if (cancelled) return;
|
||||||
|
|
||||||
|
url.searchParams.set('page', page);
|
||||||
|
|
||||||
|
currentPage = page;
|
||||||
|
|
||||||
|
try {
|
||||||
|
const res = await fetchRetry(
|
||||||
|
__paimon.env.CORS_HOST,
|
||||||
|
{
|
||||||
|
method: 'POST',
|
||||||
|
headers: { 'Content-Type': 'application/json' },
|
||||||
|
body: JSON.stringify({
|
||||||
|
url: url.toString(),
|
||||||
|
}),
|
||||||
|
signal: fetchSignal,
|
||||||
|
},
|
||||||
|
2,
|
||||||
|
);
|
||||||
|
|
||||||
|
if (cancelled) return;
|
||||||
|
|
||||||
|
if (!res.ok) {
|
||||||
|
processingLog = false;
|
||||||
|
pushToast('Error code returned from MiHoYo API, please try again later!', 'error');
|
||||||
|
throw 'error code';
|
||||||
|
}
|
||||||
|
|
||||||
|
const dat = await res.json();
|
||||||
|
|
||||||
|
if (dat.retcode !== 0) {
|
||||||
|
processingLog = false;
|
||||||
|
|
||||||
|
if (dat.message === 'authkey timeout') {
|
||||||
|
pushToast('Link expired, please try again!', 'error');
|
||||||
|
throw 'error code';
|
||||||
|
}
|
||||||
|
|
||||||
|
pushToast('Error code returned from MiHoYo API, please try again later!', 'error');
|
||||||
|
throw 'error code';
|
||||||
|
}
|
||||||
|
|
||||||
|
region = dat.data.region;
|
||||||
|
result = dat.data.list;
|
||||||
|
} catch (err) {
|
||||||
|
processingLog = false;
|
||||||
|
pushToast('Connection timeout, please wait a moment and try again later', 'error');
|
||||||
|
throw 'network error';
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
for (let row of result) {
|
||||||
|
const code = row.gacha_type;
|
||||||
|
const time = dayjs(row.time);
|
||||||
|
const name = row.name;
|
||||||
|
const type = row.item_type.replace(/ /g, '');
|
||||||
|
|
||||||
|
let id;
|
||||||
|
if (type === 'Weapon') {
|
||||||
|
id = weapons.find((e) => e.name === name).id;
|
||||||
|
} else if (type === 'Character') {
|
||||||
|
id = chars.find((e) => e.name === name).id;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (wishes[code] === undefined) {
|
||||||
|
wishes[code] = [];
|
||||||
|
}
|
||||||
|
|
||||||
|
wishes[code] = [
|
||||||
|
...wishes[code],
|
||||||
|
{
|
||||||
|
type: type.toLowerCase(),
|
||||||
|
id,
|
||||||
|
time: time.unix(),
|
||||||
|
pity: 0,
|
||||||
|
},
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
page = page + 1;
|
||||||
|
await sleep(1000);
|
||||||
|
console.log(wishes);
|
||||||
|
} catch (err) {
|
||||||
|
processingLog = false;
|
||||||
|
pushToast('Invalid data returned from API, try again later!', 'error');
|
||||||
|
throw 'invalid data';
|
||||||
|
}
|
||||||
|
} while (result.length > 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function fetchRetry(url, options, n) {
|
||||||
|
let error;
|
||||||
|
for (let i = 0; i < n; i++) {
|
||||||
|
if (cancelled) return;
|
||||||
|
|
||||||
|
try {
|
||||||
|
return await fetch(url, options);
|
||||||
|
} catch (err) {
|
||||||
|
error = err;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
|
||||||
|
function sleep(ms) {
|
||||||
|
return new Promise((resolve) => setTimeout(resolve, ms));
|
||||||
|
}
|
||||||
|
|
||||||
|
function toggleFaqs(show) {
|
||||||
|
showFaq = show;
|
||||||
|
}
|
||||||
|
|
||||||
|
function changeSelectedType(type) {
|
||||||
|
selectedType = type;
|
||||||
|
}
|
||||||
|
|
||||||
|
function detectPlatform() {
|
||||||
|
const userAgent = navigator.userAgent || navigator.vendor;
|
||||||
|
if (/android/i.test(userAgent)) {
|
||||||
|
selectedType = 'android';
|
||||||
|
}
|
||||||
|
|
||||||
|
if (/iPad|iPhone|iPod/.test(userAgent) && !window.MSStream) {
|
||||||
|
selectedType = 'ios';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function importFromGeneratedText() {
|
||||||
|
if (!generatedTextInput.startsWith('paimonmoeimporterv1###')) {
|
||||||
|
pushToast('Invalid data, please use the latest importer app', 'error');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
processingLog = true;
|
||||||
|
|
||||||
|
const rows = generatedTextInput.substring(22).split(';');
|
||||||
|
|
||||||
|
const weapons = Object.values(weaponList);
|
||||||
|
const chars = Object.values(characters);
|
||||||
|
|
||||||
|
try {
|
||||||
|
for (let row of rows) {
|
||||||
|
if (row === '') continue;
|
||||||
|
|
||||||
|
const cell = row.split(',');
|
||||||
|
const code = Number(cell[0]);
|
||||||
|
const time = dayjs(cell[1]);
|
||||||
|
const name = cell[2];
|
||||||
|
const type = cell[3].replace(/ /g, '');
|
||||||
|
|
||||||
|
let id;
|
||||||
|
if (type === 'Weapon') {
|
||||||
|
id = weapons.find((e) => e.name === name).id;
|
||||||
|
} else if (type === 'Character') {
|
||||||
|
id = chars.find((e) => e.name === name).id;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (wishes[code] === undefined) {
|
||||||
|
wishes[code] = [];
|
||||||
|
}
|
||||||
|
|
||||||
|
wishes[code] = [
|
||||||
|
...wishes[code],
|
||||||
|
{
|
||||||
|
type: type.toLowerCase(),
|
||||||
|
id,
|
||||||
|
time: time.unix(),
|
||||||
|
pity: 0,
|
||||||
|
},
|
||||||
|
];
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
processingLog = false;
|
||||||
|
pushToast('Invalid data, please use the latest importer app', 'error');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
finishedProcessingLog = true;
|
||||||
|
console.log(wishes);
|
||||||
|
}
|
||||||
|
|
||||||
|
function saveData() {
|
||||||
|
calculatingPity = true;
|
||||||
|
for (let [code, type] of Object.entries(types)) {
|
||||||
|
processWishes(code, type);
|
||||||
|
}
|
||||||
|
calculatingPity = false;
|
||||||
|
|
||||||
|
pushToast('Import success 😀!');
|
||||||
|
closeModal();
|
||||||
|
}
|
||||||
|
|
||||||
|
function processWishes(code, type) {
|
||||||
|
if (wishes[code] === undefined) return;
|
||||||
|
|
||||||
|
const path = `wish-counter-${type.id}`;
|
||||||
|
const localData = readSave(path);
|
||||||
|
|
||||||
|
let localWishes = [];
|
||||||
|
if (localData !== null) {
|
||||||
|
const counterData = JSON.parse(localData);
|
||||||
|
localWishes = counterData.pulls || [];
|
||||||
|
}
|
||||||
|
|
||||||
|
const importedWishes = wishes[code].slice().reverse();
|
||||||
|
const oldestWish = importedWishes[0];
|
||||||
|
|
||||||
|
localWishes = localWishes
|
||||||
|
.slice()
|
||||||
|
.filter((e) => e.time < oldestWish.time)
|
||||||
|
.sort((a, b) => a.time - b.time);
|
||||||
|
|
||||||
|
const combined = [...localWishes, ...importedWishes];
|
||||||
|
|
||||||
|
let rare = 0;
|
||||||
|
let legendary = 0;
|
||||||
|
for (let i = 0; i < combined.length; i++) {
|
||||||
|
if (combined[i].pity !== 0) continue;
|
||||||
|
|
||||||
|
rare++;
|
||||||
|
legendary++;
|
||||||
|
|
||||||
|
let rarity;
|
||||||
|
if (combined[i].type === 'character') {
|
||||||
|
rarity = characters[combined[i].id].rarity;
|
||||||
|
} else if (combined[i].type === 'weapon') {
|
||||||
|
rarity = weaponList[combined[i].id].rarity;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (rarity === 5) {
|
||||||
|
combined[i].pity = legendary;
|
||||||
|
legendary = 0;
|
||||||
|
rare = 0;
|
||||||
|
} else if (rarity === 4) {
|
||||||
|
combined[i].pity = rare;
|
||||||
|
rare = 0;
|
||||||
|
} else {
|
||||||
|
combined[i].pity = 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const data = JSON.stringify({
|
||||||
|
total: combined.length,
|
||||||
|
legendary,
|
||||||
|
rare,
|
||||||
|
pulls: combined,
|
||||||
|
});
|
||||||
|
|
||||||
|
updateSave(path, data);
|
||||||
|
}
|
||||||
|
|
||||||
|
onMount(() => {
|
||||||
|
detectPlatform();
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
{#if processingLog}
|
||||||
|
<h1 class="font-display text-white text-xl mb-2">Import Wish History</h1>
|
||||||
|
<div class="bg-background rounded-xl px-4 py-2 text-white mt-2">
|
||||||
|
{#if finishedProcessingLog}
|
||||||
|
<table class="min-w-full md:min-w-0">
|
||||||
|
{#each Object.entries(types) as [code, type]}
|
||||||
|
{#if wishes[code] !== undefined}
|
||||||
|
<tr>
|
||||||
|
<td class="border-b border-gray-700 py-1">
|
||||||
|
<span class="text-white mr-2 whitespace-no-wrap">{type.name} Banner</span>
|
||||||
|
</td>
|
||||||
|
<td class="border-b border-gray-700 py-1">
|
||||||
|
<span class="text-white mr-2 whitespace-no-wrap">
|
||||||
|
<Icon size={0.5} path={mdiClose} />
|
||||||
|
{numberFormat.format(wishes[code].length)}
|
||||||
|
</span>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
{/if}
|
||||||
|
{/each}
|
||||||
|
</table>
|
||||||
|
<p class="mt-4">Imported wishes will be appended or replaced accordingly to existing data</p>
|
||||||
|
<p>If you don't have any data saved before, first wish will be counted as pity 1</p>
|
||||||
|
<p class="font-semibold">Save the data?</p>
|
||||||
|
{:else if calculatingPity}
|
||||||
|
<Icon path={mdiLoading} spin color="white" />
|
||||||
|
Re-calculating pity...
|
||||||
|
{:else if fetchingWishes}
|
||||||
|
<div class="flex">
|
||||||
|
<Icon path={mdiLoading} spin color="white" />
|
||||||
|
<div class="ml-2">
|
||||||
|
<p>{`Processing ${currentBanner} Banner`}</p>
|
||||||
|
<p>{`Page ${currentPage}`}</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<table class="min-w-full md:min-w-0 mt-2">
|
||||||
|
{#each Object.entries(types) as [code, type]}
|
||||||
|
{#if wishes[code] !== undefined}
|
||||||
|
<tr>
|
||||||
|
<td class="border-b border-gray-700 py-1">
|
||||||
|
<span class="text-white mr-2 whitespace-no-wrap">{type.name} Banner</span>
|
||||||
|
</td>
|
||||||
|
<td class="border-b border-gray-700 py-1">
|
||||||
|
<span class="text-white mr-2 whitespace-no-wrap">
|
||||||
|
<Icon size={0.5} path={mdiClose} />
|
||||||
|
{numberFormat.format(wishes[code].length)}
|
||||||
|
</span>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
{/if}
|
||||||
|
{/each}
|
||||||
|
</table>
|
||||||
|
{:else}
|
||||||
|
<Icon path={mdiLoading} spin color="white" />
|
||||||
|
Parsing...
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
<div class="flex justify-end mt-4">
|
||||||
|
{#if finishedProcessingLog && !calculatingPity}
|
||||||
|
<Button on:click={saveData} color="green" className="mr-4">Save</Button>
|
||||||
|
{/if}
|
||||||
|
<Button on:click={cancel} disabled={calculatingPity || cancelled}>{cancelled ? 'Cancelling...' : 'Cancel'}</Button>
|
||||||
|
</div>
|
||||||
|
{:else}
|
||||||
|
<div>
|
||||||
|
{#if showFaq}
|
||||||
|
<h1 class="font-display text-white text-xl mb-2">Import Wish History FAQS</h1>
|
||||||
|
|
||||||
|
<div class="font-body">
|
||||||
|
<p class="text-white font-semibold">How does it work?</p>
|
||||||
|
<p class="text-gray-400">
|
||||||
|
Genshin Impact wish history is basically a web page, so you can access it by opening the web page url. A
|
||||||
|
temporary key will be generated after you open the wish history page or the feedback page, and the importer
|
||||||
|
will automatically use the MiHoYo API to fetch your wish history.
|
||||||
|
</p>
|
||||||
|
<p class="text-white font-semibold mt-4">Is it safe? Will I get banned?</p>
|
||||||
|
<p class="text-gray-400">
|
||||||
|
Paimon.moe use the same request that Genshin Impact use to get the wish history, and Paimon.moe has no way
|
||||||
|
whatsoever to modify any game files or memory, and it should be safe. But use it at your own risk (well I use
|
||||||
|
it on my main account). You still can input your data manually 😀.
|
||||||
|
</p>
|
||||||
|
<p class="text-white font-semibold mt-4">
|
||||||
|
Hey I checked the request and stuff, but why it request to your domain instead of MiHoYo API?
|
||||||
|
</p>
|
||||||
|
<p class="text-gray-400">
|
||||||
|
Paimon.moe cannot request directly to MiHoYo API because of
|
||||||
|
<a
|
||||||
|
class="text-primary hover:underline"
|
||||||
|
href="https://developer.mozilla.org/en-US/docs/Web/HTTP/CORS"
|
||||||
|
target="__blank"
|
||||||
|
>
|
||||||
|
CORS</a
|
||||||
|
>, so the request redirected to a simple cors proxy to make it work. You can see the code
|
||||||
|
<a
|
||||||
|
class="text-primary hover:underline"
|
||||||
|
href="https://gist.github.com/MadeBaruna/64785ae992c924e0cbfe575e404b7155"
|
||||||
|
target="__blank">here</a
|
||||||
|
>
|
||||||
|
</p>
|
||||||
|
<p class="text-white font-semibold mt-4">Do you store my temporary key?</p>
|
||||||
|
<p class="text-gray-400">
|
||||||
|
Paimon.moe never store your key, and use HTTPS to pass your url to a cors proxy to make the CORS works.
|
||||||
|
<!-- If you don't want any passing around your url, you can use the small importer app to process the wish
|
||||||
|
history on your local PC (PC Local option) -->
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
{:else}
|
||||||
|
<div class="flex items-center">
|
||||||
|
<h1 class="font-display text-white text-xl mb-2 mr-2">Import Wish History</h1>
|
||||||
|
<Button size="sm" on:click={() => toggleFaqs(true)}>
|
||||||
|
<Icon path={mdiHelpCircle} color="white" />
|
||||||
|
FAQS
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
<div class="flex mt-4 flex-wrap">
|
||||||
|
<button on:click={() => changeSelectedType('pc')} class={`pill ${selectedType === 'pc' ? 'active' : ''}`}>
|
||||||
|
PC
|
||||||
|
</button>
|
||||||
|
<!-- <button
|
||||||
|
on:click={() => changeSelectedType('pclocal')}
|
||||||
|
class={`pill ${selectedType === 'pclocal' ? 'active' : ''}`}
|
||||||
|
>
|
||||||
|
PC Local
|
||||||
|
</button> -->
|
||||||
|
<button
|
||||||
|
on:click={() => changeSelectedType('android')}
|
||||||
|
class={`pill ${selectedType === 'android' ? 'active' : ''}`}
|
||||||
|
>
|
||||||
|
Android
|
||||||
|
</button>
|
||||||
|
<button on:click={() => changeSelectedType('ios')} class={`pill ${selectedType === 'ios' ? 'active' : ''}`}>
|
||||||
|
iOS
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
{#if selectedType === 'pc'}
|
||||||
|
<div class="bg-background rounded-xl px-4 py-2 text-white mb-4 mt-2">
|
||||||
|
<ol class="list-decimal ml-4">
|
||||||
|
<li class="my-2">Open Paimon menu [ESC]</li>
|
||||||
|
<li class="my-2">Click Feedback</li>
|
||||||
|
<li class="my-2">Wait for it to load and a browser page should open</li>
|
||||||
|
<li class="my-2">Copy & paste the link to the textbox below</li>
|
||||||
|
</ol>
|
||||||
|
</div>
|
||||||
|
<Input bind:value={genshinLink} placeholder="Paste link here... https://webstatic..." />
|
||||||
|
{:else if selectedType === 'pclocal'}
|
||||||
|
<div class="bg-background rounded-xl px-4 py-2 text-white mb-4 mt-2">
|
||||||
|
<ol class="list-decimal ml-4">
|
||||||
|
<li class="mt-2 mb-0">
|
||||||
|
Downlod the importer app <Button size="sm" on:click={() => toggleFaqs(true)}>
|
||||||
|
<Icon path={mdiDownload} color="white" />
|
||||||
|
Download
|
||||||
|
</Button>
|
||||||
|
</li>
|
||||||
|
<li class="my-2">Open the wish history on your Genshin impact in this PC</li>
|
||||||
|
<li class="my-2">Press IMPORT</li>
|
||||||
|
<li class="my-2">Copy & paste the generated text to the textbox below</li>
|
||||||
|
</ol>
|
||||||
|
</div>
|
||||||
|
<Textarea bind:value={generatedTextInput} placeholder="Paste the generated text here..." />
|
||||||
|
{:else if selectedType === 'android'}
|
||||||
|
<div class="bg-background rounded-xl px-4 py-2 text-white mb-4 mt-2">
|
||||||
|
<ol class="list-decimal ml-4">
|
||||||
|
<li class="my-2">Open Paimon menu</li>
|
||||||
|
<li class="my-2">Press Feedback</li>
|
||||||
|
<li class="my-2">Wait for it to load and a feedback page should open</li>
|
||||||
|
<li class="my-2">Turn off your wifi and data connection</li>
|
||||||
|
<li class="my-2">Press refresh on top right corner</li>
|
||||||
|
<li class="my-2">The page should error and show you a link with black font, copy that link</li>
|
||||||
|
<li class="my-2">Turn on your wifi or data connection</li>
|
||||||
|
<li class="my-2">Paste the link to the textbox below</li>
|
||||||
|
</ol>
|
||||||
|
</div>
|
||||||
|
<Input bind:value={genshinLink} placeholder="Paste link here... https://webstatic..." />
|
||||||
|
{:else if selectedType === 'ios'}
|
||||||
|
<div class="bg-background rounded-xl px-4 py-2 text-white mb-4 mt-2">
|
||||||
|
Sorry I don't know yet how to access the link from iOS...<br />If you have the link you can still paste it
|
||||||
|
below.
|
||||||
|
</div>
|
||||||
|
<Input bind:value={genshinLink} placeholder="Paste link here... https://webstatic..." />
|
||||||
|
{/if}
|
||||||
|
{/if}
|
||||||
|
|
||||||
|
<div class="flex justify-end mt-4">
|
||||||
|
{#if !showFaq}
|
||||||
|
<Button on:click={startImport} color="green" className="mr-4">Import</Button>
|
||||||
|
{/if}
|
||||||
|
<Button on:click={showFaq ? () => toggleFaqs(false) : () => closeModal()}>Close</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
|
||||||
|
<style>
|
||||||
|
.pill {
|
||||||
|
@apply rounded-2xl;
|
||||||
|
@apply border-2;
|
||||||
|
@apply border-white;
|
||||||
|
@apply border-opacity-25;
|
||||||
|
@apply text-white;
|
||||||
|
@apply px-4;
|
||||||
|
@apply py-1;
|
||||||
|
@apply mr-2;
|
||||||
|
@apply mb-2;
|
||||||
|
@apply outline-none;
|
||||||
|
@apply transition;
|
||||||
|
@apply duration-100;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
@apply border-primary;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.active {
|
||||||
|
@apply bg-primary;
|
||||||
|
@apply border-primary;
|
||||||
|
@apply text-background;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
|
@ -32,11 +32,21 @@
|
||||||
let legendaryEdit = 0;
|
let legendaryEdit = 0;
|
||||||
let rareEdit = 0;
|
let rareEdit = 0;
|
||||||
|
|
||||||
|
let showRarity = [true, true, false];
|
||||||
|
|
||||||
$: path = `wish-counter-${id}`;
|
$: path = `wish-counter-${id}`;
|
||||||
$: if ($fromRemote) {
|
$: if ($fromRemote) {
|
||||||
readLocalData();
|
readLocalData();
|
||||||
}
|
}
|
||||||
$: sortedPull = pulls.sort((a, b) => b.time - a.time);
|
$: sortedPull = pulls
|
||||||
|
.filter((e) => {
|
||||||
|
if (e.type === 'character') {
|
||||||
|
return showRarity[5 - characters[e.id].rarity];
|
||||||
|
} else if (e.type === 'weapon') {
|
||||||
|
return showRarity[5 - weaponList[e.id].rarity];
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.sort((a, b) => b.time - a.time);
|
||||||
|
|
||||||
onMount(() => {
|
onMount(() => {
|
||||||
readLocalData();
|
readLocalData();
|
||||||
|
@ -46,6 +56,10 @@
|
||||||
isDetailOpen = !isDetailOpen;
|
isDetailOpen = !isDetailOpen;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function toggleShowRarity(index) {
|
||||||
|
showRarity[index] = !showRarity[index];
|
||||||
|
}
|
||||||
|
|
||||||
function openAddModal(pity) {
|
function openAddModal(pity) {
|
||||||
openModal(
|
openModal(
|
||||||
AddModal,
|
AddModal,
|
||||||
|
@ -124,7 +138,7 @@
|
||||||
isEdit = false;
|
isEdit = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
function readLocalData() {
|
export function readLocalData() {
|
||||||
console.log('wish read local');
|
console.log('wish read local');
|
||||||
const data = readSave(path);
|
const data = readSave(path);
|
||||||
if (data !== null) {
|
if (data !== null) {
|
||||||
|
@ -152,21 +166,42 @@
|
||||||
|
|
||||||
total += val;
|
total += val;
|
||||||
|
|
||||||
rare += val;
|
|
||||||
if (rare >= 10) {
|
|
||||||
openAddModal(rare);
|
|
||||||
rare = 0;
|
|
||||||
} else if (rare < 0) {
|
|
||||||
rare = 9;
|
|
||||||
}
|
|
||||||
|
|
||||||
legendary += val;
|
legendary += val;
|
||||||
|
let filler = val;
|
||||||
if (legendary >= legendaryPity) {
|
if (legendary >= legendaryPity) {
|
||||||
openAddModal(legendary);
|
openAddModal(Math.min(rare, legendaryPity));
|
||||||
legendary = 0;
|
legendary = 0;
|
||||||
rare = 0;
|
rare = 0;
|
||||||
|
filler--;
|
||||||
} else if (legendary < 0) {
|
} else if (legendary < 0) {
|
||||||
legendary = 89;
|
legendary = 89;
|
||||||
|
} else {
|
||||||
|
rare += val;
|
||||||
|
if (rare >= 10) {
|
||||||
|
openAddModal(Math.min(rare, 10));
|
||||||
|
rare = 0;
|
||||||
|
filler--;
|
||||||
|
} else if (rare < 0) {
|
||||||
|
rare = 9;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (filler > 0) {
|
||||||
|
pulls = [
|
||||||
|
...pulls,
|
||||||
|
...[...new Array(filler)].map((e) => ({
|
||||||
|
type: 'unknown_3_star',
|
||||||
|
id: 'unknown_3_star',
|
||||||
|
time: dayjs().unix(),
|
||||||
|
pity: 1,
|
||||||
|
})),
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
if (val < 0) {
|
||||||
|
const cloned = [...pulls];
|
||||||
|
cloned.pop();
|
||||||
|
pulls = cloned;
|
||||||
}
|
}
|
||||||
|
|
||||||
saveData();
|
saveData();
|
||||||
|
@ -274,6 +309,17 @@
|
||||||
</div>
|
</div>
|
||||||
<Button size="sm" className="w-16" on:click={() => openAddModal(0)}>Add</Button>
|
<Button size="sm" className="w-16" on:click={() => openAddModal(0)}>Add</Button>
|
||||||
</div>
|
</div>
|
||||||
|
<div class="flex">
|
||||||
|
<button on:click={() => toggleShowRarity(0)} class={`pill legendary ${showRarity[0] ? 'active' : ''}`}>
|
||||||
|
5 <Icon path={mdiStar} size={0.75} className="mb-1" />
|
||||||
|
</button>
|
||||||
|
<button on:click={() => toggleShowRarity(1)} class={`pill rare ${showRarity[1] ? 'active' : ''}`}>
|
||||||
|
4 <Icon path={mdiStar} size={0.75} className="mb-1" />
|
||||||
|
</button>
|
||||||
|
<button on:click={() => toggleShowRarity(2)} class={`pill normal ${showRarity[2] ? 'active' : ''}`}>
|
||||||
|
3 <Icon path={mdiStar} size={0.75} className="mb-1" />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
<table class="w-full">
|
<table class="w-full">
|
||||||
<tr>
|
<tr>
|
||||||
<th class="border-b border-gray-700 text-gray-400 font-display text-left pl-2">Name</th>
|
<th class="border-b border-gray-700 text-gray-400 font-display text-left pl-2">Name</th>
|
||||||
|
@ -285,17 +331,27 @@
|
||||||
{#if pull.type === 'character'}
|
{#if pull.type === 'character'}
|
||||||
<td
|
<td
|
||||||
class={`border-b border-gray-700 py-1 pl-2 font-semibold ${
|
class={`border-b border-gray-700 py-1 pl-2 font-semibold ${
|
||||||
characters[pull.id].rarity === 5 ? 'text-legendary-from' : 'text-rare-from'
|
characters[pull.id].rarity === 5
|
||||||
|
? 'text-legendary-from'
|
||||||
|
: characters[pull.id].rarity === 4
|
||||||
|
? 'text-rare-from'
|
||||||
|
: 'text-primary'
|
||||||
}`}>{characters[pull.id].name}</td
|
}`}>{characters[pull.id].name}</td
|
||||||
>
|
>
|
||||||
{:else}
|
{:else if pull.type === 'weapon'}
|
||||||
<td
|
<td
|
||||||
class={`border-b border-gray-700 py-1 pl-2 font-semibold ${
|
class={`border-b border-gray-700 py-1 pl-2 font-semibold ${
|
||||||
weaponList[pull.id].rarity === 5 ? 'text-legendary-from' : 'text-rare-from'
|
weaponList[pull.id].rarity === 5
|
||||||
|
? 'text-legendary-from'
|
||||||
|
: weaponList[pull.id].rarity === 4
|
||||||
|
? 'text-rare-from'
|
||||||
|
: 'text-primary'
|
||||||
}`}>{weaponList[pull.id].name}</td
|
}`}>{weaponList[pull.id].name}</td
|
||||||
>
|
>
|
||||||
{/if}
|
{/if}
|
||||||
<td class="border-b border-gray-700 text-sm py-1 px-2 whitespace-no-wrap" style="font-family: monospace;">{dayjs.unix(pull.time).format('YYYY-MM-DD HH:mm:ss')}</td>
|
<td class="border-b border-gray-700 text-sm py-1 px-2 whitespace-no-wrap" style="font-family: monospace;"
|
||||||
|
>{dayjs.unix(pull.time).format('YYYY-MM-DD HH:mm:ss')}</td
|
||||||
|
>
|
||||||
<td class="text-right border-b border-gray-700 py-1">{pull.pity}</td>
|
<td class="text-right border-b border-gray-700 py-1">{pull.pity}</td>
|
||||||
</tr>
|
</tr>
|
||||||
{/each}
|
{/each}
|
||||||
|
@ -303,3 +359,40 @@
|
||||||
</div>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
.pill {
|
||||||
|
@apply rounded-2xl;
|
||||||
|
@apply border-2;
|
||||||
|
@apply border-white;
|
||||||
|
@apply border-opacity-25;
|
||||||
|
@apply text-white;
|
||||||
|
@apply px-4;
|
||||||
|
@apply py-1;
|
||||||
|
@apply mr-2;
|
||||||
|
@apply mb-2;
|
||||||
|
@apply outline-none;
|
||||||
|
@apply transition;
|
||||||
|
@apply duration-100;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
@apply border-primary;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.active {
|
||||||
|
@apply bg-primary;
|
||||||
|
@apply border-primary;
|
||||||
|
@apply text-background;
|
||||||
|
|
||||||
|
&.legendary {
|
||||||
|
@apply bg-legendary-from;
|
||||||
|
@apply border-legendary-from;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.rare {
|
||||||
|
@apply bg-rare-from;
|
||||||
|
@apply border-rare-from;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
|
@ -0,0 +1,120 @@
|
||||||
|
<script>
|
||||||
|
import { mdiStar } from '@mdi/js';
|
||||||
|
import { onMount } from 'svelte';
|
||||||
|
import Icon from '../../components/Icon.svelte';
|
||||||
|
import { characters } from '../../data/characters';
|
||||||
|
import { weaponList } from '../../data/weaponList';
|
||||||
|
|
||||||
|
import { readSave, updateTime, fromRemote } from '../../stores/saveManager';
|
||||||
|
import SummaryItem from './_summaryItem.svelte';
|
||||||
|
|
||||||
|
const types = [
|
||||||
|
{
|
||||||
|
name: 'Character Event',
|
||||||
|
id: 'character-event',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'Weapon Event',
|
||||||
|
id: 'weapon-event',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'Standard',
|
||||||
|
id: 'standard',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Beginners' Wish",
|
||||||
|
id: 'beginners',
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
let loading = true;
|
||||||
|
const avg = {};
|
||||||
|
|
||||||
|
$: if ($fromRemote) {
|
||||||
|
readLocalData();
|
||||||
|
}
|
||||||
|
|
||||||
|
$: if ($updateTime) {
|
||||||
|
readLocalData();
|
||||||
|
}
|
||||||
|
|
||||||
|
onMount(() => {
|
||||||
|
readLocalData();
|
||||||
|
});
|
||||||
|
|
||||||
|
export function readLocalData() {
|
||||||
|
console.log('wish summary read local');
|
||||||
|
|
||||||
|
for (let type of types) {
|
||||||
|
const path = `wish-counter-${type.id}`;
|
||||||
|
const data = readSave(path);
|
||||||
|
if (data !== null) {
|
||||||
|
const counterData = JSON.parse(data);
|
||||||
|
const pulls = counterData.pulls || [];
|
||||||
|
const total = counterData.total;
|
||||||
|
|
||||||
|
let legendary = 0;
|
||||||
|
let legendaryPity = 0;
|
||||||
|
let legendaryPulls = [];
|
||||||
|
let rare = 0;
|
||||||
|
let rarePity = 0;
|
||||||
|
for (let pull of pulls) {
|
||||||
|
let rarity;
|
||||||
|
let itemName;
|
||||||
|
if (pull.type === 'character') {
|
||||||
|
rarity = characters[pull.id].rarity;
|
||||||
|
itemName = characters[pull.id].name;
|
||||||
|
} else if (pull.type === 'weapon') {
|
||||||
|
rarity = weaponList[pull.id].rarity;
|
||||||
|
itemName = weaponList[pull.id].name;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (rarity === 5) {
|
||||||
|
legendary++;
|
||||||
|
legendaryPity += pull.pity;
|
||||||
|
legendaryPulls.push({ name: itemName, pity: pull.pity });
|
||||||
|
} else if (rarity === 4) {
|
||||||
|
rare++;
|
||||||
|
rarePity += pull.pity;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
avg[type.id] = {
|
||||||
|
rare: {
|
||||||
|
total: rare,
|
||||||
|
percentage: total > 0 ? rare / total : 0,
|
||||||
|
pity: rare > 0 ? rarePity / rare : 0,
|
||||||
|
},
|
||||||
|
legendary: {
|
||||||
|
total: legendary,
|
||||||
|
percentage: total > 0 ? legendary / total : 0,
|
||||||
|
pity: legendary > 0 ? legendaryPity / legendary : 0,
|
||||||
|
pulls: legendaryPulls,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log(avg);
|
||||||
|
loading = false;
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
{#if !loading}
|
||||||
|
<div class="flex flex-col">
|
||||||
|
{#if avg[types[0].id]}
|
||||||
|
<SummaryItem avg={avg[types[0].id]} type={types[0]} withBottomSpace />
|
||||||
|
{/if}
|
||||||
|
{#if avg[types[1].id]}
|
||||||
|
<SummaryItem avg={avg[types[1].id]} type={types[1]} />
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
<div class="flex flex-col">
|
||||||
|
{#if avg[types[2].id]}
|
||||||
|
<SummaryItem avg={avg[types[2].id]} type={types[2]} withBottomSpace />
|
||||||
|
{/if}
|
||||||
|
{#if avg[types[3].id]}
|
||||||
|
<SummaryItem avg={avg[types[3].id]} type={types[3]} />
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
{/if}
|
|
@ -0,0 +1,90 @@
|
||||||
|
<script>
|
||||||
|
import { mdiStar } from '@mdi/js';
|
||||||
|
|
||||||
|
import Icon from '../../components/Icon.svelte';
|
||||||
|
|
||||||
|
export let withBottomSpace;
|
||||||
|
export let avg;
|
||||||
|
export let type;
|
||||||
|
|
||||||
|
let numberFormat = Intl.NumberFormat('en', {
|
||||||
|
maximumFractionDigits: 1,
|
||||||
|
minimumFractionDigits: 0,
|
||||||
|
});
|
||||||
|
|
||||||
|
function calculateColor(percentage) {
|
||||||
|
const a = [255, 177, 63];
|
||||||
|
const b = [255, 77, 77];
|
||||||
|
|
||||||
|
const av = percentage;
|
||||||
|
const bv = 1 - percentage;
|
||||||
|
|
||||||
|
const color = [a[0] * av + b[0] * bv, a[1] * av + b[1] * bv, a[2] * av + b[2] * bv];
|
||||||
|
|
||||||
|
return `color: rgb(${color[0]},${color[1]},${color[2]});`;
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<div class={`bg-item rounded-xl p-4 flex flex-col w-full ${withBottomSpace ? 'mb-4' : ''}`} style="height: min-content;">
|
||||||
|
<table>
|
||||||
|
<tr>
|
||||||
|
<td class="text-white text-md font-semibold pr-2 md:pr-4 flex-1 w-full">{type.name}</td>
|
||||||
|
<td class="text-gray-400 text-sm font-display pr-2 md:pr-4 text-right">Total</td>
|
||||||
|
<td class="text-gray-400 text-sm font-display pr-2 md:pr-4 text-right">Percent</td>
|
||||||
|
<td class="text-gray-400 text-sm font-display text-right whitespace-no-wrap">Pity AVG</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td class="text-legendary-from font-semibold pr-2 md:pr-4 border-t border-gray-700">
|
||||||
|
5 <Icon path={mdiStar} color="#FFB13F" size="0.6" />
|
||||||
|
</td>
|
||||||
|
<td class="text-legendary-from font-semibold pr-2 md:pr-4 text-right border-t border-gray-700">
|
||||||
|
{numberFormat.format(avg.legendary.total)}
|
||||||
|
</td>
|
||||||
|
<td class="text-legendary-from font-semibold pr-2 md:pr-4 text-right border-t border-gray-700">
|
||||||
|
{numberFormat.format(avg.legendary.percentage * 100)}%
|
||||||
|
</td>
|
||||||
|
<td class="text-legendary-from font-semibold text-right border-t border-gray-700">
|
||||||
|
{numberFormat.format(avg.legendary.pity)}
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td class="text-rare-from font-semibold pr-2 md:pr-4 border-t border-gray-700">
|
||||||
|
4 <Icon path={mdiStar} color="#AD76B0" size="0.6" />
|
||||||
|
</td>
|
||||||
|
<td class="text-rare-from font-semibold pr-2 md:pr-4 text-right border-t border-gray-700">
|
||||||
|
{numberFormat.format(avg.rare.total)}
|
||||||
|
</td>
|
||||||
|
<td class="text-rare-from font-semibold pr-2 md:pr-4 text-right border-t border-gray-700">
|
||||||
|
{numberFormat.format(avg.rare.percentage * 100)}%
|
||||||
|
</td>
|
||||||
|
<td class="text-rare-from font-semibold text-right border-t border-gray-700">
|
||||||
|
{numberFormat.format(avg.rare.pity)}
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</table>
|
||||||
|
{#if avg.legendary.pulls.length > 0}
|
||||||
|
<div class="flex flex-wrap mt-2">
|
||||||
|
{#each avg.legendary.pulls as pull}
|
||||||
|
<span class="pity">{pull.name} <span style={calculateColor((90 - pull.pity) / 90)}>{pull.pity}</span></span>
|
||||||
|
{/each}
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
span.pity {
|
||||||
|
@apply rounded-xl;
|
||||||
|
@apply text-gray-400;
|
||||||
|
@apply border;
|
||||||
|
@apply border-legendary-from;
|
||||||
|
@apply whitespace-no-wrap;
|
||||||
|
@apply px-2;
|
||||||
|
@apply mb-1;
|
||||||
|
@apply mr-1;
|
||||||
|
|
||||||
|
& > span {
|
||||||
|
@apply font-semibold;
|
||||||
|
@apply pl-1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
|
@ -1,14 +1,22 @@
|
||||||
<script>
|
<script>
|
||||||
import { mdiHelpCircle } from '@mdi/js';
|
import { mdiDatabaseImport, mdiHelpCircle } from '@mdi/js';
|
||||||
import { getContext } from 'svelte';
|
import { getContext } from 'svelte';
|
||||||
|
|
||||||
import Button from '../../components/Button.svelte';
|
import Button from '../../components/Button.svelte';
|
||||||
import Icon from '../../components/Icon.svelte';
|
import Icon from '../../components/Icon.svelte';
|
||||||
import HowToModal from '../../components/WishCounterHowToModal.svelte';
|
import HowToModal from '../../components/WishCounterHowToModal.svelte';
|
||||||
|
import ImportModal from '../../components/WishImportModal.svelte';
|
||||||
|
|
||||||
|
import Summary from './_summary.svelte';
|
||||||
import Counter from './_counter.svelte';
|
import Counter from './_counter.svelte';
|
||||||
|
|
||||||
const { open: openModal } = getContext('simple-modal');
|
const { open: openModal, close: closeModal } = getContext('simple-modal');
|
||||||
|
|
||||||
|
let counter1;
|
||||||
|
let counter2;
|
||||||
|
let counter3;
|
||||||
|
let counter4;
|
||||||
|
let summary;
|
||||||
|
|
||||||
function openHowTo() {
|
function openHowTo() {
|
||||||
openModal(
|
openModal(
|
||||||
|
@ -20,30 +28,68 @@
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function openImport() {
|
||||||
|
openModal(
|
||||||
|
ImportModal,
|
||||||
|
{
|
||||||
|
closeModal: closeImportModal,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
closeButton: false,
|
||||||
|
closeOnOuterClick: false,
|
||||||
|
styleWindow: { background: '#25294A', width: '800px' },
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function closeImportModal() {
|
||||||
|
closeModal();
|
||||||
|
counter1.readLocalData();
|
||||||
|
counter2.readLocalData();
|
||||||
|
counter3.readLocalData();
|
||||||
|
counter4.readLocalData();
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<svelte:head>
|
<svelte:head>
|
||||||
<title>Wish Counter - Paimon.moe</title>
|
<title>Wish Counter - Paimon.moe</title>
|
||||||
<meta
|
<meta
|
||||||
name="description"
|
name="description"
|
||||||
content="Genshin Impact Wish Counter to track your pity counter and track when you get the character or weapon"
|
content="Genshin Impact Wish Counter to track your pity counter and track when you get the character or weapon. You can also auto import the logs from your PC."
|
||||||
/>
|
/>
|
||||||
<meta
|
<meta
|
||||||
property="og:description"
|
property="og:description"
|
||||||
content="Genshin Impact Wish Counter to track your pity counter and track when you get the character or weapon"
|
content="Genshin Impact Wish Counter to track your pity counter and track when you get the character or weapon. You can also auto import the logs from your PC."
|
||||||
/>
|
/>
|
||||||
</svelte:head>
|
</svelte:head>
|
||||||
<div class="pt-20 lg:ml-64 lg:pt-8 px-4 md:px-8">
|
<div class="pt-20 lg:ml-64 lg:pt-8 px-4 md:px-8">
|
||||||
<div class="flex flex-col md:flex-row mb-4 items-center">
|
<div class="flex flex-col md:flex-row mb-4 items-center">
|
||||||
<h1 class="font-display font-black text-5xl text-white text-center md:text-left md:mr-4">Wish Counter</h1>
|
<h1 class="font-display font-black text-5xl text-white text-center md:text-left md:mr-4">Wish Counter</h1>
|
||||||
<Button on:click={openHowTo}>
|
<Button on:click={openHowTo} className="hidden md:block">
|
||||||
<Icon size={0.8} path={mdiHelpCircle} />
|
<Icon size={0.8} path={mdiHelpCircle} />
|
||||||
How To Use
|
How To Use
|
||||||
</Button>
|
</Button>
|
||||||
|
<Button className="ml-2 hidden md:block" on:click={openImport}>
|
||||||
|
<Icon size={0.8} path={mdiDatabaseImport} />
|
||||||
|
Auto Import
|
||||||
|
</Button>
|
||||||
|
<div class="md:hidden flex flex-wrap justify-center">
|
||||||
|
<Button className="m-1" on:click={openHowTo}>
|
||||||
|
<Icon size={0.8} path={mdiHelpCircle} />
|
||||||
|
How To Use
|
||||||
|
</Button>
|
||||||
|
<Button className="m-1" on:click={openImport}>
|
||||||
|
<Icon size={0.8} path={mdiDatabaseImport} />
|
||||||
|
Auto Import
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="grid gap-4 grid-cols-1 md:grid-cols-2 xl:grid-cols-3 max-w-screen-xl">
|
<div class="grid gap-4 grid-cols-1 md:grid-cols-2 xl:grid-cols-3 max-w-screen-xl">
|
||||||
<Counter id="character-event" name="Character Event" />
|
<Counter bind:this={counter1} id="character-event" name="Character Event" />
|
||||||
<Counter id="weapon-event" name="Weapon Event" legendaryPity={80} />
|
<Counter bind:this={counter2} id="weapon-event" name="Weapon Event" legendaryPity={80} />
|
||||||
<Counter id="standard" name="Standard" />
|
<Counter bind:this={counter3} id="standard" name="Standard" />
|
||||||
|
<Counter bind:this={counter4} id="beginners" name="Beginners' Wish" />
|
||||||
|
<Summary bind:this={summary} />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -25,7 +25,7 @@ module.exports = {
|
||||||
to: '#665680',
|
to: '#665680',
|
||||||
},
|
},
|
||||||
legendary: {
|
legendary: {
|
||||||
from: '#B9812E',
|
from: '#FFB13F',
|
||||||
to: '#846332',
|
to: '#846332',
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|
Loading…
Reference in New Issue