blandat.. fixar o nullcheckar. filtreringar
This commit is contained in:
		@@ -5,7 +5,7 @@ import { Card } from "solid-bootstrap";
 | 
			
		||||
const BookCard: Component<{ book: book }> = (props: { book: book }) => {
 | 
			
		||||
	return (
 | 
			
		||||
		<Card class="book-card col-lg-2 col-md-3 col-sm-4">
 | 
			
		||||
			<Show when={props.book.hasCover}>
 | 
			
		||||
			<Show when={true || props.book.hasCover}>
 | 
			
		||||
				<Card.Img
 | 
			
		||||
					variant="top"
 | 
			
		||||
					class="padding-1"
 | 
			
		||||
 
 | 
			
		||||
@@ -1,11 +1,12 @@
 | 
			
		||||
import { createSignal, onMount, For, type Component } from "solid-js";
 | 
			
		||||
import AuthorCard from "./AuthorCard";
 | 
			
		||||
import { createSignal, onMount, For, type Component, Show } from "solid-js";
 | 
			
		||||
import BookCard from "./BookCard";
 | 
			
		||||
import BibblanService from "../services/bibblanservice";
 | 
			
		||||
import { series, listBook } from "../types/types";
 | 
			
		||||
 | 
			
		||||
import { series, listBook, book } from "../types/types";
 | 
			
		||||
import { OverlayTrigger, Popover, Button } from "solid-bootstrap";
 | 
			
		||||
const SeriesList: Component = () => {
 | 
			
		||||
	const [series, setSeries] = createSignal<series[]>([]);
 | 
			
		||||
	const [query, setQuery] = createSignal("");
 | 
			
		||||
	const [selected, setSelected] = createSignal<number>(-1);
 | 
			
		||||
 | 
			
		||||
	const update = (query: string) => {
 | 
			
		||||
		setQuery(query);
 | 
			
		||||
@@ -40,6 +41,30 @@ const SeriesList: Component = () => {
 | 
			
		||||
		return d?.toISOString().substr(0, 10) ?? "";
 | 
			
		||||
	};
 | 
			
		||||
 | 
			
		||||
	function extendBook(item: listBook, seriesName: string): book {
 | 
			
		||||
		return {
 | 
			
		||||
			...item,
 | 
			
		||||
 | 
			
		||||
			author: item.authorName,
 | 
			
		||||
			comments: "",
 | 
			
		||||
 | 
			
		||||
			language: "",
 | 
			
		||||
			path: item.path,
 | 
			
		||||
			hasCover: item.hasCover,
 | 
			
		||||
			formats: [],
 | 
			
		||||
			seriesName,
 | 
			
		||||
			seriesNumber: item.seriesIndex,
 | 
			
		||||
		};
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	function orderBySeriesIndex(books: listBook[]): listBook[] {
 | 
			
		||||
		return [...books].sort((a, b) => {
 | 
			
		||||
			const indexA = a.seriesIndex ?? 0;
 | 
			
		||||
			const indexB = b.seriesIndex ?? 0;
 | 
			
		||||
			return indexA - indexB;
 | 
			
		||||
		});
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	return (
 | 
			
		||||
		<div>
 | 
			
		||||
			<h1>Series!</h1>
 | 
			
		||||
@@ -49,7 +74,7 @@ const SeriesList: Component = () => {
 | 
			
		||||
				placeholder="Search..."
 | 
			
		||||
				onInput={(e) => update(e.currentTarget.value)}
 | 
			
		||||
			/>
 | 
			
		||||
			<table class="table p-3">
 | 
			
		||||
			<table class="table p-3 series-table">
 | 
			
		||||
				<thead>
 | 
			
		||||
					<tr>
 | 
			
		||||
						<th>ID</th>
 | 
			
		||||
@@ -57,27 +82,53 @@ const SeriesList: Component = () => {
 | 
			
		||||
						<th>Author</th>
 | 
			
		||||
						<th>Book Count</th>
 | 
			
		||||
						<th>Published</th>
 | 
			
		||||
						<th></th>
 | 
			
		||||
					</tr>
 | 
			
		||||
				</thead>
 | 
			
		||||
				<tbody>
 | 
			
		||||
					<For each={series()}>
 | 
			
		||||
						{(item) => (
 | 
			
		||||
							<tr>
 | 
			
		||||
								<td>{item.id}</td>
 | 
			
		||||
								<td>{item.name}</td>
 | 
			
		||||
								<td>
 | 
			
		||||
									<For each={distinctAuthors(item.books)}>
 | 
			
		||||
										{(author) => (
 | 
			
		||||
											<a href={`/books/author/${author.id}`}>{author.name}</a>
 | 
			
		||||
										)}
 | 
			
		||||
									</For>
 | 
			
		||||
								</td>
 | 
			
		||||
								<td>{item.books.length}</td>
 | 
			
		||||
								<td>
 | 
			
		||||
									{minDate(item.books) ?? "N/A"} -{" "}
 | 
			
		||||
									{maxDate(item.books) ?? "N/A"}
 | 
			
		||||
								</td>
 | 
			
		||||
							</tr>
 | 
			
		||||
							<>
 | 
			
		||||
								<tr>
 | 
			
		||||
									<td>{item.id}</td>
 | 
			
		||||
									<td>{item.name}</td>
 | 
			
		||||
									<td>
 | 
			
		||||
										<For each={distinctAuthors(item.books)}>
 | 
			
		||||
											{(author) => (
 | 
			
		||||
												<a href={`/books/author/${author.id}`}>{author.name}</a>
 | 
			
		||||
											)}
 | 
			
		||||
										</For>
 | 
			
		||||
									</td>
 | 
			
		||||
									<td>{item.books.length}</td>
 | 
			
		||||
									<td>
 | 
			
		||||
										{minDate(item.books) ?? "N/A"} -{" "}
 | 
			
		||||
										{maxDate(item.books) ?? "N/A"}
 | 
			
		||||
									</td>
 | 
			
		||||
									<td>
 | 
			
		||||
										<OverlayTrigger
 | 
			
		||||
											trigger="click"
 | 
			
		||||
											offset={[0, 8]}
 | 
			
		||||
											placement="left"
 | 
			
		||||
											overlay={
 | 
			
		||||
												<Popover id="popover-series" class="shadow">
 | 
			
		||||
													<Popover.Header as="h3">{item.name}</Popover.Header>
 | 
			
		||||
													<Popover.Body>
 | 
			
		||||
														<div class="book-grid d-flex flex-wrap justify-content-between gap-3 p-3">
 | 
			
		||||
															<For each={orderBySeriesIndex(item.books)}>
 | 
			
		||||
																{(b) => (
 | 
			
		||||
																	<BookCard book={extendBook(b, item.name)} />
 | 
			
		||||
																)}
 | 
			
		||||
															</For>
 | 
			
		||||
														</div>
 | 
			
		||||
													</Popover.Body>
 | 
			
		||||
												</Popover>
 | 
			
		||||
											}
 | 
			
		||||
										>
 | 
			
		||||
											<Button variant="secondary">Books</Button>
 | 
			
		||||
										</OverlayTrigger>
 | 
			
		||||
									</td>
 | 
			
		||||
								</tr>
 | 
			
		||||
							</>
 | 
			
		||||
						)}
 | 
			
		||||
					</For>
 | 
			
		||||
				</tbody>
 | 
			
		||||
 
 | 
			
		||||
@@ -6,3 +6,7 @@
 | 
			
		||||
		aspect-ratio: 3 / 4;
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
#popover-series {
 | 
			
		||||
	--bs-popover-max-width: 80%;
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -23,6 +23,8 @@ interface listBook {
 | 
			
		||||
	title: string;
 | 
			
		||||
	authorId: number;
 | 
			
		||||
	authorName: string;
 | 
			
		||||
	path: string;
 | 
			
		||||
	hasCover: boolean;
 | 
			
		||||
	pubDate: Date;
 | 
			
		||||
	seriesIndex: number;
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -1,5 +1,6 @@
 | 
			
		||||
using Bibblan.Models;
 | 
			
		||||
using Bibblan.ViewModels;
 | 
			
		||||
using Microsoft.AspNetCore.Http.HttpResults;
 | 
			
		||||
using Microsoft.Extensions.Options;
 | 
			
		||||
 | 
			
		||||
namespace Bibblan.Business.Services
 | 
			
		||||
@@ -17,6 +18,9 @@ namespace Bibblan.Business.Services
 | 
			
		||||
        public byte[] Cover(string path)
 | 
			
		||||
        {
 | 
			
		||||
            var fullPath = Path.Combine(_options.CalibreRoot, path, "cover.jpg");
 | 
			
		||||
            if (!File.Exists(fullPath))
 | 
			
		||||
                return null;
 | 
			
		||||
 | 
			
		||||
            return File.ReadAllBytes(fullPath);
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -13,18 +13,16 @@ namespace Bibblan.Business.Services
 | 
			
		||||
        public string? Query;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public class DatabaseService
 | 
			
		||||
    public class DatabaseService(IOptions<BibblanOptions> options)
 | 
			
		||||
    {
 | 
			
		||||
        BibblanOptions settings;
 | 
			
		||||
        public DatabaseService(IOptions<BibblanOptions> options) {
 | 
			
		||||
            settings = options.Value;
 | 
			
		||||
        }
 | 
			
		||||
        readonly BibblanOptions settings = options.Value;
 | 
			
		||||
 | 
			
		||||
        public IEnumerable<Books> GetBooks(int count, BookFilter filter = null)
 | 
			
		||||
        {
 | 
			
		||||
            var conn = new NpgsqlConnection(settings.BibblanConnection);
 | 
			
		||||
            var query = "select * from books";
 | 
			
		||||
            if(filter != null)
 | 
			
		||||
            object parameters = null;
 | 
			
		||||
            if (filter != null)
 | 
			
		||||
            {
 | 
			
		||||
                query += " where ";
 | 
			
		||||
                if(filter.Author != null)
 | 
			
		||||
@@ -33,46 +31,48 @@ namespace Bibblan.Business.Services
 | 
			
		||||
                } else if(!String.IsNullOrWhiteSpace(query))
 | 
			
		||||
                {
 | 
			
		||||
                    filter.Query = filter.Query.ToLowerInvariant();
 | 
			
		||||
                    query += $"lower(title) like '%{filter.Query}%' or lower(author_sort) like '%{filter.Query}%'";
 | 
			
		||||
                    query += $"lower(title) like @query or lower(author_sort) like @query";
 | 
			
		||||
                    parameters = new { query = "%" + filter.Query + "%" };
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
            return conn.Query<Books>(query).Take(count).ToList();
 | 
			
		||||
            return conn.Query<Books>(query, parameters).Take(count).ToList();
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        public IEnumerable<AuthorVm> GetAuthors(int count, BookFilter filter = null)
 | 
			
		||||
        {
 | 
			
		||||
            var query = "select a.id, a.name, count(bal.*) as bookcount\r\nfrom authors a\r\nleft join books_authors_link bal on a.id  = bal.author\r\ngroup by a.id , a.name ";
 | 
			
		||||
            if(!String.IsNullOrWhiteSpace(filter?.Query))
 | 
			
		||||
            object parameters = null;
 | 
			
		||||
            if (!String.IsNullOrWhiteSpace(filter?.Query))
 | 
			
		||||
            {
 | 
			
		||||
                filter.Query = filter.Query.ToLowerInvariant();
 | 
			
		||||
                query += $" having lower(a.name) like '%{filter.Query}%'";
 | 
			
		||||
                query += $" having lower(a.name) like @query";
 | 
			
		||||
                parameters = new { query = "%" + filter.Query + "%" };
 | 
			
		||||
            }
 | 
			
		||||
            var conn = new NpgsqlConnection(settings.BibblanConnection);
 | 
			
		||||
            return conn.Query<AuthorVm>(query).Take(count).ToList();
 | 
			
		||||
            return conn.Query<AuthorVm>(query, parameters).Take(count).ToList();
 | 
			
		||||
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        internal List<SeriesVm> GetSeries(int count, BookFilter? filter)
 | 
			
		||||
        {
 | 
			
		||||
            var query = @"select s.id as seriesid, s.name as seriesname, b.*, a.* 
 | 
			
		||||
                --b.id as bookid, b.title, b.pubdate, b.series_index, 
 | 
			
		||||
                --a.id as authorid, a.name as authorname 
 | 
			
		||||
                from series s
 | 
			
		||||
                inner join books_series_link bsl on s.id = bsl.series
 | 
			
		||||
                inner join books b on bsl.book = b.id
 | 
			
		||||
                inner join books_authors_link bal on b.id = bal.book 
 | 
			
		||||
                inner join authors a on bal.author = a.id";
 | 
			
		||||
            object parameters = null;
 | 
			
		||||
            if (!String.IsNullOrWhiteSpace(filter?.Query))
 | 
			
		||||
            {
 | 
			
		||||
                filter.Query = filter.Query?.ToLowerInvariant().Trim() ?? "";
 | 
			
		||||
                query += $" where lower(s.name) like '%{filter.Query}%'";
 | 
			
		||||
                query += $" where lower(s.name) like @query";
 | 
			
		||||
                parameters = new { query = "%" + filter.Query + "%" };
 | 
			
		||||
            }
 | 
			
		||||
            var conn = new NpgsqlConnection(settings.BibblanConnection);
 | 
			
		||||
            var lookup = new Dictionary<long, SeriesVm>();
 | 
			
		||||
            conn.Query<SeriesVm, Books, Authors, SeriesVm>(query, (s,b,a) =>
 | 
			
		||||
            {
 | 
			
		||||
                SeriesVm svm;
 | 
			
		||||
                if (!lookup.TryGetValue(s.Id, out svm))
 | 
			
		||||
                if (!lookup.TryGetValue(s.Id, out SeriesVm svm))
 | 
			
		||||
                {
 | 
			
		||||
                    lookup.Add(s.Id, svm = s);
 | 
			
		||||
                }
 | 
			
		||||
@@ -82,13 +82,14 @@ namespace Bibblan.Business.Services
 | 
			
		||||
                    AuthorName = a.Name,
 | 
			
		||||
                    Id = b.Id,
 | 
			
		||||
                    PubDate = b.Pubdate,
 | 
			
		||||
                    HasCover = b.HasCover,
 | 
			
		||||
                    Path = b.Path,
 | 
			
		||||
                    SeriesIndex = b.SeriesIndex,
 | 
			
		||||
                    Title = b.Title
 | 
			
		||||
                    
 | 
			
		||||
                });
 | 
			
		||||
                return svm;
 | 
			
		||||
            }, splitOn: "id").Take(count).ToList();
 | 
			
		||||
            return lookup.Values.AsList();
 | 
			
		||||
            }, splitOn: "id", param: parameters);
 | 
			
		||||
            return lookup.Values.Take(count).ToList();
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        internal List<Tag> GetTags(BookFilter? filter)
 | 
			
		||||
@@ -97,13 +98,15 @@ namespace Bibblan.Business.Services
 | 
			
		||||
                inner join books_tags_link btl on t.id = btl.tag 
 | 
			
		||||
                group by t.id, t.name
 | 
			
		||||
                order by bookcount desc";
 | 
			
		||||
            object parameters = null;
 | 
			
		||||
            if (!String.IsNullOrWhiteSpace(filter?.Query))
 | 
			
		||||
            {
 | 
			
		||||
                filter.Query = filter.Query.ToLowerInvariant();
 | 
			
		||||
                query += $" having lower(name) like '%{filter.Query}%'";
 | 
			
		||||
                query += $" having lower(name) like @query";
 | 
			
		||||
                parameters = new { query = "%" + filter.Query + "%" };
 | 
			
		||||
            }
 | 
			
		||||
            var conn = new NpgsqlConnection(settings.BibblanConnection);
 | 
			
		||||
            return conn.Query<Tag>(query).ToList();
 | 
			
		||||
            return conn.Query<Tag>(query,parameters).ToList();
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -21,6 +21,9 @@ namespace Bibblan.Controllers
 | 
			
		||||
        {
 | 
			
		||||
            //TODO: Bör kanske inte gå direkt mot calibres filer.. 
 | 
			
		||||
            var bytes = _calibre.Cover(path);
 | 
			
		||||
            if (bytes == null)
 | 
			
		||||
                return NotFound();
 | 
			
		||||
 | 
			
		||||
            return File(bytes, "image/jpeg", true);
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -31,6 +31,8 @@ namespace Bibblan.Controllers
 | 
			
		||||
        public IActionResult GetCover(string path)
 | 
			
		||||
        {
 | 
			
		||||
            var bytes = _service.Cover(path);
 | 
			
		||||
            if (bytes == null)
 | 
			
		||||
                return NotFound();
 | 
			
		||||
            return File(bytes, "image/jpeg", true);
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -17,6 +17,8 @@ namespace Bibblan.ViewModels
 | 
			
		||||
        public double SeriesIndex { get; set; }
 | 
			
		||||
        public long AuthorId { get; set; }
 | 
			
		||||
        public string AuthorName { get; set; }
 | 
			
		||||
        public bool HasCover { get; set; }
 | 
			
		||||
        public string Path { get; set; }
 | 
			
		||||
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
		Reference in New Issue
	
	Block a user