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;
|
||||
}
|
||||
|
||||
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": {
|
||||
const { url } = request;
|
||||
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 { 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,39 +86,48 @@
|
||||
>
|
||||
<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>
|
||||
<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)}
|
||||
<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}
|
||||
<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"/>
|
||||
@@ -117,7 +136,7 @@
|
||||
<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">
|
||||
<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"/>
|
||||
@@ -126,6 +145,7 @@
|
||||
<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}
|
||||
|
||||
@@ -20,6 +20,7 @@
|
||||
import { settingsPopup } from "../hooks/SettingsPopup";
|
||||
|
||||
let devModeSequence = "";
|
||||
let settingsActiveTab = $state(0);
|
||||
let showDisclaimerModal = $state(false);
|
||||
let disclaimerCallbacks = $state<{ onConfirm: () => void, onCancel: () => void } | null>(null);
|
||||
|
||||
@@ -72,13 +73,18 @@
|
||||
showDisclaimerModal = true;
|
||||
};
|
||||
|
||||
onMount(async () => {
|
||||
onMount(() => {
|
||||
settingsPopup.addListener(() => {
|
||||
showColourPicker = false;
|
||||
});
|
||||
|
||||
if (!standalone) return;
|
||||
if (window.location.hash === "#cloud") {
|
||||
settingsActiveTab = 3;
|
||||
}
|
||||
|
||||
if (standalone) {
|
||||
StandaloneStore.setStandalone(true);
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
@@ -276,6 +282,7 @@
|
||||
</div>
|
||||
|
||||
<TabbedContainer
|
||||
bind:activeTab={settingsActiveTab}
|
||||
tabs={[
|
||||
{
|
||||
title: "Settings",
|
||||
|
||||
@@ -39,47 +39,98 @@
|
||||
async function handleLogout() {
|
||||
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>
|
||||
|
||||
<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">
|
||||
<h2 class="mb-2 text-lg font-bold">BetterSEQTA Cloud</h2>
|
||||
<p class="mb-4 text-sm text-zinc-600 dark:text-zinc-400">
|
||||
Sign in to favorite themes in the theme store. Your favorites sync across devices when logged in.
|
||||
</p>
|
||||
<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">
|
||||
<!-- Header with icon -->
|
||||
<div class="flex items-center gap-3 px-4 pt-4 pb-2">
|
||||
<div class="flex items-center justify-center w-10 h-10 rounded-xl bg-zinc-200/80 dark:bg-zinc-700/60">
|
||||
<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}
|
||||
<div class="flex flex-col gap-3">
|
||||
<p class="text-sm">
|
||||
Signed in as
|
||||
<span class="font-medium">
|
||||
<!-- Logged in state -->
|
||||
<div class="flex flex-col gap-4 rounded-lg bg-zinc-100/80 dark:bg-zinc-800/50 p-4">
|
||||
<div class="flex items-center gap-3">
|
||||
{#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"}
|
||||
</span>
|
||||
</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" />
|
||||
</div>
|
||||
{: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
|
||||
class="flex flex-col gap-3"
|
||||
autocomplete="off"
|
||||
onsubmit={(e) => {
|
||||
e.preventDefault();
|
||||
handleLogin();
|
||||
}}
|
||||
>
|
||||
<div>
|
||||
<label for="cloud-username" class="sr-only">Email or username</label>
|
||||
<input
|
||||
id="cloud-username"
|
||||
type="text"
|
||||
name="bscloud-login"
|
||||
autocomplete="off"
|
||||
placeholder="Email or username"
|
||||
bind:value={username}
|
||||
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
|
||||
id="cloud-password"
|
||||
type="password"
|
||||
name="bscloud-password"
|
||||
autocomplete="new-password"
|
||||
placeholder="Password"
|
||||
bind:value={password}
|
||||
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}
|
||||
<p class="text-sm text-red-600 dark:text-red-400">{error}</p>
|
||||
{/if}
|
||||
@@ -92,12 +143,16 @@
|
||||
href="https://accounts.betterseqta.org/register"
|
||||
target="_blank"
|
||||
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
|
||||
</a>
|
||||
</div>
|
||||
</form>
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -63,11 +63,18 @@
|
||||
action: isFavorite ? 'favorite' : 'unfavorite',
|
||||
})) as { success?: boolean };
|
||||
if (result?.success) {
|
||||
const delta = isFavorite ? 1 : -1;
|
||||
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) {
|
||||
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;
|
||||
is_favorited?: boolean;
|
||||
favorite_count?: number;
|
||||
download_count?: number;
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user