diff --git a/app/BookShelf.php b/app/Bookshelf.php similarity index 88% rename from app/BookShelf.php rename to app/Bookshelf.php index 47f873bcd..1e33e31f6 100644 --- a/app/BookShelf.php +++ b/app/Bookshelf.php @@ -1,7 +1,7 @@ <?php namespace BookStack; -class BookShelf extends Entity +class Bookshelf extends Entity { protected $table = 'bookshelves'; @@ -9,6 +9,15 @@ class BookShelf extends Entity protected $fillable = ['name', 'description', 'image_id']; + /** + * Get the books in this shelf. + * @return \Illuminate\Database\Eloquent\Relations\BelongsToMany + */ + public function books() + { + return $this->belongsToMany(Book::class, 'bookshelves_books', 'bookshelf_id', 'book_id'); + } + /** * Get the url for this bookshelf. * @param string|bool $path diff --git a/app/Http/Controllers/BookshelfController.php b/app/Http/Controllers/BookshelfController.php new file mode 100644 index 000000000..a1c56f29a --- /dev/null +++ b/app/Http/Controllers/BookshelfController.php @@ -0,0 +1,341 @@ +<?php namespace BookStack\Http\Controllers; + +use Activity; +use BookStack\Book; +use BookStack\Repos\EntityRepo; +use BookStack\Repos\UserRepo; +use BookStack\Services\ExportService; +use Illuminate\Http\Request; +use Illuminate\Http\Response; +use Views; + +class BookshelfController extends Controller +{ + + protected $entityRepo; + protected $userRepo; + protected $exportService; + + /** + * BookController constructor. + * @param EntityRepo $entityRepo + * @param UserRepo $userRepo + * @param ExportService $exportService + */ + public function __construct(EntityRepo $entityRepo, UserRepo $userRepo, ExportService $exportService) + { + $this->entityRepo = $entityRepo; + $this->userRepo = $userRepo; + $this->exportService = $exportService; + parent::__construct(); + } + + /** + * Display a listing of the book. + * @return Response + */ + public function index() + { + $shelves = $this->entityRepo->getAllPaginated('bookshelf', 18); + $recents = $this->signedIn ? $this->entityRepo->getRecentlyViewed('bookshelf', 4, 0) : false; + $popular = $this->entityRepo->getPopular('bookshelf', 4, 0); + $new = $this->entityRepo->getRecentlyCreated('bookshelf', 4, 0); + $shelvesViewType = setting()->getUser($this->currentUser, 'bookshelves_view_type', config('app.views.bookshelves', 'grid')); + $this->setPageTitle(trans('entities.shelves')); + return view('shelves/index', [ + 'shelves' => $shelves, + 'recents' => $recents, + 'popular' => $popular, + 'new' => $new, + 'shelvesViewType' => $shelvesViewType + ]); + } + + /** + * Show the form for creating a new bookshelf. + * @return Response + */ + public function create() + { + $this->checkPermission('bookshelf-create-all'); + $this->setPageTitle(trans('entities.shelves_create')); + $books = $this->entityRepo->getAll('book', false, 'update'); + return view('shelves/create', ['books' => $books]); + } + + /** + * Store a newly created book in storage. + * @param Request $request + * @return Response + */ + public function store(Request $request) + { + $this->checkPermission('bookshelf-create-all'); + $this->validate($request, [ + 'name' => 'required|string|max:255', + 'description' => 'string|max:1000', + ]); + + $bookshelf = $this->entityRepo->createFromInput('bookshelf', $request->all()); + $this->entityRepo->updateShelfBooks($bookshelf, $request->get('books', '')); + Activity::add($bookshelf, 'bookshelf_create'); + + return redirect($bookshelf->getUrl()); + } + +// +// /** +// * Display the specified book. +// * @param $slug +// * @return Response +// */ +// public function show($slug) +// { +// $book = $this->entityRepo->getBySlug('book', $slug); +// $this->checkOwnablePermission('book-view', $book); +// $bookChildren = $this->entityRepo->getBookChildren($book); +// Views::add($book); +// $this->setPageTitle($book->getShortName()); +// return view('books/show', [ +// 'book' => $book, +// 'current' => $book, +// 'bookChildren' => $bookChildren, +// 'activity' => Activity::entityActivity($book, 20, 0) +// ]); +// } +// +// /** +// * Show the form for editing the specified book. +// * @param $slug +// * @return Response +// */ +// public function edit($slug) +// { +// $book = $this->entityRepo->getBySlug('book', $slug); +// $this->checkOwnablePermission('book-update', $book); +// $this->setPageTitle(trans('entities.books_edit_named', ['bookName'=>$book->getShortName()])); +// return view('books/edit', ['book' => $book, 'current' => $book]); +// } +// +// /** +// * Update the specified book in storage. +// * @param Request $request +// * @param $slug +// * @return Response +// */ +// public function update(Request $request, $slug) +// { +// $book = $this->entityRepo->getBySlug('book', $slug); +// $this->checkOwnablePermission('book-update', $book); +// $this->validate($request, [ +// 'name' => 'required|string|max:255', +// 'description' => 'string|max:1000' +// ]); +// $book = $this->entityRepo->updateFromInput('book', $book, $request->all()); +// Activity::add($book, 'book_update', $book->id); +// return redirect($book->getUrl()); +// } +// +// /** +// * Shows the page to confirm deletion +// * @param $bookSlug +// * @return \Illuminate\View\View +// */ +// public function showDelete($bookSlug) +// { +// $book = $this->entityRepo->getBySlug('book', $bookSlug); +// $this->checkOwnablePermission('book-delete', $book); +// $this->setPageTitle(trans('entities.books_delete_named', ['bookName'=>$book->getShortName()])); +// return view('books/delete', ['book' => $book, 'current' => $book]); +// } +// +// /** +// * Shows the view which allows pages to be re-ordered and sorted. +// * @param string $bookSlug +// * @return \Illuminate\View\View +// */ +// public function sort($bookSlug) +// { +// $book = $this->entityRepo->getBySlug('book', $bookSlug); +// $this->checkOwnablePermission('book-update', $book); +// $bookChildren = $this->entityRepo->getBookChildren($book, true); +// $books = $this->entityRepo->getAll('book', false, 'update'); +// $this->setPageTitle(trans('entities.books_sort_named', ['bookName'=>$book->getShortName()])); +// return view('books/sort', ['book' => $book, 'current' => $book, 'books' => $books, 'bookChildren' => $bookChildren]); +// } +// +// /** +// * Shows the sort box for a single book. +// * Used via AJAX when loading in extra books to a sort. +// * @param $bookSlug +// * @return \Illuminate\Contracts\View\Factory|\Illuminate\View\View +// */ +// public function getSortItem($bookSlug) +// { +// $book = $this->entityRepo->getBySlug('book', $bookSlug); +// $bookChildren = $this->entityRepo->getBookChildren($book); +// return view('books/sort-box', ['book' => $book, 'bookChildren' => $bookChildren]); +// } +// +// /** +// * Saves an array of sort mapping to pages and chapters. +// * @param string $bookSlug +// * @param Request $request +// * @return \Illuminate\Http\RedirectResponse|\Illuminate\Routing\Redirector +// */ +// public function saveSort($bookSlug, Request $request) +// { +// $book = $this->entityRepo->getBySlug('book', $bookSlug); +// $this->checkOwnablePermission('book-update', $book); +// +// // Return if no map sent +// if (!$request->filled('sort-tree')) { +// return redirect($book->getUrl()); +// } +// +// // Sort pages and chapters +// $sortMap = collect(json_decode($request->get('sort-tree'))); +// $bookIdsInvolved = collect([$book->id]); +// +// // Load models into map +// $sortMap->each(function ($mapItem) use ($bookIdsInvolved) { +// $mapItem->type = ($mapItem->type === 'page' ? 'page' : 'chapter'); +// $mapItem->model = $this->entityRepo->getById($mapItem->type, $mapItem->id); +// // Store source and target books +// $bookIdsInvolved->push(intval($mapItem->model->book_id)); +// $bookIdsInvolved->push(intval($mapItem->book)); +// }); +// +// // Get the books involved in the sort +// $bookIdsInvolved = $bookIdsInvolved->unique()->toArray(); +// $booksInvolved = $this->entityRepo->book->newQuery()->whereIn('id', $bookIdsInvolved)->get(); +// // Throw permission error if invalid ids or inaccessible books given. +// if (count($bookIdsInvolved) !== count($booksInvolved)) { +// $this->showPermissionError(); +// } +// // Check permissions of involved books +// $booksInvolved->each(function (Book $book) { +// $this->checkOwnablePermission('book-update', $book); +// }); +// +// // Perform the sort +// $sortMap->each(function ($mapItem) { +// $model = $mapItem->model; +// +// $priorityChanged = intval($model->priority) !== intval($mapItem->sort); +// $bookChanged = intval($model->book_id) !== intval($mapItem->book); +// $chapterChanged = ($mapItem->type === 'page') && intval($model->chapter_id) !== $mapItem->parentChapter; +// +// if ($bookChanged) { +// $this->entityRepo->changeBook($mapItem->type, $mapItem->book, $model); +// } +// if ($chapterChanged) { +// $model->chapter_id = intval($mapItem->parentChapter); +// $model->save(); +// } +// if ($priorityChanged) { +// $model->priority = intval($mapItem->sort); +// $model->save(); +// } +// }); +// +// // Rebuild permissions and add activity for involved books. +// $booksInvolved->each(function (Book $book) { +// $this->entityRepo->buildJointPermissionsForBook($book); +// Activity::add($book, 'book_sort', $book->id); +// }); +// +// return redirect($book->getUrl()); +// } +// +// /** +// * Remove the specified book from storage. +// * @param $bookSlug +// * @return Response +// */ +// public function destroy($bookSlug) +// { +// $book = $this->entityRepo->getBySlug('book', $bookSlug); +// $this->checkOwnablePermission('book-delete', $book); +// Activity::addMessage('book_delete', 0, $book->name); +// $this->entityRepo->destroyBook($book); +// return redirect('/books'); +// } +// +// /** +// * Show the Restrictions view. +// * @param $bookSlug +// * @return \Illuminate\Contracts\View\Factory|\Illuminate\View\View +// */ +// public function showRestrict($bookSlug) +// { +// $book = $this->entityRepo->getBySlug('book', $bookSlug); +// $this->checkOwnablePermission('restrictions-manage', $book); +// $roles = $this->userRepo->getRestrictableRoles(); +// return view('books/restrictions', [ +// 'book' => $book, +// 'roles' => $roles +// ]); +// } +// +// /** +// * Set the restrictions for this book. +// * @param $bookSlug +// * @param $bookSlug +// * @param Request $request +// * @return \Illuminate\Http\RedirectResponse|\Illuminate\Routing\Redirector +// */ +// public function restrict($bookSlug, Request $request) +// { +// $book = $this->entityRepo->getBySlug('book', $bookSlug); +// $this->checkOwnablePermission('restrictions-manage', $book); +// $this->entityRepo->updateEntityPermissionsFromRequest($request, $book); +// session()->flash('success', trans('entities.books_permissions_updated')); +// return redirect($book->getUrl()); +// } +// +// /** +// * Export a book as a PDF file. +// * @param string $bookSlug +// * @return mixed +// */ +// public function exportPdf($bookSlug) +// { +// $book = $this->entityRepo->getBySlug('book', $bookSlug); +// $pdfContent = $this->exportService->bookToPdf($book); +// return response()->make($pdfContent, 200, [ +// 'Content-Type' => 'application/octet-stream', +// 'Content-Disposition' => 'attachment; filename="' . $bookSlug . '.pdf' +// ]); +// } +// +// /** +// * Export a book as a contained HTML file. +// * @param string $bookSlug +// * @return mixed +// */ +// public function exportHtml($bookSlug) +// { +// $book = $this->entityRepo->getBySlug('book', $bookSlug); +// $htmlContent = $this->exportService->bookToContainedHtml($book); +// return response()->make($htmlContent, 200, [ +// 'Content-Type' => 'application/octet-stream', +// 'Content-Disposition' => 'attachment; filename="' . $bookSlug . '.html' +// ]); +// } +// +// /** +// * Export a book as a plain text file. +// * @param $bookSlug +// * @return mixed +// */ +// public function exportPlainText($bookSlug) +// { +// $book = $this->entityRepo->getBySlug('book', $bookSlug); +// $htmlContent = $this->exportService->bookToPlainText($book); +// return response()->make($htmlContent, 200, [ +// 'Content-Type' => 'application/octet-stream', +// 'Content-Disposition' => 'attachment; filename="' . $bookSlug . '.txt' +// ]); +// } +} diff --git a/app/Http/Controllers/UserController.php b/app/Http/Controllers/UserController.php index d50baa86f..f6bd13e6f 100644 --- a/app/Http/Controllers/UserController.php +++ b/app/Http/Controllers/UserController.php @@ -252,7 +252,7 @@ class UserController extends Controller return $this->currentUser->id == $id; }); - $viewType = $request->get('book_view_type'); + $viewType = $request->get('view_type'); if (!in_array($viewType, ['grid', 'list'])) { $viewType = 'list'; } @@ -262,4 +262,27 @@ class UserController extends Controller return redirect()->back(302, [], "/settings/users/$id"); } + + /** + * Update the user's preferred shelf-list display setting. + * @param $id + * @param Request $request + * @return \Illuminate\Http\RedirectResponse + */ + public function switchShelfView($id, Request $request) + { + $this->checkPermissionOr('users-manage', function () use ($id) { + return $this->currentUser->id == $id; + }); + + $viewType = $request->get('view_type'); + if (!in_array($viewType, ['grid', 'list'])) { + $viewType = 'list'; + } + + $user = $this->user->findOrFail($id); + setting()->putUser($user, 'bookshelves_view_type', $viewType); + + return redirect()->back(302, [], "/settings/users/$id"); + } } diff --git a/app/Repos/EntityRepo.php b/app/Repos/EntityRepo.php index bdd1e37b1..ea7fc4882 100644 --- a/app/Repos/EntityRepo.php +++ b/app/Repos/EntityRepo.php @@ -1,6 +1,7 @@ <?php namespace BookStack\Repos; use BookStack\Book; +use BookStack\Bookshelf; use BookStack\Chapter; use BookStack\Entity; use BookStack\Exceptions\NotFoundException; @@ -18,6 +19,10 @@ use Illuminate\Support\Collection; class EntityRepo { + /** + * @var Bookshelf + */ + public $bookshelf; /** * @var Book $book @@ -67,6 +72,7 @@ class EntityRepo /** * EntityRepo constructor. + * @param Bookshelf $bookshelf * @param Book $book * @param Chapter $chapter * @param Page $page @@ -77,6 +83,7 @@ class EntityRepo * @param SearchService $searchService */ public function __construct( + Bookshelf $bookshelf, Book $book, Chapter $chapter, Page $page, @@ -86,11 +93,13 @@ class EntityRepo TagRepo $tagRepo, SearchService $searchService ) { + $this->bookshelf = $bookshelf; $this->book = $book; $this->chapter = $chapter; $this->page = $page; $this->pageRevision = $pageRevision; $this->entities = [ + 'bookshelf' => $this->bookshelf, 'page' => $this->page, 'chapter' => $this->chapter, 'book' => $this->book @@ -533,6 +542,23 @@ class EntityRepo return $entityModel; } + /** + * Sync the books assigned to a shelf from a comma-separated list + * of book IDs. + * @param Bookshelf $shelf + * @param string $books + */ + public function updateShelfBooks(Bookshelf $shelf, string $books) + { + $ids = explode(',', $books); + if (count($ids) === 0) { + return; + } + + $bookIds = $this->entityQuery('book')->whereIn('id', $ids)->get(['id'])->pluck('id'); + $shelf->books()->sync($bookIds); + } + /** * Change the book that an entity belongs to. * @param string $type @@ -1157,6 +1183,8 @@ class EntityRepo /** * Destroy the provided book and all its child entities. * @param Book $book + * @throws NotifyException + * @throws \Throwable */ public function destroyBook(Book $book) { @@ -1177,6 +1205,7 @@ class EntityRepo /** * Destroy a chapter and its relations. * @param Chapter $chapter + * @throws \Throwable */ public function destroyChapter(Chapter $chapter) { @@ -1198,6 +1227,7 @@ class EntityRepo * Destroy a given page along with its dependencies. * @param Page $page * @throws NotifyException + * @throws \Throwable */ public function destroyPage(Page $page) { diff --git a/app/Services/PermissionService.php b/app/Services/PermissionService.php index 0dd316b34..428cb895f 100644 --- a/app/Services/PermissionService.php +++ b/app/Services/PermissionService.php @@ -204,6 +204,7 @@ class PermissionService /** * Rebuild the entity jointPermissions for a particular entity. * @param Entity $entity + * @throws \Throwable */ public function buildJointPermissionsForEntity(Entity $entity) { @@ -214,7 +215,9 @@ class PermissionService return; } - $entities[] = $entity->book; + if ($entity->book) { + $entities[] = $entity->book; + } if ($entity->isA('page') && $entity->chapter_id) { $entities[] = $entity->chapter; @@ -226,13 +229,13 @@ class PermissionService } } - $this->deleteManyJointPermissionsForEntities($entities); $this->buildJointPermissionsForEntities(collect($entities)); } /** * Rebuild the entity jointPermissions for a collection of entities. * @param Collection $entities + * @throws \Throwable */ public function buildJointPermissionsForEntities(Collection $entities) { @@ -412,7 +415,7 @@ class PermissionService return $this->createJointPermissionDataArray($entity, $role, $action, $hasAccess, $hasAccess); } - if ($entity->isA('book')) { + if ($entity->isA('book') || $entity->isA('bookshelf')) { return $this->createJointPermissionDataArray($entity, $role, $action, $roleHasPermission, $roleHasPermissionOwn); } diff --git a/database/migrations/2018_08_04_115700_create_bookshelves_table.php b/database/migrations/2018_08_04_115700_create_bookshelves_table.php index f32a1cdfb..c7840e1a1 100644 --- a/database/migrations/2018_08_04_115700_create_bookshelves_table.php +++ b/database/migrations/2018_08_04_115700_create_bookshelves_table.php @@ -30,6 +30,18 @@ class CreateBookshelvesTable extends Migration $table->index('restricted'); }); + Schema::create('bookshelves_books', function (Blueprint $table) { + $table->integer('bookshelf_id')->unsigned(); + $table->integer('book_id')->unsigned(); + + $table->foreign('bookshelf_id')->references('id')->on('bookshelves') + ->onUpdate('cascade')->onDelete('cascade'); + $table->foreign('book_id')->references('id')->on('books') + ->onUpdate('cascade')->onDelete('cascade'); + + $table->primary(['bookshelf_id', 'book_id']); + }); + // Copy existing role permissions from Books $ops = ['View All', 'View Own', 'Create All', 'Create Own', 'Update All', 'Update Own', 'Delete All', 'Delete Own']; foreach ($ops as $op) { @@ -75,5 +87,6 @@ class CreateBookshelvesTable extends Migration // Drop shelves table Schema::dropIfExists('bookshelves'); + Schema::dropIfExists('bookshelves_books'); } } diff --git a/package-lock.json b/package-lock.json index ec4da5ce2..f8c43993b 100644 --- a/package-lock.json +++ b/package-lock.json @@ -5857,6 +5857,21 @@ "resolved": "https://registry.npmjs.org/jquery/-/jquery-3.3.1.tgz", "integrity": "sha512-Ubldcmxp5np52/ENotGxlLe6aGMvmF4R8S6tZjsP6Knsaxd/xp3Zrh50cG93lR6nPXyUFwzN3ZSOQI0wRJNdGg==" }, + "jquery-sortable": { + "version": "0.9.13", + "resolved": "https://registry.npmjs.org/jquery-sortable/-/jquery-sortable-0.9.13.tgz", + "integrity": "sha1-HL+2VQE6B0c3BXHwbiL1JKAP+6I=", + "requires": { + "jquery": "^2.1.2" + }, + "dependencies": { + "jquery": { + "version": "2.2.4", + "resolved": "https://registry.npmjs.org/jquery/-/jquery-2.2.4.tgz", + "integrity": "sha1-LInWiJterFIqfuoywUUhVZxsvwI=" + } + } + }, "js-base64": { "version": "2.4.3", "resolved": "https://registry.npmjs.org/js-base64/-/js-base64-2.4.3.tgz", diff --git a/package.json b/package.json index 12d972cf9..58f2dad5e 100644 --- a/package.json +++ b/package.json @@ -33,6 +33,7 @@ "codemirror": "^5.26.0", "dropzone": "^5.4.0", "jquery": "^3.3.1", + "jquery-sortable": "^0.9.13", "markdown-it": "^8.3.1", "markdown-it-task-lists": "^2.0.0", "vue": "^2.2.6", diff --git a/resources/assets/js/components/index.js b/resources/assets/js/components/index.js index aa69f3265..e1aef032c 100644 --- a/resources/assets/js/components/index.js +++ b/resources/assets/js/components/index.js @@ -18,6 +18,7 @@ let componentMapping = { 'collapsible': require('./collapsible'), 'toggle-switch': require('./toggle-switch'), 'page-display': require('./page-display'), + 'shelf-sort': require('./shelf-sort'), }; window.components = {}; diff --git a/resources/assets/js/components/shelf-sort.js b/resources/assets/js/components/shelf-sort.js new file mode 100644 index 000000000..91713ab41 --- /dev/null +++ b/resources/assets/js/components/shelf-sort.js @@ -0,0 +1,41 @@ + +class ShelfSort { + + constructor(elem) { + this.elem = elem; + this.sortGroup = this.initSortable(); + this.input = document.getElementById('books-input'); + } + + initSortable() { + const sortable = require('jquery-sortable'); + const placeHolderContent = this.getPlaceholderHTML(); + + return $('.scroll-box').sortable({ + group: 'shelf-books', + exclude: '.instruction,.scroll-box-placeholder', + containerSelector: 'div.scroll-box', + itemSelector: '.scroll-box-item', + placeholder: placeHolderContent, + onDrop: this.onDrop.bind(this) + }); + } + + onDrop($item, container, _super) { + const data = this.sortGroup.sortable('serialize').get(); + this.input.value = data[0].map(item => item.id).join(','); + _super($item, container); + } + + getPlaceholderHTML() { + const placeHolder = document.querySelector('.scroll-box-placeholder'); + placeHolder.style.display = 'block'; + const placeHolderContent = placeHolder.outerHTML; + placeHolder.style.display = 'none'; + return placeHolderContent; + } + + +} + +module.exports = ShelfSort; \ No newline at end of file diff --git a/resources/assets/sass/_grid.scss b/resources/assets/sass/_grid.scss index 8f15153b5..0e1f85ce6 100644 --- a/resources/assets/sass/_grid.scss +++ b/resources/assets/sass/_grid.scss @@ -192,8 +192,26 @@ div[class^="col-"] img { flex-direction: column; border: 1px solid #ddd; min-width: 100px; + h2 { + width: 100%; + font-size: 1.5em; + margin: 0 0 10px; + } + h2 a { + display: block; + width: 100%; + line-height: 1.2; + text-decoration: none; + } + p { + font-size: .85em; + margin: 0; + line-height: 1.6em; + } .grid-card-content { flex: 1; + border-top: 0; + border-bottom-width: 2px; } .grid-card-content, .grid-card-footer { padding: $-l; @@ -203,6 +221,23 @@ div[class^="col-"] img { } } +.book-grid-item .grid-card-content h2 a { + color: $color-book; + fill: $color-book; +} + +.bookshelf-grid-item .grid-card-content h2 a { + color: $color-bookshelf; + fill: $color-bookshelf; +} + +.book-grid-item .grid-card-footer { + p.small { + font-size: .8em; + margin: 0; + } +} + @include smaller-than($m) { .grid.third { grid-template-columns: 1fr 1fr; diff --git a/resources/assets/sass/_lists.scss b/resources/assets/sass/_lists.scss index 3338b3938..0afed4b36 100644 --- a/resources/assets/sass/_lists.scss +++ b/resources/assets/sass/_lists.scss @@ -408,32 +408,3 @@ ul.pagination { } } -.book-grid-item .grid-card-content { - border-top: 0; - border-bottom-width: 2px; - h2 { - width: 100%; - font-size: 1.5em; - margin: 0 0 10px; - } - h2 a { - display: block; - width: 100%; - line-height: 1.2; - color: #009688;; - fill: #009688;; - text-decoration: none; - } - p { - font-size: .85em; - margin: 0; - line-height: 1.6em; - } -} - -.book-grid-item .grid-card-footer { - p.small { - font-size: .8em; - margin: 0; - } -} \ No newline at end of file diff --git a/resources/assets/sass/_text.scss b/resources/assets/sass/_text.scss index da11846d8..63a91c968 100644 --- a/resources/assets/sass/_text.scss +++ b/resources/assets/sass/_text.scss @@ -281,6 +281,14 @@ p.secondary, p .secondary, span.secondary, .text-secondary { } } +.text-bookshelf { + color: $color-bookshelf; + fill: $color-bookshelf; + &:hover { + color: $color-bookshelf; + fill: $color-bookshelf; + } +} .text-book { color: $color-book; fill: $color-book; diff --git a/resources/assets/sass/_variables.scss b/resources/assets/sass/_variables.scss index e62d37efe..006d1b3f0 100644 --- a/resources/assets/sass/_variables.scss +++ b/resources/assets/sass/_variables.scss @@ -47,6 +47,7 @@ $warning: $secondary; $primary-faded: rgba(21, 101, 192, 0.15); // Item Colors +$color-bookshelf: #af5a5a; $color-book: #009688; $color-chapter: #ef7c3c; $color-page: $primary; diff --git a/resources/assets/sass/styles.scss b/resources/assets/sass/styles.scss index 0b2dfbf75..ab5972cbd 100644 --- a/resources/assets/sass/styles.scss +++ b/resources/assets/sass/styles.scss @@ -206,6 +206,12 @@ $btt-size: 40px; transition: all ease-in-out 120ms; cursor: pointer; } + &.compact { + font-size: 10px; + .entity-item-snippet { + display: none; + } + } } .entity-list-item.selected { @@ -214,6 +220,20 @@ $btt-size: 40px; } } +.scroll-box { + max-height: 250px; + overflow-y: scroll; + border: 1px solid #DDD; + border-radius: 3px; + .scroll-box-item { + padding: $-xs $-m; + border-bottom: 1px solid #DDD; + &:last-child { + border-bottom: 0; + } + } +} + .center-box { margin: $-xxl auto 0 auto; width: 420px; diff --git a/resources/lang/en/entities.php b/resources/lang/en/entities.php index 834b977e7..c744c5557 100644 --- a/resources/lang/en/entities.php +++ b/resources/lang/en/entities.php @@ -69,6 +69,16 @@ return [ */ 'shelves' => 'Shelves', 'shelves_long' => 'BookShelves', + 'shelves_empty' => 'No shelves have been created', + 'shelves_create' => 'Create New Shelf', + 'shelves_popular' => 'Popular Shelves', + 'shelves_new' => 'New Shelves', + 'shelves_popular_empty' => 'The most popular shelves will appear here.', + 'shelves_new_empty' => 'The most recently created shelves will appear here.', + 'shelves_save' => 'Save Shelf', + 'shelves_books' => 'Books on this shelf', + 'shelves_add_books' => 'Add books to this shelf', + 'shelves_drag_books' => 'Drag books here to add them to this shelf', /** * Books @@ -212,6 +222,7 @@ return [ 'page_tags' => 'Page Tags', 'chapter_tags' => 'Chapter Tags', 'book_tags' => 'Book Tags', + 'shelf_tags' => 'Shelf Tags', 'tag' => 'Tag', 'tags' => 'Tags', 'tag_value' => 'Tag Value (Optional)', diff --git a/resources/views/base.blade.php b/resources/views/base.blade.php index 93517ef6f..93ee6cdc6 100644 --- a/resources/views/base.blade.php +++ b/resources/views/base.blade.php @@ -33,7 +33,7 @@ <header id="header"> <div class="container fluid"> <div class="row"> - <div class="col-sm-4"> + <div class="col-sm-4 col-md-3"> <a href="{{ baseUrl('/') }}" class="logo"> @if(setting('app-logo', '') !== 'none') <img class="logo-image" src="{{ setting('app-logo', '') === '' ? baseUrl('/logo.png') : baseUrl(setting('app-logo', '')) }}" alt="Logo"> @@ -43,7 +43,7 @@ @endif </a> </div> - <div class="col-sm-8"> + <div class="col-sm-8 col-md-9"> <div class="float right"> <div class="header-search"> <form action="{{ baseUrl('/search') }}" method="GET" class="search-box"> diff --git a/resources/views/books/list.blade.php b/resources/views/books/list.blade.php index 1c2056a79..9459cc008 100644 --- a/resources/views/books/list.blade.php +++ b/resources/views/books/list.blade.php @@ -1,9 +1,5 @@ -@if($booksViewType === 'list') - <div class="container small"> -@else - <div class="container"> -@endif +<div class="container{{ $booksViewType === 'list' ? ' small' : '' }}"> <h1>{{ trans('entities.books') }}</h1> @if(count($books) > 0) @if($booksViewType === 'list') @@ -25,7 +21,7 @@ @else <p class="text-muted">{{ trans('entities.books_empty') }}</p> @if(userCan('books-create-all')) - <a href="{{ baseUrl("/create-book") }}" class="text-pos">@icon('edit'){{ trans('entities.create_one_now') }}</a> + <a href="{{ baseUrl("/create-book") }}" class="text-pos">@icon('edit'){{ trans('entities.create_now') }}</a> @endif @endif </div> \ No newline at end of file diff --git a/resources/views/books/view-toggle.blade.php b/resources/views/books/view-toggle.blade.php index 61df7ab8d..63eb9b9d3 100644 --- a/resources/views/books/view-toggle.blade.php +++ b/resources/views/books/view-toggle.blade.php @@ -1,7 +1,7 @@ <form action="{{ baseUrl("/settings/users/{$currentUser->id}/switch-book-view") }}" method="POST" class="inline"> {!! csrf_field() !!} {!! method_field('PATCH') !!} - <input type="hidden" value="{{ $booksViewType === 'list'? 'grid' : 'list' }}" name="book_view_type"> + <input type="hidden" value="{{ $booksViewType === 'list'? 'grid' : 'list' }}" name="view_type"> @if ($booksViewType === 'list') <button type="submit" class="text-pos text-button">@icon('grid'){{ trans('common.grid_view') }}</button> @else diff --git a/resources/views/partials/entity-list.blade.php b/resources/views/partials/entity-list.blade.php index c90b953ea..371f38d71 100644 --- a/resources/views/partials/entity-list.blade.php +++ b/resources/views/partials/entity-list.blade.php @@ -8,6 +8,8 @@ @include('books/list-item', ['book' => $entity]) @elseif($entity->isA('chapter')) @include('chapters/list-item', ['chapter' => $entity, 'hidePages' => true]) + @elseif($entity->isA('bookshelf')) + @include('shelves/list-item', ['bookshelf' => $entity]) @endif @if($index !== count($entities) - 1) diff --git a/resources/views/shelves/_breadcrumbs.blade.php b/resources/views/shelves/_breadcrumbs.blade.php new file mode 100644 index 000000000..e4ecc36c6 --- /dev/null +++ b/resources/views/shelves/_breadcrumbs.blade.php @@ -0,0 +1,3 @@ +<div class="breadcrumbs"> + <a href="{{$book->getUrl()}}" class="text-book text-button">@icon('book'){{ $book->getShortName() }}</a> +</div> \ No newline at end of file diff --git a/resources/views/shelves/create.blade.php b/resources/views/shelves/create.blade.php new file mode 100644 index 000000000..aaff08974 --- /dev/null +++ b/resources/views/shelves/create.blade.php @@ -0,0 +1,31 @@ +@extends('simple-layout') + +@section('toolbar') + <div class="col-sm-8 faded"> + <div class="breadcrumbs"> + <a href="{{ baseUrl('/shelves') }}" class="text-button">@icon('bookshelf'){{ trans('entities.shelves') }}</a> + <span class="sep">»</span> + <a href="{{ baseUrl('/create-shelf') }}" class="text-button">@icon('add'){{ trans('entities.shelves_create') }}</a> + </div> + </div> +@stop + +@section('body') + + <div class="container small"> + <p> </p> + <div class="card"> + <h3>@icon('add') {{ trans('entities.shelves_create') }}</h3> + <div class="body"> + <form action="{{ baseUrl("/shelves") }}" method="POST" enctype="multipart/form-data"> + @include('shelves/form') + </form> + </div> + </div> + </div> + + <p class="margin-top large"><br></p> + + @include('components.image-manager', ['imageType' => 'cover']) + +@stop \ No newline at end of file diff --git a/resources/views/shelves/delete.blade.php b/resources/views/shelves/delete.blade.php new file mode 100644 index 000000000..0ac98e895 --- /dev/null +++ b/resources/views/shelves/delete.blade.php @@ -0,0 +1,30 @@ +@extends('simple-layout') + +@section('toolbar') + <div class="col-sm-12 faded"> + @include('books._breadcrumbs', ['book' => $book]) + </div> +@stop + +@section('body') + + <div class="container small"> + <p> </p> + <div class="card"> + <h3>@icon('delete') {{ trans('entities.books_delete') }}</h3> + <div class="body"> + <p>{{ trans('entities.books_delete_explain', ['bookName' => $book->name]) }}</p> + <p class="text-neg">{{ trans('entities.books_delete_confirmation') }}</p> + + <form action="{{$book->getUrl()}}" method="POST"> + {!! csrf_field() !!} + <input type="hidden" name="_method" value="DELETE"> + <a href="{{$book->getUrl()}}" class="button outline">{{ trans('common.cancel') }}</a> + <button type="submit" class="button neg">{{ trans('common.confirm') }}</button> + </form> + </div> + </div> + + </div> + +@stop \ No newline at end of file diff --git a/resources/views/shelves/edit.blade.php b/resources/views/shelves/edit.blade.php new file mode 100644 index 000000000..cb1ffc461 --- /dev/null +++ b/resources/views/shelves/edit.blade.php @@ -0,0 +1,24 @@ +@extends('simple-layout') + +@section('toolbar') + <div class="col-sm-12 faded"> + @include('books._breadcrumbs', ['book' => $book]) + </div> +@stop + +@section('body') + + <div class="container small"> + <p> </p> + <div class="card"> + <h3>@icon('edit') {{ trans('entities.books_edit') }}</h3> + <div class="body"> + <form action="{{ $book->getUrl() }}" method="POST"> + <input type="hidden" name="_method" value="PUT"> + @include('books/form', ['model' => $book]) + </form> + </div> + </div> + </div> +@include('components.image-manager', ['imageType' => 'cover']) +@stop \ No newline at end of file diff --git a/resources/views/shelves/export.blade.php b/resources/views/shelves/export.blade.php new file mode 100644 index 000000000..462ad7991 --- /dev/null +++ b/resources/views/shelves/export.blade.php @@ -0,0 +1,80 @@ +<!doctype html> +<html lang="en"> +<head> + <meta http-equiv="Content-Type" content="text/html; charset=utf-8"/> + <title>{{ $book->name }}</title> + + <style> + @if (!app()->environment('testing')) + {!! file_get_contents(public_path('/dist/export-styles.css')) !!} + @endif + .page-break { + page-break-after: always; + } + .chapter-hint { + color: #888; + margin-top: 32px; + } + .chapter-hint + h1 { + margin-top: 0; + } + ul.contents ul li { + list-style: circle; + } + @media screen { + .page-break { + border-top: 1px solid #DDD; + } + } + </style> + @yield('head') +</head> +<body> +<div class="container"> + <div class="row"> + <div class="col-md-8 col-md-offset-2"> + <div class="page-content"> + + <h1 style="font-size: 4.8em">{{$book->name}}</h1> + + <p>{{ $book->description }}</p> + + @if(count($bookChildren) > 0) + <ul class="contents"> + @foreach($bookChildren as $bookChild) + <li><a href="#{{$bookChild->getType()}}-{{$bookChild->id}}">{{ $bookChild->name }}</a></li> + @if($bookChild->isA('chapter') && count($bookChild->pages) > 0) + <ul> + @foreach($bookChild->pages as $page) + <li><a href="#page-{{$page->id}}">{{ $page->name }}</a></li> + @endforeach + </ul> + @endif + @endforeach + </ul> + @endif + + @foreach($bookChildren as $bookChild) + <div class="page-break"></div> + <h1 id="{{$bookChild->getType()}}-{{$bookChild->id}}">{{ $bookChild->name }}</h1> + @if($bookChild->isA('chapter')) + <p>{{ $bookChild->description }}</p> + @if(count($bookChild->pages) > 0) + @foreach($bookChild->pages as $page) + <div class="page-break"></div> + <div class="chapter-hint">{{$bookChild->name}}</div> + <h1 id="page-{{$page->id}}">{{ $page->name }}</h1> + {!! $page->html !!} + @endforeach + @endif + @else + {!! $bookChild->html !!} + @endif + @endforeach + + </div> + </div> + </div> +</div> +</body> +</html> diff --git a/resources/views/shelves/form.blade.php b/resources/views/shelves/form.blade.php new file mode 100644 index 000000000..2a4b38b8b --- /dev/null +++ b/resources/views/shelves/form.blade.php @@ -0,0 +1,84 @@ + +{{ csrf_field() }} +<div class="form-group title-input"> + <label for="name">{{ trans('common.name') }}</label> + @include('form/text', ['name' => 'name']) +</div> + +<div class="form-group description-input"> + <label for="description">{{ trans('common.description') }}</label> + @include('form/textarea', ['name' => 'description']) +</div> + +<div class="row"> + <div class="col-md-6"> + <div shelf-sort class="form-group"> + <label for="books">{{ trans('entities.shelves_books') }}</label> + <input type="hidden" id="books-input" name="books" + value="{{ isset($shelf) ? $shelf->books->implode('id', ',') : '' }}"> + <div class="scroll-box"> + <div class="scroll-box-item text-small text-muted instruction"> + {{ trans('entities.shelves_drag_books') }} + </div> + <div class="scroll-box-item scroll-box-placeholder" style="display: none;"> + <a href="#" class="text-muted">@icon('book') ...</a> + </div> + @if (isset($shelf) && count($shelf->books) > 0) + @foreach ($shelf->books as $book) + <div data-id="{{ $book->id }}" class="scroll-box-item"> + <a href="{{ $book->getUrl() }}" class="text-book">@icon('book'){{ $book->name }}</a> + </div> + @endforeach + @endif + </div> + </div> + </div> + <div class="col-md-6"> + <div class="form-group"> + <label for="books">{{ trans('entities.shelves_add_books') }}</label> + <div class="scroll-box"> + @foreach ($books as $book) + <div data-id="{{ $book->id }}" class="scroll-box-item"> + <a href="{{ $book->getUrl() }}" class="text-book">@icon('book'){{ $book->name }}</a> + </div> + @endforeach + </div> + </div> + </div> +</div> + + + +<div class="form-group" collapsible id="logo-control"> + <div class="collapse-title text-primary" collapsible-trigger> + <label for="user-avatar">{{ trans('common.cover_image') }}</label> + </div> + <div class="collapse-content" collapsible-content> + <p class="small">{{ trans('common.cover_image_description') }}</p> + + @include('components.image-picker', [ + 'resizeHeight' => '512', + 'resizeWidth' => '512', + 'showRemove' => false, + 'defaultImage' => baseUrl('/book_default_cover.png'), + 'currentImage' => isset($shelf) ? $shelf->getBookCover() : baseUrl('/book_default_cover.png') , + 'currentId' => isset($shelf) && $shelf->image_id ? $shelf->image_id : 0, + 'name' => 'image_id', + 'imageClass' => 'cover' + ]) + </div> +</div> + +<div class="form-group" collapsible id="tags-control"> + <div class="collapse-title text-primary" collapsible-trigger> + <label for="tag-manager">{{ trans('entities.shelf_tags') }}</label> + </div> + <div class="collapse-content" collapsible-content> + @include('components.tag-manager', ['entity' => $shelf ?? null, 'entityType' => 'bookshelf']) + </div> +</div> + +<div class="form-group text-right"> + <a href="{{ isset($shelf) ? $shelf->getUrl() : baseUrl('/shelves') }}" class="button outline">{{ trans('common.cancel') }}</a> + <button type="submit" class="button pos">{{ trans('entities.shelves_save') }}</button> +</div> \ No newline at end of file diff --git a/resources/views/shelves/grid-item.blade.php b/resources/views/shelves/grid-item.blade.php new file mode 100644 index 000000000..b70b5166e --- /dev/null +++ b/resources/views/shelves/grid-item.blade.php @@ -0,0 +1,18 @@ +<div class="bookshelf-grid-item grid-card" data-entity-type="bookshelf" data-entity-id="{{$bookshelf->id}}"> + <div class="featured-image-container"> + <a href="{{$bookshelf->getUrl()}}" title="{{$bookshelf->name}}"> + <img src="{{$bookshelf->getBookCover()}}" alt="{{$bookshelf->name}}"> + </a> + </div> + <div class="grid-card-content"> + <h2><a class="break-text" href="{{$bookshelf->getUrl()}}" title="{{$bookshelf->name}}">{{$bookshelf->getShortName(35)}}</a></h2> + @if(isset($bookshelf->searchSnippet)) + <p >{!! $bookshelf->searchSnippet !!}</p> + @else + <p >{{ $bookshelf->getExcerpt(130) }}</p> + @endif + </div> + <div class="grid-card-footer text-muted text-small"> + <span>@include('partials.entity-meta', ['entity' => $bookshelf])</span> + </div> +</div> \ No newline at end of file diff --git a/resources/views/shelves/index.blade.php b/resources/views/shelves/index.blade.php new file mode 100644 index 000000000..a887a843e --- /dev/null +++ b/resources/views/shelves/index.blade.php @@ -0,0 +1,48 @@ +@extends('sidebar-layout') + +@section('toolbar') + <div class="col-xs-6 faded"> + <div class="action-buttons text-left"> + @include('shelves/view-toggle', ['shelvesViewType' => $shelvesViewType]) + </div> + </div> + <div class="col-xs-6 faded"> + <div class="action-buttons"> + @if($currentUser->can('bookshelf-create-all')) + <a href="{{ baseUrl("/create-shelf") }}" class="text-pos text-button">@icon('add'){{ trans('entities.shelves_create') }}</a> + @endif + </div> + </div> +@stop + +@section('sidebar') + @if($recents) + <div id="recents" class="card"> + <h3>@icon('view') {{ trans('entities.recently_viewed') }}</h3> + @include('partials/entity-list', ['entities' => $recents, 'style' => 'compact']) + </div> + @endif + + <div id="popular" class="card"> + <h3>@icon('popular') {{ trans('entities.shelves_popular') }}</h3> + @if(count($popular) > 0) + @include('partials/entity-list', ['entities' => $popular, 'style' => 'compact']) + @else + <div class="body text-muted">{{ trans('entities.shelves_popular_empty') }}</div> + @endif + </div> + + <div id="new" class="card"> + <h3>@icon('star-circle') {{ trans('entities.shelves_new') }}</h3> + @if(count($new) > 0) + @include('partials/entity-list', ['entities' => $new, 'style' => 'compact']) + @else + <div class="body text-muted">{{ trans('entities.shelves_new_empty') }}</div> + @endif + </div> +@stop + +@section('body') + @include('shelves/list', ['shelves' => $shelves, 'shelvesViewType' => $shelvesViewType]) + <p><br></p> +@stop \ No newline at end of file diff --git a/resources/views/shelves/list-item.blade.php b/resources/views/shelves/list-item.blade.php new file mode 100644 index 000000000..0b8e79fe5 --- /dev/null +++ b/resources/views/shelves/list-item.blade.php @@ -0,0 +1,10 @@ +<div class="shelf entity-list-item" data-entity-type="bookshelf" data-entity-id="{{$bookshelf->id}}"> + <h4 class="text-shelf"><a class="text-bookshelf entity-list-item-link" href="{{$bookshelf->getUrl()}}">@icon('bookshelf')<span class="entity-list-item-name break-text">{{$bookshelf->name}}</span></a></h4> + <div class="entity-item-snippet"> + @if(isset($bookshelf->searchSnippet)) + <p class="text-muted break-text">{!! $bookshelf->searchSnippet !!}</p> + @else + <p class="text-muted break-text">{{ $bookshelf->getExcerpt() }}</p> + @endif + </div> +</div> \ No newline at end of file diff --git a/resources/views/shelves/list.blade.php b/resources/views/shelves/list.blade.php new file mode 100644 index 000000000..ff11d2d67 --- /dev/null +++ b/resources/views/shelves/list.blade.php @@ -0,0 +1,26 @@ + +<div class="container{{ $shelvesViewType === 'list' ? ' small' : '' }}"> + <h1>{{ trans('entities.shelves') }}</h1> + @if(count($shelves) > 0) + @if($shelvesViewType === 'grid') + <div class="grid third"> + @foreach($shelves as $key => $shelf) + @include('shelves/grid-item', ['bookshelf' => $shelf]) + @endforeach + </div> + @else + @foreach($shelves as $shelf) + @include('shelves/list-item', ['bookshelf' => $shelf]) + <hr> + @endforeach + @endif + <div> + {!! $shelves->render() !!} + </div> + @else + <p class="text-muted">{{ trans('entities.shelves_empty') }}</p> + @if(userCan('bookshelf-create-all')) + <a href="{{ baseUrl("/create-shelf") }}" class="button outline">@icon('edit'){{ trans('entities.create_now') }}</a> + @endif + @endif +</div> \ No newline at end of file diff --git a/resources/views/shelves/restrictions.blade.php b/resources/views/shelves/restrictions.blade.php new file mode 100644 index 000000000..2a6eb0bea --- /dev/null +++ b/resources/views/shelves/restrictions.blade.php @@ -0,0 +1,21 @@ +@extends('simple-layout') + +@section('toolbar') + <div class="col-sm-12 faded"> + @include('books._breadcrumbs', ['book' => $book]) + </div> +@stop + +@section('body') + + <div class="container"> + <p> </p> + <div class="card"> + <h3>@icon('lock') {{ trans('entities.books_permissions') }}</h3> + <div class="body"> + @include('form/restriction-form', ['model' => $book]) + </div> + </div> + </div> + +@stop diff --git a/resources/views/shelves/show.blade.php b/resources/views/shelves/show.blade.php new file mode 100644 index 000000000..e5845b495 --- /dev/null +++ b/resources/views/shelves/show.blade.php @@ -0,0 +1,135 @@ +@extends('sidebar-layout') + +@section('toolbar') + <div class="col-sm-6 col-xs-1 faded"> + @include('books._breadcrumbs', ['book' => $book]) + </div> + <div class="col-sm-6 col-xs-11"> + <div class="action-buttons faded"> + <span dropdown class="dropdown-container"> + <div dropdown-toggle class="text-button text-primary">@icon('export'){{ trans('entities.export') }}</div> + <ul class="wide"> + <li><a href="{{ $book->getUrl('/export/html') }}" target="_blank">{{ trans('entities.export_html') }} <span class="text-muted float right">.html</span></a></li> + <li><a href="{{ $book->getUrl('/export/pdf') }}" target="_blank">{{ trans('entities.export_pdf') }} <span class="text-muted float right">.pdf</span></a></li> + <li><a href="{{ $book->getUrl('/export/plaintext') }}" target="_blank">{{ trans('entities.export_text') }} <span class="text-muted float right">.txt</span></a></li> + </ul> + </span> + @if(userCan('page-create', $book)) + <a href="{{ $book->getUrl('/create-page') }}" class="text-pos text-button">@icon('add'){{ trans('entities.pages_new') }}</a> + @endif + @if(userCan('chapter-create', $book)) + <a href="{{ $book->getUrl('/create-chapter') }}" class="text-pos text-button">@icon('add'){{ trans('entities.chapters_new') }}</a> + @endif + @if(userCan('book-update', $book) || userCan('restrictions-manage', $book) || userCan('book-delete', $book)) + <div dropdown class="dropdown-container"> + <a dropdown-toggle class="text-primary text-button">@icon('more'){{ trans('common.more') }}</a> + <ul> + @if(userCan('book-update', $book)) + <li><a href="{{ $book->getUrl('/edit') }}" class="text-primary">@icon('edit'){{ trans('common.edit') }}</a></li> + <li><a href="{{ $book->getUrl('/sort') }}" class="text-primary">@icon('sort'){{ trans('common.sort') }}</a></li> + @endif + @if(userCan('restrictions-manage', $book)) + <li><a href="{{ $book->getUrl('/permissions') }}" class="text-primary">@icon('lock'){{ trans('entities.permissions') }}</a></li> + @endif + @if(userCan('book-delete', $book)) + <li><a href="{{ $book->getUrl('/delete') }}" class="text-neg">@icon('delete'){{ trans('common.delete') }}</a></li> + @endif + </ul> + </div> + @endif + </div> + </div> +@stop + +@section('sidebar') + + @if($book->tags->count() > 0) + <section> + @include('components.tag-list', ['entity' => $book]) + </section> + @endif + + <div class="card"> + <div class="body"> + <form v-on:submit.prevent="searchBook" class="search-box"> + <input v-model="searchTerm" v-on:change="checkSearchForm()" type="text" name="term" placeholder="{{ trans('entities.books_search_this') }}"> + <button type="submit">@icon('search')</button> + <button v-if="searching" v-cloak class="text-neg" v-on:click="clearSearch()" type="button">@icon('close')</button> + </form> + </div> + </div> + + <div class="card entity-details"> + <h3>@icon('info') {{ trans('common.details') }}</h3> + <div class="body text-small text-muted blended-links"> + @include('partials.entity-meta', ['entity' => $book]) + @if($book->restricted) + <div class="active-restriction"> + @if(userCan('restrictions-manage', $book)) + <a href="{{ $book->getUrl('/permissions') }}">@icon('lock'){{ trans('entities.books_permissions_active') }}</a> + @else + @icon('lock'){{ trans('entities.books_permissions_active') }} + @endif + </div> + @endif + </div> + </div> + + @if(count($activity) > 0) + <div class="activity card"> + <h3>@icon('time') {{ trans('entities.recent_activity') }}</h3> + @include('partials/activity-list', ['activity' => $activity]) + </div> + @endif +@stop + +@section('container-attrs') + id="entity-dashboard" + entity-id="{{ $book->id }}" + entity-type="book" +@stop + +@section('body') + + <div class="container small nopad"> + <h1 class="break-text" v-pre>{{$book->name}}</h1> + <div class="book-content" v-show="!searching"> + <p class="text-muted" v-pre>{!! nl2br(e($book->description)) !!}</p> + @if(count($bookChildren) > 0) + <div class="page-list" v-pre> + <hr> + @foreach($bookChildren as $childElement) + @if($childElement->isA('chapter')) + @include('chapters/list-item', ['chapter' => $childElement]) + @else + @include('pages/list-item', ['page' => $childElement]) + @endif + <hr> + @endforeach + </div> + @else + <div class="well"> + <p class="text-muted italic">{{ trans('entities.books_empty_contents') }}</p> + @if(userCan('page-create', $book)) + <a href="{{ $book->getUrl('/create-page') }}" class="button outline page">@icon('page'){{ trans('entities.books_empty_create_page') }}</a> + @endif + @if(userCan('page-create', $book) && userCan('chapter-create', $book)) + <em class="text-muted">-{{ trans('entities.books_empty_or') }}-</em> + @endif + @if(userCan('chapter-create', $book)) + <a href="{{ $book->getUrl('/create-chapter') }}" class="button outline chapter">@icon('chapter'){{ trans('entities.books_empty_add_chapter') }}</a> + @endif + </div> + @endif + + </div> + <div class="search-results" v-cloak v-show="searching"> + <h3 class="text-muted">{{ trans('entities.search_results') }} <a v-if="searching" v-on:click="clearSearch()" class="text-small">@icon('close'){{ trans('entities.search_clear') }}</a></h3> + <div v-if="!searchResults"> + @include('partials/loading-icon') + </div> + <div v-html="searchResults"></div> + </div> + </div> + +@stop diff --git a/resources/views/shelves/view-toggle.blade.php b/resources/views/shelves/view-toggle.blade.php new file mode 100644 index 000000000..785e8cac1 --- /dev/null +++ b/resources/views/shelves/view-toggle.blade.php @@ -0,0 +1,10 @@ +<form action="{{ baseUrl("/settings/users/{$currentUser->id}/switch-shelf-view") }}" method="POST" class="inline"> + {!! csrf_field() !!} + {!! method_field('PATCH') !!} + <input type="hidden" value="{{ $shelvesViewType === 'list'? 'grid' : 'list' }}" name="view_type"> + @if ($shelvesViewType === 'list') + <button type="submit" class="text-pos text-button">@icon('grid'){{ trans('common.grid_view') }}</button> + @else + <button type="submit" class="text-pos text-button">@icon('list'){{ trans('common.list_view') }}</button> + @endif +</form> \ No newline at end of file diff --git a/routes/web.php b/routes/web.php index c4e7469fe..7eae4a3fa 100644 --- a/routes/web.php +++ b/routes/web.php @@ -14,6 +14,13 @@ Route::group(['middleware' => 'auth'], function () { Route::get('/recently-updated', 'PageController@showRecentlyUpdated'); }); + // Shelves + Route::get('/create-shelf', 'BookshelfController@create'); + Route::group(['prefix' => 'shelves'], function() { + Route::get('/', 'BookshelfController@index'); + Route::post('/', 'BookshelfController@store'); + }); + Route::get('/create-book', 'BookController@create'); Route::group(['prefix' => 'books'], function () { @@ -160,6 +167,7 @@ Route::group(['middleware' => 'auth'], function () { Route::get('/users/create', 'UserController@create'); Route::get('/users/{id}/delete', 'UserController@delete'); Route::patch('/users/{id}/switch-book-view', 'UserController@switchBookView'); + Route::patch('/users/{id}/switch-shelf-view', 'UserController@switchShelfView'); Route::post('/users/create', 'UserController@store'); Route::get('/users/{id}', 'UserController@edit'); Route::put('/users/{id}', 'UserController@update');