280 lines
11 KiB
Rust
280 lines
11 KiB
Rust
use std::sync::Arc;
|
|
|
|
use axum::extract::State;
|
|
use maud::{html, Markup, PreEscaped, DOCTYPE};
|
|
|
|
use crate::{
|
|
clock,
|
|
data::{self, CombinedFish, Filters},
|
|
AppState,
|
|
};
|
|
|
|
pub struct ViewData<'a> {
|
|
pub state: State<Arc<AppState>>,
|
|
pub fish: Vec<&'a CombinedFish<'a>>,
|
|
pub caught_fish: Vec<i32>,
|
|
pub acc_id: String,
|
|
pub filters: Filters,
|
|
pub only_list: bool,
|
|
}
|
|
|
|
pub fn layout(content: Markup) -> Markup {
|
|
html! {
|
|
(DOCTYPE)
|
|
html {
|
|
head {
|
|
title { "Beacon" }
|
|
meta name="viewport" content="width=device-width";
|
|
meta charset="utf-8";
|
|
meta name="description" content="A big fish tracker for FFXIV.";
|
|
meta name="author" content="the insects institute";
|
|
meta property="og:url" content="https://fish.insects.institute";
|
|
meta property="og:title" content="Beacon";
|
|
meta property="og:description" content="A big fish tracker for FFXIV.";
|
|
link rel="stylesheet" href="/static/style.css";
|
|
script src="/static/htmx.js" {}
|
|
}
|
|
|
|
body { (content) }
|
|
}
|
|
}
|
|
}
|
|
|
|
pub fn main_page(data: ViewData) -> Markup {
|
|
let list = html! {
|
|
h2.clock { "ET " (clock::get_current_eorzea_date().format("%H:%M")) }
|
|
(fish_list(&data))
|
|
script src="/static/scripts/dates.js" type="text/javascript" {}
|
|
};
|
|
|
|
let template = html! {
|
|
span style="display: none;" id="account-id" { (data.acc_id) }
|
|
(header(&data))
|
|
div id="list" hx-get="" hx-trigger="every 10s" hx-swap="innerHTML" hx-target="this" hx-on="changeDates" {
|
|
(list)
|
|
}
|
|
|
|
script src="/static/scripts/save.js" type="text/javascript" {}
|
|
};
|
|
|
|
if data.only_list {
|
|
list
|
|
} else {
|
|
layout(html! {
|
|
main {
|
|
(template)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
pub fn fish_list(data: &ViewData) -> Markup {
|
|
html! {
|
|
@for fish in data.fish.clone() {
|
|
section.fish.up[fish.is_up].alwaysup[fish.is_always_up] {
|
|
.title {
|
|
div {
|
|
@if !data.caught_fish.contains(&(fish.entry.id as i32)) {
|
|
form action=(format!("/{}/catch/{}", data.acc_id, fish.entry.id)) method="post" {
|
|
button.catch-button type="submit" { (PreEscaped("✓")) }
|
|
}
|
|
}
|
|
}
|
|
div {
|
|
h3 { (fish.meta.name_en) }
|
|
.subtitle {
|
|
"Patch " (fish.entry.patch)
|
|
}
|
|
}
|
|
}
|
|
.when {
|
|
@if let Some(window) = fish.windows.first() {
|
|
@if fish.is_up {
|
|
"closes " (window.display_end_time())
|
|
} @else {
|
|
"opens " (window.display_start_time())
|
|
}
|
|
br;
|
|
@if fish.is_up {
|
|
.date data-ts=(clock::to_earth_time(window.start_time + window.duration).timestamp_millis()) {
|
|
.inner id=(format!("date-{}", fish.entry.id)) hx-preserve {
|
|
(clock::to_earth_time(window.start_time + window.duration).format("%c %Z"))
|
|
}
|
|
}
|
|
|
|
@if let Some(window2) = fish.windows.get(1) {
|
|
.date.tiny data-ts=(clock::to_earth_time(window2.start_time).timestamp_millis()) {
|
|
"next: "
|
|
.inner id=(format!("nextwindow-{}", fish.entry.id)) hx-preserve {
|
|
(clock::to_earth_time(window2.start_time).format("%c %Z"))
|
|
}
|
|
}
|
|
}
|
|
} @else {
|
|
.date data-ts=(clock::to_earth_time(window.start_time).timestamp_millis()) {
|
|
.inner id=(format!("date-{}", fish.entry.id)) hx-preserve {
|
|
(clock::to_earth_time(window.start_time).format("%c %Z"))
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
}
|
|
.how {
|
|
@for item_id in &fish.entry.best_catch_path {
|
|
@if let Some(item) = data.state.data.db_data.items.get(item_id) {
|
|
span.catchpath title=(item.name_en) {
|
|
img src=(item.get_icon_url()) width="35";
|
|
|
|
@if let Some(hookset) = item.get_hookset(&data.state.data) {
|
|
img.hookset src=(hookset) width="20";
|
|
}
|
|
|
|
@if let Some(tug) = item.get_tug(&data.state.data) {
|
|
span.tug { (tug) }
|
|
}
|
|
}
|
|
}
|
|
}
|
|
@if let Some(hookset) = fish.entry.hookset {
|
|
span.catchpath {
|
|
img src=(hookset) width="35";
|
|
|
|
@if let Some(tug) = fish.entry.tug {
|
|
span.tug { (tug) }
|
|
}
|
|
}
|
|
|
|
}
|
|
}
|
|
.meta {
|
|
@if fish.entry.start_hour.is_some() && fish.entry.end_hour.is_some() {
|
|
div {
|
|
@if !fish.is_always_up {
|
|
(clock::display_eorzea_time(&clock::set_hm_from_float(&clock::get_current_eorzea_date(), fish.entry.start_hour.unwrap())))
|
|
" to "
|
|
(clock::display_eorzea_time(&clock::set_hm_from_float(&clock::get_current_eorzea_date(), fish.entry.end_hour.unwrap())))
|
|
} @else {
|
|
"always up!"
|
|
}
|
|
}
|
|
}
|
|
div { "Rarity: " (format!("{:.2}", fish.rarity * 100.)) "%" }
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
pub fn header(data: &ViewData) -> Markup {
|
|
html! {
|
|
.header {
|
|
div {}
|
|
.side {
|
|
.menu {
|
|
span { "Beacon " (env!("CARGO_PKG_VERSION")) }
|
|
a href="/changelog" { "Changelog" }
|
|
a href="https://git.insects.institute/insects/beacon" { "Source code" }
|
|
a href="/logout" { "Log out" }
|
|
}
|
|
details {
|
|
summary { "Filters" }
|
|
form {
|
|
fieldset {
|
|
@if data.filters.non_big_fish {
|
|
input name="big" id="big" type="checkbox" checked;
|
|
} @else {
|
|
input name="big" id="big" type="checkbox";
|
|
}
|
|
label for="big" { "Include non big fish?" }
|
|
}
|
|
|
|
fieldset {
|
|
@if data.filters.include_caught {
|
|
input name="caught" id="caught" type="checkbox" checked;
|
|
} @else {
|
|
input name="caught" id="caught" type="checkbox";
|
|
}
|
|
label for="caught" { "Include caught fish?" }
|
|
}
|
|
|
|
fieldset {
|
|
label for="patches" { "Filter by patch" }
|
|
br;
|
|
select name="patches" id="patches" multiple {
|
|
@for patch in data::PATCHES {
|
|
@if data.filters.patches.contains(&patch) {
|
|
option value=(patch) selected { (format!("{:.1}", patch)) }
|
|
} @else {
|
|
option value=(patch) { (format!("{:.1}", patch)) }
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
button type="submit" { "Apply" }
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
pub fn root() -> Markup {
|
|
layout(html! {
|
|
h1 { "Beacon" }
|
|
p {
|
|
"Beacon is a website to track progress for collecting " i { "big fish" } " in Final Fantasy XIV. "
|
|
"To use it, you need an " b { "account ID" } ", which serves to remember which fish you've caught. "
|
|
"If you have an account ID, you can enter it here, or if you don't, create a new one. Usually, your "
|
|
"browser will remember your ID and automatically redirect you, though."
|
|
}
|
|
|
|
form method="post" {
|
|
button type="submit" { "Generate a new account ID" }
|
|
}
|
|
|
|
p { "or" }
|
|
|
|
form method="get" action="/to" {
|
|
input name="id" type="text" placeholder="Enter your account ID...";
|
|
button type="submit" { "Go to tracker" }
|
|
}
|
|
|
|
h2 { "What is this about?" }
|
|
p {
|
|
"In Final Fantasy XIV, you can catch a number of elusive and rare fish called big fish. These can be quite tricky to even find -- most of them have a specific time window and only appear during certain weather patterns. This website helps with keeping track of all of that."
|
|
}
|
|
|
|
h2 { "Why not use the other tracker website?" }
|
|
p {
|
|
"By all means! " a href="https://ff14fish.carbuncleplushy.com" { "The original tracker" } " is a great resource, and functionally superior to this website. "
|
|
"I mostly created this because of two reasons:"
|
|
}
|
|
ol {
|
|
li { "Everything is rendered on the server - meaning it doesn't eat away at your system resources (good for laptop players!)" }
|
|
li { "Your caught fish are synchronized across devices, and data doesn't get deleted when you clear your browser data" }
|
|
}
|
|
p {
|
|
"These were the two main reasons why I created this. This website couldn't exist without Carbuncle Plushy's tracker -- it uses the same data "
|
|
"that has been painstakingly compiled by hand! If you can, please support them."
|
|
}
|
|
|
|
script src="/static/scripts/load.js" type="text/javascript" {}
|
|
})
|
|
}
|
|
|
|
pub fn new_account(id: String) -> Markup {
|
|
layout(html! {
|
|
p {
|
|
"Your new account ID has been created. Plase save it in a place you remember, like a password manager. If you lose it, you will be unable to "
|
|
"access your account."
|
|
}
|
|
|
|
h1 id="account-id" { (id) }
|
|
|
|
a href=(format!("/{}", id)) { "Click here to proceed to the tracker" }
|
|
|
|
script src="/static/scripts/save.js" type="text/javascript" {}
|
|
})
|
|
}
|