diff --git a/config/apache/standardebooks.org.conf b/config/apache/standardebooks.org.conf index f9885203..d5e2cb1e 100644 --- a/config/apache/standardebooks.org.conf +++ b/config/apache/standardebooks.org.conf @@ -219,7 +219,7 @@ Define domain standardebooks.org RewriteRule ^/images/covers/(.+?)\-[a-z0-9]{8}\-(cover|hero)(@2x)?\.(jpg|avif)$ /images/covers/$1-$2$3.$4 RewriteRule ^/ebooks/([^\./]+?)$ /ebooks/author.php?url-path=$1 [QSA] - RewriteRule ^/tags/([^\./]+?)$ /ebooks/index.php?tag=$1 [QSA] + RewriteRule ^/tags/([^\./]+?)$ /ebooks/index.php?tags[]=$1 [QSA] RewriteRule ^/collections/([^\./]+?)$ /ebooks/index.php?collection=$1 [QSA] # Prevent this rule from firing if we're getting a distribution file diff --git a/config/apache/standardebooks.test.conf b/config/apache/standardebooks.test.conf index ae4daa5d..ff07bfd7 100644 --- a/config/apache/standardebooks.test.conf +++ b/config/apache/standardebooks.test.conf @@ -218,7 +218,7 @@ Define domain standardebooks.test RewriteRule ^/images/covers/(.+?)\-[a-z0-9]{8}\-(cover|hero)(@2x)?\.(jpg|avif)$ /images/covers/$1-$2$3.$4 RewriteRule ^/ebooks/([^\./]+?)$ /ebooks/author.php?url-path=$1 [QSA] - RewriteRule ^/tags/([^\./]+?)$ /ebooks/index.php?tag=$1 [QSA] + RewriteRule ^/tags/([^\./]+?)$ /ebooks/index.php?tags[]=$1 [QSA] RewriteRule ^/collections/([^\./]+?)$ /ebooks/index.php?collection=$1 [QSA] # Prevent this rule from firing if we're getting a distribution file diff --git a/lib/Constants.php b/lib/Constants.php index 0b2c58eb..f4aa2c3b 100644 --- a/lib/Constants.php +++ b/lib/Constants.php @@ -15,6 +15,10 @@ const HTTP_VAR_INT = 0; const HTTP_VAR_STR = 1; const HTTP_VAR_BOOL = 2; const HTTP_VAR_DEC = 3; +const HTTP_VAR_ARRAY = 4; + +const VIEW_GRID = 'grid'; +const VIEW_LIST = 'list'; const SOURCE_PROJECT_GUTENBERG = 0; const SOURCE_HATHI_TRUST = 1; diff --git a/lib/HttpInput.php b/lib/HttpInput.php index 68cb3b9d..2755426c 100644 --- a/lib/HttpInput.php +++ b/lib/HttpInput.php @@ -3,6 +3,10 @@ class HttpInput{ public static function GetString(string $variable, bool $allowEmptyString = true, string $default = null): ?string{ $var = self::GetHttpVar($variable, HTTP_VAR_STR, GET, $default); + if(is_array($var)){ + return $default; + } + if(!$allowEmptyString && $var === ''){ return null; } @@ -22,6 +26,10 @@ class HttpInput{ return self::GetHttpVar($variable, HTTP_VAR_DEC, GET, $default); } + public static function GetArray(string $variable, array $default = null): ?array{ + return self::GetHttpVar($variable, HTTP_VAR_ARRAY, GET, $default); + } + private static function GetHttpVar(string $variable, int $type, int $set, $default){ $vars = array(); @@ -38,7 +46,12 @@ class HttpInput{ } if(isset($vars[$variable])){ - $var = trim($vars[$variable]); + if(is_array($vars[$variable])){ + return $vars[$variable]; + } + else{ + $var = trim($vars[$variable]); + } switch($type){ case HTTP_VAR_STR: diff --git a/lib/Library.php b/lib/Library.php index 104410e6..b64fe1f8 100644 --- a/lib/Library.php +++ b/lib/Library.php @@ -10,64 +10,107 @@ class Library{ /** * @return array */ - public static function GetEbooks(string $sort = null): array{ - $ebooks = []; + public static function FilterEbooks($query = null, $tags = [], $sort = null){ + $ebooks = Library::GetEbooks(); + $matches = $ebooks; + + if($sort === null){ + $sort = SORT_NEWEST; + } + + if(sizeof($tags) > 0 && !in_array('all', $tags)){ // 0 tags means "all ebooks" + $matches = []; + foreach($tags as $tag){ + foreach($ebooks as $ebook){ + if($ebook->HasTag($tag)){ + $matches[$ebook->Identifier] = $ebook; + } + } + } + } + + if($query !== null){ + $filteredMatches = []; + + foreach($matches as $ebook){ + if($ebook->Contains($query)){ + $filteredMatches[$ebook->Identifier] = $ebook; + } + } + + $matches = $filteredMatches; + } switch($sort){ case SORT_AUTHOR_ALPHA: - // Get all ebooks, sorted by author alpha first. - try{ - $ebooks = apcu_fetch('ebooks-alpha'); - } - catch(Safe\Exceptions\ApcuException $ex){ - Library::RebuildCache(); - $ebooks = apcu_fetch('ebooks-alpha'); - } + usort($matches, function($a, $b){ + return strcmp(mb_strtolower($a->Authors[0]->SortName), mb_strtolower($b->Authors[0]->SortName)); + }); break; case SORT_NEWEST: - // Get all ebooks, sorted by release date first. - try{ - $ebooks = apcu_fetch('ebooks-newest'); - } - catch(Safe\Exceptions\ApcuException $ex){ - Library::RebuildCache(); - $ebooks = apcu_fetch('ebooks-newest'); - } + usort($matches, function($a, $b){ + if($a->Timestamp < $b->Timestamp){ + return -1; + } + elseif($a->Timestamp == $b->Timestamp){ + return 0; + } + else{ + return 1; + } + }); + + $matches = array_reverse($matches); break; case SORT_READING_EASE: - // Get all ebooks, sorted by easiest first. - try{ - $ebooks = apcu_fetch('ebooks-reading-ease'); - } - catch(Safe\Exceptions\ApcuException $ex){ - Library::RebuildCache(); - $ebooks = apcu_fetch('ebooks-reading-ease'); - } + usort($matches, function($a, $b){ + if($a->ReadingEase < $b->ReadingEase){ + return -1; + } + elseif($a->ReadingEase == $b->ReadingEase){ + return 0; + } + else{ + return 1; + } + }); + + $matches = array_reverse($matches); break; case SORT_LENGTH: - // Get all ebooks, sorted by fewest words first. - try{ - $ebooks = apcu_fetch('ebooks-length'); - } - catch(Safe\Exceptions\ApcuException $ex){ - Library::RebuildCache(); - $ebooks = apcu_fetch('ebooks-length'); - } + usort($matches, function($a, $b){ + if($a->WordCount < $b->WordCount){ + return -1; + } + elseif($a->WordCount == $b->WordCount){ + return 0; + } + else{ + return 1; + } + }); break; + } - default: - // Get all ebooks, unsorted. - try{ - $ebooks = apcu_fetch('ebooks'); - } - catch(Safe\Exceptions\ApcuException $ex){ - Library::RebuildCache(); - $ebooks = apcu_fetch('ebooks'); - } - break; + return $matches; + } + + /** + * @return array + */ + public static function GetEbooks(): array{ + $ebooks = []; + + // Get all ebooks, unsorted. + try{ + $ebooks = apcu_fetch('ebooks'); + } + catch(Safe\Exceptions\ApcuException $ex){ + Library::RebuildCache(); + $ebooks = apcu_fetch('ebooks'); } return $ebooks; @@ -121,6 +164,10 @@ class Library{ return $ebooks; } + public static function GetTags(): array{ + return apcu_fetch('tags'); + } + /** * @return array */ @@ -153,6 +200,7 @@ class Library{ $collections = []; $tags = []; $authors = []; + $tagsByName = []; foreach(explode("\n", trim(shell_exec('find ' . EBOOKS_DIST_PATH . ' -name "content.opf"') ?? '')) as $filename){ try{ @@ -179,6 +227,7 @@ class Library{ // Create the tags cache foreach($ebook->Tags as $tag){ + $tagsByName[] = $tag->Name; $lcTag = strtolower($tag->Name); if(!array_key_exists($lcTag, $tags)){ $tags[$lcTag] = []; @@ -209,66 +258,6 @@ class Library{ apcu_store('ebook-' . $ebookWwwFilesystemPath, $ebook); } - // Sort ebooks by release date, then save - usort($ebooks, function($a, $b){ - if($a->Timestamp < $b->Timestamp){ - return -1; - } - elseif($a->Timestamp == $b->Timestamp){ - return 0; - } - else{ - return 1; - } - }); - - $ebooks = array_reverse($ebooks); - - apcu_delete('ebooks-newest'); - apcu_store('ebooks-newest', $ebooks); - - // Sort ebooks by title alpha, then save - usort($ebooks, function($a, $b){ - return strcmp(mb_strtolower($a->Authors[0]->SortName), mb_strtolower($b->Authors[0]->SortName)); - }); - - apcu_delete('ebooks-alpha'); - apcu_store('ebooks-alpha', $ebooks); - - // Sort ebooks by reading ease, then save - usort($ebooks, function($a, $b){ - if($a->ReadingEase < $b->ReadingEase){ - return -1; - } - elseif($a->ReadingEase == $b->ReadingEase){ - return 0; - } - else{ - return 1; - } - }); - - $ebooks = array_reverse($ebooks); - - apcu_delete('ebooks-reading-ease'); - apcu_store('ebooks-reading-ease', $ebooks); - - // Sort ebooks by word count, then save - usort($ebooks, function($a, $b){ - if($a->WordCount < $b->WordCount){ - return -1; - } - elseif($a->WordCount == $b->WordCount){ - return 0; - } - else{ - return 1; - } - }); - - apcu_delete('ebooks-length'); - apcu_store('ebooks-length', $ebooks); - // Now store various collections apcu_delete(new APCUIterator('/^collection-/')); foreach($collections as $collection => $ebooks){ @@ -282,6 +271,11 @@ class Library{ apcu_store('tag-' . $tag, $ebooks); } + apcu_delete('tags'); + $tagsByName = array_unique($tagsByName, SORT_STRING); + natsort($tagsByName); + apcu_store('tags', $tagsByName); + apcu_delete(new APCUIterator('/^author-/')); foreach($authors as $author => $ebooks){ apcu_store('author-' . $author, $ebooks); diff --git a/templates/EbookGrid.php b/templates/EbookGrid.php index 6fbb5840..6b31741c 100644 --- a/templates/EbookGrid.php +++ b/templates/EbookGrid.php @@ -3,10 +3,10 @@ if(!isset($ebooks)){ $ebooks = []; } ?> -
    + class="list">
  1. - + CoverImage2xAvifUrl !== null){ ?> @@ -15,9 +15,16 @@ if(!isset($ebooks)){

    Title) ?>

    Authors as $author){ ?> - Name != 'Anonymous'){ ?> -

    Name) ?>

    - +

    Name != 'Anonymous'){ ?>Name) ?>

    + + +
    + ContributorsHtml !== null){ ?> +

    ContributorsHtml, '.') ?>

    + +

    WordCount) ?> words • ReadingEase ?> reading ease

    + +
  2. diff --git a/templates/SearchForm.php b/templates/SearchForm.php index ffd5d89c..c1f11324 100644 --- a/templates/SearchForm.php +++ b/templates/SearchForm.php @@ -1,3 +1,48 @@ +
    - + + + + + +
    diff --git a/www/css/core.css b/www/css/core.css index 74266255..032f9e7b 100644 --- a/www/css/core.css +++ b/www/css/core.css @@ -6,7 +6,7 @@ } @font-face{ - /* Note: Don"t use local() as a source, because our version fixes the font"s strange default line-height */ + /* Note: Don't use local() as a source, because our version fixes the font's strange default line-height */ font-family: "League Spartan"; src: url("/fonts/league-spartan-bold.woff2") format("woff2"); font-weight: bold; @@ -14,7 +14,7 @@ } @font-face{ - /* Note: Don"t use local() as a source, because our version fixes the font"s strange default line-height */ + /* Note: Don't use local() as a source, because our version fixes the font's strange default line-height */ font-family: "Crimson Pro"; src: url("/fonts/crimson-pro.woff2") format("woff2"); font-weight: normal; @@ -22,7 +22,7 @@ } @font-face{ - /* Note: Don"t use local() as a source, because our version fixes the font"s strange default line-height */ + /* Note: Don't use local() as a source, because our version fixes the font's strange default line-height */ font-family: "Crimson Pro"; src: url("/fonts/crimson-pro-bold.woff2") format("woff2"); font-weight: bold; @@ -30,7 +30,7 @@ } @font-face{ - /* Note: Don"t use local() as a source, because our version fixes the font"s strange default line-height */ + /* Note: Don't use local() as a source, because our version fixes the font's strange default line-height */ font-family: "Crimson Pro"; src: url("/fonts/crimson-pro-italic.woff2") format("woff2"); font-weight: normal; @@ -38,7 +38,7 @@ } @font-face{ - /* Note: Don"t use local() as a source, because our version fixes the font"s strange default line-height */ + /* Note: Don't use local() as a source, because our version fixes the font's strange default line-height */ font-family: "Crimson Pro"; src: url("/fonts/crimson-pro-bold-italic.woff2") format("woff2"); font-weight: bold; @@ -67,6 +67,9 @@ --light-border: #222; --light-sub-text: #777; --light-body-bg: #e9e7e0; + --light-input-hover: #000; + --light-input-border: #777; + --light-input-outline: #000; --dark-body-text: #fff; --dark-highlight: #3da5bb; --dark-button: #118460; @@ -74,6 +77,9 @@ --dark-border: #000; --dark-sub-text: #aaa; --dark-body-bg: #293542; + --dark-input-border: #aaa; + --dark-input-hover: #118460; + --dark-input-outline: #fff; --body-text: var(--light-body-text); --highlight: var(--light-highlight); @@ -82,6 +88,9 @@ --border: var(--light-border); --sub-text: var(--light-sub-text); --body-bg: var(--light-body-bg); + --input-hover: var(--light-input-hover); + --input-border: var(--light-input-border); + --input-outline: var(--light-input-outline); } @media(prefers-color-scheme: dark){ @@ -93,6 +102,9 @@ --border: var(--dark-border); --sub-text: var(--dark-sub-text); --body-bg: var(--dark-body-bg); + --input-hover: var(--dark-input-hover); + --input-border: var(--dark-input-border); + --input-outline: var(--dark-input-outline); } } @@ -119,9 +131,8 @@ a:hover{ color: var(--button-highlight); } -label.search:focus-within, a:focus{ - outline: 1px dashed var(--button-highlight); + outline: 1px dashed var(--input-outline); } main{ @@ -222,9 +233,9 @@ body > header{ } body > header > a{ - background: url("/images/logo-white-full.svg"); + background: url("/images/logo-full.svg"); -webkit-filter: drop-shadow(1px 1px 0 rgba(0, 0, 0, .75)); - filter: drop-shadow(1px 1px 0 rgba(0, 0, 0, .75)); + filter: invert(100%) drop-shadow(1px 1px 0 rgba(0, 0, 0, .75)); height: 70px; width: 300px; background-size: contain; @@ -239,7 +250,7 @@ body > header > a:visited{ } body > header a:focus{ - outline: 1px dashed #fff; + outline: 1px dashed var(--input-outline); } body > header > a:hover{ @@ -475,7 +486,7 @@ h1 + section > h2:first-child{ a.button, .ebooks nav > a, -aside.sort button{ +form button{ font-style: normal; box-sizing: border-box; background-color: #1d6878; /* fallback for IE */ @@ -491,14 +502,17 @@ aside.sort button{ position: relative; text-transform: lowercase; cursor: pointer; + white-space: nowrap; + font-size: 1rem; } -a.button:focus, .ebooks nav li.highlighted a:focus, -button:focus, +a.button:focus, +input[type="search"]:focus, select:focus, +button:focus, nav a[rel]:focus{ - outline: 1px dashed #fff; + outline: 1px dashed var(--input-outline); } select:-moz-focusring, @@ -509,7 +523,7 @@ select::-moz-focus-inner{ a.button:active, .ebooks nav > a[href]:active, -aside.sort button:active{ +form button:active{ top: 2px; left: 2px; box-shadow: none; @@ -547,32 +561,19 @@ a[href].button.next:hover::after, transition: all 200ms ease; } -aside.sort select{ - margin-left: 1rem; - padding: 1rem; - background-color: rgba(0, 0, 0, .1); - border: 1px solid #777; - border-radius: 5px; - color: var(--body-text); - font-size: 1rem; - text-transform: lowercase; - font-family: "Crimson Pro", serif; - box-shadow: 1px 1px 0 rgba(255, 255, 255, .5) inset; - cursor: pointer; +a.button.next:hover::after, +.ebooks nav > a:last-child[href]:hover::after{ + left: .25rem; + position: relative; + transition: all 200ms ease; } -aside.sort button{ - margin-left: 1rem; - font-size: 1rem; -} - - article nav ol li a:hover, main.ebooks nav ol li a:hover, main.ebooks nav ol li.highlighted a:hover, a.button:hover, .ebooks nav > a:hover, -aside.sort button:hover{ +form button:hover{ /* fallback for ie */ background-color: #288da4; background-color: var(--button-highlight); @@ -925,30 +926,6 @@ article.ebook aside#reading-ease{ text-align: center; } -aside.sort form{ - display: flex; - flex-wrap: wrap; - align-content: center; - justify-content: center; - margin-top: 2rem; - font-style: italic; - text-align: center; -} - - -aside.sort form label{ - display: flex; - align-items: center; -} - -aside.sort a.button{ - margin-left: 1rem; -} - -form[action="/ebooks"]{ - margin: 0 1rem; -} - h2{ font-size: 1.2rem; font-family: "League Spartan", sans-serif; @@ -1048,16 +1025,50 @@ main.ebooks > aside.alert + ol{ } main.ebooks > ol{ - display: flex; - flex-wrap: wrap; - max-width: none; + display: grid; + grid-template-columns: repeat(4, 1fr); + grid-gap: 4rem; + margin: 0; + margin-bottom: 4rem; list-style: none; - margin-top: 0; +} + +main.ebooks > ol.list{ + display: flex; + flex-direction: column; + gap: 0; + margin: auto; + margin-bottom: 4rem; } main.ebooks > ol > li{ - width: calc(25% - 4rem); - margin: 2rem; + margin: 0; + text-align: center; +} + +main.ebooks > ol.list > li{ + display: grid; + grid-template-columns: 6rem 1fr; + grid-column-gap: 1rem; + grid-template-rows: auto auto 1fr; +} + +main.ebooks > ol.list > li + li { + margin-top: 2rem; +} + +main.ebooks > ol.list > li > a{ + grid-row: 1 / span 3; +} + +main.ebooks > ol.list ul.tags{ + display: flex; + justify-content: flex-start; + margin-left: 0; +} + +main.ebooks > ol.list > li p{ + text-align: left; } main.ebooks > ol img:hover{ @@ -1080,19 +1091,22 @@ main.ebooks > ol > li p{ main.ebooks > ol > li img{ box-sizing: border-box; - width: 100%; + max-width: 100%; border: 1px solid var(--border); border-radius: .25rem; transition: all .2s ease; } -main.ebooks > ol > li p:nth-of-type(1) a{ +main.ebooks > ol > li > p:nth-of-type(1) > a{ font-weight: bold; text-decoration: none; display: flex; justify-content: center; - line-height: 1.2; - margin-bottom: .25rem; +} + +main.ebooks > ol.list > li p.author a, +main.ebooks > ol.list > li > p:nth-of-type(1) > a{ + display: inline; } main.ebooks > ol > li > a:first-child{ @@ -1104,7 +1118,6 @@ main.ebooks > ol > li p.author a{ text-decoration: none; display: flex; justify-content: center; - line-height: 1.2; } article nav ol, @@ -1176,6 +1189,10 @@ ul.tags li{ margin: 0; } +ul.tags li + li{ + margin-left: .5rem; +} + ul.tags li a{ border: 1px solid var(--body-text); border-radius: 5px; @@ -1197,10 +1214,6 @@ ul.tags li a:hover{ background: var(--button-highlight); } -ul.tags li + li{ - margin-left: .5rem; -} - figure img{ border-radius: .25rem; border: 1px solid var(--border); @@ -1266,15 +1279,6 @@ p.no-results{ margin-top: 1rem; } -input[type="search"]{ - font-size: 1rem; - width: 100%; - background: none; - border: none; - font-family: "Crimson Pro"; - color: var(--body-text); -} - /* remove some styles from Chromium / Webkit */ input[type="search"], input[type="search"]::-webkit-search-decoration, @@ -1287,30 +1291,80 @@ input::placeholder{ color: rgba(0, 0, 0, .75); } -main > article > form{ - max-width: calc(100% - 2rem); +select{ + -webkit-appearance: none; + appearance: none; + font-size: 1rem; + font-family: "FontAwesome", "Crimson Pro"; + color: var(--body-text); + background: rgba(0, 0, 0, .1); + border-radius: 5px; + border: 1px solid var(--input-border); + padding: 1rem; + padding-right: 2rem; + line-height: 1.4rem; + display: block; } -label.search{ +input[type="search"]{ box-sizing: border-box; width: 100%; background: rgba(0, 0, 0, .1); - box-shadow: 1px 1px 0 rgba(255, 255, 255, .5) inset; border-radius: 5px; - border: 1px solid var(--sub-text); + border: 1px solid var(--input-border); display: flex; align-items: center; padding: 1rem; - font-size: 0; + padding-left: 2.5rem; color: inherit; + font-size: 1rem; + font-family: "Crimson Pro"; + color: var(--body-text); + line-height: 1.4; } -input[type="search"]:focus{ - outline: none; +select, +input[type="search"]{ + box-shadow: 1px 1px 0 rgba(255, 255, 255, .5) inset; +} + +label.select > span{ + display: block; +} + +label.select > span + span{ + display: inline-block; +} + +label.select > span + span::after{ + display: block; + position: absolute; + top: calc((2rem + 1.4rem + 2px) / 2 - 6px); + right: calc(1rem - 12px / 2); + content: "\f107"; + font-family: "FontAwesome"; + font-size: 1rem; + line-height: 1; + color: var(--sub-text); + margin-top: -3px; /* Adjust for Crimson Pro line-height */ + text-shadow: 1px 1px 0 rgba(0, 0, 0, .5); +} + +label.select:hover > span + span::after{ + color: var(--input-outline); +} + +label.select > span + span, +label.search{ + position: relative; + max-width: 100%; } label.search::before{ display: block; + position: absolute; + top: calc(2rem + 4px + .7rem); + left: 1rem; content: "\f002"; font-family: "FontAwesome"; font-size: 1rem; @@ -1319,6 +1373,95 @@ label.search::before{ color: var(--body-text); margin-top: -3px; /* Adjust for Crimson Pro line-height */ text-shadow: 1px 1px 0 rgba(255, 255, 255, .5); + width: 1rem; +} + +nav li.highlighted a, +nav a[rel], +a.button, +button, +input[type="search"], +select{ + transition: border-color .5s, background-color .5s; +} + +a.button:hover, +nav li.highlighted a:hover, +nav a[rel]:hover, +button:hover{ + transition: none; +} + +input[type="search"]:focus, +input[type="search"]:hover, +select:focus, +select:hover{ + border-color: var(--input-outline); + background: rgba(0, 0, 0, .15); + transition: none; +} + +select[multiple] option:last-child{ + margin-bottom: 1rem; /* needed for firefox */ +} + +form[action="/ebooks"] label{ + max-width: 100%; +} + +form[action="/ebooks"] select[multiple] option[value="all"]{ + border-bottom: 1px dashed var(--sub-text); +} + +form[action="/ebooks"]{ + display: grid; + grid-gap: 1rem; + grid-template-columns: 25% auto auto auto 1fr; + margin: 0 1rem; + margin-bottom: 4rem; + max-width: calc(100% - 2rem); +} + +form[action="/ebooks"] label.tags{ + grid-column: 1; + grid-row: 1 / span 2; + white-space: nowrap; +} + +form[action="/ebooks"] label.tags span{ + font-style: italic; + opacity: .5; +} + +form[action="/ebooks"] label.tags select{ + width: 100%; + height: calc(100% - 1.4rem); +} + +form[action="/ebooks"] label.search{ + grid-column: 2 / span 4; +} + +form[action="/ebooks"] label.sort{ + grid-column: 2; +} + +form[action="/ebooks"] label.sort select{ + max-width: 100%; +} + +form[action="/ebooks"] label.view{ + grid-column: 3; +} + +form[action="/ebooks"] label.per-page{ + grid-column: 4; +} + +form[action="/ebooks"] button{ + height: calc(1.4rem + 2rem + 2px); + justify-self: end; + margin-top: 1.4rem; } article nav ol li:not(:first-child):not(:last-child):not(.highlighted), @@ -1349,6 +1492,86 @@ main.ebooks nav ol li.highlighted:nth-last-child(2)::after{ display: none; } +@media (hover: none) and (pointer: coarse){ /* target ipads and smartphones without a mouse */ + /* For iPad, unset the height so it matches the other elements */ + select[multiple]{ + height: calc(1rem + 1.4rem + 1rem + 2px) !important; + } + + label.tags > span, + select[multiple] option[value="all"]{ + /* Hide the "all" button, because touchscreen devices + have clearer ways to deselect options */ + display: none; + } + + form[action="/ebooks"]{ + grid-template-columns: auto auto auto 1fr; + } + + form[action="/ebooks"] label.tags{ + grid-row: 1; + } + + + form[action="/ebooks"] label.search{ + grid-column: 2 / span 3; + } + + form[action="/ebooks"] label.sort{ + grid-column: 1; + grid-row: 2; + } + + form[action="/ebooks"] label.view{ + grid-column: 2; + grid-row: 2; + } + + form[action="/ebooks"] label.per-page{ + grid-column: 3; + grid-row: 2; + } + + form[action="/ebooks"] button{ + grid-column: 4; + grid-row: 2; + } +} + +@media(max-width: 1300px){ + form[action="/ebooks"]{ + /* We create 5 columns so that the last one can be 1fr wide, pushing + the rest into their smallest necessary size */ + grid-template-columns: 13rem auto auto 1fr; + } + + form[action="/ebooks"] label.tags{ + grid-row: 1 / span 3; + } + + form[action="/ebooks"] label.search{ + grid-column: 2 / span 3; + } + + form[action="/ebooks"] label.view{ + grid-row: 2; + grid-column: 3; + } + + form[action="/ebooks"] label.per-page{ + grid-row: 3; + grid-column: 2; + } + + form[action="/ebooks"] button{ + grid-row: 3; + grid-column: 3 / span 2; + margin-top: 0; + align-self: end; + } +} + @media(max-width: 1100px){ article.ebook header{ width: calc(100% + 4rem); @@ -1376,6 +1599,40 @@ main.ebooks nav ol li.highlighted:nth-last-child(2)::after{ } } +@media (hover: none) and (pointer: coarse) and (max-width: 1100px){ /* target ipads and smartphones without a mouse */ + form[action="/ebooks"]{ + grid-template-columns: auto auto auto 1fr; + } + + form[action="/ebooks"] label.tags{ + grid-row: 1; + } + + form[action="/ebooks"] label.search{ + grid-column: 2 / span 3; + } + + form[action="/ebooks"] label.sort{ + grid-column: 1; + grid-row: 2; + } + + form[action="/ebooks"] label.view{ + grid-column: 2; + grid-row: 2; + } + + form[action="/ebooks"] label.per-page{ + grid-column: 3; + grid-row: 2; + } + + form[action="/ebooks"] button{ + grid-column: 4; + grid-row: 2; + } +} + @media(max-width: 1000px){ article.ebook #more-ebooks ul{ flex-wrap: wrap; @@ -1394,20 +1651,6 @@ main.ebooks nav ol li.highlighted:nth-last-child(2)::after{ display: none; } - main.ebooks > ol > li{ - width: calc(33% - 4rem); - } - - aside.sort{ - flex-wrap: wrap; - } - - aside.sort p{ - width: 100%; - margin: auto; - margin-bottom: 1rem; - } - footer ul li{ display: inline; } @@ -1427,6 +1670,46 @@ main.ebooks nav ol li.highlighted:nth-last-child(2)::after{ margin-left: 2rem; padding-top: 0; } + + form[action="/ebooks"]{ + grid-template-columns: auto 1fr; + } + + form[action="/ebooks"] label.tags{ + grid-row: 1; + grid-column: 1 / span 2; + } + + form[action="/ebooks"] label.tags select{ + height: calc((1rem + 1.4rem + 1rem + 2px) * 2); /* Size equal to two regular select boxes */ + } + + form[action="/ebooks"] label.search{ + grid-row: 2; + grid-column: 1 / span 2; + } + + form[action="/ebooks"] label.sort{ + grid-row: 3; + grid-column: 1; + } + + form[action="/ebooks"] label.view{ + grid-row: 3; + grid-column: 2; + } + + form[action="/ebooks"] button, + form[action="/ebooks"] label.per-page{ + grid-row: 4; + grid-column: 1 / span 2; + } +} + +@media(max-width: 900px){ + main.ebooks > ol{ + grid-template-columns: repeat(3, 1fr); + } } @media(max-width: 860px){ @@ -1441,6 +1724,83 @@ main.ebooks nav ol li.highlighted:nth-last-child(2)::after{ } } +@media (hover: none) and (pointer: coarse) and (max-width: 768px){ /* target ipads and smartphones without a mouse */ + form[action="/ebooks"]{ + grid-template-columns: auto auto 1fr; + } + + form[action="/ebooks"] label.search{ + grid-column: 2 / span 2; + grid-row: 1; + } + + form[action="/ebooks"] label.tags{ + grid-column: 1; + grid-row: 1; + } + + form[action="/ebooks"] label.sort{ + grid-column: 1 / span 2; + grid-row: 2; + } + + form[action="/ebooks"] label.view{ + grid-column: 3; + grid-row: 2; + } + + form[action="/ebooks"] label.per-page{ + grid-column: 1; + grid-row: 3; + } + + form[action="/ebooks"] button{ + grid-row: 3; + grid-column: 2 / span 2; + } +} + +@media(max-width: 700px){ + main.ebooks > ol{ + grid-template-columns: repeat(2, 1fr); + } +} + +@media (hover: none) and (pointer: coarse) and (max-width: 700px){ /* target ipads and smartphones without a mouse */ + form[action="/ebooks"]{ + grid-template-columns: auto 1fr; + } + + form[action="/ebooks"] label.search{ + grid-column: 1 / span 2; + grid-row: 1; + } + + form[action="/ebooks"] label.tags{ + grid-column: 1 / span 2; + grid-row: 2; + } + + form[action="/ebooks"] label.sort{ + grid-column: 1 / span 2; + grid-row: 3; + } + + form[action="/ebooks"] label.view{ + grid-column: 1; + grid-row: 4; + } + + form[action="/ebooks"] label.per-page{ + grid-column: 2; + grid-row: 4; + } + + form[action="/ebooks"] button{ + grid-row: 5; + grid-column: 1 / span 2; + } +} @media(max-width: 680px){ body > header{ @@ -1452,24 +1812,11 @@ main.ebooks nav ol li.highlighted:nth-last-child(2)::after{ margin-top: 2rem; } - main.ebooks > ol > li{ - width: calc(50% - 4rem); - } - main.front-page h1{ padding: 3rem; font-size: 2.4rem; } - aside.sort form{ - flex-direction: column; - } - - aside.sort form button{ - margin-left: 0; - margin-top: 1rem; - } - article.ebook section#details ul{ flex-direction: column; } @@ -1489,6 +1836,25 @@ main.ebooks nav ol li.highlighted:nth-last-child(2)::after{ main.ebooks nav ol li.highlighted::after{ display: none; } + + form[action="/ebooks"] label.sort{ + grid-column: 1 / span 2; + } + + form[action="/ebooks"] label.view{ + grid-row: 4; + grid-column: 1; + } + + form[action="/ebooks"] label.per-page{ + grid-row: 4; + grid-column: 2; + } + + form[action="/ebooks"] button{ + grid-row: 5; + grid-column: 1 / span 2; + } } @media(max-width: 500px){ @@ -1519,15 +1885,6 @@ main.ebooks nav ol li.highlighted:nth-last-child(2)::after{ padding: 0; } - main.ebooks > ol{ - margin-top: 1rem; - } - - main.ebooks > ol > li{ - width: calc(50% - 2rem); - margin: 1rem; - } - article nav ol li:not(.highlighted), main.ebooks nav ol li:not(.highlighted){ display: none; @@ -1592,6 +1949,22 @@ main.ebooks nav ol li.highlighted:nth-last-child(2)::after{ } } +@media(max-width: 470px){ + main.ebooks > ol{ + grid-gap: 2rem; + } +} + +@media(max-width: 400px){ + form[action="/ebooks"] button{ + grid-row: 5; + } + + form[action="/ebooks"] label.tags span{ + display: none; + } +} + @media(max-width: 380px){ body > header{ padding: 1rem 0; @@ -1635,6 +2008,31 @@ main.ebooks nav ol li.highlighted:nth-last-child(2)::after{ footer ul li::after{ display: none; } + + main.ebooks > ol{ + grid-template-columns: 1fr; + } + + form[action="/ebooks"]{ + grid-template-columns: 100%; + } + + form[action="/ebooks"] label.search, + form[action="/ebooks"] label.tags, + form[action="/ebooks"] label.sort, + form[action="/ebooks"] label.view, + form[action="/ebooks"] label.per-page, + form[action="/ebooks"] button{ + grid-column: 1; + } + + form[action="/ebooks"] label.per-page{ + grid-row: 5; + } + + form[action="/ebooks"] button{ + grid-row: 6; + } } @supports not(hyphens: auto){ @@ -1649,6 +2047,13 @@ main.ebooks nav ol li.highlighted:nth-last-child(2)::after{ box-shadow: 3px 3px 1px rgba(0, 0, 0, .5); } + body > header > a:focus{ + /* Since we use invert() to change the color of the SE logo, + we must specify the light outline (which is dark) + so that it inverts to white */ + outline: 1px dashed var(--light-input-outline); + } + article.ebook section#read-online a::before, article.ebook section#download ul li a[class]::before, article.ebook section#details ul li a[class]::before, @@ -1656,10 +2061,12 @@ main.ebooks nav ol li.highlighted:nth-last-child(2)::after{ main.front-page > section > section figure.oss img + img, main.front-page > section > h2::before, main.front-page > section > h2::after{ - filter: invert() grayscale(100%) brightness(120%); /* grayscale and brightness makes it hit about #eeeeee */ + /* brightness(0) ensures the images are stark white when inverted, since the originals are usually not #000 */ + filter: brightness(0) invert() grayscale(100%) brightness(120%); /* grayscale and brightness makes it hit about #eeeeee */ } - label.search{ + select, + input[type="search"]{ box-shadow: 1px 1px 0 rgba(0, 0, 0, .5) inset; } diff --git a/www/ebooks/author.php b/www/ebooks/author.php index 3812aac8..43b3ed79 100644 --- a/www/ebooks/author.php +++ b/www/ebooks/author.php @@ -24,8 +24,7 @@ catch(\Exception $ex){ ?> 'Ebooks by ' . strip_tags($ebooks[0]->AuthorsHtml), 'highlight' => 'ebooks', 'description' => 'All of the Standard Ebooks ebooks by ' . strip_tags($ebooks[0]->AuthorsHtml)]) ?>

    Ebooks by AuthorsHtml ?>

    - - $ebooks]) ?> + $ebooks, 'view' => VIEW_GRID]) ?>
    diff --git a/www/ebooks/index.php b/www/ebooks/index.php index cbfb5dcf..43e62bd8 100644 --- a/www/ebooks/index.php +++ b/www/ebooks/index.php @@ -5,10 +5,12 @@ use function Safe\preg_replace; try{ $page = HttpInput::GetInt('page', 1); + $perPage = HttpInput::GetInt('per-page', EBOOKS_PER_PAGE); $query = HttpInput::GetString('query', false); - $tag = HttpInput::GetString('tag', false); + $tags = HttpInput::GetArray('tags', []); $collection = HttpInput::GetString('collection', false); - $sort = HttpInput::GetString('sort', false, SORT_NEWEST); + $view = Httpinput::GetString('view', false); + $sort = HttpInput::GetString('sort', false); $pages = 0; $totalEbooks = 0; @@ -16,30 +18,38 @@ try{ $page = 1; } - if($sort != SORT_AUTHOR_ALPHA && $sort != SORT_NEWEST && $sort != SORT_READING_EASE && $sort != SORT_LENGTH){ - $sort = SORT_NEWEST; + if($perPage != EBOOKS_PER_PAGE && $perPage != 24 && $perPage != 48){ + $perPage = EBOOKS_PER_PAGE; } - if($query !== null){ - $ebooks = Library::Search($query); - $pageTitle = 'Search Standard Ebooks'; - $pageDescription = 'Search results'; - $pageHeader = 'Search Ebooks'; + // If we're passed string values that are the same as the defaults, + // set them to null so that we can have cleaner query strings in the navigation footer + if($view !== null){ + $view = mb_strtolower($view); } - elseif($tag !== null){ - $tag = strtolower(str_replace('-', ' ', $tag)); - $ebooks = Library::GetEbooksByTag($tag); - $pageTitle = 'Browse ebooks tagged “' . Formatter::ToPlainText($tag) . '”'; - $pageDescription = 'Page ' . $page . ' of ebooks tagged “' . Formatter::ToPlainText($tag) . '”'; - $pageHeader = 'Ebooks tagged “' . Formatter::ToPlainText($tag) . '”'; - $pages = ceil(sizeof($ebooks) / EBOOKS_PER_PAGE); - - $totalEbooks = sizeof($ebooks); - - $ebooks = array_slice($ebooks, ($page - 1) * EBOOKS_PER_PAGE, EBOOKS_PER_PAGE); + if($sort !== null){ + $sort = mb_strtolower($sort); } - elseif($collection !== null){ + + if($view === 'grid'){ + $view = null; + } + + if($sort === 'newest'){ + $sort = null; + } + + if($query === ''){ + $query = null; + } + + if(sizeof($tags) == 1 && mb_strtolower($tags[0]) == 'all'){ + $tags = []; + } + + // Are we looking at a collection? + if($collection !== null){ $ebooks = Library::GetEbooksByCollection($collection); $collectionObject = null; // Get the *actual* name of the collection, in case there are accent marks (like "Arsène Lupin") @@ -71,32 +81,47 @@ try{ } } else{ + $ebooks = Library::FilterEbooks($query, $tags, $sort); $pageTitle = 'Browse Standard Ebooks'; $pageHeader = 'Browse Ebooks'; - $ebooks = Library::GetEbooks($sort); + } - $pages = ceil(sizeof($ebooks) / EBOOKS_PER_PAGE); + $pages = ceil(sizeof($ebooks) / $perPage); - $totalEbooks = sizeof($ebooks); + $totalEbooks = sizeof($ebooks); - $ebooks = array_slice($ebooks, ($page - 1) * EBOOKS_PER_PAGE, EBOOKS_PER_PAGE); + $ebooks = array_slice($ebooks, ($page - 1) * $perPage, $perPage); - $pageDescription = 'Page ' . $page . ' of the Standard Ebooks ebook library, sorted '; - switch($sort){ - case SORT_NEWEST: - $pageDescription .= 'by newest ebooks first.'; - break; - case SORT_AUTHOR_ALPHA: - $pageDescription .= 'alphabetically by author name.'; - break; - case SORT_READING_EASE: - $pageDescription .= 'by easiest ebooks first.'; - break; - case SORT_LENGTH: - $pageDescription .= 'by shortest ebooks first.'; - break; + if($page > 1){ + $pageTitle .= ', page ' . $page; + } + $pageDescription = 'Page ' . $page . ' of the Standard Ebooks ebook library'; + + $queryString = ''; + + if($collection === null){ + if($query != ''){ + $queryString .= '&query=' . urlencode($query); + } + + foreach($tags as $tag){ + $queryString .= '&tags[]=' . urlencode($tag); + } + + if($view !== null){ + $queryString .= '&view=' . urlencode($view); + } + + if($sort !== null){ + $queryString .= '&sort=' . urlencode($sort); + } + + if($perPage !== EBOOKS_PER_PAGE){ + $queryString .= '&per-page=' . urlencode($perPage); } } + + $queryString = preg_replace('/^&/ius', '', $queryString); } catch(\Exception $ex){ http_response_code(404); @@ -106,50 +131,27 @@ catch(\Exception $ex){ ?> $pageTitle, 'highlight' => 'ebooks', 'description' => $pageDescription]) ?>

    - $query]) ?> + + $query, 'tags' => $tags, 'sort' => $sort, 'view' => $view, 'perPage' => $perPage]) ?> + -

    No ebooks matched your search. You can try different search terms, or browse all of our ebooks.

    +

    No ebooks matched your filters. You can try different filters, or browse all of our ebooks.

    - $ebooks]) ?> + $ebooks, 'view' => $view]) ?> - 0 && $query === null && $tag === null && $collection === null){ ?> + 0){ ?> - 0 && $tag !== null){ ?> - - 0 && $query === null && $tag === null && $collection === null){ ?> - - + 0 && $query === null && sizeof($tags) == 0 && $collection === null && $page == 1){ ?> -
    diff --git a/www/fonts/font-awesome.woff2 b/www/fonts/font-awesome.woff2 index 4d13fc60..120b3007 100644 Binary files a/www/fonts/font-awesome.woff2 and b/www/fonts/font-awesome.woff2 differ diff --git a/www/images/logo-white-full.svg b/www/images/logo-white-full.svg deleted file mode 100644 index 3a7152f5..00000000 --- a/www/images/logo-white-full.svg +++ /dev/null @@ -1,56 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/www/images/logo-white.svg b/www/images/logo-white.svg deleted file mode 100644 index b839bdfc..00000000 --- a/www/images/logo-white.svg +++ /dev/null @@ -1,71 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -