mirror of
https://github.com/BookStackApp/BookStack.git
synced 2024-10-01 01:36:00 -04:00
Added custom user avatars
This commit is contained in:
parent
db3acabc66
commit
8f7c642f32
@ -33,6 +33,9 @@ GOOGLE_APP_SECRET=false
|
||||
# URL used for social login redirects, NO TRAILING SLASH
|
||||
APP_URL=http://bookstack.dev
|
||||
|
||||
# External services
|
||||
USE_GRAVATAR=true
|
||||
|
||||
# Mail settings
|
||||
MAIL_DRIVER=smtp
|
||||
MAIL_HOST=localhost
|
||||
|
@ -33,7 +33,7 @@ class ImageController extends Controller
|
||||
|
||||
|
||||
/**
|
||||
* Get all gallery images, Paginated
|
||||
* Get all images for a specific type, Paginated
|
||||
* @param int $page
|
||||
* @return \Illuminate\Http\JsonResponse
|
||||
*/
|
||||
@ -43,6 +43,17 @@ class ImageController extends Controller
|
||||
return response()->json($imgData);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all images for a user.
|
||||
* @param int $page
|
||||
* @return \Illuminate\Http\JsonResponse
|
||||
*/
|
||||
public function getAllForUserType($page = 0)
|
||||
{
|
||||
$imgData = $this->imageRepo->getPaginatedByType('user', $page, 24, $this->currentUser->id);
|
||||
return response()->json($imgData);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Handles image uploads for use on pages.
|
||||
|
@ -62,7 +62,7 @@ class UserController extends Controller
|
||||
$this->checkPermission('user-create');
|
||||
$this->validate($request, [
|
||||
'name' => 'required',
|
||||
'email' => 'required|email',
|
||||
'email' => 'required|email|unique:users,email',
|
||||
'password' => 'required|min:5',
|
||||
'password-confirm' => 'required|same:password',
|
||||
'role' => 'required|exists:roles,id'
|
||||
|
@ -57,6 +57,9 @@ Route::group(['middleware' => 'auth'], function () {
|
||||
|
||||
// Image routes
|
||||
Route::group(['prefix' => 'images'], function() {
|
||||
// Get for user images
|
||||
Route::get('/user/all', 'ImageController@getAllForUserType');
|
||||
Route::get('/user/all/{page}', 'ImageController@getAllForUserType');
|
||||
// Standard get, update and deletion for all types
|
||||
Route::get('/thumb/{id}/{width}/{height}/{crop}', 'ImageController@getThumbnail');
|
||||
Route::put('/update/{imageId}', 'ImageController@update');
|
||||
|
@ -3,9 +3,10 @@
|
||||
namespace BookStack;
|
||||
|
||||
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Images;
|
||||
|
||||
class Image
|
||||
class Image extends Model
|
||||
{
|
||||
use Ownable;
|
||||
|
||||
@ -16,9 +17,10 @@ class Image
|
||||
* @param int $width
|
||||
* @param int $height
|
||||
* @param bool|false $hardCrop
|
||||
* @return string
|
||||
*/
|
||||
public function getThumb($width, $height, $hardCrop = false)
|
||||
{
|
||||
Images::getThumbnail($this, $width, $height, $hardCrop);
|
||||
return Images::getThumbnail($this, $width, $height, $hardCrop);
|
||||
}
|
||||
}
|
||||
|
@ -17,7 +17,7 @@ class ImageRepo
|
||||
* @param Image $image
|
||||
* @param ImageService $imageService
|
||||
*/
|
||||
public function __construct(Image $image,ImageService $imageService)
|
||||
public function __construct(Image $image, ImageService $imageService)
|
||||
{
|
||||
$this->image = $image;
|
||||
$this->imageService = $imageService;
|
||||
@ -40,12 +40,18 @@ class ImageRepo
|
||||
* @param string $type
|
||||
* @param int $page
|
||||
* @param int $pageSize
|
||||
* @param bool|int $userFilter
|
||||
* @return array
|
||||
*/
|
||||
public function getPaginatedByType($type, $page = 0, $pageSize = 24)
|
||||
public function getPaginatedByType($type, $page = 0, $pageSize = 24, $userFilter = false)
|
||||
{
|
||||
$images = $this->image->where('type', '=', strtolower($type))
|
||||
->orderBy('created_at', 'desc')->skip($pageSize * $page)->take($pageSize + 1)->get();
|
||||
$images = $this->image->where('type', '=', strtolower($type));
|
||||
|
||||
if ($userFilter !== false) {
|
||||
$images = $images->where('created_by', '=', $userFilter);
|
||||
}
|
||||
|
||||
$images = $images->orderBy('created_at', 'desc')->skip($pageSize * $page)->take($pageSize + 1)->get();
|
||||
$hasMore = count($images) > $pageSize;
|
||||
|
||||
$returnImages = $images->take(24);
|
||||
@ -67,7 +73,7 @@ class ImageRepo
|
||||
*/
|
||||
public function saveNew(UploadedFile $uploadFile, $type)
|
||||
{
|
||||
$image = $this->imageService->saveNew($this->image, $uploadFile, $type);
|
||||
$image = $this->imageService->saveNewFromUpload($uploadFile, $type);
|
||||
$this->loadThumbs($image);
|
||||
return $image;
|
||||
}
|
||||
|
@ -1,6 +1,7 @@
|
||||
<?php namespace BookStack\Services;
|
||||
|
||||
use BookStack\Image;
|
||||
use BookStack\User;
|
||||
use Intervention\Image\ImageManager;
|
||||
use Illuminate\Contracts\Filesystem\Factory as FileSystem;
|
||||
use Illuminate\Contracts\Filesystem\Filesystem as FileSystemInstance;
|
||||
@ -34,11 +35,48 @@ class ImageService
|
||||
$this->cache = $cache;
|
||||
}
|
||||
|
||||
public function saveNew(Image $image, UploadedFile $uploadedFile, $type)
|
||||
/**
|
||||
* Saves a new image from an upload.
|
||||
* @param UploadedFile $uploadedFile
|
||||
* @param string $type
|
||||
* @return mixed
|
||||
*/
|
||||
public function saveNewFromUpload(UploadedFile $uploadedFile, $type)
|
||||
{
|
||||
$imageName = $uploadedFile->getClientOriginalName();
|
||||
$imageData = file_get_contents($uploadedFile->getRealPath());
|
||||
return $this->saveNew($imageName, $imageData, $type);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Gets an image from url and saves it to the database.
|
||||
* @param $url
|
||||
* @param string $type
|
||||
* @param bool|string $imageName
|
||||
* @return mixed
|
||||
* @throws \Exception
|
||||
*/
|
||||
private function saveNewFromUrl($url, $type, $imageName = false)
|
||||
{
|
||||
$imageName = $imageName ? $imageName : basename($url);
|
||||
$imageData = file_get_contents($url);
|
||||
if($imageData === false) throw new \Exception('Cannot get image from ' . $url);
|
||||
return $this->saveNew($imageName, $imageData, $type);
|
||||
}
|
||||
|
||||
/**
|
||||
* Saves a new image
|
||||
* @param string $imageName
|
||||
* @param string $imageData
|
||||
* @param string $type
|
||||
* @return Image
|
||||
*/
|
||||
private function saveNew($imageName, $imageData, $type)
|
||||
{
|
||||
$storage = $this->getStorage();
|
||||
$secureUploads = Setting::get('app-secure-images');
|
||||
$imageName = str_replace(' ', '-', $uploadedFile->getClientOriginalName());
|
||||
$imageName = str_replace(' ', '-', $imageName);
|
||||
|
||||
if ($secureUploads) $imageName = str_random(16) . '-' . $imageName;
|
||||
|
||||
@ -48,10 +86,10 @@ class ImageService
|
||||
}
|
||||
$fullPath = $imagePath . $imageName;
|
||||
|
||||
$storage->put($fullPath, file_get_contents($uploadedFile->getRealPath()));
|
||||
$storage->put($fullPath, $imageData);
|
||||
|
||||
$userId = auth()->user()->id;
|
||||
$image = $image->forceCreate([
|
||||
$image = Image::forceCreate([
|
||||
'name' => $imageName,
|
||||
'path' => $fullPath,
|
||||
'url' => $this->getPublicUrl($fullPath),
|
||||
@ -137,6 +175,26 @@ class ImageService
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Save a gravatar image and set a the profile image for a user.
|
||||
* @param User $user
|
||||
* @param int $size
|
||||
* @return mixed
|
||||
*/
|
||||
public function saveUserGravatar(User $user, $size = 500)
|
||||
{
|
||||
if (!env('USE_GRAVATAR', false)) return false;
|
||||
$emailHash = md5(strtolower(trim($user->email)));
|
||||
$url = 'http://www.gravatar.com/avatar/' . $emailHash . '?s=' . $size . '&d=identicon';
|
||||
$imageName = str_replace(' ', '-', $user->name . '-gravatar.png');
|
||||
$image = $this->saveNewFromUrl($url, 'user', $imageName);
|
||||
$image->created_by = $user->id;
|
||||
$image->save();
|
||||
$user->avatar()->associate($image);
|
||||
$user->save();
|
||||
return $image;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the storage that will be used for storing images.
|
||||
* @return FileSystemInstance
|
||||
|
15
app/User.php
15
app/User.php
@ -24,7 +24,7 @@ class User extends Model implements AuthenticatableContract, CanResetPasswordCon
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
protected $fillable = ['name', 'email', 'password'];
|
||||
protected $fillable = ['name', 'email', 'password', 'image_id'];
|
||||
|
||||
/**
|
||||
* The attributes excluded from the model's JSON form.
|
||||
@ -145,8 +145,17 @@ class User extends Model implements AuthenticatableContract, CanResetPasswordCon
|
||||
*/
|
||||
public function getAvatar($size = 50)
|
||||
{
|
||||
$emailHash = md5(strtolower(trim($this->email)));
|
||||
return '//www.gravatar.com/avatar/' . $emailHash . '?s=' . $size . '&d=identicon';
|
||||
if ($this->image_id === 0 || $this->image_id === null) return '/user_avatar.png';
|
||||
return $this->avatar->getThumb($size, $size, true);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the avatar for the user.
|
||||
* @return \Illuminate\Database\Eloquent\Relations\BelongsTo
|
||||
*/
|
||||
public function avatar()
|
||||
{
|
||||
return $this->belongsTo('BookStack\Image', 'image_id');
|
||||
}
|
||||
|
||||
/**
|
||||
|
31
database/migrations/2015_12_09_195748_add_user_avatars.php
Normal file
31
database/migrations/2015_12_09_195748_add_user_avatars.php
Normal file
@ -0,0 +1,31 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
|
||||
class AddUserAvatars extends Migration
|
||||
{
|
||||
/**
|
||||
* Run the migrations.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function up()
|
||||
{
|
||||
Schema::table('users', function (Blueprint $table) {
|
||||
$table->integer('image_id')->default(0);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Reverse the migrations.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function down()
|
||||
{
|
||||
Schema::table('users', function (Blueprint $table) {
|
||||
$table->dropColumn('image_id');
|
||||
});
|
||||
}
|
||||
}
|
BIN
public/user_avatar.png
Normal file
BIN
public/user_avatar.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 7.2 KiB |
@ -80,15 +80,6 @@
|
||||
imageType: {
|
||||
type: String,
|
||||
required: true
|
||||
},
|
||||
resizeWidth: {
|
||||
type: String
|
||||
},
|
||||
resizeHeight: {
|
||||
type: String
|
||||
},
|
||||
resizeCrop: {
|
||||
type: Boolean
|
||||
}
|
||||
},
|
||||
|
||||
@ -137,21 +128,7 @@
|
||||
},
|
||||
|
||||
returnCallback: function (image) {
|
||||
var _this = this;
|
||||
var isResized = _this.resizeWidth && _this.resizeHeight;
|
||||
|
||||
if (!isResized) {
|
||||
_this.callback(image);
|
||||
return;
|
||||
}
|
||||
|
||||
var cropped = _this.resizeCrop ? 'true' : 'false';
|
||||
var requestString = '/images/thumb/' + image.id + '/' + _this.resizeWidth + '/' + _this.resizeHeight + '/' + cropped;
|
||||
_this.$http.get(requestString, function(data) {
|
||||
image.thumbs.custom = data.url;
|
||||
_this.callback(image);
|
||||
});
|
||||
|
||||
this.callback(image);
|
||||
},
|
||||
|
||||
imageClick: function (image) {
|
||||
|
@ -7,31 +7,89 @@
|
||||
</div>
|
||||
<button class="button" type="button" @click="showImageManager">Select Image</button>
|
||||
<br>
|
||||
<button class="text-button" @click="reset" type="button">Reset</button> <span class="sep">|</span> <button class="text-button neg" v-on:click="remove" type="button">Remove</button>
|
||||
<input type="hidden" :name="name" :id="name" v-model="image">
|
||||
<button class="text-button" @click="reset" type="button">Reset</button> <span v-show="showRemove" class="sep">|</span> <button v-show="showRemove" class="text-button neg" @click="remove" type="button">Remove</button>
|
||||
<input type="hidden" :name="name" :id="name" v-model="value">
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
module.exports = {
|
||||
props: ['currentImage', 'name', 'imageClass', 'defaultImage'],
|
||||
data: function() {
|
||||
return {
|
||||
image: this.currentImage
|
||||
props: {
|
||||
currentImage: {
|
||||
required: true,
|
||||
type: String
|
||||
},
|
||||
currentId: {
|
||||
required: false,
|
||||
default: 'false',
|
||||
type: String
|
||||
},
|
||||
name: {
|
||||
required: true,
|
||||
type: String
|
||||
},
|
||||
defaultImage: {
|
||||
required: true,
|
||||
type: String
|
||||
},
|
||||
imageClass: {
|
||||
required: true,
|
||||
type: String
|
||||
},
|
||||
resizeWidth: {
|
||||
type: String
|
||||
},
|
||||
resizeHeight: {
|
||||
type: String
|
||||
},
|
||||
resizeCrop: {
|
||||
type: Boolean
|
||||
},
|
||||
showRemove: {
|
||||
type: Boolean,
|
||||
default: 'true'
|
||||
}
|
||||
},
|
||||
data: function() {
|
||||
return {
|
||||
image: this.currentImage,
|
||||
value: false
|
||||
}
|
||||
},
|
||||
compiled: function() {
|
||||
this.value = this.currentId === 'false' ? this.currentImage : this.currentId;
|
||||
},
|
||||
methods: {
|
||||
setCurrentValue: function(imageModel, imageUrl) {
|
||||
this.image = imageUrl;
|
||||
this.value = this.currentId === 'false' ? imageUrl : imageModel.id;
|
||||
},
|
||||
showImageManager: function(e) {
|
||||
var _this = this;
|
||||
ImageManager.show(function(image) {
|
||||
_this.image = image.thumbs.custom || image.url;
|
||||
_this.updateImageFromModel(image);
|
||||
});
|
||||
},
|
||||
reset: function() {
|
||||
this.image = '';
|
||||
this.setCurrentValue({id: 0}, this.defaultImage);
|
||||
},
|
||||
remove: function() {
|
||||
this.image = 'none';
|
||||
},
|
||||
updateImageFromModel: function(model) {
|
||||
var _this = this;
|
||||
var isResized = _this.resizeWidth && _this.resizeHeight;
|
||||
|
||||
if (!isResized) {
|
||||
_this.setCurrentValue(model, model.url);
|
||||
return;
|
||||
}
|
||||
|
||||
var cropped = _this.resizeCrop ? 'true' : 'false';
|
||||
var requestString = '/images/thumb/' + model.id + '/' + _this.resizeWidth + '/' + _this.resizeHeight + '/' + cropped;
|
||||
_this.$http.get(requestString, function(data) {
|
||||
_this.setCurrentValue(model, data.url);
|
||||
});
|
||||
}
|
||||
}
|
||||
};
|
||||
|
@ -36,6 +36,10 @@ body.dragging, body.dragging * {
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
}
|
||||
&.large {
|
||||
width: 80px;
|
||||
height: 80px;
|
||||
}
|
||||
}
|
||||
|
||||
// System wide notifications
|
||||
|
@ -33,7 +33,7 @@
|
||||
<div class="form-group" id="logo-control">
|
||||
<label for="setting-app-logo">Application Logo</label>
|
||||
<p class="small">This image should be 43px in height. <br>Large images will be scaled down.</p>
|
||||
<image-picker current-image="{{ Setting::get('app-logo', '') }}" default-image="/logo.png" name="setting-app-logo" image-class="logo-image"></image-picker>
|
||||
<image-picker resize-height="43" resize-width="200" current-image="{{ Setting::get('app-logo', '') }}" default-image="/logo.png" name="setting-app-logo" image-class="logo-image"></image-picker>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@ -86,6 +86,6 @@
|
||||
|
||||
</div>
|
||||
|
||||
<image-manager image-type="system" resize-height="43" resize-width="200"></image-manager>
|
||||
<image-manager image-type="system"></image-manager>
|
||||
|
||||
@stop
|
||||
|
@ -19,26 +19,25 @@
|
||||
|
||||
|
||||
<div class="container small">
|
||||
|
||||
<form action="/users/{{$user->id}}" method="post">
|
||||
<div class="row">
|
||||
<div class="col-md-6">
|
||||
<h1>Edit {{ $user->id === $currentUser->id ? 'Profile' : 'User' }}</h1>
|
||||
<form action="/users/{{$user->id}}" method="post">
|
||||
{!! csrf_field() !!}
|
||||
<input type="hidden" name="_method" value="put">
|
||||
@include('users/form', ['model' => $user])
|
||||
</form>
|
||||
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<h1> </h1>
|
||||
<div class="shaded padded margin-top">
|
||||
<p>
|
||||
<img class="avatar" src="{{ $user->getAvatar(80) }}" alt="{{ $user->name }}">
|
||||
</p>
|
||||
<p class="text-muted">You can change your profile picture at <a href="http://en.gravatar.com/">Gravatar</a>.</p>
|
||||
<div class="form-group" id="logo-control">
|
||||
<label for="user-avatar">User Avatar</label>
|
||||
<p class="small">This image should be approx 256px square.</p>
|
||||
<image-picker resize-height="512" resize-width="512" current-image="{{ $user->getAvatar(80) }}" current-id="{{ $user->image_id }}" default-image="/user_avatar.png" name="image_id" show-remove="false" image-class="avatar large"></image-picker>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
<hr class="margin-top large">
|
||||
|
||||
@ -80,5 +79,5 @@
|
||||
</div>
|
||||
|
||||
<p class="margin-top large"><br></p>
|
||||
|
||||
<image-manager image-type="user"></image-manager>
|
||||
@stop
|
||||
|
@ -37,3 +37,4 @@
|
||||
<a href="/users" class="button muted">Cancel</a>
|
||||
<button class="button pos" type="submit">Save</button>
|
||||
</div>
|
||||
|
||||
|
Loading…
Reference in New Issue
Block a user