Add excel import

pull/1/head
Made Baruna 2021-06-01 16:02:32 +07:00
parent 464a45db98
commit 18475ae76f
No known key found for this signature in database
GPG Key ID: 5AA5DA16AA5DCEAD
8 changed files with 596 additions and 82 deletions

View File

@ -447,20 +447,14 @@
}
if (rarity === 5) {
if (combined[i].pity === 0) {
combined[i].pity = legendary;
}
combined[i].pity = legendary;
legendary = 0;
// rare = 0;
} else if (rarity === 4) {
if (combined[i].pity === 0) {
combined[i].pity = rare;
}
combined[i].pity = rare;
rare = 0;
} else {
if (combined[i].pity === 0) {
combined[i].pity = 1;
}
combined[i].pity = 1;
}
}
@ -743,7 +737,7 @@
{/if}
<div class="flex flex-col md:flex-row mt-4 md:justify-end items-center">
{#if !showFaq}
<!-- {#if !showFaq}
<div class="flex-1 flex mb-4 md:mb-0 md:ml-4">
<Checkbox disabled={false} bind:checked={newOnly}>
<span class="text-white select-none">{$t('wish.import.importNewWishOnly')}</span>
@ -753,7 +747,7 @@
<span class="tooltip-content">{$t('wish.import.importNewWishUncheck')}</span>
</span>
</div>
{/if}
{/if} -->
<div>
{#if !showFaq}
<Button on:click={startImport} color="green" className="mr-4">{$t('wish.import.import')}</Button>
@ -791,7 +785,7 @@
}
}
.tooltip {
/* .tooltip {
@apply relative;
.tooltip-content {
@ -815,6 +809,6 @@
&:hover .tooltip-content {
@apply block;
}
}
} */
</style>

View File

@ -104,6 +104,8 @@
"title": "Wish Counter",
"autoImport": "Auto Import",
"helpAndSetting": "Help & Settings",
"helps": "Help",
"settings": "Settings",
"wishesWorth": "Wishes Worth",
"lifetimePulls": "Lifetime Pulls",
"guarantee": "Guaranteed at {pity}",
@ -237,13 +239,41 @@
]
}
},
"excel": {
"title": "Excel Importer",
"subtitle": "Select where your excel come from:",
"default": "Paimon.moe Export",
"takagg": "TakaGG Gacha Export",
"notice": [
"This feature still in BETA please backup first by going to Setting then Export to Excel!",
"Wish with the same timestamp and reward name will NOT be touched (so existing wish will not be rewritten)",
"Will only append and prepend wishes, this mean nothing will be inserted on the middle of the list",
"Currently only support excel with ENGLISH reward name"
],
"selectFile": {
"default": "Drag & drop Paimon.moe excel file here, or click here to select",
"takagg": "Drag & drop TakaGG gacha export excel file here, or click here to select"
},
"processing": "Processing...",
"addedOn": "Inserted on the:",
"beginning": "Beginning",
"end": "End",
"total": "Total",
"saveNotice": "Save the imported wishes?",
"save": "Save",
"success": "Excel import success 😀!",
"errorInvalidFile": "Invalid excel file",
"errorReadExcel": "Error reading the excel file!",
"errorExcelPaimon": "This is not excel from paimon.moe export",
"errorUnknownItem": "Got an unknown reward name"
},
"help": {
"title": "Wish Counter Help & Settings",
"exportTitle": "Export Wish History",
"exportMessage": "You can export your wish history to an excel file here",
"exportTitle": "Export & Import Wish History",
"exportMessage": "You can export your wish history to an excel file here. You can also import the exported excel from paimon.moe or TakaGG Gacha Export.",
"export": "Export to Excel",
"exporting": "Exporting...",
"import": "Import",
"import": "Import From Excel",
"exportFinish": "Export success, please wait until the browser download the file!",
"wishTallyTitle": "Submit Wish Tally",
"wishTally": "We are doing a global wish tally! You can submit your wish tally to participate. All pity data will be aggregated to know what is the average pity of paimon.moe users.",

View File

@ -0,0 +1,434 @@
<script>
import { t } from 'svelte-i18n';
import { Workbook } from 'exceljs';
import dayjs from 'dayjs';
import Button from '../../components/Button.svelte';
import { pushToast } from '../../stores/toast';
import { weaponList } from '../../data/weaponList';
import { characters } from '../../data/characters';
import { getAccountPrefix } from '../../stores/account';
import { readSave, updateSave } from '../../stores/saveManager';
export let closeModal;
const bannerCategories = {
'character-event': 'Character Event',
'weapon-event': 'Weapon Event',
standard: 'Standard',
beginners: "Beginners' Wish",
};
let selectedType = 'default';
let fileInput;
let step = 0;
let loading = false;
let added = {};
function changeType(type) {
selectedType = type;
}
function selectFile() {
fileInput.click();
}
async function readLocalData(id) {
const prefix = getAccountPrefix();
const path = `wish-counter-${id}`;
const data = await readSave(`${prefix}${path}`);
let pulls = [];
let newest = dayjs().year(2000);
let oldest = dayjs().year(2200);
if (data !== null) {
newest = dayjs(data.pulls[data.pulls.length - 1].time);
oldest = dayjs(data.pulls[0].time);
pulls = data.pulls;
}
return {
path: `${prefix}${path}`,
data: pulls,
newest,
oldest,
};
}
async function processWishes(path, combined) {
let rare = 0;
let legendary = 0;
for (let i = 0; i < combined.length; i++) {
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 = {
total: combined.length,
legendary,
rare,
pulls: combined,
};
await updateSave(path, data);
}
async function save() {
loading = true;
for (const id of Object.keys(bannerCategories)) {
const { path, data } = await readLocalData(id);
const { append, prepend } = added[id];
const beginning = prepend.map((e) => ({
id: e[2],
time: e[1],
type: e[0],
pity: 0,
}));
const end = append.map((e) => ({
id: e[2],
time: e[1],
type: e[0],
pity: 0,
}));
const combined = [...beginning, ...data, ...end];
await processWishes(path, combined);
}
const prefix = getAccountPrefix();
await updateSave(`${prefix}collectables-updated`, true);
pushToast($t('wish.excel.success'));
loading = false;
closeModal();
}
async function parseData(id, imported) {
const { newest, oldest } = await readLocalData(id);
const append = [];
const prepend = [];
console.log(newest, oldest);
// new wishes
imported.reverse();
let index = 0;
for (const row of imported) {
if (dayjs(row[1]).isAfter(newest)) {
append.push(row);
index++;
} else {
break;
}
}
imported = imported.slice(index, imported.length);
// old wishes
imported.reverse();
for (const row of imported) {
if (dayjs(row[1]).isBefore(oldest)) {
prepend.push(row);
} else {
break;
}
}
append.reverse();
console.log(prepend);
console.log(append);
added[id] = {
append,
prepend,
};
}
async function readPaimonExcel(workbook) {
const informationSheet = workbook.getWorksheet('Information');
if (!informationSheet) {
pushToast($t('wish.excel.errorExcelPaimon'), 'error');
loading = false;
return;
}
const row = informationSheet.getCell('A1').value;
if (row !== 'Paimon.moe Wish History Export') {
pushToast($t('wish.excel.errorExcelPaimon'), 'error');
loading = false;
return;
}
const bannerCategories = {
'character-event': 'Character Event',
'weapon-event': 'Weapon Event',
standard: 'Standard',
beginners: "Beginners' Wish",
};
const weapons = Object.values(weaponList);
const chars = Object.values(characters);
for (const [id, name] of Object.entries(bannerCategories)) {
const sheet = workbook.getWorksheet(name);
const wishes = [];
sheet.eachRow((row, index) => {
if (index === 1) return;
const type = row.getCell(1).text.toLowerCase();
const time = row.getCell(3).text;
const fullName = row.getCell(2).text;
let name = '';
if (type === 'weapon') {
name = weapons.find((e) => e.name === fullName).id;
} else if (type === 'character') {
name = chars.find((e) => e.name === fullName).id;
}
if (name === '') {
pushToast($t('wish.excel.errorUnknownItem'), 'error');
loading = false;
throw 'unknown reward name';
}
wishes.push([type, time, name]);
});
console.log('from excel', name, wishes.length);
await parseData(id, wishes);
}
step = 1;
loading = false;
}
async function readGachaExportExcel(workbook) {
const bannerCategories = {
'character-event': 'Character Event Wish',
'weapon-event': 'Weapon Event Wish',
standard: 'Permanent Wish',
beginners: "Novice Wishes",
};
const weapons = Object.values(weaponList);
const chars = Object.values(characters);
for (const [id, name] of Object.entries(bannerCategories)) {
const sheet = workbook.getWorksheet(name);
const wishes = [];
sheet.eachRow((row, index) => {
if (index === 1) return;
const type = row.getCell(3).text.toLowerCase();
const time = row.getCell(1).text;
const fullName = row.getCell(2).text;
let name = '';
if (type === 'weapon') {
name = weapons.find((e) => e.name === fullName).id;
} else if (type === 'character') {
name = chars.find((e) => e.name === fullName).id;
}
if (name === '') {
pushToast($t('wish.excel.errorUnknownItem'), 'error');
loading = false;
throw 'unknown reward name';
}
wishes.push([type, time, name]);
});
console.log('from excel', name, wishes.length);
await parseData(id, wishes);
}
step = 1;
loading = false;
}
async function readExcel(file) {
loading = true;
const workbook = new Workbook();
try {
const buffer = await fileToBuffer(file);
await workbook.xlsx.load(buffer);
} catch (err) {
pushToast($t('wish.excel.errorReadExcel'), 'error');
loading = false;
}
try {
if (selectedType === 'default') {
readPaimonExcel(workbook);
} else {
readGachaExportExcel(workbook);
}
} catch (err) {
console.log(err);
pushToast($t('wish.excel.errorReadExcel'), 'error');
loading = false;
}
}
function checkFile(file) {
console.log(file.type);
if (file.type === 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet') {
readExcel(file);
} else {
pushToast($t('wish.excel.errorInvalidFile'), 'error');
}
}
function onFileChange(event) {
const target = event.target;
const { files } = target;
if (files === null || files.length === 0) return;
const file = files[0];
checkFile(file);
fileInput.value = null;
}
function dropHandler(ev) {
ev.preventDefault();
if (ev.dataTransfer.items) {
if (ev.dataTransfer.items[0].kind === 'file') {
const file = ev.dataTransfer.items[0].getAsFile();
checkFile(file);
}
} else {
checkFile(ev.dataTransfer.files[0]);
}
}
function dragOverHandler(ev) {
ev.preventDefault();
}
const fileToBuffer = (file) =>
new Promise((resolve, reject) => {
const reader = new FileReader();
reader.onload = (event) => {
if (event.target === null) {
reject(new Error('Failed to read file'));
} else {
const buffer = event.target.result;
resolve(buffer);
}
};
reader.readAsArrayBuffer(file);
});
</script>
<div>
<h1 class="font-display text-white text-xl mb-2">{$t('wish.excel.title')}</h1>
{#if step === 0}
<div class="mb-4 bg-background rounded-xl p-4">
<ol class="list-decimal ml-4">
<li class="text-red-300">{$t('wish.excel.notice.0')}</li>
<li class="text-white">{$t('wish.excel.notice.1')}</li>
<li class="text-white">{$t('wish.excel.notice.2')}</li>
<li class="text-white">{$t('wish.excel.notice.3')}</li>
</ol>
</div>
<p class="text-gray-200 mb-2">{$t('wish.excel.subtitle')}</p>
<div class="flex flex-row">
<button on:click={() => changeType('default')} class={`pill ${selectedType === 'default' ? 'active' : ''}`}>
{$t('wish.excel.default')}
</button>
<button on:click={() => changeType('takagg')} class={`pill ${selectedType === 'takagg' ? 'active' : ''}`}>
{$t('wish.excel.takagg')}
</button>
</div>
<input on:change={onFileChange} type="file" style="display: none;" bind:this={fileInput} />
<!-- <Button disabled={loading} on:click={selectFile}>
{loading ? $t('wish.excel.processing') : $t(`wish.excel.selectFile.${selectedType}`)}
</Button> -->
<div
on:click={selectFile}
on:drop={dropHandler}
on:dragover={dragOverHandler}
class="w-full h-32 rounded-xl border-dashed border-2 border-gray-400 flex items-center justify-center cursor-pointer p-8"
>
<p class="text-white">{loading ? $t('wish.excel.processing') : $t(`wish.excel.selectFile.${selectedType}`)}</p>
</div>
{/if}
{#if step === 1}
<table>
<tr>
<td class="px-2 text-white border-r border-gray-700">{$t('wish.excel.addedOn')}</td>
<td class="px-2 text-white border-r border-gray-700 text-center">{$t('wish.excel.beginning')}</td>
<td class="px-2 text-white border-r border-gray-700 text-center">{$t('wish.excel.end')}</td>
<td class="px-2 text-white border-gray-700 text-center">{$t('wish.excel.total')}</td>
</tr>
{#each Object.entries(added) as [id, data]}
<tr>
<td class="px-2 text-white border-r border-t border-gray-700">{bannerCategories[id]}</td>
<td class="px-2 text-white border-r border-t border-gray-700 text-center">{data.prepend.length}</td>
<td class="px-2 text-white border-r border-t border-gray-700 text-center">{data.append.length}</td>
<td class="px-2 text-white border-t border-gray-700 text-center">
{data.prepend.length + data.append.length}
</td>
</tr>
{/each}
</table>
<p class="text-white py-2">{$t('wish.excel.saveNotice')}</p>
<Button disabled={loading} on:click={save}>{$t('wish.excel.save')}</Button>
{/if}
</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;
}
}
</style>

View File

@ -1,61 +1,12 @@
<script>
import { t } from 'svelte-i18n';
import { mdiCheckCircleOutline, mdiLoading, mdiPencil, mdiStar } from '@mdi/js';
import Icon from './Icon.svelte';
import Button from './Button.svelte';
import Checkbox from '../components/Checkbox.svelte';
import { exportToExcel } from '../functions/export';
import { submitWishTally } from '../functions/wishTally';
import { pushToast } from '../stores/toast';
export let setManualInput;
export let settings;
let loadingExport = false;
let wishTallySubmitted = false;
let enableManual = settings.manualInput;
function toggleManual() {
setManualInput(enableManual);
}
async function exportFile() {
loadingExport = true;
await exportToExcel();
loadingExport = false;
pushToast($t('wish.help.exportFinish'));
}
$: enableManual, toggleManual();
import { mdiPencil, mdiStar } from '@mdi/js';
import Icon from '../../components/Icon.svelte';
</script>
<div>
<!-- <h1 class="font-display text-white text-xl mb-4">{$t('wish.help.title')}</h1> -->
<h1 class="font-display text-white text-xl mb-2">{$t('wish.help.exportTitle')}</h1>
<div class="text-white p-2 bg-background rounded-xl">
<p class="mb-2">{$t('wish.help.exportMessage')}</p>
<Button className="mr-2" disabled={loadingExport} on:click={exportFile}>
{#if loadingExport}
<Icon path={mdiLoading} spin size={0.8} className="mr-2" />
{/if}
{$t(loadingExport ? 'wish.help.exporting' : 'wish.help.export')}
</Button>
<!-- <Button disabled={loadingExport}>{$t('wish.help.import')}</Button> -->
</div>
<h1 class="font-display text-white text-xl mt-8 mb-2">{$t('wish.help.manualTitle')}</h1>
<div class="text-white p-2 bg-background rounded-xl">
<div class="py-2 pl-4">
<Checkbox disabled={false} bind:checked={enableManual}
><span class="select-none cursor-pointer">{$t('wish.help.enableManual')}</span></Checkbox
>
</div>
<p class="text-red-300">{$t('wish.help.notice')}</p>
<p>{$t('wish.help.consider')}</p>
</div>
<h1 class="font-display text-white text-xl mt-8 mb-2">{$t('wish.help.howto.title')}</h1>
<h1 class="font-display text-white text-xl mb-2">{$t('wish.help.howto.title')}</h1>
<div class="text-white p-2 bg-background rounded-xl">
<p class="mb-2">{$t('wish.help.howto.subtitle')}</p>
<p class="mb-2">

View File

@ -0,0 +1,74 @@
<script>
import { t } from 'svelte-i18n';
import { mdiLoading } from '@mdi/js';
import Icon from '../../components/Icon.svelte';
import Button from '../../components/Button.svelte';
import Checkbox from '../../components/Checkbox.svelte';
import { exportToExcel } from '../../functions/export';
import { pushToast } from '../../stores/toast';
import ExcelImportModal from './_excelImport.svelte';
import { getContext } from 'svelte';
const { open: openModal, close: closeModal } = getContext('simple-modal');
export let setManualInput;
export let settings;
export let closeImportModal;
let loadingExport = false;
let enableManual = settings.manualInput;
function toggleManual() {
setManualInput(enableManual);
}
async function exportFile() {
loadingExport = true;
await exportToExcel();
loadingExport = false;
pushToast($t('wish.help.exportFinish'));
}
function openImporter() {
openModal(
ExcelImportModal,
{
closeModal: closeImportModal
},
{
closeButton: false,
styleWindow: { background: '#25294A', width: '800px' },
},
);
}
$: enableManual, toggleManual();
</script>
<div>
<h1 class="font-display text-white text-xl mb-2">{$t('wish.help.exportTitle')}</h1>
<div class="text-white p-2 bg-background rounded-xl">
<p class="mb-2">{$t('wish.help.exportMessage')}</p>
<Button className="mr-2" disabled={loadingExport} on:click={exportFile}>
{#if loadingExport}
<Icon path={mdiLoading} spin size={0.8} className="mr-2" />
{/if}
{$t(loadingExport ? 'wish.help.exporting' : 'wish.help.export')}
</Button>
<Button disabled={loadingExport} on:click={openImporter}>{$t('wish.help.import')}</Button>
</div>
<h1 class="font-display text-white text-xl mt-8 mb-2">{$t('wish.help.manualTitle')}</h1>
<div class="text-white p-2 bg-background rounded-xl">
<div class="py-2 pl-4">
<Checkbox disabled={false} bind:checked={enableManual}
><span class="select-none cursor-pointer">{$t('wish.help.enableManual')}</span></Checkbox
>
</div>
<p class="text-red-300">{$t('wish.help.notice')}</p>
<p>{$t('wish.help.consider')}</p>
</div>
</div>

View File

@ -3,6 +3,7 @@
import { onMount } from 'svelte';
import dayjs from 'dayjs';
import debounce from 'lodash/debounce';
import { characters } from '../../data/characters';
import { weaponList } from '../../data/weaponList';
@ -24,14 +25,16 @@
let wishCount = 0;
const avg = {};
$: if ($fromRemote) {
const readDebounced = debounce(() => {
readLocalData();
}, 1000);
$: if ($fromRemote) {
readDebounced();
}
$: if ($updateTime) {
setTimeout(() => {
readLocalData();
}, 1000);
readDebounced();
}
onMount(async () => {
@ -76,6 +79,8 @@
},
};
export async function readLocalData() {
let totalWish = 0;
console.log('wish summary read local');
@ -94,7 +99,7 @@
}
updateCollectedCharacters = true;
} else {
collectedCharacters = {...defaultChars};
collectedCharacters = JSON.parse(JSON.stringify(defaultChars));
}
const collectablesNeedUpdateData = await readSave(`${prefix}collectables-updated`);
if (collectablesNeedUpdateData === null || collectablesNeedUpdateData === true) {

View File

@ -86,7 +86,7 @@
</tr>
</table>
{#if avg.legendary.pulls.length > 0}
<div class="flex flex-wrap mt-2 overflow-y-auto" style="max-height: 300px;">
<div class="flex flex-wrap mt-2 overflow-y-auto" style="max-height: 500px;">
{#each avg.legendary.pulls as pull}
<span class="pity">{pull.name} <span style={calculateColor((90 - pull.pity) / 90)}>{pull.pity}</span></span>
{/each}

View File

@ -1,12 +1,11 @@
<script>
import { t } from 'svelte-i18n';
import { mdiDatabaseImport, mdiHelpCircle } from '@mdi/js';
import { mdiCog, mdiDatabaseImport, mdiHelpCircle } from '@mdi/js';
import { getContext, onMount } from 'svelte';
import Button from '../../components/Button.svelte';
import Icon from '../../components/Icon.svelte';
import HowToModal from '../../components/WishCounterHowToModal.svelte';
import ImportModal from '../../components/WishImportModal.svelte';
import { fromRemote, readSave, updateSave } from '../../stores/saveManager';
@ -14,6 +13,8 @@
import Counter from './_counter.svelte';
import FirstTimePopup from './_firstTime.svelte';
import MonthlyGraph from './_monthlyGraph.svelte';
import HowToModal from './_helpModal.svelte';
import SettingModal from './_settingModal.svelte';
const { open: openModal, close: closeModal } = getContext('simple-modal');
@ -78,9 +79,21 @@
function openHowTo() {
openModal(
HowToModal,
{},
{
closeButton: false,
styleWindow: { background: '#25294A', width: '800px' },
},
);
}
function openSetting() {
openModal(
SettingModal,
{
setManualInput,
settings,
closeImportModal,
},
{
closeButton: false,
@ -111,6 +124,7 @@
counter3.readLocalData();
counter4.readLocalData();
}
</script>
<svelte:head>
@ -131,18 +145,30 @@
<Icon size={0.8} path={mdiDatabaseImport} />
{$t('wish.autoImport')}
</Button>
<Button on:click={openHowTo} className="hidden md:block">
<Icon size={0.8} path={mdiHelpCircle} />
{$t('wish.helpAndSetting')}
{#if settings.manualInput}
<Button on:click={openHowTo} className="mr-2 hidden md:block">
<Icon size={0.8} path={mdiHelpCircle} />
{$t('wish.helps')}
</Button>
{/if}
<Button on:click={openSetting} className="hidden md:block">
<Icon size={0.8} path={mdiCog} />
{$t('wish.settings')}
</Button>
<div class="md:hidden flex flex-wrap justify-center">
<Button className="m-1" on:click={openImport}>
<Icon size={0.8} path={mdiDatabaseImport} />
{$t('wish.autoImport')}
</Button>
<Button className="m-1" on:click={openHowTo}>
<Icon size={0.8} path={mdiHelpCircle} />
{$t('wish.helpAndSetting')}
{#if settings.manualInput}
<Button className="m-1" on:click={openHowTo}>
<Icon size={0.8} path={mdiHelpCircle} />
{$t('wish.helps')}
</Button>
{/if}
<Button className="m-1" on:click={openSetting}>
<Icon size={0.8} path={mdiCog} />
{$t('wish.settings')}
</Button>
</div>
</div>