Compare commits

..

44 Commits

Author SHA1 Message Date
Dan Brown
d7d8fa1e5b Updated version and assets for release v21.05 2021-05-30 16:17:56 +01:00
Dan Brown
18562f1e10 Merge branch 'master' into release 2021-05-30 16:17:44 +01:00
Dan Brown
fdabafffda Added thumbnail attribute to complete .env 2021-05-30 15:22:58 +01:00
Dan Brown
e2df15fe20 Updated translators before next release 2021-05-30 15:11:59 +01:00
Dan Brown
54bac17ef0 New Crowdin updates (#2764) 2021-05-30 15:10:11 +01:00
Dan Brown
7634ac4e12 Updated test to align with export date format change 2021-05-30 13:23:51 +01:00
Dan Brown
c4f5ab12cf Aligned export and revision shown date format
As raised in #2771
2021-05-30 00:02:32 +01:00
Dan Brown
57a063cdfb Updated nav tests to look for shortened item names 2021-05-29 23:46:33 +01:00
Dan Brown
1fa90e4f12 Converted another couple of tests from browserkit 2021-05-29 23:42:21 +01:00
Dan Brown
d62cdd58d3 Upgraded php and npm deps
- Sass upgrade had some breaking changes where division was used
hence updated for newer sass version support.
2021-05-29 13:08:28 +01:00
Dan Brown
ed6ec341df Added testing to cover next/previous navigation
For #2511
2021-05-29 12:49:10 +01:00
Dan Brown
0cfff6ab6f Reviewed and refactored next/previous navigation button implementation
- Updated styling to include item name.
- Extracted used text to translations.
- Updated the design to better suit the surrounding blocks.
- Removed newly added model/repo methods.
- Moved core logic out of controller and instead into a "NextPreviousContentLocator"
helper with re-uses the output from the book-tree generation.
- Also added the system to chapters.

For #2511
2021-05-29 12:39:41 +01:00
Dan Brown
7ca66c5d5e Merge branch 'prev-next-button' of https://github.com/shubhamosmosys/BookStack into shubhamosmosys-prev-next-button 2021-05-26 22:13:19 +01:00
Dan Brown
9cbea1eb08 Updated drawing upload error to shown/handle server limit errors
Closes #2740
2021-05-26 18:23:27 +01:00
Dan Brown
1a2d374f24 Revert "Added app logo to outgoing emails"
This reverts commit e32929029b.
2021-05-26 17:13:59 +01:00
Dan Brown
e32929029b Added app logo to outgoing emails
Required changing the header bar of the email to be solid color to match
the configuration of the main app header since otherwise colors may not
work together.

Closes #2577
2021-05-26 17:11:03 +01:00
Dan Brown
eb76e882c5 Added deletion of revisions on page delete
Added testing to cover.
Closes #2668
2021-05-26 16:40:56 +01:00
Dan Brown
d326417edc Added name input autofocus on shelves, books and chapters
Closes #1956
2021-05-26 15:25:23 +01:00
Dan Brown
a3a8fef6b2 Made users header interface more adaptable
Search input was stacking on create button on default desktop view
due when viewing in russian due to combined width exceeding container.
Made into normal flexbox instead.

Closes #2147
2021-05-26 15:20:35 +01:00
Dan Brown
0c16334426 Merge branch 'master' of github.com:BookStackApp/BookStack 2021-05-25 00:06:13 +01:00
Dan Brown
600f8cd142 Added origin verification to postMessage usage.
Closes #2769
2021-05-25 00:05:20 +01:00
Dan Brown
5c8c85a0ff Merge pull request #2768 from CorruptComputer/RSPEC-5148-Fixes
[sec] Fixes a few minor vulnerabilies when using target="_blank" on links (RSPEC-5148)
2021-05-24 22:11:49 +01:00
Nickolas Gupton
7a6f21648a Fixes minor vulnerability when using target="_blank" on links (RSPEC-5148) 2021-05-24 16:17:08 -04:00
Dan Brown
df0e03cd07 Reviewed PR to add import user avatars va LDAP
- Reduced options to single new configuration paramter instead of two.
- Moved more logic into UserAvatars class.
- Updated LDAP avatar import to also run on login when no image is
  currently set.
- Added thumbnail fetching to search requests.
- Added testing to cover.

Related to PR #2320, and issue #1161
2021-05-24 18:54:08 +01:00
Dan Brown
85db812fea Merge branch 'master' of https://github.com/jasonhoule/BookStack into jasonhoule-master 2021-05-24 17:06:50 +01:00
Dan Brown
fb5b5e138d Updated existing tag tests away from browserkit testing 2021-05-24 16:16:58 +01:00
Dan Brown
3eaf03a7ac Reviewed tag in seach work
- Refactored some tag code bits while reviewing.
- Updated tag design in search listing to be more subtle.
- Moved tags out of entity-list-item-basic template and instead moved
  them into entity-list-item, below the existing content.
- Tweaked existing tag colors a little.
- Changed tag icon to be more tag-like.
- Added tag-on-search test case.

Review of #2487, Related to #2462
2021-05-24 16:12:09 +01:00
Dan Brown
5420f3451c Merge branch 'show-tags' of https://github.com/burnoutberni/BookStack into burnoutberni-show-tags 2021-05-24 15:12:45 +01:00
Dan Brown
7d94da10fb Merge branch 'v21.04.x' 2021-05-24 13:08:51 +01:00
Dan Brown
dd6076049c Merge pull request #2748 from BookStackApp/favourite_system
Favourite System
2021-05-23 14:45:42 +01:00
Dan Brown
ba8ba5c634 Added testing to favourite system
- Also removed some old view service references.
- Updated TopFavourites query to be based on favourites table and join
  in the views instead of the other way around, so that favourites still
show even if they have no views.
2021-05-23 14:34:36 +01:00
Dan Brown
c2069f37cc Added deletion of favourites on entity/user delete 2021-05-23 13:41:56 +01:00
Dan Brown
1e0aa7ee2c Added favourites page with link from header and home 2021-05-23 13:34:08 +01:00
Dan Brown
27942f5ce8 Deleted redundant complex relationmultimodel query class 2021-05-22 14:07:57 +01:00
Dan Brown
d0ff79ea60 Revamped some complex queries, added favourites to home
- Removed old view system and started use of new query classes instead.
- Finished off RelationMultiModelQuery but found it was less efficient
than x-many queries due to the amount of tables being scanned.
Adding now for history but will delete as not used.
- Updated recently viewed to use same query system as popular items
  rather than running and joining x-entities queries.
- Added "Most Viewed Faviourites" listing to homepages.
2021-05-22 14:05:28 +01:00
Dan Brown
3de02566bf Started building system for cross-model queries 2021-05-19 23:37:23 +01:00
Dan Brown
93fd869ba3 Started refactoring of view service
Phasing out the view service from being a generic 'service' class,
moving the core create/delete methods into the model.
The idea is that the existing query work will need to interlink
with the favourite system so maybe we have a (or many composable)
query building classes rather than mixing query building and
create/delete work as per the old service.
2021-05-16 10:49:37 +01:00
Dan Brown
3ca149137e Added faviourtes to other entity types 2021-05-16 10:26:28 +01:00
Dan Brown
db9aa41096 Started writing testing for favourites 2021-05-16 01:07:20 +01:00
Dan Brown
bf8e7f3393 Started addition of favourite system 2021-05-16 00:29:56 +01:00
Shubham Tiwari
99c42033b1 Add prev and next button to navigate through different pages 2021-01-27 10:15:28 +05:30
Bernhard Hayden
aad2ee675c Show tags of all search results 2021-01-15 15:52:03 +01:00
Jason Houle
a192b600fc Missed a variable when updating LdapService. 2020-10-12 12:47:36 -04:00
Jason Houle
b714652e10 Import thumbnail photos when LDAP users are created. 2020-10-12 12:33:55 -04:00
206 changed files with 2013 additions and 879 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(),
]);
}
@@ -242,8 +247,8 @@ class PageController extends Controller
$updateTime = $draft->updated_at->timestamp;
return response()->json([
'status' => 'success',
'message' => trans('entities.pages_edit_draft_save_at'),
'status' => 'success',
'message' => trans('entities.pages_edit_draft_save_at'),
'timestamp' => $updateTime
]);
}
@@ -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
]);
}
@@ -377,7 +382,7 @@ class PageController extends Controller
try {
$parent = $this->pageRepo->move($page, $entitySelection);
} catch (Exception $exception) {
if ($exception instanceof PermissionsException) {
if ($exception instanceof PermissionsException) {
$this->showPermissionError();
}
@@ -421,7 +426,7 @@ class PageController extends Controller
try {
$pageCopy = $this->pageRepo->copy($page, $entitySelection, $newName);
} catch (Exception $exception) {
if ($exception instanceof PermissionsException) {
if ($exception instanceof PermissionsException) {
$this->showPermissionError();
}
@@ -442,7 +447,7 @@ class PageController extends Controller
$page = $this->pageRepo->getBySlug($bookSlug, $pageSlug);
$this->checkOwnablePermission('restrictions-manage', $page);
return view('pages.permissions', [
'page' => $page,
'page' => $page,
]);
}

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"
}

97
public/dist/app.js vendored

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

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,12 +491,20 @@ class MarkdownEditor {
this.cm.focus();
DrawIO.close();
}).catch(err => {
window.$events.emit('error', this.imageUploadErrorText);
console.log(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(error);
}
// Make the editor full screen
actionFullScreen() {
const alreadyFullscreen = this.elem.classList.contains('fullscreen');

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',

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