Merge branch 'master' into fix/#960

This commit is contained in:
Dan Brown 2018-09-22 15:57:53 +01:00
commit 07bc0612c0
No known key found for this signature in database
GPG Key ID: 46D9F943C24A2EF9
125 changed files with 3534 additions and 367 deletions

View File

@ -48,14 +48,6 @@ class Book extends Entity
{
return $this->belongsTo(Image::class, 'image_id');
}
/*
* Get the edit url for this book.
* @return string
*/
public function getEditUrl()
{
return $this->getUrl() . '/edit';
}
/**
* Get all pages within this book.
@ -75,6 +67,15 @@ class Book extends Entity
return $this->hasMany(Chapter::class);
}
/**
* Get the shelves this book is contained within.
* @return \Illuminate\Database\Eloquent\Relations\BelongsToMany
*/
public function shelves()
{
return $this->belongsToMany(Bookshelf::class, 'bookshelves_books', 'book_id', 'bookshelf_id');
}
/**
* Get an excerpt of this book's description to the specified length or less.
* @param int $length

83
app/Bookshelf.php Normal file
View File

@ -0,0 +1,83 @@
<?php namespace BookStack;
class Bookshelf extends Entity
{
protected $table = 'bookshelves';
public $searchFactor = 3;
protected $fillable = ['name', 'description', 'image_id'];
/**
* Get the books in this shelf.
* Should not be used directly since does not take into account permissions.
* @return \Illuminate\Database\Eloquent\Relations\BelongsToMany
*/
public function books()
{
return $this->belongsToMany(Book::class, 'bookshelves_books', 'bookshelf_id', 'book_id')->orderBy('order', 'asc');
}
/**
* Get the url for this bookshelf.
* @param string|bool $path
* @return string
*/
public function getUrl($path = false)
{
if ($path !== false) {
return baseUrl('/shelves/' . urlencode($this->slug) . '/' . trim($path, '/'));
}
return baseUrl('/shelves/' . urlencode($this->slug));
}
/**
* Returns BookShelf cover image, if cover does not exists return default cover image.
* @param int $width - Width of the image
* @param int $height - Height of the image
* @return string
*/
public function getBookCover($width = 440, $height = 250)
{
$default = baseUrl('/book_default_cover.png');
if (!$this->image_id) {
return $default;
}
try {
$cover = $this->cover ? baseUrl($this->cover->getThumb($width, $height, false)) : $default;
} catch (\Exception $err) {
$cover = $default;
}
return $cover;
}
/**
* Get the cover image of the book
* @return \Illuminate\Database\Eloquent\Relations\BelongsTo
*/
public function cover()
{
return $this->belongsTo(Image::class, 'image_id');
}
/**
* Get an excerpt of this book's description to the specified length or less.
* @param int $length
* @return string
*/
public function getExcerpt($length = 100)
{
$description = $this->description;
return strlen($description) > $length ? substr($description, 0, $length-3) . '...' : $description;
}
/**
* Return a generalised, common raw query that can be 'unioned' across entities.
* @return string
*/
public function entityRawQuery()
{
return "'BookStack\\\\BookShelf' as entity_type, id, id as entity_id, slug, name, {$this->textField} as text,'' as html, '0' as book_id, '0' as priority, '0' as chapter_id, '0' as draft, created_by, updated_by, updated_at, created_at";
}
}

View File

@ -72,7 +72,9 @@ class CleanupImages extends Command
protected function showDeletedImages($paths)
{
if ($this->getOutput()->getVerbosity() <= OutputInterface::VERBOSITY_NORMAL) return;
if ($this->getOutput()->getVerbosity() <= OutputInterface::VERBOSITY_NORMAL) {
return;
}
if (count($paths) > 0) {
$this->line('Images to delete:');
}

View File

@ -152,7 +152,7 @@ class Entity extends Ownable
*/
public static function getEntityInstance($type)
{
$types = ['Page', 'Book', 'Chapter'];
$types = ['Page', 'Book', 'Chapter', 'Bookshelf'];
$className = str_replace([' ', '-', '_'], '', ucwords($type));
if (!in_array($className, $types)) {
return null;

View File

@ -0,0 +1,6 @@
<?php namespace BookStack\Exceptions;
class SocialSignInAccountNotUsed extends SocialSignInException
{
}

View File

@ -201,10 +201,7 @@ class AttachmentController extends Controller
}
$attachmentContents = $this->attachmentService->getAttachmentFromStorage($attachment);
return response($attachmentContents, 200, [
'Content-Type' => 'application/octet-stream',
'Content-Disposition' => 'attachment; filename="'. $attachment->getFileName() .'"'
]);
return $this->downloadResponse($attachmentContents, $attachment->getFileName());
}
/**

View File

@ -2,7 +2,7 @@
namespace BookStack\Http\Controllers\Auth;
use BookStack\Exceptions\ConfirmationEmailException;
use BookStack\Exceptions\SocialSignInAccountNotUsed;
use BookStack\Exceptions\SocialSignInException;
use BookStack\Exceptions\UserRegistrationException;
use BookStack\Repos\UserRepo;
@ -16,6 +16,7 @@ use Illuminate\Http\Response;
use Validator;
use BookStack\Http\Controllers\Controller;
use Illuminate\Foundation\Auth\RegistersUsers;
use Laravel\Socialite\Contracts\User as SocialUser;
class RegisterController extends Controller
{
@ -133,25 +134,28 @@ class RegisterController extends Controller
* The registrations flow for all users.
* @param array $userData
* @param bool|false|SocialAccount $socialAccount
* @param bool $emailVerified
* @return \Illuminate\Http\RedirectResponse|\Illuminate\Routing\Redirector
* @throws UserRegistrationException
*/
protected function registerUser(array $userData, $socialAccount = false)
protected function registerUser(array $userData, $socialAccount = false, $emailVerified = false)
{
if (setting('registration-restrict')) {
$restrictedEmailDomains = explode(',', str_replace(' ', '', setting('registration-restrict')));
$registrationRestrict = setting('registration-restrict');
if ($registrationRestrict) {
$restrictedEmailDomains = explode(',', str_replace(' ', '', $registrationRestrict));
$userEmailDomain = $domain = substr(strrchr($userData['email'], "@"), 1);
if (!in_array($userEmailDomain, $restrictedEmailDomains)) {
throw new UserRegistrationException(trans('auth.registration_email_domain_invalid'), '/register');
}
}
$newUser = $this->userRepo->registerNew($userData);
$newUser = $this->userRepo->registerNew($userData, $emailVerified);
if ($socialAccount) {
$newUser->socialAccounts()->save($socialAccount);
}
if (setting('registration-confirmation') || setting('registration-restrict')) {
if ((setting('registration-confirmation') || $registrationRestrict) && !$emailVerified) {
$newUser->save();
try {
@ -250,7 +254,6 @@ class RegisterController extends Controller
* @throws SocialSignInException
* @throws UserRegistrationException
* @throws \BookStack\Exceptions\SocialDriverNotConfigured
* @throws ConfirmationEmailException
*/
public function socialCallback($socialDriver, Request $request)
{
@ -267,12 +270,24 @@ class RegisterController extends Controller
}
$action = session()->pull('social-callback');
// Attempt login or fall-back to register if allowed.
$socialUser = $this->socialAuthService->getSocialUser($socialDriver);
if ($action == 'login') {
return $this->socialAuthService->handleLoginCallback($socialDriver);
try {
return $this->socialAuthService->handleLoginCallback($socialDriver, $socialUser);
} catch (SocialSignInAccountNotUsed $exception) {
if ($this->socialAuthService->driverAutoRegisterEnabled($socialDriver)) {
return $this->socialRegisterCallback($socialDriver, $socialUser);
}
throw $exception;
}
}
if ($action == 'register') {
return $this->socialRegisterCallback($socialDriver);
return $this->socialRegisterCallback($socialDriver, $socialUser);
}
return redirect()->back();
}
@ -288,15 +303,16 @@ class RegisterController extends Controller
/**
* Register a new user after a registration callback.
* @param $socialDriver
* @param string $socialDriver
* @param SocialUser $socialUser
* @return \Illuminate\Http\RedirectResponse|\Illuminate\Routing\Redirector
* @throws UserRegistrationException
* @throws \BookStack\Exceptions\SocialDriverNotConfigured
*/
protected function socialRegisterCallback($socialDriver)
protected function socialRegisterCallback(string $socialDriver, SocialUser $socialUser)
{
$socialUser = $this->socialAuthService->handleRegistrationCallback($socialDriver);
$socialUser = $this->socialAuthService->handleRegistrationCallback($socialDriver, $socialUser);
$socialAccount = $this->socialAuthService->fillSocialAccount($socialDriver, $socialUser);
$emailVerified = $this->socialAuthService->driverAutoConfirmEmailEnabled($socialDriver);
// Create an array of the user data to create a new user instance
$userData = [
@ -304,6 +320,6 @@ class RegisterController extends Controller
'email' => $socialUser->getEmail(),
'password' => str_random(30)
];
return $this->registerUser($userData, $socialAccount);
return $this->registerUser($userData, $socialAccount, $emailVerified);
}
}

View File

@ -299,10 +299,7 @@ class BookController extends Controller
{
$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'
]);
return $this->downloadResponse($pdfContent, $bookSlug . '.pdf');
}
/**
@ -314,10 +311,7 @@ class BookController extends Controller
{
$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'
]);
return $this->downloadResponse($htmlContent, $bookSlug . '.html');
}
/**
@ -328,10 +322,7 @@ class BookController extends Controller
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'
]);
$textContent = $this->exportService->bookToPlainText($book);
return $this->downloadResponse($textContent, $bookSlug . '.txt');
}
}

View File

@ -0,0 +1,243 @@
<?php namespace BookStack\Http\Controllers;
use Activity;
use BookStack\Book;
use BookStack\Bookshelf;
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');
$books = $this->entityRepo->getAll('book', false, 'update');
$this->setPageTitle(trans('entities.shelves_create'));
return view('shelves/create', ['books' => $books]);
}
/**
* Store a newly created bookshelf 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 bookshelf.
* @param String $slug
* @return Response
* @throws \BookStack\Exceptions\NotFoundException
*/
public function show(string $slug)
{
$bookshelf = $this->entityRepo->getBySlug('bookshelf', $slug); /** @var $bookshelf Bookshelf */
$this->checkOwnablePermission('book-view', $bookshelf);
$books = $this->entityRepo->getBookshelfChildren($bookshelf);
Views::add($bookshelf);
$this->setPageTitle($bookshelf->getShortName());
return view('shelves/show', [
'shelf' => $bookshelf,
'books' => $books,
'activity' => Activity::entityActivity($bookshelf, 20, 0)
]);
}
/**
* Show the form for editing the specified bookshelf.
* @param $slug
* @return Response
* @throws \BookStack\Exceptions\NotFoundException
*/
public function edit(string $slug)
{
$bookshelf = $this->entityRepo->getBySlug('bookshelf', $slug); /** @var $bookshelf Bookshelf */
$this->checkOwnablePermission('bookshelf-update', $bookshelf);
$shelfBooks = $this->entityRepo->getBookshelfChildren($bookshelf);
$shelfBookIds = $shelfBooks->pluck('id');
$books = $this->entityRepo->getAll('book', false, 'update');
$books = $books->filter(function ($book) use ($shelfBookIds) {
return !$shelfBookIds->contains($book->id);
});
$this->setPageTitle(trans('entities.shelves_edit_named', ['name' => $bookshelf->getShortName()]));
return view('shelves/edit', [
'shelf' => $bookshelf,
'books' => $books,
'shelfBooks' => $shelfBooks,
]);
}
/**
* Update the specified bookshelf in storage.
* @param Request $request
* @param string $slug
* @return Response
* @throws \BookStack\Exceptions\NotFoundException
*/
public function update(Request $request, string $slug)
{
$shelf = $this->entityRepo->getBySlug('bookshelf', $slug); /** @var $bookshelf Bookshelf */
$this->checkOwnablePermission('bookshelf-update', $shelf);
$this->validate($request, [
'name' => 'required|string|max:255',
'description' => 'string|max:1000',
]);
$shelf = $this->entityRepo->updateFromInput('bookshelf', $shelf, $request->all());
$this->entityRepo->updateShelfBooks($shelf, $request->get('books', ''));
Activity::add($shelf, 'bookshelf_update');
return redirect($shelf->getUrl());
}
/**
* Shows the page to confirm deletion
* @param $slug
* @return \Illuminate\View\View
* @throws \BookStack\Exceptions\NotFoundException
*/
public function showDelete(string $slug)
{
$bookshelf = $this->entityRepo->getBySlug('bookshelf', $slug); /** @var $bookshelf Bookshelf */
$this->checkOwnablePermission('bookshelf-delete', $bookshelf);
$this->setPageTitle(trans('entities.shelves_delete_named', ['name' => $bookshelf->getShortName()]));
return view('shelves/delete', ['shelf' => $bookshelf]);
}
/**
* Remove the specified bookshelf from storage.
* @param string $slug
* @return Response
* @throws \BookStack\Exceptions\NotFoundException
* @throws \Throwable
*/
public function destroy(string $slug)
{
$bookshelf = $this->entityRepo->getBySlug('bookshelf', $slug); /** @var $bookshelf Bookshelf */
$this->checkOwnablePermission('bookshelf-delete', $bookshelf);
Activity::addMessage('bookshelf_delete', 0, $bookshelf->name);
$this->entityRepo->destroyBookshelf($bookshelf);
return redirect('/shelves');
}
/**
* Show the Restrictions view.
* @param $slug
* @return \Illuminate\Contracts\View\Factory|\Illuminate\View\View
* @throws \BookStack\Exceptions\NotFoundException
*/
public function showRestrict(string $slug)
{
$bookshelf = $this->entityRepo->getBySlug('bookshelf', $slug);
$this->checkOwnablePermission('restrictions-manage', $bookshelf);
$roles = $this->userRepo->getRestrictableRoles();
return view('shelves.restrictions', [
'shelf' => $bookshelf,
'roles' => $roles
]);
}
/**
* Set the restrictions for this bookshelf.
* @param $slug
* @param Request $request
* @return \Illuminate\Http\RedirectResponse|\Illuminate\Routing\Redirector
* @throws \BookStack\Exceptions\NotFoundException
*/
public function restrict(string $slug, Request $request)
{
$bookshelf = $this->entityRepo->getBySlug('bookshelf', $slug);
$this->checkOwnablePermission('restrictions-manage', $bookshelf);
$this->entityRepo->updateEntityPermissionsFromRequest($request, $bookshelf);
session()->flash('success', trans('entities.shelves_permissions_updated'));
return redirect($bookshelf->getUrl());
}
/**
* Copy the permissions of a bookshelf to the child books.
* @param string $slug
* @return \Illuminate\Http\RedirectResponse|\Illuminate\Routing\Redirector
* @throws \BookStack\Exceptions\NotFoundException
*/
public function copyPermissions(string $slug)
{
$bookshelf = $this->entityRepo->getBySlug('bookshelf', $slug);
$this->checkOwnablePermission('restrictions-manage', $bookshelf);
$updateCount = $this->entityRepo->copyBookshelfPermissions($bookshelf);
session()->flash('success', trans('entities.shelves_copy_permission_success', ['count' => $updateCount]));
return redirect($bookshelf->getUrl());
}
}

View File

@ -250,10 +250,7 @@ class ChapterController extends Controller
{
$chapter = $this->entityRepo->getBySlug('chapter', $chapterSlug, $bookSlug);
$pdfContent = $this->exportService->chapterToPdf($chapter);
return response()->make($pdfContent, 200, [
'Content-Type' => 'application/octet-stream',
'Content-Disposition' => 'attachment; filename="' . $chapterSlug . '.pdf'
]);
return $this->downloadResponse($pdfContent, $chapterSlug . '.pdf');
}
/**
@ -266,10 +263,7 @@ class ChapterController extends Controller
{
$chapter = $this->entityRepo->getBySlug('chapter', $chapterSlug, $bookSlug);
$containedHtml = $this->exportService->chapterToContainedHtml($chapter);
return response()->make($containedHtml, 200, [
'Content-Type' => 'application/octet-stream',
'Content-Disposition' => 'attachment; filename="' . $chapterSlug . '.html'
]);
return $this->downloadResponse($containedHtml, $chapterSlug . '.html');
}
/**
@ -281,10 +275,7 @@ class ChapterController extends Controller
public function exportPlainText($bookSlug, $chapterSlug)
{
$chapter = $this->entityRepo->getBySlug('chapter', $chapterSlug, $bookSlug);
$containedHtml = $this->exportService->chapterToPlainText($chapter);
return response()->make($containedHtml, 200, [
'Content-Type' => 'application/octet-stream',
'Content-Disposition' => 'attachment; filename="' . $chapterSlug . '.txt'
]);
$chapterText = $this->exportService->chapterToPlainText($chapter);
return $this->downloadResponse($chapterText, $chapterSlug . '.txt');
}
}

View File

@ -136,7 +136,6 @@ abstract class Controller extends BaseController
/**
* Create the response for when a request fails validation.
*
* @param \Illuminate\Http\Request $request
* @param array $errors
* @return \Symfony\Component\HttpFoundation\Response
@ -151,4 +150,18 @@ abstract class Controller extends BaseController
->withInput($request->input())
->withErrors($errors, $this->errorBag());
}
/**
* Create a response that forces a download in the browser.
* @param string $content
* @param string $fileName
* @return \Illuminate\Http\Response
*/
protected function downloadResponse(string $content, string $fileName)
{
return response()->make($content, 200, [
'Content-Type' => 'application/octet-stream',
'Content-Disposition' => 'attachment; filename="' . $fileName . '"'
]);
}
}

View File

@ -33,42 +33,42 @@ class HomeController extends Controller
$recents = $this->signedIn ? Views::getUserRecentlyViewed(12*$recentFactor, 0) : $this->entityRepo->getRecentlyCreated('book', 12*$recentFactor);
$recentlyUpdatedPages = $this->entityRepo->getRecentlyUpdated('page', 12);
$customHomepage = false;
$books = false;
$booksViewType = false;
// Check book homepage
$bookHomepageSetting = setting('app-book-homepage');
if ($bookHomepageSetting) {
$books = $this->entityRepo->getAllPaginated('book', 18);
$booksViewType = setting()->getUser($this->currentUser, 'books_view_type', config('app.views.books', 'list'));
} else {
// Check custom homepage
$homepageSetting = setting('app-homepage');
if ($homepageSetting) {
$id = intval(explode(':', $homepageSetting)[0]);
$customHomepage = $this->entityRepo->getById('page', $id, false, true);
$this->entityRepo->renderPage($customHomepage, true);
}
$homepageOptions = ['default', 'books', 'bookshelves', 'page'];
$homepageOption = setting('app-homepage-type', 'default');
if (!in_array($homepageOption, $homepageOptions)) {
$homepageOption = 'default';
}
$view = 'home';
if ($bookHomepageSetting) {
$view = 'home-book';
} else if ($customHomepage) {
$view = 'home-custom';
}
return view('common/' . $view, [
$commonData = [
'activity' => $activity,
'recents' => $recents,
'recentlyUpdatedPages' => $recentlyUpdatedPages,
'draftPages' => $draftPages,
'customHomepage' => $customHomepage,
'books' => $books,
'booksViewType' => $booksViewType
]);
];
if ($homepageOption === 'bookshelves') {
$shelves = $this->entityRepo->getAllPaginated('bookshelf', 18);
$shelvesViewType = setting()->getUser($this->currentUser, 'bookshelves_view_type', config('app.views.bookshelves', 'grid'));
$data = array_merge($commonData, ['shelves' => $shelves, 'shelvesViewType' => $shelvesViewType]);
return view('common.home-shelves', $data);
}
if ($homepageOption === 'books') {
$books = $this->entityRepo->getAllPaginated('book', 18);
$booksViewType = setting()->getUser($this->currentUser, 'books_view_type', config('app.views.books', 'list'));
$data = array_merge($commonData, ['books' => $books, 'booksViewType' => $booksViewType]);
return view('common.home-book', $data);
}
if ($homepageOption === 'page') {
$homepageSetting = setting('app-homepage', '0:');
$id = intval(explode(':', $homepageSetting)[0]);
$customHomepage = $this->entityRepo->getById('page', $id, false, true);
$this->entityRepo->renderPage($customHomepage, true);
return view('common.home-custom', array_merge($commonData, ['customHomepage' => $customHomepage]));
}
return view('common.home', $commonData);
}
/**

View File

@ -454,6 +454,40 @@ class PageController extends Controller
return redirect($page->getUrl());
}
/**
* Deletes a revision using the id of the specified revision.
* @param string $bookSlug
* @param string $pageSlug
* @param int $revId
* @throws NotFoundException
* @throws BadRequestException
* @return \Illuminate\Http\RedirectResponse|\Illuminate\Routing\Redirector
*/
public function destroyRevision($bookSlug, $pageSlug, $revId)
{
$page = $this->entityRepo->getBySlug('page', $pageSlug, $bookSlug);
$this->checkOwnablePermission('page-delete', $page);
$revision = $page->revisions()->where('id', '=', $revId)->first();
if ($revision === null) {
throw new NotFoundException("Revision #{$revId} not found");
}
// Get the current revision for the page
$currentRevision = $page->getCurrentRevision();
// Check if its the latest revision, cannot delete latest revision.
if (intval($currentRevision->id) === intval($revId)) {
session()->flash('error', trans('entities.revision_cannot_delete_latest'));
return response()->view('pages/revisions', ['page' => $page, 'book' => $page->book, 'current' => $page], 400);
}
$revision->delete();
session()->flash('success', trans('entities.revision_delete_success'));
return view('pages/revisions', ['page' => $page, 'book' => $page->book, 'current' => $page]);
}
/**
* Exports a page to a PDF.
* https://github.com/barryvdh/laravel-dompdf
@ -466,10 +500,7 @@ class PageController extends Controller
$page = $this->entityRepo->getBySlug('page', $pageSlug, $bookSlug);
$page->html = $this->entityRepo->renderPage($page);
$pdfContent = $this->exportService->pageToPdf($page);
return response()->make($pdfContent, 200, [
'Content-Type' => 'application/octet-stream',
'Content-Disposition' => 'attachment; filename="' . $pageSlug . '.pdf'
]);
return $this->downloadResponse($pdfContent, $pageSlug . '.pdf');
}
/**
@ -483,10 +514,7 @@ class PageController extends Controller
$page = $this->entityRepo->getBySlug('page', $pageSlug, $bookSlug);
$page->html = $this->entityRepo->renderPage($page);
$containedHtml = $this->exportService->pageToContainedHtml($page);
return response()->make($containedHtml, 200, [
'Content-Type' => 'application/octet-stream',
'Content-Disposition' => 'attachment; filename="' . $pageSlug . '.html'
]);
return $this->downloadResponse($containedHtml, $pageSlug . '.html');
}
/**
@ -498,11 +526,8 @@ class PageController extends Controller
public function exportPlainText($bookSlug, $pageSlug)
{
$page = $this->entityRepo->getBySlug('page', $pageSlug, $bookSlug);
$containedHtml = $this->exportService->pageToPlainText($page);
return response()->make($containedHtml, 200, [
'Content-Type' => 'application/octet-stream',
'Content-Disposition' => 'attachment; filename="' . $pageSlug . '.txt'
]);
$pageText = $this->exportService->pageToPlainText($page);
return $this->downloadResponse($pageText, $pageSlug . '.txt');
}
/**

View File

@ -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");
}
}

View File

@ -6,6 +6,9 @@ use Illuminate\Http\Request;
class Localization
{
protected $rtlLocales = ['ar'];
/**
* Handle an incoming request.
*
@ -23,6 +26,11 @@ class Localization
$locale = setting()->getUser(user(), 'language', $defaultLang);
}
// Set text direction
if (in_array($locale, $this->rtlLocales)) {
config()->set('app.rtl', true);
}
app()->setLocale($locale);
Carbon::setLocale($locale);
return $next($request);

View File

@ -19,5 +19,4 @@ class Image extends Ownable
{
return Images::getThumbnail($this, $width, $height, $keepRatio);
}
}

View File

@ -112,4 +112,13 @@ class Page extends Entity
$htmlQuery = $withContent ? 'html' : "'' as html";
return "'BookStack\\\\Page' as entity_type, id, id as entity_id, slug, name, {$this->textField} as text, {$htmlQuery}, book_id, priority, chapter_id, draft, created_by, updated_by, updated_at, created_at";
}
/**
* Get the current revision for the page if existing
* @return \BookStack\PageRevision|null
*/
public function getCurrentRevision()
{
return $this->revisions()->first();
}
}

View File

@ -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
@ -331,6 +340,17 @@ class EntityRepo
->skip($count * $page)->take($count)->get();
}
/**
* Get the child items for a chapter sorted by priority but
* with draft items floated to the top.
* @param Bookshelf $bookshelf
* @return \Illuminate\Database\Eloquent\Collection|static[]
*/
public function getBookshelfChildren(Bookshelf $bookshelf)
{
return $this->permissionService->enforceEntityRestrictions('book', $bookshelf->books())->get();
}
/**
* Get all child objects of a book.
* Returns a sorted collection of Pages and Chapters.
@ -533,6 +553,28 @@ 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);
// Check books exist and match ordering
$bookIds = $this->entityQuery('book')->whereIn('id', $ids)->get(['id'])->pluck('id');
$syncData = [];
foreach ($ids as $index => $id) {
if ($bookIds->contains($id)) {
$syncData[$id] = ['order' => $index];
}
}
$shelf->books()->sync($syncData);
}
/**
* Change the book that an entity belongs to.
* @param string $type
@ -1154,9 +1196,22 @@ class EntityRepo
$this->permissionService->buildJointPermissionsForEntity($book);
}
/**
* Destroy a bookshelf instance
* @param Bookshelf $shelf
* @throws \Throwable
*/
public function destroyBookshelf(Bookshelf $shelf)
{
$this->destroyEntityCommonRelations($shelf);
$shelf->delete();
}
/**
* Destroy the provided book and all its child entities.
* @param Book $book
* @throws NotifyException
* @throws \Throwable
*/
public function destroyBook(Book $book)
{
@ -1166,17 +1221,14 @@ class EntityRepo
foreach ($book->chapters as $chapter) {
$this->destroyChapter($chapter);
}
\Activity::removeEntity($book);
$book->views()->delete();
$book->permissions()->delete();
$this->permissionService->deleteJointPermissionsForEntity($book);
$this->searchService->deleteEntityTerms($book);
$this->destroyEntityCommonRelations($book);
$book->delete();
}
/**
* Destroy a chapter and its relations.
* @param Chapter $chapter
* @throws \Throwable
*/
public function destroyChapter(Chapter $chapter)
{
@ -1186,11 +1238,7 @@ class EntityRepo
$page->save();
}
}
\Activity::removeEntity($chapter);
$chapter->views()->delete();
$chapter->permissions()->delete();
$this->permissionService->deleteJointPermissionsForEntity($chapter);
$this->searchService->deleteEntityTerms($chapter);
$this->destroyEntityCommonRelations($chapter);
$chapter->delete();
}
@ -1198,23 +1246,18 @@ class EntityRepo
* Destroy a given page along with its dependencies.
* @param Page $page
* @throws NotifyException
* @throws \Throwable
*/
public function destroyPage(Page $page)
{
\Activity::removeEntity($page);
$page->views()->delete();
$page->tags()->delete();
$page->revisions()->delete();
$page->permissions()->delete();
$this->permissionService->deleteJointPermissionsForEntity($page);
$this->searchService->deleteEntityTerms($page);
// Check if set as custom homepage
$customHome = setting('app-homepage', '0:');
if (intval($page->id) === intval(explode(':', $customHome)[0])) {
throw new NotifyException(trans('errors.page_custom_home_deletion'), $page->getUrl());
}
$this->destroyEntityCommonRelations($page);
// Delete Attached Files
$attachmentService = app(AttachmentService::class);
foreach ($page->attachments as $attachment) {
@ -1223,4 +1266,48 @@ class EntityRepo
$page->delete();
}
/**
* Destroy or handle the common relations connected to an entity.
* @param Entity $entity
* @throws \Throwable
*/
protected function destroyEntityCommonRelations(Entity $entity)
{
\Activity::removeEntity($entity);
$entity->views()->delete();
$entity->permissions()->delete();
$entity->tags()->delete();
$entity->comments()->delete();
$this->permissionService->deleteJointPermissionsForEntity($entity);
$this->searchService->deleteEntityTerms($entity);
}
/**
* Copy the permissions of a bookshelf to all child books.
* Returns the number of books that had permissions updated.
* @param Bookshelf $bookshelf
* @return int
* @throws \Throwable
*/
public function copyBookshelfPermissions(Bookshelf $bookshelf)
{
$shelfPermissions = $bookshelf->permissions()->get(['role_id', 'action'])->toArray();
$shelfBooks = $bookshelf->books()->get();
$updatedBookCount = 0;
foreach ($shelfBooks as $book) {
if (!userCan('restrictions-manage', $book)) {
continue;
}
$book->permissions()->delete();
$book->restricted = $bookshelf->restricted;
$book->permissions()->createMany($shelfPermissions);
$book->save();
$this->permissionService->buildJointPermissionsForEntity($book);
$updatedBookCount++;
}
return $updatedBookCount;
}
}

View File

@ -80,7 +80,7 @@ class PermissionsRepo
/**
* Updates an existing role.
* Ensure Admin role always has all permissions.
* Ensure Admin role always have core permissions.
* @param $roleId
* @param $roleData
* @throws PermissionsException
@ -90,13 +90,18 @@ class PermissionsRepo
$role = $this->role->findOrFail($roleId);
$permissions = isset($roleData['permissions']) ? array_keys($roleData['permissions']) : [];
$this->assignRolePermissions($role, $permissions);
if ($role->system_name === 'admin') {
$permissions = $this->permission->all()->pluck('id')->toArray();
$role->permissions()->sync($permissions);
$permissions = array_merge($permissions, [
'users-manage',
'user-roles-manage',
'restrictions-manage-all',
'restrictions-manage-own',
'settings-manage',
]);
}
$this->assignRolePermissions($role, $permissions);
$role->fill($roleData);
$role->save();
$this->permissionService->buildJointPermissionForRole($role);

View File

@ -76,14 +76,15 @@ class UserRepo
return $query->paginate($count);
}
/**
/**
* Creates a new user and attaches a role to them.
* @param array $data
* @param boolean $verifyEmail
* @return User
*/
public function registerNew(array $data)
public function registerNew(array $data, $verifyEmail = false)
{
$user = $this->create($data);
$user = $this->create($data, $verifyEmail);
$this->attachDefaultRole($user);
// Get avatar from gravatar and save
@ -141,15 +142,17 @@ class UserRepo
/**
* Create a new basic instance of user.
* @param array $data
* @param boolean $verifyEmail
* @return User
*/
public function create(array $data)
public function create(array $data, $verifyEmail = false)
{
return $this->user->forceCreate([
'name' => $data['name'],
'email' => $data['email'],
'password' => bcrypt($data['password']),
'email_confirmed' => false
'email_confirmed' => $verifyEmail
]);
}

View File

@ -316,25 +316,25 @@ class ImageService extends UploadService
$deletedPaths = [];
$this->image->newQuery()->whereIn('type', $types)
->chunk(1000, function($images) use ($types, $checkRevisions, &$deletedPaths, $dryRun) {
foreach ($images as $image) {
$searchQuery = '%' . basename($image->path) . '%';
$inPage = DB::table('pages')
->chunk(1000, function ($images) use ($types, $checkRevisions, &$deletedPaths, $dryRun) {
foreach ($images as $image) {
$searchQuery = '%' . basename($image->path) . '%';
$inPage = DB::table('pages')
->where('html', 'like', $searchQuery)->count() > 0;
$inRevision = false;
if ($checkRevisions) {
$inRevision = DB::table('page_revisions')
$inRevision = false;
if ($checkRevisions) {
$inRevision = DB::table('page_revisions')
->where('html', 'like', $searchQuery)->count() > 0;
}
}
if (!$inPage && !$inRevision) {
$deletedPaths[] = $image->path;
if (!$dryRun) {
$this->destroy($image);
}
}
}
});
if (!$inPage && !$inRevision) {
$deletedPaths[] = $image->path;
if (!$dryRun) {
$this->destroy($image);
}
}
}
});
return $deletedPaths;
}

View File

@ -330,14 +330,14 @@ class LdapService
$groupNames[$i] = str_replace(' ', '-', trim(strtolower($groupName)));
}
$roles = Role::query()->where(function(Builder $query) use ($groupNames) {
$roles = Role::query()->where(function (Builder $query) use ($groupNames) {
$query->whereIn('name', $groupNames);
foreach ($groupNames as $groupName) {
$query->orWhere('external_auth_id', 'LIKE', '%' . $groupName . '%');
}
})->get();
$matchedRoles = $roles->filter(function(Role $role) use ($groupNames) {
$matchedRoles = $roles->filter(function (Role $role) use ($groupNames) {
return $this->roleMatchesGroupNames($role, $groupNames);
});
@ -366,5 +366,4 @@ class LdapService
$roleName = str_replace(' ', '-', trim(strtolower($role->display_name)));
return in_array($roleName, $groupNames);
}
}

View File

@ -1,6 +1,7 @@
<?php namespace BookStack\Services;
use BookStack\Book;
use BookStack\Bookshelf;
use BookStack\Chapter;
use BookStack\Entity;
use BookStack\EntityPermission;
@ -25,6 +26,7 @@ class PermissionService
public $book;
public $chapter;
public $page;
public $bookshelf;
protected $db;
@ -38,22 +40,31 @@ class PermissionService
* PermissionService constructor.
* @param JointPermission $jointPermission
* @param EntityPermission $entityPermission
* @param Role $role
* @param Connection $db
* @param Bookshelf $bookshelf
* @param Book $book
* @param Chapter $chapter
* @param Page $page
* @param Role $role
*/
public function __construct(JointPermission $jointPermission, EntityPermission $entityPermission, Connection $db, Book $book, Chapter $chapter, Page $page, Role $role)
{
public function __construct(
JointPermission $jointPermission,
EntityPermission $entityPermission,
Role $role,
Connection $db,
Bookshelf $bookshelf,
Book $book,
Chapter $chapter,
Page $page
) {
$this->db = $db;
$this->jointPermission = $jointPermission;
$this->entityPermission = $entityPermission;
$this->role = $role;
$this->bookshelf = $bookshelf;
$this->book = $book;
$this->chapter = $chapter;
$this->page = $page;
// TODO - Update so admin still goes through filters
}
/**
@ -159,6 +170,12 @@ class PermissionService
$this->bookFetchQuery()->chunk(5, function ($books) use ($roles) {
$this->buildJointPermissionsForBooks($books, $roles);
});
// Chunk through all bookshelves
$this->bookshelf->newQuery()->select(['id', 'restricted', 'created_by'])
->chunk(50, function ($shelves) use ($roles) {
$this->buildJointPermissionsForShelves($shelves, $roles);
});
}
/**
@ -174,6 +191,20 @@ class PermissionService
}]);
}
/**
* @param Collection $shelves
* @param array $roles
* @param bool $deleteOld
* @throws \Throwable
*/
protected function buildJointPermissionsForShelves($shelves, $roles, $deleteOld = false)
{
if ($deleteOld) {
$this->deleteManyJointPermissionsForEntities($shelves->all());
}
$this->createManyJointPermissions($shelves, $roles);
}
/**
* Build joint permissions for an array of books
* @param Collection $books
@ -204,6 +235,7 @@ class PermissionService
/**
* Rebuild the entity jointPermissions for a particular entity.
* @param Entity $entity
* @throws \Throwable
*/
public function buildJointPermissionsForEntity(Entity $entity)
{
@ -214,7 +246,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 +260,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)
{
@ -254,6 +288,12 @@ class PermissionService
$this->bookFetchQuery()->chunk(20, function ($books) use ($roles) {
$this->buildJointPermissionsForBooks($books, $roles);
});
// Chunk through all bookshelves
$this->bookshelf->newQuery()->select(['id', 'restricted', 'created_by'])
->chunk(50, function ($shelves) use ($roles) {
$this->buildJointPermissionsForShelves($shelves, $roles);
});
}
/**
@ -412,7 +452,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);
}
@ -484,11 +524,6 @@ class PermissionService
*/
public function checkOwnableUserAccess(Ownable $ownable, $permission)
{
if ($this->isAdmin()) {
$this->clean();
return true;
}
$explodedPermission = explode('-', $permission);
$baseQuery = $ownable->where('id', '=', $ownable->id);
@ -581,17 +616,16 @@ class PermissionService
$query = $this->db->query()->select('*')->from($this->db->raw("({$pageSelect->toSql()} UNION {$chapterSelect->toSql()}) AS U"))
->mergeBindings($pageSelect)->mergeBindings($chapterSelect);
if (!$this->isAdmin()) {
$whereQuery = $this->db->table('joint_permissions as jp')->selectRaw('COUNT(*)')
->whereRaw('jp.entity_id=U.id')->whereRaw('jp.entity_type=U.entity_type')
->where('jp.action', '=', 'view')->whereIn('jp.role_id', $this->getRoles())
->where(function ($query) {
$query->where('jp.has_permission', '=', 1)->orWhere(function ($query) {
$query->where('jp.has_permission_own', '=', 1)->where('jp.created_by', '=', $this->currentUser()->id);
});
// Add joint permission filter
$whereQuery = $this->db->table('joint_permissions as jp')->selectRaw('COUNT(*)')
->whereRaw('jp.entity_id=U.id')->whereRaw('jp.entity_type=U.entity_type')
->where('jp.action', '=', 'view')->whereIn('jp.role_id', $this->getRoles())
->where(function ($query) {
$query->where('jp.has_permission', '=', 1)->orWhere(function ($query) {
$query->where('jp.has_permission_own', '=', 1)->where('jp.created_by', '=', $this->currentUser()->id);
});
$query->whereRaw("({$whereQuery->toSql()}) > 0")->mergeBindings($whereQuery);
}
});
$query->whereRaw("({$whereQuery->toSql()}) > 0")->mergeBindings($whereQuery);
$query->orderBy('draft', 'desc')->orderBy('priority', 'asc');
$this->clean();
@ -619,11 +653,6 @@ class PermissionService
});
}
if ($this->isAdmin()) {
$this->clean();
return $query;
}
$this->currentAction = $action;
return $this->entityRestrictionQuery($query);
}
@ -639,10 +668,6 @@ class PermissionService
*/
public function filterRestrictedEntityRelations($query, $tableName, $entityIdColumn, $entityTypeColumn, $action = 'view')
{
if ($this->isAdmin()) {
$this->clean();
return $query;
}
$this->currentAction = $action;
$tableDetails = ['tableName' => $tableName, 'entityIdColumn' => $entityIdColumn, 'entityTypeColumn' => $entityTypeColumn];
@ -675,11 +700,6 @@ class PermissionService
*/
public function filterRelatedPages($query, $tableName, $entityIdColumn)
{
if ($this->isAdmin()) {
$this->clean();
return $query;
}
$this->currentAction = 'view';
$tableDetails = ['tableName' => $tableName, 'entityIdColumn' => $entityIdColumn];
@ -704,19 +724,6 @@ class PermissionService
return $q;
}
/**
* Check if the current user is an admin.
* @return bool
*/
private function isAdmin()
{
if ($this->isAdminUser === null) {
$this->isAdminUser = ($this->currentUser()->id !== null) ? $this->currentUser()->hasSystemRole('admin') : false;
}
return $this->isAdminUser;
}
/**
* Get the current user
* @return User

View File

@ -1,13 +1,12 @@
<?php namespace BookStack\Services;
use BookStack\Http\Requests\Request;
use GuzzleHttp\Exception\ClientException;
use BookStack\Exceptions\SocialSignInAccountNotUsed;
use Laravel\Socialite\Contracts\Factory as Socialite;
use BookStack\Exceptions\SocialDriverNotConfigured;
use BookStack\Exceptions\SocialSignInException;
use BookStack\Exceptions\UserRegistrationException;
use BookStack\Repos\UserRepo;
use BookStack\SocialAccount;
use Laravel\Socialite\Contracts\User as SocialUser;
class SocialAuthService
{
@ -58,18 +57,13 @@ class SocialAuthService
/**
* Handle the social registration process on callback.
* @param $socialDriver
* @return \Laravel\Socialite\Contracts\User
* @throws SocialDriverNotConfigured
* @param string $socialDriver
* @param SocialUser $socialUser
* @return SocialUser
* @throws UserRegistrationException
*/
public function handleRegistrationCallback($socialDriver)
public function handleRegistrationCallback(string $socialDriver, SocialUser $socialUser)
{
$driver = $this->validateDriver($socialDriver);
// Get user details from social driver
$socialUser = $this->socialite->driver($driver)->user();
// Check social account has not already been used
if ($this->socialAccount->where('driver_id', '=', $socialUser->getId())->exists()) {
throw new UserRegistrationException(trans('errors.social_account_in_use', ['socialAccount'=>$socialDriver]), '/login');
@ -84,17 +78,26 @@ class SocialAuthService
}
/**
* Handle the login process on a oAuth callback.
* @param $socialDriver
* @return \Illuminate\Http\RedirectResponse|\Illuminate\Routing\Redirector
* Get the social user details via the social driver.
* @param string $socialDriver
* @return SocialUser
* @throws SocialDriverNotConfigured
* @throws SocialSignInException
*/
public function handleLoginCallback($socialDriver)
public function getSocialUser(string $socialDriver)
{
$driver = $this->validateDriver($socialDriver);
// Get user details from social driver
$socialUser = $this->socialite->driver($driver)->user();
return $this->socialite->driver($driver)->user();
}
/**
* Handle the login process on a oAuth callback.
* @param $socialDriver
* @param SocialUser $socialUser
* @return \Illuminate\Http\RedirectResponse|\Illuminate\Routing\Redirector
* @throws SocialSignInAccountNotUsed
*/
public function handleLoginCallback($socialDriver, SocialUser $socialUser)
{
$socialId = $socialUser->getId();
// Get any attached social accounts or users
@ -136,7 +139,7 @@ class SocialAuthService
$message .= trans('errors.social_account_register_instructions', ['socialAccount' => title_case($socialDriver)]);
}
throw new SocialSignInException($message, '/login');
throw new SocialSignInAccountNotUsed($message, '/login');
}
/**
@ -199,8 +202,28 @@ class SocialAuthService
}
/**
* @param string $socialDriver
* @param \Laravel\Socialite\Contracts\User $socialUser
* Check if the current config for the given driver allows auto-registration.
* @param string $driver
* @return bool
*/
public function driverAutoRegisterEnabled(string $driver)
{
return config('services.' . strtolower($driver) . '.auto_register') === true;
}
/**
* Check if the current config for the given driver allow email address auto-confirmation.
* @param string $driver
* @return bool
*/
public function driverAutoConfirmEmailEnabled(string $driver)
{
return config('services.' . strtolower($driver) . '.auto_confirm') === true;
}
/**
* @param string $socialDriver
* @param SocialUser $socialUser
* @return SocialAccount
*/
public function fillSocialAccount($socialDriver, $socialUser)

View File

@ -77,8 +77,20 @@ return [
*/
'locale' => env('APP_LANG', 'en'),
'locales' => ['en', 'ar', 'de', 'es', 'es_AR', 'fr', 'nl', 'pt_BR', 'sk', 'sv', 'ja', 'pl', 'it', 'ru', 'zh_CN', 'zh_TW'],
'locales' => ['en', 'de', 'es', 'es_AR', 'fr', 'nl', 'pt_BR', 'sk', 'sv', 'ja', 'pl', 'it', 'ru', 'zh_CN', 'zh_TW'],
/*
|--------------------------------------------------------------------------
| Right-to-left text control
|--------------------------------------------------------------------------
|
| Right-to-left text control is set to false by default since English
| is the primary supported application but this may be dynamically
| altered by the applications localization system.
|
*/
'rtl' => false,
/*
|--------------------------------------------------------------------------

View File

@ -48,6 +48,8 @@ return [
'client_secret' => env('GITHUB_APP_SECRET', false),
'redirect' => env('APP_URL') . '/login/service/github/callback',
'name' => 'GitHub',
'auto_register' => env('GITHUB_AUTO_REGISTER', false),
'auto_confirm' => env('GITHUB_AUTO_CONFIRM_EMAIL', false),
],
'google' => [
@ -55,6 +57,8 @@ return [
'client_secret' => env('GOOGLE_APP_SECRET', false),
'redirect' => env('APP_URL') . '/login/service/google/callback',
'name' => 'Google',
'auto_register' => env('GOOGLE_AUTO_REGISTER', false),
'auto_confirm' => env('GOOGLE_AUTO_CONFIRM_EMAIL', false),
],
'slack' => [
@ -62,6 +66,8 @@ return [
'client_secret' => env('SLACK_APP_SECRET', false),
'redirect' => env('APP_URL') . '/login/service/slack/callback',
'name' => 'Slack',
'auto_register' => env('SLACK_AUTO_REGISTER', false),
'auto_confirm' => env('SLACK_AUTO_CONFIRM_EMAIL', false),
],
'facebook' => [
@ -69,6 +75,8 @@ return [
'client_secret' => env('FACEBOOK_APP_SECRET', false),
'redirect' => env('APP_URL') . '/login/service/facebook/callback',
'name' => 'Facebook',
'auto_register' => env('FACEBOOK_AUTO_REGISTER', false),
'auto_confirm' => env('FACEBOOK_AUTO_CONFIRM_EMAIL', false),
],
'twitter' => [
@ -76,6 +84,8 @@ return [
'client_secret' => env('TWITTER_APP_SECRET', false),
'redirect' => env('APP_URL') . '/login/service/twitter/callback',
'name' => 'Twitter',
'auto_register' => env('TWITTER_AUTO_REGISTER', false),
'auto_confirm' => env('TWITTER_AUTO_CONFIRM_EMAIL', false),
],
'azure' => [
@ -84,6 +94,8 @@ return [
'tenant' => env('AZURE_TENANT', false),
'redirect' => env('APP_URL') . '/login/service/azure/callback',
'name' => 'Microsoft Azure',
'auto_register' => env('AZURE_AUTO_REGISTER', false),
'auto_confirm' => env('AZURE_AUTO_CONFIRM_EMAIL', false),
],
'okta' => [
@ -92,6 +104,8 @@ return [
'redirect' => env('APP_URL') . '/login/service/okta/callback',
'base_url' => env('OKTA_BASE_URL'),
'name' => 'Okta',
'auto_register' => env('OKTA_AUTO_REGISTER', false),
'auto_confirm' => env('OKTA_AUTO_CONFIRM_EMAIL', false),
],
'gitlab' => [
@ -100,6 +114,8 @@ return [
'redirect' => env('APP_URL') . '/login/service/gitlab/callback',
'instance_uri' => env('GITLAB_BASE_URI'), // Needed only for self hosted instances
'name' => 'GitLab',
'auto_register' => env('GITLAB_AUTO_REGISTER', false),
'auto_confirm' => env('GITLAB_AUTO_CONFIRM_EMAIL', false),
],
'twitch' => [
@ -107,12 +123,17 @@ return [
'client_secret' => env('TWITCH_APP_SECRET'),
'redirect' => env('APP_URL') . '/login/service/twitch/callback',
'name' => 'Twitch',
'auto_register' => env('TWITCH_AUTO_REGISTER', false),
'auto_confirm' => env('TWITCH_AUTO_CONFIRM_EMAIL', false),
],
'discord' => [
'client_id' => env('DISCORD_APP_ID'),
'client_secret' => env('DISCORD_APP_SECRET'),
'redirect' => env('APP_URL') . '/login/service/discord/callback',
'name' => 'Discord',
'auto_register' => env('DISCORD_AUTO_REGISTER', false),
'auto_confirm' => env('DISCORD_AUTO_CONFIRM_EMAIL', false),
],
'ldap' => [

View File

@ -109,7 +109,7 @@ return [
|
*/
'cookie' => 'laravel_session',
'cookie' => env('SESSION_COOKIE_NAME', 'bookstack_session'),
/*
|--------------------------------------------------------------------------

View File

@ -21,6 +21,14 @@ $factory->define(BookStack\User::class, function ($faker) {
];
});
$factory->define(BookStack\Bookshelf::class, function ($faker) {
return [
'name' => $faker->sentence,
'slug' => str_random(10),
'description' => $faker->paragraph
];
});
$factory->define(BookStack\Book::class, function ($faker) {
return [
'name' => $faker->sentence,

View File

@ -74,10 +74,6 @@ class CreateJointPermissionsTable extends Migration
// Update admin role with system name
DB::table('roles')->where('name', '=', 'admin')->update(['system_name' => 'admin']);
// Generate the new entity jointPermissions
$restrictionService = app(\BookStack\Services\PermissionService::class);
$restrictionService->buildJointPermissions();
}
/**

View File

@ -0,0 +1,101 @@
<?php
use Illuminate\Support\Facades\Schema;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Database\Migrations\Migration;
class CreateBookshelvesTable extends Migration
{
/**
* Run the migrations.
*
* @return void
*/
public function up()
{
Schema::create('bookshelves', function (Blueprint $table) {
$table->increments('id');
$table->string('name', 200);
$table->string('slug', 200);
$table->text('description');
$table->integer('created_by')->nullable()->default(null);
$table->integer('updated_by')->nullable()->default(null);
$table->boolean('restricted')->default(false);
$table->integer('image_id')->nullable()->default(null);
$table->timestamps();
$table->index('slug');
$table->index('created_by');
$table->index('updated_by');
$table->index('restricted');
});
Schema::create('bookshelves_books', function (Blueprint $table) {
$table->integer('bookshelf_id')->unsigned();
$table->integer('book_id')->unsigned();
$table->integer('order')->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) {
$dbOpName = strtolower(str_replace(' ', '-', $op));
$roleIdsWithBookPermission = DB::table('role_permissions')
->leftJoin('permission_role', 'role_permissions.id', '=', 'permission_role.permission_id')
->leftJoin('roles', 'roles.id', '=', 'permission_role.role_id')
->where('role_permissions.name', '=', 'book-' . $dbOpName)->get(['roles.id'])->pluck('id');
$permId = DB::table('role_permissions')->insertGetId([
'name' => 'bookshelf-' . $dbOpName,
'display_name' => $op . ' ' . 'BookShelves',
'created_at' => \Carbon\Carbon::now()->toDateTimeString(),
'updated_at' => \Carbon\Carbon::now()->toDateTimeString()
]);
$rowsToInsert = $roleIdsWithBookPermission->map(function($roleId) use ($permId) {
return [
'role_id' => $roleId,
'permission_id' => $permId
];
})->toArray();
// Assign view permission to all current roles
DB::table('permission_role')->insert($rowsToInsert);
}
}
/**
* Reverse the migrations.
*
* @return void
*/
public function down()
{
// Drop created permissions
$ops = ['bookshelf-create-all','bookshelf-create-own','bookshelf-delete-all','bookshelf-delete-own','bookshelf-update-all','bookshelf-update-own','bookshelf-view-all','bookshelf-view-own'];
$permissionIds = DB::table('role_permissions')->whereIn('name', $ops)
->get(['id'])->pluck('id')->toArray();
DB::table('permission_role')->whereIn('permission_id', $permissionIds)->delete();
DB::table('role_permissions')->whereIn('id', $permissionIds)->delete();
// Drop shelves table
Schema::dropIfExists('bookshelves_books');
Schema::dropIfExists('bookshelves');
// Drop related polymorphic items
DB::table('activities')->where('entity_type', '=', 'BookStack\Bookshelf')->delete();
DB::table('views')->where('viewable_type', '=', 'BookStack\Bookshelf')->delete();
DB::table('entity_permissions')->where('restrictable_type', '=', 'BookStack\Bookshelf')->delete();
DB::table('tags')->where('entity_type', '=', 'BookStack\Bookshelf')->delete();
DB::table('search_terms')->where('entity_type', '=', 'BookStack\Bookshelf')->delete();
DB::table('comments')->where('entity_type', '=', 'BookStack\Bookshelf')->delete();
}
}

View File

@ -21,23 +21,29 @@ class DummyContentSeeder extends Seeder
$role = \BookStack\Role::getRole('viewer');
$viewerUser->attachRole($role);
factory(\BookStack\Book::class, 5)->create(['created_by' => $editorUser->id, 'updated_by' => $editorUser->id])
->each(function($book) use ($editorUser) {
$chapters = factory(\BookStack\Chapter::class, 3)->create(['created_by' => $editorUser->id, 'updated_by' => $editorUser->id])
->each(function($chapter) use ($editorUser, $book){
$pages = factory(\BookStack\Page::class, 3)->make(['created_by' => $editorUser->id, 'updated_by' => $editorUser->id, 'book_id' => $book->id]);
$byData = ['created_by' => $editorUser->id, 'updated_by' => $editorUser->id];
factory(\BookStack\Book::class, 5)->create($byData)
->each(function($book) use ($editorUser, $byData) {
$chapters = factory(\BookStack\Chapter::class, 3)->create($byData)
->each(function($chapter) use ($editorUser, $book, $byData){
$pages = factory(\BookStack\Page::class, 3)->make(array_merge($byData, ['book_id' => $book->id]));
$chapter->pages()->saveMany($pages);
});
$pages = factory(\BookStack\Page::class, 3)->make(['created_by' => $editorUser->id, 'updated_by' => $editorUser->id]);
$pages = factory(\BookStack\Page::class, 3)->make($byData);
$book->chapters()->saveMany($chapters);
$book->pages()->saveMany($pages);
});
$largeBook = factory(\BookStack\Book::class)->create(['name' => 'Large book' . str_random(10), 'created_by' => $editorUser->id, 'updated_by' => $editorUser->id]);
$pages = factory(\BookStack\Page::class, 200)->make(['created_by' => $editorUser->id, 'updated_by' => $editorUser->id]);
$chapters = factory(\BookStack\Chapter::class, 50)->make(['created_by' => $editorUser->id, 'updated_by' => $editorUser->id]);
$largeBook = factory(\BookStack\Book::class)->create(array_merge($byData, ['name' => 'Large book' . str_random(10)]));
$pages = factory(\BookStack\Page::class, 200)->make($byData);
$chapters = factory(\BookStack\Chapter::class, 50)->make($byData);
$largeBook->pages()->saveMany($pages);
$largeBook->chapters()->saveMany($chapters);
$shelves = factory(\BookStack\Bookshelf::class, 10)->create($byData);
$largeBook->shelves()->attach($shelves->pluck('id'));
app(\BookStack\Services\PermissionService::class)->buildJointPermissions();
app(\BookStack\Services\SearchService::class)->indexAllEntities();
}

15
package-lock.json generated
View File

@ -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",

View File

@ -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",

View File

@ -35,8 +35,12 @@
<env name="STORAGE_TYPE" value="local"/>
<env name="GITHUB_APP_ID" value="aaaaaaaaaaaaaa"/>
<env name="GITHUB_APP_SECRET" value="aaaaaaaaaaaaaa"/>
<env name="GITHUB_AUTO_REGISTER" value=""/>
<env name="GITHUB_AUTO_CONFIRM_EMAIL" value=""/>
<env name="GOOGLE_APP_ID" value="aaaaaaaaaaaaaa"/>
<env name="GOOGLE_APP_SECRET" value="aaaaaaaaaaaaaa"/>
<env name="GOOGLE_AUTO_REGISTER" value=""/>
<env name="GOOGLE_AUTO_CONFIRM_EMAIL" value=""/>
<env name="APP_URL" value="http://bookstack.dev"/>
</php>
</phpunit>

View File

@ -0,0 +1,2 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path fill="none" d="M0 0h24v24H0V0z"/><path d="M1.088 2.566h17.42v17.42H1.088z" fill="none"/><path d="M4 20.058h15.892V22H4z"/><path d="M2.902 1.477h17.42v17.42H2.903z" fill="none"/><g><path d="M6.658 3.643V18h-2.38V3.643zM11.326 3.643V18H8.947V3.643zM14.722 3.856l5.613 13.214-2.19.93-5.613-13.214z"/></g></svg>

After

Width:  |  Height:  |  Size: 375 B

View File

@ -0,0 +1,22 @@
class HomepageControl {
constructor(elem) {
this.elem = elem;
this.typeControl = elem.querySelector('[name="setting-app-homepage-type"]');
this.pagePickerContainer = elem.querySelector('[page-picker-container]');
this.typeControl.addEventListener('change', this.controlPagePickerVisibility.bind(this));
this.controlPagePickerVisibility();
}
controlPagePickerVisibility() {
const showPagePicker = this.typeControl.value === 'page';
this.pagePickerContainer.style.display = (showPagePicker ? 'block' : 'none');
}
}
module.exports = HomepageControl;

View File

@ -18,6 +18,8 @@ let componentMapping = {
'collapsible': require('./collapsible'),
'toggle-switch': require('./toggle-switch'),
'page-display': require('./page-display'),
'shelf-sort': require('./shelf-sort'),
'homepage-control': require('./homepage-control'),
};
window.components = {};

View File

@ -8,6 +8,7 @@ class MarkdownEditor {
constructor(elem) {
this.elem = elem;
this.textDirection = document.getElementById('page-editor').getAttribute('text-direction');
this.markdown = new MarkdownIt({html: true});
this.markdown.use(mdTasksLists, {label: true});
@ -98,6 +99,9 @@ class MarkdownEditor {
codeMirrorSetup() {
let cm = this.cm;
// Text direction
// cm.setOption('direction', this.textDirection);
cm.setOption('direction', 'ltr'); // Will force to remain as ltr for now due to issues when HTML is in editor.
// Custom key commands
let metaKey = code.getMetaKey();
const extraKeys = {};

View File

@ -128,7 +128,7 @@ class PageDisplay {
let $bookTreeParent = $sidebar.parent();
// Check the page is scrollable and the content is taller than the tree
let pageScrollable = ($(document).height() > $window.height()) && ($sidebar.height() < $('.page-content').height());
let pageScrollable = ($(document).height() > ($window.height() + 40)) && ($sidebar.height() < $('.page-content').height());
// Get current tree's width and header height
let headerHeight = $("#header").height() + $(".toolbar").height();

View File

@ -15,18 +15,20 @@ class PagePicker {
}
setupListeners() {
// Select click
this.selectButton.addEventListener('click', event => {
window.EntitySelectorPopup.show(entity => {
this.setValue(entity.id, entity.name);
});
});
this.selectButton.addEventListener('click', this.showPopup.bind(this));
this.display.parentElement.addEventListener('click', this.showPopup.bind(this));
this.resetButton.addEventListener('click', event => {
this.setValue('', '');
});
}
showPopup() {
window.EntitySelectorPopup.show(entity => {
this.setValue(entity.id, entity.name);
});
}
setValue(value, name) {
this.value = value;
this.input.value = value;

View File

@ -0,0 +1,71 @@
class ShelfSort {
constructor(elem) {
this.elem = elem;
this.sortGroup = this.initSortable();
this.input = document.getElementById('books-input');
this.setupListeners();
}
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)
});
}
setupListeners() {
this.elem.addEventListener('click', event => {
const sortItem = event.target.closest('.scroll-box-item:not(.instruction)');
if (sortItem) {
event.preventDefault();
this.sortItemClick(sortItem);
}
});
}
/**
* Called when a sort item is clicked.
* @param {Element} sortItem
*/
sortItemClick(sortItem) {
const lists = this.elem.querySelectorAll('.scroll-box');
const newList = Array.from(lists).filter(list => sortItem.parentElement !== list);
if (newList.length > 0) {
newList[0].appendChild(sortItem);
}
this.onChange();
}
onDrop($item, container, _super) {
this.onChange();
_super($item, container);
}
onChange() {
const data = this.sortGroup.sortable('serialize').get();
this.input.value = data[0].map(item => item.id).join(',');
const instruction = this.elem.querySelector('.scroll-box-item.instruction');
instruction.parentNode.insertBefore(instruction, instruction.parentNode.children[0]);
}
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;

View File

@ -370,6 +370,7 @@ class WysiwygEditor {
constructor(elem) {
this.elem = elem;
this.textDirection = document.getElementById('page-editor').getAttribute('text-direction');
this.plugins = "image table textcolor paste link autolink fullscreen imagetools code customhr autosave lists codeeditor media";
this.loadPlugins();
@ -385,6 +386,14 @@ class WysiwygEditor {
drawIoPlugin();
this.plugins += ' drawio';
}
if (this.textDirection === 'rtl') {
this.plugins += ' directionality'
}
}
getToolBar() {
const textDirPlugins = this.textDirection === 'rtl' ? 'ltr rtl' : '';
return `undo redo | styleselect | bold italic underline strikethrough superscript subscript | forecolor backcolor | alignleft aligncenter alignright alignjustify | bullist numlist outdent indent | table image-insert link hr drawio media | removeformat code ${textDirPlugins} fullscreen`
}
getTinyMceConfig() {
@ -397,6 +406,7 @@ class WysiwygEditor {
body_class: 'page-content',
browser_spellcheck: true,
relative_urls: false,
directionality : this.textDirection,
remove_script_host: false,
document_base_url: window.baseUrl('/'),
statusbar: false,
@ -407,7 +417,7 @@ class WysiwygEditor {
valid_children: "-div[p|h1|h2|h3|h4|h5|h6|blockquote],+div[pre],+div[img]",
plugins: this.plugins,
imagetools_toolbar: 'imageoptions',
toolbar: "undo redo | styleselect | bold italic underline strikethrough superscript subscript | forecolor backcolor | alignleft aligncenter alignright alignjustify | bullist numlist outdent indent | table image-insert link hr drawio media | removeformat code fullscreen",
toolbar: this.getToolBar(),
content_style: "body {padding-left: 15px !important; padding-right: 15px !important; margin:0!important; margin-left:auto!important;margin-right:auto!important;}",
style_formats: [
{title: "Header Large", format: "h2"},

View File

@ -113,7 +113,7 @@ function addCopyIcon(cmInstance) {
copyButton.classList.add('success');
setTimeout(() => {
copyButton.classList.remove('success');
}, 360);
}, 240);
});
}
@ -157,6 +157,7 @@ function wysiwygView(elem) {
newWrap.className = 'CodeMirrorContainer';
newWrap.setAttribute('data-lang', lang);
newWrap.setAttribute('dir', 'ltr');
newTextArea.style.display = 'none';
elem.parentNode.replaceChild(newWrap, elem);

View File

@ -419,11 +419,12 @@ span.CodeMirror-selectedtext { background: none; }
cursor: pointer;
fill: #444;
z-index: 5;
transition: all ease-in 180ms;
transition: all ease-in 240ms;
user-select: none;
opacity: 0.7;
opacity: 0;
pointer-events: none;
svg {
transition: transform ease-in 180ms;
transition: transform ease-in 240ms;
transform: translateY(0);
}
&.success {
@ -437,4 +438,5 @@ span.CodeMirror-selectedtext { background: none; }
.CodeMirror:hover .CodeMirror-copy {
user-select: all;
opacity: 1;
pointer-events: all;
}

View File

@ -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;

View File

@ -367,7 +367,7 @@ ul.pagination {
padding: $-xs $-m;
line-height: 1.2;
}
a {
a, button {
display: block;
padding: $-xs $-m;
color: #555;
@ -382,6 +382,10 @@ ul.pagination {
width: 16px;
}
}
button {
width: 100%;
text-align: left;
}
li.border-bottom {
border-bottom: 1px solid #DDD;
}
@ -408,32 +412,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;
}
}

View File

@ -41,6 +41,9 @@ table.table {
.text-center {
text-align: center;
}
td.actions {
overflow: visible;
}
}
table.no-style {

View File

@ -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;
@ -343,6 +351,7 @@ ul, ol {
}
ul {
padding-left: $-m * 1.3;
padding-right: $-m * 1.3;
list-style: disc;
ul {
list-style: circle;
@ -357,6 +366,7 @@ ul {
ol {
list-style: decimal;
padding-left: $-m * 2;
padding-right: $-m * 2;
}
li.checkbox-item, li.task-list-item {

View File

@ -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;

View File

@ -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;

View File

@ -0,0 +1,42 @@
<?php
return [
/**
* Activity text strings.
* Is used for all the text within activity logs & notifications.
*/
// Pages
'page_create' => 'تم إنشاء صفحة',
'page_create_notification' => 'تم إنشاء الصفحة بنجاح',
'page_update' => 'تم تحديث الصفحة',
'page_update_notification' => 'تم تحديث الصفحة بنجاح',
'page_delete' => 'تم حذف الصفحة',
'page_delete_notification' => 'تم حذف الصفحة بنجاح',
'page_restore' => 'تمت استعادة الصفحة',
'page_restore_notification' => 'تمت استعادة الصفحة بنجاح',
'page_move' => 'تم نقل الصفحة',
// Chapters
'chapter_create' => 'تم إنشاء فصل',
'chapter_create_notification' => 'تم إنشاء فصل بنجاح',
'chapter_update' => 'تم تحديث الفصل',
'chapter_update_notification' => 'تم تحديث الفصل بنجاح',
'chapter_delete' => 'تم حذف الفصل',
'chapter_delete_notification' => 'تم حذف الفصل بنجاح',
'chapter_move' => 'تم نقل الفصل',
// Books
'book_create' => 'تم إنشاء كتاب',
'book_create_notification' => 'تم إنشاء كتاب بنجاح',
'book_update' => 'تم تحديث الكتاب',
'book_update_notification' => 'تم تحديث الكتاب بنجاح',
'book_delete' => 'تم حذف الكتاب',
'book_delete_notification' => 'تم حذف الكتاب بنجاح',
'book_sort' => 'تم سرد الكتاب',
'book_sort_notification' => 'تمت إعادة سرد الكتاب بنجاح',
// Other
'commented_on' => 'تم التعليق',
];

View File

@ -0,0 +1,76 @@
<?php
return [
/*
|--------------------------------------------------------------------------
| Authentication Language Lines
|--------------------------------------------------------------------------
|
| The following language lines are used during authentication for various
| messages that we need to display to the user. You are free to modify
| these language lines according to your application's requirements.
|
*/
'failed' => 'البيانات المعطاة لا توافق سجلاتنا.',
'throttle' => 'تجاوزت الحد الأقصى من المحاولات. الرجاء المحاولة مرة أخرى بعد :seconds seconds.',
/**
* Login & Register
*/
'sign_up' => 'إنشاء حساب',
'log_in' => 'تسجيل الدخول',
'log_in_with' => 'تسجيل الدخول باستخدام :socialDriver',
'sign_up_with' => 'إنشاء حساب باستخدام :socialDriver',
'logout' => 'تسجيل الخروج',
'name' => 'الاسم',
'username' => 'اسم المستخدم',
'email' => 'البريد الإلكتروني',
'password' => 'كلمة المرور',
'password_confirm' => 'تأكيد كلمة المرور',
'password_hint' => 'يجب أن تكون أكثر من 5 حروف',
'forgot_password' => 'نسيت كلمة المرور؟',
'remember_me' => 'تذكرني',
'ldap_email_hint' => 'الرجاء إدخال عنوان بريد إلكتروني لاستخدامه مع الحساب.',
'create_account' => 'إنشاء حساب',
'social_login' => 'تسجيل الدخول باستخدام حسابات التواصل الاجتماعي',
'social_registration' => 'إنشاء حساب باستخدام حسابات التواصل الاجتماعي',
'social_registration_text' => 'إنشاء حساب والدخول باستخدام خدمة أخرى.',
'register_thanks' => 'شكراً لتسجيل حسابك!',
'register_confirm' => 'الرجاء مراجعة البريد الإلكتروني والضغط على زر التأكيد لاستخدام :appName.',
'registrations_disabled' => 'التسجيل مغلق حالياً',
'registration_email_domain_invalid' => 'المجال الخاص بالبريد الإلكتروني لا يملك حق الوصول لهذا التطبيق',
'register_success' => 'شكراً لإنشاء حسابكم! تم تسجيلكم ودخولكم للحساب الخاص بكم.',
/**
* Password Reset
*/
'reset_password' => 'استعادة كلمة المرور',
'reset_password_send_instructions' => 'أدخل بريدك الإلكتروني بالأسفل وسيتم إرسال رسالة برابط لاستعادة كلمة المرور.',
'reset_password_send_button' => 'أرسل رابط الاستعادة',
'reset_password_sent_success' => 'تم إرسال رابط استعادة كلمة المرور إلى :email.',
'reset_password_success' => 'تمت استعادة كلمة المرور بنجاح.',
'email_reset_subject' => 'استعد كلمة المرور الخاصة بتطبيق :appName',
'email_reset_text' => 'تم إرسال هذه الرسالة بسبب تلقينا لطلب استعادة كلمة المرور الخاصة بحسابكم.',
'email_reset_not_requested' => 'إذا لم يتم طلب استعادة كلمة المرور من قبلكم, فلا حاجة لاتخاذ أية خطوات.',
/**
* Email Confirmation
*/
'email_confirm_subject' => 'تأكيد بريدكم الإلكتروني لتطبيق :appName',
'email_confirm_greeting' => 'شكرا لانضمامكم إلى :appName!',
'email_confirm_text' => 'الرجاء تأكيد بريدكم الإلكتروني بالضغط على الزر أدناه:',
'email_confirm_action' => 'تأكيد البريد الإلكتروني',
'email_confirm_send_error' => 'تأكيد البريد الإلكتروني مطلوب ولكن النظام لم يستطع إرسال الرسالة. تواصل مع مشرف النظام للتأكد من إعدادات البريد.',
'email_confirm_success' => 'تم تأكيد بريدكم الإلكتروني!',
'email_confirm_resent' => 'تمت إعادة إرسال رسالة التأكيد. الرجاء مراجعة صندوق الوارد',
'email_not_confirmed' => 'لم يتم تأكيد البريد الإلكتروني',
'email_not_confirmed_text' => 'لم يتم بعد تأكيد عنوان البريد الإلكتروني.',
'email_not_confirmed_click_link' => 'الرجاء الضغط على الرابط المرسل إلى بريدكم الإلكتروني بعد تسجيلكم.',
'email_not_confirmed_resend' => 'إذا لم يتم إيجاد الرسالة, بإمكانكم إعادة إرسال رسالة التأكيد عن طريق تعبئة النموذج أدناه.',
'email_not_confirmed_resend_button' => 'إعادة إرسال رسالة التأكيد',
];

View File

@ -0,0 +1,67 @@
<?php
return [
/**
* Buttons
*/
'cancel' => 'إلغاء',
'confirm' => 'تأكيد',
'back' => 'رجوع',
'save' => 'حفظ',
'continue' => 'استمرار',
'select' => 'تحديد',
'more' => 'المزيد',
/**
* Form Labels
*/
'name' => 'الاسم',
'description' => 'الوصف',
'role' => 'الدور',
'cover_image' => 'صورة الغلاف',
'cover_image_description' => 'الصورة يجب أن تكون مقاربة لحجم 440×250 بكسل.',
/**
* Actions
*/
'actions' => 'إجراءات',
'view' => 'عرض',
'create' => 'إنشاء',
'update' => 'تحديث',
'edit' => 'تعديل',
'sort' => 'سرد',
'move' => 'نقل',
'copy' => 'نسخ',
'reply' => 'رد',
'delete' => 'حذف',
'search' => 'بحث',
'search_clear' => 'مسح البحث',
'reset' => 'إعادة تعيين',
'remove' => 'إزالة',
'add' => 'إضافة',
/**
* Misc
*/
'deleted_user' => 'حذف مستخدم',
'no_activity' => 'لا يوجد نشاط لعرضه',
'no_items' => 'لا توجد عناصر متوفرة',
'back_to_top' => 'العودة للبداية',
'toggle_details' => 'عرض / إخفاء التفاصيل',
'toggle_thumbnails' => 'عرض / إخفاء الصور المصغرة',
'details' => 'التفاصيل',
'grid_view' => 'عرض شبكي',
'list_view' => 'عرض منسدل',
/**
* Header
*/
'view_profile' => 'عرض الملف الشخصي',
'edit_profile' => 'تعديل الملف الشخصي',
/**
* Email Content
*/
'email_action_help' => 'إذا واجهتكم مشكلة بضغط زر ":actionText" فبإمكانكم نسخ الرابط أدناه ولصقه بالمتصفح:',
'email_rights' => 'جميع الحقوق محفوظة',
];

View File

@ -0,0 +1,34 @@
<?php
return [
/**
* Image Manager
*/
'image_select' => 'تحديد صورة',
'image_all' => 'الكل',
'image_all_title' => 'عرض جميع الصور',
'image_book_title' => 'عرض الصور المرفوعة لهذا الكتاب',
'image_page_title' => 'عرض الصور المرفوعة لهذه الصفحة',
'image_search_hint' => 'البحث باستخدام اسم الصورة',
'image_uploaded' => 'وقت الرفع :uploadedDate',
'image_load_more' => 'المزيد',
'image_image_name' => 'اسم الصورة',
'image_delete_used' => 'هذه الصورة مستخدمة بالصفحات أدناه.',
'image_delete_confirm' => 'اضغط زر الحذف مرة أخرى لتأكيد حذف هذه الصورة.',
'image_select_image' => 'تحديد الصورة',
'image_dropzone' => 'قم بإسقاط الصورة أو اضغط هنا للرفع',
'images_deleted' => 'تم حذف الصور',
'image_preview' => 'معاينة الصور',
'image_upload_success' => 'تم رفع الصورة بنجاح',
'image_update_success' => 'تم تحديث تفاصيل الصورة بنجاح',
'image_delete_success' => 'تم حذف الصورة بنجاح',
'image_upload_remove' => 'إزالة',
/**
* Code editor
*/
'code_editor' => 'تعديل الشفرة',
'code_language' => 'لغة الشفرة',
'code_content' => 'محتويات الشفرة',
'code_save' => 'حفظ الشفرة',
];

View File

@ -0,0 +1,268 @@
<?php
return [
/**
* Shared
*/
'recently_created' => 'أنشئت مؤخراً',
'recently_created_pages' => 'صفحات أنشئت مؤخراً',
'recently_updated_pages' => 'صفحات حُدثت مؤخراً',
'recently_created_chapters' => 'فصول أنشئت مؤخراً',
'recently_created_books' => 'كتب أنشئت مؤخراً',
'recently_update' => 'حُدثت مؤخراً',
'recently_viewed' => 'عُرضت مؤخراً',
'recent_activity' => 'نشاطات حديثة',
'create_now' => 'أنشئ الآن',
'revisions' => 'مراجعات',
'meta_revision' => 'مراجعة #:revisionCount',
'meta_created' => 'أنشئ :timeLength',
'meta_created_name' => 'أنشئ :timeLength بواسطة :user',
'meta_updated' => 'مُحدث :timeLength',
'meta_updated_name' => 'مُحدث :timeLength بواسطة :user',
'entity_select' => 'Entity Select', // جار البحث عن الترجمة الأنسب
'images' => 'صور',
'my_recent_drafts' => 'مسوداتي الحديثة',
'my_recently_viewed' => 'ما عرضته مؤخراً',
'no_pages_viewed' => 'لم تستعرض أي صفحات',
'no_pages_recently_created' => 'لم يتم إنشاء أي صفحات مؤخراً',
'no_pages_recently_updated' => 'لم يتم تحديث أي صفحات مؤخراً',
'export' => 'تصدير',
'export_html' => 'صفحة ويب',
'export_pdf' => 'ملف PDF',
'export_text' => 'ملف نص عادي',
/**
* Permissions and restrictions
*/
'permissions' => 'الأذونات',
'permissions_intro' => 'في حال التفعيل, ستتم تبدية هذه الأذونات على أذونات الأدوار.',
'permissions_enable' => 'تفعيل الأذونات المخصصة',
'permissions_save' => 'حفظ الأذونات',
/**
* Search //
*/
'search_results' => 'نتائج البحث',
'search_total_results_found' => 'عدد النتائج :count|مجموع النتائج :count',
'search_clear' => 'مسح البحث',
'search_no_pages' => 'لم يطابق بحثكم أي صفحة',
'search_for_term' => 'ابحث عن :term',
'search_more' => 'المزيد من النتائج',
'search_filters' => 'تصفية البحث',
'search_content_type' => 'نوع المحتوى',
'search_exact_matches' => 'نتائج مطابقة تماماً',
'search_tags' => 'بحث الوسوم',
'search_viewed_by_me' => 'تم استعراضها من قبلي',
'search_not_viewed_by_me' => 'لم يتم استعراضها من قبلي',
'search_permissions_set' => 'حزمة الأذونات',
'search_created_by_me' => 'أنشئت بواسطتي',
'search_updated_by_me' => 'حُدثت بواسطتي',
'search_updated_before' => 'حدثت قبل',
'search_updated_after' => 'حدثت بعد',
'search_created_before' => 'أنشئت قبل',
'search_created_after' => 'أنشئت بعد',
'search_set_date' => 'تحديد التاريخ',
'search_update' => 'تحديث البحث',
/**
* Books
*/
'book' => 'كتاب',
'books' => 'كتب',
'x_books' => ':count كتاب|:count كتب',
'books_empty' => 'لم يتم إنشاء أي كتب',
'books_popular' => 'كتب رائجة',
'books_recent' => 'كتب حديثة',
'books_new' => 'كتب جديدة',
'books_popular_empty' => 'الكتب الأكثر رواجاً ستظهر هنا.',
'books_new_empty' => 'الكتب المنشأة مؤخراً ستظهر هنا.',
'books_create' => 'إنشاء كتاب جديد',
'books_delete' => 'حذف الكتاب',
'books_delete_named' => 'حذف كتاب :bookName',
'books_delete_explain' => 'سيتم حذف كتاب \':bookName\'. ستتم إزالة جميع الفصول والصفحات.',
'books_delete_confirmation' => 'تأكيد حذف الكتاب؟',
'books_edit' => 'تعديل الكتاب',
'books_edit_named' => 'تعديل كتاب :bookName',
'books_form_book_name' => 'اسم الكتاب',
'books_save' => 'حفظ الكتاب',
'books_permissions' => 'أذونات الكتاب',
'books_permissions_updated' => 'تم تحديث أذونات الكتاب',
'books_empty_contents' => 'لم يتم إنشاء أي صفحات أو فصول لهذا الكتاب.',
'books_empty_create_page' => 'إنشاء صفحة جديدة',
'books_empty_or' => 'أو',
'books_empty_sort_current_book' => 'فرز الكتاب الحالي',
'books_empty_add_chapter' => 'إضافة فصل',
'books_permissions_active' => 'أذونات الكتاب مفعلة',
'books_search_this' => 'البحث في هذا الكتاب',
'books_navigation' => 'تصفح الكتاب',
'books_sort' => 'فرز محتويات الكتاب',
'books_sort_named' => 'فرز كتاب :bookName',
'books_sort_show_other' => 'عرض كتب أخرى',
'books_sort_save' => 'حفظ الترتيب الجديد',
/**
* Chapters
*/
'chapter' => 'فصل',
'chapters' => 'فصول',
'x_chapters' => ':count فصل|:count فصول',
'chapters_popular' => 'فصول رائجة',
'chapters_new' => 'فصل جديد',
'chapters_create' => 'إنشاء فصل جديد',
'chapters_delete' => 'حذف الفصل',
'chapters_delete_named' => 'حذف فصل :chapterName',
'chapters_delete_explain' => 'سيتم حذف فصل \':chapterName\'. جميع الصفحات ستزال وستتم إضافتها مباشرة للكتاب الرئيسي.',
'chapters_delete_confirm' => 'تأكيد حذف الفصل؟',
'chapters_edit' => 'تعديل الفصل',
'chapters_edit_named' => 'تعديل فصل :chapterName',
'chapters_save' => 'حفظ الفصل',
'chapters_move' => 'نقل الفصل',
'chapters_move_named' => 'نقل فصل :chapterName',
'chapter_move_success' => 'تم نقل الفصل إلى :bookName',
'chapters_permissions' => 'أذونات الفصل',
'chapters_empty' => 'لا توجد أي صفحات في هذا الفصل حالياً',
'chapters_permissions_active' => 'أذونات الفصل مفعلة',
'chapters_permissions_success' => 'تم تحديث أذونات الفصل',
'chapters_search_this' => 'البحث في هذا الفصل',
/**
* Pages
*/
'page' => 'صفحة',
'pages' => 'صفحات',
'x_pages' => ':count صفحة|:count صفحات',
'pages_popular' => 'صفحات رائجة',
'pages_new' => 'صفحة جديدة',
'pages_attachments' => 'مرفقات',
'pages_navigation' => 'تصفح الصفحة',
'pages_delete' => 'حذف الصفحة',
'pages_delete_named' => 'حذف صفحة :pageName',
'pages_delete_draft_named' => 'حذف مسودة :pageName',
'pages_delete_draft' => 'حذف المسودة',
'pages_delete_success' => 'تم حذف الصفحة',
'pages_delete_draft_success' => 'تم حذف المسودة',
'pages_delete_confirm' => 'تأكيد حذف الصفحة؟',
'pages_delete_draft_confirm' => 'تأكيد حذف المسودة؟',
'pages_editing_named' => ':pageName قيد التعديل',
'pages_edit_toggle_header' => 'إظهار / إخفاء الترويسة',
'pages_edit_save_draft' => 'حفظ المسودة',
'pages_edit_draft' => 'تعديل مسودة الصفحة',
'pages_editing_draft' => 'المسودة قيد التعديل',
'pages_editing_page' => 'الصفحة قيد التعديل',
'pages_edit_draft_save_at' => 'تم خفظ المسودة في ',
'pages_edit_delete_draft' => 'حذف المسودة',
'pages_edit_discard_draft' => 'التخلص من المسودة',
'pages_edit_set_changelog' => 'تثبيت سجل التعديل',
'pages_edit_enter_changelog_desc' => 'ضع وصف مختصر للتعديلات التي تمت',
'pages_edit_enter_changelog' => 'أدخل سجل التعديل',
'pages_save' => 'حفظ الصفحة',
'pages_title' => 'عنوان الصفحة',
'pages_name' => 'اسم الصفحة',
'pages_md_editor' => 'المحرر',
'pages_md_preview' => 'معاينة',
'pages_md_insert_image' => 'إدخال صورة',
'pages_md_insert_link' => 'Insert Entity Link', // جار البحث عن الترجمة الأنسب
'pages_md_insert_drawing' => 'إدخال رسمة',
'pages_not_in_chapter' => 'صفحة ليست في فصل',
'pages_move' => 'نقل الصفحة',
'pages_move_success' => 'تم نقل الصفحة إلى ":parentName"',
'pages_copy' => 'نسخ الصفحة',
'pages_copy_desination' => 'نسخ مكان الوصول',
'pages_copy_success' => 'تم نسخ الصفحة بنجاح',
'pages_permissions' => 'أذونات الصفحة',
'pages_permissions_success' => 'تم تحديث أذونات الصفحة',
'pages_revision' => 'مراجعة',
'pages_revisions' => 'مراجعات الصفحة',
'pages_revisions_named' => 'مراجعات صفحة :pageName',
'pages_revision_named' => 'مراجعة صفحة :pageName',
'pages_revisions_created_by' => 'أنشئ بواسطة',
'pages_revisions_date' => 'تاريخ المراجعة',
'pages_revisions_number' => '#',
'pages_revisions_changelog' => 'سجل التعديل',
'pages_revisions_changes' => 'التعديلات',
'pages_revisions_current' => 'النسخة الحالية',
'pages_revisions_preview' => 'معاينة',
'pages_revisions_restore' => 'استرجاع',
'pages_revisions_none' => 'لا توجد مراجعات لهذه الصفحة',
'pages_copy_link' => 'نسخ الرابط',
'pages_edit_content_link' => 'تعديل المحتوى',
'pages_permissions_active' => 'أذونات الصفحة مفعلة',
'pages_initial_revision' => 'نشر مبدئي',
'pages_initial_name' => 'صفحة جديدة',
'pages_editing_draft_notification' => 'جار تعديل مسودة لم يتم حفظها من :timeDiff.',
'pages_draft_edited_notification' => 'تم تحديث هذه الصفحة منذ ذلك الوقت. من الأفضل التخلص من هذه المسودة.',
'pages_draft_edit_active' => [
'start_a' => ':count من المستخدمين بدأوا بتعديل هذه الصفحة',
'start_b' => ':userName بدأ بتعديل هذه الصفحة',
'time_a' => 'منذ أن تم تحديث هذه الصفحة',
'time_b' => 'في آخر :minCount دقيقة/دقائق',
'message' => ':start :time. Take care not to overwrite each other\'s updates!', // جار البحث عن الترجمة الأنسب
],
'pages_draft_discarded' => 'تم التخلص من المسودة. تم تحديث المحرر بمحتوى الصفحة الحالي',
/**
* Editor sidebar
*/
'page_tags' => 'وسوم الصفحة',
'chapter_tags' => 'وسوم الفصل',
'book_tags' => 'وسوم الكتاب',
'tag' => 'وسم',
'tags' => 'وسوم',
'tag_value' => 'قيمة الوسم (اختياري)',
'tags_explain' => "إضافة الوسوم تساعد بترتيب وتقسيم المحتوى. \n من الممكن وضع قيمة لكل وسم لترتيب أفضل وأدق.",
'tags_add' => 'إضافة وسم آخر',
'attachments' => 'المرفقات',
'attachments_explain' => 'ارفع بعض الملفات أو أرفق بعض الروابط لعرضها بصفحتك. ستكون الملفات والروابط معروضة في الشريط الجانبي للصفحة.',
'attachments_explain_instant_save' => 'سيتم حفظ التغييرات هنا بلحظتها',
'attachments_items' => 'العناصر المرفقة',
'attachments_upload' => 'رفع ملف',
'attachments_link' => 'إرفاق رابط',
'attachments_set_link' => 'تحديد الرابط',
'attachments_delete_confirm' => 'اضغط على زر الحذف مرة أخرى لتأكيد حذف المرفق.',
'attachments_dropzone' => 'أسقط الملفات أو اضغط هنا لإرفاق ملف',
'attachments_no_files' => 'لم يتم رفع أي ملفات',
'attachments_explain_link' => 'بالإمكان إرفاق رابط في حال عدم تفضيل رفع ملف. قد يكون الرابط لصفحة أخرى أو لملف في أحد خدمات التخزين السحابي.',
'attachments_link_name' => 'اسم الرابط',
'attachment_link' => 'رابط المرفق',
'attachments_link_url' => 'Link to file', // جار البحث عن الترجمة الأنسب - هل المقصود الربط بالملف أو رابط يشير إلى ملف
'attachments_link_url_hint' => 'رابط الموقع أو الملف',
'attach' => 'Attach',
'attachments_edit_file' => 'تعديل الملف',
'attachments_edit_file_name' => 'اسم الملف',
'attachments_edit_drop_upload' => 'أسقط الملفات أو اضغط هنا للرفع والاستبدال',
'attachments_order_updated' => 'تم تحديث ترتيب المرفقات',
'attachments_updated_success' => 'تم تحديث تفاصيل المرفق',
'attachments_deleted' => 'تم حذف المرفق',
'attachments_file_uploaded' => 'تم رفع الملف بنجاح',
'attachments_file_updated' => 'تم تحديث الملف بنجاح',
'attachments_link_attached' => 'تم إرفاق الرابط بالصفحة بنجاح',
/**
* Profile View
*/
'profile_user_for_x' => 'User for :time', // جار البحث عن الترجمة الأنسب
'profile_created_content' => 'المحتوى المنشأ',
'profile_not_created_pages' => 'لم يتم إنشاء أي صفحات بواسطة :userName',
'profile_not_created_chapters' => 'لم يتم إنشاء أي فصول بواسطة :userName',
'profile_not_created_books' => 'لم يتم إنشاء أي كتب بواسطة :userName',
/**
* Comments
*/
'comment' => 'تعليق',
'comments' => 'تعليقات',
'comment_add' => 'إضافة تعليق',
'comment_placeholder' => 'ضع تعليقاً هنا',
'comment_count' => '{0} ا توجد تعليقات|{1} تعليق واحد|{2} تعليقان|[3,*] :count تعليقات',
'comment_save' => 'حفظ التعليق',
'comment_saving' => 'جار حفظ التعليق...',
'comment_deleting' => 'جار حذف التعليق...',
'comment_new' => 'تعليق جديد',
'comment_created' => 'تم التعليق :createDiff',
'comment_updated' => 'تم التحديث :updateDiff بواسطة :username',
'comment_deleted_success' => 'تم حذف التعليق',
'comment_created_success' => 'تمت إضافة التعليق',
'comment_updated_success' => 'تم تحديث التعليق',
'comment_delete_confirm' => 'تأكيد حذف التعليق؟',
'comment_in_reply_to' => 'رداً على :commentId',
];

View File

@ -0,0 +1,82 @@
<?php
return [
/**
* Error text strings.
*/
// Permissions
'permission' => 'لم يؤذن لك بالدخول للصفحة المطلوبة.',
'permissionJson' => 'لم يؤذن لك بعمل الإجراء المطلوب.',
// Auth
'error_user_exists_different_creds' => 'يوجد مستخدم ببيانات مختلفة مسجل بالنظام للبريد الإلكتروني :email.',
'email_already_confirmed' => 'تم تأكيد البريد الإلكتروني من قبل, الرجاء محاولة تسجيل الدخول.',
'email_confirmation_invalid' => 'رابط التأكيد غير صحيح أو قد تم استخدامه من قبل, الرجاء محاولة التسجيل من جديد.',
'email_confirmation_expired' => 'صلاحية رابط التأكيد انتهت, تم إرسال رسالة تأكيد جديدة لعنوان البريد الإلكتروني.',
'ldap_fail_anonymous' => 'فشل الوصول إلى LDAP باستخدام الربط المجهول',
'ldap_fail_authed' => 'فشل الوصول إلى LDAP باستخدام dn و password المعطاة',
'ldap_extension_not_installed' => 'لم يتم تثبيت إضافة LDAP PHP',
'ldap_cannot_connect' => 'لا يمكن الاتصال بخادم ldap, فشل الاتصال المبدئي',
'social_no_action_defined' => 'لم يتم تعريف أي إجراء',
'social_login_bad_response' => "حصل خطأ خلال تسجيل الدخول باستخدام :socialAccount \n:error",
'social_account_in_use' => 'حساب :socialAccount قيد الاستخدام حالياً, الرجاء محاولة الدخول باستخدام خيار :socialAccount.',
'social_account_email_in_use' => 'البريد الإلكتروني :email مستخدم. إذا كان لديكم حساب فبإمكانكم ربط حساب :socialAccount من إعدادات ملفكم.',
'social_account_existing' => 'تم ربط حساب :socialAccount بملفكم من قبل.',
'social_account_already_used_existing' => 'حساب :socialAccount مستخدَم من قبل مستخدم آخر.',
'social_account_not_used' => 'حساب :socialAccount غير مرتبط بأي مستخدم. الرجاء ربطه من خلال إعدادات ملفكم. ',
'social_account_register_instructions' => 'إذا لم يكن لديكم حساب فيمكنكم التجسيل باستخدام خيار :socialAccount.',
'social_driver_not_found' => 'Social driver not found', // جار البحث عن الترجمة الأنسب
'social_driver_not_configured' => 'Your :socialAccount social settings are not configured correctly.', // جار البحث عن الترجمة الأنسب
// System
'path_not_writable' => 'لا يمكن الرفع إلى مسار :filePath. الرجاء التأكد من قابلية الكتابة إلى الخادم.',
'cannot_get_image_from_url' => 'لا يمكن الحصول على الصورة من :url',
'cannot_create_thumbs' => 'لا يمكن للخادم إنشاء صور مصغرة. الرجاء التأكد من تثبيت إضافة GD PHP.',
'server_upload_limit' => 'الخادم لا يسمح برفع ملفات بهذا الحجم. الرجاء محاولة الرفع بحجم أصغر.',
'uploaded' => 'الخادم لا يسمح برفع ملفات بهذا الحجم. الرجاء محاولة الرفع بحجم أصغر.',
'image_upload_error' => 'حدث خطأ خلال رفع الصورة',
'image_upload_type_error' => 'صيغة الصورة المرفوعة غير صالحة',
// Attachments
'attachment_page_mismatch' => 'Page mismatch during attachment update', // جار البحث عن الترجمة الأنسب
'attachment_not_found' => 'لم يتم العثور على المرفق',
// Pages
'page_draft_autosave_fail' => 'فشل حفظ المسودة. الرجاء التأكد من وجود اتصال بالإنترنت قبل حفظ الصفحة',
'page_custom_home_deletion' => 'لا يمكن حذف الصفحة إذا كانت محددة كصفحة رئيسية',
// Entities
'entity_not_found' => 'Entity not found', // جار البحث عن الترجمة الأنسب
'book_not_found' => 'لم يتم العثور على الكتاب',
'page_not_found' => 'لم يتم العثور على الصفحة',
'chapter_not_found' => 'لم يتم العثور على الفصل',
'selected_book_not_found' => 'لم يتم العثور على الكتاب المحدد',
'selected_book_chapter_not_found' => 'لم يتم العثور على الكتاب أو الفصل المحدد',
'guests_cannot_save_drafts' => 'لا يمكن حفظ المسودات من قبل الضيوف',
// Users
'users_cannot_delete_only_admin' => 'لا يمكن حذف المشرف الوحيد',
'users_cannot_delete_guest' => 'لا يمكن حذف المستخدم الضيف',
// Roles
'role_cannot_be_edited' => 'لا يمكن تعديل هذا الدور',
'role_system_cannot_be_deleted' => 'هذا الدور خاص بالنظام ولا يمكن حذفه',
'role_registration_default_cannot_delete' => 'لا يمكن حذف الدور إذا كان مسجل كالدور الأساسي بعد تسجيل الحساب',
// Comments
'comment_list' => 'حصل خطأ خلال جلب التعليقات.',
'cannot_add_comment_to_draft' => 'لا يمكن إضافة تعليقات على مسودة.',
'comment_add' => 'حصل خطاً خلال إضافة / تحديث التعليق.',
'comment_delete' => 'حصل خطأ خلال حذف التعليق.',
'empty_comment' => 'لايمكن إضافة تعليق فارغ.',
// Error pages
'404_page_not_found' => 'لم يتم العثور على الصفحة',
'sorry_page_not_found' => 'عفواً, لا يمكن العثور على الصفحة التي تبحث عنها.',
'return_home' => 'العودة للصفحة الرئيسية',
'error_occurred' => 'حدث خطأ',
'app_down' => ':appName لا يعمل حالياً',
'back_soon' => 'سيعود للعمل قريباً.',
];

View File

@ -0,0 +1,19 @@
<?php
return [
/*
|--------------------------------------------------------------------------
| Pagination Language Lines
|--------------------------------------------------------------------------
|
| The following language lines are used by the paginator library to build
| the simple pagination links. You are free to change them to anything
| you want to customize your views to better match your application.
|
*/
'previous' => '&laquo; السابق',
'next' => 'التالي &raquo;',
];

View File

@ -0,0 +1,22 @@
<?php
return [
/*
|--------------------------------------------------------------------------
| Password Reminder Language Lines
|--------------------------------------------------------------------------
|
| The following language lines are the default lines which match reasons
| that are given by the password broker for a password update attempt
| has failed, such as for an invalid token or invalid new password.
|
*/
'password' => 'يجب أن تتكون كلمة المرور من ستة أحرف على الأقل وأن تطابق التأكيد.',
'user' => "لم يتم العثور على مستخدم بعنوان البريد الإلكتروني المعطى.",
'token' => 'رابط تجديد كلمة المرور غير صحيح.',
'sent' => 'تم إرسال رابط تجديد كلمة المرور إلى بريدكم الإلكتروني!',
'reset' => 'تم تجديد كلمة المرور الخاصة بكم!',
];

131
resources/lang/ar/settings.php Executable file
View File

@ -0,0 +1,131 @@
<?php
return [
/**
* Settings text strings
* Contains all text strings used in the general settings sections of BookStack
* including users and roles.
*/
'settings' => 'الإعدادات',
'settings_save' => 'حفظ الإعدادات',
'settings_save_success' => 'تم حفظ الإعدادات',
/**
* App settings
*/
'app_settings' => 'إعدادات التطبيق',
'app_name' => 'اسم التطبيق',
'app_name_desc' => 'سيتم عرض هذا الاسم في الترويسة وفي أي رسالة بريد إلكتروني.',
'app_name_header' => 'عرض اسم التطبيق في الترويسة؟',
'app_public_viewing' => 'السماح بالعرض على العامة؟',
'app_secure_images' => 'تفعيل حماية أكبر لرفع الصور؟',
'app_secure_images_desc' => 'لتحسين أداء النظام, ستكون جميع الصور متاحة للعامة. هذا الخيار يضيف سلسلة من الحروف والأرقام العشوائية صعبة التخمين إلى رابط الصورة. الرجاء التأكد من تعطيل فهرسة المسارات لمنع الوصول السهل.',
'app_editor' => 'محرر الصفحة',
'app_editor_desc' => 'الرجاء اختيار محرر النص الذي سيستخدم من قبل جميع المستخدمين لتحرير الصفحات.',
'app_custom_html' => 'Custom HTML head content', // جار البحث عن الترجمة الأنسب
'app_custom_html_desc' => 'Any content added here will be inserted into the bottom of the <head> section of every page. This is handy for overriding styles or adding analytics code.', // جار البحث عن الترجمة الأنسب
'app_logo' => 'شعار التطبيق',
'app_logo_desc' => 'يجب أن تكون الصورة بارتفاع 43 بكسل. <br>سيتم تصغير الصور الأكبر من ذلك.',
'app_primary_color' => 'اللون الأساسي للتطبيق',
'app_primary_color_desc' => 'يجب أن تكون القيمة من نوع hex. <br>اترك الخانة فارغة للرجوع للون الافتراضي.',
'app_homepage' => 'الصفحة الرئيسية للتطبيق',
'app_homepage_desc' => 'الرجاء اختيار صفحة لتصبح الصفحة الرئيسية بدل من الافتراضية. سيتم تجاهل جميع الأذونات الخاصة بالصفحة المختارة.',
'app_homepage_default' => 'شكل الصفحة الافتراضية المختارة',
'app_homepage_books' => 'أو من الممكن اختيار صفحة الكتب كصفحة رئيسية. سيتم استبدالها بأي صفحة سابقة تم اختيارها كصفحة رئيسية.',
'app_disable_comments' => 'تعطيل التعليقات',
'app_disable_comments_desc' => 'تعطيل التعليقات على جميع الصفحات داخل التطبيق. التعليقات الموجودة من الأصل لن تكون ظاهرة.',
/**
* Registration settings
*/
'reg_settings' => 'إعدادات التسجيل',
'reg_allow' => 'السماح بالتسجيل؟',
'reg_default_role' => 'دور المستخدم الأساسي بعد التسجيل',
'reg_confirm_email' => 'فرض التأكيد عن طريق البريد الإلكتروني؟',
'reg_confirm_email_desc' => 'إذا تم استخدام قيود للمجال سيصبح التأكيد عن طريق البريد الإلكتروني إلزامي وسيتم تجاهل القيمة أسفله.',
'reg_confirm_restrict_domain' => 'تقييد التسجيل على مجال محدد',
'reg_confirm_restrict_domain_desc' => 'Enter a comma separated list of email domains you would like to restrict registration to. Users will be sent an email to confirm their address before being allowed to interact with the application. <br> Note that users will be able to change their email addresses after successful registration.', // جار البحث عن الترجمة الأنسب
'reg_confirm_restrict_domain_placeholder' => 'لم يتم اختيار أي قيود',
/**
* Maintenance settings
*/
'maint' => 'الصيانة',
'maint_image_cleanup' => 'تنظيف الصور',
'maint_image_cleanup_desc' => "Scans page & revision content to check which images and drawings are currently in use and which images are redundant. Ensure you create a full database and image backup before running this.", // جار البحث عن الترجمة الأنسب
'maint_image_cleanup_ignore_revisions' => 'تجاهل الصور في المراجعات',
'maint_image_cleanup_run' => 'بدء التنظيف',
'maint_image_cleanup_warning' => 'يوجد عدد :count من الصور المحتمل عدم استخدامها. تأكيد حذف الصور؟',
'maint_image_cleanup_success' => 'تم إيجاد وحذف عدد :count من الصور المحتمل عدم استخدامها!',
'maint_image_cleanup_nothing_found' => 'لم يتم حذف أي شيء لعدم وجود أي صور غير مسمتخدمة',
/**
* Role settings
*/
'roles' => 'الأدوار',
'role_user_roles' => 'أدوار المستخدمين',
'role_create' => 'إنشاء دور جديد',
'role_create_success' => 'تم إنشاء الدور بنجاح',
'role_delete' => 'حذف الدور',
'role_delete_confirm' => 'سيتم حذف الدور المسمى \':roleName\'.',
'role_delete_users_assigned' => 'This role has :userCount users assigned to it. If you would like to migrate the users from this role select a new role below.', // جار البحث عن الترجمة الأنسب
'role_delete_no_migration' => "لا تقم بترجيل المستخدمين",
'role_delete_sure' => 'تأكيد حذف الدور؟',
'role_delete_success' => 'تم حذف الدور بنجاح',
'role_edit' => 'تعديل الدور',
'role_details' => 'تفاصيل الدور',
'role_name' => 'اسم الدور',
'role_desc' => 'وصف مختصر للدور',
'role_external_auth_id' => 'External Authentication IDs', // جار البحث عن الترجمة الأنسب
'role_system' => 'أذونات النظام',
'role_manage_users' => 'إدارة المستخدمين',
'role_manage_roles' => 'إدارة الأدوار وأذوناتها',
'role_manage_entity_permissions' => 'إدارة جميع أذونات الكتب والفصول والصفحات',
'role_manage_own_entity_permissions' => 'إدارة الأذونات الخاصة بكتابك أو فصلك أو صفحاتك',
'role_manage_settings' => 'إدارة إعدادات التطبيق',
'role_asset' => 'Asset Permissions', // جار البحث عن الترجمة الأنسب
'role_asset_desc' => 'These permissions control default access to the assets within the system. Permissions on Books, Chapters and Pages will override these permissions.', // جار البحث عن الترجمة الأنسب
'role_all' => 'الكل',
'role_own' => 'Own',
'role_controlled_by_asset' => 'Controlled by the asset they are uploaded to', // جار البحث عن الترجمة الأنسب
'role_save' => 'حفظ الدور',
'role_update_success' => 'تم تحديث الدور بنجاح',
'role_users' => 'مستخدمون داخل هذا الدور',
'role_users_none' => 'لم يتم تعيين أي مستخدمين لهذا الدور',
/**
* Users
*/
'users' => 'المستخدمون',
'user_profile' => 'ملف المستخدم',
'users_add_new' => 'إضافة مستخدم جديد',
'users_search' => 'بحث عن مستخدم',
'users_role' => 'أدوار المستخدمين',
'users_external_auth_id' => 'External Authentication ID', // جار البحث عن الترجمة الأنسب
'users_password_warning' => 'الرجاء ملئ الحقل أدناه فقط في حال أردتم تغيير كلمة المرور:',
'users_system_public' => 'هذا المستخدم يمثل أي ضيف يقوم بزيارة شيء يخصك. لا يمكن استخدامه لتسجيل الدخول ولكن يتم تعيينه تلقائياً.',
'users_delete' => 'حذف المستخدم',
'users_delete_named' => 'حذف المستخدم :userName',
'users_delete_warning' => 'سيتم حذف المستخدم \':userName\' بشكل تام من النظام.',
'users_delete_confirm' => 'تأكيد حذف المستخدم؟',
'users_delete_success' => 'تم حذف المستخدم بنجاح',
'users_edit' => 'تعديل المستخدم',
'users_edit_profile' => 'تعديل الملف',
'users_edit_success' => 'تم تحديث المستخدم بنجاح',
'users_avatar' => 'صورة المستخدم',
'users_avatar_desc' => 'يجب أن تكون الصورة مربعة ومقاربة لحجم 256 بكسل',
'users_preferred_language' => 'اللغة المفضلة',
'users_social_accounts' => 'الحسابات الاجتماعية',
'users_social_accounts_info' => 'Here you can connect your other accounts for quicker and easier login. Disconnecting an account here does not previously authorized access. Revoke access from your profile settings on the connected social account.', // جار البحث عن الترجمة الأنسب
'users_social_connect' => 'ربط الحساب',
'users_social_disconnect' => 'فصل الحساب',
'users_social_connected' => 'تم ربط حساب :socialAccount بملفك بنجاح.',
'users_social_disconnected' => 'تم فصل حساب :socialAccount من ملفك بنجاح.',
];

View File

@ -0,0 +1,108 @@
<?php
return [
/*
|--------------------------------------------------------------------------
| Validation Language Lines
|--------------------------------------------------------------------------
|
| The following language lines contain the default error messages used by
| the validator class. Some of these rules have multiple versions such
| as the size rules. Feel free to tweak each of these messages here.
|
*/
'accepted' => 'يجب الموافقة على :attribute.',
'active_url' => ':attribute ليس رابط صالح.',
'after' => 'يجب أن يكون التاريخ :attribute بعد :date.',
'alpha' => 'يجب أن يقتصر :attribute على الحروف فقط.',
'alpha_dash' => 'يجب أن يقتصر :attribute على حروف أو أرقام أو شرطات فقط.',
'alpha_num' => 'يجب أن يقتصر :attribute على الحروف والأرقام فقط.',
'array' => 'The :attribute must be an array.', // جار البحث عن الترجمة الأنسب
'before' => 'يجب أن يكون التاريخ :attribute قبل :date.',
'between' => [
'numeric' => 'يجب أن يكون :attribute بين :min و :max.',
'file' => 'يجب أن يكون :attribute بين :min و :max كيلو بايت.',
'string' => 'يجب أن يكون :attribute بين :min و :max حرف / حروف.',
'array' => 'يجب أن يكون :attribute بين :min و :max عنصر / عناصر.',
],
'boolean' => 'The :attribute field must be true or false.', // جار البحث عن الترجمة الأنسب
'confirmed' => ':attribute غير مطابق.',
'date' => ':attribute ليس تاريخ صالح.',
'date_format' => ':attribute لا يطابق الصيغة :format.',
'different' => 'يجب أن يكون :attribute مختلف عن :other.',
'digits' => 'يجب أن يكون :attribute بعدد :digits خانات.',
'digits_between' => 'يجب أن يكون :attribute بعدد خانات بين :min و :max.',
'email' => 'يجب أن يكون :attribute عنوان بريد إلكتروني صالح.',
'filled' => 'حقل :attribute مطلوب.',
'exists' => ':attribute المحدد غير صالح.',
'image' => 'يجب أن يكون :attribute صورة.',
'in' => ':attribute المحدد غير صالح.',
'integer' => 'يجب أن يكون :attribute عدد صحيح.',
'ip' => 'يجب أن يكون :attribute عنوان IP صالح.',
'max' => [
'numeric' => 'يجب ألا يكون :attribute أكبر من :max.',
'file' => 'يجب ألا يكون :attribute أكبر من :max كيلو بايت.',
'string' => 'يجب ألا يكون :attribute أكثر من :max حرف / حروف.',
'array' => 'يجب ألا يحتوي :attribute على أكثر من :max عنصر / عناصر.',
],
'mimes' => 'يجب أن يكون :attribute ملف من نوع: :values.',
'min' => [
'numeric' => 'يجب أن يكون :attribute على الأقل :min.',
'file' => 'يجب أن يكون :attribute على الأقل :min كيلو بايت.',
'string' => 'يجب أن يكون :attribute على الأقل :min حرف / حروف.',
'array' => 'يجب أن يحتوي :attribute على :min عنصر / عناصر كحد أدنى.',
],
'not_in' => ':attribute المحدد غير صالح.',
'numeric' => 'يجب أن يكون :attribute رقم.',
'regex' => 'صيغة :attribute غير صالحة.',
'required' => 'حقل :attribute مطلوب.',
'required_if' => 'حقل :attribute مطلوب عندما يكون :other :value.',
'required_with' => 'حقل :attribute مطلوب عندما تكون :values موجودة.',
'required_with_all' => 'حقل :attribute مطلوب عندما تكون :values موجودة.',
'required_without' => 'حقل :attribute مطلوب عندما تكون :values غير موجودة.',
'required_without_all' => 'حقل :attribute مطلوب عندما لا يكون أي من :values موجودة.',
'same' => 'يجب تطابق :attribute مع :other.',
'size' => [
'numeric' => 'يجب أن يكون :attribute بحجم :size.',
'file' => 'يجب أن يكون :attribute بحجم :size كيلو بايت.',
'string' => 'يجب أن يكون :attribute بعدد :size حرف / حروف.',
'array' => 'يجب أن يحتوي :attribute على :size عنصر / عناصر.',
],
'string' => 'The :attribute must be a string.', // جار البحث عن الترجمة الأنسب
'timezone' => 'يجب أن تكون :attribute منطقة صالحة.',
'unique' => 'تم حجز :attribute من قبل.',
'url' => 'صيغة :attribute غير صالحة.',
/*
|--------------------------------------------------------------------------
| Custom Validation Language Lines
|--------------------------------------------------------------------------
|
| Here you may specify custom validation messages for attributes using the
| convention "attribute.rule" to name the lines. This makes it quick to
| specify a specific custom language line for a given attribute rule.
|
*/
'custom' => [
'password-confirm' => [
'required_with' => 'يجب تأكيد كلمة المرور',
],
],
/*
|--------------------------------------------------------------------------
| Custom Validation Attributes
|--------------------------------------------------------------------------
|
| The following language lines are used to swap attribute place-holders
| with something more reader friendly such as E-Mail Address instead
| of "email". This simply helps us make messages a little cleaner.
|
*/
'attributes' => [],
];

View File

@ -256,4 +256,11 @@ return [
'comment_updated_success' => 'Kommentar aktualisiert',
'comment_delete_confirm' => 'Möchten Sie diesen Kommentar wirklich löschen?',
'comment_in_reply_to' => 'Antwort auf :commentId',
/**
* Revision
*/
'revision_delete_confirm' => 'Sind Sie sicher, dass Sie diese Revision löschen wollen?',
'revision_delete_success' => 'Revision gelöscht',
'revision_cannot_delete_latest' => 'Die letzte Version kann nicht gelöscht werden.'
];

View File

@ -37,6 +37,14 @@ return [
'book_sort' => 'sorted book',
'book_sort_notification' => 'Book Successfully Re-sorted',
// Bookshelves
'bookshelf_create' => 'created Bookshelf',
'bookshelf_create_notification' => 'Bookshelf Successfully Created',
'bookshelf_update' => 'updated bookshelf',
'bookshelf_update_notification' => 'Bookshelf Successfully Updated',
'bookshelf_delete' => 'deleted bookshelf',
'bookshelf_delete_notification' => 'Bookshelf Successfully Deleted',
// Other
'commented_on' => 'commented on',
];

View File

@ -52,6 +52,7 @@ return [
'details' => 'Details',
'grid_view' => 'Grid View',
'list_view' => 'List View',
'default' => 'Default',
/**
* Header

View File

@ -64,6 +64,37 @@ return [
'search_set_date' => 'Set Date',
'search_update' => 'Update Search',
/**
* Shelves
*/
'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',
'shelves_empty_contents' => 'This shelf has no books assigned to it',
'shelves_edit_and_assign' => 'Edit shelf to assign books',
'shelves_edit_named' => 'Edit Bookshelf :name',
'shelves_edit' => 'Edit Bookshelf',
'shelves_delete' => 'Delete Bookshelf',
'shelves_delete_named' => 'Delete Bookshelf :name',
'shelves_delete_explain' => "This will delete the bookshelf with the name ':name'. Contained books will not be deleted.",
'shelves_delete_confirmation' => 'Are you sure you want to delete this bookshelf?',
'shelves_permissions' => 'Bookshelf Permissions',
'shelves_permissions_updated' => 'Bookshelf Permissions Updated',
'shelves_permissions_active' => 'Bookshelf Permissions Active',
'shelves_copy_permissions_to_books' => 'Copy Permissions to Books',
'shelves_copy_permissions' => 'Copy Permissions',
'shelves_copy_permissions_explain' => 'This will apply the current permission settings of this bookshelf to all books contained within. Before activating, ensure any changes to the permissions of this bookshelf have been saved.',
'shelves_copy_permission_success' => 'Bookshelf permissions copied to :count books',
/**
* Books
*/
@ -199,6 +230,7 @@ return [
'message' => ':start :time. Take care not to overwrite each other\'s updates!',
],
'pages_draft_discarded' => 'Draft discarded, The editor has been updated with the current page content',
'pages_specific' => 'Specific Page',
/**
* Editor sidebar
@ -206,6 +238,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)',
@ -265,4 +298,11 @@ return [
'comment_updated_success' => 'Comment updated',
'comment_delete_confirm' => 'Are you sure you want to delete this comment?',
'comment_in_reply_to' => 'In reply to :commentId',
/**
* Revision
*/
'revision_delete_confirm' => 'Are you sure you want to delete this revision?',
'revision_delete_success' => 'Revision deleted',
'revision_cannot_delete_latest' => 'Cannot delete the latest revision.'
];

View File

@ -49,6 +49,7 @@ return [
// Entities
'entity_not_found' => 'Entity not found',
'bookshelf_not_found' => 'Bookshelf not found',
'book_not_found' => 'Book not found',
'page_not_found' => 'Page not found',
'chapter_not_found' => 'Chapter not found',

View File

@ -32,9 +32,8 @@ return [
'app_primary_color' => 'Application primary color',
'app_primary_color_desc' => 'This should be a hex value. <br>Leave empty to reset to the default color.',
'app_homepage' => 'Application Homepage',
'app_homepage_desc' => 'Select a page to show on the homepage instead of the default view. Page permissions are ignored for selected pages.',
'app_homepage_default' => 'Default homepage view chosen',
'app_homepage_books' => 'Or select the books page as your homepage. This will override any page selected as your homepage.',
'app_homepage_desc' => 'Select a view to show on the homepage instead of the default view. Page permissions are ignored for selected pages.',
'app_homepage_select' => 'Select a page',
'app_disable_comments' => 'Disable comments',
'app_disable_comments_desc' => 'Disable comments across all pages in the application. Existing comments are not shown.',
@ -91,6 +90,7 @@ return [
'role_manage_settings' => 'Manage app settings',
'role_asset' => 'Asset Permissions',
'role_asset_desc' => 'These permissions control default access to the assets within the system. Permissions on Books, Chapters and Pages will override these permissions.',
'role_asset_admins' => 'Admins are automatically given access to all content but these options may show or hide UI options.',
'role_all' => 'All',
'role_own' => 'Own',
'role_controlled_by_asset' => 'Controlled by the asset they are uploaded to',
@ -135,6 +135,7 @@ return [
///////////////////////////////////
'language_select' => [
'en' => 'English',
'ar' => 'العربية',
'de' => 'Deutsch',
'es' => 'Español',
'es_AR' => 'Español Argentina',
@ -148,7 +149,7 @@ return [
'it' => 'Italian',
'ru' => 'Русский',
'zh_CN' => '简体中文',
'zh_TW' => '繁體中文'
'zh_TW' => '繁體中文'
]
///////////////////////////////////
];

View File

@ -265,4 +265,11 @@ return [
'comment_updated_success' => 'Comentario actualizado',
'comment_delete_confirm' => '¿Está seguro de que quiere borrar este comentario?',
'comment_in_reply_to' => 'En respuesta a :commentId',
/**
* Revision
*/
'revision_delete_confirm' => '¿Está seguro de que desea eliminar esta revisión?',
'revision_delete_success' => 'Revisión eliminada',
'revision_cannot_delete_latest' => 'No se puede eliminar la última revisión.'
];

View File

@ -265,4 +265,11 @@ return [
'comment_updated_success' => 'Comentario actualizado',
'comment_delete_confirm' => '¿Está seguro que quiere borrar este comentario?',
'comment_in_reply_to' => 'En respuesta a :commentId',
/**
* Revision
*/
'revision_delete_confirm' => 'Are you sure you want to delete this revision?',
'revision_delete_success' => 'Revisión eliminada',
'revision_cannot_delete_latest' => 'No se puede eliminar la última revisión.'
];

View File

@ -265,4 +265,11 @@ return [
'comment_updated_success' => 'Commentaire mis à jour',
'comment_delete_confirm' => 'Etes-vous sûr de vouloir supprimer ce commentaire ?',
'comment_in_reply_to' => 'En réponse à :commentId',
/**
* Revision
*/
'revision_delete_confirm' => 'Êtes-vous sûr de vouloir supprimer cette révision?',
'revision_delete_success' => 'Révision supprimée',
'revision_cannot_delete_latest' => 'Impossible de supprimer la dernière révision.'
];

View File

@ -260,4 +260,11 @@ return [
'comment_updated_success' => 'Commento aggiornato',
'comment_delete_confirm' => 'Sei sicuro di voler elminare questo commento?',
'comment_in_reply_to' => 'In risposta a :commentId',
/**
* Revision
*/
'revision_delete_confirm' => 'Sei sicuro di voler eliminare questa revisione?',
'revision_delete_success' => 'Revisione cancellata',
'revision_cannot_delete_latest' => 'Impossibile eliminare l\'ultima revisione.'
];

View File

@ -257,4 +257,11 @@ return [
'comment_updated_success' => 'コメントを更新しました',
'comment_delete_confirm' => '本当にこのコメントを削除しますか?',
'comment_in_reply_to' => ':commentIdへ返信',
/**
* Revision
*/
'revision_delete_confirm' => 'このリビジョンを削除しますか?',
'revision_delete_success' => 'リビジョンを削除しました',
'revision_cannot_delete_latest' => '最新のリビジョンを削除できません。'
];

View File

@ -259,4 +259,11 @@ return [
'comment_updated_success' => 'Reactie bijgewerkt',
'comment_delete_confirm' => 'Zeker reactie verwijderen?',
'comment_in_reply_to' => 'Antwoord op :commentId',
/**
* Revision
*/
'revision_delete_confirm' => 'Weet u zeker dat u deze revisie wilt verwijderen?',
'revision_delete_success' => 'Revisie verwijderd',
'revision_cannot_delete_latest' => 'Kan de laatste revisie niet verwijderen.'
];

View File

@ -257,4 +257,11 @@ return [
'comment_updated_success' => 'Komentarz zaktualizowany',
'comment_delete_confirm' => 'Czy na pewno chcesz usunąc ten komentarz?',
'comment_in_reply_to' => 'W odpowiedzi na :commentId',
/**
* Revision
*/
'revision_delete_confirm' => 'Czy na pewno chcesz usunąć tę wersję?',
'revision_delete_success' => 'Usunięto wersję',
'revision_cannot_delete_latest' => 'Nie można usunąć najnowszej wersji.'
];

View File

@ -22,6 +22,7 @@ return [
'image_upload_success' => 'Upload de imagem efetuado com sucesso',
'image_update_success' => 'Upload de detalhes da imagem efetuado com sucesso',
'image_delete_success' => 'Imagem excluída com sucesso',
'image_upload_remove' => 'Remover',
/**
* Code editor
@ -30,4 +31,4 @@ return [
'code_language' => 'Linguagem do Código',
'code_content' => 'Código',
'code_save' => 'Salvar Código',
];
];

View File

@ -162,9 +162,13 @@ return [
'pages_md_preview' => 'Preview',
'pages_md_insert_image' => 'Inserir Imagem',
'pages_md_insert_link' => 'Inserir Link para Entidade',
'pages_md_insert_drawing' => 'Inserir Desenho',
'pages_not_in_chapter' => 'Página não está dentro de um Capítulo',
'pages_move' => 'Mover Página',
'pages_move_success' => 'Pagina movida para ":parentName"',
'pages_copy' => 'Copiar Página',
'pages_copy_desination' => 'Destino da Cópia',
'pages_copy_success' => 'Página copiada com sucesso',
'pages_permissions' => 'Permissões de Página',
'pages_permissions_success' => 'Permissões de Página atualizadas',
'pages_revision' => 'Revisão',
@ -200,6 +204,8 @@ return [
* Editor sidebar
*/
'page_tags' => 'Tags de Página',
'chapter_tags' => 'Tags de Capítulo',
'book_tags' => 'Tags de Livro',
'tag' => 'Tag',
'tags' => '',
'tag_value' => 'Valor da Tag (Opcional)',
@ -245,6 +251,7 @@ return [
*/
'comment' => 'Comentário',
'comments' => 'Comentários',
'comment_add' => 'Adicionar Comentário',
'comment_placeholder' => 'Digite seus comentários aqui',
'comment_count' => '{0} Nenhum comentário|{1} 1 Comentário|[2,*] :count Comentários',
'comment_save' => 'Salvar comentário',
@ -258,4 +265,11 @@ return [
'comment_updated_success' => 'Comentário editado',
'comment_delete_confirm' => 'Você tem certeza de que quer deletar este comentário?',
'comment_in_reply_to' => 'Em resposta à :commentId',
/**
* Revision
*/
'revision_delete_confirm' => 'Tem certeza de que deseja excluir esta revisão?',
'revision_delete_success' => 'Revisão excluída',
'revision_cannot_delete_latest' => 'Não é possível excluir a revisão mais recente.'
];

View File

@ -16,10 +16,11 @@ return [
'email_confirmation_invalid' => 'Esse token de confirmação não é válido ou já foi utilizado. Por favor, tente efetuar o registro novamente.',
'email_confirmation_expired' => 'O token de confirmação já expirou. Um novo e-mail foi enviado.',
'ldap_fail_anonymous' => 'O acesso LDAP falhou ao tentar usar o anonymous bind',
'ldap_fail_authed' => 'O acesso LDAPfalou ao tentar os detalhes do dn e senha fornecidos',
'ldap_fail_authed' => 'O acesso LDAP falhou ao tentar os detalhes do dn e senha fornecidos',
'ldap_extension_not_installed' => 'As extensões LDAP PHP não estão instaladas',
'ldap_cannot_connect' => 'Não foi possível conectar ao servidor LDAP. Conexão inicial falhou',
'social_no_action_defined' => 'Nenhuma ação definida',
'social_login_bad_response' => "Erro recebido durante o login :socialAccount: \n:error",
'social_account_in_use' => 'Essa conta :socialAccount já está em uso. Por favor, tente se logar usando a opção :socialAccount',
'social_account_email_in_use' => 'O e-mail :email já está e muso. Se você já tem uma conta você poderá se conectar a conta :socialAccount a partir das configurações de seu perfil.',
'social_account_existing' => 'Essa conta :socialAccount já está atrelada a esse perfil.',
@ -34,13 +35,16 @@ return [
'cannot_get_image_from_url' => 'Não foi possivel capturar a imagem a partir de :url',
'cannot_create_thumbs' => 'O servidor não pôde criar as miniaturas de imagem. Por favor, verifique se a extensão GD PHP está instalada.',
'server_upload_limit' => 'O servidor não permite o upload de arquivos com esse tamanho. Por favor, tente fazer o upload de arquivos de menor tamanho.',
'uploaded' => 'O servidor não permite o upload de arquivos com esse tamanho. Por favor, tente fazer o upload de arquivos de menor tamanho.',
'image_upload_error' => 'Um erro aconteceu enquanto o servidor tentava efetuar o upload da imagem',
'image_upload_type_error' => 'O tipo de imagem que está sendo feito upload é inválido',
// Attachments
'attachment_page_mismatch' => 'Erro de \'Page mismatch\' durante a atualização do anexo',
'attachment_not_found' => 'Anexo não encontrado',
// Pages
'page_draft_autosave_fail' => 'Falou ao tentar salvar o rascunho. Certifique-se que a conexão de internet está funcional antes de tentar salvar essa página',
'page_draft_autosave_fail' => 'Falhou ao tentar salvar o rascunho. Certifique-se que a conexão de internet está funcional antes de tentar salvar essa página',
'page_custom_home_deletion' => 'Não pode deletar uma página que está definida como página inicial',
// Entities
@ -57,7 +61,7 @@ return [
'users_cannot_delete_guest' => 'Você não pode excluir o usuário convidado',
// Roles
'role_cannot_be_edited' => 'Esse perfil não poed ser editado',
'role_cannot_be_edited' => 'Esse perfil não pode ser editado',
'role_system_cannot_be_deleted' => 'Esse perfil é um perfil de sistema e não pode ser excluído',
'role_registration_default_cannot_delete' => 'Esse perfil não poderá se excluído enquando estiver registrado como o perfil padrão',
@ -75,4 +79,4 @@ return [
'error_occurred' => 'Um erro ocorreu',
'app_down' => ':appName está fora do ar no momento',
'back_soon' => 'Voltaremos em seguida.',
];
];

View File

@ -35,6 +35,7 @@ return [
'app_homepage_desc' => 'Selecione a página para ser usada como página inicial em vez da padrão. Permissões da página serão ignoradas.',
'app_homepage_default' => 'Escolhida página inicial padrão',
'app_disable_comments' => 'Desativar comentários',
'app_homepage_books' => 'Ou selecione a página de livros como sua página inicial. Isso substituirá qualquer página selecionada como sua página inicial.',
'app_disable_comments_desc' => 'Desativar comentários em todas as páginas no aplicativo. Os comentários existentes não são exibidos.',
/**
@ -50,6 +51,19 @@ return [
'reg_confirm_restrict_domain_desc' => 'Entre com uma lista de domínios de e-mails separados por vírgula para os quais você deseja restringir os registros. Será enviado um e-mail de confirmação para o usuário validar o e-mail antes de ser permitido interação com a aplicação. <br> Note que os usuários serão capazes de alterar o e-mail cadastrado após o sucesso na confirmação do registro.',
'reg_confirm_restrict_domain_placeholder' => 'Nenhuma restrição configurada',
/**
* Maintenance settings
*/
'maint' => 'Manutenção',
'maint_image_cleanup' => 'Limpeza de Imagens',
'maint_image_cleanup_desc' => "Examina páginas & revisa o conteúdo para verificar quais imagens e desenhos estão atualmente em uso e quais imagens são redundantes. Certifique-se de criar um backup completo do banco de dados e imagens antes de executar isso.",
'maint_image_cleanup_ignore_revisions' => 'Ignorar imagens em revisões',
'maint_image_cleanup_run' => 'Executar Limpeza',
'maint_image_cleanup_warning' => ':count imagens potencialmente não utilizadas foram encontradas. Tem certeza de que deseja excluir estas imagens?',
'maint_image_cleanup_success' => ':count imagens potencialmente não utilizadas foram encontradas e excluídas!',
'maint_image_cleanup_nothing_found' => 'Nenhuma imagem não utilizada foi encontrada, nada foi excluído!',
/**
* Role settings
*/
@ -68,6 +82,7 @@ return [
'role_details' => 'Detalhes do Perfil',
'role_name' => 'Nome do Perfil',
'role_desc' => 'Descrição Curta do Perfil',
'role_external_auth_id' => 'IDs de Autenticação Externa',
'role_system' => 'Permissões do Sistema',
'role_manage_users' => 'Gerenciar Usuários',
'role_manage_roles' => 'Gerenciar Perfis & Permissões de Perfis',

View File

@ -258,4 +258,11 @@ return [
'comment_updated_success' => 'Комментарий обновлён',
'comment_delete_confirm' => 'Вы уверенны, что хотите удалить этот комментарий?',
'comment_in_reply_to' => 'В ответ на :commentId',
/**
* Revision
*/
'revision_delete_confirm' => 'Вы действительно хотите удалить эту ревизию?',
'revision_delete_success' => 'Редактирование удалено',
'revision_cannot_delete_latest' => 'Не удается удалить последнюю версию.'
];

View File

@ -232,4 +232,11 @@ return [
'comments' => 'Komentáre',
'comment_placeholder' => 'Tu zadajte svoje pripomienky',
'comment_save' => 'Uložiť komentár',
/**
* Revision
*/
'revision_delete_confirm' => 'Naozaj chcete túto revíziu odstrániť?',
'revision_delete_success' => 'Revízia bola vymazaná',
'revision_cannot_delete_latest' => 'Nie je možné vymazať poslednú revíziu.'
];

View File

@ -265,4 +265,11 @@ return [
'comment_updated_success' => 'Kommentaren har uppdaterats',
'comment_delete_confirm' => 'Är du säker på att du vill ta bort den här kommentaren?',
'comment_in_reply_to' => 'Som svar på :commentId',
/**
* Revision
*/
'revision_delete_confirm' => 'Är du säker på att du vill radera den här versionen?',
'revision_delete_success' => 'Revisionen raderad',
'revision_cannot_delete_latest' => 'Det går inte att ta bort den senaste versionen.'
];

View File

@ -258,4 +258,11 @@ return [
'comment_updated_success' => '评论已更新',
'comment_delete_confirm' => '你确定要删除这条评论?',
'comment_in_reply_to' => '回复 :commentId',
/**
* Revision
*/
'revision_delete_confirm' => '您确定要删除此修订版吗?',
'revision_delete_success' => '修订删除',
'revision_cannot_delete_latest' => '无法删除最新版本。'
];

View File

@ -259,4 +259,11 @@ return [
'comment_updated_success' => '評論已更新',
'comment_delete_confirm' => '你確定要刪除這條評論?',
'comment_in_reply_to' => '回覆 :commentId',
/**
* Revision
*/
'revision_delete_confirm' => '您確定要刪除此修訂版嗎?',
'revision_delete_success' => '修訂刪除',
'revision_cannot_delete_latest' => '無法刪除最新版本。'
];

View File

@ -20,11 +20,7 @@
@include('partials/custom-styles')
@if(setting('app-custom-head') && \Route::currentRouteName() !== 'settings')
<!-- Custom user content -->
{!! setting('app-custom-head') !!}
<!-- End custom user content -->
@endif
@include('partials.custom-head')
</head>
<body class="@yield('body-class')" ng-app="bookStack">
@ -33,7 +29,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 +39,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">
@ -52,6 +48,9 @@
</form>
</div>
<div class="links text-center">
@if(userCan('bookshelf-view-all') || userCan('bookshelf-view-own'))
<a href="{{ baseUrl('/shelves') }}">@icon('bookshelf'){{ trans('entities.shelves') }}</a>
@endif
<a href="{{ baseUrl('/books') }}">@icon('book'){{ trans('entities.books') }}</a>
@if(signedInUser() && userCan('settings-manage'))
<a href="{{ baseUrl('/settings') }}">@icon('settings'){{ trans('settings.settings') }}</a>

View File

@ -28,6 +28,7 @@
}
</style>
@yield('head')
@include('partials.custom-head')
</head>
<body>
<div class="container">

View File

@ -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>

View File

@ -25,7 +25,7 @@
<a dropdown-toggle class="text-primary text-button">@icon('more'){{ trans('common.more') }}</a>
<ul>
@if(userCan('book-update', $book))
<li><a href="{{$book->getEditUrl()}}" class="text-primary">@icon('edit'){{ trans('common.edit') }}</a></li>
<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))

View File

@ -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

View File

@ -21,6 +21,7 @@
}
</style>
@yield('head')
@include('partials.custom-head')
</head>
<body>
<div class="container">

View File

@ -0,0 +1,18 @@
@extends('sidebar-layout')
@section('toolbar')
<div class="col-sm-6 faded">
<div class="action-buttons text-left">
<a expand-toggle=".entity-list.compact .entity-item-snippet" class="text-primary text-button">@icon('expand-text'){{ trans('common.toggle_details') }}</a>
@include('shelves/view-toggle', ['shelvesViewType' => $shelvesViewType])
</div>
</div>
@stop
@section('sidebar')
@include('common/home-sidebar')
@stop
@section('body')
@include('shelves/list', ['shelves' => $shelves, 'shelvesViewType' => $shelvesViewType])
@stop

View File

@ -10,7 +10,7 @@
@section('body')
<div class="container">
<div class="container" id="home-default">
<div class="row">
<div class="col-sm-4">

View File

@ -10,6 +10,7 @@
@endif
</style>
@yield('head')
@include('partials.custom-head')
</head>
<body>
<div class="container" id="page-show">

View File

@ -4,6 +4,7 @@
drawio-enabled="{{ config('services.drawio') ? 'true' : 'false' }}"
editor-type="{{ setting('app-editor') }}"
page-id="{{ $model->id or 0 }}"
text-direction="{{ config('app.rtl') ? 'rtl' : 'ltr' }}"
page-new-draft="{{ $model->draft or 0 }}"
page-update-draft="{{ $model->isDraft or 0 }}">

View File

@ -1,4 +1,4 @@
<div>
<div dir="auto">
<h1 class="break-text" v-pre id="bkmrk-page-title">{{$page->name}}</h1>

View File

@ -36,16 +36,31 @@
<td> @if($revision->createdBy) {{ $revision->createdBy->name }} @else {{ trans('common.deleted_user') }} @endif</td>
<td><small>{{ $revision->created_at->format('jS F, Y H:i:s') }} <br> ({{ $revision->created_at->diffForHumans() }})</small></td>
<td>{{ $revision->summary }}</td>
<td>
<td class="actions">
<a href="{{ $revision->getUrl('changes') }}" target="_blank">{{ trans('entities.pages_revisions_changes') }}</a>
<span class="text-muted">&nbsp;|&nbsp;</span>
@if ($index === 0)
<a target="_blank" href="{{ $page->getUrl() }}"><i>{{ trans('entities.pages_revisions_current') }}</i></a>
@else
<a href="{{ $revision->getUrl() }}" target="_blank">{{ trans('entities.pages_revisions_preview') }}</a>
<span class="text-muted">&nbsp;|&nbsp;</span>
<a href="{{ $revision->getUrl('restore') }}">{{ trans('entities.pages_revisions_restore') }}</a>
<span class="text-muted">&nbsp;|&nbsp;</span>
<div dropdown class="dropdown-container">
<a dropdown-toggle>{{ trans('common.delete') }}</a>
<ul>
<li class="padded"><small class="text-muted">{{trans('entities.revision_delete_confirm')}}</small></li>
<li>
<form action="{{ $revision->getUrl('/delete/') }}" method="POST">
{!! csrf_field() !!}
<input type="hidden" name="_method" value="DELETE">
<button type="submit" class="text-button neg">@icon('delete'){{ trans('common.delete') }}</button>
</form>
</li>
</ul>
</div>
@endif
</td>
</tr>

View File

@ -0,0 +1,5 @@
@if(setting('app-custom-head') && \Route::currentRouteName() !== 'settings')
<!-- Custom user content -->
{!! setting('app-custom-head') !!}
<!-- End custom user content -->
@endif

View File

@ -19,4 +19,4 @@
color: {{ setting('app-color') }};
fill: {{ setting('app-color') }};
}
</style>
</style>

View File

@ -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)

View File

@ -76,12 +76,22 @@
<input type="text" value="{{ setting('app-color') }}" name="setting-app-color" id="setting-app-color" placeholder="#0288D1">
<input type="hidden" value="{{ setting('app-color-light') }}" name="setting-app-color-light" id="setting-app-color-light">
</div>
<div class="form-group" id="homepage-control">
<div homepage-control class="form-group" id="homepage-control">
<label for="setting-app-homepage">{{ trans('settings.app_homepage') }}</label>
<p class="small">{{ trans('settings.app_homepage_desc') }}</p>
@include('components.page-picker', ['name' => 'setting-app-homepage', 'placeholder' => trans('settings.app_homepage_default'), 'value' => setting('app-homepage')])
<p class="small">{{ trans('settings.app_homepage_books') }}</p>
@include('components.toggle-switch', ['name' => 'setting-app-book-homepage', 'value' => setting('app-book-homepage')])
<select name="setting-app-homepage-type" id="setting-app-homepage-type">
<option @if(setting('app-homepage-type') === 'default') selected @endif value="default">{{ trans('common.default') }}</option>
<option @if(setting('app-homepage-type') === 'books') selected @endif value="books">{{ trans('entities.books') }}</option>
<option @if(setting('app-homepage-type') === 'bookshelves') selected @endif value="bookshelves">{{ trans('entities.shelves') }}</option>
<option @if(setting('app-homepage-type') === 'page') selected @endif value="page">{{ trans('entities.pages_specific') }}</option>
</select>
<br><br>
<div page-picker-container style="display: none;">
@include('components.page-picker', ['name' => 'setting-app-homepage', 'placeholder' => trans('settings.app_homepage_select'), 'value' => setting('app-homepage')])
</div>
</div>
</div>

View File

@ -36,6 +36,10 @@
<h5>{{ trans('settings.role_asset') }}</h5>
<p>{{ trans('settings.role_asset_desc') }}</p>
@if (isset($role) && $role->system_name === 'admin')
<p>{{ trans('settings.role_asset_admins') }}</p>
@endif
<table class="table">
<tr>
<th width="20%"></th>
@ -44,6 +48,24 @@
<th width="20%">{{ trans('common.edit') }}</th>
<th width="20%">{{ trans('common.delete') }}</th>
</tr>
<tr>
<td>{{ trans('entities.shelves_long') }}</td>
<td>
<label>@include('settings/roles/checkbox', ['permission' => 'bookshelf-create-all']) {{ trans('settings.role_all') }}</label>
</td>
<td>
<label>@include('settings/roles/checkbox', ['permission' => 'bookshelf-view-own']) {{ trans('settings.role_own') }}</label>
<label>@include('settings/roles/checkbox', ['permission' => 'bookshelf-view-all']) {{ trans('settings.role_all') }}</label>
</td>
<td>
<label>@include('settings/roles/checkbox', ['permission' => 'bookshelf-update-own']) {{ trans('settings.role_own') }}</label>
<label>@include('settings/roles/checkbox', ['permission' => 'bookshelf-update-all']) {{ trans('settings.role_all') }}</label>
</td>
<td>
<label>@include('settings/roles/checkbox', ['permission' => 'bookshelf-delete-own']) {{ trans('settings.role_own') }}</label>
<label>@include('settings/roles/checkbox', ['permission' => 'bookshelf-delete-all']) {{ trans('settings.role_all') }}</label>
</td>
</tr>
<tr>
<td>{{ trans('entities.books') }}</td>
<td>

View File

@ -0,0 +1,3 @@
<div class="breadcrumbs">
<a href="{{$shelf->getUrl()}}" class="text-bookshelf text-button">@icon('bookshelf'){{ $shelf->getShortName() }}</a>
</div>

Some files were not shown because too many files have changed in this diff Show More