Files
Kalulu/sources/menus/settings/teacher_settings.gd
T
2026-01-29 11:14:53 +01:00

538 lines
22 KiB
GDScript

class_name SettingsTeacherSettings
extends Control
const MAIN_MENU_PATH: String = "res://sources/menus/main/main_menu.tscn"
const LOGIN_MENU_PATH: String = "res://sources/menus/login/login.tscn"
const DEVICE_SELECTION_SCENE_PATH: String = "res://sources/menus/device_selection/device_selection.tscn"
const DEVICE_TAB_SCENE: PackedScene = preload("res://sources/menus/settings/device_tab.tscn")
const PASSWORD_VISUALIZER_SCENE: PackedScene = preload("res://sources/menus/components/password_visualizer.tscn")
const EXPORT_COLUMNS: int = 2
const EXPORT_MAX_ROWS: int = 18
const EXPORT_TITLE_FONT_SIZE: int = 44
const EXPORT_SECTION_FONT_SIZE: int = 32
const EXPORT_TEXT_FONT_SIZE: int = 28
const EXPORT_STUDENTS_PER_COLUMN: int = 16
const EXPORT_STUDENTS_PER_PAGE: int = EXPORT_STUDENTS_PER_COLUMN * EXPORT_COLUMNS
var last_device_id: int = -1
@onready var devices_tab_container: TabContainer = %DevicesTabContainer
@onready var lesson_unlocks: LessonUnlocks = $LessonUnlocks
@onready var delete_popup: ConfirmPopup = %DeletePopup
@onready var loading_popup: LoadingPopup = %LoadingPopup
@onready var account_type_option_button: OptionButton = %AccountTypeOptionButton
@onready var education_method_option_button: OptionButton = %EducationMethodOptionButton
@onready var add_device_button: Button = %AddDeviceButton
@onready var add_student_button: Button = %AddStudentButton
@onready var label_internet_mandatory: Label = %LabelInternetMandatory
@onready var add_device_popup: CanvasLayer = %AddDevicePopup
@onready var add_student_popup: CanvasLayer = %AddStudentPopup
@onready var delete_student_popup: CanvasLayer = %DeleteStudentPopup
@onready var export_codes_file_dialog: FileDialog = %ExportCodesFileDialog
func _ready() -> void:
refresh_devices_tabs()
# Internet mandatory to add student because only the server can ensure the student code is not a duplicate
if await ServerManager.check_internet_access():
add_device_button.show()
add_student_button.show()
label_internet_mandatory.hide()
else:
add_device_button.hide()
add_student_button.hide()
label_internet_mandatory.show()
OpeningCurtain.open()
lesson_unlocks.teacher_settings = self
account_type_option_button.select(UserDataManager.teacher_settings.account_type)
education_method_option_button.select(UserDataManager.teacher_settings.education_method)
UserDataManager.user_database_synchronizer.loading_popup = loading_popup
UserDataManager.user_database_synchronizer.account_type_option_button = account_type_option_button
UserDataManager.user_database_synchronizer.education_method_option_button = education_method_option_button
export_codes_file_dialog.set_title(tr("EXPORT_STUDENT_CODES"))
export_codes_file_dialog.set_ok_button_text(tr("EXPORT_STUDENT_CODES"))
export_codes_file_dialog.filters = []
export_codes_file_dialog.add_filter("*.pdf", "pdf")
export_codes_file_dialog.file_selected.connect(_on_export_codes_file_selected)
Log.info("SettingsTeacherSettings: Export codes dialog configured")
func _on_account_type_option_button_item_selected(index: int) -> void:
if TeacherSettings.AccountType.values().has(index):
UserDataManager.teacher_settings.account_type = index as TeacherSettings.AccountType
UserDataManager.teacher_settings.last_modified = Time.get_datetime_string_from_system(true)
UserDataManager.save_teacher_settings()
else:
Log.warn("SettingsTeacherSettings: Cannot assign index %d to AccountType" % index)
func _on_education_method_option_button_item_selected(index: int) -> void:
if TeacherSettings.EducationMethod.values().has(index):
UserDataManager.teacher_settings.education_method = index as TeacherSettings.EducationMethod
UserDataManager.teacher_settings.last_modified = Time.get_datetime_string_from_system(true)
UserDataManager.save_teacher_settings()
else:
Log.warn("SettingsTeacherSettings: Cannot assign index %d to EducationMethod" % index)
func refresh_devices_tabs() -> void:
for child: Node in devices_tab_container.get_children(false):
child.queue_free()
if not UserDataManager.teacher_settings:
Log.error("SettingsTeacherSettings: Teacher settings not found")
return
for device: int in UserDataManager.teacher_settings.students.keys():
var device_tab: DeviceTab
device_tab = DEVICE_TAB_SCENE.instantiate()
device_tab.device_id = device
devices_tab_container.add_child(device_tab)
await get_tree().process_frame # Not optional or an auto-rename bug will occur on the tabs (especially if there are a lot of them)
(device_tab as DeviceTab).students = UserDataManager.teacher_settings.students[device] as Array[StudentData]
device_tab.name = tr("DEVICE_NUMBER").format({"number": device})
device_tab.student_pressed.connect(_on_student_pressed)
device_tab.refresh()
last_device_id = device
func _on_back_button_pressed() -> void:
await OpeningCurtain.close()
if not UserDataManager.get_device_settings().device_id:
get_tree().change_scene_to_file(DEVICE_SELECTION_SCENE_PATH)
else:
get_tree().change_scene_to_file(LOGIN_MENU_PATH)
func _on_delete_button_pressed() -> void:
delete_popup.show()
func _on_delete_popup_accepted() -> void:
var res: Dictionary = await ServerManager.delete_account()
if res.code == 200:
UserDataManager.delete_teacher_data()
UserDataManager.logout()
get_tree().change_scene_to_file(MAIN_MENU_PATH)
func _on_logout_button_pressed() -> void:
await OpeningCurtain.close()
UserDataManager.logout()
get_tree().change_scene_to_file(MAIN_MENU_PATH)
func _on_devices_tab_container_tab_changed(tab: int) -> void:
var device_tab: DeviceTab = devices_tab_container.get_tab_control(tab) as DeviceTab
if not device_tab:
Log.error("SettingsTeacherSettings: DeviceTab not found for tab " + str(tab))
return
lesson_unlocks.device = device_tab.device_id
func _on_student_pressed(code: int) -> void:
lesson_unlocks.student = code
lesson_unlocks.show()
func _on_add_student_button_pressed() -> void:
add_student_popup.show()
func _on_add_student_popup_accepted() -> void:
var current_tab: DeviceTab = devices_tab_container.get_current_tab_control() as DeviceTab
if not current_tab:
Log.error("SettingsTeacherSettings: DeviceTab not found")
return
var res: Dictionary = await ServerManager.add_student({"device": current_tab.device_id})
if res.code == 200:
UserDataManager.update_configuration(res.body as Dictionary)
current_tab.students = UserDataManager.teacher_settings.students[current_tab.device_id]
current_tab.refresh()
else:
Log.error("SettingsTeacherSettings: Request to add student failed. Error code " + str(res.code))
func _on_add_device_button_pressed() -> void:
add_device_popup.show()
func _on_add_device_popup_accepted() -> void:
var res: Dictionary = await ServerManager.add_student({"device": last_device_id + 1})
if res.code == 200:
UserDataManager.update_configuration(res.body as Dictionary)
refresh_devices_tabs()
await get_tree().create_timer(1).timeout
var count: int = devices_tab_container.get_tab_count()
devices_tab_container.current_tab = count -1
func _on_lesson_unlocks_student_deleted(_code: int) -> void:
delete_student_popup.show()
func _on_delete_student_popup_accepted() -> void:
var current_tab: DeviceTab = devices_tab_container.get_current_tab_control() as DeviceTab
if not current_tab:
return
var res: Dictionary = await ServerManager.remove_student(int(lesson_unlocks.student))
if res.code == 200:
lesson_unlocks.hide()
UserDataManager.update_configuration(res.body as Dictionary)
await get_tree().create_timer(1).timeout
if UserDataManager.teacher_settings.students.has(current_tab.device_id):
current_tab.students = UserDataManager.teacher_settings.students[current_tab.device_id]
current_tab.refresh()
else:
refresh_devices_tabs()
func update_student_name(student_code: int, student_name: String) -> void:
for device: DeviceTab in devices_tab_container.get_children(false):
for student_panel: StudentPanel in device.students_container.get_children(false):
if student_panel.student_data.code == student_code:
student_panel.name_label.text = student_name
return
Log.warn("SettingsTeacherSettings: update_student_name: student not found with code " + str(student_code))
#region Synchronization
func _on_dashboard_button_pressed() -> void:
var res: Dictionary = await ServerManager.get_dashboard()
if res.code == 200:
if res.has("body") and res.body is Dictionary:
if (res.body as Dictionary).has("url"):
OS.shell_open(res.body.url as String)
return
Log.error("SettingsTeacherSettings: Request to get Dashboard link has an invalid content")
else:
Log.error("SettingsTeacherSettings: Request to get Dashboard link failed. Error code " + str(res.code))
func _on_synchronize_button_pressed() -> void:
UserDataManager.user_database_synchronizer.synchronize()
func _on_loading_popup_ok() -> void:
loading_popup.hide()
func _on_loading_popup_cancel() -> void:
Log.warn("SettingsTeacherSettings: User wanted to cancel synchronization but it is impossible to interrupt.")
func _on_export_codes_button_pressed() -> void:
if not UserDataManager.teacher_settings:
Log.warn("SettingsTeacherSettings: Cannot export student codes without teacher settings")
return
Log.info("SettingsTeacherSettings: Opening export codes dialog")
export_codes_file_dialog.current_file = "Codes.pdf"
export_codes_file_dialog.show()
func _on_export_codes_file_selected(path: String) -> void:
if not UserDataManager.teacher_settings:
Log.warn("SettingsTeacherSettings: Export canceled because teacher settings are missing")
return
Log.info("SettingsTeacherSettings: Export path selected: %s" % path)
var code_pages: Array[Control] = _build_export_codes_pages()
Log.info("SettingsTeacherSettings: Prepared %d export pages" % code_pages.size())
var page_images: Array[Image] = []
for page: Control in code_pages:
var export_viewport: SubViewport = SubViewport.new()
export_viewport.disable_3d = true
export_viewport.transparent_bg = true
export_viewport.size = page.size
export_viewport.render_target_update_mode = SubViewport.UPDATE_ONCE
export_viewport.add_child(page)
add_child(export_viewport)
await get_tree().process_frame
await get_tree().process_frame
Log.info("SettingsTeacherSettings: Capturing export codes layout page %d/%d" % [page_images.size() + 1, code_pages.size()])
page_images.append(export_viewport.get_texture().get_image())
export_viewport.queue_free()
var export_path: String = path
if not export_path.ends_with(".pdf"):
export_path += ".pdf"
Log.info("SettingsTeacherSettings: Writing export PDF to %s" % export_path)
var save_error: Error = _save_codes_pdf(export_path, page_images)
if save_error != OK:
Log.error("SettingsTeacherSettings: Failed to save export PDF. Error: %s" % error_string(save_error))
else:
Log.info("SettingsTeacherSettings: Export PDF saved successfully")
func _build_export_codes_pages() -> Array[Control]:
Log.info("SettingsTeacherSettings: Building export codes layout")
var pages: Array[Control] = []
var teacher_settings: TeacherSettings = UserDataManager.teacher_settings
var devices: Array = teacher_settings.students.keys()
devices.sort()
Log.info("SettingsTeacherSettings: Building export layout for %d devices" % devices.size())
for device_id: int in devices:
var students: Array[StudentData] = teacher_settings.students[device_id] as Array[StudentData]
Log.info("SettingsTeacherSettings: Splitting device %d into pages of %d students" % [device_id, EXPORT_STUDENTS_PER_PAGE])
for start_index: int in range(0, students.size(), EXPORT_STUDENTS_PER_PAGE):
var end_index: int = min(start_index + EXPORT_STUDENTS_PER_PAGE, students.size())
var current_page: Dictionary = _create_export_codes_page()
var column_containers: Array[VBoxContainer] = current_page["columns"]
var page_students: Array[StudentData] = students.slice(start_index, end_index)
Log.info("SettingsTeacherSettings: Creating export page %d for device %d with %d students" % [
pages.size() + 1,
device_id,
page_students.size()
])
for column_index: int in range(column_containers.size()):
var column_start: int = column_index * EXPORT_STUDENTS_PER_COLUMN
var column_end: int = min(column_start + EXPORT_STUDENTS_PER_COLUMN, page_students.size())
if column_start >= page_students.size():
break
var column_students: Array[StudentData] = page_students.slice(column_start, column_end)
var device_section: VBoxContainer = _build_device_section(device_id, column_students)
column_containers[column_index].add_child(device_section)
pages.append(current_page["canvas"])
return pages
func _create_export_codes_page() -> Dictionary:
var export_canvas: Control = Control.new()
export_canvas.name = "ExportCodesCanvas"
export_canvas.size = Vector2(1920, 1080)
var panel: PanelContainer = PanelContainer.new()
panel.anchor_left = 0.0
panel.anchor_top = 0.0
panel.anchor_right = 1.0
panel.anchor_bottom = 1.0
panel.offset_left = 40
panel.offset_top = 40
panel.offset_right = -40
panel.offset_bottom = -40
panel.theme_type_variation = &"PanelKaluluBig"
var export_panel_style: StyleBoxFlat = StyleBoxFlat.new()
export_panel_style.bg_color = Color.WHITE
panel.set("theme_override_styles/panel", export_panel_style)
export_canvas.add_child(panel)
var main_margin: MarginContainer = MarginContainer.new()
main_margin.anchor_left = 0.0
main_margin.anchor_top = 0.0
main_margin.anchor_right = 1.0
main_margin.anchor_bottom = 1.0
main_margin.offset_left = 40
main_margin.offset_top = 40
main_margin.offset_right = -40
main_margin.offset_bottom = -40
panel.add_child(main_margin)
var main_vbox: VBoxContainer = VBoxContainer.new()
main_vbox.set("theme_override_constants/separation", 24)
main_margin.add_child(main_vbox)
var title_label: Label = Label.new()
title_label.text = tr("STUDENT_CODES")
title_label.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER
title_label.set("theme_override_font_sizes/font_size", EXPORT_TITLE_FONT_SIZE)
title_label.set("theme_override_colors/font_color", Color.BLACK)
main_vbox.add_child(title_label)
var content_hbox: HBoxContainer = HBoxContainer.new()
content_hbox.size_flags_vertical = Control.SIZE_EXPAND_FILL
content_hbox.set("theme_override_constants/separation", 40)
main_vbox.add_child(content_hbox)
var column_containers: Array[VBoxContainer] = []
for column_index: int in range(EXPORT_COLUMNS):
var column: VBoxContainer = VBoxContainer.new()
column.size_flags_horizontal = Control.SIZE_EXPAND_FILL
column.set("theme_override_constants/separation", 18)
content_hbox.add_child(column)
column_containers.append(column)
return {
"canvas": export_canvas,
"columns": column_containers
}
func _build_device_section(device_id: int, students: Array[StudentData]) -> VBoxContainer:
Log.info("SettingsTeacherSettings: Building device %d section with %d students" % [device_id, students.size()])
var section: VBoxContainer = VBoxContainer.new()
section.set("theme_override_constants/separation", 10)
var device_label: Label = Label.new()
device_label.text = tr("DEVICE_NUMBER").format({"number": device_id})
device_label.set("theme_override_font_sizes/font_size", EXPORT_SECTION_FONT_SIZE)
device_label.set("theme_override_colors/font_color", Color.BLACK)
section.add_child(device_label)
var students_container: VBoxContainer = VBoxContainer.new()
students_container.set("theme_override_constants/separation", 8)
section.add_child(students_container)
var sorted_students: Array[StudentData] = students.duplicate()
sorted_students.sort_custom(func(a: StudentData, b: StudentData) -> bool:
return a.name.naturalnocasecmp_to(b.name) < 0
)
for student_index: int in range(sorted_students.size()):
var student_data: StudentData = sorted_students[student_index]
Log.info("SettingsTeacherSettings: Adding student row for %s (%s)" % [student_data.name, str(student_data.code)])
students_container.add_child(_build_student_row(student_data))
if student_index < sorted_students.size() - 1:
students_container.add_child(_build_student_separator())
section.add_child(_build_device_separator())
return section
func _build_student_row(student_data: StudentData) -> HBoxContainer:
Log.trace("SettingsTeacherSettings: Building row for student code %s" % str(student_data.code))
var row: HBoxContainer = HBoxContainer.new()
row.set("theme_override_constants/separation", 12)
var name_label: Label = Label.new()
name_label.text = student_data.name if student_data.name else tr("STUDENT_NUM").format({"number": student_data.code})
name_label.set("theme_override_font_sizes/font_size", EXPORT_TEXT_FONT_SIZE)
name_label.set("theme_override_colors/font_color", Color.BLACK)
name_label.size_flags_horizontal = Control.SIZE_EXPAND_FILL
row.add_child(name_label)
var visualizer: PasswordVisualizer = PASSWORD_VISUALIZER_SCENE.instantiate() as PasswordVisualizer
visualizer.key_size = 32
visualizer.password = str(student_data.code)
visualizer.show_backgrounds = false
row.add_child(visualizer)
return row
func _build_student_separator() -> HSeparator:
var separator: HSeparator = HSeparator.new()
separator.size_flags_horizontal = Control.SIZE_EXPAND_FILL
var line_style: StyleBoxLine = StyleBoxLine.new()
line_style.color = Color("c7c7c7")
line_style.thickness = 2
separator.set("theme_override_styles/separator", line_style)
return separator
func _build_device_separator() -> HSeparator:
var separator: HSeparator = HSeparator.new()
separator.size_flags_horizontal = Control.SIZE_EXPAND_FILL
var line_style: StyleBoxLine = StyleBoxLine.new()
line_style.color = Color("8c8c8c")
line_style.thickness = 4
separator.set("theme_override_styles/separator", line_style)
return separator
func _save_codes_pdf(path: String, images: Array[Image]) -> Error:
if images.is_empty():
Log.error("SettingsTeacherSettings: No pages to export")
return ERR_CANT_CREATE
var jpg_pages: Array[PackedByteArray] = []
for page: Image in images:
Log.info("SettingsTeacherSettings: Starting PDF build (%dx%d)" % [page.get_width(), page.get_height()])
var jpg_data: PackedByteArray = page.save_jpg_to_buffer(0.9)
if jpg_data.is_empty():
Log.error("SettingsTeacherSettings: Failed to encode JPEG buffer for PDF")
return ERR_CANT_CREATE
jpg_pages.append(jpg_data)
var pdf_data: PackedByteArray = PackedByteArray()
var xref_offsets: Array[int] = [0]
Log.info("SettingsTeacherSettings: Creating PDF objects")
_append_pdf_string(pdf_data, "%PDF-1.4\n")
_add_pdf_object(pdf_data, xref_offsets, "1 0 obj\n<< /Type /Catalog /Pages 2 0 R >>\nendobj\n")
var page_count: int = images.size()
var kids_entries: PackedStringArray = []
for page_index: int in range(page_count):
var page_object_id: int = 3 + page_index * 3
kids_entries.append("%d 0 R" % page_object_id)
_add_pdf_object(
pdf_data,
xref_offsets,
"2 0 obj\n<< /Type /Pages /Kids [%s] /Count %d >>\nendobj\n" % [" ".join(kids_entries), page_count]
)
for page_index: int in range(page_count):
var page: Image = images[page_index]
var jpg_data: PackedByteArray = jpg_pages[page_index]
var width: int = page.get_width()
var height: int = page.get_height()
var page_object_id: int = 3 + page_index * 3
var image_object_id: int = page_object_id + 1
var content_object_id: int = page_object_id + 2
var image_name: String = "Im%d" % page_index
_add_pdf_object(
pdf_data,
xref_offsets,
"%d 0 obj\n<< /Type /Page /Parent 2 0 R /Resources << /XObject << /%s %d 0 R >> /ProcSet [/PDF /ImageC] >> /MediaBox [0 0 %d %d] /Contents %d 0 R >>\nendobj\n"
% [page_object_id, image_name, image_object_id, width, height, content_object_id]
)
var image_header: String = "%d 0 obj\n<< /Type /XObject /Subtype /Image /Width %d /Height %d /ColorSpace /DeviceRGB /BitsPerComponent 8 /Filter /DCTDecode /Length %d >>\nstream\n" % [image_object_id, width, height, jpg_data.size()]
_add_pdf_object_with_stream(pdf_data, xref_offsets, image_header, jpg_data)
var content_stream: String = "q\n%d 0 0 %d 0 0 cm\n/%s Do\nQ\n" % [width, height, image_name]
var content_bytes: PackedByteArray = content_stream.to_utf8_buffer()
var content_header: String = "%d 0 obj\n<< /Length %d >>\nstream\n" % [content_object_id, content_bytes.size()]
_add_pdf_object_with_stream(pdf_data, xref_offsets, content_header, content_bytes)
Log.info("SettingsTeacherSettings: Writing PDF xref table")
var xref_offset: int = pdf_data.size()
var xref_lines: PackedStringArray = []
xref_lines.append("xref")
xref_lines.append("0 %d" % xref_offsets.size())
xref_lines.append("0000000000 65535 f ")
for offset: int in xref_offsets.slice(1, xref_offsets.size()):
xref_lines.append("%010d 00000 n " % offset)
_append_pdf_string(pdf_data, "\n".join(xref_lines) + "\n")
_append_pdf_string(pdf_data, "trailer\n<< /Size %d /Root 1 0 R >>\nstartxref\n%d\n%%EOF\n" % [xref_offsets.size(), xref_offset])
var file: FileAccess = FileAccess.open(path, FileAccess.WRITE)
if not file:
Log.error("SettingsTeacherSettings: Failed to open export path for writing")
return ERR_CANT_OPEN
Log.info("SettingsTeacherSettings: Storing PDF buffer (%d bytes)" % pdf_data.size())
file.store_buffer(pdf_data)
file.close()
Log.info("SettingsTeacherSettings: PDF file written")
return OK
func _add_pdf_object(pdf_data: PackedByteArray, offsets: Array[int], object_text: String) -> void:
offsets.append(pdf_data.size())
_append_pdf_string(pdf_data, object_text)
func _add_pdf_object_with_stream(pdf_data: PackedByteArray, offsets: Array[int], header: String, stream_data: PackedByteArray) -> void:
offsets.append(pdf_data.size())
_append_pdf_string(pdf_data, header)
pdf_data.append_array(stream_data)
_append_pdf_string(pdf_data, "\nendstream\nendobj\n")
func _append_pdf_string(pdf_data: PackedByteArray, text: String) -> void:
pdf_data.append_array(text.to_utf8_buffer())
#endregion