Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
8b9691d692 | ||
|
|
5bac29b0af | ||
|
|
9b94e2d405 | ||
|
|
20434ca273 | ||
|
|
feb39ee2de | ||
|
|
1d578debe2 | ||
|
|
5adc4a6526 | ||
|
|
66e3af043c | ||
|
|
9a632b8d62 | ||
|
|
cb0e838964 | ||
|
|
8954f9aaba | ||
|
|
94843311b4 | ||
|
|
52e5f68da0 | ||
|
|
99b3030180 | ||
|
|
7ca504a70a |
@@ -1,4 +0,0 @@
|
|||||||
ID problema,Descrizione,Stato,Gravità,Punti della storia,Percorso progetto,Tipologia di posizione,PERCORSO,Riga,Colonna,ID evento imprevisto,Collegamento alla Guida,Nome dell'assembly,Versione assembly,Chiave pubblica dell'assembly,Frammento
|
|
||||||
NuGet.0002,È consigliabile eseguire l'aggiornamento del pacchetto NuGet,Active,Potential,1,AutoBidder.csproj,File,AutoBidder.csproj,,,Microsoft.EntityFrameworkCore.Sqlite 8.0.0,,,,,"Microsoft.EntityFrameworkCore.Sqlite, 8.0.0 Raccomandazione: Remove Microsoft.EntityFrameworkCore.Sqlite, and replace with new package Microsoft.EntityFrameworkCore.Sqlite 10.0.5"
|
|
||||||
NuGet.0002,È consigliabile eseguire l'aggiornamento del pacchetto NuGet,Active,Potential,1,AutoBidder.csproj,File,AutoBidder.csproj,,,Microsoft.AspNetCore.Identity.EntityFrameworkCore 8.0.0,,,,,"Microsoft.AspNetCore.Identity.EntityFrameworkCore, 8.0.0 Raccomandazione: Remove Microsoft.AspNetCore.Identity.EntityFrameworkCore, and replace with new package Microsoft.AspNetCore.Identity.EntityFrameworkCore 10.0.5"
|
|
||||||
Project.0002,È necessario modificare i framework di destinazione del progetto,Active,Mandatory,1,AutoBidder.csproj,File,AutoBidder.csproj,,,,,,,,Framework di destinazione corrente: net8.0 Framework di destinazione consigliato: net10.0
|
|
||||||
|
@@ -1,167 +0,0 @@
|
|||||||
{
|
|
||||||
"settings": {
|
|
||||||
"components": {
|
|
||||||
"code": false,
|
|
||||||
"binaries": false
|
|
||||||
},
|
|
||||||
"targetId": "net10.0",
|
|
||||||
"targetDisplayName": ".NETCoreApp,Version=v10.0"
|
|
||||||
},
|
|
||||||
"analysisStartTime": "2026-03-31T16:24:00.9934354Z",
|
|
||||||
"analysisEndTime": "2026-03-31T16:24:01.3926356Z",
|
|
||||||
"privacyModeHelpUrl": "https://go.microsoft.com/fwlink/?linkid=2270980",
|
|
||||||
"stats": {
|
|
||||||
"summary": {
|
|
||||||
"projects": 1,
|
|
||||||
"issues": 2,
|
|
||||||
"incidents": 3,
|
|
||||||
"effort": 3
|
|
||||||
},
|
|
||||||
"charts": {
|
|
||||||
"severity": {
|
|
||||||
"Mandatory": 1,
|
|
||||||
"Optional": 0,
|
|
||||||
"Potential": 2,
|
|
||||||
"Information": 0
|
|
||||||
},
|
|
||||||
"category": {
|
|
||||||
"NuGet": 2,
|
|
||||||
"Project": 1
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"projects": [
|
|
||||||
{
|
|
||||||
"path": "AutoBidder.csproj",
|
|
||||||
"startingProject": true,
|
|
||||||
"issues": 2,
|
|
||||||
"storyPoints": 3,
|
|
||||||
"properties": {
|
|
||||||
"appName": "AutoBidder",
|
|
||||||
"projectKind": "AspNetCore",
|
|
||||||
"frameworks": [
|
|
||||||
"net8.0"
|
|
||||||
],
|
|
||||||
"languages": [
|
|
||||||
"C#"
|
|
||||||
],
|
|
||||||
"tools": [
|
|
||||||
"MSBuild"
|
|
||||||
],
|
|
||||||
"isSdkStyle": true,
|
|
||||||
"numberOfFiles": 76,
|
|
||||||
"numberOfCodeFiles": 50,
|
|
||||||
"linesTotal": 24601,
|
|
||||||
"linesOfCode": 14361,
|
|
||||||
"totalApiScanned": 0,
|
|
||||||
"minLinesOfCodeToChange": 0,
|
|
||||||
"maxLinesOfCodeToChange": 0
|
|
||||||
},
|
|
||||||
"ruleInstances": [
|
|
||||||
{
|
|
||||||
"incidentId": "3d50aa32-1a3a-4f89-8e30-cc70a68af57e",
|
|
||||||
"ruleId": "NuGet.0002",
|
|
||||||
"projectPath": "AutoBidder.csproj",
|
|
||||||
"state": "Active",
|
|
||||||
"location": {
|
|
||||||
"snippetModel": {
|
|
||||||
"unrestricted": "Microsoft.EntityFrameworkCore.Sqlite, 8.0.0\n\nRaccomandazione:\n\nRemove Microsoft.EntityFrameworkCore.Sqlite, and replace with new package Microsoft.EntityFrameworkCore.Sqlite 10.0.5",
|
|
||||||
"protected": "Microsoft.EntityFrameworkCore.Sqlite, 8.0.0\n\nRaccomandazione:\n\nRemove Microsoft.EntityFrameworkCore.Sqlite, and replace with new package Microsoft.EntityFrameworkCore.Sqlite 10.0.5"
|
|
||||||
},
|
|
||||||
"kind": "File",
|
|
||||||
"path": "AutoBidder.csproj",
|
|
||||||
"snippet": "Microsoft.EntityFrameworkCore.Sqlite, 8.0.0\n\nRaccomandazione:\n\nRemove Microsoft.EntityFrameworkCore.Sqlite, and replace with new package Microsoft.EntityFrameworkCore.Sqlite 10.0.5",
|
|
||||||
"protectedSnippet": "Microsoft.EntityFrameworkCore.Sqlite, 8.0.0\n\nRaccomandazione:\n\nRemove Microsoft.EntityFrameworkCore.Sqlite, and replace with new package Microsoft.EntityFrameworkCore.Sqlite 10.0.5",
|
|
||||||
"label": "Microsoft.EntityFrameworkCore.Sqlite 8.0.0",
|
|
||||||
"properties": {
|
|
||||||
"PackageId": "Microsoft.EntityFrameworkCore.Sqlite",
|
|
||||||
"PackageVersion": "8.0.0",
|
|
||||||
"PackageNewVersion": "10.0.5",
|
|
||||||
"PackageReplacements": null
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"incidentId": "1876bbae-2d8e-419c-aedc-f4b0d63ad5ee",
|
|
||||||
"ruleId": "NuGet.0002",
|
|
||||||
"projectPath": "AutoBidder.csproj",
|
|
||||||
"state": "Active",
|
|
||||||
"location": {
|
|
||||||
"snippetModel": {
|
|
||||||
"unrestricted": "Microsoft.AspNetCore.Identity.EntityFrameworkCore, 8.0.0\n\nRaccomandazione:\n\nRemove Microsoft.AspNetCore.Identity.EntityFrameworkCore, and replace with new package Microsoft.AspNetCore.Identity.EntityFrameworkCore 10.0.5",
|
|
||||||
"protected": "Microsoft.AspNetCore.Identity.EntityFrameworkCore, 8.0.0\n\nRaccomandazione:\n\nRemove Microsoft.AspNetCore.Identity.EntityFrameworkCore, and replace with new package Microsoft.AspNetCore.Identity.EntityFrameworkCore 10.0.5"
|
|
||||||
},
|
|
||||||
"kind": "File",
|
|
||||||
"path": "AutoBidder.csproj",
|
|
||||||
"snippet": "Microsoft.AspNetCore.Identity.EntityFrameworkCore, 8.0.0\n\nRaccomandazione:\n\nRemove Microsoft.AspNetCore.Identity.EntityFrameworkCore, and replace with new package Microsoft.AspNetCore.Identity.EntityFrameworkCore 10.0.5",
|
|
||||||
"protectedSnippet": "Microsoft.AspNetCore.Identity.EntityFrameworkCore, 8.0.0\n\nRaccomandazione:\n\nRemove Microsoft.AspNetCore.Identity.EntityFrameworkCore, and replace with new package Microsoft.AspNetCore.Identity.EntityFrameworkCore 10.0.5",
|
|
||||||
"label": "Microsoft.AspNetCore.Identity.EntityFrameworkCore 8.0.0",
|
|
||||||
"properties": {
|
|
||||||
"PackageId": "Microsoft.AspNetCore.Identity.EntityFrameworkCore",
|
|
||||||
"PackageVersion": "8.0.0",
|
|
||||||
"PackageNewVersion": "10.0.5",
|
|
||||||
"PackageReplacements": null
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"incidentId": "ad92ba91-4bd4-46fa-8249-f369bb27e7d6",
|
|
||||||
"ruleId": "Project.0002",
|
|
||||||
"projectPath": "AutoBidder.csproj",
|
|
||||||
"state": "Active",
|
|
||||||
"location": {
|
|
||||||
"snippetModel": {
|
|
||||||
"unrestricted": "Framework di destinazione corrente: net8.0\nFramework di destinazione consigliato: net10.0",
|
|
||||||
"protected": "Framework di destinazione corrente: net8.0\nFramework di destinazione consigliato: net10.0"
|
|
||||||
},
|
|
||||||
"kind": "File",
|
|
||||||
"path": "AutoBidder.csproj",
|
|
||||||
"snippet": "Framework di destinazione corrente: net8.0\nFramework di destinazione consigliato: net10.0",
|
|
||||||
"protectedSnippet": "Framework di destinazione corrente: net8.0\nFramework di destinazione consigliato: net10.0",
|
|
||||||
"properties": {
|
|
||||||
"CurrentTargetFramework": "net8.0",
|
|
||||||
"RecommendedTargetFramework": "net10.0"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"features": []
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"rules": {
|
|
||||||
"NuGet.0002": {
|
|
||||||
"id": "NuGet.0002",
|
|
||||||
"isFeature": false,
|
|
||||||
"description": "Per il framework di destinazione selezionato \u00E8 consigliato l\u0027aggiornamento del pacchetto NuGet.\n\n\u00C8 consigliabile che i pacchetti .NET standard abbiano versioni corrispondenti alla versione di .NET a cui \u00E8 destinato il progetto.\n\nAlcuni altri pacchetti sono anche noti per funzionare meglio con i framework di destinazione selezionati.",
|
|
||||||
"label": "\u00C8 consigliabile eseguire l\u0027aggiornamento del pacchetto NuGet",
|
|
||||||
"severity": "Potential",
|
|
||||||
"effort": 1,
|
|
||||||
"links": [
|
|
||||||
{
|
|
||||||
"url": "https://go.microsoft.com/fwlink/?linkid=2262530",
|
|
||||||
"isCustom": false
|
|
||||||
}
|
|
||||||
]
|
|
||||||
},
|
|
||||||
"Project.0002": {
|
|
||||||
"id": "Project.0002",
|
|
||||||
"isFeature": false,
|
|
||||||
"description": "Il framework di destinazione del progetto devono essere sostituiti con il nuovo framework di destinazione selezionato per questo aggiornamento.\n\nDurante l\u0027aggiornamento, il framework di destinazione verr\u00E0 modificato in base alla piattaforma corrispondente, se applicabile. In alcuni casi il progetto avr\u00E0 pi\u00F9 framework di destinazione dopo l\u0027aggiornamento se prima usava funzionalit\u00E0 che ora dispongono di piattaforme proprie nelle versioni moderne di .NET Framework (Windows, iOS, Android e cos\u00EC via).",
|
|
||||||
"label": "\u00C8 necessario modificare i framework di destinazione del progetto",
|
|
||||||
"severity": "Mandatory",
|
|
||||||
"effort": 1,
|
|
||||||
"links": [
|
|
||||||
{
|
|
||||||
"title": "Panoramica del trasferimento da .NET Framework a .NET",
|
|
||||||
"url": "https://go.microsoft.com/fwlink/?linkid=2265227",
|
|
||||||
"isCustom": false
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"title": "SDK di progetto .NET",
|
|
||||||
"url": "https://go.microsoft.com/fwlink/?linkid=2265226",
|
|
||||||
"isCustom": false
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,136 +0,0 @@
|
|||||||
# Projects and dependencies analysis
|
|
||||||
|
|
||||||
This document provides a comprehensive overview of the projects and their dependencies in the context of upgrading to .NETCoreApp,Version=v10.0.
|
|
||||||
|
|
||||||
## Table of Contents
|
|
||||||
|
|
||||||
- [Executive Summary](#executive-Summary)
|
|
||||||
- [Highlevel Metrics](#highlevel-metrics)
|
|
||||||
- [Projects Compatibility](#projects-compatibility)
|
|
||||||
- [Package Compatibility](#package-compatibility)
|
|
||||||
- [API Compatibility](#api-compatibility)
|
|
||||||
- [Aggregate NuGet packages details](#aggregate-nuget-packages-details)
|
|
||||||
- [Top API Migration Challenges](#top-api-migration-challenges)
|
|
||||||
- [Technologies and Features](#technologies-and-features)
|
|
||||||
- [Most Frequent API Issues](#most-frequent-api-issues)
|
|
||||||
- [Projects Relationship Graph](#projects-relationship-graph)
|
|
||||||
- [Project Details](#project-details)
|
|
||||||
|
|
||||||
- [AutoBidder.csproj](#autobiddercsproj)
|
|
||||||
|
|
||||||
|
|
||||||
## Executive Summary
|
|
||||||
|
|
||||||
### Highlevel Metrics
|
|
||||||
|
|
||||||
| Metric | Count | Status |
|
|
||||||
| :--- | :---: | :--- |
|
|
||||||
| Total Projects | 1 | All require upgrade |
|
|
||||||
| Total NuGet Packages | 3 | 2 need upgrade |
|
|
||||||
| Total Code Files | 50 | |
|
|
||||||
| Total Code Files with Incidents | 1 | |
|
|
||||||
| Total Lines of Code | 14361 | |
|
|
||||||
| Total Number of Issues | 3 | |
|
|
||||||
| Estimated LOC to modify | 0+ | at least 0,0% of codebase |
|
|
||||||
|
|
||||||
### Projects Compatibility
|
|
||||||
|
|
||||||
| Project | Target Framework | Difficulty | Package Issues | API Issues | Est. LOC Impact | Description |
|
|
||||||
| :--- | :---: | :---: | :---: | :---: | :---: | :--- |
|
|
||||||
| [AutoBidder.csproj](#autobiddercsproj) | net8.0 | 🟢 Low | 2 | 0 | | AspNetCore, Sdk Style = True |
|
|
||||||
|
|
||||||
### Package Compatibility
|
|
||||||
|
|
||||||
| Status | Count | Percentage |
|
|
||||||
| :--- | :---: | :---: |
|
|
||||||
| ✅ Compatible | 1 | 33,3% |
|
|
||||||
| ⚠️ Incompatible | 0 | 0,0% |
|
|
||||||
| 🔄 Upgrade Recommended | 2 | 66,7% |
|
|
||||||
| ***Total NuGet Packages*** | ***3*** | ***100%*** |
|
|
||||||
|
|
||||||
### API Compatibility
|
|
||||||
|
|
||||||
| Category | Count | Impact |
|
|
||||||
| :--- | :---: | :--- |
|
|
||||||
| 🔴 Binary Incompatible | 0 | High - Require code changes |
|
|
||||||
| 🟡 Source Incompatible | 0 | Medium - Needs re-compilation and potential conflicting API error fixing |
|
|
||||||
| 🔵 Behavioral change | 0 | Low - Behavioral changes that may require testing at runtime |
|
|
||||||
| ✅ Compatible | 0 | |
|
|
||||||
| ***Total APIs Analyzed*** | ***0*** | |
|
|
||||||
|
|
||||||
## Aggregate NuGet packages details
|
|
||||||
|
|
||||||
| Package | Current Version | Suggested Version | Projects | Description |
|
|
||||||
| :--- | :---: | :---: | :--- | :--- |
|
|
||||||
| Microsoft.AspNetCore.Identity.EntityFrameworkCore | 8.0.0 | 10.0.5 | [AutoBidder.csproj](#autobiddercsproj) | È consigliabile eseguire l'aggiornamento del pacchetto NuGet |
|
|
||||||
| Microsoft.EntityFrameworkCore.Sqlite | 8.0.0 | 10.0.5 | [AutoBidder.csproj](#autobiddercsproj) | È consigliabile eseguire l'aggiornamento del pacchetto NuGet |
|
|
||||||
| Npgsql.EntityFrameworkCore.PostgreSQL | 8.0.0 | | [AutoBidder.csproj](#autobiddercsproj) | ✅Compatible |
|
|
||||||
|
|
||||||
## Top API Migration Challenges
|
|
||||||
|
|
||||||
### Technologies and Features
|
|
||||||
|
|
||||||
| Technology | Issues | Percentage | Migration Path |
|
|
||||||
| :--- | :---: | :---: | :--- |
|
|
||||||
|
|
||||||
### Most Frequent API Issues
|
|
||||||
|
|
||||||
| API | Count | Percentage | Category |
|
|
||||||
| :--- | :---: | :---: | :--- |
|
|
||||||
|
|
||||||
## Projects Relationship Graph
|
|
||||||
|
|
||||||
Legend:
|
|
||||||
📦 SDK-style project
|
|
||||||
⚙️ Classic project
|
|
||||||
|
|
||||||
```mermaid
|
|
||||||
flowchart LR
|
|
||||||
P1["<b>📦 AutoBidder.csproj</b><br/><small>net8.0</small>"]
|
|
||||||
click P1 "#autobiddercsproj"
|
|
||||||
|
|
||||||
```
|
|
||||||
|
|
||||||
## Project Details
|
|
||||||
|
|
||||||
<a id="autobiddercsproj"></a>
|
|
||||||
### AutoBidder.csproj
|
|
||||||
|
|
||||||
#### Project Info
|
|
||||||
|
|
||||||
- **Current Target Framework:** net8.0
|
|
||||||
- **Proposed Target Framework:** net10.0
|
|
||||||
- **SDK-style**: True
|
|
||||||
- **Project Kind:** AspNetCore
|
|
||||||
- **Dependencies**: 0
|
|
||||||
- **Dependants**: 0
|
|
||||||
- **Number of Files**: 76
|
|
||||||
- **Number of Files with Incidents**: 1
|
|
||||||
- **Lines of Code**: 14361
|
|
||||||
- **Estimated LOC to modify**: 0+ (at least 0,0% of the project)
|
|
||||||
|
|
||||||
#### Dependency Graph
|
|
||||||
|
|
||||||
Legend:
|
|
||||||
📦 SDK-style project
|
|
||||||
⚙️ Classic project
|
|
||||||
|
|
||||||
```mermaid
|
|
||||||
flowchart TB
|
|
||||||
subgraph current["AutoBidder.csproj"]
|
|
||||||
MAIN["<b>📦 AutoBidder.csproj</b><br/><small>net8.0</small>"]
|
|
||||||
click MAIN "#autobiddercsproj"
|
|
||||||
end
|
|
||||||
|
|
||||||
```
|
|
||||||
|
|
||||||
### API Compatibility
|
|
||||||
|
|
||||||
| Category | Count | Impact |
|
|
||||||
| :--- | :---: | :--- |
|
|
||||||
| 🔴 Binary Incompatible | 0 | High - Require code changes |
|
|
||||||
| 🟡 Source Incompatible | 0 | Medium - Needs re-compilation and potential conflicting API error fixing |
|
|
||||||
| 🔵 Behavioral change | 0 | Low - Behavioral changes that may require testing at runtime |
|
|
||||||
| ✅ Compatible | 0 | |
|
|
||||||
| ***Total APIs Analyzed*** | ***0*** | |
|
|
||||||
|
|
||||||
@@ -1,69 +0,0 @@
|
|||||||
|
|
||||||
## [2026-03-31 20:58] TASK-001: Verify Prerequisites
|
|
||||||
|
|
||||||
Status: Complete
|
|
||||||
|
|
||||||
- **Verified**:
|
|
||||||
- .NET 10.0 SDK is installed and available
|
|
||||||
- No global.json file present, no conflicts with net10.0
|
|
||||||
- Current branch is `upgrade-to-NET10`
|
|
||||||
|
|
||||||
### Outcome
|
|
||||||
Success - All prerequisites verified successfully.
|
|
||||||
|
|
||||||
|
|
||||||
## [2026-03-31 21:05] TASK-002: Update Target Framework
|
|
||||||
|
|
||||||
Status: Complete
|
|
||||||
|
|
||||||
- **Verified**: AutoBidder.csproj now contains `<TargetFramework>net10.0</TargetFramework>` at line 4
|
|
||||||
- **Files Modified**: Mimante/AutoBidder.csproj
|
|
||||||
- **Code Changes**: Updated TargetFramework from net8.0 to net10.0
|
|
||||||
|
|
||||||
### Outcome
|
|
||||||
Success - Target framework updated to net10.0.
|
|
||||||
|
|
||||||
|
|
||||||
## [2026-03-31 21:10] TASK-003: Update NuGet Packages
|
|
||||||
|
|
||||||
Status: Complete
|
|
||||||
|
|
||||||
- **Verified**:
|
|
||||||
- Microsoft.AspNetCore.Identity.EntityFrameworkCore updated to 10.0.5
|
|
||||||
- Microsoft.EntityFrameworkCore.Sqlite updated to 10.0.5
|
|
||||||
- Npgsql.EntityFrameworkCore.PostgreSQL updated to 10.0.1 (compatible with EF Core 10)
|
|
||||||
- dotnet restore completed with no dependency conflicts
|
|
||||||
- **Files Modified**: Mimante/AutoBidder.csproj
|
|
||||||
- **Code Changes**: Updated 3 PackageReference versions in AutoBidder.csproj
|
|
||||||
|
|
||||||
### Outcome
|
|
||||||
Success - All NuGet packages updated and restored successfully.
|
|
||||||
|
|
||||||
|
|
||||||
## [2026-03-31 21:14] TASK-004: Build and Fix Compilation Errors
|
|
||||||
|
|
||||||
Status: Complete
|
|
||||||
|
|
||||||
- **Verified**: Solution builds with 0 errors on first attempt
|
|
||||||
- **Build Status**: Successful: 0 errors
|
|
||||||
|
|
||||||
### Outcome
|
|
||||||
Success - Solution builds successfully with .NET 10.0 and all updated packages.
|
|
||||||
|
|
||||||
|
|
||||||
## [2026-03-31 21:19] TASK-005: Commit and Final Validation
|
|
||||||
|
|
||||||
Status: Complete
|
|
||||||
|
|
||||||
- **Verified**:
|
|
||||||
- All success criteria from Plan §11 met
|
|
||||||
- AutoBidder.csproj targets net10.0
|
|
||||||
- All 3 NuGet packages updated to target versions
|
|
||||||
- Solution builds with 0 errors
|
|
||||||
- No dependency conflicts
|
|
||||||
- No security vulnerabilities
|
|
||||||
- **Commits**: 0db3c51: "Upgrade to .NET 10.0 — update framework and packages"
|
|
||||||
|
|
||||||
### Outcome
|
|
||||||
Success - All changes committed. Upgrade to .NET 10.0 complete.
|
|
||||||
|
|
||||||
@@ -1,43 +0,0 @@
|
|||||||
# Execution Log — .NET 10.0 Upgrade
|
|
||||||
|
|
||||||
## Summary
|
|
||||||
|
|
||||||
- **Status**: ? Complete
|
|
||||||
- **Branch**: `upgrade-to-NET10`
|
|
||||||
- **Commit**: `0db3c51` — "Upgrade to .NET 10.0 — update framework and packages"
|
|
||||||
- **Total Tasks**: 5/5 completed, 0 failed, 0 skipped
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Task Execution Log
|
|
||||||
|
|
||||||
### TASK-001: Verify Prerequisites ?
|
|
||||||
- .NET 10 SDK: installed and available
|
|
||||||
- global.json: no file found, no conflicts
|
|
||||||
- Branch: `upgrade-to-NET10` confirmed
|
|
||||||
|
|
||||||
### TASK-002: Update Target Framework ?
|
|
||||||
- Updated `AutoBidder.csproj` line 4: `net8.0` ? `net10.0`
|
|
||||||
|
|
||||||
### TASK-003: Update NuGet Packages ?
|
|
||||||
- `Microsoft.AspNetCore.Identity.EntityFrameworkCore`: 8.0.0 ? 10.0.5
|
|
||||||
- `Microsoft.EntityFrameworkCore.Sqlite`: 8.0.0 ? 10.0.5
|
|
||||||
- `Npgsql.EntityFrameworkCore.PostgreSQL`: 8.0.0 ? 10.0.1
|
|
||||||
- `dotnet restore` completed with 0 dependency conflicts
|
|
||||||
|
|
||||||
### TASK-004: Build and Fix Compilation Errors ?
|
|
||||||
- Build succeeded on first attempt with 0 errors
|
|
||||||
- No compilation fixes needed
|
|
||||||
|
|
||||||
### TASK-005: Commit and Final Validation ?
|
|
||||||
- All Plan §11 success criteria verified
|
|
||||||
- Committed: `0db3c51` on `upgrade-to-NET10`
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Files Modified
|
|
||||||
|
|
||||||
- `Mimante/AutoBidder.csproj` — TargetFramework + 3 PackageReference versions
|
|
||||||
- `.github/upgrades/assessment.md` — Created during analysis
|
|
||||||
- `.github/upgrades/plan.md` — Created during planning
|
|
||||||
- `.github/upgrades/tasks.md` — Created and tracked during execution
|
|
||||||
@@ -1,318 +0,0 @@
|
|||||||
# .NET 10.0 Upgrade Plan — AutoBidder Solution
|
|
||||||
|
|
||||||
## Table of Contents
|
|
||||||
|
|
||||||
- [1. Executive Summary](#1-executive-summary)
|
|
||||||
- [2. Migration Strategy](#2-migration-strategy)
|
|
||||||
- [3. Detailed Dependency Analysis](#3-detailed-dependency-analysis)
|
|
||||||
- [4. Project-by-Project Plans](#4-project-by-project-plans)
|
|
||||||
- [4.1 AutoBidder.csproj](#41-autobiddercsproj)
|
|
||||||
- [5. Package Update Reference](#5-package-update-reference)
|
|
||||||
- [6. Breaking Changes Catalog](#6-breaking-changes-catalog)
|
|
||||||
- [7. Risk Management](#7-risk-management)
|
|
||||||
- [8. Testing & Validation Strategy](#8-testing--validation-strategy)
|
|
||||||
- [9. Complexity & Effort Assessment](#9-complexity--effort-assessment)
|
|
||||||
- [10. Source Control Strategy](#10-source-control-strategy)
|
|
||||||
- [11. Success Criteria](#11-success-criteria)
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 1. Executive Summary
|
|
||||||
|
|
||||||
### Scenario
|
|
||||||
Upgrade the **AutoBidder** Blazor/ASP.NET Core solution from **.NET 8.0** to **.NET 10.0 (LTS)**.
|
|
||||||
|
|
||||||
### Scope
|
|
||||||
| Metric | Value |
|
|
||||||
|---|---|
|
|
||||||
| Total Projects | 1 |
|
|
||||||
| Total NuGet Packages | 3 (2 need upgrade, 1 compatible) |
|
|
||||||
| Total Lines of Code | 14,361 |
|
|
||||||
| Total Code Files | 50 |
|
|
||||||
| API Incompatibilities | 0 |
|
|
||||||
| Security Vulnerabilities | 0 |
|
|
||||||
| Estimated LOC Impact | 0+ (minimal) |
|
|
||||||
|
|
||||||
### Target State
|
|
||||||
- All projects targeting `net10.0`
|
|
||||||
- All NuGet packages updated to .NET 10 compatible versions
|
|
||||||
- Solution builds with 0 errors
|
|
||||||
- No security vulnerabilities
|
|
||||||
|
|
||||||
### Selected Strategy
|
|
||||||
**All-At-Once Strategy** — All projects upgraded simultaneously in a single atomic operation.
|
|
||||||
|
|
||||||
**Rationale**:
|
|
||||||
- 1 project (small solution)
|
|
||||||
- Currently on .NET 8.0 (modern SDK-style)
|
|
||||||
- Zero project-to-project dependencies
|
|
||||||
- All packages have known target framework versions available
|
|
||||||
- No API incompatibilities detected
|
|
||||||
- No security vulnerabilities
|
|
||||||
|
|
||||||
### Complexity Classification
|
|
||||||
**Simple** — 1 project, dependency depth 0, no high-risk indicators, no vulnerabilities.
|
|
||||||
|
|
||||||
**Iteration strategy**: Fast batch (2-3 detail iterations covering all sections).
|
|
||||||
|
|
||||||
### Critical Issues
|
|
||||||
- ?? `Npgsql.EntityFrameworkCore.PostgreSQL` 8.0.0 is marked compatible but has no suggested new version — verify compatibility with EF Core 10.0.5 at build time.
|
|
||||||
|
|
||||||
## 2. Migration Strategy
|
|
||||||
|
|
||||||
### Approach: All-At-Once
|
|
||||||
|
|
||||||
The entire solution is upgraded in a **single coordinated atomic operation**. There are no intermediate states — the project file, all package references, and any required code modifications are applied together.
|
|
||||||
|
|
||||||
### Justification
|
|
||||||
- **1 project** — no dependency ordering complexity
|
|
||||||
- **0 inter-project dependencies** — no phasing required
|
|
||||||
- **0 API incompatibilities** — minimal risk of breaking changes
|
|
||||||
- **Low difficulty** rating from assessment
|
|
||||||
- **SDK-style project** — clean project file structure
|
|
||||||
- All packages have target versions or are compatible
|
|
||||||
|
|
||||||
### Execution Approach
|
|
||||||
|
|
||||||
All operations are performed as a unified upgrade:
|
|
||||||
|
|
||||||
1. Update `TargetFramework` in `AutoBidder.csproj` from `net8.0` to `net10.0`
|
|
||||||
2. Update all NuGet package references to their target versions
|
|
||||||
3. Restore dependencies
|
|
||||||
4. Build the solution and fix all compilation errors
|
|
||||||
5. Verify solution builds with 0 errors
|
|
||||||
|
|
||||||
No parallel vs sequential decision is needed — there is only one project.
|
|
||||||
|
|
||||||
## 3. Detailed Dependency Analysis
|
|
||||||
|
|
||||||
### Dependency Graph
|
|
||||||
|
|
||||||
The solution contains a single project with no inter-project dependencies.
|
|
||||||
|
|
||||||
```
|
|
||||||
Level 0 (Foundation):
|
|
||||||
??? AutoBidder.csproj (AspNetCore, Blazor)
|
|
||||||
??? Microsoft.AspNetCore.Identity.EntityFrameworkCore 8.0.0
|
|
||||||
??? Microsoft.EntityFrameworkCore.Sqlite 8.0.0
|
|
||||||
??? Npgsql.EntityFrameworkCore.PostgreSQL 8.0.0
|
|
||||||
```
|
|
||||||
|
|
||||||
### Project Groupings
|
|
||||||
|
|
||||||
Since there is only 1 project with 0 dependencies, all work is performed in a **single atomic operation** with no phasing required.
|
|
||||||
|
|
||||||
| Phase | Projects | Description |
|
|
||||||
|---|---|---|
|
|
||||||
| Phase 1: Atomic Upgrade | AutoBidder.csproj | Framework + packages upgrade, build, fix errors |
|
|
||||||
|
|
||||||
### Critical Path
|
|
||||||
There is no critical path — the single project is both leaf and root. The entire upgrade is a single step.
|
|
||||||
|
|
||||||
### Circular Dependencies
|
|
||||||
None detected.
|
|
||||||
|
|
||||||
## 4. Project-by-Project Plans
|
|
||||||
|
|
||||||
### 4.1 AutoBidder.csproj
|
|
||||||
|
|
||||||
#### Current State
|
|
||||||
|
|
||||||
| Attribute | Value |
|
|
||||||
|---|---|
|
|
||||||
| Target Framework | `net8.0` |
|
|
||||||
| Project Kind | AspNetCore (Blazor) |
|
|
||||||
| SDK-style | Yes |
|
|
||||||
| Lines of Code | 14,361 |
|
|
||||||
| Code Files | 50 (76 total files) |
|
|
||||||
| NuGet Packages | 3 |
|
|
||||||
| Project Dependencies | 0 |
|
|
||||||
| Dependants | 0 |
|
|
||||||
| Risk Level | ?? Low |
|
|
||||||
|
|
||||||
#### Target State
|
|
||||||
|
|
||||||
| Attribute | Value |
|
|
||||||
|---|---|
|
|
||||||
| Target Framework | `net10.0` |
|
|
||||||
| Updated Packages | 2 of 3 |
|
|
||||||
|
|
||||||
#### Migration Steps
|
|
||||||
|
|
||||||
**1. Prerequisites**
|
|
||||||
- .NET 10 SDK installed ?
|
|
||||||
- No `global.json` to update ?
|
|
||||||
|
|
||||||
**2. Framework Update**
|
|
||||||
- Update `TargetFramework` property in `AutoBidder.csproj` from `net8.0` to `net10.0`
|
|
||||||
|
|
||||||
**3. Package Updates**
|
|
||||||
|
|
||||||
| Package | Current Version | Target Version | Reason |
|
|
||||||
|---|---|---|---|
|
|
||||||
| Microsoft.AspNetCore.Identity.EntityFrameworkCore | 8.0.0 | 10.0.5 | Upgrade recommended for .NET 10 compatibility |
|
|
||||||
| Microsoft.EntityFrameworkCore.Sqlite | 8.0.0 | 10.0.5 | Upgrade recommended for .NET 10 compatibility |
|
|
||||||
| Npgsql.EntityFrameworkCore.PostgreSQL | 8.0.0 | (keep) | ? Compatible — verify at build time with EF Core 10.0.5 |
|
|
||||||
|
|
||||||
**4. Expected Breaking Changes**
|
|
||||||
- No API incompatibilities detected by assessment
|
|
||||||
- Potential behavioral changes from EF Core 8 ? 10 (see §6 Breaking Changes Catalog)
|
|
||||||
- Potential ASP.NET Core / Blazor configuration or middleware changes
|
|
||||||
|
|
||||||
**5. Code Modifications**
|
|
||||||
- No code modifications identified by the assessment (0 estimated LOC impact)
|
|
||||||
- Compilation errors (if any) will be discovered and fixed during the build step
|
|
||||||
|
|
||||||
**6. Validation Checklist**
|
|
||||||
- [ ] `AutoBidder.csproj` targets `net10.0`
|
|
||||||
- [ ] All package references updated to target versions
|
|
||||||
- [ ] Solution restores without dependency conflicts
|
|
||||||
- [ ] Solution builds with 0 errors
|
|
||||||
- [ ] No security vulnerabilities
|
|
||||||
|
|
||||||
## 5. Package Update Reference
|
|
||||||
|
|
||||||
### Common Package Updates (affecting AutoBidder.csproj)
|
|
||||||
|
|
||||||
| Package | Current | Target | Update Reason |
|
|
||||||
|---|---|---|---|
|
|
||||||
| Microsoft.AspNetCore.Identity.EntityFrameworkCore | 8.0.0 | 10.0.5 | Framework compatibility — align with .NET 10 |
|
|
||||||
| Microsoft.EntityFrameworkCore.Sqlite | 8.0.0 | 10.0.5 | Framework compatibility — align with .NET 10 |
|
|
||||||
|
|
||||||
### Compatible Packages (no update required)
|
|
||||||
|
|
||||||
| Package | Current Version | Status |
|
|
||||||
|---|---|---|
|
|
||||||
| Npgsql.EntityFrameworkCore.PostgreSQL | 8.0.0 | ? Compatible — verify at build time that it works with EF Core 10.0.5 |
|
|
||||||
|
|
||||||
### Notes
|
|
||||||
- All EF Core-related packages should align to the same major version where possible
|
|
||||||
- `Npgsql.EntityFrameworkCore.PostgreSQL` 8.0.0 is marked compatible by the assessment; however, if build errors occur related to EF Core version mismatch, investigate whether a newer Npgsql version is required
|
|
||||||
|
|
||||||
## 6. Breaking Changes Catalog
|
|
||||||
|
|
||||||
The assessment detected **0 API incompatibilities**. However, the following areas should be monitored during the build and test phases:
|
|
||||||
|
|
||||||
### Framework Breaking Changes (.NET 8 ? .NET 10)
|
|
||||||
|
|
||||||
| Area | Risk | Description |
|
|
||||||
|---|---|---|
|
|
||||||
| ASP.NET Core / Blazor | Low | Possible middleware, routing, or configuration changes between .NET 8 and .NET 10 |
|
|
||||||
| Entity Framework Core 8 ? 10 | Low | Possible behavioral changes in query translation, migrations, or model building |
|
|
||||||
| Base Class Library (BCL) | Low | Minor API behavioral changes possible |
|
|
||||||
|
|
||||||
### Package-Specific Considerations
|
|
||||||
|
|
||||||
| Package | Consideration |
|
|
||||||
|---|---|
|
|
||||||
| Npgsql.EntityFrameworkCore.PostgreSQL 8.0.0 | Verify compatibility with EF Core 10.0.5 — if incompatible, investigate newer Npgsql version |
|
|
||||||
| Microsoft.AspNetCore.Identity.EntityFrameworkCore 10.0.5 | Identity API changes possible between major versions |
|
|
||||||
|
|
||||||
### Discovery Approach
|
|
||||||
Since no specific breaking changes were identified, the primary discovery mechanism is:
|
|
||||||
1. Build the solution after all updates
|
|
||||||
2. Analyze compilation errors
|
|
||||||
3. Fix identified issues referencing .NET 10 migration documentation
|
|
||||||
|
|
||||||
## 7. Risk Management
|
|
||||||
|
|
||||||
### Risk Assessment
|
|
||||||
|
|
||||||
| Project | Risk Level | Description | Mitigation |
|
|
||||||
|---|---|---|---|
|
|
||||||
| AutoBidder.csproj | ?? Low | Single project, 0 API issues, 2 straightforward package updates | Standard upgrade process; rollback via Git if needed |
|
|
||||||
|
|
||||||
### Specific Risks
|
|
||||||
|
|
||||||
| Risk | Probability | Impact | Mitigation |
|
|
||||||
|---|---|---|---|
|
|
||||||
| Npgsql EF Core compatibility | Low | Medium | If build fails due to EF Core version mismatch, upgrade Npgsql to a .NET 10-compatible version |
|
|
||||||
| Undiscovered breaking changes in Blazor/ASP.NET Core | Low | Low | Fix compilation errors during build step; consult .NET 10 migration guide |
|
|
||||||
| EF Core migration compatibility | Low | Medium | Verify existing database migrations still apply correctly with EF Core 10 |
|
|
||||||
|
|
||||||
### Contingency Plans
|
|
||||||
|
|
||||||
- **Build failures from Npgsql**: Search for a newer version of `Npgsql.EntityFrameworkCore.PostgreSQL` compatible with EF Core 10. If none exists, evaluate alternative PostgreSQL providers.
|
|
||||||
- **Unexpected compilation errors**: Use .NET 10 breaking changes documentation to resolve. The upgrade branch isolates all changes — the `docker` branch remains untouched.
|
|
||||||
- **Rollback**: Discard the `upgrade-to-NET10` branch and return to `docker` branch.
|
|
||||||
|
|
||||||
## 8. Testing & Validation Strategy
|
|
||||||
|
|
||||||
### Build Validation
|
|
||||||
|
|
||||||
After the atomic upgrade:
|
|
||||||
- [ ] `dotnet restore` completes without dependency conflicts
|
|
||||||
- [ ] `dotnet build` completes with 0 errors
|
|
||||||
- [ ] No new warnings related to deprecated APIs
|
|
||||||
|
|
||||||
### Test Execution
|
|
||||||
|
|
||||||
No dedicated test projects were identified in the solution. Validation relies on:
|
|
||||||
1. Successful build with 0 errors
|
|
||||||
2. No dependency conflicts
|
|
||||||
3. No security vulnerabilities
|
|
||||||
|
|
||||||
> ?? **Recommendation**: If there are test projects not captured in the solution file, or integration/manual tests, execute them after the upgrade to verify runtime behavior — particularly around Entity Framework Core database operations and Blazor component rendering.
|
|
||||||
|
|
||||||
## 9. Complexity & Effort Assessment
|
|
||||||
|
|
||||||
### Per-Project Complexity
|
|
||||||
|
|
||||||
| Project | Complexity | Dependencies | Package Updates | Risk | Notes |
|
|
||||||
|---|---|---|---|---|---|
|
|
||||||
| AutoBidder.csproj | ?? Low | 0 projects | 2 packages | Low | Single project, no API issues, straightforward upgrade |
|
|
||||||
|
|
||||||
### Overall Assessment
|
|
||||||
|
|
||||||
| Factor | Rating |
|
|
||||||
|---|---|
|
|
||||||
| Solution Complexity | Low (1 project, 0 dependencies) |
|
|
||||||
| Package Update Complexity | Low (2 packages with known target versions) |
|
|
||||||
| Breaking Change Risk | Low (0 API incompatibilities detected) |
|
|
||||||
| Testing Complexity | Low (build verification only — no test projects) |
|
|
||||||
| Overall Upgrade Complexity | **Low** |
|
|
||||||
|
|
||||||
## 10. Source Control Strategy
|
|
||||||
|
|
||||||
### Branching
|
|
||||||
|
|
||||||
| Parameter | Value |
|
|
||||||
|---|---|
|
|
||||||
| Source branch | `docker` |
|
|
||||||
| Upgrade branch | `upgrade-to-NET10` |
|
|
||||||
| Strategy | Single branch for all upgrade changes |
|
|
||||||
|
|
||||||
### Commit Approach
|
|
||||||
|
|
||||||
**Single commit** for the entire atomic upgrade operation:
|
|
||||||
- Framework update, package updates, compilation fixes — all in one commit
|
|
||||||
- Commit message: `Upgrade to .NET 10.0 — update framework and packages`
|
|
||||||
|
|
||||||
### Merge Process
|
|
||||||
|
|
||||||
1. Verify solution builds with 0 errors on `upgrade-to-NET10`
|
|
||||||
2. Merge `upgrade-to-NET10` into `docker` (or open a Pull Request for review)
|
|
||||||
3. Delete `upgrade-to-NET10` branch after successful merge
|
|
||||||
|
|
||||||
## 11. Success Criteria
|
|
||||||
|
|
||||||
### Technical Criteria
|
|
||||||
|
|
||||||
- [ ] `AutoBidder.csproj` targets `net10.0`
|
|
||||||
- [ ] `Microsoft.AspNetCore.Identity.EntityFrameworkCore` updated to 10.0.5
|
|
||||||
- [ ] `Microsoft.EntityFrameworkCore.Sqlite` updated to 10.0.5
|
|
||||||
- [ ] `Npgsql.EntityFrameworkCore.PostgreSQL` compatible (build succeeds)
|
|
||||||
- [ ] Solution builds with 0 errors
|
|
||||||
- [ ] No package dependency conflicts
|
|
||||||
- [ ] No security vulnerabilities
|
|
||||||
|
|
||||||
### Quality Criteria
|
|
||||||
|
|
||||||
- [ ] Code quality maintained (no degraded patterns)
|
|
||||||
- [ ] No new compiler warnings from deprecated APIs
|
|
||||||
|
|
||||||
### Process Criteria
|
|
||||||
|
|
||||||
- [ ] All-At-Once strategy followed (single atomic operation)
|
|
||||||
- [ ] All changes on `upgrade-to-NET10` branch
|
|
||||||
- [ ] Single commit for entire upgrade
|
|
||||||
@@ -1,70 +0,0 @@
|
|||||||
# .NET 10.0 Upgrade — Execution Tasks
|
|
||||||
|
|
||||||
## Progress Dashboard
|
|
||||||
|
|
||||||
| Metric | Value |
|
|
||||||
|---|---|
|
|
||||||
| Total Tasks | 5 |
|
|
||||||
| Completed | 0 |
|
|
||||||
| In Progress | 0 |
|
|
||||||
**Progress**: 5/5 tasks complete (100%) 
|
|
||||||
| Not Started | 5 |
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Tasks
|
|
||||||
|
|
||||||
### [?] TASK-001: Verify Prerequisites *(Completed: 2026-03-31 20:59)*
|
|
||||||
**Scope**: Environment validation
|
|
||||||
**References**: Plan §1, §4.1
|
|
||||||
|
|
||||||
**Actions:**
|
|
||||||
- [?] (1) Verify .NET 10 SDK is installed and available on the machine
|
|
||||||
- [?] (2) Verify no `global.json` file conflicts with target framework `net10.0`
|
|
||||||
- [?] (3) Verify current branch is `upgrade-to-NET10`
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
### [?] TASK-002: Update Target Framework *(Completed: 2026-03-31 21:05)*
|
|
||||||
**Scope**: AutoBidder.csproj
|
|
||||||
**References**: Plan §2, §4.1
|
|
||||||
|
|
||||||
**Actions:**
|
|
||||||
- [?] (1) Update `TargetFramework` property in `AutoBidder.csproj` from `net8.0` to `net10.0`
|
|
||||||
- [?] (2) Verify the project file contains `<TargetFramework>net10.0</TargetFramework>`
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
### [?] TASK-003: Update NuGet Packages *(Completed: 2026-03-31 21:11)*
|
|
||||||
**Scope**: AutoBidder.csproj
|
|
||||||
**References**: Plan §5
|
|
||||||
|
|
||||||
**Actions:**
|
|
||||||
- [?] (1) Update `Microsoft.AspNetCore.Identity.EntityFrameworkCore` from 8.0.0 to 10.0.5
|
|
||||||
- [?] (2) Update `Microsoft.EntityFrameworkCore.Sqlite` from 8.0.0 to 10.0.5
|
|
||||||
- [?] (3) Verify `Npgsql.EntityFrameworkCore.PostgreSQL` 8.0.0 compatibility ? if incompatible with EF Core 10, find and update to a compatible version
|
|
||||||
- [?] (4) Restore packages and verify no dependency conflicts
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
### [?] TASK-004: Build and Fix Compilation Errors *(Completed: 2026-03-31 21:15)*
|
|
||||||
**Scope**: AutoBidder.sln
|
|
||||||
**References**: Plan §6, §8
|
|
||||||
|
|
||||||
**Actions:**
|
|
||||||
- [?] (1) Build the solution
|
|
||||||
- [?] (2) If build errors exist, analyze and fix all compilation errors
|
|
||||||
- [?] (3) Rebuild and verify solution builds with 0 errors
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
### [?] TASK-005: Commit and Final Validation *(Completed: 2026-03-31 21:20)*
|
|
||||||
**Scope**: Repository
|
|
||||||
**References**: Plan §10, §11
|
|
||||||
|
|
||||||
**Actions:**
|
|
||||||
- [?] (1) Verify all success criteria from Plan ?11 are met
|
|
||||||
- [?] (2) Stage all changes and commit with message: `Upgrade to .NET 10.0 ? update framework and packages`
|
|
||||||
- [?] (3) Verify commit succeeded
|
|
||||||
|
|
||||||
---
|
|
||||||
+18
@@ -414,3 +414,21 @@ FodyWeavers.xsd
|
|||||||
# Built Visual Studio Code Extensions
|
# Built Visual Studio Code Extensions
|
||||||
*.vsix
|
*.vsix
|
||||||
|
|
||||||
|
|
||||||
|
# ---> AutoBidder / Mimante
|
||||||
|
|
||||||
|
# Configurazione di rilascio: contiene un token di Gitea
|
||||||
|
gitea.json
|
||||||
|
|
||||||
|
# Riepilogo della rigiocata sui dossier (target Backtest)
|
||||||
|
*backtest-report.txt
|
||||||
|
|
||||||
|
# Pacchetti prodotti da build/Release.proj
|
||||||
|
bin/installer/
|
||||||
|
|
||||||
|
# Rider / JetBrains
|
||||||
|
.idea/
|
||||||
|
|
||||||
|
# Windows
|
||||||
|
Thumbs.db
|
||||||
|
Desktop.ini
|
||||||
|
|||||||
@@ -1,88 +0,0 @@
|
|||||||
**Dockerignore file**
|
|
||||||
|
|
||||||
# Build artifacts
|
|
||||||
**/bin/
|
|
||||||
**/obj/
|
|
||||||
**/out/
|
|
||||||
**/publish/
|
|
||||||
|
|
||||||
# User-specific files
|
|
||||||
*.user
|
|
||||||
*.suo
|
|
||||||
*.userosscache
|
|
||||||
*.sln.docstates
|
|
||||||
|
|
||||||
# IDE files
|
|
||||||
.vs/
|
|
||||||
.vscode/
|
|
||||||
.idea/
|
|
||||||
*.swp
|
|
||||||
*.swo
|
|
||||||
*~
|
|
||||||
|
|
||||||
# Build results
|
|
||||||
[Dd]ebug/
|
|
||||||
[Dd]ebugPublic/
|
|
||||||
[Rr]elease/
|
|
||||||
[Rr]eleases/
|
|
||||||
x64/
|
|
||||||
x86/
|
|
||||||
[Ww][Ii][Nn]32/
|
|
||||||
[Aa][Rr][Mm]/
|
|
||||||
[Aa][Rr][Mm]64/
|
|
||||||
bld/
|
|
||||||
[Bb]in/
|
|
||||||
[Oo]bj/
|
|
||||||
[Ll]og/
|
|
||||||
[Ll]ogs/
|
|
||||||
|
|
||||||
# NuGet packages
|
|
||||||
*.nupkg
|
|
||||||
*.snupkg
|
|
||||||
**/packages/*
|
|
||||||
!**/packages/build/
|
|
||||||
|
|
||||||
# Test project and results
|
|
||||||
Tests/
|
|
||||||
[Tt]est[Rr]esult*/
|
|
||||||
[Bb]uild[Ll]og.*
|
|
||||||
|
|
||||||
# Data and databases (exclude from image)
|
|
||||||
**/data/*.db
|
|
||||||
**/data/*.db-shm
|
|
||||||
**/data/*.db-wal
|
|
||||||
**/data/backups/
|
|
||||||
**/data/logs/
|
|
||||||
|
|
||||||
# Git files
|
|
||||||
.git
|
|
||||||
.gitignore
|
|
||||||
.gitattributes
|
|
||||||
.github/
|
|
||||||
|
|
||||||
# CI/CD files
|
|
||||||
.gitea/
|
|
||||||
|
|
||||||
# Documentation
|
|
||||||
*.md
|
|
||||||
!README.md
|
|
||||||
|
|
||||||
# Docker files
|
|
||||||
Dockerfile*
|
|
||||||
docker-compose*
|
|
||||||
.dockerignore
|
|
||||||
|
|
||||||
# Environment files
|
|
||||||
.env
|
|
||||||
.env.*
|
|
||||||
|
|
||||||
# Temporary files
|
|
||||||
*.tmp
|
|
||||||
*.temp
|
|
||||||
*.cache
|
|
||||||
*.bak
|
|
||||||
*.log
|
|
||||||
|
|
||||||
# OS files
|
|
||||||
.DS_Store
|
|
||||||
Thumbs.db
|
|
||||||
@@ -1,92 +0,0 @@
|
|||||||
# AutoBidder Environment Variables
|
|
||||||
# Copia questo file in .env e configura i valori
|
|
||||||
|
|
||||||
# === ASP.NET Core Configuration ===
|
|
||||||
ASPNETCORE_ENVIRONMENT=Production
|
|
||||||
ASPNETCORE_URLS=http://+:8080
|
|
||||||
|
|
||||||
# === AUTENTICAZIONE APPLICAZIONE (SICUREZZA) ===
|
|
||||||
# Username amministratore
|
|
||||||
ADMIN_USERNAME=admin
|
|
||||||
|
|
||||||
# Password amministratore (OBBLIGATORIO in produzione!)
|
|
||||||
# REQUISITI: min 12 caratteri, maiuscole, minuscole, numeri, simboli
|
|
||||||
# Esempio: Admin@SecurePass2024!
|
|
||||||
ADMIN_PASSWORD=
|
|
||||||
|
|
||||||
# === NOTA: SESSIONE BIDOO ===
|
|
||||||
# Non servono credenziali Bidoo!
|
|
||||||
# Il cookie di sessione Bidoo viene configurato manualmente
|
|
||||||
# dall'interfaccia web in Settings ? Sessione Bidoo
|
|
||||||
|
|
||||||
# === PostgreSQL Database (Statistiche) ===
|
|
||||||
# Username PostgreSQL
|
|
||||||
POSTGRES_USER=autobidder
|
|
||||||
|
|
||||||
# Password PostgreSQL (CAMBIA IN PRODUZIONE!)
|
|
||||||
POSTGRES_PASSWORD=autobidder_password
|
|
||||||
|
|
||||||
# Database name
|
|
||||||
POSTGRES_DB=autobidder_stats
|
|
||||||
|
|
||||||
# Usa PostgreSQL per statistiche (true/false)
|
|
||||||
USE_POSTGRES=true
|
|
||||||
|
|
||||||
# === Application Settings ===
|
|
||||||
# Logging level (Debug, Information, Warning, Error)
|
|
||||||
LOG_LEVEL=Information
|
|
||||||
|
|
||||||
# Porta applicazione (default: 8080 container, mappata su host)
|
|
||||||
APP_PORT=5000
|
|
||||||
|
|
||||||
# === Database Configuration ===
|
|
||||||
# Path database SQLite locale (default: /app/data/autobidder.db in container)
|
|
||||||
# DATABASE_PATH=/app/data/autobidder.db
|
|
||||||
|
|
||||||
# Giorni di retention backup database (default: 30)
|
|
||||||
DB_BACKUP_RETENTION_DAYS=30
|
|
||||||
|
|
||||||
# Auto-ottimizzazione database (VACUUM automatico)
|
|
||||||
DB_AUTO_OPTIMIZE=true
|
|
||||||
|
|
||||||
# === Logging ===
|
|
||||||
# Livello log: Trace, Debug, Information, Warning, Error, Critical
|
|
||||||
LOG_LEVEL=Information
|
|
||||||
|
|
||||||
# Livello log Microsoft: Trace, Debug, Information, Warning, Error, Critical
|
|
||||||
LOG_LEVEL_MICROSOFT=Warning
|
|
||||||
|
|
||||||
# Livello log Entity Framework: Trace, Debug, Information, Warning, Error, Critical
|
|
||||||
LOG_LEVEL_EF=Warning
|
|
||||||
|
|
||||||
# === Application Settings ===
|
|
||||||
# Numero massimo connessioni concorrenti HTTP
|
|
||||||
# MAX_HTTP_CONNECTIONS=10
|
|
||||||
|
|
||||||
# Timeout richieste HTTP (secondi)
|
|
||||||
# HTTP_TIMEOUT=30
|
|
||||||
|
|
||||||
# === Backup Configuration ===
|
|
||||||
# Directory backup (default: /app/data/backups)
|
|
||||||
# BACKUP_DIR=/app/data/backups
|
|
||||||
|
|
||||||
# Numero giorni backup da mantenere
|
|
||||||
# BACKUP_RETENTION_DAYS=30
|
|
||||||
|
|
||||||
# === Security ===
|
|
||||||
# Chiave segreta per DataProtection (genera random se non specificato)
|
|
||||||
# DATA_PROTECTION_KEY=your-random-key-here
|
|
||||||
|
|
||||||
# === Monitoring ===
|
|
||||||
# Abilita metriche Prometheus (true/false)
|
|
||||||
# ENABLE_METRICS=false
|
|
||||||
|
|
||||||
# Porta metriche (se ENABLE_METRICS=true)
|
|
||||||
# METRICS_PORT=9090
|
|
||||||
|
|
||||||
# === Advanced ===
|
|
||||||
# Numero thread worker per polling aste
|
|
||||||
# AUCTION_WORKER_THREADS=4
|
|
||||||
|
|
||||||
# Intervallo pulizia cache (minuti)
|
|
||||||
# CACHE_CLEANUP_INTERVAL=60
|
|
||||||
@@ -1,71 +0,0 @@
|
|||||||
name: Database Backup
|
|
||||||
|
|
||||||
on:
|
|
||||||
schedule:
|
|
||||||
# Esegui backup ogni giorno alle 2:00 AM UTC
|
|
||||||
- cron: '0 2 * * *'
|
|
||||||
workflow_dispatch: # Permette trigger manuale
|
|
||||||
|
|
||||||
jobs:
|
|
||||||
backup-database:
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
|
|
||||||
steps:
|
|
||||||
- name: Execute remote backup
|
|
||||||
uses: appleboy/ssh-action@master
|
|
||||||
with:
|
|
||||||
host: ${{ secrets.DEPLOY_HOST }}
|
|
||||||
username: ${{ secrets.DEPLOY_USER }}
|
|
||||||
key: ${{ secrets.DEPLOY_SSH_KEY }}
|
|
||||||
script: |
|
|
||||||
echo "??? Starting database backup..."
|
|
||||||
|
|
||||||
cd /opt/autobidder
|
|
||||||
|
|
||||||
# Directory backup
|
|
||||||
BACKUP_DIR="./data/backups"
|
|
||||||
mkdir -p $BACKUP_DIR
|
|
||||||
|
|
||||||
# Timestamp
|
|
||||||
TIMESTAMP=$(date +%Y%m%d_%H%M%S)
|
|
||||||
|
|
||||||
# Backup database
|
|
||||||
if [ -f ./data/autobidder.db ]; then
|
|
||||||
echo "?? Backing up autobidder.db..."
|
|
||||||
cp ./data/autobidder.db $BACKUP_DIR/autobidder_backup_$TIMESTAMP.db
|
|
||||||
|
|
||||||
# Verifica backup
|
|
||||||
if [ -f $BACKUP_DIR/autobidder_backup_$TIMESTAMP.db ]; then
|
|
||||||
SIZE=$(du -h $BACKUP_DIR/autobidder_backup_$TIMESTAMP.db | cut -f1)
|
|
||||||
echo "? Backup created successfully: $SIZE"
|
|
||||||
else
|
|
||||||
echo "? Backup failed!"
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
else
|
|
||||||
echo "?? Database file not found!"
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
|
|
||||||
# Cleanup backup vecchi (mantieni ultimi 30 giorni)
|
|
||||||
echo "?? Cleaning up old backups..."
|
|
||||||
find $BACKUP_DIR -name "autobidder_backup_*.db" -mtime +30 -delete
|
|
||||||
|
|
||||||
# Conta backup rimanenti
|
|
||||||
BACKUP_COUNT=$(find $BACKUP_DIR -name "autobidder_backup_*.db" | wc -l)
|
|
||||||
echo "?? Total backups: $BACKUP_COUNT"
|
|
||||||
|
|
||||||
# Mostra dimensione totale backup
|
|
||||||
TOTAL_SIZE=$(du -sh $BACKUP_DIR | cut -f1)
|
|
||||||
echo "?? Total backup size: $TOTAL_SIZE"
|
|
||||||
|
|
||||||
echo "?? Backup completed successfully!"
|
|
||||||
|
|
||||||
- name: Backup summary
|
|
||||||
if: always()
|
|
||||||
run: |
|
|
||||||
if [ "${{ job.status }}" == "success" ]; then
|
|
||||||
echo "? Database backup SUCCESSFUL"
|
|
||||||
else
|
|
||||||
echo "? Database backup FAILED"
|
|
||||||
fi
|
|
||||||
@@ -1,229 +0,0 @@
|
|||||||
name: Build and Deploy AutoBidder
|
|
||||||
|
|
||||||
on:
|
|
||||||
push:
|
|
||||||
branches:
|
|
||||||
- main
|
|
||||||
- docker
|
|
||||||
pull_request:
|
|
||||||
branches:
|
|
||||||
- main
|
|
||||||
workflow_dispatch: # Permette trigger manuale
|
|
||||||
|
|
||||||
env:
|
|
||||||
DOTNET_VERSION: '10.0.x'
|
|
||||||
REGISTRY: ${{ secrets.GITEA_REGISTRY }}
|
|
||||||
|
|
||||||
jobs:
|
|
||||||
# Job 1: Build e Test .NET
|
|
||||||
build-and-test:
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
|
|
||||||
steps:
|
|
||||||
- name: Checkout code
|
|
||||||
uses: actions/checkout@v3
|
|
||||||
with:
|
|
||||||
fetch-depth: 0 # Fetch completo per analisi
|
|
||||||
|
|
||||||
- name: Set up .NET
|
|
||||||
uses: actions/setup-dotnet@v3
|
|
||||||
with:
|
|
||||||
dotnet-version: ${{ env.DOTNET_VERSION }}
|
|
||||||
|
|
||||||
- name: Cache NuGet packages
|
|
||||||
uses: actions/cache@v3
|
|
||||||
with:
|
|
||||||
path: ~/.nuget/packages
|
|
||||||
key: ${{ runner.os }}-nuget-${{ hashFiles('**/*.csproj') }}
|
|
||||||
restore-keys: |
|
|
||||||
${{ runner.os }}-nuget-
|
|
||||||
|
|
||||||
- name: Restore dependencies
|
|
||||||
run: dotnet restore
|
|
||||||
|
|
||||||
- name: Build
|
|
||||||
run: dotnet build --configuration Release --no-restore
|
|
||||||
|
|
||||||
- name: Run tests
|
|
||||||
run: dotnet test --no-restore --verbosity normal --logger "console;verbosity=detailed" --results-directory ./test-results --logger "trx;LogFileName=results.trx"
|
|
||||||
|
|
||||||
- name: Upload test results
|
|
||||||
uses: actions/upload-artifact@v3
|
|
||||||
if: always()
|
|
||||||
with:
|
|
||||||
name: test-results
|
|
||||||
path: ./test-results
|
|
||||||
retention-days: 30
|
|
||||||
|
|
||||||
- name: Publish artifacts
|
|
||||||
run: dotnet publish --configuration Release --no-build --output ./publish
|
|
||||||
|
|
||||||
- name: Upload publish artifacts
|
|
||||||
uses: actions/upload-artifact@v3
|
|
||||||
with:
|
|
||||||
name: publish-artifacts
|
|
||||||
path: ./publish
|
|
||||||
retention-days: 7
|
|
||||||
|
|
||||||
# Job 2: Build e Push Docker Image
|
|
||||||
build-docker:
|
|
||||||
needs: build-and-test
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
if: github.event_name == 'push'
|
|
||||||
|
|
||||||
steps:
|
|
||||||
- name: Checkout code
|
|
||||||
uses: actions/checkout@v3
|
|
||||||
|
|
||||||
- name: Set up Docker Buildx
|
|
||||||
uses: docker/setup-buildx-action@v2
|
|
||||||
|
|
||||||
- name: Log in to Gitea Container Registry
|
|
||||||
uses: docker/login-action@v2
|
|
||||||
with:
|
|
||||||
registry: ${{ env.REGISTRY }}
|
|
||||||
username: ${{ secrets.GITEA_USERNAME }}
|
|
||||||
password: ${{ secrets.GITEA_PASSWORD }}
|
|
||||||
|
|
||||||
- name: Extract metadata for Docker
|
|
||||||
id: meta
|
|
||||||
uses: docker/metadata-action@v4
|
|
||||||
with:
|
|
||||||
images: ${{ env.REGISTRY }}/autobidder
|
|
||||||
tags: |
|
|
||||||
type=ref,event=branch
|
|
||||||
type=ref,event=pr
|
|
||||||
type=semver,pattern={{version}}
|
|
||||||
type=semver,pattern={{major}}.{{minor}}
|
|
||||||
type=sha,prefix=,format=short
|
|
||||||
type=raw,value=latest,enable={{is_default_branch}}
|
|
||||||
labels: |
|
|
||||||
org.opencontainers.image.title=AutoBidder
|
|
||||||
org.opencontainers.image.description=Sistema automatizzato gestione aste Blazor
|
|
||||||
org.opencontainers.image.vendor=Alby96
|
|
||||||
|
|
||||||
- name: Build and push Docker image
|
|
||||||
uses: docker/build-push-action@v4
|
|
||||||
with:
|
|
||||||
context: .
|
|
||||||
file: ./Dockerfile
|
|
||||||
push: true
|
|
||||||
tags: ${{ steps.meta.outputs.tags }}
|
|
||||||
labels: ${{ steps.meta.outputs.labels }}
|
|
||||||
cache-from: type=registry,ref=${{ env.REGISTRY }}/autobidder:buildcache
|
|
||||||
cache-to: type=registry,ref=${{ env.REGISTRY }}/autobidder:buildcache,mode=max
|
|
||||||
build-args: |
|
|
||||||
BUILD_DATE=${{ github.event.head_commit.timestamp }}
|
|
||||||
VCS_REF=${{ github.sha }}
|
|
||||||
VERSION=${{ steps.meta.outputs.version }}
|
|
||||||
|
|
||||||
# Job 3: Security Scan (opzionale)
|
|
||||||
security-scan:
|
|
||||||
needs: build-docker
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
if: github.event_name == 'push'
|
|
||||||
continue-on-error: true
|
|
||||||
|
|
||||||
steps:
|
|
||||||
- name: Run Trivy vulnerability scanner
|
|
||||||
uses: aquasecurity/trivy-action@master
|
|
||||||
with:
|
|
||||||
image-ref: ${{ env.REGISTRY }}/autobidder:latest
|
|
||||||
format: 'sarif'
|
|
||||||
output: 'trivy-results.sarif'
|
|
||||||
|
|
||||||
- name: Upload Trivy results
|
|
||||||
uses: github/codeql-action/upload-sarif@v2
|
|
||||||
if: always()
|
|
||||||
with:
|
|
||||||
sarif_file: 'trivy-results.sarif'
|
|
||||||
|
|
||||||
# Job 4: Deploy su Server
|
|
||||||
deploy:
|
|
||||||
needs: build-docker
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
if: (github.ref == 'refs/heads/main' || github.ref == 'refs/heads/docker') && github.event_name == 'push'
|
|
||||||
environment:
|
|
||||||
name: production
|
|
||||||
url: https://${{ secrets.DEPLOY_HOST }}:5001
|
|
||||||
|
|
||||||
steps:
|
|
||||||
- name: Deploy to server
|
|
||||||
uses: appleboy/ssh-action@master
|
|
||||||
with:
|
|
||||||
host: ${{ secrets.DEPLOY_HOST }}
|
|
||||||
username: ${{ secrets.DEPLOY_USER }}
|
|
||||||
key: ${{ secrets.DEPLOY_SSH_KEY }}
|
|
||||||
script: |
|
|
||||||
echo "?? Starting deployment..."
|
|
||||||
|
|
||||||
# Vai alla directory deploy
|
|
||||||
cd /opt/autobidder || exit 1
|
|
||||||
|
|
||||||
# Carica variabili ambiente
|
|
||||||
if [ -f .env ]; then
|
|
||||||
export $(cat .env | grep -v '^#' | xargs)
|
|
||||||
fi
|
|
||||||
|
|
||||||
# Login al registry
|
|
||||||
echo "$GITEA_PASSWORD" | docker login $GITEA_REGISTRY -u $GITEA_USERNAME --password-stdin
|
|
||||||
|
|
||||||
# Backup database prima del deploy
|
|
||||||
echo "?? Creating database backup..."
|
|
||||||
if [ -f data/autobidder.db ]; then
|
|
||||||
mkdir -p data/backups
|
|
||||||
cp data/autobidder.db data/backups/autobidder_predeploy_$(date +%Y%m%d_%H%M%S).db
|
|
||||||
fi
|
|
||||||
|
|
||||||
# Pull nuova immagine
|
|
||||||
echo "?? Pulling latest image..."
|
|
||||||
docker-compose pull
|
|
||||||
|
|
||||||
# Stop vecchi container
|
|
||||||
echo "?? Stopping old containers..."
|
|
||||||
docker-compose down
|
|
||||||
|
|
||||||
# Start nuovi container
|
|
||||||
echo "?? Starting new containers..."
|
|
||||||
docker-compose up -d
|
|
||||||
|
|
||||||
# Attendi healthcheck
|
|
||||||
echo "?? Waiting for healthcheck..."
|
|
||||||
sleep 15
|
|
||||||
|
|
||||||
# Verifica status
|
|
||||||
echo "?? Container status:"
|
|
||||||
docker-compose ps
|
|
||||||
|
|
||||||
# Verifica healthcheck
|
|
||||||
if docker inspect --format='{{.State.Health.Status}}' autobidder | grep -q "healthy"; then
|
|
||||||
echo "? Deploy successful! Container is healthy."
|
|
||||||
else
|
|
||||||
echo "?? Warning: Container may not be healthy yet. Check logs."
|
|
||||||
fi
|
|
||||||
|
|
||||||
# Mostra ultimi log
|
|
||||||
echo "?? Recent logs:"
|
|
||||||
docker-compose logs --tail=30
|
|
||||||
|
|
||||||
echo "?? Deployment completed!"
|
|
||||||
|
|
||||||
- name: Notify deployment status
|
|
||||||
if: always()
|
|
||||||
run: |
|
|
||||||
if [ "${{ job.status }}" == "success" ]; then
|
|
||||||
echo "? Deployment SUCCESSFUL"
|
|
||||||
else
|
|
||||||
echo "? Deployment FAILED"
|
|
||||||
fi
|
|
||||||
|
|
||||||
# Job 5: Cleanup (rimuove vecchie immagini dal registry)
|
|
||||||
cleanup:
|
|
||||||
needs: deploy
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
if: github.ref == 'refs/heads/main' && github.event_name == 'push'
|
|
||||||
|
|
||||||
steps:
|
|
||||||
- name: Cleanup old images
|
|
||||||
run: |
|
|
||||||
echo "?? Cleanup task completed (manual cleanup required on Gitea)"
|
|
||||||
@@ -1,70 +0,0 @@
|
|||||||
name: Health Check Monitor
|
|
||||||
|
|
||||||
on:
|
|
||||||
schedule:
|
|
||||||
# Verifica ogni ora
|
|
||||||
- cron: '0 * * * *'
|
|
||||||
workflow_dispatch: # Permette trigger manuale
|
|
||||||
|
|
||||||
jobs:
|
|
||||||
health-check:
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
|
|
||||||
steps:
|
|
||||||
- name: Check application health
|
|
||||||
uses: appleboy/ssh-action@master
|
|
||||||
with:
|
|
||||||
host: ${{ secrets.DEPLOY_HOST }}
|
|
||||||
username: ${{ secrets.DEPLOY_USER }}
|
|
||||||
key: ${{ secrets.DEPLOY_SSH_KEY }}
|
|
||||||
script: |
|
|
||||||
echo "?? Health Check Starting..."
|
|
||||||
|
|
||||||
# Verifica container running
|
|
||||||
if ! docker ps | grep -q "autobidder"; then
|
|
||||||
echo "? Container NOT running!"
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
|
|
||||||
# Verifica Docker healthcheck
|
|
||||||
HEALTH_STATUS=$(docker inspect --format='{{.State.Health.Status}}' autobidder 2>/dev/null || echo "unknown")
|
|
||||||
echo "Docker Health: $HEALTH_STATUS"
|
|
||||||
|
|
||||||
if [ "$HEALTH_STATUS" != "healthy" ]; then
|
|
||||||
echo "?? Container not healthy!"
|
|
||||||
echo "Recent logs:"
|
|
||||||
docker logs autobidder --tail=50
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
|
|
||||||
# Test HTTP endpoint
|
|
||||||
if curl -f -s http://localhost:5000/health > /dev/null; then
|
|
||||||
echo "? HTTP endpoint: OK"
|
|
||||||
else
|
|
||||||
echo "? HTTP endpoint: FAILED"
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
|
|
||||||
# Verifica database
|
|
||||||
cd /opt/autobidder
|
|
||||||
if [ -f ./data/autobidder.db ]; then
|
|
||||||
DB_SIZE=$(du -h ./data/autobidder.db | cut -f1)
|
|
||||||
echo "?? Database size: $DB_SIZE"
|
|
||||||
else
|
|
||||||
echo "?? Database file not found!"
|
|
||||||
fi
|
|
||||||
|
|
||||||
# Verifica risorse container
|
|
||||||
echo "?? Container resources:"
|
|
||||||
docker stats autobidder --no-stream --format " CPU: {{.CPUPerc}}\n Memory: {{.MemUsage}}"
|
|
||||||
|
|
||||||
echo "? All health checks passed!"
|
|
||||||
|
|
||||||
- name: Health check summary
|
|
||||||
if: always()
|
|
||||||
run: |
|
|
||||||
if [ "${{ job.status }}" == "success" ]; then
|
|
||||||
echo "? Health check PASSED"
|
|
||||||
else
|
|
||||||
echo "? Health check FAILED - Check server status!"
|
|
||||||
fi
|
|
||||||
Vendored
-104
@@ -1,104 +0,0 @@
|
|||||||
name: AutoBidder CI/CD
|
|
||||||
|
|
||||||
on:
|
|
||||||
push:
|
|
||||||
branches: [ main, docker ]
|
|
||||||
pull_request:
|
|
||||||
branches: [ main ]
|
|
||||||
|
|
||||||
env:
|
|
||||||
REGISTRY: 192.168.30.23/Alby96
|
|
||||||
IMAGE_NAME: autobidder
|
|
||||||
|
|
||||||
jobs:
|
|
||||||
build-and-test:
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
|
|
||||||
steps:
|
|
||||||
- uses: actions/checkout@v3
|
|
||||||
|
|
||||||
- name: Setup .NET
|
|
||||||
uses: actions/setup-dotnet@v3
|
|
||||||
with:
|
|
||||||
dotnet-version: 10.0.x
|
|
||||||
|
|
||||||
- name: Restore dependencies
|
|
||||||
run: dotnet restore
|
|
||||||
|
|
||||||
- name: Build
|
|
||||||
run: dotnet build --configuration Release --no-restore
|
|
||||||
|
|
||||||
- name: Test
|
|
||||||
run: dotnet test --no-restore --verbosity normal --results-directory ./test-results --logger "trx;LogFileName=results.trx"
|
|
||||||
|
|
||||||
- name: Upload test results
|
|
||||||
uses: actions/upload-artifact@v3
|
|
||||||
if: always()
|
|
||||||
with:
|
|
||||||
name: test-results
|
|
||||||
path: ./test-results
|
|
||||||
retention-days: 30
|
|
||||||
|
|
||||||
- name: Publish
|
|
||||||
run: dotnet publish --configuration Release --no-build --output ./publish
|
|
||||||
|
|
||||||
docker-build-push:
|
|
||||||
needs: build-and-test
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
if: github.event_name == 'push'
|
|
||||||
|
|
||||||
steps:
|
|
||||||
- uses: actions/checkout@v3
|
|
||||||
|
|
||||||
- name: Set up Docker Buildx
|
|
||||||
uses: docker/setup-buildx-action@v2
|
|
||||||
|
|
||||||
- name: Login to Gitea Registry
|
|
||||||
uses: docker/login-action@v2
|
|
||||||
with:
|
|
||||||
registry: ${{ secrets.GITEA_REGISTRY }}
|
|
||||||
username: ${{ secrets.GITEA_USERNAME }}
|
|
||||||
password: ${{ secrets.GITEA_PASSWORD }}
|
|
||||||
|
|
||||||
- name: Extract metadata
|
|
||||||
id: meta
|
|
||||||
uses: docker/metadata-action@v4
|
|
||||||
with:
|
|
||||||
images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}
|
|
||||||
tags: |
|
|
||||||
type=ref,event=branch
|
|
||||||
type=sha
|
|
||||||
type=raw,value=latest,enable={{is_default_branch}}
|
|
||||||
|
|
||||||
- name: Build and push
|
|
||||||
uses: docker/build-push-action@v4
|
|
||||||
with:
|
|
||||||
context: .
|
|
||||||
push: true
|
|
||||||
tags: ${{ steps.meta.outputs.tags }}
|
|
||||||
labels: ${{ steps.meta.outputs.labels }}
|
|
||||||
cache-from: type=registry,ref=${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:buildcache
|
|
||||||
cache-to: type=registry,ref=${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:buildcache,mode=max
|
|
||||||
|
|
||||||
deploy:
|
|
||||||
needs: docker-build-push
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
if: github.ref == 'refs/heads/docker' || github.ref == 'refs/heads/main'
|
|
||||||
|
|
||||||
steps:
|
|
||||||
- name: Deploy to server
|
|
||||||
uses: appleboy/ssh-action@master
|
|
||||||
with:
|
|
||||||
host: ${{ secrets.DEPLOY_HOST }}
|
|
||||||
username: ${{ secrets.DEPLOY_USER }}
|
|
||||||
key: ${{ secrets.DEPLOY_SSH_KEY }}
|
|
||||||
script: |
|
|
||||||
cd /opt/autobidder
|
|
||||||
source .env
|
|
||||||
echo "$GITEA_PASSWORD" | docker login $GITEA_REGISTRY -u $GITEA_USERNAME --password-stdin
|
|
||||||
docker-compose pull
|
|
||||||
docker-compose down
|
|
||||||
docker-compose up -d
|
|
||||||
sleep 10
|
|
||||||
docker-compose ps
|
|
||||||
docker-compose logs --tail=30
|
|
||||||
@@ -1,456 +0,0 @@
|
|||||||
## Ignore Visual Studio temporary files, build results, and
|
|
||||||
## files generated by popular Visual Studio add-ons.
|
|
||||||
|
|
||||||
# User-specific files
|
|
||||||
*.rsuser
|
|
||||||
*.suo
|
|
||||||
*.user
|
|
||||||
*.userosscache
|
|
||||||
*.sln.docstates
|
|
||||||
|
|
||||||
# User-specific files (MonoDevelop/Xamarin Studio)
|
|
||||||
*.userprefs
|
|
||||||
|
|
||||||
# Mono auto generated files
|
|
||||||
mono_crash.*
|
|
||||||
|
|
||||||
# Build results
|
|
||||||
[Dd]ebug/
|
|
||||||
[Dd]ebugPublic/
|
|
||||||
[Rr]elease/
|
|
||||||
[Rr]eleases/
|
|
||||||
x64/
|
|
||||||
x86/
|
|
||||||
[Ww][Ii][Nn]32/
|
|
||||||
[Aa][Rr][Mm]/
|
|
||||||
[Aa][Rr][Mm]64/
|
|
||||||
bld/
|
|
||||||
[Bb]in/
|
|
||||||
[Oo]bj/
|
|
||||||
[Ll]og/
|
|
||||||
[Ll]ogs/
|
|
||||||
|
|
||||||
# Visual Studio 2015/2017 cache/options directory
|
|
||||||
.vs/
|
|
||||||
# Uncomment if you have tasks that create the project's static files in wwwroot
|
|
||||||
#wwwroot/
|
|
||||||
|
|
||||||
# Visual Studio 2017 auto generated files
|
|
||||||
Generated\ Files/
|
|
||||||
|
|
||||||
# MSTest test Results
|
|
||||||
[Tt]est[Rr]esult*/
|
|
||||||
[Bb]uild[Ll]og.*
|
|
||||||
|
|
||||||
# NUnit
|
|
||||||
*.VisualState.xml
|
|
||||||
TestResult.xml
|
|
||||||
nunit-*.xml
|
|
||||||
|
|
||||||
# Build Results of an ATL Project
|
|
||||||
[Dd]ebugPS/
|
|
||||||
[Rr]eleasePS/
|
|
||||||
dlldata.c
|
|
||||||
|
|
||||||
# Benchmark Results
|
|
||||||
BenchmarkDotNet.Artifacts/
|
|
||||||
|
|
||||||
# .NET Core
|
|
||||||
project.lock.json
|
|
||||||
project.fragment.lock.json
|
|
||||||
artifacts/
|
|
||||||
|
|
||||||
# ASP.NET Scaffolding
|
|
||||||
ScaffoldingReadMe.txt
|
|
||||||
|
|
||||||
# StyleCop
|
|
||||||
StyleCopReport.xml
|
|
||||||
|
|
||||||
# Files built by Visual Studio
|
|
||||||
*_i.c
|
|
||||||
*_p.c
|
|
||||||
*_h.h
|
|
||||||
*.ilk
|
|
||||||
*.meta
|
|
||||||
*.obj
|
|
||||||
*.iobj
|
|
||||||
*.pch
|
|
||||||
*.pdb
|
|
||||||
*.ipdb
|
|
||||||
*.pgc
|
|
||||||
*.pgd
|
|
||||||
*.rsp
|
|
||||||
*.sbr
|
|
||||||
*.tlb
|
|
||||||
*.tli
|
|
||||||
*.tlh
|
|
||||||
*.tmp
|
|
||||||
*.tmp_proj
|
|
||||||
*_wpftmp.csproj
|
|
||||||
*.log
|
|
||||||
*.tlog
|
|
||||||
*.vspscc
|
|
||||||
*.vssscc
|
|
||||||
.builds
|
|
||||||
*.pidb
|
|
||||||
*.svclog
|
|
||||||
*.scc
|
|
||||||
|
|
||||||
# Chutzpah Test files
|
|
||||||
_Chutzpah*
|
|
||||||
|
|
||||||
# Visual C++ cache files
|
|
||||||
ipch/
|
|
||||||
*.aps
|
|
||||||
*.ncb
|
|
||||||
*.opendb
|
|
||||||
*.opensdf
|
|
||||||
*.sdf
|
|
||||||
*.cachefile
|
|
||||||
*.VC.db
|
|
||||||
*.VC.VC.opendb
|
|
||||||
|
|
||||||
# Visual Studio profiler
|
|
||||||
*.psess
|
|
||||||
*.vsp
|
|
||||||
*.vspx
|
|
||||||
*.sap
|
|
||||||
|
|
||||||
# Visual Studio Trace Files
|
|
||||||
*.e2e
|
|
||||||
|
|
||||||
# TFS 2012 Local Workspace
|
|
||||||
$tf/
|
|
||||||
|
|
||||||
# Guidance Automation Toolkit
|
|
||||||
*.gpState
|
|
||||||
|
|
||||||
# ReSharper is a .NET coding add-in
|
|
||||||
_ReSharper*/
|
|
||||||
*.[Rr]e[Ss]harper
|
|
||||||
*.DotSettings.user
|
|
||||||
|
|
||||||
# TeamCity is a build add-in
|
|
||||||
_TeamCity*
|
|
||||||
|
|
||||||
# DotCover is a Code Coverage Tool
|
|
||||||
*.dotCover
|
|
||||||
|
|
||||||
# AxoCover is a Code Coverage Tool
|
|
||||||
.axoCover/*
|
|
||||||
!.axoCover/settings.json
|
|
||||||
|
|
||||||
# Coverlet is a free, cross platform Code Coverage Tool
|
|
||||||
coverage*.json
|
|
||||||
coverage*.xml
|
|
||||||
coverage*.info
|
|
||||||
|
|
||||||
# Visual Studio code coverage results
|
|
||||||
*.coverage
|
|
||||||
*.coveragexml
|
|
||||||
|
|
||||||
# NCrunch
|
|
||||||
_NCrunch_*
|
|
||||||
.*crunch*.local.xml
|
|
||||||
nCrunchTemp_*
|
|
||||||
|
|
||||||
# MightyMoose
|
|
||||||
*.mm.*
|
|
||||||
AutoTest.Net/
|
|
||||||
|
|
||||||
# Web workbench (sass)
|
|
||||||
.sass-cache/
|
|
||||||
|
|
||||||
# Installshield output folder
|
|
||||||
[Ee]xpress/
|
|
||||||
|
|
||||||
# DocProject is a documentation generator add-in
|
|
||||||
DocProject/buildhelp/
|
|
||||||
DocProject/Help/*.HxT
|
|
||||||
DocProject/Help/*.HxC
|
|
||||||
DocProject/Help/*.hhc
|
|
||||||
DocProject/Help/*.hhk
|
|
||||||
DocProject/Help/*.hhp
|
|
||||||
DocProject/Help/Html2
|
|
||||||
DocProject/Help/html
|
|
||||||
|
|
||||||
# Click-Once directory
|
|
||||||
publish/
|
|
||||||
|
|
||||||
# Publish Web Output
|
|
||||||
*.[Pp]ublish.xml
|
|
||||||
*.azurePubxml
|
|
||||||
# Note: Comment the next line if you want to checkin your web deploy settings,
|
|
||||||
# but database connection strings (with potential passwords) will be unencrypted
|
|
||||||
*.pubxml
|
|
||||||
*.publishproj
|
|
||||||
|
|
||||||
# Microsoft Azure Web App publish settings. Comment the next line if you want to
|
|
||||||
# checkin your Azure Web App publish settings, but sensitive information contained
|
|
||||||
# in these scripts will be unencrypted
|
|
||||||
PublishScripts/
|
|
||||||
|
|
||||||
# NuGet Packages
|
|
||||||
*.nupkg
|
|
||||||
# NuGet Symbol Packages
|
|
||||||
*.snupkg
|
|
||||||
# The packages folder can be ignored because of Package Restore
|
|
||||||
**/[Pp]ackages/*
|
|
||||||
# except build/, which is used as an MSBuild target.
|
|
||||||
!**/[Pp]ackages/build/
|
|
||||||
# Uncomment if necessary however generally it will be regenerated when needed
|
|
||||||
#!**/[Pp]ackages/repositories.config
|
|
||||||
# NuGet v3's project.json files produces more ignorable files
|
|
||||||
*.nuget.props
|
|
||||||
*.nuget.targets
|
|
||||||
|
|
||||||
# Microsoft Azure Build Output
|
|
||||||
csx/
|
|
||||||
*.build.csdef
|
|
||||||
|
|
||||||
# Microsoft Azure Emulator
|
|
||||||
ecf/
|
|
||||||
rcf/
|
|
||||||
|
|
||||||
# Windows Store app package directories and files
|
|
||||||
AppPackages/
|
|
||||||
BundleArtifacts/
|
|
||||||
Package.StoreAssociation.xml
|
|
||||||
_pkginfo.txt
|
|
||||||
*.appx
|
|
||||||
*.appxbundle
|
|
||||||
*.appxupload
|
|
||||||
|
|
||||||
# Visual Studio cache files
|
|
||||||
# files ending in .cache can be ignored
|
|
||||||
*.[Cc]ache
|
|
||||||
# but keep track of directories ending in .cache
|
|
||||||
!?*.[Cc]ache/
|
|
||||||
|
|
||||||
# Others
|
|
||||||
ClientBin/
|
|
||||||
~$*
|
|
||||||
*~
|
|
||||||
*.dbmdl
|
|
||||||
*.dbproj.schemaview
|
|
||||||
*.jfm
|
|
||||||
*.pfx
|
|
||||||
*.publishsettings
|
|
||||||
orleans.codegen.cs
|
|
||||||
|
|
||||||
# Including strong name files can present a security risk
|
|
||||||
# (https://github.com/github/gitignore/pull/2483#issue-259490424)
|
|
||||||
#*.snk
|
|
||||||
|
|
||||||
# Since there are multiple workflows, uncomment next line to ignore bower_components
|
|
||||||
# (https://github.com/github/gitignore/pull/1529#issuecomment-104372622)
|
|
||||||
#bower_components/
|
|
||||||
|
|
||||||
# RIA/Silverlight projects
|
|
||||||
Generated_Code/
|
|
||||||
|
|
||||||
# Backup & report files from converting an old project file
|
|
||||||
# to a newer Visual Studio version. Backup files are not needed,
|
|
||||||
# because we have git ;-)
|
|
||||||
_UpgradeReport_Files/
|
|
||||||
Backup*/
|
|
||||||
UpgradeLog*.XML
|
|
||||||
UpgradeLog*.htm
|
|
||||||
ServiceFabricBackup/
|
|
||||||
*.rptproj.bak
|
|
||||||
|
|
||||||
# SQL Server files
|
|
||||||
*.mdf
|
|
||||||
*.ldf
|
|
||||||
*.ndf
|
|
||||||
|
|
||||||
# Business Intelligence projects
|
|
||||||
*.rdl.data
|
|
||||||
*.bim.layout
|
|
||||||
*.bim_*.settings
|
|
||||||
*.rptproj.rsuser
|
|
||||||
*- [Bb]ackup.rdl
|
|
||||||
*- [Bb]ackup ([0-9]).rdl
|
|
||||||
*- [Bb]ackup ([0-9][0-9]).rdl
|
|
||||||
|
|
||||||
# Microsoft Fakes
|
|
||||||
FakesAssemblies/
|
|
||||||
|
|
||||||
# GhostDoc plugin setting file
|
|
||||||
*.GhostDoc.xml
|
|
||||||
|
|
||||||
# Node.js Tools for Visual Studio
|
|
||||||
.ntvs_analysis.dat
|
|
||||||
node_modules/
|
|
||||||
|
|
||||||
# Visual Studio 6 build log
|
|
||||||
*.plg
|
|
||||||
|
|
||||||
# Visual Studio 6 workspace options file
|
|
||||||
*.opt
|
|
||||||
|
|
||||||
# Visual Studio 6 auto-generated workspace file (contains which files were open etc.)
|
|
||||||
*.vbw
|
|
||||||
|
|
||||||
# Visual Studio 6 auto-generated project file (contains which files were open etc.)
|
|
||||||
*.vbp
|
|
||||||
|
|
||||||
# Visual Studio 6 workspace and project file (working project files containing files to include in project)
|
|
||||||
*.dsw
|
|
||||||
*.dsp
|
|
||||||
|
|
||||||
# Visual Studio 6 technical files
|
|
||||||
*.ncb
|
|
||||||
*.aps
|
|
||||||
|
|
||||||
# Visual Studio LightSwitch build output
|
|
||||||
**/*.HTMLClient/GeneratedArtifacts
|
|
||||||
**/*.DesktopClient/GeneratedArtifacts
|
|
||||||
**/*.DesktopClient/ModelManifest.xml
|
|
||||||
**/*.Server/GeneratedArtifacts
|
|
||||||
**/*.Server/ModelManifest.xml
|
|
||||||
_Pvt_Extensions
|
|
||||||
|
|
||||||
# Paket dependency manager
|
|
||||||
.paket/paket.exe
|
|
||||||
paket-files/
|
|
||||||
|
|
||||||
# FAKE - F# Make
|
|
||||||
.fake/
|
|
||||||
|
|
||||||
# CodeRush personal settings
|
|
||||||
.cr/personal
|
|
||||||
|
|
||||||
# Python Tools for Visual Studio (PTVS)
|
|
||||||
__pycache__/
|
|
||||||
*.pyc
|
|
||||||
|
|
||||||
# Cake - Uncomment if you are using it
|
|
||||||
# tools/**
|
|
||||||
# !tools/packages.config
|
|
||||||
|
|
||||||
# Tabs Studio
|
|
||||||
*.tss
|
|
||||||
|
|
||||||
# Telerik's JustMock configuration file
|
|
||||||
*.jmconfig
|
|
||||||
|
|
||||||
# BizTalk build output
|
|
||||||
*.btp.cs
|
|
||||||
*.btm.cs
|
|
||||||
*.odx.cs
|
|
||||||
*.xsd.cs
|
|
||||||
|
|
||||||
# OpenCover UI analysis results
|
|
||||||
OpenCover/
|
|
||||||
|
|
||||||
# Azure Stream Analytics local run output
|
|
||||||
ASALocalRun/
|
|
||||||
|
|
||||||
# MSBuild Binary and Structured Log
|
|
||||||
*.binlog
|
|
||||||
|
|
||||||
# NVidia Nsight GPU debugger configuration file
|
|
||||||
*.nvuser
|
|
||||||
|
|
||||||
# MFractors (Xamarin productivity tool) working folder
|
|
||||||
.mfractor/
|
|
||||||
|
|
||||||
# Local History for Visual Studio
|
|
||||||
.localhistory/
|
|
||||||
|
|
||||||
# Visual Studio History (VSHistory) files
|
|
||||||
.vshistory/
|
|
||||||
|
|
||||||
# BeatPulse healthcheck temp database
|
|
||||||
healthchecksdb
|
|
||||||
|
|
||||||
# Backup folder for Package Reference Convert tool in Visual Studio 2017
|
|
||||||
MigrationBackup/
|
|
||||||
|
|
||||||
# Ionide (cross platform F# VS Code tools) working folder
|
|
||||||
.ionide/
|
|
||||||
|
|
||||||
# Fody - auto-generated XML schema
|
|
||||||
FodyWeavers.xsd
|
|
||||||
|
|
||||||
# VS Code files for those working on multiple tools
|
|
||||||
.vscode/*
|
|
||||||
!.vscode/settings.json
|
|
||||||
!.vscode/tasks.json
|
|
||||||
!.vscode/launch.json
|
|
||||||
!.vscode/extensions.json
|
|
||||||
*.code-workspace
|
|
||||||
|
|
||||||
# Local History for Visual Studio Code
|
|
||||||
.history/
|
|
||||||
|
|
||||||
# Windows Installer files from build outputs
|
|
||||||
*.cab
|
|
||||||
*.msi
|
|
||||||
*.msix
|
|
||||||
*.msm
|
|
||||||
*.msp
|
|
||||||
|
|
||||||
# JetBrains Rider
|
|
||||||
*.sln.iml
|
|
||||||
.idea/
|
|
||||||
|
|
||||||
# ============================================
|
|
||||||
# AutoBidder Specific
|
|
||||||
# ============================================
|
|
||||||
|
|
||||||
# Database files (local development)
|
|
||||||
*.db
|
|
||||||
*.db-shm
|
|
||||||
*.db-wal
|
|
||||||
data/*.db
|
|
||||||
data/*.db-*
|
|
||||||
|
|
||||||
# Backups (keep structure, ignore files)
|
|
||||||
data/backups/*.db
|
|
||||||
data/backups/*.json
|
|
||||||
|
|
||||||
# Logs
|
|
||||||
logs/*.log
|
|
||||||
logs/*.txt
|
|
||||||
*.log
|
|
||||||
|
|
||||||
# Environment files with secrets
|
|
||||||
.env
|
|
||||||
.env.local
|
|
||||||
.env.*.local
|
|
||||||
|
|
||||||
# Certificates and keys
|
|
||||||
*.pfx
|
|
||||||
*.key
|
|
||||||
*.crt
|
|
||||||
*.pem
|
|
||||||
cert/*
|
|
||||||
!cert/.gitkeep
|
|
||||||
|
|
||||||
# Docker volumes data
|
|
||||||
test-data/
|
|
||||||
|
|
||||||
# Published artifacts
|
|
||||||
publish/
|
|
||||||
PublishProfiles/
|
|
||||||
|
|
||||||
# Temp directories
|
|
||||||
temp/
|
|
||||||
tmp/
|
|
||||||
|
|
||||||
# OS files
|
|
||||||
.DS_Store
|
|
||||||
.DS_Store?
|
|
||||||
._*
|
|
||||||
.Spotlight-V100
|
|
||||||
.Trashes
|
|
||||||
ehthumbs.db
|
|
||||||
Thumbs.db
|
|
||||||
|
|
||||||
# Keep important empty directories
|
|
||||||
!data/.gitkeep
|
|
||||||
!data/backups/.gitkeep
|
|
||||||
!logs/.gitkeep
|
|
||||||
!cert/.gitkeep
|
|
||||||
Vendored
+23
@@ -0,0 +1,23 @@
|
|||||||
|
{
|
||||||
|
"version": "0.2.0",
|
||||||
|
"configurations": [
|
||||||
|
{
|
||||||
|
"name": "Debug AutoBidder (WPF)",
|
||||||
|
"type": "coreclr",
|
||||||
|
"request": "launch",
|
||||||
|
"preLaunchTask": "build",
|
||||||
|
"program": "${workspaceFolder}/bin/Debug/net10.0-windows/AutoBidder.exe",
|
||||||
|
"args": [],
|
||||||
|
"cwd": "${workspaceFolder}",
|
||||||
|
"console": "internalConsole",
|
||||||
|
"stopAtEntry": false,
|
||||||
|
"enableStepFiltering": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Attach to AutoBidder (processo già avviato)",
|
||||||
|
"type": "coreclr",
|
||||||
|
"request": "attach",
|
||||||
|
"processName": "AutoBidder.exe"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
Vendored
+162
@@ -0,0 +1,162 @@
|
|||||||
|
{
|
||||||
|
// Tutte le attività passano da build/Release.proj: la catena è un solo file
|
||||||
|
// MSBuild versionato col codice, e qui restano soltanto i nomi e le domande.
|
||||||
|
// MSBuild non può chiedere niente a nessuno — i prompt stanno in "inputs".
|
||||||
|
"version": "2.0.0",
|
||||||
|
"tasks": [
|
||||||
|
{
|
||||||
|
"label": "build",
|
||||||
|
"detail": "Compilazione di debug, per F5 e per il controllo rapido degli errori.",
|
||||||
|
"command": "dotnet",
|
||||||
|
"type": "process",
|
||||||
|
"args": [
|
||||||
|
"build",
|
||||||
|
"${workspaceFolder}/AutoBidder.csproj",
|
||||||
|
"-c",
|
||||||
|
"Debug",
|
||||||
|
"/property:GenerateFullPaths=true",
|
||||||
|
"/consoleloggerparameters:NoSummary"
|
||||||
|
],
|
||||||
|
"group": {
|
||||||
|
"kind": "build",
|
||||||
|
"isDefault": true
|
||||||
|
},
|
||||||
|
"problemMatcher": "$msCompile"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"label": "verifica",
|
||||||
|
"detail": "Compila e lancia i test. Da eseguire dopo ogni modifica.",
|
||||||
|
"type": "process",
|
||||||
|
"command": "dotnet",
|
||||||
|
"args": [
|
||||||
|
"msbuild",
|
||||||
|
"${workspaceFolder}/build/Release.proj",
|
||||||
|
"-t:Verifica",
|
||||||
|
"-nologo",
|
||||||
|
"-v:m"
|
||||||
|
],
|
||||||
|
"group": {
|
||||||
|
"kind": "test",
|
||||||
|
"isDefault": true
|
||||||
|
},
|
||||||
|
"problemMatcher": "$msCompile"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"label": "backtest",
|
||||||
|
"detail": "Rigioca i dossier delle aste concluse: quante puntate costerebbe ciascun anticipo, e quante ne fermano i controlli di budget.",
|
||||||
|
"type": "process",
|
||||||
|
"command": "dotnet",
|
||||||
|
"args": [
|
||||||
|
"msbuild",
|
||||||
|
"${workspaceFolder}/build/Release.proj",
|
||||||
|
"-t:Backtest",
|
||||||
|
"-nologo",
|
||||||
|
"-v:m"
|
||||||
|
],
|
||||||
|
"presentation": {
|
||||||
|
"reveal": "always",
|
||||||
|
"panel": "dedicated"
|
||||||
|
},
|
||||||
|
"problemMatcher": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"label": "backtest (giro veloce, 400 aste)",
|
||||||
|
"detail": "Come sopra ma su un campione: utile mentre si tarano le strategie.",
|
||||||
|
"type": "process",
|
||||||
|
"command": "dotnet",
|
||||||
|
"args": [
|
||||||
|
"msbuild",
|
||||||
|
"${workspaceFolder}/build/Release.proj",
|
||||||
|
"-t:Backtest",
|
||||||
|
"-p:MaxDossier=400",
|
||||||
|
"-nologo",
|
||||||
|
"-v:m"
|
||||||
|
],
|
||||||
|
"presentation": {
|
||||||
|
"reveal": "always",
|
||||||
|
"panel": "dedicated"
|
||||||
|
},
|
||||||
|
"problemMatcher": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"label": "crea installatore",
|
||||||
|
"detail": "Verifica, pubblica ed esegue Inno Setup: bin/installer/AutoBidder-<versione>-setup.exe. Crea il tag a pacchetto pronto. Non tocca Gitea.",
|
||||||
|
"type": "process",
|
||||||
|
"command": "dotnet",
|
||||||
|
"args": [
|
||||||
|
"msbuild",
|
||||||
|
"${workspaceFolder}/build/Release.proj",
|
||||||
|
"-t:Pacchetto",
|
||||||
|
"-p:Versione=${input:versione}",
|
||||||
|
"-nologo",
|
||||||
|
"-v:m"
|
||||||
|
],
|
||||||
|
"presentation": {
|
||||||
|
"reveal": "always",
|
||||||
|
"panel": "dedicated"
|
||||||
|
},
|
||||||
|
"problemMatcher": "$msCompile"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"label": "crea installatore (senza rieseguire i test)",
|
||||||
|
"detail": "Solo pubblicazione e Inno Setup. Da usare quando i test sono appena passati.",
|
||||||
|
"type": "process",
|
||||||
|
"command": "dotnet",
|
||||||
|
"args": [
|
||||||
|
"msbuild",
|
||||||
|
"${workspaceFolder}/build/Release.proj",
|
||||||
|
"-t:Pacchetto",
|
||||||
|
"-p:Versione=${input:versione}",
|
||||||
|
"-p:SaltaVerifica=true",
|
||||||
|
"-nologo",
|
||||||
|
"-v:m"
|
||||||
|
],
|
||||||
|
"presentation": {
|
||||||
|
"reveal": "always",
|
||||||
|
"panel": "dedicated"
|
||||||
|
},
|
||||||
|
"problemMatcher": "$msCompile"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"label": "rilascia su Gitea",
|
||||||
|
"detail": "Verifica, pubblica, installatore, tag e release su Gitea con i file allegati. La versione viene dal tag su HEAD. Richiede build/gitea.json.",
|
||||||
|
"type": "process",
|
||||||
|
"command": "dotnet",
|
||||||
|
"args": [
|
||||||
|
"msbuild",
|
||||||
|
"${workspaceFolder}/build/Release.proj",
|
||||||
|
"-t:Rilascia",
|
||||||
|
"-p:Versione=${input:versione}",
|
||||||
|
"-nologo",
|
||||||
|
"-v:m"
|
||||||
|
],
|
||||||
|
// Le note passano dall'ambiente, non da -p:. MSBuild spezza il valore di
|
||||||
|
// una proprietà sulle virgole e una nota in italiano ne ha quasi sempre
|
||||||
|
// una: si otterrebbe MSB1006 «proprietà non valida». Vedi Release.proj.
|
||||||
|
"options": {
|
||||||
|
"env": {
|
||||||
|
"AUTOBIDDER_NOTE": "${input:note}"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"presentation": {
|
||||||
|
"reveal": "always",
|
||||||
|
"panel": "dedicated"
|
||||||
|
},
|
||||||
|
"problemMatcher": "$msCompile"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"inputs": [
|
||||||
|
{
|
||||||
|
"id": "versione",
|
||||||
|
"type": "promptString",
|
||||||
|
"description": "Versione — lascia vuoto se hai già taggato (git tag v4.14.0), o per la minor successiva",
|
||||||
|
"default": ""
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "note",
|
||||||
|
"type": "promptString",
|
||||||
|
"description": "Note di rilascio (vuoto = solo il numero di versione)",
|
||||||
|
"default": ""
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
@@ -1,36 +0,0 @@
|
|||||||
<CascadingAuthenticationState>
|
|
||||||
<Router AppAssembly="@typeof(App).Assembly">
|
|
||||||
<Found Context="routeData">
|
|
||||||
<AuthorizeRouteView RouteData="@routeData" DefaultLayout="@typeof(MainLayout)">
|
|
||||||
<NotAuthorized>
|
|
||||||
@if (context.User.Identity?.IsAuthenticated != true)
|
|
||||||
{
|
|
||||||
<RedirectToLogin />
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
<p>Non sei autorizzato ad accedere a questa risorsa.</p>
|
|
||||||
}
|
|
||||||
</NotAuthorized>
|
|
||||||
</AuthorizeRouteView>
|
|
||||||
<FocusOnNavigate RouteData="@routeData" Selector="h1" />
|
|
||||||
</Found>
|
|
||||||
<NotFound>
|
|
||||||
<PageTitle>Non trovato</PageTitle>
|
|
||||||
<LayoutView Layout="@typeof(MainLayout)">
|
|
||||||
<div style="padding: 2rem; text-align: center;">
|
|
||||||
<svg style="width: 64px; height: 64px; margin-bottom: 1rem; opacity: 0.5;" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
|
||||||
<circle cx="12" cy="12" r="10"></circle>
|
|
||||||
<line x1="12" y1="8" x2="12" y2="12"></line>
|
|
||||||
<line x1="12" y1="16" x2="12.01" y2="16"></line>
|
|
||||||
</svg>
|
|
||||||
<h1 style="font-size: 1.5rem; margin-bottom: 0.5rem;">Pagina non trovata</h1>
|
|
||||||
<p style="color: var(--text-muted);">Spiacenti, non c'è nulla a questo indirizzo.</p>
|
|
||||||
<a href="/" style="color: var(--primary-color); text-decoration: none; margin-top: 1rem; display: inline-block;">
|
|
||||||
?? Torna alla Home
|
|
||||||
</a>
|
|
||||||
</div>
|
|
||||||
</LayoutView>
|
|
||||||
</NotFound>
|
|
||||||
</Router>
|
|
||||||
</CascadingAuthenticationState>
|
|
||||||
+8
-21
@@ -1,29 +1,16 @@
|
|||||||
<Application x:Class="Mimante.App"
|
<Application x:Class="AutoBidder.App"
|
||||||
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
|
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
|
||||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||||
xmlns:local="clr-namespace:Mimante"
|
xmlns:local="clr-namespace:AutoBidder"
|
||||||
StartupUri="MainWindow.xaml">
|
StartupUri="MainWindow.xaml">
|
||||||
<Application.Resources>
|
<Application.Resources>
|
||||||
<ResourceDictionary>
|
<ResourceDictionary>
|
||||||
<!-- Stile pulsanti globale -->
|
<ResourceDictionary.MergedDictionaries>
|
||||||
<Style x:Key="SmallButtonStyle" TargetType="Button">
|
<!-- [0] SLOT TEMA: sostituito a runtime da ThemeManager (Dark/Light) -->
|
||||||
<Setter Property="Foreground" Value="White" />
|
<ResourceDictionary Source="Themes/Tokens.Dark.xaml"/>
|
||||||
<Setter Property="FontWeight" Value="SemiBold" />
|
<!-- [1] Stili controlli (usano DynamicResource sui token) -->
|
||||||
<Setter Property="FontSize" Value="12" />
|
<ResourceDictionary Source="Themes/Controls.xaml"/>
|
||||||
<Setter Property="BorderThickness" Value="0" />
|
</ResourceDictionary.MergedDictionaries>
|
||||||
<Setter Property="Cursor" Value="Hand" />
|
|
||||||
<Setter Property="Template">
|
|
||||||
<Setter.Value>
|
|
||||||
<ControlTemplate TargetType="Button">
|
|
||||||
<Border Background="{TemplateBinding Background}"
|
|
||||||
CornerRadius="12"
|
|
||||||
Padding="{TemplateBinding Padding}">
|
|
||||||
<ContentPresenter HorizontalAlignment="Center" VerticalAlignment="Center" />
|
|
||||||
</Border>
|
|
||||||
</ControlTemplate>
|
|
||||||
</Setter.Value>
|
|
||||||
</Setter>
|
|
||||||
</Style>
|
|
||||||
</ResourceDictionary>
|
</ResourceDictionary>
|
||||||
</Application.Resources>
|
</Application.Resources>
|
||||||
</Application>
|
</Application>
|
||||||
|
|||||||
+68
-4
@@ -1,14 +1,78 @@
|
|||||||
using System.Configuration;
|
using System;
|
||||||
using System.Data;
|
|
||||||
using System.Windows;
|
using System.Windows;
|
||||||
|
using AutoBidder.Utilities;
|
||||||
|
|
||||||
namespace Mimante
|
namespace AutoBidder
|
||||||
{
|
{
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Interaction logic for App.xaml
|
/// Interaction logic for App.xaml
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public partial class App : Application
|
public partial class App : Application
|
||||||
{
|
{
|
||||||
}
|
protected override void OnStartup(StartupEventArgs e)
|
||||||
|
{
|
||||||
|
base.OnStartup(e);
|
||||||
|
|
||||||
|
// Le descrizioni delle impostazioni vivono nei suggerimenti invece che stampate
|
||||||
|
// sotto ogni campo. Quello predefinito di Windows sparisce dopo cinque secondi:
|
||||||
|
// per un paragrafo di tre righe significa doverlo rincorrere.
|
||||||
|
System.Windows.Controls.ToolTipService.ShowDurationProperty.OverrideMetadata(
|
||||||
|
typeof(DependencyObject), new FrameworkPropertyMetadata(60000));
|
||||||
|
|
||||||
|
// I percorsi vanno decisi per primi: da qui in poi ogni archivio li usa.
|
||||||
|
// Le impostazioni stanno nella radice fissa, quindi si possono leggere anche
|
||||||
|
// prima di sapere dove finiranno gli altri dati.
|
||||||
|
var settings = SettingsManager.Load();
|
||||||
|
AppPaths.Configure(settings.DataFolder, settings.StatsFolder, settings.LogFolder);
|
||||||
|
AppPaths.EnsureFolders();
|
||||||
|
|
||||||
|
// Le versioni precedenti scrivevano parte dei dati in Roaming e parte in Local,
|
||||||
|
// senza sottocartelle: senza questo passaggio storico e aste sparirebbero.
|
||||||
|
AppPaths.MigrateLegacyFiles();
|
||||||
|
|
||||||
|
// I registri su file partono subito: quello che succede durante l'avvio è
|
||||||
|
// proprio ciò che non si riesce a leggere a video.
|
||||||
|
TextLogService.SessionStarted(AppInfo.Version);
|
||||||
|
TextLogService.PurgeOldLogs();
|
||||||
|
|
||||||
|
// Le righe di registro delle aste confluiscono nei rispettivi dossier.
|
||||||
|
AuctionDossier.CaptureAuctionLogs();
|
||||||
|
|
||||||
|
// Lo storico e' cresciuto per mesi con versioni diverse dell'applicazione:
|
||||||
|
// alcuni record hanno campi che allora non esistevano. Quelli deducibili si
|
||||||
|
// ricalcolano qui, una volta, prima che qualcuno li legga. Quelli NON
|
||||||
|
// deducibili non si toccano: se ne occupa la pulizia, che chiede all'utente.
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var repair = CompletedAuctionsStore.RepairOnStartup();
|
||||||
|
if (repair.AnythingDone)
|
||||||
|
{
|
||||||
|
TextLogService.App("INFO", $"[STORICO] Riparati {repair.Total} campi mancanti " +
|
||||||
|
$"(chiavi prodotto {repair.ProductKeys}, esiti {repair.Outcomes}, " +
|
||||||
|
$"nomi {repair.Names})");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
TextLogService.App("WARN", $"[STORICO] Riparazione non riuscita: {ex.Message}");
|
||||||
|
}
|
||||||
|
|
||||||
|
// Applica il tema salvato (chiaro/scuro) prima di mostrare la finestra
|
||||||
|
ThemeManager.ApplyFromSettings();
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override void OnExit(ExitEventArgs e)
|
||||||
|
{
|
||||||
|
// Quello che è ancora in coda va sul disco adesso: un registro che perde
|
||||||
|
// proprio le ultime righe è inutile esattamente quando serve di più.
|
||||||
|
TextLogService.SessionEnded();
|
||||||
|
FileLogWriter.DisposeAll();
|
||||||
|
|
||||||
|
// L'icona di notifica sopravvive al processo se non la si rimuove:
|
||||||
|
// resterebbe nell'area di notifica come icona fantasma.
|
||||||
|
WindowsNotifier.Dispose();
|
||||||
|
|
||||||
|
base.OnExit(e);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+35
-145
@@ -1,166 +1,56 @@
|
|||||||
<Project Sdk="Microsoft.NET.Sdk.Web">
|
<Project Sdk="Microsoft.NET.Sdk">
|
||||||
|
|
||||||
<PropertyGroup>
|
<PropertyGroup>
|
||||||
<TargetFramework>net10.0</TargetFramework>
|
<OutputType>WinExe</OutputType>
|
||||||
|
<TargetFramework>net10.0-windows</TargetFramework>
|
||||||
<Nullable>enable</Nullable>
|
<Nullable>enable</Nullable>
|
||||||
<ImplicitUsings>enable</ImplicitUsings>
|
<ImplicitUsings>enable</ImplicitUsings>
|
||||||
|
<UseWPF>true</UseWPF>
|
||||||
|
<!-- Solo per NotifyIcon: le notifiche moderne richiederebbero un'applicazione
|
||||||
|
registrata, incompatibile con l'avvio da singolo eseguibile. -->
|
||||||
|
<UseWindowsForms>true</UseWindowsForms>
|
||||||
<AssemblyName>AutoBidder</AssemblyName>
|
<AssemblyName>AutoBidder</AssemblyName>
|
||||||
<RootNamespace>AutoBidder</RootNamespace>
|
<RootNamespace>AutoBidder</RootNamespace>
|
||||||
<DockerDefaultTargetOS>Linux</DockerDefaultTargetOS>
|
<ApplicationIcon>Icon\favicon.ico</ApplicationIcon>
|
||||||
<DockerfileContext>.</DockerfileContext>
|
|
||||||
<DockerfileFile>Dockerfile</DockerfileFile>
|
<!-- App desktop leggera per Bidoo. Nessun software aggiuntivo:
|
||||||
|
WebView2 runtime è preinstallato su Windows 11. -->
|
||||||
<!-- Versioning per Docker & Gitea Registry -->
|
<!-- Versione delle sole compilazioni di sviluppo. NON va alzata per rilasciare:
|
||||||
<!-- v1.3.0: Database management + bug fixes (duplicates, race conditions, warnings) -->
|
nei pacchetti pubblicati questi quattro numeri vengono dal tag git, passati
|
||||||
<Version>1.3.0</Version>
|
da build/Release.proj a dotnet publish. Vedi Utilities/AppInfo. -->
|
||||||
<AssemblyVersion>1.3.0.0</AssemblyVersion>
|
<Version>4.13.0</Version>
|
||||||
<FileVersion>1.3.0.0</FileVersion>
|
<AssemblyVersion>4.13.0.0</AssemblyVersion>
|
||||||
<InformationalVersion>1.3.0</InformationalVersion>
|
<FileVersion>4.13.0.0</FileVersion>
|
||||||
|
<InformationalVersion>4.13.0</InformationalVersion>
|
||||||
<!-- Metadata immagine Docker -->
|
|
||||||
<ContainerImageName>autobidder</ContainerImageName>
|
<!-- Runtime unico supportato per la pubblicazione self-contained -->
|
||||||
<ContainerImageTag>$(Version)</ContainerImageTag>
|
<RuntimeIdentifiers>win-x64</RuntimeIdentifiers>
|
||||||
<!-- CORRETTO: Convenzione Gitea {registro}/{proprietario}/{immagine} -->
|
<SatelliteResourceLanguages>it;en</SatelliteResourceLanguages>
|
||||||
<ContainerRegistry>gitea.encke-hake.ts.net/alby96</ContainerRegistry>
|
|
||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<!-- Exclude test project from main build -->
|
<!-- Il progetto di test vive in una sottocartella: senza questa esclusione i
|
||||||
<Compile Remove="Tests\AutoBidder.Tests\**" />
|
progetti SDK-style, che raccolgono **/*.cs, lo compilerebbero qui dentro. -->
|
||||||
<Content Remove="Tests\AutoBidder.Tests\**" />
|
<Compile Remove="Tests\**" />
|
||||||
<EmbeddedResource Remove="Tests\AutoBidder.Tests\**" />
|
<None Remove="Tests\**" />
|
||||||
<None Remove="Tests\AutoBidder.Tests\**" />
|
|
||||||
|
|
||||||
<!-- Exclude WPF files from compilation -->
|
|
||||||
<Compile Remove=".github\**" />
|
|
||||||
<Compile Remove=".vscode\**" />
|
|
||||||
<Compile Remove="obj\**" />
|
|
||||||
<Compile Remove="Controls\**" />
|
|
||||||
<Compile Remove="Dialogs\**" />
|
|
||||||
<Compile Remove="Core\**" />
|
|
||||||
<Compile Remove="MainWindow.xaml.cs" />
|
|
||||||
<Compile Remove="App.xaml.cs" />
|
|
||||||
<Compile Remove="AssemblyInfo.cs" />
|
|
||||||
<Compile Remove="ViewModels\**" />
|
|
||||||
<Compile Remove="Utilities\NumericTextBoxHelper.cs" />
|
|
||||||
<Compile Remove="Utilities\BooleanToOpacityConverter.cs" />
|
|
||||||
<Compile Remove="Utilities\RelayCommand.cs" />
|
|
||||||
|
|
||||||
<Content Remove=".github\**" />
|
|
||||||
<Content Remove=".vscode\**" />
|
|
||||||
<Content Remove="obj\**" />
|
|
||||||
<Content Remove="Controls\**" />
|
|
||||||
<Content Remove="Dialogs\**" />
|
|
||||||
<Content Remove="**\*.xaml" />
|
|
||||||
|
|
||||||
<EmbeddedResource Remove=".github\**" />
|
|
||||||
<EmbeddedResource Remove=".vscode\**" />
|
|
||||||
<EmbeddedResource Remove="obj\**" />
|
|
||||||
<EmbeddedResource Remove="Controls\**" />
|
|
||||||
<EmbeddedResource Remove="Dialogs\**" />
|
|
||||||
|
|
||||||
<None Remove=".github\**" />
|
|
||||||
<None Remove=".vscode\**" />
|
|
||||||
<None Remove="obj\**" />
|
|
||||||
<None Remove="Controls\**" />
|
|
||||||
<None Remove="Dialogs\**" />
|
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<None Remove="Icon\favicon.ico" />
|
<!-- UseWindowsForms aggiunge questi using in tutti i file, e ogni nome in comune
|
||||||
|
con WPF (UserControl, TextBox, Application, Color...) diventa ambiguo.
|
||||||
|
Serve solo a WindowsNotifier, che li importa per conto proprio. -->
|
||||||
|
<Using Remove="System.Windows.Forms" />
|
||||||
|
<Using Remove="System.Drawing" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite" Version="10.0.5" />
|
<PackageReference Include="Microsoft.Web.WebView2" Version="1.0.3351.48" />
|
||||||
<PackageReference Include="Npgsql.EntityFrameworkCore.PostgreSQL" Version="10.0.1" />
|
|
||||||
<PackageReference Include="Microsoft.AspNetCore.Identity.EntityFrameworkCore" Version="10.0.5" />
|
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<Content Include="Icon\favicon.ico">
|
<!-- Incorporata come Resource (non Content) così l'icona della finestra
|
||||||
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
|
funziona anche in modalitàsingle-file self-contained. -->
|
||||||
</Content>
|
<Resource Include="Icon\favicon.ico" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
|
||||||
<None Include=".gitea\workflows\backup.yml" />
|
|
||||||
<None Include=".gitea\workflows\deploy.yml" />
|
|
||||||
<None Include=".gitea\workflows\health-check.yml" />
|
|
||||||
<None Include=".github\workflows\ci-cd.yml" />
|
|
||||||
<None Include="Dockerfile" />
|
|
||||||
<None Include=".dockerignore" />
|
|
||||||
<None Include="Properties\PublishProfiles\GiteaRegistry-Versioned.pubxml.user" />
|
|
||||||
<None Include="Tests\AutoBidder.Tests\AutoBidder.Tests.csproj" />
|
|
||||||
</ItemGroup>
|
|
||||||
|
|
||||||
<!-- ============================================ -->
|
|
||||||
<!-- POST-BUILD TARGET: Push automatico su Gitea -->
|
|
||||||
<!-- con versionamento da <Version> della solution -->
|
|
||||||
<!-- ============================================ -->
|
|
||||||
<Target Name="PushDockerImageToGitea" AfterTargets="Publish" Condition="'$(PushToGiteaRegistry)' == 'true'">
|
|
||||||
<PropertyGroup>
|
|
||||||
<GiteaRegistry>gitea.encke-hake.ts.net/alby96</GiteaRegistry>
|
|
||||||
<LocalImageName>autobidder</LocalImageName>
|
|
||||||
<GiteaImageLatest>$(GiteaRegistry)/$(LocalImageName):latest</GiteaImageLatest>
|
|
||||||
<GiteaImageVersion>$(GiteaRegistry)/$(LocalImageName):$(Version)</GiteaImageVersion>
|
|
||||||
</PropertyGroup>
|
|
||||||
|
|
||||||
<Message Importance="high" Text="" />
|
|
||||||
<Message Importance="high" Text="+-------------------------------------------------------------------+" />
|
|
||||||
<Message Importance="high" Text="� POST-BUILD: Pubblicazione su Gitea Container Registry �" />
|
|
||||||
<Message Importance="high" Text="+-------------------------------------------------------------------+" />
|
|
||||||
<Message Importance="high" Text="" />
|
|
||||||
<Message Importance="high" Text="?? Solution Version: $(Version)" />
|
|
||||||
<Message Importance="high" Text="?? Local Image: $(LocalImageName):latest" />
|
|
||||||
<Message Importance="high" Text="??? Target Tags:" />
|
|
||||||
<Message Importance="high" Text=" � $(GiteaImageLatest)" />
|
|
||||||
<Message Importance="high" Text=" � $(GiteaImageVersion)" />
|
|
||||||
<Message Importance="high" Text="" />
|
|
||||||
<Message Importance="high" Text="-------------------------------------------------------------------" />
|
|
||||||
<Message Importance="high" Text="??? Tagging images..." />
|
|
||||||
<Message Importance="high" Text="-------------------------------------------------------------------" />
|
|
||||||
|
|
||||||
<!-- Tag immagine locale per Gitea (latest) -->
|
|
||||||
<Exec Command="docker tag $(LocalImageName):latest $(GiteaImageLatest)" />
|
|
||||||
<Message Importance="high" Text="? Tagged: $(GiteaImageLatest)" />
|
|
||||||
|
|
||||||
<!-- Tag immagine locale per Gitea (versione solution) -->
|
|
||||||
<Exec Command="docker tag $(LocalImageName):latest $(GiteaImageVersion)" />
|
|
||||||
<Message Importance="high" Text="? Tagged: $(GiteaImageVersion)" />
|
|
||||||
|
|
||||||
<Message Importance="high" Text="" />
|
|
||||||
<Message Importance="high" Text="-------------------------------------------------------------------" />
|
|
||||||
<Message Importance="high" Text="?? Pushing to Gitea Registry..." />
|
|
||||||
<Message Importance="high" Text="-------------------------------------------------------------------" />
|
|
||||||
|
|
||||||
<!-- Push latest -->
|
|
||||||
<Exec Command="docker push $(GiteaImageLatest)" />
|
|
||||||
<Message Importance="high" Text="? Pushed: $(GiteaImageLatest)" />
|
|
||||||
|
|
||||||
<!-- Push version -->
|
|
||||||
<Exec Command="docker push $(GiteaImageVersion)" />
|
|
||||||
<Message Importance="high" Text="? Pushed: $(GiteaImageVersion)" />
|
|
||||||
|
|
||||||
<Message Importance="high" Text="" />
|
|
||||||
<Message Importance="high" Text="+-------------------------------------------------------------------+" />
|
|
||||||
<Message Importance="high" Text="� ? PUBBLICAZIONE COMPLETATA CON SUCCESSO! �" />
|
|
||||||
<Message Importance="high" Text="+-------------------------------------------------------------------+" />
|
|
||||||
<Message Importance="high" Text="" />
|
|
||||||
<Message Importance="high" Text="?? Visualizza su Gitea:" />
|
|
||||||
<Message Importance="high" Text=" https://gitea.encke-hake.ts.net/Alby96/-/packages/container/autobidder" />
|
|
||||||
<Message Importance="high" Text="" />
|
|
||||||
<Message Importance="high" Text="?? Tag pubblicati:" />
|
|
||||||
<Message Importance="high" Text=" � latest (sempre aggiornato all'ultima versione)" />
|
|
||||||
<Message Importance="high" Text=" � $(Version) (versione solution corrente)" />
|
|
||||||
<Message Importance="high" Text="" />
|
|
||||||
<Message Importance="high" Text="?? Pull command:" />
|
|
||||||
<Message Importance="high" Text=" docker pull $(GiteaImageLatest)" />
|
|
||||||
<Message Importance="high" Text=" docker pull $(GiteaImageVersion)" />
|
|
||||||
<Message Importance="high" Text="" />
|
|
||||||
<Message Importance="high" Text="-------------------------------------------------------------------" />
|
|
||||||
<Message Importance="high" Text="" />
|
|
||||||
</Target>
|
|
||||||
|
|
||||||
</Project>
|
</Project>
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
+16
-36
@@ -1,74 +1,54 @@
|
|||||||
|
|
||||||
Microsoft Visual Studio Solution File, Format Version 12.00
|
Microsoft Visual Studio Solution File, Format Version 12.00
|
||||||
# Visual Studio Version 18
|
# Visual Studio Version 17
|
||||||
VisualStudioVersion = 18.0.11217.181
|
VisualStudioVersion = 17.0.31903.59
|
||||||
MinimumVisualStudioVersion = 10.0.40219.1
|
MinimumVisualStudioVersion = 10.0.40219.1
|
||||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "AutoBidder", "AutoBidder.csproj", "{9BBAEF93-DF66-432C-9349-459E272D6538}"
|
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "AutoBidder", "AutoBidder.csproj", "{9BBAEF93-DF66-432C-9349-459E272D6538}"
|
||||||
EndProject
|
EndProject
|
||||||
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Tests", "Tests", "{0AB3BF05-4346-4AA6-1389-037BE0695223}"
|
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Tests", "Tests", "{0AB3BF05-4346-4AA6-1389-037BE0695223}"
|
||||||
EndProject
|
EndProject
|
||||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "AutoBidder.Tests", "Tests\AutoBidder.Tests\AutoBidder.Tests.csproj", "{A54E2BBB-8921-428F-A9B4-C042ABD1B584}"
|
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "AutoBidder.Tests", "Tests\AutoBidder.Tests.csproj", "{CAC096C0-E399-4268-B2FF-E205947CB733}"
|
||||||
EndProject
|
EndProject
|
||||||
Global
|
Global
|
||||||
GlobalSection(SolutionConfigurationPlatforms) = preSolution
|
GlobalSection(SolutionConfigurationPlatforms) = preSolution
|
||||||
Debug|Any CPU = Debug|Any CPU
|
Debug|Any CPU = Debug|Any CPU
|
||||||
Debug|ARM = Debug|ARM
|
|
||||||
Debug|ARM64 = Debug|ARM64
|
|
||||||
Debug|x64 = Debug|x64
|
Debug|x64 = Debug|x64
|
||||||
Debug|x86 = Debug|x86
|
Debug|x86 = Debug|x86
|
||||||
Release|Any CPU = Release|Any CPU
|
Release|Any CPU = Release|Any CPU
|
||||||
Release|ARM = Release|ARM
|
|
||||||
Release|ARM64 = Release|ARM64
|
|
||||||
Release|x64 = Release|x64
|
Release|x64 = Release|x64
|
||||||
Release|x86 = Release|x86
|
Release|x86 = Release|x86
|
||||||
EndGlobalSection
|
EndGlobalSection
|
||||||
GlobalSection(ProjectConfigurationPlatforms) = postSolution
|
GlobalSection(ProjectConfigurationPlatforms) = postSolution
|
||||||
{9BBAEF93-DF66-432C-9349-459E272D6538}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
{9BBAEF93-DF66-432C-9349-459E272D6538}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||||
{9BBAEF93-DF66-432C-9349-459E272D6538}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
{9BBAEF93-DF66-432C-9349-459E272D6538}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||||
{9BBAEF93-DF66-432C-9349-459E272D6538}.Debug|ARM.ActiveCfg = Debug|Any CPU
|
|
||||||
{9BBAEF93-DF66-432C-9349-459E272D6538}.Debug|ARM.Build.0 = Debug|Any CPU
|
|
||||||
{9BBAEF93-DF66-432C-9349-459E272D6538}.Debug|ARM64.ActiveCfg = Debug|Any CPU
|
|
||||||
{9BBAEF93-DF66-432C-9349-459E272D6538}.Debug|ARM64.Build.0 = Debug|Any CPU
|
|
||||||
{9BBAEF93-DF66-432C-9349-459E272D6538}.Debug|x64.ActiveCfg = Debug|Any CPU
|
{9BBAEF93-DF66-432C-9349-459E272D6538}.Debug|x64.ActiveCfg = Debug|Any CPU
|
||||||
{9BBAEF93-DF66-432C-9349-459E272D6538}.Debug|x64.Build.0 = Debug|Any CPU
|
{9BBAEF93-DF66-432C-9349-459E272D6538}.Debug|x64.Build.0 = Debug|Any CPU
|
||||||
{9BBAEF93-DF66-432C-9349-459E272D6538}.Debug|x86.ActiveCfg = Debug|Any CPU
|
{9BBAEF93-DF66-432C-9349-459E272D6538}.Debug|x86.ActiveCfg = Debug|Any CPU
|
||||||
{9BBAEF93-DF66-432C-9349-459E272D6538}.Debug|x86.Build.0 = Debug|Any CPU
|
{9BBAEF93-DF66-432C-9349-459E272D6538}.Debug|x86.Build.0 = Debug|Any CPU
|
||||||
{9BBAEF93-DF66-432C-9349-459E272D6538}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
{9BBAEF93-DF66-432C-9349-459E272D6538}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||||
{9BBAEF93-DF66-432C-9349-459E272D6538}.Release|Any CPU.Build.0 = Release|Any CPU
|
{9BBAEF93-DF66-432C-9349-459E272D6538}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||||
{9BBAEF93-DF66-432C-9349-459E272D6538}.Release|ARM.ActiveCfg = Release|Any CPU
|
|
||||||
{9BBAEF93-DF66-432C-9349-459E272D6538}.Release|ARM.Build.0 = Release|Any CPU
|
|
||||||
{9BBAEF93-DF66-432C-9349-459E272D6538}.Release|ARM64.ActiveCfg = Release|Any CPU
|
|
||||||
{9BBAEF93-DF66-432C-9349-459E272D6538}.Release|ARM64.Build.0 = Release|Any CPU
|
|
||||||
{9BBAEF93-DF66-432C-9349-459E272D6538}.Release|x64.ActiveCfg = Release|Any CPU
|
{9BBAEF93-DF66-432C-9349-459E272D6538}.Release|x64.ActiveCfg = Release|Any CPU
|
||||||
{9BBAEF93-DF66-432C-9349-459E272D6538}.Release|x64.Build.0 = Release|Any CPU
|
{9BBAEF93-DF66-432C-9349-459E272D6538}.Release|x64.Build.0 = Release|Any CPU
|
||||||
{9BBAEF93-DF66-432C-9349-459E272D6538}.Release|x86.ActiveCfg = Release|Any CPU
|
{9BBAEF93-DF66-432C-9349-459E272D6538}.Release|x86.ActiveCfg = Release|Any CPU
|
||||||
{9BBAEF93-DF66-432C-9349-459E272D6538}.Release|x86.Build.0 = Release|Any CPU
|
{9BBAEF93-DF66-432C-9349-459E272D6538}.Release|x86.Build.0 = Release|Any CPU
|
||||||
{A54E2BBB-8921-428F-A9B4-C042ABD1B584}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
{CAC096C0-E399-4268-B2FF-E205947CB733}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||||
{A54E2BBB-8921-428F-A9B4-C042ABD1B584}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
{CAC096C0-E399-4268-B2FF-E205947CB733}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||||
{A54E2BBB-8921-428F-A9B4-C042ABD1B584}.Debug|ARM.ActiveCfg = Debug|Any CPU
|
{CAC096C0-E399-4268-B2FF-E205947CB733}.Debug|x64.ActiveCfg = Debug|Any CPU
|
||||||
{A54E2BBB-8921-428F-A9B4-C042ABD1B584}.Debug|ARM.Build.0 = Debug|Any CPU
|
{CAC096C0-E399-4268-B2FF-E205947CB733}.Debug|x64.Build.0 = Debug|Any CPU
|
||||||
{A54E2BBB-8921-428F-A9B4-C042ABD1B584}.Debug|ARM64.ActiveCfg = Debug|Any CPU
|
{CAC096C0-E399-4268-B2FF-E205947CB733}.Debug|x86.ActiveCfg = Debug|Any CPU
|
||||||
{A54E2BBB-8921-428F-A9B4-C042ABD1B584}.Debug|ARM64.Build.0 = Debug|Any CPU
|
{CAC096C0-E399-4268-B2FF-E205947CB733}.Debug|x86.Build.0 = Debug|Any CPU
|
||||||
{A54E2BBB-8921-428F-A9B4-C042ABD1B584}.Debug|x64.ActiveCfg = Debug|Any CPU
|
{CAC096C0-E399-4268-B2FF-E205947CB733}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||||
{A54E2BBB-8921-428F-A9B4-C042ABD1B584}.Debug|x64.Build.0 = Debug|Any CPU
|
{CAC096C0-E399-4268-B2FF-E205947CB733}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||||
{A54E2BBB-8921-428F-A9B4-C042ABD1B584}.Debug|x86.ActiveCfg = Debug|Any CPU
|
{CAC096C0-E399-4268-B2FF-E205947CB733}.Release|x64.ActiveCfg = Release|Any CPU
|
||||||
{A54E2BBB-8921-428F-A9B4-C042ABD1B584}.Debug|x86.Build.0 = Debug|Any CPU
|
{CAC096C0-E399-4268-B2FF-E205947CB733}.Release|x64.Build.0 = Release|Any CPU
|
||||||
{A54E2BBB-8921-428F-A9B4-C042ABD1B584}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
{CAC096C0-E399-4268-B2FF-E205947CB733}.Release|x86.ActiveCfg = Release|Any CPU
|
||||||
{A54E2BBB-8921-428F-A9B4-C042ABD1B584}.Release|Any CPU.Build.0 = Release|Any CPU
|
{CAC096C0-E399-4268-B2FF-E205947CB733}.Release|x86.Build.0 = Release|Any CPU
|
||||||
{A54E2BBB-8921-428F-A9B4-C042ABD1B584}.Release|ARM.ActiveCfg = Release|Any CPU
|
|
||||||
{A54E2BBB-8921-428F-A9B4-C042ABD1B584}.Release|ARM.Build.0 = Release|Any CPU
|
|
||||||
{A54E2BBB-8921-428F-A9B4-C042ABD1B584}.Release|ARM64.ActiveCfg = Release|Any CPU
|
|
||||||
{A54E2BBB-8921-428F-A9B4-C042ABD1B584}.Release|ARM64.Build.0 = Release|Any CPU
|
|
||||||
{A54E2BBB-8921-428F-A9B4-C042ABD1B584}.Release|x64.ActiveCfg = Release|Any CPU
|
|
||||||
{A54E2BBB-8921-428F-A9B4-C042ABD1B584}.Release|x64.Build.0 = Release|Any CPU
|
|
||||||
{A54E2BBB-8921-428F-A9B4-C042ABD1B584}.Release|x86.ActiveCfg = Release|Any CPU
|
|
||||||
{A54E2BBB-8921-428F-A9B4-C042ABD1B584}.Release|x86.Build.0 = Release|Any CPU
|
|
||||||
EndGlobalSection
|
EndGlobalSection
|
||||||
GlobalSection(SolutionProperties) = preSolution
|
GlobalSection(SolutionProperties) = preSolution
|
||||||
HideSolutionNode = FALSE
|
HideSolutionNode = FALSE
|
||||||
EndGlobalSection
|
EndGlobalSection
|
||||||
GlobalSection(NestedProjects) = preSolution
|
GlobalSection(NestedProjects) = preSolution
|
||||||
{A54E2BBB-8921-428F-A9B4-C042ABD1B584} = {0AB3BF05-4346-4AA6-1389-037BE0695223}
|
{CAC096C0-E399-4268-B2FF-E205947CB733} = {0AB3BF05-4346-4AA6-1389-037BE0695223}
|
||||||
EndGlobalSection
|
EndGlobalSection
|
||||||
GlobalSection(ExtensibilityGlobals) = postSolution
|
GlobalSection(ExtensibilityGlobals) = postSolution
|
||||||
SolutionGuid = {1C55CA56-D270-4D9A-91DA-410BF131E905}
|
SolutionGuid = {1C55CA56-D270-4D9A-91DA-410BF131E905}
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -1,4 +1,4 @@
|
|||||||
using System.Windows;
|
using System.Windows;
|
||||||
using System.Windows.Controls;
|
using System.Windows.Controls;
|
||||||
using System.Windows.Input;
|
using System.Windows.Input;
|
||||||
|
|
||||||
@@ -87,6 +87,139 @@ namespace AutoBidder.Controls
|
|||||||
RaiseEvent(new RoutedEventArgs(ExportClickedEvent, this));
|
RaiseEvent(new RoutedEventArgs(ExportClickedEvent, this));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private void RemoveFinishedButton_Click(object sender, RoutedEventArgs e)
|
||||||
|
{
|
||||||
|
RaiseEvent(new RoutedEventArgs(RemoveFinishedClickedEvent, this));
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Aggiorna i contatori della barra strumenti. Li calcola il chiamante, che ha
|
||||||
|
/// l'elenco completo: qui si presentano soltanto.
|
||||||
|
/// </summary>
|
||||||
|
public void UpdateCounters(int total, int active, int watch, int stopped, int won, int lost)
|
||||||
|
{
|
||||||
|
PillTotal.Text = total == 1 ? "1 asta" : $"{total} aste";
|
||||||
|
PillActive.Text = $"{active} attive";
|
||||||
|
PillWatch.Text = $"{watch} osserva";
|
||||||
|
PillStopped.Text = $"{stopped} ferme";
|
||||||
|
PillWon.Text = $"{won} vinte";
|
||||||
|
PillLost.Text = $"{lost} perse";
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Stato del motore: traffico prodotto, aggancio all'orologio del server e ritardo
|
||||||
|
/// medio di rete. Tutti e tre i valori arrivano già misurati dal chiamante.
|
||||||
|
/// </summary>
|
||||||
|
public void UpdateEngineStatus(long requestsSent, bool clockSynced, int clockSamples, double avgPingMs)
|
||||||
|
{
|
||||||
|
RequestsText.Text = requestsSent.ToString("N0");
|
||||||
|
|
||||||
|
// Senza campioni non c'è nulla da dire: meglio un trattino di un numero finto.
|
||||||
|
if (clockSamples == 0)
|
||||||
|
{
|
||||||
|
ClockText.Text = "—";
|
||||||
|
SetClockTone("Brush.TextFaint");
|
||||||
|
ClockPill.ToolTip =
|
||||||
|
"Nessuna misura: il motore non sta interrogando Bidoo.\n" +
|
||||||
|
"Il pallino diventa verde quando l'orologio è agganciato e la rete risponde in fretta.";
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
ClockText.Text = avgPingMs > 0 ? $"{avgPingMs:F0} ms" : "—";
|
||||||
|
|
||||||
|
var tone = !clockSynced ? "Brush.Warning" : PingTone(avgPingMs);
|
||||||
|
SetClockTone(tone);
|
||||||
|
|
||||||
|
var sync = clockSynced
|
||||||
|
? $"Orologio agganciato al server ({clockSamples} campioni)."
|
||||||
|
: $"Orologio in aggancio: {clockSamples} campioni su 3.";
|
||||||
|
|
||||||
|
ClockPill.ToolTip =
|
||||||
|
$"{sync}\n" +
|
||||||
|
$"Ritardo medio di rete: {avgPingMs:F0} ms — {PingVerdict(avgPingMs)}.\n" +
|
||||||
|
"Tieni l'anticipo puntata sopra questo valore.";
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Soglie del ritardo di rete. Sono tarate su ciò che serve al motore: con un
|
||||||
|
/// anticipo tipico di qualche centinaio di millisecondi, oltre i 400 ms di
|
||||||
|
/// round-trip il margine per arrivare in tempo si assottiglia davvero.
|
||||||
|
/// </summary>
|
||||||
|
private static string PingTone(double ms) => ms switch
|
||||||
|
{
|
||||||
|
<= 0 => "Brush.TextFaint",
|
||||||
|
< 150 => "Brush.Success",
|
||||||
|
< 400 => "Brush.Warning",
|
||||||
|
_ => "Brush.Danger"
|
||||||
|
};
|
||||||
|
|
||||||
|
private static string PingVerdict(double ms) => ms switch
|
||||||
|
{
|
||||||
|
<= 0 => "non misurato",
|
||||||
|
< 150 => "rete pronta",
|
||||||
|
< 400 => "rete lenta, alza l'anticipo",
|
||||||
|
_ => "rete molto lenta, puntate a rischio"
|
||||||
|
};
|
||||||
|
|
||||||
|
private void SetClockTone(string brushKey)
|
||||||
|
{
|
||||||
|
ClockDot.SetResourceReference(ForegroundProperty, brushKey);
|
||||||
|
ClockText.SetResourceReference(ForegroundProperty, brushKey);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Stato del conto: puntate, credito e vincite da confermare.
|
||||||
|
///
|
||||||
|
/// <para>I valori arrivano annullabili di proposito: finché non si è connessi, o
|
||||||
|
/// finché una risposta non è arrivata, la barra mostra un trattino invece di uno
|
||||||
|
/// zero. Uno zero inventato è peggio di un dato mancante — è esattamente l'errore
|
||||||
|
/// per cui le aste da confermare risultavano sempre nessuna.</para>
|
||||||
|
/// </summary>
|
||||||
|
public void UpdateAccountStatus(int? remainingBids, decimal? shopCredit, int? auctionsToConfirm)
|
||||||
|
{
|
||||||
|
RemainingBidsText.Text = remainingBids?.ToString("N0") ?? "—";
|
||||||
|
|
||||||
|
ShopCreditText.Text = shopCredit.HasValue ? $"{shopCredit.Value:N2} €" : "— €";
|
||||||
|
|
||||||
|
if (auctionsToConfirm is > 0)
|
||||||
|
{
|
||||||
|
BannerAsteDaRiscattare.Text = auctionsToConfirm.Value.ToString("N0");
|
||||||
|
ToConfirmPill.Visibility = Visibility.Visible;
|
||||||
|
ToConfirmPill.ToolTip = auctionsToConfirm.Value == 1
|
||||||
|
? "1 asta vinta in attesa di conferma su Bidoo"
|
||||||
|
: $"{auctionsToConfirm.Value} aste vinte in attesa di conferma su Bidoo";
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
ToConfirmPill.Visibility = Visibility.Collapsed;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>Contatori del motore relativi alla sola asta selezionata.</summary>
|
||||||
|
public void UpdateSelectedStats(int resets, int myBids, double avgPingMs, long polls, long pollErrors)
|
||||||
|
{
|
||||||
|
StatResets.Text = resets.ToString();
|
||||||
|
StatMyBids.Text = myBids.ToString();
|
||||||
|
StatPing.Text = avgPingMs > 0 ? $"{avgPingMs:F0} ms" : "—";
|
||||||
|
StatPolls.Text = polls.ToString("N0");
|
||||||
|
StatPollErrors.Text = pollErrors.ToString("N0");
|
||||||
|
StatPollErrors.SetResourceReference(ForegroundProperty,
|
||||||
|
pollErrors > 0 ? "Brush.Warning" : "Brush.Text");
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>Verdetto di convenienza nella scheda Prodotto.</summary>
|
||||||
|
public void SetVerdict(string label, string value, string tone)
|
||||||
|
{
|
||||||
|
VerdictLabel.Text = label;
|
||||||
|
VerdictText.Text = value;
|
||||||
|
VerdictText.SetResourceReference(ForegroundProperty, tone switch
|
||||||
|
{
|
||||||
|
"ok" => "Brush.Success",
|
||||||
|
"danger" => "Brush.Danger",
|
||||||
|
_ => "Brush.Text"
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
private void MultiAuctionsGrid_SelectionChanged(object sender, SelectionChangedEventArgs e)
|
private void MultiAuctionsGrid_SelectionChanged(object sender, SelectionChangedEventArgs e)
|
||||||
{
|
{
|
||||||
// Forza il focus sul DataGrid quando viene selezionata una riga
|
// Forza il focus sul DataGrid quando viene selezionata una riga
|
||||||
@@ -115,14 +248,14 @@ namespace AutoBidder.Controls
|
|||||||
System.Diagnostics.Debug.WriteLine("[DELETE KEY] Tasto Canc premuto su asta selezionata");
|
System.Diagnostics.Debug.WriteLine("[DELETE KEY] Tasto Canc premuto su asta selezionata");
|
||||||
|
|
||||||
// Lancia direttamente l'evento senza chiedere conferma
|
// Lancia direttamente l'evento senza chiedere conferma
|
||||||
// La conferma verrà mostrata dal gestore RemoveUrlButton_Click
|
// La conferma verr� mostrata dal gestore RemoveUrlButton_Click
|
||||||
System.Diagnostics.Debug.WriteLine("[DELETE KEY] Lancio evento RemoveUrlClicked");
|
System.Diagnostics.Debug.WriteLine("[DELETE KEY] Lancio evento RemoveUrlClicked");
|
||||||
RaiseEvent(new RoutedEventArgs(RemoveUrlClickedEvent, this));
|
RaiseEvent(new RoutedEventArgs(RemoveUrlClickedEvent, this));
|
||||||
|
|
||||||
// Previeni che l'evento venga gestito da altri controlli
|
// Previeni che l'evento venga gestito da altri controlli
|
||||||
e.Handled = true;
|
e.Handled = true;
|
||||||
}
|
}
|
||||||
// NUOVO: Gestione esplicita frecce Su/Giù per navigazione
|
// NUOVO: Gestione esplicita frecce Su/Gi� per navigazione
|
||||||
else if (e.Key == Key.Up && MultiAuctionsGrid.Items.Count > 0)
|
else if (e.Key == Key.Up && MultiAuctionsGrid.Items.Count > 0)
|
||||||
{
|
{
|
||||||
int currentIndex = MultiAuctionsGrid.SelectedIndex;
|
int currentIndex = MultiAuctionsGrid.SelectedIndex;
|
||||||
@@ -210,11 +343,6 @@ namespace AutoBidder.Controls
|
|||||||
RaiseEvent(new RoutedEventArgs(BidBeforeDeadlineMsChangedEvent, this));
|
RaiseEvent(new RoutedEventArgs(BidBeforeDeadlineMsChangedEvent, this));
|
||||||
}
|
}
|
||||||
|
|
||||||
private void SelectedCheckAuctionOpen_Changed(object sender, RoutedEventArgs e)
|
|
||||||
{
|
|
||||||
RaiseEvent(new RoutedEventArgs(CheckAuctionOpenChangedEvent, this));
|
|
||||||
}
|
|
||||||
|
|
||||||
private void SelectedMinPrice_TextChanged(object sender, TextChangedEventArgs e)
|
private void SelectedMinPrice_TextChanged(object sender, TextChangedEventArgs e)
|
||||||
{
|
{
|
||||||
RaiseEvent(new RoutedEventArgs(MinPriceChangedEvent, this));
|
RaiseEvent(new RoutedEventArgs(MinPriceChangedEvent, this));
|
||||||
@@ -230,7 +358,23 @@ namespace AutoBidder.Controls
|
|||||||
RaiseEvent(new RoutedEventArgs(MaxClicksChangedEvent, this));
|
RaiseEvent(new RoutedEventArgs(MaxClicksChangedEvent, this));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private void SelectedMaxSpend_TextChanged(object sender, TextChangedEventArgs e)
|
||||||
|
{
|
||||||
|
RaiseEvent(new RoutedEventArgs(MaxSpendChangedEvent, this));
|
||||||
|
}
|
||||||
|
|
||||||
|
private void SelectedStopAtBreakEven_Changed(object sender, RoutedEventArgs e)
|
||||||
|
{
|
||||||
|
RaiseEvent(new RoutedEventArgs(BreakEvenChangedEvent, this));
|
||||||
|
}
|
||||||
|
|
||||||
// Routed Events
|
// Routed Events
|
||||||
|
public static readonly RoutedEvent MaxSpendChangedEvent = EventManager.RegisterRoutedEvent(
|
||||||
|
"MaxSpendChanged", RoutingStrategy.Bubble, typeof(RoutedEventHandler), typeof(AuctionMonitorControl));
|
||||||
|
|
||||||
|
public static readonly RoutedEvent BreakEvenChangedEvent = EventManager.RegisterRoutedEvent(
|
||||||
|
"BreakEvenChanged", RoutingStrategy.Bubble, typeof(RoutedEventHandler), typeof(AuctionMonitorControl));
|
||||||
|
|
||||||
public static readonly RoutedEvent StartClickedEvent = EventManager.RegisterRoutedEvent(
|
public static readonly RoutedEvent StartClickedEvent = EventManager.RegisterRoutedEvent(
|
||||||
"StartClicked", RoutingStrategy.Bubble, typeof(RoutedEventHandler), typeof(AuctionMonitorControl));
|
"StartClicked", RoutingStrategy.Bubble, typeof(RoutedEventHandler), typeof(AuctionMonitorControl));
|
||||||
|
|
||||||
@@ -246,6 +390,9 @@ namespace AutoBidder.Controls
|
|||||||
public static readonly RoutedEvent RemoveUrlClickedEvent = EventManager.RegisterRoutedEvent(
|
public static readonly RoutedEvent RemoveUrlClickedEvent = EventManager.RegisterRoutedEvent(
|
||||||
"RemoveUrlClicked", RoutingStrategy.Bubble, typeof(RoutedEventHandler), typeof(AuctionMonitorControl));
|
"RemoveUrlClicked", RoutingStrategy.Bubble, typeof(RoutedEventHandler), typeof(AuctionMonitorControl));
|
||||||
|
|
||||||
|
public static readonly RoutedEvent RemoveFinishedClickedEvent = EventManager.RegisterRoutedEvent(
|
||||||
|
"RemoveFinishedClicked", RoutingStrategy.Bubble, typeof(RoutedEventHandler), typeof(AuctionMonitorControl));
|
||||||
|
|
||||||
public static readonly RoutedEvent RemoveAllClickedEvent = EventManager.RegisterRoutedEvent(
|
public static readonly RoutedEvent RemoveAllClickedEvent = EventManager.RegisterRoutedEvent(
|
||||||
"RemoveAllClicked", RoutingStrategy.Bubble, typeof(RoutedEventHandler), typeof(AuctionMonitorControl));
|
"RemoveAllClicked", RoutingStrategy.Bubble, typeof(RoutedEventHandler), typeof(AuctionMonitorControl));
|
||||||
|
|
||||||
@@ -273,9 +420,6 @@ namespace AutoBidder.Controls
|
|||||||
public static readonly RoutedEvent BidBeforeDeadlineMsChangedEvent = EventManager.RegisterRoutedEvent(
|
public static readonly RoutedEvent BidBeforeDeadlineMsChangedEvent = EventManager.RegisterRoutedEvent(
|
||||||
"BidBeforeDeadlineMsChanged", RoutingStrategy.Bubble, typeof(RoutedEventHandler), typeof(AuctionMonitorControl));
|
"BidBeforeDeadlineMsChanged", RoutingStrategy.Bubble, typeof(RoutedEventHandler), typeof(AuctionMonitorControl));
|
||||||
|
|
||||||
public static readonly RoutedEvent CheckAuctionOpenChangedEvent = EventManager.RegisterRoutedEvent(
|
|
||||||
"CheckAuctionOpenChanged", RoutingStrategy.Bubble, typeof(RoutedEventHandler), typeof(AuctionMonitorControl));
|
|
||||||
|
|
||||||
public static readonly RoutedEvent MinPriceChangedEvent = EventManager.RegisterRoutedEvent(
|
public static readonly RoutedEvent MinPriceChangedEvent = EventManager.RegisterRoutedEvent(
|
||||||
"MinPriceChanged", RoutingStrategy.Bubble, typeof(RoutedEventHandler), typeof(AuctionMonitorControl));
|
"MinPriceChanged", RoutingStrategy.Bubble, typeof(RoutedEventHandler), typeof(AuctionMonitorControl));
|
||||||
|
|
||||||
@@ -343,6 +487,12 @@ namespace AutoBidder.Controls
|
|||||||
remove { RemoveHandler(RemoveAllClickedEvent, value); }
|
remove { RemoveHandler(RemoveAllClickedEvent, value); }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public event RoutedEventHandler RemoveFinishedClicked
|
||||||
|
{
|
||||||
|
add { AddHandler(RemoveFinishedClickedEvent, value); }
|
||||||
|
remove { RemoveHandler(RemoveFinishedClickedEvent, value); }
|
||||||
|
}
|
||||||
|
|
||||||
public event RoutedEventHandler ExportClicked
|
public event RoutedEventHandler ExportClicked
|
||||||
{
|
{
|
||||||
add { AddHandler(ExportClickedEvent, value); }
|
add { AddHandler(ExportClickedEvent, value); }
|
||||||
@@ -391,12 +541,6 @@ namespace AutoBidder.Controls
|
|||||||
remove { RemoveHandler(BidBeforeDeadlineMsChangedEvent, value); }
|
remove { RemoveHandler(BidBeforeDeadlineMsChangedEvent, value); }
|
||||||
}
|
}
|
||||||
|
|
||||||
public event RoutedEventHandler CheckAuctionOpenChanged
|
|
||||||
{
|
|
||||||
add { AddHandler(CheckAuctionOpenChangedEvent, value); }
|
|
||||||
remove { RemoveHandler(CheckAuctionOpenChangedEvent, value); }
|
|
||||||
}
|
|
||||||
|
|
||||||
public event RoutedEventHandler MinPriceChanged
|
public event RoutedEventHandler MinPriceChanged
|
||||||
{
|
{
|
||||||
add { AddHandler(MinPriceChangedEvent, value); }
|
add { AddHandler(MinPriceChangedEvent, value); }
|
||||||
@@ -415,6 +559,18 @@ namespace AutoBidder.Controls
|
|||||||
remove { RemoveHandler(MaxClicksChangedEvent, value); }
|
remove { RemoveHandler(MaxClicksChangedEvent, value); }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public event RoutedEventHandler MaxSpendChanged
|
||||||
|
{
|
||||||
|
add { AddHandler(MaxSpendChangedEvent, value); }
|
||||||
|
remove { RemoveHandler(MaxSpendChangedEvent, value); }
|
||||||
|
}
|
||||||
|
|
||||||
|
public event RoutedEventHandler BreakEvenChanged
|
||||||
|
{
|
||||||
|
add { AddHandler(BreakEvenChangedEvent, value); }
|
||||||
|
remove { RemoveHandler(BreakEvenChangedEvent, value); }
|
||||||
|
}
|
||||||
|
|
||||||
public event RoutedEventHandler OpenAuctionInternalClicked
|
public event RoutedEventHandler OpenAuctionInternalClicked
|
||||||
{
|
{
|
||||||
add { AddHandler(OpenAuctionInternalClickedEvent, value); }
|
add { AddHandler(OpenAuctionInternalClickedEvent, value); }
|
||||||
|
|||||||
@@ -1,127 +1,318 @@
|
|||||||
<UserControl x:Class="AutoBidder.Controls.BrowserControl"
|
<UserControl x:Class="AutoBidder.Controls.BrowserControl"
|
||||||
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
|
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
|
||||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||||
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
|
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
|
||||||
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
|
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
|
||||||
xmlns:wv2="clr-namespace:Microsoft.Web.WebView2.Wpf;assembly=Microsoft.Web.WebView2.Wpf"
|
xmlns:wv2="clr-namespace:Microsoft.Web.WebView2.Wpf;assembly=Microsoft.Web.WebView2.Wpf"
|
||||||
mc:Ignorable="d"
|
mc:Ignorable="d"
|
||||||
d:DesignHeight="800" d:DesignWidth="1200"
|
d:DesignHeight="800" d:DesignWidth="1200"
|
||||||
Background="#1E1E1E">
|
Background="{DynamicResource Brush.Bg}">
|
||||||
|
|
||||||
<UserControl.Resources>
|
<UserControl.Resources>
|
||||||
<Style x:Key="RoundedButton" TargetType="Button">
|
<Style x:Key="Glyph" TargetType="TextBlock">
|
||||||
<Setter Property="Template">
|
<Setter Property="FontFamily" Value="Segoe MDL2 Assets"/>
|
||||||
<Setter.Value>
|
<Setter Property="VerticalAlignment" Value="Center"/>
|
||||||
<ControlTemplate TargetType="Button">
|
<Setter Property="HorizontalAlignment" Value="Center"/>
|
||||||
<Border Background="{TemplateBinding Background}"
|
|
||||||
CornerRadius="4"
|
|
||||||
Padding="{TemplateBinding Padding}">
|
|
||||||
<ContentPresenter HorizontalAlignment="Center" VerticalAlignment="Center"/>
|
|
||||||
</Border>
|
|
||||||
</ControlTemplate>
|
|
||||||
</Setter.Value>
|
|
||||||
</Setter>
|
|
||||||
<Setter Property="Foreground" Value="White"/>
|
|
||||||
<Setter Property="FontWeight" Value="SemiBold"/>
|
|
||||||
<Setter Property="BorderThickness" Value="0"/>
|
|
||||||
<Setter Property="Cursor" Value="Hand"/>
|
|
||||||
</Style>
|
</Style>
|
||||||
|
|
||||||
<!-- Nav Button Style (text only) -->
|
<!-- Scheda di una singola asta del catalogo -->
|
||||||
<Style x:Key="NavButton" TargetType="Button" BasedOn="{StaticResource RoundedButton}">
|
<DataTemplate x:Key="AuctionCardTemplate">
|
||||||
<Setter Property="MinWidth" Value="50"/>
|
<Border Style="{StaticResource AuctionCard}" Width="250">
|
||||||
<Setter Property="Height" Value="30"/>
|
<Grid>
|
||||||
<Setter Property="FontSize" Value="11"/>
|
<Grid.RowDefinitions>
|
||||||
<Setter Property="Padding" Value="8,0"/>
|
<RowDefinition Height="Auto"/>
|
||||||
</Style>
|
<RowDefinition Height="Auto"/>
|
||||||
|
<RowDefinition Height="Auto"/>
|
||||||
|
<RowDefinition Height="Auto"/>
|
||||||
|
</Grid.RowDefinitions>
|
||||||
|
|
||||||
|
<!-- Immagine prodotto: arriva dalla rete, quindi puo' mancare -->
|
||||||
|
<Border Grid.Row="0" Height="110" CornerRadius="8"
|
||||||
|
Background="{DynamicResource Brush.SurfaceAlt}" Margin="0,0,0,8">
|
||||||
|
<Image Source="{Binding ImageUrl}" Stretch="Uniform" Margin="6"
|
||||||
|
RenderOptions.BitmapScalingMode="HighQuality"/>
|
||||||
|
</Border>
|
||||||
|
|
||||||
|
<TextBlock Grid.Row="1" Text="{Binding Name}" ToolTip="{Binding Name}"
|
||||||
|
FontWeight="SemiBold" TextTrimming="CharacterEllipsis"
|
||||||
|
MaxHeight="34" TextWrapping="Wrap"
|
||||||
|
Foreground="{DynamicResource Brush.Text}" Margin="0,0,0,6"/>
|
||||||
|
|
||||||
|
<StackPanel Grid.Row="2" Orientation="Horizontal" Margin="0,0,0,6">
|
||||||
|
<TextBlock Text="{Binding PriceDisplay}" FontWeight="Bold"
|
||||||
|
Foreground="{DynamicResource Brush.Accent}" Margin="0,0,10,0"/>
|
||||||
|
<TextBlock Text="{Binding TimerDisplay}" Margin="0,0,10,0">
|
||||||
|
<TextBlock.Style>
|
||||||
|
<Style TargetType="TextBlock">
|
||||||
|
<Setter Property="Foreground" Value="{DynamicResource Brush.TextMuted}"/>
|
||||||
|
<Style.Triggers>
|
||||||
|
<DataTrigger Binding="{Binding TimerUrgency}" Value="warn">
|
||||||
|
<Setter Property="Foreground" Value="{DynamicResource Brush.Warning}"/>
|
||||||
|
</DataTrigger>
|
||||||
|
<DataTrigger Binding="{Binding TimerUrgency}" Value="danger">
|
||||||
|
<Setter Property="Foreground" Value="{DynamicResource Brush.Danger}"/>
|
||||||
|
<Setter Property="FontWeight" Value="Bold"/>
|
||||||
|
</DataTrigger>
|
||||||
|
</Style.Triggers>
|
||||||
|
</Style>
|
||||||
|
</TextBlock.Style>
|
||||||
|
</TextBlock>
|
||||||
|
<TextBlock Text="{Binding BuyNowDisplay}"
|
||||||
|
FontSize="{StaticResource Font.Size.Sm}"
|
||||||
|
Foreground="{DynamicResource Brush.TextFaint}"
|
||||||
|
ToolTip="Valore del prodotto (Compra Subito)"/>
|
||||||
|
</StackPanel>
|
||||||
|
|
||||||
|
<Grid Grid.Row="3">
|
||||||
|
<StackPanel Orientation="Horizontal" VerticalAlignment="Center">
|
||||||
|
<TextBlock Text="{Binding LastBidderDisplay}"
|
||||||
|
FontSize="{StaticResource Font.Size.Sm}"
|
||||||
|
Foreground="{DynamicResource Brush.TextMuted}"
|
||||||
|
TextTrimming="CharacterEllipsis" MaxWidth="90"/>
|
||||||
|
<!-- Solo l'icona della mano: la parola "manuale" mangiava spazio
|
||||||
|
al nome dell'ultimo puntatore, che è il dato che si legge
|
||||||
|
davvero. Il significato resta nel suggerimento. -->
|
||||||
|
<Border Background="{DynamicResource Brush.WarningSubtle}" CornerRadius="5"
|
||||||
|
Padding="4,1" Margin="6,0,0,0"
|
||||||
|
Visibility="{Binding ManualBadgeVisibility}"
|
||||||
|
ToolTip="Asta solo manuale: non accetta puntate automatiche">
|
||||||
|
<TextBlock FontFamily="Segoe MDL2 Assets" Text="" FontSize="10"
|
||||||
|
Foreground="{DynamicResource Brush.Warning}"/>
|
||||||
|
</Border>
|
||||||
|
</StackPanel>
|
||||||
|
|
||||||
|
<StackPanel Orientation="Horizontal" HorizontalAlignment="Right">
|
||||||
|
<!-- Segui il prodotto: le aste future dello stesso articolo
|
||||||
|
entreranno nel monitor da sole. -->
|
||||||
|
<Button Style="{StaticResource MiniIconButton}"
|
||||||
|
ToolTip="{Binding WatchTooltip}"
|
||||||
|
Command="{Binding DataContext.CatalogWatchCommand, RelativeSource={RelativeSource AncestorType=UserControl}}"
|
||||||
|
CommandParameter="{Binding}">
|
||||||
|
<TextBlock FontFamily="Segoe MDL2 Assets" Text="{Binding WatchGlyph}">
|
||||||
|
<TextBlock.Style>
|
||||||
|
<Style TargetType="TextBlock">
|
||||||
|
<Setter Property="Foreground" Value="{DynamicResource Brush.TextFaint}"/>
|
||||||
|
<Style.Triggers>
|
||||||
|
<DataTrigger Binding="{Binding IsWatched}" Value="True">
|
||||||
|
<Setter Property="Foreground" Value="{DynamicResource Brush.Gold}"/>
|
||||||
|
</DataTrigger>
|
||||||
|
</Style.Triggers>
|
||||||
|
</Style>
|
||||||
|
</TextBlock.Style>
|
||||||
|
</TextBlock>
|
||||||
|
</Button>
|
||||||
|
<!-- Mette il prodotto nella scheda Prodotti e ci porta sopra:
|
||||||
|
serve a dargli limiti propri anche senza seguirlo. -->
|
||||||
|
<Button Style="{StaticResource MiniIconButton}"
|
||||||
|
ToolTip="{Binding ConfigureTooltip}"
|
||||||
|
Command="{Binding DataContext.CatalogConfigureCommand, RelativeSource={RelativeSource AncestorType=UserControl}}"
|
||||||
|
CommandParameter="{Binding}">
|
||||||
|
<TextBlock FontFamily="Segoe MDL2 Assets" Text="">
|
||||||
|
<TextBlock.Style>
|
||||||
|
<Style TargetType="TextBlock">
|
||||||
|
<Setter Property="Foreground" Value="{DynamicResource Brush.TextFaint}"/>
|
||||||
|
<Style.Triggers>
|
||||||
|
<DataTrigger Binding="{Binding IsListed}" Value="True">
|
||||||
|
<Setter Property="Foreground" Value="{DynamicResource Brush.Accent}"/>
|
||||||
|
</DataTrigger>
|
||||||
|
</Style.Triggers>
|
||||||
|
</Style>
|
||||||
|
</TextBlock.Style>
|
||||||
|
</TextBlock>
|
||||||
|
</Button>
|
||||||
|
<Button Style="{StaticResource MiniIconButton}"
|
||||||
|
ToolTip="Apri su Bidoo"
|
||||||
|
Command="{Binding DataContext.CatalogOpenCommand, RelativeSource={RelativeSource AncestorType=UserControl}}"
|
||||||
|
CommandParameter="{Binding}">
|
||||||
|
<TextBlock FontFamily="Segoe MDL2 Assets" Text=""/>
|
||||||
|
</Button>
|
||||||
|
<Button Style="{StaticResource MiniIconButton}"
|
||||||
|
Foreground="{DynamicResource Brush.Accent}"
|
||||||
|
ToolTip="Aggiungi al monitor"
|
||||||
|
Command="{Binding DataContext.CatalogAddCommand, RelativeSource={RelativeSource AncestorType=UserControl}}"
|
||||||
|
CommandParameter="{Binding}">
|
||||||
|
<TextBlock FontFamily="Segoe MDL2 Assets" Text=""/>
|
||||||
|
</Button>
|
||||||
|
</StackPanel>
|
||||||
|
</Grid>
|
||||||
|
</Grid>
|
||||||
|
</Border>
|
||||||
|
</DataTemplate>
|
||||||
</UserControl.Resources>
|
</UserControl.Resources>
|
||||||
|
|
||||||
<Grid>
|
<Grid>
|
||||||
<Grid.RowDefinitions>
|
<Grid.RowDefinitions>
|
||||||
|
<RowDefinition Height="Auto"/>
|
||||||
<RowDefinition Height="Auto"/>
|
<RowDefinition Height="Auto"/>
|
||||||
<RowDefinition Height="*"/>
|
<RowDefinition Height="*"/>
|
||||||
</Grid.RowDefinitions>
|
</Grid.RowDefinitions>
|
||||||
|
|
||||||
<!-- Browser Toolbar -->
|
<!-- ═══ Barra strumenti ═══ -->
|
||||||
<Border Grid.Row="0" Background="#2D2D30" Padding="10" BorderBrush="#3E3E42" BorderThickness="0,0,0,1">
|
<Border Grid.Row="0" Background="{DynamicResource Brush.Surface}" Padding="10,7"
|
||||||
|
BorderBrush="{DynamicResource Brush.Border}" BorderThickness="0,0,0,1">
|
||||||
<Grid>
|
<Grid>
|
||||||
<Grid.ColumnDefinitions>
|
<Grid.ColumnDefinitions>
|
||||||
|
<ColumnDefinition Width="Auto"/>
|
||||||
<ColumnDefinition Width="Auto"/>
|
<ColumnDefinition Width="Auto"/>
|
||||||
<ColumnDefinition Width="*"/>
|
<ColumnDefinition Width="*"/>
|
||||||
<ColumnDefinition Width="Auto"/>
|
<ColumnDefinition Width="Auto"/>
|
||||||
</Grid.ColumnDefinitions>
|
</Grid.ColumnDefinitions>
|
||||||
|
|
||||||
<!-- Navigation Buttons (TEXT ONLY - NO SYMBOLS) -->
|
<!-- Catalogo e Browser ora sono due schede separate nella barra laterale
|
||||||
<StackPanel Grid.Column="0" Orientation="Horizontal">
|
("Cerca" e "Browser"): l'interruttore interno resta solo come stato
|
||||||
<Button x:Name="BrowserBackButton"
|
pilotato da fuori, non più visibile all'utente. -->
|
||||||
Content="Indietro"
|
<StackPanel Grid.Column="0" Orientation="Horizontal" VerticalAlignment="Center"
|
||||||
Margin="0,0,5,0"
|
Visibility="Collapsed">
|
||||||
Background="#3E3E42"
|
<RadioButton x:Name="ModeCatalogRadio" Style="{StaticResource SegmentedButton}"
|
||||||
Style="{StaticResource NavButton}"
|
GroupName="ExploreMode" IsChecked="True" Content="Catalogo"
|
||||||
Click="BrowserBackButton_Click"
|
Checked="ModeCatalogRadio_Checked"
|
||||||
ToolTip="Torna alla pagina precedente"/>
|
ToolTip="Griglia delle aste: veloce, senza caricare pagine"/>
|
||||||
|
<RadioButton x:Name="ModeBrowserRadio" Style="{StaticResource SegmentedButton}"
|
||||||
<Button x:Name="BrowserForwardButton"
|
GroupName="ExploreMode" Content="Browser"
|
||||||
Content="Avanti"
|
Checked="ModeBrowserRadio_Checked"
|
||||||
Margin="0,0,5,0"
|
ToolTip="Browser Bidoo integrato: serve per il login (il cookie viene importato da solo)"/>
|
||||||
Background="#3E3E42"
|
<Border Width="1" Background="{DynamicResource Brush.Border}" Margin="8,4"/>
|
||||||
Style="{StaticResource NavButton}"
|
|
||||||
Click="BrowserForwardButton_Click"
|
|
||||||
ToolTip="Vai alla pagina successiva"/>
|
|
||||||
|
|
||||||
<Button x:Name="BrowserRefreshButton"
|
|
||||||
Content="Ricarica"
|
|
||||||
Margin="0,0,5,0"
|
|
||||||
Background="#3E3E42"
|
|
||||||
Style="{StaticResource NavButton}"
|
|
||||||
Click="BrowserRefreshButton_Click"
|
|
||||||
ToolTip="Ricarica la pagina corrente"/>
|
|
||||||
|
|
||||||
<Button x:Name="BrowserHomeButton"
|
|
||||||
Content="Home"
|
|
||||||
Margin="0,0,10,0"
|
|
||||||
Background="#3E3E42"
|
|
||||||
Style="{StaticResource NavButton}"
|
|
||||||
Click="BrowserHomeButton_Click"
|
|
||||||
ToolTip="Vai alla homepage Bidoo"/>
|
|
||||||
</StackPanel>
|
</StackPanel>
|
||||||
|
|
||||||
<!-- Address Bar -->
|
<StackPanel Grid.Column="1" x:Name="CatalogToolbarLeft" Orientation="Horizontal"
|
||||||
<Border Grid.Column="1"
|
VerticalAlignment="Center">
|
||||||
Background="#1E1E1E"
|
<Button x:Name="CatalogRefreshButton" Style="{StaticResource IconButton}"
|
||||||
BorderBrush="#3E3E42"
|
ToolTip="Ricarica categorie e prezzi" Click="CatalogRefreshButton_Click">
|
||||||
BorderThickness="1"
|
<TextBlock Style="{StaticResource Glyph}" Text=""/>
|
||||||
CornerRadius="4"
|
</Button>
|
||||||
Margin="10,0">
|
<CheckBox x:Name="CatalogAutoRefresh" Content="Prezzi in tempo reale"
|
||||||
<TextBox x:Name="BrowserAddress"
|
Margin="10,0,0,0" VerticalAlignment="Center"
|
||||||
VerticalAlignment="Center"
|
Checked="CatalogAutoRefresh_Changed" Unchecked="CatalogAutoRefresh_Changed"
|
||||||
BorderThickness="0"
|
ToolTip="Aggiorna prezzi e timer sul posto. Spento di default: acceso, i valori cambiano mentre stai leggendo."/>
|
||||||
Background="Transparent"
|
<CheckBox x:Name="CatalogHideManual" Content="Nascondi aste manuali"
|
||||||
Foreground="#CCCCCC"
|
Margin="14,0,0,0" VerticalAlignment="Center"
|
||||||
Padding="10,0"
|
Checked="CatalogFilterChanged" Unchecked="CatalogFilterChanged"
|
||||||
FontSize="13"
|
ToolTip="Le aste manuali non accettano puntate automatiche"/>
|
||||||
IsReadOnly="True"
|
</StackPanel>
|
||||||
Cursor="Arrow"
|
|
||||||
ToolTip="Indirizzo della pagina corrente (non editabile)"/>
|
<!-- Barra indirizzi del browser: occupa lo stesso posto dei filtri -->
|
||||||
|
<Border Grid.Column="1" Grid.ColumnSpan="2" x:Name="BrowserToolbar"
|
||||||
|
Visibility="Collapsed" Margin="0,0,10,0">
|
||||||
|
<Grid>
|
||||||
|
<Grid.ColumnDefinitions>
|
||||||
|
<ColumnDefinition Width="Auto"/>
|
||||||
|
<ColumnDefinition Width="*"/>
|
||||||
|
</Grid.ColumnDefinitions>
|
||||||
|
|
||||||
|
<StackPanel Grid.Column="0" Orientation="Horizontal" VerticalAlignment="Center">
|
||||||
|
<Button x:Name="BrowserBackButton" Style="{StaticResource IconButton}"
|
||||||
|
ToolTip="Indietro" Click="BrowserBackButton_Click">
|
||||||
|
<TextBlock Style="{StaticResource Glyph}" Text=""/>
|
||||||
|
</Button>
|
||||||
|
<Button x:Name="BrowserForwardButton" Style="{StaticResource IconButton}"
|
||||||
|
ToolTip="Avanti" Click="BrowserForwardButton_Click">
|
||||||
|
<TextBlock Style="{StaticResource Glyph}" Text=""/>
|
||||||
|
</Button>
|
||||||
|
<Button x:Name="BrowserRefreshButton" Style="{StaticResource IconButton}"
|
||||||
|
ToolTip="Ricarica" Click="BrowserRefreshButton_Click">
|
||||||
|
<TextBlock Style="{StaticResource Glyph}" Text=""/>
|
||||||
|
</Button>
|
||||||
|
<Button x:Name="BrowserHomeButton" Style="{StaticResource IconButton}"
|
||||||
|
ToolTip="Homepage Bidoo" Click="BrowserHomeButton_Click">
|
||||||
|
<TextBlock Style="{StaticResource Glyph}" Text=""/>
|
||||||
|
</Button>
|
||||||
|
</StackPanel>
|
||||||
|
|
||||||
|
<Border Grid.Column="1" Background="{DynamicResource Brush.Bg}"
|
||||||
|
BorderBrush="{DynamicResource Brush.Border}" BorderThickness="1"
|
||||||
|
CornerRadius="8" Margin="8,0,0,0">
|
||||||
|
<TextBox x:Name="BrowserAddress" VerticalAlignment="Center"
|
||||||
|
BorderThickness="0" Background="Transparent"
|
||||||
|
Foreground="{DynamicResource Brush.Text}" Padding="10,0"
|
||||||
|
IsReadOnly="True" Cursor="Arrow"
|
||||||
|
ToolTip="Indirizzo della pagina corrente"/>
|
||||||
|
</Border>
|
||||||
|
</Grid>
|
||||||
</Border>
|
</Border>
|
||||||
|
|
||||||
<!-- Action Buttons -->
|
<!-- Campo di ricerca con suggerimento: senza, un riquadro vuoto in barra
|
||||||
<StackPanel Grid.Column="2" Orientation="Horizontal" Margin="10,0,0,0">
|
non si capisce a cosa serva. -->
|
||||||
<Button x:Name="BrowserAddAuctionButton"
|
<Grid Grid.Column="2" x:Name="CatalogSearchHost" Margin="16,0,10,0"
|
||||||
Content="Aggiungi Asta"
|
Width="240" HorizontalAlignment="Right" VerticalAlignment="Center">
|
||||||
Padding="20,7"
|
<TextBox x:Name="CatalogSearchBox"
|
||||||
FontSize="13"
|
TextChanged="CatalogSearchBox_TextChanged"
|
||||||
Background="#00D800"
|
ToolTip="Filtra per nome prodotto"/>
|
||||||
Style="{StaticResource RoundedButton}"
|
<TextBlock x:Name="CatalogSearchHint" Text="Filtra per nome…"
|
||||||
Click="BrowserAddAuctionButton_Click"
|
IsHitTestVisible="False" Margin="10,0,0,0"
|
||||||
ToolTip="Aggiungi l'asta corrente al monitoraggio"/>
|
VerticalAlignment="Center"
|
||||||
|
Foreground="{DynamicResource Brush.TextFaint}"/>
|
||||||
|
</Grid>
|
||||||
|
|
||||||
|
<StackPanel Grid.Column="3" Orientation="Horizontal" VerticalAlignment="Center">
|
||||||
|
<Border Style="{StaticResource Pill}">
|
||||||
|
<TextBlock x:Name="CatalogCountText" Text="0 aste"
|
||||||
|
FontSize="{StaticResource Font.Size.Sm}"
|
||||||
|
Foreground="{DynamicResource Brush.TextMuted}"/>
|
||||||
|
</Border>
|
||||||
|
<Button x:Name="BrowserAddAuctionButton" Style="{StaticResource IconButton}"
|
||||||
|
Foreground="{DynamicResource Brush.Accent}" Visibility="Collapsed"
|
||||||
|
ToolTip="Aggiungi al monitor l'asta aperta nel browser"
|
||||||
|
Click="BrowserAddAuctionButton_Click">
|
||||||
|
<TextBlock Style="{StaticResource Glyph}" Text=""/>
|
||||||
|
</Button>
|
||||||
</StackPanel>
|
</StackPanel>
|
||||||
</Grid>
|
</Grid>
|
||||||
</Border>
|
</Border>
|
||||||
|
|
||||||
<!-- WebView2 -->
|
<!-- Riga vuota: tiene la struttura a tre righe leggibile -->
|
||||||
<Border Grid.Row="1" Background="#1E1E1E">
|
<Border Grid.Row="1" Height="0"/>
|
||||||
<wv2:WebView2 x:Name="EmbeddedWebView"
|
|
||||||
|
<!-- ═══ Catalogo ═══ -->
|
||||||
|
<Grid Grid.Row="2" x:Name="CatalogPanel">
|
||||||
|
<Grid.ColumnDefinitions>
|
||||||
|
<ColumnDefinition Width="Auto"/>
|
||||||
|
<ColumnDefinition Width="*"/>
|
||||||
|
</Grid.ColumnDefinitions>
|
||||||
|
|
||||||
|
<!-- Categorie -->
|
||||||
|
<Border Grid.Column="0" Width="190" Background="{DynamicResource Brush.Surface}"
|
||||||
|
BorderBrush="{DynamicResource Brush.Border}" BorderThickness="0,0,1,0">
|
||||||
|
<ScrollViewer VerticalScrollBarVisibility="Auto" Padding="6,8">
|
||||||
|
<ItemsControl x:Name="CatalogCategoryList">
|
||||||
|
<ItemsControl.ItemTemplate>
|
||||||
|
<DataTemplate>
|
||||||
|
<RadioButton Style="{StaticResource CategoryButton}"
|
||||||
|
GroupName="CatalogCategories"
|
||||||
|
Content="{Binding DisplayName}"
|
||||||
|
IsChecked="{Binding IsSelected, Mode=TwoWay}"
|
||||||
|
Checked="CategoryButton_Checked"/>
|
||||||
|
</DataTemplate>
|
||||||
|
</ItemsControl.ItemTemplate>
|
||||||
|
</ItemsControl>
|
||||||
|
</ScrollViewer>
|
||||||
|
</Border>
|
||||||
|
|
||||||
|
<!-- Schede -->
|
||||||
|
<Grid Grid.Column="1">
|
||||||
|
<ScrollViewer x:Name="CatalogScroller" VerticalScrollBarVisibility="Auto" Padding="12,12,2,12">
|
||||||
|
<ItemsControl x:Name="CatalogItems" ItemTemplate="{StaticResource AuctionCardTemplate}">
|
||||||
|
<ItemsControl.ItemsPanel>
|
||||||
|
<ItemsPanelTemplate>
|
||||||
|
<WrapPanel Orientation="Horizontal"/>
|
||||||
|
</ItemsPanelTemplate>
|
||||||
|
</ItemsControl.ItemsPanel>
|
||||||
|
</ItemsControl>
|
||||||
|
</ScrollViewer>
|
||||||
|
|
||||||
|
<!-- Stato vuoto / caricamento -->
|
||||||
|
<StackPanel x:Name="CatalogEmptyState" VerticalAlignment="Center" HorizontalAlignment="Center">
|
||||||
|
<TextBlock Style="{StaticResource Glyph}" Text="" FontSize="34"
|
||||||
|
Foreground="{DynamicResource Brush.TextFaint}"/>
|
||||||
|
<TextBlock x:Name="CatalogEmptyText" Text="Caricamento categorie…"
|
||||||
|
Margin="0,10,0,0" HorizontalAlignment="Center"
|
||||||
|
Foreground="{DynamicResource Brush.TextMuted}"/>
|
||||||
|
</StackPanel>
|
||||||
|
</Grid>
|
||||||
|
</Grid>
|
||||||
|
|
||||||
|
<!-- ═══ Browser integrato ═══ -->
|
||||||
|
<Border Grid.Row="2" x:Name="BrowserPanel" Background="{DynamicResource Brush.Bg}" Visibility="Collapsed">
|
||||||
|
<wv2:WebView2 x:Name="EmbeddedWebView"
|
||||||
PreviewMouseRightButtonUp="EmbeddedWebView_PreviewMouseRightButtonUp"/>
|
PreviewMouseRightButtonUp="EmbeddedWebView_PreviewMouseRightButtonUp"/>
|
||||||
</Border>
|
</Border>
|
||||||
</Grid>
|
</Grid>
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
using System.Windows;
|
using System.Windows;
|
||||||
using System.Windows.Controls;
|
using System.Windows.Controls;
|
||||||
using System.Windows.Input;
|
using System.Windows.Input;
|
||||||
using Microsoft.Web.WebView2.Core;
|
using Microsoft.Web.WebView2.Core;
|
||||||
@@ -44,7 +44,7 @@ namespace AutoBidder.Controls
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// ? NUOVO: Aggiorna address bar quando la navigazione è completata
|
/// ? NUOVO: Aggiorna address bar quando la navigazione � completata
|
||||||
/// </summary>
|
/// </summary>
|
||||||
private void WebView_NavigationCompleted(object? sender, CoreWebView2NavigationCompletedEventArgs e)
|
private void WebView_NavigationCompleted(object? sender, CoreWebView2NavigationCompletedEventArgs e)
|
||||||
{
|
{
|
||||||
@@ -93,6 +93,133 @@ namespace AutoBidder.Controls
|
|||||||
e.Handled = true;
|
e.Handled = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ===== MODALITA' ESPLORA =====
|
||||||
|
|
||||||
|
private void ModeCatalogRadio_Checked(object sender, RoutedEventArgs e) => ApplyMode(catalog: true);
|
||||||
|
|
||||||
|
private void ModeBrowserRadio_Checked(object sender, RoutedEventArgs e) => ApplyMode(catalog: false);
|
||||||
|
|
||||||
|
private void ApplyMode(bool catalog)
|
||||||
|
{
|
||||||
|
// Chiamato anche durante InitializeComponent, quando gli elementi dichiarati piu'
|
||||||
|
// in basso nel XAML non esistono ancora. Vanno verificati tutti: fidarsi
|
||||||
|
// dell'ordine di dichiarazione rende il controllo fragile a ogni riordino.
|
||||||
|
if (CatalogPanel == null || BrowserPanel == null || BrowserToolbar == null ||
|
||||||
|
CatalogToolbarLeft == null || CatalogSearchHost == null ||
|
||||||
|
CatalogCountText == null || BrowserAddAuctionButton == null)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
CatalogPanel.Visibility = catalog ? Visibility.Visible : Visibility.Collapsed;
|
||||||
|
BrowserPanel.Visibility = catalog ? Visibility.Collapsed : Visibility.Visible;
|
||||||
|
|
||||||
|
// I due gruppi di comandi occupano lo stesso spazio: ne vive uno solo per volta.
|
||||||
|
BrowserToolbar.Visibility = catalog ? Visibility.Collapsed : Visibility.Visible;
|
||||||
|
CatalogToolbarLeft.Visibility = catalog ? Visibility.Visible : Visibility.Collapsed;
|
||||||
|
CatalogSearchHost.Visibility = catalog ? Visibility.Visible : Visibility.Collapsed;
|
||||||
|
CatalogCountText.Visibility = catalog ? Visibility.Visible : Visibility.Collapsed;
|
||||||
|
BrowserAddAuctionButton.Visibility = catalog ? Visibility.Collapsed : Visibility.Visible;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>Porta in primo piano il browser integrato (usato dal login e dal catalogo).</summary>
|
||||||
|
public void ShowBrowser() => ModeBrowserRadio.IsChecked = true;
|
||||||
|
|
||||||
|
/// <summary>Porta in primo piano il catalogo nativo (scheda "Cerca").</summary>
|
||||||
|
public void ShowCatalog() => ModeCatalogRadio.IsChecked = true;
|
||||||
|
|
||||||
|
/// <summary>True quando l'utente sta guardando il catalogo nativo.</summary>
|
||||||
|
public bool IsCatalogMode => ModeCatalogRadio.IsChecked == true;
|
||||||
|
|
||||||
|
// ===== CATALOGO =====
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Impedisce che il riempimento iniziale dell'elenco categorie faccia partire
|
||||||
|
/// un caricamento per ogni voce aggiunta.
|
||||||
|
/// </summary>
|
||||||
|
private bool _catalogReady;
|
||||||
|
|
||||||
|
private void CategoryButton_Checked(object sender, RoutedEventArgs e)
|
||||||
|
{
|
||||||
|
if (!_catalogReady) return;
|
||||||
|
RaiseEvent(new RoutedEventArgs(CatalogCategoryChangedEvent, this));
|
||||||
|
}
|
||||||
|
|
||||||
|
private void CatalogRefreshButton_Click(object sender, RoutedEventArgs e)
|
||||||
|
=> RaiseEvent(new RoutedEventArgs(CatalogRefreshClickedEvent, this));
|
||||||
|
|
||||||
|
private void CatalogSearchBox_TextChanged(object sender, TextChangedEventArgs e)
|
||||||
|
{
|
||||||
|
if (CatalogSearchHint != null)
|
||||||
|
{
|
||||||
|
CatalogSearchHint.Visibility = string.IsNullOrEmpty(CatalogSearchBox.Text)
|
||||||
|
? Visibility.Visible
|
||||||
|
: Visibility.Collapsed;
|
||||||
|
}
|
||||||
|
|
||||||
|
RaiseEvent(new RoutedEventArgs(CatalogSearchChangedEvent, this));
|
||||||
|
}
|
||||||
|
|
||||||
|
private void CatalogFilterChanged(object sender, RoutedEventArgs e)
|
||||||
|
=> RaiseEvent(new RoutedEventArgs(CatalogSearchChangedEvent, this));
|
||||||
|
|
||||||
|
private void CatalogAutoRefresh_Changed(object sender, RoutedEventArgs e)
|
||||||
|
=> RaiseEvent(new RoutedEventArgs(CatalogAutoRefreshChangedEvent, this));
|
||||||
|
|
||||||
|
/// <summary>Riempie l'elenco categorie senza scatenare un caricamento per voce.</summary>
|
||||||
|
public void SetCategories(System.Collections.IEnumerable categories)
|
||||||
|
{
|
||||||
|
_catalogReady = false;
|
||||||
|
try { CatalogCategoryList.ItemsSource = categories; }
|
||||||
|
finally { _catalogReady = true; }
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>Mostra le schede oppure, se non ce ne sono, un messaggio di stato.</summary>
|
||||||
|
public void SetCatalogItems(System.Collections.IList items, int totalCount)
|
||||||
|
{
|
||||||
|
CatalogItems.ItemsSource = items;
|
||||||
|
CatalogCountText.Text = items.Count == 1 ? "1 asta" : $"{items.Count} aste";
|
||||||
|
|
||||||
|
var empty = items.Count == 0;
|
||||||
|
CatalogEmptyState.Visibility = empty ? Visibility.Visible : Visibility.Collapsed;
|
||||||
|
CatalogScroller.Visibility = empty ? Visibility.Collapsed : Visibility.Visible;
|
||||||
|
|
||||||
|
if (empty)
|
||||||
|
{
|
||||||
|
CatalogEmptyText.Text = totalCount > 0
|
||||||
|
? "Nessuna asta corrisponde ai filtri."
|
||||||
|
: "Nessuna asta in questa categoria.";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>Messaggio a tutto pannello (caricamento in corso, errore di rete...).</summary>
|
||||||
|
public void SetCatalogMessage(string message)
|
||||||
|
{
|
||||||
|
CatalogEmptyState.Visibility = Visibility.Visible;
|
||||||
|
CatalogScroller.Visibility = Visibility.Collapsed;
|
||||||
|
CatalogEmptyText.Text = message;
|
||||||
|
}
|
||||||
|
|
||||||
|
public bool AutoRefreshEnabled => CatalogAutoRefresh.IsChecked == true;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Allinea l'interruttore all'impostazione salvata senza far partire un giro di
|
||||||
|
/// aggiornamento: chi chiama sta ancora costruendo la pagina.
|
||||||
|
/// </summary>
|
||||||
|
public void SetAutoRefresh(bool enabled)
|
||||||
|
{
|
||||||
|
CatalogAutoRefresh.Checked -= CatalogAutoRefresh_Changed;
|
||||||
|
CatalogAutoRefresh.Unchecked -= CatalogAutoRefresh_Changed;
|
||||||
|
try { CatalogAutoRefresh.IsChecked = enabled; }
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
CatalogAutoRefresh.Checked += CatalogAutoRefresh_Changed;
|
||||||
|
CatalogAutoRefresh.Unchecked += CatalogAutoRefresh_Changed;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
public bool HideManualAuctions => CatalogHideManual.IsChecked == true;
|
||||||
|
public string SearchText => CatalogSearchBox.Text?.Trim() ?? "";
|
||||||
|
|
||||||
// Routed Events
|
// Routed Events
|
||||||
public static readonly RoutedEvent BrowserBackClickedEvent = EventManager.RegisterRoutedEvent(
|
public static readonly RoutedEvent BrowserBackClickedEvent = EventManager.RegisterRoutedEvent(
|
||||||
"BrowserBackClicked", RoutingStrategy.Bubble, typeof(RoutedEventHandler), typeof(BrowserControl));
|
"BrowserBackClicked", RoutingStrategy.Bubble, typeof(RoutedEventHandler), typeof(BrowserControl));
|
||||||
@@ -144,6 +271,44 @@ namespace AutoBidder.Controls
|
|||||||
add { AddHandler(BrowserAddAuctionClickedEvent, value); }
|
add { AddHandler(BrowserAddAuctionClickedEvent, value); }
|
||||||
remove { RemoveHandler(BrowserAddAuctionClickedEvent, value); }
|
remove { RemoveHandler(BrowserAddAuctionClickedEvent, value); }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ===== Eventi del catalogo =====
|
||||||
|
|
||||||
|
public static readonly RoutedEvent CatalogCategoryChangedEvent = EventManager.RegisterRoutedEvent(
|
||||||
|
"CatalogCategoryChanged", RoutingStrategy.Bubble, typeof(RoutedEventHandler), typeof(BrowserControl));
|
||||||
|
|
||||||
|
public static readonly RoutedEvent CatalogRefreshClickedEvent = EventManager.RegisterRoutedEvent(
|
||||||
|
"CatalogRefreshClicked", RoutingStrategy.Bubble, typeof(RoutedEventHandler), typeof(BrowserControl));
|
||||||
|
|
||||||
|
public static readonly RoutedEvent CatalogAutoRefreshChangedEvent = EventManager.RegisterRoutedEvent(
|
||||||
|
"CatalogAutoRefreshChanged", RoutingStrategy.Bubble, typeof(RoutedEventHandler), typeof(BrowserControl));
|
||||||
|
|
||||||
|
public static readonly RoutedEvent CatalogSearchChangedEvent = EventManager.RegisterRoutedEvent(
|
||||||
|
"CatalogSearchChanged", RoutingStrategy.Bubble, typeof(RoutedEventHandler), typeof(BrowserControl));
|
||||||
|
|
||||||
|
public event RoutedEventHandler CatalogCategoryChanged
|
||||||
|
{
|
||||||
|
add { AddHandler(CatalogCategoryChangedEvent, value); }
|
||||||
|
remove { RemoveHandler(CatalogCategoryChangedEvent, value); }
|
||||||
|
}
|
||||||
|
|
||||||
|
public event RoutedEventHandler CatalogRefreshClicked
|
||||||
|
{
|
||||||
|
add { AddHandler(CatalogRefreshClickedEvent, value); }
|
||||||
|
remove { RemoveHandler(CatalogRefreshClickedEvent, value); }
|
||||||
|
}
|
||||||
|
|
||||||
|
public event RoutedEventHandler CatalogAutoRefreshChanged
|
||||||
|
{
|
||||||
|
add { AddHandler(CatalogAutoRefreshChangedEvent, value); }
|
||||||
|
remove { RemoveHandler(CatalogAutoRefreshChangedEvent, value); }
|
||||||
|
}
|
||||||
|
|
||||||
|
public event RoutedEventHandler CatalogSearchChanged
|
||||||
|
{
|
||||||
|
add { AddHandler(CatalogSearchChangedEvent, value); }
|
||||||
|
remove { RemoveHandler(CatalogSearchChangedEvent, value); }
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public class BrowserNavigationEventArgs : RoutedEventArgs
|
public class BrowserNavigationEventArgs : RoutedEventArgs
|
||||||
|
|||||||
@@ -0,0 +1,241 @@
|
|||||||
|
<UserControl x:Class="AutoBidder.Controls.ExportControl"
|
||||||
|
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
|
||||||
|
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||||
|
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
|
||||||
|
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
|
||||||
|
xmlns:ctl="clr-namespace:AutoBidder.Controls"
|
||||||
|
mc:Ignorable="d"
|
||||||
|
d:DesignHeight="800" d:DesignWidth="1200"
|
||||||
|
Background="{DynamicResource Brush.Bg}">
|
||||||
|
|
||||||
|
<UserControl.Resources>
|
||||||
|
<Style x:Key="Glyph" TargetType="TextBlock">
|
||||||
|
<Setter Property="FontFamily" Value="Segoe MDL2 Assets"/>
|
||||||
|
<Setter Property="VerticalAlignment" Value="Center"/>
|
||||||
|
<Setter Property="HorizontalAlignment" Value="Center"/>
|
||||||
|
</Style>
|
||||||
|
|
||||||
|
<Style x:Key="FilterLabel" TargetType="TextBlock">
|
||||||
|
<Setter Property="Foreground" Value="{DynamicResource Brush.Text}"/>
|
||||||
|
<Setter Property="VerticalAlignment" Value="Center"/>
|
||||||
|
<Setter Property="Margin" Value="0,8"/>
|
||||||
|
</Style>
|
||||||
|
</UserControl.Resources>
|
||||||
|
|
||||||
|
<Grid>
|
||||||
|
<Grid.RowDefinitions>
|
||||||
|
<RowDefinition Height="Auto"/>
|
||||||
|
<RowDefinition Height="*"/>
|
||||||
|
</Grid.RowDefinitions>
|
||||||
|
|
||||||
|
<!-- ═══ Barra strumenti ═══ -->
|
||||||
|
<Border Grid.Row="0" Background="{DynamicResource Brush.Surface}" Padding="10,7"
|
||||||
|
BorderBrush="{DynamicResource Brush.Border}" BorderThickness="0,0,0,1">
|
||||||
|
<Grid>
|
||||||
|
<Grid.ColumnDefinitions>
|
||||||
|
<ColumnDefinition Width="Auto"/>
|
||||||
|
<ColumnDefinition Width="Auto"/>
|
||||||
|
<ColumnDefinition Width="*"/>
|
||||||
|
<ColumnDefinition Width="Auto"/>
|
||||||
|
</Grid.ColumnDefinitions>
|
||||||
|
|
||||||
|
<StackPanel Grid.Column="0" Orientation="Horizontal" VerticalAlignment="Center">
|
||||||
|
<Button x:Name="PreviewButton" Style="{StaticResource IconButton}"
|
||||||
|
ToolTip="Ricalcola quante aste corrispondono ai filtri"
|
||||||
|
Click="PreviewButton_Click">
|
||||||
|
<TextBlock Style="{StaticResource Glyph}" Text=""/>
|
||||||
|
</Button>
|
||||||
|
<TextBlock Text="Esporta" FontWeight="SemiBold" Margin="8,0,0,0"
|
||||||
|
FontSize="{StaticResource Font.Size.Lg}"
|
||||||
|
Foreground="{DynamicResource Brush.Text}" VerticalAlignment="Center"/>
|
||||||
|
<Border Width="1" Background="{DynamicResource Brush.Border}" Margin="10,4"/>
|
||||||
|
</StackPanel>
|
||||||
|
|
||||||
|
<StackPanel Grid.Column="1" Orientation="Horizontal" VerticalAlignment="Center">
|
||||||
|
<Border Style="{StaticResource Pill}" ToolTip="Aste che corrispondono ai filtri">
|
||||||
|
<TextBlock x:Name="MatchCountText" Text="—"
|
||||||
|
FontSize="{StaticResource Font.Size.Sm}"
|
||||||
|
Foreground="{DynamicResource Brush.Text}"/>
|
||||||
|
</Border>
|
||||||
|
<Border Style="{StaticResource Pill}" ToolTip="Di queste, quelle seguite dall'inizio alla fine">
|
||||||
|
<TextBlock x:Name="CompleteCountText" Text="—"
|
||||||
|
FontSize="{StaticResource Font.Size.Sm}"
|
||||||
|
Foreground="{DynamicResource Brush.TextMuted}"/>
|
||||||
|
</Border>
|
||||||
|
</StackPanel>
|
||||||
|
|
||||||
|
<StackPanel Grid.Column="3" Orientation="Horizontal" VerticalAlignment="Center">
|
||||||
|
<Button x:Name="ExportButton" Style="{StaticResource SmallRoundedButton}"
|
||||||
|
Background="{DynamicResource Brush.Accent}"
|
||||||
|
Foreground="{DynamicResource Brush.TextOnAccent}"
|
||||||
|
Content="Esporta in un file" Margin="0,0,8,0"
|
||||||
|
Click="ExportButton_Click"/>
|
||||||
|
|
||||||
|
<Button x:Name="OpenFolderButton" Style="{StaticResource SmallRoundedButton}"
|
||||||
|
Background="{DynamicResource Brush.SurfaceAlt}"
|
||||||
|
Foreground="{DynamicResource Brush.Text}"
|
||||||
|
Content="Apri la cartella" Margin="0,0,8,0"
|
||||||
|
Click="OpenFolderButton_Click"/>
|
||||||
|
|
||||||
|
<Button x:Name="ResetFiltersButton" Style="{StaticResource IconButton}"
|
||||||
|
ToolTip="Azzera i filtri" Click="ResetFiltersButton_Click">
|
||||||
|
<TextBlock Style="{StaticResource Glyph}" Text=""/>
|
||||||
|
</Button>
|
||||||
|
</StackPanel>
|
||||||
|
</Grid>
|
||||||
|
</Border>
|
||||||
|
|
||||||
|
<ScrollViewer Grid.Row="1" VerticalScrollBarVisibility="Auto">
|
||||||
|
<StackPanel Margin="20,16" MaxWidth="1000" HorizontalAlignment="Left">
|
||||||
|
|
||||||
|
<!-- ═══ Cosa esporta ═══ -->
|
||||||
|
<Border Style="{StaticResource CardBlock}">
|
||||||
|
<StackPanel>
|
||||||
|
<ctl:HelpHeader Title="Cosa finisce nel file"
|
||||||
|
Margin="0,0,0,12"
|
||||||
|
Help="Un unico file JSON con le aste scelte: esito, prezzo finale, valore del prodotto, puntate di ogni utente, ping e latenza, andamento del prezzo e — dove esiste il dossier — ogni singolo evento registrato mentre l'asta era in corso. Il file si spiega da sé: contiene la legenda dei campi, così resta leggibile anche fra sei mesi."/>
|
||||||
|
</StackPanel>
|
||||||
|
</Border>
|
||||||
|
|
||||||
|
<!-- ═══ Filtri ═══ -->
|
||||||
|
<Border Style="{StaticResource CardBlock}">
|
||||||
|
<StackPanel>
|
||||||
|
<TextBlock Text="Filtri" Style="{StaticResource BlockTitle}"/>
|
||||||
|
|
||||||
|
<Grid>
|
||||||
|
<Grid.ColumnDefinitions>
|
||||||
|
<ColumnDefinition Width="230"/>
|
||||||
|
<ColumnDefinition Width="*"/>
|
||||||
|
</Grid.ColumnDefinitions>
|
||||||
|
<Grid.RowDefinitions>
|
||||||
|
<RowDefinition Height="Auto"/>
|
||||||
|
<RowDefinition Height="Auto"/>
|
||||||
|
<RowDefinition Height="Auto"/>
|
||||||
|
<RowDefinition Height="Auto"/>
|
||||||
|
</Grid.RowDefinitions>
|
||||||
|
|
||||||
|
<TextBlock Grid.Row="0" Grid.Column="0" Text="Periodo (chiusura)"
|
||||||
|
Style="{StaticResource FilterLabel}"/>
|
||||||
|
<StackPanel Grid.Row="0" Grid.Column="1" Orientation="Horizontal" Margin="0,4">
|
||||||
|
<DatePicker x:Name="FromDatePicker" Width="150"
|
||||||
|
ToolTip="Dalla data di chiusura (vuoto = dall'inizio)"/>
|
||||||
|
<TextBlock Text="→" Margin="8,0" VerticalAlignment="Center"
|
||||||
|
Foreground="{DynamicResource Brush.TextMuted}"/>
|
||||||
|
<DatePicker x:Name="ToDatePicker" Width="150"
|
||||||
|
ToolTip="Fino alla data di chiusura (vuoto = fino a oggi)"/>
|
||||||
|
|
||||||
|
<Button Content="Ultimi 7 giorni" Style="{StaticResource SmallRoundedButton}"
|
||||||
|
Background="{DynamicResource Brush.SurfaceAlt}"
|
||||||
|
Foreground="{DynamicResource Brush.Text}"
|
||||||
|
Margin="12,0,0,0" Click="Last7Days_Click"/>
|
||||||
|
<Button Content="Ultimi 30" Style="{StaticResource SmallRoundedButton}"
|
||||||
|
Background="{DynamicResource Brush.SurfaceAlt}"
|
||||||
|
Foreground="{DynamicResource Brush.Text}"
|
||||||
|
Margin="6,0,0,0" Click="Last30Days_Click"/>
|
||||||
|
</StackPanel>
|
||||||
|
|
||||||
|
<TextBlock Grid.Row="1" Grid.Column="0" Text="Prodotto contiene"
|
||||||
|
Style="{StaticResource FilterLabel}"/>
|
||||||
|
<TextBox Grid.Row="1" Grid.Column="1" x:Name="NameFilterTextBox"
|
||||||
|
Width="320" HorizontalAlignment="Left" Margin="0,4"
|
||||||
|
ToolTip="Parte del nome del prodotto, senza distinzione fra maiuscole"/>
|
||||||
|
|
||||||
|
<TextBlock Grid.Row="2" Grid.Column="0" Text="Prezzo finale (€)"
|
||||||
|
Style="{StaticResource FilterLabel}"/>
|
||||||
|
<StackPanel Grid.Row="2" Grid.Column="1" Orientation="Horizontal" Margin="0,4">
|
||||||
|
<TextBox x:Name="MinPriceTextBox" Width="100"
|
||||||
|
ToolTip="Prezzo finale minimo (vuoto = nessun limite)"/>
|
||||||
|
<TextBlock Text="→" Margin="8,0" VerticalAlignment="Center"
|
||||||
|
Foreground="{DynamicResource Brush.TextMuted}"/>
|
||||||
|
<TextBox x:Name="MaxPriceTextBox" Width="100"
|
||||||
|
ToolTip="Prezzo finale massimo (vuoto = nessun limite)"/>
|
||||||
|
</StackPanel>
|
||||||
|
|
||||||
|
<TextBlock Grid.Row="3" Grid.Column="0" Text="Tetto di aste"
|
||||||
|
Style="{StaticResource FilterLabel}"/>
|
||||||
|
<StackPanel Grid.Row="3" Grid.Column="1" Orientation="Horizontal" Margin="0,4">
|
||||||
|
<TextBox x:Name="MaxAuctionsTextBox" Width="100" Text="0"
|
||||||
|
ToolTip="0 = tutte. Con gli eventi inclusi, cento aste sono già diversi megabyte."/>
|
||||||
|
<TextBlock Text="0 = tutte quelle che corrispondono" VerticalAlignment="Center"
|
||||||
|
Margin="10,0,0,0" FontSize="{StaticResource Font.Size.Sm}"
|
||||||
|
Foreground="{DynamicResource Brush.TextMuted}"/>
|
||||||
|
</StackPanel>
|
||||||
|
</Grid>
|
||||||
|
</StackPanel>
|
||||||
|
</Border>
|
||||||
|
|
||||||
|
<!-- ═══ Quali aste ═══ -->
|
||||||
|
<Border Style="{StaticResource CardBlock}">
|
||||||
|
<StackPanel>
|
||||||
|
<TextBlock Text="Quali aste" Style="{StaticResource BlockTitle}"/>
|
||||||
|
|
||||||
|
<Grid>
|
||||||
|
<Grid.ColumnDefinitions>
|
||||||
|
<ColumnDefinition Width="*"/>
|
||||||
|
<ColumnDefinition Width="*"/>
|
||||||
|
</Grid.ColumnDefinitions>
|
||||||
|
|
||||||
|
<StackPanel Grid.Column="0">
|
||||||
|
<CheckBox x:Name="IncludeWonCheckBox" IsChecked="True" Margin="0,4"
|
||||||
|
Content="Vinte" Checked="Filter_Changed" Unchecked="Filter_Changed"/>
|
||||||
|
<CheckBox x:Name="IncludeLostCheckBox" IsChecked="True" Margin="0,4"
|
||||||
|
Content="Perse (ci avevo puntato)" Checked="Filter_Changed" Unchecked="Filter_Changed"/>
|
||||||
|
<CheckBox x:Name="IncludeClosedCheckBox" IsChecked="True" Margin="0,4"
|
||||||
|
Content="Solo osservate (mai puntato)"
|
||||||
|
ToolTip="Non le ho giocate, ma dicono a quanto chiude il mercato"
|
||||||
|
Checked="Filter_Changed" Unchecked="Filter_Changed"/>
|
||||||
|
</StackPanel>
|
||||||
|
|
||||||
|
<StackPanel Grid.Column="1">
|
||||||
|
<CheckBox x:Name="OnlyCompleteCheckBox" Margin="0,4"
|
||||||
|
Content="Solo quelle seguite dall'inizio alla fine"
|
||||||
|
ToolTip="Sono le più preziose: su un'asta presa a metà non si sa quante puntate erano già state spese"
|
||||||
|
Checked="Filter_Changed" Unchecked="Filter_Changed"/>
|
||||||
|
<CheckBox x:Name="OnlyWithMyBidsCheckBox" Margin="0,4"
|
||||||
|
Content="Solo quelle su cui ho puntato"
|
||||||
|
Checked="Filter_Changed" Unchecked="Filter_Changed"/>
|
||||||
|
<CheckBox x:Name="OnlyWithDossierCheckBox" Margin="0,4"
|
||||||
|
Content="Solo quelle con il dossier degli eventi"
|
||||||
|
Checked="Filter_Changed" Unchecked="Filter_Changed"/>
|
||||||
|
</StackPanel>
|
||||||
|
</Grid>
|
||||||
|
|
||||||
|
<Border Background="{DynamicResource Brush.SurfaceAlt}" CornerRadius="8"
|
||||||
|
Padding="12,10" Margin="0,12,0,0">
|
||||||
|
<StackPanel>
|
||||||
|
<CheckBox x:Name="IncludeEventsCheckBox" IsChecked="True"
|
||||||
|
Content="Includi tutti gli eventi registrati (dossier completo)"
|
||||||
|
ToolTip="Senza, il file contiene solo i riepiloghi: molto più leggero, molto meno utile. Gli eventi sono ogni interrogazione a Bidoo, ogni puntata di ogni utente con l'ora al millisecondo e ogni mia puntata con anticipo pianificato ed effettivo. È il materiale con cui si tara l'anticipo; è anche ciò che fa pesare il file."/>
|
||||||
|
</StackPanel>
|
||||||
|
</Border>
|
||||||
|
</StackPanel>
|
||||||
|
</Border>
|
||||||
|
|
||||||
|
<!-- ═══ Esito ═══ -->
|
||||||
|
<Border Style="{StaticResource CardBlock}">
|
||||||
|
<StackPanel>
|
||||||
|
<TextBlock Text="Ultima esportazione" Style="{StaticResource BlockTitle}"/>
|
||||||
|
|
||||||
|
<Border Background="{DynamicResource Brush.SurfaceAlt}" CornerRadius="8" Padding="12,10">
|
||||||
|
<StackPanel Orientation="Horizontal">
|
||||||
|
<TextBlock Style="{StaticResource Glyph}" Text="" FontSize="13"
|
||||||
|
Foreground="{DynamicResource Brush.Accent}" Margin="0,0,8,0"/>
|
||||||
|
<TextBlock x:Name="ResultText" VerticalAlignment="Center" TextWrapping="Wrap"
|
||||||
|
Text="Nessuna esportazione in questa sessione."
|
||||||
|
FontSize="{StaticResource Font.Size.Sm}"
|
||||||
|
Foreground="{DynamicResource Brush.TextMuted}"/>
|
||||||
|
</StackPanel>
|
||||||
|
</Border>
|
||||||
|
|
||||||
|
<Button x:Name="OpenLastFileButton" HorizontalAlignment="Left"
|
||||||
|
Style="{StaticResource SmallRoundedButton}"
|
||||||
|
Background="{DynamicResource Brush.SurfaceAlt}"
|
||||||
|
Foreground="{DynamicResource Brush.Text}"
|
||||||
|
Content="Apri il file" Margin="0,10,0,0"
|
||||||
|
Visibility="Collapsed" Click="OpenLastFileButton_Click"/>
|
||||||
|
</StackPanel>
|
||||||
|
</Border>
|
||||||
|
</StackPanel>
|
||||||
|
</ScrollViewer>
|
||||||
|
</Grid>
|
||||||
|
</UserControl>
|
||||||
@@ -0,0 +1,192 @@
|
|||||||
|
using System;
|
||||||
|
using System.Globalization;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Windows;
|
||||||
|
using System.Windows.Controls;
|
||||||
|
using AutoBidder.Utilities;
|
||||||
|
|
||||||
|
namespace AutoBidder.Controls
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Scheda Esporta: sceglie quali aste finiscono in un unico file e lo scrive.
|
||||||
|
///
|
||||||
|
/// <para>Come gli altri pannelli non conosce gli archivi: costruisce un
|
||||||
|
/// <see cref="AuctionExportFilter"/> e lascia il lavoro a
|
||||||
|
/// <see cref="AuctionExporter"/>. Qui dentro c'è solo la traduzione fra i campi
|
||||||
|
/// dell'interfaccia e i criteri — e il conteggio in tempo reale, che serve a non
|
||||||
|
/// scoprire dopo un minuto di scrittura che il filtro non selezionava nulla.</para>
|
||||||
|
/// </summary>
|
||||||
|
public partial class ExportControl : UserControl
|
||||||
|
{
|
||||||
|
private string? _lastExportPath;
|
||||||
|
|
||||||
|
public ExportControl()
|
||||||
|
{
|
||||||
|
InitializeComponent();
|
||||||
|
Loaded += (_, _) => RefreshPreview();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>I criteri come sono impostati adesso.</summary>
|
||||||
|
public AuctionExportFilter BuildFilter() => new()
|
||||||
|
{
|
||||||
|
From = FromDatePicker.SelectedDate,
|
||||||
|
To = ToDatePicker.SelectedDate,
|
||||||
|
NameContains = string.IsNullOrWhiteSpace(NameFilterTextBox.Text) ? null : NameFilterTextBox.Text.Trim(),
|
||||||
|
|
||||||
|
IncludeWon = IncludeWonCheckBox.IsChecked == true,
|
||||||
|
IncludeLost = IncludeLostCheckBox.IsChecked == true,
|
||||||
|
IncludeClosed = IncludeClosedCheckBox.IsChecked == true,
|
||||||
|
|
||||||
|
OnlyComplete = OnlyCompleteCheckBox.IsChecked == true,
|
||||||
|
OnlyWithMyBids = OnlyWithMyBidsCheckBox.IsChecked == true,
|
||||||
|
OnlyWithDossier = OnlyWithDossierCheckBox.IsChecked == true,
|
||||||
|
|
||||||
|
MinFinalPrice = ParseNumber(MinPriceTextBox.Text),
|
||||||
|
MaxFinalPrice = ParseNumber(MaxPriceTextBox.Text),
|
||||||
|
MaxAuctions = (int)(ParseNumber(MaxAuctionsTextBox.Text) ?? 0),
|
||||||
|
|
||||||
|
IncludeEvents = IncludeEventsCheckBox.IsChecked == true
|
||||||
|
};
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Ricalcola quante aste corrispondono. Si fa a ogni modifica dei filtri: sapere
|
||||||
|
/// prima che il risultato è vuoto — o che sono duemila — evita di scoprirlo a file
|
||||||
|
/// scritto.
|
||||||
|
/// </summary>
|
||||||
|
public void RefreshPreview()
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var selected = AuctionExporter.Select(BuildFilter());
|
||||||
|
|
||||||
|
MatchCountText.Text = selected.Count == 1 ? "1 asta" : $"{selected.Count} aste";
|
||||||
|
|
||||||
|
var complete = selected.Count(r => r.IsComplete);
|
||||||
|
CompleteCountText.Text = $"{complete} complete";
|
||||||
|
|
||||||
|
ExportButton.IsEnabled = selected.Count > 0;
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
MatchCountText.Text = "—";
|
||||||
|
CompleteCountText.Text = ex.Message;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>Mostra l'esito dell'esportazione appena fatta.</summary>
|
||||||
|
public void ShowResult(AuctionExportResult result)
|
||||||
|
{
|
||||||
|
if (result.Success)
|
||||||
|
{
|
||||||
|
_lastExportPath = result.Path;
|
||||||
|
|
||||||
|
var size = result.Bytes >= 1024 * 1024
|
||||||
|
? $"{result.Bytes / 1024.0 / 1024.0:F1} MB"
|
||||||
|
: $"{Math.Max(1, result.Bytes / 1024)} kB";
|
||||||
|
|
||||||
|
ResultText.Text = $"{result.AuctionCount} aste ({result.WithEvents} con eventi), {size} — {result.Path}";
|
||||||
|
ResultText.SetResourceReference(ForegroundProperty, "Brush.Text");
|
||||||
|
OpenLastFileButton.Visibility = Visibility.Visible;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
ResultText.Text = result.Message;
|
||||||
|
ResultText.SetResourceReference(ForegroundProperty, "Brush.Warning");
|
||||||
|
OpenLastFileButton.Visibility = Visibility.Collapsed;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>Percorso dell'ultimo file scritto, per il pulsante "Apri il file".</summary>
|
||||||
|
public string? LastExportPath => _lastExportPath;
|
||||||
|
|
||||||
|
// ── Eventi dell'interfaccia ──────────────────────────────────────
|
||||||
|
|
||||||
|
private void PreviewButton_Click(object sender, RoutedEventArgs e) => RefreshPreview();
|
||||||
|
|
||||||
|
private void Filter_Changed(object sender, RoutedEventArgs e) => RefreshPreview();
|
||||||
|
|
||||||
|
private void Last7Days_Click(object sender, RoutedEventArgs e) => SetPeriod(7);
|
||||||
|
|
||||||
|
private void Last30Days_Click(object sender, RoutedEventArgs e) => SetPeriod(30);
|
||||||
|
|
||||||
|
private void SetPeriod(int days)
|
||||||
|
{
|
||||||
|
FromDatePicker.SelectedDate = DateTime.Today.AddDays(-days);
|
||||||
|
ToDatePicker.SelectedDate = DateTime.Today;
|
||||||
|
RefreshPreview();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void ResetFiltersButton_Click(object sender, RoutedEventArgs e)
|
||||||
|
{
|
||||||
|
FromDatePicker.SelectedDate = null;
|
||||||
|
ToDatePicker.SelectedDate = null;
|
||||||
|
NameFilterTextBox.Text = "";
|
||||||
|
MinPriceTextBox.Text = "";
|
||||||
|
MaxPriceTextBox.Text = "";
|
||||||
|
MaxAuctionsTextBox.Text = "0";
|
||||||
|
|
||||||
|
IncludeWonCheckBox.IsChecked = true;
|
||||||
|
IncludeLostCheckBox.IsChecked = true;
|
||||||
|
IncludeClosedCheckBox.IsChecked = true;
|
||||||
|
OnlyCompleteCheckBox.IsChecked = false;
|
||||||
|
OnlyWithMyBidsCheckBox.IsChecked = false;
|
||||||
|
OnlyWithDossierCheckBox.IsChecked = false;
|
||||||
|
IncludeEventsCheckBox.IsChecked = true;
|
||||||
|
|
||||||
|
RefreshPreview();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void ExportButton_Click(object sender, RoutedEventArgs e)
|
||||||
|
=> RaiseEvent(new RoutedEventArgs(ExportRequestedEvent, this));
|
||||||
|
|
||||||
|
private void OpenFolderButton_Click(object sender, RoutedEventArgs e)
|
||||||
|
=> RaiseEvent(new RoutedEventArgs(OpenExportFolderClickedEvent, this));
|
||||||
|
|
||||||
|
private void OpenLastFileButton_Click(object sender, RoutedEventArgs e)
|
||||||
|
=> RaiseEvent(new RoutedEventArgs(OpenLastFileClickedEvent, this));
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Legge un numero accettando sia la virgola sia il punto: chi scrive "12,50" e chi
|
||||||
|
/// scrive "12.50" intende la stessa cosa, e rifiutarne uno dei due sarebbe pedanteria.
|
||||||
|
/// </summary>
|
||||||
|
private static double? ParseNumber(string? text)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrWhiteSpace(text)) return null;
|
||||||
|
|
||||||
|
var clean = text.Trim().Replace(',', '.');
|
||||||
|
|
||||||
|
return double.TryParse(clean, NumberStyles.Any, CultureInfo.InvariantCulture, out var value)
|
||||||
|
? value
|
||||||
|
: null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Routed events ────────────────────────────────────────────────
|
||||||
|
|
||||||
|
public static readonly RoutedEvent ExportRequestedEvent = EventManager.RegisterRoutedEvent(
|
||||||
|
"ExportRequested", RoutingStrategy.Bubble, typeof(RoutedEventHandler), typeof(ExportControl));
|
||||||
|
|
||||||
|
public static readonly RoutedEvent OpenExportFolderClickedEvent = EventManager.RegisterRoutedEvent(
|
||||||
|
"OpenExportFolderClicked", RoutingStrategy.Bubble, typeof(RoutedEventHandler), typeof(ExportControl));
|
||||||
|
|
||||||
|
public static readonly RoutedEvent OpenLastFileClickedEvent = EventManager.RegisterRoutedEvent(
|
||||||
|
"OpenLastFileClicked", RoutingStrategy.Bubble, typeof(RoutedEventHandler), typeof(ExportControl));
|
||||||
|
|
||||||
|
public event RoutedEventHandler ExportRequested
|
||||||
|
{
|
||||||
|
add { AddHandler(ExportRequestedEvent, value); }
|
||||||
|
remove { RemoveHandler(ExportRequestedEvent, value); }
|
||||||
|
}
|
||||||
|
|
||||||
|
public event RoutedEventHandler OpenExportFolderClicked
|
||||||
|
{
|
||||||
|
add { AddHandler(OpenExportFolderClickedEvent, value); }
|
||||||
|
remove { RemoveHandler(OpenExportFolderClickedEvent, value); }
|
||||||
|
}
|
||||||
|
|
||||||
|
public event RoutedEventHandler OpenLastFileClicked
|
||||||
|
{
|
||||||
|
add { AddHandler(OpenLastFileClickedEvent, value); }
|
||||||
|
remove { RemoveHandler(OpenLastFileClickedEvent, value); }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,365 @@
|
|||||||
|
<UserControl x:Class="AutoBidder.Controls.FreeBidsControl"
|
||||||
|
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
|
||||||
|
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||||
|
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
|
||||||
|
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
|
||||||
|
xmlns:ctl="clr-namespace:AutoBidder.Controls"
|
||||||
|
mc:Ignorable="d"
|
||||||
|
d:DesignHeight="800" d:DesignWidth="1200"
|
||||||
|
Background="{DynamicResource Brush.Bg}">
|
||||||
|
|
||||||
|
<UserControl.Resources>
|
||||||
|
<Style x:Key="Glyph" TargetType="TextBlock">
|
||||||
|
<Setter Property="FontFamily" Value="Segoe MDL2 Assets"/>
|
||||||
|
<Setter Property="VerticalAlignment" Value="Center"/>
|
||||||
|
<Setter Property="HorizontalAlignment" Value="Center"/>
|
||||||
|
</Style>
|
||||||
|
|
||||||
|
<Style x:Key="CounterCaption" TargetType="TextBlock">
|
||||||
|
<Setter Property="FontSize" Value="{StaticResource Font.Size.Sm}"/>
|
||||||
|
<Setter Property="Foreground" Value="{DynamicResource Brush.TextFaint}"/>
|
||||||
|
<Setter Property="Margin" Value="0,0,0,3"/>
|
||||||
|
</Style>
|
||||||
|
|
||||||
|
<Style x:Key="CounterValue" TargetType="TextBlock">
|
||||||
|
<Setter Property="FontSize" Value="22"/>
|
||||||
|
<Setter Property="FontWeight" Value="SemiBold"/>
|
||||||
|
<Setter Property="Foreground" Value="{DynamicResource Brush.Text}"/>
|
||||||
|
</Style>
|
||||||
|
</UserControl.Resources>
|
||||||
|
|
||||||
|
<Grid>
|
||||||
|
<Grid.RowDefinitions>
|
||||||
|
<RowDefinition Height="Auto"/>
|
||||||
|
<RowDefinition Height="*"/>
|
||||||
|
</Grid.RowDefinitions>
|
||||||
|
|
||||||
|
<!-- ═══ Barra strumenti ═══ -->
|
||||||
|
<Border Grid.Row="0" Background="{DynamicResource Brush.Surface}" Padding="10,7"
|
||||||
|
BorderBrush="{DynamicResource Brush.Border}" BorderThickness="0,0,0,1">
|
||||||
|
<Grid>
|
||||||
|
<Grid.ColumnDefinitions>
|
||||||
|
<ColumnDefinition Width="Auto"/>
|
||||||
|
<ColumnDefinition Width="Auto"/>
|
||||||
|
<ColumnDefinition Width="*"/>
|
||||||
|
<ColumnDefinition Width="Auto"/>
|
||||||
|
</Grid.ColumnDefinitions>
|
||||||
|
|
||||||
|
<StackPanel Grid.Column="0" Orientation="Horizontal" VerticalAlignment="Center">
|
||||||
|
<Button x:Name="RefreshButton" Style="{StaticResource IconButton}"
|
||||||
|
ToolTip="Aggiorna il saldo puntate" Click="RefreshButton_Click">
|
||||||
|
<TextBlock Style="{StaticResource Glyph}" Text=""/>
|
||||||
|
</Button>
|
||||||
|
<TextBlock Text="Puntate" FontWeight="SemiBold" Margin="8,0,0,0"
|
||||||
|
FontSize="{StaticResource Font.Size.Lg}"
|
||||||
|
Foreground="{DynamicResource Brush.Text}" VerticalAlignment="Center"/>
|
||||||
|
<Border Width="1" Background="{DynamicResource Brush.Border}" Margin="10,4"/>
|
||||||
|
</StackPanel>
|
||||||
|
|
||||||
|
<StackPanel Grid.Column="1" Orientation="Horizontal" VerticalAlignment="Center">
|
||||||
|
<Border Style="{StaticResource Pill}" ToolTip="Puntate attualmente sul conto">
|
||||||
|
<TextBlock x:Name="BalanceText" Text="— puntate"
|
||||||
|
FontSize="{StaticResource Font.Size.Sm}"
|
||||||
|
Foreground="{DynamicResource Brush.Text}"/>
|
||||||
|
</Border>
|
||||||
|
<Border Style="{StaticResource Pill}" ToolTip="Stato del riscatto automatico">
|
||||||
|
<TextBlock x:Name="StatusPillText" Text="riscatto spento"
|
||||||
|
FontSize="{StaticResource Font.Size.Sm}"
|
||||||
|
Foreground="{DynamicResource Brush.TextMuted}"/>
|
||||||
|
</Border>
|
||||||
|
</StackPanel>
|
||||||
|
|
||||||
|
<StackPanel Grid.Column="3" Orientation="Horizontal" VerticalAlignment="Center">
|
||||||
|
<Button x:Name="CheckNowButton" Style="{StaticResource SmallRoundedButton}"
|
||||||
|
Background="{DynamicResource Brush.Accent}"
|
||||||
|
Foreground="{DynamicResource Brush.TextOnAccent}"
|
||||||
|
Content="Controlla adesso" Margin="0,0,8,0"
|
||||||
|
ToolTip="Cerca subito ricompense da riscuotere, senza aspettare il prossimo giro"
|
||||||
|
Click="CheckNowButton_Click"/>
|
||||||
|
|
||||||
|
<Button x:Name="OpenExternalButton" Style="{StaticResource SmallRoundedButton}"
|
||||||
|
Background="{DynamicResource Brush.SurfaceAlt}"
|
||||||
|
Foreground="{DynamicResource Brush.Text}"
|
||||||
|
Margin="0,0,8,0"
|
||||||
|
ToolTip="Apre la pagina delle ricompense nel browser predefinito di Windows"
|
||||||
|
Click="OpenExternalButton_Click">
|
||||||
|
<StackPanel Orientation="Horizontal">
|
||||||
|
<TextBlock FontFamily="Segoe MDL2 Assets" Text="" FontSize="11"
|
||||||
|
VerticalAlignment="Center" Margin="0,0,6,0"/>
|
||||||
|
<TextBlock Text="Apri su Bidoo" VerticalAlignment="Center"/>
|
||||||
|
</StackPanel>
|
||||||
|
</Button>
|
||||||
|
|
||||||
|
<Button x:Name="OpenInternalButton" Style="{StaticResource IconButton}"
|
||||||
|
ToolTip="Apri le ricompense nel browser integrato (è lì che arrivano le notifiche)"
|
||||||
|
Click="OpenInternalButton_Click">
|
||||||
|
<TextBlock Style="{StaticResource Glyph}" Text=""/>
|
||||||
|
</Button>
|
||||||
|
</StackPanel>
|
||||||
|
</Grid>
|
||||||
|
</Border>
|
||||||
|
|
||||||
|
<ScrollViewer Grid.Row="1" VerticalScrollBarVisibility="Auto">
|
||||||
|
<StackPanel Margin="20,16" MaxWidth="1000" HorizontalAlignment="Left">
|
||||||
|
|
||||||
|
<!-- ═══ Riscatto automatico ═══ -->
|
||||||
|
<Border Style="{StaticResource CardBlock}">
|
||||||
|
<StackPanel>
|
||||||
|
<ctl:HelpHeader Title="Riscatto automatico"
|
||||||
|
Margin="0,0,0,12"
|
||||||
|
Help="Bidoo lascia in attesa le ricompense dei giorni scorsi. Con questo acceso, l'applicazione controlla a intervalli regolari e riscuote quello che trova, mentre continua a seguire le aste."/>
|
||||||
|
|
||||||
|
<Grid>
|
||||||
|
<Grid.ColumnDefinitions>
|
||||||
|
<ColumnDefinition Width="300"/>
|
||||||
|
<ColumnDefinition Width="Auto"/>
|
||||||
|
</Grid.ColumnDefinitions>
|
||||||
|
<Grid.RowDefinitions>
|
||||||
|
<RowDefinition Height="Auto"/>
|
||||||
|
<RowDefinition Height="Auto"/>
|
||||||
|
</Grid.RowDefinitions>
|
||||||
|
|
||||||
|
<TextBlock Grid.Row="0" Grid.Column="0" Text="Riscuoti da solo le ricompense"
|
||||||
|
Foreground="{DynamicResource Brush.Text}" Margin="0,8"
|
||||||
|
VerticalAlignment="Center"/>
|
||||||
|
<CheckBox Grid.Row="0" Grid.Column="1" x:Name="AutoClaimCheckBox"
|
||||||
|
Margin="10,8" VerticalAlignment="Center"
|
||||||
|
Checked="AutoClaimCheckBox_Changed" Unchecked="AutoClaimCheckBox_Changed"/>
|
||||||
|
|
||||||
|
<TextBlock Grid.Row="1" Grid.Column="0" Text="Controlla ogni (minuti)"
|
||||||
|
Foreground="{DynamicResource Brush.Text}" Margin="0,8"
|
||||||
|
VerticalAlignment="Center"
|
||||||
|
ToolTip="Minimo 5 minuti: le ricompense sono giornaliere"/>
|
||||||
|
<TextBox Grid.Row="1" Grid.Column="1" x:Name="CheckMinutesTextBox"
|
||||||
|
Text="30" Width="90" Margin="10,8" HorizontalAlignment="Left"
|
||||||
|
LostFocus="CheckMinutesTextBox_LostFocus"/>
|
||||||
|
</Grid>
|
||||||
|
|
||||||
|
<Border Background="{DynamicResource Brush.SurfaceAlt}" CornerRadius="8"
|
||||||
|
Padding="12,10" Margin="0,10,0,0">
|
||||||
|
<StackPanel Orientation="Horizontal">
|
||||||
|
<TextBlock Style="{StaticResource Glyph}" Text="" FontSize="13"
|
||||||
|
Foreground="{DynamicResource Brush.Accent}" Margin="0,0,8,0"/>
|
||||||
|
<TextBlock x:Name="ScheduleText" VerticalAlignment="Center"
|
||||||
|
Text="Riscatto automatico spento."
|
||||||
|
FontSize="{StaticResource Font.Size.Sm}"
|
||||||
|
Foreground="{DynamicResource Brush.TextMuted}"/>
|
||||||
|
</StackPanel>
|
||||||
|
</Border>
|
||||||
|
</StackPanel>
|
||||||
|
</Border>
|
||||||
|
|
||||||
|
<!-- ═══ Contatori ═══ -->
|
||||||
|
<Border Style="{StaticResource CardBlock}">
|
||||||
|
<StackPanel>
|
||||||
|
<Grid Margin="0,0,0,12">
|
||||||
|
<TextBlock Text="Quanto ho riscattato" Style="{StaticResource BlockTitle}"
|
||||||
|
Margin="0"/>
|
||||||
|
<Button x:Name="ResetCountersButton" HorizontalAlignment="Right"
|
||||||
|
Style="{StaticResource SmallRoundedButton}"
|
||||||
|
Background="{DynamicResource Brush.SurfaceAlt}"
|
||||||
|
Foreground="{DynamicResource Brush.Danger}"
|
||||||
|
Content="Azzera contatori"
|
||||||
|
Click="ResetCountersButton_Click"/>
|
||||||
|
</Grid>
|
||||||
|
|
||||||
|
<UniformGrid Columns="4">
|
||||||
|
<StackPanel Margin="0,0,16,0">
|
||||||
|
<TextBlock Text="Puntate — questa sessione" Style="{StaticResource CounterCaption}"/>
|
||||||
|
<TextBlock x:Name="SessionBidsText" Text="0" Style="{StaticResource CounterValue}"
|
||||||
|
Foreground="{DynamicResource Brush.Accent}"/>
|
||||||
|
</StackPanel>
|
||||||
|
<StackPanel Margin="0,0,16,0">
|
||||||
|
<TextBlock Text="Puntate — oggi" Style="{StaticResource CounterCaption}"/>
|
||||||
|
<TextBlock x:Name="TodayBidsText" Text="0" Style="{StaticResource CounterValue}"/>
|
||||||
|
</StackPanel>
|
||||||
|
<StackPanel Margin="0,0,16,0">
|
||||||
|
<TextBlock Text="Puntate — da sempre" Style="{StaticResource CounterCaption}"/>
|
||||||
|
<TextBlock x:Name="TotalBidsText" Text="0" Style="{StaticResource CounterValue}"
|
||||||
|
Foreground="{DynamicResource Brush.Gold}"/>
|
||||||
|
</StackPanel>
|
||||||
|
<StackPanel>
|
||||||
|
<TextBlock Text="Ricompense riscosse" Style="{StaticResource CounterCaption}"/>
|
||||||
|
<TextBlock x:Name="TotalClaimedText" Text="0" Style="{StaticResource CounterValue}"/>
|
||||||
|
</StackPanel>
|
||||||
|
</UniformGrid>
|
||||||
|
|
||||||
|
<TextBlock x:Name="CountersDetailText" Margin="0,12,0,0"
|
||||||
|
FontSize="{StaticResource Font.Size.Sm}"
|
||||||
|
Foreground="{DynamicResource Brush.TextFaint}"
|
||||||
|
Text="Nessun controllo ancora eseguito."/>
|
||||||
|
</StackPanel>
|
||||||
|
</Border>
|
||||||
|
|
||||||
|
<!-- ═══ Registro ═══ -->
|
||||||
|
<Border Style="{StaticResource CardBlock}">
|
||||||
|
<StackPanel>
|
||||||
|
<Grid Margin="0,0,0,10">
|
||||||
|
<TextBlock Text="Registro dei riscatti" Style="{StaticResource BlockTitle}"
|
||||||
|
Margin="0"/>
|
||||||
|
<Button x:Name="ClearLogButton" HorizontalAlignment="Right"
|
||||||
|
Style="{StaticResource SmallRoundedButton}"
|
||||||
|
Background="{DynamicResource Brush.SurfaceAlt}"
|
||||||
|
Foreground="{DynamicResource Brush.Text}"
|
||||||
|
Content="Pulisci" Click="ClearLogButton_Click"/>
|
||||||
|
</Grid>
|
||||||
|
|
||||||
|
<Border Background="{DynamicResource Brush.Bg}" CornerRadius="8"
|
||||||
|
BorderBrush="{DynamicResource Brush.Border}" BorderThickness="1"
|
||||||
|
Height="190">
|
||||||
|
<Grid>
|
||||||
|
<ListBox x:Name="ActivityList" Background="Transparent"
|
||||||
|
BorderThickness="0" Margin="4"
|
||||||
|
ScrollViewer.HorizontalScrollBarVisibility="Disabled">
|
||||||
|
<ListBox.ItemTemplate>
|
||||||
|
<DataTemplate>
|
||||||
|
<StackPanel Orientation="Horizontal" Margin="2,1">
|
||||||
|
<TextBlock Text="{Binding Time}" FontSize="11"
|
||||||
|
Foreground="{DynamicResource Brush.TextFaint}"
|
||||||
|
Margin="0,0,10,0" VerticalAlignment="Top"/>
|
||||||
|
<TextBlock Text="{Binding Message}" FontSize="12"
|
||||||
|
TextWrapping="Wrap" MaxWidth="760">
|
||||||
|
<TextBlock.Style>
|
||||||
|
<Style TargetType="TextBlock">
|
||||||
|
<Setter Property="Foreground" Value="{DynamicResource Brush.Text}"/>
|
||||||
|
<Style.Triggers>
|
||||||
|
<DataTrigger Binding="{Binding IsProblem}" Value="True">
|
||||||
|
<Setter Property="Foreground" Value="{DynamicResource Brush.Warning}"/>
|
||||||
|
</DataTrigger>
|
||||||
|
</Style.Triggers>
|
||||||
|
</Style>
|
||||||
|
</TextBlock.Style>
|
||||||
|
</TextBlock>
|
||||||
|
</StackPanel>
|
||||||
|
</DataTemplate>
|
||||||
|
</ListBox.ItemTemplate>
|
||||||
|
</ListBox>
|
||||||
|
|
||||||
|
<TextBlock x:Name="ActivityEmptyText"
|
||||||
|
Text="Nessun riscatto registrato in questa sessione."
|
||||||
|
HorizontalAlignment="Center" VerticalAlignment="Center"
|
||||||
|
FontSize="12"
|
||||||
|
Foreground="{DynamicResource Brush.TextMuted}"/>
|
||||||
|
</Grid>
|
||||||
|
</Border>
|
||||||
|
</StackPanel>
|
||||||
|
</Border>
|
||||||
|
|
||||||
|
<!-- ═══ Collegamenti promozionali pubblicati ═══ -->
|
||||||
|
<Border Style="{StaticResource CardBlock}">
|
||||||
|
<StackPanel>
|
||||||
|
<ctl:HelpHeader Title="Puntate gratis dai collegamenti pubblicati"
|
||||||
|
Margin="0,0,0,12"
|
||||||
|
Help="Bidoo regala puntate con collegamenti già firmati che i siti di raccolta ripubblicano. L'applicazione legge quella pagina, scarta i collegamenti già presi e apre i nuovi con la tua sessione, uno alla volta. I codici presi restano in memoria: senza, ogni giro riaprirebbe gli stessi."/>
|
||||||
|
|
||||||
|
<StackPanel Orientation="Horizontal" Margin="0,0,0,10">
|
||||||
|
<Button x:Name="HarvestButton" Style="{StaticResource SmallRoundedButton}"
|
||||||
|
Background="{DynamicResource Brush.Accent}"
|
||||||
|
Foreground="{DynamicResource Brush.TextOnAccent}"
|
||||||
|
Content="Cerca collegamenti adesso" Margin="0,0,8,0"
|
||||||
|
ToolTip="Legge subito la pagina dei collegamenti e riscuote quelli nuovi"
|
||||||
|
Click="HarvestButton_Click"/>
|
||||||
|
|
||||||
|
<Button x:Name="OpenSourceButton" Style="{StaticResource SmallRoundedButton}"
|
||||||
|
Background="{DynamicResource Brush.SurfaceAlt}"
|
||||||
|
Foreground="{DynamicResource Brush.Text}"
|
||||||
|
Content="Apri la pagina dei collegamenti" Margin="0,0,8,0"
|
||||||
|
ToolTip="Apre nel browser integrato la pagina da cui vengono presi i collegamenti"
|
||||||
|
Click="OpenSourceButton_Click"/>
|
||||||
|
|
||||||
|
<Button x:Name="ForgetPromosButton" Style="{StaticResource SmallRoundedButton}"
|
||||||
|
Background="{DynamicResource Brush.SurfaceAlt}"
|
||||||
|
Foreground="{DynamicResource Brush.Text}"
|
||||||
|
Content="Dimentica i codici presi"
|
||||||
|
ToolTip="Azzera la memoria dei collegamenti già aperti (serve dopo un cambio di account)"
|
||||||
|
Click="ForgetPromosButton_Click"/>
|
||||||
|
</StackPanel>
|
||||||
|
|
||||||
|
<Border Background="{DynamicResource Brush.SurfaceAlt}" CornerRadius="8"
|
||||||
|
Padding="12,10" Margin="0,0,0,4">
|
||||||
|
<StackPanel Orientation="Horizontal">
|
||||||
|
<TextBlock Style="{StaticResource Glyph}" Text="" FontSize="13"
|
||||||
|
Foreground="{DynamicResource Brush.Accent}" Margin="0,0,8,0"/>
|
||||||
|
<TextBlock x:Name="HarvestStatusText" VerticalAlignment="Center"
|
||||||
|
TextWrapping="Wrap"
|
||||||
|
Text="Nessuna raccolta ancora eseguita."
|
||||||
|
FontSize="{StaticResource Font.Size.Sm}"
|
||||||
|
Foreground="{DynamicResource Brush.TextMuted}"/>
|
||||||
|
</StackPanel>
|
||||||
|
</Border>
|
||||||
|
</StackPanel>
|
||||||
|
</Border>
|
||||||
|
|
||||||
|
<!-- ═══ Riscatto da codice o collegamento ═══ -->
|
||||||
|
<Border Style="{StaticResource CardBlock}">
|
||||||
|
<StackPanel>
|
||||||
|
<ctl:HelpHeader Title="Codice o collegamento singolo"
|
||||||
|
Margin="0,0,0,12"
|
||||||
|
Help="Per un collegamento ricevuto per email o per messaggio, che la pagina di raccolta non ha. Incollalo intero: viene aperto con la sessione attiva, come se ci avessi cliccato sopra. Quelli che non portano a Bidoo vengono rifiutati. Un codice da solo di norma non basta — questi indirizzi portano una firma (sign) che non si può ricostruire."/>
|
||||||
|
|
||||||
|
<StackPanel Orientation="Horizontal">
|
||||||
|
<TextBox x:Name="PromoCodeTextBox" Width="420" VerticalAlignment="Center"
|
||||||
|
KeyDown="PromoCodeTextBox_KeyDown"
|
||||||
|
ToolTip="Codice promozionale, oppure indirizzo completo del riscatto"/>
|
||||||
|
<Button x:Name="ClaimPromoButton" Style="{StaticResource SmallRoundedButton}"
|
||||||
|
Background="{DynamicResource Brush.Accent}"
|
||||||
|
Foreground="{DynamicResource Brush.TextOnAccent}"
|
||||||
|
Content="Riscatta" Margin="8,0,0,0"
|
||||||
|
Click="ClaimPromoButton_Click"/>
|
||||||
|
</StackPanel>
|
||||||
|
|
||||||
|
<Border Background="{DynamicResource Brush.SurfaceAlt}" CornerRadius="8"
|
||||||
|
Padding="12,10" Margin="0,12,0,0">
|
||||||
|
<Grid>
|
||||||
|
<Grid.ColumnDefinitions>
|
||||||
|
<ColumnDefinition Width="*"/>
|
||||||
|
<ColumnDefinition Width="Auto"/>
|
||||||
|
</Grid.ColumnDefinitions>
|
||||||
|
|
||||||
|
<TextBlock Grid.Column="0" x:Name="ConfigPathText" TextWrapping="Wrap"
|
||||||
|
VerticalAlignment="Center"
|
||||||
|
FontSize="{StaticResource Font.Size.Sm}"
|
||||||
|
Foreground="{DynamicResource Brush.TextMuted}"
|
||||||
|
Text="Indirizzi, intestazioni e parametri del riscatto stanno in site-config.json."/>
|
||||||
|
|
||||||
|
<Button Grid.Column="1" x:Name="OpenConfigButton"
|
||||||
|
Style="{StaticResource SmallRoundedButton}"
|
||||||
|
Background="{DynamicResource Brush.Surface}"
|
||||||
|
Foreground="{DynamicResource Brush.Text}"
|
||||||
|
Content="Apri i riferimenti" Margin="10,0,0,0"
|
||||||
|
ToolTip="Apre il file con indirizzi, intestazioni e parametri del riscatto"
|
||||||
|
Click="OpenConfigButton_Click"/>
|
||||||
|
</Grid>
|
||||||
|
</Border>
|
||||||
|
</StackPanel>
|
||||||
|
</Border>
|
||||||
|
|
||||||
|
<!-- ═══ Cosa resta manuale ═══ -->
|
||||||
|
<Border Style="{StaticResource CardBlock}">
|
||||||
|
<StackPanel>
|
||||||
|
<TextBlock Text="Cosa resta da fare a mano" Style="{StaticResource BlockTitle}"/>
|
||||||
|
|
||||||
|
<Border Style="{StaticResource InfoBox}">
|
||||||
|
<TextBlock TextWrapping="Wrap" FontSize="12" LineHeight="18"
|
||||||
|
Foreground="{DynamicResource Brush.Text}">
|
||||||
|
<Run Text="Le puntate consegnate per notifica non passano da qui." FontWeight="Bold"/>
|
||||||
|
<Run Text=" Bidoo le manda tramite le notifiche push del browser (OneSignal) e per email, con collegamenti personali validi una volta sola: sono legate all'abbonamento del browser, non all'account, quindi non esiste una chiamata da fare con il cookie di sessione. Per quelle apri le ricompense nel browser integrato, concedi le notifiche e lascia la scheda aperta."/>
|
||||||
|
</TextBlock>
|
||||||
|
</Border>
|
||||||
|
|
||||||
|
<StackPanel Orientation="Horizontal" Margin="0,10,0,0">
|
||||||
|
<Button Content="Buoni e voucher" Style="{StaticResource SmallRoundedButton}"
|
||||||
|
Background="{DynamicResource Brush.SurfaceAlt}"
|
||||||
|
Foreground="{DynamicResource Brush.Text}"
|
||||||
|
Margin="0,0,8,0" Click="OpenVouchersButton_Click"/>
|
||||||
|
<Button Content="Acquista puntate" Style="{StaticResource SmallRoundedButton}"
|
||||||
|
Background="{DynamicResource Brush.SurfaceAlt}"
|
||||||
|
Foreground="{DynamicResource Brush.Text}"
|
||||||
|
Click="OpenBuyBidsButton_Click"/>
|
||||||
|
</StackPanel>
|
||||||
|
</StackPanel>
|
||||||
|
</Border>
|
||||||
|
</StackPanel>
|
||||||
|
</ScrollViewer>
|
||||||
|
</Grid>
|
||||||
|
</UserControl>
|
||||||
@@ -0,0 +1,352 @@
|
|||||||
|
using System;
|
||||||
|
using System.Collections.ObjectModel;
|
||||||
|
using System.Windows;
|
||||||
|
using System.Windows.Controls;
|
||||||
|
using AutoBidder.Utilities;
|
||||||
|
|
||||||
|
namespace AutoBidder.Controls
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Scheda Puntate: stato del riscatto automatico, contatori e registro.
|
||||||
|
///
|
||||||
|
/// <para>Come gli altri pannelli non conosce né la rete né gli archivi: mostra ciò che
|
||||||
|
/// gli viene passato e segnala le richieste dell'utente con eventi.</para>
|
||||||
|
/// </summary>
|
||||||
|
public partial class FreeBidsControl : UserControl
|
||||||
|
{
|
||||||
|
/// <summary>Una riga del registro dei riscatti.</summary>
|
||||||
|
public sealed record ActivityEntry(string Time, string Message, bool IsProblem);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Il registro è volutamente limitato e solo in memoria: serve a capire cosa è
|
||||||
|
/// successo mentre si guardava altrove, non a essere uno storico — quello sono i
|
||||||
|
/// contatori, che invece durano.
|
||||||
|
/// </summary>
|
||||||
|
private const int MaxActivityEntries = 200;
|
||||||
|
|
||||||
|
private readonly ObservableCollection<ActivityEntry> _activity = new();
|
||||||
|
|
||||||
|
/// <summary>Evita che riempire i campi dalle impostazioni scateni un salvataggio.</summary>
|
||||||
|
private bool _loading;
|
||||||
|
|
||||||
|
public FreeBidsControl()
|
||||||
|
{
|
||||||
|
InitializeComponent();
|
||||||
|
ActivityList.ItemsSource = _activity;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Aggiornamento dello stato ────────────────────────────────────
|
||||||
|
|
||||||
|
public void SetBalance(int? bids) =>
|
||||||
|
BalanceText.Text = bids.HasValue
|
||||||
|
? $"{bids.Value} puntate sul conto"
|
||||||
|
: "saldo non disponibile";
|
||||||
|
|
||||||
|
/// <summary>Allinea i campi alle impostazioni salvate senza scatenare eventi.</summary>
|
||||||
|
public void SetOptions(bool autoClaimEnabled, int checkMinutes)
|
||||||
|
{
|
||||||
|
_loading = true;
|
||||||
|
try
|
||||||
|
{
|
||||||
|
AutoClaimCheckBox.IsChecked = autoClaimEnabled;
|
||||||
|
CheckMinutesTextBox.Text = checkMinutes.ToString();
|
||||||
|
}
|
||||||
|
finally { _loading = false; }
|
||||||
|
}
|
||||||
|
|
||||||
|
public bool AutoClaimEnabled => AutoClaimCheckBox.IsChecked == true;
|
||||||
|
|
||||||
|
/// <summary>Codice o collegamento digitato per il riscatto manuale.</summary>
|
||||||
|
public string PromoCode => PromoCodeTextBox.Text?.Trim() ?? "";
|
||||||
|
|
||||||
|
/// <summary>Svuota il campo dopo un riscatto andato a buon fine.</summary>
|
||||||
|
public void ClearPromoCode() => PromoCodeTextBox.Text = "";
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Riga di stato della raccolta: da dove legge, quanti codici sono già stati presi e
|
||||||
|
/// com'è andato l'ultimo giro.
|
||||||
|
/// </summary>
|
||||||
|
public void SetHarvestStatus(string sourceUrl, int claimedCodes, string? lastOutcome)
|
||||||
|
{
|
||||||
|
var known = claimedCodes == 1
|
||||||
|
? "1 codice già preso"
|
||||||
|
: $"{claimedCodes} codici già presi";
|
||||||
|
|
||||||
|
HarvestStatusText.Text = lastOutcome == null
|
||||||
|
? $"Sorgente: {sourceUrl} · {known}."
|
||||||
|
: $"Sorgente: {sourceUrl} · {known} · ultimo giro: {lastOutcome}";
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Mostra dove sta il file dei riferimenti, ed eventualmente perché non è stato
|
||||||
|
/// possibile usarlo: sono i valori che decidono ogni chiamata di riscatto, e sapere
|
||||||
|
/// che se ne stanno usando altri è la differenza fra correggere il file e cercare un
|
||||||
|
/// guasto che non c'è.
|
||||||
|
/// </summary>
|
||||||
|
public void SetConfigPath(string path, string? problem)
|
||||||
|
{
|
||||||
|
ConfigPathText.Text = problem == null
|
||||||
|
? $"Indirizzi, intestazioni e parametri del riscatto: {path}"
|
||||||
|
: $"{problem} — {path}";
|
||||||
|
|
||||||
|
ConfigPathText.SetResourceReference(TextBlock.ForegroundProperty,
|
||||||
|
problem == null ? "Brush.TextMuted" : "Brush.Warning");
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>Minuti richiesti, già riportati entro il minimo consentito.</summary>
|
||||||
|
public int CheckMinutes =>
|
||||||
|
int.TryParse(CheckMinutesTextBox.Text?.Trim(), out var minutes) ? Math.Max(5, minutes) : 30;
|
||||||
|
|
||||||
|
/// <summary>Riga di stato: acceso o spento, e quando tocca al prossimo giro.</summary>
|
||||||
|
public void SetSchedule(bool running, DateTime? lastCheck, DateTime? nextCheck)
|
||||||
|
{
|
||||||
|
StatusPillText.Text = running ? "riscatto attivo" : "riscatto spento";
|
||||||
|
StatusPillText.SetResourceReference(TextBlock.ForegroundProperty,
|
||||||
|
running ? "Brush.Success" : "Brush.TextMuted");
|
||||||
|
|
||||||
|
if (!running)
|
||||||
|
{
|
||||||
|
ScheduleText.Text = "Riscatto automatico spento: puoi comunque usare «Controlla adesso».";
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var last = lastCheck.HasValue
|
||||||
|
? $"ultimo controllo alle {lastCheck.Value:HH:mm}"
|
||||||
|
: "nessun controllo ancora eseguito";
|
||||||
|
|
||||||
|
var next = nextCheck.HasValue
|
||||||
|
? $", il prossimo alle {nextCheck.Value:HH:mm}"
|
||||||
|
: "";
|
||||||
|
|
||||||
|
ScheduleText.Text = char.ToUpper(last[0]) + last[1..] + next + ".";
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>Ridisegna i contatori.</summary>
|
||||||
|
public void SetCounters(FreeBidsStats.Snapshot s)
|
||||||
|
{
|
||||||
|
SessionBidsText.Text = s.SessionBids.ToString();
|
||||||
|
TodayBidsText.Text = s.BidsToday.ToString();
|
||||||
|
TotalBidsText.Text = s.TotalBids.ToString();
|
||||||
|
TotalClaimedText.Text = s.TotalClaimed.ToString();
|
||||||
|
|
||||||
|
var lastClaim = s.LastClaimAt.HasValue
|
||||||
|
? $"ultimo riscatto il {s.LastClaimAt.Value:dd/MM/yyyy} alle {s.LastClaimAt.Value:HH:mm}"
|
||||||
|
: "nessun riscatto ancora andato a buon fine";
|
||||||
|
|
||||||
|
CountersDetailText.Text =
|
||||||
|
$"{s.SessionChecks} controlli in questa sessione · {s.TotalChecks} da sempre · " +
|
||||||
|
$"{s.SessionClaimed} ricompense prese in sessione · {lastClaim}.";
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Aggiunge una riga al registro, a video e su file.
|
||||||
|
///
|
||||||
|
/// <para>Il file serve perché questo pannello è l'unico posto in cui si vede cosa ha
|
||||||
|
/// fatto il riscatto automatico, e il riscatto lavora quando nessuno guarda: senza
|
||||||
|
/// registro su disco, tutto ciò che è successo mentre l'applicazione era chiusa —
|
||||||
|
/// o nelle duecento righe precedenti — sarebbe perso.</para>
|
||||||
|
/// </summary>
|
||||||
|
public void AppendActivity(string message, bool isProblem = false)
|
||||||
|
{
|
||||||
|
TextLogService.FreeBids(message, isProblem);
|
||||||
|
|
||||||
|
_activity.Insert(0, new ActivityEntry(DateTime.Now.ToString("HH:mm:ss"), message, isProblem));
|
||||||
|
|
||||||
|
while (_activity.Count > MaxActivityEntries)
|
||||||
|
_activity.RemoveAt(_activity.Count - 1);
|
||||||
|
|
||||||
|
ActivityEmptyText.Visibility = Visibility.Collapsed;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Eventi dell'interfaccia ──────────────────────────────────────
|
||||||
|
|
||||||
|
private void RefreshButton_Click(object sender, RoutedEventArgs e)
|
||||||
|
=> RaiseEvent(new RoutedEventArgs(RefreshBalanceClickedEvent, this));
|
||||||
|
|
||||||
|
private void CheckNowButton_Click(object sender, RoutedEventArgs e)
|
||||||
|
=> RaiseEvent(new RoutedEventArgs(CheckNowClickedEvent, this));
|
||||||
|
|
||||||
|
private void OpenExternalButton_Click(object sender, RoutedEventArgs e)
|
||||||
|
=> RaiseEvent(new RoutedEventArgs(OpenExternalClickedEvent, this));
|
||||||
|
|
||||||
|
private void OpenInternalButton_Click(object sender, RoutedEventArgs e)
|
||||||
|
=> RaiseEvent(new RoutedEventArgs(OpenInternalClickedEvent, this));
|
||||||
|
|
||||||
|
private void OpenVouchersButton_Click(object sender, RoutedEventArgs e)
|
||||||
|
=> RaiseEvent(new RoutedEventArgs(OpenVouchersClickedEvent, this));
|
||||||
|
|
||||||
|
private void OpenBuyBidsButton_Click(object sender, RoutedEventArgs e)
|
||||||
|
=> RaiseEvent(new RoutedEventArgs(OpenBuyBidsClickedEvent, this));
|
||||||
|
|
||||||
|
private void ResetCountersButton_Click(object sender, RoutedEventArgs e)
|
||||||
|
=> RaiseEvent(new RoutedEventArgs(ResetCountersClickedEvent, this));
|
||||||
|
|
||||||
|
private void AutoClaimCheckBox_Changed(object sender, RoutedEventArgs e)
|
||||||
|
{
|
||||||
|
if (_loading) return;
|
||||||
|
RaiseEvent(new RoutedEventArgs(OptionsChangedEvent, this));
|
||||||
|
}
|
||||||
|
|
||||||
|
private void CheckMinutesTextBox_LostFocus(object sender, RoutedEventArgs e)
|
||||||
|
{
|
||||||
|
if (_loading) return;
|
||||||
|
|
||||||
|
// Si riscrive il valore corretto: chi ha digitato "2" deve vedere che il
|
||||||
|
// minimo applicato è 5, invece di credere di aver impostato due minuti.
|
||||||
|
var minutes = CheckMinutes;
|
||||||
|
if (CheckMinutesTextBox.Text != minutes.ToString())
|
||||||
|
CheckMinutesTextBox.Text = minutes.ToString();
|
||||||
|
|
||||||
|
RaiseEvent(new RoutedEventArgs(OptionsChangedEvent, this));
|
||||||
|
}
|
||||||
|
|
||||||
|
private void HarvestButton_Click(object sender, RoutedEventArgs e)
|
||||||
|
=> RaiseEvent(new RoutedEventArgs(HarvestClickedEvent, this));
|
||||||
|
|
||||||
|
private void OpenSourceButton_Click(object sender, RoutedEventArgs e)
|
||||||
|
=> RaiseEvent(new RoutedEventArgs(OpenPromoSourceClickedEvent, this));
|
||||||
|
|
||||||
|
private void ForgetPromosButton_Click(object sender, RoutedEventArgs e)
|
||||||
|
=> RaiseEvent(new RoutedEventArgs(ForgetPromosClickedEvent, this));
|
||||||
|
|
||||||
|
private void ClaimPromoButton_Click(object sender, RoutedEventArgs e)
|
||||||
|
=> RaiseEvent(new RoutedEventArgs(ClaimPromoClickedEvent, this));
|
||||||
|
|
||||||
|
/// <summary>Invio nel campo vale come clic: si incolla e si preme invio.</summary>
|
||||||
|
private void PromoCodeTextBox_KeyDown(object sender, System.Windows.Input.KeyEventArgs e)
|
||||||
|
{
|
||||||
|
if (e.Key != System.Windows.Input.Key.Enter) return;
|
||||||
|
|
||||||
|
e.Handled = true;
|
||||||
|
RaiseEvent(new RoutedEventArgs(ClaimPromoClickedEvent, this));
|
||||||
|
}
|
||||||
|
|
||||||
|
private void OpenConfigButton_Click(object sender, RoutedEventArgs e)
|
||||||
|
=> RaiseEvent(new RoutedEventArgs(OpenConfigClickedEvent, this));
|
||||||
|
|
||||||
|
private void ClearLogButton_Click(object sender, RoutedEventArgs e)
|
||||||
|
{
|
||||||
|
_activity.Clear();
|
||||||
|
ActivityEmptyText.Visibility = Visibility.Visible;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Routed events ────────────────────────────────────────────────
|
||||||
|
|
||||||
|
public static readonly RoutedEvent RefreshBalanceClickedEvent = EventManager.RegisterRoutedEvent(
|
||||||
|
"RefreshBalanceClicked", RoutingStrategy.Bubble, typeof(RoutedEventHandler), typeof(FreeBidsControl));
|
||||||
|
|
||||||
|
public static readonly RoutedEvent CheckNowClickedEvent = EventManager.RegisterRoutedEvent(
|
||||||
|
"CheckNowClicked", RoutingStrategy.Bubble, typeof(RoutedEventHandler), typeof(FreeBidsControl));
|
||||||
|
|
||||||
|
public static readonly RoutedEvent OpenExternalClickedEvent = EventManager.RegisterRoutedEvent(
|
||||||
|
"OpenExternalClicked", RoutingStrategy.Bubble, typeof(RoutedEventHandler), typeof(FreeBidsControl));
|
||||||
|
|
||||||
|
public static readonly RoutedEvent OpenInternalClickedEvent = EventManager.RegisterRoutedEvent(
|
||||||
|
"OpenInternalClicked", RoutingStrategy.Bubble, typeof(RoutedEventHandler), typeof(FreeBidsControl));
|
||||||
|
|
||||||
|
public static readonly RoutedEvent OpenVouchersClickedEvent = EventManager.RegisterRoutedEvent(
|
||||||
|
"OpenVouchersClicked", RoutingStrategy.Bubble, typeof(RoutedEventHandler), typeof(FreeBidsControl));
|
||||||
|
|
||||||
|
public static readonly RoutedEvent OpenBuyBidsClickedEvent = EventManager.RegisterRoutedEvent(
|
||||||
|
"OpenBuyBidsClicked", RoutingStrategy.Bubble, typeof(RoutedEventHandler), typeof(FreeBidsControl));
|
||||||
|
|
||||||
|
public static readonly RoutedEvent ResetCountersClickedEvent = EventManager.RegisterRoutedEvent(
|
||||||
|
"ResetCountersClicked", RoutingStrategy.Bubble, typeof(RoutedEventHandler), typeof(FreeBidsControl));
|
||||||
|
|
||||||
|
public static readonly RoutedEvent OptionsChangedEvent = EventManager.RegisterRoutedEvent(
|
||||||
|
"OptionsChanged", RoutingStrategy.Bubble, typeof(RoutedEventHandler), typeof(FreeBidsControl));
|
||||||
|
|
||||||
|
public static readonly RoutedEvent ClaimPromoClickedEvent = EventManager.RegisterRoutedEvent(
|
||||||
|
"ClaimPromoClicked", RoutingStrategy.Bubble, typeof(RoutedEventHandler), typeof(FreeBidsControl));
|
||||||
|
|
||||||
|
public static readonly RoutedEvent HarvestClickedEvent = EventManager.RegisterRoutedEvent(
|
||||||
|
"HarvestClicked", RoutingStrategy.Bubble, typeof(RoutedEventHandler), typeof(FreeBidsControl));
|
||||||
|
|
||||||
|
public static readonly RoutedEvent OpenPromoSourceClickedEvent = EventManager.RegisterRoutedEvent(
|
||||||
|
"OpenPromoSourceClicked", RoutingStrategy.Bubble, typeof(RoutedEventHandler), typeof(FreeBidsControl));
|
||||||
|
|
||||||
|
public static readonly RoutedEvent ForgetPromosClickedEvent = EventManager.RegisterRoutedEvent(
|
||||||
|
"ForgetPromosClicked", RoutingStrategy.Bubble, typeof(RoutedEventHandler), typeof(FreeBidsControl));
|
||||||
|
|
||||||
|
public static readonly RoutedEvent OpenConfigClickedEvent = EventManager.RegisterRoutedEvent(
|
||||||
|
"OpenConfigClicked", RoutingStrategy.Bubble, typeof(RoutedEventHandler), typeof(FreeBidsControl));
|
||||||
|
|
||||||
|
public event RoutedEventHandler RefreshBalanceClicked
|
||||||
|
{
|
||||||
|
add { AddHandler(RefreshBalanceClickedEvent, value); }
|
||||||
|
remove { RemoveHandler(RefreshBalanceClickedEvent, value); }
|
||||||
|
}
|
||||||
|
|
||||||
|
public event RoutedEventHandler CheckNowClicked
|
||||||
|
{
|
||||||
|
add { AddHandler(CheckNowClickedEvent, value); }
|
||||||
|
remove { RemoveHandler(CheckNowClickedEvent, value); }
|
||||||
|
}
|
||||||
|
|
||||||
|
public event RoutedEventHandler OpenExternalClicked
|
||||||
|
{
|
||||||
|
add { AddHandler(OpenExternalClickedEvent, value); }
|
||||||
|
remove { RemoveHandler(OpenExternalClickedEvent, value); }
|
||||||
|
}
|
||||||
|
|
||||||
|
public event RoutedEventHandler OpenInternalClicked
|
||||||
|
{
|
||||||
|
add { AddHandler(OpenInternalClickedEvent, value); }
|
||||||
|
remove { RemoveHandler(OpenInternalClickedEvent, value); }
|
||||||
|
}
|
||||||
|
|
||||||
|
public event RoutedEventHandler OpenVouchersClicked
|
||||||
|
{
|
||||||
|
add { AddHandler(OpenVouchersClickedEvent, value); }
|
||||||
|
remove { RemoveHandler(OpenVouchersClickedEvent, value); }
|
||||||
|
}
|
||||||
|
|
||||||
|
public event RoutedEventHandler OpenBuyBidsClicked
|
||||||
|
{
|
||||||
|
add { AddHandler(OpenBuyBidsClickedEvent, value); }
|
||||||
|
remove { RemoveHandler(OpenBuyBidsClickedEvent, value); }
|
||||||
|
}
|
||||||
|
|
||||||
|
public event RoutedEventHandler ResetCountersClicked
|
||||||
|
{
|
||||||
|
add { AddHandler(ResetCountersClickedEvent, value); }
|
||||||
|
remove { RemoveHandler(ResetCountersClickedEvent, value); }
|
||||||
|
}
|
||||||
|
|
||||||
|
public event RoutedEventHandler OptionsChanged
|
||||||
|
{
|
||||||
|
add { AddHandler(OptionsChangedEvent, value); }
|
||||||
|
remove { RemoveHandler(OptionsChangedEvent, value); }
|
||||||
|
}
|
||||||
|
|
||||||
|
public event RoutedEventHandler ClaimPromoClicked
|
||||||
|
{
|
||||||
|
add { AddHandler(ClaimPromoClickedEvent, value); }
|
||||||
|
remove { RemoveHandler(ClaimPromoClickedEvent, value); }
|
||||||
|
}
|
||||||
|
|
||||||
|
public event RoutedEventHandler HarvestClicked
|
||||||
|
{
|
||||||
|
add { AddHandler(HarvestClickedEvent, value); }
|
||||||
|
remove { RemoveHandler(HarvestClickedEvent, value); }
|
||||||
|
}
|
||||||
|
|
||||||
|
public event RoutedEventHandler OpenPromoSourceClicked
|
||||||
|
{
|
||||||
|
add { AddHandler(OpenPromoSourceClickedEvent, value); }
|
||||||
|
remove { RemoveHandler(OpenPromoSourceClickedEvent, value); }
|
||||||
|
}
|
||||||
|
|
||||||
|
public event RoutedEventHandler ForgetPromosClicked
|
||||||
|
{
|
||||||
|
add { AddHandler(ForgetPromosClickedEvent, value); }
|
||||||
|
remove { RemoveHandler(ForgetPromosClickedEvent, value); }
|
||||||
|
}
|
||||||
|
|
||||||
|
public event RoutedEventHandler OpenConfigClicked
|
||||||
|
{
|
||||||
|
add { AddHandler(OpenConfigClickedEvent, value); }
|
||||||
|
remove { RemoveHandler(OpenConfigClickedEvent, value); }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,42 @@
|
|||||||
|
<UserControl x:Class="AutoBidder.Controls.HelpHeader"
|
||||||
|
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
|
||||||
|
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||||
|
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
|
||||||
|
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
|
||||||
|
mc:Ignorable="d"
|
||||||
|
x:Name="Root"
|
||||||
|
HorizontalAlignment="Left">
|
||||||
|
|
||||||
|
<!-- Titolo di sezione con la spiegazione a portata di puntatore.
|
||||||
|
|
||||||
|
Le descrizioni stavano stampate sotto ogni titolo: si leggono una volta e poi
|
||||||
|
restano lì a occupare metà pagina per sempre. Qui il testo c'è ancora — non è
|
||||||
|
stato buttato — ma si mostra passandoci sopra, come per tutti gli altri comandi
|
||||||
|
dell'applicazione. Il pallino accanto al titolo serve a far sapere che c'è. -->
|
||||||
|
|
||||||
|
<StackPanel Orientation="Horizontal"
|
||||||
|
Background="Transparent"
|
||||||
|
Margin="{Binding Margin, ElementName=Root}"
|
||||||
|
ToolTipService.InitialShowDelay="250"
|
||||||
|
ToolTipService.ShowDuration="60000">
|
||||||
|
<StackPanel.ToolTip>
|
||||||
|
<ToolTip Visibility="{Binding HelpVisibility, ElementName=Root}">
|
||||||
|
<TextBlock Text="{Binding Help, ElementName=Root}"
|
||||||
|
TextWrapping="Wrap"
|
||||||
|
MaxWidth="420"/>
|
||||||
|
</ToolTip>
|
||||||
|
</StackPanel.ToolTip>
|
||||||
|
|
||||||
|
<TextBlock Text="{Binding Title, ElementName=Root}"
|
||||||
|
Style="{StaticResource BlockTitle}"
|
||||||
|
VerticalAlignment="Center"/>
|
||||||
|
|
||||||
|
<TextBlock Text=""
|
||||||
|
FontFamily="Segoe MDL2 Assets"
|
||||||
|
FontSize="12"
|
||||||
|
Margin="6,2,0,0"
|
||||||
|
VerticalAlignment="Center"
|
||||||
|
Foreground="{DynamicResource Brush.TextFaint}"
|
||||||
|
Visibility="{Binding HelpVisibility, ElementName=Root}"/>
|
||||||
|
</StackPanel>
|
||||||
|
</UserControl>
|
||||||
@@ -0,0 +1,58 @@
|
|||||||
|
using System.Windows;
|
||||||
|
using System.Windows.Controls;
|
||||||
|
|
||||||
|
namespace AutoBidder.Controls
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Titolo di sezione con la spiegazione nel suggerimento invece che stampata sotto.
|
||||||
|
///
|
||||||
|
/// <para>Serve a togliere dalla pagina i paragrafi descrittivi senza perderne il
|
||||||
|
/// contenuto: si leggono una volta e poi restano a occupare spazio a ogni apertura
|
||||||
|
/// delle Impostazioni. Passando il puntatore il testo torna, esattamente come per i
|
||||||
|
/// pulsanti del resto dell'applicazione.</para>
|
||||||
|
///
|
||||||
|
/// <para>Uso: <c><ctl:HelpHeader Title="Motore di Precisione" Help="…"/></c>.
|
||||||
|
/// Senza <see cref="Help"/> si comporta come un titolo semplice, senza pallino.</para>
|
||||||
|
/// </summary>
|
||||||
|
public partial class HelpHeader : UserControl
|
||||||
|
{
|
||||||
|
public static readonly DependencyProperty TitleProperty =
|
||||||
|
DependencyProperty.Register(nameof(Title), typeof(string), typeof(HelpHeader),
|
||||||
|
new PropertyMetadata(""));
|
||||||
|
|
||||||
|
public static readonly DependencyProperty HelpProperty =
|
||||||
|
DependencyProperty.Register(nameof(Help), typeof(string), typeof(HelpHeader),
|
||||||
|
new PropertyMetadata("", OnHelpChanged));
|
||||||
|
|
||||||
|
private static readonly DependencyPropertyKey HelpVisibilityKey =
|
||||||
|
DependencyProperty.RegisterReadOnly(nameof(HelpVisibility), typeof(Visibility), typeof(HelpHeader),
|
||||||
|
new PropertyMetadata(Visibility.Collapsed));
|
||||||
|
|
||||||
|
public static readonly DependencyProperty HelpVisibilityProperty = HelpVisibilityKey.DependencyProperty;
|
||||||
|
|
||||||
|
public HelpHeader() => InitializeComponent();
|
||||||
|
|
||||||
|
/// <summary>Il titolo della sezione.</summary>
|
||||||
|
public string Title
|
||||||
|
{
|
||||||
|
get => (string)GetValue(TitleProperty);
|
||||||
|
set => SetValue(TitleProperty, value);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>La spiegazione, mostrata al passaggio del puntatore.</summary>
|
||||||
|
public string Help
|
||||||
|
{
|
||||||
|
get => (string)GetValue(HelpProperty);
|
||||||
|
set => SetValue(HelpProperty, value);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>Il pallino compare solo quando c'è davvero qualcosa da leggere.</summary>
|
||||||
|
public Visibility HelpVisibility => (Visibility)GetValue(HelpVisibilityProperty);
|
||||||
|
|
||||||
|
private static void OnHelpChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
|
||||||
|
{
|
||||||
|
var visible = !string.IsNullOrWhiteSpace(e.NewValue as string);
|
||||||
|
d.SetValue(HelpVisibilityKey, visible ? Visibility.Visible : Visibility.Collapsed);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,448 @@
|
|||||||
|
<UserControl x:Class="AutoBidder.Controls.ProductsControl"
|
||||||
|
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
|
||||||
|
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||||
|
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
|
||||||
|
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
|
||||||
|
xmlns:vm="clr-namespace:AutoBidder.ViewModels"
|
||||||
|
mc:Ignorable="d"
|
||||||
|
d:DesignHeight="800" d:DesignWidth="1200"
|
||||||
|
Background="{DynamicResource Brush.Bg}">
|
||||||
|
|
||||||
|
<UserControl.Resources>
|
||||||
|
<Style x:Key="Glyph" TargetType="TextBlock">
|
||||||
|
<Setter Property="FontFamily" Value="Segoe MDL2 Assets"/>
|
||||||
|
<Setter Property="VerticalAlignment" Value="Center"/>
|
||||||
|
<Setter Property="HorizontalAlignment" Value="Center"/>
|
||||||
|
</Style>
|
||||||
|
|
||||||
|
<!-- ═══ Modificabile o no: deve vedersi a colpo d'occhio ═══
|
||||||
|
Le celle che si possono scrivere hanno il riquadro di una casella di testo;
|
||||||
|
le statistiche restano testo semplice e smorzato. Senza questa differenza la
|
||||||
|
griglia sembra tutta uguale e non si capisce dove si può intervenire. -->
|
||||||
|
<Style x:Key="EditableCell" TargetType="DataGridCell">
|
||||||
|
<Setter Property="Foreground" Value="{DynamicResource Brush.Text}"/>
|
||||||
|
<Setter Property="VerticalAlignment" Value="Stretch"/>
|
||||||
|
<Setter Property="Cursor" Value="IBeam"/>
|
||||||
|
<Setter Property="Template">
|
||||||
|
<Setter.Value>
|
||||||
|
<ControlTemplate TargetType="DataGridCell">
|
||||||
|
<Border x:Name="box"
|
||||||
|
Background="{DynamicResource Brush.Bg}"
|
||||||
|
BorderBrush="{DynamicResource Brush.Border}"
|
||||||
|
BorderThickness="1"
|
||||||
|
CornerRadius="5"
|
||||||
|
Margin="3,3"
|
||||||
|
Padding="6,0">
|
||||||
|
<ContentPresenter VerticalAlignment="Center"/>
|
||||||
|
</Border>
|
||||||
|
<ControlTemplate.Triggers>
|
||||||
|
<Trigger Property="IsMouseOver" Value="True">
|
||||||
|
<Setter TargetName="box" Property="BorderBrush"
|
||||||
|
Value="{DynamicResource Brush.BorderStrong}"/>
|
||||||
|
</Trigger>
|
||||||
|
<Trigger Property="IsEditing" Value="True">
|
||||||
|
<Setter TargetName="box" Property="BorderBrush"
|
||||||
|
Value="{DynamicResource Brush.Accent}"/>
|
||||||
|
<Setter TargetName="box" Property="Background"
|
||||||
|
Value="{DynamicResource Brush.Surface}"/>
|
||||||
|
</Trigger>
|
||||||
|
</ControlTemplate.Triggers>
|
||||||
|
</ControlTemplate>
|
||||||
|
</Setter.Value>
|
||||||
|
</Setter>
|
||||||
|
</Style>
|
||||||
|
|
||||||
|
<!-- Il pennino segnala nell'intestazione le colonne che accettano scrittura -->
|
||||||
|
<DataTemplate x:Key="EditableHeader">
|
||||||
|
<StackPanel Orientation="Horizontal">
|
||||||
|
<TextBlock Text="{Binding}" VerticalAlignment="Center"/>
|
||||||
|
<TextBlock FontFamily="Segoe MDL2 Assets" Text="" FontSize="9"
|
||||||
|
Margin="5,0,0,0" VerticalAlignment="Center"
|
||||||
|
Foreground="{DynamicResource Brush.Accent}"/>
|
||||||
|
</StackPanel>
|
||||||
|
</DataTemplate>
|
||||||
|
|
||||||
|
<!-- Numeri modificabili: allineati a destra, colore pieno -->
|
||||||
|
<Style x:Key="EditableText" TargetType="TextBlock">
|
||||||
|
<Setter Property="HorizontalAlignment" Value="Right"/>
|
||||||
|
<Setter Property="VerticalAlignment" Value="Center"/>
|
||||||
|
<Setter Property="Foreground" Value="{DynamicResource Brush.Text}"/>
|
||||||
|
</Style>
|
||||||
|
|
||||||
|
<!-- Statistiche: sola lettura, quindi smorzate -->
|
||||||
|
<Style x:Key="StatText" TargetType="TextBlock">
|
||||||
|
<Setter Property="HorizontalAlignment" Value="Right"/>
|
||||||
|
<Setter Property="VerticalAlignment" Value="Center"/>
|
||||||
|
<Setter Property="Foreground" Value="{DynamicResource Brush.TextMuted}"/>
|
||||||
|
</Style>
|
||||||
|
|
||||||
|
<!-- Statistiche estese: il numero da solo non basta, il dettaglio (quartili,
|
||||||
|
campione verificato, frequenza) sta nel suggerimento. -->
|
||||||
|
<Style x:Key="StatsText" TargetType="TextBlock" BasedOn="{StaticResource StatText}">
|
||||||
|
<Setter Property="ToolTip" Value="{Binding StatsExplanation}"/>
|
||||||
|
</Style>
|
||||||
|
|
||||||
|
<!-- Valore, costo reale e risparmio: il ragionamento sta nel suggerimento.
|
||||||
|
In rosso quando vincere costa piu' che comprare: e' il caso che va visto
|
||||||
|
subito, non nascosto in mezzo agli altri numeri. -->
|
||||||
|
<Style x:Key="ValueText" TargetType="TextBlock" BasedOn="{StaticResource StatText}">
|
||||||
|
<Setter Property="ToolTip" Value="{Binding ValueExplanation}"/>
|
||||||
|
<Style.Triggers>
|
||||||
|
<DataTrigger Binding="{Binding HasSavings}" Value="True">
|
||||||
|
<Setter Property="Foreground" Value="{DynamicResource Brush.Success}"/>
|
||||||
|
</DataTrigger>
|
||||||
|
<DataTrigger Binding="{Binding SavingsIsNegative}" Value="True">
|
||||||
|
<Setter Property="Foreground" Value="{DynamicResource Brush.Danger}"/>
|
||||||
|
</DataTrigger>
|
||||||
|
</Style.Triggers>
|
||||||
|
</Style>
|
||||||
|
|
||||||
|
<!-- Limiti consigliati: il numero da solo non basta, il ragionamento sta nel
|
||||||
|
suggerimento. In arancione quando il tetto non copre il prezzo tipico:
|
||||||
|
è il caso in cui l'articolo, con quelle puntate, non conviene. -->
|
||||||
|
<Style x:Key="AdviceText" TargetType="TextBlock" BasedOn="{StaticResource StatText}">
|
||||||
|
<Setter Property="ToolTip" Value="{Binding AdviceExplanation}"/>
|
||||||
|
<Setter Property="Foreground" Value="{DynamicResource Brush.Info}"/>
|
||||||
|
<Style.Triggers>
|
||||||
|
<DataTrigger Binding="{Binding HasAdvice}" Value="False">
|
||||||
|
<Setter Property="Foreground" Value="{DynamicResource Brush.TextFaint}"/>
|
||||||
|
</DataTrigger>
|
||||||
|
<DataTrigger Binding="{Binding AdviceWarns}" Value="True">
|
||||||
|
<Setter Property="Foreground" Value="{DynamicResource Brush.Warning}"/>
|
||||||
|
</DataTrigger>
|
||||||
|
</Style.Triggers>
|
||||||
|
</Style>
|
||||||
|
</UserControl.Resources>
|
||||||
|
|
||||||
|
<Grid>
|
||||||
|
<Grid.RowDefinitions>
|
||||||
|
<RowDefinition Height="Auto"/>
|
||||||
|
<RowDefinition Height="*"/>
|
||||||
|
</Grid.RowDefinitions>
|
||||||
|
|
||||||
|
<!-- ═══ Barra strumenti ═══ -->
|
||||||
|
<Border Grid.Row="0" Background="{DynamicResource Brush.Surface}" Padding="10,7"
|
||||||
|
BorderBrush="{DynamicResource Brush.Border}" BorderThickness="0,0,0,1">
|
||||||
|
<Grid>
|
||||||
|
<Grid.ColumnDefinitions>
|
||||||
|
<ColumnDefinition Width="Auto"/>
|
||||||
|
<ColumnDefinition Width="Auto"/>
|
||||||
|
<ColumnDefinition Width="*"/>
|
||||||
|
<ColumnDefinition Width="Auto"/>
|
||||||
|
</Grid.ColumnDefinitions>
|
||||||
|
|
||||||
|
<StackPanel Grid.Column="0" Orientation="Horizontal" VerticalAlignment="Center">
|
||||||
|
<Button x:Name="RefreshButton" Style="{StaticResource IconButton}"
|
||||||
|
ToolTip="Ricarica elenco e statistiche" Click="RefreshButton_Click">
|
||||||
|
<TextBlock Style="{StaticResource Glyph}" Text=""/>
|
||||||
|
</Button>
|
||||||
|
<TextBlock Text="Prodotti" FontWeight="SemiBold" Margin="8,0,0,0"
|
||||||
|
FontSize="{StaticResource Font.Size.Lg}"
|
||||||
|
Foreground="{DynamicResource Brush.Text}" VerticalAlignment="Center"/>
|
||||||
|
<Border Width="1" Background="{DynamicResource Brush.Border}" Margin="10,4"/>
|
||||||
|
</StackPanel>
|
||||||
|
|
||||||
|
<StackPanel Grid.Column="1" Orientation="Horizontal" VerticalAlignment="Center">
|
||||||
|
<Border Style="{StaticResource Pill}" ToolTip="Prodotti in elenco">
|
||||||
|
<TextBlock x:Name="ProductsCountText" Text="0 prodotti"
|
||||||
|
FontSize="{StaticResource Font.Size.Sm}"
|
||||||
|
Foreground="{DynamicResource Brush.TextMuted}"/>
|
||||||
|
</Border>
|
||||||
|
<Border Style="{StaticResource Pill}" ToolTip="Prodotti con la stellina accesa">
|
||||||
|
<TextBlock x:Name="ProductsWatchedText" Text="0 seguiti"
|
||||||
|
FontSize="{StaticResource Font.Size.Sm}"
|
||||||
|
Foreground="{DynamicResource Brush.Gold}"/>
|
||||||
|
</Border>
|
||||||
|
</StackPanel>
|
||||||
|
|
||||||
|
<!-- Ricerca + legenda "dove posso scrivere?" -->
|
||||||
|
<StackPanel Grid.Column="2" Orientation="Horizontal" VerticalAlignment="Center"
|
||||||
|
Margin="14,0,10,0">
|
||||||
|
<Border Background="{DynamicResource Brush.Bg}"
|
||||||
|
BorderBrush="{DynamicResource Brush.Border}" BorderThickness="1"
|
||||||
|
CornerRadius="6" Padding="8,0" Margin="0,0,12,0"
|
||||||
|
VerticalAlignment="Center">
|
||||||
|
<Grid Width="230">
|
||||||
|
<StackPanel Orientation="Horizontal">
|
||||||
|
<TextBlock Style="{StaticResource Glyph}" Text="" FontSize="12"
|
||||||
|
Foreground="{DynamicResource Brush.TextFaint}" Margin="0,0,6,0"/>
|
||||||
|
<TextBox x:Name="SearchBox" Width="196" BorderThickness="0"
|
||||||
|
Background="Transparent" VerticalAlignment="Center"
|
||||||
|
Foreground="{DynamicResource Brush.Text}"
|
||||||
|
TextChanged="SearchBox_TextChanged"
|
||||||
|
ToolTip="Filtra i prodotti per nome mentre scrivi"/>
|
||||||
|
</StackPanel>
|
||||||
|
|
||||||
|
<!-- Testo guida: sparisce appena si scrive qualcosa -->
|
||||||
|
<TextBlock x:Name="SearchPlaceholder" Text="Cerca un prodotto…"
|
||||||
|
IsHitTestVisible="False" Margin="20,0,0,0"
|
||||||
|
VerticalAlignment="Center"
|
||||||
|
FontSize="{StaticResource Font.Size.Sm}"
|
||||||
|
Foreground="{DynamicResource Brush.TextFaint}"/>
|
||||||
|
</Grid>
|
||||||
|
</Border>
|
||||||
|
|
||||||
|
<Border Background="{DynamicResource Brush.Bg}"
|
||||||
|
BorderBrush="{DynamicResource Brush.Border}" BorderThickness="1"
|
||||||
|
CornerRadius="4" Width="18" Height="14" VerticalAlignment="Center"/>
|
||||||
|
<TextBlock Text="= modificabile"
|
||||||
|
Margin="6,0,0,0" VerticalAlignment="Center"
|
||||||
|
TextTrimming="CharacterEllipsis"
|
||||||
|
FontSize="{StaticResource Font.Size.Sm}"
|
||||||
|
Foreground="{DynamicResource Brush.TextFaint}"/>
|
||||||
|
</StackPanel>
|
||||||
|
|
||||||
|
<StackPanel Grid.Column="3" Orientation="Horizontal" VerticalAlignment="Center">
|
||||||
|
<Button x:Name="ScanNowButton" Style="{StaticResource SmallRoundedButton}"
|
||||||
|
Background="{DynamicResource Brush.SurfaceAlt}"
|
||||||
|
Foreground="{DynamicResource Brush.Text}"
|
||||||
|
Content="Cerca aste ora" Margin="0,0,8,0"
|
||||||
|
ToolTip="Cerca subito aste nuove dei prodotti seguiti, senza aspettare il prossimo giro"
|
||||||
|
Click="ScanNowButton_Click"/>
|
||||||
|
|
||||||
|
<Button x:Name="ApplySuggestedButton" Style="{StaticResource SmallRoundedButton}"
|
||||||
|
Background="{DynamicResource Brush.SurfaceAlt}"
|
||||||
|
Foreground="{DynamicResource Brush.Text}"
|
||||||
|
Content="Usa consigliati" Margin="0,0,8,0"
|
||||||
|
ToolTip="Scrive prezzo minimo e massimo consigliati nel prodotto selezionato. Il consiglio nasce da come è andata finora: il massimo copre l'85% delle chiusure passate (regolabile in Impostazioni), abbassato se il budget non lo regge — valore del prodotto meno il costo delle puntate massime. Il minimo è il decimo percentile: sotto quel prezzo l'asta non si chiude quasi mai."
|
||||||
|
Click="ApplySuggestedButton_Click"/>
|
||||||
|
|
||||||
|
<Button x:Name="ApplyAllSuggestedButton" Style="{StaticResource SmallRoundedButton}"
|
||||||
|
Background="{DynamicResource Brush.SurfaceAlt}"
|
||||||
|
Foreground="{DynamicResource Brush.Text}"
|
||||||
|
Content="Usa consigliati su tutti" Margin="0,0,8,0"
|
||||||
|
ToolTip="Scrive i limiti consigliati su tutti i prodotti che hanno abbastanza aste concluse. Chiede conferma e dice quante righe cambierebbe."
|
||||||
|
Click="ApplyAllSuggestedButton_Click"/>
|
||||||
|
|
||||||
|
<Button x:Name="ReapplyButton" Style="{StaticResource SmallRoundedButton}"
|
||||||
|
Background="{DynamicResource Brush.Accent}"
|
||||||
|
Foreground="{DynamicResource Brush.TextOnAccent}"
|
||||||
|
Content="Riapplica" Margin="0,0,8,0"
|
||||||
|
ToolTip="Riscrive i limiti del prodotto selezionato sulle aste già presenti nel monitor"
|
||||||
|
Click="ReapplyButton_Click"/>
|
||||||
|
|
||||||
|
<Button x:Name="RemoveButton" Style="{StaticResource IconButton}"
|
||||||
|
ToolTip="Togli dall'elenco il prodotto selezionato"
|
||||||
|
Click="RemoveButton_Click">
|
||||||
|
<TextBlock Style="{StaticResource Glyph}" Text=""/>
|
||||||
|
</Button>
|
||||||
|
<Button x:Name="ClearButton" Style="{StaticResource IconButton}"
|
||||||
|
Foreground="{DynamicResource Brush.Danger}"
|
||||||
|
ToolTip="Svuota l'elenco dei prodotti"
|
||||||
|
Click="ClearButton_Click">
|
||||||
|
<TextBlock Style="{StaticResource Glyph}" Text=""/>
|
||||||
|
</Button>
|
||||||
|
</StackPanel>
|
||||||
|
</Grid>
|
||||||
|
</Border>
|
||||||
|
|
||||||
|
<!-- ═══ Elenco prodotti: impostazioni e statistiche sulla stessa riga ═══ -->
|
||||||
|
<Grid Grid.Row="1" Margin="12">
|
||||||
|
<Border Background="{DynamicResource Brush.Surface}"
|
||||||
|
BorderBrush="{DynamicResource Brush.Border}" BorderThickness="1" CornerRadius="12">
|
||||||
|
<DataGrid x:Name="ProductsGrid" AutoGenerateColumns="False"
|
||||||
|
Background="Transparent" Margin="4"
|
||||||
|
CanUserAddRows="False" CanUserDeleteRows="False"
|
||||||
|
SelectionMode="Single" HeadersVisibility="Column"
|
||||||
|
RowHeight="32"
|
||||||
|
HorizontalScrollBarVisibility="Auto"
|
||||||
|
CellEditEnding="ProductsGrid_CellEditEnding"
|
||||||
|
SelectionChanged="ProductsGrid_SelectionChanged">
|
||||||
|
<DataGrid.Columns>
|
||||||
|
<!-- Stellina: acceso = le aste nuove entrano da sole -->
|
||||||
|
<DataGridTemplateColumn Header="" Width="Auto" MinWidth="38" IsReadOnly="True"
|
||||||
|
CanUserSort="False">
|
||||||
|
<DataGridTemplateColumn.CellTemplate>
|
||||||
|
<DataTemplate>
|
||||||
|
<Button Style="{StaticResource MiniIconButton}"
|
||||||
|
ToolTip="{Binding WatchTooltip}"
|
||||||
|
Command="{Binding DataContext.ProductWatchCommand,
|
||||||
|
RelativeSource={RelativeSource AncestorType=UserControl}}"
|
||||||
|
CommandParameter="{Binding}">
|
||||||
|
<TextBlock FontFamily="Segoe MDL2 Assets" Text="{Binding WatchGlyph}">
|
||||||
|
<TextBlock.Style>
|
||||||
|
<Style TargetType="TextBlock">
|
||||||
|
<Setter Property="Foreground" Value="{DynamicResource Brush.TextFaint}"/>
|
||||||
|
<Style.Triggers>
|
||||||
|
<DataTrigger Binding="{Binding IsWatched}" Value="True">
|
||||||
|
<Setter Property="Foreground" Value="{DynamicResource Brush.Gold}"/>
|
||||||
|
</DataTrigger>
|
||||||
|
</Style.Triggers>
|
||||||
|
</Style>
|
||||||
|
</TextBlock.Style>
|
||||||
|
</TextBlock>
|
||||||
|
</Button>
|
||||||
|
</DataTemplate>
|
||||||
|
</DataGridTemplateColumn.CellTemplate>
|
||||||
|
</DataGridTemplateColumn>
|
||||||
|
|
||||||
|
<!-- Auto, non *: con venti colonne accanto lo spazio residuo non basta mai
|
||||||
|
e i nomi lunghi restano tagliati. Cosi' la colonna prende la
|
||||||
|
larghezza del nome piu' lungo e la griglia scorre in orizzontale. -->
|
||||||
|
<DataGridTextColumn Header="Prodotto" Binding="{Binding DisplayName}"
|
||||||
|
Width="Auto" MinWidth="220" IsReadOnly="True"/>
|
||||||
|
|
||||||
|
<!-- ── Limiti propri del prodotto: modificabili ── -->
|
||||||
|
<DataGridTextColumn Header="Min €" Binding="{Binding MinPriceText}"
|
||||||
|
Width="Auto" MinWidth="74"
|
||||||
|
CellStyle="{StaticResource EditableCell}"
|
||||||
|
HeaderTemplate="{StaticResource EditableHeader}"
|
||||||
|
ElementStyle="{StaticResource EditableText}"/>
|
||||||
|
<DataGridTextColumn Header="Max €" Binding="{Binding MaxPriceText}"
|
||||||
|
Width="Auto" MinWidth="74"
|
||||||
|
CellStyle="{StaticResource EditableCell}"
|
||||||
|
HeaderTemplate="{StaticResource EditableHeader}"
|
||||||
|
ElementStyle="{StaticResource EditableText}"/>
|
||||||
|
<DataGridTextColumn Header="Puntate" Binding="{Binding MaxClicksText}"
|
||||||
|
Width="Auto" MinWidth="78"
|
||||||
|
CellStyle="{StaticResource EditableCell}"
|
||||||
|
HeaderTemplate="{StaticResource EditableHeader}"
|
||||||
|
ElementStyle="{StaticResource EditableText}"/>
|
||||||
|
<DataGridTextColumn Header="Entro min" Binding="{Binding MaxStartMinutesText}"
|
||||||
|
Width="Auto" MinWidth="86"
|
||||||
|
CellStyle="{StaticResource EditableCell}"
|
||||||
|
HeaderTemplate="{StaticResource EditableHeader}"
|
||||||
|
ElementStyle="{StaticResource EditableText}"/>
|
||||||
|
|
||||||
|
<!-- Le colonne non stanno nell'albero visuale: un Binding qui non
|
||||||
|
erediterebbe alcun DataContext, quindi il valore è statico. -->
|
||||||
|
<DataGridComboBoxColumn Header="Stato" Width="Auto" MinWidth="112"
|
||||||
|
CellStyle="{StaticResource EditableCell}"
|
||||||
|
HeaderTemplate="{StaticResource EditableHeader}"
|
||||||
|
SelectedItemBinding="{Binding StateChoice}"
|
||||||
|
ItemsSource="{x:Static vm:ProductViewModel.StateChoices}"/>
|
||||||
|
|
||||||
|
<!-- ── Statistiche dello storico: sola lettura ── -->
|
||||||
|
<DataGridTextColumn Header="Aste" Binding="{Binding StatCountDisplay}"
|
||||||
|
Width="Auto" MinWidth="52" IsReadOnly="True"
|
||||||
|
ElementStyle="{StaticResource StatText}"/>
|
||||||
|
<DataGridTextColumn Header="Vinte" Binding="{Binding StatWonCount}"
|
||||||
|
Width="Auto" MinWidth="56" IsReadOnly="True"
|
||||||
|
ElementStyle="{StaticResource StatText}"/>
|
||||||
|
<DataGridTextColumn Header="%" Binding="{Binding StatWinRateDisplay}"
|
||||||
|
Width="Auto" MinWidth="52" IsReadOnly="True"
|
||||||
|
ElementStyle="{StaticResource StatText}"/>
|
||||||
|
<DataGridTextColumn Header="Media" Binding="{Binding StatAverageDisplay}"
|
||||||
|
Width="Auto" MinWidth="78" IsReadOnly="True"
|
||||||
|
ElementStyle="{StaticResource StatText}"/>
|
||||||
|
<DataGridTextColumn Header="Min" Binding="{Binding StatMinDisplay}"
|
||||||
|
Width="Auto" MinWidth="72" IsReadOnly="True"
|
||||||
|
ElementStyle="{StaticResource StatText}"/>
|
||||||
|
<DataGridTextColumn Header="Max" Binding="{Binding StatMaxDisplay}"
|
||||||
|
Width="Auto" MinWidth="72" IsReadOnly="True"
|
||||||
|
ElementStyle="{StaticResource StatText}"/>
|
||||||
|
|
||||||
|
<!-- ── Quanto vale, quanto costa vincerlo davvero ── -->
|
||||||
|
<DataGridTextColumn Header="Valore" Binding="{Binding StatValueDisplay}"
|
||||||
|
Width="Auto" MinWidth="80" IsReadOnly="True"
|
||||||
|
ElementStyle="{StaticResource ValueText}"/>
|
||||||
|
<DataGridTextColumn Header="Punt. vinc." Binding="{Binding StatWinnerBidsDisplay}"
|
||||||
|
Width="Auto" MinWidth="88" IsReadOnly="True"
|
||||||
|
ElementStyle="{StaticResource ValueText}"/>
|
||||||
|
<DataGridTextColumn Header="Costo reale" Binding="{Binding StatWinCostDisplay}"
|
||||||
|
Width="Auto" MinWidth="90" IsReadOnly="True"
|
||||||
|
ElementStyle="{StaticResource ValueText}"/>
|
||||||
|
<DataGridTextColumn Header="Risparmio" Binding="{Binding StatSavingsDisplay}"
|
||||||
|
Width="Auto" MinWidth="84" IsReadOnly="True"
|
||||||
|
ElementStyle="{StaticResource ValueText}"/>
|
||||||
|
|
||||||
|
<!-- ── La forbice, non solo il valore tipico ──
|
||||||
|
Su un articolo dove si vince con 12 puntate o con 900, la sola
|
||||||
|
mediana porta a impostare limiti che non reggono. -->
|
||||||
|
<DataGridTextColumn Header="Punt. min" Binding="{Binding StatMinWinnerBidsDisplay}"
|
||||||
|
Width="Auto" MinWidth="78" IsReadOnly="True"
|
||||||
|
ElementStyle="{StaticResource StatsText}"/>
|
||||||
|
<DataGridTextColumn Header="Punt. 75°" Binding="{Binding StatP75WinnerBidsDisplay}"
|
||||||
|
Width="Auto" MinWidth="78" IsReadOnly="True"
|
||||||
|
ElementStyle="{StaticResource StatsText}"/>
|
||||||
|
<DataGridTextColumn Header="Punt. max" Binding="{Binding StatMaxWinnerBidsDisplay}"
|
||||||
|
Width="Auto" MinWidth="80" IsReadOnly="True"
|
||||||
|
ElementStyle="{StaticResource StatsText}"/>
|
||||||
|
<DataGridTextColumn Header="Risp. min" Binding="{Binding StatMinSavingsDisplay}"
|
||||||
|
Width="Auto" MinWidth="80" IsReadOnly="True"
|
||||||
|
ElementStyle="{StaticResource StatsText}"/>
|
||||||
|
<DataGridTextColumn Header="Risp. max" Binding="{Binding StatMaxSavingsDisplay}"
|
||||||
|
Width="Auto" MinWidth="80" IsReadOnly="True"
|
||||||
|
ElementStyle="{StaticResource StatsText}"/>
|
||||||
|
<DataGridTextColumn Header="In utile" Binding="{Binding StatProfitableShareDisplay}"
|
||||||
|
Width="Auto" MinWidth="72" IsReadOnly="True"
|
||||||
|
ElementStyle="{StaticResource StatsText}"/>
|
||||||
|
<DataGridTextColumn Header="Ogni" Binding="{Binding StatFrequencyDisplay}"
|
||||||
|
Width="Auto" MinWidth="70" IsReadOnly="True"
|
||||||
|
ElementStyle="{StaticResource StatsText}"/>
|
||||||
|
<DataGridTextColumn Header="Verificate" Binding="{Binding StatWinnerBidsSampleDisplay}"
|
||||||
|
Width="Auto" MinWidth="82" IsReadOnly="True"
|
||||||
|
ElementStyle="{StaticResource StatsText}"/>
|
||||||
|
<!-- ── Limiti consigliati: sola lettura, più il bottone che li applica ── -->
|
||||||
|
<DataGridTextColumn Header="Min cons." Binding="{Binding SuggestedMinDisplay}"
|
||||||
|
Width="Auto" MinWidth="84" IsReadOnly="True"
|
||||||
|
ElementStyle="{StaticResource AdviceText}"/>
|
||||||
|
<DataGridTextColumn Header="Max cons." Binding="{Binding SuggestedMaxDisplay}"
|
||||||
|
Width="Auto" MinWidth="84" IsReadOnly="True"
|
||||||
|
ElementStyle="{StaticResource AdviceText}"/>
|
||||||
|
<DataGridTextColumn Header="Punt. cons." Binding="{Binding SuggestedMaxBidsDisplay}"
|
||||||
|
Width="Auto" MinWidth="88" IsReadOnly="True"
|
||||||
|
ElementStyle="{StaticResource AdviceText}"/>
|
||||||
|
|
||||||
|
<DataGridTemplateColumn Header="" Width="Auto" MinWidth="40" IsReadOnly="True" CanUserSort="False">
|
||||||
|
<DataGridTemplateColumn.CellTemplate>
|
||||||
|
<DataTemplate>
|
||||||
|
<!-- Applica i consigliati a questa riga soltanto. Il segno di
|
||||||
|
spunta si accende quando i limiti scritti sono già quelli
|
||||||
|
consigliati: serve a vedere a colpo d'occhio quali righe
|
||||||
|
resta da sistemare. -->
|
||||||
|
<Button Style="{StaticResource MiniIconButton}"
|
||||||
|
ToolTip="{Binding AdviceExplanation}"
|
||||||
|
Command="{Binding DataContext.ProductApplyAdviceCommand,
|
||||||
|
RelativeSource={RelativeSource AncestorType=UserControl}}"
|
||||||
|
CommandParameter="{Binding}">
|
||||||
|
<TextBlock FontFamily="Segoe MDL2 Assets" Text="">
|
||||||
|
<TextBlock.Style>
|
||||||
|
<Style TargetType="TextBlock">
|
||||||
|
<Setter Property="Foreground" Value="{DynamicResource Brush.TextFaint}"/>
|
||||||
|
<Style.Triggers>
|
||||||
|
<DataTrigger Binding="{Binding HasAdvice}" Value="False">
|
||||||
|
<Setter Property="Opacity" Value="0.25"/>
|
||||||
|
</DataTrigger>
|
||||||
|
<DataTrigger Binding="{Binding MatchesAdvice}" Value="True">
|
||||||
|
<Setter Property="Foreground" Value="{DynamicResource Brush.Success}"/>
|
||||||
|
</DataTrigger>
|
||||||
|
<DataTrigger Binding="{Binding AdviceWarns}" Value="True">
|
||||||
|
<Setter Property="Foreground" Value="{DynamicResource Brush.Warning}"/>
|
||||||
|
</DataTrigger>
|
||||||
|
</Style.Triggers>
|
||||||
|
</Style>
|
||||||
|
</TextBlock.Style>
|
||||||
|
</TextBlock>
|
||||||
|
</Button>
|
||||||
|
</DataTemplate>
|
||||||
|
</DataGridTemplateColumn.CellTemplate>
|
||||||
|
</DataGridTemplateColumn>
|
||||||
|
|
||||||
|
<DataGridTextColumn Header="Punt./asta" Binding="{Binding StatAverageBidsDisplay}"
|
||||||
|
Width="Auto" MinWidth="82" IsReadOnly="True"
|
||||||
|
ElementStyle="{StaticResource StatText}"/>
|
||||||
|
<DataGridTextColumn Header="Aggiunte" Binding="{Binding AutoAddedCountDisplay}"
|
||||||
|
Width="Auto" MinWidth="74" IsReadOnly="True"
|
||||||
|
ElementStyle="{StaticResource StatText}"/>
|
||||||
|
</DataGrid.Columns>
|
||||||
|
</DataGrid>
|
||||||
|
</Border>
|
||||||
|
|
||||||
|
<!-- Stato vuoto -->
|
||||||
|
<StackPanel x:Name="ProductsEmptyState" VerticalAlignment="Center" HorizontalAlignment="Center"
|
||||||
|
Visibility="Collapsed">
|
||||||
|
<TextBlock Style="{StaticResource Glyph}" Text="" FontSize="34"
|
||||||
|
Foreground="{DynamicResource Brush.TextFaint}"/>
|
||||||
|
<TextBlock Text="Nessun prodotto in elenco."
|
||||||
|
Margin="0,10,0,0" HorizontalAlignment="Center"
|
||||||
|
Foreground="{DynamicResource Brush.TextMuted}"/>
|
||||||
|
<TextBlock Text="Aggiungili dalla scheda Cerca: la stellina lo segue, l'ingranaggio lo mette qui per dargli dei limiti."
|
||||||
|
Margin="0,4,0,0" HorizontalAlignment="Center" TextWrapping="Wrap" MaxWidth="420"
|
||||||
|
TextAlignment="Center"
|
||||||
|
FontSize="{StaticResource Font.Size.Sm}"
|
||||||
|
Foreground="{DynamicResource Brush.TextFaint}"/>
|
||||||
|
</StackPanel>
|
||||||
|
</Grid>
|
||||||
|
</Grid>
|
||||||
|
</UserControl>
|
||||||
@@ -0,0 +1,227 @@
|
|||||||
|
using System.Collections;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Windows;
|
||||||
|
using System.Windows.Controls;
|
||||||
|
using AutoBidder.ViewModels;
|
||||||
|
|
||||||
|
namespace AutoBidder.Controls
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Scheda Prodotti: una riga per articolo, con la stellina, i limiti su misura e le
|
||||||
|
/// statistiche dello storico tutte sulla stessa riga.
|
||||||
|
///
|
||||||
|
/// <para>Il controllo non conosce né gli archivi né il monitor: espone eventi e lascia
|
||||||
|
/// decidere a MainWindow, com'è per gli altri pannelli.</para>
|
||||||
|
/// </summary>
|
||||||
|
public partial class ProductsControl : UserControl
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Elenco completo, indipendente da quello mostrato: la ricerca filtra la vista, non
|
||||||
|
/// i dati. Senza, scrivere nella casella cancellerebbe le righe invece di nasconderle.
|
||||||
|
/// </summary>
|
||||||
|
private IList? _allProducts;
|
||||||
|
|
||||||
|
public ProductsControl()
|
||||||
|
{
|
||||||
|
InitializeComponent();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>Prodotto selezionato, o <c>null</c> se la griglia è vuota o senza selezione.</summary>
|
||||||
|
public ProductViewModel? SelectedProduct => ProductsGrid.SelectedItem as ProductViewModel;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Riempie l'elenco. Le righe sono già ViewModel: la griglia scrive direttamente
|
||||||
|
/// nelle schede prodotto, e la persistenza avviene alla conferma della cella.
|
||||||
|
/// </summary>
|
||||||
|
public void SetProducts(IList products)
|
||||||
|
{
|
||||||
|
_allProducts = products;
|
||||||
|
ApplySearch();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Mostra solo i prodotti il cui nome contiene il testo cercato.
|
||||||
|
///
|
||||||
|
/// <para>Il filtro si applica a una copia: la griglia è modificabile, e gli oggetti
|
||||||
|
/// mostrati sono gli stessi dell'elenco completo — quindi i limiti scritti mentre
|
||||||
|
/// un filtro è attivo restano quando il filtro sparisce.</para>
|
||||||
|
/// </summary>
|
||||||
|
private void ApplySearch()
|
||||||
|
{
|
||||||
|
if (_allProducts == null) return;
|
||||||
|
|
||||||
|
var query = SearchBox.Text?.Trim() ?? "";
|
||||||
|
SearchPlaceholder.Visibility = query.Length == 0 ? Visibility.Visible : Visibility.Collapsed;
|
||||||
|
|
||||||
|
var all = _allProducts.OfType<ProductViewModel>().ToList();
|
||||||
|
|
||||||
|
var shown = query.Length == 0
|
||||||
|
? all
|
||||||
|
: all.Where(p => (p.DisplayName ?? "")
|
||||||
|
.Contains(query, System.StringComparison.OrdinalIgnoreCase))
|
||||||
|
.ToList();
|
||||||
|
|
||||||
|
ProductsGrid.ItemsSource = shown;
|
||||||
|
|
||||||
|
ProductsCountText.Text = query.Length == 0
|
||||||
|
? (all.Count == 1 ? "1 prodotto" : $"{all.Count} prodotti")
|
||||||
|
: $"{shown.Count} di {all.Count}";
|
||||||
|
|
||||||
|
ProductsWatchedText.Text = $"{all.Count(p => p.IsWatched)} seguiti";
|
||||||
|
|
||||||
|
// Con la ricerca attiva e nessun risultato non si mostra il messaggio di elenco
|
||||||
|
// vuoto: direbbe "non segui ancora nessun prodotto", che sarebbe falso.
|
||||||
|
ProductsEmptyState.Visibility = all.Count == 0 ? Visibility.Visible : Visibility.Collapsed;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void SearchBox_TextChanged(object sender, TextChangedEventArgs e) => ApplySearch();
|
||||||
|
|
||||||
|
/// <summary>Riallinea i contatori dopo che una stellina è cambiata.</summary>
|
||||||
|
public void RefreshCounters()
|
||||||
|
{
|
||||||
|
// Si conta sull'elenco completo, non su quello mostrato: con una ricerca attiva
|
||||||
|
// il numero dei seguiti non deve cambiare solo perché sono nascosti.
|
||||||
|
var source = _allProducts ?? ProductsGrid.ItemsSource as IList;
|
||||||
|
if (source == null) return;
|
||||||
|
|
||||||
|
ProductsWatchedText.Text = $"{source.OfType<ProductViewModel>().Count(p => p.IsWatched)} seguiti";
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>Porta la selezione sul prodotto indicato (usato arrivando dalla scheda Cerca).</summary>
|
||||||
|
public void SelectProduct(string identity)
|
||||||
|
{
|
||||||
|
if (ProductsGrid.ItemsSource is not IEnumerable items) return;
|
||||||
|
|
||||||
|
var match = items.OfType<ProductViewModel>()
|
||||||
|
.FirstOrDefault(p => p.Identity == identity);
|
||||||
|
if (match == null) return;
|
||||||
|
|
||||||
|
ProductsGrid.SelectedItem = match;
|
||||||
|
ProductsGrid.ScrollIntoView(match);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Eventi dell'interfaccia ──────────────────────────────────────
|
||||||
|
|
||||||
|
private void ProductsGrid_SelectionChanged(object sender, SelectionChangedEventArgs e)
|
||||||
|
=> RaiseEvent(new RoutedEventArgs(ProductSelectionChangedEvent, this));
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// La cella è stata confermata: il valore è già nel ViewModel, resta da renderlo
|
||||||
|
/// durevole. Si rimanda al termine dell'operazione perché durante l'evento la
|
||||||
|
/// modifica non è ancora stata trasferita al modello.
|
||||||
|
/// </summary>
|
||||||
|
private void ProductsGrid_CellEditEnding(object? sender, DataGridCellEditEndingEventArgs e)
|
||||||
|
{
|
||||||
|
if (e.EditAction != DataGridEditAction.Commit) return;
|
||||||
|
|
||||||
|
Dispatcher.BeginInvoke(new System.Action(() =>
|
||||||
|
RaiseEvent(new RoutedEventArgs(ProductLimitsEditedEvent, this))),
|
||||||
|
System.Windows.Threading.DispatcherPriority.Background);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void RefreshButton_Click(object sender, RoutedEventArgs e)
|
||||||
|
=> RaiseEvent(new RoutedEventArgs(RefreshClickedEvent, this));
|
||||||
|
|
||||||
|
private void ScanNowButton_Click(object sender, RoutedEventArgs e)
|
||||||
|
=> RaiseEvent(new RoutedEventArgs(ScanNowClickedEvent, this));
|
||||||
|
|
||||||
|
private void ReapplyButton_Click(object sender, RoutedEventArgs e)
|
||||||
|
=> RaiseEvent(new RoutedEventArgs(ReapplyClickedEvent, this));
|
||||||
|
|
||||||
|
private void RemoveButton_Click(object sender, RoutedEventArgs e)
|
||||||
|
=> RaiseEvent(new RoutedEventArgs(RemoveClickedEvent, this));
|
||||||
|
|
||||||
|
private void ClearButton_Click(object sender, RoutedEventArgs e)
|
||||||
|
=> RaiseEvent(new RoutedEventArgs(ClearClickedEvent, this));
|
||||||
|
|
||||||
|
private void ApplySuggestedButton_Click(object sender, RoutedEventArgs e)
|
||||||
|
=> RaiseEvent(new RoutedEventArgs(ApplySuggestedClickedEvent, this));
|
||||||
|
|
||||||
|
private void ApplyAllSuggestedButton_Click(object sender, RoutedEventArgs e)
|
||||||
|
=> RaiseEvent(new RoutedEventArgs(ApplyAllSuggestedClickedEvent, this));
|
||||||
|
|
||||||
|
// ── Routed events ────────────────────────────────────────────────
|
||||||
|
|
||||||
|
public static readonly RoutedEvent RefreshClickedEvent = EventManager.RegisterRoutedEvent(
|
||||||
|
"RefreshClicked", RoutingStrategy.Bubble, typeof(RoutedEventHandler), typeof(ProductsControl));
|
||||||
|
|
||||||
|
public static readonly RoutedEvent ScanNowClickedEvent = EventManager.RegisterRoutedEvent(
|
||||||
|
"ScanNowClicked", RoutingStrategy.Bubble, typeof(RoutedEventHandler), typeof(ProductsControl));
|
||||||
|
|
||||||
|
public static readonly RoutedEvent ReapplyClickedEvent = EventManager.RegisterRoutedEvent(
|
||||||
|
"ReapplyClicked", RoutingStrategy.Bubble, typeof(RoutedEventHandler), typeof(ProductsControl));
|
||||||
|
|
||||||
|
public static readonly RoutedEvent RemoveClickedEvent = EventManager.RegisterRoutedEvent(
|
||||||
|
"RemoveClicked", RoutingStrategy.Bubble, typeof(RoutedEventHandler), typeof(ProductsControl));
|
||||||
|
|
||||||
|
public static readonly RoutedEvent ClearClickedEvent = EventManager.RegisterRoutedEvent(
|
||||||
|
"ClearClicked", RoutingStrategy.Bubble, typeof(RoutedEventHandler), typeof(ProductsControl));
|
||||||
|
|
||||||
|
public static readonly RoutedEvent ApplySuggestedClickedEvent = EventManager.RegisterRoutedEvent(
|
||||||
|
"ApplySuggestedClicked", RoutingStrategy.Bubble, typeof(RoutedEventHandler), typeof(ProductsControl));
|
||||||
|
|
||||||
|
public static readonly RoutedEvent ApplyAllSuggestedClickedEvent = EventManager.RegisterRoutedEvent(
|
||||||
|
"ApplyAllSuggestedClicked", RoutingStrategy.Bubble, typeof(RoutedEventHandler), typeof(ProductsControl));
|
||||||
|
|
||||||
|
public static readonly RoutedEvent ProductLimitsEditedEvent = EventManager.RegisterRoutedEvent(
|
||||||
|
"ProductLimitsEdited", RoutingStrategy.Bubble, typeof(RoutedEventHandler), typeof(ProductsControl));
|
||||||
|
|
||||||
|
public static readonly RoutedEvent ProductSelectionChangedEvent = EventManager.RegisterRoutedEvent(
|
||||||
|
"ProductSelectionChanged", RoutingStrategy.Bubble, typeof(RoutedEventHandler), typeof(ProductsControl));
|
||||||
|
|
||||||
|
public event RoutedEventHandler RefreshClicked
|
||||||
|
{
|
||||||
|
add { AddHandler(RefreshClickedEvent, value); }
|
||||||
|
remove { RemoveHandler(RefreshClickedEvent, value); }
|
||||||
|
}
|
||||||
|
|
||||||
|
public event RoutedEventHandler ScanNowClicked
|
||||||
|
{
|
||||||
|
add { AddHandler(ScanNowClickedEvent, value); }
|
||||||
|
remove { RemoveHandler(ScanNowClickedEvent, value); }
|
||||||
|
}
|
||||||
|
|
||||||
|
public event RoutedEventHandler ReapplyClicked
|
||||||
|
{
|
||||||
|
add { AddHandler(ReapplyClickedEvent, value); }
|
||||||
|
remove { RemoveHandler(ReapplyClickedEvent, value); }
|
||||||
|
}
|
||||||
|
|
||||||
|
public event RoutedEventHandler RemoveClicked
|
||||||
|
{
|
||||||
|
add { AddHandler(RemoveClickedEvent, value); }
|
||||||
|
remove { RemoveHandler(RemoveClickedEvent, value); }
|
||||||
|
}
|
||||||
|
|
||||||
|
public event RoutedEventHandler ClearClicked
|
||||||
|
{
|
||||||
|
add { AddHandler(ClearClickedEvent, value); }
|
||||||
|
remove { RemoveHandler(ClearClickedEvent, value); }
|
||||||
|
}
|
||||||
|
|
||||||
|
public event RoutedEventHandler ApplySuggestedClicked
|
||||||
|
{
|
||||||
|
add { AddHandler(ApplySuggestedClickedEvent, value); }
|
||||||
|
remove { RemoveHandler(ApplySuggestedClickedEvent, value); }
|
||||||
|
}
|
||||||
|
|
||||||
|
public event RoutedEventHandler ApplyAllSuggestedClicked
|
||||||
|
{
|
||||||
|
add { AddHandler(ApplyAllSuggestedClickedEvent, value); }
|
||||||
|
remove { RemoveHandler(ApplyAllSuggestedClickedEvent, value); }
|
||||||
|
}
|
||||||
|
|
||||||
|
public event RoutedEventHandler ProductLimitsEdited
|
||||||
|
{
|
||||||
|
add { AddHandler(ProductLimitsEditedEvent, value); }
|
||||||
|
remove { RemoveHandler(ProductLimitsEditedEvent, value); }
|
||||||
|
}
|
||||||
|
|
||||||
|
public event RoutedEventHandler ProductSelectionChanged
|
||||||
|
{
|
||||||
|
add { AddHandler(ProductSelectionChangedEvent, value); }
|
||||||
|
remove { RemoveHandler(ProductSelectionChangedEvent, value); }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -3,81 +3,13 @@
|
|||||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||||
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
|
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
|
||||||
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
|
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
|
||||||
|
xmlns:ctl="clr-namespace:AutoBidder.Controls"
|
||||||
mc:Ignorable="d"
|
mc:Ignorable="d"
|
||||||
d:DesignHeight="800" d:DesignWidth="1200"
|
d:DesignHeight="800" d:DesignWidth="1200"
|
||||||
Background="#1E1E1E">
|
Background="{DynamicResource Brush.Bg}">
|
||||||
|
|
||||||
<UserControl.Resources>
|
<!-- Stili centralizzati in Themes/Controls.xaml -->
|
||||||
<!-- Modern Button Style -->
|
|
||||||
<Style x:Key="ModernButton" TargetType="Button">
|
|
||||||
<Setter Property="Template">
|
|
||||||
<Setter.Value>
|
|
||||||
<ControlTemplate TargetType="Button">
|
|
||||||
<Border Background="{TemplateBinding Background}"
|
|
||||||
CornerRadius="4"
|
|
||||||
Padding="{TemplateBinding Padding}">
|
|
||||||
<ContentPresenter HorizontalAlignment="Center" VerticalAlignment="Center"/>
|
|
||||||
</Border>
|
|
||||||
</ControlTemplate>
|
|
||||||
</Setter.Value>
|
|
||||||
</Setter>
|
|
||||||
<Setter Property="Foreground" Value="White"/>
|
|
||||||
<Setter Property="FontWeight" Value="SemiBold"/>
|
|
||||||
<Setter Property="BorderThickness" Value="0"/>
|
|
||||||
<Setter Property="Cursor" Value="Hand"/>
|
|
||||||
<Setter Property="Padding" Value="20,10"/>
|
|
||||||
<Setter Property="FontSize" Value="13"/>
|
|
||||||
</Style>
|
|
||||||
|
|
||||||
<!-- Section Header Style -->
|
|
||||||
<Style x:Key="SectionHeader" TargetType="TextBlock">
|
|
||||||
<Setter Property="FontSize" Value="16"/>
|
|
||||||
<Setter Property="FontWeight" Value="Bold"/>
|
|
||||||
<Setter Property="Foreground" Value="White"/>
|
|
||||||
<Setter Property="Margin" Value="0,0,0,15"/>
|
|
||||||
</Style>
|
|
||||||
|
|
||||||
<!-- Label Style -->
|
|
||||||
<Style x:Key="FieldLabel" TargetType="TextBlock">
|
|
||||||
<Setter Property="FontSize" Value="12"/>
|
|
||||||
<Setter Property="Foreground" Value="#CCCCCC"/>
|
|
||||||
<Setter Property="Margin" Value="0,0,0,5"/>
|
|
||||||
</Style>
|
|
||||||
|
|
||||||
<!-- TextBox Style -->
|
|
||||||
<Style TargetType="TextBox">
|
|
||||||
<Setter Property="Background" Value="#252526"/>
|
|
||||||
<Setter Property="Foreground" Value="White"/>
|
|
||||||
<Setter Property="BorderBrush" Value="#3E3E42"/>
|
|
||||||
<Setter Property="BorderThickness" Value="1"/>
|
|
||||||
<Setter Property="Padding" Value="10"/>
|
|
||||||
<Setter Property="FontSize" Value="13"/>
|
|
||||||
</Style>
|
|
||||||
|
|
||||||
<!-- CheckBox Style -->
|
|
||||||
<Style TargetType="CheckBox">
|
|
||||||
<Setter Property="Foreground" Value="#CCCCCC"/>
|
|
||||||
<Setter Property="Margin" Value="0,6"/>
|
|
||||||
<Setter Property="FontSize" Value="13"/>
|
|
||||||
</Style>
|
|
||||||
|
|
||||||
<!-- RadioButton Style -->
|
|
||||||
<Style TargetType="RadioButton">
|
|
||||||
<Setter Property="Foreground" Value="#CCCCCC"/>
|
|
||||||
<Setter Property="Margin" Value="0,0,20,0"/>
|
|
||||||
<Setter Property="FontSize" Value="13"/>
|
|
||||||
</Style>
|
|
||||||
|
|
||||||
<!-- Info Box Style -->
|
|
||||||
<Style x:Key="InfoBox" TargetType="Border">
|
|
||||||
<Setter Property="Background" Value="#2D2D30"/>
|
|
||||||
<Setter Property="BorderBrush" Value="#3E3E42"/>
|
|
||||||
<Setter Property="BorderThickness" Value="1"/>
|
|
||||||
<Setter Property="CornerRadius" Value="4"/>
|
|
||||||
<Setter Property="Padding" Value="15"/>
|
|
||||||
<Setter Property="Margin" Value="0,10,0,0"/>
|
|
||||||
</Style>
|
|
||||||
</UserControl.Resources>
|
|
||||||
|
|
||||||
<Grid>
|
<Grid>
|
||||||
<Grid.RowDefinitions>
|
<Grid.RowDefinitions>
|
||||||
@@ -88,23 +20,89 @@
|
|||||||
<!-- Scrollable Content -->
|
<!-- Scrollable Content -->
|
||||||
<ScrollViewer Grid.Row="0" VerticalScrollBarVisibility="Auto">
|
<ScrollViewer Grid.Row="0" VerticalScrollBarVisibility="Auto">
|
||||||
<StackPanel Margin="30,20">
|
<StackPanel Margin="30,20">
|
||||||
|
|
||||||
<!-- SEZIONE 1: Impostazioni Predefinite Aste -->
|
<!-- SEZIONE 0: Aspetto -->
|
||||||
<Border Background="#252526"
|
<Border Style="{StaticResource CardBlock}">
|
||||||
BorderBrush="#3E3E42"
|
|
||||||
BorderThickness="1"
|
|
||||||
CornerRadius="4"
|
|
||||||
Padding="20"
|
|
||||||
Margin="0,0,0,20">
|
|
||||||
<StackPanel>
|
<StackPanel>
|
||||||
<TextBlock Text="Impostazioni Predefinite Aste"
|
<ctl:HelpHeader Title="Aspetto"
|
||||||
Style="{StaticResource SectionHeader}"/>
|
Margin="0,0,0,14"
|
||||||
|
Help="Scegli il tema dell'interfaccia. Il cambiamento è immediato."/>
|
||||||
<TextBlock Text="Queste impostazioni verranno applicate automaticamente alle nuove aste."
|
|
||||||
Foreground="#999999"
|
<StackPanel Orientation="Horizontal">
|
||||||
FontSize="12"
|
<RadioButton x:Name="ThemeDarkRadio"
|
||||||
TextWrapping="Wrap"
|
Content="Scuro"
|
||||||
Margin="0,0,0,20"/>
|
GroupName="AppTheme"
|
||||||
|
IsChecked="True"
|
||||||
|
Checked="ThemeDarkRadio_Checked"
|
||||||
|
ToolTip="Tema scuro (consigliato per sessioni lunghe)"/>
|
||||||
|
<RadioButton x:Name="ThemeLightRadio"
|
||||||
|
Content="Chiaro"
|
||||||
|
GroupName="AppTheme"
|
||||||
|
Checked="ThemeLightRadio_Checked"
|
||||||
|
ToolTip="Tema chiaro"/>
|
||||||
|
</StackPanel>
|
||||||
|
</StackPanel>
|
||||||
|
</Border>
|
||||||
|
|
||||||
|
<!-- SEZIONE 0-bis: Sessione / Autenticazione -->
|
||||||
|
<Border Style="{StaticResource CardBlock}">
|
||||||
|
<StackPanel>
|
||||||
|
<ctl:HelpHeader Title="Sessione Bidoo (Autenticazione)"
|
||||||
|
Margin="0,0,0,14"
|
||||||
|
Help="Accedi dalla scheda Browser: il cookie viene rilevato e importato da solo, e vale per tutta l'applicazione. La sessione resta salvata e protetta con DPAPI, quindi l'accesso non va rifatto a ogni avvio. Un solo modo di accedere e nessun campo per il cookie a mano: due strade vorrebbero dire due sessioni possibili, e la domanda "quale sta usando?" senza risposta."/>
|
||||||
|
|
||||||
|
<!-- Stato corrente -->
|
||||||
|
<Border Background="{DynamicResource Brush.SurfaceAlt}"
|
||||||
|
BorderBrush="{DynamicResource Brush.Border}"
|
||||||
|
BorderThickness="1"
|
||||||
|
CornerRadius="6"
|
||||||
|
Padding="12,10"
|
||||||
|
Margin="0,0,0,14">
|
||||||
|
<StackPanel>
|
||||||
|
<TextBlock x:Name="SessionStatusText"
|
||||||
|
Text="Non connesso"
|
||||||
|
Foreground="{DynamicResource Brush.Danger}"
|
||||||
|
FontWeight="SemiBold"/>
|
||||||
|
<TextBlock x:Name="SessionStatusDetail"
|
||||||
|
Text="Nessuna sessione attiva."
|
||||||
|
Foreground="{DynamicResource Brush.TextMuted}"
|
||||||
|
FontSize="11.5"
|
||||||
|
Margin="0,3,0,0"
|
||||||
|
TextWrapping="Wrap"/>
|
||||||
|
</StackPanel>
|
||||||
|
</Border>
|
||||||
|
|
||||||
|
<StackPanel Orientation="Horizontal">
|
||||||
|
<Button Content="Apri Browser per il login"
|
||||||
|
Background="{DynamicResource Brush.Accent}"
|
||||||
|
Foreground="{DynamicResource Brush.TextOnAccent}"
|
||||||
|
Style="{StaticResource SmallRoundedButton}"
|
||||||
|
Click="OpenBrowserLoginButton_Click"
|
||||||
|
Margin="0,0,8,0"/>
|
||||||
|
<Button x:Name="ConnectSessionButton"
|
||||||
|
Content="Verifica connessione"
|
||||||
|
Background="{DynamicResource Brush.SurfaceAlt}"
|
||||||
|
Foreground="{DynamicResource Brush.Text}"
|
||||||
|
Style="{StaticResource SmallRoundedButton}"
|
||||||
|
ToolTip="Ricontrolla la sessione salvata contro il sito"
|
||||||
|
Click="ConnectSessionButton_Click"
|
||||||
|
Margin="0,0,8,0"/>
|
||||||
|
<Button x:Name="DisconnectSessionButton"
|
||||||
|
Content="Disconnetti"
|
||||||
|
Background="{DynamicResource Brush.SurfaceAlt}"
|
||||||
|
Foreground="{DynamicResource Brush.Text}"
|
||||||
|
Style="{StaticResource SmallRoundedButton}"
|
||||||
|
Click="DisconnectSessionButton_Click"/>
|
||||||
|
</StackPanel>
|
||||||
|
</StackPanel>
|
||||||
|
</Border>
|
||||||
|
|
||||||
|
<!-- SEZIONE 1: Impostazioni Predefinite Aste -->
|
||||||
|
<Border Style="{StaticResource CardBlock}">
|
||||||
|
<StackPanel>
|
||||||
|
<ctl:HelpHeader Title="Impostazioni Predefinite Aste"
|
||||||
|
Margin="0,0,0,14"
|
||||||
|
Help="Valgono per ogni asta nuova che non abbia limiti propri. I limiti scritti nella scheda Prodotti hanno sempre la precedenza su questi."/>
|
||||||
|
|
||||||
<Grid>
|
<Grid>
|
||||||
<Grid.ColumnDefinitions>
|
<Grid.ColumnDefinitions>
|
||||||
@@ -119,40 +117,67 @@
|
|||||||
<RowDefinition Height="Auto"/>
|
<RowDefinition Height="Auto"/>
|
||||||
</Grid.RowDefinitions>
|
</Grid.RowDefinitions>
|
||||||
|
|
||||||
<TextBlock Grid.Row="0" Grid.Column="0" Text="Anticipo Puntata (millisecondi)" Foreground="#CCCCCC" Margin="0,10" VerticalAlignment="Center" ToolTip="Millisecondi prima della scadenza per puntare"/>
|
<TextBlock Grid.Row="0" Grid.Column="0" Text="Anticipo Puntata (millisecondi)" Foreground="{DynamicResource Brush.Text}" Margin="0,10" VerticalAlignment="Center" ToolTip="Quanti millisecondi prima della scadenza parte la puntata. È il parametro che decide quanto costa vincere: il motore punta solo nei cicli che arrivano fin qui senza che nessun altro abbia puntato. Dalla rigiocata di 2.095 aste concluse: sotto il secondo basta in mediana 1 puntata per asta, sotto i 2 s ne servono 15, sotto i 3 s 46. Ma sotto i 600 ms un picco di rete fa arrivare la puntata a giochi chiusi (il ping tocca 444 ms nel p99,9). Fascia consigliata: 600–2000 ms. Predefinito 1000."/>
|
||||||
<TextBox Grid.Row="0" Grid.Column="1" x:Name="DefaultBidBeforeDeadlineMsTextBox" Text="200" Margin="10,10"/>
|
<TextBox Grid.Row="0" Grid.Column="1" x:Name="DefaultBidBeforeDeadlineMsTextBox" Text="200" Margin="10,10"/>
|
||||||
|
|
||||||
<TextBlock Grid.Row="1" Grid.Column="0" Text="Verifica Stato Prima Di Puntare" Foreground="#CCCCCC" Margin="0,10" VerticalAlignment="Center" ToolTip="Controlla che l'asta sia ancora aperta prima di puntare"/>
|
|
||||||
<CheckBox Grid.Row="1" Grid.Column="1" x:Name="DefaultCheckAuctionOpenCheckBox" Margin="10,10" VerticalAlignment="Center"/>
|
|
||||||
|
|
||||||
<TextBlock Grid.Row="2" Grid.Column="0" Text="Prezzo Minimo (€)" Foreground="#CCCCCC" Margin="0,10" VerticalAlignment="Center"/>
|
<TextBlock Grid.Row="2" Grid.Column="0" Text="Prezzo Minimo (€)" Foreground="{DynamicResource Brush.Text}" Margin="0,10" VerticalAlignment="Center"/>
|
||||||
<TextBox Grid.Row="2" Grid.Column="1" x:Name="DefaultMinPriceTextBox" Text="0" Margin="10,10"/>
|
<TextBox Grid.Row="2" Grid.Column="1" x:Name="DefaultMinPriceTextBox" Text="0" Margin="10,10"/>
|
||||||
|
|
||||||
<TextBlock Grid.Row="3" Grid.Column="0" Text="Prezzo Massimo (€)" Foreground="#CCCCCC" Margin="0,10" VerticalAlignment="Center"/>
|
<TextBlock Grid.Row="3" Grid.Column="0" Text="Prezzo Massimo (€)" Foreground="{DynamicResource Brush.Text}" Margin="0,10" VerticalAlignment="Center"/>
|
||||||
<TextBox Grid.Row="3" Grid.Column="1" x:Name="DefaultMaxPriceTextBox" Text="0" Margin="10,10"/>
|
<TextBox Grid.Row="3" Grid.Column="1" x:Name="DefaultMaxPriceTextBox" Text="0" Margin="10,10"/>
|
||||||
|
|
||||||
<TextBlock Grid.Row="4" Grid.Column="0" Text="Max Click" Foreground="#CCCCCC" Margin="0,10" VerticalAlignment="Center"/>
|
<TextBlock Grid.Row="4" Grid.Column="0" Text="Max Click" Foreground="{DynamicResource Brush.Text}" Margin="0,10" VerticalAlignment="Center" ToolTip="Quante puntate al massimo spendere su una singola asta. Entra anche nel calcolo dei limiti consigliati per prodotto: ogni puntata in più è denaro che non si può mettere sul prezzo. 0 = illimitate."/>
|
||||||
<TextBox Grid.Row="4" Grid.Column="1" x:Name="DefaultMaxClicksTextBox" Text="0" Margin="10,10"/>
|
<TextBox Grid.Row="4" Grid.Column="1" x:Name="DefaultMaxClicksTextBox" Text="0" Margin="10,10"/>
|
||||||
</Grid>
|
</Grid>
|
||||||
</StackPanel>
|
</StackPanel>
|
||||||
</Border>
|
</Border>
|
||||||
|
|
||||||
<!-- SEZIONE 2: Stato Iniziale Aste -->
|
<!-- SEZIONE: Limiti consigliati per prodotto -->
|
||||||
<Border Background="#252526"
|
<Border Style="{StaticResource CardBlock}">
|
||||||
BorderBrush="#3E3E42"
|
|
||||||
BorderThickness="1"
|
|
||||||
CornerRadius="4"
|
|
||||||
Padding="20"
|
|
||||||
Margin="0,0,0,20">
|
|
||||||
<StackPanel>
|
<StackPanel>
|
||||||
<TextBlock Text="Stato Iniziale Aste"
|
<ctl:HelpHeader Title="Limiti Consigliati per Prodotto"
|
||||||
Style="{StaticResource SectionHeader}"/>
|
Margin="0,0,0,14"
|
||||||
|
Help="La scheda Prodotti propone, per ogni articolo, un prezzo minimo e uno massimo ricavati da come sono andate le sue aste finora. Il massimo nasce da due vincoli e vince il più stretto: il prezzo che copre la quota di chiusure impostata qui sotto, e quanto si può spendere — valore del prodotto, meno il costo delle puntate massime. Il minimo è il decimo percentile delle chiusure: sotto quel prezzo l'asta non si chiude quasi mai, quindi puntare lì è quasi sempre speso a vuoto. Perché non basta la moda: il prezzo a cui un articolo si aggiudica più spesso copre fra il 15% e il 44% delle sue aste, a seconda del prodotto. Fermandosi lì si resterebbe fuori dalla maggior parte delle occasioni."/>
|
||||||
<TextBlock Text="Configura come devono comportarsi le aste quando vengono caricate o aggiunte."
|
|
||||||
Foreground="#999999"
|
<Grid>
|
||||||
FontSize="12"
|
<Grid.ColumnDefinitions>
|
||||||
TextWrapping="Wrap"
|
<ColumnDefinition Width="250"/>
|
||||||
Margin="0,0,0,20"/>
|
<ColumnDefinition Width="200"/>
|
||||||
|
</Grid.ColumnDefinitions>
|
||||||
|
<Grid.RowDefinitions>
|
||||||
|
<RowDefinition Height="Auto"/>
|
||||||
|
<RowDefinition Height="Auto"/>
|
||||||
|
</Grid.RowDefinitions>
|
||||||
|
|
||||||
|
<TextBlock Grid.Row="0" Grid.Column="0" Text="Copertura delle chiusure (%)"
|
||||||
|
Foreground="{DynamicResource Brush.Text}" Margin="0,10" VerticalAlignment="Center"
|
||||||
|
ToolTip="In quante aste passate saresti stato ancora in gioco fermandoti al massimo consigliato. Alzarla vuol dire restare in corsa più spesso ma spingersi su prezzi più alti; abbassarla vuol dire puntare solo sulle occasioni buone e lasciar perdere le altre. Fascia sensata: 50–95."/>
|
||||||
|
<TextBox Grid.Row="0" Grid.Column="1" x:Name="SuggestedCoverageTextBox"
|
||||||
|
Text="85" Margin="10,10"/>
|
||||||
|
|
||||||
|
<TextBlock Grid.Row="1" Grid.Column="0" Text="Costo medio di una puntata (€)"
|
||||||
|
Foreground="{DynamicResource Brush.Text}" Margin="0,10" VerticalAlignment="Center"
|
||||||
|
ToolTip="Quanto ti costa una puntata. Serve a calcolare il tetto di spesa: valore del prodotto meno le puntate massime moltiplicate per questo costo. Il valore predefinito è 0,20 €, cioè il pacchetto meno vantaggioso in vendita su Bidoo: tenerlo così fa stimare per eccesso, e una stima per eccesso al massimo ti fa rinunciare a un affare, mentre una per difetto ti fa spendere più del valore del prodotto. Se compri pacchetti più grandi, abbassalo al costo che paghi davvero."/>
|
||||||
|
<TextBox Grid.Row="1" Grid.Column="1" x:Name="AverageBidCostTextBox"
|
||||||
|
Text="0,20" Margin="10,10"/>
|
||||||
|
</Grid>
|
||||||
|
</StackPanel>
|
||||||
|
</Border>
|
||||||
|
|
||||||
|
<!-- SEZIONE 2: Stato Iniziale Aste -->
|
||||||
|
<Border Style="{StaticResource CardBlock}">
|
||||||
|
<StackPanel>
|
||||||
|
<ctl:HelpHeader Title="Stato Iniziale Aste"
|
||||||
|
Margin="0,0,0,14"
|
||||||
|
Help="Come si comportano le aste quando vengono caricate o aggiunte.
|
||||||
|
|
||||||
|
• Fermata: non viene monitorata fino all'avvio manuale.
|
||||||
|
• Osserva: viene seguita (prezzo, scadenza, avversari) ma non punta mai. Utile per studiarla prima di entrare.
|
||||||
|
• Attiva: viene monitorata e punta da sola quando serve.
|
||||||
|
• Ricorda Stato: ogni asta riprende lo stato che aveva alla chiusura, e sovrascrive le altre opzioni.
|
||||||
|
|
||||||
|
Consiglio: usa Fermata per caricare senza avviare, poi avvia a mano quelle che vuoi."/>
|
||||||
|
|
||||||
<!-- Stato all'apertura applicazione -->
|
<!-- Stato all'apertura applicazione -->
|
||||||
<TextBlock Text="Stato aste al caricamento dell'applicazione"
|
<TextBlock Text="Stato aste al caricamento dell'applicazione"
|
||||||
@@ -165,9 +190,9 @@
|
|||||||
IsChecked="True"
|
IsChecked="True"
|
||||||
ToolTip="Le aste salvate verranno caricate in stato fermo"/>
|
ToolTip="Le aste salvate verranno caricate in stato fermo"/>
|
||||||
<RadioButton x:Name="LoadAuctionsPaused"
|
<RadioButton x:Name="LoadAuctionsPaused"
|
||||||
Content="In Pausa"
|
Content="Osserva"
|
||||||
GroupName="LoadState"
|
GroupName="LoadState"
|
||||||
ToolTip="Le aste salvate verranno caricate in pausa (pronte ad essere avviate)"/>
|
ToolTip="Le aste salvate seguiranno prezzo e scadenza senza mai puntare"/>
|
||||||
<RadioButton x:Name="LoadAuctionsActive"
|
<RadioButton x:Name="LoadAuctionsActive"
|
||||||
Content="Attive"
|
Content="Attive"
|
||||||
GroupName="LoadState"
|
GroupName="LoadState"
|
||||||
@@ -189,9 +214,9 @@
|
|||||||
IsChecked="True"
|
IsChecked="True"
|
||||||
ToolTip="Le nuove aste verranno aggiunte in stato fermo"/>
|
ToolTip="Le nuove aste verranno aggiunte in stato fermo"/>
|
||||||
<RadioButton x:Name="NewAuctionPaused"
|
<RadioButton x:Name="NewAuctionPaused"
|
||||||
Content="In Pausa"
|
Content="Osserva"
|
||||||
GroupName="NewAuctionState"
|
GroupName="NewAuctionState"
|
||||||
ToolTip="Le nuove aste verranno aggiunte in pausa"/>
|
ToolTip="Le nuove aste seguiranno prezzo e scadenza senza mai puntare"/>
|
||||||
<RadioButton x:Name="NewAuctionActive"
|
<RadioButton x:Name="NewAuctionActive"
|
||||||
Content="Attiva"
|
Content="Attiva"
|
||||||
GroupName="NewAuctionState"
|
GroupName="NewAuctionState"
|
||||||
@@ -199,45 +224,419 @@
|
|||||||
</StackPanel>
|
</StackPanel>
|
||||||
|
|
||||||
<!-- Info Box -->
|
<!-- Info Box -->
|
||||||
|
</StackPanel>
|
||||||
|
</Border>
|
||||||
|
|
||||||
|
<!-- SEZIONE: Motore di precisione -->
|
||||||
|
<Border Style="{StaticResource CardBlock}">
|
||||||
|
<StackPanel>
|
||||||
|
<ctl:HelpHeader Title="Motore di Precisione"
|
||||||
|
Margin="0,0,0,14"
|
||||||
|
Help="Ogni asta ha il proprio motore indipendente. Il polling si infittisce solo avvicinandosi alla scadenza: un'asta lontana non consuma richieste inutili, una vicina sì."/>
|
||||||
|
|
||||||
|
<Grid>
|
||||||
|
<Grid.ColumnDefinitions>
|
||||||
|
<ColumnDefinition Width="250"/>
|
||||||
|
<ColumnDefinition Width="150"/>
|
||||||
|
</Grid.ColumnDefinitions>
|
||||||
|
<Grid.RowDefinitions>
|
||||||
|
<RowDefinition Height="Auto"/>
|
||||||
|
<RowDefinition Height="Auto"/>
|
||||||
|
<RowDefinition Height="Auto"/>
|
||||||
|
<RowDefinition Height="Auto"/>
|
||||||
|
<RowDefinition Height="Auto"/>
|
||||||
|
<RowDefinition Height="Auto"/>
|
||||||
|
<RowDefinition Height="Auto"/>
|
||||||
|
</Grid.RowDefinitions>
|
||||||
|
|
||||||
|
<TextBlock Grid.Row="0" Grid.Column="0" Text="Polling oltre 60s (ms)" Foreground="{DynamicResource Brush.Text}" Margin="0,10" VerticalAlignment="Center" ToolTip="Cadenza quando la scadenza è lontana"/>
|
||||||
|
<TextBox Grid.Row="0" Grid.Column="1" x:Name="PollIntervalFarMsTextBox" Text="2000" Margin="10,10"/>
|
||||||
|
|
||||||
|
<TextBlock Grid.Row="1" Grid.Column="0" Text="Polling 10-60s (ms)" Foreground="{DynamicResource Brush.Text}" Margin="0,10" VerticalAlignment="Center"/>
|
||||||
|
<TextBox Grid.Row="1" Grid.Column="1" x:Name="PollIntervalMidMsTextBox" Text="900" Margin="10,10"/>
|
||||||
|
|
||||||
|
<TextBlock Grid.Row="2" Grid.Column="0" Text="Polling sotto 10s (ms)" Foreground="{DynamicResource Brush.Text}" Margin="0,10" VerticalAlignment="Center"/>
|
||||||
|
<TextBox Grid.Row="2" Grid.Column="1" x:Name="PollIntervalNearMsTextBox" Text="400" Margin="10,10"/>
|
||||||
|
|
||||||
|
<TextBlock Grid.Row="3" Grid.Column="0" Text="Polling in finestra critica (ms)" Foreground="{DynamicResource Brush.Text}" Margin="0,10" VerticalAlignment="Center" ToolTip="Usato solo dalle aste in stato Attiva: in Osserva non c'è puntata da azzeccare"/>
|
||||||
|
<TextBox Grid.Row="3" Grid.Column="1" x:Name="PollIntervalCriticalMsTextBox" Text="220" Margin="10,10"/>
|
||||||
|
|
||||||
|
<TextBlock Grid.Row="4" Grid.Column="0" Text="Ampiezza finestra critica (ms)" Foreground="{DynamicResource Brush.Text}" Margin="0,10" VerticalAlignment="Center"/>
|
||||||
|
<TextBox Grid.Row="4" Grid.Column="1" x:Name="CriticalWindowMsTextBox" Text="4000" Margin="10,10"/>
|
||||||
|
|
||||||
|
<TextBlock Grid.Row="5" Grid.Column="0" Text="Limite richieste al secondo" Foreground="{DynamicResource Brush.Text}" Margin="0,10" VerticalAlignment="Center" ToolTip="Le puntate hanno corsia preferenziale e non sono soggette a questo limite"/>
|
||||||
|
<TextBox Grid.Row="5" Grid.Column="1" x:Name="MaxRequestsPerSecondTextBox" Text="40" Margin="10,10"/>
|
||||||
|
|
||||||
|
<TextBlock Grid.Row="6" Grid.Column="0" Text="Timer di sistema a 1 ms" Foreground="{DynamicResource Brush.Text}" Margin="0,10" VerticalAlignment="Center" ToolTip="Senza, ogni attesa può sforare di ~15 ms e l'anticipo perde significato"/>
|
||||||
|
<CheckBox Grid.Row="6" Grid.Column="1" x:Name="PrecisionTimerCheckBox" Margin="10,10" VerticalAlignment="Center" IsChecked="True"/>
|
||||||
|
</Grid>
|
||||||
|
|
||||||
<Border Style="{StaticResource InfoBox}" Margin="0,15,0,0">
|
<Border Style="{StaticResource InfoBox}" Margin="0,15,0,0">
|
||||||
<StackPanel>
|
<StackPanel>
|
||||||
<TextBlock Text="Informazioni"
|
<TextBlock Text="Diagnostica motore"
|
||||||
FontWeight="Bold"
|
FontWeight="Bold"
|
||||||
Foreground="#007ACC"
|
Foreground="{DynamicResource Brush.Accent}"
|
||||||
Margin="0,0,0,10"/>
|
Margin="0,0,0,10"/>
|
||||||
<TextBlock TextWrapping="Wrap"
|
<TextBlock x:Name="EngineDiagnosticsText"
|
||||||
Foreground="#CCCCCC"
|
Text="Monitoraggio non avviato."
|
||||||
|
TextWrapping="Wrap"
|
||||||
|
Foreground="{DynamicResource Brush.Text}"
|
||||||
FontSize="12"
|
FontSize="12"
|
||||||
LineHeight="18">
|
LineHeight="18"/>
|
||||||
• <Bold>Fermata:</Bold> L'asta non viene monitorata fino all'avvio manuale.<LineBreak/>
|
|
||||||
• <Bold>In Pausa:</Bold> L'asta è pronta ma non punta automaticamente (utile per preparare le aste).<LineBreak/>
|
|
||||||
• <Bold>Attiva:</Bold> L'asta viene monitorata e punta automaticamente quando necessario.<LineBreak/>
|
|
||||||
• <Bold>Ricorda Stato:</Bold> Ogni asta ripristina lo stato esatto che aveva alla chiusura (SOVRASCRIVE le altre opzioni).<LineBreak/>
|
|
||||||
<LineBreak/>
|
|
||||||
<Bold>Consiglio:</Bold> Usa "Fermata" per caricare le aste senza avviarle, poi avvia manualmente quelle desiderate.<LineBreak/>
|
|
||||||
Usa "Ricorda Stato" per riprendere esattamente da dove avevi lasciato.
|
|
||||||
</TextBlock>
|
|
||||||
</StackPanel>
|
</StackPanel>
|
||||||
</Border>
|
</Border>
|
||||||
</StackPanel>
|
</StackPanel>
|
||||||
</Border>
|
</Border>
|
||||||
|
|
||||||
<!-- SEZIONE 3: Protezione Account -->
|
<!-- SEZIONE: Prodotti seguiti (aggiunta automatica) -->
|
||||||
<Border Background="#252526"
|
<Border Style="{StaticResource CardBlock}">
|
||||||
BorderBrush="#3E3E42"
|
|
||||||
BorderThickness="1"
|
|
||||||
CornerRadius="4"
|
|
||||||
Padding="20"
|
|
||||||
Margin="0,0,0,20">
|
|
||||||
<StackPanel>
|
<StackPanel>
|
||||||
<TextBlock Text="Protezione Account"
|
<ctl:HelpHeader Title="Prodotti Seguiti (aggiunta automatica)"
|
||||||
Style="{StaticResource SectionHeader}"/>
|
Margin="0,0,0,14"
|
||||||
|
Help="Bidoo rimette all'asta lo stesso articolo di continuo. Con la stellina in Esplora segui un prodotto: le aste nuove di quell'articolo entrano nel monitor da sole.
|
||||||
<TextBlock Text="Impostazioni di sicurezza per proteggere il tuo account dalle puntate eccessive."
|
|
||||||
Foreground="#999999"
|
Perché il valore consigliato è Osserva: un'asta che entra da sola e comincia subito a puntare spenderebbe puntate vere senza che tu l'abbia mai vista.
|
||||||
FontSize="12"
|
|
||||||
TextWrapping="Wrap"
|
L'elenco dei prodotti è nella scheda Prodotti: lì ogni articolo ha la stellina e i limiti suoi — prezzo, puntate, orizzonte e stato d'ingresso — che vincono su questi."/>
|
||||||
Margin="0,0,0,20"/>
|
|
||||||
|
<Grid>
|
||||||
|
<Grid.ColumnDefinitions>
|
||||||
|
<ColumnDefinition Width="250"/>
|
||||||
|
<ColumnDefinition Width="200"/>
|
||||||
|
</Grid.ColumnDefinitions>
|
||||||
|
<Grid.RowDefinitions>
|
||||||
|
<RowDefinition Height="Auto"/>
|
||||||
|
<RowDefinition Height="Auto"/>
|
||||||
|
<RowDefinition Height="Auto"/>
|
||||||
|
<RowDefinition Height="Auto"/>
|
||||||
|
<RowDefinition Height="Auto"/>
|
||||||
|
<RowDefinition Height="Auto"/>
|
||||||
|
<RowDefinition Height="Auto"/>
|
||||||
|
</Grid.RowDefinitions>
|
||||||
|
|
||||||
|
<TextBlock Grid.Row="0" Grid.Column="0" Text="Aggiungi le aste nuove da solo"
|
||||||
|
Foreground="{DynamicResource Brush.Text}" Margin="0,10" VerticalAlignment="Center"/>
|
||||||
|
<CheckBox Grid.Row="0" Grid.Column="1" x:Name="AutoAddEnabledCheckBox"
|
||||||
|
Margin="10,10" VerticalAlignment="Center" IsChecked="True"/>
|
||||||
|
|
||||||
|
<TextBlock Grid.Row="1" Grid.Column="0" Text="Stato delle aste aggiunte"
|
||||||
|
Foreground="{DynamicResource Brush.Text}" Margin="0,10" VerticalAlignment="Center"
|
||||||
|
ToolTip="Con cosa entrano nel monitor le aste trovate dalla sorveglianza"/>
|
||||||
|
<ComboBox Grid.Row="1" Grid.Column="1" x:Name="AutoAddStateCombo"
|
||||||
|
Margin="10,10" VerticalAlignment="Center">
|
||||||
|
<ComboBoxItem Content="Osserva (consigliato)" Tag="Watch"/>
|
||||||
|
<ComboBoxItem Content="Ferma" Tag="Stopped"/>
|
||||||
|
<ComboBoxItem Content="Attiva — punta subito" Tag="Active"/>
|
||||||
|
</ComboBox>
|
||||||
|
|
||||||
|
<TextBlock Grid.Row="2" Grid.Column="0" Text="Cerca aste nuove ogni (secondi)"
|
||||||
|
Foreground="{DynamicResource Brush.Text}" Margin="0,10" VerticalAlignment="Center"/>
|
||||||
|
<TextBox Grid.Row="2" Grid.Column="1" x:Name="AutoAddScanSecondsTextBox"
|
||||||
|
Text="90" Margin="10,10"/>
|
||||||
|
|
||||||
|
<TextBlock Grid.Row="3" Grid.Column="0" Text="Massimo aste aggiunte insieme"
|
||||||
|
Foreground="{DynamicResource Brush.Text}" Margin="0,10" VerticalAlignment="Center"
|
||||||
|
ToolTip="0 = nessun limite"/>
|
||||||
|
<TextBox Grid.Row="3" Grid.Column="1" x:Name="AutoAddMaxAuctionsTextBox"
|
||||||
|
Text="20" Margin="10,10"/>
|
||||||
|
|
||||||
|
<TextBlock Grid.Row="4" Grid.Column="0" Text="Solo se cominciano entro (minuti)"
|
||||||
|
Foreground="{DynamicResource Brush.Text}" Margin="0,10" VerticalAlignment="Center"
|
||||||
|
ToolTip="Un'asta che comincia oltre questo tempo non viene aggiunta ancora: verrà ripresa alla scansione successiva, quando sarà più vicina. Serve a non riempire il monitor di aste che non succederanno per mezza giornata, consumando il tetto qui sopra. A mano puoi sempre aggiungere qualunque asta. 0 = nessun limite."/>
|
||||||
|
<TextBox Grid.Row="4" Grid.Column="1" x:Name="AutoAddMaxStartMinutesTextBox"
|
||||||
|
Text="60" Margin="10,10"/>
|
||||||
|
|
||||||
|
<TextBlock Grid.Row="5" Grid.Column="0" Text="Solo aste non ancora cominciate"
|
||||||
|
Foreground="{DynamicResource Brush.Text}" Margin="0,10" VerticalAlignment="Center"
|
||||||
|
ToolTip="Aggiungi da solo soltanto le aste su cui nessuno ha ancora puntato, per poterle seguire dal primo istante all'ultimo. Un'asta presa a metà si può seguire, ma la sua storia è già scritta per buona parte: non si sa quanto è durata davvero, chi c'era prima, come è salito il prezzo. Le statistiche che ne escono sono monche e i dossier raccontano solo la seconda metà. Il prezzo da pagare è che si perdono le occasioni già in corso."/>
|
||||||
|
<CheckBox Grid.Row="5" Grid.Column="1" x:Name="AutoAddOnlyNotStartedCheckBox"
|
||||||
|
Margin="10,10" VerticalAlignment="Center"/>
|
||||||
|
|
||||||
|
<TextBlock Grid.Row="6" Grid.Column="0" Text="Quante aste percorrere nella ricerca"
|
||||||
|
Foreground="{DynamicResource Brush.Text}" Margin="0,10" VerticalAlignment="Center"
|
||||||
|
ToolTip="Il listato è ordinato per scadenza: le aste che devono ancora aprire stanno in fondo. Un valore alto è ciò che permette di prenderle prima che inizino. Abbassarlo fa risparmiare richieste, ma le aste vengono prese solo quando sono già cominciate."/>
|
||||||
|
<TextBox Grid.Row="6" Grid.Column="1" x:Name="AutoAddScanDepthTextBox"
|
||||||
|
Text="2500" Margin="10,10"/>
|
||||||
|
</Grid>
|
||||||
|
|
||||||
|
|
||||||
|
</StackPanel>
|
||||||
|
</Border>
|
||||||
|
|
||||||
|
<!-- SEZIONE: Anticipo puntata - misura e taratura -->
|
||||||
|
<Border Style="{StaticResource CardBlock}">
|
||||||
|
<StackPanel>
|
||||||
|
<ctl:HelpHeader Title="Anticipo Puntata (misura e taratura)"
|
||||||
|
Margin="0,0,0,14"
|
||||||
|
Help="L'anticipo impostato è una scommessa; quello che conta è dove la puntata arriva davvero. L'applicazione misura ogni puntata riuscita e, con abbastanza dati, dice se c'è margine per correggere.
|
||||||
|
|
||||||
|
Dalla rigiocata di 2.095 aste concluse: con anticipo sotto il secondo serve in mediana 1 puntata per asta, sotto i 2 s ne servono 15, sotto i 3 s 46. Dall'altro lato il ping tocca 444 ms nel p99,9, quindi sotto i 600 ms la puntata rischia di arrivare a giochi chiusi. La fascia sensata è 600–2000 ms."/>
|
||||||
|
|
||||||
|
<Grid>
|
||||||
|
<Grid.ColumnDefinitions>
|
||||||
|
<ColumnDefinition Width="250"/>
|
||||||
|
<ColumnDefinition Width="200"/>
|
||||||
|
</Grid.ColumnDefinitions>
|
||||||
|
<Grid.RowDefinitions>
|
||||||
|
<RowDefinition Height="Auto"/>
|
||||||
|
<RowDefinition Height="Auto"/>
|
||||||
|
<RowDefinition Height="Auto"/>
|
||||||
|
</Grid.RowDefinitions>
|
||||||
|
|
||||||
|
<TextBlock Grid.Row="0" Grid.Column="0" Text="Misura l'anticipo effettivo"
|
||||||
|
Foreground="{DynamicResource Brush.Text}" Margin="0,10" VerticalAlignment="Center"/>
|
||||||
|
<CheckBox Grid.Row="0" Grid.Column="1" x:Name="BidLeadTrackingCheckBox"
|
||||||
|
Margin="10,10" VerticalAlignment="Center" IsChecked="True"/>
|
||||||
|
|
||||||
|
<TextBlock Grid.Row="1" Grid.Column="0" Text="Proponi correzioni"
|
||||||
|
Foreground="{DynamicResource Brush.Text}" Margin="0,10" VerticalAlignment="Center"
|
||||||
|
ToolTip="Il consiglio non viene mai applicato da solo"/>
|
||||||
|
<CheckBox Grid.Row="1" Grid.Column="1" x:Name="BidLeadSuggestionsCheckBox"
|
||||||
|
Margin="10,10" VerticalAlignment="Center" IsChecked="True"/>
|
||||||
|
|
||||||
|
<TextBlock Grid.Row="2" Grid.Column="0" Text="Puntate minime per un consiglio"
|
||||||
|
Foreground="{DynamicResource Brush.Text}" Margin="0,10" VerticalAlignment="Center"/>
|
||||||
|
<TextBox Grid.Row="2" Grid.Column="1" x:Name="BidLeadMinSamplesTextBox"
|
||||||
|
Text="15" Margin="10,10"/>
|
||||||
|
</Grid>
|
||||||
|
|
||||||
|
<Border Style="{StaticResource InfoBox}" Margin="0,12,0,10">
|
||||||
|
<StackPanel>
|
||||||
|
<TextBlock Text="Misure raccolte" FontWeight="Bold"
|
||||||
|
Foreground="{DynamicResource Brush.Accent}" Margin="0,0,0,8"/>
|
||||||
|
<TextBlock x:Name="BidLeadSummaryText" TextWrapping="Wrap"
|
||||||
|
Foreground="{DynamicResource Brush.Text}" FontSize="12" LineHeight="18"
|
||||||
|
Text="Nessuna puntata registrata finora."/>
|
||||||
|
</StackPanel>
|
||||||
|
</Border>
|
||||||
|
|
||||||
|
<StackPanel Orientation="Horizontal">
|
||||||
|
<Button Content="Calcola consiglio" Style="{StaticResource SmallRoundedButton}"
|
||||||
|
Background="{DynamicResource Brush.Accent}"
|
||||||
|
Foreground="{DynamicResource Brush.TextOnAccent}"
|
||||||
|
Margin="0,0,8,0" Click="ComputeBidLeadAdviceButton_Click"/>
|
||||||
|
<Button Content="Azzera misure" Style="{StaticResource SmallRoundedButton}"
|
||||||
|
Background="{DynamicResource Brush.SurfaceAlt}"
|
||||||
|
Foreground="{DynamicResource Brush.Danger}"
|
||||||
|
Click="ClearBidLeadStatsButton_Click"/>
|
||||||
|
</StackPanel>
|
||||||
|
</StackPanel>
|
||||||
|
</Border>
|
||||||
|
|
||||||
|
<!-- SEZIONE: Aste programmate -->
|
||||||
|
<Border Style="{StaticResource CardBlock}">
|
||||||
|
<StackPanel>
|
||||||
|
<ctl:HelpHeader Title="Aste Programmate"
|
||||||
|
Margin="0,0,0,14"
|
||||||
|
Help="Un'asta che comincia fra ore non va interrogata ogni due secondi: fino all'apertura non succede nulla. La cadenza si allarga in proporzione all'attesa e torna normale poco prima dell'inizio, così il motore è comunque pronto a puntare."/>
|
||||||
|
|
||||||
|
<Grid>
|
||||||
|
<Grid.ColumnDefinitions>
|
||||||
|
<ColumnDefinition Width="250"/>
|
||||||
|
<ColumnDefinition Width="200"/>
|
||||||
|
</Grid.ColumnDefinitions>
|
||||||
|
<Grid.RowDefinitions>
|
||||||
|
<RowDefinition Height="Auto"/>
|
||||||
|
<RowDefinition Height="Auto"/>
|
||||||
|
<RowDefinition Height="Auto"/>
|
||||||
|
<RowDefinition Height="Auto"/>
|
||||||
|
</Grid.RowDefinitions>
|
||||||
|
|
||||||
|
<TextBlock Grid.Row="0" Grid.Column="0" Text="Rallenta prima dell'apertura"
|
||||||
|
Foreground="{DynamicResource Brush.Text}" Margin="0,10" VerticalAlignment="Center"/>
|
||||||
|
<CheckBox Grid.Row="0" Grid.Column="1" x:Name="ScheduledBackoffCheckBox"
|
||||||
|
Margin="10,10" VerticalAlignment="Center" IsChecked="True"/>
|
||||||
|
|
||||||
|
<TextBlock Grid.Row="1" Grid.Column="0" Text="Oltre un'ora, ogni (secondi)"
|
||||||
|
Foreground="{DynamicResource Brush.Text}" Margin="0,10" VerticalAlignment="Center"/>
|
||||||
|
<TextBox Grid.Row="1" Grid.Column="1" x:Name="ScheduledPollFarTextBox"
|
||||||
|
Text="1200" Margin="10,10"/>
|
||||||
|
|
||||||
|
<TextBlock Grid.Row="2" Grid.Column="0" Text="Fra 10 e 60 minuti, ogni (secondi)"
|
||||||
|
Foreground="{DynamicResource Brush.Text}" Margin="0,10" VerticalAlignment="Center"/>
|
||||||
|
<TextBox Grid.Row="2" Grid.Column="1" x:Name="ScheduledPollMidTextBox"
|
||||||
|
Text="120" Margin="10,10"/>
|
||||||
|
|
||||||
|
<TextBlock Grid.Row="3" Grid.Column="0" Text="Torna normale (secondi prima)"
|
||||||
|
Foreground="{DynamicResource Brush.Text}" Margin="0,10" VerticalAlignment="Center"
|
||||||
|
ToolTip="Margine di sicurezza per non perdere l'apertura"/>
|
||||||
|
<TextBox Grid.Row="3" Grid.Column="1" x:Name="ScheduledPollWakeTextBox"
|
||||||
|
Text="120" Margin="10,10"/>
|
||||||
|
</Grid>
|
||||||
|
</StackPanel>
|
||||||
|
</Border>
|
||||||
|
|
||||||
|
<!-- SEZIONE: Notifiche -->
|
||||||
|
<Border Style="{StaticResource CardBlock}">
|
||||||
|
<StackPanel>
|
||||||
|
<ctl:HelpHeader Title="Notifiche"
|
||||||
|
Margin="0,0,0,14"
|
||||||
|
Help="Avvisi di Windows per gli eventi che vale la pena vedere anche a finestra chiusa."/>
|
||||||
|
|
||||||
|
<CheckBox x:Name="NotifyOnWinCheckBox" Content="Avvisami quando vinco un'asta"
|
||||||
|
IsChecked="True" Margin="0,4"/>
|
||||||
|
<CheckBox x:Name="NotifyOnLossCheckBox"
|
||||||
|
Content="Avvisami anche quando perdo un'asta su cui avevo puntato"
|
||||||
|
Margin="0,4"/>
|
||||||
|
</StackPanel>
|
||||||
|
</Border>
|
||||||
|
|
||||||
|
<!-- SEZIONE: Cartelle dei dati -->
|
||||||
|
<Border Style="{StaticResource CardBlock}">
|
||||||
|
<StackPanel>
|
||||||
|
<ctl:HelpHeader Title="Cartelle dei Dati"
|
||||||
|
Margin="0,0,0,14"
|
||||||
|
Help="Ogni campo mostra il percorso in uso: cambialo per spostare i dati altrove, svuotalo per tornare alla posizione predefinita. Impostazioni e sessione restano sempre nella cartella dell'applicazione, perché è lì che si legge dove stanno tutti gli altri."/>
|
||||||
|
|
||||||
|
<TextBlock Text="Dati d'esercizio (aste monitorate, prodotti seguiti)"
|
||||||
|
Style="{StaticResource FieldLabel}"/>
|
||||||
|
<Grid Margin="0,0,0,12">
|
||||||
|
<Grid.ColumnDefinitions>
|
||||||
|
<ColumnDefinition Width="*"/>
|
||||||
|
<ColumnDefinition Width="Auto"/>
|
||||||
|
<ColumnDefinition Width="Auto"/>
|
||||||
|
</Grid.ColumnDefinitions>
|
||||||
|
<TextBox Grid.Column="0" x:Name="DataFolderTextBox"/>
|
||||||
|
<Button Grid.Column="1" Content="Sfoglia…" Margin="8,0,0,0"
|
||||||
|
Style="{StaticResource SmallRoundedButton}"
|
||||||
|
Background="{DynamicResource Brush.SurfaceAlt}"
|
||||||
|
Foreground="{DynamicResource Brush.Text}"
|
||||||
|
Click="BrowseDataFolderButton_Click"/>
|
||||||
|
<Button Grid.Column="2" Margin="6,0,0,0"
|
||||||
|
Style="{StaticResource IconButton}"
|
||||||
|
ToolTip="Apri questa cartella"
|
||||||
|
Click="OpenDataFolderButton_Click">
|
||||||
|
<TextBlock FontFamily="Segoe MDL2 Assets" Text=""/>
|
||||||
|
</Button>
|
||||||
|
</Grid>
|
||||||
|
|
||||||
|
<TextBlock Text="Storico ed esportazioni" Style="{StaticResource FieldLabel}"/>
|
||||||
|
<Grid Margin="0,0,0,12">
|
||||||
|
<Grid.ColumnDefinitions>
|
||||||
|
<ColumnDefinition Width="*"/>
|
||||||
|
<ColumnDefinition Width="Auto"/>
|
||||||
|
<ColumnDefinition Width="Auto"/>
|
||||||
|
</Grid.ColumnDefinitions>
|
||||||
|
<TextBox Grid.Column="0" x:Name="StatsFolderTextBox"/>
|
||||||
|
<Button Grid.Column="1" Content="Sfoglia…" Margin="8,0,0,0"
|
||||||
|
Style="{StaticResource SmallRoundedButton}"
|
||||||
|
Background="{DynamicResource Brush.SurfaceAlt}"
|
||||||
|
Foreground="{DynamicResource Brush.Text}"
|
||||||
|
Click="BrowseStatsFolderButton_Click"/>
|
||||||
|
<Button Grid.Column="2" Margin="6,0,0,0"
|
||||||
|
Style="{StaticResource IconButton}"
|
||||||
|
ToolTip="Apri questa cartella"
|
||||||
|
Click="OpenStatsFolderSettingsButton_Click">
|
||||||
|
<TextBlock FontFamily="Segoe MDL2 Assets" Text=""/>
|
||||||
|
</Button>
|
||||||
|
</Grid>
|
||||||
|
|
||||||
|
<TextBlock Text="Registri (log applicativo, riscatto puntate, dossier delle aste)"
|
||||||
|
Style="{StaticResource FieldLabel}"/>
|
||||||
|
<Grid Margin="0,0,0,12">
|
||||||
|
<Grid.ColumnDefinitions>
|
||||||
|
<ColumnDefinition Width="*"/>
|
||||||
|
<ColumnDefinition Width="Auto"/>
|
||||||
|
<ColumnDefinition Width="Auto"/>
|
||||||
|
</Grid.ColumnDefinitions>
|
||||||
|
<TextBox Grid.Column="0" x:Name="LogFolderTextBox"/>
|
||||||
|
<Button Grid.Column="1" Content="Sfoglia…" Margin="8,0,0,0"
|
||||||
|
Style="{StaticResource SmallRoundedButton}"
|
||||||
|
Background="{DynamicResource Brush.SurfaceAlt}"
|
||||||
|
Foreground="{DynamicResource Brush.Text}"
|
||||||
|
Click="BrowseLogFolderButton_Click"/>
|
||||||
|
<Button Grid.Column="2" Margin="6,0,0,0"
|
||||||
|
Style="{StaticResource IconButton}"
|
||||||
|
ToolTip="Apri questa cartella"
|
||||||
|
Click="OpenLogFolderButton_Click">
|
||||||
|
<TextBlock FontFamily="Segoe MDL2 Assets" Text=""/>
|
||||||
|
</Button>
|
||||||
|
</Grid>
|
||||||
|
|
||||||
|
<CheckBox x:Name="DetailedStatsCheckBox" IsChecked="True" Margin="0,0,0,6"
|
||||||
|
Content="Registra una scheda dettagliata per ogni asta conclusa (serie prezzi, puntate per utente, durata)"/>
|
||||||
|
</StackPanel>
|
||||||
|
</Border>
|
||||||
|
|
||||||
|
<!-- SEZIONE: Registri su file -->
|
||||||
|
<Border Style="{StaticResource CardBlock}">
|
||||||
|
<StackPanel>
|
||||||
|
<ctl:HelpHeader Title="Registri su file"
|
||||||
|
Margin="0,0,0,14"
|
||||||
|
Help="I registri si scrivono mentre l'applicazione lavora, non alla chiusura: quello che vedi nelle schede è già sul disco. Servono a rileggere cosa è successo mentre non guardavi — e il dossier delle aste è il materiale su cui si studiano le strategie, perché Bidoo non espone nulla di tutto questo a posteriori. È anche ciò che legge backtest.ps1 per dire quanto costerebbe ciascun anticipo."/>
|
||||||
|
|
||||||
|
<CheckBox x:Name="WriteAppLogCheckBox" IsChecked="True" Margin="0,4"
|
||||||
|
Content="Registro applicativo su file (un file per giorno)"/>
|
||||||
|
<CheckBox x:Name="WriteFreeBidsLogCheckBox" IsChecked="True" Margin="0,4"
|
||||||
|
Content="Registro del riscatto puntate su file"/>
|
||||||
|
<CheckBox x:Name="WriteDossiersCheckBox" IsChecked="True" Margin="0,4"
|
||||||
|
Content="Dossier per ogni asta (eventi, puntate di ogni utente, ping)"/>
|
||||||
|
<CheckBox x:Name="RawPollsCheckBox" IsChecked="True" Margin="0,4"
|
||||||
|
Content="Nel dossier registra anche ogni singola interrogazione a Bidoo"
|
||||||
|
ToolTip="Risoluzione massima: prezzo, timer e ping a ogni poll. Nella finestra critica sono quattro righe al secondo, quindi i file crescono in fretta."/>
|
||||||
|
|
||||||
|
<Grid Margin="0,10,0,0">
|
||||||
|
<Grid.ColumnDefinitions>
|
||||||
|
<ColumnDefinition Width="250"/>
|
||||||
|
<ColumnDefinition Width="200"/>
|
||||||
|
</Grid.ColumnDefinitions>
|
||||||
|
|
||||||
|
<TextBlock Grid.Column="0" Text="Cancella i registri dopo (giorni)"
|
||||||
|
Foreground="{DynamicResource Brush.Text}" Margin="0,10"
|
||||||
|
VerticalAlignment="Center"
|
||||||
|
ToolTip="0 = tienili per sempre"/>
|
||||||
|
<TextBox Grid.Column="1" x:Name="LogRetentionTextBox" Text="90" Margin="10,10"/>
|
||||||
|
</Grid>
|
||||||
|
</StackPanel>
|
||||||
|
</Border>
|
||||||
|
|
||||||
|
<!-- SEZIONE: Catalogo Esplora -->
|
||||||
|
<Border Style="{StaticResource CardBlock}">
|
||||||
|
<StackPanel>
|
||||||
|
<ctl:HelpHeader Title="Catalogo (scheda Esplora)"
|
||||||
|
Margin="0,0,0,14"
|
||||||
|
Help="L'app scende nel listato pagina per pagina, come il sito quando scorri verso il basso: cinquanta aste per richiesta, fino al tetto impostato qui. L'ordine è per scadenza, quindi le prime pagine sono le aste che chiudono prima. Alzare il tetto allarga la vista ma aggiunge una richiesta ogni cinquanta aste."/>
|
||||||
|
|
||||||
|
<Grid>
|
||||||
|
<Grid.ColumnDefinitions>
|
||||||
|
<ColumnDefinition Width="250"/>
|
||||||
|
<ColumnDefinition Width="200"/>
|
||||||
|
</Grid.ColumnDefinitions>
|
||||||
|
<Grid.RowDefinitions>
|
||||||
|
<RowDefinition Height="Auto"/>
|
||||||
|
<RowDefinition Height="Auto"/>
|
||||||
|
<RowDefinition Height="Auto"/>
|
||||||
|
</Grid.RowDefinitions>
|
||||||
|
|
||||||
|
<TextBlock Grid.Row="0" Grid.Column="0" Text="Aste da caricare per categoria"
|
||||||
|
Foreground="{DynamicResource Brush.Text}" Margin="0,10" VerticalAlignment="Center"
|
||||||
|
ToolTip="Da 20 a 5000. Una richiesta ogni 50 aste; le categorie piccole finiscono prima e si fermano da sole."/>
|
||||||
|
<TextBox Grid.Row="0" Grid.Column="1" x:Name="CatalogMaxAuctionsTextBox"
|
||||||
|
Text="500" Margin="10,10"/>
|
||||||
|
|
||||||
|
<TextBlock Grid.Row="1" Grid.Column="0" Text="Aggiorna prezzi mentre guardi"
|
||||||
|
Foreground="{DynamicResource Brush.Text}" Margin="0,10" VerticalAlignment="Center"
|
||||||
|
ToolTip="Acceso, prezzi e timer cambiano sotto gli occhi mentre stai leggendo"/>
|
||||||
|
<CheckBox Grid.Row="1" Grid.Column="1" x:Name="CatalogAutoRefreshCheckBox"
|
||||||
|
Margin="10,10" VerticalAlignment="Center"/>
|
||||||
|
|
||||||
|
<TextBlock Grid.Row="2" Grid.Column="0" Text="Riusa le aste scaricate per (secondi)"
|
||||||
|
Foreground="{DynamicResource Brush.Text}" Margin="0,10" VerticalAlignment="Center"
|
||||||
|
ToolTip="0 = nessuna cache. Evita di riscaricare tutte le pagine a ogni cambio categoria."/>
|
||||||
|
<TextBox Grid.Row="2" Grid.Column="1" x:Name="CatalogCacheSecondsTextBox"
|
||||||
|
Text="60" Margin="10,10"/>
|
||||||
|
</Grid>
|
||||||
|
</StackPanel>
|
||||||
|
</Border>
|
||||||
|
|
||||||
|
<!-- SEZIONE 3: Protezione Account -->
|
||||||
|
<Border Style="{StaticResource CardBlock}">
|
||||||
|
<StackPanel>
|
||||||
|
<ctl:HelpHeader Title="Protezione Account"
|
||||||
|
Margin="0,0,0,14"
|
||||||
|
Help="Tiene da parte un cuscinetto di puntate sull'account.
|
||||||
|
|
||||||
|
Se il valore è maggiore di zero, il sistema non punta quando le puntate residue scenderebbero sotto la soglia. Nel banner principale compare un indicatore: verde oltre le +10 dal limite, giallo entro 10, rosso al limite o sotto (puntate bloccate). Zero = nessun limite."/>
|
||||||
|
|
||||||
<Grid>
|
<Grid>
|
||||||
<Grid.ColumnDefinitions>
|
<Grid.ColumnDefinitions>
|
||||||
@@ -251,7 +650,7 @@
|
|||||||
<!-- Puntate Minime da Mantenere -->
|
<!-- Puntate Minime da Mantenere -->
|
||||||
<TextBlock Grid.Row="0" Grid.Column="0"
|
<TextBlock Grid.Row="0" Grid.Column="0"
|
||||||
Text="Puntate Minime da Mantenere"
|
Text="Puntate Minime da Mantenere"
|
||||||
Foreground="#CCCCCC"
|
Foreground="{DynamicResource Brush.Text}"
|
||||||
Margin="0,10"
|
Margin="0,10"
|
||||||
VerticalAlignment="Center"
|
VerticalAlignment="Center"
|
||||||
ToolTip="Numero minimo di puntate residue da mantenere sull'account. Se > 0, non punterà se scende sotto questa soglia (0 = nessun limite)"/>
|
ToolTip="Numero minimo di puntate residue da mantenere sull'account. Se > 0, non punterà se scende sotto questa soglia (0 = nessun limite)"/>
|
||||||
@@ -262,44 +661,23 @@
|
|||||||
</Grid>
|
</Grid>
|
||||||
|
|
||||||
<!-- Info Box -->
|
<!-- Info Box -->
|
||||||
<Border Style="{StaticResource InfoBox}" Margin="0,15,0,0">
|
|
||||||
<StackPanel>
|
|
||||||
<TextBlock Text="🛡️ Protezione Puntate"
|
|
||||||
FontWeight="Bold"
|
|
||||||
Foreground="#00D800"
|
|
||||||
Margin="0,0,0,10"/>
|
|
||||||
<TextBlock TextWrapping="Wrap"
|
|
||||||
Foreground="#CCCCCC"
|
|
||||||
FontSize="12"
|
|
||||||
LineHeight="18">
|
|
||||||
• Se impostato > 0, il sistema non punterà se le puntate residue scenderebbero sotto questa soglia.<LineBreak/>
|
|
||||||
• Utile per mantenere sempre un "cuscinetto" di sicurezza sull'account.<LineBreak/>
|
|
||||||
• Nel banner principale apparirà un indicatore colorato: <LineBreak/>
|
|
||||||
- <Bold>Verde:</Bold> Puntate abbondanti (oltre +10 dal limite)<LineBreak/>
|
|
||||||
- <Bold>Giallo:</Bold> Vicino al limite (entro 10 puntate)<LineBreak/>
|
|
||||||
- <Bold>Rosso:</Bold> Al limite o sotto (puntate bloccate)<LineBreak/>
|
|
||||||
• Valore 0 = nessun limite (comportamento default).
|
|
||||||
</TextBlock>
|
|
||||||
</StackPanel>
|
|
||||||
</Border>
|
|
||||||
</StackPanel>
|
</StackPanel>
|
||||||
</Border>
|
</Border>
|
||||||
|
|
||||||
<!-- SEZIONE 4: Limiti Log -->
|
<!-- SEZIONE 4: Limiti Log -->
|
||||||
<Border Background="#252526"
|
<Border Background="{DynamicResource Brush.Surface}"
|
||||||
BorderBrush="#3E3E42"
|
BorderBrush="{DynamicResource Brush.Border}"
|
||||||
BorderThickness="1"
|
BorderThickness="1"
|
||||||
CornerRadius="4"
|
CornerRadius="4"
|
||||||
Padding="20">
|
Padding="20">
|
||||||
<StackPanel>
|
<StackPanel>
|
||||||
<TextBlock Text="Limiti Log"
|
<ctl:HelpHeader Title="Limiti Log"
|
||||||
Style="{StaticResource SectionHeader}"/>
|
Margin="0,0,0,14"
|
||||||
|
Help="Quante righe di registro tenere in memoria. Le più vecchie vengono scartate al raggiungimento del limite.
|
||||||
<TextBlock Text="Configura il numero massimo di righe di log da mantenere in memoria per ottimizzare le performance."
|
|
||||||
Foreground="#999999"
|
Valori bassi = meno memoria, meno storico a video. Valori alti = più storico, più memoria. Consigliati: 500–1000 per asta, 1000–2000 per il registro globale.
|
||||||
FontSize="12"
|
|
||||||
TextWrapping="Wrap"
|
Riguarda solo la memoria: sul file i registri vengono scritti comunque per intero."/>
|
||||||
Margin="0,0,0,20"/>
|
|
||||||
|
|
||||||
<Grid>
|
<Grid>
|
||||||
<Grid.ColumnDefinitions>
|
<Grid.ColumnDefinitions>
|
||||||
@@ -314,7 +692,7 @@
|
|||||||
|
|
||||||
<TextBlock Grid.Row="0" Grid.Column="0"
|
<TextBlock Grid.Row="0" Grid.Column="0"
|
||||||
Text="Max Righe Log per Asta"
|
Text="Max Righe Log per Asta"
|
||||||
Foreground="#CCCCCC"
|
Foreground="{DynamicResource Brush.Text}"
|
||||||
Margin="0,10"
|
Margin="0,10"
|
||||||
VerticalAlignment="Center"
|
VerticalAlignment="Center"
|
||||||
ToolTip="Numero massimo di righe di log da mantenere per ogni singola asta (raccomandato: 500-1000)"/>
|
ToolTip="Numero massimo di righe di log da mantenere per ogni singola asta (raccomandato: 500-1000)"/>
|
||||||
@@ -325,7 +703,7 @@
|
|||||||
|
|
||||||
<TextBlock Grid.Row="1" Grid.Column="0"
|
<TextBlock Grid.Row="1" Grid.Column="0"
|
||||||
Text="Max Righe Log Globale"
|
Text="Max Righe Log Globale"
|
||||||
Foreground="#CCCCCC"
|
Foreground="{DynamicResource Brush.Text}"
|
||||||
Margin="0,10"
|
Margin="0,10"
|
||||||
VerticalAlignment="Center"
|
VerticalAlignment="Center"
|
||||||
ToolTip="Numero massimo di righe di log da mantenere nel log globale dell'applicazione (raccomandato: 1000-2000)"/>
|
ToolTip="Numero massimo di righe di log da mantenere nel log globale dell'applicazione (raccomandato: 1000-2000)"/>
|
||||||
@@ -337,7 +715,7 @@
|
|||||||
<!-- 📊 NUOVO: Max Storia Puntate -->
|
<!-- 📊 NUOVO: Max Storia Puntate -->
|
||||||
<TextBlock Grid.Row="2" Grid.Column="0"
|
<TextBlock Grid.Row="2" Grid.Column="0"
|
||||||
Text="Max Puntate da Visualizzare"
|
Text="Max Puntate da Visualizzare"
|
||||||
Foreground="#CCCCCC"
|
Foreground="{DynamicResource Brush.Text}"
|
||||||
Margin="0,10"
|
Margin="0,10"
|
||||||
VerticalAlignment="Center"
|
VerticalAlignment="Center"
|
||||||
ToolTip="Numero massimo di puntate da mostrare nella scheda Storia Puntate (raccomandato: 20-50, 0 = tutte)"/>
|
ToolTip="Numero massimo di puntate da mostrare nella scheda Storia Puntate (raccomandato: 20-50, 0 = tutte)"/>
|
||||||
@@ -348,42 +726,28 @@
|
|||||||
</Grid>
|
</Grid>
|
||||||
|
|
||||||
<!-- Info Box -->
|
<!-- Info Box -->
|
||||||
<Border Style="{StaticResource InfoBox}" Margin="0,15,0,0">
|
|
||||||
<StackPanel>
|
|
||||||
<TextBlock Text="Informazioni"
|
|
||||||
FontWeight="Bold"
|
|
||||||
Foreground="#007ACC"
|
|
||||||
Margin="0,0,0,10"/>
|
|
||||||
<TextBlock TextWrapping="Wrap"
|
|
||||||
Foreground="#CCCCCC"
|
|
||||||
FontSize="12"
|
|
||||||
LineHeight="18">
|
|
||||||
• I log più vecchi verranno automaticamente rimossi quando si raggiunge il limite.<LineBreak/>
|
|
||||||
• Valori più bassi = meno memoria utilizzata, ma meno storico disponibile.<LineBreak/>
|
|
||||||
• Valori più alti = più storico disponibile, ma maggiore uso di memoria.<LineBreak/>
|
|
||||||
• Valori consigliati: 500-1000 per asta, 1000-2000 per log globale.
|
|
||||||
</TextBlock>
|
|
||||||
</StackPanel>
|
|
||||||
</Border>
|
|
||||||
</StackPanel>
|
</StackPanel>
|
||||||
</Border>
|
</Border>
|
||||||
|
|
||||||
<!-- SEZIONE 5: Livello di Dettaglio Log -->
|
<!-- SEZIONE 5: Livello di Dettaglio Log -->
|
||||||
<Border Background="#252526"
|
<Border Background="{DynamicResource Brush.Surface}"
|
||||||
BorderBrush="#3E3E42"
|
BorderBrush="{DynamicResource Brush.Border}"
|
||||||
BorderThickness="1"
|
BorderThickness="1"
|
||||||
CornerRadius="4"
|
CornerRadius="4"
|
||||||
Padding="20"
|
Padding="20"
|
||||||
Margin="0,20,0,0">
|
Margin="0,20,0,0">
|
||||||
<StackPanel>
|
<StackPanel>
|
||||||
<TextBlock Text="Livello di Dettaglio Log"
|
<ctl:HelpHeader Title="Livello di Dettaglio Log"
|
||||||
Style="{StaticResource SectionHeader}"/>
|
Margin="0,0,0,14"
|
||||||
|
Help="Livello minimo dei messaggi mostrati a video. Il file di registro li riceve tutti comunque, perché serve proprio quando il messaggio utile è uno di quelli nascosti.
|
||||||
<TextBlock Text="Configura il livello minimo dei messaggi da visualizzare nel log. Livelli più bassi mostrano solo messaggi critici, livelli più alti mostrano tutti i dettagli (utile per debug)."
|
|
||||||
Foreground="#999999"
|
• Solo Errori: in esercizio, per vedere solo i problemi critici.
|
||||||
FontSize="12"
|
• Normale: uso giornaliero. Errori e avvisi importanti.
|
||||||
TextWrapping="Wrap"
|
• Informativo: segue le operazioni principali (aste aggiunte, puntate).
|
||||||
Margin="0,0,0,20"/>
|
• Debug: parametri delle chiamate e valori interni.
|
||||||
|
• Trace: ogni singola chiamata. Molto verboso.
|
||||||
|
|
||||||
|
Colori: rosso = errori, arancione = avvisi, blu = informazioni, verde = operazioni riuscite, magenta = debug, grigio = trace."/>
|
||||||
|
|
||||||
<!-- Radio Buttons per livello log -->
|
<!-- Radio Buttons per livello log -->
|
||||||
<StackPanel>
|
<StackPanel>
|
||||||
@@ -420,76 +784,6 @@
|
|||||||
</StackPanel>
|
</StackPanel>
|
||||||
|
|
||||||
<!-- Info Box -->
|
<!-- Info Box -->
|
||||||
<Border Style="{StaticResource InfoBox}" Margin="0,15,0,0">
|
|
||||||
<StackPanel>
|
|
||||||
<TextBlock Text="🔍 Guida alla Scelta"
|
|
||||||
FontWeight="Bold"
|
|
||||||
Foreground="#FFB700"
|
|
||||||
Margin="0,0,0,10"/>
|
|
||||||
<TextBlock Foreground="#CCCCCC"
|
|
||||||
FontSize="12"
|
|
||||||
TextWrapping="Wrap"
|
|
||||||
Margin="0,0,0,5">
|
|
||||||
<Run>Solo Errori: Usa in produzione per vedere solo problemi critici.</Run>
|
|
||||||
</TextBlock>
|
|
||||||
<TextBlock Foreground="#CCCCCC"
|
|
||||||
FontSize="12"
|
|
||||||
TextWrapping="Wrap"
|
|
||||||
Margin="0,0,0,5">
|
|
||||||
<Run>Normale: Raccomandato per uso giornaliero. Mostra errori e avvisi importanti.</Run>
|
|
||||||
</TextBlock>
|
|
||||||
<TextBlock Foreground="#CCCCCC"
|
|
||||||
FontSize="12"
|
|
||||||
TextWrapping="Wrap"
|
|
||||||
Margin="0,0,0,5">
|
|
||||||
<Run>Informativo: Utile per seguire le operazioni principali (aggiunte aste, puntate).</Run>
|
|
||||||
</TextBlock>
|
|
||||||
<TextBlock Foreground="#CCCCCC"
|
|
||||||
FontSize="12"
|
|
||||||
TextWrapping="Wrap"
|
|
||||||
Margin="0,0,0,5">
|
|
||||||
<Run>Debug: Per sviluppo. Mostra parametri chiamate API e valori interni.</Run>
|
|
||||||
</TextBlock>
|
|
||||||
<TextBlock Foreground="#CCCCCC"
|
|
||||||
FontSize="12"
|
|
||||||
TextWrapping="Wrap"
|
|
||||||
Margin="0,0,0,10">
|
|
||||||
<Run>Trace: Debug avanzato. Mostra ogni singola chiamata (molto verboso).</Run>
|
|
||||||
</TextBlock>
|
|
||||||
|
|
||||||
<TextBlock Foreground="#CCCCCC"
|
|
||||||
FontSize="12"
|
|
||||||
FontWeight="Bold"
|
|
||||||
Margin="0,5,0,5">Legenda colori log:</TextBlock>
|
|
||||||
|
|
||||||
<StackPanel>
|
|
||||||
<TextBlock FontSize="11" Margin="0,2">
|
|
||||||
<Run Foreground="#E81123">■ ROSSO</Run>
|
|
||||||
<Run Foreground="#CCCCCC"> = Errori critici</Run>
|
|
||||||
</TextBlock>
|
|
||||||
<TextBlock FontSize="11" Margin="0,2">
|
|
||||||
<Run Foreground="#FFB700">■ ARANCIONE</Run>
|
|
||||||
<Run Foreground="#CCCCCC"> = Avvisi</Run>
|
|
||||||
</TextBlock>
|
|
||||||
<TextBlock FontSize="11" Margin="0,2">
|
|
||||||
<Run Foreground="#64B4FF">■ BLU</Run>
|
|
||||||
<Run Foreground="#CCCCCC"> = Informazioni</Run>
|
|
||||||
</TextBlock>
|
|
||||||
<TextBlock FontSize="11" Margin="0,2">
|
|
||||||
<Run Foreground="#00D800">■ VERDE</Run>
|
|
||||||
<Run Foreground="#CCCCCC"> = Operazioni riuscite</Run>
|
|
||||||
</TextBlock>
|
|
||||||
<TextBlock FontSize="11" Margin="0,2">
|
|
||||||
<Run Foreground="#FF8CFF">■ MAGENTA</Run>
|
|
||||||
<Run Foreground="#CCCCCC"> = Debug</Run>
|
|
||||||
</TextBlock>
|
|
||||||
<TextBlock FontSize="11" Margin="0,2">
|
|
||||||
<Run Foreground="#A0A0A0">■ GRIGIO</Run>
|
|
||||||
<Run Foreground="#CCCCCC"> = Trace</Run>
|
|
||||||
</TextBlock>
|
|
||||||
</StackPanel>
|
|
||||||
</StackPanel>
|
|
||||||
</Border>
|
|
||||||
</StackPanel>
|
</StackPanel>
|
||||||
</Border>
|
</Border>
|
||||||
</StackPanel>
|
</StackPanel>
|
||||||
@@ -497,20 +791,20 @@
|
|||||||
|
|
||||||
<!-- Fixed Bottom Bar with Save/Cancel -->
|
<!-- Fixed Bottom Bar with Save/Cancel -->
|
||||||
<Border Grid.Row="1"
|
<Border Grid.Row="1"
|
||||||
Background="#2D2D30"
|
Background="{DynamicResource Brush.SurfaceAlt}"
|
||||||
BorderBrush="#3E3E42"
|
BorderBrush="{DynamicResource Brush.Border}"
|
||||||
BorderThickness="0,1,0,0"
|
BorderThickness="0,1,0,0"
|
||||||
Padding="30,15">
|
Padding="30,15">
|
||||||
<StackPanel Orientation="Horizontal" HorizontalAlignment="Right">
|
<StackPanel Orientation="Horizontal" HorizontalAlignment="Right">
|
||||||
<Button Content="Salva"
|
<Button Content="Salva"
|
||||||
Background="#00D800"
|
Background="{DynamicResource Brush.Success}"
|
||||||
Style="{StaticResource ModernButton}"
|
Style="{StaticResource ModernButton}"
|
||||||
Margin="0,0,10,0"
|
Margin="0,0,10,0"
|
||||||
Padding="40,10"
|
Padding="40,10"
|
||||||
Click="SaveAllSettings_Click"/>
|
Click="SaveAllSettings_Click" Foreground="{DynamicResource Brush.TextOnAccent}"/>
|
||||||
|
|
||||||
<Button Content="Annulla"
|
<Button Content="Annulla"
|
||||||
Background="#3E3E42"
|
Background="{DynamicResource Brush.SurfaceAlt}"
|
||||||
Style="{StaticResource ModernButton}"
|
Style="{StaticResource ModernButton}"
|
||||||
Padding="40,10"
|
Padding="40,10"
|
||||||
Click="CancelAllSettings_Click"/>
|
Click="CancelAllSettings_Click"/>
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
using System.Linq;
|
||||||
using System.Windows;
|
using System.Windows;
|
||||||
using System.Windows.Controls;
|
using System.Windows.Controls;
|
||||||
|
|
||||||
@@ -8,23 +9,252 @@ namespace AutoBidder.Controls
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
public partial class SettingsControl : UserControl
|
public partial class SettingsControl : UserControl
|
||||||
{
|
{
|
||||||
|
/// <summary>Evita che i Checked scattino mentre si sincronizza la UI.</summary>
|
||||||
|
private bool _suppressThemeEvents;
|
||||||
|
|
||||||
public SettingsControl()
|
public SettingsControl()
|
||||||
{
|
{
|
||||||
InitializeComponent();
|
InitializeComponent();
|
||||||
|
Loaded += (_, _) => SyncThemeSelection();
|
||||||
}
|
}
|
||||||
|
|
||||||
// Non servono proprietà wrapper - MainWindow.xaml.cs accede direttamente ai controlli tramite:
|
/// <summary>
|
||||||
|
/// Allinea i radio button al tema attualmente applicato.
|
||||||
|
/// </summary>
|
||||||
|
public void SyncThemeSelection()
|
||||||
|
{
|
||||||
|
_suppressThemeEvents = true;
|
||||||
|
try
|
||||||
|
{
|
||||||
|
bool dark = Utilities.ThemeManager.IsDark;
|
||||||
|
ThemeDarkRadio.IsChecked = dark;
|
||||||
|
ThemeLightRadio.IsChecked = !dark;
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
_suppressThemeEvents = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void ThemeDarkRadio_Checked(object sender, RoutedEventArgs e)
|
||||||
|
{
|
||||||
|
if (_suppressThemeEvents) return;
|
||||||
|
Utilities.ThemeManager.SetAndSave(true);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void ThemeLightRadio_Checked(object sender, RoutedEventArgs e)
|
||||||
|
{
|
||||||
|
if (_suppressThemeEvents) return;
|
||||||
|
Utilities.ThemeManager.SetAndSave(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Non servono propriet� wrapper - MainWindow.xaml.cs accede direttamente ai controlli tramite:
|
||||||
// Settings.DefaultBidBeforeDeadlineMsTextBox (definito nel XAML con x:Name)
|
// Settings.DefaultBidBeforeDeadlineMsTextBox (definito nel XAML con x:Name)
|
||||||
// Settings.MaxLogLinesPerAuctionTextBox (definito nel XAML con x:Name)
|
// Settings.MaxLogLinesPerAuctionTextBox (definito nel XAML con x:Name)
|
||||||
// etc.
|
// etc.
|
||||||
|
|
||||||
// Proprietà per limiti log
|
// Propriet� per limiti log
|
||||||
public TextBox MaxLogLinesPerAuction => MaxLogLinesPerAuctionTextBox;
|
public TextBox MaxLogLinesPerAuction => MaxLogLinesPerAuctionTextBox;
|
||||||
public TextBox MaxGlobalLogLines => MaxGlobalLogLinesTextBox;
|
public TextBox MaxGlobalLogLines => MaxGlobalLogLinesTextBox;
|
||||||
|
|
||||||
// ?? NUOVO: Proprietà per limite storia puntate
|
// ?? NUOVO: Propriet� per limite storia puntate
|
||||||
public TextBox MaxBidHistoryEntries => MaxBidHistoryEntriesTextBox;
|
public TextBox MaxBidHistoryEntries => MaxBidHistoryEntriesTextBox;
|
||||||
|
|
||||||
|
// ===== ANTICIPO, CARTELLE, ESPORTAZIONE =====
|
||||||
|
|
||||||
|
private void ComputeBidLeadAdviceButton_Click(object sender, RoutedEventArgs e)
|
||||||
|
=> RaiseEvent(new RoutedEventArgs(ComputeBidLeadAdviceClickedEvent, this));
|
||||||
|
|
||||||
|
private void ClearBidLeadStatsButton_Click(object sender, RoutedEventArgs e)
|
||||||
|
=> RaiseEvent(new RoutedEventArgs(ClearBidLeadStatsClickedEvent, this));
|
||||||
|
|
||||||
|
private void BrowseDataFolderButton_Click(object sender, RoutedEventArgs e)
|
||||||
|
=> RaiseEvent(new RoutedEventArgs(BrowseDataFolderClickedEvent, this));
|
||||||
|
|
||||||
|
private void BrowseStatsFolderButton_Click(object sender, RoutedEventArgs e)
|
||||||
|
=> RaiseEvent(new RoutedEventArgs(BrowseStatsFolderClickedEvent, this));
|
||||||
|
|
||||||
|
private void OpenDataFolderButton_Click(object sender, RoutedEventArgs e)
|
||||||
|
=> RaiseEvent(new RoutedEventArgs(OpenDataFolderClickedEvent, this));
|
||||||
|
|
||||||
|
private void OpenStatsFolderSettingsButton_Click(object sender, RoutedEventArgs e)
|
||||||
|
=> RaiseEvent(new RoutedEventArgs(OpenStatsFolderClickedEvent, this));
|
||||||
|
|
||||||
|
private void BrowseLogFolderButton_Click(object sender, RoutedEventArgs e)
|
||||||
|
=> RaiseEvent(new RoutedEventArgs(BrowseLogFolderClickedEvent, this));
|
||||||
|
|
||||||
|
private void OpenLogFolderButton_Click(object sender, RoutedEventArgs e)
|
||||||
|
=> RaiseEvent(new RoutedEventArgs(OpenLogFolderClickedEvent, this));
|
||||||
|
|
||||||
|
/// <summary>Riepilogo delle misure sull'anticipo, mostrato nel riquadro informativo.</summary>
|
||||||
|
public void SetBidLeadSummary(string text) => BidLeadSummaryText.Text = text;
|
||||||
|
|
||||||
|
public static readonly RoutedEvent ComputeBidLeadAdviceClickedEvent = EventManager.RegisterRoutedEvent(
|
||||||
|
"ComputeBidLeadAdviceClicked", RoutingStrategy.Bubble, typeof(RoutedEventHandler), typeof(SettingsControl));
|
||||||
|
|
||||||
|
public static readonly RoutedEvent ClearBidLeadStatsClickedEvent = EventManager.RegisterRoutedEvent(
|
||||||
|
"ClearBidLeadStatsClicked", RoutingStrategy.Bubble, typeof(RoutedEventHandler), typeof(SettingsControl));
|
||||||
|
|
||||||
|
public static readonly RoutedEvent BrowseDataFolderClickedEvent = EventManager.RegisterRoutedEvent(
|
||||||
|
"BrowseDataFolderClicked", RoutingStrategy.Bubble, typeof(RoutedEventHandler), typeof(SettingsControl));
|
||||||
|
|
||||||
|
public static readonly RoutedEvent BrowseStatsFolderClickedEvent = EventManager.RegisterRoutedEvent(
|
||||||
|
"BrowseStatsFolderClicked", RoutingStrategy.Bubble, typeof(RoutedEventHandler), typeof(SettingsControl));
|
||||||
|
|
||||||
|
public static readonly RoutedEvent OpenDataFolderClickedEvent = EventManager.RegisterRoutedEvent(
|
||||||
|
"OpenDataFolderClicked", RoutingStrategy.Bubble, typeof(RoutedEventHandler), typeof(SettingsControl));
|
||||||
|
|
||||||
|
public static readonly RoutedEvent OpenStatsFolderClickedEvent = EventManager.RegisterRoutedEvent(
|
||||||
|
"OpenStatsFolderClicked", RoutingStrategy.Bubble, typeof(RoutedEventHandler), typeof(SettingsControl));
|
||||||
|
|
||||||
|
public static readonly RoutedEvent BrowseLogFolderClickedEvent = EventManager.RegisterRoutedEvent(
|
||||||
|
"BrowseLogFolderClicked", RoutingStrategy.Bubble, typeof(RoutedEventHandler), typeof(SettingsControl));
|
||||||
|
|
||||||
|
public static readonly RoutedEvent OpenLogFolderClickedEvent = EventManager.RegisterRoutedEvent(
|
||||||
|
"OpenLogFolderClicked", RoutingStrategy.Bubble, typeof(RoutedEventHandler), typeof(SettingsControl));
|
||||||
|
|
||||||
|
public event RoutedEventHandler ComputeBidLeadAdviceClicked
|
||||||
|
{
|
||||||
|
add { AddHandler(ComputeBidLeadAdviceClickedEvent, value); }
|
||||||
|
remove { RemoveHandler(ComputeBidLeadAdviceClickedEvent, value); }
|
||||||
|
}
|
||||||
|
|
||||||
|
public event RoutedEventHandler ClearBidLeadStatsClicked
|
||||||
|
{
|
||||||
|
add { AddHandler(ClearBidLeadStatsClickedEvent, value); }
|
||||||
|
remove { RemoveHandler(ClearBidLeadStatsClickedEvent, value); }
|
||||||
|
}
|
||||||
|
|
||||||
|
public event RoutedEventHandler BrowseDataFolderClicked
|
||||||
|
{
|
||||||
|
add { AddHandler(BrowseDataFolderClickedEvent, value); }
|
||||||
|
remove { RemoveHandler(BrowseDataFolderClickedEvent, value); }
|
||||||
|
}
|
||||||
|
|
||||||
|
public event RoutedEventHandler BrowseStatsFolderClicked
|
||||||
|
{
|
||||||
|
add { AddHandler(BrowseStatsFolderClickedEvent, value); }
|
||||||
|
remove { RemoveHandler(BrowseStatsFolderClickedEvent, value); }
|
||||||
|
}
|
||||||
|
|
||||||
|
public event RoutedEventHandler OpenDataFolderClicked
|
||||||
|
{
|
||||||
|
add { AddHandler(OpenDataFolderClickedEvent, value); }
|
||||||
|
remove { RemoveHandler(OpenDataFolderClickedEvent, value); }
|
||||||
|
}
|
||||||
|
|
||||||
|
public event RoutedEventHandler OpenStatsFolderClicked
|
||||||
|
{
|
||||||
|
add { AddHandler(OpenStatsFolderClickedEvent, value); }
|
||||||
|
remove { RemoveHandler(OpenStatsFolderClickedEvent, value); }
|
||||||
|
}
|
||||||
|
|
||||||
|
public event RoutedEventHandler BrowseLogFolderClicked
|
||||||
|
{
|
||||||
|
add { AddHandler(BrowseLogFolderClickedEvent, value); }
|
||||||
|
remove { RemoveHandler(BrowseLogFolderClickedEvent, value); }
|
||||||
|
}
|
||||||
|
|
||||||
|
public event RoutedEventHandler OpenLogFolderClicked
|
||||||
|
{
|
||||||
|
add { AddHandler(OpenLogFolderClickedEvent, value); }
|
||||||
|
remove { RemoveHandler(OpenLogFolderClickedEvent, value); }
|
||||||
|
}
|
||||||
|
|
||||||
|
// ===== AGGIUNTA AUTOMATICA =====
|
||||||
|
//
|
||||||
|
// L'elenco dei prodotti (stellina e limiti su misura) sta nella scheda Prodotti:
|
||||||
|
// qui restano solo gli interruttori che valgono per tutti.
|
||||||
|
|
||||||
|
/// <summary>Stato con cui entrano le aste aggiunte automaticamente ("Watch"/"Stopped"/"Active").</summary>
|
||||||
|
public string AutoAddState
|
||||||
|
{
|
||||||
|
get => (AutoAddStateCombo.SelectedItem as ComboBoxItem)?.Tag as string ?? "Watch";
|
||||||
|
set
|
||||||
|
{
|
||||||
|
foreach (var item in AutoAddStateCombo.Items.OfType<ComboBoxItem>())
|
||||||
|
{
|
||||||
|
if ((item.Tag as string) == value)
|
||||||
|
{
|
||||||
|
AutoAddStateCombo.SelectedItem = item;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
AutoAddStateCombo.SelectedIndex = 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ===== SESSIONE / AUTENTICAZIONE =====
|
||||||
|
//
|
||||||
|
// Non c'è più un campo per incollare il cookie a mano: l'accesso si fa dal browser
|
||||||
|
// integrato e vale per tutta l'applicazione. Un secondo modo significava due
|
||||||
|
// sessioni possibili e diverse — quella digitata e quella del browser — con la
|
||||||
|
// domanda "quale sta usando adesso?" senza una risposta visibile.
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Aggiorna il riquadro di stato della sessione.
|
||||||
|
/// </summary>
|
||||||
|
public void SetSessionStatus(bool connected, string? username = null,
|
||||||
|
int remainingBids = 0, double shopCredit = 0)
|
||||||
|
{
|
||||||
|
if (connected && !string.IsNullOrEmpty(username))
|
||||||
|
{
|
||||||
|
SessionStatusText.Text = $"Connesso come {username}";
|
||||||
|
SessionStatusText.SetResourceReference(ForegroundProperty, "Brush.Success");
|
||||||
|
SessionStatusDetail.Text =
|
||||||
|
$"Puntate residue: {remainingBids} • Credito Shop: EUR {shopCredit:F2}";
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
SessionStatusText.Text = "Non connesso";
|
||||||
|
SessionStatusText.SetResourceReference(ForegroundProperty, "Brush.Danger");
|
||||||
|
SessionStatusDetail.Text = "Nessuna sessione attiva.";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void ConnectSessionButton_Click(object sender, RoutedEventArgs e)
|
||||||
|
{
|
||||||
|
RaiseEvent(new RoutedEventArgs(ConnectSessionClickedEvent, this));
|
||||||
|
}
|
||||||
|
|
||||||
|
private void DisconnectSessionButton_Click(object sender, RoutedEventArgs e)
|
||||||
|
{
|
||||||
|
RaiseEvent(new RoutedEventArgs(DisconnectSessionClickedEvent, this));
|
||||||
|
}
|
||||||
|
|
||||||
|
private void OpenBrowserLoginButton_Click(object sender, RoutedEventArgs e)
|
||||||
|
{
|
||||||
|
RaiseEvent(new RoutedEventArgs(OpenBrowserLoginClickedEvent, this));
|
||||||
|
}
|
||||||
|
|
||||||
|
public static readonly RoutedEvent ConnectSessionClickedEvent = EventManager.RegisterRoutedEvent(
|
||||||
|
"ConnectSessionClicked", RoutingStrategy.Bubble, typeof(RoutedEventHandler), typeof(SettingsControl));
|
||||||
|
|
||||||
|
public static readonly RoutedEvent DisconnectSessionClickedEvent = EventManager.RegisterRoutedEvent(
|
||||||
|
"DisconnectSessionClicked", RoutingStrategy.Bubble, typeof(RoutedEventHandler), typeof(SettingsControl));
|
||||||
|
|
||||||
|
public static readonly RoutedEvent OpenBrowserLoginClickedEvent = EventManager.RegisterRoutedEvent(
|
||||||
|
"OpenBrowserLoginClicked", RoutingStrategy.Bubble, typeof(RoutedEventHandler), typeof(SettingsControl));
|
||||||
|
|
||||||
|
public event RoutedEventHandler ConnectSessionClicked
|
||||||
|
{
|
||||||
|
add { AddHandler(ConnectSessionClickedEvent, value); }
|
||||||
|
remove { RemoveHandler(ConnectSessionClickedEvent, value); }
|
||||||
|
}
|
||||||
|
|
||||||
|
public event RoutedEventHandler DisconnectSessionClicked
|
||||||
|
{
|
||||||
|
add { AddHandler(DisconnectSessionClickedEvent, value); }
|
||||||
|
remove { RemoveHandler(DisconnectSessionClickedEvent, value); }
|
||||||
|
}
|
||||||
|
|
||||||
|
public event RoutedEventHandler OpenBrowserLoginClicked
|
||||||
|
{
|
||||||
|
add { AddHandler(OpenBrowserLoginClickedEvent, value); }
|
||||||
|
remove { RemoveHandler(OpenBrowserLoginClickedEvent, value); }
|
||||||
|
}
|
||||||
|
|
||||||
private void SaveDefaultsButton_Click(object sender, RoutedEventArgs e)
|
private void SaveDefaultsButton_Click(object sender, RoutedEventArgs e)
|
||||||
{
|
{
|
||||||
RaiseEvent(new RoutedEventArgs(SaveDefaultsClickedEvent, this));
|
RaiseEvent(new RoutedEventArgs(SaveDefaultsClickedEvent, this));
|
||||||
|
|||||||
@@ -1,135 +0,0 @@
|
|||||||
<UserControl x:Class="AutoBidder.Controls.StatisticsControl"
|
|
||||||
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
|
|
||||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
|
||||||
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
|
|
||||||
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
|
|
||||||
mc:Ignorable="d"
|
|
||||||
d:DesignHeight="800" d:DesignWidth="1200"
|
|
||||||
Background="#1E1E1E">
|
|
||||||
|
|
||||||
<UserControl.Resources>
|
|
||||||
<Style x:Key="RoundedButton" TargetType="Button">
|
|
||||||
<Setter Property="Template">
|
|
||||||
<Setter.Value>
|
|
||||||
<ControlTemplate TargetType="Button">
|
|
||||||
<Border Background="{TemplateBinding Background}"
|
|
||||||
CornerRadius="8"
|
|
||||||
Padding="{TemplateBinding Padding}">
|
|
||||||
<ContentPresenter HorizontalAlignment="Center" VerticalAlignment="Center"/>
|
|
||||||
</Border>
|
|
||||||
</ControlTemplate>
|
|
||||||
</Setter.Value>
|
|
||||||
</Setter>
|
|
||||||
<Setter Property="Foreground" Value="White"/>
|
|
||||||
<Setter Property="FontWeight" Value="Bold"/>
|
|
||||||
<Setter Property="BorderThickness" Value="0"/>
|
|
||||||
<Setter Property="Cursor" Value="Hand"/>
|
|
||||||
<Setter Property="Padding" Value="15,10"/>
|
|
||||||
</Style>
|
|
||||||
</UserControl.Resources>
|
|
||||||
|
|
||||||
<Grid>
|
|
||||||
<Grid.RowDefinitions>
|
|
||||||
<RowDefinition Height="Auto"/>
|
|
||||||
<RowDefinition Height="*"/>
|
|
||||||
<RowDefinition Height="Auto"/>
|
|
||||||
</Grid.RowDefinitions>
|
|
||||||
|
|
||||||
<!-- Header -->
|
|
||||||
<Border Grid.Row="0" Background="#2D2D30" Padding="15" BorderBrush="#3E3E42" BorderThickness="0,0,0,1">
|
|
||||||
<Grid>
|
|
||||||
<TextBlock Text="📊 Dati Statistici - Analisi Aste Chiuse"
|
|
||||||
Foreground="#00D800"
|
|
||||||
FontSize="16"
|
|
||||||
FontWeight="Bold"
|
|
||||||
VerticalAlignment="Center"/>
|
|
||||||
|
|
||||||
<Button x:Name="LoadClosedAuctionsButton"
|
|
||||||
Content="🔄 Carica Statistiche"
|
|
||||||
HorizontalAlignment="Right"
|
|
||||||
Background="#007ACC"
|
|
||||||
Style="{StaticResource RoundedButton}"
|
|
||||||
Click="LoadClosedAuctionsButton_Click"/>
|
|
||||||
</Grid>
|
|
||||||
</Border>
|
|
||||||
|
|
||||||
<!-- DataGrid Statistiche -->
|
|
||||||
<DataGrid Grid.Row="1"
|
|
||||||
x:Name="StatsDataGrid"
|
|
||||||
AutoGenerateColumns="False"
|
|
||||||
IsReadOnly="True"
|
|
||||||
Background="#1E1E1E"
|
|
||||||
Foreground="#CCCCCC"
|
|
||||||
RowBackground="#1E1E1E"
|
|
||||||
AlternatingRowBackground="#252526"
|
|
||||||
GridLinesVisibility="Horizontal"
|
|
||||||
HeadersVisibility="Column"
|
|
||||||
BorderThickness="0"
|
|
||||||
Margin="15">
|
|
||||||
<DataGrid.ColumnHeaderStyle>
|
|
||||||
<Style TargetType="DataGridColumnHeader">
|
|
||||||
<Setter Property="Background" Value="#2D2D30"/>
|
|
||||||
<Setter Property="Foreground" Value="#CCCCCC"/>
|
|
||||||
<Setter Property="FontWeight" Value="Bold"/>
|
|
||||||
<Setter Property="Padding" Value="10,8"/>
|
|
||||||
<Setter Property="BorderThickness" Value="0,0,1,1"/>
|
|
||||||
<Setter Property="BorderBrush" Value="#3E3E42"/>
|
|
||||||
</Style>
|
|
||||||
</DataGrid.ColumnHeaderStyle>
|
|
||||||
<DataGrid.CellStyle>
|
|
||||||
<Style TargetType="DataGridCell">
|
|
||||||
<Setter Property="BorderThickness" Value="0"/>
|
|
||||||
<Setter Property="Padding" Value="10,5"/>
|
|
||||||
<Setter Property="Background" Value="Transparent"/>
|
|
||||||
<Setter Property="Foreground" Value="#CCCCCC"/>
|
|
||||||
</Style>
|
|
||||||
</DataGrid.CellStyle>
|
|
||||||
<DataGrid.Columns>
|
|
||||||
<DataGridTextColumn Header="Prodotto" Binding="{Binding ProductName}" Width="3*"/>
|
|
||||||
<DataGridTextColumn Header="Prezzo Medio" Binding="{Binding AverageFinalPrice, StringFormat=€{0:F2}}" Width="120"/>
|
|
||||||
<DataGridTextColumn Header="Click Medi" Binding="{Binding AverageBidsUsed, StringFormat={}{0:F0}}" Width="100"/>
|
|
||||||
<DataGridTextColumn Header="Vincitore Frequente" Binding="{Binding Winner}" Width="150"/>
|
|
||||||
<DataGridTextColumn Header="# Aste" Binding="{Binding Count}" Width="80"/>
|
|
||||||
</DataGrid.Columns>
|
|
||||||
</DataGrid>
|
|
||||||
|
|
||||||
<!-- Footer: Status -->
|
|
||||||
<Border Grid.Row="2"
|
|
||||||
Background="#252526"
|
|
||||||
Padding="15"
|
|
||||||
BorderBrush="#3E3E42"
|
|
||||||
BorderThickness="0,1,0,0">
|
|
||||||
<Grid>
|
|
||||||
<Grid.ColumnDefinitions>
|
|
||||||
<ColumnDefinition Width="*"/>
|
|
||||||
<ColumnDefinition Width="Auto"/>
|
|
||||||
</Grid.ColumnDefinitions>
|
|
||||||
|
|
||||||
<StackPanel Grid.Column="0">
|
|
||||||
<TextBlock x:Name="StatsStatusText"
|
|
||||||
Text="Pronto per caricare statistiche"
|
|
||||||
FontSize="13"
|
|
||||||
Foreground="#CCCCCC"
|
|
||||||
VerticalAlignment="Center"/>
|
|
||||||
|
|
||||||
<TextBlock x:Name="ExportProgressText"
|
|
||||||
Text=""
|
|
||||||
FontSize="11"
|
|
||||||
Foreground="#999999"
|
|
||||||
Margin="0,5,0,0"
|
|
||||||
Visibility="Collapsed"/>
|
|
||||||
</StackPanel>
|
|
||||||
|
|
||||||
<ProgressBar Grid.Column="1"
|
|
||||||
x:Name="ExportProgressBar"
|
|
||||||
Width="200"
|
|
||||||
Height="20"
|
|
||||||
IsIndeterminate="True"
|
|
||||||
Foreground="#007ACC"
|
|
||||||
Background="#1E1E1E"
|
|
||||||
BorderBrush="#3E3E42"
|
|
||||||
Visibility="Collapsed"/>
|
|
||||||
</Grid>
|
|
||||||
</Border>
|
|
||||||
</Grid>
|
|
||||||
</UserControl>
|
|
||||||
@@ -1,31 +0,0 @@
|
|||||||
using System.Windows;
|
|
||||||
using System.Windows.Controls;
|
|
||||||
|
|
||||||
namespace AutoBidder.Controls
|
|
||||||
{
|
|
||||||
/// <summary>
|
|
||||||
/// Interaction logic for StatisticsControl.xaml
|
|
||||||
/// </summary>
|
|
||||||
public partial class StatisticsControl : UserControl
|
|
||||||
{
|
|
||||||
public StatisticsControl()
|
|
||||||
{
|
|
||||||
InitializeComponent();
|
|
||||||
}
|
|
||||||
|
|
||||||
private void LoadClosedAuctionsButton_Click(object sender, RoutedEventArgs e)
|
|
||||||
{
|
|
||||||
RaiseEvent(new RoutedEventArgs(LoadClosedAuctionsClickedEvent, this));
|
|
||||||
}
|
|
||||||
|
|
||||||
// Routed Events
|
|
||||||
public static readonly RoutedEvent LoadClosedAuctionsClickedEvent = EventManager.RegisterRoutedEvent(
|
|
||||||
"LoadClosedAuctionsClicked", RoutingStrategy.Bubble, typeof(RoutedEventHandler), typeof(StatisticsControl));
|
|
||||||
|
|
||||||
public event RoutedEventHandler LoadClosedAuctionsClicked
|
|
||||||
{
|
|
||||||
add { AddHandler(LoadClosedAuctionsClickedEvent, value); }
|
|
||||||
remove { RemoveHandler(LoadClosedAuctionsClickedEvent, value); }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
using System;
|
using System;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
using System.Windows;
|
using System.Windows;
|
||||||
using AutoBidder.Utilities;
|
using AutoBidder.Utilities;
|
||||||
@@ -21,7 +21,6 @@ namespace AutoBidder
|
|||||||
|
|
||||||
// Carica impostazioni predefinite aste
|
// Carica impostazioni predefinite aste
|
||||||
DefaultBidBeforeDeadlineMs.Text = settings.DefaultBidBeforeDeadlineMs.ToString();
|
DefaultBidBeforeDeadlineMs.Text = settings.DefaultBidBeforeDeadlineMs.ToString();
|
||||||
DefaultCheckAuctionOpen.IsChecked = settings.DefaultCheckAuctionOpenBeforeBid;
|
|
||||||
DefaultMinPrice.Text = settings.DefaultMinPrice.ToString("F2", System.Globalization.CultureInfo.InvariantCulture);
|
DefaultMinPrice.Text = settings.DefaultMinPrice.ToString("F2", System.Globalization.CultureInfo.InvariantCulture);
|
||||||
DefaultMaxPrice.Text = settings.DefaultMaxPrice.ToString("F2", System.Globalization.CultureInfo.InvariantCulture);
|
DefaultMaxPrice.Text = settings.DefaultMaxPrice.ToString("F2", System.Globalization.CultureInfo.InvariantCulture);
|
||||||
DefaultMaxClicks.Text = settings.DefaultMaxClicks.ToString();
|
DefaultMaxClicks.Text = settings.DefaultMaxClicks.ToString();
|
||||||
@@ -63,11 +62,64 @@ namespace AutoBidder
|
|||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Motore di precisione
|
||||||
|
Settings.PollIntervalFarMsTextBox.Text = settings.PollIntervalFarMs.ToString();
|
||||||
|
Settings.PollIntervalMidMsTextBox.Text = settings.PollIntervalMidMs.ToString();
|
||||||
|
Settings.PollIntervalNearMsTextBox.Text = settings.PollIntervalNearMs.ToString();
|
||||||
|
Settings.PollIntervalCriticalMsTextBox.Text = settings.PollIntervalCriticalMs.ToString();
|
||||||
|
Settings.CriticalWindowMsTextBox.Text = settings.CriticalWindowMs.ToString();
|
||||||
|
Settings.MaxRequestsPerSecondTextBox.Text = settings.MaxRequestsPerSecond.ToString("F0", System.Globalization.CultureInfo.InvariantCulture);
|
||||||
|
Settings.PrecisionTimerCheckBox.IsChecked = settings.PrecisionTimerEnabled;
|
||||||
|
|
||||||
|
RefreshEngineDiagnostics();
|
||||||
|
|
||||||
|
// Prodotti seguiti e catalogo
|
||||||
|
Settings.AutoAddEnabledCheckBox.IsChecked = settings.AutoAddProductsEnabled;
|
||||||
|
Settings.AutoAddState = settings.AutoAddNewAuctionState;
|
||||||
|
Settings.AutoAddScanSecondsTextBox.Text = settings.AutoAddScanSeconds.ToString();
|
||||||
|
Settings.AutoAddMaxAuctionsTextBox.Text = settings.AutoAddMaxAuctions.ToString();
|
||||||
|
Settings.AutoAddMaxStartMinutesTextBox.Text = settings.AutoAddMaxStartMinutes.ToString();
|
||||||
|
Settings.AutoAddOnlyNotStartedCheckBox.IsChecked = settings.AutoAddOnlyNotStarted;
|
||||||
|
Settings.SuggestedCoverageTextBox.Text = settings.SuggestedPriceCoveragePercent.ToString("0", System.Globalization.CultureInfo.CurrentCulture);
|
||||||
|
Settings.AverageBidCostTextBox.Text = settings.AverageBidCostEuro.ToString("0.00", System.Globalization.CultureInfo.CurrentCulture);
|
||||||
|
Settings.AutoAddScanDepthTextBox.Text = settings.AutoAddScanMaxAuctions.ToString();
|
||||||
|
Settings.CatalogMaxAuctionsTextBox.Text = settings.CatalogMaxAuctions.ToString();
|
||||||
|
Settings.CatalogAutoRefreshCheckBox.IsChecked = settings.CatalogAutoRefresh;
|
||||||
|
|
||||||
|
// Anticipo, aste programmate, notifiche, cartelle
|
||||||
|
Settings.BidLeadTrackingCheckBox.IsChecked = settings.BidLeadTrackingEnabled;
|
||||||
|
Settings.BidLeadSuggestionsCheckBox.IsChecked = settings.BidLeadSuggestionsEnabled;
|
||||||
|
Settings.BidLeadMinSamplesTextBox.Text = settings.BidLeadMinSamples.ToString();
|
||||||
|
|
||||||
|
Settings.ScheduledBackoffCheckBox.IsChecked = settings.ScheduledAuctionBackoffEnabled;
|
||||||
|
Settings.ScheduledPollFarTextBox.Text = settings.ScheduledPollFarSeconds.ToString();
|
||||||
|
Settings.ScheduledPollMidTextBox.Text = settings.ScheduledPollMidSeconds.ToString();
|
||||||
|
Settings.ScheduledPollWakeTextBox.Text = settings.ScheduledPollWakeSeconds.ToString();
|
||||||
|
|
||||||
|
Settings.NotifyOnWinCheckBox.IsChecked = settings.NotifyOnWin;
|
||||||
|
Settings.NotifyOnLossCheckBox.IsChecked = settings.NotifyOnLoss;
|
||||||
|
|
||||||
|
// I percorsi si mostrano <b>risolti</b>, non come sono salvati: vuoto nel
|
||||||
|
// file significa "predefinito", ma a video il predefinito ha un nome preciso
|
||||||
|
// ed è quello che serve sapere.
|
||||||
|
RefreshDataFolderFields();
|
||||||
|
|
||||||
|
Settings.DetailedStatsCheckBox.IsChecked = settings.DetailedStatsEnabled;
|
||||||
|
Settings.CatalogCacheSecondsTextBox.Text = settings.CatalogCacheSeconds.ToString();
|
||||||
|
|
||||||
|
Settings.WriteAppLogCheckBox.IsChecked = settings.WriteAppLogFile;
|
||||||
|
Settings.WriteFreeBidsLogCheckBox.IsChecked = settings.WriteFreeBidsLogFile;
|
||||||
|
Settings.WriteDossiersCheckBox.IsChecked = settings.WriteAuctionDossiers;
|
||||||
|
Settings.RawPollsCheckBox.IsChecked = settings.DossierIncludeRawPolls;
|
||||||
|
Settings.LogRetentionTextBox.Text = settings.LogRetentionDays.ToString();
|
||||||
|
|
||||||
|
RefreshBidLeadSummary();
|
||||||
|
|
||||||
// Aggiorna indicatore visivo
|
// Aggiorna indicatore visivo
|
||||||
UpdateMinBidsIndicator(settings.MinimumRemainingBids);
|
UpdateMinBidsIndicator(settings.MinimumRemainingBids);
|
||||||
|
|
||||||
// Carica stato iniziale aste
|
// Carica stato iniziale aste
|
||||||
// ? NUOVO: Se RememberAuctionStates è attivo, seleziona "Ricorda Stato"
|
// ? NUOVO: Se RememberAuctionStates � attivo, seleziona "Ricorda Stato"
|
||||||
if (settings.RememberAuctionStates)
|
if (settings.RememberAuctionStates)
|
||||||
{
|
{
|
||||||
Settings.LoadAuctionsRemember.IsChecked = true;
|
Settings.LoadAuctionsRemember.IsChecked = true;
|
||||||
@@ -130,7 +182,6 @@ namespace AutoBidder
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
settings.DefaultCheckAuctionOpenBeforeBid = DefaultCheckAuctionOpen.IsChecked ?? false;
|
|
||||||
|
|
||||||
if (double.TryParse(DefaultMinPrice.Text.Replace(',', '.'), System.Globalization.NumberStyles.Any,
|
if (double.TryParse(DefaultMinPrice.Text.Replace(',', '.'), System.Globalization.NumberStyles.Any,
|
||||||
System.Globalization.CultureInfo.InvariantCulture, out var minPrice))
|
System.Globalization.CultureInfo.InvariantCulture, out var minPrice))
|
||||||
@@ -238,7 +289,7 @@ namespace AutoBidder
|
|||||||
{
|
{
|
||||||
// Attiva RememberAuctionStates
|
// Attiva RememberAuctionStates
|
||||||
settings.RememberAuctionStates = true;
|
settings.RememberAuctionStates = true;
|
||||||
// DefaultStartAuctionsOnLoad diventa irrilevante, ma lo lasciamo a "Stopped" per compatibilità
|
// DefaultStartAuctionsOnLoad diventa irrilevante, ma lo lasciamo a "Stopped" per compatibilit�
|
||||||
settings.DefaultStartAuctionsOnLoad = "Stopped";
|
settings.DefaultStartAuctionsOnLoad = "Stopped";
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
@@ -254,10 +305,136 @@ namespace AutoBidder
|
|||||||
var newAuctionPaused = Settings.FindName("NewAuctionPaused") as System.Windows.Controls.RadioButton;
|
var newAuctionPaused = Settings.FindName("NewAuctionPaused") as System.Windows.Controls.RadioButton;
|
||||||
|
|
||||||
settings.DefaultNewAuctionState = newAuctionActive?.IsChecked == true ? "Active" :
|
settings.DefaultNewAuctionState = newAuctionActive?.IsChecked == true ? "Active" :
|
||||||
newAuctionPaused?.IsChecked == true ? "Paused" :
|
newAuctionPaused?.IsChecked == true ? "Paused" :
|
||||||
"Stopped";
|
"Stopped";
|
||||||
|
|
||||||
|
// === SEZIONE: Motore di precisione ===
|
||||||
|
// Ogni cadenza ha un minimo sensato: sotto i 100 ms si spreca banda senza
|
||||||
|
// guadagnare precisione, perché la puntata la decide il cecchino, non il poll.
|
||||||
|
settings.PollIntervalFarMs = ReadBounded(Settings.PollIntervalFarMsTextBox.Text, settings.PollIntervalFarMs, 200, 30000, "polling lontano");
|
||||||
|
settings.PollIntervalMidMs = ReadBounded(Settings.PollIntervalMidMsTextBox.Text, settings.PollIntervalMidMs, 150, 10000, "polling medio");
|
||||||
|
settings.PollIntervalNearMs = ReadBounded(Settings.PollIntervalNearMsTextBox.Text, settings.PollIntervalNearMs, 100, 5000, "polling vicino");
|
||||||
|
settings.PollIntervalCriticalMs = ReadBounded(Settings.PollIntervalCriticalMsTextBox.Text, settings.PollIntervalCriticalMs, 100, 3000, "polling critico");
|
||||||
|
settings.CriticalWindowMs = ReadBounded(Settings.CriticalWindowMsTextBox.Text, settings.CriticalWindowMs, 1000, 60000, "finestra critica");
|
||||||
|
|
||||||
|
if (double.TryParse(Settings.MaxRequestsPerSecondTextBox.Text.Replace(',', '.'),
|
||||||
|
System.Globalization.NumberStyles.Any,
|
||||||
|
System.Globalization.CultureInfo.InvariantCulture, out var rps) && rps >= 1 && rps <= 200)
|
||||||
|
{
|
||||||
|
settings.MaxRequestsPerSecond = rps;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
Log("[ERRORE] Limite richieste al secondo non valido (1-200): valore precedente mantenuto", LogLevel.Error);
|
||||||
|
}
|
||||||
|
|
||||||
|
settings.PrecisionTimerEnabled = Settings.PrecisionTimerCheckBox.IsChecked ?? true;
|
||||||
|
|
||||||
|
// === SEZIONE: Prodotti seguiti ===
|
||||||
|
settings.AutoAddProductsEnabled = Settings.AutoAddEnabledCheckBox.IsChecked ?? true;
|
||||||
|
settings.AutoAddNewAuctionState = Settings.AutoAddState;
|
||||||
|
settings.AutoAddScanSeconds = ReadBounded(Settings.AutoAddScanSecondsTextBox.Text,
|
||||||
|
settings.AutoAddScanSeconds, 30, 3600, "intervallo scansione prodotti seguiti", " s");
|
||||||
|
|
||||||
|
if (int.TryParse(Settings.AutoAddMaxAuctionsTextBox.Text, out var autoMax) && autoMax >= 0)
|
||||||
|
settings.AutoAddMaxAuctions = autoMax;
|
||||||
|
else
|
||||||
|
Log("[ERRORE] Massimo aste aggiunte automaticamente non valido (>= 0)", LogLevel.Error);
|
||||||
|
|
||||||
|
settings.AutoAddScanMaxAuctions = ReadBounded(Settings.AutoAddScanDepthTextBox.Text,
|
||||||
|
settings.AutoAddScanMaxAuctions, 50, 6000, "profondita ricerca prodotti seguiti", " aste");
|
||||||
|
|
||||||
|
// 0 = nessun orizzonte, ed e' una scelta vera: per questo il minimo del
|
||||||
|
// controllo di validita' e' zero e non uno.
|
||||||
|
settings.AutoAddOnlyNotStarted = Settings.AutoAddOnlyNotStartedCheckBox.IsChecked ?? false;
|
||||||
|
|
||||||
|
// Limiti consigliati per prodotto: due valori che entrano nel calcolo del
|
||||||
|
// tetto di prezzo. Si accetta la virgola oltre al punto, come nel resto
|
||||||
|
// dell'applicazione: chi scrive "0,20" non deve vedersi rifiutare il valore.
|
||||||
|
if (TryReadDouble(Settings.SuggestedCoverageTextBox.Text, out var copertura) &&
|
||||||
|
copertura is >= 10 and <= 99)
|
||||||
|
settings.SuggestedPriceCoveragePercent = copertura;
|
||||||
|
else
|
||||||
|
Log("[ERRORE] Copertura delle chiusure non valida (10-99%)", LogLevel.Error);
|
||||||
|
|
||||||
|
if (TryReadDouble(Settings.AverageBidCostTextBox.Text, out var costo) &&
|
||||||
|
costo is > 0 and <= 5)
|
||||||
|
settings.AverageBidCostEuro = costo;
|
||||||
|
else
|
||||||
|
Log("[ERRORE] Costo medio di una puntata non valido (0-5 EUR)", LogLevel.Error);
|
||||||
|
|
||||||
|
if (int.TryParse(Settings.AutoAddMaxStartMinutesTextBox.Text, out var orizzonte) && orizzonte >= 0)
|
||||||
|
settings.AutoAddMaxStartMinutes = orizzonte;
|
||||||
|
else
|
||||||
|
Log("[ERRORE] Orizzonte di aggiunta automatica non valido (>= 0 minuti)", LogLevel.Error);
|
||||||
|
|
||||||
|
if (settings.AutoAddProductsEnabled && settings.AutoAddNewAuctionState == "Active")
|
||||||
|
{
|
||||||
|
Log("[ATTENZIONE] Le aste seguite entreranno in stato Attiva: punteranno da sole, spendendo puntate reali.",
|
||||||
|
LogLevel.Warning);
|
||||||
|
}
|
||||||
|
|
||||||
|
// === SEZIONE: Catalogo ===
|
||||||
|
settings.CatalogMaxAuctions = ReadBounded(Settings.CatalogMaxAuctionsTextBox.Text,
|
||||||
|
settings.CatalogMaxAuctions, 20, 5000, "numero massimo aste catalogo", " aste");
|
||||||
|
settings.CatalogAutoRefresh = Settings.CatalogAutoRefreshCheckBox.IsChecked ?? false;
|
||||||
|
settings.CatalogCacheSeconds = ReadBounded(Settings.CatalogCacheSecondsTextBox.Text,
|
||||||
|
settings.CatalogCacheSeconds, 0, 3600, "cache catalogo", " s");
|
||||||
|
|
||||||
|
// === SEZIONE: Anticipo puntata ===
|
||||||
|
settings.BidLeadTrackingEnabled = Settings.BidLeadTrackingCheckBox.IsChecked ?? true;
|
||||||
|
settings.BidLeadSuggestionsEnabled = Settings.BidLeadSuggestionsCheckBox.IsChecked ?? true;
|
||||||
|
settings.BidLeadMinSamples = ReadBounded(Settings.BidLeadMinSamplesTextBox.Text,
|
||||||
|
settings.BidLeadMinSamples, 5, 500, "puntate minime per il consiglio", "");
|
||||||
|
|
||||||
|
// === SEZIONE: Aste programmate ===
|
||||||
|
settings.ScheduledAuctionBackoffEnabled = Settings.ScheduledBackoffCheckBox.IsChecked ?? true;
|
||||||
|
settings.ScheduledPollFarSeconds = ReadBounded(Settings.ScheduledPollFarTextBox.Text,
|
||||||
|
settings.ScheduledPollFarSeconds, 60, 3600, "polling aste lontane", " s");
|
||||||
|
settings.ScheduledPollMidSeconds = ReadBounded(Settings.ScheduledPollMidTextBox.Text,
|
||||||
|
settings.ScheduledPollMidSeconds, 30, 1800, "polling aste vicine all'apertura", " s");
|
||||||
|
settings.ScheduledPollWakeSeconds = ReadBounded(Settings.ScheduledPollWakeTextBox.Text,
|
||||||
|
settings.ScheduledPollWakeSeconds, 30, 600, "margine di risveglio", " s");
|
||||||
|
|
||||||
|
// === SEZIONE: Notifiche ===
|
||||||
|
settings.NotifyOnWin = Settings.NotifyOnWinCheckBox.IsChecked ?? true;
|
||||||
|
settings.NotifyOnLoss = Settings.NotifyOnLossCheckBox.IsChecked ?? false;
|
||||||
|
|
||||||
|
// === SEZIONE: Cartelle dei dati ===
|
||||||
|
var previousData = AppPaths.DataFolder;
|
||||||
|
var previousStats = AppPaths.StatsFolder;
|
||||||
|
|
||||||
|
// Il campo contiene il percorso risolto: se coincide con il predefinito si
|
||||||
|
// salva vuoto, così spostando la cartella dati i registri la seguono invece
|
||||||
|
// di restare inchiodati a un percorso che nessuno ha mai scelto davvero.
|
||||||
|
settings.DataFolder = NormalizeFolderChoice(Settings.DataFolderTextBox.Text, AppPaths.DataFolder, settings.DataFolder);
|
||||||
|
settings.StatsFolder = NormalizeFolderChoice(Settings.StatsFolderTextBox.Text, AppPaths.StatsFolder, settings.StatsFolder);
|
||||||
|
settings.LogFolder = NormalizeFolderChoice(Settings.LogFolderTextBox.Text, AppPaths.LogFolder, settings.LogFolder);
|
||||||
|
|
||||||
|
settings.DetailedStatsEnabled = Settings.DetailedStatsCheckBox.IsChecked ?? true;
|
||||||
|
|
||||||
|
settings.WriteAppLogFile = Settings.WriteAppLogCheckBox.IsChecked ?? true;
|
||||||
|
settings.WriteFreeBidsLogFile = Settings.WriteFreeBidsLogCheckBox.IsChecked ?? true;
|
||||||
|
settings.WriteAuctionDossiers = Settings.WriteDossiersCheckBox.IsChecked ?? true;
|
||||||
|
settings.DossierIncludeRawPolls = Settings.RawPollsCheckBox.IsChecked ?? true;
|
||||||
|
|
||||||
|
if (int.TryParse(Settings.LogRetentionTextBox.Text?.Trim(), out var retention) && retention >= 0)
|
||||||
|
settings.LogRetentionDays = retention;
|
||||||
|
|
||||||
Utilities.SettingsManager.Save(settings);
|
Utilities.SettingsManager.Save(settings);
|
||||||
|
|
||||||
|
ApplyDataFolderSettings(settings, previousData, previousStats);
|
||||||
|
|
||||||
|
// Il limite di ritmo si applica a caldo, senza ricreare il trasporto.
|
||||||
|
_auctionMonitor.ApplyTransportSettings(settings);
|
||||||
|
ApplyProductWatchSettings(settings);
|
||||||
|
|
||||||
|
// L'interruttore in Esplora non deve dissentire dalle impostazioni, e il
|
||||||
|
// ciclo va fermato o riavviato di conseguenza.
|
||||||
|
Browser.SetAutoRefresh(settings.CatalogAutoRefresh);
|
||||||
|
if (settings.CatalogAutoRefresh) StartCatalogAutoRefresh();
|
||||||
|
else StopCatalogAutoRefresh();
|
||||||
|
RefreshEngineDiagnostics();
|
||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
{
|
{
|
||||||
@@ -265,6 +442,64 @@ namespace AutoBidder
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Legge un intero entro limiti, mantenendo il valore precedente se il testo non è
|
||||||
|
/// utilizzabile: un campo sbagliato non deve far ripartire il motore a caso.
|
||||||
|
/// </summary>
|
||||||
|
/// <summary>
|
||||||
|
/// Legge un numero con la virgola o col punto. Le impostazioni si scrivono a mano
|
||||||
|
/// in un'applicazione italiana: rifiutare "0,20" sarebbe un dispetto.
|
||||||
|
/// </summary>
|
||||||
|
private static bool TryReadDouble(string? text, out double value)
|
||||||
|
{
|
||||||
|
value = 0;
|
||||||
|
if (string.IsNullOrWhiteSpace(text)) return false;
|
||||||
|
|
||||||
|
return double.TryParse(text.Trim().Replace(',', '.'),
|
||||||
|
System.Globalization.NumberStyles.Any,
|
||||||
|
System.Globalization.CultureInfo.InvariantCulture, out value);
|
||||||
|
}
|
||||||
|
|
||||||
|
private int ReadBounded(string text, int current, int min, int max, string label, string unit = "ms")
|
||||||
|
{
|
||||||
|
if (int.TryParse(text, out var value) && value >= min && value <= max)
|
||||||
|
return value;
|
||||||
|
|
||||||
|
Log($"[ERRORE] Valore {label} non valido ({min}-{max}{unit}): mantenuto {current}{unit}", LogLevel.Error);
|
||||||
|
return current;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Mostra lo stato del motore: aggancio all'orologio del server e traffico prodotto.
|
||||||
|
/// </summary>
|
||||||
|
private void RefreshEngineDiagnostics()
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var box = Settings.EngineDiagnosticsText;
|
||||||
|
if (box == null) return;
|
||||||
|
|
||||||
|
var clock = _auctionMonitor.Clock;
|
||||||
|
var sent = _auctionMonitor.RequestsSent;
|
||||||
|
var failed = _auctionMonitor.RequestsFailed;
|
||||||
|
|
||||||
|
// Attenzione a come si presenta lo scarto: Bidoo dichiara i secondi interi,
|
||||||
|
// quindi i campioni si distribuiscono per forza su una finestra di circa
|
||||||
|
// 1000 ms. Non è l'errore della stima — la stima usa il minimo, che converge
|
||||||
|
// al confine reale del secondo. Un valore molto oltre i 1000 ms segnala
|
||||||
|
// invece una rete instabile.
|
||||||
|
var clockLine = clock.IsSynced
|
||||||
|
? $"Orologio server agganciato su {clock.SampleCount} campioni: le scadenze seguono il server, non l'orologio locale.\n" +
|
||||||
|
$"Finestra campioni {clock.SpreadMs:F0} ms (intorno a 1000 ms è normale: Bidoo dichiara i secondi interi)."
|
||||||
|
: $"Orologio server in sincronizzazione ({clock.SampleCount}/3 campioni): finché non è agganciato la scadenza è stimata localmente.";
|
||||||
|
|
||||||
|
box.Text = $"{clockLine}\n" +
|
||||||
|
$"Richieste inviate: {sent:N0} — fallite: {failed:N0}.\n" +
|
||||||
|
$"Motori attivi: {_auctionMonitor.ActiveRunners}.";
|
||||||
|
}
|
||||||
|
catch { /* la diagnostica non deve mai far fallire il salvataggio */ }
|
||||||
|
}
|
||||||
|
|
||||||
private void CancelDefaultsButton_Click(object sender, RoutedEventArgs e)
|
private void CancelDefaultsButton_Click(object sender, RoutedEventArgs e)
|
||||||
{
|
{
|
||||||
try
|
try
|
||||||
|
|||||||
@@ -1,271 +1,214 @@
|
|||||||
using System;
|
using System;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.Globalization;
|
|
||||||
using System.IO;
|
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
using System.Text;
|
|
||||||
using System.Text.RegularExpressions;
|
|
||||||
using System.Threading.Tasks;
|
|
||||||
using System.Windows;
|
using System.Windows;
|
||||||
using System.Xml.Linq;
|
|
||||||
using AutoBidder.Models;
|
using AutoBidder.Models;
|
||||||
using AutoBidder.Utilities;
|
using AutoBidder.Utilities;
|
||||||
using Microsoft.EntityFrameworkCore;
|
|
||||||
|
|
||||||
namespace AutoBidder
|
namespace AutoBidder
|
||||||
{
|
{
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Statistics and closed auctions event handlers
|
/// Registrazione delle aste concluse.
|
||||||
/// NOTA: Funzionalità statistiche temporaneamente disabilitate - in sviluppo
|
///
|
||||||
|
/// Ne escono due archivi con scopi diversi: un riepilogo compatto che alimenta la
|
||||||
|
/// pagina Statistiche, e una scheda dettagliata per ogni asta destinata alle analisi
|
||||||
|
/// successive. Il secondo esiste perché quei dati, se non raccolti mentre l'asta è in
|
||||||
|
/// corso, non sono più recuperabili: Bidoo non li espone a posteriori.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public partial class MainWindow
|
public partial class MainWindow
|
||||||
{
|
{
|
||||||
private void ExportStatsButton_Click(object sender, RoutedEventArgs e)
|
private void AuctionMonitor_OnAuctionCompleted(AuctionInfo auction, AuctionState state, bool won)
|
||||||
{
|
{
|
||||||
MessageBox.Show(this, "Funzionalità statistiche in sviluppo", "Info", MessageBoxButton.OK, MessageBoxImage.Information);
|
|
||||||
}
|
|
||||||
|
|
||||||
private async void LoadClosedAuctionsButton_Click(object sender, RoutedEventArgs e)
|
|
||||||
{
|
|
||||||
MessageBox.Show(this, "Funzionalità statistiche in sviluppo", "Info", MessageBoxButton.OK, MessageBoxImage.Information);
|
|
||||||
|
|
||||||
/* CODICE TEMPORANEAMENTE DISABILITATO - Statistiche in sviluppo
|
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
StatsStatusText.Text = "Avvio caricamento statistiche...";
|
var settings = SettingsManager.Load();
|
||||||
var settings = Utilities.SettingsManager.Load();
|
|
||||||
if (settings == null || string.IsNullOrWhiteSpace(settings.ExportPath) || !Directory.Exists(settings.ExportPath))
|
var record = new CompletedAuctionRecord
|
||||||
{
|
{
|
||||||
MessageBox.Show(this, "Percorso export non configurato o non valido. Configuralo nelle impostazioni.", "Carica Statistiche", MessageBoxButton.OK, MessageBoxImage.Warning);
|
AuctionId = auction.AuctionId,
|
||||||
StatsStatusText.Text = "Percorso export non valido";
|
Name = string.IsNullOrWhiteSpace(auction.Name) ? auction.AuctionId : auction.Name,
|
||||||
return;
|
ProductKey = ProductKeyHelper.GenerateProductKey(auction.Name),
|
||||||
|
OriginalUrl = auction.OriginalUrl,
|
||||||
|
FinalPrice = state?.Price ?? 0,
|
||||||
|
Winner = state?.LastBidder ?? "",
|
||||||
|
WonByMe = won,
|
||||||
|
Outcome = won ? "Vinta"
|
||||||
|
: (state?.Status == AuctionStatus.EndedLost ? "Persa" : "Chiusa"),
|
||||||
|
MyBidsUsed = auction.BidsUsedOnThisAuction ?? 0,
|
||||||
|
TotalResets = auction.ResetCount,
|
||||||
|
BuyNowPrice = auction.BuyNowPrice,
|
||||||
|
AddedAt = auction.AddedAt,
|
||||||
|
|
||||||
|
// Le puntate spese dal vincitore arrivano dal server insieme allo
|
||||||
|
// stato finale: e' il numero che il sito mostra come "Puntate
|
||||||
|
// utilizzate", e l'unico affidabile (lo storico si ferma a cinquanta).
|
||||||
|
WinnerBidsPaid = state?.WinnerBidsPaid,
|
||||||
|
WinnerBidsFree = state?.WinnerBidsFree,
|
||||||
|
|
||||||
|
// L'ora della fine vera, non quella della registrazione. Un'asta
|
||||||
|
// terminata resta nel monitor finché non la si toglie: prendere
|
||||||
|
// DateTime.Now qui significherebbe datare la chiusura al momento in cui
|
||||||
|
// si è fatto pulizia, magari il giorno dopo.
|
||||||
|
EndedAt = ResolveEndedAt(auction, state)
|
||||||
|
};
|
||||||
|
|
||||||
|
CompletedAuctionsStore.Append(record);
|
||||||
|
|
||||||
|
// Un dato incoerente non va nelle medie: si dice e si scarta, invece di
|
||||||
|
// spostare in silenzio i limiti di prezzo del prodotto.
|
||||||
|
var integrity = AuctionIntegrity.CheckWinnerBids(record);
|
||||||
|
if (record.WinnerBidsUsed.HasValue && !integrity.IsTrustworthy)
|
||||||
|
{
|
||||||
|
Log($"[STATISTICHE] {record.Name}: puntate del vincitore scartate ({integrity.Reason})",
|
||||||
|
LogLevel.Warning);
|
||||||
|
}
|
||||||
|
else if (integrity.IsTrustworthy)
|
||||||
|
{
|
||||||
|
Log($"[STATISTICHE] {record.Name}: vinta con {record.WinnerBidsUsed} puntate " +
|
||||||
|
$"(prezzo {record.FinalPrice:F2} EUR)", LogLevel.Info);
|
||||||
}
|
}
|
||||||
|
|
||||||
ExportProgressBar.Visibility = Visibility.Visible;
|
var detail = BuildDetail(auction, state, record);
|
||||||
ExportProgressText.Visibility = Visibility.Visible;
|
|
||||||
ExportProgressText.Text = "Caricamento statistiche...";
|
|
||||||
|
|
||||||
var folder = settings.ExportPath!;
|
if (settings.DetailedStatsEnabled)
|
||||||
var files = Directory.GetFiles(folder, "auction_*.*");
|
|
||||||
if (files.Length == 0)
|
|
||||||
{
|
{
|
||||||
MessageBox.Show(this, "Nessun file di aste trovato nella cartella di export.", "Carica Statistiche", MessageBoxButton.OK, MessageBoxImage.Information);
|
AuctionDetailStore.Append(detail);
|
||||||
StatsStatusText.Text = "Nessun file trovato";
|
|
||||||
ExportProgressBar.Visibility = Visibility.Collapsed;
|
|
||||||
ExportProgressText.Visibility = Visibility.Collapsed;
|
|
||||||
return;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
var aggregated = new Dictionary<string, List<ClosedAuctionRecord>>(StringComparer.OrdinalIgnoreCase);
|
// Il dossier si chiude con il riepilogo: da lì in poi quel file è una
|
||||||
|
// storia completa, ed è così che l'analisi sa di poterlo usare.
|
||||||
|
AuctionDossier.Close(auction, detail, state);
|
||||||
|
|
||||||
await Task.Run(() =>
|
ProductStatsStore.RecordCompleted(record, detail);
|
||||||
|
|
||||||
|
NotifyOutcome(record, settings);
|
||||||
|
|
||||||
|
Dispatcher.BeginInvoke(() =>
|
||||||
{
|
{
|
||||||
foreach (var f in files)
|
Log($"[STORICO] Asta '{record.Name}' salvata ({record.Outcome}, €{record.FinalPrice:F2})",
|
||||||
|
won ? LogLevel.Success : LogLevel.Info);
|
||||||
|
|
||||||
|
// Una vincita è il solo momento in cui il conto delle aste da confermare
|
||||||
|
// cambia: chiederlo subito evita che la barra resti indietro fino al
|
||||||
|
// giro successivo del timer.
|
||||||
|
if (won) _ = RefreshAuctionsToConfirmAsync();
|
||||||
|
|
||||||
|
if (StatisticsPanel != null && StatisticsPanel.Visibility == Visibility.Visible)
|
||||||
{
|
{
|
||||||
try
|
LoadStatistics();
|
||||||
{
|
|
||||||
var ext = Path.GetExtension(f).ToLowerInvariant();
|
|
||||||
if (ext == ".json")
|
|
||||||
{
|
|
||||||
var txt = File.ReadAllText(f, Encoding.UTF8);
|
|
||||||
try
|
|
||||||
{
|
|
||||||
var rec = System.Text.Json.JsonSerializer.Deserialize<ClosedAuctionRecord>(txt);
|
|
||||||
if (rec != null)
|
|
||||||
{
|
|
||||||
var key = (rec.ProductName ?? ExtractProductFromFilename(f) ?? "<unknown>").Trim();
|
|
||||||
if (!aggregated.ContainsKey(key)) aggregated[key] = new List<ClosedAuctionRecord>();
|
|
||||||
aggregated[key].Add(rec);
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
catch { }
|
|
||||||
|
|
||||||
try
|
|
||||||
{
|
|
||||||
var arr = System.Text.Json.JsonSerializer.Deserialize<List<ClosedAuctionRecord>>(txt);
|
|
||||||
if (arr != null)
|
|
||||||
{
|
|
||||||
foreach (var r in arr)
|
|
||||||
{
|
|
||||||
var key = (r.ProductName ?? ExtractProductFromFilename(f) ?? "<unknown>").Trim();
|
|
||||||
if (!aggregated.ContainsKey(key)) aggregated[key] = new List<ClosedAuctionRecord>();
|
|
||||||
aggregated[key].Add(r);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
catch { }
|
|
||||||
}
|
|
||||||
else if (ext == ".xml")
|
|
||||||
{
|
|
||||||
try
|
|
||||||
{
|
|
||||||
var doc = XDocument.Load(f);
|
|
||||||
var auctionElems = doc.Descendants("Auction");
|
|
||||||
if (!auctionElems.Any()) auctionElems = doc.Descendants("AuctionExport");
|
|
||||||
foreach (var n in auctionElems)
|
|
||||||
{
|
|
||||||
var name = n.Descendants("Name").FirstOrDefault()?.Value ?? n.Descendants("ProductName").FirstOrDefault()?.Value;
|
|
||||||
double d = 0; double.TryParse(n.Descendants("FinalPrice").FirstOrDefault()?.Value, NumberStyles.Any, CultureInfo.InvariantCulture, out d);
|
|
||||||
int bids = 0; int.TryParse(n.Descendants("TotalBids").FirstOrDefault()?.Value, out bids);
|
|
||||||
var winner = n.Descendants("Winner").FirstOrDefault()?.Value ?? string.Empty;
|
|
||||||
var url = n.Descendants("OriginalUrl").FirstOrDefault()?.Value ?? string.Empty;
|
|
||||||
var rec = new ClosedAuctionRecord { ProductName = name, FinalPrice = d == 0 ? null : (double?)d, Winner = winner, BidsUsed = bids, AuctionUrl = url };
|
|
||||||
var key = (rec.ProductName ?? ExtractProductFromFilename(f) ?? "<unknown>").Trim();
|
|
||||||
if (!aggregated.ContainsKey(key)) aggregated[key] = new List<ClosedAuctionRecord>();
|
|
||||||
aggregated[key].Add(rec);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
catch { }
|
|
||||||
}
|
|
||||||
else // CSV or text
|
|
||||||
{
|
|
||||||
try
|
|
||||||
{
|
|
||||||
var lines = File.ReadAllLines(f, Encoding.UTF8);
|
|
||||||
string product = ExtractProductFromFilename(f) ?? "<unknown>";
|
|
||||||
double? price = null; int? bids = null; string winner = string.Empty; string url = string.Empty;
|
|
||||||
foreach (var l in lines)
|
|
||||||
{
|
|
||||||
var line = l.Trim();
|
|
||||||
if (line.StartsWith("Name,") || line.StartsWith("ProductName,"))
|
|
||||||
{
|
|
||||||
var parts = line.Split(',', 2);
|
|
||||||
if (parts.Length == 2) product = parts[1].Trim('"');
|
|
||||||
}
|
|
||||||
else if (line.StartsWith("FinalPrice", StringComparison.OrdinalIgnoreCase) || line.StartsWith("Price,"))
|
|
||||||
{
|
|
||||||
var parts = line.Split(',', 2);
|
|
||||||
if (parts.Length == 2 && double.TryParse(parts[1].Trim('"').Replace('€', ' ').Trim(), NumberStyles.Any, CultureInfo.InvariantCulture, out var p)) price = p;
|
|
||||||
}
|
|
||||||
else if (line.StartsWith("TotalBids", StringComparison.OrdinalIgnoreCase) || line.StartsWith("BidsUsed", StringComparison.OrdinalIgnoreCase))
|
|
||||||
{
|
|
||||||
var parts = line.Split(',', 2);
|
|
||||||
if (parts.Length == 2 && int.TryParse(parts[1].Trim('"'), out var b)) bids = b;
|
|
||||||
}
|
|
||||||
else if (line.StartsWith("Winner", StringComparison.OrdinalIgnoreCase))
|
|
||||||
{
|
|
||||||
var parts = line.Split(',', 2);
|
|
||||||
if (parts.Length == 2) winner = parts[1].Trim('"');
|
|
||||||
}
|
|
||||||
else if (line.StartsWith("OriginalUrl", StringComparison.OrdinalIgnoreCase) || line.StartsWith("AuctionUrl", StringComparison.OrdinalIgnoreCase))
|
|
||||||
{
|
|
||||||
var parts = line.Split(',', 2);
|
|
||||||
if (parts.Length == 2) url = parts[1].Trim('"');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
var rec = new ClosedAuctionRecord { ProductName = product, FinalPrice = price, BidsUsed = bids, Winner = winner, AuctionUrl = url };
|
|
||||||
var key = (rec.ProductName ?? ExtractProductFromFilename(f) ?? "<unknown>").Trim();
|
|
||||||
if (!aggregated.ContainsKey(key)) aggregated[key] = new List<ClosedAuctionRecord>();
|
|
||||||
aggregated[key].Add(rec);
|
|
||||||
}
|
|
||||||
catch { }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
catch { }
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
var stats = new List<object>();
|
|
||||||
foreach (var kv in aggregated)
|
|
||||||
{
|
|
||||||
var list = kv.Value.Where(x => x.FinalPrice.HasValue || x.BidsUsed.HasValue).ToList();
|
|
||||||
if (list.Count == 0) continue;
|
|
||||||
var avgPrice = list.Where(x => x.FinalPrice.HasValue).Select(x => x.FinalPrice!.Value).DefaultIfEmpty(0).Average();
|
|
||||||
var avgBids = list.Where(x => x.BidsUsed.HasValue).Select(x => x.BidsUsed!.Value).DefaultIfEmpty(0).Average();
|
|
||||||
var winner = list.Where(x => !string.IsNullOrEmpty(x.Winner)).GroupBy(x => x.Winner).OrderByDescending(g => g.Count()).Select(g => g.Key).FirstOrDefault() ?? string.Empty;
|
|
||||||
var example = list.FirstOrDefault(x => !string.IsNullOrEmpty(x.AuctionUrl))?.AuctionUrl ?? string.Empty;
|
|
||||||
|
|
||||||
stats.Add(new
|
|
||||||
{
|
|
||||||
ProductName = kv.Key,
|
|
||||||
FinalPrice = avgPrice,
|
|
||||||
Winner = winner,
|
|
||||||
BidsUsed = (int)Math.Round(avgBids),
|
|
||||||
AuctionUrl = example,
|
|
||||||
Count = list.Count,
|
|
||||||
AverageFinalPrice = avgPrice,
|
|
||||||
AverageBidsUsed = avgBids
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
Dispatcher.Invoke(() =>
|
|
||||||
{
|
|
||||||
StatsDataGrid.ItemsSource = stats.OrderByDescending(s => (int)s.GetType().GetProperty("Count")!.GetValue(s)).ToList();
|
|
||||||
StatsStatusText.Text = $"Caricati {stats.Count} prodotti ({files.Length} file analizzati)";
|
|
||||||
ExportProgressBar.Visibility = Visibility.Collapsed;
|
|
||||||
ExportProgressText.Visibility = Visibility.Collapsed;
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
{
|
{
|
||||||
StatsStatusText.Text = "Errore caricamento statistiche";
|
Console.WriteLine($"[STORICO ERROR] {ex.Message}");
|
||||||
Log($"[ERRORE] Carica statistiche: {ex.Message}", LogLevel.Error);
|
|
||||||
MessageBox.Show(this, "Errore durante caricamento statistiche: " + ex.Message, "Errore", MessageBoxButton.OK, MessageBoxImage.Error);
|
|
||||||
ExportProgressBar.Visibility = Visibility.Collapsed;
|
|
||||||
ExportProgressText.Visibility = Visibility.Collapsed;
|
|
||||||
}
|
}
|
||||||
*/
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private static string? ExtractProductFromFilename(string path)
|
/// <summary>
|
||||||
|
/// Quando l'asta è finita davvero, in ordine di attendibilità: l'istante fissato dal
|
||||||
|
/// monitor quando ha visto la conclusione dal vivo; la scadenza dichiarata dal
|
||||||
|
/// server nell'ultima risposta; l'istante di quella risposta. Solo se non c'è
|
||||||
|
/// nessuna delle tre si ripiega sull'ora corrente.
|
||||||
|
///
|
||||||
|
/// <para>La differenza non è cosmetica: lo storico si legge per capire a che ora del
|
||||||
|
/// giorno chiudono le aste che conviene seguire, e una data presa al momento della
|
||||||
|
/// rimozione rende quel numero privo di significato.</para>
|
||||||
|
/// </summary>
|
||||||
|
private static DateTime ResolveEndedAt(AuctionInfo auction, AuctionState? state)
|
||||||
|
{
|
||||||
|
if (auction.ConcludedAt.HasValue) return auction.ConcludedAt.Value;
|
||||||
|
|
||||||
|
if (state != null && state.ExpiryUnixSeconds > 0)
|
||||||
|
return DateTimeOffset.FromUnixTimeSeconds(state.ExpiryUnixSeconds).ToLocalTime().DateTime;
|
||||||
|
|
||||||
|
if (state != null && state.SnapshotTime != default)
|
||||||
|
return state.SnapshotTime.ToLocalTime();
|
||||||
|
|
||||||
|
return DateTime.Now;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Costruisce la scheda dettagliata a partire da ciò che il monitor ha osservato.
|
||||||
|
/// </summary>
|
||||||
|
private AuctionDetailRecord BuildDetail(AuctionInfo auction, AuctionState? state, CompletedAuctionRecord summary)
|
||||||
|
{
|
||||||
|
var bidders = auction.SnapshotBidderStats();
|
||||||
|
var bidsByUser = bidders.ToDictionary(b => b.Username, b => b.BidCount, StringComparer.OrdinalIgnoreCase);
|
||||||
|
|
||||||
|
var series = auction.SnapshotPriceSeries();
|
||||||
|
var firstSeen = auction.FirstSeenAt ?? auction.AddedAt;
|
||||||
|
var observedMinutes = Math.Max(0, (summary.EndedAt - firstSeen).TotalMinutes);
|
||||||
|
|
||||||
|
return new AuctionDetailRecord
|
||||||
|
{
|
||||||
|
AuctionId = summary.AuctionId,
|
||||||
|
Name = summary.Name,
|
||||||
|
ProductKey = summary.ProductKey,
|
||||||
|
Url = summary.OriginalUrl,
|
||||||
|
|
||||||
|
Outcome = summary.Outcome,
|
||||||
|
WonByMe = summary.WonByMe,
|
||||||
|
Winner = summary.Winner,
|
||||||
|
FinalPrice = summary.FinalPrice,
|
||||||
|
BuyNowPrice = auction.BuyNowPrice,
|
||||||
|
ShippingCost = auction.ShippingCost,
|
||||||
|
|
||||||
|
FirstSeenAt = firstSeen,
|
||||||
|
EndedAt = summary.EndedAt,
|
||||||
|
ObservedMinutes = observedMinutes,
|
||||||
|
|
||||||
|
MyBids = summary.MyBidsUsed,
|
||||||
|
TotalObservedBids = bidsByUser.Values.Sum(),
|
||||||
|
DistinctBidders = bidsByUser.Count,
|
||||||
|
Resets = auction.ResetCount,
|
||||||
|
BidsByUser = bidsByUser,
|
||||||
|
TopBidderShare = AuctionDetailRecord.ComputeTopShare(bidsByUser),
|
||||||
|
|
||||||
|
PriceSeries = series,
|
||||||
|
PriceVelocityPerMinute = observedMinutes > 0.01 ? summary.FinalPrice / observedMinutes : 0,
|
||||||
|
|
||||||
|
AveragePingMs = auction.AverageLatencyMs,
|
||||||
|
PollCount = auction.PollCount,
|
||||||
|
PollErrors = auction.PollErrors,
|
||||||
|
ConfiguredLeadMs = auction.BidBeforeDeadlineMs,
|
||||||
|
TimerExpiredCount = auction.TimerExpiredCount,
|
||||||
|
SuccessfulBids = auction.SuccessfulBidCount,
|
||||||
|
FailedBids = auction.FailedBidCount,
|
||||||
|
BidCostEuro = auction.BidCost,
|
||||||
|
|
||||||
|
ObservedFromStart = auction.ObservedFromStart,
|
||||||
|
ObservedToEnd = auction.ObservedToEnd,
|
||||||
|
DossierPath = AuctionDossier.For(auction.AuctionId)?.Path
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Avvisa con una notifica di sistema. Una vittoria va confermata su Bidoo, e
|
||||||
|
/// accorgersene solo riaprendo la finestra è facile da dimenticare.
|
||||||
|
/// </summary>
|
||||||
|
private static void NotifyOutcome(CompletedAuctionRecord record, AppSettings settings)
|
||||||
{
|
{
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
var name = Path.GetFileNameWithoutExtension(path);
|
if (record.WonByMe && settings.NotifyOnWin)
|
||||||
var m = Regex.Match(name, @"auction_(.+)");
|
|
||||||
if (m.Success)
|
|
||||||
{
|
{
|
||||||
var v = m.Groups[1].Value;
|
WindowsNotifier.Show(
|
||||||
if (Regex.IsMatch(v, "^\\d+$")) return null;
|
"Asta vinta!",
|
||||||
return v.Replace('_', ' ');
|
$"{record.Name}\nAggiudicata a € {record.FinalPrice:F2} con {record.MyBidsUsed} puntate.\n" +
|
||||||
|
"Ricordati di completare l'acquisto su Bidoo.");
|
||||||
}
|
}
|
||||||
return null;
|
else if (!record.WonByMe && settings.NotifyOnLoss && record.MyBidsUsed > 0)
|
||||||
}
|
|
||||||
catch { return null; }
|
|
||||||
}
|
|
||||||
|
|
||||||
private async void ApplyInsightsButton_Click(object sender, RoutedEventArgs e)
|
|
||||||
{
|
|
||||||
MessageBox.Show(this, "Funzionalità statistiche in sviluppo", "Info", MessageBoxButton.OK, MessageBoxImage.Information);
|
|
||||||
|
|
||||||
/* CODICE TEMPORANEAMENTE DISABILITATO
|
|
||||||
try
|
|
||||||
{
|
|
||||||
if (_selectedAuction == null)
|
|
||||||
{
|
{
|
||||||
MessageBox.Show(this, "Seleziona un'asta prima di applicare le raccomandazioni.", "Applica Raccomandazioni", MessageBoxButton.OK, MessageBoxImage.Information);
|
// Solo se ci avevamo davvero puntato: le aste solo osservate non
|
||||||
return;
|
// meritano un avviso.
|
||||||
|
WindowsNotifier.Show(
|
||||||
|
"Asta persa",
|
||||||
|
$"{record.Name}\nChiusa a € {record.FinalPrice:F2} da {record.Winner}.",
|
||||||
|
warning: true);
|
||||||
}
|
}
|
||||||
|
|
||||||
var optionsBuilder = new Microsoft.EntityFrameworkCore.DbContextOptionsBuilder<Data.StatisticsContext>();
|
|
||||||
var dbPath = Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "stats.db");
|
|
||||||
optionsBuilder.UseSqlite($"Data Source={dbPath}");
|
|
||||||
using var ctx = new Data.StatisticsContext(optionsBuilder.Options);
|
|
||||||
var svc = new Services.StatsService(ctx);
|
|
||||||
|
|
||||||
var (recBids, recPrice) = await svc.GetRecommendationAsync(_selectedAuction.AuctionInfo.Name, _selectedAuction.AuctionInfo.OriginalUrl);
|
|
||||||
_selectedAuction.MaxClicks = Math.Max(_selectedAuction.MaxClicks, recBids);
|
|
||||||
_selectedAuction.MaxPrice = Math.Max(_selectedAuction.MaxPrice, recPrice);
|
|
||||||
Log($"[OK] Raccomandazioni: MaxClicks={recBids}, MaxPrice={recPrice:F2} applicate a {_selectedAuction.Name}", LogLevel.Success);
|
|
||||||
UpdateSelectedAuctionDetails(_selectedAuction);
|
|
||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch { }
|
||||||
{
|
|
||||||
Log($"[ERRORE] ApplyInsights: {ex.Message}", LogLevel.Error);
|
|
||||||
MessageBox.Show(this, "Errore applicazione raccomandazioni: " + ex.Message, "Errore", MessageBoxButton.OK, MessageBoxImage.Error);
|
|
||||||
}
|
|
||||||
*/
|
|
||||||
}
|
|
||||||
|
|
||||||
private void FreeBidsStart_Click(object sender, RoutedEventArgs e)
|
|
||||||
{
|
|
||||||
MessageBox.Show(this, "Funzionalità non ancora implementata", "Info", MessageBoxButton.OK, MessageBoxImage.Information);
|
|
||||||
}
|
|
||||||
|
|
||||||
private void FreeBidsStop_Click(object sender, RoutedEventArgs e)
|
|
||||||
{
|
|
||||||
MessageBox.Show(this, "Funzionalità non ancora implementata", "Info", MessageBoxButton.OK, MessageBoxImage.Information);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,7 +5,8 @@ using System.Windows;
|
|||||||
using AutoBidder.Models;
|
using AutoBidder.Models;
|
||||||
using AutoBidder.ViewModels;
|
using AutoBidder.ViewModels;
|
||||||
using AutoBidder.Utilities;
|
using AutoBidder.Utilities;
|
||||||
using AutoBidder.Services; // ? AGGIUNTO per RequestPriority e HtmlResponse
|
using AutoBidder.Services; // HtmlCacheService, HtmlResponse
|
||||||
|
using AutoBidder.Net; // RequestPriority
|
||||||
|
|
||||||
namespace AutoBidder
|
namespace AutoBidder
|
||||||
{
|
{
|
||||||
@@ -24,14 +25,14 @@ namespace AutoBidder
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
string auctionId;
|
string? auctionId;
|
||||||
string? productName = null;
|
string? productName = null;
|
||||||
string originalUrl;
|
string originalUrl;
|
||||||
|
|
||||||
// Verifica se è un URL o solo un ID
|
// Verifica se � un URL o solo un ID
|
||||||
if (input.Contains("bidoo.com") || input.Contains("http"))
|
if (input.Contains("bidoo.com") || input.Contains("http"))
|
||||||
{
|
{
|
||||||
// È un URL - estrai ID e nome prodotto dall'URL stesso
|
// � un URL - estrai ID e nome prodotto dall'URL stesso
|
||||||
originalUrl = input.Trim();
|
originalUrl = input.Trim();
|
||||||
auctionId = ExtractAuctionId(originalUrl);
|
auctionId = ExtractAuctionId(originalUrl);
|
||||||
if (string.IsNullOrEmpty(auctionId))
|
if (string.IsNullOrEmpty(auctionId))
|
||||||
@@ -44,7 +45,7 @@ namespace AutoBidder
|
|||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
// È solo un ID numerico - costruisci URL generico
|
// � solo un ID numerico - costruisci URL generico
|
||||||
auctionId = input.Trim();
|
auctionId = input.Trim();
|
||||||
originalUrl = $"https://it.bidoo.com/auction.php?a=asta_{auctionId}";
|
originalUrl = $"https://it.bidoo.com/auction.php?a=asta_{auctionId}";
|
||||||
}
|
}
|
||||||
@@ -52,11 +53,11 @@ namespace AutoBidder
|
|||||||
// Verifica duplicati
|
// Verifica duplicati
|
||||||
if (_auctionViewModels.Any(a => a.AuctionId == auctionId))
|
if (_auctionViewModels.Any(a => a.AuctionId == auctionId))
|
||||||
{
|
{
|
||||||
MessageBox.Show("Asta già monitorata!", "Duplicato", MessageBoxButton.OK, MessageBoxImage.Information);
|
MessageBox.Show("Asta gi� monitorata!", "Duplicato", MessageBoxButton.OK, MessageBoxImage.Information);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// ? MODIFICATO: Nome senza ID (già nella colonna separata)
|
// ? MODIFICATO: Nome senza ID (gi� nella colonna separata)
|
||||||
var displayName = string.IsNullOrEmpty(productName)
|
var displayName = string.IsNullOrEmpty(productName)
|
||||||
? $"Asta {auctionId}"
|
? $"Asta {auctionId}"
|
||||||
: DecodeAllHtmlEntities(productName);
|
: DecodeAllHtmlEntities(productName);
|
||||||
@@ -92,7 +93,6 @@ namespace AutoBidder
|
|||||||
Name = DecodeAllHtmlEntities(displayName),
|
Name = DecodeAllHtmlEntities(displayName),
|
||||||
OriginalUrl = originalUrl,
|
OriginalUrl = originalUrl,
|
||||||
BidBeforeDeadlineMs = settings.DefaultBidBeforeDeadlineMs,
|
BidBeforeDeadlineMs = settings.DefaultBidBeforeDeadlineMs,
|
||||||
CheckAuctionOpenBeforeBid = settings.DefaultCheckAuctionOpenBeforeBid,
|
|
||||||
IsActive = isActive,
|
IsActive = isActive,
|
||||||
IsPaused = isPaused
|
IsPaused = isPaused
|
||||||
};
|
};
|
||||||
@@ -100,16 +100,20 @@ namespace AutoBidder
|
|||||||
// Aggiungi al monitor
|
// Aggiungi al monitor
|
||||||
_auctionMonitor.AddAuction(auction);
|
_auctionMonitor.AddAuction(auction);
|
||||||
|
|
||||||
// Crea ViewModel con valori dalle impostazioni
|
// Limiti del prodotto se ne ha di suoi, altrimenti i predefiniti. Lo stato
|
||||||
|
// non si tocca: qui l'ha scelto l'utente nella finestra di aggiunta, mentre
|
||||||
|
// lo stato del prodotto vale per le aste che entrano da sole.
|
||||||
|
var limits = ProductRuleResolver.ResolveByName(auction.Name, settings);
|
||||||
|
|
||||||
var vm = new AuctionViewModel(auction)
|
var vm = new AuctionViewModel(auction)
|
||||||
{
|
{
|
||||||
MinPrice = settings.DefaultMinPrice,
|
MinPrice = limits.MinPrice,
|
||||||
MaxPrice = settings.DefaultMaxPrice,
|
MaxPrice = limits.MaxPrice,
|
||||||
MaxClicks = settings.DefaultMaxClicks
|
MaxClicks = limits.MaxClicks
|
||||||
};
|
};
|
||||||
_auctionViewModels.Add(vm);
|
_auctionViewModels.Add(vm);
|
||||||
|
|
||||||
// ? Auto-start del monitoraggio se l'asta è attiva e il monitoraggio è fermo
|
// ? Auto-start del monitoraggio se l'asta � attiva e il monitoraggio � fermo
|
||||||
if (isActive && !_isAutomationActive)
|
if (isActive && !_isAutomationActive)
|
||||||
{
|
{
|
||||||
_auctionMonitor.Start();
|
_auctionMonitor.Start();
|
||||||
@@ -124,7 +128,7 @@ namespace AutoBidder
|
|||||||
var stateText = isActive ? (isPaused ? "Paused" : "Active") : "Stopped";
|
var stateText = isActive ? (isPaused ? "Paused" : "Active") : "Stopped";
|
||||||
Log($"[ADD] Asta aggiunta con stato={stateText}, Anticipo={settings.DefaultBidBeforeDeadlineMs}ms", Utilities.LogLevel.Info);
|
Log($"[ADD] Asta aggiunta con stato={stateText}, Anticipo={settings.DefaultBidBeforeDeadlineMs}ms", Utilities.LogLevel.Info);
|
||||||
|
|
||||||
// ? NUOVO: Se il nome non è stato estratto, recuperalo in background DOPO l'aggiunta
|
// ? NUOVO: Se il nome non � stato estratto, recuperalo in background DOPO l'aggiunta
|
||||||
if (string.IsNullOrEmpty(productName))
|
if (string.IsNullOrEmpty(productName))
|
||||||
{
|
{
|
||||||
_ = FetchAuctionNameInBackgroundAsync(auction, vm);
|
_ = FetchAuctionNameInBackgroundAsync(auction, vm);
|
||||||
@@ -206,12 +210,12 @@ namespace AutoBidder
|
|||||||
decoded = decoded.Replace("+", "+");
|
decoded = decoded.Replace("+", "+");
|
||||||
decoded = decoded.Replace("=", "=");
|
decoded = decoded.Replace("=", "=");
|
||||||
decoded = decoded.Replace("−", "-");
|
decoded = decoded.Replace("−", "-");
|
||||||
decoded = decoded.Replace("×", "×");
|
decoded = decoded.Replace("×", "�");
|
||||||
decoded = decoded.Replace("÷", "÷");
|
decoded = decoded.Replace("÷", "�");
|
||||||
decoded = decoded.Replace("%", "%");
|
decoded = decoded.Replace("%", "%");
|
||||||
decoded = decoded.Replace("$", "$");
|
decoded = decoded.Replace("$", "$");
|
||||||
decoded = decoded.Replace("€", "€");
|
decoded = decoded.Replace("€", "�");
|
||||||
decoded = decoded.Replace("£", "£");
|
decoded = decoded.Replace("£", "�");
|
||||||
|
|
||||||
return decoded;
|
return decoded;
|
||||||
}
|
}
|
||||||
@@ -236,7 +240,7 @@ namespace AutoBidder
|
|||||||
// Verifica duplicati
|
// Verifica duplicati
|
||||||
if (_auctionViewModels.Any(a => a.AuctionId == auctionId))
|
if (_auctionViewModels.Any(a => a.AuctionId == auctionId))
|
||||||
{
|
{
|
||||||
MessageBox.Show("Asta già monitorata!", "Duplicato", MessageBoxButton.OK, MessageBoxImage.Information);
|
MessageBox.Show("Asta gi� monitorata!", "Duplicato", MessageBoxButton.OK, MessageBoxImage.Information);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -289,7 +293,6 @@ namespace AutoBidder
|
|||||||
Name = DecodeAllHtmlEntities(name),
|
Name = DecodeAllHtmlEntities(name),
|
||||||
OriginalUrl = url,
|
OriginalUrl = url,
|
||||||
BidBeforeDeadlineMs = settings.DefaultBidBeforeDeadlineMs,
|
BidBeforeDeadlineMs = settings.DefaultBidBeforeDeadlineMs,
|
||||||
CheckAuctionOpenBeforeBid = settings.DefaultCheckAuctionOpenBeforeBid,
|
|
||||||
IsActive = isActive,
|
IsActive = isActive,
|
||||||
IsPaused = isPaused
|
IsPaused = isPaused
|
||||||
};
|
};
|
||||||
@@ -297,16 +300,20 @@ namespace AutoBidder
|
|||||||
// Aggiungi al monitor
|
// Aggiungi al monitor
|
||||||
_auctionMonitor.AddAuction(auction);
|
_auctionMonitor.AddAuction(auction);
|
||||||
|
|
||||||
// Crea ViewModel con valori dalle impostazioni
|
// Limiti del prodotto se ne ha di suoi, altrimenti i predefiniti. Lo stato
|
||||||
|
// non si tocca: qui l'ha scelto l'utente nella finestra di aggiunta, mentre
|
||||||
|
// lo stato del prodotto vale per le aste che entrano da sole.
|
||||||
|
var limits = ProductRuleResolver.ResolveByName(auction.Name, settings);
|
||||||
|
|
||||||
var vm = new AuctionViewModel(auction)
|
var vm = new AuctionViewModel(auction)
|
||||||
{
|
{
|
||||||
MinPrice = settings.DefaultMinPrice,
|
MinPrice = limits.MinPrice,
|
||||||
MaxPrice = settings.DefaultMaxPrice,
|
MaxPrice = limits.MaxPrice,
|
||||||
MaxClicks = settings.DefaultMaxClicks
|
MaxClicks = limits.MaxClicks
|
||||||
};
|
};
|
||||||
_auctionViewModels.Add(vm);
|
_auctionViewModels.Add(vm);
|
||||||
|
|
||||||
// ? Auto-start del monitoraggio se l'asta è attiva e il monitoraggio è fermo
|
// ? Auto-start del monitoraggio se l'asta � attiva e il monitoraggio � fermo
|
||||||
if (isActive && !_isAutomationActive)
|
if (isActive && !_isAutomationActive)
|
||||||
{
|
{
|
||||||
_auctionMonitor.Start();
|
_auctionMonitor.Start();
|
||||||
@@ -353,12 +360,12 @@ namespace AutoBidder
|
|||||||
{
|
{
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
// Aspetta 30 secondi prima di ritentare (dà tempo alle altre richieste di completare)
|
// Aspetta 30 secondi prima di ritentare (d� tempo alle altre richieste di completare)
|
||||||
await System.Threading.Tasks.Task.Delay(TimeSpan.FromSeconds(30));
|
await System.Threading.Tasks.Task.Delay(TimeSpan.FromSeconds(30));
|
||||||
|
|
||||||
// Trova aste con nomi generici "Asta XXXX"
|
// Trova aste con nomi generici "Asta XXXX"
|
||||||
var auctionsWithGenericNames = _auctionViewModels
|
var auctionsWithGenericNames = _auctionViewModels
|
||||||
.Where(vm => vm.Name.StartsWith("Asta ") && !vm.Name.Contains("Shop") && !vm.Name.Contains("€"))
|
.Where(vm => vm.Name.StartsWith("Asta ") && !vm.Name.Contains("Shop") && !vm.Name.Contains("�"))
|
||||||
.ToList();
|
.ToList();
|
||||||
|
|
||||||
if (auctionsWithGenericNames.Count > 0)
|
if (auctionsWithGenericNames.Count > 0)
|
||||||
@@ -426,7 +433,7 @@ namespace AutoBidder
|
|||||||
if (settings.RememberAuctionStates)
|
if (settings.RememberAuctionStates)
|
||||||
{
|
{
|
||||||
// MODO 1: Ripristina lo stato salvato di ogni asta (IsActive e IsPaused vengono dal file salvato)
|
// MODO 1: Ripristina lo stato salvato di ogni asta (IsActive e IsPaused vengono dal file salvato)
|
||||||
// Non serve fare nulla, lo stato è già quello salvato nel file
|
// Non serve fare nulla, lo stato � gi� quello salvato nel file
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
@@ -517,9 +524,10 @@ namespace AutoBidder
|
|||||||
if (vm == null || vm.AuctionInfo == null)
|
if (vm == null || vm.AuctionInfo == null)
|
||||||
{
|
{
|
||||||
// Resetta campi se nessuna asta selezionata
|
// Resetta campi se nessuna asta selezionata
|
||||||
AuctionMonitor.ProductBuyNowPriceText.Text = "-";
|
AuctionMonitor.ProductBuyNowPriceText.Text = "�";
|
||||||
AuctionMonitor.ProductShippingCostText.Text = "-";
|
AuctionMonitor.ProductShippingCostText.Text = "�";
|
||||||
AuctionMonitor.ProductWinLimitText.Text = "-";
|
AuctionMonitor.ProductWinLimitText.Text = "";
|
||||||
|
RefreshProductVerdict(null);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -538,21 +546,21 @@ namespace AutoBidder
|
|||||||
// Aggiorna Valore (Compra Subito)
|
// Aggiorna Valore (Compra Subito)
|
||||||
if (auction.BuyNowPrice.HasValue)
|
if (auction.BuyNowPrice.HasValue)
|
||||||
{
|
{
|
||||||
AuctionMonitor.ProductBuyNowPriceText.Text = $"{auction.BuyNowPrice.Value:F2}€";
|
AuctionMonitor.ProductBuyNowPriceText.Text = $"{auction.BuyNowPrice.Value:F2}�";
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
AuctionMonitor.ProductBuyNowPriceText.Text = "-";
|
AuctionMonitor.ProductBuyNowPriceText.Text = "�";
|
||||||
}
|
}
|
||||||
|
|
||||||
// Aggiorna Spese di Spedizione
|
// Aggiorna Spese di Spedizione
|
||||||
if (auction.ShippingCost.HasValue)
|
if (auction.ShippingCost.HasValue)
|
||||||
{
|
{
|
||||||
AuctionMonitor.ProductShippingCostText.Text = $"{auction.ShippingCost.Value:F2}€";
|
AuctionMonitor.ProductShippingCostText.Text = $"{auction.ShippingCost.Value:F2}�";
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
AuctionMonitor.ProductShippingCostText.Text = "-";
|
AuctionMonitor.ProductShippingCostText.Text = "�";
|
||||||
}
|
}
|
||||||
|
|
||||||
// Aggiorna Limiti di Vincita
|
// Aggiorna Limiti di Vincita
|
||||||
@@ -562,12 +570,16 @@ namespace AutoBidder
|
|||||||
}
|
}
|
||||||
else if (!auction.HasWinLimit)
|
else if (!auction.HasWinLimit)
|
||||||
{
|
{
|
||||||
AuctionMonitor.ProductWinLimitText.Text = "Nessun limite";
|
AuctionMonitor.ProductWinLimitText.Text = "";
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
AuctionMonitor.ProductWinLimitText.Text = "-";
|
AuctionMonitor.ProductWinLimitText.Text = "";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Verdetto di convenienza: costo totale se vinci contro valore del prodotto.
|
||||||
|
RefreshProductVerdict(vm);
|
||||||
|
RefreshSelectedStats(vm);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@@ -579,7 +591,7 @@ namespace AutoBidder
|
|||||||
{
|
{
|
||||||
bool hasGenericName = auction.Name.StartsWith("Asta ") &&
|
bool hasGenericName = auction.Name.StartsWith("Asta ") &&
|
||||||
!auction.Name.Contains("Shop") &&
|
!auction.Name.Contains("Shop") &&
|
||||||
!auction.Name.Contains("€") &&
|
!auction.Name.Contains("�") &&
|
||||||
!auction.Name.Contains("Buono") &&
|
!auction.Name.Contains("Buono") &&
|
||||||
!auction.Name.Contains("Carburante");
|
!auction.Name.Contains("Carburante");
|
||||||
|
|
||||||
@@ -588,7 +600,7 @@ namespace AutoBidder
|
|||||||
// ? USA IL SERVIZIO CENTRALIZZATO
|
// ? USA IL SERVIZIO CENTRALIZZATO
|
||||||
var response = await _htmlCacheService.GetHtmlAsync(
|
var response = await _htmlCacheService.GetHtmlAsync(
|
||||||
auction.OriginalUrl,
|
auction.OriginalUrl,
|
||||||
RequestPriority.High, // Priorità alta per info prodotto
|
RequestPriority.Normal, // Priorit� alta per info prodotto
|
||||||
bypassCache: false
|
bypassCache: false
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -622,10 +634,10 @@ namespace AutoBidder
|
|||||||
if (extracted)
|
if (extracted)
|
||||||
{
|
{
|
||||||
updated = true;
|
updated = true;
|
||||||
Log($"[PRODUCT INFO] Valore={auction.BuyNowPrice:F2}€, Spedizione={auction.ShippingCost:F2}€{(response.FromCache ? " (cached)" : "")}", Utilities.LogLevel.Success);
|
Log($"[PRODUCT INFO] Valore={auction.BuyNowPrice:F2}�, Spedizione={auction.ShippingCost:F2}�{(response.FromCache ? " (cached)" : "")}", Utilities.LogLevel.Success);
|
||||||
}
|
}
|
||||||
|
|
||||||
// 3. ? Salva e aggiorna UI solo se qualcosa è cambiato
|
// 3. ? Salva e aggiorna UI solo se qualcosa � cambiato
|
||||||
if (updated)
|
if (updated)
|
||||||
{
|
{
|
||||||
SaveAuctions();
|
SaveAuctions();
|
||||||
|
|||||||
@@ -14,7 +14,8 @@ namespace AutoBidder
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
public partial class MainWindow
|
public partial class MainWindow
|
||||||
{
|
{
|
||||||
private void StartButton_Click(object sender, RoutedEventArgs e)
|
// sender null = azione interna (non richiesta dall'utente): si evita di loggarla.
|
||||||
|
private void StartButton_Click(object? sender, RoutedEventArgs e)
|
||||||
{
|
{
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
@@ -52,7 +53,7 @@ namespace AutoBidder
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private void StopButton_Click(object sender, RoutedEventArgs e)
|
private void StopButton_Click(object? sender, RoutedEventArgs e)
|
||||||
{
|
{
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
@@ -84,22 +85,31 @@ namespace AutoBidder
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private async void PauseAllButton_Click(object sender, RoutedEventArgs e)
|
private async void PauseAllButton_Click(object? sender, RoutedEventArgs e)
|
||||||
{
|
{
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
foreach (var vm in _auctionViewModels.Where(a => a.IsActive))
|
// Osserva vale anche per le aste ferme: servono comunque i motori accesi
|
||||||
|
// per seguirle, semplicemente non punteranno.
|
||||||
|
foreach (var vm in _auctionViewModels)
|
||||||
{
|
{
|
||||||
|
vm.IsActive = true;
|
||||||
vm.IsPaused = true;
|
vm.IsPaused = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (_auctionViewModels.Count > 0 && !_isAutomationActive)
|
||||||
|
{
|
||||||
|
_auctionMonitor.Start();
|
||||||
|
_isAutomationActive = true;
|
||||||
|
}
|
||||||
|
|
||||||
// ? Salva gli stati aggiornati su disco
|
// ? Salva gli stati aggiornati su disco
|
||||||
SaveAuctions();
|
SaveAuctions();
|
||||||
UpdateGlobalControlButtons();
|
UpdateGlobalControlButtons();
|
||||||
|
|
||||||
if (sender != null) // Solo se chiamato dall'utente
|
if (sender != null) // Solo se chiamato dall'utente
|
||||||
{
|
{
|
||||||
Log("[PAUSE ALL] Tutte le aste in pausa", LogLevel.Warning);
|
Log("[OSSERVA] Tutte le aste seguite senza puntare", LogLevel.Warning);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
@@ -187,7 +197,7 @@ namespace AutoBidder
|
|||||||
|
|
||||||
// Conferma rimozione
|
// Conferma rimozione
|
||||||
var result = MessageBox.Show(
|
var result = MessageBox.Show(
|
||||||
$"Rimuovere l'asta dal monitoraggio?\n\n{auctionName}\n(ID: {auctionId})\n\nL'asta verrà eliminata dalla lista e non sarà più monitorata.",
|
$"Rimuovere l'asta dal monitoraggio?\n\n{auctionName}\n(ID: {auctionId})\n\nL'asta verr� eliminata dalla lista e non sar� pi� monitorata.",
|
||||||
"Conferma Rimozione",
|
"Conferma Rimozione",
|
||||||
MessageBoxButton.YesNo,
|
MessageBoxButton.YesNo,
|
||||||
MessageBoxImage.Question);
|
MessageBoxImage.Question);
|
||||||
@@ -216,7 +226,7 @@ namespace AutoBidder
|
|||||||
// ? NUOVO: Sposta il focus sulla riga successiva
|
// ? NUOVO: Sposta il focus sulla riga successiva
|
||||||
if (_auctionViewModels.Count > 0)
|
if (_auctionViewModels.Count > 0)
|
||||||
{
|
{
|
||||||
// Se c'è ancora almeno un'asta nella lista
|
// Se c'� ancora almeno un'asta nella lista
|
||||||
int newIndex;
|
int newIndex;
|
||||||
|
|
||||||
if (currentIndex >= _auctionViewModels.Count)
|
if (currentIndex >= _auctionViewModels.Count)
|
||||||
@@ -278,7 +288,7 @@ namespace AutoBidder
|
|||||||
|
|
||||||
// Conferma rimozione
|
// Conferma rimozione
|
||||||
var result = MessageBox.Show(
|
var result = MessageBox.Show(
|
||||||
$"Rimuovere TUTTE le aste dal monitoraggio?\n\nSono presenti {count} aste monitorate.\n\nTutte le aste verranno eliminate dalla lista e non saranno più monitorate.",
|
$"Rimuovere TUTTE le aste dal monitoraggio?\n\nSono presenti {count} aste monitorate.\n\nTutte le aste verranno eliminate dalla lista e non saranno pi� monitorate.",
|
||||||
"Conferma Rimozione Totale",
|
"Conferma Rimozione Totale",
|
||||||
MessageBoxButton.YesNo,
|
MessageBoxButton.YesNo,
|
||||||
MessageBoxImage.Warning);
|
MessageBoxImage.Warning);
|
||||||
@@ -394,9 +404,10 @@ namespace AutoBidder
|
|||||||
if (string.IsNullOrEmpty(url))
|
if (string.IsNullOrEmpty(url))
|
||||||
url = $"https://it.bidoo.com/auction.php?a=asta_{_selectedAuction.AuctionId}";
|
url = $"https://it.bidoo.com/auction.php?a=asta_{_selectedAuction.AuctionId}";
|
||||||
|
|
||||||
// Naviga alla scheda Browser
|
// Naviga alla scheda Browser, in modalita' browser (non catalogo)
|
||||||
TabBrowser.IsChecked = true;
|
TabBrowser.IsChecked = true;
|
||||||
|
Browser.ShowBrowser();
|
||||||
|
|
||||||
// Naviga all'URL
|
// Naviga all'URL
|
||||||
if (EmbeddedWebView?.CoreWebView2 != null)
|
if (EmbeddedWebView?.CoreWebView2 != null)
|
||||||
{
|
{
|
||||||
@@ -406,7 +417,7 @@ namespace AutoBidder
|
|||||||
else
|
else
|
||||||
{
|
{
|
||||||
Log($"[WARN] Browser interno non ancora inizializzato", LogLevel.Warning);
|
Log($"[WARN] Browser interno non ancora inizializzato", LogLevel.Warning);
|
||||||
MessageBox.Show("Il browser interno non è ancora pronto.\nRiprova tra qualche secondo.", "Browser", MessageBoxButton.OK, MessageBoxImage.Warning);
|
MessageBox.Show("Il browser interno non � ancora pronto.\nRiprova tra qualche secondo.", "Browser", MessageBoxButton.OK, MessageBoxImage.Warning);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
@@ -456,12 +467,12 @@ namespace AutoBidder
|
|||||||
try
|
try
|
||||||
{
|
{
|
||||||
MessageBox.Show(
|
MessageBox.Show(
|
||||||
$"Esportazione singola asta:\n\n{_selectedAuction.Name}\n(ID: {_selectedAuction.AuctionId})\n\nFunzionalità in sviluppo.\nUsa 'Esporta' dalla toolbar per esportare tutte le aste.",
|
$"Esportazione singola asta:\n\n{_selectedAuction.Name}\n(ID: {_selectedAuction.AuctionId})\n\nFunzionalit� in sviluppo.\nUsa 'Esporta' dalla toolbar per esportare tutte le aste.",
|
||||||
"Export Asta",
|
"Export Asta",
|
||||||
MessageBoxButton.OK,
|
MessageBoxButton.OK,
|
||||||
MessageBoxImage.Information);
|
MessageBoxImage.Information);
|
||||||
|
|
||||||
Log($"[INFO] Richiesto export singolo per asta: {_selectedAuction.Name} (funzionalità in sviluppo)", LogLevel.Info);
|
Log($"[INFO] Richiesto export singolo per asta: {_selectedAuction.Name} (funzionalit� in sviluppo)", LogLevel.Info);
|
||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
{
|
{
|
||||||
@@ -503,12 +514,12 @@ namespace AutoBidder
|
|||||||
double shippingCost = auction.ShippingCost ?? 0;
|
double shippingCost = auction.ShippingCost ?? 0;
|
||||||
double totalValue = buyNowPrice + shippingCost;
|
double totalValue = buyNowPrice + shippingCost;
|
||||||
|
|
||||||
// Max EUR = 40% del valore TOTALE (più conservativo del 50%)
|
// Max EUR = 40% del valore TOTALE (pi� conservativo del 50%)
|
||||||
double suggestedMaxPrice = totalValue * 0.40;
|
double suggestedMaxPrice = totalValue * 0.40;
|
||||||
suggestedMaxPrice = Math.Round(suggestedMaxPrice, 2);
|
suggestedMaxPrice = Math.Round(suggestedMaxPrice, 2);
|
||||||
|
|
||||||
// CALCOLA MAX CLICKS (numero massimo puntate conservativo)
|
// CALCOLA MAX CLICKS (numero massimo puntate conservativo)
|
||||||
// Formula: (Valore Totale - Max EUR) / 0.20€ per puntata
|
// Formula: (Valore Totale - Max EUR) / 0.20� per puntata
|
||||||
// Poi riduciamo del 20% per maggiore margine di sicurezza
|
// Poi riduciamo del 20% per maggiore margine di sicurezza
|
||||||
int maxClicksTheoretical = (int)Math.Floor((totalValue - suggestedMaxPrice) / 0.20);
|
int maxClicksTheoretical = (int)Math.Floor((totalValue - suggestedMaxPrice) / 0.20);
|
||||||
int suggestedMaxClicks = (int)Math.Floor(maxClicksTheoretical * 0.80); // 80% del teorico
|
int suggestedMaxClicks = (int)Math.Floor(maxClicksTheoretical * 0.80); // 80% del teorico
|
||||||
@@ -516,12 +527,12 @@ namespace AutoBidder
|
|||||||
// Minimo 10 puntate per dare comunque una chance
|
// Minimo 10 puntate per dare comunque una chance
|
||||||
if (suggestedMaxClicks < 10) suggestedMaxClicks = 10;
|
if (suggestedMaxClicks < 10) suggestedMaxClicks = 10;
|
||||||
|
|
||||||
Log($"[LIMITI] Valore={buyNowPrice:F2}€ + Extra={shippingCost:F2}€ = Tot={totalValue:F2}€ ? MaxEUR={suggestedMaxPrice:F2}€ (40%), MaxClicks={suggestedMaxClicks}", LogLevel.Info);
|
Log($"[LIMITI] Valore={buyNowPrice:F2}� + Extra={shippingCost:F2}� = Tot={totalValue:F2}� ? MaxEUR={suggestedMaxPrice:F2}� (40%), MaxClicks={suggestedMaxClicks}", LogLevel.Info);
|
||||||
|
|
||||||
// CHIEDI CONFERMA
|
// CHIEDI CONFERMA
|
||||||
var result = MessageBox.Show(
|
var result = MessageBox.Show(
|
||||||
$"Limiti suggeriti (conservativi):\n\n" +
|
$"Limiti suggeriti (conservativi):\n\n" +
|
||||||
$"Max EUR: {suggestedMaxPrice:F2}€\n" +
|
$"Max EUR: {suggestedMaxPrice:F2}�\n" +
|
||||||
$"Max Clicks: {suggestedMaxClicks}\n\n" +
|
$"Max Clicks: {suggestedMaxClicks}\n\n" +
|
||||||
$"Applicare questi valori?",
|
$"Applicare questi valori?",
|
||||||
"Conferma Limiti",
|
"Conferma Limiti",
|
||||||
@@ -544,7 +555,7 @@ namespace AutoBidder
|
|||||||
// SALVA
|
// SALVA
|
||||||
SaveAuctions();
|
SaveAuctions();
|
||||||
|
|
||||||
Log($"[LIMITI] Applicati: MaxEUR={suggestedMaxPrice:F2}€, MaxClicks={suggestedMaxClicks}", LogLevel.Success);
|
Log($"[LIMITI] Applicati: MaxEUR={suggestedMaxPrice:F2}�, MaxClicks={suggestedMaxClicks}", LogLevel.Success);
|
||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
{
|
{
|
||||||
@@ -574,8 +585,8 @@ namespace AutoBidder
|
|||||||
|
|
||||||
if (currentIndex <= 0)
|
if (currentIndex <= 0)
|
||||||
{
|
{
|
||||||
// Già in cima o non trovata
|
// Gi� in cima o non trovata
|
||||||
Log($"[MOVE] L'asta è già in cima alla lista", LogLevel.Info);
|
Log($"[MOVE] L'asta � gi� in cima alla lista", LogLevel.Info);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -615,8 +626,8 @@ namespace AutoBidder
|
|||||||
|
|
||||||
if (currentIndex < 0 || currentIndex >= _auctionViewModels.Count - 1)
|
if (currentIndex < 0 || currentIndex >= _auctionViewModels.Count - 1)
|
||||||
{
|
{
|
||||||
// Già in fondo o non trovata
|
// Gi� in fondo o non trovata
|
||||||
Log($"[MOVE] L'asta è già in fondo alla lista", LogLevel.Info);
|
Log($"[MOVE] L'asta � gi� in fondo alla lista", LogLevel.Info);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,405 @@
|
|||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Threading;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
using System.Windows;
|
||||||
|
using AutoBidder.Models;
|
||||||
|
using AutoBidder.Services;
|
||||||
|
using AutoBidder.Utilities;
|
||||||
|
|
||||||
|
namespace AutoBidder
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Catalogo nativo della scheda Esplora.
|
||||||
|
///
|
||||||
|
/// Affianca il browser integrato senza sostituirlo: il browser resta l'unico modo per
|
||||||
|
/// fare il login (da cui il cookie viene importato da solo), questa e' la via veloce
|
||||||
|
/// per confrontare molte aste senza caricare pagine.
|
||||||
|
/// </summary>
|
||||||
|
public partial class MainWindow
|
||||||
|
{
|
||||||
|
private BidooCatalogClient? _catalogClient;
|
||||||
|
|
||||||
|
private readonly List<CatalogCategory> _catalogCategories = new();
|
||||||
|
|
||||||
|
/// <summary>Aste della categoria corrente, prima dei filtri.</summary>
|
||||||
|
private List<CatalogAuction> _catalogAuctions = new();
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Le aste effettivamente mostrate, nell'ordine in cui compaiono. È su queste che
|
||||||
|
/// lavora l'aggiornamento prezzi: aggiornare anche quelle nascoste dai filtri
|
||||||
|
/// costerebbe richieste per numeri che nessuno sta guardando.
|
||||||
|
/// </summary>
|
||||||
|
private List<CatalogAuction> _catalogVisible = new();
|
||||||
|
|
||||||
|
private CatalogCategory? _catalogCategory;
|
||||||
|
private CancellationTokenSource? _catalogCts;
|
||||||
|
private CancellationTokenSource? _catalogRefreshCts;
|
||||||
|
|
||||||
|
/// <summary>Comandi usati dai pulsanti sulle singole schede prodotto.</summary>
|
||||||
|
public RelayCommand? CatalogAddCommand { get; private set; }
|
||||||
|
public RelayCommand? CatalogOpenCommand { get; private set; }
|
||||||
|
public RelayCommand? CatalogWatchCommand { get; private set; }
|
||||||
|
public RelayCommand? CatalogConfigureCommand { get; private set; }
|
||||||
|
|
||||||
|
private BidooCatalogClient CatalogClient
|
||||||
|
{
|
||||||
|
get
|
||||||
|
{
|
||||||
|
if (_catalogClient == null)
|
||||||
|
{
|
||||||
|
_catalogClient = new BidooCatalogClient(_auctionMonitor.GetApiClient().Transport)
|
||||||
|
{
|
||||||
|
Diagnostic = msg => Dispatcher.Invoke(() => Log(msg, LogLevel.Info))
|
||||||
|
};
|
||||||
|
}
|
||||||
|
return _catalogClient;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void InitializeCatalogCommands()
|
||||||
|
{
|
||||||
|
CatalogAddCommand = new RelayCommand(async p => await AddFromCatalogAsync(p as CatalogAuction));
|
||||||
|
CatalogOpenCommand = new RelayCommand(p => OpenCatalogAuction(p as CatalogAuction));
|
||||||
|
CatalogWatchCommand = new RelayCommand(p => ToggleWatchedProduct(p as CatalogAuction));
|
||||||
|
CatalogConfigureCommand = new RelayCommand(p => ConfigureCatalogProduct(p as CatalogAuction));
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>Carica l'elenco categorie la prima volta che si apre la scheda Esplora.</summary>
|
||||||
|
private async Task EnsureCatalogLoadedAsync()
|
||||||
|
{
|
||||||
|
if (_catalogCategories.Count > 0) return;
|
||||||
|
|
||||||
|
// L'interruttore riflette l'impostazione salvata.
|
||||||
|
Browser.SetAutoRefresh(SettingsManager.Load().CatalogAutoRefresh);
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
Browser.SetCatalogMessage("Caricamento categorie…");
|
||||||
|
|
||||||
|
var categories = await CatalogClient.GetCategoriesAsync(false, CancellationToken.None);
|
||||||
|
|
||||||
|
_catalogCategories.Clear();
|
||||||
|
_catalogCategories.AddRange(categories);
|
||||||
|
Browser.SetCategories(_catalogCategories);
|
||||||
|
|
||||||
|
var first = _catalogCategories.FirstOrDefault();
|
||||||
|
if (first != null)
|
||||||
|
{
|
||||||
|
// Prima si registra la categoria corrente, poi si spunta il pulsante:
|
||||||
|
// spuntarlo scatena CategoryChanged, e senza questo ordine partirebbero
|
||||||
|
// due caricamenti concorrenti che si annullano a vicenda.
|
||||||
|
_catalogCategory = first;
|
||||||
|
first.IsSelected = true;
|
||||||
|
|
||||||
|
await LoadCategoryAsync(first);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
Browser.SetCatalogMessage("Nessuna categoria disponibile.");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
Browser.SetCatalogMessage($"Impossibile caricare le categorie: {ex.Message}");
|
||||||
|
Log($"[CATALOGO] Errore categorie: {ex.Message}", LogLevel.Error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task LoadCategoryAsync(CatalogCategory category)
|
||||||
|
{
|
||||||
|
// Una richiesta per volta: cambiare categoria durante un caricamento deve
|
||||||
|
// annullare il precedente, non accodarne un altro.
|
||||||
|
var previous = _catalogCts;
|
||||||
|
var cts = new CancellationTokenSource();
|
||||||
|
_catalogCts = cts;
|
||||||
|
previous?.Cancel();
|
||||||
|
previous?.Dispose();
|
||||||
|
|
||||||
|
_catalogCategory = category;
|
||||||
|
StopCatalogAutoRefresh();
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
Browser.SetCatalogMessage($"Carico \"{category.DisplayName}\"…");
|
||||||
|
|
||||||
|
var settings = SettingsManager.Load();
|
||||||
|
var max = Math.Max(20, settings.CatalogMaxAuctions);
|
||||||
|
var auctions = await CatalogClient.GetAllAuctionsAsync(
|
||||||
|
category, max, cts.Token, settings.CatalogCacheSeconds);
|
||||||
|
if (cts.Token.IsCancellationRequested) return;
|
||||||
|
|
||||||
|
// La pagina HTML da sola darebbe sempre 0,01 € e il timer di partenza:
|
||||||
|
// un secondo giro su data.php porta prezzi e scadenze reali.
|
||||||
|
if (auctions.Count > 0)
|
||||||
|
await CatalogClient.UpdateStatesAsync(auctions, cts.Token);
|
||||||
|
|
||||||
|
if (cts.Token.IsCancellationRequested) return;
|
||||||
|
|
||||||
|
MarkWatchedProducts(auctions);
|
||||||
|
|
||||||
|
_catalogAuctions = auctions;
|
||||||
|
RebuildCatalogView();
|
||||||
|
StartCatalogAutoRefresh();
|
||||||
|
}
|
||||||
|
catch (OperationCanceledException)
|
||||||
|
{
|
||||||
|
// Sostituita da una richiesta piu' recente: nessun messaggio, e' normale.
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
Browser.SetCatalogMessage($"Errore: {ex.Message}");
|
||||||
|
Log($"[CATALOGO] {ex.Message}", LogLevel.Error);
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
if (ReferenceEquals(_catalogCts, cts))
|
||||||
|
{
|
||||||
|
_catalogCts = null;
|
||||||
|
cts.Dispose();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Segna quali schede appartengono a un prodotto gia' in elenco, distinguendo
|
||||||
|
/// "seguito" (stellina) da "solo configurato" (ingranaggio acceso).
|
||||||
|
/// </summary>
|
||||||
|
private static void MarkWatchedProducts(IEnumerable<CatalogAuction> auctions)
|
||||||
|
{
|
||||||
|
var products = WatchedProductsStore.GetAll();
|
||||||
|
|
||||||
|
foreach (var auction in auctions)
|
||||||
|
{
|
||||||
|
var rule = products.FirstOrDefault(p => p.Matches(auction));
|
||||||
|
auction.IsWatched = rule?.IsWatched == true;
|
||||||
|
auction.IsListed = rule != null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Ricostruisce (e riordina) l'elenco mostrato. Si chiama SOLO al caricamento o
|
||||||
|
/// quando cambiano i filtri: riordinare mentre si legge farebbe saltare le schede
|
||||||
|
/// da una posizione all'altra sotto il cursore.
|
||||||
|
/// </summary>
|
||||||
|
private void RebuildCatalogView()
|
||||||
|
{
|
||||||
|
var filter = Browser.SearchText;
|
||||||
|
var hideManual = Browser.HideManualAuctions;
|
||||||
|
|
||||||
|
var view = _catalogAuctions
|
||||||
|
.Where(a => !hideManual || !a.IsManualOnly)
|
||||||
|
.Where(a => filter.Length == 0 || a.Name.Contains(filter, StringComparison.OrdinalIgnoreCase))
|
||||||
|
// Le aste che stanno per chiudere sono quelle su cui si decide: prima.
|
||||||
|
.OrderBy(a => a.RemainingSeconds <= 0 ? int.MaxValue : a.RemainingSeconds)
|
||||||
|
.ToList();
|
||||||
|
|
||||||
|
_catalogVisible = view;
|
||||||
|
Browser.SetCatalogItems(view, _catalogAuctions.Count);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Aggiornamento prezzi sul posto ───────────────────────────────
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Quante aste tenere aggiornate mentre si guarda la griglia. Il listato si sfoglia
|
||||||
|
/// ormai a migliaia, e <c>data.php</c> ne accetta una sessantina per chiamata: senza
|
||||||
|
/// un tetto, un catalogo grande significherebbe decine di richieste ogni due secondi
|
||||||
|
/// per aggiornare aste che chiudono fra ore.
|
||||||
|
/// </summary>
|
||||||
|
private const int CatalogRefreshBudget = 300;
|
||||||
|
|
||||||
|
private void StartCatalogAutoRefresh()
|
||||||
|
{
|
||||||
|
StopCatalogAutoRefresh();
|
||||||
|
|
||||||
|
if (!Browser.AutoRefreshEnabled || _catalogAuctions.Count == 0) return;
|
||||||
|
|
||||||
|
var cts = new CancellationTokenSource();
|
||||||
|
_catalogRefreshCts = cts;
|
||||||
|
_ = CatalogRefreshLoopAsync(cts.Token);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void StopCatalogAutoRefresh()
|
||||||
|
{
|
||||||
|
var cts = _catalogRefreshCts;
|
||||||
|
_catalogRefreshCts = null;
|
||||||
|
|
||||||
|
cts?.Cancel();
|
||||||
|
cts?.Dispose();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Aggiorna prezzi e timer delle aste in vista. I valori cambiano <i>sul posto</i>:
|
||||||
|
/// nessun riordino e nessuna ricostruzione della lista, altrimenti le schede
|
||||||
|
/// ballerebbero mentre le si guarda.
|
||||||
|
///
|
||||||
|
/// <para>Si aggiornano le prime <see cref="CatalogRefreshBudget"/> della griglia,
|
||||||
|
/// che essendo ordinata per scadenza sono quelle che stanno per chiudere: sono le
|
||||||
|
/// uniche i cui numeri cambiano da un momento all'altro.</para>
|
||||||
|
/// </summary>
|
||||||
|
private async Task CatalogRefreshLoopAsync(CancellationToken ct)
|
||||||
|
{
|
||||||
|
while (!ct.IsCancellationRequested)
|
||||||
|
{
|
||||||
|
if (!await Wait.DelayAsync(2000, ct).ConfigureAwait(false)) return;
|
||||||
|
|
||||||
|
var visible = _catalogVisible;
|
||||||
|
if (visible.Count == 0) continue;
|
||||||
|
|
||||||
|
var snapshot = visible.Count > CatalogRefreshBudget
|
||||||
|
? visible.GetRange(0, CatalogRefreshBudget)
|
||||||
|
: visible;
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
await CatalogClient.UpdateStatesAsync(snapshot, ct).ConfigureAwait(false);
|
||||||
|
}
|
||||||
|
catch (OperationCanceledException)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
catch
|
||||||
|
{
|
||||||
|
// Un aggiornamento saltato non compromette la pagina.
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (ct.IsCancellationRequested) return;
|
||||||
|
|
||||||
|
await Dispatcher.InvokeAsync(() =>
|
||||||
|
{
|
||||||
|
foreach (var auction in snapshot) auction.NotifyStateChanged();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Azioni sulle schede ──────────────────────────────────────────
|
||||||
|
|
||||||
|
private async Task AddFromCatalogAsync(CatalogAuction? auction)
|
||||||
|
{
|
||||||
|
if (auction == null) return;
|
||||||
|
|
||||||
|
if (_auctionViewModels.Any(a => a.AuctionId == auction.AuctionId))
|
||||||
|
{
|
||||||
|
Log($"[CATALOGO] {auction.Name} è già nel monitor", LogLevel.Info);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
await AddAuctionById(auction.Url);
|
||||||
|
WatchedProductsStore.MarkHandled(auction.AuctionId);
|
||||||
|
Log($"[CATALOGO] Aggiunta al monitor: {auction.Name}", LogLevel.Info);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void OpenCatalogAuction(CatalogAuction? auction)
|
||||||
|
{
|
||||||
|
if (auction == null) return;
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
// Passa alla scheda Browser: TabBrowser_Checked mostra il pannello e il
|
||||||
|
// browser integrato, poi si naviga all'asta.
|
||||||
|
TabBrowser.IsChecked = true;
|
||||||
|
Browser.ShowBrowser();
|
||||||
|
Browser.EmbeddedWebView?.CoreWebView2?.Navigate(auction.Url);
|
||||||
|
Browser.BrowserAddress.Text = auction.Url;
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
Log($"[CATALOGO] Apertura nel browser fallita: {ex.Message}", LogLevel.Error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Attiva o disattiva la sorveglianza del prodotto (stellina). Da qui in poi le aste
|
||||||
|
/// nuove dello stesso articolo entrano nel monitor da sole.
|
||||||
|
/// </summary>
|
||||||
|
private void ToggleWatchedProduct(CatalogAuction? auction)
|
||||||
|
{
|
||||||
|
if (auction == null) return;
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
if (auction.IsWatched)
|
||||||
|
{
|
||||||
|
// Spegnere la stellina non butta via i limiti scritti a mano: la scheda
|
||||||
|
// resta fra i Prodotti, solo senza aggiunta automatica.
|
||||||
|
WatchedProductsStore.SetWatched(auction, false);
|
||||||
|
Log($"[SEGUITI] Non seguo più: {auction.Name}", LogLevel.Info);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
WatchedProductsStore.SetWatched(auction, true);
|
||||||
|
|
||||||
|
var settings = SettingsManager.Load();
|
||||||
|
if (settings.AutoAddProductsEnabled)
|
||||||
|
{
|
||||||
|
Log($"[SEGUITI] Ora seguo: {auction.Name} — le aste nuove entreranno in stato {StateLabel(settings.AutoAddNewAuctionState)}",
|
||||||
|
LogLevel.Success);
|
||||||
|
_ = _productWatcher?.ScanNowAsync();
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
Log($"[SEGUITI] Ora seguo: {auction.Name} — l'aggiunta automatica è però disattivata in Impostazioni",
|
||||||
|
LogLevel.Warning);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Tutte le schede dello stesso prodotto cambiano stella insieme.
|
||||||
|
MarkWatchedProducts(_catalogAuctions);
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
Log($"[SEGUITI] Errore: {ex.Message}", LogLevel.Error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static string StateLabel(string state) => state switch
|
||||||
|
{
|
||||||
|
"Active" => "Attiva",
|
||||||
|
"Stopped" => "Ferma",
|
||||||
|
_ => "Osserva"
|
||||||
|
};
|
||||||
|
|
||||||
|
// ── Handler degli eventi del controllo ───────────────────────────
|
||||||
|
|
||||||
|
private async void Browser_CatalogCategoryChanged(object sender, RoutedEventArgs e)
|
||||||
|
{
|
||||||
|
var selected = _catalogCategories.FirstOrDefault(c => c.IsSelected);
|
||||||
|
if (selected == null || ReferenceEquals(selected, _catalogCategory)) return;
|
||||||
|
|
||||||
|
await LoadCategoryAsync(selected);
|
||||||
|
}
|
||||||
|
|
||||||
|
private async void Browser_CatalogRefreshClicked(object sender, RoutedEventArgs e)
|
||||||
|
{
|
||||||
|
// Chi preme "Aggiorna" vuole i dati dal server, non quelli in cache.
|
||||||
|
CatalogClient.InvalidateCache();
|
||||||
|
|
||||||
|
if (_catalogCategory == null)
|
||||||
|
{
|
||||||
|
_catalogCategories.Clear();
|
||||||
|
await EnsureCatalogLoadedAsync();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
await LoadCategoryAsync(_catalogCategory);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void Browser_CatalogSearchChanged(object sender, RoutedEventArgs e)
|
||||||
|
{
|
||||||
|
// Filtri cambiati: qui il riordino ci sta, è l'utente ad averlo chiesto.
|
||||||
|
RebuildCatalogView();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void Browser_CatalogAutoRefreshChanged(object sender, RoutedEventArgs e)
|
||||||
|
{
|
||||||
|
var settings = SettingsManager.Load();
|
||||||
|
settings.CatalogAutoRefresh = Browser.AutoRefreshEnabled;
|
||||||
|
SettingsManager.Save(settings);
|
||||||
|
|
||||||
|
if (Browser.AutoRefreshEnabled) StartCatalogAutoRefresh();
|
||||||
|
else StopCatalogAutoRefresh();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
using System.Linq;
|
using System.Linq;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
using System.Windows;
|
using System.Windows;
|
||||||
using AutoBidder.Utilities;
|
using AutoBidder.Utilities;
|
||||||
@@ -21,21 +21,24 @@ namespace AutoBidder
|
|||||||
GridPauseCommand = new RelayCommand(param => ExecuteGridPause(param as AuctionViewModel));
|
GridPauseCommand = new RelayCommand(param => ExecuteGridPause(param as AuctionViewModel));
|
||||||
GridStopCommand = new RelayCommand(param => ExecuteGridStop(param as AuctionViewModel));
|
GridStopCommand = new RelayCommand(param => ExecuteGridStop(param as AuctionViewModel));
|
||||||
GridBidCommand = new RelayCommand(async param => await ExecuteGridBidAsync(param as AuctionViewModel));
|
GridBidCommand = new RelayCommand(async param => await ExecuteGridBidAsync(param as AuctionViewModel));
|
||||||
|
|
||||||
|
InitializeCatalogCommands();
|
||||||
|
InitializeProductCommands();
|
||||||
}
|
}
|
||||||
|
|
||||||
private void ExecuteStartAll()
|
private void ExecuteStartAll()
|
||||||
{
|
{
|
||||||
StartButton_Click(null, null);
|
StartButton_Click(null, new RoutedEventArgs());
|
||||||
}
|
}
|
||||||
|
|
||||||
private void ExecuteStopAll()
|
private void ExecuteStopAll()
|
||||||
{
|
{
|
||||||
StopButton_Click(null, null);
|
StopButton_Click(null, new RoutedEventArgs());
|
||||||
}
|
}
|
||||||
|
|
||||||
private void ExecutePauseAll()
|
private void ExecutePauseAll()
|
||||||
{
|
{
|
||||||
PauseAllButton_Click(null, null);
|
PauseAllButton_Click(null, new RoutedEventArgs());
|
||||||
}
|
}
|
||||||
|
|
||||||
private void ExecuteGridStart(AuctionViewModel? vm)
|
private void ExecuteGridStart(AuctionViewModel? vm)
|
||||||
@@ -46,7 +49,7 @@ namespace AutoBidder
|
|||||||
vm.IsActive = true;
|
vm.IsActive = true;
|
||||||
vm.IsPaused = false;
|
vm.IsPaused = false;
|
||||||
|
|
||||||
// Se il monitoraggio globale non è attivo, avvialo automaticamente
|
// Se il monitoraggio globale non � attivo, avvialo automaticamente
|
||||||
if (!_isAutomationActive)
|
if (!_isAutomationActive)
|
||||||
{
|
{
|
||||||
_auctionMonitor.Start();
|
_auctionMonitor.Start();
|
||||||
@@ -66,9 +69,20 @@ namespace AutoBidder
|
|||||||
private void ExecuteGridPause(AuctionViewModel? vm)
|
private void ExecuteGridPause(AuctionViewModel? vm)
|
||||||
{
|
{
|
||||||
if (vm == null) return;
|
if (vm == null) return;
|
||||||
|
|
||||||
|
// Osserva significa "segui ma non puntare": serve comunque un motore acceso,
|
||||||
|
// quindi l'asta va resa attiva anche se partiva da ferma.
|
||||||
|
vm.IsActive = true;
|
||||||
vm.IsPaused = true;
|
vm.IsPaused = true;
|
||||||
Log($"[PAUSA] Asta in pausa: {vm.Name}", LogLevel.Info);
|
|
||||||
|
if (!_isAutomationActive)
|
||||||
|
{
|
||||||
|
_auctionMonitor.Start();
|
||||||
|
_isAutomationActive = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
Log($"[OSSERVA] Asta seguita senza puntare: {vm.Name}", LogLevel.Info);
|
||||||
|
|
||||||
// ? Salva gli stati aggiornati su disco
|
// ? Salva gli stati aggiornati su disco
|
||||||
SaveAuctions();
|
SaveAuctions();
|
||||||
UpdateGlobalControlButtons();
|
UpdateGlobalControlButtons();
|
||||||
|
|||||||
@@ -30,7 +30,7 @@ namespace AutoBidder
|
|||||||
|
|
||||||
if (session != null && !string.IsNullOrEmpty(session.Username))
|
if (session != null && !string.IsNullOrEmpty(session.Username))
|
||||||
{
|
{
|
||||||
// Già connesso - Mostra opzioni
|
// Gi� connesso - Mostra opzioni
|
||||||
var result = MessageBox.Show(
|
var result = MessageBox.Show(
|
||||||
this,
|
this,
|
||||||
$"Connesso come: {session.Username}\n" +
|
$"Connesso come: {session.Username}\n" +
|
||||||
@@ -54,14 +54,15 @@ namespace AutoBidder
|
|||||||
this,
|
this,
|
||||||
"Per accedere:\n\n" +
|
"Per accedere:\n\n" +
|
||||||
"1. Fai login su Bidoo nella scheda Browser\n" +
|
"1. Fai login su Bidoo nella scheda Browser\n" +
|
||||||
"2. La connessione sarà automatica\n\n" +
|
"2. La connessione sar� automatica\n\n" +
|
||||||
"Apertura scheda Browser...",
|
"Apertura scheda Browser...",
|
||||||
"Accedi a Bidoo",
|
"Accedi a Bidoo",
|
||||||
MessageBoxButton.OK,
|
MessageBoxButton.OK,
|
||||||
MessageBoxImage.Information);
|
MessageBoxImage.Information);
|
||||||
|
|
||||||
// Apri tab Browser
|
// Apri tab Browser, in modalita' browser: il login passa da li'
|
||||||
TabBrowser.IsChecked = true;
|
TabBrowser.IsChecked = true;
|
||||||
|
Browser.ShowBrowser();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
@@ -70,6 +71,103 @@ namespace AutoBidder
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ===== SEZIONE SESSIONE NELLE IMPOSTAZIONI =====
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Aggiorna il riquadro di stato sessione nelle Impostazioni.
|
||||||
|
/// </summary>
|
||||||
|
private void RefreshSettingsSessionStatus()
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var session = _sessionService?.GetCurrentSession();
|
||||||
|
bool connected = session != null && !string.IsNullOrEmpty(session.Username);
|
||||||
|
|
||||||
|
Settings.SetSessionStatus(
|
||||||
|
connected,
|
||||||
|
session?.Username,
|
||||||
|
session?.RemainingBids ?? 0,
|
||||||
|
session?.ShopCredit ?? 0);
|
||||||
|
}
|
||||||
|
catch { }
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// "Verifica connessione": ricontrolla contro il sito la sessione già in uso.
|
||||||
|
///
|
||||||
|
/// <para>Non chiede più un cookie da incollare: l'accesso si fa una volta dal
|
||||||
|
/// browser integrato e vale per tutta l'applicazione. Questo pulsante serve a
|
||||||
|
/// rispondere alla domanda che ci si fa davvero — "sono ancora connesso?" — che
|
||||||
|
/// prima si poteva solo dedurre da un'asta che smetteva di funzionare.</para>
|
||||||
|
/// </summary>
|
||||||
|
private async void Settings_ConnectSessionClicked(object sender, RoutedEventArgs e)
|
||||||
|
{
|
||||||
|
var cookie = _auctionMonitor.GetApiClient().Transport.Cookie;
|
||||||
|
|
||||||
|
if (string.IsNullOrWhiteSpace(cookie))
|
||||||
|
{
|
||||||
|
MessageBox.Show(this,
|
||||||
|
"Nessuna sessione da verificare.\n\n" +
|
||||||
|
"Accedi a Bidoo dalla scheda Browser: il cookie viene rilevato e importato da solo.",
|
||||||
|
"Non connesso", MessageBoxButton.OK, MessageBoxImage.Information);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
Log("[SESSION] Verifica della sessione in corso...", Utilities.LogLevel.Info);
|
||||||
|
|
||||||
|
var result = await _sessionService.ValidateAndActivateSessionAsync(cookie);
|
||||||
|
|
||||||
|
if (result.Success && result.Session != null)
|
||||||
|
{
|
||||||
|
_sessionService.SaveSession(result.Session);
|
||||||
|
SetUserBanner(result.Session.Username, result.Session.RemainingBids);
|
||||||
|
RefreshSettingsSessionStatus();
|
||||||
|
|
||||||
|
Log($"[SESSION] Connesso come {result.Session.Username} " +
|
||||||
|
$"({result.Session.RemainingBids} puntate)", Utilities.LogLevel.Success);
|
||||||
|
|
||||||
|
MessageBox.Show(this,
|
||||||
|
$"Connesso come: {result.Session.Username}\n" +
|
||||||
|
$"Puntate residue: {result.Session.RemainingBids}\n" +
|
||||||
|
$"Credito Shop: EUR {result.Session.ShopCredit:F2}",
|
||||||
|
"Sessione attiva", MessageBoxButton.OK, MessageBoxImage.Information);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
Log($"[SESSION] Sessione non più valida: {result.ErrorMessage}", Utilities.LogLevel.Error);
|
||||||
|
RefreshSettingsSessionStatus();
|
||||||
|
|
||||||
|
MessageBox.Show(this,
|
||||||
|
"La sessione non è più valida.\n\n" +
|
||||||
|
(result.ErrorMessage ?? "Cookie scaduto.") +
|
||||||
|
"\n\nRiaccedi a Bidoo dalla scheda Browser.",
|
||||||
|
"Sessione scaduta", MessageBoxButton.OK, MessageBoxImage.Warning);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
Log($"[ERRORE] Verifica sessione: {ex.Message}", Utilities.LogLevel.Error);
|
||||||
|
MessageBox.Show(this, "Errore durante la verifica: " + ex.Message,
|
||||||
|
"Errore", MessageBoxButton.OK, MessageBoxImage.Error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>"Disconnetti" dalla sezione Impostazioni.</summary>
|
||||||
|
private void Settings_DisconnectSessionClicked(object sender, RoutedEventArgs e)
|
||||||
|
{
|
||||||
|
DisconnectSession();
|
||||||
|
RefreshSettingsSessionStatus();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>"Apri Browser per il login": passa alla scheda Browser.</summary>
|
||||||
|
private void Settings_OpenBrowserLoginClicked(object sender, RoutedEventArgs e)
|
||||||
|
{
|
||||||
|
TabBrowser.IsChecked = true;
|
||||||
|
Browser.ShowBrowser();
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Disconnette la sessione corrente
|
/// Disconnette la sessione corrente
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
using System.Windows;
|
using System.Windows;
|
||||||
|
|
||||||
namespace AutoBidder
|
namespace AutoBidder
|
||||||
{
|
{
|
||||||
@@ -17,16 +17,41 @@ namespace AutoBidder
|
|||||||
private void TabBrowser_Checked(object sender, RoutedEventArgs e)
|
private void TabBrowser_Checked(object sender, RoutedEventArgs e)
|
||||||
{
|
{
|
||||||
ShowPanel(Browser);
|
ShowPanel(Browser);
|
||||||
|
Browser.ShowBrowser();
|
||||||
|
}
|
||||||
|
|
||||||
|
private async void TabCerca_Checked(object sender, RoutedEventArgs e)
|
||||||
|
{
|
||||||
|
ShowPanel(Browser);
|
||||||
|
Browser.ShowCatalog();
|
||||||
|
await EnsureCatalogLoadedAsync();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void TabProdotti_Checked(object sender, RoutedEventArgs e)
|
||||||
|
{
|
||||||
|
ShowPanel(Products);
|
||||||
|
LoadProducts();
|
||||||
}
|
}
|
||||||
|
|
||||||
private void TabPuntateGratis_Checked(object sender, RoutedEventArgs e)
|
private void TabPuntateGratis_Checked(object sender, RoutedEventArgs e)
|
||||||
{
|
{
|
||||||
ShowPanel(PuntateGratisPanel);
|
ShowPanel(FreeBids);
|
||||||
|
RefreshFreeBidsPanel();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void TabEsporta_Checked(object sender, RoutedEventArgs e)
|
||||||
|
{
|
||||||
|
ShowPanel(Export);
|
||||||
|
|
||||||
|
// Il conteggio si rifà all'apertura: nel frattempo possono essersi concluse
|
||||||
|
// altre aste, e un numero vecchio farebbe scegliere i filtri al buio.
|
||||||
|
Export.RefreshPreview();
|
||||||
}
|
}
|
||||||
|
|
||||||
private void TabDatiStatistici_Checked(object sender, RoutedEventArgs e)
|
private void TabDatiStatistici_Checked(object sender, RoutedEventArgs e)
|
||||||
{
|
{
|
||||||
ShowPanel(StatisticsPanel);
|
ShowPanel(StatisticsPanel);
|
||||||
|
LoadStatistics();
|
||||||
}
|
}
|
||||||
|
|
||||||
private void TabImpostazioni_Checked(object sender, RoutedEventArgs e)
|
private void TabImpostazioni_Checked(object sender, RoutedEventArgs e)
|
||||||
@@ -38,8 +63,9 @@ namespace AutoBidder
|
|||||||
|
|
||||||
// Carica impostazioni quando si apre la tab
|
// Carica impostazioni quando si apre la tab
|
||||||
LoadDefaultSettings();
|
LoadDefaultSettings();
|
||||||
|
|
||||||
// NOTA: Caricamento cookie RIMOSSO - ora automatico tramite browser
|
// Aggiorna il riquadro di stato della sezione Sessione
|
||||||
|
RefreshSettingsSessionStatus();
|
||||||
}
|
}
|
||||||
catch { }
|
catch { }
|
||||||
}
|
}
|
||||||
@@ -47,14 +73,17 @@ namespace AutoBidder
|
|||||||
private void ShowPanel(System.Windows.UIElement? panelToShow)
|
private void ShowPanel(System.Windows.UIElement? panelToShow)
|
||||||
{
|
{
|
||||||
// Prevent NullReferenceException during initialization
|
// Prevent NullReferenceException during initialization
|
||||||
if (AuctionMonitor == null || Browser == null || StatisticsPanel == null || Settings == null || PuntateGratisPanel == null)
|
if (AuctionMonitor == null || Browser == null || StatisticsPanel == null ||
|
||||||
|
Settings == null || FreeBids == null || Products == null || Export == null)
|
||||||
return;
|
return;
|
||||||
|
|
||||||
// Hide all panels
|
// Hide all panels
|
||||||
AuctionMonitor.Visibility = Visibility.Collapsed;
|
AuctionMonitor.Visibility = Visibility.Collapsed;
|
||||||
Browser.Visibility = Visibility.Collapsed;
|
Browser.Visibility = Visibility.Collapsed;
|
||||||
PuntateGratisPanel.Visibility = Visibility.Collapsed;
|
Products.Visibility = Visibility.Collapsed;
|
||||||
|
FreeBids.Visibility = Visibility.Collapsed;
|
||||||
StatisticsPanel.Visibility = Visibility.Collapsed;
|
StatisticsPanel.Visibility = Visibility.Collapsed;
|
||||||
|
Export.Visibility = Visibility.Collapsed;
|
||||||
Settings.Visibility = Visibility.Collapsed;
|
Settings.Visibility = Visibility.Collapsed;
|
||||||
|
|
||||||
// Show selected panel
|
// Show selected panel
|
||||||
@@ -93,8 +122,8 @@ namespace AutoBidder
|
|||||||
}
|
}
|
||||||
|
|
||||||
// TODO: Implementare dialog export con scelta formato
|
// TODO: Implementare dialog export con scelta formato
|
||||||
System.Windows.MessageBox.Show($"Export di {auctions.Count} aste.\n\nFunzionalità in sviluppo.\nUsa le impostazioni nella scheda Impostazioni per configurare l'export.", "Export Aste", System.Windows.MessageBoxButton.OK, System.Windows.MessageBoxImage.Information);
|
System.Windows.MessageBox.Show($"Export di {auctions.Count} aste.\n\nFunzionalit� in sviluppo.\nUsa le impostazioni nella scheda Impostazioni per configurare l'export.", "Export Aste", System.Windows.MessageBoxButton.OK, System.Windows.MessageBoxImage.Information);
|
||||||
Log($"[INFO] Richiesto export di {auctions.Count} aste (funzionalità in sviluppo)", Utilities.LogLevel.Info);
|
Log($"[INFO] Richiesto export di {auctions.Count} aste (funzionalit� in sviluppo)", Utilities.LogLevel.Info);
|
||||||
}
|
}
|
||||||
catch (System.Exception ex)
|
catch (System.Exception ex)
|
||||||
{
|
{
|
||||||
@@ -138,7 +167,7 @@ namespace AutoBidder
|
|||||||
var auction = selected.AuctionInfo;
|
var auction = selected.AuctionInfo;
|
||||||
bool hasGenericName = auction.Name.StartsWith("Asta ") &&
|
bool hasGenericName = auction.Name.StartsWith("Asta ") &&
|
||||||
!auction.Name.Contains("Shop") &&
|
!auction.Name.Contains("Shop") &&
|
||||||
!auction.Name.Contains("€") &&
|
!auction.Name.Contains("�") &&
|
||||||
!auction.Name.Contains("Buono") &&
|
!auction.Name.Contains("Buono") &&
|
||||||
!auction.Name.Contains("Carburante");
|
!auction.Name.Contains("Carburante");
|
||||||
|
|
||||||
@@ -209,7 +238,11 @@ namespace AutoBidder
|
|||||||
|
|
||||||
private void AuctionMonitor_BidBeforeDeadlineMsChanged(object sender, RoutedEventArgs e)
|
private void AuctionMonitor_BidBeforeDeadlineMsChanged(object sender, RoutedEventArgs e)
|
||||||
{
|
{
|
||||||
// Gestito internamente dal binding WPF
|
// Riempire i campi alla selezione di un'asta scatena i TextChanged:
|
||||||
|
// senza questa guardia ogni selezione riscriveva auctions.json quattro volte
|
||||||
|
// con gli stessi valori appena letti.
|
||||||
|
if (_isUpdatingSelection) return;
|
||||||
|
|
||||||
if (_selectedAuction != null && int.TryParse(AuctionMonitor.SelectedBidBeforeDeadlineMs.Text, out int ms))
|
if (_selectedAuction != null && int.TryParse(AuctionMonitor.SelectedBidBeforeDeadlineMs.Text, out int ms))
|
||||||
{
|
{
|
||||||
_selectedAuction.AuctionInfo.BidBeforeDeadlineMs = ms;
|
_selectedAuction.AuctionInfo.BidBeforeDeadlineMs = ms;
|
||||||
@@ -217,19 +250,13 @@ namespace AutoBidder
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private void AuctionMonitor_CheckAuctionOpenChanged(object sender, RoutedEventArgs e)
|
|
||||||
{
|
|
||||||
// Gestito internamente dal binding WPF
|
|
||||||
if (_selectedAuction != null)
|
|
||||||
{
|
|
||||||
_selectedAuction.AuctionInfo.CheckAuctionOpenBeforeBid = AuctionMonitor.SelectedCheckAuctionOpen.IsChecked ?? false;
|
|
||||||
SaveAuctions();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private void AuctionMonitor_MinPriceChanged(object sender, RoutedEventArgs e)
|
private void AuctionMonitor_MinPriceChanged(object sender, RoutedEventArgs e)
|
||||||
{
|
{
|
||||||
// Gestito internamente dal binding WPF
|
// Riempire i campi alla selezione di un'asta scatena i TextChanged:
|
||||||
|
// senza questa guardia ogni selezione riscriveva auctions.json quattro volte
|
||||||
|
// con gli stessi valori appena letti.
|
||||||
|
if (_isUpdatingSelection) return;
|
||||||
|
|
||||||
if (_selectedAuction != null && double.TryParse(AuctionMonitor.SelectedMinPrice.Text, System.Globalization.NumberStyles.Float, System.Globalization.CultureInfo.InvariantCulture, out double price))
|
if (_selectedAuction != null && double.TryParse(AuctionMonitor.SelectedMinPrice.Text, System.Globalization.NumberStyles.Float, System.Globalization.CultureInfo.InvariantCulture, out double price))
|
||||||
{
|
{
|
||||||
_selectedAuction.MinPrice = price;
|
_selectedAuction.MinPrice = price;
|
||||||
@@ -239,7 +266,11 @@ namespace AutoBidder
|
|||||||
|
|
||||||
private void AuctionMonitor_MaxPriceChanged(object sender, RoutedEventArgs e)
|
private void AuctionMonitor_MaxPriceChanged(object sender, RoutedEventArgs e)
|
||||||
{
|
{
|
||||||
// Gestito internamente dal binding WPF
|
// Riempire i campi alla selezione di un'asta scatena i TextChanged:
|
||||||
|
// senza questa guardia ogni selezione riscriveva auctions.json quattro volte
|
||||||
|
// con gli stessi valori appena letti.
|
||||||
|
if (_isUpdatingSelection) return;
|
||||||
|
|
||||||
if (_selectedAuction != null && double.TryParse(AuctionMonitor.SelectedMaxPrice.Text, System.Globalization.NumberStyles.Float, System.Globalization.CultureInfo.InvariantCulture, out double price))
|
if (_selectedAuction != null && double.TryParse(AuctionMonitor.SelectedMaxPrice.Text, System.Globalization.NumberStyles.Float, System.Globalization.CultureInfo.InvariantCulture, out double price))
|
||||||
{
|
{
|
||||||
_selectedAuction.MaxPrice = price;
|
_selectedAuction.MaxPrice = price;
|
||||||
@@ -249,7 +280,11 @@ namespace AutoBidder
|
|||||||
|
|
||||||
private void AuctionMonitor_MaxClicksChanged(object sender, RoutedEventArgs e)
|
private void AuctionMonitor_MaxClicksChanged(object sender, RoutedEventArgs e)
|
||||||
{
|
{
|
||||||
// Gestito internamente dal binding WPF
|
// Riempire i campi alla selezione di un'asta scatena i TextChanged:
|
||||||
|
// senza questa guardia ogni selezione riscriveva auctions.json quattro volte
|
||||||
|
// con gli stessi valori appena letti.
|
||||||
|
if (_isUpdatingSelection) return;
|
||||||
|
|
||||||
if (_selectedAuction != null && int.TryParse(AuctionMonitor.SelectedMaxClicks.Text, out int clicks))
|
if (_selectedAuction != null && int.TryParse(AuctionMonitor.SelectedMaxClicks.Text, out int clicks))
|
||||||
{
|
{
|
||||||
_selectedAuction.MaxClicks = clicks;
|
_selectedAuction.MaxClicks = clicks;
|
||||||
@@ -257,6 +292,35 @@ namespace AutoBidder
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Tetto di spesa in euro per l'asta selezionata. E' il fratello di MaxClicks detto
|
||||||
|
/// in denaro: chi ragiona in euro non deve fare la divisione a mente.
|
||||||
|
/// </summary>
|
||||||
|
private void AuctionMonitor_MaxSpendChanged(object sender, RoutedEventArgs e)
|
||||||
|
{
|
||||||
|
if (_isUpdatingSelection) return;
|
||||||
|
|
||||||
|
if (_selectedAuction == null) return;
|
||||||
|
|
||||||
|
var testo = AuctionMonitor.SelectedMaxSpend.Text.Trim().Replace(',', '.');
|
||||||
|
if (double.TryParse(testo, System.Globalization.NumberStyles.Any,
|
||||||
|
System.Globalization.CultureInfo.InvariantCulture, out var spesa) && spesa >= 0)
|
||||||
|
{
|
||||||
|
_selectedAuction.AuctionInfo.MaxTotalSpendEuro = spesa;
|
||||||
|
SaveAuctions();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>Interruttore del controllo di pareggio sull'asta selezionata.</summary>
|
||||||
|
private void AuctionMonitor_BreakEvenChanged(object sender, RoutedEventArgs e)
|
||||||
|
{
|
||||||
|
if (_isUpdatingSelection) return;
|
||||||
|
if (_selectedAuction == null) return;
|
||||||
|
|
||||||
|
_selectedAuction.AuctionInfo.StopAtBreakEven = AuctionMonitor.SelectedStopAtBreakEven.IsChecked == true;
|
||||||
|
SaveAuctions();
|
||||||
|
}
|
||||||
|
|
||||||
// ===== BROWSER CONTROL EVENTS =====
|
// ===== BROWSER CONTROL EVENTS =====
|
||||||
|
|
||||||
private void Browser_BrowserBackClicked(object sender, RoutedEventArgs e)
|
private void Browser_BrowserBackClicked(object sender, RoutedEventArgs e)
|
||||||
|
|||||||
@@ -0,0 +1,257 @@
|
|||||||
|
using System;
|
||||||
|
using System.Diagnostics;
|
||||||
|
using System.IO;
|
||||||
|
using System.Windows;
|
||||||
|
using AutoBidder.Utilities;
|
||||||
|
|
||||||
|
namespace AutoBidder
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Cartelle dei dati, misura dell'anticipo ed esportazione dello storico.
|
||||||
|
/// </summary>
|
||||||
|
public partial class MainWindow
|
||||||
|
{
|
||||||
|
// ── Anticipo: misura e consiglio ─────────────────────────────────
|
||||||
|
|
||||||
|
private void RefreshBidLeadSummary()
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var s = BidLeadStats.Summarize();
|
||||||
|
|
||||||
|
if (!s.HasData)
|
||||||
|
{
|
||||||
|
Settings.SetBidLeadSummary(
|
||||||
|
"Nessuna puntata registrata finora. Le misure si raccolgono da sole a ogni puntata.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
Settings.SetBidLeadSummary(
|
||||||
|
$"{s.Count} puntate misurate — {s.Successes} riuscite ({s.SuccessRate:F0}%), {s.TooLate} tardive.\n" +
|
||||||
|
$"Arrivano in media {s.AverageActualMs:F0} ms prima della scadenza " +
|
||||||
|
$"(mediana {s.MedianActualMs:F0} ms, caso peggiore {s.MinActualMs:F0} ms).\n" +
|
||||||
|
$"Ping medio {s.AveragePingMs:F0} ms, anticipo impostato di norma {s.TypicalConfiguredMs} ms.");
|
||||||
|
}
|
||||||
|
catch { }
|
||||||
|
}
|
||||||
|
|
||||||
|
private void Settings_ComputeBidLeadAdviceClicked(object sender, RoutedEventArgs e)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var settings = SettingsManager.Load();
|
||||||
|
var current = settings.DefaultBidBeforeDeadlineMs;
|
||||||
|
var advice = BidLeadStats.Suggest(current, settings.BidLeadMinSamples);
|
||||||
|
|
||||||
|
RefreshBidLeadSummary();
|
||||||
|
|
||||||
|
if (!advice.HasAdvice)
|
||||||
|
{
|
||||||
|
MessageBox.Show(this, advice.Reason, "Anticipo puntata",
|
||||||
|
MessageBoxButton.OK, MessageBoxImage.Information);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Il consiglio non si applica mai da solo: cambia il parametro che decide
|
||||||
|
// se una puntata arriva o no, e la decisione resta dell'utente.
|
||||||
|
var answer = MessageBox.Show(this,
|
||||||
|
$"{advice.Reason}\n\nVuoi impostare l'anticipo predefinito a {advice.SuggestedLeadMs} ms?\n\n" +
|
||||||
|
"Le aste già nel monitor mantengono il proprio valore.",
|
||||||
|
"Anticipo puntata", MessageBoxButton.YesNo, MessageBoxImage.Question);
|
||||||
|
|
||||||
|
if (answer != MessageBoxResult.Yes) return;
|
||||||
|
|
||||||
|
settings.DefaultBidBeforeDeadlineMs = advice.SuggestedLeadMs;
|
||||||
|
SettingsManager.Save(settings);
|
||||||
|
|
||||||
|
DefaultBidBeforeDeadlineMs.Text = advice.SuggestedLeadMs.ToString();
|
||||||
|
Log($"[ANTICIPO] Anticipo predefinito portato a {advice.SuggestedLeadMs} ms", LogLevel.Success);
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
Log($"[ANTICIPO] {ex.Message}", LogLevel.Error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void Settings_ClearBidLeadStatsClicked(object sender, RoutedEventArgs e)
|
||||||
|
{
|
||||||
|
var answer = MessageBox.Show(this,
|
||||||
|
"Vuoi azzerare tutte le misure sull'anticipo?\n\n" +
|
||||||
|
"Serviranno di nuovo diverse puntate prima di poter dare un consiglio.",
|
||||||
|
"Misure anticipo", MessageBoxButton.YesNo, MessageBoxImage.Question);
|
||||||
|
|
||||||
|
if (answer != MessageBoxResult.Yes) return;
|
||||||
|
|
||||||
|
BidLeadStats.Clear();
|
||||||
|
RefreshBidLeadSummary();
|
||||||
|
Log("[ANTICIPO] Misure azzerate", LogLevel.Info);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Cartelle dei dati ────────────────────────────────────────────
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Riempie i campi dei percorsi con quelli davvero in uso.
|
||||||
|
///
|
||||||
|
/// <para>I campi non restano mai vuoti: un campo vuoto costringeva a cercare altrove
|
||||||
|
/// dove finiscono i file, ed è il motivo per cui sotto c'era una riga che ripeteva i
|
||||||
|
/// percorsi. Scriverli direttamente nel campo toglie la ripetizione e rende il valore
|
||||||
|
/// selezionabile e copiabile. Chi vuole tornare al predefinito lo svuota: al
|
||||||
|
/// salvataggio si ricompila da sé.</para>
|
||||||
|
/// </summary>
|
||||||
|
private void RefreshDataFolderFields()
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
Settings.DataFolderTextBox.Text = AppPaths.DataFolder;
|
||||||
|
Settings.StatsFolderTextBox.Text = AppPaths.StatsFolder;
|
||||||
|
Settings.LogFolderTextBox.Text = AppPaths.LogFolder;
|
||||||
|
}
|
||||||
|
catch { }
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Traduce quello che c'è nel campo in ciò che va salvato.
|
||||||
|
///
|
||||||
|
/// <para>Il campo mostra sempre un percorso pieno, ma nelle impostazioni "vuoto"
|
||||||
|
/// ha un significato che non si vuole perdere: <i>usa il predefinito</i>. Se il
|
||||||
|
/// testo coincide con il predefinito corrente si salva vuoto — così spostando la
|
||||||
|
/// cartella dati le sottocartelle la seguono — e se il campo è stato svuotato a mano
|
||||||
|
/// si torna al predefinito. Solo un percorso davvero diverso viene registrato.</para>
|
||||||
|
/// </summary>
|
||||||
|
private static string NormalizeFolderChoice(string? typed, string resolvedDefault, string previouslySaved)
|
||||||
|
{
|
||||||
|
var text = typed?.Trim() ?? "";
|
||||||
|
|
||||||
|
if (text.Length == 0) return "";
|
||||||
|
|
||||||
|
// Uguale a ciò che è in uso, e in uso c'era il predefinito: resta predefinito.
|
||||||
|
if (string.Equals(text, resolvedDefault, StringComparison.OrdinalIgnoreCase) &&
|
||||||
|
string.IsNullOrWhiteSpace(previouslySaved))
|
||||||
|
return "";
|
||||||
|
|
||||||
|
return text;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void Settings_BrowseDataFolderClicked(object sender, RoutedEventArgs e)
|
||||||
|
{
|
||||||
|
var chosen = PickFolder("Scegli la cartella dei dati d'esercizio", AppPaths.DataFolder);
|
||||||
|
if (chosen != null) Settings.DataFolderTextBox.Text = chosen;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void Settings_BrowseStatsFolderClicked(object sender, RoutedEventArgs e)
|
||||||
|
{
|
||||||
|
var chosen = PickFolder("Scegli la cartella delle statistiche", AppPaths.StatsFolder);
|
||||||
|
if (chosen != null) Settings.StatsFolderTextBox.Text = chosen;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void Settings_BrowseLogFolderClicked(object sender, RoutedEventArgs e)
|
||||||
|
{
|
||||||
|
var chosen = PickFolder("Scegli la cartella dei registri", AppPaths.LogFolder);
|
||||||
|
if (chosen != null) Settings.LogFolderTextBox.Text = chosen;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void Settings_OpenDataFolderClicked(object sender, RoutedEventArgs e)
|
||||||
|
=> OpenFolder(AppPaths.DataFolder);
|
||||||
|
|
||||||
|
private void Settings_OpenStatsFolderClicked(object sender, RoutedEventArgs e)
|
||||||
|
=> OpenFolder(AppPaths.StatsFolder);
|
||||||
|
|
||||||
|
private void Settings_OpenLogFolderClicked(object sender, RoutedEventArgs e)
|
||||||
|
=> OpenFolder(AppPaths.LogFolder);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Selettore di cartella. Usa quello di WinForms perché WPF non ne offre uno:
|
||||||
|
/// l'alternativa sarebbe una finestra scritta a mano, molto peggio.
|
||||||
|
/// </summary>
|
||||||
|
private static string? PickFolder(string description, string startFrom)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
using var dialog = new System.Windows.Forms.FolderBrowserDialog
|
||||||
|
{
|
||||||
|
Description = description,
|
||||||
|
UseDescriptionForTitle = true,
|
||||||
|
SelectedPath = Directory.Exists(startFrom) ? startFrom : "",
|
||||||
|
ShowNewFolderButton = true
|
||||||
|
};
|
||||||
|
|
||||||
|
return dialog.ShowDialog() == System.Windows.Forms.DialogResult.OK
|
||||||
|
? dialog.SelectedPath
|
||||||
|
: null;
|
||||||
|
}
|
||||||
|
catch
|
||||||
|
{
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void OpenFolder(string path)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
Directory.CreateDirectory(path);
|
||||||
|
Process.Start(new ProcessStartInfo { FileName = path, UseShellExecute = true });
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
Log($"[DATI] Impossibile aprire {path}: {ex.Message}", LogLevel.Error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Applica i percorsi dopo un salvataggio. Se sono cambiati serve un riavvio: gli
|
||||||
|
/// archivi hanno già in memoria i dati letti dalla posizione precedente, e
|
||||||
|
/// spostarli a caldo rischierebbe di mescolarli.
|
||||||
|
/// </summary>
|
||||||
|
private void ApplyDataFolderSettings(AppSettings settings, string previousData, string previousStats)
|
||||||
|
{
|
||||||
|
AppPaths.Configure(settings.DataFolder, settings.StatsFolder, settings.LogFolder);
|
||||||
|
AppPaths.EnsureFolders();
|
||||||
|
|
||||||
|
// I campi tornano a mostrare i percorsi effettivi: chi ne ha svuotato uno deve
|
||||||
|
// vedere subito qual è il predefinito che ha appena scelto.
|
||||||
|
RefreshDataFolderFields();
|
||||||
|
|
||||||
|
if (previousData == AppPaths.DataFolder && previousStats == AppPaths.StatsFolder) return;
|
||||||
|
|
||||||
|
MessageBox.Show(this,
|
||||||
|
"Le cartelle dei dati sono cambiate.\n\n" +
|
||||||
|
$"Dati: {AppPaths.DataFolder}\nStatistiche: {AppPaths.StatsFolder}\n\n" +
|
||||||
|
"Riavvia l'applicazione perché vengano usate davvero. I file già scritti " +
|
||||||
|
"restano dove sono: puoi spostarli a mano nella nuova posizione.",
|
||||||
|
"Cartelle dei dati", MessageBoxButton.OK, MessageBoxImage.Information);
|
||||||
|
|
||||||
|
Log($"[DATI] Nuovi percorsi impostati (riavvio necessario)", LogLevel.Warning);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Esportazione dello storico ───────────────────────────────────
|
||||||
|
|
||||||
|
private void ExportStatsCsvButton_Click(object sender, RoutedEventArgs e)
|
||||||
|
=> RunExport(StatsExporter.ExportCsv(), "CSV");
|
||||||
|
|
||||||
|
private void ExportStatsJsonButton_Click(object sender, RoutedEventArgs e)
|
||||||
|
=> RunExport(StatsExporter.ExportJson(), "JSON");
|
||||||
|
|
||||||
|
private void RunExport(StatsExporter.Result result, string format)
|
||||||
|
{
|
||||||
|
if (!result.Success)
|
||||||
|
{
|
||||||
|
MessageBox.Show(this, result.Error ?? "Esportazione non riuscita.",
|
||||||
|
$"Esporta {format}", MessageBoxButton.OK, MessageBoxImage.Warning);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
Log($"[EXPORT] {result.Records} aste esportate in {result.Path}", LogLevel.Success);
|
||||||
|
|
||||||
|
var answer = MessageBox.Show(this,
|
||||||
|
$"Esportate {result.Records} aste in formato {format}.\n\n{result.Path}\n\n" +
|
||||||
|
"Vuoi aprire la cartella?",
|
||||||
|
$"Esporta {format}", MessageBoxButton.YesNo, MessageBoxImage.Information);
|
||||||
|
|
||||||
|
if (answer == MessageBoxResult.Yes)
|
||||||
|
{
|
||||||
|
OpenFolder(Path.GetDirectoryName(result.Path) ?? AppPaths.ExportFolder);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,96 @@
|
|||||||
|
using System;
|
||||||
|
using System.Diagnostics;
|
||||||
|
using System.IO;
|
||||||
|
using System.Windows;
|
||||||
|
using AutoBidder.Utilities;
|
||||||
|
|
||||||
|
namespace AutoBidder
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Scheda Esporta: raccoglie in un unico file le aste che rispondono ai filtri.
|
||||||
|
///
|
||||||
|
/// <para>Qui c'è solo il collegamento fra il pannello e
|
||||||
|
/// <see cref="AuctionExporter"/>. La scrittura può durare qualche secondo — con gli
|
||||||
|
/// eventi inclusi si leggono decine di dossier — quindi gira su un thread di lavoro:
|
||||||
|
/// bloccare l'interfaccia mentre il motore sta seguendo un'asta non è accettabile.</para>
|
||||||
|
/// </summary>
|
||||||
|
public partial class MainWindow
|
||||||
|
{
|
||||||
|
private async void Export_ExportRequested(object sender, RoutedEventArgs e)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var filter = Export.BuildFilter();
|
||||||
|
|
||||||
|
Log("[ESPORTA] Esportazione in corso…", LogLevel.Info);
|
||||||
|
|
||||||
|
var result = await System.Threading.Tasks.Task
|
||||||
|
.Run(() => AuctionExporter.Export(filter))
|
||||||
|
.ConfigureAwait(true);
|
||||||
|
|
||||||
|
Export.ShowResult(result);
|
||||||
|
|
||||||
|
Log(result.Success
|
||||||
|
? $"[ESPORTA] {result.Message} → {result.Path}"
|
||||||
|
: $"[ESPORTA] {result.Message}",
|
||||||
|
result.Success ? LogLevel.Success : LogLevel.Warning);
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
Log($"[ESPORTA] Esportazione non riuscita: {ex.Message}", LogLevel.Error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void Export_OpenFolderClicked(object sender, RoutedEventArgs e)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
Directory.CreateDirectory(AppPaths.ExportFolder);
|
||||||
|
OpenInExplorer(AppPaths.ExportFolder);
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
Log($"[ESPORTA] Apertura cartella non riuscita: {ex.Message}", LogLevel.Error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void Export_OpenLastFileClicked(object sender, RoutedEventArgs e)
|
||||||
|
{
|
||||||
|
var path = Export.LastExportPath;
|
||||||
|
if (string.IsNullOrEmpty(path) || !File.Exists(path)) return;
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
Process.Start(new ProcessStartInfo(path) { UseShellExecute = true });
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
Log($"[ESPORTA] Apertura file non riuscita: {ex.Message}", LogLevel.Error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Apre una cartella in Esplora risorse. Sta qui perché la usano più schede: i
|
||||||
|
/// pulsanti "apri" dei percorsi, le esportazioni e i registri.
|
||||||
|
/// </summary>
|
||||||
|
internal void OpenInExplorer(string folder)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
if (string.IsNullOrWhiteSpace(folder)) return;
|
||||||
|
|
||||||
|
Directory.CreateDirectory(folder);
|
||||||
|
|
||||||
|
Process.Start(new ProcessStartInfo
|
||||||
|
{
|
||||||
|
FileName = folder,
|
||||||
|
UseShellExecute = true
|
||||||
|
});
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
Log($"[CARTELLE] Impossibile aprire {folder}: {ex.Message}", LogLevel.Error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,420 @@
|
|||||||
|
using System;
|
||||||
|
using System.Diagnostics;
|
||||||
|
using System.Threading;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
using System.Windows;
|
||||||
|
using AutoBidder.Models;
|
||||||
|
using AutoBidder.Services;
|
||||||
|
using AutoBidder.Utilities;
|
||||||
|
|
||||||
|
namespace AutoBidder
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Scheda "Puntate": riscatto automatico delle ricompense, contatori e registro.
|
||||||
|
///
|
||||||
|
/// <para>Il lavoro vero sta in <see cref="FreeBidsAutoClaimService"/> e
|
||||||
|
/// <see cref="BidooFreeBidsClaimer"/>: qui c'è solo il collegamento fra quei servizi e
|
||||||
|
/// l'interfaccia, compreso il passaggio sul thread dell'interfaccia — il ciclo gira su
|
||||||
|
/// un thread di lavoro e non deve toccare i controlli.</para>
|
||||||
|
///
|
||||||
|
/// <para>Resta invece manuale la parte consegnata per notifica push (OneSignal) e per
|
||||||
|
/// email: quei premi sono agganciati all'abbonamento del browser, non all'account, e
|
||||||
|
/// non esiste una chiamata da fare con il cookie di sessione.</para>
|
||||||
|
/// </summary>
|
||||||
|
public partial class MainWindow
|
||||||
|
{
|
||||||
|
private FreeBidsAutoClaimService? _freeBidsService;
|
||||||
|
private BidooFreeBidsClaimer? _freeBidsClaimer;
|
||||||
|
private BidooPromoRedeemer? _promoRedeemer;
|
||||||
|
|
||||||
|
/// <summary>Esito dell'ultima raccolta, da mostrare nella riga di stato.</summary>
|
||||||
|
private string? _lastHarvestOutcome;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Indirizzi presi dai riferimenti configurati, non scritti qui: sono gli stessi che
|
||||||
|
/// usa il riscatto automatico, e devono restare gli stessi anche quando cambiano —
|
||||||
|
/// altrimenti «Apri su Bidoo» porterebbe a una pagina diversa da quella che
|
||||||
|
/// l'applicazione sta davvero interrogando.
|
||||||
|
/// </summary>
|
||||||
|
private static FreeBidsSiteConfig FreeBidsConfig => FreeBidsConfigStore.Current;
|
||||||
|
|
||||||
|
private static string ChestsUrl => FreeBidsConfig.Url(FreeBidsConfig.Endpoints.Rewards);
|
||||||
|
|
||||||
|
private void StartFreeBidsService()
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
// Alla prima esecuzione il file dei riferimenti non c'è: crearlo subito è il
|
||||||
|
// modo per farne sapere l'esistenza a chi dovrà correggerlo.
|
||||||
|
if (FreeBidsConfigStore.EnsureFileExists())
|
||||||
|
Log($"[PUNTATE] Riferimenti del riscatto creati in {FreeBidsConfigStore.FilePath}", LogLevel.Info);
|
||||||
|
|
||||||
|
// Il file va letto davvero prima di poter dire se ha qualcosa che non va:
|
||||||
|
// un problema non ancora incontrato non è un problema che si può segnalare.
|
||||||
|
FreeBidsConfigStore.Reload();
|
||||||
|
|
||||||
|
if (FreeBidsConfigStore.LastProblem is { } problem)
|
||||||
|
Log($"[PUNTATE] {problem}", LogLevel.Warning);
|
||||||
|
|
||||||
|
var transport = _auctionMonitor.GetApiClient().Transport;
|
||||||
|
|
||||||
|
_freeBidsClaimer = new BidooFreeBidsClaimer(transport)
|
||||||
|
{
|
||||||
|
Diagnostic = msg => Dispatcher.BeginInvoke(() => Log($"[PUNTATE] {msg}", LogLevel.Info))
|
||||||
|
};
|
||||||
|
|
||||||
|
_promoRedeemer = new BidooPromoRedeemer(transport)
|
||||||
|
{
|
||||||
|
Diagnostic = msg => Dispatcher.BeginInvoke(() =>
|
||||||
|
{
|
||||||
|
Log($"[PUNTATE] {msg}", LogLevel.Info);
|
||||||
|
FreeBids.AppendActivity(msg);
|
||||||
|
})
|
||||||
|
};
|
||||||
|
|
||||||
|
_freeBidsService = new FreeBidsAutoClaimService(_freeBidsClaimer, ReadBalanceAsync, _promoRedeemer);
|
||||||
|
|
||||||
|
_freeBidsService.OnLog += (message, isProblem) => Dispatcher.BeginInvoke(() =>
|
||||||
|
{
|
||||||
|
Log($"[PUNTATE] {message}", isProblem ? LogLevel.Warning : LogLevel.Success);
|
||||||
|
FreeBids.AppendActivity(message, isProblem);
|
||||||
|
});
|
||||||
|
|
||||||
|
_freeBidsService.OnCycleCompleted += _ => Dispatcher.BeginInvoke(() =>
|
||||||
|
{
|
||||||
|
if (_freeBidsService?.LastHarvest is { } harvest) _lastHarvestOutcome = harvest.Message;
|
||||||
|
RefreshFreeBidsPanel();
|
||||||
|
});
|
||||||
|
|
||||||
|
var settings = SettingsManager.Load();
|
||||||
|
FreeBids.SetOptions(settings.FreeBidsAutoClaimEnabled, settings.FreeBidsCheckMinutes);
|
||||||
|
|
||||||
|
if (settings.FreeBidsAutoClaimEnabled) _freeBidsService.Start();
|
||||||
|
|
||||||
|
RefreshFreeBidsPanel();
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
// Il riscatto è un accessorio: se non parte, il resto dell'applicazione
|
||||||
|
// deve continuare a funzionare senza accorgersene.
|
||||||
|
Log($"[PUNTATE] Riscatto automatico non avviato: {ex.Message}", LogLevel.Error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Rilegge il saldo dal sito. Serve al servizio per misurare quante puntate ha
|
||||||
|
/// portato davvero un riscatto, invece di fidarsi del messaggio di risposta.
|
||||||
|
/// </summary>
|
||||||
|
private async Task<int?> ReadBalanceAsync(CancellationToken ct)
|
||||||
|
{
|
||||||
|
await _auctionMonitor.UpdateUserInfoAsync().ConfigureAwait(false);
|
||||||
|
return _auctionMonitor.GetSession()?.RemainingBids;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void RefreshFreeBidsPanel()
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var session = _auctionMonitor.GetSession();
|
||||||
|
FreeBids.SetBalance(session?.RemainingBids);
|
||||||
|
|
||||||
|
FreeBids.SetSchedule(
|
||||||
|
running: _freeBidsService?.IsRunning == true,
|
||||||
|
lastCheck: _freeBidsService?.LastCheckAt,
|
||||||
|
nextCheck: _freeBidsService?.NextCheckAt);
|
||||||
|
|
||||||
|
FreeBids.SetCounters(FreeBidsStats.Read());
|
||||||
|
FreeBids.SetConfigPath(FreeBidsConfigStore.FilePath, FreeBidsConfigStore.LastProblem);
|
||||||
|
|
||||||
|
FreeBids.SetHarvestStatus(
|
||||||
|
FreeBidsConfig.PromoHarvest.SourceUrl,
|
||||||
|
ClaimedPromoStore.ClaimedCount,
|
||||||
|
_lastHarvestOutcome);
|
||||||
|
}
|
||||||
|
catch { }
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Eventi della scheda ──────────────────────────────────────────
|
||||||
|
|
||||||
|
private async void FreeBids_RefreshBalanceClicked(object sender, RoutedEventArgs e)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
await _auctionMonitor.UpdateUserInfoAsync();
|
||||||
|
RefreshFreeBidsPanel();
|
||||||
|
UpdateRemainingBidsDisplay();
|
||||||
|
|
||||||
|
Log("[PUNTATE] Saldo aggiornato", LogLevel.Info);
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
Log($"[PUNTATE] Aggiornamento saldo non riuscito: {ex.Message}", LogLevel.Error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private async void FreeBids_CheckNowClicked(object sender, RoutedEventArgs e)
|
||||||
|
{
|
||||||
|
if (_freeBidsService == null) return;
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
FreeBids.AppendActivity("Controllo richiesto a mano…");
|
||||||
|
|
||||||
|
var result = await _freeBidsService.CheckNowAsync();
|
||||||
|
|
||||||
|
if (result.IsSuccess && result.ClaimedCount == 0)
|
||||||
|
FreeBids.AppendActivity(result.Message);
|
||||||
|
|
||||||
|
RefreshFreeBidsPanel();
|
||||||
|
UpdateRemainingBidsDisplay();
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
Log($"[PUNTATE] Controllo non riuscito: {ex.Message}", LogLevel.Error);
|
||||||
|
FreeBids.AppendActivity($"controllo non riuscito: {ex.Message}", isProblem: true);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>Apre la pagina nel browser predefinito di Windows, fuori dall'applicazione.</summary>
|
||||||
|
private void FreeBids_OpenExternalClicked(object sender, RoutedEventArgs e)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
Process.Start(new ProcessStartInfo(ChestsUrl) { UseShellExecute = true });
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
Log($"[PUNTATE] Apertura nel browser esterno non riuscita: {ex.Message}", LogLevel.Error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void FreeBids_OpenInternalClicked(object sender, RoutedEventArgs e)
|
||||||
|
=> OpenInEmbeddedBrowser(ChestsUrl);
|
||||||
|
|
||||||
|
private void FreeBids_OpenVouchersClicked(object sender, RoutedEventArgs e)
|
||||||
|
=> OpenInEmbeddedBrowser(FreeBidsConfig.Url(FreeBidsConfig.Endpoints.Vouchers));
|
||||||
|
|
||||||
|
private void FreeBids_OpenBuyBidsClicked(object sender, RoutedEventArgs e)
|
||||||
|
=> OpenInEmbeddedBrowser(FreeBidsConfig.Url(FreeBidsConfig.Endpoints.BuyBids));
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Riscatta il codice o il collegamento incollato dall'utente.
|
||||||
|
///
|
||||||
|
/// <para>Il saldo viene riletto in ogni caso: il messaggio del sito dice quello che
|
||||||
|
/// vuole, la differenza di puntate sul conto no.</para>
|
||||||
|
/// </summary>
|
||||||
|
private async void FreeBids_ClaimPromoClicked(object sender, RoutedEventArgs e)
|
||||||
|
{
|
||||||
|
if (_freeBidsClaimer == null) return;
|
||||||
|
|
||||||
|
var code = FreeBids.PromoCode;
|
||||||
|
|
||||||
|
if (code.Length == 0)
|
||||||
|
{
|
||||||
|
FreeBids.AppendActivity("manca il codice o il collegamento da riscattare", isProblem: true);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
FreeBids.AppendActivity("Riscatto del codice richiesto a mano…");
|
||||||
|
|
||||||
|
var before = _auctionMonitor.GetSession()?.RemainingBids;
|
||||||
|
var result = await _freeBidsClaimer.ClaimPromoAsync(code);
|
||||||
|
|
||||||
|
if (!result.IsSuccess)
|
||||||
|
{
|
||||||
|
FreeBids.AppendActivity(result.Message, isProblem: true);
|
||||||
|
Log($"[PUNTATE] Riscatto codice non riuscito: {result.Message}", LogLevel.Warning);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
await _auctionMonitor.UpdateUserInfoAsync();
|
||||||
|
|
||||||
|
var after = _auctionMonitor.GetSession()?.RemainingBids;
|
||||||
|
var gained = before.HasValue && after.HasValue ? Math.Max(0, after.Value - before.Value) : 0;
|
||||||
|
|
||||||
|
if (gained > 0) FreeBidsStats.RecordClaim(claimedCount: 1, bidsGained: gained);
|
||||||
|
|
||||||
|
FreeBids.AppendActivity(gained > 0
|
||||||
|
? $"codice riscattato: +{gained} puntate"
|
||||||
|
: $"codice accettato ({result.Message}), saldo invariato");
|
||||||
|
|
||||||
|
FreeBids.ClearPromoCode();
|
||||||
|
RefreshFreeBidsPanel();
|
||||||
|
UpdateRemainingBidsDisplay();
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
Log($"[PUNTATE] Riscatto codice non riuscito: {ex.Message}", LogLevel.Error);
|
||||||
|
FreeBids.AppendActivity($"riscatto non riuscito: {ex.Message}", isProblem: true);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Esegue subito una raccolta dei collegamenti pubblicati, senza aspettare il giro.
|
||||||
|
///
|
||||||
|
/// <para>Le puntate ottenute si misurano sul saldo prima e dopo: la pagina di Bidoo
|
||||||
|
/// risponde allo stesso modo per un codice appena riscosso e per uno già usato.</para>
|
||||||
|
/// </summary>
|
||||||
|
private async void FreeBids_HarvestClicked(object sender, RoutedEventArgs e)
|
||||||
|
{
|
||||||
|
if (_promoRedeemer == null) return;
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
FreeBids.AppendActivity("Raccolta dei collegamenti richiesta a mano…");
|
||||||
|
|
||||||
|
var before = _auctionMonitor.GetSession()?.RemainingBids;
|
||||||
|
var report = await _promoRedeemer.HarvestAndRedeemAsync();
|
||||||
|
|
||||||
|
_lastHarvestOutcome = report.Message;
|
||||||
|
|
||||||
|
if (!report.SourceReadable)
|
||||||
|
{
|
||||||
|
FreeBids.AppendActivity(report.Message, isProblem: true);
|
||||||
|
Log($"[PUNTATE] Raccolta non riuscita: {report.Message}", LogLevel.Warning);
|
||||||
|
RefreshFreeBidsPanel();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (report.Claimed > 0)
|
||||||
|
{
|
||||||
|
await _auctionMonitor.UpdateUserInfoAsync();
|
||||||
|
|
||||||
|
var after = _auctionMonitor.GetSession()?.RemainingBids;
|
||||||
|
var gained = before.HasValue && after.HasValue ? Math.Max(0, after.Value - before.Value) : 0;
|
||||||
|
|
||||||
|
FreeBidsStats.RecordClaim(report.Claimed, gained);
|
||||||
|
|
||||||
|
FreeBids.AppendActivity(gained > 0
|
||||||
|
? $"{report.Message}: +{gained} puntate"
|
||||||
|
: $"{report.Message} (saldo invariato)");
|
||||||
|
|
||||||
|
UpdateRemainingBidsDisplay();
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
FreeBids.AppendActivity(report.Message);
|
||||||
|
}
|
||||||
|
|
||||||
|
RefreshFreeBidsPanel();
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
Log($"[PUNTATE] Raccolta non riuscita: {ex.Message}", LogLevel.Error);
|
||||||
|
FreeBids.AppendActivity($"raccolta non riuscita: {ex.Message}", isProblem: true);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void FreeBids_OpenPromoSourceClicked(object sender, RoutedEventArgs e)
|
||||||
|
=> OpenInEmbeddedBrowser(FreeBidsConfig.PromoHarvest.SourceUrl);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Azzera la memoria dei collegamenti già aperti. Serve dopo un cambio di account:
|
||||||
|
/// i codici presi da un utente non dicono nulla su quelli disponibili per un altro.
|
||||||
|
/// </summary>
|
||||||
|
private void FreeBids_ForgetPromosClicked(object sender, RoutedEventArgs e)
|
||||||
|
{
|
||||||
|
var answer = MessageBox.Show(this,
|
||||||
|
$"Vuoi dimenticare i {ClaimedPromoStore.ClaimedCount} codici già presi?\n\n" +
|
||||||
|
"Alla raccolta successiva verranno riaperti tutti i collegamenti pubblicati, " +
|
||||||
|
"compresi quelli già usati: serve dopo un cambio di account, non nell'uso normale.",
|
||||||
|
"Puntate", MessageBoxButton.YesNo, MessageBoxImage.Warning);
|
||||||
|
|
||||||
|
if (answer != MessageBoxResult.Yes) return;
|
||||||
|
|
||||||
|
ClaimedPromoStore.Clear();
|
||||||
|
_lastHarvestOutcome = null;
|
||||||
|
RefreshFreeBidsPanel();
|
||||||
|
|
||||||
|
Log("[PUNTATE] Memoria dei collegamenti azzerata", LogLevel.Info);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Apre <c>site-config.json</c> con l'editor predefinito. Il file viene creato se
|
||||||
|
/// manca: aprire il vuoto non direbbe a nessuno quali valori si possono cambiare.
|
||||||
|
/// </summary>
|
||||||
|
private void FreeBids_OpenConfigClicked(object sender, RoutedEventArgs e)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
FreeBidsConfigStore.EnsureFileExists();
|
||||||
|
|
||||||
|
Process.Start(new ProcessStartInfo(FreeBidsConfigStore.FilePath) { UseShellExecute = true });
|
||||||
|
|
||||||
|
FreeBids.AppendActivity(
|
||||||
|
"Riferimenti aperti: salva il file e premi «Controlla adesso», " +
|
||||||
|
"le modifiche valgono subito.");
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
Log($"[PUNTATE] Apertura dei riferimenti non riuscita: {ex.Message}", LogLevel.Error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void FreeBids_ResetCountersClicked(object sender, RoutedEventArgs e)
|
||||||
|
{
|
||||||
|
var answer = MessageBox.Show(this,
|
||||||
|
"Vuoi azzerare i contatori dei riscatti?\n\n" +
|
||||||
|
"Si perde il totale storico delle puntate raccolte. Le puntate sul conto non vengono toccate.",
|
||||||
|
"Puntate", MessageBoxButton.YesNo, MessageBoxImage.Warning);
|
||||||
|
|
||||||
|
if (answer != MessageBoxResult.Yes) return;
|
||||||
|
|
||||||
|
FreeBidsStats.Clear();
|
||||||
|
RefreshFreeBidsPanel();
|
||||||
|
Log("[PUNTATE] Contatori azzerati", LogLevel.Info);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void FreeBids_OptionsChanged(object sender, RoutedEventArgs e)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var settings = SettingsManager.Load();
|
||||||
|
settings.FreeBidsAutoClaimEnabled = FreeBids.AutoClaimEnabled;
|
||||||
|
settings.FreeBidsCheckMinutes = FreeBids.CheckMinutes;
|
||||||
|
SettingsManager.Save(settings);
|
||||||
|
|
||||||
|
if (_freeBidsService != null)
|
||||||
|
{
|
||||||
|
if (settings.FreeBidsAutoClaimEnabled && !_freeBidsService.IsRunning)
|
||||||
|
{
|
||||||
|
_freeBidsService.Start();
|
||||||
|
Log($"[PUNTATE] Riscatto automatico attivo, ogni {settings.FreeBidsCheckMinutes} minuti", LogLevel.Success);
|
||||||
|
}
|
||||||
|
else if (!settings.FreeBidsAutoClaimEnabled && _freeBidsService.IsRunning)
|
||||||
|
{
|
||||||
|
_freeBidsService.Stop();
|
||||||
|
Log("[PUNTATE] Riscatto automatico spento", LogLevel.Info);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
RefreshFreeBidsPanel();
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
Log($"[PUNTATE] Impostazioni non salvate: {ex.Message}", LogLevel.Error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Apre una pagina nel browser integrato. Deve essere quello, non il browser di
|
||||||
|
/// sistema: è lì che la sessione è attiva ed è lì che vanno concesse le notifiche.
|
||||||
|
/// </summary>
|
||||||
|
private void OpenInEmbeddedBrowser(string url)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
TabBrowser.IsChecked = true;
|
||||||
|
Browser.ShowBrowser();
|
||||||
|
Browser.EmbeddedWebView?.CoreWebView2?.Navigate(url);
|
||||||
|
Browser.BrowserAddress.Text = url;
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
Log($"[PUNTATE] Apertura pagina non riuscita: {ex.Message}", LogLevel.Error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -14,44 +14,38 @@ namespace AutoBidder
|
|||||||
/// Scrive un messaggio nel log globale con filtraggio basato sul livello minimo configurato
|
/// Scrive un messaggio nel log globale con filtraggio basato sul livello minimo configurato
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="message">Messaggio da loggare</param>
|
/// <param name="message">Messaggio da loggare</param>
|
||||||
/// <param name="level">Livello di severità del messaggio</param>
|
/// <param name="level">Livello di severit� del messaggio</param>
|
||||||
private void Log(string message, LogLevel level = LogLevel.Info)
|
private void Log(string message, LogLevel level = LogLevel.Info)
|
||||||
{
|
{
|
||||||
|
// Il file si scrive subito, fuori dal Dispatcher e senza filtro di livello:
|
||||||
|
// il registro serve proprio quando qualcosa è andato storto, e in quel momento
|
||||||
|
// il messaggio utile è spesso uno di quelli che a video sono nascosti. La
|
||||||
|
// scrittura è accodata, quindi non costa nulla al chiamante.
|
||||||
|
TextLogService.App(LevelTag(level), message);
|
||||||
|
|
||||||
Dispatcher.BeginInvoke(() =>
|
Dispatcher.BeginInvoke(() =>
|
||||||
{
|
{
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
// Carica impostazioni per ottenere livello minimo e limite righe
|
// Carica impostazioni per ottenere livello minimo e limite righe
|
||||||
var settings = SettingsManager.Load();
|
var settings = SettingsManager.Load();
|
||||||
|
|
||||||
// Filtra messaggi in base al livello minimo configurato
|
// Filtra messaggi in base al livello minimo configurato
|
||||||
MinimumLogLevel minLevel = MinimumLogLevel.Normal; // Default
|
MinimumLogLevel minLevel = MinimumLogLevel.Normal; // Default
|
||||||
if (Enum.TryParse<MinimumLogLevel>(settings.MinLogLevel, out var parsedLevel))
|
if (Enum.TryParse<MinimumLogLevel>(settings.MinLogLevel, out var parsedLevel))
|
||||||
{
|
{
|
||||||
minLevel = parsedLevel;
|
minLevel = parsedLevel;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Se il livello del messaggio è maggiore del minimo configurato, ignora
|
// Se il livello del messaggio � maggiore del minimo configurato, ignora
|
||||||
if ((int)level > (int)minLevel)
|
if ((int)level > (int)minLevel)
|
||||||
{
|
{
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
var timestamp = DateTime.Now.ToString("HH:mm:ss");
|
var timestamp = DateTime.Now.ToString("HH:mm:ss");
|
||||||
|
|
||||||
// Prefisso in base al livello per chiarezza
|
var logEntry = $"[{timestamp}] [{LevelTag(level)}] {message}";
|
||||||
string prefix = level switch
|
|
||||||
{
|
|
||||||
LogLevel.Error => "[ERROR]",
|
|
||||||
LogLevel.Warning => "[WARN]",
|
|
||||||
LogLevel.Info => "[INFO]",
|
|
||||||
LogLevel.Success => "[OK]",
|
|
||||||
LogLevel.Debug => "[DEBUG]",
|
|
||||||
LogLevel.Trace => "[TRACE]",
|
|
||||||
_ => "[LOG]"
|
|
||||||
};
|
|
||||||
|
|
||||||
var logEntry = $"[{timestamp}] {prefix} {message}";
|
|
||||||
|
|
||||||
// Color coding based on severity for dark theme
|
// Color coding based on severity for dark theme
|
||||||
var color = level switch
|
var color = level switch
|
||||||
@@ -75,7 +69,7 @@ namespace AutoBidder
|
|||||||
|
|
||||||
if (LogBox.Document.Blocks.Count > maxLogLines)
|
if (LogBox.Document.Blocks.Count > maxLogLines)
|
||||||
{
|
{
|
||||||
// Rimuovi i paragrafi più vecchi (primi inseriti)
|
// Rimuovi i paragrafi pi� vecchi (primi inseriti)
|
||||||
int excessCount = LogBox.Document.Blocks.Count - maxLogLines;
|
int excessCount = LogBox.Document.Blocks.Count - maxLogLines;
|
||||||
for (int i = 0; i < excessCount; i++)
|
for (int i = 0; i < excessCount; i++)
|
||||||
{
|
{
|
||||||
@@ -95,5 +89,20 @@ namespace AutoBidder
|
|||||||
catch { }
|
catch { }
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Sigla del livello, uguale a video e su file: due formati diversi renderebbero
|
||||||
|
/// impossibile ritrovare nel file la riga che si è vista nella finestra.
|
||||||
|
/// </summary>
|
||||||
|
private static string LevelTag(LogLevel level) => level switch
|
||||||
|
{
|
||||||
|
LogLevel.Error => "ERROR",
|
||||||
|
LogLevel.Warning => "WARN",
|
||||||
|
LogLevel.Info => "INFO",
|
||||||
|
LogLevel.Success => "OK",
|
||||||
|
LogLevel.Debug => "DEBUG",
|
||||||
|
LogLevel.Trace => "TRACE",
|
||||||
|
_ => "LOG"
|
||||||
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,216 @@
|
|||||||
|
using System;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Windows;
|
||||||
|
using System.Windows.Threading;
|
||||||
|
using AutoBidder.Models;
|
||||||
|
using AutoBidder.Utilities;
|
||||||
|
using AutoBidder.ViewModels;
|
||||||
|
|
||||||
|
namespace AutoBidder
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Barra strumenti del Monitor: contatori delle aste e stato del motore.
|
||||||
|
///
|
||||||
|
/// Sono informazioni che cambiano di continuo ma non a ogni singolo poll: un
|
||||||
|
/// battito fisso di un secondo le tiene aggiornate senza far ridisegnare la
|
||||||
|
/// barra decine di volte al secondo.
|
||||||
|
/// </summary>
|
||||||
|
public partial class MainWindow
|
||||||
|
{
|
||||||
|
private DispatcherTimer? _headerTimer;
|
||||||
|
private DispatcherTimer? _timerTicker;
|
||||||
|
|
||||||
|
private void StartMonitorHeaderTimer()
|
||||||
|
{
|
||||||
|
_headerTimer = new DispatcherTimer { Interval = TimeSpan.FromSeconds(1) };
|
||||||
|
_headerTimer.Tick += (_, _) => RefreshMonitorHeader();
|
||||||
|
_headerTimer.Start();
|
||||||
|
|
||||||
|
// ── Il conto alla rovescia scorre da solo ────────────────────────
|
||||||
|
//
|
||||||
|
// Il tempo mancante non ha bisogno di una risposta dal server per essere
|
||||||
|
// ricalcolato: la scadenza è ancorata all'orologio di Bidoo e il resto è
|
||||||
|
// sottrazione. Prima però veniva notificato solo all'arrivo di un poll, e
|
||||||
|
// nelle prove reali fra un poll e l'altro passava anche un secondo e mezzo:
|
||||||
|
// il numero restava fermo e sembrava tutto bloccato.
|
||||||
|
//
|
||||||
|
// Cinque battiti al secondo: la colonna mostra i decimi sotto il minuto,
|
||||||
|
// quindi si vede scorrere davvero, e restano un ventesimo delle notifiche
|
||||||
|
// che servirebbero per seguire il decimo esatto.
|
||||||
|
_timerTicker = new DispatcherTimer { Interval = TimeSpan.FromMilliseconds(200) };
|
||||||
|
_timerTicker.Tick += (_, _) => TickTimers();
|
||||||
|
_timerTicker.Start();
|
||||||
|
|
||||||
|
RefreshMonitorHeader();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void TickTimers()
|
||||||
|
{
|
||||||
|
var vms = _auctionViewModels;
|
||||||
|
|
||||||
|
for (var i = 0; i < vms.Count; i++)
|
||||||
|
{
|
||||||
|
try { vms[i].RefreshTimeDisplay(); }
|
||||||
|
catch { /* una riga che non si aggiorna non deve fermare le altre */ }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void RefreshMonitorHeader()
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var vms = _auctionViewModels;
|
||||||
|
|
||||||
|
var active = 0;
|
||||||
|
var watch = 0;
|
||||||
|
var stopped = 0;
|
||||||
|
var won = 0;
|
||||||
|
var lost = 0;
|
||||||
|
|
||||||
|
foreach (var vm in vms)
|
||||||
|
{
|
||||||
|
switch (vm.StatusKind)
|
||||||
|
{
|
||||||
|
case "won": won++; break;
|
||||||
|
case "lost": lost++; break;
|
||||||
|
case "leading":
|
||||||
|
case "active": active++; break;
|
||||||
|
case "watch": watch++; break;
|
||||||
|
default: stopped++; break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
AuctionMonitor.UpdateCounters(vms.Count, active, watch, stopped, won, lost);
|
||||||
|
|
||||||
|
var clock = _auctionMonitor.Clock;
|
||||||
|
AuctionMonitor.UpdateEngineStatus(
|
||||||
|
_auctionMonitor.RequestsSent, clock.IsSynced, clock.SampleCount, clock.AverageLatencyMs);
|
||||||
|
|
||||||
|
RefreshAccountPills();
|
||||||
|
|
||||||
|
// Lo stato dei pulsanti globali dipende da tutte le aste: ricalcolarlo a
|
||||||
|
// ogni risposta di ogni asta era uno scorrimento completo dell'elenco
|
||||||
|
// decine di volte al secondo. Una volta al secondo è più che sufficiente.
|
||||||
|
UpdateGlobalControlButtons();
|
||||||
|
|
||||||
|
if (_selectedAuction != null) RefreshSelectedStats(_selectedAuction);
|
||||||
|
}
|
||||||
|
catch { /* la barra informativa non deve mai disturbare il resto */ }
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Puntate, credito e vincite da confermare, riletti dalla sessione viva a ogni
|
||||||
|
/// battito.
|
||||||
|
///
|
||||||
|
/// <para>Prima ciascuno di questi numeri veniva scritto da chi lo aggiornava —
|
||||||
|
/// il ripristino della sessione, il timer di rinfresco, il codice dopo la puntata —
|
||||||
|
/// e bastava che uno dei tre non passasse perché la barra restasse su un valore
|
||||||
|
/// vecchio. Rileggerli qui costa una lettura di campo al secondo e toglie di mezzo
|
||||||
|
/// tutta quella categoria di errori.</para>
|
||||||
|
/// </summary>
|
||||||
|
private void RefreshAccountPills()
|
||||||
|
{
|
||||||
|
var session = _auctionMonitor.GetSession();
|
||||||
|
|
||||||
|
var connected = session != null && !string.IsNullOrEmpty(session.Username);
|
||||||
|
|
||||||
|
AuctionMonitor.UpdateAccountStatus(
|
||||||
|
connected ? session!.RemainingBids : null,
|
||||||
|
connected ? (decimal)session!.ShopCredit : null,
|
||||||
|
AuctionsToConfirm);
|
||||||
|
|
||||||
|
UpdateMinBidsIndicator(SettingsManager.Load().MinimumRemainingBids);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>Contatori del motore relativi alla sola asta selezionata.</summary>
|
||||||
|
private void RefreshSelectedStats(AuctionViewModel vm)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var info = vm.AuctionInfo;
|
||||||
|
AuctionMonitor.UpdateSelectedStats(
|
||||||
|
info.ResetCount,
|
||||||
|
info.BidsUsedOnThisAuction ?? vm.MyClicks,
|
||||||
|
info.AverageLatencyMs,
|
||||||
|
info.PollCount,
|
||||||
|
info.PollErrors);
|
||||||
|
}
|
||||||
|
catch { }
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Scheda Prodotto: costo totale se si vince e verdetto di convenienza.
|
||||||
|
/// </summary>
|
||||||
|
private void RefreshProductVerdict(AuctionViewModel? vm)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var totalBox = AuctionMonitor.FindName("ProductTotalCostText") as System.Windows.Controls.TextBlock;
|
||||||
|
var value = vm?.AuctionInfo.CalculatedValue;
|
||||||
|
|
||||||
|
if (value == null)
|
||||||
|
{
|
||||||
|
if (totalBox != null) totalBox.Text = "—";
|
||||||
|
AuctionMonitor.SetVerdict("Convenienza", "non calcolabile", "neutral");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (totalBox != null) totalBox.Text = $"{value.TotalCostIfWin:F2} €";
|
||||||
|
|
||||||
|
if (value.Savings is not { } savings)
|
||||||
|
{
|
||||||
|
AuctionMonitor.SetVerdict("Convenienza", "valore prodotto ignoto", "neutral");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var pct = Math.Abs(value.SavingsPercentage ?? 0);
|
||||||
|
AuctionMonitor.SetVerdict(
|
||||||
|
savings > 0 ? "Risparmio" : "Sovrapprezzo",
|
||||||
|
$"{Math.Abs(savings):F2} € ({pct:F1}%)",
|
||||||
|
savings > 0 ? "ok" : "danger");
|
||||||
|
}
|
||||||
|
catch { }
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// "Rimuovi le aste concluse": ripulisce l'elenco da ciò che non serve più
|
||||||
|
/// senza toccare quelle ancora in corso.
|
||||||
|
/// </summary>
|
||||||
|
private void AuctionMonitor_RemoveFinishedClicked(object sender, RoutedEventArgs e)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var finished = _auctionViewModels
|
||||||
|
.Where(a => a.StatusKind is "won" or "lost")
|
||||||
|
.ToList();
|
||||||
|
|
||||||
|
if (finished.Count == 0)
|
||||||
|
{
|
||||||
|
MessageBox.Show(this, "Nessuna asta conclusa da rimuovere.", "Aste concluse",
|
||||||
|
MessageBoxButton.OK, MessageBoxImage.Information);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach (var vm in finished)
|
||||||
|
{
|
||||||
|
_auctionMonitor.RemoveAuction(vm.AuctionId);
|
||||||
|
_auctionViewModels.Remove(vm);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (_selectedAuction != null && !_auctionViewModels.Contains(_selectedAuction))
|
||||||
|
_selectedAuction = null;
|
||||||
|
|
||||||
|
SaveAuctions();
|
||||||
|
UpdateTotalCount();
|
||||||
|
UpdateGlobalControlButtons();
|
||||||
|
RefreshMonitorHeader();
|
||||||
|
|
||||||
|
Log($"[REMOVE] Rimosse {finished.Count} aste concluse", LogLevel.Info);
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
Log($"[ERRORE] Rimozione aste concluse: {ex.Message}", LogLevel.Error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,124 @@
|
|||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Windows;
|
||||||
|
using AutoBidder.Models;
|
||||||
|
using AutoBidder.Services;
|
||||||
|
using AutoBidder.Utilities;
|
||||||
|
using AutoBidder.ViewModels;
|
||||||
|
|
||||||
|
namespace AutoBidder
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Prodotti seguiti: le aste nuove dello stesso articolo entrano nel monitor da sole.
|
||||||
|
///
|
||||||
|
/// L'aggiunta qui deve essere silenziosa — succede mentre l'utente sta facendo altro,
|
||||||
|
/// quindi niente finestre di dialogo: solo righe di log.
|
||||||
|
/// </summary>
|
||||||
|
public partial class MainWindow
|
||||||
|
{
|
||||||
|
private ProductWatchService? _productWatcher;
|
||||||
|
|
||||||
|
/// <summary>Aste presenti nel monitor perché aggiunte automaticamente.</summary>
|
||||||
|
private readonly HashSet<string> _autoAddedAuctionIds = new(StringComparer.Ordinal);
|
||||||
|
|
||||||
|
private void StartProductWatcher()
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
_productWatcher = new ProductWatchService(CatalogClient)
|
||||||
|
{
|
||||||
|
CountAutoAdded = () => _autoAddedAuctionIds.Count(id =>
|
||||||
|
_auctionViewModels.Any(a => a.AuctionId == id))
|
||||||
|
};
|
||||||
|
|
||||||
|
_productWatcher.OnLog += msg => Dispatcher.BeginInvoke(() => Log(msg, LogLevel.Info));
|
||||||
|
_productWatcher.OnAuctionFound += (auction, product) =>
|
||||||
|
Dispatcher.BeginInvoke(() => AutoAddAuction(auction, product));
|
||||||
|
|
||||||
|
_productWatcher.Start();
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
Log($"[SEGUITI] Impossibile avviare la sorveglianza: {ex.Message}", LogLevel.Error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Aggiunge al monitor un'asta trovata dalla sorveglianza. Volutamente separata da
|
||||||
|
/// AddAuctionById: quella mostra finestre di dialogo, qui inaccettabili.
|
||||||
|
/// </summary>
|
||||||
|
private void AutoAddAuction(CatalogAuction auction, WatchedProduct product)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
// Marcala comunque: anche se la scartiamo, non va riproposta a ogni giro.
|
||||||
|
WatchedProductsStore.MarkHandled(auction.AuctionId);
|
||||||
|
|
||||||
|
if (_auctionViewModels.Any(a => a.AuctionId == auction.AuctionId)) return;
|
||||||
|
|
||||||
|
var settings = SettingsManager.Load();
|
||||||
|
|
||||||
|
// I limiti del prodotto, dove ci sono, hanno la precedenza sui predefiniti:
|
||||||
|
// è il senso della scheda Prodotti.
|
||||||
|
var limits = ProductRuleResolver.Resolve(product, settings);
|
||||||
|
|
||||||
|
var info = new AuctionInfo
|
||||||
|
{
|
||||||
|
AuctionId = auction.AuctionId,
|
||||||
|
Name = DecodeAllHtmlEntities(auction.Name),
|
||||||
|
OriginalUrl = auction.Url,
|
||||||
|
BidBeforeDeadlineMs = settings.DefaultBidBeforeDeadlineMs,
|
||||||
|
BuyNowPrice = auction.BuyNowPrice.HasValue ? (double)auction.BuyNowPrice.Value : null,
|
||||||
|
State = limits.State
|
||||||
|
};
|
||||||
|
|
||||||
|
_auctionMonitor.AddAuction(info);
|
||||||
|
|
||||||
|
var vm = new AuctionViewModel(info)
|
||||||
|
{
|
||||||
|
MinPrice = limits.MinPrice,
|
||||||
|
MaxPrice = limits.MaxPrice,
|
||||||
|
MaxClicks = limits.MaxClicks
|
||||||
|
};
|
||||||
|
_auctionViewModels.Add(vm);
|
||||||
|
_autoAddedAuctionIds.Add(auction.AuctionId);
|
||||||
|
|
||||||
|
// Un'asta che entra in Osserva o Attiva ha bisogno del motore acceso.
|
||||||
|
if (info.State != RunState.Stopped && !_isAutomationActive)
|
||||||
|
{
|
||||||
|
_auctionMonitor.Start();
|
||||||
|
_isAutomationActive = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
WatchedProductsStore.CountAutoAdd(product);
|
||||||
|
|
||||||
|
SaveAuctions();
|
||||||
|
UpdateTotalCount();
|
||||||
|
UpdateGlobalControlButtons();
|
||||||
|
RefreshMonitorHeader();
|
||||||
|
|
||||||
|
var origine = limits.FromProduct ? " — limiti del prodotto" : "";
|
||||||
|
Log($"[SEGUITI] Aggiunta automaticamente: {info.Name} ({StateLabel(product.AutoAddState ?? settings.AutoAddNewAuctionState)}){origine}",
|
||||||
|
LogLevel.Success);
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
Log($"[SEGUITI] Aggiunta automatica non riuscita: {ex.Message}", LogLevel.Error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>Applica a caldo le impostazioni della sorveglianza dopo un salvataggio.</summary>
|
||||||
|
private void ApplyProductWatchSettings(AppSettings settings)
|
||||||
|
{
|
||||||
|
if (_productWatcher == null) return;
|
||||||
|
|
||||||
|
if (settings.AutoAddProductsEnabled && !_productWatcher.IsRunning) _productWatcher.Start();
|
||||||
|
else if (!settings.AutoAddProductsEnabled && _productWatcher.IsRunning) _productWatcher.Stop();
|
||||||
|
}
|
||||||
|
|
||||||
|
// L'elenco dei prodotti, con stellina e limiti, vive ora nella scheda Prodotti:
|
||||||
|
// vedi MainWindow.Products.cs. Qui restano solo l'avvio della sorveglianza e
|
||||||
|
// l'aggiunta automatica delle aste che trova.
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,388 @@
|
|||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Collections.ObjectModel;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Windows;
|
||||||
|
using AutoBidder.Models;
|
||||||
|
using AutoBidder.Utilities;
|
||||||
|
using AutoBidder.ViewModels;
|
||||||
|
|
||||||
|
namespace AutoBidder
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Scheda Prodotti: l'elenco degli articoli con la stellina e i limiti su misura.
|
||||||
|
///
|
||||||
|
/// <para>Raccoglie in un posto solo ciò che prima stava diviso fra le Impostazioni
|
||||||
|
/// (l'elenco dei prodotti seguiti) e i valori predefiniti delle aste. Ragionare per
|
||||||
|
/// prodotto è il modo naturale di usare Bidoo, che rimette all'asta lo stesso articolo
|
||||||
|
/// di continuo: i limiti giusti per un buono da 50 € non sono quelli di un telefono.</para>
|
||||||
|
/// </summary>
|
||||||
|
public partial class MainWindow
|
||||||
|
{
|
||||||
|
private readonly ObservableCollection<ProductViewModel> _productViewModels = new();
|
||||||
|
|
||||||
|
/// <summary>Stellina di una riga della scheda Prodotti.</summary>
|
||||||
|
public RelayCommand? ProductWatchCommand { get; private set; }
|
||||||
|
|
||||||
|
/// <summary>Applica i limiti consigliati alla singola riga.</summary>
|
||||||
|
public RelayCommand? ProductApplyAdviceCommand { get; private set; }
|
||||||
|
|
||||||
|
private void InitializeProductCommands()
|
||||||
|
{
|
||||||
|
ProductWatchCommand = new RelayCommand(p => ToggleProductWatch(p as ProductViewModel));
|
||||||
|
ProductApplyAdviceCommand = new RelayCommand(p => ApplyAdviceToProduct(p as ProductViewModel));
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Ricostruisce l'elenco unendo le schede prodotto e lo storico. Le statistiche
|
||||||
|
/// arrivano dallo stesso aggregato che alimenta la scheda Statistiche: un solo modo
|
||||||
|
/// di calcolarle, così i due pannelli non possono raccontare numeri diversi.
|
||||||
|
/// </summary>
|
||||||
|
private void LoadProducts()
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var selectedIdentity = Products.SelectedProduct?.Identity;
|
||||||
|
|
||||||
|
var stats = CompletedAuctionsStore.AggregateByProduct()
|
||||||
|
.GroupBy(s => s.ProductKey, StringComparer.Ordinal)
|
||||||
|
.ToDictionary(g => g.Key, g => g.First(), StringComparer.Ordinal);
|
||||||
|
|
||||||
|
_productViewModels.Clear();
|
||||||
|
|
||||||
|
foreach (var rule in WatchedProductsStore.GetAll()
|
||||||
|
.OrderByDescending(r => r.IsWatched)
|
||||||
|
.ThenBy(r => r.DisplayName, StringComparer.CurrentCultureIgnoreCase))
|
||||||
|
{
|
||||||
|
stats.TryGetValue(StatsKeyFor(rule), out var summary);
|
||||||
|
_productViewModels.Add(new ProductViewModel(rule, summary));
|
||||||
|
}
|
||||||
|
|
||||||
|
Products.SetProducts(_productViewModels);
|
||||||
|
|
||||||
|
// Ricaricare non deve far perdere il posto in cui si stava lavorando.
|
||||||
|
if (selectedIdentity != null) Products.SelectProduct(selectedIdentity);
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
Log($"[PRODOTTI] Caricamento non riuscito: {ex.Message}", LogLevel.Error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Chiave con cui il prodotto compare nello storico. Le schede salvate senza chiave
|
||||||
|
/// (le più vecchie) la ricavano dal nome, com'è per le aste concluse.
|
||||||
|
/// </summary>
|
||||||
|
private static string StatsKeyFor(WatchedProduct rule) =>
|
||||||
|
string.IsNullOrEmpty(rule.ProductKey)
|
||||||
|
? ProductKeyHelper.GenerateProductKey(rule.DisplayName)
|
||||||
|
: rule.ProductKey;
|
||||||
|
|
||||||
|
private void ToggleProductWatch(ProductViewModel? product)
|
||||||
|
{
|
||||||
|
if (product == null) return;
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
product.IsWatched = !product.IsWatched;
|
||||||
|
WatchedProductsStore.Persist();
|
||||||
|
|
||||||
|
Products.RefreshCounters();
|
||||||
|
MarkWatchedProducts(_catalogAuctions);
|
||||||
|
|
||||||
|
Log(product.IsWatched
|
||||||
|
? $"[PRODOTTI] Ora seguo: {product.DisplayName}"
|
||||||
|
: $"[PRODOTTI] Non seguo più: {product.DisplayName} (i limiti restano)",
|
||||||
|
LogLevel.Info);
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
Log($"[PRODOTTI] {ex.Message}", LogLevel.Error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Mette il prodotto in elenco e apre la scheda Prodotti su quella riga. È la strada
|
||||||
|
/// dell'ingranaggio sulle schede del catalogo: serve a dare limiti a un articolo
|
||||||
|
/// anche senza volerlo seguire.
|
||||||
|
/// </summary>
|
||||||
|
private void ConfigureCatalogProduct(CatalogAuction? auction)
|
||||||
|
{
|
||||||
|
if (auction == null) return;
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var existing = WatchedProductsStore.FindFor(auction);
|
||||||
|
|
||||||
|
if (existing == null)
|
||||||
|
{
|
||||||
|
// Senza stellina: chi arriva da qui vuole i limiti, non l'aggiunta automatica.
|
||||||
|
WatchedProductsStore.Add(auction, watched: false);
|
||||||
|
Log($"[PRODOTTI] Aggiunto all'elenco: {auction.Name}", LogLevel.Success);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
Log($"[PRODOTTI] {auction.Name} era già in elenco: apro la sua riga", LogLevel.Info);
|
||||||
|
}
|
||||||
|
|
||||||
|
MarkWatchedProducts(_catalogAuctions);
|
||||||
|
|
||||||
|
// Il cambio scheda ricarica già l'elenco, ma solo se la scheda cambia
|
||||||
|
// davvero: ricaricare qui rende la riga nuova visibile in ogni caso.
|
||||||
|
TabProdotti.IsChecked = true;
|
||||||
|
LoadProducts();
|
||||||
|
|
||||||
|
var identity = WatchedProductsStore.FindFor(auction)?.Identity;
|
||||||
|
if (identity != null) Products.SelectProduct(identity);
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
Log($"[PRODOTTI] Aggiunta non riuscita: {ex.Message}", LogLevel.Error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Eventi della scheda ──────────────────────────────────────────
|
||||||
|
|
||||||
|
private void Products_RefreshClicked(object sender, RoutedEventArgs e) => LoadProducts();
|
||||||
|
|
||||||
|
private void Products_ProductLimitsEdited(object sender, RoutedEventArgs e)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
PersistProductLimits();
|
||||||
|
|
||||||
|
// Il tetto consigliato dipende dalle puntate massime: se è quella che è
|
||||||
|
// appena cambiata, la colonna accanto deve rifarsi il conto.
|
||||||
|
Products.SelectedProduct?.RefreshAdvice();
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
Log($"[PRODOTTI] Salvataggio non riuscito: {ex.Message}", LogLevel.Error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void Products_ProductSelectionChanged(object sender, RoutedEventArgs e)
|
||||||
|
{
|
||||||
|
// Il riquadro statistiche si aggiorna da solo: è legato alla riga selezionata.
|
||||||
|
}
|
||||||
|
|
||||||
|
private async void Products_ScanNowClicked(object sender, RoutedEventArgs e)
|
||||||
|
{
|
||||||
|
if (_productWatcher == null) return;
|
||||||
|
|
||||||
|
Log("[PRODOTTI] Cerco aste dei prodotti seguiti…", LogLevel.Info);
|
||||||
|
await _productWatcher.ScanNowAsync();
|
||||||
|
LoadProducts();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void Products_RemoveClicked(object sender, RoutedEventArgs e)
|
||||||
|
{
|
||||||
|
var product = Products.SelectedProduct;
|
||||||
|
if (product == null)
|
||||||
|
{
|
||||||
|
MessageBox.Show(this, "Seleziona prima un prodotto dall'elenco.",
|
||||||
|
"Prodotti", MessageBoxButton.OK, MessageBoxImage.Information);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var answer = MessageBox.Show(this,
|
||||||
|
$"Vuoi togliere \"{product.DisplayName}\" dall'elenco?\n\n" +
|
||||||
|
"Si perdono i limiti impostati per questo prodotto. Le aste già nel monitor restano dove sono.",
|
||||||
|
"Prodotti", MessageBoxButton.YesNo, MessageBoxImage.Question);
|
||||||
|
|
||||||
|
if (answer != MessageBoxResult.Yes) return;
|
||||||
|
|
||||||
|
WatchedProductsStore.RemoveByIdentity(product.Identity);
|
||||||
|
LoadProducts();
|
||||||
|
MarkWatchedProducts(_catalogAuctions);
|
||||||
|
|
||||||
|
Log($"[PRODOTTI] Tolto dall'elenco: {product.DisplayName}", LogLevel.Info);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void Products_ClearClicked(object sender, RoutedEventArgs e)
|
||||||
|
{
|
||||||
|
if (_productViewModels.Count == 0) return;
|
||||||
|
|
||||||
|
var answer = MessageBox.Show(this,
|
||||||
|
$"Vuoi svuotare l'elenco dei {_productViewModels.Count} prodotti?\n\n" +
|
||||||
|
"Si perdono tutti i limiti impostati. Le aste già nel monitor restano dove sono.",
|
||||||
|
"Prodotti", MessageBoxButton.YesNo, MessageBoxImage.Warning);
|
||||||
|
|
||||||
|
if (answer != MessageBoxResult.Yes) return;
|
||||||
|
|
||||||
|
WatchedProductsStore.Clear();
|
||||||
|
LoadProducts();
|
||||||
|
MarkWatchedProducts(_catalogAuctions);
|
||||||
|
|
||||||
|
Log("[PRODOTTI] Elenco svuotato", LogLevel.Info);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Scrive nei limiti del prodotto i valori consigliati dallo storico — minimo e
|
||||||
|
/// massimo insieme, perché è la coppia a definire la finestra in cui ha senso
|
||||||
|
/// puntare: un tetto senza pavimento lascia sprecare puntate su aste ancora lontane
|
||||||
|
/// dalla chiusura, un pavimento senza tetto non protegge da niente.
|
||||||
|
/// </summary>
|
||||||
|
private void ApplyAdviceToProduct(ProductViewModel? product)
|
||||||
|
{
|
||||||
|
if (product == null) return;
|
||||||
|
|
||||||
|
if (!product.ApplyAdvice())
|
||||||
|
{
|
||||||
|
MessageBox.Show(this,
|
||||||
|
$"Per \"{product.DisplayName}\" non ci sono abbastanza aste concluse " +
|
||||||
|
$"(ne servono almeno {PriceAdvisor.MinimumSample}) per ricavare un consiglio.",
|
||||||
|
"Limiti consigliati", MessageBoxButton.OK, MessageBoxImage.Information);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
PersistProductLimits();
|
||||||
|
|
||||||
|
Log($"[PRODOTTI] {product.DisplayName}: limiti consigliati applicati " +
|
||||||
|
$"({product.SuggestedMinPrice:F2} € – {product.SuggestedMaxPrice:F2} €)",
|
||||||
|
LogLevel.Success);
|
||||||
|
|
||||||
|
if (product.AdviceWarns)
|
||||||
|
{
|
||||||
|
Log($"[PRODOTTI] {product.DisplayName}: il tetto è sotto il prezzo tipico di " +
|
||||||
|
"aggiudicazione — con questo numero di puntate l'articolo non conviene.",
|
||||||
|
LogLevel.Warning);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void Products_ApplySuggestedClicked(object sender, RoutedEventArgs e)
|
||||||
|
{
|
||||||
|
var product = Products.SelectedProduct;
|
||||||
|
if (product == null)
|
||||||
|
{
|
||||||
|
MessageBox.Show(this, "Seleziona prima un prodotto dall'elenco.",
|
||||||
|
"Limiti consigliati", MessageBoxButton.OK, MessageBoxImage.Information);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
ApplyAdviceToProduct(product);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Applica i limiti consigliati a tutto l'elenco.
|
||||||
|
///
|
||||||
|
/// <para>Chiede conferma dicendo <b>quante righe cambierebbe davvero</b>, non quante
|
||||||
|
/// ce ne sono: su un elenco dove la metà è già a posto, "applico a 40 prodotti"
|
||||||
|
/// sarebbe una risposta a una domanda che nessuno ha fatto. I prodotti senza abbastanza
|
||||||
|
/// storico restano intoccati, e quelli su cui il consiglio avverte vengono contati a
|
||||||
|
/// parte: sono i casi da guardare a mano.</para>
|
||||||
|
/// </summary>
|
||||||
|
private void Products_ApplyAllSuggestedClicked(object sender, RoutedEventArgs e)
|
||||||
|
{
|
||||||
|
var candidates = _productViewModels.Where(p => p.HasAdvice && !p.MatchesAdvice).ToList();
|
||||||
|
var senzaStorico = _productViewModels.Count(p => !p.HasAdvice);
|
||||||
|
|
||||||
|
if (candidates.Count == 0)
|
||||||
|
{
|
||||||
|
MessageBox.Show(this,
|
||||||
|
senzaStorico == _productViewModels.Count
|
||||||
|
? $"Nessun prodotto ha abbastanza aste concluse (ne servono almeno " +
|
||||||
|
$"{PriceAdvisor.MinimumSample}) per ricavare un consiglio."
|
||||||
|
: "I limiti consigliati sono già scritti su tutti i prodotti che ne hanno uno.",
|
||||||
|
"Limiti consigliati", MessageBoxButton.OK, MessageBoxImage.Information);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var avvisi = candidates.Count(p => p.AdviceWarns);
|
||||||
|
|
||||||
|
var messaggio =
|
||||||
|
$"Scrivo i limiti consigliati su {candidates.Count} " +
|
||||||
|
(candidates.Count == 1 ? "prodotto" : "prodotti") + ".\n\n" +
|
||||||
|
"I valori scritti a mano su quei prodotti verranno sostituiti.";
|
||||||
|
|
||||||
|
if (senzaStorico > 0)
|
||||||
|
messaggio += $"\n\n{senzaStorico} prodotti restano invariati: non hanno abbastanza aste concluse.";
|
||||||
|
|
||||||
|
if (avvisi > 0)
|
||||||
|
messaggio += $"\n\nAttenzione: su {avvisi} il tetto resta sotto il prezzo tipico di " +
|
||||||
|
"aggiudicazione — con le puntate impostate quegli articoli non convengono.";
|
||||||
|
|
||||||
|
messaggio += "\n\nProcedo?";
|
||||||
|
|
||||||
|
var answer = MessageBox.Show(this, messaggio, "Limiti consigliati",
|
||||||
|
MessageBoxButton.YesNo, MessageBoxImage.Warning);
|
||||||
|
|
||||||
|
if (answer != MessageBoxResult.Yes) return;
|
||||||
|
|
||||||
|
var applicati = candidates.Count(p => p.ApplyAdvice());
|
||||||
|
|
||||||
|
PersistProductLimits();
|
||||||
|
|
||||||
|
Log($"[PRODOTTI] Limiti consigliati applicati a {applicati} prodotti" +
|
||||||
|
(avvisi > 0 ? $" ({avvisi} con avviso di convenienza)" : ""),
|
||||||
|
LogLevel.Success);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Rende durevoli i limiti e li rispecchia nella scheda prodotto. Le due scritture
|
||||||
|
/// vanno sempre insieme: <c>watched-products.json</c> è la fonte di verità,
|
||||||
|
/// <c>products.json</c> è il file che si apre per capire com'è impostato un articolo,
|
||||||
|
/// e disallinearli vuol dire avere due risposte alla stessa domanda.
|
||||||
|
/// </summary>
|
||||||
|
private void PersistProductLimits()
|
||||||
|
{
|
||||||
|
WatchedProductsStore.Persist();
|
||||||
|
ProductStatsStore.SyncOptionsFromWatchList();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Riscrive i limiti del prodotto sulle aste di quell'articolo già presenti nel
|
||||||
|
/// monitor. Serve la conferma perché sovrascrive anche i ritocchi fatti a mano su
|
||||||
|
/// una singola asta, che è esattamente ciò che qualcuno potrebbe non volere.
|
||||||
|
/// </summary>
|
||||||
|
private void Products_ReapplyClicked(object sender, RoutedEventArgs e)
|
||||||
|
{
|
||||||
|
var product = Products.SelectedProduct;
|
||||||
|
if (product == null)
|
||||||
|
{
|
||||||
|
MessageBox.Show(this, "Seleziona prima un prodotto dall'elenco.",
|
||||||
|
"Riapplica limiti", MessageBoxButton.OK, MessageBoxImage.Information);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var targets = _auctionViewModels
|
||||||
|
.Where(a => product.Rule.MatchesName(a.AuctionInfo.Name))
|
||||||
|
.ToList();
|
||||||
|
|
||||||
|
if (targets.Count == 0)
|
||||||
|
{
|
||||||
|
MessageBox.Show(this,
|
||||||
|
$"Nel monitor non ci sono aste di \"{product.DisplayName}\".",
|
||||||
|
"Riapplica limiti", MessageBoxButton.OK, MessageBoxImage.Information);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var answer = MessageBox.Show(this,
|
||||||
|
$"Riscrivo i limiti di \"{product.DisplayName}\" su {targets.Count} " +
|
||||||
|
(targets.Count == 1 ? "asta" : "aste") + " già nel monitor.\n\n" +
|
||||||
|
"Le modifiche fatte a mano su quelle aste verranno sostituite. Procedo?",
|
||||||
|
"Riapplica limiti", MessageBoxButton.YesNo, MessageBoxImage.Warning);
|
||||||
|
|
||||||
|
if (answer != MessageBoxResult.Yes) return;
|
||||||
|
|
||||||
|
var limits = ProductRuleResolver.Resolve(product.Rule, SettingsManager.Load());
|
||||||
|
|
||||||
|
foreach (var auction in targets)
|
||||||
|
{
|
||||||
|
auction.MinPrice = limits.MinPrice;
|
||||||
|
auction.MaxPrice = limits.MaxPrice;
|
||||||
|
auction.MaxClicks = limits.MaxClicks;
|
||||||
|
}
|
||||||
|
|
||||||
|
SaveAuctions();
|
||||||
|
|
||||||
|
// Il pannello dei dettagli mostra i valori dell'asta selezionata: se è fra
|
||||||
|
// quelle toccate, i campi devono cambiare sotto gli occhi.
|
||||||
|
if (_selectedAuction != null && targets.Contains(_selectedAuction))
|
||||||
|
UpdateAuctionSettingsDisplay(_selectedAuction);
|
||||||
|
|
||||||
|
Log($"[PRODOTTI] Limiti di {product.DisplayName} riapplicati a {targets.Count} aste",
|
||||||
|
LogLevel.Success);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,64 @@
|
|||||||
|
using System;
|
||||||
|
using System.ComponentModel;
|
||||||
|
using AutoBidder.Utilities;
|
||||||
|
|
||||||
|
namespace AutoBidder
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Chiusura ordinata dell'applicazione.
|
||||||
|
///
|
||||||
|
/// Senza, alla chiusura restavano in volo i motori delle aste, la sorveglianza dei
|
||||||
|
/// prodotti e il cerchio di aggiornamento del catalogo: il processo moriva e Windows
|
||||||
|
/// recuperava tutto, ma una puntata in corso veniva troncata a metà senza che nulla
|
||||||
|
/// lo registrasse, e la richiesta di timer a 1 ms non veniva mai restituita al sistema.
|
||||||
|
/// </summary>
|
||||||
|
public partial class MainWindow
|
||||||
|
{
|
||||||
|
private bool _shutdownDone;
|
||||||
|
|
||||||
|
protected override void OnClosing(CancelEventArgs e)
|
||||||
|
{
|
||||||
|
base.OnClosing(e);
|
||||||
|
ShutdownServices();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void ShutdownServices()
|
||||||
|
{
|
||||||
|
if (_shutdownDone) return;
|
||||||
|
_shutdownDone = true;
|
||||||
|
|
||||||
|
// Ogni pezzo va spento separatamente: se uno fallisce, gli altri devono
|
||||||
|
// comunque avere la loro occasione di chiudersi bene.
|
||||||
|
TryShutdown("timer interfaccia", () =>
|
||||||
|
{
|
||||||
|
_headerTimer?.Stop();
|
||||||
|
_userBannerTimer?.Stop();
|
||||||
|
_userHtmlTimer?.Stop();
|
||||||
|
});
|
||||||
|
|
||||||
|
TryShutdown("catalogo", StopCatalogAutoRefresh);
|
||||||
|
|
||||||
|
TryShutdown("sorveglianza prodotti", () => _productWatcher?.Stop());
|
||||||
|
|
||||||
|
TryShutdown("riscatto puntate", () => _freeBidsService?.Stop());
|
||||||
|
|
||||||
|
// Salva prima di spegnere il motore: fermare le aste ne cambia lo stato.
|
||||||
|
TryShutdown("salvataggio aste", SaveAuctions);
|
||||||
|
|
||||||
|
// Ferma i motori per asta e rilascia rete e risoluzione del timer di sistema.
|
||||||
|
TryShutdown("motore aste", () => _auctionMonitor?.Dispose());
|
||||||
|
|
||||||
|
TryShutdown("browser integrato", () => Browser?.EmbeddedWebView?.Dispose());
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void TryShutdown(string what, Action action)
|
||||||
|
{
|
||||||
|
try { action(); }
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
// In chiusura non c'è più un'interfaccia a cui riportarlo.
|
||||||
|
Console.WriteLine($"[SHUTDOWN] {what}: {ex.Message}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,201 @@
|
|||||||
|
using System;
|
||||||
|
using System.Diagnostics;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Windows;
|
||||||
|
using System.Windows.Controls;
|
||||||
|
using AutoBidder.Utilities;
|
||||||
|
|
||||||
|
namespace AutoBidder
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Sezione Statistiche: legge lo storico delle aste concluse
|
||||||
|
/// (CompletedAuctionsStore) e popola la griglia asta per asta.
|
||||||
|
/// </summary>
|
||||||
|
public partial class MainWindow
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Carica lo storico asta per asta.
|
||||||
|
///
|
||||||
|
/// <para>La lettura aggregata per prodotto non sta più qui: vive nella scheda
|
||||||
|
/// Prodotti, accanto ai limiti che quelle stesse cifre servono a decidere.</para>
|
||||||
|
/// </summary>
|
||||||
|
private void LoadStatistics()
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var records = CompletedAuctionsStore.LoadAll();
|
||||||
|
|
||||||
|
StatsDetailGrid.ItemsSource = records;
|
||||||
|
|
||||||
|
StatsPillRecords.Text = records.Count == 1 ? "1 asta" : $"{records.Count} aste";
|
||||||
|
StatsPillWon.Text = $"{records.Count(r => r.WonByMe)} vinte";
|
||||||
|
|
||||||
|
// Con lo storico vuoto la griglia mostrerebbe solo intestazioni:
|
||||||
|
// meglio spiegare che si popola da sola.
|
||||||
|
var empty = records.Count == 0;
|
||||||
|
StatsEmptyState.Visibility = empty ? Visibility.Visible : Visibility.Collapsed;
|
||||||
|
StatsAuctionsPane.Visibility = empty ? Visibility.Collapsed : Visibility.Visible;
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
Log($"[STATISTICHE] Errore caricamento: {ex.Message}", LogLevel.Error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>Ricarica manuale delle statistiche (pulsante).</summary>
|
||||||
|
private void ReloadStatisticsButton_Click(object sender, RoutedEventArgs e)
|
||||||
|
{
|
||||||
|
LoadStatistics();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>Apre la cartella contenente il file storico JSON.</summary>
|
||||||
|
private void OpenStatsFolderButton_Click(object sender, RoutedEventArgs e)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var folder = AppPaths.StatsFolder;
|
||||||
|
if (!string.IsNullOrEmpty(folder) && System.IO.Directory.Exists(folder))
|
||||||
|
{
|
||||||
|
Process.Start(new ProcessStartInfo
|
||||||
|
{
|
||||||
|
FileName = folder,
|
||||||
|
UseShellExecute = true
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
Log($"[STATISTICHE] Impossibile aprire la cartella: {ex.Message}", LogLevel.Error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>Svuota lo storico delle aste concluse (con conferma).</summary>
|
||||||
|
private void ClearStatsButton_Click(object sender, RoutedEventArgs e)
|
||||||
|
{
|
||||||
|
var res = MessageBox.Show(this,
|
||||||
|
"Vuoi eliminare tutto lo storico delle aste concluse? L'operazione non è reversibile.",
|
||||||
|
"Svuota storico",
|
||||||
|
MessageBoxButton.YesNo,
|
||||||
|
MessageBoxImage.Warning);
|
||||||
|
|
||||||
|
if (res == MessageBoxResult.Yes)
|
||||||
|
{
|
||||||
|
CompletedAuctionsStore.Clear();
|
||||||
|
LoadStatistics();
|
||||||
|
Log("[STATISTICHE] Storico svuotato", LogLevel.Info);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Analizza lo storico e toglie le aste incomplete secondo le regole scelte
|
||||||
|
/// dall'utente. Il dialogo mostra il conto prima di toccare qualunque cosa.
|
||||||
|
/// </summary>
|
||||||
|
private void CleanupStatsButton_Click(object sender, RoutedEventArgs e)
|
||||||
|
{
|
||||||
|
var records = CompletedAuctionsStore.LoadAll();
|
||||||
|
|
||||||
|
if (records.Count == 0)
|
||||||
|
{
|
||||||
|
MessageBox.Show(this, "Lo storico e' vuoto: non c'e' nulla da pulire.",
|
||||||
|
"Pulizia", MessageBoxButton.OK, MessageBoxImage.Information);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var dialog = new Dialogs.StatsCleanupDialog(records) { Owner = this };
|
||||||
|
|
||||||
|
if (dialog.ShowDialog() != true) return;
|
||||||
|
|
||||||
|
LoadStatistics();
|
||||||
|
LoadProducts();
|
||||||
|
|
||||||
|
Log($"[STORICO] Pulizia: tolte {dialog.RemovedCount} aste. " +
|
||||||
|
$"Copia di sicurezza in {dialog.BackupPath}", LogLevel.Success);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Chiede al server quante puntate ha speso il vincitore delle aste gia' concluse.
|
||||||
|
///
|
||||||
|
/// <para>Il dato non scade: le aste registrate prima che l'applicazione sapesse
|
||||||
|
/// leggerlo non sono perse, basta richiederle. Senza questo recupero le statistiche
|
||||||
|
/// sulle puntate ripartirebbero da zero e servirebbero mesi per averne abbastanza.</para>
|
||||||
|
/// </summary>
|
||||||
|
private async void BackfillWinnerBidsButton_Click(object sender, RoutedEventArgs e)
|
||||||
|
{
|
||||||
|
var missing = Services.WinnerBidsBackfill.FindMissing();
|
||||||
|
|
||||||
|
if (missing.Count == 0)
|
||||||
|
{
|
||||||
|
MessageBox.Show(this,
|
||||||
|
"Tutte le aste concluse hanno gia' il conteggio delle puntate del vincitore.",
|
||||||
|
"Recupero", MessageBoxButton.OK, MessageBoxImage.Information);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var answer = MessageBox.Show(this,
|
||||||
|
$"Chiedo al server le puntate del vincitore per {missing.Count} aste concluse.\n\n" +
|
||||||
|
"Serve una richiesta per asta, e viaggia in corsia di fondo dietro alle aste " +
|
||||||
|
"in corso: misurato sul campo, circa un'asta e mezza al secondo, quindi " +
|
||||||
|
$"{Math.Max(1, (int)(missing.Count * 1.6 / 60))} minuti circa.\n\n" +
|
||||||
|
"Puoi continuare a usare l'applicazione: le aste in corso hanno la precedenza " +
|
||||||
|
"e non ne risentono.\n\nProcedo?",
|
||||||
|
"Recupera puntate dei vincitori", MessageBoxButton.YesNo, MessageBoxImage.Question);
|
||||||
|
|
||||||
|
if (answer != MessageBoxResult.Yes) return;
|
||||||
|
|
||||||
|
var transport = _auctionMonitor?.GetApiClient()?.Transport;
|
||||||
|
if (transport == null)
|
||||||
|
{
|
||||||
|
Log("[RECUPERO] Trasporto HTTP non disponibile", LogLevel.Error);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var button = sender as System.Windows.Controls.Button;
|
||||||
|
if (button != null) button.IsEnabled = false;
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var backfill = new Services.WinnerBidsBackfill(transport);
|
||||||
|
backfill.OnLog += m => Dispatcher.Invoke(() => Log(m, LogLevel.Warning));
|
||||||
|
|
||||||
|
var lastReported = 0;
|
||||||
|
backfill.OnProgress += (done, total, updated) =>
|
||||||
|
{
|
||||||
|
// Una riga di registro ogni cinquanta: una per asta seppellirebbe
|
||||||
|
// tutto il resto sotto migliaia di righe.
|
||||||
|
if (done - lastReported < 50 && done != total) return;
|
||||||
|
lastReported = done;
|
||||||
|
Dispatcher.Invoke(() =>
|
||||||
|
Log($"[RECUPERO] {done}/{total} aste esaminate, {updated} aggiornate", LogLevel.Info));
|
||||||
|
};
|
||||||
|
|
||||||
|
Log($"[RECUPERO] Avvio: {missing.Count} aste da completare", LogLevel.Info);
|
||||||
|
|
||||||
|
var result = await backfill.RunAsync(0, System.Threading.CancellationToken.None);
|
||||||
|
|
||||||
|
LoadStatistics();
|
||||||
|
LoadProducts();
|
||||||
|
|
||||||
|
Log($"[RECUPERO] Fatto: {result.Updated} aggiornate, {result.Skipped} senza dato utilizzabile, " +
|
||||||
|
$"{result.Failed} non riuscite", LogLevel.Success);
|
||||||
|
|
||||||
|
MessageBox.Show(this,
|
||||||
|
$"Aggiornate {result.Updated} aste su {result.Examined}.\n\n" +
|
||||||
|
(result.Skipped > 0
|
||||||
|
? $"{result.Skipped} senza un conteggio utilizzabile: il server non l'ha dato, " +
|
||||||
|
"oppure non superava il controllo di coerenza col prezzo finale.\n"
|
||||||
|
: "") +
|
||||||
|
(result.Failed > 0 ? $"{result.Failed} non riuscite per errori di rete.\n" : "") +
|
||||||
|
"\nLe statistiche per prodotto sono gia' aggiornate.",
|
||||||
|
"Recupero completato", MessageBoxButton.OK, MessageBoxImage.Information);
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
Log($"[RECUPERO] Interrotto: {ex.Message}", LogLevel.Error);
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
if (button != null) button.IsEnabled = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
using System;
|
using System;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
using System.Windows;
|
using System.Windows;
|
||||||
using System.Windows.Controls;
|
using System.Windows.Controls;
|
||||||
@@ -13,32 +13,54 @@ namespace AutoBidder
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
public partial class MainWindow
|
public partial class MainWindow
|
||||||
{
|
{
|
||||||
|
/// <summary>Firma dell'ultimo log disegnato: asta, righe e ultima voce.</summary>
|
||||||
|
private (string Id, int Count, DateTime Last, int Repeat) _lastLogSignature;
|
||||||
|
|
||||||
|
/// <summary>Firma dell'ultima griglia puntatori disegnata.</summary>
|
||||||
|
private (string Id, int Count, int TotalBids) _lastBiddersSignature;
|
||||||
|
|
||||||
private void UpdateAuctionLog(AuctionViewModel auction)
|
private void UpdateAuctionLog(AuctionViewModel auction)
|
||||||
{
|
{
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
var auctionInfo = auction.AuctionInfo;
|
var auctionInfo = auction.AuctionInfo;
|
||||||
|
var log = auctionInfo.AuctionLog;
|
||||||
|
if (log == null) return;
|
||||||
|
|
||||||
|
// Ricostruire il documento significa creare un Paragraph e un Run per ogni
|
||||||
|
// riga, fino a duecento, a ogni risposta del server. Se il log non è
|
||||||
|
// cambiato non c'è nulla da ridisegnare.
|
||||||
|
var last = log.Count > 0 ? log[^1] : null;
|
||||||
|
var signature = (auction.AuctionId, log.Count,
|
||||||
|
last?.Timestamp ?? DateTime.MinValue, last?.RepeatCount ?? 0);
|
||||||
|
|
||||||
|
if (signature == _lastLogSignature) return;
|
||||||
|
_lastLogSignature = signature;
|
||||||
|
|
||||||
var logBox = SelectedAuctionLog;
|
var logBox = SelectedAuctionLog;
|
||||||
var doc = logBox.Document;
|
var doc = logBox.Document;
|
||||||
doc.Blocks.Clear();
|
doc.Blocks.Clear();
|
||||||
|
|
||||||
foreach (var entry in auctionInfo.AuctionLog)
|
foreach (var entry in log)
|
||||||
{
|
{
|
||||||
var upper = entry.ToUpperInvariant();
|
// Color coding based on structured log level
|
||||||
|
Brush color = entry.Level switch
|
||||||
// Color coding based on log content
|
{
|
||||||
Brush color;
|
Models.AuctionLogLevel.Error => new SolidColorBrush(Color.FromRgb(232, 17, 35)), // Red
|
||||||
if (upper.Contains("[ERRORE]") || upper.Contains("[FAIL]") || upper.Contains("EXCEPTION"))
|
Models.AuctionLogLevel.Warning => new SolidColorBrush(Color.FromRgb(255, 183, 0)), // Yellow/Orange
|
||||||
color = new SolidColorBrush(Color.FromRgb(232, 17, 35)); // Red
|
Models.AuctionLogLevel.Success => new SolidColorBrush(Color.FromRgb(0, 216, 0)), // Green
|
||||||
else if (upper.Contains("[WARN]") || upper.Contains("ATTENZIONE"))
|
Models.AuctionLogLevel.Bid => new SolidColorBrush(Color.FromRgb(0, 216, 0)), // Green
|
||||||
color = new SolidColorBrush(Color.FromRgb(255, 183, 0)); // Yellow/Orange
|
Models.AuctionLogLevel.Strategy => new SolidColorBrush(Color.FromRgb(200, 160, 255)),// Purple
|
||||||
else if (upper.Contains("[OK]") || upper.Contains("SUCCESS"))
|
Models.AuctionLogLevel.Timing => new SolidColorBrush(Color.FromRgb(150, 150, 150)), // Gray
|
||||||
color = new SolidColorBrush(Color.FromRgb(0, 216, 0)); // Green
|
Models.AuctionLogLevel.Debug => new SolidColorBrush(Color.FromRgb(120, 120, 120)), // Dark gray
|
||||||
else
|
_ => new SolidColorBrush(Color.FromRgb(100, 180, 255)) // Light Blue
|
||||||
color = new SolidColorBrush(Color.FromRgb(100, 180, 255)); // Light Blue - #64B4FF (più chiaro e leggibile)
|
};
|
||||||
|
|
||||||
|
var repeatSuffix = entry.RepeatCount > 1 ? $" (x{entry.RepeatCount})" : "";
|
||||||
|
var line = $"[{entry.TimeDisplay}] [{entry.LevelLabel}] {entry.Message}{repeatSuffix}";
|
||||||
|
|
||||||
var p = new System.Windows.Documents.Paragraph { Margin = new Thickness(0, 2, 0, 2) };
|
var p = new System.Windows.Documents.Paragraph { Margin = new Thickness(0, 2, 0, 2) };
|
||||||
var r = new System.Windows.Documents.Run(entry) { Foreground = color };
|
var r = new System.Windows.Documents.Run(line) { Foreground = color };
|
||||||
p.Inlines.Add(r);
|
p.Inlines.Add(r);
|
||||||
doc.Blocks.Add(p);
|
doc.Blocks.Add(p);
|
||||||
}
|
}
|
||||||
@@ -59,13 +81,29 @@ namespace AutoBidder
|
|||||||
{
|
{
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
var bidders = auction.AuctionInfo.BidderStats.Values
|
// Copia sotto lucchetto: il motore riscrive il dizionario dal proprio thread.
|
||||||
.OrderByDescending(b => b.BidCount)
|
var bidders = auction.AuctionInfo.SnapshotBidderStats();
|
||||||
.ToList();
|
|
||||||
|
|
||||||
SelectedAuctionBiddersGrid.ItemsSource = null;
|
// La quota si calcola qui: il singolo puntatore non conosce il totale dell'asta.
|
||||||
SelectedAuctionBiddersGrid.ItemsSource = bidders;
|
var totalBids = bidders.Sum(b => b.BidCount);
|
||||||
SelectedAuctionBiddersCount.Text = $"Utenti: {bidders?.Count ?? 0}";
|
foreach (var bidder in bidders)
|
||||||
|
{
|
||||||
|
bidder.SharePercent = totalBids > 0 ? bidder.BidCount * 100.0 / totalBids : 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Riassegnare ItemsSource ricostruisce l'intera griglia e la fa lampeggiare:
|
||||||
|
// si rifà solo quando i numeri sono davvero cambiati.
|
||||||
|
var signature = (auction.AuctionId, bidders.Count, totalBids);
|
||||||
|
if (signature != _lastBiddersSignature)
|
||||||
|
{
|
||||||
|
_lastBiddersSignature = signature;
|
||||||
|
|
||||||
|
SelectedAuctionBiddersGrid.ItemsSource = null;
|
||||||
|
SelectedAuctionBiddersGrid.ItemsSource = bidders;
|
||||||
|
SelectedAuctionBiddersCount.Text = bidders.Count == 0
|
||||||
|
? "Nessun dato sui puntatori."
|
||||||
|
: $"{bidders.Count} puntatori · {totalBids} puntate osservate";
|
||||||
|
}
|
||||||
|
|
||||||
// ?? NUOVO: Aggiorna il contatore della storia puntate con limite configurato
|
// ?? NUOVO: Aggiorna il contatore della storia puntate con limite configurato
|
||||||
var settings = SettingsManager.Load();
|
var settings = SettingsManager.Load();
|
||||||
@@ -75,7 +113,7 @@ namespace AutoBidder
|
|||||||
var bidHistoryCountTextBlock = AuctionMonitor.FindName("BidHistoryCount") as TextBlock;
|
var bidHistoryCountTextBlock = AuctionMonitor.FindName("BidHistoryCount") as TextBlock;
|
||||||
if (bidHistoryCountTextBlock != null)
|
if (bidHistoryCountTextBlock != null)
|
||||||
{
|
{
|
||||||
// Mostra "Ultime 20 puntate" se il limite è attivo
|
// Mostra "Ultime 20 puntate" se il limite � attivo
|
||||||
if (maxEntries > 0)
|
if (maxEntries > 0)
|
||||||
{
|
{
|
||||||
bidHistoryCountTextBlock.Text = $"Ultime {maxEntries} puntate";
|
bidHistoryCountTextBlock.Text = $"Ultime {maxEntries} puntate";
|
||||||
@@ -98,10 +136,12 @@ namespace AutoBidder
|
|||||||
|
|
||||||
SelectedAuctionName.Text = auction.Name;
|
SelectedAuctionName.Text = auction.Name;
|
||||||
SelectedBidBeforeDeadlineMs.Text = auction.AuctionInfo.BidBeforeDeadlineMs.ToString();
|
SelectedBidBeforeDeadlineMs.Text = auction.AuctionInfo.BidBeforeDeadlineMs.ToString();
|
||||||
SelectedCheckAuctionOpen.IsChecked = auction.AuctionInfo.CheckAuctionOpenBeforeBid;
|
|
||||||
SelectedMinPrice.Text = auction.MinPrice.ToString("F2", System.Globalization.CultureInfo.InvariantCulture);
|
SelectedMinPrice.Text = auction.MinPrice.ToString("F2", System.Globalization.CultureInfo.InvariantCulture);
|
||||||
SelectedMaxPrice.Text = auction.MaxPrice.ToString("F2", System.Globalization.CultureInfo.InvariantCulture);
|
SelectedMaxPrice.Text = auction.MaxPrice.ToString("F2", System.Globalization.CultureInfo.InvariantCulture);
|
||||||
SelectedMaxClicks.Text = auction.MaxClicks.ToString();
|
SelectedMaxClicks.Text = auction.MaxClicks.ToString();
|
||||||
|
AuctionMonitor.SelectedMaxSpend.Text = auction.AuctionInfo.MaxTotalSpendEuro
|
||||||
|
.ToString("F2", System.Globalization.CultureInfo.InvariantCulture);
|
||||||
|
AuctionMonitor.SelectedStopAtBreakEven.IsChecked = auction.AuctionInfo.StopAtBreakEven;
|
||||||
|
|
||||||
var url = auction.AuctionInfo.OriginalUrl;
|
var url = auction.AuctionInfo.OriginalUrl;
|
||||||
if (string.IsNullOrEmpty(url))
|
if (string.IsNullOrEmpty(url))
|
||||||
@@ -151,18 +191,17 @@ namespace AutoBidder
|
|||||||
int canPauseCount = _auctionViewModels.Count(a => a.CanPause);
|
int canPauseCount = _auctionViewModels.Count(a => a.CanPause);
|
||||||
int canStopCount = _auctionViewModels.Count(a => a.CanStop);
|
int canStopCount = _auctionViewModels.Count(a => a.CanStop);
|
||||||
|
|
||||||
// AVVIA TUTTI: abilitato se ALMENO UNA asta può essere avviata
|
// AVVIA TUTTI: abilitato se ALMENO UNA asta pu� essere avviata
|
||||||
// Scuro se NESSUNA asta può essere avviata (tutte già avviate)
|
// Scuro se NESSUNA asta pu� essere avviata (tutte gi� avviate)
|
||||||
StartButton.IsEnabled = canStartCount > 0;
|
StartButton.IsEnabled = canStartCount > 0;
|
||||||
StartButton.Opacity = canStartCount > 0 ? 1.0 : 0.4;
|
StartButton.Opacity = canStartCount > 0 ? 1.0 : 0.4;
|
||||||
|
|
||||||
// PAUSA TUTTI: abilitato se ALMENO UNA asta può essere messa in pausa
|
// OSSERVA TUTTE: abilitato se ALMENO UNA asta non e' gia' in sola osservazione
|
||||||
// Scuro se NESSUNA asta può essere messa in pausa (tutte già in pausa o ferme)
|
|
||||||
PauseAllButton.IsEnabled = canPauseCount > 0;
|
PauseAllButton.IsEnabled = canPauseCount > 0;
|
||||||
PauseAllButton.Opacity = canPauseCount > 0 ? 1.0 : 0.4;
|
PauseAllButton.Opacity = canPauseCount > 0 ? 1.0 : 0.4;
|
||||||
|
|
||||||
// FERMA TUTTI: abilitato se ALMENO UNA asta può essere fermata
|
// FERMA TUTTI: abilitato se ALMENO UNA asta pu� essere fermata
|
||||||
// Scuro se NESSUNA asta può essere fermata (tutte già ferme)
|
// Scuro se NESSUNA asta pu� essere fermata (tutte gi� ferme)
|
||||||
StopButton.IsEnabled = canStopCount > 0;
|
StopButton.IsEnabled = canStopCount > 0;
|
||||||
StopButton.Opacity = canStopCount > 0 ? 1.0 : 0.4;
|
StopButton.Opacity = canStopCount > 0 ? 1.0 : 0.4;
|
||||||
}
|
}
|
||||||
@@ -233,7 +272,6 @@ namespace AutoBidder
|
|||||||
|
|
||||||
// Resetta ai valori predefiniti dalle impostazioni
|
// Resetta ai valori predefiniti dalle impostazioni
|
||||||
_selectedAuction.AuctionInfo.BidBeforeDeadlineMs = settings.DefaultBidBeforeDeadlineMs;
|
_selectedAuction.AuctionInfo.BidBeforeDeadlineMs = settings.DefaultBidBeforeDeadlineMs;
|
||||||
_selectedAuction.AuctionInfo.CheckAuctionOpenBeforeBid = settings.DefaultCheckAuctionOpenBeforeBid;
|
|
||||||
_selectedAuction.MinPrice = settings.DefaultMinPrice;
|
_selectedAuction.MinPrice = settings.DefaultMinPrice;
|
||||||
_selectedAuction.MaxPrice = settings.DefaultMaxPrice;
|
_selectedAuction.MaxPrice = settings.DefaultMaxPrice;
|
||||||
_selectedAuction.MaxClicks = settings.DefaultMaxClicks;
|
_selectedAuction.MaxClicks = settings.DefaultMaxClicks;
|
||||||
@@ -267,7 +305,7 @@ namespace AutoBidder
|
|||||||
}
|
}
|
||||||
|
|
||||||
var result = MessageBox.Show(
|
var result = MessageBox.Show(
|
||||||
$"Pulire la lista degli utenti per questa asta?\n\n{_selectedAuction.Name}\n\nLa lista degli utenti che hanno puntato verrà svuotata.",
|
$"Pulire la lista degli utenti per questa asta?\n\n{_selectedAuction.Name}\n\nLa lista degli utenti che hanno puntato verr� svuotata.",
|
||||||
"Conferma Pulizia",
|
"Conferma Pulizia",
|
||||||
MessageBoxButton.YesNo,
|
MessageBoxButton.YesNo,
|
||||||
MessageBoxImage.Question);
|
MessageBoxImage.Question);
|
||||||
|
|||||||
@@ -12,9 +12,31 @@ namespace AutoBidder
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
public partial class MainWindow
|
public partial class MainWindow
|
||||||
{
|
{
|
||||||
private System.Windows.Threading.DispatcherTimer _userBannerTimer;
|
// Creati in InitializeUserInfo(), non nel costruttore.
|
||||||
private System.Windows.Threading.DispatcherTimer _userHtmlTimer;
|
private System.Windows.Threading.DispatcherTimer? _userBannerTimer;
|
||||||
private SessionService _sessionService; // NUOVO: Servizio centralizzato
|
private System.Windows.Threading.DispatcherTimer? _userHtmlTimer;
|
||||||
|
private System.Windows.Threading.DispatcherTimer? _toConfirmTimer;
|
||||||
|
private SessionService _sessionService = null!;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Aste vinte in attesa di conferma su Bidoo, o <c>-1</c> finché non è arrivata una
|
||||||
|
/// risposta utilizzabile. È un <c>int</c> e non un <c>int?</c> perché lo scrive la
|
||||||
|
/// rete e lo legge il battito dell'interfaccia: un annullabile sono due campi, e
|
||||||
|
/// due campi si possono leggere a metà aggiornamento.
|
||||||
|
/// </summary>
|
||||||
|
private int _auctionsToConfirmRaw = Unknown;
|
||||||
|
|
||||||
|
private const int Unknown = -1;
|
||||||
|
|
||||||
|
/// <summary>Aste da confermare, o <c>null</c> se ancora non si sa.</summary>
|
||||||
|
private int? AuctionsToConfirm
|
||||||
|
{
|
||||||
|
get
|
||||||
|
{
|
||||||
|
var value = System.Threading.Volatile.Read(ref _auctionsToConfirmRaw);
|
||||||
|
return value < 0 ? null : value;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private void InitializeUserInfoTimers()
|
private void InitializeUserInfoTimers()
|
||||||
{
|
{
|
||||||
@@ -29,19 +51,105 @@ namespace AutoBidder
|
|||||||
_userBannerTimer.Interval = TimeSpan.FromMinutes(10);
|
_userBannerTimer.Interval = TimeSpan.FromMinutes(10);
|
||||||
_userBannerTimer.Tick += UserBannerTimer_Tick;
|
_userBannerTimer.Tick += UserBannerTimer_Tick;
|
||||||
_userBannerTimer.Start();
|
_userBannerTimer.Start();
|
||||||
|
|
||||||
|
// Aste da confermare: una richiesta da un byte, quindi si può chiedere spesso.
|
||||||
|
// Un minuto è il compromesso fra "il numero compare subito dopo una vincita"
|
||||||
|
// e "non si tempesta il server per un dato che cambia di rado".
|
||||||
|
_toConfirmTimer = new System.Windows.Threading.DispatcherTimer();
|
||||||
|
_toConfirmTimer.Interval = TimeSpan.FromSeconds(60);
|
||||||
|
_toConfirmTimer.Tick += (_, _) => _ = RefreshAuctionsToConfirmAsync();
|
||||||
|
_toConfirmTimer.Start();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Rilegge da Bidoo quante aste vinte aspettano conferma.
|
||||||
|
///
|
||||||
|
/// <para>Una risposta non utilizzabile <b>non</b> azzera il valore noto: se la rete
|
||||||
|
/// cade, la barra continua a mostrare l'ultimo numero certo invece di far sparire
|
||||||
|
/// una vincita che esiste.</para>
|
||||||
|
/// </summary>
|
||||||
|
private async Task RefreshAuctionsToConfirmAsync()
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var session = _auctionMonitor.GetSession();
|
||||||
|
if (session == null || string.IsNullOrEmpty(session.Username))
|
||||||
|
{
|
||||||
|
System.Threading.Volatile.Write(ref _auctionsToConfirmRaw, Unknown);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var count = await _auctionMonitor.GetAuctionsWonToConfirmAsync().ConfigureAwait(false);
|
||||||
|
if (count.HasValue) System.Threading.Volatile.Write(ref _auctionsToConfirmRaw, count.Value);
|
||||||
|
}
|
||||||
|
catch { /* la barra non deve mai disturbare il resto */ }
|
||||||
}
|
}
|
||||||
|
|
||||||
private void InitializeSessionService()
|
private void InitializeSessionService()
|
||||||
{
|
{
|
||||||
// NUOVO: Inizializza SessionService
|
// NUOVO: Inizializza SessionService
|
||||||
_sessionService = new SessionService(_auctionMonitor.GetApiClient());
|
_sessionService = new SessionService(_auctionMonitor.GetApiClient());
|
||||||
|
|
||||||
// Event handlers
|
// Event handlers
|
||||||
_sessionService.OnLog += (msg) => Log(msg, LogLevel.Info);
|
_sessionService.OnLog += (msg) => Log(msg, LogLevel.Info);
|
||||||
_sessionService.OnSessionChanged += (session) =>
|
_sessionService.OnSessionChanged += (session) =>
|
||||||
{
|
{
|
||||||
Dispatcher.Invoke(() => SetUserBanner(session.Username, session.RemainingBids));
|
Dispatcher.Invoke(() => SetUserBanner(session.Username, session.RemainingBids));
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// La sessione salvata va ripresa, altrimenti salvarla non serve a niente.
|
||||||
|
_ = RestoreSavedSessionAsync();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Riprende la sessione salvata su disco e la riattiva verso Bidoo.
|
||||||
|
///
|
||||||
|
/// Senza questo passaggio il cookie veniva scritto in session.dat e mai più letto:
|
||||||
|
/// a ogni avvio l'applicazione risultava "Non connesso" e bisognava reincollarlo,
|
||||||
|
/// pur avendone una copia valida sul disco.
|
||||||
|
///
|
||||||
|
/// Non blocca l'avvio: la validazione richiede un giro di rete, quindi la finestra
|
||||||
|
/// si apre subito e il banner si aggiorna quando la risposta arriva.
|
||||||
|
/// </summary>
|
||||||
|
private async Task RestoreSavedSessionAsync()
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var saved = _sessionService.LoadSession();
|
||||||
|
if (saved == null || string.IsNullOrWhiteSpace(saved.CookieString)) return;
|
||||||
|
|
||||||
|
Log("[SESSION] Sessione salvata trovata: verifica in corso…", LogLevel.Info);
|
||||||
|
|
||||||
|
// Il cookie potrebbe essere scaduto da giorni: si riattiva contro il server
|
||||||
|
// invece di fidarsi del file.
|
||||||
|
var result = await _sessionService.ValidateAndActivateSessionAsync(
|
||||||
|
saved.CookieString, saved.Username);
|
||||||
|
|
||||||
|
if (result.Success && result.Session != null)
|
||||||
|
{
|
||||||
|
// Il motore deve conoscere il cookie per poter interrogare e puntare.
|
||||||
|
_auctionMonitor.InitializeSessionWithCookie(
|
||||||
|
saved.CookieString, result.Session.Username);
|
||||||
|
|
||||||
|
Dispatcher.Invoke(() =>
|
||||||
|
{
|
||||||
|
SetUserBanner(result.Session.Username, result.Session.RemainingBids);
|
||||||
|
RefreshSettingsSessionStatus();
|
||||||
|
});
|
||||||
|
|
||||||
|
Log($"[SESSION] Riconnesso come {result.Session.Username}", LogLevel.Success);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
// Si lascia il cookie sul disco: l'utente lo vede in Impostazioni e
|
||||||
|
// decide se rinnovarlo dal browser.
|
||||||
|
Log($"[SESSION] Sessione salvata non più valida: {result.ErrorMessage}", LogLevel.Warning);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
Log($"[SESSION] Ripristino non riuscito: {ex.Message}", LogLevel.Error);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private void SetUserBanner(string username, int? remainingBids)
|
private void SetUserBanner(string username, int? remainingBids)
|
||||||
@@ -53,26 +161,20 @@ namespace AutoBidder
|
|||||||
if (!string.IsNullOrEmpty(username))
|
if (!string.IsNullOrEmpty(username))
|
||||||
{
|
{
|
||||||
// === CONNESSO ===
|
// === CONNESSO ===
|
||||||
|
|
||||||
// Header - Puntate + Credito
|
// Puntate, credito e aste da confermare li ridisegna il battito da un
|
||||||
RemainingBidsText.Text = remainingBids?.ToString() ?? "0";
|
// secondo (RefreshAccountPills) leggendo la sessione viva: scriverli
|
||||||
|
// anche qui non aggiungerebbe nulla e riporterebbe il rischio di due
|
||||||
if (session?.ShopCredit > 0)
|
// fonti che si contraddicono.
|
||||||
{
|
AuctionMonitor.UpdateAccountStatus(
|
||||||
AuctionMonitor.ShopCreditText.Text = $"EUR {session.ShopCredit:F2}";
|
remainingBids ?? session?.RemainingBids,
|
||||||
}
|
session is null ? null : (decimal)session.ShopCredit,
|
||||||
else
|
AuctionsToConfirm);
|
||||||
{
|
|
||||||
AuctionMonitor.ShopCreditText.Text = "EUR 0.00";
|
// Appena la sessione è viva si può finalmente chiedere quante vincite
|
||||||
}
|
// aspettano conferma: prima non si poteva sapere.
|
||||||
|
_ = RefreshAuctionsToConfirmAsync();
|
||||||
// Aste vinte
|
|
||||||
BannerAsteDaRiscattare.Text = "0";
|
|
||||||
|
|
||||||
// Indicatore limite puntate
|
|
||||||
var settings = Utilities.SettingsManager.Load();
|
|
||||||
UpdateMinBidsIndicator(settings.MinimumRemainingBids);
|
|
||||||
|
|
||||||
// === SIDEBAR - Mostra dati utente ===
|
// === SIDEBAR - Mostra dati utente ===
|
||||||
SidebarUsernameText.Text = username;
|
SidebarUsernameText.Text = username;
|
||||||
SidebarUsernameText.Foreground = new System.Windows.Media.SolidColorBrush(
|
SidebarUsernameText.Foreground = new System.Windows.Media.SolidColorBrush(
|
||||||
@@ -80,7 +182,7 @@ namespace AutoBidder
|
|||||||
SidebarUsernameText.FontWeight = System.Windows.FontWeights.Bold;
|
SidebarUsernameText.FontWeight = System.Windows.FontWeights.Bold;
|
||||||
SidebarUsernameText.ToolTip = $"Connesso come {username} - Click per disconnettere";
|
SidebarUsernameText.ToolTip = $"Connesso come {username} - Click per disconnettere";
|
||||||
|
|
||||||
// Mostra dettagli (ID + Email)
|
// Solo l'ID: l'indirizzo di posta non aggiungeva nulla di utile qui.
|
||||||
if (session?.UserId > 0)
|
if (session?.UserId > 0)
|
||||||
{
|
{
|
||||||
SidebarUserIdText.Text = $"ID: {session.UserId}";
|
SidebarUserIdText.Text = $"ID: {session.UserId}";
|
||||||
@@ -90,31 +192,21 @@ namespace AutoBidder
|
|||||||
{
|
{
|
||||||
SidebarUserIdText.Visibility = System.Windows.Visibility.Collapsed;
|
SidebarUserIdText.Visibility = System.Windows.Visibility.Collapsed;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!string.IsNullOrEmpty(session?.Email))
|
|
||||||
{
|
|
||||||
SidebarUserEmailText.Text = session.Email;
|
|
||||||
SidebarUserEmailText.Visibility = System.Windows.Visibility.Visible;
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
SidebarUserEmailText.Visibility = System.Windows.Visibility.Collapsed;
|
|
||||||
}
|
|
||||||
|
|
||||||
SidebarUserDetailsPanel.Visibility = System.Windows.Visibility.Visible;
|
SidebarUserDetailsPanel.Visibility = System.Windows.Visibility.Visible;
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
// === NON CONNESSO ===
|
// === NON CONNESSO ===
|
||||||
|
|
||||||
// Reset header
|
// Senza sessione questi numeri non esistono: un trattino lo dice,
|
||||||
RemainingBidsText.Text = "0";
|
// uno zero mentirebbe.
|
||||||
AuctionMonitor.ShopCreditText.Text = "EUR 0.00";
|
System.Threading.Volatile.Write(ref _auctionsToConfirmRaw, Unknown);
|
||||||
BannerAsteDaRiscattare.Text = "0";
|
AuctionMonitor.UpdateAccountStatus(null, null, null);
|
||||||
|
|
||||||
// Nascondi indicatore limite
|
// Nascondi indicatore limite
|
||||||
MinBidsLimitIndicator.Visibility = Visibility.Collapsed;
|
MinBidsLimitIndicator.Visibility = Visibility.Collapsed;
|
||||||
|
|
||||||
// === SIDEBAR - Mostra "Non connesso" ===
|
// === SIDEBAR - Mostra "Non connesso" ===
|
||||||
SidebarUsernameText.Text = "Non connesso";
|
SidebarUsernameText.Text = "Non connesso";
|
||||||
SidebarUsernameText.Foreground = new System.Windows.Media.SolidColorBrush(
|
SidebarUsernameText.Foreground = new System.Windows.Media.SolidColorBrush(
|
||||||
@@ -165,12 +257,12 @@ namespace AutoBidder
|
|||||||
// Aggiorna UI con stato connesso (ottimistico)
|
// Aggiorna UI con stato connesso (ottimistico)
|
||||||
SetUserBanner(session.Username, session.RemainingBids);
|
SetUserBanner(session.Username, session.RemainingBids);
|
||||||
|
|
||||||
// Verifica validità cookie in background
|
// Verifica validit� cookie in background
|
||||||
System.Threading.Tasks.Task.Run(async () =>
|
System.Threading.Tasks.Task.Run(async () =>
|
||||||
{
|
{
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
Log("[SESSION] Verifica validità sessione...", LogLevel.Info);
|
Log("[SESSION] Verifica validit� sessione...", LogLevel.Info);
|
||||||
var success = await _auctionMonitor.UpdateUserInfoAsync();
|
var success = await _auctionMonitor.UpdateUserInfoAsync();
|
||||||
var updatedSession = _auctionMonitor.GetSession();
|
var updatedSession = _auctionMonitor.GetSession();
|
||||||
|
|
||||||
@@ -234,9 +326,9 @@ namespace AutoBidder
|
|||||||
Log("[WARN] WebView non inizializzata dopo 60 secondi", LogLevel.Warning);
|
Log("[WARN] WebView non inizializzata dopo 60 secondi", LogLevel.Warning);
|
||||||
Log("[INFO] Per accedere:", LogLevel.Info);
|
Log("[INFO] Per accedere:", LogLevel.Info);
|
||||||
Log("[INFO] 1. Click su 'Non connesso' nella sidebar", LogLevel.Info);
|
Log("[INFO] 1. Click su 'Non connesso' nella sidebar", LogLevel.Info);
|
||||||
Log("[INFO] 2. Si aprirà la scheda Browser", LogLevel.Info);
|
Log("[INFO] 2. Si aprir� la scheda Browser", LogLevel.Info);
|
||||||
Log("[INFO] 3. Fai login su Bidoo", LogLevel.Info);
|
Log("[INFO] 3. Fai login su Bidoo", LogLevel.Info);
|
||||||
Log("[INFO] 4. La connessione sarà automatica", LogLevel.Info);
|
Log("[INFO] 4. La connessione sar� automatica", LogLevel.Info);
|
||||||
});
|
});
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -251,9 +343,9 @@ namespace AutoBidder
|
|||||||
Log("[INFO] Nessun cookie nel browser", LogLevel.Info);
|
Log("[INFO] Nessun cookie nel browser", LogLevel.Info);
|
||||||
Log("[INFO] Per accedere:", LogLevel.Info);
|
Log("[INFO] Per accedere:", LogLevel.Info);
|
||||||
Log("[INFO] 1. Click su 'Non connesso' nella sidebar", LogLevel.Info);
|
Log("[INFO] 1. Click su 'Non connesso' nella sidebar", LogLevel.Info);
|
||||||
Log("[INFO] 2. Si aprirà la scheda Browser", LogLevel.Info);
|
Log("[INFO] 2. Si aprir� la scheda Browser", LogLevel.Info);
|
||||||
Log("[INFO] 3. Fai login su Bidoo", LogLevel.Info);
|
Log("[INFO] 3. Fai login su Bidoo", LogLevel.Info);
|
||||||
Log("[INFO] 4. La connessione sarà automatica", LogLevel.Info);
|
Log("[INFO] 4. La connessione sar� automatica", LogLevel.Info);
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
@@ -269,18 +361,17 @@ namespace AutoBidder
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Aggiorna immediatamente il banner delle puntate residue (chiamato dopo ogni puntata)
|
/// Ridisegna subito la barra del conto dopo una puntata, senza aspettare il battito.
|
||||||
|
///
|
||||||
|
/// <para>Prima aggiornava il numero solo se era maggiore di zero: finite le puntate,
|
||||||
|
/// la barra restava sull'ultimo valore positivo — cioè mentiva proprio nel momento
|
||||||
|
/// in cui contava di più.</para>
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public void UpdateRemainingBidsDisplay()
|
public void UpdateRemainingBidsDisplay()
|
||||||
{
|
{
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
var session = _sessionService?.GetCurrentSession();
|
RefreshAccountPills();
|
||||||
if (session != null && session.RemainingBids > 0)
|
|
||||||
{
|
|
||||||
RemainingBidsText.Text = session.RemainingBids.ToString();
|
|
||||||
Log($"[BANNER UPDATE] Puntate residue aggiornate: {session.RemainingBids}", LogLevel.Info);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
{
|
{
|
||||||
@@ -289,48 +380,41 @@ namespace AutoBidder
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// ? Aggiorna l'indicatore del limite minimo puntate nel banner
|
/// Indicatore del limite minimo puntate, accanto al saldo.
|
||||||
|
///
|
||||||
|
/// <para>Il colore va ricalcolato anche a zero puntate: era proprio il caso in cui
|
||||||
|
/// prima restava dell'ultimo colore utile, cioè verde, mentre il conto era vuoto.</para>
|
||||||
/// </summary>
|
/// </summary>
|
||||||
private void UpdateMinBidsIndicator(int minBidsLimit)
|
private void UpdateMinBidsIndicator(int minBidsLimit)
|
||||||
{
|
{
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
if (minBidsLimit > 0)
|
if (minBidsLimit <= 0)
|
||||||
{
|
{
|
||||||
// Mostra indicatore con solo il numero tra parentesi
|
|
||||||
MinBidsLimitIndicator.Visibility = Visibility.Visible;
|
|
||||||
MinBidsLimitIndicator.Text = $"({minBidsLimit})";
|
|
||||||
MinBidsLimitIndicator.ToolTip = $"Limite minimo puntate attivo: non scendera sotto {minBidsLimit} puntate";
|
|
||||||
|
|
||||||
// Colore basato su puntate residue
|
|
||||||
var session = _sessionService?.GetCurrentSession();
|
|
||||||
if (session != null && session.RemainingBids > 0)
|
|
||||||
{
|
|
||||||
if (session.RemainingBids <= minBidsLimit)
|
|
||||||
{
|
|
||||||
// Al limite - Rosso chiaro (più visibile su sfondo scuro)
|
|
||||||
MinBidsLimitIndicator.Foreground = new System.Windows.Media.SolidColorBrush(
|
|
||||||
System.Windows.Media.Color.FromRgb(255, 82, 82)); // #FF5252 - Rosso chiaro
|
|
||||||
}
|
|
||||||
else if (session.RemainingBids <= minBidsLimit + 10)
|
|
||||||
{
|
|
||||||
// Vicino al limite - Giallo
|
|
||||||
MinBidsLimitIndicator.Foreground = new System.Windows.Media.SolidColorBrush(
|
|
||||||
System.Windows.Media.Color.FromRgb(255, 193, 7)); // #FFC107 - Giallo
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
// Sopra il limite - Verde
|
|
||||||
MinBidsLimitIndicator.Foreground = new System.Windows.Media.SolidColorBrush(
|
|
||||||
System.Windows.Media.Color.FromRgb(0, 216, 0)); // #00D800 - Verde
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
// Nascondi indicatore
|
|
||||||
MinBidsLimitIndicator.Visibility = Visibility.Collapsed;
|
MinBidsLimitIndicator.Visibility = Visibility.Collapsed;
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
MinBidsLimitIndicator.Visibility = Visibility.Visible;
|
||||||
|
MinBidsLimitIndicator.Text = $"({minBidsLimit})";
|
||||||
|
MinBidsLimitIndicator.ToolTip =
|
||||||
|
$"Limite minimo puntate attivo: il bot non scende sotto {minBidsLimit} puntate";
|
||||||
|
|
||||||
|
var session = _auctionMonitor.GetSession();
|
||||||
|
if (session == null || string.IsNullOrEmpty(session.Username))
|
||||||
|
{
|
||||||
|
MinBidsLimitIndicator.SetResourceReference(
|
||||||
|
System.Windows.Controls.TextBlock.ForegroundProperty, "Brush.TextFaint");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var key =
|
||||||
|
session.RemainingBids <= minBidsLimit ? "Brush.Danger" :
|
||||||
|
session.RemainingBids <= minBidsLimit + 10 ? "Brush.Warning" :
|
||||||
|
"Brush.Success";
|
||||||
|
|
||||||
|
MinBidsLimitIndicator.SetResourceReference(
|
||||||
|
System.Windows.Controls.TextBlock.ForegroundProperty, key);
|
||||||
}
|
}
|
||||||
catch { }
|
catch { }
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -38,6 +38,8 @@ namespace AutoBidder
|
|||||||
// Salva tab corrente e switcha temporaneamente a Browser
|
// Salva tab corrente e switcha temporaneamente a Browser
|
||||||
var wasVisible = Browser.Visibility == Visibility.Visible;
|
var wasVisible = Browser.Visibility == Visibility.Visible;
|
||||||
var currentTab = TabAsteAttive.IsChecked == true ? "AsteAttive" :
|
var currentTab = TabAsteAttive.IsChecked == true ? "AsteAttive" :
|
||||||
|
TabCerca.IsChecked == true ? "Cerca" :
|
||||||
|
TabProdotti.IsChecked == true ? "Prodotti" :
|
||||||
TabBrowser.IsChecked == true ? "Browser" :
|
TabBrowser.IsChecked == true ? "Browser" :
|
||||||
TabPuntateGratis.IsChecked == true ? "PuntateGratis" :
|
TabPuntateGratis.IsChecked == true ? "PuntateGratis" :
|
||||||
TabDatiStatistici.IsChecked == true ? "DatiStatistici" :
|
TabDatiStatistici.IsChecked == true ? "DatiStatistici" :
|
||||||
@@ -85,9 +87,18 @@ namespace AutoBidder
|
|||||||
TabAsteAttive.IsChecked = true;
|
TabAsteAttive.IsChecked = true;
|
||||||
AuctionMonitor.Visibility = Visibility.Visible;
|
AuctionMonitor.Visibility = Visibility.Visible;
|
||||||
break;
|
break;
|
||||||
|
case "Cerca":
|
||||||
|
// "Cerca" mostra lo stesso controllo Browser, in modalità catalogo.
|
||||||
|
TabCerca.IsChecked = true;
|
||||||
|
Browser.Visibility = Visibility.Visible;
|
||||||
|
break;
|
||||||
|
case "Prodotti":
|
||||||
|
TabProdotti.IsChecked = true;
|
||||||
|
Products.Visibility = Visibility.Visible;
|
||||||
|
break;
|
||||||
case "PuntateGratis":
|
case "PuntateGratis":
|
||||||
TabPuntateGratis.IsChecked = true;
|
TabPuntateGratis.IsChecked = true;
|
||||||
PuntateGratisPanel.Visibility = Visibility.Visible;
|
FreeBids.Visibility = Visibility.Visible;
|
||||||
break;
|
break;
|
||||||
case "DatiStatistici":
|
case "DatiStatistici":
|
||||||
TabDatiStatistici.IsChecked = true;
|
TabDatiStatistici.IsChecked = true;
|
||||||
@@ -113,15 +124,15 @@ namespace AutoBidder
|
|||||||
// Registra evento per rilevare login automatico
|
// Registra evento per rilevare login automatico
|
||||||
EmbeddedWebView.CoreWebView2.NavigationCompleted += OnWebViewNavigationCompleted;
|
EmbeddedWebView.CoreWebView2.NavigationCompleted += OnWebViewNavigationCompleted;
|
||||||
|
|
||||||
// Notifica che WebView è pronta
|
// Notifica che WebView � pronta
|
||||||
_webViewInitCompletionSource?.TrySetResult(true);
|
_webViewInitCompletionSource?.TrySetResult(true);
|
||||||
|
|
||||||
// Verifica immediata se c'è già un cookie
|
// Verifica immediata se c'� gi� un cookie
|
||||||
await CheckAndImportCookieIfAvailable();
|
await CheckAndImportCookieIfAvailable();
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
Log("[ERROR] CoreWebView2 è null dopo init", LogLevel.Error);
|
Log("[ERROR] CoreWebView2 � null dopo init", LogLevel.Error);
|
||||||
_webViewInitCompletionSource?.TrySetResult(false);
|
_webViewInitCompletionSource?.TrySetResult(false);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -188,7 +199,7 @@ namespace AutoBidder
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Evento chiamato quando la navigazione nella WebView è completata
|
/// Evento chiamato quando la navigazione nella WebView � completata
|
||||||
/// Rileva automaticamente se l'utente ha effettuato il login
|
/// Rileva automaticamente se l'utente ha effettuato il login
|
||||||
/// </summary>
|
/// </summary>
|
||||||
private async void OnWebViewNavigationCompleted(object? sender, CoreWebView2NavigationCompletedEventArgs e)
|
private async void OnWebViewNavigationCompleted(object? sender, CoreWebView2NavigationCompletedEventArgs e)
|
||||||
@@ -200,7 +211,7 @@ namespace AutoBidder
|
|||||||
|
|
||||||
var url = EmbeddedWebView.CoreWebView2.Source;
|
var url = EmbeddedWebView.CoreWebView2.Source;
|
||||||
|
|
||||||
// Se l'utente è sulla homepage di Bidoo (dopo login), verifica cookie
|
// Se l'utente � sulla homepage di Bidoo (dopo login), verifica cookie
|
||||||
if (url.Contains("bidoo.com") && !url.Contains("login"))
|
if (url.Contains("bidoo.com") && !url.Contains("login"))
|
||||||
{
|
{
|
||||||
// ? REFACTORED: Delega a CheckAndImportCookieIfAvailable
|
// ? REFACTORED: Delega a CheckAndImportCookieIfAvailable
|
||||||
@@ -303,7 +314,7 @@ namespace AutoBidder
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
// ? NOTA: Non aggiorna più TextBox (rimossa) - direttamente alla validazione
|
// ? NOTA: Non aggiorna pi� TextBox (rimossa) - direttamente alla validazione
|
||||||
|
|
||||||
// Valida e attiva il cookie usando SessionService
|
// Valida e attiva il cookie usando SessionService
|
||||||
var result = await _sessionService.ValidateAndActivateSessionAsync(cookieString);
|
var result = await _sessionService.ValidateAndActivateSessionAsync(cookieString);
|
||||||
@@ -334,7 +345,7 @@ namespace AutoBidder
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Verifica se WebView2 è pronta per l'uso
|
/// Verifica se WebView2 � pronta per l'uso
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public bool IsWebViewReady()
|
public bool IsWebViewReady()
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -1 +0,0 @@
|
|||||||
# This file ensures the data directory is tracked by git
|
|
||||||
@@ -1,27 +0,0 @@
|
|||||||
using Microsoft.AspNetCore.Identity.EntityFrameworkCore;
|
|
||||||
using Microsoft.EntityFrameworkCore;
|
|
||||||
using AutoBidder.Models;
|
|
||||||
|
|
||||||
namespace AutoBidder.Data;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// DbContext per autenticazione Identity
|
|
||||||
/// </summary>
|
|
||||||
public class ApplicationDbContext : IdentityDbContext<ApplicationUser>
|
|
||||||
{
|
|
||||||
public ApplicationDbContext(DbContextOptions<ApplicationDbContext> options)
|
|
||||||
: base(options)
|
|
||||||
{
|
|
||||||
}
|
|
||||||
|
|
||||||
protected override void OnModelCreating(ModelBuilder builder)
|
|
||||||
{
|
|
||||||
base.OnModelCreating(builder);
|
|
||||||
|
|
||||||
// Personalizza nomi tabelle Identity (opzionale)
|
|
||||||
builder.Entity<ApplicationUser>(entity =>
|
|
||||||
{
|
|
||||||
entity.ToTable("Users");
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,211 +0,0 @@
|
|||||||
using Microsoft.EntityFrameworkCore;
|
|
||||||
using AutoBidder.Models;
|
|
||||||
|
|
||||||
namespace AutoBidder.Data
|
|
||||||
{
|
|
||||||
/// <summary>
|
|
||||||
/// Context Entity Framework per PostgreSQL - Database Statistiche Aste
|
|
||||||
/// Gestisce aste concluse, metriche strategiche e analisi performance
|
|
||||||
/// </summary>
|
|
||||||
public class PostgresStatsContext : DbContext
|
|
||||||
{
|
|
||||||
public PostgresStatsContext(DbContextOptions<PostgresStatsContext> options)
|
|
||||||
: base(options)
|
|
||||||
{
|
|
||||||
}
|
|
||||||
|
|
||||||
// Tabelle principali
|
|
||||||
public DbSet<CompletedAuction> CompletedAuctions { get; set; }
|
|
||||||
public DbSet<BidderPerformance> BidderPerformances { get; set; }
|
|
||||||
public DbSet<ProductStatistic> ProductStatistics { get; set; }
|
|
||||||
public DbSet<DailyMetric> DailyMetrics { get; set; }
|
|
||||||
public DbSet<StrategicInsight> StrategicInsights { get; set; }
|
|
||||||
|
|
||||||
protected override void OnModelCreating(ModelBuilder modelBuilder)
|
|
||||||
{
|
|
||||||
base.OnModelCreating(modelBuilder);
|
|
||||||
|
|
||||||
// Configurazione CompletedAuction
|
|
||||||
modelBuilder.Entity<CompletedAuction>(entity =>
|
|
||||||
{
|
|
||||||
entity.ToTable("completed_auctions");
|
|
||||||
entity.HasKey(e => e.Id);
|
|
||||||
|
|
||||||
entity.Property(e => e.Id).HasColumnName("id");
|
|
||||||
entity.Property(e => e.AuctionId).HasColumnName("auction_id").IsRequired().HasMaxLength(100);
|
|
||||||
entity.Property(e => e.ProductName).HasColumnName("product_name").IsRequired().HasMaxLength(500);
|
|
||||||
entity.Property(e => e.FinalPrice).HasColumnName("final_price").HasColumnType("decimal(10,2)");
|
|
||||||
entity.Property(e => e.BuyNowPrice).HasColumnName("buy_now_price").HasColumnType("decimal(10,2)");
|
|
||||||
entity.Property(e => e.ShippingCost).HasColumnName("shipping_cost").HasColumnType("decimal(10,2)");
|
|
||||||
entity.Property(e => e.TotalBids).HasColumnName("total_bids");
|
|
||||||
entity.Property(e => e.MyBidsCount).HasColumnName("my_bids_count");
|
|
||||||
entity.Property(e => e.ResetCount).HasColumnName("reset_count");
|
|
||||||
entity.Property(e => e.Won).HasColumnName("won");
|
|
||||||
entity.Property(e => e.WinnerUsername).HasColumnName("winner_username").HasMaxLength(100);
|
|
||||||
entity.Property(e => e.CompletedAt).HasColumnName("completed_at");
|
|
||||||
entity.Property(e => e.DurationSeconds).HasColumnName("duration_seconds");
|
|
||||||
entity.Property(e => e.AverageLatency).HasColumnName("average_latency").HasColumnType("decimal(10,2)");
|
|
||||||
entity.Property(e => e.Savings).HasColumnName("savings").HasColumnType("decimal(10,2)");
|
|
||||||
entity.Property(e => e.TotalCost).HasColumnName("total_cost").HasColumnType("decimal(10,2)");
|
|
||||||
entity.Property(e => e.CreatedAt).HasColumnName("created_at").HasDefaultValueSql("CURRENT_TIMESTAMP");
|
|
||||||
|
|
||||||
entity.HasIndex(e => e.AuctionId).HasDatabaseName("idx_auction_id");
|
|
||||||
entity.HasIndex(e => e.ProductName).HasDatabaseName("idx_product_name");
|
|
||||||
entity.HasIndex(e => e.CompletedAt).HasDatabaseName("idx_completed_at");
|
|
||||||
entity.HasIndex(e => e.Won).HasDatabaseName("idx_won");
|
|
||||||
});
|
|
||||||
|
|
||||||
// Configurazione BidderPerformance
|
|
||||||
modelBuilder.Entity<BidderPerformance>(entity =>
|
|
||||||
{
|
|
||||||
entity.ToTable("bidder_performances");
|
|
||||||
entity.HasKey(e => e.Id);
|
|
||||||
|
|
||||||
entity.Property(e => e.Id).HasColumnName("id");
|
|
||||||
entity.Property(e => e.Username).HasColumnName("username").IsRequired().HasMaxLength(100);
|
|
||||||
entity.Property(e => e.TotalAuctions).HasColumnName("total_auctions");
|
|
||||||
entity.Property(e => e.AuctionsWon).HasColumnName("auctions_won");
|
|
||||||
entity.Property(e => e.AuctionsLost).HasColumnName("auctions_lost");
|
|
||||||
entity.Property(e => e.TotalBidsPlaced).HasColumnName("total_bids_placed");
|
|
||||||
entity.Property(e => e.WinRate).HasColumnName("win_rate").HasColumnType("decimal(5,2)");
|
|
||||||
entity.Property(e => e.AverageBidsPerAuction).HasColumnName("average_bids_per_auction").HasColumnType("decimal(10,2)");
|
|
||||||
entity.Property(e => e.AverageCompetition).HasColumnName("average_competition").HasColumnType("decimal(10,2)");
|
|
||||||
entity.Property(e => e.IsAggressive).HasColumnName("is_aggressive");
|
|
||||||
entity.Property(e => e.LastSeenAt).HasColumnName("last_seen_at");
|
|
||||||
entity.Property(e => e.UpdatedAt).HasColumnName("updated_at").HasDefaultValueSql("CURRENT_TIMESTAMP");
|
|
||||||
|
|
||||||
entity.HasIndex(e => e.Username).IsUnique().HasDatabaseName("idx_username");
|
|
||||||
entity.HasIndex(e => e.WinRate).HasDatabaseName("idx_win_rate");
|
|
||||||
});
|
|
||||||
|
|
||||||
// Configurazione ProductStatistic
|
|
||||||
modelBuilder.Entity<ProductStatistic>(entity =>
|
|
||||||
{
|
|
||||||
entity.ToTable("product_statistics");
|
|
||||||
entity.HasKey(e => e.Id);
|
|
||||||
|
|
||||||
entity.Property(e => e.Id).HasColumnName("id");
|
|
||||||
entity.Property(e => e.ProductKey).HasColumnName("product_key").IsRequired().HasMaxLength(200);
|
|
||||||
entity.Property(e => e.ProductName).HasColumnName("product_name").IsRequired().HasMaxLength(500);
|
|
||||||
entity.Property(e => e.TotalAuctions).HasColumnName("total_auctions");
|
|
||||||
entity.Property(e => e.AverageWinningBids).HasColumnName("average_winning_bids").HasColumnType("decimal(10,2)");
|
|
||||||
entity.Property(e => e.AverageFinalPrice).HasColumnName("average_final_price").HasColumnType("decimal(10,2)");
|
|
||||||
entity.Property(e => e.AverageResets).HasColumnName("average_resets").HasColumnType("decimal(10,2)");
|
|
||||||
entity.Property(e => e.MinBidsSeen).HasColumnName("min_bids_seen");
|
|
||||||
entity.Property(e => e.MaxBidsSeen).HasColumnName("max_bids_seen");
|
|
||||||
entity.Property(e => e.RecommendedMaxBids).HasColumnName("recommended_max_bids");
|
|
||||||
entity.Property(e => e.RecommendedMaxPrice).HasColumnName("recommended_max_price").HasColumnType("decimal(10,2)");
|
|
||||||
entity.Property(e => e.CompetitionLevel).HasColumnName("competition_level").HasMaxLength(20);
|
|
||||||
entity.Property(e => e.LastUpdated).HasColumnName("last_updated").HasDefaultValueSql("CURRENT_TIMESTAMP");
|
|
||||||
|
|
||||||
entity.HasIndex(e => e.ProductKey).IsUnique().HasDatabaseName("idx_product_key");
|
|
||||||
entity.HasIndex(e => e.ProductName).HasDatabaseName("idx_product_name_stats");
|
|
||||||
});
|
|
||||||
|
|
||||||
// Configurazione DailyMetric
|
|
||||||
modelBuilder.Entity<DailyMetric>(entity =>
|
|
||||||
{
|
|
||||||
entity.ToTable("daily_metrics");
|
|
||||||
entity.HasKey(e => e.Id);
|
|
||||||
|
|
||||||
entity.Property(e => e.Id).HasColumnName("id");
|
|
||||||
entity.Property(e => e.Date).HasColumnName("date").HasColumnType("date");
|
|
||||||
entity.Property(e => e.TotalBidsUsed).HasColumnName("total_bids_used");
|
|
||||||
entity.Property(e => e.MoneySpent).HasColumnName("money_spent").HasColumnType("decimal(10,2)");
|
|
||||||
entity.Property(e => e.AuctionsWon).HasColumnName("auctions_won");
|
|
||||||
entity.Property(e => e.AuctionsLost).HasColumnName("auctions_lost");
|
|
||||||
entity.Property(e => e.TotalSavings).HasColumnName("total_savings").HasColumnType("decimal(10,2)");
|
|
||||||
entity.Property(e => e.AverageLatency).HasColumnName("average_latency").HasColumnType("decimal(10,2)");
|
|
||||||
entity.Property(e => e.WinRate).HasColumnName("win_rate").HasColumnType("decimal(5,2)");
|
|
||||||
entity.Property(e => e.ROI).HasColumnName("roi").HasColumnType("decimal(10,2)");
|
|
||||||
|
|
||||||
entity.HasIndex(e => e.Date).IsUnique().HasDatabaseName("idx_date");
|
|
||||||
});
|
|
||||||
|
|
||||||
// Configurazione StrategicInsight
|
|
||||||
modelBuilder.Entity<StrategicInsight>(entity =>
|
|
||||||
{
|
|
||||||
entity.ToTable("strategic_insights");
|
|
||||||
entity.HasKey(e => e.Id);
|
|
||||||
|
|
||||||
entity.Property(e => e.Id).HasColumnName("id");
|
|
||||||
entity.Property(e => e.InsightType).HasColumnName("insight_type").IsRequired().HasMaxLength(50);
|
|
||||||
entity.Property(e => e.ProductKey).HasColumnName("product_key").HasMaxLength(200);
|
|
||||||
entity.Property(e => e.RecommendedAction).HasColumnName("recommended_action").IsRequired();
|
|
||||||
entity.Property(e => e.ConfidenceLevel).HasColumnName("confidence_level").HasColumnType("decimal(5,2)");
|
|
||||||
entity.Property(e => e.DataPoints).HasColumnName("data_points");
|
|
||||||
entity.Property(e => e.Reasoning).HasColumnName("reasoning");
|
|
||||||
entity.Property(e => e.CreatedAt).HasColumnName("created_at").HasDefaultValueSql("CURRENT_TIMESTAMP");
|
|
||||||
entity.Property(e => e.IsActive).HasColumnName("is_active").HasDefaultValue(true);
|
|
||||||
|
|
||||||
entity.HasIndex(e => e.InsightType).HasDatabaseName("idx_insight_type");
|
|
||||||
entity.HasIndex(e => e.ProductKey).HasDatabaseName("idx_product_key_insight");
|
|
||||||
entity.HasIndex(e => e.CreatedAt).HasDatabaseName("idx_created_at");
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Verifica e crea lo schema del database
|
|
||||||
/// </summary>
|
|
||||||
public async Task<bool> EnsureSchemaAsync()
|
|
||||||
{
|
|
||||||
try
|
|
||||||
{
|
|
||||||
// Verifica connessione
|
|
||||||
if (!await Database.CanConnectAsync())
|
|
||||||
{
|
|
||||||
Console.WriteLine("[PostgreSQL] Cannot connect to database");
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Crea schema se non esistono le tabelle (senza migrations)
|
|
||||||
var created = await Database.EnsureCreatedAsync();
|
|
||||||
|
|
||||||
if (created)
|
|
||||||
{
|
|
||||||
Console.WriteLine("[PostgreSQL] Schema created successfully");
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
Console.WriteLine("[PostgreSQL] Schema already exists");
|
|
||||||
}
|
|
||||||
|
|
||||||
// Verifica che tutte le tabelle esistano
|
|
||||||
var hasCompletedAuctions = await CompletedAuctions.AnyAsync();
|
|
||||||
Console.WriteLine($"[PostgreSQL] Database verified - {(hasCompletedAuctions ? "has data" : "empty")}");
|
|
||||||
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
catch (Exception ex)
|
|
||||||
{
|
|
||||||
Console.WriteLine($"[PostgreSQL ERROR] Schema creation failed: {ex.Message}");
|
|
||||||
Console.WriteLine($"[PostgreSQL ERROR] Stack trace: {ex.StackTrace}");
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Verifica che tutte le tabelle richieste esistano
|
|
||||||
/// </summary>
|
|
||||||
public async Task<bool> ValidateSchemaAsync()
|
|
||||||
{
|
|
||||||
try
|
|
||||||
{
|
|
||||||
// Prova a contare le righe di ogni tabella (forza check esistenza)
|
|
||||||
await CompletedAuctions.CountAsync();
|
|
||||||
await BidderPerformances.CountAsync();
|
|
||||||
await ProductStatistics.CountAsync();
|
|
||||||
await DailyMetrics.CountAsync();
|
|
||||||
await StrategicInsights.CountAsync();
|
|
||||||
|
|
||||||
Console.WriteLine("[PostgreSQL] All tables validated successfully");
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
catch (Exception ex)
|
|
||||||
{
|
|
||||||
Console.WriteLine($"[PostgreSQL ERROR] Schema validation failed: {ex.Message}");
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,23 +0,0 @@
|
|||||||
using Microsoft.EntityFrameworkCore;
|
|
||||||
using AutoBidder.Models;
|
|
||||||
|
|
||||||
namespace AutoBidder.Data
|
|
||||||
{
|
|
||||||
public class StatisticsContext : DbContext
|
|
||||||
{
|
|
||||||
public DbSet<ProductStat> ProductStats { get; set; }
|
|
||||||
|
|
||||||
public StatisticsContext(DbContextOptions<StatisticsContext> options) : base(options)
|
|
||||||
{
|
|
||||||
}
|
|
||||||
|
|
||||||
protected override void OnModelCreating(ModelBuilder modelBuilder)
|
|
||||||
{
|
|
||||||
modelBuilder.Entity<ProductStat>()
|
|
||||||
.HasIndex(p => p.ProductKey)
|
|
||||||
.IsUnique(false);
|
|
||||||
|
|
||||||
base.OnModelCreating(modelBuilder);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -4,11 +4,10 @@
|
|||||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||||
xmlns:controls="clr-namespace:AutoBidder.Controls"
|
xmlns:controls="clr-namespace:AutoBidder.Controls"
|
||||||
Title="Aggiungi Asta" Height="320" Width="700"
|
Title="Aggiungi Asta" Height="320" Width="700"
|
||||||
Background="#0a0a0a" Foreground="#FFFFFF"
|
Background="{DynamicResource Brush.Bg}" Foreground="{DynamicResource Brush.Text}"
|
||||||
WindowStartupLocation="CenterOwner"
|
WindowStartupLocation="CenterOwner"
|
||||||
Icon="pack://application:,,,/Icon/favicon.ico"
|
|
||||||
ResizeMode="NoResize">
|
ResizeMode="NoResize">
|
||||||
<Border Background="#1a1a1a" CornerRadius="8" Padding="16" Margin="8">
|
<Border Background="{DynamicResource Brush.Bg}" CornerRadius="8" Padding="16" Margin="8">
|
||||||
<Grid>
|
<Grid>
|
||||||
<Grid.RowDefinitions>
|
<Grid.RowDefinitions>
|
||||||
<RowDefinition Height="Auto" />
|
<RowDefinition Height="Auto" />
|
||||||
@@ -17,11 +16,11 @@
|
|||||||
<RowDefinition Height="Auto" />
|
<RowDefinition Height="Auto" />
|
||||||
</Grid.RowDefinitions>
|
</Grid.RowDefinitions>
|
||||||
|
|
||||||
<TextBlock Text="Inserire URL dell'asta (uno o più)" Foreground="#CCCCCC" FontSize="14" Margin="0,0,0,6" />
|
<TextBlock Text="Inserire URL dell'asta (uno o più)" Foreground="{DynamicResource Brush.Text}" FontSize="14" Margin="0,0,0,6" />
|
||||||
<TextBlock Grid.Row="1" Text="Puoi aggiungere più link separandoli con 'a capo', 'spazio' o ';'" Foreground="#999999" FontSize="12" Margin="0,0,0,10" />
|
<TextBlock Grid.Row="1" Text="Puoi aggiungere più link separandoli con 'a capo', 'spazio' o ';'" Foreground="{DynamicResource Brush.TextMuted}" FontSize="12" Margin="0,0,0,10" />
|
||||||
|
|
||||||
<TextBox x:Name="AuctionUrlBox" Grid.Row="2" MinWidth="560" Margin="0,0,0,8"
|
<TextBox x:Name="AuctionUrlBox" Grid.Row="2" MinWidth="560" Margin="0,0,0,8"
|
||||||
Background="#181818" Foreground="#00CCFF" BorderBrush="#444" BorderThickness="1"
|
Background="{DynamicResource Brush.Bg}" Foreground="{DynamicResource Brush.Info}" BorderBrush="{DynamicResource Brush.Border}" BorderThickness="1"
|
||||||
Padding="8" FontSize="13" ToolTip="Inserisci uno o più URL/ID dell'asta. Separali con a capo, spazio o ';'"
|
Padding="8" FontSize="13" ToolTip="Inserisci uno o più URL/ID dell'asta. Separali con a capo, spazio o ';'"
|
||||||
AcceptsReturn="True" TextWrapping="Wrap" VerticalScrollBarVisibility="Auto" Height="160" />
|
AcceptsReturn="True" TextWrapping="Wrap" VerticalScrollBarVisibility="Auto" Height="160" />
|
||||||
|
|
||||||
@@ -29,9 +28,9 @@
|
|||||||
<controls:SimpleToolbar.RightContent>
|
<controls:SimpleToolbar.RightContent>
|
||||||
<StackPanel Orientation="Horizontal" HorizontalAlignment="Right">
|
<StackPanel Orientation="Horizontal" HorizontalAlignment="Right">
|
||||||
<Button Content="OK" Width="110" Margin="6" Padding="10,8"
|
<Button Content="OK" Width="110" Margin="6" Padding="10,8"
|
||||||
Style="{StaticResource SmallButtonStyle}" Background="#00CC66" Foreground="White" Click="OkButton_Click" />
|
Style="{StaticResource SmallButtonStyle}" Background="{DynamicResource Brush.Success}" Foreground="{DynamicResource Brush.TextOnAccent}" Click="OkButton_Click" />
|
||||||
<Button Content="Annulla" Width="110" Margin="6" Padding="10,8"
|
<Button Content="Annulla" Width="110" Margin="6" Padding="10,8"
|
||||||
Style="{StaticResource SmallButtonStyle}" Background="#666" Foreground="White" Click="CancelButton_Click" />
|
Style="{StaticResource SmallButtonStyle}" Background="{DynamicResource Brush.SurfaceAlt}" Foreground="{DynamicResource Brush.Text}" Click="CancelButton_Click" />
|
||||||
</StackPanel>
|
</StackPanel>
|
||||||
</controls:SimpleToolbar.RightContent>
|
</controls:SimpleToolbar.RightContent>
|
||||||
</controls:SimpleToolbar>
|
</controls:SimpleToolbar>
|
||||||
|
|||||||
@@ -1,89 +0,0 @@
|
|||||||
<Window x:Class="AutoBidder.Dialogs.ClosedAuctionsWindow"
|
|
||||||
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
|
|
||||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
|
||||||
Title="Aste Chiuse - Estrazione" Height="600" Width="1000"
|
|
||||||
Background="#0a0a0a" Foreground="#FFFFFF" WindowStartupLocation="CenterOwner">
|
|
||||||
<Grid Margin="10">
|
|
||||||
<Grid.RowDefinitions>
|
|
||||||
<RowDefinition Height="Auto" />
|
|
||||||
<RowDefinition Height="8" />
|
|
||||||
<RowDefinition Height="*" />
|
|
||||||
</Grid.RowDefinitions>
|
|
||||||
|
|
||||||
<Border Background="#1a1a1a" Padding="10" CornerRadius="6" Grid.Row="0" BorderBrush="#333333" BorderThickness="1">
|
|
||||||
<DockPanel>
|
|
||||||
<TextBlock Text="Estrazione Aste Chiuse" FontSize="16" FontWeight="Bold" Foreground="#00CC66" VerticalAlignment="Center" DockPanel.Dock="Left" />
|
|
||||||
<StackPanel Orientation="Horizontal" HorizontalAlignment="Right" DockPanel.Dock="Right">
|
|
||||||
<Button x:Name="StartExtractButton" Content="Avvia Estrazione" Click="StartExtractButton_Click" Width="140" Height="36" Margin="8,0,0,0" Background="#00CC66" Style="{StaticResource SmallButtonStyle}"/>
|
|
||||||
<Button x:Name="ExportStatsButton" Content="Esporta Statistiche" Click="ExportStatsButton_Click" Width="160" Height="36" Margin="8,0,0,0" Background="#8B5CF6" Style="{StaticResource SmallButtonStyle}"/>
|
|
||||||
<Button x:Name="CloseButton" Content="Chiudi" Click="CloseButton_Click" Width="80" Height="36" Margin="8,0,0,0" Background="#666" Style="{StaticResource SmallButtonStyle}"/>
|
|
||||||
</StackPanel>
|
|
||||||
</DockPanel>
|
|
||||||
</Border>
|
|
||||||
|
|
||||||
<Border Grid.Row="2" Background="#1a1a1a" Padding="8" CornerRadius="6" BorderBrush="#333333" BorderThickness="1">
|
|
||||||
<Grid>
|
|
||||||
<Grid.ColumnDefinitions>
|
|
||||||
<ColumnDefinition Width="2*" />
|
|
||||||
<ColumnDefinition Width="8" />
|
|
||||||
<ColumnDefinition Width="*" />
|
|
||||||
</Grid.ColumnDefinitions>
|
|
||||||
|
|
||||||
<!-- Products grid: styled like MainWindow -->
|
|
||||||
<DataGrid x:Name="ProductsGrid" AutoGenerateColumns="False" Grid.Column="0" Background="#1a1a1a" Foreground="#FFFFFF" BorderBrush="#333333" BorderThickness="1"
|
|
||||||
RowBackground="#1a1a1a" AlternatingRowBackground="#222222" GridLinesVisibility="Horizontal" HorizontalGridLinesBrush="#333333"
|
|
||||||
IsReadOnly="True" SelectionUnit="CellOrRowHeader" SelectionMode="Extended" ClipboardCopyMode="IncludeHeader" CanUserAddRows="False" CanUserDeleteRows="False">
|
|
||||||
<DataGrid.Resources>
|
|
||||||
<Style TargetType="DataGridColumnHeader">
|
|
||||||
<Setter Property="Background" Value="#2a2a2a" />
|
|
||||||
<Setter Property="Foreground" Value="#FFFFFF" />
|
|
||||||
<Setter Property="FontWeight" Value="Bold" />
|
|
||||||
<Setter Property="Padding" Value="10,8" />
|
|
||||||
<Setter Property="BorderThickness" Value="0,0,0,2" />
|
|
||||||
<Setter Property="BorderBrush" Value="#00CC66" />
|
|
||||||
</Style>
|
|
||||||
<Style TargetType="DataGridRow">
|
|
||||||
<Setter Property="Height" Value="36" />
|
|
||||||
<Style.Triggers>
|
|
||||||
<Trigger Property="IsSelected" Value="True">
|
|
||||||
<Setter Property="Background" Value="#0099FF" />
|
|
||||||
<Setter Property="Foreground" Value="White" />
|
|
||||||
</Trigger>
|
|
||||||
</Style.Triggers>
|
|
||||||
</Style>
|
|
||||||
<Style TargetType="DataGridCell">
|
|
||||||
<Setter Property="BorderThickness" Value="0" />
|
|
||||||
<Setter Property="Padding" Value="10,6" />
|
|
||||||
</Style>
|
|
||||||
</DataGrid.Resources>
|
|
||||||
|
|
||||||
<DataGrid.ContextMenu>
|
|
||||||
<ContextMenu>
|
|
||||||
<MenuItem Command="ApplicationCommands.Copy" Header="Copia" />
|
|
||||||
</ContextMenu>
|
|
||||||
</DataGrid.ContextMenu>
|
|
||||||
|
|
||||||
<DataGrid.Columns>
|
|
||||||
<DataGridTextColumn Header="Asta URL" Binding="{Binding AuctionUrl}" Width="2*" />
|
|
||||||
<DataGridTextColumn Header="Nome" Binding="{Binding ProductName}" Width="3*"/>
|
|
||||||
<DataGridTextColumn Header="Prezzo" Binding="{Binding FinalPrice}" Width="80"/>
|
|
||||||
<DataGridTextColumn Header="Vincitore" Binding="{Binding Winner}" Width="120"/>
|
|
||||||
<DataGridTextColumn Header="Puntate Usate" Binding="{Binding BidsUsed}" Width="100"/>
|
|
||||||
<DataGridTextColumn Header="Scraped At" Binding="{Binding ScrapedAt}" Width="140"/>
|
|
||||||
</DataGrid.Columns>
|
|
||||||
</DataGrid>
|
|
||||||
|
|
||||||
<GridSplitter Grid.Column="1" Width="8" />
|
|
||||||
|
|
||||||
<!-- Log area styled like main window log -->
|
|
||||||
<Border Grid.Column="2" Background="#0f0f0f" Padding="8" CornerRadius="6" BorderBrush="#333333" BorderThickness="1">
|
|
||||||
<DockPanel>
|
|
||||||
<TextBlock Text="Log Operazioni" FontWeight="Bold" Foreground="#00CC66" DockPanel.Dock="Top" Margin="0,0,0,8" />
|
|
||||||
<RichTextBox x:Name="ExtractLogBox" IsReadOnly="True" VerticalScrollBarVisibility="Auto" FontFamily="Consolas" FontSize="11" Background="#0f0f0f" Foreground="#CCC" BorderBrush="#333333" BorderThickness="1" />
|
|
||||||
</DockPanel>
|
|
||||||
</Border>
|
|
||||||
</Grid>
|
|
||||||
</Border>
|
|
||||||
|
|
||||||
</Grid>
|
|
||||||
</Window>
|
|
||||||
@@ -1,140 +0,0 @@
|
|||||||
using System;
|
|
||||||
using System.Collections.ObjectModel;
|
|
||||||
using System.IO;
|
|
||||||
using System.Linq;
|
|
||||||
using System.Threading.Tasks;
|
|
||||||
using System.Windows;
|
|
||||||
using System.Windows.Documents;
|
|
||||||
using Microsoft.Win32;
|
|
||||||
using AutoBidder.Models;
|
|
||||||
using AutoBidder.Services;
|
|
||||||
using AutoBidder.Data;
|
|
||||||
using Microsoft.EntityFrameworkCore;
|
|
||||||
|
|
||||||
namespace AutoBidder.Dialogs
|
|
||||||
{
|
|
||||||
public partial class ClosedAuctionsWindow : Window
|
|
||||||
{
|
|
||||||
private ObservableCollection<ClosedAuctionRecord> _products = new();
|
|
||||||
|
|
||||||
private bool _isRunning = false;
|
|
||||||
|
|
||||||
// StatsService using local DB. Create context with default sqlite file in app folder
|
|
||||||
private readonly StatsService _statsService;
|
|
||||||
|
|
||||||
public ClosedAuctionsWindow()
|
|
||||||
{
|
|
||||||
InitializeComponent();
|
|
||||||
ProductsGrid.ItemsSource = _products;
|
|
||||||
|
|
||||||
var optionsBuilder = new DbContextOptionsBuilder<StatisticsContext>();
|
|
||||||
var dbPath = Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "stats.db");
|
|
||||||
optionsBuilder.UseSqlite($"Data Source={dbPath}");
|
|
||||||
var ctx = new StatisticsContext(optionsBuilder.Options);
|
|
||||||
_statsService = new StatsService(ctx);
|
|
||||||
|
|
||||||
Log("Finestra pronta");
|
|
||||||
}
|
|
||||||
|
|
||||||
private void Log(string message)
|
|
||||||
{
|
|
||||||
try
|
|
||||||
{
|
|
||||||
var para = new Paragraph(new Run($"{DateTime.Now:HH:mm} - {message}"));
|
|
||||||
ExtractLogBox.Document.Blocks.Add(para);
|
|
||||||
// keep size manageable
|
|
||||||
while (ExtractLogBox.Document.Blocks.Count > 500)
|
|
||||||
ExtractLogBox.Document.Blocks.Remove(ExtractLogBox.Document.Blocks.FirstBlock);
|
|
||||||
ExtractLogBox.ScrollToEnd();
|
|
||||||
}
|
|
||||||
catch { }
|
|
||||||
}
|
|
||||||
|
|
||||||
private async void StartExtractButton_Click(object sender, RoutedEventArgs e)
|
|
||||||
{
|
|
||||||
if (_isRunning)
|
|
||||||
{
|
|
||||||
Log("Estrazione già in corso");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
_isRunning = true;
|
|
||||||
StartExtractButton.IsEnabled = false;
|
|
||||||
Log("Avvio procedura di estrazione da closed_auctions.php...");
|
|
||||||
|
|
||||||
try
|
|
||||||
{
|
|
||||||
var scraper = new ClosedAuctionsScraper(null, _statsService, Log);
|
|
||||||
var closedUrl = "https://it.bidoo.com/closed_auctions.php";
|
|
||||||
|
|
||||||
Log($"Scarico: {closedUrl}");
|
|
||||||
|
|
||||||
int count = 0;
|
|
||||||
await foreach (var rec in scraper.ScrapeYieldAsync(closedUrl))
|
|
||||||
{
|
|
||||||
// Filter out records without bids info (user requested)
|
|
||||||
if (!rec.BidsUsed.HasValue)
|
|
||||||
{
|
|
||||||
Log($"Scartata asta (mancano puntate): {rec.AuctionUrl} - '{rec.ProductName ?? "?"}'");
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Add and log incrementally so user sees progress
|
|
||||||
_products.Add(rec);
|
|
||||||
count++;
|
|
||||||
Log($"[{count}] {rec.ProductName} | Prezzo: {(rec.FinalPrice.HasValue?rec.FinalPrice.Value.ToString("F2")+"€":"--")} | Vincitore: {rec.Winner ?? "--"} | Puntate: {rec.BidsUsed.Value} | URL: {rec.AuctionUrl}");
|
|
||||||
}
|
|
||||||
|
|
||||||
Log($"Estrazione completata: {count} record aggiunti.");
|
|
||||||
}
|
|
||||||
catch (Exception ex)
|
|
||||||
{
|
|
||||||
Log($"[ERRORE] Estrattore: {ex.Message}");
|
|
||||||
}
|
|
||||||
finally
|
|
||||||
{
|
|
||||||
_isRunning = false;
|
|
||||||
StartExtractButton.IsEnabled = true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private void CloseButton_Click(object sender, RoutedEventArgs e)
|
|
||||||
{
|
|
||||||
this.Close();
|
|
||||||
}
|
|
||||||
|
|
||||||
private async void ExportStatsButton_Click(object sender, RoutedEventArgs e)
|
|
||||||
{
|
|
||||||
try
|
|
||||||
{
|
|
||||||
Log("Preparazione esportazione statistiche...");
|
|
||||||
var stats = await _statsService.GetAllStatsAsync();
|
|
||||||
if (stats == null || stats.Count == 0)
|
|
||||||
{
|
|
||||||
Log("Nessuna statistica disponibile da esportare.");
|
|
||||||
MessageBox.Show(this, "Nessuna statistica disponibile.", "Esporta Statistiche", MessageBoxButton.OK, MessageBoxImage.Information);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
var dlg = new SaveFileDialog() { Filter = "CSV files|*.csv|All files|*.*", FileName = "auction_stats.csv" };
|
|
||||||
if (dlg.ShowDialog(this) != true) return;
|
|
||||||
|
|
||||||
using var sw = new StreamWriter(dlg.FileName, false, System.Text.Encoding.UTF8);
|
|
||||||
sw.WriteLine("ProductKey,ProductName,TotalAuctions,AverageBidsUsed,AverageFinalPrice,LastSeen");
|
|
||||||
foreach (var s in stats)
|
|
||||||
{
|
|
||||||
var line = $"\"{s.ProductKey}\",\"{s.ProductName}\",{s.TotalAuctions},{s.AverageBidsUsed:F2},{s.AverageFinalPrice:F2},{s.LastSeen:O}";
|
|
||||||
sw.WriteLine(line);
|
|
||||||
}
|
|
||||||
|
|
||||||
Log($"Statistiche esportate su: {dlg.FileName}");
|
|
||||||
MessageBox.Show(this, "Statistiche esportate con successo.", "Esporta Statistiche", MessageBoxButton.OK, MessageBoxImage.Information);
|
|
||||||
}
|
|
||||||
catch (Exception ex)
|
|
||||||
{
|
|
||||||
Log($"[ERRORE] Esporta: {ex.Message}");
|
|
||||||
MessageBox.Show(this, "Errore durante esportazione: " + ex.Message, "Esporta Statistiche", MessageBoxButton.OK, MessageBoxImage.Error);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -2,11 +2,10 @@
|
|||||||
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
|
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
|
||||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||||
Title="Configura Sessione" Height="440" Width="700"
|
Title="Configura Sessione" Height="440" Width="700"
|
||||||
Background="#0a0a0a" Foreground="#FFFFFF"
|
Background="{DynamicResource Brush.Bg}" Foreground="{DynamicResource Brush.Text}"
|
||||||
WindowStartupLocation="CenterOwner"
|
WindowStartupLocation="CenterOwner"
|
||||||
Icon="pack://application:,,,/Icon/favicon.ico"
|
|
||||||
ResizeMode="NoResize">
|
ResizeMode="NoResize">
|
||||||
<Border Background="#1a1a1a" CornerRadius="8" Padding="16" Margin="8">
|
<Border Background="{DynamicResource Brush.Bg}" CornerRadius="8" Padding="16" Margin="8">
|
||||||
<Grid>
|
<Grid>
|
||||||
<Grid.RowDefinitions>
|
<Grid.RowDefinitions>
|
||||||
<RowDefinition Height="Auto" />
|
<RowDefinition Height="Auto" />
|
||||||
@@ -15,9 +14,9 @@
|
|||||||
<RowDefinition Height="*" />
|
<RowDefinition Height="*" />
|
||||||
<RowDefinition Height="Auto" />
|
<RowDefinition Height="Auto" />
|
||||||
</Grid.RowDefinitions>
|
</Grid.RowDefinitions>
|
||||||
<TextBlock Text="Inserisci Cookie di sessione" FontSize="16" FontWeight="SemiBold" Foreground="#00CC66" Margin="0,0,0,12" />
|
<TextBlock Text="Inserisci Cookie di sessione" FontSize="16" FontWeight="SemiBold" Foreground="{DynamicResource Brush.Success}" Margin="0,0,0,12" />
|
||||||
<TextBlock Grid.Row="1" Text="Cookie di sessione (copia dal menu delle Impostazioni/Applicazioni di Chrome)" Foreground="#CCCCCC" FontSize="13" />
|
<TextBlock Grid.Row="1" Text="Cookie di sessione (copia dal menu delle Impostazioni/Applicazioni di Chrome)" Foreground="{DynamicResource Brush.Text}" FontSize="13" />
|
||||||
<TextBlock Grid.Row="2" Foreground="#888" FontSize="12" TextWrapping="Wrap" Margin="0,4,0,10">
|
<TextBlock Grid.Row="2" Foreground="{DynamicResource Brush.TextMuted}" FontSize="12" TextWrapping="Wrap" Margin="0,4,0,10">
|
||||||
<Run Text="Come trovare i cookie di sessione in Chrome:" />
|
<Run Text="Come trovare i cookie di sessione in Chrome:" />
|
||||||
<LineBreak/>
|
<LineBreak/>
|
||||||
<Run Text="1. Apri Chrome e premi F12 (Windows) o Cmd+Option+I (Mac) per aprire gli Strumenti per sviluppatori." />
|
<Run Text="1. Apri Chrome e premi F12 (Windows) o Cmd+Option+I (Mac) per aprire gli Strumenti per sviluppatori." />
|
||||||
@@ -29,12 +28,12 @@
|
|||||||
<Run Text="4. Scegli il sito desiderato e copia il valore del cookie di sessione." />
|
<Run Text="4. Scegli il sito desiderato e copia il valore del cookie di sessione." />
|
||||||
</TextBlock>
|
</TextBlock>
|
||||||
<TextBox x:Name="CookieBox" Grid.Row="3" MinWidth="320" MinHeight="120" MaxHeight="220" VerticalScrollBarVisibility="Auto"
|
<TextBox x:Name="CookieBox" Grid.Row="3" MinWidth="320" MinHeight="120" MaxHeight="220" VerticalScrollBarVisibility="Auto"
|
||||||
Background="#181818" Foreground="#00CCFF" BorderBrush="#444" BorderThickness="1" Padding="8" FontSize="14" AcceptsReturn="True" TextWrapping="Wrap" />
|
Background="{DynamicResource Brush.Bg}" Foreground="{DynamicResource Brush.Info}" BorderBrush="{DynamicResource Brush.Border}" BorderThickness="1" Padding="8" FontSize="14" AcceptsReturn="True" TextWrapping="Wrap" />
|
||||||
<StackPanel Grid.Row="4" Orientation="Horizontal" HorizontalAlignment="Right" Margin="0,12,0,0">
|
<StackPanel Grid.Row="4" Orientation="Horizontal" HorizontalAlignment="Right" Margin="0,12,0,0">
|
||||||
<Button x:Name="OkButton" Content="OK" Width="110" Margin="6" Padding="10,8"
|
<Button x:Name="OkButton" Content="OK" Width="110" Margin="6" Padding="10,8"
|
||||||
Style="{StaticResource SmallButtonStyle}" Background="#00CC66" Foreground="White" Click="OkButton_Click" />
|
Style="{StaticResource SmallButtonStyle}" Background="{DynamicResource Brush.Success}" Foreground="{DynamicResource Brush.TextOnAccent}" Click="OkButton_Click" />
|
||||||
<Button x:Name="CancelButton" Content="Annulla" Width="110" Margin="6" Padding="10,8"
|
<Button x:Name="CancelButton" Content="Annulla" Width="110" Margin="6" Padding="10,8"
|
||||||
Style="{StaticResource SmallButtonStyle}" Background="#666" Foreground="White" Click="CancelButton_Click" />
|
Style="{StaticResource SmallButtonStyle}" Background="{DynamicResource Brush.SurfaceAlt}" Foreground="{DynamicResource Brush.Text}" Click="CancelButton_Click" />
|
||||||
</StackPanel>
|
</StackPanel>
|
||||||
</Grid>
|
</Grid>
|
||||||
</Border>
|
</Border>
|
||||||
|
|||||||
@@ -0,0 +1,95 @@
|
|||||||
|
<Window x:Class="AutoBidder.Dialogs.StatsCleanupDialog"
|
||||||
|
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
|
||||||
|
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||||
|
Title="Pulizia dello storico"
|
||||||
|
Width="620" SizeToContent="Height"
|
||||||
|
WindowStartupLocation="CenterOwner"
|
||||||
|
ShowInTaskbar="False"
|
||||||
|
Background="{DynamicResource Brush.Bg}">
|
||||||
|
|
||||||
|
<StackPanel Margin="22,18">
|
||||||
|
|
||||||
|
<TextBlock Text="Cosa considerare incompleto"
|
||||||
|
FontSize="{StaticResource Font.Size.Lg}" FontWeight="SemiBold"
|
||||||
|
Foreground="{DynamicResource Brush.Text}"/>
|
||||||
|
|
||||||
|
<TextBlock Style="{StaticResource Hint}" Margin="0,6,0,14"
|
||||||
|
Text="Il conto si aggiorna mentre scegli. Niente viene toccato finché non premi Pulisci, e viene salvata una copia dello storico prima di procedere."/>
|
||||||
|
|
||||||
|
<CheckBox x:Name="ChkNoPrice" Margin="0,4" IsChecked="True"
|
||||||
|
Content="Aste senza prezzo finale"
|
||||||
|
ToolTip="Non dicono nulla e sporcano ogni media: il prezzo è il dato su cui si regge tutto il resto."
|
||||||
|
Checked="Option_Changed" Unchecked="Option_Changed"/>
|
||||||
|
|
||||||
|
<CheckBox x:Name="ChkBadBids" Margin="0,4" IsChecked="True"
|
||||||
|
Content="Aste con puntate del vincitore incoerenti col prezzo"
|
||||||
|
ToolTip="Ogni puntata alza il prezzo di un centesimo, quindi il prezzo in centesimi è il totale delle puntate dell'asta. Un vincitore che ne dichiara di più — o zero — porta un dato che non può essere vero."
|
||||||
|
Checked="Option_Changed" Unchecked="Option_Changed"/>
|
||||||
|
|
||||||
|
<CheckBox x:Name="ChkDuplicates" Margin="0,4" IsChecked="True"
|
||||||
|
Content="Doppioni con lo stesso identificativo (tiene il più recente)"
|
||||||
|
ToolTip="La stessa asta può essere registrata due volte: quando il motore ne vede la fine e quando viene tolta dal monitor. Si tiene la versione più recente."
|
||||||
|
Checked="Option_Changed" Unchecked="Option_Changed"/>
|
||||||
|
|
||||||
|
<CheckBox x:Name="ChkNoWinnerBids" Margin="0,4"
|
||||||
|
Content="Aste di cui non si conoscono le puntate del vincitore"
|
||||||
|
ToolTip="Severo: toglie tutte le aste su cui non si può calcolare il costo reale. Prima di usarlo, prova «Recupera puntate dei vincitori» nella scheda Storico — il dato si può quasi sempre riprendere dal server."
|
||||||
|
Checked="Option_Changed" Unchecked="Option_Changed"/>
|
||||||
|
|
||||||
|
<CheckBox x:Name="ChkNoWinner" Margin="0,4"
|
||||||
|
Content="Aste senza vincitore"
|
||||||
|
ToolTip="Aste scadute senza offerte, oppure registrate a metà."
|
||||||
|
Checked="Option_Changed" Unchecked="Option_Changed"/>
|
||||||
|
|
||||||
|
<CheckBox x:Name="ChkNoValue" Margin="0,4"
|
||||||
|
Content="Aste senza valore del prodotto"
|
||||||
|
ToolTip="Senza il «Compra Subito» non si può calcolare il risparmio, ma il prezzo e le puntate restano validi."
|
||||||
|
Checked="Option_Changed" Unchecked="Option_Changed"/>
|
||||||
|
|
||||||
|
<StackPanel Orientation="Horizontal" Margin="0,10,0,0">
|
||||||
|
<CheckBox x:Name="ChkCoverage" VerticalAlignment="Center"
|
||||||
|
Content="Aste seguite per meno del"
|
||||||
|
ToolTip="Toglie le aste agganciate a metà. Sono il caso peggiore: i numeri sono veri ma parziali, quindi sembrano buone e abbassano le medie senza dare segnali. Quanta asta si è vista si ricava dal prezzo finale, senza bisogno di alcun dato in più: ogni puntata vale un centesimo, quindi il prezzo in centesimi è il totale delle puntate dell'asta, e i reset osservati dicono quanti se ne sono visti. Sul tuo storico, su 5876 aste: al 75% ne resterebbero circa 78 su 100, al 90% circa 64, al 95% circa 54, al 99% circa 34."
|
||||||
|
Checked="Option_Changed" Unchecked="Option_Changed"/>
|
||||||
|
<TextBox x:Name="CoverageBox" Text="90" Width="50" Margin="8,0"
|
||||||
|
VerticalAlignment="Center" TextChanged="Option_Changed"/>
|
||||||
|
<TextBlock Text="% della loro durata" VerticalAlignment="Center"
|
||||||
|
Foreground="{DynamicResource Brush.Text}"/>
|
||||||
|
</StackPanel>
|
||||||
|
|
||||||
|
<StackPanel Orientation="Horizontal" Margin="0,10,0,0">
|
||||||
|
<CheckBox x:Name="ChkOlderThan" VerticalAlignment="Center"
|
||||||
|
Content="Aste chiuse da più di"
|
||||||
|
ToolTip="Le aste vecchie descrivono un mercato che può essere cambiato."
|
||||||
|
Checked="Option_Changed" Unchecked="Option_Changed"/>
|
||||||
|
<TextBox x:Name="DaysBox" Text="180" Width="60" Margin="8,0"
|
||||||
|
VerticalAlignment="Center" TextChanged="Option_Changed"/>
|
||||||
|
<TextBlock Text="giorni" VerticalAlignment="Center"
|
||||||
|
Foreground="{DynamicResource Brush.Text}"/>
|
||||||
|
</StackPanel>
|
||||||
|
|
||||||
|
<Border Style="{StaticResource InfoBox}" Margin="0,16,0,0">
|
||||||
|
<StackPanel>
|
||||||
|
<TextBlock x:Name="SummaryText" TextWrapping="Wrap"
|
||||||
|
Foreground="{DynamicResource Brush.Text}"
|
||||||
|
FontSize="{StaticResource Font.Size.Sm}" LineHeight="18"
|
||||||
|
Text="Calcolo…"/>
|
||||||
|
<TextBlock x:Name="DetailText" TextWrapping="Wrap" Margin="0,8,0,0"
|
||||||
|
Foreground="{DynamicResource Brush.TextMuted}"
|
||||||
|
FontSize="{StaticResource Font.Size.Sm}" LineHeight="17"/>
|
||||||
|
</StackPanel>
|
||||||
|
</Border>
|
||||||
|
|
||||||
|
<StackPanel Orientation="Horizontal" HorizontalAlignment="Right" Margin="0,18,0,0">
|
||||||
|
<Button x:Name="CleanButton" Content="Pulisci" Padding="26,7" Margin="0,0,10,0"
|
||||||
|
Style="{StaticResource ModernButton}"
|
||||||
|
Background="{DynamicResource Brush.Danger}"
|
||||||
|
Foreground="{DynamicResource Brush.TextOnAccent}"
|
||||||
|
Click="Clean_Click"/>
|
||||||
|
<Button Content="Annulla" Padding="26,7"
|
||||||
|
Style="{StaticResource ModernButton}"
|
||||||
|
Background="{DynamicResource Brush.SurfaceAlt}"
|
||||||
|
IsCancel="True"/>
|
||||||
|
</StackPanel>
|
||||||
|
</StackPanel>
|
||||||
|
</Window>
|
||||||
@@ -0,0 +1,125 @@
|
|||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Windows;
|
||||||
|
using AutoBidder.Models;
|
||||||
|
using AutoBidder.Utilities;
|
||||||
|
|
||||||
|
namespace AutoBidder.Dialogs
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Sceglie cosa considerare incompleto nello storico e lo toglie.
|
||||||
|
///
|
||||||
|
/// <para>Il conto si aggiorna a ogni spunta, <b>prima</b> di toccare qualunque cosa: una
|
||||||
|
/// pulizia è irreversibile, e l'unico modo di renderla una decisione informata è mostrare
|
||||||
|
/// quante righe cadrebbero e per quale motivo mentre si sceglie. La copia di sicurezza
|
||||||
|
/// prima di scrivere è la seconda rete: le statistiche sono mesi di raccolta che nessun
|
||||||
|
/// pulsante ha il diritto di far sparire per un clic distratto.</para>
|
||||||
|
/// </summary>
|
||||||
|
public partial class StatsCleanupDialog : Window
|
||||||
|
{
|
||||||
|
private readonly List<CompletedAuctionRecord> _records;
|
||||||
|
|
||||||
|
public StatsCleanupDialog(List<CompletedAuctionRecord> records)
|
||||||
|
{
|
||||||
|
InitializeComponent();
|
||||||
|
_records = records ?? new List<CompletedAuctionRecord>();
|
||||||
|
Loaded += (_, _) => Refresh();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>Righe rimosse, dopo la conferma. 0 se l'utente ha annullato.</summary>
|
||||||
|
public int RemovedCount { get; private set; }
|
||||||
|
|
||||||
|
/// <summary>Dove è finita la copia di sicurezza.</summary>
|
||||||
|
public string? BackupPath { get; private set; }
|
||||||
|
|
||||||
|
private StatsMaintenance.CleanupOptions ReadOptions()
|
||||||
|
{
|
||||||
|
var options = new StatsMaintenance.CleanupOptions
|
||||||
|
{
|
||||||
|
RemoveWithoutPrice = ChkNoPrice.IsChecked == true,
|
||||||
|
RemoveInconsistentBids = ChkBadBids.IsChecked == true,
|
||||||
|
RemoveDuplicates = ChkDuplicates.IsChecked == true,
|
||||||
|
RemoveWithoutWinnerBids = ChkNoWinnerBids.IsChecked == true,
|
||||||
|
RemoveWithoutWinner = ChkNoWinner.IsChecked == true,
|
||||||
|
RemoveWithoutValue = ChkNoValue.IsChecked == true
|
||||||
|
};
|
||||||
|
|
||||||
|
// Una soglia illeggibile o fuori scala non deve diventare "togli tutto":
|
||||||
|
// il filtro resta spento finché il numero non ha senso.
|
||||||
|
if (ChkCoverage.IsChecked == true &&
|
||||||
|
double.TryParse(CoverageBox.Text.Trim().Replace(',', '.'),
|
||||||
|
System.Globalization.NumberStyles.Float,
|
||||||
|
System.Globalization.CultureInfo.InvariantCulture, out var percento) &&
|
||||||
|
percento is > 0 and <= 100)
|
||||||
|
{
|
||||||
|
options.MinObservedCoverage = percento / 100.0;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (ChkOlderThan.IsChecked == true &&
|
||||||
|
int.TryParse(DaysBox.Text.Trim(), out var days) && days > 0)
|
||||||
|
{
|
||||||
|
options.OlderThan = DateTime.Now.AddDays(-days);
|
||||||
|
}
|
||||||
|
|
||||||
|
return options;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void Option_Changed(object sender, RoutedEventArgs e) => Refresh();
|
||||||
|
|
||||||
|
private void Refresh()
|
||||||
|
{
|
||||||
|
if (SummaryText == null) return;
|
||||||
|
|
||||||
|
var report = StatsMaintenance.Preview(_records, ReadOptions());
|
||||||
|
|
||||||
|
SummaryText.Text = report.Removed == 0
|
||||||
|
? $"Nessuna delle {report.Total} aste verrebbe tolta."
|
||||||
|
: $"Verrebbero tolte {report.Removed} aste su {report.Total}. " +
|
||||||
|
$"Ne resterebbero {report.Kept}.";
|
||||||
|
|
||||||
|
DetailText.Text = report.ByReason.Count == 0
|
||||||
|
? ""
|
||||||
|
: string.Join("\n", report.ByReason
|
||||||
|
.OrderByDescending(kv => kv.Value)
|
||||||
|
.Select(kv => $"• {kv.Value} {kv.Key}"));
|
||||||
|
|
||||||
|
CleanButton.IsEnabled = report.Removed > 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void Clean_Click(object sender, RoutedEventArgs e)
|
||||||
|
{
|
||||||
|
var options = ReadOptions();
|
||||||
|
var report = StatsMaintenance.Preview(_records, options);
|
||||||
|
|
||||||
|
if (report.Removed == 0) return;
|
||||||
|
|
||||||
|
var answer = MessageBox.Show(this,
|
||||||
|
$"Tolgo {report.Removed} aste su {report.Total}.\n\n" +
|
||||||
|
"L'operazione non è reversibile, ma prima viene salvata una copia " +
|
||||||
|
"dello storico nella cartella dei backup.\n\nProcedo?",
|
||||||
|
"Pulizia dello storico", MessageBoxButton.YesNo, MessageBoxImage.Warning);
|
||||||
|
|
||||||
|
if (answer != MessageBoxResult.Yes) return;
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
BackupPath = CompletedAuctionsStore.BackupNow();
|
||||||
|
|
||||||
|
// La stessa risposta usata per il conto: contare con una regola e
|
||||||
|
// cancellare con un'altra è il modo classico di far sparire dati che
|
||||||
|
// nessuno voleva perdere.
|
||||||
|
var kept = StatsMaintenance.Apply(_records, options);
|
||||||
|
CompletedAuctionsStore.ReplaceAll(kept);
|
||||||
|
|
||||||
|
RemovedCount = report.Removed;
|
||||||
|
DialogResult = true;
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
MessageBox.Show(this, $"Pulizia non riuscita: {ex.Message}",
|
||||||
|
"Pulizia dello storico", MessageBoxButton.OK, MessageBoxImage.Error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,81 +0,0 @@
|
|||||||
# ============================================
|
|
||||||
# STAGE 1: Build
|
|
||||||
# ============================================
|
|
||||||
FROM mcr.microsoft.com/dotnet/sdk:10.0 AS build
|
|
||||||
ARG BUILD_CONFIGURATION=Release
|
|
||||||
WORKDIR /src
|
|
||||||
|
|
||||||
# Copy csproj and restore dependencies (cache layer)
|
|
||||||
COPY ["AutoBidder.csproj", "."]
|
|
||||||
RUN dotnet restore "./AutoBidder.csproj"
|
|
||||||
|
|
||||||
# Copy all source files
|
|
||||||
COPY . .
|
|
||||||
|
|
||||||
# Build application
|
|
||||||
WORKDIR "/src/."
|
|
||||||
RUN dotnet build "./AutoBidder.csproj" -c $BUILD_CONFIGURATION -o /app/build --no-restore
|
|
||||||
|
|
||||||
# ============================================
|
|
||||||
# STAGE 2: Publish
|
|
||||||
# ============================================
|
|
||||||
FROM build AS publish
|
|
||||||
ARG BUILD_CONFIGURATION=Release
|
|
||||||
# RIMOSSO --no-build per evitare errore path
|
|
||||||
RUN dotnet publish "./AutoBidder.csproj" \
|
|
||||||
-c $BUILD_CONFIGURATION \
|
|
||||||
-o /app/publish \
|
|
||||||
/p:UseAppHost=false
|
|
||||||
|
|
||||||
# ============================================
|
|
||||||
# STAGE 3: Final Runtime
|
|
||||||
# ============================================
|
|
||||||
FROM mcr.microsoft.com/dotnet/aspnet:10.0 AS final
|
|
||||||
WORKDIR /app
|
|
||||||
|
|
||||||
# Install curl for healthcheck and sqlite3
|
|
||||||
RUN apt-get update && \
|
|
||||||
apt-get install -y --no-install-recommends \
|
|
||||||
curl \
|
|
||||||
ca-certificates \
|
|
||||||
sqlite3 && \
|
|
||||||
rm -rf /var/lib/apt/lists/*
|
|
||||||
|
|
||||||
# Create data directories for persistence
|
|
||||||
RUN mkdir -p /app/Data /app/Data/backups /app/logs && \
|
|
||||||
chmod 777 /app/Data /app/logs
|
|
||||||
|
|
||||||
# Copy published application
|
|
||||||
COPY --from=publish /app/publish .
|
|
||||||
|
|
||||||
# Expose port (single HTTP for simplicity)
|
|
||||||
EXPOSE 8080
|
|
||||||
|
|
||||||
# Environment variables (overridable via docker-compose/unraid)
|
|
||||||
ENV ASPNETCORE_URLS=http://+:8080
|
|
||||||
ENV ASPNETCORE_ENVIRONMENT=Production
|
|
||||||
ENV Kestrel__EnableHttps=false
|
|
||||||
|
|
||||||
# Database path - tutti i database SQLite e dati persistenti
|
|
||||||
# Può essere sovrascritto nel docker-compose per mappare un volume persistente
|
|
||||||
ENV DATA_PATH=/app/Data
|
|
||||||
|
|
||||||
# Autenticazione applicazione (OBBLIGATORIO)
|
|
||||||
ENV ADMIN_USERNAME=admin
|
|
||||||
ENV ADMIN_PASSWORD=
|
|
||||||
|
|
||||||
# Health check
|
|
||||||
# Aumentato timeout e start-period per Blazor Server
|
|
||||||
HEALTHCHECK --interval=30s --timeout=30s --start-period=90s --retries=5 \
|
|
||||||
CMD curl -f http://localhost:8080/ || exit 1
|
|
||||||
|
|
||||||
# Labels for metadata
|
|
||||||
LABEL org.opencontainers.image.title="AutoBidder" \
|
|
||||||
org.opencontainers.image.description="Sistema automatizzato gestione aste Bidoo - Blazor .NET 10" \
|
|
||||||
org.opencontainers.image.version="1.2.0" \
|
|
||||||
org.opencontainers.image.vendor="Alby96" \
|
|
||||||
org.opencontainers.image.source="https://gitea.encke-hake.ts.net/Alby96/Mimante"
|
|
||||||
|
|
||||||
# Entry point
|
|
||||||
ENTRYPOINT ["dotnet", "AutoBidder.dll"]
|
|
||||||
|
|
||||||
@@ -1,26 +0,0 @@
|
|||||||
______________________________________________________________________________________________________________
|
|
||||||
FUNZIONALITA
|
|
||||||
|
|
||||||
Cambiare la pagina delle statistiche in modo da aggiungere una sezione in più, oltre alle statistiche memorizzate in un automatico, in cui posso associare un range di prezzo e di puntate per ogni articolo, identificato tramite il suo nome
|
|
||||||
|
|
||||||
Aggiungere una scansione periodica e automatica delle aste terminate in modo da aggiornare automaticamente il mio elenco degli articoli delle aste terminate per aggiornare prezzo e numero di puntate usate in automatico. Molto importante: salvare anche l'ora di chiusura dell'asta
|
|
||||||
|
|
||||||
Aggiungere una funzionalità di aggiunta automatica delle aste al monitor appena compaiono nell'elenco delle aste disponibile cercando tramite sezione e nome articolo
|
|
||||||
|
|
||||||
Aggiungi una indicazione visiva nella colonna dello stato che indica quando un'asta pur essendo nello stato attiva il bot non punta perché fuori range oppure per altri motivi
|
|
||||||
|
|
||||||
Fare una tasto nelle statistiche che applichi massivamente i limiti a tutti gli articoli attualmente monitorati che hanno delle informazioni salvate nel database delle aste terminate
|
|
||||||
|
|
||||||
_______________________________________________________________________________________________________________
|
|
||||||
REWORK
|
|
||||||
|
|
||||||
Esegui un rework generico del sistema di log della singola asta e del log globale. Ci sono troppe righe inutili come tante righe simili duplicate nel log della singola asta e informazioni inutili nel log globale come per esempio l'indicazione del focus che si sposta su una certa riga. Valuta i cambiamenti e le ottimizzazioni da fare e applica le modifiche.
|
|
||||||
|
|
||||||
Esegui un rework della grafica in modo da eliminare le animazioni popup che danno fastidio all'usabilità del programma. In particolare intendo che quando il mouse passa su un pulsante o una griglia questa aumenta leggermente di dimensione per evidenziarsi ma questo non mi piace. Elimina questa cosa e sostituiscila piuttosto con una illuminazione o colorazione più chiara o scura per evidenziare il fatto che sto per selezionare quel particolare pulsante
|
|
||||||
|
|
||||||
_______________________________________________________________________________________________________________
|
|
||||||
CORREZIONI
|
|
||||||
|
|
||||||
Aggiungi più stati per indicare la strategia o il fatto che non sta puntando e per quale motivo.
|
|
||||||
In particolare oltre agli stati già presenti indicare anche il motivo per cui non sta puntando come per esempio "fuori range di prezzo", "fuori range di puntate", "asta terminata", "strategia non permette puntata", ecc
|
|
||||||
|
|
||||||
@@ -0,0 +1,328 @@
|
|||||||
|
using System;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Threading;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
using AutoBidder.Models;
|
||||||
|
using AutoBidder.Utilities;
|
||||||
|
|
||||||
|
namespace AutoBidder.Engine
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Servizi che il runner si aspetta dal monitor. Esiste per non legare il motore
|
||||||
|
/// all'implementazione del monitor: qui dentro c'è solo il timing.
|
||||||
|
/// </summary>
|
||||||
|
public interface IAuctionRunnerHost
|
||||||
|
{
|
||||||
|
/// <summary>Impostazioni correnti (già memorizzate in cache dal chiamante).</summary>
|
||||||
|
AppSettings Settings { get; }
|
||||||
|
|
||||||
|
/// <summary>Una singola interrogazione a data.php. null = nessuna risposta utilizzabile.</summary>
|
||||||
|
Task<AuctionState?> PollAsync(AuctionInfo auction, CancellationToken ct);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Incorpora la risposta: aggiorna orologio server, scadenza, storico e interfaccia.
|
||||||
|
/// </summary>
|
||||||
|
void ApplySnapshot(AuctionInfo auction, AuctionState snapshot);
|
||||||
|
|
||||||
|
/// <summary>True quando l'asta è conclusa e il runner può spegnersi.</summary>
|
||||||
|
bool IsFinished(AuctionInfo auction);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Verdetto delle strategie sulla puntata. Il runner decide <i>quando</i>,
|
||||||
|
/// questo metodo decide <i>se</i>.
|
||||||
|
/// </summary>
|
||||||
|
bool EvaluateBid(AuctionInfo auction, AuctionState state);
|
||||||
|
|
||||||
|
/// <summary>Invia la puntata e registra l'esito.</summary>
|
||||||
|
Task FireBidAsync(AuctionInfo auction, AuctionState state, int leadMs, double actualLeadMs, CancellationToken ct);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Motore di una singola asta. Due anelli indipendenti che girano in parallelo:
|
||||||
|
///
|
||||||
|
/// - <b>polling</b>: interroga data.php con cadenza che si stringe man mano che la
|
||||||
|
/// scadenza si avvicina, e aggiorna prezzo, ultimo puntatore e scadenza assoluta;
|
||||||
|
/// - <b>cecchino</b>: dorme fino a "scadenza meno anticipo" e spara la puntata lì.
|
||||||
|
///
|
||||||
|
/// Tenerli separati è la differenza sostanziale rispetto a un ticker unico condiviso:
|
||||||
|
/// una risposta lenta del server su un'asta non sposta di un millisecondo il momento
|
||||||
|
/// della puntata sulle altre, e il numero di aste seguite non degrada la precisione.
|
||||||
|
/// </summary>
|
||||||
|
public sealed class AuctionRunner
|
||||||
|
{
|
||||||
|
/// <summary>Quanto prima dell'istante di fuoco si eseguono i controlli di strategia.</summary>
|
||||||
|
private const double PreCheckMs = 60;
|
||||||
|
|
||||||
|
private readonly AuctionInfo _auction;
|
||||||
|
private readonly IAuctionRunnerHost _host;
|
||||||
|
|
||||||
|
private CancellationTokenSource? _cts;
|
||||||
|
private Task? _pollTask;
|
||||||
|
private Task? _sniperTask;
|
||||||
|
|
||||||
|
/// <summary>Scadenza (in secondi unix) del ciclo già gestito: garantisce una sola puntata per ciclo.</summary>
|
||||||
|
private long _handledCycle = -1;
|
||||||
|
|
||||||
|
public AuctionRunner(AuctionInfo auction, IAuctionRunnerHost host)
|
||||||
|
{
|
||||||
|
_auction = auction;
|
||||||
|
_host = host;
|
||||||
|
}
|
||||||
|
|
||||||
|
public AuctionInfo Auction => _auction;
|
||||||
|
|
||||||
|
public bool IsRunning => _pollTask is { IsCompleted: false };
|
||||||
|
|
||||||
|
public void Start()
|
||||||
|
{
|
||||||
|
if (IsRunning) return;
|
||||||
|
|
||||||
|
_cts = new CancellationTokenSource();
|
||||||
|
var token = _cts.Token;
|
||||||
|
|
||||||
|
_pollTask = Task.Run(() => PollLoopAsync(token), token);
|
||||||
|
_sniperTask = Task.Run(() => SniperLoopAsync(token), token);
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task StopAsync()
|
||||||
|
{
|
||||||
|
var cts = _cts;
|
||||||
|
if (cts is null) return;
|
||||||
|
|
||||||
|
_cts = null;
|
||||||
|
try { await cts.CancelAsync().ConfigureAwait(false); } catch { /* già annullato */ }
|
||||||
|
|
||||||
|
var tasks = new[] { _pollTask, _sniperTask }.Where(t => t is not null).Cast<Task>().ToArray();
|
||||||
|
if (tasks.Length > 0)
|
||||||
|
{
|
||||||
|
try { await Task.WhenAll(tasks).WaitAsync(TimeSpan.FromSeconds(3)).ConfigureAwait(false); }
|
||||||
|
catch { /* uscita forzata */ }
|
||||||
|
}
|
||||||
|
|
||||||
|
_pollTask = null;
|
||||||
|
_sniperTask = null;
|
||||||
|
cts.Dispose();
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Anello di polling ────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
private async Task PollLoopAsync(CancellationToken ct)
|
||||||
|
{
|
||||||
|
var consecutiveErrors = 0;
|
||||||
|
|
||||||
|
while (!ct.IsCancellationRequested)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var settings = _host.Settings;
|
||||||
|
|
||||||
|
var snapshot = await _host.PollAsync(_auction, ct).ConfigureAwait(false);
|
||||||
|
|
||||||
|
if (snapshot is null)
|
||||||
|
{
|
||||||
|
consecutiveErrors++;
|
||||||
|
_auction.PollErrors++;
|
||||||
|
|
||||||
|
if (consecutiveErrors == 3)
|
||||||
|
{
|
||||||
|
_auction.AddLog(
|
||||||
|
"Nessuna risposta da data.php: verifica la connessione o il cookie",
|
||||||
|
AuctionLogLevel.Warning, AuctionLogCategory.Polling);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Backoff progressivo, ma senza superare i 5 s: l'asta potrebbe ripartire.
|
||||||
|
var backoff = Math.Min(300 * consecutiveErrors, 5000);
|
||||||
|
if (!await Wait.DelayAsync(backoff, ct).ConfigureAwait(false)) return;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
consecutiveErrors = 0;
|
||||||
|
_auction.PollCount++;
|
||||||
|
_host.ApplySnapshot(_auction, snapshot);
|
||||||
|
|
||||||
|
if (_host.IsFinished(_auction)) return;
|
||||||
|
|
||||||
|
if (!await Wait.DelayAsync(NextPollDelay(settings), ct).ConfigureAwait(false)) return;
|
||||||
|
}
|
||||||
|
catch (OperationCanceledException)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
_auction.AddLog($"Errore nel polling: {ex.Message}",
|
||||||
|
AuctionLogLevel.Error, AuctionLogCategory.Polling);
|
||||||
|
if (!await Wait.DelayAsync(1000, ct).ConfigureAwait(false)) return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Cadenza del polling: fitta solo dove serve. Un'asta a otto minuti dalla scadenza
|
||||||
|
/// non ha bisogno di quattro chiamate al secondo, una a tre secondi sì.
|
||||||
|
/// </summary>
|
||||||
|
private int NextPollDelay(AppSettings settings)
|
||||||
|
{
|
||||||
|
// Un'asta non ancora cominciata ha una cadenza tutta sua: vedi NotStartedDelay.
|
||||||
|
// È l'unico caso rimasto in cui si aspetta, e non è una scelta di ritmo: fino
|
||||||
|
// all'apertura il server non ha niente di diverso da dire, e interrogarlo
|
||||||
|
// seicento volte al minuto per tre ore non anticipa l'inizio di un istante.
|
||||||
|
var notStarted = NotStartedDelay(settings);
|
||||||
|
if (notStarted > 0) return notStarted;
|
||||||
|
|
||||||
|
// ── Nessuna attesa: la cadenza la detta la rete ──────────────────────
|
||||||
|
//
|
||||||
|
// Prima qui c'erano quattro soglie configurabili (lontano / medio / vicino /
|
||||||
|
// critico). Sui registri di tre giorni non hanno mai funzionato come scritto:
|
||||||
|
// con 40 richieste al secondo di tetto globale spartite fra tutte le aste
|
||||||
|
// seguite, i 220 ms della finestra critica diventavano 596 ms misurati, e con
|
||||||
|
// il timer a un secondo restava un solo sguardo prima della scadenza.
|
||||||
|
//
|
||||||
|
// Il ritmo adesso è quello del giro di andata e ritorno: si riparte appena la
|
||||||
|
// risposta è arrivata. Con ping ~57 ms misurato sono circa diciassette
|
||||||
|
// interrogazioni al secondo per asta.
|
||||||
|
//
|
||||||
|
// Il freno non è più un numero scelto a tavolino ma la risposta del server:
|
||||||
|
// vedi la reazione a 429 e 503 in PollLoopAsync. Se Bidoo dice di rallentare,
|
||||||
|
// si rallenta; finché non lo dice, si va al massimo che regge la rete.
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Cadenza per un'asta che deve ancora cominciare, oppure 0 se non è questo il caso.
|
||||||
|
///
|
||||||
|
/// <para>Bidoo programma le aste con ore di anticipo, e fino all'apertura non
|
||||||
|
/// succede assolutamente nulla: interrogarla ogni due secondi per tre ore sono
|
||||||
|
/// migliaia di richieste per non vedere mai un cambiamento. La cadenza si allarga
|
||||||
|
/// quindi in proporzione all'attesa.</para>
|
||||||
|
///
|
||||||
|
/// <para>Il punto delicato è il risveglio. L'attesa non supera mai il tempo che
|
||||||
|
/// manca all'apertura meno un margine, così l'ultima interrogazione cade
|
||||||
|
/// <i>prima</i> dell'inizio e il motore è già alla cadenza normale quando l'asta
|
||||||
|
/// parte: allargare la cadenza non deve mai far perdere l'apertura.</para>
|
||||||
|
/// </summary>
|
||||||
|
private int NotStartedDelay(AppSettings settings)
|
||||||
|
{
|
||||||
|
if (!settings.ScheduledAuctionBackoffEnabled) return 0;
|
||||||
|
|
||||||
|
var status = _auction.LastState?.Status;
|
||||||
|
if (status is not (AuctionStatus.Scheduled or AuctionStatus.Pending)) return 0;
|
||||||
|
|
||||||
|
var toStartMs = _auction.EstimatedRemainingMs();
|
||||||
|
if (toStartMs == double.MaxValue || toStartMs <= 0) return 0;
|
||||||
|
|
||||||
|
// Vicino all'apertura si torna subito alla cadenza normale.
|
||||||
|
var wakeMs = Math.Max(30, settings.ScheduledPollWakeSeconds) * 1000.0;
|
||||||
|
if (toStartMs <= wakeMs) return 0;
|
||||||
|
|
||||||
|
var desired = toStartMs > 3_600_000
|
||||||
|
? Math.Max(60, settings.ScheduledPollFarSeconds) * 1000.0 // oltre un'ora
|
||||||
|
: toStartMs > 600_000
|
||||||
|
? Math.Max(30, settings.ScheduledPollMidSeconds) * 1000.0 // 10-60 minuti
|
||||||
|
: 30_000.0; // ultimi 10 minuti
|
||||||
|
|
||||||
|
// Non dormire mai oltre il momento del risveglio: è ciò che garantisce di
|
||||||
|
// essere pronti all'inizio.
|
||||||
|
var untilWake = toStartMs - wakeMs;
|
||||||
|
|
||||||
|
return (int)Math.Clamp(Math.Min(desired, untilWake), 1000, 1_800_000);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Anello del cecchino ──────────────────────────────────────────────────
|
||||||
|
|
||||||
|
private async Task SniperLoopAsync(CancellationToken ct)
|
||||||
|
{
|
||||||
|
while (!ct.IsCancellationRequested)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
if (_auction.State != RunState.Active || _host.IsFinished(_auction))
|
||||||
|
{
|
||||||
|
if (!await Wait.DelayAsync(150, ct).ConfigureAwait(false)) return;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
var state = _auction.LastState;
|
||||||
|
var deadline = _auction.DeadlineTicks;
|
||||||
|
|
||||||
|
if (state is null || deadline == 0)
|
||||||
|
{
|
||||||
|
if (!await Wait.DelayAsync(100, ct).ConfigureAwait(false)) return;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
var cycle = state.ExpiryUnixSeconds;
|
||||||
|
if (cycle == _handledCycle)
|
||||||
|
{
|
||||||
|
// Su questo ciclo abbiamo già deciso: non succede più nulla per noi
|
||||||
|
// finché la scadenza non passa o non ne arriva una nuova. Invece di
|
||||||
|
// svegliarsi quaranta volte al secondo a vuoto — che con venti aste
|
||||||
|
// significa ottocento risvegli inutili — si dorme fino alla scadenza,
|
||||||
|
// con un tetto di un secondo per accorgersi dei cambi di stato.
|
||||||
|
var idle = Math.Clamp(PrecisionWait.MsUntil(deadline), 25, 1000);
|
||||||
|
if (!await Wait.DelayAsync((int)idle, ct).ConfigureAwait(false)) return;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
var settings = _host.Settings;
|
||||||
|
var leadMs = _auction.BidBeforeDeadlineMs > 0
|
||||||
|
? _auction.BidBeforeDeadlineMs
|
||||||
|
: settings.DefaultBidBeforeDeadlineMs;
|
||||||
|
var fireTicks = deadline - PrecisionWait.MsToTicks(leadMs);
|
||||||
|
|
||||||
|
var msToDeadline = PrecisionWait.MsUntil(deadline);
|
||||||
|
if (msToDeadline <= 0)
|
||||||
|
{
|
||||||
|
// Il ciclo è scaduto senza che si arrivasse alla finestra utile.
|
||||||
|
if (_handledCycle != cycle)
|
||||||
|
{
|
||||||
|
_handledCycle = cycle;
|
||||||
|
_auction.AddLog(
|
||||||
|
$"Ciclo scaduto senza puntata (anticipo {leadMs} ms, ping medio {_auction.AverageLatencyMs:F0} ms)",
|
||||||
|
AuctionLogLevel.Debug, AuctionLogCategory.Ticker);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!await Wait.DelayAsync(25, ct).ConfigureAwait(false)) return;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
var msToFire = PrecisionWait.MsUntil(fireTicks);
|
||||||
|
if (msToFire > PreCheckMs)
|
||||||
|
{
|
||||||
|
// Ricontrolla spesso: una puntata altrui sposta la scadenza in avanti.
|
||||||
|
var sleep = Math.Clamp(msToFire - PreCheckMs, 5, 200);
|
||||||
|
if (!await Wait.DelayAsync((int)sleep, ct).ConfigureAwait(false)) return;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!_host.EvaluateBid(_auction, state))
|
||||||
|
{
|
||||||
|
_handledCycle = cycle;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Da qui il ciclo è nostro: nessun'altra iterazione lo riprenderà.
|
||||||
|
_handledCycle = cycle;
|
||||||
|
|
||||||
|
await PrecisionWait.UntilAsync(fireTicks, ct).ConfigureAwait(false);
|
||||||
|
|
||||||
|
// Ultimo controllo a costo zero: nel frattempo potremmo essere passati in testa.
|
||||||
|
if (_auction.LastState?.IsMyBid == true) continue;
|
||||||
|
|
||||||
|
var actualLeadMs = PrecisionWait.MsUntil(_auction.DeadlineTicks);
|
||||||
|
await _host.FireBidAsync(_auction, state, leadMs, actualLeadMs, ct).ConfigureAwait(false);
|
||||||
|
}
|
||||||
|
catch (OperationCanceledException)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
_auction.AddLog($"Errore nel cecchino: {ex.Message}",
|
||||||
|
AuctionLogLevel.Error, AuctionLogCategory.BidAttempt);
|
||||||
|
if (!await Wait.DelayAsync(250, ct).ConfigureAwait(false)) return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,174 @@
|
|||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Globalization;
|
||||||
|
using System.IO;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Text;
|
||||||
|
using AutoBidder.Utilities;
|
||||||
|
|
||||||
|
namespace AutoBidder.Engine.Backtest
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Rigioca una cartella di dossier e riassume cosa avrebbe fatto il motore, con un
|
||||||
|
/// anticipo alla volta.
|
||||||
|
///
|
||||||
|
/// <para>Serve a due cose distinte. La prima è la <b>taratura</b>: si vede subito
|
||||||
|
/// quante puntate costerebbe ciascun anticipo, e dove sta il gradino oltre il quale
|
||||||
|
/// il costo esplode. La seconda è il <b>controllo di regressione</b>: se una strategia
|
||||||
|
/// comincia a rifiutare puntate che prima passavano, qui si vede come un numero,
|
||||||
|
/// invece che come aste perse una alla volta senza capire perché.</para>
|
||||||
|
/// </summary>
|
||||||
|
public static class BacktestReport
|
||||||
|
{
|
||||||
|
/// <summary>Riepilogo su un intero insieme di aste, per un dato anticipo.</summary>
|
||||||
|
public sealed class Aggregate
|
||||||
|
{
|
||||||
|
public int LeadMs { get; init; }
|
||||||
|
public int Auctions { get; init; }
|
||||||
|
public int Cycles { get; init; }
|
||||||
|
|
||||||
|
/// <summary>Cicli arrivati fino all'anticipo su tutte le aste.</summary>
|
||||||
|
public int Reached { get; init; }
|
||||||
|
|
||||||
|
/// <summary>Puntate che sarebbero partite davvero.</summary>
|
||||||
|
public int Bids { get; init; }
|
||||||
|
|
||||||
|
public Dictionary<string, int> Blocks { get; init; } = new(StringComparer.Ordinal);
|
||||||
|
|
||||||
|
/// <summary>Puntate per asta: mediana e coda.</summary>
|
||||||
|
public double MedianBidsPerAuction { get; init; }
|
||||||
|
public int P90BidsPerAuction { get; init; }
|
||||||
|
public int MaxBidsPerAuction { get; init; }
|
||||||
|
|
||||||
|
/// <summary>Aste in cui sarebbe bastata una sola puntata.</summary>
|
||||||
|
public int AuctionsWithOneBid { get; init; }
|
||||||
|
|
||||||
|
public int BlockedTotal => Blocks.Values.Sum();
|
||||||
|
|
||||||
|
public double BlockedShare => Reached > 0 ? BlockedTotal * 100.0 / Reached : 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Rigioca ogni dossier della cartella con ciascun anticipo indicato.
|
||||||
|
/// <paramref name="onProgress"/> riceve (fatti, totale).
|
||||||
|
/// </summary>
|
||||||
|
public static List<Aggregate> Run(
|
||||||
|
string dossierFolder,
|
||||||
|
IReadOnlyList<int> leads,
|
||||||
|
AppSettings settings,
|
||||||
|
string username = "",
|
||||||
|
int maxFiles = 0,
|
||||||
|
Action<int, int>? onProgress = null)
|
||||||
|
{
|
||||||
|
var files = Directory.Exists(dossierFolder)
|
||||||
|
? Directory.GetFiles(dossierFolder, "*.jsonl").OrderBy(f => f).ToArray()
|
||||||
|
: Array.Empty<string>();
|
||||||
|
|
||||||
|
if (maxFiles > 0 && files.Length > maxFiles) files = files.Take(maxFiles).ToArray();
|
||||||
|
|
||||||
|
var perLead = leads.ToDictionary(l => l, _ => new List<BacktestRunner.Result>());
|
||||||
|
|
||||||
|
for (var i = 0; i < files.Length; i++)
|
||||||
|
{
|
||||||
|
DossierReader.Session session;
|
||||||
|
try { session = DossierReader.ReadFile(files[i]); }
|
||||||
|
catch (IOException) { continue; }
|
||||||
|
|
||||||
|
if (!session.IsUsable) continue;
|
||||||
|
|
||||||
|
var cost = session.Header?.BidCostEuro ?? 0.20;
|
||||||
|
|
||||||
|
foreach (var lead in leads)
|
||||||
|
{
|
||||||
|
var options = new BacktestRunner.Options(lead, settings, username, cost);
|
||||||
|
perLead[lead].Add(BacktestRunner.Run(session, options));
|
||||||
|
}
|
||||||
|
|
||||||
|
onProgress?.Invoke(i + 1, files.Length);
|
||||||
|
}
|
||||||
|
|
||||||
|
return leads.Select(lead => Summarize(lead, perLead[lead])).ToList();
|
||||||
|
}
|
||||||
|
|
||||||
|
public static Aggregate Summarize(int leadMs, IReadOnlyList<BacktestRunner.Result> results)
|
||||||
|
{
|
||||||
|
var blocks = new Dictionary<string, int>(StringComparer.Ordinal);
|
||||||
|
foreach (var r in results)
|
||||||
|
foreach (var (key, n) in r.Blocks)
|
||||||
|
blocks[key] = blocks.TryGetValue(key, out var have) ? have + n : n;
|
||||||
|
|
||||||
|
var perAuction = results.Select(r => r.Bids).OrderBy(n => n).ToList();
|
||||||
|
|
||||||
|
return new Aggregate
|
||||||
|
{
|
||||||
|
LeadMs = leadMs,
|
||||||
|
Auctions = results.Count,
|
||||||
|
Cycles = results.Sum(r => r.Cycles),
|
||||||
|
Reached = results.Sum(r => r.Reached),
|
||||||
|
Bids = results.Sum(r => r.Bids),
|
||||||
|
Blocks = blocks,
|
||||||
|
MedianBidsPerAuction = perAuction.Count == 0 ? 0 : perAuction[perAuction.Count / 2],
|
||||||
|
P90BidsPerAuction = perAuction.Count == 0 ? 0 : perAuction[(int)(perAuction.Count * 0.90)],
|
||||||
|
MaxBidsPerAuction = perAuction.Count == 0 ? 0 : perAuction[^1],
|
||||||
|
AuctionsWithOneBid = perAuction.Count(n => n == 1)
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>Rende il riepilogo in testo, pronto per il registro o per il terminale.</summary>
|
||||||
|
public static string Format(IReadOnlyList<Aggregate> aggregates)
|
||||||
|
{
|
||||||
|
var sb = new StringBuilder();
|
||||||
|
var c = CultureInfo.GetCultureInfo("it-IT");
|
||||||
|
|
||||||
|
sb.AppendLine("RIGIOCATA SUI DOSSIER DELLE ASTE CONCLUSE");
|
||||||
|
sb.AppendLine();
|
||||||
|
sb.AppendLine(" Il motore punta solo nei cicli che arrivano fino all'anticipo senza che");
|
||||||
|
sb.AppendLine(" nessun altro abbia puntato. Piu' l'anticipo e' largo, piu' cicli lo");
|
||||||
|
sb.AppendLine(" raggiungono, piu' puntate si spendono.");
|
||||||
|
sb.AppendLine();
|
||||||
|
sb.AppendLine(" Bidoo dichiara la scadenza al secondo intero: sotto il secondo i dati non");
|
||||||
|
sb.AppendLine(" distinguono nulla, quindi gli anticipi si confrontano a passi di 1000 ms.");
|
||||||
|
sb.AppendLine();
|
||||||
|
sb.AppendLine(" anticipo aste cicli raggiunti puntate bloccate mediana p90 max");
|
||||||
|
sb.AppendLine(" ---------------------------------------------------------------------------");
|
||||||
|
|
||||||
|
foreach (var a in aggregates)
|
||||||
|
{
|
||||||
|
sb.AppendLine(string.Format(c,
|
||||||
|
" {0,6} ms {1,6} {2,8} {3,11} {4,9} {5,9} {6,7} {7,4} {8,5}",
|
||||||
|
a.LeadMs, a.Auctions, a.Cycles, a.Reached, a.Bids,
|
||||||
|
$"{a.BlockedTotal} ({a.BlockedShare:F1}%)",
|
||||||
|
a.MedianBidsPerAuction, a.P90BidsPerAuction, a.MaxBidsPerAuction));
|
||||||
|
}
|
||||||
|
|
||||||
|
var withBlocks = aggregates.Where(a => a.BlockedTotal > 0).ToList();
|
||||||
|
if (withBlocks.Count > 0)
|
||||||
|
{
|
||||||
|
sb.AppendLine();
|
||||||
|
sb.AppendLine(" Blocchi per motivo:");
|
||||||
|
foreach (var a in withBlocks)
|
||||||
|
{
|
||||||
|
var detail = string.Join(", ", a.Blocks.OrderByDescending(kv => kv.Value)
|
||||||
|
.Select(kv => $"{kv.Key}={kv.Value}"));
|
||||||
|
sb.AppendLine($" anticipo {a.LeadMs} ms: {detail}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
sb.AppendLine();
|
||||||
|
sb.AppendLine(" Nessuna puntata bloccata dalle strategie.");
|
||||||
|
}
|
||||||
|
|
||||||
|
sb.AppendLine();
|
||||||
|
sb.AppendLine(" I blocchi 'pareggio', 'tetto-spesa' e 'tetto-puntate' sono il");
|
||||||
|
sb.AppendLine(" comportamento voluto: sono le puntate che avrebbero fatto perdere");
|
||||||
|
sb.AppendLine(" soldi. Gli altri motivi sono strategie difensive, e vanno guardati.");
|
||||||
|
sb.AppendLine();
|
||||||
|
sb.AppendLine(" NOTA: la rigiocata misura i costi, non l'esito. Una nostra puntata rimette");
|
||||||
|
sb.AppendLine(" in gioco l'asta, e come avrebbero reagito gli avversari non e' registrato");
|
||||||
|
sb.AppendLine(" da nessuna parte: dichiarare delle vittorie sarebbe inventarle.");
|
||||||
|
|
||||||
|
return sb.ToString();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,320 @@
|
|||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Linq;
|
||||||
|
using AutoBidder.Models;
|
||||||
|
using AutoBidder.Services;
|
||||||
|
using AutoBidder.Utilities;
|
||||||
|
|
||||||
|
namespace AutoBidder.Engine.Backtest
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Rigioca un'asta già conclusa e conta cosa avrebbe fatto il motore.
|
||||||
|
///
|
||||||
|
/// <para><b>Cosa misura davvero.</b> Il motore punta soltanto nei cicli che arrivano
|
||||||
|
/// fino all'anticipo impostato senza che nessun altro abbia puntato: se un avversario
|
||||||
|
/// punta prima, la scadenza si sposta e il cecchino non spara. Il dossier registra,
|
||||||
|
/// per ogni ciclo, quanto è sceso il timer prima che qualcuno intervenisse — che è
|
||||||
|
/// esattamente il dato che serve. Da lì si ricava <b>quante puntate sarebbero state
|
||||||
|
/// spese</b> e <b>quante volte una strategia le avrebbe bloccate</b>.</para>
|
||||||
|
///
|
||||||
|
/// <para><b>Cosa non può misurare, e perché va detto.</b> Non dice se avresti vinto.
|
||||||
|
/// Una nostra puntata rimette in gioco l'asta, e come avrebbero reagito gli avversari
|
||||||
|
/// non sta in nessun registro: nessuna rigiocata può inventarselo. L'ultimo ciclo di
|
||||||
|
/// ogni asta arriva per forza a zero — è il motivo per cui l'asta è finita — quindi
|
||||||
|
/// contarlo come vittoria sarebbe una conclusione costruita a tavolino. Qui si contano
|
||||||
|
/// i costi e i blocchi, che sono osservabili; l'esito no.</para>
|
||||||
|
/// </summary>
|
||||||
|
public static class BacktestRunner
|
||||||
|
{
|
||||||
|
/// <summary>Parametri della rigiocata.</summary>
|
||||||
|
public sealed record Options(
|
||||||
|
int LeadMs,
|
||||||
|
AppSettings Settings,
|
||||||
|
string Username = "",
|
||||||
|
double BidCostEuro = 0.20);
|
||||||
|
|
||||||
|
/// <summary>Esito su una singola asta.</summary>
|
||||||
|
public sealed class Result
|
||||||
|
{
|
||||||
|
public string AuctionId { get; init; } = "";
|
||||||
|
public string Name { get; init; } = "";
|
||||||
|
public string ProductKey { get; init; } = "";
|
||||||
|
|
||||||
|
/// <summary>Cicli di timer osservati nel dossier.</summary>
|
||||||
|
public int Cycles { get; init; }
|
||||||
|
|
||||||
|
/// <summary>Cicli arrivati fino all'anticipo: qui il motore avrebbe sparato.</summary>
|
||||||
|
public int Reached { get; init; }
|
||||||
|
|
||||||
|
/// <summary>Puntate davvero inviate, cioè i cicli raggiunti che nessuna strategia ha fermato.</summary>
|
||||||
|
public int Bids { get; init; }
|
||||||
|
|
||||||
|
/// <summary>Blocchi per motivo, con il testo che avrebbe scritto la strategia.</summary>
|
||||||
|
public Dictionary<string, int> Blocks { get; init; } = new(StringComparer.Ordinal);
|
||||||
|
|
||||||
|
public double FinalPrice { get; init; }
|
||||||
|
public double? BuyNowPrice { get; init; }
|
||||||
|
|
||||||
|
/// <summary>Costo se l'asta fosse stata vinta all'ultima puntata: prezzo + puntate spese.</summary>
|
||||||
|
public double CostIfWon { get; init; }
|
||||||
|
|
||||||
|
public int BlockedTotal => Blocks.Values.Sum();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>Motivi di blocco riconosciuti, per raggrupparli senza dipendere dal testo esatto.</summary>
|
||||||
|
public static string Categorize(string? reason)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrEmpty(reason)) return "altro";
|
||||||
|
|
||||||
|
if (reason.StartsWith("Anti-bot", StringComparison.OrdinalIgnoreCase)) return "anti-bot";
|
||||||
|
if (reason.StartsWith("Soft retreat", StringComparison.OrdinalIgnoreCase)) return "soft-retreat";
|
||||||
|
if (reason.StartsWith("Asta troppo calda", StringComparison.OrdinalIgnoreCase)) return "asta-calda";
|
||||||
|
if (reason.StartsWith("Bidder aggressivi", StringComparison.OrdinalIgnoreCase)) return "avversari-aggressivi";
|
||||||
|
if (reason.StartsWith("Skip probabilistico", StringComparison.OrdinalIgnoreCase)) return "probabilistico";
|
||||||
|
if (reason.StartsWith("Prezzo sale", StringComparison.OrdinalIgnoreCase)) return "velocita-prezzo";
|
||||||
|
if (reason.StartsWith("Limite puntate", StringComparison.OrdinalIgnoreCase)) return "limite-puntate";
|
||||||
|
if (reason.StartsWith("Budget", StringComparison.OrdinalIgnoreCase)) return "budget";
|
||||||
|
|
||||||
|
return "altro";
|
||||||
|
}
|
||||||
|
|
||||||
|
public static Result Run(DossierReader.Session session, Options options)
|
||||||
|
{
|
||||||
|
var header = session.Header;
|
||||||
|
var cycles = BuildCycles(session.Polls);
|
||||||
|
|
||||||
|
var strategy = new BidStrategyService();
|
||||||
|
var auction = NewAuction(header, options);
|
||||||
|
|
||||||
|
var blocks = new Dictionary<string, int>(StringComparer.Ordinal);
|
||||||
|
var reached = 0;
|
||||||
|
var bids = 0;
|
||||||
|
|
||||||
|
// Le puntate in ordine cronologico: a ogni ciclo si mostra al motore solo
|
||||||
|
// quelle che a quel momento erano già visibili. Mostrargliele tutte sarebbe
|
||||||
|
// dargli il futuro, e le strategie che guardano lo storico ne uscirebbero
|
||||||
|
// giudicate su informazioni che non avranno mai.
|
||||||
|
var bidsInOrder = session.Bids
|
||||||
|
.Where(b => b.UnixSeconds > 0)
|
||||||
|
.OrderBy(b => b.T)
|
||||||
|
.ToList();
|
||||||
|
|
||||||
|
var nextBid = 0;
|
||||||
|
|
||||||
|
foreach (var cycle in cycles)
|
||||||
|
{
|
||||||
|
while (nextBid < bidsInOrder.Count && bidsInOrder[nextBid].T <= cycle.LastPollT)
|
||||||
|
{
|
||||||
|
Publish(auction, bidsInOrder[nextBid], options.Username);
|
||||||
|
nextBid++;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Il ciclo è arrivato fino a noi solo se il timer è sceso sotto l'anticipo.
|
||||||
|
//
|
||||||
|
// Bidoo dichiara la scadenza al secondo intero, quindi un ciclo che risulta
|
||||||
|
// "sceso a 2" aveva in realtà fra 2 e 3 secondi davanti: il valore letto è un
|
||||||
|
// pavimento, non una misura. Per non contare puntate che forse non sarebbero
|
||||||
|
// partite si richiede il secondo <b>pieno</b> — con anticipo 2000 ms si
|
||||||
|
// considerano solo i cicli scesi sotto i 2 s. È il conteggio prudente, e la
|
||||||
|
// prudenza qui va verso il basso: meglio sottostimare il costo che promettere
|
||||||
|
// un risparmio che i dati non possono garantire.
|
||||||
|
if ((cycle.MinTimerSeconds + 1) * 1000.0 > options.LeadMs) continue;
|
||||||
|
|
||||||
|
var state = ToState(cycle, options.Username);
|
||||||
|
|
||||||
|
// Fuori corsa: il motore non punta su un'asta non aperta.
|
||||||
|
if (state.Status != AuctionStatus.Running && state.Status != AuctionStatus.Pending) continue;
|
||||||
|
if (state.IsMyBid) continue;
|
||||||
|
|
||||||
|
reached++;
|
||||||
|
|
||||||
|
// Budget e pareggio: e' il controllo che decide se la puntata ha ancora
|
||||||
|
// senso economico, e va rigiocato come tutti gli altri. Senza, la prova
|
||||||
|
// misurerebbe tutto tranne la cosa che protegge il portafoglio.
|
||||||
|
var budget = BidBudget.Evaluate(new BidBudget.Situation(
|
||||||
|
CurrentPrice: cycle.Price,
|
||||||
|
BidsAlreadyUsed: bids,
|
||||||
|
BidCostEuro: options.BidCostEuro,
|
||||||
|
ProductValue: header?.BuyNowPrice,
|
||||||
|
ShippingCost: null,
|
||||||
|
MaxBids: auction.MaxClicks,
|
||||||
|
MaxTotalSpendEuro: auction.MaxTotalSpendEuro,
|
||||||
|
MinSavingsPercentage: options.Settings.MinSavingsPercentage,
|
||||||
|
StopAtBreakEven: auction.StopAtBreakEven));
|
||||||
|
|
||||||
|
if (!budget.CanBid)
|
||||||
|
{
|
||||||
|
var key = budget.Reason?.Contains("pareggio") == true ? "pareggio"
|
||||||
|
: budget.Reason?.Contains("spesa") == true ? "tetto-spesa"
|
||||||
|
: "tetto-puntate";
|
||||||
|
blocks[key] = blocks.TryGetValue(key, out var b) ? b + 1 : 1;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
strategy.UpdateHeatMetric(auction, options.Settings, options.Username);
|
||||||
|
var decision = strategy.ShouldPlaceBid(auction, state, options.Settings, options.Username);
|
||||||
|
|
||||||
|
if (!decision.ShouldBid)
|
||||||
|
{
|
||||||
|
var key = Categorize(decision.Reason);
|
||||||
|
blocks[key] = blocks.TryGetValue(key, out var n) ? n + 1 : 1;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
bids++;
|
||||||
|
|
||||||
|
// Una puntata riuscita: è ciò che il motore registrerebbe, e alimenta i
|
||||||
|
// contatori su cui le strategie stesse decidono ai giri successivi.
|
||||||
|
strategy.RecordBidAttempt(auction, success: true);
|
||||||
|
auction.BidsUsedOnThisAuction = (auction.BidsUsedOnThisAuction ?? 0) + 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
var finalPrice = session.Summary?.FinalPrice ?? cycles.LastOrDefault()?.Price ?? 0;
|
||||||
|
|
||||||
|
return new Result
|
||||||
|
{
|
||||||
|
AuctionId = header?.AuctionId ?? "",
|
||||||
|
Name = header?.Name ?? "",
|
||||||
|
ProductKey = header?.ProductKey ?? "",
|
||||||
|
Cycles = cycles.Count,
|
||||||
|
Reached = reached,
|
||||||
|
Bids = bids,
|
||||||
|
Blocks = blocks,
|
||||||
|
FinalPrice = finalPrice,
|
||||||
|
BuyNowPrice = header?.BuyNowPrice,
|
||||||
|
CostIfWon = finalPrice + bids * options.BidCostEuro
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Ricostruzione dei cicli ──────────────────────────────────────
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Un ciclo di timer: dalla puntata che l'ha aperto alla successiva (o alla fine).
|
||||||
|
/// Lo identifica la <b>scadenza</b> dichiarata dal server, che cambia a ogni
|
||||||
|
/// puntata: è la stessa chiave che usa il cecchino per non puntare due volte
|
||||||
|
/// sullo stesso ciclo.
|
||||||
|
/// </summary>
|
||||||
|
public sealed record Cycle(
|
||||||
|
long ExpiryUnix,
|
||||||
|
double MinTimerSeconds,
|
||||||
|
double Price,
|
||||||
|
string LastBidder,
|
||||||
|
bool Mine,
|
||||||
|
string Status,
|
||||||
|
long ServerUnix,
|
||||||
|
double PingMs,
|
||||||
|
double LastPollT);
|
||||||
|
|
||||||
|
public static List<Cycle> BuildCycles(IReadOnlyList<DossierReader.Poll> polls)
|
||||||
|
{
|
||||||
|
var cycles = new List<Cycle>();
|
||||||
|
|
||||||
|
long currentExpiry = 0;
|
||||||
|
var minTimer = double.MaxValue;
|
||||||
|
DossierReader.Poll? deepest = null;
|
||||||
|
|
||||||
|
foreach (var poll in polls)
|
||||||
|
{
|
||||||
|
if (poll.ExpiryUnix <= 0 || poll.ServerUnix <= 0) continue;
|
||||||
|
|
||||||
|
if (poll.ExpiryUnix != currentExpiry)
|
||||||
|
{
|
||||||
|
if (deepest != null) cycles.Add(Close(currentExpiry, minTimer, deepest));
|
||||||
|
|
||||||
|
currentExpiry = poll.ExpiryUnix;
|
||||||
|
minTimer = double.MaxValue;
|
||||||
|
deepest = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Il timer calcolato dai due istanti del server, non quello riportato:
|
||||||
|
// sono la stessa cosa, ma questo non dipende da come è stato arrotondato.
|
||||||
|
var timer = poll.ExpiryUnix - poll.ServerUnix;
|
||||||
|
|
||||||
|
if (timer <= minTimer)
|
||||||
|
{
|
||||||
|
minTimer = timer;
|
||||||
|
deepest = poll;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (deepest != null) cycles.Add(Close(currentExpiry, minTimer, deepest));
|
||||||
|
|
||||||
|
return cycles;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static Cycle Close(long expiry, double minTimer, DossierReader.Poll deepest) => new(
|
||||||
|
ExpiryUnix: expiry,
|
||||||
|
MinTimerSeconds: Math.Max(0, minTimer),
|
||||||
|
Price: deepest.Price,
|
||||||
|
LastBidder: deepest.LastBidder,
|
||||||
|
Mine: deepest.Mine,
|
||||||
|
Status: deepest.Status,
|
||||||
|
ServerUnix: deepest.ServerUnix,
|
||||||
|
PingMs: deepest.PingMs,
|
||||||
|
LastPollT: deepest.T);
|
||||||
|
|
||||||
|
// ── Adattatori verso i modelli del motore ────────────────────────
|
||||||
|
|
||||||
|
private static AuctionState ToState(Cycle cycle, string username) => new()
|
||||||
|
{
|
||||||
|
AuctionId = "",
|
||||||
|
Timer = cycle.MinTimerSeconds,
|
||||||
|
Price = cycle.Price,
|
||||||
|
LastBidder = cycle.LastBidder,
|
||||||
|
IsMyBid = cycle.Mine ||
|
||||||
|
(!string.IsNullOrEmpty(username) &&
|
||||||
|
cycle.LastBidder.Equals(username, StringComparison.OrdinalIgnoreCase)),
|
||||||
|
Status = ParseStatus(cycle.Status),
|
||||||
|
ServerUnixSeconds = cycle.ServerUnix,
|
||||||
|
ExpiryUnixSeconds = cycle.ExpiryUnix,
|
||||||
|
PollingLatencyMs = (int)cycle.PingMs,
|
||||||
|
SnapshotTime = DateTimeOffset.FromUnixTimeSeconds(cycle.ServerUnix).UtcDateTime
|
||||||
|
};
|
||||||
|
|
||||||
|
private static AuctionStatus ParseStatus(string? status) =>
|
||||||
|
Enum.TryParse<AuctionStatus>(status, ignoreCase: true, out var parsed)
|
||||||
|
? parsed
|
||||||
|
: AuctionStatus.Unknown;
|
||||||
|
|
||||||
|
private static AuctionInfo NewAuction(DossierReader.Header? header, Options options) => new()
|
||||||
|
{
|
||||||
|
AuctionId = header?.AuctionId ?? "",
|
||||||
|
Name = header?.Name ?? "",
|
||||||
|
BuyNowPrice = header?.BuyNowPrice,
|
||||||
|
BidBeforeDeadlineMs = options.LeadMs,
|
||||||
|
BidsUsedOnThisAuction = 0
|
||||||
|
};
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Rende visibile al motore una puntata, come farebbe l'incorporazione di una
|
||||||
|
/// risposta di data.php: in testa alla lista, con il tetto delle ultime cinquanta.
|
||||||
|
/// </summary>
|
||||||
|
private static void Publish(AuctionInfo auction, DossierReader.Bid bid, string username)
|
||||||
|
{
|
||||||
|
var entry = new BidHistoryEntry
|
||||||
|
{
|
||||||
|
Price = (decimal)bid.Price,
|
||||||
|
Username = bid.User,
|
||||||
|
Timestamp = bid.UnixSeconds,
|
||||||
|
BidType = "Auto",
|
||||||
|
IsMyBid = bid.Mine ||
|
||||||
|
(!string.IsNullOrEmpty(username) &&
|
||||||
|
bid.User.Equals(username, StringComparison.OrdinalIgnoreCase))
|
||||||
|
};
|
||||||
|
|
||||||
|
lock (auction.BidsLock)
|
||||||
|
{
|
||||||
|
auction.RecentBids.Insert(0, entry);
|
||||||
|
if (auction.RecentBids.Count > 50) auction.RecentBids.RemoveRange(50, auction.RecentBids.Count - 50);
|
||||||
|
|
||||||
|
if (!auction.BidderStats.TryGetValue(bid.User, out var stats))
|
||||||
|
{
|
||||||
|
stats = new BidderInfo { Username = bid.User };
|
||||||
|
auction.BidderStats[bid.User] = stats;
|
||||||
|
}
|
||||||
|
|
||||||
|
stats.BidCount++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,218 @@
|
|||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Globalization;
|
||||||
|
using System.IO;
|
||||||
|
using System.Text.Json;
|
||||||
|
|
||||||
|
namespace AutoBidder.Engine.Backtest
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Legge un dossier d'asta (JSON Lines) e lo riporta in memoria come sequenza di
|
||||||
|
/// eventi tipizzati.
|
||||||
|
///
|
||||||
|
/// <para>È volutamente separato da chi lo consuma e non conosce il motore: il dossier
|
||||||
|
/// è un formato scritto altrove e cambierà, quindi la sua interpretazione va tenuta in
|
||||||
|
/// un punto solo e verificabile su righe scritte a mano.</para>
|
||||||
|
///
|
||||||
|
/// <para>Una riga che non si capisce viene <b>saltata</b>, non fa fallire la lettura:
|
||||||
|
/// i dossier vengono chiusi dal processo mentre gira, quindi l'ultima riga di un file
|
||||||
|
/// interrotto può benissimo essere monca — e sarebbe assurdo buttare via otto ore di
|
||||||
|
/// osservazione per l'ultimo mezzo evento.</para>
|
||||||
|
/// </summary>
|
||||||
|
public static class DossierReader
|
||||||
|
{
|
||||||
|
/// <summary>Intestazione: identità dell'asta e configurazione con cui era seguita.</summary>
|
||||||
|
public sealed record Header(
|
||||||
|
string AuctionId,
|
||||||
|
string Name,
|
||||||
|
string ProductKey,
|
||||||
|
double? BuyNowPrice,
|
||||||
|
double BidCostEuro,
|
||||||
|
int ConfiguredLeadMs,
|
||||||
|
string State);
|
||||||
|
|
||||||
|
/// <summary>Una singola interrogazione a data.php.</summary>
|
||||||
|
public sealed record Poll(
|
||||||
|
double T,
|
||||||
|
double Price,
|
||||||
|
double Timer,
|
||||||
|
string Status,
|
||||||
|
string LastBidder,
|
||||||
|
bool Mine,
|
||||||
|
long ExpiryUnix,
|
||||||
|
long ServerUnix,
|
||||||
|
double PingMs);
|
||||||
|
|
||||||
|
/// <summary>Una puntata altrui (o nostra) letta dallo storico.</summary>
|
||||||
|
public sealed record Bid(
|
||||||
|
double T,
|
||||||
|
string User,
|
||||||
|
long UnixSeconds,
|
||||||
|
double Price,
|
||||||
|
bool Mine);
|
||||||
|
|
||||||
|
/// <summary>Riepilogo scritto alla chiusura dell'asta.</summary>
|
||||||
|
public sealed record Summary(
|
||||||
|
string Outcome,
|
||||||
|
string Winner,
|
||||||
|
double FinalPrice,
|
||||||
|
bool WonByMe,
|
||||||
|
int Resets,
|
||||||
|
int DistinctBidders,
|
||||||
|
int MyBids);
|
||||||
|
|
||||||
|
/// <summary>Un dossier completo.</summary>
|
||||||
|
public sealed class Session
|
||||||
|
{
|
||||||
|
public Header? Header { get; init; }
|
||||||
|
public Summary? Summary { get; init; }
|
||||||
|
public List<Poll> Polls { get; init; } = new();
|
||||||
|
public List<Bid> Bids { get; init; } = new();
|
||||||
|
|
||||||
|
/// <summary>Righe che non si è riusciti a interpretare.</summary>
|
||||||
|
public int SkippedLines { get; init; }
|
||||||
|
|
||||||
|
/// <summary>Un dossier senza intestazione non dice di che asta parla.</summary>
|
||||||
|
public bool IsUsable => Header != null && Polls.Count > 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static Session ReadFile(string path) => Read(File.ReadLines(path));
|
||||||
|
|
||||||
|
public static Session Read(IEnumerable<string> lines)
|
||||||
|
{
|
||||||
|
Header? header = null;
|
||||||
|
Summary? summary = null;
|
||||||
|
var polls = new List<Poll>();
|
||||||
|
var bids = new List<Bid>();
|
||||||
|
var skipped = 0;
|
||||||
|
|
||||||
|
foreach (var raw in lines)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrWhiteSpace(raw)) continue;
|
||||||
|
|
||||||
|
// Il primo file scritto porta il segnaposto di codifica in testa.
|
||||||
|
var line = raw.TrimStart('', ' ', '\t');
|
||||||
|
if (line.Length == 0 || line[0] != '{') { skipped++; continue; }
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
using var doc = JsonDocument.Parse(line);
|
||||||
|
var root = doc.RootElement;
|
||||||
|
|
||||||
|
switch (Str(root, "type"))
|
||||||
|
{
|
||||||
|
case "header": header = ReadHeader(root); break;
|
||||||
|
case "summary": summary = ReadSummary(root); break;
|
||||||
|
case "poll": polls.Add(ReadPoll(root)); break;
|
||||||
|
case "bid": bids.Add(ReadBid(root)); break;
|
||||||
|
// reset e log non servono alla simulazione: il reset è deducibile
|
||||||
|
// dal cambio di scadenza, e il log è testo per l'utente.
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch (JsonException)
|
||||||
|
{
|
||||||
|
skipped++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return new Session
|
||||||
|
{
|
||||||
|
Header = header,
|
||||||
|
Summary = summary,
|
||||||
|
Polls = polls,
|
||||||
|
Bids = bids,
|
||||||
|
SkippedLines = skipped
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Lettura dei singoli eventi ───────────────────────────────────
|
||||||
|
|
||||||
|
private static Header ReadHeader(JsonElement root)
|
||||||
|
{
|
||||||
|
var product = Obj(root, "product");
|
||||||
|
var config = Obj(root, "config");
|
||||||
|
|
||||||
|
return new Header(
|
||||||
|
AuctionId: Str(root, "auctionId") ?? "",
|
||||||
|
Name: Str(root, "name") ?? "",
|
||||||
|
ProductKey: Str(root, "productKey") ?? "",
|
||||||
|
BuyNowPrice: product.HasValue ? NullableNum(product.Value, "buyNowPrice") : null,
|
||||||
|
BidCostEuro: product.HasValue ? Num(product.Value, "bidCostEuro", 0.20) : 0.20,
|
||||||
|
ConfiguredLeadMs: config.HasValue ? (int)Num(config.Value, "bidBeforeDeadlineMs", 0) : 0,
|
||||||
|
State: config.HasValue ? Str(config.Value, "state") ?? "" : "");
|
||||||
|
}
|
||||||
|
|
||||||
|
private static Summary ReadSummary(JsonElement root)
|
||||||
|
{
|
||||||
|
var part = Obj(root, "participation");
|
||||||
|
|
||||||
|
return new Summary(
|
||||||
|
Outcome: Str(root, "outcome") ?? "",
|
||||||
|
Winner: Str(root, "winner") ?? "",
|
||||||
|
FinalPrice: Num(root, "finalPrice", 0),
|
||||||
|
WonByMe: Bool(root, "wonByMe"),
|
||||||
|
Resets: part.HasValue ? (int)Num(part.Value, "resets", 0) : 0,
|
||||||
|
DistinctBidders: part.HasValue ? (int)Num(part.Value, "distinctBidders", 0) : 0,
|
||||||
|
MyBids: part.HasValue ? (int)Num(part.Value, "myBids", 0) : 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static Poll ReadPoll(JsonElement root) => new(
|
||||||
|
T: Num(root, "t", 0),
|
||||||
|
Price: Num(root, "price", 0),
|
||||||
|
Timer: Num(root, "timer", 0),
|
||||||
|
Status: Str(root, "status") ?? "",
|
||||||
|
LastBidder: Str(root, "lastBidder") ?? "",
|
||||||
|
Mine: Bool(root, "mine"),
|
||||||
|
ExpiryUnix: (long)Num(root, "expiryUnix", 0),
|
||||||
|
ServerUnix: (long)Num(root, "serverUnix", 0),
|
||||||
|
PingMs: Num(root, "pingMs", 0));
|
||||||
|
|
||||||
|
private static Bid ReadBid(JsonElement root) => new(
|
||||||
|
T: Num(root, "t", 0),
|
||||||
|
User: Str(root, "user") ?? "",
|
||||||
|
UnixSeconds: ParseUnix(Str(root, "bidAt")),
|
||||||
|
Price: Num(root, "price", 0),
|
||||||
|
Mine: Bool(root, "mine"));
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Le puntate portano l'istante come data ISO con fuso. Serve il secondo unix,
|
||||||
|
/// perché è nella stessa unità dei timestamp che il motore vede a runtime.
|
||||||
|
/// </summary>
|
||||||
|
private static long ParseUnix(string? iso)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrWhiteSpace(iso)) return 0;
|
||||||
|
|
||||||
|
return DateTimeOffset.TryParse(iso, CultureInfo.InvariantCulture,
|
||||||
|
DateTimeStyles.RoundtripKind, out var parsed)
|
||||||
|
? parsed.ToUnixTimeSeconds()
|
||||||
|
: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Accessori tolleranti ─────────────────────────────────────────
|
||||||
|
|
||||||
|
private static string? Str(JsonElement owner, string name) =>
|
||||||
|
owner.TryGetProperty(name, out var v) && v.ValueKind == JsonValueKind.String
|
||||||
|
? v.GetString()
|
||||||
|
: null;
|
||||||
|
|
||||||
|
private static bool Bool(JsonElement owner, string name) =>
|
||||||
|
owner.TryGetProperty(name, out var v) && v.ValueKind == JsonValueKind.True;
|
||||||
|
|
||||||
|
private static double Num(JsonElement owner, string name, double fallback) =>
|
||||||
|
owner.TryGetProperty(name, out var v) && v.ValueKind == JsonValueKind.Number &&
|
||||||
|
v.TryGetDouble(out var d)
|
||||||
|
? d
|
||||||
|
: fallback;
|
||||||
|
|
||||||
|
private static double? NullableNum(JsonElement owner, string name) =>
|
||||||
|
owner.TryGetProperty(name, out var v) && v.ValueKind == JsonValueKind.Number &&
|
||||||
|
v.TryGetDouble(out var d)
|
||||||
|
? d
|
||||||
|
: null;
|
||||||
|
|
||||||
|
private static JsonElement? Obj(JsonElement owner, string name) =>
|
||||||
|
owner.TryGetProperty(name, out var v) && v.ValueKind == JsonValueKind.Object
|
||||||
|
? v
|
||||||
|
: null;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,116 @@
|
|||||||
|
using System;
|
||||||
|
using System.Diagnostics;
|
||||||
|
using System.Runtime.CompilerServices;
|
||||||
|
using System.Runtime.InteropServices;
|
||||||
|
using System.Threading;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
|
||||||
|
namespace AutoBidder.Engine
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Attesa fino a un istante espresso in tick di <see cref="Stopwatch"/>.
|
||||||
|
///
|
||||||
|
/// Task.Delay si appoggia al timer di sistema, che di default scatta ogni 15,6 ms: per un
|
||||||
|
/// anticipo di 200 ms sulla scadenza è un errore inaccettabile. Qui si dorme finché manca
|
||||||
|
/// parecchio, lasciando un margine più ampio della granularità del timer, e si copre
|
||||||
|
/// l'ultimo tratto con attesa attiva. Il costo è qualche decina di millisecondi di CPU per
|
||||||
|
/// puntata; il guadagno è un errore sotto il millisecondo.
|
||||||
|
/// </summary>
|
||||||
|
public static class PrecisionWait
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Margine da coprire in attesa attiva. Più ampio della granularità del timer di
|
||||||
|
/// sistema, così anche un Task.Delay che sfora si risveglia comunque prima del bersaglio.
|
||||||
|
/// </summary>
|
||||||
|
private const double SpinThresholdMs = 22;
|
||||||
|
|
||||||
|
/// <summary>Ultimo tratto in cui non si cede più il core a nessuno.</summary>
|
||||||
|
private const double TightSpinMs = 2.0;
|
||||||
|
|
||||||
|
public static async Task UntilAsync(long targetTicks, CancellationToken ct)
|
||||||
|
{
|
||||||
|
var frequency = Stopwatch.Frequency;
|
||||||
|
|
||||||
|
while (!ct.IsCancellationRequested)
|
||||||
|
{
|
||||||
|
var remainingMs = (targetTicks - Stopwatch.GetTimestamp()) * 1000.0 / frequency;
|
||||||
|
if (remainingMs <= 0) return;
|
||||||
|
|
||||||
|
if (remainingMs > SpinThresholdMs)
|
||||||
|
{
|
||||||
|
var sleepMs = remainingMs - SpinThresholdMs;
|
||||||
|
await Task.Delay(TimeSpan.FromMilliseconds(Math.Min(sleepMs, 1000)), ct).ConfigureAwait(false);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
SpinUntil(targetTicks, ct);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
[MethodImpl(MethodImplOptions.AggressiveOptimization)]
|
||||||
|
private static void SpinUntil(long targetTicks, CancellationToken ct)
|
||||||
|
{
|
||||||
|
var frequency = Stopwatch.Frequency;
|
||||||
|
|
||||||
|
while (true)
|
||||||
|
{
|
||||||
|
var remainingMs = (targetTicks - Stopwatch.GetTimestamp()) * 1000.0 / frequency;
|
||||||
|
if (remainingMs <= 0 || ct.IsCancellationRequested) return;
|
||||||
|
|
||||||
|
// Finché c'è margine si cede volentieri il core; nell'ultimo tratto no,
|
||||||
|
// altrimenti bastano due millisecondi di preemption per arrivare tardi.
|
||||||
|
if (remainingMs > TightSpinMs) Thread.Yield();
|
||||||
|
else Thread.SpinWait(40);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>Millisecondi mancanti a un istante espresso in tick.</summary>
|
||||||
|
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||||
|
public static double MsUntil(long targetTicks) =>
|
||||||
|
(targetTicks - Stopwatch.GetTimestamp()) * 1000.0 / Stopwatch.Frequency;
|
||||||
|
|
||||||
|
/// <summary>Converte millisecondi in tick di <see cref="Stopwatch"/>.</summary>
|
||||||
|
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||||
|
public static long MsToTicks(double ms) => (long)(ms / 1000.0 * Stopwatch.Frequency);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Porta la granularità del timer di sistema a 1 ms per tutta la durata del processo.
|
||||||
|
///
|
||||||
|
/// Senza questa richiesta ogni Task.Delay può sforare di ~15 ms: si ripercuote sulla
|
||||||
|
/// cadenza del polling e allunga l'attesa attiva necessaria prima di ogni puntata.
|
||||||
|
/// È la stessa cosa che fanno browser e riproduttori multimediali; l'unico effetto
|
||||||
|
/// collaterale è un consumo leggermente maggiore, accettabile per un'applicazione che
|
||||||
|
/// deve rispettare scadenze al millisecondo.
|
||||||
|
/// </summary>
|
||||||
|
public sealed class SystemTimerResolution : IDisposable
|
||||||
|
{
|
||||||
|
[DllImport("winmm.dll", EntryPoint = "timeBeginPeriod")]
|
||||||
|
private static extern uint TimeBeginPeriod(uint milliseconds);
|
||||||
|
|
||||||
|
[DllImport("winmm.dll", EntryPoint = "timeEndPeriod")]
|
||||||
|
private static extern uint TimeEndPeriod(uint milliseconds);
|
||||||
|
|
||||||
|
private const uint TargetMs = 1;
|
||||||
|
private const uint Ok = 0;
|
||||||
|
|
||||||
|
private bool _active;
|
||||||
|
|
||||||
|
public bool IsActive => _active;
|
||||||
|
|
||||||
|
public SystemTimerResolution()
|
||||||
|
{
|
||||||
|
try { _active = TimeBeginPeriod(TargetMs) == Ok; }
|
||||||
|
catch { _active = false; }
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Dispose()
|
||||||
|
{
|
||||||
|
if (!_active) return;
|
||||||
|
|
||||||
|
try { TimeEndPeriod(TargetMs); } catch { /* rilascio best-effort */ }
|
||||||
|
_active = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,122 @@
|
|||||||
|
using System;
|
||||||
|
using System.Diagnostics;
|
||||||
|
|
||||||
|
namespace AutoBidder.Engine
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Allinea l'orologio monotono locale a quello del server Bidoo.
|
||||||
|
///
|
||||||
|
/// data.php restituisce timestamp in secondi interi: preso da solo, un singolo campione
|
||||||
|
/// vale ±1 s. Ma ogni risposta dice anche "il mio secondo corrente è S", e la risposta è
|
||||||
|
/// stata generata prima di arrivare qui. Detto A = istante locale del confine del secondo
|
||||||
|
/// server 0, ogni campione fornisce un limite superiore per A; il minimo su molti campioni
|
||||||
|
/// converge al valore vero (filtro alla NTP).
|
||||||
|
///
|
||||||
|
/// L'errore residuo è per costruzione verso il basso, cioè verso una scadenza stimata
|
||||||
|
/// leggermente in anticipo: si punta un filo prima, mai dopo.
|
||||||
|
/// </summary>
|
||||||
|
public sealed class ServerClock
|
||||||
|
{
|
||||||
|
private const int WindowSize = 256;
|
||||||
|
|
||||||
|
private readonly long[] _samples = new long[WindowSize];
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Round-trip di ciascun campione. Vive qui perché qui arriva già misurato, e
|
||||||
|
/// perché è la latenza che conta davvero: quella delle richieste di polling, le
|
||||||
|
/// stesse su cui il motore decide quando puntare.
|
||||||
|
/// </summary>
|
||||||
|
private readonly int[] _latencies = new int[WindowSize];
|
||||||
|
|
||||||
|
private readonly object _sync = new();
|
||||||
|
private int _index;
|
||||||
|
private int _count;
|
||||||
|
private long _latencySum;
|
||||||
|
|
||||||
|
/// <summary>Numero di campioni utilizzati per la stima corrente.</summary>
|
||||||
|
public int SampleCount { get { lock (_sync) return _count; } }
|
||||||
|
|
||||||
|
/// <summary>True quando la stima è abbastanza solida da essere usata per il timing.</summary>
|
||||||
|
public bool IsSynced { get { lock (_sync) return _count >= 3; } }
|
||||||
|
|
||||||
|
/// <summary>Scarto residuo stimato in millisecondi (dispersione dei campioni).</summary>
|
||||||
|
public double SpreadMs { get; private set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Round-trip medio verso Bidoo sugli ultimi campioni, in millisecondi.
|
||||||
|
/// Zero finché non è arrivata nessuna risposta.
|
||||||
|
/// </summary>
|
||||||
|
public double AverageLatencyMs
|
||||||
|
{
|
||||||
|
get { lock (_sync) return _count == 0 ? 0 : (double)_latencySum / _count; }
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>Tick locale corrispondente al secondo unix 0 del server.</summary>
|
||||||
|
public long Origin { get; private set; }
|
||||||
|
|
||||||
|
/// <summary>Registra una risposta del server.</summary>
|
||||||
|
/// <param name="serverUnixSeconds">Secondo unix dichiarato dal server.</param>
|
||||||
|
/// <param name="receivedTicks">Tick di <see cref="Stopwatch"/> alla ricezione.</param>
|
||||||
|
/// <param name="latencyMs">Round-trip misurato.</param>
|
||||||
|
public void AddSample(long serverUnixSeconds, long receivedTicks, int latencyMs)
|
||||||
|
{
|
||||||
|
if (serverUnixSeconds <= 0 || receivedTicks <= 0) return;
|
||||||
|
|
||||||
|
var f = Stopwatch.Frequency;
|
||||||
|
|
||||||
|
// Momento in cui il server ha prodotto la risposta: metà round-trip fa.
|
||||||
|
var generatedTicks = receivedTicks - (long)(latencyMs / 2.0 / 1000.0 * f);
|
||||||
|
|
||||||
|
// Limite superiore per l'origine A.
|
||||||
|
var candidate = generatedTicks - serverUnixSeconds * f;
|
||||||
|
|
||||||
|
lock (_sync)
|
||||||
|
{
|
||||||
|
// La finestra è circolare: prima di sovrascrivere si toglie dalla somma
|
||||||
|
// il campione che esce, altrimenti la media resterebbe ferma sul passato.
|
||||||
|
if (_count == WindowSize) _latencySum -= _latencies[_index];
|
||||||
|
|
||||||
|
_samples[_index] = candidate;
|
||||||
|
_latencies[_index] = latencyMs < 0 ? 0 : latencyMs;
|
||||||
|
_latencySum += _latencies[_index];
|
||||||
|
|
||||||
|
_index = (_index + 1) % WindowSize;
|
||||||
|
if (_count < WindowSize) _count++;
|
||||||
|
|
||||||
|
long min = long.MaxValue, max = long.MinValue;
|
||||||
|
for (var i = 0; i < _count; i++)
|
||||||
|
{
|
||||||
|
var v = _samples[i];
|
||||||
|
if (v < min) min = v;
|
||||||
|
if (v > max) max = v;
|
||||||
|
}
|
||||||
|
|
||||||
|
Origin = min;
|
||||||
|
SpreadMs = (max - min) * 1000.0 / f;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>Converte un timestamp unix del server in tick locali di <see cref="Stopwatch"/>.</summary>
|
||||||
|
public long ToLocalTicks(long serverUnixSeconds)
|
||||||
|
{
|
||||||
|
lock (_sync)
|
||||||
|
{
|
||||||
|
return Origin + serverUnixSeconds * Stopwatch.Frequency;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Reset()
|
||||||
|
{
|
||||||
|
lock (_sync)
|
||||||
|
{
|
||||||
|
Array.Clear(_samples);
|
||||||
|
Array.Clear(_latencies);
|
||||||
|
_index = 0;
|
||||||
|
_count = 0;
|
||||||
|
_latencySum = 0;
|
||||||
|
Origin = 0;
|
||||||
|
SpreadMs = 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
File diff suppressed because it is too large
Load Diff
@@ -1,18 +0,0 @@
|
|||||||
<?xml version="1.0" encoding="UTF-8"?>
|
|
||||||
<svg width="17px" height="18px" viewBox="0 0 17 18" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
|
|
||||||
<!-- Generator: sketchtool 57.1 (101010) - https://sketch.com -->
|
|
||||||
<title>E3DC3394-397D-4994-B12B-47234FB13863</title>
|
|
||||||
<desc>Created with sketchtool.</desc>
|
|
||||||
<g id="Product-Pages" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
|
|
||||||
<g id="Home---Portrait-Version-A-Copy-6" transform="translate(-77.000000, -637.000000)">
|
|
||||||
<g id="Group-20" transform="translate(63.000000, 553.000000)">
|
|
||||||
<g id="Group-17-Copy" transform="translate(14.000000, 84.000000)">
|
|
||||||
<g id="001-settings" transform="translate(0.000000, 0.500000)">
|
|
||||||
<path d="M15.6392002,7.48800011 C14.1532002,7.20200011 13.4720002,5.46640008 14.3672002,4.24600006 L14.9952002,3.38800005 L13.6376002,2.03040003 L12.7936002,2.60200004 C11.5408002,3.45200005 9.83120015,2.70520004 9.60160014,1.21000002 L9.44080014,0.160000002 L7.52040011,0.160000002 L7.27200011,1.45360002 C6.9920001,2.90520004 5.31720008,3.59920005 4.09200006,2.76920004 L3.00320004,2.03040003 L1.64520002,3.38800005 L2.27360003,4.24600006 C3.16880005,5.46640008 2.48600004,7.20200011 1.00160001,7.48800011 L0,7.68040011 L0,9.60080014 L1.05000002,9.76160015 C2.54520004,9.99120015 3.29200005,11.7008002 2.44200004,12.9536002 L1.87040003,13.7976002 L3.22800005,15.1552002 L4.08600006,14.5272002 C5.30640008,13.6320002 7.0420001,14.3132002 7.32800011,15.7992002 L7.52040011,16.8008003 L9.44080014,16.8008003 L9.55320014,16.0616002 C9.78920015,14.5336002 11.5608002,13.7992002 12.8080002,14.7132002 L13.4108002,15.1552002 L14.7688002,13.7976002 L14.1968002,12.9536002 C13.3484002,11.7008002 14.0936002,9.99120015 15.5892002,9.76160015 L16.6408002,9.60080014 L16.6408002,7.68040011 L15.6392002,7.48800011 Z M8.47960013,10.0804002 C7.68440011,10.0804002 7.0408001,9.43520014 7.0408001,8.63960013 C7.0408001,7.84440012 7.68440011,7.20080011 8.47960013,7.20080011 C9.27520014,7.20080011 9.92040015,7.84440012 9.92040015,8.63960013 C9.92040015,9.43520014 9.27520014,10.0804002 8.47960013,10.0804002 Z" id="Fill-1" fill="#C7CAC7"/>
|
|
||||||
<path d="M8.47960013,5.60080008 C6.8016001,5.60080008 5.44080008,6.9616001 5.44080008,8.63960013 C5.44080008,10.3192002 6.8016001,11.6804002 8.47960013,11.6804002 C10.1592002,11.6804002 11.5204002,10.3192002 11.5204002,8.63960013 C11.5204002,6.9616001 10.1592002,5.60080008 8.47960013,5.60080008 Z M8.47960013,10.0804002 C7.68440011,10.0804002 7.0408001,9.43520014 7.0408001,8.63960013 C7.0408001,7.84440012 7.68440011,7.20080011 8.47960013,7.20080011 C9.27520014,7.20080011 9.92040015,7.84440012 9.92040015,8.63960013 C9.92040015,9.43520014 9.27520014,10.0804002 8.47960013,10.0804002 Z" id="Fill-2" fill="#556080"/>
|
|
||||||
</g>
|
|
||||||
</g>
|
|
||||||
</g>
|
|
||||||
</g>
|
|
||||||
</g>
|
|
||||||
</svg>
|
|
||||||
|
Before Width: | Height: | Size: 2.9 KiB |
@@ -1 +0,0 @@
|
|||||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 18.96 32.35"><defs><style>.cls-1{fill:#38454f;}.cls-2{fill:#1caee4;}.cls-3{fill:#1081e0;}.cls-4{fill:#cbd4d8;}.cls-5{fill:#546a79;}</style></defs><title>Asset 10002-app</title><g id="Layer_2" data-name="Layer 2"><g id="Livello_1" data-name="Livello 1"><path class="cls-1" d="M15.76,32.35H3.2A3.21,3.21,0,0,1,0,29.14V3.2A3.2,3.2,0,0,1,3.2,0H15.76A3.2,3.2,0,0,1,19,3.2V29.14A3.21,3.21,0,0,1,15.76,32.35Z"/><rect class="cls-2" x="1.67" y="3.35" width="15.61" height="23.98"/><path class="cls-3" d="M3.35,7.81a.56.56,0,0,0,.39-.17L6,5.41a.56.56,0,0,0,0-.79.57.57,0,0,0-.79,0L3,6.86a.54.54,0,0,0,0,.78A.56.56,0,0,0,3.35,7.81Z"/><path class="cls-3" d="M3.35,10.6a.56.56,0,0,0,.39-.17L4.86,9.32a.57.57,0,0,0,0-.79.56.56,0,0,0-.79,0L3,9.64a.57.57,0,0,0,.4,1Z"/><path class="cls-3" d="M5.18,7.41a.59.59,0,0,0-.16.4.57.57,0,0,0,.16.39.6.6,0,0,0,.4.17A.58.58,0,0,0,6,8.2a.57.57,0,0,0,.16-.39.56.56,0,0,0-1-.4Z"/><path class="cls-3" d="M6.3,7.09a.54.54,0,0,0,.39.16.57.57,0,0,0,.4-.16L8.76,5.41a.56.56,0,0,0,0-.79.57.57,0,0,0-.79,0L6.3,6.3A.56.56,0,0,0,6.3,7.09Z"/><path class="cls-3" d="M8,7.41l-5,5a.56.56,0,0,0,.4,1,.54.54,0,0,0,.39-.16l5-5a.56.56,0,0,0,0-.79A.57.57,0,0,0,8,7.41Z"/><path class="cls-3" d="M9.08,6.3a.57.57,0,0,0-.16.39.59.59,0,0,0,.16.4.61.61,0,0,0,.4.16A.55.55,0,0,0,10,6.69a.57.57,0,0,0-.16-.39A.59.59,0,0,0,9.08,6.3Z"/><path class="cls-3" d="M11.55,4.62a.57.57,0,0,0-.79,0l-.56.56a.56.56,0,0,0,.4,1A.54.54,0,0,0,11,6l.56-.56A.56.56,0,0,0,11.55,4.62Z"/><path class="cls-4" d="M11.15,2.23H7.81a.56.56,0,0,1-.56-.56.55.55,0,0,1,.56-.55h3.34a.55.55,0,0,1,.56.55A.56.56,0,0,1,11.15,2.23Z"/><path class="cls-5" d="M16.17,2.23h-.56a.56.56,0,0,1-.55-.56.55.55,0,0,1,.55-.55h.56a.55.55,0,0,1,.56.55A.56.56,0,0,1,16.17,2.23Z"/><path class="cls-5" d="M13.94,2.23h-.56a.56.56,0,0,1-.55-.56.55.55,0,0,1,.55-.55h.56a.55.55,0,0,1,.56.55A.56.56,0,0,1,13.94,2.23Z"/><path class="cls-4" d="M10.87,30.67H8.09a.84.84,0,0,1-.84-.83h0A.85.85,0,0,1,8.09,29h2.78a.85.85,0,0,1,.84.84h0A.84.84,0,0,1,10.87,30.67Z"/></g></g></svg>
|
|
||||||
|
Before Width: | Height: | Size: 2.0 KiB |
@@ -1 +0,0 @@
|
|||||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 31.83 32.52"><defs><style>.cls-1{fill:#ffd039;}.cls-2{fill:#f4b70c;}.cls-3{fill:#ffbb64;}.cls-4{fill:#ffae47;}.cls-5{fill:#ffdf65;}.cls-6{fill:#ffcd2c;}.cls-7{fill:#ffa035;}.cls-8{fill:#f78819;}</style></defs><title>Asset 9002-cup</title><g id="Layer_2" data-name="Layer 2"><g id="Livello_1" data-name="Livello 1"><path class="cls-1" d="M31.05,5.85h0a3.61,3.61,0,0,0-2.83-1.36H24.84a.47.47,0,0,0-.47.48V7.05a.47.47,0,0,0,.47.48h3.38a.56.56,0,0,1,.45.22.58.58,0,0,1,.11.46,9,9,0,0,1-1.93,4,5.79,5.79,0,0,1-2.37,1.58.46.46,0,0,0-.31.34,8.32,8.32,0,0,1-.84,2.26.46.46,0,0,0,0,.5.46.46,0,0,0,.39.2h.08a9,9,0,0,0,5.27-2.83,11.8,11.8,0,0,0,2.64-5.33A3.61,3.61,0,0,0,31.05,5.85Z"/><path class="cls-2" d="M27,12.28V12l-.15.16a5.79,5.79,0,0,1-2.37,1.58.46.46,0,0,0-.31.34,8.32,8.32,0,0,1-.84,2.26.46.46,0,0,0,0,.5.46.46,0,0,0,.39.2h.08a10,10,0,0,0,2.22-.65A9.45,9.45,0,0,0,27,12.28Z"/><path class="cls-2" d="M24.37,5V7.05a.47.47,0,0,0,.47.48H27v-3H24.84A.47.47,0,0,0,24.37,5Z"/><path class="cls-3" d="M19.5,28.05c-.14-.11-1.42-1.18-1.58-7a.49.49,0,0,0-.17-.36.54.54,0,0,0-.39-.1,8.66,8.66,0,0,1-1.44.13h0a8.69,8.69,0,0,1-1.45-.13.54.54,0,0,0-.39.1.49.49,0,0,0-.17.36c-.16,5.8-1.44,6.87-1.58,7a.44.44,0,0,0-.32.5.51.51,0,0,0,.5.42h6.81a.51.51,0,0,0,.5-.42A.44.44,0,0,0,19.5,28.05Z"/><path class="cls-4" d="M19.5,28.05c-.14-.11-1.42-1.18-1.58-7a.49.49,0,0,0-.17-.36.54.54,0,0,0-.39-.1,8.66,8.66,0,0,1-1.44.13h0l-.46,0a.48.48,0,0,1,.16.35c.16,5.8,1.43,6.87,1.58,7a.44.44,0,0,1,.32.5A.51.51,0,0,1,17,29h2.31a.51.51,0,0,0,.5-.42A.44.44,0,0,0,19.5,28.05Z"/><path class="cls-1" d="M.79,5.85h0A3.58,3.58,0,0,1,3.61,4.49H7A.47.47,0,0,1,7.46,5V7.05A.47.47,0,0,1,7,7.53H3.61a.56.56,0,0,0-.45.22.58.58,0,0,0-.11.46,9,9,0,0,0,1.93,4,5.79,5.79,0,0,0,2.37,1.58.46.46,0,0,1,.31.34,8.32,8.32,0,0,0,.84,2.26.46.46,0,0,1,0,.5.46.46,0,0,1-.39.2H8a9,9,0,0,1-5.27-2.83A11.8,11.8,0,0,1,.09,8.89,3.58,3.58,0,0,1,.79,5.85Z"/><path class="cls-2" d="M4.83,12.28V12l.15.16a5.79,5.79,0,0,0,2.37,1.58.46.46,0,0,1,.31.34,8.32,8.32,0,0,0,.84,2.26.46.46,0,0,1,0,.5.46.46,0,0,1-.39.2H8a10.16,10.16,0,0,1-2.22-.65A9.45,9.45,0,0,1,4.83,12.28Z"/><path class="cls-2" d="M7.46,5V7.05A.47.47,0,0,1,7,7.53H4.83v-3H7A.47.47,0,0,1,7.46,5Z"/><path class="cls-5" d="M24.84,2.76H7a.47.47,0,0,0-.48.48v9a9.41,9.41,0,1,0,18.81,0v-9A.47.47,0,0,0,24.84,2.76Z"/><path class="cls-6" d="M24.84,2.76H22.62a.47.47,0,0,1,.47.48v9a9.41,9.41,0,0,1-8.29,9.34,8.32,8.32,0,0,0,1.12.07,9.42,9.42,0,0,0,9.4-9.41v-9A.47.47,0,0,0,24.84,2.76Z"/><path class="cls-7" d="M20.06,11.17a1.05,1.05,0,0,0,.27-1.08,1,1,0,0,0-.85-.71l-1.76-.26a.08.08,0,0,1-.07,0l-.79-1.6a1,1,0,0,0-.94-.58h0a1,1,0,0,0-.95.58l-.79,1.6a.08.08,0,0,1-.07,0l-1.76.26a1,1,0,0,0-.85.71,1.05,1.05,0,0,0,.27,1.08L13,12.41a.1.1,0,0,1,0,.09l-.3,1.75a1.05,1.05,0,0,0,1.53,1.11l1.57-.83H16l1.57.83a1.11,1.11,0,0,0,.49.12,1.07,1.07,0,0,0,.62-.2,1,1,0,0,0,.42-1l-.3-1.75a.14.14,0,0,1,0-.09Z"/><path class="cls-8" d="M19.48,9.38,19,9.31,16.74,11.5a.57.57,0,0,0-.17.51l.52,3.11.44.24a1.11,1.11,0,0,0,.49.12,1.07,1.07,0,0,0,.62-.2,1,1,0,0,0,.42-1l-.3-1.75a.14.14,0,0,1,0-.09l1.27-1.24a1.05,1.05,0,0,0,.27-1.08A1,1,0,0,0,19.48,9.38Z"/><path class="cls-5" d="M25.12,31.89A5.14,5.14,0,0,0,20.43,28h-9a5.14,5.14,0,0,0-4.69,3.88.47.47,0,0,0,.07.43.49.49,0,0,0,.39.2h17.5a.48.48,0,0,0,.38-.2A.47.47,0,0,0,25.12,31.89Z"/><path class="cls-6" d="M25.12,31.89A5.14,5.14,0,0,0,20.43,28H16.77a5.14,5.14,0,0,1,4.69,3.88.5.5,0,0,1-.07.43.49.49,0,0,1-.39.2h3.67a.48.48,0,0,0,.38-.2A.47.47,0,0,0,25.12,31.89Z"/><path class="cls-1" d="M25.62,0H6.21a1.86,1.86,0,0,0,0,3.71H25.62a1.86,1.86,0,0,0,0-3.71Z"/><path class="cls-2" d="M25.62,0H23.46a1.86,1.86,0,1,1,0,3.71h2.16a1.86,1.86,0,0,0,0-3.71Z"/></g></g></svg>
|
|
||||||
|
Before Width: | Height: | Size: 3.6 KiB |
File diff suppressed because one or more lines are too long
Binary file not shown.
|
Before Width: | Height: | Size: 73 KiB |
-1
@@ -1 +0,0 @@
|
|||||||
var configuration_map = {"notificationRuleList":[],"config":{"enableNotification":true},"passKey":"{}"};
|
|
||||||
-2
File diff suppressed because one or more lines are too long
@@ -1,2 +0,0 @@
|
|||||||
!function(){"use strict";"undefined"!=typeof PushSubscriptionOptions&&PushSubscriptionOptions.prototype.hasOwnProperty("applicationServerKey")||void 0!==window.safari&&void 0!==window.safari.pushNotification?function(){const n=document.createElement("script");n.src="https://cdn.onesignal.com/sdks/web/v16/OneSignalSDK.page.es6.js?v=160510",n.defer=!0,document.head.appendChild(n)}():function(){let n="Incompatible browser.";"Apple Computer, Inc."===navigator.vendor&&navigator.maxTouchPoints>0&&(n+=" Try these steps: https://tinyurl.com/bdh2j9f7"),console.info(n)}()}();
|
|
||||||
//# sourceMappingURL=OneSignalSDK.page.js.map
|
|
||||||
@@ -1 +0,0 @@
|
|||||||
window.google_ad_status = 1;
|
|
||||||
@@ -1,220 +0,0 @@
|
|||||||
|
|
||||||
var mult_send = 0;
|
|
||||||
|
|
||||||
function Contest_Send(){
|
|
||||||
|
|
||||||
mult_send = mult_send + 1;
|
|
||||||
PreparaContestSend('senduscontestform',false);
|
|
||||||
|
|
||||||
if (mult_send == 1)
|
|
||||||
{
|
|
||||||
AJAXReqContestSend("POST","send_us_contest.php",true);
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
function PreparaContestSend(nome,ele){
|
|
||||||
stringa = "";
|
|
||||||
var form = document.forms[nome];
|
|
||||||
|
|
||||||
var numeroElementi = form.elements.length;
|
|
||||||
|
|
||||||
for(var i = 0; i < numeroElementi; i++){
|
|
||||||
|
|
||||||
nmfrm = form.elements[i].name;
|
|
||||||
|
|
||||||
if(i < numeroElementi-1)
|
|
||||||
{
|
|
||||||
stringa += form.elements[i].name+"="+encodeURIComponent(form.elements[i].value)+"&";
|
|
||||||
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
|
|
||||||
stringa += form.elements[i].name+"="+encodeURIComponent(form.elements[i].value);
|
|
||||||
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
function AJAXReqContestSend(method,url,bool){
|
|
||||||
if(window.XMLHttpRequest){
|
|
||||||
myReq = new XMLHttpRequest();
|
|
||||||
} else
|
|
||||||
|
|
||||||
if(window.ActiveXObject){
|
|
||||||
myReq = new ActiveXObject("Microsoft.XMLHTTP");
|
|
||||||
|
|
||||||
if(!myReq){
|
|
||||||
myReq = new ActiveXObject("Msxml2.XMLHTTP");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if(myReq){
|
|
||||||
|
|
||||||
myReq.onreadystatechange = state_ContestSend;
|
|
||||||
|
|
||||||
myReq.open(method,url,bool);
|
|
||||||
|
|
||||||
|
|
||||||
myReq.setRequestHeader("Content-Type","application/x-www-form-urlencoded; charset=UTF-8");
|
|
||||||
myReq.send(stringa);
|
|
||||||
|
|
||||||
}else{
|
|
||||||
alert("Impossibilitati ad usare AJAX");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
function state_ContestSend(bReload){
|
|
||||||
|
|
||||||
if (myReq.readyState==4){
|
|
||||||
|
|
||||||
mult_send = 0;
|
|
||||||
|
|
||||||
if (myReq.status==200){
|
|
||||||
|
|
||||||
ResponseContestSend(myReq.responseText);
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
if (bDebug) {alert("Problem retrieving XML data");}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
function ResponseContestSend(sResponse){
|
|
||||||
|
|
||||||
|
|
||||||
var vetResp = sResponse.split('|');
|
|
||||||
|
|
||||||
if (vetResp[0].toUpperCase() == 'OK')
|
|
||||||
{
|
|
||||||
if (MM_findObj("contest_name_msg"))
|
|
||||||
{
|
|
||||||
DisplayHTMLData(MM_findObj('contest_name_msg'), ' ');
|
|
||||||
}
|
|
||||||
if (MM_findObj("contest_video_msg"))
|
|
||||||
{
|
|
||||||
DisplayHTMLData(MM_findObj('contest_video_msg'), ' ');
|
|
||||||
}
|
|
||||||
if (MM_findObj("send_box_contest"))
|
|
||||||
{
|
|
||||||
MM_findObj("send_box_contest").style.display='none';
|
|
||||||
}
|
|
||||||
if (MM_findObj("send_box_contest_response"))
|
|
||||||
{
|
|
||||||
MM_findObj("send_box_contest_response").style.display='block';
|
|
||||||
}
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
|
|
||||||
if (MM_findObj("contest_name_msg"))
|
|
||||||
{
|
|
||||||
DisplayHTMLData(MM_findObj('contest_name_msg'), ' ');
|
|
||||||
}
|
|
||||||
if (MM_findObj("contest_video_msg"))
|
|
||||||
{
|
|
||||||
DisplayHTMLData(MM_findObj('contest_video_msg'), ' ');
|
|
||||||
}
|
|
||||||
for (b=1; b<vetResp.length; b++)
|
|
||||||
{
|
|
||||||
var f = vetResp[b].split(';');
|
|
||||||
var fldcont = f[0];
|
|
||||||
var msgcont = f[1];
|
|
||||||
|
|
||||||
if (fldcont == 'contest_name')
|
|
||||||
{
|
|
||||||
DisplayHTMLData(MM_findObj(fldcont + '_msg'), msgcont);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (fldcont == 'contest_video')
|
|
||||||
{
|
|
||||||
DisplayHTMLData(MM_findObj(fldcont + '_msg'), msgcont);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
var HTTP_FIRSTAUCT_URL = new String ('first_auct.php');
|
|
||||||
var xmlhttpFirstAuct = null;
|
|
||||||
|
|
||||||
function FirstAuct() {
|
|
||||||
|
|
||||||
var sUrlFirstAuct = HTTP_FIRSTAUCT_URL + "?chk=" + new Date().valueOf();
|
|
||||||
|
|
||||||
if (xmlhttpFirstAuct) {
|
|
||||||
|
|
||||||
if ((xmlhttpFirstAuct.readyState != 4) && (xmlhttpFirstAuct.readyState != 0)) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
try
|
|
||||||
{
|
|
||||||
if (window.XMLHttpRequest){
|
|
||||||
|
|
||||||
xmlhttpFirstAuct = new XMLHttpRequest();
|
|
||||||
} else if (window.ActiveXObject){
|
|
||||||
|
|
||||||
xmlhttpFirstAuct = new ActiveXObject("Microsoft.XMLHTTP");
|
|
||||||
}
|
|
||||||
if (xmlhttpFirstAuct != null){
|
|
||||||
xmlhttpFirstAuct.onreadystatechange = state_FirstAuct;
|
|
||||||
xmlhttpFirstAuct.open("GET",sUrlFirstAuct,true);
|
|
||||||
xmlhttpFirstAuct.send(null);
|
|
||||||
return true;
|
|
||||||
} else {
|
|
||||||
if (bDebug)
|
|
||||||
{
|
|
||||||
alert("Your browser does not support XMLHTTP.");
|
|
||||||
}
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
catch (e) {
|
|
||||||
xmlhttpFirstAuct = null;
|
|
||||||
if (bDebug) alert('Errore in loadXMLDocElenco');
|
|
||||||
}
|
|
||||||
finally {}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
function state_FirstAuct(){
|
|
||||||
|
|
||||||
if (xmlhttpFirstAuct.readyState==4){
|
|
||||||
|
|
||||||
if (xmlhttpFirstAuct.status!=200){
|
|
||||||
|
|
||||||
if (bDebug) {
|
|
||||||
alert("Problem retrieving XML data");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
File diff suppressed because one or more lines are too long
@@ -1,129 +0,0 @@
|
|||||||
btn-promo {
|
|
||||||
border-radius: 3px;
|
|
||||||
background: linear-gradient(rgb(82, 157, 253), rgb(46, 114, 202));
|
|
||||||
background: -moz-linear-gradient(rgb(82, 157, 253), rgb(46, 114, 202));
|
|
||||||
background: -webkit-linear-gradient(rgb(82, 157, 253), rgb(46, 114, 202));
|
|
||||||
background: -o-linear-gradient(rgb(82, 157, 253), rgb(46, 114, 202));
|
|
||||||
background: -ms-linear-gradient(rgb(82, 157, 253), rgb(46, 114, 202));
|
|
||||||
color: #fff;
|
|
||||||
}
|
|
||||||
|
|
||||||
.btn.btn-promo:hover {
|
|
||||||
background: linear-gradient(rgb(117, 175, 250), rgb(53, 124, 216));
|
|
||||||
background: -moz-linear-gradient(rgb(117, 175, 250), rgb(53, 124, 216));
|
|
||||||
background: -webkit-linear-gradient(rgb(117, 175, 250), rgb(53, 124, 216));
|
|
||||||
background: -o-linear-gradient(rgb(117, 175, 250), rgb(53, 124, 216));
|
|
||||||
background: -ms-linear-gradient(rgb(117, 175, 250), rgb(53, 124, 216));
|
|
||||||
color: #fff;
|
|
||||||
}
|
|
||||||
|
|
||||||
.mCSB_inside > .mCSB_container {
|
|
||||||
margin-right: 0px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.mCSB_scrollTools .mCSB_draggerRail {
|
|
||||||
width: 6px;
|
|
||||||
background-color: #e2e2e2;
|
|
||||||
}
|
|
||||||
|
|
||||||
.mCSB_scrollTools .mCSB_draggerContainer {
|
|
||||||
left: 10px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.mCSB_scrollTools .mCSB_dragger .mCSB_dragger_bar {
|
|
||||||
background-color: #20cb9a !important;
|
|
||||||
width: 100%;
|
|
||||||
}
|
|
||||||
|
|
||||||
.loader {
|
|
||||||
margin: 10px auto;
|
|
||||||
border: 5px solid #f3f3f3;
|
|
||||||
/* Light grey */
|
|
||||||
border-top: 5px solid #20cb9a;
|
|
||||||
/* Blue */
|
|
||||||
border-radius: 50%;
|
|
||||||
width: 20px;
|
|
||||||
height: 20px;
|
|
||||||
-webkit-animation: spin 2s linear infinite;
|
|
||||||
animation: spin 2s linear infinite;
|
|
||||||
}
|
|
||||||
|
|
||||||
.stopScroll{
|
|
||||||
overflow: hidden !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
@-webkit-keyframes spin {
|
|
||||||
0% {
|
|
||||||
-webkit-transform: rotate(0deg);
|
|
||||||
}
|
|
||||||
|
|
||||||
100% {
|
|
||||||
-webkit-transform: rotate(360deg);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@keyframes spin {
|
|
||||||
0% {
|
|
||||||
transform: rotate(0deg);
|
|
||||||
}
|
|
||||||
|
|
||||||
100% {
|
|
||||||
transform: rotate(360deg);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@media screen and (max-width: 384px) {
|
|
||||||
#prod_win_cont_modal h3 {
|
|
||||||
padding-left: 30px;
|
|
||||||
padding-right: 30px;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@media screen and (max-width: 320px) {
|
|
||||||
.prod_won__2 {
|
|
||||||
margin-left: 1px !important;
|
|
||||||
margin-right: 1px !important;
|
|
||||||
}
|
|
||||||
#prod_win_cont_modal .col-xs-6{
|
|
||||||
padding-left: 5px;
|
|
||||||
padding-right: 5px;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#modal iframe {
|
|
||||||
width: 99%;
|
|
||||||
}
|
|
||||||
|
|
||||||
#myModal3 .modal-dialog, #myModal2 .modal-dialog {
|
|
||||||
margin: 30px auto;
|
|
||||||
}
|
|
||||||
|
|
||||||
.settingBox form div {
|
|
||||||
border-bottom: 1px solid #efefef;
|
|
||||||
padding: 15px;
|
|
||||||
font-weight: 500;
|
|
||||||
font-size: 12px;
|
|
||||||
color: #818181;
|
|
||||||
}
|
|
||||||
|
|
||||||
@media screen and (max-width: 991px) {
|
|
||||||
#menuModal .show {
|
|
||||||
display: block;
|
|
||||||
}
|
|
||||||
|
|
||||||
#menuModal .modal-header {
|
|
||||||
height: auto;
|
|
||||||
}
|
|
||||||
|
|
||||||
#menuModal .height {
|
|
||||||
height: 0px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.parentOverflowY {
|
|
||||||
overflow-y: hidden;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#notifBoxContainer .mCSB_container {
|
|
||||||
top: 0px;
|
|
||||||
}
|
|
||||||
File diff suppressed because it is too large
Load Diff
Binary file not shown.
|
Before Width: | Height: | Size: 2.6 KiB |
@@ -1,206 +0,0 @@
|
|||||||
const AuctionsBidManage = (function() {
|
|
||||||
|
|
||||||
const _defaultPart = "divAsta";
|
|
||||||
let _nAstePerBonus = 10;
|
|
||||||
let _limiteAsteVinte = 10;
|
|
||||||
let _nAstePuntataVinte = 0;
|
|
||||||
let _percentualeBonus = 0;
|
|
||||||
let _nPuntateVinteOggi = 0;
|
|
||||||
let _nAsteConfermate = 0;
|
|
||||||
let _nPuntateRiscattate = 0;
|
|
||||||
let _nPuntateDaRiscattare = 0;
|
|
||||||
let _initialized = false;
|
|
||||||
let _defaultValidUntil = null;
|
|
||||||
let _viewSlot = false;
|
|
||||||
|
|
||||||
function _defaultObj() {
|
|
||||||
return {
|
|
||||||
auctions: {},
|
|
||||||
nAsteConfermate: 0,
|
|
||||||
percentualeBonus: 0,
|
|
||||||
limiteAsteVinte: 10,
|
|
||||||
nAstePerBonus: 10,
|
|
||||||
validUntil: getDefaultValidUntil()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Aggiunge una nuova asta all'oggetto delle aste di puntata vinte in un giorno
|
|
||||||
* @param idAuction {int}
|
|
||||||
*/
|
|
||||||
function add(idAuction){
|
|
||||||
|
|
||||||
let result = get();
|
|
||||||
let retrievedObject = JSON.parse(result);
|
|
||||||
let auctionBidWin = retrievedObject === null ? _defaultObj() : retrievedObject ;
|
|
||||||
let auctionElement = document.getElementById(_defaultPart + idAuction);
|
|
||||||
let creditValue = parseInt(auctionElement.getAttribute('data-credit-value')) > 0 ? parseInt(auctionElement.getAttribute('data-credit-value')) : 0;
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
if(creditValue > 0 && Object.keys(auctionBidWin.auctions).length <= auctionBidWin.limiteAsteVinte){
|
|
||||||
|
|
||||||
// let obj =
|
|
||||||
// {
|
|
||||||
// idAuction: idAuction,
|
|
||||||
// value: creditValue
|
|
||||||
// }
|
|
||||||
// ;
|
|
||||||
//
|
|
||||||
// if(!auctionBidWin.auctions.hasOwnProperty(idAuction)){
|
|
||||||
// auctionBidWin.auctions[idAuction] = obj;
|
|
||||||
// }
|
|
||||||
// let newObj = JSON.stringify(auctionBidWin);
|
|
||||||
//
|
|
||||||
// localStorage.setItem("auctionBidWin", newObj);
|
|
||||||
getRemoteData();
|
|
||||||
}
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
function getDefaultValidUntil(){
|
|
||||||
|
|
||||||
return _defaultValidUntil;
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
function setDefaultValidUntil(untilTimestamp){
|
|
||||||
_defaultValidUntil = untilTimestamp;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Ritornano le informazioni salvate nel localStorage
|
|
||||||
* @returns {string}
|
|
||||||
*/
|
|
||||||
function get(){
|
|
||||||
return localStorage.getItem('auctionBidWin');
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Rimuove dal localStorage
|
|
||||||
*/
|
|
||||||
function remove(){
|
|
||||||
localStorage.removeItem('auctionBidWin');
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
function getRemoteData(callback = null){
|
|
||||||
|
|
||||||
fetch('./ajax/get_auction_bids_info_banner.php',{
|
|
||||||
method: "GET"
|
|
||||||
})
|
|
||||||
// gestisci il successo
|
|
||||||
.then(response => response.json()) // converti a json
|
|
||||||
.then(function (data) {
|
|
||||||
let obj = {
|
|
||||||
auctions: data.auctions,
|
|
||||||
nAsteConfermate: data.nAsteConfermate,
|
|
||||||
nAsteVinte: data.nAsteVinte,
|
|
||||||
nPuntateRiscattate: data.nPuntateRiscattate,
|
|
||||||
nPuntateDaRiscattare: data.nPuntateDaRiscattare,
|
|
||||||
limiteAsteVinte: data.limiteAsteVinte,
|
|
||||||
nAstePerBonus: data.nAstePerBonus,
|
|
||||||
percentualeBonus: data.percentualeBonus,
|
|
||||||
validUntil: data.validUntil,
|
|
||||||
viewSlot: data.viewSlot,
|
|
||||||
extraSlots: data.extraSlots, //un elenco degli slots non scaduti e non aperti
|
|
||||||
nPuntateBonus: data.nPuntateBonus
|
|
||||||
};
|
|
||||||
let newObj = JSON.stringify(obj);
|
|
||||||
|
|
||||||
localStorage.setItem("auctionBidWin", newObj);
|
|
||||||
if(callback !== null){
|
|
||||||
callback();
|
|
||||||
}
|
|
||||||
|
|
||||||
})
|
|
||||||
.catch(err => console.log('Request Failed', err)); // gestisci gli errori
|
|
||||||
}
|
|
||||||
|
|
||||||
function retriveInfoComponent(){
|
|
||||||
let result = JSON.parse(get());
|
|
||||||
|
|
||||||
if(result) {
|
|
||||||
|
|
||||||
_nAstePuntataVinte = result.nAsteVinte;
|
|
||||||
_limiteAsteVinte = result.limiteAsteVinte;
|
|
||||||
_nAsteConfermate = result.nAsteConfermate;
|
|
||||||
_nPuntateDaRiscattare = result.nPuntateDaRiscattare;
|
|
||||||
_nPuntateRiscattate = result.nPuntateRiscattate;
|
|
||||||
_nAstePerBonus = result.nAstePerBonus;
|
|
||||||
_percentualeBonus = result.percentualeBonus;
|
|
||||||
_viewSlot = result.viewSlot;
|
|
||||||
_nPuntateVinteOggi = result.nPuntateDaRiscattare + result.nPuntateRiscattate;
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
/*if (_percentualeBonus > 0) {
|
|
||||||
let valorePercentualeBonus = ((_nPuntateVinteOggi * _percentualeBonus) / 100);
|
|
||||||
_nPuntateVinteOggi = _nPuntateVinteOggi + valorePercentualeBonus;
|
|
||||||
}*/
|
|
||||||
|
|
||||||
|
|
||||||
let asteRimanentiPerBonus = _nAstePerBonus - _nAstePuntataVinte;
|
|
||||||
|
|
||||||
return {
|
|
||||||
auctions: result.auctions,
|
|
||||||
asteRimanentiPerBonus: asteRimanentiPerBonus,
|
|
||||||
nAstePerBonus: _nAstePerBonus,
|
|
||||||
nAstePuntataVinte: _nAstePuntataVinte,
|
|
||||||
percentualeBonus: _percentualeBonus,
|
|
||||||
nPuntateVinteOggi: parseInt(_nPuntateVinteOggi),
|
|
||||||
limiteAsteVinte: _limiteAsteVinte,
|
|
||||||
nAsteConfermate: _nAsteConfermate,
|
|
||||||
nPuntateDaRiscattare: _nPuntateDaRiscattare,
|
|
||||||
nPuntateRiscattate: _nPuntateRiscattate,
|
|
||||||
|
|
||||||
viewSlot: _viewSlot,
|
|
||||||
extraSlots: result.extraSlots,
|
|
||||||
nPuntateBonus: result.nPuntateBonus
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
*
|
|
||||||
* @param auctionId
|
|
||||||
* @returns {boolean}
|
|
||||||
*/
|
|
||||||
function searchByAuctionId(auctionId){
|
|
||||||
let data = retriveInfoComponent();
|
|
||||||
let controllo = false;
|
|
||||||
if(data == undefined || data == null ){ return; }
|
|
||||||
|
|
||||||
let keys = Object.keys(data.auctions);
|
|
||||||
|
|
||||||
for(let i= 0; i <= keys.length; i++){
|
|
||||||
if(keys[i] === auctionId){
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
function initRemoteData(){
|
|
||||||
if(!_initialized){
|
|
||||||
getRemoteData();
|
|
||||||
setInterval(function (){
|
|
||||||
getRemoteData();
|
|
||||||
}, 1000 * 60);
|
|
||||||
}
|
|
||||||
_initialized = true;
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
|
||||||
add,
|
|
||||||
get,
|
|
||||||
remove,
|
|
||||||
getRemoteData,
|
|
||||||
retriveInfoComponent,
|
|
||||||
initRemoteData,
|
|
||||||
setDefaultValidUntil,
|
|
||||||
searchByAuctionId
|
|
||||||
}
|
|
||||||
})();
|
|
||||||
|
|
||||||
|
|
||||||
@@ -1,927 +0,0 @@
|
|||||||
#wrapBonusSection{
|
|
||||||
width: 100%;
|
|
||||||
background-color: #fff;
|
|
||||||
margin-bottom: 20px;
|
|
||||||
-webkit-box-shadow: 0px 2px 5px 0px rgba(0,0,0,0.2);
|
|
||||||
-moz-box-shadow: 0px 2px 5px 0px rgba(0,0,0,0.2);
|
|
||||||
box-shadow: 0px 2px 5px 0px rgba(0,0,0,0.2);
|
|
||||||
border-top: 1px solid #d0d0d0;
|
|
||||||
display: none;
|
|
||||||
cursor: pointer;
|
|
||||||
}
|
|
||||||
#wrapBonusSection.visible{
|
|
||||||
display: block;
|
|
||||||
}
|
|
||||||
#BonusSection{
|
|
||||||
display: flex;
|
|
||||||
justify-content: space-between;
|
|
||||||
width: 65%;
|
|
||||||
min-height: 40px;
|
|
||||||
margin: 0 auto;
|
|
||||||
align-items: center;
|
|
||||||
margin-bottom: -25px;
|
|
||||||
}
|
|
||||||
@media (max-width: 576px) {
|
|
||||||
#BonusSection{
|
|
||||||
margin-bottom: 0;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
#BonusSection .wrap-msg-bonus, #BonusSection .pt-2 .wrap-progress, #BonusSection .pt-1 .wrap-progress, #BonusSection .pt-3 .wrap-progress, #BonusSection .pt-3 .wrap-bids, #auctionBidsModal .wrap-msg-bonus, #auctionBidsModal .wrap-msg-bonus .wrap-progress, #bidsBonusSection #section2 .wrap-progress, #bidsBonusSection #section3 .wrap-progress{
|
|
||||||
display: flex;
|
|
||||||
justify-content: center;
|
|
||||||
align-items: center;
|
|
||||||
}
|
|
||||||
#bidsBonusSection #section2 .wrap-progress{
|
|
||||||
font-size: 14px;
|
|
||||||
}
|
|
||||||
#auctionBidsModal #countdownAddSlot{
|
|
||||||
font-weight: bold;
|
|
||||||
color: #55bc62;
|
|
||||||
}
|
|
||||||
#auctionBidsModal .wrap-msg-bonus{
|
|
||||||
margin-bottom: 25px;
|
|
||||||
}
|
|
||||||
#BonusSection .pt-2 .item, #BonusSection .pt-3 .item{
|
|
||||||
margin: 2px;
|
|
||||||
}
|
|
||||||
#BonusSection .pt-2, #BonusSection .pt-1, #BonusSection .pt-3, #auctionBidsModal .wrap-pt2{
|
|
||||||
display: flex;
|
|
||||||
justify-content: center;
|
|
||||||
align-items: center;
|
|
||||||
}
|
|
||||||
#auctionBidsModal .wrap-pt2 .bonus-obtained{
|
|
||||||
font-size: 12px;
|
|
||||||
color: #6F6F6F;
|
|
||||||
font-weight: bold;
|
|
||||||
margin-top: 3px;
|
|
||||||
display: none;
|
|
||||||
}
|
|
||||||
#auctionBidsModal .wrap-pt2{
|
|
||||||
justify-content: space-around;
|
|
||||||
align-items: flex-start;
|
|
||||||
margin-top: 20px;
|
|
||||||
border-top: 1px solid #DBDBDB;
|
|
||||||
padding-top: 20px;
|
|
||||||
font-size: 13px;
|
|
||||||
margin-bottom: 10px;
|
|
||||||
}
|
|
||||||
@media (max-width: 340px) {
|
|
||||||
#auctionBidsModal .wrap-pt2{
|
|
||||||
font-size: 12px;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#auctionToBonusModal{
|
|
||||||
font-size: 15px;
|
|
||||||
color: #333;
|
|
||||||
}
|
|
||||||
#BonusSection .text{
|
|
||||||
color: #6D6D6D;
|
|
||||||
font-size: 15px;
|
|
||||||
margin: auto 4px;
|
|
||||||
}
|
|
||||||
#BonusSection .pt-2 img{
|
|
||||||
height: 29px;
|
|
||||||
}
|
|
||||||
#BonusSection .pt-2 .img-emoji img, #auctionBidsModal .img-emoji img {
|
|
||||||
height: 18px;
|
|
||||||
width: 18px;
|
|
||||||
margin-left: -10px;
|
|
||||||
margin-top: -3px;
|
|
||||||
}
|
|
||||||
#auctionBidsModal .img-emoji img{
|
|
||||||
width: 24px;
|
|
||||||
height: 24px;
|
|
||||||
}
|
|
||||||
#auctionBidsModal .img-emoji{
|
|
||||||
z-index: 9;
|
|
||||||
margin-left: -3px;
|
|
||||||
margin-right: 2px;
|
|
||||||
}
|
|
||||||
#BonusSection .pt-dx img, #auctionBidsModal .pt-dx img, #auctionBidsModal .pt-center img{
|
|
||||||
width: 16px;
|
|
||||||
}
|
|
||||||
#BonusSection .pt-2 .wrap-progress .progress{
|
|
||||||
margin-left: 5px;
|
|
||||||
background-color: #D1D1D1;
|
|
||||||
}
|
|
||||||
#auctionBidsModal .progress{
|
|
||||||
width: 56px;
|
|
||||||
height: 24px;
|
|
||||||
margin: 0 6px;
|
|
||||||
position: relative;
|
|
||||||
background-color: #D1D1D1;
|
|
||||||
border-radius: 20px;
|
|
||||||
}
|
|
||||||
#auctionBidsModal .progress{
|
|
||||||
width: 110px;
|
|
||||||
height: 22px;
|
|
||||||
}
|
|
||||||
#auctionBidsModal .pt-1 .progress{
|
|
||||||
height: 11px;
|
|
||||||
}
|
|
||||||
#BonusSection .progress, #auctionBidsModal .progress{
|
|
||||||
width: 60px;
|
|
||||||
height: 13px;
|
|
||||||
margin-bottom: 0px;
|
|
||||||
}
|
|
||||||
#auctionBidsModal .progress{
|
|
||||||
width: 90px;
|
|
||||||
}
|
|
||||||
#BonusSection #countdown-bonus{
|
|
||||||
width: 60px;
|
|
||||||
color: #fff;
|
|
||||||
background-color: #FF0658;
|
|
||||||
font-weight: bold;
|
|
||||||
font-size: 11px;
|
|
||||||
padding: 0px 4px;
|
|
||||||
height: 16px;
|
|
||||||
border-radius: 5px;
|
|
||||||
}
|
|
||||||
#BonusSection #bonus-earned, #BonusSection #bonus-active-all{
|
|
||||||
display: none;
|
|
||||||
font-weight: bold;
|
|
||||||
margin-right: 15px;
|
|
||||||
}
|
|
||||||
@media (max-width: 576px) {
|
|
||||||
#BonusSection #bonus-earned, #BonusSection #bonus-active-all{
|
|
||||||
font-size: 12px;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
#BonusSection #bonus-active-all{
|
|
||||||
color: #55bc62;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
#BonusSection .progress .progress-bar, #auctionBidsModal .pt-1 .progress .progress-bar, #auctionBidsModal #bidsBonusSection #section2 .progress .progress-bar, #auctionBidsModal .progress .progress-bar{
|
|
||||||
background: rgb(4,170,176);
|
|
||||||
background: linear-gradient(90deg, rgba(4,170,176,1) 0%, rgba(7,206,173,1) 100%);
|
|
||||||
box-shadow: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
#BonusSection .progress .progressbar-text{
|
|
||||||
position: absolute;
|
|
||||||
top: 2px;
|
|
||||||
height: 22px;
|
|
||||||
color: #000;
|
|
||||||
width: 100%;
|
|
||||||
left: 0;
|
|
||||||
margin: 0;
|
|
||||||
font-size: 16px;
|
|
||||||
font-weight: bold;
|
|
||||||
}
|
|
||||||
#auctionBidsModal .progress .progressbar-text{
|
|
||||||
font-size: 15px;
|
|
||||||
top: 0;
|
|
||||||
}
|
|
||||||
#todayBids, #auctionBidsModal{
|
|
||||||
font-size: 16px;
|
|
||||||
color: #333;
|
|
||||||
}
|
|
||||||
#auctionBidsModal{
|
|
||||||
font-weight: normal;
|
|
||||||
}
|
|
||||||
#auctionToBonus.active{
|
|
||||||
font-weight: bold;
|
|
||||||
color: #000;
|
|
||||||
}
|
|
||||||
#confirmedAuctionWithBonus{
|
|
||||||
display: none;
|
|
||||||
margin-left: 5px;
|
|
||||||
}
|
|
||||||
#confirmedAuctionWithBonus .value{
|
|
||||||
font-weight: bold;
|
|
||||||
}
|
|
||||||
.wrap-bonus-mobile{
|
|
||||||
display: flex;
|
|
||||||
justify-content: center;
|
|
||||||
align-items: center;
|
|
||||||
}
|
|
||||||
.wrap-bonus-mobile .wrap-bonus-value{
|
|
||||||
font-size: 14px;
|
|
||||||
color: #504E4E;
|
|
||||||
margin-left: 5px;
|
|
||||||
}
|
|
||||||
#auctionBidsModal .wrap-bonus-value{
|
|
||||||
margin-right: 2px;
|
|
||||||
}
|
|
||||||
#BonusSection .pt-3 img{
|
|
||||||
width: 15px;
|
|
||||||
}
|
|
||||||
#BonusSection .wrap-title-bonus{
|
|
||||||
display: flex;
|
|
||||||
}
|
|
||||||
#BonusSection .wrap-title-bonus .icon-check{
|
|
||||||
width: 15px;
|
|
||||||
display: none;
|
|
||||||
margin-right: 4px;
|
|
||||||
}
|
|
||||||
#BonusSection .pt-2 #countdown-bonus{
|
|
||||||
display: none;
|
|
||||||
}
|
|
||||||
@media (max-width: 1040px) {
|
|
||||||
#BonusSection{
|
|
||||||
width: 100%;
|
|
||||||
justify-content: space-around;
|
|
||||||
min-height: 50px;
|
|
||||||
}
|
|
||||||
#BonusSection .pt-1, #BonusSection .pt-2, #BonusSection .pt-3{
|
|
||||||
flex-direction: column;
|
|
||||||
}
|
|
||||||
#BonusSection .pt-3 .wrap-bids{
|
|
||||||
display: flex;
|
|
||||||
justify-content: center;
|
|
||||||
align-items: center;
|
|
||||||
}
|
|
||||||
#BonusSection .text {
|
|
||||||
font-size: 12px;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
#BonusSection .pt-2 img{
|
|
||||||
margin-right: 5px;
|
|
||||||
height: 18px;
|
|
||||||
width: 10px;
|
|
||||||
}
|
|
||||||
#BonusSection .pt-1 .progress {
|
|
||||||
width: 90px;
|
|
||||||
height: 10px;
|
|
||||||
}
|
|
||||||
#todayBids{
|
|
||||||
font-size: 14px;
|
|
||||||
}
|
|
||||||
#BonusSection .pt-3 .item{
|
|
||||||
margin: 0;
|
|
||||||
}
|
|
||||||
#BonusSection .pt-3 img{
|
|
||||||
width: 13px;
|
|
||||||
margin-top: -1px;
|
|
||||||
margin-left: 3px;
|
|
||||||
}
|
|
||||||
#BonusSection .wrap-msg-bonus{
|
|
||||||
flex-direction: column;
|
|
||||||
justify-content: flex-start;
|
|
||||||
align-items: baseline;
|
|
||||||
}
|
|
||||||
|
|
||||||
.wrap-bonus-mobile{
|
|
||||||
display: flex;
|
|
||||||
justify-content: center;
|
|
||||||
align-items: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
@media (max-width: 576px) {
|
|
||||||
#BonusSection {
|
|
||||||
min-height: 45px;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@media (max-width: 360px) {
|
|
||||||
#BonusSection .text {
|
|
||||||
font-size: 11px;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
#auctionBidsModal .pt-left .wrap-progress, #auctionBidsModal .pt-dx .wrap-bids, #auctionBidsModal .pt-center .wrap-bids{
|
|
||||||
display: flex;
|
|
||||||
justify-content: center;
|
|
||||||
align-items: center;
|
|
||||||
margin-top: 5px;
|
|
||||||
font-weight: bold;
|
|
||||||
}
|
|
||||||
#auctionBidsModal .pt-center .item, #auctionBidsModal .pt-dx .item{
|
|
||||||
margin-left: 1px;
|
|
||||||
margin-right: 1px;
|
|
||||||
}
|
|
||||||
#auctionBidsModal .pt-center .item img, #auctionBidsModal .pt-dx .item img{
|
|
||||||
margin-left: 2px;
|
|
||||||
margin-right: 2px;
|
|
||||||
margin-top: -5px;
|
|
||||||
}
|
|
||||||
|
|
||||||
#auctionToGoModal{
|
|
||||||
font-size: 14px;
|
|
||||||
font-weight: bold;
|
|
||||||
}
|
|
||||||
@media (max-width: 340px) {
|
|
||||||
#auctionBidsModal .wrap-pt2{
|
|
||||||
font-size: 12px;
|
|
||||||
}
|
|
||||||
#auctionToGoModal, #todayBidsModal, #todayBidsPayedModal{
|
|
||||||
font-size: 13px;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#todayBidsModal, #todayBidsPayedModal{
|
|
||||||
font-size: 14px;
|
|
||||||
font-weight: bold;
|
|
||||||
}
|
|
||||||
#loaderAuctionBids{
|
|
||||||
min-height: 40px;
|
|
||||||
text-align: center;
|
|
||||||
padding: 5px;
|
|
||||||
font-size: 20px;
|
|
||||||
}
|
|
||||||
#auctionToGo{
|
|
||||||
color: #000;
|
|
||||||
margin-left: 5px;
|
|
||||||
}
|
|
||||||
.loader-data{
|
|
||||||
display: block;
|
|
||||||
position: relative;
|
|
||||||
margin-right: 0px !important;
|
|
||||||
margin-left: 0px !important;
|
|
||||||
}
|
|
||||||
.loader-data::before{
|
|
||||||
content: "";
|
|
||||||
background-color: #eaeaea;
|
|
||||||
display: block;
|
|
||||||
width: 100%;
|
|
||||||
height: 22px;
|
|
||||||
position: absolute;
|
|
||||||
}
|
|
||||||
@media (max-width: 576px) {
|
|
||||||
.loader-data{
|
|
||||||
margin-top: 2px !important;
|
|
||||||
margin-bottom: 2px !important;
|
|
||||||
}
|
|
||||||
.loader-data::before{
|
|
||||||
height: 16px;
|
|
||||||
min-width: 20px;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
#BonusSection .pt-1, #BonusSection .pt-2, #BonusSection .pt-3{
|
|
||||||
position: relative;
|
|
||||||
}
|
|
||||||
.loader-data img{
|
|
||||||
display: none !important;
|
|
||||||
}
|
|
||||||
.wrap-countdown-auctionBidsModal{
|
|
||||||
color: #55BC62;
|
|
||||||
font-weight: bold;
|
|
||||||
}
|
|
||||||
#auctionBidsModal .wrapTitle{
|
|
||||||
margin: 20px auto 10px;
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
}
|
|
||||||
#auctionBidsModal .modal-body{
|
|
||||||
padding: 0;
|
|
||||||
}
|
|
||||||
#auctionBidsModal .contentModal #bidsBonusSection .content, #auctionBidsModal .contentModal #rankingBonusSection{
|
|
||||||
font-size: 16px;
|
|
||||||
text-align: center;
|
|
||||||
margin-top: 15px;
|
|
||||||
padding: 15px;
|
|
||||||
}
|
|
||||||
#auctionBidsModal .contentModal #bidsBonusSection .content{
|
|
||||||
margin-top: 0;
|
|
||||||
padding: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
#auctionBidsModal .contentModal #bidsBonusSection .content.parent-content-div {
|
|
||||||
padding: 0 0 5px 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
#tabsSection{
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
@media (max-width: 576px) {
|
|
||||||
#tabsSection{
|
|
||||||
font-size: 14px;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
#tabsSection .pt-1{
|
|
||||||
width: 55%;
|
|
||||||
}
|
|
||||||
#tabsSection .pt-2{
|
|
||||||
width: 40%;
|
|
||||||
}
|
|
||||||
#tabsSection .pt-1, #tabsSection .pt-2{
|
|
||||||
|
|
||||||
padding: 8px 18px;
|
|
||||||
border-bottom: 1px solid #BCBCBC;
|
|
||||||
text-align: center;
|
|
||||||
font-size: 14px;
|
|
||||||
}
|
|
||||||
#tabsSection .pt-1 .fa, #tabsSection .pt-2 .fa{
|
|
||||||
margin-right: 5px;
|
|
||||||
}
|
|
||||||
#tabsSection .pt-3{
|
|
||||||
width: 10%;
|
|
||||||
padding: 5px 10px;
|
|
||||||
border-bottom: 1px solid #BCBCBC;
|
|
||||||
}
|
|
||||||
button[aria-label='Close'] span{
|
|
||||||
font-size: 26px;
|
|
||||||
}
|
|
||||||
@media (max-width: 576px) {
|
|
||||||
#tabsSection .pt-3{
|
|
||||||
padding: 4px 10px;
|
|
||||||
}
|
|
||||||
#tabsSection .pt-1, #tabsSection .pt-2{
|
|
||||||
font-size: 12px;
|
|
||||||
padding: 8px 10px;
|
|
||||||
}
|
|
||||||
button[aria-label='Close'] span{
|
|
||||||
font-size: 25px;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#tabsSection .pt-1.active, #tabsSection .pt-2.active, #tabsSection .pt-3.active{
|
|
||||||
border-color: #2F80ED;
|
|
||||||
}
|
|
||||||
#tabsSection .pt-1.active a, #tabsSection .pt-2.active a, #tabsSection .pt-3.active a, #tabsSection .pt-1.active a:hover, #tabsSection .pt-2.active a:hover{
|
|
||||||
color: #2F80ED;
|
|
||||||
font-weight: bold;
|
|
||||||
text-decoration: none;
|
|
||||||
}
|
|
||||||
#tabsSection .pt-1 a, #tabsSection .pt-2 a{
|
|
||||||
color: #7d7d7d;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
#tabsSection .pt-1 a:hover, #tabsSection .pt-2 a:hover{
|
|
||||||
text-decoration: none;
|
|
||||||
font-weight: normal;
|
|
||||||
color: #2F80ED;
|
|
||||||
}
|
|
||||||
#rankingBonusSection{
|
|
||||||
display: none;
|
|
||||||
}
|
|
||||||
#bidsBonusSection #section2 .box-congrats,
|
|
||||||
#bidsBonusSection #section3 .box-congrats{
|
|
||||||
margin-top: 12px;
|
|
||||||
}
|
|
||||||
#bidsBonusSection #section2, #bidsBonusSection #section3{
|
|
||||||
display: none;
|
|
||||||
}
|
|
||||||
#bidsBonusSection #section2 .titleModal{
|
|
||||||
font-size: 20px;
|
|
||||||
}
|
|
||||||
#auctionBidsModal .wrap-credit-bonus{
|
|
||||||
display: inline-block;
|
|
||||||
}
|
|
||||||
#auctionBidsModal #bidsBonusSection .wrap-content{
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
}
|
|
||||||
#bidsBonusSection #section2 .summary-body #countdown-bonus{
|
|
||||||
font-size: 14px;
|
|
||||||
}
|
|
||||||
#bidsBonusSection #section2 .summary-body .countdown{
|
|
||||||
color: #FF0658;
|
|
||||||
}
|
|
||||||
#bidsBonusSection #section2 .summary-body #countdownForBonus{
|
|
||||||
font-weight: bold;
|
|
||||||
}
|
|
||||||
#bidsBonusSection #section2 .summary{
|
|
||||||
margin-top: 30px;
|
|
||||||
}
|
|
||||||
#bidsBonusSection #section2 .summary-body{
|
|
||||||
width: 250px;
|
|
||||||
margin: -18px auto 25px;
|
|
||||||
border: 1px solid #000;
|
|
||||||
border-radius: 5px;
|
|
||||||
padding: 20px 15px;
|
|
||||||
box-shadow: 2px 2px 3px 1px rgba(208, 209, 213, 0.2), 0 2px 2px 1px rgba(220, 221, 224, 0.2);
|
|
||||||
-webkit-box-shadow: 2px 2px 3px 1px rgba(208, 209, 213, 0.2), 0 2px 2px 1px rgba(220, 221, 224, 0.2);
|
|
||||||
-moz-box-shadow: 2px 2px 3px 1px rgba(208, 209, 213, 0.2), 0 2px 2px 1px rgba(220, 221, 224, 0.2);
|
|
||||||
}
|
|
||||||
#bidsBonusSection #section3 .summaryTitle{
|
|
||||||
font-size: 20px;
|
|
||||||
font-weight: bold;
|
|
||||||
margin-top: 40px;
|
|
||||||
|
|
||||||
}
|
|
||||||
#bidsBonusSection #section3 .summaryList img{
|
|
||||||
width: 18px;
|
|
||||||
}
|
|
||||||
#bidsBonusSection #section3 .summaryList ul{
|
|
||||||
text-align: left;
|
|
||||||
width: 300px;
|
|
||||||
margin: 5px auto 20px;
|
|
||||||
line-height: 30px;
|
|
||||||
}
|
|
||||||
.bottom-area{
|
|
||||||
font-size: 16px;
|
|
||||||
}
|
|
||||||
.bottom-area.highlight{
|
|
||||||
color: #FF0658;
|
|
||||||
font-weight: bold;
|
|
||||||
margin-top: 15px;
|
|
||||||
margin-bottom: -10px;
|
|
||||||
}
|
|
||||||
#auctionBidsModal #bidsBonusSection #bonusSection img{
|
|
||||||
width: 20px;
|
|
||||||
margin-top: -2px;
|
|
||||||
}
|
|
||||||
#auctionBidsModal #bidsBonusSection #bonusSection .bottomSection img{
|
|
||||||
width: 16px;
|
|
||||||
margin-right: 4px;
|
|
||||||
}
|
|
||||||
#auctionBidsModal #bidsBonusSection #bonusSection .bottomSection{
|
|
||||||
font-size: 16px;
|
|
||||||
margin-bottom: 15px;
|
|
||||||
color: #5F5F5F;
|
|
||||||
}
|
|
||||||
|
|
||||||
#bonusSection .btnConfirm{
|
|
||||||
font-size: 18px;
|
|
||||||
display: inline-block;
|
|
||||||
color: #333;
|
|
||||||
background-color: #fcc62d;
|
|
||||||
padding: 5px;
|
|
||||||
line-height: 25px;
|
|
||||||
border-radius: 5px;
|
|
||||||
font-weight: bold;
|
|
||||||
margin-bottom: 10px;
|
|
||||||
margin-top: 10px;
|
|
||||||
width: 95%;
|
|
||||||
text-decoration: none;
|
|
||||||
|
|
||||||
}
|
|
||||||
@media (max-width: 576px) {
|
|
||||||
#auctionBidsModal #bidsBonusSection #bonusSection .bottomSection {
|
|
||||||
font-size: 15px;
|
|
||||||
}
|
|
||||||
#bonusSection .btnConfirm{
|
|
||||||
font-size: 16px;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
#rankingBonusSection .title{
|
|
||||||
font-size: 20px;
|
|
||||||
font-weight: bold;
|
|
||||||
margin-bottom: 5px;
|
|
||||||
}
|
|
||||||
#rankingBonusSection .subtitle{
|
|
||||||
font-size: 14px;
|
|
||||||
}
|
|
||||||
#rankingBonusSection #ranking{
|
|
||||||
padding: 0 10px;
|
|
||||||
}
|
|
||||||
#rankingBonusSection #ranking table{
|
|
||||||
margin-top:30px;
|
|
||||||
width: 100%;
|
|
||||||
}
|
|
||||||
#rankingBonusSection #ranking table td{
|
|
||||||
text-align: left;
|
|
||||||
}
|
|
||||||
#rankingBonusSection #ranking table .td1{
|
|
||||||
text-align: center;
|
|
||||||
font-weight: bold;
|
|
||||||
font-size: 14px;
|
|
||||||
}
|
|
||||||
#rankingBonusSection #ranking table .td1 img{
|
|
||||||
width: 38px;
|
|
||||||
}
|
|
||||||
#rankingBonusSection #ranking table td.td2{
|
|
||||||
font-size: 16px;
|
|
||||||
width: 70%;
|
|
||||||
padding: 8px 10px;
|
|
||||||
text-transform: capitalize;
|
|
||||||
}
|
|
||||||
#rankingBonusSection #ranking table td.td3{
|
|
||||||
width: 25%;
|
|
||||||
font-size: 14px;
|
|
||||||
font-weight: bold;
|
|
||||||
text-align: right;
|
|
||||||
}
|
|
||||||
#rankingBonusSection #ranking table td.td3 img{
|
|
||||||
width: 17px;
|
|
||||||
margin-left: 4px;
|
|
||||||
}
|
|
||||||
#auctionBidsModal #section1, #auctionBidsModal #section4{
|
|
||||||
display: none;
|
|
||||||
}
|
|
||||||
#auctionBidsModal #section1{
|
|
||||||
padding: 0px 20px;
|
|
||||||
}
|
|
||||||
#auctionBidsModal #section4 .sad{
|
|
||||||
width: 25px;
|
|
||||||
margin-bottom: 10px;
|
|
||||||
}
|
|
||||||
#BonusSection .img-lock, #BonusSection .img-lock-open{
|
|
||||||
display: none;
|
|
||||||
}
|
|
||||||
#BonusSection .img-lock img, #BonusSection .img-lock-open img{
|
|
||||||
width: 12px;
|
|
||||||
margin-left: 5px;
|
|
||||||
margin-top: -1px;
|
|
||||||
}
|
|
||||||
#BonusSection .pay-bids-counter{
|
|
||||||
margin-left: 5px;
|
|
||||||
color: #fff;
|
|
||||||
background-color: #FF0658;
|
|
||||||
border-radius: 40px;
|
|
||||||
width: 19px;
|
|
||||||
height: 19px;
|
|
||||||
text-align: center;
|
|
||||||
font-weight: bold;
|
|
||||||
font-size: 11px;
|
|
||||||
padding: 2px;
|
|
||||||
display: none;
|
|
||||||
}
|
|
||||||
#auctionBidsModal .wrap-new-daily-challenge.wrap2{
|
|
||||||
margin-top: 5px;
|
|
||||||
}
|
|
||||||
#auctionBidsModal .wrap-new-daily-challenge{
|
|
||||||
color: #2F80ED;
|
|
||||||
font-size: 16px;
|
|
||||||
font-weight: bold;
|
|
||||||
margin: 20px 60px 0px;
|
|
||||||
display: none;
|
|
||||||
}
|
|
||||||
@media (max-width: 576px) {
|
|
||||||
#auctionBidsModal .wrap-new-daily-challenge {
|
|
||||||
margin: 20px 40px 0;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
#BonusSection .img-plus-not-active, #BonusSection .img-plus-active{
|
|
||||||
display: none;
|
|
||||||
}
|
|
||||||
#auctionBidsModal .extraSlots .slot-title .open-lock{
|
|
||||||
margin-top: -5px;
|
|
||||||
width: 15px;
|
|
||||||
margin-right: 3px;
|
|
||||||
}
|
|
||||||
#auctionBidsModal .extraSlots .slot-title span .fa{
|
|
||||||
font-size: 20px;
|
|
||||||
position: absolute;
|
|
||||||
margin-left: 5px;
|
|
||||||
}
|
|
||||||
#auctionBidsModal .extraSlots .wrap-content-slot{
|
|
||||||
padding: 10px 0 15px;
|
|
||||||
}
|
|
||||||
#auctionBidsModal .bonus-obtained{
|
|
||||||
display: none;
|
|
||||||
}
|
|
||||||
#auctionBidsModal .extraSlots{
|
|
||||||
border: none;
|
|
||||||
margin: -5px auto 0;
|
|
||||||
padding: 10px 15px;
|
|
||||||
text-align: center;
|
|
||||||
background-color: #f3f6f9;
|
|
||||||
border-radius: 0;
|
|
||||||
border-bottom-left-radius: 5px;
|
|
||||||
border-bottom-right-radius: 5px;
|
|
||||||
}
|
|
||||||
#auctionBidsModal .extraSlots.avaible{
|
|
||||||
border-color: #55BC62;
|
|
||||||
}
|
|
||||||
#extraSlotTemplate{
|
|
||||||
display: none;
|
|
||||||
}
|
|
||||||
#wrapSlots{
|
|
||||||
display: flex;
|
|
||||||
justify-content: space-evenly;
|
|
||||||
flex-wrap: wrap;
|
|
||||||
}
|
|
||||||
#wrapSlots .box-extra-slot{
|
|
||||||
border: 1px solid #6F6F6F;
|
|
||||||
background-color: #f3f6f9;
|
|
||||||
padding: 10px;
|
|
||||||
border-radius: 5px;
|
|
||||||
width: 100px;
|
|
||||||
margin-top: 15px;
|
|
||||||
display: none;
|
|
||||||
}
|
|
||||||
.wrap-num-other-slot{
|
|
||||||
position: relative;
|
|
||||||
display: none;
|
|
||||||
font-weight: bold;
|
|
||||||
margin-top: 10px;
|
|
||||||
font-size: 12px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.wrap-num-other-slot .reduce{
|
|
||||||
display: none;
|
|
||||||
position: absolute;
|
|
||||||
top: 0;
|
|
||||||
right: 20px;
|
|
||||||
color: #333;
|
|
||||||
text-decoration: underline;
|
|
||||||
}
|
|
||||||
.wrap-num-other-slot .open{
|
|
||||||
color: #55BC62;
|
|
||||||
text-decoration: underline;
|
|
||||||
display: none;
|
|
||||||
}
|
|
||||||
#wrapSlots .box-extra-slot .expire{
|
|
||||||
font-size: 10px;
|
|
||||||
font-weight: bold;
|
|
||||||
color: #6A6B6C;
|
|
||||||
margin-bottom: 5px;
|
|
||||||
}
|
|
||||||
#wrapSlots .box-extra-slot .expire img{
|
|
||||||
margin-left: 2px;
|
|
||||||
}
|
|
||||||
#wrapSlots .box-extra-slot .content{
|
|
||||||
display: flex;
|
|
||||||
justify-content: center;
|
|
||||||
}
|
|
||||||
#wrapSlots .box-extra-slot .content .slot-value{
|
|
||||||
font-size: 18px;
|
|
||||||
font-weight: bold;
|
|
||||||
color: #333;
|
|
||||||
}
|
|
||||||
#wrapSlots .box-extra-slot .wrap-cta .cta{
|
|
||||||
background-color: #B4B4B4;
|
|
||||||
color: #fff;
|
|
||||||
padding: 0px 10px;
|
|
||||||
font-size: 12px;
|
|
||||||
font-weight: bold;
|
|
||||||
width: 100%;
|
|
||||||
max-height: 23px;
|
|
||||||
|
|
||||||
}
|
|
||||||
#wrapSlots .box-extra-slot .wrap-cta .cta img{
|
|
||||||
margin-top: -2px;
|
|
||||||
margin-right: 2px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.extraSlots .wrap-content-slot{
|
|
||||||
display: none;
|
|
||||||
}
|
|
||||||
.extraSlots .slot-title a{
|
|
||||||
width: 100%;
|
|
||||||
display: block;
|
|
||||||
color: #333;
|
|
||||||
text-decoration: none;
|
|
||||||
}
|
|
||||||
.extraSlots .slot-title{
|
|
||||||
color: #000;
|
|
||||||
font-size: 14px;
|
|
||||||
font-weight: bold;
|
|
||||||
}
|
|
||||||
.extraSlots .slot-content{
|
|
||||||
font-size: 12px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.extraSlots .slot-content .beforeConfirmed, .extraSlots .slot-content .afterConfirmed{
|
|
||||||
display: none;
|
|
||||||
}
|
|
||||||
.wrap-extra-slots .box-noSlot .titleNoSlot{
|
|
||||||
color: #FE4E4E;
|
|
||||||
font-size: 14px;
|
|
||||||
font-weight: bold;
|
|
||||||
}
|
|
||||||
.wrap-extra-slots .box-noSlot .contentNoSlot{
|
|
||||||
font-size: 12px;
|
|
||||||
}
|
|
||||||
.wrap-extra-slots .box-noSlot .contentNoSlot img{
|
|
||||||
width: 17px;
|
|
||||||
margin-top: -3px;
|
|
||||||
margin-left: 3px;
|
|
||||||
}
|
|
||||||
.wrap-extra-slots .box-noSlot{
|
|
||||||
margin-top: 10px;
|
|
||||||
}
|
|
||||||
.wrap-extra-slots .box-noSlot{
|
|
||||||
display: none;
|
|
||||||
}
|
|
||||||
.wrap-extra-slots .box-noSlot .box-noextra-slot .no-extra-slot-content .slot-img img{
|
|
||||||
width: 20px;
|
|
||||||
opacity: 0.6;
|
|
||||||
margin-bottom: 5px;
|
|
||||||
}
|
|
||||||
.wrap-extra-slots .box-noSlot .box-noextra-slot .no-extra-slot-content{
|
|
||||||
font-size: 10px;
|
|
||||||
color: #333;
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
justify-content: center;
|
|
||||||
align-items: center;
|
|
||||||
height: 100%;
|
|
||||||
}
|
|
||||||
.wrap-extra-slots .box-noSlot .box-noextra-slot{
|
|
||||||
width: 92px;
|
|
||||||
margin: 20px auto;
|
|
||||||
height: 81px;
|
|
||||||
border: 1px solid #55BC62;
|
|
||||||
border-radius: 5px;
|
|
||||||
}
|
|
||||||
|
|
||||||
#wrapSlots.avaible .box-extra-slot{
|
|
||||||
border-color: #55BC62;
|
|
||||||
background-color: #EDF8EF;
|
|
||||||
}
|
|
||||||
#wrapSlots.avaible .box-extra-slot .wrap-cta .cta{
|
|
||||||
background-color: #55BC62;
|
|
||||||
}
|
|
||||||
.wrap-extra-slots{
|
|
||||||
display: none;
|
|
||||||
}
|
|
||||||
.wrap-extra-slots .title-extraSlot-blocked{
|
|
||||||
font-size: 18px;
|
|
||||||
font-weight: bold;
|
|
||||||
width: 100%;
|
|
||||||
margin: 0 0 10px;
|
|
||||||
display: none;
|
|
||||||
text-align: center;
|
|
||||||
}
|
|
||||||
.wrap-extra-slots .title-extraSlot-blocked img{
|
|
||||||
width: 18px;
|
|
||||||
margin-top: -5px;
|
|
||||||
}
|
|
||||||
#auctionBidsModal #bidsBonusSection .img-lock, #auctionBidsModal #bidsBonusSection .img-lock-open{
|
|
||||||
display: none;
|
|
||||||
|
|
||||||
}
|
|
||||||
#auctionBidsModal #bidsBonusSection .img-lock img, #auctionBidsModal #bidsBonusSection .img-lock-open img{
|
|
||||||
margin-left: 5px;
|
|
||||||
width: 14px;
|
|
||||||
margin-top: -5px;
|
|
||||||
}
|
|
||||||
#auctionBidsModal #differenzaAsteDaConfermare{
|
|
||||||
color: #fff;
|
|
||||||
background-color: #FF0658;
|
|
||||||
border-radius: 40px;
|
|
||||||
width: 23px;
|
|
||||||
height: 23px;
|
|
||||||
text-align: center;
|
|
||||||
font-weight: bold;
|
|
||||||
padding: 1px 2px;
|
|
||||||
display: inline-block;
|
|
||||||
font-size: 15px;
|
|
||||||
}
|
|
||||||
#auctionBidsModal #bonusEarned img{
|
|
||||||
width: 15px;
|
|
||||||
margin-left: 5px;
|
|
||||||
}
|
|
||||||
#auctionBidsModal .wrap-already-taken, #auctionBidsModal .bonus-yet-to-be-obtained{
|
|
||||||
display: none;
|
|
||||||
}
|
|
||||||
#auctionBidsModal .wrap-already-taken .txt-already-taken{
|
|
||||||
color: #797979;
|
|
||||||
}
|
|
||||||
#auctionBidsModal .bonus-yet-to-be-obtained{
|
|
||||||
color: #FF0658;
|
|
||||||
}
|
|
||||||
#auctionBidsModal #bonusEarned .wrap-details-bonus{
|
|
||||||
text-align: center;
|
|
||||||
margin: 7px 0;
|
|
||||||
}
|
|
||||||
#auctionBidsModal #modalConfirmSlotStopGame{
|
|
||||||
z-index: 11;
|
|
||||||
position: absolute;
|
|
||||||
top: 0;
|
|
||||||
bottom: 0;
|
|
||||||
height: 130px;
|
|
||||||
background-color: #fff;
|
|
||||||
width: 270px;
|
|
||||||
margin: auto;
|
|
||||||
left: 0;
|
|
||||||
right: 0;
|
|
||||||
padding: 15px;
|
|
||||||
border-radius: 5px;
|
|
||||||
text-align: center;
|
|
||||||
display: none;
|
|
||||||
}
|
|
||||||
#auctionBidsModal #modalConfirmSlotStopGame .contentButton .btn{
|
|
||||||
padding: 2px 13px;
|
|
||||||
font-size: 12px;
|
|
||||||
color: #fff;
|
|
||||||
font-weight: bold;
|
|
||||||
margin: 3px;
|
|
||||||
}
|
|
||||||
#auctionBidsModal #modalConfirmSlotStopGame .contentButton .btn-confirm{
|
|
||||||
background-color: #55BC62
|
|
||||||
}
|
|
||||||
#auctionBidsModal #modalConfirmSlotStopGame .contentButton .btn-cancel{
|
|
||||||
background-color: #BFBFBF;
|
|
||||||
}
|
|
||||||
#auctionBidsModal #modalConfirmSlotStopGame .contentButton{
|
|
||||||
display: flex;
|
|
||||||
justify-content: center;
|
|
||||||
margin-top: 10px;
|
|
||||||
}
|
|
||||||
#auctionBidsModal #modalConfirmSlotStopGame .content{
|
|
||||||
font-size: 12px;
|
|
||||||
text-align: center;
|
|
||||||
}
|
|
||||||
#auctionBidsModal #modalConfirmSlotStopGame .title{
|
|
||||||
color: #FF0202;
|
|
||||||
text-align: center;
|
|
||||||
font-size: 13px;
|
|
||||||
font-weight: bold;
|
|
||||||
margin-bottom: 5px;
|
|
||||||
}
|
|
||||||
#auctionBidsModal .overlayModalConfirmSlotStopGame{
|
|
||||||
background: rgba(0,0,0,0.2);
|
|
||||||
top: 0;
|
|
||||||
left: 0;
|
|
||||||
position: absolute;
|
|
||||||
width: 100%;
|
|
||||||
height: 100%;
|
|
||||||
display: none;
|
|
||||||
}
|
|
||||||
-113
@@ -1,113 +0,0 @@
|
|||||||
$(document).ready(function () {
|
|
||||||
window.myAuctionsControlDetail = new Array(); // array di oggetti deputato a contenere l'asta dove si sta autopuntando
|
|
||||||
window.myAuctionsControlDetail_lock = false; // flag to lock SetInterval execution
|
|
||||||
|
|
||||||
/*
|
|
||||||
*
|
|
||||||
* @returns {undefined}
|
|
||||||
* questa funzione si occupa di controllare ogni 2 secondi se l'asta (dettaglio) su cui c'è un'autopuntata sia realmente attive o c'è stato un blocco lato UI
|
|
||||||
*
|
|
||||||
*/
|
|
||||||
|
|
||||||
setInterval(
|
|
||||||
function () {
|
|
||||||
|
|
||||||
if (window.myAuctionsControlDetail_lock == false) {
|
|
||||||
|
|
||||||
window.myAuctionsControlDetail_lock = true; // lock setinterval execution
|
|
||||||
|
|
||||||
//console.log("--- Checking autobids..."); // FOR DEBUG
|
|
||||||
let isAuctionStarted_element = $(".auction-action-timer.auction-header-item-size.closed-timer"); // element not present if auction started
|
|
||||||
|
|
||||||
let callingAjax = false;
|
|
||||||
if (isAuctionStarted_element.length == 0) { // check element not present if auction started
|
|
||||||
|
|
||||||
let element_value = $('.auction-autobid-current-value'); // recupero il valore delle puntate rimanenti nell'asta
|
|
||||||
|
|
||||||
if (element_value.length > 0) {
|
|
||||||
let value = $(element_value[0]).text(); // recupero il valore delle puntate rimanenti nell'asta
|
|
||||||
//console.log("Puntate autobid = "+value); // FOR DEBUG
|
|
||||||
|
|
||||||
if (value > 0) {
|
|
||||||
let idasta = $('input.js-switch.autobid-switch').data("id"); // recupero l'id dell'asta
|
|
||||||
//console.log("Checking Asta: " + idasta); // FOR DEBUG
|
|
||||||
let timestamp = Date.now();
|
|
||||||
|
|
||||||
let myAuctions = {
|
|
||||||
idasta: idasta,
|
|
||||||
value: value,
|
|
||||||
timestamp: timestamp,
|
|
||||||
element_value: element_value
|
|
||||||
}
|
|
||||||
|
|
||||||
if (window.myAuctionsControlDetail.length == 0) { // controllo che questa asta non sia già nell'array
|
|
||||||
//console.log("Adding Asta in array."); // FOR DEBUG
|
|
||||||
window.myAuctionsControlDetail = myAuctions;
|
|
||||||
} else {
|
|
||||||
if (window.myAuctionsControlDetail.value != value) { // controllo che il valore sia cambiato per in modo da aggiornare le informazioni
|
|
||||||
//console.log("Value changed."); // FOR DEBUG
|
|
||||||
window.myAuctionsControlDetail = myAuctions;
|
|
||||||
} else {
|
|
||||||
//console.log("Checking time..."); // FOR DEBUG
|
|
||||||
// in questa condizione il valore non è cambiato dunque controllerò da quanto tempo non cambia
|
|
||||||
var diffMs = (Date.now() - window.myAuctionsControlDetail.timestamp);
|
|
||||||
//console.log("diffMs = "+diffMs); // FOR DEBUG
|
|
||||||
//var diffMins = Math.round(((diffMs % 86400000) % 3600000) / 60000); // minutes
|
|
||||||
var diffSecs = Math.round(((diffMs % 86400000) % 3600000) / 1000); // minutes
|
|
||||||
//console.log("diffSecs = "+diffSecs); // FOR DEBUG
|
|
||||||
|
|
||||||
// nel caso in cui la differenza è maggiore o uguale a 2 minuti invoco la funzione che si occuperà di spedire le informazioni lato backend
|
|
||||||
if (diffSecs >= 70) { // default 70 secs
|
|
||||||
callingAjax = true;
|
|
||||||
sentToVerification(myAuctions);
|
|
||||||
window.myAuctionsControlDetail = new Array(); // elimino l'asta dall'array
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
} else {
|
|
||||||
window.myAuctionsControlDetail = new Array(); // elimino l'asta dall'array
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
if (callingAjax === false) {
|
|
||||||
window.myAuctionsControlDetail_lock = false; // unlock setinterval execution
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
},
|
|
||||||
2000);
|
|
||||||
|
|
||||||
|
|
||||||
function sentToVerification(myAuctions) {
|
|
||||||
|
|
||||||
// funzione che serve ad inviare al backend l'asta attiva ma con valori di autopuntata fermi da 2 min
|
|
||||||
//console.log(myAuctions); // FOR DEBUG
|
|
||||||
|
|
||||||
$.ajax({
|
|
||||||
url: "check_autobid.php",
|
|
||||||
//dataType: json,
|
|
||||||
method: 'POST',
|
|
||||||
timeout: 10000, // default 10000
|
|
||||||
data : {
|
|
||||||
idasta: myAuctions.idasta,
|
|
||||||
value: myAuctions.value,
|
|
||||||
timestamp: myAuctions.timestamp
|
|
||||||
},
|
|
||||||
}).done(function (response) {
|
|
||||||
window.myAuctionsControlDetail_lock = false; // unlock setinterval execution
|
|
||||||
//console.log("response = " + response); // FOR DEBUG
|
|
||||||
$(myAuctions.element_value).text(response);
|
|
||||||
}).fail(function(jqXHR, textStatus){
|
|
||||||
if(textStatus === 'timeout') {
|
|
||||||
//console.log("Ajax timeout. Recall ajax."); // FOR DEBUG
|
|
||||||
sentToVerification(myAuctions);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
}
|
|
||||||
});
|
|
||||||
@@ -1,322 +0,0 @@
|
|||||||
function getTexts() {
|
|
||||||
"use strict"
|
|
||||||
return {
|
|
||||||
dialog_confirm: "Sei sicuro di voler rimuovere l\'AutoPuntata?",
|
|
||||||
autobid_active: "Hai attivato la funzione utilizzando le puntate prenotate",
|
|
||||||
autobid_not_active: "Attiva la funzione utilizzando le puntate prenotate",
|
|
||||||
autobid_add: "AGGIUNGI",
|
|
||||||
autobid_insert: "INSERISCI"
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
function enableAutobid() {
|
|
||||||
"use strict";
|
|
||||||
|
|
||||||
$(".auction-action-autobid-trigger")
|
|
||||||
.toggleClass("button-fucsia-flat", true)
|
|
||||||
.toggleClass("button-gray-flat", false)
|
|
||||||
.off('click')
|
|
||||||
.on('click', setAutobid);
|
|
||||||
|
|
||||||
$(".auction-action-autobid-input")
|
|
||||||
.attr('disabled', false)
|
|
||||||
.off("keyup").keyup(function (e) {
|
|
||||||
if (13 == e.which)
|
|
||||||
$(".auction-action-autobid-trigger").click();
|
|
||||||
$(".auction-action-autobid-mobile .auction-action-autobid-trigger").toggleClass("disable", $(this).val().length == 0);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
function disableAutobid(reason) {
|
|
||||||
"use strict";
|
|
||||||
|
|
||||||
$(".auction-action-autobid-trigger")
|
|
||||||
.off('click')
|
|
||||||
.on('click', function () {
|
|
||||||
if (!reason)
|
|
||||||
return;
|
|
||||||
showErrorTooltip('.auction-action-autobid-trigger:eq(' + getAuctionSelector() + ')', {
|
|
||||||
title: reason,
|
|
||||||
html: true,
|
|
||||||
container: "body",
|
|
||||||
trigger: "manual",
|
|
||||||
placement: "top",
|
|
||||||
template: getTemplateTooltip("error")
|
|
||||||
}, 3000);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
function unsetAutobid(evt) {
|
|
||||||
"use strict";
|
|
||||||
if ('undefined' == typeof window['autobid_switchery']) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (!isSwitchEnabled()) {
|
|
||||||
if (evt) {
|
|
||||||
window._autoController.setAutobid('delete', null, cleanUpAutobidSwitch);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function cleanUpAutobidSwitch() {
|
|
||||||
"use strict"
|
|
||||||
$(".auction-autobid-button")
|
|
||||||
.toggleClass("active", false)
|
|
||||||
.find(".bi-autobid")
|
|
||||||
.toggleClass("bi-dark", true)
|
|
||||||
.toggleClass("bi-green", false);
|
|
||||||
$(".auction-action-bid-mobile .auction-autobid-current-value").empty();
|
|
||||||
setTimeout(function () {
|
|
||||||
if (!isSmartphoneDevice())
|
|
||||||
$('.auction-action-autobid-trigger').text(getTexts().autobid_insert);
|
|
||||||
}, 400);
|
|
||||||
updateAutobid(0);
|
|
||||||
$('.auction-action-autobid:not(.auction-seat-autobid) .autobid-switch-container, .auction-action-autobid-mobile .autobid-switch-container').hide();
|
|
||||||
}
|
|
||||||
|
|
||||||
function isSwitchEnabled() {
|
|
||||||
return 'undefined' != typeof window['autobid_switchery'] && window.autobid_switchery[isSmartphoneDevice() ? 1 : 0].isChecked();
|
|
||||||
}
|
|
||||||
|
|
||||||
function hideAutobid() {
|
|
||||||
"use strict";
|
|
||||||
$(".auction-action-autobid:visible").hide();
|
|
||||||
}
|
|
||||||
|
|
||||||
function showLoginAutobid() {
|
|
||||||
"use strict";
|
|
||||||
$(".auction-action-autobid-trigger")
|
|
||||||
.off('click')
|
|
||||||
.on('click', window.parent.showLogin);
|
|
||||||
|
|
||||||
$(".auction-action-autobid-input").attr('disabled', true);
|
|
||||||
}
|
|
||||||
|
|
||||||
function bindAutobidTrigger() {
|
|
||||||
"use strict";
|
|
||||||
var sNickLoggato = $("#NickLoggato").length > 0 ? $("#NickLoggato").val() : "";
|
|
||||||
|
|
||||||
if (sNickLoggato.length <= 0) {
|
|
||||||
return showLoginAutobid();
|
|
||||||
}
|
|
||||||
$('.auction-action-autobid-trigger').off('click').on('click', function (evt) {
|
|
||||||
var triggerElement = $(this);
|
|
||||||
rippleButton(triggerElement, evt);
|
|
||||||
|
|
||||||
var autobidInputElement = $(".auction-action-autobid-input").eq(isSmartphoneDevice() ? 1 : 0);
|
|
||||||
|
|
||||||
var inputAmount = parseInt(autobidInputElement.val(), 10);
|
|
||||||
var dataInputAmount = parseInt(autobidInputElement.data("amount"), 10);
|
|
||||||
var autobidAmount = !isNaN(dataInputAmount) ? dataInputAmount : inputAmount;
|
|
||||||
autobidInputElement.removeData("amount");
|
|
||||||
|
|
||||||
var autobidLoader = $(".auction-autobid-loader-container");
|
|
||||||
var switchContainer = $('.autobid-switch-container');
|
|
||||||
|
|
||||||
triggerElement.removeAttr("data-autobid-button");
|
|
||||||
|
|
||||||
var isNotValidAmount = isNaN(inputAmount) && isNaN(dataInputAmount);
|
|
||||||
if (isNotValidAmount) {
|
|
||||||
if (isSmartphoneDevice() && !$("[data-stage='2']").is(":visible"))
|
|
||||||
return $(".auction-action-autobid-mobile .auction-action-autobid-input").trigger("focus");
|
|
||||||
} else {
|
|
||||||
autobidLoader.removeClass("hidden");
|
|
||||||
switchContainer.hide();
|
|
||||||
}
|
|
||||||
|
|
||||||
cleanAutobidRequest();
|
|
||||||
window._autoController.setAutobid('create', autobidAmount, function () {
|
|
||||||
switchContainer.show();
|
|
||||||
if (true == isSwitchEnabled())
|
|
||||||
return;
|
|
||||||
$(".autobid-switch.js-switch:hidden")
|
|
||||||
.eq(isSmartphoneDevice() ? 1 : 0)
|
|
||||||
.data("autobid-enabled", "true")
|
|
||||||
.trigger('click');
|
|
||||||
$(".auction-autobid-button")
|
|
||||||
.toggleClass("active", true)
|
|
||||||
.find(".bi-autobid")
|
|
||||||
.toggleClass("bi-dark", false)
|
|
||||||
.toggleClass("bi-green", true);
|
|
||||||
|
|
||||||
var id_product = getUrlParam("a").split("_").reverse()[0];
|
|
||||||
$("#DA"+id_product).find('.favorite').attr('title', "Non puoi rimuoverla dai preferiti se è attiva l\'autopuntata");
|
|
||||||
$("#DA"+id_product).find('.favorite').attr('data-original-title', "Non puoi rimuoverla dai preferiti se è attiva l\'autopuntata");
|
|
||||||
$("#DA"+id_product).find('.favorite').attr('disabled', 'disabled');
|
|
||||||
$("#DA"+id_product).find('.favorite').addClass('active');
|
|
||||||
if (isDeepModal()) {
|
|
||||||
window.parent.BidooCnf.instances.auction.features.startAutobidAuctionUpdate();
|
|
||||||
}
|
|
||||||
|
|
||||||
if (isSmartphoneDevice())
|
|
||||||
return;
|
|
||||||
|
|
||||||
setTimeout(function () {
|
|
||||||
$('.auction-action-autobid-trigger').text(getTexts().autobid_add);
|
|
||||||
}, 400);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
function setAutobid() {
|
|
||||||
"use strict";
|
|
||||||
if ('undefined' == typeof window['autobid_switchery']) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
bindAutobidTrigger();
|
|
||||||
$('.auction-action-autobid-trigger').not("[data-autobid-button]").trigger('click');
|
|
||||||
}
|
|
||||||
|
|
||||||
function updateAutobid(value) {
|
|
||||||
"use strict";
|
|
||||||
var element = $(".auction-autobid-current-value");
|
|
||||||
var oldValue = parseInt(element.eq(0).text(), 10);
|
|
||||||
|
|
||||||
if (value != oldValue) {
|
|
||||||
element.toggle(value > 0);
|
|
||||||
element.text(value);
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
function cleanAutobidRequest() {
|
|
||||||
"use strict";
|
|
||||||
$(".auction-action-autobid-input").val('');
|
|
||||||
window._autoController.stopTicker();
|
|
||||||
}
|
|
||||||
|
|
||||||
function updateAutobidStatus(status, value) {
|
|
||||||
"use strict";
|
|
||||||
switch (status) {
|
|
||||||
case 'set':
|
|
||||||
case 'create':
|
|
||||||
{
|
|
||||||
if (0 == value) {
|
|
||||||
closeSwitch();
|
|
||||||
} else {
|
|
||||||
updateAutobid(value);
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
case 'unset':
|
|
||||||
{
|
|
||||||
closeSwitch();
|
|
||||||
unsetAutobid(0);
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
function closeSwitch() {
|
|
||||||
$('.js-switch.autobid-switch')
|
|
||||||
.eq(isSmartphoneDevice() ? 1 : 0)
|
|
||||||
.data("autobid-enabled", "false")
|
|
||||||
.trigger("click");
|
|
||||||
}
|
|
||||||
function setAutobidUI(isAutobid) {
|
|
||||||
"use strict"
|
|
||||||
$(".auction-action-autobid-mobile .autobid-switch-container").toggle(isAutobid);
|
|
||||||
$(".auction-action-bid-mobile .auction-autobid-button")
|
|
||||||
.toggleClass("active", isAutobid)
|
|
||||||
.find(".bi-autobid")
|
|
||||||
.toggleClass("bi-dark", !isAutobid)
|
|
||||||
.toggleClass("bi-green", isAutobid);
|
|
||||||
$(".js-switch.autobid-switch")
|
|
||||||
.data("autobid-enabled", "false")
|
|
||||||
.trigger('click');
|
|
||||||
}
|
|
||||||
|
|
||||||
function setCorrectPlaceholder(isFocused) {
|
|
||||||
"use strict"
|
|
||||||
this.attr("placeholder", isFocused ? "" : $(this).data("placeholder"));
|
|
||||||
$(".auction-action-autobid-mobile .auction-action-autobid-trigger").toggleClass("disable", this.val().length == 0);
|
|
||||||
}
|
|
||||||
|
|
||||||
$(document).ready(function () {
|
|
||||||
"use strict";
|
|
||||||
window.autobid_switchery = [];
|
|
||||||
window.autobid_seat_switchery = [];
|
|
||||||
$(".js-switch.autobid-switch").each(function (k, item) {
|
|
||||||
window.autobid_switchery.push(new Switchery(item, {size: 'small'}));
|
|
||||||
});
|
|
||||||
|
|
||||||
$(".js-switch.autobid-seat-switch").each(function (k, item) {
|
|
||||||
window.autobid_seat_switchery.push(new Switchery(item, {size: 'small'}));
|
|
||||||
});
|
|
||||||
|
|
||||||
$('.js-switch.autobid-seat-switch').off('change').on('change', function () {
|
|
||||||
var self = this;
|
|
||||||
var isEnabled = $(this).is(':checked');
|
|
||||||
if (isEnabled) {
|
|
||||||
window.stage.getUpdate(function (update) {
|
|
||||||
window._autoController.setAutobid('create', update.me.budget.total, function () {
|
|
||||||
$(".autobid-seat-status").text(getTexts().autobid_active);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
window._autoController.setAutobid('delete', null, function () {
|
|
||||||
updateAutobid(0);
|
|
||||||
$(".autobid-seat-status").text(getTexts().autobid_not_active);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
if (isSmartphoneDevice())
|
|
||||||
setAutobidUI(isEnabled);
|
|
||||||
return true;
|
|
||||||
});
|
|
||||||
|
|
||||||
function confirmAutobidUnset() {
|
|
||||||
|
|
||||||
if(confirm(getTexts().dialog_confirm)){
|
|
||||||
|
|
||||||
return true;
|
|
||||||
}else{
|
|
||||||
if (isSmartphoneDevice()){
|
|
||||||
$('.switchery-small small').css('left', '30px');
|
|
||||||
}else{
|
|
||||||
$('#boxcontent .autobid-switch').click();
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
$('.js-switch.autobid-switch').off('change').on('change', function () {
|
|
||||||
var switchAutobid = $(this);
|
|
||||||
if (isSmartphoneDevice() && $("[data-stage='2']").is(":visible"))
|
|
||||||
return true;
|
|
||||||
var id_product = getUrlParam("a").split("_").reverse()[0];
|
|
||||||
if (!switchAutobid.is(':checked') && ("false" == switchAutobid.data("autobid-enabled") || confirmAutobidUnset())) {
|
|
||||||
unsetAutobid({});
|
|
||||||
|
|
||||||
|
|
||||||
$("#DA"+id_product).find('.favorite').removeAttr('disabled');
|
|
||||||
$("#DA"+id_product).find('.favorite').attr('title', "Rimuovi quest\'asta dalle tue preferite");
|
|
||||||
if (isDeepModal()) {
|
|
||||||
$("#DA"+id_product).find('.favorite').removeAttr('data-original-title');
|
|
||||||
window.parent.BidooCnf.instances.auction.features.stopAutobidAuctionUpdate();
|
|
||||||
setTimeout(function () {
|
|
||||||
$("#divAsta"+id_product, parent.document).find('.favorite').removeAttr('data-original-title');
|
|
||||||
$("#divAsta"+id_product, parent.document).find('.favorite').removeAttr('disabled');
|
|
||||||
$("#divAsta"+id_product, parent.document).find('.favorite').attr('title', "Rimuovi quest\'asta dalle tue preferite");
|
|
||||||
}, 500);
|
|
||||||
} else {
|
|
||||||
$("#DA"+id_product).find('.favorite').attr("data-original-title", "Rimuovi quest\'asta dalle tue preferite");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return true;
|
|
||||||
});
|
|
||||||
|
|
||||||
$(".autobid-speed-dial > div > a").on('click', function (e) {
|
|
||||||
e.preventDefault();
|
|
||||||
|
|
||||||
var self = $(this);
|
|
||||||
var amount = parseInt(self.attr("data-amount"));
|
|
||||||
$(".auction-action-autobid-input").data("amount", amount);
|
|
||||||
$('.auction-action-autobid-trigger').attr("data-autobid-button", true).trigger('click');
|
|
||||||
});
|
|
||||||
|
|
||||||
var scopeElement = $(".auction-action-autobid-input[data-placeholder]");
|
|
||||||
scopeElement
|
|
||||||
.focus(setCorrectPlaceholder.bind(scopeElement, true))
|
|
||||||
.blur(setCorrectPlaceholder.bind(scopeElement, false));
|
|
||||||
});
|
|
||||||
Binary file not shown.
|
Before Width: | Height: | Size: 35 KiB |
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -1,255 +0,0 @@
|
|||||||
.btn-promo, .btn-promo:hover {
|
|
||||||
background: #2196f3 !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
.mCSB_inside > .mCSB_container {
|
|
||||||
margin-right: 0px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.mCSB_scrollTools .mCSB_draggerRail {
|
|
||||||
width: 6px;
|
|
||||||
background-color: #e2e2e2;
|
|
||||||
}
|
|
||||||
|
|
||||||
.mCSB_scrollTools .mCSB_draggerContainer {
|
|
||||||
left: 10px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.mCSB_scrollTools .mCSB_dragger .mCSB_dragger_bar {
|
|
||||||
background-color: #20cb9a !important;
|
|
||||||
width: 100%;
|
|
||||||
}
|
|
||||||
|
|
||||||
#toggleBar {
|
|
||||||
margin-left: 0 !important;
|
|
||||||
left: 20px;
|
|
||||||
display: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
.barra {
|
|
||||||
-webkit-font-smoothing: antialiased;
|
|
||||||
}
|
|
||||||
|
|
||||||
.btn-promo {
|
|
||||||
margin-top: -2px;
|
|
||||||
line-height: 17.5px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.view_gray_link {
|
|
||||||
text-decoration: none;
|
|
||||||
cursor: pointer;
|
|
||||||
}
|
|
||||||
|
|
||||||
.leader-btn {
|
|
||||||
margin: 0;
|
|
||||||
padding: 0;
|
|
||||||
border: 0;
|
|
||||||
text-transform: uppercase;
|
|
||||||
font-size: 10px;
|
|
||||||
margin-top: 2px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.bid_chal img.img-lock{
|
|
||||||
display: none;
|
|
||||||
}
|
|
||||||
.bid_chal img{
|
|
||||||
margin-top: -3px;
|
|
||||||
}
|
|
||||||
.wrap-limit-unlock{
|
|
||||||
color: #fff;
|
|
||||||
background-color: #55bc62;
|
|
||||||
border-radius: 5px;
|
|
||||||
font-weight: bold;
|
|
||||||
padding: 0;
|
|
||||||
text-transform: initial;
|
|
||||||
width: 120px;
|
|
||||||
margin: -4px auto 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.wrap-button-get-bonus{
|
|
||||||
color: #000;
|
|
||||||
background-color: #FFC642;
|
|
||||||
border-radius: 5px;
|
|
||||||
font-weight: bold;
|
|
||||||
padding: 0;
|
|
||||||
display: none;
|
|
||||||
text-transform: initial;
|
|
||||||
margin-top: -4px;
|
|
||||||
}
|
|
||||||
.wrap-button-get-bonus img{
|
|
||||||
width: 14px;
|
|
||||||
}
|
|
||||||
.bid_chal {
|
|
||||||
font-weight: bold;
|
|
||||||
font-size: 13px;
|
|
||||||
text-transform: none;
|
|
||||||
margin-bottom: 3px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.next-level {
|
|
||||||
background-color: #eaeaea;
|
|
||||||
margin: 0;
|
|
||||||
width: 124px;
|
|
||||||
height: 8px;
|
|
||||||
box-shadow: none;
|
|
||||||
margin: 0 auto;
|
|
||||||
}
|
|
||||||
|
|
||||||
#tickNotif {
|
|
||||||
background: white;
|
|
||||||
width: 15px;
|
|
||||||
height: 15px;
|
|
||||||
transform: rotate(-45deg);
|
|
||||||
border: 1px solid #e2e2e2;
|
|
||||||
position: fixed;
|
|
||||||
margin-top: -15px;
|
|
||||||
display: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
.notifIcon {
|
|
||||||
margin-left: 0px;
|
|
||||||
}
|
|
||||||
|
|
||||||
#boxarea.dodici {
|
|
||||||
width: 920px;
|
|
||||||
}
|
|
||||||
|
|
||||||
#tickNotif {
|
|
||||||
margin-left: 17px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.small_notif {
|
|
||||||
margin-left: 119px !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
.bonus_dialog {
|
|
||||||
left: 48.5%;
|
|
||||||
}
|
|
||||||
|
|
||||||
.leader_btn {
|
|
||||||
margin-right: 4px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.tooltip.reach > .tooltip-inner .wrap{
|
|
||||||
display: flex;
|
|
||||||
}
|
|
||||||
.tooltip.reach > .tooltip-inner .wrap img{
|
|
||||||
width: 16px;
|
|
||||||
margin-right: 10px;
|
|
||||||
}
|
|
||||||
.tooltip.reach > .tooltip-inner {
|
|
||||||
background-color: #fff;
|
|
||||||
border: 1px solid #333;
|
|
||||||
color: #232323;
|
|
||||||
padding: 10px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.tooltip.reach > .tooltip-inner > span {
|
|
||||||
text-align: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
.tooltip.reach > .tooltip-inner > span:last-child {
|
|
||||||
padding: 10px 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.tooltip.reach > .tooltip-inner strong {
|
|
||||||
display: block;
|
|
||||||
}
|
|
||||||
|
|
||||||
.bid-challenge > strong {
|
|
||||||
color: darkorange;
|
|
||||||
display: block;
|
|
||||||
}
|
|
||||||
|
|
||||||
.tooltip.reach > .tooltip-inner {
|
|
||||||
max-width: 300px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.leader-btn:hover, .leader-btn {
|
|
||||||
background-color: transparent !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
.paid-all {
|
|
||||||
color: #565454;
|
|
||||||
margin-top: -5px;
|
|
||||||
text-transform: initial;
|
|
||||||
}
|
|
||||||
.active-all {
|
|
||||||
color: #55bc62;
|
|
||||||
margin-top: -5px;
|
|
||||||
text-transform: initial;
|
|
||||||
}
|
|
||||||
.paid-all img, .active-all img{
|
|
||||||
width: 16px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.bar-right-side .pull-right{
|
|
||||||
margin-right: -30px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.bar-right-side .pull-right > *,
|
|
||||||
.bar-left-side > *{
|
|
||||||
display: inline-block;
|
|
||||||
}
|
|
||||||
|
|
||||||
.bar-left-side{
|
|
||||||
margin-top: 5px;
|
|
||||||
}
|
|
||||||
.auctions_won_bottom_bar { /*[GR]*/
|
|
||||||
border: 2px solid #ffc518 !important;
|
|
||||||
background-color:#fff;
|
|
||||||
-webkit-box-sizing: border-box;
|
|
||||||
-moz-box-sizing: border-box;
|
|
||||||
box-sizing: border-box;
|
|
||||||
border-radius: 5px;
|
|
||||||
padding: 5px 10px;
|
|
||||||
color:#3d3a3a;
|
|
||||||
outline: 0;
|
|
||||||
font-weight:bold;
|
|
||||||
height:29px;
|
|
||||||
vertical-align: top;
|
|
||||||
}
|
|
||||||
.auctions_won_bottom_bar:hover{ /*[GR]*/
|
|
||||||
background-color: #ffc518 !important;
|
|
||||||
color: #fff;
|
|
||||||
}
|
|
||||||
.auctions_won_bottom_bar .badge { /*[GR]*/
|
|
||||||
background-color: #ff2f4e;
|
|
||||||
left: 20px;
|
|
||||||
top: -12px;
|
|
||||||
margin-left: -20px;
|
|
||||||
}
|
|
||||||
.barra[data-lang="es"] #ba #boxarea .notifIcon{
|
|
||||||
margin-left: 10px;
|
|
||||||
}
|
|
||||||
.barra[data-lang="es"] #ba #boxarea #lim{
|
|
||||||
margin-left: 5px;
|
|
||||||
|
|
||||||
}
|
|
||||||
.barra[data-lang="es"] #ba #boxarea .auctions_won_bottom_bar{
|
|
||||||
margin-left: 5px;
|
|
||||||
margin-top: 1px;
|
|
||||||
|
|
||||||
}
|
|
||||||
@media(max-width: 1200px){
|
|
||||||
.barra[data-lang="es"] #ba #boxarea .auctions_won_bottom_bar, .barra[data-lang="es"] #ba #boxarea #lim{
|
|
||||||
width: 110px;
|
|
||||||
font-size: 11px;
|
|
||||||
padding: 5px;
|
|
||||||
}
|
|
||||||
.barra[data-lang="es"] .notif{
|
|
||||||
margin: -5px 6px !important;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
.bidooBell{
|
|
||||||
color: #c3c0c1;
|
|
||||||
font-size: 21px;
|
|
||||||
margin-top: 4px;
|
|
||||||
}
|
|
||||||
.bidooBell:hover, .bidooBell:active, .bidooBell:focus, .bidooBell.active{
|
|
||||||
color: #666666;
|
|
||||||
transition: color 0.4s;
|
|
||||||
}
|
|
||||||
#auctionBidBottomBar{
|
|
||||||
outline: none !important;
|
|
||||||
}
|
|
||||||
-21
@@ -1,21 +0,0 @@
|
|||||||
function BottomBar(){
|
|
||||||
"use strict"
|
|
||||||
var self = this;
|
|
||||||
self.footer = $(".footer");
|
|
||||||
self.checkFooter();
|
|
||||||
}
|
|
||||||
|
|
||||||
BottomBar.prototype.checkFooter = function() {
|
|
||||||
"use strict"
|
|
||||||
var self = this;
|
|
||||||
if (!self.footer.length) return;
|
|
||||||
$(window).scroll(function() {
|
|
||||||
$(".goTop").find("i")
|
|
||||||
.toggleClass("white_top_arrow", isElementInView(self.footer, false));
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
$(document).ready(function() {
|
|
||||||
"use strict"
|
|
||||||
new BottomBar();
|
|
||||||
});
|
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user