diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 4343a0f7..65d8375b 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -51,14 +51,13 @@ jobs: strategy: fail-fast: false matrix: - platform: [macos-latest, ubuntu-20.04, windows-latest] + platform: [windows-latest] defaults: run: working-directory: 'frontend' runs-on: ${{ matrix.platform }} steps: - uses: actions/checkout@v4 - - name: setup node uses: actions/setup-node@v4 with: @@ -67,6 +66,10 @@ jobs: - name: install Rust stable uses: dtolnay/rust-toolchain@stable + - uses: Swatinem/rust-cache@v2 + with: + key: ${{ matrix.config.rust_target }} + - name: install dependencies (ubuntu only) if: matrix.platform == 'ubuntu-20.04' run: | @@ -79,4 +82,6 @@ jobs: # If tagName and releaseId are omitted tauri-action will only build the app and won't try to upload any asstes. - uses: tauri-apps/tauri-action@v0 env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} \ No newline at end of file + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + TAURI_PRIVATE_KEY: ${{ secrets.TAURI_PRIVATE_KEY }} + TAURI_KEY_PASSWORD: ${{ secrets.TAURI_KEY_PASSWORD }} \ No newline at end of file diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index e5b7046d..9c397d81 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -13,7 +13,7 @@ jobs: strategy: fail-fast: false matrix: - platform: [macos-latest, ubuntu-20.04, windows-latest] + platform: [windows-latest] defaults: run: working-directory: 'frontend' @@ -29,6 +29,10 @@ jobs: - name: install Rust stable uses: dtolnay/rust-toolchain@stable + - uses: Swatinem/rust-cache@v2 + with: + key: ${{ matrix.config.rust_target }} + - name: install dependencies (ubuntu only) if: matrix.platform == 'ubuntu-20.04' run: | @@ -41,12 +45,15 @@ jobs: - uses: tauri-apps/tauri-action@v0 env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + TAURI_PRIVATE_KEY: ${{ secrets.TAURI_PRIVATE_KEY }} + TAURI_KEY_PASSWORD: ${{ secrets.TAURI_KEY_PASSWORD }} with: - tagName: app-v__VERSION__ # the action automatically replaces \_\_VERSION\_\_ with the app version + tagName: betterfleet-v__VERSION__ # the action automatically replaces \_\_VERSION\_\_ with the app version releaseName: "BetterFleet v__VERSION__" releaseBody: "See the assets to download this version and install." releaseDraft: true prerelease: false + publish-backend: name: Backend app runs-on: ubuntu-latest diff --git a/.gitignore b/.gitignore index 5233c456..11a36e97 100644 --- a/.gitignore +++ b/.gitignore @@ -1,2 +1,2 @@ -deployement/psql-data/ +deployment/psql-data/ .idea/ \ No newline at end of file diff --git a/LICENSE b/LICENSE index 261eeb9e..b51aaae6 100644 --- a/LICENSE +++ b/LICENSE @@ -2,6 +2,20 @@ Version 2.0, January 2004 http://www.apache.org/licenses/ + 0. Specific Exclusion Clause. + + It is expressly stipulated that [Lazarus], identified by their online pseudonym + associated with the creation and management of the FleetCreator application, + is under no circumstances granted the right to use, modify, distribute, + or sell the [BetterFleet] application (the "Product"), for both personal + and commercial purposes. This prohibition is indefinite until expressly + revoked in writing by the Product's owner. Any violation of this clause will + result in legal action and demands for remedy in accordance with applicable laws. + + This clause is incorporated into the Project [BetterFleet]'s license, + and its breach constitutes an infringement of the terms of this license, + potentially leading to civil and/or criminal penalties. + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 1. Definitions. diff --git a/README.md b/README.md index 6225be0d..6111c506 100644 --- a/README.md +++ b/README.md @@ -1,2 +1,76 @@ -![banner](/frontend/src/assets/banners/banner.png) -# BetterFleet \ No newline at end of file +![image](/frontend/src/assets/banners/banner.png) + +# BetterFleet + +## About BetterFleet + +BetterFleet is a free open source application designed to enhance the gaming experience in Sea Of Thieves by +facilitating the creation of alliances among players. With BetterFleet, you can organize game sessions and invite your +friends to join in a simple and intuitive manner. + +:warning: BetterFleet is not an official application of Sea Of Thieves. It was developed by the community for +players looking to improve their gaming experience. +--- + +## Features + +- **Automatic Session Management:** Facilitates joining the same server with your friends by providing real-time in-game status and server information. + +- **Increase likelihood of finding a server:** Includes an automatic "Set sail" feature so that everyone clicks at the same time. + +- **Self-Hosted Backend:** The open-source nature of the application allows users to host the backend, offering greater control over deployment and maintenance. + +- **Statistics Tracking:** Provides statistical insights to help users assess their server-finding success rate. +--- + +## Comparison of Fleet Management Applications: BetterFleet vs. FleetCreator + +| Feature | BetterFleet | FleetCreator | +|---------------------------------------|-----------------------------|-----------------------------| +| Speed | :question: (Need benchmark) | :question: (Need benchmark) | +| Ad free | :white_check_mark: | :x: | +| Complete free access | :white_check_mark: | :x: | +| UX friendly | :white_check_mark: | :x: | +| Open source | :white_check_mark: | :x: | +| IPv6 support | :white_check_mark: | :x: | +| Automatic click between the same crew | :white_check_mark: | :x: | +| Size of file | <20MB | >200MB | +| No memory leak | :white_check_mark: | :warning:* | + +\* FleetCreator has been observed to consume 8GB of RAM after 10 hours of usage, indicating a possible memory leak. + +--- + +## OS Compatibility + +| Operating System | Compatible | +|------------------|--------------------| +| Windows 11 | :white_check_mark: | +| Windows 10 | :white_check_mark: | +| macOS | :x: | +| Linux | :x: | + +--- + +## Credits đŸ‘„ + +- **Development:** [Zelytra](https://zelytra.fr) & [dadodasyra](https://github.com/dadodasyra) +- **Design/Graphics:** [ZeTro](https://zetro.fr) +- **Translator/proofreader:** [Ichabodt](https://github.com/Ichabodt) + +We thank everyone who contributes to making BetterFleet better every day. If you would like to contribute to the +project, feel free to fork the repository and submit your pull requests. + +--- + +## License 📄 + +BetterFleet is distributed under the MIT license. See the [LICENSE](/LICENSE) file for more information. + +--- + +## Support + +If you have any questions or encounter problems with the app, please open an issue. + +We hope you enjoy using BetterFleet as much as we enjoyed developing it! diff --git a/backend/pom.xml b/backend/pom.xml index 56a9bd92..94fbf969 100644 --- a/backend/pom.xml +++ b/backend/pom.xml @@ -13,7 +13,7 @@ UTF-8 quarkus-bom io.quarkus.platform - 3.7.4 + 3.8.1 true 3.2.5 @@ -52,10 +52,20 @@ rest-assured test + + com.fasterxml.jackson.datatype + jackson-datatype-jsr310 + 2.16.1 + io.quarkus quarkus-websockets + + org.json + json + 20231013 + diff --git a/backend/src/main/java/fr/zelytra/PublicEndpoints.java b/backend/src/main/java/fr/zelytra/PublicEndpoints.java index aa2a8bee..c247f703 100644 --- a/backend/src/main/java/fr/zelytra/PublicEndpoints.java +++ b/backend/src/main/java/fr/zelytra/PublicEndpoints.java @@ -1,12 +1,9 @@ package fr.zelytra; -import io.quarkus.logging.Log; import jakarta.ws.rs.GET; import jakarta.ws.rs.Path; import jakarta.ws.rs.core.Response; -import java.util.UUID; - @Path("/") public class PublicEndpoints { diff --git a/backend/src/main/java/fr/zelytra/session/SessionManager.java b/backend/src/main/java/fr/zelytra/session/SessionManager.java index 15ce4bc7..ced13d27 100644 --- a/backend/src/main/java/fr/zelytra/session/SessionManager.java +++ b/backend/src/main/java/fr/zelytra/session/SessionManager.java @@ -1,13 +1,15 @@ package fr.zelytra.session; -import com.fasterxml.jackson.databind.ObjectMapper; import fr.zelytra.session.fleet.Fleet; -import fr.zelytra.session.fleet.Player; +import fr.zelytra.session.player.Player; +import fr.zelytra.session.server.SotServer; +import fr.zelytra.session.socket.MessageType; import io.quarkus.logging.Log; import jakarta.annotation.Nullable; -import java.io.IOException; -import java.util.*; +import java.util.HashMap; +import java.util.Map; +import java.util.UUID; /** * Manages sessions for a multiplayer game, allowing players to create, join, and leave sessions. @@ -18,11 +20,15 @@ public class SessionManager { private final HashMap sessions; + // SotServer cached to avoid API spam and faster server response + private final HashMap sotServers; + /** * Private constructor for singleton pattern. */ private SessionManager() { this.sessions = new HashMap<>(); + this.sotServers = new HashMap<>(); } /** @@ -65,9 +71,9 @@ public boolean isSessionExist(String sessionId) { * * @param sessionId The ID of the session to join. * @param player The player attempting to join the session. - * @return true if the player was successfully added, false otherwise. + * @return the Fleet where the player was added or null */ - public boolean joinSession(String sessionId, Player player) { + public Fleet joinSession(String sessionId, Player player) { // First, leave any session the player might currently be in if (getPlayerFromSessionId(player.getSocket().getId()) != null) { leaveSession(player); @@ -76,11 +82,11 @@ public boolean joinSession(String sessionId, Player player) { Fleet fleet = getFleetFromId(sessionId); if (fleet == null) { Log.error("[" + sessionId + "] Session doesnt exist for player : " + player.getUsername()); - return false; + return null; } fleet.getPlayers().add(player); Log.info("[" + sessionId + "] " + player.getUsername() + " Join the session !"); - return true; + return fleet; } /** @@ -91,7 +97,10 @@ public boolean joinSession(String sessionId, Player player) { public void leaveSession(Player player) { for (Fleet fleet : sessions.values()) { fleet.getPlayers().remove(player); - SessionSocket.broadcastSessionUpdate(fleet.getSessionId()); + fleet.getServers().forEach((key, value) -> { + value.getConnectedPlayers().remove(player); + }); + SessionSocket.broadcastDataToSession(fleet.getSessionId(), MessageType.UPDATE, fleet); Log.info("[" + fleet.getSessionId() + "] " + player.getUsername() + " Leave the session !"); // Clean empty session @@ -170,5 +179,55 @@ public boolean isPlayerInSession(Player player, String sessionId) { return false; // The specified player is not found in the session } + public SotServer getServerFromHashing(SotServer server) { + String hash = server.generateHash(); + + // Return cached SOT server + if (sotServers.containsKey(hash)) { + return sotServers.get(hash); + } + + // The object inject may not be completed, so we're creating fresh one to make sure all data has been initialized + SotServer newServer = new SotServer(server.getIp(), server.getPort()); + sotServers.put(newServer.getHash(), newServer); + return newServer; + } + public void playerJoinSotServer(Player player, SotServer server) { + SotServer findedSotServer = getServerFromHashing(server); + + Fleet fleet = getFleetByPlayerName(player.getUsername()); + assert fleet != null; + + // Detect if the server is not already know by the fleet + if (!fleet.getServers().containsKey(findedSotServer.getHash())) { + fleet.getServers().put(findedSotServer.getHash(), findedSotServer); + } + // Do not add player if already in + if (fleet.getServers().get(findedSotServer.getHash()).getConnectedPlayers().contains(player)) { + return; + } + + // Add player to SotServer in Fleet and broadcast update + fleet.getServers().get(findedSotServer.getHash()).getConnectedPlayers().add(player); + Log.info("[" + fleet.getSessionId() + "] " + player.getUsername() + " join the SotServer: " + fleet.getServers().get(findedSotServer.getHash()).getHash()); + SessionSocket.broadcastDataToSession(fleet.getSessionId(), MessageType.UPDATE, fleet); + } + + public void playerLeaveSotServer(Player player, SotServer server) { + SotServer findedSotServer = getServerFromHashing(server); + + Fleet fleet = getFleetByPlayerName(player.getUsername()); + assert fleet != null; + + SotServer fleetFindedServer = fleet.getServers().get(findedSotServer.getHash()); + fleetFindedServer.getConnectedPlayers().remove(player); + + // If SotServer empty remove server from the list + if (fleetFindedServer.getConnectedPlayers().isEmpty()) { + fleet.getServers().remove(fleetFindedServer.getHash()); + } + Log.info("[" + fleet.getSessionId() + "] " + player.getUsername() + " leave the SotServer: " + fleetFindedServer.getHash()); + SessionSocket.broadcastDataToSession(fleet.getSessionId(), MessageType.UPDATE, fleet); + } } diff --git a/backend/src/main/java/fr/zelytra/session/SessionSocket.java b/backend/src/main/java/fr/zelytra/session/SessionSocket.java index 49ac333e..f31ea644 100644 --- a/backend/src/main/java/fr/zelytra/session/SessionSocket.java +++ b/backend/src/main/java/fr/zelytra/session/SessionSocket.java @@ -1,11 +1,18 @@ package fr.zelytra.session; import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.core.type.TypeReference; import com.fasterxml.jackson.databind.DeserializationFeature; import com.fasterxml.jackson.databind.MapperFeature; import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.SerializationFeature; +import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule; +import fr.zelytra.session.countdown.SessionCountDown; import fr.zelytra.session.fleet.Fleet; -import fr.zelytra.session.fleet.Player; +import fr.zelytra.session.player.Player; +import fr.zelytra.session.server.SotServer; +import fr.zelytra.session.socket.MessageType; +import fr.zelytra.session.socket.SocketMessage; import io.quarkus.logging.Log; import jakarta.enterprise.context.ApplicationScoped; import jakarta.websocket.*; @@ -13,7 +20,7 @@ import jakarta.websocket.server.ServerEndpoint; import java.io.IOException; -import java.util.Objects; +import java.util.TimeZone; import java.util.concurrent.*; @ServerEndpoint("/sessions/{sessionId}") // WebSocket endpoint @@ -39,55 +46,128 @@ public void onOpen(Session session) { }); sessionTimeoutTasks.put(session.getId(), timeoutTask); Log.info("[ANYONE] Connecting..."); - } + @OnMessage public void onMessage(String message, Session session, @PathParam("sessionId") String sessionId) throws JsonProcessingException { - // Cancel the timeout task since we've received the message - Future timeoutTask = sessionTimeoutTasks.remove(session.getId()); - if (timeoutTask != null) { - timeoutTask.cancel(true); - } - ObjectMapper objectMapper = new ObjectMapper(); + objectMapper.registerModule(new JavaTimeModule()); objectMapper.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false); objectMapper.enable(MapperFeature.ACCEPT_CASE_INSENSITIVE_ENUMS); - Player player = objectMapper.readValue(message, Player.class); - player.setSocket(session); + // Deserialize the incoming message to SocketMessage + SocketMessage socketMessage = objectMapper.readValue(message, new TypeReference<>() { + }); + + // Handle the message based on its type + switch (socketMessage.messageType()) { + case CONNECT -> { + Player player = objectMapper.convertValue(socketMessage.data(), Player.class); + handleConnectMessage(player, session, sessionId); + } + case UPDATE -> { + Player player = objectMapper.convertValue(socketMessage.data(), Player.class); + handleLeaveMessage(player); + } + case START_COUNTDOWN -> { + SessionCountDown countDown = objectMapper.convertValue(socketMessage.data(), SessionCountDown.class); + handleStartCountdown(session, countDown); + } + case CLEAR_STATUS -> { + handleClearStatus(session); + } + case JOIN_SERVER -> { + SotServer sotServer = objectMapper.convertValue(socketMessage.data(), SotServer.class); + handleJoinServerMessage(session, sotServer); + } + case LEAVE_SERVER -> { + SotServer sotServer = objectMapper.convertValue(socketMessage.data(), SotServer.class); + handleLeaveServerMessage(session, sotServer); + } + default -> Log.info("Unhandled message type: " + socketMessage.messageType()); + } + } + + private void handleClearStatus(Session session) { SessionManager manager = SessionManager.getInstance(); + Player player = manager.getPlayerFromSessionId(session.getId()); + Fleet fleet = manager.getFleetByPlayerName(player.getUsername()); + fleet.getPlayers().forEach((playerInList) -> { + playerInList.setReady(false); + }); + Log.info("[" + fleet.getSessionId() + "] Clearing status of all player"); + broadcastDataToSession(fleet.getSessionId(), MessageType.UPDATE, fleet); + } - //Check if it's an update player request - if (manager.isPlayerInSession(player, player.getSessionId())) { + private void handleStartCountdown(Session session, SessionCountDown countDown) { - Fleet fleet = manager.getFleetFromId(player.getSessionId()); - assert fleet != null; - Player foundedplayer = fleet.getPlayerFromUsername(player.getUsername()); + countDown.calculateClickTime(); - foundedplayer.setReady(player.isReady()); - foundedplayer.setStatus(player.getStatus()); + SessionManager manager = SessionManager.getInstance(); + Player player = manager.getPlayerFromSessionId(session.getId()); + Fleet fleet = manager.getFleetByPlayerName(player.getUsername()); - broadcastSessionUpdate(player.getSessionId().toUpperCase()); + Log.info("[" + fleet.getSessionId() + "] Starting countdown at " + countDown.getClickTime().toString()); + broadcastDataToSession(fleet.getSessionId(), MessageType.RUN_COUNTDOWN, countDown); + } - Log.info("[" + player.getUsername() + "] Data updated for session !"); - return; + // Extracted method to handle JOIN_SERVER messages + private void handleJoinServerMessage(Session session, SotServer sotServer) { + SessionManager manager = SessionManager.getInstance(); + Player player = manager.getPlayerFromSessionId(session.getId()); + manager.playerJoinSotServer(player, sotServer); + } + + // Extracted method to handle LEAVE_SERVER messages + private void handleLeaveServerMessage(Session session, SotServer sotServer) { + SessionManager manager = SessionManager.getInstance(); + Player player = manager.getPlayerFromSessionId(session.getId()); + manager.playerLeaveSotServer(player, sotServer); + } + + // Extracted method to handle CONNECT messages + private void handleConnectMessage(Player player, Session session, String sessionId) { + // Cancel the timeout task since we've received the message + Future timeoutTask = sessionTimeoutTasks.remove(session.getId()); + if (timeoutTask != null) { + timeoutTask.cancel(true); } + SessionManager manager = SessionManager.getInstance(); + player.setSocket(session); + Log.info("[" + player.getUsername() + "] Connected !"); + //Create session if no id provided if (sessionId == null || sessionId.isEmpty()) { String newSessionId = manager.createSession(); - manager.joinSession(newSessionId, player); + Fleet fleet = manager.joinSession(newSessionId, player); player.setMaster(true); - broadcastSessionUpdate(newSessionId); + broadcastDataToSession(newSessionId, MessageType.UPDATE, fleet); } else { - manager.joinSession(sessionId, player); - broadcastSessionUpdate(sessionId); + Fleet fleet = manager.joinSession(sessionId, player); + broadcastDataToSession(sessionId, MessageType.UPDATE, fleet); } + + } + + // Extracted method to handle LEAVE messages + private void handleLeaveMessage(Player player) { + SessionManager manager = SessionManager.getInstance(); + Fleet fleet = manager.getFleetFromId(player.getSessionId()); + assert fleet != null; + Player foundedplayer = fleet.getPlayerFromUsername(player.getUsername()); + + foundedplayer.setReady(player.isReady()); + foundedplayer.setStatus(player.getStatus()); + + broadcastDataToSession(player.getSessionId(), MessageType.UPDATE, fleet); + + Log.info("[" + player.getUsername() + "] Data updated for session !"); } @OnClose @@ -120,10 +200,28 @@ public void onError(Session session, Throwable throwable) throws IOException { session.close(); } + /** - * @param sessionId Fleet session id + * Broadcasts a message to all players within a session. + *

+ * This method sends a specified data object to all players in a session identified by the sessionId. The message type + * and data to be broadcast are specified by the parameters. It uses {@link SessionManager} to check if the session + * exists and to retrieve the corresponding {@link Fleet} of players. If the session does not exist, it logs an info + * message and returns without sending any data. It constructs a {@link SocketMessage} with the messageType and data, + * converts it into JSON format, and then broadcasts this JSON string to all players in the session using their sockets. + * If any error occurs during the JSON conversion or broadcasting, it logs an error or throws an {@link Error} respectively. + * + * @param The type of data to be broadcasted. This allows the method to be used with various types of + * data objects. + * @param sessionId The ID of the session to which the data will be broadcast. This is used to identify the + * group of players who should receive the message. + * @param messageType The type of the message to be sent. This helps in identifying the purpose or action of + * the message on the client side. + * @param data The data to be broadcast. This is the actual content of the message being sent to the players. + * The type of this data is generic, allowing for flexibility in what can be sent. + * @throws Error if there is an issue with converting the {@link SocketMessage} object to a JSON string. */ - public static void broadcastSessionUpdate(String sessionId) { + public static void broadcastDataToSession(String sessionId, MessageType messageType, T data) { SessionManager manager = SessionManager.getInstance(); if (!manager.isSessionExist(sessionId)) { @@ -133,21 +231,28 @@ public static void broadcastSessionUpdate(String sessionId) { Fleet fleet = manager.getFleetFromId(sessionId); assert fleet != null; - // Send to all players the Fleet data + SocketMessage message = new SocketMessage<>(messageType, data); + + // Send to all players the Countdown data ObjectMapper objectMapper = new ObjectMapper(); + objectMapper.registerModule(new JavaTimeModule()); + objectMapper.disable(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS); // To serialize as ISO-8601 strings + objectMapper.setTimeZone(TimeZone.getTimeZone("UTC")); String json; try { - json = objectMapper.writeValueAsString(fleet); + json = objectMapper.writeValueAsString(message); } catch (JsonProcessingException e) { - throw new RuntimeException(e); + throw new Error(e); } for (Player player : fleet.getPlayers()) { player.getSocket().getAsyncRemote().sendText(json, result -> { if (result.getException() != null) { - System.out.println("Unable to send message: " + result.getException()); + Log.error("Unable to send message: " + result.getException()); } }); } } + + } diff --git a/backend/src/main/java/fr/zelytra/session/fleet/SessionStatus.java b/backend/src/main/java/fr/zelytra/session/SessionStatus.java similarity index 85% rename from backend/src/main/java/fr/zelytra/session/fleet/SessionStatus.java rename to backend/src/main/java/fr/zelytra/session/SessionStatus.java index 080fe0cf..08ffe79c 100644 --- a/backend/src/main/java/fr/zelytra/session/fleet/SessionStatus.java +++ b/backend/src/main/java/fr/zelytra/session/SessionStatus.java @@ -1,4 +1,4 @@ -package fr.zelytra.session.fleet; +package fr.zelytra.session; public enum SessionStatus { WAITING, // Waiting for player to be ready diff --git a/backend/src/main/java/fr/zelytra/session/countdown/SessionCountDown.java b/backend/src/main/java/fr/zelytra/session/countdown/SessionCountDown.java new file mode 100644 index 00000000..6d83a2c9 --- /dev/null +++ b/backend/src/main/java/fr/zelytra/session/countdown/SessionCountDown.java @@ -0,0 +1,37 @@ +package fr.zelytra.session.countdown; + +import java.time.LocalTime; + +public class SessionCountDown { + private LocalTime startingTimer; + private LocalTime clickTime; + + public SessionCountDown() { + + } + + public SessionCountDown(LocalTime startingTimer) { + this.startingTimer = startingTimer; + calculateClickTime(); + } + + public void calculateClickTime() { + this.clickTime = startingTimer.plusSeconds(6); + } + + public LocalTime getStartingTimer() { + return startingTimer; + } + + public void setStartingTimer(LocalTime startingTimer) { + this.startingTimer = startingTimer; + } + + public LocalTime getClickTime() { + return clickTime; + } + + public void setClickTime(LocalTime clickTime) { + this.clickTime = clickTime; + } +} diff --git a/backend/src/main/java/fr/zelytra/session/fleet/Fleet.java b/backend/src/main/java/fr/zelytra/session/fleet/Fleet.java index 2cd88ae2..d0861159 100644 --- a/backend/src/main/java/fr/zelytra/session/fleet/Fleet.java +++ b/backend/src/main/java/fr/zelytra/session/fleet/Fleet.java @@ -1,6 +1,11 @@ package fr.zelytra.session.fleet; +import fr.zelytra.session.SessionStatus; +import fr.zelytra.session.player.Player; +import fr.zelytra.session.server.SotServer; + import java.util.ArrayList; +import java.util.HashMap; import java.util.List; import java.util.stream.Collectors; @@ -9,14 +14,14 @@ public class Fleet { private String sessionId; private String sessionName; private List players; - private List servers; + private final HashMap servers; private SessionStatus status; public Fleet(String sessionId) { this.sessionId = sessionId; this.sessionName = "A session name"; //TODO this.players = new ArrayList<>(); - this.servers = new ArrayList<>(); + this.servers = new HashMap<>(); this.status = SessionStatus.WAITING; } @@ -53,14 +58,10 @@ public void setPlayers(List players) { this.players = players; } - public List getServers() { + public HashMap getServers() { return servers; } - public void setServers(List servers) { - this.servers = servers; - } - public SessionStatus getStatus() { return status; } diff --git a/backend/src/main/java/fr/zelytra/session/fleet/PlayerStates.java b/backend/src/main/java/fr/zelytra/session/fleet/PlayerStates.java deleted file mode 100644 index b7bfc2bf..00000000 --- a/backend/src/main/java/fr/zelytra/session/fleet/PlayerStates.java +++ /dev/null @@ -1,8 +0,0 @@ -package fr.zelytra.session.fleet; - -public enum PlayerStates { - OFFLINE, // Game not detected - ONLINE, // Game detected and open but not in game - IN_GAME // Player in a server -} - diff --git a/backend/src/main/java/fr/zelytra/session/fleet/SotServer.java b/backend/src/main/java/fr/zelytra/session/fleet/SotServer.java deleted file mode 100644 index b93dde3d..00000000 --- a/backend/src/main/java/fr/zelytra/session/fleet/SotServer.java +++ /dev/null @@ -1,53 +0,0 @@ -package fr.zelytra.session.fleet; - -import java.util.List; - -public class SotServer { - - private String ip; - private int port; - private String location; - private List connectedPlayers; - - // Constructor - public SotServer(String ip, int port, String location, List connectedPlayers) { - this.ip = ip; - this.port = port; - this.location = location; - this.connectedPlayers = connectedPlayers; - } - - // Getters and Setters - public String getIp() { - return ip; - } - - public void setIp(String ip) { - this.ip = ip; - } - - public int getPort() { - return port; - } - - public void setPort(int port) { - this.port = port; - } - - public String getLocation() { - return location; - } - - public void setLocation(String location) { - this.location = location; - } - - public List getConnectedPlayers() { - return connectedPlayers; - } - - public void setConnectedPlayers(List connectedPlayers) { - this.connectedPlayers = connectedPlayers; - } -} - diff --git a/backend/src/main/java/fr/zelytra/session/ip/ProxyCheckAPI.java b/backend/src/main/java/fr/zelytra/session/ip/ProxyCheckAPI.java new file mode 100644 index 00000000..afadc764 --- /dev/null +++ b/backend/src/main/java/fr/zelytra/session/ip/ProxyCheckAPI.java @@ -0,0 +1,84 @@ +package fr.zelytra.session.ip; + +import io.quarkus.logging.Log; +import org.json.JSONObject; + +import java.io.BufferedReader; +import java.io.InputStreamReader; +import java.net.HttpURLConnection; +import java.net.URL; + +/** + * Class retrieving data from ProxyChecker api + * web site + *

+ * The api have a limitation of 100 request per day without token. Increased to 1000 with a token. + */ +public class ProxyCheckAPI { + + private static final String apiURL = "https://proxycheck.io/v2/"; + private static final String requestPathParam = "asn=1"; + private static final String tokenParam = "key="; + + private final StringBuilder finalUrl = new StringBuilder(); + private final String ip; + + public ProxyCheckAPI(String ip) { + this.ip = ip; + finalUrl.append(apiURL).append(ip).append("?").append(requestPathParam); + } + + public ProxyCheckAPI(String ip, String apiKey) { + this.ip = ip; + finalUrl.append(apiURL) + .append(ip) + .append("?") + .append(requestPathParam) + .append("&") + .append(tokenParam) + .append(apiKey); + } + + public String retrieveCountry() { + try { + URL url = new URL(finalUrl.toString()); + + HttpURLConnection connection = (HttpURLConnection) url.openConnection(); + connection.setRequestMethod("GET"); + + int responseCode = connection.getResponseCode(); + if (responseCode == HttpURLConnection.HTTP_OK) { + + BufferedReader in = new BufferedReader(new InputStreamReader(connection.getInputStream())); + String inputLine; + StringBuilder response = new StringBuilder(); + + while ((inputLine = in.readLine()) != null) { + response.append(inputLine); + } + in.close(); + + StringBuilder location = new StringBuilder(); + JSONObject jsonResponse = new JSONObject(response.toString()); + JSONObject ipJsonObject = jsonResponse.getJSONObject(ip); // Assuming the value is a String + + location.append(ipJsonObject.getString("continent")).append(" - "); + location.append(ipJsonObject.getString("country")).append(" - "); + location.append(ipJsonObject.getString("region")).append(" - "); + location.append(ipJsonObject.getString("city")); + + Log.info("[PROXY CHECK] New SOT server detected !"); + + return location.toString(); + + } else { + Log.error("GET request not worked, Response Code: " + responseCode); + } + } catch (Exception e) { + Log.error("Failed to retrieve information via ProxyChecker of ip " + this.ip); + e.printStackTrace(); + } + return ""; + } + +} diff --git a/backend/src/main/java/fr/zelytra/session/fleet/Player.java b/backend/src/main/java/fr/zelytra/session/player/Player.java similarity index 97% rename from backend/src/main/java/fr/zelytra/session/fleet/Player.java rename to backend/src/main/java/fr/zelytra/session/player/Player.java index a94c06c7..d9efdfa4 100644 --- a/backend/src/main/java/fr/zelytra/session/fleet/Player.java +++ b/backend/src/main/java/fr/zelytra/session/player/Player.java @@ -1,4 +1,4 @@ -package fr.zelytra.session.fleet; +package fr.zelytra.session.player; import com.fasterxml.jackson.annotation.JsonIgnore; import com.fasterxml.jackson.annotation.JsonProperty; diff --git a/backend/src/main/java/fr/zelytra/session/player/PlayerStates.java b/backend/src/main/java/fr/zelytra/session/player/PlayerStates.java new file mode 100644 index 00000000..777de624 --- /dev/null +++ b/backend/src/main/java/fr/zelytra/session/player/PlayerStates.java @@ -0,0 +1,9 @@ +package fr.zelytra.session.player; + +public enum PlayerStates { + CLOSED, // Game is closed + STARTED, // Game detected an // Game is in first menu after launch / launching / stopping + MAIN_MENU, // In menu to select game mode + IN_GAME, // Status when the remote IP and port was found and player is in game +} + diff --git a/backend/src/main/java/fr/zelytra/session/server/SotServer.java b/backend/src/main/java/fr/zelytra/session/server/SotServer.java new file mode 100644 index 00000000..71c39fca --- /dev/null +++ b/backend/src/main/java/fr/zelytra/session/server/SotServer.java @@ -0,0 +1,90 @@ +package fr.zelytra.session.server; + +import fr.zelytra.session.ip.ProxyCheckAPI; +import fr.zelytra.session.player.Player; +import io.quarkus.logging.Log; +import org.eclipse.microprofile.config.inject.ConfigProperty; + +import java.nio.charset.StandardCharsets; +import java.security.MessageDigest; +import java.security.NoSuchAlgorithmException; +import java.util.ArrayList; +import java.util.List; + +public class SotServer { + + private String ip; + private int port; + private String location; + private String hash; + private List connectedPlayers; + + @ConfigProperty(name = "proxy.check.api.key") + String proxyApiKey; + + public SotServer() { + } + + // Constructor + public SotServer(String ip, int port) { + this.ip = ip; + this.port = port; + + ProxyCheckAPI proxyCheckAPI = new ProxyCheckAPI(ip); + this.location = proxyCheckAPI.retrieveCountry(); + this.hash = generateHash(); + this.connectedPlayers = new ArrayList<>(); + } + + public String generateHash() { + // Combine IP and port into a single string + String input = this.ip + ":" + this.port; + + // Use SHA-256 hash function + MessageDigest digest = null; + try { + digest = MessageDigest.getInstance("SHA-256"); + } catch (NoSuchAlgorithmException e) { + throw new RuntimeException(e); + } + byte[] hashBytes = digest.digest(input.getBytes(StandardCharsets.UTF_8)); + + // Convert the hash bytes to hexadecimal format + StringBuilder hexString = new StringBuilder(); + for (int i = 0; i < hashBytes.length && hexString.length() < 6; i++) { + String hex = Integer.toHexString(0xff & hashBytes[i]); + if (hex.length() == 1) hexString.append('0'); + hexString.append(hex); + } + + // Return the first 6 characters of the hex string + return hexString.substring(0, 6); + } + + // Getters and Setters + public String getIp() { + return ip; + } + + public int getPort() { + return port; + } + + public String getLocation() { + return location; + } + + public List getConnectedPlayers() { + return connectedPlayers; + } + + public String getHash() { + return hash; + } + + @Override + public String toString() { + return ip + ":" + port + " | " + hash + " | " + location; + } +} + diff --git a/backend/src/main/java/fr/zelytra/session/server/SotServerMessage.java b/backend/src/main/java/fr/zelytra/session/server/SotServerMessage.java new file mode 100644 index 00000000..1fc70c0e --- /dev/null +++ b/backend/src/main/java/fr/zelytra/session/server/SotServerMessage.java @@ -0,0 +1,5 @@ +package fr.zelytra.session.server; + +// The variable serverIpPort need to be in this format : "x.x.x.x:x" +public record SotServerMessage(int port,String ip) { +} diff --git a/backend/src/main/java/fr/zelytra/session/socket/MessageType.java b/backend/src/main/java/fr/zelytra/session/socket/MessageType.java new file mode 100644 index 00000000..1d1b5ee2 --- /dev/null +++ b/backend/src/main/java/fr/zelytra/session/socket/MessageType.java @@ -0,0 +1,12 @@ +package fr.zelytra.session.socket; + +public enum MessageType { + CONNECT, // When a player join a session + UPDATE, // When the data of the player need to be broadcast to other player of the session + START_COUNTDOWN, + CANCEL_COUNTDOWN, + RUN_COUNTDOWN, + JOIN_SERVER, + LEAVE_SERVER, + CLEAR_STATUS, +} diff --git a/backend/src/main/java/fr/zelytra/session/socket/SocketMessage.java b/backend/src/main/java/fr/zelytra/session/socket/SocketMessage.java new file mode 100644 index 00000000..4737d793 --- /dev/null +++ b/backend/src/main/java/fr/zelytra/session/socket/SocketMessage.java @@ -0,0 +1,3 @@ +package fr.zelytra.session.socket; + +public record SocketMessage(MessageType messageType, T data){} diff --git a/backend/src/main/resources/application.properties b/backend/src/main/resources/application.properties index e69de29b..36156ea2 100644 --- a/backend/src/main/resources/application.properties +++ b/backend/src/main/resources/application.properties @@ -0,0 +1 @@ +proxy.check.api.key=${PROXY_CHECK_API_KEY:test} \ No newline at end of file diff --git a/deployment/DEPLOYMENT.md b/deployment/DEPLOYMENT.md new file mode 100644 index 00000000..ae67e58c --- /dev/null +++ b/deployment/DEPLOYMENT.md @@ -0,0 +1,32 @@ +## Deploy backend +``docker run -d -p 8301:8080 zelytra/better-fleet-backend:latest`` + +## Nginx conf +```nginx configuration +server { + + server_name betterfleet.fr; + + location / { + proxy_pass http://127.0.0.1:8300; + } + + location /api { + rewrite /api/(.*) /$1 break; + proxy_pass http://127.0.0.1:8301; + proxy_redirect off; + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-Proto $scheme; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + # Socket conf + proxy_set_header Upgrade $http_upgrade; + proxy_set_header Connection "Upgrade"; + proxy_hide_header 'Access-Control-Allow-Origin'; + proxy_http_version 1.1; + } + + listen [::]:80; + listen 80; +} +``` \ No newline at end of file diff --git a/frontend/.env.production b/frontend/.env.production index c5645a3f..0de079e7 100644 --- a/frontend/.env.production +++ b/frontend/.env.production @@ -1,4 +1,2 @@ -VITE_BACKEND_HOST=VITE_BACKEND_HOST_PLACEHOLDER -VITE_KEYCLOAK_HOST=VITE_KEYCLOAK_HOST_PLACEHOLDER -VITE_SOCKET_HOST=ws://127.0.0.1:8080/sessions +VITE_SOCKET_HOST=wss://betterfleet.fr/api/sessions VITE_VERSION=$npm_package_version \ No newline at end of file diff --git a/frontend/.eslintrc.cjs b/frontend/.eslintrc.cjs deleted file mode 100644 index 84dd7502..00000000 --- a/frontend/.eslintrc.cjs +++ /dev/null @@ -1,12 +0,0 @@ -/* eslint-env node */ -module.exports = { - root: true, - extends: [ - 'plugin:vue/vue3-recommended', - ], - rules: { - // Disable the 'vue/no-use-v-if-with-v-for' rule - "vue/no-use-v-if-with-v-for": "off", - "vue/multi-word-component-names": "off" - }, -}; diff --git a/frontend/package-lock.json b/frontend/package-lock.json index 0e471f54..7210352c 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -1,37 +1,27 @@ { "name": "betterfleet", - "version": "0.0.0", + "version": "0.1.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "betterfleet", - "version": "0.0.0", + "version": "0.1.0", "dependencies": { + "@js-joda/core": "^5.6.1", + "@tauri-apps/api": "^1.5.3", "axios": "^1.6.7", "vue": "^3.4.21", - "vue-i18n": "^9.8.0" + "vue-i18n": "^9.10.1" }, "devDependencies": { "@tauri-apps/cli": "^1.5.10", "@vitejs/plugin-vue": "^5.0.4", - "eslint": "^8.57.0", - "eslint-plugin-vue": "^9.22.0", - "prettier": "^3.2.5", "sass": "^1.71.1", "typescript": "^5.2.2", "vite": "^5.1.4", "vue-router": "^4.2.2", - "vue-tsc": "^1.8.27" - } - }, - "node_modules/@aashutoshrathi/word-wrap": { - "version": "1.2.6", - "resolved": "https://registry.npmjs.org/@aashutoshrathi/word-wrap/-/word-wrap-1.2.6.tgz", - "integrity": "sha512-1Yjs2SvM8TflER/OD3cOjhWWOZb58A2t7wpE2S9XfBYTiIl+XFhQG2bjy4Pu1I+EAlCNUzRDYDdFwFYUKvXcIA==", - "dev": true, - "engines": { - "node": ">=0.10.0" + "vue-tsc": "^2.0.3" } }, "node_modules/@babel/parser": { @@ -413,146 +403,13 @@ "node": ">=12" } }, - "node_modules/@eslint-community/eslint-utils": { - "version": "4.4.0", - "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.4.0.tgz", - "integrity": "sha512-1/sA4dwrzBAyeUoQ6oxahHKmrZvsnLCg4RfxW3ZFGGmQkSNQPFNLV9CUEFQP1x9EYXHTo5p6xdhZM1Ne9p/AfA==", - "dev": true, - "dependencies": { - "eslint-visitor-keys": "^3.3.0" - }, - "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" - }, - "peerDependencies": { - "eslint": "^6.0.0 || ^7.0.0 || >=8.0.0" - } - }, - "node_modules/@eslint-community/regexpp": { - "version": "4.10.0", - "resolved": "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.10.0.tgz", - "integrity": "sha512-Cu96Sd2By9mCNTx2iyKOmq10v22jUVQv0lQnlGNy16oE9589yE+QADPbrMGCkA51cKZSg3Pu/aTJVTGfL/qjUA==", - "dev": true, - "engines": { - "node": "^12.0.0 || ^14.0.0 || >=16.0.0" - } - }, - "node_modules/@eslint/eslintrc": { - "version": "2.1.4", - "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-2.1.4.tgz", - "integrity": "sha512-269Z39MS6wVJtsoUl10L60WdkhJVdPG24Q4eZTH3nnF6lpvSShEK3wQjDX9JRWAUPvPh7COouPpU9IrqaZFvtQ==", - "dev": true, - "dependencies": { - "ajv": "^6.12.4", - "debug": "^4.3.2", - "espree": "^9.6.0", - "globals": "^13.19.0", - "ignore": "^5.2.0", - "import-fresh": "^3.2.1", - "js-yaml": "^4.1.0", - "minimatch": "^3.1.2", - "strip-json-comments": "^3.1.1" - }, - "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" - }, - "funding": { - "url": "https://opencollective.com/eslint" - } - }, - "node_modules/@eslint/eslintrc/node_modules/brace-expansion": { - "version": "1.1.11", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", - "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", - "dev": true, - "dependencies": { - "balanced-match": "^1.0.0", - "concat-map": "0.0.1" - } - }, - "node_modules/@eslint/eslintrc/node_modules/minimatch": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", - "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", - "dev": true, - "dependencies": { - "brace-expansion": "^1.1.7" - }, - "engines": { - "node": "*" - } - }, - "node_modules/@eslint/js": { - "version": "8.57.0", - "resolved": "https://registry.npmjs.org/@eslint/js/-/js-8.57.0.tgz", - "integrity": "sha512-Ys+3g2TaW7gADOJzPt83SJtCDhMjndcDMFVQ/Tj9iA1BfJzFKD9mAUXT3OenpuPHbI6P/myECxRJrofUsDx/5g==", - "dev": true, - "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" - } - }, - "node_modules/@humanwhocodes/config-array": { - "version": "0.11.14", - "resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.11.14.tgz", - "integrity": "sha512-3T8LkOmg45BV5FICb15QQMsyUSWrQ8AygVfC7ZG32zOalnqrilm018ZVCw0eapXux8FtA33q8PSRSstjee3jSg==", - "dev": true, - "dependencies": { - "@humanwhocodes/object-schema": "^2.0.2", - "debug": "^4.3.1", - "minimatch": "^3.0.5" - }, - "engines": { - "node": ">=10.10.0" - } - }, - "node_modules/@humanwhocodes/config-array/node_modules/brace-expansion": { - "version": "1.1.11", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", - "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", - "dev": true, - "dependencies": { - "balanced-match": "^1.0.0", - "concat-map": "0.0.1" - } - }, - "node_modules/@humanwhocodes/config-array/node_modules/minimatch": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", - "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", - "dev": true, - "dependencies": { - "brace-expansion": "^1.1.7" - }, - "engines": { - "node": "*" - } - }, - "node_modules/@humanwhocodes/module-importer": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/@humanwhocodes/module-importer/-/module-importer-1.0.1.tgz", - "integrity": "sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==", - "dev": true, - "engines": { - "node": ">=12.22" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/nzakas" - } - }, - "node_modules/@humanwhocodes/object-schema": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/@humanwhocodes/object-schema/-/object-schema-2.0.2.tgz", - "integrity": "sha512-6EwiSjwWYP7pTckG6I5eyFANjPhmPjUX9JRLUSfNPC7FX7zK9gyZAfUEaECL6ALTpGX5AjnBq3C9XmVWPitNpw==", - "dev": true - }, "node_modules/@intlify/core-base": { - "version": "9.9.1", - "resolved": "https://registry.npmjs.org/@intlify/core-base/-/core-base-9.9.1.tgz", - "integrity": "sha512-qsV15dg7jNX2faBRyKMgZS8UcFJViWEUPLdzZ9UR0kQZpFVeIpc0AG7ZOfeP7pX2T9SQ5jSiorq/tii9nkkafA==", + "version": "9.10.1", + "resolved": "https://registry.npmjs.org/@intlify/core-base/-/core-base-9.10.1.tgz", + "integrity": "sha512-0+Wtjj04GIyglh5KKiNjRwgjpHrhqqGZhaKY/QVjjogWKZq5WHROrTi84pNVsRN18QynyPmjtsVUWqFKPQ45xQ==", "dependencies": { - "@intlify/message-compiler": "9.9.1", - "@intlify/shared": "9.9.1" + "@intlify/message-compiler": "9.10.1", + "@intlify/shared": "9.10.1" }, "engines": { "node": ">= 16" @@ -562,11 +419,11 @@ } }, "node_modules/@intlify/message-compiler": { - "version": "9.9.1", - "resolved": "https://registry.npmjs.org/@intlify/message-compiler/-/message-compiler-9.9.1.tgz", - "integrity": "sha512-zTvP6X6HeumHOXuAE1CMMsV6tTX+opKMOxO1OHTCg5N5Sm/F7d8o2jdT6W6L5oHUsJ/vvkGefHIs7Q3hfowmsA==", + "version": "9.10.1", + "resolved": "https://registry.npmjs.org/@intlify/message-compiler/-/message-compiler-9.10.1.tgz", + "integrity": "sha512-b68UTmRhgZfswJZI7VAgW6BXZK5JOpoi5swMLGr4j6ss2XbFY13kiw+Hu+xYAfulMPSapcHzdWHnq21VGnMCnA==", "dependencies": { - "@intlify/shared": "9.9.1", + "@intlify/shared": "9.10.1", "source-map-js": "^1.0.2" }, "engines": { @@ -577,9 +434,9 @@ } }, "node_modules/@intlify/shared": { - "version": "9.9.1", - "resolved": "https://registry.npmjs.org/@intlify/shared/-/shared-9.9.1.tgz", - "integrity": "sha512-b3Pta1nwkz5rGq434v0psHwEwHGy1pYCttfcM22IE//K9owbpkEvFptx9VcuRAxjQdrO2If249cmDDjBu5wMDA==", + "version": "9.10.1", + "resolved": "https://registry.npmjs.org/@intlify/shared/-/shared-9.10.1.tgz", + "integrity": "sha512-liyH3UMoglHBUn70iCYcy9CQlInx/lp50W2aeSxqqrvmG+LDj/Jj7tBJhBoQL4fECkldGhbmW0g2ommHfL6Wmw==", "engines": { "node": ">= 16" }, @@ -592,40 +449,10 @@ "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.4.15.tgz", "integrity": "sha512-eF2rxCRulEKXHTRiDrDy6erMYWqNw4LPdQ8UQA4huuxaQsVeRPFl2oM8oDGxMFhJUWZf9McpLtJasDDZb/Bpeg==" }, - "node_modules/@nodelib/fs.scandir": { - "version": "2.1.5", - "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", - "integrity": "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==", - "dev": true, - "dependencies": { - "@nodelib/fs.stat": "2.0.5", - "run-parallel": "^1.1.9" - }, - "engines": { - "node": ">= 8" - } - }, - "node_modules/@nodelib/fs.stat": { - "version": "2.0.5", - "resolved": "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz", - "integrity": "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==", - "dev": true, - "engines": { - "node": ">= 8" - } - }, - "node_modules/@nodelib/fs.walk": { - "version": "1.2.8", - "resolved": "https://registry.npmjs.org/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz", - "integrity": "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==", - "dev": true, - "dependencies": { - "@nodelib/fs.scandir": "2.1.5", - "fastq": "^1.6.0" - }, - "engines": { - "node": ">= 8" - } + "node_modules/@js-joda/core": { + "version": "5.6.1", + "resolved": "https://registry.npmjs.org/@js-joda/core/-/core-5.6.1.tgz", + "integrity": "sha512-Xla/d7ZMMR6+zRd6lTio0wRZECfcfFJP7GGe9A9L4tDOlD5CX4YcZ4YZle9w58bBYzssojVapI84RraKWDQZRg==" }, "node_modules/@rollup/rollup-android-arm-eabi": { "version": "4.12.0", @@ -796,6 +623,20 @@ "win32" ] }, + "node_modules/@tauri-apps/api": { + "version": "1.5.3", + "resolved": "https://registry.npmjs.org/@tauri-apps/api/-/api-1.5.3.tgz", + "integrity": "sha512-zxnDjHHKjOsrIzZm6nO5Xapb/BxqUq1tc7cGkFXsFkGTsSWgCPH1D8mm0XS9weJY2OaR73I3k3S+b7eSzJDfqA==", + "engines": { + "node": ">= 14.6.0", + "npm": ">= 6.6.0", + "yarn": ">= 1.19.1" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/tauri" + } + }, "node_modules/@tauri-apps/cli": { "version": "1.5.10", "resolved": "https://registry.npmjs.org/@tauri-apps/cli/-/cli-1.5.10.tgz", @@ -990,12 +831,6 @@ "integrity": "sha512-/kYRxGDLWzHOB7q+wtSUQlFrtcdUccpfy+X+9iMBpHK8QLLhx2wIPYuS5DYtR9Wa/YlZAbIovy7qVdB1Aq6Lyw==", "dev": true }, - "node_modules/@ungap/structured-clone": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/@ungap/structured-clone/-/structured-clone-1.2.0.tgz", - "integrity": "sha512-zuVdFrMJiuCDQUMCzQaD6KL28MjnqqN8XnAqiEq9PNm/hCPTSGfrXCOfwj1ow4LFb/tNymJPwsNbVePc1xFqrQ==", - "dev": true - }, "node_modules/@vitejs/plugin-vue": { "version": "5.0.4", "resolved": "https://registry.npmjs.org/@vitejs/plugin-vue/-/plugin-vue-5.0.4.tgz", @@ -1010,30 +845,30 @@ } }, "node_modules/@volar/language-core": { - "version": "1.11.1", - "resolved": "https://registry.npmjs.org/@volar/language-core/-/language-core-1.11.1.tgz", - "integrity": "sha512-dOcNn3i9GgZAcJt43wuaEykSluAuOkQgzni1cuxLxTV0nJKanQztp7FxyswdRILaKH+P2XZMPRp2S4MV/pElCw==", + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/@volar/language-core/-/language-core-2.1.0.tgz", + "integrity": "sha512-BrYEgYHx92ocpt1OUxJs2x3TAXEjpPLxsQoARb96g2GdF62xnfRQUqCNBwiU7Z3MQ/0tOAdqdHNYNmrFtx6q4A==", "dev": true, "dependencies": { - "@volar/source-map": "1.11.1" + "@volar/source-map": "2.1.0" } }, "node_modules/@volar/source-map": { - "version": "1.11.1", - "resolved": "https://registry.npmjs.org/@volar/source-map/-/source-map-1.11.1.tgz", - "integrity": "sha512-hJnOnwZ4+WT5iupLRnuzbULZ42L7BWWPMmruzwtLhJfpDVoZLjNBxHDi2sY2bgZXCKlpU5XcsMFoYrsQmPhfZg==", + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/@volar/source-map/-/source-map-2.1.0.tgz", + "integrity": "sha512-VPyi+DTv67cvUOkUewzsOQJY3VUhjOjQxigT487z/H7tEI8ZFd5RksC5afk3JelOK+a/3Y8LRDbKmYKu1dz87g==", "dev": true, "dependencies": { - "muggle-string": "^0.3.1" + "muggle-string": "^0.4.0" } }, "node_modules/@volar/typescript": { - "version": "1.11.1", - "resolved": "https://registry.npmjs.org/@volar/typescript/-/typescript-1.11.1.tgz", - "integrity": "sha512-iU+t2mas/4lYierSnoFOeRFQUhAEMgsFuQxoxvwn5EdQopw43j+J27a4lt9LMInx1gLJBC6qL14WYGlgymaSMQ==", + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/@volar/typescript/-/typescript-2.1.0.tgz", + "integrity": "sha512-2cicVoW4q6eU/omqfOBv+6r9JdrF5bBelujbJhayPNKiOj/xwotSJ/DM8IeMvTZvtkOZkm6suyOCLEokLY0w2w==", "dev": true, "dependencies": { - "@volar/language-core": "1.11.1", + "@volar/language-core": "2.1.0", "path-browserify": "^1.0.1" } }, @@ -1089,18 +924,16 @@ "integrity": "sha512-LgPscpE3Vs0x96PzSSB4IGVSZXZBZHpfxs+ZA1d+VEPwHdOXowy/Y2CsvCAIFrf+ssVU1pD1jidj505EpUnfbA==" }, "node_modules/@vue/language-core": { - "version": "1.8.27", - "resolved": "https://registry.npmjs.org/@vue/language-core/-/language-core-1.8.27.tgz", - "integrity": "sha512-L8Kc27VdQserNaCUNiSFdDl9LWT24ly8Hpwf1ECy3aFb9m6bDhBGQYOujDm21N7EW3moKIOKEanQwe1q5BK+mA==", + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/@vue/language-core/-/language-core-2.0.3.tgz", + "integrity": "sha512-hnVF/Q3cD2v+EFD4pD1YdITGBcdM38P18SYqilVQDezKw5RobWny4BwIckWGS1fJmUstsO9mTX30ZOyzyR2Q+Q==", "dev": true, "dependencies": { - "@volar/language-core": "~1.11.1", - "@volar/source-map": "~1.11.1", - "@vue/compiler-dom": "^3.3.0", - "@vue/shared": "^3.3.0", + "@volar/language-core": "~2.1.0", + "@vue/compiler-dom": "^3.4.0", + "@vue/shared": "^3.4.0", "computeds": "^0.0.1", "minimatch": "^9.0.3", - "muggle-string": "^0.3.1", "path-browserify": "^1.0.1", "vue-template-compiler": "^2.7.14" }, @@ -1157,67 +990,6 @@ "resolved": "https://registry.npmjs.org/@vue/shared/-/shared-3.4.21.tgz", "integrity": "sha512-PuJe7vDIi6VYSinuEbUIQgMIRZGgM8e4R+G+/dQTk0X1NEdvgvvgv7m+rfmDH1gZzyA1OjjoWskvHlfRNfQf3g==" }, - "node_modules/acorn": { - "version": "8.11.3", - "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.11.3.tgz", - "integrity": "sha512-Y9rRfJG5jcKOE0CLisYbojUjIrIEE7AGMzA/Sm4BslANhbS+cDMpgBdcPT91oJ7OuJ9hYJBx59RjbhxVnrF8Xg==", - "dev": true, - "bin": { - "acorn": "bin/acorn" - }, - "engines": { - "node": ">=0.4.0" - } - }, - "node_modules/acorn-jsx": { - "version": "5.3.2", - "resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.2.tgz", - "integrity": "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==", - "dev": true, - "peerDependencies": { - "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" - } - }, - "node_modules/ajv": { - "version": "6.12.6", - "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", - "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", - "dev": true, - "dependencies": { - "fast-deep-equal": "^3.1.1", - "fast-json-stable-stringify": "^2.0.0", - "json-schema-traverse": "^0.4.1", - "uri-js": "^4.2.2" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/epoberezkin" - } - }, - "node_modules/ansi-regex": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", - "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", - "dev": true, - "engines": { - "node": ">=8" - } - }, - "node_modules/ansi-styles": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", - "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", - "dev": true, - "dependencies": { - "color-convert": "^2.0.1" - }, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/chalk/ansi-styles?sponsor=1" - } - }, "node_modules/anymatch": { "version": "3.1.3", "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.3.tgz", @@ -1231,12 +1003,6 @@ "node": ">= 8" } }, - "node_modules/argparse": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", - "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", - "dev": true - }, "node_modules/asynckit": { "version": "0.4.0", "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", @@ -1267,12 +1033,6 @@ "node": ">=8" } }, - "node_modules/boolbase": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/boolbase/-/boolbase-1.0.0.tgz", - "integrity": "sha512-JZOSA7Mo9sNGB8+UjSgzdLtokWAky1zbztM3WRLCbZ70/3cTANmQmOdR7y2g+J0e2WXywy1yS468tY+IruqEww==", - "dev": true - }, "node_modules/brace-expansion": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", @@ -1294,31 +1054,6 @@ "node": ">=8" } }, - "node_modules/callsites": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz", - "integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==", - "dev": true, - "engines": { - "node": ">=6" - } - }, - "node_modules/chalk": { - "version": "4.1.2", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", - "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", - "dev": true, - "dependencies": { - "ansi-styles": "^4.1.0", - "supports-color": "^7.1.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/chalk/chalk?sponsor=1" - } - }, "node_modules/chokidar": { "version": "3.6.0", "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.6.0.tgz", @@ -1343,24 +1078,6 @@ "fsevents": "~2.3.2" } }, - "node_modules/color-convert": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", - "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", - "dev": true, - "dependencies": { - "color-name": "~1.1.4" - }, - "engines": { - "node": ">=7.0.0" - } - }, - "node_modules/color-name": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", - "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", - "dev": true - }, "node_modules/combined-stream": { "version": "1.0.8", "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", @@ -1378,38 +1095,6 @@ "integrity": "sha512-7CEBgcMjVmitjYo5q8JTJVra6X5mQ20uTThdK+0kR7UEaDrAWEQcRiBtWJzga4eRpP6afNwwLsX2SET2JhVB1Q==", "dev": true }, - "node_modules/concat-map": { - "version": "0.0.1", - "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", - "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==", - "dev": true - }, - "node_modules/cross-spawn": { - "version": "7.0.3", - "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.3.tgz", - "integrity": "sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w==", - "dev": true, - "dependencies": { - "path-key": "^3.1.0", - "shebang-command": "^2.0.0", - "which": "^2.0.1" - }, - "engines": { - "node": ">= 8" - } - }, - "node_modules/cssesc": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/cssesc/-/cssesc-3.0.0.tgz", - "integrity": "sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg==", - "dev": true, - "bin": { - "cssesc": "bin/cssesc" - }, - "engines": { - "node": ">=4" - } - }, "node_modules/csstype": { "version": "3.1.3", "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.3.tgz", @@ -1421,29 +1106,6 @@ "integrity": "sha512-e/1zu3xH5MQryN2zdVaF0OrdNLUbvWxzMbi+iNA6Bky7l1RoP8a2fIbRocyHclXt/arDrrR6lL3TqFD9pMQTsg==", "dev": true }, - "node_modules/debug": { - "version": "4.3.4", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", - "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==", - "dev": true, - "dependencies": { - "ms": "2.1.2" - }, - "engines": { - "node": ">=6.0" - }, - "peerDependenciesMeta": { - "supports-color": { - "optional": true - } - } - }, - "node_modules/deep-is": { - "version": "0.1.4", - "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz", - "integrity": "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==", - "dev": true - }, "node_modules/delayed-stream": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", @@ -1452,18 +1114,6 @@ "node": ">=0.4.0" } }, - "node_modules/doctrine": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/doctrine/-/doctrine-3.0.0.tgz", - "integrity": "sha512-yS+Q5i3hBf7GBkd4KG8a7eBNNWNGLTaEwwYWUijIYM7zrlYDM0BFXHjjPWlWZ1Rg7UaddZeIDmi9jF3HmqiQ2w==", - "dev": true, - "dependencies": { - "esutils": "^2.0.2" - }, - "engines": { - "node": ">=6.0.0" - } - }, "node_modules/entities": { "version": "4.5.0", "resolved": "https://registry.npmjs.org/entities/-/entities-4.5.0.tgz", @@ -1513,379 +1163,69 @@ "@esbuild/win32-x64": "0.19.12" } }, - "node_modules/escape-string-regexp": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", - "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==", - "dev": true, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } + "node_modules/estree-walker": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-2.0.2.tgz", + "integrity": "sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w==" }, - "node_modules/eslint": { - "version": "8.57.0", - "resolved": "https://registry.npmjs.org/eslint/-/eslint-8.57.0.tgz", - "integrity": "sha512-dZ6+mexnaTIbSBZWgou51U6OmzIhYM2VcNdtiTtI7qPNZm35Akpr0f6vtw3w1Kmn5PYo+tZVfh13WrhpS6oLqQ==", + "node_modules/fill-range": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.0.1.tgz", + "integrity": "sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ==", "dev": true, "dependencies": { - "@eslint-community/eslint-utils": "^4.2.0", - "@eslint-community/regexpp": "^4.6.1", - "@eslint/eslintrc": "^2.1.4", - "@eslint/js": "8.57.0", - "@humanwhocodes/config-array": "^0.11.14", - "@humanwhocodes/module-importer": "^1.0.1", - "@nodelib/fs.walk": "^1.2.8", - "@ungap/structured-clone": "^1.2.0", - "ajv": "^6.12.4", - "chalk": "^4.0.0", - "cross-spawn": "^7.0.2", - "debug": "^4.3.2", - "doctrine": "^3.0.0", - "escape-string-regexp": "^4.0.0", - "eslint-scope": "^7.2.2", - "eslint-visitor-keys": "^3.4.3", - "espree": "^9.6.1", - "esquery": "^1.4.2", - "esutils": "^2.0.2", - "fast-deep-equal": "^3.1.3", - "file-entry-cache": "^6.0.1", - "find-up": "^5.0.0", - "glob-parent": "^6.0.2", - "globals": "^13.19.0", - "graphemer": "^1.4.0", - "ignore": "^5.2.0", - "imurmurhash": "^0.1.4", - "is-glob": "^4.0.0", - "is-path-inside": "^3.0.3", - "js-yaml": "^4.1.0", - "json-stable-stringify-without-jsonify": "^1.0.1", - "levn": "^0.4.1", - "lodash.merge": "^4.6.2", - "minimatch": "^3.1.2", - "natural-compare": "^1.4.0", - "optionator": "^0.9.3", - "strip-ansi": "^6.0.1", - "text-table": "^0.2.0" - }, - "bin": { - "eslint": "bin/eslint.js" + "to-regex-range": "^5.0.1" }, "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" - }, - "funding": { - "url": "https://opencollective.com/eslint" + "node": ">=8" } }, - "node_modules/eslint-plugin-vue": { - "version": "9.22.0", - "resolved": "https://registry.npmjs.org/eslint-plugin-vue/-/eslint-plugin-vue-9.22.0.tgz", - "integrity": "sha512-7wCXv5zuVnBtZE/74z4yZ0CM8AjH6bk4MQGm7hZjUC2DBppKU5ioeOk5LGSg/s9a1ZJnIsdPLJpXnu1Rc+cVHg==", - "dev": true, - "dependencies": { - "@eslint-community/eslint-utils": "^4.4.0", - "natural-compare": "^1.4.0", - "nth-check": "^2.1.1", - "postcss-selector-parser": "^6.0.15", - "semver": "^7.6.0", - "vue-eslint-parser": "^9.4.2", - "xml-name-validator": "^4.0.0" - }, + "node_modules/follow-redirects": { + "version": "1.15.5", + "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.5.tgz", + "integrity": "sha512-vSFWUON1B+yAw1VN4xMfxgn5fTUiaOzAJCKBwIIgT/+7CuGy9+r+5gITvP62j3RmaD5Ph65UaERdOSRGUzZtgw==", + "funding": [ + { + "type": "individual", + "url": "https://github.com/sponsors/RubenVerborgh" + } + ], "engines": { - "node": "^14.17.0 || >=16.0.0" + "node": ">=4.0" }, - "peerDependencies": { - "eslint": "^6.2.0 || ^7.0.0 || ^8.0.0" + "peerDependenciesMeta": { + "debug": { + "optional": true + } } }, - "node_modules/eslint-scope": { - "version": "7.2.2", - "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-7.2.2.tgz", - "integrity": "sha512-dOt21O7lTMhDM+X9mB4GX+DZrZtCUJPL/wlcTqxyrx5IvO0IYtILdtrQGQp+8n5S0gwSVmOf9NQrjMOgfQZlIg==", - "dev": true, + "node_modules/form-data": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.0.tgz", + "integrity": "sha512-ETEklSGi5t0QMZuiXoA/Q6vcnxcLQP5vdugSpuAyi6SVGi2clPPp+xgEhuMaHC+zGgn31Kd235W35f7Hykkaww==", "dependencies": { - "esrecurse": "^4.3.0", - "estraverse": "^5.2.0" + "asynckit": "^0.4.0", + "combined-stream": "^1.0.8", + "mime-types": "^2.1.12" }, "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" - }, - "funding": { - "url": "https://opencollective.com/eslint" + "node": ">= 6" } }, - "node_modules/eslint-visitor-keys": { - "version": "3.4.3", - "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.4.3.tgz", - "integrity": "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==", + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", "dev": true, - "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" - }, - "funding": { - "url": "https://opencollective.com/eslint" - } - }, - "node_modules/eslint/node_modules/brace-expansion": { - "version": "1.1.11", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", - "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", - "dev": true, - "dependencies": { - "balanced-match": "^1.0.0", - "concat-map": "0.0.1" - } - }, - "node_modules/eslint/node_modules/glob-parent": { - "version": "6.0.2", - "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz", - "integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==", - "dev": true, - "dependencies": { - "is-glob": "^4.0.3" - }, - "engines": { - "node": ">=10.13.0" - } - }, - "node_modules/eslint/node_modules/minimatch": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", - "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", - "dev": true, - "dependencies": { - "brace-expansion": "^1.1.7" - }, - "engines": { - "node": "*" - } - }, - "node_modules/espree": { - "version": "9.6.1", - "resolved": "https://registry.npmjs.org/espree/-/espree-9.6.1.tgz", - "integrity": "sha512-oruZaFkjorTpF32kDSI5/75ViwGeZginGGy2NoOSg3Q9bnwlnmDm4HLnkl0RE3n+njDXR037aY1+x58Z/zFdwQ==", - "dev": true, - "dependencies": { - "acorn": "^8.9.0", - "acorn-jsx": "^5.3.2", - "eslint-visitor-keys": "^3.4.1" - }, - "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" - }, - "funding": { - "url": "https://opencollective.com/eslint" - } - }, - "node_modules/esquery": { - "version": "1.5.0", - "resolved": "https://registry.npmjs.org/esquery/-/esquery-1.5.0.tgz", - "integrity": "sha512-YQLXUplAwJgCydQ78IMJywZCceoqk1oH01OERdSAJc/7U2AylwjhSCLDEtqwg811idIS/9fIU5GjG73IgjKMVg==", - "dev": true, - "dependencies": { - "estraverse": "^5.1.0" - }, - "engines": { - "node": ">=0.10" - } - }, - "node_modules/esrecurse": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/esrecurse/-/esrecurse-4.3.0.tgz", - "integrity": "sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==", - "dev": true, - "dependencies": { - "estraverse": "^5.2.0" - }, - "engines": { - "node": ">=4.0" - } - }, - "node_modules/estraverse": { - "version": "5.3.0", - "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz", - "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==", - "dev": true, - "engines": { - "node": ">=4.0" - } - }, - "node_modules/estree-walker": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-2.0.2.tgz", - "integrity": "sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w==" - }, - "node_modules/esutils": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz", - "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==", - "dev": true, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/fast-deep-equal": { - "version": "3.1.3", - "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", - "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", - "dev": true - }, - "node_modules/fast-json-stable-stringify": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", - "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==", - "dev": true - }, - "node_modules/fast-levenshtein": { - "version": "2.0.6", - "resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz", - "integrity": "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==", - "dev": true - }, - "node_modules/fastq": { - "version": "1.17.1", - "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.17.1.tgz", - "integrity": "sha512-sRVD3lWVIXWg6By68ZN7vho9a1pQcN/WBFaAAsDDFzlJjvoGx0P8z7V1t72grFJfJhu3YPZBuu25f7Kaw2jN1w==", - "dev": true, - "dependencies": { - "reusify": "^1.0.4" - } - }, - "node_modules/file-entry-cache": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-6.0.1.tgz", - "integrity": "sha512-7Gps/XWymbLk2QLYK4NzpMOrYjMhdIxXuIvy2QBsLE6ljuodKvdkWs/cpyJJ3CVIVpH0Oi1Hvg1ovbMzLdFBBg==", - "dev": true, - "dependencies": { - "flat-cache": "^3.0.4" - }, - "engines": { - "node": "^10.12.0 || >=12.0.0" - } - }, - "node_modules/fill-range": { - "version": "7.0.1", - "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.0.1.tgz", - "integrity": "sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ==", - "dev": true, - "dependencies": { - "to-regex-range": "^5.0.1" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/find-up": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz", - "integrity": "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==", - "dev": true, - "dependencies": { - "locate-path": "^6.0.0", - "path-exists": "^4.0.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/flat-cache": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-3.2.0.tgz", - "integrity": "sha512-CYcENa+FtcUKLmhhqyctpclsq7QF38pKjZHsGNiSQF5r4FtoKDWabFDl3hzaEQMvT1LHEysw5twgLvpYYb4vbw==", - "dev": true, - "dependencies": { - "flatted": "^3.2.9", - "keyv": "^4.5.3", - "rimraf": "^3.0.2" - }, - "engines": { - "node": "^10.12.0 || >=12.0.0" - } - }, - "node_modules/flatted": { - "version": "3.3.1", - "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.3.1.tgz", - "integrity": "sha512-X8cqMLLie7KsNUDSdzeN8FYK9rEt4Dt67OsG/DNGnYTSDBG4uFAJFBnUeiV+zCVAvwFy56IjM9sH51jVaEhNxw==", - "dev": true - }, - "node_modules/follow-redirects": { - "version": "1.15.5", - "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.5.tgz", - "integrity": "sha512-vSFWUON1B+yAw1VN4xMfxgn5fTUiaOzAJCKBwIIgT/+7CuGy9+r+5gITvP62j3RmaD5Ph65UaERdOSRGUzZtgw==", - "funding": [ - { - "type": "individual", - "url": "https://github.com/sponsors/RubenVerborgh" - } - ], - "engines": { - "node": ">=4.0" - }, - "peerDependenciesMeta": { - "debug": { - "optional": true - } - } - }, - "node_modules/form-data": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.0.tgz", - "integrity": "sha512-ETEklSGi5t0QMZuiXoA/Q6vcnxcLQP5vdugSpuAyi6SVGi2clPPp+xgEhuMaHC+zGgn31Kd235W35f7Hykkaww==", - "dependencies": { - "asynckit": "^0.4.0", - "combined-stream": "^1.0.8", - "mime-types": "^2.1.12" - }, - "engines": { - "node": ">= 6" - } - }, - "node_modules/fs.realpath": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", - "integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==", - "dev": true - }, - "node_modules/fsevents": { - "version": "2.3.3", - "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", - "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", - "dev": true, - "hasInstallScript": true, - "optional": true, - "os": [ - "darwin" - ], + "hasInstallScript": true, + "optional": true, + "os": [ + "darwin" + ], "engines": { "node": "^8.16.0 || ^10.6.0 || >=11.0.0" } }, - "node_modules/glob": { - "version": "7.2.3", - "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", - "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", - "dev": true, - "dependencies": { - "fs.realpath": "^1.0.0", - "inflight": "^1.0.4", - "inherits": "2", - "minimatch": "^3.1.1", - "once": "^1.3.0", - "path-is-absolute": "^1.0.0" - }, - "engines": { - "node": "*" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, "node_modules/glob-parent": { "version": "5.1.2", "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", @@ -1898,58 +1238,6 @@ "node": ">= 6" } }, - "node_modules/glob/node_modules/brace-expansion": { - "version": "1.1.11", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", - "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", - "dev": true, - "dependencies": { - "balanced-match": "^1.0.0", - "concat-map": "0.0.1" - } - }, - "node_modules/glob/node_modules/minimatch": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", - "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", - "dev": true, - "dependencies": { - "brace-expansion": "^1.1.7" - }, - "engines": { - "node": "*" - } - }, - "node_modules/globals": { - "version": "13.24.0", - "resolved": "https://registry.npmjs.org/globals/-/globals-13.24.0.tgz", - "integrity": "sha512-AhO5QUcj8llrbG09iWhPU2B204J1xnPeL8kQmVorSsy+Sjj1sk8gIyh6cUocGmH4L0UuhAJy+hJMRA4mgA4mFQ==", - "dev": true, - "dependencies": { - "type-fest": "^0.20.2" - }, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/graphemer": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/graphemer/-/graphemer-1.4.0.tgz", - "integrity": "sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag==", - "dev": true - }, - "node_modules/has-flag": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", - "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", - "dev": true, - "engines": { - "node": ">=8" - } - }, "node_modules/he": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/he/-/he-1.2.0.tgz", @@ -1959,62 +1247,12 @@ "he": "bin/he" } }, - "node_modules/ignore": { - "version": "5.3.1", - "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.1.tgz", - "integrity": "sha512-5Fytz/IraMjqpwfd34ke28PTVMjZjJG2MPn5t7OE4eUCUNf8BAa7b5WUS9/Qvr6mwOQS7Mk6vdsMno5he+T8Xw==", - "dev": true, - "engines": { - "node": ">= 4" - } - }, "node_modules/immutable": { "version": "4.3.5", "resolved": "https://registry.npmjs.org/immutable/-/immutable-4.3.5.tgz", "integrity": "sha512-8eabxkth9gZatlwl5TBuJnCsoTADlL6ftEr7A4qgdaTsPyreilDSnUk57SO+jfKcNtxPa22U5KK6DSeAYhpBJw==", "dev": true }, - "node_modules/import-fresh": { - "version": "3.3.0", - "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.0.tgz", - "integrity": "sha512-veYYhQa+D1QBKznvhUHxb8faxlrwUnxseDAbAp457E0wLNio2bOSKnjYDhMj+YiAq61xrMGhQk9iXVk5FzgQMw==", - "dev": true, - "dependencies": { - "parent-module": "^1.0.0", - "resolve-from": "^4.0.0" - }, - "engines": { - "node": ">=6" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/imurmurhash": { - "version": "0.1.4", - "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", - "integrity": "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==", - "dev": true, - "engines": { - "node": ">=0.8.19" - } - }, - "node_modules/inflight": { - "version": "1.0.6", - "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", - "integrity": "sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==", - "dev": true, - "dependencies": { - "once": "^1.3.0", - "wrappy": "1" - } - }, - "node_modules/inherits": { - "version": "2.0.4", - "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", - "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", - "dev": true - }, "node_modules/is-binary-path": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz", @@ -2057,100 +1295,6 @@ "node": ">=0.12.0" } }, - "node_modules/is-path-inside": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/is-path-inside/-/is-path-inside-3.0.3.tgz", - "integrity": "sha512-Fd4gABb+ycGAmKou8eMftCupSir5lRxqf4aD/vd0cD2qc4HL07OjCeuHMr8Ro4CoMaeCKDB0/ECBOVWjTwUvPQ==", - "dev": true, - "engines": { - "node": ">=8" - } - }, - "node_modules/isexe": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", - "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", - "dev": true - }, - "node_modules/js-yaml": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz", - "integrity": "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==", - "dev": true, - "dependencies": { - "argparse": "^2.0.1" - }, - "bin": { - "js-yaml": "bin/js-yaml.js" - } - }, - "node_modules/json-buffer": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.1.tgz", - "integrity": "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==", - "dev": true - }, - "node_modules/json-schema-traverse": { - "version": "0.4.1", - "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", - "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", - "dev": true - }, - "node_modules/json-stable-stringify-without-jsonify": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz", - "integrity": "sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==", - "dev": true - }, - "node_modules/keyv": { - "version": "4.5.4", - "resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz", - "integrity": "sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==", - "dev": true, - "dependencies": { - "json-buffer": "3.0.1" - } - }, - "node_modules/levn": { - "version": "0.4.1", - "resolved": "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz", - "integrity": "sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==", - "dev": true, - "dependencies": { - "prelude-ls": "^1.2.1", - "type-check": "~0.4.0" - }, - "engines": { - "node": ">= 0.8.0" - } - }, - "node_modules/locate-path": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz", - "integrity": "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==", - "dev": true, - "dependencies": { - "p-locate": "^5.0.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/lodash": { - "version": "4.17.21", - "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz", - "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==", - "dev": true - }, - "node_modules/lodash.merge": { - "version": "4.6.2", - "resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz", - "integrity": "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==", - "dev": true - }, "node_modules/lru-cache": { "version": "6.0.0", "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", @@ -2164,9 +1308,9 @@ } }, "node_modules/magic-string": { - "version": "0.30.8", - "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.8.tgz", - "integrity": "sha512-ISQTe55T2ao7XtlAStud6qwYPZjE4GK1S/BeVPus4jrq6JuOnQ00YKQC581RWhR122W7msZV263KzVeLoqidyQ==", + "version": "0.30.7", + "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.7.tgz", + "integrity": "sha512-8vBuFF/I/+OSLRmdf2wwFCJCz+nSn0m6DPvGH1fS/KiQoSaR+sETbov0eIk9KhEKy8CYqIkIAnbohxT/4H0kuA==", "dependencies": { "@jridgewell/sourcemap-codec": "^1.4.15" }, @@ -2208,16 +1352,10 @@ "url": "https://github.com/sponsors/isaacs" } }, - "node_modules/ms": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", - "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", - "dev": true - }, "node_modules/muggle-string": { - "version": "0.3.1", - "resolved": "https://registry.npmjs.org/muggle-string/-/muggle-string-0.3.1.tgz", - "integrity": "sha512-ckmWDJjphvd/FvZawgygcUeQCxzvohjFO5RxTjj4eq8kw359gFF3E1brjfI+viLMxss5JrHTDRHZvu2/tuy0Qg==", + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/muggle-string/-/muggle-string-0.4.1.tgz", + "integrity": "sha512-VNTrAak/KhO2i8dqqnqnAHOa3cYBwXEZe9h+D5h/1ZqFSTEFHdM65lR7RoIqq3tBBYavsOXV84NoHXZ0AkPyqQ==", "dev": true }, "node_modules/nanoid": { @@ -2237,12 +1375,6 @@ "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" } }, - "node_modules/natural-compare": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz", - "integrity": "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==", - "dev": true - }, "node_modules/normalize-path": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", @@ -2252,119 +1384,12 @@ "node": ">=0.10.0" } }, - "node_modules/nth-check": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/nth-check/-/nth-check-2.1.1.tgz", - "integrity": "sha512-lqjrjmaOoAnWfMmBPL+XNnynZh2+swxiX3WUE0s4yEHI6m+AwrK2UZOimIRl3X/4QctVqS8AiZjFqyOGrMXb/w==", - "dev": true, - "dependencies": { - "boolbase": "^1.0.0" - }, - "funding": { - "url": "https://github.com/fb55/nth-check?sponsor=1" - } - }, - "node_modules/once": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", - "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", - "dev": true, - "dependencies": { - "wrappy": "1" - } - }, - "node_modules/optionator": { - "version": "0.9.3", - "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.3.tgz", - "integrity": "sha512-JjCoypp+jKn1ttEFExxhetCKeJt9zhAgAve5FXHixTvFDW/5aEktX9bufBKLRRMdU7bNtpLfcGu94B3cdEJgjg==", - "dev": true, - "dependencies": { - "@aashutoshrathi/word-wrap": "^1.2.3", - "deep-is": "^0.1.3", - "fast-levenshtein": "^2.0.6", - "levn": "^0.4.1", - "prelude-ls": "^1.2.1", - "type-check": "^0.4.0" - }, - "engines": { - "node": ">= 0.8.0" - } - }, - "node_modules/p-limit": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", - "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==", - "dev": true, - "dependencies": { - "yocto-queue": "^0.1.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/p-locate": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-5.0.0.tgz", - "integrity": "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==", - "dev": true, - "dependencies": { - "p-limit": "^3.0.2" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/parent-module": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz", - "integrity": "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==", - "dev": true, - "dependencies": { - "callsites": "^3.0.0" - }, - "engines": { - "node": ">=6" - } - }, "node_modules/path-browserify": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/path-browserify/-/path-browserify-1.0.1.tgz", "integrity": "sha512-b7uo2UCUOYZcnF/3ID0lulOJi/bafxa1xPe7ZPsammBSpjSWQkjNxlt635YGS2MiR9GjvuXCtz2emr3jbsz98g==", "dev": true }, - "node_modules/path-exists": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", - "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", - "dev": true, - "engines": { - "node": ">=8" - } - }, - "node_modules/path-is-absolute": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", - "integrity": "sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==", - "dev": true, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/path-key": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", - "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", - "dev": true, - "engines": { - "node": ">=8" - } - }, "node_modules/picocolors": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.0.0.tgz", @@ -2409,77 +1434,11 @@ "node": "^10 || ^12 || >=14" } }, - "node_modules/postcss-selector-parser": { - "version": "6.0.15", - "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-6.0.15.tgz", - "integrity": "sha512-rEYkQOMUCEMhsKbK66tbEU9QVIxbhN18YiniAwA7XQYTVBqrBy+P2p5JcdqsHgKM2zWylp8d7J6eszocfds5Sw==", - "dev": true, - "dependencies": { - "cssesc": "^3.0.0", - "util-deprecate": "^1.0.2" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/prelude-ls": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz", - "integrity": "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==", - "dev": true, - "engines": { - "node": ">= 0.8.0" - } - }, - "node_modules/prettier": { - "version": "3.2.5", - "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.2.5.tgz", - "integrity": "sha512-3/GWa9aOC0YeD7LUfvOG2NiDyhOWRvt1k+rcKhOuYnMY24iiCphgneUfJDyFXd6rZCAnuLBv6UeAULtrhT/F4A==", - "dev": true, - "bin": { - "prettier": "bin/prettier.cjs" - }, - "engines": { - "node": ">=14" - }, - "funding": { - "url": "https://github.com/prettier/prettier?sponsor=1" - } - }, "node_modules/proxy-from-env": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz", "integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==" }, - "node_modules/punycode": { - "version": "2.3.1", - "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", - "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==", - "dev": true, - "engines": { - "node": ">=6" - } - }, - "node_modules/queue-microtask": { - "version": "1.2.3", - "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz", - "integrity": "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==", - "dev": true, - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/feross" - }, - { - "type": "patreon", - "url": "https://www.patreon.com/feross" - }, - { - "type": "consulting", - "url": "https://feross.org/support" - } - ] - }, "node_modules/readdirp": { "version": "3.6.0", "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz", @@ -2492,40 +1451,6 @@ "node": ">=8.10.0" } }, - "node_modules/resolve-from": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz", - "integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==", - "dev": true, - "engines": { - "node": ">=4" - } - }, - "node_modules/reusify": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.0.4.tgz", - "integrity": "sha512-U9nH88a3fc/ekCF1l0/UP1IosiuIjyTh7hBvXVMHYgVcfGvt897Xguj2UOLDeI5BG2m7/uwyaLVT6fbtCwTyzw==", - "dev": true, - "engines": { - "iojs": ">=1.0.0", - "node": ">=0.10.0" - } - }, - "node_modules/rimraf": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz", - "integrity": "sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==", - "dev": true, - "dependencies": { - "glob": "^7.1.3" - }, - "bin": { - "rimraf": "bin.js" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, "node_modules/rollup": { "version": "4.12.0", "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.12.0.tgz", @@ -2558,29 +1483,6 @@ "fsevents": "~2.3.2" } }, - "node_modules/run-parallel": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz", - "integrity": "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==", - "dev": true, - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/feross" - }, - { - "type": "patreon", - "url": "https://www.patreon.com/feross" - }, - { - "type": "consulting", - "url": "https://feross.org/support" - } - ], - "dependencies": { - "queue-microtask": "^1.2.2" - } - }, "node_modules/sass": { "version": "1.71.1", "resolved": "https://registry.npmjs.org/sass/-/sass-1.71.1.tgz", @@ -2613,27 +1515,6 @@ "node": ">=10" } }, - "node_modules/shebang-command": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", - "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", - "dev": true, - "dependencies": { - "shebang-regex": "^3.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/shebang-regex": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", - "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", - "dev": true, - "engines": { - "node": ">=8" - } - }, "node_modules/source-map-js": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.0.2.tgz", @@ -2642,48 +1523,6 @@ "node": ">=0.10.0" } }, - "node_modules/strip-ansi": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", - "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", - "dev": true, - "dependencies": { - "ansi-regex": "^5.0.1" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/strip-json-comments": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz", - "integrity": "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==", - "dev": true, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/supports-color": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", - "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", - "dev": true, - "dependencies": { - "has-flag": "^4.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/text-table": { - "version": "0.2.0", - "resolved": "https://registry.npmjs.org/text-table/-/text-table-0.2.0.tgz", - "integrity": "sha512-N+8UisAXDGk8PFXP4HAzVR9nbfmVJ3zYLAWiTIoqC5v5isinhr+r5uaO8+7r3BMfuNIufIsA7RdpVgacC2cSpw==", - "dev": true - }, "node_modules/to-regex-range": { "version": "5.0.1", "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", @@ -2696,30 +1535,6 @@ "node": ">=8.0" } }, - "node_modules/type-check": { - "version": "0.4.0", - "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz", - "integrity": "sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==", - "dev": true, - "dependencies": { - "prelude-ls": "^1.2.1" - }, - "engines": { - "node": ">= 0.8.0" - } - }, - "node_modules/type-fest": { - "version": "0.20.2", - "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.20.2.tgz", - "integrity": "sha512-Ne+eE4r0/iWnpAxD852z3A+N0Bt5RN//NjJwRd2VFHEmrywxf5vsZlh4R6lixl6B+wz/8d+maTSAkN1FIkI3LQ==", - "dev": true, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, "node_modules/typescript": { "version": "5.3.3", "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.3.3.tgz", @@ -2733,21 +1548,6 @@ "node": ">=14.17" } }, - "node_modules/uri-js": { - "version": "4.4.1", - "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz", - "integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==", - "dev": true, - "dependencies": { - "punycode": "^2.1.0" - } - }, - "node_modules/util-deprecate": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", - "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==", - "dev": true - }, "node_modules/vite": { "version": "5.1.4", "resolved": "https://registry.npmjs.org/vite/-/vite-5.1.4.tgz", @@ -2823,37 +1623,13 @@ } } }, - "node_modules/vue-eslint-parser": { - "version": "9.4.2", - "resolved": "https://registry.npmjs.org/vue-eslint-parser/-/vue-eslint-parser-9.4.2.tgz", - "integrity": "sha512-Ry9oiGmCAK91HrKMtCrKFWmSFWvYkpGglCeFAIqDdr9zdXmMMpJOmUJS7WWsW7fX81h6mwHmUZCQQ1E0PkSwYQ==", - "dev": true, - "dependencies": { - "debug": "^4.3.4", - "eslint-scope": "^7.1.1", - "eslint-visitor-keys": "^3.3.0", - "espree": "^9.3.1", - "esquery": "^1.4.0", - "lodash": "^4.17.21", - "semver": "^7.3.6" - }, - "engines": { - "node": "^14.17.0 || >=16.0.0" - }, - "funding": { - "url": "https://github.com/sponsors/mysticatea" - }, - "peerDependencies": { - "eslint": ">=6.0.0" - } - }, "node_modules/vue-i18n": { - "version": "9.9.1", - "resolved": "https://registry.npmjs.org/vue-i18n/-/vue-i18n-9.9.1.tgz", - "integrity": "sha512-xyQ4VspLdNSPTKBFBPWa1tvtj+9HuockZwgFeD2OhxxXuC2CWeNvV4seu2o9+vbQOyQbhAM5Ez56oxUrrnTWdw==", + "version": "9.10.1", + "resolved": "https://registry.npmjs.org/vue-i18n/-/vue-i18n-9.10.1.tgz", + "integrity": "sha512-37HVJQZ/pZaRXGzFmmMomM1u1k7kndv3xCBPYHKEVfv5W3UVK67U/TpBug71ILYLNmjHLHdvTUPRF81pFT5fFg==", "dependencies": { - "@intlify/core-base": "9.9.1", - "@intlify/shared": "9.9.1", + "@intlify/core-base": "9.10.1", + "@intlify/shared": "9.10.1", "@vue/devtools-api": "^6.5.0" }, "engines": { @@ -2892,13 +1668,13 @@ } }, "node_modules/vue-tsc": { - "version": "1.8.27", - "resolved": "https://registry.npmjs.org/vue-tsc/-/vue-tsc-1.8.27.tgz", - "integrity": "sha512-WesKCAZCRAbmmhuGl3+VrdWItEvfoFIPXOvUJkjULi+x+6G/Dy69yO3TBRJDr9eUlmsNAwVmxsNZxvHKzbkKdg==", + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/vue-tsc/-/vue-tsc-2.0.3.tgz", + "integrity": "sha512-aMJqbgLiKDAwAglWqMoGf1Ez6Wwqhlk2MDxEjFGziiLW0A+tHOWE1+YQJZQ1Vm6zaENPA2KJAubFhaR988UvGg==", "dev": true, "dependencies": { - "@volar/typescript": "~1.11.1", - "@vue/language-core": "1.8.27", + "@volar/typescript": "~2.1.0", + "@vue/language-core": "2.0.3", "semver": "^7.5.4" }, "bin": { @@ -2908,53 +1684,11 @@ "typescript": "*" } }, - "node_modules/which": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", - "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", - "dev": true, - "dependencies": { - "isexe": "^2.0.0" - }, - "bin": { - "node-which": "bin/node-which" - }, - "engines": { - "node": ">= 8" - } - }, - "node_modules/wrappy": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", - "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", - "dev": true - }, - "node_modules/xml-name-validator": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/xml-name-validator/-/xml-name-validator-4.0.0.tgz", - "integrity": "sha512-ICP2e+jsHvAj2E2lIHxa5tjXRlKDJo4IdvPvCXbXQGdzSfmSpNVyIKMvoZHjDY9DP0zV17iI85o90vRFXNccRw==", - "dev": true, - "engines": { - "node": ">=12" - } - }, "node_modules/yallist": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", "dev": true - }, - "node_modules/yocto-queue": { - "version": "0.1.0", - "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", - "integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==", - "dev": true, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } } } } diff --git a/frontend/package.json b/frontend/package.json index 95bf4219..ae70d817 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -1,7 +1,7 @@ { "name": "betterfleet", "private": true, - "version": "0.0.0", + "version": "0.1.0", "type": "module", "scripts": { "dev": "vite --host", @@ -12,20 +12,19 @@ "tauri-build": "npm run tauri build" }, "dependencies": { + "@js-joda/core": "^5.6.1", + "@tauri-apps/api": "^1.5.3", "axios": "^1.6.7", "vue": "^3.4.21", - "vue-i18n": "^9.8.0" + "vue-i18n": "^9.10.1" }, "devDependencies": { "@tauri-apps/cli": "^1.5.10", "@vitejs/plugin-vue": "^5.0.4", - "eslint": "^8.57.0", - "eslint-plugin-vue": "^9.22.0", "sass": "^1.71.1", "typescript": "^5.2.2", "vite": "^5.1.4", - "prettier": "^3.2.5", "vue-router": "^4.2.2", - "vue-tsc": "^1.8.27" + "vue-tsc": "^2.0.3" } } diff --git a/frontend/src-tauri/Cargo.toml b/frontend/src-tauri/Cargo.toml index f1c3cb4f..0b458139 100644 --- a/frontend/src-tauri/Cargo.toml +++ b/frontend/src-tauri/Cargo.toml @@ -1,7 +1,7 @@ [package] name = "better_fleet" version = "0.1.0" -description = "To kill FleetCreator" +description = "A better fleet creator" authors = ["Zelytra", "dadodasyra"] license = "" repository = "" @@ -17,7 +17,15 @@ tauri-build = { version = "1.5.1", features = [] } [dependencies] serde_json = "1.0" serde = { version = "1.0", features = ["derive"] } -tauri = { version = "1.6.0", features = [] } +tauri = { version = "1.6.0", features = [ "updater", "shell-all"] } +anyhow = "1.0.80" +socket2 = "0.5.6" +tokio = { version = "1.36.0", features = ["full"] } +winapi = { version = "0.3.9", features = ["winsock2", "winuser"] } +etherparse = "0.14.2" +hostname = "0.3" +sysinfo = "0.30.6" +netstat2 = "0.9.1" [features] # this feature is used for production builds or when `devPath` points to the filesystem and the built-in dev server is disabled. diff --git a/frontend/src-tauri/src/api.rs b/frontend/src-tauri/src/api.rs new file mode 100644 index 00000000..f887cbb7 --- /dev/null +++ b/frontend/src-tauri/src/api.rs @@ -0,0 +1,66 @@ +use std::time::Instant; +use serde::Serialize; + +// This whole file is used to cache data from thread and expose it to the frontend. +#[derive(Clone)] +pub struct Api { + pub game_status: GameStatus, + pub server_ip: String, + pub server_port: u16, + pub last_updated_server_ip: Instant, + pub main_menu_port: u16 +} + +#[derive(PartialEq, Debug, Clone, Serialize)] +pub enum GameStatus { + Closed, // Game is closed + Started, // Game is in first menu after launch / launching / stopping + MainMenu, // In menu to select game mode + InGame, // Status when the remote IP and port was found and player is in game + Unknown // Default / errored status, this should not be used +} + +impl Api { + pub fn new() -> Self { + Self { + game_status: GameStatus::Unknown, + server_ip: String::new(), + server_port: 0, + last_updated_server_ip: Instant::now(), + main_menu_port: 0, + } + } + + /** + * This is the most accurate info you can get about what's going on. + * It's updated AT LEAST every 5 seconds. (1-5 secs) + * This information is prioritized over the others. + */ + pub async fn get_game_status(&self) -> GameStatus { + self.game_status.clone() + } + + /** + * Server IP, should only be used when GameStatus is InGame. + */ + pub async fn get_server_ip(&self) -> String { + self.server_ip.clone() + } + + + /** + * Server port, should only be used when GameStatus is InGame. + */ + pub async fn get_server_port(&self) -> u16 { + self.server_port + } + + /** + * This corresponds to a timestamp of the last time the server IP was updated. + * This may be used to check if the thread crashed / if something gones wrong. + */ + pub async fn get_last_updated_server_ip(&self) -> Instant { + self.last_updated_server_ip + } +} + diff --git a/frontend/src-tauri/src/fetch_informations.rs b/frontend/src-tauri/src/fetch_informations.rs new file mode 100644 index 00000000..160c948d --- /dev/null +++ b/frontend/src-tauri/src/fetch_informations.rs @@ -0,0 +1,383 @@ +use std::io::{BufRead, BufReader}; +use std::string::String; +use std::sync::Arc; +use tokio::sync::RwLock; +use crate::api::Api; +use etherparse::PacketHeaders; +use anyhow::{Result, bail}; +use std::time::{Duration, Instant}; +use socket2::{Domain, Protocol, Socket, Type}; +use std::mem::size_of_val; +use std::net::{IpAddr, SocketAddr, ToSocketAddrs}; +use std::net::UdpSocket as StdSocket; +use tokio::net::UdpSocket; +use std::os::windows::io::{AsRawSocket, FromRawSocket, IntoRawSocket}; +use std::os::windows::process::CommandExt; +use std::process::{Command, Stdio}; +use std::ptr::null_mut; +use netstat2::{get_sockets_info, AddressFamilyFlags, ProtocolFlags, ProtocolSocketInfo}; +use winapi::shared::minwindef::DWORD; +use winapi::um::winsock2; +use crate::api::GameStatus; +use sysinfo::{System}; + +const SIO_RCVALL: DWORD = 0x98000001; + + +pub async fn init() -> std::result::Result>, anyhow::Error> { + let api_base = Arc::new(RwLock::new(Api::new())); + let api = Arc::clone(&api_base); + + tokio::spawn(async move { + loop { + // Fetch pid game + let pid = find_pid_of("SoTGame.exe"); + + if pid.is_empty() { + api.write().await.game_status = GameStatus::Closed; + } else { + let pid = pid[0].parse().unwrap(); + // List of udp sockets used by the game + let udp_connections = get_udp_connections(pid); + + // Update game_status + if udp_connections.len() == 0 { // 0 = First menu/launching + api.write().await.game_status = GameStatus::Started; + } else if udp_connections.len() == 1 { // Main menu + api.write().await.game_status = GameStatus::MainMenu; + api.write().await.main_menu_port = udp_connections[0]; + } else if udp_connections.len() == 2 { // 2 sockets = connected to a server + // Get UDP Listen port, that the other one that is not main_menu_port + let mut listen_port = udp_connections[0]; + + let main_menu_port = api.read().await.main_menu_port; + if main_menu_port == 0 { + // This may happen when BetterFleet was launched after the connection to the server + // So we use the old technic of netstat powershell which output in order, mainmenu = first udp socket + println!("Using netstat powershell to get main_menu_port"); + let udp_connections = get_udp_connections_powershell(pid); + println!("{:?}", udp_connections); + if udp_connections.len() == 2 { + listen_port = udp_connections[1]; + api.write().await.main_menu_port = udp_connections[0]; + } + } else if udp_connections[0] == main_menu_port { + listen_port = udp_connections[1]; + } + + // Get hostname + let hostname = match get_local_hostname() { + Ok(hn) => hn, + Err(e) => { + eprintln!("Error getting local hostname: {}", e); + continue; + } + }; + + // We need to add the port to the hostname to get the IP + let hostname = format!("{}:0", hostname); + let ip_addresses = match hostname.to_socket_addrs() { //TODO Optimization: Cache ip_addresses + Ok(addrs) => addrs.map(|socket_addr| socket_addr.ip()).collect::>(), + Err(e) => { + eprintln!("Error getting IP addresses: {}", e); + continue; + } + }; + + // Object for each "local" IP used by every network interface (ipv4 and ipv6) + let socket_addresses: Vec = ip_addresses.into_iter().map(|ip| SocketAddr::new(ip, 0)).collect(); + for socket_addr in socket_addresses { + let api_clone = Arc::clone(&api); + + // One thread / IP + tokio::spawn(async move { + // Init RAW listen socket + let socket = match create_raw_socket(socket_addr).await { + Ok(socket) => socket, + Err(e) => { + eprintln!("Error creating raw socket: {}", e); + return; + } + }; + + // Capture the IP by filtering headers + match capture_ip(socket, listen_port).await { + Some((ip, port)) => { + // Got an IP, lock api and update every information + let mut api_lock = api_clone.write().await; + api_lock.game_status = GameStatus::InGame; + api_lock.server_ip = ip; + api_lock.server_port = port; + api_lock.last_updated_server_ip = Instant::now(); + + // Release the lock + drop(api_lock); + } + + None => { + // Got no result, get the last update time and check if it's too old + // This is not a typical timeout and should never happen, it's a security + let last_updated_server_ip = api_clone.read().await.last_updated_server_ip; + let last_server_ip = api_clone.read().await.server_ip.clone(); + if last_updated_server_ip.elapsed() > Duration::from_secs(20) && last_server_ip != ""{ + println!("Resetting server_ip, no result"); + let mut api_lock = api_clone.write().await; + + api_lock.server_ip = String::new(); + api_lock.server_port = 0; + api_lock.last_updated_server_ip = Instant::now(); + + drop(api_lock); + } + } + } + }); + } + } + } + + let game_status = api.read().await.game_status.clone(); + let dynamic_time = match game_status { + GameStatus::Closed => 5000, + GameStatus::Started => 3000, + GameStatus::MainMenu => 500, + GameStatus::InGame => 3000, + GameStatus::Unknown => 2000, + }; + + tokio::time::sleep(Duration::from_millis(dynamic_time)).await; + } + }); + + Ok(api_base) +} + +// Fetch game UDP connections +fn get_udp_connections(target_pid: usize) -> Vec { + let af_flags = AddressFamilyFlags::IPV4 | AddressFamilyFlags::IPV6; + let proto_flags = ProtocolFlags::UDP; + let sockets_info = match get_sockets_info(af_flags, proto_flags) { + Ok(sockets_info) => sockets_info, + Err(e) => { + eprintln!("Failed to get socket information: {}", e); + return Vec::new(); + } + }; + + let ports: Vec = sockets_info.iter().filter_map(|si| { + if let ProtocolSocketInfo::Udp(udp_si) = &si.protocol_socket_info { + // Check if any of the associated PIDs match the target PID + if si.associated_pids.iter().any(|&pid| pid == (target_pid as u32)) { + Some(udp_si.local_port) + } else { None } + } else { + None // This line is technically unnecessary due to the UDP filter applied earlier + } + }).collect(); + + //Filter out duplicates + return ports.into_iter().collect::>().into_iter().collect(); +} + +// In this netstat powershell command, we get the UDP endpoints of a process +fn get_udp_connections_powershell(pid: usize) -> Vec { + let ps_script = format!( + "Get-NetUDPEndpoint -OwningProcess {} | Select-Object -ExpandProperty LocalPort", + pid + ); + + const CREATE_NO_WINDOW: u32 = 0x08000000; + + let mut command = Command::new("powershell"); + command.args(&["-Command", &ps_script]) + .stdout(Stdio::piped()); + + command.creation_flags(CREATE_NO_WINDOW); + + let output = command.spawn() + .expect("Failed to start PowerShell command") + .stdout + .expect("Failed to open stdout"); + + let reader = BufReader::new(output); + let mut ports = Vec::new(); + + for line in reader.lines().filter_map(|l| l.ok()) { + if let Ok(port) = line.parse::() { + ports.push(port); + } + } + + ports +} + +// Get local hostname +fn get_local_hostname() -> std::io::Result { + Ok(hostname::get()?.into_string().unwrap_or_else(|_| "localhost".into())) +} + +// Get game PID +pub fn find_pid_of(process_name: &str) -> Vec { + let mut system = System::new_all(); + let mut pids = Vec::new(); + system.refresh_all(); + + for (pid, process) in system.processes() { + if process.name().to_lowercase() == process_name.to_lowercase() { + pids.push(pid.to_string()); + } + } + + pids +} + +// Receive & filter packets to get the server IP +async fn capture_ip(socket: UdpSocket, listen_port: u16) -> Option<(String, u16)> { + let mut buf = [0u8; (256 * 256) - 1]; + let timeout = Duration::from_millis(500); // This needs to be lower than the main loop sleep duration + let start_time = Instant::now(); + + loop { + if start_time.elapsed() > timeout { + // Timeout reached without receiving a packet + return None; + } + + // select! macro is used to wait for the first of two futures to complete, returning the result of that future. + tokio::select! { + recv_result = socket.recv(&mut buf) => { + match recv_result { + Ok(len) if len > 0 => { + // We got a packet, let's parse it + let recv_result = socket.recv(&mut buf).await; + return match recv_result { + Ok(len) => { + let packet = PacketHeaders::from_ip_slice(&buf[0..len]).ok()?; + + let net = packet.net.unwrap(); + let transport = packet.transport.unwrap(); + + // Parse source_ip and destination_ip + let (source_ip, destination_ip) = match net { + etherparse::NetHeaders::Ipv4(header, _) => { + let source = std::net::Ipv4Addr::new(header.source[0], header.source[1], header.source[2], header.source[3]); + let destination = std::net::Ipv4Addr::new(header.destination[0], header.destination[1], header.destination[2], header.destination[3]); + (source.to_string(), destination.to_string()) + }, + etherparse::NetHeaders::Ipv6(header, _) => { + let source = std::net::Ipv6Addr::from(header.source); + let destination = std::net::Ipv6Addr::from(header.destination); + (source.to_string(), destination.to_string()) + }, + }; + + let source_port; + let destination_port; + + // Parse ports, we don't need to support anything else than UDP + match transport { + etherparse::TransportHeader::Udp(header) => { + source_port = header.source_port; + destination_port = header.destination_port; + }, + _ => return None + } + + let mut remote_ip = String::new(); + let mut remote_port = 0; + + if source_port == listen_port { + // We are the source + remote_ip = destination_ip; + remote_port = destination_port; + } else if destination_port == listen_port { + // We are the destination + remote_ip = source_ip; + remote_port = source_port; + } + + if remote_port > 30000 && remote_port < 40000 { + // Got a plausible result + Some((remote_ip, remote_port)) + } else { + // Result make no sense for SoT + continue; + } + } + Err(err) => { + eprintln!("Error receiving packet: {}", err); + None + } + } + }, + + Ok(_) => continue, + Err(e) => eprintln!("Error receiving packet: {}", e), + } + } + _ = tokio::time::sleep(timeout) => { + // Timeout reached without receiving a packet + return None; + } + } + } +} + + +// Puts a socket into promiscuous mode so that it can receive all packets. +async fn enter_promiscuous(socket: &mut StdSocket) -> Result<()> { + let rc = unsafe { + let in_value: DWORD = 1; + + let mut out: DWORD = 0; + winsock2::WSAIoctl( + socket.as_raw_socket() as usize, + SIO_RCVALL, + &in_value as *const _ as *mut _, + size_of_val(&in_value) as DWORD, + null_mut(), //out value + 0, //size of out value + &mut out as *mut _, //byte returned + null_mut(), //pointer zero + None, + ) + }; + if rc == winsock2::SOCKET_ERROR { + bail!("WSAIoctl() failed: {}", unsafe { winsock2::WSAGetLastError() }) + } else { + Ok(()) + } +} + +// Creates a raw socket used to capture packets (disguised as a UdpSocket) +pub async fn create_raw_socket(socket_addr: SocketAddr) -> Result { + // Specify protocol + let protocol = Protocol::UDP; // IPPROTO_IP is typically 0 + + // Check if IPv4 or IPv6 + let domain = match socket_addr.ip() { + IpAddr::V4(_) => Domain::IPV4, + IpAddr::V6(_) => Domain::IPV6, + }; + + // Create a raw socket with domain, Type::RAW, and IPPROTO_IP + let socket = Socket::new(domain, Type::RAW, Some(protocol))?; + socket.set_nonblocking(true)?; + + // Convert SocketAddr to SockAddr + let sock_addr = socket2::SockAddr::from(socket_addr); + + // Bind the socket using a reference to the parsed address + socket.bind(&sock_addr)?; + + // Raw socket + let raw_socket = socket.into_raw_socket(); + let mut socket = unsafe { StdSocket::from_raw_socket(raw_socket) }; + enter_promiscuous(&mut socket).await?; + + // Set a read timeout of 500ms + socket.set_read_timeout(Some(Duration::from_millis(500)))?; + + let socket = UdpSocket::from_std(socket)?; + Ok(socket) +} diff --git a/frontend/src-tauri/src/main.rs b/frontend/src-tauri/src/main.rs index f5c5be23..ba0696c6 100644 --- a/frontend/src-tauri/src/main.rs +++ b/frontend/src-tauri/src/main.rs @@ -1,8 +1,105 @@ // Prevents additional console window on Windows in release, DO NOT REMOVE!! #![cfg_attr(not(debug_assertions), windows_subsystem = "windows")] -fn main() { - tauri::Builder::default() - .run(tauri::generate_context!()) - .expect("error while running tauri application"); +use std::sync::Arc; +use std::thread::sleep; +use std::time::Duration; +use serde::Serialize; +use tauri::State; +use tokio::sync::RwLock; +use crate::api::{Api, GameStatus}; +use crate::window_interaction::{set_focus_to_window, send_key}; + +mod fetch_informations; +mod api; +mod window_interaction; + +#[derive(Serialize)] +struct GameObject { + ip: String, + port: u16, + status: GameStatus +} + +// Here's how to call Rust functions from frontend : https://tauri.app/v1/guides/features/command/ + +#[tokio::main] +async fn main() { + let api_arc = fetch_informations::init().await.expect("Failed to initialize API"); + + tauri::Builder::default() + .manage(api_arc) + .invoke_handler(tauri::generate_handler![ + get_game_status, + get_server_ip, + get_server_port, + get_game_object, + get_last_updated_server_ip, + drop_anchor + ]) + .run(tauri::generate_context!()) + .expect("error while running tauri application"); +} + +#[tauri::command] +async fn get_game_status(api: State<'_, Arc>>) -> Result { + let api_lock = api.inner().read().await; + Ok(api_lock.get_game_status().await) +} + +#[tauri::command] +async fn get_server_ip(api: State<'_, Arc>>) -> Result { + let api_lock = api.inner().read().await; + Ok(api_lock.get_server_ip().await) +} + +#[tauri::command] +async fn get_server_port(api: State<'_, Arc>>) -> Result { + let api_lock = api.inner().read().await; + Ok(api_lock.get_server_port().await) +} + +#[tauri::command] +async fn get_game_object(api: State<'_, Arc>>) -> Result { + // Let's build an array with ip, port and status + let api_lock = api.inner().read().await; + let game_object = GameObject { + ip: api_lock.get_server_ip().await, + port: api_lock.get_server_port().await, + status: api_lock.get_game_status().await + }; + + Ok(game_object.into()) } + +#[tauri::command] +async fn get_last_updated_server_ip(api: State<'_, Arc>>) -> Result { + let api_lock = api.inner().read().await; + + let instant = api_lock.get_last_updated_server_ip().await; + let now = std::time::SystemTime::now(); + let epoch = std::time::UNIX_EPOCH; + let duration_since_epoch = now.duration_since(epoch).expect("Time went backwards"); + let instant_duration = instant.elapsed(); + let total_duration = duration_since_epoch.checked_sub(instant_duration).expect("Time went backwards"); + Ok(total_duration.as_secs()) +} + +#[tauri::command] +fn drop_anchor() -> bool { + if set_focus_to_window("Sea Of Thieves") { + // Maybe we shouldn't hardcode a sleep duration, but I don't see any other way to do it + sleep(Duration::from_millis(10)); + + // 2x Left arrow key to focus the button + send_key(0x25); + sleep(Duration::from_millis(1)); + send_key(0x25); + + sleep(Duration::from_millis(1)); + send_key(0x0D); // Enter key + return true; + } + + return false; +} \ No newline at end of file diff --git a/frontend/src-tauri/src/window_interaction.rs b/frontend/src-tauri/src/window_interaction.rs new file mode 100644 index 00000000..1a70740b --- /dev/null +++ b/frontend/src-tauri/src/window_interaction.rs @@ -0,0 +1,53 @@ +// Prevents additional console window on Windows in release, DO NOT REMOVE!! +#![cfg_attr(not(debug_assertions), windows_subsystem = "windows")] + +use std::ffi::CString; +use std::ptr::null_mut; +use winapi::um::winuser::{FindWindowA, SetForegroundWindow, INPUT, INPUT_KEYBOARD, KEYBDINPUT, KEYEVENTF_KEYUP, SendInput}; +use winapi::ctypes::c_int; +use std::mem::size_of; + +pub(crate) fn set_focus_to_window(window_name: &str) -> bool { + let window_name_cstring = CString::new(window_name).unwrap(); + let window_handle = unsafe { FindWindowA(null_mut(), window_name_cstring.as_ptr()) }; + + if window_handle.is_null() { + println!("Could not find window with name: {}", window_name); + false + } else { + unsafe { SetForegroundWindow(window_handle) }; + true + } +} + +pub(crate) fn send_key(virtual_keycode: u16) { + let mut key_input = INPUT { + type_: INPUT_KEYBOARD, + u: unsafe { std::mem::zeroed() }, + }; + + // Key pressed + unsafe { + *key_input.u.ki_mut() = KEYBDINPUT { + wVk: virtual_keycode, + wScan: 0, + dwFlags: 0, + time: 0, + dwExtraInfo: 0, + }; + } + + unsafe { SendInput(1, &mut key_input, size_of::() as c_int) }; + + // Key release + unsafe { + *key_input.u.ki_mut() = KEYBDINPUT { + wVk: virtual_keycode, + wScan: 0, + dwFlags: KEYEVENTF_KEYUP, + time: 0, + dwExtraInfo: 0, + }; + } + unsafe { SendInput(1, &mut key_input, size_of::() as c_int) }; +} \ No newline at end of file diff --git a/frontend/src-tauri/tauri.conf.json b/frontend/src-tauri/tauri.conf.json index 1035f13f..4a22fbc7 100644 --- a/frontend/src-tauri/tauri.conf.json +++ b/frontend/src-tauri/tauri.conf.json @@ -12,7 +12,13 @@ }, "tauri": { "allowlist": { - "all": false + "all": false, + "shell": { + "all": true, + "execute": true, + "sidecar": true, + "open": true + } }, "bundle": { "active": true, @@ -40,7 +46,10 @@ }, "resources": [], "shortDescription": "", - "targets": "all", + "targets": [ + "nsis", + "updater" + ], "windows": { "certificateThumbprint": null, "digestAlgorithm": "sha256", @@ -51,15 +60,23 @@ "csp": null }, "updater": { - "active": false + "active": true, + "endpoints": [ + "https://github.com/zelytra/BetterFleet/releases/latest/download/latest.json" + ], + "dialog": true, + "pubkey": "dW50cnVzdGVkIGNvbW1lbnQ6IG1pbmlzaWduIHB1YmxpYyBrZXk6IDE2RDBEMjE0MEIwOTZBQzMKUldURGFna0xGTkxRRnJ1bW0welJxZCtPUmNBZGdoZG9XTC9PL1JSZTBGZjJkWVg3b3NSOVZCZUIK", + "windows": { + "installMode": "passive" + } }, "windows": [ { "fullscreen": false, - "height": 600, + "height": 700, "resizable": true, - "title": "betterfleet", - "width": 1000 + "title": "BetterFleet", + "width": 1250 } ] } diff --git a/frontend/src/App.vue b/frontend/src/App.vue index e8463109..6db2ac56 100644 --- a/frontend/src/App.vue +++ b/frontend/src/App.vue @@ -17,36 +17,37 @@ !UserStore.player.username || UserStore.player.username.length === 0 " /> + \ No newline at end of file + .social-wrapper { + display: flex; + align-items: center; + gap: 12px; + } + } + } + } +} + diff --git a/frontend/src/components/Fleet.vue b/frontend/src/components/Fleet.vue index ac47a5ac..44bd1906 100644 --- a/frontend/src/components/Fleet.vue +++ b/frontend/src/components/Fleet.vue @@ -1,28 +1,67 @@ \ No newline at end of file + diff --git a/frontend/src/components/fleet/FleetSessionChoice.vue b/frontend/src/components/fleet/FleetSessionChoice.vue index 44a8a8c1..d0cd748a 100644 --- a/frontend/src/components/fleet/FleetSessionChoice.vue +++ b/frontend/src/components/fleet/FleetSessionChoice.vue @@ -43,6 +43,7 @@ import InputText from "@/vue/form/InputText.vue"; const { t } = useI18n(); const isModalOpen = ref(false); const sessionId = ref(""); + const props = defineProps({ session: { type: Object as PropType, required: true }, }); diff --git a/frontend/src/components/fleet/SessionCountdown.vue b/frontend/src/components/fleet/SessionCountdown.vue new file mode 100644 index 00000000..5c5f2553 --- /dev/null +++ b/frontend/src/components/fleet/SessionCountdown.vue @@ -0,0 +1,94 @@ + + + + + \ No newline at end of file diff --git a/frontend/src/components/global/Header.vue b/frontend/src/components/global/Header.vue index f62ae6eb..4debeb22 100644 --- a/frontend/src/components/global/Header.vue +++ b/frontend/src/components/global/Header.vue @@ -29,11 +29,13 @@

+
{ - document.body.addEventListener('click', el.clickOutsideEvent) - }); - }, - unmounted(el) { - document.body.removeEventListener('click', el.clickOutsideEvent); - } +const alertProvider = reactive(new AlertProvider()); +app.provide("alertProvider", alertProvider); +app.directive("click-outside", { + mounted(el, binding) { + el.clickOutsideEvent = function (event: any) { + if (!(el === event.target || el.contains(event.target))) { + binding.value(event, el); + } + }; + window.requestAnimationFrame(() => { + document.body.addEventListener("click", el.clickOutsideEvent); + }); + }, + unmounted(el) { + document.body.removeEventListener("click", el.clickOutsideEvent); + }, }); app.use(router); -app.use(i18n) -app.mount('#app') \ No newline at end of file +app.use(i18n); +app.mount("#app"); + +export { alertProvider }; diff --git a/frontend/src/objects/Fleet.ts b/frontend/src/objects/Fleet.ts index 632faee8..7483e8e1 100644 --- a/frontend/src/objects/Fleet.ts +++ b/frontend/src/objects/Fleet.ts @@ -1,118 +1,177 @@ import {UserStore} from "@/objects/stores/UserStore.ts"; +import {WebSocketMessage, WebSocketMessageType} from "@/objects/WebSocet.ts"; +import {AlertType} from "@/vue/alert/Alert.ts"; +import {alertProvider} from "@/main.ts"; +import i18n from "@/objects/i18n"; +import {SessionRunner} from "@/objects/SessionRunner.ts"; +import {Player} from "@/objects/Player.ts"; +import {SotServer} from "@/objects/SotServer.ts"; + +const {t} = i18n.global; export interface FleetInterface { - sessionId: string; - sessionName: string; - players: Player[]; - servers: SotServer[]; - status: SessionStatus; - socket?: WebSocket; + sessionId: string; + sessionName: string; + players: Player[]; + servers: Map; + socket?: WebSocket; } export class Fleet { - public sessionId: string; - public sessionName: string; - public players: Player[]; - public servers: SotServer[]; - public status: SessionStatus; - public socket?: WebSocket; - - constructor() { - this.sessionId = ""; - this.sessionName = ""; - this.players = []; - this.servers = []; - this.status = SessionStatus.WAITING; + public sessionId: string; + public sessionName: string; + public players: Player[]; + public servers: Map; + public socket?: WebSocket; + + constructor() { + this.sessionId = ""; + this.sessionName = ""; + this.players = []; + this.servers = new Map(); + } + + joinSession(sessionId: string) { + if (this.socket && this.socket.readyState >= 2) { + this.socket.close(); } - joinSession(sessionId: string): void { - if (this.socket) { - this.socket.close(); + UserStore.player.isReady = false; + UserStore.player.isMaster = false; + + this.socket = new WebSocket( + import.meta.env.VITE_SOCKET_HOST + "/" + sessionId, + ); + + // Send player data to backend for initialization + this.socket.onopen = () => { + if (!this.socket) return; + const message: WebSocketMessage = { + data: UserStore.player, + messageType: WebSocketMessageType.CONNECT, + }; + this.socket.send(JSON.stringify(message)); + + // If player already connect to a server + if (UserStore.player.server) { + this.joinServer() + } + }; + + this.socket.onmessage = (ev: MessageEvent) => { + const message: WebSocketMessage = JSON.parse(ev.data) as WebSocketMessage; + switch (message.messageType) { + case WebSocketMessageType.UPDATE: { + this.handleFleetUpdate(message.data as FleetInterface); + break; } - - UserStore.player.isReady = false; - UserStore.player.isMaster= false; - - this.socket = new WebSocket( - import.meta.env.VITE_SOCKET_HOST + "/" + sessionId, - ); - - // Send player data to backend for initialization - this.socket.onopen = () => { - if (!this.socket) return; - this.socket.send(JSON.stringify(UserStore.player)); - }; - - this.socket.onmessage = (ev: MessageEvent) => { - const receivedFleet: FleetInterface = JSON.parse(ev.data) as FleetInterface //TODO inspect - this.sessionId = receivedFleet.sessionId; - this.sessionName = receivedFleet.sessionName; - this.players = receivedFleet.players; - this.servers = receivedFleet.servers; - this.status = receivedFleet.status; - - UserStore.player.sessionId = receivedFleet.sessionId; - }; - } - - leaveSession(): void { - if (!this.socket) { - return; + case WebSocketMessageType.RUN_COUNTDOWN: { + this.handleSessionRunner(message.data as SessionRunner); + break; } - this.socket.close(); - this.sessionId = ""; - } - - updateToSession() { - if (!this.socket) return; - this.socket.send(JSON.stringify(UserStore.player)); - } - - getReadyPlayers(): Player[] { - return this.players.filter((player) => player.isReady); - } - - public static getFormatedStatus(player: Player) { - return player.status.toString().toLowerCase().replace("_", "-"); - } - - /** - * @return List of the players with the right master - */ - public getMasters(): Player[] { - return this.players.filter((player) => player.isMaster); + default: { + throw new Error( + "Failed to handle this message type : " + message.messageType, + ); + } + } + }; + + this.socket.onerror = () => { + alertProvider.sendAlert({ + content: t("alert.socket.connectionFailed"), + title: t("alert.socket.title"), + type: AlertType.ERROR, + }); + }; + } + + private handleFleetUpdate(receivedFleet: FleetInterface) { + this.sessionId = receivedFleet.sessionId; + this.sessionName = receivedFleet.sessionName; + this.players = receivedFleet.players; + this.servers = new Map(Object.entries(receivedFleet.servers)); + UserStore.player.sessionId = receivedFleet.sessionId; + const player: Player = receivedFleet.players.filter(x => x.username == UserStore.player.username)[0] + UserStore.player.isMaster = player.isMaster; + UserStore.player.isReady = player.isReady; + } + + private handleSessionRunner(countdown: SessionRunner) { + UserStore.player.countDown = countdown + + } + + leaveSession(): void { + if (!this.socket) { + return; } + this.socket.close(); + this.sessionId = ""; + } + + updateToSession(): void { + if (!this.socket) return; + const message: WebSocketMessage = { + data: UserStore.player, + messageType: WebSocketMessageType.UPDATE, + }; + this.socket.send(JSON.stringify(message)); + } + + runCountDown() { + if (!this.socket) return; + const message: WebSocketMessage = { + data: UserStore.player.countDown, + messageType: WebSocketMessageType.START_COUNTDOWN, + }; + this.socket.send(JSON.stringify(message)); + } + + clearPlayersStatus(){ + if (!this.socket) return; + const message: WebSocketMessage = { + data: undefined, + messageType: WebSocketMessageType.CLEAR_STATUS, + }; + this.socket.send(JSON.stringify(message)); + } + + joinServer(): void { + if (!this.socket) return; + const message: WebSocketMessage = { + data: UserStore.player.server, + messageType: WebSocketMessageType.JOIN_SERVER, + }; + this.socket.send(JSON.stringify(message)); + } + + leaveServer(): void { + if (!this.socket) return; + const message: WebSocketMessage = { + data: UserStore.player.server, + messageType: WebSocketMessageType.LEAVE_SERVER, + }; + this.socket.send(JSON.stringify(message)); + UserStore.player.server = undefined; + } + + getReadyPlayers(): Player[] { + return this.players.filter((player) => player.isReady); + } + + public static getFormatedStatus(player: Player) { + return player.status.toString().toLowerCase().replace("_", "-"); + } + + /** + * @return List of the players with the right master + */ + public getMasters(): Player[] { + return this.players.filter((player) => player.isMaster); + } } -export interface Player extends Preferences { - username: string; - status: PlayerStates; - isReady: boolean; - isMaster: boolean; - fleet?: Fleet; - sessionId?:string -} -export interface Preferences { - lang?: string; -} -export interface SotServer { - ip: string; - port: number; - location: string; - connectedPlayers: Player[]; -} - -export enum PlayerStates { - OFFLINE = "OFFLINE", // Game not detected - ONLINE = "ONLINE", // Game detected and open but not in game - IN_GAME = "IN_GAME", // Player in a server -} -export enum SessionStatus { - WAITING = "WAITING", // Waiting for player to be ready - READY = "READY", // All player ready - COUNTDOWN = "COUNTDOWN", // Countdown to start the click - ACTION = "ACTION", // Clicking in the game -} diff --git a/frontend/src/objects/Player.ts b/frontend/src/objects/Player.ts new file mode 100644 index 00000000..c566dc93 --- /dev/null +++ b/frontend/src/objects/Player.ts @@ -0,0 +1,26 @@ +import {SessionRunner} from "@/objects/SessionRunner.ts"; +import {Fleet} from "@/objects/Fleet.ts"; +import {SotServer} from "@/objects/SotServer.ts"; + +export enum PlayerStates { + CLOSED = "CLOSED", // Game is closed + STARTED = "STARTED", // Game detected an // Game is in first menu after launch / launching / stopping + MAIN_MENU = "MAIN_MENU", // In menu to select game mode + IN_GAME = "IN_GAME", // Status when the remote IP and port was found and player is in game +} + +export interface Player extends Preferences { + username: string; + status: PlayerStates; + isReady: boolean; + isMaster: boolean; + fleet?: Fleet; + sessionId?: string; + serverHostName?: string; + countDown?: SessionRunner; + server?: SotServer; +} + +export interface Preferences { + lang?: string; +} \ No newline at end of file diff --git a/frontend/src/objects/SessionRunner.ts b/frontend/src/objects/SessionRunner.ts new file mode 100644 index 00000000..316be650 --- /dev/null +++ b/frontend/src/objects/SessionRunner.ts @@ -0,0 +1,4 @@ +export interface SessionRunner { + startingTimer: string + clickTime?: string +} \ No newline at end of file diff --git a/frontend/src/objects/SotServer.ts b/frontend/src/objects/SotServer.ts new file mode 100644 index 00000000..fae3591c --- /dev/null +++ b/frontend/src/objects/SotServer.ts @@ -0,0 +1,15 @@ +import {Player, PlayerStates} from "@/objects/Player.ts"; + +export interface SotServer { + ip: string; + port: number; + location: string; + hash?: string; + connectedPlayers: Player[]; +} + +export interface RustSotServer { + ip: string; + port: number; + status: PlayerStates; +} \ No newline at end of file diff --git a/frontend/src/objects/Utils.ts b/frontend/src/objects/Utils.ts index 1c35b975..d8a0d723 100644 --- a/frontend/src/objects/Utils.ts +++ b/frontend/src/objects/Utils.ts @@ -1,13 +1,31 @@ +import {PlayerStates} from "@/objects/Player.ts"; + + export abstract class Utils { - public static generateRandomColor(): string { - // Generate random color components - const r = Math.floor(Math.random() * 256).toString(16).padStart(2, '0'); - const g = Math.floor(Math.random() * 256).toString(16).padStart(2, '0'); - const b = Math.floor(Math.random() * 256).toString(16).padStart(2, '0'); + public static generateRandomColor(): string { + // Generate random color components + const r = Math.floor(Math.random() * 256).toString(16).padStart(2, '0'); + const g = Math.floor(Math.random() * 256).toString(16).padStart(2, '0'); + const b = Math.floor(Math.random() * 256).toString(16).padStart(2, '0'); + + // Append '80' to the hex value for 50% opacity + const opacity = '80'; - // Append '80' to the hex value for 50% opacity - const opacity = '80'; + return `#${r}${g}${b}${opacity}`; + } - return `#${r}${g}${b}${opacity}`; + public static parseRustPlayerStatus(status: string):PlayerStates { + switch (status.toString().toLowerCase()) { + case "closed": + return PlayerStates.CLOSED; + case "started": + return PlayerStates.STARTED; + case "mainmenu": + return PlayerStates.MAIN_MENU; + case "ingame": + return PlayerStates.IN_GAME; + default: + return PlayerStates.CLOSED; } + } } diff --git a/frontend/src/objects/WebSocet.ts b/frontend/src/objects/WebSocet.ts new file mode 100644 index 00000000..085901d8 --- /dev/null +++ b/frontend/src/objects/WebSocet.ts @@ -0,0 +1,15 @@ +export interface WebSocketMessage { + messageType: WebSocketMessageType; + data: any; // Anytype of data that backend can handle +} + +export enum WebSocketMessageType { + CONNECT = "CONNECT", // When a player join a session + UPDATE = "UPDATE", // when the data of the player need to be broadcast to other player of the session + START_COUNTDOWN = "START_COUNTDOWN", + RUN_COUNTDOWN = "RUN_COUNTDOWN", + CANCEL_COUNTDOWN = "CANCEL_COUNTDOWN", + CLEAR_STATUS = "CLEAR_STATUS", + JOIN_SERVER = "JOIN_SERVER", + LEAVE_SERVER = "LEAVE_SERVER", +} diff --git a/frontend/src/objects/i18n/index.ts b/frontend/src/objects/i18n/index.ts new file mode 100644 index 00000000..5ccdec67 --- /dev/null +++ b/frontend/src/objects/i18n/index.ts @@ -0,0 +1,10 @@ +import { createI18n } from "vue-i18n"; +import fr from "@assets/locales/fr.json"; +import en from "@assets/locales/en.json"; + +export default createI18n({ + legacy: false, // you must set `false`, to use Composition API + locale: "fr", // set locale + fallbackLocale: "en", // set fallback locale + messages: { fr, en }, +}); diff --git a/frontend/src/objects/stores/UserStore.ts b/frontend/src/objects/stores/UserStore.ts index f22313ae..c3d9130a 100644 --- a/frontend/src/objects/stores/UserStore.ts +++ b/frontend/src/objects/stores/UserStore.ts @@ -1,27 +1,35 @@ -import {reactive} from "vue"; -import LocalStore, {LocalKey} from "@/objects/stores/LocalStore.ts"; -import {i18n} from "@/main.ts"; -import type {Player} from "@/objects/Fleet.ts"; - +import { reactive } from "vue"; +import LocalStore, { LocalKey } from "@/objects/stores/LocalStore.ts"; +import { i18n } from "@/main.ts"; +import {Player} from "@/objects/Player.ts"; export const UserStore = reactive({ - player: {} as Player, - init(defaultPlayerValue: Player) { - const userStoreKey = LocalStore(LocalKey.USER_STORE, {}); - const browserLang = navigator.language.substring(0, 2); - const readedPlayer = userStoreKey.value as Player; - this.player = defaultPlayerValue; - this.player.username = readedPlayer.username; - this.player.lang = readedPlayer.lang; + player: {} as Player, + init(defaultPlayerValue: Player) { + const userStoreKey = LocalStore(LocalKey.USER_STORE, {}); + const browserLang = navigator.language.substring(0, 2); + const readedPlayer = userStoreKey.value as Player; + this.player = defaultPlayerValue; + this.player.username = readedPlayer.username; + this.player.lang = readedPlayer.lang; + this.player.serverHostName = readedPlayer.serverHostName; + + if (!this.player.lang) this.player.lang = browserLang; + if (!this.player.username) this.player.username = ""; + if (!this.player.serverHostName) + this.player.serverHostName = import.meta.env.VITE_SOCKET_HOST; - if (!this.player.lang) this.player.lang = browserLang; - if (!this.player.username) this.player.username = ""; + //@ts-ignore I18N typescript implementation + i18n.global.locale.value = this.player.lang; + }, - //@ts-ignore I18N typescript implementation - i18n.global.locale.value = this.player.lang; - }, + setUser(user: Player) { + this.player = user; + }, - setUser(user: Player) { - this.player = user; - }, + setLang(lang: string) { + //@ts-ignore I18N typescript implementation + this.player.lang = lang; + i18n.global.locale.value = (this.player.lang as "fr") || "en"; + }, }); diff --git a/frontend/src/vue/alert/Alert.ts b/frontend/src/vue/alert/Alert.ts new file mode 100644 index 00000000..11936b83 --- /dev/null +++ b/frontend/src/vue/alert/Alert.ts @@ -0,0 +1,71 @@ +import flame from "@/assets/icons/alert-flame.svg"; +import {AlertUtils} from "@/vue/alert/AlertUtils.ts"; + +export class AlertType { + static readonly VALID = new AlertType("VALID", flame); + static readonly ERROR = new AlertType("ERROR", flame); + static readonly WARNING = new AlertType("WARNING", flame); + + // private to disallow creating other instances of this type + private constructor( + public readonly key: string, + public readonly value: any, + ) { + } + + toString() { + return this.value; + } +} + +export interface Alert { + type: AlertType; + title: string; + content: string; + id?: number; +} + +export class AlertProvider { + private alerts: Alert[] = []; + + constructor() { + } + + public get getAlerts() { + return this.alerts; + } + + public sendAlert(alert: Alert) { + alert.id = Math.floor(Math.random() * 1000); + this.alerts.push(alert); + + setTimeout(() => { + const index = this.alerts.indexOf(alert, 0); + if (index > -1) { + this.alerts.splice(index, 1); + } + }, 5000); + } + + public handleError(status: number) { + switch (status) { + case 403: { + this.sendAlert(AlertUtils.getForbiddenAccessAlert()); + break; + } + case 500: { + this.sendAlert(AlertUtils.getErrorAlert()); + break; + } + case 400: { + this.sendAlert(AlertUtils.getBadRequest()); + break; + } + case 415: { + this.sendAlert(AlertUtils.getUnsupportedMediaType()); + break; + } + + } + } +} diff --git a/frontend/src/vue/alert/AlertBox.vue b/frontend/src/vue/alert/AlertBox.vue new file mode 100644 index 00000000..9bfeb027 --- /dev/null +++ b/frontend/src/vue/alert/AlertBox.vue @@ -0,0 +1,78 @@ + + + + + diff --git a/frontend/src/vue/alert/AlertComponent.vue b/frontend/src/vue/alert/AlertComponent.vue new file mode 100644 index 00000000..cf73c6a1 --- /dev/null +++ b/frontend/src/vue/alert/AlertComponent.vue @@ -0,0 +1,44 @@ + + + + + diff --git a/frontend/src/vue/alert/AlertUtils.ts b/frontend/src/vue/alert/AlertUtils.ts new file mode 100644 index 00000000..c3d09e12 --- /dev/null +++ b/frontend/src/vue/alert/AlertUtils.ts @@ -0,0 +1,38 @@ +import {Alert, AlertType} from "@/vue/alert/Alert.ts"; +import i18n from "@/objects/i18n"; + +const {t} = i18n.global; + +export class AlertUtils { + public static getErrorAlert(): Alert { + return { + type: AlertType.ERROR, + title: t('alert.error.title'), + content: t('alert.error.content'), + }; + } + + public static getForbiddenAccessAlert(): Alert { + return { + type: AlertType.ERROR, + title: t('alert.forbidden.title'), + content: t('alert.forbidden.content'), + }; + } + + public static getBadRequest(): Alert { + return { + type: AlertType.ERROR, + title: t('alert.badRequest.title'), + content: t('alert.badRequest.content'), + }; + } + + public static getUnsupportedMediaType(): Alert { + return { + type: AlertType.ERROR, + title: t('alert.unsupportedMedia.title'), + content: t('alert.unsupportedMedia.content'), + }; + } +} diff --git a/frontend/src/vue/fleet/PlayerFleet.vue b/frontend/src/vue/fleet/PlayerFleet.vue index 71b54c39..a13c85bf 100644 --- a/frontend/src/vue/fleet/PlayerFleet.vue +++ b/frontend/src/vue/fleet/PlayerFleet.vue @@ -19,9 +19,10 @@ diff --git a/frontend/src/vue/form/Inputs.ts b/frontend/src/vue/form/Inputs.ts new file mode 100644 index 00000000..d68e4f47 --- /dev/null +++ b/frontend/src/vue/form/Inputs.ts @@ -0,0 +1,10 @@ +export interface SingleSelectInterface { + selectedValue?: InputData; + data:InputData[]; +} + +export interface InputData { + image: any; + display: string; + id: string +} \ No newline at end of file diff --git a/frontend/src/vue/form/SelectInput.vue b/frontend/src/vue/form/SelectInput.vue new file mode 100644 index 00000000..c1d0c2d6 --- /dev/null +++ b/frontend/src/vue/form/SelectInput.vue @@ -0,0 +1,204 @@ + + + + + diff --git a/frontend/src/vue/form/SingleSelect.vue b/frontend/src/vue/form/SingleSelect.vue new file mode 100644 index 00000000..59880deb --- /dev/null +++ b/frontend/src/vue/form/SingleSelect.vue @@ -0,0 +1,130 @@ + + + + + diff --git a/frontend/src/vue/templates/BannerTemplate.vue b/frontend/src/vue/templates/BannerTemplate.vue index 95220dbf..9a5ed809 100644 --- a/frontend/src/vue/templates/BannerTemplate.vue +++ b/frontend/src/vue/templates/BannerTemplate.vue @@ -7,9 +7,7 @@
- + \ No newline at end of file + diff --git a/frontend/src/vue/templates/FirstLogin.vue b/frontend/src/vue/templates/FirstLogin.vue index 9d74db40..d2802528 100644 --- a/frontend/src/vue/templates/FirstLogin.vue +++ b/frontend/src/vue/templates/FirstLogin.vue @@ -72,9 +72,10 @@ function updateUsername() { justify-content: center; align-items: center; gap: 15px; + margin-bottom: 50px; .main-content { - padding: 50px 14px; + padding: 30px 14px; display: flex; flex-direction: column; justify-content: center; @@ -105,7 +106,7 @@ function updateUsername() { flex-direction: column; justify-content: center; align-items: center; - padding: 40px 40px; + padding: 20px 40px; background: linear-gradient( 0deg, rgba(50, 144, 212, 0.2) 0%, @@ -155,7 +156,7 @@ function updateUsername() { display: flex; align-items: center; max-width: 350px; - padding: 26px 18px; + padding: 18px 16px; gap: 18px; border-radius: 5px; diff --git a/frontend/src/vue/templates/ServerContainer.vue b/frontend/src/vue/templates/ServerContainer.vue new file mode 100644 index 00000000..cf156c03 --- /dev/null +++ b/frontend/src/vue/templates/ServerContainer.vue @@ -0,0 +1,41 @@ + + + + + \ No newline at end of file diff --git a/frontend/tsconfig.json b/frontend/tsconfig.json index 5c36df7e..0804de15 100644 --- a/frontend/tsconfig.json +++ b/frontend/tsconfig.json @@ -21,7 +21,8 @@ "strict": true, "noUnusedLocals": true, "noUnusedParameters": true, - "noFallthroughCasesInSwitch": true + "noFallthroughCasesInSwitch": true, + "allowSyntheticDefaultImports": true }, "include": ["src/**/*.ts", "src/**/*.tsx", "src/**/*.vue"], "references": [{ "path": "./tsconfig.node.json" }]