feat: implement right-click context menu for style customization and enhance real-time stats tracking
This commit is contained in:
58
frontend/src/composables/useContextMenu.ts
Normal file
58
frontend/src/composables/useContextMenu.ts
Normal file
@@ -0,0 +1,58 @@
|
||||
/**
|
||||
* Global singleton for the right-click style context menu.
|
||||
* Any component calls openContextMenu() to display the floating picker,
|
||||
* and StyleContextMenu.vue (mounted once in App.vue) renders it.
|
||||
*/
|
||||
import { reactive } from 'vue';
|
||||
|
||||
export interface ContextMenuItem {
|
||||
value: string;
|
||||
label: string;
|
||||
swatch?: string; // optional color swatch dot
|
||||
isHeader?: boolean; // non-interactive section heading
|
||||
}
|
||||
|
||||
interface MenuState {
|
||||
visible: boolean;
|
||||
x: number;
|
||||
y: number;
|
||||
title: string;
|
||||
items: ContextMenuItem[];
|
||||
current: string;
|
||||
onSelect: (value: string) => void;
|
||||
}
|
||||
|
||||
const state = reactive<MenuState>({
|
||||
visible: false,
|
||||
x: 0,
|
||||
y: 0,
|
||||
title: '',
|
||||
items: [],
|
||||
current: '',
|
||||
onSelect: () => {},
|
||||
});
|
||||
|
||||
export function openContextMenu(opts: {
|
||||
x: number;
|
||||
y: number;
|
||||
title: string;
|
||||
items: ContextMenuItem[];
|
||||
current: string;
|
||||
onSelect: (value: string) => void;
|
||||
}): void {
|
||||
state.visible = true;
|
||||
state.x = opts.x;
|
||||
state.y = opts.y;
|
||||
state.title = opts.title;
|
||||
state.items = opts.items;
|
||||
state.current = opts.current;
|
||||
state.onSelect = opts.onSelect;
|
||||
}
|
||||
|
||||
export function closeContextMenu(): void {
|
||||
state.visible = false;
|
||||
}
|
||||
|
||||
export function useContextMenu() {
|
||||
return { state };
|
||||
}
|
||||
78
frontend/src/composables/useCustomStyles.ts
Normal file
78
frontend/src/composables/useCustomStyles.ts
Normal file
@@ -0,0 +1,78 @@
|
||||
/**
|
||||
* Viewer-side visual customisations, persisted in localStorage.
|
||||
* None of these affect other users — they're purely local display overrides.
|
||||
*/
|
||||
import { reactive, watch } from 'vue';
|
||||
|
||||
const STORAGE_KEY = 'xip_custom_styles_v1';
|
||||
|
||||
// ── Preset catalogues ────────────────────────────────────────────────────────
|
||||
|
||||
export const SEND_BUTTON_PRESETS = {
|
||||
default: { bg: '#004488', color: '#00ddff', radius: '50%', label: 'Cyan (défaut)' },
|
||||
green: { bg: '#1a4a1a', color: '#00ee77', radius: '50%', label: 'Vert' },
|
||||
purple: { bg: '#2a1040', color: '#cc44ff', radius: '50%', label: 'Violet' },
|
||||
red: { bg: '#3a0a0a', color: '#ff5533', radius: '50%', label: 'Rouge' },
|
||||
square: { bg: '#1a1a1a', color: '#ffffff', radius: '4px', label: 'Blanc carré' },
|
||||
} as const;
|
||||
export type SendButtonKey = keyof typeof SEND_BUTTON_PRESETS;
|
||||
|
||||
export const AD_FRAME_PRESETS = {
|
||||
default: { border: '1px solid #1e1e2a', bg: '#121218', label: 'Défaut' },
|
||||
neon: { border: '1px solid #00ddff66', bg: '#0a1220', label: 'Néon bleu' },
|
||||
gold: { border: '1px solid #ffdd4466', bg: '#141208', label: 'Or' },
|
||||
minimal: { border: '1px solid transparent', bg: '#0c0c10', label: 'Minimal' },
|
||||
} as const;
|
||||
export type AdFrameKey = keyof typeof AD_FRAME_PRESETS;
|
||||
|
||||
export const IP_COLOR_OPTIONS: { value: string; label: string; swatch?: string }[] = [
|
||||
{ value: 'auto', label: 'Auto (palette)' },
|
||||
{ value: '#00ddff', label: 'Cyan', swatch: '#00ddff' },
|
||||
{ value: '#ff00cc', label: 'Rose', swatch: '#ff00cc' },
|
||||
{ value: '#00ee77', label: 'Vert', swatch: '#00ee77' },
|
||||
{ value: '#ffdd44', label: 'Or', swatch: '#ffdd44' },
|
||||
{ value: '#ff5533', label: 'Rouge', swatch: '#ff5533' },
|
||||
{ value: '#ffffff', label: 'Blanc', swatch: '#ffffff' },
|
||||
];
|
||||
|
||||
export const PET_OPTIONS: { value: string; label: string }[] = [
|
||||
{ value: '', label: 'Aucun' },
|
||||
{ value: '🐱', label: '🐱 Chat' },
|
||||
{ value: '🐶', label: '🐶 Chien' },
|
||||
{ value: '✨', label: '✨ Sparkle' },
|
||||
{ value: '🔥', label: '🔥 Feu' },
|
||||
{ value: '👾', label: '👾 Ghost' },
|
||||
{ value: '⚡', label: '⚡ Éclair' },
|
||||
{ value: '🌙', label: '🌙 Lune' },
|
||||
];
|
||||
|
||||
// ── Preferences shape ────────────────────────────────────────────────────────
|
||||
|
||||
export interface CustomStylePrefs {
|
||||
sendButton: SendButtonKey;
|
||||
adFrame: AdFrameKey;
|
||||
ipColors: Record<string, string>; // ip → hex or 'auto'
|
||||
ipPets: Record<string, string>; // ip → emoji or ''
|
||||
}
|
||||
|
||||
function defaults(): CustomStylePrefs {
|
||||
return { sendButton: 'default', adFrame: 'default', ipColors: {}, ipPets: {} };
|
||||
}
|
||||
|
||||
function load(): CustomStylePrefs {
|
||||
try {
|
||||
const raw = localStorage.getItem(STORAGE_KEY);
|
||||
if (raw) return { ...defaults(), ...JSON.parse(raw) };
|
||||
} catch { /* ignore */ }
|
||||
return defaults();
|
||||
}
|
||||
|
||||
const prefs = reactive<CustomStylePrefs>(load());
|
||||
|
||||
watch(prefs, (v) => {
|
||||
localStorage.setItem(STORAGE_KEY, JSON.stringify(v));
|
||||
}, { deep: true });
|
||||
|
||||
export function useCustomStyles() {
|
||||
return { prefs };
|
||||
}
|
||||
Reference in New Issue
Block a user