From 5833efcc4859556dfa6576f59be6d27c07c5a525 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?PontualTech=20/=20Karl=C3=A3o?= Date: Tue, 28 Apr 2026 06:37:10 -0300 Subject: [PATCH] =?UTF-8?q?feat(fleet):=20foto=20da=20embarca=C3=A7=C3=A3o?= =?UTF-8?q?=20+=20hor=C3=ADmetro=20+=20cadastro=20+=20matr=C3=ADcula=20v1.?= =?UTF-8?q?4.1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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) --- app/diario-bordo.html | 232 +++++++++++++++++++++++++++++++- mobile/android/app/build.gradle | 4 +- mobile/package.json | 2 +- server/public/index.html | 232 +++++++++++++++++++++++++++++++- server/src/index.js | 2 +- 5 files changed, 454 insertions(+), 18 deletions(-) diff --git a/app/diario-bordo.html b/app/diario-bordo.html index 0aff4db..1da2e13 100644 --- a/app/diario-bordo.html +++ b/app/diario-bordo.html @@ -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{ N +
Diário de Bordo · Logbook
+
+ +
@@ -1910,8 +1959,25 @@ Hora: {HORA} -
- +
+
+ +
+
+ +
+
+
+
+ +
Horas do motor no dia que começou a registrar.
+
+
+ +
+
+
+
⚓ Comprimento + calado são usados pra calcular raio de giro no fundeio.
@@ -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=``; + } + }).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 `
-
${t.icon}
+
${t.icon}
${escapeHtml(b.name)}
${meta.map(escapeHtml).join(' · ')}
@@ -2286,6 +2395,20 @@ function renderFleetList(){
`; }).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=``; + }); + }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=`${t.icon}`; + if(clearBtn)clearBtn.style.display='none'; + return; + } + try{ + const item=await dbGet(photoId); + if(!item){wrap.innerHTML='';return} + const url=getMediaUrl(item); + wrap.innerHTML=`foto da embarcação`; + if(clearBtn)clearBtn.style.display='block'; + }catch(e){ + wrap.innerHTML=''; + } +} + +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(); diff --git a/mobile/android/app/build.gradle b/mobile/android/app/build.gradle index 83b7add..d22f079 100644 --- a/mobile/android/app/build.gradle +++ b/mobile/android/app/build.gradle @@ -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. diff --git a/mobile/package.json b/mobile/package.json index a5af7b0..03183e9 100644 --- a/mobile/package.json +++ b/mobile/package.json @@ -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", diff --git a/server/public/index.html b/server/public/index.html index 0aff4db..1da2e13 100644 --- a/server/public/index.html +++ b/server/public/index.html @@ -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{ N +
Diário de Bordo · Logbook
+
+
+
@@ -1910,8 +1959,25 @@ Hora: {HORA} -
- +
+
+ +
+
+ +
+
+
+
+ +
Horas do motor no dia que começou a registrar.
+
+
+ +
+
+
+
⚓ Comprimento + calado são usados pra calcular raio de giro no fundeio.
@@ -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=``; + } + }).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 `
-
${t.icon}
+
${t.icon}
${escapeHtml(b.name)}
${meta.map(escapeHtml).join(' · ')}
@@ -2286,6 +2395,20 @@ function renderFleetList(){
`; }).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=``; + }); + }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=`${t.icon}`; + if(clearBtn)clearBtn.style.display='none'; + return; + } + try{ + const item=await dbGet(photoId); + if(!item){wrap.innerHTML='';return} + const url=getMediaUrl(item); + wrap.innerHTML=`foto da embarcação`; + if(clearBtn)clearBtn.style.display='block'; + }catch(e){ + wrap.innerHTML=''; + } +} + +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(); diff --git a/server/src/index.js b/server/src/index.js index 92f8bae..b3a5d1f 100644 --- a/server/src/index.js +++ b/server/src/index.js @@ -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)