Library Update (#22120)

Updates the Library to TGUI and the new DBCore

The GUI was created using Claude Code

Public Library Computer
<img width="652" height="501" alt="image"
src="https://github.com/user-attachments/assets/eba1841a-5784-4fe4-b337-6a9e9503bb58"
/>


Library Management Computer:
Inventory Page:
<img width="750" height="605" alt="image"
src="https://github.com/user-attachments/assets/e4f38fcb-7299-4baf-a8a2-0974297a544c"
/>

Check Out Page with Book Picker active:
<img width="750" height="607" alt="image"
src="https://github.com/user-attachments/assets/e9189157-1617-4535-a053-942b17378614"
/>

Library Order Page with pagination:
<img width="747" height="598" alt="image"
src="https://github.com/user-attachments/assets/07d3abaf-5a16-4229-8ab8-e64e2f73de24"
/>

Emag action:
<img width="752" height="602" alt="image"
src="https://github.com/user-attachments/assets/285cd4bf-0ed3-4cf2-a9cb-4469e6466ad0"
/>

Book Uploader:
<img width="747" height="602" alt="image"
src="https://github.com/user-attachments/assets/b4b648ab-59e4-48f1-90a3-77e8440f01e4"
/>

And its orderable instantly:
<img width="750" height="602" alt="image"
src="https://github.com/user-attachments/assets/1c1c21fa-28fd-42fe-82b5-1c859cd74fdf"
/>

---------

Co-authored-by: Werner <Arrow768@users.noreply.github.com>
This commit is contained in:
Arrow768
2026-04-11 12:21:13 +00:00
committed by GitHub
co-authored by Werner
parent cfdcac3453
commit 0ccd900aac
11 changed files with 1733 additions and 556 deletions
+4 -1
View File
@@ -2551,8 +2551,11 @@
#include "code\modules\law\laws\high_severity.dm"
#include "code\modules\law\laws\low_severity.dm"
#include "code\modules\law\laws\med_severity.dm"
#include "code\modules\library\lib_binder.dm"
#include "code\modules\library\lib_computer.dm"
#include "code\modules\library\lib_items.dm"
#include "code\modules\library\lib_machines.dm"
#include "code\modules\library\lib_public_computer.dm"
#include "code\modules\library\lib_scanner.dm"
#include "code\modules\lighting\emissive_blocker.dm"
#include "code\modules\lighting\lighting_area.dm"
#include "code\modules\lighting\lighting_atom.dm"
+58
View File
@@ -0,0 +1,58 @@
/*
* Book Binder
*/
/obj/machinery/bookbinder
name = "book binder"
desc = "A machine that takes paper and binds them into books. Fascinating!"
icon = 'icons/obj/library.dmi'
icon_state = "binder"
anchored = TRUE
density = TRUE
var/binding = FALSE
/obj/machinery/bookbinder/attackby(obj/item/attacking_item, mob/user)
if(istype(attacking_item, /obj/item/paper))
var/obj/item/paper/paper = attacking_item
if(!anchored)
to_chat(user, SPAN_WARNING("\The [src] must be secured to the floor first!"))
return
if(binding)
to_chat(user, SPAN_WARNING("You must wait for \the [src] to finish its current operation!"))
return
var/turf/work_turf = get_turf(src)
user.drop_from_inventory(paper, src)
user.visible_message(
SPAN_NOTICE("\The [user] loads some paper into \the [src]."),
SPAN_NOTICE("You load some paper into \the [src]."))
visible_message(SPAN_NOTICE("\The [src] begins to hum as it warms up its printing drums."))
playsound(work_turf, 'sound/items/bureaucracy/binder.ogg', 75, 1)
binding = TRUE
sleep(rand(20 SECONDS, 40 SECONDS))
binding = FALSE
if(!anchored)
visible_message(SPAN_WARNING("\The [src] buzzes and flashes an error light."))
paper.forceMove(work_turf)
return
visible_message(SPAN_NOTICE("\The [src] whirs as it prints and binds a new book."))
playsound(work_turf, 'sound/items/bureaucracy/print.ogg', 75, 1)
var/obj/item/book/bound_book = new(work_turf)
bound_book.dat = paper.info
bound_book.name = "blank book"
bound_book.icon_state = "book[rand(1,7)]"
bound_book.item_state = icon_state
qdel(paper)
return
if(attacking_item.tool_behaviour == TOOL_WRENCH)
attacking_item.play_tool_sound(get_turf(src), 75)
if(anchored)
user.visible_message(
SPAN_NOTICE("\The [user] unsecures \the [src] from the floor."),
SPAN_NOTICE("You unsecure \the [src] from the floor."),
SPAN_WARNING("You hear a ratcheting noise."))
else
user.visible_message(
SPAN_NOTICE("\The [user] secures \the [src] to the floor."),
SPAN_NOTICE("You secure \the [src] to the floor."),
SPAN_WARNING("You hear a ratcheting noise."))
anchored = !anchored
+425
View File
@@ -0,0 +1,425 @@
/*
* Borrowbook datum
*/
/// Tracks a single book checkout: what was borrowed, by whom, and when it is due.
/datum/borrowbook
var/book_name
var/mob_name
var/get_date
var/due_date
/*
* Library Computer
*/
/obj/machinery/librarycomp
name = "library computer"
desc = "A computer."
icon = 'icons/obj/library.dmi'
icon_state = "computer"
anchored = TRUE
density = TRUE
var/upload_category = "Fiction"
/// Active book checkouts
var/list/datum/borrowbook/checkouts = list()
/// Weakrefs to physical books currently in the library's physical inventory
var/list/datum/weakref/inventory = list()
/// How long a checkout lasts, in minutes
var/checkout_period_minutes = 5
var/bible_on_cooldown = FALSE
var/is_public = FALSE
var/buffer_book // Set by barcode scanner mode 1
/// Title of the last successfully uploaded book, shown in the UI as confirmation
var/last_uploaded_title
var/datum/weakref/scanner_ref // Weakref to the nearest anchored book scanner
/// Current page of archive results (one page = 20 entries)
var/list/archive_results = list()
var/archive_loading = FALSE
var/archive_error = FALSE
var/archive_page = 1
/// Total number of archive entries matching the current search
var/archive_total = 0
var/archive_search = ""
var/archive_sort_field = "title"
var/archive_sort_dir = "asc"
/obj/machinery/librarycomp/attack_hand(var/mob/user)
. = ..()
if(.)
return TRUE
ui_interact(user)
/obj/machinery/librarycomp/ui_interact(mob/user, datum/tgui/ui)
ui = SStgui.try_update_ui(user, src, ui)
if(!ui)
var/title = is_public ? "[SSatlas.current_map.station_name] Library" : "[SSatlas.current_map.station_name] Library Management"
ui = new(user, src, "LibraryComputer", title, 750, 600)
ui.open()
/obj/machinery/librarycomp/ui_data(mob/user)
var/obj/machinery/libraryscanner/scanner = scanner_ref?.resolve()
if(!scanner)
scanner_ref = null
for(var/obj/machinery/libraryscanner/nearby_scanner in range(9))
if(nearby_scanner.anchored)
scanner_ref = WEAKREF(nearby_scanner)
scanner = nearby_scanner
break
var/list/data = list()
data["is_public"] = is_public
data["is_emagged"] = emagged
data["bible_on_cooldown"] = bible_on_cooldown
data["checkout_period_minutes"] = checkout_period_minutes
data["archive_loading"] = archive_loading
data["archive_error"] = archive_error
data["archive_results"] = archive_results
data["archive_page"] = archive_page
data["archive_total"] = archive_total
data["archive_search"] = archive_search
data["archive_sort_field"] = archive_sort_field
data["archive_sort_dir"] = archive_sort_dir
data["archive_page_size"] = 20
data["upload_category"] = upload_category
data["buffer_book"] = buffer_book
data["last_uploaded_title"] = last_uploaded_title
var/list/inv = list()
var/list/stale = list()
for(var/datum/weakref/wref in inventory)
var/obj/item/book/book = wref.resolve()
if(book)
inv += list(list("ref" = REF(book), "name" = book.name))
else
stale += wref
inventory -= stale
data["inventory"] = inv
var/list/co = list()
for(var/datum/borrowbook/checkout in checkouts)
var/taken_minutes = round((world.time - checkout.get_date) / 1 MINUTES)
var/due_raw = (checkout.due_date - world.time) / 1 MINUTES
co += list(list(
"ref" = REF(checkout),
"book_name" = checkout.book_name,
"mob_name" = checkout.mob_name,
"taken_minutes" = taken_minutes,
"due_minutes" = abs(round(due_raw)),
"overdue" = (due_raw <= 0)
))
data["checkouts"] = co
var/list/scanner_data = list()
scanner_data["found"] = !!(scanner?.anchored)
scanner_data["title"] = scanner?.cache ? scanner.cache.name : null
scanner_data["author"] = scanner?.cache ? (scanner.cache.author ? scanner.cache.author : "Anonymous") : null
data["scanner"] = scanner_data
var/list/crew_names = list()
var/list/manifest = SSrecords.get_manifest_list()
for(var/dept in manifest)
for(var/list/entry in manifest[dept])
if(!entry["ooc_role"] && entry["name"])
crew_names |= entry["name"]
data["crew_names"] = crew_names
return data
/obj/machinery/librarycomp/emag_act(var/remaining_charges, var/mob/user)
if(src.density && !src.emagged)
src.emagged = TRUE
return 1
/obj/machinery/librarycomp/attackby(obj/item/attacking_item, mob/user)
if(istype(attacking_item, /obj/item/barcodescanner))
var/obj/item/barcodescanner/barcode_scanner = attacking_item
barcode_scanner.computer_ref = REF(src)
to_chat(user, "[barcode_scanner]'s associated machine has been set to [src].")
for(var/mob/hearer in hearers(src))
hearer.show_message("[src] lets out a low, short blip.", 2)
else
..()
/obj/machinery/librarycomp/ui_act(action, list/params, datum/tgui/ui, datum/ui_state/state)
. = ..()
if(.)
return
add_fingerprint(usr)
switch(action)
if("print_bible")
if(!bible_on_cooldown)
var/obj/item/storage/bible/bible = new /obj/item/storage/bible(src.loc)
bible.verbs += /obj/item/storage/bible/verb/Set_Religion
var/rand_book = "book" + pick("1","2","3","4","5","6","7","8","9","10","11","12","13","14","15","16")
bible.icon_state = rand_book
bible.item_state = rand_book
bible.name = "religious book"
bible_on_cooldown = TRUE
addtimer(CALLBACK(src, PROC_REF(end_bible_cooldown)), 6 SECONDS, TIMER_UNIQUE|TIMER_STOPPABLE)
. = TRUE
if("arcane_confirm")
if(emagged && !bible_on_cooldown)
new /obj/item/book/tome(get_turf(src))
to_chat(usr, SPAN_WARNING("Your sanity barely endures the seconds spent in the vault's browsing window. The only thing to remind you of this when you stop browsing is a dusty old tome sitting on the desk. You don't really remember printing it."))
usr.visible_message(
SPAN_NOTICE("\The [usr] stares at the blank screen for a few moments, [usr.get_pronoun("his")] expression frozen in fear. When [usr.get_pronoun("he")] finally awakens from it, [usr.get_pronoun("he")] looks a lot older."),
range = 2)
bible_on_cooldown = TRUE
addtimer(CALLBACK(src, PROC_REF(end_bible_cooldown)), 6 SECONDS, TIMER_UNIQUE|TIMER_STOPPABLE)
. = TRUE
if("increase_checkout_period")
checkout_period_minutes += 1
. = TRUE
if("decrease_checkout_period")
checkout_period_minutes = max(1, checkout_period_minutes - 1)
. = TRUE
if("checkout_book")
var/book_title = sanitizeSafe(params["book_title"])
var/recipient = sanitize(params["recipient"], MAX_NAME_LEN)
if(book_title && recipient)
var/datum/borrowbook/new_checkout = new /datum/borrowbook
new_checkout.book_name = book_title
new_checkout.mob_name = recipient
new_checkout.get_date = world.time
new_checkout.due_date = world.time + (checkout_period_minutes * 1 MINUTES)
checkouts.Add(new_checkout)
. = TRUE
if("checkin_book")
var/datum/borrowbook/checkout = locate(params["ref"])
if(checkout && (checkout in checkouts))
checkouts.Remove(checkout)
. = TRUE
if("delete_inventory_book")
var/obj/item/book/book = locate(params["ref"])
if(book && !QDELETED(book))
inventory.Remove(WEAKREF(book))
. = TRUE
if("set_upload_category")
var/new_category = params["value"]
if(new_category in list("Fiction", "Non-Fiction", "Reference", "Religion"))
upload_category = new_category
. = TRUE
if("set_upload_author")
var/obj/machinery/libraryscanner/sc = get_scanner()
if(sc?.cache)
sc.cache.author = sanitize(params["value"])
. = TRUE
if("clear_scanner_cache")
var/obj/machinery/libraryscanner/sc = get_scanner()
if(sc)
sc.cache = null
SStgui.update_uis(sc)
last_uploaded_title = null
. = TRUE
if("upload_book")
var/obj/machinery/libraryscanner/sc = get_scanner()
if(!is_public && sc?.anchored && sc.cache)
if(sc.cache.unique)
to_chat(usr, SPAN_WARNING("This book has been rejected from the database."))
else
INVOKE_ASYNC(src, PROC_REF(async_upload_book), usr)
. = TRUE
if("fetch_archive")
if(!archive_loading)
archive_page = 1
archive_search = ""
INVOKE_ASYNC(src, PROC_REF(async_fetch_archive))
. = TRUE
if("archive_go_to_page")
var/new_page = text2num(params["page"])
if(isnum(new_page) && new_page >= 1 && !archive_loading)
archive_page = round(new_page)
INVOKE_ASYNC(src, PROC_REF(async_fetch_archive))
. = TRUE
if("archive_set_search")
if(!archive_loading)
archive_search = sanitize(params["query"])
archive_page = 1
INVOKE_ASYNC(src, PROC_REF(async_fetch_archive))
. = TRUE
if("archive_set_sort")
var/field = params["field"]
if(field in list("title", "author", "category"))
var/direction = params["dir"]
archive_sort_field = field
archive_sort_dir = (direction == "desc") ? "desc" : "asc"
archive_page = 1
if(!archive_loading)
INVOKE_ASYNC(src, PROC_REF(async_fetch_archive))
. = TRUE
if("order_book")
var/book_id = text2num(params["id"])
if(isnum(book_id) && book_id > 0)
if(!bible_on_cooldown)
INVOKE_ASYNC(src, PROC_REF(async_order_book), book_id)
else
visible_message("\The [src]'s monitor flashes: \"Printer unavailable. Please allow a short time before attempting to print.\"")
. = TRUE
if("extend_checkout")
var/datum/borrowbook/checkout = locate(params["ref"])
if(checkout && (checkout in checkouts))
checkout.due_date += checkout_period_minutes * 1 MINUTES
. = TRUE
if("print_inventory")
var/dat = "<B>Library Inventory</B><BR><BR>"
var/printed = 0
for(var/datum/weakref/wref in inventory)
var/obj/item/book/book = wref.resolve()
if(book)
dat += "- [book.name]<BR>"
printed++
if(!printed)
dat += "No books in inventory.<BR>"
var/obj/item/paper/printout = new /obj/item/paper(src.loc)
printout.info = dat
printout.name = "paper- 'Library Inventory'"
. = TRUE
if("print_checkouts")
var/dat = "<B>Active Checkouts</B><BR><BR>"
if(checkouts.len)
for(var/datum/borrowbook/checkout in checkouts)
var/taken_minutes = round((world.time - checkout.get_date) / 1 MINUTES)
var/due_raw = (checkout.due_date - world.time) / 1 MINUTES
var/status = due_raw <= 0 ? "OVERDUE by [abs(round(due_raw))] min" : "due in [round(due_raw)] min"
dat += "- <B>[checkout.book_name]</B> ([checkout.mob_name]) — taken [taken_minutes] min ago, [status]<BR>"
else
dat += "No books currently checked out.<BR>"
var/obj/item/paper/printout = new /obj/item/paper(src.loc)
printout.info = dat
printout.name = "paper- 'Active Checkouts'"
. = TRUE
/obj/machinery/librarycomp/proc/end_bible_cooldown()
bible_on_cooldown = FALSE
SStgui.update_uis(src)
/obj/machinery/librarycomp/proc/get_scanner()
var/obj/machinery/libraryscanner/scanner = scanner_ref?.resolve()
if(!scanner)
scanner_ref = null
for(var/obj/machinery/libraryscanner/nearby_scanner in range(9))
if(nearby_scanner.anchored)
scanner_ref = WEAKREF(nearby_scanner)
return nearby_scanner
return null
return scanner
/obj/machinery/librarycomp/proc/async_fetch_archive()
archive_loading = TRUE
archive_error = FALSE
archive_total = 0
archive_results = list()
SStgui.update_uis(src)
// Build WHERE clause — keep % wildcards out of params via CONCAT so
// the param value is the raw search term without special characters
var/sql_where = ""
var/list/count_args
var/list/select_args
if(length(archive_search))
sql_where = "WHERE (title LIKE CONCAT('%', :search, '%') OR author LIKE CONCAT('%', :search, '%') OR category LIKE CONCAT('%', :search, '%'))"
count_args = list("search" = archive_search)
select_args = list("search" = archive_search)
else
count_args = list()
select_args = list()
// Validate sort field and direction before interpolating into SQL
var/safe_field
switch(archive_sort_field)
if("author") safe_field = "author"
if("category") safe_field = "category"
else safe_field = "title"
var/safe_dir = (archive_sort_dir == "desc") ? "DESC" : "ASC"
// COUNT query to get total matching entries
var/datum/db_query/count_query = SSdbcore.NewQuery(
"SELECT COUNT(*) FROM ss13_library [sql_where]",
count_args)
if(!count_query.Execute())
archive_error = TRUE
qdel(count_query)
archive_loading = FALSE
SStgui.update_uis(src)
return
archive_total = count_query.NextRow() ? text2num(count_query.item[1]) : 0
qdel(count_query)
// Clamp page to valid range now that we know the total
var/total_pages = max(1, CEILING(archive_total, 20) / 20)
archive_page = clamp(archive_page, 1, total_pages)
var/sql_offset = (archive_page - 1) * 20
// Paginated SELECT
var/datum/db_query/query = SSdbcore.NewQuery(
"SELECT id, author, title, category FROM ss13_library [sql_where] ORDER BY [safe_field] [safe_dir] LIMIT 20 OFFSET [sql_offset]",
select_args)
if(!query.Execute())
archive_error = TRUE
qdel(query)
archive_loading = FALSE
SStgui.update_uis(src)
return
var/list/results = list()
while(query.NextRow())
results += list(list(
"id" = text2num(query.item[1]),
"author" = query.item[2],
"title" = query.item[3],
"category" = query.item[4]
))
qdel(query)
archive_results = results
archive_loading = FALSE
SStgui.update_uis(src)
/obj/machinery/librarycomp/proc/async_upload_book(mob/uploader)
var/obj/machinery/libraryscanner/scanner = get_scanner()
var/obj/item/book/book = scanner?.cache
if(!book)
return
var/datum/db_query/query = SSdbcore.NewQuery(
"INSERT INTO ss13_library (author, title, content, category, uploadtime, uploader) VALUES (:author, :title, :content, :category, NOW(), :uploader)",
list(
"author" = book.author ? book.author : "Anonymous",
"title" = book.name,
"content" = book.dat,
"category" = upload_category,
"uploader" = ckey(uploader.client.ckey)
))
if(!query.Execute())
to_chat(uploader, SPAN_WARNING("Upload failed: [query.last_error]"))
qdel(query)
return
qdel(query)
log_and_message_admins("has uploaded the book titled [book.name], [length(book.dat)] signs")
log_game("[uploader.name]/[uploader.key] has uploaded the book titled [book.name], [length(book.dat)] signs")
to_chat(uploader, SPAN_NOTICE("Upload complete."))
last_uploaded_title = book.name
SStgui.update_uis(src)
/obj/machinery/librarycomp/proc/async_order_book(var/book_id)
bible_on_cooldown = TRUE
addtimer(CALLBACK(src, PROC_REF(end_bible_cooldown)), 6 SECONDS, TIMER_UNIQUE|TIMER_STOPPABLE)
SStgui.update_uis(src)
var/datum/db_query/query = SSdbcore.NewQuery(
"SELECT id, author, title, content FROM ss13_library WHERE id = :id",
list("id" = book_id))
if(!query.Execute() || !query.NextRow())
qdel(query)
return
var/obj/item/book/ordered_book = new(src.loc)
ordered_book.author = query.item[2]
ordered_book.title = query.item[3]
ordered_book.name = "Book: [query.item[3]]"
ordered_book.dat = query.item[4]
ordered_book.icon_state = "book[rand(1,16)]"
ordered_book.item_state = ordered_book.icon_state
qdel(query)
src.visible_message("\The [src]'s printer hums as it produces a book.")
// Public Related Code
/obj/machinery/librarycomp/public
name = "public library computer"
is_public = TRUE
+54 -38
View File
@@ -183,6 +183,14 @@
update_icon()
/*
* Barcode Scanner mode defines — used in book/attackby and barcodescanner/attack_self
*/
#define BARCODE_SCAN_BUFFER_ONLY 0
#define BARCODE_SCAN_BUFFER_COMPUTER 1
#define BARCODE_SCAN_CHECKIN 2
#define BARCODE_SCAN_ADD_INVENTORY 3
/*
* Book
*/
@@ -273,35 +281,37 @@
return
else if(istype(attacking_item, /obj/item/barcodescanner))
var/obj/item/barcodescanner/scanner = attacking_item
if(!scanner.computer)
var/obj/machinery/librarycomp/comp = locate(scanner.computer_ref)
if(!comp || QDELETED(comp))
to_chat(user, "[attacking_item]'s screen flashes: 'No associated computer found!'")
else
switch(scanner.mode)
if(0)
scanner.book = src
if(BARCODE_SCAN_BUFFER_ONLY)
scanner.book_ref = REF(src)
to_chat(user, "[attacking_item]'s screen flashes: 'Book stored in buffer.'")
if(1)
scanner.book = src
scanner.computer.buffer_book = src.name
if(BARCODE_SCAN_BUFFER_COMPUTER)
scanner.book_ref = REF(src)
comp.buffer_book = src.name
to_chat(user, "[attacking_item]'s screen flashes: 'Book stored in buffer. Book title stored in associated computer buffer.'")
if(2)
scanner.book = src
for(var/datum/borrowbook/b in scanner.computer.checkouts)
if(b.bookname == src.name)
scanner.computer.checkouts.Remove(b)
if(BARCODE_SCAN_CHECKIN)
scanner.book_ref = REF(src)
for(var/datum/borrowbook/checkout in comp.checkouts)
if(checkout.book_name == src.name)
comp.checkouts.Remove(checkout)
to_chat(user, "[attacking_item]'s screen flashes: 'Book stored in buffer. Book has been checked in.'")
return
to_chat(user, "[attacking_item]'s screen flashes: 'Book stored in buffer. No active check-out record found for current title.'")
if(3)
scanner.book = src
for(var/obj/item/book in scanner.computer.inventory)
if(book == src)
to_chat(user, "[attacking_item]'s screen flashes: 'Book stored in buffer. Title already present in inventory, aborting to avoid duplicate entry.'")
return
scanner.computer.inventory.Add(src)
if(BARCODE_SCAN_ADD_INVENTORY)
scanner.book_ref = REF(src)
var/datum/weakref/src_wref = WEAKREF(src)
if(src_wref in comp.inventory)
to_chat(user, "[attacking_item]'s screen flashes: 'Book stored in buffer. Title already present in inventory, aborting to avoid duplicate entry.'")
return
comp.inventory.Add(src_wref)
to_chat(user, "[attacking_item]'s screen flashes: 'Book stored in buffer. Title added to general inventory.'")
else if(istype(attacking_item, /obj/item/material/knife) || attacking_item.tool_behaviour == TOOL_WIRECUTTER)
if(carved) return
if(carved)
return
to_chat(user, SPAN_NOTICE("You begin to carve out [title]."))
if(attacking_item.use_tool(src, user, 30, volume = 50))
to_chat(user, SPAN_NOTICE("You carve out the pages from [title]! You didn't want to read it anyway."))
@@ -326,34 +336,40 @@
/obj/item/barcodescanner
name = "book scanner"
icon = 'icons/obj/library.dmi'
icon_state ="scanner"
icon_state = "scanner"
throw_speed = 1
throw_range = 5
w_class = WEIGHT_CLASS_SMALL
var/obj/machinery/librarycomp/computer // Associated computer - Modes 1 to 3 use this
var/obj/item/book/book // Currently scanned book
var/mode = 0 // 0 - Scan only, 1 - Scan and Set Buffer, 2 - Scan and Attempt to Check In, 3 - Scan and Attempt to Add to Inventory
var/computer_ref // REF string of the associated library computer
var/book_ref // REF string of the currently scanned book
var/mode = BARCODE_SCAN_BUFFER_ONLY
/obj/item/barcodescanner/attack_self(mob/user as mob)
/obj/item/barcodescanner/attack_self(mob/user)
mode += 1
if(mode > 3)
mode = 0
if(mode > BARCODE_SCAN_ADD_INVENTORY)
mode = BARCODE_SCAN_BUFFER_ONLY
to_chat(user, "[src] Status Display:")
var/modedesc
var/mode_desc
switch(mode)
if(0)
modedesc = "Scan book to local buffer."
if(1)
modedesc = "Scan book to local buffer and set associated computer buffer to match."
if(2)
modedesc = "Scan book to local buffer, attempt to check in scanned book."
if(3)
modedesc = "Scan book to local buffer, attempt to add book to general inventory."
if(BARCODE_SCAN_BUFFER_ONLY)
mode_desc = "Scan book to local buffer."
if(BARCODE_SCAN_BUFFER_COMPUTER)
mode_desc = "Scan book to local buffer and set associated computer buffer to match."
if(BARCODE_SCAN_CHECKIN)
mode_desc = "Scan book to local buffer, attempt to check in scanned book."
if(BARCODE_SCAN_ADD_INVENTORY)
mode_desc = "Scan book to local buffer, attempt to add book to general inventory."
else
modedesc = "ERROR"
to_chat(user, " - Mode [mode] : [modedesc]")
if(src.computer)
mode_desc = "ERROR"
to_chat(user, " - Mode [mode] : [mode_desc]")
var/obj/machinery/librarycomp/comp = locate(src.computer_ref)
if(comp && !QDELETED(comp))
to_chat(user, SPAN_NOTICE("Computer has been associated with this unit."))
else
to_chat(user, SPAN_WARNING("No associated computer found. Only local scans will function properly."))
to_chat(user, "\n")
#undef BARCODE_SCAN_BUFFER_ONLY
#undef BARCODE_SCAN_BUFFER_COMPUTER
#undef BARCODE_SCAN_CHECKIN
#undef BARCODE_SCAN_ADD_INVENTORY
-517
View File
@@ -1,517 +0,0 @@
/* Library Machines
*
* Contains:
* * Borrowbook datum
* * Library Public Computer
* * Library Computer
* * Library Scanner
* * Book Binder
*/
/*
* Borrowbook datum
*/
/datum/borrowbook // Datum used to keep track of who has borrowed what when and for how long.
var/bookname
var/mobname
var/getdate
var/duedate
/*
* Library Public Computer
*/
/obj/machinery/librarypubliccomp
name = "public library computer"
desc = "A computer."
icon = 'icons/obj/library.dmi'
icon_state = "computer"
anchored = TRUE
density = TRUE
var/screenstate = 0
var/title
var/category = "Any"
var/author
var/SQLquery
/obj/machinery/librarypubliccomp/attack_hand(var/mob/user)
usr.set_machine(src)
var/dat = ""
switch(screenstate)
if(0)
dat += {"<h2>Search Settings</h2><br>
<a href='byond://?src=[REF(src)];settitle=1'>Filter by Title: [title]</a><br>
<a href='byond://?src=[REF(src)];setcategory=1'>Filter by Category: [category]</a><br>
<a href='byond://?src=[REF(src)];setauthor=1'>Filter by Author: [author]</a><br>
<a href='byond://?src=[REF(src)];search=1'>\[Start Search\]</a><br>"}
if(1)
if(!establish_db_connection(GLOB.dbcon))
dat += "<font color=red><b>ERROR</b>: Unable to contact External Archive. Please contact your system administrator for assistance.</font><br>"
else if(!SQLquery)
dat += "<font color=red><b>ERROR</b>: Malformed search request. Please contact your system administrator for assistance.</font><br>"
else
dat += {"<table>
<tr><td>AUTHOR</td><td>TITLE</td><td>CATEGORY</td><td>SS<sup>13</sup>BN</td></tr>"}
var/DBQuery/query = GLOB.dbcon.NewQuery(SQLquery)
query.Execute()
while(query.NextRow())
var/author = query.item[1]
var/title = query.item[2]
var/category = query.item[3]
var/id = query.item[4]
dat += "<tr><td>[author]</td><td>[title]</td><td>[category]</td><td>[id]</td></tr>"
dat += "</table><br>"
dat += "<a href='byond://?src=[REF(src)];back=1'>\[Go Back\]</a><br>"
user << browse(HTML_SKELETON_TITLE("Library Visitor", dat), "window=publiclibrary")
onclose(user, "publiclibrary")
/obj/machinery/librarypubliccomp/Topic(href, href_list)
if(..())
usr << browse(null, "window=publiclibrary")
onclose(usr, "publiclibrary")
return
if(href_list["settitle"])
var/newtitle = input("Enter a title to search for:") as text|null
if(newtitle)
title = sanitize(newtitle)
else
title = null
title = sanitizeSQL(title)
if(href_list["setcategory"])
var/newcategory = input("Choose a category to search for:") in list("Any", "Fiction", "Non-Fiction", "Reference", "Religion")
if(newcategory)
category = sanitize(newcategory)
else
category = "Any"
category = sanitizeSQL(category)
if(href_list["setauthor"])
var/newauthor = input("Enter an author to search for:") as text|null
if(newauthor)
author = sanitize(newauthor)
else
author = null
author = sanitizeSQL(author)
if(href_list["search"])
SQLquery = "SELECT author, title, category, id FROM ss13_library WHERE "
if(category == "Any")
SQLquery += "author LIKE '%[author]%' AND title LIKE '%[title]%'"
else
SQLquery += "author LIKE '%[author]%' AND title LIKE '%[title]%' AND category='[category]'"
screenstate = 1
if(href_list["back"])
screenstate = 0
src.add_fingerprint(usr)
src.updateUsrDialog()
return
/*
* Library Computer
*/
/obj/machinery/librarycomp
name = "library computer"
desc = "A computer."
icon = 'icons/obj/library.dmi'
icon_state = "computer"
anchored = TRUE
density = TRUE
var/arcanecheckout = FALSE
var/screenstate = 0 // 0: Main Menu - 1: Inventory - 2: Checked Out - 3: Check Out
var/sortby = "author"
var/buffer_book
var/buffer_mob
var/upload_category = "Fiction"
var/list/checkouts = list()
var/list/inventory = list()
var/checkoutperiod = 5 // In minutes
var/bibledelay = 0
var/is_public = FALSE
var/obj/machinery/libraryscanner/scanner // Book scanner that will be used when uploading books to the Archive
/obj/machinery/librarycomp/attack_hand(var/mob/user)
user.set_machine(src)
var/dat = ""
switch(screenstate)
if(0)
// Main Menu
dat += "<a href='byond://?src=[REF(src)];switchscreen=1'>View Stock</a><br>"
dat += "<a href='byond://?src=[REF(src)];switchscreen=2'>View Checked Out Books</a><br>"
dat += "<a href='byond://?src=[REF(src)];switchscreen=3'>Check out a Book</a><br>"
dat += "<a href='byond://?src=[REF(src)];switchscreen=4'>Order From Library Database</a><br>"
if(!is_public)
dat += "<a href='byond://?src=[REF(src)];switchscreen=5'>Upload New Title to Library Database</a><br>"
dat += "<a href='byond://?src=[REF(src)];switchscreen=6'>Print a Bible</a><br>"
if(emagged)
dat += "<a href='byond://?src=[REF(src)];switchscreen=7'>7. Access the Forbidden Lore Vault</a><br>"
if(arcanecheckout)
new /obj/item/book/tome(get_turf(src))
to_chat(user, SPAN_WARNING("Your sanity barely endures the seconds spent in the vault's browsing window. The only thing to remind you of this when you stop browsing is a dusty old tome sitting on the desk. You don't really remember printing it."))
user.visible_message(SPAN_NOTICE("\The [user] stares at the blank screen for a few moments, [user.get_pronoun("his")] expression frozen in fear. When [user.get_pronoun("he")] finally awakens from it, [user.get_pronoun("he")] looks a lot older."), range = 2)
arcanecheckout = FALSE
if(1)
// Inventory
dat += "<H3>Inventory</H3><br>"
for(var/obj/item/book/b in inventory)
dat += "[b.name] <a href='byond://?src=[REF(src)];delbook=[REF(b)]'>(Delete)</a><br>"
dat += "<a href='byond://?src=[REF(src)];switchscreen=0'>(<-- Return to Main Menu)</a><br>"
if(2)
// Checked Out
dat += "<h3>Checked Out Books</h3><br>"
for(var/datum/borrowbook/b in checkouts)
var/timetaken = world.time - b.getdate
//timetaken *= 10
timetaken /= 600
timetaken = round(timetaken)
var/timedue = b.duedate - world.time
//timedue *= 10
timedue /= 600
if(timedue <= 0)
timedue = "<font color=red><b>(OVERDUE)</b> [timedue]</font>"
else
timedue = round(timedue)
dat += {"\"[b.bookname]\", Checked out to: [b.mobname]<br>--- Taken: [timetaken] minutes ago, Due: in [timedue] minutes<br>
<a href='byond://?src=[REF(src)];checkin=[REF(b)]'>(Check In)</a><br><br>"}
dat += "<a href='byond://?src=[REF(src)];switchscreen=0'>(<-- Return to Main Menu)</a><br>"
if(3)
// Check Out a Book
dat += {"<h3>Check Out a Book</h3><br>
Book: [src.buffer_book]
<a href='byond://?src=[REF(src)];editbook=1'>\[Edit\]</a><br>
Recipient: [src.buffer_mob]
<a href='byond://?src=[REF(src)];editmob=1'>\[Edit\]</a><br>
Checkout Date: [world.time / 600]<br>
Due Date: [(world.time + checkoutperiod) / 600]<br>
(Checkout Period: [checkoutperiod] minutes) (<a href='byond://?src=[REF(src)];increasetime=1'>+</a>/<a href='byond://?src=[REF(src)];decreasetime=1'>-</a>)<br>
<a href='byond://?src=[REF(src)];checkout=1'>(Commit Entry)</a><br>
<a href='byond://?src=[REF(src)];switchscreen=0'>(<-- Return to Main Menu)</a><br>"}
if(4)
dat += "<h3>External Archive</h3>"
if(!establish_db_connection(GLOB.dbcon))
dat += "<font color=red><b>ERROR</b>: Unable to contact External Archive. Please contact your system administrator for assistance.</font>"
else
dat += {"<a href='byond://?src=[REF(src)];orderbyid=1'>(Order Book by ISBN)</a><br><br>
<table>
<tr><td><a href='byond://?src=[REF(src)];sort=author'>AUTHOR</a></td><td><a href='byond://?src=[REF(src)];sort=title'>TITLE</a></td><td><a href='byond://?src=[REF(src)];sort=category'>CATEGORY</a></td><td></td></tr>"}
var/DBQuery/query = GLOB.dbcon.NewQuery("SELECT id, author, title, category FROM ss13_library ORDER BY [sortby]")
query.Execute()
while(query.NextRow())
var/id = query.item[1]
var/author = query.item[2]
var/title = query.item[3]
var/category = query.item[4]
dat += "<tr><td>[author]</td><td>[title]</td><td>[category]</td><td><a href='byond://?src=[REF(src)];targetid=[id]'>\[Order\]</a></td></tr>"
dat += "</table>"
dat += "<br><a href='byond://?src=[REF(src)];switchscreen=0'>(<-- Return to Main Menu)</a><br>"
if(5)
dat += "<H3>Upload a New Title</H3>"
if(!scanner)
for(var/obj/machinery/libraryscanner/S in range(9))
if(S.anchored)
scanner = S
break
if(!(scanner?.anchored))
dat += "<FONT color=red>No scanner found within wireless network range.</FONT><br>"
else if(!scanner.cache)
dat += "<FONT color=red>No data found in scanner memory.</FONT><br>"
else
dat += {"<TT>Data marked for upload...</TT><br>
<TT>Title: </TT>[scanner.cache.name]<br>"}
if(!scanner.cache.author)
scanner.cache.author = "Anonymous"
dat += {"<TT>Author: </TT><a href='byond://?src=[REF(src)];setauthor=1'>[scanner.cache.author]</a><br>
<TT>Category: </TT><a href='byond://?src=[REF(src)];setcategory=1'>[upload_category]</a><br>
<a href='byond://?src=[REF(src)];upload=1'>\[Upload\]</a><br>"}
dat += "<a href='byond://?src=[REF(src)];switchscreen=0'>(<-- Return to Main Menu)</a><br>"
if(7)
dat += {"<h3>Accessing Forbidden Lore Vault v 1.3</h3>
Are you absolutely sure you want to proceed? EldritchTomes Inc. takes no responsibilities for loss of sanity resulting from this action.<p>
<a href='byond://?src=[REF(src)];arccheckout=1'>Yes.</a><br>
<a href='byond://?src=[REF(src)];switchscreen=0'>No.</a><br>"}
//dat += "<a href='byond://?src=[REF(user)];mach_close=library'>Close</a><br><br>"
user << browse(HTML_SKELETON_TITLE(is_public ? "[SSatlas.current_map.station_name] Library" : "[SSatlas.current_map.station_name] Library Management", dat), "window=library")
onclose(user, "library")
/obj/machinery/librarycomp/emag_act(var/remaining_charges, var/mob/user)
if (src.density && !src.emagged)
src.emagged = 1
return 1
/obj/machinery/librarycomp/attackby(obj/item/attacking_item, mob/user)
if(istype(attacking_item, /obj/item/barcodescanner))
var/obj/item/barcodescanner/scanner = attacking_item
scanner.computer = src
to_chat(user, "[scanner]'s associated machine has been set to [src].")
for (var/mob/V in hearers(src))
V.show_message("[src] lets out a low, short blip.", 2)
else
..()
/obj/machinery/librarycomp/Topic(href, href_list)
if(..())
usr << browse(null, "window=library")
onclose(usr, "library")
return
if(href_list["switchscreen"])
switch(href_list["switchscreen"])
if("0")
screenstate = 0
if("1")
screenstate = 1
if("2")
screenstate = 2
if("3")
screenstate = 3
if("4")
screenstate = 4
if("5")
screenstate = 5
if("6")
if(!bibledelay)
var/obj/item/storage/bible/B = new /obj/item/storage/bible(src.loc)
B.verbs += /obj/item/storage/bible/verb/Set_Religion
var/randbook = "book" + pick("1", "2", "3", "4", "5", "6" , "7", "8", "9", "10", "11", "12", "13" , "14", "15" , "16")
B.icon_state = randbook
B.item_state = randbook
B.name = "religious book"
bibledelay = 1
spawn(60)
bibledelay = 0
else
for (var/mob/V in hearers(src))
V.show_message("<b>[src]</b>'s monitor flashes, \"Bible printer currently unavailable, please wait a moment.\"")
if("7")
screenstate = 7
if(href_list["arccheckout"])
if(src.emagged)
src.arcanecheckout = 1
src.screenstate = 0
if(href_list["increasetime"])
checkoutperiod += 1
if(href_list["decreasetime"])
checkoutperiod -= 1
if(checkoutperiod < 1)
checkoutperiod = 1
if(href_list["editbook"])
buffer_book = sanitizeSafe(input("Enter the book's title:") as text|null)
if(href_list["editmob"])
buffer_mob = sanitize(input("Enter the recipient's name:") as text|null, MAX_NAME_LEN)
if(href_list["checkout"])
var/datum/borrowbook/b = new /datum/borrowbook
b.bookname = sanitizeSafe(buffer_book)
b.mobname = sanitize(buffer_mob)
b.getdate = world.time
b.duedate = world.time + (checkoutperiod * 600)
checkouts.Add(b)
if(href_list["checkin"])
var/datum/borrowbook/b = locate(href_list["checkin"])
checkouts.Remove(b)
if(href_list["delbook"])
var/obj/item/book/b = locate(href_list["delbook"])
inventory.Remove(b)
if(href_list["setauthor"])
var/newauthor = sanitize(input("Enter the author's name: ") as text|null)
if(newauthor)
scanner.cache.author = newauthor
if(href_list["setcategory"])
var/newcategory = input("Choose a category: ") in list("Fiction", "Non-Fiction", "Reference", "Religion")
if(newcategory)
upload_category = newcategory
if(href_list["upload"])
if(scanner?.anchored)
if(scanner.cache)
var/choice = input("Are you certain you wish to upload this title to the Archive?") in list("Confirm", "Abort")
if(choice == "Confirm")
if(scanner.cache.unique)
alert("This book has been rejected from the database. Aborting!")
else
if(!establish_db_connection(GLOB.dbcon))
alert("Connection to Archive has been severed. Aborting.")
else
var/sqltitle = sanitizeSQL(scanner.cache.name)
var/sqlauthor = sanitizeSQL(scanner.cache.author)
var/sqlcontent = sanitizeSQL(scanner.cache.dat)
var/sqlcategory = sanitizeSQL(upload_category)
var/sqlckey = sanitizeSQL(ckey(usr.client.ckey))
var/DBQuery/query = GLOB.dbcon.NewQuery("INSERT INTO ss13_library (author, title, content, category, uploadtime, uploader) VALUES ('[sqlauthor]', '[sqltitle]', '[sqlcontent]', '[sqlcategory]', NOW(), '[sqlckey]')")
if(!query.Execute())
to_chat(usr, query.ErrorMsg())
else
log_and_message_admins("has uploaded the book titled [scanner.cache.name], [length(scanner.cache.dat)] signs")
log_game("[usr.name]/[usr.key] has uploaded the book titled [scanner.cache.name], [length(scanner.cache.dat)] signs")
alert("Upload Complete.")
if(href_list["targetid"])
var/sqlid = sanitizeSQL(href_list["targetid"])
if(!establish_db_connection(GLOB.dbcon))
alert("Connection to Archive has been severed. Aborting.")
if(bibledelay)
for (var/mob/V in hearers(src))
V.show_message("<b>[src]</b>'s monitor flashes, \"Printer unavailable. Please allow a short time before attempting to print.\"")
else
bibledelay = 1
spawn(60)
bibledelay = 0
var/DBQuery/query = GLOB.dbcon.NewQuery("SELECT * FROM ss13_library WHERE id=[sqlid]")
query.Execute()
while(query.NextRow())
var/author = query.item[2]
var/title = query.item[3]
var/content = query.item[4]
var/obj/item/book/B = new(src.loc)
B.name = "Book: [title]"
B.title = title
B.author = author
B.dat = content
B.icon_state = "book[rand(1,16)]"
B.item_state = B.icon_state
src.visible_message("\The [src]\s printer hums as it produces a book.")
break
if(href_list["orderbyid"])
var/orderid = input("Enter your order:") as num|null
if(orderid)
if(isnum(orderid))
var/nhref = "src=[REF(src)];targetid=[orderid]"
spawn() src.Topic(nhref, params2list(nhref), src)
if(href_list["sort"] in list("author", "title", "category"))
sortby = href_list["sort"]
src.add_fingerprint(usr)
src.updateUsrDialog()
return
// Public Related Code
/obj/machinery/librarycomp/public
name = "public library computer"
is_public = TRUE
/*
* Library Scanner
*/
/obj/machinery/libraryscanner
name = "book scanner"
desc = "A machine that scans books for upload to the library database."
icon = 'icons/obj/library.dmi'
icon_state = "bigscanner"
var/insert_anim = "bigscanner1"
anchored = TRUE
density = TRUE
var/obj/item/book/cache // Last scanned book
/obj/machinery/libraryscanner/attackby(obj/item/attacking_item, mob/user)
if(istype(attacking_item, /obj/item/book))
if(!anchored)
to_chat(user, SPAN_WARNING("\The [src] must be secured to the floor first!"))
return
user.drop_from_inventory(attacking_item,src)
if(attacking_item.tool_behaviour == TOOL_WRENCH)
attacking_item.play_tool_sound(get_turf(src), 75)
if(anchored)
user.visible_message(SPAN_NOTICE("\The [user] unsecures \the [src] from the floor."),
SPAN_NOTICE("You unsecure \the [src] from the floor."),
SPAN_WARNING("You hear a ratcheting noise."))
else
user.visible_message(SPAN_NOTICE("\The [user] secures \the [src] to the floor."),
SPAN_NOTICE("You secure \the [src] to the floor."),
SPAN_WARNING("You hear a ratcheting noise."))
anchored = !anchored
/obj/machinery/libraryscanner/attack_hand(var/mob/user)
usr.set_machine(src)
var/dat = ""
if(cache)
dat += "<FONT color=#005500>Data stored in memory.</FONT><br>"
else
dat += "No data stored in memory.<br>"
dat += "<a href='byond://?src=[REF(src)];scan=1'>\[Scan\]</a>"
if(cache)
dat += " <a href='byond://?src=[REF(src)];clear=1'>\[Clear Memory\]</a><br><br><a href='byond://?src=[REF(src)];eject=1'>\[Remove Book\]</a>"
else
dat += "<br>"
user << browse(HTML_SKELETON_TITLE("Scanner Control Interface", dat), "window=scanner")
onclose(user, "scanner")
/obj/machinery/libraryscanner/Topic(href, href_list)
if(..())
usr << browse(null, "window=scanner")
onclose(usr, "scanner")
return
if(href_list["scan"])
flick(insert_anim, src)
playsound(loc, 'sound/items/bureaucracy/scan.ogg', 75, 1)
for(var/obj/item/book/B in contents)
cache = B
break
if(href_list["clear"])
cache = null
if(href_list["eject"])
for(var/obj/item/book/B in contents)
B.forceMove(src.loc)
src.add_fingerprint(usr)
src.updateUsrDialog()
return
/*
* Book binder
*/
/obj/machinery/bookbinder
name = "book binder"
desc = "A machine that takes paper and binds them into books. Fascinating!"
icon = 'icons/obj/library.dmi'
icon_state = "binder"
anchored = TRUE
density = TRUE
var/binding = FALSE
/obj/machinery/bookbinder/attackby(obj/item/attacking_item, mob/user)
if(istype(attacking_item, /obj/item/paper))
var/obj/item/paper/paper = attacking_item
if(!anchored)
to_chat(user, SPAN_WARNING("\The [src] must be secured to the floor first!"))
return
if(binding)
to_chat(user, SPAN_WARNING("You must wait for \the [src] to finish its current operation!"))
return
var/turf/T = get_turf(src)
user.drop_from_inventory(paper,src)
user.visible_message(SPAN_NOTICE("\The [user] loads some paper into \the [src]."), SPAN_NOTICE("You load some paper into \the [src]."))
visible_message(SPAN_NOTICE("\The [src] begins to hum as it warms up its printing drums."))
playsound(T, 'sound/items/bureaucracy/binder.ogg', 75, 1)
binding = TRUE
sleep(rand(200,400))
binding = FALSE
if(!anchored)
visible_message(SPAN_WARNING("\The [src] buzzes and flashes an error light."))
paper.forceMove(T)
return
visible_message(SPAN_NOTICE("\The [src] whirs as it prints and binds a new book."))
playsound(T, 'sound/items/bureaucracy/print.ogg', 75, 1)
var/obj/item/book/b = new(T)
b.dat = paper.info
b.name = "blank book"
b.icon_state = "book[rand(1,7)]"
b.item_state = icon_state
qdel(paper)
return
if(attacking_item.tool_behaviour == TOOL_WRENCH)
attacking_item.play_tool_sound(get_turf(src), 75)
if(anchored)
user.visible_message(SPAN_NOTICE("\The [user] unsecures \the [src] from the floor."), \
SPAN_NOTICE("You unsecure \the [src] from the floor."), \
SPAN_WARNING("You hear a ratcheting noise."))
else
user.visible_message(SPAN_NOTICE("\The [user] secures \the [src] to the floor."), \
SPAN_NOTICE("You secure \the [src] to the floor."), \
SPAN_WARNING("You hear a ratcheting noise."))
anchored = !anchored
@@ -0,0 +1,97 @@
/*
* Library Public Computer
*/
/obj/machinery/librarypubliccomp
name = "public library computer"
desc = "A computer."
icon = 'icons/obj/library.dmi'
icon_state = "computer"
anchored = TRUE
density = TRUE
var/list/search_results = list()
var/search_title = ""
var/search_author = ""
var/search_category = "Any"
var/db_loading = FALSE
var/db_error = FALSE
/obj/machinery/librarypubliccomp/attack_hand(var/mob/user)
. = ..()
if(.)
return TRUE
ui_interact(user)
/obj/machinery/librarypubliccomp/ui_interact(mob/user, datum/tgui/ui)
ui = SStgui.try_update_ui(user, src, ui)
if(!ui)
ui = new(user, src, "LibraryPublicComputer", "Public Library Terminal", 650, 500)
ui.open()
/obj/machinery/librarypubliccomp/ui_data(mob/user)
var/list/data = list()
data["search_title"] = search_title
data["search_author"] = search_author
data["search_category"] = search_category
data["db_loading"] = db_loading
data["db_error"] = db_error
data["search_results"] = search_results
return data
/obj/machinery/librarypubliccomp/ui_act(action, list/params, datum/tgui/ui, datum/ui_state/state)
. = ..()
if(.)
return
add_fingerprint(usr)
switch(action)
if("search")
if(!db_loading)
search_title = sanitize(params["title"])
search_author = sanitize(params["author"])
var/new_category = params["category"]
if(new_category in list("Any", "Fiction", "Non-Fiction", "Reference", "Religion"))
search_category = new_category
INVOKE_ASYNC(src, PROC_REF(async_search))
. = TRUE
/obj/machinery/librarypubliccomp/proc/async_search()
db_loading = TRUE
db_error = FALSE
search_results = list()
SStgui.update_uis(src)
var/sql
var/list/query_args
if(search_category != "Any")
sql = "SELECT author, title, category, id FROM ss13_library WHERE author LIKE :author AND title LIKE :title AND category = :category"
query_args = list(
"title" = "%[search_title]%",
"author" = "%[search_author]%",
"category" = search_category
)
else
sql = "SELECT author, title, category, id FROM ss13_library WHERE author LIKE :author AND title LIKE :title"
query_args = list(
"title" = "%[search_title]%",
"author" = "%[search_author]%"
)
var/datum/db_query/query = SSdbcore.NewQuery(sql, query_args)
if(!query.Execute())
db_error = TRUE
qdel(query)
db_loading = FALSE
SStgui.update_uis(src)
return
var/list/results = list()
while(query.NextRow())
results += list(list(
"author" = query.item[1],
"title" = query.item[2],
"category" = query.item[3],
"id" = text2num(query.item[4])
))
qdel(query)
search_results = results
db_loading = FALSE
SStgui.update_uis(src)
+74
View File
@@ -0,0 +1,74 @@
/*
* Library Scanner
*/
/obj/machinery/libraryscanner
name = "book scanner"
desc = "A machine that scans books for upload to the library database."
icon = 'icons/obj/library.dmi'
icon_state = "bigscanner"
var/insert_animation = "bigscanner1"
anchored = TRUE
density = TRUE
var/obj/item/book/cache // Last scanned book
/obj/machinery/libraryscanner/attackby(obj/item/attacking_item, mob/user)
if(istype(attacking_item, /obj/item/book))
if(!anchored)
to_chat(user, SPAN_WARNING("\The [src] must be secured to the floor first!"))
return
user.drop_from_inventory(attacking_item, src)
if(attacking_item.tool_behaviour == TOOL_WRENCH)
attacking_item.play_tool_sound(get_turf(src), 75)
if(anchored)
user.visible_message(
SPAN_NOTICE("\The [user] unsecures \the [src] from the floor."),
SPAN_NOTICE("You unsecure \the [src] from the floor."),
SPAN_WARNING("You hear a ratcheting noise."))
else
user.visible_message(
SPAN_NOTICE("\The [user] secures \the [src] to the floor."),
SPAN_NOTICE("You secure \the [src] to the floor."),
SPAN_WARNING("You hear a ratcheting noise."))
anchored = !anchored
/obj/machinery/libraryscanner/attack_hand(var/mob/user)
. = ..()
if(.)
return TRUE
ui_interact(user)
/obj/machinery/libraryscanner/ui_interact(mob/user, datum/tgui/ui)
ui = SStgui.try_update_ui(user, src, ui)
if(!ui)
ui = new(user, src, "LibraryScanner", "Book Scanner", 400, 250)
ui.open()
/obj/machinery/libraryscanner/ui_data(mob/user)
var/list/data = list()
data["has_book"] = !!cache
data["book_title"] = cache ? cache.name : null
data["book_author"] = cache ? cache.author : null
data["is_anchored"] = anchored
return data
/obj/machinery/libraryscanner/ui_act(action, list/params, datum/tgui/ui, datum/ui_state/state)
. = ..()
if(.)
return
add_fingerprint(usr)
switch(action)
if("scan")
flick(insert_animation, src)
playsound(loc, 'sound/items/bureaucracy/scan.ogg', 75, 1)
for(var/obj/item/book/book in contents)
cache = book
break
. = TRUE
if("clear")
cache = null
. = TRUE
if("eject")
for(var/obj/item/book/book in contents)
book.forceMove(src.loc)
cache = null
. = TRUE
+58
View File
@@ -0,0 +1,58 @@
################################
# Example Changelog File
#
# Note: This file, and files beginning with ".", and files that don't end in ".yml" will not be read. If you change this file, you will look really dumb.
#
# Your changelog will be merged with a master changelog. (New stuff added only, and only on the date entry for the day it was merged.)
# When it is, any changes listed below will disappear.
#
# Valid Prefixes:
# bugfix
# - (fixes bugs)
# wip
# - (work in progress)
# qol
# - (quality of life)
# soundadd
# - (adds a sound)
# sounddel
# - (removes a sound)
# rscadd
# - (adds a feature)
# rscdel
# - (removes a feature)
# imageadd
# - (adds an image or sprite)
# imagedel
# - (removes an image or sprite)
# spellcheck
# - (fixes spelling or grammar)
# experiment
# - (experimental change)
# balance
# - (balance changes)
# code_imp
# - (misc internal code change)
# refactor
# - (refactors code)
# config
# - (makes a change to the config files)
# admin
# - (makes changes to administrator tools)
# server
# - (miscellaneous changes to server)
#################################
# Your name.
author: arrow768
# Optional: Remove this file after generating master changelog. Useful for PR changelogs that won't get used again.
delete-after: True
# Any changes you've made. See valid prefix list above.
# INDENT WITH TWO SPACES. NOT TABS. SPACES.
# SCREW THIS UP AND IT WON'T WORK.
# Also, this gets changed to [] after reading. Just remove the brackets when you add new shit.
# Please surround your changes in double quotes ("). It works without them, but if you use certain characters it screws up compiling. The quotes will not show up in the changelog.
changes:
- refactor: "Updates the Library Machines to TGUI"
@@ -0,0 +1,755 @@
import { BooleanLike } from '../../common/react';
import { useBackend, useLocalState } from '../backend';
import {
Box,
Button,
Icon,
Input,
LabeledList,
NoticeBox,
Section,
Stack,
Table,
Tabs,
} from '../components';
import { Dropdown } from '../components/Dropdown';
import { Window } from '../layouts';
export type LibraryComputerData = {
is_public: BooleanLike;
is_emagged: BooleanLike;
bible_on_cooldown: BooleanLike;
checkout_period_minutes: number;
archive_loading: BooleanLike;
archive_error: BooleanLike;
archive_results: ArchiveEntry[];
archive_page: number;
archive_total: number;
archive_search: string;
archive_sort_field: string;
archive_sort_dir: string;
archive_page_size: number;
upload_category: string;
buffer_book: string | null;
last_uploaded_title: string | null;
inventory: InventoryBook[];
checkouts: CheckoutEntry[];
scanner: ScannerState;
crew_names: string[];
};
type InventoryBook = {
ref: string;
name: string;
};
type CheckoutEntry = {
ref: string;
book_name: string;
mob_name: string;
taken_minutes: number;
due_minutes: number;
overdue: BooleanLike;
};
type ArchiveEntry = {
id: number;
author: string;
title: string;
category: string;
};
type ScannerState = {
found: BooleanLike;
title: string | null;
author: string | null;
};
const UPLOAD_CATEGORIES = ['Fiction', 'Non-Fiction', 'Reference', 'Religion'];
export const LibraryComputer = (props, context) => {
const { act, data } = useBackend<LibraryComputerData>(context);
const [tab, setTab] = useLocalState(context, 'tab', 'inventory');
const handleTabChange = (newTab: string) => {
setTab(newTab);
if (
newTab === 'archive' &&
!data.archive_loading &&
data.archive_results.length === 0
) {
act('fetch_archive');
}
};
return (
<Window resizable width={750} height={600}>
<Window.Content>
<Tabs>
<Tabs.Tab
selected={tab === 'inventory'}
onClick={() => handleTabChange('inventory')}
>
Inventory
</Tabs.Tab>
<Tabs.Tab
selected={tab === 'checkouts'}
onClick={() => handleTabChange('checkouts')}
>
Checked Out
</Tabs.Tab>
<Tabs.Tab
selected={tab === 'checkout'}
onClick={() => handleTabChange('checkout')}
>
Check Out
</Tabs.Tab>
<Tabs.Tab
selected={tab === 'archive'}
onClick={() => handleTabChange('archive')}
>
Order From Archive
</Tabs.Tab>
{!data.is_public && (
<Tabs.Tab
selected={tab === 'upload'}
onClick={() => handleTabChange('upload')}
>
Upload Title
</Tabs.Tab>
)}
<Tabs.Tab
selected={tab === 'bible'}
onClick={() => handleTabChange('bible')}
>
Bible Printer
</Tabs.Tab>
{!!data.is_emagged && (
<Tabs.Tab
selected={tab === 'vault'}
onClick={() => handleTabChange('vault')}
>
Forbidden Lore
</Tabs.Tab>
)}
</Tabs>
<Box>
{tab === 'inventory' && <InventoryTab />}
{tab === 'checkouts' && <CheckoutsTab />}
{tab === 'checkout' && <CheckOutTab />}
{tab === 'archive' && <ArchiveTab />}
{tab === 'upload' && !data.is_public && <UploadTab />}
{tab === 'bible' && <BibleTab />}
{tab === 'vault' && !!data.is_emagged && <VaultTab />}
</Box>
</Window.Content>
</Window>
);
};
const InventoryTab = (props, context) => {
const { act, data } = useBackend<LibraryComputerData>(context);
return (
<Section
title="Physical Inventory"
buttons={
data.inventory.length > 0 && (
<Button
icon="print"
content="Print List"
onClick={() => act('print_inventory')}
/>
)
}
>
{data.inventory.length === 0 ? (
<NoticeBox>No books in inventory.</NoticeBox>
) : (
<Table>
<Table.Row header>
<Table.Cell>Title</Table.Cell>
<Table.Cell collapsing />
</Table.Row>
{data.inventory.map((b) => (
<Table.Row key={b.ref}>
<Table.Cell>{b.name}</Table.Cell>
<Table.Cell collapsing>
<Button
color="bad"
icon="trash"
content="Delete"
onClick={() => act('delete_inventory_book', { ref: b.ref })}
/>
</Table.Cell>
</Table.Row>
))}
</Table>
)}
</Section>
);
};
const CheckoutsTab = (props, context) => {
const { act, data } = useBackend<LibraryComputerData>(context);
return (
<Section
title="Active Checkouts"
buttons={
data.checkouts.length > 0 && (
<Button
icon="print"
content="Print List"
onClick={() => act('print_checkouts')}
/>
)
}
>
{data.checkouts.length === 0 ? (
<NoticeBox>No books currently checked out.</NoticeBox>
) : (
<Table>
<Table.Row header>
<Table.Cell>Book</Table.Cell>
<Table.Cell>Recipient</Table.Cell>
<Table.Cell>Taken</Table.Cell>
<Table.Cell>Due</Table.Cell>
<Table.Cell collapsing />
</Table.Row>
{data.checkouts.map((c) => (
<Table.Row key={c.ref}>
<Table.Cell>{c.book_name}</Table.Cell>
<Table.Cell>{c.mob_name}</Table.Cell>
<Table.Cell>{c.taken_minutes} min ago</Table.Cell>
<Table.Cell color={c.overdue ? 'bad' : 'default'}>
{c.overdue
? `OVERDUE by ${c.due_minutes} min`
: `in ${c.due_minutes} min`}
</Table.Cell>
<Table.Cell collapsing>
<Button
icon="clock"
tooltip={`Extend by ${data.checkout_period_minutes} min`}
onClick={() => act('extend_checkout', { ref: c.ref })}
/>
<Button
content="Check In"
onClick={() => act('checkin_book', { ref: c.ref })}
/>
</Table.Cell>
</Table.Row>
))}
</Table>
)}
</Section>
);
};
const CheckOutTab = (props, context) => {
const { act, data } = useBackend<LibraryComputerData>(context);
const [bookTitle, setBookTitle] = useLocalState(context, 'co_book', '');
const [recipient, setRecipient] = useLocalState(context, 'co_recip', '');
const [showBookPicker, setShowBookPicker] = useLocalState(
context,
'co_book_picker',
false,
);
const [showRecipientPicker, setShowRecipientPicker] = useLocalState(
context,
'co_recip_picker',
false,
);
return (
<Section title="Check Out a Book">
{!!data.buffer_book && bookTitle !== data.buffer_book && (
<Box mb={1}>
<Button
icon="barcode"
content={`Use scanned book: "${data.buffer_book}"`}
onClick={() => setBookTitle(data.buffer_book!)}
/>
</Box>
)}
<LabeledList>
<LabeledList.Item label="Book Title">
<Stack>
<Stack.Item grow>
<Input
fluid
value={bookTitle}
placeholder="Enter book title"
onInput={(e, v) => setBookTitle(v)}
/>
</Stack.Item>
{data.inventory.length > 0 && (
<Stack.Item>
<Button
icon="list"
selected={showBookPicker}
tooltip="Pick from inventory"
onClick={() => {
setShowBookPicker(!showBookPicker);
setShowRecipientPicker(false);
}}
/>
</Stack.Item>
)}
</Stack>
{showBookPicker && (
<Box
mt={0.5}
style={{
maxHeight: '150px',
overflowY: 'auto',
border: '1px solid rgba(255,255,255,0.2)',
}}
>
{data.inventory.map((b) => (
<Button
key={b.ref}
fluid
content={b.name}
onClick={() => {
setBookTitle(b.name);
setShowBookPicker(false);
}}
/>
))}
</Box>
)}
</LabeledList.Item>
<LabeledList.Item label="Recipient">
<Stack>
<Stack.Item grow>
<Input
fluid
value={recipient}
placeholder="Enter recipient name"
onInput={(e, v) => setRecipient(v)}
/>
</Stack.Item>
{data.crew_names.length > 0 && (
<Stack.Item>
<Button
icon="users"
selected={showRecipientPicker}
tooltip="Pick from crew manifest"
onClick={() => {
setShowRecipientPicker(!showRecipientPicker);
setShowBookPicker(false);
}}
/>
</Stack.Item>
)}
</Stack>
{showRecipientPicker && (
<Box
mt={0.5}
style={{
maxHeight: '150px',
overflowY: 'auto',
border: '1px solid rgba(255,255,255,0.2)',
}}
>
{data.crew_names.map((name) => (
<Button
key={name}
fluid
content={name}
onClick={() => {
setRecipient(name);
setShowRecipientPicker(false);
}}
/>
))}
</Box>
)}
</LabeledList.Item>
<LabeledList.Item label="Checkout Period">
<Button
icon="minus"
onClick={() => act('decrease_checkout_period')}
/>
<Box inline mx={1}>
{data.checkout_period_minutes} minute
{data.checkout_period_minutes !== 1 && 's'}
</Box>
<Button icon="plus" onClick={() => act('increase_checkout_period')} />
</LabeledList.Item>
</LabeledList>
<Box mt={1}>
<Button
icon="check"
content="Commit Checkout"
color="good"
disabled={!bookTitle || !recipient}
onClick={() => {
act('checkout_book', {
book_title: bookTitle,
recipient: recipient,
});
setBookTitle('');
setRecipient('');
}}
/>
</Box>
</Section>
);
};
type SortField = 'author' | 'title' | 'category';
const ArchiveTab = (props, context) => {
const { act, data } = useBackend<LibraryComputerData>(context);
// Local state only for the search input — committed to server on Enter or button click
const [searchInput, setSearchInput] = useLocalState(
context,
'archive_search_input',
data.archive_search,
);
const [isbnInput, setIsbnInput] = useLocalState(context, 'isbn_input', '');
const totalPages = Math.max(
1,
Math.ceil(data.archive_total / data.archive_page_size),
);
const commitSearch = (value: string) => {
if (!data.archive_loading) {
act('archive_set_search', { query: value });
}
};
const handleSort = (field: SortField) => {
const newDir =
data.archive_sort_field === field && data.archive_sort_dir === 'asc'
? 'desc'
: 'asc';
act('archive_set_sort', { field, dir: newDir });
};
const SortHeader = ({
field,
label,
}: {
field: SortField;
label: string;
}) => (
<Table.Cell
style={{ cursor: 'pointer', userSelect: 'none' }}
onClick={() => handleSort(field)}
>
{label}
{data.archive_sort_field === field && (
<Icon
name={data.archive_sort_dir === 'asc' ? 'sort-up' : 'sort-down'}
ml={0.5}
/>
)}
</Table.Cell>
);
return (
<Section
title="External Archive"
buttons={
<Button
icon="refresh"
content="Refresh"
disabled={!!data.archive_loading}
onClick={() => act('fetch_archive')}
/>
}
>
<Stack mb={1}>
<Stack.Item grow>
<Input
fluid
value={searchInput}
placeholder="Search by title, author, or category..."
onInput={(e, v) => setSearchInput(v)}
onEnter={(e, v) => commitSearch(v)}
/>
</Stack.Item>
<Stack.Item>
<Button
icon="search"
content="Search"
disabled={!!data.archive_loading}
onClick={() => commitSearch(searchInput)}
/>
</Stack.Item>
{!!data.archive_search && (
<Stack.Item>
<Button
icon="times"
content="Clear"
onClick={() => {
setSearchInput('');
act('archive_set_search', { query: '' });
}}
/>
</Stack.Item>
)}
<Stack.Item>
<Input
value={isbnInput}
placeholder="ISBN"
width={8}
onInput={(e, v) => setIsbnInput(v)}
/>
</Stack.Item>
<Stack.Item>
<Button
icon="download"
content="Order by ISBN"
disabled={!!data.bible_on_cooldown || !isbnInput}
onClick={() => {
const id = parseInt(isbnInput, 10);
if (id > 0) {
act('order_book', { id });
setIsbnInput('');
}
}}
/>
</Stack.Item>
</Stack>
{!!data.archive_error && (
<NoticeBox danger>Unable to contact External Archive.</NoticeBox>
)}
{!!data.archive_loading && (
<Stack align="center" justify="center" mt={2} mb={2}>
<Stack.Item>
<Icon color="blue" name="spinner" spin size={3} />
</Stack.Item>
<Stack.Item>Loading archive...</Stack.Item>
</Stack>
)}
{!data.archive_loading &&
!data.archive_error &&
data.archive_total > 0 && (
<>
<Box
style={{
overflowY: 'auto',
maxHeight: '460px',
borderBottom: '1px solid rgba(255,255,255,0.1)',
}}
>
<Table>
<Table.Row header>
<SortHeader field="author" label="Author" />
<SortHeader field="title" label="Title" />
<SortHeader field="category" label="Category" />
<Table.Cell collapsing />
</Table.Row>
{data.archive_results.map((entry) => (
<Table.Row key={entry.id}>
<Table.Cell
style={{
maxWidth: '160px',
overflow: 'hidden',
textOverflow: 'ellipsis',
whiteSpace: 'nowrap',
}}
title={entry.author}
>
{entry.author}
</Table.Cell>
<Table.Cell
style={{
maxWidth: '220px',
overflow: 'hidden',
textOverflow: 'ellipsis',
whiteSpace: 'nowrap',
}}
title={entry.title}
>
{entry.title}
</Table.Cell>
<Table.Cell>{entry.category}</Table.Cell>
<Table.Cell collapsing>
<Button
content="Order"
disabled={!!data.bible_on_cooldown}
onClick={() => act('order_book', { id: entry.id })}
/>
</Table.Cell>
</Table.Row>
))}
</Table>
</Box>
<Stack mt={1} align="center">
<Stack.Item>
<Button
icon="chevron-left"
disabled={!!data.archive_loading || data.archive_page <= 1}
onClick={() =>
act('archive_go_to_page', { page: data.archive_page - 1 })
}
/>
</Stack.Item>
<Stack.Item grow textAlign="center">
Page {data.archive_page} of {totalPages}
{' — '}
{data.archive_total} result{data.archive_total !== 1 && 's'}
{!!data.archive_search && ` for "${data.archive_search}"`}
</Stack.Item>
<Stack.Item>
<Button
icon="chevron-right"
disabled={
!!data.archive_loading || data.archive_page >= totalPages
}
onClick={() =>
act('archive_go_to_page', { page: data.archive_page + 1 })
}
/>
</Stack.Item>
</Stack>
</>
)}
{!data.archive_loading &&
!data.archive_error &&
data.archive_total === 0 && (
<NoticeBox mt={1}>
{data.archive_search
? `No books match "${data.archive_search}".`
: 'No archive data loaded. Click Refresh to load books.'}
</NoticeBox>
)}
</Section>
);
};
const UploadTab = (props, context) => {
const { act, data } = useBackend<LibraryComputerData>(context);
const { scanner } = data;
return (
<Section title="Upload New Title">
{!!data.last_uploaded_title && (
<NoticeBox>
<Stack align="center">
<Stack.Item grow>
<Icon name="check-circle" mr={1} />
Uploaded:{' '}
<Box inline bold>
{data.last_uploaded_title}
</Box>
</Stack.Item>
<Stack.Item>
<Button
icon="times"
content="Clear Scanner"
onClick={() => act('clear_scanner_cache')}
/>
</Stack.Item>
</Stack>
</NoticeBox>
)}
{!scanner.found ? (
<NoticeBox danger>
No scanner found within wireless network range.
</NoticeBox>
) : !scanner.title ? (
<NoticeBox>
<Stack align="center">
<Stack.Item grow>No data found in scanner memory.</Stack.Item>
{!!data.last_uploaded_title && (
<Stack.Item>
<Button
icon="times"
content="Clear Scanner"
onClick={() => act('clear_scanner_cache')}
/>
</Stack.Item>
)}
</Stack>
</NoticeBox>
) : (
<>
<LabeledList>
<LabeledList.Item label="Title">{scanner.title}</LabeledList.Item>
<LabeledList.Item label="Author">
<Input
value={scanner.author || ''}
onInput={(e, val) => act('set_upload_author', { value: val })}
/>
</LabeledList.Item>
<LabeledList.Item label="Category">
<Dropdown
options={UPLOAD_CATEGORIES}
selected={data.upload_category}
onSelected={(val) => act('set_upload_category', { value: val })}
/>
</LabeledList.Item>
</LabeledList>
<Box mt={1}>
<Button
icon="upload"
content="Upload to Archive"
color="good"
onClick={() => act('upload_book')}
/>
<Button
icon="times"
content="Clear Scanner"
ml={1}
onClick={() => act('clear_scanner_cache')}
/>
</Box>
</>
)}
</Section>
);
};
const BibleTab = (props, context) => {
const { act, data } = useBackend<LibraryComputerData>(context);
return (
<Section title="Bible Printer">
<Button
icon="bible"
content="Print a Bible"
disabled={!!data.bible_on_cooldown}
onClick={() => act('print_bible')}
/>
{!!data.bible_on_cooldown && (
<Box mt={1} color="average">
Printer cooling down. Please wait a moment.
</Box>
)}
</Section>
);
};
const VaultTab = (props, context) => {
const { act, data } = useBackend<LibraryComputerData>(context);
return (
<Section title="Forbidden Lore Vault v1.3">
<Box>
Are you absolutely sure you want to proceed? EldritchTomes Inc. takes no
responsibility for loss of sanity resulting from this action.
</Box>
<Box mt={1}>
<Button
color="bad"
icon="skull"
content="Yes."
disabled={!!data.bible_on_cooldown}
onClick={() => act('arcane_confirm')}
/>
</Box>
{!!data.bible_on_cooldown && (
<Box mt={1} color="average">
Printer cooling down. Please wait a moment.
</Box>
)}
</Section>
);
};
@@ -0,0 +1,136 @@
import { BooleanLike } from '../../common/react';
import { useBackend, useLocalState } from '../backend';
import {
Box,
Button,
Icon,
Input,
LabeledList,
NoticeBox,
Section,
Stack,
Table,
} from '../components';
import { Dropdown } from '../components/Dropdown';
import { Window } from '../layouts';
export type LibraryPublicComputerData = {
search_title: string;
search_author: string;
search_category: string;
db_loading: BooleanLike;
db_error: BooleanLike;
search_results: SearchResult[];
};
type SearchResult = {
author: string;
title: string;
category: string;
id: number;
};
const CATEGORIES = ['Any', 'Fiction', 'Non-Fiction', 'Reference', 'Religion'];
export const LibraryPublicComputer = (props, context) => {
const { act, data } = useBackend<LibraryPublicComputerData>(context);
const [title, setTitle] = useLocalState(context, 'title', '');
const [author, setAuthor] = useLocalState(context, 'author', '');
const [category, setCategory] = useLocalState(
context,
'category',
data.search_category,
);
const [hasSearched, setHasSearched] = useLocalState(
context,
'hasSearched',
false,
);
return (
<Window title="Public Library Terminal" width={650} height={500} resizable>
<Window.Content scrollable>
<Section title="Search the Archive">
<LabeledList>
<LabeledList.Item label="Title">
<Input
fluid
value={title}
placeholder="Leave blank for any"
onInput={(e, val) => setTitle(val)}
/>
</LabeledList.Item>
<LabeledList.Item label="Author">
<Input
fluid
value={author}
placeholder="Leave blank for any"
onInput={(e, val) => setAuthor(val)}
/>
</LabeledList.Item>
<LabeledList.Item label="Category">
<Dropdown
options={CATEGORIES}
selected={category}
onSelected={(val) => setCategory(val)}
/>
</LabeledList.Item>
</LabeledList>
<Box mt={1}>
<Button
icon="search"
content="Search"
disabled={!!data.db_loading}
onClick={() => {
setHasSearched(true);
act('search', { title, author, category });
}}
/>
</Box>
</Section>
{!!data.db_error && (
<NoticeBox danger>
Unable to contact External Archive. Please contact your system
administrator.
</NoticeBox>
)}
{!!data.db_loading && (
<Section>
<Stack align="center" justify="center" fill>
<Stack.Item>
<Icon color="blue" name="spinner" spin size={3} />
</Stack.Item>
<Stack.Item>Searching archive...</Stack.Item>
</Stack>
</Section>
)}
{!data.db_loading && data.search_results.length > 0 && (
<Section title={`Results (${data.search_results.length})`}>
<Table>
<Table.Row header>
<Table.Cell>Author</Table.Cell>
<Table.Cell>Title</Table.Cell>
<Table.Cell>Category</Table.Cell>
<Table.Cell>SS13BN</Table.Cell>
</Table.Row>
{data.search_results.map((r) => (
<Table.Row key={r.id}>
<Table.Cell>{r.author}</Table.Cell>
<Table.Cell>{r.title}</Table.Cell>
<Table.Cell>{r.category}</Table.Cell>
<Table.Cell>{r.id}</Table.Cell>
</Table.Row>
))}
</Table>
</Section>
)}
{!data.db_loading &&
!data.db_error &&
hasSearched &&
data.search_results.length === 0 && (
<NoticeBox>No results found. Try a different search.</NoticeBox>
)}
</Window.Content>
</Window>
);
};
@@ -0,0 +1,72 @@
import { BooleanLike } from '../../common/react';
import { useBackend } from '../backend';
import { Box, Button, NoticeBox, Section } from '../components';
import { Window } from '../layouts';
export type LibraryScannerData = {
has_book: BooleanLike;
book_title: string | null;
book_author: string | null;
is_anchored: BooleanLike;
};
export const LibraryScanner = (props, context) => {
const { act, data } = useBackend<LibraryScannerData>(context);
if (!data.is_anchored) {
return (
<Window title="Book Scanner" width={400} height={150}>
<Window.Content>
<NoticeBox danger>
The scanner must be secured to the floor first.
</NoticeBox>
</Window.Content>
</Window>
);
}
return (
<Window title="Book Scanner" width={400} height={250}>
<Window.Content>
<Section title="Memory Status">
{data.has_book ? (
<>
<Box color="good">Data stored in memory.</Box>
<Box mt={1}>
<Box inline bold>
Title:
</Box>{' '}
{data.book_title}
</Box>
<Box>
<Box inline bold>
Author:
</Box>{' '}
{data.book_author || 'Anonymous'}
</Box>
</>
) : (
<Box color="average">No data stored in memory.</Box>
)}
</Section>
<Section title="Controls">
<Button icon="barcode" content="Scan" onClick={() => act('scan')} />
{!!data.has_book && (
<>
<Button
icon="times"
content="Clear Memory"
onClick={() => act('clear')}
/>
<Button
icon="eject"
content="Remove Book"
onClick={() => act('eject')}
/>
</>
)}
</Section>
</Window.Content>
</Window>
);
};