Merge branch 'master' into release

This commit is contained in:
Dan Brown 2021-05-30 16:17:44 +01:00
commit 18562f1e10
No known key found for this signature in database
GPG Key ID: 46D9F943C24A2EF9
202 changed files with 1961 additions and 828 deletions

View File

@ -200,6 +200,7 @@ LDAP_TLS_INSECURE=false
LDAP_ID_ATTRIBUTE=uid
LDAP_EMAIL_ATTRIBUTE=mail
LDAP_DISPLAY_NAME_ATTRIBUTE=cn
LDAP_THUMBNAIL_ATTRIBUTE=null
LDAP_FOLLOW_REFERRALS=true
LDAP_DUMP_USER_DETAILS=false

View File

@ -158,7 +158,7 @@ HenrijsS :: Latvian
Pascal R-B (pborgner) :: German
Boris (Ginfred) :: Russian
Jonas Anker Rasmussen (jonasanker) :: Danish
Gerwin de Keijzer (gdekeijzer) :: Dutch; German Informal; German
Gerwin de Keijzer (gdekeijzer) :: Dutch; German; German Informal
kometchtech :: Japanese
Auri (Atalonica) :: Catalan
Francesco Franchina (ffranchina) :: Italian

17
app/Actions/Favourite.php Normal file
View File

@ -0,0 +1,17 @@
<?php namespace BookStack\Actions;
use BookStack\Model;
use Illuminate\Database\Eloquent\Relations\MorphTo;
class Favourite extends Model
{
protected $fillable = ['user_id'];
/**
* Get the related model that can be favourited.
*/
public function favouritable(): MorphTo
{
return $this->morphTo();
}
}

View File

@ -1,6 +1,7 @@
<?php namespace BookStack\Actions;
use BookStack\Model;
use Illuminate\Database\Eloquent\Relations\MorphTo;
class Tag extends Model
{
@ -9,10 +10,25 @@ class Tag extends Model
/**
* Get the entity that this tag belongs to
* @return \Illuminate\Database\Eloquent\Relations\MorphTo
*/
public function entity()
public function entity(): MorphTo
{
return $this->morphTo('entity');
}
/**
* Get a full URL to start a tag name search for this tag name.
*/
public function nameUrl(): string
{
return url('/search?term=%5B' . urlencode($this->name) .'%5D');
}
/**
* Get a full URL to start a tag name and value search for this tag's values.
*/
public function valueUrl(): string
{
return url('/search?term=%5B' . urlencode($this->name) .'%3D' . urlencode($this->value) . '%5D');
}
}

View File

@ -1,7 +1,19 @@
<?php namespace BookStack\Actions;
use BookStack\Interfaces\Viewable;
use BookStack\Model;
use Illuminate\Database\Eloquent\Relations\MorphTo;
/**
* Class View
* Views are stored per-item per-person within the database.
* They can be used to find popular items or recently viewed items
* at a per-person level. They do not record every view instance as an
* activity. Only the latest and original view times could be recognised.
*
* @property int $views
* @property int $user_id
*/
class View extends Model
{
@ -9,10 +21,37 @@ class View extends Model
/**
* Get all owning viewable models.
* @return \Illuminate\Database\Eloquent\Relations\MorphTo
*/
public function viewable()
public function viewable(): MorphTo
{
return $this->morphTo();
}
/**
* Increment the current user's view count for the given viewable model.
*/
public static function incrementFor(Viewable $viewable): int
{
$user = user();
if (is_null($user) || $user->isDefault()) {
return 0;
}
/** @var View $view */
$view = $viewable->views()->firstOrNew([
'user_id' => $user->id,
], ['views' => 0]);
$view->forceFill(['views' => $view->views + 1])->save();
return $view->views;
}
/**
* Clear all views from the system.
*/
public static function clearAll()
{
static::query()->truncate();
}
}

View File

@ -1,117 +0,0 @@
<?php namespace BookStack\Actions;
use BookStack\Auth\Permissions\PermissionService;
use BookStack\Entities\Models\Book;
use BookStack\Entities\Models\Entity;
use BookStack\Entities\EntityProvider;
use DB;
use Illuminate\Support\Collection;
class ViewService
{
protected $view;
protected $permissionService;
protected $entityProvider;
/**
* ViewService constructor.
* @param View $view
* @param PermissionService $permissionService
* @param EntityProvider $entityProvider
*/
public function __construct(View $view, PermissionService $permissionService, EntityProvider $entityProvider)
{
$this->view = $view;
$this->permissionService = $permissionService;
$this->entityProvider = $entityProvider;
}
/**
* Add a view to the given entity.
* @param \BookStack\Entities\Models\Entity $entity
* @return int
*/
public function add(Entity $entity)
{
$user = user();
if ($user === null || $user->isDefault()) {
return 0;
}
$view = $entity->views()->where('user_id', '=', $user->id)->first();
// Add view if model exists
if ($view) {
$view->increment('views');
return $view->views;
}
// Otherwise create new view count
$entity->views()->save($this->view->newInstance([
'user_id' => $user->id,
'views' => 1
]));
return 1;
}
/**
* Get the entities with the most views.
* @param int $count
* @param int $page
* @param string|array $filterModels
* @param string $action - used for permission checking
* @return Collection
*/
public function getPopular(int $count = 10, int $page = 0, array $filterModels = null, string $action = 'view')
{
$skipCount = $count * $page;
$query = $this->permissionService
->filterRestrictedEntityRelations($this->view->newQuery(), 'views', 'viewable_id', 'viewable_type', $action)
->select('*', 'viewable_id', 'viewable_type', DB::raw('SUM(views) as view_count'))
->groupBy('viewable_id', 'viewable_type')
->orderBy('view_count', 'desc');
if ($filterModels) {
$query->whereIn('viewable_type', $this->entityProvider->getMorphClasses($filterModels));
}
return $query->with('viewable')
->skip($skipCount)
->take($count)
->get()
->pluck('viewable')
->filter();
}
/**
* Get all recently viewed entities for the current user.
*/
public function getUserRecentlyViewed(int $count = 10, int $page = 1)
{
$user = user();
if ($user === null || $user->isDefault()) {
return collect();
}
$all = collect();
/** @var Entity $instance */
foreach ($this->entityProvider->all() as $name => $instance) {
$items = $instance::visible()->withLastView()
->having('last_viewed_at', '>', 0)
->orderBy('last_viewed_at', 'desc')
->skip($count * ($page - 1))
->take($count)
->get();
$all = $all->concat($items);
}
return $all->sortByDesc('last_viewed_at')->slice(0, $count);
}
/**
* Reset all view counts by deleting all views.
*/
public function resetAll()
{
$this->view->truncate();
}
}

View File

@ -90,6 +90,11 @@ class LdapSessionGuard extends ExternalBaseSessionGuard
$this->ldapService->syncGroups($user, $username);
}
// Attach avatar if non-existent
if (is_null($user->avatar)) {
$this->ldapService->saveAndAttachAvatar($user, $userDetails);
}
$this->login($user, $remember);
return true;
}
@ -115,6 +120,8 @@ class LdapSessionGuard extends ExternalBaseSessionGuard
'password' => Str::random(32),
];
return $this->registrationService->registerUser($details, null, false);
$user = $this->registrationService->registerUser($details, null, false);
$this->ldapService->saveAndAttachAvatar($user, $ldapUserDetails);
return $user;
}
}

View File

@ -3,7 +3,9 @@
use BookStack\Auth\User;
use BookStack\Exceptions\JsonDebugException;
use BookStack\Exceptions\LdapException;
use BookStack\Uploads\UserAvatars;
use ErrorException;
use Illuminate\Support\Facades\Log;
/**
* Class LdapService
@ -14,15 +16,17 @@ class LdapService extends ExternalAuthService
protected $ldap;
protected $ldapConnection;
protected $userAvatars;
protected $config;
protected $enabled;
/**
* LdapService constructor.
*/
public function __construct(Ldap $ldap)
public function __construct(Ldap $ldap, UserAvatars $userAvatars)
{
$this->ldap = $ldap;
$this->userAvatars = $userAvatars;
$this->config = config('services.ldap');
$this->enabled = config('auth.method') === 'ldap';
}
@ -76,10 +80,13 @@ class LdapService extends ExternalAuthService
$idAttr = $this->config['id_attribute'];
$emailAttr = $this->config['email_attribute'];
$displayNameAttr = $this->config['display_name_attribute'];
$thumbnailAttr = $this->config['thumbnail_attribute'];
$user = $this->getUserWithAttributes($userName, ['cn', 'dn', $idAttr, $emailAttr, $displayNameAttr]);
$user = $this->getUserWithAttributes($userName, array_filter([
'cn', 'dn', $idAttr, $emailAttr, $displayNameAttr, $thumbnailAttr,
]));
if ($user === null) {
if (is_null($user)) {
return null;
}
@ -89,6 +96,7 @@ class LdapService extends ExternalAuthService
'name' => $this->getUserResponseProperty($user, $displayNameAttr, $userCn),
'dn' => $user['dn'],
'email' => $this->getUserResponseProperty($user, $emailAttr, null),
'avatar'=> $thumbnailAttr ? $this->getUserResponseProperty($user, $thumbnailAttr, null) : null,
];
if ($this->config['dump_user_details']) {
@ -350,4 +358,22 @@ class LdapService extends ExternalAuthService
$userLdapGroups = $this->getUserGroups($username);
$this->syncWithGroups($user, $userLdapGroups);
}
/**
* Save and attach an avatar image, if found in the ldap details, and attach
* to the given user model.
*/
public function saveAndAttachAvatar(User $user, array $ldapUserDetails): void
{
if (is_null(config('services.ldap.thumbnail_attribute')) || is_null($ldapUserDetails['avatar'])) {
return;
}
try {
$imageData = $ldapUserDetails['avatar'];
$this->userAvatars->assignToUserFromExistingData($user, $imageData, 'jpg');
} catch (\Exception $exception) {
Log::info("Failed to use avatar image from LDAP data for user id {$user->id}");
}
}
}

View File

@ -580,14 +580,15 @@ class PermissionService
/**
* Filter items that have entities set as a polymorphic relation.
* @param Builder|\Illuminate\Database\Query\Builder $query
*/
public function filterRestrictedEntityRelations(Builder $query, string $tableName, string $entityIdColumn, string $entityTypeColumn, string $action = 'view'): Builder
public function filterRestrictedEntityRelations($query, string $tableName, string $entityIdColumn, string $entityTypeColumn, string $action = 'view')
{
$tableDetails = ['tableName' => $tableName, 'entityIdColumn' => $entityIdColumn, 'entityTypeColumn' => $entityTypeColumn];
$q = $query->where(function ($query) use ($tableDetails, $action) {
$query->whereExists(function ($permissionQuery) use (&$tableDetails, $action) {
$permissionQuery->select('id')->from('joint_permissions')
$permissionQuery->select(['role_id'])->from('joint_permissions')
->whereRaw('joint_permissions.entity_id=' . $tableDetails['tableName'] . '.' . $tableDetails['entityIdColumn'])
->whereRaw('joint_permissions.entity_type=' . $tableDetails['tableName'] . '.' . $tableDetails['entityTypeColumn'])
->where('action', '=', $action)

View File

@ -1,5 +1,6 @@
<?php namespace BookStack\Auth;
use BookStack\Actions\Favourite;
use BookStack\Api\ApiToken;
use BookStack\Entities\Tools\SlugGenerator;
use BookStack\Interfaces\Loggable;
@ -240,6 +241,14 @@ class User extends Model implements AuthenticatableContract, CanResetPasswordCon
return $this->hasMany(ApiToken::class);
}
/**
* Get the favourite instances for this user.
*/
public function favourites(): HasMany
{
return $this->hasMany(Favourite::class);
}
/**
* Get the last activity time for this user.
*/

View File

@ -8,13 +8,11 @@ use BookStack\Entities\Models\Chapter;
use BookStack\Entities\Models\Page;
use BookStack\Exceptions\NotFoundException;
use BookStack\Exceptions\UserUpdateException;
use BookStack\Uploads\Image;
use BookStack\Uploads\UserAvatars;
use Exception;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Database\Eloquent\Collection;
use Illuminate\Pagination\LengthAwarePaginator;
use Images;
use Log;
class UserRepo
@ -184,16 +182,11 @@ class UserRepo
{
$user->socialAccounts()->delete();
$user->apiTokens()->delete();
$user->favourites()->delete();
$user->delete();
// Delete user profile images
$profileImages = Image::query()->where('type', '=', 'user')
->where('uploaded_to', '=', $user->id)
->get();
foreach ($profileImages as $image) {
Images::destroy($image);
}
$this->userAvatar->destroyAllForUser($user);
if (!empty($newOwnerId)) {
$newOwner = User::query()->find($newOwnerId);

View File

@ -184,11 +184,8 @@ return [
// Custom BookStack
'Activity' => BookStack\Facades\Activity::class,
'Views' => BookStack\Facades\Views::class,
'Images' => BookStack\Facades\Images::class,
'Permissions' => BookStack\Facades\Permissions::class,
'Theme' => BookStack\Facades\Theme::class,
],
// Proxy configuration

View File

@ -133,6 +133,7 @@ return [
'remove_from_groups' => env('LDAP_REMOVE_FROM_GROUPS', false),
'tls_insecure' => env('LDAP_TLS_INSECURE', false),
'start_tls' => env('LDAP_START_TLS', false),
'thumbnail_attribute' => env('LDAP_THUMBNAIL_ATTRIBUTE', null),
],
];

View File

@ -2,6 +2,7 @@
namespace BookStack\Console\Commands;
use BookStack\Actions\View;
use Illuminate\Console\Command;
class ClearViews extends Command
@ -36,7 +37,7 @@ class ClearViews extends Command
*/
public function handle()
{
\Views::resetAll();
View::clearAll();
$this->comment('Views cleared');
}
}

View File

@ -2,6 +2,7 @@
use BookStack\Actions\Activity;
use BookStack\Actions\Comment;
use BookStack\Actions\Favourite;
use BookStack\Actions\Tag;
use BookStack\Actions\View;
use BookStack\Auth\Permissions\EntityPermission;
@ -9,7 +10,9 @@ use BookStack\Auth\Permissions\JointPermission;
use BookStack\Entities\Tools\SearchIndex;
use BookStack\Entities\Tools\SlugGenerator;
use BookStack\Facades\Permissions;
use BookStack\Interfaces\Favouritable;
use BookStack\Interfaces\Sluggable;
use BookStack\Interfaces\Viewable;
use BookStack\Model;
use BookStack\Traits\HasCreatorAndUpdater;
use BookStack\Traits\HasOwner;
@ -38,7 +41,7 @@ use Illuminate\Database\Eloquent\SoftDeletes;
* @method static Builder withLastView()
* @method static Builder withViewCount()
*/
abstract class Entity extends Model implements Sluggable
abstract class Entity extends Model implements Sluggable, Favouritable, Viewable
{
use SoftDeletes;
use HasCreatorAndUpdater;
@ -297,4 +300,22 @@ abstract class Entity extends Model implements Sluggable
$this->slug = app(SlugGenerator::class)->generate($this);
return $this->slug;
}
/**
* @inheritdoc
*/
public function favourites(): MorphMany
{
return $this->morphMany(Favourite::class, 'favouritable');
}
/**
* Check if the entity is a favourite of the current user.
*/
public function isFavourite(): bool
{
return $this->favourites()
->where('user_id', '=', user()->id)
->exists();
}
}

View File

@ -75,11 +75,23 @@ class Page extends BookChild
/**
* Get the associated page revisions, ordered by created date.
* @return mixed
* Only provides actual saved page revision instances, Not drafts.
*/
public function revisions()
public function revisions(): HasMany
{
return $this->hasMany(PageRevision::class)->where('type', '=', 'version')->orderBy('created_at', 'desc')->orderBy('id', 'desc');
return $this->allRevisions()
->where('type', '=', 'version')
->orderBy('created_at', 'desc')
->orderBy('id', 'desc');
}
/**
* Get all revision instances assigned to this page.
* Includes all types of revisions.
*/
public function allRevisions(): HasMany
{
return $this->hasMany(PageRevision::class);
}
/**

View File

@ -0,0 +1,17 @@
<?php namespace BookStack\Entities\Queries;
use BookStack\Auth\Permissions\PermissionService;
use BookStack\Entities\EntityProvider;
abstract class EntityQuery
{
protected function permissionService(): PermissionService
{
return app()->make(PermissionService::class);
}
protected function entityProvider(): EntityProvider
{
return app()->make(EntityProvider::class);
}
}

View File

@ -0,0 +1,29 @@
<?php namespace BookStack\Entities\Queries;
use BookStack\Actions\View;
use Illuminate\Support\Facades\DB;
class Popular extends EntityQuery
{
public function run(int $count, int $page, array $filterModels = null, string $action = 'view')
{
$query = $this->permissionService()
->filterRestrictedEntityRelations(View::query(), 'views', 'viewable_id', 'viewable_type', $action)
->select('*', 'viewable_id', 'viewable_type', DB::raw('SUM(views) as view_count'))
->groupBy('viewable_id', 'viewable_type')
->orderBy('view_count', 'desc');
if ($filterModels) {
$query->whereIn('viewable_type', $this->entityProvider()->getMorphClasses($filterModels));
}
return $query->with('viewable')
->skip($count * ($page - 1))
->take($count)
->get()
->pluck('viewable')
->filter();
}
}

View File

@ -0,0 +1,32 @@
<?php namespace BookStack\Entities\Queries;
use BookStack\Actions\View;
use Illuminate\Support\Collection;
class RecentlyViewed extends EntityQuery
{
public function run(int $count, int $page): Collection
{
$user = user();
if ($user === null || $user->isDefault()) {
return collect();
}
$query = $this->permissionService()->filterRestrictedEntityRelations(
View::query(),
'views',
'viewable_id',
'viewable_type',
'view'
)
->orderBy('views.updated_at', 'desc')
->where('user_id', '=', user()->id);
return $query->with('viewable')
->skip(($page - 1) * $count)
->take($count)
->get()
->pluck('viewable')
->filter();
}
}

View File

@ -0,0 +1,33 @@
<?php namespace BookStack\Entities\Queries;
use BookStack\Actions\Favourite;
use Illuminate\Database\Query\JoinClause;
class TopFavourites extends EntityQuery
{
public function run(int $count, int $skip = 0)
{
$user = user();
if (is_null($user) || $user->isDefault()) {
return collect();
}
$query = $this->permissionService()
->filterRestrictedEntityRelations(Favourite::query(), 'favourites', 'favouritable_id', 'favouritable_type', 'view')
->select('favourites.*')
->leftJoin('views', function (JoinClause $join) {
$join->on('favourites.favouritable_id', '=', 'views.viewable_id');
$join->on('favourites.favouritable_type', '=', 'views.viewable_type');
$join->where('views.user_id', '=', user()->id);
})
->orderBy('views.views', 'desc')
->where('favourites.user_id', '=', user()->id);
return $query->with('favouritable')
->skip($skip)
->take($count)
->get()
->pluck('favouritable')
->filter();
}
}

View File

@ -0,0 +1,69 @@
<?php namespace BookStack\Entities\Tools;
use BookStack\Entities\Models\BookChild;
use BookStack\Entities\Models\Entity;
use Illuminate\Support\Collection;
/**
* Finds the next or previous content of a book element (page or chapter).
*/
class NextPreviousContentLocator
{
protected $relativeBookItem;
protected $flatTree;
protected $currentIndex = null;
/**
* NextPreviousContentLocator constructor.
*/
public function __construct(BookChild $relativeBookItem, Collection $bookTree)
{
$this->relativeBookItem = $relativeBookItem;
$this->flatTree = $this->treeToFlatOrderedCollection($bookTree);
$this->currentIndex = $this->getCurrentIndex();
}
/**
* Get the next logical entity within the book hierarchy.
*/
public function getNext(): ?Entity
{
return $this->flatTree->get($this->currentIndex + 1);
}
/**
* Get the next logical entity within the book hierarchy.
*/
public function getPrevious(): ?Entity
{
return $this->flatTree->get($this->currentIndex - 1);
}
/**
* Get the index of the current relative item.
*/
protected function getCurrentIndex(): ?int
{
$index = $this->flatTree->search(function (Entity $entity) {
return get_class($entity) === get_class($this->relativeBookItem)
&& $entity->id === $this->relativeBookItem->id;
});
return $index === false ? null : $index;
}
/**
* Convert a book tree collection to a flattened version
* where all items follow the expected order of user flow.
*/
protected function treeToFlatOrderedCollection(Collection $bookTree): Collection
{
$flatOrdered = collect();
/** @var Entity $item */
foreach ($bookTree->all() as $item) {
$flatOrdered->push($item);
$childPages = $item->visible_pages ?? [];
$flatOrdered = $flatOrdered->concat($childPages);
}
return $flatOrdered;
}
}

View File

@ -151,6 +151,7 @@ class TrashCan
protected function destroyPage(Page $page): int
{
$this->destroyCommonRelations($page);
$page->allRevisions()->delete();
// Delete Attached Files
$attachmentService = app(AttachmentService::class);
@ -317,6 +318,7 @@ class TrashCan
$entity->jointPermissions()->delete();
$entity->searchTerms()->delete();
$entity->deletions()->delete();
$entity->favourites()->delete();
if ($entity instanceof HasCoverImage && $entity->cover) {
$imageService = app()->make(ImageService::class);

View File

@ -1,16 +0,0 @@
<?php namespace BookStack\Facades;
use Illuminate\Support\Facades\Facade;
class Images extends Facade
{
/**
* Get the registered name of the component.
*
* @return string
*/
protected static function getFacadeAccessor()
{
return 'images';
}
}

View File

@ -1,16 +0,0 @@
<?php namespace BookStack\Facades;
use Illuminate\Support\Facades\Facade;
class Views extends Facade
{
/**
* Get the registered name of the component.
*
* @return string
*/
protected static function getFacadeAccessor()
{
return 'views';
}
}

View File

@ -12,6 +12,7 @@ use BookStack\Http\Controllers\Controller;
use BookStack\Theming\ThemeEvents;
use Illuminate\Foundation\Auth\AuthenticatesUsers;
use Illuminate\Http\Request;
use Illuminate\Validation\ValidationException;
class LoginController extends Controller
{
@ -198,4 +199,19 @@ class LoginController extends Controller
return redirect('/login');
}
/**
* Get the failed login response instance.
*
* @param \Illuminate\Http\Request $request
* @return \Symfony\Component\HttpFoundation\Response
*
* @throws \Illuminate\Validation\ValidationException
*/
protected function sendFailedLoginResponse(Request $request)
{
throw ValidationException::withMessages([
$this->username() => [trans('auth.failed')],
])->redirectTo('/login');
}
}

View File

@ -2,6 +2,7 @@
use Activity;
use BookStack\Actions\ActivityType;
use BookStack\Actions\View;
use BookStack\Entities\Tools\BookContents;
use BookStack\Entities\Models\Bookshelf;
use BookStack\Entities\Tools\PermissionsUpdater;
@ -11,7 +12,6 @@ use BookStack\Exceptions\ImageUploadException;
use Illuminate\Http\Request;
use Illuminate\Validation\ValidationException;
use Throwable;
use Views;
class BookController extends Controller
{
@ -112,7 +112,7 @@ class BookController extends Controller
$bookChildren = (new BookContents($book))->getTree(true);
$bookParentShelves = $book->shelves()->visible()->get();
Views::add($book);
View::incrementFor($book);
if ($request->has('shelf')) {
$this->entityContextManager->setShelfContext(intval($request->get('shelf')));
}

View File

@ -1,6 +1,7 @@
<?php namespace BookStack\Http\Controllers;
use Activity;
use BookStack\Actions\View;
use BookStack\Entities\Models\Book;
use BookStack\Entities\Tools\PermissionsUpdater;
use BookStack\Entities\Tools\ShelfContext;
@ -109,7 +110,7 @@ class BookshelfController extends Controller
->values()
->all();
Views::add($shelf);
View::incrementFor($shelf);
$this->entityContextManager->setShelfContext($shelf->id);
$view = setting()->getForCurrentUser('bookshelf_view_type');

View File

@ -1,15 +1,16 @@
<?php namespace BookStack\Http\Controllers;
use BookStack\Actions\View;
use BookStack\Entities\Models\Book;
use BookStack\Entities\Tools\BookContents;
use BookStack\Entities\Repos\ChapterRepo;
use BookStack\Entities\Tools\NextPreviousContentLocator;
use BookStack\Entities\Tools\PermissionsUpdater;
use BookStack\Exceptions\MoveOperationException;
use BookStack\Exceptions\NotFoundException;
use Illuminate\Http\Request;
use Illuminate\Validation\ValidationException;
use Throwable;
use Views;
class ChapterController extends Controller
{
@ -64,7 +65,8 @@ class ChapterController extends Controller
$sidebarTree = (new BookContents($chapter->book))->getTree();
$pages = $chapter->getVisiblePages();
Views::add($chapter);
$nextPreviousLocator = new NextPreviousContentLocator($chapter, $sidebarTree);
View::incrementFor($chapter);
$this->setPageTitle($chapter->getShortName());
return view('chapters.show', [
@ -72,7 +74,9 @@ class ChapterController extends Controller
'chapter' => $chapter,
'current' => $chapter,
'sidebarTree' => $sidebarTree,
'pages' => $pages
'pages' => $pages,
'next' => $nextPreviousLocator->getNext(),
'previous' => $nextPreviousLocator->getPrevious(),
]);
}

View File

@ -0,0 +1,95 @@
<?php
namespace BookStack\Http\Controllers;
use BookStack\Entities\Models\Entity;
use BookStack\Entities\Queries\TopFavourites;
use BookStack\Interfaces\Favouritable;
use BookStack\Model;
use Illuminate\Http\Request;
class FavouriteController extends Controller
{
/**
* Show a listing of all favourite items for the current user.
*/
public function index(Request $request)
{
$viewCount = 20;
$page = intval($request->get('page', 1));
$favourites = (new TopFavourites)->run($viewCount + 1, (($page - 1) * $viewCount));
$hasMoreLink = ($favourites->count() > $viewCount) ? url("/favourites?page=" . ($page+1)) : null;
return view('common.detailed-listing-with-more', [
'title' => trans('entities.my_favourites'),
'entities' => $favourites->slice(0, $viewCount),
'hasMoreLink' => $hasMoreLink,
]);
}
/**
* Add a new item as a favourite.
*/
public function add(Request $request)
{
$favouritable = $this->getValidatedModelFromRequest($request);
$favouritable->favourites()->firstOrCreate([
'user_id' => user()->id,
]);
$this->showSuccessNotification(trans('activities.favourite_add_notification', [
'name' => $favouritable->name,
]));
return redirect()->back();
}
/**
* Remove an item as a favourite.
*/
public function remove(Request $request)
{
$favouritable = $this->getValidatedModelFromRequest($request);
$favouritable->favourites()->where([
'user_id' => user()->id,
])->delete();
$this->showSuccessNotification(trans('activities.favourite_remove_notification', [
'name' => $favouritable->name,
]));
return redirect()->back();
}
/**
* @throws \Illuminate\Validation\ValidationException
* @throws \Exception
*/
protected function getValidatedModelFromRequest(Request $request): Favouritable
{
$modelInfo = $this->validate($request, [
'type' => 'required|string',
'id' => 'required|integer',
]);
if (!class_exists($modelInfo['type'])) {
throw new \Exception('Model not found');
}
/** @var Model $model */
$model = new $modelInfo['type'];
if (! $model instanceof Favouritable) {
throw new \Exception('Model not favouritable');
}
$modelInstance = $model->newQuery()
->where('id', '=', $modelInfo['id'])
->first(['id', 'name']);
$inaccessibleEntity = ($modelInstance instanceof Entity && !userCan('view', $modelInstance));
if (is_null($modelInstance) || $inaccessibleEntity) {
throw new \Exception('Model instance not found');
}
return $modelInstance;
}
}

View File

@ -2,11 +2,12 @@
use Activity;
use BookStack\Entities\Models\Book;
use BookStack\Entities\Queries\RecentlyViewed;
use BookStack\Entities\Queries\TopFavourites;
use BookStack\Entities\Tools\PageContent;
use BookStack\Entities\Models\Page;
use BookStack\Entities\Repos\BookRepo;
use BookStack\Entities\Repos\BookshelfRepo;
use Illuminate\Http\Response;
use Views;
class HomeController extends Controller
@ -32,12 +33,13 @@ class HomeController extends Controller
$recentFactor = count($draftPages) > 0 ? 0.5 : 1;
$recents = $this->isSignedIn() ?
Views::getUserRecentlyViewed(12*$recentFactor, 1)
(new RecentlyViewed)->run(12*$recentFactor, 1)
: Book::visible()->orderBy('created_at', 'desc')->take(12 * $recentFactor)->get();
$favourites = (new TopFavourites)->run(6);
$recentlyUpdatedPages = Page::visible()->with('book')
->where('draft', false)
->orderBy('updated_at', 'desc')
->take(12)
->take($favourites->count() > 0 ? 6 : 12)
->get();
$homepageOptions = ['default', 'books', 'bookshelves', 'page'];
@ -51,6 +53,7 @@ class HomeController extends Controller
'recents' => $recents,
'recentlyUpdatedPages' => $recentlyUpdatedPages,
'draftPages' => $draftPages,
'favourites' => $favourites,
];
// Add required list ordering & sorting for books & shelves views.

View File

@ -1,6 +1,8 @@
<?php namespace BookStack\Http\Controllers;
use BookStack\Actions\View;
use BookStack\Entities\Tools\BookContents;
use BookStack\Entities\Tools\NextPreviousContentLocator;
use BookStack\Entities\Tools\PageContent;
use BookStack\Entities\Tools\PageEditActivity;
use BookStack\Entities\Models\Page;
@ -12,7 +14,6 @@ use Exception;
use Illuminate\Http\Request;
use Illuminate\Validation\ValidationException;
use Throwable;
use Views;
class PageController extends Controller
{
@ -141,7 +142,9 @@ class PageController extends Controller
$page->load(['comments.createdBy']);
}
Views::add($page);
$nextPreviousLocator = new NextPreviousContentLocator($page, $sidebarTree);
View::incrementFor($page);
$this->setPageTitle($page->getShortName());
return view('pages.show', [
'page' => $page,
@ -149,7 +152,9 @@ class PageController extends Controller
'current' => $page,
'sidebarTree' => $sidebarTree,
'commentsEnabled' => $commentsEnabled,
'pageNav' => $pageNav
'pageNav' => $pageNav,
'next' => $nextPreviousLocator->getNext(),
'previous' => $nextPreviousLocator->getPrevious(),
]);
}
@ -266,7 +271,7 @@ class PageController extends Controller
{
$page = $this->pageRepo->getBySlug($bookSlug, $pageSlug);
$this->checkOwnablePermission('page-delete', $page);
$this->setPageTitle(trans('entities.pages_delete_named', ['pageName'=>$page->getShortName()]));
$this->setPageTitle(trans('entities.pages_delete_named', ['pageName' => $page->getShortName()]));
return view('pages.delete', [
'book' => $page->book,
'page' => $page,
@ -282,7 +287,7 @@ class PageController extends Controller
{
$page = $this->pageRepo->getById($pageId);
$this->checkOwnablePermission('page-update', $page);
$this->setPageTitle(trans('entities.pages_delete_draft_named', ['pageName'=>$page->getShortName()]));
$this->setPageTitle(trans('entities.pages_delete_draft_named', ['pageName' => $page->getShortName()]));
return view('pages.delete', [
'book' => $page->book,
'page' => $page,
@ -337,9 +342,9 @@ class PageController extends Controller
->paginate(20)
->setPath(url('/pages/recently-updated'));
return view('pages.detailed-listing', [
return view('common.detailed-listing-paginated', [
'title' => trans('entities.recently_updated_pages'),
'pages' => $pages
'entities' => $pages
]);
}

View File

@ -1,6 +1,6 @@
<?php namespace BookStack\Http\Controllers;
use BookStack\Actions\ViewService;
use BookStack\Entities\Queries\Popular;
use BookStack\Entities\Tools\SearchRunner;
use BookStack\Entities\Tools\ShelfContext;
use BookStack\Entities\Tools\SearchOptions;
@ -9,16 +9,13 @@ use Illuminate\Http\Request;
class SearchController extends Controller
{
protected $viewService;
protected $searchRunner;
protected $entityContextManager;
public function __construct(
ViewService $viewService,
SearchRunner $searchRunner,
ShelfContext $entityContextManager
) {
$this->viewService = $viewService;
$this->searchRunner = $searchRunner;
$this->entityContextManager = $entityContextManager;
}
@ -82,7 +79,7 @@ class SearchController extends Controller
$searchTerm .= ' {type:'. implode('|', $entityTypes) .'}';
$entities = $this->searchRunner->searchEntities(SearchOptions::fromString($searchTerm), 'all', 1, 20, $permission)['results'];
} else {
$entities = $this->viewService->getPopular(20, 0, $entityTypes, $permission);
$entities = (new Popular)->run(20, 0, $entityTypes, $permission);
}
return view('search.entity-ajax-list', ['entities' => $entities]);

View File

@ -0,0 +1,11 @@
<?php namespace BookStack\Interfaces;
use Illuminate\Database\Eloquent\Relations\MorphMany;
interface Favouritable
{
/**
* Get the related favourite instances.
*/
public function favourites(): MorphMany;
}

View File

@ -0,0 +1,11 @@
<?php namespace BookStack\Interfaces;
use Illuminate\Database\Eloquent\Relations\MorphMany;
interface Viewable
{
/**
* Get all view instances for this viewable model.
*/
public function views(): MorphMany;
}

View File

@ -3,7 +3,6 @@
namespace BookStack\Providers;
use BookStack\Actions\ActivityService;
use BookStack\Actions\ViewService;
use BookStack\Auth\Permissions\PermissionService;
use BookStack\Theming\ThemeService;
use BookStack\Uploads\ImageService;
@ -32,10 +31,6 @@ class CustomFacadeProvider extends ServiceProvider
return $this->app->make(ActivityService::class);
});
$this->app->singleton('views', function () {
return $this->app->make(ViewService::class);
});
$this->app->singleton('images', function () {
return $this->app->make(ImageService::class);
});

View File

@ -3,7 +3,6 @@
use BookStack\Entities\Models\Page;
use BookStack\Model;
use BookStack\Traits\HasCreatorAndUpdater;
use Images;
class Image extends Model
{
@ -14,23 +13,18 @@ class Image extends Model
/**
* Get a thumbnail for this image.
* @param int $width
* @param int $height
* @param bool|false $keepRatio
* @return string
* @throws \Exception
*/
public function getThumb($width, $height, $keepRatio = false)
public function getThumb(int $width, int $height, bool $keepRatio = false): string
{
return Images::getThumbnail($this, $width, $height, $keepRatio);
return app()->make(ImageService::class)->getThumbnail($this, $width, $height, $keepRatio);
}
/**
* Get the page this image has been uploaded to.
* Only applicable to gallery or drawio image types.
* @return Page|null
*/
public function getPage()
public function getPage(): ?Page
{
return $this->belongsTo(Page::class, 'uploaded_to')->first();
}

View File

@ -26,6 +26,7 @@ class UserAvatars
}
try {
$this->destroyAllForUser($user);
$avatar = $this->saveAvatarImage($user);
$user->avatar()->associate($avatar);
$user->save();
@ -34,6 +35,35 @@ class UserAvatars
}
}
/**
* Assign a new avatar image to the given user using the given image data.
*/
public function assignToUserFromExistingData(User $user, string $imageData, string $extension): void
{
try {
$this->destroyAllForUser($user);
$avatar = $this->createAvatarImageFromData($user, $imageData, $extension);
$user->avatar()->associate($avatar);
$user->save();
} catch (Exception $e) {
Log::error('Failed to save user avatar image');
}
}
/**
* Destroy all user avatars uploaded to the given user.
*/
public function destroyAllForUser(User $user)
{
$profileImages = Image::query()->where('type', '=', 'user')
->where('uploaded_to', '=', $user->id)
->get();
foreach ($profileImages as $image) {
$this->imageService->destroy($image);
}
}
/**
* Save an avatar image from an external service.
* @throws Exception
@ -50,8 +80,16 @@ class UserAvatars
];
$userAvatarUrl = strtr($avatarUrl, $replacements);
$imageName = str_replace(' ', '-', $user->id . '-avatar.png');
$imageData = $this->getAvatarImageData($userAvatarUrl);
return $this->createAvatarImageFromData($user, $imageData, 'png');
}
/**
* Creates a new image instance and saves it in the system as a new user avatar image.
*/
protected function createAvatarImageFromData(User $user, string $imageData, string $extension): Image
{
$imageName = str_replace(' ', '-', $user->id . '-avatar.' . $extension);
$image = $this->imageService->saveNew($imageName, $imageData, 'user', $user->id);
$image->created_by = $user->id;

356
composer.lock generated

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,36 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
class CreateFavouritesTable extends Migration
{
/**
* Run the migrations.
*
* @return void
*/
public function up()
{
Schema::create('favourites', function (Blueprint $table) {
$table->increments('id');
$table->integer('user_id')->index();
$table->integer('favouritable_id');
$table->string('favouritable_type', 100);
$table->timestamps();
$table->index(['favouritable_id', 'favouritable_type'], 'favouritable_index');
});
}
/**
* Reverse the migrations.
*
* @return void
*/
public function down()
{
Schema::dropIfExists('favourites');
}
}

139
package-lock.json generated
View File

@ -6,19 +6,19 @@
"": {
"dependencies": {
"clipboard": "^2.0.8",
"codemirror": "^5.60.0",
"codemirror": "^5.61.1",
"dropzone": "^5.9.2",
"markdown-it": "^11.0.1",
"markdown-it": "^12.0.6",
"markdown-it-task-lists": "^2.1.1",
"sortablejs": "^1.13.0"
},
"devDependencies": {
"chokidar-cli": "^2.1.0",
"esbuild": "0.7.8",
"esbuild": "0.12.5",
"livereload": "^0.9.3",
"npm-run-all": "^4.1.5",
"punycode": "^2.1.1",
"sass": "^1.32.8"
"sass": "^1.34.0"
}
},
"node_modules/ansi-regex": {
@ -56,12 +56,9 @@
}
},
"node_modules/argparse": {
"version": "1.0.10",
"resolved": "https://registry.npmjs.org/argparse/-/argparse-1.0.10.tgz",
"integrity": "sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==",
"dependencies": {
"sprintf-js": "~1.0.2"
}
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz",
"integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q=="
},
"node_modules/balanced-match": {
"version": "1.0.0",
@ -184,9 +181,9 @@
}
},
"node_modules/codemirror": {
"version": "5.60.0",
"resolved": "https://registry.npmjs.org/codemirror/-/codemirror-5.60.0.tgz",
"integrity": "sha512-AEL7LhFOlxPlCL8IdTcJDblJm8yrAGib7I+DErJPdZd4l6imx8IMgKK3RblVgBQqz3TZJR4oknQ03bz+uNjBYA=="
"version": "5.61.1",
"resolved": "https://registry.npmjs.org/codemirror/-/codemirror-5.61.1.tgz",
"integrity": "sha512-+D1NZjAucuzE93vJGbAaXzvoBHwp9nJZWWWF9utjv25+5AZUiah6CIlfb4ikG4MoDsFsCG8niiJH5++OO2LgIQ=="
},
"node_modules/color-convert": {
"version": "1.9.3",
@ -263,9 +260,12 @@
"dev": true
},
"node_modules/entities": {
"version": "2.0.3",
"resolved": "https://registry.npmjs.org/entities/-/entities-2.0.3.tgz",
"integrity": "sha512-MyoZ0jgnLvB2X3Lg5HqpFmn1kybDiIfEQmKzTb5apr51Rb+T3KdmMiqa70T+bhGnyv7bQ6WMj2QMHpGMmlrUYQ=="
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/entities/-/entities-2.1.0.tgz",
"integrity": "sha512-hCx1oky9PFrJ611mf0ifBLBRW8lUUVRlFolb5gWRfIELabBlbp9xZvrqZLZAs+NxFnbfQoeGd8wDkygjg7U85w==",
"funding": {
"url": "https://github.com/fb55/entities?sponsor=1"
}
},
"node_modules/error-ex": {
"version": "1.3.2",
@ -313,10 +313,11 @@
}
},
"node_modules/esbuild": {
"version": "0.7.8",
"resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.7.8.tgz",
"integrity": "sha512-6UT1nZB+8ja5avctUC6d3kGOUAhy6/ZYHljL4nk3++1ipadghBhUCAcwsTHsmUvdu04CcGKzo13mE+ZQ2O3zrA==",
"version": "0.12.5",
"resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.12.5.tgz",
"integrity": "sha512-vcuP53pA5XiwUU4FnlXM+2PnVjTfHGthM7uP1gtp+9yfheGvFFbq/KyuESThmtoHPUrfZH5JpxGVJIFDVD1Egw==",
"dev": true,
"hasInstallScript": true,
"bin": {
"esbuild": "bin/esbuild"
}
@ -674,12 +675,12 @@
"dev": true
},
"node_modules/markdown-it": {
"version": "11.0.1",
"resolved": "https://registry.npmjs.org/markdown-it/-/markdown-it-11.0.1.tgz",
"integrity": "sha512-aU1TzmBKcWNNYvH9pjq6u92BML+Hz3h5S/QpfTFwiQF852pLT+9qHsrhM9JYipkOXZxGn+sGH8oyJE9FD9WezQ==",
"version": "12.0.6",
"resolved": "https://registry.npmjs.org/markdown-it/-/markdown-it-12.0.6.tgz",
"integrity": "sha512-qv3sVLl4lMT96LLtR7xeRJX11OUFjsaD5oVat2/SNBIb21bJXwal2+SklcRbTwGwqWpWH/HRtYavOoJE+seL8w==",
"dependencies": {
"argparse": "^1.0.7",
"entities": "~2.0.0",
"argparse": "^2.0.1",
"entities": "~2.1.0",
"linkify-it": "^3.0.1",
"mdurl": "^1.0.1",
"uc.micro": "^1.0.5"
@ -979,12 +980,12 @@
}
},
"node_modules/sass": {
"version": "1.32.8",
"resolved": "https://registry.npmjs.org/sass/-/sass-1.32.8.tgz",
"integrity": "sha512-Sl6mIeGpzjIUZqvKnKETfMf0iDAswD9TNlv13A7aAF3XZlRPMq4VvJWBC2N2DXbp94MQVdNSFG6LfF/iOXrPHQ==",
"version": "1.34.0",
"resolved": "https://registry.npmjs.org/sass/-/sass-1.34.0.tgz",
"integrity": "sha512-rHEN0BscqjUYuomUEaqq3BMgsXqQfkcMVR7UhscsAVub0/spUrZGBMxQXFS2kfiDsPLZw5yuU9iJEFNC2x38Qw==",
"dev": true,
"dependencies": {
"chokidar": ">=2.0.0 <4.0.0"
"chokidar": ">=3.0.0 <4.0.0"
},
"bin": {
"sass": "sass.js"
@ -1077,11 +1078,6 @@
"integrity": "sha512-J+FWzZoynJEXGphVIS+XEh3kFSjZX/1i9gFBaWQcB+/tmpe2qUsSBABpcxqxnAxFdiUFEgAX1bjYGQvIZmoz9Q==",
"dev": true
},
"node_modules/sprintf-js": {
"version": "1.0.3",
"resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.0.3.tgz",
"integrity": "sha1-BOaSb2YolTVPPdAVIDYzuFcpfiw="
},
"node_modules/string-width": {
"version": "3.1.0",
"resolved": "https://registry.npmjs.org/string-width/-/string-width-3.1.0.tgz",
@ -1227,12 +1223,24 @@
}
},
"node_modules/ws": {
"version": "7.4.4",
"resolved": "https://registry.npmjs.org/ws/-/ws-7.4.4.tgz",
"integrity": "sha512-Qm8k8ojNQIMx7S+Zp8u/uHOx7Qazv3Yv4q68MiWWWOJhiwG5W3x7iqmRtJo8xxrciZUY4vRxUTJCKuRnF28ZZw==",
"version": "7.4.6",
"resolved": "https://registry.npmjs.org/ws/-/ws-7.4.6.tgz",
"integrity": "sha512-YmhHDO4MzaDLB+M9ym/mDA5z0naX8j7SIlT8f8z+I0VtzsRbekxEutHSme7NPS2qE8StCYQNUnfWdXta/Yu85A==",
"dev": true,
"engines": {
"node": ">=8.3.0"
},
"peerDependencies": {
"bufferutil": "^4.0.1",
"utf-8-validate": "^5.0.2"
},
"peerDependenciesMeta": {
"bufferutil": {
"optional": true
},
"utf-8-validate": {
"optional": true
}
}
},
"node_modules/y18n": {
@ -1297,12 +1305,9 @@
}
},
"argparse": {
"version": "1.0.10",
"resolved": "https://registry.npmjs.org/argparse/-/argparse-1.0.10.tgz",
"integrity": "sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==",
"requires": {
"sprintf-js": "~1.0.2"
}
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz",
"integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q=="
},
"balanced-match": {
"version": "1.0.0",
@ -1402,9 +1407,9 @@
}
},
"codemirror": {
"version": "5.60.0",
"resolved": "https://registry.npmjs.org/codemirror/-/codemirror-5.60.0.tgz",
"integrity": "sha512-AEL7LhFOlxPlCL8IdTcJDblJm8yrAGib7I+DErJPdZd4l6imx8IMgKK3RblVgBQqz3TZJR4oknQ03bz+uNjBYA=="
"version": "5.61.1",
"resolved": "https://registry.npmjs.org/codemirror/-/codemirror-5.61.1.tgz",
"integrity": "sha512-+D1NZjAucuzE93vJGbAaXzvoBHwp9nJZWWWF9utjv25+5AZUiah6CIlfb4ikG4MoDsFsCG8niiJH5++OO2LgIQ=="
},
"color-convert": {
"version": "1.9.3",
@ -1472,9 +1477,9 @@
"dev": true
},
"entities": {
"version": "2.0.3",
"resolved": "https://registry.npmjs.org/entities/-/entities-2.0.3.tgz",
"integrity": "sha512-MyoZ0jgnLvB2X3Lg5HqpFmn1kybDiIfEQmKzTb5apr51Rb+T3KdmMiqa70T+bhGnyv7bQ6WMj2QMHpGMmlrUYQ=="
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/entities/-/entities-2.1.0.tgz",
"integrity": "sha512-hCx1oky9PFrJ611mf0ifBLBRW8lUUVRlFolb5gWRfIELabBlbp9xZvrqZLZAs+NxFnbfQoeGd8wDkygjg7U85w=="
},
"error-ex": {
"version": "1.3.2",
@ -1516,9 +1521,9 @@
}
},
"esbuild": {
"version": "0.7.8",
"resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.7.8.tgz",
"integrity": "sha512-6UT1nZB+8ja5avctUC6d3kGOUAhy6/ZYHljL4nk3++1ipadghBhUCAcwsTHsmUvdu04CcGKzo13mE+ZQ2O3zrA==",
"version": "0.12.5",
"resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.12.5.tgz",
"integrity": "sha512-vcuP53pA5XiwUU4FnlXM+2PnVjTfHGthM7uP1gtp+9yfheGvFFbq/KyuESThmtoHPUrfZH5JpxGVJIFDVD1Egw==",
"dev": true
},
"escape-string-regexp": {
@ -1793,12 +1798,12 @@
"dev": true
},
"markdown-it": {
"version": "11.0.1",
"resolved": "https://registry.npmjs.org/markdown-it/-/markdown-it-11.0.1.tgz",
"integrity": "sha512-aU1TzmBKcWNNYvH9pjq6u92BML+Hz3h5S/QpfTFwiQF852pLT+9qHsrhM9JYipkOXZxGn+sGH8oyJE9FD9WezQ==",
"version": "12.0.6",
"resolved": "https://registry.npmjs.org/markdown-it/-/markdown-it-12.0.6.tgz",
"integrity": "sha512-qv3sVLl4lMT96LLtR7xeRJX11OUFjsaD5oVat2/SNBIb21bJXwal2+SklcRbTwGwqWpWH/HRtYavOoJE+seL8w==",
"requires": {
"argparse": "^1.0.7",
"entities": "~2.0.0",
"argparse": "^2.0.1",
"entities": "~2.1.0",
"linkify-it": "^3.0.1",
"mdurl": "^1.0.1",
"uc.micro": "^1.0.5"
@ -2027,12 +2032,12 @@
}
},
"sass": {
"version": "1.32.8",
"resolved": "https://registry.npmjs.org/sass/-/sass-1.32.8.tgz",
"integrity": "sha512-Sl6mIeGpzjIUZqvKnKETfMf0iDAswD9TNlv13A7aAF3XZlRPMq4VvJWBC2N2DXbp94MQVdNSFG6LfF/iOXrPHQ==",
"version": "1.34.0",
"resolved": "https://registry.npmjs.org/sass/-/sass-1.34.0.tgz",
"integrity": "sha512-rHEN0BscqjUYuomUEaqq3BMgsXqQfkcMVR7UhscsAVub0/spUrZGBMxQXFS2kfiDsPLZw5yuU9iJEFNC2x38Qw==",
"dev": true,
"requires": {
"chokidar": ">=2.0.0 <4.0.0"
"chokidar": ">=3.0.0 <4.0.0"
}
},
"select": {
@ -2110,11 +2115,6 @@
"integrity": "sha512-J+FWzZoynJEXGphVIS+XEh3kFSjZX/1i9gFBaWQcB+/tmpe2qUsSBABpcxqxnAxFdiUFEgAX1bjYGQvIZmoz9Q==",
"dev": true
},
"sprintf-js": {
"version": "1.0.3",
"resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.0.3.tgz",
"integrity": "sha1-BOaSb2YolTVPPdAVIDYzuFcpfiw="
},
"string-width": {
"version": "3.1.0",
"resolved": "https://registry.npmjs.org/string-width/-/string-width-3.1.0.tgz",
@ -2236,10 +2236,11 @@
}
},
"ws": {
"version": "7.4.4",
"resolved": "https://registry.npmjs.org/ws/-/ws-7.4.4.tgz",
"integrity": "sha512-Qm8k8ojNQIMx7S+Zp8u/uHOx7Qazv3Yv4q68MiWWWOJhiwG5W3x7iqmRtJo8xxrciZUY4vRxUTJCKuRnF28ZZw==",
"dev": true
"version": "7.4.6",
"resolved": "https://registry.npmjs.org/ws/-/ws-7.4.6.tgz",
"integrity": "sha512-YmhHDO4MzaDLB+M9ym/mDA5z0naX8j7SIlT8f8z+I0VtzsRbekxEutHSme7NPS2qE8StCYQNUnfWdXta/Yu85A==",
"dev": true,
"requires": {}
},
"y18n": {
"version": "4.0.1",

View File

@ -16,17 +16,17 @@
},
"devDependencies": {
"chokidar-cli": "^2.1.0",
"esbuild": "0.7.8",
"esbuild": "0.12.5",
"livereload": "^0.9.3",
"npm-run-all": "^4.1.5",
"punycode": "^2.1.1",
"sass": "^1.32.8"
"sass": "^1.34.0"
},
"dependencies": {
"clipboard": "^2.0.8",
"codemirror": "^5.60.0",
"codemirror": "^5.61.1",
"dropzone": "^5.9.2",
"markdown-it": "^11.0.1",
"markdown-it": "^12.0.6",
"markdown-it-task-lists": "^2.1.1",
"sortablejs": "^1.13.0"
}

View File

@ -1,4 +1,3 @@
<svg viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
<path d="M0 0h24v24H0z" fill="none"/>
<path d="M11.99 2C6.47 2 2 6.48 2 12s4.47 10 9.99 10C17.52 22 22 17.52 22 12S17.52 2 11.99 2zm4.24 16L12 15.45 7.77 18l1.12-4.81-3.73-3.23 4.92-.42L12 5l1.92 4.53 4.92.42-3.73 3.23L16.23 18z"/>
</svg>

Before

Width:  |  Height:  |  Size: 307 B

After

Width:  |  Height:  |  Size: 265 B

View File

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path d="M22 9.24l-7.19-.62L12 2 9.19 8.63 2 9.24l5.46 4.73L5.82 21 12 17.27 18.18 21l-1.63-7.03L22 9.24zM12 15.4l-3.76 2.27 1-4.28-3.32-2.88 4.38-.38L12 6.1l1.71 4.04 4.38.38-3.32 2.88 1 4.28L12 15.4z"/></svg>

After

Width:  |  Height:  |  Size: 270 B

View File

@ -1,4 +1 @@
<svg viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
<path d="M0 0h24v24H0z" fill="none"/>
<path d="M17.63 5.84C17.27 5.33 16.67 5 16 5L5 5.01C3.9 5.01 3 5.9 3 7v10c0 1.1.9 1.99 2 1.99L16 19c.67 0 1.27-.33 1.63-.84L22 12l-4.37-6.16z"/>
</svg>
<svg xmlns="http://www.w3.org/2000/svg" enable-background="new 0 0 24 24" viewBox="0 0 24 24"><path d="M21.41,11.41l-8.83-8.83C12.21,2.21,11.7,2,11.17,2H4C2.9,2,2,2.9,2,4v7.17c0,0.53,0.21,1.04,0.59,1.41l8.83,8.83 c0.78,0.78,2.05,0.78,2.83,0l7.17-7.17C22.2,13.46,22.2,12.2,21.41,11.41z M6.5,8C5.67,8,5,7.33,5,6.5S5.67,5,6.5,5S8,5.67,8,6.5 S7.33,8,6.5,8z"/></svg>

Before

Width:  |  Height:  |  Size: 258 B

After

Width:  |  Height:  |  Size: 361 B

View File

@ -14,6 +14,7 @@ class MarkdownEditor {
this.pageId = this.$opts.pageId;
this.textDirection = this.$opts.textDirection;
this.imageUploadErrorText = this.$opts.imageUploadErrorText;
this.serverUploadLimitText = this.$opts.serverUploadLimitText;
this.markdown = new MarkdownIt({html: true});
this.markdown.use(mdTasksLists, {label: true});
@ -446,8 +447,7 @@ class MarkdownEditor {
this.insertDrawing(resp.data, cursorPos);
DrawIO.close();
}).catch(err => {
window.$events.emit('error', trans('errors.image_upload_error'));
console.log(err);
this.handleDrawingUploadError(err);
});
});
}
@ -491,10 +491,18 @@ class MarkdownEditor {
this.cm.focus();
DrawIO.close();
}).catch(err => {
this.handleDrawingUploadError(err);
});
});
}
handleDrawingUploadError(error) {
if (error.status === 413) {
window.$events.emit('error', this.serverUploadLimitText);
} else {
window.$events.emit('error', this.imageUploadErrorText);
console.log(err);
});
});
}
console.log(error);
}
// Make the editor full screen

View File

@ -283,6 +283,15 @@ function drawIoPlugin(drawioUrl, isDarkMode, pageId, wysiwygComponent) {
const id = "image-" + Math.random().toString(16).slice(2);
const loadingImage = window.baseUrl('/loading.gif');
const handleUploadError = (error) => {
if (error.status === 413) {
window.$events.emit('error', wysiwygComponent.serverUploadLimitText);
} else {
window.$events.emit('error', wysiwygComponent.imageUploadErrorText);
}
console.log(error);
};
// Handle updating an existing image
if (currentNode) {
DrawIO.close();
@ -292,8 +301,7 @@ function drawIoPlugin(drawioUrl, isDarkMode, pageId, wysiwygComponent) {
pageEditor.dom.setAttrib(imgElem, 'src', img.url);
pageEditor.dom.setAttrib(currentNode, 'drawio-diagram', img.id);
} catch (err) {
window.$events.emit('error', wysiwygComponent.imageUploadErrorText);
console.log(err);
handleUploadError(err);
}
return;
}
@ -307,8 +315,7 @@ function drawIoPlugin(drawioUrl, isDarkMode, pageId, wysiwygComponent) {
pageEditor.dom.get(id).parentNode.setAttribute('drawio-diagram', img.id);
} catch (err) {
pageEditor.dom.remove(id);
window.$events.emit('error', wysiwygComponent.imageUploadErrorText);
console.log(err);
handleUploadError(err);
}
}, 5);
}
@ -432,6 +439,7 @@ class WysiwygEditor {
this.pageId = this.$opts.pageId;
this.textDirection = this.$opts.textDirection;
this.imageUploadErrorText = this.$opts.imageUploadErrorText;
this.serverUploadLimitText = this.$opts.serverUploadLimitText;
this.isDarkMode = document.documentElement.classList.contains('dark-mode');
this.plugins = "image imagetools table textcolor paste link autolink fullscreen code customhr autosave lists codeeditor media";

View File

@ -1,5 +1,5 @@
let iFrame = null;
let lastApprovedOrigin;
let onInit, onSave;
/**
@ -19,15 +19,22 @@ function show(drawioUrl, onInitCallback, onSaveCallback) {
iFrame.setAttribute('class', 'fullscreen');
iFrame.style.backgroundColor = '#FFFFFF';
document.body.appendChild(iFrame);
lastApprovedOrigin = (new URL(drawioUrl)).origin;
}
function close() {
drawEventClose();
}
/**
* Receive and handle a message event from the draw.io window.
* @param {MessageEvent} event
*/
function drawReceive(event) {
if (!event.data || event.data.length < 1) return;
let message = JSON.parse(event.data);
if (event.origin !== lastApprovedOrigin) return;
const message = JSON.parse(event.data);
if (message.event === 'init') {
drawEventInit();
} else if (message.event === 'exit') {
@ -62,7 +69,7 @@ function drawEventClose() {
}
function drawPostMessage(data) {
iFrame.contentWindow.postMessage(JSON.stringify(data), '*');
iFrame.contentWindow.postMessage(JSON.stringify(data), lastApprovedOrigin);
}
async function upload(imageData, pageUploadedToId) {

View File

@ -43,6 +43,10 @@ return [
'bookshelf_delete' => 'تم تحديث الرف',
'bookshelf_delete_notification' => 'تم حذف الرف بنجاح',
// Favourites
'favourite_add_notification' => '":name" has been added to your favourites',
'favourite_remove_notification' => '":name" has been removed from your favourites',
// Other
'commented_on' => 'تم التعليق',
'permissions_update' => 'تحديث الأذونات',

View File

@ -40,6 +40,10 @@ return [
'remove' => 'إزالة',
'add' => 'إضافة',
'fullscreen' => 'شاشة كاملة',
'favourite' => 'Favourite',
'unfavourite' => 'Unfavourite',
'next' => 'Next',
'previous' => 'Previous',
// Sort Options
'sort_options' => 'خيارات الفرز',

View File

@ -27,6 +27,8 @@ return [
'images' => 'صور',
'my_recent_drafts' => 'مسوداتي الحديثة',
'my_recently_viewed' => 'ما عرضته مؤخراً',
'my_most_viewed_favourites' => 'My Most Viewed Favourites',
'my_favourites' => 'My Favourites',
'no_pages_viewed' => 'لم تستعرض أي صفحات',
'no_pages_recently_created' => 'لم تنشأ أي صفحات مؤخراً',
'no_pages_recently_updated' => 'لم تُحدّث أي صفحات مؤخراً',

View File

@ -43,6 +43,10 @@ return [
'bookshelf_delete' => 'изтрит рафт',
'bookshelf_delete_notification' => 'Рафтът беше успешно изтрит',
// Favourites
'favourite_add_notification' => '":name" has been added to your favourites',
'favourite_remove_notification' => '":name" has been removed from your favourites',
// Other
'commented_on' => 'коментирано на',
'permissions_update' => 'updated permissions',

View File

@ -40,6 +40,10 @@ return [
'remove' => 'Премахване',
'add' => 'Добави',
'fullscreen' => 'Пълен екран',
'favourite' => 'Favourite',
'unfavourite' => 'Unfavourite',
'next' => 'Next',
'previous' => 'Previous',
// Sort Options
'sort_options' => 'Опции за сортиране',

View File

@ -27,6 +27,8 @@ return [
'images' => 'Изображения',
'my_recent_drafts' => 'Моите скорошни драфтове',
'my_recently_viewed' => 'Моите скорошни преглеждания',
'my_most_viewed_favourites' => 'My Most Viewed Favourites',
'my_favourites' => 'My Favourites',
'no_pages_viewed' => 'Не сте прегледали никакви страници',
'no_pages_recently_created' => 'Не са били създавани страници скоро',
'no_pages_recently_updated' => 'Не са били актуализирани страници скоро',

View File

@ -43,6 +43,10 @@ return [
'bookshelf_delete' => 'je izbrisao/la policu za knjige',
'bookshelf_delete_notification' => 'Polica za knjige Uspješno Izbrisana',
// Favourites
'favourite_add_notification' => '":name" has been added to your favourites',
'favourite_remove_notification' => '":name" has been removed from your favourites',
// Other
'commented_on' => 'je komentarisao/la na',
'permissions_update' => 'je ažurirao/la dozvole',

View File

@ -40,6 +40,10 @@ return [
'remove' => 'Ukloni',
'add' => 'Dodaj',
'fullscreen' => 'Prikaz preko čitavog ekrana',
'favourite' => 'Favourite',
'unfavourite' => 'Unfavourite',
'next' => 'Next',
'previous' => 'Previous',
// Sort Options
'sort_options' => 'Opcije sortiranja',

View File

@ -27,6 +27,8 @@ return [
'images' => 'Slike',
'my_recent_drafts' => 'Moje nedavne skice',
'my_recently_viewed' => 'Moji nedavni pregledi',
'my_most_viewed_favourites' => 'My Most Viewed Favourites',
'my_favourites' => 'My Favourites',
'no_pages_viewed' => 'Niste pogledali nijednu stranicu',
'no_pages_recently_created' => 'Nijedna stranica nije napravljena nedavno',
'no_pages_recently_updated' => 'Niijedna stranica nije ažurirana nedavno',

View File

@ -43,6 +43,10 @@ return [
'bookshelf_delete' => 'ha suprimit un prestatge',
'bookshelf_delete_notification' => 'Prestatge suprimit correctament',
// Favourites
'favourite_add_notification' => '":name" has been added to your favourites',
'favourite_remove_notification' => '":name" has been removed from your favourites',
// Other
'commented_on' => 'ha comentat a',
'permissions_update' => 'ha actualitzat els permisos',

View File

@ -40,6 +40,10 @@ return [
'remove' => 'Elimina',
'add' => 'Afegeix',
'fullscreen' => 'Pantalla completa',
'favourite' => 'Favourite',
'unfavourite' => 'Unfavourite',
'next' => 'Next',
'previous' => 'Previous',
// Sort Options
'sort_options' => 'Opcions d\'ordenació',

View File

@ -27,6 +27,8 @@ return [
'images' => 'Imatges',
'my_recent_drafts' => 'Els vostres esborranys recents',
'my_recently_viewed' => 'Les vostres visualitzacions recents',
'my_most_viewed_favourites' => 'My Most Viewed Favourites',
'my_favourites' => 'My Favourites',
'no_pages_viewed' => 'No heu vist cap pàgina',
'no_pages_recently_created' => 'No s\'ha creat cap pàgina fa poc',
'no_pages_recently_updated' => 'No s\'ha actualitzat cap pàgina fa poc',

View File

@ -43,6 +43,10 @@ return [
'bookshelf_delete' => 'odstranil/a knihovnu',
'bookshelf_delete_notification' => 'Knihovna byla úspěšně odstraněna',
// Favourites
'favourite_add_notification' => '":name" has been added to your favourites',
'favourite_remove_notification' => '":name" has been removed from your favourites',
// Other
'commented_on' => 'okomentoval/a',
'permissions_update' => 'updated permissions',

View File

@ -40,6 +40,10 @@ return [
'remove' => 'Odebrat',
'add' => 'Přidat',
'fullscreen' => 'Celá obrazovka',
'favourite' => 'Favourite',
'unfavourite' => 'Unfavourite',
'next' => 'Next',
'previous' => 'Previous',
// Sort Options
'sort_options' => 'Možnosti řazení',

View File

@ -27,6 +27,8 @@ return [
'images' => 'Obrázky',
'my_recent_drafts' => 'Mé nedávné koncepty',
'my_recently_viewed' => 'Mé nedávno zobrazené',
'my_most_viewed_favourites' => 'My Most Viewed Favourites',
'my_favourites' => 'My Favourites',
'no_pages_viewed' => 'Nezobrazili jste žádné stránky',
'no_pages_recently_created' => 'Nedávno nebyly vytvořeny žádné stránky',
'no_pages_recently_updated' => 'Nedávno nebyly aktualizovány žádné stránky',

View File

@ -43,6 +43,10 @@ return [
'bookshelf_delete' => 'slettede bogreol',
'bookshelf_delete_notification' => 'Bogreolen blev opdateret',
// Favourites
'favourite_add_notification' => '":name" has been added to your favourites',
'favourite_remove_notification' => '":name" has been removed from your favourites',
// Other
'commented_on' => 'kommenterede til',
'permissions_update' => 'Tilladelser opdateret',

View File

@ -40,6 +40,10 @@ return [
'remove' => 'Fjern',
'add' => 'Tilføj',
'fullscreen' => 'Fuld skærm',
'favourite' => 'Favourite',
'unfavourite' => 'Unfavourite',
'next' => 'Next',
'previous' => 'Previous',
// Sort Options
'sort_options' => 'Sorteringsindstillinger',

View File

@ -27,6 +27,8 @@ return [
'images' => 'Billeder',
'my_recent_drafts' => 'Mine seneste kladder',
'my_recently_viewed' => 'Mine senest viste',
'my_most_viewed_favourites' => 'My Most Viewed Favourites',
'my_favourites' => 'My Favourites',
'no_pages_viewed' => 'Du har ikke besøgt nogle sider',
'no_pages_recently_created' => 'Ingen sider er blevet oprettet for nyligt',
'no_pages_recently_updated' => 'Ingen sider er blevet opdateret for nyligt',

View File

@ -43,6 +43,10 @@ return [
'bookshelf_delete' => 'hat das Bücherregal gelöscht',
'bookshelf_delete_notification' => 'Das Bücherregal wurde erfolgreich gelöscht',
// Favourites
'favourite_add_notification' => '":name" wurde zu deinen Favoriten hinzugefügt',
'favourite_remove_notification' => '":name" wurde aus Ihren Favoriten entfernt',
// Other
'commented_on' => 'hat einen Kommentar hinzugefügt',
'permissions_update' => 'hat die Berechtigungen aktualisiert',

View File

@ -40,6 +40,10 @@ return [
'remove' => 'Entfernen',
'add' => 'Hinzufügen',
'fullscreen' => 'Vollbild',
'favourite' => 'Favorit',
'unfavourite' => 'Kein Favorit',
'next' => 'Next',
'previous' => 'Previous',
// Sort Options
'sort_options' => 'Sortieroptionen',

View File

@ -27,6 +27,8 @@ return [
'images' => 'Bilder',
'my_recent_drafts' => 'Meine kürzlichen Entwürfe',
'my_recently_viewed' => 'Kürzlich von mir angesehen',
'my_most_viewed_favourites' => 'Meine meistgesehenen Favoriten',
'my_favourites' => 'Meine Favoriten',
'no_pages_viewed' => 'Sie haben bisher keine Seiten angesehen',
'no_pages_recently_created' => 'Sie haben bisher keine Seiten angelegt',
'no_pages_recently_updated' => 'Sie haben bisher keine Seiten aktualisiert',

View File

@ -83,9 +83,9 @@ return [
'404_page_not_found' => 'Seite nicht gefunden',
'sorry_page_not_found' => 'Entschuldigung. Die Seite, die Sie angefordert haben, wurde nicht gefunden.',
'sorry_page_not_found_permission_warning' => 'Wenn Sie erwartet haben, dass diese Seite existiert, haben Sie möglicherweise nicht die Berechtigung, sie anzuzeigen.',
'image_not_found' => 'Image Not Found',
'image_not_found_subtitle' => 'Sorry, The image file you were looking for could not be found.',
'image_not_found_details' => 'If you expected this image to exist it might have been deleted.',
'image_not_found' => 'Bild nicht gefunden',
'image_not_found_subtitle' => 'Entschuldigung. Das Bild, die Sie angefordert haben, wurde nicht gefunden.',
'image_not_found_details' => 'Wenn Sie erwartet haben, dass dieses Bild existiert, könnte es gelöscht worden sein.',
'return_home' => 'Zurück zur Startseite',
'error_occurred' => 'Es ist ein Fehler aufgetreten',
'app_down' => ':appName befindet sich aktuell im Wartungsmodus.',

View File

@ -43,6 +43,10 @@ return [
'bookshelf_delete' => 'löscht Bücherregal',
'bookshelf_delete_notification' => 'Das Bücherregal wurde erfolgreich gelöscht',
// Favourites
'favourite_add_notification' => '":name" has been added to your favourites',
'favourite_remove_notification' => '":name" has been removed from your favourites',
// Other
'commented_on' => 'kommentiert',
'permissions_update' => 'aktualisierte Berechtigungen',

View File

@ -40,6 +40,10 @@ return [
'remove' => 'Entfernen',
'add' => 'Hinzufügen',
'fullscreen' => 'Vollbild',
'favourite' => 'Favorit',
'unfavourite' => 'Kein Favorit',
'next' => 'Next',
'previous' => 'Previous',
// Sort Options
'sort_options' => 'Sortieroptionen',

View File

@ -27,6 +27,8 @@ return [
'images' => 'Bilder',
'my_recent_drafts' => 'Meine kürzlichen Entwürfe',
'my_recently_viewed' => 'Kürzlich von mir angesehen',
'my_most_viewed_favourites' => 'My Most Viewed Favourites',
'my_favourites' => 'My Favourites',
'no_pages_viewed' => 'Du hast bisher keine Seiten angesehen.',
'no_pages_recently_created' => 'Du hast bisher keine Seiten angelegt.',
'no_pages_recently_updated' => 'Du hast bisher keine Seiten aktualisiert.',

View File

@ -43,6 +43,10 @@ return [
'bookshelf_delete' => 'deleted bookshelf',
'bookshelf_delete_notification' => 'Bookshelf Successfully Deleted',
// Favourites
'favourite_add_notification' => '":name" has been added to your favourites',
'favourite_remove_notification' => '":name" has been removed from your favourites',
// Other
'commented_on' => 'commented on',
'permissions_update' => 'updated permissions',

View File

@ -40,6 +40,10 @@ return [
'remove' => 'Remove',
'add' => 'Add',
'fullscreen' => 'Fullscreen',
'favourite' => 'Favourite',
'unfavourite' => 'Unfavourite',
'next' => 'Next',
'previous' => 'Previous',
// Sort Options
'sort_options' => 'Sort Options',

View File

@ -27,6 +27,8 @@ return [
'images' => 'Images',
'my_recent_drafts' => 'My Recent Drafts',
'my_recently_viewed' => 'My Recently Viewed',
'my_most_viewed_favourites' => 'My Most Viewed Favourites',
'my_favourites' => 'My Favourites',
'no_pages_viewed' => 'You have not viewed any pages',
'no_pages_recently_created' => 'No pages have been recently created',
'no_pages_recently_updated' => 'No pages have been recently updated',

View File

@ -43,6 +43,10 @@ return [
'bookshelf_delete' => 'estante eliminado',
'bookshelf_delete_notification' => 'Estante eliminado correctamente',
// Favourites
'favourite_add_notification' => '".name" ha sido añadido a sus favoritos',
'favourite_remove_notification' => '".name" ha sido eliminado de sus favoritos',
// Other
'commented_on' => 'comentada el',
'permissions_update' => 'permisos actualizados',

View File

@ -40,6 +40,10 @@ return [
'remove' => 'Remover',
'add' => 'Añadir',
'fullscreen' => 'Pantalla completa',
'favourite' => 'Añadir a favoritos',
'unfavourite' => 'Eliminar de favoritos',
'next' => 'Siguiente',
'previous' => 'Anterior',
// Sort Options
'sort_options' => 'Opciones de ordenación',

View File

@ -27,6 +27,8 @@ return [
'images' => 'Imágenes',
'my_recent_drafts' => 'Mis borradores recientes',
'my_recently_viewed' => 'Mis visualizaciones recientes',
'my_most_viewed_favourites' => 'Mis favoritos más vistos',
'my_favourites' => 'Mis favoritos',
'no_pages_viewed' => 'No ha visto ninguna página',
'no_pages_recently_created' => 'Ninguna página ha sido creada recientemente',
'no_pages_recently_updated' => 'Ninguna página ha sido actualizada recientemente',

View File

@ -83,9 +83,9 @@ return [
'404_page_not_found' => 'Página no encontrada',
'sorry_page_not_found' => 'Lo sentimos, la página a la que intenta acceder no pudo ser encontrada.',
'sorry_page_not_found_permission_warning' => 'Si esperaba que esta página existiera, puede que no tenga permiso para verla.',
'image_not_found' => 'Image Not Found',
'image_not_found_subtitle' => 'Sorry, The image file you were looking for could not be found.',
'image_not_found_details' => 'If you expected this image to exist it might have been deleted.',
'image_not_found' => 'Imagen no encontrada',
'image_not_found_subtitle' => 'Lo sentimos, no se pudo encontrar el archivo de imagen que estaba buscando.',
'image_not_found_details' => 'Si esperaba que esta imagen existiera, podría haber sido eliminada.',
'return_home' => 'Volver a la página de inicio',
'error_occurred' => 'Ha ocurrido un error',
'app_down' => 'La aplicación :appName se encuentra caída en este momento',

View File

@ -43,6 +43,10 @@ return [
'bookshelf_delete' => 'Estante borrado',
'bookshelf_delete_notification' => 'Estante borrado exitosamente',
// Favourites
'favourite_add_notification' => '":name" has been added to your favourites',
'favourite_remove_notification' => '":name" has been removed from your favourites',
// Other
'commented_on' => 'comentado',
'permissions_update' => 'permisos actualizados',

View File

@ -40,6 +40,10 @@ return [
'remove' => 'Remover',
'add' => 'Agregar',
'fullscreen' => 'Pantalla completa',
'favourite' => 'Añadir a favoritos',
'unfavourite' => 'Eliminar de favoritos',
'next' => 'Next',
'previous' => 'Previous',
// Sort Options
'sort_options' => 'Opciones de Orden',

View File

@ -27,6 +27,8 @@ return [
'images' => 'Imágenes',
'my_recent_drafts' => 'Mis borradores recientes',
'my_recently_viewed' => 'Mis visualizaciones recientes',
'my_most_viewed_favourites' => 'My Most Viewed Favourites',
'my_favourites' => 'My Favourites',
'no_pages_viewed' => 'Ud. no ha visto ninguna página',
'no_pages_recently_created' => 'Ninguna página ha sido creada recientemente',
'no_pages_recently_updated' => 'Ninguna página ha sido actualizada recientemente',

View File

@ -43,6 +43,10 @@ return [
'bookshelf_delete' => 'deleted bookshelf',
'bookshelf_delete_notification' => 'Bookshelf Successfully Deleted',
// Favourites
'favourite_add_notification' => '":name" has been added to your favourites',
'favourite_remove_notification' => '":name" has been removed from your favourites',
// Other
'commented_on' => 'commented on',
'permissions_update' => 'updated permissions',

View File

@ -40,6 +40,10 @@ return [
'remove' => 'Remove',
'add' => 'Add',
'fullscreen' => 'Fullscreen',
'favourite' => 'Favourite',
'unfavourite' => 'Unfavourite',
'next' => 'Next',
'previous' => 'Previous',
// Sort Options
'sort_options' => 'Sort Options',

View File

@ -27,6 +27,8 @@ return [
'images' => 'Images',
'my_recent_drafts' => 'My Recent Drafts',
'my_recently_viewed' => 'My Recently Viewed',
'my_most_viewed_favourites' => 'My Most Viewed Favourites',
'my_favourites' => 'My Favourites',
'no_pages_viewed' => 'You have not viewed any pages',
'no_pages_recently_created' => 'No pages have been recently created',
'no_pages_recently_updated' => 'No pages have been recently updated',

View File

@ -43,6 +43,10 @@ return [
'bookshelf_delete' => 'a supprimé l\'étagère',
'bookshelf_delete_notification' => 'Étagère supprimée avec succès',
// Favourites
'favourite_add_notification' => '":name" a été ajouté dans vos favoris',
'favourite_remove_notification' => '":name" a été supprimé de vos favoris',
// Other
'commented_on' => 'a commenté',
'permissions_update' => 'mettre à jour les autorisations',

View File

@ -40,6 +40,10 @@ return [
'remove' => 'Enlever',
'add' => 'Ajouter',
'fullscreen' => 'Plein écran',
'favourite' => 'Favoris',
'unfavourite' => 'Supprimer des favoris',
'next' => 'Next',
'previous' => 'Previous',
// Sort Options
'sort_options' => 'Options de tri',

View File

@ -27,6 +27,8 @@ return [
'images' => 'Images',
'my_recent_drafts' => 'Mes brouillons récents',
'my_recently_viewed' => 'Vus récemment',
'my_most_viewed_favourites' => 'Mes Favoris les plus vus',
'my_favourites' => 'Mes favoris',
'no_pages_viewed' => 'Vous n\'avez rien visité récemment',
'no_pages_recently_created' => 'Aucune page créée récemment',
'no_pages_recently_updated' => 'Aucune page mise à jour récemment',

View File

@ -43,6 +43,10 @@ return [
'bookshelf_delete' => 'deleted bookshelf',
'bookshelf_delete_notification' => 'מדף הספרים הוסר בהצלחה',
// Favourites
'favourite_add_notification' => '":name" has been added to your favourites',
'favourite_remove_notification' => '":name" has been removed from your favourites',
// Other
'commented_on' => 'commented on',
'permissions_update' => 'updated permissions',

View File

@ -40,6 +40,10 @@ return [
'remove' => 'הסר',
'add' => 'הוסף',
'fullscreen' => 'Fullscreen',
'favourite' => 'Favourite',
'unfavourite' => 'Unfavourite',
'next' => 'Next',
'previous' => 'Previous',
// Sort Options
'sort_options' => 'Sort Options',

View File

@ -27,6 +27,8 @@ return [
'images' => 'תמונות',
'my_recent_drafts' => 'הטיוטות האחרונות שלי',
'my_recently_viewed' => 'הנצפים לאחרונה שלי',
'my_most_viewed_favourites' => 'My Most Viewed Favourites',
'my_favourites' => 'My Favourites',
'no_pages_viewed' => 'לא צפית בדפים כלשהם',
'no_pages_recently_created' => 'לא נוצרו דפים לאחרונה',
'no_pages_recently_updated' => 'לא עודכנו דפים לאחרונה',

View File

@ -43,6 +43,10 @@ return [
'bookshelf_delete' => 'törölte a könyvespolcot:',
'bookshelf_delete_notification' => 'Könyvespolc sikeresen törölve',
// Favourites
'favourite_add_notification' => '":name" has been added to your favourites',
'favourite_remove_notification' => '":name" has been removed from your favourites',
// Other
'commented_on' => 'megjegyzést fűzött hozzá:',
'permissions_update' => 'updated permissions',

View File

@ -40,6 +40,10 @@ return [
'remove' => 'Eltávolítás',
'add' => 'Hozzáadás',
'fullscreen' => 'Teljes képernyő',
'favourite' => 'Favourite',
'unfavourite' => 'Unfavourite',
'next' => 'Next',
'previous' => 'Previous',
// Sort Options
'sort_options' => 'Rendezési beállítások',

View File

@ -27,6 +27,8 @@ return [
'images' => 'Képek',
'my_recent_drafts' => 'Legutóbbi vázlataim',
'my_recently_viewed' => 'Általam legutóbb megtekintett',
'my_most_viewed_favourites' => 'My Most Viewed Favourites',
'my_favourites' => 'My Favourites',
'no_pages_viewed' => 'Még nincsenek általam megtekintett oldalak',
'no_pages_recently_created' => 'Nincsenek legutóbb létrehozott oldalak',
'no_pages_recently_updated' => 'Nincsenek legutóbb frissített oldalak',

View File

@ -43,6 +43,10 @@ return [
'bookshelf_delete' => 'hapus rak buku',
'bookshelf_delete_notification' => 'Rak berhasil dihapus',
// Favourites
'favourite_add_notification' => '":name" has been added to your favourites',
'favourite_remove_notification' => '":name" has been removed from your favourites',
// Other
'commented_on' => 'berkomentar pada',
'permissions_update' => 'perbaharui izin',

View File

@ -40,6 +40,10 @@ return [
'remove' => 'Hapus',
'add' => 'Tambah',
'fullscreen' => 'Layar Penuh',
'favourite' => 'Favourite',
'unfavourite' => 'Unfavourite',
'next' => 'Next',
'previous' => 'Previous',
// Sort Options
'sort_options' => 'Sortir Pilihan',

View File

@ -27,6 +27,8 @@ return [
'images' => 'Gambar-gambar',
'my_recent_drafts' => 'Draf Terbaru Saya',
'my_recently_viewed' => 'Baru saja saya lihat',
'my_most_viewed_favourites' => 'My Most Viewed Favourites',
'my_favourites' => 'My Favourites',
'no_pages_viewed' => 'Anda belum melihat halaman apa pun',
'no_pages_recently_created' => 'Tidak ada halaman yang baru saja dibuat',
'no_pages_recently_updated' => 'Tidak ada halaman yang baru-baru ini diperbarui',

View File

@ -43,6 +43,10 @@ return [
'bookshelf_delete' => 'ha eliminato la libreria',
'bookshelf_delete_notification' => 'Libreria Eliminata Correttamente',
// Favourites
'favourite_add_notification' => '":name" has been added to your favourites',
'favourite_remove_notification' => '":name" has been removed from your favourites',
// Other
'commented_on' => 'ha commentato in',
'permissions_update' => 'autorizzazioni aggiornate',

View File

@ -40,6 +40,10 @@ return [
'remove' => 'Rimuovi',
'add' => 'Aggiungi',
'fullscreen' => 'Schermo intero',
'favourite' => 'Favourite',
'unfavourite' => 'Unfavourite',
'next' => 'Next',
'previous' => 'Previous',
// Sort Options
'sort_options' => 'Opzioni Ordinamento',

View File

@ -27,6 +27,8 @@ return [
'images' => 'Immagini',
'my_recent_drafts' => 'Bozze Recenti',
'my_recently_viewed' => 'Visti di recente',
'my_most_viewed_favourites' => 'My Most Viewed Favourites',
'my_favourites' => 'My Favourites',
'no_pages_viewed' => 'Non hai visto nessuna pagina',
'no_pages_recently_created' => 'Nessuna pagina è stata creata di recente',
'no_pages_recently_updated' => 'Nessuna pagina è stata aggiornata di recente',

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