diff --git a/config/sql/se/Benefits.sql b/config/sql/se/Benefits.sql index adaabd8e..e11a7e62 100644 --- a/config/sql/se/Benefits.sql +++ b/config/sql/se/Benefits.sql @@ -7,6 +7,7 @@ CREATE TABLE IF NOT EXISTS `Benefits` ( `CanReviewArtwork` tinyint(1) unsigned NOT NULL DEFAULT 0, `CanReviewOwnArtwork` tinyint(1) unsigned NOT NULL DEFAULT 0, `CanEditUsers` tinyint(1) unsigned NOT NULL DEFAULT 0, + `CanCreateEbookPlaceholders` tinyint(1) unsigned NOT NULL DEFAULT 0, PRIMARY KEY (`UserId`), KEY `idxBenefits` (`CanAccessFeeds`,`CanVote`,`CanBulkDownload`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; diff --git a/config/sql/se/EbookPlaceholders.sql b/config/sql/se/EbookPlaceholders.sql new file mode 100644 index 00000000..409591ec --- /dev/null +++ b/config/sql/se/EbookPlaceholders.sql @@ -0,0 +1,11 @@ +CREATE TABLE IF NOT EXISTS `EbookPlaceholders` ( + `EbookId` int(10) unsigned NOT NULL, + `YearPublished` smallint unsigned NULL, + `Status` enum('wanted', 'in_progress') NULL, + `Difficulty` enum('beginner', 'intermediate', 'advanced') NULL, + `TranscriptionUrl` varchar(511) NULL, + `IsWanted` boolean NOT NULL DEFAULT FALSE, + `IsPatron` boolean NOT NULL DEFAULT FALSE, + `Notes` TEXT NULL DEFAULT NULL, + PRIMARY KEY (`EbookId`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; diff --git a/config/sql/se/Ebooks.sql b/config/sql/se/Ebooks.sql index dc6bde60..9eda89e9 100644 --- a/config/sql/se/Ebooks.sql +++ b/config/sql/se/Ebooks.sql @@ -3,8 +3,8 @@ CREATE TABLE IF NOT EXISTS `Ebooks` ( `Identifier` varchar(511) NOT NULL, `Created` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP, `Updated` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, - `WwwFilesystemPath` varchar(511) NOT NULL, - `RepoFilesystemPath` varchar(511) NOT NULL, + `WwwFilesystemPath` varchar(511) NULL, + `RepoFilesystemPath` varchar(511) NULL, `KindleCoverUrl` varchar(511) NULL, `EpubUrl` varchar(511) NULL, `AdvancedEpubUrl` varchar(511) NULL, @@ -14,16 +14,16 @@ CREATE TABLE IF NOT EXISTS `Ebooks` ( `Title` varchar(255) NOT NULL, `FullTitle` varchar(255) NULL, `AlternateTitle` varchar(255) NULL, - `Description` text NOT NULL, - `LongDescription` text NOT NULL, - `Language` varchar(10) NOT NULL, - `WordCount` int(10) unsigned NOT NULL, - `ReadingEase` float NOT NULL, + `Description` text NULL, + `LongDescription` text NULL, + `Language` varchar(10) NULL, + `WordCount` int(10) unsigned NULL, + `ReadingEase` float NULL, `GitHubUrl` varchar(255) NULL, `WikipediaUrl` varchar(255) NULL, - `EbookCreated` datetime NOT NULL, - `EbookUpdated` datetime NOT NULL, - `TextSinglePageByteCount` bigint unsigned NOT NULL, + `EbookCreated` datetime NULL, + `EbookUpdated` datetime NULL, + `TextSinglePageByteCount` bigint unsigned NULL, `IndexableText` text NOT NULL, PRIMARY KEY (`EbookId`), UNIQUE KEY `index1` (`Identifier`), diff --git a/lib/Benefits.php b/lib/Benefits.php index e439b570..9de908fd 100644 --- a/lib/Benefits.php +++ b/lib/Benefits.php @@ -15,6 +15,7 @@ class Benefits{ public bool $CanReviewArtwork = false; public bool $CanReviewOwnArtwork = false; public bool $CanEditUsers = false; + public bool $CanCreateEbookPlaceholders = false; protected bool $_HasBenefits; @@ -27,6 +28,8 @@ class Benefits{ $this->CanReviewOwnArtwork || $this->CanEditUsers + || + $this->CanCreateEbookPlaceholders ){ return true; } @@ -58,18 +61,18 @@ class Benefits{ public function Create(): void{ Db::Query(' - INSERT into Benefits (UserId, CanAccessFeeds, CanVote, CanBulkDownload, CanUploadArtwork, CanReviewArtwork, CanReviewOwnArtwork, CanEditUsers) - values (?, ?, ?, ?, ?, ?, ?, ?) - ', [$this->UserId, $this->CanAccessFeeds, $this->CanVote, $this->CanBulkDownload, $this->CanUploadArtwork, $this->CanReviewArtwork, $this->CanReviewOwnArtwork, $this->CanEditUsers]); + INSERT into Benefits (UserId, CanAccessFeeds, CanVote, CanBulkDownload, CanUploadArtwork, CanReviewArtwork, CanReviewOwnArtwork, CanEditUsers, CanCreateEbookPlaceholders) + values (?, ?, ?, ?, ?, ?, ?, ?, ?) + ', [$this->UserId, $this->CanAccessFeeds, $this->CanVote, $this->CanBulkDownload, $this->CanUploadArtwork, $this->CanReviewArtwork, $this->CanReviewOwnArtwork, $this->CanEditUsers, $this->CanCreateEbookPlaceholders]); } public function Save(): void{ Db::Query(' UPDATE Benefits - set CanAccessFeeds = ?, CanVote = ?, CanBulkDownload = ?, CanUploadArtwork = ?, CanReviewArtwork = ?, CanReviewOwnArtwork = ?, CanEditUsers = ? + set CanAccessFeeds = ?, CanVote = ?, CanBulkDownload = ?, CanUploadArtwork = ?, CanReviewArtwork = ?, CanReviewOwnArtwork = ?, CanEditUsers = ?, CanCreateEbookPlaceholders = ? where UserId = ? - ', [$this->CanAccessFeeds, $this->CanVote, $this->CanBulkDownload, $this->CanUploadArtwork, $this->CanReviewArtwork, $this->CanReviewOwnArtwork, $this->CanEditUsers, $this->UserId]); + ', [$this->CanAccessFeeds, $this->CanVote, $this->CanBulkDownload, $this->CanUploadArtwork, $this->CanReviewArtwork, $this->CanReviewOwnArtwork, $this->CanEditUsers, $this->CanCreateEbookPlaceholders, $this->UserId]); } public function FillFromHttpPost(): void{ @@ -80,5 +83,6 @@ class Benefits{ $this->PropertyFromHttp('CanReviewArtwork'); $this->PropertyFromHttp('CanReviewOwnArtwork'); $this->PropertyFromHttp('CanEditUsers'); + $this->PropertyFromHttp('CanCreateEbookPlaceholders'); } } diff --git a/lib/Constants.php b/lib/Constants.php index 6b27bb16..be3641bc 100644 --- a/lib/Constants.php +++ b/lib/Constants.php @@ -29,7 +29,8 @@ const MANUAL_PATH = WEB_ROOT . '/manual'; const EBOOKS_DIST_PATH = WEB_ROOT . '/ebooks/'; const COVER_ART_UPLOAD_PATH = '/images/cover-uploads/'; -const EBOOKS_IDENTIFIER_PREFIX = 'url:https://standardebooks.org/ebooks/'; +const EBOOKS_IDENTIFIER_ROOT = 'url:https://standardebooks.org'; +const EBOOKS_IDENTIFIER_PREFIX = EBOOKS_IDENTIFIER_ROOT . '/ebooks/'; const DATABASE_DEFAULT_DATABASE = 'se'; const DATABASE_DEFAULT_HOST = 'localhost'; diff --git a/lib/Contributor.php b/lib/Contributor.php index 4513b594..33ab0b45 100644 --- a/lib/Contributor.php +++ b/lib/Contributor.php @@ -41,6 +41,28 @@ class Contributor{ // METHODS // ******* + /** + * @return array + */ + public static function GetAllAuthorNames(): array{ + return Db::Query(' + SELECT DISTINCT Name + from Contributors + where MarcRole = "aut" + order by Name asc', [], Contributor::class); + } + + /** + * @return array + */ + public static function GetAllTranslatorNames(): array{ + return Db::Query(' + SELECT DISTINCT Name + from Contributors + where MarcRole = "trl" + order by Name asc', [], Contributor::class); + } + /** * @throws Exceptions\ValidationException */ diff --git a/lib/Ebook.php b/lib/Ebook.php index 1b007aef..4cf7dec0 100644 --- a/lib/Ebook.php +++ b/lib/Ebook.php @@ -41,14 +41,15 @@ use function Safe\shell_exec; * @property string $TextSinglePageUrl * @property string $TextSinglePageSizeFormatted * @property string $IndexableText + * @property EbookPlaceholder $EbookPlaceholder */ class Ebook{ use Traits\Accessor; public int $EbookId; public string $Identifier; - public string $WwwFilesystemPath; - public string $RepoFilesystemPath; + public ?string $WwwFilesystemPath = null; + public ?string $RepoFilesystemPath = null; public ?string $KindleCoverUrl = null; public ?string $EpubUrl = null; public ?string $AdvancedEpubUrl = null; @@ -58,17 +59,17 @@ class Ebook{ public string $Title; public ?string $FullTitle = null; public ?string $AlternateTitle = null; - public string $Description; - public string $LongDescription; - public string $Language; - public int $WordCount; - public float $ReadingEase; + public ?string $Description = null; + public ?string $LongDescription = null; + public ?string $Language = null; + public ?int $WordCount = null; + public ?float $ReadingEase = null; public ?string $GitHubUrl = null; public ?string $WikipediaUrl = null; /** When the ebook was published. */ - public DateTimeImmutable $EbookCreated; + public ?DateTimeImmutable $EbookCreated = null; /** When the ebook was updated. */ - public DateTimeImmutable $EbookUpdated; + public ?DateTimeImmutable $EbookUpdated = null; /** When the database row was created. */ public DateTimeImmutable $Created; /** When the database row was updated. */ @@ -116,6 +117,7 @@ class Ebook{ protected string $_TextSinglePageUrl; protected string $_TextSinglePageSizeFormatted; protected string $_IndexableText; + protected ?EbookPlaceholder $_EbookPlaceholder = null; // ******* // GETTERS @@ -318,7 +320,7 @@ class Ebook{ protected function GetUrl(): string{ if(!isset($this->_Url)){ - $this->_Url = str_replace(WEB_ROOT, '', $this->WwwFilesystemPath); + $this->_Url = str_replace(EBOOKS_IDENTIFIER_ROOT, '', $this->Identifier); } return $this->_Url; @@ -554,7 +556,7 @@ class Ebook{ protected function GetTextSinglePageSizeFormatted(): string{ if(!isset($this->_TextSinglePageSizeFormatted)){ $bytes = $this->TextSinglePageByteCount; - $sizes = array('B', 'KB', 'MB', 'GB', 'TB', 'PB'); + $sizes = ['B', 'KB', 'MB', 'GB', 'TB', 'PB']; $index = 0; while($bytes >= 1024 && $index < count($sizes) - 1){ @@ -608,6 +610,18 @@ class Ebook{ return $this->_IndexableText; } + protected function GetEbookPlaceholder(): ?EbookPlaceholder{ + if(!isset($this->_EbookPlaceholder)){ + $this->_EbookPlaceholder = Db::Query(' + SELECT * + from EbookPlaceholders + where EbookId = ? + ', [$this->EbookId], EbookPlaceholder::class)[0] ?? null; + } + + return $this->_EbookPlaceholder; + } + // *********** // ORM METHODS @@ -970,6 +984,59 @@ class Ebook{ return $ebook; } + /** + * Joins the `Name` properites of `Contributor` objects as a URL slug, e.g., + * + * ``` + * ([0] => Contributor Object ([Name] => William Wordsworth), ([1] => Contributor Object ([Name] => Samuel Coleridge))) + * ``` + * + * returns `william-wordsworth_samuel-taylor-coleridge`. + * + * @param array $contributors + */ + protected static function GetContributorsUrlSlug(array $contributors): string{ + return implode('_', array_map('Formatter::MakeUrlSafe', array_column($contributors, 'Name'))); + } + + /** + * Populates the `Identifier` property based on the `Title`, `Authors`, `Translators`, and `Illustrators`. Used when creating ebook placeholders. + * + * @throws Exceptions\InvalidEbookIdentifierException + */ + public function FillIdentifierFromTitleAndContributors(): void{ + if(!isset($this->Authors) || sizeof($this->Authors) == 0){ + throw new Exceptions\InvalidEbookIdentifierException('Authors required'); + } + + if(!isset($this->Title)){ + throw new Exceptions\InvalidEbookIdentifierException('Title required'); + } + + $authorString = Ebook::GetContributorsUrlSlug($this->Authors); + $titleString = Formatter::MakeUrlSafe($this->Title); + $translatorString = ''; + $illustratorString = ''; + + if(isset($this->Translators) && sizeof($this->Translators) > 0){ + $translatorString = Ebook::GetContributorsUrlSlug($this->Translators); + } + + if(isset($this->Illustrators) && sizeof($this->Illustrators) > 0){ + $illustratorString = Ebook::GetContributorsUrlSlug($this->Illustrators); + } + + $this->Identifier = EBOOKS_IDENTIFIER_PREFIX . $authorString . '/' . $titleString; + + if($translatorString != ''){ + $this->Identifier .= '/' . $translatorString; + } + + if($illustratorString != ''){ + $this->Identifier .= '/' . $illustratorString; + } + } + // ******* // METHODS @@ -996,13 +1063,12 @@ class Ebook{ $error->Add(new Exceptions\EbookIdentifierRequiredException()); } + $this->WwwFilesystemPath = trim($this->WwwFilesystemPath ?? ''); + if($this->WwwFilesystemPath == ''){ + $this->WwwFilesystemPath = null; + } + if(isset($this->WwwFilesystemPath)){ - $this->WwwFilesystemPath = trim($this->WwwFilesystemPath); - - if($this->WwwFilesystemPath == ''){ - $error->Add(new Exceptions\EbookWwwFilesystemPathRequiredException()); - } - if(strlen($this->WwwFilesystemPath) > EBOOKS_MAX_LONG_STRING_LENGTH){ $error->Add(new Exceptions\StringTooLongException('Ebook WwwFilesystemPath')); } @@ -1011,17 +1077,13 @@ class Ebook{ $error->Add(new Exceptions\InvalidEbookWwwFilesystemPathException($this->WwwFilesystemPath)); } } - else{ - $error->Add(new Exceptions\EbookWwwFilesystemPathRequiredException()); + + $this->RepoFilesystemPath = trim($this->RepoFilesystemPath ?? ''); + if($this->RepoFilesystemPath == ''){ + $this->RepoFilesystemPath = null; } if(isset($this->RepoFilesystemPath)){ - $this->RepoFilesystemPath = trim($this->RepoFilesystemPath); - - if($this->RepoFilesystemPath == ''){ - $error->Add(new Exceptions\EbookRepoFilesystemPathRequiredException()); - } - if(strlen($this->RepoFilesystemPath) > EBOOKS_MAX_LONG_STRING_LENGTH){ $error->Add(new Exceptions\StringTooLongException('Ebook RepoFilesystemPath')); } @@ -1030,9 +1092,6 @@ class Ebook{ $error->Add(new Exceptions\InvalidEbookRepoFilesystemPathException($this->RepoFilesystemPath)); } } - else{ - $error->Add(new Exceptions\EbookRepoFilesystemPathRequiredException()); - } $this->KindleCoverUrl = trim($this->KindleCoverUrl ?? ''); if($this->KindleCoverUrl == ''){ @@ -1157,51 +1216,36 @@ class Ebook{ $error->Add(new Exceptions\StringTooLongException('Ebook AlternateTitle')); } - if(isset($this->Description)){ - $this->Description = trim($this->Description); - - if($this->Description == ''){ - $error->Add(new Exceptions\EbookDescriptionRequiredException()); - } - } - else{ - $error->Add(new Exceptions\EbookDescriptionRequiredException()); + $this->Description = trim($this->Description ?? ''); + if($this->Description == ''){ + $this->Description = null; } - if(isset($this->LongDescription)){ - $this->LongDescription = trim($this->LongDescription); - - if($this->LongDescription == ''){ - $error->Add(new Exceptions\EbookLongDescriptionRequiredException()); - } + if(isset($this->Description) && strlen($this->Description) > EBOOKS_MAX_STRING_LENGTH){ + $error->Add(new Exceptions\StringTooLongException('Ebook Description')); } - else{ - $error->Add(new Exceptions\EbookLongDescriptionRequiredException()); + + $this->LongDescription = trim($this->LongDescription ?? ''); + if($this->LongDescription == ''){ + $this->LongDescription = null; + } + + $this->Language = trim($this->Language ?? ''); + if($this->Language == ''){ + $this->Language = null; } if(isset($this->Language)){ - $this->Language = trim($this->Language); - - if($this->Language == ''){ - $error->Add(new Exceptions\EbookLanguageRequiredException()); - } - if(strlen($this->Language) > 10){ $error->Add(new Exceptions\StringTooLongException('Ebook Language: ' . $this->Language)); } } - else{ - $error->Add(new Exceptions\EbookLanguageRequiredException()); - } if(isset($this->WordCount)){ if($this->WordCount <= 0){ $error->Add(new Exceptions\InvalidEbookWordCountException('Invalid Ebook WordCount: ' . $this->WordCount)); } } - else{ - $error->Add(new Exceptions\EbookWordCountRequiredException()); - } if(isset($this->ReadingEase)){ // In theory, Flesch reading ease can be negative, but in practice it's positive. @@ -1209,9 +1253,6 @@ class Ebook{ $error->Add(new Exceptions\InvalidEbookReadingEaseException('Invalid Ebook ReadingEase: ' . $this->ReadingEase)); } } - else{ - $error->Add(new Exceptions\EbookReadingEaseRequiredException()); - } $this->GitHubUrl = trim($this->GitHubUrl ?? ''); if($this->GitHubUrl == ''){ @@ -1248,9 +1289,6 @@ class Ebook{ $error->Add(new Exceptions\InvalidEbookCreatedDatetimeException($this->EbookCreated)); } } - else{ - $error->Add(new Exceptions\EbookCreatedDatetimeRequiredException()); - } if(isset($this->EbookUpdated)){ if($this->EbookUpdated > NOW){ @@ -1258,18 +1296,12 @@ class Ebook{ } } - else{ - $error->Add(new Exceptions\EbookUpdatedDatetimeRequiredException()); - } if(isset($this->TextSinglePageByteCount)){ if($this->TextSinglePageByteCount <= 0){ $error->Add(new Exceptions\InvalidEbookTextSinglePageByteCountException('Invalid Ebook TextSinglePageByteCount: ' . $this->TextSinglePageByteCount)); } } - else{ - $error->Add(new Exceptions\EbookTextSinglePageByteCountRequiredException()); - } if(isset($this->IndexableText)){ $this->IndexableText = trim($this->IndexableText ?? ''); @@ -1282,6 +1314,23 @@ class Ebook{ $error->Add(new Exceptions\EbookIndexableTextRequiredException()); } + if(isset($this->EbookPlaceholder)){ + try{ + $this->EbookPlaceholder->Validate(); + } + catch(Exceptions\ValidationException $ex){ + $error->Add($ex); + } + } + + if($this->IsPlaceholder() && !isset($this->EbookPlaceholder)){ + $error->Add(new Exceptions\EbookMissingPlaceholderException()); + } + + if(!$this->IsPlaceholder() && isset($this->EbookPlaceholder)){ + $error->Add(new Exceptions\EbookUnexpectedPlaceholderException()); + } + if($error->HasExceptions){ throw $error; } @@ -1289,6 +1338,7 @@ class Ebook{ /** * @throws Exceptions\ValidationException + * @throws Exceptions\DuplicateEbookException */ public function CreateOrUpdate(): void{ try{ @@ -1550,6 +1600,10 @@ class Ebook{ return $string; } + public function IsPlaceholder(): bool{ + return $this->WwwFilesystemPath === null; + } + /** * If the given list of elements has an element that is not `''`, return that value; otherwise, return `null`. * @@ -1572,10 +1626,19 @@ class Ebook{ /** * @throws Exceptions\ValidationException + * @throws Exceptions\DuplicateEbookException If an `Ebook` with the given identifier already exists. */ public function Create(): void{ $this->Validate(); + try{ + Ebook::GetByIdentifier($this->Identifier); + throw new Exceptions\DuplicateEbookException($this->Identifier); + } + catch(Exceptions\EbookNotFoundException){ + // Pass. + } + $this->CreateTags(); $this->CreateLocSubjects(); $this->CreateCollections(); @@ -1623,6 +1686,7 @@ class Ebook{ $this->AddSources(); $this->AddContributors(); $this->AddTocEntries(); + $this->AddEbookPlaceholder(); } /** @@ -1690,6 +1754,9 @@ class Ebook{ $this->RemoveTocEntries(); $this->AddTocEntries(); + + $this->RemoveEbookPlaceholder(); + $this->AddEbookPlaceholder(); } private function RemoveTags(): void{ @@ -1848,6 +1915,24 @@ class Ebook{ } } + private function RemoveEbookPlaceholder(): void{ + Db::Query(' + DELETE from EbookPlaceholders + where EbookId = ? + ', [$this->EbookId] + ); + } + + /** + * @throws Exceptions\ValidationException + */ + private function AddEbookPlaceholder(): void{ + if(isset($this->EbookPlaceholder)){ + $this->EbookPlaceholder->EbookId = $this->EbookId; + $this->EbookPlaceholder->Create(); + } + } + // *********** // ORM METHODS // *********** @@ -1929,6 +2014,9 @@ class Ebook{ } /** + * Queries for books in a collection. + * + * Puts ebooks without a `SequenceNumber` at the end of the list, which is more common in a collection with both published and placeholder ebooks. * @return array */ public static function GetAllByCollection(string $collection): array{ @@ -1938,13 +2026,16 @@ class Ebook{ inner join CollectionEbooks ce using (EbookId) inner join Collections c using (CollectionId) where c.UrlName = ? - order by ce.SequenceNumber, e.EbookCreated desc + order by ce.SequenceNumber is null, ce.SequenceNumber, e.EbookCreated desc ', [$collection], Ebook::class); return $ebooks; } /** + * Queries for related to books to be shown, e.g., in a carousel. + * + * Filters out placeholder books because they are not useful for browsing. * @return array */ public static function GetAllByRelated(Ebook $ebook, int $count, ?EbookTag $relatedTag): array{ @@ -1955,6 +2046,7 @@ class Ebook{ inner join EbookTags et using (EbookId) where et.TagId = ? and et.EbookId != ? + and e.WwwFilesystemPath is not null order by rand() limit ? ', [$relatedTag->TagId, $ebook->EbookId, $count], Ebook::class); @@ -1964,6 +2056,7 @@ class Ebook{ SELECT * from Ebooks where EbookId != ? + and WwwFilesystemPath is not null order by rand() limit ? ', [$ebook->EbookId, $count], Ebook::class); @@ -1977,25 +2070,43 @@ class Ebook{ * * @return array{ebooks: array, ebooksCount: int} */ - public static function GetAllByFilter(string $query = null, array $tags = [], Enums\EbookSortType $sort = null, int $page = 1, int $perPage = EBOOKS_PER_PAGE): array{ + public static function GetAllByFilter(string $query = null, array $tags = [], Enums\EbookSortType $sort = null, int $page = 1, int $perPage = EBOOKS_PER_PAGE, Enums\EbookReleaseStatusFilter $releaseStatusFilter = Enums\EbookReleaseStatusFilter::All): array{ $limit = $perPage; $offset = (($page - 1) * $perPage); $joinContributors = ''; $joinTags = ''; $params = []; - $whereCondition = 'where true'; + + switch($releaseStatusFilter){ + case Enums\EbookReleaseStatusFilter::Released: + $whereCondition = 'where e.WwwFilesystemPath is not null'; + break; + case Enums\EbookReleaseStatusFilter::Placeholder: + $whereCondition = 'where e.WwwFilesystemPath is null'; + break; + case Enums\EbookReleaseStatusFilter::All: + default: + if($query !== null && $query != ''){ + // If the query is present, show both released and placeholder ebooks. + $whereCondition = 'where true'; + }else{ + // If there is no query, hide placeholder ebooks. + $whereCondition = 'where e.WwwFilesystemPath is not null'; + } + break; + } $orderBy = 'e.EbookCreated desc'; if($sort == Enums\EbookSortType::AuthorAlpha){ $joinContributors = 'inner join Contributors con using (EbookId)'; $whereCondition .= ' and con.MarcRole = "aut"'; - $orderBy = 'con.SortName, e.EbookCreated desc'; + $orderBy = 'e.WwwFilesystemPath is null, con.SortName, e.EbookCreated desc'; // Put placeholders at the end } elseif($sort == Enums\EbookSortType::ReadingEase){ $orderBy = 'e.ReadingEase desc'; } elseif($sort == Enums\EbookSortType::Length){ - $orderBy = 'e.WordCount'; + $orderBy = 'e.WwwFilesystemPath is null, e.WordCount'; // Put placeholders at the end } if(sizeof($tags) > 0 && !in_array('all', $tags)){ // 0 tags means "all ebooks" diff --git a/lib/EbookPlaceholder.php b/lib/EbookPlaceholder.php new file mode 100644 index 00000000..653bac5d --- /dev/null +++ b/lib/EbookPlaceholder.php @@ -0,0 +1,88 @@ +_IsPublicDomain)){ + $this->_IsPublicDomain = $this->YearPublished === null ? true : $this->YearPublished <= PD_YEAR; + } + + return $this->_IsPublicDomain; + } + + public function FillFromHttpPost(): void{ + $this->PropertyFromHttp('YearPublished'); + $this->PropertyFromHttp('IsWanted'); + + // These properties apply only to books on the SE wanted list. + if($this->IsWanted){ + $this->PropertyFromHttp('Status'); + $this->PropertyFromHttp('Difficulty'); + $this->PropertyFromHttp('TranscriptionUrl'); + $this->PropertyFromHttp('IsPatron'); + $this->PropertyFromHttp('Notes'); + } + } + + /** + * @throws Exceptions\ValidationException + */ + public function Validate(): void{ + $thisYear = intval(NOW->format('Y')); + $error = new Exceptions\ValidationException(); + + if(isset($this->YearPublished) && ($this->YearPublished <= 0 || $this->YearPublished > $thisYear)){ + $error->Add(new Exceptions\InvalidEbookPlaceholderYearPublishedException()); + } + + $this->TranscriptionUrl = trim($this->TranscriptionUrl ?? ''); + if($this->TranscriptionUrl == ''){ + $this->TranscriptionUrl = null; + } + + $this->Notes = trim($this->Notes ?? ''); + if($this->Notes == ''){ + $this->Notes = null; + } + + if($error->HasExceptions){ + throw $error; + } + } + + /** + * @throws Exceptions\ValidationException + */ + public function Create(): void{ + $this->Validate(); + Db::Query(' + INSERT into EbookPlaceholders (EbookId, YearPublished, Status, Difficulty, TranscriptionUrl, + IsWanted, IsPatron, Notes) + values (?, + ?, + ?, + ?, + ?, + ?, + ?, + ?) + ', [$this->EbookId, $this->YearPublished, $this->Status, $this->Difficulty, $this->TranscriptionUrl, + $this->IsWanted, $this->IsPatron, $this->Notes]); + } +} diff --git a/lib/Enums/EbookPlaceholderDifficulty.php b/lib/Enums/EbookPlaceholderDifficulty.php new file mode 100644 index 00000000..6d633f2c --- /dev/null +++ b/lib/Enums/EbookPlaceholderDifficulty.php @@ -0,0 +1,8 @@ +message = 'Ebook already exists with identifier: ' . $identifier; + + parent::__construct($this->message); + } +} diff --git a/lib/Exceptions/EbookCreatedDatetimeRequiredException.php b/lib/Exceptions/EbookCreatedDatetimeRequiredException.php deleted file mode 100644 index bd3f3914..00000000 --- a/lib/Exceptions/EbookCreatedDatetimeRequiredException.php +++ /dev/null @@ -1,7 +0,0 @@ -IsPlaceholder()){ + continue; + } + $timestamp = $ebook->EbookCreated->format('Y-m'); $updatedTimestamp = $ebook->EbookUpdated->getTimestamp(); diff --git a/scripts/generate-feeds b/scripts/generate-feeds index 907e3a0b..ed45a09e 100755 --- a/scripts/generate-feeds +++ b/scripts/generate-feeds @@ -100,6 +100,10 @@ foreach($dirs as $dir){ // Iterate over all ebooks to build the various feeds. foreach(Ebook::GetAll() as $ebook){ + if($ebook->IsPlaceholder()){ + continue; + } + $allEbooks[] = $ebook; $newestEbooks[] = $ebook; diff --git a/templates/ArtworkForm.php b/templates/ArtworkForm.php index e668a1fd..dbf4dde4 100644 --- a/templates/ArtworkForm.php +++ b/templates/ArtworkForm.php @@ -12,7 +12,7 @@ $isEditForm = $isEditForm ?? false; ?>
Artist details -
Artwork details -
diff --git a/www/artworks/get.php b/www/artworks/get.php index db959911..477eb22e 100644 --- a/www/artworks/get.php +++ b/www/artworks/get.php @@ -102,7 +102,15 @@ catch(Exceptions\InvalidPermissionsException){ Tags - + +
    + Tags as $tag){ ?> +
  • + Name) ?> +
  • + +
+ Dimensions diff --git a/www/artworks/new.php b/www/artworks/new.php index e68bc531..8dc275f5 100644 --- a/www/artworks/new.php +++ b/www/artworks/new.php @@ -16,15 +16,14 @@ try{ throw new Exceptions\InvalidPermissionsException(); } - // We got here because an artwork was successfully submitted. if($isCreated){ + // We got here because an `Artwork` was successfully submitted. http_response_code(Enums\HttpCode::Created->value); $artwork = null; session_unset(); } - - // We got here because an artwork submission had errors and the user has to try again. - if($exception){ + elseif($exception){ + // We got here because an `Artwork` submission had errors and the user has to try again. http_response_code(Enums\HttpCode::UnprocessableContent->value); session_unset(); } diff --git a/www/css/artwork.css b/www/css/artwork.css index 5e61c243..6c7eae2e 100644 --- a/www/css/artwork.css +++ b/www/css/artwork.css @@ -166,6 +166,10 @@ form[action^="/artworks/"]{ grid-column: 1 / span 4; } +.year + label:has(input[type="checkbox"]){ + margin-top: .5rem; +} + label.picture::before{ bottom: 2.6rem; /* Fix alignment caused by "circa" checkbox */ } @@ -226,17 +230,6 @@ form.create-update-artwork fieldset p:first-of-type{ margin-top: 0; } -form.create-update-artwork legend{ - font-size: 1.4rem; - font-family: "League Spartan", Arial, sans-serif; - margin-top: 2rem; - margin-bottom: 1rem; - line-height: 1.2; - letter-spacing: 1px; - text-transform: uppercase; - border-bottom: none; -} - form.create-update-artwork label{ display: block; } diff --git a/www/css/core.css b/www/css/core.css index 4b52d829..65e262eb 100644 --- a/www/css/core.css +++ b/www/css/core.css @@ -840,7 +840,8 @@ label:has(input[type="checkbox"]):focus-within, select:focus, button:focus, nav a[rel]:focus, -textarea:focus{ +textarea:focus, +summary:focus{ outline: 1px dashed var(--input-outline); } @@ -1572,6 +1573,19 @@ ol.ebooks-list > li p.author a{ justify-content: center; } +.placeholder-cover{ + background-color: transparent; + border-radius: .25rem; + border: 2px dashed var(--sub-text); + height: 331px; + width: 221px; +} + +ol.ebooks-list.list .placeholder-cover{ + height: 191px; + width: 126px; +} + article nav ol, main nav.pagination ol{ list-style: none; @@ -1790,12 +1804,9 @@ input[type="file"]{ label:has(input[type="email"]) input, label:has(input[type="search"]) input, label:has(input[type="url"]) input, -label.captcha input, -label.year input, -label.tags input, -label.picture input, -label.user input, -label:has(input[type="password"]) input{ +label.icon input, +label:has(input[type="password"]) input, +label.icon select{ padding-left: 2.5rem; } @@ -1850,8 +1861,8 @@ label:has(input[type="radio"]):has(> span) input, label:has(input[type="checkbox"]):has(> span) input{ grid-row: 1 / span 2; justify-self: center; - align-self: start; - margin-top: 10px; + align-self: center; + margin: 0; margin-right: .5rem; } @@ -1895,11 +1906,7 @@ label:has(select) > span + span, label:has(input[type="email"]), label:has(input[type="search"]), label:has(input[type="url"]), -label.captcha, -label.year, -label.tags, -label.picture, -label.user, +label.icon, label:has(input[type="password"]){ display: block; position: relative; @@ -1908,11 +1915,7 @@ label:has(input[type="password"]){ label:has(input[type="email"])::before, label:has(input[type="search"])::before, -label.captcha::before, -label.year::before, -label.tags::before, -label.picture::before, -label.user::before, +label.icon::before, label:has(input[type="url"])::before, label:has(input[type="password"])::before{ display: block; @@ -1954,6 +1957,26 @@ label.year::before{ content: "\f073"; } +label.meter::before{ + content: "\f0e4"; +} + +label.collection::before{ + content: "\f0e8"; +} + +label.book::before{ + content: "\f02d"; +} + +label.ordered-list::before{ + content: "\f0cb"; +} + +label.hourglass::before{ + content: "\f254"; +} + label.tags:not(:has(select))::before{ content: "\f02c"; } @@ -2489,15 +2512,37 @@ form[action="/newsletter/subscriptions"] fieldset{ fieldset{ padding: 0; + margin: 0; border: none; } -fieldset p, -fieldset legend{ +label.controls-following-fieldset + fieldset, +details summary ~ *{ + margin-left: 1rem; +} + +fieldset p{ border-bottom: 1px dashed var(--input-border); } -fieldset legend, +fieldset legend{ + font-size: 1.4rem; + font-family: "League Spartan", Arial, sans-serif; + margin-top: 2rem; + margin-bottom: 1rem; + line-height: 1.2; + letter-spacing: 1px; + text-transform: uppercase; +} + +summary{ + cursor: pointer; +} + +summary + fieldset{ + margin-top: 1rem; +} + fieldset p:has(+ ul){ font-weight: bold; } @@ -2604,8 +2649,8 @@ aside header{ font-size: 1.5rem; } -.meter, -.progress{ +div.meter, +div.progress{ position: relative; font-size: 0; } @@ -2677,13 +2722,13 @@ aside header{ border-top: 1px solid #000; } -.progress + p{ +div.progress + p{ margin-top: 2rem; hyphens: auto; } -.meter p, -.progress p{ +div.meter p, +div.progress p{ font-size: 1rem; font-family: "League Spartan", Arial, sans-serif; left: 0; @@ -2701,7 +2746,7 @@ aside header{ margin-top: 0; } -.progress p.start{ +div.progress p.start{ margin-right: auto; margin-left: 1rem; border: none; @@ -2710,7 +2755,7 @@ aside header{ font-size: .75rem; } -.progress p.target{ +div.progress p.target{ margin-left: auto; margin-right: 1rem; border: none; @@ -2719,8 +2764,8 @@ aside header{ font-size: .75rem; } -.meter > div, -.progress > div{ +div.meter > div, +div.progress > div{ position: absolute; width: 100%; height: 100%; @@ -3121,6 +3166,74 @@ form[action="/settings"] label{ display: inline-block; } +ol.ebooks-list > li a.wanted, +ol.ebooks-list > li.wanted .placeholder-cover{ + position: relative; +} + +ol.ebooks-list > li.wanted .placeholder-cover::before, +ol.ebooks-list > li.wanted a::after{ + /* Ribbon */ + font-family: "League Spartan", sans-serif; + position: absolute; + left: -.5rem; + top: calc(.25rem + 3px); + padding: .5rem 1rem .5rem .5rem; + line-height: 1; + color: #ffffff; + text-shadow: 1px 1px 0 rgba(0, 0, 0, .75); + text-transform: uppercase; + font-size: .5rem; + white-space: nowrap; + font-weight: bold; + clip-path: polygon(0 0, 100% 0, 85% 100%, 0 100%); + z-index: 2; +} + +ol.ebooks-list > li[property="schema:hasPart"][value].wanted .placeholder-cover::before, +ol.ebooks-list > li[property="schema:hasPart"][value].wanted a::after{ + top: calc(.25rem + 3px + 2rem); +} + +ol.ebooks-list > li.wanted .placeholder-cover::after{ + /* Ribbon bottom wrap-around */ + content: ""; + position: absolute; + top: calc(.5rem + .5rem + .5rem + .5rem - 2px); + height: .5rem; + width: .5rem; + left: -.5rem; + clip-path: polygon(0 0, 100% 0, 100% 100%, 0 0); + z-index: 1; + filter: brightness(50%); +} + +ol.ebooks-list > li[property="schema:hasPart"][value].wanted .placeholder-cover::after{ + top: calc(.25rem + 3px + 2rem + .5rem + .5rem + .5rem); +} + +ol.ebooks-list > li.wanted a::after{ + /* Ribbon shadow */ + left: calc(-.5rem + 4px); + top: calc(.5rem + 2px); + background: rgba(0, 0, 0, .4); + z-index: auto; +} + +ol.ebooks-list > li[property="schema:hasPart"][value].wanted a::after{ + top: calc(.25rem + 3px + 2rem + 4px); +} + +ol.ebooks-list > li.wanted .placeholder-cover::before, +ol.ebooks-list > li.wanted .placeholder-cover::after{ + background: #861d1d; +} + +ol.ebooks-list > li.wanted .placeholder-cover::before, +ol.ebooks-list > li.wanted a::after{ + content: "wanted"; +} + @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]{ @@ -3717,13 +3830,13 @@ form[action="/settings"] label{ line-height: 1.4; } - .progress p.start, - .progress p.target, - .progress p.stretch-base{ + div.progress p.start, + div.progress p.target, + div.progress p.stretch-base{ display: none; } - .progress p{ + div.progress p{ border: none; background: transparent; } diff --git a/www/css/ebook-placeholder.css b/www/css/ebook-placeholder.css new file mode 100644 index 00000000..5e164e3d --- /dev/null +++ b/www/css/ebook-placeholder.css @@ -0,0 +1,92 @@ +form.create-update-ebook-placeholder fieldset{ + display: grid; + gap: 2rem; +} + +form.create-update-ebook-placeholder details + fieldset, +form.create-update-ebook-placeholder fieldset + fieldset{ + margin-top: 2rem; +} + +form.create-update-ebook-placeholder > fieldset:nth-of-type(1), +form.create-update-ebook-placeholder details:nth-of-type(1) fieldset{ + grid-template-columns: 1fr 1fr; +} + +form.create-update-ebook-placeholder > fieldset:nth-of-type(2), +form.create-update-ebook-placeholder details:nth-of-type(2) fieldset{ + grid-template-columns: 1fr 200px; +} + +form.create-update-ebook-placeholder fieldset label:has(input[name="ebook-placeholder-transcription-url"]), +form.create-update-ebook-placeholder fieldset label:has(textarea[name="ebook-placeholder-notes"]){ + grid-column: 1 / span 2; +} + +form.create-update-ebook-placeholder fieldset fieldset:has(input[name="sequence-number-collection-name-1"]), +form.create-update-ebook-placeholder fieldset fieldset:has(input[name="sequence-number-collection-name-2"]), +form.create-update-ebook-placeholder fieldset fieldset:has(input[name="sequence-number-collection-name-3"]), +form.create-update-ebook-placeholder fieldset fieldset:has(input[name="ebook-placeholder-year-published"]){ + display: grid; + grid-template-columns: 200px 1fr; + gap: 2rem; +} + +form.create-update-ebook-placeholder fieldset label:has(input[type="checkbox"]){ + grid-column: 1 / span 2; +} + +form.create-update-ebook-placeholder details{ + margin-top: 1rem; +} + +form.create-update-ebook-placeholder summary{ + font-style: italic; +} + +form.create-update-ebook-placeholder fieldset p{ + font-style: italic; + margin: 0; + border: none; +} + +form.create-update-ebook-placeholder fieldset p:first-of-type{ + margin-top: 0; +} + +form.create-update-ebook-placeholder legend{ + font-size: 1.4rem; + font-family: "League Spartan", Arial, sans-serif; + margin-bottom: 1rem; + line-height: 1.2; + letter-spacing: 1px; + text-transform: uppercase; +} + +form.create-update-ebook-placeholder label{ + display: block; +} + +form div.footer{ + margin-top: 1rem; + text-align: right; +} + +/* Hide the next fieldset unless the ebook-placeholder-is-wanted checkbox is checked. */ +form.create-update-ebook-placeholder fieldset:has(input[name="ebook-placeholder-is-wanted"]) fieldset{ + display: none; + grid-column: 1 / span 2; +} + +form.create-update-ebook-placeholder fieldset:has(input[name="ebook-placeholder-is-wanted"]:checked) fieldset{ + display: grid; +} + +article.ebook.ebook-placeholder > header{ + border: 2px dashed var(--sub-text); + border-top: 0; +} + +article.ebook.ebook-placeholder .placeholder-details{ + padding-top: 2rem; +} diff --git a/www/css/manual.css b/www/css/manual.css index 54cd0cff..e77d4274 100644 --- a/www/css/manual.css +++ b/www/css/manual.css @@ -349,7 +349,6 @@ code.full .utf{ } .step-by-step-guide summary{ - cursor: pointer; /* reproduce H2 styling: */ font-size: 1.4rem; font-family: "League Spartan", Arial, sans-serif; diff --git a/www/ebook-placeholders/get.php b/www/ebook-placeholders/get.php new file mode 100644 index 00000000..5455bd9c --- /dev/null +++ b/www/ebook-placeholders/get.php @@ -0,0 +1,88 @@ + strip_tags($ebook->TitleWithCreditsHtml), + 'css' => ['/css/ebook-placeholder.css'], + 'highlight' => 'ebooks', + 'canonicalUrl' => SITE_URL . $ebook->Url + ]) +?> +
+
+
+
+

Title) ?>

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

+ + Name) ?> + + NacoafUrl){ ?> + + + WikipediaUrl){ ?> + + + +

+ + +
+
+ + + +
+ EbookPlaceholder->IsPublicDomain){ ?> +

We don’t have this ebook in our catalog yet.

+

You can sponsor the production of this ebook and we’ll get working on it immediately!

+ EbookPlaceholder->YearPublished !== null){ ?> +

This book was published in EbookPlaceholder->YearPublished ?>, and will therefore enter the U.S. public domain on January 1, EbookPlaceholder->YearPublished + 96 ?>.

+

We can’t work on it any earlier than that.

+ +

This book is not yet in the U.S. public domain. We can’t offer it until it is.

+ +
+
+
+ diff --git a/www/ebook-placeholders/new.php b/www/ebook-placeholders/new.php new file mode 100644 index 00000000..5021c3e1 --- /dev/null +++ b/www/ebook-placeholders/new.php @@ -0,0 +1,62 @@ +Benefits->CanCreateEbookPlaceholders){ + throw new Exceptions\InvalidPermissionsException(); + } + + if($isCreated){ + // We got here because an `Ebook` was successfully created. + http_response_code(Enums\HttpCode::Created->value); + $createdEbook = $ebook; + $ebook = null; + session_unset(); + } + elseif($exception){ + // We got here because an `Ebook` submission had errors and the user has to try again. + http_response_code(Enums\HttpCode::UnprocessableContent->value); + session_unset(); + } +} +catch(Exceptions\LoginRequiredException){ + Template::RedirectToLogin(); +} +catch(Exceptions\InvalidPermissionsException){ + Template::Emit403(); // No permissions to create an ebook placeholder. +} +?> + 'Create an Ebook Placeholder', + 'css' => ['/css/ebook-placeholder.css'], + 'highlight' => '', + 'description' => 'Create a placeholder for an ebook not yet in the collection.' + ] +) ?> +
+
+

Create an Ebook Placeholder

+ + $exception]) ?> + + +

Ebook Placeholder created: Title) ?>

+ + + + $ebook]) ?> + +
+
+ diff --git a/www/ebook-placeholders/post.php b/www/ebook-placeholders/post.php new file mode 100644 index 00000000..e363d8cb --- /dev/null +++ b/www/ebook-placeholders/post.php @@ -0,0 +1,111 @@ +Benefits->CanCreateEbookPlaceholders){ + throw new Exceptions\InvalidPermissionsException(); + } + + $ebook = new Ebook(); + + $title = HttpInput::Str(POST, 'ebook-title'); + if(isset($title)){ + $ebook->Title = $title; + } + + $authors = []; + $authorFields = ['author-name-1', 'author-name-2', 'author-name-3']; + foreach($authorFields as $authorField){ + $authorName = HttpInput::Str(POST, $authorField); + if(!isset($authorName)){ + continue; + } + $author = new Contributor(); + $author->Name = $authorName; + $author->UrlName = Formatter::MakeUrlSafe($author->Name); + $author->MarcRole = Enums\MarcRole::Author; + $authors[] = $author; + } + $ebook->Authors = $authors; + + $translators = []; + $translatorFields = ['translator-name-1', 'translator-name-2']; + foreach($translatorFields as $translatorField){ + $translatorName = HttpInput::Str(POST, $translatorField); + if(!isset($translatorName)){ + continue; + } + $translator = new Contributor(); + $translator->Name = $translatorName; + $translator->UrlName = Formatter::MakeUrlSafe($translator->Name); + $translator->MarcRole = Enums\MarcRole::Translator; + $translators[] = $translator; + } + $ebook->Translators = $translators; + + $collectionMemberships = []; + $collectionNameFields = ['collection-name-1', 'collection-name-2', 'collection-name-3']; + foreach($collectionNameFields as $collectionNameField){ + $collectionName = HttpInput::Str(POST, $collectionNameField); + if(!isset($collectionName)){ + continue; + } + $collectionSequenceNumber = HttpInput::Int(POST, 'sequence-number-' . $collectionNameField); + $collection = Collection::FromName($collectionName); + + $cm = new CollectionMembership(); + $cm->Collection = $collection; + $cm->SequenceNumber = $collectionSequenceNumber; + $collectionMemberships[] = $cm; + } + $ebook->CollectionMemberships = $collectionMemberships; + + $ebookPlaceholder = new EbookPlaceholder(); + $ebookPlaceholder->FillFromHttpPost(); + $ebook->EbookPlaceholder = $ebookPlaceholder; + + $ebook->FillIdentifierFromTitleAndContributors(); + try{ + $existingEbook = Ebook::GetByIdentifier($ebook->Identifier); + throw new Exceptions\DuplicateEbookException($ebook->Identifier); + } + catch(Exceptions\EbookNotFoundException){ + // Pass and create the placeholder. There is no existing ebook with this identifier. + } + + // These properties must be set before calling `Ebook::Create()` to prevent the getters from triggering DB queries or accessing `Ebook::$EbookId` before it is set. + $ebook->Tags = []; + $ebook->LocSubjects = []; + $ebook->Illustrators = []; + $ebook->Contributors = []; + $ebook->Create(); + + $_SESSION['ebook'] = $ebook; + $_SESSION['is-ebook-placeholder-created'] = true; + + http_response_code(Enums\HttpCode::SeeOther->value); + header('Location: /ebook-placeholders/new'); + } +} +catch(Exceptions\LoginRequiredException){ + Template::RedirectToLogin(); +} +catch(Exceptions\InvalidPermissionsException | Exceptions\InvalidHttpMethodException | Exceptions\HttpMethodNotAllowedException){ + Template::Emit403(); +} +catch(Exceptions\AppException $ex){ + $_SESSION['ebook'] = $ebook; + $_SESSION['exception'] = $ex; + + http_response_code(Enums\HttpCode::SeeOther->value); + header('Location: ' . $exceptionRedirectUrl); +} diff --git a/www/ebooks/download.php b/www/ebooks/download.php index 3eb7cb10..c8a3fa2a 100644 --- a/www/ebooks/download.php +++ b/www/ebooks/download.php @@ -13,6 +13,10 @@ try{ $identifier = EBOOKS_IDENTIFIER_PREFIX . $urlPath; $ebook = Ebook::GetByIdentifier($identifier); + if($ebook->IsPlaceholder()){ + throw new Exceptions\InvalidFileException(); + } + $format = Enums\EbookFormatType::tryFrom(HttpInput::Str(GET, 'format') ?? '') ?? Enums\EbookFormatType::Epub; switch($format){ case Enums\EbookFormatType::Kepub: diff --git a/www/ebooks/get.php b/www/ebooks/get.php index deba68aa..c8b5c1e3 100644 --- a/www/ebooks/get.php +++ b/www/ebooks/get.php @@ -19,6 +19,11 @@ try{ $ebook = Ebook::GetByIdentifier($identifier); + if($ebook->IsPlaceholder()){ + require('/standardebooks.org/web/www/ebook-placeholders/get.php'); + exit(); + } + // Divide our sources into transcriptions and scans. foreach($ebook->Sources as $source){ switch($source->Type){ diff --git a/www/ebooks/index.php b/www/ebooks/index.php index 45ed8829..6254efcc 100644 --- a/www/ebooks/index.php +++ b/www/ebooks/index.php @@ -34,7 +34,7 @@ try{ $tags = []; } - $result = Ebook::GetAllByFilter($query != '' ? $query : null, $tags, $sort, $page, $perPage); + $result = Ebook::GetAllByFilter($query != '' ? $query : null, $tags, $sort, $page, $perPage, Enums\EbookReleaseStatusFilter::All); $ebooks = $result['ebooks']; $totalEbooks = $result['ebooksCount']; $pageTitle = 'Browse Standard Ebooks'; diff --git a/www/feeds/atom/search.php b/www/feeds/atom/search.php index e5e1ad97..ca82f6dd 100644 --- a/www/feeds/atom/search.php +++ b/www/feeds/atom/search.php @@ -7,7 +7,7 @@ try{ $count = HttpInput::Int(GET, 'per-page') ?? EBOOKS_PER_PAGE; if($query !== ''){ - $ebooks = Ebook::GetAllByFilter($query, [], Enums\EbookSortType::Newest, $startPage, $count)['ebooks']; + $ebooks = Ebook::GetAllByFilter($query, [], Enums\EbookSortType::Newest, $startPage, $count, Enums\EbookReleaseStatusFilter::Released)['ebooks']; } } catch(\Exception){ diff --git a/www/feeds/opds/search.php b/www/feeds/opds/search.php index a138f422..63a5bc5f 100644 --- a/www/feeds/opds/search.php +++ b/www/feeds/opds/search.php @@ -7,7 +7,7 @@ try{ $count = HttpInput::Int(GET, 'per-page') ?? EBOOKS_PER_PAGE; if($query !== ''){ - $ebooks = Ebook::GetAllByFilter($query, [], Enums\EbookSortType::Newest, $startPage, $count)['ebooks']; + $ebooks = Ebook::GetAllByFilter($query, [], Enums\EbookSortType::Newest, $startPage, $count, Enums\EbookReleaseStatusFilter::Released)['ebooks']; } } catch(\Exception){ diff --git a/www/feeds/rss/search.php b/www/feeds/rss/search.php index ee7a4c9f..5ddd169f 100644 --- a/www/feeds/rss/search.php +++ b/www/feeds/rss/search.php @@ -7,7 +7,7 @@ try{ $count = HttpInput::Int(GET, 'per-page') ?? EBOOKS_PER_PAGE; if($query !== ''){ - $ebooks = Ebook::GetAllByFilter($query, [], Enums\EbookSortType::Newest, $startPage, $count)['ebooks']; + $ebooks = Ebook::GetAllByFilter($query, [], Enums\EbookSortType::Newest, $startPage, $count, Enums\EbookReleaseStatusFilter::Released)['ebooks']; } } catch(\Exception){ diff --git a/www/fonts/fork-awesome-subset.woff2 b/www/fonts/fork-awesome-subset.woff2 index 3c302eef..61e78d74 100644 Binary files a/www/fonts/fork-awesome-subset.woff2 and b/www/fonts/fork-awesome-subset.woff2 differ diff --git a/www/newsletter/subscriptions/new.php b/www/newsletter/subscriptions/new.php index 3ad584a6..12fce89b 100644 --- a/www/newsletter/subscriptions/new.php +++ b/www/newsletter/subscriptions/new.php @@ -36,7 +36,7 @@ if($exception){ -