feat: use svelte query (#63)

This commit is contained in:
Ax333l 2023-02-11 21:22:55 +01:00 committed by GitHub
parent 368b2c9ee5
commit e1b37fab7d
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
17 changed files with 367 additions and 315 deletions

85
package-lock.json generated
View File

@ -8,6 +8,10 @@
"name": "revanced-website",
"version": "0.0.1",
"dependencies": {
"@tanstack/query-core": "^4.24.4",
"@tanstack/query-persist-client-core": "^4.24.4",
"@tanstack/query-sync-storage-persister": "^4.24.4",
"@tanstack/svelte-query": "^4.24.4",
"asciidoctor": "^2.2.6",
"marked": "^4.1.1"
},
@ -624,6 +628,54 @@
"vite": "^4.0.0"
}
},
"node_modules/@tanstack/query-core": {
"version": "4.24.4",
"resolved": "https://registry.npmjs.org/@tanstack/query-core/-/query-core-4.24.4.tgz",
"integrity": "sha512-9dqjv9eeB6VHN7lD3cLo16ZAjfjCsdXetSAD5+VyKqLUvcKTL0CklGQRJu+bWzdrS69R6Ea4UZo8obHYZnG6aA==",
"funding": {
"type": "github",
"url": "https://github.com/sponsors/tannerlinsley"
}
},
"node_modules/@tanstack/query-persist-client-core": {
"version": "4.24.4",
"resolved": "https://registry.npmjs.org/@tanstack/query-persist-client-core/-/query-persist-client-core-4.24.4.tgz",
"integrity": "sha512-t4BR/th3tu2tkfF0Tcl5z+MbglDjTdIbsLZYlly7l2M6EhGXRjOLbvVUA5Kcx7E2a81Vup0zx0z0wlx+RPGfmg==",
"dependencies": {
"@tanstack/query-core": "4.24.4"
},
"funding": {
"type": "github",
"url": "https://github.com/sponsors/tannerlinsley"
}
},
"node_modules/@tanstack/query-sync-storage-persister": {
"version": "4.24.4",
"resolved": "https://registry.npmjs.org/@tanstack/query-sync-storage-persister/-/query-sync-storage-persister-4.24.4.tgz",
"integrity": "sha512-0wffVqoOydMc1TDjOiATv/TM8wJfMpRcM82Cr19TuepListopTsuZ3RzSzLKBCo8WRl/0zCR1Ti9t1zn+Oai/A==",
"dependencies": {
"@tanstack/query-persist-client-core": "4.24.4"
},
"funding": {
"type": "github",
"url": "https://github.com/sponsors/tannerlinsley"
}
},
"node_modules/@tanstack/svelte-query": {
"version": "4.24.4",
"resolved": "https://registry.npmjs.org/@tanstack/svelte-query/-/svelte-query-4.24.4.tgz",
"integrity": "sha512-B2N+nNPOe9wxZ01i3Ebvkc8vDX9ZDQTiaazXqpld7O27R+zhIBlk6MU8vNzPBDSKrhhl2kak8SwyJOExsVZQXA==",
"dependencies": {
"@tanstack/query-core": "4.24.4"
},
"funding": {
"type": "github",
"url": "https://github.com/sponsors/tannerlinsley"
},
"peerDependencies": {
"svelte": "^3.54.0"
}
},
"node_modules/@types/cookie": {
"version": "0.5.1",
"resolved": "https://registry.npmjs.org/@types/cookie/-/cookie-0.5.1.tgz",
@ -3200,7 +3252,6 @@
"version": "3.55.0",
"resolved": "https://registry.npmjs.org/svelte/-/svelte-3.55.0.tgz",
"integrity": "sha512-uGu2FVMlOuey4JoKHKrpZFkoYyj0VLjJdz47zX5+gVK5odxHM40RVhar9/iK2YFRVxvfg9FkhfVlR0sjeIrOiA==",
"dev": true,
"engines": {
"node": ">= 8"
}
@ -3990,6 +4041,35 @@
"vitefu": "^0.2.3"
}
},
"@tanstack/query-core": {
"version": "4.24.4",
"resolved": "https://registry.npmjs.org/@tanstack/query-core/-/query-core-4.24.4.tgz",
"integrity": "sha512-9dqjv9eeB6VHN7lD3cLo16ZAjfjCsdXetSAD5+VyKqLUvcKTL0CklGQRJu+bWzdrS69R6Ea4UZo8obHYZnG6aA=="
},
"@tanstack/query-persist-client-core": {
"version": "4.24.4",
"resolved": "https://registry.npmjs.org/@tanstack/query-persist-client-core/-/query-persist-client-core-4.24.4.tgz",
"integrity": "sha512-t4BR/th3tu2tkfF0Tcl5z+MbglDjTdIbsLZYlly7l2M6EhGXRjOLbvVUA5Kcx7E2a81Vup0zx0z0wlx+RPGfmg==",
"requires": {
"@tanstack/query-core": "4.24.4"
}
},
"@tanstack/query-sync-storage-persister": {
"version": "4.24.4",
"resolved": "https://registry.npmjs.org/@tanstack/query-sync-storage-persister/-/query-sync-storage-persister-4.24.4.tgz",
"integrity": "sha512-0wffVqoOydMc1TDjOiATv/TM8wJfMpRcM82Cr19TuepListopTsuZ3RzSzLKBCo8WRl/0zCR1Ti9t1zn+Oai/A==",
"requires": {
"@tanstack/query-persist-client-core": "4.24.4"
}
},
"@tanstack/svelte-query": {
"version": "4.24.4",
"resolved": "https://registry.npmjs.org/@tanstack/svelte-query/-/svelte-query-4.24.4.tgz",
"integrity": "sha512-B2N+nNPOe9wxZ01i3Ebvkc8vDX9ZDQTiaazXqpld7O27R+zhIBlk6MU8vNzPBDSKrhhl2kak8SwyJOExsVZQXA==",
"requires": {
"@tanstack/query-core": "4.24.4"
}
},
"@types/cookie": {
"version": "0.5.1",
"resolved": "https://registry.npmjs.org/@types/cookie/-/cookie-0.5.1.tgz",
@ -5799,8 +5879,7 @@
"svelte": {
"version": "3.55.0",
"resolved": "https://registry.npmjs.org/svelte/-/svelte-3.55.0.tgz",
"integrity": "sha512-uGu2FVMlOuey4JoKHKrpZFkoYyj0VLjJdz47zX5+gVK5odxHM40RVhar9/iK2YFRVxvfg9FkhfVlR0sjeIrOiA==",
"dev": true
"integrity": "sha512-uGu2FVMlOuey4JoKHKrpZFkoYyj0VLjJdz47zX5+gVK5odxHM40RVhar9/iK2YFRVxvfg9FkhfVlR0sjeIrOiA=="
},
"svelte-check": {
"version": "2.10.3",

View File

@ -35,6 +35,10 @@
},
"type": "module",
"dependencies": {
"@tanstack/query-core": "^4.24.4",
"@tanstack/query-persist-client-core": "^4.24.4",
"@tanstack/query-sync-storage-persister": "^4.24.4",
"@tanstack/svelte-query": "^4.24.4",
"asciidoctor": "^2.2.6",
"marked": "^4.1.1"
}

View File

@ -1,56 +0,0 @@
import { browser } from '$app/environment';
import { dev_log } from '$lib/utils';
const CACHE_KEY_PREFIX = 'revanced_api_cache_l1';
const L1_CACHE_VALIDITY = 5 * 60 * 1000; // 5 minutes
function l1_key_name(endpoint: string) {
return `${CACHE_KEY_PREFIX}:${endpoint}`;
}
// Get item from the cache
export function get(endpoint: string) {
if (!browser) {
return null;
}
const key_name = l1_key_name(endpoint);
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()) {
dev_log('Cache', `missed "${endpoint}"`);
localStorage.removeItem(key_name);
return null;
}
dev_log('Cache', `hit "${endpoint}"`);
return ls_data.data;
}
// Update the cache
export function update(endpoint: string, data: any) {
if (!browser) {
return;
}
localStorage.setItem(
l1_key_name(endpoint),
JSON.stringify({
data,
valid_until: Date.now() + L1_CACHE_VALIDITY
})
);
}
// Clear the cache and reload
export function clear_and_reload() {
for (const key of Object.keys(localStorage)) {
if (key.startsWith(CACHE_KEY_PREFIX)) {
localStorage.removeItem(key);
}
}
location.reload();
}

View File

@ -1,130 +1,24 @@
import type { Readable, Subscriber, Unsubscriber, Writable } from 'svelte/store';
import { writable } from 'svelte/store';
import { error } from '@sveltejs/kit';
import { building, browser, dev } from '$app/environment';
import * as settings from './settings';
import * as cache from './cache';
export class API<T> implements Readable<T> {
private store: Writable<T>;
// True if we have or are about to request data from the API.
has_requested: boolean;
// `transform` will transform the data received from the API.
constructor(
public readonly endpoint: string,
private readonly default_value: T,
private readonly transform: (v: any) => T = (v) => v as T
) {
// Initialize with cached data if possible.
const cached_data = cache.get(this.endpoint);
this.has_requested = cached_data !== null;
this.store = writable(cached_data || this.default_value);
}
private url() {
return `${settings.api_base_url()}/${this.endpoint}`;
}
// Please don't call this directly
private async _update(fetch_fn: typeof fetch) {
// Try to get data from the cache.
let data = cache.get(this.endpoint);
if (data === null) {
// Fetch and transform data
const response = await fetch_fn(this.url());
data = this.transform(await response.json());
// Update the cache.
cache.update(this.endpoint, data);
}
this.store.set(data);
}
// Retrieve data and update.
private update(fetch_fn = fetch) {
// Make sure we set this immediately outside of the async function to avoid JS event loop weirdness.
this.has_requested = true;
return this._update(fetch_fn);
}
// Start retrieving data if needed.
retrieve_if_needed() {
if (!this.has_requested) {
return this.update();
}
return Promise.resolve();
}
// Implements the load function found in `+page/layout.ts` files.
page_load_impl() {
return async ({ fetch }) => {
if (building) {
return {};
}
// Might be better to actually return some data from the load function and use that on the client.
if (!(dev || browser || building)) {
throw new Error(
'The API client is not optimized for production server-side rendering. Please change that :)'
);
}
try {
await this.update(fetch);
return {};
} catch (e) {
console.error(e);
throw error(504, 'API Request Error');
}
};
}
// Implement Svelte store.
subscribe(run: Subscriber<T>, invalidate?: any): Unsubscriber {
// Make sure we have up-to-date data from the API.
if (browser) {
this.retrieve_if_needed();
}
return this.store.subscribe(run, invalidate);
}
}
// API Endpoints
import type { Patch, Repository, Tool } from '../../utils/types';
import { dev_log } from '$lib/utils';
import type { Patch, Repository, Tool } from '$lib/types';
export type ReposData = Repository[];
export type PatchesData = { patches: Patch[]; packages: string[] };
export type ToolsData = { [repo: string]: Tool };
export const repositories = new API<ReposData>('contributors', [], (json) => json.repositories);
async function get_json(endpoint: string) {
const url = `${settings.api_base_url()}/${endpoint}`;
return await fetch(url).then((r) => r.json());
}
// It needs to look this way to not break everything.
const tools_placeholder: ToolsData = {
'revanced/revanced-manager': {
version: 'v0.0.0',
timestamp: '',
repository: '',
assets: [
{
url: '',
name: '',
content_type: '',
size: null
}
]
}
};
async function repositories(): Promise<ReposData> {
return await get_json('contributors').then((json) => json.repositories);
}
export const tools = new API<ToolsData>('tools', tools_placeholder, (json) => {
// The API returns data in a weird shape. Make it easier to work with.
async function tools(): Promise<ToolsData> {
const json = await get_json('tools');
// Make the data easier to work with.
let map: Map<string, Tool> = new Map();
for (const tool of json['tools']) {
const repo: string = tool.repository;
@ -139,7 +33,7 @@ export const tools = new API<ToolsData>('tools', tools_placeholder, (json) => {
});
}
let value = map.get(repo);
let value = map.get(repo)!!;
value.assets.push({
name: tool.name,
size: tool.size,
@ -151,14 +45,19 @@ export const tools = new API<ToolsData>('tools', tools_placeholder, (json) => {
}
return Object.fromEntries(map);
});
}
export const patches = new API<PatchesData>('patches', { patches: [], packages: [] }, (patches) => {
async function manager(): Promise<Tool> {
return await tools().then((data) => data['revanced/revanced-manager']);
}
async function patches(): Promise<PatchesData> {
const json = await get_json('patches');
const packagesWithCount: { [key: string]: number } = {};
// gets packages and patch count
for (let i = 0; i < patches.length; i++) {
patches[i].compatiblePackages.forEach((pkg: Patch) => {
for (let i = 0; i < json.length; i++) {
json[i].compatiblePackages.forEach((pkg: Patch) => {
packagesWithCount[pkg.name] = (packagesWithCount[pkg.name] || 0) + 1;
});
}
@ -168,5 +67,24 @@ export const patches = new API<PatchesData>('patches', { patches: [], packages:
.sort((a, b) => b[1] - a[1])
.map((pkg) => pkg[0]);
return { patches, packages };
});
return { patches: json, packages };
}
export const staleTime = 5 * 60 * 1000;
export const queries = {
manager: {
queryKey: ['manager'],
queryFn: manager,
staleTime
},
patches: {
queryKey: ['patches'],
queryFn: patches,
staleTime
},
repositories: {
queryKey: ['repositories'],
queryFn: repositories,
staleTime
}
};

View File

@ -1,6 +1,11 @@
<script lang="ts">
import { repositories } from '$data/api';
import { queries } from '$data/api';
import { friendlyName } from '$lib/utils';
import { createQuery } from '@tanstack/svelte-query';
import Query from '$lib/components/Query.svelte';
const query = createQuery(['repositories'], queries.repositories);
</script>
<svg aria-hidden="true" width="100%" height="8" fill="none" xmlns="http://www.w3.org/2000/svg">
@ -20,14 +25,15 @@
<img src="/logo.svg" class="logo-image" alt="ReVanced Logo" />
<div>
<p>
ReVanced was born out of Vanced's discontinuation and it is our goal to continue the legacy
of what Vanced left behind. Thanks to ReVanced Patcher, it's possible to create long-lasting
patches for nearly any Android app. ReVanced's patching system is designed to allow patches
to work on new versions of the apps automatically with bare minimum maintenance.
ReVanced was born out of Vanced's discontinuation and it is our goal to continue the
legacy of what Vanced left behind. Thanks to ReVanced Patcher, it's possible to create
long-lasting patches for nearly any Android app. ReVanced's patching system is designed to
allow patches to work on new versions of the apps automatically with bare minimum
maintenance.
</p>
</div>
</section>
<section class="links-container">
<div class="link-column">
<li>Pages</li>
@ -39,23 +45,31 @@
</div>
<div class="link-column">
<li>Repositories</li>
{#each $repositories as { name }}
<li>
<a href="https://github.com/{name}" target="_blank" rel="noreferrer">
{friendlyName(name)}
</a>
</li>
{/each}
<Query {query} let:data>
{#each data as { name }}
<li>
<a href="https://github.com/{name}" target="_blank" rel="noreferrer">
{friendlyName(name)}
</a>
</li>
{/each}
</Query>
</div>
<div class="link-column">
<!-- to replace -->
<li>Socials</li>
<li><a href="https://github.com/revanced" target="_blank" rel="noreferrer">GitHub</a></li>
<li><a href="https://revanced.app/discord" target="_blank" rel="noreferrer">Discord</a></li>
<li><a href="https://reddit.com/r/revancedapp" target="_blank" rel="noreferrer">Reddit</a></li>
<li>
<a href="https://reddit.com/r/revancedapp" target="_blank" rel="noreferrer">Reddit</a>
</li>
<li><a href="https://t.me/app_revanced" target="_blank" rel="noreferrer">Telegram</a></li>
<li><a href="https://twitter.com/revancedapp" target="_blank" rel="noreferrer">Twitter</a></li>
<li><a href="https://www.youtube.com/c/ReVanced" target="_blank" rel="noreferrer">YouTube</a></li>
<li>
<a href="https://twitter.com/revancedapp" target="_blank" rel="noreferrer">Twitter</a>
</li>
<li>
<a href="https://www.youtube.com/c/ReVanced" target="_blank" rel="noreferrer">YouTube</a>
</li>
</div>
</section>
</div>
@ -66,7 +80,6 @@
</div>
</footer>
<style>
footer {
margin: 4rem 0 5rem 0;
@ -95,7 +108,7 @@
}
#logo-name span {
color: var(--accent-color)
color: var(--accent-color);
}
.footer-bottom a {

View File

@ -1,9 +1,24 @@
<script lang="ts">
import { queries } from '$data/api';
import { dev_log } from '$lib/utils';
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 = null;
function prefetch() {
if (queryKey !== null) {
const query = queries[queryKey];
dev_log('Prefetching', query);
client.prefetchQuery(query as any);
}
}
</script>
<a data-sveltekit-preload-data {href}>
<a data-sveltekit-preload-data on:mouseenter={prefetch} {href}>
<!-- Check if href is equal to the first path -->
<li class:selected={href === '/' + $RouterEvents.target_url.pathname.split('/')[1]}>
<span><slot /></span>

View File

@ -9,10 +9,21 @@
import Modal from '$lib/components/Dialogue.svelte';
import Button from '$lib/components/Button.svelte';
import { clear_and_reload } from '$data/api/cache';
import * as settings from '$data/api/settings';
import RouterEvents from '$data/RouterEvents';
import { useQueryClient } from '@tanstack/svelte-query';
const client = useQueryClient();
function clear_and_reload() {
client.clear();
// `client.clear()` does technically do this for us, but it takes a while.
localStorage.clear();
location.reload();
}
let url = settings.api_base_url();
function save() {
@ -58,13 +69,13 @@
<div id="main-navigation">
<div class="nav-buttons">
<Navigation href="/">Home</Navigation>
<Navigation href="/download">Download</Navigation>
<Navigation href="/patches">Patches</Navigation>
<Navigation queryKey="manager" href="/download">Download</Navigation>
<Navigation queryKey="patches" href="/patches">Patches</Navigation>
<div hidden>
<!-- This is just temporary so the build doesn't fail -->
<Navigation href="/docs">Docs</Navigation>
</div>
<Navigation href="/contributors">Contributors</Navigation>
<Navigation queryKey="repositories" href="/contributors">Contributors</Navigation>
</div>
</div>
<div id="secondary-navigation">
@ -93,14 +104,16 @@
<Modal bind:modalOpen>
<svelte:fragment slot="icon">
<Svg viewBoxHeight={24} viewBoxWidth={24} svgHeight={24}>
<rect fill="none" height="24" width="24"/>
<rect fill="none" height="24" width="24" />
<path
d="M19.5,12c0-0.23-0.01-0.45-0.03-0.68l1.86-1.41c0.4-0.3,0.51-0.86,0.26-1.3l-1.87-3.23c-0.25-0.44-0.79-0.62-1.25-0.42 l-2.15,0.91c-0.37-0.26-0.76-0.49-1.17-0.68l-0.29-2.31C14.8,2.38,14.37,2,13.87,2h-3.73C9.63,2,9.2,2.38,9.14,2.88L8.85,5.19 c-0.41,0.19-0.8,0.42-1.17,0.68L5.53,4.96c-0.46-0.2-1-0.02-1.25,0.42L2.41,8.62c-0.25,0.44-0.14,0.99,0.26,1.3l1.86,1.41 C4.51,11.55,4.5,11.77,4.5,12s0.01,0.45,0.03,0.68l-1.86,1.41c-0.4,0.3-0.51,0.86-0.26,1.3l1.87,3.23c0.25,0.44,0.79,0.62,1.25,0.42 l2.15-0.91c0.37,0.26,0.76,0.49,1.17,0.68l0.29,2.31C9.2,21.62,9.63,22,10.13,22h3.73c0.5,0,0.93-0.38,0.99-0.88l0.29-2.31 c0.41-0.19,0.8-0.42,1.17-0.68l2.15,0.91c0.46,0.2,1,0.02,1.25-0.42l1.87-3.23c0.25-0.44,0.14-0.99-0.26-1.3l-1.86-1.41 C19.49,12.45,19.5,12.23,19.5,12z M12.04,15.5c-1.93,0-3.5-1.57-3.5-3.5s1.57-3.5,3.5-3.5s3.5,1.57,3.5,3.5S13.97,15.5,12.04,15.5z"
/>
</Svg>
</svelte:fragment>
<svelte:fragment slot="title">Settings</svelte:fragment>
<svelte:fragment slot="description">Configure the website's API. Defaults to ReVanced.</svelte:fragment>
<svelte:fragment slot="description"
>Configure the website's API. Defaults to ReVanced.</svelte:fragment
>
<div id="settings-content">
<div class="input-wrapper">
<input name="api-url" type="text" bind:value={url} />

View File

@ -0,0 +1,27 @@
<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,11 +1,44 @@
<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 { 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 NavHost from '$layout/Navbar/NavHost.svelte';
import Spinner from '$lib/components/Spinner.svelte';
import { staleTime } from '$data/api';
import RouterEvents from '$data/RouterEvents';
const queryClient = new QueryClient({
defaultOptions: {
queries: {
enabled: browser,
cacheTime: staleTime
}
}
});
onMount(() => {
isRestoring.set(true);
const [unsubscribe, promise] = persistQueryClient({
queryClient,
persister: createSyncStoragePersister({ storage: localStorage })
});
promise.then(() => isRestoring.set(false));
return unsubscribe;
});
// Just like the set/clearInterval example found here: https://svelte.dev/docs#run-time-svelte-store-derived
const show_loading_animation = derived(
RouterEvents,
@ -22,14 +55,14 @@
);
</script>
<NavHost />
<QueryClientProvider client={queryClient}>
<NavHost />
{#if $show_loading_animation}
<Spinner />
{:else}
<slot />
{/if}
<!--
afn if you are moving the footer here, please make it not use the repositories store directly and instead use component props :) -->
<!-- <Footer repos={$repositories}> -->
{#if $show_loading_animation}
<Spinner />
{:else}
<slot />
{/if}
<!-- guhh afn -->
<!-- <Footer> -->
</QueryClientProvider>

View File

@ -1,14 +1 @@
import type { PageLoad } from './$types';
import { repositories } from '$data/api';
export const prerender = true;
const base = repositories.page_load_impl();
export const load: PageLoad = async ({ fetch }) => {
// The entire site may softlock if the user sets a bad API url if we don't do this.
try {
return await base({ fetch });
} catch (err) {
console.log(err);
}
};

View File

@ -2,11 +2,15 @@
import { fly } from 'svelte/transition';
import { quintOut } from 'svelte/easing';
import { repositories } from '$data/api';
import ContributorHost from './ContributorSection.svelte';
import Footer from '$layout/Footer.svelte';
import Meta from '$lib/components/Meta.svelte';
import Query from '$lib/components/Query.svelte';
import { queries } from '$data/api';
import { createQuery } from '@tanstack/svelte-query';
const query = createQuery(['repositories'], queries.repositories);
</script>
<Meta title="Contributors" />
@ -24,11 +28,13 @@
</h4>
</div>
<div class="repos">
{#each $repositories as { contributors, name: repo }}
<div in:fly={{ y: 10, easing: quintOut, duration: 750 }}>
<ContributorHost {contributors} {repo} />
</div>
{/each}
<Query {query} let:data>
{#each data as { contributors, name: repo }}
<div in:fly={{ y: 10, easing: quintOut, duration: 750 }}>
<ContributorHost {contributors} {repo} />
</div>
{/each}
</Query>
</div>
</div>
</main>

View File

@ -1,4 +0,0 @@
import { repositories } from '$data/api';
import type { PageLoad } from './$types';
export const load: PageLoad = repositories.page_load_impl();

View File

@ -1,16 +1,19 @@
<script lang="ts">
import { tools } from '$data/api';
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_two.png?format=avif;webp;png&picture';
import Meta from '$lib/components/Meta.svelte';
import Query from '$lib/components/Query.svelte';
import Button from '$lib/components/Button.svelte';
import Footer from '$layout/Footer.svelte';
import Picture from '$lib/components/Picture.svelte';
$: manager = $tools['revanced/revanced-manager'];
const query = createQuery(['manager'], queries.manager);
</script>
<Meta title="Download" />
@ -19,9 +22,11 @@
<h2>ReVanced <span>Manager</span></h2>
<p>Patch your favourite apps, right on your device.</p>
<div class="buttons">
<Button kind="primary" icon="download" href={manager.assets[0].url} target="_blank">
{manager.version}
</Button>
<Query {query} let:data>
<Button kind="primary" icon="download" href={data.assets[0].url} target="_blank">
{data.version}
</Button>
</Query>
<Button href="https://github.com/revanced/revanced-manager" target="_blank">View Source</Button>
</div>
<div class="screenshot">

View File

@ -1,5 +0,0 @@
import type { PageLoad } from './$types';
import { tools } from '$data/api';
export const load: PageLoad = tools.page_load_impl();

View File

@ -4,7 +4,9 @@
import type { PageData } from './$types';
import type { Patch } from '$lib/types';
import { patches as api_patches } from '$data/api';
import { createQuery } from '@tanstack/svelte-query';
import { queries } from '$data/api';
import Meta from '$lib/components/Meta.svelte';
import PackageMenu from '../PackageMenu.svelte';
@ -14,8 +16,9 @@
import Search from '$lib/components/Search.svelte';
import FilterChip from '$lib/components/FilterChip.svelte';
import Dialogue from '$lib/components/Dialogue.svelte';
import Query from '$lib/components/Query.svelte';
$: ({ patches, packages } = $api_patches);
const query = createQuery(['patches'], queries.patches);
export let data: PageData;
$: ({ selectedPkg } = data);
@ -25,24 +28,44 @@
let timeout: any = null;
let mobilePackages = false;
function filterByPackage(pkg: string | undefined, packageList: any[]) {
return packageList.find((x: Package) => x.name === pkg);
function checkCompatibility(patch: Patch, pkg: string, cmp: (a: string, b: string) => boolean) {
if (pkg === '') {
return false;
}
return !!patch.compatiblePackages.find((compat) => cmp(compat.name, pkg));
}
function checkPkgName(pkg: string, packageList: any[]) {
// Basically the same function as before lol
return packageList.find((x: Package) => x.name.replace(/\./g, '').includes(pkg));
function searchString(str: string, term: string, replacement_regex: RegExp) {
return str
.toLowerCase()
.replace(replacement_regex, '')
.includes(term || '');
}
function search(patch: Patch) {
return (
patch.description.toLowerCase().replace(/\s/g, '').includes(searchTermFiltered) ||
patch.name.toLowerCase().replace(/-/g, '').includes(searchTermFiltered) ||
checkPkgName(searchTermFiltered, patch.compatiblePackages)
);
function filter(patches: Patch[], pkg: string, search?: string): Patch[] {
if (search === undefined && pkg === '') {
return patches;
}
return patches.filter((patch) => {
// Don't show if the patch doesn't support the selected package
if (pkg && !checkCompatibility(patch, pkg, (a, b) => a === b)) {
return false;
}
// Filter based on the search term.
if (search !== undefined) {
return (
searchString(patch.description, search, /\s/g) ||
searchString(patch.name, search, /-/g) ||
checkCompatibility(patch, pkg, (a, b) => searchString(a, b, /\./g))
);
}
return true;
});
}
// Make sure we don't have filter the patches after every key press
// Make sure we don't have to filter the patches after every key press
const debounce = () => {
clearTimeout(timeout);
timeout = setTimeout(() => {
@ -83,55 +106,51 @@
<FilterChip>Patch options</FilterChip> -->
</div>
<div class="mobile-packages-Dialogue">
<Dialogue bind:modalOpen={mobilePackages} fullscreen>
<svelte:fragment slot="title">Packages</svelte:fragment>
<div class="mobile-packages">
<span
on:click={() => (mobilePackages = !mobilePackages)}
on:keypress={() => (mobilePackages = !mobilePackages)}
>
<Package {selectedPkg} name="All packages" />
</span>
{#each packages as pkg}
<Query {query} let:data>
<div class="mobile-packages-Dialogue">
<Dialogue bind:modalOpen={mobilePackages} fullscreen>
<svelte:fragment slot="title">Packages</svelte:fragment>
<div class="mobile-packages">
<span
on:click={() => (mobilePackages = !mobilePackages)}
on:keypress={() => (mobilePackages = !mobilePackages)}
>
<Package {selectedPkg} name={pkg} />
<Package {selectedPkg} name="All packages" />
</span>
{/each}
</div>
</Dialogue>
</div>
{#each data.packages as pkg}
<span
on:click={() => (mobilePackages = !mobilePackages)}
on:keypress={() => (mobilePackages = !mobilePackages)}
>
<Package {selectedPkg} name={pkg} />
</span>
{/each}
</div>
</Dialogue>
</div>
<aside in:fly={{ y: 10, easing: quintOut, duration: 750 }}>
<PackageMenu>
<span class="packages">
<Package {selectedPkg} name="All packages" />
{#each packages as pkg}
<Package {selectedPkg} name={pkg} />
{/each}
</span>
</PackageMenu>
</aside>
<aside in:fly={{ y: 10, easing: quintOut, duration: 750 }}>
<PackageMenu>
<span class="packages">
<Package {selectedPkg} name="All packages" />
{#each data.packages as pkg}
<Package {selectedPkg} name={pkg} />
{/each}
</span>
</PackageMenu>
</aside>
<div class="patches-container">
{#each patches as patch}
<!-- Trigger new animations when package or search changes (I love Svelte) -->
{#key selectedPkg || searchTermFiltered}
<!-- Show patch if it supports the selected package, or if no package has been selected -->
{#if filterByPackage(selectedPkg, patch.compatiblePackages) || !selectedPkg}
<!-- ...same with search -->
{#if search(patch) || !searchTermFiltered}
<div in:fly={{ y: 10, easing: quintOut, duration: 750 }}>
<PatchItem {patch} />
</div>
{/if}
{/if}
{/key}
{/each}
</div>
<div class="patches-container">
{#each filter(data.patches, selectedPkg || '', searchTermFiltered) as patch}
<!-- Trigger new animations when package or search changes (I love Svelte) -->
{#key selectedPkg || searchTermFiltered}
<div in:fly={{ y: 10, easing: quintOut, duration: 750 }}>
<PatchItem {patch} />
</div>
{/key}
{/each}
</div>
</Query>
</main>
<Footer />

View File

@ -1,10 +1,6 @@
import { patches } from '$data/api';
import type { PageLoad } from './$types';
const api = patches.page_load_impl();
export const load: PageLoad = async ({ params, fetch }) => {
export const load: PageLoad = async ({ params }) => {
const selectedPkg = params.package || undefined;
await api({ fetch });
return { selectedPkg };
};

View File

@ -10,11 +10,13 @@
"sourceMap": true,
"strict": true,
"types": ["node"],
"lib": ["ESNext", "DOM"],
"paths": {
"$data/*": ["./src/data/*"],
"$lib": ["src/lib"],
"$lib/*": ["./src/lib/*"],
"$layout/*": ["./src/layout/*"],
"$images/*": ["./images/*"],
},
"$images/*": ["./images/*"]
}
}
}