feat: download & like count + UI tweaks and cleanup

This commit is contained in:
2026-02-20 18:29:11 +10:30
parent 889175f3de
commit d21ce90a5c
10 changed files with 395 additions and 106 deletions
@@ -0,0 +1,77 @@
<script lang="ts">
import { fade } from "svelte/transition";
import { animate } from "motion";
import { closeExtensionPopup } from "@/seqta/utils/Closers/closeExtensionPopup";
let { onClose } = $props<{ onClose: () => void }>();
let modalElement: HTMLElement;
$effect(() => {
if (modalElement) {
animate(modalElement, { scale: [0.9, 1], opacity: [0, 1] }, { type: "spring", stiffness: 300, damping: 25 });
}
});
function handleSignIn() {
onClose();
if (document.getElementById("ExtensionPopup")) {
closeExtensionPopup();
} else {
window.close();
}
}
</script>
<div
class="flex fixed inset-0 z-[10000] justify-center items-center bg-black/50"
onclick={(e) => {
if (e.target === e.currentTarget) onClose();
}}
onkeydown={(e) => {
if (e.key === "Escape") onClose();
}}
role="button"
tabindex="-1"
transition:fade={{ duration: 150 }}
>
<!-- svelte-ignore a11y_no_static_element_interactions -->
<div
bind:this={modalElement}
class="p-4 mx-4 w-full max-w-md bg-white rounded-2xl shadow-2xl dark:bg-zinc-800 dark:text-white"
onclick={(e) => e.stopPropagation()}
onkeydown={(e) => e.stopPropagation()}
>
<h2 class="mb-3 text-xl font-bold text-zinc-900 dark:text-white">
Sign in to favorite themes
</h2>
<p class="mb-6 text-zinc-600 dark:text-zinc-400">
Go to Settings → BetterSEQTA Cloud to sign in, or create an account to get started.
</p>
<div class="flex flex-wrap gap-2 justify-end">
<button
type="button"
onclick={onClose}
class="px-4 py-2 text-sm font-medium rounded-lg bg-zinc-200 dark:bg-zinc-700 text-zinc-700 dark:text-zinc-200 hover:bg-zinc-300 dark:hover:bg-zinc-600 transition-colors duration-200"
>
OK
</button>
<a
href="https://accounts.betterseqta.org/register"
target="_blank"
rel="noopener noreferrer"
class="inline-flex items-center gap-2 px-4 py-2 text-sm font-medium rounded-lg border border-zinc-200 dark:border-zinc-600 text-zinc-700 dark:text-zinc-300 hover:bg-zinc-100 dark:hover:bg-zinc-800 transition-all duration-200"
>
Create account
</a>
<button
type="button"
onclick={handleSignIn}
class="px-4 py-2 text-sm font-medium rounded-lg bg-zinc-800 dark:bg-zinc-200 text-white dark:text-zinc-900 hover:bg-zinc-700 dark:hover:bg-zinc-300 transition-colors duration-200"
>
Sign in
</button>
</div>
</div>
</div>
@@ -3,8 +3,7 @@
import './TabbedContainer.css';
import { onMount } from 'svelte';
let { tabs } = $props<{ tabs: { title: string, Content: any, props?: any }[] }>();
let activeTab = $state(0);
let { tabs, activeTab = $bindable(0) } = $props<{ tabs: { title: string, Content: any, props?: any }[]; activeTab?: number }>();
let containerRef: HTMLElement | null = null;
let tabWidth = $state(0);
@@ -2,6 +2,7 @@
import type { Theme } from '@/interface/types/Theme'
import { fade } from 'svelte/transition';
import { onMount } from 'svelte';
import SignInToFavoriteModal from '@/interface/components/SignInToFavoriteModal.svelte';
let { theme, onClick, toggleFavorite, isLoggedIn } = $props<{
theme: Theme;
@@ -10,6 +11,7 @@
isLoggedIn: boolean;
}>();
let menuOpen = $state(false);
let showSignInModal = $state(false);
let menuRef: HTMLDivElement;
onMount(() => {
@@ -29,7 +31,11 @@
function handleFavoriteClick(e: MouseEvent) {
e.stopPropagation();
if (isLoggedIn) toggleFavorite(theme);
if (isLoggedIn) {
toggleFavorite(theme);
} else {
showSignInModal = true;
}
menuOpen = false;
}
</script>
@@ -55,7 +61,7 @@
>
<button
type="button"
class="flex gap-2 items-center w-full px-3 py-2 text-left text-sm hover:bg-zinc-100 dark:hover:bg-zinc-700 {!isLoggedIn ? 'opacity-50 cursor-not-allowed' : ''}"
class="flex gap-2 items-center w-full px-3 py-2 text-left text-sm hover:bg-zinc-100 dark:hover:bg-zinc-700"
role="menuitem"
onclick={handleFavoriteClick}
title={isLoggedIn ? (theme.is_favorited ? 'Remove from favorites' : 'Add to favorites') : 'Sign in to favorite themes'}
@@ -75,8 +81,22 @@
</div>
{/if}
</div>
<div class="absolute bottom-1 left-3 z-10 mb-1 text-xl font-bold text-white">
{theme.name}
<div class="absolute bottom-1 left-3 right-3 z-10 mb-1 flex flex-col gap-0.5">
<span class="text-xl font-bold text-white drop-shadow-md">{theme.name}</span>
<div class="flex gap-3 text-xs font-medium text-white/90 drop-shadow-sm">
<span class="flex items-center gap-1">
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="w-3.5 h-3.5">
<path stroke-linecap="round" stroke-linejoin="round" d="M3 16.5v2.25A2.25 2.25 0 005.25 21h13.5A2.25 2.25 0 0021 18.75V16.5M16.5 12L12 16.5m0 0L7.5 12m4.5 4.5V3" />
</svg>
{(theme.download_count ?? 0).toLocaleString()}
</span>
<span class="flex items-center gap-1">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill={theme.is_favorited ? 'currentColor' : 'none'} stroke="currentColor" stroke-width="1.5" class="w-3.5 h-3.5">
<path stroke-linecap="round" stroke-linejoin="round" d="M4.318 6.318a4.5 4.5 0 000 6.364L12 20.364l7.682-7.682a4.5 4.5 0 00-6.364-6.364L12 7.636l-1.318-1.318a4.5 4.5 0 00-6.364 0z" />
</svg>
{(theme.favorite_count ?? 0).toLocaleString()}
</span>
</div>
</div>
<div class='absolute bottom-0 z-0 w-full h-3/4 bg-linear-to-t to-transparent from-black/80'></div>
<div class='w-full'>
@@ -84,3 +104,7 @@
</div>
</div>
</div>
{#if showSignInModal}
<SignInToFavoriteModal onClose={() => (showSignInModal = false)} />
{/if}
@@ -2,6 +2,7 @@
import type { Theme } from '@/interface/types/Theme'
import { fade } from 'svelte/transition';
import { animate } from 'motion';
import SignInToFavoriteModal from '@/interface/components/SignInToFavoriteModal.svelte';
let { theme, currentThemes, setDisplayTheme, onInstall, onRemove, allThemes, displayTheme, toggleFavorite, isLoggedIn } = $props<{
theme: Theme | null;
@@ -15,8 +16,17 @@
isLoggedIn?: boolean;
}>();
let installing = $state(false);
let showSignInModal = $state(false);
let modalElement: HTMLElement;
function handleFavoriteClick() {
if (isLoggedIn && toggleFavorite && theme) {
toggleFavorite(theme);
} else {
showSignInModal = true;
}
}
// Function to get related themes
function getRelatedThemes() {
return allThemes
@@ -76,56 +86,66 @@
>
<div class="relative h-auto">
<div class="absolute top-0 right-0 flex gap-1 items-center">
{#if isLoggedIn && toggleFavorite && theme}
<button
type="button"
class="p-2 rounded-lg transition-all hover:bg-zinc-100 dark:hover:bg-zinc-700 {theme.is_favorited ? 'text-red-500' : 'text-gray-600 dark:text-gray-200'}"
onclick={() => toggleFavorite(theme)}
title={theme.is_favorited ? 'Remove from favorites' : 'Add to favorites'}
aria-label={theme.is_favorited ? 'Unfavorite' : 'Favorite'}
>
<svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 24 24"
fill={theme.is_favorited ? 'currentColor' : 'none'}
stroke="currentColor"
stroke-width="2"
class="w-6 h-6"
>
<path stroke-linecap="round" stroke-linejoin="round" d="M4.318 6.318a4.5 4.5 0 000 6.364L12 20.364l7.682-7.682a4.5 4.5 0 00-6.364-6.364L12 7.636l-1.318-1.318a4.5 4.5 0 00-6.364 0z" />
</svg>
</button>
{/if}
<button class="p-2 text-xl font-bold text-gray-600 font-IconFamily dark:text-gray-200" onclick={() => hideModal()}>
{'\ued8a'}
</button>
</div>
<h2 class="mb-4 text-2xl font-bold">
<h2 class="mb-2 text-2xl font-bold">
{theme.name}
</h2>
<div class="flex gap-4 mb-4 text-sm text-zinc-600 dark:text-zinc-400">
<span class="flex items-center gap-1.5">
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="w-4 h-4">
<path stroke-linecap="round" stroke-linejoin="round" d="M3 16.5v2.25A2.25 2.25 0 005.25 21h13.5A2.25 2.25 0 0021 18.75V16.5M16.5 12L12 16.5m0 0L7.5 12m4.5 4.5V3" />
</svg>
{(theme.download_count ?? 0).toLocaleString()} downloads
</span>
<span class="flex items-center gap-1.5">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill={theme.is_favorited ? 'currentColor' : 'none'} stroke="currentColor" stroke-width="1.5" class="w-4 h-4">
<path stroke-linecap="round" stroke-linejoin="round" d="M4.318 6.318a4.5 4.5 0 000 6.364L12 20.364l7.682-7.682a4.5 4.5 0 00-6.364-6.364L12 7.636l-1.318-1.318a4.5 4.5 0 00-6.364 0z" />
</svg>
{(theme.favorite_count ?? 0).toLocaleString()} favorites
</span>
</div>
<img src={theme.marqueeImage || theme.coverImage} alt="Theme Cover" class="object-cover mb-4 w-full rounded-md" />
<p class="mb-4 text-gray-700 dark:text-gray-300">
{theme.description}
</p>
{#if currentThemes.includes(theme.id)}
<button onclick={async () => {installing = true; await onRemove(theme.id); installing = false}} class="flex relative justify-center items-center px-4 py-2 mt-4 ml-auto w-32 text-black rounded-full dark:text-white bg-zinc-300 dark:bg-zinc-700 dark:hover:bg-zinc-600/50 hover:bg-zinc-200">
{#if installing}
<svg class="absolute w-4 h-4 { installing ? 'opacity-100' : 'opacity-0' }" width="24" height="24" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
<path stroke="currentColor" fill="currentColor" class="origin-center animate-spin-fast" d="M2,12A11.2,11.2,0,0,1,13,1.05C12.67,1,12.34,1,12,1a11,11,0,0,0,0,22c.34,0,.67,0,1-.05C6,23,2,17.74,2,12Z"/>
<div class="flex flex-wrap gap-2 mt-4 justify-end items-center">
{#if toggleFavorite && theme}
<button
type="button"
class="flex items-center gap-2 px-4 py-2 rounded-full transition-all duration-200 hover:scale-105 active:scale-95 {theme.is_favorited ? 'text-red-500 bg-red-500/10 dark:bg-red-500/20' : 'bg-zinc-200 dark:bg-zinc-700 dark:text-white hover:bg-zinc-300 dark:hover:bg-zinc-600'}"
onclick={handleFavoriteClick}
title={isLoggedIn ? (theme.is_favorited ? 'Remove from favorites' : 'Add to favorites') : 'Sign in to favorite themes'}
aria-label={theme.is_favorited ? 'Unfavorite' : 'Favorite'}
>
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill={theme.is_favorited ? 'currentColor' : 'none'} stroke="currentColor" stroke-width="2" class="w-5 h-5">
<path stroke-linecap="round" stroke-linejoin="round" d="M4.318 6.318a4.5 4.5 0 000 6.364L12 20.364l7.682-7.682a4.5 4.5 0 00-6.364-6.364L12 7.636l-1.318-1.318a4.5 4.5 0 00-6.364 0z" />
</svg>
{/if}
<span class="{ installing ? 'opacity-0' : 'opacity-100' }">Remove</span>
</button>
{:else}
<button onclick={async () => {installing = true; await onInstall(theme.id); installing = false}} class="flex relative justify-center items-center px-4 py-2 mt-4 ml-auto w-32 text-black rounded-full dark:text-white bg-zinc-300 dark:bg-zinc-700 dark:hover:bg-zinc-600/50 hover:bg-zinc-200">
{#if installing}
<svg class="absolute w-4 h-4 { installing ? 'opacity-100' : 'opacity-0' }" width="24" height="24" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
<path stroke="currentColor" fill="currentColor" class="origin-center animate-spin-fast" d="M2,12A11.2,11.2,0,0,1,13,1.05C12.67,1,12.34,1,12,1a11,11,0,0,0,0,22c.34,0,.67,0,1-.05C6,23,2,17.74,2,12Z"/>
</svg>
{/if}
<span class="{ installing ? 'opacity-0' : 'opacity-100' }">Install</span>
</button>
{/if}
{theme.is_favorited ? 'Favorited' : 'Favorite'}
</button>
{/if}
{#if currentThemes.includes(theme.id)}
<button onclick={async () => {installing = true; await onRemove(theme.id); installing = false}} class="flex relative justify-center items-center px-4 py-2 w-32 text-black rounded-full dark:text-white bg-zinc-300 dark:bg-zinc-700 dark:hover:bg-zinc-600/50 hover:bg-zinc-200 transition-all duration-200 hover:scale-105 active:scale-95">
{#if installing}
<svg class="absolute w-4 h-4 { installing ? 'opacity-100' : 'opacity-0' }" width="24" height="24" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
<path stroke="currentColor" fill="currentColor" class="origin-center animate-spin-fast" d="M2,12A11.2,11.2,0,0,1,13,1.05C12.67,1,12.34,1,12,1a11,11,0,0,0,0,22c.34,0,.67,0,1-.05C6,23,2,17.74,2,12Z"/>
</svg>
{/if}
<span class="{ installing ? 'opacity-0' : 'opacity-100' }">Remove</span>
</button>
{:else}
<button onclick={async () => {installing = true; await onInstall(theme.id); installing = false}} class="flex relative justify-center items-center px-4 py-2 w-32 text-black rounded-full dark:text-white bg-zinc-300 dark:bg-zinc-700 dark:hover:bg-zinc-600/50 hover:bg-zinc-200 transition-all duration-200 hover:scale-105 active:scale-95">
{#if installing}
<svg class="absolute w-4 h-4 { installing ? 'opacity-100' : 'opacity-0' }" width="24" height="24" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
<path stroke="currentColor" fill="currentColor" class="origin-center animate-spin-fast" d="M2,12A11.2,11.2,0,0,1,13,1.05C12.67,1,12.34,1,12,1a11,11,0,0,0,0,22c.34,0,.67,0,1-.05C6,23,2,17.74,2,12Z"/>
</svg>
{/if}
<span class="{ installing ? 'opacity-0' : 'opacity-100' }">Install</span>
</button>
{/if}
</div>
<div class="my-8 border-b border-zinc-200 dark:border-zinc-700"></div>
@@ -148,3 +168,7 @@
</div>
</div>
</div>
{#if showSignInModal}
<SignInToFavoriteModal onClose={() => (showSignInModal = false)} />
{/if}
@@ -1,11 +1,14 @@
<script lang="ts">
import type { CustomTheme, ThemeList } from '@/types/CustomThemes'
import { onDestroy, onMount } from 'svelte'
import browser from 'webextension-polyfill'
import { OpenThemeCreator } from '@/plugins/built-in/themes/ThemeCreator'
import { OpenStorePage } from '@/seqta/ui/renderStore'
import { themeUpdates } from '@/interface/hooks/ThemeUpdates'
import { closeExtensionPopup } from '@/seqta/utils/Closers/closeExtensionPopup'
import { ThemeManager } from '@/plugins/built-in/themes/theme-manager'
import { cloudAuth } from '@/seqta/utils/CloudAuth'
import SignInToFavoriteModal from '@/interface/components/SignInToFavoriteModal.svelte'
const themeManager = ThemeManager.getInstance();
@@ -13,6 +16,17 @@
let { isEditMode } = $props<{ isEditMode: boolean }>();
let isDragging = $state(false);
let tempTheme = $state(null);
let favoriteStatus = $state<Record<string, boolean>>({});
let cloudLoggedIn = $state(cloudAuth.state.isLoggedIn);
let prevLoggedIn = $state(false);
let showSignInModal = $state(false);
cloudAuth.subscribe((s) => {
const now = s.isLoggedIn;
if (now && !prevLoggedIn && themes) void fetchThemes();
prevLoggedIn = now;
cloudLoggedIn = now;
});
const handleThemeClick = async (theme: CustomTheme, e: MouseEvent) => {
if (isEditMode) return;
@@ -87,11 +101,55 @@
themes: await themeManager.getAvailableThemes(),
selectedTheme: themeManager.getSelectedThemeId() || '',
}
if (themes && cloudLoggedIn) {
const token = await cloudAuth.getStoredToken();
if (token) {
const status: Record<string, boolean> = {};
await Promise.all(
themes.themes.map(async (t) => {
try {
const res = (await browser.runtime.sendMessage({
type: 'fetchThemeDetails',
themeId: t.id,
token,
})) as { success?: boolean; data?: { theme?: { is_favorited?: boolean } } };
if (res?.success && res?.data?.theme) {
status[t.id] = !!res.data.theme.is_favorited;
}
} catch {
// Theme may not exist on store (e.g. locally created)
}
})
);
favoriteStatus = status;
}
} else {
favoriteStatus = {};
}
}
const handleToggleFavorite = async (theme: CustomTheme, e: MouseEvent) => {
e.stopPropagation();
if (!cloudLoggedIn) {
showSignInModal = true;
return;
}
const token = await cloudAuth.getStoredToken();
if (!token) return;
const isFavorite = !favoriteStatus[theme.id];
const result = (await browser.runtime.sendMessage({
type: 'cloudFavorite',
themeId: theme.id,
token,
action: isFavorite ? 'favorite' : 'unfavorite',
})) as { success?: boolean };
if (result?.success) {
favoriteStatus = { ...favoriteStatus, [theme.id]: isFavorite };
}
}
onMount(async () => {
await fetchThemes();
themeUpdates.addListener(fetchThemes);
})
@@ -144,6 +202,18 @@
{/if}
{#if !isEditMode}
<div
class="flex absolute right-24 top-1/4 z-20 place-items-center p-2 w-8 h-8 text-center rounded-full opacity-0 transition-all -translate-y-1/2 group-hover:opacity-100 group-hover:top-1/2 {(favoriteStatus[theme.id] ?? false) ? 'text-red-400' : 'text-white/80'} bg-black/50"
onclick={(event) => handleToggleFavorite(theme, event)}
onkeydown={(event) => { if (event.key === 'Enter' || event.key === ' ') handleToggleFavorite(theme, event as any) }}
role="button"
tabindex="-1"
title={cloudLoggedIn ? ((favoriteStatus[theme.id] ?? false) ? 'Remove from favorites' : 'Add to favorites') : 'Sign in to favorite themes'}
>
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill={(favoriteStatus[theme.id] ?? false) ? 'currentColor' : 'none'} stroke="currentColor" stroke-width="2" class="w-5 h-5">
<path stroke-linecap="round" stroke-linejoin="round" d="M4.318 6.318a4.5 4.5 0 000 6.364L12 20.364l7.682-7.682a4.5 4.5 0 00-6.364-6.364L12 7.636l-1.318-1.318a4.5 4.5 0 00-6.364 0z" />
</svg>
</div>
<div
class="absolute z-20 flex w-8 h-8 p-2 text-white transition-all rounded-full delay-[20ms] opacity-0 top-1/4 right-2 bg-black/50 place-items-center group-hover:opacity-100 group-hover:top-1/2 -translate-y-1/2"
onclick={(event) => { event.stopPropagation(); OpenThemeCreator(theme.id); closeExtensionPopup() }}
@@ -211,3 +281,7 @@
</button>
</div>
</div>
{#if showSignInModal}
<SignInToFavoriteModal onClose={() => (showSignInModal = false)} />
{/if}