feat(fleet): foto da embarcação + horímetro + cadastro + matrícula v1.4.1
Some checks are pending
Build Android (APK + AAB) / build-android (push) Waiting to run
Some checks are pending
Build Android (APK + AAB) / build-android (push) Waiting to run
Novos campos por embarcação: - Foto (capture câmera ou galeria, resize automático max 1280px JPEG q=0.85) - Horímetro inicial do motor - Data de cadastro (defaulta hoje em novas) - Matrícula / TIE (Capitania) - Notas livres UI: - Preview circular no editor com botões câmera/galeria/remover - Avatar circular no header (foto se houver, ícone do tipo senão) - Avatar 44x44 na lista da frota - Foto guardada no IndexedDB (mesma store das mídias de viagem/manutenção) - Lifecycle pareado: remover barco apaga foto, trocar foto apaga antiga - /apk redirect aponta pra v1.4.1 Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
21af0f00b7
commit
5833efcc48
5 changed files with 454 additions and 18 deletions
|
|
@ -528,6 +528,34 @@ header{
|
|||
.fleet-units-toggle button.active{background:var(--brass);color:var(--bg-paper)}
|
||||
.fleet-units-toggle button:hover:not(.active){background:var(--bg-aged)}
|
||||
|
||||
/* === Boat Photo === */
|
||||
.boat-photo-row{display:flex;gap:14px;align-items:flex-start}
|
||||
.boat-photo-preview{
|
||||
width:96px;height:96px;flex-shrink:0;
|
||||
background:var(--bg-canvas);border:1px solid var(--rule);
|
||||
display:flex;align-items:center;justify-content:center;
|
||||
overflow:hidden;position:relative;
|
||||
}
|
||||
.boat-photo-preview img{width:100%;height:100%;object-fit:cover;display:block}
|
||||
.boat-photo-placeholder{font-size:36px;opacity:.4}
|
||||
.boat-photo-actions{flex:1;display:flex;flex-direction:column;justify-content:center}
|
||||
|
||||
/* Avatares pequenos (lista frota + header) */
|
||||
.fleet-avatar{
|
||||
width:44px;height:44px;flex-shrink:0;
|
||||
background:var(--bg-aged);border:1px solid var(--rule);
|
||||
display:flex;align-items:center;justify-content:center;
|
||||
overflow:hidden;font-size:22px;
|
||||
}
|
||||
.fleet-avatar img{width:100%;height:100%;object-fit:cover}
|
||||
.boat-header-avatar{
|
||||
width:34px;height:34px;border-radius:50%;flex-shrink:0;
|
||||
background:rgba(250,242,221,.12);border:1.5px solid rgba(250,242,221,.3);
|
||||
overflow:hidden;display:flex;align-items:center;justify-content:center;
|
||||
font-size:16px;color:rgba(250,242,221,.6);
|
||||
}
|
||||
.boat-header-avatar img{width:100%;height:100%;object-fit:cover}
|
||||
|
||||
/* === Anchor Calculator === */
|
||||
.anchor-calc{
|
||||
background:var(--bg-canvas);border:1px solid var(--rule);
|
||||
|
|
@ -1338,6 +1366,7 @@ header{
|
|||
</g>
|
||||
<text x="50" y="14" text-anchor="middle" font-family="JetBrains Mono,monospace" font-size="6" fill="currentColor" font-weight="600">N</text>
|
||||
</svg>
|
||||
<div class="boat-header-avatar" id="boat-header-avatar" onclick="openFleetManager()" style="cursor:pointer" title="Gerenciar frota">⛵</div>
|
||||
<div class="boat-info">
|
||||
<div class="boat-tagline">Diário de Bordo · Logbook</div>
|
||||
<button type="button" class="boat-name boat-selector" id="boat-selector" onclick="openFleetManager()" title="Gerenciar frota">
|
||||
|
|
@ -1879,6 +1908,26 @@ Hora: {HORA}</textarea>
|
|||
<div class="modal-body">
|
||||
<form id="boat-editor-form" onsubmit="saveBoatFromForm(event)">
|
||||
<input type="hidden" id="boat-edit-id">
|
||||
<input type="hidden" id="boat-edit-photo-id">
|
||||
<div class="field">
|
||||
<label class="field-label">Foto da embarcação</label>
|
||||
<div class="boat-photo-row">
|
||||
<div class="boat-photo-preview" id="boat-photo-preview">
|
||||
<span class="boat-photo-placeholder">⛵</span>
|
||||
</div>
|
||||
<div class="boat-photo-actions">
|
||||
<label class="btn btn-sm btn-block" style="cursor:pointer;display:block">
|
||||
📷 Tirar foto
|
||||
<input type="file" accept="image/*" capture="environment" onchange="handleBoatPhoto(event)" style="display:none">
|
||||
</label>
|
||||
<label class="btn btn-sm btn-block" style="cursor:pointer;display:block;margin-top:6px">
|
||||
🖼 Da galeria
|
||||
<input type="file" accept="image/*" onchange="handleBoatPhoto(event)" style="display:none">
|
||||
</label>
|
||||
<button type="button" class="btn btn-sm btn-danger btn-block" onclick="clearBoatPhoto()" id="boat-photo-clear" style="margin-top:6px;display:none">Remover foto</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="field"><label class="field-label">Nome</label>
|
||||
<input type="text" id="boat-edit-name" required maxlength="40" placeholder="ex: Shivao, Maré Alta">
|
||||
</div>
|
||||
|
|
@ -1910,9 +1959,26 @@ Hora: {HORA}</textarea>
|
|||
<input type="number" id="boat-edit-chain" step="1" min="0" placeholder="total disponível">
|
||||
</div>
|
||||
</div>
|
||||
<div class="field-row">
|
||||
<div class="field"><label class="field-label">Ano (opcional)</label>
|
||||
<input type="number" id="boat-edit-year" min="1900" max="2100" placeholder="ex: 2018">
|
||||
</div>
|
||||
<div class="field"><label class="field-label">Data de cadastro</label>
|
||||
<input type="date" id="boat-edit-registered-at">
|
||||
</div>
|
||||
</div>
|
||||
<div class="field-row">
|
||||
<div class="field"><label class="field-label">Horímetro inicial</label>
|
||||
<input type="number" id="boat-edit-engine-hours-initial" step="0.1" min="0" placeholder="ex: 1240.5">
|
||||
<div class="field-hint" style="margin-top:4px">Horas do motor no dia que começou a registrar.</div>
|
||||
</div>
|
||||
<div class="field"><label class="field-label">Matrícula / TIE</label>
|
||||
<input type="text" id="boat-edit-registration" maxlength="32" placeholder="ex: SP-2348-CT">
|
||||
</div>
|
||||
</div>
|
||||
<div class="field"><label class="field-label">Notas (opcional)</label>
|
||||
<textarea id="boat-edit-notes" maxlength="500" rows="2" placeholder="Marina, observações, manutenção pendente..."></textarea>
|
||||
</div>
|
||||
<div class="field-hint">⚓ Comprimento + calado são usados pra calcular raio de giro no fundeio.</div>
|
||||
</form>
|
||||
</div>
|
||||
|
|
@ -1965,11 +2031,24 @@ function migrateBoatsSchema(){
|
|||
length:null,beam:null,draft:null,
|
||||
chainTotal:null,
|
||||
year:null,
|
||||
photoId:null,
|
||||
engineHoursInitial:null,
|
||||
registeredAt:null,
|
||||
registrationNumber:'',
|
||||
notes:'',
|
||||
createdAt:Date.now(),
|
||||
};
|
||||
state.boats.push(b);
|
||||
state.activeBoatId=b.id;
|
||||
}
|
||||
// Garante campos novos em barcos existentes (forward-compat)
|
||||
state.boats.forEach(b=>{
|
||||
if(!('photoId' in b))b.photoId=null;
|
||||
if(!('engineHoursInitial' in b))b.engineHoursInitial=null;
|
||||
if(!('registeredAt' in b))b.registeredAt=null;
|
||||
if(!('registrationNumber' in b))b.registrationNumber='';
|
||||
if(!('notes' in b))b.notes='';
|
||||
});
|
||||
if(!state.activeBoatId&&state.boats[0])state.activeBoatId=state.boats[0].id;
|
||||
if(!state.units)state.units='metric';
|
||||
}
|
||||
|
|
@ -1996,6 +2075,11 @@ function addBoat(data){
|
|||
draft:lenFromInput(data.draft),
|
||||
chainTotal:lenFromInput(data.chainTotal),
|
||||
year:data.year?parseInt(data.year):null,
|
||||
photoId:data.photoId||null,
|
||||
engineHoursInitial:data.engineHoursInitial?parseFloat(data.engineHoursInitial):null,
|
||||
registeredAt:data.registeredAt||null,
|
||||
registrationNumber:data.registrationNumber||'',
|
||||
notes:data.notes||'',
|
||||
createdAt:Date.now(),
|
||||
};
|
||||
state.boats.push(b);
|
||||
|
|
@ -2015,11 +2099,19 @@ function updateBoat(id,data){
|
|||
if('draft' in data)b.draft=lenFromInput(data.draft);
|
||||
if('chainTotal' in data)b.chainTotal=lenFromInput(data.chainTotal);
|
||||
if('year' in data)b.year=data.year?parseInt(data.year):null;
|
||||
if('photoId' in data)b.photoId=data.photoId;
|
||||
if('engineHoursInitial' in data)b.engineHoursInitial=data.engineHoursInitial?parseFloat(data.engineHoursInitial):null;
|
||||
if('registeredAt' in data)b.registeredAt=data.registeredAt||null;
|
||||
if('registrationNumber' in data)b.registrationNumber=data.registrationNumber||'';
|
||||
if('notes' in data)b.notes=data.notes||'';
|
||||
saveState();
|
||||
}
|
||||
|
||||
function removeBoat(id){
|
||||
if(state.boats.length<=1){toast('Mantenha pelo menos 1 embarcação');return}
|
||||
const b=state.boats.find(x=>x.id===id);
|
||||
// Limpa foto do IndexedDB se houver
|
||||
if(b?.photoId){dbDelete(b.photoId).catch(()=>{})}
|
||||
state.boats=state.boats.filter(b=>b.id!==id);
|
||||
if(state.activeBoatId===id)state.activeBoatId=state.boats[0]?.id||null;
|
||||
saveState();
|
||||
|
|
@ -2243,16 +2335,33 @@ function renderGPSBanner(){
|
|||
function bindHeader(){
|
||||
const nameEl=document.getElementById('boat-name-display');
|
||||
const metaEl=document.getElementById('boat-model-display');
|
||||
const avatarEl=document.getElementById('boat-header-avatar');
|
||||
if(!nameEl||!metaEl)return;
|
||||
const b=activeBoat();
|
||||
if(!b){nameEl.textContent='Sem embarcação';metaEl.textContent='Toque para adicionar';return}
|
||||
if(!b){
|
||||
nameEl.textContent='Sem embarcação';
|
||||
metaEl.textContent='Toque para adicionar';
|
||||
if(avatarEl)avatarEl.innerHTML='⛵';
|
||||
return;
|
||||
}
|
||||
nameEl.textContent=b.name||'Embarcação';
|
||||
const t=BOAT_TYPES[b.type]||BOAT_TYPES.sailing;
|
||||
const parts=[`${t.icon} ${t.label}`];
|
||||
if(b.model)parts.push(b.model);
|
||||
if(b.length)parts.push(fmtLen(b.length,1));
|
||||
metaEl.textContent=parts.join(' · ');
|
||||
// sync legacy state.boat para compat com qualquer código antigo que ainda use
|
||||
// Avatar: foto se houver, senão ícone do tipo
|
||||
if(avatarEl){
|
||||
avatarEl.innerHTML=t.icon;
|
||||
if(b.photoId){
|
||||
dbGet(b.photoId).then(item=>{
|
||||
if(item&&activeBoat()?.id===b.id){
|
||||
avatarEl.innerHTML=`<img src="${getMediaUrl(item)}" alt="">`;
|
||||
}
|
||||
}).catch(()=>{});
|
||||
}
|
||||
}
|
||||
// sync legacy state.boat para compat
|
||||
state.boat={name:b.name,model:b.model};
|
||||
}
|
||||
|
||||
|
|
@ -2277,7 +2386,7 @@ function renderFleetList(){
|
|||
if(b.model)meta.push(b.model);
|
||||
if(b.length)meta.push(fmtLen(b.length,1));
|
||||
return `<div class="fleet-item ${isActive?'active':''}" onclick="setActiveBoatAndClose('${b.id}')">
|
||||
<div class="fleet-icon">${t.icon}</div>
|
||||
<div class="fleet-avatar" data-boat-id="${b.id}">${t.icon}</div>
|
||||
<div class="fleet-info">
|
||||
<div class="fleet-name">${escapeHtml(b.name)}</div>
|
||||
<div class="fleet-meta">${meta.map(escapeHtml).join(' · ')}</div>
|
||||
|
|
@ -2286,6 +2395,20 @@ function renderFleetList(){
|
|||
<button type="button" class="fleet-edit-icon" onclick="event.stopPropagation();openBoatEditor('${b.id}')" title="Editar">✎</button>
|
||||
</div>`;
|
||||
}).join('');
|
||||
// Carrega fotos async — não bloqueia a render
|
||||
state.boats.forEach(b=>{if(b.photoId)loadBoatAvatarInto(`.fleet-avatar[data-boat-id="${b.id}"]`,b.photoId)});
|
||||
}
|
||||
|
||||
async function loadBoatAvatarInto(selector,photoId){
|
||||
if(!photoId)return;
|
||||
try{
|
||||
const item=await dbGet(photoId);
|
||||
if(!item)return;
|
||||
const url=getMediaUrl(item);
|
||||
document.querySelectorAll(selector).forEach(el=>{
|
||||
el.innerHTML=`<img src="${url}" alt="">`;
|
||||
});
|
||||
}catch(e){}
|
||||
}
|
||||
|
||||
function setActiveBoatAndClose(id){
|
||||
|
|
@ -2311,6 +2434,13 @@ function openBoatEditor(id){
|
|||
const chainEl=document.getElementById('boat-edit-chain');
|
||||
chainEl.value=b.chainTotal==null?'':lenToDisplay(b.chainTotal).toFixed(0);
|
||||
document.getElementById('boat-edit-year').value=b.year||'';
|
||||
// Novos campos
|
||||
document.getElementById('boat-edit-engine-hours-initial').value=b.engineHoursInitial||'';
|
||||
document.getElementById('boat-edit-registered-at').value=b.registeredAt||(isNew?new Date().toISOString().slice(0,10):'');
|
||||
document.getElementById('boat-edit-registration').value=b.registrationNumber||'';
|
||||
document.getElementById('boat-edit-notes').value=b.notes||'';
|
||||
document.getElementById('boat-edit-photo-id').value=b.photoId||'';
|
||||
renderBoatPhotoPreview(b.photoId);
|
||||
// Atualizar labels de unidade
|
||||
const unitLabel=`(${lengthUnit()})`;
|
||||
['boat-len-unit','boat-beam-unit','boat-draft-unit','boat-chain-unit'].forEach(id=>{
|
||||
|
|
@ -2323,9 +2453,81 @@ function openBoatEditor(id){
|
|||
setTimeout(()=>document.getElementById('boat-edit-name').focus(),100);
|
||||
}
|
||||
|
||||
function saveBoatFromForm(ev){
|
||||
async function renderBoatPhotoPreview(photoId){
|
||||
const wrap=document.getElementById('boat-photo-preview');
|
||||
const clearBtn=document.getElementById('boat-photo-clear');
|
||||
if(!wrap)return;
|
||||
if(!photoId){
|
||||
const t=BOAT_TYPES[document.getElementById('boat-edit-type').value]||BOAT_TYPES.sailing;
|
||||
wrap.innerHTML=`<span class="boat-photo-placeholder">${t.icon}</span>`;
|
||||
if(clearBtn)clearBtn.style.display='none';
|
||||
return;
|
||||
}
|
||||
try{
|
||||
const item=await dbGet(photoId);
|
||||
if(!item){wrap.innerHTML='<span class="boat-photo-placeholder">⛵</span>';return}
|
||||
const url=getMediaUrl(item);
|
||||
wrap.innerHTML=`<img src="${url}" alt="foto da embarcação">`;
|
||||
if(clearBtn)clearBtn.style.display='block';
|
||||
}catch(e){
|
||||
wrap.innerHTML='<span class="boat-photo-placeholder">⛵</span>';
|
||||
}
|
||||
}
|
||||
|
||||
async function handleBoatPhoto(ev){
|
||||
const file=ev.target.files?.[0];
|
||||
ev.target.value='';
|
||||
if(!file)return;
|
||||
if(!file.type.startsWith('image/')){toast('Selecione uma imagem');return}
|
||||
// Comprime/redimensiona pra max 1280px (economia de storage e sync)
|
||||
try{
|
||||
const blob=await resizeImage(file,1280,0.85);
|
||||
const oldId=document.getElementById('boat-edit-photo-id').value;
|
||||
if(oldId){await dbDelete(oldId).catch(()=>{})}
|
||||
const newId='boat-photo-'+uid();
|
||||
await dbPut({id:newId,kind:'photo',blob,mime:blob.type||'image/jpeg',parentId:document.getElementById('boat-edit-id').value||'pending',parentType:'boat',createdAt:Date.now()});
|
||||
document.getElementById('boat-edit-photo-id').value=newId;
|
||||
await renderBoatPhotoPreview(newId);
|
||||
toast('Foto carregada');
|
||||
}catch(e){
|
||||
console.warn('photo upload failed',e);
|
||||
toast('Erro ao carregar foto');
|
||||
}
|
||||
}
|
||||
|
||||
async function clearBoatPhoto(){
|
||||
const oldId=document.getElementById('boat-edit-photo-id').value;
|
||||
if(oldId){await dbDelete(oldId).catch(()=>{})}
|
||||
document.getElementById('boat-edit-photo-id').value='';
|
||||
renderBoatPhotoPreview(null);
|
||||
}
|
||||
|
||||
// Resize/compress image via canvas (mantém proporção, max maxDim, jpeg quality)
|
||||
function resizeImage(file,maxDim=1280,quality=0.85){
|
||||
return new Promise((resolve,reject)=>{
|
||||
const img=new Image();
|
||||
img.onload=()=>{
|
||||
try{
|
||||
let{width,height}=img;
|
||||
if(width>height){if(width>maxDim){height=Math.round(height*maxDim/width);width=maxDim}}
|
||||
else{if(height>maxDim){width=Math.round(width*maxDim/height);height=maxDim}}
|
||||
const canvas=document.createElement('canvas');
|
||||
canvas.width=width;canvas.height=height;
|
||||
const ctx=canvas.getContext('2d');
|
||||
ctx.drawImage(img,0,0,width,height);
|
||||
canvas.toBlob(b=>b?resolve(b):reject(new Error('toBlob null')),'image/jpeg',quality);
|
||||
}catch(e){reject(e)}
|
||||
finally{URL.revokeObjectURL(img.src)}
|
||||
};
|
||||
img.onerror=()=>{URL.revokeObjectURL(img.src);reject(new Error('image load error'))};
|
||||
img.src=URL.createObjectURL(file);
|
||||
});
|
||||
}
|
||||
|
||||
async function saveBoatFromForm(ev){
|
||||
ev.preventDefault();
|
||||
const id=document.getElementById('boat-edit-id').value;
|
||||
const photoId=document.getElementById('boat-edit-photo-id').value||null;
|
||||
const data={
|
||||
name:document.getElementById('boat-edit-name').value.trim(),
|
||||
type:document.getElementById('boat-edit-type').value,
|
||||
|
|
@ -2335,10 +2537,26 @@ function saveBoatFromForm(ev){
|
|||
draft:document.getElementById('boat-edit-draft').value,
|
||||
chainTotal:document.getElementById('boat-edit-chain').value,
|
||||
year:document.getElementById('boat-edit-year').value,
|
||||
photoId,
|
||||
engineHoursInitial:document.getElementById('boat-edit-engine-hours-initial').value,
|
||||
registeredAt:document.getElementById('boat-edit-registered-at').value,
|
||||
registrationNumber:document.getElementById('boat-edit-registration').value.trim(),
|
||||
notes:document.getElementById('boat-edit-notes').value.trim(),
|
||||
};
|
||||
if(!data.name){toast('Informe o nome da embarcação');return}
|
||||
let boatId=id;
|
||||
if(id){updateBoat(id,data);toast('Embarcação atualizada')}
|
||||
else{const b=addBoat(data);state.activeBoatId=b.id;saveState();toast('Embarcação adicionada')}
|
||||
else{const b=addBoat(data);state.activeBoatId=b.id;boatId=b.id;saveState();toast('Embarcação adicionada')}
|
||||
// Re-vincular parentId da foto (nova embarcação tinha 'pending' como parentId)
|
||||
if(photoId){
|
||||
try{
|
||||
const item=await dbGet(photoId);
|
||||
if(item&&item.parentId!==boatId){
|
||||
item.parentId=boatId;
|
||||
await dbPut(item);
|
||||
}
|
||||
}catch(e){}
|
||||
}
|
||||
closeModal('boat-editor-modal');
|
||||
bindHeader();
|
||||
renderAll();
|
||||
|
|
|
|||
|
|
@ -7,8 +7,8 @@ android {
|
|||
applicationId "br.com.pontualtech.shivao"
|
||||
minSdkVersion rootProject.ext.minSdkVersion
|
||||
targetSdkVersion rootProject.ext.targetSdkVersion
|
||||
versionCode 4
|
||||
versionName "1.4.0"
|
||||
versionCode 5
|
||||
versionName "1.4.1"
|
||||
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
|
||||
aaptOptions {
|
||||
// Files and dirs to omit from the packaged assets dir, modified to accommodate modern web apps.
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"name": "shivao-mobile",
|
||||
"version": "1.4.0",
|
||||
"version": "1.4.1",
|
||||
"description": "Shivao app nativo (Capacitor wrapper Android/iOS)",
|
||||
"main": "index.js",
|
||||
"type": "module",
|
||||
|
|
|
|||
|
|
@ -528,6 +528,34 @@ header{
|
|||
.fleet-units-toggle button.active{background:var(--brass);color:var(--bg-paper)}
|
||||
.fleet-units-toggle button:hover:not(.active){background:var(--bg-aged)}
|
||||
|
||||
/* === Boat Photo === */
|
||||
.boat-photo-row{display:flex;gap:14px;align-items:flex-start}
|
||||
.boat-photo-preview{
|
||||
width:96px;height:96px;flex-shrink:0;
|
||||
background:var(--bg-canvas);border:1px solid var(--rule);
|
||||
display:flex;align-items:center;justify-content:center;
|
||||
overflow:hidden;position:relative;
|
||||
}
|
||||
.boat-photo-preview img{width:100%;height:100%;object-fit:cover;display:block}
|
||||
.boat-photo-placeholder{font-size:36px;opacity:.4}
|
||||
.boat-photo-actions{flex:1;display:flex;flex-direction:column;justify-content:center}
|
||||
|
||||
/* Avatares pequenos (lista frota + header) */
|
||||
.fleet-avatar{
|
||||
width:44px;height:44px;flex-shrink:0;
|
||||
background:var(--bg-aged);border:1px solid var(--rule);
|
||||
display:flex;align-items:center;justify-content:center;
|
||||
overflow:hidden;font-size:22px;
|
||||
}
|
||||
.fleet-avatar img{width:100%;height:100%;object-fit:cover}
|
||||
.boat-header-avatar{
|
||||
width:34px;height:34px;border-radius:50%;flex-shrink:0;
|
||||
background:rgba(250,242,221,.12);border:1.5px solid rgba(250,242,221,.3);
|
||||
overflow:hidden;display:flex;align-items:center;justify-content:center;
|
||||
font-size:16px;color:rgba(250,242,221,.6);
|
||||
}
|
||||
.boat-header-avatar img{width:100%;height:100%;object-fit:cover}
|
||||
|
||||
/* === Anchor Calculator === */
|
||||
.anchor-calc{
|
||||
background:var(--bg-canvas);border:1px solid var(--rule);
|
||||
|
|
@ -1338,6 +1366,7 @@ header{
|
|||
</g>
|
||||
<text x="50" y="14" text-anchor="middle" font-family="JetBrains Mono,monospace" font-size="6" fill="currentColor" font-weight="600">N</text>
|
||||
</svg>
|
||||
<div class="boat-header-avatar" id="boat-header-avatar" onclick="openFleetManager()" style="cursor:pointer" title="Gerenciar frota">⛵</div>
|
||||
<div class="boat-info">
|
||||
<div class="boat-tagline">Diário de Bordo · Logbook</div>
|
||||
<button type="button" class="boat-name boat-selector" id="boat-selector" onclick="openFleetManager()" title="Gerenciar frota">
|
||||
|
|
@ -1879,6 +1908,26 @@ Hora: {HORA}</textarea>
|
|||
<div class="modal-body">
|
||||
<form id="boat-editor-form" onsubmit="saveBoatFromForm(event)">
|
||||
<input type="hidden" id="boat-edit-id">
|
||||
<input type="hidden" id="boat-edit-photo-id">
|
||||
<div class="field">
|
||||
<label class="field-label">Foto da embarcação</label>
|
||||
<div class="boat-photo-row">
|
||||
<div class="boat-photo-preview" id="boat-photo-preview">
|
||||
<span class="boat-photo-placeholder">⛵</span>
|
||||
</div>
|
||||
<div class="boat-photo-actions">
|
||||
<label class="btn btn-sm btn-block" style="cursor:pointer;display:block">
|
||||
📷 Tirar foto
|
||||
<input type="file" accept="image/*" capture="environment" onchange="handleBoatPhoto(event)" style="display:none">
|
||||
</label>
|
||||
<label class="btn btn-sm btn-block" style="cursor:pointer;display:block;margin-top:6px">
|
||||
🖼 Da galeria
|
||||
<input type="file" accept="image/*" onchange="handleBoatPhoto(event)" style="display:none">
|
||||
</label>
|
||||
<button type="button" class="btn btn-sm btn-danger btn-block" onclick="clearBoatPhoto()" id="boat-photo-clear" style="margin-top:6px;display:none">Remover foto</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="field"><label class="field-label">Nome</label>
|
||||
<input type="text" id="boat-edit-name" required maxlength="40" placeholder="ex: Shivao, Maré Alta">
|
||||
</div>
|
||||
|
|
@ -1910,9 +1959,26 @@ Hora: {HORA}</textarea>
|
|||
<input type="number" id="boat-edit-chain" step="1" min="0" placeholder="total disponível">
|
||||
</div>
|
||||
</div>
|
||||
<div class="field-row">
|
||||
<div class="field"><label class="field-label">Ano (opcional)</label>
|
||||
<input type="number" id="boat-edit-year" min="1900" max="2100" placeholder="ex: 2018">
|
||||
</div>
|
||||
<div class="field"><label class="field-label">Data de cadastro</label>
|
||||
<input type="date" id="boat-edit-registered-at">
|
||||
</div>
|
||||
</div>
|
||||
<div class="field-row">
|
||||
<div class="field"><label class="field-label">Horímetro inicial</label>
|
||||
<input type="number" id="boat-edit-engine-hours-initial" step="0.1" min="0" placeholder="ex: 1240.5">
|
||||
<div class="field-hint" style="margin-top:4px">Horas do motor no dia que começou a registrar.</div>
|
||||
</div>
|
||||
<div class="field"><label class="field-label">Matrícula / TIE</label>
|
||||
<input type="text" id="boat-edit-registration" maxlength="32" placeholder="ex: SP-2348-CT">
|
||||
</div>
|
||||
</div>
|
||||
<div class="field"><label class="field-label">Notas (opcional)</label>
|
||||
<textarea id="boat-edit-notes" maxlength="500" rows="2" placeholder="Marina, observações, manutenção pendente..."></textarea>
|
||||
</div>
|
||||
<div class="field-hint">⚓ Comprimento + calado são usados pra calcular raio de giro no fundeio.</div>
|
||||
</form>
|
||||
</div>
|
||||
|
|
@ -1965,11 +2031,24 @@ function migrateBoatsSchema(){
|
|||
length:null,beam:null,draft:null,
|
||||
chainTotal:null,
|
||||
year:null,
|
||||
photoId:null,
|
||||
engineHoursInitial:null,
|
||||
registeredAt:null,
|
||||
registrationNumber:'',
|
||||
notes:'',
|
||||
createdAt:Date.now(),
|
||||
};
|
||||
state.boats.push(b);
|
||||
state.activeBoatId=b.id;
|
||||
}
|
||||
// Garante campos novos em barcos existentes (forward-compat)
|
||||
state.boats.forEach(b=>{
|
||||
if(!('photoId' in b))b.photoId=null;
|
||||
if(!('engineHoursInitial' in b))b.engineHoursInitial=null;
|
||||
if(!('registeredAt' in b))b.registeredAt=null;
|
||||
if(!('registrationNumber' in b))b.registrationNumber='';
|
||||
if(!('notes' in b))b.notes='';
|
||||
});
|
||||
if(!state.activeBoatId&&state.boats[0])state.activeBoatId=state.boats[0].id;
|
||||
if(!state.units)state.units='metric';
|
||||
}
|
||||
|
|
@ -1996,6 +2075,11 @@ function addBoat(data){
|
|||
draft:lenFromInput(data.draft),
|
||||
chainTotal:lenFromInput(data.chainTotal),
|
||||
year:data.year?parseInt(data.year):null,
|
||||
photoId:data.photoId||null,
|
||||
engineHoursInitial:data.engineHoursInitial?parseFloat(data.engineHoursInitial):null,
|
||||
registeredAt:data.registeredAt||null,
|
||||
registrationNumber:data.registrationNumber||'',
|
||||
notes:data.notes||'',
|
||||
createdAt:Date.now(),
|
||||
};
|
||||
state.boats.push(b);
|
||||
|
|
@ -2015,11 +2099,19 @@ function updateBoat(id,data){
|
|||
if('draft' in data)b.draft=lenFromInput(data.draft);
|
||||
if('chainTotal' in data)b.chainTotal=lenFromInput(data.chainTotal);
|
||||
if('year' in data)b.year=data.year?parseInt(data.year):null;
|
||||
if('photoId' in data)b.photoId=data.photoId;
|
||||
if('engineHoursInitial' in data)b.engineHoursInitial=data.engineHoursInitial?parseFloat(data.engineHoursInitial):null;
|
||||
if('registeredAt' in data)b.registeredAt=data.registeredAt||null;
|
||||
if('registrationNumber' in data)b.registrationNumber=data.registrationNumber||'';
|
||||
if('notes' in data)b.notes=data.notes||'';
|
||||
saveState();
|
||||
}
|
||||
|
||||
function removeBoat(id){
|
||||
if(state.boats.length<=1){toast('Mantenha pelo menos 1 embarcação');return}
|
||||
const b=state.boats.find(x=>x.id===id);
|
||||
// Limpa foto do IndexedDB se houver
|
||||
if(b?.photoId){dbDelete(b.photoId).catch(()=>{})}
|
||||
state.boats=state.boats.filter(b=>b.id!==id);
|
||||
if(state.activeBoatId===id)state.activeBoatId=state.boats[0]?.id||null;
|
||||
saveState();
|
||||
|
|
@ -2243,16 +2335,33 @@ function renderGPSBanner(){
|
|||
function bindHeader(){
|
||||
const nameEl=document.getElementById('boat-name-display');
|
||||
const metaEl=document.getElementById('boat-model-display');
|
||||
const avatarEl=document.getElementById('boat-header-avatar');
|
||||
if(!nameEl||!metaEl)return;
|
||||
const b=activeBoat();
|
||||
if(!b){nameEl.textContent='Sem embarcação';metaEl.textContent='Toque para adicionar';return}
|
||||
if(!b){
|
||||
nameEl.textContent='Sem embarcação';
|
||||
metaEl.textContent='Toque para adicionar';
|
||||
if(avatarEl)avatarEl.innerHTML='⛵';
|
||||
return;
|
||||
}
|
||||
nameEl.textContent=b.name||'Embarcação';
|
||||
const t=BOAT_TYPES[b.type]||BOAT_TYPES.sailing;
|
||||
const parts=[`${t.icon} ${t.label}`];
|
||||
if(b.model)parts.push(b.model);
|
||||
if(b.length)parts.push(fmtLen(b.length,1));
|
||||
metaEl.textContent=parts.join(' · ');
|
||||
// sync legacy state.boat para compat com qualquer código antigo que ainda use
|
||||
// Avatar: foto se houver, senão ícone do tipo
|
||||
if(avatarEl){
|
||||
avatarEl.innerHTML=t.icon;
|
||||
if(b.photoId){
|
||||
dbGet(b.photoId).then(item=>{
|
||||
if(item&&activeBoat()?.id===b.id){
|
||||
avatarEl.innerHTML=`<img src="${getMediaUrl(item)}" alt="">`;
|
||||
}
|
||||
}).catch(()=>{});
|
||||
}
|
||||
}
|
||||
// sync legacy state.boat para compat
|
||||
state.boat={name:b.name,model:b.model};
|
||||
}
|
||||
|
||||
|
|
@ -2277,7 +2386,7 @@ function renderFleetList(){
|
|||
if(b.model)meta.push(b.model);
|
||||
if(b.length)meta.push(fmtLen(b.length,1));
|
||||
return `<div class="fleet-item ${isActive?'active':''}" onclick="setActiveBoatAndClose('${b.id}')">
|
||||
<div class="fleet-icon">${t.icon}</div>
|
||||
<div class="fleet-avatar" data-boat-id="${b.id}">${t.icon}</div>
|
||||
<div class="fleet-info">
|
||||
<div class="fleet-name">${escapeHtml(b.name)}</div>
|
||||
<div class="fleet-meta">${meta.map(escapeHtml).join(' · ')}</div>
|
||||
|
|
@ -2286,6 +2395,20 @@ function renderFleetList(){
|
|||
<button type="button" class="fleet-edit-icon" onclick="event.stopPropagation();openBoatEditor('${b.id}')" title="Editar">✎</button>
|
||||
</div>`;
|
||||
}).join('');
|
||||
// Carrega fotos async — não bloqueia a render
|
||||
state.boats.forEach(b=>{if(b.photoId)loadBoatAvatarInto(`.fleet-avatar[data-boat-id="${b.id}"]`,b.photoId)});
|
||||
}
|
||||
|
||||
async function loadBoatAvatarInto(selector,photoId){
|
||||
if(!photoId)return;
|
||||
try{
|
||||
const item=await dbGet(photoId);
|
||||
if(!item)return;
|
||||
const url=getMediaUrl(item);
|
||||
document.querySelectorAll(selector).forEach(el=>{
|
||||
el.innerHTML=`<img src="${url}" alt="">`;
|
||||
});
|
||||
}catch(e){}
|
||||
}
|
||||
|
||||
function setActiveBoatAndClose(id){
|
||||
|
|
@ -2311,6 +2434,13 @@ function openBoatEditor(id){
|
|||
const chainEl=document.getElementById('boat-edit-chain');
|
||||
chainEl.value=b.chainTotal==null?'':lenToDisplay(b.chainTotal).toFixed(0);
|
||||
document.getElementById('boat-edit-year').value=b.year||'';
|
||||
// Novos campos
|
||||
document.getElementById('boat-edit-engine-hours-initial').value=b.engineHoursInitial||'';
|
||||
document.getElementById('boat-edit-registered-at').value=b.registeredAt||(isNew?new Date().toISOString().slice(0,10):'');
|
||||
document.getElementById('boat-edit-registration').value=b.registrationNumber||'';
|
||||
document.getElementById('boat-edit-notes').value=b.notes||'';
|
||||
document.getElementById('boat-edit-photo-id').value=b.photoId||'';
|
||||
renderBoatPhotoPreview(b.photoId);
|
||||
// Atualizar labels de unidade
|
||||
const unitLabel=`(${lengthUnit()})`;
|
||||
['boat-len-unit','boat-beam-unit','boat-draft-unit','boat-chain-unit'].forEach(id=>{
|
||||
|
|
@ -2323,9 +2453,81 @@ function openBoatEditor(id){
|
|||
setTimeout(()=>document.getElementById('boat-edit-name').focus(),100);
|
||||
}
|
||||
|
||||
function saveBoatFromForm(ev){
|
||||
async function renderBoatPhotoPreview(photoId){
|
||||
const wrap=document.getElementById('boat-photo-preview');
|
||||
const clearBtn=document.getElementById('boat-photo-clear');
|
||||
if(!wrap)return;
|
||||
if(!photoId){
|
||||
const t=BOAT_TYPES[document.getElementById('boat-edit-type').value]||BOAT_TYPES.sailing;
|
||||
wrap.innerHTML=`<span class="boat-photo-placeholder">${t.icon}</span>`;
|
||||
if(clearBtn)clearBtn.style.display='none';
|
||||
return;
|
||||
}
|
||||
try{
|
||||
const item=await dbGet(photoId);
|
||||
if(!item){wrap.innerHTML='<span class="boat-photo-placeholder">⛵</span>';return}
|
||||
const url=getMediaUrl(item);
|
||||
wrap.innerHTML=`<img src="${url}" alt="foto da embarcação">`;
|
||||
if(clearBtn)clearBtn.style.display='block';
|
||||
}catch(e){
|
||||
wrap.innerHTML='<span class="boat-photo-placeholder">⛵</span>';
|
||||
}
|
||||
}
|
||||
|
||||
async function handleBoatPhoto(ev){
|
||||
const file=ev.target.files?.[0];
|
||||
ev.target.value='';
|
||||
if(!file)return;
|
||||
if(!file.type.startsWith('image/')){toast('Selecione uma imagem');return}
|
||||
// Comprime/redimensiona pra max 1280px (economia de storage e sync)
|
||||
try{
|
||||
const blob=await resizeImage(file,1280,0.85);
|
||||
const oldId=document.getElementById('boat-edit-photo-id').value;
|
||||
if(oldId){await dbDelete(oldId).catch(()=>{})}
|
||||
const newId='boat-photo-'+uid();
|
||||
await dbPut({id:newId,kind:'photo',blob,mime:blob.type||'image/jpeg',parentId:document.getElementById('boat-edit-id').value||'pending',parentType:'boat',createdAt:Date.now()});
|
||||
document.getElementById('boat-edit-photo-id').value=newId;
|
||||
await renderBoatPhotoPreview(newId);
|
||||
toast('Foto carregada');
|
||||
}catch(e){
|
||||
console.warn('photo upload failed',e);
|
||||
toast('Erro ao carregar foto');
|
||||
}
|
||||
}
|
||||
|
||||
async function clearBoatPhoto(){
|
||||
const oldId=document.getElementById('boat-edit-photo-id').value;
|
||||
if(oldId){await dbDelete(oldId).catch(()=>{})}
|
||||
document.getElementById('boat-edit-photo-id').value='';
|
||||
renderBoatPhotoPreview(null);
|
||||
}
|
||||
|
||||
// Resize/compress image via canvas (mantém proporção, max maxDim, jpeg quality)
|
||||
function resizeImage(file,maxDim=1280,quality=0.85){
|
||||
return new Promise((resolve,reject)=>{
|
||||
const img=new Image();
|
||||
img.onload=()=>{
|
||||
try{
|
||||
let{width,height}=img;
|
||||
if(width>height){if(width>maxDim){height=Math.round(height*maxDim/width);width=maxDim}}
|
||||
else{if(height>maxDim){width=Math.round(width*maxDim/height);height=maxDim}}
|
||||
const canvas=document.createElement('canvas');
|
||||
canvas.width=width;canvas.height=height;
|
||||
const ctx=canvas.getContext('2d');
|
||||
ctx.drawImage(img,0,0,width,height);
|
||||
canvas.toBlob(b=>b?resolve(b):reject(new Error('toBlob null')),'image/jpeg',quality);
|
||||
}catch(e){reject(e)}
|
||||
finally{URL.revokeObjectURL(img.src)}
|
||||
};
|
||||
img.onerror=()=>{URL.revokeObjectURL(img.src);reject(new Error('image load error'))};
|
||||
img.src=URL.createObjectURL(file);
|
||||
});
|
||||
}
|
||||
|
||||
async function saveBoatFromForm(ev){
|
||||
ev.preventDefault();
|
||||
const id=document.getElementById('boat-edit-id').value;
|
||||
const photoId=document.getElementById('boat-edit-photo-id').value||null;
|
||||
const data={
|
||||
name:document.getElementById('boat-edit-name').value.trim(),
|
||||
type:document.getElementById('boat-edit-type').value,
|
||||
|
|
@ -2335,10 +2537,26 @@ function saveBoatFromForm(ev){
|
|||
draft:document.getElementById('boat-edit-draft').value,
|
||||
chainTotal:document.getElementById('boat-edit-chain').value,
|
||||
year:document.getElementById('boat-edit-year').value,
|
||||
photoId,
|
||||
engineHoursInitial:document.getElementById('boat-edit-engine-hours-initial').value,
|
||||
registeredAt:document.getElementById('boat-edit-registered-at').value,
|
||||
registrationNumber:document.getElementById('boat-edit-registration').value.trim(),
|
||||
notes:document.getElementById('boat-edit-notes').value.trim(),
|
||||
};
|
||||
if(!data.name){toast('Informe o nome da embarcação');return}
|
||||
let boatId=id;
|
||||
if(id){updateBoat(id,data);toast('Embarcação atualizada')}
|
||||
else{const b=addBoat(data);state.activeBoatId=b.id;saveState();toast('Embarcação adicionada')}
|
||||
else{const b=addBoat(data);state.activeBoatId=b.id;boatId=b.id;saveState();toast('Embarcação adicionada')}
|
||||
// Re-vincular parentId da foto (nova embarcação tinha 'pending' como parentId)
|
||||
if(photoId){
|
||||
try{
|
||||
const item=await dbGet(photoId);
|
||||
if(item&&item.parentId!==boatId){
|
||||
item.parentId=boatId;
|
||||
await dbPut(item);
|
||||
}
|
||||
}catch(e){}
|
||||
}
|
||||
closeModal('boat-editor-modal');
|
||||
bindHeader();
|
||||
renderAll();
|
||||
|
|
|
|||
|
|
@ -264,7 +264,7 @@ app.get('/.well-known/assetlinks.json', (req, res) => {
|
|||
});
|
||||
|
||||
// Atalho: /apk redireciona pra última APK release no Forgejo
|
||||
const LATEST_APK_URL = 'https://git.pontualtech.work/karlao/shivao-projeto/releases/download/v1.4.0/Shivao-v1.4.0.apk';
|
||||
const LATEST_APK_URL = 'https://git.pontualtech.work/karlao/shivao-projeto/releases/download/v1.4.1/Shivao-v1.4.1.apk';
|
||||
app.get('/apk', (req, res) => res.redirect(302, LATEST_APK_URL));
|
||||
|
||||
// Página A4 imprimível com QR Code + instruções (cola no barco/marina)
|
||||
|
|
|
|||
Loading…
Reference in a new issue