740 lines
28 KiB
GDScript
740 lines
28 KiB
GDScript
extends Control
|
||
|
||
enum FileCheckResult {
|
||
OK,
|
||
ERROR_NOT_FOUND,
|
||
ERROR_CASE_MISMATCH
|
||
}
|
||
|
||
const BASE_PATH: String = "user://language_resources/"
|
||
const SAVE_FILE_PATH: String = "user://prof_tool_save.tres"
|
||
|
||
var save_file: ProfToolSave
|
||
|
||
@onready var language_select_button: OptionButton = %LanguageSelectButton
|
||
@onready var new_language_container: PanelContainer = %NewLanguageContainer
|
||
@onready var line_edit: LineEdit = %LineEdit
|
||
@onready var file_dialog: FileDialog = $FileDialog
|
||
@onready var file_dialog_export: FileDialog = $FileDialogExport
|
||
@onready var add_word_list_button: Button = $VBoxContainer/AddWordListButton
|
||
@onready var error_label: Label = %ErrorLabel
|
||
@onready var tab_container: TabContainer = $CenterContainer/TabContainer
|
||
|
||
|
||
func _ready() -> void:
|
||
MusicManager.stop()
|
||
Database.load_additional_word_list()
|
||
_update_add_word_list_button()
|
||
if not ResourceLoader.exists(SAVE_FILE_PATH):
|
||
save_file = ProfToolSave.new()
|
||
save_file.resource_path = SAVE_FILE_PATH
|
||
ResourceSaver.save(save_file, SAVE_FILE_PATH)
|
||
save_file = load(SAVE_FILE_PATH)
|
||
_display_available_languages()
|
||
tab_container.current_tab = Globals.main_menu_selected_tab
|
||
tab_container.tab_changed.connect(_on_tab_container_tab_changed)
|
||
|
||
|
||
func _display_available_languages() -> void:
|
||
for item: int in range(1, language_select_button.item_count):
|
||
language_select_button.remove_item(item)
|
||
var available_languages: Array[String] = _get_available_languages()
|
||
var ind: int = 1
|
||
for available_language: String in available_languages:
|
||
language_select_button.add_item(available_language)
|
||
if available_language == save_file.selected_language:
|
||
Database.language = save_file.selected_language
|
||
language_select_button.select(ind)
|
||
ind += 1
|
||
|
||
|
||
func _update_add_word_list_button() -> void:
|
||
if FileAccess.file_exists(Database.get_additional_word_list_path()):
|
||
add_word_list_button.text = "Change Word List"
|
||
else:
|
||
add_word_list_button.text = "Add Word List"
|
||
|
||
|
||
func _on_gp_list_button_pressed() -> void:
|
||
get_tree().change_scene_to_file("res://sources/language_tool/gp_list.tscn")
|
||
|
||
|
||
func _on_word_list_button_pressed() -> void:
|
||
get_tree().change_scene_to_file("res://sources/language_tool/word_list.tscn")
|
||
|
||
|
||
func _on_syllable_list_button_pressed() -> void:
|
||
get_tree().change_scene_to_file("res://sources/language_tool/syllable_list.tscn")
|
||
|
||
|
||
func _on_sentence_list_button_pressed() -> void:
|
||
get_tree().change_scene_to_file("res://sources/language_tool/sentence_list.tscn")
|
||
|
||
|
||
func _on_lesson_list_button_pressed() -> void:
|
||
get_tree().change_scene_to_file("res://sources/language_tool/lessons_list.tscn")
|
||
|
||
|
||
func _on_tracing_builder_button_pressed() -> void:
|
||
get_tree().change_scene_to_file("res://sources/language_tool/tracing_builder.tscn")
|
||
|
||
|
||
func _on_data_loader_button_pressed() -> void:
|
||
get_tree().change_scene_to_file("res://sources/language_tool/gp_image_and_sound_descriptions.tscn")
|
||
|
||
|
||
func _on_data_loader_button_2_pressed() -> void:
|
||
get_tree().change_scene_to_file("res://sources/language_tool/gp_video_descriptions.tscn")
|
||
|
||
|
||
func _on_game_sounds_button_pressed() -> void:
|
||
get_tree().change_scene_to_file("res://sources/language_tool/in_game_sounds.tscn")
|
||
|
||
|
||
func _on_pseudowords_button_pressed() -> void:
|
||
get_tree().change_scene_to_file("res://sources/language_tool/fish_word_list.tscn")
|
||
|
||
|
||
func _on_speeches_button_pressed() -> void:
|
||
get_tree().change_scene_to_file("res://sources/language_tool/kalulu_speeches.tscn")
|
||
|
||
|
||
func _on_exercises_button_pressed() -> void:
|
||
get_tree().change_scene_to_file("res://sources/language_tool/lesson_exercises.tscn")
|
||
|
||
|
||
func _on_export_filename_selected(filename: String) -> void:
|
||
var version_file: FileAccess = FileAccess.open(BASE_PATH.path_join(Database.language).path_join("version.txt"), FileAccess.WRITE)
|
||
version_file.store_line(Time.get_datetime_string_from_system(true, false))
|
||
version_file.close()
|
||
|
||
var summary_file: FileAccess = FileAccess.open(BASE_PATH.path_join(Database.language).path_join("summary.txt"), FileAccess.WRITE)
|
||
var sentences_by_lesson: Dictionary = Database.get_sentences_by_lessons()
|
||
for index: int in range(Database.get_lessons_count()):
|
||
|
||
var lesson: int = index +1
|
||
|
||
summary_file.store_line("Lesson %s --------------------" % lesson)
|
||
summary_file.store_line("\t \t Words ---")
|
||
var words: String = ""
|
||
for element: Dictionary in Database.get_words_for_lesson(lesson, true):
|
||
words += element.Word + ", "
|
||
summary_file.store_line(words.trim_suffix(", "))
|
||
summary_file.store_line("\n")
|
||
summary_file.store_line("\t \t Syllables ---")
|
||
var syllables: String = ""
|
||
for element: Dictionary in Database.get_syllables_for_lesson(lesson, true):
|
||
syllables += element.Grapheme + ", "
|
||
summary_file.store_line(syllables.trim_suffix(", "))
|
||
summary_file.store_line("\n")
|
||
summary_file.store_line("\t \t Sentences ---")
|
||
for element: Dictionary in Database.get_sentences(lesson, true, sentences_by_lesson):
|
||
summary_file.store_line(element.Sentence as String)
|
||
summary_file.store_line("\n\n")
|
||
summary_file.close()
|
||
|
||
_create_gp_csv()
|
||
_create_words_csv()
|
||
_create_syllable_csv()
|
||
_create_sentence_csv()
|
||
|
||
var folder_zipper: FolderZipper = FolderZipper.new()
|
||
folder_zipper.compress(BASE_PATH.path_join(Database.language), filename)
|
||
|
||
#region Database integrity check
|
||
var integrity_checking: bool = false
|
||
@onready var check_box_log: CheckBox = $CheckIntegrityButton/CheckBoxLog
|
||
var integrity_log_path: String = "user://database-integrity-log.txt"
|
||
var total_integrity_warnings: int = 0
|
||
|
||
|
||
func _check_db_integrity() -> void:
|
||
if integrity_checking:
|
||
return
|
||
integrity_checking = true
|
||
error_label.text = "Database integrity check started."
|
||
total_integrity_warnings = 0
|
||
await get_tree().process_frame
|
||
if check_box_log.button_pressed:
|
||
if FileAccess.file_exists(integrity_log_path):
|
||
DirAccess.remove_absolute(integrity_log_path)
|
||
|
||
var sentences_list: Dictionary = Database.get_sentences_by_lessons()
|
||
if sentences_list.is_empty():
|
||
if !log_message("No sentence found in database"):
|
||
return
|
||
var lesson_id: int
|
||
var known_words_list: Dictionary = {}
|
||
var known_gps_list: Array[Dictionary] = []
|
||
var gp_known: bool = false
|
||
for index: int in range(Database.get_lessons_count()):
|
||
lesson_id = index +1
|
||
error_label.text = "Database integrity checks lesson " + str(lesson_id)
|
||
await get_tree().process_frame
|
||
Logger.trace("ProfToolMenu: Lesson_id = " + str(lesson_id))
|
||
|
||
var new_gps_for_lesson: Array = Database.get_gps_for_lesson(lesson_id, false, true, false, false, true)
|
||
for new_gp: Dictionary in new_gps_for_lesson:
|
||
if !new_gp.has("ID"):
|
||
if !log_message("GP with no ID in lesson " + str(lesson_id)):
|
||
return
|
||
if !new_gp.has("Grapheme"):
|
||
if !log_message("GP (ID " + str(new_gp.ID) + ") with no Grapheme in lesson " + str(lesson_id)):
|
||
return
|
||
if !new_gp.has("Phoneme"):
|
||
if !log_message("GP (ID " + str(new_gp.ID) + ") with no Phoneme in lesson " + str(lesson_id)):
|
||
return
|
||
var exists: bool = known_gps_list.any(func(d: Dictionary) -> bool: return d == new_gp)
|
||
if exists:
|
||
if !log_message("GP ID " + str(new_gp.ID) + " already exists in lesson " + str(lesson_id)):
|
||
return
|
||
else:
|
||
known_gps_list.push_back(new_gp)
|
||
|
||
var new_words_for_lesson: Array = Database.get_words_for_lesson(lesson_id, true, 1, 99, true)
|
||
for new_word: Dictionary in new_words_for_lesson:
|
||
if !new_word.has("ID"):
|
||
if !log_message("Word with no ID in lesson ID " + str(lesson_id)):
|
||
return
|
||
if !new_word.has("GPs"):
|
||
if !log_message("Word with no GPs (key) at lesson ID " + str(lesson_id) + " and word ID " + str(new_word.ID)):
|
||
return
|
||
if !new_word.has("Word"):
|
||
if !log_message("Word with no Word (key) at lesson ID " + str(lesson_id) + " and word ID " + str(new_word.ID)):
|
||
return
|
||
for gp: Dictionary in new_word.GPs:
|
||
if !gp.has("ID"):
|
||
if !log_message("GP with no ID (key) at lesson ID " + str(lesson_id) + " in word " + (new_word.Word as String) + " (ID " + str(new_word.ID) + ")"):
|
||
return
|
||
gp_known = false
|
||
for known_gp: Dictionary in known_gps_list:
|
||
if known_gp.ID == gp.ID:
|
||
gp_known = true
|
||
break
|
||
if !gp_known:
|
||
if !log_message("Word " + (new_word.Word as String) + " with unknown GP (ID " + str(gp.ID) + ") at lesson ID " + str(lesson_id) + " and word ID " + str(new_word.ID)):
|
||
return
|
||
if known_words_list.has(new_word.Word):
|
||
if !log_message('Word "' + (new_word.Word as String) + '" is introduced in lesson ID ' + str(lesson_id) + " but it already was introduced in lesson ID " + str(known_words_list[new_word.Word])):
|
||
return
|
||
known_words_list[new_word.Word] = lesson_id
|
||
|
||
if sentences_list.has(lesson_id):
|
||
for sentence: Dictionary in sentences_list[lesson_id]:
|
||
if !sentence.has("ID"):
|
||
if !log_message("Sentence with no ID in lesson ID " + str(lesson_id)):
|
||
return
|
||
if !sentence.has("Sentence"):
|
||
if !log_message("Sentence with no Sentence (key) at lesson ID " + str(lesson_id) + " and sentence ID " + str(sentence.ID)):
|
||
return
|
||
var word_count: int = ((sentence.Sentence) as String).replace("!", " ").replace("?", " ").replace(".", " ").replace(",", " ").replace(";", " ").replace(":", " ").replace("'", " ").replace(" ", " ").trim_suffix(" ").split(" ", false).size()
|
||
var word_list: Array[Dictionary] = Database.get_words_in_sentence(sentence.ID as int)
|
||
if word_list.size() != word_count:
|
||
if !log_message('Sentence "' + (sentence.Sentence as String) + '" has incoherent words count: ' + str(word_list.size())):
|
||
return
|
||
for word: Dictionary in word_list:
|
||
if !word.has("ID"):
|
||
if !log_message("Word with no ID in lesson ID " + str(lesson_id) + " and sentence ID " + str(sentence.ID)):
|
||
return
|
||
if !word.has("Word"):
|
||
if !log_message("Word with no Word (key) at lesson ID " + str(lesson_id) + ", sentence ID " + str(sentence.ID) + " and word ID " + str(word.ID)):
|
||
return
|
||
if !known_words_list.has(word.Word):
|
||
if !log_message('Word "' + (word.Word as String) + '" is used in lesson ID ' + str(lesson_id) + ", sentence ID " + str(sentence.ID) + ", but it has not been introduced yet."):
|
||
return
|
||
else:
|
||
continue #No sentence in this lesson
|
||
|
||
error_label.text = "Database integrity checks all GP sounds file names."
|
||
var all_gps: Array = Database.get_gps_for_lesson(Database.get_lessons_count() + 1, false, false, false, true, true)
|
||
for gp: Dictionary in all_gps:
|
||
var gp_sound_path: String = Database.get_gp_sound_path(gp)
|
||
var result: Dictionary = file_exists_case_sensitive(gp_sound_path)
|
||
match result.status:
|
||
FileCheckResult.OK:
|
||
pass # Nothing
|
||
FileCheckResult.ERROR_CASE_MISMATCH:
|
||
if !log_message("GP " + Database.get_gp_name(gp) + " sound file exists, but its name does not match the expected casing"):
|
||
return
|
||
FileCheckResult.ERROR_NOT_FOUND:
|
||
if !log_message("GP " + Database.get_gp_name(gp) + " sound file does not exists"):
|
||
return
|
||
|
||
error_label.text = "Database integrity check finished. " + str(total_integrity_warnings) + " warnings found."
|
||
integrity_checking = false
|
||
if check_box_log.button_pressed:
|
||
var file_path: String = ProjectSettings.globalize_path(integrity_log_path)
|
||
Logger.trace("ProfToolMenu: Logs saved at " + file_path)
|
||
OS.shell_open(file_path)
|
||
|
||
#endregion
|
||
|
||
func log_message(message: String) -> bool:
|
||
total_integrity_warnings += 1
|
||
if check_box_log.button_pressed:
|
||
var file: FileAccess
|
||
if FileAccess.file_exists(integrity_log_path):
|
||
file = FileAccess.open(integrity_log_path, FileAccess.READ_WRITE)
|
||
file.seek_end() # Se placer à la fin pour ajouter
|
||
else:
|
||
file = FileAccess.open(integrity_log_path, FileAccess.WRITE_READ)
|
||
if file:
|
||
file.seek_end()
|
||
file.store_line(message)
|
||
file.close()
|
||
else:
|
||
Logger.warn("ProfToolMenu: Integrity log file not found")
|
||
return true
|
||
else:
|
||
error_label.text = message
|
||
integrity_checking = false
|
||
return false
|
||
|
||
|
||
func file_exists_case_sensitive(path: String) -> Dictionary:
|
||
var result: Dictionary = {
|
||
"status": FileCheckResult.OK,
|
||
"exists": false
|
||
}
|
||
|
||
if not FileAccess.file_exists(path):
|
||
result.status = FileCheckResult.ERROR_NOT_FOUND
|
||
return result
|
||
|
||
var dir_path: String = path.get_base_dir()
|
||
var file_name: String = path.get_file()
|
||
|
||
var dir: DirAccess = DirAccess.open(dir_path)
|
||
if dir == null:
|
||
result.status = FileCheckResult.ERROR_NOT_FOUND
|
||
return result
|
||
|
||
dir.list_dir_begin()
|
||
while true:
|
||
var test_name: String = dir.get_next()
|
||
if test_name == "":
|
||
break
|
||
if !dir.current_is_dir():
|
||
if test_name == file_name:
|
||
result.exists = true
|
||
result.status = FileCheckResult.OK
|
||
dir.list_dir_end()
|
||
return result
|
||
elif test_name.to_lower() == file_name.to_lower():
|
||
result.status = FileCheckResult.ERROR_CASE_MISMATCH
|
||
dir.list_dir_end()
|
||
|
||
if result.status != FileCheckResult.ERROR_CASE_MISMATCH:
|
||
result.status = FileCheckResult.ERROR_NOT_FOUND
|
||
|
||
return result
|
||
|
||
|
||
func _get_available_languages() -> Array[String]:
|
||
var available_languages: Array[String] = []
|
||
var dir: DirAccess = DirAccess.open(BASE_PATH)
|
||
if dir:
|
||
dir.list_dir_begin()
|
||
var file_name: String = dir.get_next()
|
||
while file_name != "":
|
||
if dir.current_is_dir():
|
||
if FileAccess.file_exists(BASE_PATH.path_join(file_name).path_join("language.db")):
|
||
available_languages.append(file_name)
|
||
file_name = dir.get_next()
|
||
dir.list_dir_end()
|
||
return available_languages
|
||
|
||
|
||
func _on_language_select_button_item_selected(index: int) -> void:
|
||
if index == 0:
|
||
new_language_container.show()
|
||
line_edit.grab_focus()
|
||
return
|
||
|
||
save_file.selected_language = language_select_button.get_item_text(index)
|
||
ResourceSaver.save(save_file, SAVE_FILE_PATH)
|
||
Database.language = save_file.selected_language
|
||
|
||
|
||
func _on_validate_language_pressed() -> void:
|
||
if not line_edit.text:
|
||
return
|
||
|
||
DirAccess.make_dir_recursive_absolute(BASE_PATH.path_join(line_edit.text))
|
||
var file: FileAccess = FileAccess.open("res://model_database.db", FileAccess.READ)
|
||
var dest: FileAccess = FileAccess.open(BASE_PATH.path_join(line_edit.text).path_join("language.db"), FileAccess.WRITE)
|
||
dest.store_buffer(file.get_buffer(file.get_length()))
|
||
file.close()
|
||
dest.close()
|
||
save_file.selected_language = line_edit.text
|
||
ResourceSaver.save(save_file, SAVE_FILE_PATH)
|
||
get_tree().reload_current_scene()
|
||
|
||
|
||
func _word_list_file_selected(file_path: String) -> void:
|
||
if FileAccess.file_exists(file_path):
|
||
DirAccess.copy_absolute(file_path, Database.get_additional_word_list_path())
|
||
var msg: String = Database.load_additional_word_list()
|
||
error_label.text = msg
|
||
# Check that the words we have in database have the correct gpmatch, if not update them
|
||
var query: String = "SELECT Words.ID as WordId, Word, group_concat(Grapheme, ' ') as Graphemes, group_concat(Phoneme, ' ') as Phonemes, group_concat(GPs.ID, ' ') as GPIDs, Words.Exception
|
||
FROM Words
|
||
INNER JOIN ( SELECT * FROM GPsInWords ORDER BY GPsInWords.Position ) GPsInWords ON Words.ID = GPsInWords.WordID
|
||
INNER JOIN GPs ON GPs.ID = GPsInWords.GPID
|
||
GROUP BY Words.ID"
|
||
Database.db.query(query)
|
||
var results: Array[Dictionary] = Database.db.query_result
|
||
var word_list_element: WordListElement = (load("res://sources/language_tool/word_list_element.tscn") as PackedScene).instantiate()
|
||
for result: Dictionary in results:
|
||
var same: bool = true
|
||
var master_gpmatch: String = Database.additional_word_list[result.Word].GPMATCH
|
||
var master_gplist: PackedStringArray = master_gpmatch.trim_prefix("(").trim_suffix(")").split(".")
|
||
var graphemes: PackedStringArray = (result.Graphemes as String).split(" ")
|
||
var phonemes: PackedStringArray = (result.Phonemes as String).split(" ")
|
||
if master_gplist.size() != graphemes.size():
|
||
same = false
|
||
else:
|
||
for index: int in range(master_gplist.size()):
|
||
same = same and (graphemes[index] + "-" + phonemes[index] == master_gplist[index])
|
||
if not same:
|
||
Database.db.delete_rows("Words", "ID=%s" % result.WordId)
|
||
word_list_element._add_from_additional_word_list(result.Word as String)
|
||
|
||
|
||
func _on_add_word_list_button_pressed() -> void:
|
||
file_dialog.filters = []
|
||
file_dialog.add_filter("*.csv", "csv")
|
||
Utils.disconnect_all(file_dialog.file_selected)
|
||
file_dialog.file_selected.connect(_word_list_file_selected)
|
||
file_dialog.show()
|
||
_update_add_word_list_button()
|
||
|
||
|
||
func _on_import_language_button_pressed() -> void:
|
||
file_dialog.filters = []
|
||
file_dialog.add_filter("*.zip", "zip")
|
||
Utils.disconnect_all(file_dialog.file_selected)
|
||
file_dialog.file_selected.connect(_language_data_selected)
|
||
file_dialog.show()
|
||
|
||
|
||
func _on_export_button_pressed() -> void:
|
||
file_dialog_export.filters = []
|
||
file_dialog_export.add_filter("*.zip", "zip")
|
||
Utils.disconnect_all(file_dialog_export.file_selected)
|
||
file_dialog_export.file_selected.connect(_on_export_filename_selected)
|
||
file_dialog_export.show()
|
||
|
||
|
||
func _language_data_selected(file_path: String) -> void:
|
||
if FileAccess.file_exists(file_path):
|
||
var folder_unzipper: FolderUnzipper = FolderUnzipper.new()
|
||
folder_unzipper.extract(file_path, BASE_PATH, false)
|
||
get_tree().reload_current_scene()
|
||
|
||
|
||
func _create_gp_csv() -> void:
|
||
var gp_list_file: FileAccess = FileAccess.open(BASE_PATH.path_join(Database.language).path_join("gp_list.csv"), FileAccess.WRITE)
|
||
gp_list_file.store_csv_line(["Grapheme", "Phoneme", "Type", "Exception"])
|
||
var query: String = "Select * FROM GPs ORDER BY GPs.Grapheme"
|
||
Database.db.query(query)
|
||
var result: Array[Dictionary] = Database.db.query_result
|
||
var types_text: Array[String] = ["Silent", "Vowel", "Consonant"]
|
||
for element: Dictionary in result:
|
||
gp_list_file.store_csv_line([element.Grapheme, element.Phoneme, types_text[element.Type], element.Exception])
|
||
|
||
|
||
func _create_words_csv() -> void:
|
||
var gp_list_file: FileAccess = FileAccess.open(BASE_PATH.path_join(Database.language).path_join("words_list.csv"), FileAccess.WRITE)
|
||
gp_list_file.store_csv_line(["ORTHO", "GPMATCH", "LESSON", "READING", "WRITING"])
|
||
var query: String = "SELECT Words.ID as WordId, Word, group_concat(Grapheme, ' ') as Graphemes, group_concat(Phoneme, ' ') as Phonemes, group_concat(GPs.ID, ' ') as GPIDs, Words.Exception, Reading, Writing
|
||
FROM Words
|
||
INNER JOIN ( SELECT * FROM GPsInWords ORDER BY GPsInWords.Position ) GPsInWords ON Words.ID = GPsInWords.WordID
|
||
INNER JOIN GPs ON GPs.ID = GPsInWords.GPID
|
||
GROUP BY Words.ID"
|
||
Database.db.query(query)
|
||
var result: Array[Dictionary] = Database.db.query_result
|
||
for element: Dictionary in result:
|
||
var gpmatch: String = "("
|
||
var graphemes: PackedStringArray = (element.Graphemes as String).split(" ")
|
||
var phonemes: PackedStringArray = (element.Phonemes as String).split(" ")
|
||
for index: int in range(graphemes.size() - 1):
|
||
gpmatch += graphemes[index] + "-" + phonemes[index] + "."
|
||
var last_index: int = graphemes.size() - 1
|
||
gpmatch += graphemes[last_index] + "-" + phonemes[last_index] + ")"
|
||
var lesson: int = -1
|
||
for gp_id: String in (element.GPIDs as String).split(' '):
|
||
var gp_id_lesson: int = Database.get_min_lesson_for_gp_id(int(gp_id))
|
||
if gp_id_lesson < 0:
|
||
lesson = -1
|
||
break
|
||
lesson = maxi(lesson, gp_id_lesson)
|
||
gp_list_file.store_csv_line([element.Word, gpmatch, lesson, element.Reading, element.Writing])
|
||
|
||
|
||
func _create_syllable_csv() -> void:
|
||
var gp_list_file: FileAccess = FileAccess.open(BASE_PATH.path_join(Database.language).path_join("syllables_list.csv"), FileAccess.WRITE)
|
||
gp_list_file.store_csv_line(["ORTHO", "GPMATCH", "LESSON", "READING", "WRITING"])
|
||
var query: String = "SELECT Syllables.ID as SyllableId, Syllable, group_concat(Grapheme, ' ') as Graphemes, group_concat(Phoneme, ' ') as Phonemes, group_concat(GPs.ID, ' ') as GPIDs, Syllables.Exception, Reading, Writing
|
||
FROM Syllables
|
||
INNER JOIN ( SELECT * FROM GPsInSyllables ORDER BY GPsInSyllables.Position ) GPsInSyllables ON Syllables.ID = GPsInSyllables.SyllableID
|
||
INNER JOIN GPs ON GPs.ID = GPsInSyllables.GPID
|
||
GROUP BY Syllables.ID"
|
||
Database.db.query(query)
|
||
var result: Array[Dictionary] = Database.db.query_result
|
||
for element: Dictionary in result:
|
||
var gpmatch: String = "("
|
||
var graphemes: PackedStringArray = (element.Graphemes as String).split(" ")
|
||
var phonemes: PackedStringArray = (element.Phonemes as String).split(" ")
|
||
for index: int in range(graphemes.size() - 1):
|
||
gpmatch += graphemes[index] + "-" + phonemes[index] + "."
|
||
var last_index: int = graphemes.size() - 1
|
||
gpmatch += graphemes[last_index] + "-" + phonemes[last_index] + ")"
|
||
var lesson: int = -1
|
||
for gp_id: String in (element.GPIDs as String).split(' '):
|
||
var gp_id_lesson: int = Database.get_min_lesson_for_gp_id(int(gp_id))
|
||
if gp_id_lesson < 0:
|
||
lesson = -1
|
||
break
|
||
lesson = maxi(lesson, gp_id_lesson)
|
||
gp_list_file.store_csv_line([element.Syllable, gpmatch, lesson, element.Reading, element.Writing])
|
||
|
||
|
||
func _create_sentence_csv() -> void:
|
||
var gp_list_file: FileAccess = FileAccess.open(BASE_PATH.path_join(Database.language).path_join("sentences_list.csv"), FileAccess.WRITE)
|
||
gp_list_file.store_csv_line(["Sentence", "Lesson"])
|
||
var query: String = "SELECT Sentences.ID as SentenceId, Sentence, group_concat(Word, ' ') as Words, group_concat(Word, ' ') as Words, group_concat(Words.ID, ' ') as WordIDs, Sentences.Exception
|
||
FROM Sentences
|
||
INNER JOIN ( SELECT * FROM WordsInSentences ORDER BY WordsInSentences.Position ) WordsInSentences ON Sentences.ID = WordsInSentences.SentenceID
|
||
INNER JOIN Words ON Words.ID = WordsInSentences.WordID
|
||
GROUP BY Sentences.ID"
|
||
Database.db.query(query)
|
||
var result: Array[Dictionary] = Database.db.query_result
|
||
for element: Dictionary in result:
|
||
var lesson: int = -1
|
||
for word_id: String in (element.WordIDs as String).split(' '):
|
||
var ind: int = Database.get_min_lesson_for_word_id(int(word_id))
|
||
if ind < 0:
|
||
lesson = -1
|
||
break
|
||
lesson = maxi(lesson, ind)
|
||
gp_list_file.store_csv_line([element.Sentence, lesson])
|
||
|
||
|
||
func _on_open_folder_button_pressed() -> void:
|
||
OS.shell_show_in_file_manager(ProjectSettings.globalize_path(BASE_PATH))
|
||
|
||
|
||
func _on_tab_container_tab_changed(tab: int) -> void:
|
||
Globals.main_menu_selected_tab = tab
|
||
|
||
#region Book Generation
|
||
|
||
func create_book() -> void:
|
||
var lang_path: String = BASE_PATH.path_join(Database.language)
|
||
var file_names: Dictionary[String, String] = {
|
||
"word": "words_list.csv",
|
||
"syllable": "syllables_list.csv",
|
||
"sentence": "sentences_list.csv",
|
||
}
|
||
|
||
var columns: Dictionary[String, PackedStringArray] = {}
|
||
var all_headers: Array[String] = []
|
||
var headers_seen: Dictionary[String, bool] = {}
|
||
|
||
for category: String in file_names.keys():
|
||
var file_path: String = lang_path.path_join(file_names[category])
|
||
var file: FileAccess = FileAccess.open(file_path, FileAccess.READ)
|
||
if file == null:
|
||
Logger.error("ProfToolMenu: Error opening file: " + file_path)
|
||
continue
|
||
|
||
if file.eof_reached():
|
||
file.close()
|
||
continue
|
||
|
||
var headers_line: String = read_csv_record(file)
|
||
var raw_headers: PackedStringArray = parse_csv_line(headers_line)
|
||
var header_map: Dictionary[String, String] = {} # Original -> Normalized
|
||
|
||
# On mesure combien de lignes ont déjà été ajoutées
|
||
var current_row_count: int = 0
|
||
if columns.has("Categorie"):
|
||
current_row_count = columns["Categorie"].size()
|
||
|
||
for header: String in raw_headers:
|
||
var normalized: String = normalize_header(header)
|
||
header_map[header] = normalized
|
||
|
||
if normalized != "Writing" and normalized != "Reading" and normalized != "Categorie" and not headers_seen.has(normalized):
|
||
headers_seen[normalized] = true
|
||
all_headers.append(normalized)
|
||
var filler: PackedStringArray
|
||
filler.resize(current_row_count)
|
||
for index: int in range(current_row_count):
|
||
filler[index] = "" # Valeur vide pour rattraper
|
||
columns[normalized] = filler
|
||
|
||
# Init colonne "Categorie" si pas encore
|
||
if not columns.has("Categorie"):
|
||
var filler: PackedStringArray
|
||
filler.resize(current_row_count)
|
||
for index: int in range(current_row_count):
|
||
filler[index] = ""
|
||
columns["Categorie"] = filler
|
||
|
||
while not file.eof_reached():
|
||
var line: String = read_csv_record(file)
|
||
if line.strip_edges() == "":
|
||
continue
|
||
|
||
var values: PackedStringArray = parse_csv_line(line)
|
||
if values.size() != raw_headers.size():
|
||
Logger.warn("ProfToolMenu: Malformed line ignored: %s" % values)
|
||
continue
|
||
|
||
var row_dict: Dictionary[String, String] = {}
|
||
for index: int in range(values.size()):
|
||
var original: String = raw_headers[index]
|
||
var normalized: String = header_map.get(original, original)
|
||
row_dict[normalized] = values[index]
|
||
|
||
# Ligne principale
|
||
add_row(columns, row_dict, category, all_headers)
|
||
|
||
if row_dict.get("Writing", "0") == "1":
|
||
add_row(columns, row_dict, "writing", all_headers)
|
||
|
||
if row_dict.get("Reading", "0") == "1":
|
||
add_row(columns, row_dict, "reading", all_headers)
|
||
|
||
file.close()
|
||
|
||
# Forcer "Lesson" en tête
|
||
var ordered_headers: Array[String] = all_headers.duplicate()
|
||
if "Lesson" in ordered_headers:
|
||
ordered_headers.erase("Lesson")
|
||
ordered_headers = ["Lesson"] + ordered_headers
|
||
|
||
# Ajouter Categorie à la fin
|
||
ordered_headers.append("Categorie")
|
||
|
||
# Écriture du fichier final
|
||
var output_path: String = lang_path.path_join("booklet.csv")
|
||
var output_file: FileAccess = FileAccess.open(output_path, FileAccess.WRITE)
|
||
if output_file == null:
|
||
Logger.error("ProfToolMenu: Impossible to write: " + output_path)
|
||
return
|
||
|
||
output_file.store_line(escape_csv_line(PackedStringArray(ordered_headers)))
|
||
|
||
var row_count: int = (columns["Categorie"] as PackedStringArray).size() # Toutes les colonnes sont synchronisées
|
||
for index: int in range(row_count):
|
||
var row: PackedStringArray = []
|
||
for header: String in ordered_headers:
|
||
row.append(columns[header][index] as String)
|
||
output_file.store_line(escape_csv_line(row))
|
||
|
||
output_file.close()
|
||
|
||
error_label.text = "📘 Export data of the booklet finished to path: " + output_path
|
||
Logger.trace("ProfToolMenu: " + error_label.text)
|
||
|
||
|
||
# Fonction qui ajoute une ligne au dictionnaire
|
||
func add_row(dict: Dictionary[String, PackedStringArray], row_data: Dictionary[String, String], categorie: String, all_headers: Array) -> void:
|
||
# Nombre de lignes déjà enregistrées (doit être égal pour chaque colonne)
|
||
var current_size: int = 0
|
||
if dict.has("Categorie"):
|
||
current_size= dict["Categorie"].size()
|
||
|
||
# S'assurer que toutes les colonnes existantes reçoivent une valeur
|
||
for header: String in all_headers:
|
||
if not dict.has(header):
|
||
var filler: PackedStringArray
|
||
filler.resize(current_size) # rattrape les lignes précédentes
|
||
for index: int in range(current_size):
|
||
filler[index] = ""
|
||
dict[header] = filler
|
||
dict[header].append(row_data.get(header, "") as String)
|
||
|
||
# Special column: "Categorie"
|
||
if not dict.has("Categorie"):
|
||
var filler: PackedStringArray
|
||
filler.resize(current_size)
|
||
for index: int in range(current_size):
|
||
filler[index] = ""
|
||
dict["Categorie"] = filler
|
||
|
||
dict["Categorie"].append(categorie)
|
||
|
||
|
||
# Parse une ligne CSV même si elle contient des virgules et guillemets
|
||
func parse_csv_line(line: String) -> PackedStringArray:
|
||
var result: PackedStringArray = []
|
||
var current: String = ""
|
||
var in_quotes: bool = false
|
||
var index: int = 0
|
||
|
||
while index < line.length():
|
||
var character: String = line[index]
|
||
if character == "\"":
|
||
if in_quotes and index + 1 < line.length() and line[index + 1] == "\"":
|
||
current += "\"" # escaped quote
|
||
index += 1
|
||
else:
|
||
in_quotes = !in_quotes
|
||
elif character == "," and not in_quotes:
|
||
result.append(current)
|
||
current = ""
|
||
else:
|
||
current += character
|
||
index += 1
|
||
|
||
result.append(current)
|
||
return result
|
||
|
||
|
||
# Transforme une ligne pour l'écriture CSV, avec échappement
|
||
func escape_csv_line(fields: PackedStringArray) -> String:
|
||
var output: String = ""
|
||
for index: int in range(fields.size()):
|
||
var field: String = fields[index]
|
||
if field.find("\"") != -1 or field.find(",") != -1 or field.find("\n") != -1:
|
||
field = "\"" + field.replace("\"", "\"\"") + "\""
|
||
output += field
|
||
if index < fields.size() - 1:
|
||
output += ","
|
||
return output
|
||
|
||
|
||
# Normalise les noms de colonnes (ex: writing page -> Writing page)
|
||
func normalize_header(header_name: String) -> String:
|
||
return header_name.strip_edges()[0].to_upper() + header_name.strip_edges().substr(1, -1).to_lower()
|
||
|
||
|
||
# Lit une "ligne logique" complète d’un CSV (même si elle est sur plusieurs lignes à cause des guillemets)
|
||
func read_csv_record(file: FileAccess) -> String:
|
||
var record: String = ""
|
||
var open_quotes: bool = false
|
||
|
||
while not file.eof_reached():
|
||
var line: String = file.get_line()
|
||
if record != "":
|
||
record += "\n"
|
||
record += line
|
||
|
||
var quote_count: int = 0
|
||
for chara: String in line:
|
||
if chara == "\"":
|
||
quote_count += 1
|
||
|
||
open_quotes = (quote_count % 2 != 0)
|
||
|
||
if not open_quotes:
|
||
break
|
||
|
||
return record
|
||
|
||
#endregion
|