diff options
| -rw-r--r-- | src/lib/AniList/activity.ts | 21 | ||||
| -rw-r--r-- | src/lib/Locale/english.ts | 7 | ||||
| -rw-r--r-- | src/lib/Locale/japanese.ts | 7 | ||||
| -rw-r--r-- | src/lib/Locale/layout.ts | 7 | ||||
| -rw-r--r-- | src/routes/user/[user]/badges/+page.svelte | 153 | ||||
| -rw-r--r-- | src/styles/popup.scss | 7 |
6 files changed, 199 insertions, 3 deletions
diff --git a/src/lib/AniList/activity.ts b/src/lib/AniList/activity.ts index 0b0929d6..f6209b57 100644 --- a/src/lib/AniList/activity.ts +++ b/src/lib/AniList/activity.ts @@ -279,3 +279,24 @@ export const activityLikes = async (id: number): Promise<Partial<User>[]> => { return activityResponse['data']['Activity']['likes']; }; + +export const activityText = async (id: number): Promise<string> => { + const activityResponse = await ( + await fetch('https://graphql.anilist.co', { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + Accept: 'application/json' + }, + body: JSON.stringify({ + query: `{ + Activity(id: ${id}) { + ... on TextActivity { text(asHtml: true) } + } + }` + }) + }) + ).json(); + + return activityResponse['data']['Activity']['text']; +}; diff --git a/src/lib/Locale/english.ts b/src/lib/Locale/english.ts index 92a1f608..f562dc71 100644 --- a/src/lib/Locale/english.ts +++ b/src/lib/Locale/english.ts @@ -149,6 +149,13 @@ const English: Locale = { update: 'Update', or: 'or', delete: 'Delete (Click Twice)' + }, + importMode: { + enable: 'Enable Import Mode', + disable: 'Disable Import Mode', + cancel: 'Cancel', + import: 'Import', + fetch: 'Fetch' } }, profile: { diff --git a/src/lib/Locale/japanese.ts b/src/lib/Locale/japanese.ts index 1f92279d..a8cbb7f2 100644 --- a/src/lib/Locale/japanese.ts +++ b/src/lib/Locale/japanese.ts @@ -150,6 +150,13 @@ const Japanese: Locale = { update: 'バッジを更新', or: 'または', delete: 'バッジを削除する(2回クリック)' + }, + importMode: { + enable: 'インポートモードを有効にする', + disable: 'インポートモードを無効にする', + cancel: 'キャンセル', + import: 'インポート', + fetch: 'フェッチ' } }, profile: { diff --git a/src/lib/Locale/layout.ts b/src/lib/Locale/layout.ts index 00c19ea9..ec44391b 100644 --- a/src/lib/Locale/layout.ts +++ b/src/lib/Locale/layout.ts @@ -154,6 +154,13 @@ export interface Locale { or: LocaleValue; delete: LocaleValue; }; + importMode: { + enable: LocaleValue; + disable: LocaleValue; + cancel: LocaleValue; + import: LocaleValue; + fetch: LocaleValue; + }; }; profile: { statistics: LocaleValue; diff --git a/src/routes/user/[user]/badges/+page.svelte b/src/routes/user/[user]/badges/+page.svelte index a5ae83fe..523f9030 100644 --- a/src/routes/user/[user]/badges/+page.svelte +++ b/src/routes/user/[user]/badges/+page.svelte @@ -15,11 +15,19 @@ import Message from '$lib/Loading/Message.svelte'; import Dropdown from '$lib/Dropdown.svelte'; import AnimeRateLimited from '$lib/Error/AnimeRateLimited.svelte'; + import { activityText } from '$lib/AniList/activity.js'; + import SettingHint from '$lib/Settings/SettingHint.svelte'; // import { io } from 'socket.io-client'; export let data; + interface ImportImage { + link?: string; + image: string; + } + let editMode = false; + let importMode = false; let currentUserIdentity: ReturnType<typeof userIdentity>; let error: null | string; // const socket = io(); @@ -30,7 +38,11 @@ let confirmDelete = 0; let selectedBadge: Badge | undefined = undefined; let loadError: string | null = null; + const isId = /^\d+$/.test(data.username); + let importImages: ImportImage[] | undefined = undefined; // let badgeCount = 0; + let importLinks = false; + let importCategory = ''; // $: downloadDisabled = badgeCount > 20; @@ -50,7 +62,6 @@ onMount(async () => { // socket.on('badges', (message) => (badges = message)); - const isId = /^\d+$/.test(data.username); const badger = isId ? { id: parseInt(data.username), @@ -67,7 +78,7 @@ badgesPromise = fetch(root(`/api/badges?id=${badger.id}`)); awcPromise = fetch(proxy(`https://awc.moe/challenger/${badger.name}`)); - if (data.user) { + if (data.user && !isId) { currentUserIdentity = userIdentity(data.user); // socket.emit('badges', data.user); @@ -311,6 +322,71 @@ return url; }; + + const parsePost = async () => { + if (importImages && importImages.length > 0) importImages = undefined; + + const link = (document.querySelector('#import_activity_url') as HTMLInputElement).value; + const type = link.replace(/.*\/(activity|thread)\/(\d+).*/, '$1'); + const id = link.replace(/.*\/(activity|thread)\/(\d+).*/, '$2'); + + if (type !== 'activity') return null; + + let text = await activityText(parseInt(id)); + + const images: ImportImage[] = []; + + if (importLinks) { + Array.from(new DOMParser().parseFromString(text, 'text/html').querySelectorAll('a')).forEach( + (a) => { + const anchor = a as HTMLAnchorElement; + + if (anchor.querySelector('img')) { + images.push({ + link: anchor.href, + image: (anchor.querySelector('img') as HTMLImageElement).src + }); + } + } + ); + + text = text.replace(/<a.*?>.*?<img.*?>.*?<\/a>/g, ''); + + Array.from( + new DOMParser().parseFromString(text, 'text/html').querySelectorAll('img') + ).forEach((img) => { + const image = img as HTMLImageElement; + + images.push({ + image: image.src + }); + }); + } else { + Array.from( + new DOMParser().parseFromString(text, 'text/html').querySelectorAll('img') + ).forEach((img) => { + const image = img as HTMLImageElement; + + images.push({ + image: image.src + }); + }); + } + + importImages = images; + }; + + const importBadges = () => + importImages?.forEach((image) => { + badgesPromise = fetch( + `/api/badges?image=${encodeURIComponent(image.image)}&post=${encodeURIComponent( + image.link || '#' + )}${importCategory.length > 0 ? `&category=${encodeURIComponent(importCategory)}` : ''}`, + { + method: 'PUT' + } + ); + }); </script> <HeadTitle route={`${data.username}'s Badge Wall`} path={`/user/${data.username}`} /> @@ -325,7 +401,7 @@ <Skeleton grid={true} count={100} width="150px" height="170px" /> {:then identity} - {@const isOwner = identity && identity.name === data.username} + {@const isOwner = identity && (isId ? identity.id : identity.name) === data.username} {#await badgesPromise} <Message message="Loading badges ..." /> @@ -399,6 +475,18 @@ ? $locale().user.badges.editMode.disable : $locale().user.badges.editMode.enable} </button> + <span style="margin: 0 0.625rem;">•</span> + <button + on:click={() => { + if (importMode) selectedBadge = undefined; + + importMode = !importMode; + }} + > + {importMode + ? $locale().user.badges.importMode.disable + : $locale().user.badges.importMode.enable} + </button> {#if editMode && isOwner} {@const groups = groupedBadges @@ -581,6 +669,65 @@ {/await} {/if} +{#if importMode} + <div class="popup popup-fullscreen"> + <div class="card"> + <SettingHint>Import badges from an activity post</SettingHint> + + <p /> + + <input + type="text" + placeholder={$locale().user.badges.editMode.activityURL} + id="import_activity_url" + minlength="1" + maxlength="1000" + size="20" + /> + <input + type="text" + placeholder={$locale().user.badges.editMode.category} + id="import_category" + minlength="1" + maxlength="1000" + size="20" + /> + + <p /> + + <input type="checkbox" id="import_links" name="import_links" bind:checked={importLinks} /> + Import Links + <SettingHint lineBreak> + If your images are nested in links, check this box to import the links as well. + </SettingHint> + + <p /> + + <button + on:click={() => { + importMode = false; + importImages = undefined; + }} + class="button-lined" + > + {$locale().user.badges.importMode.cancel} + </button> + <button on:click={() => parsePost()} class="button-lined" style="float: right;"> + {$locale().user.badges.importMode.fetch} + </button> + + {#if importImages && importImages.length > 0} + <p /> + + Import {importImages.length} badges? + <button on:click={() => importBadges()} class="button-lined no-shadow"> + {$locale().user.badges.importMode.import} + </button> + {/if} + </div> + </div> +{/if} + <style> /* body { margin: 0; diff --git a/src/styles/popup.scss b/src/styles/popup.scss index 645e9d23..fda4831e 100644 --- a/src/styles/popup.scss +++ b/src/styles/popup.scss @@ -9,3 +9,10 @@ justify-content: center; align-items: center; } + +.popup-fullscreen { + width: 100%; + height: 100%; + background-color: rgba(0, 0, 0, 0.5); + z-index: 9999; +} |