File Manager Refresh (#22649)

- Abstracts edit and rename function in the File Manager.
- Files are now edited in the program and not a separate input box.
- File editor now has an editor toolbar with formatting buttons and
keybind functionality.
- File descriptions can now be edited.
- Forms can now be downloaded into a computer's file manager as a text
file.
- File browser now shows a file description.
- Fixed an issue where copying to/from a USB would throw an error
because it didn't read the ID of the device being held.

I was planning to add folder functionality, but that requires far more
work since we store our files directly into the hard drive, which does
file manipulation. Meaning, in order to add folders, I'd need to
completely redo how hard drives and the file structure works. I'll
tackle that later.

CCIA Forms currently generate the entire form, which can be edited
without reservation. This is fine, however a future PR may be viable for
converting forms to use limited edit-access on fields to streamline the
form-filling process. That or just adding the ability for files to
display [field] properly.

Forms tested by Fabian :)

- [x] Fix ID check for copying programs to/from work devices
- [x] Add editor/formatting functionality
- [x] Test Forms functionality (unless an ultra cool dev can for me :) )
- [x] Add description box for editing
- [x] Add actually good formatting to editing
- [x] Misc bugtesting with encrypting, decrypting
- [x] Fix USB files sticking in browser after USB is removed

<details><summary>Video</summary>
<p>


https://github.com/user-attachments/assets/c6c666de-18fd-41a0-9751-3961aeda48fc

</p>
</details>

---------

Signed-off-by: CatsinHD <31459154+CatsinHD@users.noreply.github.com>
This commit is contained in:
CatsinHD
2026-06-19 17:05:40 +00:00
committed by GitHub
parent 0625b841c3
commit 5c7a012e8b
5 changed files with 602 additions and 177 deletions
@@ -1,4 +1,9 @@
#define MAX_TEXTFILE_LENGTH 128000 // 512GQ file
#define FMS_FILEBROWSER 0
#define FMS_SHOWFILE 1
#define FMS_FORMS 2
#define FMS_EDIT 3
/datum/computer_file/program/filemanager
filename = "filemanager"
filedesc = "NTOS File Manager"
@@ -11,6 +16,10 @@
available_on_ntnet = FALSE
undeletable = TRUE
tgui_id = "FileManager"
//Current screen of file manager.
var/screen = FMS_FILEBROWSER
//Department filter for forms browser.
var/sql_filter_dept = ""
var/open_file
var/open_file_is_usb = FALSE
var/error
@@ -21,61 +30,89 @@
var/obj/item/computer_hardware/hard_drive/HDD
var/obj/item/computer_hardware/hard_drive/portable/RHDD
data["error"] = error
data["screen"] = screen
if(!computer || !computer.hard_drive)
data["error"] = "I/O ERROR: Unable to access hard drive."
return data
if(screen == FMS_FILEBROWSER)
data["script_data"] = null
data["file_data"] = null
data["file_is_usb"] = FALSE
data["file_name"] = null
HDD = computer.hard_drive
RHDD = computer.portable_drive
var/list/files = list()
for(var/datum/computer_file/F in HDD.stored_files)
files.Add(list(list(
"name" = F.filename,
"type" = F.filetype,
"desc" = F.filedesc,
"size" = F.size,
"undeletable" = F.undeletable,
"password" = !!F.password
)))
data["files"] = files
if(RHDD)
data["usb_connected"] = TRUE
var/list/usb_files = list()
for(var/datum/computer_file/F in RHDD.stored_files)
usb_files.Add(list(list(
"name" = F.filename,
"type" = F.filetype,
"desc" = F.filedesc,
"size" = F.size,
"undeletable" = F.undeletable,
"password" = !!F.password
)))
data["usb_files"] = usb_files
else
data["usb_connected"] = FALSE
if(screen == FMS_FORMS)
if(!SSdbcore.Connect())
data["sql_error"] = 1
else
var/datum/db_query/query
if(sql_filter_dept)
query = SSdbcore.NewQuery(
"SELECT id, name, department FROM ss13_forms WHERE department LIKE :filter ORDER BY id",
list("filter" = "%[sql_filter_dept]%"))
else
query = SSdbcore.NewQuery("SELECT id, name, department FROM ss13_forms ORDER BY id")
if(!query.Execute())
data["sql_error"] = 1
else
var/list/forms = list()
while(query.NextRow())
forms += list(list("id" = query.item[1], "name" = query.item[2], "department" = query.item[3]))
if(!forms.len)
data["sql_error"] = 1
data["forms"] = forms
qdel(query)
if(open_file)
var/datum/computer_file/data/file
var/datum/computer_file/script/script
if(!computer || !computer.hard_drive)
data["error"] = "I/O ERROR: Unable to access hard drive."
else
HDD = open_file_is_usb ? computer.portable_drive : computer.hard_drive
data["file_is_usb"] = open_file_is_usb
if(!HDD)
HDD = open_file_is_usb ? computer.portable_drive : computer.hard_drive
data["file_is_usb"] = open_file_is_usb
if(!HDD)
data["error"] = "I/O ERROR: Unable to open file."
return data
file = HDD.find_file_by_name(open_file)
script = file
data["file_name"] = "[file.filename]"
data["file_type"] = "[file.filetype]"
data["file_desc"] = "[file.filedesc]"
if(!istype(file))
if(!istype(script))
data["error"] = "I/O ERROR: Unable to open file."
return data
file = HDD.find_file_by_name(open_file)
script = file
if(!istype(file))
if(!istype(script))
data["error"] = "I/O ERROR: Unable to open file."
else
data["scriptdata"] = html_encode(script.code)
data["filename"] = "[script.filename].[script.filetype]"
else
data["filedata"] = pencode2html(file.stored_data)
data["filename"] = "[file.filename].[file.filetype]"
else
if(!computer || !computer.hard_drive)
data["error"] = "I/O ERROR: Unable to access hard drive."
data["script_data"] = html_encode(script.code)
return data
if(screen == FMS_EDIT)
data["file_data"] = file.stored_data
else
data["scriptdata"] = null
data["filedata"] = null
data["file_is_usb"] = FALSE
data["filename"] = null
HDD = computer.hard_drive
RHDD = computer.portable_drive
var/list/files = list()
for(var/datum/computer_file/F in HDD.stored_files)
files.Add(list(list(
"name" = F.filename,
"type" = F.filetype,
"size" = F.size,
"undeletable" = F.undeletable,
"encrypted" = !!F.password
)))
data["files"] = files
if(RHDD)
data["usbconnected"] = TRUE
var/list/usbfiles = list()
for(var/datum/computer_file/F in RHDD.stored_files)
usbfiles.Add(list(list(
"name" = F.filename,
"type" = F.filetype,
"size" = F.size,
"undeletable" = F.undeletable,
"encrypted" = !!F.password
)))
data["usbfiles"] = usbfiles
data["file_data"] = pencode2html(file.stored_data)
return data
/datum/computer_file/program/filemanager/ui_act(action, list/params, datum/tgui/ui, datum/ui_state/state)
@@ -83,68 +120,87 @@
return
switch(action)
if("PRG_openfile")
if("set_screen")
. = TRUE
var/new_screen = text2num(params["screen"])
if(new_screen == FMS_EDIT)
if(!open_file)
return TRUE
var/obj/item/computer_hardware/hard_drive/HDD = computer.hard_drive
if(!HDD)
return TRUE
var/datum/computer_file/data/F = HDD.find_file_by_name(open_file)
if(!F || !istype(F))
return TRUE
if(F.do_not_edit && (alert("WARNING: This file is not compatible with editor. Editing it may result in permanently corrupted formatting or damaged data consistency. Edit anyway?", "Incompatible File", "No", "Yes") == "No"))
return TRUE
screen = new_screen
if("PRG_open_file")
. = TRUE
var/obj/item/computer_hardware/hard_drive/HDD = computer.hard_drive
var/datum/computer_file/F = HDD.find_file_by_name(params["PRG_openfile"])
var/datum/computer_file/F = HDD.find_file_by_name(params["PRG_open_file"])
if(!F)
return
if(F.can_access_file(usr))
open_file = params["PRG_openfile"]
open_file = params["PRG_open_file"]
open_file_is_usb = FALSE
screen = FMS_SHOWFILE
if("PRG_usbopenfile")
if("PRG_usb_open_file")
. = TRUE
var/obj/item/computer_hardware/hard_drive/RHDD = computer.portable_drive
if(!RHDD)
return
var/datum/computer_file/F = RHDD.find_file_by_name(params["PRG_usbopenfile"])
var/datum/computer_file/F = RHDD.find_file_by_name(params["PRG_usb_open_file"])
if(!F)
return
if(F.can_access_file(usr))
open_file = params["PRG_usbopenfile"]
open_file = params["PRG_usb_open_file"]
open_file_is_usb = TRUE
screen = FMS_SHOWFILE
if("PRG_newtextfile")
if("PRG_new_text_file")
. = TRUE
var/newname = sanitize_filename(tgui_input_text(usr, "Enter file name or leave blank to cancel:", "File rename"))
if(!newname)
return TRUE
var/obj/item/computer_hardware/hard_drive/HDD = computer.hard_drive
if(!HDD)
return TRUE
var/datum/computer_file/data/F = new/datum/computer_file/data()
F.filename = newname
F.filename = "NewFile"
F.filetype = "TXT"
F.stored_data = ""
if(!HDD.store_file(F))
qdel(F)
error = "I/O ERROR: Unable to create file. The hard drive may be full, read-only, or contain a conflicting file."
open_file = F.filename
screen = FMS_EDIT
if("PRG_deletefile")
if("PRG_delete_file")
. = TRUE
var/obj/item/computer_hardware/hard_drive/HDD = computer.hard_drive
if(!HDD)
return TRUE
var/datum/computer_file/file = HDD.find_file_by_name(params["PRG_deletefile"])
var/datum/computer_file/file = HDD.find_file_by_name(params["PRG_delete_file"])
if(!file || file.undeletable)
return TRUE
HDD.remove_file(file)
if("PRG_usbdeletefile")
if("PRG_usb_delete_file")
. = TRUE
var/obj/item/computer_hardware/hard_drive/RHDD = computer.portable_drive
if(!RHDD)
return TRUE
var/datum/computer_file/file = RHDD.find_file_by_name(params["PRG_usbdeletefile"])
var/datum/computer_file/file = RHDD.find_file_by_name(params["PRG_usb_delete_file"])
if(!file || file.undeletable)
return TRUE
RHDD.remove_file(file)
if("PRG_closefile")
if("PRG_close_file")
. = TRUE
open_file = null
open_file_is_usb = FALSE
error = null
screen = FMS_FILEBROWSER
if("PRG_clone")
. = TRUE
@@ -162,42 +218,29 @@
qdel(C)
error = "I/O ERROR: Unable to clone file. The hard drive may be full, read-only, or contain a conflicting file."
if("PRG_rename")
. = TRUE
var/obj/item/computer_hardware/hard_drive/HDD = computer.hard_drive
if(!HDD)
return TRUE
var/datum/computer_file/file = HDD.find_file_by_name(params["PRG_rename"])
if(!file || !istype(file))
return TRUE
var/newname = sanitize_filename(tgui_input_text(usr, "Enter new file name or leave blank to cancel:", "File rename", file.filename))
if(file && newname)
file.filename = newname
if("PRG_edit")
. = TRUE
if(!open_file)
return TRUE
var/obj/item/computer_hardware/hard_drive/HDD = computer.hard_drive
if(!HDD)
return TRUE
var/datum/computer_file/data/F = HDD.find_file_by_name(open_file)
if(!F || !istype(F))
return TRUE
if(F.do_not_edit && (alert("WARNING: This file is not compatible with editor. Editing it may result in permanently corrupted formatting or damaged data consistency. Edit anyway?", "Incompatible File", "No", "Yes") == "No"))
return TRUE
var/oldtext = html_decode(F.stored_data)
oldtext = replacetext(oldtext, "\[editorbr\]", "\n")
var/newtext = sanitize_filename(replacetext(tgui_input_text(usr, "Editing file [open_file]. You may use most tags used in paper formatting:", "Text Editor", oldtext), "\n", "\[editorbr\]"), MAX_TEXTFILE_LENGTH)
if(!newtext)
if(params["PRG_rename"])
var/newname = sanitize_filename(params["PRG_rename"])
F.filename = newname
open_file = newname
return
if(F)
if(params["PRG_desc"])
F.filedesc = params["PRG_desc"]
return
if(params["PRG_edit"])
var/datum/computer_file/data/backup = F.clone()
HDD.remove_file(F)
F.stored_data = newtext
F.stored_data = params["PRG_edit"]
F.calculate_size()
// We can't store the updated file, it's probably too large. Print an error and restore backed up version.
// This is mostly intended to prevent people from losing texts they spent lot of time working on due to running out of space.
@@ -206,7 +249,7 @@
error = "I/O error: Unable to overwrite file. Hard drive is probably full. You may want to backup your changes before closing this window:<br><br>[html_decode(F.stored_data)]<br><br>"
HDD.store_file(backup)
if("PRG_printfile")
if("PRG_print_file")
. = TRUE
if(!open_file)
return TRUE
@@ -221,29 +264,26 @@
if(!F)
return TRUE
if(istype(F))
if(!computer.nano_printer.print_text(F.stored_data))
if(!computer.nano_printer.print_text(F.stored_data, F.filename))
error = "Hardware error: Printer was unable to print the file. It may be out of paper."
return FALSE
else if(istype(S))
if(!computer.nano_printer.print_text(S.code))
if(!computer.nano_printer.print_text(S.code, S.filename))
error = "Hardware error: Printer was unable to print the file. It may be out of paper."
return FALSE
if("PRG_copytousb")
if("PRG_copy_to_usb")
. = TRUE
var/obj/item/computer_hardware/hard_drive/HDD = computer.hard_drive
var/obj/item/computer_hardware/hard_drive/portable/RHDD = computer.portable_drive
if(!HDD || !RHDD)
return FALSE
var/datum/computer_file/F = HDD.find_file_by_name(params["PRG_copytousb"])
var/datum/computer_file/F = HDD.find_file_by_name(params["PRG_copy_to_usb"])
if(!F || !istype(F))
return FALSE
var/is_usr_tech_support = FALSE
if(ishuman(usr))
var/mob/living/carbon/human/H = usr
var/obj/item/card/id/ID = H.GetIdCard()
if(ACCESS_IT in ID.access)
is_usr_tech_support = TRUE
if(can_run(usr,FALSE,ACCESS_IT))
is_usr_tech_support = TRUE
if(!is_usr_tech_support && computer.enrolled != DEVICE_PRIVATE && istype(F, /datum/computer_file/program))
to_chat(usr, SPAN_WARNING("Work devices can't export programs to portable drives! Contact Tech Support to get them to load it."))
return TRUE
@@ -258,21 +298,18 @@
return
RHDD.store_file(C)
if("PRG_copyfromusb")
if("PRG_copy_from_usb")
. = TRUE
var/obj/item/computer_hardware/hard_drive/HDD = computer.hard_drive
var/obj/item/computer_hardware/hard_drive/portable/RHDD = computer.portable_drive
if(!HDD || !RHDD)
return TRUE
var/datum/computer_file/F = RHDD.find_file_by_name(params["PRG_copyfromusb"])
var/datum/computer_file/F = RHDD.find_file_by_name(params["PRG_copy_from_usb"])
if(!F || !istype(F))
return TRUE
var/is_usr_tech_support = FALSE
if(ishuman(usr))
var/mob/living/carbon/human/H = usr
var/obj/item/card/id/ID = H.GetIdCard()
if(ACCESS_IT in ID.access)
is_usr_tech_support = TRUE
if(can_run(usr,FALSE,ACCESS_IT))
is_usr_tech_support = TRUE
if(!is_usr_tech_support && computer.enrolled != DEVICE_PRIVATE && istype(F, /datum/computer_file/program))
to_chat(usr, SPAN_WARNING("Work devices can't import programs from portable drives! Contact Tech Support to get them to load it."))
return TRUE
@@ -292,22 +329,72 @@
var/obj/item/computer_hardware/hard_drive/HDD = computer.hard_drive
if (!HDD)
return
var/datum/computer_file/F = HDD.find_file_by_name(params["PRG_encrypt"])
var/datum/computer_file/F = HDD.find_file_by_name(params["PRG_file_to_encrypt"])
if(!F || F.undeletable)
return
if(F.password)
return
F.password = sanitize_filename(tgui_input_text(usr, "Enter an encryption key:", "Encrypt File"))
if("PRG_decrypt")
. = TRUE
var/obj/item/computer_hardware/hard_drive/HDD = computer.hard_drive
if (!HDD)
return
var/datum/computer_file/F = HDD.find_file_by_name(params["PRG_encrypt"])
if(!F || F.undeletable)
return
if (F.can_access_file(usr))
if(F.password && F.password == params["PRG_encrypt"])
F.password = ""
return
F.password = params["PRG_encrypt"]
if("PRG_sort_forms")
sql_filter_dept = sanitize(params["department"])
return TRUE
if("PRG_reset_sql")
sql_filter_dept = ""
return TRUE
if("PRG_generate_form")
var/obj/item/computer_hardware/hard_drive/HDD = computer.hard_drive
var/printid = text2num(params["id"])
if(!printid)
return TRUE
if(!SSdbcore.Connect())
to_chat(usr, SPAN_WARNING("Connection to the database lost. Aborting."))
return TRUE
var/datum/db_query/query = SSdbcore.NewQuery(
"SELECT id, name, data FROM ss13_forms WHERE id = :id",
list("id" = printid))
if(!query.Execute())
to_chat(usr, SPAN_WARNING("Connection to the database lost. Aborting."))
qdel(query)
return TRUE
while(query.NextRow())
var/datum/computer_file/data/F = new/datum/computer_file/data()
F.filename = "SCCF-[query.item[1]]-[query.item[2]]"
F.filetype = "TXT"
F.stored_data = query.item[3]
if(!HDD.store_file(F))
qdel(F)
error = "I/O ERROR: Unable to create file. The hard drive may be full, read-only, or contain a conflicting file."
qdel(query)
screen = FMS_FILEBROWSER
return TRUE
if("PRG_whatis")
var/whatisid = text2num(params["id"])
if(!whatisid)
return TRUE
if(!SSdbcore.Connect())
to_chat(usr, SPAN_WARNING("Connection to the database lost. Aborting."))
return TRUE
var/datum/db_query/query = SSdbcore.NewQuery(
"SELECT id, name, department, info FROM ss13_forms WHERE id = :id",
list("id" = whatisid))
if(!query.Execute())
to_chat(usr, SPAN_WARNING("Connection to the database lost. Aborting."))
qdel(query)
return TRUE
var/dat = "<center><b>Stellar Corporate Conglomerate Form</b><br>"
while(query.NextRow())
dat += "<b>SCCF-[query.item[1]]</b><br><br>"
dat += "<b>[query.item[2]]</b><br>"
dat += "<b>[query.item[3]] Department</b><hr>"
dat += "[query.item[4]]"
dat += "</center>"
qdel(query)
usr << browse(HTML_SKELETON(dat), "window=Information;size=560x240")
return TRUE
#undef MAX_TEXTFILE_LENGTH