[FEAT] We now have a functional APP
26
AGENTS.md
@@ -132,8 +132,34 @@ MySQL database connection configured in `api/config/database.php`:
|
|||||||
- Site targets French-speaking users
|
- Site targets French-speaking users
|
||||||
- Production URL: `ohmj2.free.fr`
|
- Production URL: `ohmj2.free.fr`
|
||||||
|
|
||||||
|
## Design Guidelines
|
||||||
|
|
||||||
|
- **Style**: Clean, sober, and modern
|
||||||
|
- **Icons**: Use SVG icons instead of emojis for better consistency
|
||||||
|
- **Buttons**: Integrate related actions (view + download) in a single row with consistent styling
|
||||||
|
- **Hide unnecessary labels**: Don't show "Piece 1" or "Version 1" when there's only one
|
||||||
|
- **Visual hierarchy**: Use spacing, subtle borders, and hover effects for interactivity
|
||||||
|
|
||||||
## Temporary Work Files
|
## Temporary Work Files
|
||||||
|
|
||||||
- Use `_builds/` directory for temporary scripts and working files
|
- Use `_builds/` directory for temporary scripts and working files
|
||||||
- Only `scripts/convert_final_v2.js` should be kept in the scripts folder (committed to git)
|
- Only `scripts/convert_final_v2.js` should be kept in the scripts folder (committed to git)
|
||||||
- CSV output files belong in `_builds/`
|
- CSV output files belong in `_builds/`
|
||||||
|
|
||||||
|
## Authentication
|
||||||
|
|
||||||
|
- **Login**: Use `apiService.login('admin', 'password')` from frontend
|
||||||
|
- **API calls**: Include token in Authorization header:
|
||||||
|
```
|
||||||
|
Authorization: Bearer <token>
|
||||||
|
```
|
||||||
|
- **Token format**: JWT (HS256) - see `api/lib/Auth.php`
|
||||||
|
- **Frontend**: Token stored in localStorage, auto-attached to API requests via axios interceptor
|
||||||
|
- **Test**: http://localhost:5173 - login with admin/password
|
||||||
|
- **API base URL**: http://localhost:8000/api/
|
||||||
|
|
||||||
|
## Current Tech Stack (2024)
|
||||||
|
|
||||||
|
- **Frontend**: SvelteKit (NOT Vue.js 2) in `/partitions/`
|
||||||
|
- **Backend**: PHP API in `/api/`
|
||||||
|
- **Scores storage**: `/legacy/Scores/` (directory-based, not MySQL)
|
||||||
|
|||||||
@@ -9,6 +9,11 @@
|
|||||||
"username": "user",
|
"username": "user",
|
||||||
"password": "$2y$10$92IXUNpkjO0rOQ5byMi.Ye4oKoEa3Ro9llC/.og/at2.uheWG/igi",
|
"password": "$2y$10$92IXUNpkjO0rOQ5byMi.Ye4oKoEa3Ro9llC/.og/at2.uheWG/igi",
|
||||||
"role": "user"
|
"role": "user"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"username": "ohmj",
|
||||||
|
"password": "$2y$10$qKjbai66h7sAY3ciIaO6z.ch1PwQweVCDtqWk85avk4sFcSBmTMBm",
|
||||||
|
"role": "user"
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -86,8 +86,11 @@ export const apiService = {
|
|||||||
return response.data.score;
|
return response.data.score;
|
||||||
},
|
},
|
||||||
|
|
||||||
async getInstruments(scoreId: string): Promise<Instrument[]> {
|
async getInstruments(scoreId: string, pieceId?: number): Promise<Instrument[]> {
|
||||||
const response = await api.get(`/scores/${scoreId}/instruments`);
|
const url = pieceId
|
||||||
|
? `/scores/${scoreId}/instruments?piece=${pieceId}`
|
||||||
|
: `/scores/${scoreId}/instruments`;
|
||||||
|
const response = await api.get(url);
|
||||||
return response.data.instruments;
|
return response.data.instruments;
|
||||||
},
|
},
|
||||||
|
|
||||||
|
|||||||
@@ -28,6 +28,7 @@
|
|||||||
let loading = $state(true);
|
let loading = $state(true);
|
||||||
let error = $state('');
|
let error = $state('');
|
||||||
let rendering = $state(false);
|
let rendering = $state(false);
|
||||||
|
let isFullscreen = $state(false);
|
||||||
|
|
||||||
async function loadPdf() {
|
async function loadPdf() {
|
||||||
loading = true;
|
loading = true;
|
||||||
@@ -102,8 +103,14 @@
|
|||||||
renderPage(currentPage);
|
renderPage(currentPage);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function toggleFullscreen() {
|
||||||
|
isFullscreen = !isFullscreen;
|
||||||
|
}
|
||||||
|
|
||||||
function handleKeydown(e: KeyboardEvent) {
|
function handleKeydown(e: KeyboardEvent) {
|
||||||
if (e.key === 'ArrowLeft' || e.key === 'ArrowUp') {
|
if (e.key === 'Escape' && isFullscreen) {
|
||||||
|
isFullscreen = false;
|
||||||
|
} else if (e.key === 'ArrowLeft' || e.key === 'ArrowUp') {
|
||||||
prevPage();
|
prevPage();
|
||||||
} else if (e.key === 'ArrowRight' || e.key === 'ArrowDown') {
|
} else if (e.key === 'ArrowRight' || e.key === 'ArrowDown') {
|
||||||
nextPage();
|
nextPage();
|
||||||
@@ -129,8 +136,8 @@
|
|||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div class="flex flex-col h-full">
|
<div class="flex flex-col h-full" class:fixed={isFullscreen} class:inset-0={isFullscreen} class:z-50={isFullscreen} class:bg-gray-900={isFullscreen}>
|
||||||
<div class="bg-ohmj-dark text-white px-4 py-2 flex items-center justify-between">
|
<div class="bg-ohmj-dark text-white px-4 py-2 flex items-center justify-between border-b border-gray-700">
|
||||||
<div class="flex items-center gap-2">
|
<div class="flex items-center gap-2">
|
||||||
<span class="font-semibold">{title}</span>
|
<span class="font-semibold">{title}</span>
|
||||||
{#if totalPages > 0}
|
{#if totalPages > 0}
|
||||||
@@ -143,7 +150,9 @@
|
|||||||
class="px-2 py-1 bg-gray-700 hover:bg-gray-600 rounded text-sm"
|
class="px-2 py-1 bg-gray-700 hover:bg-gray-600 rounded text-sm"
|
||||||
title="Zoom -"
|
title="Zoom -"
|
||||||
>
|
>
|
||||||
➖
|
<svg xmlns="http://www.w3.org/2000/svg" class="h-4 w-4" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||||
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M20 12H4" />
|
||||||
|
</svg>
|
||||||
</button>
|
</button>
|
||||||
<span class="text-sm w-16 text-center">{Math.round(scale * 100)}%</span>
|
<span class="text-sm w-16 text-center">{Math.round(scale * 100)}%</span>
|
||||||
<button
|
<button
|
||||||
@@ -151,7 +160,24 @@
|
|||||||
class="px-2 py-1 bg-gray-700 hover:bg-gray-600 rounded text-sm"
|
class="px-2 py-1 bg-gray-700 hover:bg-gray-600 rounded text-sm"
|
||||||
title="Zoom +"
|
title="Zoom +"
|
||||||
>
|
>
|
||||||
➕
|
<svg xmlns="http://www.w3.org/2000/svg" class="h-4 w-4" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||||
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 4v16m8-8H4" />
|
||||||
|
</svg>
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
onclick={toggleFullscreen}
|
||||||
|
class="px-2 py-1 bg-gray-700 hover:bg-gray-600 rounded text-sm ml-2"
|
||||||
|
title={isFullscreen ? 'Quitter plein écran' : 'Plein écran'}
|
||||||
|
>
|
||||||
|
{#if isFullscreen}
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" class="h-4 w-4" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||||
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 9V4.5M9 9H4.5M9 9L3.75 3.75M9 15v4.5M9 15H4.5M9 15l-5.25 5.25M15 9h4.5M15 9V4.5M15 9l5.25-5.25M15 15h4.5M15 15v4.5m0-4.5l5.25 5.25" />
|
||||||
|
</svg>
|
||||||
|
{:else}
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" class="h-4 w-4" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||||
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M3.75 3.75v4.5m0-4.5h4.5m-4.5 0L9 9M3.75 20.25v-4.5m0 4.5h4.5m-4.5 0L9 15M20.25 3.75h-4.5m4.5 0v4.5m0-4.5L15 9m5.25 11.25h-4.5m4.5 0v-4.5m0 4.5L15 15" />
|
||||||
|
</svg>
|
||||||
|
{/if}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -172,7 +198,7 @@
|
|||||||
{/if}
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="bg-ohmj-dark text-white px-4 py-3 flex items-center justify-center gap-4">
|
<div class="bg-ohmj-dark text-white px-4 py-3 flex items-center justify-center gap-4 border-t border-gray-700">
|
||||||
<button
|
<button
|
||||||
onclick={prevPage}
|
onclick={prevPage}
|
||||||
disabled={currentPage <= 1}
|
disabled={currentPage <= 1}
|
||||||
|
|||||||
@@ -48,9 +48,6 @@
|
|||||||
</span>
|
</span>
|
||||||
</a>
|
</a>
|
||||||
<nav class="flex items-center gap-4">
|
<nav class="flex items-center gap-4">
|
||||||
<a href="/scores" class="hover:text-ohmj-secondary transition-colors">
|
|
||||||
Mes Partitions
|
|
||||||
</a>
|
|
||||||
<button
|
<button
|
||||||
onclick={logout}
|
onclick={logout}
|
||||||
class="bg-red-600 hover:bg-red-700 px-3 py-1 rounded text-sm transition-colors"
|
class="bg-red-600 hover:bg-red-700 px-3 py-1 rounded text-sm transition-colors"
|
||||||
|
|||||||
@@ -31,22 +31,23 @@
|
|||||||
$: sortedScores = [...scores].sort((a, b) => {
|
$: sortedScores = [...scores].sort((a, b) => {
|
||||||
let aVal = a[sortBy] || '';
|
let aVal = a[sortBy] || '';
|
||||||
let bVal = b[sortBy] || '';
|
let bVal = b[sortBy] || '';
|
||||||
|
let cmp: number;
|
||||||
if (sortBy === 'id') {
|
if (sortBy === 'id') {
|
||||||
aVal = a.id;
|
cmp = parseInt(a.id) - parseInt(b.id);
|
||||||
bVal = b.id;
|
} else {
|
||||||
|
cmp = aVal.localeCompare(bVal);
|
||||||
}
|
}
|
||||||
const cmp = aVal.localeCompare(bVal);
|
|
||||||
return sortOrder === 'asc' ? cmp : -cmp;
|
return sortOrder === 'asc' ? cmp : -cmp;
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<svelte:head>
|
<svelte:head>
|
||||||
<title>Mes Partitions - OHMJ</title>
|
<title>Répertoire - OHMJ</title>
|
||||||
</svelte:head>
|
</svelte:head>
|
||||||
|
|
||||||
<div class="container mx-auto px-4 py-8">
|
<div class="container mx-auto px-4 py-8">
|
||||||
<div class="mb-6">
|
<div class="mb-6">
|
||||||
<h1 class="text-3xl font-bold text-ohmj-primary">Mes Partitions</h1>
|
<h1 class="text-3xl font-bold text-ohmj-primary">Répertoire</h1>
|
||||||
<p class="text-gray-600 mt-2">{scores.length} partitions disponibles</p>
|
<p class="text-gray-600 mt-2">{scores.length} partitions disponibles</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -64,7 +65,7 @@
|
|||||||
<thead class="bg-gray-50">
|
<thead class="bg-gray-50">
|
||||||
<tr>
|
<tr>
|
||||||
<th
|
<th
|
||||||
class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider cursor-pointer hover:bg-gray-100"
|
class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider cursor-pointer hover:bg-gray-100 w-20"
|
||||||
onclick={() => sortScores('id')}
|
onclick={() => sortScores('id')}
|
||||||
>
|
>
|
||||||
№ {sortBy === 'id' ? (sortOrder === 'asc' ? '↑' : '↓') : ''}
|
№ {sortBy === 'id' ? (sortOrder === 'asc' ? '↑' : '↓') : ''}
|
||||||
@@ -81,31 +82,20 @@
|
|||||||
>
|
>
|
||||||
Compositeur {sortBy === 'compositor' ? (sortOrder === 'asc' ? '↑' : '↓') : ''}
|
Compositeur {sortBy === 'compositor' ? (sortOrder === 'asc' ? '↑' : '↓') : ''}
|
||||||
</th>
|
</th>
|
||||||
<th class="px-6 py-3 text-right text-xs font-medium text-gray-500 uppercase tracking-wider">
|
|
||||||
Action
|
|
||||||
</th>
|
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody class="divide-y divide-gray-200">
|
<tbody class="divide-y divide-gray-200">
|
||||||
{#each sortedScores as score}
|
{#each sortedScores as score}
|
||||||
<tr class="hover:bg-gray-50 transition-colors">
|
<tr class="hover:bg-ohmj-light transition-colors cursor-pointer" onclick={() => window.location.href = '/scores/' + score.id}>
|
||||||
<td class="px-6 py-4 whitespace-nowrap text-sm font-medium text-gray-900">
|
<td class="px-6 py-4 whitespace-nowrap text-sm font-medium text-gray-900">
|
||||||
{score.id}
|
{score.id}
|
||||||
</td>
|
</td>
|
||||||
<td class="px-6 py-4 whitespace-nowrap text-sm text-gray-700">
|
<td class="px-6 py-4 whitespace-nowrap text-sm font-medium text-ohmj-primary hover:text-ohmj-secondary">
|
||||||
{score.name}
|
{score.name}
|
||||||
</td>
|
</td>
|
||||||
<td class="px-6 py-4 whitespace-nowrap text-sm text-gray-500">
|
<td class="px-6 py-4 whitespace-nowrap text-sm text-gray-500">
|
||||||
{score.compositor || '-'}
|
{score.compositor || '-'}
|
||||||
</td>
|
</td>
|
||||||
<td class="px-6 py-4 whitespace-nowrap text-right text-sm">
|
|
||||||
<a
|
|
||||||
href="/scores/{score.id}"
|
|
||||||
class="inline-flex items-center px-3 py-1 border border-ohmj-primary text-sm font-medium rounded text-ohmj-primary hover:bg-ohmj-primary hover:text-white transition-colors"
|
|
||||||
>
|
|
||||||
Voir →
|
|
||||||
</a>
|
|
||||||
</td>
|
|
||||||
</tr>
|
</tr>
|
||||||
{/each}
|
{/each}
|
||||||
</tbody>
|
</tbody>
|
||||||
|
|||||||
@@ -5,10 +5,10 @@
|
|||||||
import { goto } from '$app/navigation';
|
import { goto } from '$app/navigation';
|
||||||
|
|
||||||
const INSTRUMENT_ICONS: Record<string, string> = {
|
const INSTRUMENT_ICONS: Record<string, string> = {
|
||||||
dir: '🎼', pic: '🎺', flu: '🎵', cla: '🎵', clb: '🎵',
|
dir: '🎼', pic: '/icons/piccolo.png', flu: '/icons/flute.png', cla: '/icons/clarinette.png', clb: '/icons/clarinette_basse.png',
|
||||||
sax: '🎷', sab: '🎷', sat: '🎷', coa: '🎵', cba: '🎸',
|
sax: '/icons/sax-alto.png', sab: '/icons/sax-barython.png', sat: '/icons/sax-tenor.png', coa: '/icons/cor-anglais.jpg', cba: '/icons/contrebasse.png',
|
||||||
cor: '🥇', trp: '🎺', trb: '🎺', tub: '🎺', htb: '🎵',
|
cor: '/icons/cor.png', trp: '/icons/trumpet.png', trb: '/icons/tronbonne.png', tub: '/icons/tuba.png', htb: '/icons/hautbois.png',
|
||||||
bas: '🎻', per: '🥁', crn: '🎺', eup: '🎺', har: '🎵',
|
bas: '/icons/basson.png', per: '/icons/percussion.png', crn: '/icons/cornet.png', eup: '/icons/euphonium.png', har: '/icons/harpe.png',
|
||||||
pia: '🎹', sup: '📄', par: '📄'
|
pia: '🎹', sup: '📄', par: '📄'
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -39,6 +39,11 @@
|
|||||||
|
|
||||||
page.subscribe(($page) => {
|
page.subscribe(($page) => {
|
||||||
scoreId = $page.params.id || '';
|
scoreId = $page.params.id || '';
|
||||||
|
// Read piece from URL param or query string
|
||||||
|
const pieceParam = $page.params.piece || $page.url.searchParams.get('piece');
|
||||||
|
if (pieceParam) {
|
||||||
|
selectedPiece = parseInt(pieceParam) || 1;
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
onMount(async () => {
|
onMount(async () => {
|
||||||
@@ -46,15 +51,13 @@
|
|||||||
score = await apiService.getScore(scoreId);
|
score = await apiService.getScore(scoreId);
|
||||||
pieces = await apiService.getPieces(scoreId);
|
pieces = await apiService.getPieces(scoreId);
|
||||||
|
|
||||||
if (pieces.length > 1) {
|
// Set default piece to 1 if not in URL
|
||||||
// Multi-pieces score - use instruments from API
|
if (pieces.length > 0 && !pieces.find(p => p.id === selectedPiece)) {
|
||||||
instruments = score?.instruments || [];
|
selectedPiece = pieces[0].id;
|
||||||
loading = false;
|
|
||||||
} else {
|
|
||||||
// Single piece - load instruments
|
|
||||||
selectedPiece = 1;
|
|
||||||
instruments = score?.instruments || [];
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Load instruments for the selected piece
|
||||||
|
await loadInstrumentsForPiece(selectedPiece);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
error = 'Erreur lors du chargement';
|
error = 'Erreur lors du chargement';
|
||||||
console.error(err);
|
console.error(err);
|
||||||
@@ -63,8 +66,21 @@
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
async function loadInstrumentsForPiece(pieceId: number) {
|
||||||
|
loading = true;
|
||||||
|
try {
|
||||||
|
instruments = await apiService.getInstruments(scoreId, pieceId);
|
||||||
|
} catch (err) {
|
||||||
|
console.error('Error loading instruments:', err);
|
||||||
|
error = 'Erreur lors du chargement des instruments';
|
||||||
|
} finally {
|
||||||
|
loading = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
async function selectPiece(pieceId: number) {
|
async function selectPiece(pieceId: number) {
|
||||||
goto(`/scores/${scoreId}/${pieceId}`);
|
selectedPiece = pieceId;
|
||||||
|
await loadInstrumentsForPiece(pieceId);
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
@@ -130,14 +146,21 @@
|
|||||||
{@const code = instrument.id || instrument.code || ''}
|
{@const code = instrument.id || instrument.code || ''}
|
||||||
{@const title = instrument.title || getInstrumentName(code)}
|
{@const title = instrument.title || getInstrumentName(code)}
|
||||||
{@const fileCount = instrument.parts?.[0]?.files?.length || 0}
|
{@const fileCount = instrument.parts?.[0]?.files?.length || 0}
|
||||||
|
{@const icon = getInstrumentIcon(code)}
|
||||||
<a
|
<a
|
||||||
href="/scores/{scoreId}/{selectedPiece}/{code}"
|
href="/scores/{scoreId}/{selectedPiece}/{code}"
|
||||||
class="bg-white p-6 rounded-lg shadow hover:shadow-lg transition-shadow border border-gray-200 hover:border-ohmj-primary"
|
class="bg-white p-6 rounded-lg shadow hover:shadow-lg transition-shadow border border-gray-200 hover:border-ohmj-primary"
|
||||||
>
|
>
|
||||||
<div class="text-center">
|
<div class="text-center">
|
||||||
<div class="text-4xl mb-2">{getInstrumentIcon(code)}</div>
|
<div class="text-4xl mb-2">
|
||||||
|
{#if icon.startsWith('/')}
|
||||||
|
<img src={icon} alt={title} class="w-12 h-12 mx-auto object-contain" />
|
||||||
|
{:else}
|
||||||
|
{icon}
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
<h3 class="font-semibold text-gray-800">{title}</h3>
|
<h3 class="font-semibold text-gray-800">{title}</h3>
|
||||||
<p class="text-sm text-gray-500 mt-1">{fileCount} fichier{fileCount !== 1 ? 's' : ''}</p>
|
<p class="text-sm text-gray-500 mt-1">{fileCount} partie{fileCount !== 1 ? 's' : ''}</p>
|
||||||
</div>
|
</div>
|
||||||
</a>
|
</a>
|
||||||
{/each}
|
{/each}
|
||||||
|
|||||||
@@ -4,10 +4,10 @@
|
|||||||
import { apiService, type Score, type Instrument } from '$lib/api';
|
import { apiService, type Score, type Instrument } from '$lib/api';
|
||||||
|
|
||||||
const INSTRUMENT_ICONS: Record<string, string> = {
|
const INSTRUMENT_ICONS: Record<string, string> = {
|
||||||
dir: '🎼', pic: '🎺', flu: '🎵', cla: '🎵', clb: '🎵',
|
dir: '🎼', pic: '/icons/piccolo.png', flu: '/icons/flute.png', cla: '/icons/clarinette.png', clb: '/icons/clarinette_basse.png',
|
||||||
sax: '🎷', sab: '🎷', sat: '🎷', coa: '🎵', cba: '🎸',
|
sax: '/icons/sax-alto.png', sab: '/icons/sax-barython.png', sat: '/icons/sax-tenor.png', coa: '/icons/cor-anglais.jpg', cba: '/icons/contrebasse.png',
|
||||||
cor: '🥇', trp: '🎺', trb: '🎺', tub: '🎺', htb: '🎵',
|
cor: '/icons/cor.png', trp: '/icons/trumpet.png', trb: '/icons/tronbonne.png', tub: '/icons/tuba.png', htb: '/icons/hautbois.png',
|
||||||
bas: '🎻', per: '🥁', crn: '🎺', eup: '🎺', har: '🎵',
|
bas: '/icons/basson.png', per: '/icons/percussion.png', crn: '/icons/cornet.png', eup: '/icons/euphonium.png', har: '/icons/harpe.png',
|
||||||
pia: '🎹', sup: '📄', par: '📄'
|
pia: '🎹', sup: '📄', par: '📄'
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -63,12 +63,19 @@
|
|||||||
<div class="grid grid-cols-2 md:grid-cols-3 lg:grid-cols-4 gap-4">
|
<div class="grid grid-cols-2 md:grid-cols-3 lg:grid-cols-4 gap-4">
|
||||||
{#each instruments as instrument}
|
{#each instruments as instrument}
|
||||||
{@const fileCount = instrument.parts?.[0]?.files?.length || 0}
|
{@const fileCount = instrument.parts?.[0]?.files?.length || 0}
|
||||||
|
{@const icon = getInstrumentIcon(instrument.id)}
|
||||||
<a
|
<a
|
||||||
href="/scores/{scoreId}/{pieceId}/{instrument.id}"
|
href="/scores/{scoreId}/{pieceId}/{instrument.id}"
|
||||||
class="bg-white p-6 rounded-lg shadow hover:shadow-lg transition-shadow border border-gray-200 hover:border-ohmj-primary"
|
class="bg-white p-6 rounded-lg shadow hover:shadow-lg transition-shadow border border-gray-200 hover:border-ohmj-primary"
|
||||||
>
|
>
|
||||||
<div class="text-center">
|
<div class="text-center">
|
||||||
<div class="text-4xl mb-2">{getInstrumentIcon(instrument.id)}</div>
|
<div class="text-4xl mb-2">
|
||||||
|
{#if icon.startsWith('/')}
|
||||||
|
<img src={icon} alt={instrument.title} class="w-12 h-12 mx-auto object-contain" />
|
||||||
|
{:else}
|
||||||
|
{icon}
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
<h3 class="font-semibold text-gray-800">{instrument.title}</h3>
|
<h3 class="font-semibold text-gray-800">{instrument.title}</h3>
|
||||||
<p class="text-sm text-gray-500 mt-1">{fileCount} fichier{fileCount !== 1 ? 's' : ''}</p>
|
<p class="text-sm text-gray-500 mt-1">{fileCount} fichier{fileCount !== 1 ? 's' : ''}</p>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -40,6 +40,8 @@
|
|||||||
let error = $state('');
|
let error = $state('');
|
||||||
let selectedFile = $state('');
|
let selectedFile = $state('');
|
||||||
let showSidebar = $state(true);
|
let showSidebar = $state(true);
|
||||||
|
let pieceName = $state('');
|
||||||
|
let pieceCount = $state(0);
|
||||||
|
|
||||||
let pdfViewerUrl = $derived(selectedFile ? apiService.getDownloadUrl(selectedFile) : '');
|
let pdfViewerUrl = $derived(selectedFile ? apiService.getDownloadUrl(selectedFile) : '');
|
||||||
let pdfFileName = $derived(selectedFile ? selectedFile.split('/').pop() || 'Partition' : 'Partition');
|
let pdfFileName = $derived(selectedFile ? selectedFile.split('/').pop() || 'Partition' : 'Partition');
|
||||||
@@ -52,6 +54,14 @@
|
|||||||
|
|
||||||
onMount(async () => {
|
onMount(async () => {
|
||||||
try {
|
try {
|
||||||
|
// Get pieces info
|
||||||
|
const pieces = await apiService.getPieces(scoreId);
|
||||||
|
pieceCount = pieces.length;
|
||||||
|
const piece = pieces.find(p => p.id.toString() === pieceId);
|
||||||
|
if (piece) {
|
||||||
|
pieceName = piece.name;
|
||||||
|
}
|
||||||
|
|
||||||
const score = await apiService.getScore(scoreId);
|
const score = await apiService.getScore(scoreId);
|
||||||
const instrument = (score.instruments || []).find(
|
const instrument = (score.instruments || []).find(
|
||||||
i => i.id === instrumentCode && i.piece === pieceId
|
i => i.id === instrumentCode && i.piece === pieceId
|
||||||
@@ -107,12 +117,12 @@
|
|||||||
|
|
||||||
<div class="h-[calc(100vh-64px)] flex flex-col">
|
<div class="h-[calc(100vh-64px)] flex flex-col">
|
||||||
<div class="container mx-auto px-4 py-4">
|
<div class="container mx-auto px-4 py-4">
|
||||||
<a href="/scores/{scoreId}/{pieceId}" class="inline-flex items-center text-ohmj-primary hover:underline mb-4">
|
<a href="/scores/{scoreId}?piece={pieceId}" class="inline-flex items-center text-ohmj-primary hover:underline mb-4">
|
||||||
← Retour aux instruments
|
← Retour aux instruments
|
||||||
</a>
|
</a>
|
||||||
|
|
||||||
<h1 class="text-2xl font-bold text-ohmj-primary">
|
<h1 class="text-2xl font-bold text-ohmj-primary">
|
||||||
{instrumentTitle || getInstrumentName(instrumentCode)} - Partition {scoreId} - Pièce {pieceId}
|
Partition {scoreId} - {instrumentTitle || getInstrumentName(instrumentCode)}{pieceCount > 1 ? (pieceName ? ' - ' + pieceName : ' - Pièce ' + pieceId) : ''}
|
||||||
</h1>
|
</h1>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -144,22 +154,26 @@
|
|||||||
|
|
||||||
{#each parts as part}
|
{#each parts as part}
|
||||||
<div class="mb-4">
|
<div class="mb-4">
|
||||||
<h3 class="font-medium text-gray-800 mb-2">Version {part.id}</h3>
|
{#if parts.length > 1}
|
||||||
|
<h3 class="font-medium text-gray-800 mb-2">Version {part.id}</h3>
|
||||||
|
{/if}
|
||||||
<ul class="space-y-1">
|
<ul class="space-y-1">
|
||||||
{#each part.files as file}
|
{#each part.files as file}
|
||||||
<li class="flex items-center gap-2">
|
<li class="flex items-stretch gap-1 rounded overflow-hidden border border-gray-200 hover:border-ohmj-primary transition-colors">
|
||||||
<button
|
<button
|
||||||
onclick={() => openViewer(file.path)}
|
onclick={() => openViewer(file.path)}
|
||||||
class="flex-1 text-left px-3 py-2 rounded hover:bg-ohmj-light border border-transparent hover:border-ohmj-primary transition-colors text-sm {selectedFile === file.path ? 'bg-ohmj-primary text-white' : 'text-ohmj-primary'}"
|
class="flex-1 text-left px-3 py-2 bg-white hover:bg-ohmj-light text-sm text-ohmj-primary font-medium"
|
||||||
>
|
>
|
||||||
📖 {file.name} {getFileInfo(file)}
|
📖 {file.name} {getFileInfo(file)}
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
onclick={() => downloadFile(file.path)}
|
onclick={() => downloadFile(file.path)}
|
||||||
class="px-3 py-2 text-gray-500 hover:text-ohmj-secondary transition-colors text-sm"
|
class="px-3 py-2 bg-gray-50 hover:bg-gray-100 text-gray-600 hover:text-ohmj-primary transition-colors text-sm"
|
||||||
title="Télécharger"
|
title="Télécharger"
|
||||||
>
|
>
|
||||||
⬇️
|
<svg xmlns="http://www.w3.org/2000/svg" class="h-4 w-4" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||||
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 16v1a3 3 0 003 3h10a3 3 0 003-3v-1m-4-4l-4 4m0 0l-4-4m4 4V4" />
|
||||||
|
</svg>
|
||||||
</button>
|
</button>
|
||||||
</li>
|
</li>
|
||||||
{/each}
|
{/each}
|
||||||
|
|||||||
@@ -1,25 +1,25 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
const INSTRUMENT_ICONS: Record<string, string> = {
|
const INSTRUMENT_ICONS: Record<string, string> = {
|
||||||
dir: '🎼',
|
dir: '🎼',
|
||||||
pic: '🎵',
|
pic: '/icons/piccolo.png',
|
||||||
flu: '🎵',
|
flu: '/icons/flute.png',
|
||||||
cla: '🎵',
|
cla: '/icons/clarinette.png',
|
||||||
clb: '🎵',
|
clb: '/icons/clarinette_basse.png',
|
||||||
sax: '🎷',
|
sax: '/icons/sax-alto.png',
|
||||||
sab: '🎷',
|
sab: '/icons/sax-barython.png',
|
||||||
sat: '🎷',
|
sat: '/icons/sax-tenor.png',
|
||||||
coa: '🎵',
|
coa: '/icons/cor-anglais.jpg',
|
||||||
cba: '🎸',
|
cba: '/icons/contrebasse.png',
|
||||||
cor: '🥇',
|
cor: '/icons/cor.png',
|
||||||
trp: '🎺',
|
trp: '/icons/trumpet.png',
|
||||||
trb: '🎺',
|
trb: '/icons/tronbonne.png',
|
||||||
tub: '🎺',
|
tub: '/icons/tuba.png',
|
||||||
htb: '🎵',
|
htb: '/icons/hautbois.png',
|
||||||
bas: '🎻',
|
bas: '/icons/basson.png',
|
||||||
per: '🥁',
|
per: '/icons/percussion.png',
|
||||||
crn: '🎺',
|
crn: '/icons/cornet.png',
|
||||||
eup: '🎺',
|
eup: '/icons/euphonium.png',
|
||||||
har: '🎵',
|
har: '/icons/harpe.png',
|
||||||
pia: '🎹',
|
pia: '🎹',
|
||||||
sup: '📄',
|
sup: '📄',
|
||||||
par: '📄'
|
par: '📄'
|
||||||
@@ -68,7 +68,13 @@
|
|||||||
<div class="grid grid-cols-2 md:grid-cols-3 lg:grid-cols-4 gap-4">
|
<div class="grid grid-cols-2 md:grid-cols-3 lg:grid-cols-4 gap-4">
|
||||||
{#each instruments as inst}
|
{#each instruments as inst}
|
||||||
<div class="bg-white p-6 rounded-lg shadow border border-gray-200 text-center">
|
<div class="bg-white p-6 rounded-lg shadow border border-gray-200 text-center">
|
||||||
<div class="text-5xl mb-2">{inst.icon}</div>
|
<div class="text-5xl mb-2">
|
||||||
|
{#if inst.icon.startsWith('/')}
|
||||||
|
<img src={inst.icon} alt={inst.name} class="w-12 h-12 mx-auto object-contain" />
|
||||||
|
{:else}
|
||||||
|
{inst.icon}
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
<p class="font-semibold">{inst.name}</p>
|
<p class="font-semibold">{inst.name}</p>
|
||||||
<p class="text-sm text-gray-400">{inst.code}</p>
|
<p class="text-sm text-gray-400">{inst.code}</p>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
BIN
partitions/static/icons/basson.png
Normal file
|
After Width: | Height: | Size: 1.9 KiB |
BIN
partitions/static/icons/clarinette.png
Normal file
|
After Width: | Height: | Size: 1.6 KiB |
BIN
partitions/static/icons/clarinette_basse.png
Normal file
|
After Width: | Height: | Size: 6.5 KiB |
BIN
partitions/static/icons/contrebasse.png
Normal file
|
After Width: | Height: | Size: 2.7 KiB |
BIN
partitions/static/icons/cor-anglais.jpg
Normal file
|
After Width: | Height: | Size: 79 KiB |
BIN
partitions/static/icons/cor.png
Normal file
|
After Width: | Height: | Size: 3.3 KiB |
BIN
partitions/static/icons/cornet.png
Normal file
|
After Width: | Height: | Size: 2.4 KiB |
BIN
partitions/static/icons/euphonium.png
Normal file
|
After Width: | Height: | Size: 2.6 KiB |
BIN
partitions/static/icons/flute.png
Normal file
|
After Width: | Height: | Size: 1004 B |
BIN
partitions/static/icons/harpe.png
Normal file
|
After Width: | Height: | Size: 2.2 KiB |
BIN
partitions/static/icons/hautbois.png
Normal file
|
After Width: | Height: | Size: 452 B |
BIN
partitions/static/icons/percussion.png
Normal file
|
After Width: | Height: | Size: 3.5 KiB |
BIN
partitions/static/icons/piccolo.png
Normal file
|
After Width: | Height: | Size: 652 B |
BIN
partitions/static/icons/sax-alto.png
Normal file
|
After Width: | Height: | Size: 3.0 KiB |
BIN
partitions/static/icons/sax-barython.png
Normal file
|
After Width: | Height: | Size: 3.8 KiB |
BIN
partitions/static/icons/sax-soprano.png
Normal file
|
After Width: | Height: | Size: 1.4 KiB |
BIN
partitions/static/icons/sax-tenor.png
Normal file
|
After Width: | Height: | Size: 2.8 KiB |
BIN
partitions/static/icons/tronbonne.png
Normal file
|
After Width: | Height: | Size: 1.1 KiB |
BIN
partitions/static/icons/trumpet.png
Normal file
|
After Width: | Height: | Size: 1.7 KiB |
BIN
partitions/static/icons/tuba.png
Normal file
|
After Width: | Height: | Size: 2.6 KiB |