feat: enhance keyboard shortcuts and background image handling

- Added `GlobalKeyboardShortcuts` component to manage global keyboard shortcuts.
- Introduced new keyboard shortcut (Shift + B) for changing the background.
- Updated `BackgroundImageSelector` to preserve custom background URLs and allow restoration of previously set backgrounds.
- Improved local storage handling for custom backgrounds to enhance user experience.
This commit is contained in:
Julien Froidefond
2025-10-02 13:52:18 +02:00
parent d4e8dc144b
commit 9094aca1ff
6 changed files with 169 additions and 9 deletions

View File

@@ -8,6 +8,7 @@ import { UserPreferencesProvider } from "@/contexts/UserPreferencesContext";
import { KeyboardShortcutsProvider } from "@/contexts/KeyboardShortcutsContext"; import { KeyboardShortcutsProvider } from "@/contexts/KeyboardShortcutsContext";
import { userPreferencesService } from "@/services/core/user-preferences"; import { userPreferencesService } from "@/services/core/user-preferences";
import { KeyboardShortcuts } from "@/components/KeyboardShortcuts"; import { KeyboardShortcuts } from "@/components/KeyboardShortcuts";
import { GlobalKeyboardShortcuts } from "@/components/GlobalKeyboardShortcuts";
import { AuthProvider } from "../components/AuthProvider"; import { AuthProvider } from "../components/AuthProvider";
import { getServerSession } from 'next-auth'; import { getServerSession } from 'next-auth';
import { authOptions } from '@/lib/auth'; import { authOptions } from '@/lib/auth';
@@ -55,6 +56,7 @@ export default async function RootLayout({
<KeyboardShortcuts /> <KeyboardShortcuts />
<JiraConfigProvider config={initialPreferences?.jiraConfig || { enabled: false }}> <JiraConfigProvider config={initialPreferences?.jiraConfig || { enabled: false }}>
<UserPreferencesProvider initialPreferences={initialPreferences}> <UserPreferencesProvider initialPreferences={initialPreferences}>
<GlobalKeyboardShortcuts />
<BackgroundProvider> <BackgroundProvider>
{children} {children}
</BackgroundProvider> </BackgroundProvider>

View File

@@ -0,0 +1,14 @@
'use client';
import { useGlobalKeyboardShortcuts } from '@/hooks/useGlobalKeyboardShortcuts';
import { useBackgroundCycle } from '@/hooks/useBackgroundCycle';
export function GlobalKeyboardShortcuts() {
const { cycleBackground } = useBackgroundCycle();
useGlobalKeyboardShortcuts({
onCycleBackground: cycleBackground
});
return null;
}

View File

@@ -87,11 +87,20 @@ export function BackgroundImageSelector() {
const [customUrl, setCustomUrl] = useState(''); const [customUrl, setCustomUrl] = useState('');
const [showCustomInput, setShowCustomInput] = useState(false); const [showCustomInput, setShowCustomInput] = useState(false);
const [showAdvancedOptions, setShowAdvancedOptions] = useState(false); const [showAdvancedOptions, setShowAdvancedOptions] = useState(false);
const [preservedCustomUrl, setPreservedCustomUrl] = useState<string>('');
const currentBackground = preferences?.viewPreferences?.backgroundImage; const currentBackground = preferences?.viewPreferences?.backgroundImage;
const backgroundBlur = preferences?.viewPreferences?.backgroundBlur || 0; const backgroundBlur = preferences?.viewPreferences?.backgroundBlur || 0;
const backgroundOpacity = preferences?.viewPreferences?.backgroundOpacity || 100; const backgroundOpacity = preferences?.viewPreferences?.backgroundOpacity || 100;
// Préserver l'URL personnalisée si elle existe
useEffect(() => {
if (currentBackground && !PRESET_BACKGROUNDS.some(preset => preset.id === currentBackground)) {
setPreservedCustomUrl(currentBackground);
localStorage.setItem('preservedCustomBackground', currentBackground);
}
}, [currentBackground]);
const handlePresetSelect = (presetId: string) => { const handlePresetSelect = (presetId: string) => {
const backgroundImage = presetId === 'none' ? undefined : presetId; const backgroundImage = presetId === 'none' ? undefined : presetId;
updateViewPreferences({ backgroundImage }); updateViewPreferences({ backgroundImage });
@@ -100,14 +109,24 @@ export function BackgroundImageSelector() {
const handleCustomUrlSubmit = () => { const handleCustomUrlSubmit = () => {
if (!customUrl.trim()) return; if (!customUrl.trim()) return;
updateViewPreferences({ backgroundImage: customUrl.trim() }); const url = customUrl.trim();
updateViewPreferences({ backgroundImage: url });
setPreservedCustomUrl(url);
localStorage.setItem('preservedCustomBackground', url);
setCustomUrl(''); setCustomUrl('');
setShowCustomInput(false); setShowCustomInput(false);
}; };
const handleRemoveCustom = () => { const handleRemoveCustom = () => {
updateViewPreferences({ backgroundImage: undefined }); updateViewPreferences({ backgroundImage: undefined });
setPreservedCustomUrl('');
localStorage.removeItem('preservedCustomBackground');
};
const handleRestoreCustom = () => {
if (preservedCustomUrl) {
updateViewPreferences({ backgroundImage: preservedCustomUrl });
}
}; };
const handleBlurChange = (blur: number) => { const handleBlurChange = (blur: number) => {
@@ -212,6 +231,17 @@ export function BackgroundImageSelector() {
Ajoutez votre propre image de fond Ajoutez votre propre image de fond
</p> </p>
</div> </div>
<div className="flex gap-2">
{preservedCustomUrl && (
<Button
onClick={handleRestoreCustom}
variant="secondary"
size="sm"
className="text-xs"
>
Restaurer
</Button>
)}
<Button <Button
onClick={() => setShowCustomInput(!showCustomInput)} onClick={() => setShowCustomInput(!showCustomInput)}
variant="secondary" variant="secondary"
@@ -220,6 +250,7 @@ export function BackgroundImageSelector() {
{showCustomInput ? 'Masquer' : 'Ajouter'} {showCustomInput ? 'Masquer' : 'Ajouter'}
</Button> </Button>
</div> </div>
</div>
{showCustomInput && ( {showCustomInput && (
<div className="space-y-3"> <div className="space-y-3">

View File

@@ -32,6 +32,11 @@ const PAGE_SHORTCUTS: PageShortcuts = {
description: 'Faire tourner les thèmes dark', description: 'Faire tourner les thèmes dark',
category: 'Apparence' category: 'Apparence'
}, },
{
keys: ['Shift', 'B'],
description: 'Changer le background',
category: 'Apparence'
},
{ {
keys: ['Esc'], keys: ['Esc'],
description: 'Fermer les modales/annuler', description: 'Fermer les modales/annuler',

View File

@@ -0,0 +1,103 @@
'use client';
import { useUserPreferences } from '@/contexts/UserPreferencesContext';
// Liste des backgrounds prédéfinis pour le cycle
const BACKGROUND_CYCLE = [
'none',
'theme-subtle',
'theme-primary',
'theme-accent',
'theme-success',
'theme-purple',
'theme-diagonal',
'theme-radial',
'theme-sunset',
'theme-ocean',
'theme-forest',
'theme-galaxy'
];
export function useBackgroundCycle() {
const { preferences, updateViewPreferences } = useUserPreferences();
const cycleBackground = () => {
const currentBackground = preferences?.viewPreferences?.backgroundImage;
// Construire la liste complète des backgrounds (prédéfinis + personnalisé)
const allBackgrounds = [...BACKGROUND_CYCLE];
// Ajouter l'image personnalisée préservée si elle existe
const preservedCustomUrl = localStorage.getItem('preservedCustomBackground');
console.log('Debug cycle:', {
currentBackground,
preservedCustomUrl,
localStorageKeys: Object.keys(localStorage)
});
if (preservedCustomUrl && !BACKGROUND_CYCLE.includes(preservedCustomUrl)) {
allBackgrounds.push(preservedCustomUrl);
}
// Ajouter aussi l'image actuelle si elle est personnalisée ET différente de celle préservée
if (currentBackground &&
!BACKGROUND_CYCLE.includes(currentBackground) &&
currentBackground !== preservedCustomUrl) {
allBackgrounds.push(currentBackground);
}
const currentIndex = allBackgrounds.findIndex(bg => bg === currentBackground);
// Si on ne trouve pas l'index, c'est qu'on est sur "none" (undefined)
// Dans ce cas, on commence au début de la liste
let actualCurrentIndex = currentIndex;
if (currentIndex === -1) {
actualCurrentIndex = -1; // On est sur "none", on va commencer à l'index 0
}
const nextIndex = (actualCurrentIndex + 1) % allBackgrounds.length;
const nextBackground = allBackgrounds[nextIndex];
// Si c'est 'none', on met undefined
const backgroundImage = nextBackground === 'none' ? undefined : nextBackground;
// Si on est sur "none" (undefined) et qu'on va vers "none", on va vers le suivant
if (currentBackground === undefined && nextBackground === 'none') {
const nextNextIndex = (nextIndex + 1) % allBackgrounds.length;
const nextNextBackground = allBackgrounds[nextNextIndex];
const finalBackgroundImage = nextNextBackground === 'none' ? undefined : nextNextBackground;
console.log('Cycle result:', {
allBackgrounds,
currentIndex,
actualCurrentIndex,
nextIndex,
nextBackground,
backgroundImage,
currentBackground,
nextNextIndex,
nextNextBackground,
finalBackgroundImage
});
updateViewPreferences({ backgroundImage: finalBackgroundImage });
return;
}
console.log('Cycle result:', {
allBackgrounds,
currentIndex,
actualCurrentIndex,
nextIndex,
nextBackground,
backgroundImage,
currentBackground
});
updateViewPreferences({ backgroundImage });
};
return {
cycleBackground
};
}

View File

@@ -21,6 +21,7 @@ interface KeyboardShortcutsActions {
onSave?: () => void; onSave?: () => void;
onBackup?: () => void; onBackup?: () => void;
onOpenSearch?: () => void; onOpenSearch?: () => void;
onCycleBackground?: () => void;
} }
export function useGlobalKeyboardShortcuts(actions: KeyboardShortcutsActions = {}) { export function useGlobalKeyboardShortcuts(actions: KeyboardShortcutsActions = {}) {
@@ -56,6 +57,10 @@ export function useGlobalKeyboardShortcuts(actions: KeyboardShortcutsActions = {
event.preventDefault(); event.preventDefault();
actions.onOpenSearch?.(); actions.onOpenSearch?.();
return; return;
case 'B':
event.preventDefault();
actions.onCycleBackground?.();
return;
} }
} }