diff --git a/.gitignore b/.gitignore index 2cf7bf8..1be974f 100644 --- a/.gitignore +++ b/.gitignore @@ -1,4 +1,5 @@ # Godot 4+ specific ignores .godot/ addons/ -build/ \ No newline at end of file +build/ +*.tmp \ No newline at end of file diff --git a/scenes/board/board.gd b/scenes/board/board.gd index e829f4c..bb6dfa2 100644 --- a/scenes/board/board.gd +++ b/scenes/board/board.gd @@ -9,14 +9,18 @@ const WorkingDayEnd = 20 const MinimalMinutesToShowTitle = 30 const MinimalMinutesForBigSize = 60 +const DateUpdateInterval := 1.0 + @onready var _main: Main = get_tree().get_current_scene() @onready var _timeline = $Panel/Timeline @onready var _reservations = $Panel/Reservations @onready var _date = $TopBar/DateButton +var _date_update_timer = 0.0 + func _process(delta): _process_hour_size() - _process_date() + _process_date(delta) func _process_hour_size(): var hour_size = get_viewport_rect().size.y/15 @@ -24,17 +28,29 @@ func _process_hour_size(): for time_slot in _timeline.get_children(): time_slot.set_height(hour_size) -func _process_date(): +func _process_date(delta): + _date_update_timer += delta + if _date_update_timer >= DateUpdateInterval: + _date.text = _main.get_date() + _date_update_timer = 0.0 + var date = Time.get_date_dict_from_system() _date.text = "%02d.%02d.%04d" % [date.day, date.month, date.year] func _ready(): + _connect_signals() + _remove_time_slots() _remove_reservations() _fill_with_slots() _update_schedule() +func _connect_signals(): + await _main.ready + var event_handler = _main.get_event_handler() + event_handler.reservations_updated.connect(_on_reservations_updated) + func _remove_time_slots(): for time_slot in _timeline.get_children(): time_slot.queue_free() @@ -49,13 +65,16 @@ func _remove_reservations(): for reservation in _reservations.get_children(): reservation.queue_free() -func _update_schedule(): +func _update_schedule(reservations=null): if not _main.is_node_ready(): await _main.ready var repo = _main.get_reservation_repo() - for reservation in repo.list_reservations(): + if reservations == null: + reservations = await repo.list_reservations({"date": _main.get_date()}) + + for reservation in reservations: var start_time_hours = reservation.start_time.hours var start_time_minutes = reservation.start_time.minutes var start_time = (start_time_hours - WorkingDayStart)*60 + start_time_minutes @@ -123,5 +142,8 @@ func _on_room_button_pressed(): func _on_date_button_pressed(): print("emit change date signal") +func _on_reservations_updated(reservations): + update() + func update(): _ready() diff --git a/scenes/board/reservation.gd b/scenes/board/reservation.gd index 6c29304..15e08cf 100644 --- a/scenes/board/reservation.gd +++ b/scenes/board/reservation.gd @@ -46,7 +46,7 @@ func set_duration_time(minutes): func set_color(color): var new_style_box: StyleBoxFlat = _panel.get("theme_override_styles/panel").duplicate() - new_style_box.bg_color = Colors[color] + new_style_box.bg_color = Colors[int(color)] _panel.set("theme_override_styles/panel", new_style_box) func set_font(font: Fonts): diff --git a/scenes/common/time/time_edit.tscn b/scenes/common/time/time_edit.tscn index 5e6ec5a..05ea010 100644 --- a/scenes/common/time/time_edit.tscn +++ b/scenes/common/time/time_edit.tscn @@ -14,7 +14,7 @@ script = ExtResource("1_2wxyg") [node name="TimeEdit" type="LineEdit" parent="."] visible = false layout_mode = 2 -text = "1:34 " +text = "11:10 " placeholder_text = "hh:mm (a/p)m" script = ExtResource("2_7d4ae") current_time = true diff --git a/scenes/main/main.gd b/scenes/main/main.gd index 2b55644..6b3c945 100644 --- a/scenes/main/main.gd +++ b/scenes/main/main.gd @@ -20,5 +20,11 @@ func go_to_previous_page(with_update=true): func get_reservation_repo() -> AbstractReservationRepo: return AbstractReservationRepo.new() +func get_event_handler() -> EventHandler: + return EventHandler.new() + func get_reservation_service() -> ReservationService: return ReservationService.new() + +func get_date() -> String: + return "" diff --git a/scenes/main/main_mobile.tscn b/scenes/main/main_mobile.tscn index 92d1c0c..1be0803 100644 --- a/scenes/main/main_mobile.tscn +++ b/scenes/main/main_mobile.tscn @@ -4,7 +4,7 @@ [ext_resource type="PackedScene" uid="uid://c431r28ef5edp" path="res://scenes/board/board.tscn" id="2_brvql"] [ext_resource type="PackedScene" uid="uid://csfn8q6b5hj4y" path="res://scenes/reservation/reservation_creation.tscn" id="3_qr4p2"] [ext_resource type="PackedScene" uid="uid://cxs8xe5w32jo4" path="res://scenes/common/time/time_setting.tscn" id="4_popa2"] -[ext_resource type="Script" path="res://src/infra/repos/local/reservation.gd" id="5_50dbn"] +[ext_resource type="Script" path="res://src/infra/repos/local/reservation_local.gd" id="5_50dbn"] [node name="Main" type="Control"] layout_mode = 3 diff --git a/scenes/main/main_tablet.gd b/scenes/main/main_tablet.gd index f4ac8d9..ee16fdf 100644 --- a/scenes/main/main_tablet.gd +++ b/scenes/main/main_tablet.gd @@ -16,6 +16,7 @@ enum Status { } @onready var _reservation_repo : AbstractReservationRepo = $Repos/Reservation +@onready var _event_handler : EventHandler = $Repos/EventHandler @onready var _reservation_service : ReservationService = $Services/ReservationService @export var current_page : Pages: @@ -48,6 +49,7 @@ func _ready(): initialize_signals() func initialize_signals(): + _event_handler.reservations_updated.connect(_on_reservations_updated) _create_reservation_button.pressed.connect(_on_create_reservation_button_pressed) _15_min_button.pressed.connect(_on_15_min_button_pressed) _30_min_button.pressed.connect(_on_30_min_button_pressed) @@ -59,7 +61,6 @@ func _process(delta): var time = Time.get_time_dict_from_system() _process_time(time) - _process_status(time) func _process_time_status_indent(): var indent = get_viewport_rect().size.y/3 @@ -69,36 +70,36 @@ func _process_time_status_indent(): func _process_time(time): _time_label.text = "%02d:%02d" % [time.hour, time.minute] -func _process_status(time): - var reservations = _reservation_repo.list_reservations() +func _update_status(reservations=null): + var time = Time.get_time_dict_from_system() var current_time_in_minutes = time.hour*60 + time.minute + if reservations == null: + reservations = await _reservation_repo.list_reservations({"date": get_date()}) + for reservation in reservations: var start_time_in_minutes = reservation.start_time.hours*60 + reservation.start_time.minutes var finish_time_in_minutes = reservation.finish_time.hours*60 + reservation.finish_time.minutes if current_time_in_minutes >= start_time_in_minutes \ and current_time_in_minutes < finish_time_in_minutes: - _status = Status.BUSY - _status_label.text = "Занято" - _background.color = BgColors.busy + _update_ui_status(Status.BUSY, "Занято", BgColors.busy) return elif current_time_in_minutes <= start_time_in_minutes \ and current_time_in_minutes + MinutesForTemporarilyFree > start_time_in_minutes: - _status = Status.FREE - _status_label.text = "Свободно" - _background.color = BgColors.temporarily_free + _update_ui_status(Status.FREE, "Свободно", BgColors.temporarily_free) return if len(reservations) > 0: - _status = Status.FREE - _status_label.text = "Свободно" - _background.color = BgColors.free + _update_ui_status(Status.FREE, "Свободно", BgColors.free) else: - _status = Status.FREE - _status_label.text = "Свободно" - _background.color = BgColors.standart + _update_ui_status(Status.FREE, "Свободно", BgColors.standart) + +func _update_ui_status(status: Status, text: String, color: Color): + _status = status + _status_label.text = text + _background.color = color func get_current_page(): return _current_page @@ -124,6 +125,9 @@ func go_to_previous_page(with_update=true): func get_reservation_repo() -> AbstractReservationRepo: return _reservation_repo +func get_event_handler() -> EventHandler: + return _event_handler + func get_reservation_service() -> ReservationService: return _reservation_service @@ -143,7 +147,7 @@ func _create_default_reservation(minutes_of_reservation): var finish_time_hours = floor(finish_time_in_minutes/60) var finish_time_minutes = finish_time_in_minutes - finish_time_hours * 60 - if get_reservation_service().is_time_busy(start_time_in_minutes, finish_time_in_minutes): + if await get_reservation_service().is_time_busy(start_time_in_minutes, finish_time_in_minutes): return var dto = CreateReservationDTO.new() @@ -163,6 +167,9 @@ func _create_default_reservation(minutes_of_reservation): load_page(Main.Pages.Board, true) +func _on_reservations_updated(reservations): + _update_status() + func _on_create_reservation_button_pressed(): load_page(Pages.ReservationCreation) @@ -177,3 +184,7 @@ func _on_45_min_button_pressed(): func _on_1_hour_button_pressed(): _create_default_reservation(60) + +func get_date() -> String: + var date = Time.get_date_dict_from_system() + return "%02d.%02d.%04d" % [date.day, date.month, date.year] diff --git a/scenes/main/main_tablet.tscn b/scenes/main/main_tablet.tscn index 4d68879..7d7b3a6 100644 --- a/scenes/main/main_tablet.tscn +++ b/scenes/main/main_tablet.tscn @@ -1,16 +1,17 @@ -[gd_scene load_steps=13 format=3 uid="uid://bkrvh8vjpgqot"] +[gd_scene load_steps=14 format=3 uid="uid://bkrvh8vjpgqot"] [ext_resource type="Script" path="res://scenes/main/main_tablet.gd" id="1_fr6s5"] [ext_resource type="PackedScene" uid="uid://c431r28ef5edp" path="res://scenes/board/board.tscn" id="2_n47h4"] [ext_resource type="PackedScene" uid="uid://csfn8q6b5hj4y" path="res://scenes/reservation/reservation_creation.tscn" id="3_j6x1g"] [ext_resource type="PackedScene" uid="uid://cu6e3hfdorwcg" path="res://scenes/reservation/reservation_edit.tscn" id="4_hyj5n"] [ext_resource type="PackedScene" uid="uid://cxs8xe5w32jo4" path="res://scenes/common/time/time_setting.tscn" id="4_wyf5q"] -[ext_resource type="Script" path="res://src/infra/repos/local/reservation.gd" id="5_6h0eq"] [ext_resource type="Theme" uid="uid://byopik87nb8vv" path="res://assets/themes/status_font.tres" id="5_atujq"] [ext_resource type="Theme" uid="uid://yn1nbokvmv6n" path="res://assets/themes/small.tres" id="6_nde4h"] [ext_resource type="Theme" uid="uid://b8tbd62jtmgdx" path="res://assets/themes/instant_button_font.tres" id="8_bmn8p"] [ext_resource type="Theme" uid="uid://cmhwbyqu6nh38" path="res://assets/themes/big.tres" id="9_wpf8g"] +[ext_resource type="Script" path="res://src/infra/repos/backend/reservation_http.gd" id="10_v7sup"] [ext_resource type="Script" path="res://src/domain/services/reservation.gd" id="11_5xy2x"] +[ext_resource type="Script" path="res://src/infra/repos/backend/event_handler_ws.gd" id="11_de30t"] [sub_resource type="StyleBoxFlat" id="StyleBoxFlat_uus54"] bg_color = Color(0.6, 0.6, 0.6, 0) @@ -70,14 +71,14 @@ size_flags_horizontal = 10 size_flags_vertical = 0 [node name="Indent" type="BoxContainer" parent="Left/TimeStatusContainer"] -custom_minimum_size = Vector2(0, 207.333) +custom_minimum_size = Vector2(0, 195) layout_mode = 2 size_flags_vertical = 3 [node name="TimeLabel" type="Label" parent="Left/TimeStatusContainer"] layout_mode = 2 theme = ExtResource("5_atujq") -text = "01:38" +text = "20:38" horizontal_alignment = 1 [node name="StatusLabel" type="Label" parent="Left/TimeStatusContainer"] @@ -142,7 +143,10 @@ text = "Забронировать" [node name="Repos" type="Node" parent="."] [node name="Reservation" type="Node" parent="Repos"] -script = ExtResource("5_6h0eq") +script = ExtResource("10_v7sup") + +[node name="EventHandler" type="Node" parent="Repos"] +script = ExtResource("11_de30t") [node name="Services" type="Node" parent="."] diff --git a/scenes/reservation/reservation_page.gd b/scenes/reservation/reservation_page.gd index 9848919..01a9a55 100644 --- a/scenes/reservation/reservation_page.gd +++ b/scenes/reservation/reservation_page.gd @@ -51,19 +51,19 @@ func _on_delete_button_pressed(): func _load_info(): var repo = _main.get_reservation_repo() var reservation_id = repo.get_current_reservation_id() - var reservation = repo.get_reservation(reservation_id) + var reservation = await repo.get_reservation(reservation_id) _title_field.set_value(reservation.title) _start_time_field.set_value(reservation.start_time) _finish_time_field.set_value(reservation.finish_time) func _create_reservation(): - if not _fields_are_correct(): + if not await _fields_are_correct(): return var dto = CreateReservationDTO.new() dto.title = _title_field.get_value() - dto.date = _date_field.get_value() + dto.date = _main.get_date() dto.start_time = _start_time_field.get_value() dto.finish_time = _finish_time_field.get_value() dto.creator = _creator_field.get_value() @@ -80,7 +80,7 @@ func _create_reservation(): clean() func _update_reservation(): - if not _fields_are_correct(): + if not await _fields_are_correct(): return var dto = UpdateReservationDTO.new() @@ -119,7 +119,7 @@ func _fields_are_correct(): # _error_box.set_message("Не введено название встречи") # successful = false - if not _time_is_correct(): + if not await _time_is_correct(): successful = false return successful @@ -149,7 +149,7 @@ func _time_is_correct(): var start_time_in_minutes = start_time.hours*60 + start_time.minutes var finish_time_in_minutes = finish_time.hours*60 + finish_time.minutes var service = _main.get_reservation_service() - var is_busy = service.is_time_busy(start_time_in_minutes, finish_time_in_minutes) + var is_busy = await service.is_time_busy(start_time_in_minutes, finish_time_in_minutes) if is_busy: print("The selected time slot is busy.") diff --git a/src/domain/services/reservation.gd b/src/domain/services/reservation.gd index 831fdd6..70ff632 100644 --- a/src/domain/services/reservation.gd +++ b/src/domain/services/reservation.gd @@ -5,7 +5,7 @@ class_name ReservationService func is_time_busy(new_start_time_minutes, new_finish_time_minutes): var repo = _main.get_reservation_repo() - var reservations = repo.list_reservations() + var reservations = await repo.list_reservations({"date": _main.get_date()}) var current_reservation_id = repo.get_current_reservation_id() for reservation in reservations: diff --git a/src/infra/dtos/common/time.gd b/src/infra/dtos/common/time.gd new file mode 100644 index 0000000..df4aec5 --- /dev/null +++ b/src/infra/dtos/common/time.gd @@ -0,0 +1,5 @@ +class_name TimeDTO +extends RefCounted + +var hours: int +var minutes: int diff --git a/src/infra/repos/abstract/event_handler.gd b/src/infra/repos/abstract/event_handler.gd new file mode 100644 index 0000000..a6ecf34 --- /dev/null +++ b/src/infra/repos/abstract/event_handler.gd @@ -0,0 +1,13 @@ +extends Node +class_name EventHandler + +signal connected() +signal disconnected() +signal reservations_updated(reservations) +signal error_occurred(message) + +func connect_to_server(): + pass + +func subscribe_to_room(room_id: String): + pass diff --git a/src/infra/repos/abstract/reservation.gd b/src/infra/repos/abstract/reservation.gd index 94e1b68..be3aa80 100644 --- a/src/infra/repos/abstract/reservation.gd +++ b/src/infra/repos/abstract/reservation.gd @@ -2,6 +2,9 @@ extends Node class_name AbstractReservationRepo +signal connected +signal not_connected + var _current_reservation_id = null func create_reservation(dto: CreateReservationDTO): @@ -16,7 +19,7 @@ func change_reservation(reservation_id, dto: UpdateReservationDTO): func get_reservation(reservation_id): pass -func list_reservations(): +func list_reservations(filters: Dictionary = {}): pass func set_current_reservation_id(value): diff --git a/src/infra/repos/backend/event_handler_ws.gd b/src/infra/repos/backend/event_handler_ws.gd new file mode 100644 index 0000000..5458faf --- /dev/null +++ b/src/infra/repos/backend/event_handler_ws.gd @@ -0,0 +1,66 @@ +class_name EventHandlerWS +extends EventHandler + +const BACKEND_URL = "http://127.0.0.1:5000/socket.io" +var _room_id: String = "" +var _jwt_token: String = "" +var _client: SocketIOClient + +func _ready(): + initialize_client() + setup_connections() + +func initialize_client(): + _client = SocketIOClient.new( + BACKEND_URL, + {"token": _jwt_token, "EIO": "4", "transport": "websocket"} + ) + add_child(_client) + +func setup_connections(): + _client.on_engine_connected.connect(_on_engine_connected) + _client.on_connect.connect(_on_socket_connect) + _client.on_disconnect.connect(_on_socket_disconnect) + _client.on_event.connect(_on_socket_event) + +func connect_to_server(jwt: String = ""): + if !jwt.is_empty(): + _jwt_token = jwt + _client.update_query_params({"token": _jwt_token}) + _client.socketio_connect() + +func _on_engine_connected(_sid: String): + print("Engine.IO connected") + _client.socketio_connect() + +func _on_socket_connect(_payload, _namespace, error: bool): + if error: + error_occurred.emit("Connection failed") + else: + print("Socket.IO connected") + subscribe_to_room(_room_id) + +func _on_socket_disconnect(): + print("Disconnected from server") + disconnected.emit() + +func _on_socket_event(event: String, payload: Variant, _namespace): + print(event, payload) + match event: + "reservations_update": + reservations_updated.emit(payload) + "error": + error_occurred.emit(payload.get("message", "Unknown error")) + +func subscribe_to_room(room_id: String): + _room_id = room_id + _client.socketio_send( + "subscribe_reservations", + {"room_id": room_id} + ) + +func disconnect_from_server(): + _client.socketio_disconnect() + +func _exit_tree(): + disconnect_from_server() diff --git a/src/infra/repos/backend/reservation_http.gd b/src/infra/repos/backend/reservation_http.gd new file mode 100644 index 0000000..c52da8a --- /dev/null +++ b/src/infra/repos/backend/reservation_http.gd @@ -0,0 +1,192 @@ +@tool +extends AbstractReservationRepo +class_name ReservationRepoHTTP + +const BASE_URL = "http://127.0.0.1:5000" +const RESERVATION_ENDPOINT = "/reservation/" +const HEALTH_ENDPOINT = "/health/" + +signal request_failed(error_message: String) + +var _jwt_token: String = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJmcmVzaCI6ZmFsc2UsImlhdCI6MTc0MDA0MDQzNywianRpIjoiOTM4NTUyMjMtMjhiNC00OWVhLWI3ZjUtZmYxMTg4YzI1Mjg2IiwidHlwZSI6ImFjY2VzcyIsInN1YiI6InRlc3RfdXNlciIsIm5iZiI6MTc0MDA0MDQzNywiY3NyZiI6ImMwYzI2MTU0LTdkNjItNGYyZi04ZjBhLWI0MjA0ODJlMmEzZCIsImV4cCI6MTc0MDA0MTMzN30.esPRTXNxrtziuOc2dUsc9XqbedErjcyvPlL3aUZIaaA" + +var _timed_out := false + +func _ready(): + if await _backend_is_accessible(): + connected.emit() + else: + not_connected.emit() + +func _backend_is_accessible(): + var result = await _make_request(HEALTH_ENDPOINT, HTTPClient.METHOD_GET) + + if not result: + push_error("Server initialization failed") + return false + if result.has("error"): + push_error("Server initialization failed: " + result.error) + return false + + return true + +#region Public Methods +func create_reservation(dto: CreateReservationDTO) -> void: + _make_request(RESERVATION_ENDPOINT, HTTPClient.METHOD_POST, _dto_to_json(dto)) + +func cancel_reservation(reservation_id: String) -> void: + _make_request(RESERVATION_ENDPOINT + "%s" % reservation_id, HTTPClient.METHOD_DELETE) + +func change_reservation(reservation_id: String, dto: UpdateReservationDTO) -> void: + _make_request(RESERVATION_ENDPOINT + "%s" % reservation_id, HTTPClient.METHOD_PUT, _dto_to_json(dto)) + +func get_reservation(reservation_id: String) -> ReservationEntity: + var result = await _make_request(RESERVATION_ENDPOINT + "%s" % reservation_id, HTTPClient.METHOD_GET) + return _parse_reservation_entity(result) if result is Dictionary else null + +func list_reservations(filters: Dictionary = {}) -> Array: + var query = "?" + for key in filters: + query += "%s=%s&" % [key, filters[key]] + query = query.rstrip("&") if filters else "" + + var endpoint = RESERVATION_ENDPOINT + query + var result = await _make_request(endpoint, HTTPClient.METHOD_GET) + return _parse_reservation_list(result) if result is Array else [] + +func set_current_reservation_id(value): + _current_reservation_id = value + +func get_current_reservation_id(): + return _current_reservation_id + +func set_jwt_token(token: String): + _jwt_token = token +#endregion + +#region Private Methods +func _get_headers() -> PackedStringArray: + return PackedStringArray([ + "Content-Type: application/json", + "Authorization: Bearer %s" % _jwt_token + ]) + +func _dto_to_json(dto) -> String: + var dict = {} + match typeof(dto): + TYPE_OBJECT when dto is CreateReservationDTO: + dict = { + "title": dto.title, + "description": dto.description, + "room_id": dto.room_id, + "date": dto.date, + "start_time": dto.start_time, + "finish_time": dto.finish_time, + "color": dto.color + } + TYPE_OBJECT when dto is UpdateReservationDTO: + if dto.title: dict["title"] = dto.title + if dto.description: dict["description"] = dto.description + if dto.room_id: dict["room_id"] = dto.room_id + if dto.date: dict["date"] = dto.date + if dto.start_time: dict["start_time"] = dto.start_time + if dto.finish_time: dict["finish_time"] = dto.finish_time + if dto.color: dict["color"] = dto.color + return JSON.stringify(dict) + +func _time_dto_to_dict(time_dto: TimeDTO) -> Dictionary: + return {"hours": time_dto.hours, "minutes": time_dto.minutes} + +func _make_request(endpoint: String, method: int, body: String = "") -> Variant: + var request = HTTPRequest.new() + + add_child(request) + + var url = BASE_URL + endpoint + var error = request.request(url, _get_headers(), method, body) + if error != OK: + request.queue_free() + emit_signal("request_failed", "Request creation failed: %d" % error) + return null + + var response = await _wait_for_request_or_timeout(request) + + request.queue_free() + + var result = response[0] + var response_code = response[1] + var response_body = response[3] + + if result != HTTPRequest.RESULT_SUCCESS: + emit_signal("request_failed", "HTTP request failed: %d" % result) + return null + + var json = JSON.new() + var parse_error = json.parse(response_body.get_string_from_utf8()) + + if parse_error != OK: + emit_signal("request_failed", "JSON parse error: %d" % parse_error) + return null + + var response_data = json.get_data() + + if response_code >= 400: + emit_signal("request_failed", response_data.get("error", "Unknown error")) + return null + + return response_data + +func _wait_for_request_or_timeout(request: HTTPRequest) -> Array: + var timer = Timer.new() + add_child(timer) + + timer.wait_time = 5.0 + timer.one_shot = true + + timer.timeout.connect(func(): + _timed_out = true + if request.get_http_client_status() != HTTPClient.STATUS_DISCONNECTED: + request.cancel_request() + ) + + timer.start() + + while true: + if _timed_out: + _timed_out = false + timer.queue_free() + return [HTTPRequest.RESULT_REQUEST_FAILED, 408, [], PackedByteArray()] + if request.get_http_client_status() == HTTPClient.STATUS_REQUESTING: + break + await get_tree().process_frame + + timer.queue_free() + return await request.request_completed + +func _parse_reservation_entity(data: Dictionary) -> ReservationEntity: + var entity = ReservationEntity.new() + entity.id = data.get("id", "") + entity.title = data.get("title", "") + entity.description = data.get("description", "") + entity.room_id = data.get("room_id", "") + entity.creator = data.get("creator", "") + entity.date = data.get("date", "") + entity.color = data.get("color", 0) + entity.start_time = data.get("start_time", {}) + entity.finish_time = data.get("finish_time", {}) + + return entity + +func _parse_reservation_list(data: Array) -> Array[ReservationEntity]: + var entities: Array[ReservationEntity] = [] + for item in data: + entities.append(_parse_reservation_entity(item)) + + entities.sort_custom(_compare_reservations_by_start_time) + return entities + +func _compare_reservations_by_start_time(a: ReservationEntity, b: ReservationEntity) -> int: + var time_a = a.start_time.get("hours", 0) * 60 + a.start_time.get("minutes", 0) + var time_b = b.start_time.get("hours", 0) * 60 + b.start_time.get("minutes", 0) + return time_a < time_b +#endregion diff --git a/src/infra/repos/local/event_handler_local.gd b/src/infra/repos/local/event_handler_local.gd new file mode 100644 index 0000000..a4533df --- /dev/null +++ b/src/infra/repos/local/event_handler_local.gd @@ -0,0 +1,2 @@ +extends EventHandler +class_name EventHandlerLocal diff --git a/src/infra/repos/local/reservation.gd b/src/infra/repos/local/reservation_local.gd similarity index 94% rename from src/infra/repos/local/reservation.gd rename to src/infra/repos/local/reservation_local.gd index a27203a..b763df2 100644 --- a/src/infra/repos/local/reservation.gd +++ b/src/infra/repos/local/reservation_local.gd @@ -6,6 +6,9 @@ const uuid_util = preload('res://addons/uuid/uuid.gd') var _reservations = {} +func _ready(): + connected.emit() + func create_reservation(dto: CreateReservationDTO): var entity = ReservationEntity.new() @@ -38,7 +41,7 @@ func change_reservation(reservation_id, dto: UpdateReservationDTO): func get_reservation(reservation_id): return _reservations[reservation_id] -func list_reservations(): +func list_reservations(filters: Dictionary = {}): var reservations = [] for key in _reservations: diff --git a/src/libs/socketio_client.gd b/src/libs/socketio_client.gd new file mode 100644 index 0000000..bf7ff49 --- /dev/null +++ b/src/libs/socketio_client.gd @@ -0,0 +1,247 @@ +class_name SocketIOClient extends Node + +enum EngineIOPacketType { + open = 0, + close = 1, + ping = 2, + pong = 3, + message = 4, + upgrade = 5, + noop = 6, +} + +enum SocketIOPacketType { + CONNECT = 0, + DISCONNECT = 1, + EVENT = 2, + ACK = 3, + CONNECT_ERROR = 4, + BINARY_EVENT = 5, + BINARY_ACK = 6, +} + +enum ConnectionState { + DISCONNECTED, + CONNECTED, + RECONNECTING, +} + +var _url: String +var _client: WebSocketPeer = WebSocketPeer.new() +var _sid: String +var _pingTimeout: int = 0 +var _pingInterval: int = 0 +var _auth: Variant = null +var _connection_state: ConnectionState = ConnectionState.DISCONNECTED +var _reconnect_timer: Timer = null + +# triggered when engine.io connection is established +signal on_engine_connected(sid: String) +# triggered when engine.io connection is closed +signal on_engine_disconnected(code: int, reason: String) +# triggered when engine.io message is received +signal on_engine_message(payload: String) + +# triggered when socket.io connection is established +signal on_connect(payload: Variant, name_space: String, error: bool) +# triggered when socket.io connection is closed +signal on_disconnect(name_space: String) +# triggered when socket.io event is received +signal on_event(event_name: String, payload: Variant, name_space: String) + +# triggered when lost connection in not-clean way (i.e. service shut down) +# and now trying to reconnect. When re-connects successfully, +# the on_reconnected signal is emitted, instead of on_connect +signal on_connection_lost + +# triggered when connects again after losing connection +# it's alternative to on_connect signal, but only emitted +# after automatically re-connecting with socket.io server +signal on_reconnected(payload: Variant, name_space: String, error: bool) + + +func _init(url: String, auth: Variant=null): + _auth = auth + url = _preprocess_url(url) + _url = "%s?EIO=4&transport=websocket" % url + +func _preprocess_url(url: String) -> String: + if not url.ends_with("/"): + url = url + "/" + if url.begins_with("https"): + url = "wss" + url.erase(0, len("https")) + elif url.begins_with("http"): + url = "ws" + url.erase(0, len("http")) + return url + +func _ready(): + _client.connect_to_url(_url) + +func _process(_delta): + _client.poll() + + var state = _client.get_ready_state() + if state == WebSocketPeer.STATE_OPEN: + while _client.get_available_packet_count(): + var packet = _client.get_packet() + var packetString = packet.get_string_from_utf8() + if len(packetString) > 0: + _engineio_decode_packet(packetString) + # TODO: handle binary data? + + elif state == WebSocketPeer.STATE_CLOSED: + set_process(false) + + var code = _client.get_close_code() + var reason = _client.get_close_reason() + + if code == -1: + # -1 is not-clean disconnect (i.e. Service shut down) + # we should try to reconnect + + _connection_state = ConnectionState.RECONNECTING + + _reconnect_timer = Timer.new() + _reconnect_timer.wait_time = 1.0 + _reconnect_timer.timeout.connect(_on_reconnect_timer_timeout) + _reconnect_timer.autostart = true + add_child(_reconnect_timer) + + on_connection_lost.emit() + else: + _connection_state = ConnectionState.DISCONNECTED + on_engine_disconnected.emit(code, reason) + +func _on_reconnect_timer_timeout(): + _client.poll() + var state = _client.get_ready_state() + if state == WebSocketPeer.STATE_CLOSED: + _client.connect_to_url(_url) + else: + set_process(true) + _reconnect_timer.queue_free() + +func _exit_tree(): + if _connection_state == ConnectionState.CONNECTED: + _engineio_send_packet(EngineIOPacketType.close) + + _client.close() + +func _engineio_decode_packet(packet: String): + var packetType = int(packet.substr(0, 1)) + var packetPayload = packet.substr(1) + + match packetType: + EngineIOPacketType.open: + var json = JSON.new() + json.parse(packetPayload) + _sid = json.data["sid"] + _pingTimeout = int(json.data["pingTimeout"]) + _pingInterval = int(json.data["pingInterval"]) + on_engine_connected.emit(_sid) + + EngineIOPacketType.ping: + _engineio_send_packet(EngineIOPacketType.pong) + + EngineIOPacketType.message: + _socketio_parse_packet(packetPayload) + on_engine_message.emit(packetPayload) + +func _engineio_send_packet(type: EngineIOPacketType, payload: String=""): + if len(payload) == 0: + _client.send_text("%d" % type) + else: + _client.send_text("%d%s" % [type, payload]) + +func _socketio_parse_packet(payload: String): + var packetType = int(payload.substr(0, 1)) + payload = payload.substr(1) + + var regex = RegEx.new() + regex.compile("(\\d+)-") + var regexMatch = regex.search(payload) + if regexMatch and regexMatch.get_start() == 0: + payload = payload.substr(regexMatch.get_end()) + push_error("Binary data payload not supported!") + + var name_space = "/" + regex.compile("(\\w),") + regexMatch = regex.search(payload) + if regexMatch and regexMatch.get_start() == 0: + payload = payload.substr(regexMatch.get_end()) + name_space = regexMatch.get_string(1) + + # var ack_id = null + regex.compile("(\\d+)") + regexMatch = regex.search(payload) + if regexMatch and regexMatch.get_start() == 0: + payload = payload.substr(regexMatch.get_end()) + push_warning("Ignoring acknowledge ID!") + + var data = null + if len(payload) > 0: + var json = JSON.new() + if json.parse(payload) == OK: + data = json.data + + match packetType: + SocketIOPacketType.CONNECT: + if _connection_state == ConnectionState.RECONNECTING: + _connection_state = ConnectionState.CONNECTED + on_reconnected.emit(data, name_space, false) + else: + _connection_state = ConnectionState.CONNECTED + on_connect.emit(data, name_space, false) + + SocketIOPacketType.CONNECT_ERROR: + if _connection_state == ConnectionState.RECONNECTING: + _connection_state = ConnectionState.CONNECTED + on_reconnected.emit(data, name_space, true) + else: + _connection_state = ConnectionState.CONNECTED + on_connect.emit(data, name_space, true) + + SocketIOPacketType.EVENT: + if typeof(data) != TYPE_ARRAY: + push_error("Invalid socketio event format!") + var eventName = data[0] + var eventData = data[1] if len(data) > 1 else null + on_event.emit(eventName, eventData, name_space) + +func _socketio_send_packet(type: SocketIOPacketType, name_space: String, data: Variant=null, binaryData: Array[PackedByteArray]=[], ack_id: Variant=null): + var payload = "%d" % type + if binaryData.size() > 0: + payload += "%d-" % binaryData.size() + if name_space != "/": + payload += "%s," % name_space + if ack_id != null: + payload += "%d" % ack_id + if data != null: + payload += "%s" % JSON.stringify(data) + + _engineio_send_packet(EngineIOPacketType.message, payload) + + for binary in binaryData: + _client.put_packet(binary) + +# connect to socket.io server by namespace +func socketio_connect(name_space: String="/"): + _socketio_send_packet(SocketIOPacketType.CONNECT, name_space, _auth) + +# disconnect from socket.io server by namespace +func socketio_disconnect(name_space: String="/"): + if _connection_state == ConnectionState.CONNECTED: + # We should ONLY send disconnect packet when we're connected + _socketio_send_packet(SocketIOPacketType.DISCONNECT, name_space) + + on_disconnect.emit(name_space) + +# send event to socket.io server by namespace +func socketio_send(event_name: String, payload: Variant=null, name_space: String="/"): + if payload == null: + _socketio_send_packet(SocketIOPacketType.EVENT, name_space, [event_name]) + else: + _socketio_send_packet(SocketIOPacketType.EVENT, name_space, [event_name, payload]) + +func get_connection_state() -> ConnectionState: + return _connection_state