mirror of
https://github.com/ReVanced/revanced-website.git
synced 2026-01-11 05:36:17 +00:00
feat: brand new Modal component (#41)
* feat: brand new Modal component * Apply suggestions from code review Co-authored-by: Ax333l <main@axelen.xyz>
This commit is contained in:
17
src/app.css
17
src/app.css
@@ -33,6 +33,17 @@ body {
|
|||||||
margin-top: 7rem;
|
margin-top: 7rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.button-reset {
|
||||||
|
background-color: transparent;
|
||||||
|
border: none;
|
||||||
|
font-family: inherit;
|
||||||
|
font-size: inherit;
|
||||||
|
font-style: inherit;
|
||||||
|
font-weight: inherit;
|
||||||
|
line-height: inherit;
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
|
|
||||||
:root {
|
:root {
|
||||||
--main-font: "Manrope", sans-serif;
|
--main-font: "Manrope", sans-serif;
|
||||||
--mono-font: ui-monospace,SFMono-Regular,SF Mono,Menlo,Consolas,Liberation Mono,monospace;
|
--mono-font: ui-monospace,SFMono-Regular,SF Mono,Menlo,Consolas,Liberation Mono,monospace;
|
||||||
@@ -100,6 +111,12 @@ h6 {
|
|||||||
font-size: 0.85rem;
|
font-size: 0.85rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
p {
|
||||||
|
color: var(--grey-five);
|
||||||
|
font-weight: 400;
|
||||||
|
font-size: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
/*---------------*/
|
/*---------------*/
|
||||||
|
|
||||||
::-webkit-scrollbar {
|
::-webkit-scrollbar {
|
||||||
|
|||||||
@@ -1,51 +1,56 @@
|
|||||||
import { browser } from "$app/environment";
|
import { browser } from '$app/environment';
|
||||||
|
|
||||||
import { dev_log } from "$lib/utils";
|
import { dev_log } from '$lib/utils';
|
||||||
|
|
||||||
const CACHE_KEY_PREFIX = "revanced_api_cache_l1";
|
const CACHE_KEY_PREFIX = 'revanced_api_cache_l1';
|
||||||
const L1_CACHE_VALIDITY = 5 * 60 * 1000; // 5 minutes
|
const L1_CACHE_VALIDITY = 5 * 60 * 1000; // 5 minutes
|
||||||
|
|
||||||
function l1_key_name(endpoint: string) {
|
function l1_key_name(endpoint: string) {
|
||||||
return `${CACHE_KEY_PREFIX}:${endpoint}`;
|
return `${CACHE_KEY_PREFIX}:${endpoint}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Get item from the cache
|
// Get item from the cache
|
||||||
export function get(endpoint: string) {
|
export function get(endpoint: string) {
|
||||||
if (!browser) {
|
if (!browser) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
const key_name = l1_key_name(endpoint);
|
const key_name = l1_key_name(endpoint);
|
||||||
const ls_data: { valid_until: number; data: any } | null = JSON.parse(localStorage.getItem(key_name));
|
const ls_data: { valid_until: number; data: any } | null = JSON.parse(
|
||||||
|
localStorage.getItem(key_name) as string
|
||||||
|
);
|
||||||
|
|
||||||
if (ls_data === null || ls_data.valid_until <= Date.now()) {
|
if (ls_data === null || ls_data.valid_until <= Date.now()) {
|
||||||
dev_log("Cache", `missed "${endpoint}"`);
|
dev_log('Cache', `missed "${endpoint}"`);
|
||||||
localStorage.removeItem(key_name);
|
localStorage.removeItem(key_name);
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
dev_log('Cache', `hit "${endpoint}"`);
|
||||||
dev_log("Cache", `hit "${endpoint}"`);
|
return ls_data.data;
|
||||||
return ls_data.data;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Update the cache
|
// Update the cache
|
||||||
export function update(endpoint: string, data: any) {
|
export function update(endpoint: string, data: any) {
|
||||||
if (!browser) {
|
if (!browser) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
localStorage.setItem(l1_key_name(endpoint), JSON.stringify({
|
localStorage.setItem(
|
||||||
data,
|
l1_key_name(endpoint),
|
||||||
valid_until: Date.now() + L1_CACHE_VALIDITY
|
JSON.stringify({
|
||||||
}));
|
data,
|
||||||
|
valid_until: Date.now() + L1_CACHE_VALIDITY
|
||||||
|
})
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Clear the cache
|
// Clear the cache and reload
|
||||||
export function clear() {
|
export function clear_and_reload() {
|
||||||
for (const key of Object.keys(localStorage)) {
|
for (const key of Object.keys(localStorage)) {
|
||||||
if (key.startsWith(CACHE_KEY_PREFIX)) {
|
if (key.startsWith(CACHE_KEY_PREFIX)) {
|
||||||
localStorage.removeItem(key);
|
localStorage.removeItem(key);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
location.reload();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,9 +2,10 @@ import { browser } from "$app/environment";
|
|||||||
|
|
||||||
const URL_KEY = "revanced_api_url";
|
const URL_KEY = "revanced_api_url";
|
||||||
|
|
||||||
|
export const default_base_url = "https://releases.revanced.app";
|
||||||
|
|
||||||
// Get base URL
|
// Get base URL
|
||||||
export function api_base_url(): string {
|
export function api_base_url(): string {
|
||||||
const default_base_url = "https://releases.revanced.app";
|
|
||||||
if (browser) {
|
if (browser) {
|
||||||
return localStorage.getItem(URL_KEY) || default_base_url;
|
return localStorage.getItem(URL_KEY) || default_base_url;
|
||||||
}
|
}
|
||||||
|
|||||||
126
src/lib/components/atoms/ApiSettingsButton.svelte
Normal file
126
src/lib/components/atoms/ApiSettingsButton.svelte
Normal file
@@ -0,0 +1,126 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import * as settings from '../../../data/api/settings';
|
||||||
|
import Modal from '$lib/components/atoms/Modal.svelte';
|
||||||
|
import { clear_and_reload } from '../../../data/api/cache';
|
||||||
|
|
||||||
|
let url = settings.api_base_url();
|
||||||
|
|
||||||
|
function save() {
|
||||||
|
settings.set_api_base_url(url);
|
||||||
|
clear_and_reload();
|
||||||
|
}
|
||||||
|
|
||||||
|
function reset() {
|
||||||
|
url = settings.default_base_url;
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<Modal icon="/icons/settings.svg" alt="Settings">
|
||||||
|
<div class="settings-menu">
|
||||||
|
<h3>Configure the backend</h3>
|
||||||
|
<div class="url-stuff">
|
||||||
|
<h4>API URL:</h4>
|
||||||
|
<div class="input-wrapper">
|
||||||
|
<input name="api-url" type="text" bind:value={url} />
|
||||||
|
<button class="button-reset" on:click={reset}>
|
||||||
|
<img src="/icons/reset.svg" alt="Reset" />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="btns">
|
||||||
|
<button class="save-url-btn" on:click={save}>
|
||||||
|
<h4>Save</h4>
|
||||||
|
</button>
|
||||||
|
<button class="clear-cache-btn" on:click={clear_and_reload}>
|
||||||
|
<h4>Clear cache</h4>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</Modal>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
div.settings-menu {
|
||||||
|
position: fixed;
|
||||||
|
width: min(95%, 500px);
|
||||||
|
/* width: clamp(45vw, 850px, 850px); */
|
||||||
|
top: 50%;
|
||||||
|
left: 50%;
|
||||||
|
transform: translate(-50%, -50%);
|
||||||
|
padding: 40px;
|
||||||
|
border-radius: 12px;
|
||||||
|
background-color: var(--grey-six);
|
||||||
|
display: flex;
|
||||||
|
user-select: none;
|
||||||
|
gap: 5%;
|
||||||
|
z-index: 999;
|
||||||
|
white-space: normal;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 2px;
|
||||||
|
box-shadow: 0 3rem 5rem rgba(0, 0, 0, 0.3);
|
||||||
|
}
|
||||||
|
|
||||||
|
div.btns {
|
||||||
|
display: flex;
|
||||||
|
gap: 1rem;
|
||||||
|
text-align: center;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btns button {
|
||||||
|
min-width: max-content;
|
||||||
|
font-size: 1rem;
|
||||||
|
color: var(--white);
|
||||||
|
font-weight: 600;
|
||||||
|
border: none;
|
||||||
|
border-radius: 12px;
|
||||||
|
flex-grow: 2;
|
||||||
|
padding: 1rem 1.75rem;
|
||||||
|
display: block;
|
||||||
|
cursor: pointer;
|
||||||
|
background-color: var(--grey-two);
|
||||||
|
transition: transform 0.4s var(--bezier-one), filter 0.4s var(--bezier-one);
|
||||||
|
user-select: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btns .save-url-btn {
|
||||||
|
background-color: var(--accent-color);
|
||||||
|
box-shadow: 0px 0px 32px 1px var(--accent-color-glow);
|
||||||
|
flex-grow: 1;
|
||||||
|
}
|
||||||
|
.save-url-btn h4 {
|
||||||
|
color: var(--grey-four);
|
||||||
|
}
|
||||||
|
div.input-wrapper {
|
||||||
|
display: flex;
|
||||||
|
background-color: #2a2c33;
|
||||||
|
padding: 12px;
|
||||||
|
width: 100%;
|
||||||
|
margin: 10px 0 10px 0;
|
||||||
|
border-radius: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
img {
|
||||||
|
height: 25px;
|
||||||
|
}
|
||||||
|
|
||||||
|
input[type='text'] {
|
||||||
|
background-color: transparent;
|
||||||
|
border: 0;
|
||||||
|
color: white;
|
||||||
|
font-size: 1rem;
|
||||||
|
outline: none;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
h3 {
|
||||||
|
margin: 0 0 20px 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 780px) {
|
||||||
|
div.settings-menu {
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 2rem;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -9,7 +9,9 @@
|
|||||||
|
|
||||||
<a {href} {target}>
|
<a {href} {target}>
|
||||||
<div class={type} style="width: {maxWidth ? '100%' : 'max-content'}">
|
<div class={type} style="width: {maxWidth ? '100%' : 'max-content'}">
|
||||||
<img src="../icons/{icon}.svg" alt={icon} />
|
{#if icon}
|
||||||
|
<img src="../icons/{icon}.svg" alt={icon} />
|
||||||
|
{/if}
|
||||||
<slot />
|
<slot />
|
||||||
</div>
|
</div>
|
||||||
</a>
|
</a>
|
||||||
@@ -46,8 +48,6 @@
|
|||||||
filter: brightness(85%);
|
filter: brightness(85%);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
div,
|
div,
|
||||||
.button-secondary {
|
.button-secondary {
|
||||||
display: flex;
|
display: flex;
|
||||||
|
|||||||
71
src/lib/components/atoms/Modal.svelte
Normal file
71
src/lib/components/atoms/Modal.svelte
Normal file
@@ -0,0 +1,71 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import { click_outside } from '$lib/utils';
|
||||||
|
import { fade } from 'svelte/transition';
|
||||||
|
|
||||||
|
let modalOpen = false;
|
||||||
|
export let icon: string;
|
||||||
|
export let alt: string;
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<button class="modal-btn" on:click={() => (modalOpen = !modalOpen)}>
|
||||||
|
<img src={icon} {alt} />
|
||||||
|
</button>
|
||||||
|
{#if modalOpen}
|
||||||
|
<div class="overlay" />
|
||||||
|
<div
|
||||||
|
class="modal-container"
|
||||||
|
role="dialog"
|
||||||
|
aria-modal="true"
|
||||||
|
use:click_outside
|
||||||
|
on:click_outside={() => (modalOpen = false)}
|
||||||
|
transition:fade={{ duration: 125 }}
|
||||||
|
>
|
||||||
|
<slot />
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
button.modal-btn {
|
||||||
|
border: 0;
|
||||||
|
background-color: transparent;
|
||||||
|
transition-timing-function: var(--bezier-one);
|
||||||
|
transition-duration: 0.25s;
|
||||||
|
padding: 10px 25px;
|
||||||
|
border-radius: 200px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
button.modal-btn:hover {
|
||||||
|
color: var(--white);
|
||||||
|
background-color: var(--grey-one);
|
||||||
|
}
|
||||||
|
|
||||||
|
img {
|
||||||
|
height: 1.75rem;
|
||||||
|
width: auto;
|
||||||
|
cursor: pointer;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.overlay {
|
||||||
|
position: fixed;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
background-color: rgba(0, 0, 0, 0.5);
|
||||||
|
z-index: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 768px) {
|
||||||
|
button.modal-btn {
|
||||||
|
padding: 1rem 1.5rem;
|
||||||
|
border-radius: 16px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -1,10 +1,10 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import Navigation from '../atoms/NavButton.svelte';
|
import Navigation from '../atoms/NavButton.svelte';
|
||||||
import { page } from '$app/stores';
|
import { page } from '$app/stores';
|
||||||
import Button from '../atoms/Button.svelte';
|
|
||||||
import MobileDropdown from './MobileDropdown.svelte';
|
import MobileDropdown from './MobileDropdown.svelte';
|
||||||
import { onMount } from 'svelte';
|
import { onMount } from 'svelte';
|
||||||
import RouterEvents from '../../../data/RouterEvents';
|
import RouterEvents from '../../../data/RouterEvents';
|
||||||
|
import ApiSettingsButton from '$lib/components/atoms/ApiSettingsButton.svelte';
|
||||||
|
|
||||||
let menuOpen = false;
|
let menuOpen = false;
|
||||||
|
|
||||||
@@ -25,7 +25,6 @@
|
|||||||
<img src="/logo.svg" class="logo-image" alt="ReVanced Logo" />
|
<img src="/logo.svg" class="logo-image" alt="ReVanced Logo" />
|
||||||
</span>
|
</span>
|
||||||
</a>
|
</a>
|
||||||
|
|
||||||
<span class="desktop">
|
<span class="desktop">
|
||||||
<Navigation href="/">Home</Navigation>
|
<Navigation href="/">Home</Navigation>
|
||||||
<Navigation href="/download">Download</Navigation>
|
<Navigation href="/download">Download</Navigation>
|
||||||
@@ -44,14 +43,12 @@
|
|||||||
<Navigation href="/contributors/">
|
<Navigation href="/contributors/">
|
||||||
<img src="/icons/contrib.svg" alt="Contributors" />
|
<img src="/icons/contrib.svg" alt="Contributors" />
|
||||||
</Navigation>
|
</Navigation>
|
||||||
<Navigation href="/api-settings/">
|
<ApiSettingsButton />
|
||||||
<img src="/icons/settings.svg" alt="Settings" />
|
|
||||||
</Navigation>
|
|
||||||
</span>
|
</span>
|
||||||
|
|
||||||
<!-- Should probably be moved to its own component. -->
|
<!-- Should probably be moved to its own component. -->
|
||||||
<button
|
<button
|
||||||
class="menu-btn mobile"
|
class="menu-btn button-reset mobile"
|
||||||
class:open={menuOpen}
|
class:open={menuOpen}
|
||||||
on:click={() => {
|
on:click={() => {
|
||||||
menuOpen = !menuOpen;
|
menuOpen = !menuOpen;
|
||||||
@@ -76,9 +73,9 @@
|
|||||||
<Navigation href="/contributors/">
|
<Navigation href="/contributors/">
|
||||||
<img src="/icons/contrib.svg" alt="Contributors" />
|
<img src="/icons/contrib.svg" alt="Contributors" />
|
||||||
</Navigation>
|
</Navigation>
|
||||||
<Navigation href="/api-settings/">
|
<ApiSettingsButton on:click={() => {
|
||||||
<img src="/icons/settings.svg" alt="Settings" />
|
menuOpen = false;
|
||||||
</Navigation>
|
}}/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</MobileDropdown>
|
</MobileDropdown>
|
||||||
@@ -171,6 +168,7 @@
|
|||||||
display: none !important;
|
display: none !important;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.menu-btn {
|
.menu-btn {
|
||||||
user-select: none;
|
user-select: none;
|
||||||
position: relative;
|
position: relative;
|
||||||
@@ -181,17 +179,8 @@
|
|||||||
height: 60px;
|
height: 60px;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
transition: all 0.5s var(--bezier-one);
|
transition: all 0.5s var(--bezier-one);
|
||||||
|
|
||||||
/* We don't want it to look like a normal button. */
|
|
||||||
background-color: transparent;
|
|
||||||
border: none;
|
|
||||||
font-family: inherit;
|
|
||||||
font-size: inherit;
|
|
||||||
font-style: inherit;
|
|
||||||
font-weight: inherit;
|
|
||||||
line-height: inherit;
|
|
||||||
padding: 0;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.menu-btn__burger {
|
.menu-btn__burger {
|
||||||
width: 25px;
|
width: 25px;
|
||||||
height: 2px;
|
height: 2px;
|
||||||
|
|||||||
@@ -1,26 +0,0 @@
|
|||||||
<script lang="ts">
|
|
||||||
import * as settings from '../../data/api/settings';
|
|
||||||
import { clear } from '../../data/api/cache';
|
|
||||||
|
|
||||||
let url = settings.api_base_url();
|
|
||||||
|
|
||||||
function handler() {
|
|
||||||
clear();
|
|
||||||
settings.set_api_base_url(url);
|
|
||||||
location.reload(true);
|
|
||||||
}
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<section class="settings">
|
|
||||||
<input name="api-url" type="text" bind:value={url} />
|
|
||||||
<button on:click={handler}>Save</button>
|
|
||||||
</section>
|
|
||||||
<section class="cache">
|
|
||||||
<button on:click={clear}>Clear cache</button>
|
|
||||||
</section>
|
|
||||||
|
|
||||||
<style>
|
|
||||||
.settings {
|
|
||||||
padding-top: 5rem;
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
1
static/icons/reset.svg
Normal file
1
static/icons/reset.svg
Normal file
@@ -0,0 +1 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" height="48" width="48" fill="#ACC1D2"><path d="M14 38v-3h14.45q3.5 0 6.025-2.325Q37 30.35 37 26.9t-2.525-5.775Q31.95 18.8 28.45 18.8H13.7l5.7 5.7-2.1 2.1L8 17.3 17.3 8l2.1 2.1-5.7 5.7h14.7q4.75 0 8.175 3.2Q40 22.2 40 26.9t-3.425 7.9Q33.15 38 28.4 38Z"/></svg>
|
||||||
|
After Width: | Height: | Size: 299 B |
Reference in New Issue
Block a user