chore: init new SvelteKit project

This commit is contained in:
madkarmaa
2025-11-10 14:31:55 +01:00
parent 00760a0b02
commit 3906a58640
144 changed files with 1110 additions and 14686 deletions

29
src/app.d.ts vendored
View File

@@ -1,22 +1,13 @@
/// <reference types="@sveltejs/kit" />
// See https://kit.svelte.dev/docs/types#app
// See https://svelte.dev/docs/kit/types#app.d.ts
// for information about these interfaces
// and what to do when importing types
declare namespace App {
// interface Locals {}
// interface Platform {}
// interface Session {}
// interface Stuff {}
declare global {
namespace App {
// interface Error {}
// interface Locals {}
// interface PageData {}
// interface PageState {}
// interface Platform {}
}
}
declare module '*&as=picture' {
/**
* actual types
* taken from https://github.com/JonasKruckenberg/imagetools/issues/160#issuecomment-1009292026
* - code https://github.com/JonasKruckenberg/imagetools/blob/main/packages/core/src/output-formats.ts
* - docs https://github.com/JonasKruckenberg/imagetools/blob/main/docs/guide/getting-started.md#metadata
*/
const out;
export default out;
}
export {};

View File

@@ -2,26 +2,10 @@
<html lang="en">
<head>
<meta charset="utf-8" />
<link rel="icon" href="/favicon.ico" />
<meta
name="robots"
content="index, follow, max-image-preview:large, max-snippet:-1, max-video-preview:-1"
/>
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<!-- OpenGraph -->
<meta property="og:type" content="website" />
<meta property="og:image" content="/logo.png" />
<!-- Twitter -->
<meta name="twitter:image" itemprop="image" content="/logo.png" />
<meta name="twitter:card" content="summary" />
%sveltekit.head%
</head>
<body style="background-color: var(--background-one)">
<body data-sveltekit-preload-data="hover">
<div style="display: contents">%sveltekit.body%</div>
</body>
</html>

View File

@@ -1,197 +0,0 @@
@import url('https://fonts.googleapis.com/css2?family=Manrope:wght@200;300;400;500;600;700;800&display=swap');
* {
box-sizing: inherit;
margin: 0;
padding: 0;
font-family: var(--main-font);
-webkit-tap-highlight-color: rgba(0, 0, 0, 0);
transition:
color 0.5s ease-in-out,
background-color 0.5s ease-in-out,
fill 0.5s ease-in-out;
}
html {
margin: 0;
padding: 0;
font-size: 100%;
box-sizing: border-box;
overflow-y: scroll;
}
body {
margin: 0;
padding: 0;
}
html,
body {
max-width: 100%;
}
.wrapper {
margin-inline: auto;
width: min(90%, 80rem);
margin-top: 2.6rem;
}
:root {
--main-font: 'Manrope', sans-serif;
--mono-font: ui-monospace, SFMono-Regular, SF Mono, Menlo, Consolas, Liberation Mono, monospace;
--text-one: hsl(calc(var(--hue, 206) + 0), 100%, 94%);
--surface-one: hsl(calc(var(--hue, 206) + 0), 100%, 94%);
--primary: hsl(calc(var(--hue, 206) + 0), 100%, 81%);
--secondary: hsl(calc(var(--hue, 206) + 2), 75%, 82%);
--tertiary: hsla(calc(var(--hue, 206) - 1), 91%, 69%, 0.15);
--background-one: hsl(calc(var(--hue, 206) + 46), 10%, 11%);
--surface-two: hsl(calc(var(--hue, 206) + 46), 10%, 11%);
--surface-three: hsl(calc(var(--hue, 206) + 4), 14%, 17%);
--surface-four: hsl(calc(var(--hue, 206) + 6), 19%, 19%);
--text-two: hsl(calc(var(--hue, 206) + 6), 19%, 19%);
--border: hsl(calc(var(--hue, 206) + 15), 17%, 26%);
--surface-five: hsl(calc(var(--hue, 206) + 15), 17%, 26%);
--text-three: hsl(calc(var(--hue, 206) + 20), 48%, 18%);
--text-four: hsl(calc(var(--hue, 206) + 2), 30%, 75%);
--surface-six: hsl(calc(var(--hue, 206) + 2), 30%, 75%);
--surface-seven: hsl(calc(var(--hue, 206) + 24), 9%, 13%);
--surface-eight: hsl(calc(var(--hue, 206) + 34), 9%, 13.5%);
--surface-nine: hsl(calc(var(--hue, 206) + 24), 9.5%, 17.5%);
--red-one: hsl(333, 84%, 62%);
--red-two: hsl(357, 74%, 60%);
--red-three: hsl(2, 68%, 83%);
--yellow-one: hsl(59, 100%, 72%);
--bezier-one: ease-out;
--drop-shadow-one: 0px 4px 5px 0px rgba(0, 0, 0, 0.14), 0px 1px 10px 0px rgba(0, 0, 0, 0.12),
0px 2px 4px -1px rgba(0, 0, 0, 0.2);
}
::selection {
background-color: var(--tertiary);
}
mark {
background-color: var(--secondary);
}
/*-----headings-----*/
h1 {
color: var(--text-one);
line-height: 4rem;
font-size: 3.5rem;
font-weight: 700;
letter-spacing: -0.025em;
}
h2 {
color: var(--text-four);
font-size: 2.5rem;
letter-spacing: -0.04rem;
font-weight: 600;
}
h3 {
font-size: 1.25rem;
color: var(--secondary);
font-weight: 600;
}
h4 {
color: var(--secondary);
font-weight: 400;
font-size: 1rem;
letter-spacing: 0.02rem;
line-height: 2rem;
}
h5 {
color: var(--text-four);
font-weight: 400;
font-size: 0.9rem;
letter-spacing: 0.02rem;
}
h6 {
color: var(--text-four);
font-weight: 500;
font-size: 0.85rem;
}
p {
color: var(--text-four);
font-weight: 400;
font-size: 1rem;
letter-spacing: 0.02rem;
line-height: 1.75rem;
}
@media (max-width: 768px) {
h1 {
font-size: 2.6rem;
line-height: 3.75rem;
}
h2 {
font-size: 2rem;
}
}
/*---------------*/
::-webkit-scrollbar {
width: 5px;
background-color: transparent;
}
::-webkit-scrollbar-thumb {
background-color: var(--primary);
background-clip: content-box;
border-radius: 100px;
}
::-webkit-scrollbar-thumb:hover {
background-color: var(--surface-five);
}
hr {
display: block;
height: 1px;
border: 0;
border-top: 1px solid var(--border);
}
textarea {
resize: vertical;
field-sizing: content;
}
input,
textarea {
padding: 1rem;
border-radius: 12px;
border: 1px solid var(--border);
background-color: transparent;
color: var(--secondary);
}
input:focus,
textarea:focus {
outline: 1px solid var(--primary);
}

View File

@@ -1,42 +0,0 @@
import { navigating, page } from '$app/stores';
import { derived, type Readable } from 'svelte/store';
export interface RouterEvent {
// URL of the current page or the page we are navigating to.
target_url: URL;
// Are we navigating?
navigating: boolean;
}
function makeStore(): Readable<RouterEvent> {
// This stuff will run both client side and server side.
if (typeof location === 'undefined') {
// `location` does not exist on the server.
// Return a derived store based on `page` for SSR.
// Server will never navigate so this is fine.
return derived(page, ($page) => {
return { navigating: false, target_url: $page.url };
});
}
// On client.
let current = new URL(location.href);
// Return store that responds to navigation events.
// The `navigating` store immediately "pushes" `null`.
// This in turn causes this derived store to immediately "push" the current URL.
return derived(navigating, ($nav) => {
let navigating = false;
// $nav is null when navigation finishes.
if ($nav != null && $nav.to != null) {
current = $nav.to.url;
navigating = true;
}
return { navigating, target_url: current };
});
}
// Do not subscribe to it outside of components!
export default makeStore();

View File

@@ -1,248 +0,0 @@
import * as settings from './settings';
// API Endpoints
import type {
Patch,
Contributable,
Release,
TeamMember,
DonationPlatform,
CryptoWallet,
Social,
About,
ResponseAnnouncement,
Announcement,
Tags
} from '$lib/types';
import { get_access_token, is_logged_in, UnauthenticatedError } from '$lib/auth';
export type ContributorsData = { contributables: Contributable[] };
export type PatchesData = { patches: Patch[]; packages: string[] };
export type ReleaseData = { release: Release };
export type TeamData = { members: TeamMember[] };
export type AboutData = { about: About };
export type DonationData = { wallets: CryptoWallet[]; platforms: DonationPlatform[] };
export type SocialsData = { socials: Social[] };
export type AnnouncementsData = { announcements: ResponseAnnouncement[] };
type GetAnnouncementsOptions = Partial<{
tags: string[];
count: number;
cursor: number;
}>;
export function build_url(endpoint: string) {
// //////v4/contributors -> v4/contributors
endpoint = endpoint.replace(/^\/+/, '');
// v4/contributors -> contributors
if (endpoint.startsWith(settings.API_VERSION)) endpoint = endpoint.split('/').slice(1).join('/');
return `${settings.api_base_url()}/${settings.API_VERSION}/${endpoint}`;
}
function build_headers() {
const access_token_data = get_access_token();
return {
'Content-Type': 'application/json',
Authorization: access_token_data ? `Bearer ${access_token_data.token}` : ''
};
}
async function get_json(endpoint: string) {
return await fetch(build_url(endpoint)).then((r) => r.json());
}
async function post_json(endpoint: string, body?: any) {
if (!is_logged_in()) throw new UnauthenticatedError();
const headers = build_headers();
return await fetch(build_url(endpoint), {
method: 'POST',
headers,
body: body ? JSON.stringify(body) : ''
}).then((r) => {
return r.headers.get('content-length') === '0' ? null : r.json();
});
}
async function patch_json(endpoint: string, body?: any) {
if (!is_logged_in()) throw new UnauthenticatedError();
const headers = build_headers();
return await fetch(build_url(endpoint), {
method: 'PATCH',
headers,
body: body ? JSON.stringify(body) : ''
}).then((r) => {
return r.headers.get('content-length') === '0' ? null : r.json();
});
}
async function delete_json(endpoint: string, body?: any) {
if (!is_logged_in()) throw new UnauthenticatedError();
const headers = build_headers();
return await fetch(build_url(endpoint), {
method: 'DELETE',
headers,
body: body ? JSON.stringify(body) : ''
}).then((r) => {
return r.headers.get('content-length') === '0' ? null : r.json();
});
}
async function contributors(): Promise<ContributorsData> {
const json = await get_json('v4/contributors');
return { contributables: json };
}
async function manager(): Promise<ReleaseData> {
const json = await get_json('v4/manager');
return { release: json };
}
async function patches(): Promise<PatchesData> {
const json = await get_json('v4/patches/list');
const packagesWithCount: { [key: string]: number } = {};
for (const patch of json) {
if (!patch.compatiblePackages) continue;
patch.compatiblePackages = Object.keys(patch.compatiblePackages).map((name) => ({
name,
versions: patch.compatiblePackages[name]
}));
}
// gets packages and patch count
for (const { compatiblePackages } of json) {
if (!compatiblePackages) continue;
for (const pkg of compatiblePackages) {
packagesWithCount[pkg.name] = (packagesWithCount[pkg.name] || 0) + 1;
}
}
// sort packages by patch count to get most relevant apps on top
const packages = Object.keys(packagesWithCount);
packages.sort((a, b) => packagesWithCount[b] - packagesWithCount[a]);
return { patches: json, packages };
}
async function team(): Promise<TeamData> {
const json = await get_json('v4/team');
return { members: json };
}
async function about(): Promise<AboutData> {
const json = await get_json('v4/about');
return { about: json };
}
async function announcements(options: GetAnnouncementsOptions = {}): Promise<AnnouncementsData> {
const url = new URL(build_url('announcements'));
if (options.tags && options.tags.length > 0) url.searchParams.set('tags', options.tags.join(','));
if (options.count) url.searchParams.set('count', String(options.count));
if (options.cursor) url.searchParams.set('cursor', String(options.cursor));
const announcements = (await get_json('announcements')) as ResponseAnnouncement[];
return { announcements };
}
async function get_announcement_by_id(id: number): Promise<{ announcement: ResponseAnnouncement }> {
return { announcement: (await get_json(`announcements/${id}`)) as ResponseAnnouncement };
}
async function announcementTags(): Promise<{ tags: Tags }> {
return { tags: (await get_json(`announcements/tags`)) as Tags };
}
function show_error_alert(res: Response) {
alert(`A ${res.status < 500 ? 'user' : 'server'} error occurred. Please try again.`);
}
export async function create_announcement(announcement: Announcement) {
await post_json('announcements', announcement).catch(show_error_alert);
}
export async function update_announcement(id: number, announcement: Announcement) {
await patch_json(`announcements/${id}`, announcement).catch(show_error_alert);
}
export async function delete_announcement(id: number) {
await delete_json(`announcements/${id}`).catch(show_error_alert);
}
export async function archive_announcement(id: number) {
await post_json(`announcements/${id}/archive`).catch(show_error_alert);
}
export async function unarchive_announcement(id: number) {
await post_json(`announcements/${id}/unarchive`).catch(show_error_alert);
}
export const admin = {
create_announcement,
update_announcement,
delete_announcement,
archive_announcement,
unarchive_announcement
};
async function ping(): Promise<boolean> {
try {
const res = await fetch(`${settings.api_base_url()}/v4/ping`, { method: 'HEAD' });
return res.ok;
} catch (error) {
return false;
}
}
export const staleTime = 5 * 60 * 1000;
export const queries = {
manager: () => ({
queryKey: ['manager'],
queryFn: manager,
staleTime
}),
patches: () => ({
queryKey: ['patches'],
queryFn: patches,
staleTime
}),
contributors: () => ({
queryKey: ['contributors'],
queryFn: contributors,
staleTime
}),
team: () => ({
queryKey: ['team'],
queryFn: team,
staleTime
}),
about: () => ({
queryKey: ['info'],
queryFn: about,
staleTime
}),
announcements: () => ({
queryKey: ['announcements'],
queryFn: () => announcements(),
staleTime
}),
announcementById: (id: number) => ({
queryKey: ['announcementById', id],
queryFn: () => get_announcement_by_id(id)
}),
announcementTags: () => ({
queryKey: ['announcementTags'],
queryFn: () => announcementTags(),
staleTime
}),
ping: () => ({
queryKey: ['ping'],
queryFn: ping,
staleTime
})
};

View File

@@ -1,64 +0,0 @@
import { browser } from '$app/environment';
import { RV_API_URL, RV_EMAIL, RV_STATUS_URL } from '$env/static/public';
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 STATUS_KEY = 'revanced_status_url';
const EMAIL_KEY = 'revanced_email';
export const API_VERSION = 'v4';
// Get base URL
export function api_base_url(): string {
if (browser) {
const apiUrl = localStorage.getItem(URL_KEY) || default_api_url;
return apiUrl;
}
return default_api_url;
}
export function status_url(): string {
if (browser) {
return localStorage.getItem(STATUS_KEY) || default_status_url;
}
return default_status_url;
}
export function email(): string {
if (browser) {
return localStorage.getItem(EMAIL_KEY) || default_email;
}
return default_email;
}
// (re)set base URL.
export function set_api_base_url(url?: string) {
if (!url) {
localStorage.removeItem(URL_KEY);
} else {
localStorage.setItem(URL_KEY, url);
}
set_about_info(api_base_url());
}
export 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

@@ -1,61 +0,0 @@
<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

@@ -1,105 +0,0 @@
<script lang="ts">
import { createEventDispatcher } from 'svelte';
import Close from 'svelte-material-icons/Close.svelte';
import ArrowRight from 'svelte-material-icons/ArrowRight.svelte';
import Button from '$lib/components/Button.svelte';
export let title: string;
export let description: string | undefined = undefined;
export let buttonText: string | undefined = undefined;
export let buttonOnClick: any | undefined = undefined;
export let level: 'info' | 'caution' = 'info';
export let permanent: boolean = false;
export let onDismiss: () => void = () => {};
const dispatch = createEventDispatcher();
let closed: boolean = false;
function getVariant(level: string): 'default' | 'onDangerBackground' {
return level === 'caution' ? 'onDangerBackground' : 'default';
}
const dismiss = () => {
if (onDismiss) onDismiss();
closed = true;
dispatch('dismissed');
};
</script>
{#if !closed}
<div class="banner {level}" class:permanent>
<div class="text">
<h1 id="title">{title}</h1>
<h2 id="description">{description}</h2>
</div>
<div class="actions">
{#if !permanent}
<Button type={'icon'} icon={Close} on:click={dismiss} />
{/if}
{#if buttonText && buttonOnClick}
<Button variant={getVariant(level)} on:click={buttonOnClick}>
{buttonText}
<ArrowRight />
</Button>
{/if}
</div>
</div>
{/if}
<style lang="scss">
#title {
line-height: 26px;
color: currentColor;
font-size: 20px;
}
#description {
line-height: 20px;
color: currentColor;
font-size: 14px;
}
.banner {
display: flex;
justify-content: space-between;
box-sizing: border-box;
gap: 1.3rem;
width: 100%;
margin: 0;
padding: 24px 40px;
border-radius: 0;
font-size: 0.87rem;
&.info {
background-color: var(--surface-four);
color: var(--text-one);
#description {
color: var(--text-four);
}
}
&.caution {
background-color: var(--red-three);
color: #601410;
}
@media (max-width: 768px) {
flex-direction: column;
padding: 1.1rem 1.3rem;
}
.text {
display: flex;
flex-direction: column;
flex: 1;
gap: 0.55rem;
word-wrap: break-word;
}
.actions {
display: flex;
justify-content: end;
gap: 1rem;
}
}
</style>

View File

@@ -1,18 +0,0 @@
<script lang="ts">
import { goto } from '$app/navigation';
import { email } from '$data/api/settings';
import Banner from '$layout/Banners/Banner.svelte';
export let statusUrl: string | null = null;
const handleClick = () => statusUrl && goto(statusUrl);
</script>
<Banner
title="API service is currently down"
description="Some features of the site might be impacted. If this issue persists, reach out to {email()}."
buttonText={statusUrl ? 'Check status' : undefined}
buttonOnClick={statusUrl ? handleClick : undefined}
level="caution"
permanent
/>

View File

@@ -1,59 +0,0 @@
<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 = [
{
'gtm.start': new Date().getTime(),
event: 'gtm.js'
}
];
var firstScript = document.getElementsByTagName('script')[0];
var script = document.createElement('script');
script.async = true;
script.src = 'https://www.googletagmanager.com/gtm.js?id=' + RV_GOOGLE_TAG_MANAGER_ID;
//@ts-ignore
firstScript.parentNode.insertBefore(script, firstScript);
}
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

@@ -1,145 +0,0 @@
<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

@@ -1,177 +0,0 @@
<script lang="ts">
import { fade } from 'svelte/transition';
import { quadInOut } from 'svelte/easing';
import ArrowLeft from 'svelte-material-icons/ArrowLeft.svelte';
export let dialogOpen = false;
export let fullscreen = false;
export let notDismissible = false;
let element: HTMLDialogElement;
let y = 0;
function parseScroll() {
y = element.scrollTop;
}
</script>
{#if dialogOpen}
<!-- svelte-ignore a11y-no-static-element-interactions -->
<div
class="overlay"
on:click={() => {
if (!notDismissible) dialogOpen = !dialogOpen;
}}
on:keypress={() => {
if (!notDismissible) dialogOpen = !dialogOpen;
}}
transition:fade={{ easing: quadInOut, duration: 150 }}
/>
<dialog
class:fullscreen
class:scrolled={y > 10}
aria-modal="true"
bind:this={element}
on:scroll={parseScroll}
transition:fade={{ easing: quadInOut, duration: 150 }}
>
<div class="title" class:hasIcon={$$slots.icon}>
{#if fullscreen}
<button id="back-button" on:click={() => (dialogOpen = !dialogOpen)}>
<ArrowLeft size="24px" color="var(--surface-six)" />
</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}
<div class="slot"><slot /></div>
{#if $$slots.buttons}
<div class="buttons">
<slot name="buttons" />
</div>
{/if}
</dialog>
{/if}
<style lang="scss">
.overlay {
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
background-color: rgba(0, 0, 0, 0.5);
z-index: 6;
}
dialog {
position: fixed;
width: min(85%, 425px);
max-height: 75%;
overflow-y: scroll;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
border: none;
border-radius: 26px;
background-color: var(--surface-seven);
display: flex;
gap: 5%;
white-space: normal;
display: flex;
flex-direction: column;
align-items: center;
gap: 1rem;
z-index: 7;
padding: 32px;
box-shadow:
0px 4px 5px 0px rgba(0, 0, 0, 0.14),
0px 1px 10px 0px rgba(0, 0, 0, 0.12),
0px 2px 4px -1px rgba(0, 0, 0, 0.2);
scrollbar-width: 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 {
padding: 0;
margin: 0;
border: none;
background-color: transparent;
display: flex;
align-items: center;
}
.slot {
display: flex;
flex-direction: column;
align-content: center;
width: 100%;
}
</style>

View File

@@ -1,25 +0,0 @@
<script lang="ts">
import Button from '$lib/components/Button.svelte';
import Query from '$lib/components/Query.svelte';
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

@@ -1,66 +0,0 @@
<script lang="ts">
import Divider from '$lib/components/Divider.svelte';
import { email } from '$data/api/settings';
import Dialog from '$layout/Dialogs/Dialog.svelte';
import Button from '$lib/components/Button.svelte';
import Input from '$lib/components/Input.svelte';
export let showEmailDialog: boolean;
let enableInputSeconds = 15;
let keyword: string = '';
let interval: number | undefined;
function closeDialog() {
showEmailDialog = false;
clearInterval(interval);
interval = undefined;
enableInputSeconds = 15;
keyword = '';
}
$: if (showEmailDialog && !interval && enableInputSeconds != 0) {
interval = setInterval(() => {
if (enableInputSeconds <= 0) {
clearInterval(interval);
return;
}
enableInputSeconds -= 1;
}, 1000);
}
</script>
<Dialog bind:dialogOpen={showEmailDialog} notDismissible>
<svelte:fragment slot="title">Abuse notice</svelte:fragment>
<svelte:fragment slot="description">
<p>
This E-Mail address is <b>not</b> for support, help, bug reports or feature requests. It must
have a subject and body and have the keyword <span style="user-select: none;">'Reficio'</span>
in either, otherwise your mail will be
<b>ignored</b>.
</p>
<br />
{#if enableInputSeconds == 0}
<span>Enter the keyword, then click "Send".</span>
<br />
<br />
<Input placeholder="Keyword" type="text" bind:value={keyword}></Input>
{:else}
<span>Read above and wait <b>{enableInputSeconds}</b> seconds.</span>
{/if}
</svelte:fragment>
<svelte:fragment slot="buttons">
{#if keyword.toLowerCase() === 'reficio'}
<Button type="text"><a href="mailto:{email()}">Send</a></Button>
{/if}
<Button type="filled" on:click={closeDialog}>Close</Button>
</svelte:fragment>
</Dialog>
<style>
a {
text-decoration: none;
color: var(--text-four);
font-weight: 600;
}
</style>

View File

@@ -1,76 +0,0 @@
<script lang="ts">
import { createEventDispatcher } from 'svelte';
import { fade } from 'svelte/transition';
export let src: string;
export let alt: string;
const dispatch = createEventDispatcher();
function closeDialog() {
dispatch('close');
}
function handleKeydown(event: KeyboardEvent) {
if (event.key === 'Escape') closeDialog();
}
</script>
<svelte:window on:keydown={handleKeydown} />
<!-- svelte-ignore a11y-click-events-have-key-events -->
<!-- svelte-ignore a11y-no-static-element-interactions -->
<div class="dialog-overlay" on:click={closeDialog} transition:fade={{ duration: 175 }}>
<!-- svelte-ignore a11y-no-static-element-interactions -->
<div class="dialog-content" on:click|stopPropagation transition:fade={{ duration: 175 }}>
<button class="close-button" on:click={closeDialog}>×</button>
<img {src} {alt} />
</div>
</div>
<style>
.dialog-overlay {
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
background: rgba(0, 0, 0, 0.85);
display: flex;
justify-content: center;
align-items: center;
z-index: 1000;
padding: 2rem;
box-sizing: border-box;
}
.dialog-content {
position: relative;
max-width: 90vw;
max-height: 90vh;
}
img {
max-width: 100%;
max-height: 90vh;
object-fit: contain;
}
.close-button {
position: absolute;
top: -1rem;
right: -2rem;
background: none;
border: none;
color: white;
font-size: 2rem;
cursor: pointer;
padding: 0.5rem;
line-height: 1;
z-index: 1001;
}
.close-button:hover {
color: #ddd;
}
</style>

View File

@@ -1,85 +0,0 @@
<script lang="ts">
import { login } from '$lib/auth';
import Input from '$lib/components/Input.svelte';
import Button from '$lib/components/Button.svelte';
import Dialog from '$layout/Dialogs/Dialog.svelte';
import { passed_login_with_creds } from '$lib/stores';
export let dialogOpen: boolean;
let loginForm: HTMLFormElement;
let wrong_credentials = false;
async function handle_login(e: SubmitEvent) {
const data = new FormData(e.target as HTMLFormElement);
const username = data.get('username') as string;
const password = data.get('password') as string;
const success = await login(username, password);
dialogOpen = !success;
passed_login_with_creds.set(success);
wrong_credentials = !success;
}
</script>
<Dialog bind:dialogOpen>
<div class="container">
<h2>Login</h2>
<p>This login is reserved for site administrators. Go back!</p>
{#if wrong_credentials}
<p style="color: var(--red-one)">Username or password do not match. Try again.</p>
{/if}
<form on:submit|preventDefault={handle_login} bind:this={loginForm}>
<div>
<Input placeholder="Username" required />
<Input
placeholder="Password"
type="password"
onkeydown={(event) => event.key === 'Enter' && loginForm.requestSubmit()}
required
/>
</div>
</form>
</div>
<svelte:fragment slot="buttons">
<Button type="text" on:click={() => (dialogOpen = !dialogOpen)}>Cancel</Button>
<!-- first paragraph of https://developer.mozilla.org/en-US/docs/Web/API/HTMLFormElement/submit -->
<Button type="filled" on:click={() => loginForm.requestSubmit()}>Login</Button>
</svelte:fragment>
</Dialog>
<style lang="scss">
.container {
display: flex;
flex-direction: column;
gap: 1rem;
position: relative;
h2 {
color: var(--primary);
}
form {
display: flex;
gap: 1rem;
div:has(input) {
display: flex;
flex-direction: column;
gap: 1rem;
margin-bottom: 1rem;
width: 100%;
}
div:has(svg) {
display: flex;
align-items: center;
justify-content: center;
flex: 1;
}
}
}
</style>

View File

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

View File

@@ -1,40 +0,0 @@
<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,35 +0,0 @@
<script lang="ts">
import { set_access_token } from '$lib/auth';
import { admin_login } from '$lib/stores';
import Button from '$lib/components/Button.svelte';
import Dialog from '$layout/Dialogs/Dialog.svelte';
export let loginOpen: boolean;
$: session_expired = $admin_login.logged_in_previously && !$admin_login.logged_in;
function reset_session() {
set_access_token();
session_expired = !session_expired;
}
</script>
<Dialog dialogOpen={session_expired}>
<svelte:fragment slot="title">Expired session</svelte:fragment>
<div>
This session has expired, log in again to renew or lose all access to administrative power.
</div>
<svelte:fragment slot="buttons">
<Button type="text" on:click={reset_session}>OK</Button>
<Button type="filled" on:click={() => (reset_session(), (loginOpen = !loginOpen))}>
Login
</Button>
</svelte:fragment>
</Dialog>
<style>
div {
color: var(--text-four);
}
</style>

View File

@@ -1,117 +0,0 @@
<script lang="ts">
import { fromNow } from '$util/fromNow';
import { admin_login } from '$lib/stores';
import { api_base_url, set_api_base_url, default_api_url } from '$data/api/settings';
import { useQueryClient } from '@tanstack/svelte-query';
import Button from '$lib/components/Button.svelte';
import Dialog from '$layout/Dialogs/Dialog.svelte';
import Replay from 'svelte-material-icons/Replay.svelte';
import Cog from 'svelte-material-icons/Cog.svelte';
export let loginOpen: boolean;
export let dialogOpen: boolean;
const client = useQueryClient();
let url = api_base_url();
function reload() {
location.reload();
}
function clear_and_reload() {
client.clear();
// `client.clear()` does technically do this for us, but it takes a while.
localStorage.clear();
reload();
}
function save() {
set_api_base_url(url);
reload();
}
function reset() {
url = default_api_url;
}
</script>
<Dialog bind:dialogOpen>
<svelte:fragment slot="icon">
<Cog size="24px" color="var(--surface-six)" />
</svelte:fragment>
<svelte:fragment slot="title">Settings</svelte:fragment>
<div id="settings-content">
<p>Configure the API for this website.</p>
<div class="input-wrapper">
<input name="api-url" id="api-url" type="text" bind:value={url} />
<button id="button-reset" on:click={reset} aria-label="Reset Button">
<Replay size="24px" color="var(--surface-six)" />
</button>
</div>
</div>
<svelte:fragment slot="buttons">
<div class="buttons-container">
<Button
type="text"
disabled={$admin_login.logged_in}
on:click={() => ((loginOpen = !loginOpen), (dialogOpen = !dialogOpen))}
>
{$admin_login.logged_in ? `Logged in for ${fromNow($admin_login.expires)}` : 'Login'}
</Button>
<div class="buttons">
<Button type="text" on:click={clear_and_reload} label="Reset Button">Reset</Button>
<Button type="text" on:click={save} label="Save Button">Save</Button>
</div>
</div>
</svelte:fragment>
</Dialog>
<style lang="scss">
input {
width: 100%;
position: relative;
padding-right: 3rem;
margin-top: 1rem;
}
#button-reset {
position: absolute;
right: 12px;
top: 30px;
}
button {
background-color: transparent;
border: none;
cursor: pointer;
display: flex;
justify-content: center;
align-items: center;
}
.input-wrapper {
margin-bottom: 0.75rem;
display: flex;
justify-content: space-between;
position: relative;
}
.buttons-container {
width: 100%;
display: flex;
justify-content: space-between;
flex-wrap: wrap;
gap: 1rem;
.buttons {
display: flex;
justify-content: flex-end;
flex-wrap: wrap;
gap: 2rem;
}
}
</style>

View File

@@ -1,193 +0,0 @@
<script lang="ts">
import { fly } from 'svelte/transition';
import { quintOut } from 'svelte/easing';
import { queries } from '$data/api';
import { createQuery } from '@tanstack/svelte-query';
import Query from '$lib/components/Query.svelte';
import FooterSection from './FooterSection.svelte';
import { RV_DMCA_GUID } from '$env/static/public';
import { onMount } from 'svelte';
import Divider from '$lib/components/Divider.svelte';
import Button from '$lib/components/Button.svelte';
import EmailDialog from '$layout/Dialogs/EmailDialog.svelte';
const aboutQuery = createQuery(queries.about());
let location: string;
let showEmailDialog = false;
onMount(() => {
// DMCA Protection Badge
location = document.location.href;
});
</script>
<EmailDialog bind:showEmailDialog />
<Divider horizontalPadding={'15px'} />
<footer in:fly={{ y: 10, easing: quintOut, duration: 750 }}>
<div class="top">
<section class="main-content">
<img src="/logo.svg" class="logo-image" alt="ReVanced Logo" />
<Query query={aboutQuery} let:data>
<div>
<p>
{data.about.about}
</p>
</div>
</Query>
</section>
<section class="links-container">
<FooterSection title="Pages">
<li><a href="/">Home</a></li>
<li><a href="/download">Download</a></li>
<li><a href="/patches">Patches</a></li>
<li><a href="/contributors">Contributors</a></li>
<li><a href="/donate">Donate</a></li>
</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="bottom">
<div id="logo-name"><span>Re</span>Vanced</div>
<Button type="text" style="color: var(--text-four); font-weight: 600;">
<a href="/donate">Donate</a>
</Button>
<Button
type="text"
style="color: var(--text-four); font-weight: 600;"
on:click={() => (showEmailDialog = true)}
>
E-Mail
</Button>
<!-- DMCA Protection Badge -->
<a
href="//www.dmca.com/Protection/Status.aspx?ID={RV_DMCA_GUID}&refurl={location}"
title="DMCA.com Protection Status"
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>
</footer>
<style lang="scss">
footer {
max-width: min(87%, 100rem);
padding: 5rem 0rem;
margin: 0 auto;
.top {
display: flex;
gap: 8rem;
justify-content: space-between;
margin-bottom: 4rem;
@media (max-width: 1050px) {
flex-direction: column;
gap: 2rem;
}
}
.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 {
display: flex;
align-items: center;
}
#logo-name {
font-size: 1.4rem;
color: var(--text-one);
font-weight: 600;
span {
color: var(--primary);
}
}
li {
list-style: none;
color: var(--text-four);
font-size: 0.9rem;
font-weight: 500;
a {
color: var(--primary);
font-weight: 600;
font-size: 0.95rem;
}
}
.main-content {
display: flex;
flex-direction: column;
gap: 1.5rem;
align-items: flex-start;
}
.logo-image {
height: 2.5rem;
}
a {
text-decoration: none;
&:hover {
text-decoration: underline var(--secondary);
color: var(--text-one);
}
}
.links-container {
display: flex;
gap: 10rem;
margin-top: 1rem;
@media (max-width: 1050px) {
display: grid;
gap: 2rem;
grid-template-columns: repeat(auto-fit, minmax(80px, 1fr));
}
@media (max-width: 768px) {
display: flex;
flex-direction: column;
gap: initial;
}
}
</style>

View File

@@ -1,35 +0,0 @@
<script lang="ts">
export let title: string;
</script>
<div>
<span>
{title}
</span>
<ul>
<slot />
</ul>
</div>
<style lang="scss">
span {
display: flex;
margin: 1.5rem 0;
font-size: 0.9rem;
font-weight: 600;
color: var(--text-four);
}
ul {
display: flex;
flex-direction: column;
margin: 1.25rem 0rem;
gap: 1rem;
}
@media (max-width: 768px) {
div:not(:last-child) {
border-bottom: 1px solid var(--border);
}
}
</style>

View File

@@ -1,23 +0,0 @@
<script>
import Picture from '$lib/components/Picture.svelte';
import manager_screenshot from '$images/manager.png?w=1233;822;411&format=avif;webp;png&as=picture';
</script>
<div class="hero-img">
<Picture data={manager_screenshot} alt="Screenshot of ReVanced Manager" />
</div>
<style lang="scss">
.hero-img {
height: max(100vh, 600px);
padding: 0.5rem 0.5rem;
border-radius: 1.75rem;
background-color: var(--surface-seven);
user-select: none;
:global(img) {
height: 100%;
border-radius: 1.75rem;
}
}
</style>

View File

@@ -1,92 +0,0 @@
<script>
import { queries } from '$data/api';
import { createQuery } from '@tanstack/svelte-query';
import Query from '$lib/components/Query.svelte';
import TrayArrowDown from 'svelte-material-icons/TrayArrowDown.svelte';
import FileDocumentOutline from 'svelte-material-icons/FileDocumentOutline.svelte';
import Button from '$lib/components/Button.svelte';
import SocialButton from './SocialButton.svelte';
const aboutQuery = createQuery(queries.about());
export let socialsVisibility = true;
</script>
<section class="hero">
<h1>Continuing the <br />legacy of <span>Vanced.</span></h1>
<p>
Customize your mobile experience through ReVanced <br /> by applying patches to your applications.
</p>
<div class="buttons-container">
<div class="hero-buttons internal-buttons">
<Button type="filled" icon={TrayArrowDown} href="download">Download</Button>
<Button type="tonal" icon={FileDocumentOutline} href="patches">View patches</Button>
</div>
<div class="hero-buttons social-buttons" style:opacity={socialsVisibility ? '100%' : '0'}>
<Query query={aboutQuery} let:data>
{#if data}
{#each data.about.socials.filter((s) => s.name != 'Website') as social}
<SocialButton {social} />
{/each}
{/if}
</Query>
</div>
</div>
</section>
<style lang="scss">
.hero {
display: flex;
flex-direction: column;
gap: 1rem;
@media (min-width: 1100px) {
padding-top: 10vh;
}
h1 {
color: var(--text-one);
}
span {
color: var(--primary);
}
.buttons-container {
display: flex;
flex-direction: column;
gap: 2.5rem;
.social-buttons {
max-width: 30rem;
position: absolute;
bottom: 1rem;
transition: opacity 0.1s var(--bezier-one);
@media (max-width: 450px) {
justify-content: center;
left: 0;
}
@media (max-height: 600px), (max-width: 450px) and (max-height: 780px) {
position: static;
opacity: 100% !important;
}
}
.hero-buttons {
flex-wrap: wrap;
display: flex;
user-select: none;
gap: 1rem;
@media (max-width: 450px) {
&.internal-buttons {
flex-direction: column;
gap: 1rem;
}
}
}
}
}
</style>

View File

@@ -1,40 +0,0 @@
<script lang="ts">
import type { Social } from '$lib/types';
export let social: Social;
</script>
<a href={social.url} rel="noreferrer" target="_blank">
<img src="socials/{social.name.toLowerCase()}.svg" alt={social.name} />
</a>
<style lang="scss">
a {
display: flex;
justify-content: center;
border: 0;
width: 60px;
height: 60px;
padding: 14px;
cursor: pointer;
border-radius: 200px;
transition: transform 0.4s var(--bezier-one);
background-color: var(--surface-four);
color: var(--text-one);
user-select: none;
&:hover {
transform: translateY(-5%);
}
img {
transition: filter 0.4s var(--bezier-one);
width: 30px;
}
&:hover img {
filter: brightness(1.2);
}
}
</style>

View File

@@ -1,96 +0,0 @@
<script lang="ts">
import { queries } from '$data/api';
import { dev_log } from '$util/dev';
import RouterEvents from '$data/RouterEvents';
import { useQueryClient } from '@tanstack/svelte-query';
const client = useQueryClient();
export let href: string;
export let queryKey: null | keyof typeof queries | Array<keyof typeof queries> = null;
export let label: string;
function prefetch() {
if (queryKey !== null) {
if (Array.isArray(queryKey)) {
queryKey.forEach((key) => {
const query = (queries[key] as Function)();
dev_log('Prefetching', query);
client.prefetchQuery(query as any);
});
} else {
const query = (queries[queryKey] as Function)();
dev_log('Prefetching', query);
client.prefetchQuery(query as any);
}
}
}
</script>
<li
class:selected={href === '/' + $RouterEvents.target_url.pathname.split('/')[1]}
class:unclickable={$RouterEvents.target_url.pathname === href}
>
<a data-sveltekit-preload-data on:mouseenter={prefetch} {href} aria-label={label}>
<span><slot /></span>
</a>
</li>
<style lang="scss">
li {
list-style: none;
position: relative;
transition-timing-function: var(--bezier-one);
transition-duration: 0.25s;
border-radius: 10px;
&.selected {
background-color: var(--tertiary);
color: var(--primary);
span {
color: var(--primary);
}
}
&.unclickable {
pointer-events: none;
}
:hover {
color: var(--text-one);
background-color: var(--surface-three);
}
}
a {
text-decoration: none;
user-select: none;
border-radius: 10px;
display: flex;
align-items: center;
justify-content: center;
padding: 10px 16px;
}
span {
display: flex;
justify-content: center;
font-weight: 400;
font-size: 0.9rem;
letter-spacing: 0.02rem;
color: var(--text-four);
}
@media (max-width: 768px) {
a {
padding: 0.75rem 1.25rem;
justify-content: left;
}
span {
font-size: 1rem;
font-weight: 500;
}
}
</style>

View File

@@ -1,359 +0,0 @@
<script lang="ts">
import { onMount } from 'svelte';
import { horizontalSlide } from '$util/horizontalSlide';
import { fade } from 'svelte/transition';
import { expoOut } from 'svelte/easing';
import { createQuery } from '@tanstack/svelte-query';
import Navigation from '$layout/Navbar/NavButton.svelte';
import Query from '$lib/components/Query.svelte';
import AnnouncementBanner from '$layout/Banners/AnnouncementBanner.svelte';
import Cog from 'svelte-material-icons/Cog.svelte';
import { status_url } from '$data/api/settings';
import RouterEvents from '$data/RouterEvents';
import { queries } from '$data/api';
import StatusBanner from '$layout/Banners/StatusBanner.svelte';
import SettingsDialog from '$layout/Dialogs/SettingsDialog.svelte';
import LoginDialog from '$layout/Dialogs/LoginDialog.svelte';
import LoginSuccessfulDialog from '$layout/Dialogs/LoginSuccessfulDialog.svelte';
import SessionExpiredDialog from '$layout/Dialogs/SessionExpiredDialog.svelte';
const ping = createQuery(queries.ping());
const statusUrl = status_url();
let menuOpen = false;
const dialogs: Record<string, boolean> = {
settings: false,
login: false
};
let scrollY: number;
onMount(() => {
return RouterEvents.subscribe((event) => {
if (event.navigating) menuOpen = false;
});
});
</script>
<svelte:window bind:scrollY />
<span class="banner" class:hide={menuOpen}>
<Query query={ping} let:data>
{#if !data}
<StatusBanner {statusUrl} />
{/if}
</Query>
<AnnouncementBanner />
</span>
<nav class:scrolled={scrollY > 10}>
<a class="menu-btn skiptab-btn" href="#skiptab">Skip navigation</a>
<button
class="menu-btn mobile-only"
on:click={() => (menuOpen = !menuOpen)}
class:open={menuOpen}
aria-label="Menu"
>
<span class="menu-btn__burger" />
</button>
<a href="/" id="logo"><img src="/logo.svg" alt="ReVanced Logo" /></a>
{#key menuOpen}
<div
id="nav-wrapper-container"
class:desktop-only={!menuOpen}
transition:horizontalSlide={{ direction: 'inline', easing: expoOut, duration: 400 }}
>
<div class="nav-wrapper">
<div id="main-navigation">
<ul class="nav-buttons">
<Navigation href="/" label="Home">Home</Navigation>
<Navigation queryKey="manager" href="/download" label="Download">Download</Navigation>
<Navigation queryKey="patches" href="/patches" label="Patches">Patches</Navigation>
<span class="mobile-only">
<Navigation
queryKey={['announcements', 'announcementTags']}
href="/announcements"
label="Announcements"
>
Announcements
</Navigation>
</span>
<Navigation queryKey="contributors" href="/contributors" label="Contributors">
Contributors
</Navigation>
<Navigation queryKey={['about', 'team']} href="/donate" label="Donate"
>Donate</Navigation
>
</ul>
</div>
<div id="secondary-navigation">
<span class="desktop-only">
<Navigation
queryKey={['announcements', 'announcementTags']}
href="/announcements"
label="Announcements"
>
<svg
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>
{/key}
{#if menuOpen}
<!-- svelte-ignore a11y-no-static-element-interactions -->
<div
class="overlay mobile-only"
transition:fade={{ duration: 350 }}
on:click={() => (menuOpen = !menuOpen)}
on:keypress={() => (menuOpen = !menuOpen)}
/>
{/if}
</nav>
<SettingsDialog bind:dialogOpen={dialogs.settings} bind:loginOpen={dialogs.login} />
<LoginDialog bind:dialogOpen={dialogs.login} />
<LoginSuccessfulDialog />
<SessionExpiredDialog bind:loginOpen={dialogs.login} />
<style lang="scss">
#secondary-navigation {
display: flex;
gap: 1rem;
button {
border-radius: 10px;
padding: 10px 16px;
&:hover {
background-color: var(--surface-three);
}
&.selected {
background-color: var(--tertiary);
}
}
}
#logo {
padding: 0.5rem;
}
button {
background-color: transparent;
border: none;
cursor: pointer;
display: flex;
justify-content: center;
align-items: center;
}
nav {
position: sticky;
top: 0;
z-index: 5;
display: flex;
align-items: center;
justify-content: space-between;
gap: 2rem;
height: 70px;
padding: 1rem 2rem;
width: 100%;
background-color: var(--surface-eight);
}
img {
height: 22px;
}
.nav-buttons {
display: flex;
gap: 1rem;
}
.scrolled {
box-shadow: var(--drop-shadow-one);
}
.overlay {
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
background-color: rgba(0, 0, 0, 0.5);
z-index: 3;
}
.nav-wrapper {
display: flex;
width: 100%;
justify-content: space-between;
}
@media (min-width: 768px) {
.nav-wrapper {
align-items: center;
}
}
.banner.hide {
display: none;
}
#nav-wrapper-container {
width: 100%;
}
@media (max-width: 768px) {
#nav-wrapper-container {
overflow: hidden;
position: fixed;
width: 20rem;
top: 0;
left: 0;
height: 100%;
background-color: var(--surface-eight);
z-index: 4;
}
.nav-wrapper {
flex-direction: column;
gap: 0.5rem;
height: 100%;
margin: 0 auto;
width: 20rem;
border-radius: 0px 24px 24px 0px;
padding: 1rem;
padding-top: 6rem;
}
.desktop-only {
display: none !important;
}
nav {
justify-content: normal;
}
.nav-buttons {
flex-direction: column;
gap: 0.5rem;
width: 100%;
}
#secondary-navigation {
z-index: 4;
padding: 16px;
}
}
@media (min-width: 768px) {
.mobile-only {
display: none !important;
}
}
/* Hamburger mmm yum */
.menu-btn {
user-select: none;
position: relative;
display: flex;
height: 50px;
z-index: 6;
justify-content: center;
align-items: center;
cursor: pointer;
&__burger {
display: flex;
flex-wrap: wrap;
&,
&::before,
&::after {
width: 24px;
height: 2px;
background: var(--surface-six);
transition: all 0.3s var(--bezier-one);
}
&::before,
&::after {
content: '';
position: absolute;
}
&::before {
transform: translateY(-6.5px);
}
&::after {
transform: translateY(6.5px);
}
}
/* ANIMATION */
&.open {
.menu-btn__burger {
transform: translateX(-10px);
background: transparent;
box-shadow: none;
&::before {
transform: rotate(45deg) translate(10px, -10px);
}
&::after {
transform: rotate(-45deg) translate(10px, 10px);
}
}
}
}
.skiptab-btn {
position: fixed;
left: -300px;
border-radius: 100px;
text-decoration: none;
background-color: var(--primary);
z-index: 2;
color: var(--text-three);
font-weight: 600;
font-size: 0.95rem;
padding: 16px 24px;
&:focus {
left: 12px;
}
}
</style>

View File

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="107" height="128" viewBox="0 0 107 128"><title>svelte-logo</title><path d="M94.157 22.819c-10.4-14.885-30.94-19.297-45.792-9.835L22.282 29.608A29.92 29.92 0 0 0 8.764 49.65a31.5 31.5 0 0 0 3.108 20.231 30 30 0 0 0-4.477 11.183 31.9 31.9 0 0 0 5.448 24.116c10.402 14.887 30.942 19.297 45.791 9.835l26.083-16.624A29.92 29.92 0 0 0 98.235 78.35a31.53 31.53 0 0 0-3.105-20.232 30 30 0 0 0 4.474-11.182 31.88 31.88 0 0 0-5.447-24.116" style="fill:#ff3e00"/><path d="M45.817 106.582a20.72 20.72 0 0 1-22.237-8.243 19.17 19.17 0 0 1-3.277-14.503 18 18 0 0 1 .624-2.435l.49-1.498 1.337.981a33.6 33.6 0 0 0 10.203 5.098l.97.294-.09.968a5.85 5.85 0 0 0 1.052 3.878 6.24 6.24 0 0 0 6.695 2.485 5.8 5.8 0 0 0 1.603-.704L69.27 76.28a5.43 5.43 0 0 0 2.45-3.631 5.8 5.8 0 0 0-.987-4.371 6.24 6.24 0 0 0-6.698-2.487 5.7 5.7 0 0 0-1.6.704l-9.953 6.345a19 19 0 0 1-5.296 2.326 20.72 20.72 0 0 1-22.237-8.243 19.17 19.17 0 0 1-3.277-14.502 17.99 17.99 0 0 1 8.13-12.052l26.081-16.623a19 19 0 0 1 5.3-2.329 20.72 20.72 0 0 1 22.237 8.243 19.17 19.17 0 0 1 3.277 14.503 18 18 0 0 1-.624 2.435l-.49 1.498-1.337-.98a33.6 33.6 0 0 0-10.203-5.1l-.97-.294.09-.968a5.86 5.86 0 0 0-1.052-3.878 6.24 6.24 0 0 0-6.696-2.485 5.8 5.8 0 0 0-1.602.704L37.73 51.72a5.42 5.42 0 0 0-2.449 3.63 5.79 5.79 0 0 0 .986 4.372 6.24 6.24 0 0 0 6.698 2.486 5.8 5.8 0 0 0 1.602-.704l9.952-6.342a19 19 0 0 1 5.295-2.328 20.72 20.72 0 0 1 22.237 8.242 19.17 19.17 0 0 1 3.277 14.503 18 18 0 0 1-8.13 12.053l-26.081 16.622a19 19 0 0 1-5.3 2.328" style="fill:#fff"/></svg>

After

Width:  |  Height:  |  Size: 1.5 KiB

View File

@@ -1,134 +0,0 @@
import { browser } from '$app/environment';
import { build_url } from '$data/api';
export type AuthToken = {
token: string;
expires: number;
};
type JwtPayload = {
exp: number;
iss: string;
iat: number;
};
export class UnauthenticatedError extends Error {
constructor() {
super('Unauthenticated. Cannot perform admin operations.');
}
}
// Get access token.
export function get_access_token(): AuthToken | null {
if (!browser) return null;
const data = localStorage.getItem('revanced_api_access_token');
if (data) return JSON.parse(data) as AuthToken;
return null;
}
// (Re)set access token.
export function set_access_token(token?: AuthToken) {
if (!token) localStorage.removeItem('revanced_api_access_token');
else localStorage.setItem('revanced_api_access_token', JSON.stringify(token));
}
// Parse a JWT token
export function parseJwt(token: string): JwtPayload {
const base64Url = token.split('.')[1];
const base64 = base64Url.replace(/-/g, '+').replace(/_/g, '/');
const jsonPayload = decodeURIComponent(
atob(base64)
.split('')
.map((c) => '%' + ('00' + c.charCodeAt(0).toString(16)).slice(-2))
.join('')
);
return JSON.parse(jsonPayload) as JwtPayload;
}
// Check if the admin is authenticated
export function is_logged_in(): boolean {
const token = get_access_token();
if (!token) return false;
return Date.now() < token.expires;
}
async function digest_fetch(
url: string,
username: string,
password: string,
options: RequestInit = {}
): Promise<Response> {
// Helper function to convert ArrayBuffer to Hex string
function bufferToHex(buffer: ArrayBuffer): string {
return Array.from(new Uint8Array(buffer))
.map((b) => b.toString(16).padStart(2, '0'))
.join('');
}
// Generate SHA-256 digest
async function sha256(message: string): Promise<string> {
const encoder = new TextEncoder();
const data = encoder.encode(message);
const hashBuffer = await crypto.subtle.digest('SHA-256', data);
return bufferToHex(hashBuffer);
}
// Perform an initial request to get the `WWW-Authenticate` header
const initialResponse = await fetch(url, {
method: options.method || 'GET',
headers: options.headers || {}
});
if (!initialResponse.ok && initialResponse.status !== 401)
throw new Error(`Initial request failed with status: ${initialResponse.status}`);
if (initialResponse.ok && initialResponse.status === 200) return initialResponse;
const authHeader = initialResponse.headers.get('Www-Authenticate');
if (!authHeader || !authHeader.startsWith('Digest '))
throw new Error('No Digest authentication header found');
// Parse the `WWW-Authenticate` header to extract the fields
const authParams = authHeader
.replace('Digest ', '')
.split(',')
.reduce((acc: Record<string, string>, item) => {
const [key, value] = item.trim().split('=');
acc[key] = value.replace(/"/g, '');
return acc;
}, {});
const { realm, nonce, algorithm } = authParams;
const method = options.method || 'GET';
const uri = new URL(url).pathname;
// https://ktor.io/docs/server-digest-auth.html#flow
const HA1 = await sha256(`${username}:${realm}:${password}`);
const HA2 = await sha256(`${method}:${uri}`);
const responseHash = await sha256(`${HA1}:${nonce}:${HA2}`);
// Build the Authorization header
const authHeaderDigest = `Digest username="${username}", realm="${realm}", nonce="${nonce}", uri="${uri}", algorithm=${algorithm}, response="${responseHash}"`;
// Perform the final request with the Authorization header
const finalResponse = await fetch(url, {
...options,
headers: {
...options.headers,
Authorization: authHeaderDigest
}
});
return finalResponse;
}
export async function login(username: string, password: string) {
const res = await digest_fetch(build_url('token'), username, password);
if (!res.ok) return false;
const data = await res.json();
const payload = parseJwt(data.token);
set_access_token({ token: data.token, expires: payload.exp * 1000 });
return true;
}

View File

@@ -1,112 +0,0 @@
<script lang="ts">
import ToolTip from './ToolTip.svelte';
export let type: 'filled' | 'tonal' | 'text' | 'outlined' | 'icon' = 'filled';
export let variant: 'default' | 'danger' | 'onDangerBackground' = 'default';
export let functionType: typeof HTMLButtonElement.prototype.type = 'button';
export let icon: any | undefined = undefined;
export let iconSize = 20;
export let iconColor = 'currentColor';
export let href: string = '';
export let target: string = '';
export let label: string = '';
export let disabled: boolean = false;
export let toolTipText: string | undefined = undefined;
export let style: string = '';
$: type = $$slots.default ? type : 'icon';
</script>
<ToolTip content={toolTipText} html={false}>
{#if href}
<a {href} {target} aria-label={label} class={`${type} ${variant}`} class:disabled>
<svelte:component this={icon} size={iconSize} color={iconColor} />
<slot />
</a>
{:else}
<button
on:click
class={`${type} ${variant}`}
style="{style}"
class:disabled
aria-label={label}
type={functionType}
{disabled}
>
<svelte:component this={icon} size={iconSize} color={iconColor} />
<slot />
</button>
{/if}
</ToolTip>
<style lang="scss">
a,
button {
min-width: max-content;
font-size: 0.95rem;
text-decoration: none;
color: var(--text-one);
font-weight: 600;
border: none;
border-radius: 100px;
display: flex;
justify-content: center;
align-items: center;
gap: 0.5rem;
cursor: pointer;
transition:
transform 0.4s var(--bezier-one),
filter 0.4s var(--bezier-one);
user-select: none;
padding: 16px 24px;
&:hover:not(.disabled) {
filter: brightness(85%);
}
&.disabled {
filter: grayscale(100%);
cursor: not-allowed;
}
&.filled {
background-color: var(--primary);
color: var(--text-three);
}
&.tonal {
background-color: var(--surface-four);
}
&.text {
background-color: transparent;
color: var(--primary);
font-weight: 500;
padding: 0;
}
&.outlined {
border: 2px solid var(--primary);
background-color: transparent;
}
&.icon {
&:hover {
filter: brightness(75%);
}
background-color: transparent;
color: currentColor;
padding: 0;
}
&.danger {
background-color: var(--red-one);
color: var(--surface-four);
}
&.onDangerBackground {
background-color: #ffd3d3;
color: #601410;
}
}
</style>

View File

@@ -1,30 +0,0 @@
<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">
<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"
stroke-linecap="square"
/>
</pattern>
<rect width="100%" height="100%" fill="url(#a)" />
</svg>
<style lang="scss">
svg {
margin: 1.5rem 0;
path {
stroke: var(--border);
}
}
</style>

View File

@@ -1,42 +0,0 @@
<script>
import Check from 'svelte-material-icons/Check.svelte';
import ChevronDown from 'svelte-material-icons/ChevronDown.svelte';
export let dropdown = false;
export let check = false;
export let selected = false;
</script>
<button class:selected on:click>
{#if check}
<Check size="18px" color="var(--surface-six)" />
{/if}
<slot />
{#if dropdown}
<ChevronDown size="18px" color="var(--surface-six)" />
{/if}
</button>
<style lang="scss">
button {
font-family: var(--font-two);
border: none;
border: 1.5px solid var(--border);
background-color: transparent;
color: var(--text-four);
height: 32px;
padding: 0 16px;
border-radius: 8px;
font-size: 0.85rem;
cursor: pointer;
display: flex;
align-items: center;
gap: 8px;
&.selected {
background-color: var(--tertiary);
color: var(--primary);
}
}
</style>

View File

@@ -1,82 +0,0 @@
<script lang="ts">
import ImageDialog from '$layout/Dialogs/ImageDialog.svelte';
export let images: string[];
export let columns: number = 3;
export let gap: string = '1rem';
let selectedImage: { src: string; alt: string } | null = null;
function openDialog(image: string, index: number) {
selectedImage = {
src: image,
alt: `Gallery image ${index + 1}`
};
}
function closeDialog() {
selectedImage = null;
}
</script>
<div class="gallery" style="--columns: {columns}; --gap: {gap}">
{#each images as image, i}
<!-- svelte-ignore a11y-no-noninteractive-element-interactions -->
<div class="image-container">
<!-- svelte-ignore a11y-no-noninteractive-tabindex -->
<img
src={image}
alt={`Gallery image ${i + 1}`}
loading="lazy"
on:click={() => openDialog(image, i)}
on:keydown={(e) => e.key === 'Enter' && openDialog(image, i)}
tabindex="0"
/>
</div>
{/each}
</div>
{#if selectedImage}
<ImageDialog src={selectedImage.src} alt={selectedImage.alt} on:close={closeDialog} />
{/if}
<style>
.gallery {
display: grid;
grid-template-columns: repeat(var(--columns), 1fr);
gap: var(--gap);
width: 100%;
padding: 1rem;
}
.image-container {
aspect-ratio: 4 / 3;
overflow: hidden;
border-radius: 8px;
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
}
img {
width: 100%;
height: 100%;
object-fit: cover;
transition: transform 0.3s ease;
cursor: pointer;
}
img:hover {
transform: scale(1.05);
}
@media (max-width: 768px) {
.gallery {
--columns: 2;
}
}
@media (max-width: 480px) {
.gallery {
--columns: 1;
}
}
</style>

View File

@@ -1,32 +0,0 @@
<script lang="ts">
import { JsonLd } from 'svelte-meta-tags';
let _title: string = '';
$: title = _title === '' ? 'ReVanced' : `ReVanced - ${_title}`;
export { _title as title };
export let description: string = 'Continuing the legacy of Vanced.';
export let schemas: any[] | undefined;
</script>
<svelte:head>
<title>{title}</title>
<meta name="description" content={description} />
<meta name="theme-color" content="#9FD5FF" />
<!-- OpenGraph -->
<meta property="og:title" content={title} />
<meta property="og:description" content={description} />
<!-- Twitter -->
<meta name="twitter:title" content={title} />
<meta name="twitter:description" content={description} />
{#if schemas}
{#each schemas as schema}
<JsonLd {schema} />
{/each}
{/if}
</svelte:head>

View File

@@ -1,65 +0,0 @@
<script lang="ts">
export let placeholder: string;
export let required: boolean = false;
export let value: any = '';
export let type: string = 'text';
export let onenter: () => void = () => {};
export let onexit: () => void = () => {};
export let oninput: () => void = () => {};
export let onkeydown: (event: KeyboardEvent) => void = (event) => {};
const set_type = (node: HTMLInputElement) => {
node.type = type;
};
</script>
<div class="input-wrapper">
<input
id={placeholder.toLowerCase()}
name={placeholder.toLowerCase()}
{required}
use:set_type
on:focus={onenter}
on:blur={onexit}
on:input={oninput}
on:keydown={onkeydown}
bind:value
/>
<label for={placeholder.toLowerCase()}>{placeholder}</label>
</div>
<style lang="scss">
.input-wrapper {
width: auto;
height: auto;
position: relative;
label {
position: absolute;
top: 29%;
left: 1rem;
transition: all 0.2s ease-in-out;
color: var(--surface-six);
pointer-events: none;
padding: 0;
margin: 0;
font-size: 1rem;
}
input {
font-size: 1rem;
width: 100%;
height: 100%;
&:focus + label,
&:valid + label {
top: -0.65rem;
font-size: 0.85rem;
background-color: var(--surface-seven);
color: var(--text-one);
padding: 0 0.3rem;
}
}
}
</style>

View File

@@ -1,13 +0,0 @@
<script lang="ts">
// See: https://github.com/JonasKruckenberg/imagetools/blob/main/docs/directives.md#picture
import type { Picture } from 'vite-imagetools';
export let data: Picture;
export let alt: string;
</script>
<picture>
{#each Object.entries(data.sources) as [format, srcset]}
<source {srcset} type="image/{format}" />
{/each}
<img {alt} src={data.img.src} />
</picture>

View File

@@ -1,25 +0,0 @@
<script lang="ts">
import { onMount } from 'svelte';
import QRious from 'qrious/dist/qrious';
export let codeValue: string;
export let squareSize: number = 150;
onMount(() => {
new QRious({
element: document.getElementById('qrcode'),
value: codeValue,
size: squareSize
});
});
</script>
<canvas id="qrcode" />
<style>
canvas {
border-radius: 0.5rem;
background-color: white;
padding: 0.25rem;
}
</style>

View File

@@ -1,27 +0,0 @@
<script lang="ts">
import type { CreateQueryResult } from '@tanstack/svelte-query';
import { isRestoring } from '../../routes/+layout.svelte';
// I might try to get this merged into tanstack query.
// So basically, this is how you do generics here.
//https://github.com/sveltejs/language-tools/issues/273#issuecomment-1003496094
type T = $$Generic;
interface $$Slots {
default: {
// slot name
data: T;
};
}
// TODO: errors
export let query: CreateQueryResult<T, any>;
</script>
{#if !$isRestoring}
{#if $query.isSuccess}
<slot data={$query.data} />
{:else if $query.isError}
<slot name="error" />
{/if}
{/if}

View File

@@ -1,102 +0,0 @@
<script lang="ts">
import { fade } from 'svelte/transition';
import { quintOut } from 'svelte/easing';
import { page } from '$app/stores';
import { goto } from '$app/navigation';
import Close from 'svelte-material-icons/Close.svelte';
import Magnify from 'svelte-material-icons/Magnify.svelte';
export let title: string;
export let searchTerm: string | null;
export let displayedTerm: string | undefined;
function clear() {
searchTerm = '';
displayedTerm = '';
const url = new URL($page.url);
url.searchParams.delete('s');
goto(url.pathname + url.search);
}
</script>
<div class="search-container">
<div id="search">
<Magnify size="24px" color="var(--surface-six)" />
</div>
{#if searchTerm}
<div
id="clear"
on:click={clear}
on:keypress={clear}
transition:fade={{ easing: quintOut, duration: 250 }}
>
<Close size="24px" color="var(--surface-six)" />
</div>
{/if}
<input
type="text"
class:clear={searchTerm}
placeholder={title}
bind:value={searchTerm}
on:keyup
/>
</div>
<style lang="scss">
#search {
/* umm dont ask */
position: absolute;
z-index: 1;
left: 16px;
top: 14px;
height: 24px;
}
#clear {
position: absolute;
right: 16px;
top: 14px;
z-index: 1;
height: 24px;
cursor: pointer;
}
.search-container {
position: relative;
}
input {
position: relative;
display: flex;
padding: 1rem 3.25rem;
width: 100%;
color: var(--secondary);
font-weight: 500;
font-size: 0.92rem;
border-radius: 100px;
border: none;
background-color: var(--surface-nine);
outline: none;
transition: background-color 0.3s var(--bezier-one);
&:hover {
background-color: var(--surface-five);
}
&:focus::placeholder {
color: var(--primary);
}
&:focus {
background-color: var(--surface-two);
}
}
input::placeholder {
color: var(--text-four);
font-size: 0.9rem;
font-weight: 500;
transition: all 0.2s var(--bezier-one);
}
</style>

View File

@@ -1,44 +0,0 @@
<script lang="ts">
import { backIn, expoOut } from 'svelte/easing';
import { slide } from 'svelte/transition';
export let open = false;
export let dismissTime = 3000;
let timeout: ReturnType<typeof setTimeout>;
$: if (open) {
clearTimeout(timeout);
timeout = setTimeout(() => (open = false), dismissTime);
}
</script>
{#if open}
<div id="snackbar" in:slide={{ duration: 400, easing: expoOut }} out:slide={{ duration: 300, easing: backIn }}>
<slot name="text" />
</div>
{/if}
<style>
#snackbar {
display: flex;
align-items: center;
position: fixed;
bottom: 2rem;
left: 2rem;
padding: 0.5rem 1rem;
border-radius: 0.25rem;
min-width: 12.5rem;
max-width: 35rem;
height: 3rem;
background: var(--surface-one);
color: var(--text-two);
box-shadow: var(--drop-shadow-one);
font-size: 14px;
font-weight: 500;
z-index: 10;
}
</style>

View File

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

View File

@@ -1,16 +0,0 @@
<script lang="ts">
export let viewBoxHeight: number;
export let viewBoxWidth = viewBoxHeight;
export let svgHeight: number;
export let svgWidth = svgHeight;
</script>
<svg
xmlns="http://www.w3.org/2000/svg"
preserveAspectRatio="none"
viewBox="0 0 {viewBoxHeight} {viewBoxWidth}"
style:height={svgHeight + 'px'}
style:width={svgWidth + 'px'}
>
<slot />
</svg>

View File

@@ -1,33 +0,0 @@
<script lang="ts">
import { tooltip } from 'svooltip';
import '../styles/ToolTip.scss';
export let content: string | undefined;
export let html: boolean = false;
</script>
{#if content}
<div
use:tooltip={{
content: content,
html: html
}}
>
<slot />
</div>
{:else}
<slot />
{/if}
<style>
:root {
--svooltip-bg: var(--surface-three);
--svooltip-text: var(--text-four);
--svooltip-padding: 0.75rem 1rem;
--svooltip-weight: bold;
--svooltip-text-size: 16px;
--svooltip-shadow: var(--drop-shadow-one);
--svooltip-arrow-size: 0;
--svooltip-roundness: 12px;
}
</style>

View File

@@ -1,64 +0,0 @@
<script>
export let visibility = true;
</script>
<svg
class="wave"
viewBox="0 0 1440 500"
xmlns="http://www.w3.org/2000/svg"
preserveAspectRatio="none"
style="opacity: {visibility ? '100%' : '0'}; height: {visibility ? '40vh' : '0px'}"
>
<path class="wave" />
</svg>
<style>
svg {
transition: opacity 0.1s var(--bezier-one);
position: absolute;
bottom: -1px;
z-index: -1;
width: 100%;
}
@media (max-height: 780px) {
svg {
opacity: 0 !important;
}
}
.wave {
animation: wave-anim 30s;
animation-timing-function: cubic-bezier(0.5, 0, 0.5, 1);
animation-iteration-count: infinite;
fill: var(--primary);
}
@keyframes wave-anim {
0% {
d: path(
'M0 500C0 500 0 250 0 250 176.5333 300.1333 353.0667 350.2667 496 325 638.9333 299.7333 748.2667 199.0667 900 174 1051.7333 148.9333 1245.8667 199.4667 1440 250 1440 250 1440 500 1440 500Z'
);
}
25% {
d: path(
'M0 500C0 500 0 250 0 250 154.1333 219.2 308.2667 188.4 449 209 589.7333 229.6 717.0667 301.6 880 317 1042.9333 332.4 1241.4667 291.2 1440 250 1440 250 1440 500 1440 500Z'
);
}
50% {
d: path(
'M0 500C0 500 0 250 0 250 132.8 242.9333 265.6 235.8667 414 246 562.4 256.1333 726.4 283.4667 900 287 1073.6 290.5333 1256.8 270.2667 1440 250 1440 250 1440 500 1440 500Z'
);
}
75% {
d: path(
'M0 500C0 500 0 250 0 250 151.3333 206.6667 302.6667 163.3333 472 176 641.3333 188.6667 828.6667 257.3333 993 279 1157.3333 300.6667 1298.6667 275.3333 1440 250 1440 250 1440 500 1440 500Z'
);
}
100% {
d: path(
'M0 500C0 500 0 250 0 250 176.5333 300.1333 353.0667 350.2667 496 325 638.9333 299.7333 748.2667 199.0667 900 174 1051.7333 148.9333 1245.8667 199.4667 1440 250 1440 250 1440 500 1440 500Z'
);
}
}
</style>

1
src/lib/index.ts Normal file
View File

@@ -0,0 +1 @@
// place files you want to import through the `$lib` alias in this folder.

View File

@@ -1,74 +0,0 @@
import { readable, writable } from 'svelte/store';
import { is_logged_in, get_access_token } from './auth';
import { browser } from '$app/environment';
type AdminLoginInfo =
| {
logged_in: true;
expires: number;
logged_in_previously: boolean;
}
| {
logged_in: false;
expires: undefined;
logged_in_previously: boolean;
};
const admin_login_info = (): AdminLoginInfo => {
if (is_logged_in())
return {
logged_in: true,
expires: get_access_token()!.expires,
logged_in_previously: !!get_access_token()?.token
};
else
return {
logged_in: false,
expires: undefined,
logged_in_previously: !!get_access_token()?.token
};
};
export const admin_login = readable<AdminLoginInfo>(admin_login_info(), (set) => {
const checkLoginStatus = () => set(admin_login_info());
checkLoginStatus();
const interval = setInterval(checkLoginStatus, 100);
return () => clearInterval(interval);
});
export const read_announcements = writable<Set<number>>(new Set(), (set) => {
if (!browser) return;
const key = 'read_announcements';
const data = localStorage.getItem(key);
const parsedArray = data ? JSON.parse(data) : [];
const currentState = new Set(parsedArray);
const updateStoreState = () => {
set(currentState);
};
const handleLocalStorageUpdate = (e: StorageEvent) => {
if (e.key === key) updateStoreState();
};
window.addEventListener('storage', handleLocalStorageUpdate);
updateStoreState();
return () => {
window.removeEventListener('storage', handleLocalStorageUpdate);
localStorage.setItem(key, JSON.stringify(Array.from(currentState)));
};
});
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 allowAnalytics = writable(false);

View File

@@ -1,12 +0,0 @@
@use 'svooltip/styles.scss' as SvoolTip;
.svooltip a {
text-decoration: none;
color: var(--text-four);
pointer-events: all;
&:hover {
text-decoration: underline var(--secondary);
color: var(--secondary);
}
}

View File

@@ -1,107 +0,0 @@
export type ResponseAnnouncement = {
archived_at?: string;
attachments?: string[];
author?: string;
tags?: string[];
content?: string;
created_at: string;
id: number;
level?: number;
title: string;
};
export type Announcement = Omit<ResponseAnnouncement, 'id'>;
export type Tags = { name: string }[];
export interface Contributor {
name: string;
avatar_url: string;
url: string;
contributions: number;
}
export interface Contributable {
name: string;
url: string;
contributors: Contributor[];
}
export interface Patch {
name: string;
description: string;
use: boolean;
compatiblePackages: CompatiblePackage[] | null;
options: PatchOption[];
}
export interface CompatiblePackage {
name: string;
versions: string[] | null;
}
export interface PatchOption {
key: string;
title: string | null;
description: string;
required: boolean;
type: string;
default: any | null;
values: any[] | null;
}
export interface Release {
version: string;
created_at: string;
description: string;
download_url: string;
}
export interface TeamMember {
name: string;
avatar_url: string;
url: string;
bio?: string;
gpg_key: GpgKey;
}
export interface GpgKey {
id: string;
url: string;
}
export interface CryptoWallet {
network: string;
currency_code: string;
address: string;
preferred: boolean;
}
export interface DonationPlatform {
name: string;
url: string;
preferred: boolean;
}
export interface Social {
name: string;
url: string;
preferred: boolean;
}
interface Donations {
wallets: CryptoWallet[];
links: DonationPlatform[];
}
interface Contact {
email: string;
}
export interface About {
name: string;
about: string;
contact: Contact;
socials: Social[];
donations: Donations;
}

View File

@@ -1,36 +0,0 @@
<script lang="ts">
import Meta from '$lib/components/Head.svelte';
import { page } from '$app/stores';
import Button from '$lib/components/Button.svelte';
$: status = $page.status;
</script>
<Meta title={status.toString()} />
<section class="wrapper">
<h1>{status}</h1>
{#if status == 404}
<p>This page received a cease and desist letter from a multi-billion dollar tech company.</p>
<br />
<Button type="filled" href="/">Return home</Button>
{:else}
<p>
{$page.error?.message}
</p>
{/if}
</section>
<style>
section {
display: flex;
flex-direction: column;
width: fit-content;
text-align: center;
margin-top: 10rem;
}
h1 {
color: var(--primary);
}
</style>

View File

@@ -1,79 +1,11 @@
<script lang="ts" context="module">
import { writable } from 'svelte/store';
// There might be a better place to put this, but I am not entirely sure...
export const isRestoring = writable(false);
</script>
<script lang="ts">
import '../app.scss';
import { derived } from 'svelte/store';
import { onMount } from 'svelte';
import { browser } from '$app/environment';
import favicon from '$lib/assets/favicon.svg';
import { QueryClient } from '@tanstack/query-core';
import { persistQueryClient } from '@tanstack/query-persist-client-core';
import { QueryClientProvider } from '@tanstack/svelte-query';
import { createSyncStoragePersister } from '@tanstack/query-sync-storage-persister';
import { DateTriggerEventHandler } from 'datetrigger';
import NavHost from '$layout/Navbar/NavHost.svelte';
import Spinner from '$lib/components/Spinner.svelte';
import ConsentDialog from '$layout/Dialogs/ConsentDialog.svelte';
import { staleTime } from '$data/api';
import RouterEvents from '$data/RouterEvents';
import { events as themeEvents } from '$util/themeEvents';
import FooterHost from '$layout/Footer/FooterHost.svelte';
import { api_base_url, set_about_info } from '$data/api/settings';
const queryClient = new QueryClient({
defaultOptions: {
queries: {
enabled: browser,
cacheTime: staleTime
}
}
});
// Just like the set/clearInterval example found here: https://svelte.dev/docs#run-time-svelte-store-derived
const show_loading_animation = derived(
RouterEvents,
($event, set) => {
if ($event.navigating) {
// Wait 250 ms before showing the animation.
const timeout = setTimeout(() => set(true), 250);
return () => clearTimeout(timeout);
} else {
set(false);
}
},
false
);
onMount(() => {
set_about_info(api_base_url());
new DateTriggerEventHandler(themeEvents);
isRestoring.set(true);
const [unsubscribe, promise] = persistQueryClient({
queryClient,
persister: createSyncStoragePersister({ storage: localStorage })
});
promise.then(() => isRestoring.set(false));
return unsubscribe;
});
let { children } = $props();
</script>
<ConsentDialog />
<svelte:head>
<link rel="icon" href={favicon} />
</svelte:head>
<QueryClientProvider client={queryClient}>
<NavHost />
<div id="skiptab">
{#if $show_loading_animation}
<Spinner />
{:else}
<slot />
{/if}
</div>
<FooterHost />
</QueryClientProvider>
{@render children()}

View File

@@ -1 +0,0 @@
export const prerender = true;

View File

@@ -1,179 +1,2 @@
<script>
import HeroImage from '$layout/Hero/HeroImage.svelte';
import Home from '$layout/Hero/HeroSection.svelte';
import Head from '$lib/components/Head.svelte';
import Wave from '$lib/components/Wave.svelte';
import { onMount } from 'svelte';
let bottomVisibility = true;
const checkVisibility = () => {
const wave = document.querySelector('.wave');
bottomVisibility = !(wave && wave.getBoundingClientRect().bottom < window.innerHeight - 1);
};
onMount(() => {
checkVisibility(); // Initial check
});
</script>
<Head
schemas={[
{
'@context': 'https://schema.org',
'@type': 'Organization',
url: 'https://revanced.app/',
logo: 'https://revanced.app/logo.png'
},
{
'@context': 'https://schema.org',
'@type': 'BreadcrumbList',
itemListElement: [
{
'@type': 'ListItem',
position: 1,
name: 'Home',
item: 'https://revanced.app/'
}
]
},
{
'@context': 'https://schema.org',
'@type': 'FAQPage',
mainEntity: [
{
'@type': 'Question',
name: 'What is ReVanced?',
acceptedAnswer: {
'@type': 'Answer',
text: 'ReVanced is an <b>open-source patcher</b> for <b>Android apps</b>. With ReVanced we <b>continue the legacy of Vanced</b>.'
}
},
{
'@type': 'Question',
name: 'How to get ReVanced?',
acceptedAnswer: {
'@type': 'Answer',
text: 'You can follow <a href="https://github.com/revanced/revanced-manager/tree/main/docs">ReVanced Manager documentation</a> to use <b>ReVanced Manager</b> or the <a href="https://github.com/revanced/revanced-cli/tree/main/docs">ReVanced CLI documentation</a> to use <b>ReVanced CLI</b>.'
}
},
{
'@type': 'Question',
name: 'How does it work?',
acceptedAnswer: {
'@type': 'Answer',
text: 'ReVanced uses a technique called <b>patching</b>. It patches <b>your choice of an app</b> and adds <b>new features</b> to it. Thanks to the <b>modularity of ReVanced</b>, you can choose <b>any combination of features you want</b> to use.'
}
},
{
'@type': 'Question',
name: 'Does ReVanced support non-rooted devices?',
acceptedAnswer: {
'@type': 'Answer',
text: '<b>Yes</b>! ReVanced supports <b>non-root and rooted devices</b>.'
}
},
{
'@type': 'Question',
name: 'Is ReVanced affiliated with Vanced?',
acceptedAnswer: {
'@type': 'Answer',
text: 'ReVanced is <b>not affiliated</b> with Vanced.'
}
},
{
'@type': 'Question',
name: 'How can I help?',
acceptedAnswer: {
'@type': 'Answer',
text: 'Since we are an <b>open-source community</b> and depend on outside help, you can always check out our <a href="https://github.com/revanced">GitHub repositories</a> and <b>contribute to ReVanced</b> by creating an issue or pull requests.'
}
}
]
},
{
'@context': 'https://schema.org',
'@type': 'SoftwareApplication',
name: 'ReVanced Manager',
operatingSystem: 'ANDROID',
applicationCategory: 'UtilitiesApplication',
aggregateRating: {
'@type': 'AggregateRating',
ratingValue: '4.0',
ratingCount: '100'
},
offers: {
'@type': 'Offer',
price: '0',
priceCurrency: 'USD'
}
},
{
'@context': 'https://schema.org',
'@type': 'SoftwareApplication',
name: 'ReVanced CLI',
operatingSystem: 'All',
applicationCategory: 'UtilitiesApplication',
aggregateRating: {
'@type': 'AggregateRating',
ratingValue: '4.0',
ratingCount: '30'
},
offers: {
'@type': 'Offer',
price: '0',
priceCurrency: 'USD'
}
}
]}
/>
<svelte:window on:scroll={checkVisibility} on:resize={checkVisibility} />
<main class:visibility={!bottomVisibility}>
<div class="content">
<Home socialsVisibility={bottomVisibility} />
<div class="hero-img-container">
<HeroImage />
</div>
</div>
</main>
<Wave visibility={bottomVisibility} />
<style lang="scss">
.content {
display: flex;
align-items: flex-start;
justify-content: space-evenly;
width: min(87%, 80rem);
gap: 1rem;
}
main {
overflow: hidden;
padding: 5rem 0;
min-height: max(100vh, 600px);
display: flex;
flex-direction: column;
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 {
z-index: 0;
@media (max-width: 1100px) {
display: none;
}
}
</style>
<h1>Welcome to SvelteKit</h1>
<p>Visit <a href="https://svelte.dev/docs/kit">svelte.dev/docs/kit</a> to read the documentation</p>

View File

@@ -1,195 +0,0 @@
<script lang="ts">
import { createQuery } from '@tanstack/svelte-query';
import { derived, readable, type Readable } from 'svelte/store';
import { building } from '$app/environment';
import { page } from '$app/stores';
import { fly, slide } from 'svelte/transition';
import { quintIn, quintOut } from 'svelte/easing';
import Query from '$lib/components/Query.svelte';
import AnnouncementCard from './AnnouncementCard.svelte';
import { queries } from '$data/api';
import TagsHost from './TagsHost.svelte';
import Search from '$lib/components/Search.svelte';
import { onMount } from 'svelte';
import type { ResponseAnnouncement } from '$lib/types';
import { admin_login, read_announcements } from '$lib/stores';
import Button from '$lib/components/Button.svelte';
import moment from 'moment';
import { debounce } from '$util/debounce';
import createFilter from '$util/filter';
import ChevronDown from 'svelte-material-icons/ChevronDown.svelte';
import Create from 'svelte-material-icons/Plus.svelte';
let expanded = false;
const searchParams: Readable<URLSearchParams> = building
? readable(new URLSearchParams())
: derived(page, ($page) => $page.url.searchParams);
let searchTerm = $searchParams.get('s') || '';
let displayedTerm = '';
$: query = createQuery(queries.announcements());
$: tagsQuery = createQuery(queries.announcementTags());
$: selectedTags = $searchParams.getAll('tag');
const update = () => {
displayedTerm = searchTerm;
const url = new URL(window.location.href);
url.pathname = '/announcements';
searchTerm ? url.searchParams.set('s', searchTerm) : url.searchParams.delete('s');
};
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>
<div class="search">
<div class="search-contain">
<!-- Must bind both variables: we get searchTerm from the text input, -->
<div class="search-bar">
<Search
bind:searchTerm
bind:displayedTerm
title="Search for announcements"
on:keyup={debounce(update)}
/>
</div>
{#if $admin_login.logged_in}
<Button type="filled" icon={Create} href="/announcements/create">Create</Button>
{/if}
</div>
</div>
<main class="wrapper" in:fly={{ y: 10, easing: quintOut, duration: 750 }}>
<Query query={tagsQuery} let:data>
<TagsHost tags={data.tags} />
</Query>
<Query {query} let:data>
{#if activeAnnouncements(filterAnnouncements(data.announcements, displayedTerm, selectedTags)).length}
<div class="cards">
{#each activeAnnouncements(filterAnnouncements(data.announcements, displayedTerm, selectedTags)) as announcement}
<div in:fly={{ y: 10, easing: quintOut, duration: 750 }}>
<AnnouncementCard {announcement} />
</div>
{/each}
</div>
{/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>
</main>
<style lang="scss">
main {
display: flex;
flex-direction: column;
gap: 1rem;
}
.expand-archived {
display: flex;
align-items: center;
justify-content: space-between;
cursor: pointer;
user-select: none;
padding-inline: 0.25rem;
#arrow {
height: 1.5rem;
transition: all 0.2s var(--bezier-one);
}
}
.search {
padding-top: 0.6rem;
padding-bottom: 1.25rem;
background-color: var(--surface-eight);
.search-contain {
display: flex;
justify-content: center;
gap: 1rem;
margin-inline: auto;
width: min(90%, 80rem);
.search-bar {
flex: 1;
}
}
}
.cards {
display: grid;
grid-template-columns: repeat(3, 1fr);
width: 100%;
gap: 16px;
@media (max-width: 768px) {
display: flex;
flex-direction: column;
}
}
</style>

View File

@@ -1,161 +0,0 @@
<script lang="ts">
import moment from 'moment';
import { onMount } from 'svelte';
import type { ResponseAnnouncement } from '$lib/types';
import NewHeader from './NewHeader.svelte';
import { queries } from '$data/api';
import { dev_log } from '$util/dev';
import { useQueryClient } from '@tanstack/svelte-query';
import { read_announcements } from '$lib/stores';
import TagsHost from './TagsHost.svelte';
import Content from './[slug]/Content.svelte';
import ToolTip from '$lib/components/ToolTip.svelte';
import { relativeTime } from '$util/relativeTime';
import Archive from 'svelte-material-icons/ArchiveOutline.svelte';
export let announcement: ResponseAnnouncement;
const client = useQueryClient();
$: isRead = $read_announcements.has(announcement.id);
function prefetch() {
const query = queries['announcementById'](announcement.id);
dev_log('Prefetching', query);
client.prefetchQuery(query);
}
function setAnnouncementRead() {
read_announcements.update((set) => {
const updated = new Set(set);
updated.add(announcement.id);
return updated;
});
}
function generateSlug(title: string) {
return title
.toLowerCase()
.replace(/[^a-z0-9]+/g, '-')
.replace(/^-+|-+$/g, '');
}
</script>
<!-- svelte-ignore a11y-no-static-element-interactions -->
<!-- svelte-ignore a11y-click-events-have-key-events -->
<a
data-sveltekit-preload-data
on:mouseenter={prefetch}
href={`/announcements/${announcement.id}-${generateSlug(announcement.title)}`}
on:click={setAnnouncementRead}
>
<div
class="card"
class:attachment={announcement.attachments && announcement.attachments.length > 0}
>
{#if isRead !== undefined && !isRead}
<NewHeader />
{/if}
{#if announcement.attachments && announcement.attachments.length > 0}
<img
src={announcement.attachments[0]}
class={isRead === undefined || isRead ? '' : 'no-border-radius'}
alt="Banner"
onerror="this.style.display='none'"
/>
{/if}
<div class="content">
<div class="header">
<h3>{announcement.title}</h3>
<span>
{relativeTime(announcement.created_at)}
{#if announcement.archived_at && moment(announcement.archived_at).isBefore(moment())}
<ToolTip
content={`This announcement was archived ${relativeTime(announcement.archived_at)}`}
>
<Archive size="24" />
</ToolTip>
{/if}
</span>
</div>
<div class="footer">
{#if announcement.content}
<Content content={announcement.content} clamp={true} />
{/if}
{#if announcement.tags && announcement.tags.length > 0}
<hr />
<TagsHost tags={announcement.tags.map((tag) => ({ name: tag }))} clickable={false} />
{/if}
</div>
</div>
</div>
</a>
<style lang="scss">
a {
text-decoration: inherit;
}
.card {
&.attachment {
grid-row: span 2;
}
&:hover {
background-color: var(--surface-four);
filter: none;
}
display: flex;
flex-direction: column;
height: 100%;
background-color: var(--surface-seven);
border: 1px solid var(--border);
border-radius: 12px;
img {
height: 150px;
object-fit: cover;
width: 100%;
border-radius: 12px 12px 0px 0px;
&.no-border-radius {
border-radius: 0;
}
}
.content {
display: flex;
flex-direction: column;
justify-content: space-between;
gap: 12px;
height: 100%;
padding: 12px 16px;
color: var(--text-four);
.header,
.footer {
display: flex;
flex-direction: column;
overflow-wrap: anywhere;
span {
display: flex;
gap: 4px;
img {
height: 24px;
width: 24px;
}
}
}
.footer {
gap: 12px;
}
}
}
</style>

View File

@@ -1,14 +0,0 @@
<span>NEW</span>
<style>
span {
text-align: center;
background-color: var(--surface-four);
color: var(--primary);
font-weight: bold;
padding: 4px 0;
border-radius: 12px 12px 0 0;
pointer-events: none;
letter-spacing: 0.05em;
}
</style>

View File

@@ -1,64 +0,0 @@
<script lang="ts">
import Check from 'svelte-material-icons/Check.svelte';
export let tag: string;
export let clickable: boolean = true;
export let selected: boolean = false;
export let onClick: (event?: MouseEvent) => void = () => {};
selected = clickable && selected;
</script>
<button class:selected class:clickable on:click={clickable ? onClick : () => {}}>
{#if selected && clickable}
<div class="icon">
<Check />
</div>
{/if}
{tag}
</button>
<style lang="scss">
button {
display: flex;
justify-content: center;
align-items: center;
gap: 8px;
height: 32px;
padding: 0 12px;
border-radius: 8px;
border: none;
background-color: var(--tertiary);
color: var(--text-four);
letter-spacing: 0.02rem;
font-size: 0.85rem;
user-select: none;
transition: all 0.2s var(--bezier-one);
&.clickable {
background-color: transparent;
border: 1px solid var(--border);
&.selected {
border-color: transparent;
background-color: var(--tertiary);
color: var(--primary);
.icon {
display: inherit;
margin-left: -6px;
transition: none;
}
}
&:hover {
background-color: var(--surface-three);
}
}
}
</style>

View File

@@ -1,85 +0,0 @@
<script lang="ts">
import TagChip from './TagChip.svelte';
import { derived } from 'svelte/store';
import { page } from '$app/stores';
import { goto } from '$app/navigation';
import type { Tags } from '$lib/types';
import Button from '$lib/components/Button.svelte';
import ChevronDown from 'svelte-material-icons/ChevronDown.svelte';
export let tags: Tags;
export let expandable: boolean = false;
export let clickable: boolean = true;
let showAllTags = expandable ? false : true;
const searchParams = derived(page, ($page) => $page.url.searchParams);
$: selectedTags = $searchParams.getAll('tag');
$: displayedTags = (() => {
if (showAllTags) return tags.map((tag) => tag.name);
if (selectedTags.length > 0) {
return [tags[0]?.name, ...selectedTags.filter((tag) => tag !== tags[0]?.name)];
}
return tags.length > 0 ? [tags[0]?.name] : [];
})();
const handleClick = (tag: string) => {
const url = new URL(window.location.href);
const params = new URLSearchParams(url.search);
if (params.getAll('tag').includes(tag)) params.delete('tag', tag);
else params.append('tag', tag);
url.search = params.toString();
goto(url.pathname + url.search);
window.scrollTo({ top: 0, behavior: 'smooth' });
};
</script>
<div>
{#each displayedTags as tag}
<TagChip
{tag}
selected={$searchParams.getAll('tag').includes(tag)}
onClick={() => handleClick(tag)}
{clickable}
/>
{/each}
{#if expandable && tags.length > 1}
<li>
<Button type="text" on:click={() => (showAllTags = !showAllTags)}>
<div
class="expand-arrow"
style:transform={showAllTags ? 'rotate(90deg)' : 'rotate(-90deg)'}
>
<ChevronDown size="24px" color="var(--surface-six)" />
</div>
</Button>
</li>
{/if}
</div>
<style lang="scss">
div {
display: flex;
align-items: center;
flex-wrap: wrap;
white-space: nowrap;
gap: 4px;
li {
display: flex;
align-items: center;
}
.expand-arrow {
transition: all 0.2s var(--bezier-one);
user-select: none;
height: 1.5rem;
}
}
</style>

View File

@@ -1 +0,0 @@
export const prerender = false;

View File

@@ -1,48 +0,0 @@
<script lang="ts">
import { fly } from 'svelte/transition';
import { quintOut } from 'svelte/easing';
import { createQuery } from '@tanstack/svelte-query';
import { queries } from '$data/api';
import { page } from '$app/stores';
import Announcement from './Announcement.svelte';
import Query from '$lib/components/Query.svelte';
let announcementIdNumber: number | undefined = undefined;
let isCreating: boolean = false;
$: {
const lastSegment = $page.url.pathname.split('/').pop();
isCreating = lastSegment === 'create';
announcementIdNumber = isCreating ? undefined : Number(lastSegment.split('-')[0]);
}
$: query = announcementIdNumber
? createQuery(queries.announcementById(announcementIdNumber))
: null;
$: announcement = $query?.data?.announcement || undefined;
$: slug = announcement?.title
? announcement.title
.toLowerCase()
.replace(/[^a-z0-9]+/g, '-')
.replace(/^-+|-+$/g, '')
: '';
$: {
const slugPathname = `/announcements/${announcementIdNumber}-${slug}`;
if (slug && $page.url.pathname !== slugPathname) {
window.history.replaceState(null, '', slugPathname);
}
}
</script>
<main class="wrapper" in:fly={{ y: 10, easing: quintOut, duration: 750 }}>
{#if query}
<Query {query}>
<Announcement {isCreating} {announcement} {announcementIdNumber} {query} />
</Query>
{:else}
<Announcement {isCreating} {announcement} {announcementIdNumber} />
{/if}
</main>

View File

@@ -1,177 +0,0 @@
<script lang="ts">
import { useQueryClient, type CreateQueryResult } from '@tanstack/svelte-query';
import { admin, queries } from '$data/api';
import { goto } from '$app/navigation';
import Button from '$lib/components/Button.svelte';
import Dialog from '$layout/Dialogs/Dialog.svelte';
import type { Announcement, ResponseAnnouncement } from '$lib/types';
import moment from 'moment';
import { isValidUrl } from '$util/isValidUrl';
import Delete from 'svelte-material-icons/DeleteOutline.svelte';
import Edit from 'svelte-material-icons/PencilOutline.svelte';
import Archive from 'svelte-material-icons/ArchiveArrowDownOutline.svelte';
import Close from 'svelte-material-icons/Close.svelte';
import Check from 'svelte-material-icons/Check.svelte';
import Show from 'svelte-material-icons/EyeOutline.svelte';
import Hide from 'svelte-material-icons/EyeOffOutline.svelte';
import Unarchive from 'svelte-material-icons/ArchiveArrowUpOutline.svelte';
import { formatUTC } from '$util/formatUtc';
export let isEditing: boolean;
export let isCreating: boolean;
export let isPreviewing: boolean;
export let archivedAtInput: string | undefined;
export let showDeleteConfirm: boolean;
export let announcementIdNumber: number | undefined;
export let draftInputs: Announcement;
export let query: CreateQueryResult<{ announcement: ResponseAnnouncement }, unknown> | undefined;
const client = useQueryClient();
const toggleArchived = () => {
if (archivedAtInput) archivedAtInput = undefined;
else archivedAtInput = moment().format('YYYY-MM-DDTHH:mm');
};
const isValid = () => {
const hasEmptyTitle = !draftInputs.title;
const hasEmptyAttachments = draftInputs.attachments?.some((a) => !isValidUrl(a));
if (hasEmptyTitle || hasEmptyAttachments) {
alert(
`${[hasEmptyTitle && 'Title', hasEmptyAttachments && 'Attachments']
.filter(Boolean)
.join(' and ')} must be filled properly`
);
return false;
}
return true;
};
const sanitize = (draftInputs: Announcement) => {
return {
...draftInputs,
content: draftInputs.content?.trim() || undefined,
tags: draftInputs.tags && draftInputs.tags.length > 0 ? draftInputs.tags : undefined,
archived_at: draftInputs.archived_at?.trim() || undefined,
attachments:
draftInputs.attachments && draftInputs.attachments?.length > 0
? draftInputs.attachments
: undefined,
author: draftInputs.author?.trim() || undefined,
level: draftInputs.level ?? undefined
};
};
const save = async () => {
if (!isValid()) return;
Object.assign(draftInputs, {
created_at: formatUTC(draftInputs.created_at),
archived_at: formatUTC(draftInputs.archived_at)
});
await admin.update_announcement(announcementIdNumber!, sanitize(draftInputs));
await $query?.refetch();
isEditing = false;
};
const createAnnouncement = async () => {
if (!isValid()) return;
Object.assign(draftInputs, {
created_at: formatUTC(draftInputs.created_at),
archived_at: formatUTC(draftInputs.archived_at)
});
await admin.create_announcement(sanitize(draftInputs));
await client.invalidateQueries(queries.announcements());
goto('/announcements', { invalidateAll: true });
};
const deleteAnnouncement = async () => {
admin.delete_announcement(announcementIdNumber!);
await client.invalidateQueries(queries['announcements']());
goto('/announcements', { invalidateAll: true });
};
const handleUnload = (e: BeforeUnloadEvent) => {
if (isEditing) {
e.preventDefault();
e.returnValue = '';
}
};
</script>
<svelte:window on:beforeunload={handleUnload} />
<Dialog bind:dialogOpen={showDeleteConfirm}>
<svelte:fragment slot="title">Confirm?</svelte:fragment>
<svelte:fragment slot="description">Do you want to delete this announcement?</svelte:fragment>
<svelte:fragment slot="buttons">
<Button type="text" on:click={() => (showDeleteConfirm = !showDeleteConfirm)}>Cancel</Button>
<Button type="filled" on:click={deleteAnnouncement}>OK</Button>
</svelte:fragment>
</Dialog>
<div>
{#if isEditing || isCreating}
<Button
toolTipText={isPreviewing ? 'Hide preview' : 'Show preview'}
icon={isPreviewing ? Hide : Show}
iconColor="var(--secondary)"
on:click={() => (isPreviewing = !isPreviewing)}
/>
<Button
toolTipText={archivedAtInput ? 'Disable archive field' : 'Enable archive field'}
icon={archivedAtInput ? Unarchive : Archive}
iconColor="var(--secondary)"
on:click={toggleArchived}
/>
{#if isEditing}
<Button
toolTipText="Cancel editing"
icon={Close}
iconColor="var(--secondary)"
on:click={() => {
isPreviewing = false;
isEditing = false;
}}
/>
{/if}
<Button
toolTipText={isEditing ? 'Save changes' : 'Create announcement'}
icon={Check}
iconColor="var(--secondary)"
on:click={isEditing ? save : createAnnouncement}
/>
{:else}
<Button
toolTipText="Delete announcement"
icon={Delete}
iconColor="var(--secondary)"
on:click={() => (showDeleteConfirm = !showDeleteConfirm)}
/>
<Button
toolTipText="Edit announcement"
icon={Edit}
iconColor="var(--secondary)"
on:click={() => (isEditing = !isEditing)}
/>
{/if}
</div>
<style>
div {
display: flex;
gap: 1rem;
color: var(--secondary);
}
</style>

View File

@@ -1,121 +0,0 @@
<script lang="ts">
import { admin_login } from '$lib/stores';
import Title from './Title.svelte';
import Divider from '$lib/components/Divider.svelte';
import AdminButtons from './AdminButtons.svelte';
import Author from './Author.svelte';
import Date from './Date.svelte';
import Content from './Content.svelte';
import Attachments from './Attachments.svelte';
import Tags from './Tags.svelte';
import type { Announcement, ResponseAnnouncement } from '$lib/types';
import type { CreateQueryResult } from '@tanstack/svelte-query';
export let isCreating: boolean;
export let announcement: Announcement | undefined;
export let announcementIdNumber: number | undefined;
export let query: CreateQueryResult<{ announcement: ResponseAnnouncement }, unknown> | undefined =
undefined;
let isPreviewing = false;
let isEditing = false;
let showDeleteConfirm = false;
const draftInputs: Announcement = {
...announcement,
id: undefined
};
</script>
<div class="card">
<div class="header">
<div>
<Title
{isCreating}
{isEditing}
{isPreviewing}
title={announcement?.title}
bind:titleInput={draftInputs.title}
/>
<h4>
<Date
{isCreating}
{isEditing}
{isPreviewing}
createdAt={announcement?.created_at}
archivedAt={announcement?.archived_at}
bind:archivedAtInput={draftInputs.archived_at}
bind:createdAtInput={draftInputs.created_at}
/>
<Author
{isCreating}
{isEditing}
{isPreviewing}
author={announcement?.author}
bind:authorInput={draftInputs.author}
/>
</h4>
<Tags {isCreating} {isEditing} {isPreviewing} bind:tagsInput={draftInputs.tags} />
</div>
{#if $admin_login.logged_in}
<AdminButtons
{isCreating}
bind:isEditing
bind:isPreviewing
bind:showDeleteConfirm
bind:archivedAtInput={draftInputs.archived_at}
{draftInputs}
{announcementIdNumber}
{query}
/>
{/if}
</div>
<Divider />
<Content
{isCreating}
{isEditing}
{isPreviewing}
content={announcement?.content}
bind:contentInput={draftInputs.content}
/>
<Attachments
{isCreating}
{isEditing}
{isPreviewing}
attachments={announcement?.attachments}
bind:attachmentsInput={draftInputs.attachments}
/>
</div>
<style lang="scss">
.card {
background-color: var(--surface-eight);
display: flex;
flex-direction: column;
padding: 2rem;
margin-bottom: 3rem;
border-radius: 1rem;
}
.header {
display: flex;
flex-wrap: wrap;
justify-content: space-between;
align-items: center;
gap: 2rem;
}
@media (max-width: 768px) {
.card {
background-color: initial;
padding: 0;
border-radius: 0;
}
}
</style>

View File

@@ -1,154 +0,0 @@
<script lang="ts">
import Button from '$lib/components/Button.svelte';
import Divider from '$lib/components/Divider.svelte';
import Gallery from '$lib/components/Gallery.svelte';
import { isValidUrl } from '$util/isValidUrl';
import Create from 'svelte-material-icons/Plus.svelte';
import Delete from 'svelte-material-icons/DeleteOutline.svelte';
export let isEditing: boolean;
export let isCreating: boolean;
export let isPreviewing: boolean;
export let attachments: string[] | undefined;
export let attachmentsInput: string[] | undefined;
let newAttachment: string | null = null;
const isValidAnnouncement = (attachment: string | null) => {
return attachment && attachment && isValidUrl(attachment);
};
const addAttachment = (attachment: string | null) => {
if (!isValidAnnouncement(attachment)) return;
attachmentsInput = [...(attachmentsInput ?? []), attachment ? attachment : ''];
return true;
};
const removeAttachment = (index: number) => {
if (!attachmentsInput) return;
attachmentsInput = attachmentsInput.filter((_, i) => i !== index);
};
$: displayAttachments = isPreviewing ? attachmentsInput : attachments;
</script>
{#if (isEditing || isCreating) && !isPreviewing}
<Divider />
<div class="attachments-wrapper">
{#if attachmentsInput}
{#each attachmentsInput as attachment, index}
<div class="attachments">
<input
bind:value={attachmentsInput[index]}
class:empty={!attachment || (attachment && !isValidUrl(attachment))}
placeholder="Attachment URL"
/>
<button
class:last={index == attachmentsInput.length - 1}
on:click={() => removeAttachment(index)}
>
<Delete size="24" color="var(--text-four)" />
</button>
</div>
{/each}
{/if}
<span id="new-attachment">
<input
bind:value={newAttachment}
class:empty={!isValidAnnouncement(newAttachment)}
on:blur={() => {
addAttachment(newAttachment);
newAttachment = null;
}}
on:keydown={(event) => {
if (event.key === 'Enter' && addAttachment(newAttachment)) newAttachment = null;
}}
/>
<span>
<Button icon={Create} />
</span>
</span>
</div>
{:else if displayAttachments && displayAttachments?.length > 0}
<Divider />
<Gallery images={displayAttachments} />
{/if}
<style lang="scss">
button {
display: flex;
justify-content: center;
background-color: transparent;
border: none;
cursor: pointer;
}
input {
width: 100%;
padding-right: 40px;
letter-spacing: 0.02rem;
font-size: 0.85rem;
transition: all 0.2s var(--bezier-one);
&:focus {
outline: none;
border: 1px solid var(--primary);
}
&.empty {
border: 1px solid var(--red-one);
}
}
#new-attachment {
display: inline-flex;
align-items: center;
position: relative;
input {
width: 52px;
border: 1px solid var(--border);
padding-right: 0;
&:focus {
width: 100%;
+ span {
display: none;
}
&.empty {
border: 1px solid var(--red-one);
}
}
}
span {
pointer-events: none;
position: absolute;
left: 15.5px;
}
}
.attachments-wrapper {
display: flex;
flex-direction: column;
width: 100%;
gap: 1rem;
.attachments {
display: flex;
justify-content: center;
align-items: center;
position: relative;
gap: 1rem;
button {
position: absolute;
right: 10px;
top: 14px;
}
}
}
</style>

View File

@@ -1,38 +0,0 @@
<script lang="ts">
export let isEditing: boolean;
export let isCreating: boolean;
export let isPreviewing: boolean;
export let author: string | undefined;
export let authorInput: string | undefined;
$: displayAuthor = isPreviewing ? authorInput : author;
</script>
{#if (isEditing || isCreating) && !isPreviewing}
·
<input
bind:value={authorInput}
class:empty={!authorInput?.trim()}
placeholder="Enter author name"
/>
{:else if displayAuthor}
·
<span>
{displayAuthor}
</span>
{/if}
<style lang="scss">
input {
&,
&:focus {
border: none;
outline: none;
border-radius: 0;
}
padding: 0;
font-size: 1rem;
letter-spacing: 0.02rem;
}
</style>

View File

@@ -1,112 +0,0 @@
<script lang="ts">
export let isEditing: boolean = false;
export let isCreating: boolean = false;
export let isPreviewing: boolean = false;
export let content: string | undefined;
export let contentInput: string | undefined = undefined;
export let clamp: boolean = false;
$: displayContent = isPreviewing ? contentInput : content;
</script>
{#if (isEditing || isCreating) && !isPreviewing}
<textarea bind:value={contentInput} class:empty={!content?.trim()} placeholder="Enter content" />
{:else if displayContent}
<div class:clamp>
{@html displayContent}
</div>
{/if}
<style lang="scss">
textarea {
&,
&:focus {
border: none;
outline: none;
border-radius: 0;
}
padding: 0;
font-size: 1rem;
letter-spacing: 0.02rem;
}
div {
color: var(--text-four);
&.clamp {
display: -webkit-inline-box;
line-clamp: 3;
-webkit-line-clamp: 3;
-webkit-box-orient: vertical;
overflow: hidden;
:global(a) {
pointer-events: none;
}
:global(h1),
:global(h2),
:global(h3),
:global(h4),
:global(h5),
:global(h6) {
color: var(--secondary);
line-height: 1.75rem;
margin: 0;
}
}
:global(a) {
color: var(--primary);
font-weight: 600;
font-size: 0.95rem;
text-decoration: none;
&:hover {
text-decoration: underline var(--secondary);
color: var(--text-one);
}
}
:global(h2),
:global(h3),
:global(h4),
:global(h5),
:global(h6) {
color: var(--secondary);
margin-top: 1.25rem;
margin-bottom: 1.25rem;
}
:global(h1) {
font-size: 1.8rem;
}
:global(h2) {
font-size: 1.6rem;
}
:global(h3) {
font-size: 1.4rem;
}
:global(h4) {
font-size: 1.2rem;
}
:global(h5) {
font-size: 1.1rem;
}
:global(h6) {
font-size: 1rem;
}
:global(li) {
list-style-position: inside;
font-size: 0.9rem;
font-weight: 500;
}
}
</style>

View File

@@ -1,71 +0,0 @@
<script lang="ts">
import { relativeTime } from '$util/relativeTime';
import moment from 'moment';
import ArrowRight from 'svelte-material-icons/ArrowRight.svelte';
export let isEditing: boolean;
export let isCreating: boolean;
export let isPreviewing: boolean;
export let createdAt: string | undefined;
export let createdAtInput: string | undefined;
export let archivedAt: string | undefined;
export let archivedAtInput: string | undefined;
if (createdAtInput) {
createdAtInput = moment(createdAtInput).format('YYYY-MM-DDTHH:mm');
} else {
createdAtInput = moment().format('YYYY-MM-DDTHH:mm');
}
$: displayCreatedAt = isPreviewing ? createdAtInput : createdAt;
$: displayArchivedAt = (() => {
const date = isPreviewing ? archivedAtInput : archivedAt;
return date && moment(date).isBefore() ? date : null;
})();
</script>
{#if (isEditing || isCreating) && !isPreviewing}
<span>
<input type="datetime-local" max="9999-12-31T23:59" bind:value={createdAtInput} />
{#if archivedAtInput}
<ArrowRight size="24" />
<input type="datetime-local" max="9999-12-31T23:59" bind:value={archivedAtInput} />
{/if}
</span>
{:else if displayCreatedAt}
<span>
{relativeTime(displayCreatedAt)}
{#if displayArchivedAt}
<ArrowRight size="24" />
{relativeTime(displayArchivedAt)}
{/if}
</span>
{/if}
<style lang="scss">
span {
display: inline-flex;
flex-wrap: wrap;
align-items: center;
column-gap: 1rem;
}
input {
&,
&:focus {
border: none;
outline: none;
border-radius: 0;
}
&::-webkit-calendar-picker-indicator {
filter: invert(88%) sepia(60%) saturate(4731%) hue-rotate(173deg) brightness(91%)
contrast(111%);
}
padding: 0;
font-size: 1rem;
letter-spacing: 0.02rem;
}
</style>

View File

@@ -1,117 +0,0 @@
<script lang="ts">
import Button from '$lib/components/Button.svelte';
import TagChip from '../TagChip.svelte';
import { createQuery } from '@tanstack/svelte-query';
import { queries } from '$data/api';
import Query from '$lib/components/Query.svelte';
import Create from 'svelte-material-icons/Plus.svelte';
export let isEditing: boolean;
export let isCreating: boolean;
export let isPreviewing: boolean;
export let tagsInput: string[];
$: query = createQuery(queries.announcementTags());
$: tags = $query.data?.tags || [];
let newTag: string | null;
function handleTag(tag: string | null) {
if (!tag) return;
if (tags.some((t) => t.name === tag)) {
if (tagsInput?.includes(tag)) {
tagsInput = tagsInput.filter((t) => t !== tag);
if (!$query.data?.tags.some((t) => t.name === tag)) {
tags = tags.filter((t) => t.name !== tag);
}
} else {
tagsInput = [...(tagsInput || []), tag];
}
} else {
tags = [...tags, { name: tag }];
tagsInput = [...(tagsInput || []), tag];
}
newTag = null;
}
</script>
{#if (isEditing || isCreating) && !isPreviewing}
<Query {query}>
<div>
{#each tags as tag}
<TagChip
tag={tag.name}
selected={tagsInput && tagsInput.includes(tag.name)}
onClick={() => handleTag(tag.name)}
/>
{/each}
<div id="new-tag">
<input
bind:value={newTag}
class:empty={!newTag}
on:blur={() => handleTag(newTag)}
on:keydown={(event) => {
if (event.key === 'Enter') handleTag(newTag);
}}
/>
<span>
<Button icon={Create} iconColor="var(--text-four)" />
</span>
</div>
</div>
</Query>
{/if}
<style lang="scss">
div {
display: flex;
flex-wrap: wrap;
align-items: center;
gap: 4px;
}
#new-tag {
display: inline-flex;
position: relative;
input {
&,
&:focus {
border: none;
outline: none;
}
&:focus {
width: 100%;
+ span {
display: none;
}
}
width: 38px;
}
span {
pointer-events: none;
position: absolute;
left: 9px;
top: 6px;
}
}
input {
height: 32px;
border-radius: 8px;
background-color: var(--tertiary);
color: var(--text-four);
letter-spacing: 0.02rem;
font-size: 0.85rem;
transition: all 0.2s var(--bezier-one);
}
</style>

View File

@@ -1,41 +0,0 @@
<script lang="ts">
export let isEditing: boolean;
export let isCreating: boolean;
export let isPreviewing: boolean;
export let title: string | undefined;
export let titleInput: string;
$: displayTitle = isPreviewing ? titleInput : title;
</script>
{#if (isEditing || isCreating) && !isPreviewing}
<input bind:value={titleInput} class:empty={!titleInput?.trim()} placeholder="Enter title" />
{:else if displayTitle}
<h1>
{displayTitle}
</h1>
{/if}
<style lang="scss">
h1 {
font-size: 2.5rem;
}
input {
&,
&:focus {
border: none;
outline: none;
border-radius: 0;
}
width: 100%;
padding: 0;
color: var(--text-one);
font-size: 2.5rem;
font-weight: 700;
line-height: 4rem;
letter-spacing: -0.025em;
}
</style>

View File

@@ -1,119 +0,0 @@
<script lang="ts">
import { fly } from 'svelte/transition';
import { quintOut } from 'svelte/easing';
import ContributorHost from './ContributorSection.svelte';
import Head from '$lib/components/Head.svelte';
import Query from '$lib/components/Query.svelte';
import { queries } from '$data/api';
import { createQuery } from '@tanstack/svelte-query';
const query = createQuery(queries.contributors());
</script>
<Head
title="Contributors of ReVanced"
description="ReVanced is made possible by the community. Check out the people who have contributed to the project and how you can contribute too."
schemas={[
{
'@context': 'https://schema.org',
'@type': 'BreadcrumbList',
itemListElement: [
{
'@type': 'ListItem',
position: 1,
name: 'Home',
item: 'https://revanced.app/'
},
{
'@type': 'ListItem',
position: 2,
name: 'Contributors',
item: 'https://revanced.app/contributors'
}
]
}
]}
/>
<div class="wrapper">
<div class="text-container" in:fly={{ y: 10, easing: quintOut, duration: 750 }}>
<h2>Made possible by the community.</h2>
<p>
Want to show up here? <span>
<a href="https://github.com/revanced" target="_blank" rel="noreferrer"
>Become a contributor
</a>
</span>
</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>
<style>
.wrapper {
margin-bottom: 5rem;
}
.repos {
display: flex;
flex-direction: column;
gap: 2rem;
}
h2 {
text-align: center;
color: var(--text-three);
margin-bottom: 0.3rem;
}
p {
text-align: center;
color: var(--text-three);
}
.text-container {
display: flex;
align-items: center;
flex-direction: column;
margin-bottom: 2rem;
background-color: var(--primary);
padding: 2.5rem 1.75rem;
border-radius: 20px;
}
a {
text-decoration: none;
color: var(--text-three);
}
a::after {
padding-left: 5px;
content: '→';
position: absolute;
transition: all 0.3s var(--bezier-one);
}
a:hover {
text-decoration: underline var(--text-three);
}
a:hover::after {
transform: translateX(5px);
}
@media (max-width: 768px) {
.text-container {
padding: 2rem 1.75rem;
}
}
</style>

View File

@@ -1,65 +0,0 @@
<script lang="ts">
export let name: string;
export let pfp: string;
export let url: string;
let alt = `${name}'s profile picture`;
</script>
<a href={url} rel="noreferrer" target="_blank">
<img src={pfp} {alt} />
<h5>{name}</h5>
</a>
<style lang="scss">
a {
color: var(--text-one);
text-decoration: none;
cursor: pointer;
padding: 0.9rem 1rem;
width: 100%;
transition: background-color 0.3s var(--bezier-one);
display: flex;
gap: 1rem;
align-items: center;
border-right: 1px solid var(--border);
border-bottom: 1px solid var(--border);
&:hover {
background: var(--surface-three);
text-decoration: underline var(--primary);
color: var(--text-one);
}
}
h5 {
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
padding: 0.25rem 0;
}
img {
border-radius: 50%;
height: 32px;
width: 32px;
background-color: var(--surface-four);
transition: transform 0.4s var(--bezier-one);
user-select: none;
}
@media (max-width: 768px) {
h5 {
display: none;
}
img {
height: 42px;
width: 42px;
}
a {
width: max-content;
background-color: transparent;
border: none;
}
}
</style>

View File

@@ -1,101 +0,0 @@
<script lang="ts">
import { slide } from 'svelte/transition';
import { quintOut } from 'svelte/easing';
import type { Contributor } from '$lib/types';
import ContributorButton from './ContributorPerson.svelte';
import ChevronUp from 'svelte-material-icons/ChevronUp.svelte';
export let contributors: Contributor[];
export let name: string;
export let url: string;
let expanded = true;
let bots = ['semantic-release-bot', 'revanced-bot'];
</script>
<div class="container">
<!-- svelte-ignore a11y-no-static-element-interactions -->
<div
class="title"
class:closed={!expanded}
on:click={() => (expanded = !expanded)}
on:keypress={() => (expanded = !expanded)}
>
<a href={url} rel="noreferrer" target="_blank" on:click|stopPropagation>
<h4>{name}</h4>
</a>
<div id="arrow" style:transform={expanded ? 'rotate(0deg)' : 'rotate(180deg)'}>
<ChevronUp size="24px" color="var(--surface-six)" />
</div>
</div>
{#if expanded}
<div class="contributors" transition:slide={{ easing: quintOut, duration: 500 }}>
{#each contributors as { name, avatar_url, url }}
{#if !bots.includes(name)}
<ContributorButton {name} pfp={avatar_url} {url} />
{/if}
{/each}
</div>
{/if}
</div>
<style lang="scss">
.container {
border-radius: 20px;
overflow: hidden;
border: 1px solid var(--border);
.title {
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;
}
a {
display: flex;
text-decoration: none;
width: max-content;
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>

View File

@@ -1,221 +0,0 @@
<script lang="ts">
import { fly } from 'svelte/transition';
import { quintOut } from 'svelte/easing';
import { queries } from '$data/api';
import { createQuery } from '@tanstack/svelte-query';
import Head from '$lib/components/Head.svelte';
import Query from '$lib/components/Query.svelte';
import CryptoDialog from '$layout/Dialogs/CryptoDialog.svelte';
import DonateHeartAnimation from './DonateHeartAnimation.svelte';
import TeamMember from './TeamMember.svelte';
import { supportsWebP } from '$util/supportsWebP';
const teamQuery = createQuery(queries.team());
const aboutQuery = createQuery(queries.about());
let cryptoDialogue = false;
const shuffle = <T,>(array: T[]) =>
array
.map((value) => ({ value, sort: Math.random() }))
.sort((a, b) => a.sort - b.sort)
.map(({ value }) => value);
</script>
<Head
title="Donate to ReVanced"
description="Donate to ReVanced with a variety of donation methods, including cryptocurrencies in order to allow us to maintain our servers and develop new features."
schemas={[
{
'@context': 'https://schema.org',
'@type': 'BreadcrumbList',
itemListElement: [
{
'@type': 'ListItem',
position: 1,
name: 'Home',
item: 'https://revanced.app/'
},
{
'@type': 'ListItem',
position: 2,
name: 'Download',
item: 'https://revanced.app/donate'
}
]
}
]}
/>
<main class="wrapper" in:fly={{ y: 10, easing: quintOut, duration: 750 }}>
<section>
<div>
<h2>🎉 Support <span style="color: var(--primary);">ReVanced</span></h2>
<p>
ReVanced offers a variety of patches, including ad-blocking, custom themes, and innovative
features. All of which is completely open source and free of charge. Donating will allow
ReVanced maintain our servers and develop new features.
</p>
</div>
<div id="heart">
<DonateHeartAnimation
backgroundImageUrl="/revanced-logo-background.svg"
foregroundImageUrl="/icons/heart.svg"
alt="ReVanced Logo"
/>
</div>
</section>
<h3>Donate</h3>
<Query query={aboutQuery} let:data>
<div class="donate-cards">
{#if data.about.donations.links}
{#each data.about.donations.links as link}
<a class="donate-card" target="_blank" rel="noreferrer" href={link.url}>
<!-- not using <img/> because we want the image height to always be 200px -->
<div
style="background-image: url('/donate/card-images/{link.name}.{supportsWebP()
? 'webp'
: 'png'}'), url('/donate/card-images/fallback.svg');"
role="img"
aria-label="{link.name} preview image"
/>
<span>{link.name}</span>
</a>
{/each}
{/if}
{#if data.about.donations.wallets}
<button class="donate-card" on:click={() => (cryptoDialogue = !cryptoDialogue)}>
<div
style="background-image: url('/donate/card-images/Cryptocurrencies.{supportsWebP()
? 'webp'
: 'png'}'), url('/donate/card-images/fallback.svg');"
role="img"
aria-label="Cryptocurrencies preview image"
/>
<span>Cryptocurrencies</span>
</button>
{/if}
</div>
</Query>
<Query query={teamQuery} let:data>
<h3>Meet the team</h3>
{#if data.members.length > 0}
<section class="team">
<!-- randomize team members because equality -->
{#each shuffle(data.members) as member, i}
<TeamMember {member} {i} />
{/each}
</section>
{/if}
</Query>
</main>
<Query query={aboutQuery} let:data>
<CryptoDialog bind:dialogOpen={cryptoDialogue} wallets={data.about.donations.wallets} />
</Query>
<style lang="scss">
main {
display: flex;
flex-direction: column;
margin-bottom: 5rem;
section {
display: flex;
justify-content: center;
align-items: center;
@media (max-width: 768px) {
flex-direction: column-reverse;
}
}
}
h2 {
margin-bottom: 0.5rem;
color: var(--text-one);
}
h3 {
margin-bottom: 1.5rem;
}
p {
margin-bottom: 2rem;
width: 60%;
@media (max-width: 1200px) {
width: 90%;
}
@media (max-width: 768px) {
width: 100%;
}
}
@media (max-width: 768px) {
#heart {
display: none;
}
}
.donate-cards {
display: flex;
gap: 1rem;
margin-bottom: 3rem;
@media (max-width: 768px) {
flex-direction: column;
}
}
.donate-card {
text-decoration: none;
background-color: var(--surface-nine);
border-radius: 1.5rem;
width: 100%;
cursor: pointer;
text-align: left;
border: none;
overflow: hidden;
transition:
0.3s border-radius var(--bezier-one),
0.3s background-color var(--bezier-one);
&:hover {
background-color: var(--tertiary);
}
&:active {
border-radius: 2.75rem;
}
span {
display: block;
color: var(--text-four);
font-size: 1.05rem;
font-weight: 500;
padding: 1.5rem;
}
div {
height: 200px;
background-size: cover;
background-position: center;
max-width: 100%;
}
}
.team {
width: 100%;
display: grid;
grid-template-columns: repeat(auto-fill, minmax(325px, 1fr));
justify-content: space-between;
align-items: stretch;
gap: 1rem;
}
</style>

View File

@@ -1,358 +0,0 @@
<script lang="ts">
import { onMount } from 'svelte';
export let backgroundImageUrl: string;
export let foregroundImageUrl: string;
export let alt: string;
let showHeart: (e: MouseEvent, behind: boolean) => void = (_e, _behind) => {};
onMount(() => {
const background = document.getElementById('pulsating-image')!;
let lastHeartTime = 0;
showHeart = (e, behind) => {
const rect = background.getBoundingClientRect();
if (Date.now() - lastHeartTime < 100) return;
lastHeartTime = Date.now();
const heart = document.createElement('img');
heart.src = foregroundImageUrl;
heart.style.position = 'absolute';
let x = e.clientX - rect.left - heart.width;
let y = e.clientY - rect.top - heart.height;
x += Math.floor(Math.random() * 30) - 15;
y += Math.floor(Math.random() * 30) - 15;
heart.style.left = `${x}px`;
heart.style.top = `${y}px`;
heart.style.height = '50px';
heart.style.width = '50px';
if (behind) heart.style.zIndex = '-1';
const animations = [
'float-up',
'float-scale-rotate-left',
'float-scale-rotate-right',
'float-scale-rotate-right-2',
'float-scale-rotate-left-2'
];
heart.classList.add(animations[Math.floor(Math.random() * animations.length)]);
heart.classList.add('float');
background.appendChild(heart);
setTimeout(() => {
heart.remove();
}, 1000);
};
let interval: number | undefined;
function showHeartRandom() {
const rect = background.getBoundingClientRect();
const x = rect.left + Math.floor(Math.random() * rect.width);
const y = rect.top + Math.floor(Math.random() * rect.height);
showHeart!({ clientX: x, clientY: y } as MouseEvent, true);
interval = setTimeout(showHeartRandom, Math.random() * 300);
}
showHeartRandom();
return () => clearTimeout(interval);
});
</script>
<!-- svelte-ignore a11y-click-events-have-key-events -->
<!-- svelte-ignore a11y-no-static-element-interactions -->
<div
id="pulsating-image"
on:mousemove={(e) => showHeart(e, false)}
on:mousedown={(e) => showHeart(e, false)}
>
<div id="pulsating-image-rotate">
<div id="pulsating-image-hover">
<div id="background" style:background-image="url({backgroundImageUrl})">
<img id="heart-secondary" src={foregroundImageUrl} {alt} />
<img id="heart-primary" src={foregroundImageUrl} {alt} />
</div>
</div>
</div>
</div>
<style lang="scss">
:root {
overflow-x: hidden;
}
:global(.float) {
pointer-events: none;
opacity: 0;
filter: blur(0);
}
:global(.float-up) {
animation: floatUp 1s ease-out forwards;
@keyframes floatUp {
0% {
transform: translateY(0px);
}
50% {
opacity: 0.3;
filter: blur(0);
}
100% {
transform: translateY(-40px);
opacity: 0;
filter: blur(5px);
}
}
}
:global(.float-scale-rotate-left) {
animation: floatScaleRotateLeft 1s ease-out forwards;
@keyframes floatScaleRotateLeft {
0% {
transform: translateY(0px) scale(1.2);
}
50% {
opacity: 0.3;
filter: blur(0);
}
100% {
transform: translateY(-20px) scale(0.5) rotate(-45deg);
opacity: 0;
filter: blur(5px);
}
}
}
:global(.float-scale-rotate-right) {
animation: floatScaleRotateRight 1s ease-out forwards;
@keyframes floatScaleRotateRight {
0% {
transform: translateY(0px) scale(1.2);
}
50% {
opacity: 0.3;
filter: blur(0);
}
100% {
transform: translateY(-20px) scale(0.5) rotate(45deg);
opacity: 0;
filter: blur(5px);
}
}
}
:global(.float-scale-rotate-right-2) {
animation: floatScaleRotateRight2 1s ease-out forwards;
@keyframes floatScaleRotateRight2 {
0% {
transform: translateY(0px) scale(1.8);
}
50% {
opacity: 0.3;
filter: blur(0);
}
100% {
transform: translateY(-30px) scale(1) rotate(50deg);
opacity: 0;
filter: blur(5px);
}
}
}
:global(.float-scale-rotate-left-2) {
animation: floatScaleRotateLeft2 1s ease-out forwards;
@keyframes floatScaleRotateLeft2 {
0% {
transform: translateY(0px) scale(1.8);
opacity: 0.3;
}
50% {
opacity: 0.3;
filter: blur(0);
}
100% {
transform: translateY(-30px) scale(1) rotate(-50deg);
opacity: 0;
filter: blur(5px);
}
}
}
#pulsating-image {
user-select: none;
border-radius: 100%;
cursor: pointer;
filter: brightness(1);
transition:
transform 0.4s ease,
filter 0.2s ease;
&:hover {
transform: scale(1.05);
}
&:active {
filter: brightness(1.3);
transform: scaleY(0.95) rotate(5deg);
}
#pulsating-image-rotate {
animation: rotate 1.25s infinite;
@keyframes rotate {
32% {
transform: rotate(-5deg);
}
50% {
transform: rotate(4deg);
}
80% {
transform: rotate(-3deg);
}
}
#pulsating-image-hover {
height: 225px;
width: 225px;
transition: all 0.2s ease;
&:hover {
animation: wiggle 1s;
@keyframes wiggle {
0% {
transform: rotate(0);
}
25% {
transform: rotate(5deg);
}
50% {
transform: rotate(-5deg);
}
75% {
transform: rotate(5deg);
}
100% {
transform: rotate(0deg);
}
}
}
#background {
border-radius: 100%;
box-shadow: 0 0 0 1rem black;
pointer-events: none;
display: flex;
background-repeat: no-repeat;
justify-content: center;
align-items: center;
animation: pulse-size 1.25s infinite;
will-change: transform;
height: 100%;
width: 100%;
#heart-secondary {
animation: pulse-fade 1.25s infinite ease-out;
opacity: 0.5;
will-change: transform, filter;
filter: opacity(0);
transform: scale(0.8);
@keyframes pulse-fade {
30% {
transform: scale(0.7);
filter: blur(0rem) opacity(0.7);
}
100% {
transform: scale(1.8);
filter: blur(1rem) opacity(0);
}
}
}
#heart-primary {
animation:
double-pulse-size 1.25s infinite,
pulse-glow 1.25s infinite;
@keyframes double-pulse-size {
0% {
transform: scale(0.7) rotate(0);
}
13% {
transform: scale(0.84);
}
16% {
transform: scale(0.82);
}
30% {
transform: scale(0.7);
}
50% {
transform: scale(0.8);
}
100% {
transform: scale(0.7);
}
}
& {
filter: drop-shadow(0 0 0 var(--red-one));
}
@keyframes pulse-glow {
33% {
filter: drop-shadow(0 0 0rem var(--red-one));
}
45% {
filter: drop-shadow(0 0 0.5rem var(--red-one));
}
80% {
filter: drop-shadow(0 0 0.5rem var(--red-one));
}
}
}
#heart-primary,
#heart-secondary {
position: absolute;
height: 50%;
will-change: transform, box-shadow;
}
& {
box-shadow: 0 0 0 0 #000000;
}
@keyframes pulse-size {
0% {
transform: scale(0.7);
filter: brightness(1);
}
32% {
box-shadow: 0 0 50px 3px #000000;
transform: scale(0.64) scaleY(0.95);
}
38% {
box-shadow: 0 0 0 0 #000000;
filter: brightness(1.2);
}
50% {
transform: scale(0.78);
}
100% {
box-shadow: 0 0 0 0 #000000;
filter: brightness(1);
transform: scale(0.7);
}
}
}
}
}
}
</style>

View File

@@ -1,126 +0,0 @@
<script lang="ts">
import { fly } from 'svelte/transition';
import { quintOut } from 'svelte/easing';
import type { TeamMember } from '$lib/types';
import ToolTip from '$lib/components/ToolTip.svelte';
import CheckDecagramOutline from 'svelte-material-icons/CheckDecagramOutline.svelte';
export let member: TeamMember;
export let i: number;
const transitionOptions = {
y: 10,
easing: quintOut,
duration: 750,
delay: 50 * i
};
</script>
<div class="member">
<a href={member.url} rel="noreferrer" target="_blank" in:fly|global={transitionOptions}>
<img src={member.avatar_url} alt="{member.name}'s profile picture." />
</a>
<div class="member-text">
<div class="member-title">
<a href={member.url} rel="noreferrer" target="_blank" in:fly|global={transitionOptions}>
<h4>{member.name}</h4>
</a>
{#if member.gpg_key}
<div class="verified-badge">
<ToolTip
content="<p>GPG key ID:</p> <a class='gpg-url' href={member.gpg_key
.url} rel='noreferrer' target='_blank'>{member.gpg_key.id}</a>"
html={true}
>
<div class="desktop">
<a href={member.gpg_key.url} rel="noreferrer" target="_blank">
<CheckDecagramOutline size="20px" color="var(--secondary)" />
</a>
</div>
<div class="mobile">
<CheckDecagramOutline size="20px" color="var(--secondary)" />
<h5>GPG key</h5>
</div>
</ToolTip>
</div>
{/if}
</div>
{#if member.bio}
<h6>{member.bio}</h6>
{/if}
</div>
</div>
<style lang="scss">
a {
text-decoration: none;
}
.member {
width: 100%;
color: var(--text-one);
border: 1px solid var(--border);
padding: 1rem;
border-radius: 12px;
display: flex;
gap: 1rem;
transition: 0.3s background-color var(--bezier-one);
&:hover {
background-color: var(--surface-seven);
}
}
.member-text {
display: flex;
flex-direction: column;
word-break: break-word;
.member-title {
display: flex;
align-items: center;
gap: 0.5rem;
.verified-badge {
display: flex;
align-items: center;
fill: var(--secondary) !important;
line-height: 16px;
height: 16px;
.mobile {
display: none;
}
@media (max-width: 768px) {
.desktop {
display: none;
}
.mobile {
display: flex;
flex-direction: row;
justify-content: center;
align-items: center;
gap: 0.25rem;
}
}
}
}
}
img {
border-radius: 50%;
height: 64px;
width: 64px;
transition: transform 0.4s var(--bezier-one);
user-select: none;
margin-bottom: 1rem;
@media (max-width: 768px) {
margin-bottom: 0;
height: 48px;
width: 48px;
}
}
</style>

View File

@@ -1,139 +0,0 @@
<script lang="ts">
import { queries } from '$data/api';
import { fly } from 'svelte/transition';
import { quintOut } from 'svelte/easing';
import { createQuery } from '@tanstack/svelte-query';
import manager_screenshot from '$images/manager.png?format=avif;webp;png&as=picture';
import TrayArrowDown from 'svelte-material-icons/TrayArrowDown.svelte';
import Head from '$lib/components/Head.svelte';
import Query from '$lib/components/Query.svelte';
import Button from '$lib/components/Button.svelte';
import Picture from '$lib/components/Picture.svelte';
import DownloadCompatibilityWarningDialog from '$layout/Dialogs/DownloadCompatibilityWarningDialog.svelte';
import { onMount } from 'svelte';
const query = createQuery(queries.manager());
let warning: string;
let warningDialogue = false;
let userAgent: string;
let isAndroid: boolean;
let androidVersionMatch: RegExpExecArray | null;
let androidVersion: number;
onMount(() => {
userAgent = navigator.userAgent;
androidVersionMatch = /Android\s([\d.]+)/i.exec(userAgent);
androidVersion = androidVersionMatch ? parseInt(androidVersionMatch[1]) : 0;
isAndroid = !!androidVersion;
});
function handleClick() {
if (!isAndroid) {
warning = 'Your device is not running Android.';
warningDialogue = true;
} else if (androidVersion < 8) {
warning = `Your device is running ${androidVersion}. ReVanced only supports Android versions 8 and above.`;
warningDialogue = true;
}
}
</script>
<Head
title="Download ReVanced"
description="Download ReVanced Manager to patch your favourite apps, right on your device."
schemas={[
{
'@context': 'https://schema.org',
'@type': 'BreadcrumbList',
itemListElement: [
{
'@type': 'ListItem',
position: 1,
name: 'Home',
item: 'https://revanced.app/'
},
{
'@type': 'ListItem',
position: 2,
name: 'Download',
item: 'https://revanced.app/download'
}
]
}
]}
/>
<main class="wrapper center" in:fly={{ y: 10, easing: quintOut, duration: 750 }}>
<h2>ReVanced <span>Manager</span></h2>
<p>Patch your favourite apps, right on your device.</p>
<div class="buttons">
<Query {query} let:data>
{#if !isAndroid || androidVersion < 8}
<Button on:click={handleClick} icon={TrayArrowDown} type="filled">
{data.release.version}
</Button>
{:else}
<Button
on:click={handleClick}
icon={TrayArrowDown}
type="filled"
href={data.release.download_url}
>
{data.release.version}
</Button>
{/if}
</Query>
<Button type="tonal" href="https://github.com/revanced/revanced-manager" target="_blank">
View source
</Button>
</div>
<div class="screenshot">
<Picture data={manager_screenshot} alt="Manager Screenshot" />
</div>
</main>
<DownloadCompatibilityWarningDialog bind:dialogOpen={warningDialogue} {warning} />
<style>
.center {
display: flex;
flex-direction: column;
align-items: center;
}
h2 {
text-align: center;
color: var(--text-one);
}
p {
text-align: center;
margin-bottom: 1.5rem;
}
.screenshot :global(img) {
margin-top: 2.5rem;
margin-bottom: 2.5rem;
height: 50rem;
width: auto;
padding: 0.5rem 0.5rem;
border-radius: 2rem;
background-color: var(--surface-seven);
user-select: none;
}
.buttons {
display: flex;
gap: 1rem;
}
span {
color: var(--primary);
}
</style>

View File

@@ -1,221 +0,0 @@
<script lang="ts">
import { building } from '$app/environment';
import { fly } from 'svelte/transition';
import { quintOut } from 'svelte/easing';
import { derived, readable, type Readable } from 'svelte/store';
import { page } from '$app/stores';
import type { CompatiblePackage, Patch } from '$lib/types';
import { createQuery } from '@tanstack/svelte-query';
import { queries } from '$data/api';
import Head from '$lib/components/Head.svelte';
import PackageMenu from './PackageMenu.svelte';
import Package from './Package.svelte';
import PatchItem from './PatchItem.svelte';
import Search from '$lib/components/Search.svelte';
import FilterChip from '$lib/components/FilterChip.svelte';
import MobilePatchesPackagesDialog from '$layout/Dialogs/MobilePatchesPackagesDialog.svelte';
import Query from '$lib/components/Query.svelte';
import Fuse from 'fuse.js';
import { onMount } from 'svelte';
import createFilter from '$util/filter';
import { debounce } from '$util/debounce';
const query = createQuery(queries.patches());
let searchParams: Readable<URLSearchParams>;
if (building) {
searchParams = readable(new URLSearchParams());
} else {
searchParams = derived(page, ($page) => $page.url.searchParams);
}
$: selectedPkg = $searchParams.get('pkg');
let searchTerm = $searchParams.get('s') || '';
let mobilePackages = false;
let showAllVersions = false;
function filterPatches(patches: Patch[], pkg: string, search?: string): Patch[] {
const patchFilter = createFilter(patches, {
searcherOptions: {
keys: ['name', 'description', 'compatiblePackages.name', 'compatiblePackages.versions']
},
additionalFilter: (patch: Patch, pkg: string): boolean => {
if (!pkg) return true;
return (
patch.compatiblePackages?.some(
(compatiblePackage: CompatiblePackage) =>
compatiblePackage.name === pkg || compatiblePackage.versions?.includes(pkg)
) || false
);
}
});
return patchFilter(pkg, search);
}
// Make sure we don't have to filter the patches after every key press
let displayedTerm = '';
const update = () => {
displayedTerm = searchTerm;
const url = new URL(window.location.href);
url.pathname = '/patches';
if (searchTerm) {
url.searchParams.set('s', searchTerm);
} else {
url.searchParams.delete('s');
}
window.history.pushState(null, '', url);
};
onMount(update);
</script>
<Head
title="Patches for ReVanced"
description="Browse our rich collection of patches for ReVanced you can use to patch your favourite apps."
schemas={[
{
'@context': 'https://schema.org',
'@type': 'BreadcrumbList',
itemListElement: [
{
'@type': 'ListItem',
position: 1,
name: 'Home',
item: 'https://revanced.app/'
},
{
'@type': 'ListItem',
position: 2,
name: 'Patches',
item: 'https://revanced.app/patches'
}
]
}
]}
/>
<div class="search">
<div class="search-contain">
<!-- Must bind both variables: we get searchTerm from the text input, -->
<Search
bind:searchTerm
bind:displayedTerm
title="Search for patches"
on:keyup={debounce(update)}
/>
</div>
</div>
<main>
<div class="filter-chips" in:fly={{ y: 10, easing: quintOut, duration: 750 }}>
<FilterChip
selected={!!selectedPkg}
dropdown
on:click={() => (mobilePackages = !mobilePackages)}
>
{selectedPkg || 'Packages'}
</FilterChip>
</div>
<Query {query} let:data>
<MobilePatchesPackagesDialog
bind:dialogOpen={mobilePackages}
bind:searchTerm
{data}
{selectedPkg}
/>
<aside in:fly={{ y: 10, easing: quintOut, duration: 750 }}>
<PackageMenu>
<span class="packages">
<Package {selectedPkg} name="All packages" bind:searchTerm />
{#each data.packages as pkg}
<Package {selectedPkg} name={pkg} bind:searchTerm />
{/each}
</span>
</PackageMenu>
</aside>
<div class="patches-container">
{#each filterPatches(data.patches, selectedPkg || '', displayedTerm) as patch}
{#key selectedPkg || displayedTerm}
<div in:fly={{ y: 10, easing: quintOut, duration: 750 }}>
<PatchItem {patch} bind:showAllVersions />
</div>
{/key}
{/each}
</div>
</Query>
</main>
<style>
main {
display: grid;
grid-template-columns: 300px 3fr;
width: min(90%, 80rem);
margin-inline: auto;
gap: 1.5rem;
}
.search {
padding-top: 0.6rem;
padding-bottom: 1.25rem;
background-color: var(--surface-eight);
}
.search-contain {
width: min(90%, 80rem);
margin-inline: auto;
}
.patches-container {
overflow: hidden;
border-radius: 20px;
margin-top: 1.5rem;
display: flex;
flex-direction: column;
gap: 0.5rem;
width: 100%;
position: sticky;
z-index: 1;
min-height: calc(100vh - 6rem);
margin-bottom: 3rem;
}
.filter-chips {
display: none;
}
@media (max-width: 768px) {
main {
grid-template-columns: none;
flex-direction: column;
gap: 0;
}
aside {
display: none;
}
.patches-container {
margin-top: 1rem;
margin-bottom: 1.5rem;
gap: 0.75rem;
}
.filter-chips {
display: flex;
flex-wrap: wrap;
margin-top: 1rem;
gap: 0.75rem;
padding-bottom: 0rem;
}
}
</style>

View File

@@ -1,72 +0,0 @@
<script lang="ts">
import { goto } from '$app/navigation';
export let selectedPkg: string | null;
export let name: string;
export let searchTerm: string | null;
function handleClick() {
// Assign the selected package. If it's already selected, deselect it.
const url = new URL(window.location.href);
const params = new URLSearchParams();
url.pathname = '/patches';
if (selectedPkg !== name && name !== 'All packages') {
params.set('pkg', name);
}
if (searchTerm) {
params.set('s', searchTerm);
}
url.search = params.toString();
goto(url.pathname + url.search);
window.scrollTo({ top: 0, behavior: 'smooth' });
}
</script>
<!-- svelte-ignore a11y-click-events-have-key-events -->
<!-- svelte-ignore a11y-no-static-element-interactions -->
<div
class="package"
class:selected={selectedPkg === name || (name === 'All packages' && !selectedPkg)}
on:click={handleClick}
>
{name}
</div>
<style lang="scss">
.package {
padding: 0.75rem 1rem;
border-radius: 100px;
font-size: 0.85rem;
font-weight: 500;
color: var(--text-four);
cursor: pointer;
user-select: none;
transition:
background-color 0.4s var(--bezier-one),
color 0.3s var(--bezier-one);
@media (max-width: 768px) {
border-radius: 0px;
font-size: 0.9rem;
padding: 1rem;
word-break: break-all;
overflow: hidden;
text-overflow: ellipsis;
border-bottom: 1px solid var(--border);
}
&.selected {
color: var(--primary);
background-color: var(--tertiary);
}
&:hover:not(.selected) {
background-color: var(--surface-seven);
color: var(--text-one);
}
}
</style>

View File

@@ -1,38 +0,0 @@
<div>
<h6>Packages</h6>
<hr />
<span>
<slot />
</span>
</div>
<style lang="scss">
div {
display: flex;
flex-direction: column;
position: sticky;
height: calc(100vh - 60px);
top: 60px;
padding: calc(6rem - 60px) 30px 30px 10px;
overflow-y: scroll;
&::-webkit-scrollbar-thumb {
background-color: transparent;
}
&:hover::-webkit-scrollbar-thumb {
background-color: var(--primary);
}
}
span {
margin-top: 0.75rem;
word-break: break-all;
}
h6 {
margin-bottom: 1rem;
color: var(--primary);
}
</style>

View File

@@ -1,229 +0,0 @@
<script lang="ts">
import { slide, fade } from 'svelte/transition';
import { quintOut } from 'svelte/easing';
import type { Patch } from '$lib/types';
import Button from '$lib/components/Button.svelte';
import ChevronDown from 'svelte-material-icons/ChevronDown.svelte';
export let patch: Patch;
export let showAllVersions: boolean;
let expanded: boolean = false;
const options = Object.entries(patch.options).map(([optionKey, option]) => ({
optionKey,
...option
}));
const hasPatchOptions = options.length > 0;
</script>
<!-- svelte-ignore a11y-click-events-have-key-events -->
<!-- svelte-ignore a11y-no-static-element-interactions -->
<div
class="patch-container"
class:expanded={hasPatchOptions}
class:rotate={expanded}
on:click={() => (expanded = !expanded)}
>
<div class="things">
<div class="title">
<h3>{patch.name}</h3>
</div>
{#if hasPatchOptions}
<div class="expand-arrow">
<ChevronDown size="24px" color="var(--surface-six)" />
</div>
{/if}
</div>
{#if patch.description}
<h5>{patch.description}</h5>
{/if}
<ul class="info-container">
{#if !patch.compatiblePackages}
<li class="patch-info">🌎 Universal patch</li>
{:else}
{#each patch.compatiblePackages as pkg}
<li class="patch-info">
<a
href="https://play.google.com/store/apps/details?id={pkg.name}"
target="_blank"
rel="noreferrer"
>
📦 {pkg.name}
</a>
</li>
{/each}
{/if}
{#if hasPatchOptions}
<li class="patch-info">⚙️ Patch options</li>
{/if}
<!-- Should this be hardcoded to get the version of the first package? -->
{#if patch.compatiblePackages?.length && patch.compatiblePackages[0].versions?.length}
{#if showAllVersions}
{#each patch.compatiblePackages[0].versions.reverse() as version}
<li class="patch-info">
🎯 {version}
</li>
{/each}
{:else}
<li class="patch-info">
🎯 {patch.compatiblePackages[0].versions.slice(-1)}
</li>
{/if}
{#if patch.compatiblePackages[0].versions.length > 1}
<li class="button">
<Button
type="text"
on:click={(e) => {
e.stopPropagation();
showAllVersions = !showAllVersions;
}}
>
<div
class="expand-arrow"
style:transform={showAllVersions ? 'rotate(90deg)' : 'rotate(-90deg)'}
>
<ChevronDown size="24px" color="var(--surface-six)" />
</div>
</Button>
</li>
{/if}
{:else}
<li class="patch-info">🎯 Any version</li>
{/if}
</ul>
{#if expanded && hasPatchOptions}
<span transition:fade={{ easing: quintOut, duration: 1000 }}>
<div class="options" transition:slide={{ easing: quintOut, duration: 500 }}>
{#each options as option}
<!-- svelte-ignore a11y-no-static-element-interactions -->
<div class="option" on:click|stopPropagation>
<h5 id="option-title">{option.title}</h5>
<h5>
<pre id="option-description">{option.description}</pre>
</h5>
</div>
{/each}
</div>
</span>
{/if}
</div>
<style lang="scss">
h3 {
margin-right: 0.5rem;
margin-bottom: 0.2rem;
color: var(--primary);
}
#option-description {
white-space: pre-wrap;
word-break: break-word;
}
#option-title {
color: var(--secondary);
}
.button {
display: flex;
align-items: center;
}
.patch-info {
display: flex;
justify-content: center;
align-items: center;
list-style: none;
font-size: 0.8rem;
font-weight: 500;
color: var(--text-four);
padding: 0.25rem 0.5rem;
border: 1px solid var(--border);
border-radius: 8px;
&:hover {
background-color: var(--surface-four);
}
}
a {
text-decoration: none;
color: var(--text-four);
&:hover {
text-decoration: underline var(--secondary);
color: var(--secondary);
}
}
.info-container {
display: flex;
flex-wrap: wrap;
gap: 0.25rem;
margin: 0.3rem 0rem;
width: 100%;
margin-top: 0.5rem;
}
.patch-container {
transition: all 0.1s var(--bezier-one);
background-color: var(--surface-seven);
padding: 1.25rem;
border-radius: 12px;
}
.title {
display: flex;
align-items: center;
}
.things {
display: flex;
justify-content: space-between;
}
.expand-arrow {
transition: all 0.2s var(--bezier-one);
user-select: none;
height: 1.5rem;
}
.rotate .expand-arrow {
transform: rotate(180deg);
}
.expanded {
cursor: pointer;
&:hover {
background-color: var(--surface-three);
}
&:active {
filter: brightness(1.15);
}
}
.option {
padding: 1rem;
}
/* thanks piknik */
.option + .option {
border-top: 1px solid var(--border);
}
.options {
border: 1px solid var(--border);
overflow: hidden;
border-radius: 8px;
margin-top: 1rem;
display: flex;
flex-direction: column;
}
</style>

View File

@@ -1,7 +0,0 @@
export const debounce = <T extends any[]>(f: (...args: T) => void) => {
let timeout: number;
return (...args: T) => {
clearTimeout(timeout);
timeout = setTimeout(() => f(...args), 350);
};
};

View File

@@ -1,8 +0,0 @@
import { dev } from '$app/environment';
// console.log, but only if in dev environment.
export function dev_log(part: string, ...args: any[]) {
if (dev) {
console.log(`[${part}]:`, ...args);
}
}

View File

@@ -1,34 +0,0 @@
import Fuse from 'fuse.js';
type SearcherOptions<T> = {
keys: string[];
shouldSort?: boolean;
threshold?: number;
};
type FilterOptions<T, C> = {
searcherOptions: SearcherOptions<T>;
additionalFilter?: (item: T, context: C) => boolean;
};
function createFilter<T, C>(items: T[], options: FilterOptions<T, C>) {
const { searcherOptions, additionalFilter } = options;
const searcher = new Fuse(items, {
keys: searcherOptions.keys,
shouldSort: searcherOptions.shouldSort ?? true,
threshold: searcherOptions.threshold ?? 0.3
});
return (context: C, search?: string): T[] => {
if (!search) {
return additionalFilter ? items.filter((item) => additionalFilter(item, context)) : items;
}
const results = searcher.search(search).map(({ item }) => item);
return additionalFilter ? results.filter((item) => additionalFilter(item, context)) : results;
};
}
export default createFilter;

View File

@@ -1,3 +0,0 @@
import moment from 'moment';
export const formatUTC = (d: any) => d && moment(d).utc().format('YYYY-MM-DDTHH:mm[Z]');

View File

@@ -1,3 +0,0 @@
import moment from 'moment';
export const fromNow = (timestamp: number) => moment(timestamp).fromNow(true);

View File

@@ -1,38 +0,0 @@
// @ts-nocheck
import { cubicOut } from 'svelte/easing';
// stolen from https://svelte.dev/repl/6d5239f09b0b4dc6aafeb70606a0fe94?version=3.46.4
// please add this svelte thanks ily <3
export function horizontalSlide(
node,
{ delay = 0, duration = 400, easing = cubicOut, direction = 'block' } = {}
) {
const style = getComputedStyle(node);
const opacity = +style.opacity;
const capitalized_logical_property = `${direction[0].toUpperCase()}${direction.slice(1)}`;
const size_value = parseFloat(style[`${direction}Size`]);
const padding_start_value = parseFloat(style[`padding${capitalized_logical_property}Start`]);
const padding_end_value = parseFloat(style[`padding${capitalized_logical_property}End`]);
const margin_start_value = parseFloat(style[`margin${capitalized_logical_property}Start`]);
const margin_end_value = parseFloat(style[`margin${capitalized_logical_property}End`]);
const border_width_start_value = parseFloat(
style[`border${capitalized_logical_property}StartWidth`]
);
const border_width_end_value = parseFloat(style[`border${capitalized_logical_property}EndWidth`]);
return {
delay,
duration,
easing,
css: (t) =>
'overflow: hidden;' +
`opacity: ${Math.min(t * 20, 1) * opacity};` +
`${direction}-size: ${t * size_value}px;` +
`padding-${direction}-start: ${t * padding_start_value}px;` +
`padding-${direction}-end: ${t * padding_end_value}px;` +
`margin-${direction}-start: ${t * margin_start_value}px;` +
`margin-${direction}-end: ${t * margin_end_value}px;` +
`border-${direction}-start-width: ${t * border_width_start_value}px;` +
`border-${direction}-start-width: ${t * border_width_end_value}px;`
};
}

View File

@@ -1,8 +0,0 @@
export const isValidUrl = (url: string) => {
try {
new URL(url);
return true;
} catch (err) {
return false;
}
};

View File

@@ -1,7 +0,0 @@
import moment from 'moment';
export const relativeTime = (date: string, withinDays: number = 7) => {
return moment().diff(moment(date), 'days') <= withinDays
? moment(date).fromNow()
: moment(date).format('on MMMM D, YYYY [at] h:mm A');
};

View File

@@ -1,3 +0,0 @@
export function supportsWebP() {
return document.createElement('canvas').toDataURL('image/webp').indexOf('data:image/webp') === 0;
}

View File

@@ -1,55 +0,0 @@
import { DateTriggerEvent, DAY_IN_MINUTES } from 'datetrigger';
const changeHue = (n: number) => document.documentElement.style.setProperty('--hue', n.toString());
/**
* Get the date of when easter should happen based off a given year
* @param Y The year to get Easter's date from
* @returns When Easter should happen
*/
function getEaster(Y: number): Date {
const a = Y % 19,
b = Math.floor(Y / 100),
c = Y % 100,
d = Math.floor(b / 4),
e = b % 4,
f = Math.floor((b + 8) / 25),
g = Math.floor((b - f + 1) / 3),
h = (19 * a + b - d - g + 15) % 30,
i = Math.floor(c / 4),
k = c % 4,
L = (32 + 2 * e + 2 * i - h - k) % 7,
m = Math.floor((a + 11 * h + 22 * L) / 451),
month = Math.floor((h + L - 7 * m + 114) / 31),
day = ((h + L - 7 * m + 114) % 31) + 1;
return new Date(Y, month - 1, day);
}
const currentYear = new Date().getFullYear();
export const events: DateTriggerEvent[] = [
// New Year.
new DateTriggerEvent(new Date(currentYear, 0, 1), DAY_IN_MINUTES, () => {
changeHue(240);
}),
// Christmas.
new DateTriggerEvent(new Date(currentYear, 11, 25), DAY_IN_MINUTES, () => {
changeHue(120);
}),
// Valentine's day.
new DateTriggerEvent(new Date(currentYear, 1, 14), DAY_IN_MINUTES, () => {
changeHue(300);
}),
// Halloween.
new DateTriggerEvent(new Date(currentYear, 9, 31), DAY_IN_MINUTES, () => {
changeHue(0);
}),
// Easter.
new DateTriggerEvent(getEaster(currentYear), DAY_IN_MINUTES, () => {
changeHue(100);
}),
// April Fools.
new DateTriggerEvent(new Date(currentYear, 3, 1), DAY_IN_MINUTES, () => {
changeHue(69);
})
];