Merge pull request #1 from MadeBaruna/main

pull/1/head
fadhlu 2021-03-19 12:12:25 +07:00 committed by GitHub
commit 592cf0fd88
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
48 changed files with 1536 additions and 213 deletions

View File

@ -28,6 +28,7 @@
"@rollup/plugin-replace": "^2.3.4",
"@rollup/plugin-url": "^5.0.0",
"autoprefixer": "^10.0.1",
"chart.js": "^2.9.4",
"dayjs": "^1.9.4",
"dotenv": "^8.2.0",
"postcss": "^8.1.2",

View File

@ -9,7 +9,7 @@
}
</script>
<div class="flex items-center lg:hidden fixed w-full h-16 header bg-background z-30 shadow-md overflow-hidden">
<div class="flex items-center lg:hidden fixed w-full h-16 header bg-background z-50 shadow-md overflow-hidden">
<a href="/" class="flex-1 pl-4 md:pl-8 font-display text-3xl font-black text-white relative z-10 pt-2">
Paimon<span class="text-xl text-primary">.moe</span>
</a>

View File

@ -4,13 +4,21 @@
import Icon from '../Icon.svelte';
export let className = '';
export let styleList = '';
export let sort = false;
export let order = false;
export let align = 'left';
export let padding = 'px-4';
</script>
<th class={`text-gray-400 select-none font-display text-lg cursor-pointer px-4 text-${align}`} on:click>
<span class="relative"><slot />
<th
class={`text-gray-400 select-none font-display text-lg cursor-pointer ${padding} text-${align} ${className}`}
style={styleList}
on:click
>
<span class="relative"
><slot />
{#if sort}
<div transition:fade={{ duration: 100 }} class="absolute" style="right: -21px; top: 3px;">
<Icon className={`mb-1 duration-100 ${order ? 'transform -rotate-180' : ''}`} path={mdiChevronDown} />

View File

@ -135,7 +135,7 @@
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('gacha_id', 'b8fd0d8a6c940c7a16a486367de5f6d2232f53');
url.searchParams.set('lang', 'en');
url.searchParams.set('device_type', getDeviceType());
if (region !== '') url.searchParams.set('region', region);
@ -155,10 +155,12 @@
let page = 1;
let result = [];
let lastTime = 0;
let lastId = 0;
do {
if (cancelled) return;
url.searchParams.set('page', page);
url.searchParams.set('end_id', lastId);
currentPage = page;
@ -242,6 +244,7 @@
}
page = page + 1;
lastId = result.length > 0 ? result[result.length - 1].id : 0;
await sleep(1000);
console.log(wishes);
} catch (err) {

178
src/data/banners.js Normal file
View File

@ -0,0 +1,178 @@
export const banners = {
beginners: [
{
name: "Beginners' Wish",
shortName: 'Beginners\' Wish',
image: 1,
start: '2000-01-01 00:00:00',
end: '2200-01-01 00:00:00',
color: '#FFFFFF',
},
],
standard: [
{
name: 'Wanderlust Invocation',
shortName: 'Wanderlust Invocation',
image: 1,
start: '2000-01-01 00:00:00',
end: '2200-01-01 00:00:00',
color: '#FFFFFF',
},
],
characters: [
{
name: 'Ballad in Goblets',
image: 1,
shortName: 'Venti',
start: '2020-09-28 00:00:00',
end: '2020-10-18 18:00:00',
color: '#55E4B0',
},
{
name: 'Sparkling Steps',
image: 1,
shortName: 'Klee',
start: '2020-10-20 18:00:00',
end: '2020-11-10 18:00:00',
color: '#CA360E',
},
{
name: 'Farewell of Snezhnaya',
image: 1,
shortName: 'Tartaglia',
start: '2020-11-11 06:00:00',
end: '2020-12-01 18:00:00',
color: '#50A3C0',
},
{
name: 'Gentry of Hermitage',
image: 1,
shortName: 'Zhongli',
start: '2020-12-01 18:00:00',
end: '2020-12-22 15:00:00',
color: '#D1A55C',
},
{
name: 'Secretum Secretorum',
image: 1,
shortName: 'Albedo',
start: '2020-12-23 10:00:00',
end: '2021-01-12 16:00:00',
color: '#FCFE83',
},
{
name: 'Adrift in the Harbor',
image: 1,
shortName: 'Ganyu',
start: '2021-01-12 18:00:00',
end: '2021-02-02 15:00:00',
color: '#6994DF',
},
{
name: 'Invitation to Mundane Life',
image: 1,
shortName: 'Xiao',
start: '2021-02-03 06:00:00',
end: '2021-02-17 16:00:00',
color: '#2BE3F8',
},
{
name: 'Dance of Lanterns',
image: 1,
shortName: 'Keqing',
start: '2021-02-17 18:00:00',
end: '2021-03-02 16:00:00',
color: '#AB6CD7',
},
{
name: 'Moment of Bloom',
image: 1,
shortName: 'Hu Tao',
start: '2021-03-02 18:00:00',
end: '2021-03-16 15:00:00',
color: '#BF5042',
},
{
name: 'Ballad in Goblets',
image: 2,
shortName: 'Venti',
start: '2021-03-17 06:00:00',
end: '2021-04-06 16:00:00',
color: '#35C297',
},
],
weapons: [
{
name: 'Epitome Invocation',
image: 1,
start: '2020-09-28 00:00:00',
end: '2020-10-18 18:00:00',
shortName: 'Amos',
color: '#f54e42'
},
{
name: 'Epitome Invocation',
image: 2,
start: '2020-10-20 18:00:00',
end: '2020-11-10 18:00:00',
shortName: 'WGS',
color: '#f5c242'
},
{
name: 'Epitome Invocation',
image: 3,
start: '2020-11-11 06:00:00',
end: '2020-12-01 18:00:00',
shortName: 'Skyward',
color: '#f5ef42'
},
{
name: 'Epitome Invocation',
image: 4,
start: '2020-12-01 18:00:00',
end: '2020-12-22 15:00:00',
shortName: 'Vortex',
color: '#7ef542'
},
{
name: 'Epitome Invocation',
image: 5,
start: '2020-12-23 10:00:00',
end: '2021-01-12 16:00:00',
shortName: 'Summit',
color: '#42ecf5'
},
{
name: 'Epitome Invocation',
image: 6,
start: '2021-01-12 18:00:00',
end: '2021-02-02 15:00:00',
shortName: 'Amos',
color: '#424ef5'
},
{
name: 'Epitome Invocation',
image: 7,
start: '2021-02-03 06:00:00',
end: '2021-02-23 16:00:00',
shortName: 'Primordial',
color: '#b042f5'
},
{
name: 'Epitome Invocation',
image: 8,
start: '2021-02-23 18:00:00',
end: '2021-03-16 15:00:00',
shortName: 'Homa',
color: '#f542c8'
},
{
name: 'Epitome Invocation',
image: 9,
start: '2021-03-17 06:00:00',
end: '2021-04-06 16:00:00',
shortName: 'Elegy',
color: '#f54e42'
},
],
};

View File

@ -1071,7 +1071,7 @@ export const weaponList = {
atk: 42,
secondary: 'Elemental Mastery',
type: weapons.sword,
source: 'forgingnorthlander sword prototype x1crystal chunk x50white iron chunk x50',
source: 'forging',
ascension: [
{
items: [
@ -1130,7 +1130,7 @@ export const weaponList = {
atk: 44,
secondary: 'Physical DMG Bonus',
type: weapons.sword,
source: 'adventure rank 10 rewardforging:northlander sword prototype x1crystal chunk x50white iron chunk x50',
source: 'adventure rank 10 reward, forging',
ascension: [
{
items: [
@ -1307,7 +1307,7 @@ export const weaponList = {
atk: 42,
secondary: 'ATK',
type: weapons.bow,
source: 'forgingnorthlander bow prototype x1crystal chunk x50white iron chunk x50',
source: 'forging',
ascension: [
{
items: [
@ -1602,7 +1602,7 @@ export const weaponList = {
atk: 44,
secondary: 'Physical DMG Bonus',
type: weapons.polearm,
source: 'forgingnorthlander polearm prototype x1crystal chunk x50white iron chunk x50',
source: 'forging',
ascension: [
{
items: [
@ -2137,49 +2137,49 @@ export const weaponList = {
ascension: [
{
items: [
{ item: itemList.unknown, amount: 0 },
{ item: itemList.unknown, amount: 0 },
{ item: itemList.unknown, amount: 0 },
{ item: itemList.tile_of_decarabians_tower, amount: 3 },
{ item: itemList.heavy_horn, amount: 3 },
{ item: itemList.divining_scroll, amount: 2 },
],
mora: 5000,
},
{
items: [
{ item: itemList.unknown, amount: 0 },
{ item: itemList.unknown, amount: 0 },
{ item: itemList.unknown, amount: 0 },
{ item: itemList.debris_of_decarabians_city, amount: 3 },
{ item: itemList.heavy_horn, amount: 12 },
{ item: itemList.divining_scroll, amount: 8 },
],
mora: 15000,
},
{
items: [
{ item: itemList.unknown, amount: 0 },
{ item: itemList.unknown, amount: 0 },
{ item: itemList.unknown, amount: 0 },
{ item: itemList.debris_of_decarabians_city, amount: 6 },
{ item: itemList.black_bronze_horn, amount: 6 },
{ item: itemList.sealed_scroll, amount: 6 },
],
mora: 20000,
},
{
items: [
{ item: itemList.unknown, amount: 0 },
{ item: itemList.unknown, amount: 0 },
{ item: itemList.unknown, amount: 0 },
{ item: itemList.fragment_of_decarabians_epic, amount: 3 },
{ item: itemList.black_bronze_horn, amount: 12 },
{ item: itemList.sealed_scroll, amount: 9 },
],
mora: 30000,
},
{
items: [
{ item: itemList.unknown, amount: 0 },
{ item: itemList.unknown, amount: 0 },
{ item: itemList.unknown, amount: 0 },
{ item: itemList.fragment_of_decarabians_epic, amount: 6 },
{ item: itemList.black_crystal_horn, amount: 9 },
{ item: itemList.forbidden_curse_scroll, amount: 6 },
],
mora: 35000,
},
{
items: [
{ item: itemList.unknown, amount: 0 },
{ item: itemList.unknown, amount: 0 },
{ item: itemList.unknown, amount: 0 },
{ item: itemList.scattered_piece_of_decarabians_dream, amount: 4 },
{ item: itemList.black_crystal_horn, amount: 18 },
{ item: itemList.forbidden_curse_scroll, amount: 12 },
],
mora: 45000,
},
@ -2303,65 +2303,65 @@ export const weaponList = {
},
],
},
// alley_hunter: {
// name: 'Alley Hunter',
// id: 'alley_hunter',
// rarity: 4,
// atk: 41,
// secondary: 'CRIT Rate',
// type: weapons.bow,
// source: 'wish',
// ascension: [
// {
// items: [
// { item: itemList.boreal_wolfs_milk_tooth, amount: 3 },
// { item: itemList.dead_ley_line_branch, amount: 3 },
// { item: itemList.treasure_hoarder_insignia, amount: 2 },
// ],
// mora: 5000,
// },
// {
// items: [
// { item: itemList.boreal_wolfs_cracked_tooth, amount: 3 },
// { item: itemList.dead_ley_line_branch, amount: 12 },
// { item: itemList.treasure_hoarder_insignia, amount: 8 },
// ],
// mora: 15000,
// },
// {
// items: [
// { item: itemList.boreal_wolfs_cracked_tooth, amount: 6 },
// { item: itemList.dead_ley_line_leaves, amount: 6 },
// { item: itemList.silver_raven_insignia, amount: 6 },
// ],
// mora: 20000,
// },
// {
// items: [
// { item: itemList.boreal_wolfs_broken_fang, amount: 3 },
// { item: itemList.dead_ley_line_leaves, amount: 12 },
// { item: itemList.silver_raven_insignia, amount: 9 },
// ],
// mora: 30000,
// },
// {
// items: [
// { item: itemList.boreal_wolfs_broken_fang, amount: 6 },
// { item: itemList.ley_line_sprouts, amount: 9 },
// { item: itemList.golden_raven_insignia, amount: 6 },
// ],
// mora: 35000,
// },
// {
// items: [
// { item: itemList.boreal_wolfs_nostalgia, amount: 4 },
// { item: itemList.ley_line_sprouts, amount: 18 },
// { item: itemList.golden_raven_insignia, amount: 12 },
// ],
// mora: 45000,
// },
// ],
// },
alley_hunter: {
name: 'Alley Hunter',
id: 'alley_hunter',
rarity: 4,
atk: 41,
secondary: 'CRIT Rate',
type: weapons.bow,
source: 'wish',
ascension: [
{
items: [
{ item: itemList.boreal_wolfs_milk_tooth, amount: 3 },
{ item: itemList.dead_ley_line_branch, amount: 3 },
{ item: itemList.treasure_hoarder_insignia, amount: 2 },
],
mora: 5000,
},
{
items: [
{ item: itemList.boreal_wolfs_cracked_tooth, amount: 3 },
{ item: itemList.dead_ley_line_branch, amount: 12 },
{ item: itemList.treasure_hoarder_insignia, amount: 8 },
],
mora: 15000,
},
{
items: [
{ item: itemList.boreal_wolfs_cracked_tooth, amount: 6 },
{ item: itemList.dead_ley_line_leaves, amount: 6 },
{ item: itemList.silver_raven_insignia, amount: 6 },
],
mora: 20000,
},
{
items: [
{ item: itemList.boreal_wolfs_broken_fang, amount: 3 },
{ item: itemList.dead_ley_line_leaves, amount: 12 },
{ item: itemList.silver_raven_insignia, amount: 9 },
],
mora: 30000,
},
{
items: [
{ item: itemList.boreal_wolfs_broken_fang, amount: 6 },
{ item: itemList.ley_line_sprouts, amount: 9 },
{ item: itemList.golden_raven_insignia, amount: 6 },
],
mora: 35000,
},
{
items: [
{ item: itemList.boreal_wolfs_nostalgia, amount: 4 },
{ item: itemList.ley_line_sprouts, amount: 18 },
{ item: itemList.golden_raven_insignia, amount: 12 },
],
mora: 45000,
},
],
},
lithic_spear: {
name: 'Lithic Spear',
id: 'lithic_spear',
@ -2421,65 +2421,65 @@ export const weaponList = {
},
],
},
// wine_and_song: {
// name: 'Wine and Song',
// id: 'wine_and_song',
// rarity: 4,
// atk: 42,
// secondary: 'Energy Recharge',
// type: weapons.catalyst,
// source: 'currently unobtainable',
// ascension: [
// {
// items: [
// { item: itemList.boreal_wolfs_milk_tooth, amount: 3 },
// { item: itemList.dead_ley_line_branch, amount: 3 },
// { item: itemList.divining_scroll, amount: 2 },
// ],
// mora: 5000,
// },
// {
// items: [
// { item: itemList.boreal_wolfs_cracked_tooth, amount: 3 },
// { item: itemList.dead_ley_line_branch, amount: 12 },
// { item: itemList.divining_scroll, amount: 8 },
// ],
// mora: 15000,
// },
// {
// items: [
// { item: itemList.boreal_wolfs_cracked_tooth, amount: 6 },
// { item: itemList.dead_ley_line_leaves, amount: 6 },
// { item: itemList.sealed_scroll, amount: 6 },
// ],
// mora: 20000,
// },
// {
// items: [
// { item: itemList.boreal_wolfs_broken_fang, amount: 3 },
// { item: itemList.dead_ley_line_leaves, amount: 12 },
// { item: itemList.sealed_scroll, amount: 9 },
// ],
// mora: 30000,
// },
// {
// items: [
// { item: itemList.boreal_wolfs_broken_fang, amount: 6 },
// { item: itemList.ley_line_sprouts, amount: 9 },
// { item: itemList.forbidden_curse_scroll, amount: 6 },
// ],
// mora: 35000,
// },
// {
// items: [
// { item: itemList.boreal_wolfs_nostalgia, amount: 4 },
// { item: itemList.ley_line_sprouts, amount: 18 },
// { item: itemList.forbidden_curse_scroll, amount: 12 },
// ],
// mora: 45000,
// },
// ],
// },
wine_and_song: {
name: 'Wine and Song',
id: 'wine_and_song',
rarity: 4,
atk: 42,
secondary: 'Energy Recharge',
type: weapons.catalyst,
source: 'currently unobtainable',
ascension: [
{
items: [
{ item: itemList.boreal_wolfs_milk_tooth, amount: 3 },
{ item: itemList.dead_ley_line_branch, amount: 3 },
{ item: itemList.treasure_hoarder_insignia, amount: 2 },
],
mora: 5000,
},
{
items: [
{ item: itemList.boreal_wolfs_cracked_tooth, amount: 3 },
{ item: itemList.dead_ley_line_branch, amount: 12 },
{ item: itemList.treasure_hoarder_insignia, amount: 8 },
],
mora: 15000,
},
{
items: [
{ item: itemList.boreal_wolfs_cracked_tooth, amount: 6 },
{ item: itemList.dead_ley_line_leaves, amount: 6 },
{ item: itemList.silver_raven_insignia, amount: 6 },
],
mora: 20000,
},
{
items: [
{ item: itemList.boreal_wolfs_broken_fang, amount: 3 },
{ item: itemList.dead_ley_line_leaves, amount: 12 },
{ item: itemList.silver_raven_insignia, amount: 9 },
],
mora: 30000,
},
{
items: [
{ item: itemList.boreal_wolfs_broken_fang, amount: 6 },
{ item: itemList.ley_line_sprouts, amount: 9 },
{ item: itemList.golden_raven_insignia, amount: 6 },
],
mora: 35000,
},
{
items: [
{ item: itemList.boreal_wolfs_nostalgia, amount: 4 },
{ item: itemList.ley_line_sprouts, amount: 18 },
{ item: itemList.golden_raven_insignia, amount: 12 },
],
mora: 45000,
},
],
},
blackcliff_warbow: {
name: 'Blackcliff Warbow',
id: 'blackcliff_warbow',
@ -2664,7 +2664,7 @@ export const weaponList = {
atk: 42,
secondary: 'Energy Recharge',
type: weapons.polearm,
source: 'forgingnorthlander polearm prototype x1crystal chunk x50white iron chunk x50',
source: 'forging',
ascension: [
{
items: [
@ -2723,7 +2723,7 @@ export const weaponList = {
atk: 42,
secondary: 'DEF',
type: weapons.claymore,
source: 'forgingnorthlander claymore prototype x1crystal chunk x50white iron chunk x50',
source: 'forging',
ascension: [
{
items: [
@ -2841,7 +2841,7 @@ export const weaponList = {
atk: 41,
secondary: 'Physical DMG Bonus',
type: weapons.bow,
source: 'forgingnorthlander bow prototype x1crystal chunk x50white iron chunk x50',
source: 'forging',
ascension: [
{
items: [
@ -3313,7 +3313,7 @@ export const weaponList = {
atk: 44,
secondary: 'Elemental Mastery',
type: weapons.catalyst,
source: 'forgingnorthlander catalyst prototype x1crystal chunk x50white iron chunk x50',
source: 'forging',
ascension: [
{
items: [
@ -3903,7 +3903,7 @@ export const weaponList = {
atk: 44,
secondary: 'ATK',
type: weapons.claymore,
source: 'forgingnorthlander claymore prototype x1crystal chunk x50white iron chunk x50',
source: 'forging',
ascension: [
{
items: [
@ -6256,4 +6256,63 @@ export const weaponList = {
},
],
},
elegy_for_the_end: {
name: "Elegy for the End",
id: "elegy_for_the_end",
rarity: 5,
type: weapons.bow,
source: "wish",
atk: 46,
secondary: "Energy Recharge",
ascension: [
{
items: [
{ item: itemList.boreal_wolfs_milk_tooth, amount: 3 },
{ item: itemList.heavy_horn, amount: 3 },
{ item: itemList.recruits_insignia, amount: 2 },
],
mora: 10000,
},
{
items: [
{ item: itemList.boreal_wolfs_cracked_tooth, amount: 3 },
{ item: itemList.heavy_horn, amount: 12 },
{ item: itemList.recruits_insignia, amount: 8 },
],
mora: 20000,
},
{
items: [
{ item: itemList.boreal_wolfs_cracked_tooth, amount: 6 },
{ item: itemList.black_bronze_horn, amount: 6 },
{ item: itemList.sergeants_insignia, amount: 6 },
],
mora: 30000,
},
{
items: [
{ item: itemList.boreal_wolfs_broken_fang, amount: 3 },
{ item: itemList.black_bronze_horn, amount: 12 },
{ item: itemList.sergeants_insignia, amount: 9 },
],
mora: 45000,
},
{
items: [
{ item: itemList.boreal_wolfs_broken_fang, amount: 6 },
{ item: itemList.black_crystal_horn, amount: 9 },
{ item: itemList.lieutenants_insignia, amount: 6 },
],
mora: 55000,
},
{
items: [
{ item: itemList.boreal_wolfs_nostalgia, amount: 4 },
{ item: itemList.black_crystal_horn, amount: 18 },
{ item: itemList.lieutenants_insignia, amount: 12 },
],
mora: 65000,
},
],
},
};

View File

@ -12,7 +12,7 @@
},
"wish": {
"autoImport": "Auto Import",
"helpAndSetting": "Help & Setting",
"helpAndSetting": "Help & Settings",
"wishesWorth": "Wishes Worth",
"lifetimePulls": "Lifetime Pulls",
"guarantee": "Guaranteed at {pity}",
@ -26,11 +26,11 @@
"manualButton": "Enable Manual Input",
"import": {
"title": "Import Wish History",
"faqsButton": "FAQS - READ FIRST",
"faqsButton": "FAQ - READ FIRST",
"nonew": "No New Wishes",
"importNotice1": "Imported wishes will be appended or replaced accordingly to existing data",
"importNotice2": "If you don't have any data saved before, first wish will be counted as pity 1",
"saveData": "Save the data?",
"saveData": "Save wish data?",
"reCalculating": "Re-calculating pity...",
"processing": "Processing",
"banner": "Banner",
@ -49,26 +49,26 @@
"invalidData": "Invalid data returned from API, try again later",
"success": "Import success 😀!",
"faqs": {
"title": "Import Wish History FAQS",
"title": "Import Wish History FAQ",
"q1": "How does it work?",
"a1": "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.",
"a1": "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 MiHoYo API to fetch your wish history.",
"q2": "Is it safe? Will I get banned?",
"a2": "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 😀.",
"a2": "Paimon.moe uses 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 😀.",
"q3": "Can you hack my account then?",
"a3": [
"Paimon.moe never save anything related to your account (even your uid or nickname), so the answer is no. This project is open source on",
", I'm not planning to damage my reputation by hacking other people account."
"Paimon.moe never saves anything related to your account (even your UID or nickname), so the answer is no. This project is open source on",
"; I'm not planning to damage my reputation by hacking other people's account."
],
"q4": "Hey I checked the request and stuff, but why it request to your domain instead of MiHoYo API?",
"q4": "Hey I checked the requests and stuff, but why does it request to your domain instead of MiHoYo API?",
"a4": [
"Paimon.moe cannot request directly to MiHoYo API because of",
", so the request redirected to a simple cors proxy to make it work. You can see the code",
", so the request is redirected to a simple CORS proxy to make it work. You can see the code",
"here"
],
"q5": "Do you store my temporary key or wish history?",
"a5": "Paimon.moe never store your key, and use HTTPS to pass your url to a cors proxy to make the CORS works. All your wish history is saved on your device only (or your google drive if you turn on sync on setting). Paimon.moe does not save anything to the server (yes anything).",
"q6": "I tried the step, but I got some API error?",
"a6": "Make sure you copy all the text (just hold and press select all), maybe you left over some text that are needed for the importer to work"
"a5": "Paimon.moe will never store your keys, and uses HTTPS to pass your URL to a CORS proxy to make the CORS work. All your wish history is saved on your device only (or your Google Drive if you turned on sync). Paimon.moe does not save anything to the server (yes, anything).",
"q6": "I've done all the steps, but I got some API error?",
"a6": "Make sure you copy all the text (hold and press Select All for mobile devices); maybe you missed some text that are needed for the importer to work."
},
"guide": {
"pc": [
@ -82,11 +82,11 @@
"Open Paimon menu",
"Press Feedback",
"Wait for it to load and a feedback page should open",
"Turn off your wifi and data connection",
"Turn off your Wi-Fi and data connection",
"Press refresh on top right corner",
"The page should error and show you a text with black font",
"The page should display an error and show you some text with black font",
"Hold the text and press select all, then copy that text (don't copy only some portion of the text)",
"Turn on your wifi or data connection",
"Turn on your Wi-Fi or data connection",
"Paste the text to the textbox below",
"Paste text here... Webpage not available..."
],
@ -96,8 +96,8 @@
"Wait for it to load and a feedback page should open",
"Press In-game issue",
"Press Co-Op Mode",
"There is a link on the bottom of the reply, press that",
"A browser should open up, copy the link and paste it below",
"There is a link on the bottom of the reply; press that",
"A browser should open up. Copy the link and paste it below",
"Paste link here... https://genshin.mihoyo..."
]
}
@ -105,33 +105,53 @@
"help": {
"title": "Wish Counter Help & Settings",
"enableManual": "Enable Manual Input",
"notice": "Using the Auto Import and manual input together is not recommended still need some testing!",
"consider": "Consider using the Auto Import first, access it on button beside the button you click to open this How To",
"notice": "Using Auto Import and manual input together is not recommended as it still requires some more testing!",
"consider": "Consider using the Auto Importer first. Access it by clicking the button next to the button you clicked to open this How To.",
"howto": {
"title": "How to use manual input",
"subtitle": "After you do x1 pull Wish:",
"subtitle": "After a 1x Wish:",
"press": "Press",
"whenYouGet": "when you get",
"p1": "It will automatically add the lifetime pulls,",
"and": "and",
"p2": ["When the", "pity reach 10, it will automatically reset to 0"],
"p3": ["When the", "pity reach 90, it will automatically reset to 0"],
"p2": ["When the", "pity reaches 10, it will automatically be reset to 0"],
"p3": ["When the", "pity reaches 90, it will automatically be reset to 0"],
"p4": [
"For when you do x10 pulls Wish, press",
"but the pity counter won't be accurate, because there is no way to tell when the drop occur (maybe you got it on the 1st or even the 10th). To make the counter still accurate, you need to check it on the history table and add it 1 by 1 like you do 1 pull Wish."
"After a 10x Wish, press",
"but keep in mind that the pity counter might not be accurate, because there is no way to tell when the drop occured (maybe you got it on the 1st or even the 10th pull). To ensure that the counter is still accurate, you need to check the history table and add it one-by-one like you do 1x Wishes."
],
"p5": ["You can also press", "button to edit the values manually!"],
"p5": ["You can also press the", "button to edit the values manually!"],
"p6": [
"Press the arrow on the bottom to see your pulls detail. A popup will show up when you get",
"Press the arrow on the bottom to see your pulls' details. A popup will show up when you get a",
"or",
"Or you can add or edit the table manually."
"You can also add or edit the table manually."
]
}
},
"types": {
"beginners": "Beginners' Wish",
"standard": "Standard",
"character-event": "Character Event",
"weapon-event": "Weapon Event"
},
"detail": {
"weapon": "Weapon",
"character": "Character",
"time": "Time",
"pity": "Pity",
"name": "Name",
"type": "Type",
"banner": "Banner",
"roll": "#Roll",
"totalThisBanner": "Total pull on this banner",
"worth": "Worth",
"loading": "Loading... (If this stuck, change your server on settings page)"
}
},
"calculator": {
"titleWeapon": "Weapon Calculator",
"titleCharacter": "Character Calculator",
"titleResin": "Resin Calculator",
"goto": "Go To",
"howToUse": "How to Use",
"guide": {
@ -143,12 +163,12 @@
"calculateAscension": "Calculate Ascension Material?",
"selectRarity": "Select weapon rarity",
"selectWeapon": "Select weapon",
"current": "Current Weapon Level, Exp, & Ascension",
"current": "Current Weapon Level, EXP, & Ascension",
"inputCurrentLevel": "Input current weapon level...",
"inputCurrentExp": "Input current weapon exp...",
"inputCurrentExp": "Input current weapon EXP...",
"intended": "Intended Weapon Level & Ascension",
"inputIntendedLevel": "Input intended weapon level...",
"resource": "Resource to Use",
"resource": "Resources to Use",
"calculate": "Calculate",
"unknownInformation": "There are some unknown information",
"ascensionLevel": "Ascension level",
@ -160,9 +180,9 @@
"character": {
"calculateAscension": "Calculate Ascension Material?",
"selectCharacter": "Select character",
"current": "Current Character Level, Exp, & Ascension",
"current": "Current Character Level, EXP, & Ascension",
"inputCurrentLevel": "Input current character level...",
"inputCurrentExp": "Input current character exp...",
"inputCurrentExp": "Input current character EXP...",
"intended": "Intended Character Level & Ascension",
"inputIntendedLevel": "Input intended character level...",
"resource": "Resource to Use",
@ -182,8 +202,15 @@
"expTable": {
"level": "Level",
"items": "Items",
"wasted": "Wasted Exp",
"wasted": "Wasted EXP",
"mora": "Mora Cost"
},
"resin": {
"inputCurrentResin": "Input Current Resin...",
"timeFormat": "en",
"calculate": "Calculate",
"currentTime": "Current Time",
"fullTime": "Resin Will Be Replenished At"
}
},
"items": {
@ -216,7 +243,7 @@
"summary": "Summary",
"empty": ["Nothing to do yet 😀", "Add some from the Items page or the Calculator!"],
"farmableToday": "Farmable Today",
"resin": "resin needed",
"resin": "Resin needed",
"based": "Based on AR:{ar} and WL:{wl}",
"change": "(change on settings)",
"approximation": "Approximation calculated from drop rates by",
@ -236,15 +263,15 @@
},
"settings": {
"version": "Data Version:",
"multiple": "Have multiple account? Choose account here to separate your wish and todo data",
"multiple": "Have multiple accounts? Choose accounts here to separate your Wish and Todo data",
"selectAccount": "Select your account",
"reset": "Reset",
"delete": "Delete",
"add": "Add",
"server": "Select your server:",
"drive": [
"Paimon.moe use Application Data Directory on your Google Drive to save and sync your wish counter and todo list.",
"Paimon.moe can only read and write file that this site create."
"Paimon.moe uses the Application Data Directory on your Google Drive to save and sync your Wish counter and Todo list.",
"Paimon.moe can only read and write files that this site created."
],
"driveError": "Google Drive API cannot be loaded",
"driveSignIn": "Sign in to Google Drive",
@ -253,11 +280,11 @@
"waiting": "Waiting...",
"syncing": "Syncing...",
"lastSync": "Last Sync:",
"feedback": "If you found any bug, wrong data, or you have any feedback, please leave a message on",
"feedback": "If you found any bugs, wrong data, or any other feedback, please leave a message on",
"or": "or",
"thanks": "Thanks😁!",
"modal": {
"notice": "All todo and wish history data will be deleted",
"notice": "All Todo and Wish history data will be deleted",
"cancel": "Cancel",
"delete": "Delete",
"reset": "Reset"

View File

@ -127,11 +127,31 @@
"Atau kamu bisa menambahkan atau mengedit tabel nya secara manual."
]
}
},
"types": {
"beginners": "Beginners' Wish",
"standard": "Standard",
"character-event": "Event Karakter",
"weapon-event": "Event Senjata"
},
"detail": {
"weapon": "Senjata",
"character": "Karakter",
"time": "Waktu",
"pity": "Pity",
"name": "Nama",
"type": "Tipe",
"banner": "Banner",
"roll": "#Roll",
"totalThisBanner": "Total pull di banner ini",
"worth": "Setara dengan",
"loading": "Loading... (Kalau tidak selesai-selesai, ganti server di halaman settings)"
}
},
"calculator": {
"titleWeapon": "Kalulator Senjata",
"titleWeapon": "Kalkulator Senjata",
"titleCharacter": "Kalkulator Karakter",
"titleResin": "Kalkulator Resin",
"goto": "Ke",
"howToUse": "Cara Penggunaan",
"guide": {
@ -184,6 +204,13 @@
"items": "Items",
"wasted": "Exp Terbuang",
"mora": "Jumlah Mora"
},
"resin": {
"inputCurrentResin": "Masukkan Jumlah Resin Sekarang...",
"timeFormat": "id",
"calculate": "Hitung",
"currentTime": "Waktu Sekarang",
"fullTime": "Resin Akan Penuh Pada"
}
},
"items": {

View File

@ -17,7 +17,7 @@
import SettingData from '../components/SettingData.svelte';
import Toast from '../components/Toast.svelte';
import Icon from '../components/Icon.svelte';
import { mdiDiscord } from '@mdi/js';
import { mdiDiscord, mdiFacebook, mdiReddit, mdiTelegram } from '@mdi/js';
export let segment;
@ -61,9 +61,31 @@
Paimon.moe is not affiliated with miHoYo.<br />
Genshin Impact, game content and materials are trademarks and copyrights of miHoYo.
</p>
<a class="text-gray-400 hover:text-primary mt-4" href="https://discord.gg/tPURAYgHV9" target="__blank">
<Icon path={mdiDiscord} size={1.5} /> Join Our Discord
</a>
<div class="flex mt-4 md:items-center flex-col md:flex-row">
<a class="text-gray-400 hover:text-primary" href="https://discord.gg/tPURAYgHV9" target="__blank">
<Icon path={mdiDiscord} size={1.5} /> Join Our Discord
</a>
<div class="text-gray-400 mt-4 md:mt-0 md:ml-4 flex flex-col md:pl-4 md:border-l border-gray-600">
<span class="text-gray-500">Community Links</span>
<a class="text-gray-400 hover:text-primary" href="https://t.me/GenshinImpact_ID" target="__blank">
<Icon path={mdiTelegram} size={1} /> Telegram
</a>
</div>
<div class="text-gray-400 mt-4 md:mt-0 md:ml-4 flex flex-col md:pl-4 md:border-l border-gray-600">
<span class="text-gray-500">Official Links</span>
<div>
<a class="text-gray-400 hover:text-primary mr-1 whitespace-no-wrap" href="https://discord.gg/2UqwpAr" target="__blank">
<Icon path={mdiDiscord} size={1} /> Discord
</a>
<a class="text-gray-400 hover:text-primary mr-1 whitespace-no-wrap" href="https://www.facebook.com/Genshinimpact/" target="__blank">
<Icon path={mdiFacebook} size={1} /> Facebook
</a>
<a class="text-gray-400 hover:text-primary whitespace-no-wrap" href="https://www.reddit.com/r/Genshin_Impact/" target="__blank">
<Icon path={mdiReddit} size={1} /> Reddit
</a>
</div>
</div>
</div>
</div>
<style>

View File

@ -0,0 +1,102 @@
<script>
import { mdiClose } from '@mdi/js';
import { t } from 'svelte-i18n';
import { fade } from 'svelte/transition';
import Button from '../../components/Button.svelte';
import Icon from '../../components/Icon.svelte';
import Input from '../../components/Input.svelte';
import { time } from '../../stores/time';
let changed = false;
let currentResin = '';
let maxResin = 160;
let millisecondsToWait;
let fullTime = null;
let missingResin = 160;
let originalResin = {
id: 'original_resin',
image: '/images/resin.png',
label: 'Original Resin',
value: 8,
};
// 8 menit per resin * 60 seconds * 1000 millisec
let minutePerResin = originalResin.value * 60 * 1000;
let dateTimeOptions = {
hour: 'numeric',
minute: 'numeric',
second: 'numeric',
weekday: 'long',
};
$: canCalculate = currentResin !== '' && currentResin >= 0 && currentResin < 160;
function calculate() {
missingResin = maxResin - currentResin;
millisecondsToWait = missingResin * minutePerResin;
fullTime = new Date($time.getTime() + millisecondsToWait);
}
function onChange() {
changed = true;
}
</script>
<div class="bg-item rounded-xl p-4">
<div class="grid grid-cols-1 md:grid-cols-1 lg:grid-cols-1 xl:grid-cols-3 gap-4">
<!-- input -->
<div class="md:col-span-1 xl:col-span-1">
<Input
className="mb-2"
on:change={onChange}
type="number"
min={0}
max={160}
bind:value={currentResin}
placeholder={$t('calculator.resin.inputCurrentResin')}
/>
<p class="text-white text-center mt-3 mb-2">
{$t('calculator.resin.currentTime')}: {new Intl.DateTimeFormat(
$t('calculator.resin.timeFormat'),
dateTimeOptions,
).format($time)}
</p>
</div>
<div class="md:col-span-2 xl:col-span-2">
<Button disabled={!canCalculate} className="block w-full md:w-auto" on:click={calculate}
>{$t('calculator.resin.calculate')}</Button
>
{#if fullTime}
<div transition:fade={{ duration: 100 }} class="bg-background rounded-xl p-4 mt-2 block xl:inline-block">
<tr>
<td class="text-right border-b border-gray-700 py-1">
<span class="text-white mr-2 whitespace-no-wrap"
>{missingResin}
<Icon size={0.5} path={mdiClose} /></span
>
</td>
<td class="border-b border-gray-700 py-1">
<span class="text-white">
<span class="w-6 inline-block">
<img class="h-6 inline-block mr-1" src={originalResin.image} alt={originalResin.label} />
</span>
{originalResin.label}
</span>
</td>
</tr>
<tr>
<td />
<td class="text-red-400 py-1">
{$t('calculator.resin.fullTime')}:
<span class="bg-red-400 rounded-lg mt-2 font-bold text-sm text-white p-1">
{new Intl.DateTimeFormat($t('calculator.resin.timeFormat'), dateTimeOptions).format(fullTime)}
</span></td
>
</tr>
</div>
{/if}
</div>
</div>
</div>

View File

@ -7,6 +7,7 @@
import WeaponCalculator from './_weapon.svelte';
import CharacterCalculator from './_character.svelte';
import LevelUpTable from './_characterTable.svelte';
import ResinCalculator from './_resin.svelte';
import Button from '../../components/Button.svelte';
import Icon from '../../components/Icon.svelte';
import HowToModal from '../../components/CalculatorHowToModal.svelte';
@ -15,6 +16,7 @@
let weaponCalc;
let characterCalc;
let resinCalc;
function openHowTo() {
openModal(
@ -27,16 +29,22 @@
);
}
export function scroll(type) {
const elementPosition =
type === 'character' ? characterCalc.getBoundingClientRect().top : weaponCalc.getBoundingClientRect().top;
const headerOffset = 80;
const offsetPosition = elementPosition - headerOffset;
export function findPos(id) {
let node = document.getElementById(id);
let curtop = 0;
let curtopscroll = 0;
let headerOffset = 40;
if (node.offsetParent) {
do {
curtop += node.offsetTop;
curtopscroll += node.offsetParent ? node.offsetParent.scrollTop : 0;
} while ((node = node.offsetParent));
window.scrollTo({
top: offsetPosition,
behavior: 'smooth',
});
window.scrollTo({
top: curtop - curtopscroll - headerOffset,
behavior: 'smooth',
});
}
}
</script>
@ -59,14 +67,15 @@
</Button>
</div>
<div
id="weapon"
class="flex flex-col items-center md:flex-row-reverse md:justify-end md:items-start lg:items-center mb-2"
bind:this={weaponCalc}
>
<Button on:click={() => scroll('character')}>
<Button on:click={() => findPos('character')}>
<Icon size={0.8} path={mdiArrowDown} />
{$t('calculator.goto')}
{$t('calculator.titleCharacter')}
</Button>
<h1
class="font-display font-black text-center mt-2 md:mt-0 md:mr-2 xl:mr-8 text-3xl lg:text-left lg:text-5xl text-white"
>
@ -75,14 +84,19 @@
</div>
<WeaponCalculator />
<div
id="character"
class="flex flex-col items-center md:flex-row-reverse md:justify-end md:items-start lg:items-center mt-8 mb-2"
bind:this={characterCalc}
>
<Button on:click={() => scroll('weapon')}>
<Button on:click={() => findPos('weapon')}>
<Icon size={0.8} path={mdiArrowUp} />
{$t('calculator.goto')}
{$t('calculator.titleWeapon')}
</Button>
<Button className="md:mb-0 md:ml-4 mb-4" on:click={() => findPos('resin')}>
<Icon size={0.8} path={mdiArrowDown} />
{$t('calculator.goto')}
{$t('calculator.titleResin')}
</Button>
<h1
class="font-display font-black text-center mt-2 md:mt-0 md:mr-2 xl:mr-8 text-3xl lg:text-left lg:text-5xl text-white"
>
@ -90,6 +104,22 @@
</h1>
</div>
<CharacterCalculator />
<div
id="resin"
class="flex flex-col items-center md:flex-row-reverse md:justify-end md:items-start lg:items-center mt-8 mb-2"
>
<Button on:click={() => findPos('character')}>
<Icon size={0.8} path={mdiArrowUp} />
{$t('calculator.goto')}
{$t('calculator.titleCharacter')}
</Button>
<h1
class="font-display font-black text-center mt-2 md:mt-0 md:mr-2 xl:mr-8 text-3xl lg:text-left lg:text-5xl text-white"
>
{$t('calculator.titleResin')}
</h1>
</div>
<ResinCalculator />
<div class="mt-8" />
<LevelUpTable />
</div>

View File

@ -241,7 +241,7 @@
<div class="lg:ml-64 pt-20 px-4 md:px-8 lg:pt-8">
<div class="bg-item rounded-xl mb-4 p-4">
<p class="text-white">{$t('settings.version')} <b>1.3</b></p>
<p class="text-white">{$t('settings.version')} <b>1.4</b></p>
</div>
<div class="bg-item rounded-xl mb-4 p-4 flex flex-col">
<p class="text-white">{$t('settings.multiple')}</p>
@ -343,7 +343,10 @@
</p>
{#if changelogOpen}
<div transition:slide class="mt-4">
<pre class="bg-background rounded-xl py-2 px-4 whitespace-pre-wrap">2021/03/13
<pre class="bg-background rounded-xl py-2 px-4 whitespace-pre-wrap">2021/03/17
- Fix wish import
- Add new 1.4 weapons</pre>
<pre class="bg-background rounded-xl py-2 px-4 whitespace-pre-wrap mt-2">2021/03/13
- Add timeline page</pre>
<pre
class="bg-background rounded-xl py-2 px-4 whitespace-pre-wrap mt-2">2021/03/09

669
src/routes/wish/[id].svelte Normal file
View File

@ -0,0 +1,669 @@
<script context="module">
export async function preload(page) {
const { id } = page.params;
return { id };
}
</script>
<script>
import { t } from 'svelte-i18n';
import { getContext, onMount, tick } from 'svelte';
import { mdiArrowLeft, mdiStar } from '@mdi/js';
import dayjs from 'dayjs';
import Chart from 'chart.js';
import { banners } from '../../data/banners';
import Icon from '../../components/Icon.svelte';
import TableHeader from '../../components/Table/TableHeader.svelte';
import WishDetailModal from './_detail.svelte';
import { characters } from '../../data/characters';
import { weaponList } from '../../data/weaponList';
import { getAccountPrefix } from '../../stores/account';
import { fromRemote, readSave } from '../../stores/saveManager';
import { getTimeOffset } from '../../stores/server';
Chart.defaults.global.defaultFontColor = '#cbd5e0';
Chart.defaults.global.defaultFontFamily = 'Poppins';
let numberFormat = Intl.NumberFormat();
const { open: openModal } = getContext('simple-modal');
export let id;
const bannerTypes = {
'character-event': 'characters',
'weapon-event': 'weapons',
standard: 'standard',
beginners: 'beginners',
};
const bannerType = bannerTypes[id];
let bannerChart;
let pieChart;
let loading = true;
let pullData = [];
let pulls = [];
let sorted = [];
let total = 0;
let legendary = 0;
let rare = 0;
let allLegendary = [];
let allRare = [];
let sortBy = 'time';
let sortOrder;
let currentBannerIndex = -1;
let selectedBanners;
selectedBanners = banners[bannerType].map((e) => {
// banner data based on Asia time
const diff = 8 - getTimeOffset();
const start = dayjs(e.start, 'YYYY-MM-DD HH:mm:ss').subtract(diff, 'hour');
const end = dayjs(e.end, 'YYYY-MM-DD HH:mm:ss').subtract(diff, 'hour');
const image = `/images/banners/${e.name} ${e.image}.png`;
return {
...e,
start: start.unix(),
end: end.unix(),
image,
total: 0,
legendary: [],
rare: {
character: [],
weapon: [],
},
};
});
function openDetail(banner) {
openModal(
WishDetailModal,
{
banner,
},
{
closeButton: false,
styleWindow: { background: '#25294A', width: '600px' },
},
);
}
function readLocalData() {
console.log('wish read local');
const prefix = getAccountPrefix();
const data = readSave(`${prefix}${path}`);
if (data !== null) {
const counterData = JSON.parse(data);
total = counterData.total;
legendary = counterData.legendary;
rare = counterData.rare;
pullData = counterData.pulls || [];
}
processPullData();
}
function getNextBanner(time) {
for (let i = currentBannerIndex + 1; i < selectedBanners.length; i++) {
console.log('change banner', i, dayjs.unix(time).format(), dayjs.unix(selectedBanners[i].start).format());
if (time >= selectedBanners[i].start && time < selectedBanners[i].end) {
currentBannerIndex = i;
return selectedBanners[i];
}
}
}
async function processPullData() {
const currentPulls = [];
console.log(selectedBanners);
let currentBanner = null;
let grouped = false;
let striped = false;
let startBanner = false;
for (let i = 0; i < pullData.length; i++) {
const pull = pullData[i];
const next = pullData[i + 1] || { time: dayjs().year(2000).unix() };
if (currentBanner === null || currentBanner.end < pull.time) {
currentBanner = getNextBanner(pull.time);
startBanner = true;
if (i > 0) {
currentPulls[i - 1].end = true;
}
}
const item =
pull.type === 'character'
? characters[pull.id]
: pull.type === 'weapon'
? weaponList[pull.id]
: { name: 'Unknown', rarity: 3 };
selectedBanners[currentBannerIndex].total++;
const newPull = {
...pull,
formattedTime: formatTime(pull.time),
name: item.name,
rarity: item.rarity,
banner: currentBanner,
start: startBanner,
at: selectedBanners[currentBannerIndex].total,
};
if (item.rarity === 5) {
selectedBanners[currentBannerIndex].legendary.push(newPull);
allLegendary.push(newPull);
} else if (item.rarity === 4) {
allRare.push(newPull);
if (pull.type === 'character') {
selectedBanners[currentBannerIndex].rare.character.push(newPull);
} else if (pull.type === 'weapon') {
selectedBanners[currentBannerIndex].rare.weapon.push(newPull);
}
}
if (!grouped && pull.time === next.time) {
striped = !striped;
newPull.group = 'start';
grouped = true;
} else if (grouped && pull.time !== next.time) {
newPull.group = 'end';
grouped = false;
} else if (grouped) {
newPull.group = 'group';
} else {
striped = !striped;
}
if (i === pullData.length - 1) {
newPull.end = true;
}
newPull.striped = striped;
startBanner = false;
currentPulls.push(newPull);
}
console.log(currentPulls.slice());
pulls = currentPulls;
sorted = pulls.reverse();
let labels = [];
let totalEachBanner = [];
let totalLegendaryEachBanner = [];
let totalRareEachBanner = [];
let backgrounds = [];
let borders = [];
for (let e of selectedBanners) {
const curLegendary = e.legendary.length;
const curRare = e.rare.character.length + e.rare.weapon.length;
const curLeft = e.total - curLegendary - curRare;
labels.push(e.shortName);
totalEachBanner.push(curLeft);
totalLegendaryEachBanner.push(curLegendary);
totalRareEachBanner.push(curRare);
borders.push(e.color);
const rgb = /^#?([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i.exec(e.color || '#ffffff');
backgrounds.push(`rgba(${parseInt(rgb[1], 16)}, ${parseInt(rgb[2], 16)}, ${parseInt(rgb[3], 16)}, 0.7)`);
}
console.log(totalEachBanner, totalLegendaryEachBanner, totalRareEachBanner);
loading = false;
await tick();
new Chart(pieChart, {
type: 'pie',
data: {
labels: ['Total Pulls', 'Total 5*', 'Total 4*'],
datasets: [
{
label: 'total',
data: [currentPulls.length, allLegendary.length, allRare.length],
backgroundColor: ['rgba(107, 161, 192, 0.7)', 'rgba(255, 177, 63, 0.7)', 'rgba(210, 143, 214, 0.7)'],
borderColor: ['#6BA1C0', '#FFB13F', '#D28FD6'],
borderWidth: 1,
},
],
},
options: {
responsive: true,
legend: {
display: false,
},
tooltips: {
mode: 'dataset',
},
},
});
new Chart(bannerChart, {
type: 'bar',
data: {
labels,
datasets: [
{
label: '5* pulls',
data: totalLegendaryEachBanner,
backgroundColor: 'rgba(255, 177, 63, 0.7)',
borderColor: '#FFB13F',
borderWidth: 1,
},
{
label: '4* pulls',
data: totalRareEachBanner,
backgroundColor: 'rgba(210, 143, 214, 0.7)',
borderColor: '#D28FD6',
borderWidth: 1,
},
{
label: 'Total pulls',
data: totalEachBanner,
backgroundColor: backgrounds,
borderColor: borders,
borderWidth: 1,
},
],
},
options: {
responsive: true,
maintainAspectRatio: false,
legend: {
display: false,
},
tooltips: {
mode: 'index',
intersect: false,
callbacks: {
title: (tooltipItem) => {
return selectedBanners[tooltipItem[0].index].name;
},
label: (tooltipItem, data) => {
console.log(tooltipItem, data);
const label = data.datasets[tooltipItem.datasetIndex].label;
const value =
tooltipItem.datasetIndex === 2
? data.datasets[0].data[tooltipItem.index] +
data.datasets[1].data[tooltipItem.index] +
data.datasets[2].data[tooltipItem.index]
: tooltipItem.value;
return `${label}: ${value}`;
},
},
},
scales: {
yAxes: [{ stacked: true, gridLines: { color: '#2d3748' } }],
xAxes: [{ stacked: true }],
},
},
});
}
function sortPulls() {
if (sortBy === 'time') {
if (sortOrder) {
sorted = pulls.slice().reverse();
} else {
sorted = pulls;
}
} else if (sortBy === 'type') {
if (sortOrder) {
sorted = pulls.slice().sort((a, b) => a.type.localeCompare(b.type));
} else {
sorted = pulls.slice().sort((a, b) => b.type.localeCompare(a.type));
}
} else if (sortBy === 'rare') {
if (sortOrder) {
sorted = pulls.slice().sort((a, b) => a.rarity - b.rarity);
} else {
sorted = pulls.slice().sort((a, b) => b.rarity - a.rarity);
}
} else if (sortBy === 'pity') {
if (sortOrder) {
sorted = pulls.slice().sort((a, b) => a.pity - b.pity);
} else {
sorted = pulls.slice().sort((a, b) => b.pity - a.pity);
}
} else if (sortBy === 'name') {
if (sortOrder) {
sorted = pulls.slice().sort((a, b) => a.name.localeCompare(b.name));
} else {
sorted = pulls.slice().sort((a, b) => b.name.localeCompare(a.name));
}
}
}
onMount(() => {
readLocalData();
});
function sort(by) {
if (sortBy === by) {
sortOrder = !sortOrder;
} else {
sortBy = by;
sortOrder = false;
}
sortPulls();
}
function formatTime(time) {
return dayjs.unix(time).format('ddd YYYY-MM-DD HH:mm:ss');
}
function calculateLegendaryColor(percentage) {
const hue = percentage * 120;
return `color: hsl(${hue}, 100%, 60%);`;
}
function calculateRareColor(percentage) {
const opacity = percentage + 0.3;
return `opacity: ${opacity};`;
}
$: path = `wish-counter-${id}`;
$: if ($fromRemote) {
readLocalData();
}
</script>
<div class="pt-20 lg:ml-64 lg:pt-8">
<div class="flex items-center text-gray-400 px-4 md:px-8">
<a href="/wish" class="pr-2">
<Icon path={mdiArrowLeft} size={1.2} />
</a>
<h2 class="font-display font-bold text-2xl text-gray-400 flex-1">
Wish Counter
<span class="text-white">{$t(`wish.types.${id}`)}</span>
</h2>
</div>
{#if loading}
<div class="text-white pl-4 md:pl-8 mt-4">{$t('wish.detail.loading')}</div>
{:else}
<div class="flex mt-4 wrapper">
<div class="block overflow-x-auto xl:overflow-x-visible whitespace-no-wrap px">
<div class="pr-4 pl-4 md:pl-8 xl:pr-2 table">
<table
class="{sortBy === 'time'
? 'list-table'
: ''} w-full block pl-4 pr-4 py-2 md:pl-8 md:py-4 bg-item rounded-xl"
>
<tr>
<TableHeader
className="sticky top-0 bg-item z-30"
styleList="top: -1px; background-clip: padding-box;"
on:click={() => sort('time')}
sort={sortBy === 'time'}
order={sortOrder}
align="left"
>
{$t('wish.detail.time')}
<div class="absolute h-full w-8 bg-item" style="left: -2rem; top: -1px;" />
</TableHeader>
<TableHeader
className="sticky top-0 bg-item z-30"
styleList="top: -1px; background-clip: padding-box;"
on:click={() => sort('pity')}
sort={sortBy === 'pity'}
order={sortOrder}
align="center"
padding="px-2"
>
{$t('wish.detail.pity')}
</TableHeader>
<TableHeader
className="sticky top-0 bg-item z-30"
styleList="top: -1px; background-clip: padding-box;"
on:click={() => sort('name')}
sort={sortBy === 'name'}
order={sortOrder}
align="left"
>
{$t('wish.detail.name')}
</TableHeader>
<TableHeader
className="sticky top-0 bg-item z-30"
styleList="top: -1px; background-clip: padding-box;"
on:click={() => sort('rare')}
sort={sortBy === 'rare'}
order={sortOrder}
align="center"
padding="px-2"
>
<Icon path={mdiStar} className="pb-1" />
</TableHeader>
<TableHeader
className="sticky top-0 bg-item z-30"
styleList="top: -1px; background-clip: padding-box;"
on:click={() => sort('type')}
sort={sortBy === 'type'}
order={sortOrder}
align="left"
>
{$t('wish.detail.type')}
</TableHeader>
<TableHeader
className="sticky top-0 bg-item"
styleList="top: -1px; background-clip: padding-box;"
align="center"
>
{$t('wish.detail.roll')}
</TableHeader>
<TableHeader
className="sticky top-0 bg-item z-30 {sortBy === 'time' ? 'xl:hidden' : ''}"
styleList="top: -1px; background-clip: padding-box;"
align="left"
>
{$t('wish.detail.banner')}
</TableHeader>
</tr>
{#each sorted as pull}
<tr class="rarity-{pull.rarity}{pull.striped && sortBy === 'time' ? ' striped' : ''}">
<td
class="border-t border-gray-700 px-4 text-gray-200 whitespace-no-wrap relative"
style="font-family: monospace;"
>
{pull.formattedTime}
{#if sortBy === 'time' && (sortOrder ? pull.group === 'start' : pull.group === 'end')}
<div class="group-bar"><span>x10</span></div>
{/if}
</td>
<td class="border-t border-gray-700 px-2 text-gray-200 text-center">
<span
style={pull.rarity === 5
? calculateLegendaryColor((90 - pull.pity) / 90)
: calculateRareColor((10 - pull.pity) / 10)}
>
{pull.pity}
</span>
</td>
<td class="border-t border-gray-700 pl-4 text-gray-200">
<img
class="w-8 h-8 inline mr-2"
src={pull.type === 'character'
? `/images/characters/${pull.id}.png`
: pull.type === 'weapon'
? `/images/weapons/${pull.id}.png`
: '/images/wish.png'}
alt={pull.name}
/>
<span class="h-8 leading-8 pr-4">{pull.name}</span>
</td>
<td class="border-t border-gray-700 px-2 text-gray-200 text-center">{pull.rarity}</td>
<td class="border-t border-gray-700 px-4 text-gray-200">
{$t(`wish.detail.${pull.type}`)}
</td>
<td class="border-t border-gray-700 px-2 text-gray-200 text-center">
{pull.at}
</td>
{#if sortBy === 'time' && ((pull.end && !sortOrder) || (pull.start && sortOrder))}
<td class="relative hidden xl:table-cell">
<div class="border-t border-gray-700 absolute left-0 top-0 z-10 border-start" style="width: 266px;" />
</td>
<td class="sticky w-0 hidden xl:table-cell pl-2" style="top: 8px;">
<div
class="w-64 absolute top-0 pt-2 bg-item cursor-pointer"
on:click={() => openDetail(pull.banner)}
>
<img class="w-full rounded-lg" src={pull.banner.image} alt={pull.banner.name} />
<p class="bg-gray-900 rounded-lg mt-2 text-center text-gray-200">
{pull.banner.total} Pulls
<img class="h-4 inline ml-2" src="/images/primogem.png" alt="primogem" />
{numberFormat.format(pull.banner.total * 160)}
</p>
</div>
</td>
{/if}
<td
class="border-t border-gray-700 px-4 text-gray-200 top-0 text-center {sortBy === 'time'
? 'xl:hidden'
: ''}"
>
<img
on:click={() => openDetail(pull.banner)}
class="h-8 inline cursor-pointer"
src={pull.banner.image}
alt={pull.banner.name}
/>
</td>
</tr>
{/each}
</table>
</div>
</div>
<div class="chart-area flex flex-wrap">
<div class="flex">
<div
class="bg-background px-4 py-2 rounded-xl flex flex-col items-center justify-center mr-4"
style="height: 200px;"
>
<p class="text-gray-400 font-body">Total</p>
<p class="text-gray-400 font-body text-xl font-bold">{total}</p>
<p class="text-gray-400 font-body mt-2 flex items-center">5<Icon size={0.7} path={mdiStar} /> Pity</p>
<p class="text-legendary-from font-body text-xl font-bold">{legendary}</p>
<p class="text-gray-400 font-body mt-2 flex items-center">4<Icon size={0.7} path={mdiStar} /> Pity</p>
<p class="text-rare-from font-body text-xl font-bold">{rare}</p>
</div>
<div class="bg-background rounded-xl inline-block mb-4 p-2 pie-chart mr-4">
<canvas width="200" height="200" bind:this={pieChart} />
</div>
</div>
{#if id === 'character-event' || id === 'weapon-event'}
<div class="bg-background rounded-xl inline-block mb-4 p-2 banner-chart">
<canvas width="500" height="200" bind:this={bannerChart} />
</div>
{/if}
</div>
</div>
{/if}
</div>
<style>
.wrapper {
@apply flex-col-reverse;
.chart-area {
@apply px-4;
@screen md {
@apply px-8;
}
}
@media (min-width: 1920px) {
@apply flex-row;
.chart-area {
@apply px-2;
@apply flex-col;
}
}
}
.banner-chart {
max-width: 500px;
height: 200px;
width: 100%;
}
.pie-chart {
max-width: 200px;
height: 200px;
width: 100%;
}
tr.striped {
background: linear-gradient(90deg, rgba(0, 0, 0, 0) 0%, rgba(0, 0, 0, 0.4) 20%, rgba(0, 0, 0, 0) 100%);
}
tr.rarity-5 {
background: linear-gradient(90deg, rgba(0, 0, 0, 0) 0%, rgba(185, 129, 46, 0.55) 20%, rgba(0, 0, 0, 0) 100%);
}
tr.rarity-4 {
background: linear-gradient(90deg, rgba(0, 0, 0, 0) 0%, rgba(173, 118, 176, 0.55) 20%, rgba(0, 0, 0, 0) 100%);
}
.group-bar {
width: 1px;
height: 325px;
left: 0;
top: 17px;
@apply bg-white;
@apply absolute;
@apply select-none;
&::before,
&::after {
content: '';
width: 8px;
height: 1px;
left: 0;
top: 0;
@apply bg-white;
@apply absolute;
}
&::after {
top: initial;
bottom: 0;
}
span {
top: 155px;
left: -20px;
@apply absolute;
@apply transform;
@apply -rotate-90;
}
}
table.list-table {
@screen xl {
padding-right: 17rem;
}
}
/* firefox bug */
@supports (-moz-appearance:none) {
.border-start {
top: -1px;
}
}
</style>

View File

@ -3,7 +3,7 @@
import { onMount, getContext } from 'svelte';
import { slide } from 'svelte/transition';
import { mdiPencil, mdiStar, mdiChevronDown } from '@mdi/js';
import { mdiPencil, mdiStar, mdiChevronDown, mdiTableOfContents } from '@mdi/js';
import debounce from 'lodash/debounce';
const { open: openModal, close: closeModal } = getContext('simple-modal');
@ -245,13 +245,18 @@
</script>
<div class="bg-item rounded-xl p-4 inline-flex flex-col w-full" style="height: min-content;">
<div class="flex justify-between mb-2">
<h2 class="font-display font-bold text-2xl text-white">{name}</h2>
<div class="flex mb-2">
<h2 class="font-display font-bold text-2xl text-white flex-1">{name}</h2>
{#if manualInput}
<Button size="sm" on:click={toggleEdit}>
<Icon path={mdiPencil} color="white" />
</Button>
{/if}
<a href="/wish/{id}">
<Button className="ml-2" size="sm">
<Icon path={mdiTableOfContents} color="white" />
</Button>
</a>
</div>
<div class="flex flex-col w-full">
<div

View File

@ -0,0 +1,146 @@
<script>
import { t } from 'svelte-i18n';
import dayjs from 'dayjs';
import Icon from '../../components/Icon.svelte';
import { mdiStar } from '@mdi/js';
let numberFormat = Intl.NumberFormat('en', {
maximumFractionDigits: 1,
minimumFractionDigits: 0,
});
export let banner;
function calculateColor(percentage) {
const hue = percentage * 120;
return `color: hsl(${hue}, 100%, 60%);`;
}
const legendaryPity = banner.legendary.reduce((prev, next) => {
prev += next.pity;
return prev;
}, 0);
let rarePity = 0;
let rarePityCharacter = 0;
let rarePityWeapon = 0;
let rareTotal = banner.rare.character.length + banner.rare.weapon.length;
for (let item of banner.rare.character) {
rarePity += item.pity;
rarePityCharacter += item.pity;
}
for (let item of banner.rare.weapon) {
rarePity += item.pity;
rarePityWeapon += item.pity;
}
</script>
<div>
<img src={banner.image} class="w-full rounded-lg" alt={banner.name} />
<h1 class="mt-4 text-white font-display font-semibold text-xl">{banner.name}</h1>
<p class="text-gray-400 font-body flex flex-col md:flex-row">
<span class="flex">
<span>{dayjs.unix(banner.start).format('ddd, D MMM YYYY HH:mm')}</span>
<span class="mx-2">-</span>
</span>
<span>{dayjs.unix(banner.end).format('ddd, D MMM YYYY HH:mm')}</span>
</p>
<p class="text-gray-400 pr-2 mt-4">
{$t('wish.detail.totalThisBanner')}
<span class="text-gray-200 font-semibold">{banner.total}</span>
</p>
<p class="text-gray-400 pr-2">
{$t('wish.detail.worth')}
<img class="inline h-4" src="/images/primogem.png" alt="primogem" />
<span class="text-gray-200 font-semibold">{banner.total * 160}</span>
</p>
<table class="mt-4">
<tr>
<td class="text-gray-400 text-sm font-display pr-2 md:pr-4 text-left">Rarity</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(banner.legendary.length)}
</td>
<td class="text-legendary-from font-semibold pr-2 md:pr-4 text-right border-t border-gray-700">
{numberFormat.format((banner.legendary.length / banner.total) * 100)}%
</td>
<td class="text-legendary-from font-semibold text-right border-t border-gray-700">
{banner.legendary.length ? numberFormat.format(legendaryPity / banner.legendary.length) : 0}
</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(rareTotal)}
</td>
<td class="text-rare-from font-semibold pr-2 md:pr-4 text-right border-t border-gray-700">
{numberFormat.format((rareTotal / banner.total) * 100)}%
</td>
<td class="text-rare-from font-semibold text-right border-t border-gray-700">
{rareTotal > 0 ? numberFormat.format(rarePity / rareTotal) : 0}
</td>
</tr>
<tr>
<td class="text-rare-from font-semibold pl-4 md:pl-4 pr-2 md:pr-4 border-t border-gray-700 whitespace-no-wrap">
└ Character
</td>
<td class="text-rare-from font-semibold pr-2 md:pr-4 text-right border-t border-gray-700">
{numberFormat.format(banner.rare.character.length)}
</td>
<td class="text-rare-from font-semibold pr-2 md:pr-4 text-right border-t border-gray-700">
{numberFormat.format((banner.rare.character.length / banner.total) * 100)}%
</td>
<td class="text-rare-from font-semibold text-right border-t border-gray-700">
{rareTotal > 0 ? numberFormat.format(rarePityCharacter / rareTotal) : 0}
</td>
</tr>
<tr>
<td class="text-rare-from font-semibold pl-4 md:pl-4 pr-2 md:pr-4 border-t border-gray-700 whitespace-no-wrap">
└ Weapon
</td>
<td class="text-rare-from font-semibold pr-2 md:pr-4 text-right border-t border-gray-700">
{numberFormat.format(banner.rare.weapon.length)}
</td>
<td class="text-rare-from font-semibold pr-2 md:pr-4 text-right border-t border-gray-700">
{numberFormat.format((banner.rare.weapon.length / banner.total) * 100)}%
</td>
<td class="text-rare-from font-semibold text-right border-t border-gray-700">
{rareTotal > 0 ? numberFormat.format(rarePityWeapon / rareTotal) : 0}
</td>
</tr>
</table>
<div class="flex flex-wrap mt-4">
{#each banner.legendary as pull}
<span class="pity">{pull.name} <span style={calculateColor((90 - pull.pity) / 90)}>{pull.pity}</span></span>
{/each}
</div>
</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>

View File

@ -19,6 +19,10 @@ const timeOffset = {
const weekdays = ['sunday', 'monday', 'tuesday', 'wednesday', 'thursday', 'friday', 'saturday'];
export const getTimeOffset = () => {
return timeOffset[get(server)];
}
export const getTimeDifference = () => {
const now = dayjs();
const local = now.utcOffset();

11
src/stores/time.js Normal file
View File

@ -0,0 +1,11 @@
import { readable } from 'svelte/store';
export const time = readable(new Date(), function start(set) {
const interval = setInterval(() => {
set(new Date());
}, 1000);
return function stop() {
clearInterval(interval);
};
});

Binary file not shown.

After

Width:  |  Height:  |  Size: 489 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 478 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 515 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 286 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 596 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 391 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 387 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 447 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 417 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 405 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 450 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 364 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 356 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 393 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 508 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 297 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 564 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 513 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 529 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 467 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 418 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 54 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 69 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 63 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 65 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 59 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 46 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 20 KiB

After

Width:  |  Height:  |  Size: 64 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 58 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 18 KiB

After

Width:  |  Height:  |  Size: 59 KiB

View File

@ -1163,6 +1163,29 @@ chalk@^2.0.0, chalk@^2.4.1, chalk@^2.4.2:
ansi-styles "^4.1.0"
supports-color "^7.1.0"
chart.js@^2.9.4:
version "2.9.4"
resolved "https://registry.yarnpkg.com/chart.js/-/chart.js-2.9.4.tgz#0827f9563faffb2dc5c06562f8eb10337d5b9684"
integrity sha512-B07aAzxcrikjAPyV+01j7BmOpxtQETxTSlQ26BEYJ+3iUkbNKaOJ/nDbT6JjyqYxseM0ON12COHYdU2cTIjC7A==
dependencies:
chartjs-color "^2.1.0"
moment "^2.10.2"
chartjs-color-string@^0.6.0:
version "0.6.0"
resolved "https://registry.yarnpkg.com/chartjs-color-string/-/chartjs-color-string-0.6.0.tgz#1df096621c0e70720a64f4135ea171d051402f71"
integrity sha512-TIB5OKn1hPJvO7JcteW4WY/63v6KwEdt6udfnDE9iCAZgy+V4SrbSxoIbTw/xkUIapjEI4ExGtD0+6D3KyFd7A==
dependencies:
color-name "^1.0.0"
chartjs-color@^2.1.0:
version "2.4.1"
resolved "https://registry.yarnpkg.com/chartjs-color/-/chartjs-color-2.4.1.tgz#6118bba202fe1ea79dd7f7c0f9da93467296c3b0"
integrity sha512-haqOg1+Yebys/Ts/9bLo/BqUcONQOdr/hoEr2LLTRl6C5LXctUdHxsCYfvQVg5JIxITrfCNUDr4ntqmQk9+/0w==
dependencies:
chartjs-color-string "^0.6.0"
color-convert "^1.9.3"
clean-css@^4.2.1:
version "4.2.3"
resolved "https://registry.yarnpkg.com/clean-css/-/clean-css-4.2.3.tgz#507b5de7d97b48ee53d84adb0160ff6216380f78"
@ -1170,7 +1193,7 @@ clean-css@^4.2.1:
dependencies:
source-map "~0.6.0"
color-convert@^1.9.0, color-convert@^1.9.1:
color-convert@^1.9.0, color-convert@^1.9.1, color-convert@^1.9.3:
version "1.9.3"
resolved "https://registry.yarnpkg.com/color-convert/-/color-convert-1.9.3.tgz#bb71850690e1f136567de629d2d5471deda4c1e8"
integrity sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==
@ -1874,6 +1897,11 @@ minimist@^1.1.1, minimist@^1.2.5:
resolved "https://registry.yarnpkg.com/minimist/-/minimist-1.2.5.tgz#67d66014b66a6a8aaa0c083c5fd58df4e4e97602"
integrity sha512-FM9nNUYrRBAELZQT3xeZQ7fmMOBg6nWNmJKTcgsJeaLstP/UODVpGsr5OhXhhXg6f+qtJ8uiZ+PUxkDWcgIXLw==
moment@^2.10.2:
version "2.29.1"
resolved "https://registry.yarnpkg.com/moment/-/moment-2.29.1.tgz#b2be769fa31940be9eeea6469c075e35006fa3d3"
integrity sha512-kHmoybcPV8Sqy59DwNDY3Jefr64lK/by/da0ViFcuA4DH0vQg5Q6Ze5VimxkfQNSC+Mls/Kx53s7TjP1RhFEDQ==
mri@^1.1.0:
version "1.1.6"
resolved "https://registry.yarnpkg.com/mri/-/mri-1.1.6.tgz#49952e1044db21dbf90f6cd92bc9c9a777d415a6"