chore: Merge branch dev to main (#304)

This commit is contained in:
Ushie
2025-06-26 16:51:52 +03:00
committed by GitHub
57 changed files with 1110 additions and 1164 deletions

View File

@@ -1,3 +1,5 @@
RV_API_URL=https://api.revanced.app RV_API_URL=https://api.revanced.app
RV_STATUS_URL=https://status.revanced.app
RV_EMAIL=contact@revanced.app
RV_GOOGLE_TAG_MANAGER_ID= RV_GOOGLE_TAG_MANAGER_ID=
RV_DMCA_GUID= RV_DMCA_GUID=

View File

@@ -34,6 +34,8 @@ jobs:
RV_API_URL: ${{ vars.RV_API_URL }} RV_API_URL: ${{ vars.RV_API_URL }}
RV_GOOGLE_TAG_MANAGER_ID: ${{ vars.RV_GOOGLE_TAG_MANAGER_ID }} RV_GOOGLE_TAG_MANAGER_ID: ${{ vars.RV_GOOGLE_TAG_MANAGER_ID }}
RV_DMCA_GUID: ${{ vars.RV_DMCA_GUID }} RV_DMCA_GUID: ${{ vars.RV_DMCA_GUID }}
RV_STATUS_URL: ${{ vars.RV_STATUS_URL }}
RV_EMAIL: ${{ vars.RV_EMAIL }}
run: npm run build run: npm run build
- name: Deploy - name: Deploy

View File

@@ -22,6 +22,6 @@
</head> </head>
<body> <body>
%sveltekit.body% <div style="display: contents">%sveltekit.body%</div>
</body> </body>
</html> </html>

View File

@@ -143,7 +143,7 @@ p {
line-height: 1.75rem; line-height: 1.75rem;
} }
@media screen and (max-width: 767px) { @media (max-width: 768px) {
h1 { h1 {
font-size: 2.6rem; font-size: 2.6rem;
line-height: 3.75rem; line-height: 3.75rem;

View File

@@ -1,21 +1,13 @@
import { browser } from '$app/environment'; import { browser } from '$app/environment';
import { RV_API_URL } from '$env/static/public'; import { RV_API_URL, RV_EMAIL, RV_STATUS_URL } from '$env/static/public';
export const default_api_url = RV_API_URL; export const default_api_url = RV_API_URL;
export const default_status_url = RV_STATUS_URL;
export const default_email = RV_EMAIL;
const URL_KEY = 'revanced_api_url'; const URL_KEY = 'revanced_api_url';
const STATUS_KEY = 'revanced_status_url'; const STATUS_KEY = 'revanced_status_url';
const EMAIL_KEY = 'revanced_email';
function set_status_url(apiUrl: string) {
fetch(`${apiUrl}/v4/about`)
.then((response) => (response.ok ? response.json() : null))
.then((data) => {
if (data?.status) {
localStorage.setItem(STATUS_KEY, data.status);
console.log('Status is now:', localStorage.getItem(STATUS_KEY));
}
});
}
export const API_VERSION = 'v4'; export const API_VERSION = 'v4';
@@ -24,9 +16,7 @@ export function api_base_url(): string {
if (browser) { if (browser) {
const apiUrl = localStorage.getItem(URL_KEY) || default_api_url; const apiUrl = localStorage.getItem(URL_KEY) || default_api_url;
if (!localStorage.getItem(STATUS_KEY)) { set_about_info(apiUrl);
set_status_url(apiUrl);
}
return apiUrl; return apiUrl;
} }
@@ -34,12 +24,20 @@ export function api_base_url(): string {
return default_api_url; return default_api_url;
} }
export function status_url(): string | null { export function status_url(): string {
if (browser) { if (browser) {
return localStorage.getItem(STATUS_KEY) || null; return localStorage.getItem(STATUS_KEY) || default_status_url;
} }
return null; return default_status_url;
}
export function email(): string {
if (browser) {
return localStorage.getItem(EMAIL_KEY) || default_email;
}
return default_email;
} }
// (re)set base URL. // (re)set base URL.
@@ -48,6 +46,21 @@ export function set_api_base_url(url?: string) {
localStorage.removeItem(URL_KEY); localStorage.removeItem(URL_KEY);
} else { } else {
localStorage.setItem(URL_KEY, url); localStorage.setItem(URL_KEY, url);
set_status_url(url); set_about_info(url);
}
}
function set_about_info(apiUrl: string) {
if (!localStorage.getItem(STATUS_KEY) || !localStorage.getItem(EMAIL_KEY)) {
fetch(`${apiUrl}/v4/about`)
.then((response) => (response.ok ? response.json() : null))
.then((data) => {
if (data?.status) {
localStorage.setItem(STATUS_KEY, data.status);
localStorage.setItem(EMAIL_KEY, data.contact.email);
console.log('Status is now:', localStorage.getItem(STATUS_KEY));
console.log('Email is now:', localStorage.getItem(EMAIL_KEY));
}
});
} }
} }

View File

@@ -0,0 +1,61 @@
<script lang="ts">
import { read_announcements } from '$lib/stores';
import Banner from '$layout/Banners/Banner.svelte';
import { goto } from '$app/navigation';
import { createQuery } from '@tanstack/svelte-query';
import { queries } from '$data/api';
import moment from 'moment';
const query = createQuery(queries.announcements());
$: latestUnreadAnnouncement = (() => {
const announcements = $query.data?.announcements ?? [];
const nonArchived = announcements.filter(
(a) => !a.archived_at || moment(a.archived_at).isAfter(moment())
);
const announcement = nonArchived[0];
return announcement && !$read_announcements.has(announcement.id) ? announcement : undefined;
})();
function setAsRead() {
if (!latestUnreadAnnouncement) return;
read_announcements.update((set) => {
const updated = new Set(set);
updated.add(latestUnreadAnnouncement.id);
return updated;
});
}
function handleClick() {
if (!latestUnreadAnnouncement) return;
goto(`/announcements/${latestUnreadAnnouncement.id}`);
setAsRead();
}
function handleClose() {
if (!latestUnreadAnnouncement) return;
setAsRead();
}
function getBannerLevel(level: number | undefined): 'info' | 'caution' {
if (!level || level == 0) return 'info';
return 'caution';
}
</script>
{#if latestUnreadAnnouncement}
<Banner
title={'We have an announcement'}
description={`You can read more about "${latestUnreadAnnouncement.title}" in our latest post.`}
level={getBannerLevel(latestUnreadAnnouncement.level)}
buttonText="Read more"
buttonOnClick={handleClick}
onDismiss={handleClose}
/>
{/if}

View File

@@ -2,7 +2,7 @@
import { createEventDispatcher } from 'svelte'; import { createEventDispatcher } from 'svelte';
import Close from 'svelte-material-icons/Close.svelte'; import Close from 'svelte-material-icons/Close.svelte';
import ArrowRight from 'svelte-material-icons/ArrowRight.svelte'; import ArrowRight from 'svelte-material-icons/ArrowRight.svelte';
import Button from './Button.svelte'; import Button from '$lib/components/Button.svelte';
export let title: string; export let title: string;
export let description: string | undefined = undefined; export let description: string | undefined = undefined;
@@ -83,7 +83,7 @@
color: #601410; color: #601410;
} }
@media (max-width: 767px) { @media (max-width: 768px) {
flex-direction: column; flex-direction: column;
padding: 1.1rem 1.3rem; padding: 1.1rem 1.3rem;
} }

View File

@@ -1,6 +1,7 @@
<script lang="ts"> <script lang="ts">
import { goto } from '$app/navigation'; import { goto } from '$app/navigation';
import Banner from '$lib/components/Banner.svelte'; import { email } from '$data/api/settings';
import Banner from '$layout/Banners/Banner.svelte';
export let statusUrl: string | null = null; export let statusUrl: string | null = null;
@@ -9,7 +10,7 @@
<Banner <Banner
title="API service is currently down" title="API service is currently down"
description="We're actively investigating and will update you shortly. We appreciate your patience." description="Some features of the site might be impacted. If this issue persists, reach out to mailto:{email()}"
buttonText={statusUrl ? 'View status' : undefined} buttonText={statusUrl ? 'View status' : undefined}
buttonOnClick={statusUrl ? handleClick : undefined} buttonOnClick={statusUrl ? handleClick : undefined}
level="caution" level="caution"

View File

@@ -0,0 +1,55 @@
<script lang="ts">
import Button from '$lib/components/Button.svelte';
import Dialog from '$layout/Dialogs/Dialog.svelte';
import { onMount } from 'svelte';
import { allowAnalytics } from '$lib/stores';
import { RV_GOOGLE_TAG_MANAGER_ID } from '$env/static/public';
let showConsentDialog = false;
function enableAnalytics() {
//@ts-ignore
window.dataLayer = window.dataLayer || [];
function gtag(...args: any[]) {
//@ts-ignore
window.dataLayer.push(args);
}
gtag('js', new Date());
const script = document.createElement('script');
script.src = `https://www.googletagmanager.com/gtm.js?id=${RV_GOOGLE_TAG_MANAGER_ID}`;
document.head.append(script);
}
function handleConsent(allowed: boolean) {
localStorage.setItem('analytics', allowed.toString());
allowAnalytics.set(allowed);
showConsentDialog = false;
if (allowed) enableAnalytics();
}
onMount(() => {
const savedConsent = localStorage.getItem('analytics');
if (savedConsent !== null) {
const allowed = savedConsent === 'true';
allowAnalytics.set(allowed);
if (allowed) enableAnalytics();
} else {
showConsentDialog = true;
}
});
</script>
<Dialog bind:dialogOpen={showConsentDialog} notDismissible>
<svelte:fragment slot="title">It's your choice</svelte:fragment>
<svelte:fragment slot="description">
We use analytics to improve your experience on this site. By clicking "Allow", you allow us to
collect anonymous data about your visit.
</svelte:fragment>
<svelte:fragment slot="buttons">
<Button type="text" on:click={() => handleConsent(false)}>Deny</Button>
<Button type="filled" on:click={() => handleConsent(true)}>Allow</Button>
</svelte:fragment>
</Dialog>

View File

@@ -0,0 +1,145 @@
<script lang="ts">
import Button from '$lib/components/Button.svelte';
import Dialog from '$layout/Dialogs/Dialog.svelte';
import CircleMultipleOutline from 'svelte-material-icons/CircleMultipleOutline.svelte';
import ChevronUp from 'svelte-material-icons/ChevronUp.svelte';
import QRCode from '$lib/components/QRCode.svelte';
import WalletOutline from 'svelte-material-icons/WalletOutline.svelte';
import Snackbar from '$lib/components/Snackbar.svelte';
export let dialogOpen: boolean;
export let wallets;
let qrCodeValue = '';
let qrCodeDialogueName = '';
let qrCodeDialogue = false;
let addressSnackbar = false;
async function copyToClipboard(walletAddress: string) {
addressSnackbar = true;
qrCodeDialogue = false;
try {
await navigator.clipboard.writeText(walletAddress);
} catch (error) {
console.error('Failed to copy crypto wallet:', error);
}
}
</script>
<Dialog bind:dialogOpen>
<svelte:fragment slot="icon">
<CircleMultipleOutline size="32px" color="var(--surface-six)" />
</svelte:fragment>
<svelte:fragment slot="title">Cryptocurrencies</svelte:fragment>
<svelte:fragment slot="description">
<hr style="margin: 1rem 0;" />
<div class="wallets">
{#each wallets as wallet}
<button
on:click={() => {
qrCodeValue = wallet.address;
qrCodeDialogueName = wallet.currency_code;
qrCodeDialogue = !qrCodeDialogue;
// when the user clicks a wallet the crypto wallet goes away
// because multi page dialogues aren't implemented yet oops
dialogOpen = false;
}}
>
<div class="wallet-name">
<img
src="/donate/crypto/{wallet.currency_code}.svg"
onerror="this.onerror=null; this.src='/donate/fallback.svg'"
alt={`${wallet.network} icon.'`}
/>
{`${wallet.network} (${wallet.currency_code})`}
</div>
<div id="arrow">
<ChevronUp size="20px" color="var(--surface-six)" />
</div>
</button>
{/each}
</div>
</svelte:fragment>
<svelte:fragment slot="buttons">
<Button type="filled" on:click={() => (dialogOpen = false)}>Close</Button>
</svelte:fragment>
</Dialog>
<Dialog bind:dialogOpen={qrCodeDialogue}>
<svelte:fragment slot="icon">
<WalletOutline size="32px" color="var(--surface-six)" />
</svelte:fragment>
<svelte:fragment slot="title">{qrCodeDialogueName} Wallet</svelte:fragment>
<svelte:fragment slot="description">
<div class="qr-code-body">
{qrCodeValue}
<QRCode codeValue={qrCodeValue} />
</div>
</svelte:fragment>
<svelte:fragment slot="buttons">
<Button
type="text"
on:click={() => {
qrCodeDialogue = false;
dialogOpen = true;
}}>Back</Button
>
<Button type="filled" on:click={() => copyToClipboard(qrCodeValue)}>Copy Address</Button>
</svelte:fragment>
</Dialog>
<Snackbar bind:open={addressSnackbar}>
<svelte:fragment slot="text">Address copied to clipboard</svelte:fragment>
</Snackbar>
<style lang="scss">
.wallets {
// i just guessed this
width: clamp(200px, 75vw, 375px);
#arrow {
height: 20px;
transform: rotate(90deg);
}
button {
width: 100%;
font-size: 0.9rem;
background-color: transparent;
border: none;
color: var(--text-four);
cursor: pointer;
text-align: left;
display: flex;
justify-content: space-between;
background-color: var(--surface-seven);
padding: 0.75rem 1.25rem;
transition: filter 0.4s var(--bezier-one);
&:hover {
filter: brightness(85%);
}
}
.wallet-name {
display: flex;
align-items: center;
gap: 0.5rem;
// crypto icon
img {
height: 24px;
width: 24px;
}
}
}
.qr-code-body {
display: flex;
flex-direction: column;
align-items: center;
gap: 1rem;
word-break: break-word;
text-align: center;
}
</style>

View File

@@ -4,7 +4,7 @@
import ArrowLeft from 'svelte-material-icons/ArrowLeft.svelte'; import ArrowLeft from 'svelte-material-icons/ArrowLeft.svelte';
export let modalOpen = false; export let dialogOpen = false;
export let fullscreen = false; export let fullscreen = false;
export let notDismissible = false; export let notDismissible = false;
@@ -16,20 +16,20 @@
} }
</script> </script>
{#if modalOpen} {#if dialogOpen}
<!-- svelte-ignore a11y-no-static-element-interactions -->
<div <div
class="overlay" class="overlay"
on:click={() => { on:click={() => {
if (!notDismissible) modalOpen = !modalOpen; if (!notDismissible) dialogOpen = !dialogOpen;
}} }}
on:keypress={() => { on:keypress={() => {
if (!notDismissible) modalOpen = !modalOpen; if (!notDismissible) dialogOpen = !dialogOpen;
}} }}
transition:fade={{ easing: quadInOut, duration: 150 }} transition:fade={{ easing: quadInOut, duration: 150 }}
/> />
<dialog <dialog
class="modal"
class:fullscreen class:fullscreen
class:scrolled={y > 10} class:scrolled={y > 10}
aria-modal="true" aria-modal="true"
@@ -37,41 +37,39 @@
on:scroll={parseScroll} on:scroll={parseScroll}
transition:fade={{ easing: quadInOut, duration: 150 }} transition:fade={{ easing: quadInOut, duration: 150 }}
> >
<div class="top"> <div class="title" class:hasIcon={$$slots.icon}>
<div class="title" class:hasIcon={$$slots.icon}> {#if fullscreen}
{#if fullscreen} <button id="back-button" on:click={() => (dialogOpen = !dialogOpen)}>
<button id="back-button" on:click={() => (modalOpen = !modalOpen)}> <ArrowLeft size="24px" color="var(--surface-six)" />
<ArrowLeft size="24px" color="var(--surface-six)" /> </button>
</button>
{/if}
{#if $$slots.icon}
<slot name="icon" />
{/if}
{#if $$slots.title}
<h3>
<slot name="title" />
</h3>
{/if}
</div>
{#if $$slots.description}
<p>
<slot name="description" />
</p>
{/if} {/if}
{#if $$slots.icon}
<div class="slot"><slot /></div> <slot name="icon" />
{/if}
{#if $$slots.buttons} {#if $$slots.title}
<div class="buttons"> <h3>
<slot name="buttons" /> <slot name="title" />
</div> </h3>
{/if} {/if}
</div> </div>
{#if $$slots.description}
<p>
<slot name="description" />
</p>
{/if}
<div class="slot"><slot /></div>
{#if $$slots.buttons}
<div class="buttons">
<slot name="buttons" />
</div>
{/if}
</dialog> </dialog>
{/if} {/if}
<style> <style lang="scss">
.overlay { .overlay {
position: fixed; position: fixed;
top: 0; top: 0;
@@ -82,37 +80,7 @@
z-index: 6; z-index: 6;
} }
.top { dialog {
display: flex;
flex-direction: column;
align-items: center;
gap: 1rem;
}
.title {
display: flex;
align-items: center;
gap: 1rem;
width: 100%;
background-color: var(--surface-seven);
}
.buttons {
display: flex;
gap: 2rem;
justify-content: flex-end;
width: 100%;
}
#back-button {
cursor: pointer;
}
.hasIcon {
flex-direction: column;
}
.modal {
position: fixed; position: fixed;
width: min(85%, 425px); width: min(85%, 425px);
max-height: 75%; max-height: 75%;
@@ -128,7 +96,8 @@
white-space: normal; white-space: normal;
display: flex; display: flex;
flex-direction: column; flex-direction: column;
gap: 2px; align-items: center;
gap: 1rem;
z-index: 7; z-index: 7;
padding: 32px; padding: 32px;
box-shadow: box-shadow:
@@ -137,6 +106,57 @@
0px 2px 4px -1px rgba(0, 0, 0, 0.2); 0px 2px 4px -1px rgba(0, 0, 0, 0.2);
scrollbar-width: none; scrollbar-width: none;
-ms-overflow-style: none; -ms-overflow-style: none;
&::-webkit-scrollbar {
display: none;
}
#back-button {
cursor: pointer;
}
.hasIcon {
flex-direction: column;
}
.title {
display: flex;
align-items: center;
gap: 1rem;
width: 100%;
background-color: var(--surface-seven);
}
.buttons {
display: flex;
gap: 2rem;
justify-content: flex-end;
width: 100%;
}
&.fullscreen {
padding: 0;
max-height: 100%;
width: 100%;
border-radius: 0;
&.scrolled .title {
border-bottom: 1px solid var(--border);
}
.slot {
padding: 0 32px 32px;
}
.title {
justify-content: flex-start;
position: sticky;
padding: 32px;
padding-bottom: 0.75rem;
top: 0;
left: 0;
}
}
} }
button { button {
@@ -148,38 +168,10 @@
align-items: center; align-items: center;
} }
.fullscreen {
padding: 0;
max-height: 100%;
width: 100%;
border-radius: 0;
}
.fullscreen .slot {
padding: 0 32px 32px;
}
.fullscreen .title {
justify-content: flex-start;
position: sticky;
padding: 32px;
padding-bottom: 0.75rem;
top: 0;
left: 0;
}
.fullscreen.scrolled .title {
border-bottom: 1px solid var(--border);
}
.slot { .slot {
display: flex; display: flex;
flex-direction: column; flex-direction: column;
align-content: center; align-content: center;
width: 100%; width: 100%;
} }
.modal::-webkit-scrollbar {
display: none;
}
</style> </style>

View File

@@ -0,0 +1,25 @@
<script lang="ts">
import Button from '$lib/components/Button.svelte';
import { Query } from '@tanstack/query-core';
import Dialog from '$layout/Dialogs/Dialog.svelte';
import { queries } from '$data/api';
import { createQuery } from '@tanstack/svelte-query';
export let dialogOpen: boolean;
export let warning: string;
const query = createQuery(queries.manager());
</script>
<Dialog bind:dialogOpen>
<svelte:fragment slot="title">Warning</svelte:fragment>
<svelte:fragment slot="description">{warning} Do you still want to download?</svelte:fragment>
<svelte:fragment slot="buttons">
<Query {query} let:data>
<Button type="text" href={data.release.download_url} on:click={() => (dialogOpen = false)}>
Okay
</Button>
</Query>
<Button type="text" on:click={() => (dialogOpen = false)}>Cancel</Button>
</svelte:fragment>
</Dialog>

View File

@@ -7,12 +7,12 @@
const dispatch = createEventDispatcher(); const dispatch = createEventDispatcher();
function closeModal() { function closeDialog() {
dispatch('close'); dispatch('close');
} }
function handleKeydown(event: KeyboardEvent) { function handleKeydown(event: KeyboardEvent) {
if (event.key === 'Escape') closeModal(); if (event.key === 'Escape') closeDialog();
} }
</script> </script>
@@ -20,16 +20,16 @@
<!-- svelte-ignore a11y-click-events-have-key-events --> <!-- svelte-ignore a11y-click-events-have-key-events -->
<!-- svelte-ignore a11y-no-static-element-interactions --> <!-- svelte-ignore a11y-no-static-element-interactions -->
<div class="modal-overlay" on:click={closeModal} transition:fade={{ duration: 175 }}> <div class="dialog-overlay" on:click={closeDialog} transition:fade={{ duration: 175 }}>
<!-- svelte-ignore a11y-no-static-element-interactions --> <!-- svelte-ignore a11y-no-static-element-interactions -->
<div class="modal-content" on:click|stopPropagation transition:fade={{ duration: 175 }}> <div class="dialog-content" on:click|stopPropagation transition:fade={{ duration: 175 }}>
<button class="close-button" on:click={closeModal}>×</button> <button class="close-button" on:click={closeDialog}>×</button>
<img {src} {alt} /> <img {src} {alt} />
</div> </div>
</div> </div>
<style> <style>
.modal-overlay { .dialog-overlay {
position: fixed; position: fixed;
top: 0; top: 0;
left: 0; left: 0;
@@ -44,7 +44,7 @@
box-sizing: border-box; box-sizing: border-box;
} }
.modal-content { .dialog-content {
position: relative; position: relative;
max-width: 90vw; max-width: 90vw;
max-height: 90vh; max-height: 90vh;

View File

@@ -3,10 +3,10 @@
import Input from '$lib/components/Input.svelte'; import Input from '$lib/components/Input.svelte';
import Button from '$lib/components/Button.svelte'; import Button from '$lib/components/Button.svelte';
import Modal from '$lib/components/Dialogue.svelte'; import Dialog from '$layout/Dialogs/Dialog.svelte';
import { passed_login_with_creds } from '$lib/stores'; import { passed_login_with_creds } from '$lib/stores';
export let modalOpen: boolean; export let dialogOpen: boolean;
let loginForm: HTMLFormElement; let loginForm: HTMLFormElement;
let wrong_credentials = false; let wrong_credentials = false;
@@ -19,15 +19,14 @@
const success = await login(username, password); const success = await login(username, password);
modalOpen = !success; dialogOpen = !success;
console.log(success);
passed_login_with_creds.set(success); passed_login_with_creds.set(success);
wrong_credentials = !success; wrong_credentials = !success;
} }
</script> </script>
<Modal bind:modalOpen> <Dialog bind:dialogOpen>
<div class="admin-modal-content"> <div class="container">
<h2>Login</h2> <h2>Login</h2>
<p>This login is reserved for site administrators. Go back!</p> <p>This login is reserved for site administrators. Go back!</p>
{#if wrong_credentials} {#if wrong_credentials}
@@ -46,14 +45,14 @@
</form> </form>
</div> </div>
<svelte:fragment slot="buttons"> <svelte:fragment slot="buttons">
<Button type="text" on:click={() => (modalOpen = !modalOpen)}>Cancel</Button> <Button type="text" on:click={() => (dialogOpen = !dialogOpen)}>Cancel</Button>
<!-- first paragraph of https://developer.mozilla.org/en-US/docs/Web/API/HTMLFormElement/submit --> <!-- first paragraph of https://developer.mozilla.org/en-US/docs/Web/API/HTMLFormElement/submit -->
<Button type="filled" on:click={() => loginForm.requestSubmit()}>Login</Button> <Button type="filled" on:click={() => loginForm.requestSubmit()}>Login</Button>
</svelte:fragment> </svelte:fragment>
</Modal> </Dialog>
<style lang="scss"> <style lang="scss">
.admin-modal-content { .container {
display: flex; display: flex;
flex-direction: column; flex-direction: column;
gap: 1rem; gap: 1rem;

View File

@@ -3,26 +3,26 @@
import { admin_login, passed_login_with_creds } from '$lib/stores'; import { admin_login, passed_login_with_creds } from '$lib/stores';
import Button from '$lib/components/Button.svelte'; import Button from '$lib/components/Button.svelte';
import Modal from '$lib/components/Dialogue.svelte'; import Dialog from '$layout/Dialogs/Dialog.svelte';
</script> </script>
<Modal bind:modalOpen={$passed_login_with_creds}> <Dialog bind:dialogOpen={$passed_login_with_creds}>
<svelte:fragment slot="title">Successfully logged in!</svelte:fragment> <svelte:fragment slot="title">Successfully logged in!</svelte:fragment>
<div class="login-success"> <div>
This session will expire in This session will expire in
<span class="exp-date">{$admin_login.logged_in ? fromNow($admin_login.expires) : '...'}</span> <span>{$admin_login.logged_in ? fromNow($admin_login.expires) : '...'}</span>
</div> </div>
<svelte:fragment slot="buttons"> <svelte:fragment slot="buttons">
<Button type="filled" on:click={() => passed_login_with_creds.set(false)}>OK</Button> <Button type="filled" on:click={() => passed_login_with_creds.set(false)}>OK</Button>
</svelte:fragment> </svelte:fragment>
</Modal> </Dialog>
<style> <style lang="scss">
.login-success { div {
color: var(--text-one); color: var(--text-one);
}
.exp-date { span {
color: var(--primary); color: var(--primary);
}
} }
</style> </style>

View File

@@ -0,0 +1,40 @@
<script lang="ts">
import Package from '../../routes/patches/Package.svelte';
import Dialog from '$layout/Dialogs/Dialog.svelte';
export let dialogOpen: boolean;
export let searchTerm: string;
export let data;
export let selectedPkg;
</script>
<Dialog bind:dialogOpen fullscreen>
<svelte:fragment slot="title">Packages</svelte:fragment>
<div class="mobile-packages">
<!-- svelte-ignore a11y-no-static-element-interactions -->
<span
on:click={() => (dialogOpen = !dialogOpen)}
on:keypress={() => (dialogOpen = !dialogOpen)}
>
<Package {selectedPkg} name="All packages" bind:searchTerm />
</span>
{#each data.packages as pkg}
<!-- svelte-ignore a11y-no-static-element-interactions -->
<span
on:click={() => (dialogOpen = !dialogOpen)}
on:keypress={() => (dialogOpen = !dialogOpen)}
>
<Package {selectedPkg} name={pkg} bind:searchTerm />
</span>
{/each}
</div>
</Dialog>
<style lang="scss">
.mobile-packages {
margin-bottom: -1px;
overflow: hidden;
border-radius: 12px;
border: 1px solid var(--border);
}
</style>

View File

@@ -1,9 +1,9 @@
<script lang="ts"> <script lang="ts">
import { set_access_token } from '$lib/auth'; import { set_access_token } from '$lib/auth';
import { admin_login } from '$lib/stores'; import { admin_login } from '$lib/stores';
import Button from '$lib/components/Button.svelte'; import Button from '$lib/components/Button.svelte';
import Modal from '$lib/components/Dialogue.svelte'; import Dialog from '$layout/Dialogs/Dialog.svelte';
export let loginOpen: boolean; export let loginOpen: boolean;
@@ -15,9 +15,9 @@
} }
</script> </script>
<Modal modalOpen={session_expired}> <Dialog dialogOpen={session_expired}>
<svelte:fragment slot="title">Expired session</svelte:fragment> <svelte:fragment slot="title">Expired session</svelte:fragment>
<div class="session-expired"> <div>
This session has expired, log in again to renew or lose all access to administrative power. This session has expired, log in again to renew or lose all access to administrative power.
</div> </div>
<svelte:fragment slot="buttons"> <svelte:fragment slot="buttons">
@@ -26,10 +26,10 @@
Login Login
</Button> </Button>
</svelte:fragment> </svelte:fragment>
</Modal> </Dialog>
<style> <style>
.session-expired { div {
color: var(--text-four); color: var(--text-four);
} }
</style> </style>

View File

@@ -5,12 +5,12 @@
import { useQueryClient } from '@tanstack/svelte-query'; import { useQueryClient } from '@tanstack/svelte-query';
import Button from '$lib/components/Button.svelte'; import Button from '$lib/components/Button.svelte';
import Modal from '$lib/components/Dialogue.svelte'; import Dialog from '$layout/Dialogs/Dialog.svelte';
import Replay from 'svelte-material-icons/Replay.svelte'; import Replay from 'svelte-material-icons/Replay.svelte';
import Cog from 'svelte-material-icons/Cog.svelte'; import Cog from 'svelte-material-icons/Cog.svelte';
export let loginOpen: boolean; export let loginOpen: boolean;
export let modalOpen: boolean; export let dialogOpen: boolean;
const client = useQueryClient(); const client = useQueryClient();
@@ -38,7 +38,7 @@
} }
</script> </script>
<Modal bind:modalOpen> <Dialog bind:dialogOpen>
<svelte:fragment slot="icon"> <svelte:fragment slot="icon">
<Cog size="24px" color="var(--surface-six)" /> <Cog size="24px" color="var(--surface-six)" />
</svelte:fragment> </svelte:fragment>
@@ -58,7 +58,7 @@
<Button <Button
type="text" type="text"
disabled={$admin_login.logged_in} disabled={$admin_login.logged_in}
on:click={() => ((loginOpen = !loginOpen), (modalOpen = !modalOpen))} on:click={() => ((loginOpen = !loginOpen), (dialogOpen = !dialogOpen))}
> >
{$admin_login.logged_in ? `Logged in for ${fromNow($admin_login.expires)}` : 'Login'} {$admin_login.logged_in ? `Logged in for ${fromNow($admin_login.expires)}` : 'Login'}
</Button> </Button>
@@ -68,7 +68,7 @@
</div> </div>
</div> </div>
</svelte:fragment> </svelte:fragment>
</Modal> </Dialog>
<style lang="scss"> <style lang="scss">
input { input {
@@ -106,7 +106,8 @@
justify-content: space-between; justify-content: space-between;
flex-wrap: wrap; flex-wrap: wrap;
gap: 1rem; gap: 1rem;
div {
.buttons {
display: flex; display: flex;
justify-content: flex-end; justify-content: flex-end;
flex-wrap: wrap; flex-wrap: wrap;

View File

@@ -9,6 +9,8 @@
import FooterSection from './FooterSection.svelte'; import FooterSection from './FooterSection.svelte';
import { RV_DMCA_GUID } from '$env/static/public'; import { RV_DMCA_GUID } from '$env/static/public';
import { onMount } from 'svelte'; import { onMount } from 'svelte';
import Divider from '$lib/components/Divider.svelte';
import { email } from '$data/api/settings';
const aboutQuery = createQuery(queries.about()); const aboutQuery = createQuery(queries.about());
@@ -20,104 +22,95 @@
}); });
</script> </script>
<Divider horizontalPadding={'15px'} />
<footer in:fly={{ y: 10, easing: quintOut, duration: 750 }}> <footer in:fly={{ y: 10, easing: quintOut, duration: 750 }}>
<svg <div class="top">
aria-hidden="true" <section class="main-content">
width="100%" <img src="/logo.svg" class="logo-image" alt="ReVanced Logo" />
height="8" <Query query={aboutQuery} let:data>
fill="none" <div>
xmlns="http://www.w3.org/2000/svg" <p>
in:fly={{ y: 10, easing: quintOut, duration: 750 }} {data.about.about}
> </p>
<pattern id="a" width="91" height="8" patternUnits="userSpaceOnUse"> </div>
<path </Query>
d="M114 4c-5.067 4.667-10.133 4.667-15.2 0S88.667-.667 83.6 4 73.467 8.667 68.4 4 58.267-.667 53.2 4 43.067 8.667 38 4 27.867-.667 22.8 4 12.667 8.667 7.6 4-2.533-.667-7.6 4s-10.133 4.667-15.2 0S-32.933-.667-38 4s-10.133 4.667-15.2 0-10.133-4.667-15.2 0-10.133 4.667-15.2 0-10.133-4.667-15.2 0-10.133 4.667-15.2 0-10.133-4.667-15.2 0-10.133 4.667-15.2 0-10.133-4.667-15.2 0-10.133 4.667-15.2 0-10.133-4.667-15.2 0-10.133 4.667-15.2 0-10.133-4.667-15.2 0-10.133 4.667-15.2 0-10.133-4.667-15.2 0-10.133 4.667-15.2 0-10.133-4.667-15.2 0-10.133 4.667-15.2 0-10.133-4.667-15.2 0-10.133 4.667-15.2 0-10.133-4.667-15.2 0-10.133 4.667-15.2 0-10.133-4.667-15.2 0-10.133 4.667-15.2 0-10.133-4.667-15.2 0-10.133 4.667-15.2 0" </section>
stroke-linecap="square"
/>
</pattern>
<rect width="100%" height="100%" fill="url(#a)" />
</svg>
<div class="footer-wrapper">
<div class="footer-top">
<section class="main-content">
<img src="/logo.svg" class="logo-image" alt="ReVanced Logo" />
<Query query={aboutQuery} let:data>
{#if data}
<div>
<p>
{data.about.about}
</p>
</div>
{/if}
</Query>
</section>
<section class="links-container"> <section class="links-container">
<FooterSection title="Pages"> <FooterSection title="Pages">
<li><a href="/">Home</a></li> <li><a href="/">Home</a></li>
<li><a href="/download">Download</a></li> <li><a href="/download">Download</a></li>
<li><a href="/patches">Patches</a></li> <li><a href="/patches">Patches</a></li>
<li><a href="/contributors">Contributors</a></li> <li><a href="/contributors">Contributors</a></li>
<li><a href="/donate">Donate</a></li> <li><a href="/donate">Donate</a></li>
</FooterSection> </FooterSection>
<Query query={aboutQuery} let:data>
{#if data}
<FooterSection title="Socials">
{#each data.about.socials as { name, url }}
<li>
<a href={url} target="_blank" rel="noreferrer">{name}</a>
</li>
{/each}
</FooterSection>
{/if}
</Query>
</section>
</div>
<div class="footer-bottom">
<div id="logo-name"><span>Re</span>Vanced</div>
<a href="/donate"><div>Donate</div></a>
<Query query={aboutQuery} let:data> <Query query={aboutQuery} let:data>
{#if data} {#if data}
<a href="mailto:{data.about.contact.email}"><div>Email</div></a> <FooterSection title="Socials">
{#each data.about.socials as { name, url }}
<li>
<a href={url} target="_blank" rel="noreferrer">{name}</a>
</li>
{/each}
</FooterSection>
{/if} {/if}
</Query> </Query>
<!-- DMCA Protection Badge --> </section>
<a </div>
href="//www.dmca.com/Protection/Status.aspx?ID={RV_DMCA_GUID}&refurl={location}"
title="DMCA.com Protection Status" <div class="bottom">
class="dmca-badge" <div id="logo-name"><span>Re</span>Vanced</div>
> <a href="/donate"><div>Donate</div></a>
<img <a href="mailto:{email()}"><div>Email</div></a>
src="https://images.dmca.com/Badges/dmca-badge-w150-5x1-08.png?ID={RV_DMCA_GUID}" <!-- DMCA Protection Badge -->
alt="DMCA.com Protection Status" <a
/></a href="//www.dmca.com/Protection/Status.aspx?ID={RV_DMCA_GUID}&refurl={location}"
> title="DMCA.com Protection Status"
</div> class="dmca-badge"
>
<img
src="https://images.dmca.com/Badges/dmca-badge-w150-5x1-08.png?ID={RV_DMCA_GUID}"
alt="DMCA.com Protection Status"
/>
</a>
</div> </div>
</footer> </footer>
<style> <style lang="scss">
footer { footer {
background-color: var(--background-one); background-color: var(--background-one);
}
.footer-wrapper {
max-width: min(87%, 100rem); max-width: min(87%, 100rem);
padding: 5rem 0rem; padding: 5rem 0rem;
margin: 0 auto; margin: 0 auto;
}
.footer-top { .top {
display: flex; display: flex;
gap: 8rem; gap: 8rem;
justify-content: space-between; justify-content: space-between;
margin-bottom: 4rem; margin-bottom: 4rem;
}
.footer-bottom { @media (max-width: 1050px) {
display: flex; flex-direction: column;
gap: 2rem; gap: 2rem;
align-items: center; }
}
.bottom {
display: flex;
gap: 2rem;
align-items: center;
a {
text-decoration: none;
color: var(--text-four);
font-weight: 600;
}
@media (max-width: 768px) {
flex-wrap: wrap;
gap: 1rem;
}
}
} }
.dmca-badge { .dmca-badge {
@@ -125,27 +118,14 @@
align-items: center; align-items: center;
} }
@media screen and (max-width: 768px) {
.footer-bottom {
flex-wrap: wrap;
gap: 1rem;
}
}
#logo-name { #logo-name {
font-size: 1.4rem; font-size: 1.4rem;
color: var(--text-one); color: var(--text-one);
font-weight: 600; font-weight: 600;
}
#logo-name span { span {
color: var(--primary); color: var(--primary);
} }
.footer-bottom a {
text-decoration: none;
color: var(--text-four);
font-weight: 600;
} }
li { li {
@@ -153,16 +133,12 @@
color: var(--text-four); color: var(--text-four);
font-size: 0.9rem; font-size: 0.9rem;
font-weight: 500; font-weight: 500;
}
li a { a {
color: var(--primary); color: var(--primary);
font-weight: 600; font-weight: 600;
font-size: 0.95rem; font-size: 0.95rem;
} }
path {
stroke: var(--border);
} }
.main-content { .main-content {
@@ -178,42 +154,28 @@
a { a {
text-decoration: none; text-decoration: none;
}
a:hover { &:hover {
text-decoration: underline var(--secondary); text-decoration: underline var(--secondary);
color: var(--text-one); color: var(--text-one);
}
} }
.links-container { .links-container {
display: flex; display: flex;
gap: 10rem; gap: 10rem;
margin-top: 1rem; margin-top: 1rem;
}
@media screen and (max-width: 1050px) { @media (max-width: 1050px) {
.footer-top {
flex-direction: column;
gap: 2rem;
}
.links-container {
display: grid; display: grid;
gap: 2rem; gap: 2rem;
grid-template-columns: repeat(auto-fit, minmax(80px, 1fr)); grid-template-columns: repeat(auto-fit, minmax(80px, 1fr));
} }
}
@media screen and (max-width: 768px) { @media (max-width: 768px) {
.links-container {
display: flex; display: flex;
flex-direction: column; flex-direction: column;
gap: initial; gap: initial;
} }
} }
svg {
padding-left: 15px;
padding-right: 15px;
}
</style> </style>

View File

@@ -2,19 +2,10 @@
export let title: string; export let title: string;
</script> </script>
<div class="desktop-only"> <div>
<span>{title}</span> <span>
<ul> {title}
<slot /> </span>
</ul>
</div>
<div class="mobile-only">
<button class="title">
<span>
{title}
</span>
</button>
<ul> <ul>
<slot /> <slot />
</ul> </ul>
@@ -22,49 +13,23 @@
<style lang="scss"> <style lang="scss">
span { span {
list-style: none; display: flex;
color: var(--text-four); margin: 1.5rem 0;
font-size: 0.9rem; font-size: 0.9rem;
font-weight: 600; font-weight: 600;
color: var(--text-four);
} }
ul { ul {
margin: 1.25rem 0rem;
display: flex; display: flex;
gap: 1rem;
flex-direction: column; flex-direction: column;
width: max-content; margin: 1.25rem 0rem;
gap: 1rem;
} }
.title { @media (max-width: 768px) {
display: flex; div:not(:last-child) {
justify-content: space-between; border-bottom: 1px solid var(--border);
background-color: transparent;
border: none;
width: 100%;
margin: 1.5rem 0;
cursor: pointer;
}
.mobile-only:not(:last-child) {
border-bottom: 1px solid var(--border);
}
.mobile-only {
display: none;
}
.arrow {
transition: all 0.2s var(--bezier-one);
user-select: none;
}
@media screen and (max-width: 768px) {
.mobile-only {
display: block;
}
.desktop-only {
display: none;
} }
} }
</style> </style>

View File

@@ -7,17 +7,17 @@
<Picture data={manager_screenshot} alt="Screenshot of ReVanced Manager" /> <Picture data={manager_screenshot} alt="Screenshot of ReVanced Manager" />
</div> </div>
<style> <style lang="scss">
.hero-img :global(img) {
height: 100%;
border-radius: 1.75rem;
}
.hero-img { .hero-img {
height: max(100vh, 600px); height: max(100vh, 600px);
padding: 0.5rem 0.5rem; padding: 0.5rem 0.5rem;
border-radius: 1.75rem; border-radius: 1.75rem;
background-color: var(--surface-seven); background-color: var(--surface-seven);
user-select: none; user-select: none;
:global(img) {
height: 100%;
border-radius: 1.75rem;
}
} }
</style> </style>

View File

@@ -17,7 +17,7 @@
<p> <p>
Customize your mobile experience through ReVanced <br /> by applying patches to your applications. Customize your mobile experience through ReVanced <br /> by applying patches to your applications.
</p> </p>
<div class="hero-buttons-container"> <div class="buttons-container">
<div class="hero-buttons internal-buttons"> <div class="hero-buttons internal-buttons">
<Button type="filled" icon={TrayArrowDown} href="download">Download</Button> <Button type="filled" icon={TrayArrowDown} href="download">Download</Button>
<Button type="tonal" icon={FileDocumentOutline} href="patches">View patches</Button> <Button type="tonal" icon={FileDocumentOutline} href="patches">View patches</Button>
@@ -34,83 +34,59 @@
</div> </div>
</section> </section>
<style> <style lang="scss">
h1 {
color: var(--text-one);
}
.hero-buttons-container {
display: flex;
flex-direction: column;
gap: 1.5rem;
}
.social-buttons {
max-width: 30rem;
position: absolute;
bottom: 1rem;
transition: opacity 0.1s var(--bezier-one);
}
.hero-buttons {
flex-wrap: wrap;
display: flex;
user-select: none;
gap: 1rem;
}
.hero { .hero {
padding-top: 10vh;
display: flex; display: flex;
flex-direction: column; flex-direction: column;
gap: 1rem; gap: 1rem;
}
span { @media (min-width: 1100px) {
color: var(--primary); padding-top: 10vh;
}
@media screen and (max-width: 1100px) {
.hero {
padding-top: initial;
} }
}
@media screen and (max-width: 450px) { h1 {
.internal-buttons { color: var(--text-one);
}
span {
color: var(--primary);
}
.buttons-container {
display: flex;
flex-direction: column; flex-direction: column;
gap: 1rem; gap: 2.5rem;
}
.social-buttons { .social-buttons {
left: 50%; max-width: 30rem;
transform: translateX(-50%); position: absolute;
justify-content: center; bottom: 1rem;
width: 100%; transition: opacity 0.1s var(--bezier-one);
}
.hero { @media (max-width: 450px) {
height: initial; justify-content: center;
} left: 0;
} }
@media screen and (max-width: 1100px) or (min-height: 780px) { @media (max-height: 600px), (max-width: 450px) and (max-height: 780px) {
.social-buttons { position: static;
transform: translateX(-50%); opacity: 100% !important;
width: 90%; }
position: absolute; }
left: initial;
transform: initial;
}
}
@media screen and (max-height: 780px) { .hero-buttons {
.social-buttons { flex-wrap: wrap;
transform: initial; display: flex;
left: initial; user-select: none;
position: initial; gap: 1rem;
width: initial;
opacity: 100% !important; @media (max-width: 450px) {
&.internal-buttons {
flex-direction: column;
gap: 1rem;
}
}
}
} }
} }
</style> </style>

View File

@@ -1,5 +1,6 @@
<script lang="ts"> <script lang="ts">
import type { Social } from '$lib/types'; import type { Social } from '$lib/types';
export let social: Social; export let social: Social;
</script> </script>
@@ -7,8 +8,11 @@
<img src="socials/{social.name.toLowerCase()}.svg" alt={social.name} /> <img src="socials/{social.name.toLowerCase()}.svg" alt={social.name} />
</a> </a>
<style> <style lang="scss">
a { a {
display: flex;
justify-content: center;
border: 0; border: 0;
width: 60px; width: 60px;
height: 60px; height: 60px;
@@ -19,23 +23,18 @@
background-color: var(--surface-four); background-color: var(--surface-four);
color: var(--text-one); color: var(--text-one);
user-select: none; user-select: none;
}
a { &:hover {
display: flex; transform: translateY(-5%);
justify-content: center; }
}
img { img {
transition: filter 0.4s var(--bezier-one); transition: filter 0.4s var(--bezier-one);
width: 30px; width: 30px;
} }
a:hover { &:hover img {
transform: translateY(-5%); filter: brightness(1.2);
} }
a:hover img {
filter: brightness(1.2);
} }
</style> </style>

View File

@@ -52,17 +52,17 @@
color: var(--primary); color: var(--primary);
} }
} }
&.unclickable { &.unclickable {
pointer-events: none; pointer-events: none;
} }
:hover { :hover {
color: var(--text-one); color: var(--text-one);
background-color: var(--surface-three); background-color: var(--surface-three);
} }
} }
a { a {
text-decoration: none; text-decoration: none;
user-select: none; user-select: none;
@@ -72,15 +72,18 @@
justify-content: center; justify-content: center;
padding: 10px 16px; padding: 10px 16px;
} }
span { span {
display: flex;
justify-content: center;
font-weight: 400; font-weight: 400;
font-size: 0.9rem; font-size: 0.9rem;
letter-spacing: 0.02rem; letter-spacing: 0.02rem;
color: var(--text-four); color: var(--text-four);
} }
@media (max-width: 767px) { @media (max-width: 768px) {
a { a {
padding: 0.75rem 1.25rem; padding: 0.75rem 1.25rem;
justify-content: left; justify-content: left;

View File

@@ -5,9 +5,9 @@
import { expoOut } from 'svelte/easing'; import { expoOut } from 'svelte/easing';
import { createQuery } from '@tanstack/svelte-query'; import { createQuery } from '@tanstack/svelte-query';
import Navigation from './NavButton.svelte'; import Navigation from '$layout/Navbar/NavButton.svelte';
import Query from '$lib/components/Query.svelte'; import Query from '$lib/components/Query.svelte';
import AnnouncementBanner from '../../routes/announcements/AnnouncementBanner.svelte'; import AnnouncementBanner from '$layout/Banners/AnnouncementBanner.svelte';
import Cog from 'svelte-material-icons/Cog.svelte'; import Cog from 'svelte-material-icons/Cog.svelte';
@@ -15,17 +15,17 @@
import RouterEvents from '$data/RouterEvents'; import RouterEvents from '$data/RouterEvents';
import { queries } from '$data/api'; import { queries } from '$data/api';
import StatusBanner from './StatusBanner.svelte'; import StatusBanner from '$layout/Banners/StatusBanner.svelte';
import SettingsModal from './Modals/SettingsModal.svelte'; import SettingsDialog from '$layout/Dialogs/SettingsDialog.svelte';
import LoginModal from './Modals/LoginModal.svelte'; import LoginDialog from '$layout/Dialogs/LoginDialog.svelte';
import LoginSuccessfulModal from './Modals/LoginSuccessfulModal.svelte'; import LoginSuccessfulDialog from '$layout/Dialogs/LoginSuccessfulDialog.svelte';
import SessionExpiredModal from './Modals/SessionExpiredModal.svelte'; import SessionExpiredDialog from '$layout/Dialogs/SessionExpiredDialog.svelte';
const ping = createQuery(queries.ping()); const ping = createQuery(queries.ping());
const statusUrl = status_url(); const statusUrl = status_url();
let menuOpen = false; let menuOpen = false;
const modals: Record<string, boolean> = { const dialogs: Record<string, boolean> = {
settings: false, settings: false,
login: false login: false
}; };
@@ -75,13 +75,15 @@
<Navigation href="/" label="Home">Home</Navigation> <Navigation href="/" label="Home">Home</Navigation>
<Navigation queryKey="manager" href="/download" label="Download">Download</Navigation> <Navigation queryKey="manager" href="/download" label="Download">Download</Navigation>
<Navigation queryKey="patches" href="/patches" label="Patches">Patches</Navigation> <Navigation queryKey="patches" href="/patches" label="Patches">Patches</Navigation>
<Navigation <span class="mobile-only">
queryKey={['announcements', 'announcementTags']} <Navigation
href="/announcements" queryKey={['announcements', 'announcementTags']}
label="Announcements" href="/announcements"
> label="Announcements"
Announcements >
</Navigation> Announcements
</Navigation>
</span>
<Navigation queryKey="contributors" href="/contributors" label="Contributors"> <Navigation queryKey="contributors" href="/contributors" label="Contributors">
Contributors Contributors
</Navigation> </Navigation>
@@ -91,13 +93,32 @@
</ul> </ul>
</div> </div>
<div id="secondary-navigation"> <div id="secondary-navigation">
<button <span class="desktop-only">
on:click={() => (modals.settings = !modals.settings)} <Navigation
class:selected={modals.settings} queryKey={['announcements', 'announcementTags']}
aria-label="Settings" href="/announcements"
label="Announcements"
> >
<Cog size="20px" color={modals.settings ? 'var(--primary)' : 'var(--surface-six)'} /> <svg
</button> xmlns="http://www.w3.org/2000/svg"
height="24px"
viewBox="0 -960 960 960"
width="24px"
fill="currentColor"
>
<path
d="M720-440v-80h160v80H720Zm48 280-128-96 48-64 128 96-48 64Zm-80-480-48-64 128-96 48 64-128 96ZM200-200v-160h-40q-33 0-56.5-23.5T80-440v-80q0-33 23.5-56.5T160-600h160l200-120v480L320-360h-40v160h-80Zm360-146v-268q27 24 43.5 58.5T620-480q0 41-16.5 75.5T560-346Z"
/>
</svg>
</Navigation>
</span>
<button
on:click={() => (dialogs.settings = !dialogs.settings)}
class:selected={dialogs.settings}
aria-label="Settings"
>
<Cog size="20px" color={dialogs.settings ? 'var(--primary)' : 'var(--surface-six)'} />
</button>
</div> </div>
</div> </div>
</div> </div>
@@ -114,18 +135,18 @@
{/if} {/if}
</nav> </nav>
<SettingsModal bind:modalOpen={modals.settings} bind:loginOpen={modals.login} /> <SettingsDialog bind:dialogOpen={dialogs.settings} bind:loginOpen={dialogs.login} />
<LoginModal bind:modalOpen={modals.login} /> <LoginDialog bind:dialogOpen={dialogs.login} />
<LoginSuccessfulModal /> <LoginSuccessfulDialog />
<SessionExpiredModal bind:loginOpen={modals.login} /> <SessionExpiredDialog bind:loginOpen={dialogs.login} />
<style lang="scss"> <style lang="scss">
#secondary-navigation { #secondary-navigation {
display: flex; display: flex;
gap: 1rem;
button { button {
border-radius: 10px; border-radius: 10px;
padding: 10px 16px; padding: 10px 16px;
@@ -213,7 +234,7 @@
width: 100%; width: 100%;
} }
@media (max-width: 767px) { @media (max-width: 768px) {
#nav-wrapper-container { #nav-wrapper-container {
overflow: hidden; overflow: hidden;
position: fixed; position: fixed;
@@ -256,7 +277,7 @@
} }
} }
@media screen and (min-width: 768px) { @media (min-width: 768px) {
.mobile-only { .mobile-only {
display: none !important; display: none !important;
} }

View File

@@ -1,4 +1,6 @@
<script lang="ts"> <script lang="ts">
import ToolTip from './ToolTip.svelte';
export let type: 'filled' | 'tonal' | 'text' | 'outlined' | 'icon' = 'filled'; export let type: 'filled' | 'tonal' | 'text' | 'outlined' | 'icon' = 'filled';
export let variant: 'default' | 'danger' | 'onDangerBackground' = 'default'; export let variant: 'default' | 'danger' | 'onDangerBackground' = 'default';
export let functionType: typeof HTMLButtonElement.prototype.type = 'button'; export let functionType: typeof HTMLButtonElement.prototype.type = 'button';
@@ -9,28 +11,31 @@
export let target: string = ''; export let target: string = '';
export let label: string = ''; export let label: string = '';
export let disabled: boolean = false; export let disabled: boolean = false;
export let toolTipText: string | undefined = undefined;
$: type = $$slots.default ? type : 'icon'; $: type = $$slots.default ? type : 'icon';
</script> </script>
{#if href} <ToolTip content={toolTipText} html={false}>
<a {href} {target} aria-label={label} class={`${type} ${variant}`} class:disabled> {#if href}
<svelte:component this={icon} size={iconSize} color={iconColor} /> <a {href} {target} aria-label={label} class={`${type} ${variant}`} class:disabled>
<slot /> <svelte:component this={icon} size={iconSize} color={iconColor} />
</a> <slot />
{:else} </a>
<button {:else}
on:click <button
class={`${type} ${variant}`} on:click
class:disabled class={`${type} ${variant}`}
aria-label={label} class:disabled
type={functionType} aria-label={label}
{disabled} type={functionType}
> {disabled}
<svelte:component this={icon} size={iconSize} color={iconColor} /> >
<slot /> <svelte:component this={icon} size={iconSize} color={iconColor} />
</button> <slot />
{/if} </button>
{/if}
</ToolTip>
<style lang="scss"> <style lang="scss">
a, a,

View File

@@ -1,4 +1,15 @@
<svg aria-hidden="true" width="100%" height="8" fill="none" xmlns="http://www.w3.org/2000/svg"> <script>
export let horizontalPadding;
</script>
<svg
style:padding-inline={horizontalPadding}
aria-hidden="true"
width="100%"
height="8"
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
<pattern id="a" width="91" height="8" patternUnits="userSpaceOnUse"> <pattern id="a" width="91" height="8" patternUnits="userSpaceOnUse">
<path <path
d="M114 4c-5.067 4.667-10.133 4.667-15.2 0S88.667-.667 83.6 4 73.467 8.667 68.4 4 58.267-.667 53.2 4 43.067 8.667 38 4 27.867-.667 22.8 4 12.667 8.667 7.6 4-2.533-.667-7.6 4s-10.133 4.667-15.2 0S-32.933-.667-38 4s-10.133 4.667-15.2 0-10.133-4.667-15.2 0-10.133 4.667-15.2 0-10.133-4.667-15.2 0-10.133 4.667-15.2 0-10.133-4.667-15.2 0-10.133 4.667-15.2 0-10.133-4.667-15.2 0-10.133 4.667-15.2 0-10.133-4.667-15.2 0-10.133 4.667-15.2 0-10.133-4.667-15.2 0-10.133 4.667-15.2 0-10.133-4.667-15.2 0-10.133 4.667-15.2 0-10.133-4.667-15.2 0-10.133 4.667-15.2 0-10.133-4.667-15.2 0-10.133 4.667-15.2 0-10.133-4.667-15.2 0-10.133 4.667-15.2 0-10.133-4.667-15.2 0-10.133 4.667-15.2 0-10.133-4.667-15.2 0-10.133 4.667-15.2 0" d="M114 4c-5.067 4.667-10.133 4.667-15.2 0S88.667-.667 83.6 4 73.467 8.667 68.4 4 58.267-.667 53.2 4 43.067 8.667 38 4 27.867-.667 22.8 4 12.667 8.667 7.6 4-2.533-.667-7.6 4s-10.133 4.667-15.2 0S-32.933-.667-38 4s-10.133 4.667-15.2 0-10.133-4.667-15.2 0-10.133 4.667-15.2 0-10.133-4.667-15.2 0-10.133 4.667-15.2 0-10.133-4.667-15.2 0-10.133 4.667-15.2 0-10.133-4.667-15.2 0-10.133 4.667-15.2 0-10.133-4.667-15.2 0-10.133 4.667-15.2 0-10.133-4.667-15.2 0-10.133 4.667-15.2 0-10.133-4.667-15.2 0-10.133 4.667-15.2 0-10.133-4.667-15.2 0-10.133 4.667-15.2 0-10.133-4.667-15.2 0-10.133 4.667-15.2 0-10.133-4.667-15.2 0-10.133 4.667-15.2 0-10.133-4.667-15.2 0-10.133 4.667-15.2 0-10.133-4.667-15.2 0-10.133 4.667-15.2 0"

Before

Width:  |  Height:  |  Size: 1.1 KiB

After

Width:  |  Height:  |  Size: 1.2 KiB

View File

@@ -17,7 +17,7 @@
{/if} {/if}
</button> </button>
<style> <style lang="scss">
button { button {
font-family: var(--font-two); font-family: var(--font-two);
border: none; border: none;
@@ -32,18 +32,11 @@
display: flex; display: flex;
align-items: center; align-items: center;
gap: 8px; gap: 8px;
&.selected {
background-color: var(--tertiary);
color: var(--primary);
}
} }
.selected {
background-color: var(--tertiary);
color: var(--primary);
}
#dropdown {
margin-right: -6px;
}
#check {
margin-left: -6px;
}
</style> </style>

View File

@@ -1,5 +1,5 @@
<script lang="ts"> <script lang="ts">
import ImageModal from '$lib/components/ImageModal.svelte'; import ImageDialog from '$layout/Dialogs/ImageDialog.svelte';
export let images: string[]; export let images: string[];
export let columns: number = 3; export let columns: number = 3;
@@ -7,14 +7,14 @@
let selectedImage: { src: string; alt: string } | null = null; let selectedImage: { src: string; alt: string } | null = null;
function openModal(image: string, index: number) { function openDialog(image: string, index: number) {
selectedImage = { selectedImage = {
src: image, src: image,
alt: `Gallery image ${index + 1}` alt: `Gallery image ${index + 1}`
}; };
} }
function closeModal() { function closeDialog() {
selectedImage = null; selectedImage = null;
} }
</script> </script>
@@ -28,8 +28,8 @@
src={image} src={image}
alt={`Gallery image ${i + 1}`} alt={`Gallery image ${i + 1}`}
loading="lazy" loading="lazy"
on:click={() => openModal(image, i)} on:click={() => openDialog(image, i)}
on:keydown={(e) => e.key === 'Enter' && openModal(image, i)} on:keydown={(e) => e.key === 'Enter' && openDialog(image, i)}
tabindex="0" tabindex="0"
/> />
</div> </div>
@@ -37,7 +37,7 @@
</div> </div>
{#if selectedImage} {#if selectedImage}
<ImageModal src={selectedImage.src} alt={selectedImage.alt} on:close={closeModal} /> <ImageDialog src={selectedImage.src} alt={selectedImage.alt} on:close={closeDialog} />
{/if} {/if}
<style> <style>

View File

@@ -1,6 +1,6 @@
<script lang="ts"> <script lang="ts">
import { slide, fade } from 'svelte/transition'; import { backIn, expoOut } from 'svelte/easing';
import { expoOut } from 'svelte/easing'; import { slide } from 'svelte/transition';
export let open = false; export let open = false;
export let dismissTime = 3000; export let dismissTime = 3000;
@@ -13,47 +13,32 @@
</script> </script>
{#if open} {#if open}
<div <div id="snackbar" in:slide={{ duration: 400, easing: expoOut }} out:slide={{ duration: 300, easing: backIn }}>
class="snackbar" <slot name="text" />
in:slide={{ duration: 400, easing: expoOut }}
out:fade={{ duration: 300, easing: expoOut }}
>
<div class="text">
<slot name="text" />
</div>
</div> </div>
{/if} {/if}
<style lang="scss"> <style>
.snackbar { #snackbar {
display: flex; display: flex;
box-sizing: border-box;
justify-content: space-between;
padding-top: 0.25rem;
padding-bottom: 0.25rem;
padding-left: 1rem;
padding-right: 0.5rem;
height: 3rem;
gap: 1.5rem;
align-items: center; align-items: center;
position: fixed;
bottom: 2rem;
left: 2rem;
padding: 0.5rem 1rem;
border-radius: 0.25rem; border-radius: 0.25rem;
min-width: 12.5rem; min-width: 12.5rem;
max-width: 35rem; max-width: 35rem;
position: fixed; height: 3rem;
margin-left: 2.25rem;
margin-right: 2.25rem;
z-index: 8;
left: 0;
right: 0;
bottom: 2rem;
background-color: var(--surface-one);
transition: all 0.4s var(--bezier-one);
box-shadow: var(--drop-shadow-one);
}
.text { background: var(--surface-one);
color: var(--text-two); color: var(--text-two);
font-weight: 500; box-shadow: var(--drop-shadow-one);
font-size: 14px; font-size: 14px;
font-weight: 500;
z-index: 10;
} }
</style> </style>

View File

@@ -1,30 +1,30 @@
<div class="spinner" /> <div id="spinner" />
<style> <style lang="scss">
@keyframes spinner { @keyframes spinner {
to { to {
transform: rotate(360deg); transform: rotate(360deg);
} }
} }
.spinner { #spinner {
position: fixed; position: fixed;
top: 50%; top: 50%;
left: 50%; left: 50%;
width: 50px; width: 50px;
height: 50px; height: 50px;
transform: translate(-50%, -50%); transform: translate(-50%, -50%);
}
.spinner:before { &:before {
content: ''; content: '';
box-sizing: border-box; box-sizing: border-box;
position: absolute; position: absolute;
width: 100%; width: 100%;
height: 100%; height: 100%;
border-radius: 50%; border-radius: 50%;
border: 4.5px solid transparent; border: 4.5px solid transparent;
border-top-color: var(--primary); border-top-color: var(--primary);
animation: spinner 0.6s linear infinite; animation: spinner 0.6s linear infinite;
}
} }
</style> </style>

View File

@@ -2,18 +2,22 @@
import { tooltip } from 'svooltip'; import { tooltip } from 'svooltip';
import '../styles/ToolTip.scss'; import '../styles/ToolTip.scss';
export let content: string; export let content: string | undefined;
export let html: boolean = false; export let html: boolean = false;
</script> </script>
<div {#if content}
use:tooltip={{ <div
content: content, use:tooltip={{
html: html content: content,
}} html: html
> }}
>
<slot />
</div>
{:else}
<slot /> <slot />
</div> {/if}
<style> <style>
:root { :root {

View File

@@ -7,7 +7,7 @@
viewBox="0 0 1440 500" viewBox="0 0 1440 500"
xmlns="http://www.w3.org/2000/svg" xmlns="http://www.w3.org/2000/svg"
preserveAspectRatio="none" preserveAspectRatio="none"
style="opacity: {visibility ? '100%' : '0'}" style="opacity: {visibility ? '100%' : '0'}; height: {visibility ? '40vh' : '0px'}"
> >
<path class="wave" /> <path class="wave" />
</svg> </svg>
@@ -19,10 +19,9 @@
bottom: -1px; bottom: -1px;
z-index: -1; z-index: -1;
width: 100%; width: 100%;
height: 40vh;
} }
@media screen and (max-height: 780px) { @media (max-height: 780px) {
svg { svg {
opacity: 0 !important; opacity: 0 !important;
} }

View File

@@ -63,4 +63,12 @@ export const read_announcements = writable<Set<number>>(new Set(), (set) => {
}; };
}); });
read_announcements.subscribe((value) => {
if (!browser) return;
localStorage.setItem('read_announcements', JSON.stringify(Array.from(value)));
});
export const passed_login_with_creds = writable(false); // will only change when the user INPUTS the credentials, not if the session is just valid export const passed_login_with_creds = writable(false); // will only change when the user INPUTS the credentials, not if the session is just valid
export const allowAnalytics = writable(false);

View File

@@ -18,13 +18,11 @@
import NavHost from '$layout/Navbar/NavHost.svelte'; import NavHost from '$layout/Navbar/NavHost.svelte';
import Spinner from '$lib/components/Spinner.svelte'; import Spinner from '$lib/components/Spinner.svelte';
import Dialogue from '$lib/components/Dialogue.svelte'; import ConsentDialog from '$layout/Dialogs/ConsentDialog.svelte';
import Button from '$lib/components/Button.svelte';
import { staleTime } from '$data/api'; import { staleTime } from '$data/api';
import RouterEvents from '$data/RouterEvents'; import RouterEvents from '$data/RouterEvents';
import { events as themeEvents } from '$util/themeEvents'; import { events as themeEvents } from '$util/themeEvents';
import { RV_GOOGLE_TAG_MANAGER_ID } from '$env/static/public';
import FooterHost from '$layout/Footer/FooterHost.svelte'; import FooterHost from '$layout/Footer/FooterHost.svelte';
const queryClient = new QueryClient({ const queryClient = new QueryClient({
@@ -36,50 +34,6 @@
} }
}); });
let showConsentModal = false;
let allowAnalytics = false;
function enableAnalytics() {
window.dataLayer = window.dataLayer || [];
function gtag() {
dataLayer.push(arguments);
}
gtag('js', new Date());
gtag('config', RV_GOOGLE_TAG_MANAGER_ID);
var s = document.createElement('script');
s.src = `https://www.googletagmanager.com/gtm.js?id=${RV_GOOGLE_TAG_MANAGER_ID}`;
document.head.append(s);
}
function rememberChoice(allow: boolean) {
localStorage.setItem('analytics', allow.toString());
showConsentModal = false;
allowAnalytics = allow;
if (allowAnalytics) enableAnalytics();
}
onMount(() => {
// Check if the user has already decided
const hasDecided = localStorage.getItem('analytics') !== null;
if (hasDecided) {
allowAnalytics = localStorage.getItem('analytics') === 'true';
if (allowAnalytics) enableAnalytics();
} else {
showConsentModal = true;
}
new DateTriggerEventHandler(themeEvents);
isRestoring.set(true);
const [unsubscribe, promise] = persistQueryClient({
queryClient,
persister: createSyncStoragePersister({ storage: localStorage })
});
promise.then(() => isRestoring.set(false));
return unsubscribe;
});
// Just like the set/clearInterval example found here: https://svelte.dev/docs#run-time-svelte-store-derived // Just like the set/clearInterval example found here: https://svelte.dev/docs#run-time-svelte-store-derived
const show_loading_animation = derived( const show_loading_animation = derived(
RouterEvents, RouterEvents,
@@ -94,32 +48,22 @@
}, },
false false
); );
onMount(() => {
new DateTriggerEventHandler(themeEvents);
isRestoring.set(true);
const [unsubscribe, promise] = persistQueryClient({
queryClient,
persister: createSyncStoragePersister({ storage: localStorage })
});
promise.then(() => isRestoring.set(false));
return unsubscribe;
});
</script> </script>
{#if allowAnalytics} <ConsentDialog />
<!-- Google Tag Manager (noscript) -->
<noscript>
<!-- svelte-ignore a11y-missing-attribute -->
<iframe
src="https://www.googletagmanager.com/ns.html?id={RV_GOOGLE_TAG_MANAGER_ID}"
height="0"
width="0"
style="display: none; visibility: hidden"
></iframe>
</noscript>
{/if}
<Dialogue bind:modalOpen={showConsentModal} notDismissible>
<svelte:fragment slot="title">It's your choice</svelte:fragment>
<svelte:fragment slot="description">
We use analytics to improve your experience on this site. By clicking "Allow", you allow us to
collect anonymous data about your visit.
</svelte:fragment>
<svelte:fragment slot="buttons">
<Button type="text" on:click={() => rememberChoice(false)}>Deny</Button>
<Button type="filled" on:click={() => rememberChoice(true)}>Allow</Button>
</svelte:fragment>
</Dialogue>
<QueryClientProvider client={queryClient}> <QueryClientProvider client={queryClient}>
<NavHost /> <NavHost />
<div id="skiptab"> <div id="skiptab">

View File

@@ -7,20 +7,13 @@
let bottomVisibility = true; let bottomVisibility = true;
const checkVisibility = () => {
const wave = document.querySelector('.wave');
bottomVisibility = !(wave && wave.getBoundingClientRect().bottom < window.innerHeight - 1);
};
onMount(() => { onMount(() => {
const checkVisibility = () => {
const wave = document.querySelector('.wave');
bottomVisibility = !(wave && wave.getBoundingClientRect().bottom < window.innerHeight - 1);
};
window.addEventListener('scroll', checkVisibility, { passive: true });
window.addEventListener('resize', checkVisibility);
checkVisibility(); // Initial check checkVisibility(); // Initial check
return () => {
window.removeEventListener('scroll', checkVisibility);
window.removeEventListener('resize', checkVisibility);
};
}); });
</script> </script>
@@ -135,7 +128,9 @@
]} ]}
/> />
<main> <svelte:window on:scroll={checkVisibility} on:resize={checkVisibility} />
<main class:visibility={!bottomVisibility}>
<div class="content"> <div class="content">
<Home socialsVisibility={bottomVisibility} /> <Home socialsVisibility={bottomVisibility} />
<div class="hero-img-container"> <div class="hero-img-container">
@@ -160,21 +155,25 @@
display: flex; display: flex;
flex-direction: column; flex-direction: column;
align-items: center; align-items: center;
@media (max-height: 600px), (max-width: 450px) and (max-height: 780px) {
min-height: initial;
}
@media (max-width: 335px) {
padding: 2rem 0 !important;
}
&.visibility {
min-height: initial;
}
} }
.hero-img-container { .hero-img-container {
z-index: 0; z-index: 0;
}
@media screen and (max-width: 1100px) { @media (max-width: 1100px) {
.hero-img-container {
display: none; display: none;
} }
} }
@media screen and (max-width: 335px) {
main {
padding: 2rem 0 !important;
}
}
</style> </style>

View File

@@ -13,7 +13,7 @@
import Search from '$lib/components/Search.svelte'; import Search from '$lib/components/Search.svelte';
import { onMount } from 'svelte'; import { onMount } from 'svelte';
import type { ResponseAnnouncement } from '$lib/types'; import type { ResponseAnnouncement } from '$lib/types';
import { admin_login } from '$lib/stores'; import { admin_login, read_announcements } from '$lib/stores';
import Button from '$lib/components/Button.svelte'; import Button from '$lib/components/Button.svelte';
import moment from 'moment'; import moment from 'moment';
import { debounce } from '$util/debounce'; import { debounce } from '$util/debounce';
@@ -22,52 +22,61 @@
import ChevronDown from 'svelte-material-icons/ChevronDown.svelte'; import ChevronDown from 'svelte-material-icons/ChevronDown.svelte';
import Create from 'svelte-material-icons/Plus.svelte'; import Create from 'svelte-material-icons/Plus.svelte';
let searchParams: Readable<URLSearchParams>; let expanded = false;
if (building) searchParams = readable(new URLSearchParams()); const searchParams: Readable<URLSearchParams> = building
else searchParams = derived(page, ($page) => $page.url.searchParams); ? readable(new URLSearchParams())
: derived(page, ($page) => $page.url.searchParams);
let searchTerm = $searchParams.get('s') || ''; let searchTerm = $searchParams.get('s') || '';
let displayedTerm = '';
$: query = createQuery(queries.announcements()); $: query = createQuery(queries.announcements());
$: tagsQuery = createQuery(queries.announcementTags()); $: tagsQuery = createQuery(queries.announcementTags());
$: selectedTags = $searchParams.getAll('tag'); $: selectedTags = $searchParams.getAll('tag');
let expanded = false;
function filterAnnouncements(
announcements: Iterable<ResponseAnnouncement>,
search: string,
selectedTags: string[]
): ResponseAnnouncement[] {
const announcementFilter = createFilter(Array.from(announcements), {
searcherOptions: {
keys: ['title', 'content']
},
additionalFilter: (announcement: ResponseAnnouncement, tags: string[]): boolean => {
return (
tags.length === 0 ||
tags.some((tag) => announcement.tags && announcement.tags.includes(tag))
);
}
});
return announcementFilter(selectedTags, search);
}
// Make sure we don't have to filter the announcements after every key press
let displayedTerm = '';
const update = () => { const update = () => {
displayedTerm = searchTerm; displayedTerm = searchTerm;
const url = new URL(window.location.href); const url = new URL(window.location.href);
url.pathname = '/announcements'; url.pathname = '/announcements';
if (searchTerm) url.searchParams.set('s', searchTerm); searchTerm ? url.searchParams.set('s', searchTerm) : url.searchParams.delete('s');
else url.searchParams.delete('s');
}; };
onMount(update); const archivedAnnouncements = (announcements: ResponseAnnouncement[]) =>
announcements.filter((a) => a.archived_at && moment(a.archived_at).isBefore(moment()));
const activeAnnouncements = (announcements: ResponseAnnouncement[]) =>
announcements.filter((a) => !a.archived_at || moment(a.archived_at).isAfter(moment()));
const filterAnnouncements = (
announcements: ResponseAnnouncement[],
search: string,
tags: string[]
): ResponseAnnouncement[] => {
const announcementFilter = createFilter(announcements, {
searcherOptions: { keys: ['title', 'content'] },
additionalFilter: (a: ResponseAnnouncement, tags: string[]) =>
tags.length === 0 || tags.some((tag) => a.tags?.includes(tag))
});
return announcementFilter(tags, search);
};
onMount(() => {
debounce(update)();
if ($read_announcements.size === 0) {
query.subscribe((data) => {
read_announcements.update((set) => {
const updated = new Set(set);
data.data?.announcements.forEach((a) => updated.add(a.id));
return updated;
});
});
}
});
</script> </script>
<div class="search"> <div class="search">
@@ -92,60 +101,61 @@
</Query> </Query>
<Query {query} let:data> <Query {query} let:data>
<div class="cards"> {#if activeAnnouncements(filterAnnouncements(data.announcements, displayedTerm, selectedTags)).length}
{#each filterAnnouncements(data.announcements, displayedTerm, selectedTags) as announcement} <div class="cards">
{#if !announcement.archived_at || moment(announcement.archived_at).isAfter(moment())} {#each activeAnnouncements(filterAnnouncements(data.announcements, displayedTerm, selectedTags)) as announcement}
{#key selectedTags || displayedTerm} <div in:fly={{ y: 10, easing: quintOut, duration: 750 }}>
<div in:fly={{ y: 10, easing: quintOut, duration: 750 }}> <AnnouncementCard {announcement} />
<AnnouncementCard {announcement} /> </div>
</div>
{/key}
{/if}
{/each}
</div>
<div
role="button"
class="expand-archived"
aria-expanded={expanded}
class:closed={!expanded}
on:click={() => (expanded = !expanded)}
on:keypress={() => (expanded = !expanded)}
tabindex="0"
>
<h4>Archived announcements</h4>
<div id="arrow" style:transform={expanded ? 'rotate(0deg)' : 'rotate(-180deg)'}>
<ChevronDown size="24px" color="var(--surface-six)" />
</div>
</div>
{#if expanded}
<div
class="cards"
in:slide={{ easing: quintIn, duration: 250 }}
out:slide={{ easing: quintOut, duration: 250 }}
>
{#each filterAnnouncements(data.announcements, displayedTerm, selectedTags) as announcement}
{#if announcement.archived_at && moment(announcement.archived_at).isBefore(moment())}
{#key selectedTags || displayedTerm}
<AnnouncementCard {announcement} />
{/key}
{/if}
{/each} {/each}
</div> </div>
{/if} {/if}
{#if archivedAnnouncements(filterAnnouncements(data.announcements, displayedTerm, selectedTags)).length}
<div
role="button"
class="expand-archived"
aria-expanded={expanded}
on:click={() => (expanded = !expanded)}
on:keypress={() => (expanded = !expanded)}
tabindex="0"
>
<h4>Archive</h4>
<div id="arrow" style:transform={expanded ? 'rotate(-180deg)' : 'rotate(0deg)'}>
<ChevronDown size="24px" color="var(--surface-six)" />
</div>
</div>
{#if expanded}
<div
class="cards"
in:slide={{ easing: quintIn, duration: 250 }}
out:slide={{ easing: quintOut, duration: 250 }}
>
{#each archivedAnnouncements(filterAnnouncements(data.announcements, displayedTerm, selectedTags)) as announcement}
<AnnouncementCard {announcement} />
{/each}
</div>
{/if}
{/if}
</Query> </Query>
</main> </main>
<style lang="scss"> <style lang="scss">
main {
display: flex;
flex-direction: column;
gap: 1rem;
}
.expand-archived { .expand-archived {
display: flex; display: flex;
align-items: center; align-items: center;
justify-content: space-between; justify-content: space-between;
cursor: pointer; cursor: pointer;
user-select: none; user-select: none;
padding: 0rem 0.25rem; padding-inline: 0.25rem;
#arrow { #arrow {
height: 1.5rem; height: 1.5rem;
@@ -174,14 +184,10 @@
.cards { .cards {
display: grid; display: grid;
grid-template-columns: repeat(3, 1fr); grid-template-columns: repeat(3, 1fr);
padding: 16px 0;
min-width: 0;
width: 100%; width: 100%;
gap: 16px; gap: 16px;
}
@media (max-width: 767px) { @media (max-width: 768px) {
.cards {
display: flex; display: flex;
flex-direction: column; flex-direction: column;
} }

View File

@@ -1,65 +0,0 @@
<script lang="ts">
import { read_announcements } from '$lib/stores';
import Banner from '$lib/components/Banner.svelte';
import { goto } from '$app/navigation';
import type { ResponseAnnouncement } from '$lib/types';
import { browser } from '$app/environment';
import { createQuery } from '@tanstack/svelte-query';
import { queries } from '$data/api';
import moment from 'moment';
let latestUnreadAnnouncement: ResponseAnnouncement | undefined = undefined;
const query = createQuery(queries.announcements());
$: {
if ($query.data?.announcements && $query.data.announcements.length > 0) {
const nonArchived = $query.data.announcements.filter(
(a) => !a.archived_at || moment(a.archived_at).isAfter(moment())
);
const announcement = nonArchived[0];
if (announcement && !$read_announcements.has(announcement.id)) {
latestUnreadAnnouncement = announcement;
} else {
latestUnreadAnnouncement = undefined;
}
}
}
function setAsRead() {
if (!latestUnreadAnnouncement) return;
$read_announcements.add(latestUnreadAnnouncement.id);
localStorage.setItem('read_announcements', JSON.stringify(Array.from($read_announcements)));
latestUnreadAnnouncement = undefined;
}
function handleClick() {
if (latestUnreadAnnouncement) {
goto(`/announcements/${latestUnreadAnnouncement.id}`);
setAsRead();
}
}
function handleClose() {
if (latestUnreadAnnouncement && browser) {
setAsRead();
}
}
function getBannerLevel(level: number | undefined): 'info' | 'caution' {
if (!level || level == 0) return 'info';
return 'caution';
}
</script>
{#if latestUnreadAnnouncement}
<Banner
title={'We have an announcement'}
description={`You can read more about "${latestUnreadAnnouncement.title}" in our latest post.`}
level={getBannerLevel(latestUnreadAnnouncement.level)}
buttonText="Read more"
buttonOnClick={handleClick}
onDismiss={handleClose}
/>
{/if}

View File

@@ -18,7 +18,7 @@
const client = useQueryClient(); const client = useQueryClient();
let isRead: boolean; $: isRead = $read_announcements.has(announcement.id);
function prefetch() { function prefetch() {
const query = queries['announcementById'](announcement.id); const query = queries['announcementById'](announcement.id);
@@ -27,10 +27,11 @@
} }
function setAnnouncementRead() { function setAnnouncementRead() {
isRead = true; read_announcements.update((set) => {
const updated = new Set(set);
$read_announcements.add(announcement.id); updated.add(announcement.id);
localStorage.setItem('read_announcements', JSON.stringify(Array.from($read_announcements))); return updated;
});
} }
function generateSlug(title: string) { function generateSlug(title: string) {
@@ -39,10 +40,6 @@
.replace(/[^a-z0-9]+/g, '-') .replace(/[^a-z0-9]+/g, '-')
.replace(/^-+|-+$/g, ''); .replace(/^-+|-+$/g, '');
} }
onMount(() => {
isRead = $read_announcements.has(announcement.id);
});
</script> </script>
<!-- svelte-ignore a11y-no-static-element-interactions --> <!-- svelte-ignore a11y-no-static-element-interactions -->
@@ -88,11 +85,7 @@
{/if} {/if}
{#if announcement.tags && announcement.tags.length > 0} {#if announcement.tags && announcement.tags.length > 0}
<hr /> <hr />
<TagsHost <TagsHost tags={announcement.tags.map((tag) => ({ name: tag }))} clickable={false} />
tags={announcement.tags.map((tag) => ({ name: tag }))}
expandable={false}
clickable={false}
/>
{/if} {/if}
</div> </div>
</div> </div>

View File

@@ -26,7 +26,7 @@
gap: 8px; gap: 8px;
height: 32px; height: 32px;
padding: 0 16px; padding: 0 12px;
border-radius: 8px; border-radius: 8px;
border: none; border: none;

View File

@@ -50,7 +50,7 @@
{/each} {/each}
{#if expandable && tags.length > 1} {#if expandable && tags.length > 1}
<li class="button"> <li>
<Button type="text" on:click={() => (showAllTags = !showAllTags)}> <Button type="text" on:click={() => (showAllTags = !showAllTags)}>
<div <div
class="expand-arrow" class="expand-arrow"
@@ -67,9 +67,11 @@
div { div {
display: flex; display: flex;
align-items: center; align-items: center;
flex-wrap: wrap;
white-space: nowrap;
gap: 4px; gap: 4px;
.button { li {
display: flex; display: flex;
align-items: center; align-items: center;
} }
@@ -79,9 +81,5 @@
user-select: none; user-select: none;
height: 1.5rem; height: 1.5rem;
} }
.rotate .expand-arrow {
transform: rotate(180deg);
}
} }
</style> </style>

View File

@@ -1,5 +1,4 @@
<script lang="ts"> <script lang="ts">
import Footer from '$layout/Footer/FooterHost.svelte';
import { fly } from 'svelte/transition'; import { fly } from 'svelte/transition';
import { quintOut } from 'svelte/easing'; import { quintOut } from 'svelte/easing';
import { createQuery } from '@tanstack/svelte-query'; import { createQuery } from '@tanstack/svelte-query';
@@ -46,6 +45,4 @@
{:else} {:else}
<Announcement {isCreating} {announcement} {announcementIdNumber} /> <Announcement {isCreating} {announcement} {announcementIdNumber} />
{/if} {/if}
</main> </main>
<Footer />

View File

@@ -3,7 +3,7 @@
import { admin, queries } from '$data/api'; import { admin, queries } from '$data/api';
import { goto } from '$app/navigation'; import { goto } from '$app/navigation';
import Button from '$lib/components/Button.svelte'; import Button from '$lib/components/Button.svelte';
import Dialogue from '$lib/components/Dialogue.svelte'; import Dialog from '$layout/Dialogs/Dialog.svelte';
import type { Announcement, ResponseAnnouncement } from '$lib/types'; import type { Announcement, ResponseAnnouncement } from '$lib/types';
import moment from 'moment'; import moment from 'moment';
import { isValidUrl } from '$util/isValidUrl'; import { isValidUrl } from '$util/isValidUrl';
@@ -97,29 +97,34 @@
<svelte:window on:beforeunload={handleUnload} /> <svelte:window on:beforeunload={handleUnload} />
<Dialogue bind:modalOpen={showDeleteConfirm}> <Dialog bind:dialogOpen={showDeleteConfirm}>
<svelte:fragment slot="title">Confirm?</svelte:fragment> <svelte:fragment slot="title">Confirm?</svelte:fragment>
<svelte:fragment slot="description">Do you want to delete this announcement?</svelte:fragment> <svelte:fragment slot="description">Do you want to delete this announcement?</svelte:fragment>
<svelte:fragment slot="buttons"> <svelte:fragment slot="buttons">
<Button type="text" on:click={() => (showDeleteConfirm = !showDeleteConfirm)}>Cancel</Button> <Button type="text" on:click={() => (showDeleteConfirm = !showDeleteConfirm)}>Cancel</Button>
<Button type="filled" on:click={deleteAnnouncement}>OK</Button> <Button type="filled" on:click={deleteAnnouncement}>OK</Button>
</svelte:fragment> </svelte:fragment>
</Dialogue> </Dialog>
<div> <div>
{#if isEditing || isCreating} {#if isEditing || isCreating}
<Button <Button
toolTipText={isPreviewing ? 'Hide preview' : 'Show preview'}
icon={isPreviewing ? Hide : Show} icon={isPreviewing ? Hide : Show}
iconColor="var(--secondary)" iconColor="var(--secondary)"
on:click={() => (isPreviewing = !isPreviewing)} on:click={() => (isPreviewing = !isPreviewing)}
/> />
<Button <Button
toolTipText={archivedAtInput ? 'Disable archive field' : 'Enable archive field'}
icon={archivedAtInput ? Unarchive : Archive} icon={archivedAtInput ? Unarchive : Archive}
iconColor="var(--secondary)" iconColor="var(--secondary)"
on:click={toggleArchived} on:click={toggleArchived}
/> />
{#if isEditing} {#if isEditing}
<Button <Button
toolTipText="Cancel editing"
icon={Close} icon={Close}
iconColor="var(--secondary)" iconColor="var(--secondary)"
on:click={() => { on:click={() => {
@@ -128,18 +133,27 @@
}} }}
/> />
{/if} {/if}
<Button <Button
toolTipText={isEditing ? 'Save changes' : 'Create announcement'}
icon={Check} icon={Check}
iconColor="var(--secondary)" iconColor="var(--secondary)"
on:click={isEditing ? save : createAnnouncement} on:click={isEditing ? save : createAnnouncement}
/> />
{:else} {:else}
<Button <Button
toolTipText="Delete announcement"
icon={Delete} icon={Delete}
iconColor="var(--secondary)" iconColor="var(--secondary)"
on:click={() => (showDeleteConfirm = !showDeleteConfirm)} on:click={() => (showDeleteConfirm = !showDeleteConfirm)}
/> />
<Button icon={Edit} iconColor="var(--secondary)" on:click={() => (isEditing = !isEditing)} />
<Button
toolTipText="Edit announcement"
icon={Edit}
iconColor="var(--secondary)"
on:click={() => (isEditing = !isEditing)}
/>
{/if} {/if}
</div> </div>

View File

@@ -111,7 +111,7 @@
gap: 2rem; gap: 2rem;
} }
@media (max-width: 767px) { @media (max-width: 768px) {
.card { .card {
background-color: initial; background-color: initial;
padding: 0; padding: 0;

View File

@@ -18,7 +18,11 @@
} }
$: displayCreatedAt = isPreviewing ? createdAtInput : createdAt; $: displayCreatedAt = isPreviewing ? createdAtInput : createdAt;
$: displayArchivedAt = isPreviewing ? archivedAtInput : archivedAt;
$: displayArchivedAt = (() => {
const date = isPreviewing ? archivedAtInput : archivedAt;
return date && moment(date).isBefore() ? date : null;
})();
</script> </script>
{#if (isEditing || isCreating) && !isPreviewing} {#if (isEditing || isCreating) && !isPreviewing}

View File

@@ -37,36 +37,38 @@
]} ]}
/> />
<main> <div class="wrapper">
<div class="wrapper"> <div class="text-container" in:fly={{ y: 10, easing: quintOut, duration: 750 }}>
<div class="text-container" in:fly={{ y: 10, easing: quintOut, duration: 750 }}> <h2>Made possible by the community.</h2>
<h2>Made possible by the community.</h2> <p>
<p> Want to show up here? <span>
Want to show up here? <span <a href="https://github.com/revanced" target="_blank" rel="noreferrer"
><a href="https://github.com/revanced" target="_blank" rel="noreferrer" >Become a contributor
>Become a contributor</a </a>
></span </span>
> </p>
</p>
</div>
<div class="repos">
<Query {query} let:data>
{#each data.contributables as { name, url, contributors }}
<div in:fly={{ y: 10, easing: quintOut, duration: 750 }}>
<ContributorHost {name} {url} {contributors} />
</div>
{/each}
</Query>
</div>
</div> </div>
</main>
<div class="repos">
<Query {query} let:data>
{#each data.contributables as { name, url, contributors }}
<div in:fly={{ y: 10, easing: quintOut, duration: 750 }}>
<ContributorHost {name} {url} {contributors} />
</div>
{/each}
</Query>
</div>
</div>
<style> <style>
.wrapper {
margin-bottom: 5rem;
}
.repos { .repos {
display: flex; display: flex;
flex-direction: column; flex-direction: column;
gap: 2rem; gap: 2rem;
margin-bottom: 4rem;
} }
h2 { h2 {
@@ -109,7 +111,7 @@
a:hover::after { a:hover::after {
transform: translateX(5px); transform: translateX(5px);
} }
@media screen and (max-width: 767px) { @media (max-width: 768px) {
.text-container { .text-container {
padding: 2rem 1.75rem; padding: 2rem 1.75rem;
} }

View File

@@ -10,7 +10,7 @@
<h5>{name}</h5> <h5>{name}</h5>
</a> </a>
<style> <style lang="scss">
a { a {
color: var(--text-one); color: var(--text-one);
text-decoration: none; text-decoration: none;
@@ -23,12 +23,12 @@
align-items: center; align-items: center;
border-right: 1px solid var(--border); border-right: 1px solid var(--border);
border-bottom: 1px solid var(--border); border-bottom: 1px solid var(--border);
}
a:hover { &:hover {
background: var(--surface-three); background: var(--surface-three);
text-decoration: underline var(--primary); text-decoration: underline var(--primary);
color: var(--text-one); color: var(--text-one);
}
} }
h5 { h5 {
@@ -47,7 +47,7 @@
user-select: none; user-select: none;
} }
@media (max-width: 767px) { @media (max-width: 768px) {
h5 { h5 {
display: none; display: none;
} }

View File

@@ -15,7 +15,8 @@
let bots = ['semantic-release-bot', 'revanced-bot']; let bots = ['semantic-release-bot', 'revanced-bot'];
</script> </script>
<div class="section-container"> <div class="container">
<!-- svelte-ignore a11y-no-static-element-interactions -->
<div <div
class="title" class="title"
class:closed={!expanded} class:closed={!expanded}
@@ -31,7 +32,7 @@
</div> </div>
{#if expanded} {#if expanded}
<div class="contrib-host" transition:slide={{ easing: quintOut, duration: 500 }}> <div class="contributors" transition:slide={{ easing: quintOut, duration: 500 }}>
{#each contributors as { name, avatar_url, url }} {#each contributors as { name, avatar_url, url }}
{#if !bots.includes(name)} {#if !bots.includes(name)}
<ContributorButton {name} pfp={avatar_url} {url} /> <ContributorButton {name} pfp={avatar_url} {url} />
@@ -42,61 +43,59 @@
</div> </div>
<style lang="scss"> <style lang="scss">
.title { .container {
display: flex;
align-items: center;
justify-content: space-between;
cursor: pointer;
background-color: var(--surface-seven);
padding: 0.75rem 1.25rem;
border-bottom: 1px solid var(--border);
transition: all 0.2s var(--bezier-one);
&:hover {
background-color: var(--surface-three);
}
}
.closed {
border-bottom: none;
}
#arrow {
transition: all 0.2s var(--bezier-one);
user-select: none;
}
.section-container {
border-radius: 20px; border-radius: 20px;
overflow: hidden; overflow: hidden;
border: 1px solid var(--border); border: 1px solid var(--border);
}
a { .title {
display: flex; display: flex;
text-decoration: none; align-items: center;
width: max-content; justify-content: space-between;
border-radius: 8px; cursor: pointer;
} background-color: var(--surface-seven);
padding: 0.75rem 1.25rem;
border-bottom: 1px solid var(--border);
transition: all 0.2s var(--bezier-one);
a:hover { &:hover {
text-decoration: underline var(--primary); background-color: var(--surface-three);
color: var(--text-one); }
}
.contrib-host { &.closed {
margin-right: -1px; border-bottom: none;
margin-bottom: -1px; }
display: grid;
justify-items: center;
grid-template-columns: repeat(auto-fill, minmax(150px, 1fr));
}
@media (max-width: 767px) { a {
.contrib-host { display: flex;
padding: 0.75rem; text-decoration: none;
gap: 0.25rem; width: max-content;
grid-template-columns: repeat(auto-fill, minmax(50px, 1fr)); border-radius: 8px;
&:hover {
text-decoration: underline var(--primary);
color: var(--text-one);
}
}
#arrow {
transition: all 0.2s var(--bezier-one);
user-select: none;
}
}
.contributors {
margin-right: -1px;
margin-bottom: -1px;
display: grid;
justify-items: center;
grid-template-columns: repeat(auto-fill, minmax(150px, 1fr));
@media (max-width: 768px) {
padding: 0.75rem;
gap: 0.25rem;
grid-template-columns: repeat(auto-fill, minmax(50px, 1fr));
}
} }
} }
</style> </style>

View File

@@ -6,40 +6,18 @@
import { createQuery } from '@tanstack/svelte-query'; import { createQuery } from '@tanstack/svelte-query';
import Head from '$lib/components/Head.svelte'; import Head from '$lib/components/Head.svelte';
import Button from '$lib/components/Button.svelte';
import Snackbar from '$lib/components/Snackbar.svelte';
import Query from '$lib/components/Query.svelte'; import Query from '$lib/components/Query.svelte';
import Dialogue from '$lib/components/Dialogue.svelte'; import CryptoDialog from '$layout/Dialogs/CryptoDialog.svelte';
import QRCode from './QRCode.svelte';
import DonateHeartAnimation from './DonateHeartAnimation.svelte'; import DonateHeartAnimation from './DonateHeartAnimation.svelte';
import TeamMember from './TeamMember.svelte'; import TeamMember from './TeamMember.svelte';
import CircleMultipleOutline from 'svelte-material-icons/CircleMultipleOutline.svelte';
import WalletOutline from 'svelte-material-icons/WalletOutline.svelte';
import ChevronUp from 'svelte-material-icons/ChevronUp.svelte';
import { supportsWebP } from '$util/supportsWebP'; import { supportsWebP } from '$util/supportsWebP';
const teamQuery = createQuery(queries.team()); const teamQuery = createQuery(queries.team());
const aboutQuery = createQuery(queries.about()); const aboutQuery = createQuery(queries.about());
let qrCodeDialogue = false;
let cryptoDialogue = false; let cryptoDialogue = false;
let addressSnackbar = false;
let qrCodeValue = '';
let qrCodeDialogueName = '';
async function copyToClipboard(walletAddress: string) {
addressSnackbar = true;
qrCodeDialogue = false;
try {
await navigator.clipboard.writeText(walletAddress);
} catch (error) {
console.error('Failed to copy crypto wallet:', error);
}
}
const shuffle = <T,>(array: T[]) => const shuffle = <T,>(array: T[]) =>
array array
@@ -136,86 +114,22 @@
</Query> </Query>
</main> </main>
<Dialogue bind:modalOpen={cryptoDialogue}> <Query query={aboutQuery} let:data>
<svelte:fragment slot="icon"> <CryptoDialog bind:dialogOpen={cryptoDialogue} wallets={data.about.donations.wallets} />
<CircleMultipleOutline size="32px" color="var(--surface-six)" /> </Query>
</svelte:fragment>
<svelte:fragment slot="title">Cryptocurrencies</svelte:fragment>
<svelte:fragment slot="description">
<hr style="margin: 1rem 0;" />
<div class="wallets">
<Query query={aboutQuery} let:data>
{#each data.about.donations.wallets as wallet}
<button
on:click={() => {
qrCodeValue = wallet.address;
qrCodeDialogueName = wallet.currency_code;
qrCodeDialogue = !qrCodeDialogue;
// when the user clicks a wallet the crypto wallet goes away
// because multi page dialogues aren't implemented yet oops
cryptoDialogue = false;
}}
>
<div class="wallet-name">
<img
src="/donate/crypto/{wallet.currency_code}.svg"
onerror="this.onerror=null; this.src='/donate/fallback.svg'"
alt={`${wallet.network} icon.'`}
/>
{`${wallet.network} (${wallet.currency_code})`}
</div>
<div id="arrow">
<ChevronUp size="20px" color="var(--surface-six)" />
</div>
</button>
{/each}
</Query>
</div>
</svelte:fragment>
<svelte:fragment slot="buttons">
<Button type="filled" on:click={() => (cryptoDialogue = false)}>Close</Button>
</svelte:fragment>
</Dialogue>
<Dialogue bind:modalOpen={qrCodeDialogue}>
<svelte:fragment slot="icon">
<WalletOutline size="32px" color="var(--surface-six)" />
</svelte:fragment>
<svelte:fragment slot="title">{qrCodeDialogueName} Wallet</svelte:fragment>
<svelte:fragment slot="description">
<div class="qr-code-body">
{qrCodeValue}
<QRCode codeValue={qrCodeValue} />
</div>
</svelte:fragment>
<svelte:fragment slot="buttons">
<Button
type="text"
on:click={() => {
qrCodeDialogue = false;
cryptoDialogue = true;
}}>Back</Button
>
<Button type="filled" on:click={() => copyToClipboard(qrCodeValue)}>Copy Address</Button>
</svelte:fragment>
</Dialogue>
<Snackbar bind:open={addressSnackbar}>
<svelte:fragment slot="text">Address copied to clipboard</svelte:fragment>
</Snackbar>
<style lang="scss"> <style lang="scss">
main { main {
display: flex; display: flex;
flex-direction: column; flex-direction: column;
margin-bottom: 5rem;
// support revanced and heart thingy
section { section {
display: flex; display: flex;
justify-content: center; justify-content: center;
align-items: center; align-items: center;
@media screen and (max-width: 768px) { @media (max-width: 768px) {
flex-direction: column-reverse; flex-direction: column-reverse;
} }
} }
@@ -234,17 +148,16 @@
margin-bottom: 2rem; margin-bottom: 2rem;
width: 60%; width: 60%;
@media screen and (max-width: 1200px) { @media (max-width: 1200px) {
width: 90%; width: 90%;
} }
@media screen and (max-width: 768px) { @media (max-width: 768px) {
width: 100%; width: 100%;
} }
} }
// COPEEEE @media (max-width: 768px) {
@media screen and (max-width: 768px) {
#heart { #heart {
display: none; display: none;
} }
@@ -255,7 +168,7 @@
gap: 1rem; gap: 1rem;
margin-bottom: 3rem; margin-bottom: 3rem;
@media screen and (max-width: 768px) { @media (max-width: 768px) {
flex-direction: column; flex-direction: column;
} }
} }
@@ -297,55 +210,6 @@
} }
} }
.wallets {
// i just guessed this
width: clamp(200px, 75vw, 375px);
#arrow {
height: 20px;
transform: rotate(90deg);
}
button {
width: 100%;
font-size: 0.9rem;
background-color: transparent;
border: none;
color: var(--text-four);
cursor: pointer;
text-align: left;
display: flex;
justify-content: space-between;
background-color: var(--surface-seven);
padding: 0.75rem 1.25rem;
transition: filter 0.4s var(--bezier-one);
&:hover {
filter: brightness(85%);
}
}
}
.wallet-name {
display: flex;
align-items: center;
gap: 0.5rem;
// crypto icon
img {
height: 24px;
width: 24px;
}
}
.qr-code-body {
display: flex;
flex-direction: column;
align-items: center;
gap: 1rem;
word-break: break-word;
text-align: center;
}
.team { .team {
width: 100%; width: 100%;
display: grid; display: grid;
@@ -353,6 +217,5 @@
justify-content: space-between; justify-content: space-between;
align-items: stretch; align-items: stretch;
gap: 1rem; gap: 1rem;
margin-bottom: 4rem;
} }
</style> </style>

View File

@@ -68,6 +68,7 @@
</script> </script>
<!-- svelte-ignore a11y-click-events-have-key-events --> <!-- svelte-ignore a11y-click-events-have-key-events -->
<!-- svelte-ignore a11y-no-static-element-interactions -->
<div <div
id="pulsating-image" id="pulsating-image"
on:mousemove={(e) => showHeart(e, false)} on:mousemove={(e) => showHeart(e, false)}

View File

@@ -93,7 +93,7 @@
.mobile { .mobile {
display: none; display: none;
} }
@media screen and (width <= 768px) { @media (max-width: 768px) {
.desktop { .desktop {
display: none; display: none;
} }
@@ -117,7 +117,7 @@
user-select: none; user-select: none;
margin-bottom: 1rem; margin-bottom: 1rem;
@media screen and (max-width: 768px) { @media (max-width: 768px) {
margin-bottom: 0; margin-bottom: 0;
height: 48px; height: 48px;
width: 48px; width: 48px;

View File

@@ -13,7 +13,7 @@
import Query from '$lib/components/Query.svelte'; import Query from '$lib/components/Query.svelte';
import Button from '$lib/components/Button.svelte'; import Button from '$lib/components/Button.svelte';
import Picture from '$lib/components/Picture.svelte'; import Picture from '$lib/components/Picture.svelte';
import Dialogue from '$lib/components/Dialogue.svelte'; import DownloadCompatibilityWarningDialog from '$layout/Dialogs/DownloadCompatibilityWarningDialog.svelte';
import { onMount } from 'svelte'; import { onMount } from 'svelte';
const query = createQuery(queries.manager()); const query = createQuery(queries.manager());
@@ -69,21 +69,6 @@
]} ]}
/> />
<Dialogue bind:modalOpen={warningDialogue}>
<svelte:fragment slot="title">Warning</svelte:fragment>
<svelte:fragment slot="description">{warning} Do you still want to download?</svelte:fragment>
<svelte:fragment slot="buttons">
<Query {query} let:data>
<Button
type="text"
href={data.release.download_url}
on:click={() => (warningDialogue = false)}>Okay</Button
>
</Query>
<Button type="text" on:click={() => (warningDialogue = false)}>Cancel</Button>
</svelte:fragment>
</Dialogue>
<main class="wrapper center" in:fly={{ y: 10, easing: quintOut, duration: 750 }}> <main class="wrapper center" in:fly={{ y: 10, easing: quintOut, duration: 750 }}>
<h2>ReVanced <span>Manager</span></h2> <h2>ReVanced <span>Manager</span></h2>
<p>Patch your favourite apps, right on your device.</p> <p>Patch your favourite apps, right on your device.</p>
@@ -113,6 +98,8 @@
</div> </div>
</main> </main>
<DownloadCompatibilityWarningDialog bind:dialogOpen={warningDialogue} {warning} />
<style> <style>
.center { .center {
display: flex; display: flex;

View File

@@ -16,7 +16,7 @@
import PatchItem from './PatchItem.svelte'; import PatchItem from './PatchItem.svelte';
import Search from '$lib/components/Search.svelte'; import Search from '$lib/components/Search.svelte';
import FilterChip from '$lib/components/FilterChip.svelte'; import FilterChip from '$lib/components/FilterChip.svelte';
import Dialogue from '$lib/components/Dialogue.svelte'; import MobilePatchesPackagesDialog from '$layout/Dialogs/MobilePatchesPackagesDialog.svelte';
import Query from '$lib/components/Query.svelte'; import Query from '$lib/components/Query.svelte';
import Fuse from 'fuse.js'; import Fuse from 'fuse.js';
import { onMount } from 'svelte'; import { onMount } from 'svelte';
@@ -25,8 +25,6 @@
const query = createQuery(queries.patches()); const query = createQuery(queries.patches());
let searcher: Fuse<Patch> | undefined;
let searchParams: Readable<URLSearchParams>; let searchParams: Readable<URLSearchParams>;
if (building) { if (building) {
searchParams = readable(new URLSearchParams()); searchParams = readable(new URLSearchParams());
@@ -124,32 +122,15 @@
> >
{selectedPkg || 'Packages'} {selectedPkg || 'Packages'}
</FilterChip> </FilterChip>
<!-- <FilterChip check>Universal</FilterChip>
<FilterChip>Patch options</FilterChip> -->
</div> </div>
<Query {query} let:data> <Query {query} let:data>
<div class="mobile-packages-Dialogue"> <MobilePatchesPackagesDialog
<Dialogue bind:modalOpen={mobilePackages} fullscreen> bind:dialogOpen={mobilePackages}
<svelte:fragment slot="title">Packages</svelte:fragment> bind:searchTerm
<div class="mobile-packages"> {data}
<span {selectedPkg}
on:click={() => (mobilePackages = !mobilePackages)} />
on:keypress={() => (mobilePackages = !mobilePackages)}
>
<Package {selectedPkg} name="All packages" bind:searchTerm />
</span>
{#each data.packages as pkg}
<span
on:click={() => (mobilePackages = !mobilePackages)}
on:keypress={() => (mobilePackages = !mobilePackages)}
>
<Package {selectedPkg} name={pkg} bind:searchTerm />
</span>
{/each}
</div>
</Dialogue>
</div>
<aside in:fly={{ y: 10, easing: quintOut, duration: 750 }}> <aside in:fly={{ y: 10, easing: quintOut, duration: 750 }}>
<PackageMenu> <PackageMenu>
@@ -164,7 +145,6 @@
<div class="patches-container"> <div class="patches-container">
{#each filterPatches(data.patches, selectedPkg || '', displayedTerm) as patch} {#each filterPatches(data.patches, selectedPkg || '', displayedTerm) as patch}
<!-- Trigger new animations when package or search changes (I love Svelte) -->
{#key selectedPkg || displayedTerm} {#key selectedPkg || displayedTerm}
<div in:fly={{ y: 10, easing: quintOut, duration: 750 }}> <div in:fly={{ y: 10, easing: quintOut, duration: 750 }}>
<PatchItem {patch} bind:showAllVersions /> <PatchItem {patch} bind:showAllVersions />
@@ -213,19 +193,7 @@
display: none; display: none;
} }
.mobile-packages { @media (max-width: 768px) {
margin-bottom: -1px;
overflow: hidden;
border-radius: 12px;
border: 1px solid var(--border);
}
@media (min-width: 768px) {
.mobile-packages-Dialogue {
display: none;
}
}
@media (max-width: 767px) {
main { main {
grid-template-columns: none; grid-template-columns: none;
flex-direction: column; flex-direction: column;

View File

@@ -25,6 +25,7 @@
</script> </script>
<!-- svelte-ignore a11y-click-events-have-key-events --> <!-- svelte-ignore a11y-click-events-have-key-events -->
<!-- svelte-ignore a11y-no-static-element-interactions -->
<div <div
class="package" class="package"
class:selected={selectedPkg === name || (name === 'All packages' && !selectedPkg)} class:selected={selectedPkg === name || (name === 'All packages' && !selectedPkg)}
@@ -33,57 +34,39 @@
{name} {name}
</div> </div>
<style> <style lang="scss">
.package { .package {
padding: 0.75rem 1rem; padding: 0.75rem 1rem;
border-radius: 100px;
font-size: 0.85rem; font-size: 0.85rem;
font-weight: 500; font-weight: 500;
border-radius: 100px;
cursor: pointer;
display: flex;
align-items: center;
gap: 0.6rem;
width: 100%;
user-select: none;
transition: background-color 0.4s var(--bezier-one);
color: var(--text-four); color: var(--text-four);
transition: color 0.3s var(--bezier-one);
}
.selected { cursor: pointer;
color: var(--primary); user-select: none;
transition: color 0.3s var(--bezier-one); transition:
background-color: var(--tertiary); background-color 0.4s var(--bezier-one),
} color 0.3s var(--bezier-one);
.package:hover:not(.selected) {
background-color: var(--surface-seven);
}
.package:not(.selected):hover { @media (max-width: 768px) {
color: var(--text-one);
}
@media (max-width: 767px) {
.package {
border-radius: 0px; border-radius: 0px;
font-size: 0.9rem; font-size: 0.9rem;
padding: 1rem 1rem; padding: 1rem;
width: 100%;
background-color: transparent;
word-break: break-all; word-break: break-all;
overflow: hidden; overflow: hidden;
text-overflow: ellipsis; text-overflow: ellipsis;
color: var(--text-four);
border-bottom: 1px solid var(--border); border-bottom: 1px solid var(--border);
} }
.selected { &.selected {
color: var(--primary); color: var(--primary);
background-color: var(--tertiary); background-color: var(--tertiary);
} }
.package:not(.selected):hover { &:hover:not(.selected) {
color: var(--text-four); background-color: var(--surface-seven);
color: var(--text-one);
} }
} }
</style> </style>

View File

@@ -1,37 +1,33 @@
<div class="menu"> <div>
<h6>Packages</h6> <h6>Packages</h6>
<hr /> <hr />
<div class="slot"> <span>
<slot /> <slot />
</div> </span>
</div> </div>
<style> <style lang="scss">
.menu { div {
height: calc(100vh - 60px);
width: 100%;
padding: 0px 30px 30px 10px;
display: flex; display: flex;
flex-direction: column; flex-direction: column;
position: sticky; position: sticky;
height: calc(100vh - 60px);
top: 60px; top: 60px;
padding-top: calc(6rem - 60px); padding: calc(6rem - 60px) 30px 30px 10px;
overflow-y: scroll; overflow-y: scroll;
&::-webkit-scrollbar-thumb {
background-color: transparent;
}
&:hover::-webkit-scrollbar-thumb {
background-color: var(--primary);
}
} }
.menu::-webkit-scrollbar-thumb { span {
background-color: transparent;
}
.menu:hover::-webkit-scrollbar-thumb {
background-color: var(--primary);
}
.slot {
margin-top: 0.75rem; margin-top: 0.75rem;
display: flex;
gap: 1rem;
flex-direction: column;
white-space: normal;
word-break: break-all; word-break: break-all;
} }
@@ -39,16 +35,4 @@
margin-bottom: 1rem; margin-bottom: 1rem;
color: var(--primary); color: var(--primary);
} }
@media (max-width: 767px) {
.menu {
padding: 0.75rem;
height: unset;
}
h6,
hr {
display: none;
}
}
</style> </style>

View File

@@ -19,6 +19,7 @@
</script> </script>
<!-- svelte-ignore a11y-click-events-have-key-events --> <!-- svelte-ignore a11y-click-events-have-key-events -->
<!-- svelte-ignore a11y-no-static-element-interactions -->
<div <div
class="patch-container" class="patch-container"
class:expanded={hasPatchOptions} class:expanded={hasPatchOptions}
@@ -99,6 +100,7 @@
<span transition:fade={{ easing: quintOut, duration: 1000 }}> <span transition:fade={{ easing: quintOut, duration: 1000 }}>
<div class="options" transition:slide={{ easing: quintOut, duration: 500 }}> <div class="options" transition:slide={{ easing: quintOut, duration: 500 }}>
{#each options as option} {#each options as option}
<!-- svelte-ignore a11y-no-static-element-interactions -->
<div class="option" on:click|stopPropagation> <div class="option" on:click|stopPropagation>
<h5 id="option-title">{option.title}</h5> <h5 id="option-title">{option.title}</h5>
<h5> <h5>