Compare commits
20
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
c9a3c252e1 | ||
|
|
a9860f4c94 | ||
|
|
d9e05fb487 | ||
|
|
e10663c8c0 | ||
|
|
cd03b39c20 | ||
|
|
9d8a7b3100 | ||
|
|
e99479d8fb | ||
|
|
a9150ff58c | ||
|
|
fba822a0af | ||
|
|
73407e5867 | ||
|
|
91664406c4 | ||
|
|
bc95ef157d | ||
|
|
454c12a5a9 | ||
|
|
88fb4935b5 | ||
|
|
ede8ff088c | ||
|
|
f466bb9174 | ||
|
|
b7f65a5a98 | ||
|
|
212527bfc9 | ||
|
|
4d1df14fe5 | ||
|
|
99257c3422 |
@@ -0,0 +1,33 @@
|
|||||||
|
# Copy to .env next to docker-compose.yml and edit. Compose reads it automatically;
|
||||||
|
# git ignores .env. Every value here is optional — the defaults in docker-compose.yml
|
||||||
|
# are what you get without it.
|
||||||
|
|
||||||
|
# --- Ports on your machine -----------------------------------------------------------
|
||||||
|
# Container-side ports are fixed (API 5000, nginx 80); only these move.
|
||||||
|
#LADOSE_WEB_PORT=8080
|
||||||
|
#LADOSE_API_PORT=5000
|
||||||
|
|
||||||
|
# --- Database ------------------------------------------------------------------------
|
||||||
|
# Overrides ConnectionStrings:DbContext from LaDOSE.Api/appsettings.json. The stack has
|
||||||
|
# no Postgres of its own, so this has to point at a reachable server.
|
||||||
|
#
|
||||||
|
# Postgres on the machine running the containers:
|
||||||
|
#LADOSE_DB_CONNECTION=Host=host.docker.internal;Username=tom;Password=tom;Database=ladoseapi
|
||||||
|
#
|
||||||
|
# Postgres elsewhere on the LAN — check the name resolves *inside* the container
|
||||||
|
# (`docker compose run --rm api getent hosts kafka.local`) and use the IP if it does not:
|
||||||
|
#LADOSE_DB_CONNECTION=Host=kafka.local;Username=tom;Password=tom;Database=ladoseapi
|
||||||
|
#
|
||||||
|
# Sql/dump_20240316.sql then Sql/2026-08-05_roles.sql populate an empty database.
|
||||||
|
|
||||||
|
# --- Secrets -------------------------------------------------------------------------
|
||||||
|
# appsettings.json carries placeholders for these. Any value works for JWT locally, but
|
||||||
|
# it must be at least 32 characters: Startup.cs feeds it to HMAC-SHA256 as raw ASCII.
|
||||||
|
#LADOSE_JWT_SECRET=dev-only-secret-not-for-any-deployed-environment
|
||||||
|
#LADOSE_SMASH_API_KEY=
|
||||||
|
#LADOSE_CHALLONGE_API_KEY=
|
||||||
|
|
||||||
|
# --- Frontend ------------------------------------------------------------------------
|
||||||
|
# Only needed if the API is not on http://localhost:${LADOSE_API_PORT}. Resolved by the
|
||||||
|
# browser, so container names like http://api:5000 will not work.
|
||||||
|
#LADOSE_API_BASE_URL=http://localhost:5000
|
||||||
@@ -0,0 +1,69 @@
|
|||||||
|
name: Build App
|
||||||
|
run-name: ${{ gitea.actor }} is building
|
||||||
|
on: [push]
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
Build:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- name: Update
|
||||||
|
run: |
|
||||||
|
sudo apt-get update -y -qq
|
||||||
|
sudo apt-get install zip
|
||||||
|
sudo sh -c "echo '192.168.1.253 descartes.local' >> /etc/hosts"
|
||||||
|
sudo sh -c "echo '192.168.1.253 build.ladose.net' >> /etc/hosts"
|
||||||
|
- name: GetDNS
|
||||||
|
run: |
|
||||||
|
cat /etc/resolv.conf
|
||||||
|
- name: Setup .NET 6.x
|
||||||
|
uses: actions/setup-dotnet@v3
|
||||||
|
with:
|
||||||
|
# Semantic version range syntax or exact version of a dotnet version
|
||||||
|
dotnet-version: '8.x'
|
||||||
|
|
||||||
|
- run: echo "Build."
|
||||||
|
- name: Check out repository code
|
||||||
|
uses: actions/checkout@v3
|
||||||
|
|
||||||
|
- name: Restore Deps
|
||||||
|
run : |
|
||||||
|
cd LaDOSE.Src/
|
||||||
|
dotnet restore LaDOSE.linux.sln
|
||||||
|
- name: Build
|
||||||
|
run: |
|
||||||
|
cd LaDOSE.Src/
|
||||||
|
dotnet build --configuration Release --os linux LaDOSE.DesktopApp.Avalonia
|
||||||
|
dotnet build --configuration Release --os win LaDOSE.DesktopApp.Avalonia
|
||||||
|
- name: Zip file
|
||||||
|
run: |
|
||||||
|
zip -rj build-winx64.zip ./LaDOSE.Src/LaDOSE.DesktopApp.Avalonia/bin/Release/net8.0/win-x64/
|
||||||
|
zip -rj build-linux64.zip ./LaDOSE.Src/LaDOSE.DesktopApp.Avalonia/bin/Release/net8.0/linux-x64/
|
||||||
|
- name: Upload Artifact Windows
|
||||||
|
uses: actions/upload-artifact@v3
|
||||||
|
with:
|
||||||
|
path: build-linux64.zip
|
||||||
|
name: build-linux64.zip
|
||||||
|
retention-days: 30
|
||||||
|
overwrite: true
|
||||||
|
|
||||||
|
- name: Upload Artifact Linux
|
||||||
|
uses: actions/upload-artifact@v3
|
||||||
|
with:
|
||||||
|
path: build-winx64.zip
|
||||||
|
name: build-winx64.zip
|
||||||
|
retention-days: 30
|
||||||
|
overwrite: true
|
||||||
|
|
||||||
|
- name: Get current date
|
||||||
|
id: date
|
||||||
|
run: echo "date=$(echo $(date +'%Y-%m-%d'))" >> $GITHUB_OUTPUT
|
||||||
|
- name: Release
|
||||||
|
if: github.ref_name == 'master'
|
||||||
|
uses: akkuman/gitea-release-action@v1
|
||||||
|
env:
|
||||||
|
with:
|
||||||
|
tag_name: release-${{ steps.date.outputs.date }}
|
||||||
|
files: |-
|
||||||
|
build-winx64.zip
|
||||||
|
build-linux64.zip
|
||||||
|
|
||||||
@@ -328,3 +328,10 @@ ASALocalRun/
|
|||||||
|
|
||||||
# MFractors (Xamarin productivity tool) working folder
|
# MFractors (Xamarin productivity tool) working folder
|
||||||
.mfractor/
|
.mfractor/
|
||||||
|
|
||||||
|
# Local docker-compose overrides: connection string, API keys, ports.
|
||||||
|
# .env.example is documentation and stays tracked.
|
||||||
|
.env
|
||||||
|
.env.*
|
||||||
|
!.env.example
|
||||||
|
docker-compose.override.yml
|
||||||
|
|||||||
@@ -1,2 +1,22 @@
|
|||||||
*/*/bin*
|
# Context for LaDOSE.Src/Dockerfile.
|
||||||
*/*/obj*
|
#
|
||||||
|
# The previous patterns here were */*/bin* and */*/obj*, which matched nothing: this
|
||||||
|
# context is rooted at LaDOSE.Src, so build output sits one level down (LaDOSE.Api/bin),
|
||||||
|
# not two.
|
||||||
|
**/bin/
|
||||||
|
**/obj/
|
||||||
|
|
||||||
|
# The frontend is a separate image with its own context (LaDOSE.WebApp/Dockerfile).
|
||||||
|
# Its node_modules alone was adding ~118 MB to every API build.
|
||||||
|
LaDOSE.WebApp/node_modules/
|
||||||
|
LaDOSE.WebApp/build/
|
||||||
|
LaDOSE.WebApp/.svelte-kit/
|
||||||
|
|
||||||
|
# Local state and editor noise. Note Libraries/ is NOT excluded: LaDOSE.Business
|
||||||
|
# references ChallongeCSharpDriver.dll from there by HintPath.
|
||||||
|
.git
|
||||||
|
.vs/
|
||||||
|
.vscode/
|
||||||
|
.idea/
|
||||||
|
*.user
|
||||||
|
*.suo
|
||||||
|
|||||||
+40
-11
@@ -1,15 +1,44 @@
|
|||||||
FROM microsoft/dotnet:sdk AS build-env
|
# Builds the LaDOSE.Api image. Context is LaDOSE.Src (see .dockerignore next to this file).
|
||||||
|
#
|
||||||
|
# Only LaDOSE.Api is published. LaDOSE.linux.sln also carries the Avalonia desktop app,
|
||||||
|
# the Discord bot and LinuxTest, and `dotnet publish <sln> -o out` flattens every project
|
||||||
|
# into that one directory — which is why the previous version of this file copied from
|
||||||
|
# /app/LaDOSE.Api/out/ and found nothing there.
|
||||||
|
ARG DOTNET_VERSION=9.0
|
||||||
|
|
||||||
|
FROM mcr.microsoft.com/dotnet/sdk:${DOTNET_VERSION} AS build
|
||||||
|
WORKDIR /src
|
||||||
|
|
||||||
|
# Debug is deliberate for local work, and docker-compose.yml passes it: the OpenAPI
|
||||||
|
# document and the Scalar UI are gated behind `#if DEBUG` in Startup.cs *and* behind
|
||||||
|
# Condition="'$(Configuration)' == 'Debug'" on their PackageReferences in
|
||||||
|
# LaDOSE.Api.csproj. A Release image therefore serves no /openapi/v1.json, which is
|
||||||
|
# exactly what LaDOSE.WebApp's `npm run api:sync` reads. Default stays Release.
|
||||||
|
ARG BUILD_CONFIGURATION=Release
|
||||||
|
|
||||||
|
# Project files first so this layer survives every .cs edit. Restore has to run under
|
||||||
|
# the same Configuration as the publish below, or the conditional PackageReferences
|
||||||
|
# above make the two disagree about which packages the assets file should contain.
|
||||||
|
COPY global.json ./
|
||||||
|
COPY LaDOSE.Api/LaDOSE.Api.csproj LaDOSE.Api/
|
||||||
|
COPY LaDOSE.DTO/LaDOSE.DTO.csproj LaDOSE.DTO/
|
||||||
|
COPY LaDOSE.Entity/LaDOSE.Entity.csproj LaDOSE.Entity/
|
||||||
|
COPY LaDOSE.Service/LaDOSE.Business.csproj LaDOSE.Service/
|
||||||
|
RUN dotnet restore LaDOSE.Api/LaDOSE.Api.csproj -p:Configuration=${BUILD_CONFIGURATION}
|
||||||
|
|
||||||
|
# Libraries/ChallongeCSharpDriver.dll is a HintPath reference from LaDOSE.Business,
|
||||||
|
# so the build needs the whole tree, not just the projects listed above.
|
||||||
|
COPY . .
|
||||||
|
RUN dotnet publish LaDOSE.Api/LaDOSE.Api.csproj -c ${BUILD_CONFIGURATION} --no-restore -o /app/out
|
||||||
|
|
||||||
|
FROM mcr.microsoft.com/dotnet/aspnet:${DOTNET_VERSION}
|
||||||
WORKDIR /app
|
WORKDIR /app
|
||||||
|
COPY --from=build /app/out/ ./
|
||||||
|
|
||||||
# Copy everything else and build
|
# Fixed in the image on purpose. Program.cs binds Kestrel from appsettings.json's
|
||||||
COPY . ./
|
# AllowedHosts/Port through a ConfigurationBuilder that reads *only* that file, so a
|
||||||
|
# Port env var would not move the listener — remap on the host side instead.
|
||||||
RUN dotnet publish LaDOSE.linux.sln -c Release -o out
|
# Everything Startup.cs reads does honour env vars (ConnectionStrings__DbContext,
|
||||||
|
# ApiKey__SmashApiKey, ApiKey__ChallongeApiKey, JWTTokenSecret).
|
||||||
# Build runtime image
|
|
||||||
FROM microsoft/dotnet:aspnetcore-runtime
|
|
||||||
WORKDIR /app
|
|
||||||
COPY --from=build-env /app/LaDOSE.Api/out/ .
|
|
||||||
EXPOSE 5000
|
EXPOSE 5000
|
||||||
ENTRYPOINT ["dotnet", "LaDOSE.Api.dll"]
|
ENTRYPOINT ["dotnet", "LaDOSE.Api.dll"]
|
||||||
|
|
||||||
|
|||||||
@@ -17,8 +17,17 @@ namespace LaDOSE.Api.Controllers
|
|||||||
[Produces("application/json")]
|
[Produces("application/json")]
|
||||||
public class GameController : GenericControllerDTO<IGameService, Game, GameDTO>
|
public class GameController : GenericControllerDTO<IGameService, Game, GameDTO>
|
||||||
{
|
{
|
||||||
public GameController(IMapper mapper,IGameService service) : base(mapper,service)
|
private IExternalProviderService provider;
|
||||||
|
public GameController(IMapper mapper,IGameService service, IExternalProviderService service2) : base(mapper,service)
|
||||||
{
|
{
|
||||||
|
provider = service2;
|
||||||
|
}
|
||||||
|
[HttpGet("smash/{name}")]
|
||||||
|
public async Task<List<GameDTO>> GetIdFromSmash(string name)
|
||||||
|
{
|
||||||
|
var smashGame = await provider.GetSmashGame(name);
|
||||||
|
|
||||||
|
return _mapper.Map<List<GameDTO>>(smashGame);;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,38 @@
|
|||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
using AutoMapper;
|
||||||
|
using LaDOSE.Business.Interface;
|
||||||
|
using LaDOSE.DTO;
|
||||||
|
using Microsoft.AspNetCore.Authorization;
|
||||||
|
using Microsoft.AspNetCore.Mvc;
|
||||||
|
|
||||||
|
namespace LaDOSE.Api.Controllers
|
||||||
|
{
|
||||||
|
[Authorize]
|
||||||
|
[Produces("application/json")]
|
||||||
|
[Route("api/[controller]")]
|
||||||
|
public class StatisticsController : Controller
|
||||||
|
{
|
||||||
|
private readonly IStatisticsService _service;
|
||||||
|
private readonly IMapper _mapper;
|
||||||
|
|
||||||
|
public StatisticsController(IMapper mapper, IStatisticsService service)
|
||||||
|
{
|
||||||
|
_mapper = mapper;
|
||||||
|
_service = service;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Match statistics for the given Event ids. Body is a bare JSON array of ids,
|
||||||
|
/// like TournamentController.GetResults.
|
||||||
|
/// A null or empty array returns a zeroed, empty MatchStatsDTO rather than an error.
|
||||||
|
/// Read Coverage before trusting the numbers: many brackets have no set rows at all.
|
||||||
|
/// </summary>
|
||||||
|
[HttpPost("Matches")]
|
||||||
|
public async Task<MatchStatsDTO> GetMatchStats([FromBody] List<int> ids)
|
||||||
|
{
|
||||||
|
var stats = await _service.GetMatchStats(ids);
|
||||||
|
return _mapper.Map<MatchStatsDTO>(stats);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -19,7 +19,7 @@ namespace LaDOSE.Api.Controllers
|
|||||||
|
|
||||||
private IMapper _mapper;
|
private IMapper _mapper;
|
||||||
|
|
||||||
// GET
|
// GETawa
|
||||||
public TournamentController(IMapper mapper, IExternalProviderService service)
|
public TournamentController(IMapper mapper, IExternalProviderService service)
|
||||||
{
|
{
|
||||||
_mapper = mapper;
|
_mapper = mapper;
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
using System;
|
using System;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.IdentityModel.Tokens.Jwt;
|
using System.IdentityModel.Tokens.Jwt;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
@@ -9,6 +9,7 @@ using LaDOSE.Business.Interface;
|
|||||||
using LaDOSE.DTO;
|
using LaDOSE.DTO;
|
||||||
using LaDOSE.Entity;
|
using LaDOSE.Entity;
|
||||||
using Microsoft.AspNetCore.Authorization;
|
using Microsoft.AspNetCore.Authorization;
|
||||||
|
using Microsoft.AspNetCore.Http;
|
||||||
using Microsoft.AspNetCore.Mvc;
|
using Microsoft.AspNetCore.Mvc;
|
||||||
using Microsoft.Extensions.Configuration;
|
using Microsoft.Extensions.Configuration;
|
||||||
using Microsoft.Extensions.Options;
|
using Microsoft.Extensions.Options;
|
||||||
@@ -33,12 +34,32 @@ namespace LaDOSE.Api.Controllers
|
|||||||
_configuration = configuration;
|
_configuration = configuration;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>Public view of a user: no password, no hash, no salt.</summary>
|
||||||
|
private static ApplicationUserDTO ToDto(ApplicationUser user)
|
||||||
|
{
|
||||||
|
return new ApplicationUserDTO
|
||||||
|
{
|
||||||
|
Id = user.Id,
|
||||||
|
Username = user.Username,
|
||||||
|
FirstName = user.FirstName,
|
||||||
|
LastName = user.LastName,
|
||||||
|
Roles = user.Names()
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>The id the JWT was issued for; null if the request is not authenticated.</summary>
|
||||||
|
private int? CurrentUserId()
|
||||||
|
{
|
||||||
|
return int.TryParse(User?.Identity?.Name, out var id) ? id : (int?)null;
|
||||||
|
}
|
||||||
|
|
||||||
[AllowAnonymous]
|
[AllowAnonymous]
|
||||||
[HttpPost("auth")]
|
[HttpPost("auth")]
|
||||||
public IActionResult Authenticate([FromBody]ApplicationUser userDto)
|
[ProducesResponseType(typeof(ApplicationUserDTO), StatusCodes.Status200OK)]
|
||||||
|
[ProducesResponseType(StatusCodes.Status400BadRequest)]
|
||||||
|
public IActionResult Authenticate([FromBody]ApplicationUserDTO userDto)
|
||||||
{
|
{
|
||||||
var user = _userService.Authenticate(userDto.Username, userDto.Password);
|
var user = _userService.Authenticate(userDto?.Username, userDto?.Password);
|
||||||
|
|
||||||
if (user == null)
|
if (user == null)
|
||||||
return BadRequest(new { message = "Username or password is incorrect" });
|
return BadRequest(new { message = "Username or password is incorrect" });
|
||||||
@@ -47,6 +68,9 @@ namespace LaDOSE.Api.Controllers
|
|||||||
var key = Encoding.ASCII.GetBytes(this._configuration["JWTTokenSecret"]);
|
var key = Encoding.ASCII.GetBytes(this._configuration["JWTTokenSecret"]);
|
||||||
var tokenDescriptor = new SecurityTokenDescriptor
|
var tokenDescriptor = new SecurityTokenDescriptor
|
||||||
{
|
{
|
||||||
|
// Only the user id goes in the token. Roles are read from the database on
|
||||||
|
// every request instead, so granting or revoking Admin takes effect at
|
||||||
|
// once rather than whenever the current token happens to expire.
|
||||||
Subject = new ClaimsIdentity(new Claim[]
|
Subject = new ClaimsIdentity(new Claim[]
|
||||||
{
|
{
|
||||||
new Claim(ClaimTypes.Name, user.Id.ToString()),
|
new Claim(ClaimTypes.Name, user.Id.ToString()),
|
||||||
@@ -60,29 +84,62 @@ namespace LaDOSE.Api.Controllers
|
|||||||
var tokenString = tokenHandler.WriteToken(token);
|
var tokenString = tokenHandler.WriteToken(token);
|
||||||
|
|
||||||
// return basic user info (without password) and token to store client side
|
// return basic user info (without password) and token to store client side
|
||||||
return Ok(new ApplicationUserDTO
|
var dto = ToDto(user);
|
||||||
{
|
dto.Token = tokenString;
|
||||||
Id = user.Id,
|
dto.Expire = token.ValidTo;
|
||||||
Username = user.Username,
|
return Ok(dto);
|
||||||
FirstName = user.FirstName,
|
|
||||||
LastName = user.LastName,
|
|
||||||
Token = tokenString,
|
|
||||||
Expire = token.ValidTo
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
//[AllowAnonymous]
|
/// <summary>Every account, for the admin user-management screen.</summary>
|
||||||
[HttpPost("register")]
|
[Authorize(Roles = Roles.Admin)]
|
||||||
public IActionResult Register([FromBody]ApplicationUser userDto)
|
[HttpGet]
|
||||||
|
[ProducesResponseType(typeof(List<ApplicationUserDTO>), StatusCodes.Status200OK)]
|
||||||
|
public IActionResult GetUsers()
|
||||||
{
|
{
|
||||||
// map dto to entity
|
var users = _userService.GetAll()
|
||||||
|
.OrderBy(user => user.Username)
|
||||||
|
.Select(ToDto)
|
||||||
|
.ToList();
|
||||||
|
|
||||||
|
return Ok(users);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>The role names that may be assigned, from the applicationrole table.</summary>
|
||||||
|
[Authorize(Roles = Roles.Admin)]
|
||||||
|
[HttpGet("Roles")]
|
||||||
|
[ProducesResponseType(typeof(List<string>), StatusCodes.Status200OK)]
|
||||||
|
public IActionResult GetRoles()
|
||||||
|
{
|
||||||
|
return Ok(_userService.GetAllRoles().Select(role => role.Name).ToList());
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Creates an account. This replaces the old anonymous <c>register</c> endpoint —
|
||||||
|
/// only an admin may create users now, so the very first admin has to be promoted
|
||||||
|
/// directly in the database (see Sql/2026-08-05_roles.sql).
|
||||||
|
/// </summary>
|
||||||
|
[Authorize(Roles = Roles.Admin)]
|
||||||
|
[HttpPost("AddUser")]
|
||||||
|
[ProducesResponseType(typeof(ApplicationUserDTO), StatusCodes.Status200OK)]
|
||||||
|
[ProducesResponseType(StatusCodes.Status400BadRequest)]
|
||||||
|
public IActionResult AddUser([FromBody]ApplicationUserDTO userDto)
|
||||||
|
{
|
||||||
|
if (userDto == null)
|
||||||
|
return BadRequest(new { message = "No user supplied" });
|
||||||
|
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
// save
|
var created = _userService.Create(
|
||||||
_userService.Create(userDto, userDto.Password);
|
new ApplicationUser
|
||||||
return Ok();
|
{
|
||||||
|
Username = userDto.Username?.Trim(),
|
||||||
|
FirstName = userDto.FirstName,
|
||||||
|
LastName = userDto.LastName
|
||||||
|
},
|
||||||
|
userDto.Password,
|
||||||
|
userDto.Roles);
|
||||||
|
|
||||||
|
return Ok(ToDto(created));
|
||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
{
|
{
|
||||||
@@ -91,7 +148,35 @@ namespace LaDOSE.Api.Controllers
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Deletes an account. Refuses to delete the caller: since only an admin can reach
|
||||||
|
/// this, and an admin cannot remove themselves, at least one admin always survives
|
||||||
|
/// — which matters because there is no anonymous way back in any more.
|
||||||
|
/// </summary>
|
||||||
|
[Authorize(Roles = Roles.Admin)]
|
||||||
|
[HttpDelete("{id}")]
|
||||||
|
[ProducesResponseType(StatusCodes.Status204NoContent)]
|
||||||
|
[ProducesResponseType(StatusCodes.Status400BadRequest)]
|
||||||
|
[ProducesResponseType(StatusCodes.Status404NotFound)]
|
||||||
|
public IActionResult DeleteUser(int id)
|
||||||
|
{
|
||||||
|
var user = _userService.GetById(id);
|
||||||
|
if (user == null)
|
||||||
|
return NotFound(new { message = "User not found" });
|
||||||
|
|
||||||
|
if (CurrentUserId() == id)
|
||||||
|
return BadRequest(new { message = "You cannot delete your own account" });
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
_userService.Delete(id);
|
||||||
|
return NoContent();
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
return BadRequest(new { message = ex.Message });
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -77,7 +77,7 @@ namespace LaDOSE.Api.Controllers
|
|||||||
[HttpGet("UpdateDb")]
|
[HttpGet("UpdateDb")]
|
||||||
public bool UpdateDb()
|
public bool UpdateDb()
|
||||||
{
|
{
|
||||||
return _service.UpdateBooking();
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
[HttpGet("CreateChallonge/{gameId:int}/{wpEventId:int}")]
|
[HttpGet("CreateChallonge/{gameId:int}/{wpEventId:int}")]
|
||||||
|
|||||||
@@ -0,0 +1,41 @@
|
|||||||
|
#if DEBUG
|
||||||
|
using System.Linq;
|
||||||
|
using Microsoft.AspNetCore.Mvc.ApplicationModels;
|
||||||
|
using Microsoft.AspNetCore.Mvc.Routing;
|
||||||
|
|
||||||
|
namespace LaDOSE.Api.Helpers
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// The controllers in this project are attribute-routed but do not carry [ApiController].
|
||||||
|
/// Without it MVC never sets ApiExplorer visibility, so ApiExplorer yields no descriptions
|
||||||
|
/// and the generated OpenAPI document comes out with an empty "paths" object.
|
||||||
|
/// This convention opts the attribute-routed actions into ApiExplorer for the
|
||||||
|
/// OpenAPI/Scalar tooling only, without pulling in the [ApiController] behaviours
|
||||||
|
/// (automatic 400 responses, [FromBody] inference) that would change runtime binding.
|
||||||
|
/// </summary>
|
||||||
|
public class ApiExplorerVisibilityConvention : IControllerModelConvention
|
||||||
|
{
|
||||||
|
public void Apply(ControllerModel controller)
|
||||||
|
{
|
||||||
|
// Default the controller to hidden, then opt in action by action.
|
||||||
|
controller.ApiExplorer.IsVisible ??= false;
|
||||||
|
|
||||||
|
foreach (var action in controller.Actions)
|
||||||
|
{
|
||||||
|
if (action.ApiExplorer.IsVisible != null)
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// An action with a [Route] but no verb attribute (e.g. BotEventController's
|
||||||
|
// CreateBotEvent) matches every HTTP method, so ApiExplorer reports an empty
|
||||||
|
// method and OpenAPI generation throws "Unsupported HTTP method".
|
||||||
|
// Only document actions that pin down a verb.
|
||||||
|
action.ApiExplorer.IsVisible = action.Attributes
|
||||||
|
.OfType<IActionHttpMethodProvider>()
|
||||||
|
.Any(provider => provider.HttpMethods?.Any() == true);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
#endif
|
||||||
@@ -0,0 +1,20 @@
|
|||||||
|
using System.Text;
|
||||||
|
using Microsoft.EntityFrameworkCore.Storage;
|
||||||
|
using Npgsql.EntityFrameworkCore.PostgreSQL.Storage.Internal;
|
||||||
|
|
||||||
|
namespace LaDOSE.Api;
|
||||||
|
|
||||||
|
#pragma warning disable EF1001
|
||||||
|
public class NpgsqlSqlGenerationLowercaseHelper : NpgsqlSqlGenerationHelper
|
||||||
|
|
||||||
|
{
|
||||||
|
|
||||||
|
static string ToLowerCase(string input) => input.ToLower();
|
||||||
|
public NpgsqlSqlGenerationLowercaseHelper(RelationalSqlGenerationHelperDependencies dependencies)
|
||||||
|
: base(dependencies) { }
|
||||||
|
public override string DelimitIdentifier(string identifier)
|
||||||
|
=> base.DelimitIdentifier(ToLowerCase(identifier));
|
||||||
|
public override void DelimitIdentifier(StringBuilder builder, string identifier)
|
||||||
|
=> base.DelimitIdentifier(builder, ToLowerCase(identifier));
|
||||||
|
}
|
||||||
|
#pragma warning restore EF1001
|
||||||
@@ -1,8 +1,9 @@
|
|||||||
<Project Sdk="Microsoft.NET.Sdk.Web">
|
<Project Sdk="Microsoft.NET.Sdk.Web">
|
||||||
|
|
||||||
<PropertyGroup>
|
<PropertyGroup>
|
||||||
<TargetFramework>net6.0</TargetFramework>
|
<TargetFramework>net9.0</TargetFramework>
|
||||||
<Platforms>AnyCPU;x64</Platforms>
|
<Platforms>AnyCPU;x64</Platforms>
|
||||||
|
<LangVersion>12</LangVersion>
|
||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
@@ -10,13 +11,16 @@
|
|||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<PackageReference Include="AutoMapper" Version="10.0.0" />
|
<PackageReference Include="AutoMapper" Version="13.0.1" />
|
||||||
<PackageReference Include="Microsoft.AspNetCore.Authentication.JwtBearer" Version="3.1.8" />
|
<PackageReference Include="Microsoft.AspNetCore.Authentication.JwtBearer" Version="8.0.12" />
|
||||||
<PackageReference Include="Microsoft.AspNetCore.Mvc.NewtonsoftJson" Version="3.1.8" />
|
<PackageReference Include="Microsoft.AspNetCore.Mvc.NewtonsoftJson" Version="8.0.12" />
|
||||||
<PackageReference Include="Microsoft.Extensions.Configuration.Json" Version="3.1.8" />
|
<PackageReference Include="Microsoft.Extensions.Configuration.Json" Version="8.0.1" />
|
||||||
<PackageReference Include="Microsoft.VisualStudio.Web.CodeGeneration.Design" Version="3.1.4" />
|
<PackageReference Include="Microsoft.OpenApi" Version="1.6.17" Condition="'$(Configuration)' == 'Debug'" />
|
||||||
<PackageReference Include="Newtonsoft.Json" Version="12.0.3" />
|
<PackageReference Include="Microsoft.AspNetCore.OpenApi" Version="9.0.18" Condition="'$(Configuration)' == 'Debug'" />
|
||||||
<PackageReference Include="Pomelo.EntityFrameworkCore.MySql" Version="6.0.2" />
|
<PackageReference Include="Scalar.AspNetCore" Version="2.16.17" Condition="'$(Configuration)' == 'Debug'" />
|
||||||
|
<PackageReference Include="Microsoft.VisualStudio.Web.CodeGeneration.Design" Version="8.0.7" />
|
||||||
|
<PackageReference Include="Newtonsoft.Json" Version="13.0.3" />
|
||||||
|
<PackageReference Include="Npgsql.EntityFrameworkCore.PostgreSQL" Version="8.0.11" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
|
|||||||
@@ -1,28 +1,20 @@
|
|||||||
using System;
|
using System;
|
||||||
using System.Collections.Generic;
|
|
||||||
using System.Linq;
|
|
||||||
using System.Reflection;
|
using System.Reflection;
|
||||||
|
using System.Security.Claims;
|
||||||
using System.Text;
|
using System.Text;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
using LaDOSE.Business.Interface;
|
using LaDOSE.Business.Interface;
|
||||||
using LaDOSE.Business.Provider;
|
|
||||||
using LaDOSE.Business.Service;
|
using LaDOSE.Business.Service;
|
||||||
using LaDOSE.Entity;
|
using LaDOSE.Entity;
|
||||||
using LaDOSE.Entity.Context;
|
using LaDOSE.Entity.Context;
|
||||||
using Microsoft.AspNetCore.Authentication.JwtBearer;
|
using Microsoft.AspNetCore.Authentication.JwtBearer;
|
||||||
using Microsoft.AspNetCore.Builder;
|
using Microsoft.AspNetCore.Builder;
|
||||||
using Microsoft.AspNetCore.Hosting;
|
using Microsoft.AspNetCore.Hosting;
|
||||||
//using Microsoft.AspNetCore.HttpsPolicy;
|
|
||||||
using Microsoft.AspNetCore.Identity;
|
|
||||||
using Microsoft.AspNetCore.Mvc;
|
|
||||||
using Microsoft.EntityFrameworkCore;
|
using Microsoft.EntityFrameworkCore;
|
||||||
using Microsoft.Extensions.Configuration;
|
using Microsoft.Extensions.Configuration;
|
||||||
using Microsoft.Extensions.DependencyInjection;
|
using Microsoft.Extensions.DependencyInjection;
|
||||||
using Microsoft.Extensions.Logging;
|
using Microsoft.Extensions.Logging;
|
||||||
using Microsoft.Extensions.Options;
|
|
||||||
using Microsoft.IdentityModel.Tokens;
|
using Microsoft.IdentityModel.Tokens;
|
||||||
using Pomelo.EntityFrameworkCore.MySql;
|
|
||||||
using Pomelo.EntityFrameworkCore.MySql.Infrastructure;
|
|
||||||
using AutoMapper;
|
using AutoMapper;
|
||||||
using LaDOSE.Api.Helpers;
|
using LaDOSE.Api.Helpers;
|
||||||
using LaDOSE.Business.Helper;
|
using LaDOSE.Business.Helper;
|
||||||
@@ -32,6 +24,11 @@ using LaDOSE.Entity.Challonge;
|
|||||||
using LaDOSE.Entity.Wordpress;
|
using LaDOSE.Entity.Wordpress;
|
||||||
using Result = LaDOSE.Entity.Challonge.Result;
|
using Result = LaDOSE.Entity.Challonge.Result;
|
||||||
using LaDOSE.Entity.BotEvent;
|
using LaDOSE.Entity.BotEvent;
|
||||||
|
using Microsoft.EntityFrameworkCore.Storage;
|
||||||
|
using Microsoft.Extensions.Hosting;
|
||||||
|
#if DEBUG
|
||||||
|
using Scalar.AspNetCore;
|
||||||
|
#endif
|
||||||
|
|
||||||
namespace LaDOSE.Api
|
namespace LaDOSE.Api
|
||||||
{
|
{
|
||||||
@@ -48,11 +45,6 @@ namespace LaDOSE.Api
|
|||||||
public void ConfigureServices(IServiceCollection services)
|
public void ConfigureServices(IServiceCollection services)
|
||||||
{
|
{
|
||||||
//Fix Gentoo Issue.
|
//Fix Gentoo Issue.
|
||||||
|
|
||||||
var MySqlServer = this.Configuration["MySql:Server"];
|
|
||||||
var MySqlDatabase = this.Configuration["MySql:Database"];
|
|
||||||
var MySqlUser = this.Configuration["MySql:User"];
|
|
||||||
var MySqlPassword = this.Configuration["MySql:Password"];
|
|
||||||
if (Convert.ToBoolean(this.Configuration["FixGentoo"]))
|
if (Convert.ToBoolean(this.Configuration["FixGentoo"]))
|
||||||
{
|
{
|
||||||
try
|
try
|
||||||
@@ -67,11 +59,21 @@ namespace LaDOSE.Api
|
|||||||
}
|
}
|
||||||
|
|
||||||
services.AddCors();
|
services.AddCors();
|
||||||
services.AddMvc().AddNewtonsoftJson(x =>
|
services.AddMvc(options =>
|
||||||
|
{
|
||||||
|
#if DEBUG
|
||||||
|
// Make the attribute-routed controllers visible to ApiExplorer so the
|
||||||
|
// OpenAPI document is actually populated. See ApiExplorerVisibilityConvention.
|
||||||
|
options.Conventions.Add(new ApiExplorerVisibilityConvention());
|
||||||
|
#endif
|
||||||
|
}).AddNewtonsoftJson(x =>
|
||||||
{
|
{
|
||||||
x.SerializerSettings.ReferenceLoopHandling = Newtonsoft.Json.ReferenceLoopHandling.Ignore;
|
x.SerializerSettings.ReferenceLoopHandling = Newtonsoft.Json.ReferenceLoopHandling.Ignore;
|
||||||
x.SerializerSettings.MaxDepth= 4;
|
x.SerializerSettings.MaxDepth= 4;
|
||||||
});
|
});
|
||||||
|
#if DEBUG
|
||||||
|
services.AddOpenApi();
|
||||||
|
#endif
|
||||||
// services.AddDbContextPool<LaDOSEDbContext>( // replace "YourDbContext" with the class name of your DbContext
|
// services.AddDbContextPool<LaDOSEDbContext>( // replace "YourDbContext" with the class name of your DbContext
|
||||||
//
|
//
|
||||||
// options => options.UseMySql($"Server={MySqlServer};Database={MySqlDatabase};User={MySqlUser};Password={MySqlPassword};", // replace with your Connection String
|
// options => options.UseMySql($"Server={MySqlServer};Database={MySqlDatabase};User={MySqlUser};Password={MySqlPassword};", // replace with your Connection String
|
||||||
@@ -82,9 +84,7 @@ namespace LaDOSE.Api
|
|||||||
// ));
|
// ));
|
||||||
services.AddDbContextPool<LaDOSEDbContext>(options =>
|
services.AddDbContextPool<LaDOSEDbContext>(options =>
|
||||||
{
|
{
|
||||||
options.UseMySql(
|
options.UseNpgsql(Configuration.GetConnectionString("DbContext")).ReplaceService<ISqlGenerationHelper,NpgsqlSqlGenerationLowercaseHelper>();
|
||||||
$"Server={MySqlServer};Database={MySqlDatabase};User={MySqlUser};Password={MySqlPassword};",
|
|
||||||
new MariaDbServerVersion(new Version(10, 1)));
|
|
||||||
});
|
});
|
||||||
|
|
||||||
var key = Encoding.ASCII.GetBytes(this.Configuration["JWTTokenSecret"]);
|
var key = Encoding.ASCII.GetBytes(this.Configuration["JWTTokenSecret"]);
|
||||||
@@ -106,6 +106,18 @@ namespace LaDOSE.Api
|
|||||||
{
|
{
|
||||||
// return unauthorized if user no longer exists
|
// return unauthorized if user no longer exists
|
||||||
context.Fail("Unauthorized");
|
context.Fail("Unauthorized");
|
||||||
|
return Task.CompletedTask;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Roles are attached here, from the database, rather than being
|
||||||
|
// signed into the token: a promotion or demotion then applies to
|
||||||
|
// the caller's very next request instead of waiting 16 minutes.
|
||||||
|
if (context.Principal.Identity is ClaimsIdentity identity)
|
||||||
|
{
|
||||||
|
foreach (var role in user.Names())
|
||||||
|
{
|
||||||
|
identity.AddClaim(new Claim(identity.RoleClaimType, role));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return Task.CompletedTask;
|
return Task.CompletedTask;
|
||||||
@@ -144,6 +156,13 @@ namespace LaDOSE.Api
|
|||||||
cfg.CreateMapTwoWay<Game, LaDOSE.DTO.GameDTO>();
|
cfg.CreateMapTwoWay<Game, LaDOSE.DTO.GameDTO>();
|
||||||
cfg.CreateMapTwoWay<Todo, LaDOSE.DTO.TodoDTO>();
|
cfg.CreateMapTwoWay<Todo, LaDOSE.DTO.TodoDTO>();
|
||||||
|
|
||||||
|
// Match statistics: plain POCO aggregates computed by StatisticsService,
|
||||||
|
// mapped by name (same pattern as TournamentsResult above).
|
||||||
|
cfg.CreateMap<MatchStats, LaDOSE.DTO.MatchStatsDTO>();
|
||||||
|
cfg.CreateMap<MatchCoverage, LaDOSE.DTO.MatchCoverageDTO>();
|
||||||
|
cfg.CreateMap<PlayerMatchStats, LaDOSE.DTO.PlayerMatchStatsDTO>();
|
||||||
|
cfg.CreateMap<HeadToHead, LaDOSE.DTO.HeadToHeadDTO>();
|
||||||
|
|
||||||
});
|
});
|
||||||
IMapper mapper = mapperConfig.CreateMapper();
|
IMapper mapper = mapperConfig.CreateMapper();
|
||||||
services.AddSingleton(mapper);
|
services.AddSingleton(mapper);
|
||||||
@@ -163,6 +182,7 @@ namespace LaDOSE.Api
|
|||||||
services.AddScoped<IBotEventService, BotEventService>();
|
services.AddScoped<IBotEventService, BotEventService>();
|
||||||
|
|
||||||
services.AddScoped<IPlayerService, PlayerService>();
|
services.AddScoped<IPlayerService, PlayerService>();
|
||||||
|
services.AddScoped<IStatisticsService, StatisticsService>();
|
||||||
services.AddTransient<IChallongeProvider>(p => new ChallongeProvider( p.GetRequiredService<IGameService>(),
|
services.AddTransient<IChallongeProvider>(p => new ChallongeProvider( p.GetRequiredService<IGameService>(),
|
||||||
p.GetRequiredService<IEventService>(),
|
p.GetRequiredService<IEventService>(),
|
||||||
p.GetRequiredService<IPlayerService>(),
|
p.GetRequiredService<IPlayerService>(),
|
||||||
@@ -178,7 +198,7 @@ namespace LaDOSE.Api
|
|||||||
|
|
||||||
|
|
||||||
// This method gets called by the runtime. Use this method to configure the HTTP request pipeline.
|
// This method gets called by the runtime. Use this method to configure the HTTP request pipeline.
|
||||||
public void Configure(IApplicationBuilder app, IHostingEnvironment env, ILoggerFactory loggerFactory)
|
public void Configure(IApplicationBuilder app, IHostEnvironment env, ILoggerFactory loggerFactory)
|
||||||
{
|
{
|
||||||
//loggerFactory.AddConsole(Configuration.GetSection("Logging"));
|
//loggerFactory.AddConsole(Configuration.GetSection("Logging"));
|
||||||
//loggerFactory.AddDebug();
|
//loggerFactory.AddDebug();
|
||||||
@@ -199,7 +219,17 @@ namespace LaDOSE.Api
|
|||||||
app.UseRouting();
|
app.UseRouting();
|
||||||
app.UseAuthentication();
|
app.UseAuthentication();
|
||||||
app.UseAuthorization();
|
app.UseAuthorization();
|
||||||
app.UseEndpoints(x => x.MapControllers());
|
app.UseEndpoints(x =>
|
||||||
|
{
|
||||||
|
x.MapControllers();
|
||||||
|
#if DEBUG
|
||||||
|
if (env.IsDevelopment())
|
||||||
|
{
|
||||||
|
x.MapOpenApi();
|
||||||
|
x.MapScalarApiReference();
|
||||||
|
}
|
||||||
|
#endif
|
||||||
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,20 +4,18 @@
|
|||||||
"Default": "Warning"
|
"Default": "Warning"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"ConnectionStrings": {
|
||||||
|
"DbContext":"Host=kafka.local;Username=tom;Password=tom;Database=ladoseapi"
|
||||||
|
},
|
||||||
"CertificateSettings": {
|
"CertificateSettings": {
|
||||||
"fileName": "localhost.pfx",
|
"fileName": "localhost.pfx",
|
||||||
"password": "YourSecurePassword"
|
"password": "YourSecurePassword"
|
||||||
},
|
},
|
||||||
"MySql": {
|
|
||||||
"Server": "host",
|
|
||||||
"Database": "database",
|
|
||||||
"User": "User",
|
|
||||||
"Password": "Password"
|
|
||||||
},
|
|
||||||
"ApiKey": {
|
"ApiKey": {
|
||||||
"ChallongeApiKey": "Challonge ApiKey"
|
"ChallongeApiKey": "Challonge ApiKey",
|
||||||
|
"SmashApiKey": "Smash"
|
||||||
},
|
},
|
||||||
"AllowedHosts": "*",
|
"AllowedHosts": "0.0.0.0",
|
||||||
"Port": 5000,
|
"Port": 5000,
|
||||||
"JWTTokenSecret": "here goes the custom Secret key for authnetication"
|
"JWTTokenSecret": "here goes the custom Secret key for authnetication"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
|
|
||||||
using System;
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
|
||||||
namespace LaDOSE.DTO
|
namespace LaDOSE.DTO
|
||||||
{
|
{
|
||||||
@@ -9,8 +10,13 @@ namespace LaDOSE.DTO
|
|||||||
public string FirstName { get; set; }
|
public string FirstName { get; set; }
|
||||||
public string LastName { get; set; }
|
public string LastName { get; set; }
|
||||||
public string Username { get; set; }
|
public string Username { get; set; }
|
||||||
|
|
||||||
|
/// <summary>Only ever read from a request; never populated on a response.</summary>
|
||||||
public string Password { get; set; }
|
public string Password { get; set; }
|
||||||
|
|
||||||
|
/// <summary>Role names held by the user, e.g. <c>["Admin"]</c>. Empty means a plain user.</summary>
|
||||||
|
public List<string> Roles { get; set; }
|
||||||
|
|
||||||
public string Token { get; set; }
|
public string Token { get; set; }
|
||||||
public DateTime Expire { get; set; }
|
public DateTime Expire { get; set; }
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,8 +1,13 @@
|
|||||||
namespace LaDOSE.DTO
|
using System;
|
||||||
|
|
||||||
|
namespace LaDOSE.DTO
|
||||||
{
|
{
|
||||||
public class EventDTO
|
public class EventDTO
|
||||||
{
|
{
|
||||||
public int Id { get; set; }
|
public int Id { get; set; }
|
||||||
public string Name { get; set; }
|
public string Name { get; set; }
|
||||||
|
|
||||||
|
/// <summary>Event date, mapped by convention from Event.Date. Used for time-series charts.</summary>
|
||||||
|
public DateTime Date { get; set; }
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
@@ -1,8 +1,12 @@
|
|||||||
<Project Sdk="Microsoft.NET.Sdk">
|
<Project Sdk="Microsoft.NET.Sdk">
|
||||||
|
|
||||||
<PropertyGroup>
|
<PropertyGroup>
|
||||||
<TargetFramework>net6.0</TargetFramework>
|
<TargetFramework>net9.0</TargetFramework>
|
||||||
<Platforms>AnyCPU;x64</Platforms>
|
<Platforms>AnyCPU;x64</Platforms>
|
||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<PackageReference Include="Newtonsoft.Json" Version="13.0.3" />
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
</Project>
|
</Project>
|
||||||
|
|||||||
@@ -0,0 +1,55 @@
|
|||||||
|
using System.Collections.Generic;
|
||||||
|
|
||||||
|
namespace LaDOSE.DTO
|
||||||
|
{
|
||||||
|
public class MatchStatsDTO
|
||||||
|
{
|
||||||
|
public MatchCoverageDTO Coverage { get; set; }
|
||||||
|
public List<PlayerMatchStatsDTO> Players { get; set; }
|
||||||
|
public List<HeadToHeadDTO> HeadToHead { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
public class MatchCoverageDTO
|
||||||
|
{
|
||||||
|
/// <summary>Requested event ids that actually exist.</summary>
|
||||||
|
public int Events { get; set; }
|
||||||
|
|
||||||
|
/// <summary>Tournaments (brackets) belonging to those events.</summary>
|
||||||
|
public int Brackets { get; set; }
|
||||||
|
|
||||||
|
/// <summary>Of those brackets, how many have at least one set row.</summary>
|
||||||
|
public int BracketsWithSets { get; set; }
|
||||||
|
|
||||||
|
/// <summary>Total set rows in scope, including unusable ones.</summary>
|
||||||
|
public int Sets { get; set; }
|
||||||
|
|
||||||
|
/// <summary>Sets with a determinable winner.</summary>
|
||||||
|
public int DecidedSets { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
public class PlayerMatchStatsDTO
|
||||||
|
{
|
||||||
|
public int PlayerId { get; set; }
|
||||||
|
|
||||||
|
/// <summary>Gamertag, falling back to Name, else "#<id>".</summary>
|
||||||
|
public string Player { get; set; }
|
||||||
|
|
||||||
|
/// <summary>Decided sets only. Always equals Wins + Losses.</summary>
|
||||||
|
public int Sets { get; set; }
|
||||||
|
|
||||||
|
public int Wins { get; set; }
|
||||||
|
public int Losses { get; set; }
|
||||||
|
public int GamesWon { get; set; }
|
||||||
|
public int GamesLost { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
public class HeadToHeadDTO
|
||||||
|
{
|
||||||
|
public int PlayerAId { get; set; }
|
||||||
|
public string PlayerA { get; set; }
|
||||||
|
public int PlayerBId { get; set; }
|
||||||
|
public string PlayerB { get; set; }
|
||||||
|
public int WinsA { get; set; }
|
||||||
|
public int WinsB { get; set; }
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -27,6 +27,8 @@ namespace LaDOSE.DTO
|
|||||||
public List<GameDTO> Games { get; set; }
|
public List<GameDTO> Games { get; set; }
|
||||||
|
|
||||||
public List<ResultDTO> Results { get; set; }
|
public List<ResultDTO> Results { get; set; }
|
||||||
|
|
||||||
|
public string Slug { get; set; }
|
||||||
}
|
}
|
||||||
public class ResultDTO
|
public class ResultDTO
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -0,0 +1,11 @@
|
|||||||
|
<Application xmlns="https://github.com/avaloniaui"
|
||||||
|
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||||
|
x:Class="LaDOSE.DesktopApp.Avalonia.App"
|
||||||
|
xmlns:local="using:LaDOSE.DesktopApp.Avalonia"
|
||||||
|
RequestedThemeVariant="Dark">
|
||||||
|
<!-- "Default" ThemeVariant follows system theme variant. "Dark" or "Light" are other available options. -->
|
||||||
|
<Application.Styles>
|
||||||
|
<FluentTheme />
|
||||||
|
<StyleInclude Source="avares://Avalonia.Controls.DataGrid/Themes/Fluent.xaml"/>
|
||||||
|
</Application.Styles>
|
||||||
|
</Application>
|
||||||
@@ -0,0 +1,39 @@
|
|||||||
|
using Avalonia;
|
||||||
|
using Avalonia.Controls.ApplicationLifetimes;
|
||||||
|
using Avalonia.Markup.Xaml;
|
||||||
|
using LaDOSE.DesktopApp.Avalonia.ViewModels;
|
||||||
|
using LaDOSE.DesktopApp.Avalonia.Views;
|
||||||
|
using LaDOSE.REST;
|
||||||
|
using MsBox.Avalonia;
|
||||||
|
using ReactiveUI;
|
||||||
|
using Splat;
|
||||||
|
|
||||||
|
namespace LaDOSE.DesktopApp.Avalonia;
|
||||||
|
|
||||||
|
public partial class App : Application
|
||||||
|
{
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
public override void Initialize()
|
||||||
|
{
|
||||||
|
AvaloniaXamlLoader.Load(this);
|
||||||
|
Locator.CurrentMutable.Register(() => new GamesView(), typeof(IViewFor<GamesViewModel>));
|
||||||
|
Locator.CurrentMutable.Register(() => new InfoView(), typeof(IViewFor<InfoViewModel>));
|
||||||
|
Locator.CurrentMutable.Register(() => new TournamentResultView(), typeof(IViewFor<TournamentResultViewModel>));
|
||||||
|
}
|
||||||
|
|
||||||
|
public override void OnFrameworkInitializationCompleted()
|
||||||
|
{
|
||||||
|
if (ApplicationLifetime is IClassicDesktopStyleApplicationLifetime desktop)
|
||||||
|
{
|
||||||
|
desktop.MainWindow = new MainWindow
|
||||||
|
{
|
||||||
|
DataContext = new MainWindowViewModel(),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
base.OnFrameworkInitializationCompleted();
|
||||||
|
}
|
||||||
|
}
|
||||||
Binary file not shown.
|
After Width: | Height: | Size: 172 KiB |
@@ -0,0 +1,44 @@
|
|||||||
|
<Project Sdk="Microsoft.NET.Sdk">
|
||||||
|
<PropertyGroup>
|
||||||
|
<OutputType>WinExe</OutputType>
|
||||||
|
<TargetFramework>net9.0</TargetFramework>
|
||||||
|
<Nullable>enable</Nullable>
|
||||||
|
<BuiltInComInteropSupport>true</BuiltInComInteropSupport>
|
||||||
|
<ApplicationManifest>app.manifest</ApplicationManifest>
|
||||||
|
<AvaloniaUseCompiledBindingsByDefault>true</AvaloniaUseCompiledBindingsByDefault>
|
||||||
|
<LangVersion>12</LangVersion>
|
||||||
|
</PropertyGroup>
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<Folder Include="Models\"/>
|
||||||
|
<AvaloniaResource Include="Assets\**"/>
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<PackageReference Include="Avalonia" Version="11.2.3" />
|
||||||
|
<PackageReference Include="Avalonia.Controls.DataGrid" Version="11.2.3" />
|
||||||
|
<PackageReference Include="Avalonia.Desktop" Version="11.2.3" />
|
||||||
|
<PackageReference Include="Avalonia.Themes.Fluent" Version="11.2.3" />
|
||||||
|
<PackageReference Include="Avalonia.Fonts.Inter" Version="11.2.3" />
|
||||||
|
<!--Condition below is needed to remove Avalonia.Diagnostics package from build output in Release configuration.-->
|
||||||
|
<PackageReference Condition="'$(Configuration)' == 'Debug'" Include="Avalonia.Diagnostics" Version="11.2.3" />
|
||||||
|
<PackageReference Include="Avalonia.ReactiveUI" Version="11.2.3" />
|
||||||
|
<PackageReference Include="MessageBox.Avalonia" Version="3.2.0" />
|
||||||
|
<PackageReference Include="Microsoft.Extensions.Configuration" Version="8.0.0" />
|
||||||
|
<PackageReference Include="Microsoft.Extensions.Configuration.Json" Version="8.0.1" />
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<ProjectReference Include="..\LaDOSE.DTO\LaDOSE.DTO.csproj" />
|
||||||
|
<ProjectReference Include="..\LaDOSE.REST\LaDOSE.REST.csproj" />
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<None Update="settings.json">
|
||||||
|
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
|
||||||
|
</None>
|
||||||
|
</ItemGroup>
|
||||||
|
</Project>
|
||||||
@@ -0,0 +1,67 @@
|
|||||||
|
using Avalonia;
|
||||||
|
using Avalonia.ReactiveUI;
|
||||||
|
using System;
|
||||||
|
using System.ComponentModel;
|
||||||
|
using System.IO;
|
||||||
|
using LaDOSE.REST;
|
||||||
|
using Microsoft.Extensions.Configuration;
|
||||||
|
using MsBox.Avalonia;
|
||||||
|
using MsBox.Avalonia.Enums;
|
||||||
|
using Splat;
|
||||||
|
// using Xilium.CefGlue;
|
||||||
|
// using Xilium.CefGlue.Common;
|
||||||
|
// using Avalonia.Visuals;
|
||||||
|
namespace LaDOSE.DesktopApp.Avalonia;
|
||||||
|
|
||||||
|
sealed class Program
|
||||||
|
{
|
||||||
|
|
||||||
|
|
||||||
|
// Initialization code. Don't use any Avalonia, third-party APIs or any
|
||||||
|
// SynchronizationContext-reliant code before AppMain is called: things aren't initialized
|
||||||
|
// yet and stuff might break.
|
||||||
|
[STAThread]
|
||||||
|
public static void Main(string[] args)
|
||||||
|
{
|
||||||
|
RegisterDependencies(Locator.CurrentMutable, Locator.Current);
|
||||||
|
|
||||||
|
var app = BuildAvaloniaApp();
|
||||||
|
app.StartWithClassicDesktopLifetime(args);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void RegisterDependencies(IMutableDependencyResolver currentMutable, IReadonlyDependencyResolver current)
|
||||||
|
{
|
||||||
|
|
||||||
|
var builder = new ConfigurationBuilder()
|
||||||
|
.AddJsonFile("settings.json", optional: true, reloadOnChange: true).Build();
|
||||||
|
var restUrl = builder["REST:Url"].ToString();
|
||||||
|
var restUser = builder["REST:User"].ToString();
|
||||||
|
var restPassword = builder["REST:Password"].ToString();
|
||||||
|
|
||||||
|
currentMutable.Register<RestService>(() =>
|
||||||
|
{
|
||||||
|
var restService = new RestService(new Uri(restUrl), restUser, restPassword);
|
||||||
|
try
|
||||||
|
{
|
||||||
|
restService.Connect(new Uri(restUrl), restUser, restPassword);
|
||||||
|
}
|
||||||
|
catch (Exception e)
|
||||||
|
{
|
||||||
|
Console.WriteLine(e);
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
return restService;
|
||||||
|
});
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
// Avalonia configuration, don't remove; also used by visual designer.
|
||||||
|
public static AppBuilder BuildAvaloniaApp()
|
||||||
|
=> AppBuilder
|
||||||
|
.Configure<App>()
|
||||||
|
.UsePlatformDetect()
|
||||||
|
.WithInterFont()
|
||||||
|
.LogToTrace()
|
||||||
|
.UseReactiveUI();
|
||||||
|
}
|
||||||
Binary file not shown.
|
After Width: | Height: | Size: 8.1 KiB |
@@ -0,0 +1,343 @@
|
|||||||
|
body {
|
||||||
|
color: #efefef;
|
||||||
|
background-color: #141415;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
:root {
|
||||||
|
--breakpoint-xs: 0;
|
||||||
|
--breakpoint-sm: 576px;
|
||||||
|
--breakpoint-md: 768px;
|
||||||
|
--breakpoint-lg: 992px;
|
||||||
|
--breakpoint-xl: 1200px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.table {
|
||||||
|
width: 100%;
|
||||||
|
margin-bottom: 1rem;
|
||||||
|
background-color: transparent;
|
||||||
|
}
|
||||||
|
|
||||||
|
.table th,
|
||||||
|
.table td {
|
||||||
|
padding: 0.75rem;
|
||||||
|
vertical-align: top;
|
||||||
|
border-top: 1px solid #dee2e6;
|
||||||
|
}
|
||||||
|
|
||||||
|
.table thead th {
|
||||||
|
vertical-align: bottom;
|
||||||
|
border-bottom: 2px solid #dee2e6;
|
||||||
|
}
|
||||||
|
|
||||||
|
.table tbody + tbody {
|
||||||
|
border-top: 2px solid #dee2e6;
|
||||||
|
}
|
||||||
|
|
||||||
|
.table .table {
|
||||||
|
background-color: #fff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.table-sm th,
|
||||||
|
.table-sm td {
|
||||||
|
padding: 0.3rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.table-bordered {
|
||||||
|
border: 1px solid #dee2e6;
|
||||||
|
}
|
||||||
|
|
||||||
|
.table-bordered th,
|
||||||
|
.table-bordered td {
|
||||||
|
border: 1px solid #dee2e6;
|
||||||
|
}
|
||||||
|
|
||||||
|
.table-bordered thead th,
|
||||||
|
.table-bordered thead td {
|
||||||
|
border-bottom-width: 2px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.table-borderless th,
|
||||||
|
.table-borderless td,
|
||||||
|
.table-borderless thead th,
|
||||||
|
.table-borderless tbody + tbody {
|
||||||
|
border: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.table-striped tbody tr:nth-of-type(odd) {
|
||||||
|
background-color: rgba(0, 0, 0, 0.05);
|
||||||
|
}
|
||||||
|
|
||||||
|
.table-hover tbody tr:hover {
|
||||||
|
background-color: rgba(0, 0, 0, 0.075);
|
||||||
|
}
|
||||||
|
|
||||||
|
.table-primary,
|
||||||
|
.table-primary > th,
|
||||||
|
.table-primary > td {
|
||||||
|
background-color: #b8daff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.table-hover .table-primary:hover {
|
||||||
|
background-color: #9fcdff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.table-hover .table-primary:hover > td,
|
||||||
|
.table-hover .table-primary:hover > th {
|
||||||
|
background-color: #9fcdff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.table-secondary,
|
||||||
|
.table-secondary > th,
|
||||||
|
.table-secondary > td {
|
||||||
|
background-color: #d6d8db;
|
||||||
|
}
|
||||||
|
|
||||||
|
.table-hover .table-secondary:hover {
|
||||||
|
background-color: #c8cbcf;
|
||||||
|
}
|
||||||
|
|
||||||
|
.table-hover .table-secondary:hover > td,
|
||||||
|
.table-hover .table-secondary:hover > th {
|
||||||
|
background-color: #c8cbcf;
|
||||||
|
}
|
||||||
|
|
||||||
|
.table-success,
|
||||||
|
.table-success > th,
|
||||||
|
.table-success > td {
|
||||||
|
background-color: #c3e6cb;
|
||||||
|
}
|
||||||
|
|
||||||
|
.table-hover .table-success:hover {
|
||||||
|
background-color: #b1dfbb;
|
||||||
|
}
|
||||||
|
|
||||||
|
.table-hover .table-success:hover > td,
|
||||||
|
.table-hover .table-success:hover > th {
|
||||||
|
background-color: #b1dfbb;
|
||||||
|
}
|
||||||
|
|
||||||
|
.table-info,
|
||||||
|
.table-info > th,
|
||||||
|
.table-info > td {
|
||||||
|
background-color: #bee5eb;
|
||||||
|
}
|
||||||
|
|
||||||
|
.table-hover .table-info:hover {
|
||||||
|
background-color: #abdde5;
|
||||||
|
}
|
||||||
|
|
||||||
|
.table-hover .table-info:hover > td,
|
||||||
|
.table-hover .table-info:hover > th {
|
||||||
|
background-color: #abdde5;
|
||||||
|
}
|
||||||
|
|
||||||
|
.table-warning,
|
||||||
|
.table-warning > th,
|
||||||
|
.table-warning > td {
|
||||||
|
background-color: #ffeeba;
|
||||||
|
}
|
||||||
|
|
||||||
|
.table-hover .table-warning:hover {
|
||||||
|
background-color: #ffe8a1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.table-hover .table-warning:hover > td,
|
||||||
|
.table-hover .table-warning:hover > th {
|
||||||
|
background-color: #ffe8a1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.table-danger,
|
||||||
|
.table-danger > th,
|
||||||
|
.table-danger > td {
|
||||||
|
background-color: #f5c6cb;
|
||||||
|
}
|
||||||
|
|
||||||
|
.table-hover .table-danger:hover {
|
||||||
|
background-color: #f1b0b7;
|
||||||
|
}
|
||||||
|
|
||||||
|
.table-hover .table-danger:hover > td,
|
||||||
|
.table-hover .table-danger:hover > th {
|
||||||
|
background-color: #f1b0b7;
|
||||||
|
}
|
||||||
|
|
||||||
|
.table-light,
|
||||||
|
.table-light > th,
|
||||||
|
.table-light > td {
|
||||||
|
background-color: #fdfdfe;
|
||||||
|
}
|
||||||
|
|
||||||
|
.table-hover .table-light:hover {
|
||||||
|
background-color: #ececf6;
|
||||||
|
}
|
||||||
|
|
||||||
|
.table-hover .table-light:hover > td,
|
||||||
|
.table-hover .table-light:hover > th {
|
||||||
|
background-color: #ececf6;
|
||||||
|
}
|
||||||
|
|
||||||
|
.table-dark,
|
||||||
|
.table-dark > th,
|
||||||
|
.table-dark > td {
|
||||||
|
background-color: #c6c8ca;
|
||||||
|
}
|
||||||
|
|
||||||
|
.table-hover .table-dark:hover {
|
||||||
|
background-color: #b9bbbe;
|
||||||
|
}
|
||||||
|
|
||||||
|
.table-hover .table-dark:hover > td,
|
||||||
|
.table-hover .table-dark:hover > th {
|
||||||
|
background-color: #b9bbbe;
|
||||||
|
}
|
||||||
|
|
||||||
|
.table-active,
|
||||||
|
.table-active > th,
|
||||||
|
.table-active > td {
|
||||||
|
background-color: rgba(0, 0, 0, 0.075);
|
||||||
|
}
|
||||||
|
|
||||||
|
.table-hover .table-active:hover {
|
||||||
|
background-color: rgba(0, 0, 0, 0.075);
|
||||||
|
}
|
||||||
|
|
||||||
|
.table-hover .table-active:hover > td,
|
||||||
|
.table-hover .table-active:hover > th {
|
||||||
|
background-color: rgba(0, 0, 0, 0.075);
|
||||||
|
}
|
||||||
|
|
||||||
|
.table .thead-dark th {
|
||||||
|
color: #fff;
|
||||||
|
background-color: #212529;
|
||||||
|
border-color: #32383e;
|
||||||
|
}
|
||||||
|
|
||||||
|
.table .thead-light th {
|
||||||
|
color: #495057;
|
||||||
|
background-color: #e9ecef;
|
||||||
|
border-color: #dee2e6;
|
||||||
|
}
|
||||||
|
|
||||||
|
.table-dark {
|
||||||
|
color: #fff;
|
||||||
|
background-color: #212529;
|
||||||
|
}
|
||||||
|
|
||||||
|
.table-dark th,
|
||||||
|
.table-dark td,
|
||||||
|
.table-dark thead th {
|
||||||
|
border-color: #32383e;
|
||||||
|
}
|
||||||
|
|
||||||
|
.table-dark.table-bordered {
|
||||||
|
border: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.table-dark.table-striped tbody tr:nth-of-type(odd) {
|
||||||
|
background-color: rgba(255, 255, 255, 0.05);
|
||||||
|
}
|
||||||
|
|
||||||
|
.table-dark.table-hover tbody tr:hover {
|
||||||
|
background-color: rgba(255, 255, 255, 0.075);
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 575.98px) {
|
||||||
|
.table-responsive-sm {
|
||||||
|
display: block;
|
||||||
|
width: 100%;
|
||||||
|
overflow-x: auto;
|
||||||
|
-webkit-overflow-scrolling: touch;
|
||||||
|
-ms-overflow-style: -ms-autohiding-scrollbar;
|
||||||
|
}
|
||||||
|
|
||||||
|
.table-responsive-sm > .table-bordered {
|
||||||
|
border: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 767.98px) {
|
||||||
|
.table-responsive-md {
|
||||||
|
display: block;
|
||||||
|
width: 100%;
|
||||||
|
overflow-x: auto;
|
||||||
|
-webkit-overflow-scrolling: touch;
|
||||||
|
-ms-overflow-style: -ms-autohiding-scrollbar;
|
||||||
|
}
|
||||||
|
|
||||||
|
.table-responsive-md > .table-bordered {
|
||||||
|
border: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 991.98px) {
|
||||||
|
.table-responsive-lg {
|
||||||
|
display: block;
|
||||||
|
width: 100%;
|
||||||
|
overflow-x: auto;
|
||||||
|
-webkit-overflow-scrolling: touch;
|
||||||
|
-ms-overflow-style: -ms-autohiding-scrollbar;
|
||||||
|
}
|
||||||
|
|
||||||
|
.table-responsive-lg > .table-bordered {
|
||||||
|
border: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 1199.98px) {
|
||||||
|
.table-responsive-xl {
|
||||||
|
display: block;
|
||||||
|
width: 100%;
|
||||||
|
overflow-x: auto;
|
||||||
|
-webkit-overflow-scrolling: touch;
|
||||||
|
-ms-overflow-style: -ms-autohiding-scrollbar;
|
||||||
|
}
|
||||||
|
|
||||||
|
.table-responsive-xl > .table-bordered {
|
||||||
|
border: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.table-responsive {
|
||||||
|
display: block;
|
||||||
|
width: 100%;
|
||||||
|
overflow-x: auto;
|
||||||
|
-webkit-overflow-scrolling: touch;
|
||||||
|
-ms-overflow-style: -ms-autohiding-scrollbar;
|
||||||
|
}
|
||||||
|
|
||||||
|
.table-responsive > .table-bordered {
|
||||||
|
border: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.table {
|
||||||
|
border-collapse: collapse !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.table td,
|
||||||
|
.table th {
|
||||||
|
/*background-color: #fff !important;*/
|
||||||
|
}
|
||||||
|
|
||||||
|
.table-bordered th,
|
||||||
|
.table-bordered td {
|
||||||
|
border: 1px solid #dee2e6 !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.table-dark {
|
||||||
|
color: #fff;
|
||||||
|
background-color: #212529
|
||||||
|
}
|
||||||
|
|
||||||
|
.table-dark th,
|
||||||
|
.table-dark td,
|
||||||
|
.table-dark thead th,
|
||||||
|
.table-dark tbody + tbody {
|
||||||
|
border-color: #dee2e6;
|
||||||
|
}
|
||||||
|
|
||||||
|
.table .thead-dark th {
|
||||||
|
color: inherit;
|
||||||
|
border-color: #dee2e6;
|
||||||
|
}
|
||||||
@@ -0,0 +1,24 @@
|
|||||||
|
using System.ComponentModel;
|
||||||
|
using System.Runtime.CompilerServices;
|
||||||
|
using ReactiveUI;
|
||||||
|
|
||||||
|
namespace LaDOSE.DesktopApp.Avalonia.Utils;
|
||||||
|
|
||||||
|
public abstract class BaseViewModel : ReactiveObject, IRoutableViewModel,INotifyPropertyChanged
|
||||||
|
{
|
||||||
|
|
||||||
|
public event PropertyChangedEventHandler? PropertyChanged;
|
||||||
|
protected void RaisePropertyChanged([CallerMemberName] string? propertyName = null)
|
||||||
|
{
|
||||||
|
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
|
||||||
|
}
|
||||||
|
|
||||||
|
protected BaseViewModel(IScreen hostScreen, string? urlPathSegment)
|
||||||
|
{
|
||||||
|
UrlPathSegment = urlPathSegment;
|
||||||
|
HostScreen = hostScreen;
|
||||||
|
}
|
||||||
|
|
||||||
|
public string? UrlPathSegment { get; }
|
||||||
|
public IScreen HostScreen { get; }
|
||||||
|
}
|
||||||
+3
-3
@@ -1,7 +1,7 @@
|
|||||||
using System;
|
using System;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
|
|
||||||
namespace LaDOSE.DesktopApp.Utils
|
namespace LaDOSE.DesktopApp.Avalonia.Utils
|
||||||
{
|
{
|
||||||
public static class CustomListExtension
|
public static class CustomListExtension
|
||||||
{
|
{
|
||||||
@@ -14,14 +14,14 @@ namespace LaDOSE.DesktopApp.Utils
|
|||||||
_compare = c;
|
_compare = c;
|
||||||
}
|
}
|
||||||
|
|
||||||
public bool Equals(T x, T y)
|
public bool Equals(T? x, T? y)
|
||||||
{
|
{
|
||||||
return _compare(x, y);
|
return _compare(x, y);
|
||||||
}
|
}
|
||||||
|
|
||||||
public int GetHashCode(T obj)
|
public int GetHashCode(T obj)
|
||||||
{
|
{
|
||||||
return 0;
|
return obj.GetHashCode();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -0,0 +1,101 @@
|
|||||||
|
using System.Collections.Generic;
|
||||||
|
using System.ComponentModel;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Runtime.CompilerServices;
|
||||||
|
using LaDOSE.DesktopApp.Avalonia.Utils;
|
||||||
|
using LaDOSE.DesktopApp.Avalonia.ViewModels;
|
||||||
|
using LaDOSE.DTO;
|
||||||
|
using LaDOSE.REST;
|
||||||
|
using ReactiveUI;
|
||||||
|
using Splat;
|
||||||
|
|
||||||
|
namespace LaDOSE.DesktopApp.Avalonia.ViewModels
|
||||||
|
{
|
||||||
|
public class GamesViewModel : BaseViewModel
|
||||||
|
{
|
||||||
|
|
||||||
|
public string DisplayName => "Games";
|
||||||
|
|
||||||
|
private GameDTO _currentGame;
|
||||||
|
private List<GameDTO> _games;
|
||||||
|
private List<GameDTO> _Searchgames;
|
||||||
|
private RestService RestService { get; set; }
|
||||||
|
public GamesViewModel(IScreen screen): base(screen,"Games")
|
||||||
|
{
|
||||||
|
this.RestService = Locator.Current.GetService<RestService>();
|
||||||
|
this.Games=new List<GameDTO>();
|
||||||
|
OnInitialize();
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
void OnInitialize()
|
||||||
|
{
|
||||||
|
LoadGames();
|
||||||
|
this.CurrentGame = Games.First();
|
||||||
|
}
|
||||||
|
|
||||||
|
public void LoadGames()
|
||||||
|
{
|
||||||
|
var gameDtos = this.RestService.GetGames().OrderBy(e=>e.Order).ToList();
|
||||||
|
this.Games = gameDtos;
|
||||||
|
RaisePropertyChanged(nameof(this.Games));
|
||||||
|
}
|
||||||
|
|
||||||
|
public List<GameDTO> Games
|
||||||
|
{
|
||||||
|
get => _games;
|
||||||
|
set
|
||||||
|
{
|
||||||
|
_games = value;
|
||||||
|
RaisePropertyChanged(nameof(this.Games));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public List<GameDTO> SearchGame
|
||||||
|
{
|
||||||
|
get => _Searchgames;
|
||||||
|
set
|
||||||
|
{
|
||||||
|
_Searchgames = value;
|
||||||
|
RaisePropertyChanged(nameof(this.SearchGame));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
public GameDTO CurrentGame
|
||||||
|
{
|
||||||
|
get => _currentGame;
|
||||||
|
set
|
||||||
|
{
|
||||||
|
_currentGame = value;
|
||||||
|
RaisePropertyChanged(nameof(this.CurrentGame));
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Update()
|
||||||
|
{
|
||||||
|
this.RestService.UpdateGame(this.CurrentGame);
|
||||||
|
LoadGames();
|
||||||
|
|
||||||
|
}
|
||||||
|
public void AddGame()
|
||||||
|
{
|
||||||
|
var item = new GameDTO();
|
||||||
|
this.RestService.UpdateGame(item);
|
||||||
|
LoadGames();
|
||||||
|
}
|
||||||
|
public void DeleteGame()
|
||||||
|
{
|
||||||
|
|
||||||
|
this.RestService.DeleteGame(this.CurrentGame.Id);
|
||||||
|
LoadGames();
|
||||||
|
}
|
||||||
|
|
||||||
|
public void GetGame()
|
||||||
|
{
|
||||||
|
SearchGame = this.RestService.GetSmashGames(this.CurrentGame.LongName);
|
||||||
|
}
|
||||||
|
public bool CanDeleteGame => CurrentGame != null;
|
||||||
|
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,16 @@
|
|||||||
|
using System.ComponentModel;
|
||||||
|
using ReactiveUI;
|
||||||
|
|
||||||
|
namespace LaDOSE.DesktopApp.Avalonia.ViewModels;
|
||||||
|
|
||||||
|
public class InfoViewModel: ReactiveObject, IRoutableViewModel,INotifyPropertyChanged
|
||||||
|
{
|
||||||
|
|
||||||
|
public InfoViewModel(IScreen screen)
|
||||||
|
{
|
||||||
|
HostScreen = screen;
|
||||||
|
}
|
||||||
|
|
||||||
|
public string? UrlPathSegment => "Info";
|
||||||
|
public IScreen HostScreen { get; }
|
||||||
|
}
|
||||||
@@ -0,0 +1,16 @@
|
|||||||
|
using Avalonia;
|
||||||
|
using Avalonia.Controls;
|
||||||
|
using Avalonia.Controls.ApplicationLifetimes;
|
||||||
|
|
||||||
|
namespace LaDOSE.DesktopApp.Avalonia.ViewModels;
|
||||||
|
|
||||||
|
public class MainWindowViewModel : Window
|
||||||
|
{
|
||||||
|
public RoutedViewHostViewModel RoutedViewViewHost { get; } = new();
|
||||||
|
|
||||||
|
public void CloseApp()
|
||||||
|
{
|
||||||
|
if (Application.Current != null && Application.Current.ApplicationLifetime is IClassicDesktopStyleApplicationLifetime)
|
||||||
|
((Application.Current.ApplicationLifetime as IClassicDesktopStyleApplicationLifetime)!).Shutdown();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,61 @@
|
|||||||
|
using System.ComponentModel;
|
||||||
|
using System.Runtime.CompilerServices;
|
||||||
|
using LaDOSE.DesktopApp.Avalonia.Utils;
|
||||||
|
using ReactiveUI;
|
||||||
|
|
||||||
|
namespace LaDOSE.DesktopApp.Avalonia.ViewModels;
|
||||||
|
|
||||||
|
public class RoutedViewHostViewModel : ReactiveObject, IScreen, INotifyPropertyChanged
|
||||||
|
{
|
||||||
|
public event PropertyChangedEventHandler? PropertyChanged;
|
||||||
|
protected void RaisePropertyChanged([CallerMemberName] string? propertyName = null)
|
||||||
|
{
|
||||||
|
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
|
||||||
|
}
|
||||||
|
|
||||||
|
private string _current;
|
||||||
|
|
||||||
|
public RoutedViewHostViewModel()
|
||||||
|
{
|
||||||
|
Games = new GamesViewModel(this);
|
||||||
|
Info = new InfoViewModel(this);
|
||||||
|
Tournament = new TournamentResultViewModel(this);
|
||||||
|
Router.Navigate.Execute(Tournament);
|
||||||
|
Current = "Tournament";
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
public string Current
|
||||||
|
{
|
||||||
|
get => _current;
|
||||||
|
set
|
||||||
|
{
|
||||||
|
_current = value;
|
||||||
|
RaisePropertyChanged(nameof(Current));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public RoutingState Router { get; } = new();
|
||||||
|
public GamesViewModel Games { get; }
|
||||||
|
public InfoViewModel Info { get; }
|
||||||
|
|
||||||
|
public TournamentResultViewModel Tournament { get; }
|
||||||
|
|
||||||
|
|
||||||
|
public void ShowGames()
|
||||||
|
{
|
||||||
|
Router.Navigate.Execute(Games);
|
||||||
|
Current = "Games";
|
||||||
|
}
|
||||||
|
|
||||||
|
public void ShowInfo()
|
||||||
|
{
|
||||||
|
Router.Navigate.Execute(Info);
|
||||||
|
Current = "Info";
|
||||||
|
}
|
||||||
|
public void ShowTournament()
|
||||||
|
{
|
||||||
|
Router.Navigate.Execute(Tournament);
|
||||||
|
Current = "Tournament";
|
||||||
|
}
|
||||||
|
}
|
||||||
+151
-142
@@ -1,5 +1,4 @@
|
|||||||
using System;
|
using System;
|
||||||
using System.Collections;
|
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.Collections.ObjectModel;
|
using System.Collections.ObjectModel;
|
||||||
using System.Data;
|
using System.Data;
|
||||||
@@ -8,70 +7,64 @@ using System.Linq;
|
|||||||
using System.Reflection;
|
using System.Reflection;
|
||||||
using System.Text;
|
using System.Text;
|
||||||
using System.Text.RegularExpressions;
|
using System.Text.RegularExpressions;
|
||||||
using System.Windows.Controls;
|
using Avalonia.Collections;
|
||||||
using System.Windows.Forms;
|
using Avalonia.Controls;
|
||||||
using LaDOSE.DesktopApp.Utils;
|
using LaDOSE.DesktopApp.Avalonia.Utils;
|
||||||
using LaDOSE.DTO;
|
using LaDOSE.DTO;
|
||||||
using LaDOSE.REST;
|
using LaDOSE.REST;
|
||||||
using SaveFileDialog = Microsoft.Win32.SaveFileDialog;
|
using ReactiveUI;
|
||||||
using Screen = Caliburn.Micro.Screen;
|
using Splat;
|
||||||
|
|
||||||
namespace LaDOSE.DesktopApp.ViewModels
|
namespace LaDOSE.DesktopApp.Avalonia.ViewModels
|
||||||
{
|
{
|
||||||
public class TournamentResultViewModel : Screen
|
public class TournamentResultViewModel : BaseViewModel
|
||||||
{
|
{
|
||||||
public override string DisplayName => "Tournament Result";
|
public string DisplayName => "Tournament Result";
|
||||||
|
|
||||||
private RestService RestService { get; set; }
|
private RestService? RestService { get; set; }
|
||||||
//Dictionary<string, Dictionary<int, int>> _computedResult;
|
//Dictionary<string, Dictionary<int, int>> _computedResult;
|
||||||
|
|
||||||
#region Properties
|
#region Properties
|
||||||
|
|
||||||
private string css = string.Empty;
|
private string css = string.Empty;
|
||||||
|
|
||||||
//"strong { font-weight: 700;} ". +
|
private string? _selectRegex;
|
||||||
// "a { color: #ff9024;}"+
|
|
||||||
// "body { color: #efefef;background-color: #141415; }" +
|
|
||||||
// ""+
|
|
||||||
// "a:hover, .entry-meta span a:hover, .comments-link a:hover, body.coldisplay2 #front-columns a:active {color: #cb5920;}"+
|
|
||||||
// "tr td { border: 1px dashed #3D3D3D;} ";
|
|
||||||
private String _selectRegex;
|
|
||||||
|
|
||||||
public String SelectRegex
|
public string? SelectRegex
|
||||||
{
|
{
|
||||||
get { return _selectRegex; }
|
get { return _selectRegex; }
|
||||||
set
|
set
|
||||||
{
|
{
|
||||||
_selectRegex = value;
|
_selectRegex = value;
|
||||||
NotifyOfPropertyChange(() => SelectRegex);
|
RaisePropertyChanged(nameof(SelectRegex));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private String _selectEventRegex;
|
private string? _selectEventRegex;
|
||||||
|
|
||||||
public String SelectEventRegex
|
public string? SelectEventRegex
|
||||||
{
|
{
|
||||||
get { return _selectEventRegex; }
|
get { return _selectEventRegex; }
|
||||||
set
|
set
|
||||||
{
|
{
|
||||||
_selectEventRegex = value;
|
_selectEventRegex = value;
|
||||||
NotifyOfPropertyChange(() => SelectEventRegex);
|
RaisePropertyChanged(nameof(SelectEventRegex));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
private string _slug;
|
private string? _slug;
|
||||||
public String Slug
|
public string? Slug
|
||||||
{
|
{
|
||||||
get { return _slug; }
|
get { return _slug; }
|
||||||
set
|
set
|
||||||
{
|
{
|
||||||
_slug = value;
|
_slug = value;
|
||||||
NotifyOfPropertyChange(() => Slug);
|
RaisePropertyChanged(nameof(Slug));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private String _html;
|
private string? _html;
|
||||||
|
|
||||||
public String Html
|
public string? Html
|
||||||
{
|
{
|
||||||
get { return $"<html><head><style>{this.css}</style></head><body>{HtmlContent}</body></html>"; }
|
get { return $"<html><head><style>{this.css}</style></head><body>{HtmlContent}</body></html>"; }
|
||||||
set
|
set
|
||||||
@@ -79,58 +72,58 @@ namespace LaDOSE.DesktopApp.ViewModels
|
|||||||
_html = value;
|
_html = value;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
private String _htmlContent;
|
private string? _htmlContent;
|
||||||
|
|
||||||
public String HtmlContent
|
public string? HtmlContent
|
||||||
{
|
{
|
||||||
get { return _htmlContent; }
|
get { return _htmlContent; }
|
||||||
set
|
set
|
||||||
{
|
{
|
||||||
_htmlContent = value;
|
_htmlContent = value;
|
||||||
NotifyOfPropertyChange(() => HtmlContent);
|
RaisePropertyChanged(nameof(HtmlContent));
|
||||||
NotifyOfPropertyChange(() => Html);
|
RaisePropertyChanged(nameof(Html));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
private DateTime _from;
|
private DateTimeOffset _from;
|
||||||
|
|
||||||
public DateTime From
|
public DateTimeOffset From
|
||||||
{
|
{
|
||||||
get { return _from; }
|
get { return _from; }
|
||||||
set
|
set
|
||||||
{
|
{
|
||||||
_from = value;
|
_from = value;
|
||||||
NotifyOfPropertyChange(() => From);
|
RaisePropertyChanged(nameof(From));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private DateTime _to;
|
private DateTimeOffset _to;
|
||||||
|
|
||||||
public DateTime To
|
public DateTimeOffset To
|
||||||
{
|
{
|
||||||
get { return _to; }
|
get { return _to; }
|
||||||
set
|
set
|
||||||
{
|
{
|
||||||
_to = value;
|
_to = value;
|
||||||
NotifyOfPropertyChange(() => To);
|
RaisePropertyChanged(nameof(To));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
private TournamentsResultDTO _results;
|
private TournamentsResultDTO? _results;
|
||||||
public List<TournamentDTO> Tournaments { get; set; }
|
public List<TournamentDTO> Tournaments { get; set; }
|
||||||
|
|
||||||
public List<EventDTO> Events { get; set; }
|
public List<EventDTO> Events { get; set; }
|
||||||
|
|
||||||
public TournamentsResultDTO Results
|
public TournamentsResultDTO? Results
|
||||||
{
|
{
|
||||||
get => _results;
|
get => _results;
|
||||||
set
|
set
|
||||||
{
|
{
|
||||||
_results = value;
|
_results = value;
|
||||||
NotifyOfPropertyChange(() => Results);
|
RaisePropertyChanged(nameof(Results));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -142,7 +135,7 @@ namespace LaDOSE.DesktopApp.ViewModels
|
|||||||
set
|
set
|
||||||
{
|
{
|
||||||
_selectedEvents = value;
|
_selectedEvents = value;
|
||||||
NotifyOfPropertyChange(() => SelectedEvents);
|
RaisePropertyChanged(nameof(SelectedEvents));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -154,14 +147,14 @@ namespace LaDOSE.DesktopApp.ViewModels
|
|||||||
set
|
set
|
||||||
{
|
{
|
||||||
_selectedTournaments = value;
|
_selectedTournaments = value;
|
||||||
NotifyOfPropertyChange(() => SelectedTournaments);
|
RaisePropertyChanged(nameof(SelectedTournaments));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private GameDTO _selectedGame;
|
private GameDTO? _selectedGame;
|
||||||
|
|
||||||
|
|
||||||
public GameDTO SelectedGame
|
public GameDTO? SelectedGame
|
||||||
{
|
{
|
||||||
get { return _selectedGame; }
|
get { return _selectedGame; }
|
||||||
set
|
set
|
||||||
@@ -175,136 +168,151 @@ namespace LaDOSE.DesktopApp.ViewModels
|
|||||||
SelectedGameResult = new ObservableCollection<ResultDTO>(resultForGame);
|
SelectedGameResult = new ObservableCollection<ResultDTO>(resultForGame);
|
||||||
}
|
}
|
||||||
|
|
||||||
NotifyOfPropertyChange(() => SelectedGame);
|
RaisePropertyChanged(nameof(SelectedGame));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private ObservableCollection<ResultDTO> _selectedGameResult;
|
private ObservableCollection<ResultDTO>? _selectedGameResult;
|
||||||
|
|
||||||
public ObservableCollection<ResultDTO> SelectedGameResult
|
public ObservableCollection<ResultDTO>? SelectedGameResult
|
||||||
{
|
{
|
||||||
get { return _selectedGameResult; }
|
get { return _selectedGameResult; }
|
||||||
set
|
set
|
||||||
{
|
{
|
||||||
_selectedGameResult = value;
|
_selectedGameResult = value;
|
||||||
NotifyOfPropertyChange(() => SelectedGameResult);
|
RaisePropertyChanged(nameof(SelectedGameResult));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private String _first;
|
private string? _first;
|
||||||
private DataTable _gridDataTable;
|
private DataTable? _gridDataTable;
|
||||||
|
private string? _error;
|
||||||
|
|
||||||
public String First
|
public string? First
|
||||||
{
|
{
|
||||||
get { return _first; }
|
get { return _first; }
|
||||||
set
|
set
|
||||||
{
|
{
|
||||||
_first = value;
|
_first = value;
|
||||||
NotifyOfPropertyChange(() => First);
|
RaisePropertyChanged(nameof(First));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#endregion
|
#endregion
|
||||||
|
|
||||||
public TournamentResultViewModel(RestService restService)
|
public TournamentResultViewModel(IScreen hostScreen):base(hostScreen,"Tournament")
|
||||||
{
|
{
|
||||||
this.RestService = restService;
|
this.RestService = Locator.Current.GetService<RestService>();;
|
||||||
_selectedTournaments = new ObservableCollection<TournamentDTO>();
|
_selectedTournaments = new ObservableCollection<TournamentDTO>();
|
||||||
_selectedEvents = new ObservableCollection<EventDTO>();
|
_selectedEvents = new ObservableCollection<EventDTO>();
|
||||||
Tournaments = new List<TournamentDTO>();
|
Tournaments = new List<TournamentDTO>();
|
||||||
Events = new List<EventDTO>();
|
Events = new List<EventDTO>();
|
||||||
|
OnInitialize();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
protected override void OnInitialize()
|
protected void OnInitialize()
|
||||||
{
|
{
|
||||||
var manifestResourceStream = Assembly.GetExecutingAssembly().GetManifestResourceStream("LaDOSE.DesktopApp.Resources.css.css");
|
// var manifestResourceStream = Assembly.GetExecutingAssembly().GetManifestResourceStream("LaDOSE.DesktopApp.Resources.css.css");
|
||||||
using (var sr = new StreamReader(manifestResourceStream))
|
// using (var sr = new StreamReader(manifestResourceStream))
|
||||||
{
|
// {
|
||||||
this.css = sr.ReadToEnd();
|
// this.css = sr.ReadToEnd();
|
||||||
}
|
// }
|
||||||
|
|
||||||
|
|
||||||
this.To = DateTime.Now;
|
this.To = new DateTimeOffset(DateTime.Now);
|
||||||
this.From = DateTime.Now.AddMonths(-1);
|
this.From = new DateTimeOffset(DateTime.Now.AddMonths(-1));
|
||||||
this.SelectRegex = "Ranking";
|
this.SelectRegex = "Ranking";
|
||||||
this.SelectEventRegex = @"Ranking #10\d{2}";
|
this.SelectEventRegex = @"Ranking #13\d{2}";
|
||||||
this.Slug = "ranking-1001";
|
this.Slug = "ranking-130";
|
||||||
|
|
||||||
LoadTournaments();
|
LoadTournaments();
|
||||||
LoadEvents();
|
LoadEvents();
|
||||||
base.OnInitialize();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public void LoadTournaments()
|
public void LoadTournaments()
|
||||||
{
|
{
|
||||||
WpfUtil.Await(() =>
|
|
||||||
{
|
|
||||||
var tournamentDtos = this.RestService
|
|
||||||
.GetTournaments(new TimeRangeDTO() {From = this.From, To = this.To}).ToList();
|
|
||||||
this.Tournaments = tournamentDtos;
|
|
||||||
|
|
||||||
NotifyOfPropertyChange("Tournaments");
|
// var tournamentDtos = this.RestService
|
||||||
});
|
// .GetTournaments(new TimeRangeDTO() {From = this.From, To = this.To}).ToList();
|
||||||
|
// this.Tournaments = tournamentDtos;
|
||||||
|
|
||||||
|
RaisePropertyChanged(nameof(Tournaments));
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public void LoadEvents()
|
public void LoadEvents()
|
||||||
{
|
{
|
||||||
WpfUtil.Await(() =>
|
|
||||||
{
|
List<EventDTO> eventsDtos = this.RestService
|
||||||
var eventsDtos = this.RestService
|
|
||||||
.GetAllEvents().ToList();
|
.GetAllEvents().ToList();
|
||||||
this.Events = eventsDtos;
|
this.Events = eventsDtos;
|
||||||
|
|
||||||
NotifyOfPropertyChange("Events");
|
RaisePropertyChanged(nameof(Events));
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public DataTable GridDataTable
|
public DataTable? GridDataTable
|
||||||
{
|
{
|
||||||
get => _gridDataTable;
|
get => _gridDataTable;
|
||||||
set
|
set
|
||||||
{
|
{
|
||||||
_gridDataTable = value;
|
_gridDataTable = value;
|
||||||
NotifyOfPropertyChange(() => GridDataTable);
|
RaisePropertyChanged(nameof(GridDataTable));
|
||||||
|
RaisePropertyChanged(nameof(GridDataTableView));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
public DataView? GridDataTableView
|
||||||
|
{
|
||||||
|
get
|
||||||
|
{
|
||||||
|
DataView gridDataTableView = _gridDataTable?.AsDataView();
|
||||||
|
return gridDataTableView;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public void Select()
|
public void Select()
|
||||||
{
|
{
|
||||||
WpfUtil.Await(() =>
|
|
||||||
{
|
List<int> tournamentsIds = SelectedEvents.Select(e => e.Id).ToList();
|
||||||
var tournamentsIds = SelectedEvents.Select(e => e.Id).ToList();
|
TournamentsResultDTO? resultsDto = this.RestService.GetResults(tournamentsIds);
|
||||||
var resultsDto = this.RestService.GetResults(tournamentsIds);
|
|
||||||
this.Results = resultsDto;
|
this.Results = resultsDto;
|
||||||
ComputeDataGrid();
|
ComputeDataGrid();
|
||||||
ComputeHtml();
|
ComputeHtml();
|
||||||
});
|
|
||||||
}
|
}
|
||||||
public void GetSmash()
|
public void GetSmash()
|
||||||
{
|
|
||||||
WpfUtil.Await(() =>
|
|
||||||
{
|
{
|
||||||
|
|
||||||
var resultsDto = this.RestService.ParseSmash(Slug);
|
|
||||||
|
bool resultsDto = this.RestService.ParseSmash(Slug);
|
||||||
if (!resultsDto)
|
if (!resultsDto)
|
||||||
{
|
{
|
||||||
MessageBox.Show("Fail");
|
Error = "Error getting Smash";
|
||||||
}
|
}
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public string? Error
|
||||||
|
{
|
||||||
|
get => _error;
|
||||||
|
set
|
||||||
|
{
|
||||||
|
if (value == _error) return;
|
||||||
|
_error = value;
|
||||||
|
RaisePropertyChanged();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
public void GetChallonge()
|
public void GetChallonge()
|
||||||
{
|
{
|
||||||
WpfUtil.Await(() =>
|
|
||||||
{
|
List<int> ids = SelectedTournaments.Select(e => e.ChallongeId).ToList();
|
||||||
var ids = SelectedTournaments.Select(e => e.ChallongeId).ToList();
|
bool resultsDto = this.RestService.ParseChallonge(ids);
|
||||||
var resultsDto = this.RestService.ParseChallonge(ids);
|
|
||||||
if (!resultsDto)
|
if (!resultsDto)
|
||||||
{
|
{
|
||||||
MessageBox.Show("Fail");
|
Error = "Fail";
|
||||||
}
|
}
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public void UpdateEvent()
|
public void UpdateEvent()
|
||||||
@@ -326,30 +334,30 @@ namespace LaDOSE.DesktopApp.ViewModels
|
|||||||
|
|
||||||
public void SelectRegexp()
|
public void SelectRegexp()
|
||||||
{
|
{
|
||||||
var selectedTournaments = this.Tournaments.Where(e => Regex.IsMatch(e.Name, this.SelectRegex)).ToList();
|
List<TournamentDTO> selectedTournaments = this.Tournaments.Where(e => Regex.IsMatch(e.Name, this.SelectRegex)).ToList();
|
||||||
this.SelectedTournaments.Clear();
|
this.SelectedTournaments.Clear();
|
||||||
if (selectedTournaments.Count > 0)
|
if (selectedTournaments.Count > 0)
|
||||||
selectedTournaments.ForEach(e => this.SelectedTournaments.AddUI(e));
|
selectedTournaments.ForEach(e => this.SelectedTournaments.Add(e));
|
||||||
}
|
}
|
||||||
public void SelectEvent()
|
public void SelectEvent()
|
||||||
{
|
{
|
||||||
var selectedEvents = this.Events.Where(e => Regex.IsMatch(e.Name, this.SelectEventRegex)).ToList();
|
List<EventDTO> selectedEvents = this.Events.Where(e => Regex.IsMatch(e.Name, this.SelectEventRegex)).ToList();
|
||||||
this.SelectedEvents.Clear();
|
this.SelectedEvents.Clear();
|
||||||
if (selectedEvents.Count > 0)
|
if (selectedEvents.Count > 0)
|
||||||
selectedEvents.ForEach(e => this.SelectedEvents.AddUI(e));
|
selectedEvents.ForEach(e => this.SelectedEvents.Add(e));
|
||||||
}
|
}
|
||||||
//This could be simplified the Dictionary was for a previous usage, but i m too lazy to rewrite it.
|
//This could be simplified the Dictionary was for a previous usage, but i m too lazy to rewrite it.
|
||||||
private void ComputeDataGrid()
|
private void ComputeDataGrid()
|
||||||
{
|
{
|
||||||
var resultsParticipents = this.Results.Participents.Select(e=>e.Name).Distinct(new CustomListExtension.EqualityComparer<String>((a, b) => a.ToUpperInvariant()== b.ToUpperInvariant())).OrderBy(e=>e).ToList();
|
List<string> resultsParticipents = this.Results.Participents.Select(e=>e.Name).Distinct(new CustomListExtension.EqualityComparer<String>((a, b) => a.ToUpperInvariant()== b.ToUpperInvariant())).OrderBy(e=>e).ToList();
|
||||||
//At start the dictionnary was for some fancy dataviz things, but since the point are inside
|
//At start the dictionnary was for some fancy dataviz things, but since the point are inside
|
||||||
//i m to lazy to rewrite this functions (this is so ugly...)
|
//i m to lazy to rewrite this functions (this is so ugly...)
|
||||||
//_computedResult = ResultsToDataDictionary(resultsParticipents);
|
//_computedResult = ResultsToDataDictionary(resultsParticipents);
|
||||||
|
|
||||||
StringBuilder sb = new StringBuilder();
|
StringBuilder sb = new StringBuilder();
|
||||||
|
|
||||||
DataTable grid = new DataTable();
|
DataTable? grid = new DataTable();
|
||||||
var games = Results.Games.Distinct().OrderBy(e => e.Order).ToList();
|
List<GameDTO> games = Results.Games.Distinct().OrderBy(e => e.Order).ToList();
|
||||||
grid.Columns.Add("Players");
|
grid.Columns.Add("Players");
|
||||||
games.ForEach(e => grid.Columns.Add(e.Name.Replace('.', ' '),typeof(Int32)));
|
games.ForEach(e => grid.Columns.Add(e.Name.Replace('.', ' '),typeof(Int32)));
|
||||||
grid.Columns.Add("Total").DataType = typeof(Int32);
|
grid.Columns.Add("Total").DataType = typeof(Int32);
|
||||||
@@ -357,17 +365,18 @@ namespace LaDOSE.DesktopApp.ViewModels
|
|||||||
|
|
||||||
for (int i = 0; i < resultsParticipents.Count; i++)
|
for (int i = 0; i < resultsParticipents.Count; i++)
|
||||||
{
|
{
|
||||||
var dataRow = grid.Rows.Add();
|
DataRow dataRow = grid.Rows.Add();
|
||||||
var resultsParticipent = resultsParticipents[i];
|
string resultsParticipent = resultsParticipents[i];
|
||||||
int total = 0;
|
int total = 0;
|
||||||
dataRow["Players"] = resultsParticipent;
|
dataRow["Players"] = resultsParticipent;
|
||||||
|
|
||||||
|
|
||||||
for (int j = 0; j < games.Count; j++)
|
for (int j = 0; j < games.Count; j++)
|
||||||
{
|
{
|
||||||
var resultsGame = Results.Games[j];
|
GameDTO? resultsGame = Results.Games[j];
|
||||||
var points = GetPlayerPoint(resultsParticipent, resultsGame.Id);
|
int points = GetPlayerPoint(resultsParticipent, resultsGame.Id);
|
||||||
dataRow[resultsGame.Name.Replace('.', ' ')] = points!=0?(object) points:DBNull.Value;
|
var o = dataRow[resultsGame.Name.Replace('.', ' ')];
|
||||||
|
dataRow[resultsGame.Name.Replace('.', ' ')] = points!=0?points:0;
|
||||||
total += points;
|
total += points;
|
||||||
}
|
}
|
||||||
dataRow["Total"] = total;
|
dataRow["Total"] = total;
|
||||||
@@ -389,33 +398,33 @@ namespace LaDOSE.DesktopApp.ViewModels
|
|||||||
|
|
||||||
private void ExportToCSV()
|
private void ExportToCSV()
|
||||||
{
|
{
|
||||||
if (this.GridDataTable != null)
|
// if (this.GridDataTable != null)
|
||||||
{
|
// {
|
||||||
var dataTable = this.GridDataTable.DefaultView.ToTable();
|
// var dataTable = this.GridDataTable.DefaultView.ToTable();
|
||||||
SaveFileDialog sfDialog = new SaveFileDialog()
|
// SaveFileDialog sfDialog = new SaveFileDialog()
|
||||||
{
|
// {
|
||||||
Filter = "Csv Files (*.csv)|*.csv|All Files (*.*)|*.*",
|
// Filter = "Csv Files (*.csv)|*.csv|All Files (*.*)|*.*",
|
||||||
AddExtension = true
|
// AddExtension = true
|
||||||
};
|
// };
|
||||||
if (sfDialog.ShowDialog() == true)
|
// if (sfDialog.ShowDialog() == true)
|
||||||
{
|
// {
|
||||||
StringBuilder sb = new StringBuilder();
|
// StringBuilder sb = new StringBuilder();
|
||||||
|
//
|
||||||
IEnumerable<string> columnNames = dataTable.Columns.Cast<DataColumn>()
|
// IEnumerable<string> columnNames = dataTable.Columns.Cast<DataColumn>()
|
||||||
.Select(column => column.ColumnName);
|
// .Select(column => column.ColumnName);
|
||||||
sb.AppendLine(string.Join(";", columnNames));
|
// sb.AppendLine(string.Join(";", columnNames));
|
||||||
|
//
|
||||||
foreach (DataRow row in dataTable.Rows)
|
// foreach (DataRow row in dataTable.Rows)
|
||||||
{
|
// {
|
||||||
//EXCEL IS A BITCH
|
// //EXCEL IS A BITCH
|
||||||
IEnumerable<string> fields = row.ItemArray.Select(field =>
|
// IEnumerable<string> fields = row.ItemArray.Select(field =>
|
||||||
string.Concat("\"", field.ToString().Replace("\"", "\"\""), "\""));
|
// string.Concat("\"", field.ToString().Replace("\"", "\"\""), "\""));
|
||||||
sb.AppendLine(string.Join(";", fields));
|
// sb.AppendLine(string.Join(";", fields));
|
||||||
}
|
// }
|
||||||
|
//
|
||||||
File.WriteAllText(sfDialog.FileName, sb.ToString());
|
// File.WriteAllText(sfDialog.FileName, sb.ToString());
|
||||||
}
|
// }
|
||||||
}
|
// }
|
||||||
}
|
}
|
||||||
|
|
||||||
private void ComputeHtml()
|
private void ComputeHtml()
|
||||||
@@ -426,10 +435,10 @@ namespace LaDOSE.DesktopApp.ViewModels
|
|||||||
|
|
||||||
int columns = 0;
|
int columns = 0;
|
||||||
|
|
||||||
var distinct = Results.Results.Select(e => e.GameId).Distinct();
|
IEnumerable<int> distinct = Results.Results.Select(e => e.GameId).Distinct();
|
||||||
|
|
||||||
var gamePlayed = Results.Games.Where(e=> distinct.Contains(e.Id)).OrderBy(e=>e.Order);
|
IOrderedEnumerable<GameDTO> gamePlayed = Results.Games.Where(e=> distinct.Contains(e.Id)).OrderBy(e=>e.Order);
|
||||||
foreach (var game in gamePlayed)
|
foreach (GameDTO game in gamePlayed)
|
||||||
{
|
{
|
||||||
List<ResultDTO> enumerable = Results.Results.Where(r => r.GameId == game.Id).ToList();
|
List<ResultDTO> enumerable = Results.Results.Where(r => r.GameId == game.Id).ToList();
|
||||||
List<string> top3 = enumerable.OrderBy(e => e.Rank).Take(3).Select(e => e.Player).ToList();
|
List<string> top3 = enumerable.OrderBy(e => e.Rank).Take(3).Select(e => e.Player).ToList();
|
||||||
@@ -443,7 +452,7 @@ namespace LaDOSE.DesktopApp.ViewModels
|
|||||||
sb.Append("<tr>");
|
sb.Append("<tr>");
|
||||||
}
|
}
|
||||||
columns++;
|
columns++;
|
||||||
var span = 1;
|
int span = 1;
|
||||||
if (columns == gamePlayed.Count())
|
if (columns == gamePlayed.Count())
|
||||||
{
|
{
|
||||||
if (columns % 2 != 0)
|
if (columns % 2 != 0)
|
||||||
@@ -460,10 +469,10 @@ namespace LaDOSE.DesktopApp.ViewModels
|
|||||||
{
|
{
|
||||||
sb.AppendLine($"<br> 1/ {top3[0]}<br> 2/ {top3[1]}<br> 3/ {top3[2]} <br>");
|
sb.AppendLine($"<br> 1/ {top3[0]}<br> 2/ {top3[1]}<br> 3/ {top3[2]} <br>");
|
||||||
//<a href=\"https://challonge.com/fr/{enumerable.First().TournamentUrl}\" target=\"_blank\">https://challonge.com/fr/{enumerable.First().TournamentUrl}</a>
|
//<a href=\"https://challonge.com/fr/{enumerable.First().TournamentUrl}\" target=\"_blank\">https://challonge.com/fr/{enumerable.First().TournamentUrl}</a>
|
||||||
var url = enumerable.FirstOrDefault().TournamentUrl;
|
string url = enumerable.FirstOrDefault()?.TournamentUrl;
|
||||||
url = url.Replace(" ", "-");
|
url = url.Replace(" ", "-");
|
||||||
url = url.Replace(".", "-");
|
url = url.Replace(".", "-");
|
||||||
sb.AppendLine($"<a href=\"https://smash.gg/tournament/ranking-1002/event/{url}\" target=\"_blank\">Voir le Bracket</p></td>");
|
sb.AppendLine($"<a href=\"https://start.gg/tournament/{Results.Slug}/event/{url}\" target=\"_blank\">Voir le Bracket</p></td>");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@@ -483,7 +492,7 @@ namespace LaDOSE.DesktopApp.ViewModels
|
|||||||
}
|
}
|
||||||
public void CopyHtml()
|
public void CopyHtml()
|
||||||
{
|
{
|
||||||
System.Windows.Clipboard.SetText(this.HtmlContent);
|
// System.Windows.Clipboard.SetText(this.HtmlContent);
|
||||||
}
|
}
|
||||||
|
|
||||||
private int GetPlayerPoint(string name, int gameid)
|
private int GetPlayerPoint(string name, int gameid)
|
||||||
@@ -0,0 +1,92 @@
|
|||||||
|
<UserControl xmlns="https://github.com/avaloniaui"
|
||||||
|
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||||
|
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
|
||||||
|
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
|
||||||
|
mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="650"
|
||||||
|
x:Class="LaDOSE.DesktopApp.Avalonia.Views.GamesView"
|
||||||
|
xmlns:vm="using:LaDOSE.DesktopApp.Avalonia.ViewModels"
|
||||||
|
xmlns:dto="clr-namespace:LaDOSE.DTO;assembly=LaDOSE.DTO"
|
||||||
|
x:DataType="vm:GamesViewModel"
|
||||||
|
>
|
||||||
|
<Grid Row="4" Column="1">
|
||||||
|
<Grid.RowDefinitions>
|
||||||
|
<RowDefinition Height="Auto"></RowDefinition>
|
||||||
|
<RowDefinition Height="*"></RowDefinition>
|
||||||
|
<RowDefinition Height="*"></RowDefinition>
|
||||||
|
<RowDefinition Height="Auto"></RowDefinition>
|
||||||
|
</Grid.RowDefinitions>
|
||||||
|
<Button Grid.Row="0" x:Name="LoadGames" Command="{Binding LoadGames}" >Load Games</Button>
|
||||||
|
|
||||||
|
<ListBox Grid.Row="1" ItemsSource="{Binding Games}" x:Name="GamesListView" SelectedItem="{Binding CurrentGame}">
|
||||||
|
<ListBox.ItemTemplate>
|
||||||
|
<DataTemplate>
|
||||||
|
<StackPanel Orientation="Horizontal">
|
||||||
|
<Label Content="{Binding Order}"></Label>
|
||||||
|
<Label> - </Label>
|
||||||
|
<Label Content="{Binding Name}"></Label>
|
||||||
|
</StackPanel>
|
||||||
|
</DataTemplate>
|
||||||
|
</ListBox.ItemTemplate>
|
||||||
|
|
||||||
|
</ListBox>
|
||||||
|
<Grid Grid.Row="2">
|
||||||
|
<Grid.ColumnDefinitions>
|
||||||
|
<ColumnDefinition></ColumnDefinition>
|
||||||
|
<ColumnDefinition></ColumnDefinition>
|
||||||
|
</Grid.ColumnDefinitions>
|
||||||
|
<Grid.RowDefinitions>
|
||||||
|
<RowDefinition Height="Auto"></RowDefinition>
|
||||||
|
<RowDefinition Height="Auto"></RowDefinition>
|
||||||
|
<RowDefinition Height="Auto"></RowDefinition>
|
||||||
|
<RowDefinition Height="Auto"></RowDefinition>
|
||||||
|
<RowDefinition Height="Auto"></RowDefinition>
|
||||||
|
<RowDefinition Height="Auto"></RowDefinition>
|
||||||
|
<RowDefinition Height="Auto"></RowDefinition>
|
||||||
|
<RowDefinition Height="Auto"></RowDefinition>
|
||||||
|
|
||||||
|
<RowDefinition Height="*"></RowDefinition>
|
||||||
|
<RowDefinition Height="Auto"></RowDefinition>
|
||||||
|
|
||||||
|
</Grid.RowDefinitions>
|
||||||
|
<Button Grid.Row="0" Grid.Column="0" x:Name="AddGame" Command="{Binding AddGame}">Add Game</Button>
|
||||||
|
|
||||||
|
<Button Grid.Row="0" Grid.Column="1" x:Name="DeleteGame" Command="{Binding DeleteGame}">Delete Game</Button>
|
||||||
|
<Label Grid.Row="1" Grid.Column="0">Id</Label>
|
||||||
|
<TextBox Grid.Row="1" Grid.Column="1" Text="{Binding Path=CurrentGame.Id,Mode=TwoWay}" IsReadOnly="True"></TextBox>
|
||||||
|
|
||||||
|
<Label Grid.Row="2" Grid.Column="0">Name</Label>
|
||||||
|
<TextBox Grid.Row="2" Grid.Column="1" Text="{Binding Path=CurrentGame.Name,Mode=TwoWay}" ></TextBox>
|
||||||
|
|
||||||
|
<Label Grid.Row="3" Grid.Column="0">Order</Label>
|
||||||
|
<TextBox Grid.Row="3" Grid.Column="1" Text="{Binding Path=CurrentGame.Order,Mode=TwoWay}">
|
||||||
|
<!-- <i:Interaction.Behaviors> -->
|
||||||
|
<!-- <behaviors:TextBoxInputRegExBehaviour RegularExpression="^\d+$" MaxLength="9" EmptyValue="0"> -->
|
||||||
|
<!-- -->
|
||||||
|
<!-- </behaviors:TextBoxInputRegExBehaviour> -->
|
||||||
|
<!-- </i:Interaction.Behaviors> -->
|
||||||
|
</TextBox>
|
||||||
|
<Label Grid.Row="4" Grid.Column="0">LongName</Label>
|
||||||
|
<TextBox Grid.Row="4" Grid.Column="1" Text="{Binding Path=CurrentGame.LongName,Mode=TwoWay}" ></TextBox>
|
||||||
|
|
||||||
|
<Label Grid.Row="5" Grid.Column="0">WpTag</Label>
|
||||||
|
<TextBox Grid.Row="5" Grid.Column="1" Text="{Binding Path=CurrentGame.WordPressTag,Mode=TwoWay}" ></TextBox>
|
||||||
|
<Label Grid.Row="6" Grid.Column="0">WpTagOs</Label>
|
||||||
|
<TextBox Grid.Row="6" Grid.Column="1" Text="{Binding Path=CurrentGame.WordPressTagOs,Mode=TwoWay}" ></TextBox>
|
||||||
|
<Label Grid.Row="7" Grid.Column="0">SmashId</Label>
|
||||||
|
<AutoCompleteBox Grid.Row="7" Grid.Column="1" Text="{Binding Path=CurrentGame.SmashId,Mode=TwoWay}" ItemsSource="{Binding Path=SearchGame}">
|
||||||
|
<AutoCompleteBox.ItemTemplate>
|
||||||
|
<DataTemplate>
|
||||||
|
<DockPanel LastChildFill="True" Margin="2" x:DataType="dto:GameDTO">
|
||||||
|
<TextBox Text="{Binding Id}"></TextBox>
|
||||||
|
<TextBlock Text="{Binding Name}" DockPanel.Dock="Left"/>
|
||||||
|
</DockPanel>
|
||||||
|
</DataTemplate>
|
||||||
|
</AutoCompleteBox.ItemTemplate>
|
||||||
|
</AutoCompleteBox>
|
||||||
|
|
||||||
|
<Button Grid.Row="9" x:Name="Update" Command="{Binding Update}">Update</Button>
|
||||||
|
<Button Grid.Row="9" Grid.Column="1" x:Name="SmashGame" Command="{Binding GetGame}">Get Game From Smash</Button>
|
||||||
|
|
||||||
|
</Grid>
|
||||||
|
</Grid>
|
||||||
|
</UserControl>
|
||||||
@@ -0,0 +1,23 @@
|
|||||||
|
using Avalonia;
|
||||||
|
using Avalonia.Controls;
|
||||||
|
using Avalonia.Markup.Xaml;
|
||||||
|
using LaDOSE.DesktopApp.Avalonia.ViewModels;
|
||||||
|
using ReactiveUI;
|
||||||
|
|
||||||
|
namespace LaDOSE.DesktopApp.Avalonia.Views;
|
||||||
|
|
||||||
|
public partial class GamesView : UserControl,IViewFor<GamesViewModel>
|
||||||
|
{
|
||||||
|
public GamesView()
|
||||||
|
{
|
||||||
|
InitializeComponent();
|
||||||
|
}
|
||||||
|
|
||||||
|
object? IViewFor.ViewModel
|
||||||
|
{
|
||||||
|
get => ViewModel;
|
||||||
|
set => ViewModel = (GamesViewModel?)value;
|
||||||
|
}
|
||||||
|
|
||||||
|
public GamesViewModel? ViewModel { get; set; }
|
||||||
|
}
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
<UserControl xmlns="https://github.com/avaloniaui"
|
||||||
|
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||||
|
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
|
||||||
|
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
|
||||||
|
mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450"
|
||||||
|
x:Class="LaDOSE.DesktopApp.Avalonia.Views.InfoView">
|
||||||
|
<Decorator x:Name="browserWrapper"/>
|
||||||
|
</UserControl>
|
||||||
@@ -0,0 +1,24 @@
|
|||||||
|
using Avalonia;
|
||||||
|
using Avalonia.Controls;
|
||||||
|
using Avalonia.Markup.Xaml;
|
||||||
|
using LaDOSE.DesktopApp.Avalonia.ViewModels;
|
||||||
|
using ReactiveUI;
|
||||||
|
|
||||||
|
namespace LaDOSE.DesktopApp.Avalonia.Views;
|
||||||
|
|
||||||
|
public partial class InfoView : UserControl, IViewFor<InfoViewModel>
|
||||||
|
{
|
||||||
|
// private AvaloniaCefBrowser browser;
|
||||||
|
public InfoView()
|
||||||
|
{
|
||||||
|
InitializeComponent();
|
||||||
|
}
|
||||||
|
|
||||||
|
object? IViewFor.ViewModel
|
||||||
|
{
|
||||||
|
get => ViewModel;
|
||||||
|
set => ViewModel = (InfoViewModel?)value;
|
||||||
|
}
|
||||||
|
|
||||||
|
public InfoViewModel? ViewModel { get; set; }
|
||||||
|
}
|
||||||
@@ -0,0 +1,52 @@
|
|||||||
|
<Window xmlns="https://github.com/avaloniaui"
|
||||||
|
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||||
|
xmlns:vm="using:LaDOSE.DesktopApp.Avalonia.ViewModels"
|
||||||
|
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
|
||||||
|
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
|
||||||
|
xmlns:reactiveUi="http://reactiveui.net"
|
||||||
|
mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450"
|
||||||
|
x:Class="LaDOSE.DesktopApp.Avalonia.Views.MainWindow"
|
||||||
|
x:DataType="vm:MainWindowViewModel"
|
||||||
|
Icon="/Assets/avalonia-logo.ico"
|
||||||
|
Title="LaDOSE.DesktopApp.Avalonia">
|
||||||
|
|
||||||
|
<Design.DataContext>
|
||||||
|
<!-- This only sets the DataContext for the previewer in an IDE,
|
||||||
|
to set the actual DataContext for runtime, set the DataContext property in code (look at App.axaml.cs) -->
|
||||||
|
<vm:MainWindowViewModel/>
|
||||||
|
</Design.DataContext>
|
||||||
|
|
||||||
|
<Grid Row="4" Column="1">
|
||||||
|
<Grid.RowDefinitions>
|
||||||
|
<RowDefinition Height="Auto"></RowDefinition>
|
||||||
|
<RowDefinition Height="*"></RowDefinition>
|
||||||
|
<RowDefinition Height="Auto"></RowDefinition>
|
||||||
|
</Grid.RowDefinitions>
|
||||||
|
<Menu Grid.Row="0" DockPanel.Dock="Top">
|
||||||
|
<MenuItem Header="_File">
|
||||||
|
<MenuItem Header="_Events" Command="{Binding RoutedViewViewHost.ShowTournament}">
|
||||||
|
</MenuItem>
|
||||||
|
<MenuItem Header="_Games" Command="{Binding RoutedViewViewHost.ShowGames}" >
|
||||||
|
</MenuItem>
|
||||||
|
<MenuItem Header="_Tournaments">
|
||||||
|
</MenuItem>
|
||||||
|
<MenuItem Header="_EventPlayers">
|
||||||
|
</MenuItem>
|
||||||
|
<MenuItem Header="_Info" Command="{Binding RoutedViewViewHost.ShowInfo}" />
|
||||||
|
<MenuItem Header="_Close" Command="{Binding CloseApp}" />
|
||||||
|
</MenuItem>
|
||||||
|
</Menu>
|
||||||
|
|
||||||
|
<TabControl Grid.Row="1" TabStripPlacement="Left">
|
||||||
|
<TabItem Header="{Binding Path=RoutedViewViewHost.Current}">
|
||||||
|
<DockPanel DataContext="{Binding RoutedViewViewHost}">
|
||||||
|
<reactiveUi:RoutedViewHost Router="{Binding Router}"/>
|
||||||
|
</DockPanel>
|
||||||
|
</TabItem>
|
||||||
|
</TabControl>
|
||||||
|
<StackPanel Grid.Row="2" Orientation="Horizontal">
|
||||||
|
<TextBlock> User : </TextBlock>
|
||||||
|
<TextBlock Margin="5,0,0,0"></TextBlock>
|
||||||
|
</StackPanel>
|
||||||
|
</Grid>
|
||||||
|
</Window>
|
||||||
@@ -0,0 +1,12 @@
|
|||||||
|
using Avalonia.Controls;
|
||||||
|
|
||||||
|
namespace LaDOSE.DesktopApp.Avalonia.Views;
|
||||||
|
|
||||||
|
public partial class MainWindow : Window
|
||||||
|
{
|
||||||
|
public MainWindow()
|
||||||
|
{
|
||||||
|
InitializeComponent();
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@@ -0,0 +1,212 @@
|
|||||||
|
<UserControl xmlns="https://github.com/avaloniaui"
|
||||||
|
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||||
|
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
|
||||||
|
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
|
||||||
|
mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450"
|
||||||
|
x:Class="LaDOSE.DesktopApp.Avalonia.Views.TournamentResultView"
|
||||||
|
xmlns:vm="using:LaDOSE.DesktopApp.Avalonia.ViewModels"
|
||||||
|
x:DataType="vm:TournamentResultViewModel"
|
||||||
|
>
|
||||||
|
<Grid Row="2" Column="1">
|
||||||
|
<Grid.RowDefinitions>
|
||||||
|
<RowDefinition Height="*" />
|
||||||
|
<RowDefinition Height="Auto" />
|
||||||
|
<RowDefinition Height="Auto" />
|
||||||
|
<RowDefinition Height="2*" />
|
||||||
|
<RowDefinition Height="Auto" />
|
||||||
|
</Grid.RowDefinitions>
|
||||||
|
<Grid.ColumnDefinitions>
|
||||||
|
<ColumnDefinition Width="*" />
|
||||||
|
</Grid.ColumnDefinitions>
|
||||||
|
<Grid Row="0" Column="0">
|
||||||
|
<Grid.RowDefinitions>
|
||||||
|
<RowDefinition Height="*" />
|
||||||
|
</Grid.RowDefinitions>
|
||||||
|
<Grid.ColumnDefinitions>
|
||||||
|
<ColumnDefinition Width="*" />
|
||||||
|
<ColumnDefinition Width="*" />
|
||||||
|
</Grid.ColumnDefinitions>
|
||||||
|
<Grid Row="0" Column="0">
|
||||||
|
<Grid.RowDefinitions>
|
||||||
|
<RowDefinition Height="Auto" />
|
||||||
|
<RowDefinition Height="Auto" />
|
||||||
|
<RowDefinition Height="*" />
|
||||||
|
<RowDefinition Height="Auto" />
|
||||||
|
</Grid.RowDefinitions>
|
||||||
|
<Grid.ColumnDefinitions>
|
||||||
|
<ColumnDefinition Width="*" />
|
||||||
|
</Grid.ColumnDefinitions>
|
||||||
|
<StackPanel Grid.Row="0" Orientation="Vertical" Margin="0,0,0,5">
|
||||||
|
<Label>Date :</Label>
|
||||||
|
<StackPanel Orientation="Vertical" VerticalAlignment="Stretch">
|
||||||
|
<StackPanel Orientation="Horizontal" VerticalAlignment="Stretch">
|
||||||
|
<DatePicker SelectedDate="{Binding From}" Margin="5,0,5,0" MonthFormat="MMMM" YearFormat="yyyy" DayVisible="False">
|
||||||
|
</DatePicker>
|
||||||
|
<Button Padding="5,0,5,0" Margin="5,0,5,0" x:Name="SelectMonth" HorizontalContentAlignment="Center" Command="{Binding SelectMonth}" Width="60">Month</Button>
|
||||||
|
</StackPanel>
|
||||||
|
<StackPanel Orientation="Horizontal" VerticalAlignment="Stretch">
|
||||||
|
<DatePicker SelectedDate="{Binding To}" Margin="5,0,5,0" MonthFormat="MMMM" YearFormat="yyyy" DayVisible="False">
|
||||||
|
</DatePicker>
|
||||||
|
<Button Padding="5,0,5,0" Margin="5,0,5,0" x:Name="SelectYear" HorizontalContentAlignment="Center" Width="60" Command="{Binding SelectYear}">Year</Button>
|
||||||
|
</StackPanel>
|
||||||
|
</StackPanel>
|
||||||
|
</StackPanel>
|
||||||
|
<Button Grid.Row="1" x:Name="LoadTournaments" Command="{Binding LoadTournaments}">Update</Button>
|
||||||
|
<ListBox Grid.Row="2" ItemsSource="{Binding Tournaments}" x:Name="TournamentList" Margin="0,0,0,5"
|
||||||
|
IsTextSearchEnabled="True" TextSearch.Text="Name"
|
||||||
|
SelectedItems="{Binding SelectedTournaments}"
|
||||||
|
SelectionMode="Multiple">
|
||||||
|
<ListBox.ItemTemplate>
|
||||||
|
<DataTemplate>
|
||||||
|
<StackPanel Orientation="Horizontal">
|
||||||
|
<TextBlock Margin="5,0,0,0" Text="{Binding Name}" />
|
||||||
|
</StackPanel>
|
||||||
|
</DataTemplate>
|
||||||
|
</ListBox.ItemTemplate>
|
||||||
|
|
||||||
|
</ListBox>
|
||||||
|
<DockPanel Grid.Row="3" Dock="Left">
|
||||||
|
<Label>Select :</Label>
|
||||||
|
<TextBox Width="200" Text="{Binding SelectRegex}"></TextBox>
|
||||||
|
<Button Padding="5,0,5,0" Margin="5,0,5,0" x:Name="SelectRegexp" Command="{Binding SelectRegexp}">Select</Button>
|
||||||
|
<Button Padding="5,0,5,0" Margin="5,0,5,0" x:Name="GetChallonge" Command="{Binding GetChallonge}">Import</Button>
|
||||||
|
</DockPanel>
|
||||||
|
|
||||||
|
</Grid>
|
||||||
|
<Grid Row="0" Column="1">
|
||||||
|
<Grid.RowDefinitions>
|
||||||
|
<RowDefinition Height="Auto" />
|
||||||
|
<RowDefinition Height="Auto" />
|
||||||
|
<RowDefinition Height="*" />
|
||||||
|
<RowDefinition Height="Auto" />
|
||||||
|
</Grid.RowDefinitions>
|
||||||
|
<Grid.ColumnDefinitions>
|
||||||
|
<ColumnDefinition Width="*" />
|
||||||
|
</Grid.ColumnDefinitions>
|
||||||
|
<StackPanel Grid.Row="0" Grid.Column="0" Orientation="Horizontal" Margin="0,0,0,6">
|
||||||
|
<Label> Smash Slug :</Label>
|
||||||
|
<TextBox Width="200" Text="{Binding Slug}"></TextBox>
|
||||||
|
<Button Margin="5,0,5,0" x:Name="GetSmash" Command="{Binding GetSmash}" >Import Smash Event</Button>
|
||||||
|
</StackPanel>
|
||||||
|
|
||||||
|
<Button Grid.Row="1" x:Name="UpdateEvent" Command="{Binding UpdateEvent}">Update Event</Button>
|
||||||
|
|
||||||
|
<ListBox Grid.Row="2" ItemsSource="{Binding Events}" Margin="0,0,0,5"
|
||||||
|
IsTextSearchEnabled="True" TextSearch.Text="Name" SelectionMode="Multiple"
|
||||||
|
SelectedItems="{Binding SelectedEvents}">
|
||||||
|
<ListBox.ItemTemplate>
|
||||||
|
<DataTemplate>
|
||||||
|
<StackPanel Orientation="Horizontal">
|
||||||
|
<TextBlock Text="{Binding Id}" />
|
||||||
|
<TextBlock Margin="5,0,0,0" Text="{Binding Name}" />
|
||||||
|
</StackPanel>
|
||||||
|
</DataTemplate>
|
||||||
|
</ListBox.ItemTemplate>
|
||||||
|
|
||||||
|
</ListBox>
|
||||||
|
<DockPanel Grid.Row="3" Dock="Left">
|
||||||
|
<Label>Select :</Label>
|
||||||
|
<TextBox Width="200" Text="{Binding SelectEventRegex}"></TextBox>
|
||||||
|
<Button Padding="5,0,5,0" Margin="5,0,5,0" x:Name="SelectEvent" Command="{Binding SelectEvent}">Select</Button>
|
||||||
|
</DockPanel>
|
||||||
|
</Grid>
|
||||||
|
</Grid>
|
||||||
|
|
||||||
|
<Grid Row="3">
|
||||||
|
<Grid.RowDefinitions>
|
||||||
|
<RowDefinition Height="Auto" />
|
||||||
|
<RowDefinition Height="Auto" />
|
||||||
|
<RowDefinition Height="*" />
|
||||||
|
<RowDefinition Height="Auto" />
|
||||||
|
<RowDefinition Height="Auto" />
|
||||||
|
|
||||||
|
</Grid.RowDefinitions>
|
||||||
|
<Grid.ColumnDefinitions>
|
||||||
|
<ColumnDefinition Width="*" />
|
||||||
|
<ColumnDefinition Width="*" />
|
||||||
|
<ColumnDefinition Width="2*" />
|
||||||
|
</Grid.ColumnDefinitions>
|
||||||
|
<!--<DockPanel Grid.Row="0" Grid.ColumnSpan="3" Dock="Left">
|
||||||
|
<Label>Select :</Label>
|
||||||
|
<TextBox Width="200" Text="{Binding SelectRegex}"></TextBox>
|
||||||
|
<Button Padding="5,0,5,0" Margin="5,0,5,0" x:Name="SelectRegexp">Select</Button>
|
||||||
|
<Button x:Name="Select" >Get Tournaments Result</Button>
|
||||||
|
</DockPanel>-->
|
||||||
|
<Button x:Name="Select" Grid.ColumnSpan="3" VerticalAlignment="Stretch" HorizontalAlignment="Stretch" HorizontalContentAlignment="Center" Command="{Binding Select}">Get Tournaments Result</Button>
|
||||||
|
|
||||||
|
|
||||||
|
<StackPanel Grid.Column="0" Grid.Row="1" Grid.ColumnSpan="3" Orientation="Horizontal">
|
||||||
|
<TextBlock> Game :</TextBlock>
|
||||||
|
<TextBlock Margin="5,0,0,0" Text="{Binding Results.Games.Count}" />
|
||||||
|
|
||||||
|
</StackPanel>
|
||||||
|
<ListBox Grid.Column="0" Grid.Row="2" ItemsSource="{Binding Results.Games}" Margin="5,5,5,5"
|
||||||
|
IsTextSearchEnabled="True" TextSearch.Text="Name"
|
||||||
|
SelectedItem="{Binding SelectedGame}">
|
||||||
|
<ListBox.ItemTemplate>
|
||||||
|
<DataTemplate>
|
||||||
|
<StackPanel Orientation="Horizontal">
|
||||||
|
<TextBlock Text="{Binding Id}" />
|
||||||
|
<TextBlock Margin="5,0,0,0" Text="{Binding Name}" />
|
||||||
|
</StackPanel>
|
||||||
|
</DataTemplate>
|
||||||
|
</ListBox.ItemTemplate>
|
||||||
|
|
||||||
|
</ListBox>
|
||||||
|
<StackPanel Grid.Row="1" Grid.Column="1" Grid.ColumnSpan="2" Orientation="Horizontal">
|
||||||
|
<TextBlock> Participents :</TextBlock>
|
||||||
|
<TextBlock Margin="5,0,0,0" Text="{Binding Results.Participents.Count}" />
|
||||||
|
|
||||||
|
</StackPanel>
|
||||||
|
<ListBox Grid.Row="2" Grid.Column="1" ItemsSource="{Binding Results.Participents}" Margin="5,5,5,5"
|
||||||
|
IsTextSearchEnabled="True" TextSearch.Text="Name">
|
||||||
|
<ListBox.ItemTemplate>
|
||||||
|
<DataTemplate>
|
||||||
|
<StackPanel Orientation="Horizontal">
|
||||||
|
<TextBlock Margin="5,0,0,0" Text="{Binding Name}" />
|
||||||
|
</StackPanel>
|
||||||
|
</DataTemplate>
|
||||||
|
</ListBox.ItemTemplate>
|
||||||
|
|
||||||
|
</ListBox>
|
||||||
|
|
||||||
|
<TabControl Grid.Row="2" Grid.Column="2">
|
||||||
|
<TabItem Header="Result">
|
||||||
|
<DataGrid x:Name="DataGrid" PropertyChanged="DataGrid_OnPropertyChanged" ItemsSource="{Binding GridDataTableView}" CanUserSortColumns="True" BorderThickness="1" BorderBrush="Gray"/>
|
||||||
|
</TabItem>
|
||||||
|
<TabItem Header="By Game">
|
||||||
|
<DockPanel>
|
||||||
|
|
||||||
|
<StackPanel Orientation="Horizontal" DockPanel.Dock="Top">
|
||||||
|
<TextBlock> Total :</TextBlock>
|
||||||
|
<TextBlock Text="{Binding SelectedGameResult.Count}" />
|
||||||
|
</StackPanel>
|
||||||
|
<ListBox ItemsSource="{Binding SelectedGameResult}" Margin="5,5,5,5"
|
||||||
|
IsTextSearchEnabled="True" TextSearch.Text="Name" DockPanel.Dock="Top">
|
||||||
|
<ListBox.ItemTemplate>
|
||||||
|
<DataTemplate>
|
||||||
|
<StackPanel Orientation="Horizontal">
|
||||||
|
<TextBlock Margin="5,0,0,0" Text="{Binding Player}" />
|
||||||
|
<TextBlock Margin="5,0,0,0" Text="{Binding Point}" />
|
||||||
|
</StackPanel>
|
||||||
|
</DataTemplate>
|
||||||
|
</ListBox.ItemTemplate>
|
||||||
|
|
||||||
|
</ListBox>
|
||||||
|
|
||||||
|
</DockPanel>
|
||||||
|
</TabItem>
|
||||||
|
<TabItem Header="HTML">
|
||||||
|
<DockPanel>
|
||||||
|
<Button x:Name="CopyHtml" DockPanel.Dock="Top" Command="{Binding CopyHtml}">Copy HTML to clipboard</Button>
|
||||||
|
<TextBox Text="{Binding Html}" />
|
||||||
|
</DockPanel>
|
||||||
|
</TabItem>
|
||||||
|
</TabControl>
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
</Grid>
|
||||||
|
<Button Grid.Column="0" Grid.Row="4" VerticalAlignment="Stretch" HorizontalAlignment="Stretch" HorizontalContentAlignment="Center" x:Name="Export" Command="{Binding Export}">Export</Button>
|
||||||
|
</Grid>
|
||||||
|
</UserControl>
|
||||||
@@ -0,0 +1,56 @@
|
|||||||
|
|
||||||
|
using System.Data;
|
||||||
|
using System.Diagnostics;
|
||||||
|
using System.Linq;
|
||||||
|
using Avalonia;
|
||||||
|
using Avalonia.Controls;
|
||||||
|
using Avalonia.Data;
|
||||||
|
using LaDOSE.DesktopApp.Avalonia.ViewModels;
|
||||||
|
using ReactiveUI;
|
||||||
|
|
||||||
|
namespace LaDOSE.DesktopApp.Avalonia.Views
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Interaction logic for ShellView.xaml
|
||||||
|
/// </summary>
|
||||||
|
public partial class TournamentResultView : UserControl, IViewFor<TournamentResultViewModel>
|
||||||
|
{
|
||||||
|
public TournamentResultView()
|
||||||
|
{
|
||||||
|
InitializeComponent();
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
object? IViewFor.ViewModel
|
||||||
|
{
|
||||||
|
get => ViewModel;
|
||||||
|
set => ViewModel = (TournamentResultViewModel)value!;
|
||||||
|
}
|
||||||
|
|
||||||
|
public TournamentResultViewModel? ViewModel { get; set; }
|
||||||
|
|
||||||
|
private void DataGrid_OnPropertyChanged(object? sender, AvaloniaPropertyChangedEventArgs e)
|
||||||
|
{
|
||||||
|
if (e.Property.Name == "ItemsSource")
|
||||||
|
{
|
||||||
|
Trace.WriteLine("Changed Binding");
|
||||||
|
|
||||||
|
var grid = (sender as DataGrid);
|
||||||
|
grid.Columns.Clear();
|
||||||
|
var data = ViewModel.GridDataTable;
|
||||||
|
|
||||||
|
foreach (DataColumn? view in data.Columns)
|
||||||
|
{
|
||||||
|
|
||||||
|
grid.Columns.Add(new DataGridTextColumn()
|
||||||
|
{
|
||||||
|
Header = view.ColumnName,
|
||||||
|
CanUserSort = true,
|
||||||
|
Binding = new Binding($"Row.ItemArray[{view.Ordinal}]")
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,18 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<assembly manifestVersion="1.0" xmlns="urn:schemas-microsoft-com:asm.v1">
|
||||||
|
<!-- This manifest is used on Windows only.
|
||||||
|
Don't remove it as it might cause problems with window transparency and embedded controls.
|
||||||
|
For more details visit https://learn.microsoft.com/en-us/windows/win32/sbscs/application-manifests -->
|
||||||
|
<assemblyIdentity version="1.0.0.0" name="LaDOSE.DesktopApp.Avalonia.Desktop"/>
|
||||||
|
|
||||||
|
<compatibility xmlns="urn:schemas-microsoft-com:compatibility.v1">
|
||||||
|
<application>
|
||||||
|
<!-- A list of the Windows versions that this application has been tested on
|
||||||
|
and is designed to work with. Uncomment the appropriate elements
|
||||||
|
and Windows will automatically select the most compatible environment. -->
|
||||||
|
|
||||||
|
<!-- Windows 10 -->
|
||||||
|
<supportedOS Id="{8e0f7a12-bfb3-4fe8-b9a5-48fd50a15a9a}" />
|
||||||
|
</application>
|
||||||
|
</compatibility>
|
||||||
|
</assembly>
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
#!/usr/bin/env sh
|
||||||
|
|
||||||
|
export ANDROID_HOME=/home/tom/src/android/
|
||||||
|
export PATH=$PATH:$ANDROID_HOME/build-tools/34.0.0:$ANDROID_HOME/platforms/android-34
|
||||||
|
|
||||||
|
dotnet build LaDOSE.DesktopApp.Avalonia.csproj -p:TargetFramework=net6.0-android -p:AndroidSdkDirectory=$ANDROID_HOME/build-tools/34.0.0
|
||||||
|
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
{
|
||||||
|
"REST": {
|
||||||
|
"Url": "http://localhost:5000",
|
||||||
|
"User": "user",
|
||||||
|
"Password": "password"
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -11,7 +11,7 @@
|
|||||||
<OutputType>WinExe</OutputType>
|
<OutputType>WinExe</OutputType>
|
||||||
<RootNamespace>LaDOSE.DesktopApp</RootNamespace>
|
<RootNamespace>LaDOSE.DesktopApp</RootNamespace>
|
||||||
<AssemblyName>LaDOSE.DesktopApp</AssemblyName>
|
<AssemblyName>LaDOSE.DesktopApp</AssemblyName>
|
||||||
<TargetFrameworkVersion>v4.6.1</TargetFrameworkVersion>
|
<TargetFrameworkVersion>v4.7</TargetFrameworkVersion>
|
||||||
<FileAlignment>512</FileAlignment>
|
<FileAlignment>512</FileAlignment>
|
||||||
<ProjectTypeGuids>{60dc8134-eba5-43b8-bcc9-bb4bc16c2548};{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}</ProjectTypeGuids>
|
<ProjectTypeGuids>{60dc8134-eba5-43b8-bcc9-bb4bc16c2548};{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}</ProjectTypeGuids>
|
||||||
<WarningLevel>4</WarningLevel>
|
<WarningLevel>4</WarningLevel>
|
||||||
@@ -71,27 +71,6 @@
|
|||||||
<OutputPath>bin\x64\Release\</OutputPath>
|
<OutputPath>bin\x64\Release\</OutputPath>
|
||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<Reference Include="Caliburn.Micro, Version=3.2.0.0, Culture=neutral, PublicKeyToken=8e5891231f2ed21f, processorArchitecture=MSIL">
|
|
||||||
<HintPath>..\packages\Caliburn.Micro.Core.3.2.0\lib\net45\Caliburn.Micro.dll</HintPath>
|
|
||||||
</Reference>
|
|
||||||
<Reference Include="Caliburn.Micro.Platform, Version=3.2.0.0, Culture=neutral, PublicKeyToken=8e5891231f2ed21f, processorArchitecture=MSIL">
|
|
||||||
<HintPath>..\packages\Caliburn.Micro.3.2.0\lib\net45\Caliburn.Micro.Platform.dll</HintPath>
|
|
||||||
</Reference>
|
|
||||||
<Reference Include="Caliburn.Micro.Platform.Core, Version=3.2.0.0, Culture=neutral, PublicKeyToken=8e5891231f2ed21f, processorArchitecture=MSIL">
|
|
||||||
<HintPath>..\packages\Caliburn.Micro.3.2.0\lib\net45\Caliburn.Micro.Platform.Core.dll</HintPath>
|
|
||||||
</Reference>
|
|
||||||
<Reference Include="CefSharp, Version=103.0.120.0, Culture=neutral, PublicKeyToken=40c4b6fc221f4138, processorArchitecture=MSIL">
|
|
||||||
<HintPath>..\packages\CefSharp.Common.103.0.120\lib\net452\CefSharp.dll</HintPath>
|
|
||||||
</Reference>
|
|
||||||
<Reference Include="CefSharp.Core, Version=103.0.120.0, Culture=neutral, PublicKeyToken=40c4b6fc221f4138, processorArchitecture=MSIL">
|
|
||||||
<HintPath>..\packages\CefSharp.Common.103.0.120\lib\net452\CefSharp.Core.dll</HintPath>
|
|
||||||
</Reference>
|
|
||||||
<Reference Include="CefSharp.Wpf, Version=103.0.120.0, Culture=neutral, PublicKeyToken=40c4b6fc221f4138, processorArchitecture=MSIL">
|
|
||||||
<HintPath>..\packages\CefSharp.Wpf.103.0.120\lib\net452\CefSharp.Wpf.dll</HintPath>
|
|
||||||
</Reference>
|
|
||||||
<Reference Include="RestSharp, Version=106.11.4.0, Culture=neutral, PublicKeyToken=598062e77f915f75, processorArchitecture=MSIL">
|
|
||||||
<HintPath>..\packages\RestSharp.106.11.4\lib\net452\RestSharp.dll</HintPath>
|
|
||||||
</Reference>
|
|
||||||
<Reference Include="System" />
|
<Reference Include="System" />
|
||||||
<Reference Include="System.Configuration" />
|
<Reference Include="System.Configuration" />
|
||||||
<Reference Include="System.Data" />
|
<Reference Include="System.Data" />
|
||||||
@@ -101,9 +80,6 @@
|
|||||||
<Reference Include="System.Web" />
|
<Reference Include="System.Web" />
|
||||||
<Reference Include="System.Web.Extensions" />
|
<Reference Include="System.Web.Extensions" />
|
||||||
<Reference Include="System.Windows.Forms" />
|
<Reference Include="System.Windows.Forms" />
|
||||||
<Reference Include="System.Windows.Interactivity, Version=4.5.0.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35, processorArchitecture=MSIL">
|
|
||||||
<HintPath>..\packages\Caliburn.Micro.3.2.0\lib\net45\System.Windows.Interactivity.dll</HintPath>
|
|
||||||
</Reference>
|
|
||||||
<Reference Include="System.Xml" />
|
<Reference Include="System.Xml" />
|
||||||
<Reference Include="Microsoft.CSharp" />
|
<Reference Include="Microsoft.CSharp" />
|
||||||
<Reference Include="System.Core" />
|
<Reference Include="System.Core" />
|
||||||
@@ -126,8 +102,6 @@
|
|||||||
<Compile Include="Behaviors\TextBoxInputRegExBehaviour.cs" />
|
<Compile Include="Behaviors\TextBoxInputRegExBehaviour.cs" />
|
||||||
<Compile Include="Behaviors\MultiSelectorBehaviours.cs" />
|
<Compile Include="Behaviors\MultiSelectorBehaviours.cs" />
|
||||||
<Compile Include="Bootstrapper.cs" />
|
<Compile Include="Bootstrapper.cs" />
|
||||||
<Compile Include="Themes\LeftMarginMultiplierConverter.cs" />
|
|
||||||
<Compile Include="Themes\TreeViewItemExtensions.cs" />
|
|
||||||
<Compile Include="Utils\CustomEqualityCompare.cs" />
|
<Compile Include="Utils\CustomEqualityCompare.cs" />
|
||||||
<Compile Include="Utils\PhpSerialize.cs" />
|
<Compile Include="Utils\PhpSerialize.cs" />
|
||||||
<Compile Include="UserControls\BookingUserControl.xaml.cs">
|
<Compile Include="UserControls\BookingUserControl.xaml.cs">
|
||||||
@@ -163,10 +137,6 @@
|
|||||||
<DependentUpon>App.xaml</DependentUpon>
|
<DependentUpon>App.xaml</DependentUpon>
|
||||||
<SubType>Code</SubType>
|
<SubType>Code</SubType>
|
||||||
</Compile>
|
</Compile>
|
||||||
<Page Include="Themes\Styles.xaml">
|
|
||||||
<Generator>MSBuild:Compile</Generator>
|
|
||||||
<SubType>Designer</SubType>
|
|
||||||
</Page>
|
|
||||||
<Page Include="UserControls\BookingUserControl.xaml">
|
<Page Include="UserControls\BookingUserControl.xaml">
|
||||||
<SubType>Designer</SubType>
|
<SubType>Designer</SubType>
|
||||||
<Generator>MSBuild:Compile</Generator>
|
<Generator>MSBuild:Compile</Generator>
|
||||||
@@ -214,7 +184,6 @@
|
|||||||
<Generator>ResXFileCodeGenerator</Generator>
|
<Generator>ResXFileCodeGenerator</Generator>
|
||||||
<LastGenOutput>Resources.Designer.cs</LastGenOutput>
|
<LastGenOutput>Resources.Designer.cs</LastGenOutput>
|
||||||
</EmbeddedResource>
|
</EmbeddedResource>
|
||||||
<None Include="packages.config" />
|
|
||||||
<None Include="Properties\Settings.settings">
|
<None Include="Properties\Settings.settings">
|
||||||
<Generator>SettingsSingleFileGenerator</Generator>
|
<Generator>SettingsSingleFileGenerator</Generator>
|
||||||
<LastGenOutput>Settings.Designer.cs</LastGenOutput>
|
<LastGenOutput>Settings.Designer.cs</LastGenOutput>
|
||||||
|
|||||||
@@ -1,31 +0,0 @@
|
|||||||
using System;
|
|
||||||
using System.Collections.Generic;
|
|
||||||
using System.Globalization;
|
|
||||||
using System.Linq;
|
|
||||||
using System.Text;
|
|
||||||
using System.Threading.Tasks;
|
|
||||||
using System.Windows;
|
|
||||||
using System.Windows.Controls;
|
|
||||||
using System.Windows.Data;
|
|
||||||
|
|
||||||
namespace DarkBlendTheme
|
|
||||||
{
|
|
||||||
public class LeftMarginMultiplierConverter : IValueConverter
|
|
||||||
{
|
|
||||||
public double Length { get; set; }
|
|
||||||
|
|
||||||
public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
|
|
||||||
{
|
|
||||||
var item = value as TreeViewItem;
|
|
||||||
if (item == null)
|
|
||||||
return new Thickness(0);
|
|
||||||
|
|
||||||
return new Thickness(Length * item.GetDepth(), 0, 0, 0);
|
|
||||||
}
|
|
||||||
|
|
||||||
public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
|
|
||||||
{
|
|
||||||
throw new System.NotImplementedException();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
File diff suppressed because it is too large
Load Diff
@@ -1,38 +0,0 @@
|
|||||||
using System;
|
|
||||||
using System.Collections.Generic;
|
|
||||||
using System.Globalization;
|
|
||||||
using System.Linq;
|
|
||||||
using System.Text;
|
|
||||||
using System.Threading.Tasks;
|
|
||||||
using System.Windows;
|
|
||||||
using System.Windows.Controls;
|
|
||||||
using System.Windows.Data;
|
|
||||||
using System.Windows.Media;
|
|
||||||
|
|
||||||
namespace DarkBlendTheme
|
|
||||||
{
|
|
||||||
public static class TreeViewItemExtensions
|
|
||||||
{
|
|
||||||
public static int GetDepth(this TreeViewItem item)
|
|
||||||
{
|
|
||||||
TreeViewItem parent;
|
|
||||||
while ((parent = GetParent(item)) != null)
|
|
||||||
{
|
|
||||||
return GetDepth(parent) + 1;
|
|
||||||
}
|
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
private static TreeViewItem GetParent(TreeViewItem item)
|
|
||||||
{
|
|
||||||
var parent = VisualTreeHelper.GetParent(item);
|
|
||||||
|
|
||||||
while (!(parent is TreeViewItem || parent is TreeView))
|
|
||||||
{
|
|
||||||
if (parent == null) return null;
|
|
||||||
parent = VisualTreeHelper.GetParent(parent);
|
|
||||||
}
|
|
||||||
return parent as TreeViewItem;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,11 +0,0 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
|
||||||
<packages>
|
|
||||||
<package id="Caliburn.Micro" version="3.2.0" targetFramework="net461" />
|
|
||||||
<package id="Caliburn.Micro.Core" version="3.2.0" targetFramework="net461" />
|
|
||||||
<package id="cef.redist.x64" version="103.0.12" targetFramework="net461" />
|
|
||||||
<package id="cef.redist.x86" version="103.0.12" targetFramework="net461" />
|
|
||||||
<package id="CefSharp.Common" version="103.0.120" targetFramework="net461" />
|
|
||||||
<package id="CefSharp.Wpf" version="103.0.120" targetFramework="net461" />
|
|
||||||
<package id="RestSharp" version="106.11.4" targetFramework="net461" />
|
|
||||||
<package id="WPFThemes.DarkBlend" version="1.0.8" targetFramework="net461" />
|
|
||||||
</packages>
|
|
||||||
@@ -1,58 +1,55 @@
|
|||||||
using System.Linq;
|
using System.Linq;
|
||||||
using System.Text;
|
using System.Text;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
using DSharpPlus.CommandsNext;
|
|
||||||
using DSharpPlus.CommandsNext.Attributes;
|
|
||||||
using LaDOSE.DiscordBot.Service;
|
using LaDOSE.DiscordBot.Service;
|
||||||
using LaDOSE.DTO;
|
using LaDOSE.DTO;
|
||||||
|
|
||||||
namespace LaDOSE.DiscordBot.Command
|
namespace LaDOSE.DiscordBot.Command
|
||||||
{
|
{
|
||||||
public class BotEvent : BaseCommandModule
|
// public class BotEvent : BaseCommandModule
|
||||||
{
|
// {
|
||||||
private WebService dep;
|
// private WebService dep;
|
||||||
public BotEvent(WebService d)
|
// public BotEvent(WebService d)
|
||||||
{
|
// {
|
||||||
dep = d;
|
// dep = d;
|
||||||
}
|
// }
|
||||||
|
//
|
||||||
[RequireRolesAttribute(RoleCheckMode.Any, "Staff")]
|
// [Command("newevent")]
|
||||||
[Command("newevent")]
|
// public async Task NewEventAsync(CommandContext ctx, string command)
|
||||||
public async Task NewEventAsync(CommandContext ctx, string command)
|
// {
|
||||||
{
|
//
|
||||||
|
// await ctx.RespondAsync(dep.RestService.CreateBotEvent(command).ToString());
|
||||||
await ctx.RespondAsync(dep.RestService.CreateBotEvent(command).ToString());
|
// }
|
||||||
}
|
// [RequireRolesAttribute(RoleCheckMode.Any,"Staff")]
|
||||||
[RequireRolesAttribute(RoleCheckMode.Any,"Staff")]
|
// [Command("staffs")]
|
||||||
[Command("staffs")]
|
// public async Task StaffAsync(CommandContext ctx)
|
||||||
public async Task StaffAsync(CommandContext ctx)
|
// {
|
||||||
{
|
// BotEventDTO currentEvent = dep.RestService.GetLastBotEvent();
|
||||||
BotEventDTO currentEvent = dep.RestService.GetLastBotEvent();
|
// StringBuilder stringBuilder = new StringBuilder();
|
||||||
StringBuilder stringBuilder = new StringBuilder();
|
//
|
||||||
|
// var present = currentEvent.Results.Where(x => x.Result).ToList();
|
||||||
var present = currentEvent.Results.Where(x => x.Result).ToList();
|
// var absent = currentEvent.Results.Where(x => !x.Result).ToList();
|
||||||
var absent = currentEvent.Results.Where(x => !x.Result).ToList();
|
//
|
||||||
|
// stringBuilder.AppendLine($"Pour {currentEvent.Name} : ");
|
||||||
stringBuilder.AppendLine($"Pour {currentEvent.Name} : ");
|
// present.ForEach(x => stringBuilder.AppendLine($":white_check_mark: {x.Name}"));
|
||||||
present.ForEach(x => stringBuilder.AppendLine($":white_check_mark: {x.Name}"));
|
// absent.ForEach(x => stringBuilder.AppendLine($":x: {x.Name}"));
|
||||||
absent.ForEach(x => stringBuilder.AppendLine($":x: {x.Name}"));
|
//
|
||||||
|
// await ctx.RespondAsync(stringBuilder.ToString());
|
||||||
await ctx.RespondAsync(stringBuilder.ToString());
|
//
|
||||||
|
// }
|
||||||
}
|
// [RequireRolesAttribute(RoleCheckMode.Any, "Staff")]
|
||||||
[RequireRolesAttribute(RoleCheckMode.Any, "Staff")]
|
// [Command("present")]
|
||||||
[Command("present")]
|
// public async Task PresentAsync(CommandContext ctx)
|
||||||
public async Task PresentAsync(CommandContext ctx)
|
// {
|
||||||
{
|
// await ctx.RespondAsync(dep.RestService.ResultBotEvent(new DTO.BotEventSendDTO() { DiscordId = ctx.Member.Id.ToString(), DiscordName = ctx.Member.DisplayName, Present = true }).ToString());
|
||||||
await ctx.RespondAsync(dep.RestService.ResultBotEvent(new DTO.BotEventSendDTO() { DiscordId = ctx.Member.Id.ToString(), DiscordName = ctx.Member.DisplayName, Present = true }).ToString());
|
//
|
||||||
|
//
|
||||||
|
// }
|
||||||
}
|
// [RequireRolesAttribute(RoleCheckMode.Any, "Staff")]
|
||||||
[RequireRolesAttribute(RoleCheckMode.Any, "Staff")]
|
// [Command("absent")]
|
||||||
[Command("absent")]
|
// public async Task AbsentAsync(CommandContext ctx)
|
||||||
public async Task AbsentAsync(CommandContext ctx)
|
// {
|
||||||
{
|
// await ctx.RespondAsync(dep.RestService.ResultBotEvent(new DTO.BotEventSendDTO() { DiscordId = ctx.Member.Id.ToString(), DiscordName = ctx.Member.DisplayName, Present = false }).ToString());
|
||||||
await ctx.RespondAsync(dep.RestService.ResultBotEvent(new DTO.BotEventSendDTO() { DiscordId = ctx.Member.Id.ToString(), DiscordName = ctx.Member.DisplayName, Present = false }).ToString());
|
// }
|
||||||
}
|
// }
|
||||||
}
|
|
||||||
}
|
}
|
||||||
@@ -2,14 +2,15 @@
|
|||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.Threading;
|
using System.Threading;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
using DSharpPlus.CommandsNext;
|
using DSharpPlus.Commands;
|
||||||
using DSharpPlus.CommandsNext.Attributes;
|
using DSharpPlus.Commands.ArgumentModifiers;
|
||||||
|
using DSharpPlus.Commands.Processors.TextCommands;
|
||||||
using DSharpPlus.Entities;
|
using DSharpPlus.Entities;
|
||||||
|
|
||||||
namespace LaDOSE.DiscordBot.Command
|
namespace LaDOSE.DiscordBot.Command
|
||||||
{
|
{
|
||||||
|
|
||||||
public class Hokuto : BaseCommandModule
|
public class Hokuto
|
||||||
{
|
{
|
||||||
|
|
||||||
private static List<string> Games = new List<string> { "2X", "3.3", "Karnov" };
|
private static List<string> Games = new List<string> { "2X", "3.3", "Karnov" };
|
||||||
@@ -21,14 +22,21 @@ namespace LaDOSE.DiscordBot.Command
|
|||||||
|
|
||||||
|
|
||||||
[Command("hokuto")]
|
[Command("hokuto")]
|
||||||
public async Task HokutoUserAsync(CommandContext ctx, params DiscordMember[] user)
|
public async ValueTask HokutoUserAsync(TextCommandContext ctx)
|
||||||
{
|
{
|
||||||
|
|
||||||
|
|
||||||
var i = r.Next(0, 3);
|
var i = r.Next(0, 3);
|
||||||
if (user!=null && user.Length>0)
|
if (ctx.Message.MentionedUsers is { Count: 1 } )
|
||||||
{
|
{
|
||||||
await ctx.RespondAsync(ctx.User?.Mention + " vs " + user[0].Mention + " : " + Games[i].ToString());
|
foreach (var arg in ctx.Message.MentionedUsers)
|
||||||
|
{
|
||||||
|
if (arg is DiscordUser member)
|
||||||
|
{
|
||||||
|
await ctx.RespondAsync(ctx.User?.Mention + " vs " + member.Mention + " : " + Games[i].ToString());
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -4,12 +4,11 @@ using System.Globalization;
|
|||||||
using System.IO;
|
using System.IO;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
using DSharpPlus.CommandsNext;
|
using DSharpPlus.Commands;
|
||||||
using DSharpPlus.CommandsNext.Attributes;
|
|
||||||
|
|
||||||
namespace LaDOSE.DiscordBot.Command
|
namespace LaDOSE.DiscordBot.Command
|
||||||
{
|
{
|
||||||
public class Public : BaseCommandModule
|
public class Public
|
||||||
{
|
{
|
||||||
|
|
||||||
private static List<string> Quotes { get; set; }
|
private static List<string> Quotes { get; set; }
|
||||||
|
|||||||
@@ -2,16 +2,18 @@
|
|||||||
|
|
||||||
<PropertyGroup>
|
<PropertyGroup>
|
||||||
<OutputType>Exe</OutputType>
|
<OutputType>Exe</OutputType>
|
||||||
<TargetFramework>net6.0</TargetFramework>
|
<TargetFramework>net9.0</TargetFramework>
|
||||||
<Platforms>AnyCPU;x64</Platforms>
|
<Platforms>AnyCPU;x64</Platforms>
|
||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<PackageReference Include="DSharpPlus" Version="4.2.0" />
|
<PackageReference Include="DSharpPlus" Version="5.0.0-alpha.5" />
|
||||||
<PackageReference Include="DSharpPlus.CommandsNext" Version="4.2.0" />
|
<PackageReference Include="DSharpPlus.Commands" Version="5.0.0-alpha.5" />
|
||||||
<PackageReference Include="DSharpPlus.Interactivity" Version="4.2.0" />
|
<PackageReference Include="DSharpPlus.Interactivity" Version="5.0.0-alpha.5" />
|
||||||
<PackageReference Include="Microsoft.Extensions.Configuration" Version="3.1.8" />
|
<PackageReference Include="Microsoft.Extensions.Configuration" Version="8.0.0" />
|
||||||
<PackageReference Include="Microsoft.Extensions.Configuration.Json" Version="3.1.8" />
|
<PackageReference Include="Microsoft.Extensions.Configuration.Json" Version="8.0.1" />
|
||||||
|
<PackageReference Include="Microsoft.Extensions.DependencyInjection.Abstractions" Version="8.0.2" />
|
||||||
|
<PackageReference Include="Newtonsoft.Json" Version="13.0.3" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
|
|||||||
@@ -4,8 +4,12 @@ using System.IO;
|
|||||||
using System.Threading;
|
using System.Threading;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
using DSharpPlus;
|
using DSharpPlus;
|
||||||
|
using DSharpPlus.Commands;
|
||||||
|
using DSharpPlus.Commands.Processors.SlashCommands;
|
||||||
|
using DSharpPlus.Commands.Processors.TextCommands;
|
||||||
|
using DSharpPlus.Commands.Processors.TextCommands.Parsing;
|
||||||
|
using DSharpPlus.Entities;
|
||||||
using DSharpPlus.Interactivity;
|
using DSharpPlus.Interactivity;
|
||||||
using DSharpPlus.CommandsNext;
|
|
||||||
using DSharpPlus.EventArgs;
|
using DSharpPlus.EventArgs;
|
||||||
using DSharpPlus.Interactivity.Extensions;
|
using DSharpPlus.Interactivity.Extensions;
|
||||||
//using DSharpPlus.SlashCommands;
|
//using DSharpPlus.SlashCommands;
|
||||||
@@ -20,7 +24,6 @@ namespace LaDOSE.DiscordBot
|
|||||||
{
|
{
|
||||||
class Program
|
class Program
|
||||||
{
|
{
|
||||||
static DiscordClient discord;
|
|
||||||
|
|
||||||
//static InteractivityModule Interactivity { get; set; }
|
//static InteractivityModule Interactivity { get; set; }
|
||||||
static void Main(string[] args)
|
static void Main(string[] args)
|
||||||
@@ -43,106 +46,36 @@ namespace LaDOSE.DiscordBot
|
|||||||
var restUser = builder["REST:User"].ToString();
|
var restUser = builder["REST:User"].ToString();
|
||||||
var restPassword = builder["REST:Password"].ToString();
|
var restPassword = builder["REST:Password"].ToString();
|
||||||
|
|
||||||
var service = new ServiceCollection()
|
|
||||||
.AddSingleton(typeof(WebService), new WebService(new Uri(restUrl), restUser, restPassword))
|
|
||||||
.BuildServiceProvider();
|
|
||||||
|
|
||||||
|
|
||||||
Console.WriteLine($"LaDOSE.Net Discord Bot");
|
Console.WriteLine($"LaDOSE.Net Discord Bot");
|
||||||
|
|
||||||
|
DiscordClientBuilder builder2 =
|
||||||
discord = new DiscordClient(new DiscordConfiguration
|
DiscordClientBuilder.CreateDefault(discordToken, DiscordIntents.AllUnprivileged | DiscordIntents.MessageContents | DiscordIntents.GuildMessages| TextCommandProcessor.RequiredIntents | SlashCommandProcessor.RequiredIntents);
|
||||||
{
|
|
||||||
Token = discordToken,
|
|
||||||
TokenType = TokenType.Bot,
|
|
||||||
//AutoReconnect = true,
|
|
||||||
//MinimumLogLevel = LogLevel.Debug,
|
|
||||||
//MessageCacheSize = 0,
|
|
||||||
});
|
|
||||||
|
|
||||||
discord.UseInteractivity(new InteractivityConfiguration
|
|
||||||
{
|
|
||||||
|
|
||||||
// default pagination behaviour to just ignore the reactions
|
|
||||||
//PaginationBehaviour = TimeoutBehaviour.Ignore,
|
|
||||||
|
|
||||||
// default pagination timeout to 5 minutes
|
|
||||||
//PaginationTimeout = TimeSpan.FromMinutes(5),
|
|
||||||
|
|
||||||
// default timeout for other actions to 2 minutes
|
|
||||||
Timeout = TimeSpan.FromMinutes(2)
|
|
||||||
});
|
|
||||||
|
|
||||||
|
|
||||||
var cts = new CancellationTokenSource();
|
var cts = new CancellationTokenSource();
|
||||||
|
|
||||||
var _cnext = discord.UseCommandsNext(new CommandsNextConfiguration()
|
// Setup the commands extension
|
||||||
|
builder2.UseCommands((IServiceProvider serviceProvider, CommandsExtension extension) =>
|
||||||
{
|
{
|
||||||
//CaseSensitive = false,
|
extension.AddCommands([typeof(Hokuto), typeof(Public)]);
|
||||||
//EnableDefaultHelp = true,
|
TextCommandProcessor textCommandProcessor = new();
|
||||||
//EnableDms = false,
|
extension.AddProcessor(textCommandProcessor);
|
||||||
//EnableMentionPrefix = true,
|
}, new CommandsConfiguration()
|
||||||
StringPrefixes = new List<string>() { "/", "!" },
|
{
|
||||||
//IgnoreExtraArguments = true,
|
// The default value is true, however it's shown here for clarity
|
||||||
Services = service
|
RegisterDefaultCommandProcessors = true,
|
||||||
|
UseDefaultCommandErrorHandler = false
|
||||||
|
// DebugGuildId = Environment.GetEnvironmentVariable("DEBUG_GUILD_ID") ?? 0,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
DiscordClient client = builder2.Build();
|
||||||
|
|
||||||
//var slashCommands = discord.UseSlashCommands(new SlashCommandsConfiguration() {Services = service});
|
// We can specify a status for our bot. Let's set it to "playing" and set the activity to "with fire".
|
||||||
//slashCommands.RegisterCommands<SlashCommand>(guildId:null);
|
DiscordActivity status = new("Street Fighter", DiscordActivityType.Playing);
|
||||||
|
await client.ConnectAsync(status,DiscordUserStatus.Online);
|
||||||
|
|
||||||
//_cnext.RegisterCommands<Result>();
|
|
||||||
_cnext.RegisterCommands<Public>();
|
|
||||||
//_cnext.RegisterCommands<Shutdown>();
|
|
||||||
//_cnext.RegisterCommands<Todo>();
|
|
||||||
_cnext.RegisterCommands<Hokuto>();
|
|
||||||
_cnext.RegisterCommands<BotEvent>();
|
|
||||||
|
|
||||||
foreach (var registeredCommandsKey in discord.GetCommandsNext().RegisteredCommands.Keys)
|
|
||||||
{
|
|
||||||
Console.WriteLine(registeredCommandsKey);
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
discord.Ready += (sender, eventArgs) =>
|
|
||||||
{
|
|
||||||
Console.WriteLine($"Bot READY.");
|
|
||||||
return Task.CompletedTask;
|
|
||||||
};
|
|
||||||
discord.GuildAvailable += (sender, eventArgs) =>
|
|
||||||
{
|
|
||||||
|
|
||||||
Console.WriteLine($"Joined Guild " + eventArgs.Guild.Name);
|
|
||||||
return Task.CompletedTask;
|
|
||||||
};
|
|
||||||
|
|
||||||
await discord.ConnectAsync();
|
|
||||||
|
|
||||||
|
|
||||||
await Task.Delay(Timeout.Infinite);
|
await Task.Delay(Timeout.Infinite);
|
||||||
|
|
||||||
//while (!cts.IsCancellationRequested)
|
|
||||||
//{
|
|
||||||
// await Task.Delay(200);
|
|
||||||
// //if(discord.GetConnectionsAsync().Result.Count)
|
|
||||||
//}
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
//internal class SlashCommand : ApplicationCommandModule
|
|
||||||
//{
|
|
||||||
// [SlashCommand("test", "A slash command made to test the DSharpPlusSlashCommands library!")]
|
|
||||||
|
|
||||||
// public async Task TestCommand(InteractionContext ctx)
|
|
||||||
// {
|
|
||||||
|
|
||||||
// await ctx.CreateResponseAsync("Lol");
|
|
||||||
|
|
||||||
// }
|
|
||||||
//}
|
|
||||||
|
|
||||||
}
|
}
|
||||||
@@ -34,13 +34,6 @@ namespace LaDOSE.DiscordBot.Service
|
|||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private void CheckToken()
|
|
||||||
{
|
|
||||||
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
public String GetInscrits()
|
public String GetInscrits()
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -0,0 +1,17 @@
|
|||||||
|
using System.ComponentModel.DataAnnotations;
|
||||||
|
|
||||||
|
namespace LaDOSE.Entity
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// A role a user can hold, stored in the pre-existing <c>applicationrole</c> table.
|
||||||
|
/// Rows are reference data seeded by Sql/2026-08-05_roles.sql, not created at runtime.
|
||||||
|
/// </summary>
|
||||||
|
public class ApplicationRole
|
||||||
|
{
|
||||||
|
public int Id { get; set; }
|
||||||
|
|
||||||
|
[Required]
|
||||||
|
[MaxLength(50)]
|
||||||
|
public string Name { get; set; }
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,4 +1,5 @@
|
|||||||
using System.ComponentModel.DataAnnotations;
|
using System.Collections.Generic;
|
||||||
|
using System.ComponentModel.DataAnnotations;
|
||||||
using System.ComponentModel.DataAnnotations.Schema;
|
using System.ComponentModel.DataAnnotations.Schema;
|
||||||
|
|
||||||
|
|
||||||
@@ -14,6 +15,14 @@ namespace LaDOSE.Entity
|
|||||||
public string Password { get; set; }
|
public string Password { get; set; }
|
||||||
public byte[] PasswordHash { get; set; }
|
public byte[] PasswordHash { get; set; }
|
||||||
public byte[] PasswordSalt { get; set; }
|
public byte[] PasswordSalt { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Rows of the <c>applicationuserrole</c> join table for this user. Only populated
|
||||||
|
/// when the query asks for it — see UserService, which includes it (and the role
|
||||||
|
/// itself) everywhere the role matters; authorization reads this on every request.
|
||||||
|
/// Prefer the <see cref="Roles.Names"/> / <see cref="Roles.IsAdmin"/> helpers.
|
||||||
|
/// </summary>
|
||||||
|
public List<ApplicationUserRole> UserRoles { get; set; }
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
@@ -0,0 +1,16 @@
|
|||||||
|
namespace LaDOSE.Entity
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// The <c>applicationuserrole</c> join table. Declared explicitly rather than left
|
||||||
|
/// implicit so its columns match the table that already exists in the database
|
||||||
|
/// (<c>userid</c>, <c>roleid</c>, no surrogate key).
|
||||||
|
/// </summary>
|
||||||
|
public class ApplicationUserRole
|
||||||
|
{
|
||||||
|
public int UserId { get; set; }
|
||||||
|
public ApplicationUser User { get; set; }
|
||||||
|
|
||||||
|
public int RoleId { get; set; }
|
||||||
|
public ApplicationRole Role { get; set; }
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -9,6 +9,7 @@ namespace LaDOSE.Entity.Challonge
|
|||||||
public List<Game> Games{ get; set; }
|
public List<Game> Games{ get; set; }
|
||||||
|
|
||||||
public List<Result> Results { get; set; }
|
public List<Result> Results { get; set; }
|
||||||
|
public string Slug { get; set; }
|
||||||
}
|
}
|
||||||
|
|
||||||
public class Result
|
public class Result
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ namespace LaDOSE.Entity.Context
|
|||||||
{
|
{
|
||||||
public DbSet<Game> Game { get; set; }
|
public DbSet<Game> Game { get; set; }
|
||||||
public DbSet<ApplicationUser> ApplicationUser { get; set; }
|
public DbSet<ApplicationUser> ApplicationUser { get; set; }
|
||||||
|
public DbSet<ApplicationRole> ApplicationRole { get; set; }
|
||||||
|
|
||||||
public DbSet<Todo> Todo { get; set; }
|
public DbSet<Todo> Todo { get; set; }
|
||||||
|
|
||||||
@@ -40,6 +41,8 @@ namespace LaDOSE.Entity.Context
|
|||||||
{
|
{
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
protected override void OnModelCreating(ModelBuilder modelBuilder)
|
protected override void OnModelCreating(ModelBuilder modelBuilder)
|
||||||
{
|
{
|
||||||
|
|
||||||
@@ -47,6 +50,27 @@ namespace LaDOSE.Entity.Context
|
|||||||
base.OnModelCreating(modelBuilder);
|
base.OnModelCreating(modelBuilder);
|
||||||
|
|
||||||
|
|
||||||
|
#region Users and roles
|
||||||
|
|
||||||
|
// Maps onto the applicationrole / applicationuserrole tables that already exist
|
||||||
|
// in the schema. The join table is configured explicitly rather than as a
|
||||||
|
// many-to-many skip navigation so its columns are exactly userid + roleid,
|
||||||
|
// with the pair as the key and no surrogate id.
|
||||||
|
modelBuilder.Entity<ApplicationUserRole>(join =>
|
||||||
|
{
|
||||||
|
join.HasKey(ur => new { ur.UserId, ur.RoleId });
|
||||||
|
|
||||||
|
join.HasOne(ur => ur.User)
|
||||||
|
.WithMany(u => u.UserRoles)
|
||||||
|
.HasForeignKey(ur => ur.UserId);
|
||||||
|
|
||||||
|
join.HasOne(ur => ur.Role)
|
||||||
|
.WithMany()
|
||||||
|
.HasForeignKey(ur => ur.RoleId);
|
||||||
|
});
|
||||||
|
|
||||||
|
#endregion
|
||||||
|
|
||||||
modelBuilder.Entity<Event>()
|
modelBuilder.Entity<Event>()
|
||||||
.HasMany(s => s.Tournaments);
|
.HasMany(s => s.Tournaments);
|
||||||
|
|
||||||
|
|||||||
@@ -2,11 +2,18 @@
|
|||||||
|
|
||||||
<PropertyGroup>
|
<PropertyGroup>
|
||||||
<Platforms>AnyCPU;x64</Platforms>
|
<Platforms>AnyCPU;x64</Platforms>
|
||||||
<TargetFrameworks>net6.0;netcoreapp3.1</TargetFrameworks>
|
<TargetFramework>net9.0</TargetFramework>
|
||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<PackageReference Include="Pomelo.EntityFrameworkCore.MySql" Version="3.1.2" />
|
<PackageReference Include="Microsoft.EntityFrameworkCore" Version="8.0.12" />
|
||||||
|
<PackageReference Include="Newtonsoft.Json" Version="13.0.3" />
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<Reference Include="Microsoft.EntityFrameworkCore">
|
||||||
|
<HintPath>..\..\..\..\.nuget\packages\microsoft.entityframeworkcore\8.0.11\lib\net8.0\Microsoft.EntityFrameworkCore.dll</HintPath>
|
||||||
|
</Reference>
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
</Project>
|
</Project>
|
||||||
|
|||||||
@@ -0,0 +1,41 @@
|
|||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Linq;
|
||||||
|
|
||||||
|
namespace LaDOSE.Entity
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// The role names the API knows about. They must exist as rows in
|
||||||
|
/// <c>applicationrole</c> — see Sql/2026-08-05_roles.sql.
|
||||||
|
/// </summary>
|
||||||
|
public static class Roles
|
||||||
|
{
|
||||||
|
/// <summary>May manage user accounts.</summary>
|
||||||
|
public const string Admin = "Admin";
|
||||||
|
|
||||||
|
/// <summary>May use everything else. The absence of a role means the same thing.</summary>
|
||||||
|
public const string User = "User";
|
||||||
|
|
||||||
|
public static readonly string[] All = { Admin, User };
|
||||||
|
|
||||||
|
/// <summary>Case-insensitive, so 'admin' typed into the SQL seed still counts.</summary>
|
||||||
|
public static bool IsAdmin(this ApplicationUser user)
|
||||||
|
{
|
||||||
|
return user.Names().Any(name => Admin.Equals(name, StringComparison.OrdinalIgnoreCase));
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The user's role names. Empty when the user holds no role, and also when the
|
||||||
|
/// query did not include them — callers that authorize must load them.
|
||||||
|
/// </summary>
|
||||||
|
public static List<string> Names(this ApplicationUser user)
|
||||||
|
{
|
||||||
|
return user?.UserRoles?
|
||||||
|
.Select(userRole => userRole.Role?.Name)
|
||||||
|
.Where(name => !string.IsNullOrWhiteSpace(name))
|
||||||
|
.OrderBy(name => name)
|
||||||
|
.ToList()
|
||||||
|
?? new List<string>();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,74 @@
|
|||||||
|
using System.Collections.Generic;
|
||||||
|
|
||||||
|
namespace LaDOSE.Entity
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Aggregate over the persisted <see cref="Set"/> rows of one or more Events.
|
||||||
|
/// Not an entity: it is never mapped by EF, it is computed in memory by
|
||||||
|
/// StatisticsService and mapped to MatchStatsDTO by AutoMapper (same pattern as
|
||||||
|
/// <see cref="Challonge.TournamentsResult"/>).
|
||||||
|
/// Property names must stay identical to the DTO's, the mapping is by convention.
|
||||||
|
/// </summary>
|
||||||
|
public class MatchStats
|
||||||
|
{
|
||||||
|
/// <summary>How much of the requested scope actually has set data behind it.</summary>
|
||||||
|
public MatchCoverage Coverage { get; set; } = new MatchCoverage();
|
||||||
|
|
||||||
|
public List<PlayerMatchStats> Players { get; set; } = new List<PlayerMatchStats>();
|
||||||
|
|
||||||
|
public List<HeadToHead> HeadToHead { get; set; } = new List<HeadToHead>();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Set coverage is sparse: Challonge-imported events have no sets at all, and events
|
||||||
|
/// imported before SmashProvider.GetSets existed were never backfilled. These counters
|
||||||
|
/// let a caller say "12 of 40 brackets have match data" instead of implying completeness.
|
||||||
|
/// </summary>
|
||||||
|
public class MatchCoverage
|
||||||
|
{
|
||||||
|
/// <summary>Requested event ids that actually exist.</summary>
|
||||||
|
public int Events { get; set; }
|
||||||
|
|
||||||
|
/// <summary>Tournaments (brackets) belonging to those events.</summary>
|
||||||
|
public int Brackets { get; set; }
|
||||||
|
|
||||||
|
/// <summary>Of those brackets, how many have at least one set row.</summary>
|
||||||
|
public int BracketsWithSets { get; set; }
|
||||||
|
|
||||||
|
/// <summary>Total set rows in scope, including the ones skipped as unusable.</summary>
|
||||||
|
public int Sets { get; set; }
|
||||||
|
|
||||||
|
/// <summary>Sets with a determinable winner (unequal scores, two distinct real players).</summary>
|
||||||
|
public int DecidedSets { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
public class PlayerMatchStats
|
||||||
|
{
|
||||||
|
public int PlayerId { get; set; }
|
||||||
|
|
||||||
|
/// <summary>Gamertag, falling back to Name, else "#<id>".</summary>
|
||||||
|
public string Player { get; set; }
|
||||||
|
|
||||||
|
/// <summary>Decided sets only. Always equals Wins + Losses.</summary>
|
||||||
|
public int Sets { get; set; }
|
||||||
|
|
||||||
|
public int Wins { get; set; }
|
||||||
|
public int Losses { get; set; }
|
||||||
|
public int GamesWon { get; set; }
|
||||||
|
public int GamesLost { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// One row per unordered pair of players with at least one decided set.
|
||||||
|
/// A is always the lower PlayerId so WinsA / WinsB are unambiguous.
|
||||||
|
/// </summary>
|
||||||
|
public class HeadToHead
|
||||||
|
{
|
||||||
|
public int PlayerAId { get; set; }
|
||||||
|
public string PlayerA { get; set; }
|
||||||
|
public int PlayerBId { get; set; }
|
||||||
|
public string PlayerB { get; set; }
|
||||||
|
public int WinsA { get; set; }
|
||||||
|
public int WinsB { get; set; }
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,12 +1,14 @@
|
|||||||
<Project Sdk="Microsoft.NET.Sdk">
|
<Project Sdk="Microsoft.NET.Sdk">
|
||||||
|
|
||||||
<PropertyGroup>
|
<PropertyGroup>
|
||||||
<TargetFramework>net6.0</TargetFramework>
|
<TargetFramework>net9.0</TargetFramework>
|
||||||
<Platforms>AnyCPU;x64</Platforms>
|
<Platforms>AnyCPU;x64</Platforms>
|
||||||
|
<LangVersion>12</LangVersion>
|
||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<PackageReference Include="RestSharp" Version="106.11.4" />
|
<PackageReference Include="Newtonsoft.Json" Version="13.0.3" />
|
||||||
|
<PackageReference Include="RestSharp" Version="112.1.0" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
|
|||||||
@@ -2,9 +2,10 @@
|
|||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using LaDOSE.DTO;
|
using LaDOSE.DTO;
|
||||||
using LaDOSE.REST.Event;
|
using LaDOSE.REST.Event;
|
||||||
|
using Newtonsoft.Json;
|
||||||
using RestSharp;
|
using RestSharp;
|
||||||
using RestSharp.Authenticators;
|
using RestSharp.Authenticators;
|
||||||
using RestSharp.Serialization.Json;
|
|
||||||
|
|
||||||
namespace LaDOSE.REST
|
namespace LaDOSE.REST
|
||||||
{
|
{
|
||||||
@@ -23,39 +24,54 @@ namespace LaDOSE.REST
|
|||||||
|
|
||||||
public event EventHandler<UpdatedJwtEventHandler> UpdatedJwtEvent;
|
public event EventHandler<UpdatedJwtEventHandler> UpdatedJwtEvent;
|
||||||
|
|
||||||
public RestService() { }
|
|
||||||
|
public RestService()
|
||||||
|
{
|
||||||
|
|
||||||
|
}
|
||||||
|
public RestService(Uri url, string user, string password)
|
||||||
|
{
|
||||||
|
Client = new RestClient(url);
|
||||||
|
this.username = user;
|
||||||
|
this.password = password;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
public void Connect(Uri url, string user, string password)
|
public void Connect(Uri url, string user, string password)
|
||||||
{
|
{
|
||||||
Client = new RestClient(url);
|
// Client = new RestClient(url);
|
||||||
|
// this.username = user;
|
||||||
|
// this.password = password;
|
||||||
|
string token;
|
||||||
|
try
|
||||||
|
{
|
||||||
|
token = GetToken(user, password);
|
||||||
|
}
|
||||||
|
catch (Exception)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
Client = new RestClient(url, options =>
|
||||||
|
{
|
||||||
#if DEBUG
|
#if DEBUG
|
||||||
Client.Timeout = 999*1000;
|
options.MaxTimeout = Int32.MaxValue;
|
||||||
#endif
|
#endif
|
||||||
this.username = user;
|
options.Authenticator = new JwtAuthenticator(token);
|
||||||
this.password = password;
|
});
|
||||||
GetToken(user, password);
|
|
||||||
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private void GetToken(string user, string password)
|
private string GetToken(string user, string password)
|
||||||
{
|
{
|
||||||
var restRequest = new RestRequest("users/auth", Method.POST);
|
var restRequest = new RestRequest("users/auth", Method.Post);
|
||||||
restRequest.AddJsonBody(new {username = user, password = password});
|
restRequest.AddJsonBody(new {username = user, password = password});
|
||||||
|
|
||||||
var response = Client.Post(restRequest);
|
var response = Client.Post<ApplicationUserDTO>(restRequest);
|
||||||
if (response.IsSuccessful)
|
//var applicationUser = JsonConvert.DeserializeObject<ApplicationUserDTO>(response.Content);
|
||||||
{
|
this.Auth = response;
|
||||||
JsonDeserializer d = new JsonDeserializer();
|
return response.Token;
|
||||||
var applicationUser = d.Deserialize<ApplicationUserDTO>(response);
|
|
||||||
this.Auth = applicationUser;
|
|
||||||
Client.Authenticator = new JwtAuthenticator($"{applicationUser.Token}");
|
|
||||||
RaiseUpdatedJwtEvent(new UpdatedJwtEventHandler(this.Auth));
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
|
|
||||||
throw new Exception("unable to contact services");
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private void RaiseUpdatedJwtEvent(UpdatedJwtEventHandler auth)
|
private void RaiseUpdatedJwtEvent(UpdatedJwtEventHandler auth)
|
||||||
@@ -69,126 +85,128 @@ namespace LaDOSE.REST
|
|||||||
|
|
||||||
private void CheckToken()
|
private void CheckToken()
|
||||||
{
|
{
|
||||||
if (this.Auth == null || this.Auth.Expire <= DateTime.Now)
|
if (this.Auth == null || this.Auth.Expire.ToUniversalTime() <= DateTime.Now.ToUniversalTime())
|
||||||
{
|
{
|
||||||
GetToken(this.username,this.password);
|
GetToken(this.username,this.password);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#region PostFix
|
// #region PostFix
|
||||||
|
//
|
||||||
private T Post<T>(string resource,T entity)
|
// private T Post<T>(string resource,T entity)
|
||||||
{
|
// {
|
||||||
var json = new RestSharp.Serialization.Json.JsonSerializer();
|
// var json = new RestSharp.Serialization.Json.JsonSerializer();
|
||||||
var jsonD = new RestSharp.Serialization.Json.JsonDeserializer();
|
// var jsonD = new RestSharp.Serialization.Json.JsonDeserializer();
|
||||||
var request = new RestRequest();
|
// var request = new RestRequest();
|
||||||
request.Method = Method.POST;
|
// request.Method = Method.Post;
|
||||||
request.Resource = resource;
|
// request.Resource = resource;
|
||||||
request.AddHeader("Accept", "application/json");
|
// request.AddHeader("Accept", "application/json");
|
||||||
request.AddHeader("Content-type", "application/json");
|
// request.AddHeader("Content-type", "application/json");
|
||||||
request.Parameters.Clear();
|
// request.Parameters.Clear();
|
||||||
request.AddParameter("application/json; charset=utf-8", json.Serialize(entity), ParameterType.RequestBody);
|
// request.AddParameter("application/json; charset=utf-8", json.Serialize(entity), ParameterType.RequestBody);
|
||||||
request.AddObject(entity);
|
// request.AddObject(entity);
|
||||||
var response = Client.Execute(request);
|
// var response = Client.Execute(request);
|
||||||
//var content = response.Content; // raw content as string
|
// //var content = response.Content; // raw content as string
|
||||||
try
|
// try
|
||||||
{
|
// {
|
||||||
return jsonD.Deserialize<T>(response);
|
// return jsonD.Deserialize<T>(response);
|
||||||
}
|
// }
|
||||||
catch (Exception)
|
// catch (Exception)
|
||||||
{
|
// {
|
||||||
return default(T);
|
// return default(T);
|
||||||
}
|
// }
|
||||||
|
//
|
||||||
|
//
|
||||||
}
|
// }
|
||||||
private R Post<P,R>(string resource, P entity)
|
// private R Post<P,R>(string resource, P entity)
|
||||||
{
|
// {
|
||||||
var json = new RestSharp.Serialization.Json.JsonSerializer();
|
// var json = new RestSharp.Serialization.Json.JsonSerializer();
|
||||||
var jsonD = new RestSharp.Serialization.Json.JsonDeserializer();
|
// var jsonD = new RestSharp.Serialization.Json.JsonDeserializer();
|
||||||
var request = new RestRequest();
|
// var request = new RestRequest();
|
||||||
request.Method = Method.POST;
|
// request.Method = Method.Post;
|
||||||
request.Resource = resource;
|
// request.Resource = resource;
|
||||||
request.AddHeader("Accept", "application/json");
|
// request.AddHeader("Accept", "application/json");
|
||||||
request.AddHeader("Content-type", "application/json");
|
// request.AddHeader("Content-type", "application/json");
|
||||||
request.Parameters.Clear();
|
// request.Parameters.Clear();
|
||||||
request.AddParameter("application/json; charset=utf-8", json.Serialize(entity), ParameterType.RequestBody);
|
// request.AddParameter("application/json; charset=utf-8", json.Serialize(entity), ParameterType.RequestBody);
|
||||||
//request.AddObject(entity);
|
// //request.AddObject(entity);
|
||||||
var response = Client.Execute(request);
|
// var response = Client.Execute(request);
|
||||||
//var content = response.Content; // raw content as string
|
// //var content = response.Content; // raw content as string
|
||||||
try
|
// try
|
||||||
{
|
// {
|
||||||
return jsonD.Deserialize<R>(response);
|
// return jsonD.Deserialize<R>(response);
|
||||||
}
|
// }
|
||||||
catch (Exception)
|
// catch (Exception)
|
||||||
{
|
// {
|
||||||
return default(R);
|
// return default(R);
|
||||||
}
|
// }
|
||||||
|
//
|
||||||
|
//
|
||||||
}
|
// }
|
||||||
|
//
|
||||||
#endregion
|
// #endregion
|
||||||
|
|
||||||
#region WordPress
|
#region WordPress
|
||||||
public List<WPEventDTO> GetEvents()
|
public List<WPEventDTO> GetEvents()
|
||||||
{
|
{
|
||||||
CheckToken();
|
CheckToken();
|
||||||
var restRequest = new RestRequest("/api/wordpress/WPEvent", Method.GET);
|
var restRequest = new RestRequest("/api/wordpress/WPEvent", Method.Get);
|
||||||
var restResponse = Client.Get<List<WPEventDTO>>(restRequest);
|
var restResponse = Client.Get<List<WPEventDTO>>(restRequest);
|
||||||
return restResponse.Data;
|
return restResponse;
|
||||||
}
|
}
|
||||||
public WPEventDTO GetNextEvent()
|
public WPEventDTO GetNextEvent()
|
||||||
{
|
{
|
||||||
CheckToken();
|
CheckToken();
|
||||||
var restRequest = new RestRequest("/api/wordpress/NextEvent", Method.GET);
|
var restRequest = new RestRequest("/api/wordpress/NextEvent", Method.Get);
|
||||||
var restResponse = Client.Get<WPEventDTO>(restRequest);
|
var restResponse = Client.Get<WPEventDTO>(restRequest);
|
||||||
return restResponse.Data;
|
return restResponse;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
public string GetLastChallonge()
|
public string GetLastChallonge()
|
||||||
{
|
{
|
||||||
CheckToken();
|
CheckToken();
|
||||||
var restRequest = new RestRequest($"/api/wordpress/GetLastChallonge/", Method.GET);
|
var restRequest = new RestRequest($"/api/wordpress/GetLastChallonge/", Method.Get);
|
||||||
var restResponse = Client.Get(restRequest);
|
var restResponse = Client.Get(restRequest);
|
||||||
return restResponse.Content;
|
return restResponse.Content;
|
||||||
}
|
}
|
||||||
public string CreateChallonge(int gameId, int eventId)
|
public string CreateChallonge(int gameId, int eventId)
|
||||||
{
|
{
|
||||||
CheckToken();
|
CheckToken();
|
||||||
var restRequest = new RestRequest($"/api/wordpress/CreateChallonge/{gameId}/{eventId}", Method.GET);
|
var restRequest = new RestRequest($"/api/wordpress/CreateChallonge/{gameId}/{eventId}", Method.Get);
|
||||||
var restResponse = Client.Get(restRequest);
|
var restResponse = Client.Get(restRequest);
|
||||||
return restResponse.Content;
|
return restResponse.Content;
|
||||||
}
|
}
|
||||||
public string CreateChallonge2(int gameId, int eventId, List<WPUserDTO> optionalPlayers)
|
public string CreateChallonge2(int gameId, int eventId, List<WPUserDTO> optionalPlayers)
|
||||||
{
|
{
|
||||||
CheckToken();
|
CheckToken();
|
||||||
var restResponse = Post<List<WPUserDTO>,string>($"/api/wordpress/CreateChallonge/{gameId}/{eventId}",optionalPlayers);
|
RestRequest r =
|
||||||
|
new RestRequest($"/api/wordpress/CreateChallonge/{gameId}/{eventId}").AddJsonBody(optionalPlayers);
|
||||||
|
var restResponse = Client.Post<string>(r);
|
||||||
return restResponse;
|
return restResponse;
|
||||||
}
|
}
|
||||||
public bool RefreshDb()
|
public bool RefreshDb()
|
||||||
{
|
{
|
||||||
CheckToken();
|
CheckToken();
|
||||||
var restRequest = new RestRequest("/api/Wordpress/UpdateDb", Method.GET);
|
var restRequest = new RestRequest("/api/Wordpress/UpdateDb", Method.Get);
|
||||||
var restResponse = Client.Get<bool>(restRequest);
|
var restResponse = Client.Get<bool>(restRequest);
|
||||||
return restResponse.Data;
|
return restResponse;
|
||||||
}
|
}
|
||||||
|
|
||||||
public List<WPUserDTO> GetUsers(int wpEventId, int gameId)
|
public List<WPUserDTO> GetUsers(int wpEventId, int gameId)
|
||||||
{
|
{
|
||||||
CheckToken();
|
CheckToken();
|
||||||
var restRequest = new RestRequest($"/api/Wordpress/GetUsers/{wpEventId}/{gameId}", Method.GET);
|
var restRequest = new RestRequest($"/api/Wordpress/GetUsers/{wpEventId}/{gameId}", Method.Get);
|
||||||
var restResponse = Client.Get<List<WPUserDTO>>(restRequest);
|
var restResponse = Client.Get<List<WPUserDTO>>(restRequest);
|
||||||
return restResponse.Data;
|
return restResponse;
|
||||||
}
|
}
|
||||||
|
|
||||||
public List<WPUserDTO> GetUsersOptions(int wpEventId, int gameId)
|
public List<WPUserDTO> GetUsersOptions(int wpEventId, int gameId)
|
||||||
{
|
{
|
||||||
CheckToken();
|
CheckToken();
|
||||||
var restRequest = new RestRequest($"/api/Wordpress/GetUsersOptions/{wpEventId}/{gameId}", Method.GET);
|
var restRequest = new RestRequest($"/api/Wordpress/GetUsersOptions/{wpEventId}/{gameId}", Method.Get);
|
||||||
var restResponse = Client.Get<List<WPUserDTO>>(restRequest);
|
var restResponse = Client.Get<List<WPUserDTO>>(restRequest);
|
||||||
return restResponse.Data;
|
return restResponse;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@@ -198,20 +216,29 @@ namespace LaDOSE.REST
|
|||||||
public List<GameDTO> GetGames()
|
public List<GameDTO> GetGames()
|
||||||
{
|
{
|
||||||
CheckToken();
|
CheckToken();
|
||||||
var restRequest = new RestRequest("/api/Game", Method.GET);
|
var restRequest = new RestRequest("/api/Game", Method.Get);
|
||||||
var restResponse = Client.Get<List<GameDTO>>(restRequest);
|
var restResponse = Client.Get<List<GameDTO>>(restRequest);
|
||||||
return restResponse.Data;
|
return restResponse;
|
||||||
|
}
|
||||||
|
|
||||||
|
public List<GameDTO> GetSmashGames(string name)
|
||||||
|
{
|
||||||
|
CheckToken();
|
||||||
|
var restRequest = new RestRequest($"/api/Game/Smash/{name}", Method.Get);
|
||||||
|
var restResponse = Client.Get<List<GameDTO>>(restRequest);
|
||||||
|
return restResponse;
|
||||||
}
|
}
|
||||||
|
|
||||||
public GameDTO UpdateGame(GameDTO game)
|
public GameDTO UpdateGame(GameDTO game)
|
||||||
{
|
{
|
||||||
CheckToken();
|
CheckToken();
|
||||||
return Post("Api/Game", game);
|
RestRequest r = new RestRequest("Api/Game").AddJsonBody(game);
|
||||||
|
return Client.Post<GameDTO>(r);
|
||||||
}
|
}
|
||||||
public bool DeleteGame(int gameId)
|
public bool DeleteGame(int gameId)
|
||||||
{
|
{
|
||||||
CheckToken();
|
CheckToken();
|
||||||
var restRequest = new RestRequest($"/api/Game/{gameId}", Method.DELETE);
|
var restRequest = new RestRequest($"/api/Game/{gameId}", Method.Delete);
|
||||||
var restResponse = Client.Execute(restRequest);
|
var restResponse = Client.Execute(restRequest);
|
||||||
return restResponse.IsSuccessful;
|
return restResponse.IsSuccessful;
|
||||||
}
|
}
|
||||||
@@ -227,26 +254,27 @@ namespace LaDOSE.REST
|
|||||||
public List<TodoDTO> GetTodos()
|
public List<TodoDTO> GetTodos()
|
||||||
{
|
{
|
||||||
CheckToken();
|
CheckToken();
|
||||||
var restRequest = new RestRequest("/api/Todo", Method.GET);
|
var restRequest = new RestRequest("/api/Todo", Method.Get);
|
||||||
var restResponse = Client.Get<List<TodoDTO>>(restRequest);
|
var restResponse = Client.Get<List<TodoDTO>>(restRequest);
|
||||||
return restResponse.Data;
|
return restResponse;
|
||||||
}
|
}
|
||||||
public TodoDTO GetTodoById(int id)
|
public TodoDTO GetTodoById(int id)
|
||||||
{
|
{
|
||||||
CheckToken();
|
CheckToken();
|
||||||
var restRequest = new RestRequest($"/api/Todo/{id}", Method.GET);
|
var restRequest = new RestRequest($"/api/Todo/{id}", Method.Get);
|
||||||
var restResponse = Client.Get<TodoDTO>(restRequest);
|
var restResponse = Client.Get<TodoDTO>(restRequest);
|
||||||
return restResponse.Data;
|
return restResponse;
|
||||||
}
|
}
|
||||||
public TodoDTO UpdateTodo(TodoDTO Todo)
|
public TodoDTO UpdateTodo(TodoDTO Todo)
|
||||||
{
|
{
|
||||||
CheckToken();
|
CheckToken();
|
||||||
return Post("Api/Todo", Todo);
|
var restRequest = new RestRequest($"/api/Todo/", Method.Post).AddJsonBody(Todo);
|
||||||
|
return Client.Post<TodoDTO>(restRequest);
|
||||||
}
|
}
|
||||||
public bool DeleteTodo(int todoId)
|
public bool DeleteTodo(int todoId)
|
||||||
{
|
{
|
||||||
CheckToken();
|
CheckToken();
|
||||||
var restRequest = new RestRequest($"/api/Todo/{todoId}", Method.DELETE);
|
var restRequest = new RestRequest($"/api/Todo/{todoId}", Method.Delete);
|
||||||
var restResponse = Client.Execute(restRequest);
|
var restResponse = Client.Execute(restRequest);
|
||||||
return restResponse.IsSuccessful;
|
return restResponse.IsSuccessful;
|
||||||
}
|
}
|
||||||
@@ -258,9 +286,9 @@ namespace LaDOSE.REST
|
|||||||
public TournamentsResultDTO Test(string test)
|
public TournamentsResultDTO Test(string test)
|
||||||
{
|
{
|
||||||
CheckToken();
|
CheckToken();
|
||||||
var restRequest = new RestRequest($"Api/Test/Test/{test}", Method.GET);
|
var restRequest = new RestRequest($"Api/Test/Test/{test}", Method.Get);
|
||||||
var restResponse = Client.Get<TournamentsResultDTO>(restRequest);
|
var restResponse = Client.Get<TournamentsResultDTO>(restRequest);
|
||||||
return restResponse.Data;
|
return restResponse;
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -270,14 +298,17 @@ namespace LaDOSE.REST
|
|||||||
public List<TournamentDTO> GetTournaments(TimeRangeDTO timeRange)
|
public List<TournamentDTO> GetTournaments(TimeRangeDTO timeRange)
|
||||||
{
|
{
|
||||||
CheckToken();
|
CheckToken();
|
||||||
List<TournamentDTO> tournamentDtos = Post<TimeRangeDTO, List<TournamentDTO>>("/api/Tournament/GetTournaments",timeRange);
|
RestRequest r = new RestRequest("/api/Tournament/GetTournaments").AddJsonBody(timeRange);
|
||||||
|
List<TournamentDTO> tournamentDtos = Client.Post<List<TournamentDTO>>(r);
|
||||||
return tournamentDtos;
|
return tournamentDtos;
|
||||||
}
|
}
|
||||||
|
|
||||||
public TournamentsResultDTO GetResults(List<int> ids)
|
public TournamentsResultDTO GetResults(List<int> ids)
|
||||||
{
|
{
|
||||||
|
|
||||||
CheckToken();
|
CheckToken();
|
||||||
return Post<List<int>,TournamentsResultDTO>("Api/Tournament/GetResults", ids);
|
var restRequest = new RestRequest("Api/Tournament/GetResults", Method.Post).AddJsonBody(ids);
|
||||||
|
return Client.Post<TournamentsResultDTO>(restRequest);
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -285,33 +316,34 @@ namespace LaDOSE.REST
|
|||||||
public bool ParseSmash(string slug)
|
public bool ParseSmash(string slug)
|
||||||
{
|
{
|
||||||
CheckToken();
|
CheckToken();
|
||||||
var restRequest = new RestRequest($"Api/Tournament/ParseSmash/{slug}", Method.GET);
|
var restRequest = new RestRequest($"Api/Tournament/ParseSmash/{slug}", Method.Get);
|
||||||
var restResponse = Client.Get<bool>(restRequest);
|
var restResponse = Client.Get<bool>(restRequest);
|
||||||
return restResponse.Data;
|
return restResponse;
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public bool ParseChallonge(List<int> ids)
|
public bool ParseChallonge(List<int> ids)
|
||||||
{
|
{
|
||||||
CheckToken();
|
CheckToken();
|
||||||
return Post<List<int>, bool>("Api/Tournament/ParseChallonge", ids);
|
var restRequest = new RestRequest("Api/Tournament/ParseChallonge", Method.Post).AddJsonBody(ids);
|
||||||
|
return Client.Post<bool>(restRequest);
|
||||||
}
|
}
|
||||||
#endregion
|
#endregion
|
||||||
#region Tournamenet Event / Player
|
#region Tournamenet Event / Player
|
||||||
public List<EventDTO> GetAllEvents()
|
public List<EventDTO> GetAllEvents()
|
||||||
{
|
{
|
||||||
CheckToken();
|
CheckToken();
|
||||||
var restRequest = new RestRequest("/api/Event", Method.GET);
|
var restRequest = new RestRequest("/api/Event", Method.Get);
|
||||||
var restResponse = Client.Get<List<EventDTO>>(restRequest);
|
var restResponse = Client.Get<List<EventDTO>>(restRequest);
|
||||||
return restResponse.Data;
|
return restResponse;
|
||||||
}
|
}
|
||||||
|
|
||||||
public List<string> GetPlayers(string slug)
|
public List<string> GetPlayers(string slug)
|
||||||
{
|
{
|
||||||
CheckToken();
|
CheckToken();
|
||||||
var restRequest = new RestRequest($"/api/Tournament/GetPLayers/{slug}", Method.GET);
|
var restRequest = new RestRequest($"/api/Tournament/GetPLayers/{slug}", Method.Get);
|
||||||
var restResponse = Client.Get<List<string>>(restRequest);
|
var restResponse = Client.Get<List<string>>(restRequest);
|
||||||
return restResponse.Data;
|
return restResponse;
|
||||||
}
|
}
|
||||||
#endregion
|
#endregion
|
||||||
|
|
||||||
@@ -321,23 +353,24 @@ namespace LaDOSE.REST
|
|||||||
public bool CreateBotEvent(string eventName)
|
public bool CreateBotEvent(string eventName)
|
||||||
{
|
{
|
||||||
CheckToken();
|
CheckToken();
|
||||||
var restRequest = new RestRequest($"/api/BotEvent/CreateBotEvent/{eventName}", Method.GET);
|
var restRequest = new RestRequest($"/api/BotEvent/CreateBotEvent/{eventName}", Method.Get);
|
||||||
var restResponse = Client.Get<bool>(restRequest);
|
var restResponse = Client.Get<bool>(restRequest);
|
||||||
return restResponse.Data;
|
return restResponse;
|
||||||
}
|
}
|
||||||
|
|
||||||
public BotEventDTO GetLastBotEvent()
|
public BotEventDTO GetLastBotEvent()
|
||||||
{
|
{
|
||||||
CheckToken();
|
CheckToken();
|
||||||
var restRequest = new RestRequest($"/api/BotEvent/GetLastBotEvent/", Method.GET);
|
var restRequest = new RestRequest($"/api/BotEvent/GetLastBotEvent/", Method.Get);
|
||||||
var restResponse = Client.Post<BotEventDTO>(restRequest);
|
var restResponse = Client.Post<BotEventDTO>(restRequest);
|
||||||
return restResponse.Data;
|
return restResponse;
|
||||||
}
|
}
|
||||||
|
|
||||||
public bool ResultBotEvent(BotEventSendDTO result)
|
public bool ResultBotEvent(BotEventSendDTO result)
|
||||||
{
|
{
|
||||||
CheckToken();
|
CheckToken();
|
||||||
return Post<BotEventSendDTO,bool>("/api/BotEvent/ResultBotEvent", result);
|
var restRequest = new RestRequest("/api/BotEvent/ResultBotEvent", Method.Post).AddJsonBody(result);
|
||||||
|
return Client.Post<bool>(restRequest);
|
||||||
}
|
}
|
||||||
|
|
||||||
#endregion
|
#endregion
|
||||||
|
|||||||
@@ -10,6 +10,9 @@ namespace LaDOSE.Business.Interface
|
|||||||
{
|
{
|
||||||
Task<List<ChallongeTournament>> GetTournaments(DateTime? start, DateTime? end);
|
Task<List<ChallongeTournament>> GetTournaments(DateTime? start, DateTime? end);
|
||||||
Task<Event> ParseSmash(string tournamentSlug);
|
Task<Event> ParseSmash(string tournamentSlug);
|
||||||
|
|
||||||
|
Task<List<Game>> GetSmashGame(string name);
|
||||||
|
|
||||||
//Task<List<Event>> ParseChallonge(List<int> ids);
|
//Task<List<Event>> ParseChallonge(List<int> ids);
|
||||||
|
|
||||||
//Task<TournamentsResult> GetChallongeTournamentsResult(List<int> ids);
|
//Task<TournamentsResult> GetChallongeTournamentsResult(List<int> ids);
|
||||||
|
|||||||
@@ -18,6 +18,7 @@ namespace LaDOSE.Business.Interface
|
|||||||
|
|
||||||
Task<TournamentResponse> GetNames(string slug);
|
Task<TournamentResponse> GetNames(string slug);
|
||||||
|
|
||||||
|
Task<List<Game>> GetGames(string name);
|
||||||
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -0,0 +1,16 @@
|
|||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
using LaDOSE.Entity;
|
||||||
|
|
||||||
|
namespace LaDOSE.Business.Interface
|
||||||
|
{
|
||||||
|
public interface IStatisticsService
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Aggregate the persisted sets of the given Events into per-player records,
|
||||||
|
/// head-to-head records and a coverage report.
|
||||||
|
/// A null or empty id list yields a well-formed, zeroed <see cref="MatchStats"/>.
|
||||||
|
/// </summary>
|
||||||
|
Task<MatchStats> GetMatchStats(List<int> eventIds);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -8,8 +8,11 @@ namespace LaDOSE.Business.Interface
|
|||||||
ApplicationUser Authenticate(string username, string password);
|
ApplicationUser Authenticate(string username, string password);
|
||||||
IEnumerable<ApplicationUser> GetAll();
|
IEnumerable<ApplicationUser> GetAll();
|
||||||
ApplicationUser GetById(int id);
|
ApplicationUser GetById(int id);
|
||||||
ApplicationUser Create(ApplicationUser user, string password);
|
ApplicationUser Create(ApplicationUser user, string password, IEnumerable<string> roleNames = null);
|
||||||
void Update(ApplicationUser user, string password = null);
|
void Update(ApplicationUser user, string password = null);
|
||||||
void Delete(int id);
|
void Delete(int id);
|
||||||
|
|
||||||
|
/// <summary>The roles that exist in the database — reference data, not created at runtime.</summary>
|
||||||
|
IEnumerable<ApplicationRole> GetAllRoles();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -11,7 +11,6 @@ namespace LaDOSE.Business.Interface
|
|||||||
List<WPEvent> GetWpEvent();
|
List<WPEvent> GetWpEvent();
|
||||||
List<WPUser> GetBooking(int wpEventId, Game game);
|
List<WPUser> GetBooking(int wpEventId, Game game);
|
||||||
List<WPUser> GetBookingOptions(int wpEventId, Game game);
|
List<WPUser> GetBookingOptions(int wpEventId, Game game);
|
||||||
bool UpdateBooking();
|
|
||||||
string CreateChallonge(int gameId, int wpEventId, IList<WPUser> additionPlayers);
|
string CreateChallonge(int gameId, int wpEventId, IList<WPUser> additionPlayers);
|
||||||
|
|
||||||
Task<string> GetLastChallonge();
|
Task<string> GetLastChallonge();
|
||||||
|
|||||||
@@ -1,15 +1,17 @@
|
|||||||
<Project Sdk="Microsoft.NET.Sdk">
|
<Project Sdk="Microsoft.NET.Sdk">
|
||||||
|
|
||||||
<PropertyGroup>
|
<PropertyGroup>
|
||||||
<TargetFramework>net6.0</TargetFramework>
|
<TargetFramework>net9.0</TargetFramework>
|
||||||
<AssemblyName>LaDOSE.Business</AssemblyName>
|
<AssemblyName>LaDOSE.Business</AssemblyName>
|
||||||
<RootNamespace>LaDOSE.Business</RootNamespace>
|
<RootNamespace>LaDOSE.Business</RootNamespace>
|
||||||
<Platforms>AnyCPU;x64</Platforms>
|
<Platforms>AnyCPU;x64</Platforms>
|
||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<PackageReference Include="GraphQL.Client" Version="4.0.2" />
|
<PackageReference Include="GraphQL.Client" Version="6.1.0" />
|
||||||
<PackageReference Include="GraphQL.Client.Serializer.Newtonsoft" Version="4.0.2" />
|
<PackageReference Include="GraphQL.Client.Serializer.Newtonsoft" Version="6.1.0" />
|
||||||
|
<PackageReference Include="Microsoft.EntityFrameworkCore" Version="8.0.12" />
|
||||||
|
<PackageReference Include="Newtonsoft.Json" Version="13.0.3" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
|
|||||||
@@ -15,6 +15,7 @@ namespace LaDOSE.Business.Provider.SmashProvider
|
|||||||
{
|
{
|
||||||
public class SmashProvider : ISmashProvider
|
public class SmashProvider : ISmashProvider
|
||||||
{
|
{
|
||||||
|
private static string API_FQDN = "api.start.gg";
|
||||||
public string ApiKey { get; set; }
|
public string ApiKey { get; set; }
|
||||||
//public SmashProvider(string apiKey)
|
//public SmashProvider(string apiKey)
|
||||||
//{
|
//{
|
||||||
@@ -34,7 +35,7 @@ namespace LaDOSE.Business.Provider.SmashProvider
|
|||||||
|
|
||||||
private async Task<T> QuerySmash<T>(GraphQLRequest req)
|
private async Task<T> QuerySmash<T>(GraphQLRequest req)
|
||||||
{
|
{
|
||||||
var graphQLClient = new GraphQLHttpClient("https://api.smash.gg/gql/alpha", new NewtonsoftJsonSerializer());
|
var graphQLClient = new GraphQLHttpClient($"https://{API_FQDN}/gql/alpha", new NewtonsoftJsonSerializer());
|
||||||
graphQLClient.HttpClient.DefaultRequestHeaders.Add("Authorization", $"Bearer {ApiKey}");
|
graphQLClient.HttpClient.DefaultRequestHeaders.Add("Authorization", $"Bearer {ApiKey}");
|
||||||
|
|
||||||
var graphQLResponse = await graphQLClient.SendQueryAsync<T>(req);
|
var graphQLResponse = await graphQLClient.SendQueryAsync<T>(req);
|
||||||
@@ -47,6 +48,37 @@ namespace LaDOSE.Business.Provider.SmashProvider
|
|||||||
return graphQLResponse.Data;
|
return graphQLResponse.Data;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public async Task<List<Game>> GetGames(string game)
|
||||||
|
{
|
||||||
|
var query = new GraphQLRequest()
|
||||||
|
{
|
||||||
|
Query = @"
|
||||||
|
query VideogameQuery($name:String) {
|
||||||
|
videogames(query: { filter: { name: $name }, perPage: 5 }) {
|
||||||
|
nodes {
|
||||||
|
id
|
||||||
|
name
|
||||||
|
displayName
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
",
|
||||||
|
OperationName = "VideogameQuery",
|
||||||
|
Variables = new
|
||||||
|
{
|
||||||
|
name = game,
|
||||||
|
}
|
||||||
|
};
|
||||||
|
VideoGamesResponse querySmash = await QuerySmash<VideoGamesResponse>(query);
|
||||||
|
if (querySmash.videogames != null)
|
||||||
|
{
|
||||||
|
return querySmash.videogames.nodes.Select(e => new Game() { Id = e.id, Name = e.Name }).ToList();
|
||||||
|
}
|
||||||
|
|
||||||
|
return new List<Game>();
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
public async Task<Event> GetEvent(string slug)
|
public async Task<Event> GetEvent(string slug)
|
||||||
{
|
{
|
||||||
|
|
||||||
@@ -315,7 +347,7 @@ namespace LaDOSE.Business.Provider.SmashProvider
|
|||||||
public async Task<TournamentResponse> GetNames(string slug)
|
public async Task<TournamentResponse> GetNames(string slug)
|
||||||
{
|
{
|
||||||
|
|
||||||
var graphQLClient = new GraphQLHttpClient("https://api.smash.gg/gql/alpha", new NewtonsoftJsonSerializer());
|
var graphQLClient = new GraphQLHttpClient($"https://{API_FQDN}/gql/alpha", new NewtonsoftJsonSerializer());
|
||||||
graphQLClient.HttpClient.DefaultRequestHeaders.Add("Authorization", $"Bearer {ApiKey}");
|
graphQLClient.HttpClient.DefaultRequestHeaders.Add("Authorization", $"Bearer {ApiKey}");
|
||||||
var Event = new GraphQLRequest
|
var Event = new GraphQLRequest
|
||||||
{
|
{
|
||||||
@@ -381,7 +413,7 @@ namespace LaDOSE.Business.Provider.SmashProvider
|
|||||||
public async Task<TournamentResponse> GetTournament(string slug)
|
public async Task<TournamentResponse> GetTournament(string slug)
|
||||||
{
|
{
|
||||||
|
|
||||||
var graphQLClient = new GraphQLHttpClient("https://api.smash.gg/gql/alpha", new NewtonsoftJsonSerializer());
|
var graphQLClient = new GraphQLHttpClient($"https://{API_FQDN}/gql/alpha", new NewtonsoftJsonSerializer());
|
||||||
graphQLClient.HttpClient.DefaultRequestHeaders.Add("Authorization", $"Bearer {ApiKey}");
|
graphQLClient.HttpClient.DefaultRequestHeaders.Add("Authorization", $"Bearer {ApiKey}");
|
||||||
var Event = new GraphQLRequest
|
var Event = new GraphQLRequest
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
using System;
|
#nullable enable
|
||||||
|
using System;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using Newtonsoft.Json;
|
using Newtonsoft.Json;
|
||||||
using Newtonsoft.Json.Converters;
|
using Newtonsoft.Json.Converters;
|
||||||
@@ -11,62 +12,62 @@ namespace LaDOSE.Business.Provider.SmashProvider
|
|||||||
public int totalPages { get; set; }
|
public int totalPages { get; set; }
|
||||||
public int page { get; set; }
|
public int page { get; set; }
|
||||||
public int perPage { get; set; }
|
public int perPage { get; set; }
|
||||||
public string sortBy { get; set; }
|
public string? sortBy { get; set; }
|
||||||
public string filter { get; set; }
|
public string? filter { get; set; }
|
||||||
}
|
}
|
||||||
|
|
||||||
public class TournamentType
|
public class TournamentType
|
||||||
{
|
{
|
||||||
public int id { get; set; }
|
public int id { get; set; }
|
||||||
|
|
||||||
public string Name { get; set; }
|
public string? Name { get; set; }
|
||||||
|
|
||||||
[JsonConverter(typeof(UnixDateTimeConverter))]
|
[JsonConverter(typeof(UnixDateTimeConverter))]
|
||||||
public DateTime startAt { get; set; }
|
public DateTime startAt { get; set; }
|
||||||
public List<EventType> Events { get; set; }
|
public List<EventType>? Events { get; set; }
|
||||||
|
|
||||||
}
|
}
|
||||||
public class VideoGameType
|
public class VideoGameType
|
||||||
{
|
{
|
||||||
public int id { get; set; }
|
public int id { get; set; }
|
||||||
public string Name { get; set; }
|
public string? Name { get; set; }
|
||||||
}
|
}
|
||||||
public class ScoreType
|
public class ScoreType
|
||||||
{
|
{
|
||||||
public string label { get; set; }
|
public string? label { get; set; }
|
||||||
public int? value { get; set; }
|
public int? value { get; set; }
|
||||||
public string displayValue { get; set; }
|
public string? displayValue { get; set; }
|
||||||
|
|
||||||
}
|
}
|
||||||
public class StatType
|
public class StatType
|
||||||
{
|
{
|
||||||
public ScoreType score { get; set; }
|
public ScoreType? score { get; set; }
|
||||||
}
|
}
|
||||||
|
|
||||||
public class StandingType
|
public class StandingType
|
||||||
{
|
{
|
||||||
public string id { get; set; }
|
public string? id { get; set; }
|
||||||
|
|
||||||
public int placement { get; set; }
|
public int placement { get; set; }
|
||||||
|
|
||||||
public ParticipantType player { get; set; }
|
public ParticipantType? player { get; set; }
|
||||||
|
|
||||||
public StatType stats { get; set; }
|
public StatType? stats { get; set; }
|
||||||
|
|
||||||
public EntrantType entrant { get; set; }
|
public EntrantType? entrant { get; set; }
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public class ParticipantType
|
public class ParticipantType
|
||||||
{
|
{
|
||||||
public int id { get; set; }
|
public int id { get; set; }
|
||||||
public string gamerTag { get; set; }
|
public string? gamerTag { get; set; }
|
||||||
public UserType? user { get; set; }
|
public UserType? user { get; set; }
|
||||||
}
|
}
|
||||||
public class UserType
|
public class UserType
|
||||||
{
|
{
|
||||||
public int id { get; set; }
|
public int id { get; set; }
|
||||||
public string name { get; set; }
|
public string? name { get; set; }
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -74,68 +75,71 @@ namespace LaDOSE.Business.Provider.SmashProvider
|
|||||||
{
|
{
|
||||||
public int id { get; set; }
|
public int id { get; set; }
|
||||||
|
|
||||||
public string name { get; set; }
|
public string? name { get; set; }
|
||||||
public string state { get; set; }
|
public string? state { get; set; }
|
||||||
|
|
||||||
public VideoGameType videogame { get; set; }
|
public VideoGameType? videogame { get; set; }
|
||||||
public Node<StandingType> standings { get; set; }
|
public Node<StandingType>? standings { get; set; }
|
||||||
public Node<SetType> sets { get; set; }
|
public Node<SetType>? sets { get; set; }
|
||||||
|
|
||||||
public Node<EntrantType> entrants { get; set; }
|
public Node<EntrantType>? entrants { get; set; }
|
||||||
}
|
}
|
||||||
|
|
||||||
public class EntrantType
|
public class EntrantType
|
||||||
{
|
{
|
||||||
public int id { get; set; }
|
public int id { get; set; }
|
||||||
public string name { get; set; }
|
public string? name { get; set; }
|
||||||
public bool? isDisqualified { get; set; }
|
public bool? isDisqualified { get; set; }
|
||||||
public List<ParticipantType> participants { get; set; }
|
public List<ParticipantType>? participants { get; set; }
|
||||||
|
|
||||||
}
|
}
|
||||||
public class SlotType
|
public class SlotType
|
||||||
{
|
{
|
||||||
public string id { get; set; }
|
public string? id { get; set; }
|
||||||
public int slotIndex { get; set; }
|
public int slotIndex { get; set; }
|
||||||
|
|
||||||
public StandingType standing { get; set; }
|
public StandingType? standing { get; set; }
|
||||||
|
|
||||||
public EntrantType entrant { get; set; }
|
public EntrantType? entrant { get; set; }
|
||||||
|
|
||||||
}
|
}
|
||||||
public class SetType
|
public class SetType
|
||||||
{
|
{
|
||||||
public string id { get; set; }
|
public string? id { get; set; }
|
||||||
public int? lPlacement { get; set; }
|
public int? lPlacement { get; set; }
|
||||||
public int? wPlacement { get; set; }
|
public int? wPlacement { get; set; }
|
||||||
public int? round { get; set; }
|
public int? round { get; set; }
|
||||||
public List<SlotType> slots { get; set; }
|
public List<SlotType>? slots { get; set; }
|
||||||
public string identifier { get; set; }
|
public string? identifier { get; set; }
|
||||||
|
|
||||||
}
|
}
|
||||||
public class Node<T>
|
public class Node<T>
|
||||||
{
|
{
|
||||||
public PageInfoType pageInfo { get; set; }
|
public PageInfoType? pageInfo { get; set; }
|
||||||
public List<T> nodes { get; set; }
|
public List<T>? nodes { get; set; }
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public class VideoGamesResponse
|
||||||
|
{
|
||||||
|
public Node<VideoGameType>? videogames {get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
public class TournamentResponse
|
public class TournamentResponse
|
||||||
{
|
{
|
||||||
public TournamentType Tournament { get; set; }
|
public TournamentType? Tournament { get; set; }
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public class EventResponse
|
public class EventResponse
|
||||||
{
|
{
|
||||||
public EventType Event { get; set; }
|
public EventType? Event { get; set; }
|
||||||
|
|
||||||
}
|
}
|
||||||
public class SetsResponse
|
public class SetsResponse
|
||||||
{
|
{
|
||||||
public EventType Event { get; set; }
|
public EventType? Event { get; set; }
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ using LaDOSE.Entity.BotEvent;
|
|||||||
using LaDOSE.Entity.Context;
|
using LaDOSE.Entity.Context;
|
||||||
using Microsoft.EntityFrameworkCore;
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
|
||||||
|
|
||||||
namespace LaDOSE.Business.Service
|
namespace LaDOSE.Business.Service
|
||||||
{
|
{
|
||||||
public class BotEventService : BaseService<BotEvent>, IBotEventService
|
public class BotEventService : BaseService<BotEvent>, IBotEventService
|
||||||
|
|||||||
@@ -78,6 +78,10 @@ namespace LaDOSE.Business.Service
|
|||||||
//}
|
//}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public async Task<List<Game>> GetSmashGames(string name)
|
||||||
|
{
|
||||||
|
return await _smashProvider.GetGames(name);
|
||||||
|
}
|
||||||
public async Task<Event> ParseSmash(string tournamentSlug)
|
public async Task<Event> ParseSmash(string tournamentSlug)
|
||||||
{
|
{
|
||||||
Event eventExist = GetBySlug(tournamentSlug);
|
Event eventExist = GetBySlug(tournamentSlug);
|
||||||
@@ -96,7 +100,7 @@ namespace LaDOSE.Business.Service
|
|||||||
//POKEMON.
|
//POKEMON.
|
||||||
catch (Exception e)
|
catch (Exception e)
|
||||||
{
|
{
|
||||||
throw new Exception("FUCK !");
|
throw new Exception($"FUCK ! {e.Message}");
|
||||||
}
|
}
|
||||||
return currentEvent;
|
return currentEvent;
|
||||||
}
|
}
|
||||||
@@ -109,6 +113,11 @@ namespace LaDOSE.Business.Service
|
|||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public Task<List<Game>> GetSmashGame(string name)
|
||||||
|
{
|
||||||
|
return _smashProvider.GetGames(name);
|
||||||
|
}
|
||||||
|
|
||||||
private Event GetBySlug(string tournamentSlug)
|
private Event GetBySlug(string tournamentSlug)
|
||||||
{
|
{
|
||||||
return _context.Event.FirstOrDefault(e => e.SmashSlug == tournamentSlug);
|
return _context.Event.FirstOrDefault(e => e.SmashSlug == tournamentSlug);
|
||||||
@@ -143,6 +152,10 @@ namespace LaDOSE.Business.Service
|
|||||||
var games = _context.Game.ToList();
|
var games = _context.Game.ToList();
|
||||||
|
|
||||||
TournamentsResult result = new TournamentsResult();
|
TournamentsResult result = new TournamentsResult();
|
||||||
|
if (id.Count == 1)
|
||||||
|
{
|
||||||
|
result.Slug = _context.Event.Where(e=> e.Id == id.First()).First().SmashSlug;
|
||||||
|
}
|
||||||
result.Results = new List<Result>();
|
result.Results = new List<Result>();
|
||||||
result.Games = new List<Game>();
|
result.Games = new List<Game>();
|
||||||
result.Participents = new List<ChallongeParticipent>();
|
result.Participents = new List<ChallongeParticipent>();
|
||||||
|
|||||||
@@ -0,0 +1,267 @@
|
|||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Diagnostics;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
using LaDOSE.Business.Interface;
|
||||||
|
using LaDOSE.Entity;
|
||||||
|
using LaDOSE.Entity.Context;
|
||||||
|
|
||||||
|
namespace LaDOSE.Business.Service
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Match statistics over the persisted Set rows.
|
||||||
|
///
|
||||||
|
/// Two things to know about the data:
|
||||||
|
/// - There is no Set -> Player relationship in the EF model (the navigation properties
|
||||||
|
/// and their configuration are commented out), so player names are resolved with a
|
||||||
|
/// separate query plus an in-memory dictionary. Nothing here Includes a player.
|
||||||
|
/// - There is no winner column. The winner is inferred from the scores, and start.gg
|
||||||
|
/// encodes a DQ as -1, so games are clamped at 0.
|
||||||
|
///
|
||||||
|
/// The database work and the aggregation are deliberately separated: <see cref="GetMatchStats"/>
|
||||||
|
/// issues one query per table and hands the loaded lists to the pure static
|
||||||
|
/// <see cref="Aggregate"/>, which is unit-testable without a database.
|
||||||
|
/// </summary>
|
||||||
|
public class StatisticsService : IStatisticsService
|
||||||
|
{
|
||||||
|
protected LaDOSEDbContext _context;
|
||||||
|
|
||||||
|
public StatisticsService(LaDOSEDbContext context)
|
||||||
|
{
|
||||||
|
_context = context;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Task<MatchStats> GetMatchStats(List<int> eventIds)
|
||||||
|
{
|
||||||
|
var requested = (eventIds ?? new List<int>()).Distinct().ToList();
|
||||||
|
if (requested.Count == 0)
|
||||||
|
{
|
||||||
|
// Well-formed and empty, never null and never a 500.
|
||||||
|
return Task.FromResult(new MatchStats());
|
||||||
|
}
|
||||||
|
|
||||||
|
// One query per table. No query per set and no query per player.
|
||||||
|
var events = _context.Event
|
||||||
|
.Where(e => requested.Contains(e.Id))
|
||||||
|
.ToList();
|
||||||
|
|
||||||
|
var existingEventIds = events.Select(e => e.Id).ToList();
|
||||||
|
|
||||||
|
var tournaments = existingEventIds.Count == 0
|
||||||
|
? new List<Tournament>()
|
||||||
|
: _context.Tournament
|
||||||
|
.Where(t => existingEventIds.Contains(t.EventId))
|
||||||
|
.ToList();
|
||||||
|
|
||||||
|
var tournamentIds = tournaments.Select(t => t.Id).Distinct().ToList();
|
||||||
|
|
||||||
|
// _context.Set is the DbSet<Set> property, not DbContext.Set<T>().
|
||||||
|
var sets = tournamentIds.Count == 0
|
||||||
|
? new List<Set>()
|
||||||
|
: _context.Set
|
||||||
|
.Where(s => tournamentIds.Contains(s.TournamentId))
|
||||||
|
.ToList();
|
||||||
|
|
||||||
|
var playerIds = sets.Select(s => s.Player1Id)
|
||||||
|
.Concat(sets.Select(s => s.Player2Id))
|
||||||
|
.Where(id => id != 0)
|
||||||
|
.Distinct()
|
||||||
|
.ToList();
|
||||||
|
|
||||||
|
var players = playerIds.Count == 0
|
||||||
|
? new List<Player>()
|
||||||
|
: _context.Player
|
||||||
|
.Where(p => playerIds.Contains(p.Id))
|
||||||
|
.ToList();
|
||||||
|
|
||||||
|
return Task.FromResult(Aggregate(requested, events, tournaments, sets, players));
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Pure aggregation over already-loaded rows. No database, no I/O, deterministic.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="requestedEventIds">The event ids the caller asked for.</param>
|
||||||
|
/// <param name="events">Events that exist (used for the coverage count).</param>
|
||||||
|
/// <param name="tournaments">Candidate tournaments; filtered here on EventId.</param>
|
||||||
|
/// <param name="sets">Candidate sets; filtered here on TournamentId.</param>
|
||||||
|
/// <param name="players">Players used to resolve display names; may be incomplete.</param>
|
||||||
|
public static MatchStats Aggregate(
|
||||||
|
IEnumerable<int> requestedEventIds,
|
||||||
|
IEnumerable<Event> events,
|
||||||
|
IEnumerable<Tournament> tournaments,
|
||||||
|
IEnumerable<Set> sets,
|
||||||
|
IEnumerable<Player> players)
|
||||||
|
{
|
||||||
|
var result = new MatchStats();
|
||||||
|
|
||||||
|
var requested = new HashSet<int>((requestedEventIds ?? Enumerable.Empty<int>()));
|
||||||
|
if (requested.Count == 0)
|
||||||
|
{
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Coverage: events -------------------------------------------------------
|
||||||
|
var existingEventIds = new HashSet<int>((events ?? Enumerable.Empty<Event>())
|
||||||
|
.Where(e => e != null && requested.Contains(e.Id))
|
||||||
|
.Select(e => e.Id));
|
||||||
|
result.Coverage.Events = existingEventIds.Count;
|
||||||
|
|
||||||
|
// Coverage: brackets ----------------------------------------------------
|
||||||
|
var scopedTournamentIds = new HashSet<int>((tournaments ?? Enumerable.Empty<Tournament>())
|
||||||
|
.Where(t => t != null && existingEventIds.Contains(t.EventId))
|
||||||
|
.Select(t => t.Id));
|
||||||
|
result.Coverage.Brackets = scopedTournamentIds.Count;
|
||||||
|
|
||||||
|
var scopedSets = (sets ?? Enumerable.Empty<Set>())
|
||||||
|
.Where(s => s != null && scopedTournamentIds.Contains(s.TournamentId))
|
||||||
|
.ToList();
|
||||||
|
result.Coverage.Sets = scopedSets.Count;
|
||||||
|
result.Coverage.BracketsWithSets = scopedSets.Select(s => s.TournamentId).Distinct().Count();
|
||||||
|
|
||||||
|
// Name resolution -------------------------------------------------------
|
||||||
|
var nameById = new Dictionary<int, string>();
|
||||||
|
foreach (var player in (players ?? Enumerable.Empty<Player>()).Where(p => p != null))
|
||||||
|
{
|
||||||
|
nameById[player.Id] = DisplayName(player);
|
||||||
|
}
|
||||||
|
|
||||||
|
var stats = new Dictionary<int, PlayerMatchStats>();
|
||||||
|
var h2h = new Dictionary<(int, int), HeadToHead>();
|
||||||
|
|
||||||
|
foreach (var set in scopedSets)
|
||||||
|
{
|
||||||
|
// Unusable rows still count in Coverage.Sets but nowhere else.
|
||||||
|
if (set.Player1Id == 0 || set.Player2Id == 0 || set.Player1Id == set.Player2Id)
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// No winner column: equal scores (including 0-0 and -1 / -1) are undecided.
|
||||||
|
if (set.Player1Score == set.Player2Score)
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
result.Coverage.DecidedSets++;
|
||||||
|
|
||||||
|
var p1 = set.Player1Id;
|
||||||
|
var p2 = set.Player2Id;
|
||||||
|
|
||||||
|
// A DQ is stored as -1. Clamp so it never produces negative games.
|
||||||
|
var games1 = Math.Max(0, set.Player1Score);
|
||||||
|
var games2 = Math.Max(0, set.Player2Score);
|
||||||
|
|
||||||
|
var stat1 = GetOrAdd(stats, p1, nameById);
|
||||||
|
var stat2 = GetOrAdd(stats, p2, nameById);
|
||||||
|
|
||||||
|
stat1.GamesWon += games1;
|
||||||
|
stat1.GamesLost += games2;
|
||||||
|
stat2.GamesWon += games2;
|
||||||
|
stat2.GamesLost += games1;
|
||||||
|
|
||||||
|
var winnerId = set.Player1Score > set.Player2Score ? p1 : p2;
|
||||||
|
if (winnerId == p1)
|
||||||
|
{
|
||||||
|
stat1.Wins++;
|
||||||
|
stat2.Losses++;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
stat2.Wins++;
|
||||||
|
stat1.Losses++;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Lower PlayerId is always A, so WinsA / WinsB are unambiguous.
|
||||||
|
var aId = Math.Min(p1, p2);
|
||||||
|
var bId = Math.Max(p1, p2);
|
||||||
|
var key = (aId, bId);
|
||||||
|
if (!h2h.TryGetValue(key, out var pair))
|
||||||
|
{
|
||||||
|
pair = new HeadToHead
|
||||||
|
{
|
||||||
|
PlayerAId = aId,
|
||||||
|
PlayerA = ResolveName(aId, nameById),
|
||||||
|
PlayerBId = bId,
|
||||||
|
PlayerB = ResolveName(bId, nameById)
|
||||||
|
};
|
||||||
|
h2h[key] = pair;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (winnerId == aId)
|
||||||
|
{
|
||||||
|
pair.WinsA++;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
pair.WinsB++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Sets is derived, so Sets == Wins + Losses cannot drift.
|
||||||
|
foreach (var stat in stats.Values)
|
||||||
|
{
|
||||||
|
stat.Sets = stat.Wins + stat.Losses;
|
||||||
|
Debug.Assert(stat.Sets == stat.Wins + stat.Losses, "Sets must equal Wins + Losses");
|
||||||
|
}
|
||||||
|
|
||||||
|
result.Players = stats.Values
|
||||||
|
.OrderByDescending(p => p.Wins)
|
||||||
|
.ThenByDescending(p => p.Sets)
|
||||||
|
.ThenBy(p => p.Player, StringComparer.Ordinal)
|
||||||
|
.ThenBy(p => p.PlayerId)
|
||||||
|
.ToList();
|
||||||
|
|
||||||
|
// Not capped: bounded by the number of sets in scope.
|
||||||
|
result.HeadToHead = h2h.Values
|
||||||
|
.OrderByDescending(p => p.WinsA + p.WinsB)
|
||||||
|
.ThenBy(p => p.PlayerA, StringComparer.Ordinal)
|
||||||
|
.ThenBy(p => p.PlayerB, StringComparer.Ordinal)
|
||||||
|
.ThenBy(p => p.PlayerAId)
|
||||||
|
.ThenBy(p => p.PlayerBId)
|
||||||
|
.ToList();
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static PlayerMatchStats GetOrAdd(Dictionary<int, PlayerMatchStats> stats, int playerId,
|
||||||
|
Dictionary<int, string> nameById)
|
||||||
|
{
|
||||||
|
if (!stats.TryGetValue(playerId, out var stat))
|
||||||
|
{
|
||||||
|
stat = new PlayerMatchStats
|
||||||
|
{
|
||||||
|
PlayerId = playerId,
|
||||||
|
Player = ResolveName(playerId, nameById)
|
||||||
|
};
|
||||||
|
stats[playerId] = stat;
|
||||||
|
}
|
||||||
|
|
||||||
|
return stat;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gamertag, falling back to Name, else "#<id>" (a set can reference a player row
|
||||||
|
/// that no longer exists, and Gamertag is nullable in the database).
|
||||||
|
/// </summary>
|
||||||
|
private static string ResolveName(int playerId, Dictionary<int, string> nameById)
|
||||||
|
{
|
||||||
|
return nameById.TryGetValue(playerId, out var name) ? name : $"#{playerId}";
|
||||||
|
}
|
||||||
|
|
||||||
|
private static string DisplayName(Player player)
|
||||||
|
{
|
||||||
|
if (!string.IsNullOrWhiteSpace(player.Gamertag))
|
||||||
|
{
|
||||||
|
return player.Gamertag;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!string.IsNullOrWhiteSpace(player.Name))
|
||||||
|
{
|
||||||
|
return player.Name;
|
||||||
|
}
|
||||||
|
|
||||||
|
return $"#{player.Id}";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -4,6 +4,7 @@ using System.Linq;
|
|||||||
using LaDOSE.Business.Interface;
|
using LaDOSE.Business.Interface;
|
||||||
using LaDOSE.Entity;
|
using LaDOSE.Entity;
|
||||||
using LaDOSE.Entity.Context;
|
using LaDOSE.Entity.Context;
|
||||||
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
|
||||||
namespace LaDOSE.Business.Service
|
namespace LaDOSE.Business.Service
|
||||||
{
|
{
|
||||||
@@ -20,7 +21,9 @@ namespace LaDOSE.Business.Service
|
|||||||
{
|
{
|
||||||
if (string.IsNullOrEmpty(username) || string.IsNullOrEmpty(password))
|
if (string.IsNullOrEmpty(username) || string.IsNullOrEmpty(password))
|
||||||
return null;
|
return null;
|
||||||
var user = _context.ApplicationUser.SingleOrDefault(x => x.Username == username);
|
var user = _context.ApplicationUser
|
||||||
|
.Include(x => x.UserRoles).ThenInclude(ur => ur.Role)
|
||||||
|
.SingleOrDefault(x => x.Username == username);
|
||||||
|
|
||||||
// check if username exists
|
// check if username exists
|
||||||
if (user == null)
|
if (user == null)
|
||||||
@@ -36,23 +39,44 @@ namespace LaDOSE.Business.Service
|
|||||||
|
|
||||||
public IEnumerable<ApplicationUser> GetAll()
|
public IEnumerable<ApplicationUser> GetAll()
|
||||||
{
|
{
|
||||||
return _context.ApplicationUser;
|
return _context.ApplicationUser
|
||||||
|
.Include(x => x.UserRoles).ThenInclude(ur => ur.Role)
|
||||||
|
.ToList();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Roles are included because authorization reads them on every authenticated
|
||||||
|
/// request (see the OnTokenValidated handler in Startup).
|
||||||
|
/// </summary>
|
||||||
public ApplicationUser GetById(int id)
|
public ApplicationUser GetById(int id)
|
||||||
{
|
{
|
||||||
return _context.ApplicationUser.Find(id);
|
return _context.ApplicationUser
|
||||||
|
.Include(x => x.UserRoles).ThenInclude(ur => ur.Role)
|
||||||
|
.SingleOrDefault(x => x.Id == id);
|
||||||
}
|
}
|
||||||
|
|
||||||
public ApplicationUser Create(ApplicationUser user, string password)
|
public IEnumerable<ApplicationRole> GetAllRoles()
|
||||||
|
{
|
||||||
|
return _context.ApplicationRole.OrderBy(x => x.Name).ToList();
|
||||||
|
}
|
||||||
|
|
||||||
|
public ApplicationUser Create(ApplicationUser user, string password, IEnumerable<string> roleNames = null)
|
||||||
{
|
{
|
||||||
// validation
|
// validation
|
||||||
|
if (string.IsNullOrWhiteSpace(user?.Username))
|
||||||
|
throw new Exception("Username is required");
|
||||||
|
|
||||||
if (string.IsNullOrWhiteSpace(password))
|
if (string.IsNullOrWhiteSpace(password))
|
||||||
throw new Exception("Password is required");
|
throw new Exception("Password is required");
|
||||||
|
|
||||||
if (_context.ApplicationUser.Any(x => x.Username == user.Username))
|
if (_context.ApplicationUser.Any(x => x.Username == user.Username))
|
||||||
throw new Exception("Username \"" + user.Username + "\" is already taken");
|
throw new Exception("Username \"" + user.Username + "\" is already taken");
|
||||||
|
|
||||||
|
// EF fills userid/roleid on the join rows from these navigations when it saves.
|
||||||
|
user.UserRoles = ResolveRoles(roleNames)
|
||||||
|
.Select(role => new ApplicationUserRole { Role = role })
|
||||||
|
.ToList();
|
||||||
|
|
||||||
byte[] passwordHash, passwordSalt;
|
byte[] passwordHash, passwordSalt;
|
||||||
CreatePasswordHash(password, out passwordHash, out passwordSalt);
|
CreatePasswordHash(password, out passwordHash, out passwordSalt);
|
||||||
|
|
||||||
@@ -65,6 +89,35 @@ namespace LaDOSE.Business.Service
|
|||||||
return user;
|
return user;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Turns role names into the existing rows of <c>applicationrole</c>. An unknown
|
||||||
|
/// name is an error rather than a new role, so a typo cannot quietly produce an
|
||||||
|
/// account with no privileges — or, worse, a second spelling of "Admin".
|
||||||
|
/// </summary>
|
||||||
|
private List<ApplicationRole> ResolveRoles(IEnumerable<string> roleNames)
|
||||||
|
{
|
||||||
|
var wanted = (roleNames ?? Enumerable.Empty<string>())
|
||||||
|
.Where(name => !string.IsNullOrWhiteSpace(name))
|
||||||
|
.Select(name => name.Trim())
|
||||||
|
.Distinct(StringComparer.OrdinalIgnoreCase)
|
||||||
|
.ToList();
|
||||||
|
|
||||||
|
if (wanted.Count == 0)
|
||||||
|
return new List<ApplicationRole>();
|
||||||
|
|
||||||
|
var known = _context.ApplicationRole.ToList();
|
||||||
|
var resolved = new List<ApplicationRole>();
|
||||||
|
foreach (var name in wanted)
|
||||||
|
{
|
||||||
|
var role = known.FirstOrDefault(r => string.Equals(r.Name, name, StringComparison.OrdinalIgnoreCase));
|
||||||
|
if (role == null)
|
||||||
|
throw new Exception($"Unknown role \"{name}\"");
|
||||||
|
resolved.Add(role);
|
||||||
|
}
|
||||||
|
|
||||||
|
return resolved;
|
||||||
|
}
|
||||||
|
|
||||||
public void Update(ApplicationUser userParam, string password = null)
|
public void Update(ApplicationUser userParam, string password = null)
|
||||||
{
|
{
|
||||||
var user = _context.ApplicationUser.Find(userParam.Id);
|
var user = _context.ApplicationUser.Find(userParam.Id);
|
||||||
@@ -100,13 +153,19 @@ namespace LaDOSE.Business.Service
|
|||||||
|
|
||||||
public void Delete(int id)
|
public void Delete(int id)
|
||||||
{
|
{
|
||||||
var user = _context.ApplicationUser.Find(id);
|
// applicationuserrole's foreign keys are ON DELETE RESTRICT, so the join rows
|
||||||
if (user != null)
|
// have to go first — clearing the collection makes EF delete them.
|
||||||
{
|
var user = _context.ApplicationUser
|
||||||
|
.Include(x => x.UserRoles).ThenInclude(ur => ur.Role)
|
||||||
|
.SingleOrDefault(x => x.Id == id);
|
||||||
|
|
||||||
|
if (user == null)
|
||||||
|
return;
|
||||||
|
|
||||||
|
user.UserRoles?.Clear();
|
||||||
_context.ApplicationUser.Remove(user);
|
_context.ApplicationUser.Remove(user);
|
||||||
_context.SaveChanges();
|
_context.SaveChanges();
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
// private helper methods
|
// private helper methods
|
||||||
|
|
||||||
|
|||||||
@@ -43,13 +43,6 @@ namespace LaDOSE.Business.Service
|
|||||||
return wpEvents;
|
return wpEvents;
|
||||||
}
|
}
|
||||||
|
|
||||||
public bool UpdateBooking()
|
|
||||||
{
|
|
||||||
_context.Database.SetCommandTimeout(60);
|
|
||||||
_context.Database.ExecuteSqlCommand("call ladoseapi.ImportEvent();");
|
|
||||||
_context.Database.SetCommandTimeout(30);
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
public List<WPUser> GetBooking(int wpEventId, Game game)
|
public List<WPUser> GetBooking(int wpEventId, Game game)
|
||||||
{
|
{
|
||||||
var selectedGameWpId = game.WordPressTag.Split(';');
|
var selectedGameWpId = game.WordPressTag.Split(';');
|
||||||
|
|||||||
@@ -0,0 +1,21 @@
|
|||||||
|
# Keep the build context small and free of local state.
|
||||||
|
node_modules
|
||||||
|
build
|
||||||
|
.svelte-kit
|
||||||
|
.git
|
||||||
|
.vscode
|
||||||
|
|
||||||
|
# Real URLs live here and must never reach the image. .env.example is documentation.
|
||||||
|
.env
|
||||||
|
.env.*
|
||||||
|
!.env.example
|
||||||
|
|
||||||
|
# Consumed by `podman build` itself, never copied in.
|
||||||
|
Dockerfile
|
||||||
|
.dockerignore
|
||||||
|
|
||||||
|
# Local noise
|
||||||
|
.DS_Store
|
||||||
|
Thumbs.db
|
||||||
|
vite.config.js.timestamp-*
|
||||||
|
vite.config.ts.timestamp-*
|
||||||
@@ -0,0 +1,16 @@
|
|||||||
|
# Base URL of LaDOSE.Api. Defaults to http://localhost:5000 when unset,
|
||||||
|
# which matches the Kestrel binding the API uses in development.
|
||||||
|
#
|
||||||
|
# Read by `vite dev` and inlined at build time (`npm run build`, or the Dockerfile's
|
||||||
|
# `--build-arg VITE_API_BASE_URL=...`). Baking it is optional.
|
||||||
|
VITE_API_BASE_URL=http://localhost:5000
|
||||||
|
|
||||||
|
# Container runtime only, and the reason one image serves every environment:
|
||||||
|
# docker-entrypoint.sh turns this into /config.js on start, which app.html loads
|
||||||
|
# before the bundle. Not a Vite variable, so it does NOT belong in a .env file.
|
||||||
|
#
|
||||||
|
# podman run -e LADOSE_API_BASE_URL=https://api.ladose.net ladose-webapp
|
||||||
|
#
|
||||||
|
# Precedence: /config.js > baked VITE_API_BASE_URL > http://localhost:5000.
|
||||||
|
# Leave it unset and /config.js is `{}`, so the baked value stays in charge.
|
||||||
|
# LADOSE_API_BASE_URL=https://api.ladose.net
|
||||||
@@ -0,0 +1,23 @@
|
|||||||
|
node_modules
|
||||||
|
|
||||||
|
# Output
|
||||||
|
.output
|
||||||
|
.vercel
|
||||||
|
.netlify
|
||||||
|
.wrangler
|
||||||
|
/.svelte-kit
|
||||||
|
/build
|
||||||
|
|
||||||
|
# OS
|
||||||
|
.DS_Store
|
||||||
|
Thumbs.db
|
||||||
|
|
||||||
|
# Env
|
||||||
|
.env
|
||||||
|
.env.*
|
||||||
|
!.env.example
|
||||||
|
!.env.test
|
||||||
|
|
||||||
|
# Vite
|
||||||
|
vite.config.js.timestamp-*
|
||||||
|
vite.config.ts.timestamp-*
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
engine-strict=true
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
lts/*
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
{
|
||||||
|
"recommendations": ["svelte.svelte-vscode"]
|
||||||
|
}
|
||||||
@@ -0,0 +1,41 @@
|
|||||||
|
# Build the SPA. adapter-static writes /app/build (pages == assets).
|
||||||
|
FROM node:24-alpine AS build
|
||||||
|
WORKDIR /app
|
||||||
|
|
||||||
|
# Lockfile first: this layer is reused until the dependencies actually change.
|
||||||
|
# Every build tool lives in devDependencies, so no --omit=dev here.
|
||||||
|
#
|
||||||
|
# --legacy-peer-deps works around a conflict that predates this Dockerfile:
|
||||||
|
# openapi-typescript@7.13.0 peer-requires typescript@^5.x while the project is on ^6.0.3,
|
||||||
|
# so plain `npm ci` fails ERESOLVE on every npm version tested (10.8, 10.9, 11.17).
|
||||||
|
# The flag only skips peer *validation* — the tree installed is still exactly
|
||||||
|
# package-lock.json (verified: typescript 6.0.3, lockfile unmodified).
|
||||||
|
# Drop the flag once package.json resolves that conflict.
|
||||||
|
COPY package.json package-lock.json ./
|
||||||
|
RUN npm ci --legacy-peer-deps
|
||||||
|
|
||||||
|
COPY . .
|
||||||
|
|
||||||
|
# Optional baked-in default. Precedence at runtime is
|
||||||
|
# /config.js > VITE_API_BASE_URL > the 'http://localhost:5000' in src/lib/api/client.ts.
|
||||||
|
# Declared after `npm ci` so passing it does not invalidate the dependency layer.
|
||||||
|
# client.ts uses `??`, so an empty string would beat its default: unset instead of exporting "".
|
||||||
|
ARG VITE_API_BASE_URL
|
||||||
|
|
||||||
|
# Same gate as the README's documented `npm run check`: a type regression fails the image.
|
||||||
|
RUN npm run check
|
||||||
|
RUN if [ -z "${VITE_API_BASE_URL:-}" ]; then unset VITE_API_BASE_URL; fi; npm run build
|
||||||
|
|
||||||
|
# Serve it. openapi.json / schema.d.ts are committed, so nothing here touches the live API.
|
||||||
|
FROM nginx:1.27-alpine
|
||||||
|
WORKDIR /usr/share/nginx/html
|
||||||
|
|
||||||
|
COPY nginx.conf /etc/nginx/conf.d/default.conf
|
||||||
|
COPY docker-entrypoint.sh /usr/local/bin/docker-entrypoint.sh
|
||||||
|
RUN chmod +x /usr/local/bin/docker-entrypoint.sh
|
||||||
|
|
||||||
|
COPY --from=build /app/build/ ./
|
||||||
|
|
||||||
|
EXPOSE 80
|
||||||
|
ENTRYPOINT ["/usr/local/bin/docker-entrypoint.sh"]
|
||||||
|
CMD ["nginx", "-g", "daemon off;"]
|
||||||
@@ -0,0 +1,202 @@
|
|||||||
|
# LaDOSE.WebApp
|
||||||
|
|
||||||
|
Svelte 5 + SvelteKit front-end for `LaDOSE.Api`, styled with Tailwind CSS v4.
|
||||||
|
It ships as a static SPA (`@sveltejs/adapter-static`) and talks to the API over
|
||||||
|
JWT bearer auth, so it can be served from any static host.
|
||||||
|
|
||||||
|
## Requirements
|
||||||
|
|
||||||
|
Node ≥ 22.12 (the toolchain uses Vite 8). An `.nvmrc` is checked in:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
nvm use # resolves lts/*
|
||||||
|
npm install
|
||||||
|
```
|
||||||
|
|
||||||
|
## Running
|
||||||
|
|
||||||
|
The API must be up — it serves on `http://localhost:5000` in development:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd ../LaDOSE.Api && dotnet run # terminal 1
|
||||||
|
npm run dev # terminal 2 -> http://localhost:5173
|
||||||
|
```
|
||||||
|
|
||||||
|
Point the app at a different API with `VITE_API_BASE_URL` (see `.env.example`).
|
||||||
|
`LaDOSE.Api` already allows any origin with credentials, so no dev proxy is needed.
|
||||||
|
|
||||||
|
## Typed API access
|
||||||
|
|
||||||
|
`src/lib/api/schema.d.ts` is **generated** from the API's OpenAPI document — never
|
||||||
|
edit it by hand. Regenerate whenever a C# controller or DTO changes:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
npm run api:sync # fetch openapi.json from the running API, then re-emit types
|
||||||
|
```
|
||||||
|
|
||||||
|
That is `api:fetch` (curl `/openapi/v1.json`, override the host with `LADOSE_API_URL`)
|
||||||
|
followed by `api:types` (`openapi-typescript`). `openapi.json` is committed so the
|
||||||
|
types can be rebuilt without a running API.
|
||||||
|
|
||||||
|
Because paths and DTOs come from the generated types, a renamed route or a changed
|
||||||
|
DTO field surfaces as a TypeScript error rather than a runtime 404.
|
||||||
|
|
||||||
|
| Module | Purpose |
|
||||||
|
| --- | --- |
|
||||||
|
| `src/lib/api/schema.d.ts` | Generated types — all 23 API paths and every DTO |
|
||||||
|
| `src/lib/api/schema-helpers.ts` | Friendly aliases (`ApplicationUserDTO`, `LoginRequest`, …) |
|
||||||
|
| `src/lib/api/client.ts` | `apiRequest` — bearer auth, JSON, `ApiError`; paths constrained to real routes |
|
||||||
|
| `src/lib/api/users.ts` | `login` / `register` against `/Users/auth` and `/Users/register` |
|
||||||
|
| `src/lib/api/errors.ts` | `toErrorMessage` — message to show, or redirect to `/login` on a 401 |
|
||||||
|
| `src/lib/api/tournaments.ts` | `listEvents` / `importSmashTournament` / `getResults`, authenticated from the session |
|
||||||
|
| `src/lib/api/games.ts` | `listGames` / `saveGame` / `deleteGame` / `searchSmashGames` |
|
||||||
|
| `src/lib/api/admin-users.ts` | `listUsers` / `listRoles` / `addUser` / `deleteUser` — all Admin-only |
|
||||||
|
| `src/lib/api/statistics.ts` | `getMatchStats` against `POST /api/Statistics/Matches` — set-level win/loss and head to head |
|
||||||
|
| `src/lib/tournaments/results.ts` | Pure reshaping of `TournamentsResultDTO`: ranking grid, per-game placements, WordPress HTML, CSV |
|
||||||
|
| `src/lib/statistics/aggregate.ts` | Pure aggregation for `/statistiques`: standings, per-game and per-event summaries, CSV |
|
||||||
|
| `src/lib/statistics/load.ts` | `loadEventResults` — per-event `GetResults` fan-out with progress, partial failure and abort |
|
||||||
|
| `src/lib/games/draft.ts` | `GameDTO` ⇄ editor form, including the blank-to-NULL rules |
|
||||||
|
| `src/lib/ui/classes.ts` | The Tailwind class strings shared by the pages |
|
||||||
|
| `src/lib/stores/session.svelte.ts` | Signed-in user, persisted to `localStorage`, drops expired JWTs |
|
||||||
|
|
||||||
|
Calling another endpoint takes one line, and the path is checked at compile time:
|
||||||
|
|
||||||
|
```ts
|
||||||
|
import { apiRequest, buildPath, session } from '$lib';
|
||||||
|
import type { GameDTO } from '$lib';
|
||||||
|
|
||||||
|
const games = await apiRequest<GameDTO[]>('/api/Game', { token: session.token });
|
||||||
|
const one = await apiRequest<GameDTO>(buildPath('/api/Game/{id}', { id: 3 }));
|
||||||
|
```
|
||||||
|
|
||||||
|
## Routes
|
||||||
|
|
||||||
|
- `/login` — username + password, posts to `POST /Users/auth`, stores the returned JWT
|
||||||
|
- `/` — guarded; greets the signed-in user with **Hello, \<name\>.** and offers sign-out
|
||||||
|
- `/tournaments` — guarded; the start.gg half of the old Avalonia `TournamentResultView`
|
||||||
|
- `/statistiques` — guarded; standings, attendance and match statistics over a chosen scope
|
||||||
|
- `/games` — guarded; the game catalogue editor, from the Avalonia `GamesView`
|
||||||
|
- `/users` — **Admin only**; add and remove accounts
|
||||||
|
|
||||||
|
`session.displayName` prefers `firstName lastName` and falls back to `username`.
|
||||||
|
|
||||||
|
### `/tournaments`
|
||||||
|
|
||||||
|
Ports the Smash.gg (start.gg) column of `LaDOSE.DesktopApp.Avalonia`:
|
||||||
|
|
||||||
|
1. **Import** — a slug (`start.gg/tournament/<slug>`) is sent to
|
||||||
|
`GET /api/Tournament/ParseSmash/{slug}`, which pulls the brackets, placements and
|
||||||
|
sets into the database. Every bracket must be `COMPLETED` or the API throws.
|
||||||
|
2. **Events** — `GET /api/Event`, newest first. Tick one event for a single
|
||||||
|
tournament, or several to aggregate a ranking season. The regex box replaces the
|
||||||
|
selection with every matching event name (e.g. `Ranking #13\d{2}`).
|
||||||
|
3. **Generate results** — `POST /api/Tournament/GetResults` with the selected ids;
|
||||||
|
the API applies the point rules in `ExternalProviderService`. Three views:
|
||||||
|
- *Ranking* — players × games with totals, highest first, plus CSV export
|
||||||
|
- *By game* — placements and points for one game
|
||||||
|
- *HTML* — the podium table for the WordPress recap, with copy-to-clipboard
|
||||||
|
|
||||||
|
The Challonge half of the Avalonia view (date range, Challonge tournament list,
|
||||||
|
`ParseChallonge`) is deliberately not ported.
|
||||||
|
|
||||||
|
### `/statistiques`
|
||||||
|
|
||||||
|
Pick a scope — everything, the last 6/12 events, or a regex over event names — then
|
||||||
|
**Compute statistics**. Two independent sources feed the page, and they are kept apart
|
||||||
|
on purpose:
|
||||||
|
|
||||||
|
- **Points, placements, attendance** come from `POST /api/Tournament/GetResults`, called
|
||||||
|
**one event at a time**. The endpoint merges everything it is given and never says
|
||||||
|
which event a row came from, so per-event calls are the only way to get a time series
|
||||||
|
— and they contain the damage, because it throws a 500 on any bracket missing a rank-1
|
||||||
|
or rank-2 row. One broken import is reported as a skipped event instead of taking the
|
||||||
|
whole scope down with it.
|
||||||
|
- **Set-level win rates and head to head** come from `POST /api/Statistics/Matches`, in
|
||||||
|
a single call for the whole scope.
|
||||||
|
|
||||||
|
The **Matches** tab leads with its coverage line, and it matters: brackets imported
|
||||||
|
before set rows were persisted contribute placements but no matches, so those figures
|
||||||
|
can describe a fraction of the scope while the standings above cover all of it. Win
|
||||||
|
rates count **decided sets only**, so a player with no resolvable set is left out rather
|
||||||
|
than shown at 0%.
|
||||||
|
|
||||||
|
Other things worth knowing:
|
||||||
|
|
||||||
|
- Rank `999` is the service's "unplaced" sentinel (the participation bucket in
|
||||||
|
`ExternalProviderService`), so it never counts as a placement or a podium, and
|
||||||
|
**Best** shows `—`.
|
||||||
|
- Player names are merged case-insensitively, as everywhere else in the app.
|
||||||
|
- Brackets are counted per event, so a bracket name reused every month counts once
|
||||||
|
per event rather than once overall.
|
||||||
|
- Undated events sort last in the chart and the Events tab — `GET /api/Event` returns
|
||||||
|
newest first, which is the least misleading place to put them.
|
||||||
|
- `aggregate.ts` is pure, so it can be exercised under plain Node with fixtures, the
|
||||||
|
same way `src/lib/tournaments/results.ts` is.
|
||||||
|
|
||||||
|
### `/games`
|
||||||
|
|
||||||
|
Ports `GamesView`: the list on the left (ordered by `Order`), an editor on the right.
|
||||||
|
|
||||||
|
- **Save** — `POST /api/Game`. `AddOrUpdate` inserts when `id` is 0 and otherwise
|
||||||
|
**replaces every column**, so the form always sends a complete `GameDTO`; blank
|
||||||
|
text fields are sent as `null`.
|
||||||
|
- **New game** — starts an empty draft with `id` 0 and the next free `Order`. The
|
||||||
|
desktop app instead posted a blank row immediately and let you fill it in after.
|
||||||
|
- **Delete** — `DELETE /api/Game/{id}`, behind a confirm.
|
||||||
|
- **Find on start.gg** — `GET /api/Game/smash/{name}` searches start.gg's videogame
|
||||||
|
catalogue using the long name (falling back to the name); picking a match fills
|
||||||
|
`smashId`. The ids listed are **start.gg** videogame ids, not LaDOSE game ids.
|
||||||
|
|
||||||
|
`smashId` is what bracket imports match on: a game without one collects its results
|
||||||
|
under a synthetic "GAME NOT FOUND" entry.
|
||||||
|
|
||||||
|
Unlike the desktop form, `imgUrl` is editable here — it is part of `GameDTO` and was
|
||||||
|
otherwise only reachable through the database.
|
||||||
|
|
||||||
|
### `/users` (Admin only)
|
||||||
|
|
||||||
|
Lists every account with its roles, creates accounts, and deletes them.
|
||||||
|
|
||||||
|
`POST /Users/register` used to be `[AllowAnonymous]` so that the first account could be
|
||||||
|
created. It is now `POST /Users/AddUser` and requires the **Admin** role, so before this
|
||||||
|
page is reachable at all, one account has to be promoted directly in the database:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# edit the username in the file first, then:
|
||||||
|
psql "$LADOSE_DB" -v ON_ERROR_STOP=1 -f ../../Sql/2026-08-05_roles.sql
|
||||||
|
```
|
||||||
|
|
||||||
|
How roles work:
|
||||||
|
|
||||||
|
- They live in the pre-existing `applicationrole` / `applicationuserrole` tables. The
|
||||||
|
script seeds `Admin` and `User`; no schema change was needed.
|
||||||
|
- Only user management checks a role. **Every other endpoint is unchanged** — a plain
|
||||||
|
or role-less account can still use tournaments, games and the rest.
|
||||||
|
- The JWT carries only the user id. Roles are read from the database on every request
|
||||||
|
(`OnTokenValidated` in `Startup.cs`), so granting or revoking Admin applies to the
|
||||||
|
caller's next request instead of whenever their 16-minute token expires.
|
||||||
|
- `session.isAdmin` hides the link and the page, but that is cosmetic — the API is
|
||||||
|
what enforces access, and a non-admin calling these endpoints gets a 403.
|
||||||
|
- The API refuses to delete the caller's own account. Since only an admin can reach the
|
||||||
|
endpoint, that is what guarantees at least one admin always remains.
|
||||||
|
|
||||||
|
## Notes
|
||||||
|
|
||||||
|
- The API issues 16-minute tokens. A lapsed token is treated as signed out on load;
|
||||||
|
there is no refresh flow yet, so long sessions will need a re-login. The guarded
|
||||||
|
pages turn a 401 into a redirect back to `/login` (see `toErrorMessage`).
|
||||||
|
- The API has no exception middleware, so an unhandled server error arrives as an
|
||||||
|
HTML developer page. `ApiError` then carries only the status, which is why each
|
||||||
|
call site supplies its own fallback message.
|
||||||
|
- `GetResults` only fills `slug` when **one** event id is requested, so the
|
||||||
|
"Voir le Bracket" links appear only for a single-event export.
|
||||||
|
- Player names are merged case-insensitively across brackets, matching the desktop app.
|
||||||
|
- `src/routes/+layout.ts` sets `ssr = false`: the JWT lives in the browser, so there
|
||||||
|
is nothing meaningful to render on the server.
|
||||||
|
|
||||||
|
## Checks
|
||||||
|
|
||||||
|
```bash
|
||||||
|
npm run check # svelte-check (types + template diagnostics)
|
||||||
|
npm run build # static build into ./build
|
||||||
|
```
|
||||||
@@ -0,0 +1,24 @@
|
|||||||
|
#!/bin/sh
|
||||||
|
# Vite inlines import.meta.env.VITE_* at build time, so runtime configuration cannot
|
||||||
|
# come from an env var the app reads directly. Instead we write /config.js here, which
|
||||||
|
# src/app.html loads synchronously ahead of the bundle. One image, any environment.
|
||||||
|
set -eu
|
||||||
|
|
||||||
|
config_file=/usr/share/nginx/html/config.js
|
||||||
|
|
||||||
|
# Drop control characters (newlines included) so the value cannot break out of the
|
||||||
|
# string literal, then escape backslashes before double quotes. A URL containing
|
||||||
|
# & ? " or a trailing slash stays inert data.
|
||||||
|
value=$(printf '%s' "${LADOSE_API_BASE_URL:-}" | tr -d '\001-\037')
|
||||||
|
|
||||||
|
if [ -z "$value" ]; then
|
||||||
|
# Empty object, not an empty string: lets the app keep its baked-in default.
|
||||||
|
printf 'window.__LADOSE_CONFIG__ = {};\n' >"$config_file"
|
||||||
|
echo "config.js: LADOSE_API_BASE_URL unset, deferring to the built-in default" >&2
|
||||||
|
else
|
||||||
|
escaped=$(printf '%s' "$value" | sed -e 's/\\/\\\\/g' -e 's/"/\\"/g')
|
||||||
|
printf 'window.__LADOSE_CONFIG__ = { apiBaseUrl: "%s" };\n' "$escaped" >"$config_file"
|
||||||
|
echo "config.js: apiBaseUrl=$value" >&2
|
||||||
|
fi
|
||||||
|
|
||||||
|
exec "$@"
|
||||||
@@ -0,0 +1,58 @@
|
|||||||
|
# Copied to /etc/nginx/conf.d/default.conf, replacing the image's default server.
|
||||||
|
server {
|
||||||
|
listen 80;
|
||||||
|
listen [::]:80;
|
||||||
|
server_name _;
|
||||||
|
|
||||||
|
root /usr/share/nginx/html;
|
||||||
|
index index.html;
|
||||||
|
server_tokens off;
|
||||||
|
|
||||||
|
# The build emits no .gz/.br, so compress on the fly.
|
||||||
|
gzip on;
|
||||||
|
gzip_vary on;
|
||||||
|
gzip_min_length 256;
|
||||||
|
gzip_types
|
||||||
|
application/javascript
|
||||||
|
application/json
|
||||||
|
application/manifest+json
|
||||||
|
application/wasm
|
||||||
|
image/svg+xml
|
||||||
|
text/css
|
||||||
|
text/javascript
|
||||||
|
text/plain
|
||||||
|
text/xml;
|
||||||
|
|
||||||
|
# Rewritten by docker-entrypoint.sh on every container start. Exact match, so the
|
||||||
|
# _app/immutable rule below can never apply to it.
|
||||||
|
location = /config.js {
|
||||||
|
add_header Cache-Control "no-store" always;
|
||||||
|
try_files $uri =404;
|
||||||
|
}
|
||||||
|
|
||||||
|
# Content-hashed filenames: safe forever.
|
||||||
|
location /_app/immutable/ {
|
||||||
|
add_header Cache-Control "public, max-age=31536000, immutable" always;
|
||||||
|
try_files $uri =404;
|
||||||
|
}
|
||||||
|
|
||||||
|
# Deploy marker the client polls. Stale copies mean updates are never noticed.
|
||||||
|
location = /_app/version.json {
|
||||||
|
add_header Cache-Control "no-cache" always;
|
||||||
|
try_files $uri =404;
|
||||||
|
}
|
||||||
|
|
||||||
|
# The shell. Same reason as version.json: it names the current immutable bundle.
|
||||||
|
location = /index.html {
|
||||||
|
add_header Cache-Control "no-cache" always;
|
||||||
|
}
|
||||||
|
|
||||||
|
# Nothing is prerendered (+layout.ts sets ssr = false, prerender = false) and the
|
||||||
|
# adapter's fallback is index.html, so every unknown path belongs to the client
|
||||||
|
# router. Without this, /login, /games, /tournaments, /users and /statistiques 404
|
||||||
|
# on deep-link or hard refresh. The internal redirect re-enters `location =
|
||||||
|
# /index.html`, so fallback responses pick up no-cache too.
|
||||||
|
location / {
|
||||||
|
try_files $uri $uri/ /index.html;
|
||||||
|
}
|
||||||
|
}
|
||||||
File diff suppressed because it is too large
Load Diff
+2308
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,30 @@
|
|||||||
|
{
|
||||||
|
"name": "ladose.webapp",
|
||||||
|
"private": true,
|
||||||
|
"version": "0.0.1",
|
||||||
|
"type": "module",
|
||||||
|
"scripts": {
|
||||||
|
"dev": "vite dev",
|
||||||
|
"build": "vite build",
|
||||||
|
"preview": "vite preview",
|
||||||
|
"prepare": "svelte-kit sync || echo ''",
|
||||||
|
"check": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json",
|
||||||
|
"check:watch": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json --watch",
|
||||||
|
"api:fetch": "curl -fsS ${LADOSE_API_URL:-http://localhost:5000}/openapi/v1.json -o openapi.json",
|
||||||
|
"api:types": "openapi-typescript openapi.json -o src/lib/api/schema.d.ts",
|
||||||
|
"api:sync": "npm run api:fetch && npm run api:types"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@sveltejs/adapter-auto": "^7.0.1",
|
||||||
|
"@sveltejs/adapter-static": "^3.0.10",
|
||||||
|
"@sveltejs/kit": "^2.63.0",
|
||||||
|
"@sveltejs/vite-plugin-svelte": "^7.1.2",
|
||||||
|
"@tailwindcss/vite": "^4.3.3",
|
||||||
|
"openapi-typescript": "^7.13.0",
|
||||||
|
"svelte": "^5.56.1",
|
||||||
|
"svelte-check": "^4.6.0",
|
||||||
|
"tailwindcss": "^4.3.3",
|
||||||
|
"typescript": "^6.0.3",
|
||||||
|
"vite": "^8.0.16"
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,218 @@
|
|||||||
|
@import 'tailwindcss';
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Colour is expressed twice over:
|
||||||
|
*
|
||||||
|
* 1. the LaDOSE brand ramp below — fixed values, theme-independent;
|
||||||
|
* 2. semantic tokens (canvas, surface, ink, accent, …) that every component
|
||||||
|
* actually uses, and that swap wholesale between light and dark.
|
||||||
|
*
|
||||||
|
* Components must only ever reach for the semantic layer. Adding `dark:` next to
|
||||||
|
* a hundred hard-coded shades would double every future edit; one set of tokens
|
||||||
|
* that changes underneath them does not.
|
||||||
|
*/
|
||||||
|
|
||||||
|
@theme {
|
||||||
|
--color-ladose-50: #f2f6ff;
|
||||||
|
--color-ladose-100: #e4ebff;
|
||||||
|
--color-ladose-200: #c3d4ff;
|
||||||
|
--color-ladose-300: #9db4ff;
|
||||||
|
--color-ladose-400: #6f8fff;
|
||||||
|
--color-ladose-500: #4b6cff;
|
||||||
|
--color-ladose-600: #3450e6;
|
||||||
|
--color-ladose-700: #2a3fb8;
|
||||||
|
--color-ladose-800: #1e2d80;
|
||||||
|
--color-ladose-900: #131a3d;
|
||||||
|
--color-ladose-950: #0b1027;
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
* `inline` is load-bearing. Without it Tailwind emits `--color-surface:
|
||||||
|
* var(--app-surface)` into :root and the utility resolves through that copy, so
|
||||||
|
* redefining --app-surface in a more specific selector does nothing. `inline`
|
||||||
|
* substitutes the reference straight into the utility, which is what makes
|
||||||
|
* runtime theme switching work at all in Tailwind v4.
|
||||||
|
*/
|
||||||
|
@theme inline {
|
||||||
|
--color-canvas: var(--app-canvas);
|
||||||
|
--color-surface: var(--app-surface);
|
||||||
|
--color-overlay: var(--app-overlay);
|
||||||
|
--color-inset: var(--app-inset);
|
||||||
|
--color-line: var(--app-line);
|
||||||
|
--color-line-strong: var(--app-line-strong);
|
||||||
|
--color-ink: var(--app-ink);
|
||||||
|
--color-muted: var(--app-muted);
|
||||||
|
--color-subtle: var(--app-subtle);
|
||||||
|
--color-accent: var(--app-accent);
|
||||||
|
--color-accent-hover: var(--app-accent-hover);
|
||||||
|
--color-on-accent: var(--app-on-accent);
|
||||||
|
--color-danger: var(--app-danger);
|
||||||
|
--color-danger-soft: var(--app-danger-soft);
|
||||||
|
--color-success: var(--app-success);
|
||||||
|
--color-success-soft: var(--app-success-soft);
|
||||||
|
--color-warning: var(--app-warning);
|
||||||
|
--color-warning-soft: var(--app-warning-soft);
|
||||||
|
|
||||||
|
--shadow-card: var(--app-shadow);
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Palette values live here once each. The three blocks that follow only
|
||||||
|
* re-point the --app-* aliases, so a colour is never written twice.
|
||||||
|
*/
|
||||||
|
:root {
|
||||||
|
--light-canvas: #f6f7fb;
|
||||||
|
--light-surface: #ffffff;
|
||||||
|
--light-overlay: #ffffff;
|
||||||
|
--light-inset: #f1f3f9;
|
||||||
|
--light-line: #dfe3ee;
|
||||||
|
--light-line-strong: #c6cddf;
|
||||||
|
--light-ink: #101430;
|
||||||
|
--light-muted: #4c5470;
|
||||||
|
--light-subtle: #767e99;
|
||||||
|
--light-accent: var(--color-ladose-600);
|
||||||
|
--light-accent-hover: var(--color-ladose-700);
|
||||||
|
--light-on-accent: #ffffff;
|
||||||
|
--light-danger: #b42318;
|
||||||
|
--light-danger-soft: rgb(180 35 24 / 0.09);
|
||||||
|
--light-success: #067647;
|
||||||
|
--light-success-soft: rgb(6 118 71 / 0.09);
|
||||||
|
--light-warning: #b54708;
|
||||||
|
--light-warning-soft: rgb(181 71 8 / 0.09);
|
||||||
|
--light-shadow: 0 1px 2px rgb(16 24 40 / 0.06), 0 10px 28px rgb(16 24 40 / 0.08);
|
||||||
|
--light-grad-from: #ffffff;
|
||||||
|
--light-grad-to: #eaeef8;
|
||||||
|
|
||||||
|
/* Translucent surfaces on purpose: they sit over the canvas gradient and
|
||||||
|
keep the depth the app had before tokens existed. */
|
||||||
|
--dark-canvas: var(--color-ladose-950);
|
||||||
|
--dark-surface: rgb(255 255 255 / 0.05);
|
||||||
|
/* Menus and popovers sit over content, so they need to be opaque. */
|
||||||
|
--dark-overlay: #171e3d;
|
||||||
|
--dark-inset: rgb(11 16 39 / 0.6);
|
||||||
|
--dark-line: rgb(255 255 255 / 0.1);
|
||||||
|
--dark-line-strong: rgb(255 255 255 / 0.16);
|
||||||
|
--dark-ink: var(--color-ladose-50);
|
||||||
|
--dark-muted: rgb(195 212 255 / 0.7);
|
||||||
|
--dark-subtle: rgb(195 212 255 / 0.45);
|
||||||
|
--dark-accent: var(--color-ladose-500);
|
||||||
|
--dark-accent-hover: var(--color-ladose-600);
|
||||||
|
--dark-on-accent: #ffffff;
|
||||||
|
--dark-danger: #fca5a5;
|
||||||
|
--dark-danger-soft: rgb(239 68 68 / 0.12);
|
||||||
|
--dark-success: #6ee7b7;
|
||||||
|
--dark-success-soft: rgb(16 185 129 / 0.12);
|
||||||
|
--dark-warning: #fcd34d;
|
||||||
|
--dark-warning-soft: rgb(245 158 11 / 0.12);
|
||||||
|
--dark-shadow: 0 1px 2px rgb(0 0 0 / 0.3), 0 16px 40px rgb(0 0 0 / 0.35);
|
||||||
|
--dark-grad-from: var(--color-ladose-900);
|
||||||
|
--dark-grad-to: var(--color-ladose-950);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Layer 1 — light is the unconditional default. */
|
||||||
|
:root {
|
||||||
|
--app-color-scheme: light;
|
||||||
|
--app-canvas: var(--light-canvas);
|
||||||
|
--app-surface: var(--light-surface);
|
||||||
|
--app-overlay: var(--light-overlay);
|
||||||
|
--app-inset: var(--light-inset);
|
||||||
|
--app-line: var(--light-line);
|
||||||
|
--app-line-strong: var(--light-line-strong);
|
||||||
|
--app-ink: var(--light-ink);
|
||||||
|
--app-muted: var(--light-muted);
|
||||||
|
--app-subtle: var(--light-subtle);
|
||||||
|
--app-accent: var(--light-accent);
|
||||||
|
--app-accent-hover: var(--light-accent-hover);
|
||||||
|
--app-on-accent: var(--light-on-accent);
|
||||||
|
--app-danger: var(--light-danger);
|
||||||
|
--app-danger-soft: var(--light-danger-soft);
|
||||||
|
--app-success: var(--light-success);
|
||||||
|
--app-success-soft: var(--light-success-soft);
|
||||||
|
--app-warning: var(--light-warning);
|
||||||
|
--app-warning-soft: var(--light-warning-soft);
|
||||||
|
--app-shadow: var(--light-shadow);
|
||||||
|
--app-grad-from: var(--light-grad-from);
|
||||||
|
--app-grad-to: var(--light-grad-to);
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Layer 2 — follow the OS to dark, unless the user explicitly chose light.
|
||||||
|
* The :not() is what lets an explicit light choice win on a dark machine: a bare
|
||||||
|
* :root in layer 1 could never outrank a later media block on its own.
|
||||||
|
*/
|
||||||
|
@media (prefers-color-scheme: dark) {
|
||||||
|
:root:not([data-theme='light']) {
|
||||||
|
--app-color-scheme: dark;
|
||||||
|
--app-canvas: var(--dark-canvas);
|
||||||
|
--app-surface: var(--dark-surface);
|
||||||
|
--app-overlay: var(--dark-overlay);
|
||||||
|
--app-inset: var(--dark-inset);
|
||||||
|
--app-line: var(--dark-line);
|
||||||
|
--app-line-strong: var(--dark-line-strong);
|
||||||
|
--app-ink: var(--dark-ink);
|
||||||
|
--app-muted: var(--dark-muted);
|
||||||
|
--app-subtle: var(--dark-subtle);
|
||||||
|
--app-accent: var(--dark-accent);
|
||||||
|
--app-accent-hover: var(--dark-accent-hover);
|
||||||
|
--app-on-accent: var(--dark-on-accent);
|
||||||
|
--app-danger: var(--dark-danger);
|
||||||
|
--app-danger-soft: var(--dark-danger-soft);
|
||||||
|
--app-success: var(--dark-success);
|
||||||
|
--app-success-soft: var(--dark-success-soft);
|
||||||
|
--app-warning: var(--dark-warning);
|
||||||
|
--app-warning-soft: var(--dark-warning-soft);
|
||||||
|
--app-shadow: var(--dark-shadow);
|
||||||
|
--app-grad-from: var(--dark-grad-from);
|
||||||
|
--app-grad-to: var(--dark-grad-to);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Layer 3 — an explicit dark choice beats an OS light preference. */
|
||||||
|
:root[data-theme='dark'] {
|
||||||
|
--app-color-scheme: dark;
|
||||||
|
--app-canvas: var(--dark-canvas);
|
||||||
|
--app-surface: var(--dark-surface);
|
||||||
|
--app-overlay: var(--dark-overlay);
|
||||||
|
--app-inset: var(--dark-inset);
|
||||||
|
--app-line: var(--dark-line);
|
||||||
|
--app-line-strong: var(--dark-line-strong);
|
||||||
|
--app-ink: var(--dark-ink);
|
||||||
|
--app-muted: var(--dark-muted);
|
||||||
|
--app-subtle: var(--dark-subtle);
|
||||||
|
--app-accent: var(--dark-accent);
|
||||||
|
--app-accent-hover: var(--dark-accent-hover);
|
||||||
|
--app-on-accent: var(--dark-on-accent);
|
||||||
|
--app-danger: var(--dark-danger);
|
||||||
|
--app-danger-soft: var(--dark-danger-soft);
|
||||||
|
--app-success: var(--dark-success);
|
||||||
|
--app-success-soft: var(--dark-success-soft);
|
||||||
|
--app-warning: var(--dark-warning);
|
||||||
|
--app-warning-soft: var(--dark-warning-soft);
|
||||||
|
--app-shadow: var(--dark-shadow);
|
||||||
|
--app-grad-from: var(--dark-grad-from);
|
||||||
|
--app-grad-to: var(--dark-grad-to);
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
* The canvas has to be painted by CSS on html/body, not by a class on a Svelte
|
||||||
|
* element: ssr is off, so that element does not exist until the bundle hydrates.
|
||||||
|
* Anything that relies on hydration to paint the background flashes white on
|
||||||
|
* every cold load of every route.
|
||||||
|
*/
|
||||||
|
html {
|
||||||
|
color-scheme: var(--app-color-scheme);
|
||||||
|
background-color: var(--app-canvas);
|
||||||
|
}
|
||||||
|
|
||||||
|
body {
|
||||||
|
min-height: 100svh;
|
||||||
|
background-image: radial-gradient(ellipse at top, var(--app-grad-from), var(--app-grad-to));
|
||||||
|
background-repeat: no-repeat;
|
||||||
|
background-attachment: fixed;
|
||||||
|
color: var(--app-ink);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Height of the fixed navbar, so pages that centre themselves can subtract it. */
|
||||||
|
:root {
|
||||||
|
--nav-h: 3.5rem;
|
||||||
|
}
|
||||||
+13
@@ -0,0 +1,13 @@
|
|||||||
|
// See https://svelte.dev/docs/kit/types#app.d.ts
|
||||||
|
// for information about these interfaces
|
||||||
|
declare global {
|
||||||
|
namespace App {
|
||||||
|
// interface Error {}
|
||||||
|
// interface Locals {}
|
||||||
|
// interface PageData {}
|
||||||
|
// interface PageState {}
|
||||||
|
// interface Platform {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export {};
|
||||||
@@ -0,0 +1,37 @@
|
|||||||
|
<!doctype html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8" />
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||||
|
<meta name="text-scale" content="scale" />
|
||||||
|
<!--
|
||||||
|
Runtime configuration, written by the container entrypoint from
|
||||||
|
LADOSE_API_BASE_URL. Synchronous and ahead of the app bundle so
|
||||||
|
src/lib/api/client.ts can read it at module scope. Absent in `vite dev`,
|
||||||
|
which is why the client falls back to VITE_API_BASE_URL.
|
||||||
|
-->
|
||||||
|
<script src="%sveltekit.assets%/config.js"></script>
|
||||||
|
<!--
|
||||||
|
Theme, before %sveltekit.head% emits the render-blocking stylesheet, so
|
||||||
|
data-theme is already on <html> when its selectors are first matched.
|
||||||
|
Must stay a plain synchronous inline script: type=module, defer, async or
|
||||||
|
an external file all run after first paint, which is the flash we are
|
||||||
|
avoiding. Only ever writes 'light' or 'dark' — the CSS keys off those two
|
||||||
|
exact values, so a literal data-theme="system" would match neither.
|
||||||
|
-->
|
||||||
|
<script>
|
||||||
|
try {
|
||||||
|
var t = localStorage.getItem('ladose.theme');
|
||||||
|
if (t === 'light' || t === 'dark') document.documentElement.dataset.theme = t;
|
||||||
|
} catch (e) {
|
||||||
|
/* localStorage throws in Safari private mode and with cookies blocked */
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
<!--
|
||||||
|
%sveltekit.head%
|
||||||
|
-->
|
||||||
|
</head>
|
||||||
|
<body data-sveltekit-preload-data="hover">
|
||||||
|
<div style="display: contents">%sveltekit.body%</div>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
@@ -0,0 +1,53 @@
|
|||||||
|
import { session } from '$lib/stores/session.svelte';
|
||||||
|
import { apiRequest, buildPath, type RequestOptions } from './client';
|
||||||
|
import type { ApplicationUserDTO, NewUserRequest } from './schema-helpers';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* User administration. Every endpoint here is `[Authorize(Roles = "Admin")]`, so a
|
||||||
|
* non-admin gets a 403 — the UI hides the page, but the API is what enforces it.
|
||||||
|
*
|
||||||
|
* `login` and the session live in `users.ts`; this module is only the admin screen.
|
||||||
|
*/
|
||||||
|
function authed(options: RequestOptions): RequestOptions {
|
||||||
|
return { ...options, token: options.token ?? session.token };
|
||||||
|
}
|
||||||
|
|
||||||
|
/** GET /Users — every account, ordered by username. Never includes password material. */
|
||||||
|
export function listUsers(options: RequestOptions = {}): Promise<ApplicationUserDTO[]> {
|
||||||
|
return apiRequest<ApplicationUserDTO[]>('/Users', authed(options));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* GET /Users/Roles — the role names that exist in `applicationrole`. Assigning a name
|
||||||
|
* that is not in this list is rejected by the API rather than creating a new role.
|
||||||
|
*/
|
||||||
|
export function listRoles(options: RequestOptions = {}): Promise<string[]> {
|
||||||
|
return apiRequest<string[]>('/Users/Roles', authed(options));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* POST /Users/AddUser — replaces the old anonymous `register`. Returns the created
|
||||||
|
* user. A duplicate username or an unknown role name comes back as a 400 carrying
|
||||||
|
* the reason in `message`.
|
||||||
|
*/
|
||||||
|
export function addUser(
|
||||||
|
user: NewUserRequest,
|
||||||
|
options: RequestOptions = {}
|
||||||
|
): Promise<ApplicationUserDTO> {
|
||||||
|
return apiRequest<ApplicationUserDTO>('/Users/AddUser', {
|
||||||
|
...authed(options),
|
||||||
|
method: 'POST',
|
||||||
|
body: user
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* DELETE /Users/{id} — 204 on success. The API refuses to delete the caller's own
|
||||||
|
* account (400), which is what guarantees an admin always remains.
|
||||||
|
*/
|
||||||
|
export async function deleteUser(id: number, options: RequestOptions = {}): Promise<void> {
|
||||||
|
await apiRequest<void>(buildPath('/Users/{id}', { id }), {
|
||||||
|
...authed(options),
|
||||||
|
method: 'DELETE'
|
||||||
|
});
|
||||||
|
}
|
||||||
@@ -0,0 +1,113 @@
|
|||||||
|
import type { ApiPath } from './schema-helpers';
|
||||||
|
|
||||||
|
/** Shape of the object `static/config.js` defines, rewritten at container start. */
|
||||||
|
declare global {
|
||||||
|
interface Window {
|
||||||
|
__LADOSE_CONFIG__?: { apiBaseUrl?: string };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Base URL of LaDOSE.Api, resolved in this order:
|
||||||
|
*
|
||||||
|
* 1. `window.__LADOSE_CONFIG__.apiBaseUrl` — written by the container entrypoint
|
||||||
|
* from `LADOSE_API_BASE_URL` (see Dockerfile). This is what lets one image
|
||||||
|
* serve several environments: Vite inlines `import.meta.env` at *build* time,
|
||||||
|
* so without it the URL would be frozen into the bundle.
|
||||||
|
* 2. `VITE_API_BASE_URL` — baked at build time, for `vite dev` and for anyone
|
||||||
|
* who prefers one image per environment.
|
||||||
|
* 3. the Kestrel binding the API uses in development (LaDOSE.Api/appsettings.json).
|
||||||
|
*/
|
||||||
|
function resolveBaseUrl(): string {
|
||||||
|
// `typeof window` rather than a browser guard: this runs at module scope, and
|
||||||
|
// the static build still evaluates this module while generating the shell.
|
||||||
|
const runtime =
|
||||||
|
typeof window !== 'undefined' ? window.__LADOSE_CONFIG__?.apiBaseUrl : undefined;
|
||||||
|
|
||||||
|
const configured = runtime?.trim() || import.meta.env.VITE_API_BASE_URL || 'http://localhost:5000';
|
||||||
|
return configured.replace(/\/$/, '');
|
||||||
|
}
|
||||||
|
|
||||||
|
export const API_BASE_URL = resolveBaseUrl();
|
||||||
|
|
||||||
|
/** Thrown for any non-2xx response, carrying the API's message when it sent one. */
|
||||||
|
export class ApiError extends Error {
|
||||||
|
constructor(
|
||||||
|
readonly status: number,
|
||||||
|
message: string
|
||||||
|
) {
|
||||||
|
super(message);
|
||||||
|
this.name = 'ApiError';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** The shape LaDOSE.Api uses for its error payloads: `new { message = "..." }`. */
|
||||||
|
function extractMessage(body: unknown, status: number): string {
|
||||||
|
if (body && typeof body === 'object' && 'message' in body) {
|
||||||
|
const { message } = body as { message?: unknown };
|
||||||
|
if (typeof message === 'string' && message.length > 0) return message;
|
||||||
|
}
|
||||||
|
return `Request failed with status ${status}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface RequestOptions {
|
||||||
|
method?: 'GET' | 'POST' | 'DELETE';
|
||||||
|
body?: unknown;
|
||||||
|
/** JWT from `POST /Users/auth`; sent as `Authorization: Bearer ...`. */
|
||||||
|
token?: string | null;
|
||||||
|
fetch?: typeof globalThis.fetch;
|
||||||
|
signal?: AbortSignal;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Calls LaDOSE.Api. `path` is constrained to the paths in the generated OpenAPI
|
||||||
|
* types, so a typo or a route removed on the server is a compile error.
|
||||||
|
* Templated paths (e.g. `/api/Game/{id}`) are built with `buildPath`.
|
||||||
|
*/
|
||||||
|
export async function apiRequest<TResponse>(
|
||||||
|
path: ApiPath | (string & {}),
|
||||||
|
options: RequestOptions = {}
|
||||||
|
): Promise<TResponse> {
|
||||||
|
const { method = 'GET', body, token, fetch: fetchImpl = globalThis.fetch, signal } = options;
|
||||||
|
|
||||||
|
const headers: Record<string, string> = { Accept: 'application/json' };
|
||||||
|
if (body !== undefined) headers['Content-Type'] = 'application/json';
|
||||||
|
if (token) headers['Authorization'] = `Bearer ${token}`;
|
||||||
|
|
||||||
|
let response: Response;
|
||||||
|
try {
|
||||||
|
response = await fetchImpl(`${API_BASE_URL}${path}`, {
|
||||||
|
method,
|
||||||
|
headers,
|
||||||
|
body: body === undefined ? undefined : JSON.stringify(body),
|
||||||
|
signal
|
||||||
|
});
|
||||||
|
} catch (cause) {
|
||||||
|
if (cause instanceof DOMException && cause.name === 'AbortError') throw cause;
|
||||||
|
// fetch only rejects on transport failures — the API being down, DNS, CORS.
|
||||||
|
const error = new ApiError(0, `Could not reach LaDOSE.Api at ${API_BASE_URL}`);
|
||||||
|
error.cause = cause;
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
|
||||||
|
const isJson = response.headers.get('content-type')?.includes('json') ?? false;
|
||||||
|
const payload = isJson ? await response.json().catch(() => null) : null;
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
throw new ApiError(response.status, extractMessage(payload, response.status));
|
||||||
|
}
|
||||||
|
|
||||||
|
return payload as TResponse;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Fills a templated OpenAPI path, e.g. buildPath('/api/Game/{id}', { id: 3 }). */
|
||||||
|
export function buildPath(
|
||||||
|
template: ApiPath,
|
||||||
|
params: Record<string, string | number>
|
||||||
|
): string {
|
||||||
|
return template.replace(/\{(\w+)\}/g, (_, key: string) => {
|
||||||
|
const value = params[key];
|
||||||
|
if (value === undefined) throw new Error(`Missing route parameter "${key}" for ${template}`);
|
||||||
|
return encodeURIComponent(String(value));
|
||||||
|
});
|
||||||
|
}
|
||||||
@@ -0,0 +1,21 @@
|
|||||||
|
import { goto } from '$app/navigation';
|
||||||
|
import { session } from '$lib/stores/session.svelte';
|
||||||
|
import { ApiError } from './client';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Turns a failed call into something to show the user.
|
||||||
|
*
|
||||||
|
* A 401 means the 16-minute JWT lapsed mid-session: there is nothing useful to
|
||||||
|
* display, so the session is dropped and the user is sent back to `/login`, and
|
||||||
|
* this returns null.
|
||||||
|
*/
|
||||||
|
export function toErrorMessage(cause: unknown, fallback: string): string | null {
|
||||||
|
if (cause instanceof ApiError && cause.status === 401) {
|
||||||
|
session.clear();
|
||||||
|
void goto('/login', { replaceState: true });
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ApiError already carries the API's own `message`, or "unreachable" for status 0.
|
||||||
|
return cause instanceof ApiError ? cause.message : fallback;
|
||||||
|
}
|
||||||
@@ -0,0 +1,45 @@
|
|||||||
|
import { session } from '$lib/stores/session.svelte';
|
||||||
|
import { apiRequest, buildPath, type RequestOptions } from './client';
|
||||||
|
import type { GameDTO } from './schema-helpers';
|
||||||
|
|
||||||
|
/** GameController is `[Authorize]`; reuse the session JWT unless one is passed in. */
|
||||||
|
function authed(options: RequestOptions): RequestOptions {
|
||||||
|
return { ...options, token: options.token ?? session.token };
|
||||||
|
}
|
||||||
|
|
||||||
|
/** GET /api/Game — every game, in database order (sort by `order` for display). */
|
||||||
|
export function listGames(options: RequestOptions = {}): Promise<GameDTO[]> {
|
||||||
|
return apiRequest<GameDTO[]>('/api/Game', authed(options));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* POST /api/Game — `AddOrUpdate`: an `id` of 0 inserts, anything else updates.
|
||||||
|
* The update replaces every column, so send a full DTO rather than a patch.
|
||||||
|
* Returns the saved game with its assigned id.
|
||||||
|
*/
|
||||||
|
export function saveGame(game: GameDTO, options: RequestOptions = {}): Promise<GameDTO> {
|
||||||
|
return apiRequest<GameDTO>('/api/Game', { ...authed(options), method: 'POST', body: game });
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* DELETE /api/Game/{id} — 204 on success. The service swallows `DbUpdateException`,
|
||||||
|
* so a game still referenced by tournaments comes back as a 404 rather than a 409.
|
||||||
|
*/
|
||||||
|
export async function deleteGame(id: number, options: RequestOptions = {}): Promise<void> {
|
||||||
|
await apiRequest<void>(buildPath('/api/Game/{id}', { id }), {
|
||||||
|
...authed(options),
|
||||||
|
method: 'DELETE'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* GET /api/Game/smash/{name} — searches start.gg's videogame catalogue by name.
|
||||||
|
* The `id` of each match is a **start.gg** videogame id, i.e. a candidate value for
|
||||||
|
* `GameDTO.smashId`, not a LaDOSE game id.
|
||||||
|
*/
|
||||||
|
export function searchSmashGames(
|
||||||
|
name: string,
|
||||||
|
options: RequestOptions = {}
|
||||||
|
): Promise<GameDTO[]> {
|
||||||
|
return apiRequest<GameDTO[]>(buildPath('/api/Game/smash/{name}', { name }), authed(options));
|
||||||
|
}
|
||||||
@@ -0,0 +1,43 @@
|
|||||||
|
import type { components, paths } from './schema';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Re-exports of the DTOs generated from LaDOSE.Api's OpenAPI document
|
||||||
|
* (see `npm run api:types`). Import these instead of hand-writing shapes so the
|
||||||
|
* client breaks at compile time when the C# DTOs change.
|
||||||
|
*/
|
||||||
|
export type Schemas = components['schemas'];
|
||||||
|
|
||||||
|
export type ApplicationUserDTO = Schemas['ApplicationUserDTO'];
|
||||||
|
export type GameDTO = Schemas['GameDTO'];
|
||||||
|
export type EventDTO = Schemas['EventDTO'];
|
||||||
|
export type TodoDTO = Schemas['TodoDTO'];
|
||||||
|
export type WPEventDTO = Schemas['WPEventDTO'];
|
||||||
|
export type TournamentDTO = Schemas['TournamentDTO'];
|
||||||
|
export type TournamentsResultDTO = Schemas['TournamentsResultDTO'];
|
||||||
|
export type ResultDTO = Schemas['ResultDTO'];
|
||||||
|
export type ParticipentDTO = Schemas['ParticipentDTO'];
|
||||||
|
export type MatchStatsDTO = Schemas['MatchStatsDTO'];
|
||||||
|
export type MatchCoverageDTO = Schemas['MatchCoverageDTO'];
|
||||||
|
export type PlayerMatchStatsDTO = Schemas['PlayerMatchStatsDTO'];
|
||||||
|
export type HeadToHeadDTO = Schemas['HeadToHeadDTO'];
|
||||||
|
|
||||||
|
/** The body `UsersController.Authenticate` binds — only these two fields are read. */
|
||||||
|
export type LoginRequest = Pick<ApplicationUserDTO, 'username' | 'password'>;
|
||||||
|
|
||||||
|
/** The body `UsersController.AddUser` binds. `roles` must name rows of `applicationrole`. */
|
||||||
|
export type NewUserRequest = Pick<
|
||||||
|
ApplicationUserDTO,
|
||||||
|
'username' | 'password' | 'firstName' | 'lastName' | 'roles'
|
||||||
|
>;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A logged-in user is the auth response with the fields the API always fills in
|
||||||
|
* narrowed to non-optional, so pages don't have to null-check `token`/`username`.
|
||||||
|
*/
|
||||||
|
export type AuthenticatedUser = ApplicationUserDTO & {
|
||||||
|
username: string;
|
||||||
|
token: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
/** Every path exposed by LaDOSE.Api, e.g. `'/Users/auth'` or `'/api/Game'`. */
|
||||||
|
export type ApiPath = keyof paths;
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user