Compare commits
5 Commits
1393c993e9
...
79d8d899bb
Author | SHA1 | Date |
---|---|---|
|
79d8d899bb | |
|
83a4ca0ad5 | |
|
da3d3e4c8e | |
|
e158a095d3 | |
|
f3d31a7256 |
|
@ -71,6 +71,12 @@ dependencies = [
|
||||||
"libc",
|
"libc",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "anyhow"
|
||||||
|
version = "1.0.97"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "dcfed56ad506cb2c684a14971b8861fdc3baaaae314b9e5f9bb532cbe3ba7a4f"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "arrayvec"
|
name = "arrayvec"
|
||||||
version = "0.7.6"
|
version = "0.7.6"
|
||||||
|
@ -185,6 +191,24 @@ version = "0.1.4"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "383d29d513d8764dcdc42ea295d979eb99c3c9f00607b3692cf68a431f7dca72"
|
checksum = "383d29d513d8764dcdc42ea295d979eb99c3c9f00607b3692cf68a431f7dca72"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "bindgen"
|
||||||
|
version = "0.71.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "5f58bf3d7db68cfbac37cfc485a8d711e87e064c3d0fe0435b92f7a407f9d6b3"
|
||||||
|
dependencies = [
|
||||||
|
"bitflags 2.9.0",
|
||||||
|
"cexpr",
|
||||||
|
"clang-sys",
|
||||||
|
"itertools",
|
||||||
|
"proc-macro2",
|
||||||
|
"quote",
|
||||||
|
"regex",
|
||||||
|
"rustc-hash",
|
||||||
|
"shlex",
|
||||||
|
"syn 2.0.99",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "bitflags"
|
name = "bitflags"
|
||||||
version = "1.3.2"
|
version = "1.3.2"
|
||||||
|
@ -309,6 +333,15 @@ dependencies = [
|
||||||
"shlex",
|
"shlex",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "cexpr"
|
||||||
|
version = "0.6.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "6fac387a98bb7c37292057cffc56d62ecb629900026402633ae9160df93a8766"
|
||||||
|
dependencies = [
|
||||||
|
"nom",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "cfg-if"
|
name = "cfg-if"
|
||||||
version = "1.0.0"
|
version = "1.0.0"
|
||||||
|
@ -356,6 +389,17 @@ dependencies = [
|
||||||
"phf_codegen",
|
"phf_codegen",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "clang-sys"
|
||||||
|
version = "1.8.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "0b023947811758c97c59bf9d1c188fd619ad4718dcaa767947df1cadb14f39f4"
|
||||||
|
dependencies = [
|
||||||
|
"glob",
|
||||||
|
"libc",
|
||||||
|
"libloading",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "const-oid"
|
name = "const-oid"
|
||||||
version = "0.9.6"
|
version = "0.9.6"
|
||||||
|
@ -862,6 +906,28 @@ dependencies = [
|
||||||
"walkdir",
|
"walkdir",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "graphicsmagick"
|
||||||
|
version = "0.6.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "58c4ba437067d42af001dcda9d7188f368539b0731c2f5f4a6ba3765f3b64518"
|
||||||
|
dependencies = [
|
||||||
|
"graphicsmagick-sys",
|
||||||
|
"null-terminated-str",
|
||||||
|
"num_enum",
|
||||||
|
"thiserror 2.0.12",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "graphicsmagick-sys"
|
||||||
|
version = "0.6.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "9004437800b450090fdd1045edc43647209601b68320a4be450de541d0be16f6"
|
||||||
|
dependencies = [
|
||||||
|
"anyhow",
|
||||||
|
"bindgen",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "h2"
|
name = "h2"
|
||||||
version = "0.4.8"
|
version = "0.4.8"
|
||||||
|
@ -1373,6 +1439,15 @@ dependencies = [
|
||||||
"windows-sys 0.59.0",
|
"windows-sys 0.59.0",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "itertools"
|
||||||
|
version = "0.13.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "413ee7dfc52ee1a4949ceeb7dbc8a33f2d6c088194d9f922fb8318faf1f01186"
|
||||||
|
dependencies = [
|
||||||
|
"either",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "itoa"
|
name = "itoa"
|
||||||
version = "1.0.15"
|
version = "1.0.15"
|
||||||
|
@ -1424,6 +1499,16 @@ version = "0.2.170"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "875b3680cb2f8f71bdcf9a30f38d48282f5d3c95cbf9b3fa57269bb5d5c06828"
|
checksum = "875b3680cb2f8f71bdcf9a30f38d48282f5d3c95cbf9b3fa57269bb5d5c06828"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "libloading"
|
||||||
|
version = "0.8.6"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "fc2f4eb4bc735547cfed7c0a4922cbd04a4655978c09b54f1f7b228750664c34"
|
||||||
|
dependencies = [
|
||||||
|
"cfg-if",
|
||||||
|
"windows-targets 0.52.6",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "libm"
|
name = "libm"
|
||||||
version = "0.2.11"
|
version = "0.2.11"
|
||||||
|
@ -1648,6 +1733,12 @@ dependencies = [
|
||||||
"winapi",
|
"winapi",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "null-terminated-str"
|
||||||
|
version = "0.1.4"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "594ec098b589ef9bcf24ff9d2a1ed9295851ecb4009ce1df58557c239cf250bd"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "num-bigint-dig"
|
name = "num-bigint-dig"
|
||||||
version = "0.8.4"
|
version = "0.8.4"
|
||||||
|
@ -1711,6 +1802,27 @@ dependencies = [
|
||||||
"libc",
|
"libc",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "num_enum"
|
||||||
|
version = "0.7.3"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "4e613fc340b2220f734a8595782c551f1250e969d87d3be1ae0579e8d4065179"
|
||||||
|
dependencies = [
|
||||||
|
"num_enum_derive",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "num_enum_derive"
|
||||||
|
version = "0.7.3"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "af1844ef2428cc3e1cb900be36181049ef3d3193c63e43026cfe202983b27a56"
|
||||||
|
dependencies = [
|
||||||
|
"proc-macro-crate",
|
||||||
|
"proc-macro2",
|
||||||
|
"quote",
|
||||||
|
"syn 2.0.99",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "object"
|
name = "object"
|
||||||
version = "0.36.7"
|
version = "0.36.7"
|
||||||
|
@ -2090,6 +2202,7 @@ name = "receipts"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"chrono",
|
"chrono",
|
||||||
|
"graphicsmagick",
|
||||||
"reqwest",
|
"reqwest",
|
||||||
"rocket",
|
"rocket",
|
||||||
"rocket_db_pools",
|
"rocket_db_pools",
|
||||||
|
@ -2430,6 +2543,12 @@ version = "0.1.24"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "719b953e2095829ee67db738b3bfa9fa368c94900df327b3f07fe6e794d2fe1f"
|
checksum = "719b953e2095829ee67db738b3bfa9fa368c94900df327b3f07fe6e794d2fe1f"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "rustc-hash"
|
||||||
|
version = "2.1.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "357703d41365b4b27c590e3ed91eabb1b663f07c4c084095e60cbed4362dff0d"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "rustix"
|
name = "rustix"
|
||||||
version = "1.0.1"
|
version = "1.0.1"
|
||||||
|
|
|
@ -11,6 +11,7 @@ keywords = ["personal-finance", "receipts"]
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
chrono = { version = "0.4.40", default-features = false, features = ["serde"] }
|
chrono = { version = "0.4.40", default-features = false, features = ["serde"] }
|
||||||
|
graphicsmagick = { version = "0.6.1", features = ["v1_3_38"] }
|
||||||
reqwest = { version = "0.12.12", features = ["json"] }
|
reqwest = { version = "0.12.12", features = ["json"] }
|
||||||
rocket = { version = "0.5.1", default-features = false, features = ["json"] }
|
rocket = { version = "0.5.1", default-features = false, features = ["json"] }
|
||||||
rocket_db_pools = { version = "0.2.0", features = ["sqlx_macros", "sqlx_postgres"] }
|
rocket_db_pools = { version = "0.2.0", features = ["sqlx_macros", "sqlx_postgres"] }
|
||||||
|
|
|
@ -4,7 +4,9 @@ RUN --mount=type=cache,target=/var/cache \
|
||||||
microdnf install -y \
|
microdnf install -y \
|
||||||
--setopt persistdir=/var/cache/dnf \
|
--setopt persistdir=/var/cache/dnf \
|
||||||
--setopt install_weak_deps=0 \
|
--setopt install_weak_deps=0 \
|
||||||
|
GraphicsMagick-devel \
|
||||||
cargo \
|
cargo \
|
||||||
|
clang-devel \
|
||||||
openssl-devel \
|
openssl-devel \
|
||||||
&& :
|
&& :
|
||||||
|
|
||||||
|
@ -38,6 +40,15 @@ RUN --mount=type=cache,target=/root/.cargo \
|
||||||
|
|
||||||
FROM git.pyrocufflink.net/containerimages/dch-base
|
FROM git.pyrocufflink.net/containerimages/dch-base
|
||||||
|
|
||||||
|
RUN --mount=type=cache,target=/var/cache \
|
||||||
|
microdnf install -y \
|
||||||
|
--setopt persistdir=/var/cache/dnf \
|
||||||
|
--setopt install_weak_deps=0 \
|
||||||
|
GraphicsMagick \
|
||||||
|
clang-libs \
|
||||||
|
ghostscript \
|
||||||
|
&& :
|
||||||
|
|
||||||
COPY --from=build /build/target/release/receipts /usr/local/bin
|
COPY --from=build /build/target/release/receipts /usr/local/bin
|
||||||
|
|
||||||
COPY --from=esbuild /build/dist /usr/local/share/receipts/static
|
COPY --from=esbuild /build/dist /usr/local/share/receipts/static
|
||||||
|
|
|
@ -0,0 +1,46 @@
|
||||||
|
use graphicsmagick::types::FilterTypes;
|
||||||
|
use graphicsmagick::wand::MagickWand;
|
||||||
|
|
||||||
|
pub fn thumbnail(
|
||||||
|
image: &[u8],
|
||||||
|
) -> Result<Option<Vec<u8>>, graphicsmagick::Error> {
|
||||||
|
let mut wand = MagickWand::new();
|
||||||
|
wand.read_image_blob(image)?;
|
||||||
|
|
||||||
|
// Multi-page documents like PDFs become multiple images in the
|
||||||
|
// MagickWand. We want a thumbnail of the first page, so we have
|
||||||
|
// to reset the iterator back to the first image.
|
||||||
|
wand.reset_iterator();
|
||||||
|
|
||||||
|
let orig_height = wand.get_image_height() as f64;
|
||||||
|
let orig_width = wand.get_image_width() as f64;
|
||||||
|
let min_width = 300.0;
|
||||||
|
let min_height = 450.0;
|
||||||
|
let scale_w = min_width / orig_width;
|
||||||
|
let scale_h = min_height / orig_height;
|
||||||
|
let scale = scale_w.max(scale_h);
|
||||||
|
let new_width = (orig_width * scale).round() as u64;
|
||||||
|
let new_height = (orig_height * scale).round() as u64;
|
||||||
|
|
||||||
|
wand.resize_image(
|
||||||
|
new_width,
|
||||||
|
new_height,
|
||||||
|
FilterTypes::UndefinedFilter,
|
||||||
|
1.0,
|
||||||
|
)?;
|
||||||
|
Ok(wand
|
||||||
|
.set_image_format("WEBP")?
|
||||||
|
.write_image_blob()
|
||||||
|
.map(|a| a.to_vec()))
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn get_type(image: &[u8]) -> Option<String> {
|
||||||
|
Some(
|
||||||
|
MagickWand::new()
|
||||||
|
.read_image_blob(image)
|
||||||
|
.ok()?
|
||||||
|
.get_image_format()
|
||||||
|
.to_str_lossy()
|
||||||
|
.to_string(),
|
||||||
|
)
|
||||||
|
}
|
|
@ -1,5 +1,6 @@
|
||||||
mod config;
|
mod config;
|
||||||
mod firefly;
|
mod firefly;
|
||||||
|
mod imaging;
|
||||||
mod receipts;
|
mod receipts;
|
||||||
mod routes;
|
mod routes;
|
||||||
mod transactions;
|
mod transactions;
|
||||||
|
@ -89,6 +90,8 @@ async fn rocket() -> _ {
|
||||||
.with_writer(std::io::stderr)
|
.with_writer(std::io::stderr)
|
||||||
.init();
|
.init();
|
||||||
|
|
||||||
|
graphicsmagick::initialize();
|
||||||
|
|
||||||
let rocket = rocket::build();
|
let rocket = rocket::build();
|
||||||
let figment = rocket.figment();
|
let figment = rocket.figment();
|
||||||
|
|
||||||
|
|
|
@ -6,6 +6,7 @@ use serde::Serialize;
|
||||||
use sqlx::types::Decimal;
|
use sqlx::types::Decimal;
|
||||||
use tracing::error;
|
use tracing::error;
|
||||||
|
|
||||||
|
use crate::imaging;
|
||||||
use crate::Database;
|
use crate::Database;
|
||||||
|
|
||||||
#[derive(Debug, Serialize)]
|
#[derive(Debug, Serialize)]
|
||||||
|
@ -56,6 +57,8 @@ pub enum ReceiptPostFormError {
|
||||||
Amount(#[from] rust_decimal::Error),
|
Amount(#[from] rust_decimal::Error),
|
||||||
#[error("Error reading photo: {0}")]
|
#[error("Error reading photo: {0}")]
|
||||||
Photo(#[from] std::io::Error),
|
Photo(#[from] std::io::Error),
|
||||||
|
#[error("Unsupported image type")]
|
||||||
|
UnsupportedImageFormat,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Serialize)]
|
#[derive(Serialize)]
|
||||||
|
@ -84,16 +87,31 @@ impl ReceiptPostData {
|
||||||
use rust_decimal::prelude::FromStr;
|
use rust_decimal::prelude::FromStr;
|
||||||
let amount = Decimal::from_str(&form.amount)?;
|
let amount = Decimal::from_str(&form.amount)?;
|
||||||
let notes = form.notes.clone();
|
let notes = form.notes.clone();
|
||||||
let filename = form
|
|
||||||
.photo
|
|
||||||
.raw_name()
|
|
||||||
.map(|n| n.dangerous_unsafe_unsanitized_raw().as_str())
|
|
||||||
.unwrap_or("photo.jpg")
|
|
||||||
.into();
|
|
||||||
let stream = form.photo.open().await?;
|
let stream = form.photo.open().await?;
|
||||||
let mut reader = BufReader::new(stream);
|
let mut reader = BufReader::new(stream);
|
||||||
let mut photo = Vec::new();
|
let mut photo = Vec::new();
|
||||||
reader.read_to_end(&mut photo).await?;
|
reader.read_to_end(&mut photo).await?;
|
||||||
|
let extension = match imaging::get_type(&photo).as_deref() {
|
||||||
|
Some("BMP") => "bmp",
|
||||||
|
Some("JPEG") => "jpg",
|
||||||
|
Some("PDF") => "pdf",
|
||||||
|
Some("PNG") => "png",
|
||||||
|
Some("PPM") => "ppm",
|
||||||
|
Some("TIFF") => "tiff",
|
||||||
|
Some("WEBP") => "webp",
|
||||||
|
Some(f) => {
|
||||||
|
error!("Unsupported image format: {}", f);
|
||||||
|
return Err(ReceiptPostFormError::UnsupportedImageFormat);
|
||||||
|
},
|
||||||
|
None => {
|
||||||
|
return Err(ReceiptPostFormError::UnsupportedImageFormat);
|
||||||
|
},
|
||||||
|
};
|
||||||
|
let filename = form
|
||||||
|
.photo
|
||||||
|
.name()
|
||||||
|
.map(|n| format!("{}.{}", n, extension))
|
||||||
|
.unwrap_or("photo.jpg".into());
|
||||||
Ok(Self {
|
Ok(Self {
|
||||||
date,
|
date,
|
||||||
vendor,
|
vendor,
|
||||||
|
@ -108,6 +126,7 @@ impl ReceiptPostData {
|
||||||
pub struct ReceiptsRepository {
|
pub struct ReceiptsRepository {
|
||||||
conn: Connection<Database>,
|
conn: Connection<Database>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl ReceiptsRepository {
|
impl ReceiptsRepository {
|
||||||
pub fn new(conn: Connection<Database>) -> Self {
|
pub fn new(conn: Connection<Database>) -> Self {
|
||||||
Self { conn }
|
Self { conn }
|
||||||
|
|
|
@ -8,6 +8,7 @@ use rust_decimal::prelude::ToPrimitive;
|
||||||
use tracing::{debug, error, info};
|
use tracing::{debug, error, info};
|
||||||
|
|
||||||
use crate::firefly::{TransactionStore, TransactionUpdate};
|
use crate::firefly::{TransactionStore, TransactionUpdate};
|
||||||
|
use crate::imaging;
|
||||||
use crate::receipts::*;
|
use crate::receipts::*;
|
||||||
use crate::{Context, Database};
|
use crate::{Context, Database};
|
||||||
|
|
||||||
|
@ -113,7 +114,11 @@ pub async fn add_receipt(
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
Some(ref id) => match ctx.firefly.get_transaction(id).await {
|
Some(ref id) if !id.is_empty() => match ctx
|
||||||
|
.firefly
|
||||||
|
.get_transaction(id)
|
||||||
|
.await
|
||||||
|
{
|
||||||
Ok(t) => {
|
Ok(t) => {
|
||||||
let mut needs_update = false;
|
let mut needs_update = false;
|
||||||
let mut update = TransactionUpdate::from(t.data.clone());
|
let mut update = TransactionUpdate::from(t.data.clone());
|
||||||
|
@ -128,8 +133,12 @@ pub async fn add_receipt(
|
||||||
needs_update = true;
|
needs_update = true;
|
||||||
}
|
}
|
||||||
if !data.notes.is_empty() {
|
if !data.notes.is_empty() {
|
||||||
split.notes = Some(data.notes.clone());
|
if let Some(notes) = split.notes.as_deref() {
|
||||||
needs_update = true;
|
if notes != data.notes.as_str() {
|
||||||
|
split.notes = Some(data.notes.clone());
|
||||||
|
needs_update = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
debug!("Transaction {} has no splits", id);
|
debug!("Transaction {} has no splits", id);
|
||||||
|
@ -162,6 +171,7 @@ pub async fn add_receipt(
|
||||||
None
|
None
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
Some(_) => None,
|
||||||
None => None,
|
None => None,
|
||||||
};
|
};
|
||||||
if let Some(xact) = xact {
|
if let Some(xact) = xact {
|
||||||
|
@ -218,6 +228,31 @@ impl PhotoResponse {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[rocket::get("/<id>/thumbnail/<_>")]
|
||||||
|
pub async fn view_receipt_thumbnail(
|
||||||
|
id: i32,
|
||||||
|
db: DatabaseConnection<Database>,
|
||||||
|
) -> Option<PhotoResponse> {
|
||||||
|
let mut repo = ReceiptsRepository::new(db);
|
||||||
|
match repo.get_receipt_photo(id).await {
|
||||||
|
Ok((_, image)) => {
|
||||||
|
let thumbnail = match imaging::thumbnail(&image) {
|
||||||
|
Ok(Some(t)) => t,
|
||||||
|
Ok(None) => return None,
|
||||||
|
Err(e) => {
|
||||||
|
error!("Failed to create receipt photo thumbnail: {}", e);
|
||||||
|
return None;
|
||||||
|
},
|
||||||
|
};
|
||||||
|
Some(PhotoResponse::new(thumbnail, ContentType(MediaType::WEBP)))
|
||||||
|
},
|
||||||
|
Err(e) => {
|
||||||
|
error!("Error fetching receipt image: {}", e);
|
||||||
|
None
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#[rocket::get("/<id>/view/<_>")]
|
#[rocket::get("/<id>/view/<_>")]
|
||||||
pub async fn view_receipt_photo(
|
pub async fn view_receipt_photo(
|
||||||
id: i32,
|
id: i32,
|
||||||
|
@ -264,6 +299,7 @@ pub fn routes() -> Vec<Route> {
|
||||||
get_receipt,
|
get_receipt,
|
||||||
list_receipts,
|
list_receipts,
|
||||||
receipt_form,
|
receipt_form,
|
||||||
|
view_receipt_thumbnail,
|
||||||
view_receipt_photo,
|
view_receipt_photo,
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|
|
@ -54,7 +54,12 @@
|
||||||
<img id="upload-preview" style="max-height: 400px" />
|
<img id="upload-preview" style="max-height: 400px" />
|
||||||
</p>
|
</p>
|
||||||
<p>
|
<p>
|
||||||
<input name="photo" type="file" style="display: none" />
|
<input
|
||||||
|
name="photo"
|
||||||
|
type="file"
|
||||||
|
style="display: none"
|
||||||
|
accept="image/*,application/pdf,.avif"
|
||||||
|
/>
|
||||||
<sl-button size="large" variant="primary" class="choose-file">
|
<sl-button size="large" variant="primary" class="choose-file">
|
||||||
<sl-icon slot="prefix" name="image" label="Choose File"></sl-icon>
|
<sl-icon slot="prefix" name="image" label="Choose File"></sl-icon>
|
||||||
Choose File
|
Choose File
|
||||||
|
|
|
@ -94,7 +94,7 @@
|
||||||
<sl-card class="receipt-card" data-receipt-id="{{ receipt.id }}">
|
<sl-card class="receipt-card" data-receipt-id="{{ receipt.id }}">
|
||||||
<a slot="image" href="/receipts/{{ receipt.id }}">
|
<a slot="image" href="/receipts/{{ receipt.id }}">
|
||||||
<img
|
<img
|
||||||
src="/receipts/{{ receipt.id }}/view/{{ receipt.filename }}"
|
src="/receipts/{{ receipt.id }}/thumbnail/{{ receipt.filename }}"
|
||||||
alt="Receipt {{ receipt.id }}"
|
alt="Receipt {{ receipt.id }}"
|
||||||
/>
|
/>
|
||||||
</a>
|
</a>
|
||||||
|
|
|
@ -1,6 +1,19 @@
|
||||||
{% extends "base" %} {% block head %}
|
{% extends "base" %} {% block head %}
|
||||||
<title>Receipt: ${{ amount }} at {{ vendor }}</title>
|
<title>Receipt: ${{ amount }} at {{ vendor }}</title>
|
||||||
<style>
|
<style>
|
||||||
|
.photo {
|
||||||
|
max-width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.photo img {
|
||||||
|
max-width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.photo iframe {
|
||||||
|
width: 100%;
|
||||||
|
aspect-ratio: 8.5 / 11;
|
||||||
|
}
|
||||||
|
|
||||||
@media screen and (min-width: 900px) {
|
@media screen and (min-width: 900px) {
|
||||||
article {
|
article {
|
||||||
display: flex;
|
display: flex;
|
||||||
|
@ -11,8 +24,8 @@
|
||||||
margin-right: 1em;
|
margin-right: 1em;
|
||||||
}
|
}
|
||||||
|
|
||||||
.photo img {
|
.photo {
|
||||||
max-width: 100%;
|
flex-grow: 1;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
@ -52,7 +65,11 @@
|
||||||
</div>
|
</div>
|
||||||
<div class="photo">
|
<div class="photo">
|
||||||
<p>
|
<p>
|
||||||
|
{% if filename is ending_with(".pdf") %}
|
||||||
|
<iframe src="/receipts/{{ id }}/view/{{ filename }}"></iframe>
|
||||||
|
{% else %}
|
||||||
<img src="/receipts/{{ id }}/view/{{ filename }}" />
|
<img src="/receipts/{{ id }}/view/{{ filename }}" />
|
||||||
|
{% endif %}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</article>
|
</article>
|
||||||
|
|
Loading…
Reference in New Issue