mirror of
https://github.com/Aurorastation/Aurora.3.git
synced 2026-08-26 06:22:26 +01:00
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:
+4
-1
@@ -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"
|
||||
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
@@ -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
|
||||
@@ -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>
|
||||
);
|
||||
};
|
||||
Reference in New Issue
Block a user