mirror of
https://gitea.tendokyu.moe/ppc/amnet.git
synced 2026-10-07 22:28:13 +03:00
Compare commits
59
Commits
1.0
...
alt-icon-android
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
6c47de61d9 | ||
|
|
7a4d924227 | ||
|
|
2eb61d74a7 | ||
|
|
89101c5d5f | ||
|
|
2fe9238d02 | ||
|
|
d1b626ffac | ||
|
|
fe7ab1c50f | ||
|
|
5a3a7be906 | ||
|
|
703067e6d6 | ||
|
|
67f90397b0 | ||
|
|
8ba439721f | ||
|
|
35a8c28c0e | ||
|
|
a0b44c165b | ||
|
|
489a93adfe | ||
|
|
6172f2bbd7 | ||
|
|
414a199552 | ||
|
|
a583b72798 | ||
|
|
33cd321325 | ||
|
|
85b900ea30 | ||
|
|
4038de583d | ||
|
|
b08a7cd125 | ||
|
|
4b6037fe24 | ||
|
|
4702052cc4 | ||
|
|
969edd3675 | ||
|
|
0d200a22fc | ||
|
|
c30abfe482 | ||
|
|
6628071f5b | ||
|
|
b29daecdcd | ||
|
|
57d9f07a64 | ||
|
|
a538812aee | ||
|
|
dff641e6f1 | ||
|
|
00736e72dd | ||
|
|
f4dd845ee6 | ||
|
|
e467329db8 | ||
|
|
e7c521cf2e | ||
|
|
a332562928 | ||
|
|
245a26aecf | ||
|
|
8a1c751448 | ||
|
|
73b7800bf3 | ||
|
|
cff5989a8d | ||
|
|
20577f76f0 | ||
|
|
91dc31226d | ||
|
|
7b5c1b9672 | ||
|
|
963f2ddb48 | ||
|
|
153d3ff7ad | ||
|
|
9fe0dcc35c | ||
|
|
6acddc2f78 | ||
|
|
15329d5dcd | ||
|
|
ef8e03ee9b | ||
|
|
a7217c8e31 | ||
|
|
1b3865b325 | ||
|
|
40934f999d | ||
|
|
087f657563 | ||
|
|
fdfbe73bb2 | ||
|
|
e0a0002ca1 | ||
|
|
ba58e3af1b | ||
|
|
e4cb903198 | ||
|
|
14efbc4971 | ||
|
|
ec98003d2c |
@@ -0,0 +1,81 @@
|
|||||||
|
name: Release
|
||||||
|
|
||||||
|
on:
|
||||||
|
# push:
|
||||||
|
# tags:
|
||||||
|
# - '^v?(?:[0-9]+.?){1,4}$'
|
||||||
|
workflow_dispatch:
|
||||||
|
inputs:
|
||||||
|
tag:
|
||||||
|
description: 'Tag Name'
|
||||||
|
required: true
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
get-upload-url:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
name: Get Asset Upload URL
|
||||||
|
|
||||||
|
outputs:
|
||||||
|
upload_url: ${{ steps.get-url.outputs.url }}
|
||||||
|
|
||||||
|
env:
|
||||||
|
TAG: ${{ github.event.inputs.tag || github.ref_name }}
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- name: Get Upload URL
|
||||||
|
id: get-url
|
||||||
|
run: |
|
||||||
|
UPLOAD_URL=$(curl -s -X GET -H "Authorization: token ${{ secrets.GITEA_TOKEN }}" "https://gitea.tendokyu.moe/api/v1/repos/ppc/amnet/releases/tags/$TAG" | jq -r '.upload_url')
|
||||||
|
echo "url=$UPLOAD_URL" >> $GITHUB_OUTPUT
|
||||||
|
|
||||||
|
build-server:
|
||||||
|
runs-on: windows-latest
|
||||||
|
needs: get-upload-url
|
||||||
|
name: Build amnet-server
|
||||||
|
|
||||||
|
env:
|
||||||
|
DOTNET_CLI_TELEMETRY_OPTOUT: 1
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v4
|
||||||
|
- uses: actions/setup-dotnet@v4
|
||||||
|
with:
|
||||||
|
dotnet-version: '8.x'
|
||||||
|
|
||||||
|
- name: Set version
|
||||||
|
run: |
|
||||||
|
$tag="${{ github.event.inputs.tag || github.ref_name }}".TrimStart("v")
|
||||||
|
echo "VERSION=$tag" >> $env:GITHUB_ENV
|
||||||
|
|
||||||
|
- name: Publish
|
||||||
|
run: dotnet publish -c Release -r win-x64 -o .\build-out -p:Version=$env:VERSION AMNet.Server\AMNet.Server.csproj
|
||||||
|
|
||||||
|
- name: Upload artifact
|
||||||
|
run: |
|
||||||
|
curl -s -X POST -H "Authorization: token ${{ secrets.GITEA_TOKEN }}" -H "Content-Type: application/octet-stream" --data-binary "@.\build-out\amnet.dll" "${{ needs.get-upload-url.outputs.upload_url }}?name=amnet.dll"
|
||||||
|
|
||||||
|
build-website:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
needs: get-upload-url
|
||||||
|
name: Build amnet-web
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v4
|
||||||
|
- uses: actions/setup-node@v4
|
||||||
|
with:
|
||||||
|
node-version: '22'
|
||||||
|
|
||||||
|
- name: Install dependencies
|
||||||
|
working-directory: ${{ github.workspace }}/amnet-web
|
||||||
|
run: npm install
|
||||||
|
|
||||||
|
- name: Build
|
||||||
|
working-directory: ${{ github.workspace }}/amnet-web
|
||||||
|
run: npm run build
|
||||||
|
|
||||||
|
- name: Archive Result
|
||||||
|
run: zip -r amnet-web.zip ./amnet-web/dist/
|
||||||
|
|
||||||
|
- name: Upload
|
||||||
|
run: |
|
||||||
|
curl -s -X POST -H "Authorization: token ${{ secrets.GITEA_TOKEN }}" -H "Content-Type: application/octet-stream" --data-binary "@./amnet-web.zip" "${{ needs.get-upload-url.outputs.upload_url }}?name=amnet-web.zip"
|
||||||
@@ -4,18 +4,18 @@
|
|||||||
<PublishAot>true</PublishAot>
|
<PublishAot>true</PublishAot>
|
||||||
<OutputType>Library</OutputType>
|
<OutputType>Library</OutputType>
|
||||||
<EventSourceSupport>false</EventSourceSupport>
|
<EventSourceSupport>false</EventSourceSupport>
|
||||||
<TargetFramework>net8.0-windows</TargetFramework>
|
<TargetFramework>net9.0-windows</TargetFramework>
|
||||||
<OptimizationPreference>Size</OptimizationPreference>
|
<OptimizationPreference>Size</OptimizationPreference>
|
||||||
<NoDefaultLaunchSettingsFile>true</NoDefaultLaunchSettingsFile>
|
<NoDefaultLaunchSettingsFile>true</NoDefaultLaunchSettingsFile>
|
||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
|
|
||||||
<PropertyGroup>
|
<PropertyGroup>
|
||||||
<AssemblyName>amnet</AssemblyName>
|
<Product>AMNet</Product>
|
||||||
<ProductName>AMNet</ProductName>
|
|
||||||
<CompanyName>ppc</CompanyName>
|
<CompanyName>ppc</CompanyName>
|
||||||
<Title>AMNet Server</Title>
|
<AssemblyName>amnet</AssemblyName>
|
||||||
<Description>Server component for the AMNet IC Card switcher</Description>
|
<AssemblyTitle>AMNet Server</AssemblyTitle>
|
||||||
<Copyright>Copyright 2024 (c) ppc</Copyright>
|
<Copyright>Copyright 2024 (c) ppc</Copyright>
|
||||||
|
<Description>Server component for the AMNet IC Card switcher</Description>
|
||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
|
|
||||||
<PropertyGroup>
|
<PropertyGroup>
|
||||||
|
|||||||
@@ -0,0 +1,79 @@
|
|||||||
|
using System;
|
||||||
|
using System.Globalization;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Threading;
|
||||||
|
|
||||||
|
namespace AMNet.Server;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Represents a storage location to load/read cards from
|
||||||
|
/// </summary>
|
||||||
|
internal class CardPresenter
|
||||||
|
{
|
||||||
|
private readonly Lock _cardLock = new();
|
||||||
|
private StoredCard _currentCard;
|
||||||
|
|
||||||
|
public record StoredCard(byte[] Value, string OriginalValue, long ExpiresAt)
|
||||||
|
{
|
||||||
|
public bool Expired => Environment.TickCount64 >= ExpiresAt;
|
||||||
|
|
||||||
|
public override string ToString() => string.Join(" ", Enumerable.Range(0, 5).Select(x => OriginalValue.Substring(x * 4, 4)));
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Returns the currently active card without clearing it
|
||||||
|
/// </summary>
|
||||||
|
public StoredCard PeekActiveCard()
|
||||||
|
{
|
||||||
|
lock (_cardLock)
|
||||||
|
{
|
||||||
|
return _currentCard;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public StoredCard TakeActiveCard()
|
||||||
|
{
|
||||||
|
lock (_cardLock)
|
||||||
|
{
|
||||||
|
var card = _currentCard;
|
||||||
|
_currentCard = null;
|
||||||
|
|
||||||
|
return card;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Sets the card
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="cardId">The card id to set</param>
|
||||||
|
/// <param name="validFor">How long the card should be presented to the game for in milliseconds</param>
|
||||||
|
/// <returns>Whether the card was set successfully</returns>
|
||||||
|
public StoredCard SetCard(string cardId, long validFor = 5000)
|
||||||
|
{
|
||||||
|
// ensure the matrix code is 20-digits long, otherwise pad with zeros
|
||||||
|
var matrixCode = cardId.Replace(" ", "").PadLeft(20, '0');
|
||||||
|
var bytes = new byte[10];
|
||||||
|
|
||||||
|
for (var i = 0; i < bytes.Length; i++)
|
||||||
|
{
|
||||||
|
var value = matrixCode.Substring(i * 2, 2);
|
||||||
|
if (byte.TryParse(value, NumberStyles.HexNumber, CultureInfo.InvariantCulture, out var b))
|
||||||
|
{
|
||||||
|
bytes[i] = b;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var card = new StoredCard(bytes, matrixCode, Environment.TickCount64 + validFor);
|
||||||
|
|
||||||
|
lock (_cardLock)
|
||||||
|
{
|
||||||
|
_currentCard = card;
|
||||||
|
}
|
||||||
|
|
||||||
|
return card;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,37 @@
|
|||||||
|
using System;
|
||||||
|
using Windows.Win32;
|
||||||
|
using Windows.Win32.Foundation;
|
||||||
|
|
||||||
|
namespace AMNet.Server;
|
||||||
|
|
||||||
|
internal static class Config
|
||||||
|
{
|
||||||
|
private const string ConfigFileName = @".\segatools.ini";
|
||||||
|
|
||||||
|
internal const string IOSection = "aimeio";
|
||||||
|
|
||||||
|
static Config()
|
||||||
|
{
|
||||||
|
GameId = ReadKey(IOSection, "gameId", 4);
|
||||||
|
ServerName = ReadKey(IOSection, "serverName", 26, Environment.MachineName);
|
||||||
|
|
||||||
|
EnableAimeTxt = PInvoke.GetPrivateProfileInt(IOSection, "enableKeyboardMode", 1, ConfigFileName) > 0;
|
||||||
|
AimeTxtPath = ReadKey("aime", "aimePath", PInvoke.MAX_PATH, @"DEVICE\aime.txt");
|
||||||
|
}
|
||||||
|
|
||||||
|
public static string GameId { get; }
|
||||||
|
public static string ServerName { get; }
|
||||||
|
|
||||||
|
public static bool EnableAimeTxt { get; }
|
||||||
|
public static string AimeTxtPath { get; }
|
||||||
|
|
||||||
|
internal static unsafe string ReadKey(string section, string key, uint maxLength, string @default = null)
|
||||||
|
{
|
||||||
|
// +1 for null terminator
|
||||||
|
var buffer = stackalloc char[(int)maxLength + 1];
|
||||||
|
var bufferStr = new PWSTR(buffer);
|
||||||
|
|
||||||
|
PInvoke.GetPrivateProfileString(section, key, @default ?? string.Empty, bufferStr, maxLength + 1, ConfigFileName);
|
||||||
|
return bufferStr.ToString();
|
||||||
|
}
|
||||||
|
}
|
||||||
+68
-148
@@ -1,206 +1,126 @@
|
|||||||
using System;
|
using System;
|
||||||
using System.Globalization;
|
using System.IO;
|
||||||
using System.Linq;
|
|
||||||
using System.Runtime.InteropServices;
|
using System.Runtime.InteropServices;
|
||||||
using System.Text.Json;
|
using System.Text;
|
||||||
using System.Threading;
|
using System.Threading;
|
||||||
using Windows.Win32;
|
using Windows.Win32;
|
||||||
using Windows.Win32.Foundation;
|
|
||||||
using Microsoft.AspNetCore.Builder;
|
using Microsoft.AspNetCore.Builder;
|
||||||
using Microsoft.AspNetCore.Hosting;
|
using Microsoft.Extensions.DependencyInjection;
|
||||||
using Microsoft.AspNetCore.Http;
|
|
||||||
using Microsoft.Extensions.Logging;
|
using Microsoft.Extensions.Logging;
|
||||||
|
|
||||||
namespace AMNet.Server;
|
namespace AMNet.Server;
|
||||||
|
|
||||||
public static class DllMain
|
public static class DllMain
|
||||||
{
|
{
|
||||||
private const int ApiVersion = 1;
|
|
||||||
private const string WebAddress = "http://card.ppc.moe";
|
private const string WebAddress = "http://card.ppc.moe";
|
||||||
|
|
||||||
private static ILogger _serverLogger;
|
// instance metrics
|
||||||
|
internal static long LastPollTime = -1;
|
||||||
private static string[] _serverAddresses;
|
internal static long ServerStartedAt;
|
||||||
private static string _serverName;
|
|
||||||
private static string _gameId;
|
|
||||||
|
|
||||||
private static (byte[] IdBytes, string OriginalId, long Expires)? _currentCard;
|
|
||||||
|
|
||||||
private static readonly object _cardLock = new();
|
|
||||||
|
|
||||||
static DllMain()
|
static DllMain()
|
||||||
{
|
{
|
||||||
PInvoke.AllocConsole();
|
PInvoke.AllocConsole();
|
||||||
|
|
||||||
|
App = WebServer.BuildServer(Config.ReadKey(Config.IOSection, "serverAddress", 1024, "http://+:6070").Split(';'));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static WebApplication App { get; }
|
||||||
|
|
||||||
[UnmanagedCallersOnly(EntryPoint = "aime_io_get_api_version")]
|
[UnmanagedCallersOnly(EntryPoint = "aime_io_get_api_version")]
|
||||||
public static ushort GetApiVersion() => 0x0100;
|
public static ushort GetApiVersion() => 0x0100;
|
||||||
|
|
||||||
[UnmanagedCallersOnly(EntryPoint = "aime_io_init")]
|
[UnmanagedCallersOnly(EntryPoint = "aime_io_init")]
|
||||||
public static int Init()
|
public static int Init()
|
||||||
{
|
{
|
||||||
LoadConfiguration();
|
CancellationTokenRegistration? cancellationRegistration = null;
|
||||||
|
cancellationRegistration = App.Lifetime.ApplicationStarted.Register(() =>
|
||||||
var builder = WebServer.BuildServer();
|
|
||||||
builder.WebHost.UseUrls(_serverAddresses);
|
|
||||||
|
|
||||||
var app = builder.Build();
|
|
||||||
|
|
||||||
_serverLogger = app.Logger;
|
|
||||||
|
|
||||||
app.UseCors();
|
|
||||||
app.MapGet("/amnet/info", () => Results.Ok(new SystemState(ApiVersion, _gameId, _serverName)));
|
|
||||||
app.MapPost("/amnet/signin", async ctx =>
|
|
||||||
{
|
{
|
||||||
CardReadRequest request;
|
App.Logger.LogInformation("AMNet Server ({gameId}) started successfully.", Config.GameId);
|
||||||
|
App.Logger.LogInformation("Visit {addr} from a mobile device on the same network to get started.", WebAddress);
|
||||||
|
|
||||||
try
|
// ReSharper disable once AccessToModifiedClosure
|
||||||
{
|
cancellationRegistration?.Dispose();
|
||||||
request = await ctx.Request.ReadFromJsonAsync<CardReadRequest>();
|
|
||||||
}
|
|
||||||
catch (JsonException e)
|
|
||||||
{
|
|
||||||
ctx.Response.StatusCode = 400;
|
|
||||||
await ctx.Response.WriteAsync(e.Message);
|
|
||||||
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (string.IsNullOrEmpty(request?.MatrixCode))
|
|
||||||
{
|
|
||||||
ctx.Response.StatusCode = 400;
|
|
||||||
await ctx.Response.WriteAsync("Card id not provided.");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (request.MatrixCode.Length is 0 or < 20)
|
|
||||||
{
|
|
||||||
ctx.Response.StatusCode = 422;
|
|
||||||
await ctx.Response.WriteAsync("Invalid card id length.");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (request.MatrixCode.Any(x => !char.IsNumber(x)))
|
|
||||||
{
|
|
||||||
ctx.Response.StatusCode = 422;
|
|
||||||
await ctx.Response.WriteAsync("Invalid card id format.");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
lock (_cardLock)
|
|
||||||
{
|
|
||||||
// ratelimit check
|
|
||||||
if (_currentCard?.Expires > Environment.TickCount64)
|
|
||||||
{
|
|
||||||
ctx.Response.StatusCode = 429;
|
|
||||||
ctx.Response.Headers.RetryAfter = ((int)TimeSpan.FromMilliseconds(_currentCard.Value.Expires - Environment.TickCount64).TotalSeconds).ToString();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// ensure the matrix code is 20-digits long, otherwise pad with zeros
|
|
||||||
var matrixCode = request.MatrixCode.PadLeft(20, '0');
|
|
||||||
var bytes = new byte[10];
|
|
||||||
|
|
||||||
for (var i = 0; i < 10; i++)
|
|
||||||
{
|
|
||||||
var value = matrixCode.Substring(i * 2, 2);
|
|
||||||
bytes[i] = byte.Parse(value, NumberStyles.HexNumber);
|
|
||||||
}
|
|
||||||
|
|
||||||
// it's called a tick count, but it uses milliseconds despite having a unit of time called a tick???
|
|
||||||
_currentCard = (bytes, matrixCode, Environment.TickCount64 + 5000);
|
|
||||||
}
|
|
||||||
|
|
||||||
ctx.Response.StatusCode = 202;
|
|
||||||
});
|
});
|
||||||
|
|
||||||
CancellationTokenRegistration cancellationRegistration = default;
|
Volatile.Write(ref ServerStartedAt, Environment.TickCount64);
|
||||||
cancellationRegistration = app.Lifetime.ApplicationStarted.Register(() =>
|
App.RunAsync();
|
||||||
{
|
|
||||||
_serverLogger.LogInformation("AMNet Server ({gameId}) started successfully.", _gameId);
|
|
||||||
_serverLogger.LogInformation("Visit {addr} from a mobile device on the same network to get started.", WebAddress);
|
|
||||||
|
|
||||||
cancellationRegistration.Dispose();
|
|
||||||
});
|
|
||||||
|
|
||||||
app.RunAsync();
|
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
[UnmanagedCallersOnly(EntryPoint = "aime_io_nfc_poll")]
|
[UnmanagedCallersOnly(EntryPoint = "aime_io_nfc_poll")]
|
||||||
public static int NfcPoll(byte unitNo)
|
public static int NfcPoll(byte unitNo)
|
||||||
{
|
{
|
||||||
if (_currentCard.HasValue && _currentCard.Value.Expires < Environment.TickCount64)
|
Volatile.Write(ref LastPollTime, Environment.TickCount64);
|
||||||
|
|
||||||
|
if (!Config.EnableAimeTxt)
|
||||||
{
|
{
|
||||||
lock (_cardLock)
|
return 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
// check keyboard input for the enter key being held down (0x0D)
|
||||||
|
var enterPressed = (PInvoke.GetAsyncKeyState(0x0D) & 0x8000) != 0;
|
||||||
|
|
||||||
|
if (!enterPressed)
|
||||||
|
{
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
using var aimeTxtReader = new StreamReader(File.OpenRead(Config.AimeTxtPath), Encoding.UTF8);
|
||||||
|
|
||||||
|
var fileName = Path.GetFileName(Config.AimeTxtPath);
|
||||||
|
var cardResult = App.Services.GetRequiredService<CardPresenter>().SetCard(aimeTxtReader.ReadLine());
|
||||||
|
|
||||||
|
if (cardResult == null)
|
||||||
{
|
{
|
||||||
_currentCard = null;
|
App.Logger.LogWarning("Failed to read card from {file}: invalid format", fileName);
|
||||||
}
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
App.Logger.LogInformation("Card loaded from {fileName}", fileName);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch (IOException e)
|
||||||
|
{
|
||||||
|
App.Logger.LogWarning("Failed to read '{path}': {message}", Path.GetFullPath(Config.AimeTxtPath), e.Message);
|
||||||
}
|
}
|
||||||
|
|
||||||
// there's no polling here (handled by the webserver)
|
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
[UnmanagedCallersOnly(EntryPoint = "aime_io_nfc_get_aime_id")]
|
[UnmanagedCallersOnly(EntryPoint = "aime_io_nfc_get_aime_id")]
|
||||||
public static int GetAimeId(byte unitNo, nint luid, nint luidSize)
|
public static int GetAimeId(byte unitNo, IntPtr luid, nint luidSize)
|
||||||
{
|
{
|
||||||
lock (_cardLock)
|
if (unitNo != 0)
|
||||||
{
|
{
|
||||||
if (unitNo != 0 || _currentCard == null)
|
return 1;
|
||||||
{
|
|
||||||
return 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (_currentCard.Value.Expires < Environment.TickCount64)
|
|
||||||
{
|
|
||||||
_serverLogger.LogWarning("Submitted card id expired before it could be read.");
|
|
||||||
_currentCard = null;
|
|
||||||
|
|
||||||
return 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
Marshal.Copy(_currentCard.Value.IdBytes, 0, luid, (int)luidSize);
|
|
||||||
|
|
||||||
_serverLogger.LogInformation("Card read in: {0}", string.Join(" ", Enumerable.Range(0, 5).Select(x => _currentCard.Value.OriginalId.Substring(x * 4, 4))));
|
|
||||||
_currentCard = null;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return 0;
|
var card = App.Services.GetRequiredService<CardPresenter>().TakeActiveCard();
|
||||||
|
if (card?.Expired != false)
|
||||||
|
{
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
Marshal.Copy(card.Value, 0, luid, (int)luidSize);
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
App.Logger.LogInformation("Card read in: {cardId}", card);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
[UnmanagedCallersOnly(EntryPoint = "aime_io_nfc_get_felica_id")]
|
[UnmanagedCallersOnly(EntryPoint = "aime_io_nfc_get_felica_id")]
|
||||||
public static unsafe int GetFelicaId(byte unitNo, ulong* idm)
|
public static unsafe int GetFelicaId(byte unitNo, ulong* idm) => 1; // felica not supported
|
||||||
{
|
|
||||||
// felica not supported
|
|
||||||
return 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
[UnmanagedCallersOnly(EntryPoint = "aime_io_led_set_color")]
|
[UnmanagedCallersOnly(EntryPoint = "aime_io_led_set_color")]
|
||||||
public static void SetLedColour(byte unitNo, byte r, byte g, byte b)
|
public static void SetLedColour(byte unitNo, byte r, byte g, byte b)
|
||||||
{
|
{
|
||||||
// do nothing
|
// do nothing
|
||||||
}
|
}
|
||||||
|
|
||||||
private static unsafe void LoadConfiguration()
|
|
||||||
{
|
|
||||||
const string configFileName = @".\segatools.ini";
|
|
||||||
|
|
||||||
var gameId = stackalloc char[5];
|
|
||||||
var gameIdStr = new PWSTR(gameId);
|
|
||||||
|
|
||||||
var serverName = stackalloc char[64];
|
|
||||||
var serverNameStr = new PWSTR(serverName);
|
|
||||||
|
|
||||||
var serverAddress = stackalloc char[1024];
|
|
||||||
var serverAddressStr = new PWSTR(serverAddress);
|
|
||||||
|
|
||||||
PInvoke.GetPrivateProfileString("aimeio", "gameId", string.Empty, gameIdStr, 5, configFileName);
|
|
||||||
PInvoke.GetPrivateProfileString("aimeio", "serverName", Environment.MachineName, serverNameStr, 64, configFileName);
|
|
||||||
PInvoke.GetPrivateProfileString("aimeio", "serverAddress", "http://+:6070", serverAddressStr, 1024, configFileName);
|
|
||||||
|
|
||||||
_gameId = gameIdStr.ToString();
|
|
||||||
_serverName = serverNameStr.ToString();
|
|
||||||
_serverAddresses = serverAddressStr.ToString().Split(';');
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
@@ -1,3 +1,5 @@
|
|||||||
GetPrivateProfileStringW
|
GetPrivateProfileStringW
|
||||||
GetPrivateProfileInt
|
GetPrivateProfileInt
|
||||||
AllocConsole
|
GetAsyncKeyState
|
||||||
|
AllocConsole
|
||||||
|
MAX_PATH
|
||||||
|
|||||||
@@ -1,16 +0,0 @@
|
|||||||
using System.Text.Json.Serialization;
|
|
||||||
|
|
||||||
namespace AMNet.Server;
|
|
||||||
|
|
||||||
public record SystemState(
|
|
||||||
[property: JsonPropertyName("apiVersion")] int ApiVersion,
|
|
||||||
[property: JsonPropertyName("gameId")] string GameId,
|
|
||||||
[property: JsonPropertyName("serverName")] string ServerName);
|
|
||||||
|
|
||||||
public record CardReadRequest(
|
|
||||||
[property: JsonPropertyName("cardId")] string MatrixCode);
|
|
||||||
|
|
||||||
[JsonSerializable(typeof(SystemState))]
|
|
||||||
[JsonSerializable(typeof(CardReadRequest))]
|
|
||||||
[JsonSourceGenerationOptions(WriteIndented = true)]
|
|
||||||
public partial class SerializerContext : JsonSerializerContext;
|
|
||||||
+106
-3
@@ -1,16 +1,37 @@
|
|||||||
using System;
|
using System;
|
||||||
|
using System.Globalization;
|
||||||
|
using System.Text.Json;
|
||||||
|
using System.Text.Json.Serialization;
|
||||||
|
using System.Threading;
|
||||||
|
using System.Threading.Tasks;
|
||||||
using Microsoft.AspNetCore.Builder;
|
using Microsoft.AspNetCore.Builder;
|
||||||
|
using Microsoft.AspNetCore.Hosting;
|
||||||
|
using Microsoft.AspNetCore.Http;
|
||||||
using Microsoft.Extensions.DependencyInjection;
|
using Microsoft.Extensions.DependencyInjection;
|
||||||
using Microsoft.Extensions.Logging;
|
using Microsoft.Extensions.Logging;
|
||||||
|
|
||||||
namespace AMNet.Server;
|
namespace AMNet.Server;
|
||||||
|
|
||||||
internal static class WebServer
|
internal static partial class WebServer
|
||||||
{
|
{
|
||||||
public static WebApplicationBuilder BuildServer()
|
private const int ApiVersion = 1;
|
||||||
|
|
||||||
|
public record SystemState(
|
||||||
|
[property: JsonPropertyName("apiVersion")] int ApiVersion,
|
||||||
|
[property: JsonPropertyName("gameId")] string GameId,
|
||||||
|
[property: JsonPropertyName("serverName")] string ServerName,
|
||||||
|
[property: JsonPropertyName("sessionUptime")] long SessionUptime,
|
||||||
|
[property: JsonPropertyName("timeSinceLastPoll")] long? TimeSinceLastPoll);
|
||||||
|
|
||||||
|
public record CardReadRequest(
|
||||||
|
[property: JsonPropertyName("cardId")] string MatrixCode);
|
||||||
|
|
||||||
|
public static WebApplication BuildServer(params string[] listenAddresses)
|
||||||
{
|
{
|
||||||
var builder = WebApplication.CreateSlimBuilder([]);
|
var builder = WebApplication.CreateSlimBuilder([]);
|
||||||
|
|
||||||
|
builder.WebHost.UseUrls(listenAddresses);
|
||||||
|
|
||||||
builder.Logging.ClearProviders();
|
builder.Logging.ClearProviders();
|
||||||
builder.Logging.AddSimpleConsole(o =>
|
builder.Logging.AddSimpleConsole(o =>
|
||||||
{
|
{
|
||||||
@@ -39,6 +60,88 @@ internal static class WebServer
|
|||||||
options.SerializerOptions.TypeInfoResolverChain.Insert(0, SerializerContext.Default);
|
options.SerializerOptions.TypeInfoResolverChain.Insert(0, SerializerContext.Default);
|
||||||
});
|
});
|
||||||
|
|
||||||
return builder;
|
builder.Services.AddSingleton<CardPresenter>();
|
||||||
|
|
||||||
|
var app = builder.Build();
|
||||||
|
|
||||||
|
app.UseCors();
|
||||||
|
|
||||||
|
app.MapGet("/amnet/info", ServerInfo);
|
||||||
|
app.MapPost("/amnet/signin", ProcessCard);
|
||||||
|
|
||||||
|
return app;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static IResult ServerInfo()
|
||||||
|
{
|
||||||
|
var startedAt = Volatile.Read(ref DllMain.ServerStartedAt);
|
||||||
|
var lastPollAt = Volatile.Read(ref DllMain.LastPollTime);
|
||||||
|
|
||||||
|
var state = new SystemState(
|
||||||
|
ApiVersion,
|
||||||
|
Config.GameId,
|
||||||
|
Config.ServerName,
|
||||||
|
Environment.TickCount64 - startedAt,
|
||||||
|
lastPollAt < 0 ? null : Environment.TickCount64 - lastPollAt);
|
||||||
|
|
||||||
|
return Results.Ok(state);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static async Task ProcessCard(HttpContext ctx)
|
||||||
|
{
|
||||||
|
CardReadRequest request;
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
request = await ctx.Request.ReadFromJsonAsync<CardReadRequest>();
|
||||||
|
}
|
||||||
|
catch (JsonException e)
|
||||||
|
{
|
||||||
|
ctx.Response.StatusCode = 400;
|
||||||
|
await ctx.Response.WriteAsync(e.Message);
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (string.IsNullOrEmpty(request?.MatrixCode))
|
||||||
|
{
|
||||||
|
ctx.Response.StatusCode = 400;
|
||||||
|
await ctx.Response.WriteAsync("Card id not provided.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (request.MatrixCode.Length is 0 or < 20)
|
||||||
|
{
|
||||||
|
ctx.Response.StatusCode = 422;
|
||||||
|
await ctx.Response.WriteAsync("Invalid card id length.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var cardPresenter = ctx.RequestServices.GetRequiredService<CardPresenter>();
|
||||||
|
var currentCard = cardPresenter.PeekActiveCard();
|
||||||
|
|
||||||
|
if (currentCard?.Expired == false)
|
||||||
|
{
|
||||||
|
var timeLeft = Math.Ceiling(TimeSpan.FromMilliseconds(currentCard.ExpiresAt - Environment.TickCount64).TotalSeconds);
|
||||||
|
|
||||||
|
ctx.Response.StatusCode = 429;
|
||||||
|
ctx.Response.Headers.RetryAfter = timeLeft.ToString(CultureInfo.InvariantCulture);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (cardPresenter.SetCard(request.MatrixCode) != null)
|
||||||
|
{
|
||||||
|
ctx.Response.StatusCode = 202;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
ctx.Response.StatusCode = 422;
|
||||||
|
await ctx.Response.WriteAsync("Invalid card id format.");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
[JsonSerializable(typeof(SystemState))]
|
||||||
|
[JsonSerializable(typeof(CardReadRequest))]
|
||||||
|
[JsonSourceGenerationOptions(WriteIndented = true, DefaultIgnoreCondition = JsonIgnoreCondition.Never)]
|
||||||
|
private partial class SerializerContext : JsonSerializerContext;
|
||||||
}
|
}
|
||||||
+56
-3
@@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"name": "AMNet",
|
"name": "AMNet",
|
||||||
"scheme": "amnet",
|
"scheme": "amnet",
|
||||||
"version": "1.0.0",
|
"version": "1.2.0",
|
||||||
"slug": "amnet-native",
|
"slug": "amnet-native",
|
||||||
"icon": "./assets/icon.png",
|
"icon": "./assets/icon.png",
|
||||||
"userInterfaceStyle": "automatic",
|
"userInterfaceStyle": "automatic",
|
||||||
@@ -19,7 +19,60 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"android": {
|
"android": {
|
||||||
"package": "moe.ppc.amnet"
|
"package": "moe.ppc.amnet",
|
||||||
|
"versionCode": 3,
|
||||||
|
"adaptiveIcon": {
|
||||||
|
"foregroundImage": "./assets/icon-android-alt.png",
|
||||||
|
"backgroundColor": "#DED8FD"
|
||||||
|
},
|
||||||
|
"splash": {
|
||||||
|
"image": "./assets/icon-android-alt.png",
|
||||||
|
"backgroundColor": "#DED8FD",
|
||||||
|
"dark": {
|
||||||
|
"backgroundColor": "#3B5ACC"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"intentFilters": [
|
||||||
|
{
|
||||||
|
"autoVerify": true,
|
||||||
|
"action": "VIEW",
|
||||||
|
"data": [
|
||||||
|
{
|
||||||
|
"scheme": "http",
|
||||||
|
"host": "card.ppc.moe",
|
||||||
|
"pathPrefix": "/#/servers"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"scheme": "http",
|
||||||
|
"host": "card.ppc.moe",
|
||||||
|
"pathPrefix": "/#/cards"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"scheme": "https",
|
||||||
|
"host": "card.ppc.moe",
|
||||||
|
"pathPrefix": "/#/servers"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"scheme": "https",
|
||||||
|
"host": "card.ppc.moe",
|
||||||
|
"pathPrefix": "/#/cards"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"category": [
|
||||||
|
"BROWSABLE",
|
||||||
|
"DEFAULT"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
},
|
},
|
||||||
"plugins": []
|
"plugins": [
|
||||||
|
[
|
||||||
|
"expo-build-properties",
|
||||||
|
{
|
||||||
|
"android": {
|
||||||
|
"usesCleartextTraffic": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
]
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import {generateCardNumber} from "amnet-shared";
|
import {generateCardNumber} from "amnet-shared";
|
||||||
import {useModalStyles} from "../utils/ModalStyles";
|
import {useModalStyles} from "../utils/ModalStyles";
|
||||||
import {useCallback, useLayoutEffect, useState} from "react";
|
import {useCallback, useLayoutEffect, useState} from "react";
|
||||||
import {TextInput, TouchableOpacity, View} from "react-native";
|
import {Platform, TextInput, TouchableOpacity, View} from "react-native";
|
||||||
import {Button, Card, Divider, Paragraph, useTheme} from "react-native-paper";
|
import {Button, Card, Divider, Paragraph, useTheme} from "react-native-paper";
|
||||||
|
|
||||||
import {AMCard} from "../models/AMCard";
|
import {AMCard} from "../models/AMCard";
|
||||||
@@ -9,7 +9,7 @@ import RealmContext from "../models/RealmContext";
|
|||||||
|
|
||||||
const {useRealm} = RealmContext;
|
const {useRealm} = RealmContext;
|
||||||
|
|
||||||
export default function CardAddModal({navigation}) {
|
export default function CardAddModal({navigation, route}) {
|
||||||
const realm = useRealm();
|
const realm = useRealm();
|
||||||
const theme = useTheme();
|
const theme = useTheme();
|
||||||
const modalStyles = useModalStyles();
|
const modalStyles = useModalStyles();
|
||||||
@@ -17,9 +17,10 @@ export default function CardAddModal({navigation}) {
|
|||||||
// state values
|
// state values
|
||||||
const [cardName, setCardName] = useState('');
|
const [cardName, setCardName] = useState('');
|
||||||
const [cardNumber, setCardNumber] = useState('');
|
const [cardNumber, setCardNumber] = useState('');
|
||||||
|
const [cardNumberReadOnly, setCardNumberReadOnly] = useState(false);
|
||||||
|
|
||||||
const cardAddCallback = useCallback((cardId: string, cardName: string) => {
|
const cardAddCallback = useCallback((cardId: string, cardName: string) => {
|
||||||
const duplicateCard = realm.objects<AMCard>("card").find(x => x.code == cardNumber);
|
const duplicateCard = realm.objects<AMCard>("card").find(x => x.code === cardId);
|
||||||
|
|
||||||
if (duplicateCard) {
|
if (duplicateCard) {
|
||||||
realm.write(() => {
|
realm.write(() => {
|
||||||
@@ -35,13 +36,15 @@ export default function CardAddModal({navigation}) {
|
|||||||
}, [realm]);
|
}, [realm]);
|
||||||
|
|
||||||
// header effects
|
// header effects
|
||||||
useLayoutEffect(() => {
|
if (Platform.OS === 'ios') {
|
||||||
navigation.setOptions({
|
useLayoutEffect(() => {
|
||||||
headerLeft: () => <TouchableOpacity onPress={() => navigation.goBack()}>
|
navigation.setOptions({
|
||||||
<Paragraph style={modalStyles.headerText}>Cancel</Paragraph>
|
headerLeft: () => <TouchableOpacity onPress={() => navigation.goBack()}>
|
||||||
</TouchableOpacity>,
|
<Paragraph style={modalStyles.headerText}>Cancel</Paragraph>
|
||||||
});
|
</TouchableOpacity>,
|
||||||
}, [navigation, modalStyles]);
|
});
|
||||||
|
}, [navigation, modalStyles]);
|
||||||
|
}
|
||||||
|
|
||||||
useLayoutEffect(() => {
|
useLayoutEffect(() => {
|
||||||
const canSave = cardName.length && cardNumber.length === 20;
|
const canSave = cardName.length && cardNumber.length === 20;
|
||||||
@@ -55,6 +58,25 @@ export default function CardAddModal({navigation}) {
|
|||||||
})
|
})
|
||||||
}, [navigation, modalStyles, cardName, cardNumber]);
|
}, [navigation, modalStyles, cardName, cardNumber]);
|
||||||
|
|
||||||
|
// enable renaming by passing cardId to modal
|
||||||
|
useLayoutEffect(() => {
|
||||||
|
if (!route.params?.cardId) {
|
||||||
|
setCardNumberReadOnly(false);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
setCardNumber(route.params.cardId);
|
||||||
|
setCardNumberReadOnly(true);
|
||||||
|
|
||||||
|
if (route.params.cardName) {
|
||||||
|
setCardName(route.params.cardName);
|
||||||
|
}
|
||||||
|
|
||||||
|
navigation.setOptions({
|
||||||
|
title: "Edit Card"
|
||||||
|
});
|
||||||
|
}, [route]);
|
||||||
|
|
||||||
return (<View style={{display: "flex", gap: 10, justifyContent: "center", paddingHorizontal: 15, paddingTop: 25}}>
|
return (<View style={{display: "flex", gap: 10, justifyContent: "center", paddingHorizontal: 15, paddingTop: 25}}>
|
||||||
<Card mode="contained">
|
<Card mode="contained">
|
||||||
<Card.Content>
|
<Card.Content>
|
||||||
@@ -76,21 +98,27 @@ export default function CardAddModal({navigation}) {
|
|||||||
<Paragraph style={modalStyles.formItemName}>
|
<Paragraph style={modalStyles.formItemName}>
|
||||||
Matrix Code
|
Matrix Code
|
||||||
</Paragraph>
|
</Paragraph>
|
||||||
<TextInput style={modalStyles.formItemInput}
|
<TextInput numberOfLines={1}
|
||||||
numberOfLines={1}
|
|
||||||
keyboardType="numeric"
|
keyboardType="numeric"
|
||||||
|
readOnly={cardNumberReadOnly}
|
||||||
placeholder="0000 0000 0000 0000 0000"
|
placeholder="0000 0000 0000 0000 0000"
|
||||||
placeholderTextColor={theme.colors.outline}
|
placeholderTextColor={theme.colors.outline}
|
||||||
value={cardNumber.match(/.{1,4}/g)?.join(" ")}
|
value={cardNumber.match(/.{1,4}/g)?.join(" ")}
|
||||||
onChangeText={val => setCardNumber(val?.replaceAll(/\D/g, '').substring(0, 20) ?? '')}/>
|
onChangeText={val => setCardNumber(val?.replaceAll(/\D/g, '').substring(0, 20) ?? '')}
|
||||||
|
style={{...modalStyles.formItemInput, color: cardNumberReadOnly ? theme.colors.onSurfaceDisabled : theme.colors.onSurface}}/>
|
||||||
</View>
|
</View>
|
||||||
</Card.Content>
|
</Card.Content>
|
||||||
</Card>
|
</Card>
|
||||||
|
|
||||||
<Paragraph style={modalStyles.formFootnote}>
|
{/*only show card entry info when in create mode*/}
|
||||||
If you have a physical card, you can enter the 20-digit code on the back.
|
{!cardNumberReadOnly && (
|
||||||
</Paragraph>
|
<>
|
||||||
|
<Paragraph style={modalStyles.formFootnote}>
|
||||||
|
If you have a physical card, you can enter the 20-digit code on the back.
|
||||||
|
</Paragraph>
|
||||||
|
|
||||||
<Button mode="text" onPress={() => setCardNumber(generateCardNumber())}>Generate Matrix Code</Button>
|
<Button mode="text" onPress={() => setCardNumber(generateCardNumber())}>Generate Matrix Code</Button>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
</View>)
|
</View>)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,60 +1,72 @@
|
|||||||
import * as Clipboard from "expo-clipboard";
|
import * as Clipboard from "expo-clipboard";
|
||||||
import {FlatList, View} from "react-native";
|
import {FlatList, Platform, View} from "react-native";
|
||||||
import React, {useCallback, useMemo} from "react";
|
import React, {useCallback, useMemo} from "react";
|
||||||
import {Button, useTheme} from "react-native-paper";
|
import {Button, useTheme} from "react-native-paper";
|
||||||
import {CreditCard, Plus} from "lucide-react-native";
|
import {CreditCard, Plus} from "lucide-react-native";
|
||||||
import ContextMenu from "react-native-context-menu-view";
|
import ContextMenu from "react-native-context-menu-view";
|
||||||
|
|
||||||
import {AMCard} from "../models/AMCard";
|
|
||||||
import {AimeCard} from "../components/AimeCard";
|
import {AimeCard} from "../components/AimeCard";
|
||||||
import RealmContext from "../models/RealmContext";
|
|
||||||
import {EmptyStatePlaceholder} from "../components/EmptyStatePlaceholder";
|
import {EmptyStatePlaceholder} from "../components/EmptyStatePlaceholder";
|
||||||
|
|
||||||
|
import {AMCard} from "../models/AMCard";
|
||||||
|
import RealmContext from "../models/RealmContext";
|
||||||
|
|
||||||
const {useQuery, useRealm} = RealmContext;
|
const {useQuery, useRealm} = RealmContext;
|
||||||
|
|
||||||
function CardEntry(props: { card: AMCard }) {
|
function CardEntry(props: { card: AMCard, navigation: any }) {
|
||||||
const realm = useRealm();
|
const realm = useRealm();
|
||||||
const callback = useCallback(async (index: number) => {
|
const callback = useCallback(async (indexPath: number[]) => {
|
||||||
if (!props.card.isValid()) {
|
if (!props.card.isValid()) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
switch (index) {
|
switch (indexPath.toString()) {
|
||||||
// copy card no
|
// copy card
|
||||||
case 0:
|
case "0":
|
||||||
await Clipboard.setStringAsync(props.card.code);
|
await Clipboard.setStringAsync(props.card.code);
|
||||||
break;
|
return;
|
||||||
|
|
||||||
case 1:
|
// edit card
|
||||||
realm.write(() => {
|
case "1,0":
|
||||||
realm.delete(props.card);
|
props.navigation.navigate('add-card', {cardId: props.card.code, cardName: props.card.name});
|
||||||
});
|
return;
|
||||||
break;
|
|
||||||
|
// remove card
|
||||||
|
case "1,1":
|
||||||
|
realm.write(() => realm.delete(props.card));
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
}, [realm, props.card]);
|
}, [realm, props.card, props.navigation]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<ContextMenu
|
<ContextMenu
|
||||||
onPress={e => callback(e.nativeEvent?.index)}
|
onPress={e => callback(e.nativeEvent?.indexPath)}
|
||||||
actions={[
|
actions={[
|
||||||
{title: "Copy Card Number", systemIcon: "doc.on.doc"},
|
{title: "Copy Card Number", systemIcon: "doc.on.doc"},
|
||||||
{title: "Delete Card", destructive: true, systemIcon: "trash"}]}>
|
{
|
||||||
|
title: Platform.OS === 'ios' ? "" : "Additional Actions",
|
||||||
|
inlineChildren: true,
|
||||||
|
actions: [
|
||||||
|
{title: "Edit Card", systemIcon: "square.and.pencil"},
|
||||||
|
{title: "Delete Card", destructive: true, systemIcon: "trash"}]
|
||||||
|
}]}>
|
||||||
<AimeCard card={props.card} showFullCardNumber/>
|
<AimeCard card={props.card} showFullCardNumber/>
|
||||||
</ContextMenu>
|
</ContextMenu>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function CardManager({navigation}) {
|
export default function CardManager({navigation}) {
|
||||||
const query = useQuery(AMCard);
|
|
||||||
const cards = useMemo(() => query.sorted("added", true), [query]);
|
|
||||||
|
|
||||||
const theme = useTheme();
|
const theme = useTheme();
|
||||||
|
const query = useQuery(AMCard);
|
||||||
|
const cards = useMemo(() => query.sorted("added", false), [query]);
|
||||||
|
|
||||||
if (!cards.length) {
|
if (!cards.length) {
|
||||||
return (
|
return (
|
||||||
<View style={{flex: 1, gap: 10, justifyContent: "center", alignItems: "center"}}>
|
<View style={{flex: 1, gap: 10, justifyContent: "center", alignItems: "center"}}>
|
||||||
<EmptyStatePlaceholder message="No cards added yet" icon={<CreditCard color="gray"/>}/>
|
<EmptyStatePlaceholder message="No cards added yet" icon={<CreditCard color="gray"/>}/>
|
||||||
<Button mode="text" onPress={() => navigation.navigate('add-card')} icon={() => <Plus size={18} color={theme.colors.primary}/>}>Add Card</Button>
|
<Button mode="text"
|
||||||
|
onPress={() => navigation.navigate('add-card')}
|
||||||
|
icon={() => <Plus size={18} color={theme.colors.primary}/>}>Add Card</Button>
|
||||||
</View>
|
</View>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@@ -64,8 +76,15 @@ export default function CardManager({navigation}) {
|
|||||||
style={{padding: 16, paddingTop: 24}}
|
style={{padding: 16, paddingTop: 24}}
|
||||||
data={cards}
|
data={cards}
|
||||||
keyExtractor={i => i._id.toString()}
|
keyExtractor={i => i._id.toString()}
|
||||||
contentContainerStyle={{paddingBottom: 40}}
|
contentContainerStyle={{paddingBottom: 120}}
|
||||||
|
renderItem={i => <CardEntry card={i.item} navigation={navigation}/>}
|
||||||
ItemSeparatorComponent={() => <View style={{height: 15}}/>}
|
ItemSeparatorComponent={() => <View style={{height: 15}}/>}
|
||||||
renderItem={i => <CardEntry card={i.item}/>}/>
|
ListFooterComponent={() => Platform.OS === "ios" ? null : (
|
||||||
|
<Button mode="text"
|
||||||
|
onPress={() => navigation.navigate('add-card')}
|
||||||
|
style={{marginTop: 15, marginBottom: 25}}
|
||||||
|
icon={() => <Plus size={18} color={theme.colors.primary}/>}>Add Card</Button>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@@ -1,7 +1,7 @@
|
|||||||
import {useModalStyles} from "../utils/ModalStyles";
|
import {useModalStyles} from "../utils/ModalStyles";
|
||||||
import {Card, Paragraph, useTheme} from "react-native-paper";
|
import {Card, Paragraph, useTheme} from "react-native-paper";
|
||||||
import {TextInput, TouchableOpacity, View} from "react-native";
|
|
||||||
import {useCallback, useEffect, useLayoutEffect, useState} from "react";
|
import {useCallback, useEffect, useLayoutEffect, useState} from "react";
|
||||||
|
import {Platform, TextInput, TouchableOpacity, View} from "react-native";
|
||||||
|
|
||||||
import RealmContext from "../models/RealmContext";
|
import RealmContext from "../models/RealmContext";
|
||||||
import {AMServer} from "../models/AMServer";
|
import {AMServer} from "../models/AMServer";
|
||||||
@@ -20,13 +20,15 @@ export default function ServerAddModal({route, navigation}) {
|
|||||||
handleDeepLink(realm, navigation, route, setServerAddress);
|
handleDeepLink(realm, navigation, route, setServerAddress);
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
useLayoutEffect(() => {
|
if (Platform.OS === 'ios') {
|
||||||
navigation.setOptions({
|
useLayoutEffect(() => {
|
||||||
headerLeft: () => <TouchableOpacity onPress={() => navigation.goBack()}>
|
navigation.setOptions({
|
||||||
<Paragraph style={modalStyles.headerText}>Cancel</Paragraph>
|
headerLeft: () => <TouchableOpacity onPress={() => navigation.goBack()}>
|
||||||
</TouchableOpacity>,
|
<Paragraph style={modalStyles.headerText}>Cancel</Paragraph>
|
||||||
});
|
</TouchableOpacity>,
|
||||||
}, [navigation, modalStyles]);
|
});
|
||||||
|
}, [navigation, modalStyles]);
|
||||||
|
}
|
||||||
|
|
||||||
useLayoutEffect(() => {
|
useLayoutEffect(() => {
|
||||||
let urlValid = false;
|
let urlValid = false;
|
||||||
|
|||||||
+208
-133
@@ -1,16 +1,46 @@
|
|||||||
import {BSON} from "realm";
|
import {BSON} from "realm";
|
||||||
import {fetchServerInfo, performCardIn} from "amnet-shared";
|
|
||||||
import React, {useCallback, useEffect, useLayoutEffect, useMemo, useState} from "react";
|
import React, {useCallback, useEffect, useLayoutEffect, useMemo, useState} from "react";
|
||||||
import {ActivityIndicator, Button, Card, Paragraph, Title, useTheme} from "react-native-paper";
|
import {ActivityIndicator, Button, Paragraph, Title, useTheme} from "react-native-paper";
|
||||||
import {Alert, RefreshControl, ScrollView, Share, StyleSheet, TouchableOpacity, View} from "react-native";
|
import {
|
||||||
import {CircleCheck, CircleDot, CreditCard, Nfc, Plus, SmartphoneNfcIcon, WifiOff, Share as ShareIcon} from "lucide-react-native";
|
Alert,
|
||||||
|
Platform,
|
||||||
|
RefreshControl,
|
||||||
|
ScrollView,
|
||||||
|
Share,
|
||||||
|
StyleSheet,
|
||||||
|
Text,
|
||||||
|
TouchableOpacity,
|
||||||
|
View
|
||||||
|
} from "react-native";
|
||||||
|
import {
|
||||||
|
CircleCheck,
|
||||||
|
CircleDot,
|
||||||
|
CreditCard,
|
||||||
|
Nfc,
|
||||||
|
Plus,
|
||||||
|
SmartphoneNfcIcon,
|
||||||
|
Share as ShareIcon,
|
||||||
|
WifiOff,
|
||||||
|
TvMinimalPlay,
|
||||||
|
Star
|
||||||
|
} from "lucide-react-native";
|
||||||
|
|
||||||
import {EmptyStatePlaceholder} from "../components/EmptyStatePlaceholder";
|
import {EmptyStatePlaceholder} from "../components/EmptyStatePlaceholder";
|
||||||
|
import {AMUIStatusAlert} from "../components/AMUIStatusAlert";
|
||||||
|
import {AMUICardDetail} from "../components/AMUICard";
|
||||||
import {AimeCard} from "../components/AimeCard";
|
import {AimeCard} from "../components/AimeCard";
|
||||||
|
|
||||||
import RealmContext from "../models/RealmContext";
|
import RealmContext from "../models/RealmContext";
|
||||||
import {AMServer} from "../models/AMServer";
|
import {AMServer} from "../models/AMServer";
|
||||||
import {AMCard} from "../models/AMCard";
|
import {AMCard} from "../models/AMCard";
|
||||||
import ObjectId = BSON.ObjectId;
|
import ObjectId = BSON.ObjectId;
|
||||||
|
import {
|
||||||
|
AMServerConnectionInfo,
|
||||||
|
AMServerInstanceMetrics,
|
||||||
|
fetchServerInfo,
|
||||||
|
getGameName,
|
||||||
|
performCardIn
|
||||||
|
} from "amnet-shared";
|
||||||
|
|
||||||
const {useRealm, useQuery, useObject} = RealmContext;
|
const {useRealm, useQuery, useObject} = RealmContext;
|
||||||
|
|
||||||
@@ -18,110 +48,116 @@ export default function ServerCardIn({route, navigation}) {
|
|||||||
const realm = useRealm();
|
const realm = useRealm();
|
||||||
const theme = useTheme();
|
const theme = useTheme();
|
||||||
|
|
||||||
// card listing state
|
let {serverId} = route.params;
|
||||||
const query = useQuery(AMCard);
|
const server = useObject(AMServer, ObjectId.createFromHexString(serverId));
|
||||||
const cards = useMemo(() => query.sorted("lastUsed", true), [query]);
|
const cards = useQuery({
|
||||||
|
type: AMCard,
|
||||||
const [selectedCard, setSelectedCard] = useState<AMCard>(null);
|
query: q => q.sorted("lastUsed", true)
|
||||||
const [cardInCompleted, setCardInCompleted] = useState<boolean>(null);
|
});
|
||||||
|
|
||||||
const unselectedCards = useMemo(() => {
|
|
||||||
// protect against selected card being removed
|
|
||||||
switch (selectedCard?.isValid()) {
|
|
||||||
case true:
|
|
||||||
return cards.filter(x => !x._id.equals(selectedCard._id));
|
|
||||||
|
|
||||||
// clear out invalid
|
|
||||||
case false:
|
|
||||||
setSelectedCard(null);
|
|
||||||
return cards;
|
|
||||||
|
|
||||||
default:
|
|
||||||
return cards;
|
|
||||||
}
|
|
||||||
}, [cards, selectedCard]);
|
|
||||||
|
|
||||||
// server state
|
// server state
|
||||||
const [serverRefreshing, setServerRefreshing] = useState<boolean>(false);
|
const [serverRefreshing, setServerRefreshing] = useState<boolean>(false);
|
||||||
const [serverReloadCounter, setServerReloadCounter] = useState<number>(0);
|
const [serverReloadCounter, setServerReloadCounter] = useState<number>(0);
|
||||||
const [connectionTestResult, setConnectionTestResult] = useState<boolean | null>(null);
|
const [serverConnectionSuccess, setServerConnectionSuccess] = useState<boolean | null>(null);
|
||||||
|
|
||||||
let {serverId} = route.params;
|
const [userSelectedCard, setUserSelectedCard] = useState<AMCard>(null);
|
||||||
const server = useObject(AMServer, ObjectId.createFromHexString(serverId));
|
const [cardInCompleted, setCardInCompleted] = useState<boolean>(null);
|
||||||
|
|
||||||
|
const disableCardIn = useMemo(() => cardInCompleted !== null || !serverConnectionSuccess, [cardInCompleted, serverConnectionSuccess]);
|
||||||
|
const serverDefaultCard = useMemo(() => {
|
||||||
|
if (server?.isValid() !== true || !server.gameId) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return cards.find(x => x.lastUsedGameId === server.gameId);
|
||||||
|
}, [cards, server?.gameId]);
|
||||||
|
|
||||||
|
const selectedCard = useMemo(() => [userSelectedCard, serverDefaultCard, cards.at(0)].find(x => x?.isValid() == true) ?? null, [userSelectedCard, serverDefaultCard, cards]);
|
||||||
|
const unselectedCards = useMemo(() => cards.filter(x => x.isValid() === true && x._id.toHexString() !== selectedCard?._id.toHexString()), [cards, selectedCard]);
|
||||||
|
|
||||||
useLayoutEffect(() => {
|
useLayoutEffect(() => {
|
||||||
if (!server) {
|
if (server?.isValid() !== true) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
navigation.setOptions({
|
navigation.setOptions({
|
||||||
title: server.serverName,
|
title: server.serverName,
|
||||||
headerRight: () => (
|
headerRight: () => (
|
||||||
<TouchableOpacity onPress={() => Share.share({url: `http://card.ppc.moe/#/servers/add?address=${encodeURIComponent(server.serverAddress)}`})}>
|
<TouchableOpacity onPress={() => Share.share({url: server.shareUrl})}>
|
||||||
<ShareIcon/>
|
<ShareIcon color={theme.colors.primary}/>
|
||||||
</TouchableOpacity>
|
</TouchableOpacity>
|
||||||
)
|
)
|
||||||
});
|
});
|
||||||
}, [navigation, server]);
|
}, [navigation, server]);
|
||||||
|
|
||||||
// set selected card (once)
|
// invalid card/server selection
|
||||||
useEffect(() => {
|
|
||||||
if (selectedCard?.isValid() === true) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
setSelectedCard(cards.length ? cards[0] : null);
|
|
||||||
}, [realm]);
|
|
||||||
|
|
||||||
// refresh server info (server change or reload request)
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (server?.isValid() !== true) {
|
if (server?.isValid() !== true) {
|
||||||
navigation.goBack();
|
navigation.goBack();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}, [server]);
|
||||||
|
|
||||||
// don't show error on invalid server, just means it was deleted
|
useEffect(() => {
|
||||||
if (!server) {
|
if (userSelectedCard?.isValid() === false) {
|
||||||
Alert.alert("Server not found", "The server you are looking for does not exist.");
|
setUserSelectedCard(null);
|
||||||
}
|
}
|
||||||
|
}, [userSelectedCard]);
|
||||||
|
|
||||||
|
// handle auto-refresh
|
||||||
|
useEffect(() => {
|
||||||
|
if (!serverConnectionSuccess || server?.isValid() !== true) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const metrics = server as AMServerInstanceMetrics;
|
||||||
|
if (metrics.sessionUptime > 0 && metrics.timeSinceLastPoll === null) {
|
||||||
|
const timeout = setTimeout(() => setServerReloadCounter(serverReloadCounter + 1), 5000);
|
||||||
|
return () => clearTimeout(timeout);
|
||||||
|
}
|
||||||
|
}, [serverConnectionSuccess, server]);
|
||||||
|
|
||||||
|
// server info update
|
||||||
|
useEffect(() => {
|
||||||
const abortController = new AbortController();
|
const abortController = new AbortController();
|
||||||
fetchServerInfo(server.serverAddress, abortController.signal).then(r => {
|
fetchServerInfo(server.serverAddress, abortController.signal).then(r => {
|
||||||
if (r) {
|
if (!r) {
|
||||||
setConnectionTestResult(true);
|
setServerConnectionSuccess(false);
|
||||||
realm.write(() => {
|
return;
|
||||||
server.lastUsed = new Date();
|
|
||||||
Object.keys(r).forEach(x => server[x] = r[x]);
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
setConnectionTestResult(false);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
setServerConnectionSuccess(true);
|
||||||
|
|
||||||
|
realm.write(() => {
|
||||||
|
server.lastUsed = new Date();
|
||||||
|
Object.keys(r).forEach(x => server[x] = r[x]);
|
||||||
|
});
|
||||||
}).catch(() => {
|
}).catch(() => {
|
||||||
setConnectionTestResult(false);
|
setServerConnectionSuccess(false);
|
||||||
}).finally(() => {
|
}).finally(() => {
|
||||||
setServerRefreshing(false);
|
setServerRefreshing(false);
|
||||||
});
|
});
|
||||||
|
|
||||||
setTimeout(() => abortController.abort(), 5000);
|
const timeout = setTimeout(() => abortController.abort(), 5000);
|
||||||
return () => abortController.abort();
|
return () => {
|
||||||
|
clearTimeout(timeout);
|
||||||
|
abortController.abort();
|
||||||
|
}
|
||||||
}, [serverId, serverReloadCounter, realm]);
|
}, [serverId, serverReloadCounter, realm]);
|
||||||
|
|
||||||
// card in callback
|
|
||||||
const cardInCallback = useCallback(() => {
|
const cardInCallback = useCallback(() => {
|
||||||
setCardInCompleted(false);
|
setCardInCompleted(false);
|
||||||
|
|
||||||
const abortController = new AbortController();
|
const abortController = new AbortController();
|
||||||
performCardIn(server.serverAddress, {cardId: selectedCard.code}, abortController.signal).then(async r => {
|
performCardIn(server, {cardId: selectedCard.code}, abortController.signal).then(async r => {
|
||||||
const now = new Date();
|
const now = new Date();
|
||||||
|
|
||||||
setCardInCompleted(r);
|
|
||||||
realm.write(() => {
|
realm.write(() => {
|
||||||
server.lastUsed = now;
|
server.lastUsed = now;
|
||||||
selectedCard.lastUsed = now;
|
selectedCard.lastUsed = now;
|
||||||
|
selectedCard.lastUsedGameId = server.gameId;
|
||||||
});
|
});
|
||||||
|
|
||||||
setSelectedCard(selectedCard);
|
setCardInCompleted(r);
|
||||||
}).catch(() => {
|
}).catch(() => {
|
||||||
setCardInCompleted(false);
|
setCardInCompleted(false);
|
||||||
}).finally(() => {
|
}).finally(() => {
|
||||||
@@ -132,9 +168,16 @@ export default function ServerCardIn({route, navigation}) {
|
|||||||
return () => abortController.abort();
|
return () => abortController.abort();
|
||||||
}, [server, selectedCard]);
|
}, [server, selectedCard]);
|
||||||
|
|
||||||
const disableCardIn = useMemo(() => cardInCompleted !== null || !connectionTestResult, [cardInCompleted, connectionTestResult]);
|
const performRefresh = () => {
|
||||||
|
setServerRefreshing(true);
|
||||||
|
setServerReloadCounter(serverReloadCounter + 1);
|
||||||
|
};
|
||||||
|
|
||||||
if (connectionTestResult === null) {
|
if (server?.isValid() !== true) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (serverConnectionSuccess === null) {
|
||||||
return (
|
return (
|
||||||
<View style={{flex: 1, justifyContent: "center", alignItems: "center", gap: 10}}>
|
<View style={{flex: 1, justifyContent: "center", alignItems: "center", gap: 10}}>
|
||||||
<ActivityIndicator animating/>
|
<ActivityIndicator animating/>
|
||||||
@@ -144,94 +187,126 @@ export default function ServerCardIn({route, navigation}) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<ScrollView contentInsetAdjustmentBehavior="automatic"
|
<ScrollView contentInsetAdjustmentBehavior="automatic" refreshControl={<RefreshControl refreshing={serverRefreshing} onRefresh={performRefresh}/>}>
|
||||||
contentContainerStyle={{...styles.container, paddingTop: connectionTestResult ? 0 : 10}}
|
<View style={styles.container}>
|
||||||
refreshControl={<RefreshControl refreshing={serverRefreshing}
|
<StatusAlert server={server} connectionEstablished={serverConnectionSuccess}/>
|
||||||
tintColor={theme.colors.onSurface}
|
<View style={styles.section}>
|
||||||
onRefresh={() => {
|
<Title style={styles.sectionTitle}>{server.gameId ? `Play ${getGameName(server.gameId)}` : "Selected Card"}</Title>
|
||||||
setServerRefreshing(true);
|
{selectedCard ? <>
|
||||||
setServerReloadCounter(serverReloadCounter + 1);
|
<AimeCard actionDisabled
|
||||||
}}/>}>
|
card={selectedCard}
|
||||||
{!connectionTestResult && (
|
actionIcon={<CircleCheck color="green" size={24}/>}>
|
||||||
<Card mode="outlined" style={{borderColor: theme.colors.secondary}}>
|
{serverDefaultCard?.code == selectedCard.code && <DefaultCardDetail/>}
|
||||||
<Card.Content>
|
</AimeCard>
|
||||||
<View style={{alignItems: "center", flexDirection: "row", gap: 10}}>
|
<Button textColor="white"
|
||||||
<WifiOff color={theme.colors.secondary} size={24}/>
|
onPress={cardInCallback}
|
||||||
<Title style={{color: theme.colors.secondary}}>Connection Failed</Title>
|
disabled={disableCardIn}
|
||||||
</View>
|
contentStyle={{flexDirection: 'row-reverse'}}
|
||||||
<Paragraph>
|
mode={serverConnectionSuccess ? "contained" : "outlined"}
|
||||||
There was an issue connecting to <Paragraph
|
buttonColor={cardInCompleted === false ? "red" : "green"}
|
||||||
style={{fontFamily: "JetBrainsMono_400Regular"}}>{server.serverAddress}</Paragraph>.
|
icon={() => <Nfc color={disableCardIn ? "gray" : "white"} size={18}/>}>
|
||||||
Check the network connection and try again.
|
{cardInCompleted === null ? "Card In" : cardInCompleted ? "Card In Success" : "Failed"}
|
||||||
</Paragraph>
|
</Button>
|
||||||
</Card.Content>
|
</>
|
||||||
</Card>
|
: <View style={styles.emptyPlaceholderView}>
|
||||||
)}
|
<EmptyStatePlaceholder message="No card selected" icon={<CreditCard color="gray"/>}/>
|
||||||
|
<Button mode="text"
|
||||||
|
icon={() => <Plus color={theme.colors.primary} size={18}/>}
|
||||||
|
onPress={() => navigation.navigate('add-card')}>
|
||||||
|
Add card
|
||||||
|
</Button>
|
||||||
|
</View>}
|
||||||
|
</View>
|
||||||
|
|
||||||
<View style={styles.section}>
|
{cards.length > 0 && (
|
||||||
<Title style={{fontWeight: "bold"}}>Selected Card</Title>
|
<View style={styles.section}>
|
||||||
{selectedCard ? <>
|
<Title style={styles.sectionTitle}>Additional Cards</Title>
|
||||||
<AimeCard actionDisabled
|
<Button mode="contained"
|
||||||
card={selectedCard}
|
textColor="white"
|
||||||
actionIcon={<CircleCheck color="green" size={24}/>}/>
|
buttonColor={theme.colors.tertiary}
|
||||||
<Button textColor="white"
|
icon={() => <Plus color="white" size={18}/>}
|
||||||
onPress={cardInCallback}
|
|
||||||
disabled={disableCardIn}
|
|
||||||
contentStyle={{flexDirection: 'row-reverse'}}
|
contentStyle={{flexDirection: 'row-reverse'}}
|
||||||
mode={connectionTestResult ? "contained" : "outlined"}
|
|
||||||
buttonColor={cardInCompleted === false ? "red" : "green"}
|
|
||||||
icon={() => <Nfc color={disableCardIn ? "gray" : "white"} size={18}/>}>
|
|
||||||
{cardInCompleted === null ? "Card In" : cardInCompleted ? "Card In Success" : "Failed"}
|
|
||||||
</Button>
|
|
||||||
</>
|
|
||||||
: <View style={styles.emptyPlaceholderView}>
|
|
||||||
<EmptyStatePlaceholder message="No card selected" icon={<CreditCard color="gray"/>}/>
|
|
||||||
<Button mode="text"
|
|
||||||
icon={() => <Plus color={theme.colors.primary} size={18}/>}
|
|
||||||
onPress={() => navigation.navigate('add-card')}>
|
onPress={() => navigation.navigate('add-card')}>
|
||||||
Add card
|
Add card
|
||||||
</Button>
|
</Button>
|
||||||
</View>}
|
|
||||||
|
{unselectedCards.length
|
||||||
|
? unselectedCards.map((card: AMCard) => (
|
||||||
|
<AimeCard card={card}
|
||||||
|
key={card._id.toHexString()}
|
||||||
|
actionIcon={<CircleDot color={theme.colors.tertiary} size={24}/>}
|
||||||
|
onPress={() => setUserSelectedCard(card)}>
|
||||||
|
{serverDefaultCard?.code == card.code && <DefaultCardDetail/>}
|
||||||
|
</AimeCard>))
|
||||||
|
: <View style={styles.emptyPlaceholderView}>
|
||||||
|
<EmptyStatePlaceholder message="No additional cards"
|
||||||
|
icon={<SmartphoneNfcIcon color="gray"/>}/>
|
||||||
|
</View>}
|
||||||
|
</View>)}
|
||||||
</View>
|
</View>
|
||||||
|
|
||||||
{cards.length > 0 && (
|
|
||||||
<View style={styles.section}>
|
|
||||||
<Title style={{fontWeight: "bold"}}>Additional Cards</Title>
|
|
||||||
<Button mode="contained"
|
|
||||||
textColor="white"
|
|
||||||
buttonColor={theme.colors.tertiary}
|
|
||||||
icon={() => <Plus color="white" size={18}/>}
|
|
||||||
contentStyle={{flexDirection: 'row-reverse'}}
|
|
||||||
onPress={() => navigation.navigate('add-card')}>
|
|
||||||
Add card
|
|
||||||
</Button>
|
|
||||||
|
|
||||||
{unselectedCards.length
|
|
||||||
? unselectedCards.map(card => (
|
|
||||||
<AimeCard card={card}
|
|
||||||
key={card._id.toHexString()}
|
|
||||||
actionIcon={<CircleDot color={theme.colors.tertiary} size={24}/>}
|
|
||||||
onPress={() => setSelectedCard(card)}/>))
|
|
||||||
: <View style={styles.emptyPlaceholderView}>
|
|
||||||
<EmptyStatePlaceholder message="No additional cards"
|
|
||||||
icon={<SmartphoneNfcIcon color="gray"/>}/>
|
|
||||||
</View>}
|
|
||||||
</View>)}
|
|
||||||
</ScrollView>
|
</ScrollView>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function StatusAlert(props: {
|
||||||
|
server: AMServerConnectionInfo & Partial<AMServerInstanceMetrics>,
|
||||||
|
connectionEstablished: boolean
|
||||||
|
}) {
|
||||||
|
if (!props.connectionEstablished) {
|
||||||
|
return (
|
||||||
|
<AMUIStatusAlert color="red" icon={WifiOff} title="Connection Failed">
|
||||||
|
<Paragraph>
|
||||||
|
There was an issue connecting to <Paragraph
|
||||||
|
style={{fontFamily: "JetBrainsMono_400Regular"}}>{props.server.serverAddress}</Paragraph>.
|
||||||
|
Check the network connection and try again.
|
||||||
|
</Paragraph>
|
||||||
|
</AMUIStatusAlert>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
// don't show if undefined (some older server versions)
|
||||||
|
if (props.server.timeSinceLastPoll === null) {
|
||||||
|
return (
|
||||||
|
<AMUIStatusAlert color="#822eff" icon={TvMinimalPlay} title="Server Startup">
|
||||||
|
<Paragraph>
|
||||||
|
Scanning a card below may not have any effect until the service has fully started up.
|
||||||
|
</Paragraph>
|
||||||
|
</AMUIStatusAlert>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
function DefaultCardDetail() {
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
{/* spacer */}
|
||||||
|
<View></View>
|
||||||
|
<AMUICardDetail>
|
||||||
|
<Star size={18} color="goldenrod"/>
|
||||||
|
<Text style={{color: "goldenrod"}}>Previously used with this game</Text>
|
||||||
|
</AMUICardDetail>
|
||||||
|
</>
|
||||||
|
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
const styles = StyleSheet.create({
|
const styles = StyleSheet.create({
|
||||||
section: {
|
section: {
|
||||||
gap: 10
|
gap: 10
|
||||||
},
|
},
|
||||||
|
sectionTitle: {
|
||||||
|
fontWeight: "bold"
|
||||||
|
},
|
||||||
emptyPlaceholderView: {
|
emptyPlaceholderView: {
|
||||||
marginVertical: 30,
|
marginVertical: 30,
|
||||||
gap: 15
|
gap: 15
|
||||||
},
|
},
|
||||||
container: {
|
container: {
|
||||||
|
paddingTop: Platform.OS === 'android' ? 120 : 0,
|
||||||
paddingHorizontal: 16,
|
paddingHorizontal: 16,
|
||||||
paddingBottom: 25,
|
paddingBottom: 80,
|
||||||
gap: 20
|
gap: 20
|
||||||
}
|
}
|
||||||
})
|
});
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import {Button, Paragraph, useTheme} from "react-native-paper";
|
import {Button, Paragraph, useTheme} from "react-native-paper";
|
||||||
import {Globe, History, Plus, ServerOff} from "lucide-react-native";
|
|
||||||
import {FlatList, NativeSyntheticEvent, Text, View} from "react-native";
|
|
||||||
import React, {useCallback, useLayoutEffect, useMemo, useState} from "react";
|
import React, {useCallback, useLayoutEffect, useMemo, useState} from "react";
|
||||||
|
import {Globe, History, Joystick, Plus, ServerOff} from "lucide-react-native";
|
||||||
|
import {FlatList, NativeSyntheticEvent, Platform, ScrollView, Share, View} from "react-native";
|
||||||
import ContextMenu, {ContextMenuOnPressNativeEvent} from "react-native-context-menu-view";
|
import ContextMenu, {ContextMenuOnPressNativeEvent} from "react-native-context-menu-view";
|
||||||
import {EmptyStatePlaceholder} from "../components/EmptyStatePlaceholder";
|
import {EmptyStatePlaceholder} from "../components/EmptyStatePlaceholder";
|
||||||
import {AMUICard, AMUICardDetail} from "../components/AMUICard";
|
import {AMUICard, AMUICardDetail} from "../components/AMUICard";
|
||||||
@@ -9,11 +9,14 @@ import TimeAgo from "../components/TimeAgo";
|
|||||||
|
|
||||||
import RealmContext from "../models/RealmContext";
|
import RealmContext from "../models/RealmContext";
|
||||||
import {AMServer} from "../models/AMServer";
|
import {AMServer} from "../models/AMServer";
|
||||||
|
import {getGameName} from "amnet-shared";
|
||||||
|
|
||||||
const {useRealm, useQuery} = RealmContext;
|
const {useRealm, useQuery} = RealmContext;
|
||||||
|
|
||||||
function ServerCard(props: { server: AMServer, navigation: any }) {
|
function ServerCard(props: { server: AMServer, navigation: any }) {
|
||||||
const realm = useRealm();
|
const realm = useRealm();
|
||||||
|
const gameName = useMemo(() => getGameName(props.server.gameId), [props.server.gameId]);
|
||||||
|
|
||||||
const pageCallback = useCallback(() => {
|
const pageCallback = useCallback(() => {
|
||||||
props.navigation.navigate('server', {
|
props.navigation.navigate('server', {
|
||||||
serverId: props.server._id.toHexString()
|
serverId: props.server._id.toHexString()
|
||||||
@@ -21,31 +24,46 @@ function ServerCard(props: { server: AMServer, navigation: any }) {
|
|||||||
}, [props.navigation, props.server]);
|
}, [props.navigation, props.server]);
|
||||||
|
|
||||||
const pageContextMenuCallback = useCallback((action: NativeSyntheticEvent<ContextMenuOnPressNativeEvent>) => {
|
const pageContextMenuCallback = useCallback((action: NativeSyntheticEvent<ContextMenuOnPressNativeEvent>) => {
|
||||||
switch (action.nativeEvent.index) {
|
switch (action.nativeEvent.indexPath.toString()) {
|
||||||
case 0:
|
case "0":
|
||||||
pageCallback();
|
pageCallback();
|
||||||
break;
|
return;
|
||||||
|
|
||||||
case 1:
|
case "1,0":
|
||||||
realm.write(() => {
|
Share.share({url: props.server.shareUrl});
|
||||||
realm.delete(props.server);
|
return;
|
||||||
});
|
|
||||||
break;
|
case "1,1":
|
||||||
|
realm.write(() => realm.delete(props.server));
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
}, [realm, props.server]);
|
}, [realm, props.server]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<ContextMenu onPress={pageContextMenuCallback} actions={[
|
<ContextMenu onPress={pageContextMenuCallback} actions={[
|
||||||
{title: `Select ${props.server.serverName}`, systemIcon: "chevron.right"},
|
{title: `Select ${props.server.serverName}`, systemIcon: "chevron.right"},
|
||||||
{title: "Delete Server", systemIcon: "trash", destructive: true}
|
{
|
||||||
|
title: Platform.OS === "ios" ? "" : "Additional Actions",
|
||||||
|
inlineChildren: true,
|
||||||
|
actions: [
|
||||||
|
{title: "Share Server", systemIcon: "square.and.arrow.up"},
|
||||||
|
{title: "Delete Server", systemIcon: "trash", destructive: true}
|
||||||
|
]
|
||||||
|
}
|
||||||
]}>
|
]}>
|
||||||
<AMUICard title={props.server.serverName} onPress={pageCallback}>
|
<AMUICard title={props.server.serverName} onPress={pageCallback}>
|
||||||
<AMUICardDetail>
|
<AMUICardDetail>
|
||||||
<Globe size={18} color="gray"/>
|
<Globe size={18} color="gray"/>
|
||||||
<Text style={{color: "gray", fontFamily: "JetBrainsMono_400Regular"}}>
|
<Paragraph style={{color: "gray", fontFamily: "JetBrainsMono_400Regular"}}>
|
||||||
{props.server.serverAddress}
|
{props.server.serverAddress}
|
||||||
</Text>
|
</Paragraph>
|
||||||
</AMUICardDetail>
|
</AMUICardDetail>
|
||||||
|
{gameName && (
|
||||||
|
<AMUICardDetail>
|
||||||
|
<Joystick size={18} color="gray"/>
|
||||||
|
<Paragraph style={{color: "gray"}}>{gameName}</Paragraph>
|
||||||
|
</AMUICardDetail>
|
||||||
|
)}
|
||||||
<AMUICardDetail>
|
<AMUICardDetail>
|
||||||
<History size={18} color="gray"/>
|
<History size={18} color="gray"/>
|
||||||
{props.server.lastUsed
|
{props.server.lastUsed
|
||||||
@@ -80,6 +98,8 @@ export default function ServerManager({navigation}) {
|
|||||||
value: searchQuery,
|
value: searchQuery,
|
||||||
autoCorrect: "none",
|
autoCorrect: "none",
|
||||||
autoCapitalize: "none",
|
autoCapitalize: "none",
|
||||||
|
textColor: theme.colors.onSurface,
|
||||||
|
headerIconColor: theme.colors.primary,
|
||||||
onChangeText: e => setSearchQuery(e.nativeEvent.text)
|
onChangeText: e => setSearchQuery(e.nativeEvent.text)
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
@@ -102,8 +122,14 @@ export default function ServerManager({navigation}) {
|
|||||||
data={servers}
|
data={servers}
|
||||||
keyExtractor={i => i._id.toString()}
|
keyExtractor={i => i._id.toString()}
|
||||||
style={{padding: 16, paddingTop: 24}}
|
style={{padding: 16, paddingTop: 24}}
|
||||||
contentContainerStyle={{paddingBottom: 40}}
|
contentContainerStyle={{paddingBottom: 80}}
|
||||||
|
renderItem={i => <ServerCard navigation={navigation} server={i.item}/>}
|
||||||
ItemSeparatorComponent={() => <View style={{height: 15}}/>}
|
ItemSeparatorComponent={() => <View style={{height: 15}}/>}
|
||||||
renderItem={i => <ServerCard navigation={navigation} server={i.item}/>}/>
|
ListFooterComponent={() => Platform.OS === "ios" ? null : (
|
||||||
|
<Button mode="text"
|
||||||
|
onPress={() => navigation.navigate('add-server')}
|
||||||
|
style={{marginTop: 15, marginBottom: 25}}
|
||||||
|
icon={() => <Plus size={18} color={theme.colors.primary}/>}>Add Server</Button>
|
||||||
|
)}/>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
Binary file not shown.
|
After Width: | Height: | Size: 18 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 1.5 MiB |
@@ -0,0 +1,22 @@
|
|||||||
|
import {View} from "react-native";
|
||||||
|
import React, {ReactNode} from "react";
|
||||||
|
import {Card, Title} from "react-native-paper";
|
||||||
|
import {LucideIcon, LucideProps} from "lucide-react-native";
|
||||||
|
|
||||||
|
export function AMUIStatusAlert(props: { title: string, color: string, icon: LucideIcon, children?: ReactNode }) {
|
||||||
|
return (
|
||||||
|
<Card mode="outlined" style={{borderColor: props.color}}>
|
||||||
|
<Card.Content>
|
||||||
|
<View style={{alignItems: "center", flexDirection: "row", gap: 10}}>
|
||||||
|
{renderIconWithProps(props.icon, {size: 22, color: props.color})}
|
||||||
|
<Title style={{color: props.color}}>{props.title}</Title>
|
||||||
|
</View>
|
||||||
|
{props.children}
|
||||||
|
</Card.Content>
|
||||||
|
</Card>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function renderIconWithProps(icon: LucideIcon, additionalProps: Partial<LucideProps>) {
|
||||||
|
return React.cloneElement(React.createElement(icon), additionalProps);
|
||||||
|
}
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
import {useMemo} from "react";
|
import {ReactNode, useMemo} from "react";
|
||||||
import {Text} from "react-native";
|
import {Text} from "react-native";
|
||||||
import {CreditCard, History} from "lucide-react-native";
|
import {CreditCard, History} from "lucide-react-native";
|
||||||
|
|
||||||
@@ -8,6 +8,7 @@ import {AMUICard, AMUICardActionProps, AMUICardDetail} from "./AMUICard";
|
|||||||
|
|
||||||
export interface AimeCardProps extends AMUICardActionProps {
|
export interface AimeCardProps extends AMUICardActionProps {
|
||||||
card: AMCardData,
|
card: AMCardData,
|
||||||
|
children?: ReactNode,
|
||||||
showFullCardNumber?: boolean
|
showFullCardNumber?: boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -29,6 +30,7 @@ export function AimeCard(props: AimeCardProps) {
|
|||||||
<TimeAgo style={{color: "gray"}} dateTo={props.card.lastUsed}/>
|
<TimeAgo style={{color: "gray"}} dateTo={props.card.lastUsed}/>
|
||||||
</AMUICardDetail>
|
</AMUICardDetail>
|
||||||
)}
|
)}
|
||||||
|
{props.children}
|
||||||
</AMUICard>
|
</AMUICard>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -15,14 +15,15 @@ export class AMCard extends Realm.Object<AMCard> implements AMCardData {
|
|||||||
name: string;
|
name: string;
|
||||||
|
|
||||||
added: Date;
|
added: Date;
|
||||||
lastUsed?: Date | null;
|
lastUsed?: Date;
|
||||||
|
lastUsedGameId?: string | null;
|
||||||
|
|
||||||
static generate(code: string, name: string) {
|
static generate(code: string, name: string) {
|
||||||
return {
|
return {
|
||||||
_id: new Realm.BSON.ObjectId(),
|
_id: new Realm.BSON.ObjectId(),
|
||||||
|
added: new Date(),
|
||||||
code: code,
|
code: code,
|
||||||
name: name,
|
name: name
|
||||||
added: new Date()
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -35,7 +36,8 @@ export class AMCard extends Realm.Object<AMCard> implements AMCardData {
|
|||||||
code: "string",
|
code: "string",
|
||||||
name: {type: "string", default: "New Card"},
|
name: {type: "string", default: "New Card"},
|
||||||
added: {type: "date", default: new Date()},
|
added: {type: "date", default: new Date()},
|
||||||
lastUsed: {type: "date", optional: true}
|
lastUsed: {type: "date", optional: true},
|
||||||
|
lastUsedGameId: {type: "string", optional: true}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
@@ -1,25 +1,32 @@
|
|||||||
import {Realm} from 'realm';
|
import {Realm} from 'realm';
|
||||||
import {AMServerInfoResponse} from "amnet-shared";
|
import {AMServerConnectionInfo, AMServerMetadata} from "amnet-shared";
|
||||||
|
|
||||||
export class AMServer extends Realm.Object<AMServer> implements AMServerInfoResponse {
|
export class AMServer extends Realm.Object<AMServer> implements AMServerConnectionInfo, AMServerMetadata {
|
||||||
_id!: Realm.BSON.ObjectId;
|
_id!: Realm.BSON.ObjectId;
|
||||||
|
|
||||||
|
apiVersion: number;
|
||||||
|
gameId?: string;
|
||||||
|
|
||||||
serverName: string;
|
serverName: string;
|
||||||
serverAddress: string;
|
serverAddress: string;
|
||||||
|
|
||||||
lastUsed: Date;
|
lastUsed: Date;
|
||||||
createdAt: Date;
|
createdAt: Date;
|
||||||
apiVersion: number;
|
|
||||||
|
get shareUrl() {
|
||||||
|
return `http://card.ppc.moe/#/servers/add?address=${encodeURIComponent(this.serverAddress)}`;
|
||||||
|
}
|
||||||
|
|
||||||
static generate(serverAddress: string) {
|
static generate(serverAddress: string) {
|
||||||
return {
|
return {
|
||||||
_id: new Realm.BSON.ObjectId(),
|
_id: new Realm.BSON.ObjectId(),
|
||||||
|
|
||||||
apiVersion: 0,
|
apiVersion: 0,
|
||||||
serverAddress,
|
|
||||||
serverName: "New Server",
|
serverName: "New Server",
|
||||||
|
serverAddress: serverAddress,
|
||||||
|
|
||||||
createdAt: new Date(),
|
createdAt: new Date(),
|
||||||
|
|
||||||
lastUsed: null
|
lastUsed: null
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -31,6 +38,7 @@ export class AMServer extends Realm.Object<AMServer> implements AMServerInfoResp
|
|||||||
properties: {
|
properties: {
|
||||||
_id: "objectId",
|
_id: "objectId",
|
||||||
apiVersion: {type: "int", default: 0},
|
apiVersion: {type: "int", default: 0},
|
||||||
|
gameId: {type: "string", optional: true},
|
||||||
serverAddress: "string",
|
serverAddress: "string",
|
||||||
serverName: {type: "string", default: "New Server"},
|
serverName: {type: "string", default: "New Server"},
|
||||||
createdAt: {type: "date", default: new Date()},
|
createdAt: {type: "date", default: new Date()},
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ import {AMServer} from "./AMServer";
|
|||||||
|
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
export default createRealmContext({
|
export default createRealmContext({
|
||||||
schemaVersion: 1,
|
schemaVersion: 3,
|
||||||
schema: [AMCard, AMServer],
|
schema: [AMCard, AMServer],
|
||||||
deleteRealmIfMigrationNeeded: false
|
deleteRealmIfMigrationNeeded: false
|
||||||
});
|
});
|
||||||
Generated
+2169
-1467
File diff suppressed because it is too large
Load Diff
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "amnet-native",
|
"name": "amnet-native",
|
||||||
"version": "1.0.0",
|
"version": "1.1.0",
|
||||||
"private": true,
|
"private": true,
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"android": "expo run:android",
|
"android": "expo run:android",
|
||||||
@@ -17,26 +17,27 @@
|
|||||||
"@types/react": "~18.2.79",
|
"@types/react": "~18.2.79",
|
||||||
"amnet-shared": "file:../amnet-shared",
|
"amnet-shared": "file:../amnet-shared",
|
||||||
"date-fns": "^3.6.0",
|
"date-fns": "^3.6.0",
|
||||||
"expo": "^51.0.0",
|
"expo": "^51.0.38",
|
||||||
"expo-blur": "~13.0.2",
|
"expo-blur": "~13.0.2",
|
||||||
|
"expo-build-properties": "~0.12.5",
|
||||||
"expo-clipboard": "~6.0.3",
|
"expo-clipboard": "~6.0.3",
|
||||||
"expo-constants": "~16.0.2",
|
"expo-constants": "~16.0.2",
|
||||||
"expo-dev-client": "~4.0.10",
|
"expo-dev-client": "~4.0.29",
|
||||||
"expo-linking": "~6.3.1",
|
"expo-linking": "~6.3.1",
|
||||||
"expo-status-bar": "~1.12.1",
|
"expo-status-bar": "~1.12.1",
|
||||||
"expo-system-ui": "~3.0.7",
|
"expo-system-ui": "~3.0.7",
|
||||||
"lucide-react-native": "^0.427.0",
|
"lucide-react-native": "^0.427.0",
|
||||||
"patch-package": "^8.0.0",
|
"patch-package": "^8.0.0",
|
||||||
"react": "18.2.0",
|
"react": "18.2.0",
|
||||||
"react-native": "0.74.1",
|
"react-native": "0.74.5",
|
||||||
"react-native-context-menu-view": "^1.16.0",
|
"react-native-context-menu-view": "^1.16.0",
|
||||||
"react-native-gesture-handler": "^2.18.1",
|
"react-native-gesture-handler": "~2.16.1",
|
||||||
"react-native-get-random-values": "^1.11.0",
|
"react-native-get-random-values": "^1.11.0",
|
||||||
"react-native-paper": "^5.12.5",
|
"react-native-paper": "^5.12.5",
|
||||||
"react-native-reanimated": "^3.15.0",
|
"react-native-reanimated": "~3.10.1",
|
||||||
"react-native-safe-area-context": "4.10.5",
|
"react-native-safe-area-context": "4.10.5",
|
||||||
"react-native-screens": "3.31.1",
|
"react-native-screens": "3.31.1",
|
||||||
"react-native-svg": "^15.5.0",
|
"react-native-svg": "15.2.0",
|
||||||
"react-native-url-polyfill": "^2.0.0",
|
"react-native-url-polyfill": "^2.0.0",
|
||||||
"realm": "^12.12.1",
|
"realm": "^12.12.1",
|
||||||
"typescript": "~5.3.3"
|
"typescript": "~5.3.3"
|
||||||
|
|||||||
Vendored
+36
-25
@@ -2,28 +2,51 @@
|
|||||||
* The latest version of the AMNet API this library supports
|
* The latest version of the AMNet API this library supports
|
||||||
*/
|
*/
|
||||||
declare const AMNET_API_VERSION = 1;
|
declare const AMNET_API_VERSION = 1;
|
||||||
|
/**
|
||||||
|
* The minimum version of the AMNet API this library supports
|
||||||
|
*/
|
||||||
|
declare const MIN_AMNET_API_VERSION = 1;
|
||||||
interface AMCardInRequest {
|
interface AMCardInRequest {
|
||||||
cardId: string;
|
cardId: string;
|
||||||
}
|
}
|
||||||
interface AMServerInfoResponse {
|
/**
|
||||||
serverAddress: string;
|
* Server connection info (api version, address, etc.)
|
||||||
serverName: string;
|
*/
|
||||||
|
interface AMServerConnectionInfo {
|
||||||
apiVersion: number;
|
apiVersion: number;
|
||||||
gameId: string;
|
serverAddress: string;
|
||||||
}
|
}
|
||||||
|
/**
|
||||||
|
* User-definable server metadata
|
||||||
|
*/
|
||||||
|
interface AMServerMetadata {
|
||||||
|
serverName: string;
|
||||||
|
gameId?: string | null | undefined;
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* Metrics about the current game session
|
||||||
|
*/
|
||||||
|
interface AMServerInstanceMetrics {
|
||||||
|
timeSinceLastPoll?: number;
|
||||||
|
sessionUptime?: number;
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* The response from an AMNet server's info endpoint
|
||||||
|
*/
|
||||||
|
type AMServerInfo = AMServerMetadata & AMServerConnectionInfo & AMServerInstanceMetrics;
|
||||||
/**
|
/**
|
||||||
* Performs the card-in request to the specified server
|
* Performs the card-in request to the specified server
|
||||||
* @param serverAddress The origin address of the server to perform the request against
|
* @param server The server to perform the request against
|
||||||
* @param request The card-in request to send
|
* @param request The card-in request to send
|
||||||
* @param signal The abort signal to use for the request
|
* @param signal The abort signal to use for the request
|
||||||
*/
|
*/
|
||||||
declare function performCardIn(serverAddress: string, request: AMCardInRequest, signal: AbortSignal): Promise<boolean>;
|
declare function performCardIn(server: AMServerConnectionInfo, request: AMCardInRequest, signal: AbortSignal): Promise<boolean>;
|
||||||
/**
|
/**
|
||||||
* Attempts to connect to the specified server and fetch its info
|
* Attempts to connect to the specified server and fetch its info
|
||||||
* @param serverAddress The origin address of the server to connect to
|
* @param serverAddress The origin address of the server to connect to
|
||||||
* @param abort The abort signal to use for the request (for cancellation)
|
* @param abort The abort signal to use for the request (for cancellation)
|
||||||
*/
|
*/
|
||||||
declare function fetchServerInfo(serverAddress: string, abort: AbortSignal): Promise<AMServerInfoResponse | null>;
|
declare function fetchServerInfo(serverAddress: string, abort: AbortSignal): Promise<AMServerInfo | null>;
|
||||||
|
|
||||||
declare const CARD_ID_REGEX: RegExp;
|
declare const CARD_ID_REGEX: RegExp;
|
||||||
declare function generateCardNumber(): string;
|
declare function generateCardNumber(): string;
|
||||||
@@ -33,22 +56,10 @@ declare function generateCardNumber(): string;
|
|||||||
*/
|
*/
|
||||||
declare function validateCardId(id: string): boolean;
|
declare function validateCardId(id: string): boolean;
|
||||||
|
|
||||||
declare enum GameIds {
|
/**
|
||||||
/**
|
* Get the game name from the game identifier (4-letter code)
|
||||||
* Initial D The Arcade
|
* @param id
|
||||||
*/
|
*/
|
||||||
IDAC = "SDGT",
|
declare function getGameName(id: string): string | null;
|
||||||
/**
|
|
||||||
* Chunithm (Paradise Lost)
|
|
||||||
*/
|
|
||||||
Chuni = "SDBT",
|
|
||||||
/**
|
|
||||||
* Chunithm NEW onwards
|
|
||||||
*/
|
|
||||||
Chusan = "SDHD",
|
|
||||||
ONGEKI = "SDDT",
|
|
||||||
maimaiDX = "SDEZ",
|
|
||||||
DivaFT = "SBZV"
|
|
||||||
}
|
|
||||||
|
|
||||||
export { type AMCardInRequest, AMNET_API_VERSION, type AMServerInfoResponse, CARD_ID_REGEX, GameIds, fetchServerInfo, generateCardNumber, performCardIn, validateCardId };
|
export { type AMCardInRequest, AMNET_API_VERSION, type AMServerConnectionInfo, type AMServerInfo, type AMServerInstanceMetrics, type AMServerMetadata, CARD_ID_REGEX, MIN_AMNET_API_VERSION, fetchServerInfo, generateCardNumber, getGameName, performCardIn, validateCardId };
|
||||||
|
|||||||
Vendored
+36
-25
@@ -2,28 +2,51 @@
|
|||||||
* The latest version of the AMNet API this library supports
|
* The latest version of the AMNet API this library supports
|
||||||
*/
|
*/
|
||||||
declare const AMNET_API_VERSION = 1;
|
declare const AMNET_API_VERSION = 1;
|
||||||
|
/**
|
||||||
|
* The minimum version of the AMNet API this library supports
|
||||||
|
*/
|
||||||
|
declare const MIN_AMNET_API_VERSION = 1;
|
||||||
interface AMCardInRequest {
|
interface AMCardInRequest {
|
||||||
cardId: string;
|
cardId: string;
|
||||||
}
|
}
|
||||||
interface AMServerInfoResponse {
|
/**
|
||||||
serverAddress: string;
|
* Server connection info (api version, address, etc.)
|
||||||
serverName: string;
|
*/
|
||||||
|
interface AMServerConnectionInfo {
|
||||||
apiVersion: number;
|
apiVersion: number;
|
||||||
gameId: string;
|
serverAddress: string;
|
||||||
}
|
}
|
||||||
|
/**
|
||||||
|
* User-definable server metadata
|
||||||
|
*/
|
||||||
|
interface AMServerMetadata {
|
||||||
|
serverName: string;
|
||||||
|
gameId?: string | null | undefined;
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* Metrics about the current game session
|
||||||
|
*/
|
||||||
|
interface AMServerInstanceMetrics {
|
||||||
|
timeSinceLastPoll?: number;
|
||||||
|
sessionUptime?: number;
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* The response from an AMNet server's info endpoint
|
||||||
|
*/
|
||||||
|
type AMServerInfo = AMServerMetadata & AMServerConnectionInfo & AMServerInstanceMetrics;
|
||||||
/**
|
/**
|
||||||
* Performs the card-in request to the specified server
|
* Performs the card-in request to the specified server
|
||||||
* @param serverAddress The origin address of the server to perform the request against
|
* @param server The server to perform the request against
|
||||||
* @param request The card-in request to send
|
* @param request The card-in request to send
|
||||||
* @param signal The abort signal to use for the request
|
* @param signal The abort signal to use for the request
|
||||||
*/
|
*/
|
||||||
declare function performCardIn(serverAddress: string, request: AMCardInRequest, signal: AbortSignal): Promise<boolean>;
|
declare function performCardIn(server: AMServerConnectionInfo, request: AMCardInRequest, signal: AbortSignal): Promise<boolean>;
|
||||||
/**
|
/**
|
||||||
* Attempts to connect to the specified server and fetch its info
|
* Attempts to connect to the specified server and fetch its info
|
||||||
* @param serverAddress The origin address of the server to connect to
|
* @param serverAddress The origin address of the server to connect to
|
||||||
* @param abort The abort signal to use for the request (for cancellation)
|
* @param abort The abort signal to use for the request (for cancellation)
|
||||||
*/
|
*/
|
||||||
declare function fetchServerInfo(serverAddress: string, abort: AbortSignal): Promise<AMServerInfoResponse | null>;
|
declare function fetchServerInfo(serverAddress: string, abort: AbortSignal): Promise<AMServerInfo | null>;
|
||||||
|
|
||||||
declare const CARD_ID_REGEX: RegExp;
|
declare const CARD_ID_REGEX: RegExp;
|
||||||
declare function generateCardNumber(): string;
|
declare function generateCardNumber(): string;
|
||||||
@@ -33,22 +56,10 @@ declare function generateCardNumber(): string;
|
|||||||
*/
|
*/
|
||||||
declare function validateCardId(id: string): boolean;
|
declare function validateCardId(id: string): boolean;
|
||||||
|
|
||||||
declare enum GameIds {
|
/**
|
||||||
/**
|
* Get the game name from the game identifier (4-letter code)
|
||||||
* Initial D The Arcade
|
* @param id
|
||||||
*/
|
*/
|
||||||
IDAC = "SDGT",
|
declare function getGameName(id: string): string | null;
|
||||||
/**
|
|
||||||
* Chunithm (Paradise Lost)
|
|
||||||
*/
|
|
||||||
Chuni = "SDBT",
|
|
||||||
/**
|
|
||||||
* Chunithm NEW onwards
|
|
||||||
*/
|
|
||||||
Chusan = "SDHD",
|
|
||||||
ONGEKI = "SDDT",
|
|
||||||
maimaiDX = "SDEZ",
|
|
||||||
DivaFT = "SBZV"
|
|
||||||
}
|
|
||||||
|
|
||||||
export { type AMCardInRequest, AMNET_API_VERSION, type AMServerInfoResponse, CARD_ID_REGEX, GameIds, fetchServerInfo, generateCardNumber, performCardIn, validateCardId };
|
export { type AMCardInRequest, AMNET_API_VERSION, type AMServerConnectionInfo, type AMServerInfo, type AMServerInstanceMetrics, type AMServerMetadata, CARD_ID_REGEX, MIN_AMNET_API_VERSION, fetchServerInfo, generateCardNumber, getGameName, performCardIn, validateCardId };
|
||||||
|
|||||||
Vendored
+30
-13
@@ -59,9 +59,10 @@ var src_exports = {};
|
|||||||
__export(src_exports, {
|
__export(src_exports, {
|
||||||
AMNET_API_VERSION: () => AMNET_API_VERSION,
|
AMNET_API_VERSION: () => AMNET_API_VERSION,
|
||||||
CARD_ID_REGEX: () => CARD_ID_REGEX,
|
CARD_ID_REGEX: () => CARD_ID_REGEX,
|
||||||
GameIds: () => GameIds,
|
MIN_AMNET_API_VERSION: () => MIN_AMNET_API_VERSION,
|
||||||
fetchServerInfo: () => fetchServerInfo,
|
fetchServerInfo: () => fetchServerInfo,
|
||||||
generateCardNumber: () => generateCardNumber,
|
generateCardNumber: () => generateCardNumber,
|
||||||
|
getGameName: () => getGameName,
|
||||||
performCardIn: () => performCardIn,
|
performCardIn: () => performCardIn,
|
||||||
validateCardId: () => validateCardId
|
validateCardId: () => validateCardId
|
||||||
});
|
});
|
||||||
@@ -69,10 +70,14 @@ module.exports = __toCommonJS(src_exports);
|
|||||||
|
|
||||||
// src/api.ts
|
// src/api.ts
|
||||||
var AMNET_API_VERSION = 1;
|
var AMNET_API_VERSION = 1;
|
||||||
function performCardIn(serverAddress, request, signal) {
|
var MIN_AMNET_API_VERSION = 1;
|
||||||
|
function performCardIn(server, request, signal) {
|
||||||
return __async(this, null, function* () {
|
return __async(this, null, function* () {
|
||||||
|
if (server.apiVersion < MIN_AMNET_API_VERSION || server.apiVersion > AMNET_API_VERSION) {
|
||||||
|
throw new Error("Unsupported server API version");
|
||||||
|
}
|
||||||
try {
|
try {
|
||||||
const response = yield fetch(`${new URL(serverAddress).origin}/amnet/signin`, {
|
const response = yield fetch(`${new URL(server.serverAddress).origin}/amnet/signin`, {
|
||||||
method: "POST",
|
method: "POST",
|
||||||
headers: {
|
headers: {
|
||||||
"Content-Type": "application/json"
|
"Content-Type": "application/json"
|
||||||
@@ -92,6 +97,9 @@ function fetchServerInfo(serverAddress, abort) {
|
|||||||
const response = yield fetch(`${new URL(serverAddress).origin}/amnet/info`, {
|
const response = yield fetch(`${new URL(serverAddress).origin}/amnet/info`, {
|
||||||
signal: abort
|
signal: abort
|
||||||
});
|
});
|
||||||
|
if (!response.ok) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
return __spreadProps(__spreadValues({}, yield response.json()), {
|
return __spreadProps(__spreadValues({}, yield response.json()), {
|
||||||
serverAddress
|
serverAddress
|
||||||
});
|
});
|
||||||
@@ -122,22 +130,31 @@ function validateCardId(id) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// src/gameIds.ts
|
// src/gameIds.ts
|
||||||
var GameIds = /* @__PURE__ */ ((GameIds2) => {
|
function getGameName(id) {
|
||||||
GameIds2["IDAC"] = "SDGT";
|
switch (id == null ? void 0 : id.toUpperCase()) {
|
||||||
GameIds2["Chuni"] = "SDBT";
|
case "SDGT":
|
||||||
GameIds2["Chusan"] = "SDHD";
|
return "Initial D The Arcade";
|
||||||
GameIds2["ONGEKI"] = "SDDT";
|
case "SDHD":
|
||||||
GameIds2["maimaiDX"] = "SDEZ";
|
case "SDBT":
|
||||||
GameIds2["DivaFT"] = "SBZV";
|
return "CHUNITHM";
|
||||||
return GameIds2;
|
case "SDDT":
|
||||||
})(GameIds || {});
|
return "O.N.G.E.K.I.";
|
||||||
|
case "SDEZ":
|
||||||
|
return "maimai DX";
|
||||||
|
case "SBZV":
|
||||||
|
return "Project DIVA Future Tone";
|
||||||
|
default:
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
// Annotate the CommonJS export names for ESM import in node:
|
// Annotate the CommonJS export names for ESM import in node:
|
||||||
0 && (module.exports = {
|
0 && (module.exports = {
|
||||||
AMNET_API_VERSION,
|
AMNET_API_VERSION,
|
||||||
CARD_ID_REGEX,
|
CARD_ID_REGEX,
|
||||||
GameIds,
|
MIN_AMNET_API_VERSION,
|
||||||
fetchServerInfo,
|
fetchServerInfo,
|
||||||
generateCardNumber,
|
generateCardNumber,
|
||||||
|
getGameName,
|
||||||
performCardIn,
|
performCardIn,
|
||||||
validateCardId
|
validateCardId
|
||||||
});
|
});
|
||||||
|
|||||||
Vendored
+1
-1
File diff suppressed because one or more lines are too long
Vendored
+28
-12
@@ -40,10 +40,14 @@ var __async = (__this, __arguments, generator) => {
|
|||||||
|
|
||||||
// src/api.ts
|
// src/api.ts
|
||||||
var AMNET_API_VERSION = 1;
|
var AMNET_API_VERSION = 1;
|
||||||
function performCardIn(serverAddress, request, signal) {
|
var MIN_AMNET_API_VERSION = 1;
|
||||||
|
function performCardIn(server, request, signal) {
|
||||||
return __async(this, null, function* () {
|
return __async(this, null, function* () {
|
||||||
|
if (server.apiVersion < MIN_AMNET_API_VERSION || server.apiVersion > AMNET_API_VERSION) {
|
||||||
|
throw new Error("Unsupported server API version");
|
||||||
|
}
|
||||||
try {
|
try {
|
||||||
const response = yield fetch(`${new URL(serverAddress).origin}/amnet/signin`, {
|
const response = yield fetch(`${new URL(server.serverAddress).origin}/amnet/signin`, {
|
||||||
method: "POST",
|
method: "POST",
|
||||||
headers: {
|
headers: {
|
||||||
"Content-Type": "application/json"
|
"Content-Type": "application/json"
|
||||||
@@ -63,6 +67,9 @@ function fetchServerInfo(serverAddress, abort) {
|
|||||||
const response = yield fetch(`${new URL(serverAddress).origin}/amnet/info`, {
|
const response = yield fetch(`${new URL(serverAddress).origin}/amnet/info`, {
|
||||||
signal: abort
|
signal: abort
|
||||||
});
|
});
|
||||||
|
if (!response.ok) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
return __spreadProps(__spreadValues({}, yield response.json()), {
|
return __spreadProps(__spreadValues({}, yield response.json()), {
|
||||||
serverAddress
|
serverAddress
|
||||||
});
|
});
|
||||||
@@ -93,21 +100,30 @@ function validateCardId(id) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// src/gameIds.ts
|
// src/gameIds.ts
|
||||||
var GameIds = /* @__PURE__ */ ((GameIds2) => {
|
function getGameName(id) {
|
||||||
GameIds2["IDAC"] = "SDGT";
|
switch (id == null ? void 0 : id.toUpperCase()) {
|
||||||
GameIds2["Chuni"] = "SDBT";
|
case "SDGT":
|
||||||
GameIds2["Chusan"] = "SDHD";
|
return "Initial D The Arcade";
|
||||||
GameIds2["ONGEKI"] = "SDDT";
|
case "SDHD":
|
||||||
GameIds2["maimaiDX"] = "SDEZ";
|
case "SDBT":
|
||||||
GameIds2["DivaFT"] = "SBZV";
|
return "CHUNITHM";
|
||||||
return GameIds2;
|
case "SDDT":
|
||||||
})(GameIds || {});
|
return "O.N.G.E.K.I.";
|
||||||
|
case "SDEZ":
|
||||||
|
return "maimai DX";
|
||||||
|
case "SBZV":
|
||||||
|
return "Project DIVA Future Tone";
|
||||||
|
default:
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
export {
|
export {
|
||||||
AMNET_API_VERSION,
|
AMNET_API_VERSION,
|
||||||
CARD_ID_REGEX,
|
CARD_ID_REGEX,
|
||||||
GameIds,
|
MIN_AMNET_API_VERSION,
|
||||||
fetchServerInfo,
|
fetchServerInfo,
|
||||||
generateCardNumber,
|
generateCardNumber,
|
||||||
|
getGameName,
|
||||||
performCardIn,
|
performCardIn,
|
||||||
validateCardId
|
validateCardId
|
||||||
};
|
};
|
||||||
|
|||||||
Vendored
+1
-1
File diff suppressed because one or more lines are too long
Generated
+70
-70
@@ -541,9 +541,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@rollup/rollup-android-arm-eabi": {
|
"node_modules/@rollup/rollup-android-arm-eabi": {
|
||||||
"version": "4.21.0",
|
"version": "4.22.4",
|
||||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.21.0.tgz",
|
"resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.22.4.tgz",
|
||||||
"integrity": "sha512-WTWD8PfoSAJ+qL87lE7votj3syLavxunWhzCnx3XFxFiI/BA/r3X7MUM8dVrH8rb2r4AiO8jJsr3ZjdaftmnfA==",
|
"integrity": "sha512-Fxamp4aEZnfPOcGA8KSNEohV8hX7zVHOemC8jVBoBUHu5zpJK/Eu3uJwt6BMgy9fkvzxDaurgj96F/NiLukF2w==",
|
||||||
"cpu": [
|
"cpu": [
|
||||||
"arm"
|
"arm"
|
||||||
],
|
],
|
||||||
@@ -555,9 +555,9 @@
|
|||||||
]
|
]
|
||||||
},
|
},
|
||||||
"node_modules/@rollup/rollup-android-arm64": {
|
"node_modules/@rollup/rollup-android-arm64": {
|
||||||
"version": "4.21.0",
|
"version": "4.22.4",
|
||||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.21.0.tgz",
|
"resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.22.4.tgz",
|
||||||
"integrity": "sha512-a1sR2zSK1B4eYkiZu17ZUZhmUQcKjk2/j9Me2IDjk1GHW7LB5Z35LEzj9iJch6gtUfsnvZs1ZNyDW2oZSThrkA==",
|
"integrity": "sha512-VXoK5UMrgECLYaMuGuVTOx5kcuap1Jm8g/M83RnCHBKOqvPPmROFJGQaZhGccnsFtfXQ3XYa4/jMCJvZnbJBdA==",
|
||||||
"cpu": [
|
"cpu": [
|
||||||
"arm64"
|
"arm64"
|
||||||
],
|
],
|
||||||
@@ -569,9 +569,9 @@
|
|||||||
]
|
]
|
||||||
},
|
},
|
||||||
"node_modules/@rollup/rollup-darwin-arm64": {
|
"node_modules/@rollup/rollup-darwin-arm64": {
|
||||||
"version": "4.21.0",
|
"version": "4.22.4",
|
||||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.21.0.tgz",
|
"resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.22.4.tgz",
|
||||||
"integrity": "sha512-zOnKWLgDld/svhKO5PD9ozmL6roy5OQ5T4ThvdYZLpiOhEGY+dp2NwUmxK0Ld91LrbjrvtNAE0ERBwjqhZTRAA==",
|
"integrity": "sha512-xMM9ORBqu81jyMKCDP+SZDhnX2QEVQzTcC6G18KlTQEzWK8r/oNZtKuZaCcHhnsa6fEeOBionoyl5JsAbE/36Q==",
|
||||||
"cpu": [
|
"cpu": [
|
||||||
"arm64"
|
"arm64"
|
||||||
],
|
],
|
||||||
@@ -583,9 +583,9 @@
|
|||||||
]
|
]
|
||||||
},
|
},
|
||||||
"node_modules/@rollup/rollup-darwin-x64": {
|
"node_modules/@rollup/rollup-darwin-x64": {
|
||||||
"version": "4.21.0",
|
"version": "4.22.4",
|
||||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.21.0.tgz",
|
"resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.22.4.tgz",
|
||||||
"integrity": "sha512-7doS8br0xAkg48SKE2QNtMSFPFUlRdw9+votl27MvT46vo44ATBmdZdGysOevNELmZlfd+NEa0UYOA8f01WSrg==",
|
"integrity": "sha512-aJJyYKQwbHuhTUrjWjxEvGnNNBCnmpHDvrb8JFDbeSH3m2XdHcxDd3jthAzvmoI8w/kSjd2y0udT+4okADsZIw==",
|
||||||
"cpu": [
|
"cpu": [
|
||||||
"x64"
|
"x64"
|
||||||
],
|
],
|
||||||
@@ -597,9 +597,9 @@
|
|||||||
]
|
]
|
||||||
},
|
},
|
||||||
"node_modules/@rollup/rollup-linux-arm-gnueabihf": {
|
"node_modules/@rollup/rollup-linux-arm-gnueabihf": {
|
||||||
"version": "4.21.0",
|
"version": "4.22.4",
|
||||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.21.0.tgz",
|
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.22.4.tgz",
|
||||||
"integrity": "sha512-pWJsfQjNWNGsoCq53KjMtwdJDmh/6NubwQcz52aEwLEuvx08bzcy6tOUuawAOncPnxz/3siRtd8hiQ32G1y8VA==",
|
"integrity": "sha512-j63YtCIRAzbO+gC2L9dWXRh5BFetsv0j0va0Wi9epXDgU/XUi5dJKo4USTttVyK7fGw2nPWK0PbAvyliz50SCQ==",
|
||||||
"cpu": [
|
"cpu": [
|
||||||
"arm"
|
"arm"
|
||||||
],
|
],
|
||||||
@@ -611,9 +611,9 @@
|
|||||||
]
|
]
|
||||||
},
|
},
|
||||||
"node_modules/@rollup/rollup-linux-arm-musleabihf": {
|
"node_modules/@rollup/rollup-linux-arm-musleabihf": {
|
||||||
"version": "4.21.0",
|
"version": "4.22.4",
|
||||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.21.0.tgz",
|
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.22.4.tgz",
|
||||||
"integrity": "sha512-efRIANsz3UHZrnZXuEvxS9LoCOWMGD1rweciD6uJQIx2myN3a8Im1FafZBzh7zk1RJ6oKcR16dU3UPldaKd83w==",
|
"integrity": "sha512-dJnWUgwWBX1YBRsuKKMOlXCzh2Wu1mlHzv20TpqEsfdZLb3WoJW2kIEsGwLkroYf24IrPAvOT/ZQ2OYMV6vlrg==",
|
||||||
"cpu": [
|
"cpu": [
|
||||||
"arm"
|
"arm"
|
||||||
],
|
],
|
||||||
@@ -625,9 +625,9 @@
|
|||||||
]
|
]
|
||||||
},
|
},
|
||||||
"node_modules/@rollup/rollup-linux-arm64-gnu": {
|
"node_modules/@rollup/rollup-linux-arm64-gnu": {
|
||||||
"version": "4.21.0",
|
"version": "4.22.4",
|
||||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.21.0.tgz",
|
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.22.4.tgz",
|
||||||
"integrity": "sha512-ZrPhydkTVhyeGTW94WJ8pnl1uroqVHM3j3hjdquwAcWnmivjAwOYjTEAuEDeJvGX7xv3Z9GAvrBkEzCgHq9U1w==",
|
"integrity": "sha512-AdPRoNi3NKVLolCN/Sp4F4N1d98c4SBnHMKoLuiG6RXgoZ4sllseuGioszumnPGmPM2O7qaAX/IJdeDU8f26Aw==",
|
||||||
"cpu": [
|
"cpu": [
|
||||||
"arm64"
|
"arm64"
|
||||||
],
|
],
|
||||||
@@ -639,9 +639,9 @@
|
|||||||
]
|
]
|
||||||
},
|
},
|
||||||
"node_modules/@rollup/rollup-linux-arm64-musl": {
|
"node_modules/@rollup/rollup-linux-arm64-musl": {
|
||||||
"version": "4.21.0",
|
"version": "4.22.4",
|
||||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.21.0.tgz",
|
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.22.4.tgz",
|
||||||
"integrity": "sha512-cfaupqd+UEFeURmqNP2eEvXqgbSox/LHOyN9/d2pSdV8xTrjdg3NgOFJCtc1vQ/jEke1qD0IejbBfxleBPHnPw==",
|
"integrity": "sha512-Gl0AxBtDg8uoAn5CCqQDMqAx22Wx22pjDOjBdmG0VIWX3qUBHzYmOKh8KXHL4UpogfJ14G4wk16EQogF+v8hmA==",
|
||||||
"cpu": [
|
"cpu": [
|
||||||
"arm64"
|
"arm64"
|
||||||
],
|
],
|
||||||
@@ -653,9 +653,9 @@
|
|||||||
]
|
]
|
||||||
},
|
},
|
||||||
"node_modules/@rollup/rollup-linux-powerpc64le-gnu": {
|
"node_modules/@rollup/rollup-linux-powerpc64le-gnu": {
|
||||||
"version": "4.21.0",
|
"version": "4.22.4",
|
||||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-powerpc64le-gnu/-/rollup-linux-powerpc64le-gnu-4.21.0.tgz",
|
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-powerpc64le-gnu/-/rollup-linux-powerpc64le-gnu-4.22.4.tgz",
|
||||||
"integrity": "sha512-ZKPan1/RvAhrUylwBXC9t7B2hXdpb/ufeu22pG2psV7RN8roOfGurEghw1ySmX/CmDDHNTDDjY3lo9hRlgtaHg==",
|
"integrity": "sha512-3aVCK9xfWW1oGQpTsYJJPF6bfpWfhbRnhdlyhak2ZiyFLDaayz0EP5j9V1RVLAAxlmWKTDfS9wyRyY3hvhPoOg==",
|
||||||
"cpu": [
|
"cpu": [
|
||||||
"ppc64"
|
"ppc64"
|
||||||
],
|
],
|
||||||
@@ -667,9 +667,9 @@
|
|||||||
]
|
]
|
||||||
},
|
},
|
||||||
"node_modules/@rollup/rollup-linux-riscv64-gnu": {
|
"node_modules/@rollup/rollup-linux-riscv64-gnu": {
|
||||||
"version": "4.21.0",
|
"version": "4.22.4",
|
||||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.21.0.tgz",
|
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.22.4.tgz",
|
||||||
"integrity": "sha512-H1eRaCwd5E8eS8leiS+o/NqMdljkcb1d6r2h4fKSsCXQilLKArq6WS7XBLDu80Yz+nMqHVFDquwcVrQmGr28rg==",
|
"integrity": "sha512-ePYIir6VYnhgv2C5Xe9u+ico4t8sZWXschR6fMgoPUK31yQu7hTEJb7bCqivHECwIClJfKgE7zYsh1qTP3WHUA==",
|
||||||
"cpu": [
|
"cpu": [
|
||||||
"riscv64"
|
"riscv64"
|
||||||
],
|
],
|
||||||
@@ -681,9 +681,9 @@
|
|||||||
]
|
]
|
||||||
},
|
},
|
||||||
"node_modules/@rollup/rollup-linux-s390x-gnu": {
|
"node_modules/@rollup/rollup-linux-s390x-gnu": {
|
||||||
"version": "4.21.0",
|
"version": "4.22.4",
|
||||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.21.0.tgz",
|
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.22.4.tgz",
|
||||||
"integrity": "sha512-zJ4hA+3b5tu8u7L58CCSI0A9N1vkfwPhWd/puGXwtZlsB5bTkwDNW/+JCU84+3QYmKpLi+XvHdmrlwUwDA6kqw==",
|
"integrity": "sha512-GqFJ9wLlbB9daxhVlrTe61vJtEY99/xB3C8e4ULVsVfflcpmR6c8UZXjtkMA6FhNONhj2eA5Tk9uAVw5orEs4Q==",
|
||||||
"cpu": [
|
"cpu": [
|
||||||
"s390x"
|
"s390x"
|
||||||
],
|
],
|
||||||
@@ -695,9 +695,9 @@
|
|||||||
]
|
]
|
||||||
},
|
},
|
||||||
"node_modules/@rollup/rollup-linux-x64-gnu": {
|
"node_modules/@rollup/rollup-linux-x64-gnu": {
|
||||||
"version": "4.21.0",
|
"version": "4.22.4",
|
||||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.21.0.tgz",
|
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.22.4.tgz",
|
||||||
"integrity": "sha512-e2hrvElFIh6kW/UNBQK/kzqMNY5mO+67YtEh9OA65RM5IJXYTWiXjX6fjIiPaqOkBthYF1EqgiZ6OXKcQsM0hg==",
|
"integrity": "sha512-87v0ol2sH9GE3cLQLNEy0K/R0pz1nvg76o8M5nhMR0+Q+BBGLnb35P0fVz4CQxHYXaAOhE8HhlkaZfsdUOlHwg==",
|
||||||
"cpu": [
|
"cpu": [
|
||||||
"x64"
|
"x64"
|
||||||
],
|
],
|
||||||
@@ -709,9 +709,9 @@
|
|||||||
]
|
]
|
||||||
},
|
},
|
||||||
"node_modules/@rollup/rollup-linux-x64-musl": {
|
"node_modules/@rollup/rollup-linux-x64-musl": {
|
||||||
"version": "4.21.0",
|
"version": "4.22.4",
|
||||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.21.0.tgz",
|
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.22.4.tgz",
|
||||||
"integrity": "sha512-1vvmgDdUSebVGXWX2lIcgRebqfQSff0hMEkLJyakQ9JQUbLDkEaMsPTLOmyccyC6IJ/l3FZuJbmrBw/u0A0uCQ==",
|
"integrity": "sha512-UV6FZMUgePDZrFjrNGIWzDo/vABebuXBhJEqrHxrGiU6HikPy0Z3LfdtciIttEUQfuDdCn8fqh7wiFJjCNwO+g==",
|
||||||
"cpu": [
|
"cpu": [
|
||||||
"x64"
|
"x64"
|
||||||
],
|
],
|
||||||
@@ -723,9 +723,9 @@
|
|||||||
]
|
]
|
||||||
},
|
},
|
||||||
"node_modules/@rollup/rollup-win32-arm64-msvc": {
|
"node_modules/@rollup/rollup-win32-arm64-msvc": {
|
||||||
"version": "4.21.0",
|
"version": "4.22.4",
|
||||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.21.0.tgz",
|
"resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.22.4.tgz",
|
||||||
"integrity": "sha512-s5oFkZ/hFcrlAyBTONFY1TWndfyre1wOMwU+6KCpm/iatybvrRgmZVM+vCFwxmC5ZhdlgfE0N4XorsDpi7/4XQ==",
|
"integrity": "sha512-BjI+NVVEGAXjGWYHz/vv0pBqfGoUH0IGZ0cICTn7kB9PyjrATSkX+8WkguNjWoj2qSr1im/+tTGRaY+4/PdcQw==",
|
||||||
"cpu": [
|
"cpu": [
|
||||||
"arm64"
|
"arm64"
|
||||||
],
|
],
|
||||||
@@ -737,9 +737,9 @@
|
|||||||
]
|
]
|
||||||
},
|
},
|
||||||
"node_modules/@rollup/rollup-win32-ia32-msvc": {
|
"node_modules/@rollup/rollup-win32-ia32-msvc": {
|
||||||
"version": "4.21.0",
|
"version": "4.22.4",
|
||||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.21.0.tgz",
|
"resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.22.4.tgz",
|
||||||
"integrity": "sha512-G9+TEqRnAA6nbpqyUqgTiopmnfgnMkR3kMukFBDsiyy23LZvUCpiUwjTRx6ezYCjJODXrh52rBR9oXvm+Fp5wg==",
|
"integrity": "sha512-SiWG/1TuUdPvYmzmYnmd3IEifzR61Tragkbx9D3+R8mzQqDBz8v+BvZNDlkiTtI9T15KYZhP0ehn3Dld4n9J5g==",
|
||||||
"cpu": [
|
"cpu": [
|
||||||
"ia32"
|
"ia32"
|
||||||
],
|
],
|
||||||
@@ -751,9 +751,9 @@
|
|||||||
]
|
]
|
||||||
},
|
},
|
||||||
"node_modules/@rollup/rollup-win32-x64-msvc": {
|
"node_modules/@rollup/rollup-win32-x64-msvc": {
|
||||||
"version": "4.21.0",
|
"version": "4.22.4",
|
||||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.21.0.tgz",
|
"resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.22.4.tgz",
|
||||||
"integrity": "sha512-2jsCDZwtQvRhejHLfZ1JY6w6kEuEtfF9nzYsZxzSlNVKDX+DpsDJ+Rbjkm74nvg2rdx0gwBS+IMdvwJuq3S9pQ==",
|
"integrity": "sha512-j8pPKp53/lq9lMXN57S8cFz0MynJk8OWNuUnXct/9KCpKU7DgU3bYMJhwWmcqC0UU29p8Lr0/7KEVcaM6bf47Q==",
|
||||||
"cpu": [
|
"cpu": [
|
||||||
"x64"
|
"x64"
|
||||||
],
|
],
|
||||||
@@ -1423,9 +1423,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/micromatch": {
|
"node_modules/micromatch": {
|
||||||
"version": "4.0.7",
|
"version": "4.0.8",
|
||||||
"resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.7.tgz",
|
"resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz",
|
||||||
"integrity": "sha512-LPP/3KorzCwBxfeUuZmaR6bG2kdeHSbe0P2tY3FLRU4vYrjYz5hI4QZwV0njUx3jeuKe67YukQ1LSPZBKDqO/Q==",
|
"integrity": "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
@@ -1723,9 +1723,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/rollup": {
|
"node_modules/rollup": {
|
||||||
"version": "4.21.0",
|
"version": "4.22.4",
|
||||||
"resolved": "https://registry.npmjs.org/rollup/-/rollup-4.21.0.tgz",
|
"resolved": "https://registry.npmjs.org/rollup/-/rollup-4.22.4.tgz",
|
||||||
"integrity": "sha512-vo+S/lfA2lMS7rZ2Qoubi6I5hwZwzXeUIctILZLbHI+laNtvhhOIon2S1JksA5UEDQ7l3vberd0fxK44lTYjbQ==",
|
"integrity": "sha512-vD8HJ5raRcWOyymsR6Z3o6+RzfEPCnVLMFJ6vRslO1jt4LO6dUo5Qnpg7y4RkZFM2DMe3WUirkI5c16onjrc6A==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
@@ -1739,22 +1739,22 @@
|
|||||||
"npm": ">=8.0.0"
|
"npm": ">=8.0.0"
|
||||||
},
|
},
|
||||||
"optionalDependencies": {
|
"optionalDependencies": {
|
||||||
"@rollup/rollup-android-arm-eabi": "4.21.0",
|
"@rollup/rollup-android-arm-eabi": "4.22.4",
|
||||||
"@rollup/rollup-android-arm64": "4.21.0",
|
"@rollup/rollup-android-arm64": "4.22.4",
|
||||||
"@rollup/rollup-darwin-arm64": "4.21.0",
|
"@rollup/rollup-darwin-arm64": "4.22.4",
|
||||||
"@rollup/rollup-darwin-x64": "4.21.0",
|
"@rollup/rollup-darwin-x64": "4.22.4",
|
||||||
"@rollup/rollup-linux-arm-gnueabihf": "4.21.0",
|
"@rollup/rollup-linux-arm-gnueabihf": "4.22.4",
|
||||||
"@rollup/rollup-linux-arm-musleabihf": "4.21.0",
|
"@rollup/rollup-linux-arm-musleabihf": "4.22.4",
|
||||||
"@rollup/rollup-linux-arm64-gnu": "4.21.0",
|
"@rollup/rollup-linux-arm64-gnu": "4.22.4",
|
||||||
"@rollup/rollup-linux-arm64-musl": "4.21.0",
|
"@rollup/rollup-linux-arm64-musl": "4.22.4",
|
||||||
"@rollup/rollup-linux-powerpc64le-gnu": "4.21.0",
|
"@rollup/rollup-linux-powerpc64le-gnu": "4.22.4",
|
||||||
"@rollup/rollup-linux-riscv64-gnu": "4.21.0",
|
"@rollup/rollup-linux-riscv64-gnu": "4.22.4",
|
||||||
"@rollup/rollup-linux-s390x-gnu": "4.21.0",
|
"@rollup/rollup-linux-s390x-gnu": "4.22.4",
|
||||||
"@rollup/rollup-linux-x64-gnu": "4.21.0",
|
"@rollup/rollup-linux-x64-gnu": "4.22.4",
|
||||||
"@rollup/rollup-linux-x64-musl": "4.21.0",
|
"@rollup/rollup-linux-x64-musl": "4.22.4",
|
||||||
"@rollup/rollup-win32-arm64-msvc": "4.21.0",
|
"@rollup/rollup-win32-arm64-msvc": "4.22.4",
|
||||||
"@rollup/rollup-win32-ia32-msvc": "4.21.0",
|
"@rollup/rollup-win32-ia32-msvc": "4.22.4",
|
||||||
"@rollup/rollup-win32-x64-msvc": "4.21.0",
|
"@rollup/rollup-win32-x64-msvc": "4.22.4",
|
||||||
"fsevents": "~2.3.2"
|
"fsevents": "~2.3.2"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|||||||
+44
-9
@@ -3,26 +3,57 @@
|
|||||||
*/
|
*/
|
||||||
export const AMNET_API_VERSION = 1;
|
export const AMNET_API_VERSION = 1;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The minimum version of the AMNet API this library supports
|
||||||
|
*/
|
||||||
|
export const MIN_AMNET_API_VERSION = 1;
|
||||||
|
|
||||||
export interface AMCardInRequest {
|
export interface AMCardInRequest {
|
||||||
cardId: string;
|
cardId: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface AMServerInfoResponse {
|
/**
|
||||||
serverAddress: string;
|
* Server connection info (api version, address, etc.)
|
||||||
serverName: string;
|
*/
|
||||||
|
export interface AMServerConnectionInfo {
|
||||||
apiVersion: number;
|
apiVersion: number;
|
||||||
gameId: string;
|
serverAddress: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* User-definable server metadata
|
||||||
|
*/
|
||||||
|
export interface AMServerMetadata {
|
||||||
|
serverName: string;
|
||||||
|
gameId?: string | null | undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Metrics about the current game session
|
||||||
|
*/
|
||||||
|
export interface AMServerInstanceMetrics {
|
||||||
|
timeSinceLastPoll?: number;
|
||||||
|
sessionUptime?: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The response from an AMNet server's info endpoint
|
||||||
|
*/
|
||||||
|
export type AMServerInfo = AMServerMetadata & AMServerConnectionInfo & AMServerInstanceMetrics;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Performs the card-in request to the specified server
|
* Performs the card-in request to the specified server
|
||||||
* @param serverAddress The origin address of the server to perform the request against
|
* @param server The server to perform the request against
|
||||||
* @param request The card-in request to send
|
* @param request The card-in request to send
|
||||||
* @param signal The abort signal to use for the request
|
* @param signal The abort signal to use for the request
|
||||||
*/
|
*/
|
||||||
export async function performCardIn(serverAddress: string, request: AMCardInRequest, signal: AbortSignal) {
|
export async function performCardIn(server: AMServerConnectionInfo, request: AMCardInRequest, signal: AbortSignal): Promise<boolean> {
|
||||||
|
if (server.apiVersion < MIN_AMNET_API_VERSION || server.apiVersion > AMNET_API_VERSION) {
|
||||||
|
throw new Error("Unsupported server API version");
|
||||||
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const response = await fetch(`${new URL(serverAddress).origin}/amnet/signin`, {
|
const response = await fetch(`${new URL(server.serverAddress).origin}/amnet/signin`, {
|
||||||
method: "POST",
|
method: "POST",
|
||||||
headers: {
|
headers: {
|
||||||
"Content-Type": "application/json"
|
"Content-Type": "application/json"
|
||||||
@@ -42,16 +73,20 @@ export async function performCardIn(serverAddress: string, request: AMCardInRequ
|
|||||||
* @param serverAddress The origin address of the server to connect to
|
* @param serverAddress The origin address of the server to connect to
|
||||||
* @param abort The abort signal to use for the request (for cancellation)
|
* @param abort The abort signal to use for the request (for cancellation)
|
||||||
*/
|
*/
|
||||||
export async function fetchServerInfo(serverAddress: string, abort: AbortSignal): Promise<AMServerInfoResponse | null> {
|
export async function fetchServerInfo(serverAddress: string, abort: AbortSignal): Promise<AMServerInfo | null> {
|
||||||
try {
|
try {
|
||||||
const response = await fetch(`${new URL(serverAddress).origin}/amnet/info`, {
|
const response = await fetch(`${new URL(serverAddress).origin}/amnet/info`, {
|
||||||
signal: abort
|
signal: abort
|
||||||
});
|
});
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
...await response.json(),
|
...await response.json(),
|
||||||
serverAddress: serverAddress
|
serverAddress: serverAddress
|
||||||
} satisfies AMServerInfoResponse;
|
} satisfies AMServerMetadata;
|
||||||
} catch {
|
} catch {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|||||||
+22
-20
@@ -1,20 +1,22 @@
|
|||||||
export enum GameIds {
|
/**
|
||||||
/**
|
* Get the game name from the game identifier (4-letter code)
|
||||||
* Initial D The Arcade
|
* @param id
|
||||||
*/
|
*/
|
||||||
IDAC = "SDGT",
|
export function getGameName(id: string): string | null {
|
||||||
|
switch (id?.toUpperCase()) {
|
||||||
/**
|
case "SDGT":
|
||||||
* Chunithm (Paradise Lost)
|
return "Initial D The Arcade";
|
||||||
*/
|
case "SDHD":
|
||||||
Chuni = "SDBT",
|
case "SDBT":
|
||||||
|
return "CHUNITHM";
|
||||||
/**
|
case "SDDT":
|
||||||
* Chunithm NEW onwards
|
return "O.N.G.E.K.I.";
|
||||||
*/
|
case "SDEZ":
|
||||||
Chusan = "SDHD",
|
return "maimai DX";
|
||||||
|
case "SBZV":
|
||||||
ONGEKI = "SDDT",
|
return "Project DIVA Future Tone";
|
||||||
maimaiDX = "SDEZ",
|
|
||||||
DivaFT = "SBZV"
|
default:
|
||||||
}
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
Generated
+99
-87
@@ -20,6 +20,7 @@
|
|||||||
"amnet-shared": "file:../amnet-shared",
|
"amnet-shared": "file:../amnet-shared",
|
||||||
"class-variance-authority": "^0.7.0",
|
"class-variance-authority": "^0.7.0",
|
||||||
"clsx": "^2.1.1",
|
"clsx": "^2.1.1",
|
||||||
|
"core-js": "^3.39.0",
|
||||||
"detectincognitojs": "^1.3.5",
|
"detectincognitojs": "^1.3.5",
|
||||||
"dexie": "^4.0.8",
|
"dexie": "^4.0.8",
|
||||||
"dexie-react-hooks": "^1.1.7",
|
"dexie-react-hooks": "^1.1.7",
|
||||||
@@ -50,7 +51,7 @@
|
|||||||
"tailwindcss": "^3.4.9",
|
"tailwindcss": "^3.4.9",
|
||||||
"typescript": "^5.5.3",
|
"typescript": "^5.5.3",
|
||||||
"typescript-eslint": "^8.0.0",
|
"typescript-eslint": "^8.0.0",
|
||||||
"vite": "^5.4.0"
|
"vite": "^5.4.6"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"../amnet-native": {
|
"../amnet-native": {
|
||||||
@@ -1547,9 +1548,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@rollup/rollup-android-arm-eabi": {
|
"node_modules/@rollup/rollup-android-arm-eabi": {
|
||||||
"version": "4.20.0",
|
"version": "4.22.4",
|
||||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.20.0.tgz",
|
"resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.22.4.tgz",
|
||||||
"integrity": "sha512-TSpWzflCc4VGAUJZlPpgAJE1+V60MePDQnBd7PPkpuEmOy8i87aL6tinFGKBFKuEDikYpig72QzdT3QPYIi+oA==",
|
"integrity": "sha512-Fxamp4aEZnfPOcGA8KSNEohV8hX7zVHOemC8jVBoBUHu5zpJK/Eu3uJwt6BMgy9fkvzxDaurgj96F/NiLukF2w==",
|
||||||
"cpu": [
|
"cpu": [
|
||||||
"arm"
|
"arm"
|
||||||
],
|
],
|
||||||
@@ -1561,9 +1562,9 @@
|
|||||||
]
|
]
|
||||||
},
|
},
|
||||||
"node_modules/@rollup/rollup-android-arm64": {
|
"node_modules/@rollup/rollup-android-arm64": {
|
||||||
"version": "4.20.0",
|
"version": "4.22.4",
|
||||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.20.0.tgz",
|
"resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.22.4.tgz",
|
||||||
"integrity": "sha512-u00Ro/nok7oGzVuh/FMYfNoGqxU5CPWz1mxV85S2w9LxHR8OoMQBuSk+3BKVIDYgkpeOET5yXkx90OYFc+ytpQ==",
|
"integrity": "sha512-VXoK5UMrgECLYaMuGuVTOx5kcuap1Jm8g/M83RnCHBKOqvPPmROFJGQaZhGccnsFtfXQ3XYa4/jMCJvZnbJBdA==",
|
||||||
"cpu": [
|
"cpu": [
|
||||||
"arm64"
|
"arm64"
|
||||||
],
|
],
|
||||||
@@ -1575,9 +1576,9 @@
|
|||||||
]
|
]
|
||||||
},
|
},
|
||||||
"node_modules/@rollup/rollup-darwin-arm64": {
|
"node_modules/@rollup/rollup-darwin-arm64": {
|
||||||
"version": "4.20.0",
|
"version": "4.22.4",
|
||||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.20.0.tgz",
|
"resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.22.4.tgz",
|
||||||
"integrity": "sha512-uFVfvzvsdGtlSLuL0ZlvPJvl6ZmrH4CBwLGEFPe7hUmf7htGAN+aXo43R/V6LATyxlKVC/m6UsLb7jbG+LG39Q==",
|
"integrity": "sha512-xMM9ORBqu81jyMKCDP+SZDhnX2QEVQzTcC6G18KlTQEzWK8r/oNZtKuZaCcHhnsa6fEeOBionoyl5JsAbE/36Q==",
|
||||||
"cpu": [
|
"cpu": [
|
||||||
"arm64"
|
"arm64"
|
||||||
],
|
],
|
||||||
@@ -1589,9 +1590,9 @@
|
|||||||
]
|
]
|
||||||
},
|
},
|
||||||
"node_modules/@rollup/rollup-darwin-x64": {
|
"node_modules/@rollup/rollup-darwin-x64": {
|
||||||
"version": "4.20.0",
|
"version": "4.22.4",
|
||||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.20.0.tgz",
|
"resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.22.4.tgz",
|
||||||
"integrity": "sha512-xbrMDdlev53vNXexEa6l0LffojxhqDTBeL+VUxuuIXys4x6xyvbKq5XqTXBCEUA8ty8iEJblHvFaWRJTk/icAQ==",
|
"integrity": "sha512-aJJyYKQwbHuhTUrjWjxEvGnNNBCnmpHDvrb8JFDbeSH3m2XdHcxDd3jthAzvmoI8w/kSjd2y0udT+4okADsZIw==",
|
||||||
"cpu": [
|
"cpu": [
|
||||||
"x64"
|
"x64"
|
||||||
],
|
],
|
||||||
@@ -1603,9 +1604,9 @@
|
|||||||
]
|
]
|
||||||
},
|
},
|
||||||
"node_modules/@rollup/rollup-linux-arm-gnueabihf": {
|
"node_modules/@rollup/rollup-linux-arm-gnueabihf": {
|
||||||
"version": "4.20.0",
|
"version": "4.22.4",
|
||||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.20.0.tgz",
|
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.22.4.tgz",
|
||||||
"integrity": "sha512-jMYvxZwGmoHFBTbr12Xc6wOdc2xA5tF5F2q6t7Rcfab68TT0n+r7dgawD4qhPEvasDsVpQi+MgDzj2faOLsZjA==",
|
"integrity": "sha512-j63YtCIRAzbO+gC2L9dWXRh5BFetsv0j0va0Wi9epXDgU/XUi5dJKo4USTttVyK7fGw2nPWK0PbAvyliz50SCQ==",
|
||||||
"cpu": [
|
"cpu": [
|
||||||
"arm"
|
"arm"
|
||||||
],
|
],
|
||||||
@@ -1617,9 +1618,9 @@
|
|||||||
]
|
]
|
||||||
},
|
},
|
||||||
"node_modules/@rollup/rollup-linux-arm-musleabihf": {
|
"node_modules/@rollup/rollup-linux-arm-musleabihf": {
|
||||||
"version": "4.20.0",
|
"version": "4.22.4",
|
||||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.20.0.tgz",
|
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.22.4.tgz",
|
||||||
"integrity": "sha512-1asSTl4HKuIHIB1GcdFHNNZhxAYEdqML/MW4QmPS4G0ivbEcBr1JKlFLKsIRqjSwOBkdItn3/ZDlyvZ/N6KPlw==",
|
"integrity": "sha512-dJnWUgwWBX1YBRsuKKMOlXCzh2Wu1mlHzv20TpqEsfdZLb3WoJW2kIEsGwLkroYf24IrPAvOT/ZQ2OYMV6vlrg==",
|
||||||
"cpu": [
|
"cpu": [
|
||||||
"arm"
|
"arm"
|
||||||
],
|
],
|
||||||
@@ -1631,9 +1632,9 @@
|
|||||||
]
|
]
|
||||||
},
|
},
|
||||||
"node_modules/@rollup/rollup-linux-arm64-gnu": {
|
"node_modules/@rollup/rollup-linux-arm64-gnu": {
|
||||||
"version": "4.20.0",
|
"version": "4.22.4",
|
||||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.20.0.tgz",
|
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.22.4.tgz",
|
||||||
"integrity": "sha512-COBb8Bkx56KldOYJfMf6wKeYJrtJ9vEgBRAOkfw6Ens0tnmzPqvlpjZiLgkhg6cA3DGzCmLmmd319pmHvKWWlQ==",
|
"integrity": "sha512-AdPRoNi3NKVLolCN/Sp4F4N1d98c4SBnHMKoLuiG6RXgoZ4sllseuGioszumnPGmPM2O7qaAX/IJdeDU8f26Aw==",
|
||||||
"cpu": [
|
"cpu": [
|
||||||
"arm64"
|
"arm64"
|
||||||
],
|
],
|
||||||
@@ -1645,9 +1646,9 @@
|
|||||||
]
|
]
|
||||||
},
|
},
|
||||||
"node_modules/@rollup/rollup-linux-arm64-musl": {
|
"node_modules/@rollup/rollup-linux-arm64-musl": {
|
||||||
"version": "4.20.0",
|
"version": "4.22.4",
|
||||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.20.0.tgz",
|
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.22.4.tgz",
|
||||||
"integrity": "sha512-+it+mBSyMslVQa8wSPvBx53fYuZK/oLTu5RJoXogjk6x7Q7sz1GNRsXWjn6SwyJm8E/oMjNVwPhmNdIjwP135Q==",
|
"integrity": "sha512-Gl0AxBtDg8uoAn5CCqQDMqAx22Wx22pjDOjBdmG0VIWX3qUBHzYmOKh8KXHL4UpogfJ14G4wk16EQogF+v8hmA==",
|
||||||
"cpu": [
|
"cpu": [
|
||||||
"arm64"
|
"arm64"
|
||||||
],
|
],
|
||||||
@@ -1659,9 +1660,9 @@
|
|||||||
]
|
]
|
||||||
},
|
},
|
||||||
"node_modules/@rollup/rollup-linux-powerpc64le-gnu": {
|
"node_modules/@rollup/rollup-linux-powerpc64le-gnu": {
|
||||||
"version": "4.20.0",
|
"version": "4.22.4",
|
||||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-powerpc64le-gnu/-/rollup-linux-powerpc64le-gnu-4.20.0.tgz",
|
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-powerpc64le-gnu/-/rollup-linux-powerpc64le-gnu-4.22.4.tgz",
|
||||||
"integrity": "sha512-yAMvqhPfGKsAxHN8I4+jE0CpLWD8cv4z7CK7BMmhjDuz606Q2tFKkWRY8bHR9JQXYcoLfopo5TTqzxgPUjUMfw==",
|
"integrity": "sha512-3aVCK9xfWW1oGQpTsYJJPF6bfpWfhbRnhdlyhak2ZiyFLDaayz0EP5j9V1RVLAAxlmWKTDfS9wyRyY3hvhPoOg==",
|
||||||
"cpu": [
|
"cpu": [
|
||||||
"ppc64"
|
"ppc64"
|
||||||
],
|
],
|
||||||
@@ -1673,9 +1674,9 @@
|
|||||||
]
|
]
|
||||||
},
|
},
|
||||||
"node_modules/@rollup/rollup-linux-riscv64-gnu": {
|
"node_modules/@rollup/rollup-linux-riscv64-gnu": {
|
||||||
"version": "4.20.0",
|
"version": "4.22.4",
|
||||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.20.0.tgz",
|
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.22.4.tgz",
|
||||||
"integrity": "sha512-qmuxFpfmi/2SUkAw95TtNq/w/I7Gpjurx609OOOV7U4vhvUhBcftcmXwl3rqAek+ADBwSjIC4IVNLiszoj3dPA==",
|
"integrity": "sha512-ePYIir6VYnhgv2C5Xe9u+ico4t8sZWXschR6fMgoPUK31yQu7hTEJb7bCqivHECwIClJfKgE7zYsh1qTP3WHUA==",
|
||||||
"cpu": [
|
"cpu": [
|
||||||
"riscv64"
|
"riscv64"
|
||||||
],
|
],
|
||||||
@@ -1687,9 +1688,9 @@
|
|||||||
]
|
]
|
||||||
},
|
},
|
||||||
"node_modules/@rollup/rollup-linux-s390x-gnu": {
|
"node_modules/@rollup/rollup-linux-s390x-gnu": {
|
||||||
"version": "4.20.0",
|
"version": "4.22.4",
|
||||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.20.0.tgz",
|
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.22.4.tgz",
|
||||||
"integrity": "sha512-I0BtGXddHSHjV1mqTNkgUZLnS3WtsqebAXv11D5BZE/gfw5KoyXSAXVqyJximQXNvNzUo4GKlCK/dIwXlz+jlg==",
|
"integrity": "sha512-GqFJ9wLlbB9daxhVlrTe61vJtEY99/xB3C8e4ULVsVfflcpmR6c8UZXjtkMA6FhNONhj2eA5Tk9uAVw5orEs4Q==",
|
||||||
"cpu": [
|
"cpu": [
|
||||||
"s390x"
|
"s390x"
|
||||||
],
|
],
|
||||||
@@ -1701,9 +1702,9 @@
|
|||||||
]
|
]
|
||||||
},
|
},
|
||||||
"node_modules/@rollup/rollup-linux-x64-gnu": {
|
"node_modules/@rollup/rollup-linux-x64-gnu": {
|
||||||
"version": "4.20.0",
|
"version": "4.22.4",
|
||||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.20.0.tgz",
|
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.22.4.tgz",
|
||||||
"integrity": "sha512-y+eoL2I3iphUg9tN9GB6ku1FA8kOfmF4oUEWhztDJ4KXJy1agk/9+pejOuZkNFhRwHAOxMsBPLbXPd6mJiCwew==",
|
"integrity": "sha512-87v0ol2sH9GE3cLQLNEy0K/R0pz1nvg76o8M5nhMR0+Q+BBGLnb35P0fVz4CQxHYXaAOhE8HhlkaZfsdUOlHwg==",
|
||||||
"cpu": [
|
"cpu": [
|
||||||
"x64"
|
"x64"
|
||||||
],
|
],
|
||||||
@@ -1715,9 +1716,9 @@
|
|||||||
]
|
]
|
||||||
},
|
},
|
||||||
"node_modules/@rollup/rollup-linux-x64-musl": {
|
"node_modules/@rollup/rollup-linux-x64-musl": {
|
||||||
"version": "4.20.0",
|
"version": "4.22.4",
|
||||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.20.0.tgz",
|
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.22.4.tgz",
|
||||||
"integrity": "sha512-hM3nhW40kBNYUkZb/r9k2FKK+/MnKglX7UYd4ZUy5DJs8/sMsIbqWK2piZtVGE3kcXVNj3B2IrUYROJMMCikNg==",
|
"integrity": "sha512-UV6FZMUgePDZrFjrNGIWzDo/vABebuXBhJEqrHxrGiU6HikPy0Z3LfdtciIttEUQfuDdCn8fqh7wiFJjCNwO+g==",
|
||||||
"cpu": [
|
"cpu": [
|
||||||
"x64"
|
"x64"
|
||||||
],
|
],
|
||||||
@@ -1729,9 +1730,9 @@
|
|||||||
]
|
]
|
||||||
},
|
},
|
||||||
"node_modules/@rollup/rollup-win32-arm64-msvc": {
|
"node_modules/@rollup/rollup-win32-arm64-msvc": {
|
||||||
"version": "4.20.0",
|
"version": "4.22.4",
|
||||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.20.0.tgz",
|
"resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.22.4.tgz",
|
||||||
"integrity": "sha512-psegMvP+Ik/Bg7QRJbv8w8PAytPA7Uo8fpFjXyCRHWm6Nt42L+JtoqH8eDQ5hRP7/XW2UiIriy1Z46jf0Oa1kA==",
|
"integrity": "sha512-BjI+NVVEGAXjGWYHz/vv0pBqfGoUH0IGZ0cICTn7kB9PyjrATSkX+8WkguNjWoj2qSr1im/+tTGRaY+4/PdcQw==",
|
||||||
"cpu": [
|
"cpu": [
|
||||||
"arm64"
|
"arm64"
|
||||||
],
|
],
|
||||||
@@ -1743,9 +1744,9 @@
|
|||||||
]
|
]
|
||||||
},
|
},
|
||||||
"node_modules/@rollup/rollup-win32-ia32-msvc": {
|
"node_modules/@rollup/rollup-win32-ia32-msvc": {
|
||||||
"version": "4.20.0",
|
"version": "4.22.4",
|
||||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.20.0.tgz",
|
"resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.22.4.tgz",
|
||||||
"integrity": "sha512-GabekH3w4lgAJpVxkk7hUzUf2hICSQO0a/BLFA11/RMxQT92MabKAqyubzDZmMOC/hcJNlc+rrypzNzYl4Dx7A==",
|
"integrity": "sha512-SiWG/1TuUdPvYmzmYnmd3IEifzR61Tragkbx9D3+R8mzQqDBz8v+BvZNDlkiTtI9T15KYZhP0ehn3Dld4n9J5g==",
|
||||||
"cpu": [
|
"cpu": [
|
||||||
"ia32"
|
"ia32"
|
||||||
],
|
],
|
||||||
@@ -1757,9 +1758,9 @@
|
|||||||
]
|
]
|
||||||
},
|
},
|
||||||
"node_modules/@rollup/rollup-win32-x64-msvc": {
|
"node_modules/@rollup/rollup-win32-x64-msvc": {
|
||||||
"version": "4.20.0",
|
"version": "4.22.4",
|
||||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.20.0.tgz",
|
"resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.22.4.tgz",
|
||||||
"integrity": "sha512-aJ1EJSuTdGnM6qbVC4B5DSmozPTqIag9fSzXRNNo+humQLG89XpPgdt16Ia56ORD7s+H8Pmyx44uczDQ0yDzpg==",
|
"integrity": "sha512-j8pPKp53/lq9lMXN57S8cFz0MynJk8OWNuUnXct/9KCpKU7DgU3bYMJhwWmcqC0UU29p8Lr0/7KEVcaM6bf47Q==",
|
||||||
"cpu": [
|
"cpu": [
|
||||||
"x64"
|
"x64"
|
||||||
],
|
],
|
||||||
@@ -2682,6 +2683,17 @@
|
|||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
|
"node_modules/core-js": {
|
||||||
|
"version": "3.39.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/core-js/-/core-js-3.39.0.tgz",
|
||||||
|
"integrity": "sha512-raM0ew0/jJUqkJ0E6e8UDtl+y/7ktFivgWvqw8dNSQeNWoSDLvQ1H/RN3aPXB9tBd4/FhyR4RDPGhsNIMsAn7g==",
|
||||||
|
"hasInstallScript": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"funding": {
|
||||||
|
"type": "opencollective",
|
||||||
|
"url": "https://opencollective.com/core-js"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/cross-spawn": {
|
"node_modules/cross-spawn": {
|
||||||
"version": "7.0.3",
|
"version": "7.0.3",
|
||||||
"resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.3.tgz",
|
"resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.3.tgz",
|
||||||
@@ -3644,9 +3656,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/micromatch": {
|
"node_modules/micromatch": {
|
||||||
"version": "4.0.7",
|
"version": "4.0.8",
|
||||||
"resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.7.tgz",
|
"resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz",
|
||||||
"integrity": "sha512-LPP/3KorzCwBxfeUuZmaR6bG2kdeHSbe0P2tY3FLRU4vYrjYz5hI4QZwV0njUx3jeuKe67YukQ1LSPZBKDqO/Q==",
|
"integrity": "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"braces": "^3.0.3",
|
"braces": "^3.0.3",
|
||||||
@@ -3886,9 +3898,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/picocolors": {
|
"node_modules/picocolors": {
|
||||||
"version": "1.0.1",
|
"version": "1.1.0",
|
||||||
"resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.0.1.tgz",
|
"resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.0.tgz",
|
||||||
"integrity": "sha512-anP1Z8qwhkbmu7MFP5iTt+wQKXgwzf7zTyGlcdzabySa9vd0Xt392U0rVmz9poOaBj0uHJKyyo9/upk0HrEQew==",
|
"integrity": "sha512-TQ92mBOW0l3LeMeyLV6mzy/kWr8lkd/hp3mTg7wYK7zJhuBStmGMBG0BdeDZS/dZx1IukaX6Bk11zcln25o1Aw==",
|
||||||
"license": "ISC"
|
"license": "ISC"
|
||||||
},
|
},
|
||||||
"node_modules/picomatch": {
|
"node_modules/picomatch": {
|
||||||
@@ -3922,9 +3934,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/postcss": {
|
"node_modules/postcss": {
|
||||||
"version": "8.4.41",
|
"version": "8.4.47",
|
||||||
"resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.41.tgz",
|
"resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.47.tgz",
|
||||||
"integrity": "sha512-TesUflQ0WKZqAvg52PWL6kHgLKP6xB6heTOdoYM0Wt2UHyxNa4K25EZZMgKns3BH1RLVbZCREPpLY0rhnNoHVQ==",
|
"integrity": "sha512-56rxCq7G/XfB4EkXq9Egn5GCqugWvDFjafDOThIdMBsI15iqPqR5r15TfSr1YPYeEI19YeaXMCbY6u88Y76GLQ==",
|
||||||
"funding": [
|
"funding": [
|
||||||
{
|
{
|
||||||
"type": "opencollective",
|
"type": "opencollective",
|
||||||
@@ -3942,8 +3954,8 @@
|
|||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"nanoid": "^3.3.7",
|
"nanoid": "^3.3.7",
|
||||||
"picocolors": "^1.0.1",
|
"picocolors": "^1.1.0",
|
||||||
"source-map-js": "^1.2.0"
|
"source-map-js": "^1.2.1"
|
||||||
},
|
},
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": "^10 || ^12 || >=14"
|
"node": "^10 || ^12 || >=14"
|
||||||
@@ -4327,9 +4339,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/rollup": {
|
"node_modules/rollup": {
|
||||||
"version": "4.20.0",
|
"version": "4.22.4",
|
||||||
"resolved": "https://registry.npmjs.org/rollup/-/rollup-4.20.0.tgz",
|
"resolved": "https://registry.npmjs.org/rollup/-/rollup-4.22.4.tgz",
|
||||||
"integrity": "sha512-6rbWBChcnSGzIlXeIdNIZTopKYad8ZG8ajhl78lGRLsI2rX8IkaotQhVas2Ma+GPxJav19wrSzvRvuiv0YKzWw==",
|
"integrity": "sha512-vD8HJ5raRcWOyymsR6Z3o6+RzfEPCnVLMFJ6vRslO1jt4LO6dUo5Qnpg7y4RkZFM2DMe3WUirkI5c16onjrc6A==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
@@ -4343,22 +4355,22 @@
|
|||||||
"npm": ">=8.0.0"
|
"npm": ">=8.0.0"
|
||||||
},
|
},
|
||||||
"optionalDependencies": {
|
"optionalDependencies": {
|
||||||
"@rollup/rollup-android-arm-eabi": "4.20.0",
|
"@rollup/rollup-android-arm-eabi": "4.22.4",
|
||||||
"@rollup/rollup-android-arm64": "4.20.0",
|
"@rollup/rollup-android-arm64": "4.22.4",
|
||||||
"@rollup/rollup-darwin-arm64": "4.20.0",
|
"@rollup/rollup-darwin-arm64": "4.22.4",
|
||||||
"@rollup/rollup-darwin-x64": "4.20.0",
|
"@rollup/rollup-darwin-x64": "4.22.4",
|
||||||
"@rollup/rollup-linux-arm-gnueabihf": "4.20.0",
|
"@rollup/rollup-linux-arm-gnueabihf": "4.22.4",
|
||||||
"@rollup/rollup-linux-arm-musleabihf": "4.20.0",
|
"@rollup/rollup-linux-arm-musleabihf": "4.22.4",
|
||||||
"@rollup/rollup-linux-arm64-gnu": "4.20.0",
|
"@rollup/rollup-linux-arm64-gnu": "4.22.4",
|
||||||
"@rollup/rollup-linux-arm64-musl": "4.20.0",
|
"@rollup/rollup-linux-arm64-musl": "4.22.4",
|
||||||
"@rollup/rollup-linux-powerpc64le-gnu": "4.20.0",
|
"@rollup/rollup-linux-powerpc64le-gnu": "4.22.4",
|
||||||
"@rollup/rollup-linux-riscv64-gnu": "4.20.0",
|
"@rollup/rollup-linux-riscv64-gnu": "4.22.4",
|
||||||
"@rollup/rollup-linux-s390x-gnu": "4.20.0",
|
"@rollup/rollup-linux-s390x-gnu": "4.22.4",
|
||||||
"@rollup/rollup-linux-x64-gnu": "4.20.0",
|
"@rollup/rollup-linux-x64-gnu": "4.22.4",
|
||||||
"@rollup/rollup-linux-x64-musl": "4.20.0",
|
"@rollup/rollup-linux-x64-musl": "4.22.4",
|
||||||
"@rollup/rollup-win32-arm64-msvc": "4.20.0",
|
"@rollup/rollup-win32-arm64-msvc": "4.22.4",
|
||||||
"@rollup/rollup-win32-ia32-msvc": "4.20.0",
|
"@rollup/rollup-win32-ia32-msvc": "4.22.4",
|
||||||
"@rollup/rollup-win32-x64-msvc": "4.20.0",
|
"@rollup/rollup-win32-x64-msvc": "4.22.4",
|
||||||
"fsevents": "~2.3.2"
|
"fsevents": "~2.3.2"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -4451,9 +4463,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/source-map-js": {
|
"node_modules/source-map-js": {
|
||||||
"version": "1.2.0",
|
"version": "1.2.1",
|
||||||
"resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.0.tgz",
|
"resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz",
|
||||||
"integrity": "sha512-itJW8lvSA0TXEphiRoawsCksnlf8SyvmFzIhltqAHluXd88pkCd+cXJVHTDwdCr0IzwptSm035IHQktUu1QUMg==",
|
"integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==",
|
||||||
"license": "BSD-3-Clause",
|
"license": "BSD-3-Clause",
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=0.10.0"
|
"node": ">=0.10.0"
|
||||||
@@ -4892,15 +4904,15 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/vite": {
|
"node_modules/vite": {
|
||||||
"version": "5.4.0",
|
"version": "5.4.7",
|
||||||
"resolved": "https://registry.npmjs.org/vite/-/vite-5.4.0.tgz",
|
"resolved": "https://registry.npmjs.org/vite/-/vite-5.4.7.tgz",
|
||||||
"integrity": "sha512-5xokfMX0PIiwCMCMb9ZJcMyh5wbBun0zUzKib+L65vAZ8GY9ePZMXxFrHbr/Kyll2+LSCY7xtERPpxkBDKngwg==",
|
"integrity": "sha512-5l2zxqMEPVENgvzTuBpHer2awaetimj2BGkhBPdnwKbPNOlHsODU+oiazEZzLK7KhAnOrO+XGYJYn4ZlUhDtDQ==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"esbuild": "^0.21.3",
|
"esbuild": "^0.21.3",
|
||||||
"postcss": "^8.4.40",
|
"postcss": "^8.4.43",
|
||||||
"rollup": "^4.13.0"
|
"rollup": "^4.20.0"
|
||||||
},
|
},
|
||||||
"bin": {
|
"bin": {
|
||||||
"vite": "bin/vite.js"
|
"vite": "bin/vite.js"
|
||||||
|
|||||||
@@ -22,6 +22,7 @@
|
|||||||
"amnet-shared": "file:../amnet-shared",
|
"amnet-shared": "file:../amnet-shared",
|
||||||
"class-variance-authority": "^0.7.0",
|
"class-variance-authority": "^0.7.0",
|
||||||
"clsx": "^2.1.1",
|
"clsx": "^2.1.1",
|
||||||
|
"core-js": "^3.39.0",
|
||||||
"detectincognitojs": "^1.3.5",
|
"detectincognitojs": "^1.3.5",
|
||||||
"dexie": "^4.0.8",
|
"dexie": "^4.0.8",
|
||||||
"dexie-react-hooks": "^1.1.7",
|
"dexie-react-hooks": "^1.1.7",
|
||||||
@@ -52,6 +53,6 @@
|
|||||||
"tailwindcss": "^3.4.9",
|
"tailwindcss": "^3.4.9",
|
||||||
"typescript": "^5.5.3",
|
"typescript": "^5.5.3",
|
||||||
"typescript-eslint": "^8.0.0",
|
"typescript-eslint": "^8.0.0",
|
||||||
"vite": "^5.4.0"
|
"vite": "^5.4.6"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,10 @@
|
|||||||
|
[
|
||||||
|
{
|
||||||
|
"relation": ["delegate_permission/common.handle_all_urls"],
|
||||||
|
"target": {
|
||||||
|
"namespace": "android_app",
|
||||||
|
"package_name": "moe.ppc.amnet",
|
||||||
|
"sha256_cert_fingerprints": ["9E:4C:42:6F:9E:27:85:62:E5:DB:75:23:60:4A:B9:8D:B5:39:C8:1D:AD:FE:8E:D3:DC:BA:6E:55:A9:C6:59:05"]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
@@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"id": "?pwa-home=1",
|
"id": "?pwa-home=1",
|
||||||
"name": "AimeNet",
|
"name": "AMNet",
|
||||||
"description": "Aime Card Switcher",
|
"description": "IC Card Switcher",
|
||||||
"start_url": "/",
|
"start_url": "/",
|
||||||
"categories": [
|
"categories": [
|
||||||
"utilities",
|
"utilities",
|
||||||
|
|||||||
@@ -8,20 +8,19 @@ import {AMCard, AMCardActionProps, AMCardDetail} from "@/components/am-card.tsx"
|
|||||||
|
|
||||||
export function AimeCardEntry(props: AMCardActionProps & { card: AimeCard, children?: Readonly<React.ReactNode> }) {
|
export function AimeCardEntry(props: AMCardActionProps & { card: AimeCard, children?: Readonly<React.ReactNode> }) {
|
||||||
const isDesktop = useMediaQuery("(min-width: 512px)");
|
const isDesktop = useMediaQuery("(min-width: 512px)");
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<AMCard title={props.card.name} actionIcon={props.actionIcon} actionClicked={props.actionClicked} actionDisabled={props.actionDisabled}>
|
<AMCard title={props.card.name} actionIcon={props.actionIcon} actionClicked={props.actionClicked}
|
||||||
|
actionDisabled={props.actionDisabled}>
|
||||||
{props.card.lastUsed > 0 && (
|
{props.card.lastUsed > 0 && (
|
||||||
<AMCardDetail>
|
<AMCardDetail icon={History}>
|
||||||
<History className="h-5 w-5"/>
|
|
||||||
<TimeAgo date={props.card.lastUsed}/>
|
<TimeAgo date={props.card.lastUsed}/>
|
||||||
</AMCardDetail>
|
</AMCardDetail>
|
||||||
)}
|
)}
|
||||||
<AMCardDetail>
|
<AMCardDetail icon={CreditCard} iconClicked={() => navigator.clipboard.writeText(props.card.id)}>
|
||||||
<CreditCard className="h-5 w-5 cursor-pointer" onClick={() => navigator.clipboard.writeText(props.card.id)}/>
|
<code>
|
||||||
<code>{isDesktop
|
{isDesktop
|
||||||
? props.card.id.match(/.{1,4}/g)?.join(" ")
|
? props.card.id.match(/.{1,4}/g)?.join(" ")
|
||||||
: `•••• ${props.card.id.substring(props.card.id.length - 4)}`}
|
: `•••• ${props.card.id.substring(props.card.id.length - 4)}`}
|
||||||
</code>
|
</code>
|
||||||
</AMCardDetail>
|
</AMCardDetail>
|
||||||
</AMCard>
|
</AMCard>
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import React from "react";
|
import React from "react";
|
||||||
import {ChevronRight} from "lucide-react";
|
import {ChevronRight, LucideIcon} from "lucide-react";
|
||||||
|
|
||||||
import {Card} from "@/components/ui/card.tsx";
|
import {Card} from "@/components/ui/card.tsx";
|
||||||
import {Button} from "@/components/ui/button.tsx";
|
import {Button} from "@/components/ui/button.tsx";
|
||||||
|
|
||||||
@@ -18,7 +19,7 @@ export function AMCard(props: AMCardActionProps & { title: string, titlePerforms
|
|||||||
onClick={() => props.titlePerformsAction && props.actionClicked ? props.actionClicked() : null}>
|
onClick={() => props.titlePerformsAction && props.actionClicked ? props.actionClicked() : null}>
|
||||||
{props.title}
|
{props.title}
|
||||||
</h4>
|
</h4>
|
||||||
<div className="text-sm lg:text-md flex flex-auto flex-wrap gap-3">
|
<div className="text-sm lg:text-md flex flex-auto flex-wrap gap-4">
|
||||||
{props.children}
|
{props.children}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -32,9 +33,10 @@ export function AMCard(props: AMCardActionProps & { title: string, titlePerforms
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
export function AMCardDetail(props: { children?: Readonly<React.ReactNode> }) {
|
export function AMCardDetail(props: { icon: LucideIcon, iconClicked?: () => void | Promise<any>, children?: Readonly<React.ReactNode> }) {
|
||||||
return (
|
return (
|
||||||
<div className="inline-flex items-center max-w-full gap-2 text-gray-500 select-none md:select-text">
|
<div className="inline-flex items-center max-w-full gap-2 text-gray-500 select-none md:select-text">
|
||||||
|
{React.createElement(props.icon, {className: "h-5 w-5", onClick: props.iconClicked})}
|
||||||
{props.children}
|
{props.children}
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -0,0 +1,45 @@
|
|||||||
|
import {useMemo} from "react";
|
||||||
|
import TimeAgo from "react-timeago";
|
||||||
|
import {getGameName} from "amnet-shared";
|
||||||
|
import {useNavigate} from "react-router-dom";
|
||||||
|
import {Earth, History, Joystick, Trash} from "lucide-react";
|
||||||
|
import {
|
||||||
|
ContextMenu,
|
||||||
|
ContextMenuContent,
|
||||||
|
ContextMenuItem,
|
||||||
|
ContextMenuSeparator,
|
||||||
|
ContextMenuTrigger
|
||||||
|
} from "@/components/ui/context-menu.tsx";
|
||||||
|
|
||||||
|
import {AimeServer, db} from "@/lib/database.ts";
|
||||||
|
import {AMCard, AMCardDetail} from "@/components/am-card.tsx";
|
||||||
|
|
||||||
|
export function AMServer(props: { server: AimeServer }) {
|
||||||
|
const navigate: (p: string) => void = useNavigate();
|
||||||
|
const gameName = useMemo(() => props.server.gameId ? getGameName(props.server.gameId) : null, [props.server.gameId]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<ContextMenu key={props.server.id}>
|
||||||
|
<ContextMenuTrigger>
|
||||||
|
<AMCard title={props.server.serverName} titlePerformsAction actionClicked={() => navigate(`/servers/${props.server.id}`)}>
|
||||||
|
<AMCardDetail icon={Earth}>
|
||||||
|
<code className="text-ellipsis whitespace-nowrap overflow-hidden">{props.server.serverAddress}</code>
|
||||||
|
</AMCardDetail>
|
||||||
|
<AMCardDetail icon={History}>
|
||||||
|
{props.server.lastConnected > 0 ? <TimeAgo date={props.server.lastConnected}/> :
|
||||||
|
<span>never</span>}
|
||||||
|
</AMCardDetail>
|
||||||
|
{gameName && <AMCardDetail icon={Joystick}>{gameName}</AMCardDetail>}
|
||||||
|
</AMCard>
|
||||||
|
</ContextMenuTrigger>
|
||||||
|
<ContextMenuContent>
|
||||||
|
<ContextMenuItem inset onClick={() => navigate(`/servers/${props.server.id}`)}>
|
||||||
|
Select Server
|
||||||
|
</ContextMenuItem>
|
||||||
|
<ContextMenuSeparator/>
|
||||||
|
<ContextMenuItem className="text-red-500" onClick={() => db.servers.delete(props.server.id)}>
|
||||||
|
<Trash className="mr-2 h-4 w-4"/> Remove Server
|
||||||
|
</ContextMenuItem>
|
||||||
|
</ContextMenuContent>
|
||||||
|
</ContextMenu>)
|
||||||
|
}
|
||||||
@@ -1,19 +1,31 @@
|
|||||||
import * as React from "react"
|
import * as React from "react";
|
||||||
|
|
||||||
export function useMediaQuery(query: string) {
|
export function useMediaQuery(query: string) {
|
||||||
const [value, setValue] = React.useState(false)
|
const [value, setValue] = React.useState(false);
|
||||||
|
|
||||||
React.useEffect(() => {
|
React.useEffect(() => {
|
||||||
function onChange(event: MediaQueryListEvent) {
|
function onChange(event: MediaQueryListEvent) {
|
||||||
setValue(event.matches)
|
setValue(event.matches);
|
||||||
}
|
}
|
||||||
|
|
||||||
const result = matchMedia(query)
|
const result = matchMedia(query);
|
||||||
result.addEventListener("change", onChange)
|
|
||||||
setValue(result.matches)
|
|
||||||
|
|
||||||
return () => result.removeEventListener("change", onChange)
|
// addEventListener isn't available on older versions of safari
|
||||||
}, [query])
|
if (result.addEventListener) {
|
||||||
|
result.addEventListener("change", onChange);
|
||||||
|
} else {
|
||||||
|
result.addListener(onChange);
|
||||||
|
}
|
||||||
|
setValue(result.matches);
|
||||||
|
|
||||||
return value
|
return () => {
|
||||||
|
if (result.removeEventListener) {
|
||||||
|
result.removeEventListener("change", onChange);
|
||||||
|
} else {
|
||||||
|
result.removeListener(onChange);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}, [query]);
|
||||||
|
|
||||||
|
return value;
|
||||||
}
|
}
|
||||||
@@ -1,9 +1,9 @@
|
|||||||
import Dexie, { type EntityTable } from 'dexie';
|
import Dexie, { type EntityTable } from 'dexie';
|
||||||
import {AMServerInfoResponse} from "amnet-shared";
|
import {AMServerConnectionInfo, AMServerMetadata} from "amnet-shared";
|
||||||
|
|
||||||
export const CARD_LIMIT = 15;
|
export const CARD_LIMIT = 15;
|
||||||
|
|
||||||
interface AimeServer extends AMServerInfoResponse {
|
interface AimeServer extends AMServerMetadata, AMServerConnectionInfo {
|
||||||
id: number;
|
id: number;
|
||||||
lastConnected: number;
|
lastConnected: number;
|
||||||
}
|
}
|
||||||
@@ -19,7 +19,7 @@ const db = new Dexie('amnet_db') as Dexie & {
|
|||||||
cards: EntityTable<AimeCard, 'id'>;
|
cards: EntityTable<AimeCard, 'id'>;
|
||||||
};
|
};
|
||||||
|
|
||||||
db.version(1).stores({
|
db.version(2).stores({
|
||||||
servers: '++id, serverAddress',
|
servers: '++id, serverAddress',
|
||||||
cards: 'id'
|
cards: 'id'
|
||||||
});
|
});
|
||||||
|
|||||||
+11
-1
@@ -1,5 +1,10 @@
|
|||||||
import './index.css'
|
import './index.css'
|
||||||
|
|
||||||
|
// import polyfills from core-js for older browsers
|
||||||
|
import 'core-js/actual/promise';
|
||||||
|
import 'core-js/actual/symbol';
|
||||||
|
import 'core-js/actual/object/assign';
|
||||||
|
|
||||||
import {StrictMode} from "react";
|
import {StrictMode} from "react";
|
||||||
import {createRoot} from "react-dom/client";
|
import {createRoot} from "react-dom/client";
|
||||||
import {createHashRouter, Navigate, RouterProvider} from "react-router-dom";
|
import {createHashRouter, Navigate, RouterProvider} from "react-router-dom";
|
||||||
@@ -10,6 +15,7 @@ import {ThemeProvider} from "@/components/theme-provider.tsx";
|
|||||||
import CardManager from "@/pages/card-manager.tsx";
|
import CardManager from "@/pages/card-manager.tsx";
|
||||||
import ServerCardIn from "@/pages/card-in-screen.tsx";
|
import ServerCardIn from "@/pages/card-in-screen.tsx";
|
||||||
import ServerListing from "@/pages/server-listing.tsx";
|
import ServerListing from "@/pages/server-listing.tsx";
|
||||||
|
import Privacy from "@/pages/privacy.tsx";
|
||||||
|
|
||||||
// iOS PWAs show the navigation bar when changing routes
|
// iOS PWAs show the navigation bar when changing routes
|
||||||
// HashRouter doesn't have this issue as the URL never changes
|
// HashRouter doesn't have this issue as the URL never changes
|
||||||
@@ -33,7 +39,11 @@ const router = createHashRouter([
|
|||||||
path: x,
|
path: x,
|
||||||
element: <CardManager/>
|
element: <CardManager/>
|
||||||
};
|
};
|
||||||
})
|
}),
|
||||||
|
{
|
||||||
|
path: "/privacy",
|
||||||
|
element: <Privacy/>
|
||||||
|
}
|
||||||
])
|
])
|
||||||
|
|
||||||
createRoot(document.getElementById('root')!).render(
|
createRoot(document.getElementById('root')!).render(
|
||||||
|
|||||||
@@ -1,10 +1,8 @@
|
|||||||
import {useLiveQuery} from "dexie-react-hooks";
|
import {useLiveQuery} from "dexie-react-hooks";
|
||||||
import {useEffect, useMemo, useState} from "react";
|
import {useEffect, useMemo, useState} from "react";
|
||||||
import {useNavigate, useParams} from "react-router-dom";
|
import {useNavigate, useParams} from "react-router-dom";
|
||||||
import {CircleDot, CircleCheckBig, Home, Nfc, Wallet, WifiOff} from "lucide-react";
|
import {performCardIn, fetchServerInfo, getGameName, AMServerInfo} from "amnet-shared";
|
||||||
|
import {CircleDot, CircleCheckBig, Home, Nfc, Wallet, WifiOff, TvMinimalPlay} from "lucide-react";
|
||||||
import {AimeCard, AimeServer, db} from "@/lib/database";
|
|
||||||
import {performCardIn, fetchServerInfo} from "amnet-shared";
|
|
||||||
|
|
||||||
import {Button} from "@/components/ui/button";
|
import {Button} from "@/components/ui/button";
|
||||||
import {useToast} from "@/components/ui/use-toast";
|
import {useToast} from "@/components/ui/use-toast";
|
||||||
@@ -21,6 +19,8 @@ import {
|
|||||||
AlertDialogTrigger
|
AlertDialogTrigger
|
||||||
} from "@/components/ui/alert-dialog.tsx";
|
} from "@/components/ui/alert-dialog.tsx";
|
||||||
|
|
||||||
|
import {AimeCard, AimeServer, db} from "@/lib/database";
|
||||||
|
|
||||||
import {AMHeader} from "@/components/am-header";
|
import {AMHeader} from "@/components/am-header";
|
||||||
import {AMFooter} from "@/components/am-footer";
|
import {AMFooter} from "@/components/am-footer";
|
||||||
import {Separator} from "@/components/ui/separator";
|
import {Separator} from "@/components/ui/separator";
|
||||||
@@ -38,7 +38,7 @@ async function performCardInAction(card: AimeCard, server: AimeServer, setCardIn
|
|||||||
const abort = new AbortController();
|
const abort = new AbortController();
|
||||||
setTimeout(() => abort.abort("timeout"), 10000);
|
setTimeout(() => abort.abort("timeout"), 10000);
|
||||||
|
|
||||||
await performCardIn(server.serverAddress, {cardId: card.id}, abort.signal);
|
await performCardIn(server, {cardId: card.id}, abort.signal);
|
||||||
await db.cards.update(card.id, {lastUsed: Date.now()});
|
await db.cards.update(card.id, {lastUsed: Date.now()});
|
||||||
|
|
||||||
toaster({
|
toaster({
|
||||||
@@ -60,8 +60,6 @@ async function performCardInAction(card: AimeCard, server: AimeServer, setCardIn
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/* eslint-enable @typescript-eslint/no-explicit-any */
|
|
||||||
|
|
||||||
function CardList(props: { cards: ReadonlyArray<AimeCard> | null, cardSelected: (card: AimeCard) => void }) {
|
function CardList(props: { cards: ReadonlyArray<AimeCard> | null, cardSelected: (card: AimeCard) => void }) {
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
|
|
||||||
@@ -82,6 +80,39 @@ function CardList(props: { cards: ReadonlyArray<AimeCard> | null, cardSelected:
|
|||||||
</>);
|
</>);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function AlertPanel(props: { server: AMServerInfo | null, connectionError: boolean, onReloadRequested: () => void}) {
|
||||||
|
if (!props.server) {
|
||||||
|
return <LoadingIndicator/>
|
||||||
|
}
|
||||||
|
|
||||||
|
if (props.connectionError) {
|
||||||
|
return <Alert variant="destructive">
|
||||||
|
<WifiOff className="h-4 w-4"/>
|
||||||
|
<AlertTitle>Connection Issue</AlertTitle>
|
||||||
|
<AlertDescription>
|
||||||
|
There was an issue connecting to <code
|
||||||
|
className="max-w-full text-wrap">{props.server.serverAddress}</code>.
|
||||||
|
Please check the game is running and <span className="underline cursor-pointer"
|
||||||
|
onClick={props.onReloadRequested}>try again</span>.
|
||||||
|
</AlertDescription>
|
||||||
|
</Alert>
|
||||||
|
}
|
||||||
|
|
||||||
|
if (props.server.timeSinceLastPoll === null) {
|
||||||
|
return <Alert variant="blank" className="text-violet-600 border-violet-600">
|
||||||
|
<TvMinimalPlay className="h-4 w-4"/>
|
||||||
|
<AlertTitle>Server Startup</AlertTitle>
|
||||||
|
<AlertDescription>
|
||||||
|
Scanning a card below may not have any effect until the service has fully started up. <span
|
||||||
|
className="underline cursor-pointer"
|
||||||
|
onClick={props.onReloadRequested}>reload this page</span> to perform another check.
|
||||||
|
</AlertDescription>
|
||||||
|
</Alert>
|
||||||
|
}
|
||||||
|
|
||||||
|
return <IncognitoStorageWarning/>
|
||||||
|
}
|
||||||
|
|
||||||
function ServerCardInPage() {
|
function ServerCardInPage() {
|
||||||
const {toast} = useToast();
|
const {toast} = useToast();
|
||||||
const {serverId} = useParams<{ serverId: string }>();
|
const {serverId} = useParams<{ serverId: string }>();
|
||||||
@@ -98,6 +129,7 @@ function ServerCardInPage() {
|
|||||||
const [connectionError, setConnectionError] = useState(false);
|
const [connectionError, setConnectionError] = useState(false);
|
||||||
|
|
||||||
const shareUrl = useMemo(() => `${location.origin}/#/servers/add?address=${encodeURIComponent(server?.serverAddress ?? "")}`, [server]);
|
const shareUrl = useMemo(() => `${location.origin}/#/servers/add?address=${encodeURIComponent(server?.serverAddress ?? "")}`, [server]);
|
||||||
|
const gameName = useMemo(() => server?.gameId ? getGameName(server.gameId) : null, [server]);
|
||||||
|
|
||||||
// select newest card
|
// select newest card
|
||||||
useEffect(() => setSelectedCard(cards?.sort((a, b) => b.lastUsed - a.lastUsed)[0] ?? null), [cards]);
|
useEffect(() => setSelectedCard(cards?.sort((a, b) => b.lastUsed - a.lastUsed)[0] ?? null), [cards]);
|
||||||
@@ -152,25 +184,12 @@ function ServerCardInPage() {
|
|||||||
</Button>
|
</Button>
|
||||||
</AMHeader>
|
</AMHeader>
|
||||||
|
|
||||||
{!server && (<LoadingIndicator/>)}
|
<AlertPanel server={server} connectionError={connectionError} onReloadRequested={() => setServerReloadCounter(serverReloadCounter + 1)}/>
|
||||||
{(server && !connectionError) && <IncognitoStorageWarning/>}
|
|
||||||
{(server && connectionError) && (
|
|
||||||
<Alert variant="destructive">
|
|
||||||
<WifiOff className="h-4 w-4"/>
|
|
||||||
<AlertTitle>Connection Issue</AlertTitle>
|
|
||||||
<AlertDescription>
|
|
||||||
There was an issue connecting to <code
|
|
||||||
className="max-w-full text-wrap">{server.serverAddress}</code>.
|
|
||||||
Please check the game is running and <span className="underline cursor-pointer"
|
|
||||||
onClick={() => setServerReloadCounter(serverReloadCounter + 1)}>try again</span>.
|
|
||||||
</AlertDescription>
|
|
||||||
</Alert>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{server && (
|
{server && (
|
||||||
<div className="flex flex-col gap-4 w-full flex-grow">
|
<div className="flex flex-col gap-4 w-full flex-grow">
|
||||||
{selectedCard && (<>
|
{selectedCard && (<>
|
||||||
<h4 className="text-2xl font-bold">Selected Card</h4>
|
<h4 className="text-2xl font-bold">{gameName ? `Play ${gameName}` : "Selected Card"}</h4>
|
||||||
|
|
||||||
<AimeCardEntry card={selectedCard}>
|
<AimeCardEntry card={selectedCard}>
|
||||||
<Button size="icon" variant="ghost" disabled>
|
<Button size="icon" variant="ghost" disabled>
|
||||||
@@ -198,8 +217,7 @@ function ServerCardInPage() {
|
|||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
<AMFooter
|
<AMFooter className="flex flex-col-reverse sm:flex-row sm:flex-wrap justify-center sm:justify-between sm:gap-2 gap-4">
|
||||||
className="flex flex-col-reverse sm:flex-row sm:flex-wrap justify-center sm:justify-between sm:gap-2 gap-4">
|
|
||||||
{server?.id && (
|
{server?.id && (
|
||||||
<div className="flex items-center justify-center sm:gap-5 gap-2">
|
<div className="flex items-center justify-center sm:gap-5 gap-2">
|
||||||
{location.protocol === "https:" && (<>
|
{location.protocol === "https:" && (<>
|
||||||
@@ -252,4 +270,4 @@ function ServerCardInPage() {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export default ServerCardInPage;
|
export default ServerCardInPage;
|
||||||
|
|||||||
@@ -0,0 +1,39 @@
|
|||||||
|
// Google Play forces a Privacy Policy even for closed tests...
|
||||||
|
|
||||||
|
import {Server} from "lucide-react";
|
||||||
|
import {useNavigate} from "react-router-dom";
|
||||||
|
|
||||||
|
import {Button} from "@/components/ui/button.tsx";
|
||||||
|
import {AMFooter} from "@/components/am-footer.tsx";
|
||||||
|
import {AMHeader} from "@/components/am-header.tsx";
|
||||||
|
|
||||||
|
function PrivacyPolicy() {
|
||||||
|
const navigate = useNavigate();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<main className="flex flex-col gap-5 container min-h-screen py-10">
|
||||||
|
<AMHeader title="AMNet">
|
||||||
|
<Button variant="ghost" size={"default"} onClick={() => navigate('/')}>
|
||||||
|
<Server className="h-4 w-4 sm:mr-2"/>
|
||||||
|
<span className="hidden sm:block">Manage Servers</span>
|
||||||
|
</Button>
|
||||||
|
</AMHeader>
|
||||||
|
|
||||||
|
<div className="flex flex-col gap-4 w-full flex-grow">
|
||||||
|
<h4 className="font-semibold text-2xl">Privacy</h4>
|
||||||
|
<p>The AMNet card switcher collects and stores the following information locally (on-device):</p>
|
||||||
|
<ul className="list-disc">
|
||||||
|
<li>Server Addresses</li>
|
||||||
|
<li>Server Information (including machine name, identifiers and metrics that are cached and updated on each load)</li>
|
||||||
|
<li>Card Names and Access Codes</li>
|
||||||
|
</ul>
|
||||||
|
|
||||||
|
<p>The card switcher does <strong>NOT</strong> send any data to external servers or services, except to perform the card-in action where only the card number is transmitted.</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<AMFooter/>
|
||||||
|
</main>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default PrivacyPolicy;
|
||||||
@@ -1,21 +1,19 @@
|
|||||||
import {z} from "zod";
|
import {z} from "zod";
|
||||||
import TimeAgo from "react-timeago";
|
|
||||||
import {useForm} from "react-hook-form";
|
import {useForm} from "react-hook-form";
|
||||||
import {useLiveQuery} from "dexie-react-hooks";
|
import {useLiveQuery} from "dexie-react-hooks";
|
||||||
|
import {AMNET_API_VERSION} from "amnet-shared";
|
||||||
import React, {useEffect, useState} from "react";
|
import React, {useEffect, useState} from "react";
|
||||||
import {useLocation, useNavigate} from 'react-router-dom';
|
import {useLocation, useNavigate} from 'react-router-dom';
|
||||||
import {zodResolver} from "@hookform/resolvers/zod";
|
|
||||||
import {
|
import {
|
||||||
Earth,
|
Earth,
|
||||||
History,
|
|
||||||
Plus,
|
Plus,
|
||||||
ShieldAlert,
|
ShieldAlert,
|
||||||
SquareArrowOutUpRight,
|
SquareArrowOutUpRight,
|
||||||
Trash,
|
|
||||||
WalletCards
|
WalletCards
|
||||||
} from "lucide-react";
|
} from "lucide-react";
|
||||||
|
|
||||||
import {AimeServer, db} from "@/lib/database";
|
import {AimeServer, db} from "@/lib/database";
|
||||||
|
import {zodResolver} from "@hookform/resolvers/zod";
|
||||||
|
|
||||||
import {Input} from "@/components/ui/input";
|
import {Input} from "@/components/ui/input";
|
||||||
import {Button} from "@/components/ui/button";
|
import {Button} from "@/components/ui/button";
|
||||||
@@ -23,26 +21,38 @@ import {LoadingIndicator} from "@/components/loading-indicator";
|
|||||||
import {Alert, AlertDescription, AlertTitle} from "@/components/ui/alert";
|
import {Alert, AlertDescription, AlertTitle} from "@/components/ui/alert";
|
||||||
import {Form, FormControl, FormField, FormItem, FormMessage} from "@/components/ui/form";
|
import {Form, FormControl, FormField, FormItem, FormMessage} from "@/components/ui/form";
|
||||||
import {HttpsMixedModeRequestWarning} from "@/components/https-mixed-mode-request-warning";
|
import {HttpsMixedModeRequestWarning} from "@/components/https-mixed-mode-request-warning";
|
||||||
import {
|
|
||||||
ContextMenu,
|
|
||||||
ContextMenuContent,
|
|
||||||
ContextMenuItem,
|
|
||||||
ContextMenuSeparator,
|
|
||||||
ContextMenuTrigger
|
|
||||||
} from "@/components/ui/context-menu";
|
|
||||||
|
|
||||||
import {AMFooter} from "@/components/am-footer";
|
import {AMFooter} from "@/components/am-footer";
|
||||||
import {AMHeader} from "@/components/am-header";
|
import {AMHeader} from "@/components/am-header";
|
||||||
import {AMNET_API_VERSION} from "amnet-shared";
|
import {AMServer} from "@/components/am-server.tsx";
|
||||||
import {AMCard, AMCardDetail} from "@/components/am-card";
|
|
||||||
|
|
||||||
const serverAdditionSchema = z.object({
|
const serverAdditionSchema = z.object({
|
||||||
address: z.string().startsWith("http")
|
address: z.string().startsWith("http")
|
||||||
});
|
});
|
||||||
|
|
||||||
function ServerList(props: { servers: AimeServer[] | undefined }) {
|
function ServerListing() {
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
|
const servers = useLiveQuery(() => db.servers.toArray());
|
||||||
|
|
||||||
|
return (
|
||||||
|
<main className="flex flex-col gap-5 container min-h-screen py-10">
|
||||||
|
<AMHeader title="Servers">
|
||||||
|
<Button variant="ghost" size={"default"} onClick={() => navigate('/cards')}>
|
||||||
|
<WalletCards className="h-4 w-4 sm:mr-2"/>
|
||||||
|
<span className="hidden sm:block">Manage Cards</span>
|
||||||
|
</Button>
|
||||||
|
</AMHeader>
|
||||||
|
|
||||||
|
<div className="flex flex-col gap-4 w-full flex-grow">
|
||||||
|
<ServerList servers={servers}/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<AMFooter/>
|
||||||
|
</main>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function ServerList(props: { servers: AimeServer[] | undefined }) {
|
||||||
if (!props.servers) {
|
if (!props.servers) {
|
||||||
return (<LoadingIndicator/>);
|
return (<LoadingIndicator/>);
|
||||||
}
|
}
|
||||||
@@ -65,32 +75,7 @@ function ServerList(props: { servers: AimeServer[] | undefined }) {
|
|||||||
<HttpsMixedModeRequestWarning/>
|
<HttpsMixedModeRequestWarning/>
|
||||||
<ServerAddForm/>
|
<ServerAddForm/>
|
||||||
|
|
||||||
{props.servers.map(s =>
|
{props.servers.map(s => <AMServer key={s.id} server={s}/>)}
|
||||||
<ContextMenu key={s.id}>
|
|
||||||
<ContextMenuTrigger>
|
|
||||||
<AMCard title={s.serverName} titlePerformsAction actionClicked={() => navigate(`/servers/${s.id}`)}>
|
|
||||||
{s.lastConnected > 0 && (
|
|
||||||
<AMCardDetail>
|
|
||||||
<History className="h-5 w-5"/>
|
|
||||||
<TimeAgo date={s.lastConnected}/>
|
|
||||||
</AMCardDetail>
|
|
||||||
)}
|
|
||||||
<AMCardDetail>
|
|
||||||
<Earth className="h-5 w-5"/>
|
|
||||||
<code className="text-ellipsis whitespace-nowrap overflow-hidden">{s.serverAddress}</code>
|
|
||||||
</AMCardDetail>
|
|
||||||
</AMCard>
|
|
||||||
</ContextMenuTrigger>
|
|
||||||
<ContextMenuContent>
|
|
||||||
<ContextMenuItem inset onClick={() => navigate(`/servers/${s.id}`)}>
|
|
||||||
Select Server
|
|
||||||
</ContextMenuItem>
|
|
||||||
<ContextMenuSeparator/>
|
|
||||||
<ContextMenuItem className="text-red-500" onClick={() => db.servers.delete(s.id)}>
|
|
||||||
<Trash className="mr-2 h-4 w-4"/> Remove Server
|
|
||||||
</ContextMenuItem>
|
|
||||||
</ContextMenuContent>
|
|
||||||
</ContextMenu>)}
|
|
||||||
</>);
|
</>);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -224,26 +209,4 @@ function ServerAddForm() {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function ServerListing() {
|
|
||||||
const navigate = useNavigate();
|
|
||||||
const servers = useLiveQuery(() => db.servers.toArray());
|
|
||||||
|
|
||||||
return (
|
|
||||||
<main className="flex flex-col gap-5 container min-h-screen py-10">
|
|
||||||
<AMHeader title="Servers">
|
|
||||||
<Button variant="ghost" size={"default"} onClick={() => navigate('/cards')}>
|
|
||||||
<WalletCards className="h-4 w-4 sm:mr-2"/>
|
|
||||||
<span className="hidden sm:block">Manage Cards</span>
|
|
||||||
</Button>
|
|
||||||
</AMHeader>
|
|
||||||
|
|
||||||
<div className="flex flex-col gap-4 w-full flex-grow">
|
|
||||||
<ServerList servers={servers}/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<AMFooter/>
|
|
||||||
</main>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
export default ServerListing;
|
export default ServerListing;
|
||||||
|
|||||||
@@ -7,7 +7,7 @@
|
|||||||
"DOM",
|
"DOM",
|
||||||
"DOM.Iterable"
|
"DOM.Iterable"
|
||||||
],
|
],
|
||||||
"module": "ESNext",
|
"module": "ES2020",
|
||||||
"skipLibCheck": true,
|
"skipLibCheck": true,
|
||||||
"baseUrl": ".",
|
"baseUrl": ".",
|
||||||
"paths": {
|
"paths": {
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ export default defineConfig({
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
build: {
|
build: {
|
||||||
|
target: "ES2018",
|
||||||
rollupOptions: {
|
rollupOptions: {
|
||||||
output: {
|
output: {
|
||||||
manualChunks(id: string) {
|
manualChunks(id: string) {
|
||||||
@@ -21,6 +22,10 @@ export default defineConfig({
|
|||||||
if (id.includes('react')) {
|
if (id.includes('react')) {
|
||||||
return 'react';
|
return 'react';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (id.includes('core-js')) {
|
||||||
|
return "polyfills";
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,29 @@
|
|||||||
|
diff --git a/amnet-native/android/app/build.gradle b/amnet-native/android/app/build.gradle
|
||||||
|
index 6ee4a34..eb8c5f0 100644
|
||||||
|
--- a/amnet-native/android/app/build.gradle
|
||||||
|
+++ b/amnet-native/android/app/build.gradle
|
||||||
|
@@ -124,15 +124,21 @@ android {
|
||||||
|
keyAlias 'androiddebugkey'
|
||||||
|
keyPassword 'android'
|
||||||
|
}
|
||||||
|
+ release {
|
||||||
|
+ if (project.hasProperty('AMNET_UPLOAD_STORE_FILE')) {
|
||||||
|
+ storeFile file(AMNET_UPLOAD_STORE_FILE)
|
||||||
|
+ storePassword AMNET_UPLOAD_STORE_PASSWORD
|
||||||
|
+ keyAlias AMNET_UPLOAD_KEY_ALIAS
|
||||||
|
+ keyPassword AMNET_UPLOAD_KEY_PASSWORD
|
||||||
|
+ }
|
||||||
|
+ }
|
||||||
|
}
|
||||||
|
buildTypes {
|
||||||
|
debug {
|
||||||
|
signingConfig signingConfigs.debug
|
||||||
|
}
|
||||||
|
release {
|
||||||
|
- // Caution! In production, you need to generate your own keystore file.
|
||||||
|
- // see https://reactnative.dev/docs/signed-apk-android.
|
||||||
|
- signingConfig signingConfigs.debug
|
||||||
|
+ signingConfig signingConfigs.release
|
||||||
|
shrinkResources (findProperty('android.enableShrinkResourcesInReleaseBuilds')?.toBoolean() ?: false)
|
||||||
|
minifyEnabled enableProguardInReleaseBuilds
|
||||||
|
proguardFiles getDefaultProguardFile("proguard-android.txt"), "proguard-rules.pro"
|
||||||
@@ -25,9 +25,11 @@ path=amnet.dll
|
|||||||
; gameId - the 4 character game ID (will show the game icon in the UI)
|
; gameId - the 4 character game ID (will show the game icon in the UI)
|
||||||
; serverName - the name of the server to show to users (defaults to the computer name if not set)
|
; serverName - the name of the server to show to users (defaults to the computer name if not set)
|
||||||
; serverAddress - the address to listen on, including the protocol (defaults to http://+:6070, where + is a wildcard)
|
; serverAddress - the address to listen on, including the protocol (defaults to http://+:6070, where + is a wildcard)
|
||||||
|
; enableKeyboardMode - whether to enable hold-enter-to-login mode. Reads the file set at aimePath for card number (or DEVICE/aime.txt if not set).
|
||||||
gameId=SDEZ
|
gameId=SDEZ
|
||||||
;serverName=CHUNI-PENGUIN
|
;serverName=CHUNI-PENGUIN
|
||||||
;serverAddress=http://+:6070
|
;serverAddress=http://+:6070
|
||||||
|
;enableKeyboardMode=1
|
||||||
```
|
```
|
||||||
4. Launch the game, and check the amdaemon console for server startup messages
|
4. Launch the game, and check the amdaemon console for server startup messages
|
||||||
5. Access the using one of the interfaces (see below), connect to the running server, add a card and start playing!
|
5. Access the using one of the interfaces (see below), connect to the running server, add a card and start playing!
|
||||||
|
|||||||
Reference in New Issue
Block a user