feat: API outage banner (#254)

Co-authored-by: oSumAtrIX <johan.melkonyan1@web.de>
Co-authored-by: oSumAtrIX <github@osumatrix.me>
Co-authored-by: Ushie <ushiekane@gmail.com>
This commit is contained in:
madkärma 2025-02-11 23:08:27 +01:00 committed by GitHub
parent 87ce20ff56
commit adf569c6be
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
10 changed files with 975 additions and 660 deletions

1230
pnpm-lock.yaml generated

File diff suppressed because it is too large Load Diff

View File

@ -34,7 +34,7 @@ body {
.wrapper {
margin-inline: auto;
width: min(90%, 80rem);
margin-top: 7rem;
margin-top: 2.6rem;
}
:root {
@ -73,6 +73,9 @@ body {
--surface-nine: hsl(calc(var(--hue, 206) + 24), 9.5%, 17.5%);
--red-one: hsl(333, 84%, 62%);
--red-two: hsl(357, 74%, 60%);
--yellow-one: hsl(59, 100%, 72%);
--bezier-one: cubic-bezier(0.25, 0.46, 0.45, 0.94);
--drop-shadow-one: 0px 4px 5px 0px rgba(0, 0, 0, 0.14), 0px 1px 10px 0px rgba(0, 0, 0, 0.12),

View File

@ -74,6 +74,15 @@ async function about(): Promise<AboutData> {
return { about: json };
}
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: {
@ -100,5 +109,10 @@ export const queries = {
queryKey: ['info'],
queryFn: about,
staleTime
},
ping: {
queryKey: ['ping'],
queryFn: ping,
staleTime
}
};

View File

@ -4,21 +4,48 @@ import { RV_API_URL } from '$env/static/public';
export const default_api_url = RV_API_URL;
const URL_KEY = 'revanced_api_url';
const STATUS_KEY = 'revanced_status_url';
function set_status_url(apiUrl: string) {
fetch(`${apiUrl}/v4/about`)
.then((response) => (response.ok ? response.json() : null))
.then((data) => {
if (data?.status) {
localStorage.setItem(STATUS_KEY, data.status);
console.log('status is now ' + localStorage.getItem(STATUS_KEY));
}
});
}
// Get base URL
export function api_base_url(): string {
if (browser) {
return localStorage.getItem(URL_KEY) || default_api_url;
const apiUrl = localStorage.getItem(URL_KEY) || default_api_url;
if (!localStorage.getItem(STATUS_KEY)) {
set_status_url(apiUrl);
}
return apiUrl;
}
return default_api_url;
}
export function status_url(): string | null {
if (browser) {
return localStorage.getItem(STATUS_KEY) || null;
}
return null;
}
// (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_status_url(url);
}
}

View File

@ -3,16 +3,20 @@
import { horizontalSlide } from '$util/horizontalSlide';
import { fade } from 'svelte/transition';
import { expoOut } from 'svelte/easing';
import { createQuery } from '@tanstack/svelte-query';
import Navigation from './NavButton.svelte';
import Modal from '$lib/components/Dialogue.svelte';
import Button from '$lib/components/Button.svelte';
import Banner from '$lib/components/Banner.svelte';
import Query from '$lib/components/Query.svelte';
import Cog from 'svelte-material-icons/Cog.svelte';
import Replay from 'svelte-material-icons/Replay.svelte';
import { api_base_url, set_api_base_url, default_api_url } from '$data/api/settings';
import { status_url, api_base_url, set_api_base_url, default_api_url } from '$data/api/settings';
import RouterEvents from '$data/RouterEvents';
import { queries } from '$data/api';
import { useQueryClient } from '@tanstack/svelte-query';
@ -31,6 +35,7 @@
}
let url = api_base_url();
const statusUrl = status_url();
function save() {
set_api_base_url(url);
@ -44,6 +49,7 @@
let menuOpen = false;
let modalOpen = false;
let y: number;
const pingQuery = () => createQuery(['ping'], queries.ping);
onMount(() => {
return RouterEvents.subscribe((event) => {
@ -56,53 +62,88 @@
<svelte:window bind:scrollY={y} />
<nav class:scrolled={y > 10}>
<a class="menu-btn skiptab-btn" href="#skiptab">Skip navigation</a>
<div id="nav-container">
<Query query={pingQuery()} let:data>
{#if !data}
<span class="banner">
<Banner level="caution" permanent>
The API is currently unresponsive and some services may not work correctly. {#if statusUrl}
Check the <a href={statusUrl} target="_blank" rel="noopener noreferrer">status page</a> for
updates.
{/if}
</Banner>
</span>
{/if}
</Query>
<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>
<nav class:scrolled={y > 10}>
<a class="menu-btn skiptab-btn" href="#skiptab">Skip navigation</a>
{#key menuOpen}
<div
class="nav-wrapper"
class:desktop-only={!menuOpen}
transition:horizontalSlide={{ direction: 'inline', easing: expoOut, duration: 400 }}
>
<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>
<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">
<button on:click={() => (modalOpen = !modalOpen)} aria-label="Settings">
<Cog size="20px" color="var(--surface-six)" />
</button>
</div>
</div>
{/key}
{#if menuOpen}
<div
class="overlay mobile-only"
transition:fade={{ duration: 350 }}
<button
class="menu-btn mobile-only"
on:click={() => (menuOpen = !menuOpen)}
on:keypress={() => (menuOpen = !menuOpen)}
/>
{/if}
</nav>
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 id="banner-pad">
<Query query={pingQuery()} let:data>
{#if !data}
<span class="banner">
<Banner level="caution" permanent>
The API is currently unresponsive and some services may not work correctly. {#if statusUrl}
Check the
<a href={statusUrl} target="_blank" rel="noopener noreferrer">status page</a> for
updates.
{/if}
</Banner>
</span>
{/if}
</Query>
</div>
<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>
<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">
<button on:click={() => (modalOpen = !modalOpen)} aria-label="Settings">
<Cog size="20px" color="var(--surface-six)" />
</button>
</div>
</div>
</div>
{/key}
{#if menuOpen}
<div
class="overlay mobile-only"
transition:fade={{ duration: 350 }}
on:click={() => (menuOpen = !menuOpen)}
on:keypress={() => (menuOpen = !menuOpen)}
/>
{/if}
</nav>
</div>
<!-- settings -->
<Modal bind:modalOpen>
@ -126,7 +167,7 @@
</svelte:fragment>
</Modal>
<style>
<style lang="scss">
#logo {
padding: 0.5rem;
}
@ -160,15 +201,26 @@
top: 30px;
}
#nav-container {
position: sticky;
z-index: 666;
width: 100%;
&:has(.nav-buttons > li:first-child.selected) {
margin-bottom: 2.65rem;
&:has(.banner) {
margin-bottom: 1.5rem;
}
}
}
nav {
position: fixed;
top: 0;
display: flex;
gap: 2rem;
justify-content: space-between;
align-items: center;
padding: 1rem 2rem;
z-index: 666;
height: 70px;
background-color: var(--surface-eight);
width: 100%;
@ -181,10 +233,6 @@
gap: 2rem;
}
a {
display: flex;
}
img {
height: 22px;
}
@ -220,21 +268,45 @@
}
}
#banner-pad {
display: none;
}
#nav-wrapper-container {
width: 100%;
}
@media (max-width: 767px) {
#banner-pad {
display: block;
width: 100vw;
visibility: hidden;
}
#nav-container:has(.nav-buttons > li:first-child.selected):has(.banner) {
margin-bottom: 0rem;
}
#nav-wrapper-container {
overflow: hidden;
position: fixed;
width: 20rem;
top: 0;
left: 0;
height: 100%;
background-color: var(--surface-eight);
z-index: 100;
}
.nav-wrapper {
flex-direction: column;
gap: 0.5rem;
height: 100%;
margin: 0 auto;
position: fixed;
width: 20rem;
top: 0px;
border-radius: 0px 24px 24px 0px;
left: 0px;
background-color: var(--surface-eight);
padding: 1rem;
padding-top: 6rem;
z-index: 100;
}
.desktop-only {

View File

@ -0,0 +1,160 @@
<script lang="ts">
import Info from 'svelte-material-icons/InformationOutline.svelte';
import Warning from 'svelte-material-icons/AlertOutline.svelte';
import Caution from 'svelte-material-icons/AlertCircleOutline.svelte';
import { createEventDispatcher } from 'svelte';
import Button from './Button.svelte';
export let level: 'info' | 'warning' | 'caution' = 'info';
export let permanent: boolean = false;
const icons = { info: Info, warning: Warning, caution: Caution };
const dispatch = createEventDispatcher();
let closed: boolean = false;
const dismissBanner = () => {
closed = true;
dispatch('dismissed');
};
</script>
<div class="banner-container" class:closed class:permanent>
<div class="banner {level}">
<div class="banner-text">
<svelte:component this={icons[level]} size={permanent ? 22.4 : 32} />
<span><slot /></span>
</div>
{#if !permanent}
<Button type="text" icon="close" on:click={dismissBanner}>Dismiss</Button>
{/if}
</div>
</div>
<style>
.banner-container,
.banner-container *,
.banner-container :global(*) {
transition: none;
}
.banner-text :global(a) {
color: inherit;
text-decoration: none;
font-weight: 700;
}
.banner-text :global(a:hover) {
text-decoration: underline;
}
.banner-container {
display: flex;
justify-content: center;
width: 100%;
}
.banner-container:not(.permanent) {
animation: dropDown var(--bezier-one) 0.7s forwards;
}
.banner-container.closed {
animation: swipeUp var(--bezier-one) 1.5s forwards;
}
.banner-container.permanent {
font-size: 0.87rem;
}
.banner {
margin: 0;
padding: 1.5rem 1.7rem;
box-sizing: border-box;
display: flex;
align-items: center;
justify-content: space-between;
gap: 1.3rem;
margin: 0.7rem 1rem;
border-radius: 1rem;
}
.banner-container.permanent > .banner {
padding: 0.5rem 0.7rem;
margin: 0;
border-radius: 0;
width: 100%;
}
.banner-text {
display: flex;
align-items: center;
justify-content: center;
flex: 1;
gap: 0.55rem;
word-wrap: break-word;
}
.banner.info {
background-color: var(--surface-four);
color: var(--text-one);
}
.banner.warning {
background-color: var(--yellow-one);
color: #000;
}
.banner.warning > :global(button) {
color: #000;
}
.banner.warning > :global(button img) {
filter: grayscale(1) brightness(0); /* Make the icon black */
}
.banner.caution {
background-color: var(--red-two);
color: #000;
}
.banner.caution > :global(button) {
color: #000;
}
.banner.caution > :global(button img) {
filter: grayscale(1) brightness(0); /* Make the icon white */
}
.banner > :global(button):hover {
text-decoration: underline;
}
@media screen and (max-width: 767px) {
.banner {
flex-direction: column;
padding: 1.1rem 1.3rem;
}
.banner > :global(button) {
align-self: flex-end;
}
}
@keyframes dropDown {
0% {
top: -100%;
}
100% {
top: 0;
}
}
@keyframes swipeUp {
0% {
top: 0;
}
100% {
top: -100%;
}
}
</style>

View File

@ -110,6 +110,7 @@
<QueryClientProvider client={queryClient}>
<NavHost />
<Dialogue bind:modalOpen={showConsentModal} notDismissible>
<svelte:fragment slot="title">It's your choice</svelte:fragment>
<svelte:fragment slot="description">

View File

@ -168,7 +168,6 @@
.wrap {
margin-inline: auto;
width: min(87%, 100rem);
margin-top: 7rem;
}
.wrappezoid {
height: calc(100vh - 225px);

View File

@ -211,7 +211,6 @@
main {
display: flex;
flex-direction: column;
margin-top: 7rem;
// support revanced and heart thingy
section {

View File

@ -207,7 +207,7 @@
}
.search {
padding-top: 5rem;
padding-top: 0.6rem;
padding-bottom: 1.25rem;
background-color: var(--surface-eight);
}
@ -258,10 +258,6 @@
display: none;
}
.search {
padding-top: 4.5rem;
}
.patches-container {
margin-top: 1rem;
margin-bottom: 1.5rem;