mirror of
https://github.com/BetterSEQTA/BetterSEQTA-Plus.git
synced 2026-06-05 19:24:39 +00:00
feat: download & like count + UI tweaks and cleanup
This commit is contained in:
@@ -78,6 +78,27 @@ browser.runtime.onMessage.addListener(
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
case "fetchThemeDetails": {
|
||||||
|
const { themeId, token } = request;
|
||||||
|
if (!themeId || typeof themeId !== "string") {
|
||||||
|
sendResponse({ success: false, error: "Missing themeId" });
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
const headers: Record<string, string> = {};
|
||||||
|
if (token) headers["Authorization"] = `Bearer ${token}`;
|
||||||
|
fetch(`https://betterseqta.org/api/themes/${themeId}`, {
|
||||||
|
cache: "no-store",
|
||||||
|
headers,
|
||||||
|
})
|
||||||
|
.then((r) => r.json())
|
||||||
|
.then(sendResponse)
|
||||||
|
.catch((err) => {
|
||||||
|
console.error("[Background] fetchThemeDetails error:", err);
|
||||||
|
sendResponse({ success: false, error: err?.message });
|
||||||
|
});
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
case "fetchFromUrl": {
|
case "fetchFromUrl": {
|
||||||
const { url } = request;
|
const { url } = request;
|
||||||
if (!url || typeof url !== "string") {
|
if (!url || typeof url !== "string") {
|
||||||
|
|||||||
@@ -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 './TabbedContainer.css';
|
||||||
import { onMount } from 'svelte';
|
import { onMount } from 'svelte';
|
||||||
|
|
||||||
let { tabs } = $props<{ tabs: { title: string, Content: any, props?: any }[] }>();
|
let { tabs, activeTab = $bindable(0) } = $props<{ tabs: { title: string, Content: any, props?: any }[]; activeTab?: number }>();
|
||||||
let activeTab = $state(0);
|
|
||||||
let containerRef: HTMLElement | null = null;
|
let containerRef: HTMLElement | null = null;
|
||||||
let tabWidth = $state(0);
|
let tabWidth = $state(0);
|
||||||
|
|
||||||
|
|||||||
@@ -2,6 +2,7 @@
|
|||||||
import type { Theme } from '@/interface/types/Theme'
|
import type { Theme } from '@/interface/types/Theme'
|
||||||
import { fade } from 'svelte/transition';
|
import { fade } from 'svelte/transition';
|
||||||
import { onMount } from 'svelte';
|
import { onMount } from 'svelte';
|
||||||
|
import SignInToFavoriteModal from '@/interface/components/SignInToFavoriteModal.svelte';
|
||||||
|
|
||||||
let { theme, onClick, toggleFavorite, isLoggedIn } = $props<{
|
let { theme, onClick, toggleFavorite, isLoggedIn } = $props<{
|
||||||
theme: Theme;
|
theme: Theme;
|
||||||
@@ -10,6 +11,7 @@
|
|||||||
isLoggedIn: boolean;
|
isLoggedIn: boolean;
|
||||||
}>();
|
}>();
|
||||||
let menuOpen = $state(false);
|
let menuOpen = $state(false);
|
||||||
|
let showSignInModal = $state(false);
|
||||||
let menuRef: HTMLDivElement;
|
let menuRef: HTMLDivElement;
|
||||||
|
|
||||||
onMount(() => {
|
onMount(() => {
|
||||||
@@ -29,7 +31,11 @@
|
|||||||
|
|
||||||
function handleFavoriteClick(e: MouseEvent) {
|
function handleFavoriteClick(e: MouseEvent) {
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
if (isLoggedIn) toggleFavorite(theme);
|
if (isLoggedIn) {
|
||||||
|
toggleFavorite(theme);
|
||||||
|
} else {
|
||||||
|
showSignInModal = true;
|
||||||
|
}
|
||||||
menuOpen = false;
|
menuOpen = false;
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
@@ -55,7 +61,7 @@
|
|||||||
>
|
>
|
||||||
<button
|
<button
|
||||||
type="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"
|
role="menuitem"
|
||||||
onclick={handleFavoriteClick}
|
onclick={handleFavoriteClick}
|
||||||
title={isLoggedIn ? (theme.is_favorited ? 'Remove from favorites' : 'Add to favorites') : 'Sign in to favorite themes'}
|
title={isLoggedIn ? (theme.is_favorited ? 'Remove from favorites' : 'Add to favorites') : 'Sign in to favorite themes'}
|
||||||
@@ -75,8 +81,22 @@
|
|||||||
</div>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
<div class="absolute bottom-1 left-3 z-10 mb-1 text-xl font-bold text-white">
|
<div class="absolute bottom-1 left-3 right-3 z-10 mb-1 flex flex-col gap-0.5">
|
||||||
{theme.name}
|
<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>
|
||||||
<div class='absolute bottom-0 z-0 w-full h-3/4 bg-linear-to-t to-transparent from-black/80'></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'>
|
<div class='w-full'>
|
||||||
@@ -84,3 +104,7 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{#if showSignInModal}
|
||||||
|
<SignInToFavoriteModal onClose={() => (showSignInModal = false)} />
|
||||||
|
{/if}
|
||||||
|
|||||||
@@ -2,6 +2,7 @@
|
|||||||
import type { Theme } from '@/interface/types/Theme'
|
import type { Theme } from '@/interface/types/Theme'
|
||||||
import { fade } from 'svelte/transition';
|
import { fade } from 'svelte/transition';
|
||||||
import { animate } from 'motion';
|
import { animate } from 'motion';
|
||||||
|
import SignInToFavoriteModal from '@/interface/components/SignInToFavoriteModal.svelte';
|
||||||
|
|
||||||
let { theme, currentThemes, setDisplayTheme, onInstall, onRemove, allThemes, displayTheme, toggleFavorite, isLoggedIn } = $props<{
|
let { theme, currentThemes, setDisplayTheme, onInstall, onRemove, allThemes, displayTheme, toggleFavorite, isLoggedIn } = $props<{
|
||||||
theme: Theme | null;
|
theme: Theme | null;
|
||||||
@@ -15,8 +16,17 @@
|
|||||||
isLoggedIn?: boolean;
|
isLoggedIn?: boolean;
|
||||||
}>();
|
}>();
|
||||||
let installing = $state(false);
|
let installing = $state(false);
|
||||||
|
let showSignInModal = $state(false);
|
||||||
let modalElement: HTMLElement;
|
let modalElement: HTMLElement;
|
||||||
|
|
||||||
|
function handleFavoriteClick() {
|
||||||
|
if (isLoggedIn && toggleFavorite && theme) {
|
||||||
|
toggleFavorite(theme);
|
||||||
|
} else {
|
||||||
|
showSignInModal = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Function to get related themes
|
// Function to get related themes
|
||||||
function getRelatedThemes() {
|
function getRelatedThemes() {
|
||||||
return allThemes
|
return allThemes
|
||||||
@@ -76,39 +86,48 @@
|
|||||||
>
|
>
|
||||||
<div class="relative h-auto">
|
<div class="relative h-auto">
|
||||||
<div class="absolute top-0 right-0 flex gap-1 items-center">
|
<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()}>
|
<button class="p-2 text-xl font-bold text-gray-600 font-IconFamily dark:text-gray-200" onclick={() => hideModal()}>
|
||||||
{'\ued8a'}
|
{'\ued8a'}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
<h2 class="mb-4 text-2xl font-bold">
|
<h2 class="mb-2 text-2xl font-bold">
|
||||||
{theme.name}
|
{theme.name}
|
||||||
</h2>
|
</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" />
|
<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">
|
<p class="mb-4 text-gray-700 dark:text-gray-300">
|
||||||
{theme.description}
|
{theme.description}
|
||||||
</p>
|
</p>
|
||||||
|
<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>
|
||||||
|
{theme.is_favorited ? 'Favorited' : 'Favorite'}
|
||||||
|
</button>
|
||||||
|
{/if}
|
||||||
{#if currentThemes.includes(theme.id)}
|
{#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">
|
<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}
|
{#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">
|
<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"/>
|
<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"/>
|
||||||
@@ -117,7 +136,7 @@
|
|||||||
<span class="{ installing ? 'opacity-0' : 'opacity-100' }">Remove</span>
|
<span class="{ installing ? 'opacity-0' : 'opacity-100' }">Remove</span>
|
||||||
</button>
|
</button>
|
||||||
{:else}
|
{: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">
|
<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}
|
{#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">
|
<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"/>
|
<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"/>
|
||||||
@@ -126,6 +145,7 @@
|
|||||||
<span class="{ installing ? 'opacity-0' : 'opacity-100' }">Install</span>
|
<span class="{ installing ? 'opacity-0' : 'opacity-100' }">Install</span>
|
||||||
</button>
|
</button>
|
||||||
{/if}
|
{/if}
|
||||||
|
</div>
|
||||||
|
|
||||||
<div class="my-8 border-b border-zinc-200 dark:border-zinc-700"></div>
|
<div class="my-8 border-b border-zinc-200 dark:border-zinc-700"></div>
|
||||||
|
|
||||||
@@ -148,3 +168,7 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{#if showSignInModal}
|
||||||
|
<SignInToFavoriteModal onClose={() => (showSignInModal = false)} />
|
||||||
|
{/if}
|
||||||
|
|||||||
@@ -1,11 +1,14 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import type { CustomTheme, ThemeList } from '@/types/CustomThemes'
|
import type { CustomTheme, ThemeList } from '@/types/CustomThemes'
|
||||||
import { onDestroy, onMount } from 'svelte'
|
import { onDestroy, onMount } from 'svelte'
|
||||||
|
import browser from 'webextension-polyfill'
|
||||||
import { OpenThemeCreator } from '@/plugins/built-in/themes/ThemeCreator'
|
import { OpenThemeCreator } from '@/plugins/built-in/themes/ThemeCreator'
|
||||||
import { OpenStorePage } from '@/seqta/ui/renderStore'
|
import { OpenStorePage } from '@/seqta/ui/renderStore'
|
||||||
import { themeUpdates } from '@/interface/hooks/ThemeUpdates'
|
import { themeUpdates } from '@/interface/hooks/ThemeUpdates'
|
||||||
import { closeExtensionPopup } from '@/seqta/utils/Closers/closeExtensionPopup'
|
import { closeExtensionPopup } from '@/seqta/utils/Closers/closeExtensionPopup'
|
||||||
import { ThemeManager } from '@/plugins/built-in/themes/theme-manager'
|
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();
|
const themeManager = ThemeManager.getInstance();
|
||||||
|
|
||||||
@@ -13,6 +16,17 @@
|
|||||||
let { isEditMode } = $props<{ isEditMode: boolean }>();
|
let { isEditMode } = $props<{ isEditMode: boolean }>();
|
||||||
let isDragging = $state(false);
|
let isDragging = $state(false);
|
||||||
let tempTheme = $state(null);
|
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) => {
|
const handleThemeClick = async (theme: CustomTheme, e: MouseEvent) => {
|
||||||
if (isEditMode) return;
|
if (isEditMode) return;
|
||||||
@@ -87,11 +101,55 @@
|
|||||||
themes: await themeManager.getAvailableThemes(),
|
themes: await themeManager.getAvailableThemes(),
|
||||||
selectedTheme: themeManager.getSelectedThemeId() || '',
|
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 () => {
|
onMount(async () => {
|
||||||
await fetchThemes();
|
await fetchThemes();
|
||||||
|
|
||||||
themeUpdates.addListener(fetchThemes);
|
themeUpdates.addListener(fetchThemes);
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -144,6 +202,18 @@
|
|||||||
{/if}
|
{/if}
|
||||||
|
|
||||||
{#if !isEditMode}
|
{#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
|
<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"
|
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() }}
|
onclick={(event) => { event.stopPropagation(); OpenThemeCreator(theme.id); closeExtensionPopup() }}
|
||||||
@@ -211,3 +281,7 @@
|
|||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{#if showSignInModal}
|
||||||
|
<SignInToFavoriteModal onClose={() => (showSignInModal = false)} />
|
||||||
|
{/if}
|
||||||
|
|||||||
@@ -20,6 +20,7 @@
|
|||||||
import { settingsPopup } from "../hooks/SettingsPopup";
|
import { settingsPopup } from "../hooks/SettingsPopup";
|
||||||
|
|
||||||
let devModeSequence = "";
|
let devModeSequence = "";
|
||||||
|
let settingsActiveTab = $state(0);
|
||||||
let showDisclaimerModal = $state(false);
|
let showDisclaimerModal = $state(false);
|
||||||
let disclaimerCallbacks = $state<{ onConfirm: () => void, onCancel: () => void } | null>(null);
|
let disclaimerCallbacks = $state<{ onConfirm: () => void, onCancel: () => void } | null>(null);
|
||||||
|
|
||||||
@@ -72,13 +73,18 @@
|
|||||||
showDisclaimerModal = true;
|
showDisclaimerModal = true;
|
||||||
};
|
};
|
||||||
|
|
||||||
onMount(async () => {
|
onMount(() => {
|
||||||
settingsPopup.addListener(() => {
|
settingsPopup.addListener(() => {
|
||||||
showColourPicker = false;
|
showColourPicker = false;
|
||||||
});
|
});
|
||||||
|
|
||||||
if (!standalone) return;
|
if (window.location.hash === "#cloud") {
|
||||||
|
settingsActiveTab = 3;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (standalone) {
|
||||||
StandaloneStore.setStandalone(true);
|
StandaloneStore.setStandalone(true);
|
||||||
|
}
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
@@ -276,6 +282,7 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<TabbedContainer
|
<TabbedContainer
|
||||||
|
bind:activeTab={settingsActiveTab}
|
||||||
tabs={[
|
tabs={[
|
||||||
{
|
{
|
||||||
title: "Settings",
|
title: "Settings",
|
||||||
|
|||||||
@@ -39,47 +39,98 @@
|
|||||||
async function handleLogout() {
|
async function handleLogout() {
|
||||||
await cloudAuth.logout();
|
await cloudAuth.logout();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function getInitials(): string {
|
||||||
|
const u = cloudState.user;
|
||||||
|
if (!u) return "?";
|
||||||
|
if (u.displayName) return u.displayName.slice(0, 2).toUpperCase();
|
||||||
|
if (u.username) return u.username.slice(0, 2).toUpperCase();
|
||||||
|
if (u.email) return u.email.slice(0, 2).toUpperCase();
|
||||||
|
return "?";
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div class="flex flex-col gap-4 p-1">
|
<div class="flex flex-col gap-4 p-1">
|
||||||
<div class="p-4 rounded-xl border border-zinc-200/50 dark:border-zinc-700/40 bg-zinc-50/50 dark:bg-zinc-900/30">
|
<div class="overflow-hidden rounded-xl border border-zinc-200/50 dark:border-zinc-700/40 bg-gradient-to-br from-zinc-50 to-white dark:from-zinc-900/50 dark:to-zinc-800/40 shadow-sm">
|
||||||
<h2 class="mb-2 text-lg font-bold">BetterSEQTA Cloud</h2>
|
<!-- Header with icon -->
|
||||||
<p class="mb-4 text-sm text-zinc-600 dark:text-zinc-400">
|
<div class="flex items-center gap-3 px-4 pt-4 pb-2">
|
||||||
Sign in to favorite themes in the theme store. Your favorites sync across devices when logged in.
|
<div class="flex items-center justify-center w-10 h-10 rounded-xl bg-zinc-200/80 dark:bg-zinc-700/60">
|
||||||
</p>
|
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="w-5 h-5 text-zinc-600 dark:text-zinc-300">
|
||||||
|
<path stroke-linecap="round" stroke-linejoin="round" d="M2.25 15a4.5 4.5 0 004.5 4.5H18a3.75 3.75 0 001.332-7.257 3 3 0 00-3.758-3.848 5.25 5.25 0 00-10.233 2.33A4.502 4.502 0 0012.75 15h-10.5z" />
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<h2 class="text-base font-bold text-zinc-900 dark:text-white">BetterSEQTA Cloud</h2>
|
||||||
|
<p class="text-xs text-zinc-500 dark:text-zinc-400">Sync favorites across devices</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="px-4 pb-4">
|
||||||
{#if cloudState.isLoggedIn}
|
{#if cloudState.isLoggedIn}
|
||||||
<div class="flex flex-col gap-3">
|
<!-- Logged in state -->
|
||||||
<p class="text-sm">
|
<div class="flex flex-col gap-4 rounded-lg bg-zinc-100/80 dark:bg-zinc-800/50 p-4">
|
||||||
Signed in as
|
<div class="flex items-center gap-3">
|
||||||
<span class="font-medium">
|
{#if cloudState.user?.pfpUrl}
|
||||||
|
<img
|
||||||
|
src={cloudState.user.pfpUrl}
|
||||||
|
alt=""
|
||||||
|
class="w-12 h-12 rounded-full object-cover ring-2 ring-zinc-200 dark:ring-zinc-600"
|
||||||
|
/>
|
||||||
|
{:else}
|
||||||
|
<div class="flex items-center justify-center w-12 h-12 rounded-full bg-zinc-300 dark:bg-zinc-600 text-zinc-700 dark:text-zinc-200 font-semibold text-sm">
|
||||||
|
{getInitials()}
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
<div class="min-w-0 flex-1">
|
||||||
|
<p class="text-sm font-medium text-zinc-900 dark:text-white truncate">
|
||||||
{cloudState.user?.displayName || cloudState.user?.username || cloudState.user?.email || "User"}
|
{cloudState.user?.displayName || cloudState.user?.username || cloudState.user?.email || "User"}
|
||||||
</span>
|
|
||||||
</p>
|
</p>
|
||||||
|
{#if cloudState.user?.email && cloudState.user?.email !== (cloudState.user?.displayName || cloudState.user?.username)}
|
||||||
|
<p class="text-xs text-zinc-500 dark:text-zinc-400 truncate">{cloudState.user.email}</p>
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
<Button onClick={handleLogout} text="Sign out" />
|
<Button onClick={handleLogout} text="Sign out" />
|
||||||
</div>
|
</div>
|
||||||
{:else}
|
{:else}
|
||||||
|
<!-- Login form -->
|
||||||
|
<p class="mb-4 text-sm text-zinc-600 dark:text-zinc-400">
|
||||||
|
Sign in to favorite themes in the store. Your favorites sync across devices when logged in.
|
||||||
|
</p>
|
||||||
<form
|
<form
|
||||||
class="flex flex-col gap-3"
|
class="flex flex-col gap-3"
|
||||||
|
autocomplete="off"
|
||||||
onsubmit={(e) => {
|
onsubmit={(e) => {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
handleLogin();
|
handleLogin();
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
|
<div>
|
||||||
|
<label for="cloud-username" class="sr-only">Email or username</label>
|
||||||
<input
|
<input
|
||||||
|
id="cloud-username"
|
||||||
type="text"
|
type="text"
|
||||||
|
name="bscloud-login"
|
||||||
|
autocomplete="off"
|
||||||
placeholder="Email or username"
|
placeholder="Email or username"
|
||||||
bind:value={username}
|
bind:value={username}
|
||||||
disabled={loading}
|
disabled={loading}
|
||||||
class="w-full px-3 py-2 text-sm rounded-lg bg-zinc-100 dark:bg-zinc-800 dark:text-white border border-zinc-200 dark:border-zinc-600 focus:outline-none focus:ring-2 focus:ring-accent-ring"
|
class="w-full px-3 py-2.5 text-sm rounded-lg bg-zinc-100 dark:bg-zinc-800 dark:text-white border border-zinc-200 dark:border-zinc-600 focus:outline-none focus:ring-2 focus:ring-accent-ring focus:border-transparent transition-colors duration-200"
|
||||||
/>
|
/>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label for="cloud-password" class="sr-only">Password</label>
|
||||||
<input
|
<input
|
||||||
|
id="cloud-password"
|
||||||
type="password"
|
type="password"
|
||||||
|
name="bscloud-password"
|
||||||
|
autocomplete="new-password"
|
||||||
placeholder="Password"
|
placeholder="Password"
|
||||||
bind:value={password}
|
bind:value={password}
|
||||||
disabled={loading}
|
disabled={loading}
|
||||||
class="w-full px-3 py-2 text-sm rounded-lg bg-zinc-100 dark:bg-zinc-800 dark:text-white border border-zinc-200 dark:border-zinc-600 focus:outline-none focus:ring-2 focus:ring-accent-ring"
|
class="w-full px-3 py-2.5 text-sm rounded-lg bg-zinc-100 dark:bg-zinc-800 dark:text-white border border-zinc-200 dark:border-zinc-600 focus:outline-none focus:ring-2 focus:ring-accent-ring focus:border-transparent transition-colors duration-200"
|
||||||
/>
|
/>
|
||||||
|
</div>
|
||||||
{#if error}
|
{#if error}
|
||||||
<p class="text-sm text-red-600 dark:text-red-400">{error}</p>
|
<p class="text-sm text-red-600 dark:text-red-400">{error}</p>
|
||||||
{/if}
|
{/if}
|
||||||
@@ -92,12 +143,16 @@
|
|||||||
href="https://accounts.betterseqta.org/register"
|
href="https://accounts.betterseqta.org/register"
|
||||||
target="_blank"
|
target="_blank"
|
||||||
rel="noopener noreferrer"
|
rel="noopener noreferrer"
|
||||||
class="inline-flex items-center justify-center 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 hover:scale-[1.02] focus:outline-none focus:ring-2 focus:ring-accent-ring focus:ring-offset-2"
|
class="inline-flex items-center justify-center gap-2 px-4 py-2.5 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 hover:scale-[1.02] focus:outline-none focus:ring-2 focus:ring-accent-ring focus:ring-offset-2"
|
||||||
>
|
>
|
||||||
|
<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="M19 7.5v3m0 0v3m0-3h3m-3 0h-3m-2.25-4.125a3.375 3.375 0 11-6.75 0 3.375 3.375 0 016.75 0zM4 19.235v-.11a6.375 6.375 0 0112.75 0v.109A12.318 12.318 0 0110.374 21c-2.331 0-4.512-.645-6.374-1.766z" />
|
||||||
|
</svg>
|
||||||
Create account
|
Create account
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
{/if}
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -63,11 +63,18 @@
|
|||||||
action: isFavorite ? 'favorite' : 'unfavorite',
|
action: isFavorite ? 'favorite' : 'unfavorite',
|
||||||
})) as { success?: boolean };
|
})) as { success?: boolean };
|
||||||
if (result?.success) {
|
if (result?.success) {
|
||||||
|
const delta = isFavorite ? 1 : -1;
|
||||||
themes = themes.map((t) =>
|
themes = themes.map((t) =>
|
||||||
t.id === theme.id ? { ...t, is_favorited: isFavorite } : t
|
t.id === theme.id
|
||||||
|
? { ...t, is_favorited: isFavorite, favorite_count: Math.max(0, (t.favorite_count ?? 0) + delta) }
|
||||||
|
: t
|
||||||
);
|
);
|
||||||
if (displayTheme?.id === theme.id) {
|
if (displayTheme?.id === theme.id) {
|
||||||
displayTheme = { ...displayTheme, is_favorited: isFavorite };
|
displayTheme = {
|
||||||
|
...displayTheme,
|
||||||
|
is_favorited: isFavorite,
|
||||||
|
favorite_count: Math.max(0, (displayTheme.favorite_count ?? 0) + delta),
|
||||||
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -7,4 +7,5 @@ export type Theme = {
|
|||||||
theme_json_url?: string;
|
theme_json_url?: string;
|
||||||
is_favorited?: boolean;
|
is_favorited?: boolean;
|
||||||
favorite_count?: number;
|
favorite_count?: number;
|
||||||
|
download_count?: number;
|
||||||
};
|
};
|
||||||
|
|||||||
Reference in New Issue
Block a user