mirror of
https://gitea.tendokyu.moe/ppc/amnet.git
synced 2026-10-05 21:28:08 +03:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
0d05a972ef | ||
|
|
3ef3c2937b | ||
|
|
6019eb1920 | ||
|
|
95f066817f | ||
|
|
bff0416f00 | ||
|
|
701a92dea5 | ||
|
|
3f17f9b432 | ||
|
|
6146f1c409 | ||
|
|
9b2c62c035 | ||
|
|
e77f401e0c | ||
|
|
e4353fca97 | ||
|
|
27693cda7c | ||
|
|
fdf8a248e3 | ||
|
|
041050dba8 | ||
|
|
572f5ed672 | ||
|
|
ded0109486 | ||
|
|
0b44d9bf84 | ||
|
|
0ee84c168e | ||
|
|
3cae734dbd | ||
|
|
59d1a8bd3e | ||
|
|
6630496280 | ||
|
|
d078bea395 | ||
|
|
8c5ba567e4 | ||
|
|
46b5f1ab16 | ||
|
|
d7a5911e64 | ||
|
|
c1876cf4bf | ||
|
|
3ac5fe4c78 | ||
|
|
857e718dfd | ||
|
|
dcc8cd365e | ||
|
|
a9d6d36eea | ||
|
|
4a7aea406a | ||
|
|
d043f57ad0 | ||
|
|
5c1360ecdc | ||
|
|
2125753e37 | ||
|
|
96a9e2a498 | ||
|
|
563362eea5 | ||
|
|
839dfe6e00 | ||
|
|
a03abbde4e | ||
|
|
9bf1602d5d | ||
|
|
1075253573 | ||
|
|
6c692b71fa | ||
|
|
8021ee3a7a | ||
|
|
622f87242a | ||
|
|
8bceb4d422 | ||
|
|
1ce3cea61e | ||
|
|
63847543ca | ||
|
|
81ca4a73f9 | ||
|
|
5dc7880c58 | ||
|
|
d708c6e2c5 | ||
|
|
e2cd595e8e | ||
|
|
88cc6833f2 | ||
|
|
e5e158f8d2 | ||
|
|
f032fc248a | ||
|
|
130e4c3789 | ||
|
|
ab766de83a | ||
|
|
889e492bb0 | ||
|
|
97d0a0e437 | ||
|
|
591680d521 | ||
|
|
1d0e40961c | ||
|
|
435dbad3a5 | ||
|
|
f0125be89d | ||
|
|
3511efcc04 | ||
|
|
0edd7e13fa | ||
|
|
95be807720 | ||
|
|
78668a1b6a | ||
|
|
d917f8832a | ||
|
|
e39a117018 | ||
|
|
40fbf475f4 | ||
|
|
4e69f9a4b1 | ||
|
|
672e44593f | ||
|
|
f0d6bd6c95 | ||
|
|
83de0a558b | ||
|
|
2e831831a2 | ||
|
|
318e113445 | ||
|
|
c6f9251cf2 | ||
|
|
faf688fc0b | ||
|
|
1b73b60efa | ||
|
|
f2550a84e0 | ||
|
|
71f343e9b5 | ||
|
|
270c65cddb | ||
|
|
21766ae449 | ||
|
|
0bb34f04be | ||
|
|
1e5e8b3a4d | ||
|
|
805af19006 | ||
|
|
7f51e4a0ee | ||
|
|
4d43943568 | ||
|
|
d8d29c3f84 | ||
|
|
0a204b249a | ||
|
|
ae7483b156 | ||
|
|
5bb477741d | ||
|
|
3fbea0f925 | ||
|
|
2bc9c9f673 | ||
|
|
5b19207a91 | ||
|
|
bc48917fe8 | ||
|
|
13cee9a431 | ||
|
|
874ba06e29 | ||
|
|
16c0ff54c6 | ||
|
|
73d333cc23 | ||
|
|
393965d252 | ||
|
|
29b303cde8 | ||
|
|
e83287c7b7 | ||
|
|
9324b30e7b | ||
|
|
d0c7616ed1 | ||
|
|
99fd999c06 | ||
|
|
0a42a0a912 | ||
|
|
a6ad82ffda | ||
|
|
31c56d2449 | ||
|
|
b33670d849 | ||
|
|
9c8c4fd286 | ||
|
|
0a80da6614 | ||
|
|
489b56015c | ||
|
|
89338c794a | ||
|
|
aae5039660 | ||
|
|
690a5b591c | ||
|
|
f9b2326853 | ||
|
|
b2be622271 | ||
|
|
e0ba6c04f2 | ||
|
|
6c7479225c | ||
|
|
42cb241487 | ||
|
|
297ad685a7 | ||
|
|
d52757f134 | ||
|
|
a4d4e03eb9 | ||
|
|
3e3755471d | ||
|
|
adb60dd928 | ||
|
|
c8763dd65e |
@@ -0,0 +1,8 @@
|
|||||||
|
**/node_modules
|
||||||
|
**/dist
|
||||||
|
**/*.tsbuildinfo
|
||||||
|
.git
|
||||||
|
target
|
||||||
|
amnet-native
|
||||||
|
amnet-server-test
|
||||||
|
res
|
||||||
@@ -1,9 +1,6 @@
|
|||||||
name: Release
|
name: Release
|
||||||
|
|
||||||
on:
|
on:
|
||||||
# push:
|
|
||||||
# tags:
|
|
||||||
# - '^v?(?:[0-9]+.?){1,4}$'
|
|
||||||
workflow_dispatch:
|
workflow_dispatch:
|
||||||
inputs:
|
inputs:
|
||||||
tag:
|
tag:
|
||||||
@@ -33,26 +30,31 @@ jobs:
|
|||||||
needs: get-upload-url
|
needs: get-upload-url
|
||||||
name: Build amnet-server
|
name: Build amnet-server
|
||||||
|
|
||||||
env:
|
strategy:
|
||||||
DOTNET_CLI_TELEMETRY_OPTOUT: 1
|
matrix:
|
||||||
|
include:
|
||||||
|
- { target: "x86_64-pc-windows-msvc", filename: "amnet.dll", include_test_server: true }
|
||||||
|
- { target: "i686-pc-windows-msvc", filename: "amnet_x86.dll", include_test_server: false }
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@v4
|
||||||
- uses: actions/setup-dotnet@v4
|
|
||||||
with:
|
- name: Set target
|
||||||
dotnet-version: '9.0.x'
|
|
||||||
|
|
||||||
- name: Set version
|
|
||||||
run: |
|
run: |
|
||||||
$tag="${{ github.event.inputs.tag || github.ref_name }}".TrimStart("v")
|
rustup target add ${{ matrix.target }}
|
||||||
echo "VERSION=$tag" >> $env:GITHUB_ENV
|
rustup set default-host ${{ matrix.target }}
|
||||||
|
|
||||||
- name: Publish
|
- name: Perform build
|
||||||
run: dotnet publish -c Release -r win-x64 -o .\build-out -p:Version=$env:VERSION AMNet.Server\AMNet.Server.csproj
|
run: cargo build --release
|
||||||
|
|
||||||
- name: Upload artifact
|
- name: Upload .dll
|
||||||
run: |
|
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"
|
curl -s -X POST -H "Authorization: token ${{ secrets.GITEA_TOKEN }}" -H "Content-Type: application/octet-stream" --data-binary "@.\amnet-server\target\release\amnet.dll" "${{ needs.get-upload-url.outputs.upload_url }}?name=${{ matrix.filename }}"
|
||||||
|
|
||||||
|
- name: Upload test server
|
||||||
|
if: "matrix.include_test_server"
|
||||||
|
run: |
|
||||||
|
curl -s -X POST -H "Authorization: token ${{ secrets.GITEA_TOKEN }}" -H "Content-Type: application/octet-stream" --data-binary "@.\amnet-server\target\release\amnet-server-test.exe" "${{ needs.get-upload-url.outputs.upload_url }}?name=amnet-server.exe"
|
||||||
|
|
||||||
build-website:
|
build-website:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
|
|||||||
+16
-121
@@ -1,126 +1,6 @@
|
|||||||
## Ignore Visual Studio temporary files, build results, and
|
|
||||||
## files generated by popular Visual Studio add-ons.
|
|
||||||
##
|
|
||||||
## Get latest from https://github.com/github/gitignore/blob/main/VisualStudio.gitignore
|
|
||||||
|
|
||||||
# User-specific files
|
|
||||||
*.rsuser
|
|
||||||
*.suo
|
|
||||||
*.user
|
|
||||||
*.userosscache
|
|
||||||
*.sln.docstates
|
|
||||||
|
|
||||||
# User-specific files (MonoDevelop/Xamarin Studio)
|
|
||||||
*.userprefs
|
|
||||||
|
|
||||||
# Mono auto generated files
|
|
||||||
mono_crash.*
|
|
||||||
|
|
||||||
# Build results
|
|
||||||
[Dd]ebug/
|
|
||||||
[Dd]ebugPublic/
|
|
||||||
[Rr]elease/
|
|
||||||
[Rr]eleases/
|
|
||||||
x64/
|
|
||||||
x86/
|
|
||||||
[Ww][Ii][Nn]32/
|
|
||||||
[Aa][Rr][Mm]/
|
|
||||||
[Aa][Rr][Mm]64/
|
|
||||||
bld/
|
|
||||||
[Bb]in/
|
|
||||||
[Oo]bj/
|
|
||||||
[Ll]og/
|
|
||||||
[Ll]ogs/
|
|
||||||
|
|
||||||
# Visual Studio 2015/2017 cache/options directory
|
|
||||||
.vs/
|
|
||||||
# Uncomment if you have tasks that create the project's static files in wwwroot
|
|
||||||
#wwwroot/
|
|
||||||
|
|
||||||
# Visual Studio 2017 auto generated files
|
|
||||||
Generated\ Files/
|
|
||||||
|
|
||||||
# MSTest test Results
|
|
||||||
[Tt]est[Rr]esult*/
|
|
||||||
[Bb]uild[Ll]og.*
|
|
||||||
|
|
||||||
# NUnit
|
|
||||||
*.VisualState.xml
|
|
||||||
TestResult.xml
|
|
||||||
nunit-*.xml
|
|
||||||
|
|
||||||
# Benchmark Results
|
|
||||||
BenchmarkDotNet.Artifacts/
|
|
||||||
|
|
||||||
# .NET Core
|
|
||||||
project.lock.json
|
|
||||||
project.fragment.lock.json
|
|
||||||
artifacts/
|
|
||||||
|
|
||||||
# StyleCop
|
|
||||||
StyleCopReport.xml
|
|
||||||
|
|
||||||
# Files built by Visual Studio
|
|
||||||
*_i.c
|
|
||||||
*_p.c
|
|
||||||
*_h.h
|
|
||||||
*.ilk
|
|
||||||
*.meta
|
|
||||||
*.obj
|
|
||||||
*.iobj
|
|
||||||
*.pch
|
|
||||||
*.pdb
|
|
||||||
*.ipdb
|
|
||||||
*.pgc
|
|
||||||
*.pgd
|
|
||||||
*.rsp
|
|
||||||
*.sbr
|
|
||||||
*.tlb
|
|
||||||
*.tli
|
|
||||||
*.tlh
|
|
||||||
*.tmp
|
|
||||||
*.tmp_proj
|
|
||||||
*_wpftmp.csproj
|
|
||||||
*.tlog
|
|
||||||
*.vspscc
|
|
||||||
*.vssscc
|
|
||||||
.builds
|
|
||||||
*.pidb
|
|
||||||
*.svclog
|
|
||||||
*.scc
|
|
||||||
|
|
||||||
# Visual Studio profiler
|
|
||||||
*.psess
|
|
||||||
*.vsp
|
|
||||||
*.vspx
|
|
||||||
*.sap
|
|
||||||
|
|
||||||
# Visual Studio Trace Files
|
|
||||||
*.e2e
|
|
||||||
|
|
||||||
# ReSharper
|
|
||||||
_ReSharper*/
|
|
||||||
*.[Rr]e[Ss]harper
|
|
||||||
*.DotSettings.user
|
|
||||||
|
|
||||||
# DotCover is a Code Coverage Tool
|
|
||||||
*.dotCover
|
|
||||||
|
|
||||||
# Click-Once directory
|
|
||||||
publish/
|
|
||||||
|
|
||||||
# Visual Studio cache files
|
|
||||||
# files ending in .cache can be ignored
|
|
||||||
*.[Cc]ache
|
|
||||||
# but keep track of directories ending in .cache
|
|
||||||
!?*.[Cc]ache/
|
|
||||||
|
|
||||||
# Local History for Visual Studio Code
|
# Local History for Visual Studio Code
|
||||||
.history/
|
.history/
|
||||||
|
|
||||||
# JetBrains Rider
|
|
||||||
*.sln.iml
|
|
||||||
|
|
||||||
# Logs
|
# Logs
|
||||||
logs
|
logs
|
||||||
*.log
|
*.log
|
||||||
@@ -274,4 +154,19 @@ docs/pages/versions/*/react-native/*.diff
|
|||||||
**/ios/.xcode.env.local
|
**/ios/.xcode.env.local
|
||||||
|
|
||||||
# Project-Specific
|
# Project-Specific
|
||||||
/amnet-web/dist
|
/amnet-web/dist
|
||||||
|
|
||||||
|
# Generated by Cargo
|
||||||
|
# will have compiled files and executables
|
||||||
|
debug
|
||||||
|
target
|
||||||
|
|
||||||
|
# These are backup files generated by rustfmt
|
||||||
|
**/*.rs.bk
|
||||||
|
|
||||||
|
# MSVC Windows builds of rustc generate these, which store debugging information
|
||||||
|
*.pdb
|
||||||
|
|
||||||
|
# Generated by cargo mutants
|
||||||
|
# Contains mutation testing data
|
||||||
|
**/mutants.out*/
|
||||||
|
|||||||
@@ -1,41 +0,0 @@
|
|||||||
<Project Sdk="Microsoft.NET.Sdk.Web">
|
|
||||||
|
|
||||||
<PropertyGroup>
|
|
||||||
<PublishAot>true</PublishAot>
|
|
||||||
<OutputType>Library</OutputType>
|
|
||||||
<EventSourceSupport>false</EventSourceSupport>
|
|
||||||
<TargetFramework>net9.0-windows</TargetFramework>
|
|
||||||
<OptimizationPreference>Size</OptimizationPreference>
|
|
||||||
<NoDefaultLaunchSettingsFile>true</NoDefaultLaunchSettingsFile>
|
|
||||||
</PropertyGroup>
|
|
||||||
|
|
||||||
<PropertyGroup>
|
|
||||||
<Product>AMNet</Product>
|
|
||||||
<CompanyName>ppc</CompanyName>
|
|
||||||
<AssemblyName>amnet</AssemblyName>
|
|
||||||
<AssemblyTitle>AMNet Server</AssemblyTitle>
|
|
||||||
<Copyright>Copyright 2024 (c) ppc</Copyright>
|
|
||||||
<Description>Server component for the AMNet IC Card switcher</Description>
|
|
||||||
</PropertyGroup>
|
|
||||||
|
|
||||||
<PropertyGroup>
|
|
||||||
<DebugType>None</DebugType>
|
|
||||||
<DebugSymbols>false</DebugSymbols>
|
|
||||||
<MetricsSupport>false</MetricsSupport>
|
|
||||||
<DebuggerSupport>false</DebuggerSupport>
|
|
||||||
<EventSourceSupport>false</EventSourceSupport>
|
|
||||||
<InvariantGlobalization>true</InvariantGlobalization>
|
|
||||||
<MetadataUpdaterSupport>false</MetadataUpdaterSupport>
|
|
||||||
<EnableUnsafeUTF7Encoding>false</EnableUnsafeUTF7Encoding>
|
|
||||||
<HttpActivityPropagationSupport>false</HttpActivityPropagationSupport>
|
|
||||||
<EnableUnsafeBinaryFormatterSerialization>false</EnableUnsafeBinaryFormatterSerialization>
|
|
||||||
</PropertyGroup>
|
|
||||||
|
|
||||||
<ItemGroup>
|
|
||||||
<PackageReference Include="Microsoft.Windows.CsWin32" Version="0.3.106">
|
|
||||||
<PrivateAssets>all</PrivateAssets>
|
|
||||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
|
||||||
</PackageReference>
|
|
||||||
</ItemGroup>
|
|
||||||
|
|
||||||
</Project>
|
|
||||||
@@ -1,79 +0,0 @@
|
|||||||
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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,37 +0,0 @@
|
|||||||
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();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,126 +0,0 @@
|
|||||||
using System;
|
|
||||||
using System.IO;
|
|
||||||
using System.Runtime.InteropServices;
|
|
||||||
using System.Text;
|
|
||||||
using System.Threading;
|
|
||||||
using Windows.Win32;
|
|
||||||
using Microsoft.AspNetCore.Builder;
|
|
||||||
using Microsoft.Extensions.DependencyInjection;
|
|
||||||
using Microsoft.Extensions.Logging;
|
|
||||||
|
|
||||||
namespace AMNet.Server;
|
|
||||||
|
|
||||||
public static class DllMain
|
|
||||||
{
|
|
||||||
private const string WebAddress = "http://card.ppc.moe";
|
|
||||||
|
|
||||||
// instance metrics
|
|
||||||
internal static long LastPollTime = -1;
|
|
||||||
internal static long ServerStartedAt;
|
|
||||||
|
|
||||||
static DllMain()
|
|
||||||
{
|
|
||||||
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")]
|
|
||||||
public static ushort GetApiVersion() => 0x0100;
|
|
||||||
|
|
||||||
[UnmanagedCallersOnly(EntryPoint = "aime_io_init")]
|
|
||||||
public static int Init()
|
|
||||||
{
|
|
||||||
CancellationTokenRegistration? cancellationRegistration = null;
|
|
||||||
cancellationRegistration = App.Lifetime.ApplicationStarted.Register(() =>
|
|
||||||
{
|
|
||||||
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);
|
|
||||||
|
|
||||||
// ReSharper disable once AccessToModifiedClosure
|
|
||||||
cancellationRegistration?.Dispose();
|
|
||||||
});
|
|
||||||
|
|
||||||
Volatile.Write(ref ServerStartedAt, Environment.TickCount64);
|
|
||||||
App.RunAsync();
|
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
[UnmanagedCallersOnly(EntryPoint = "aime_io_nfc_poll")]
|
|
||||||
public static int NfcPoll(byte unitNo)
|
|
||||||
{
|
|
||||||
Volatile.Write(ref LastPollTime, Environment.TickCount64);
|
|
||||||
|
|
||||||
if (!Config.EnableAimeTxt)
|
|
||||||
{
|
|
||||||
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)
|
|
||||||
{
|
|
||||||
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);
|
|
||||||
}
|
|
||||||
|
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
[UnmanagedCallersOnly(EntryPoint = "aime_io_nfc_get_aime_id")]
|
|
||||||
public static int GetAimeId(byte unitNo, IntPtr luid, nint luidSize)
|
|
||||||
{
|
|
||||||
if (unitNo != 0)
|
|
||||||
{
|
|
||||||
return 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
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")]
|
|
||||||
public static unsafe int GetFelicaId(byte unitNo, ulong* idm) => 1; // felica not supported
|
|
||||||
|
|
||||||
[UnmanagedCallersOnly(EntryPoint = "aime_io_led_set_color")]
|
|
||||||
public static void SetLedColour(byte unitNo, byte r, byte g, byte b)
|
|
||||||
{
|
|
||||||
// do nothing
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,5 +0,0 @@
|
|||||||
GetPrivateProfileStringW
|
|
||||||
GetPrivateProfileInt
|
|
||||||
GetAsyncKeyState
|
|
||||||
AllocConsole
|
|
||||||
MAX_PATH
|
|
||||||
@@ -1,147 +0,0 @@
|
|||||||
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.Hosting;
|
|
||||||
using Microsoft.AspNetCore.Http;
|
|
||||||
using Microsoft.Extensions.DependencyInjection;
|
|
||||||
using Microsoft.Extensions.Logging;
|
|
||||||
|
|
||||||
namespace AMNet.Server;
|
|
||||||
|
|
||||||
internal static partial class WebServer
|
|
||||||
{
|
|
||||||
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([]);
|
|
||||||
|
|
||||||
builder.WebHost.UseUrls(listenAddresses);
|
|
||||||
|
|
||||||
builder.Logging.ClearProviders();
|
|
||||||
builder.Logging.AddSimpleConsole(o =>
|
|
||||||
{
|
|
||||||
o.SingleLine = true;
|
|
||||||
o.IncludeScopes = false;
|
|
||||||
});
|
|
||||||
|
|
||||||
builder.Logging.SetMinimumLevel(LogLevel.Information);
|
|
||||||
builder.Logging.AddFilter("Microsoft.AspNetCore.Http.Result", LogLevel.None);
|
|
||||||
builder.Logging.AddFilter("Microsoft.AspNetCore.Routing.EndpointMiddleware", LogLevel.Error);
|
|
||||||
|
|
||||||
builder.Services.AddCors(cors =>
|
|
||||||
{
|
|
||||||
cors.AddDefaultPolicy(policy =>
|
|
||||||
{
|
|
||||||
policy.AllowAnyMethod();
|
|
||||||
policy.AllowAnyOrigin();
|
|
||||||
policy.AllowAnyHeader();
|
|
||||||
|
|
||||||
policy.SetPreflightMaxAge(TimeSpan.FromHours(6));
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
builder.Services.ConfigureHttpJsonOptions(options =>
|
|
||||||
{
|
|
||||||
options.SerializerOptions.TypeInfoResolverChain.Insert(0, SerializerContext.Default);
|
|
||||||
});
|
|
||||||
|
|
||||||
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;
|
|
||||||
}
|
|
||||||
@@ -1,16 +0,0 @@
|
|||||||
|
|
||||||
Microsoft Visual Studio Solution File, Format Version 12.00
|
|
||||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "AMNet.Server", "AMNet.Server\AMNet.Server.csproj", "{C9FA2545-F15C-4517-B3AB-55B8FC23FA04}"
|
|
||||||
EndProject
|
|
||||||
Global
|
|
||||||
GlobalSection(SolutionConfigurationPlatforms) = preSolution
|
|
||||||
Debug|Any CPU = Debug|Any CPU
|
|
||||||
Release|Any CPU = Release|Any CPU
|
|
||||||
EndGlobalSection
|
|
||||||
GlobalSection(ProjectConfigurationPlatforms) = postSolution
|
|
||||||
{C9FA2545-F15C-4517-B3AB-55B8FC23FA04}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
|
||||||
{C9FA2545-F15C-4517-B3AB-55B8FC23FA04}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
|
||||||
{C9FA2545-F15C-4517-B3AB-55B8FC23FA04}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
|
||||||
{C9FA2545-F15C-4517-B3AB-55B8FC23FA04}.Release|Any CPU.Build.0 = Release|Any CPU
|
|
||||||
EndGlobalSection
|
|
||||||
EndGlobal
|
|
||||||
Generated
+882
@@ -0,0 +1,882 @@
|
|||||||
|
# This file is automatically @generated by Cargo.
|
||||||
|
# It is not intended for manual editing.
|
||||||
|
version = 4
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "amnet"
|
||||||
|
version = "2.0.0"
|
||||||
|
dependencies = [
|
||||||
|
"axum",
|
||||||
|
"hostname",
|
||||||
|
"rust-ini",
|
||||||
|
"serde",
|
||||||
|
"tokio",
|
||||||
|
"tower",
|
||||||
|
"tower-http",
|
||||||
|
"tracing",
|
||||||
|
"tracing-subscriber",
|
||||||
|
"windows-sys 0.61.2",
|
||||||
|
"winresource",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "amnet-server-test"
|
||||||
|
version = "2.0.0"
|
||||||
|
dependencies = [
|
||||||
|
"amnet",
|
||||||
|
"axum",
|
||||||
|
"tokio",
|
||||||
|
"tracing",
|
||||||
|
"tracing-subscriber",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "atomic-waker"
|
||||||
|
version = "1.1.2"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "1505bd5d3d116872e7271a6d4e16d81d0c8570876c8de68093a09ac269d8aac0"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "axum"
|
||||||
|
version = "0.8.8"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "8b52af3cb4058c895d37317bb27508dccc8e5f2d39454016b297bf4a400597b8"
|
||||||
|
dependencies = [
|
||||||
|
"axum-core",
|
||||||
|
"bytes",
|
||||||
|
"form_urlencoded",
|
||||||
|
"futures-util",
|
||||||
|
"http",
|
||||||
|
"http-body",
|
||||||
|
"http-body-util",
|
||||||
|
"hyper",
|
||||||
|
"hyper-util",
|
||||||
|
"itoa",
|
||||||
|
"matchit",
|
||||||
|
"memchr",
|
||||||
|
"mime",
|
||||||
|
"percent-encoding",
|
||||||
|
"pin-project-lite",
|
||||||
|
"serde_core",
|
||||||
|
"serde_json",
|
||||||
|
"serde_path_to_error",
|
||||||
|
"serde_urlencoded",
|
||||||
|
"sync_wrapper",
|
||||||
|
"tokio",
|
||||||
|
"tower",
|
||||||
|
"tower-layer",
|
||||||
|
"tower-service",
|
||||||
|
"tracing",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "axum-core"
|
||||||
|
version = "0.5.6"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "08c78f31d7b1291f7ee735c1c6780ccde7785daae9a9206026862dab7d8792d1"
|
||||||
|
dependencies = [
|
||||||
|
"bytes",
|
||||||
|
"futures-core",
|
||||||
|
"http",
|
||||||
|
"http-body",
|
||||||
|
"http-body-util",
|
||||||
|
"mime",
|
||||||
|
"pin-project-lite",
|
||||||
|
"sync_wrapper",
|
||||||
|
"tower-layer",
|
||||||
|
"tower-service",
|
||||||
|
"tracing",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "bitflags"
|
||||||
|
version = "2.10.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "812e12b5285cc515a9c72a5c1d3b6d46a19dac5acfef5265968c166106e31dd3"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "bytes"
|
||||||
|
version = "1.11.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "b35204fbdc0b3f4446b89fc1ac2cf84a8a68971995d0bf2e925ec7cd960f9cb3"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "cfg-if"
|
||||||
|
version = "1.0.4"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "const-random"
|
||||||
|
version = "0.1.18"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "87e00182fe74b066627d63b85fd550ac2998d4b0bd86bfed477a0ae4c7c71359"
|
||||||
|
dependencies = [
|
||||||
|
"const-random-macro",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "const-random-macro"
|
||||||
|
version = "0.1.16"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "f9d839f2a20b0aee515dc581a6172f2321f96cab76c1a38a4c584a194955390e"
|
||||||
|
dependencies = [
|
||||||
|
"getrandom",
|
||||||
|
"once_cell",
|
||||||
|
"tiny-keccak",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "crunchy"
|
||||||
|
version = "0.2.4"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "460fbee9c2c2f33933d720630a6a0bac33ba7053db5344fac858d4b8952d77d5"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "dlv-list"
|
||||||
|
version = "0.5.2"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "442039f5147480ba31067cb00ada1adae6892028e40e45fc5de7b7df6dcc1b5f"
|
||||||
|
dependencies = [
|
||||||
|
"const-random",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "equivalent"
|
||||||
|
version = "1.0.2"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "form_urlencoded"
|
||||||
|
version = "1.2.2"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "cb4cb245038516f5f85277875cdaa4f7d2c9a0fa0468de06ed190163b1581fcf"
|
||||||
|
dependencies = [
|
||||||
|
"percent-encoding",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "futures-channel"
|
||||||
|
version = "0.3.31"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "2dff15bf788c671c1934e366d07e30c1814a8ef514e1af724a602e8a2fbe1b10"
|
||||||
|
dependencies = [
|
||||||
|
"futures-core",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "futures-core"
|
||||||
|
version = "0.3.31"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "05f29059c0c2090612e8d742178b0580d2dc940c837851ad723096f87af6663e"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "futures-task"
|
||||||
|
version = "0.3.31"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "f90f7dce0722e95104fcb095585910c0977252f286e354b5e3bd38902cd99988"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "futures-util"
|
||||||
|
version = "0.3.31"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "9fa08315bb612088cc391249efdc3bc77536f16c91f6cf495e6fbe85b20a4a81"
|
||||||
|
dependencies = [
|
||||||
|
"futures-core",
|
||||||
|
"futures-task",
|
||||||
|
"pin-project-lite",
|
||||||
|
"pin-utils",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "getrandom"
|
||||||
|
version = "0.2.16"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "335ff9f135e4384c8150d6f27c6daed433577f86b4750418338c01a1a2528592"
|
||||||
|
dependencies = [
|
||||||
|
"cfg-if",
|
||||||
|
"libc",
|
||||||
|
"wasi",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "hashbrown"
|
||||||
|
version = "0.14.5"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "e5274423e17b7c9fc20b6e7e208532f9b19825d82dfd615708b70edd83df41f1"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "hashbrown"
|
||||||
|
version = "0.16.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "841d1cc9bed7f9236f321df977030373f4a4163ae1a7dbfe1a51a2c1a51d9100"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "hostname"
|
||||||
|
version = "0.4.2"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "617aaa3557aef3810a6369d0a99fac8a080891b68bd9f9812a1eeda0c0730cbd"
|
||||||
|
dependencies = [
|
||||||
|
"cfg-if",
|
||||||
|
"libc",
|
||||||
|
"windows-link",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "http"
|
||||||
|
version = "1.4.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "e3ba2a386d7f85a81f119ad7498ebe444d2e22c2af0b86b069416ace48b3311a"
|
||||||
|
dependencies = [
|
||||||
|
"bytes",
|
||||||
|
"itoa",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "http-body"
|
||||||
|
version = "1.0.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "1efedce1fb8e6913f23e0c92de8e62cd5b772a67e7b3946df930a62566c93184"
|
||||||
|
dependencies = [
|
||||||
|
"bytes",
|
||||||
|
"http",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "http-body-util"
|
||||||
|
version = "0.1.3"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "b021d93e26becf5dc7e1b75b1bed1fd93124b374ceb73f43d4d4eafec896a64a"
|
||||||
|
dependencies = [
|
||||||
|
"bytes",
|
||||||
|
"futures-core",
|
||||||
|
"http",
|
||||||
|
"http-body",
|
||||||
|
"pin-project-lite",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "httparse"
|
||||||
|
version = "1.10.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "6dbf3de79e51f3d586ab4cb9d5c3e2c14aa28ed23d180cf89b4df0454a69cc87"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "httpdate"
|
||||||
|
version = "1.0.3"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "df3b46402a9d5adb4c86a0cf463f42e19994e3ee891101b1841f30a545cb49a9"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "hyper"
|
||||||
|
version = "1.8.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "2ab2d4f250c3d7b1c9fcdff1cece94ea4e2dfbec68614f7b87cb205f24ca9d11"
|
||||||
|
dependencies = [
|
||||||
|
"atomic-waker",
|
||||||
|
"bytes",
|
||||||
|
"futures-channel",
|
||||||
|
"futures-core",
|
||||||
|
"http",
|
||||||
|
"http-body",
|
||||||
|
"httparse",
|
||||||
|
"httpdate",
|
||||||
|
"itoa",
|
||||||
|
"pin-project-lite",
|
||||||
|
"pin-utils",
|
||||||
|
"smallvec",
|
||||||
|
"tokio",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "hyper-util"
|
||||||
|
version = "0.1.19"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "727805d60e7938b76b826a6ef209eb70eaa1812794f9424d4a4e2d740662df5f"
|
||||||
|
dependencies = [
|
||||||
|
"bytes",
|
||||||
|
"futures-core",
|
||||||
|
"http",
|
||||||
|
"http-body",
|
||||||
|
"hyper",
|
||||||
|
"pin-project-lite",
|
||||||
|
"tokio",
|
||||||
|
"tower-service",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "indexmap"
|
||||||
|
version = "2.13.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "7714e70437a7dc3ac8eb7e6f8df75fd8eb422675fc7678aff7364301092b1017"
|
||||||
|
dependencies = [
|
||||||
|
"equivalent",
|
||||||
|
"hashbrown 0.16.1",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "itoa"
|
||||||
|
version = "1.0.17"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "92ecc6618181def0457392ccd0ee51198e065e016d1d527a7ac1b6dc7c1f09d2"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "lazy_static"
|
||||||
|
version = "1.5.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "libc"
|
||||||
|
version = "0.2.179"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "c5a2d376baa530d1238d133232d15e239abad80d05838b4b59354e5268af431f"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "log"
|
||||||
|
version = "0.4.29"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "5e5032e24019045c762d3c0f28f5b6b8bbf38563a65908389bf7978758920897"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "matchit"
|
||||||
|
version = "0.8.4"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "47e1ffaa40ddd1f3ed91f717a33c8c0ee23fff369e3aa8772b9605cc1d22f4c3"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "memchr"
|
||||||
|
version = "2.7.6"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "f52b00d39961fc5b2736ea853c9cc86238e165017a493d1d5c8eac6bdc4cc273"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "mime"
|
||||||
|
version = "0.3.17"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "6877bb514081ee2a7ff5ef9de3281f14a4dd4bceac4c09388074a6b5df8a139a"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "mio"
|
||||||
|
version = "1.1.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "a69bcab0ad47271a0234d9422b131806bf3968021e5dc9328caf2d4cd58557fc"
|
||||||
|
dependencies = [
|
||||||
|
"libc",
|
||||||
|
"wasi",
|
||||||
|
"windows-sys 0.61.2",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "nu-ansi-term"
|
||||||
|
version = "0.50.3"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "7957b9740744892f114936ab4a57b3f487491bbeafaf8083688b16841a4240e5"
|
||||||
|
dependencies = [
|
||||||
|
"windows-sys 0.61.2",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "once_cell"
|
||||||
|
version = "1.21.3"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "ordered-multimap"
|
||||||
|
version = "0.7.3"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "49203cdcae0030493bad186b28da2fa25645fa276a51b6fec8010d281e02ef79"
|
||||||
|
dependencies = [
|
||||||
|
"dlv-list",
|
||||||
|
"hashbrown 0.14.5",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "percent-encoding"
|
||||||
|
version = "2.3.2"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "9b4f627cb1b25917193a259e49bdad08f671f8d9708acfd5fe0a8c1455d87220"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "pin-project-lite"
|
||||||
|
version = "0.2.16"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "3b3cff922bd51709b605d9ead9aa71031d81447142d828eb4a6eba76fe619f9b"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "pin-utils"
|
||||||
|
version = "0.1.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "proc-macro2"
|
||||||
|
version = "1.0.104"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "9695f8df41bb4f3d222c95a67532365f569318332d03d5f3f67f37b20e6ebdf0"
|
||||||
|
dependencies = [
|
||||||
|
"unicode-ident",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "quote"
|
||||||
|
version = "1.0.42"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "a338cc41d27e6cc6dce6cefc13a0729dfbb81c262b1f519331575dd80ef3067f"
|
||||||
|
dependencies = [
|
||||||
|
"proc-macro2",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "rust-ini"
|
||||||
|
version = "0.21.3"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "796e8d2b6696392a43bea58116b667fb4c29727dc5abd27d6acf338bb4f688c7"
|
||||||
|
dependencies = [
|
||||||
|
"cfg-if",
|
||||||
|
"ordered-multimap",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "ryu"
|
||||||
|
version = "1.0.22"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "a50f4cf475b65d88e057964e0e9bb1f0aa9bbb2036dc65c64596b42932536984"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "serde"
|
||||||
|
version = "1.0.228"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "9a8e94ea7f378bd32cbbd37198a4a91436180c5bb472411e48b5ec2e2124ae9e"
|
||||||
|
dependencies = [
|
||||||
|
"serde_core",
|
||||||
|
"serde_derive",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "serde_core"
|
||||||
|
version = "1.0.228"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "41d385c7d4ca58e59fc732af25c3983b67ac852c1a25000afe1175de458b67ad"
|
||||||
|
dependencies = [
|
||||||
|
"serde_derive",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "serde_derive"
|
||||||
|
version = "1.0.228"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79"
|
||||||
|
dependencies = [
|
||||||
|
"proc-macro2",
|
||||||
|
"quote",
|
||||||
|
"syn",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "serde_json"
|
||||||
|
version = "1.0.148"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "3084b546a1dd6289475996f182a22aba973866ea8e8b02c51d9f46b1336a22da"
|
||||||
|
dependencies = [
|
||||||
|
"itoa",
|
||||||
|
"memchr",
|
||||||
|
"serde",
|
||||||
|
"serde_core",
|
||||||
|
"zmij",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "serde_path_to_error"
|
||||||
|
version = "0.1.20"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "10a9ff822e371bb5403e391ecd83e182e0e77ba7f6fe0160b795797109d1b457"
|
||||||
|
dependencies = [
|
||||||
|
"itoa",
|
||||||
|
"serde",
|
||||||
|
"serde_core",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "serde_spanned"
|
||||||
|
version = "1.0.4"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "f8bbf91e5a4d6315eee45e704372590b30e260ee83af6639d64557f51b067776"
|
||||||
|
dependencies = [
|
||||||
|
"serde_core",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "serde_urlencoded"
|
||||||
|
version = "0.7.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "d3491c14715ca2294c4d6a88f15e84739788c1d030eed8c110436aafdaa2f3fd"
|
||||||
|
dependencies = [
|
||||||
|
"form_urlencoded",
|
||||||
|
"itoa",
|
||||||
|
"ryu",
|
||||||
|
"serde",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "sharded-slab"
|
||||||
|
version = "0.1.7"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "f40ca3c46823713e0d4209592e8d6e826aa57e928f09752619fc696c499637f6"
|
||||||
|
dependencies = [
|
||||||
|
"lazy_static",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "smallvec"
|
||||||
|
version = "1.15.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "67b1b7a3b5fe4f1376887184045fcf45c69e92af734b7aaddc05fb777b6fbd03"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "socket2"
|
||||||
|
version = "0.6.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "17129e116933cf371d018bb80ae557e889637989d8638274fb25622827b03881"
|
||||||
|
dependencies = [
|
||||||
|
"libc",
|
||||||
|
"windows-sys 0.60.2",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "syn"
|
||||||
|
version = "2.0.113"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "678faa00651c9eb72dd2020cbdf275d92eccb2400d568e419efdd64838145cb4"
|
||||||
|
dependencies = [
|
||||||
|
"proc-macro2",
|
||||||
|
"quote",
|
||||||
|
"unicode-ident",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "sync_wrapper"
|
||||||
|
version = "1.0.2"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "0bf256ce5efdfa370213c1dabab5935a12e49f2c58d15e9eac2870d3b4f27263"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "thread_local"
|
||||||
|
version = "1.1.9"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "f60246a4944f24f6e018aa17cdeffb7818b76356965d03b07d6a9886e8962185"
|
||||||
|
dependencies = [
|
||||||
|
"cfg-if",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "tiny-keccak"
|
||||||
|
version = "2.0.2"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "2c9d3793400a45f954c52e73d068316d76b6f4e36977e3fcebb13a2721e80237"
|
||||||
|
dependencies = [
|
||||||
|
"crunchy",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "tokio"
|
||||||
|
version = "1.49.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "72a2903cd7736441aac9df9d7688bd0ce48edccaadf181c3b90be801e81d3d86"
|
||||||
|
dependencies = [
|
||||||
|
"libc",
|
||||||
|
"mio",
|
||||||
|
"pin-project-lite",
|
||||||
|
"socket2",
|
||||||
|
"tokio-macros",
|
||||||
|
"windows-sys 0.61.2",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "tokio-macros"
|
||||||
|
version = "2.6.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "af407857209536a95c8e56f8231ef2c2e2aff839b22e07a1ffcbc617e9db9fa5"
|
||||||
|
dependencies = [
|
||||||
|
"proc-macro2",
|
||||||
|
"quote",
|
||||||
|
"syn",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "toml"
|
||||||
|
version = "0.9.11+spec-1.1.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "f3afc9a848309fe1aaffaed6e1546a7a14de1f935dc9d89d32afd9a44bab7c46"
|
||||||
|
dependencies = [
|
||||||
|
"indexmap",
|
||||||
|
"serde_core",
|
||||||
|
"serde_spanned",
|
||||||
|
"toml_datetime",
|
||||||
|
"toml_parser",
|
||||||
|
"toml_writer",
|
||||||
|
"winnow",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "toml_datetime"
|
||||||
|
version = "0.7.5+spec-1.1.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "92e1cfed4a3038bc5a127e35a2d360f145e1f4b971b551a2ba5fd7aedf7e1347"
|
||||||
|
dependencies = [
|
||||||
|
"serde_core",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "toml_parser"
|
||||||
|
version = "1.0.6+spec-1.1.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "a3198b4b0a8e11f09dd03e133c0280504d0801269e9afa46362ffde1cbeebf44"
|
||||||
|
dependencies = [
|
||||||
|
"winnow",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "toml_writer"
|
||||||
|
version = "1.0.6+spec-1.1.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "ab16f14aed21ee8bfd8ec22513f7287cd4a91aa92e44edfe2c17ddd004e92607"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "tower"
|
||||||
|
version = "0.5.2"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "d039ad9159c98b70ecfd540b2573b97f7f52c3e8d9f8ad57a24b916a536975f9"
|
||||||
|
dependencies = [
|
||||||
|
"futures-core",
|
||||||
|
"futures-util",
|
||||||
|
"pin-project-lite",
|
||||||
|
"sync_wrapper",
|
||||||
|
"tokio",
|
||||||
|
"tower-layer",
|
||||||
|
"tower-service",
|
||||||
|
"tracing",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "tower-http"
|
||||||
|
version = "0.6.8"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "d4e6559d53cc268e5031cd8429d05415bc4cb4aefc4aa5d6cc35fbf5b924a1f8"
|
||||||
|
dependencies = [
|
||||||
|
"bitflags",
|
||||||
|
"bytes",
|
||||||
|
"http",
|
||||||
|
"http-body",
|
||||||
|
"pin-project-lite",
|
||||||
|
"tower-layer",
|
||||||
|
"tower-service",
|
||||||
|
"tracing",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "tower-layer"
|
||||||
|
version = "0.3.3"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "121c2a6cda46980bb0fcd1647ffaf6cd3fc79a013de288782836f6df9c48780e"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "tower-service"
|
||||||
|
version = "0.3.3"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "8df9b6e13f2d32c91b9bd719c00d1958837bc7dec474d94952798cc8e69eeec3"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "tracing"
|
||||||
|
version = "0.1.44"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "63e71662fa4b2a2c3a26f570f037eb95bb1f85397f3cd8076caed2f026a6d100"
|
||||||
|
dependencies = [
|
||||||
|
"log",
|
||||||
|
"pin-project-lite",
|
||||||
|
"tracing-attributes",
|
||||||
|
"tracing-core",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "tracing-attributes"
|
||||||
|
version = "0.1.31"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "7490cfa5ec963746568740651ac6781f701c9c5ea257c58e057f3ba8cf69e8da"
|
||||||
|
dependencies = [
|
||||||
|
"proc-macro2",
|
||||||
|
"quote",
|
||||||
|
"syn",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "tracing-core"
|
||||||
|
version = "0.1.36"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "db97caf9d906fbde555dd62fa95ddba9eecfd14cb388e4f491a66d74cd5fb79a"
|
||||||
|
dependencies = [
|
||||||
|
"once_cell",
|
||||||
|
"valuable",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "tracing-log"
|
||||||
|
version = "0.2.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "ee855f1f400bd0e5c02d150ae5de3840039a3f54b025156404e34c23c03f47c3"
|
||||||
|
dependencies = [
|
||||||
|
"log",
|
||||||
|
"once_cell",
|
||||||
|
"tracing-core",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "tracing-subscriber"
|
||||||
|
version = "0.3.22"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "2f30143827ddab0d256fd843b7a66d164e9f271cfa0dde49142c5ca0ca291f1e"
|
||||||
|
dependencies = [
|
||||||
|
"nu-ansi-term",
|
||||||
|
"sharded-slab",
|
||||||
|
"smallvec",
|
||||||
|
"thread_local",
|
||||||
|
"tracing-core",
|
||||||
|
"tracing-log",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "unicode-ident"
|
||||||
|
version = "1.0.22"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "9312f7c4f6ff9069b165498234ce8be658059c6728633667c526e27dc2cf1df5"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "valuable"
|
||||||
|
version = "0.1.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "ba73ea9cf16a25df0c8caa16c51acb937d5712a8429db78a3ee29d5dcacd3a65"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "version_check"
|
||||||
|
version = "0.9.5"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "wasi"
|
||||||
|
version = "0.11.1+wasi-snapshot-preview1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "windows-link"
|
||||||
|
version = "0.2.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "windows-sys"
|
||||||
|
version = "0.60.2"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "f2f500e4d28234f72040990ec9d39e3a6b950f9f22d3dba18416c35882612bcb"
|
||||||
|
dependencies = [
|
||||||
|
"windows-targets",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "windows-sys"
|
||||||
|
version = "0.61.2"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "ae137229bcbd6cdf0f7b80a31df61766145077ddf49416a728b02cb3921ff3fc"
|
||||||
|
dependencies = [
|
||||||
|
"windows-link",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "windows-targets"
|
||||||
|
version = "0.53.5"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "4945f9f551b88e0d65f3db0bc25c33b8acea4d9e41163edf90dcd0b19f9069f3"
|
||||||
|
dependencies = [
|
||||||
|
"windows-link",
|
||||||
|
"windows_aarch64_gnullvm",
|
||||||
|
"windows_aarch64_msvc",
|
||||||
|
"windows_i686_gnu",
|
||||||
|
"windows_i686_gnullvm",
|
||||||
|
"windows_i686_msvc",
|
||||||
|
"windows_x86_64_gnu",
|
||||||
|
"windows_x86_64_gnullvm",
|
||||||
|
"windows_x86_64_msvc",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "windows_aarch64_gnullvm"
|
||||||
|
version = "0.53.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "a9d8416fa8b42f5c947f8482c43e7d89e73a173cead56d044f6a56104a6d1b53"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "windows_aarch64_msvc"
|
||||||
|
version = "0.53.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "b9d782e804c2f632e395708e99a94275910eb9100b2114651e04744e9b125006"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "windows_i686_gnu"
|
||||||
|
version = "0.53.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "960e6da069d81e09becb0ca57a65220ddff016ff2d6af6a223cf372a506593a3"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "windows_i686_gnullvm"
|
||||||
|
version = "0.53.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "fa7359d10048f68ab8b09fa71c3daccfb0e9b559aed648a8f95469c27057180c"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "windows_i686_msvc"
|
||||||
|
version = "0.53.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "1e7ac75179f18232fe9c285163565a57ef8d3c89254a30685b57d83a38d326c2"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "windows_x86_64_gnu"
|
||||||
|
version = "0.53.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "9c3842cdd74a865a8066ab39c8a7a473c0778a3f29370b5fd6b4b9aa7df4a499"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "windows_x86_64_gnullvm"
|
||||||
|
version = "0.53.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "0ffa179e2d07eee8ad8f57493436566c7cc30ac536a3379fdf008f47f6bb7ae1"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "windows_x86_64_msvc"
|
||||||
|
version = "0.53.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "d6bbff5f0aada427a1e5a6da5f1f98158182f26556f345ac9e04d36d0ebed650"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "winnow"
|
||||||
|
version = "0.7.14"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "5a5364e9d77fcdeeaa6062ced926ee3381faa2ee02d3eb83a5c27a8825540829"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "winresource"
|
||||||
|
version = "0.1.28"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "6b021990998587d4438bb672b5c5f034cbc927f51b45e3807ab7323645ef4899"
|
||||||
|
dependencies = [
|
||||||
|
"toml",
|
||||||
|
"version_check",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "zmij"
|
||||||
|
version = "1.0.11"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "dcb2c125bd7365735bebeb420ccb880265ed2d2bddcbcd49f597fdfe6bd5e577"
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
[workspace]
|
||||||
|
members = ["amnet-server", "amnet-server-test"]
|
||||||
|
resolver = "3"
|
||||||
|
|
||||||
|
[workspace.package]
|
||||||
|
version = "2.0.0"
|
||||||
|
authors = ["ppc"]
|
||||||
|
description = "AMNet IC Card Switcher"
|
||||||
+35
@@ -0,0 +1,35 @@
|
|||||||
|
FROM node:lts-alpine AS builder
|
||||||
|
|
||||||
|
WORKDIR /build
|
||||||
|
|
||||||
|
# amnet-shared packages
|
||||||
|
COPY amnet-shared/package.json amnet-shared/package.json
|
||||||
|
COPY amnet-shared/package-lock.json amnet-shared/package-lock.json
|
||||||
|
RUN cd amnet-shared && npm ci
|
||||||
|
|
||||||
|
# amnet-web packages
|
||||||
|
COPY amnet-web/package.json amnet-web/package.json
|
||||||
|
COPY amnet-web/package-lock.json amnet-web/package-lock.json
|
||||||
|
RUN cd amnet-web && npm ci
|
||||||
|
|
||||||
|
# main contents
|
||||||
|
COPY amnet-shared amnet-shared
|
||||||
|
COPY amnet-web amnet-web
|
||||||
|
|
||||||
|
RUN cd amnet-shared && npm run build
|
||||||
|
RUN cd amnet-web && npm run build
|
||||||
|
|
||||||
|
# ---
|
||||||
|
|
||||||
|
FROM joseluisq/static-web-server:2 AS final
|
||||||
|
|
||||||
|
ENV SERVER_PORT=8080
|
||||||
|
ENV SERVER_ROOT=/static
|
||||||
|
ENV SERVER_LOG_LEVEL=info
|
||||||
|
ENV SERVER_FALLBACK_PAGE=/static/index.html
|
||||||
|
|
||||||
|
COPY --from=builder /build/amnet-web/dist /static
|
||||||
|
|
||||||
|
EXPOSE 8080
|
||||||
|
|
||||||
|
# ENTRYPOINT/CMD doesn't need modifying
|
||||||
+29
-23
@@ -1,14 +1,14 @@
|
|||||||
import React, {useEffect} from "react";
|
import React from "react";
|
||||||
import {StatusBar} from "expo-status-bar";
|
import {StatusBar} from "expo-status-bar";
|
||||||
import {useColorScheme, View} from "react-native";
|
|
||||||
import {Server, WalletCards} from "lucide-react-native";
|
import {Server, WalletCards} from "lucide-react-native";
|
||||||
|
import {Platform, useColorScheme, View} from "react-native";
|
||||||
import {NavigationContainer} from "@react-navigation/native";
|
import {NavigationContainer} from "@react-navigation/native";
|
||||||
import {SafeAreaProvider} from "react-native-safe-area-context";
|
import {SafeAreaProvider} from "react-native-safe-area-context";
|
||||||
import {MD3Colors} from "react-native-paper/lib/typescript/types";
|
import {MD3Colors} from "react-native-paper/lib/typescript/types";
|
||||||
import {createBottomTabNavigator} from "@react-navigation/bottom-tabs";
|
import {createBottomTabNavigator} from "@react-navigation/bottom-tabs";
|
||||||
import {createNativeStackNavigator} from "@react-navigation/native-stack";
|
import {createNativeStackNavigator} from "@react-navigation/native-stack";
|
||||||
import {DefaultTheme as NavigationLightTheme, DarkTheme as NavigationDarkTheme} from '@react-navigation/native';
|
|
||||||
import {JetBrainsMono_400Regular, useFonts} from "@expo-google-fonts/jetbrains-mono";
|
import {JetBrainsMono_400Regular, useFonts} from "@expo-google-fonts/jetbrains-mono";
|
||||||
|
import {DefaultTheme as NavigationLightTheme, DarkTheme as NavigationDarkTheme} from '@react-navigation/native';
|
||||||
import {
|
import {
|
||||||
ActivityIndicator,
|
ActivityIndicator,
|
||||||
adaptNavigationTheme,
|
adaptNavigationTheme,
|
||||||
@@ -16,15 +16,18 @@ import {
|
|||||||
MD3LightTheme,
|
MD3LightTheme,
|
||||||
MD3Theme,
|
MD3Theme,
|
||||||
PaperProvider,
|
PaperProvider,
|
||||||
Paragraph
|
Text
|
||||||
} from 'react-native-paper';
|
} from 'react-native-paper';
|
||||||
|
|
||||||
import RealmContext from "./models/RealmContext";
|
|
||||||
import linking from "./Linking";
|
import linking from "./Linking";
|
||||||
|
import RealmContext from "./models/RealmContext";
|
||||||
|
|
||||||
import {AMUITabBar} from "./components/AMUITabBar";
|
import {AMUITabBar} from "./components/AMUITabBar";
|
||||||
import CardAddModal from "./app/CardAddModal";
|
|
||||||
|
import CardEditorModal from "./app/card/CardEditorModal";
|
||||||
|
import CardAddModal from "./app/card/CardAddModal";
|
||||||
import ServerAddModal from "./app/ServerAddModal";
|
import ServerAddModal from "./app/ServerAddModal";
|
||||||
|
|
||||||
import CardManagerStack from "./app/CardManagerStack";
|
import CardManagerStack from "./app/CardManagerStack";
|
||||||
import ServerManagerStack from "./app/ServerManagerStack";
|
import ServerManagerStack from "./app/ServerManagerStack";
|
||||||
|
|
||||||
@@ -69,25 +72,22 @@ const {LightTheme, DarkTheme} = adaptNavigationTheme({
|
|||||||
});
|
});
|
||||||
|
|
||||||
const {RealmProvider} = RealmContext;
|
const {RealmProvider} = RealmContext;
|
||||||
|
|
||||||
const Tab = createBottomTabNavigator();
|
const Tab = createBottomTabNavigator();
|
||||||
const Stack = createNativeStackNavigator();
|
const Stack = createNativeStackNavigator();
|
||||||
|
|
||||||
function AppContent() {
|
function AppContent() {
|
||||||
return (<Tab.Navigator tabBar={p => <AMUITabBar {...p}/>} screenOptions={{headerShown: false}}>
|
return (
|
||||||
<Tab.Screen name="servers"
|
<Tab.Navigator tabBar={p => <AMUITabBar {...p}/>} screenOptions={{headerShown: false}}>
|
||||||
component={ServerManagerStack}
|
<Tab.Screen name="servers"
|
||||||
options={{
|
component={ServerManagerStack}
|
||||||
title: 'Servers',
|
options={{tabBarIcon: ({color, size}) => (<Server color={color} size={size}/>)}}/>
|
||||||
tabBarIcon: ({color, size}) => (<Server color={color} size={size}/>)
|
|
||||||
}}/>
|
|
||||||
|
|
||||||
<Tab.Screen name="cards"
|
<Tab.Screen name="cards"
|
||||||
component={CardManagerStack}
|
component={CardManagerStack}
|
||||||
options={{
|
options={{tabBarIcon: ({color, size}) => (<WalletCards color={color} size={size}/>)}}/>
|
||||||
title: "Cards",
|
</Tab.Navigator>
|
||||||
tabBarIcon: ({color, size}) => (<WalletCards color={color} size={size}/>)
|
)
|
||||||
}}/>
|
|
||||||
</Tab.Navigator>)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function AppRoot() {
|
export default function AppRoot() {
|
||||||
@@ -100,7 +100,7 @@ export default function AppRoot() {
|
|||||||
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/>
|
||||||
<Paragraph>Starting...</Paragraph>
|
<Text variant="bodyMedium">Starting...</Text>
|
||||||
</View>
|
</View>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@@ -116,9 +116,15 @@ export default function AppRoot() {
|
|||||||
{/* main app */}
|
{/* main app */}
|
||||||
<Stack.Screen name="app" component={AppContent} options={{headerShown: false}}/>
|
<Stack.Screen name="app" component={AppContent} options={{headerShown: false}}/>
|
||||||
|
|
||||||
<Stack.Group screenOptions={{presentation: "modal"}}>
|
<Stack.Group screenOptions={{
|
||||||
|
presentation: "modal",
|
||||||
|
headerTitleStyle: {
|
||||||
|
fontWeight: "bold"
|
||||||
|
}
|
||||||
|
}}>
|
||||||
<Stack.Screen name="add-server" component={ServerAddModal} options={{title: "Add Server"}}/>
|
<Stack.Screen name="add-server" component={ServerAddModal} options={{title: "Add Server"}}/>
|
||||||
<Stack.Screen name="add-card" component={CardAddModal} options={{title: "Add Card"}}/>
|
<Stack.Screen name="add-card" component={CardAddModal} options={{title: "Add Card", headerShown: Platform.OS !== 'ios'}}/>
|
||||||
|
<Stack.Screen name="edit-card" component={CardEditorModal} options={{title: "Edit Card"}}/>
|
||||||
</Stack.Group>
|
</Stack.Group>
|
||||||
</Stack.Navigator>
|
</Stack.Navigator>
|
||||||
</NavigationContainer>
|
</NavigationContainer>
|
||||||
|
|||||||
+78
-74
@@ -1,79 +1,83 @@
|
|||||||
{
|
{
|
||||||
"name": "AMNet",
|
"expo": {
|
||||||
"scheme": "amnet",
|
"name": "AMNet",
|
||||||
"version": "1.2.0",
|
"scheme": "amnet",
|
||||||
"slug": "amnet-native",
|
"version": "1.3.2",
|
||||||
"icon": "./assets/icon.png",
|
"slug": "amnet-native",
|
||||||
"userInterfaceStyle": "automatic",
|
"icon": "./assets/icon.png",
|
||||||
"ios": {
|
"userInterfaceStyle": "automatic",
|
||||||
"bundleIdentifier": "moe.ppc.amnet",
|
"newArchEnabled": true,
|
||||||
"supportsTablet": true,
|
"ios": {
|
||||||
"associatedDomains": [
|
"bundleIdentifier": "moe.ppc.amnet",
|
||||||
"applinks:card.ppc.moe"
|
"supportsTablet": true,
|
||||||
],
|
"infoPlist": {
|
||||||
"infoPlist": {
|
"NSAppTransportSecurity": {
|
||||||
"UIViewControllerBasedStatusBarAppearance": "NO"
|
"NSAllowsArbitraryLoads": true
|
||||||
},
|
},
|
||||||
"config": {
|
"UIDesignRequiresCompatibility": true,
|
||||||
"usesNonExemptEncryption": false
|
"UIViewControllerBasedStatusBarAppearance": false
|
||||||
}
|
},
|
||||||
},
|
"config": {
|
||||||
"android": {
|
"usesNonExemptEncryption": false
|
||||||
"package": "moe.ppc.amnet",
|
|
||||||
"versionCode": 3,
|
|
||||||
"adaptiveIcon": {
|
|
||||||
"foregroundImage": "./assets/icon-android.png",
|
|
||||||
"backgroundColor": "#DED8FD"
|
|
||||||
},
|
|
||||||
"splash": {
|
|
||||||
"resizeMode": "cover",
|
|
||||||
"image": "./assets/icon-android.png",
|
|
||||||
"backgroundColor": "#DED8FD",
|
|
||||||
"dark": {
|
|
||||||
"backgroundColor": "#3B5ACC"
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"intentFilters": [
|
"android": {
|
||||||
{
|
"package": "moe.ppc.amnet",
|
||||||
"autoVerify": true,
|
"versionCode": 9,
|
||||||
"action": "VIEW",
|
"adaptiveIcon": {
|
||||||
"data": [
|
"foregroundImage": "./assets/icon-android.png",
|
||||||
{
|
"backgroundColor": "#DED8FD"
|
||||||
"scheme": "http",
|
},
|
||||||
"host": "card.ppc.moe",
|
"splash": {
|
||||||
"pathPrefix": "/#/servers"
|
"resizeMode": "cover",
|
||||||
},
|
"image": "./assets/icon-android.png",
|
||||||
{
|
"backgroundColor": "#DED8FD",
|
||||||
"scheme": "http",
|
"dark": {
|
||||||
"host": "card.ppc.moe",
|
"backgroundColor": "#3B5ACC"
|
||||||
"pathPrefix": "/#/cards"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"scheme": "https",
|
|
||||||
"host": "card.ppc.moe",
|
|
||||||
"pathPrefix": "/#/servers"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"scheme": "https",
|
|
||||||
"host": "card.ppc.moe",
|
|
||||||
"pathPrefix": "/#/cards"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"category": [
|
|
||||||
"BROWSABLE",
|
|
||||||
"DEFAULT"
|
|
||||||
]
|
|
||||||
}
|
|
||||||
]
|
|
||||||
},
|
|
||||||
"plugins": [
|
|
||||||
[
|
|
||||||
"expo-build-properties",
|
|
||||||
{
|
|
||||||
"android": {
|
|
||||||
"usesCleartextTraffic": true
|
|
||||||
}
|
}
|
||||||
}
|
},
|
||||||
|
"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": [
|
||||||
|
[
|
||||||
|
"expo-build-properties",
|
||||||
|
{
|
||||||
|
"android": {
|
||||||
|
"usesCleartextTraffic": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
]
|
]
|
||||||
]
|
}
|
||||||
}
|
}
|
||||||
@@ -1,124 +0,0 @@
|
|||||||
import {generateCardNumber} from "amnet-shared";
|
|
||||||
import {useModalStyles} from "../utils/ModalStyles";
|
|
||||||
import {useCallback, useLayoutEffect, useState} from "react";
|
|
||||||
import {Platform, TextInput, TouchableOpacity, View} from "react-native";
|
|
||||||
import {Button, Card, Divider, Paragraph, useTheme} from "react-native-paper";
|
|
||||||
|
|
||||||
import {AMCard} from "../models/AMCard";
|
|
||||||
import RealmContext from "../models/RealmContext";
|
|
||||||
|
|
||||||
const {useRealm} = RealmContext;
|
|
||||||
|
|
||||||
export default function CardAddModal({navigation, route}) {
|
|
||||||
const realm = useRealm();
|
|
||||||
const theme = useTheme();
|
|
||||||
const modalStyles = useModalStyles();
|
|
||||||
|
|
||||||
// state values
|
|
||||||
const [cardName, setCardName] = useState('');
|
|
||||||
const [cardNumber, setCardNumber] = useState('');
|
|
||||||
const [cardNumberReadOnly, setCardNumberReadOnly] = useState(false);
|
|
||||||
|
|
||||||
const cardAddCallback = useCallback((cardId: string, cardName: string) => {
|
|
||||||
const duplicateCard = realm.objects<AMCard>("card").find(x => x.code === cardId);
|
|
||||||
|
|
||||||
if (duplicateCard) {
|
|
||||||
realm.write(() => {
|
|
||||||
duplicateCard.name = cardName;
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
realm.write(() => {
|
|
||||||
realm.create("card", AMCard.generate(cardId, cardName));
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
navigation.goBack();
|
|
||||||
}, [realm]);
|
|
||||||
|
|
||||||
// header effects
|
|
||||||
if (Platform.OS === 'ios') {
|
|
||||||
useLayoutEffect(() => {
|
|
||||||
navigation.setOptions({
|
|
||||||
headerLeft: () => <TouchableOpacity onPress={() => navigation.goBack()}>
|
|
||||||
<Paragraph style={modalStyles.headerText}>Cancel</Paragraph>
|
|
||||||
</TouchableOpacity>,
|
|
||||||
});
|
|
||||||
}, [navigation, modalStyles]);
|
|
||||||
}
|
|
||||||
|
|
||||||
useLayoutEffect(() => {
|
|
||||||
const canSave = cardName.length && cardNumber.length === 20;
|
|
||||||
const themeOptions = canSave ? modalStyles.headerText : {color: theme.colors.outline};
|
|
||||||
|
|
||||||
navigation.setOptions({
|
|
||||||
headerRight: () => <TouchableOpacity disabled={!canSave}
|
|
||||||
onPress={() => cardAddCallback(cardNumber, cardName)}>
|
|
||||||
<Paragraph style={{...themeOptions, fontWeight: "600"}}>Save</Paragraph>
|
|
||||||
</TouchableOpacity>
|
|
||||||
})
|
|
||||||
}, [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}}>
|
|
||||||
<Card mode="contained">
|
|
||||||
<Card.Content>
|
|
||||||
<View style={modalStyles.formItemRow}>
|
|
||||||
<Paragraph style={modalStyles.formItemName}>
|
|
||||||
Card Name
|
|
||||||
</Paragraph>
|
|
||||||
<TextInput style={modalStyles.formItemInput}
|
|
||||||
numberOfLines={1} autoFocus
|
|
||||||
value={cardName}
|
|
||||||
onChangeText={setCardName}
|
|
||||||
placeholder="Primary Card"
|
|
||||||
placeholderTextColor={theme.colors.outline}/>
|
|
||||||
</View>
|
|
||||||
|
|
||||||
<Divider style={{marginVertical: 15}}/>
|
|
||||||
|
|
||||||
<View style={modalStyles.formItemRow}>
|
|
||||||
<Paragraph style={modalStyles.formItemName}>
|
|
||||||
Matrix Code
|
|
||||||
</Paragraph>
|
|
||||||
<TextInput numberOfLines={1}
|
|
||||||
keyboardType="numeric"
|
|
||||||
readOnly={cardNumberReadOnly}
|
|
||||||
placeholder="0000 0000 0000 0000 0000"
|
|
||||||
placeholderTextColor={theme.colors.outline}
|
|
||||||
value={cardNumber.match(/.{1,4}/g)?.join(" ")}
|
|
||||||
onChangeText={val => setCardNumber(val?.replaceAll(/\D/g, '').substring(0, 20) ?? '')}
|
|
||||||
style={{...modalStyles.formItemInput, color: cardNumberReadOnly ? theme.colors.onSurfaceDisabled : theme.colors.onSurface}}/>
|
|
||||||
</View>
|
|
||||||
</Card.Content>
|
|
||||||
</Card>
|
|
||||||
|
|
||||||
{/*only show card entry info when in create mode*/}
|
|
||||||
{!cardNumberReadOnly && (
|
|
||||||
<>
|
|
||||||
<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>
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
</View>)
|
|
||||||
}
|
|
||||||
@@ -5,7 +5,7 @@ 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 {AimeCard} from "../components/AimeCard";
|
import {ICCard} from "../components/ICCard";
|
||||||
import {EmptyStatePlaceholder} from "../components/EmptyStatePlaceholder";
|
import {EmptyStatePlaceholder} from "../components/EmptyStatePlaceholder";
|
||||||
|
|
||||||
import {AMCard} from "../models/AMCard";
|
import {AMCard} from "../models/AMCard";
|
||||||
@@ -21,19 +21,20 @@ function CardEntry(props: { card: AMCard, navigation: any }) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
switch (indexPath.toString()) {
|
switch (indexPath.toString()) {
|
||||||
// copy card
|
// copy card access code
|
||||||
case "0":
|
case "0":
|
||||||
|
case "0,0":
|
||||||
await Clipboard.setStringAsync(props.card.code);
|
await Clipboard.setStringAsync(props.card.code);
|
||||||
return;
|
return;
|
||||||
|
|
||||||
// edit card
|
// edit card
|
||||||
case "1,0":
|
case "1,0":
|
||||||
props.navigation.navigate('add-card', {cardId: props.card.code, cardName: props.card.name});
|
props.navigation.navigate('edit-card', {cardId: props.card._id.toHexString()});
|
||||||
return;
|
return;
|
||||||
|
|
||||||
// remove card
|
// remove card
|
||||||
case "1,1":
|
case "1,1":
|
||||||
realm.write(() => realm.delete(props.card));
|
AMCard.delete(realm, props.card);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
}, [realm, props.card, props.navigation]);
|
}, [realm, props.card, props.navigation]);
|
||||||
@@ -50,7 +51,7 @@ function CardEntry(props: { card: AMCard, navigation: any }) {
|
|||||||
{title: "Edit Card", systemIcon: "square.and.pencil"},
|
{title: "Edit Card", systemIcon: "square.and.pencil"},
|
||||||
{title: "Delete Card", destructive: true, systemIcon: "trash"}]
|
{title: "Delete Card", destructive: true, systemIcon: "trash"}]
|
||||||
}]}>
|
}]}>
|
||||||
<AimeCard card={props.card} showFullCardNumber/>
|
<ICCard card={props.card} showFullCardNumber/>
|
||||||
</ContextMenu>
|
</ContextMenu>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -15,14 +15,20 @@ export default function CardManagerStack({navigation}) {
|
|||||||
headerLargeTitle: true,
|
headerLargeTitle: true,
|
||||||
headerBlurEffect: "regular",
|
headerBlurEffect: "regular",
|
||||||
headerStyle: {
|
headerStyle: {
|
||||||
backgroundColor: theme.colors.background,
|
backgroundColor: theme.colors.background
|
||||||
|
},
|
||||||
|
headerTitleStyle: {
|
||||||
|
fontWeight: "bold"
|
||||||
|
},
|
||||||
|
headerLargeTitleStyle: {
|
||||||
|
fontWeight: "bold"
|
||||||
}
|
}
|
||||||
}}>
|
}}>
|
||||||
<Stack.Screen name="card-listing"
|
<Stack.Screen name="card-listing"
|
||||||
component={CardManager}
|
component={CardManager}
|
||||||
options={{
|
options={{
|
||||||
title: "Cards",
|
title: "Cards",
|
||||||
headerRight: () => Platform.OS === 'android' ? null : <TouchableOpacity onPress={() => navigation.navigate('add-card')}><Plus/></TouchableOpacity>
|
headerRight: () => Platform.OS === 'android' ? null : <TouchableOpacity onPress={() => navigation.navigate('add-card')}><Plus color={theme.colors.primary}/></TouchableOpacity>
|
||||||
}}/>
|
}}/>
|
||||||
</Stack.Navigator>
|
</Stack.Navigator>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -1,8 +1,9 @@
|
|||||||
import {useModalStyles} from "../utils/ModalStyles";
|
import {StatusBar} from "expo-status-bar";
|
||||||
import {Card, Paragraph, useTheme} from "react-native-paper";
|
import {Card, Text, useTheme} from "react-native-paper";
|
||||||
import {useCallback, useEffect, useLayoutEffect, useState} from "react";
|
import {useCallback, useEffect, useLayoutEffect, useState} from "react";
|
||||||
import {Platform, TextInput, TouchableOpacity, View} from "react-native";
|
import {Alert, Platform, TextInput, TouchableOpacity, View} from "react-native";
|
||||||
|
|
||||||
|
import {useModalStyles} from "../utils/ModalStyles";
|
||||||
import RealmContext from "../models/RealmContext";
|
import RealmContext from "../models/RealmContext";
|
||||||
import {AMServer} from "../models/AMServer";
|
import {AMServer} from "../models/AMServer";
|
||||||
|
|
||||||
@@ -24,7 +25,7 @@ export default function ServerAddModal({route, navigation}) {
|
|||||||
useLayoutEffect(() => {
|
useLayoutEffect(() => {
|
||||||
navigation.setOptions({
|
navigation.setOptions({
|
||||||
headerLeft: () => <TouchableOpacity onPress={() => navigation.goBack()}>
|
headerLeft: () => <TouchableOpacity onPress={() => navigation.goBack()}>
|
||||||
<Paragraph style={modalStyles.headerText}>Cancel</Paragraph>
|
<Text variant="bodyMedium" style={modalStyles.headerText}>Cancel</Text>
|
||||||
</TouchableOpacity>,
|
</TouchableOpacity>,
|
||||||
});
|
});
|
||||||
}, [navigation, modalStyles]);
|
}, [navigation, modalStyles]);
|
||||||
@@ -34,41 +35,74 @@ export default function ServerAddModal({route, navigation}) {
|
|||||||
let urlValid = false;
|
let urlValid = false;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
new URL(serverAddress);
|
const protocol = new URL(serverAddress).protocol;
|
||||||
urlValid = true;
|
urlValid = protocol.localeCompare("http:", undefined, {sensitivity: "case"}) === 0 || protocol.localeCompare("https:", undefined, {sensitivity: "case"}) === 0;
|
||||||
} catch {
|
} catch {
|
||||||
// do nothing
|
// do nothing
|
||||||
}
|
}
|
||||||
|
|
||||||
const themeOptions = urlValid ? modalStyles.headerText : {color: theme.colors.outline};
|
const themeOptions = urlValid ? modalStyles.headerText : {color: theme.colors.outline};
|
||||||
navigation.setOptions({
|
navigation.setOptions({
|
||||||
headerRight: () => <TouchableOpacity disabled={!urlValid} onPress={() => saveServer()}>
|
headerRight: () => <TouchableOpacity disabled={!urlValid} onPress={() => saveServer(serverAddress)}>
|
||||||
<Paragraph style={{...themeOptions, fontWeight: "600"}}>Save</Paragraph>
|
<Text variant="bodyMedium" style={{...themeOptions, fontWeight: "600"}}>Save</Text>
|
||||||
</TouchableOpacity>
|
</TouchableOpacity>
|
||||||
})
|
})
|
||||||
}, [navigation, modalStyles, serverAddress]);
|
}, [navigation, modalStyles, serverAddress]);
|
||||||
|
|
||||||
const saveServer = useCallback(() => {
|
const saveServer = useCallback(async (address: string) => {
|
||||||
let serverObject = realm.objects<AMServer>("server").find(x => x.serverAddress == serverAddress);
|
if (address.startsWith("https:")) {
|
||||||
|
await new Promise<void>(done => {
|
||||||
|
Alert.alert(
|
||||||
|
"HTTPS Warning",
|
||||||
|
"By default, AMNet doesn't support HTTPS without advanced configuration. It's highly likely you won't be able to connect.",
|
||||||
|
[
|
||||||
|
{
|
||||||
|
text: "Keep HTTPS",
|
||||||
|
style: "destructive",
|
||||||
|
onPress: () => done()
|
||||||
|
},
|
||||||
|
{
|
||||||
|
text: "Change to HTTP",
|
||||||
|
isPreferred: true,
|
||||||
|
onPress: () => {
|
||||||
|
address = address.replace(/^https:/i, "http:");
|
||||||
|
done();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
let serverObject = realm.objects<AMServer>(AMServer.schema.name).find(x => x.serverAddress == address);
|
||||||
if (!serverObject) {
|
if (!serverObject) {
|
||||||
realm.write(() => {
|
realm.write(() => {
|
||||||
serverObject = realm.create<AMServer>("server", AMServer.generate(serverAddress));
|
serverObject = realm.create<AMServer>(AMServer.schema.name, AMServer.generate(address));
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
navigation.goBack();
|
navigation.goBack();
|
||||||
setTimeout(() => navigation.navigate('server', {
|
setTimeout(() => {
|
||||||
serverId: serverObject._id.toHexString()
|
navigation.popTo('app', {
|
||||||
}), 500);
|
screen: "servers",
|
||||||
}, [serverAddress, realm]);
|
params: {
|
||||||
|
screen: "server",
|
||||||
|
params: {
|
||||||
|
serverId: serverObject._id.toHexString()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}, 500);
|
||||||
|
}, [realm]);
|
||||||
|
|
||||||
return (<View style={{display: "flex", gap: 10, justifyContent: "center", paddingHorizontal: 15, paddingTop: 25}}>
|
return (<>
|
||||||
|
{Platform.OS == 'ios' && <StatusBar style="light"/>}
|
||||||
|
<View style={{display: "flex", gap: 10, justifyContent: "center", paddingHorizontal: 15, paddingTop: 25}}>
|
||||||
<Card mode="contained">
|
<Card mode="contained">
|
||||||
<Card.Content>
|
<Card.Content>
|
||||||
<View style={{...modalStyles.formItemRow, gap: 10}}>
|
<View style={{...modalStyles.formItemRow, gap: 10}}>
|
||||||
<Paragraph style={{...modalStyles.formItemName, minWidth: 0}}>
|
<Text variant="bodyMedium" style={{...modalStyles.formItemName, minWidth: 0}}>
|
||||||
Address
|
Address
|
||||||
</Paragraph>
|
</Text>
|
||||||
<TextInput style={modalStyles.formItemInput}
|
<TextInput style={modalStyles.formItemInput}
|
||||||
autoFocus
|
autoFocus
|
||||||
numberOfLines={1}
|
numberOfLines={1}
|
||||||
@@ -84,10 +118,11 @@ export default function ServerAddModal({route, navigation}) {
|
|||||||
</Card.Content>
|
</Card.Content>
|
||||||
</Card>
|
</Card>
|
||||||
|
|
||||||
<Paragraph style={modalStyles.formFootnote}>
|
<Text variant="bodyMedium" style={modalStyles.formFootnote}>
|
||||||
If you're using HTTPS, ensure a trusted certificate has been configured via HTTP.SYS
|
AMNet uses HTTP by default. HTTPS will not work unless specifically configured using HTTP.SYS
|
||||||
</Paragraph>
|
</Text>
|
||||||
</View>)
|
</View>
|
||||||
|
</>)
|
||||||
}
|
}
|
||||||
|
|
||||||
function handleDeepLink(realm, navigation, route, setAddress: (address: string) => void) {
|
function handleDeepLink(realm, navigation, route, setAddress: (address: string) => void) {
|
||||||
@@ -104,7 +139,7 @@ function handleDeepLink(realm, navigation, route, setAddress: (address: string)
|
|||||||
navigation.goBack();
|
navigation.goBack();
|
||||||
|
|
||||||
// allow the homepage to render before moving on
|
// allow the homepage to render before moving on
|
||||||
setTimeout(() => navigation.navigate('server', {
|
setTimeout(() => navigation.popTo('server', {
|
||||||
serverId: item[0]._id.toHexString()
|
serverId: item[0]._id.toHexString()
|
||||||
}), 1000);
|
}), 1000);
|
||||||
} else {
|
} else {
|
||||||
|
|||||||
@@ -1,40 +1,31 @@
|
|||||||
import {BSON} from "realm";
|
import {BSON} from "realm";
|
||||||
|
import {ActivityIndicator, Button, Text, useTheme} from "react-native-paper";
|
||||||
import React, {useCallback, useEffect, useLayoutEffect, useMemo, useState} from "react";
|
import React, {useCallback, useEffect, useLayoutEffect, useMemo, useState} from "react";
|
||||||
import {ActivityIndicator, Button, Paragraph, Title, useTheme} from "react-native-paper";
|
import {Alert, Platform, RefreshControl, ScrollView, Share, StyleSheet, View} from "react-native";
|
||||||
import {
|
|
||||||
Alert,
|
|
||||||
Platform,
|
|
||||||
RefreshControl,
|
|
||||||
ScrollView,
|
|
||||||
Share,
|
|
||||||
StyleSheet,
|
|
||||||
Text,
|
|
||||||
TouchableOpacity,
|
|
||||||
View
|
|
||||||
} from "react-native";
|
|
||||||
import {
|
import {
|
||||||
CircleCheck,
|
CircleCheck,
|
||||||
CircleDot,
|
CircleDot,
|
||||||
|
CircleEllipsis,
|
||||||
CreditCard,
|
CreditCard,
|
||||||
Nfc,
|
|
||||||
Plus,
|
Plus,
|
||||||
SmartphoneNfcIcon,
|
SmartphoneNfcIcon,
|
||||||
Share as ShareIcon,
|
|
||||||
WifiOff,
|
WifiOff,
|
||||||
TvMinimalPlay,
|
TvMinimalPlay,
|
||||||
Star
|
Star,
|
||||||
|
SquareArrowOutUpRight
|
||||||
} from "lucide-react-native";
|
} from "lucide-react-native";
|
||||||
|
|
||||||
import {EmptyStatePlaceholder} from "../components/EmptyStatePlaceholder";
|
import {EmptyStatePlaceholder} from "../components/EmptyStatePlaceholder";
|
||||||
import {AMUIStatusAlert} from "../components/AMUIStatusAlert";
|
import {AMUIStatusAlert} from "../components/AMUIStatusAlert";
|
||||||
import {AMUICardDetail} from "../components/AMUICard";
|
import {AMUICardDetail} from "../components/AMUICard";
|
||||||
import {AimeCard} from "../components/AimeCard";
|
import {ICCard} from "../components/ICCard";
|
||||||
|
|
||||||
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 {
|
import {
|
||||||
|
AMCardInRequest,
|
||||||
AMServerConnectionInfo,
|
AMServerConnectionInfo,
|
||||||
AMServerInstanceMetrics,
|
AMServerInstanceMetrics,
|
||||||
fetchServerInfo,
|
fetchServerInfo,
|
||||||
@@ -42,26 +33,59 @@ import {
|
|||||||
performCardIn
|
performCardIn
|
||||||
} from "amnet-shared";
|
} from "amnet-shared";
|
||||||
|
|
||||||
|
import ObjectId = BSON.ObjectId;
|
||||||
|
import {AMCardRecentUse} from "../models/AMCardRecentUse";
|
||||||
|
import {MenuAction, MenuView, NativeActionEvent} from "@react-native-menu/menu";
|
||||||
|
|
||||||
const {useRealm, useQuery, useObject} = RealmContext;
|
const {useRealm, useQuery, useObject} = RealmContext;
|
||||||
|
|
||||||
export default function ServerCardIn({route, navigation}) {
|
export default function ServerCardIn({route, navigation}) {
|
||||||
const realm = useRealm();
|
const realm = useRealm();
|
||||||
const theme = useTheme();
|
const theme = useTheme()
|
||||||
|
|
||||||
let {serverId} = route.params;
|
const {serverId} = route.params;
|
||||||
const server = useObject(AMServer, ObjectId.createFromHexString(serverId));
|
const server = useObject(AMServer, ObjectId.createFromHexString(serverId));
|
||||||
const cards = useQuery({
|
const cards = useQuery({
|
||||||
type: AMCard,
|
type: AMCard,
|
||||||
query: q => q.sorted("lastUsed", true)
|
query: q => q.sorted("lastUsed", true)
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// header actions
|
||||||
|
const menuCallback = useCallback(async (e: NativeActionEvent) => {
|
||||||
|
switch (e.nativeEvent.event) {
|
||||||
|
case "share":
|
||||||
|
Share.share({url: server.shareUrl});
|
||||||
|
break;
|
||||||
|
|
||||||
|
case "delete":
|
||||||
|
Alert.alert("Delete Server", "Are you sure you want to delete this server?", [
|
||||||
|
{
|
||||||
|
text: "Cancel",
|
||||||
|
style: "cancel"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
text: "Delete",
|
||||||
|
style: "destructive",
|
||||||
|
onPress: () => {
|
||||||
|
realm.write(() => {
|
||||||
|
realm.delete(server);
|
||||||
|
navigation.goBack();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]);
|
||||||
|
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}, [realm, server, navigation]);
|
||||||
|
|
||||||
// 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 [serverConnectionSuccess, setServerConnectionSuccess] = useState<boolean | null>(null);
|
const [serverConnectionSuccess, setServerConnectionSuccess] = useState<boolean | null>(null);
|
||||||
|
|
||||||
const [userSelectedCard, setUserSelectedCard] = useState<AMCard>(null);
|
const [userSelectedCard, setUserSelectedCard] = useState<AMCard | null>(null);
|
||||||
const [cardInCompleted, setCardInCompleted] = useState<boolean>(null);
|
const [cardInCompleted, setCardInCompleted] = useState<boolean | null>(null);
|
||||||
|
|
||||||
const disableCardIn = useMemo(() => cardInCompleted !== null || !serverConnectionSuccess, [cardInCompleted, serverConnectionSuccess]);
|
const disableCardIn = useMemo(() => cardInCompleted !== null || !serverConnectionSuccess, [cardInCompleted, serverConnectionSuccess]);
|
||||||
const serverDefaultCard = useMemo(() => {
|
const serverDefaultCard = useMemo(() => {
|
||||||
@@ -69,8 +93,8 @@ export default function ServerCardIn({route, navigation}) {
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
return cards.find(x => x.lastUsedGameId === server.gameId);
|
return realm.objects<AMCardRecentUse>(AMCardRecentUse.schema.name).find(x => x.gameId === server.gameId)?.card;
|
||||||
}, [cards, server?.gameId]);
|
}, [realm, cards, server?.gameId]);
|
||||||
|
|
||||||
const selectedCard = useMemo(() => [userSelectedCard, serverDefaultCard, cards.at(0)].find(x => x?.isValid() == true) ?? null, [userSelectedCard, serverDefaultCard, cards]);
|
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]);
|
const unselectedCards = useMemo(() => cards.filter(x => x.isValid() === true && x._id.toHexString() !== selectedCard?._id.toHexString()), [cards, selectedCard]);
|
||||||
@@ -83,30 +107,20 @@ export default function ServerCardIn({route, navigation}) {
|
|||||||
navigation.setOptions({
|
navigation.setOptions({
|
||||||
title: server.serverName,
|
title: server.serverName,
|
||||||
headerRight: () => (
|
headerRight: () => (
|
||||||
<TouchableOpacity onPress={() => Share.share({url: server.shareUrl})}>
|
<MenuView actions={menuActions} onPressAction={menuCallback}>
|
||||||
<ShareIcon color={theme.colors.primary}/>
|
<CircleEllipsis color={theme.colors.primary}/>
|
||||||
</TouchableOpacity>
|
</MenuView>
|
||||||
)
|
)
|
||||||
});
|
});
|
||||||
}, [navigation, server]);
|
}, [navigation, server]);
|
||||||
|
|
||||||
// invalid card/server selection
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (server?.isValid() !== true) {
|
if (server?.isValid() !== true) {
|
||||||
navigation.goBack();
|
navigation.goBack();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
}, [server]);
|
|
||||||
|
|
||||||
useEffect(() => {
|
if (!serverConnectionSuccess) {
|
||||||
if (userSelectedCard?.isValid() === false) {
|
|
||||||
setUserSelectedCard(null);
|
|
||||||
}
|
|
||||||
}, [userSelectedCard]);
|
|
||||||
|
|
||||||
// handle auto-refresh
|
|
||||||
useEffect(() => {
|
|
||||||
if (!serverConnectionSuccess || server?.isValid() !== true) {
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -117,6 +131,13 @@ export default function ServerCardIn({route, navigation}) {
|
|||||||
}
|
}
|
||||||
}, [serverConnectionSuccess, server]);
|
}, [serverConnectionSuccess, server]);
|
||||||
|
|
||||||
|
// invalid card/server selection
|
||||||
|
useEffect(() => {
|
||||||
|
if (userSelectedCard?.isValid() === false) {
|
||||||
|
setUserSelectedCard(null);
|
||||||
|
}
|
||||||
|
}, [userSelectedCard]);
|
||||||
|
|
||||||
// server info update
|
// server info update
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const abortController = new AbortController();
|
const abortController = new AbortController();
|
||||||
@@ -145,16 +166,24 @@ export default function ServerCardIn({route, navigation}) {
|
|||||||
}
|
}
|
||||||
}, [serverId, serverReloadCounter, realm]);
|
}, [serverId, serverReloadCounter, realm]);
|
||||||
|
|
||||||
const cardInCallback = useCallback(() => {
|
const cardInCallback = useCallback((card: AMCard) => {
|
||||||
setCardInCompleted(false);
|
|
||||||
|
|
||||||
const abortController = new AbortController();
|
const abortController = new AbortController();
|
||||||
performCardIn(server, {cardId: selectedCard.code}, abortController.signal).then(async r => {
|
const request: AMCardInRequest = {
|
||||||
|
cardId: card.code,
|
||||||
|
physicalCardIDm: card.physicalCardData?.cardIDm ?? undefined
|
||||||
|
};
|
||||||
|
|
||||||
|
performCardIn(server, request, abortController.signal).then(r => {
|
||||||
const now = new Date();
|
const now = new Date();
|
||||||
realm.write(() => {
|
realm.write(() => {
|
||||||
server.lastUsed = now;
|
server.lastUsed = now;
|
||||||
selectedCard.lastUsed = now;
|
card.lastUsed = now;
|
||||||
selectedCard.lastUsedGameId = server.gameId;
|
|
||||||
|
if (server.gameId) {
|
||||||
|
// replace the "most recent card" record for the current game
|
||||||
|
realm.delete(realm.objects(AMCardRecentUse.schema.name).filtered("gameId = $0", server.gameId));
|
||||||
|
realm.create(AMCardRecentUse.schema.name, AMCardRecentUse.generate(server.gameId, card));
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
setCardInCompleted(r);
|
setCardInCompleted(r);
|
||||||
@@ -166,7 +195,7 @@ export default function ServerCardIn({route, navigation}) {
|
|||||||
|
|
||||||
setTimeout(() => abortController.abort(), 5000);
|
setTimeout(() => abortController.abort(), 5000);
|
||||||
return () => abortController.abort();
|
return () => abortController.abort();
|
||||||
}, [server, selectedCard]);
|
}, [server]);
|
||||||
|
|
||||||
const performRefresh = () => {
|
const performRefresh = () => {
|
||||||
setServerRefreshing(true);
|
setServerRefreshing(true);
|
||||||
@@ -181,7 +210,7 @@ export default function ServerCardIn({route, navigation}) {
|
|||||||
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/>
|
||||||
<Paragraph>Connecting to server...</Paragraph>
|
<Text variant="bodyMedium">Connecting to server...</Text>
|
||||||
</View>
|
</View>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@@ -191,20 +220,21 @@ export default function ServerCardIn({route, navigation}) {
|
|||||||
<View style={styles.container}>
|
<View style={styles.container}>
|
||||||
<StatusAlert server={server} connectionEstablished={serverConnectionSuccess}/>
|
<StatusAlert server={server} connectionEstablished={serverConnectionSuccess}/>
|
||||||
<View style={styles.section}>
|
<View style={styles.section}>
|
||||||
<Title style={styles.sectionTitle}>{server.gameId ? `Play ${getGameName(server.gameId)}` : "Selected Card"}</Title>
|
<Text variant="titleLarge" style={styles.sectionTitle}>{server.gameId ? `Play ${getGameName(server.gameId)}` : "Selected Card"}</Text>
|
||||||
{selectedCard ? <>
|
{selectedCard ? <>
|
||||||
<AimeCard actionDisabled
|
<ICCard actionDisabled
|
||||||
card={selectedCard}
|
card={selectedCard}
|
||||||
actionIcon={<CircleCheck color="green" size={24}/>}>
|
actionIcon={<CircleCheck color="green" size={24}/>}>
|
||||||
{serverDefaultCard?.code == selectedCard.code && <DefaultCardDetail/>}
|
{serverDefaultCard?.code == selectedCard.code && <DefaultCardDetail/>}
|
||||||
</AimeCard>
|
</ICCard>
|
||||||
<Button textColor="white"
|
<Button style={{flex: 1}}
|
||||||
onPress={cardInCallback}
|
textColor="white"
|
||||||
disabled={disableCardIn}
|
disabled={disableCardIn}
|
||||||
contentStyle={{flexDirection: 'row-reverse'}}
|
contentStyle={{flexDirection: 'row-reverse'}}
|
||||||
mode={serverConnectionSuccess ? "contained" : "outlined"}
|
mode={serverConnectionSuccess ? "contained" : "outlined"}
|
||||||
buttonColor={cardInCompleted === false ? "red" : "green"}
|
buttonColor={cardInCompleted === false ? "red" : "green"}
|
||||||
icon={() => <Nfc color={disableCardIn ? "gray" : "white"} size={18}/>}>
|
onPress={() => cardInCallback(selectedCard)}
|
||||||
|
icon={() => <SquareArrowOutUpRight color={disableCardIn ? "gray" : "white"} size={18}/>}>
|
||||||
{cardInCompleted === null ? "Card In" : cardInCompleted ? "Card In Success" : "Failed"}
|
{cardInCompleted === null ? "Card In" : cardInCompleted ? "Card In Success" : "Failed"}
|
||||||
</Button>
|
</Button>
|
||||||
</>
|
</>
|
||||||
@@ -213,34 +243,34 @@ export default function ServerCardIn({route, navigation}) {
|
|||||||
<Button mode="text"
|
<Button mode="text"
|
||||||
icon={() => <Plus color={theme.colors.primary} size={18}/>}
|
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>}
|
</View>
|
||||||
|
}
|
||||||
</View>
|
</View>
|
||||||
|
|
||||||
{cards.length > 0 && (
|
{cards.length > 0 && (
|
||||||
<View style={styles.section}>
|
<View style={styles.section}>
|
||||||
<Title style={styles.sectionTitle}>Additional Cards</Title>
|
<Text variant="titleLarge" style={styles.sectionTitle}>Additional Cards</Text>
|
||||||
<Button mode="contained"
|
<Button mode="contained"
|
||||||
textColor="white"
|
textColor="white"
|
||||||
buttonColor={theme.colors.tertiary}
|
buttonColor={theme.colors.tertiary}
|
||||||
icon={() => <Plus color="white" size={18}/>}
|
|
||||||
contentStyle={{flexDirection: 'row-reverse'}}
|
contentStyle={{flexDirection: 'row-reverse'}}
|
||||||
|
icon={() => <Plus color="white" size={18}/>}
|
||||||
onPress={() => navigation.navigate('add-card')}>
|
onPress={() => navigation.navigate('add-card')}>
|
||||||
Add card
|
Add Card
|
||||||
</Button>
|
</Button>
|
||||||
|
|
||||||
{unselectedCards.length
|
{unselectedCards.length
|
||||||
? unselectedCards.map((card: AMCard) => (
|
? unselectedCards.map((card: AMCard) => (
|
||||||
<AimeCard card={card}
|
<ICCard card={card}
|
||||||
key={card._id.toHexString()}
|
key={card._id.toHexString()}
|
||||||
actionIcon={<CircleDot color={theme.colors.tertiary} size={24}/>}
|
actionIcon={<CircleDot color={theme.colors.tertiary} size={24}/>}
|
||||||
onPress={() => setUserSelectedCard(card)}>
|
onPress={() => setUserSelectedCard(card)}>
|
||||||
{serverDefaultCard?.code == card.code && <DefaultCardDetail/>}
|
{serverDefaultCard?.code == card.code && <DefaultCardDetail/>}
|
||||||
</AimeCard>))
|
</ICCard>))
|
||||||
: <View style={styles.emptyPlaceholderView}>
|
: <View style={styles.emptyPlaceholderView}>
|
||||||
<EmptyStatePlaceholder message="No additional cards"
|
<EmptyStatePlaceholder message="No additional cards" icon={<SmartphoneNfcIcon color="gray"/>}/>
|
||||||
icon={<SmartphoneNfcIcon color="gray"/>}/>
|
|
||||||
</View>}
|
</View>}
|
||||||
</View>)}
|
</View>)}
|
||||||
</View>
|
</View>
|
||||||
@@ -255,11 +285,11 @@ function StatusAlert(props: {
|
|||||||
if (!props.connectionEstablished) {
|
if (!props.connectionEstablished) {
|
||||||
return (
|
return (
|
||||||
<AMUIStatusAlert color="red" icon={WifiOff} title="Connection Failed">
|
<AMUIStatusAlert color="red" icon={WifiOff} title="Connection Failed">
|
||||||
<Paragraph>
|
<Text variant="bodyMedium">
|
||||||
There was an issue connecting to <Paragraph
|
There was an issue connecting to <Text variant="bodyMedium"
|
||||||
style={{fontFamily: "JetBrainsMono_400Regular"}}>{props.server.serverAddress}</Paragraph>.
|
style={{fontFamily: "JetBrainsMono_400Regular"}}>{props.server.serverAddress}</Text>.
|
||||||
Check the network connection and try again.
|
Check the network connection and try again.
|
||||||
</Paragraph>
|
</Text>
|
||||||
</AMUIStatusAlert>
|
</AMUIStatusAlert>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@@ -268,9 +298,9 @@ function StatusAlert(props: {
|
|||||||
if (props.server.timeSinceLastPoll === null) {
|
if (props.server.timeSinceLastPoll === null) {
|
||||||
return (
|
return (
|
||||||
<AMUIStatusAlert color="#822eff" icon={TvMinimalPlay} title="Server Startup">
|
<AMUIStatusAlert color="#822eff" icon={TvMinimalPlay} title="Server Startup">
|
||||||
<Paragraph>
|
<Text variant="bodyMedium">
|
||||||
Scanning a card below may not have any effect until the service has fully started up.
|
Scanning a card below may not have any effect until the service has fully started up.
|
||||||
</Paragraph>
|
</Text>
|
||||||
</AMUIStatusAlert>
|
</AMUIStatusAlert>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@@ -292,6 +322,28 @@ function DefaultCardDetail() {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const menuActions: MenuAction[] = [
|
||||||
|
{
|
||||||
|
id: "share",
|
||||||
|
title: "Share",
|
||||||
|
image: Platform.select({
|
||||||
|
ios: "square.and.arrow.up",
|
||||||
|
android: "share"
|
||||||
|
})
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "delete",
|
||||||
|
title: "Delete Server",
|
||||||
|
attributes: {
|
||||||
|
destructive: true,
|
||||||
|
},
|
||||||
|
image: Platform.select({
|
||||||
|
ios: "trash",
|
||||||
|
android: "delete"
|
||||||
|
})
|
||||||
|
}
|
||||||
|
];
|
||||||
|
|
||||||
const styles = StyleSheet.create({
|
const styles = StyleSheet.create({
|
||||||
section: {
|
section: {
|
||||||
gap: 10
|
gap: 10
|
||||||
@@ -304,7 +356,7 @@ const styles = StyleSheet.create({
|
|||||||
gap: 15
|
gap: 15
|
||||||
},
|
},
|
||||||
container: {
|
container: {
|
||||||
paddingTop: Platform.OS === 'android' ? 120 : 0,
|
paddingTop: Platform.select({android: 120, default: 0}),
|
||||||
paddingHorizontal: 16,
|
paddingHorizontal: 16,
|
||||||
paddingBottom: 80,
|
paddingBottom: 80,
|
||||||
gap: 20
|
gap: 20
|
||||||
|
|||||||
@@ -1,77 +1,47 @@
|
|||||||
import {Button, Paragraph, useTheme} from "react-native-paper";
|
import {getGameName} from "amnet-shared";
|
||||||
|
import {FlatList, Platform, View} from "react-native";
|
||||||
|
import {Button, Text, useTheme} from "react-native-paper";
|
||||||
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 {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 {EmptyStatePlaceholder} from "../components/EmptyStatePlaceholder";
|
|
||||||
import {AMUICard, AMUICardDetail} from "../components/AMUICard";
|
|
||||||
import TimeAgo from "../components/TimeAgo";
|
import TimeAgo from "../components/TimeAgo";
|
||||||
|
import {AMUICard, AMUICardDetail} from "../components/AMUICard";
|
||||||
|
import {EmptyStatePlaceholder} from "../components/EmptyStatePlaceholder";
|
||||||
|
|
||||||
import RealmContext from "../models/RealmContext";
|
|
||||||
import {AMServer} from "../models/AMServer";
|
import {AMServer} from "../models/AMServer";
|
||||||
import {getGameName} from "amnet-shared";
|
import RealmContext from "../models/RealmContext";
|
||||||
|
|
||||||
const {useRealm, useQuery} = RealmContext;
|
const {useQuery} = RealmContext;
|
||||||
|
|
||||||
function ServerCard(props: { server: AMServer, navigation: any }) {
|
function ServerCard(props: { server: AMServer, navigation: any }) {
|
||||||
const realm = useRealm();
|
const gameName = useMemo(() => getGameName(props.server.gameId ?? ''), [props.server.gameId]);
|
||||||
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()
|
||||||
});
|
});
|
||||||
}, [props.navigation, props.server]);
|
}, [props.navigation, props.server]);
|
||||||
|
|
||||||
const pageContextMenuCallback = useCallback((action: NativeSyntheticEvent<ContextMenuOnPressNativeEvent>) => {
|
|
||||||
switch (action.nativeEvent.indexPath.toString()) {
|
|
||||||
case "0":
|
|
||||||
pageCallback();
|
|
||||||
return;
|
|
||||||
|
|
||||||
case "1,0":
|
|
||||||
Share.share({url: props.server.shareUrl});
|
|
||||||
return;
|
|
||||||
|
|
||||||
case "1,1":
|
|
||||||
realm.write(() => realm.delete(props.server));
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
}, [realm, props.server]);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<ContextMenu onPress={pageContextMenuCallback} actions={[
|
<AMUICard title={props.server.serverName} onPress={pageCallback}>
|
||||||
{title: `Select ${props.server.serverName}`, systemIcon: "chevron.right"},
|
<AMUICardDetail>
|
||||||
{
|
<Globe size={18} color="gray"/>
|
||||||
title: Platform.OS === "ios" ? "" : "Additional Actions",
|
<Text variant="bodyMedium" style={{color: "gray", fontFamily: "JetBrainsMono_400Regular"}}>
|
||||||
inlineChildren: true,
|
{props.server.serverAddress}
|
||||||
actions: [
|
</Text>
|
||||||
{title: "Share Server", systemIcon: "square.and.arrow.up"},
|
</AMUICardDetail>
|
||||||
{title: "Delete Server", systemIcon: "trash", destructive: true}
|
{gameName && (
|
||||||
]
|
|
||||||
}
|
|
||||||
]}>
|
|
||||||
<AMUICard title={props.server.serverName} onPress={pageCallback}>
|
|
||||||
<AMUICardDetail>
|
<AMUICardDetail>
|
||||||
<Globe size={18} color="gray"/>
|
<Joystick size={18} color="gray"/>
|
||||||
<Paragraph style={{color: "gray", fontFamily: "JetBrainsMono_400Regular"}}>
|
<Text variant="bodyMedium" style={{color: "gray"}}>{gameName}</Text>
|
||||||
{props.server.serverAddress}
|
|
||||||
</Paragraph>
|
|
||||||
</AMUICardDetail>
|
</AMUICardDetail>
|
||||||
{gameName && (
|
)}
|
||||||
<AMUICardDetail>
|
<AMUICardDetail>
|
||||||
<Joystick size={18} color="gray"/>
|
<History size={18} color="gray"/>
|
||||||
<Paragraph style={{color: "gray"}}>{gameName}</Paragraph>
|
{props.server.lastUsed
|
||||||
</AMUICardDetail>
|
? (<TimeAgo style={{color: "gray"}} dateTo={props.server.lastUsed}/>)
|
||||||
)}
|
: (<Text variant="bodyMedium" style={{color: "gray"}}>never</Text>)}
|
||||||
<AMUICardDetail>
|
</AMUICardDetail>
|
||||||
<History size={18} color="gray"/>
|
</AMUICard>
|
||||||
{props.server.lastUsed
|
|
||||||
? (<TimeAgo style={{color: "gray"}} dateTo={props.server.lastUsed}/>)
|
|
||||||
: (<Paragraph style={{color: "gray"}}>never</Paragraph>)}
|
|
||||||
</AMUICardDetail>
|
|
||||||
</AMUICard>
|
|
||||||
</ContextMenu>
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -87,7 +57,7 @@ export default function ServerManager({navigation}) {
|
|||||||
q = q.filtered("serverName CONTAINS[c] $0 OR serverAddress CONTAINS[c] $0", searchQuery);
|
q = q.filtered("serverName CONTAINS[c] $0 OR serverAddress CONTAINS[c] $0", searchQuery);
|
||||||
}
|
}
|
||||||
|
|
||||||
return q.sorted("lastUsed");
|
return q.sorted("lastUsed", true);
|
||||||
}, [query, searchQuery]);
|
}, [query, searchQuery]);
|
||||||
|
|
||||||
// enable searchbar
|
// enable searchbar
|
||||||
|
|||||||
@@ -18,16 +18,19 @@ export default function ServerManagerStack({navigation}) {
|
|||||||
headerBlurEffect: "regular",
|
headerBlurEffect: "regular",
|
||||||
headerStyle: {
|
headerStyle: {
|
||||||
backgroundColor: theme.colors.background
|
backgroundColor: theme.colors.background
|
||||||
|
},
|
||||||
|
headerTitleStyle: {
|
||||||
|
fontWeight: "bold"
|
||||||
|
},
|
||||||
|
headerLargeTitleStyle: {
|
||||||
|
fontWeight: "bold"
|
||||||
}
|
}
|
||||||
}}>
|
}}>
|
||||||
<Stack.Screen name="server-listing"
|
<Stack.Screen name="server-listing"
|
||||||
component={ServerManager}
|
component={ServerManager}
|
||||||
options={{
|
options={{
|
||||||
title: "Servers",
|
title: "Servers",
|
||||||
headerRight: () => Platform.OS === 'android' ? null : <TouchableOpacity
|
headerRight: () => Platform.OS === 'android' ? null : <TouchableOpacity onPress={() => navigation.navigate('add-server')}><Plus color={theme.colors.primary}/></TouchableOpacity>
|
||||||
onPress={() => navigation.navigate('add-server')}>
|
|
||||||
<Plus/>
|
|
||||||
</TouchableOpacity>
|
|
||||||
}}/>
|
}}/>
|
||||||
|
|
||||||
<Stack.Screen name="server" options={{title: "Server", headerTransparent: true}} component={ServerCardIn}/>
|
<Stack.Screen name="server" options={{title: "Server", headerTransparent: true}} component={ServerCardIn}/>
|
||||||
|
|||||||
@@ -0,0 +1,87 @@
|
|||||||
|
import React from "react";
|
||||||
|
import {StatusBar} from "expo-status-bar";
|
||||||
|
import {Platform, TouchableOpacity, View} from "react-native";
|
||||||
|
import {createNativeStackNavigator} from "@react-navigation/native-stack";
|
||||||
|
|
||||||
|
import {Card, Text, useTheme} from "react-native-paper";
|
||||||
|
import {SquareAsterisk, SquarePlus} from "lucide-react-native";
|
||||||
|
|
||||||
|
import ManualCardEntry from "./ManualCardEntry";
|
||||||
|
|
||||||
|
const Stack = createNativeStackNavigator();
|
||||||
|
|
||||||
|
function CardAddModal() {
|
||||||
|
return (
|
||||||
|
<Stack.Navigator initialRouteName="Select" screenOptions={{headerTitleStyle: {fontWeight: "bold"}}}>
|
||||||
|
<Stack.Screen name="Select" component={CardAddMethodSelector} options={{headerShown: false}}/>
|
||||||
|
|
||||||
|
<Stack.Screen name="Manual" component={ManualCardEntry} options={{title: "Add Card Manually"}}/>
|
||||||
|
</Stack.Navigator>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function CardAddMethodSelector({navigation}) {
|
||||||
|
const theme = useTheme();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
{Platform.OS === 'ios' && <StatusBar style="light"/>}
|
||||||
|
<View style={{paddingHorizontal: 15, paddingTop: 90, gap: 8}}>
|
||||||
|
<View style={{paddingBottom: 25, gap: 5}}>
|
||||||
|
<Text variant="titleLarge" style={{textAlign: "center", fontSize: 32, lineHeight: 32, fontWeight: "bold"}}>
|
||||||
|
Add new Card
|
||||||
|
</Text>
|
||||||
|
<Text variant="bodyMedium" style={{textAlign: "center"}}>
|
||||||
|
AMNet supports scanning physical cards, entering details manually or generating new ones.
|
||||||
|
</Text>
|
||||||
|
</View>
|
||||||
|
|
||||||
|
<MethodCard color={theme.colors.secondary}
|
||||||
|
icon={<SquareAsterisk/>}
|
||||||
|
title="Enter Details Manually"
|
||||||
|
onPress={() => navigation.navigate('Manual')}/>
|
||||||
|
|
||||||
|
<MethodCard color="#4caf50"
|
||||||
|
icon={<SquarePlus/>}
|
||||||
|
title="Generate New Card"
|
||||||
|
onPress={() => navigation.navigate('Manual', {autoGenerate: true})}/>
|
||||||
|
</View>
|
||||||
|
</>)
|
||||||
|
}
|
||||||
|
|
||||||
|
function MethodCard(props: {
|
||||||
|
title: string,
|
||||||
|
description?: string,
|
||||||
|
color: string,
|
||||||
|
disabled?: boolean,
|
||||||
|
icon: React.ReactElement,
|
||||||
|
onPress: () => void
|
||||||
|
}) {
|
||||||
|
const theme = useTheme();
|
||||||
|
const large = props.description?.length > 0;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<TouchableOpacity onPress={() => !props.disabled && props.onPress()} disabled={props.disabled}>
|
||||||
|
<Card mode={theme.dark ? "elevated" : "contained"} disabled={props.disabled}>
|
||||||
|
<Card.Content style={{display: "flex", flexDirection: "row", alignItems: "center", gap: 15}}>
|
||||||
|
<View style={{
|
||||||
|
borderRadius: large ? 10 : 5,
|
||||||
|
height: large ? 50 : 30,
|
||||||
|
width: large ? 50 : 30,
|
||||||
|
backgroundColor: props.color,
|
||||||
|
justifyContent: "center",
|
||||||
|
alignItems: "center"
|
||||||
|
}}>
|
||||||
|
{React.cloneElement(props.icon, {size: large ? 24 : 16, color: theme.colors.surfaceVariant})}
|
||||||
|
</View>
|
||||||
|
<View style={{flexShrink: 1}}>
|
||||||
|
<Text style={{fontSize: large ? 16 : 14}}>{props.title}</Text>
|
||||||
|
{props.description && (<Text variant="bodyMedium" style={{fontSize: 12}}>{props.description}</Text>)}
|
||||||
|
</View>
|
||||||
|
</Card.Content>
|
||||||
|
</Card>
|
||||||
|
</TouchableOpacity>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default CardAddModal;
|
||||||
@@ -0,0 +1,99 @@
|
|||||||
|
import {BSON} from "realm";
|
||||||
|
import {StatusBar} from "expo-status-bar";
|
||||||
|
import {Card, Divider, Text, useTheme} from "react-native-paper";
|
||||||
|
import {useCallback, useEffect, useLayoutEffect, useState} from "react";
|
||||||
|
import {Platform, TextInput, TouchableOpacity, View} from "react-native";
|
||||||
|
|
||||||
|
import {useModalStyles} from "../../utils/ModalStyles";
|
||||||
|
|
||||||
|
import {AMCard} from "../../models/AMCard";
|
||||||
|
import RealmContext from "../../models/RealmContext";
|
||||||
|
|
||||||
|
const {useObject, useRealm} = RealmContext;
|
||||||
|
|
||||||
|
export default function CardEditorModal({navigation, route}) {
|
||||||
|
if (!route.params || !route.params.cardId) {
|
||||||
|
navigation.goBack();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const theme = useTheme();
|
||||||
|
const modalStyles = useModalStyles();
|
||||||
|
|
||||||
|
const realm = useRealm();
|
||||||
|
const card = useObject(AMCard, BSON.ObjectId.createFromHexString(route.params.cardId));
|
||||||
|
|
||||||
|
const [cardName, setCardName] = useState<string>('');
|
||||||
|
|
||||||
|
const updateCardCallback = useCallback((newName: string) => {
|
||||||
|
realm.write(() => {
|
||||||
|
card.name = newName;
|
||||||
|
});
|
||||||
|
|
||||||
|
navigation.goBack();
|
||||||
|
}, [realm, navigation]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (card?.isValid() !== true) {
|
||||||
|
navigation.goBack();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!cardName.length) {
|
||||||
|
setCardName(card.name);
|
||||||
|
}
|
||||||
|
}, [card]);
|
||||||
|
|
||||||
|
// header effects
|
||||||
|
if (Platform.OS === 'ios') {
|
||||||
|
useLayoutEffect(() => {
|
||||||
|
navigation.setOptions({
|
||||||
|
headerLeft: () => <TouchableOpacity onPress={() => navigation.goBack()}>
|
||||||
|
<Text variant="bodyMedium" style={modalStyles.headerText}>Cancel</Text>
|
||||||
|
</TouchableOpacity>,
|
||||||
|
});
|
||||||
|
}, [navigation, modalStyles]);
|
||||||
|
}
|
||||||
|
|
||||||
|
useLayoutEffect(() => {
|
||||||
|
navigation.setOptions({
|
||||||
|
headerRight: () => <TouchableOpacity onPress={() => updateCardCallback(cardName)} disabled={!cardName.length}>
|
||||||
|
<Text variant="bodyMedium" style={modalStyles.headerText}>Save</Text>
|
||||||
|
</TouchableOpacity>
|
||||||
|
});
|
||||||
|
}, [cardName]);
|
||||||
|
|
||||||
|
return (<>
|
||||||
|
{Platform.OS === 'ios' && <StatusBar style="light"/>}
|
||||||
|
<View style={modalStyles.rootLayout}>
|
||||||
|
<Card mode="contained">
|
||||||
|
<Card.Content>
|
||||||
|
<View style={modalStyles.formItemRow}>
|
||||||
|
<Text variant="bodyMedium" style={modalStyles.formItemName}>
|
||||||
|
Card Name
|
||||||
|
</Text>
|
||||||
|
<TextInput style={modalStyles.formItemInput}
|
||||||
|
autoFocus
|
||||||
|
numberOfLines={1}
|
||||||
|
value={cardName}
|
||||||
|
onChangeText={setCardName}
|
||||||
|
placeholder="Primary Card"
|
||||||
|
placeholderTextColor={theme.colors.outline}/>
|
||||||
|
</View>
|
||||||
|
|
||||||
|
<Divider style={{marginVertical: 15}}/>
|
||||||
|
|
||||||
|
<View style={modalStyles.formItemRow}>
|
||||||
|
<Text variant="bodyMedium" style={modalStyles.formItemName}>
|
||||||
|
Access Code
|
||||||
|
</Text>
|
||||||
|
<TextInput readOnly
|
||||||
|
numberOfLines={1}
|
||||||
|
value={card?.code.match(/.{1,4}/g)?.join(" ")}
|
||||||
|
style={{...modalStyles.formItemInput, color: theme.colors.onSurfaceDisabled}}/>
|
||||||
|
</View>
|
||||||
|
</Card.Content>
|
||||||
|
</Card>
|
||||||
|
</View>
|
||||||
|
</>)
|
||||||
|
}
|
||||||
@@ -0,0 +1,129 @@
|
|||||||
|
import {StatusBar} from "expo-status-bar";
|
||||||
|
import {generateCardNumber, DecodeAMAccessCode} from "amnet-shared";
|
||||||
|
import {useCallback, useEffect, useLayoutEffect, useState} from "react";
|
||||||
|
import {Button, Card, Divider, Text, useTheme} from "react-native-paper";
|
||||||
|
import {Alert, Platform, TextInput, TouchableOpacity, View} from "react-native";
|
||||||
|
|
||||||
|
import {AMCard} from "../../models/AMCard";
|
||||||
|
import RealmContext from "../../models/RealmContext";
|
||||||
|
import {useModalStyles} from "../../utils/ModalStyles";
|
||||||
|
|
||||||
|
const {useRealm} = RealmContext;
|
||||||
|
|
||||||
|
export default function CardAddModal({navigation, route}) {
|
||||||
|
const realm = useRealm();
|
||||||
|
const theme = useTheme();
|
||||||
|
const modalStyles = useModalStyles();
|
||||||
|
|
||||||
|
// state values
|
||||||
|
const [cardName, setCardName] = useState('');
|
||||||
|
const [cardNumber, setCardNumber] = useState('');
|
||||||
|
|
||||||
|
const cardAddCallback = useCallback(async (cardId: string, cardName: string) => {
|
||||||
|
try {
|
||||||
|
// validate AM card numbers
|
||||||
|
cardId.startsWith('5') && DecodeAMAccessCode(cardId);
|
||||||
|
} catch {
|
||||||
|
const shouldContinue = await new Promise<boolean>(done => {
|
||||||
|
Alert.alert(
|
||||||
|
"Invalid Access Code",
|
||||||
|
"It looks like the access code isn't from an Amusement IC card. Please double-check the code, and if it's correct, note that some networks might still reject it.",
|
||||||
|
[
|
||||||
|
{
|
||||||
|
style: "destructive",
|
||||||
|
text: "Continue Anyway",
|
||||||
|
onPress: () => done(true)
|
||||||
|
},
|
||||||
|
{
|
||||||
|
style: "cancel",
|
||||||
|
text: "Cancel",
|
||||||
|
onPress: () => done(false)
|
||||||
|
}
|
||||||
|
], {
|
||||||
|
cancelable: true,
|
||||||
|
onDismiss: () => done(false)
|
||||||
|
});
|
||||||
|
})
|
||||||
|
|
||||||
|
if (!shouldContinue) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const duplicateCard = realm.objects<AMCard>(AMCard.schema.name).find(x => x.code === cardId);
|
||||||
|
|
||||||
|
if (duplicateCard) {
|
||||||
|
realm.write(() => {
|
||||||
|
duplicateCard.name = cardName;
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
realm.write(() => {
|
||||||
|
realm.create<AMCard>(AMCard.schema.name, AMCard.generate(cardId, cardName));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (Platform.OS === 'ios') {
|
||||||
|
navigation.getParent()?.goBack();
|
||||||
|
} else {
|
||||||
|
navigation.goBack();
|
||||||
|
}
|
||||||
|
}, [realm]);
|
||||||
|
|
||||||
|
useLayoutEffect(() => {
|
||||||
|
const canSave = cardName.length && cardNumber.length === 20;
|
||||||
|
const themeOptions = canSave ? modalStyles.headerText : {color: theme.colors.outline};
|
||||||
|
|
||||||
|
navigation.setOptions({
|
||||||
|
headerRight: () => <TouchableOpacity disabled={!canSave}
|
||||||
|
onPress={() => cardAddCallback(cardNumber, cardName)}>
|
||||||
|
<Text variant="bodyMedium" style={{...themeOptions}}>Save</Text>
|
||||||
|
</TouchableOpacity>
|
||||||
|
})
|
||||||
|
}, [navigation, modalStyles, cardName, cardNumber]);
|
||||||
|
|
||||||
|
// check params for autoGenerate request
|
||||||
|
useEffect(() => {
|
||||||
|
if (route.params?.autoGenerate) {
|
||||||
|
setCardNumber(generateCardNumber());
|
||||||
|
}
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
return (<>
|
||||||
|
{Platform.OS == 'ios' && <StatusBar style="light"/>}
|
||||||
|
<View style={modalStyles.rootLayout}>
|
||||||
|
<Card mode="contained">
|
||||||
|
<Card.Content>
|
||||||
|
<View style={modalStyles.formItemRow}>
|
||||||
|
<Text variant="bodyMedium" style={modalStyles.formItemName}>
|
||||||
|
Card Name
|
||||||
|
</Text>
|
||||||
|
<TextInput style={modalStyles.formItemInput}
|
||||||
|
autoFocus
|
||||||
|
numberOfLines={1}
|
||||||
|
value={cardName}
|
||||||
|
onChangeText={setCardName}
|
||||||
|
placeholder="Primary Card"
|
||||||
|
placeholderTextColor={theme.colors.outline}/>
|
||||||
|
</View>
|
||||||
|
|
||||||
|
<Divider style={{marginVertical: 15}}/>
|
||||||
|
|
||||||
|
<View style={modalStyles.formItemRow}>
|
||||||
|
<Text variant="bodyMedium" style={modalStyles.formItemName}>
|
||||||
|
Access Code
|
||||||
|
</Text>
|
||||||
|
<TextInput numberOfLines={1}
|
||||||
|
keyboardType="numeric"
|
||||||
|
placeholder="0000 0000 0000 0000 0000"
|
||||||
|
placeholderTextColor={theme.colors.outline}
|
||||||
|
value={cardNumber.match(/.{1,4}/g)?.join(" ")}
|
||||||
|
onChangeText={val => setCardNumber(val?.replaceAll(/\D/g, '').substring(0, 20) ?? '')}
|
||||||
|
style={{...modalStyles.formItemInput, color: theme.colors.onSurface}}/>
|
||||||
|
</View>
|
||||||
|
</Card.Content>
|
||||||
|
</Card>
|
||||||
|
|
||||||
|
<Button mode="text" onPress={() => setCardNumber(generateCardNumber())}>Generate Access Code</Button>
|
||||||
|
</View>
|
||||||
|
</>)
|
||||||
|
}
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
import React from "react";
|
import React from "react";
|
||||||
import {TouchableOpacity, View} from "react-native";
|
import {TouchableOpacity, View} from "react-native";
|
||||||
import {Card, Title, useTheme} from "react-native-paper";
|
import {Card, Text, useTheme} from "react-native-paper";
|
||||||
|
|
||||||
export interface AMUICardActionProps {
|
export interface AMUICardActionProps {
|
||||||
actionIcon?: Readonly<React.ReactNode>,
|
actionIcon?: Readonly<React.ReactNode>,
|
||||||
@@ -21,7 +21,7 @@ export function AMUICard(props: AMUICardActionProps & AMUICardProps) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<TouchableOpacity disabled={props.actionDisabled} onPress={props.onPress} onLongPress={props.onLongPress}>
|
<TouchableOpacity disabled={props.actionDisabled} onPress={props.onPress} onLongPress={props.onLongPress} delayLongPress={150}>
|
||||||
<CardComponent {...props}/>
|
<CardComponent {...props}/>
|
||||||
</TouchableOpacity>
|
</TouchableOpacity>
|
||||||
);
|
);
|
||||||
@@ -48,7 +48,7 @@ function CardComponent(props: AMUICardProps & AMUICardActionProps) {
|
|||||||
gap: 5
|
gap: 5
|
||||||
}}>
|
}}>
|
||||||
<View style={{flexGrow: 1}}>
|
<View style={{flexGrow: 1}}>
|
||||||
<Title style={{fontWeight: "bold"}}>{props.title}</Title>
|
<Text variant="titleLarge" style={{fontWeight: "bold"}}>{props.title}</Text>
|
||||||
<View style={{display: "flex", gap: 8, flexWrap: "wrap", marginTop: 10}}>
|
<View style={{display: "flex", gap: 8, flexWrap: "wrap", marginTop: 10}}>
|
||||||
{props.children}
|
{props.children}
|
||||||
</View>
|
</View>
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import {View} from "react-native";
|
import {View} from "react-native";
|
||||||
import React, {ReactNode} from "react";
|
import React, {ReactNode} from "react";
|
||||||
import {Card, Title} from "react-native-paper";
|
import {Card, Text} from "react-native-paper";
|
||||||
import {LucideIcon, LucideProps} from "lucide-react-native";
|
import {LucideIcon, LucideProps} from "lucide-react-native";
|
||||||
|
|
||||||
export function AMUIStatusAlert(props: { title: string, color: string, icon: LucideIcon, children?: ReactNode }) {
|
export function AMUIStatusAlert(props: { title: string, color: string, icon: LucideIcon, children?: ReactNode }) {
|
||||||
@@ -9,7 +9,7 @@ export function AMUIStatusAlert(props: { title: string, color: string, icon: Luc
|
|||||||
<Card.Content>
|
<Card.Content>
|
||||||
<View style={{alignItems: "center", flexDirection: "row", gap: 10}}>
|
<View style={{alignItems: "center", flexDirection: "row", gap: 10}}>
|
||||||
{renderIconWithProps(props.icon, {size: 22, color: props.color})}
|
{renderIconWithProps(props.icon, {size: 22, color: props.color})}
|
||||||
<Title style={{color: props.color}}>{props.title}</Title>
|
<Text variant="titleLarge" style={{color: props.color}}>{props.title}</Text>
|
||||||
</View>
|
</View>
|
||||||
{props.children}
|
{props.children}
|
||||||
</Card.Content>
|
</Card.Content>
|
||||||
|
|||||||
@@ -1,36 +0,0 @@
|
|||||||
import {ReactNode, useMemo} from "react";
|
|
||||||
import {Text} from "react-native";
|
|
||||||
import {CreditCard, History} from "lucide-react-native";
|
|
||||||
|
|
||||||
import TimeAgo from "./TimeAgo";
|
|
||||||
import {AMCardData} from "../models/AMCard";
|
|
||||||
import {AMUICard, AMUICardActionProps, AMUICardDetail} from "./AMUICard";
|
|
||||||
|
|
||||||
export interface AimeCardProps extends AMUICardActionProps {
|
|
||||||
card: AMCardData,
|
|
||||||
children?: ReactNode,
|
|
||||||
showFullCardNumber?: boolean
|
|
||||||
}
|
|
||||||
|
|
||||||
export function AimeCard(props: AimeCardProps) {
|
|
||||||
const displayedCardNumber = useMemo(() => props.showFullCardNumber
|
|
||||||
? props.card.code.match(/.{1,4}/g)?.join(" ")
|
|
||||||
: `•••• ${props.card.code.substring(props.card.code.length - 4)}`,
|
|
||||||
[props.showFullCardNumber, props.card.code]);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<AMUICard title={props.card.name} {...props}>
|
|
||||||
<AMUICardDetail>
|
|
||||||
<CreditCard size={18} color="gray"/>
|
|
||||||
<Text style={{color: "gray", fontFamily: "JetBrainsMono_400Regular"}}>{displayedCardNumber}</Text>
|
|
||||||
</AMUICardDetail>
|
|
||||||
{props.card.lastUsed && (
|
|
||||||
<AMUICardDetail>
|
|
||||||
<History size={18} color="gray"/>
|
|
||||||
<TimeAgo style={{color: "gray"}} dateTo={props.card.lastUsed}/>
|
|
||||||
</AMUICardDetail>
|
|
||||||
)}
|
|
||||||
{props.children}
|
|
||||||
</AMUICard>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -1,13 +1,13 @@
|
|||||||
import {View} from "react-native";
|
import {View} from "react-native";
|
||||||
import {Paragraph} from "react-native-paper";
|
import {Text} from "react-native-paper";
|
||||||
|
|
||||||
export function EmptyStatePlaceholder(props: { message: string, icon: React.ReactNode }) {
|
export function EmptyStatePlaceholder(props: { message: string, icon: React.ReactNode }) {
|
||||||
return (
|
return (
|
||||||
<View style={{display: "flex", gap: 5, alignItems: "center"}}>
|
<View style={{display: "flex", gap: 5, alignItems: "center"}}>
|
||||||
{props.icon}
|
{props.icon}
|
||||||
<Paragraph style={{color: "gray", fontSize: 14}}>
|
<Text variant="bodyMedium" style={{color: "gray", fontSize: 14}}>
|
||||||
{props.message}
|
{props.message}
|
||||||
</Paragraph>
|
</Text>
|
||||||
</View>
|
</View>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@@ -0,0 +1,86 @@
|
|||||||
|
import {Text} from "react-native";
|
||||||
|
import React, {ReactNode, useMemo} from "react";
|
||||||
|
import {CreditCard, Gem, History, Joystick, LucideIcon, RailSymbol} from "lucide-react-native";
|
||||||
|
|
||||||
|
import TimeAgo from "./TimeAgo";
|
||||||
|
import {AMUICard, AMUICardActionProps, AMUICardDetail} from "./AMUICard";
|
||||||
|
|
||||||
|
import {AmusementICVendor, ICType} from "../lib/iccard";
|
||||||
|
import {AMCardData} from "../models/AMCardData";
|
||||||
|
|
||||||
|
export interface ICCardProps extends AMUICardActionProps {
|
||||||
|
card: AMCardData,
|
||||||
|
children?: ReactNode,
|
||||||
|
showFullCardNumber?: boolean
|
||||||
|
}
|
||||||
|
|
||||||
|
export function ICCard(props: ICCardProps) {
|
||||||
|
const displayedCardNumber = useMemo(() => props.showFullCardNumber
|
||||||
|
? props.card.code.match(/.{1,4}/g)?.join(" ")
|
||||||
|
: `•••• ${props.card.code.substring(props.card.code.length - 4)}`,
|
||||||
|
[props.showFullCardNumber, props.card.code]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<AMUICard title={props.card.name} {...props}>
|
||||||
|
<PhysicalICInfo card={props.card}/>
|
||||||
|
|
||||||
|
<AMUICardDetail>
|
||||||
|
<CreditCard size={18} color="gray"/>
|
||||||
|
<Text style={{color: "gray", fontFamily: "JetBrainsMono_400Regular"}}>{displayedCardNumber}</Text>
|
||||||
|
</AMUICardDetail>
|
||||||
|
|
||||||
|
{props.card.lastUsed && (
|
||||||
|
<AMUICardDetail>
|
||||||
|
<History size={18} color="gray"/>
|
||||||
|
<TimeAgo style={{color: "gray"}} dateTo={props.card.lastUsed}/>
|
||||||
|
</AMUICardDetail>
|
||||||
|
)}
|
||||||
|
{props.children}
|
||||||
|
</AMUICard>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function PhysicalICInfo(props: { card: AMCardData | null }) {
|
||||||
|
if (props.card.physicalCardData?.cardType === ICType.Transport) {
|
||||||
|
return (
|
||||||
|
<AMUICardDetail>
|
||||||
|
<RailSymbol size={18} color="gray"/>
|
||||||
|
<Text style={{color: "gray"}}>Transport IC Card</Text>
|
||||||
|
</AMUICardDetail>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (props.card.physicalCardData?.cardVendor) {
|
||||||
|
const [icon, color]: [LucideIcon, string] = props.card.isLimitedAimeCard ? [Gem, "crimson"] : [Joystick, "gray"];
|
||||||
|
return (
|
||||||
|
<AMUICardDetail>
|
||||||
|
{React.createElement(icon, {size: 18, color})}
|
||||||
|
<Text style={{color}}>
|
||||||
|
{props.card.physicalCardData.isBootlegAmusementCard ? "Bootleg " : null}{ICVendorName(props.card.physicalCardData.cardVendor)}
|
||||||
|
</Text>
|
||||||
|
</AMUICardDetail>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
function ICVendorName(vendor: AmusementICVendor): string {
|
||||||
|
switch (vendor) {
|
||||||
|
case AmusementICVendor.Sega:
|
||||||
|
return "Aime Card";
|
||||||
|
|
||||||
|
case AmusementICVendor.Konami:
|
||||||
|
return "e-Amusement Pass";
|
||||||
|
|
||||||
|
case AmusementICVendor.BandaiNamcoGames:
|
||||||
|
case AmusementICVendor.BandaiNamcoEntertainment:
|
||||||
|
return "Banapass";
|
||||||
|
|
||||||
|
case AmusementICVendor.Nesica:
|
||||||
|
return "NESiCA Card";
|
||||||
|
|
||||||
|
default:
|
||||||
|
return "Amusement IC Card";
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,7 +1,7 @@
|
|||||||
|
import {Text} from "react-native-paper";
|
||||||
import React, {useEffect, useState} from 'react';
|
import React, {useEffect, useState} from 'react';
|
||||||
import {StyleProp, TextStyle} from 'react-native';
|
import {StyleProp, TextStyle} from 'react-native';
|
||||||
import {formatDistance} from 'date-fns/formatDistance';
|
import {formatDistance} from 'date-fns/formatDistance';
|
||||||
import {Paragraph} from "react-native-paper";
|
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
dateTo: Date;
|
dateTo: Date;
|
||||||
@@ -28,11 +28,11 @@ const TimeAgo: React.FC<Props> = ({
|
|||||||
}, [dateFrom, updateInterval]);
|
}, [dateFrom, updateInterval]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Paragraph {...{style}}>
|
<Text variant="bodyMedium" {...{style}}>
|
||||||
{formatDistance(dateTo, dateFrom || currentDate, {
|
{formatDistance(dateTo, dateFrom || currentDate, {
|
||||||
addSuffix: !hideAgo,
|
addSuffix: !hideAgo,
|
||||||
})}
|
})}
|
||||||
</Paragraph>
|
</Text>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,38 @@
|
|||||||
|
export enum ICType {
|
||||||
|
Amusement = 1,
|
||||||
|
Transport = 2
|
||||||
|
}
|
||||||
|
|
||||||
|
export enum AmusementICVendor {
|
||||||
|
Sega = 0x0078,
|
||||||
|
Konami = 0x0068,
|
||||||
|
BandaiNamcoEntertainment = 0x002A,
|
||||||
|
BandaiNamcoGames = 0x003A,
|
||||||
|
Nesica = 0x0079,
|
||||||
|
Unknown = 0x0000
|
||||||
|
}
|
||||||
|
|
||||||
|
type AmusementICCard = {
|
||||||
|
type: ICType.Amusement,
|
||||||
|
|
||||||
|
idm: string,
|
||||||
|
accessCode: string,
|
||||||
|
vendor: AmusementICVendor
|
||||||
|
}
|
||||||
|
|
||||||
|
type TransportICCard = {
|
||||||
|
type: ICType.Transport,
|
||||||
|
|
||||||
|
idm: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export type ICCard = AmusementICCard | TransportICCard;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Converts an IC card IDm to the 20-digit "0008" number used by some networks
|
||||||
|
* @param idm The IDm to convert
|
||||||
|
*/
|
||||||
|
export function icCard0008Number(idm: string): string {
|
||||||
|
// 0008 is the numerical representation of the idm padded with zeros until it is 20 digits
|
||||||
|
return BigInt(`0x${idm}`).toString().padStart(20, '0');
|
||||||
|
}
|
||||||
@@ -6,7 +6,8 @@ module.exports = (async () => {
|
|||||||
const config = await getDefaultConfig(__dirname);
|
const config = await getDefaultConfig(__dirname);
|
||||||
|
|
||||||
config.resolver.extraNodeModules = {
|
config.resolver.extraNodeModules = {
|
||||||
'amnet-shared': resolve(__dirname, '../amnet-shared')
|
'amnet-shared': resolve(__dirname, '../amnet-shared'),
|
||||||
|
'@babel/runtime': resolve(__dirname, 'node_modules/@babel/runtime')
|
||||||
};
|
};
|
||||||
|
|
||||||
config.watchFolders = [
|
config.watchFolders = [
|
||||||
|
|||||||
@@ -1,12 +1,10 @@
|
|||||||
import {Realm} from "realm";
|
import {Realm} from "realm";
|
||||||
|
|
||||||
export interface AMCardData {
|
import {ICType} from "../lib/iccard";
|
||||||
code: string;
|
|
||||||
name: string;
|
|
||||||
|
|
||||||
added: Date;
|
import {AMCardData} from "./AMCardData";
|
||||||
lastUsed?: Date | null;
|
import {AMCardRecentUse} from "./AMCardRecentUse";
|
||||||
}
|
import {AMPhysicalCardData} from "./AMPhysicalCardData";
|
||||||
|
|
||||||
export class AMCard extends Realm.Object<AMCard> implements AMCardData {
|
export class AMCard extends Realm.Object<AMCard> implements AMCardData {
|
||||||
_id!: Realm.BSON.ObjectId;
|
_id!: Realm.BSON.ObjectId;
|
||||||
@@ -15,8 +13,15 @@ export class AMCard extends Realm.Object<AMCard> implements AMCardData {
|
|||||||
name: string;
|
name: string;
|
||||||
|
|
||||||
added: Date;
|
added: Date;
|
||||||
lastUsed?: Date;
|
lastUsed: Date;
|
||||||
lastUsedGameId?: string | null;
|
|
||||||
|
recentUses: Realm.Results<AMCardRecentUse>;
|
||||||
|
physicalCardData?: AMPhysicalCardData | null = null;
|
||||||
|
|
||||||
|
get isLimitedAimeCard() {
|
||||||
|
// limited cards use the company code 500
|
||||||
|
return this.physicalCardData?.cardType === ICType.Amusement && this.code.startsWith('500');
|
||||||
|
}
|
||||||
|
|
||||||
static generate(code: string, name: string) {
|
static generate(code: string, name: string) {
|
||||||
return {
|
return {
|
||||||
@@ -27,6 +32,18 @@ export class AMCard extends Realm.Object<AMCard> implements AMCardData {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static delete(realm: Realm, card: AMCard) {
|
||||||
|
realm.write(() => {
|
||||||
|
if (card.physicalCardData) {
|
||||||
|
realm.delete(card.physicalCardData);
|
||||||
|
}
|
||||||
|
|
||||||
|
// remove any usage history tied to the card
|
||||||
|
realm.delete(card.recentUses);
|
||||||
|
realm.delete(card);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
static schema: Realm.ObjectSchema = {
|
static schema: Realm.ObjectSchema = {
|
||||||
name: "card",
|
name: "card",
|
||||||
primaryKey: "_id",
|
primaryKey: "_id",
|
||||||
@@ -34,10 +51,12 @@ export class AMCard extends Realm.Object<AMCard> implements AMCardData {
|
|||||||
properties: {
|
properties: {
|
||||||
_id: "objectId",
|
_id: "objectId",
|
||||||
code: "string",
|
code: "string",
|
||||||
name: {type: "string", default: "New Card"},
|
name: {type: "string", default: "New Card", indexed: true},
|
||||||
added: {type: "date", default: new Date()},
|
added: {type: "date", default: new Date(), indexed: true},
|
||||||
lastUsed: {type: "date", optional: true},
|
lastUsed: {type: "date", optional: true, indexed: true},
|
||||||
lastUsedGameId: {type: "string", optional: true}
|
recentUses: {type: "linkingObjects", objectType: AMCardRecentUse.schema.name, property: "card"},
|
||||||
|
physicalCardData: {type: "object", objectType: AMPhysicalCardData.schema.name, optional: true}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,11 @@
|
|||||||
|
import {AMPhysicalCardData} from "./AMPhysicalCardData";
|
||||||
|
|
||||||
|
export interface AMCardData {
|
||||||
|
code: string;
|
||||||
|
name: string;
|
||||||
|
|
||||||
|
added: Date;
|
||||||
|
lastUsed?: Date | null;
|
||||||
|
isLimitedAimeCard: boolean;
|
||||||
|
physicalCardData?: AMPhysicalCardData | null;
|
||||||
|
}
|
||||||
@@ -0,0 +1,37 @@
|
|||||||
|
import {Realm} from "realm";
|
||||||
|
import {AMCard} from "./AMCard";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Represents a usage of a card for determining which card was last used on a specific game.
|
||||||
|
*/
|
||||||
|
export class AMCardRecentUse extends Realm.Object<AMCardRecentUse> {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The game id that is being tracked
|
||||||
|
*/
|
||||||
|
gameId: string;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The card that was last used on the game.
|
||||||
|
* If the card is removed, this record will also be removed.
|
||||||
|
*/
|
||||||
|
card: AMCard;
|
||||||
|
|
||||||
|
static generate(gameId: string, card: AMCard) {
|
||||||
|
return {
|
||||||
|
gameId: gameId,
|
||||||
|
card: card
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
static schema: Realm.ObjectSchema = {
|
||||||
|
name: "cardUsageHistory",
|
||||||
|
primaryKey: "gameId",
|
||||||
|
|
||||||
|
properties: {
|
||||||
|
gameId: "string",
|
||||||
|
card: {type: "object", objectType: "card"}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@@ -0,0 +1,28 @@
|
|||||||
|
import {Realm} from "realm";
|
||||||
|
import {AmusementICVendor, ICType} from "../lib/iccard";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Holds physical card information when a card is imported via NFC/FeliCa
|
||||||
|
*/
|
||||||
|
export class AMPhysicalCardData extends Realm.Object<AMPhysicalCardData> {
|
||||||
|
|
||||||
|
cardIDm: string;
|
||||||
|
cardType: ICType;
|
||||||
|
cardVendor: AmusementICVendor | null;
|
||||||
|
|
||||||
|
get isBootlegAmusementCard() {
|
||||||
|
// real cards have an IDm manufacturer code of 012E (first two bytes of the IDm or in this case, the first 4 characters)
|
||||||
|
return this.cardType === ICType.Amusement && !this.cardIDm.startsWith("012E");
|
||||||
|
}
|
||||||
|
|
||||||
|
static schema: Realm.ObjectSchema = {
|
||||||
|
name: "physicalCard",
|
||||||
|
primaryKey: "cardIDm",
|
||||||
|
|
||||||
|
properties: {
|
||||||
|
cardIDm: "string",
|
||||||
|
cardType: "int",
|
||||||
|
cardVendor: {type: "int", optional: true}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
@@ -12,7 +12,9 @@ export class AMServer extends Realm.Object<AMServer> implements AMServerConnecti
|
|||||||
|
|
||||||
lastUsed: Date;
|
lastUsed: Date;
|
||||||
createdAt: Date;
|
createdAt: Date;
|
||||||
|
|
||||||
|
preferredCardIdFormat?: "idm" | "access" | null;
|
||||||
|
|
||||||
get shareUrl() {
|
get shareUrl() {
|
||||||
return `http://card.ppc.moe/#/servers/add?address=${encodeURIComponent(this.serverAddress)}`;
|
return `http://card.ppc.moe/#/servers/add?address=${encodeURIComponent(this.serverAddress)}`;
|
||||||
}
|
}
|
||||||
@@ -40,9 +42,9 @@ export class AMServer extends Realm.Object<AMServer> implements AMServerConnecti
|
|||||||
apiVersion: {type: "int", default: 0},
|
apiVersion: {type: "int", default: 0},
|
||||||
gameId: {type: "string", optional: true},
|
gameId: {type: "string", optional: true},
|
||||||
serverAddress: "string",
|
serverAddress: "string",
|
||||||
serverName: {type: "string", default: "New Server"},
|
serverName: {type: "string", default: "New Server", indexed: "full-text"},
|
||||||
createdAt: {type: "date", default: new Date()},
|
createdAt: {type: "date", default: new Date()},
|
||||||
lastUsed: {type: "date", optional: true}
|
lastUsed: {type: "date", optional: true, indexed: true}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
@@ -1,10 +1,42 @@
|
|||||||
|
import {Realm} from "realm";
|
||||||
import {createRealmContext} from "@realm/react";
|
import {createRealmContext} from "@realm/react";
|
||||||
|
|
||||||
import {AMCard} from "./AMCard";
|
import {AMCard} from "./AMCard";
|
||||||
import {AMServer} from "./AMServer";
|
import {AMServer} from "./AMServer";
|
||||||
|
import {AMCardRecentUse} from "./AMCardRecentUse";
|
||||||
|
import {AMPhysicalCardData} from "./AMPhysicalCardData";
|
||||||
|
|
||||||
|
const migrationFunction = (oldRealm: Realm, newRealm: Realm) => {
|
||||||
|
if (oldRealm.schemaVersion < 5) {
|
||||||
|
const oldCards = oldRealm.objects("card");
|
||||||
|
const gameIdMap: Map<string, { card: any, lastUsed: Date }> = new Map();
|
||||||
|
|
||||||
|
// get last used card for each game
|
||||||
|
oldCards.forEach((card: any) => {
|
||||||
|
if (card.lastUsedGameId && card.lastUsed) {
|
||||||
|
const existing = gameIdMap.get(card.lastUsedGameId);
|
||||||
|
if (!existing || card.lastUsed > existing.lastUsed) {
|
||||||
|
gameIdMap.set(card.lastUsedGameId, {card, lastUsed: card.lastUsed});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// write new history records
|
||||||
|
const newCards = newRealm.objects("card");
|
||||||
|
gameIdMap.forEach((value, gameId) => {
|
||||||
|
const newCard = newCards.filtered(`_id == $0`, value.card._id)[0];
|
||||||
|
newRealm.create("cardUsageHistory", {
|
||||||
|
gameId: gameId,
|
||||||
|
card: newCard
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
export default createRealmContext({
|
export default createRealmContext({
|
||||||
schemaVersion: 3,
|
schemaVersion: 5,
|
||||||
schema: [AMCard, AMServer],
|
schema: [AMPhysicalCardData, AMCardRecentUse, AMCard, AMServer],
|
||||||
deleteRealmIfMigrationNeeded: false
|
onMigration: migrationFunction,
|
||||||
});
|
deleteRealmIfMigrationNeeded: false,
|
||||||
|
});
|
||||||
|
|||||||
Generated
+2179
-5385
File diff suppressed because it is too large
Load Diff
+36
-33
@@ -1,45 +1,48 @@
|
|||||||
{
|
{
|
||||||
"name": "amnet-native",
|
"name": "amnet-native",
|
||||||
"version": "1.1.0",
|
"version": "1.3.9",
|
||||||
"private": true,
|
"private": true,
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"android": "expo run:android",
|
"android": "expo run:android",
|
||||||
"ios": "expo run:ios",
|
"ios": "expo run:ios",
|
||||||
"ios-prebuild": "expo prebuild --platform ios",
|
"ios-prebuild": "expo prebuild --platform ios",
|
||||||
"postinstall": "patch-package"
|
"android-prebuild": "expo prebuild --platform android"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@expo-google-fonts/jetbrains-mono": "^0.2.3",
|
"@babel/runtime": "^7.28.4",
|
||||||
"@react-navigation/bottom-tabs": "^6.6.1",
|
"@expo-google-fonts/jetbrains-mono": "^0.4.1",
|
||||||
"@react-navigation/native": "^6.1.18",
|
"@react-native-menu/menu": "^2.0.0",
|
||||||
"@react-navigation/native-stack": "^6.11.0",
|
"@react-navigation/bottom-tabs": "^7.9.0",
|
||||||
"@realm/react": "^0.11.0",
|
"@react-navigation/native": "^7.1.26",
|
||||||
"@types/react": "~18.3.12",
|
"@react-navigation/native-stack": "^7.9.0",
|
||||||
|
"@realm/react": "^0.20.0",
|
||||||
"amnet-shared": "file:../amnet-shared",
|
"amnet-shared": "file:../amnet-shared",
|
||||||
"date-fns": "^3.6.0",
|
"babel-preset-expo": "^54.0.9",
|
||||||
"expo": "^52.0.0",
|
"date-fns": "^4.1.0",
|
||||||
"expo-blur": "~14.0.1",
|
"expo": "^54.0.30",
|
||||||
"expo-build-properties": "^0.13.1",
|
"expo-build-properties": "~1.0.10",
|
||||||
"expo-clipboard": "~7.0.0",
|
"expo-clipboard": "~8.0.8",
|
||||||
"expo-constants": "~17.0.3",
|
"expo-dev-client": "~6.0.20",
|
||||||
"expo-dev-client": "~5.0.4",
|
"expo-linking": "~8.0.11",
|
||||||
"expo-linking": "~7.0.3",
|
"expo-splash-screen": "~31.0.13",
|
||||||
"expo-status-bar": "~2.0.0",
|
"expo-status-bar": "~3.0.9",
|
||||||
"expo-system-ui": "~4.0.4",
|
"lucide-react-native": "^0.562.0",
|
||||||
"lucide-react-native": "^0.427.0",
|
"pusher-js": "^8.4.0",
|
||||||
"patch-package": "^8.0.0",
|
"react": "19.1.0",
|
||||||
"react": "18.3.1",
|
"react-native": "0.81.5",
|
||||||
"react-native": "0.76.5",
|
"react-native-context-menu-view": "^1.21.0",
|
||||||
"react-native-context-menu-view": "^1.16.0",
|
"react-native-gesture-handler": "^2.30.0",
|
||||||
"react-native-gesture-handler": "~2.20.2",
|
"react-native-get-random-values": "^2.0.0",
|
||||||
"react-native-get-random-values": "^1.11.0",
|
"react-native-paper": "^5.14.5",
|
||||||
"react-native-paper": "^5.12.5",
|
"react-native-reanimated": "~4.2.1",
|
||||||
"react-native-reanimated": "~3.16.1",
|
"react-native-safe-area-context": "5.6.2",
|
||||||
"react-native-safe-area-context": "4.12.0",
|
"react-native-screens": "~4.19.0",
|
||||||
"react-native-screens": "~4.1.0",
|
"react-native-svg": "15.15.1",
|
||||||
"react-native-svg": "15.8.0",
|
"react-native-url-polyfill": "^3.0.0",
|
||||||
"react-native-url-polyfill": "^2.0.0",
|
"realm": "^20.1.0"
|
||||||
"realm": "^12.14.0",
|
},
|
||||||
"typescript": "~5.3.3"
|
"devDependencies": {
|
||||||
|
"@types/react": "~19.1.10",
|
||||||
|
"typescript": "~5.9.3"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,64 +0,0 @@
|
|||||||
diff --git a/node_modules/expo-modules-core/ios/AppDelegates/ExpoAppDelegate.swift b/node_modules/expo-modules-core/ios/AppDelegates/ExpoAppDelegate.swift
|
|
||||||
index 8ba09a7..8d7d2b0 100644
|
|
||||||
--- a/node_modules/expo-modules-core/ios/AppDelegates/ExpoAppDelegate.swift
|
|
||||||
+++ b/node_modules/expo-modules-core/ios/AppDelegates/ExpoAppDelegate.swift
|
|
||||||
@@ -101,59 +101,6 @@ open class ExpoAppDelegate: UIResponder, UIApplicationDelegate {
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
- // MARK: - Handling Remote Notification Registration
|
|
||||||
-
|
|
||||||
- open func application(_ application: UIApplication, didRegisterForRemoteNotificationsWithDeviceToken deviceToken: Data) {
|
|
||||||
- subscribers.forEach { $0.application?(application, didRegisterForRemoteNotificationsWithDeviceToken: deviceToken) }
|
|
||||||
- }
|
|
||||||
-
|
|
||||||
- open func application(_ application: UIApplication, didFailToRegisterForRemoteNotificationsWithError error: Error) {
|
|
||||||
- subscribers.forEach { $0.application?(application, didFailToRegisterForRemoteNotificationsWithError: error) }
|
|
||||||
- }
|
|
||||||
-
|
|
||||||
- open func application(
|
|
||||||
- _ application: UIApplication,
|
|
||||||
- didReceiveRemoteNotification userInfo: [AnyHashable: Any],
|
|
||||||
- fetchCompletionHandler completionHandler: @escaping (UIBackgroundFetchResult) -> Void
|
|
||||||
- ) {
|
|
||||||
- let selector = #selector(application(_:didReceiveRemoteNotification:fetchCompletionHandler:))
|
|
||||||
- let subs = subscribers.filter { $0.responds(to: selector) }
|
|
||||||
- var subscribersLeft = subs.count
|
|
||||||
- let dispatchQueue = DispatchQueue(label: "expo.application.remoteNotification", qos: .userInteractive)
|
|
||||||
- var failedCount = 0
|
|
||||||
- var newDataCount = 0
|
|
||||||
-
|
|
||||||
- let handler = { (result: UIBackgroundFetchResult) in
|
|
||||||
- dispatchQueue.sync {
|
|
||||||
- if result == .failed {
|
|
||||||
- failedCount += 1
|
|
||||||
- } else if result == .newData {
|
|
||||||
- newDataCount += 1
|
|
||||||
- }
|
|
||||||
-
|
|
||||||
- subscribersLeft -= 1
|
|
||||||
-
|
|
||||||
- if subscribersLeft == 0 {
|
|
||||||
- if newDataCount > 0 {
|
|
||||||
- completionHandler(.newData)
|
|
||||||
- } else if failedCount > 0 {
|
|
||||||
- completionHandler(.failed)
|
|
||||||
- } else {
|
|
||||||
- completionHandler(.noData)
|
|
||||||
- }
|
|
||||||
- }
|
|
||||||
- }
|
|
||||||
- }
|
|
||||||
-
|
|
||||||
- if subs.isEmpty {
|
|
||||||
- completionHandler(.noData)
|
|
||||||
- } else {
|
|
||||||
- subs.forEach { subscriber in
|
|
||||||
- subscriber.application?(application, didReceiveRemoteNotification: userInfo, fetchCompletionHandler: handler)
|
|
||||||
- }
|
|
||||||
- }
|
|
||||||
- }
|
|
||||||
-
|
|
||||||
// MARK: - Continuing User Activity and Handling Quick Actions
|
|
||||||
|
|
||||||
open func application(_ application: UIApplication, willContinueUserActivityWithType userActivityType: String) -> Bool {
|
|
||||||
@@ -1,4 +1,7 @@
|
|||||||
{
|
{
|
||||||
"compilerOptions": {},
|
"compilerOptions": {
|
||||||
|
"strictNullChecks": false,
|
||||||
|
"moduleResolution": "bundler"
|
||||||
|
},
|
||||||
"extends": "expo/tsconfig.base"
|
"extends": "expo/tsconfig.base"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -13,7 +13,14 @@ const modalStyles = StyleSheet.create({
|
|||||||
},
|
},
|
||||||
formItemDivider: {
|
formItemDivider: {
|
||||||
marginVertical: 15
|
marginVertical: 15
|
||||||
}
|
},
|
||||||
|
rootLayout: {
|
||||||
|
display: "flex",
|
||||||
|
gap: 10,
|
||||||
|
justifyContent: "center",
|
||||||
|
paddingHorizontal: 15,
|
||||||
|
paddingTop: 25
|
||||||
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
export function useModalStyles() {
|
export function useModalStyles() {
|
||||||
|
|||||||
@@ -0,0 +1,18 @@
|
|||||||
|
[package]
|
||||||
|
name = "amnet-server-test"
|
||||||
|
edition = "2024"
|
||||||
|
|
||||||
|
version.workspace = true
|
||||||
|
authors.workspace = true
|
||||||
|
description = "AMNet Server Test Application"
|
||||||
|
|
||||||
|
[[bin]]
|
||||||
|
name = "amnet-server-test"
|
||||||
|
path = "src/main.rs"
|
||||||
|
|
||||||
|
[dependencies]
|
||||||
|
amnet = { path = "../amnet-server" }
|
||||||
|
axum = "0.8.8"
|
||||||
|
tokio = { version = "1", features = ["rt-multi-thread", "macros", "net", "time"] }
|
||||||
|
tracing = "0.1"
|
||||||
|
tracing-subscriber = "0.3"
|
||||||
@@ -0,0 +1,65 @@
|
|||||||
|
use std::time::Duration;
|
||||||
|
|
||||||
|
use amnet::{build_server, CardSlot, ServerConfig, Metrics, format_hex_bytes};
|
||||||
|
use tokio::net::TcpListener;
|
||||||
|
use tracing::{info, warn};
|
||||||
|
|
||||||
|
#[tokio::main]
|
||||||
|
async fn main() {
|
||||||
|
tracing_subscriber::fmt()
|
||||||
|
.with_max_level(tracing::Level::DEBUG)
|
||||||
|
.init();
|
||||||
|
|
||||||
|
let config = ServerConfig::instance();
|
||||||
|
|
||||||
|
info!("AMNet Server (Test Application)");
|
||||||
|
|
||||||
|
info!("Configuration:");
|
||||||
|
info!(" Game ID: {}", config.game_id.as_deref().unwrap_or("(not set)"));
|
||||||
|
info!(" Server Name: {}", config.server_name);
|
||||||
|
info!(" Listen Address: {}", config.server_listen_address);
|
||||||
|
info!(" Physical Card Use IDm: {}", config.physical_card_use_idm);
|
||||||
|
info!("");
|
||||||
|
|
||||||
|
// Start the webserver in the background
|
||||||
|
let app = build_server();
|
||||||
|
let listen_addr = config.server_listen_address.as_str();
|
||||||
|
|
||||||
|
let listener = match TcpListener::bind(listen_addr).await {
|
||||||
|
Ok(l) => l,
|
||||||
|
Err(e) => {
|
||||||
|
warn!("Failed to bind to {}: {}", listen_addr, e);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
Metrics::instance().set_start_time();
|
||||||
|
|
||||||
|
info!("Server started on {}", listen_addr);
|
||||||
|
info!("Submit a card via the web API and it will be detected here.");
|
||||||
|
|
||||||
|
tokio::spawn(async move {
|
||||||
|
if let Err(e) = axum::serve(listener, app).await {
|
||||||
|
warn!("Server error: {}", e);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
loop {
|
||||||
|
if let Err(_) = Metrics::instance().set_last_poll() {
|
||||||
|
warn!("Failed to update poll metrics");
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Some(card) = CardSlot::instance().request_card(None) {
|
||||||
|
info!("=== CARD DETECTED ===");
|
||||||
|
info!(" Access Code: {}", format_hex_bytes(&card.access_code));
|
||||||
|
|
||||||
|
if let Some(idm) = card.card_idm {
|
||||||
|
info!(" Card IDm: {}", format_hex_bytes(&idm));
|
||||||
|
} else {
|
||||||
|
info!(" Card IDm: (not provided)");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
tokio::time::sleep(Duration::from_secs(1)).await;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,31 @@
|
|||||||
|
[package]
|
||||||
|
name = "amnet"
|
||||||
|
edition = "2024"
|
||||||
|
build = "build.rs"
|
||||||
|
|
||||||
|
version.workspace = true
|
||||||
|
authors.workspace = true
|
||||||
|
description.workspace = true
|
||||||
|
|
||||||
|
[lib]
|
||||||
|
name = "amnet"
|
||||||
|
crate-type = ["cdylib", "rlib"]
|
||||||
|
|
||||||
|
[profile.release]
|
||||||
|
lto = true
|
||||||
|
strip = true
|
||||||
|
|
||||||
|
[dependencies]
|
||||||
|
axum = "0.8.8"
|
||||||
|
hostname = "0.4"
|
||||||
|
tracing = "0.1"
|
||||||
|
rust-ini = "0.21"
|
||||||
|
serde = { version = "1.0", features = ["derive"] }
|
||||||
|
tokio = { version = "1", features = ["rt-multi-thread", "net"] }
|
||||||
|
tower = "0.5.2"
|
||||||
|
tower-http = { version = "0.6.8", features = ["cors", "trace"] }
|
||||||
|
windows-sys = { version = "0.61.2", features = ["Win32_System_Console", "Win32_UI_Input_KeyboardAndMouse"] }
|
||||||
|
tracing-subscriber = "0.3.22"
|
||||||
|
|
||||||
|
[build-dependencies]
|
||||||
|
winresource = "0.1"
|
||||||
@@ -0,0 +1,32 @@
|
|||||||
|
fn main() {
|
||||||
|
if std::env::var("CARGO_CFG_TARGET_OS").unwrap_or_default() == "windows" {
|
||||||
|
set_windows_resource_version();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn set_windows_resource_version() {
|
||||||
|
let version_str = env!("CARGO_PKG_VERSION");
|
||||||
|
let version_parts: Vec<&str> = version_str.split('.').collect();
|
||||||
|
let mut normalized_parts = vec!["0", "0", "0", "0"];
|
||||||
|
|
||||||
|
for (i, part) in version_parts.iter().enumerate().take(4) {
|
||||||
|
normalized_parts[i] = part;
|
||||||
|
}
|
||||||
|
|
||||||
|
let version_display = normalized_parts.join(".");
|
||||||
|
|
||||||
|
let mut res = winresource::WindowsResource::new();
|
||||||
|
|
||||||
|
res.set("FileVersion", &version_display);
|
||||||
|
res.set("ProductVersion", "2.0.0.0");
|
||||||
|
res.set("ProductName", "AMNet");
|
||||||
|
res.set("FileDescription", "AMNet Server");
|
||||||
|
res.set("CompanyName", "ppc");
|
||||||
|
res.set("InternalName", "amnet-server");
|
||||||
|
res.set("OriginalFilename", "amnet.dll");
|
||||||
|
res.set("LegalCopyright", "Copyright 2026 (c) ppc");
|
||||||
|
|
||||||
|
if let Err(e) = res.compile() {
|
||||||
|
println!("cargo:warning=Failed to set Windows resource information: {}", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,271 @@
|
|||||||
|
use std::{sync::{Mutex, OnceLock}, time::{Duration, Instant}};
|
||||||
|
|
||||||
|
use tracing::debug;
|
||||||
|
|
||||||
|
#[derive(Debug, Clone)]
|
||||||
|
pub struct StoredCard {
|
||||||
|
pub access_code: [u8; 10],
|
||||||
|
pub card_idm: Option<[u8; 8]>,
|
||||||
|
|
||||||
|
pub expiry_time: Instant
|
||||||
|
}
|
||||||
|
|
||||||
|
impl StoredCard {
|
||||||
|
pub fn is_expired(&self) -> bool {
|
||||||
|
Instant::now() > self.expiry_time
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub enum CardPresentResult {
|
||||||
|
Accepted,
|
||||||
|
SlotUnavailable(Instant /* release time */),
|
||||||
|
}
|
||||||
|
|
||||||
|
static PRESENTER: OnceLock<CardPresenter> = OnceLock::new();
|
||||||
|
|
||||||
|
pub struct CardPresenter {
|
||||||
|
current_card: Mutex<Option<StoredCard>>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Default for CardPresenter {
|
||||||
|
fn default() -> Self {
|
||||||
|
Self {
|
||||||
|
current_card: Mutex::new(None),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl CardPresenter {
|
||||||
|
pub fn instance() -> &'static CardPresenter {
|
||||||
|
PRESENTER.get_or_init(|| CardPresenter::default())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn request_card(&self, require_idm: Option<bool>) -> Option<StoredCard> {
|
||||||
|
let mut guard = self.current_card.lock().unwrap();
|
||||||
|
let card = guard.as_ref()?;
|
||||||
|
|
||||||
|
if let Some(require) = require_idm && card.card_idm.is_some() != require {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
|
||||||
|
let card = guard.take();
|
||||||
|
|
||||||
|
if let Some(ref stored_card) = card && stored_card.is_expired() {
|
||||||
|
debug!("Stored card has expired and will not be provided");
|
||||||
|
return None;
|
||||||
|
};
|
||||||
|
|
||||||
|
card
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn present_card(&self, access_code: [u8; 10], idm_hex: Option<[u8; 8]>, validity: Duration) -> CardPresentResult {
|
||||||
|
let mut guard = self.current_card.lock().unwrap();
|
||||||
|
|
||||||
|
if let Some(card) = guard.as_ref() && !card.is_expired() {
|
||||||
|
return CardPresentResult::SlotUnavailable(card.expiry_time);
|
||||||
|
}
|
||||||
|
|
||||||
|
let stored_card = StoredCard {
|
||||||
|
access_code,
|
||||||
|
card_idm: idm_hex,
|
||||||
|
expiry_time: Instant::now() + validity,
|
||||||
|
};
|
||||||
|
|
||||||
|
*guard = Some(stored_card);
|
||||||
|
|
||||||
|
CardPresentResult::Accepted
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
use crate::utils::{parse_access_code, parse_idm_hex};
|
||||||
|
|
||||||
|
mod stored_card_tests {
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn is_expired_returns_false_for_future_expiry() {
|
||||||
|
let card = StoredCard {
|
||||||
|
access_code: [0; 10],
|
||||||
|
card_idm: None,
|
||||||
|
expiry_time: Instant::now() + Duration::from_secs(60),
|
||||||
|
};
|
||||||
|
assert!(!card.is_expired());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn is_expired_returns_true_for_past_expiry() {
|
||||||
|
let card = StoredCard {
|
||||||
|
access_code: [0; 10],
|
||||||
|
card_idm: None,
|
||||||
|
expiry_time: Instant::now() - Duration::from_millis(1),
|
||||||
|
};
|
||||||
|
assert!(card.is_expired());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
mod card_presenter_tests {
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
fn create_presenter() -> CardPresenter {
|
||||||
|
CardPresenter::default()
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn request_card_returns_none_when_empty() {
|
||||||
|
let presenter = create_presenter();
|
||||||
|
assert!(presenter.request_card(None).is_none());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn present_card_accepts_valid_access_code() {
|
||||||
|
let presenter = create_presenter();
|
||||||
|
let access_code = parse_access_code(&"12345678901234567890".to_string()).unwrap();
|
||||||
|
|
||||||
|
match presenter.present_card(access_code, None, Duration::from_secs(5)) {
|
||||||
|
CardPresentResult::Accepted => {}
|
||||||
|
_ => panic!("Expected Accepted"),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn present_card_rejects_invalid_access_code() {
|
||||||
|
let access_code = parse_access_code(&"INVALID".to_string());
|
||||||
|
assert!(access_code.is_none(), "Expected parsing to fail for invalid access code");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn present_card_accepts_valid_idm() {
|
||||||
|
let presenter = create_presenter();
|
||||||
|
let access_code = parse_access_code(&"12345678901234567890".to_string()).unwrap();
|
||||||
|
let idm = parse_idm_hex(&"0102030405060708".to_string()).unwrap();
|
||||||
|
|
||||||
|
match presenter.present_card(access_code, Some(idm), Duration::from_secs(5)) {
|
||||||
|
CardPresentResult::Accepted => {}
|
||||||
|
_ => panic!("Expected Accepted"),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn present_card_rejects_invalid_idm() {
|
||||||
|
let idm = parse_idm_hex(&"INVALID_IDM".to_string());
|
||||||
|
assert!(idm.is_none(), "Expected parsing to fail for invalid IDm");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn present_card_returns_slot_unavailable_when_card_present() {
|
||||||
|
let presenter = create_presenter();
|
||||||
|
let access_code = parse_access_code(&"12345678901234567890".to_string()).unwrap();
|
||||||
|
|
||||||
|
// Present first card
|
||||||
|
presenter.present_card(access_code, None, Duration::from_secs(5));
|
||||||
|
|
||||||
|
// Try to present second card
|
||||||
|
match presenter.present_card(access_code, None, Duration::from_secs(5)) {
|
||||||
|
CardPresentResult::SlotUnavailable(_) => {}
|
||||||
|
_ => panic!("Expected SlotUnavailable"),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn request_card_returns_card_after_present() {
|
||||||
|
let presenter = create_presenter();
|
||||||
|
let access_code = parse_access_code(&"00101234567890123456".to_string()).unwrap();
|
||||||
|
|
||||||
|
presenter.present_card(access_code, None, Duration::from_secs(5));
|
||||||
|
|
||||||
|
let card = presenter.request_card(None);
|
||||||
|
assert!(card.is_some());
|
||||||
|
|
||||||
|
let card = card.unwrap();
|
||||||
|
assert_eq!(card.access_code, [0x00, 0x10, 0x12, 0x34, 0x56, 0x78, 0x90, 0x12, 0x34, 0x56]);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn request_card_consumes_the_card() {
|
||||||
|
let presenter = create_presenter();
|
||||||
|
let access_code = parse_access_code(&"12345678901234567890".to_string()).unwrap();
|
||||||
|
|
||||||
|
presenter.present_card(access_code, None, Duration::from_secs(5));
|
||||||
|
|
||||||
|
// First request should succeed
|
||||||
|
assert!(presenter.request_card(None).is_some());
|
||||||
|
|
||||||
|
// Second request should return None
|
||||||
|
assert!(presenter.request_card(None).is_none());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn request_card_filters_by_idm_requirement_true() {
|
||||||
|
let presenter = create_presenter();
|
||||||
|
let access_code = parse_access_code(&"12345678901234567890".to_string()).unwrap();
|
||||||
|
|
||||||
|
// Present card without IDm
|
||||||
|
presenter.present_card(access_code, None, Duration::from_secs(5));
|
||||||
|
|
||||||
|
// Request card that requires IDm should return None
|
||||||
|
assert!(presenter.request_card(Some(true)).is_none());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn request_card_filters_by_idm_requirement_false() {
|
||||||
|
let presenter = create_presenter();
|
||||||
|
let access_code = parse_access_code(&"12345678901234567890".to_string()).unwrap();
|
||||||
|
let idm = parse_idm_hex(&"0102030405060708".to_string()).unwrap();
|
||||||
|
|
||||||
|
// Present card with IDm
|
||||||
|
presenter.present_card(access_code, Some(idm), Duration::from_secs(5));
|
||||||
|
|
||||||
|
// Request card that requires no IDm should return None
|
||||||
|
assert!(presenter.request_card(Some(false)).is_none());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn request_card_returns_card_with_idm_when_required() {
|
||||||
|
let presenter = create_presenter();
|
||||||
|
let access_code = parse_access_code(&"12345678901234567890".to_string()).unwrap();
|
||||||
|
let idm = parse_idm_hex(&"0102030405060708".to_string()).unwrap();
|
||||||
|
|
||||||
|
presenter.present_card(access_code, Some(idm), Duration::from_secs(5));
|
||||||
|
|
||||||
|
let card = presenter.request_card(Some(true));
|
||||||
|
assert!(card.is_some());
|
||||||
|
assert!(card.unwrap().card_idm.is_some());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn expired_card_cannot_be_requested() {
|
||||||
|
let presenter = create_presenter();
|
||||||
|
let access_code = parse_access_code(&"12345678901234567890".to_string()).unwrap();
|
||||||
|
|
||||||
|
// Present card with very short validity
|
||||||
|
presenter.present_card(access_code, None, Duration::from_millis(1));
|
||||||
|
|
||||||
|
// Wait for expiry
|
||||||
|
std::thread::sleep(Duration::from_millis(10));
|
||||||
|
|
||||||
|
// Card should not be available
|
||||||
|
assert!(presenter.request_card(None).is_none());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn can_present_card_after_previous_expires() {
|
||||||
|
let presenter = create_presenter();
|
||||||
|
let access_code = parse_access_code(&"12345678901234567890".to_string()).unwrap();
|
||||||
|
|
||||||
|
// Present first card with very short validity
|
||||||
|
presenter.present_card(access_code, None, Duration::from_millis(1));
|
||||||
|
|
||||||
|
// Wait for expiry
|
||||||
|
std::thread::sleep(Duration::from_millis(10));
|
||||||
|
|
||||||
|
// Should be able to present another card
|
||||||
|
match presenter.present_card(access_code, None, Duration::from_secs(5)) {
|
||||||
|
CardPresentResult::Accepted => {}
|
||||||
|
_ => panic!("Expected Accepted after previous card expired"),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,138 @@
|
|||||||
|
use std::env;
|
||||||
|
use std::path::PathBuf;
|
||||||
|
use std::sync::OnceLock;
|
||||||
|
|
||||||
|
use ini::Ini;
|
||||||
|
use tracing::{error, warn};
|
||||||
|
|
||||||
|
mod defaults {
|
||||||
|
pub const CONFIG_FILE: &str = "./segatools.ini";
|
||||||
|
pub const CONFIG_FILE_ENV: &str = "SEGATOOLS_CONFIG_PATH";
|
||||||
|
|
||||||
|
pub const SERVER_NAME: &str = "AMNET-SERVER";
|
||||||
|
pub const SERVER_ADDRESS: &str = "0.0.0.0:6070";
|
||||||
|
|
||||||
|
pub const AIME_FILE: &str = r"DEVICE\aime.txt";
|
||||||
|
pub const AIME_KEYBOARD_ENABLED: bool = true;
|
||||||
|
pub const INCLUDE_IDM_FOR_PHYSICAL_CARDS: bool = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
mod limits {
|
||||||
|
pub const GAME_ID_LEN: usize = 4;
|
||||||
|
pub const SERVER_NAME_MAX_LEN: usize = 16;
|
||||||
|
}
|
||||||
|
|
||||||
|
static CONFIG: OnceLock<Config> = OnceLock::new();
|
||||||
|
|
||||||
|
pub struct Config {
|
||||||
|
/// Game ID (4 characters or none)
|
||||||
|
pub game_id: Option<String>,
|
||||||
|
|
||||||
|
/// Server name shown on devices (max 16 chars). Defaults to the machine hostname.
|
||||||
|
pub server_name: String,
|
||||||
|
|
||||||
|
/// Whether physically scanned cards should have their IDm forwarded to the game
|
||||||
|
/// instead of the access code.
|
||||||
|
pub physical_card_use_idm: bool,
|
||||||
|
|
||||||
|
/// Whether holding the enter key should trigger a card scan using the code
|
||||||
|
/// from the aime.txt file
|
||||||
|
pub enable_aime_txt: bool,
|
||||||
|
|
||||||
|
/// Path to the aime.txt file
|
||||||
|
pub aime_txt_path: PathBuf,
|
||||||
|
|
||||||
|
/// Server address that should be listened on
|
||||||
|
pub server_listen_address: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Config {
|
||||||
|
pub fn instance() -> &'static Config {
|
||||||
|
CONFIG.get_or_init(|| {
|
||||||
|
Config::load().unwrap_or_else(|e| {
|
||||||
|
warn!("Failed to load config: {}. Using defaults.", e);
|
||||||
|
Config::default()
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
fn load() -> Result<Self, Box<dyn std::error::Error>> {
|
||||||
|
let config_path = env::var(defaults::CONFIG_FILE_ENV).unwrap_or_else(|_| defaults::CONFIG_FILE.to_string());
|
||||||
|
let ini = Ini::load_from_file(&config_path)?;
|
||||||
|
|
||||||
|
let io_section = ini.section(Some("aimeio")).ok_or("Missing [aimeio] section")?;
|
||||||
|
let aime_section = ini.section(Some("aime"));
|
||||||
|
|
||||||
|
// gameId must be exactly 4 ascii letters or empty
|
||||||
|
let game_id = io_section.get("gameId").unwrap_or("");
|
||||||
|
let game_id = if game_id.is_empty() {
|
||||||
|
None
|
||||||
|
} else if game_id.chars().count() == limits::GAME_ID_LEN && game_id.chars().all(|c| c.is_ascii_alphabetic()) {
|
||||||
|
Some(game_id.to_ascii_uppercase().to_string())
|
||||||
|
} else {
|
||||||
|
error!("Invalid gameId '{game_id}': must be exactly {} characters or empty", limits::GAME_ID_LEN);
|
||||||
|
None
|
||||||
|
};
|
||||||
|
|
||||||
|
let default_hostname = hostname::get()
|
||||||
|
.ok()
|
||||||
|
.and_then(|h| h.into_string().ok())
|
||||||
|
.unwrap_or_else(|| defaults::SERVER_NAME.to_string());
|
||||||
|
|
||||||
|
let server_name = io_section.get("serverName")
|
||||||
|
.unwrap_or(&default_hostname)
|
||||||
|
.chars()
|
||||||
|
.take(limits::SERVER_NAME_MAX_LEN)
|
||||||
|
.collect();
|
||||||
|
|
||||||
|
// Read boolean flags
|
||||||
|
let physical_card_use_idm = io_section.get("useAimeDBForPhysicalCards")
|
||||||
|
.and_then(|v| v.parse::<i32>().ok())
|
||||||
|
.map(|v| v > 0)
|
||||||
|
.unwrap_or(defaults::INCLUDE_IDM_FOR_PHYSICAL_CARDS);
|
||||||
|
|
||||||
|
let enable_aime_txt = io_section.get("enableKeyboardMode")
|
||||||
|
.and_then(|v| v.parse::<i32>().ok())
|
||||||
|
.map(|v| v > 0)
|
||||||
|
.unwrap_or(defaults::AIME_KEYBOARD_ENABLED);
|
||||||
|
|
||||||
|
let aime_txt_path_raw = aime_section
|
||||||
|
.and_then(|s| s.get("aimePath"))
|
||||||
|
.unwrap_or(defaults::AIME_FILE);
|
||||||
|
|
||||||
|
// Normalize backslashes to forward slashes to avoid escape issues
|
||||||
|
let aime_txt_path_str = aime_txt_path_raw.replace("\\", "/");
|
||||||
|
let aime_txt_path = PathBuf::from(aime_txt_path_str);
|
||||||
|
|
||||||
|
let server_listen_address = io_section.get("listenAddress")
|
||||||
|
.unwrap_or(defaults::SERVER_ADDRESS)
|
||||||
|
.to_string();
|
||||||
|
|
||||||
|
Ok(Config {
|
||||||
|
game_id,
|
||||||
|
server_name,
|
||||||
|
physical_card_use_idm,
|
||||||
|
enable_aime_txt,
|
||||||
|
aime_txt_path,
|
||||||
|
server_listen_address,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Default for Config {
|
||||||
|
fn default() -> Self {
|
||||||
|
let default_hostname = hostname::get()
|
||||||
|
.ok()
|
||||||
|
.and_then(|h| h.into_string().ok())
|
||||||
|
.unwrap_or_else(|| defaults::SERVER_NAME.to_string());
|
||||||
|
|
||||||
|
Self {
|
||||||
|
game_id: None,
|
||||||
|
server_name: default_hostname.chars().take(limits::SERVER_NAME_MAX_LEN).collect(),
|
||||||
|
physical_card_use_idm: defaults::INCLUDE_IDM_FOR_PHYSICAL_CARDS,
|
||||||
|
enable_aime_txt: defaults::AIME_KEYBOARD_ENABLED,
|
||||||
|
aime_txt_path: PathBuf::from(defaults::AIME_FILE),
|
||||||
|
server_listen_address: defaults::SERVER_ADDRESS.to_string(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,197 @@
|
|||||||
|
use std::sync::OnceLock;
|
||||||
|
|
||||||
|
use tokio::net::TcpListener;
|
||||||
|
use tokio::runtime::Runtime;
|
||||||
|
use tracing::{info, warn};
|
||||||
|
|
||||||
|
mod config;
|
||||||
|
mod card;
|
||||||
|
mod webapi;
|
||||||
|
mod metrics;
|
||||||
|
mod utils;
|
||||||
|
mod webserver;
|
||||||
|
|
||||||
|
use card::CardPresenter;
|
||||||
|
use config::Config;
|
||||||
|
use metrics::SystemMetrics;
|
||||||
|
|
||||||
|
// Re-export public API for library users
|
||||||
|
pub use config::Config as ServerConfig;
|
||||||
|
pub use card::{CardPresenter as CardSlot, StoredCard, CardPresentResult};
|
||||||
|
pub use metrics::SystemMetrics as Metrics;
|
||||||
|
pub use webserver::build_server;
|
||||||
|
pub use utils::{format_hex_bytes, parse_access_code, parse_idm_hex};
|
||||||
|
|
||||||
|
static WEBSERVER_INIT: OnceLock<()> = OnceLock::<()>::new();
|
||||||
|
|
||||||
|
#[unsafe(no_mangle)]
|
||||||
|
pub extern "C" fn aime_io_get_api_version() -> u16 {
|
||||||
|
0x0100
|
||||||
|
}
|
||||||
|
|
||||||
|
#[unsafe(no_mangle)]
|
||||||
|
pub extern "C" fn aime_io_init() -> i32 {
|
||||||
|
WEBSERVER_INIT.get_or_init(|| {
|
||||||
|
std::thread::spawn(|| {
|
||||||
|
let rt = Runtime::new().expect("Failed to create tokio runtime");
|
||||||
|
rt.block_on(start_server_async());
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
#[unsafe(no_mangle)]
|
||||||
|
pub extern "C" fn aime_io_nfc_poll(unit_no: u8) -> i32 {
|
||||||
|
if unit_no != 0 {
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Err(_) = SystemMetrics::instance().set_last_poll() {
|
||||||
|
warn!("Failed to update last poll time");
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(target_os = "windows")]
|
||||||
|
{
|
||||||
|
use windows_sys::Win32::UI::Input::KeyboardAndMouse::GetAsyncKeyState;
|
||||||
|
|
||||||
|
let config = Config::instance();
|
||||||
|
if !config.enable_aime_txt {
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
let enter_pressed = unsafe { (GetAsyncKeyState(0x0D) & (0x8000u16 as i16)) != 0 };
|
||||||
|
if !enter_pressed {
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
let aime_code = match std::fs::read_to_string(&config.aime_txt_path) {
|
||||||
|
Ok(content) => content.trim().to_string(),
|
||||||
|
Err(e) => {
|
||||||
|
warn!("Failed to read text file at {}: {}", config.aime_txt_path.display(), e);
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
if aime_code.is_empty() {
|
||||||
|
warn!("Access code file is empty");
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
let Some(access_code) = utils::parse_access_code(&aime_code) else {
|
||||||
|
warn!("Invalid access code read from file");
|
||||||
|
return 1;
|
||||||
|
};
|
||||||
|
|
||||||
|
match CardPresenter::instance().present_card(access_code, None, std::time::Duration::from_secs(5)) {
|
||||||
|
card::CardPresentResult::Accepted => {
|
||||||
|
return 0;
|
||||||
|
},
|
||||||
|
card::CardPresentResult::SlotUnavailable(release_time) => {
|
||||||
|
warn!("Access code was not accepted as there is already a card in the active slot until {:?}", release_time);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
#[unsafe(no_mangle)]
|
||||||
|
pub extern "C" fn aime_io_nfc_get_aime_id(unit_no: u8, luid: *mut u8, luid_size: usize) -> i32 {
|
||||||
|
if unit_no != 0 {
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
let require_idm = if Config::instance().physical_card_use_idm {
|
||||||
|
Some(false)
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
};
|
||||||
|
|
||||||
|
let Some(card) = CardPresenter::instance().request_card(require_idm) else {
|
||||||
|
return 1;
|
||||||
|
};
|
||||||
|
|
||||||
|
unsafe {
|
||||||
|
let copy_size = std::cmp::min(card.access_code.len(), luid_size);
|
||||||
|
std::ptr::copy_nonoverlapping(card.access_code.as_ptr(), luid, copy_size);
|
||||||
|
}
|
||||||
|
|
||||||
|
info!("Provided access code: {}", utils::format_hex_bytes(&card.access_code));
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
#[unsafe(no_mangle)]
|
||||||
|
pub extern "C" fn aime_io_nfc_get_felica_id(unit_no: u8, idm: *mut u64) -> i32 {
|
||||||
|
if unit_no != 0 || Config::instance().physical_card_use_idm == false {
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
let Some(card) = CardPresenter::instance().request_card(Some(true)) else {
|
||||||
|
return 1;
|
||||||
|
};
|
||||||
|
|
||||||
|
let Some(card_idm) = card.card_idm else {
|
||||||
|
return 1;
|
||||||
|
};
|
||||||
|
|
||||||
|
let mut idm_value: u64 = 0;
|
||||||
|
for byte in card_idm {
|
||||||
|
idm_value = (idm_value << 8) | (byte as u64);
|
||||||
|
}
|
||||||
|
|
||||||
|
unsafe {
|
||||||
|
*idm = idm_value;
|
||||||
|
}
|
||||||
|
|
||||||
|
info!("Provided IDm: {}", utils::format_hex_bytes(&card_idm));
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
#[unsafe(no_mangle)]
|
||||||
|
pub extern "C" fn aime_io_led_set_color(_unit_no: u8, _r: u8, _g: u8, _b: u8) {
|
||||||
|
// do nothing
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
#[unsafe(no_mangle)]
|
||||||
|
#[allow(non_snake_case)]
|
||||||
|
#[cfg(target_os = "windows")]
|
||||||
|
pub extern "system" fn DllMain(_hinst_dll: *mut std::ffi::c_void, fdw_reason: u32, _lpv_reserved: *mut std::ffi::c_void) -> i32 {
|
||||||
|
if fdw_reason != 1 /* DLL_PROCESS_ATTACH */ {
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
unsafe {
|
||||||
|
windows_sys::Win32::System::Console::AllocConsole();
|
||||||
|
}
|
||||||
|
|
||||||
|
tracing_subscriber::fmt()
|
||||||
|
.with_max_level(tracing::Level::DEBUG)
|
||||||
|
.with_ansi(false) // some targets that are injected into don't play nicely with ansi codes
|
||||||
|
.init();
|
||||||
|
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn start_server_async() {
|
||||||
|
let app = webserver::build_server();
|
||||||
|
let config = Config::instance();
|
||||||
|
|
||||||
|
let listen_addr = config.server_listen_address.as_str();
|
||||||
|
let listener = TcpListener::bind(listen_addr)
|
||||||
|
.await
|
||||||
|
.unwrap_or_else(|e| panic!("Failed to bind to {}: {}", listen_addr, e));
|
||||||
|
|
||||||
|
SystemMetrics::instance().set_start_time();
|
||||||
|
|
||||||
|
let game_id = config.game_id.clone().unwrap_or_else(|| "SXXX".to_string());
|
||||||
|
|
||||||
|
info!("AMNet Server - Version {} ({}) loaded", env!("CARGO_PKG_VERSION"), game_id);
|
||||||
|
info!("Listening on {} - visit http://card.ppc.moe from a mobile device or use the AMNet App to get started.", listen_addr);
|
||||||
|
|
||||||
|
axum::serve(listener, app)
|
||||||
|
.await
|
||||||
|
.expect("Failed to start server");
|
||||||
|
}
|
||||||
@@ -0,0 +1,59 @@
|
|||||||
|
use std::{sync::{Mutex, OnceLock}, time::{Duration, Instant}};
|
||||||
|
|
||||||
|
static METRICS: OnceLock<SystemMetrics> = OnceLock::new();
|
||||||
|
|
||||||
|
#[derive(Debug)]
|
||||||
|
pub struct SystemMetrics {
|
||||||
|
start_time: OnceLock<Instant>,
|
||||||
|
last_poll_time: Mutex<Option<Instant>>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Default for SystemMetrics {
|
||||||
|
fn default() -> Self {
|
||||||
|
Self {
|
||||||
|
start_time: OnceLock::new(),
|
||||||
|
last_poll_time: Mutex::new(None),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl SystemMetrics {
|
||||||
|
pub fn instance() -> &'static SystemMetrics {
|
||||||
|
METRICS.get_or_init(|| SystemMetrics::default())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// get the start_time of the server.
|
||||||
|
pub fn get_server_uptime(&self) -> Option<Duration> {
|
||||||
|
let Some(time) = self.start_time.get() else {
|
||||||
|
return None;
|
||||||
|
};
|
||||||
|
|
||||||
|
Some(time.elapsed())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// sets the start time of the server.
|
||||||
|
/// this does nothing if the time was already set.
|
||||||
|
pub fn set_start_time(&self) {
|
||||||
|
self.start_time.get_or_init(|| Instant::now());
|
||||||
|
}
|
||||||
|
|
||||||
|
/// get the last time the card was polled by the game.
|
||||||
|
/// if the data is not available, an empty err will be returned.
|
||||||
|
pub fn get_last_poll(&self) -> Option<Duration> {
|
||||||
|
let Ok(time_guard) = self.last_poll_time.lock() else {
|
||||||
|
return None;
|
||||||
|
};
|
||||||
|
|
||||||
|
Some(time_guard.as_ref()?.elapsed())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Set the last poll time to now, returning the previously stored value
|
||||||
|
pub fn set_last_poll(&self) -> Result<(), ()> {
|
||||||
|
let Ok(mut time_guard) = self.last_poll_time.lock() else {
|
||||||
|
return Err(());
|
||||||
|
};
|
||||||
|
|
||||||
|
time_guard.replace(Instant::now());
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,157 @@
|
|||||||
|
pub fn format_hex_bytes(bytes: &[u8]) -> String {
|
||||||
|
bytes.chunks(2)
|
||||||
|
.map(|chunk| chunk.iter().map(|b| format!("{:02X}", b)).collect::<String>())
|
||||||
|
.collect::<Vec<_>>()
|
||||||
|
.join(" ")
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn parse_access_code(code: &String) -> Option<[u8; 10]> {
|
||||||
|
let matrix_code = code.replace(" ", "");
|
||||||
|
|
||||||
|
if code.chars().any(|c| !c.is_ascii_hexdigit() && !c.is_whitespace()) && matrix_code.len() != 20 {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut bytes: [u8; 10] = [0; 10];
|
||||||
|
|
||||||
|
for i in 0..bytes.len() {
|
||||||
|
let slice_str = &matrix_code[i*2..i*2+2];
|
||||||
|
let slice_val = u8::from_str_radix(slice_str, 16).ok()?;
|
||||||
|
|
||||||
|
bytes[i] = slice_val;
|
||||||
|
}
|
||||||
|
|
||||||
|
Some(bytes)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn parse_idm_hex(idm_hex: &String) -> Option<[u8; 8]> {
|
||||||
|
if idm_hex.len() != 16 || idm_hex.chars().any(|c| !c.is_ascii_hexdigit()) {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut idm_bytes: [u8; 8] = [0; 8];
|
||||||
|
|
||||||
|
for i in 0..idm_bytes.len() {
|
||||||
|
let slice_str = &idm_hex[i*2..i*2+2];
|
||||||
|
let slice_val = u8::from_str_radix(slice_str, 16).ok()?;
|
||||||
|
|
||||||
|
idm_bytes[i] = slice_val;
|
||||||
|
}
|
||||||
|
|
||||||
|
Some(idm_bytes)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
mod format_hex_bytes_tests {
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn formats_empty_bytes() {
|
||||||
|
assert_eq!(format_hex_bytes(&[]), "");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn formats_access_code_bytes() {
|
||||||
|
let bytes = [0x00, 0x10, 0x12, 0x34, 0x56, 0x78, 0x90, 0x12, 0x34, 0x56];
|
||||||
|
assert_eq!(format_hex_bytes(&bytes), "0010 1234 5678 9012 3456");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
mod parse_access_code_tests {
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn parses_full_20_digit_code() {
|
||||||
|
let code = "00101234567890123456".to_string();
|
||||||
|
let result = parse_access_code(&code);
|
||||||
|
|
||||||
|
assert!(result.is_some());
|
||||||
|
assert_eq!(result.unwrap(), [0x00, 0x10, 0x12, 0x34, 0x56, 0x78, 0x90, 0x12, 0x34, 0x56]);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn parses_code_with_spaces() {
|
||||||
|
let code = "0010 1234 5678 9012 3456".to_string();
|
||||||
|
let result = parse_access_code(&code);
|
||||||
|
|
||||||
|
assert!(result.is_some());
|
||||||
|
assert_eq!(result.unwrap(), [0x00, 0x10, 0x12, 0x34, 0x56, 0x78, 0x90, 0x12, 0x34, 0x56]);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn parses_code_with_leading_zeros() {
|
||||||
|
let code = "00000000000000001234".to_string(); // Valid 20-digit code with leading zeros
|
||||||
|
let result = parse_access_code(&code);
|
||||||
|
|
||||||
|
assert!(result.is_some());
|
||||||
|
assert_eq!(result.unwrap(), [0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x12, 0x34]);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn returns_none_for_invalid_hex() {
|
||||||
|
let code = "GHIJKLMNOPQRSTUVWXYZ".to_string();
|
||||||
|
let result = parse_access_code(&code);
|
||||||
|
|
||||||
|
assert!(result.is_none());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn parses_code_with_all_zeros() {
|
||||||
|
let code = "00000000000000000000".to_string();
|
||||||
|
let result = parse_access_code(&code);
|
||||||
|
|
||||||
|
assert!(result.is_some());
|
||||||
|
assert_eq!(result.unwrap(), [0x00; 10]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
mod parse_idm_hex_tests {
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn parses_full_16_char_idm() {
|
||||||
|
let idm = "0102030405060708".to_string();
|
||||||
|
let result = parse_idm_hex(&idm);
|
||||||
|
|
||||||
|
assert!(result.is_some());
|
||||||
|
assert_eq!(result.unwrap(), [0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08]);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn parses_short_idm_returns_none() {
|
||||||
|
let idm = "1234".to_string();
|
||||||
|
let result = parse_idm_hex(&idm);
|
||||||
|
|
||||||
|
// IDm is too short - should fail
|
||||||
|
assert!(result.is_none());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn returns_none_for_too_long_idm() {
|
||||||
|
let idm = "01020304050607080910".to_string(); // 20 chars
|
||||||
|
let result = parse_idm_hex(&idm);
|
||||||
|
|
||||||
|
assert!(result.is_none());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn returns_none_for_non_hex_characters() {
|
||||||
|
let idm = "0102GHIJ".to_string();
|
||||||
|
let result = parse_idm_hex(&idm);
|
||||||
|
|
||||||
|
assert!(result.is_none());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn parses_empty_idm_returns_none() {
|
||||||
|
let idm = "".to_string();
|
||||||
|
let result = parse_idm_hex(&idm);
|
||||||
|
|
||||||
|
// Empty IDm should fail
|
||||||
|
assert!(result.is_none());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,35 @@
|
|||||||
|
use serde::{Deserialize, Serialize};
|
||||||
|
|
||||||
|
pub const API_VERSION: u8 = 1;
|
||||||
|
|
||||||
|
#[derive(Serialize)]
|
||||||
|
pub struct ServerError {
|
||||||
|
pub error: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Serialize)]
|
||||||
|
pub struct ServerState {
|
||||||
|
#[serde(rename = "apiVersion")]
|
||||||
|
pub api_version: u8,
|
||||||
|
|
||||||
|
#[serde(rename = "gameId")]
|
||||||
|
pub game_id: Option<String>,
|
||||||
|
|
||||||
|
#[serde(rename = "serverName")]
|
||||||
|
pub server_name: String,
|
||||||
|
|
||||||
|
#[serde(rename = "sessionUptime")]
|
||||||
|
pub session_uptime: u64,
|
||||||
|
|
||||||
|
#[serde(rename = "timeSinceLastPoll")]
|
||||||
|
pub time_since_last_poll: Option<u64>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Deserialize)]
|
||||||
|
pub struct CardSubmissionRequest {
|
||||||
|
#[serde(rename = "cardId")]
|
||||||
|
pub access_code: String,
|
||||||
|
|
||||||
|
#[serde(rename = "physicalCardIDm")]
|
||||||
|
pub card_idm_hex: Option<String>,
|
||||||
|
}
|
||||||
@@ -0,0 +1,99 @@
|
|||||||
|
use std::time::{Duration, Instant};
|
||||||
|
|
||||||
|
use axum::http::{Method, StatusCode, header};
|
||||||
|
use axum::response::{IntoResponse, Response};
|
||||||
|
use axum::routing::{get, post};
|
||||||
|
use axum::{Json, Router};
|
||||||
|
use tower::ServiceBuilder;
|
||||||
|
use tower_http::{cors::{Any, CorsLayer}, trace::TraceLayer};
|
||||||
|
|
||||||
|
use crate::card::{CardPresentResult, CardPresenter};
|
||||||
|
use crate::config::Config;
|
||||||
|
use crate::metrics::SystemMetrics;
|
||||||
|
use crate::utils;
|
||||||
|
use crate::webapi::API_VERSION;
|
||||||
|
|
||||||
|
pub fn build_server() -> Router {
|
||||||
|
let services = ServiceBuilder::new()
|
||||||
|
.layer(TraceLayer::new_for_http())
|
||||||
|
.layer(CorsLayer::new().allow_origin(Any).allow_headers(Any).allow_methods([Method::GET, Method::POST]).max_age(Duration::from_secs(600)));
|
||||||
|
|
||||||
|
Router::<()>::new()
|
||||||
|
.route("/amnet/info", get(server_state_handler))
|
||||||
|
.route("/amnet/signin", post(card_submission_handler))
|
||||||
|
.layer(services)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn build_error_response(status: StatusCode, message: &str) -> Response {
|
||||||
|
let error = crate::webapi::ServerError {
|
||||||
|
error: message.to_string(),
|
||||||
|
};
|
||||||
|
|
||||||
|
(status, Json(error)).into_response()
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn server_state_handler() -> Json<crate::webapi::ServerState> {
|
||||||
|
let config = Config::instance();
|
||||||
|
let metrics = SystemMetrics::instance();
|
||||||
|
|
||||||
|
let session_uptime = metrics.get_server_uptime().map(|d| d.as_secs()).unwrap_or(0);
|
||||||
|
let time_since_last_poll = metrics.get_last_poll().map(|d| d.as_secs());
|
||||||
|
|
||||||
|
let state = crate::webapi::ServerState {
|
||||||
|
api_version: API_VERSION,
|
||||||
|
game_id: config.game_id.clone(),
|
||||||
|
server_name: config.server_name.clone(),
|
||||||
|
session_uptime,
|
||||||
|
time_since_last_poll,
|
||||||
|
};
|
||||||
|
|
||||||
|
Json(state)
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn card_submission_handler(
|
||||||
|
Json(payload): Json<crate::webapi::CardSubmissionRequest>,
|
||||||
|
) -> impl IntoResponse {
|
||||||
|
if payload.access_code.len() > 20 || payload.access_code.len() == 0 {
|
||||||
|
return build_error_response(StatusCode::BAD_REQUEST, "Access code is required");
|
||||||
|
}
|
||||||
|
|
||||||
|
let Some(access_code) = utils::parse_access_code(&payload.access_code) else {
|
||||||
|
return build_error_response(StatusCode::UNPROCESSABLE_ENTITY, "Invalid access code provided");
|
||||||
|
};
|
||||||
|
|
||||||
|
let card_idm_bytes = if let Some(idm_hex) = payload.card_idm_hex.as_ref() {
|
||||||
|
if idm_hex.len() != 16 {
|
||||||
|
return build_error_response(StatusCode::BAD_REQUEST, "IDm must be a 16-char hex string");
|
||||||
|
}
|
||||||
|
|
||||||
|
let Some(idm_bytes) = utils::parse_idm_hex(idm_hex) else {
|
||||||
|
return build_error_response(StatusCode::UNPROCESSABLE_ENTITY, "Invalid IDm provided");
|
||||||
|
};
|
||||||
|
|
||||||
|
Some(idm_bytes)
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
};
|
||||||
|
|
||||||
|
match CardPresenter::instance().present_card(access_code, card_idm_bytes, Duration::from_secs(5)) {
|
||||||
|
CardPresentResult::Accepted => StatusCode::ACCEPTED.into_response(),
|
||||||
|
CardPresentResult::SlotUnavailable(release_time) => {
|
||||||
|
let now = Instant::now();
|
||||||
|
let wait_secs = if release_time > now {
|
||||||
|
(release_time - now).as_secs()
|
||||||
|
} else {
|
||||||
|
0
|
||||||
|
};
|
||||||
|
|
||||||
|
let mut response = build_error_response(
|
||||||
|
StatusCode::TOO_MANY_REQUESTS,
|
||||||
|
"A card has already been submitted. Please wait before submitting another.",
|
||||||
|
);
|
||||||
|
|
||||||
|
let headers = response.headers_mut();
|
||||||
|
headers.insert(header::RETRY_AFTER, wait_secs.to_string().parse().unwrap());
|
||||||
|
|
||||||
|
response
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
Vendored
+10
-1
@@ -8,6 +8,7 @@ declare const AMNET_API_VERSION = 1;
|
|||||||
declare const MIN_AMNET_API_VERSION = 1;
|
declare const MIN_AMNET_API_VERSION = 1;
|
||||||
interface AMCardInRequest {
|
interface AMCardInRequest {
|
||||||
cardId: string;
|
cardId: string;
|
||||||
|
physicalCardIDm?: string;
|
||||||
}
|
}
|
||||||
/**
|
/**
|
||||||
* Server connection info (api version, address, etc.)
|
* Server connection info (api version, address, etc.)
|
||||||
@@ -49,6 +50,7 @@ declare function performCardIn(server: AMServerConnectionInfo, request: AMCardIn
|
|||||||
declare function fetchServerInfo(serverAddress: string, abort: AbortSignal): Promise<AMServerInfo | null>;
|
declare function fetchServerInfo(serverAddress: string, abort: AbortSignal): Promise<AMServerInfo | null>;
|
||||||
|
|
||||||
declare const CARD_ID_REGEX: RegExp;
|
declare const CARD_ID_REGEX: RegExp;
|
||||||
|
declare const AMNET_COMPANY_ID = 597;
|
||||||
declare function generateCardNumber(): string;
|
declare function generateCardNumber(): string;
|
||||||
/**
|
/**
|
||||||
* Validates the provided card id by known parameters
|
* Validates the provided card id by known parameters
|
||||||
@@ -62,4 +64,11 @@ declare function validateCardId(id: string): boolean;
|
|||||||
*/
|
*/
|
||||||
declare function getGameName(id: string): string | null;
|
declare function getGameName(id: string): string | null;
|
||||||
|
|
||||||
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 };
|
type DecodedAccessCode = {
|
||||||
|
revision: number;
|
||||||
|
serial: number;
|
||||||
|
};
|
||||||
|
declare function decodeAccessCode(code: string): DecodedAccessCode | null;
|
||||||
|
declare function encodeAccessCode(company: number, serial: number, rev?: number): string;
|
||||||
|
|
||||||
|
export { type AMCardInRequest, AMNET_API_VERSION, AMNET_COMPANY_ID, type AMServerConnectionInfo, type AMServerInfo, type AMServerInstanceMetrics, type AMServerMetadata, CARD_ID_REGEX, decodeAccessCode as DecodeAMAccessCode, encodeAccessCode as EncodeAMAccessCode, MIN_AMNET_API_VERSION, fetchServerInfo, generateCardNumber, getGameName, performCardIn, validateCardId };
|
||||||
|
|||||||
Vendored
+10
-1
@@ -8,6 +8,7 @@ declare const AMNET_API_VERSION = 1;
|
|||||||
declare const MIN_AMNET_API_VERSION = 1;
|
declare const MIN_AMNET_API_VERSION = 1;
|
||||||
interface AMCardInRequest {
|
interface AMCardInRequest {
|
||||||
cardId: string;
|
cardId: string;
|
||||||
|
physicalCardIDm?: string;
|
||||||
}
|
}
|
||||||
/**
|
/**
|
||||||
* Server connection info (api version, address, etc.)
|
* Server connection info (api version, address, etc.)
|
||||||
@@ -49,6 +50,7 @@ declare function performCardIn(server: AMServerConnectionInfo, request: AMCardIn
|
|||||||
declare function fetchServerInfo(serverAddress: string, abort: AbortSignal): Promise<AMServerInfo | null>;
|
declare function fetchServerInfo(serverAddress: string, abort: AbortSignal): Promise<AMServerInfo | null>;
|
||||||
|
|
||||||
declare const CARD_ID_REGEX: RegExp;
|
declare const CARD_ID_REGEX: RegExp;
|
||||||
|
declare const AMNET_COMPANY_ID = 597;
|
||||||
declare function generateCardNumber(): string;
|
declare function generateCardNumber(): string;
|
||||||
/**
|
/**
|
||||||
* Validates the provided card id by known parameters
|
* Validates the provided card id by known parameters
|
||||||
@@ -62,4 +64,11 @@ declare function validateCardId(id: string): boolean;
|
|||||||
*/
|
*/
|
||||||
declare function getGameName(id: string): string | null;
|
declare function getGameName(id: string): string | null;
|
||||||
|
|
||||||
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 };
|
type DecodedAccessCode = {
|
||||||
|
revision: number;
|
||||||
|
serial: number;
|
||||||
|
};
|
||||||
|
declare function decodeAccessCode(code: string): DecodedAccessCode | null;
|
||||||
|
declare function encodeAccessCode(company: number, serial: number, rev?: number): string;
|
||||||
|
|
||||||
|
export { type AMCardInRequest, AMNET_API_VERSION, AMNET_COMPANY_ID, type AMServerConnectionInfo, type AMServerInfo, type AMServerInstanceMetrics, type AMServerMetadata, CARD_ID_REGEX, decodeAccessCode as DecodeAMAccessCode, encodeAccessCode as EncodeAMAccessCode, MIN_AMNET_API_VERSION, fetchServerInfo, generateCardNumber, getGameName, performCardIn, validateCardId };
|
||||||
|
|||||||
Vendored
+145
-70
File diff suppressed because one or more lines are too long
Vendored
+1
-1
File diff suppressed because one or more lines are too long
Vendored
+129
-67
File diff suppressed because one or more lines are too long
Vendored
+1
-1
File diff suppressed because one or more lines are too long
Generated
+575
-751
File diff suppressed because it is too large
Load Diff
@@ -16,7 +16,12 @@
|
|||||||
".": "./dist/index.mjs"
|
".": "./dist/index.mjs"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"tsup": "^8.2.4",
|
"@types/lodash": "^4.17.21",
|
||||||
"typescript": "^5.5.4"
|
"tsup": "^8.5.1",
|
||||||
|
"typescript": "^5.9.3"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"big-integer": "^1.6.52",
|
||||||
|
"lodash": "^4.17.21"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ export const MIN_AMNET_API_VERSION = 1;
|
|||||||
|
|
||||||
export interface AMCardInRequest {
|
export interface AMCardInRequest {
|
||||||
cardId: string;
|
cardId: string;
|
||||||
|
physicalCardIDm?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -51,7 +52,7 @@ export async function performCardIn(server: AMServerConnectionInfo, request: AMC
|
|||||||
if (server.apiVersion < MIN_AMNET_API_VERSION || server.apiVersion > AMNET_API_VERSION) {
|
if (server.apiVersion < MIN_AMNET_API_VERSION || server.apiVersion > AMNET_API_VERSION) {
|
||||||
throw new Error("Unsupported server API version");
|
throw new Error("Unsupported server API version");
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const response = await fetch(`${new URL(server.serverAddress).origin}/amnet/signin`, {
|
const response = await fetch(`${new URL(server.serverAddress).origin}/amnet/signin`, {
|
||||||
method: "POST",
|
method: "POST",
|
||||||
@@ -78,7 +79,7 @@ export async function fetchServerInfo(serverAddress: string, abort: AbortSignal)
|
|||||||
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) {
|
if (!response.ok) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|||||||
File diff suppressed because one or more lines are too long
@@ -1,23 +1,12 @@
|
|||||||
|
import {encodeAccessCode} from "./card-encoding";
|
||||||
|
|
||||||
export const CARD_ID_REGEX = /^(?:[0-9]{4} ?){5}$/i;
|
export const CARD_ID_REGEX = /^(?:[0-9]{4} ?){5}$/i;
|
||||||
|
export const AMNET_COMPANY_ID = 597;
|
||||||
|
|
||||||
export function generateCardNumber(): string {
|
export function generateCardNumber(): string {
|
||||||
let cardId = '';
|
// serial between 5000 and 99999999999
|
||||||
|
const serial = Math.floor(Math.random() * 99999994999) + 5000;
|
||||||
while (true) {
|
return encodeAccessCode(AMNET_COMPANY_ID, serial, 0);
|
||||||
// don't start with a 3
|
|
||||||
const firstDigit = Math.floor(Math.random() * 10);
|
|
||||||
|
|
||||||
if (firstDigit !== 3) {
|
|
||||||
cardId = firstDigit.toString();
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
for (let i = 0; i < 19; i++) {
|
|
||||||
cardId += Math.floor(Math.random() * 10).toString();
|
|
||||||
}
|
|
||||||
|
|
||||||
return cardId;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -4,17 +4,37 @@
|
|||||||
*/
|
*/
|
||||||
export function getGameName(id: string): string | null {
|
export function getGameName(id: string): string | null {
|
||||||
switch (id?.toUpperCase()) {
|
switch (id?.toUpperCase()) {
|
||||||
case "SDGT":
|
case "SDGT": // IDAC JPN
|
||||||
|
case "SDJJ": // IDAC CHN
|
||||||
return "Initial D The Arcade";
|
return "Initial D The Arcade";
|
||||||
case "SDHD":
|
|
||||||
case "SDBT":
|
case "SDGS": // CHUNITHM INTL
|
||||||
|
case "SDHD": // CHUNITHM NEW
|
||||||
|
case "SDHJ": // CHUNITHM CHN
|
||||||
|
case "SDBT": // CHUNITHM
|
||||||
return "CHUNITHM";
|
return "CHUNITHM";
|
||||||
|
|
||||||
|
case "SDEZ": // maimai DX
|
||||||
|
case "SDGA": // maimai DX INTL
|
||||||
|
case "SDGB": // maimai DX CHN
|
||||||
|
return "maimai DX";
|
||||||
|
|
||||||
case "SDDT":
|
case "SDDT":
|
||||||
return "O.N.G.E.K.I.";
|
return "O.N.G.E.K.I.";
|
||||||
case "SDEZ":
|
|
||||||
return "maimai DX";
|
case "SDED":
|
||||||
|
return "Card Maker";
|
||||||
|
|
||||||
case "SBZV":
|
case "SBZV":
|
||||||
return "Project DIVA Future Tone";
|
return "Project DIVA Future Tone";
|
||||||
|
|
||||||
|
case "SDDS":
|
||||||
|
return "SEGA World Driver Championship";
|
||||||
|
|
||||||
|
case "SDFE": // Wacca JPN
|
||||||
|
case "SDHN": // Wacca Ver.2/2R CHN
|
||||||
|
case "SDJE": // Wacca Ver.3 CHN
|
||||||
|
return "Wacca";
|
||||||
|
|
||||||
default:
|
default:
|
||||||
return null;
|
return null;
|
||||||
|
|||||||
@@ -1,3 +1,5 @@
|
|||||||
export * from './api';
|
export * from './api';
|
||||||
export * from './card';
|
export * from './card';
|
||||||
export * from './gameIds';
|
export * from './gameIds';
|
||||||
|
|
||||||
|
export {decodeAccessCode as DecodeAMAccessCode, encodeAccessCode as EncodeAMAccessCode} from './card-encoding';
|
||||||
@@ -1,14 +1,15 @@
|
|||||||
{
|
{
|
||||||
"compilerOptions": {
|
"compilerOptions": {
|
||||||
"target": "ES2016",
|
"baseUrl": ".",
|
||||||
|
"target": "ES2017",
|
||||||
"module": "CommonJS",
|
"module": "CommonJS",
|
||||||
"noEmit": true,
|
"noEmit": true,
|
||||||
|
"strict": true,
|
||||||
|
"skipLibCheck": true,
|
||||||
"esModuleInterop": true,
|
"esModuleInterop": true,
|
||||||
"forceConsistentCasingInFileNames": true,
|
"forceConsistentCasingInFileNames": true,
|
||||||
"strict": true,
|
|
||||||
"noUncheckedIndexedAccess": true,
|
|
||||||
"skipLibCheck": true,
|
|
||||||
"baseUrl": "."
|
|
||||||
},
|
},
|
||||||
"include": ["src/**/*.ts"]
|
"include": [
|
||||||
|
"src/**/*.ts"
|
||||||
|
]
|
||||||
}
|
}
|
||||||
|
|||||||
Generated
+2455
-2183
File diff suppressed because it is too large
Load Diff
+39
-39
@@ -10,49 +10,49 @@
|
|||||||
"preview": "vite preview"
|
"preview": "vite preview"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@hookform/resolvers": "^3.9.0",
|
"@hookform/resolvers": "^5.2.2",
|
||||||
"@radix-ui/react-alert-dialog": "^1.1.1",
|
"@radix-ui/react-alert-dialog": "^1.1.15",
|
||||||
"@radix-ui/react-context-menu": "^2.2.1",
|
"@radix-ui/react-context-menu": "^2.2.16",
|
||||||
"@radix-ui/react-dialog": "^1.1.1",
|
"@radix-ui/react-dialog": "^1.1.15",
|
||||||
"@radix-ui/react-label": "^2.1.0",
|
"@radix-ui/react-label": "^2.1.8",
|
||||||
"@radix-ui/react-separator": "^1.1.0",
|
"@radix-ui/react-separator": "^1.1.8",
|
||||||
"@radix-ui/react-slot": "^1.1.0",
|
"@radix-ui/react-slot": "^1.1.4",
|
||||||
"@radix-ui/react-toast": "^1.2.1",
|
"@radix-ui/react-toast": "^1.2.15",
|
||||||
"@radix-ui/react-tooltip": "^1.1.2",
|
"@radix-ui/react-tooltip": "^1.2.8",
|
||||||
"amnet-shared": "file:../amnet-shared",
|
"amnet-shared": "file:../amnet-shared",
|
||||||
"class-variance-authority": "^0.7.0",
|
"class-variance-authority": "^0.7.1",
|
||||||
"clsx": "^2.1.1",
|
"clsx": "^2.1.1",
|
||||||
"core-js": "^3.39.0",
|
"core-js": "^3.47.0",
|
||||||
"detectincognitojs": "^1.3.5",
|
"detectincognitojs": "^1.6.2",
|
||||||
"dexie": "^4.0.8",
|
"dexie": "^4.2.1",
|
||||||
"dexie-react-hooks": "^1.1.7",
|
"dexie-react-hooks": "^4.2.0",
|
||||||
"lucide-react": "^0.427.0",
|
"lucide-react": "^0.562.0",
|
||||||
"react": "^18.3.1",
|
"react": "^19.2.3",
|
||||||
"react-dom": "^18.3.1",
|
"react-dom": "^19.2.3",
|
||||||
"react-hook-form": "^7.52.2",
|
"react-hook-form": "^7.69.0",
|
||||||
"react-router-dom": "^6.26.0",
|
"react-router-dom": "^7.11.0",
|
||||||
"react-timeago": "^7.2.0",
|
"react-timeago": "^8.3.0",
|
||||||
"tailwind-merge": "^2.4.0",
|
"tailwind-merge": "^3.4.0",
|
||||||
"tailwindcss-animate": "^1.0.7",
|
"tw-animate-css": "^1.4.0",
|
||||||
"vaul": "^0.9.1",
|
"vaul": "^1.1.2",
|
||||||
"zod": "^3.23.8"
|
"zod": "^4.2.1"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@eslint/js": "^9.8.0",
|
"@eslint/js": "^9.23.0",
|
||||||
"@types/node": "^22.2.0",
|
"@tailwindcss/postcss": "^4.1.18",
|
||||||
"@types/react": "^18.3.3",
|
"@types/node": "^24.0.0",
|
||||||
"@types/react-dom": "^18.3.0",
|
"@types/react": "^19.2.7",
|
||||||
|
"@types/react-dom": "^19.2.3",
|
||||||
"@types/react-timeago": "^4.1.7",
|
"@types/react-timeago": "^4.1.7",
|
||||||
"@vitejs/plugin-react-swc": "^3.5.0",
|
"@vitejs/plugin-react-swc": "^4.2.2",
|
||||||
"autoprefixer": "^10.4.20",
|
"eslint": "^9.23.0",
|
||||||
"eslint": "^9.8.0",
|
"eslint-plugin-react-hooks": "^7.0.1",
|
||||||
"eslint-plugin-react-hooks": "^5.1.0-rc.0",
|
"eslint-plugin-react-refresh": "^0.4.26",
|
||||||
"eslint-plugin-react-refresh": "^0.4.9",
|
"globals": "^16.5.0",
|
||||||
"globals": "^15.9.0",
|
"postcss": "^8.5.6",
|
||||||
"postcss": "^8.4.41",
|
"tailwindcss": "^4.1.18",
|
||||||
"tailwindcss": "^3.4.9",
|
"typescript": "^5.9.3",
|
||||||
"typescript": "^5.5.3",
|
"typescript-eslint": "^8.51.0",
|
||||||
"typescript-eslint": "^8.0.0",
|
"vite": "^7.3.0"
|
||||||
"vite": "^5.4.6"
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
export default {
|
export default {
|
||||||
plugins: {
|
plugins: {
|
||||||
tailwindcss: {},
|
'@tailwindcss/postcss': {},
|
||||||
autoprefixer: {},
|
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,29 +0,0 @@
|
|||||||
{
|
|
||||||
"applinks": {
|
|
||||||
"details": [
|
|
||||||
{
|
|
||||||
"appIDs": [
|
|
||||||
"Q824VHAT9S.moe.ppc.amnet"
|
|
||||||
],
|
|
||||||
"components": [
|
|
||||||
{
|
|
||||||
"#": "/cards*"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"#": "/servers"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"#": "/servers/"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"#": "/servers/add*"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"#": "/servers/*",
|
|
||||||
"exclude": true
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -24,7 +24,7 @@ export function AMCard(props: AMCardActionProps & { title: string, titlePerforms
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{props.actionClicked && (
|
{props.actionClicked && (
|
||||||
<Button className="flex-shrink-0" variant="ghost" size="icon" disabled={props.actionDisabled} onClick={props.actionClicked}>
|
<Button className="shrink-0" variant="ghost" size="icon" disabled={props.actionDisabled} onClick={props.actionClicked}>
|
||||||
{props.actionIcon ?? <ChevronRight className="h-4 w-4"/>}
|
{props.actionIcon ?? <ChevronRight className="h-4 w-4"/>}
|
||||||
</Button>
|
</Button>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ export function AMHeader(props: { title: string, children?: Readonly<React.React
|
|||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<div className="flex items-center gap-1">
|
<div className="flex items-center gap-1">
|
||||||
<h2 className="text-4xl font-bold select-none flex-grow">{props.title}</h2>
|
<h2 className="text-4xl font-bold select-none grow">{props.title}</h2>
|
||||||
{props.children}
|
{props.children}
|
||||||
</div>
|
</div>
|
||||||
<Separator/>
|
<Separator/>
|
||||||
|
|||||||
@@ -98,10 +98,9 @@ function AimeCardCreationForm(props: { callback?: () => void }) {
|
|||||||
<FormControl>
|
<FormControl>
|
||||||
<Input placeholder="0000 0000 0000 0000 0000" {...field} />
|
<Input placeholder="0000 0000 0000 0000 0000" {...field} />
|
||||||
</FormControl>
|
</FormControl>
|
||||||
<FormDescription>
|
<FormDescription className="select-none">
|
||||||
If you've got a card, enter the matrix code here. If not, you can
|
If you've got a card, enter the matrix code here. If not, you can
|
||||||
<span className="font-bold hover:cursor-pointer ml-1"
|
<span className="font-bold hover:cursor-pointer ml-1" onClick={() => form.setValue("id", generateCardNumber())}>
|
||||||
onClick={() => form.setValue("id", generateCardNumber())}>
|
|
||||||
generate a new one.
|
generate a new one.
|
||||||
</span>
|
</span>
|
||||||
</FormDescription>
|
</FormDescription>
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ import {Loader} from "lucide-react";
|
|||||||
|
|
||||||
export function LoadingIndicator() {
|
export function LoadingIndicator() {
|
||||||
return (
|
return (
|
||||||
<div className="flex flex-grow gap-2 justify-center items-center text-gray-500 py-5">
|
<div className="flex grow gap-2 justify-center items-center text-gray-500 py-5">
|
||||||
<Loader className="h-8 w-8"/>
|
<Loader className="h-8 w-8"/>
|
||||||
<span className="text-center">Loading...</span>
|
<span className="text-center">Loading...</span>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -10,37 +10,39 @@ const AlertDialogTrigger = AlertDialogPrimitive.Trigger
|
|||||||
|
|
||||||
const AlertDialogPortal = AlertDialogPrimitive.Portal
|
const AlertDialogPortal = AlertDialogPrimitive.Portal
|
||||||
|
|
||||||
const AlertDialogOverlay = React.forwardRef<
|
function AlertDialogOverlay({
|
||||||
React.ElementRef<typeof AlertDialogPrimitive.Overlay>,
|
className,
|
||||||
React.ComponentPropsWithoutRef<typeof AlertDialogPrimitive.Overlay>
|
...props
|
||||||
>(({ className, ...props }, ref) => (
|
}: React.ComponentProps<typeof AlertDialogPrimitive.Overlay>) {
|
||||||
<AlertDialogPrimitive.Overlay
|
return (
|
||||||
className={cn(
|
<AlertDialogPrimitive.Overlay
|
||||||
"fixed inset-0 z-50 bg-black/80 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0",
|
|
||||||
className
|
|
||||||
)}
|
|
||||||
{...props}
|
|
||||||
ref={ref}
|
|
||||||
/>
|
|
||||||
))
|
|
||||||
AlertDialogOverlay.displayName = AlertDialogPrimitive.Overlay.displayName
|
|
||||||
|
|
||||||
const AlertDialogContent = React.forwardRef<
|
|
||||||
React.ElementRef<typeof AlertDialogPrimitive.Content>,
|
|
||||||
React.ComponentPropsWithoutRef<typeof AlertDialogPrimitive.Content>
|
|
||||||
>(({ className, ...props }, ref) => (
|
|
||||||
<AlertDialogPortal>
|
|
||||||
<AlertDialogOverlay />
|
|
||||||
<AlertDialogPrimitive.Content
|
|
||||||
ref={ref}
|
|
||||||
className={cn(
|
className={cn(
|
||||||
"fixed left-[50%] top-[50%] z-50 grid w-full max-w-lg translate-x-[-50%] translate-y-[-50%] gap-4 border bg-background p-6 shadow-lg duration-200 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[state=closed]:slide-out-to-left-1/2 data-[state=closed]:slide-out-to-top-[48%] data-[state=open]:slide-in-from-left-1/2 data-[state=open]:slide-in-from-top-[48%] sm:rounded-lg",
|
"fixed inset-0 z-50 bg-black/80 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0",
|
||||||
className
|
className
|
||||||
)}
|
)}
|
||||||
{...props}
|
{...props}
|
||||||
/>
|
/>
|
||||||
</AlertDialogPortal>
|
)
|
||||||
))
|
}
|
||||||
|
AlertDialogOverlay.displayName = AlertDialogPrimitive.Overlay.displayName
|
||||||
|
|
||||||
|
function AlertDialogContent({
|
||||||
|
className,
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<typeof AlertDialogPrimitive.Content>) {
|
||||||
|
return (
|
||||||
|
<AlertDialogPortal>
|
||||||
|
<AlertDialogOverlay />
|
||||||
|
<AlertDialogPrimitive.Content
|
||||||
|
className={cn(
|
||||||
|
"fixed left-[50%] top-[50%] z-50 grid w-full max-w-lg translate-x-[-50%] translate-y-[-50%] gap-4 border bg-background p-6 shadow-lg duration-200 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[state=closed]:slide-out-to-left-1/2 data-[state=closed]:slide-out-to-top-[48%] data-[state=open]:slide-in-from-left-1/2 data-[state=open]:slide-in-from-top-[48%] sm:rounded-lg",
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
</AlertDialogPortal>
|
||||||
|
)
|
||||||
|
}
|
||||||
AlertDialogContent.displayName = AlertDialogPrimitive.Content.displayName
|
AlertDialogContent.displayName = AlertDialogPrimitive.Content.displayName
|
||||||
|
|
||||||
const AlertDialogHeader = ({
|
const AlertDialogHeader = ({
|
||||||
@@ -71,57 +73,61 @@ const AlertDialogFooter = ({
|
|||||||
)
|
)
|
||||||
AlertDialogFooter.displayName = "AlertDialogFooter"
|
AlertDialogFooter.displayName = "AlertDialogFooter"
|
||||||
|
|
||||||
const AlertDialogTitle = React.forwardRef<
|
function AlertDialogTitle({
|
||||||
React.ElementRef<typeof AlertDialogPrimitive.Title>,
|
className,
|
||||||
React.ComponentPropsWithoutRef<typeof AlertDialogPrimitive.Title>
|
...props
|
||||||
>(({ className, ...props }, ref) => (
|
}: React.ComponentProps<typeof AlertDialogPrimitive.Title>) {
|
||||||
<AlertDialogPrimitive.Title
|
return (
|
||||||
ref={ref}
|
<AlertDialogPrimitive.Title
|
||||||
className={cn("text-lg font-semibold", className)}
|
className={cn("text-lg font-semibold", className)}
|
||||||
{...props}
|
{...props}
|
||||||
/>
|
/>
|
||||||
))
|
)
|
||||||
|
}
|
||||||
AlertDialogTitle.displayName = AlertDialogPrimitive.Title.displayName
|
AlertDialogTitle.displayName = AlertDialogPrimitive.Title.displayName
|
||||||
|
|
||||||
const AlertDialogDescription = React.forwardRef<
|
function AlertDialogDescription({
|
||||||
React.ElementRef<typeof AlertDialogPrimitive.Description>,
|
className,
|
||||||
React.ComponentPropsWithoutRef<typeof AlertDialogPrimitive.Description>
|
...props
|
||||||
>(({ className, ...props }, ref) => (
|
}: React.ComponentProps<typeof AlertDialogPrimitive.Description>) {
|
||||||
<AlertDialogPrimitive.Description
|
return (
|
||||||
ref={ref}
|
<AlertDialogPrimitive.Description
|
||||||
className={cn("text-sm text-muted-foreground", className)}
|
className={cn("text-sm text-muted-foreground", className)}
|
||||||
{...props}
|
{...props}
|
||||||
/>
|
/>
|
||||||
))
|
)
|
||||||
|
}
|
||||||
AlertDialogDescription.displayName =
|
AlertDialogDescription.displayName =
|
||||||
AlertDialogPrimitive.Description.displayName
|
AlertDialogPrimitive.Description.displayName
|
||||||
|
|
||||||
const AlertDialogAction = React.forwardRef<
|
function AlertDialogAction({
|
||||||
React.ElementRef<typeof AlertDialogPrimitive.Action>,
|
className,
|
||||||
React.ComponentPropsWithoutRef<typeof AlertDialogPrimitive.Action>
|
...props
|
||||||
>(({ className, ...props }, ref) => (
|
}: React.ComponentProps<typeof AlertDialogPrimitive.Action>) {
|
||||||
<AlertDialogPrimitive.Action
|
return (
|
||||||
ref={ref}
|
<AlertDialogPrimitive.Action
|
||||||
className={cn(buttonVariants(), className)}
|
className={cn(buttonVariants(), className)}
|
||||||
{...props}
|
{...props}
|
||||||
/>
|
/>
|
||||||
))
|
)
|
||||||
|
}
|
||||||
AlertDialogAction.displayName = AlertDialogPrimitive.Action.displayName
|
AlertDialogAction.displayName = AlertDialogPrimitive.Action.displayName
|
||||||
|
|
||||||
const AlertDialogCancel = React.forwardRef<
|
function AlertDialogCancel({
|
||||||
React.ElementRef<typeof AlertDialogPrimitive.Cancel>,
|
className,
|
||||||
React.ComponentPropsWithoutRef<typeof AlertDialogPrimitive.Cancel>
|
...props
|
||||||
>(({ className, ...props }, ref) => (
|
}: React.ComponentProps<typeof AlertDialogPrimitive.Cancel>) {
|
||||||
<AlertDialogPrimitive.Cancel
|
return (
|
||||||
ref={ref}
|
<AlertDialogPrimitive.Cancel
|
||||||
className={cn(
|
className={cn(
|
||||||
buttonVariants({ variant: "outline" }),
|
buttonVariants({ variant: "outline" }),
|
||||||
"mt-2 sm:mt-0",
|
"mt-2 sm:mt-0",
|
||||||
className
|
className
|
||||||
)}
|
)}
|
||||||
{...props}
|
{...props}
|
||||||
/>
|
/>
|
||||||
))
|
)
|
||||||
|
}
|
||||||
AlertDialogCancel.displayName = AlertDialogPrimitive.Cancel.displayName
|
AlertDialogCancel.displayName = AlertDialogPrimitive.Cancel.displayName
|
||||||
|
|
||||||
export {
|
export {
|
||||||
|
|||||||
@@ -19,41 +19,45 @@ const alertVariants = cva(
|
|||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
const Alert = React.forwardRef<
|
function Alert({
|
||||||
HTMLDivElement,
|
className,
|
||||||
React.HTMLAttributes<HTMLDivElement> & VariantProps<typeof alertVariants>
|
variant,
|
||||||
>(({ className, variant, ...props }, ref) => (
|
...props
|
||||||
<div
|
}: React.HTMLAttributes<HTMLDivElement> & VariantProps<typeof alertVariants>) {
|
||||||
ref={ref}
|
return (
|
||||||
role="alert"
|
<div
|
||||||
className={cn(alertVariants({ variant }), className)}
|
role="alert"
|
||||||
{...props}
|
className={cn(alertVariants({ variant }), className)}
|
||||||
/>
|
{...props}
|
||||||
))
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
Alert.displayName = "Alert"
|
Alert.displayName = "Alert"
|
||||||
|
|
||||||
const AlertTitle = React.forwardRef<
|
function AlertTitle({
|
||||||
HTMLParagraphElement,
|
className,
|
||||||
React.HTMLAttributes<HTMLHeadingElement>
|
...props
|
||||||
>(({ className, ...props }, ref) => (
|
}: React.HTMLAttributes<HTMLHeadingElement>) {
|
||||||
<h5
|
return (
|
||||||
ref={ref}
|
<h5
|
||||||
className={cn("mb-1 font-medium leading-none tracking-tight", className)}
|
className={cn("mb-1 font-medium leading-none tracking-tight", className)}
|
||||||
{...props}
|
{...props}
|
||||||
/>
|
/>
|
||||||
))
|
)
|
||||||
|
}
|
||||||
AlertTitle.displayName = "AlertTitle"
|
AlertTitle.displayName = "AlertTitle"
|
||||||
|
|
||||||
const AlertDescription = React.forwardRef<
|
function AlertDescription({
|
||||||
HTMLParagraphElement,
|
className,
|
||||||
React.HTMLAttributes<HTMLParagraphElement>
|
...props
|
||||||
>(({ className, ...props }, ref) => (
|
}: React.HTMLAttributes<HTMLParagraphElement>) {
|
||||||
<div
|
return (
|
||||||
ref={ref}
|
<div
|
||||||
className={cn("text-sm [&_p]:leading-relaxed", className)}
|
className={cn("text-sm [&_p]:leading-relaxed", className)}
|
||||||
{...props}
|
{...props}
|
||||||
/>
|
/>
|
||||||
))
|
)
|
||||||
|
}
|
||||||
AlertDescription.displayName = "AlertDescription"
|
AlertDescription.displayName = "AlertDescription"
|
||||||
|
|
||||||
export { Alert, AlertTitle, AlertDescription }
|
export { Alert, AlertTitle, AlertDescription }
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ import { cva, type VariantProps } from "class-variance-authority"
|
|||||||
import { cn } from "@/lib/utils"
|
import { cn } from "@/lib/utils"
|
||||||
|
|
||||||
const buttonVariants = cva(
|
const buttonVariants = cva(
|
||||||
"inline-flex items-center justify-center whitespace-nowrap rounded-md text-sm font-medium ring-offset-background transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50",
|
"inline-flex items-center justify-center whitespace-nowrap rounded-md text-sm font-medium ring-offset-background transition-colors focus-visible:outline-hidden focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50",
|
||||||
{
|
{
|
||||||
variants: {
|
variants: {
|
||||||
variant: {
|
variant: {
|
||||||
@@ -39,18 +39,21 @@ export interface ButtonProps
|
|||||||
asChild?: boolean
|
asChild?: boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
const Button = React.forwardRef<HTMLButtonElement, ButtonProps>(
|
function Button({
|
||||||
({ className, variant, size, asChild = false, ...props }, ref) => {
|
className,
|
||||||
const Comp = asChild ? Slot : "button"
|
variant,
|
||||||
return (
|
size,
|
||||||
<Comp
|
asChild = false,
|
||||||
className={cn(buttonVariants({ variant, size, className }))}
|
...props
|
||||||
ref={ref}
|
}: ButtonProps) {
|
||||||
{...props}
|
const Comp = asChild ? Slot : "button"
|
||||||
/>
|
return (
|
||||||
)
|
<Comp
|
||||||
}
|
className={cn(buttonVariants({ variant, size, className }))}
|
||||||
)
|
{...props}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
Button.displayName = "Button"
|
Button.displayName = "Button"
|
||||||
|
|
||||||
export { Button, buttonVariants }
|
export { Button, buttonVariants }
|
||||||
|
|||||||
@@ -2,78 +2,85 @@ import * as React from "react"
|
|||||||
|
|
||||||
import { cn } from "@/lib/utils"
|
import { cn } from "@/lib/utils"
|
||||||
|
|
||||||
const Card = React.forwardRef<
|
function Card({
|
||||||
HTMLDivElement,
|
className,
|
||||||
React.HTMLAttributes<HTMLDivElement>
|
...props
|
||||||
>(({ className, ...props }, ref) => (
|
}: React.HTMLAttributes<HTMLDivElement>) {
|
||||||
<div
|
return (
|
||||||
ref={ref}
|
<div
|
||||||
className={cn(
|
className={cn(
|
||||||
"rounded-lg border bg-card text-card-foreground shadow-sm",
|
"rounded-lg border bg-card text-card-foreground shadow-xs",
|
||||||
className
|
className
|
||||||
)}
|
)}
|
||||||
{...props}
|
{...props}
|
||||||
/>
|
/>
|
||||||
))
|
)
|
||||||
|
}
|
||||||
Card.displayName = "Card"
|
Card.displayName = "Card"
|
||||||
|
|
||||||
const CardHeader = React.forwardRef<
|
function CardHeader({
|
||||||
HTMLDivElement,
|
className,
|
||||||
React.HTMLAttributes<HTMLDivElement>
|
...props
|
||||||
>(({ className, ...props }, ref) => (
|
}: React.HTMLAttributes<HTMLDivElement>) {
|
||||||
<div
|
return (
|
||||||
ref={ref}
|
<div
|
||||||
className={cn("flex flex-col space-y-1.5 p-6", className)}
|
className={cn("flex flex-col space-y-1.5 p-6", className)}
|
||||||
{...props}
|
{...props}
|
||||||
/>
|
/>
|
||||||
))
|
)
|
||||||
|
}
|
||||||
CardHeader.displayName = "CardHeader"
|
CardHeader.displayName = "CardHeader"
|
||||||
|
|
||||||
const CardTitle = React.forwardRef<
|
function CardTitle({
|
||||||
HTMLParagraphElement,
|
className,
|
||||||
React.HTMLAttributes<HTMLHeadingElement>
|
...props
|
||||||
>(({ className, ...props }, ref) => (
|
}: React.HTMLAttributes<HTMLHeadingElement>) {
|
||||||
<h3
|
return (
|
||||||
ref={ref}
|
<h3
|
||||||
className={cn(
|
className={cn(
|
||||||
"text-2xl font-semibold leading-none tracking-tight",
|
"text-2xl font-semibold leading-none tracking-tight",
|
||||||
className
|
className
|
||||||
)}
|
)}
|
||||||
{...props}
|
{...props}
|
||||||
/>
|
/>
|
||||||
))
|
)
|
||||||
|
}
|
||||||
CardTitle.displayName = "CardTitle"
|
CardTitle.displayName = "CardTitle"
|
||||||
|
|
||||||
const CardDescription = React.forwardRef<
|
function CardDescription({
|
||||||
HTMLParagraphElement,
|
className,
|
||||||
React.HTMLAttributes<HTMLParagraphElement>
|
...props
|
||||||
>(({ className, ...props }, ref) => (
|
}: React.HTMLAttributes<HTMLParagraphElement>) {
|
||||||
<p
|
return (
|
||||||
ref={ref}
|
<p
|
||||||
className={cn("text-sm text-muted-foreground", className)}
|
className={cn("text-sm text-muted-foreground", className)}
|
||||||
{...props}
|
{...props}
|
||||||
/>
|
/>
|
||||||
))
|
)
|
||||||
|
}
|
||||||
CardDescription.displayName = "CardDescription"
|
CardDescription.displayName = "CardDescription"
|
||||||
|
|
||||||
const CardContent = React.forwardRef<
|
function CardContent({
|
||||||
HTMLDivElement,
|
className,
|
||||||
React.HTMLAttributes<HTMLDivElement>
|
...props
|
||||||
>(({ className, ...props }, ref) => (
|
}: React.HTMLAttributes<HTMLDivElement>) {
|
||||||
<div ref={ref} className={cn("p-6 pt-0", className)} {...props} />
|
return (
|
||||||
))
|
<div className={cn("p-6 pt-0", className)} {...props} />
|
||||||
|
)
|
||||||
|
}
|
||||||
CardContent.displayName = "CardContent"
|
CardContent.displayName = "CardContent"
|
||||||
|
|
||||||
const CardFooter = React.forwardRef<
|
function CardFooter({
|
||||||
HTMLDivElement,
|
className,
|
||||||
React.HTMLAttributes<HTMLDivElement>
|
...props
|
||||||
>(({ className, ...props }, ref) => (
|
}: React.HTMLAttributes<HTMLDivElement>) {
|
||||||
<div
|
return (
|
||||||
ref={ref}
|
<div
|
||||||
className={cn("flex items-center p-6 pt-0", className)}
|
className={cn("flex items-center p-6 pt-0", className)}
|
||||||
{...props}
|
{...props}
|
||||||
/>
|
/>
|
||||||
))
|
)
|
||||||
|
}
|
||||||
CardFooter.displayName = "CardFooter"
|
CardFooter.displayName = "CardFooter"
|
||||||
|
|
||||||
export { Card, CardHeader, CardFooter, CardTitle, CardDescription, CardContent }
|
export { Card, CardHeader, CardFooter, CardTitle, CardDescription, CardContent }
|
||||||
|
|||||||
@@ -16,151 +16,166 @@ const ContextMenuSub = ContextMenuPrimitive.Sub
|
|||||||
|
|
||||||
const ContextMenuRadioGroup = ContextMenuPrimitive.RadioGroup
|
const ContextMenuRadioGroup = ContextMenuPrimitive.RadioGroup
|
||||||
|
|
||||||
const ContextMenuSubTrigger = React.forwardRef<
|
function ContextMenuSubTrigger({
|
||||||
React.ElementRef<typeof ContextMenuPrimitive.SubTrigger>,
|
className,
|
||||||
React.ComponentPropsWithoutRef<typeof ContextMenuPrimitive.SubTrigger> & {
|
inset,
|
||||||
inset?: boolean
|
children,
|
||||||
}
|
...props
|
||||||
>(({ className, inset, children, ...props }, ref) => (
|
}: React.ComponentProps<typeof ContextMenuPrimitive.SubTrigger> & {
|
||||||
<ContextMenuPrimitive.SubTrigger
|
inset?: boolean
|
||||||
ref={ref}
|
}) {
|
||||||
className={cn(
|
return (
|
||||||
"flex cursor-default select-none items-center rounded-sm px-2 py-1.5 text-sm outline-none focus:bg-accent focus:text-accent-foreground data-[state=open]:bg-accent data-[state=open]:text-accent-foreground",
|
<ContextMenuPrimitive.SubTrigger
|
||||||
inset && "pl-8",
|
className={cn(
|
||||||
className
|
"flex cursor-default select-none items-center rounded-sm px-2 py-1.5 text-sm outline-hidden focus:bg-accent focus:text-accent-foreground data-[state=open]:bg-accent data-[state=open]:text-accent-foreground",
|
||||||
)}
|
inset && "pl-8",
|
||||||
{...props}
|
className
|
||||||
>
|
)}
|
||||||
{children}
|
{...props}
|
||||||
<ChevronRight className="ml-auto h-4 w-4" />
|
>
|
||||||
</ContextMenuPrimitive.SubTrigger>
|
{children}
|
||||||
))
|
<ChevronRight className="ml-auto h-4 w-4" />
|
||||||
|
</ContextMenuPrimitive.SubTrigger>
|
||||||
|
)
|
||||||
|
}
|
||||||
ContextMenuSubTrigger.displayName = ContextMenuPrimitive.SubTrigger.displayName
|
ContextMenuSubTrigger.displayName = ContextMenuPrimitive.SubTrigger.displayName
|
||||||
|
|
||||||
const ContextMenuSubContent = React.forwardRef<
|
function ContextMenuSubContent({
|
||||||
React.ElementRef<typeof ContextMenuPrimitive.SubContent>,
|
className,
|
||||||
React.ComponentPropsWithoutRef<typeof ContextMenuPrimitive.SubContent>
|
...props
|
||||||
>(({ className, ...props }, ref) => (
|
}: React.ComponentProps<typeof ContextMenuPrimitive.SubContent>) {
|
||||||
<ContextMenuPrimitive.SubContent
|
return (
|
||||||
ref={ref}
|
<ContextMenuPrimitive.SubContent
|
||||||
className={cn(
|
|
||||||
"z-50 min-w-[8rem] overflow-hidden rounded-md border bg-popover p-1 text-popover-foreground shadow-md data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2",
|
|
||||||
className
|
|
||||||
)}
|
|
||||||
{...props}
|
|
||||||
/>
|
|
||||||
))
|
|
||||||
ContextMenuSubContent.displayName = ContextMenuPrimitive.SubContent.displayName
|
|
||||||
|
|
||||||
const ContextMenuContent = React.forwardRef<
|
|
||||||
React.ElementRef<typeof ContextMenuPrimitive.Content>,
|
|
||||||
React.ComponentPropsWithoutRef<typeof ContextMenuPrimitive.Content>
|
|
||||||
>(({ className, ...props }, ref) => (
|
|
||||||
<ContextMenuPrimitive.Portal>
|
|
||||||
<ContextMenuPrimitive.Content
|
|
||||||
ref={ref}
|
|
||||||
className={cn(
|
className={cn(
|
||||||
"z-50 min-w-[8rem] overflow-hidden rounded-md border bg-popover p-1 text-popover-foreground shadow-md animate-in fade-in-80 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2",
|
"z-50 min-w-32 overflow-hidden rounded-md border bg-popover p-1 text-popover-foreground shadow-md data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2",
|
||||||
className
|
className
|
||||||
)}
|
)}
|
||||||
{...props}
|
{...props}
|
||||||
/>
|
/>
|
||||||
</ContextMenuPrimitive.Portal>
|
)
|
||||||
))
|
}
|
||||||
|
ContextMenuSubContent.displayName = ContextMenuPrimitive.SubContent.displayName
|
||||||
|
|
||||||
|
function ContextMenuContent({
|
||||||
|
className,
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<typeof ContextMenuPrimitive.Content>) {
|
||||||
|
return (
|
||||||
|
<ContextMenuPrimitive.Portal>
|
||||||
|
<ContextMenuPrimitive.Content
|
||||||
|
className={cn(
|
||||||
|
"z-50 min-w-32 overflow-hidden rounded-md border bg-popover p-1 text-popover-foreground shadow-md animate-in fade-in-80 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2",
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
</ContextMenuPrimitive.Portal>
|
||||||
|
)
|
||||||
|
}
|
||||||
ContextMenuContent.displayName = ContextMenuPrimitive.Content.displayName
|
ContextMenuContent.displayName = ContextMenuPrimitive.Content.displayName
|
||||||
|
|
||||||
const ContextMenuItem = React.forwardRef<
|
function ContextMenuItem({
|
||||||
React.ElementRef<typeof ContextMenuPrimitive.Item>,
|
className,
|
||||||
React.ComponentPropsWithoutRef<typeof ContextMenuPrimitive.Item> & {
|
inset,
|
||||||
inset?: boolean
|
...props
|
||||||
}
|
}: React.ComponentProps<typeof ContextMenuPrimitive.Item> & {
|
||||||
>(({ className, inset, ...props }, ref) => (
|
inset?: boolean
|
||||||
<ContextMenuPrimitive.Item
|
}) {
|
||||||
ref={ref}
|
return (
|
||||||
className={cn(
|
<ContextMenuPrimitive.Item
|
||||||
"relative flex cursor-default select-none items-center rounded-sm px-2 py-1.5 text-sm outline-none focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50",
|
className={cn(
|
||||||
inset && "pl-8",
|
"relative flex cursor-default select-none items-center rounded-sm px-2 py-1.5 text-sm outline-hidden focus:bg-accent focus:text-accent-foreground data-disabled:pointer-events-none data-disabled:opacity-50",
|
||||||
className
|
inset && "pl-8",
|
||||||
)}
|
className
|
||||||
{...props}
|
)}
|
||||||
/>
|
{...props}
|
||||||
))
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
ContextMenuItem.displayName = ContextMenuPrimitive.Item.displayName
|
ContextMenuItem.displayName = ContextMenuPrimitive.Item.displayName
|
||||||
|
|
||||||
const ContextMenuCheckboxItem = React.forwardRef<
|
function ContextMenuCheckboxItem({
|
||||||
React.ElementRef<typeof ContextMenuPrimitive.CheckboxItem>,
|
className,
|
||||||
React.ComponentPropsWithoutRef<typeof ContextMenuPrimitive.CheckboxItem>
|
children,
|
||||||
>(({ className, children, checked, ...props }, ref) => (
|
checked,
|
||||||
<ContextMenuPrimitive.CheckboxItem
|
...props
|
||||||
ref={ref}
|
}: React.ComponentProps<typeof ContextMenuPrimitive.CheckboxItem>) {
|
||||||
className={cn(
|
return (
|
||||||
"relative flex cursor-default select-none items-center rounded-sm py-1.5 pl-8 pr-2 text-sm outline-none focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50",
|
<ContextMenuPrimitive.CheckboxItem
|
||||||
className
|
className={cn(
|
||||||
)}
|
"relative flex cursor-default select-none items-center rounded-sm py-1.5 pl-8 pr-2 text-sm outline-hidden focus:bg-accent focus:text-accent-foreground data-disabled:pointer-events-none data-disabled:opacity-50",
|
||||||
checked={checked}
|
className
|
||||||
{...props}
|
)}
|
||||||
>
|
checked={checked}
|
||||||
<span className="absolute left-2 flex h-3.5 w-3.5 items-center justify-center">
|
{...props}
|
||||||
<ContextMenuPrimitive.ItemIndicator>
|
>
|
||||||
<Check className="h-4 w-4" />
|
<span className="absolute left-2 flex h-3.5 w-3.5 items-center justify-center">
|
||||||
</ContextMenuPrimitive.ItemIndicator>
|
<ContextMenuPrimitive.ItemIndicator>
|
||||||
</span>
|
<Check className="h-4 w-4" />
|
||||||
{children}
|
</ContextMenuPrimitive.ItemIndicator>
|
||||||
</ContextMenuPrimitive.CheckboxItem>
|
</span>
|
||||||
))
|
{children}
|
||||||
|
</ContextMenuPrimitive.CheckboxItem>
|
||||||
|
)
|
||||||
|
}
|
||||||
ContextMenuCheckboxItem.displayName =
|
ContextMenuCheckboxItem.displayName =
|
||||||
ContextMenuPrimitive.CheckboxItem.displayName
|
ContextMenuPrimitive.CheckboxItem.displayName
|
||||||
|
|
||||||
const ContextMenuRadioItem = React.forwardRef<
|
function ContextMenuRadioItem({
|
||||||
React.ElementRef<typeof ContextMenuPrimitive.RadioItem>,
|
className,
|
||||||
React.ComponentPropsWithoutRef<typeof ContextMenuPrimitive.RadioItem>
|
children,
|
||||||
>(({ className, children, ...props }, ref) => (
|
...props
|
||||||
<ContextMenuPrimitive.RadioItem
|
}: React.ComponentProps<typeof ContextMenuPrimitive.RadioItem>) {
|
||||||
ref={ref}
|
return (
|
||||||
className={cn(
|
<ContextMenuPrimitive.RadioItem
|
||||||
"relative flex cursor-default select-none items-center rounded-sm py-1.5 pl-8 pr-2 text-sm outline-none focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50",
|
className={cn(
|
||||||
className
|
"relative flex cursor-default select-none items-center rounded-sm py-1.5 pl-8 pr-2 text-sm outline-hidden focus:bg-accent focus:text-accent-foreground data-disabled:pointer-events-none data-disabled:opacity-50",
|
||||||
)}
|
className
|
||||||
{...props}
|
)}
|
||||||
>
|
{...props}
|
||||||
<span className="absolute left-2 flex h-3.5 w-3.5 items-center justify-center">
|
>
|
||||||
<ContextMenuPrimitive.ItemIndicator>
|
<span className="absolute left-2 flex h-3.5 w-3.5 items-center justify-center">
|
||||||
<Circle className="h-2 w-2 fill-current" />
|
<ContextMenuPrimitive.ItemIndicator>
|
||||||
</ContextMenuPrimitive.ItemIndicator>
|
<Circle className="h-2 w-2 fill-current" />
|
||||||
</span>
|
</ContextMenuPrimitive.ItemIndicator>
|
||||||
{children}
|
</span>
|
||||||
</ContextMenuPrimitive.RadioItem>
|
{children}
|
||||||
))
|
</ContextMenuPrimitive.RadioItem>
|
||||||
|
)
|
||||||
|
}
|
||||||
ContextMenuRadioItem.displayName = ContextMenuPrimitive.RadioItem.displayName
|
ContextMenuRadioItem.displayName = ContextMenuPrimitive.RadioItem.displayName
|
||||||
|
|
||||||
const ContextMenuLabel = React.forwardRef<
|
function ContextMenuLabel({
|
||||||
React.ElementRef<typeof ContextMenuPrimitive.Label>,
|
className,
|
||||||
React.ComponentPropsWithoutRef<typeof ContextMenuPrimitive.Label> & {
|
inset,
|
||||||
inset?: boolean
|
...props
|
||||||
}
|
}: React.ComponentProps<typeof ContextMenuPrimitive.Label> & {
|
||||||
>(({ className, inset, ...props }, ref) => (
|
inset?: boolean
|
||||||
<ContextMenuPrimitive.Label
|
}) {
|
||||||
ref={ref}
|
return (
|
||||||
className={cn(
|
<ContextMenuPrimitive.Label
|
||||||
"px-2 py-1.5 text-sm font-semibold text-foreground",
|
className={cn(
|
||||||
inset && "pl-8",
|
"px-2 py-1.5 text-sm font-semibold text-foreground",
|
||||||
className
|
inset && "pl-8",
|
||||||
)}
|
className
|
||||||
{...props}
|
)}
|
||||||
/>
|
{...props}
|
||||||
))
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
ContextMenuLabel.displayName = ContextMenuPrimitive.Label.displayName
|
ContextMenuLabel.displayName = ContextMenuPrimitive.Label.displayName
|
||||||
|
|
||||||
const ContextMenuSeparator = React.forwardRef<
|
function ContextMenuSeparator({
|
||||||
React.ElementRef<typeof ContextMenuPrimitive.Separator>,
|
className,
|
||||||
React.ComponentPropsWithoutRef<typeof ContextMenuPrimitive.Separator>
|
...props
|
||||||
>(({ className, ...props }, ref) => (
|
}: React.ComponentProps<typeof ContextMenuPrimitive.Separator>) {
|
||||||
<ContextMenuPrimitive.Separator
|
return (
|
||||||
ref={ref}
|
<ContextMenuPrimitive.Separator
|
||||||
className={cn("-mx-1 my-1 h-px bg-border", className)}
|
className={cn("-mx-1 my-1 h-px bg-border", className)}
|
||||||
{...props}
|
{...props}
|
||||||
/>
|
/>
|
||||||
))
|
)
|
||||||
|
}
|
||||||
ContextMenuSeparator.displayName = ContextMenuPrimitive.Separator.displayName
|
ContextMenuSeparator.displayName = ContextMenuPrimitive.Separator.displayName
|
||||||
|
|
||||||
const ContextMenuShortcut = ({
|
const ContextMenuShortcut = ({
|
||||||
|
|||||||
@@ -12,43 +12,46 @@ const DialogPortal = DialogPrimitive.Portal
|
|||||||
|
|
||||||
const DialogClose = DialogPrimitive.Close
|
const DialogClose = DialogPrimitive.Close
|
||||||
|
|
||||||
const DialogOverlay = React.forwardRef<
|
function DialogOverlay({
|
||||||
React.ElementRef<typeof DialogPrimitive.Overlay>,
|
className,
|
||||||
React.ComponentPropsWithoutRef<typeof DialogPrimitive.Overlay>
|
...props
|
||||||
>(({ className, ...props }, ref) => (
|
}: React.ComponentProps<typeof DialogPrimitive.Overlay>) {
|
||||||
<DialogPrimitive.Overlay
|
return (
|
||||||
ref={ref}
|
<DialogPrimitive.Overlay
|
||||||
className={cn(
|
|
||||||
"fixed inset-0 z-50 bg-black/80 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0",
|
|
||||||
className
|
|
||||||
)}
|
|
||||||
{...props}
|
|
||||||
/>
|
|
||||||
))
|
|
||||||
DialogOverlay.displayName = DialogPrimitive.Overlay.displayName
|
|
||||||
|
|
||||||
const DialogContent = React.forwardRef<
|
|
||||||
React.ElementRef<typeof DialogPrimitive.Content>,
|
|
||||||
React.ComponentPropsWithoutRef<typeof DialogPrimitive.Content>
|
|
||||||
>(({ className, children, ...props }, ref) => (
|
|
||||||
<DialogPortal>
|
|
||||||
<DialogOverlay />
|
|
||||||
<DialogPrimitive.Content
|
|
||||||
ref={ref}
|
|
||||||
className={cn(
|
className={cn(
|
||||||
"fixed left-[50%] top-[50%] z-50 grid w-full max-w-lg translate-x-[-50%] translate-y-[-50%] gap-4 border bg-background p-6 shadow-lg duration-200 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[state=closed]:slide-out-to-left-1/2 data-[state=closed]:slide-out-to-top-[48%] data-[state=open]:slide-in-from-left-1/2 data-[state=open]:slide-in-from-top-[48%] sm:rounded-lg",
|
"fixed inset-0 z-50 bg-black/80 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0",
|
||||||
className
|
className
|
||||||
)}
|
)}
|
||||||
{...props}
|
{...props}
|
||||||
>
|
/>
|
||||||
{children}
|
)
|
||||||
<DialogPrimitive.Close className="absolute right-4 top-4 rounded-sm opacity-70 ring-offset-background transition-opacity hover:opacity-100 focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2 disabled:pointer-events-none data-[state=open]:bg-accent data-[state=open]:text-muted-foreground">
|
}
|
||||||
<X className="h-4 w-4" />
|
DialogOverlay.displayName = DialogPrimitive.Overlay.displayName
|
||||||
<span className="sr-only">Close</span>
|
|
||||||
</DialogPrimitive.Close>
|
function DialogContent({
|
||||||
</DialogPrimitive.Content>
|
className,
|
||||||
</DialogPortal>
|
children,
|
||||||
))
|
...props
|
||||||
|
}: React.ComponentProps<typeof DialogPrimitive.Content>) {
|
||||||
|
return (
|
||||||
|
<DialogPortal>
|
||||||
|
<DialogOverlay />
|
||||||
|
<DialogPrimitive.Content
|
||||||
|
className={cn(
|
||||||
|
"fixed left-[50%] top-[50%] z-50 grid w-full max-w-lg translate-x-[-50%] translate-y-[-50%] gap-4 border bg-background p-6 shadow-lg duration-200 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[state=closed]:slide-out-to-left-1/2 data-[state=closed]:slide-out-to-top-[48%] data-[state=open]:slide-in-from-left-1/2 data-[state=open]:slide-in-from-top-[48%] sm:rounded-lg",
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
>
|
||||||
|
{children}
|
||||||
|
<DialogPrimitive.Close className="absolute right-4 top-4 rounded-sm opacity-70 ring-offset-background transition-opacity hover:opacity-100 focus:outline-hidden focus:ring-2 focus:ring-ring focus:ring-offset-2 disabled:pointer-events-none data-[state=open]:bg-accent data-[state=open]:text-muted-foreground">
|
||||||
|
<X className="h-4 w-4" />
|
||||||
|
<span className="sr-only">Close</span>
|
||||||
|
</DialogPrimitive.Close>
|
||||||
|
</DialogPrimitive.Content>
|
||||||
|
</DialogPortal>
|
||||||
|
)
|
||||||
|
}
|
||||||
DialogContent.displayName = DialogPrimitive.Content.displayName
|
DialogContent.displayName = DialogPrimitive.Content.displayName
|
||||||
|
|
||||||
const DialogHeader = ({
|
const DialogHeader = ({
|
||||||
@@ -79,31 +82,33 @@ const DialogFooter = ({
|
|||||||
)
|
)
|
||||||
DialogFooter.displayName = "DialogFooter"
|
DialogFooter.displayName = "DialogFooter"
|
||||||
|
|
||||||
const DialogTitle = React.forwardRef<
|
function DialogTitle({
|
||||||
React.ElementRef<typeof DialogPrimitive.Title>,
|
className,
|
||||||
React.ComponentPropsWithoutRef<typeof DialogPrimitive.Title>
|
...props
|
||||||
>(({ className, ...props }, ref) => (
|
}: React.ComponentProps<typeof DialogPrimitive.Title>) {
|
||||||
<DialogPrimitive.Title
|
return (
|
||||||
ref={ref}
|
<DialogPrimitive.Title
|
||||||
className={cn(
|
className={cn(
|
||||||
"text-lg font-semibold leading-none tracking-tight",
|
"text-lg font-semibold leading-none tracking-tight",
|
||||||
className
|
className
|
||||||
)}
|
)}
|
||||||
{...props}
|
{...props}
|
||||||
/>
|
/>
|
||||||
))
|
)
|
||||||
|
}
|
||||||
DialogTitle.displayName = DialogPrimitive.Title.displayName
|
DialogTitle.displayName = DialogPrimitive.Title.displayName
|
||||||
|
|
||||||
const DialogDescription = React.forwardRef<
|
function DialogDescription({
|
||||||
React.ElementRef<typeof DialogPrimitive.Description>,
|
className,
|
||||||
React.ComponentPropsWithoutRef<typeof DialogPrimitive.Description>
|
...props
|
||||||
>(({ className, ...props }, ref) => (
|
}: React.ComponentProps<typeof DialogPrimitive.Description>) {
|
||||||
<DialogPrimitive.Description
|
return (
|
||||||
ref={ref}
|
<DialogPrimitive.Description
|
||||||
className={cn("text-sm text-muted-foreground", className)}
|
className={cn("text-sm text-muted-foreground", className)}
|
||||||
{...props}
|
{...props}
|
||||||
/>
|
/>
|
||||||
))
|
)
|
||||||
|
}
|
||||||
DialogDescription.displayName = DialogPrimitive.Description.displayName
|
DialogDescription.displayName = DialogPrimitive.Description.displayName
|
||||||
|
|
||||||
export {
|
export {
|
||||||
|
|||||||
@@ -20,37 +20,40 @@ const DrawerPortal = DrawerPrimitive.Portal
|
|||||||
|
|
||||||
const DrawerClose = DrawerPrimitive.Close
|
const DrawerClose = DrawerPrimitive.Close
|
||||||
|
|
||||||
const DrawerOverlay = React.forwardRef<
|
function DrawerOverlay({
|
||||||
React.ElementRef<typeof DrawerPrimitive.Overlay>,
|
className,
|
||||||
React.ComponentPropsWithoutRef<typeof DrawerPrimitive.Overlay>
|
...props
|
||||||
>(({ className, ...props }, ref) => (
|
}: React.ComponentProps<typeof DrawerPrimitive.Overlay>) {
|
||||||
<DrawerPrimitive.Overlay
|
return (
|
||||||
ref={ref}
|
<DrawerPrimitive.Overlay
|
||||||
className={cn("fixed inset-0 z-50 bg-black/80", className)}
|
className={cn("fixed inset-0 z-50 bg-black/80", className)}
|
||||||
{...props}
|
{...props}
|
||||||
/>
|
/>
|
||||||
))
|
)
|
||||||
|
}
|
||||||
DrawerOverlay.displayName = DrawerPrimitive.Overlay.displayName
|
DrawerOverlay.displayName = DrawerPrimitive.Overlay.displayName
|
||||||
|
|
||||||
const DrawerContent = React.forwardRef<
|
function DrawerContent({
|
||||||
React.ElementRef<typeof DrawerPrimitive.Content>,
|
className,
|
||||||
React.ComponentPropsWithoutRef<typeof DrawerPrimitive.Content>
|
children,
|
||||||
>(({ className, children, ...props }, ref) => (
|
...props
|
||||||
<DrawerPortal>
|
}: React.ComponentProps<typeof DrawerPrimitive.Content>) {
|
||||||
<DrawerOverlay />
|
return (
|
||||||
<DrawerPrimitive.Content
|
<DrawerPortal>
|
||||||
ref={ref}
|
<DrawerOverlay />
|
||||||
className={cn(
|
<DrawerPrimitive.Content
|
||||||
"fixed inset-x-0 bottom-0 z-50 mt-24 flex h-auto flex-col rounded-t-[10px] border bg-background",
|
className={cn(
|
||||||
className
|
"fixed inset-x-0 bottom-0 z-50 mt-24 flex h-auto flex-col rounded-t-[10px] border bg-background",
|
||||||
)}
|
className
|
||||||
{...props}
|
)}
|
||||||
>
|
{...props}
|
||||||
<div className="mx-auto mt-4 h-2 w-[100px] rounded-full bg-muted" />
|
>
|
||||||
{children}
|
<div className="mx-auto mt-4 h-2 w-[100px] rounded-full bg-muted" />
|
||||||
</DrawerPrimitive.Content>
|
{children}
|
||||||
</DrawerPortal>
|
</DrawerPrimitive.Content>
|
||||||
))
|
</DrawerPortal>
|
||||||
|
)
|
||||||
|
}
|
||||||
DrawerContent.displayName = "DrawerContent"
|
DrawerContent.displayName = "DrawerContent"
|
||||||
|
|
||||||
const DrawerHeader = ({
|
const DrawerHeader = ({
|
||||||
@@ -75,31 +78,33 @@ const DrawerFooter = ({
|
|||||||
)
|
)
|
||||||
DrawerFooter.displayName = "DrawerFooter"
|
DrawerFooter.displayName = "DrawerFooter"
|
||||||
|
|
||||||
const DrawerTitle = React.forwardRef<
|
function DrawerTitle({
|
||||||
React.ElementRef<typeof DrawerPrimitive.Title>,
|
className,
|
||||||
React.ComponentPropsWithoutRef<typeof DrawerPrimitive.Title>
|
...props
|
||||||
>(({ className, ...props }, ref) => (
|
}: React.ComponentProps<typeof DrawerPrimitive.Title>) {
|
||||||
<DrawerPrimitive.Title
|
return (
|
||||||
ref={ref}
|
<DrawerPrimitive.Title
|
||||||
className={cn(
|
className={cn(
|
||||||
"text-lg font-semibold leading-none tracking-tight",
|
"text-lg font-semibold leading-none tracking-tight",
|
||||||
className
|
className
|
||||||
)}
|
)}
|
||||||
{...props}
|
{...props}
|
||||||
/>
|
/>
|
||||||
))
|
)
|
||||||
|
}
|
||||||
DrawerTitle.displayName = DrawerPrimitive.Title.displayName
|
DrawerTitle.displayName = DrawerPrimitive.Title.displayName
|
||||||
|
|
||||||
const DrawerDescription = React.forwardRef<
|
function DrawerDescription({
|
||||||
React.ElementRef<typeof DrawerPrimitive.Description>,
|
className,
|
||||||
React.ComponentPropsWithoutRef<typeof DrawerPrimitive.Description>
|
...props
|
||||||
>(({ className, ...props }, ref) => (
|
}: React.ComponentProps<typeof DrawerPrimitive.Description>) {
|
||||||
<DrawerPrimitive.Description
|
return (
|
||||||
ref={ref}
|
<DrawerPrimitive.Description
|
||||||
className={cn("text-sm text-muted-foreground", className)}
|
className={cn("text-sm text-muted-foreground", className)}
|
||||||
{...props}
|
{...props}
|
||||||
/>
|
/>
|
||||||
))
|
)
|
||||||
|
}
|
||||||
DrawerDescription.displayName = DrawerPrimitive.Description.displayName
|
DrawerDescription.displayName = DrawerPrimitive.Description.displayName
|
||||||
|
|
||||||
export {
|
export {
|
||||||
|
|||||||
@@ -70,46 +70,43 @@ const FormItemContext = React.createContext<FormItemContextValue>(
|
|||||||
{} as FormItemContextValue
|
{} as FormItemContextValue
|
||||||
)
|
)
|
||||||
|
|
||||||
const FormItem = React.forwardRef<
|
function FormItem({
|
||||||
HTMLDivElement,
|
className,
|
||||||
React.HTMLAttributes<HTMLDivElement>
|
...props
|
||||||
>(({ className, ...props }, ref) => {
|
}: React.HTMLAttributes<HTMLDivElement>) {
|
||||||
const id = React.useId()
|
const id = React.useId()
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<FormItemContext.Provider value={{ id }}>
|
<FormItemContext.Provider value={{ id }}>
|
||||||
<div ref={ref} className={cn("space-y-2", className)} {...props} />
|
<div className={cn("space-y-2", className)} {...props} />
|
||||||
</FormItemContext.Provider>
|
</FormItemContext.Provider>
|
||||||
)
|
)
|
||||||
})
|
}
|
||||||
FormItem.displayName = "FormItem"
|
FormItem.displayName = "FormItem"
|
||||||
|
|
||||||
const FormLabel = React.forwardRef<
|
function FormLabel({
|
||||||
React.ElementRef<typeof LabelPrimitive.Root>,
|
className,
|
||||||
React.ComponentPropsWithoutRef<typeof LabelPrimitive.Root>
|
...props
|
||||||
>(({ className, ...props }, ref) => {
|
}: React.ComponentProps<typeof LabelPrimitive.Root>) {
|
||||||
const { error, formItemId } = useFormField()
|
const { error, formItemId } = useFormField()
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Label
|
<Label
|
||||||
ref={ref}
|
|
||||||
className={cn(error && "text-destructive", className)}
|
className={cn(error && "text-destructive", className)}
|
||||||
htmlFor={formItemId}
|
htmlFor={formItemId}
|
||||||
{...props}
|
{...props}
|
||||||
/>
|
/>
|
||||||
)
|
)
|
||||||
})
|
}
|
||||||
FormLabel.displayName = "FormLabel"
|
FormLabel.displayName = "FormLabel"
|
||||||
|
|
||||||
const FormControl = React.forwardRef<
|
function FormControl({
|
||||||
React.ElementRef<typeof Slot>,
|
...props
|
||||||
React.ComponentPropsWithoutRef<typeof Slot>
|
}: React.ComponentProps<typeof Slot>) {
|
||||||
>(({ ...props }, ref) => {
|
|
||||||
const { error, formItemId, formDescriptionId, formMessageId } = useFormField()
|
const { error, formItemId, formDescriptionId, formMessageId } = useFormField()
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Slot
|
<Slot
|
||||||
ref={ref}
|
|
||||||
id={formItemId}
|
id={formItemId}
|
||||||
aria-describedby={
|
aria-describedby={
|
||||||
!error
|
!error
|
||||||
@@ -120,30 +117,30 @@ const FormControl = React.forwardRef<
|
|||||||
{...props}
|
{...props}
|
||||||
/>
|
/>
|
||||||
)
|
)
|
||||||
})
|
}
|
||||||
FormControl.displayName = "FormControl"
|
FormControl.displayName = "FormControl"
|
||||||
|
|
||||||
const FormDescription = React.forwardRef<
|
function FormDescription({
|
||||||
HTMLParagraphElement,
|
className,
|
||||||
React.HTMLAttributes<HTMLParagraphElement>
|
...props
|
||||||
>(({ className, ...props }, ref) => {
|
}: React.HTMLAttributes<HTMLParagraphElement>) {
|
||||||
const { formDescriptionId } = useFormField()
|
const { formDescriptionId } = useFormField()
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<p
|
<p
|
||||||
ref={ref}
|
|
||||||
id={formDescriptionId}
|
id={formDescriptionId}
|
||||||
className={cn("text-sm text-muted-foreground", className)}
|
className={cn("text-sm text-muted-foreground", className)}
|
||||||
{...props}
|
{...props}
|
||||||
/>
|
/>
|
||||||
)
|
)
|
||||||
})
|
}
|
||||||
FormDescription.displayName = "FormDescription"
|
FormDescription.displayName = "FormDescription"
|
||||||
|
|
||||||
const FormMessage = React.forwardRef<
|
function FormMessage({
|
||||||
HTMLParagraphElement,
|
className,
|
||||||
React.HTMLAttributes<HTMLParagraphElement>
|
children,
|
||||||
>(({ className, children, ...props }, ref) => {
|
...props
|
||||||
|
}: React.HTMLAttributes<HTMLParagraphElement>) {
|
||||||
const { error, formMessageId } = useFormField()
|
const { error, formMessageId } = useFormField()
|
||||||
const body = error ? String(error?.message) : children
|
const body = error ? String(error?.message) : children
|
||||||
|
|
||||||
@@ -153,7 +150,6 @@ const FormMessage = React.forwardRef<
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<p
|
<p
|
||||||
ref={ref}
|
|
||||||
id={formMessageId}
|
id={formMessageId}
|
||||||
className={cn("text-sm font-medium text-destructive", className)}
|
className={cn("text-sm font-medium text-destructive", className)}
|
||||||
{...props}
|
{...props}
|
||||||
@@ -161,7 +157,7 @@ const FormMessage = React.forwardRef<
|
|||||||
{body}
|
{body}
|
||||||
</p>
|
</p>
|
||||||
)
|
)
|
||||||
})
|
}
|
||||||
FormMessage.displayName = "FormMessage"
|
FormMessage.displayName = "FormMessage"
|
||||||
|
|
||||||
export {
|
export {
|
||||||
|
|||||||
@@ -5,21 +5,22 @@ import { cn } from "@/lib/utils"
|
|||||||
export interface InputProps
|
export interface InputProps
|
||||||
extends React.InputHTMLAttributes<HTMLInputElement> {}
|
extends React.InputHTMLAttributes<HTMLInputElement> {}
|
||||||
|
|
||||||
const Input = React.forwardRef<HTMLInputElement, InputProps>(
|
function Input({
|
||||||
({ className, type, ...props }, ref) => {
|
className,
|
||||||
return (
|
type,
|
||||||
<input
|
...props
|
||||||
type={type}
|
}: InputProps) {
|
||||||
className={cn(
|
return (
|
||||||
"flex h-10 w-full rounded-md border border-input bg-background px-3 py-2 text-sm ring-offset-background file:border-0 file:bg-transparent file:text-sm file:font-medium placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50",
|
<input
|
||||||
className
|
type={type}
|
||||||
)}
|
className={cn(
|
||||||
ref={ref}
|
"flex h-10 w-full rounded-md border border-input bg-background px-3 py-2 text-sm ring-offset-background file:border-0 file:bg-transparent file:text-sm file:font-medium placeholder:text-muted-foreground focus-visible:outline-hidden focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50",
|
||||||
{...props}
|
className
|
||||||
/>
|
)}
|
||||||
)
|
{...props}
|
||||||
}
|
/>
|
||||||
)
|
)
|
||||||
|
}
|
||||||
Input.displayName = "Input"
|
Input.displayName = "Input"
|
||||||
|
|
||||||
export { Input }
|
export { Input }
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import * as React from "react"
|
import * as React from "react"
|
||||||
import * as LabelPrimitive from "@radix-ui/react-label"
|
import * as LabelPrimitive from "@radix-ui/react-label"
|
||||||
import { cva, type VariantProps } from "class-variance-authority"
|
import { cva } from "class-variance-authority"
|
||||||
|
|
||||||
import { cn } from "@/lib/utils"
|
import { cn } from "@/lib/utils"
|
||||||
|
|
||||||
@@ -8,17 +8,17 @@ const labelVariants = cva(
|
|||||||
"text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70"
|
"text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70"
|
||||||
)
|
)
|
||||||
|
|
||||||
const Label = React.forwardRef<
|
function Label({
|
||||||
React.ElementRef<typeof LabelPrimitive.Root>,
|
className,
|
||||||
React.ComponentPropsWithoutRef<typeof LabelPrimitive.Root> &
|
...props
|
||||||
VariantProps<typeof labelVariants>
|
}: React.ComponentProps<typeof LabelPrimitive.Root>) {
|
||||||
>(({ className, ...props }, ref) => (
|
return (
|
||||||
<LabelPrimitive.Root
|
<LabelPrimitive.Root
|
||||||
ref={ref}
|
className={cn(labelVariants(), className)}
|
||||||
className={cn(labelVariants(), className)}
|
{...props}
|
||||||
{...props}
|
/>
|
||||||
/>
|
)
|
||||||
))
|
}
|
||||||
Label.displayName = LabelPrimitive.Root.displayName
|
Label.displayName = LabelPrimitive.Root.displayName
|
||||||
|
|
||||||
export { Label }
|
export { Label }
|
||||||
|
|||||||
@@ -3,27 +3,25 @@ import * as SeparatorPrimitive from "@radix-ui/react-separator"
|
|||||||
|
|
||||||
import { cn } from "@/lib/utils"
|
import { cn } from "@/lib/utils"
|
||||||
|
|
||||||
const Separator = React.forwardRef<
|
function Separator({
|
||||||
React.ElementRef<typeof SeparatorPrimitive.Root>,
|
className,
|
||||||
React.ComponentPropsWithoutRef<typeof SeparatorPrimitive.Root>
|
orientation = "horizontal",
|
||||||
>(
|
decorative = true,
|
||||||
(
|
...props
|
||||||
{ className, orientation = "horizontal", decorative = true, ...props },
|
}: React.ComponentProps<typeof SeparatorPrimitive.Root>) {
|
||||||
ref
|
return (
|
||||||
) => (
|
|
||||||
<SeparatorPrimitive.Root
|
<SeparatorPrimitive.Root
|
||||||
ref={ref}
|
|
||||||
decorative={decorative}
|
decorative={decorative}
|
||||||
orientation={orientation}
|
orientation={orientation}
|
||||||
className={cn(
|
className={cn(
|
||||||
"shrink-0 bg-border",
|
"shrink-0 bg-border",
|
||||||
orientation === "horizontal" ? "h-[1px] w-full" : "h-full w-[1px]",
|
orientation === "horizontal" ? "h-px w-full" : "h-full w-px",
|
||||||
className
|
className
|
||||||
)}
|
)}
|
||||||
{...props}
|
{...props}
|
||||||
/>
|
/>
|
||||||
)
|
)
|
||||||
)
|
}
|
||||||
Separator.displayName = SeparatorPrimitive.Root.displayName
|
Separator.displayName = SeparatorPrimitive.Root.displayName
|
||||||
|
|
||||||
export { Separator }
|
export { Separator }
|
||||||
|
|||||||
@@ -7,23 +7,24 @@ import { cn } from "@/lib/utils"
|
|||||||
|
|
||||||
const ToastProvider = ToastPrimitives.Provider
|
const ToastProvider = ToastPrimitives.Provider
|
||||||
|
|
||||||
const ToastViewport = React.forwardRef<
|
function ToastViewport({
|
||||||
React.ElementRef<typeof ToastPrimitives.Viewport>,
|
className,
|
||||||
React.ComponentPropsWithoutRef<typeof ToastPrimitives.Viewport>
|
...props
|
||||||
>(({ className, ...props }, ref) => (
|
}: React.ComponentProps<typeof ToastPrimitives.Viewport>) {
|
||||||
<ToastPrimitives.Viewport
|
return (
|
||||||
ref={ref}
|
<ToastPrimitives.Viewport
|
||||||
className={cn(
|
className={cn(
|
||||||
"fixed top-0 z-[100] flex max-h-screen w-full flex-col-reverse p-4 sm:bottom-0 sm:right-0 sm:top-auto sm:flex-col md:max-w-[420px]",
|
"fixed top-0 z-100 flex max-h-screen w-full flex-col-reverse p-4 sm:bottom-0 sm:right-0 sm:top-auto sm:flex-col md:max-w-[420px]",
|
||||||
className
|
className
|
||||||
)}
|
)}
|
||||||
{...props}
|
{...props}
|
||||||
/>
|
/>
|
||||||
))
|
)
|
||||||
|
}
|
||||||
ToastViewport.displayName = ToastPrimitives.Viewport.displayName
|
ToastViewport.displayName = ToastPrimitives.Viewport.displayName
|
||||||
|
|
||||||
const toastVariants = cva(
|
const toastVariants = cva(
|
||||||
"group pointer-events-auto relative flex w-full items-center justify-between space-x-4 overflow-hidden rounded-md border p-6 pr-8 shadow-lg transition-all data-[swipe=cancel]:translate-x-0 data-[swipe=end]:translate-x-[var(--radix-toast-swipe-end-x)] data-[swipe=move]:translate-x-[var(--radix-toast-swipe-move-x)] data-[swipe=move]:transition-none data-[state=open]:animate-in data-[state=closed]:animate-out data-[swipe=end]:animate-out data-[state=closed]:fade-out-80 data-[state=closed]:slide-out-to-right-full data-[state=open]:slide-in-from-top-full data-[state=open]:sm:slide-in-from-bottom-full",
|
"group pointer-events-auto relative flex w-full items-center justify-between space-x-4 overflow-hidden rounded-md border p-6 pr-8 shadow-lg transition-all data-[swipe=cancel]:translate-x-0 data-[swipe=end]:translate-x-(--radix-toast-swipe-end-x) data-[swipe=move]:translate-x-(--radix-toast-swipe-move-x) data-[swipe=move]:transition-none data-[state=open]:animate-in data-[state=closed]:animate-out data-[swipe=end]:animate-out data-[state=closed]:fade-out-80 data-[state=closed]:slide-out-to-right-full data-[state=open]:slide-in-from-top-full data-[state=open]:sm:slide-in-from-bottom-full",
|
||||||
{
|
{
|
||||||
variants: {
|
variants: {
|
||||||
variant: {
|
variant: {
|
||||||
@@ -38,76 +39,79 @@ const toastVariants = cva(
|
|||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
const Toast = React.forwardRef<
|
function Toast({
|
||||||
React.ElementRef<typeof ToastPrimitives.Root>,
|
className,
|
||||||
React.ComponentPropsWithoutRef<typeof ToastPrimitives.Root> &
|
variant,
|
||||||
VariantProps<typeof toastVariants>
|
...props
|
||||||
>(({ className, variant, ...props }, ref) => {
|
}: React.ComponentProps<typeof ToastPrimitives.Root> & VariantProps<typeof toastVariants>) {
|
||||||
return (
|
return (
|
||||||
<ToastPrimitives.Root
|
<ToastPrimitives.Root
|
||||||
ref={ref}
|
|
||||||
className={cn(toastVariants({ variant }), className)}
|
className={cn(toastVariants({ variant }), className)}
|
||||||
{...props}
|
{...props}
|
||||||
/>
|
/>
|
||||||
)
|
)
|
||||||
})
|
}
|
||||||
Toast.displayName = ToastPrimitives.Root.displayName
|
Toast.displayName = ToastPrimitives.Root.displayName
|
||||||
|
|
||||||
const ToastAction = React.forwardRef<
|
function ToastAction({
|
||||||
React.ElementRef<typeof ToastPrimitives.Action>,
|
className,
|
||||||
React.ComponentPropsWithoutRef<typeof ToastPrimitives.Action>
|
...props
|
||||||
>(({ className, ...props }, ref) => (
|
}: React.ComponentProps<typeof ToastPrimitives.Action>) {
|
||||||
<ToastPrimitives.Action
|
return (
|
||||||
ref={ref}
|
<ToastPrimitives.Action
|
||||||
className={cn(
|
className={cn(
|
||||||
"inline-flex h-8 shrink-0 items-center justify-center rounded-md border bg-transparent px-3 text-sm font-medium ring-offset-background transition-colors hover:bg-secondary focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2 disabled:pointer-events-none disabled:opacity-50 group-[.destructive]:border-muted/40 group-[.destructive]:hover:border-destructive/30 group-[.destructive]:hover:bg-destructive group-[.destructive]:hover:text-destructive-foreground group-[.destructive]:focus:ring-destructive",
|
"inline-flex h-8 shrink-0 items-center justify-center rounded-md border bg-transparent px-3 text-sm font-medium ring-offset-background transition-colors hover:bg-secondary focus:outline-hidden focus:ring-2 focus:ring-ring focus:ring-offset-2 disabled:pointer-events-none disabled:opacity-50 group-[.destructive]:border-muted/40 hover:group-[.destructive]:border-destructive/30 hover:group-[.destructive]:bg-destructive hover:group-[.destructive]:text-destructive-foreground focus:group-[.destructive]:ring-destructive",
|
||||||
className
|
className
|
||||||
)}
|
)}
|
||||||
{...props}
|
{...props}
|
||||||
/>
|
/>
|
||||||
))
|
)
|
||||||
|
}
|
||||||
ToastAction.displayName = ToastPrimitives.Action.displayName
|
ToastAction.displayName = ToastPrimitives.Action.displayName
|
||||||
|
|
||||||
const ToastClose = React.forwardRef<
|
function ToastClose({
|
||||||
React.ElementRef<typeof ToastPrimitives.Close>,
|
className,
|
||||||
React.ComponentPropsWithoutRef<typeof ToastPrimitives.Close>
|
...props
|
||||||
>(({ className, ...props }, ref) => (
|
}: React.ComponentProps<typeof ToastPrimitives.Close>) {
|
||||||
<ToastPrimitives.Close
|
return (
|
||||||
ref={ref}
|
<ToastPrimitives.Close
|
||||||
className={cn(
|
className={cn(
|
||||||
"absolute right-2 top-2 rounded-md p-1 text-foreground/50 opacity-0 transition-opacity hover:text-foreground focus:opacity-100 focus:outline-none focus:ring-2 group-hover:opacity-100 group-[.destructive]:text-red-300 group-[.destructive]:hover:text-red-50 group-[.destructive]:focus:ring-red-400 group-[.destructive]:focus:ring-offset-red-600",
|
"absolute right-2 top-2 rounded-md p-1 text-foreground/50 opacity-0 transition-opacity hover:text-foreground focus:opacity-100 focus:outline-hidden focus:ring-2 group-hover:opacity-100 group-[.destructive]:text-red-300 hover:group-[.destructive]:text-red-50 focus:group-[.destructive]:ring-red-400 focus:group-[.destructive]:ring-offset-red-600",
|
||||||
className
|
className
|
||||||
)}
|
)}
|
||||||
toast-close=""
|
toast-close=""
|
||||||
{...props}
|
{...props}
|
||||||
>
|
>
|
||||||
<X className="h-4 w-4" />
|
<X className="h-4 w-4" />
|
||||||
</ToastPrimitives.Close>
|
</ToastPrimitives.Close>
|
||||||
))
|
)
|
||||||
|
}
|
||||||
ToastClose.displayName = ToastPrimitives.Close.displayName
|
ToastClose.displayName = ToastPrimitives.Close.displayName
|
||||||
|
|
||||||
const ToastTitle = React.forwardRef<
|
function ToastTitle({
|
||||||
React.ElementRef<typeof ToastPrimitives.Title>,
|
className,
|
||||||
React.ComponentPropsWithoutRef<typeof ToastPrimitives.Title>
|
...props
|
||||||
>(({ className, ...props }, ref) => (
|
}: React.ComponentProps<typeof ToastPrimitives.Title>) {
|
||||||
<ToastPrimitives.Title
|
return (
|
||||||
ref={ref}
|
<ToastPrimitives.Title
|
||||||
className={cn("text-sm font-semibold", className)}
|
className={cn("text-sm font-semibold", className)}
|
||||||
{...props}
|
{...props}
|
||||||
/>
|
/>
|
||||||
))
|
)
|
||||||
|
}
|
||||||
ToastTitle.displayName = ToastPrimitives.Title.displayName
|
ToastTitle.displayName = ToastPrimitives.Title.displayName
|
||||||
|
|
||||||
const ToastDescription = React.forwardRef<
|
function ToastDescription({
|
||||||
React.ElementRef<typeof ToastPrimitives.Description>,
|
className,
|
||||||
React.ComponentPropsWithoutRef<typeof ToastPrimitives.Description>
|
...props
|
||||||
>(({ className, ...props }, ref) => (
|
}: React.ComponentProps<typeof ToastPrimitives.Description>) {
|
||||||
<ToastPrimitives.Description
|
return (
|
||||||
ref={ref}
|
<ToastPrimitives.Description
|
||||||
className={cn("text-sm opacity-90", className)}
|
className={cn("text-sm opacity-90", className)}
|
||||||
{...props}
|
{...props}
|
||||||
/>
|
/>
|
||||||
))
|
)
|
||||||
|
}
|
||||||
ToastDescription.displayName = ToastPrimitives.Description.displayName
|
ToastDescription.displayName = ToastPrimitives.Description.displayName
|
||||||
|
|
||||||
type ToastProps = React.ComponentPropsWithoutRef<typeof Toast>
|
type ToastProps = React.ComponentPropsWithoutRef<typeof Toast>
|
||||||
|
|||||||
@@ -9,20 +9,22 @@ const Tooltip = TooltipPrimitive.Root
|
|||||||
|
|
||||||
const TooltipTrigger = TooltipPrimitive.Trigger
|
const TooltipTrigger = TooltipPrimitive.Trigger
|
||||||
|
|
||||||
const TooltipContent = React.forwardRef<
|
function TooltipContent({
|
||||||
React.ElementRef<typeof TooltipPrimitive.Content>,
|
className,
|
||||||
React.ComponentPropsWithoutRef<typeof TooltipPrimitive.Content>
|
sideOffset = 4,
|
||||||
>(({ className, sideOffset = 4, ...props }, ref) => (
|
...props
|
||||||
<TooltipPrimitive.Content
|
}: React.ComponentProps<typeof TooltipPrimitive.Content>) {
|
||||||
ref={ref}
|
return (
|
||||||
sideOffset={sideOffset}
|
<TooltipPrimitive.Content
|
||||||
className={cn(
|
sideOffset={sideOffset}
|
||||||
"z-50 overflow-hidden rounded-md border bg-popover px-3 py-1.5 text-sm text-popover-foreground shadow-md animate-in fade-in-0 zoom-in-95 data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=closed]:zoom-out-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2",
|
className={cn(
|
||||||
className
|
"z-50 overflow-hidden rounded-md border bg-popover px-3 py-1.5 text-sm text-popover-foreground shadow-md animate-in fade-in-0 zoom-in-95 data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=closed]:zoom-out-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2",
|
||||||
)}
|
className
|
||||||
{...props}
|
)}
|
||||||
/>
|
{...props}
|
||||||
))
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
TooltipContent.displayName = TooltipPrimitive.Content.displayName
|
TooltipContent.displayName = TooltipPrimitive.Content.displayName
|
||||||
|
|
||||||
export { Tooltip, TooltipTrigger, TooltipContent, TooltipProvider }
|
export { Tooltip, TooltipTrigger, TooltipContent, TooltipProvider }
|
||||||
|
|||||||
+136
-55
@@ -1,62 +1,143 @@
|
|||||||
@tailwind base;
|
@import 'tailwindcss';
|
||||||
@tailwind components;
|
@import 'tw-animate-css';
|
||||||
@tailwind utilities;
|
|
||||||
|
|
||||||
|
@custom-variant dark (&:is(.dark *));
|
||||||
|
|
||||||
|
@utility container {
|
||||||
|
margin-inline: auto;
|
||||||
|
padding-inline: 2rem;
|
||||||
|
@media (width >= --theme(--breakpoint-sm)) {
|
||||||
|
max-width: none;
|
||||||
|
}
|
||||||
|
@media (width >= 1400px) {
|
||||||
|
max-width: 1400px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@theme inline {
|
||||||
|
--color-border: var(--border);
|
||||||
|
--color-input: var(--input);
|
||||||
|
--color-ring: var(--ring);
|
||||||
|
--color-background: var(--background);
|
||||||
|
--color-foreground: var(--foreground);
|
||||||
|
|
||||||
|
--color-primary: var(--primary);
|
||||||
|
--color-primary-foreground: var(--primary-foreground);
|
||||||
|
|
||||||
|
--color-secondary: var(--secondary);
|
||||||
|
--color-secondary-foreground: var(--secondary-foreground);
|
||||||
|
|
||||||
|
--color-destructive: var(--destructive);
|
||||||
|
--color-destructive-foreground: var(--destructive-foreground);
|
||||||
|
|
||||||
|
--color-muted: var(--muted);
|
||||||
|
--color-muted-foreground: var(--muted-foreground);
|
||||||
|
|
||||||
|
--color-accent: var(--accent);
|
||||||
|
--color-accent-foreground: var(--accent-foreground);
|
||||||
|
|
||||||
|
--color-popover: var(--popover);
|
||||||
|
--color-popover-foreground: var(--popover-foreground);
|
||||||
|
|
||||||
|
--color-card: var(--card);
|
||||||
|
--color-card-foreground: var(--card-foreground);
|
||||||
|
|
||||||
|
--radius-lg: var(--radius);
|
||||||
|
--radius-md: calc(var(--radius) - 2px);
|
||||||
|
--radius-sm: calc(var(--radius) - 4px);
|
||||||
|
|
||||||
|
--animate-accordion-down: accordion-down 0.2s ease-out;
|
||||||
|
--animate-accordion-up: accordion-up 0.2s ease-out;
|
||||||
|
|
||||||
|
@keyframes accordion-down {
|
||||||
|
from {
|
||||||
|
height: 0;
|
||||||
|
}
|
||||||
|
to {
|
||||||
|
height: var(--radix-accordion-content-height);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@keyframes accordion-up {
|
||||||
|
from {
|
||||||
|
height: var(--radix-accordion-content-height);
|
||||||
|
}
|
||||||
|
to {
|
||||||
|
height: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
The default border color has changed to `currentcolor` in Tailwind CSS v4,
|
||||||
|
so we've added these compatibility styles to make sure everything still
|
||||||
|
looks the same as it did with Tailwind CSS v3.
|
||||||
|
|
||||||
|
If we ever want to remove these styles, we need to add an explicit border
|
||||||
|
color utility to any element that depends on these defaults.
|
||||||
|
*/
|
||||||
@layer base {
|
@layer base {
|
||||||
:root {
|
*,
|
||||||
--background: 0 0% 100%;
|
::after,
|
||||||
--foreground: 222.2 84% 4.9%;
|
::before,
|
||||||
--card: 0 0% 100%;
|
::backdrop,
|
||||||
--card-foreground: 222.2 84% 4.9%;
|
::file-selector-button {
|
||||||
--popover: 0 0% 100%;
|
border-color: var(--color-gray-200, currentcolor);
|
||||||
--popover-foreground: 222.2 84% 4.9%;
|
|
||||||
--primary: 222.2 47.4% 11.2%;
|
|
||||||
--primary-foreground: 210 40% 98%;
|
|
||||||
--secondary: 210 40% 96.1%;
|
|
||||||
--secondary-foreground: 222.2 47.4% 11.2%;
|
|
||||||
--muted: 210 40% 96.1%;
|
|
||||||
--muted-foreground: 215.4 16.3% 46.9%;
|
|
||||||
--accent: 210 40% 96.1%;
|
|
||||||
--accent-foreground: 222.2 47.4% 11.2%;
|
|
||||||
--destructive: 0 84.2% 60.2%;
|
|
||||||
--destructive-foreground: 210 40% 98%;
|
|
||||||
--border: 214.3 31.8% 91.4%;
|
|
||||||
--input: 214.3 31.8% 91.4%;
|
|
||||||
--ring: 222.2 84% 4.9%;
|
|
||||||
--radius: 0.5rem;
|
|
||||||
--chart-1: 12 76% 61%;
|
|
||||||
--chart-2: 173 58% 39%;
|
|
||||||
--chart-3: 197 37% 24%;
|
|
||||||
--chart-4: 43 74% 66%;
|
|
||||||
--chart-5: 27 87% 67%;
|
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
.dark {
|
:root {
|
||||||
--background: 222.2 84% 4.9%;
|
--background: hsl(0 0% 100%);
|
||||||
--foreground: 210 40% 98%;
|
--foreground: hsl(222.2 84% 4.9%);
|
||||||
--card: 222.2 84% 4.9%;
|
--card: hsl(0 0% 100%);
|
||||||
--card-foreground: 210 40% 98%;
|
--card-foreground: hsl(222.2 84% 4.9%);
|
||||||
--popover: 222.2 84% 4.9%;
|
--popover: hsl(0 0% 100%);
|
||||||
--popover-foreground: 210 40% 98%;
|
--popover-foreground: hsl(222.2 84% 4.9%);
|
||||||
--primary: 210 40% 98%;
|
--primary: hsl(222.2 47.4% 11.2%);
|
||||||
--primary-foreground: 222.2 47.4% 11.2%;
|
--primary-foreground: hsl(210 40% 98%);
|
||||||
--secondary: 217.2 32.6% 17.5%;
|
--secondary: hsl(210 40% 96.1%);
|
||||||
--secondary-foreground: 210 40% 98%;
|
--secondary-foreground: hsl(222.2 47.4% 11.2%);
|
||||||
--muted: 217.2 32.6% 17.5%;
|
--muted: hsl(210 40% 96.1%);
|
||||||
--muted-foreground: 215 20.2% 65.1%;
|
--muted-foreground: hsl(215.4 16.3% 46.9%);
|
||||||
--accent: 217.2 32.6% 17.5%;
|
--accent: hsl(210 40% 96.1%);
|
||||||
--accent-foreground: 210 40% 98%;
|
--accent-foreground: hsl(222.2 47.4% 11.2%);
|
||||||
--destructive: 0 62.8% 30.6%;
|
--destructive: hsl(0 84.2% 60.2%);
|
||||||
--destructive-foreground: 210 40% 98%;
|
--destructive-foreground: hsl(210 40% 98%);
|
||||||
--border: 217.2 32.6% 17.5%;
|
--border: hsl(214.3 31.8% 91.4%);
|
||||||
--input: 217.2 32.6% 17.5%;
|
--input: hsl(214.3 31.8% 91.4%);
|
||||||
--ring: 212.7 26.8% 83.9%;
|
--ring: hsl(222.2 84% 4.9%);
|
||||||
--chart-1: 220 70% 50%;
|
--radius: 0.5rem;
|
||||||
--chart-2: 160 60% 45%;
|
--chart-1: hsl(12 76% 61%);
|
||||||
--chart-3: 30 80% 55%;
|
--chart-2: hsl(173 58% 39%);
|
||||||
--chart-4: 280 65% 60%;
|
--chart-3: hsl(197 37% 24%);
|
||||||
--chart-5: 340 75% 55%;
|
--chart-4: hsl(43 74% 66%);
|
||||||
}
|
--chart-5: hsl(27 87% 67%);
|
||||||
|
}
|
||||||
|
|
||||||
|
.dark {
|
||||||
|
--background: hsl(222.2 84% 4.9%);
|
||||||
|
--foreground: hsl(210 40% 98%);
|
||||||
|
--card: hsl(222.2 84% 4.9%);
|
||||||
|
--card-foreground: hsl(210 40% 98%);
|
||||||
|
--popover: hsl(222.2 84% 4.9%);
|
||||||
|
--popover-foreground: hsl(210 40% 98%);
|
||||||
|
--primary: hsl(210 40% 98%);
|
||||||
|
--primary-foreground: hsl(222.2 47.4% 11.2%);
|
||||||
|
--secondary: hsl(217.2 32.6% 17.5%);
|
||||||
|
--secondary-foreground: hsl(210 40% 98%);
|
||||||
|
--muted: hsl(217.2 32.6% 17.5%);
|
||||||
|
--muted-foreground: hsl(215 20.2% 65.1%);
|
||||||
|
--accent: hsl(217.2 32.6% 17.5%);
|
||||||
|
--accent-foreground: hsl(210 40% 98%);
|
||||||
|
--destructive: hsl(0 62.8% 30.6%);
|
||||||
|
--destructive-foreground: hsl(210 40% 98%);
|
||||||
|
--border: hsl(217.2 32.6% 17.5%);
|
||||||
|
--input: hsl(217.2 32.6% 17.5%);
|
||||||
|
--ring: hsl(212.7 26.8% 83.9%);
|
||||||
|
--chart-1: hsl(220 70% 50%);
|
||||||
|
--chart-2: hsl(160 60% 45%);
|
||||||
|
--chart-3: hsl(30 80% 55%);
|
||||||
|
--chart-4: hsl(280 65% 60%);
|
||||||
|
--chart-5: hsl(340 75% 55%);
|
||||||
}
|
}
|
||||||
|
|
||||||
@layer base {
|
@layer base {
|
||||||
|
|||||||
@@ -46,11 +46,17 @@ const router = createHashRouter([
|
|||||||
}
|
}
|
||||||
])
|
])
|
||||||
|
|
||||||
createRoot(document.getElementById('root')!).render(
|
function App() {
|
||||||
<StrictMode>
|
return (
|
||||||
<ThemeProvider defaultTheme="system" storageKey="vite-ui-theme">
|
<ThemeProvider defaultTheme="system" storageKey="vite-ui-theme">
|
||||||
<RouterProvider router={router}/>
|
<RouterProvider router={router}/>
|
||||||
<Toaster/>
|
<Toaster/>
|
||||||
</ThemeProvider>
|
</ThemeProvider>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
createRoot(document.getElementById('root')!).render(
|
||||||
|
<StrictMode>
|
||||||
|
<App/>
|
||||||
</StrictMode>,
|
</StrictMode>,
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -66,7 +66,7 @@ function CardList(props: { cards: ReadonlyArray<AimeCard> | null, cardSelected:
|
|||||||
return (<>
|
return (<>
|
||||||
<h4 className="text-2xl font-bold">{props.cards?.length ? "Cards" : "Additional Cards"}</h4>
|
<h4 className="text-2xl font-bold">{props.cards?.length ? "Cards" : "Additional Cards"}</h4>
|
||||||
<div className="flex gap-2">
|
<div className="flex gap-2">
|
||||||
<CardCreationDialog className="flex-grow"/>
|
<CardCreationDialog className="grow"/>
|
||||||
<Button onClick={() => navigate('/cards')} variant="outline" size="icon">
|
<Button onClick={() => navigate('/cards')} variant="outline" size="icon">
|
||||||
<Wallet className="h-4 w-4"/>
|
<Wallet className="h-4 w-4"/>
|
||||||
</Button>
|
</Button>
|
||||||
@@ -187,7 +187,7 @@ function ServerCardInPage() {
|
|||||||
<AlertPanel server={server} connectionError={connectionError} onReloadRequested={() => setServerReloadCounter(serverReloadCounter + 1)}/>
|
<AlertPanel server={server} connectionError={connectionError} onReloadRequested={() => setServerReloadCounter(serverReloadCounter + 1)}/>
|
||||||
|
|
||||||
{server && (
|
{server && (
|
||||||
<div className="flex flex-col gap-4 w-full flex-grow">
|
<div className="flex flex-col gap-4 w-full grow">
|
||||||
{selectedCard && (<>
|
{selectedCard && (<>
|
||||||
<h4 className="text-2xl font-bold">{gameName ? `Play ${gameName}` : "Selected Card"}</h4>
|
<h4 className="text-2xl font-bold">{gameName ? `Play ${gameName}` : "Selected Card"}</h4>
|
||||||
|
|
||||||
@@ -197,7 +197,7 @@ function ServerCardInPage() {
|
|||||||
</Button>
|
</Button>
|
||||||
</AimeCardEntry>
|
</AimeCardEntry>
|
||||||
|
|
||||||
<Button className="flex-grow-0 bg-green-600 mb-5"
|
<Button className="grow-0 bg-green-600 mb-5"
|
||||||
disabled={(cardInStatus || connectionError || !server)}
|
disabled={(cardInStatus || connectionError || !server)}
|
||||||
onClick={() => performCardInAction(selectedCard, server as AimeServer, setCardInStatus, toast)}>
|
onClick={() => performCardInAction(selectedCard, server as AimeServer, setCardInStatus, toast)}>
|
||||||
{cardInStatus ? "Carding in..." : <>Card In <Nfc className="ml-2 h-4 w-4"/></>}
|
{cardInStatus ? "Carding in..." : <>Card In <Nfc className="ml-2 h-4 w-4"/></>}
|
||||||
|
|||||||
@@ -53,7 +53,7 @@ function CardManagerPage() {
|
|||||||
</Button>
|
</Button>
|
||||||
</AMHeader>
|
</AMHeader>
|
||||||
|
|
||||||
<div className="flex flex-col gap-4 w-full flex-grow">
|
<div className="flex flex-col gap-4 w-full grow">
|
||||||
{cards ? <CardList cards={cards}/> : <LoadingIndicator/>}
|
{cards ? <CardList cards={cards}/> : <LoadingIndicator/>}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
@@ -19,7 +19,7 @@ function PrivacyPolicy() {
|
|||||||
</Button>
|
</Button>
|
||||||
</AMHeader>
|
</AMHeader>
|
||||||
|
|
||||||
<div className="flex flex-col gap-4 w-full flex-grow">
|
<div className="flex flex-col gap-4 w-full grow">
|
||||||
<h4 className="font-semibold text-2xl">Privacy</h4>
|
<h4 className="font-semibold text-2xl">Privacy</h4>
|
||||||
<p>The AMNet card switcher collects and stores the following information locally (on-device):</p>
|
<p>The AMNet card switcher collects and stores the following information locally (on-device):</p>
|
||||||
<ul className="list-disc">
|
<ul className="list-disc">
|
||||||
|
|||||||
@@ -43,7 +43,7 @@ function ServerListing() {
|
|||||||
</Button>
|
</Button>
|
||||||
</AMHeader>
|
</AMHeader>
|
||||||
|
|
||||||
<div className="flex flex-col gap-4 w-full flex-grow">
|
<div className="flex flex-col gap-4 w-full grow">
|
||||||
<ServerList servers={servers}/>
|
<ServerList servers={servers}/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -172,7 +172,7 @@ function ServerAddForm() {
|
|||||||
control={form.control}
|
control={form.control}
|
||||||
name="address"
|
name="address"
|
||||||
render={({field}) => (
|
render={({field}) => (
|
||||||
<FormItem className="flex-grow">
|
<FormItem className="grow">
|
||||||
<FormControl>
|
<FormControl>
|
||||||
<Input {...field}
|
<Input {...field}
|
||||||
placeholder="Server Address (http://192.168.1.1:6070)"
|
placeholder="Server Address (http://192.168.1.1:6070)"
|
||||||
@@ -183,7 +183,7 @@ function ServerAddForm() {
|
|||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
<Button type="submit" disabled={blockSubmit} variant="outline" size="icon"
|
<Button type="submit" disabled={blockSubmit} variant="outline" size="icon"
|
||||||
className="flex-shrink-0">
|
className="shrink-0">
|
||||||
<Plus className="h-4 w-4"/>
|
<Plus className="h-4 w-4"/>
|
||||||
</Button>
|
</Button>
|
||||||
</form>
|
</form>
|
||||||
|
|||||||
@@ -1,76 +0,0 @@
|
|||||||
import * as animate from "tailwindcss-animate";
|
|
||||||
|
|
||||||
/** @type {import('tailwindcss').Config} */
|
|
||||||
export default {
|
|
||||||
darkMode: ["class"],
|
|
||||||
content: [
|
|
||||||
'./src/**/*.{ts,tsx}',
|
|
||||||
],
|
|
||||||
prefix: "",
|
|
||||||
theme: {
|
|
||||||
container: {
|
|
||||||
center: true,
|
|
||||||
padding: "2rem",
|
|
||||||
screens: {
|
|
||||||
"2xl": "1400px",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
extend: {
|
|
||||||
colors: {
|
|
||||||
border: "hsl(var(--border))",
|
|
||||||
input: "hsl(var(--input))",
|
|
||||||
ring: "hsl(var(--ring))",
|
|
||||||
background: "hsl(var(--background))",
|
|
||||||
foreground: "hsl(var(--foreground))",
|
|
||||||
primary: {
|
|
||||||
DEFAULT: "hsl(var(--primary))",
|
|
||||||
foreground: "hsl(var(--primary-foreground))",
|
|
||||||
},
|
|
||||||
secondary: {
|
|
||||||
DEFAULT: "hsl(var(--secondary))",
|
|
||||||
foreground: "hsl(var(--secondary-foreground))",
|
|
||||||
},
|
|
||||||
destructive: {
|
|
||||||
DEFAULT: "hsl(var(--destructive))",
|
|
||||||
foreground: "hsl(var(--destructive-foreground))",
|
|
||||||
},
|
|
||||||
muted: {
|
|
||||||
DEFAULT: "hsl(var(--muted))",
|
|
||||||
foreground: "hsl(var(--muted-foreground))",
|
|
||||||
},
|
|
||||||
accent: {
|
|
||||||
DEFAULT: "hsl(var(--accent))",
|
|
||||||
foreground: "hsl(var(--accent-foreground))",
|
|
||||||
},
|
|
||||||
popover: {
|
|
||||||
DEFAULT: "hsl(var(--popover))",
|
|
||||||
foreground: "hsl(var(--popover-foreground))",
|
|
||||||
},
|
|
||||||
card: {
|
|
||||||
DEFAULT: "hsl(var(--card))",
|
|
||||||
foreground: "hsl(var(--card-foreground))",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
borderRadius: {
|
|
||||||
lg: "var(--radius)",
|
|
||||||
md: "calc(var(--radius) - 2px)",
|
|
||||||
sm: "calc(var(--radius) - 4px)",
|
|
||||||
},
|
|
||||||
keyframes: {
|
|
||||||
"accordion-down": {
|
|
||||||
from: { height: "0" },
|
|
||||||
to: { height: "var(--radix-accordion-content-height)" },
|
|
||||||
},
|
|
||||||
"accordion-up": {
|
|
||||||
from: { height: "var(--radix-accordion-content-height)" },
|
|
||||||
to: { height: "0" },
|
|
||||||
},
|
|
||||||
},
|
|
||||||
animation: {
|
|
||||||
"accordion-down": "accordion-down 0.2s ease-out",
|
|
||||||
"accordion-up": "accordion-up 0.2s ease-out",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
plugins: [animate],
|
|
||||||
}
|
|
||||||
@@ -1,20 +1,23 @@
|
|||||||
<div align="center">
|
<div align="center">
|
||||||
|
|
||||||
# AMNet
|
# AMNet
|
||||||
[TestFlight](https://testflight.apple.com/join/kHbeJTsQ) • [Firebase](https://appdistribution.firebase.dev/i/6acc3151357deaa8) • [Website](http://card.ppc.moe) • [Server DLLs](https://gitea.tendokyu.moe/ppc/amnet/releases)
|
[Latest .ipa](https://amnet-dist.ppc.moe/amnet-1.3.9.ipa) • [Firebase](https://appdistribution.firebase.dev/i/6acc3151357deaa8) • [Website](http://card.ppc.moe) • [Server DLLs](https://gitea.tendokyu.moe/ppc/amnet/releases)
|
||||||
|
|
||||||
An Aime Card Switcher for segatools-based games.
|
An Amusement IC Card Switcher for segatools-based games.
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
## Overview
|
## Overview
|
||||||
AMNet is designed to simulate the `DEVICE/aime.txt` file, allowing multiple users to log into a game using their phone.
|
AMNet is designed to acts as dynamic version of the `DEVICE/aime.txt` file, allowing multiple users to log into a game using their own mobile devices.
|
||||||
This was originally intended for use with maimai and Chunithm, but should be compatible with any segatools-based game with Aime support.
|
This was originally intended for use with maimai and CHUNITHM, but should be compatible with any segatools-based game with that uses Amusement IC for account management.
|
||||||
|
|
||||||
|
> [!WARNING]
|
||||||
|
> TestFlight is no longer available and must be sideloaded using AltStore or similar tooling for the forseeable future.
|
||||||
|
|
||||||
## Setup
|
## Setup
|
||||||
1. Download the latest `amnet.dll` from the releases page
|
1. Download the latest `amnet.dll` from the releases page
|
||||||
2. Move `amnet.dll` to the game directory
|
2. Move `amnet.dll` to the game directory
|
||||||
3. Edit `segatools.ini` to use the new DLL:
|
3. Edit `segatools.ini`, replacing the existing [aimeio] section with this one (read all the comments and change settings as needed):
|
||||||
```ini
|
```ini
|
||||||
[aimeio]
|
[aimeio]
|
||||||
; To use a custom card reader IO DLL enter its path here.
|
; To use a custom card reader IO DLL enter its path here.
|
||||||
@@ -24,15 +27,29 @@ This was originally intended for use with maimai and Chunithm, but should be com
|
|||||||
; AMNet-specific options (all optional)
|
; AMNet-specific options (all optional)
|
||||||
; 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 (don't set/change unless you know what you're doing)
|
||||||
; 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).
|
; 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).
|
||||||
|
; useAimeDBForPhysicalCards - Whether codes from real cards should use AiMeDB to retrieve access codes. If the game is using a hosted network, set this to 1 to load the same account data/profile as you would get on a physical cab.
|
||||||
gameId=SDEZ
|
gameId=SDEZ
|
||||||
;serverName=CHUNI-PENGUIN
|
;serverName=CHUNI-PENGUIN
|
||||||
;serverAddress=http://+:6070
|
;serverAddress=http://0.0.0.0:6070
|
||||||
;enableKeyboardMode=1
|
;enableKeyboardMode=1
|
||||||
|
;useAimeDBForPhysicalCards=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), add a new server `http://<your-ip-address>:6070`, add a card and start playing!
|
5. Access the using one of the interfaces (see below), add a new server `http://<your-ip-address>:6070`, import your existing card (or create a new one) and start playing!
|
||||||
|
|
||||||
|
### Physical Cards
|
||||||
|
AMNet 1.3 adds support for scanning physical cards on iOS devices. If you own a physical Aime/Banapass/NESiCA/e-Amusement card, you can scan it using the app and use it in-game.
|
||||||
|
By changing the `useAimeDBForPhysicalCards` in segatools.ini, you can choose how the card is read on a per-game basis:
|
||||||
|
|
||||||
|
- `0` will send the 20-digit access code on the back of the card to the game and load the save data directly (default option).
|
||||||
|
- `1` - Send the card's IDm to the game and use AiMeDB to retrieve the 20-digit code assigned to the card.
|
||||||
|
|
||||||
|
Mode `1` makes the app function in the same way to a real card reader and should be used if you want the same access code to show when scanning the card on a cab.
|
||||||
|
You should also use this option if you're connecting to a hosted server (AquaNet/rinnet/etc).
|
||||||
|
|
||||||
|
Additionally, if `useAimeDBForPhysicalCards` is set to `1` and the game is failing to read the card, make sure an up-to-date version of segatools is being used.
|
||||||
|
|
||||||
## Interfaces
|
## Interfaces
|
||||||
AMNet can be used on most platforms, see below for options available for each platform:
|
AMNet can be used on most platforms, see below for options available for each platform:
|
||||||
@@ -46,8 +63,8 @@ AMNet can be used on most platforms, see below for options available for each pl
|
|||||||
</tr>
|
</tr>
|
||||||
<tr>
|
<tr>
|
||||||
<td>iOS</td>
|
<td>iOS</td>
|
||||||
<td><a href="https://testflight.apple.com/join/kHbeJTsQ" target="_blank">TestFlight</a></td>
|
<td><a href="https://amnet-dist.ppc.moe/amnet-1.3.9.ipa" target="_blank">.ipa</a></td>
|
||||||
<td></td>
|
<td>*TestFlight has been shut down and won't be back for the forseeable future, sideload using AltStore or similar tooling</td>
|
||||||
</tr>
|
</tr>
|
||||||
<tr>
|
<tr>
|
||||||
<td rowspan="2">Android</td>
|
<td rowspan="2">Android</td>
|
||||||
@@ -71,18 +88,17 @@ AMNet can be used on most platforms, see below for options available for each pl
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
## Building
|
## Building
|
||||||
### Server (AMNet.Server)
|
### Server (amnet-server and amnet-server-test)
|
||||||
Building the server from source requires the following dependencies:
|
Building the server from source requires a recent version of **Rust** in
|
||||||
Visual Studio 2022 (or Build Tools) with **Desktop Development with C++** and **.NET Desktop Development** workloads, including **MSVC x64/x86** compiler and a recent **Windows 10/11 SDK**.
|
addition to an **MSVC compiler** and (probably) a recent **Windows 10/11 SDK**.
|
||||||
|
|
||||||
After cloning the repository, the server can be built by publishing the project.
|
After cloning the repository, the server can be built using cargo, specifying a target if an x86 version is desired.
|
||||||
Building the server without publish mode will result in a DLL that cannot be run by the game.
|
|
||||||
|
|
||||||
```shell
|
```shell
|
||||||
dotnet publish -c Release -r win-x64 -p:Version=1.0.0 AMNet.Server/AMNet.Server.csproj
|
cargo build --release
|
||||||
```
|
|
||||||
|
|
||||||
The dll will be available at `.\AMNet.Server\bin\Release\net9.0-windows\win-x64\publish\amnet.dll`.
|
# find the .dll at target/release/amnet.dll
|
||||||
|
```
|
||||||
|
|
||||||
### Website (amnet-web)
|
### Website (amnet-web)
|
||||||
The web interface is built using Vite and React. Building the web interface requires Node.js (or alternative runtime).
|
The web interface is built using Vite and React. Building the web interface requires Node.js (or alternative runtime).
|
||||||
|
|||||||
Reference in New Issue
Block a user