Compare commits

...

2 Commits

Author SHA1 Message Date
Patrick Cleavelin 899ff7de8d DOWNLOADS 2023-03-03 23:13:55 -06:00
Patrick Cleavelin 1e68c465c9 refactor intro selector 2023-03-03 19:13:02 -06:00
8 changed files with 228 additions and 75 deletions

View File

@ -0,0 +1,90 @@
<script>
import { intros, member } from './store.ts';
import { member_can, Permission } from './permissions.ts';
import { onMount } from 'svelte';
let enteredUrl = '';
let enteredTitle = '';
let selectedGuild = null;
let canDownloadAny = false;
let downloadPromise = null;
let allowedGuildList = [];
$: allowedGuildList = $member.guilds
.filter((guild) => member_can(guild.permissions, Permission.CanDownload))
.map((guild) => guild.name);
$: canDownloadAny = allowedGuildList.length > 0;
const download = () => {
if (!!selectedGuild) {
downloadPromise = (async () => {
await intros.addIntro(selectedGuild, enteredUrl, enteredTitle, $member.token);
await intros.fetchIntros($member.guilds);
})();
} else {
}
};
</script>
{#if canDownloadAny}
<div>
<h3>Download New Intro</h3>
{#if !!downloadPromise}
{#await downloadPromise}
<p>downloading...</p>
{:then result}
<p>Downloaded</p>
<button on:click={() => {downloadPromise = null}}>Add another</button>
{:catch err}
<p style='color: red'>{err}</p>
<button on:click={() => {downloadPromise = null}}>Ok</button>
{/await}
{:else}
<select bind:value={selectedGuild}>
{#each allowedGuildList as guild}
<option value={guild}>{guild}</option>
{/each}
</select>
<input bind:value={enteredTitle} placeholder='enter intro title'>
<input bind:value={enteredUrl} placeholder='enter video url'>
<button on:click={download}>Download</button>
{/if}
</div>
{/if}
<style>
div {
display: flex;
width: 80%;
flex-direction: column;
align-items: center;
background-color: #2a2a4a;
padding: 1.5em;
box-shadow: 1px 3px 4px 1px #1f1f36;
}
h3 {
margin-top: 0;
margin-bottom: 2em;
}
input, button {
margin: 0.5em;
}
input {
border-style: solid;
border-color: #323259;
width: 100%;
text-align: center;
color: lightgrey;
background-image: linear-gradient(0deg, #23233d, #1f1f36);
}
input:focus-visible {
outline: 2px solid #393963;
}
</style>

View File

@ -1,16 +1,55 @@
<script>
import { intros } from './store.ts';
import { createEventDispatcher } from 'svelte';
import { createEventDispatcher, onMount } from 'svelte';
import { fade, fly, crossfade } from 'svelte/transition';
import { quintOut } from 'svelte/easing';
import { flip } from 'svelte/animate';
export let guild = null;
export let channel = null;
export let guildIntros = null;
export let introList = null;
export let exclude = null;
export let include = null;
export let btnLabel = 'Add';
export let emptyMsg = null;
let filteredIntroList = [];
const getFiltered = () => {
const guildIntros = Array.from($intros.get(guild).entries()).map(([index, intro]) => {
return {
index: index,
...intro,
}
});
return guildIntros.filter((e) => {
if (!!exclude) {
for(const item of exclude) {
if (e.index == item) {
return false;
}
}
return true;
} else if (!!include) {
for(const item of include) {
if (e.index == item) {
return true;
}
}
return false;
}
return true;
});
};
$: if (!!($intros.get(guild)) && (!!include || !!exclude)) {
filteredIntroList = getFiltered();
};
const dispatch = createEventDispatcher();
const onConfirm = () => {
@ -22,34 +61,37 @@
selectedIntros = [];
}
const [send, receive] = crossfade({
duration: (d) => Math.sqrt(d * 20),
fallback(node, params) {
const style = getComputedStyle(node);
const transform = style.transform === 'none' ? '' : style.transform;
return {
duration: 200,
easing: quintOut,
css: (t) => `
transform: ${transform} scale(${t});
opacity: ${t}
`
}
}
});
let selectedIntros = [];
</script>
<div id="list">
{#if !!introList && !!guildIntros}
{#if introList.length > 0}
{#each introList as intro}
<label id="list-item">
<input type="checkbox" bind:group={selectedIntros} name="selectedIntros" value={intro.index}>
{guildIntros[intro.index].name} ({!!guildIntros[intro.index].filename ? guildIntros[intro.index].filename : guildIntros[intro.index].url})
</label>
{/each}
{:else if !!emptyMsg}
<p style='color: yellow'>{emptyMsg}</p>
{:else}
<p>No intros</p>
{/if}
{:else if !!exclude && !!guildIntros}
{#each guildIntros as intro, i}
{#if (!exclude.map((e) => e.index).includes(i))}
<label id="list-item">
<input type="checkbox" bind:group={selectedIntros} name="selectedIntros" value={i}>
{intro.name} ({!!intro.filename ? intro.filename : intro.url})
</label>
{/if}
{#if !!filteredIntroList && filteredIntroList.length > 0}
{#each filteredIntroList as intro (intro.index)}
<label out:send={{ key: intro.index }} in:receive={{ key: intro.index }} animate:flip={{ duration: 200 }} id="list-item">
<input type="checkbox" bind:group={selectedIntros} name="selectedIntros" value={intro.index}>
{intro.name}
</label>
{/each}
{:else}
<p>No intros</p>
<p style="color: red">{emptyMsg}</p>
{/if}
<button on:click={onConfirm}>{btnLabel}</button>
</div>

View File

@ -1,12 +1,20 @@
<script>
import { onMount } from 'svelte';
import { member, intros } from './store.ts';
const authorizeUri = "https://discord.com/api/oauth2/authorize?client_id=577634620728934400&redirect_uri=http%3A%2F%2Flocalhost%3A5173%2Fauth&response_type=code&scope=identify%20guilds%20guilds.members.read";
onMount(async () => {
const token = window.localStorage.getItem('token');
if (!!token) {
await member.pullData(token);
await intros.fetchIntros($member.guilds);
}
});
const login = async (username) => {
window.location = authorizeUri;
//await member._fakeLogin(username);
//await intros.fetchIntros($member.guilds);
}
let loginPromise = null;

View File

@ -12,6 +12,7 @@
flex-direction: column;
align-items: center;
background-color: #313052;
font-family: 'Cantarell';
}
h1, h2, h3, h4, h5, p, label, li {
@ -24,6 +25,7 @@
align-items: center;
background-color: #323259;
padding: 0.5em;
box-shadow: 1px 3px 15px 1px #1f1f36;
}
div#guild-settings {
@ -32,6 +34,7 @@
align-items: center;
background-color: #2a2a4a;
margin: 1em;
box-shadow: 1px 3px 4px 1px #1f1f36;
}
div#channel-settings {
@ -49,22 +52,27 @@
align-items: center;
}
#list-item {
border-style: solid;
button, #list-item {
color: lightgray;
border-style: none;
border-radius: 4px;
border-color: #1f1f36;
background-color: #1f1f36;
background-image: linear-gradient(0deg, #1f1f36, #23233d);
padding: 0.5em 0.5em 0.5em;
margin: 0 0 16px 0px;
width: 100%;
user-select: none;
}
button:hover, #list-item:hover {
background-image: linear-gradient(0deg, #34345b, #393963);
box-shadow: 1px 3px 15px 1px #1f1f36;
}
#list-item > input[type="checkbox"] {
display: none;
}
#list-item:has(input[type="checkbox"]:checked) {
background-color: #40406e;
background-image: linear-gradient(0deg, #40406e, #444475);
}
</style>
<body data-sveltekit-preload-data="hover">

8
src/permissions.ts Normal file
View File

@ -0,0 +1,8 @@
export const Permission = {
None: 0,
CanDownload: 1,
}
export const member_can = (permissions, perm) => {
return (permissions & perm) > 0;
};

View File

@ -1,9 +1,12 @@
<script>
import { member, intros } from '../store.ts';
import { member_can } from '../permissions.ts';
import Login from '../Login.svelte';
import IntroSelector from '../IntroSelector.svelte';
import IntroDownloader from '../IntroDownloader.svelte';
let addIntroPromise = null;
let removeIntroPromise = null;
const apiAddIntro = async (guild, channel, username, selectedIntros) => {
for (const intro of selectedIntros) {
@ -17,7 +20,7 @@
}
}
await member._fakeLogin(username);
await member.pullData($member.token);
};
const apiRemoveIntro = async (guild, channel, username, selectedIntros) => {
@ -32,7 +35,7 @@
}
}
await member._fakeLogin(username);
await member.pullData($member.token);
};
const addIntros = (event) => {
@ -40,7 +43,7 @@
}
const removeIntros = (event) => {
addIntroPromise = apiRemoveIntro(event.detail.guild, event.detail.channel, $member.username, event.detail.intros);
removeIntroPromise = apiRemoveIntro(event.detail.guild, event.detail.channel, $member.username, event.detail.intros);
}
</script>
@ -53,19 +56,23 @@
<div id="intros">
{#each $member.guilds as guild}
<h4>{guild.name}</h4>
<IntroDownloader />
<div id="guild-settings">
{#each guild.channels as channel}
<div id="channel-settings">
<h4>{channel.name}</h4>
{#await addIntroPromise then result}
{:catch err}
<p style='color: red'>Failed to add intro: {err}</p>
<p style='color: red'>Failed to add intro</p>
{/await}
{#await removeIntroPromise then result}
{:catch err}
<p style='color: red'>Failed to remove intro</p>
{/await}
<IntroSelector
guildIntros={$intros[guild.name]}
guild={guild.name}
channel={channel.name}
introList={channel.intros}
include={channel.intros.map((x) => x.index)}
on:confirm={removeIntros}
btnLabel="Remove"
emptyMsg="You don't have any intros, try adding one"
@ -73,11 +80,11 @@
<h3>Add Intros</h3>
<IntroSelector
guildIntros={$intros[guild.name]}
guild={guild.name}
channel={channel.name}
exclude={channel.intros}
exclude={channel.intros.map((x) => x.index)}
on:confirm={addIntros}
emptyMsg="There are no intros"
/>
</div>
{/each}

View File

@ -15,10 +15,12 @@
if (!response.ok) {
loginFailed = true
} else {
await member.login(body.token);
await member.pullData(body.token);
await intros.fetchIntros($member.guilds);
goto('/')
window.localStorage.setItem('token', body.token);
goto('/')
}
});
</script>

View File

@ -1,26 +1,14 @@
import { readable, writable } from 'svelte/store';
type IntroIndex = number;
interface MemberStore {
username: string
intros: IntroIndex[]
}
interface Intro {
name: string
filename: string
length: number
}
function createMemberStore(): MemberStore {
function createMemberStore() {
const { subscribe, set, update } = writable(null)
return {
subscribe: subscribe,
set: set,
addIntro: (intro: IntroIndex) => { update((n) => n.intros.push(intro)); return intro },
login: async (token) => {
pullData: async (token) => {
const response = (await (await fetch(
'http://localhost:7756/me',
{ headers: {"token": token} })).json())
@ -34,44 +22,44 @@ function createMemberStore(): MemberStore {
}
}
function createIntroStore(): IntroStore {
const { subscribe, set, update } = writable({})
function createIntroStore() {
const { subscribe, set, update } = writable(new Map())
return {
subscribe: subscribe,
set: set,
update: update,
fetchIntros: async (guilds) => {
console.debug('Fetching intros');
console.log(guilds)
addIntro: async (guild, url, title, token) => {
const response = await fetch(`http://localhost:7756/intros/${guild}/add/${encodeURIComponent(url)}?name=${encodeURIComponent(title)}`,
{ method: 'GET', headers: { 'token': token } });
let intros = {};
if (!response.ok) {
throw new Error(await response.body);
}
},
fetchIntros: async (guilds) => {
let intros = new Map();
for (const guild of guilds) {
const response = (await (await fetch(`http://localhost:7756/intros/${guild.name}`)).json())
if (response !== "NoGuildFound") {
const guild_intros = response.Intros.map((intro) => {
let guild_intros = new Map();
Object.entries(response.Intros).forEach(([index, intro]) => {
if (!!intro.File) {
return { name: intro.File.friendlyName, filename: intro.File.filename }
guild_intros.set(index, { name: intro.File.friendlyName, filename: intro.File.filename });
} else if (!!intro.Online) {
return { name: intro.Online.friendlyName, url: intro.Online.url }
guild_intros.set(index, { name: intro.Online.friendlyName, filename: intro.Online.filename });
}
})
intros[guild.name] = guild_intros;
intros.set(guild.name, guild_intros);
}
}
console.debug('Setting Intros store');
console.debug(intros);
console.debug(intros[123]);
set(intros)
}
}
}
export const intros: IntroStore = createIntroStore()
export const member: MemberStore = createMemberStore()
export const intros = createIntroStore()
export const member = createMemberStore()