Fix achievement icons in launcher, allow opening OAuth URLs, live map docs
- Shared achievement icon catalogue; launcher resolves icon/background/border ids - Tauri opener capability now allows http(s) URLs (fixes Discord sign-in) - Rewrite live map docs, admin guide progression section Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01DjMbLQujBHunCCu5GpsHaT
This commit is contained in:
17 files changed
+511
-151
No files matched your search
@@ -15,6 +15,14 @@
|
||||
"core:window:allow-start-dragging",
|
||||
"core:window:allow-is-maximized",
|
||||
"dialog:allow-open",
|
||||
"opener:allow-open-url"
|
||||
{
|
||||
"identifier": "opener:allow-open-url",
|
||||
"allow": [
|
||||
{ "url": "https://*" },
|
||||
{ "url": "https://*/**" },
|
||||
{ "url": "http://*" },
|
||||
{ "url": "http://*/**" }
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
<script lang="ts">
|
||||
import { resolveIcon } from '../lib/achievementIcons';
|
||||
|
||||
let { ach, size = 2.8, locked = false }: {
|
||||
ach: { icon_item?: string | null; icon_bg?: string | null; icon_border?: string | null; frame_type?: string | null };
|
||||
size?: number;
|
||||
locked?: boolean;
|
||||
} = $props();
|
||||
|
||||
// Achievements store ids ("diamond_sword", "obsidian", "gold"); resolve them into something drawable.
|
||||
const icon = $derived(resolveIcon(ach));
|
||||
const frame = $derived(ach.frame_type === 'goal' || ach.frame_type === 'challenge' ? ach.frame_type : 'task');
|
||||
</script>
|
||||
|
||||
<span
|
||||
class="frame {frame}"
|
||||
class:locked
|
||||
style:--size="{size}rem"
|
||||
style:--rim={icon.borderColor}
|
||||
style:--glow={icon.glow}
|
||||
aria-hidden="true"
|
||||
>
|
||||
<span class="inner" style:background={icon.background}><span class="glyph">{icon.emoji}</span></span>
|
||||
</span>
|
||||
|
||||
<style>
|
||||
.frame { width: var(--size); height: var(--size); flex-shrink: 0; display: grid; place-items: center; border: 2px solid var(--rim); background: #18181b; box-shadow: var(--glow); transition: filter 0.2s, box-shadow 0.2s; }
|
||||
.frame.task { border-radius: 0.4rem; }
|
||||
.frame.goal { border-radius: 50%; }
|
||||
.frame.challenge { border-radius: 0.4rem; clip-path: polygon(15% 0%, 85% 0%, 100% 15%, 100% 85%, 85% 100%, 15% 100%, 0% 85%, 0% 15%); box-shadow: none; }
|
||||
.inner { width: 78%; height: 78%; display: grid; place-items: center; border-radius: inherit; box-shadow: inset 0 0 0.4rem rgba(0, 0, 0, 0.5); }
|
||||
.glyph { font-size: calc(var(--size) * 0.5); line-height: 1; filter: drop-shadow(0 1px 1px rgba(0, 0, 0, 0.6)); }
|
||||
.frame.locked { filter: grayscale(0.85) brightness(0.75); box-shadow: none; }
|
||||
</style>
|
||||
@@ -1,4 +1,5 @@
|
||||
<script lang="ts">
|
||||
import AchievementIcon from './AchievementIcon.svelte';
|
||||
import { onMount } from 'svelte';
|
||||
import {
|
||||
X, Trophy, Shield, Heart, Image as ImageIcon, Send, Edit3, Check,
|
||||
@@ -338,9 +339,7 @@
|
||||
<div class="ach-grid">
|
||||
{#each profile.achievements as ach (ach.id)}
|
||||
<div class="ach-item glass" class:frame-challenge={ach.frame_type === 'challenge'}>
|
||||
<div class="ach-icon-box" style:background={ach.icon_bg || 'rgba(0,0,0,0.5)'}>
|
||||
<span>{ach.icon_item || '🏆'}</span>
|
||||
</div>
|
||||
<AchievementIcon {ach} size={2.2} />
|
||||
<div class="ach-meta">
|
||||
<span class="ach-name">{ach.title}</span>
|
||||
<p class="ach-sub tiny muted">{ach.description}</p>
|
||||
@@ -760,15 +759,6 @@
|
||||
border-radius: var(--radius-sm);
|
||||
border: 1px solid var(--line);
|
||||
}
|
||||
.ach-icon-box {
|
||||
width: 2.2rem;
|
||||
height: 2.2rem;
|
||||
border-radius: 4px;
|
||||
display: grid;
|
||||
place-items: center;
|
||||
font-size: 1.1rem;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
.ach-meta {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
|
||||
@@ -0,0 +1,116 @@
|
||||
// Achievement icon catalogue shared by the admin panel and the launcher.
|
||||
//
|
||||
// Achievements store *ids* (`icon_item: "diamond_sword"`, `icon_bg: "obsidian"`,
|
||||
// `icon_border: "gold"`), not pictures, so every app has to turn them into
|
||||
// something drawable. This file is that one translation. The launcher keeps an
|
||||
// identical copy at launcher/src/lib/achievementIcons.ts — change both together.
|
||||
|
||||
export interface ItemIcon { id: string; label: string; emoji: string }
|
||||
|
||||
export const ITEM_ICONS: ItemIcon[] = [
|
||||
{ id: 'diamond_pickaxe', label: 'Diamond Pickaxe', emoji: '⛏️' },
|
||||
{ id: 'wooden_pickaxe', label: 'Wooden Pickaxe', emoji: '⛏️' },
|
||||
{ id: 'diamond', label: 'Diamond', emoji: '💎' },
|
||||
{ id: 'emerald', label: 'Emerald', emoji: '🟢' },
|
||||
{ id: 'gold_ingot', label: 'Gold Ingot', emoji: '🪙' },
|
||||
{ id: 'iron_sword', label: 'Iron Sword', emoji: '🗡️' },
|
||||
{ id: 'diamond_sword', label: 'Diamond Sword', emoji: '⚔️' },
|
||||
{ id: 'netherite_sword', label: 'Netherite Sword', emoji: '⚔️' },
|
||||
{ id: 'netherite_axe', label: 'Netherite Axe', emoji: '🪓' },
|
||||
{ id: 'bow', label: 'Bow', emoji: '🏹' },
|
||||
{ id: 'trident', label: 'Trident', emoji: '🔱' },
|
||||
{ id: 'shield', label: 'Shield', emoji: '🛡️' },
|
||||
{ id: 'bricks', label: 'Bricks', emoji: '🧱' },
|
||||
{ id: 'stone_bricks', label: 'Stone Bricks', emoji: '🧱' },
|
||||
{ id: 'grass_block', label: 'Grass Block', emoji: '🟩' },
|
||||
{ id: 'crafting_table', label: 'Crafting Table', emoji: '🪚' },
|
||||
{ id: 'chest', label: 'Treasure Chest', emoji: '📦' },
|
||||
{ id: 'beacon', label: 'Beacon', emoji: '✨' },
|
||||
{ id: 'clock', label: 'Clock', emoji: '🕰️' },
|
||||
{ id: 'compass', label: 'Compass', emoji: '🧭' },
|
||||
{ id: 'map', label: 'Map', emoji: '🗺️' },
|
||||
{ id: 'book', label: 'Book', emoji: '📖' },
|
||||
{ id: 'enchanted_book', label: 'Enchanted Book', emoji: '📘' },
|
||||
{ id: 'experience_bottle', label: 'Bottle o’ Enchanting', emoji: '🧪' },
|
||||
{ id: 'golden_apple', label: 'Golden Apple', emoji: '🍏' },
|
||||
{ id: 'cake', label: 'Cake', emoji: '🎂' },
|
||||
{ id: 'elytra', label: 'Elytra', emoji: '🪽' },
|
||||
{ id: 'ender_eye', label: 'Eye of Ender', emoji: '👁️' },
|
||||
{ id: 'ender_pearl', label: 'Ender Pearl', emoji: '🔮' },
|
||||
{ id: 'flint_and_steel', label: 'Flint and Steel', emoji: '🔥' },
|
||||
{ id: 'blaze_rod', label: 'Blaze Rod', emoji: '🔥' },
|
||||
{ id: 'heart_of_the_sea', label: 'Heart of the Sea', emoji: '💙' },
|
||||
{ id: 'nether_star', label: 'Nether Star', emoji: '⭐' },
|
||||
{ id: 'totem_of_undying', label: 'Totem of Undying', emoji: '🗿' },
|
||||
{ id: 'dragon_egg', label: 'Dragon Egg', emoji: '🥚' },
|
||||
{ id: 'dragon_head', label: 'Dragon Head', emoji: '🐉' },
|
||||
{ id: 'skull', label: 'Skull', emoji: '💀' },
|
||||
{ id: 'fishing_rod', label: 'Fishing Rod', emoji: '🎣' },
|
||||
{ id: 'wheat', label: 'Wheat', emoji: '🌾' },
|
||||
{ id: 'castle', label: 'Castle Flag', emoji: '🚩' },
|
||||
{ id: 'trophy', label: 'Trophy', emoji: '🏆' },
|
||||
];
|
||||
|
||||
export interface Backdrop { id: string; label: string; css: string }
|
||||
|
||||
export const BACKGROUNDS: Backdrop[] = [
|
||||
{ id: 'stone', label: 'Stone', css: 'radial-gradient(circle, #6b7280 0%, #27272a 100%)' },
|
||||
{ id: 'deepslate', label: 'Deepslate', css: 'radial-gradient(circle, #27272a 0%, #09090b 100%)' },
|
||||
{ id: 'obsidian', label: 'Obsidian', css: 'radial-gradient(circle, #2e1065 0%, #0f172a 100%)' },
|
||||
{ id: 'dirt', label: 'Dirt', css: 'radial-gradient(circle, #92400e 0%, #451a03 100%)' },
|
||||
{ id: 'bedrock', label: 'Bedrock', css: 'radial-gradient(circle, #3f3f46 0%, #09090b 100%)' },
|
||||
{ id: 'oak_planks', label: 'Oak Planks', css: 'radial-gradient(circle, #c08a4b 0%, #6b4423 100%)' },
|
||||
{ id: 'netherrack', label: 'Netherrack', css: 'radial-gradient(circle, #9f1239 0%, #3b0a16 100%)' },
|
||||
{ id: 'end_stone', label: 'End Stone', css: 'radial-gradient(circle, #e8e2a8 0%, #8a8456 100%)' },
|
||||
{ id: 'gilded_blackstone', label: 'Gilded Blackstone', css: 'radial-gradient(circle, #a16207 0%, #1c1917 100%)' },
|
||||
];
|
||||
|
||||
export interface Rim { id: string; label: string; color: string }
|
||||
|
||||
export const BORDERS: Rim[] = [
|
||||
{ id: 'bronze', label: 'Bronze', color: '#b45309' },
|
||||
{ id: 'silver', label: 'Silver', color: '#94a3b8' },
|
||||
{ id: 'gold', label: 'Gold', color: '#eab308' },
|
||||
{ id: 'diamond', label: 'Diamond', color: '#06b6d4' },
|
||||
{ id: 'purple', label: 'Amethyst / Purple', color: '#c084fc' },
|
||||
{ id: 'netherite', label: 'Netherite', color: '#475569' },
|
||||
];
|
||||
|
||||
export const FRAMES = [
|
||||
{ id: 'task', label: 'Task (Normal)', bannerTitle: 'Advancement Made!', bannerColor: '#55ff55' },
|
||||
{ id: 'goal', label: 'Goal (Rounded)', bannerTitle: 'Goal Reached!', bannerColor: '#55ffff' },
|
||||
{ id: 'challenge', label: 'Challenge (Spiked & Glowing)', bannerTitle: 'Challenge Complete!', bannerColor: '#ff55ff' },
|
||||
] as const;
|
||||
|
||||
// Ids that aren't in the catalogue (added through the API, or from an older
|
||||
// data set) still get a sensible picture from their name.
|
||||
const KEYWORDS: [RegExp, string][] = [
|
||||
[/pickaxe|shovel|hoe/, '⛏️'], [/axe/, '🪓'], [/sword|dagger/, '⚔️'], [/bow|arrow/, '🏹'], [/trident/, '🔱'],
|
||||
[/shield/, '🛡️'], [/helmet|chestplate|leggings|boots|armor/, '🛡️'], [/apple|carrot|bread|food|steak/, '🍏'],
|
||||
[/potion|bottle/, '🧪'], [/book/, '📖'], [/ore|ingot|nugget/, '🪙'], [/diamond|emerald|amethyst|gem/, '💎'],
|
||||
[/star|beacon/, '✨'], [/egg/, '🥚'], [/head|skull/, '💀'], [/block|brick|stone|planks|log/, '🧱'],
|
||||
[/fire|flame|blaze|lava/, '🔥'], [/fish|rod/, '🎣'], [/clock|watch/, '🕰️'], [/map|compass/, '🧭'],
|
||||
];
|
||||
|
||||
const isCss = (v: string) => /^(#|rgb|hsl|linear-gradient|radial-gradient|var\()/i.test(v.trim());
|
||||
const isId = (v: string) => /^[a-z0-9_]+$/.test(v);
|
||||
|
||||
export interface ResolvedIcon { emoji: string; background: string; borderColor: string; glow: string }
|
||||
|
||||
export function resolveIcon(a: { icon_item?: string | null; icon_bg?: string | null; icon_border?: string | null }): ResolvedIcon {
|
||||
const item = (a.icon_item ?? '').trim();
|
||||
let emoji = '🏆';
|
||||
if (item) {
|
||||
const known = ITEM_ICONS.find((i) => i.id === item);
|
||||
if (known) emoji = known.emoji;
|
||||
else if (!isId(item)) emoji = item; // already an emoji or symbol
|
||||
else emoji = KEYWORDS.find(([re]) => re.test(item))?.[1] ?? '🏆';
|
||||
}
|
||||
|
||||
const bgRaw = (a.icon_bg ?? '').trim();
|
||||
const background = BACKGROUNDS.find((b) => b.id === bgRaw)?.css ?? (bgRaw && isCss(bgRaw) ? bgRaw : BACKGROUNDS[1].css);
|
||||
|
||||
const rimRaw = (a.icon_border ?? '').trim();
|
||||
const borderColor = BORDERS.find((b) => b.id === rimRaw)?.color ?? (rimRaw && isCss(rimRaw) ? rimRaw : '#eab308');
|
||||
return { emoji, background, borderColor, glow: `0 0 12px ${borderColor}99` };
|
||||
}
|
||||
@@ -231,7 +231,7 @@ export async function mockInvoke(cmd: string, args: Record<string, any>): Promis
|
||||
{ id: 'ach1', title: 'Stone Age', description: 'Mine your first piece of cobblestone.', category: 'mining', xp_reward: 50, frame_type: 'task', icon_item: '🪨', icon_bg: '#334155', icon_border: '#64748b', unlocked: true, unlocked_at: '2026-09-20T12:00:00Z' },
|
||||
{ id: 'ach2', title: 'Acquire Hardware', description: 'Smelt an iron ingot in a furnace.', category: 'mining', xp_reward: 100, frame_type: 'task', icon_item: '⛓️', icon_bg: '#475569', icon_border: '#94a3b8', unlocked: true, unlocked_at: '2026-09-21T14:30:00Z' },
|
||||
{ id: 'ach3', title: 'Diamonds!', description: 'Acquire diamonds from deep underground.', category: 'mining', xp_reward: 350, frame_type: 'goal', icon_item: '💎', icon_bg: '#0284c7', icon_border: '#38bdf8', unlocked: true, unlocked_at: '2026-09-24T18:15:00Z' },
|
||||
{ id: 'ach4', title: 'Free the End', description: 'Defeat the Ender Dragon in the End dimension.', category: 'combat', xp_reward: 1000, frame_type: 'challenge', icon_item: '🐉', icon_bg: '#581c87', icon_border: '#f59e0b', unlocked: false, unlocked_at: null },
|
||||
{ id: 'ach4', title: 'Free the End', description: 'Defeat the Ender Dragon in the End dimension.', category: 'combat', xp_reward: 1000, frame_type: 'challenge', icon_item: 'dragon_head', icon_bg: 'end_stone', icon_border: 'purple', unlocked: false, unlocked_at: null },
|
||||
];
|
||||
case 'get_guilds':
|
||||
return [
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
Target, Trophy, Clock, Sparkles, Check, Gift, Pickaxe, Swords, Hammer,
|
||||
Wheat, Users, Compass, RefreshCw, LoaderCircle, Flame, Star, Award, Shield
|
||||
} from '@lucide/svelte';
|
||||
import AchievementIcon from '../components/AchievementIcon.svelte';
|
||||
import { invoke } from '../lib/tauri';
|
||||
import { toast } from '../lib/store.svelte';
|
||||
import type { UserLevelInfo, ServerLevelInfo, UserQuest, Achievement } from '../lib/types';
|
||||
@@ -217,9 +218,7 @@
|
||||
<div class="achievements-grid">
|
||||
{#each achievements as ach (ach.id)}
|
||||
<div class="achievement-card glass" class:unlocked={ach.unlocked} class:frame-task={ach.frame_type === 'task'} class:frame-goal={ach.frame_type === 'goal'} class:frame-challenge={ach.frame_type === 'challenge'}>
|
||||
<div class="achievement-icon-frame" style:background={ach.icon_bg || 'rgba(0,0,0,0.5)'} style:border-color={ach.icon_border || 'var(--line)'}>
|
||||
<span class="sprite-icon">{ach.icon_item || '💎'}</span>
|
||||
</div>
|
||||
<AchievementIcon {ach} locked={!ach.unlocked} />
|
||||
<div class="achievement-body">
|
||||
<div class="achievement-header">
|
||||
<span class="ach-title">{ach.title}</span>
|
||||
|
||||
Reference in new issue
Block a user