Merge branch 'BookStackApp-master' of git://github.com/OsmosysSoftware/BookStack into OsmosysSoftware-BookStackApp-master

This commit is contained in:
Dan Brown 2017-12-06 15:52:54 +00:00
commit bc1302a8d8
No known key found for this signature in database
GPG Key ID: 46D9F943C24A2EF9
31 changed files with 301 additions and 31 deletions

View File

@ -3,7 +3,7 @@
class Book extends Entity
{
protected $fillable = ['name', 'description'];
protected $fillable = ['name', 'description', 'image_id'];
/**
* Get the url for this book.
@ -17,7 +17,46 @@ class Book extends Entity
}
return baseUrl('/books/' . urlencode($this->slug));
}
/**
* Returns book cover image, if book cover not exists return default cover image.
* @param int $height - Height of the image
* @param type $width - Width of the image
* @return type string
*/
public function getBookCover($height = 170, $width = 300)
{
$default = baseUrl('/book_default_cover.png');
$image = $this->image_id;
if ($image === 0 || $image === '0' || $image === null)
return $default;
try {
$cover = $this->cover ? baseUrl($this->cover->getThumb($width, $height, false)) : $default;
} catch (\Exception $err) {
$cover = $default;
}
return $cover;
}
/**
* Get an excerpt of this book's name to the specified length or less.
* @param int $length
* @return string
*/
public function getHeadingExcerpt($length = 35)
{
$bookHeading = $this->name;
return strlen($bookHeading) > $length ? substr($bookHeading, 0, $length-3) . '...' : $bookHeading;
}
/**
* Get the cover image of the book
* @return \Illuminate\Database\Eloquent\Relations\BelongsTo
*/
public function cover()
{
return $this->belongsTo(Image::class, 'image_id');
}
/*
* Get the edit url for this book.
* @return string
@ -64,5 +103,14 @@ class Book extends Entity
{
return "'BookStack\\\\Book' 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";
}
/**
* Get the user that created the page revision
* @return \Illuminate\Database\Eloquent\Relations\BelongsTo
*/
public function createdBy()
{
return $this->belongsTo(User::class, 'created_by');
}
}

View File

@ -40,12 +40,14 @@ class BookController extends Controller
$recents = $this->signedIn ? $this->entityRepo->getRecentlyViewed('book', 4, 0) : false;
$popular = $this->entityRepo->getPopular('book', 4, 0);
$new = $this->entityRepo->getRecentlyCreated('book', 4, 0);
$booksViewType = $this->currentUser->books_view_type;
$this->setPageTitle('Books');
return view('books/index', [
'books' => $books,
'recents' => $recents,
'popular' => $popular,
'new' => $new
'new' => $new,
'booksViewType' => $booksViewType
]);
}
@ -125,9 +127,9 @@ class BookController extends Controller
'name' => 'required|string|max:255',
'description' => 'string|max:1000'
]);
$book = $this->entityRepo->updateFromInput('book', $book, $request->all());
Activity::add($book, 'book_update', $book->id);
return redirect($book->getUrl());
$book = $this->entityRepo->updateFromInput('book', $book, $request->all());
Activity::add($book, 'book_update', $book->id);
return redirect($book->getUrl());
}
/**

View File

@ -22,7 +22,7 @@ class User extends Model implements AuthenticatableContract, CanResetPasswordCon
* The attributes that are mass assignable.
* @var array
*/
protected $fillable = ['name', 'email', 'image_id'];
protected $fillable = ['name', 'email', 'image_id', 'books_view_type' ];
/**
* The attributes excluded from the model's JSON form.

View File

@ -0,0 +1,40 @@
<?php
use Illuminate\Support\Facades\Schema;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Database\Migrations\Migration;
class AddCoverImageDisplay extends Migration
{
/**
* Run the migrations.
*
* @return void
*/
public function up()
{
Schema::table('users', function (Blueprint $table) {
$table->string('books_view_type',10)->default('grid');
});
Schema::table('books', function (Blueprint $table) {
$table->integer('image_id');
});
}
/**
* Reverse the migrations.
*
* @return void
*/
public function down()
{
Schema::table('users', function (Blueprint $table) {
$table->dropColumn('books_view_type');
});
Schema::table('books', function (Blueprint $table) {
$table->dropColumn('image_id');
});
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.5 KiB

View File

@ -119,6 +119,19 @@ jQuery.expr[":"].contains = $.expr.createPseudo(function (arg) {
};
});
// Common jQuery actions
$('[data-action="expand-entity-list-details"]').click(function() {
$('.entity-list.compact').find('p').not('.empty-text').slideToggle(240);
});
// Toggle thumbnail::hide image and reduce grid size
$(document).ready(function(){
$('[data-action="expand-thumbnail"]').click(function(){
$('.gallery-item').toggleClass("collapse").find('img').slideToggle(50);
});
});
// Detect IE for css
if(navigator.userAgent.indexOf('MSIE')!==-1
|| navigator.appVersion.indexOf('Trident/') > 0

View File

@ -233,5 +233,73 @@ $btt-size: 40px;
}
}
// styles for Books grid view
.cover {
width: 290px;
border-radius: 3px;
}
.featured-image-container {
position: relative;
overflow: hidden;
background: #F2F2F2;
border: 1px solid #ddd;
border-bottom: 0px;
}
.featured-image-container img {
display: block;
max-width: 100%;
height: auto;
-webkit-transition: all .5s ease;
-moz-transition: all .5s ease;
-ms-transition: all .5s ease;
-o-transition: all .5s ease;
transition: all .5s ease;
}
.book-content {
padding: 30px;
border: 1px solid #ddd;
border-top: 0px;
border-bottom-width: 2px;
}
.book-content h2 {
font-size: 1.5em;
line-height: 1.2;
margin: 0 0 10px;
}
.book-content h2 a {
display: block;
color: #009688;;
text-decoration: none;
}
.book-content p {
font-size: .85em;
margin: 0 0 10px;
line-height: 1.6em;
}
.featured-image-container img:hover {
-webkit-transform: scale(1.15);
-moz-transform: scale(1.15);
-ms-transform: scale(1.15);
-o-transform: scale(1.15);
transform: scale(1.15);
opacity: .5;
}
.books-grid-div {
margin-bottom : 20px;
}
@media (min-width:992px){
.row.auto-clear .col-md-4:nth-child(3n+1){clear:left;}
}
@media (min-width:992px){
.row.auto-clear .col-md-4:nth-child(3n+1){clear:left;}
}
@media (max-width:991px){
.row.auto-clear .col-xs-6:nth-child(2n+1){clear:left;}
}

View File

@ -17,6 +17,8 @@ return [
'name' => 'Name',
'description' => 'Beschreibung',
'role' => 'Rolle',
'cover_image' => 'Titelbild',
'cover_image_description' => 'Das Bild sollte eine Auflösung von 300x170px haben.',
/**
* Actions
@ -43,7 +45,7 @@ return [
'no_items' => 'Keine Einträge gefunden.',
'back_to_top' => 'nach oben',
'toggle_details' => 'Details zeigen/verstecken',
'toggle_thumbnails' => 'Thumbnails zeigen/verstecken',
/**
* Header
*/

View File

@ -96,6 +96,7 @@ return [
'users_delete_warning' => 'Der Benutzer ":userName" wird aus dem System gelöscht.',
'users_delete_confirm' => 'Sind Sie sicher, dass Sie diesen Benutzer löschen möchten?',
'users_delete_success' => 'Benutzer erfolgreich gelöscht.',
'users_books_view_type' => 'Bevorzugtes Display-Layout für Bücher',
'users_edit' => 'Benutzer bearbeiten',
'users_edit_profile' => 'Profil bearbeiten',
'users_edit_success' => 'Benutzer erfolgreich aktualisisert',

View File

@ -18,7 +18,9 @@ return [
'name' => 'Name',
'description' => 'Description',
'role' => 'Role',
'cover_image' => 'Cover image',
'cover_image_description' => 'This image should be approx 300x170px.',
/**
* Actions
*/
@ -45,8 +47,8 @@ return [
'no_items' => 'No items available',
'back_to_top' => 'Back to top',
'toggle_details' => 'Toggle Details',
'toggle_thumbnails' => 'Toggle Thumbnails',
'details' => 'Details',
/**
* Header
*/

View File

@ -94,6 +94,7 @@ return [
'users_external_auth_id' => 'External Authentication ID',
'users_password_warning' => 'Only fill the below if you would like to change your password:',
'users_system_public' => 'This user represents any guest users that visit your instance. It cannot be used to log in but is assigned automatically.',
'users_books_view_type' => 'Preferred layout for books viewing',
'users_delete' => 'Delete User',
'users_delete_named' => 'Delete user :userName',
'users_delete_warning' => 'This will fully delete this user with the name \':userName\' from the system.',

View File

@ -17,7 +17,8 @@ return [
'name' => 'Nombre',
'description' => 'Descripción',
'role' => 'Rol',
'cover_image' => 'Imagen de portada',
'cover_image_description' => 'Esta imagen debe ser aproximadamente 300x170px.',
/**
* Actions
*/
@ -43,6 +44,7 @@ return [
'no_items' => 'No hay items disponibles',
'back_to_top' => 'Volver arriba',
'toggle_details' => 'Alternar detalles',
'toggle_thumbnails' => 'Alternar miniaturas',
/**
* Header

View File

@ -91,6 +91,7 @@ return [
'users_external_auth_id' => 'ID externo de autenticación',
'users_password_warning' => 'Solo rellene a continuación si desea cambiar su password:',
'users_system_public' => 'Este usuario representa cualquier usuario invitado que visita la aplicación. No puede utilizarse para hacer login sio que es asignado automáticamente.',
'users_books_view_type' => 'Diseño de pantalla preferido para libros',
'users_delete' => 'Borrar usuario',
'users_delete_named' => 'Borrar usuario :userName',
'users_delete_warning' => 'Se borrará completamente el usuario con el nombre \':userName\' del sistema.',

View File

@ -17,7 +17,8 @@ return [
'name' => 'Nom',
'description' => 'Description',
'role' => 'Rôle',
'cover_image' => 'Image de couverture',
'cover_image_description' => 'Cette image doit être environ 300x170px.',
/**
* Actions
*/
@ -43,6 +44,7 @@ return [
'no_items' => 'Aucun élément',
'back_to_top' => 'Retour en haut',
'toggle_details' => 'Afficher les détails',
'toggle_thumbnails' => 'Afficher les vignettes',
/**
* Header

View File

@ -91,6 +91,7 @@ return [
'users_external_auth_id' => 'Identifiant d\'authentification externe',
'users_password_warning' => 'Remplissez ce fomulaire uniquement si vous souhaitez changer de mot de passe:',
'users_system_public' => 'Cet utilisateur représente les invités visitant votre instance. Il est assigné automatiquement aux invités.',
'users_books_view_type' => 'Disposition d\'affichage préférée pour les livres',
'users_delete' => 'Supprimer un utilisateur',
'users_delete_named' => 'Supprimer l\'utilisateur :userName',
'users_delete_warning' => 'Ceci va supprimer \':userName\' du système.',

View File

@ -17,7 +17,7 @@ return [
'name' => '名称',
'description' => '概要',
'role' => '権限',
'cover_image_description' => 'この画像は約 300x170px をする必要があります。',
/**
* Actions
*/

View File

@ -18,7 +18,8 @@ return [
'name' => 'Naam',
'description' => 'Beschrijving',
'role' => 'Rol',
'cover_image' => 'Omslagfoto',
'cover_image_description' => 'Deze afbeelding moet ongeveer 300x170px zijn.',
/**
* Actions
*/
@ -44,6 +45,7 @@ return [
'no_items' => 'Geen items beschikbaar',
'back_to_top' => 'Terug naar boven',
'toggle_details' => 'Details Weergeven',
'toggle_thumbnails' => 'Thumbnails Weergeven',
/**
* Header

View File

@ -91,6 +91,7 @@ return [
'users_external_auth_id' => 'External Authentication ID',
'users_password_warning' => 'Vul onderstaande formulier alleen in als je het wachtwoord wilt aanpassen:',
'users_system_public' => 'De eigenschappen van deze gebruiker worden voor elke gastbezoeker gebruikt. Er kan niet mee ingelogd worden en wordt automatisch toegewezen.',
'users_books_view_type' => 'Voorkeursuitleg voor het weergeven van boeken',
'users_delete' => 'Verwijder gebruiker',
'users_delete_named' => 'Verwijder gebruiker :userName',
'users_delete_warning' => 'Dit zal de gebruiker \':userName\' volledig uit het systeem verwijderen.',

View File

@ -17,7 +17,9 @@ return [
'name' => 'Nazwa',
'description' => 'Opis',
'role' => 'Rola',
'cover_image' => 'Zdjęcie z okładki',
'cover_image_description' => 'Ten obraz powinien wynosić około 300 x 170 piksli.',
/**
* Actions
*/

View File

@ -18,7 +18,8 @@ return [
'name' => 'Nome',
'description' => 'Descrição',
'role' => 'Regra',
'cover_image' => 'Imagem de capa',
'cover_image_description' => 'Esta imagem deve ser aproximadamente 300x170px.',
/**
* Actions
*/
@ -45,6 +46,7 @@ return [
'no_items' => 'Nenhum item disponível',
'back_to_top' => 'Voltar ao topo',
'toggle_details' => 'Alternar Detalhes',
'toggle_thumbnails' => 'Alternar Miniaturas',
'details' => 'Detalhes',
/**

View File

@ -94,6 +94,7 @@ return [
'users_external_auth_id' => 'ID de Autenticação Externa',
'users_password_warning' => 'Preencha os dados abaixo caso queira modificar a sua senha:',
'users_system_public' => 'Esse usuário representa quaisquer convidados que visitam o aplicativo. Ele não pode ser usado para login.',
'users_books_view_type' => 'Layout preferido para mostrar livros',
'users_delete' => 'Excluir Usuário',
'users_delete_named' => 'Excluir :userName',
'users_delete_warning' => 'A ação vai excluir completamente o usuário de nome \':userName\' do sistema.',

View File

@ -17,7 +17,8 @@ return [
'name' => 'Meno',
'description' => 'Popis',
'role' => 'Rola',
'cover_image' => 'Obal knihy',
'cover_image_description' => 'Tento obrázok by mal byť približne 300 x 170 pixelov.',
/**
* Actions
*/
@ -43,6 +44,7 @@ return [
'no_items' => 'Žiadne položky nie sú dostupné',
'back_to_top' => 'Späť nahor',
'toggle_details' => 'Prepnúť detaily',
'toggle_thumbnails' => 'Prepnúť náhľady',
/**
* Header

View File

@ -91,6 +91,7 @@ return [
'users_external_auth_id' => 'Externé autentifikačné ID',
'users_password_warning' => 'Pole nižšie vyplňte iba ak chcete zmeniť heslo:',
'users_system_public' => 'Tento účet reprezentuje každého hosťovského používateľa, ktorý navštívi Vašu inštanciu. Nedá sa pomocou neho prihlásiť a je priradený automaticky.',
'users_books_view_type' => 'Preferované rozloženie pre prezeranie kníh',
'users_delete' => 'Zmazať používateľa',
'users_delete_named' => 'Zmazať používateľa :userName',
'users_delete_warning' => ' Toto úplne odstráni používateľa menom \':userName\' zo systému.',

View File

@ -17,11 +17,12 @@
<div class="card">
<h3><i class="zmdi zmdi-plus"></i> {{ trans('entities.books_create') }}</h3>
<div class="body">
<form action="{{ baseUrl("/books") }}" method="POST">
<form action="{{ baseUrl("/books") }}" method="POST" enctype="multipart/form-data">
@include('books/form')
</form>
</div>
</div>
</div>
<p class="margin-top large"><br></p>
@include('components.image-manager', ['imageType' => 'cover'])
@stop

View File

@ -20,5 +20,5 @@
</div>
</div>
</div>
@include('components.image-manager', ['imageType' => 'cover'])
@stop

View File

@ -9,6 +9,21 @@
<label for="description">{{ trans('common.description') }}</label>
@include('form/textarea', ['name' => 'description'])
</div>
<div class="form-group" id="logo-control">
<label for="user-avatar">{{ trans('common.cover_image') }}</label>
<p class="small">{{ trans('common.cover_image_description') }}</p>
@include('components.image-picker', [
'resizeHeight' => '512',
'resizeWidth' => '512',
'showRemove' => true,
'defaultImage' => baseUrl('/book_default_cover.png'),
'currentImage' => @isset($model) ? $model->getBookCover() : baseUrl('/book_default_cover.png') ,
'currentId' => @isset($model) ? $model->image_id : 0,
'name' => 'image_id',
'imageClass' => 'cover'
])
</div>
<div class="form-group text-right">
<a href="{{ isset($book) ? $book->getUrl() : baseUrl('/books') }}" class="button outline">{{ trans('common.cancel') }}</a>

View File

@ -0,0 +1,18 @@
<div class="col-xs-12 col-sm-4 col-md-4 col-lg-4 books-grid-div" data-entity-type="book" data-entity-id="{{$book->id}}">
<div class="featured-image-container">
<a href="{{$book->getUrl()}}" title="{{$book->name}}">
<img width="1600" height="900" src="{{$book->getBookCover()}}" alt="{{$book->name}}">
</a>
</div>
<div class="book-content">
<h2><a href="{{$book->getUrl()}}" title="{{$book->name}}" > {{$book->getHeadingExcerpt()}} </a></h2>
@if(isset($book->searchSnippet))
<p >{{!! $book->searchSnippet !!}}</p>
@else
<p >{{ $book->getExcerpt(130) }}</p>
@endif
<div >
<span>@include('partials.entity-meta', ['entity' => $book])</span>
</div>
</div>
</div>

View File

@ -39,15 +39,29 @@
@stop
@section('body')
<div class="container small" ng-non-bindable>
@if($booksViewType === 'list')
<div class="container small" ng-non-bindable>
@else
<div class="container" ng-non-bindable>
@endif
<h1>{{ trans('entities.books') }}</h1>
@if(count($books) > 0)
@foreach($books as $book)
@include('books/list-item', ['book' => $book])
<hr>
@endforeach
{!! $books->render() !!}
@if($booksViewType === 'list')
@foreach($books as $book)
@include('books/list-item', ['book' => $book])
<hr>
@endforeach
{!! $books->render() !!}
@else
<div class="row auto-clear">
@foreach($books as $key => $book)
@include('books/grid-item', ['book' => $book])
@endforeach
<div class="col-xs-12">
{!! $books->render() !!}
</div>
</div>
@endif
@else
<p class="text-muted">{{ trans('entities.books_empty') }}</p>
@if(userCan('books-create-all'))
@ -55,5 +69,4 @@
@endif
@endif
</div>
@stop

View File

@ -13,7 +13,7 @@
<button class="text-button neg" data-action="remove-image" type="button">{{ trans('common.remove') }}</button>
@endif
<input type="hidden" name="{{$name}}" id="{{$name}}" value="{{ isset($currentId) && ($currentId !== '' && $currentId !== false) ? $currentId : $currentImage}}">
<input type="hidden" name="{{$name}}" id="{{$name}}" value="{{ isset($currentId) && ($currentId !== 0 && $currentId !== false) ? $currentId : $currentImage}}">
</div>
<script>

View File

@ -43,6 +43,13 @@
@endforeach
</select>
</div>
<div class="form-group">
<label for="books-view-type">{{ trans('settings.users_books_view_type') }}</label>
<select name="books_view_type" id="books-view-type">
<option @if($user->books_view_type === 'grid') selected @endif value="grid">Grid</option>
<option @if($user->books_view_type === 'list') selected @endif value="list">List</option>
</select>
</div>
</div>
</div>
<div class="form-group text-right">
@ -87,4 +94,4 @@
<p class="margin-top large"><br></p>
@include('components.image-manager', ['imageType' => 'user'])
@stop
@stop

View File

@ -94,5 +94,27 @@ class UserProfileTest extends BrowserKitTest
->seePageIs('/settings/users/' . $guestUser->id)
->see('cannot delete the guest user');
}
public function test_books_view_is_list()
{
$editor = $this->getEditor([
'books_view_type' => 'list'
]);
$this->actingAs($editor)
->visit('/books')
->pageNotHasElement('.featured-image-container')
->pageHasElement('.entity-list-item');
}
public function test_books_view_is_grid()
{
$editor = $this->getEditor([
'books_view_type' => 'grid'
]);
$this->actingAs($editor)
->visit('/books')
->pageHasElement('.featured-image-container');
}
}