“Thank you so much for taking the time to help me. I really appreciate your dedication, and I’m happy to wait while you work on this. I’m sure your solution will be very valuable.”
I read that webviewextra don't permit geolocation in a powerfull program that search all informations on the web, but I don't sure of this information..
I have a working method for AI2 with the webviewer, just need to tidy things up a bit. Will be ready some time tomorrow.
Thank you
OK, sorry to have taken so long, been doing much testing, and with the release of nb202, ensuring that everything was still behaving.
This issue regarding geolocation and webviewextra: you are correct to a degree, they work together just fine when using companion in some situations, and always when calling a local html file, occasionally when using a remote file. There appears to be a permission or setting being blocked, what, where and when I am yet to resolve! But in general, they won't work together with a remote html.
The workaround for this is to grab the device's coordinates locally, and then feed this into the remote html file (easily done if you have control of the remote html file. I use a second webviewer to run a simple get coordinates script, this then passes out the fetched coordinates using the webviewstring to the app. The app can then pass these values into the remote html (the one with the file upload and webviewextra) and everything works as expected. There are other methods to perform this, it should be possible to run a javascript to inject the values when you do not have control over the remote html file.
We also have to handle whether the remote html is being accessed on the appinventor app, on a device, or being accessed using a computer browser or other method.
I have tested with companion and compiled app on Pixel 8a Android 16, with AppInventor running at nb202.
Here first are the blocks in the test app:
A timer is used to set the FineLocation permission, the hidden webviewer running the local html gets the coordinates and passes them to the visible webviewer's webviewstring. Javascript sets these values into the upload form.
Here is the local html file:
gl1.html
<!DOCTYPE html>
<html>
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no">
<head>
<title>gl1</title>
</head>
<body>
<p>Get your coordinates...</p>
<p id="geo"></p>
<p id="lat"></p>
<p id="lon"></p>
<script>
var options = {
enableHighAccuracy: true,
timeout: 5000,
maximumAge: 0
};
if (navigator.geolocation) {
navigator.geolocation.getCurrentPosition(showPosition, error, options);
} else {
document.getElementById("geo").innerHTML("Geolocation is not supported by this browser");
if( window.AppInventor ){
var msg = "Geolocation is not supported by this browser";
window.AppInventor.setWebViewString(msg);
}
};
function showPosition(position) {
document.getElementById("lat").innerHTML = "lat: " + position.coords.latitude;
document.getElementById("lon").innerHTML = "lon: " + position.coords.longitude;
if( window.AppInventor ){
var msg = "[" + position.coords.latitude + "," + position.coords.longitude + "]"
window.AppInventor.setWebViewString(msg);
}
};
function error(err) {
document.getElementById("geo").innerHTML = 'ERROR(' + err.code + '): ' + err.message;
};
</script>
</body>
</html>
and here is the remote html file used for the file upload:
locandup.html
<!DOCTYPE html>
<html>
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no">
<head>
<title>locandup</title>
</head>
<body>
<form action="/upload.php" method="post" enctype="multipart/form-data">
Select image to upload:
<input type="file" name="fileToUpload" id="fileToUpload"><br><br>
<input type="text" id="txtLat" name="txtLat" value="empty"><br><br>
<input type="text" id="txtLon" name="txtLon" value="empty"><br><br>
<input type="submit" value="Upload Image" name="submit">
</form>
<script>
if (window.AppInventor) {
var coords = JSON.parse(window.AppInventor.getWebViewString());
document.getElementById("txtLat").value = coords[0];
document.getElementById("txtLon").value = coords[1];
}
else {
var options = {
enableHighAccuracy: true,
timeout: 5000,
maximumAge: 0
};
if (navigator.geolocation) {
navigator.geolocation.getCurrentPosition(showPosition, error, options);
} else {
alert("Geolocation is not supported by this browser");
};
function showPosition(position) {
document.getElementById("txtLat").value = position.coords.latitude;
document.getElementById("txtLon").value = position.coords.longitude;
};
function error(err) {
alert( 'ERROR(' + err.code + '): ' + err.message);
};
}
</script>
</body>
</html>
You should be able to see that if appinventor is not present, then I use the html5 geolocation in this page
and a screenshot of the app in action:
Once a file has been selected, it is passed to a php file on the remote server (in same directory as the remote html file) This makes some checks (is it an image etc.), then stores the image file. At the same time the filename, lat and lon are appended to a text file (what you do with the values is down to your requirements)
Here is the php file:
upload.php
<?php
$target_dir = "images/";
$target_file = $target_dir . basename($_FILES["fileToUpload"]["name"]);
$uploadOk = 1;
$imageFileType = strtolower(pathinfo($target_file,PATHINFO_EXTENSION));
$lat = $_REQUEST['txtLat'];
$lon = $_REQUEST['txtLon'];
// Check if image file is a actual image or fake image
if(isset($_POST["submit"])) {
$check = getimagesize($_FILES["fileToUpload"]["tmp_name"]);
if($check !== false) {
// echo "File is an image - " . $check["mime"] . ".";
$uploadOk = 1;
} else {
echo "File is not an image.";
$uploadOk = 0;
}
}
// Check if file already exists
if (file_exists($target_file)) {
echo "Sorry, file already exists.";
$uploadOk = 0;
}
// Check file size
//if ($_FILES["fileToUpload"]["size"] > 500000) {
// echo "Sorry, your file is too large.";
// $uploadOk = 0;
//}
// Allow certain file formats
if($imageFileType != "jpg" && $imageFileType != "png" && $imageFileType != "jpeg" && $imageFileType != "gif" ) {
echo "Sorry, only JPG, JPEG, PNG & GIF files are allowed.";
$uploadOk = 0;
}
// Check if $uploadOk is set to 0 by an error
if ($uploadOk == 0) {
echo "Sorry, your file was not uploaded.";
// if everything is ok, try to upload file
} else {
if (move_uploaded_file($_FILES["fileToUpload"]["tmp_name"], $target_file)) {
// Get the lat and lon values and append thse to a text file with the filename
$myfile = fopen("images/datafile.txt", "a+") or die("Unable to open file!");
$txt = basename( $_FILES["fileToUpload"]["name"]) . " Lat: " . $lat . " Lon: " . $lon . "\n";
fwrite($myfile, $txt);
fclose($myfile);
echo "The file ". htmlspecialchars( basename( $_FILES["fileToUpload"]["name"])). " has been uploaded.";
} else {
echo "Sorry, there was an error uploading your file.";
}
}
?>
I have commented out the file size check...
Uploaded image files are stored in a directory called images, along with the datafile.txt
Images folder
datafile.text contents
ai2metricrat.png Lat: 53.1416528 Lon: -2.327943
pinkSquare.png Lat: 53.1416528 Lon: -2.327943
star.png Lat: 53.11908 Lon: -2.3703808
orangeSquare.png Lat: 53.1416406 Lon: -2.3279549
purpleSquare.png Lat: 53.1416388 Lon: -2.3279316
redSquare.png Lat: 53.1416504 Lon: -2.3279644
ros.png Lat: 52.1416504 Lon: -1.3279644
framed_1738083675927_img.jpg Lat: 52.1416504 Lon: -1.3279644
You should be able to build in the required html and php to your existing files ?
This a bit of a workaround, but it works, and at present the best I can offer. You can of course go back to CustomWebView which has all the required features built-in, you will just have to find the method that handles both geolocation and upload!
Ok, thank you very much, now I try your solution.
I believe I have now fixed webviewextra, so no need for the second webviewer/running local html.
Please try this version:
uk.co.metricrat.webviewextraV2.15GeoLoc2.aix (25.1 KB)
You do need to request FINE_LOCATION permission.
Hi Sir, I uploaded gl1.html into my App Inventor project and I’m loading it in a hidden WebViewer to get the device location.
In the app I pass the coordinates to my profile page with this link: <li><a href="https://www.vitamarinaweb.com/indexb.php?lat=LAT&lon=LON">Profile</a></li>
In my profile page (indexb.php
), where the position is saved, I added this script at the end:<script> (function(){ // If no query params, try to get coords from localStorage const q = new URLSearchParams(location.search); if(!q.get('lat') || !q.get('lon')){ const lat = localStorage.getItem('app_lat'); const lon = localStorage.getItem('app_lon'); if(lat && lon){ // reload profile page with lat/lon location.replace(location.pathname + '?lat=' + lat + '&lon=' + lon); } } })(); </script>
This way, in the homepage index.html don't take coordinates and the profile page indexb.php is opened without the lat/lon
parameters, it can still retrieve them from localStorage and reload itself with the correct coordinates. Here are my blocks, thank you
in advance, Ok now take the coordinates is working! I don't remembered to add the url in the webviewer2 in settings.. thank you very much for now!
Ok now this script give me the lat and long
<script>
(function(){
// Se la query non c'è, prendo le coord salvate dall'app
const q=new URLSearchParams(location.search);
if(!q.get('lat') || !q.get('lon')){
const lat=localStorage.getItem('app_lat');
const lon=localStorage.getItem('app_lon');
if(lat && lon){
// ricarico il profilo con i parametri
location.replace(location.pathname + '?lat=' + lat + '&lon=' + lon);
}
}
})();
</script>`
but when I used this 2 script to take the position say me permission denied script to save the position
<script>
// Token CSRF generato dal server (core.php)
window.CSRF_TOKEN = '<?= csrf_token() ?>';
document.addEventListener('DOMContentLoaded', function(){
const saveBtn = document.getElementById('save-loc');
const pubBox = document.getElementById('loc-public');
const noteEl = document.getElementById('loc-note');
const guardian = document.getElementById('guardian-ok');
if (!saveBtn) return;
// ========== NEW: ripristina lo stato del toggle da localStorage ==========
try {
const pref = localStorage.getItem('locDefaultPublic');
if (pubBox && pref !== null) pubBox.checked = (pref === '1');
} catch(e) {}
// ========== NEW: salva in localStorage quando cambi il toggle ==========
if (pubBox) {
pubBox.addEventListener('change', () => {
try { localStorage.setItem('locDefaultPublic', pubBox.checked ? '1' : '0'); } catch(e) {}
});
}
// ---- gestione messaggi vicino al bottone ----
function getOut() {
let el = document.getElementById('save-loc-msg');
if (!el) {
el = document.createElement('small');
el.id = 'save-loc-msg';
el.className = 'loc-status';
saveBtn.insertAdjacentElement('afterend', el);
}
return el;
}
function showStatus(msg, ok = true) {
const out = getOut();
out.textContent = msg;
out.classList.add('show');
out.classList.toggle('error', !ok);
clearTimeout(out._t);
out._t = setTimeout(()=> out.classList.remove('show'), 3000);
}
saveBtn.addEventListener('click', function(){
const is_public = pubBox && pubBox.checked ? 1 : 0;
const note = noteEl ? noteEl.value.trim() : '';
// Se vuole rendere PUBBLICA, deve confermare 14+ o consenso genitore
if (is_public && guardian && !guardian.checked) {
alert('Per pubblicare la località serve la conferma: "almeno 14 anni oppure consenso del genitore/tutore".');
return;
}
if (!('geolocation' in navigator)) {
showStatus('Geolocalizzazione non supportata', false);
return;
}
showStatus('Acquisisco la posizione…', true);
navigator.geolocation.getCurrentPosition(
pos => {
const { latitude: lat, longitude: lng, accuracy } = pos.coords;
fetch('save_location.php', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-CSRF-Token': window.CSRF_TOKEN
},
credentials: 'same-origin',
body: JSON.stringify({ lat, lng, accuracy, note, is_public })
})
.then(r => r.ok ? r.json() : Promise.reject(new Error('HTTP ' + r.status)))
.then(j => {
if (j && j.success) {
showStatus('Posizione salvata ✔', true);
// ========== NEW: allinea anche la preferenza salvata ==========
try { localStorage.setItem('locDefaultPublic', String(is_public)); } catch(e) {}
} else {
showStatus('Errore: ' + ((j && j.message) || 'operazione non riuscita'), false);
}
})
.catch(() => { showStatus('Errore di rete', false); });
},
err => {
const msg = {1:'Permesso negato',2:'Posizione non disponibile',3:'Timeout'}[err.code] || 'Errore';
showStatus(msg, false);
},
{ enableHighAccuracy: true, timeout: 10000, maximumAge: 0 }
);
});
});
</script>
and code to share the position
<script>
/* ========= CSRF helper ========= */
function getCsrf() {
return (typeof window.CSRF_TOKEN === 'string' && window.CSRF_TOKEN.length)
? window.CSRF_TOKEN
: '';
}
/* ========= LocalStorage helpers ========= */
const LS_KEY = (id) => `postLoc:${id}`;
function loadLoc(postId){
try { return JSON.parse(localStorage.getItem(LS_KEY(postId)) || 'null'); }
catch { return null; }
}
function saveLoc(postId, data){
try { localStorage.setItem(LS_KEY(postId), JSON.stringify(data)); } catch {}
}
/* ========= Utils ========= */
function coarse(n, digits = 2){ // ~1km
const f = Math.pow(10, digits);
return Math.round(Number(n) * f) / f;
}
function renderChip($wrap, lat, lng, placeName){
const url = `https://www.openstreetmap.org/?mlat=${lat}&mlon=${lng}#map=14/${lat}/${lng}`;
let $chip = $wrap.prev('.post-loc-chip');
if ($chip.length === 0) {
$chip = $('<a/>', { class: 'post-loc-chip', target: '_blank', rel: 'noopener' })
.insertBefore($wrap);
}
$chip.attr('href', url).text(placeName ? `📍 ${placeName}` : '📍 Posizione');
return $chip;
}
function reverseName(lat, lng, onName){
fetch(`https://nominatim.openstreetmap.org/reverse?format=json&lat=${lat}&lon=${lng}&zoom=10&addressdetails=1`)
.then(r => r.json()).then(d => { if (d?.display_name) onName(d.display_name); })
.catch(()=>{});
}
function setLocStatus($wrap, msg, ok=true){
// cerca un <small class="loc-status"> dentro la riga .loc-actions.
// se non c’è, lo crea in coda alla riga (accanto al toggle)
let $s = $wrap.find('.loc-status');
if ($s.length === 0){
$s = $('<small class="loc-status" role="status" aria-live="polite"/>')
.appendTo($wrap);
}
$s.text(msg).toggleClass('error', !ok).addClass('show');
clearTimeout($s.data('t'));
$s.data('t', setTimeout(()=> $s.removeClass('show'), 3000));
}
/* ========= Re-hydrate chip da localStorage all'avvio ========= */
function hydrateAllChips(){
$('.loc-actions').each(function(){
const $wrap = $(this);
const postId = Number($wrap.data('postId'));
const $chk = $wrap.find('.make-public');
const $lab = $wrap.find('.loc-toggle-text');
if ($lab.length && $chk.length) $lab.text($chk.is(':checked') ? 'Pubblica' : 'Privata');
const loc = loadLoc(postId);
if (!loc || typeof loc.lat !== 'number' || typeof loc.lng !== 'number') return;
const isPublic = Number(loc.is_public) === 1;
const latShow = isPublic ? coarse(loc.lat) : loc.lat;
const lngShow = isPublic ? coarse(loc.lng) : loc.lng;
const $chip = renderChip($wrap, latShow, lngShow, loc.place_name || '');
if (!loc.place_name) {
reverseName(latShow, lngShow, (name)=>{
$chip.text(`📍 ${name}`);
// salva anche il nome per i prossimi refresh
saveLoc(postId, { ...loc, place_name: name });
});
}
});
}
/* ===========================
📍 Condividi posizione
=========================== */
/* ===========================
📍 Condividi posizione
=========================== */
$(document).on('click', '.share-location-btn', function () {
const $btn = $(this);
const $wrap = $btn.closest('.loc-actions');
const postId = Number($wrap.data('postId'));
const isPublic = $wrap.find('.make-public').prop('checked') ? 1 : 0;
if (!postId) { alert('ID post mancante'); return; }
if (!navigator.geolocation) {
alert('Geolocalizzazione non supportata');
setLocStatus($wrap, 'Geolocalizzazione non supportata', false); // <-- AGGIUNTA
return;
}
const oldTxt = $btn.text();
$btn.prop('disabled', true).text('Salvo posizione…');
navigator.geolocation.getCurrentPosition(async pos => {
try {
const lat = pos.coords.latitude;
const lng = pos.coords.longitude;
const res = await fetch('save_post_location.php', {
method: 'POST',
headers: { 'Content-Type': 'application/json', 'X-CSRF-Token': getCsrf() },
credentials: 'same-origin',
body: JSON.stringify({ post_id: postId, lat, lng, is_public: isPublic })
});
const j = await res.json();
if (!res.ok || !j || j.success !== true) throw new Error(j?.message || 'Errore salvataggio');
const latShow = isPublic ? (j.coarse_lat ?? coarse(lat)) : lat;
const lngShow = isPublic ? (j.coarse_lng ?? coarse(lng)) : lng;
const $chip = renderChip($wrap, latShow, lngShow, '');
reverseName(latShow, lngShow, (name)=>{
$chip.text(`📍 ${name}`);
saveLoc(postId, { lat, lng, is_public: isPublic, place_name: name, ts: Date.now() });
});
saveLoc(postId, { lat, lng, is_public: isPublic, place_name: '', ts: Date.now() });
$btn.text('Posizione condivisa ✔');
setLocStatus($wrap, 'Posizione salvata ✔', true); // <-- AGGIUNTA
} catch (err) {
alert(err.message || 'Errore imprevisto');
setLocStatus($wrap, 'Errore: ' + (err.message || 'imprevisto'), false); // <-- AGGIUNTA
$btn.text(oldTxt);
} finally {
setTimeout(() => $btn.prop('disabled', false).text('📍 Condividi posizione'), 1200);
}
}, err => {
const msg = {1:'Permesso negato',2:'Posizione non disponibile',3:'Timeout'}[err.code] || 'Errore';
alert('Non riesco a ottenere la posizione: ' + msg);
setLocStatus($wrap, msg, false); // <-- AGGIUNTA
$btn.prop('disabled', false).text(oldTxt);
}, { enableHighAccuracy: false, timeout: 10000, maximumAge: 60000 });
});
/* ===========================
Toggle Pubblica / Privata
=========================== */
let toggling = false;
$(document).on('change', '.make-public', function () {
if (toggling) return;
const $chk = $(this);
const $wrap = $chk.closest('.loc-actions');
const postId = Number($wrap.data('postId'));
const $lab = $wrap.find('.loc-toggle-text');
const nextVal = $chk.is(':checked') ? 1 : 0;
if (!postId) { alert('ID post mancante'); $chk.prop('checked', !nextVal); return; }
if (nextVal === 1) {
const ok = confirm("Confermando dichiari di avere almeno 14 anni o il consenso di un genitore/tutore e accetti l'informativa privacy.");
if (!ok) { $chk.prop('checked', false); return; }
}
if ($lab.length) $lab.text(nextVal ? 'Pubblica' : 'Privata');
toggling = true;
$.ajax({
url: 'update_post_privacy.php',
type: 'POST',
dataType: 'json',
contentType: 'application/x-www-form-urlencoded; charset=UTF-8',
headers: { 'X-CSRF-Token': getCsrf() },
xhrFields: { withCredentials: true },
data: { post_id: String(postId), is_public: String(nextVal) }
})
.done(function (j) {
if (!j || j.success !== true) {
alert(j?.message || 'Impossibile salvare la preferenza');
$chk.prop('checked', !nextVal);
if ($lab.length) $lab.text($chk.is(':checked') ? 'Pubblica' : 'Privata');
return;
}
// Aggiorna il chip localmente in base al nuovo stato
const loc = loadLoc(postId);
if (loc && typeof loc.lat === 'number' && typeof loc.lng === 'number') {
const isPublic = nextVal === 1;
const latShow = isPublic ? coarse(loc.lat) : loc.lat;
const lngShow = isPublic ? coarse(loc.lng) : loc.lng;
const $chip = renderChip($wrap, latShow, lngShow, loc.place_name || '');
if (!loc.place_name) {
reverseName(latShow, lngShow, (name)=>{
$chip.text(`📍 ${name}`);
saveLoc(postId, { ...loc, is_public: nextVal, place_name: name, ts: Date.now() });
});
}
// salva nuovo flag anche senza nome
saveLoc(postId, { ...loc, is_public: nextVal, ts: Date.now() });
}
})
.fail(function () {
alert('Errore di rete nel salvataggio della preferenza');
$chk.prop('checked', !nextVal);
if ($lab.length) $lab.text($chk.is(':checked') ? 'Pubblica' : 'Privata');
})
.always(function(){ toggling = false; });
});
/* ========= Avvio ========= */
$(hydrateAllChips);
</script>
Can you help me please?
Have you tried the updated extension I provided above?
no now I try, Ok now It's working fine, thank you very much for all TIMAI2 you are a genius!