Also refactored tab styles which affected the settings area.
Closes #41
-<?php
-
-namespace BookStack\Http\Controllers;
+<?php namespace BookStack\Http\Controllers;
use BookStack\Exceptions\ImageUploadException;
use BookStack\Repos\ImageRepo;
use Illuminate\Filesystem\Filesystem as File;
use Illuminate\Http\Request;
-use Illuminate\Support\Facades\Auth;
-use Intervention\Image\Facades\Image as ImageTool;
-use Illuminate\Support\Facades\DB;
use BookStack\Image;
use BookStack\Repos\PageRepo;
return response()->json($imgData);
}
+ /**
+ * Search through images within a particular type.
+ * @param $type
+ * @param int $page
+ * @param Request $request
+ * @return mixed
+ */
+ public function searchByType($type, $page = 0, Request $request)
+ {
+ $this->validate($request, [
+ 'term' => 'required|string'
+ ]);
+
+ $searchTerm = $request->get('term');
+ $imgData = $this->imageRepo->searchPaginatedByType($type, $page,24, $searchTerm);
+ return response()->json($imgData);
+ }
+
/**
* Get all images for a user.
* @param int $page
return response()->json($imgData);
}
+ /**
+ * Get gallery images with a specific filter such as book or page
+ * @param $filter
+ * @param int $page
+ * @param Request $request
+ */
+ public function getGalleryFiltered($filter, $page = 0, Request $request)
+ {
+ $this->validate($request, [
+ 'page_id' => 'required|integer'
+ ]);
+
+ $validFilters = collect(['page', 'book']);
+ if (!$validFilters->contains($filter)) return response('Invalid filter', 500);
+
+ $pageId = $request->get('page_id');
+ $imgData = $this->imageRepo->getGalleryFiltered($page, 24, strtolower($filter), $pageId);
+
+ return response()->json($imgData);
+ }
+
/**
* Handles image uploads for use on pages.
* @param string $type
Route::post('/{type}/upload', 'ImageController@uploadByType');
Route::get('/{type}/all', 'ImageController@getAllByType');
Route::get('/{type}/all/{page}', 'ImageController@getAllByType');
+ Route::get('/{type}/search/{page}', 'ImageController@searchByType');
+ Route::get('/gallery/{filter}/{page}', 'ImageController@getGalleryFiltered');
Route::delete('/{imageId}', 'ImageController@destroy');
});
use BookStack\Image;
+use BookStack\Page;
use BookStack\Services\ImageService;
use BookStack\Services\RestrictionService;
use Setting;
protected $image;
protected $imageService;
protected $restictionService;
+ protected $page;
/**
* ImageRepo constructor.
* @param Image $image
* @param ImageService $imageService
* @param RestrictionService $restrictionService
+ * @param Page $page
*/
- public function __construct(Image $image, ImageService $imageService, RestrictionService $restrictionService)
+ public function __construct(Image $image, ImageService $imageService, RestrictionService $restrictionService, Page $page)
{
$this->image = $image;
$this->imageService = $imageService;
$this->restictionService = $restrictionService;
+ $this->page = $page;
}
}
/**
- * Gets a load images paginated, filtered by image type.
- * @param string $type
+ * Execute a paginated query, returning in a standard format.
+ * Also runs the query through the restriction system.
+ * @param $query
* @param int $page
* @param int $pageSize
- * @param bool|int $userFilter
* @return array
*/
- public function getPaginatedByType($type, $page = 0, $pageSize = 24, $userFilter = false)
+ private function returnPaginated($query, $page = 0, $pageSize = 24)
{
- $images = $this->image->where('type', '=', strtolower($type));
-
- if ($userFilter !== false) {
- $images = $images->where('created_by', '=', $userFilter);
- }
-
- $images = $this->restictionService->filterRelatedPages($images, 'images', 'uploaded_to');
+ $images = $this->restictionService->filterRelatedPages($query, 'images', 'uploaded_to');
$images = $images->orderBy('created_at', 'desc')->skip($pageSize * $page)->take($pageSize + 1)->get();
$hasMore = count($images) > $pageSize;
];
}
+ /**
+ * Gets a load images paginated, filtered by image type.
+ * @param string $type
+ * @param int $page
+ * @param int $pageSize
+ * @param bool|int $userFilter
+ * @return array
+ */
+ public function getPaginatedByType($type, $page = 0, $pageSize = 24, $userFilter = false)
+ {
+ $images = $this->image->where('type', '=', strtolower($type));
+
+ if ($userFilter !== false) {
+ $images = $images->where('created_by', '=', $userFilter);
+ }
+
+ return $this->returnPaginated($images, $page, $pageSize);
+ }
+
+ /**
+ * Search for images by query, of a particular type.
+ * @param string $type
+ * @param int $page
+ * @param int $pageSize
+ * @param string $searchTerm
+ * @return array
+ */
+ public function searchPaginatedByType($type, $page = 0, $pageSize = 24, $searchTerm)
+ {
+ $images = $this->image->where('type', '=', strtolower($type))->where('name', 'LIKE', '%' . $searchTerm . '%');
+ return $this->returnPaginated($images, $page, $pageSize);
+ }
+
+ /**
+ * Get gallery images with a particular filter criteria such as
+ * being within the current book or page.
+ * @param int $pagination
+ * @param int $pageSize
+ * @param $filter
+ * @param $pageId
+ * @return array
+ */
+ public function getGalleryFiltered($pagination = 0, $pageSize = 24, $filter, $pageId)
+ {
+ $images = $this->image->where('type', '=', 'gallery');
+
+ $page = $this->page->findOrFail($pageId);
+
+ if ($filter === 'page') {
+ $images = $images->where('uploaded_to', '=', $page->id);
+ } elseif ($filter === 'book') {
+ $validPageIds = $page->book->pages->pluck('id')->toArray();
+ $images = $images->whereIn('uploaded_to', $validPageIds);
+ }
+
+ return $this->returnPaginated($images, $pagination, $pageSize);
+ }
+
/**
* Save a new image into storage and return the new image.
* @param UploadedFile $uploadFile
$scope.imageUpdateSuccess = false;
$scope.imageDeleteSuccess = false;
$scope.uploadedTo = $attrs.uploadedTo;
+ $scope.view = 'all';
+
+ $scope.searching = false;
+ $scope.searchTerm = '';
var page = 0;
var previousClickTime = 0;
+ var previousClickImage = 0;
var dataLoaded = false;
var callback = false;
+ var preSearchImages = [];
+ var preSearchHasMore = false;
+
/**
- * Simple returns the appropriate upload url depending on the image type set.
+ * Used by dropzone to get the endpoint to upload to.
* @returns {string}
*/
$scope.getUploadUrl = function () {
return '/images/' + $scope.imageType + '/upload';
};
+ /**
+ * Cancel the current search operation.
+ */
+ function cancelSearch() {
+ $scope.searching = false;
+ $scope.searchTerm = '';
+ $scope.images = preSearchImages;
+ $scope.hasMore = preSearchHasMore;
+ }
+ $scope.cancelSearch = cancelSearch;
+
+
/**
* Runs on image upload, Adds an image to local list of images
* and shows a success message to the user.
var currentTime = Date.now();
var timeDiff = currentTime - previousClickTime;
- if (timeDiff < dblClickTime) {
+ if (timeDiff < dblClickTime && image.id === previousClickImage) {
// If double click
callbackAndHide(image);
} else {
$scope.dependantPages = false;
}
previousClickTime = currentTime;
+ previousClickImage = image.id;
};
/**
$scope.showing = false;
};
+ var baseUrl = '/images/' + $scope.imageType + '/all/'
+
/**
* Fetch the list image data from the server.
*/
function fetchData() {
- var url = '/images/' + $scope.imageType + '/all/' + page;
+ var url = baseUrl + page + '?';
+ var components = {};
+ if ($scope.uploadedTo) components['page_id'] = $scope.uploadedTo;
+ if ($scope.searching) components['term'] = $scope.searchTerm;
+
+
+ var urlQueryString = Object.keys(components).map((key) => {
+ return key + '=' + encodeURIComponent(components[key]);
+ }).join('&');
+ url += urlQueryString;
+
$http.get(url).then((response) => {
$scope.images = $scope.images.concat(response.data.images);
$scope.hasMore = response.data.hasMore;
page++;
});
}
-
$scope.fetchData = fetchData;
+ /**
+ * Start a search operation
+ * @param searchTerm
+ */
+ $scope.searchImages = function() {
+
+ if ($scope.searchTerm === '') {
+ cancelSearch();
+ return;
+ }
+
+ if (!$scope.searching) {
+ preSearchImages = $scope.images;
+ preSearchHasMore = $scope.hasMore;
+ }
+
+ $scope.searching = true;
+ $scope.images = [];
+ $scope.hasMore = false;
+ page = 0;
+ baseUrl = '/images/' + $scope.imageType + '/search/';
+ fetchData();
+ };
+
+ /**
+ * Set the current image listing view.
+ * @param viewName
+ */
+ $scope.setView = function(viewName) {
+ cancelSearch();
+ $scope.images = [];
+ $scope.hasMore = false;
+ page = 0;
+ $scope.view = viewName;
+ baseUrl = '/images/' + $scope.imageType + '/' + viewName + '/';
+ fetchData();
+ }
+
/**
* Save the details of an image.
* @param event
}
}
-.setting-nav {
+.nav-tabs {
text-align: center;
- a {
+ a, .tab-item {
padding: $-m;
display: inline-block;
color: #666;
+ cursor: pointer;
&.selected {
border-bottom: 2px solid $primary;
}
.image-manager-list {
overflow-y: scroll;
flex: 1;
- border-top: 1px solid #ddd;
}
.image-manager-content {
flex-direction: column;
height: 100%;
flex: 1;
+ .container {
+ width: 100%;
+ }
+ .full-tab {
+ text-align: center;
+ }
}
// Dropzone
position: relative;
top: -5px;
}
+}
+
+.contained-search-box {
+ display: flex;
+ input, button {
+ border-radius: 0;
+ border: 1px solid #DDD;
+ margin-left: -1px;
+ }
+ input {
+ flex: 5;
+ &:focus, &:active {
+ outline: 0;
+ }
+ }
+ button {
+ width: 60px;
+ }
+ button i {
+ padding: 0;
+ }
+ button.cancel.active {
+ background-color: $negative;
+ color: #EEE;
+ }
}
\ No newline at end of file
.button-base:hover, .button:hover, input[type="button"]:hover, input[type="submit"]:hover, .button:focus {
background-color: {{ Setting::get('app-color') }};
}
- .setting-nav a.selected {
+ .nav-tabs a.selected, .nav-tabs .tab-item.selected {
border-bottom-color: {{ Setting::get('app-color') }};
}
p.primary:hover, p .primary:hover, span.primary:hover, .text-primary:hover, a, a:hover, a:focus {
<div class="image-manager-body" ng-click="$event.stopPropagation()">
<div class="image-manager-content">
+ <div ng-if="imageType === 'gallery'" class="container">
+ <div class="image-manager-header row faded-small nav-tabs">
+ <div class="col-xs-4 tab-item" title="View all images" ng-class="{selected: (view=='all')}" ng-click="setView('all')"><i class="zmdi zmdi-collection-image"></i> All</div>
+ <div class="col-xs-4 tab-item" title="View images uploaded to this book" ng-class="{selected: (view=='book')}" ng-click="setView('book')"><i class="zmdi zmdi-book text-book"></i> Book</div>
+ <div class="col-xs-4 tab-item" title="View images uploaded to this page" ng-class="{selected: (view=='page')}" ng-click="setView('page')"><i class="zmdi zmdi-file-text text-page"></i> Page</div>
+ </div>
+ </div>
+ <div ng-show="view === 'all'" >
+ <form ng-submit="searchImages()" class="contained-search-box">
+ <input type="text" placeholder="Search by image name" ng-model="searchTerm">
+ <button ng-class="{active: searching}" title="Clear Search" type="button" ng-click="cancelSearch()" class="text-button cancel"><i class="zmdi zmdi-close-circle-o"></i></button>
+ <button title="Search" class="text-button" type="submit"><i class="zmdi zmdi-search"></i></button>
+ </form>
+ </div>
<div class="image-manager-list">
<div ng-repeat="image in images">
<div class="image anim fadeIn" ng-style="{animationDelay: ($index > 26) ? '160ms' : ($index * 25) + 'ms'}"
<div class="faded-small toolbar">
<div class="container">
<div class="row">
- <div class="col-md-12 setting-nav">
+ <div class="col-md-12 setting-nav nav-tabs">
<a href="/settings" @if($selected == 'settings') class="selected text-button" @endif><i class="zmdi zmdi-settings"></i>Settings</a>
<a href="/settings/users" @if($selected == 'users') class="selected text-button" @endif><i class="zmdi zmdi-accounts"></i>Users</a>
<a href="/settings/roles" @if($selected == 'roles') class="selected text-button" @endif><i class="zmdi zmdi-lock-open"></i>Roles</a>