feat: prepare Pro licensing and Android API 36

This commit is contained in:
livio committed 2026-09-28 09:50:58 +02:00
1 parent b600412285
commit 11a91863a3
25 files changed
+2447 -62

No files matched your search

+3 -1
View File
@@ -1,7 +1,9 @@
@inject AppLocalizationService L10n
<details class="menu-help">
<summary>
<span aria-hidden="true">?</span>
<strong>Guida rapida</strong>
<strong>@L10n.T("menu.quickHelp")</strong>
</summary>
<div class="menu-help-content">
<p>@Introduction</p>
+254 -26
View File
@@ -10,9 +10,11 @@
@inject MapExportService MapExportService
@inject CloudSyncService CloudSyncService
@inject AppUpdateService AppUpdateService
@inject AppLocalizationService L10n
@inject EntitlementService Entitlements
@implements IAsyncDisposable
<PageTitle>LME Mappe Catastali</PageTitle>
<PageTitle>LME Mappe Catastali Pro</PageTitle>
<div class="map-app">
<section class="map-stage @(ActiveTab == "search" ? "search-active" : null)">
@@ -40,14 +42,14 @@
@if (NavigationMenuOpen)
{
<nav class="map-navigation-menu" aria-label="Menu principale">
<button @onclick='() => SelectTab("map")'>Mappa</button>
<button @onclick='() => SelectTab("search")'>Cerca località</button>
<button @onclick='() => SelectTab("cadastre")'>Ricerca catastale</button>
<button @onclick='() => SelectTab("data")'>Dati e disegni</button>
<button @onclick='() => SelectTab("projects")'>Progetti</button>
<button @onclick='() => SelectTab("save")'>Esporta</button>
<button @onclick='() => SelectTab("tools")'>Strumenti avanzati</button>
<button @onclick='() => SelectTab("settings")'>Impostazioni</button>
<button @onclick='() => SelectTab("map")'>@L("menu.map")</button>
<button @onclick='() => SelectTab("search")'>@L("menu.search")</button>
<button @onclick='() => SelectTab("cadastre")'>@L("menu.cadastre")</button>
<button @onclick='() => SelectTab("data")'>@L("menu.data")</button>
<button @onclick='() => SelectTab("projects")'>@L("menu.projects")</button>
<button @onclick='() => SelectTab("save")'>@L("menu.export")</button>
<button @onclick='() => SelectTab("tools")'>@L("menu.tools")</button>
<button @onclick='() => SelectTab("settings")'>@L("menu.settings")</button>
<div class="menu-credits"><strong>LME Mappe Catastali</strong><span>Autore: Livio Merola</span><a href="mailto:info@liviomerola.net">info@liviomerola.net</a><span>Mappe: Esri, OpenStreetMap, CARTO, OpenTopoMap. Catasto: Agenzia delle Entrate CC BY 4.0.</span></div>
</nav>
}
@@ -307,9 +309,13 @@
<span>Traccia GPS registrata</span>
</label>
</div>
<button class="primary-command" @onclick="ExportMapFile" disabled="@(ExportBusy || string.IsNullOrWhiteSpace(ExportFileName))">
<button class="primary-command" @onclick="ExportMapFile" disabled="@(ExportBusy || string.IsNullOrWhiteSpace(ExportFileName) || !CanUse(AppFeatures.Export))">
@(ExportBusy ? "Preparazione file..." : "Esporta e salva file")
</button>
@if (!CanUse(AppFeatures.Export))
{
<small class="field-help">@ProUpgradeText</small>
}
<small class="field-help">KML e KMZ sono compatibili con Google Earth. Gli sfondi satellitari e il WMS non vengono incorporati nel file vettoriale.</small>
</section>
@@ -329,7 +335,11 @@
<input type="checkbox" @bind="IncludeCadastralParcels" @bind:after="SaveFlagPreferences" />
<span>Includi le particelle catastali evidenziate</span>
</label>
<button class="primary-command" @onclick="SaveProject" disabled="@(ProjectBusy || string.IsNullOrWhiteSpace(NewProjectName))">Salva progetto</button>
<button class="primary-command" @onclick="SaveProject" disabled="@(ProjectBusy || string.IsNullOrWhiteSpace(NewProjectName) || !CanUse(AppFeatures.ProjectArchive))">Salva progetto</button>
@if (!CanUse(AppFeatures.ProjectArchive))
{
<small class="field-help">@ProUpgradeText</small>
}
</section>
<section class="panel-section">
@@ -353,12 +363,12 @@
<input @bind="ProjectFolder" placeholder="Es. Cantieri 2026" maxlength="60" />
</label>
<div class="project-command-stack">
<button class="secondary-command" @onclick="UpdateProjectFolder" disabled="@(ProjectBusy || string.IsNullOrWhiteSpace(SelectedProjectId))">Sposta</button>
<button class="secondary-command" @onclick="RenameProject" disabled="@(ProjectBusy || string.IsNullOrWhiteSpace(SelectedProjectId) || SelectedProject?.IsReadOnly == true)">Rinomina</button>
<button class="secondary-command" @onclick="DuplicateProject" disabled="@(ProjectBusy || string.IsNullOrWhiteSpace(SelectedProjectId))">Duplica</button>
<button class="secondary-command" @onclick="ToggleProjectArchive" disabled="@(ProjectBusy || string.IsNullOrWhiteSpace(SelectedProjectId))">@(SelectedProject?.IsArchived == true ? "Ripristina" : "Archivia")</button>
<button class="secondary-command" @onclick='() => ShareProject(false)' disabled="@(ProjectBusy || string.IsNullOrWhiteSpace(SelectedProjectId))">Condividi modificabile</button>
<button class="secondary-command" @onclick='() => ShareProject(true)' disabled="@(ProjectBusy || string.IsNullOrWhiteSpace(SelectedProjectId))">Condividi in sola lettura</button>
<button class="secondary-command" @onclick="UpdateProjectFolder" disabled="@(ProjectBusy || string.IsNullOrWhiteSpace(SelectedProjectId) || !CanUse(AppFeatures.ProjectArchive))">Sposta</button>
<button class="secondary-command" @onclick="RenameProject" disabled="@(ProjectBusy || string.IsNullOrWhiteSpace(SelectedProjectId) || SelectedProject?.IsReadOnly == true || !CanUse(AppFeatures.ProjectArchive))">Rinomina</button>
<button class="secondary-command" @onclick="DuplicateProject" disabled="@(ProjectBusy || string.IsNullOrWhiteSpace(SelectedProjectId) || !CanUse(AppFeatures.ProjectArchive))">Duplica</button>
<button class="secondary-command" @onclick="ToggleProjectArchive" disabled="@(ProjectBusy || string.IsNullOrWhiteSpace(SelectedProjectId) || !CanUse(AppFeatures.ProjectArchive))">@(SelectedProject?.IsArchived == true ? "Ripristina" : "Archivia")</button>
<button class="secondary-command" @onclick='() => ShareProject(false)' disabled="@(ProjectBusy || string.IsNullOrWhiteSpace(SelectedProjectId) || !CanUse(AppFeatures.ProjectArchive))">Condividi modificabile</button>
<button class="secondary-command" @onclick='() => ShareProject(true)' disabled="@(ProjectBusy || string.IsNullOrWhiteSpace(SelectedProjectId) || !CanUse(AppFeatures.ProjectArchive))">Condividi in sola lettura</button>
</div>
@if (SelectedProject?.IsReadOnly == true)
{
@@ -392,10 +402,10 @@
<section class="panel-section">
<h2>Backup e trasferimento</h2>
<div class="button-row">
<button class="secondary-command" @onclick="ShareFullBackup">Crea backup</button>
<button class="secondary-command" @onclick="RestoreBackup">Ripristina backup</button>
<button class="secondary-command" @onclick="ShareFullBackup" disabled="@(!CanUse(AppFeatures.ProjectArchive))">Crea backup</button>
<button class="secondary-command" @onclick="RestoreBackup" disabled="@(!CanUse(AppFeatures.ProjectArchive))">Ripristina backup</button>
</div>
<button class="secondary-command" @onclick="ImportProject">Importa progetto condiviso</button>
<button class="secondary-command" @onclick="ImportProject" disabled="@(!CanUse(AppFeatures.ProjectArchive))">Importa progetto condiviso</button>
</section>
<section class="panel-section">
@@ -443,7 +453,7 @@
Livelli di zoom aggiuntivi
<input type="number" min="0" max="4" @bind="OfflineExtraZoomLevels" />
</label>
<button class="primary-command" @onclick="DownloadOfflineArea" disabled="@OfflineBusy">@(OfflineBusy ? "Download in corso..." : "Scarica area visibile")</button>
<button class="primary-command" @onclick="DownloadOfflineArea" disabled="@(OfflineBusy || !CanUse(AppFeatures.OfflineMaps))">@(OfflineBusy ? "Download in corso..." : "Scarica area visibile")</button>
<div class="button-row">
<button class="secondary-command" @onclick="UseOfflineMap">Usa mappe offline</button>
<button class="danger-command" @onclick="ClearOfflineArea">Svuota archivio</button>
@@ -456,6 +466,66 @@
else if (ActiveTab == "settings")
{
<section class="panel-content">
<section class="panel-section">
<h2>@L("settings.localization.title")</h2>
<label>
@L("settings.language")
<select @bind="LanguagePreference" @bind:after="ApplyLanguagePreference">
<option value="auto">@L("settings.language.auto")</option>
<option value="it">@L("settings.language.it")</option>
<option value="en">@L("settings.language.en")</option>
</select>
</label>
<label>
@L("settings.measurements")
<select @bind="MeasurementPreference" @bind:after="ApplyMeasurementPreference">
<option value="auto">@L("settings.measurements.auto")</option>
<option value="metric">@L("settings.measurements.metric")</option>
<option value="imperial">@L("settings.measurements.imperial")</option>
</select>
</label>
</section>
<section class="panel-section">
<h2>@L("settings.pro.title")</h2>
<div class="service-note">
<strong>@L("settings.pro.status"): @Entitlements.PlanLabel</strong>
<p>@L("settings.pro.note")</p>
<p>@L("settings.pro.offline")</p>
<div class="button-row">
<button class="primary-command" @onclick='() => SubscribeGooglePro(EntitlementService.MonthlyBasePlanId)' disabled="@BillingBusy">@L("settings.pro.monthly")</button>
<button class="primary-command" @onclick='() => SubscribeGooglePro(EntitlementService.YearlyBasePlanId)' disabled="@BillingBusy">@L("settings.pro.yearly")</button>
</div>
<div class="button-row">
<button class="secondary-command" @onclick="RestorePurchases" disabled="@BillingBusy">@L("settings.pro.restore")</button>
<button class="secondary-command" @onclick="OpenSubscriptionManagement">@L("settings.pro.manage")</button>
</div>
</div>
<label>
@L("settings.pro.manual.code")
<textarea class="license-code-input" @bind="ManualLicenseCode" rows="5" spellcheck="false" autocomplete="off" placeholder="LME1..."></textarea>
</label>
<small class="field-help">@L("settings.pro.manual.help")</small>
<div class="button-row">
<button class="primary-command" @onclick="ActivateManualLicense" disabled="@string.IsNullOrWhiteSpace(ManualLicenseCode)">@L("settings.pro.manual.activate")</button>
<button class="secondary-command" @onclick="RemoveManualLicense" disabled="@(Entitlements.ManualLicense is null)">@L("settings.pro.manual.remove")</button>
</div>
@if (Entitlements.ManualLicense is { } manualLicense)
{
<div class="service-note">
<strong>@manualLicense.HolderName</strong>
<p>@manualLicense.Email</p>
<p>@L("settings.pro.manual.expires"): @manualLicense.ExpiresAtUtc.ToLocalTime().ToString("g", L10n.Culture)</p>
<p>@(Entitlements.HasManualProAccess ? L("settings.pro.manual.valid") : L("settings.pro.manual.expired"))</p>
</div>
}
@if (!string.IsNullOrWhiteSpace(ManualLicenseMessage))
{
<small class="field-help">@ManualLicenseMessage</small>
}
</section>
<section class="panel-section">
<h2>Mappa e sfondo</h2>
<label>
@@ -546,8 +616,8 @@
</label>
<button class="secondary-command" @onclick="SaveWebDavSettings">Salva configurazione</button>
<div class="button-row">
<button class="primary-command" @onclick="SyncUpload" disabled="@SyncBusy">Invia al cloud</button>
<button class="secondary-command" @onclick="SyncDownload" disabled="@SyncBusy">Scarica dal cloud</button>
<button class="primary-command" @onclick="SyncUpload" disabled="@(SyncBusy || !CanUse(AppFeatures.CloudSync))">Invia al cloud</button>
<button class="secondary-command" @onclick="SyncDownload" disabled="@(SyncBusy || !CanUse(AppFeatures.CloudSync))">Scarica dal cloud</button>
</div>
<small class="field-help">@SyncStatus</small>
</section>
@@ -573,7 +643,7 @@
<span>Mostra punto e cerchio GPS</span>
</label>
<label class="toggle-line project-save-option">
<input type="checkbox" @bind="TrailEnabled" @bind:after="ApplyTrailOption" />
<input type="checkbox" @bind="TrailEnabled" @bind:after="ApplyTrailOption" disabled="@(!CanUse(AppFeatures.GpsTrail))" />
<span>Registra la traccia mentre cammini</span>
</label>
<div class="button-row">
@@ -644,6 +714,11 @@
private string ProjectArchiveMessage { get; set; } = "Nessun progetto salvato.";
private string ProjectFolder { get; set; } = string.Empty;
private string SelectedVersionId { get; set; } = string.Empty;
private string LanguagePreference { get; set; } = "auto";
private string MeasurementPreference { get; set; } = "auto";
private string ManualLicenseCode { get; set; } = string.Empty;
private string ManualLicenseMessage { get; set; } = string.Empty;
private bool BillingBusy { get; set; }
private string Status { get; set; } = "online";
private string SelectedBaseLayer { get; set; } = "satellite";
private string GoogleMapsApiKey { get; set; } = string.Empty;
@@ -700,6 +775,7 @@
private string UpdateStatus { get; set; } = $"Versione installata: {AppUpdateService.CurrentVersion}";
private string UpdateDownloadUrl { get; set; } = string.Empty;
private string UpdateReleaseNotes { get; set; } = string.Empty;
private string ProUpgradeText => "Funzione inclusa in LME Pro. I dati esistenti restano consultabili.";
private CancellationTokenSource? CatalogCancellation { get; set; }
private CancellationTokenSource? GpsLookupCancellation { get; set; }
private CancellationTokenSource? ProjectAutoSaveCancellation { get; set; }
@@ -727,7 +803,7 @@
"projects" => "Archivio progetti",
"save" => "Salvataggio ed esportazione",
"tools" => "Strumenti avanzati",
"settings" => "Impostazioni",
"settings" => L("panel.settings.title"),
_ => string.Empty
};
@@ -738,7 +814,7 @@
"projects" => "Salva, apri e visualizza i tuoi lavori",
"save" => "Esporta file GIS",
"tools" => "Oggetti, geometrie, coordinate e offline",
"settings" => "Mappa, GPS e servizi ufficiali",
"settings" => L("panel.settings.eyebrow"),
_ => string.Empty
};
@@ -754,6 +830,11 @@
protected override async Task OnInitializedAsync()
{
L10n.Load();
L10n.ApplyCulture();
await Entitlements.LoadAsync();
LanguagePreference = L10n.LanguagePreference;
MeasurementPreference = L10n.MeasurementPreference;
LoadFlagPreferences();
try
@@ -797,6 +878,8 @@
{
MapCallbackReference = DotNetObjectReference.Create(this);
await JS.InvokeVoidAsync("lmeMap.init", MapCallbackReference);
await ApplyLocalizationToMapAsync();
await ApplyEntitlementsToMapAsync();
await ApplyFlagPreferencesToMapAsync();
await ApplyAdvancedOptions();
await CenterOnInitialLocationAsync();
@@ -1418,6 +1501,7 @@
private async Task DuplicateProject()
{
if (!CanUse(AppFeatures.ProjectArchive)) { await ShowProRequiredAsync(); return; }
if (SelectedProject is null) return;
var copy = await ProjectStorageService.DuplicateAsync(SelectedProject.Id);
await RefreshProjectsAsync();
@@ -1429,6 +1513,7 @@
private async Task RenameProject()
{
if (!CanUse(AppFeatures.ProjectArchive)) { await ShowProRequiredAsync(); return; }
if (SelectedProject is not { } project) return;
var requestedName = await JS.InvokeAsync<string?>("lmeMap.promptProjectName", project.Name);
if (string.IsNullOrWhiteSpace(requestedName)
@@ -1463,6 +1548,7 @@
private async Task UpdateProjectFolder()
{
if (!CanUse(AppFeatures.ProjectArchive)) { await ShowProRequiredAsync(); return; }
if (SelectedProject is null) return;
await ProjectStorageService.UpdateOrganizationAsync(SelectedProject.Id, ProjectFolder);
await RefreshProjectsAsync();
@@ -1471,6 +1557,7 @@
private async Task ToggleProjectArchive()
{
if (!CanUse(AppFeatures.ProjectArchive)) { await ShowProRequiredAsync(); return; }
if (SelectedProject is not { } project) return;
await ProjectStorageService.UpdateOrganizationAsync(project.Id, isArchived: !project.IsArchived);
await RefreshProjectsAsync();
@@ -1479,6 +1566,7 @@
private async Task ShareProject(bool readOnly)
{
if (!CanUse(AppFeatures.ProjectArchive)) { await ShowProRequiredAsync(); return; }
if (SelectedProject is null) return;
var path = await ProjectStorageService.CreateProjectPackageAsync(SelectedProject.Id, readOnly);
await Share.Default.RequestAsync(new ShareFileRequest { Title = readOnly ? "Condividi progetto LME in sola lettura" : "Condividi progetto LME modificabile", File = new ShareFile(path, "application/json") });
@@ -1486,12 +1574,14 @@
private async Task ShareFullBackup()
{
if (!CanUse(AppFeatures.ProjectArchive)) { await ShowProRequiredAsync(); return; }
var path = await ProjectStorageService.CreateBackupAsync();
await Share.Default.RequestAsync(new ShareFileRequest { Title = "Backup completo LME Mappe Catastali", File = new ShareFile(path, "application/zip") });
}
private async Task RestoreBackup()
{
if (!CanUse(AppFeatures.ProjectArchive)) { await ShowProRequiredAsync(); return; }
var file = await FilePicker.Default.PickAsync(new PickOptions { PickerTitle = "Scegli backup LME" });
if (file is null) return;
var path = await CopyPickedFileToCacheAsync(file);
@@ -1502,6 +1592,7 @@
private async Task ImportProject()
{
if (!CanUse(AppFeatures.ProjectArchive)) { await ShowProRequiredAsync(); return; }
var file = await FilePicker.Default.PickAsync(new PickOptions { PickerTitle = "Scegli progetto LME" });
if (file is null) return;
var path = await CopyPickedFileToCacheAsync(file);
@@ -1541,6 +1632,12 @@
private async Task ExportMapFile()
{
if (!CanUse(AppFeatures.Export))
{
await ShowProRequiredAsync();
return;
}
if (!ExportImported && !ExportImportedSheets && !ExportCadastral
&& !ExportDrawings && !ExportPoi && !ExportTrail)
{
@@ -1583,6 +1680,12 @@
private async Task SaveProject()
{
if (!CanUse(AppFeatures.ProjectArchive))
{
await ShowProRequiredAsync();
return;
}
var name = NewProjectName.Trim();
if (string.IsNullOrWhiteSpace(name))
{
@@ -1731,6 +1834,12 @@
private async Task DownloadOfflineArea()
{
if (!CanUse(AppFeatures.OfflineMaps))
{
await ShowProRequiredAsync();
return;
}
OfflineBusy = true;
try
{
@@ -1765,6 +1874,12 @@
private async Task SyncUpload()
{
if (!CanUse(AppFeatures.CloudSync))
{
await ShowProRequiredAsync();
return;
}
SyncBusy = true;
try
{
@@ -1779,6 +1894,12 @@
private async Task SyncDownload()
{
if (!CanUse(AppFeatures.CloudSync))
{
await ShowProRequiredAsync();
return;
}
if (!await JS.InvokeAsync<bool>("lmeMap.confirmDiscardChanges", "ripristinare l'archivio cloud")) return;
SyncBusy = true;
try
@@ -1829,8 +1950,115 @@
}
private Task CenterOnGps() => JS.InvokeVoidAsync("lmeMap.centerOnGps").AsTask();
private string L(string key) => L10n.T(key);
private bool CanUse(string feature) => Entitlements.CanUse(feature);
private Task ShowProRequiredAsync() => JS.InvokeVoidAsync("lmeMap.showInfo", ProUpgradeText).AsTask();
private async Task ApplyLanguagePreference()
{
L10n.SetLanguage(LanguagePreference);
LanguagePreference = L10n.LanguagePreference;
await ApplyLocalizationToMapAsync();
}
private async Task ApplyMeasurementPreference()
{
L10n.SetMeasurementSystem(MeasurementPreference);
MeasurementPreference = L10n.MeasurementPreference;
await ApplyLocalizationToMapAsync();
}
private Task ApplyLocalizationToMapAsync() => JS.InvokeVoidAsync("lmeMap.setLocalization", new
{
culture = L10n.Culture.Name,
measurementSystem = L10n.MeasurementSystem,
labels = new
{
measureArea = L("measure.area"),
measureDistance = L("measure.distance"),
measureCircle = L("measure.circle"),
drawRectangle = L("measure.rectangle"),
addPoi = L("measure.poi"),
proRequired = ProUpgradeText
}
}).AsTask();
private Task ApplyEntitlementsToMapAsync() => JS.InvokeVoidAsync("lmeMap.setEntitlements", new
{
advancedGeometry = CanUse(AppFeatures.AdvancedGeometry)
}).AsTask();
private async Task RestorePurchases()
{
BillingBusy = true;
try
{
var result = await Entitlements.RestorePurchasesAsync();
await ApplyEntitlementsToMapAsync();
await JS.InvokeVoidAsync("lmeMap.showInfo", result.Message);
}
finally
{
BillingBusy = false;
}
}
private async Task SubscribeGooglePro(string basePlanId)
{
BillingBusy = true;
try
{
var result = await Entitlements.PurchaseGoogleProAsync(basePlanId);
await ApplyEntitlementsToMapAsync();
await JS.InvokeVoidAsync("lmeMap.showInfo", result.Message);
}
finally
{
BillingBusy = false;
}
}
private async Task ActivateManualLicense()
{
var result = await Entitlements.ActivateManualLicenseAsync(ManualLicenseCode);
ManualLicenseMessage = result.IsValid
? L("settings.pro.manual.activated")
: result.Error ?? L("settings.pro.manual.invalid");
if (result.IsValid)
{
ManualLicenseCode = string.Empty;
}
await ApplyEntitlementsToMapAsync();
await JS.InvokeVoidAsync("lmeMap.showInfo", ManualLicenseMessage);
}
private async Task RemoveManualLicense()
{
await Entitlements.RemoveManualLicenseAsync();
ManualLicenseCode = string.Empty;
ManualLicenseMessage = L("settings.pro.manual.removed");
await ApplyEntitlementsToMapAsync();
await JS.InvokeVoidAsync("lmeMap.showInfo", ManualLicenseMessage);
}
private Task OpenSubscriptionManagement() => Launcher.Default.OpenAsync(new Uri(
"https://play.google.com/store/account/subscriptions"));
private async Task ApplyTrailOption()
{
if (!CanUse(AppFeatures.GpsTrail))
{
TrailEnabled = false;
SaveFlagPreferences();
await JS.InvokeVoidAsync("lmeMap.toggleTrail", false);
await ShowProRequiredAsync();
return;
}
SaveFlagPreferences();
await JS.InvokeVoidAsync("lmeMap.toggleTrail", TrailEnabled);
}