├── LICENSE ├── Plugin.php ├── README.md ├── assets ├── css │ └── dropzone.css └── js │ ├── extend-markdown-editor.js │ └── upload.js ├── classes ├── MarkdownPhotoInsert.php └── MenuItemsProvider.php ├── components ├── Album.php ├── AlbumList.php ├── Photo.php ├── RandomPhotos.php ├── album │ └── default.htm ├── albumlist │ └── default.htm ├── photo │ └── default.htm └── randomphotos │ └── default.htm ├── controllers ├── Albums.php ├── Photos.php ├── Reorder.php ├── Upload.php ├── albums │ ├── _list_toolbar.htm │ ├── _relation_toolbar.htm │ ├── config_form.yaml │ ├── config_list.yaml │ ├── config_relation.yaml │ ├── create.htm │ ├── index.htm │ ├── preview.htm │ └── update.htm ├── photos │ ├── _list_toolbar.htm │ ├── config_form.yaml │ ├── config_list.yaml │ ├── create.htm │ ├── index.htm │ ├── preview.htm │ └── update.htm ├── reorder │ ├── _records.htm │ └── _reorder.htm └── upload │ └── _form.htm ├── lang ├── en │ └── lang.php └── es │ └── lang.php ├── models ├── Album.php ├── Photo.php ├── Settings.php ├── album │ ├── columns.yaml │ └── fields.yaml ├── photo │ ├── columns.yaml │ └── fields.yaml └── settings │ └── fields.yaml ├── phpunit.xml ├── tests └── RandomPhotosTest.php ├── updates ├── add_album_front.php ├── add_sort_order_field.php ├── create_albums_table.php ├── create_photos_table.php ├── update_sort_order_on_existing_photos.php └── version.yaml └── widgets ├── PhotoSelector.php └── photoselector ├── assets ├── css │ └── photoselector.css └── js │ └── photoselector.js └── partials ├── _albums.htm ├── _body.htm └── _photos.htm /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2016 Graker 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /Plugin.php: -------------------------------------------------------------------------------- 1 | 'graker.photoalbums::lang.plugin.name', 26 | 'description' => 'graker.photoalbums::lang.plugin.description', 27 | 'author' => 'Graker', 28 | 'icon' => 'icon-camera-retro', 29 | 'homepage' => 'https://github.com/graker/oc-photoalbums-plugin', 30 | ]; 31 | } 32 | 33 | /** 34 | * Registers any front-end components implemented in this plugin. 35 | * 36 | * @return array 37 | */ 38 | public function registerComponents() 39 | { 40 | return [ 41 | 'Graker\PhotoAlbums\Components\Photo' => 'singlePhoto', 42 | 'Graker\PhotoAlbums\Components\Album' => 'photoAlbum', 43 | 'Graker\PhotoAlbums\Components\AlbumList' => 'albumList', 44 | 'Graker\PhotoAlbums\Components\RandomPhotos' => 'randomPhotos', 45 | ]; 46 | } 47 | 48 | /** 49 | * Registers any back-end permissions used by this plugin. 50 | * At the moment there's one permission allowing overall management of albums and photos 51 | * 52 | * @return array 53 | */ 54 | public function registerPermissions() 55 | { 56 | return [ 57 | 'graker.photoalbums.manage_albums' => [ 58 | 'label' => 'graker.photoalbums::lang.plugin.manage_albums', 59 | 'tab' => 'graker.photoalbums::lang.plugin.tab', 60 | ], 61 | 'graker.photoalbums.access_settings' => [ 62 | 'label' => 'graker.photoalbums::lang.plugin.access_permission', 63 | 'tab' => 'graker.photoalbums::lang.plugin.tab', 64 | ], 65 | ]; 66 | } 67 | 68 | /** 69 | * Registers back-end navigation items for this plugin. 70 | * 71 | * @return array 72 | */ 73 | public function registerNavigation() 74 | { 75 | return [ 76 | 'photoalbums' => [ 77 | 'label' => 'graker.photoalbums::lang.plugin.tab', 78 | 'url' => Backend::url('graker/photoalbums/albums'), 79 | 'icon' => 'icon-camera-retro', 80 | 'permissions' => ['graker.photoalbums.manage_albums'], 81 | 'order' => 500, 82 | 83 | 'sideMenu' => [ 84 | 'upload_photos' => [ 85 | 'label' => 'graker.photoalbums::lang.plugin.upload_photos', 86 | 'icon' => 'icon-upload', 87 | 'url' => Backend::url('graker/photoalbums/upload/form'), 88 | 'permissions' => ['graker.photoalbums.manage_albums'], 89 | ], 90 | 'new_album' => [ 91 | 'label' => 'graker.photoalbums::lang.plugin.new_album', 92 | 'icon' => 'icon-plus', 93 | 'url' => Backend::url('graker/photoalbums/albums/create'), 94 | 'permissions' => ['graker.photoalbums.manage_albums'], 95 | ], 96 | 'albums' => [ 97 | 'label' => 'graker.photoalbums::lang.plugin.albums', 98 | 'icon' => 'icon-copy', 99 | 'url' => Backend::url('graker/photoalbums/albums'), 100 | 'permissions' => ['graker.photoalbums.manage_albums'], 101 | ], 102 | 'new_photo' => [ 103 | 'label' => 'graker.photoalbums::lang.plugin.new_photo', 104 | 'icon' => 'icon-plus-square-o', 105 | 'url' => Backend::url('graker/photoalbums/photos/create'), 106 | 'permissions' => ['graker.photoalbums.manage_albums'], 107 | ], 108 | 'photos' => [ 109 | 'label' => 'graker.photoalbums::lang.plugin.photos', 110 | 'icon' => 'icon-picture-o', 111 | 'url' => Backend::url('graker/photoalbums/photos'), 112 | 'permissions' => ['graker.photoalbums.manage_albums'], 113 | ], 114 | ], 115 | ], 116 | ]; 117 | } 118 | 119 | 120 | /** 121 | * 122 | * Registers plugin's settings 123 | * 124 | * @return array 125 | */ 126 | public function registerSettings() 127 | { 128 | return [ 129 | 'settings' => [ 130 | 'label' => 'graker.photoalbums::lang.plugin.name', 131 | 'description' => 'graker.photoalbums::lang.plugin.settings_description', 132 | 'icon' => 'icon-camera-retro', 133 | 'class' => 'Graker\PhotoAlbums\Models\Settings', 134 | 'order' => 100, 135 | 'permissions' => ['graker.photoalbums.access_settings'], 136 | ] 137 | ]; 138 | } 139 | 140 | 141 | /** 142 | * 143 | * Custom column types definition 144 | * 145 | * @return array 146 | */ 147 | public function registerListColumnTypes() { 148 | return [ 149 | 'is_front' => [$this, 'evalIsFrontListColumn'], 150 | 'image' => [$this, 'evalImageListColumn'], 151 | ]; 152 | } 153 | 154 | 155 | /** 156 | * 157 | * Special column to show photo set to be album's front in album's relations list 158 | * 159 | * @param $value 160 | * @param $column 161 | * @param $record 162 | * @return string 163 | */ 164 | public function evalIsFrontListColumn($value, $column, $record) { 165 | return ($value == $record->id) ? Lang::get('graker.photoalbums::lang.plugin.bool_positive') : ''; 166 | } 167 | 168 | 169 | /** 170 | * 171 | * Column to render image thumb for Photo model 172 | * 173 | * @param $value 174 | * @param $column 175 | * @param $record 176 | * @return string 177 | */ 178 | function evalImageListColumn($value, $column, $record) { 179 | if ($record->has('image')) { 180 | $thumb = $record->image->getThumb( 181 | isset($column->config['width']) ? $column->config['width'] : 200, 182 | isset($column->config['height']) ? $column->config['height'] : 200, 183 | ['mode' => 'auto'] 184 | ); 185 | } else { 186 | // in case the file attachment was manually deleted for some reason 187 | $thumb = ''; 188 | } 189 | return ""; 190 | } 191 | 192 | 193 | /** 194 | * boot() implementation 195 | * - Register listener to markdown.parse 196 | * - Add button to blog post form to insert photos from albums 197 | */ 198 | public function boot() { 199 | Event::listen('markdown.parse', 'Graker\PhotoAlbums\Classes\MarkdownPhotoInsert@parse'); 200 | $this->extendBlogPostForm(); 201 | $this->registerMenuItems(); 202 | } 203 | 204 | 205 | /** 206 | * Extends Blog post form by adding a new button: Insert photo from albums 207 | */ 208 | protected function extendBlogPostForm() { 209 | Event::listen('backend.form.extendFields', function (Form $widget) { 210 | // attach to post forms only 211 | $controller = $widget->getController(); 212 | if (!($controller instanceof \RainLab\Blog\Controllers\Posts)) { 213 | return ; 214 | } 215 | if (!($widget->model instanceof \RainLab\Blog\Models\Post)) { 216 | return ; 217 | } 218 | 219 | // add PhotoSelector widget to Post controller 220 | $photo_selector = new PhotoSelector($controller); 221 | $photo_selector->alias = 'photoSelector'; 222 | $photo_selector->bindToController(); 223 | 224 | // add javascript extending Markdown editor with new button 225 | $widget->addJs('/plugins/graker/photoalbums/assets/js/extend-markdown-editor.js'); 226 | }); 227 | } 228 | 229 | 230 | /** 231 | * Listen to events from RainLab.Pages plugin to register and resolve new menu items 232 | */ 233 | protected function registerMenuItems() { 234 | // register items 235 | Event::listen('pages.menuitem.listTypes', function() { 236 | return MenuItemsProvider::listTypes(); 237 | }); 238 | 239 | // return item type info 240 | Event::listen('pages.menuitem.getTypeInfo', function($type) { 241 | if (in_array($type, array_keys(MenuItemsProvider::listTypes()))) { 242 | return MenuItemsProvider::getMenuTypeInfo($type); 243 | } 244 | }); 245 | 246 | // resolve item 247 | Event::listen('pages.menuitem.resolveItem', function($type, $item, $url, $theme) { 248 | if (in_array($type, array_keys(MenuItemsProvider::listTypes()))) { 249 | return MenuItemsProvider::resolveMenuItem($type, $item, $url, $theme); 250 | } 251 | }); 252 | } 253 | 254 | } 255 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Photo Albums plugin 2 | 3 | This is [OctoberCMS](http://octobercms.com) plugin allowing to create, edit and display photos arranged in albums. Each Photo is a model with image attached to it. 4 | And Album is an another model, owning multiple of Photos. 5 | 6 | The aim of this approach is to treat each photo as a separate entity which can be displayed separately, have it's own title, description, could have comments of its own etc. 7 | And at the same time, photos are grouped in albums and can be displayed on album's page with pagination. 8 | 9 | Also now you can insert photos from galleries right into the blog posts (see below). 10 | 11 | ## Components 12 | 13 | There are 4 components in the plugin: Photo, Album, Albums List and Random Photos. 14 | 15 | ### Photo 16 | 17 | Photo component should be used to output a single photo. Data available for this single photo: 18 | 19 | * photo's title and description 20 | * photo's created date 21 | * image path 22 | * parent album's title and url 23 | * mini-navigator to go to the previous or the next photo 24 | 25 | ### Album 26 | 27 | This component is used to output album's photos. Data available: 28 | 29 | * album's title and description 30 | * each photo's title, thumb and url 31 | * pagination 32 | 33 | ### Albums list 34 | 35 | Use this component to output all albums (pagination is supported). For each album you can output title, image thumb and photos count. 36 | Image thumb is generated from selected front photo which you can set on album's edit page in the photos list (check the photo, click "Set as front" button). 37 | If no photo is selected is front, the latest uploaded photo will be used for thumb. 38 | 39 | ### Random Photos 40 | 41 | Displays given number of random photos. Note that for big database tables, selects with random sorting can slow down your site, so use the component with caution and make use of cache lifetime to avoid running the query on each component show. Also note that due to the use of RAND() function for sorting, the component would work with MySQL and Sqlite databases only. To use the component with other databases, you'd need to rewrite orderBy() call, otherwise it will just return non-random collection. After October updates to Laravel 5.5, DB-independent function will be used. 42 | 43 | ## Uploading 44 | 45 | At the moment, there are 3 ways to upload photos: 46 | 47 | * Add single photo using the New photo form 48 | * Add single photo using relations manager when in album update form 49 | * Add multiple photos to an album from the Upload photos form 50 | 51 | Uploading multiple photos is supported with the [Dropzone.js](http://www.dropzonejs.com/) plugin. You don't need to install it as it is already a part of October. 52 | 53 | ## Insert photos from galleries 54 | 55 | ### Dialog to insert photos into Blog posts 56 | 57 | You can insert photos from galleries created by this plugin into [Blog](https://octobercms.com/plugin/rainlab-blog) posts. 58 | Just click on a camera icon near media manager in the post markdown editor, then select album and photo. Markdown code for selected photo will appear in the editor. 59 | 60 | ### Markdown syntax 61 | 62 | To change the code template, go to Settings -> Photo Albums tab. The syntax is explained below and you can use `%id%` and `%title%` placeholders for photo id and title. 63 | You can use placeholders multiple times. For example, you can type in template like this: 64 | 65 | ```[![%title%]([photo:%id%:640:480:crop]){.img-responsive}]([photo:%id%] "%title%"){.magnific}``` 66 | 67 | It will result in image 640x480 cropped thumb with title having `img-responsive` class, linked to full-size image with title and `magnific` class. 68 | Note that you can't use quote symbol in the template, you have to replace quotes with `"`. 69 | 70 | The syntax for [photo] part is as follows: 71 | 72 | ```[photo:id:width:height:mode]``` 73 | 74 | Here: 75 | * `id` is a photo model id (you can get it from url). 76 | * `width` and `height` are optional, if they are provided, photo will be inserted as a thumbnail with these width and height. 77 | * `mode` is an optional mode for thumbnail generation, possible values are: `auto`, `exact`, `portrait`, `landscape`, `crop` (see October thumbs generation for more info). Defaults to `auto`. 78 | 79 | For example: 80 | 81 | * `[photo:123:640:480:crop]` for cropped thumbnail 640x480 of photo with id 123 82 | * `[photo:123:200:200]` for thumbnail 200x200 of photo with id 123 83 | * `[photo:123]` for image as is, no thumb 84 | 85 | The placeholder will be replaced with path to image (or thumb), for example: `/storage/app/uploads/public/57a/24e/bff/thumb_301_640x480_0_0_auto.jpg`. 86 | 87 | You can use this code to insert photos in any markdown-processed text. 88 | 89 | Note that to avoid possible conflicts, placeholders are only replaced inside `src=""` and `href=""` clauses. 90 | So if you add placeholder in href for anchor tag or in src for img tag (or into Markdown link or image), it will be replaced. And if you add it into plain text, it will be ignored. 91 | 92 | ## Roadmap 93 | 94 | ### Attachments location 95 | 96 | Right now plugin uses System\Models\File to attach images so they are stored in system uploads, each one in separate directory with random names. 97 | It could be nice to put them in one directory per album. 98 | 99 | ### Categories support for albums 100 | 101 | It would be nice to be able to separate albums by categories, to group them by categories in the AlbumsList component etc. 102 | -------------------------------------------------------------------------------- /assets/css/dropzone.css: -------------------------------------------------------------------------------- 1 | /* 2 | * The MIT License 3 | * Copyright (c) 2012 Matias Meno 4 | */ 5 | @-webkit-keyframes passing-through { 6 | 0% { 7 | opacity: 0; 8 | -webkit-transform: translateY(40px); 9 | -moz-transform: translateY(40px); 10 | -ms-transform: translateY(40px); 11 | -o-transform: translateY(40px); 12 | transform: translateY(40px); } 13 | 30%, 70% { 14 | opacity: 1; 15 | -webkit-transform: translateY(0px); 16 | -moz-transform: translateY(0px); 17 | -ms-transform: translateY(0px); 18 | -o-transform: translateY(0px); 19 | transform: translateY(0px); } 20 | 100% { 21 | opacity: 0; 22 | -webkit-transform: translateY(-40px); 23 | -moz-transform: translateY(-40px); 24 | -ms-transform: translateY(-40px); 25 | -o-transform: translateY(-40px); 26 | transform: translateY(-40px); } } 27 | @-moz-keyframes passing-through { 28 | 0% { 29 | opacity: 0; 30 | -webkit-transform: translateY(40px); 31 | -moz-transform: translateY(40px); 32 | -ms-transform: translateY(40px); 33 | -o-transform: translateY(40px); 34 | transform: translateY(40px); } 35 | 30%, 70% { 36 | opacity: 1; 37 | -webkit-transform: translateY(0px); 38 | -moz-transform: translateY(0px); 39 | -ms-transform: translateY(0px); 40 | -o-transform: translateY(0px); 41 | transform: translateY(0px); } 42 | 100% { 43 | opacity: 0; 44 | -webkit-transform: translateY(-40px); 45 | -moz-transform: translateY(-40px); 46 | -ms-transform: translateY(-40px); 47 | -o-transform: translateY(-40px); 48 | transform: translateY(-40px); } } 49 | @keyframes passing-through { 50 | 0% { 51 | opacity: 0; 52 | -webkit-transform: translateY(40px); 53 | -moz-transform: translateY(40px); 54 | -ms-transform: translateY(40px); 55 | -o-transform: translateY(40px); 56 | transform: translateY(40px); } 57 | 30%, 70% { 58 | opacity: 1; 59 | -webkit-transform: translateY(0px); 60 | -moz-transform: translateY(0px); 61 | -ms-transform: translateY(0px); 62 | -o-transform: translateY(0px); 63 | transform: translateY(0px); } 64 | 100% { 65 | opacity: 0; 66 | -webkit-transform: translateY(-40px); 67 | -moz-transform: translateY(-40px); 68 | -ms-transform: translateY(-40px); 69 | -o-transform: translateY(-40px); 70 | transform: translateY(-40px); } } 71 | @-webkit-keyframes slide-in { 72 | 0% { 73 | opacity: 0; 74 | -webkit-transform: translateY(40px); 75 | -moz-transform: translateY(40px); 76 | -ms-transform: translateY(40px); 77 | -o-transform: translateY(40px); 78 | transform: translateY(40px); } 79 | 30% { 80 | opacity: 1; 81 | -webkit-transform: translateY(0px); 82 | -moz-transform: translateY(0px); 83 | -ms-transform: translateY(0px); 84 | -o-transform: translateY(0px); 85 | transform: translateY(0px); } } 86 | @-moz-keyframes slide-in { 87 | 0% { 88 | opacity: 0; 89 | -webkit-transform: translateY(40px); 90 | -moz-transform: translateY(40px); 91 | -ms-transform: translateY(40px); 92 | -o-transform: translateY(40px); 93 | transform: translateY(40px); } 94 | 30% { 95 | opacity: 1; 96 | -webkit-transform: translateY(0px); 97 | -moz-transform: translateY(0px); 98 | -ms-transform: translateY(0px); 99 | -o-transform: translateY(0px); 100 | transform: translateY(0px); } } 101 | @keyframes slide-in { 102 | 0% { 103 | opacity: 0; 104 | -webkit-transform: translateY(40px); 105 | -moz-transform: translateY(40px); 106 | -ms-transform: translateY(40px); 107 | -o-transform: translateY(40px); 108 | transform: translateY(40px); } 109 | 30% { 110 | opacity: 1; 111 | -webkit-transform: translateY(0px); 112 | -moz-transform: translateY(0px); 113 | -ms-transform: translateY(0px); 114 | -o-transform: translateY(0px); 115 | transform: translateY(0px); } } 116 | @-webkit-keyframes pulse { 117 | 0% { 118 | -webkit-transform: scale(1); 119 | -moz-transform: scale(1); 120 | -ms-transform: scale(1); 121 | -o-transform: scale(1); 122 | transform: scale(1); } 123 | 10% { 124 | -webkit-transform: scale(1.1); 125 | -moz-transform: scale(1.1); 126 | -ms-transform: scale(1.1); 127 | -o-transform: scale(1.1); 128 | transform: scale(1.1); } 129 | 20% { 130 | -webkit-transform: scale(1); 131 | -moz-transform: scale(1); 132 | -ms-transform: scale(1); 133 | -o-transform: scale(1); 134 | transform: scale(1); } } 135 | @-moz-keyframes pulse { 136 | 0% { 137 | -webkit-transform: scale(1); 138 | -moz-transform: scale(1); 139 | -ms-transform: scale(1); 140 | -o-transform: scale(1); 141 | transform: scale(1); } 142 | 10% { 143 | -webkit-transform: scale(1.1); 144 | -moz-transform: scale(1.1); 145 | -ms-transform: scale(1.1); 146 | -o-transform: scale(1.1); 147 | transform: scale(1.1); } 148 | 20% { 149 | -webkit-transform: scale(1); 150 | -moz-transform: scale(1); 151 | -ms-transform: scale(1); 152 | -o-transform: scale(1); 153 | transform: scale(1); } } 154 | @keyframes pulse { 155 | 0% { 156 | -webkit-transform: scale(1); 157 | -moz-transform: scale(1); 158 | -ms-transform: scale(1); 159 | -o-transform: scale(1); 160 | transform: scale(1); } 161 | 10% { 162 | -webkit-transform: scale(1.1); 163 | -moz-transform: scale(1.1); 164 | -ms-transform: scale(1.1); 165 | -o-transform: scale(1.1); 166 | transform: scale(1.1); } 167 | 20% { 168 | -webkit-transform: scale(1); 169 | -moz-transform: scale(1); 170 | -ms-transform: scale(1); 171 | -o-transform: scale(1); 172 | transform: scale(1); } } 173 | .dropzone, .dropzone * { 174 | box-sizing: border-box; } 175 | 176 | .dropzone { 177 | min-height: 150px; 178 | border: 2px solid rgba(0, 0, 0, 0.3); 179 | background: white; 180 | padding: 20px 20px; } 181 | .dropzone.dz-clickable { 182 | cursor: pointer; } 183 | .dropzone.dz-clickable * { 184 | cursor: default; } 185 | .dropzone.dz-clickable .dz-message, .dropzone.dz-clickable .dz-message * { 186 | cursor: pointer; } 187 | .dropzone.dz-started .dz-message { 188 | display: none; } 189 | .dropzone.dz-drag-hover { 190 | border-style: solid; } 191 | .dropzone.dz-drag-hover .dz-message { 192 | opacity: 0.5; } 193 | .dropzone .dz-message { 194 | text-align: center; 195 | margin: 2em 0; } 196 | .dropzone .dz-preview { 197 | position: relative; 198 | display: inline-block; 199 | vertical-align: top; 200 | margin: 16px; 201 | min-height: 100px; } 202 | .dropzone .dz-preview:hover { 203 | z-index: 1000; } 204 | .dropzone .dz-preview:hover .dz-details { 205 | opacity: 1; } 206 | .dropzone .dz-preview.dz-file-preview .dz-image { 207 | border-radius: 20px; 208 | background: #999; 209 | background: linear-gradient(to bottom, #eee, #ddd); } 210 | .dropzone .dz-preview.dz-file-preview .dz-details { 211 | opacity: 1; } 212 | .dropzone .dz-preview.dz-image-preview { 213 | background: white; } 214 | .dropzone .dz-preview.dz-image-preview .dz-details { 215 | -webkit-transition: opacity 0.2s linear; 216 | -moz-transition: opacity 0.2s linear; 217 | -ms-transition: opacity 0.2s linear; 218 | -o-transition: opacity 0.2s linear; 219 | transition: opacity 0.2s linear; } 220 | .dropzone .dz-preview .dz-remove { 221 | font-size: 14px; 222 | text-align: center; 223 | display: block; 224 | cursor: pointer; 225 | border: none; } 226 | .dropzone .dz-preview .dz-remove:hover { 227 | text-decoration: underline; } 228 | .dropzone .dz-preview:hover .dz-details { 229 | opacity: 1; } 230 | .dropzone .dz-preview .dz-details { 231 | z-index: 20; 232 | position: absolute; 233 | top: 0; 234 | left: 0; 235 | opacity: 0; 236 | font-size: 13px; 237 | min-width: 100%; 238 | max-width: 100%; 239 | padding: 2em 1em; 240 | text-align: center; 241 | color: rgba(0, 0, 0, 0.9); 242 | line-height: 150%; } 243 | .dropzone .dz-preview .dz-details .dz-size { 244 | margin-bottom: 1em; 245 | font-size: 16px; } 246 | .dropzone .dz-preview .dz-details .dz-filename { 247 | white-space: nowrap; } 248 | .dropzone .dz-preview .dz-details .dz-filename:hover span { 249 | border: 1px solid rgba(200, 200, 200, 0.8); 250 | background-color: rgba(255, 255, 255, 0.8); } 251 | .dropzone .dz-preview .dz-details .dz-filename:not(:hover) { 252 | overflow: hidden; 253 | text-overflow: ellipsis; } 254 | .dropzone .dz-preview .dz-details .dz-filename:not(:hover) span { 255 | border: 1px solid transparent; } 256 | .dropzone .dz-preview .dz-details .dz-filename span, .dropzone .dz-preview .dz-details .dz-size span { 257 | background-color: rgba(255, 255, 255, 0.4); 258 | padding: 0 0.4em; 259 | border-radius: 3px; } 260 | .dropzone .dz-preview:hover .dz-image img { 261 | -webkit-transform: scale(1.05, 1.05); 262 | -moz-transform: scale(1.05, 1.05); 263 | -ms-transform: scale(1.05, 1.05); 264 | -o-transform: scale(1.05, 1.05); 265 | transform: scale(1.05, 1.05); 266 | -webkit-filter: blur(8px); 267 | filter: blur(8px); } 268 | .dropzone .dz-preview .dz-image { 269 | border-radius: 20px; 270 | overflow: hidden; 271 | width: 120px; 272 | height: 120px; 273 | position: relative; 274 | display: block; 275 | z-index: 10; } 276 | .dropzone .dz-preview .dz-image img { 277 | display: block; } 278 | .dropzone .dz-preview.dz-success .dz-success-mark { 279 | -webkit-animation: passing-through 3s cubic-bezier(0.77, 0, 0.175, 1); 280 | -moz-animation: passing-through 3s cubic-bezier(0.77, 0, 0.175, 1); 281 | -ms-animation: passing-through 3s cubic-bezier(0.77, 0, 0.175, 1); 282 | -o-animation: passing-through 3s cubic-bezier(0.77, 0, 0.175, 1); 283 | animation: passing-through 3s cubic-bezier(0.77, 0, 0.175, 1); } 284 | .dropzone .dz-preview.dz-error .dz-error-mark { 285 | opacity: 1; 286 | -webkit-animation: slide-in 3s cubic-bezier(0.77, 0, 0.175, 1); 287 | -moz-animation: slide-in 3s cubic-bezier(0.77, 0, 0.175, 1); 288 | -ms-animation: slide-in 3s cubic-bezier(0.77, 0, 0.175, 1); 289 | -o-animation: slide-in 3s cubic-bezier(0.77, 0, 0.175, 1); 290 | animation: slide-in 3s cubic-bezier(0.77, 0, 0.175, 1); } 291 | .dropzone .dz-preview .dz-success-mark, .dropzone .dz-preview .dz-error-mark { 292 | pointer-events: none; 293 | opacity: 0; 294 | z-index: 500; 295 | position: absolute; 296 | display: block; 297 | top: 50%; 298 | left: 50%; 299 | margin-left: -27px; 300 | margin-top: -27px; } 301 | .dropzone .dz-preview .dz-success-mark svg, .dropzone .dz-preview .dz-error-mark svg { 302 | display: block; 303 | width: 54px; 304 | height: 54px; } 305 | .dropzone .dz-preview.dz-processing .dz-progress { 306 | opacity: 1; 307 | -webkit-transition: all 0.2s linear; 308 | -moz-transition: all 0.2s linear; 309 | -ms-transition: all 0.2s linear; 310 | -o-transition: all 0.2s linear; 311 | transition: all 0.2s linear; } 312 | .dropzone .dz-preview.dz-complete .dz-progress { 313 | opacity: 0; 314 | -webkit-transition: opacity 0.4s ease-in; 315 | -moz-transition: opacity 0.4s ease-in; 316 | -ms-transition: opacity 0.4s ease-in; 317 | -o-transition: opacity 0.4s ease-in; 318 | transition: opacity 0.4s ease-in; } 319 | .dropzone .dz-preview:not(.dz-processing) .dz-progress { 320 | -webkit-animation: pulse 6s ease infinite; 321 | -moz-animation: pulse 6s ease infinite; 322 | -ms-animation: pulse 6s ease infinite; 323 | -o-animation: pulse 6s ease infinite; 324 | animation: pulse 6s ease infinite; } 325 | .dropzone .dz-preview .dz-progress { 326 | opacity: 1; 327 | z-index: 1000; 328 | pointer-events: none; 329 | position: absolute; 330 | height: 16px; 331 | left: 50%; 332 | top: 50%; 333 | margin-top: -8px; 334 | width: 80px; 335 | margin-left: -40px; 336 | background: rgba(255, 255, 255, 0.9); 337 | -webkit-transform: scale(1); 338 | border-radius: 8px; 339 | overflow: hidden; } 340 | .dropzone .dz-preview .dz-progress .dz-upload { 341 | background: #333; 342 | background: linear-gradient(to bottom, #666, #444); 343 | position: absolute; 344 | top: 0; 345 | left: 0; 346 | bottom: 0; 347 | width: 0; 348 | -webkit-transition: width 300ms ease-in-out; 349 | -moz-transition: width 300ms ease-in-out; 350 | -ms-transition: width 300ms ease-in-out; 351 | -o-transition: width 300ms ease-in-out; 352 | transition: width 300ms ease-in-out; } 353 | .dropzone .dz-preview.dz-error .dz-error-message { 354 | display: block; } 355 | .dropzone .dz-preview.dz-error:hover .dz-error-message { 356 | opacity: 1; 357 | pointer-events: auto; } 358 | .dropzone .dz-preview .dz-error-message { 359 | pointer-events: none; 360 | z-index: 1000; 361 | position: absolute; 362 | display: block; 363 | display: none; 364 | opacity: 0; 365 | -webkit-transition: opacity 0.3s ease; 366 | -moz-transition: opacity 0.3s ease; 367 | -ms-transition: opacity 0.3s ease; 368 | -o-transition: opacity 0.3s ease; 369 | transition: opacity 0.3s ease; 370 | border-radius: 8px; 371 | font-size: 13px; 372 | top: 130px; 373 | left: -10px; 374 | width: 140px; 375 | background: #be2626; 376 | background: linear-gradient(to bottom, #be2626, #a92222); 377 | padding: 0.5em 1.2em; 378 | color: white; } 379 | .dropzone .dz-preview .dz-error-message:after { 380 | content: ''; 381 | position: absolute; 382 | top: -6px; 383 | left: 64px; 384 | width: 0; 385 | height: 0; 386 | border-left: 6px solid transparent; 387 | border-right: 6px solid transparent; 388 | border-bottom: 6px solid #be2626; 389 | } 390 | .dropzone input.form-control { 391 | width: 120px; 392 | } 393 | -------------------------------------------------------------------------------- /assets/js/extend-markdown-editor.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Script to extend Markdown editor for Blog post form 3 | * - add button to insert photos from albums 4 | * - add visual dialog for this button 5 | */ 6 | 7 | +function ($) { 8 | 9 | $(document).one('ready', function () { 10 | var editor = $('[data-control="markdowneditor"]').data('oc.markdownEditor'); 11 | 12 | // to preserve last selected album so user won't open it again and again 13 | var currentAlbum = 0; 14 | 15 | // FIXME Localize label when it is supported 16 | var button = { 17 | label: 'Insert photo from Photoalbums', 18 | cssClass: 'oc-autumn-button oc-icon-camera-retro', 19 | insertAfter: 'mediaimage', 20 | action: 'showAlbumsDialog', 21 | template: '$1' 22 | }; 23 | 24 | /** 25 | * 26 | * Markdown editor method to show photo selection dialog 27 | * 28 | * @param template 29 | */ 30 | editor.showAlbumsDialog = function (template) { 31 | var editor = this.editor, 32 | pos = this.editor.getCursorPosition(); 33 | 34 | new $.oc.photoselector.popup({ 35 | alias: 'photoSelector', 36 | album: currentAlbum, 37 | onInsert: function (code, album) { 38 | editor.insert(template.replace('$1', code)); 39 | editor.moveCursorToPosition(pos); 40 | editor.focus(); 41 | this.hide(); 42 | // save current album 43 | currentAlbum = album; 44 | console.log(currentAlbum); 45 | console.log(album); 46 | } 47 | }); 48 | }; 49 | 50 | //add button to editor 51 | editor.addToolbarButton('photoalbums', button); 52 | }); 53 | 54 | }(window.jQuery); 55 | -------------------------------------------------------------------------------- /assets/js/upload.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Dropzone multiupload support to upload photos to album 3 | */ 4 | 5 | +function ($) { 6 | 7 | /** 8 | * 9 | * File is being removed from the list 10 | * We need to remove it on the server 11 | * 12 | * @param file 13 | */ 14 | var removeFile = function (file) { 15 | var $preview = $(file.previewElement); 16 | var fileData = { 17 | file_id: $preview.data('id'), 18 | _token: $('input[name="_token"]').attr('value') 19 | }; 20 | $(this).request('onFileRemove', {data: fileData}); 21 | }; 22 | 23 | 24 | /** 25 | * 26 | * File is being sent to the server 27 | * Used to add CSRF token to the form 28 | * 29 | * @param file 30 | * @param xhr 31 | * @param formData 32 | */ 33 | var sendingData = function (file, xhr, formData) { 34 | var token = $('input[name="_token"]').attr('value'); 35 | formData.append('_token', token); 36 | }; 37 | 38 | 39 | /** 40 | * 41 | * File upload success callback 42 | * 43 | * @param data 44 | * @param response 45 | */ 46 | var uploadSuccess = function (file, response) { 47 | var $preview = $(file.previewElement); 48 | if (response.id) { 49 | $preview.data('id', response.id); 50 | // hidden value to pass file id when saving form 51 | $preview.append(''); 52 | $preview.append('
'); 53 | } 54 | }; 55 | 56 | 57 | /** 58 | * Initializes Dropzone 59 | */ 60 | var initDropzone = function () { 61 | // register removed file callback 62 | this.on('removedfile', removeFile); 63 | // register before-send callback 64 | this.on('sending', sendingData); 65 | }; 66 | 67 | 68 | /** 69 | * Initialize file upload 70 | */ 71 | $(document).ready(function () { 72 | $("div.field-fileupload").each(function () { 73 | var uploadUrl = $(this).attr('data-url'); 74 | $(this).dropzone({ 75 | url: uploadUrl, 76 | init: initDropzone, 77 | addRemoveLinks: true, 78 | previewsContainer: '#filesContainer', 79 | success: uploadSuccess 80 | }); 81 | }); 82 | }); 83 | } (window.jQuery); 84 | -------------------------------------------------------------------------------- /classes/MarkdownPhotoInsert.php: -------------------------------------------------------------------------------- 1 | text, $links, PREG_SET_ORDER); 36 | preg_match_all(self::PHOTO_IMG_REGEXP, $data->text, $images, PREG_SET_ORDER); 37 | 38 | if (!empty($images)) { 39 | $data->text = $this->replaceMatches($images, $data->text); 40 | } 41 | 42 | if (!empty($links)) { 43 | $data->text = $this->replaceMatches($links, $data->text); 44 | } 45 | } 46 | 47 | 48 | /** 49 | * 50 | * Goes over all matches and replaces them in text 51 | * Returns processed text 52 | * 53 | * @param $matches 54 | * @param $text 55 | * @return mixed 56 | */ 57 | protected function replaceMatches($matches, $text) { 58 | foreach ($matches as $match) { 59 | list($entry, $placeholder) = $match; 60 | $replacement = $this->getReplacement($entry, $placeholder); 61 | $text = str_replace($entry, $replacement, $text); 62 | } 63 | return $text; 64 | } 65 | 66 | 67 | /** 68 | * 69 | * Returns replacement for text 70 | * (replaces [photo:id:width:height:mode] with resulting photo's image path) 71 | * 72 | * @param $entry 73 | * @param $placeholder 74 | * @return string 75 | */ 76 | protected function getReplacement($entry, $placeholder) { 77 | list($id, $width, $height, $mode) = $this->getPhotoParams($placeholder); 78 | $photo = Photo::where('id', $id) 79 | ->with('image') 80 | ->first(); 81 | if (!$photo) { 82 | return $placeholder; 83 | } else { 84 | if ($width && $height) { 85 | $path = $photo->image->getThumb($width, $height, ['mode' => $mode]); 86 | } else { 87 | $path = $photo->image->path; 88 | } 89 | return str_replace($placeholder, $path, $entry); 90 | } 91 | } 92 | 93 | 94 | /** 95 | * 96 | * Parses parameters of image from the tag and returns them in array 97 | * [$id, $width, $height, $mode] 98 | * Width, height and mode are optional and will return 0 and empty string 99 | * if omitted in the tag 100 | * 101 | * @param string $placeholder 102 | * @return array 103 | */ 104 | protected function getPhotoParams($placeholder) { 105 | // remove brackets 106 | $values = str_replace('[', '', $placeholder); 107 | $values = str_replace(']', '', $values); 108 | // get parameters 109 | $values = explode(':', $values); 110 | $id = $values[1]; 111 | $width = isset($values[2]) ? $values[2] : 0; 112 | $height = isset($values[3]) ? $values[3] : 0; 113 | $mode = isset($values[4]) ? $values[4] : 'auto'; 114 | return array($id, $width, $height, $mode); 115 | } 116 | 117 | } 118 | -------------------------------------------------------------------------------- /classes/MenuItemsProvider.php: -------------------------------------------------------------------------------- 1 | 'graker.photoalbums::lang.plugin.all_photo_albums', 27 | 'all-photos' => 'graker.photoalbums::lang.plugin.all_photos', 28 | 'photo-album' => 'graker.photoalbums::lang.plugin.album', 29 | ]; 30 | } 31 | 32 | 33 | /** 34 | * 35 | * Returns an array of info about menu item type 36 | * 37 | * @param string $type item name 38 | * @return array 39 | */ 40 | public static function getMenuTypeInfo($type) { 41 | switch ($type) { 42 | case 'all-photo-albums' : 43 | $result = self::getAllAlbumsInfo(); 44 | break; 45 | case 'all-photos' : 46 | $result = self::getAllPhotosInfo(); 47 | break; 48 | case 'photo-album' : 49 | $result = self::getSingleAlbumInfo(); 50 | break; 51 | default: 52 | $result = []; 53 | } 54 | 55 | return $result; 56 | } 57 | 58 | 59 | /** 60 | * 61 | * Returns information about a menu item 62 | * 63 | * @param string $type 64 | * @param MenuItem $item 65 | * @param string $url 66 | * @param Theme $theme 67 | * @return array 68 | */ 69 | public static function resolveMenuItem($type, $item, $url, $theme) { 70 | $result = []; 71 | 72 | switch ($type) { 73 | case 'all-photo-albums' : 74 | $result = self::resolveAllAlbumsItem($item, $url, $theme); 75 | break; 76 | case 'all-photos' : 77 | $result = self::resolveAllPhotosItem($item, $url, $theme); 78 | break; 79 | case 'photo-album' : 80 | $result = self::resolveSingleAlbumItem($item, $url, $theme); 81 | break; 82 | default: 83 | $result = []; 84 | } 85 | 86 | return $result; 87 | } 88 | 89 | 90 | /** 91 | * 92 | * Generates url for the item to be resolved 93 | * 94 | * @param int $year - year number 95 | * @param string $pageCode - page code to be used 96 | * @param $theme 97 | * @return string 98 | */ 99 | protected static function getUrl($year, $pageCode, $theme) { 100 | $page = CmsPage::loadCached($theme, $pageCode); 101 | if (!$page) return ''; 102 | 103 | $properties = $page->getComponentProperties('blogArchive'); 104 | if (!isset($properties['yearParam'])) { 105 | return ''; 106 | } 107 | 108 | // get year url param and strip it of {{ : }} to get pure name 109 | $paramName = str_replace(array('{', '}', ' ', ':'), '', $properties['yearParam']); 110 | $url = CmsPage::url($page->getBaseFileName(), [$paramName => $year]); 111 | 112 | return $url; 113 | } 114 | 115 | 116 | /** 117 | * 118 | * Returns menu type info for all-photo-albums menu item 119 | * 120 | * @return array 121 | */ 122 | protected static function getAllAlbumsInfo() { 123 | $result = ['dynamicItems' => TRUE,]; 124 | $result['cmsPages'] = self::getCmsPages('photoAlbum'); 125 | return $result; 126 | } 127 | 128 | 129 | /** 130 | * 131 | * Returns menu type info for all-photo-albums menu item 132 | * 133 | * @return array 134 | */ 135 | protected static function getSingleAlbumInfo() { 136 | $result = [ 137 | 'dynamicItems' => FALSE, 138 | 'nesting' => FALSE, 139 | ]; 140 | $result['cmsPages'] = self::getCmsPages('photoAlbum'); 141 | 142 | $references = []; 143 | $albums = Album::all(); 144 | foreach ($albums as $album) { 145 | $references[$album->id] = $album->title; 146 | } 147 | $result['references'] = $references; 148 | 149 | return $result; 150 | } 151 | 152 | 153 | /** 154 | * 155 | * Returns menu type info for all-photos menu item 156 | * 157 | * @return array 158 | */ 159 | protected static function getAllPhotosInfo() { 160 | $result = ['dynamicItems' => true,]; 161 | $result['cmsPages'] = self::getCmsPages('singlePhoto'); 162 | return $result; 163 | } 164 | 165 | 166 | /** 167 | * 168 | * Return array of Cms pages having $component attached 169 | * 170 | * @param string $component 171 | * @return array 172 | */ 173 | protected static function getCmsPages($component) { 174 | $theme = Theme::getActiveTheme(); 175 | 176 | $pages = CmsPage::listInTheme($theme, true); 177 | $cmsPages = []; 178 | 179 | foreach ($pages as $page) { 180 | if (!$page->hasComponent($component)) { 181 | continue; 182 | } 183 | 184 | $cmsPages[] = $page; 185 | } 186 | 187 | return $cmsPages; 188 | } 189 | 190 | 191 | /** 192 | * 193 | * Resolves All Albums menu item 194 | * 195 | * @param MenuItem $item 196 | * @param string $url 197 | * @param Theme $theme 198 | * @return array 199 | */ 200 | protected static function resolveAllAlbumsItem($item, $url, $theme) { 201 | $result = [ 202 | 'items' => [], 203 | ]; 204 | 205 | $albums = Album::all(); 206 | foreach ($albums as $album) { 207 | $albumItem = [ 208 | 'title' => $album->title, 209 | 'url' => self::getAlbumUrl($album, $item->cmsPage, $theme), 210 | 'mtime' => $album->updated_at, 211 | ]; 212 | $albumItem['isActive'] = ($albumItem['url'] == $url); 213 | $result['items'][] = $albumItem; 214 | } 215 | 216 | return $result; 217 | } 218 | 219 | 220 | /** 221 | * 222 | * Resolves single Album menu item 223 | * 224 | * @param MenuItem $item 225 | * @param string $url 226 | * @param Theme $theme 227 | * @return array 228 | */ 229 | protected static function resolveSingleAlbumItem($item, $url, $theme) { 230 | $result = []; 231 | 232 | if (!$item->reference || !$item->cmsPage) { 233 | return []; 234 | } 235 | 236 | $album = Album::find($item->reference); 237 | if (!$album) { 238 | return []; 239 | } 240 | 241 | $pageUrl = self::getAlbumUrl($album, $item->cmsPage, $theme); 242 | if (!$pageUrl) { 243 | return []; 244 | } 245 | $pageUrl = Url::to($pageUrl); 246 | 247 | $result['url'] = $pageUrl; 248 | $result['isActive'] = ($pageUrl == $url); 249 | $result['mtime'] = $album->updated_at; 250 | 251 | return $result; 252 | } 253 | 254 | 255 | /** 256 | * 257 | * Resolves All Photos menu item 258 | * 259 | * @param MenuItem $item 260 | * @param string $url 261 | * @param Theme $theme 262 | * @return array 263 | */ 264 | protected static function resolveAllPhotosItem($item, $url, $theme) { 265 | $result = [ 266 | 'items' => [], 267 | ]; 268 | 269 | $photos = Photo::all(); 270 | foreach ($photos as $photo) { 271 | $photoItem = [ 272 | 'title' => $photo->title, 273 | 'url' => self::getPhotoUrl($photo, $item->cmsPage, $theme), 274 | 'mtime' => $photo->updated_at, 275 | ]; 276 | $photoItem['isActive'] = ($photoItem['url'] == $url); 277 | $result['items'][] = $photoItem; 278 | } 279 | 280 | return $result; 281 | } 282 | 283 | 284 | /** 285 | * 286 | * Generates url for album 287 | * 288 | * @param Album $album 289 | * @param string $pageCode 290 | * @param Theme $theme 291 | * @return string 292 | */ 293 | protected static function getAlbumUrl($album, $pageCode, $theme) { 294 | $page = CmsPage::loadCached($theme, $pageCode); 295 | if (!$page) return ''; 296 | 297 | $properties = $page->getComponentProperties('photoAlbum'); 298 | if (!isset($properties['slug'])) { 299 | return ''; 300 | } 301 | 302 | if (!preg_match('/^\{\{([^\}]+)\}\}$/', $properties['slug'], $matches)) { 303 | return ''; 304 | } 305 | 306 | $paramName = substr(trim($matches[1]), 1); 307 | $params = [ 308 | $paramName => $album->slug, 309 | 'id' => $album->id, 310 | ]; 311 | $url = CmsPage::url($page->getBaseFileName(), $params); 312 | 313 | return $url; 314 | } 315 | 316 | 317 | /** 318 | * 319 | * Generates url for photo 320 | * 321 | * @param Photo $photo 322 | * @param string $pageCode 323 | * @param Theme $theme 324 | * @return string 325 | */ 326 | protected static function getPhotoUrl($photo, $pageCode, $theme) { 327 | $page = CmsPage::loadCached($theme, $pageCode); 328 | if (!$page) return ''; 329 | 330 | $properties = $page->getComponentProperties('singlePhoto'); 331 | if (!isset($properties['id'])) { 332 | return ''; 333 | } 334 | 335 | if (!preg_match('/^\{\{([^\}]+)\}\}$/', $properties['id'], $matches)) { 336 | return ''; 337 | } 338 | 339 | $paramName = substr(trim($matches[1]), 1); 340 | $params = [ 341 | $paramName => $photo->id, 342 | 'album_slug' => $photo->album->slug, 343 | ]; 344 | $url = CmsPage::url($page->getBaseFileName(), $params); 345 | 346 | return $url; 347 | } 348 | 349 | } 350 | -------------------------------------------------------------------------------- /components/Album.php: -------------------------------------------------------------------------------- 1 | 'graker.photoalbums::lang.plugin.album', 33 | 'description' => 'graker.photoalbums::lang.components.album_description' 34 | ]; 35 | } 36 | 37 | /** 38 | * @return array of component properties 39 | */ 40 | public function defineProperties() 41 | { 42 | return [ 43 | 'slug' => [ 44 | 'title' => 'graker.photoalbums::lang.plugin.slug_label', 45 | 'description' => 'graker.photoalbums::lang.plugin.slug_description', 46 | 'default' => '{{ :slug }}', 47 | 'type' => 'string' 48 | ], 49 | 'photoPage' => [ 50 | 'title' => 'graker.photoalbums::lang.components.photo_page_label', 51 | 'description' => 'graker.photoalbums::lang.components.photo_page_description', 52 | 'type' => 'dropdown', 53 | 'default' => 'photoalbums/album/photo', 54 | ], 55 | 'thumbMode' => [ 56 | 'title' => 'graker.photoalbums::lang.components.thumb_mode_label', 57 | 'description' => 'graker.photoalbums::lang.components.thumb_mode_description', 58 | 'type' => 'dropdown', 59 | 'default' => 'auto', 60 | ], 61 | 'thumbWidth' => [ 62 | 'title' => 'graker.photoalbums::lang.components.thumb_width_label', 63 | 'description' => 'graker.photoalbums::lang.components.thumb_width_description', 64 | 'default' => 640, 65 | 'type' => 'string', 66 | 'validationMessage' => 'graker.photoalbums::lang.errors.thumb_width_error', 67 | 'validationPattern' => '^[0-9]+$', 68 | 'required' => FALSE, 69 | ], 70 | 'thumbHeight' => [ 71 | 'title' => 'graker.photoalbums::lang.components.thumb_height_label', 72 | 'description' => 'graker.photoalbums::lang.components.thumb_height_description', 73 | 'default' => 480, 74 | 'type' => 'string', 75 | 'validationMessage' => 'graker.photoalbums::lang.errors.thumbs_height_error', 76 | 'validationPattern' => '^[0-9]+$', 77 | 'required' => FALSE, 78 | ], 79 | 'photosOnPage' => [ 80 | 'title' => 'graker.photoalbums::lang.components.photos_on_page_label', 81 | 'description' => 'graker.photoalbums::lang.components.photos_on_page_description', 82 | 'default' => 12, 83 | 'type' => 'string', 84 | 'validationMessage' => 'graker.photoalbums::lang.errors.photos_on_page_error', 85 | 'validationPattern' => '^[0-9]+$', 86 | 'required' => FALSE, 87 | ], 88 | ]; 89 | } 90 | 91 | /** 92 | * 93 | * Returns pages list for album page select box setting 94 | * 95 | * @return mixed 96 | */ 97 | public function getPhotoPageOptions() { 98 | return Page::sortBy('baseFileName')->lists('baseFileName', 'baseFileName'); 99 | } 100 | 101 | 102 | /** 103 | * 104 | * Returns thumb resize mode options for thumb mode select box setting 105 | * 106 | * @return array 107 | */ 108 | public function getThumbModeOptions() { 109 | return [ 110 | 'auto' => 'Auto', 111 | 'exact' => 'Exact', 112 | 'portrait' => 'Portrait', 113 | 'landscape' => 'Landscape', 114 | 'crop' => 'Crop', 115 | ]; 116 | } 117 | 118 | 119 | /** 120 | * Get photo page number from query 121 | */ 122 | protected function setCurrentPage() { 123 | if (isset($_GET['page'])) { 124 | if (ctype_digit($_GET['page']) && ($_GET['page'] > 0)) { 125 | $this->currentPage = $_GET['page']; 126 | } else { 127 | return FALSE; 128 | } 129 | } else { 130 | $this->currentPage = 1; 131 | } 132 | return TRUE; 133 | } 134 | 135 | 136 | /** 137 | * Loads album on onRun event 138 | */ 139 | public function onRun() { 140 | if (!$this->setCurrentPage()) { 141 | // if page parameter is invalid, redirect to the first page 142 | return Redirect::to($this->currentPageUrl() . '?page=1'); 143 | } 144 | $this->album = $this->page->album = $this->loadAlbum(); 145 | // if current page is greater than number of pages, redirect to the last page 146 | // check for > 1 to avoid infinite redirect when there are no photos 147 | if (($this->currentPage > 1) && ($this->currentPage > $this->lastPage)) { 148 | return Redirect::to($this->currentPageUrl() . '?page=' . $this->lastPage); 149 | } 150 | } 151 | 152 | 153 | /** 154 | * 155 | * Loads album model with it's photos 156 | * 157 | * @return AlbumModel 158 | */ 159 | protected function loadAlbum() { 160 | $slug = $this->property('slug'); 161 | $album = AlbumModel::where('slug', $slug) 162 | ->with(['photos' => function ($query) { 163 | $query->orderBy('created_at', 'desc'); 164 | $query->with('image'); 165 | $query->paginate($this->property('photosOnPage'), $this->currentPage); 166 | }]) 167 | ->first(); 168 | 169 | if ($album) { 170 | //prepare photo urls and thumbs 171 | foreach ($album->photos as $photo) { 172 | $photo->url = $photo->setUrl($this->property('photoPage'), $this->controller); 173 | $photo->thumb = $photo->image->getThumb( 174 | $this->property('thumbWidth'), 175 | $this->property('thumbHeight'), 176 | ['mode' => $this->property('thumbMode')] 177 | ); 178 | } 179 | //setup page numbers 180 | $this->lastPage = ceil($album->photosCount / $this->property('photosOnPage')); 181 | } 182 | 183 | return $album; 184 | } 185 | 186 | } 187 | -------------------------------------------------------------------------------- /components/AlbumList.php: -------------------------------------------------------------------------------- 1 | 'graker.photoalbums::lang.components.albums_list', 37 | 'description' => 'graker.photoalbums::lang.components.albums_list_description' 38 | ]; 39 | } 40 | 41 | /** 42 | * 43 | * Define properties 44 | * 45 | * @return array of component properties 46 | */ 47 | public function defineProperties() 48 | { 49 | return [ 50 | 'albumPage' => [ 51 | 'title' => 'graker.photoalbums::lang.components.album_page_label', 52 | 'description' => 'graker.photoalbums::lang.components.album_page_description', 53 | 'type' => 'dropdown', 54 | 'default' => 'photoalbums/album', 55 | ], 56 | 'thumbMode' => [ 57 | 'title' => 'graker.photoalbums::lang.components.thumb_mode_label', 58 | 'description' => 'graker.photoalbums::lang.components.thumb_mode_description', 59 | 'type' => 'dropdown', 60 | 'default' => 'auto', 61 | ], 62 | 'thumbWidth' => [ 63 | 'title' => 'graker.photoalbums::lang.components.thumb_width_label', 64 | 'description' => 'graker.photoalbums::lang.components.thumb_width_description', 65 | 'default' => 640, 66 | 'type' => 'string', 67 | 'validationMessage' => 'graker.photoalbums::lang.errors.thumb_width_error', 68 | 'validationPattern' => '^[0-9]+$', 69 | 'required' => FALSE, 70 | ], 71 | 'thumbHeight' => [ 72 | 'title' => 'graker.photoalbums::lang.components.thumb_height_label', 73 | 'description' => 'graker.photoalbums::lang.components.thumb_height_description', 74 | 'default' => 480, 75 | 'type' => 'string', 76 | 'validationMessage' => 'graker.photoalbums::lang.errors.thumb_height_error', 77 | 'validationPattern' => '^[0-9]+$', 78 | 'required' => FALSE, 79 | ], 80 | 'albumsOnPage' => [ 81 | 'title' => 'graker.photoalbums::lang.components.albums_on_page_label', 82 | 'description' => 'graker.photoalbums::lang.components.albums_on_page_description', 83 | 'default' => 12, 84 | 'type' => 'string', 85 | 'validationMessage' => 'graker.photoalbums::lang.errors.albums_on_page_error', 86 | 'validationPattern' => '^[0-9]+$', 87 | 'required' => FALSE, 88 | ], 89 | ]; 90 | } 91 | 92 | 93 | /** 94 | * 95 | * Returns pages list for album page select box setting 96 | * 97 | * @return mixed 98 | */ 99 | public function getAlbumPageOptions() { 100 | return Page::sortBy('baseFileName')->lists('baseFileName', 'baseFileName'); 101 | } 102 | 103 | 104 | /** 105 | * 106 | * Returns thumb resize mode options for thumb mode select box setting 107 | * 108 | * @return array 109 | */ 110 | public function getThumbModeOptions() { 111 | return [ 112 | 'auto' => 'Auto', 113 | 'exact' => 'Exact', 114 | 'portrait' => 'Portrait', 115 | 'landscape' => 'Landscape', 116 | 'crop' => 'Crop', 117 | ]; 118 | } 119 | 120 | 121 | /** 122 | * Get photo page number from query 123 | */ 124 | protected function setCurrentPage() { 125 | if (isset($_GET['page'])) { 126 | if (ctype_digit($_GET['page']) && ($_GET['page'] > 0)) { 127 | $this->currentPage = $_GET['page']; 128 | } else { 129 | return FALSE; 130 | } 131 | } else { 132 | $this->currentPage = 1; 133 | } 134 | return TRUE; 135 | } 136 | 137 | 138 | /** 139 | * OnRun implementation 140 | * Setup pager 141 | * Load albums 142 | */ 143 | public function onRun() { 144 | if (!$this->setCurrentPage()) { 145 | return Redirect::to($this->currentPageUrl() . '?page=1'); 146 | } 147 | $this->albums = $this->loadAlbums(); 148 | $this->prepareAlbums(); 149 | 150 | $this->lastPage = $this->albums->lastPage(); 151 | // if current page is greater than number of pages, redirect to the last page 152 | // only if lastPage > 0 to avoid redirect loop when there are no elements 153 | if ($this->lastPage && ($this->currentPage > $this->lastPage)) { 154 | return Redirect::to($this->currentPageUrl() . '?page=' . $this->lastPage); 155 | } 156 | } 157 | 158 | 159 | /** 160 | * 161 | * Returns array of site's albums to be used in component 162 | * Albums are sorted by created date desc, each one loaded with one latest photo (or photo set to be front) 163 | * Empty albums won't be displayed 164 | * 165 | * @return array 166 | */ 167 | protected function loadAlbums() { 168 | $albums = AlbumModel::orderBy('created_at', 'desc') 169 | ->has('photos') 170 | ->with(['latestPhoto' => function ($query) { 171 | $query->with('image'); 172 | }]) 173 | ->with(['front' => function ($query) { 174 | $query->with('image'); 175 | }]) 176 | ->with('photosCount') 177 | ->paginate($this->property('albumsOnPage'), $this->currentPage); 178 | 179 | return $albums; 180 | } 181 | 182 | 183 | /** 184 | * 185 | * Prepares array of album models to be displayed: 186 | * - set up album urls 187 | * - set up photo counts 188 | * - set up album thumb 189 | */ 190 | protected function prepareAlbums() { 191 | //set up photo count and url 192 | foreach ($this->albums as $album) { 193 | $album->photo_count = $album->photosCount; 194 | $album->url = $album->setUrl($this->property('albumPage'), $this->controller); 195 | 196 | // prepare thumb from $album->front if it is set or from latestPhoto otherwise 197 | $image = ($album->front) ? $album->front->image : $album->latestPhoto->image; 198 | $album->latestPhoto->thumb = $image->getThumb( 199 | $this->property('thumbWidth'), 200 | $this->property('thumbHeight'), 201 | ['mode' => $this->property('thumbMode')] 202 | ); 203 | } 204 | } 205 | 206 | } 207 | -------------------------------------------------------------------------------- /components/Photo.php: -------------------------------------------------------------------------------- 1 | 'graker.photoalbums::lang.plugin.photo', 16 | 'description' => 'graker.photoalbums::lang.components.photo_description' 17 | ]; 18 | } 19 | 20 | /** 21 | * 22 | * Properties of component 23 | * 24 | * @return array 25 | */ 26 | public function defineProperties() 27 | { 28 | return [ 29 | 'id' => [ 30 | 'title' => 'graker.photoalbums::lang.components.id_label', 31 | 'description' => 'graker.photoalbums::lang.components.id_description', 32 | 'default' => '{{ :id }}', 33 | 'type' => 'string' 34 | ], 35 | 'albumPage' => [ 36 | 'title' => 'graker.photoalbums::lang.components.album_page_label', 37 | 'description' => 'graker.photoalbums::lang.components.album_page_description', 38 | 'type' => 'dropdown', 39 | 'default' => 'photoalbums/album', 40 | ], 41 | 'photoPage' => [ 42 | 'title' => 'graker.photoalbums::lang.components.photo_page_label', 43 | 'description' => 'graker.photoalbums::lang.components.photo_page_description', 44 | 'type' => 'dropdown', 45 | 'default' => 'photoalbums/album/photo', 46 | ], 47 | ]; 48 | } 49 | 50 | 51 | /** 52 | * 53 | * Returns pages list for album page select box setting 54 | * 55 | * @return mixed 56 | */ 57 | public function getAlbumPageOptions() { 58 | return Page::sortBy('baseFileName')->lists('baseFileName', 'baseFileName'); 59 | } 60 | 61 | 62 | /** 63 | * 64 | * Returns pages list for photo page select box setting 65 | * 66 | * @return mixed 67 | */ 68 | public function getPhotoPageOptions() { 69 | return Page::sortBy('baseFileName')->lists('baseFileName', 'baseFileName'); 70 | } 71 | 72 | 73 | /** 74 | * Loads photo on onRun event 75 | */ 76 | public function onRun() { 77 | $this->photo = $this->page->photo = $this->loadPhoto(); 78 | } 79 | 80 | 81 | /** 82 | * 83 | * Loads photo to be displayed in this component 84 | * 85 | * @return PhotoModel 86 | */ 87 | protected function loadPhoto() { 88 | $id = $this->property('id'); 89 | $photo = PhotoModel::where('id', $id) 90 | ->with('image') 91 | ->with('album') 92 | ->first(); 93 | 94 | if ($photo) { 95 | // set url so we can have back link to the parent album 96 | $photo->album->url = $photo->album->setUrl($this->property('albumPage'), $this->controller); 97 | 98 | //set next and previous photos 99 | $photo->next = $photo->nextPhoto(); 100 | if ($photo->next) { 101 | $photo->next->url = $photo->next->setUrl($this->property('photoPage'), $this->controller); 102 | } 103 | $photo->previous = $photo->previousPhoto(); 104 | if ($photo->previous) { 105 | $photo->previous->url = $photo->previous->setUrl($this->property('photoPage'), $this->controller); 106 | } 107 | } 108 | 109 | return $photo; 110 | } 111 | 112 | } 113 | -------------------------------------------------------------------------------- /components/RandomPhotos.php: -------------------------------------------------------------------------------- 1 | 'graker.photoalbums::lang.components.random_photos', 17 | 'description' => 'graker.photoalbums::lang.components.random_photos_description', 18 | ]; 19 | } 20 | 21 | public function defineProperties() 22 | { 23 | return [ 24 | 'photosCount' => [ 25 | 'title' => 'graker.photoalbums::lang.components.photos_count_label', 26 | 'description' => 'graker.photoalbums::lang.components.photos_count_description', 27 | 'default' => 5, 28 | 'type' => 'string', 29 | 'validationMessage' => 'graker.photoalbums::lang.errors.photos_count_error', 30 | 'validationPattern' => '^[0-9]+$', 31 | 'required' => FALSE, 32 | ], 33 | 'cacheLifetime' => [ 34 | 'title' => 'graker.photoalbums::lang.components.cache_lifetime_label', 35 | 'description' => 'graker.photoalbums::lang.components.cache_lifetime_description', 36 | 'default' => 0, 37 | 'type' => 'string', 38 | 'validationMessage' => 'graker.photoalbums::lang.errors.cache_lifetime_error', 39 | 'validationPattern' => '^[0-9]+$', 40 | 'required' => FALSE, 41 | ], 42 | 'thumbMode' => [ 43 | 'title' => 'graker.photoalbums::lang.components.thumb_mode_label', 44 | 'description' => 'graker.photoalbums::lang.components.thumb_mode_description', 45 | 'type' => 'dropdown', 46 | 'default' => 'auto', 47 | ], 48 | 'thumbWidth' => [ 49 | 'title' => 'graker.photoalbums::lang.components.thumb_width_label', 50 | 'description' => 'graker.photoalbums::lang.components.thumb_width_description', 51 | 'default' => 640, 52 | 'type' => 'string', 53 | 'validationMessage' => 'graker.photoalbums::lang.errors.thumb_width_error', 54 | 'validationPattern' => '^[0-9]+$', 55 | 'required' => FALSE, 56 | ], 57 | 'thumbHeight' => [ 58 | 'title' => 'graker.photoalbums::lang.components.thumb_height_label', 59 | 'description' => 'graker.photoalbums::lang.components.thumb_height_description', 60 | 'default' => 480, 61 | 'type' => 'string', 62 | 'validationMessage' => 'graker.photoalbums::lang.errors.thumb_height_error', 63 | 'validationPattern' => '^[0-9]+$', 64 | 'required' => FALSE, 65 | ], 66 | 'photoPage' => [ 67 | 'title' => 'graker.photoalbums::lang.components.photo_page_label', 68 | 'description' => 'graker.photoalbums::lang.components.photo_page_description', 69 | 'type' => 'dropdown', 70 | 'default' => 'blog/post', 71 | ], 72 | ]; 73 | } 74 | 75 | 76 | /** 77 | * 78 | * Returns pages list for album page select box setting 79 | * 80 | * @return mixed 81 | */ 82 | public function getPhotoPageOptions() { 83 | return Page::sortBy('baseFileName')->lists('baseFileName', 'baseFileName'); 84 | } 85 | 86 | 87 | /** 88 | * 89 | * Returns thumb resize mode options for thumb mode select box setting 90 | * 91 | * @return array 92 | */ 93 | public function getThumbModeOptions() { 94 | return [ 95 | 'auto' => 'Auto', 96 | 'exact' => 'Exact', 97 | 'portrait' => 'Portrait', 98 | 'landscape' => 'Landscape', 99 | 'crop' => 'Crop', 100 | ]; 101 | } 102 | 103 | 104 | /** 105 | * 106 | * Returns an array of photosCount random photos 107 | * Array is returned if from Cache, Collection otherwise 108 | * 109 | * @return array|Collection 110 | */ 111 | public function photos() { 112 | $photos = []; 113 | if ($this->property('cacheLifetime')) { 114 | $photos = Cache::get('photoalbums_random_photos'); 115 | } 116 | 117 | if (empty($photos)) { 118 | $photos = $this->getPhotos(); 119 | } 120 | 121 | return $photos; 122 | } 123 | 124 | 125 | /** 126 | * 127 | * Returns a collection of random photos 128 | * Works for MySQL and Sqlite, for other drivers returns non-random selection 129 | * 130 | * @return Collection 131 | */ 132 | protected function getPhotos() { 133 | $count = $this->property('photosCount'); 134 | if (DB::connection()->getDriverName() == 'mysql') { 135 | $photos = PhotoModel::orderBy(DB::raw('RAND()')); 136 | } else if (DB::connection()->getDriverName() == 'sqlite') { 137 | $photos = PhotoModel::orderBy(DB::raw('RANDOM()')); 138 | } else { 139 | $photos = PhotoModel::orderBy('id'); 140 | } 141 | $photos = $photos->with('image')->take($count)->get(); 142 | 143 | foreach ($photos as $photo) { 144 | $photo->url = $photo->setUrl($this->property('photoPage'), $this->controller); 145 | $photo->thumb = $photo->image->getThumb( 146 | $this->property('thumbWidth'), 147 | $this->property('thumbHeight'), 148 | ['mode' => $this->property('thumbMode')] 149 | ); 150 | } 151 | 152 | $this->cachePhotos($photos); 153 | 154 | return $photos; 155 | } 156 | 157 | 158 | /** 159 | * 160 | * Cache photos if caching is enabled 161 | * 162 | * @param Collection $photos 163 | */ 164 | protected function cachePhotos($photos) { 165 | $cache = $this->property('cacheLifetime'); 166 | if ($cache) { 167 | Cache::put('photoalbums_random_photos', $photos->toArray(), $cache); 168 | } 169 | } 170 | 171 | } 172 | -------------------------------------------------------------------------------- /components/album/default.htm: -------------------------------------------------------------------------------- 1 | {% set album = __SELF__.album %} 2 | 3 |

{{ album.title }}

4 | 5 | {% if album.description %} 6 |
7 |
8 | {{ album.description|raw }} 9 |
10 |
11 | {% endif %} 12 | 13 |
14 | {% for photo in album.photos %} 15 |
16 | {{ photo.title }} 22 | 23 | {{ photo.title }} 24 |
25 | {% else %} 26 |
Album doesn't have any photos yet
27 | {% endfor %} 28 |
29 | 30 | {% if __SELF__.lastPage > 1 %} 31 | 46 | {% endif %} 47 | -------------------------------------------------------------------------------- /components/albumlist/default.htm: -------------------------------------------------------------------------------- 1 |
2 | {% for album in __SELF__.albums %} 3 |
4 |

{{ album.title }}

5 | 6 | 10 | 11 | Created on {{ album.created_at|date('M d, Y') }} 12 | {{ album.photo_count }} images 13 |
14 | {% else %} 15 |
You have not created any albums yet
16 | {% endfor %} 17 |
18 | 19 | {% if __SELF__.lastPage > 1 %} 20 | 35 | {% endif %} 36 | -------------------------------------------------------------------------------- /components/photo/default.htm: -------------------------------------------------------------------------------- 1 | {% set photo = __SELF__.photo %} 2 | 3 | {% if photo.title %} 4 |

{{ photo.title }}

5 | {% endif %} 6 |
7 |
8 | {{ photo.title }} 14 |
15 |
16 |
17 |
18 | {{ photo.created_at|date('Y/m/d') }} 19 | {{ photo.album.title }} 20 | {% if photo.description %} 21 | {{ photo.description | raw }} 22 | {% endif %} 23 |
24 |
25 | {% if photo.previous %} 26 | Previous photo 27 | {% else %} 28 | Previous photo 29 | {% endif %} 30 | {% if photo.next %} 31 | Next photo 32 | {% else %} 33 | Next photo 34 | {% endif %} 35 |
36 |
37 | -------------------------------------------------------------------------------- /components/randomphotos/default.htm: -------------------------------------------------------------------------------- 1 |
2 | {% for photo in __SELF__.photos() %} 3 |
4 | 5 | {% if photo.title %} 6 | {{ photo.title }} 7 | {% endif %} 8 |
9 | {% else %} 10 | You have not created any photos 11 | {% endfor %} 12 |
13 | -------------------------------------------------------------------------------- /controllers/Albums.php: -------------------------------------------------------------------------------- 1 | album_id != $album->id) { 64 | // attempt to use other album's photo 65 | throw new ApplicationException(Lang::get('graker.photoalbums::lang.errors.not_this_album')); 66 | } 67 | } catch (Exception $e) { 68 | return Response::json($e->getMessage(), 400); 69 | } 70 | 71 | // set front id 72 | $album->front_id = $photo->id; 73 | $album->save(); 74 | 75 | $this->initRelation($album, 'photos'); 76 | return $this->relationRefresh('photos'); 77 | } 78 | 79 | 80 | /** 81 | * 82 | * Returns path to reorder current album 83 | * 84 | * @return string 85 | */ 86 | protected function getReorderPath() { 87 | if (!isset($this->vars['formModel']->id)) { 88 | return ''; 89 | } 90 | 91 | $uri = \Backend::url('graker/photoalbums/reorder/album/' . $this->vars['formModel']->id); 92 | return $uri; 93 | } 94 | 95 | } 96 | -------------------------------------------------------------------------------- /controllers/Photos.php: -------------------------------------------------------------------------------- 1 | model = $album; 38 | $this->addJs('/modules/backend/behaviors/reordercontroller/assets/js/october.reorder.js', 'core'); 39 | 40 | $this->pageTitle = Lang::get('graker.photoalbums::lang.plugin.reorder_title', ['name' => $album->title]); 41 | 42 | return $this->makePartial('reorder', ['reorderRecords' => $this->model->photos,]); 43 | } 44 | 45 | 46 | /** 47 | * Callback to save reorder information 48 | * Calls function from Sortable trait on the model 49 | */ 50 | public function onReorder() { 51 | if (!$ids = post('record_ids')) return; 52 | if (!$orders = post('sort_orders')) return; 53 | 54 | $model = new Photo(); 55 | $model->setSortableOrder($ids, $orders); 56 | } 57 | 58 | 59 | /** 60 | * Reorder constructor 61 | */ 62 | public function __construct() 63 | { 64 | parent::__construct(); 65 | 66 | BackendMenu::setContext('Graker.PhotoAlbums', 'photoalbums', 'albums'); 67 | } 68 | 69 | } 70 | -------------------------------------------------------------------------------- /controllers/Upload.php: -------------------------------------------------------------------------------- 1 | pageTitle = Lang::get('graker.photoalbums::lang.plugin.upload_photos'); 30 | $this->addJs('/modules/backend/assets/vendor/dropzone/dropzone.js'); 31 | $this->addJs('/plugins/graker/photoalbums/assets/js/upload.js'); 32 | $this->addCss('/plugins/graker/photoalbums/assets/css/dropzone.css'); 33 | return $this->makePartial('form'); 34 | } 35 | 36 | 37 | /** 38 | * File upload controller 39 | */ 40 | public function post_files() { 41 | try { 42 | if (!Input::hasFile('file')) { 43 | throw new ApplicationException(Lang::get('graker.photoalbums::lang.errors.no_file')); 44 | } 45 | 46 | $upload = Input::file('file'); 47 | 48 | $validationRules = ['max:' . File::getMaxFilesize()]; 49 | 50 | $validation = Validator::make( 51 | ['file' => $upload], 52 | ['file' => $validationRules] 53 | ); 54 | if ($validation->fails()) { 55 | throw new ValidationException($validation); 56 | } 57 | if (!$upload->isValid()) { 58 | throw new ApplicationException(Lang::get('graker.photoalbums::lang.errors.invalid_file', ['name' => $upload->getClientOriginalName()])); 59 | } 60 | 61 | $file = new File; 62 | $file->data = $upload; 63 | $file->is_public = true; 64 | $file->save(); 65 | return Response::json(['id' => $file->id], 200); 66 | } catch (Exception $e) { 67 | return Response::json($e->getMessage(), 400); 68 | } 69 | } 70 | 71 | 72 | /** 73 | * Form save callback 74 | */ 75 | public function onSave() { 76 | $input = Input::all(); 77 | 78 | $album = AlbumModel::find($input['album']); 79 | if ($album && !empty($input['file-id'])) { 80 | $this->savePhotos($album, $input['file-id'], $input['file-title']); 81 | Flash::success(Lang::get('graker.photoalbums::lang.messages.photos_saved')); 82 | return Redirect::to(Backend::url('graker/photoalbums/albums/update/' . $album->id)); 83 | } 84 | 85 | Flash::error(Lang::get('graker.photoalbums::lang.errors.album_not_found')); 86 | return Redirect::to(Backend::url('graker/photoalbums/albums')); 87 | } 88 | 89 | 90 | /** 91 | * File remove callback 92 | */ 93 | public function onFileRemove() { 94 | if (Input::has('file_id')) { 95 | $file_id = Input::get('file_id'); 96 | $file = File::find($file_id); 97 | if ($file) { 98 | $file->delete(); 99 | } 100 | } 101 | } 102 | 103 | 104 | /** 105 | * 106 | * Saves photos with files attached from $file_ids and attaches them to album 107 | * 108 | * @param AlbumModel $album 109 | * @param array $file_ids 110 | * @param string[] $file_titles arrray of titles 111 | */ 112 | protected function savePhotos($album, $file_ids, $file_titles) { 113 | $files = File::whereIn('id', $file_ids)->get(); 114 | $photos = array(); 115 | foreach ($files as $file) { 116 | $photo = new PhotoModel(); 117 | $photo->title = isset($file_titles[$file->id]) ? $file_titles[$file->id] : ''; 118 | $photo->save(); 119 | $photo->image()->save($file); 120 | $photos[] = $photo; 121 | } 122 | $album->photos()->saveMany($photos); 123 | } 124 | 125 | 126 | /** 127 | * @return array of [album id => album title] to use in select list 128 | */ 129 | protected function getAlbumsList() { 130 | $albums = AlbumModel::orderBy('created_at', 'desc')->get(); 131 | $options = []; 132 | 133 | foreach ($albums as $album) { 134 | $options[$album->id] = $album->title; 135 | } 136 | 137 | return $options; 138 | } 139 | 140 | 141 | public function __construct() 142 | { 143 | parent::__construct(); 144 | 145 | BackendMenu::setContext('Graker.PhotoAlbums', 'photoalbums', 'upload'); 146 | } 147 | } 148 | -------------------------------------------------------------------------------- /controllers/albums/_list_toolbar.htm: -------------------------------------------------------------------------------- 1 |
2 | 5 | 6 | 7 |
-------------------------------------------------------------------------------- /controllers/albums/_relation_toolbar.htm: -------------------------------------------------------------------------------- 1 |
2 | 8 | trans($relationLabel)])) ?> 9 | 10 | 11 | 18 | 19 | 33 | 34 | 48 | 51 | 52 | 53 |
54 | -------------------------------------------------------------------------------- /controllers/albums/config_form.yaml: -------------------------------------------------------------------------------- 1 | # =================================== 2 | # Form Behavior Config 3 | # =================================== 4 | 5 | # Record name 6 | name: graker.photoalbums::lang.plugin.album 7 | 8 | # Model Form Field configuration 9 | form: $/graker/photoalbums/models/album/fields.yaml 10 | 11 | # Model Class name 12 | modelClass: Graker\Photoalbums\Models\Album 13 | 14 | # Default redirect location 15 | defaultRedirect: graker/photoalbums/albums 16 | 17 | # Create page 18 | create: 19 | title: graker.photoalbums::lang.plugin.create_album 20 | redirect: graker/photoalbums/albums/update/:id 21 | redirectClose: graker/photoalbums/albums 22 | 23 | # Update page 24 | update: 25 | title: graker.photoalbums::lang.plugin.edit_album 26 | redirect: graker/photoalbums/albums 27 | redirectClose: graker/photoalbums/albums 28 | 29 | # Preview page 30 | preview: 31 | title: graker.photoalbums::lang.plugin.preview_album -------------------------------------------------------------------------------- /controllers/albums/config_list.yaml: -------------------------------------------------------------------------------- 1 | # =================================== 2 | # List Behavior Config 3 | # =================================== 4 | 5 | # Model List Column configuration 6 | list: $/graker/photoalbums/models/album/columns.yaml 7 | 8 | # Model Class name 9 | modelClass: Graker\Photoalbums\Models\Album 10 | 11 | # List Title 12 | title: graker.photoalbums::lang.plugin.list_title 13 | 14 | # Link URL for each record 15 | recordUrl: graker/photoalbums/albums/update/:id 16 | 17 | # Message to display if the list is empty 18 | noRecordsMessage: backend::lang.list.no_records 19 | 20 | # Records to display per page 21 | recordsPerPage: 20 22 | 23 | # Displays the list column set up button 24 | showSetup: true 25 | 26 | # Displays the sorting link on each column 27 | showSorting: true 28 | 29 | # Default sorting column 30 | # defaultSort: 31 | # column: created_at 32 | # direction: desc 33 | 34 | # Display checkboxes next to each record 35 | # showCheckboxes: true 36 | 37 | # Toolbar widget configuration 38 | toolbar: 39 | # Partial for toolbar buttons 40 | buttons: list_toolbar 41 | 42 | # Search widget configuration 43 | search: 44 | prompt: backend::lang.list.search_prompt 45 | -------------------------------------------------------------------------------- /controllers/albums/config_relation.yaml: -------------------------------------------------------------------------------- 1 | # =================================== 2 | # Relation Behavior Config 3 | # =================================== 4 | 5 | photos: 6 | label: graker.photoalbums::lang.plugin.photo 7 | manage: 8 | form: $/graker/photoalbums/models/photo/fields.yaml 9 | list: $/graker/photoalbums/models/photo/columns.yaml 10 | view: 11 | list: $/graker/photoalbums/models/photo/columns.yaml 12 | toolbarPartial: relation_toolbar 13 | -------------------------------------------------------------------------------- /controllers/albums/create.htm: -------------------------------------------------------------------------------- 1 | 2 | 6 | 7 | 8 | fatalError): ?> 9 | 10 | 'layout']) ?> 11 | 12 |
13 | formRender() ?> 14 |
15 | 16 |
17 | relationRender('photos') ?> 18 |
19 | 20 |
21 |
22 | 30 | 39 | 40 | 41 | 42 |
43 |
44 | 45 | 46 | 47 | 48 | 49 |

fatalError) ?>

50 |

51 | 52 | -------------------------------------------------------------------------------- /controllers/albums/index.htm: -------------------------------------------------------------------------------- 1 | 2 | listRender() ?> 3 | -------------------------------------------------------------------------------- /controllers/albums/preview.htm: -------------------------------------------------------------------------------- 1 | 2 | 6 | 7 | 8 | fatalError): ?> 9 | 10 |
11 | formRenderPreview() ?> 12 |
13 | 14 | 15 | 16 |

fatalError) ?>

17 |

18 | 19 | -------------------------------------------------------------------------------- /controllers/albums/update.htm: -------------------------------------------------------------------------------- 1 | 2 | 6 | 7 | 8 | fatalError): ?> 9 | 10 | 'layout']) ?> 11 | 12 |
13 | formRender() ?> 14 |
15 | 16 |
17 | relationRender('photos') ?> 18 |
19 | 20 |
21 |
22 | 31 | 40 | 47 | 48 | 49 | 50 |
51 |
52 | 53 | 54 | 55 | 56 | 57 |

fatalError) ?>

58 |

59 | 60 | -------------------------------------------------------------------------------- /controllers/photos/_list_toolbar.htm: -------------------------------------------------------------------------------- 1 |
2 | 5 | 6 | 7 |
-------------------------------------------------------------------------------- /controllers/photos/config_form.yaml: -------------------------------------------------------------------------------- 1 | # =================================== 2 | # Form Behavior Config 3 | # =================================== 4 | 5 | # Record name 6 | name: graker.photoalbums::lang.plugin.photo 7 | 8 | # Model Form Field configuration 9 | form: $/graker/photoalbums/models/photo/fields.yaml 10 | 11 | # Model Class name 12 | modelClass: Graker\PhotoAlbums\Models\Photo 13 | 14 | # Default redirect location 15 | defaultRedirect: graker/photoalbums/photos 16 | 17 | # Create page 18 | create: 19 | title: graker.photoalbums::lang.plugin.create_photo 20 | redirect: graker/photoalbums/photos/update/:id 21 | redirectClose: graker/photoalbums/photos 22 | 23 | # Update page 24 | update: 25 | title: graker.photoalbums::lang.plugin.edit_photo 26 | redirect: graker/photoalbums/photos 27 | redirectClose: graker/photoalbums/photos 28 | 29 | # Preview page 30 | preview: 31 | title: graker.photoalbums::lang.plugin.preview_photo -------------------------------------------------------------------------------- /controllers/photos/config_list.yaml: -------------------------------------------------------------------------------- 1 | # =================================== 2 | # List Behavior Config 3 | # =================================== 4 | 5 | # Model List Column configuration 6 | list: $/graker/photoalbums/models/photo/columns.yaml 7 | 8 | # Model Class name 9 | modelClass: Graker\PhotoAlbums\Models\Photo 10 | 11 | # List Title 12 | title: graker.photoalbums::lang.plugin.manage_photos 13 | 14 | # Link URL for each record 15 | recordUrl: graker/photoalbums/photos/update/:id 16 | 17 | # Message to display if the list is empty 18 | noRecordsMessage: backend::lang.list.no_records 19 | 20 | # Records to display per page 21 | recordsPerPage: 20 22 | 23 | # Displays the list column set up button 24 | showSetup: true 25 | 26 | # Displays the sorting link on each column 27 | showSorting: true 28 | 29 | # Toolbar widget configuration 30 | toolbar: 31 | # Partial for toolbar buttons 32 | buttons: list_toolbar 33 | 34 | # Search widget configuration 35 | search: 36 | prompt: backend::lang.list.search_prompt 37 | -------------------------------------------------------------------------------- /controllers/photos/create.htm: -------------------------------------------------------------------------------- 1 | 2 | 6 | 7 | 8 | fatalError): ?> 9 | 10 | 'layout']) ?> 11 | 12 |
13 | formRender() ?> 14 |
15 | 16 |
17 |
18 | 26 | 35 | 36 | 37 | 38 |
39 |
40 | 41 | 42 | 43 | 44 | 45 |

fatalError) ?>

46 |

47 | 48 | -------------------------------------------------------------------------------- /controllers/photos/index.htm: -------------------------------------------------------------------------------- 1 | 2 | listRender() ?> 3 | -------------------------------------------------------------------------------- /controllers/photos/preview.htm: -------------------------------------------------------------------------------- 1 | 2 | 6 | 7 | 8 | fatalError): ?> 9 | 10 |
11 | formRenderPreview() ?> 12 |
13 | 14 | 15 | 16 |

fatalError) ?>

17 |

18 | 19 | -------------------------------------------------------------------------------- /controllers/photos/update.htm: -------------------------------------------------------------------------------- 1 | 2 | 6 | 7 | 8 | fatalError): ?> 9 | 10 | 'layout']) ?> 11 | 12 |
13 | formRender() ?> 14 |
15 | 16 |
17 |
18 | 27 | 36 | 43 | 44 | 45 | 46 |
47 |
48 | 49 | 50 | 51 | 52 | 53 |

fatalError) ?>

54 |

55 | 56 | -------------------------------------------------------------------------------- /controllers/reorder/_records.htm: -------------------------------------------------------------------------------- 1 | 2 | 3 |
  • 4 |
    5 | 6 | title ?> 7 | 8 |
    9 |
  • 10 | 11 | 12 | -------------------------------------------------------------------------------- /controllers/reorder/_reorder.htm: -------------------------------------------------------------------------------- 1 | 2 | 7 | 8 | 9 | fatalError): ?> 10 | 11 | 12 |
    19 | 20 |
      21 | makePartial('records', ['records' => $reorderRecords]) ?> 22 |
    23 | 24 |

    25 | 26 |
    27 | 28 | 29 | 32 | 33 | 34 |

    fatalError) ?>

    35 |

    36 | -------------------------------------------------------------------------------- /controllers/upload/_form.htm: -------------------------------------------------------------------------------- 1 | 2 | 6 | 7 | 8 | fatalError): ?> 9 | TRUE, 'class' => 'layout',]) ?> 10 |
    11 |
    12 | 13 | getAlbumsList()) ?> 14 |
    15 |
    16 |
    17 |
    18 | 19 |
    20 |
    21 |
    22 |
    23 |
    24 |
    25 |
    26 |
    27 | 28 |
    29 |
    30 | 38 | 39 | 40 | 41 |
    42 |
    43 | 44 | 45 | 46 | 47 | 48 |

    fatalError) ?>

    49 |

    50 | 51 | 52 | -------------------------------------------------------------------------------- /lang/en/lang.php: -------------------------------------------------------------------------------- 1 | [ 5 | 'name' => 'Photo Albums', 6 | 'description' => 'Create, display and manage galleries of photos arranged in albums.', 7 | 'settings_description' => 'Photo Albums plugin settings', 8 | 'tab' => 'Photo Albums', 9 | 'manage_albums' => 'Manage photo albums', 10 | 'access_permission' => 'Access Settings', 11 | 'upload_photos' => 'Upload photos', 12 | 'new_album' => 'New album', 13 | 'create_album' => 'Create album', 14 | 'edit_album' => 'Edit album', 15 | 'preview_album' => 'Preview album', 16 | 'creating_album' => 'Creating album...', 17 | 'saving_album' => 'Saving album...', 18 | 'deleting_album' => 'Deleting album...', 19 | 'list_title' => 'Manage albums', 20 | 'album' => 'Album', 21 | 'albums' => 'Albums', 22 | 'manage_photos' => 'Manage photos', 23 | 'new_photo' => 'New photo', 24 | 'create_photo' => 'Create photo', 25 | 'edit_photo' => 'Edit photo', 26 | 'preview_photo' => 'Preview photo', 27 | 'creating_photo' => 'Creating photo...', 28 | 'saving_photo' => 'Saving photo...', 29 | 'deleting_photo' => 'Deleting photo...', 30 | 'photo' => 'Photo', 31 | 'photos' => 'Photos', 32 | 'photo_description' => 'Description', 33 | 'set_front_button' => 'Set as front', 34 | 'reorder_button' => 'Reorder photos', 35 | 'bool_positive' => 'Yes', 36 | 'reorder_title' => 'Reorder album :name', 37 | 'reorder' => 'Reorder', 38 | 'saving_upload' => 'Saving upload...', 39 | 'upload_photos_title' => 'Upload multiple photos', 40 | 'album_to_upload' => 'Album to upload to', 41 | 'save_upload' => 'Save upload', 42 | 'title_label' => 'Title', 43 | 'title_placeholder_album' => 'Album title', 44 | 'title_placeholder_photo' => 'Photo title', 45 | 'created_label' => 'Created', 46 | 'updated_label' => 'Updated', 47 | 'slug_label' => 'Slug', 48 | 'slug_description' => 'URL slug parameter', 49 | 'slug_placeholder_album' => 'album-title', 50 | 'description_label' => 'Description', 51 | 'front_label' => 'Front', 52 | 'code_label' => 'Code', 53 | 'code_description' => 'Type in default markdown to use for photo insert. There are two placeholders: %id% and %title%, they will be replaced with photo id and photo title automatically.', 54 | 'selecting_photo' => 'Selecting photo', 55 | 'insert' => 'Insert', 56 | 'not_selected' => 'Not selected', 57 | 'back_to_albums' => 'Back to albums', 58 | 'all_photo_albums' => 'All Photo Albums', 59 | 'all_photos' => 'All Photos', 60 | ], 61 | 'errors' => [ 62 | 'album_not_found' => 'Album not found!', 63 | 'cant_find_selected' => 'Can\'t find selected photo!', 64 | 'not_this_album' => 'Selected photo doesn\'t belong to this album!', 65 | 'return_to_albums' => 'Return to albums list', 66 | 'return_to_photos' => 'Return to photos list', 67 | 'no_file' => 'No file in request', 68 | 'invalid_file' => 'File :name is not valid.', 69 | 'thumb_width_error' => 'Thumb width must be a number', 70 | 'thumb_height_error' => 'Thumb height must be a number', 71 | 'photos_on_page_error' => 'Photos on page value must be a number', 72 | 'albums_on_page_error' => 'Albums on page value must be a number', 73 | 'photos_count_error' => 'Photos count must be a number', 74 | 'cache_lifetime_error' => 'Cache lifetime must be a number', 75 | 'no_albums' => 'You don\'t have any albums yet.', 76 | ], 77 | 'messages' => [ 78 | 'set_front' => 'Are you sure to set this photo as front for the album?', 79 | 'delete' => 'Do you really want to delete this album?', 80 | 'delete_photo' => 'Do you really want to delete this photo?', 81 | 'photos_saved' => 'Photos are saved!', 82 | ], 83 | 'components' => [ 84 | 'photo_description' => 'Single photo component', 85 | 'album_description' => 'Component to output one photo album with all its photos.', 86 | 'photo_page_label' => 'Photo page', 87 | 'photo_page_description' => 'Page used to display a single photo', 88 | 'thumb_mode_label' => 'Thumb mode', 89 | 'thumb_mode_description' => 'Mode of thumb generation', 90 | 'thumb_width_label' => 'Thumb width', 91 | 'thumb_width_description' => 'Width of the thumb to be generated', 92 | 'thumb_height_label' => 'Thumb height', 93 | 'thumb_height_description' => 'Height of the thumb to be generated', 94 | 'photos_on_page_label' => 'Photos on page', 95 | 'photos_on_page_description' => 'Amount of photos on one page (to use in pagination)', 96 | 'albums_on_page_label' => 'Albums on page', 97 | 'albums_on_page_description' => 'Amount of albums on one page (to use in pagination)', 98 | 'albums_list' => 'Albums list', 99 | 'albums_list_description' => 'Lists all photo albums on site', 100 | 'album_page_label' => 'Album page', 101 | 'album_page_description' => 'Page used to display photo albums', 102 | 'id_label' => 'ID', 103 | 'id_description' => 'Photo id parameter', 104 | 'random_photos' => 'Random Photos', 105 | 'random_photos_description' => 'Output predefined number of random photos', 106 | 'photos_count_label' => 'Photos to output', 107 | 'photos_count_description' => 'Amount of random photos to output', 108 | 'cache_lifetime_label' => 'Cache lifetime', 109 | 'cache_lifetime_description' => 'Number of minutes selected photos are stored in cache. 0 for no caching.', 110 | ], 111 | ]; 112 | -------------------------------------------------------------------------------- /lang/es/lang.php: -------------------------------------------------------------------------------- 1 | [ 10 | 'name' => 'Photo Albums', 11 | 'description' => 'Crear, mostrar y gestionar galerías de fotos ordenadas en álbumes.', 12 | 'settings_description' => 'Ajustes del plugin Photo Albums', 13 | 'tab' => 'Álbumes de fotos', 14 | 'manage_albums' => 'Gestionar álbumes de fotos', 15 | 'access_permission' => 'Ajustes de permisos de acceso', 16 | 'upload_photos' => 'Subir fotos', 17 | 'new_album' => 'Nuevo álbum', 18 | 'create_album' => 'Crear álbum', 19 | 'edit_album' => 'Editar álbum', 20 | 'preview_album' => 'Previsualizar álbum', 21 | 'creating_album' => 'Creando álbum...', 22 | 'saving_album' => 'Guardando álbum...', 23 | 'deleting_album' => 'Eliminando álbum...', 24 | 'list_title' => 'Gestionar álbumes', 25 | 'album' => 'Álbum', 26 | 'albums' => 'Álbumes', 27 | 'manage_photos' => 'Gestionar fotos', 28 | 'new_photo' => 'Nueva foto', 29 | 'create_photo' => 'Crear foto', 30 | 'edit_photo' => 'Editar foto', 31 | 'preview_photo' => 'Previsualizar foto', 32 | 'creating_photo' => 'Creando foto...', 33 | 'saving_photo' => 'Guardando photo...', 34 | 'deleting_photo' => 'Eliminando foto...', 35 | 'photo' => 'Foto', 36 | 'photos' => 'Fotos', 37 | 'photo_description' => 'Descripción', 38 | 'set_front_button' => 'Poner como portada', 39 | 'reorder_button' => 'Reordenar fotos', 40 | 'bool_positive' => 'Sí', 41 | 'reorder_title' => 'Reordenar álbum :name', 42 | 'reorder' => 'Reordenar', 43 | 'saving_upload' => 'Guardando subida...', 44 | 'upload_photos_title' => 'Subir múltiples fotos', 45 | 'album_to_upload' => 'Álbum al que subir', 46 | 'save_upload' => 'Guardar subida', 47 | 'title_label' => 'Título', 48 | 'title_placeholder_album' => 'Título del Álbum', 49 | 'title_placeholder_photo' => 'Título de la Foto', 50 | 'created_label' => 'Creación', 51 | 'updated_label' => 'Actualización', 52 | 'slug_label' => 'Identificador', 53 | 'slug_description' => 'Parámetro identificador para la URL (dirección)', 54 | 'slug_placeholder_album' => 'titulo-del-album', 55 | 'description_label' => 'Descripción', 56 | 'front_label' => 'Portada', 57 | 'code_label' => 'Código', 58 | 'code_description' => 'Pon el código markdown para usar para las inserciones de fotos. Hay dos variables: %id% y %title%, serán reemplazadas con la id y el título de la foto automáticamente.', 59 | 'selecting_photo' => 'Seleccionando foto', 60 | 'insert' => 'Insertar', 61 | 'not_selected' => 'Sin seleccionar', 62 | 'back_to_albums' => 'Volver a álbumes', 63 | 'all_photo_albums' => 'Todos los álbumes de fotos', 64 | 'all_photos' => 'Todas las fotos', 65 | ], 66 | 'errors' => [ 67 | 'album_not_found' => '¡Álbum encontrado!', 68 | 'cant_find_selected' => '¡No se encuentra la foto seleccionada!', 69 | 'not_this_album' => '¡La foto seleccionada no pertenece a éste álbum!', 70 | 'return_to_albums' => 'Volver a la lista de álbumes', 71 | 'return_to_photos' => 'Volver a la lista de fotos', 72 | 'no_file' => 'No hay archivo en la petición', 73 | 'invalid_file' => 'El archivo :name no es válido.', 74 | 'thumb_width_error' => 'El ancho de miniatura debe ser un número', 75 | 'thumb_height_error' => 'La altura de la miniatura debe ser un número', 76 | 'photos_on_page_error' => 'El valor de fotos por página debe ser un número', 77 | 'albums_on_page_error' => 'El valor de álbumes por página debe ser un número', 78 | 'photos_count_error' => 'La cantidad de fotos debe ser un número', 79 | 'cache_lifetime_error' => 'La vida de la caché debe ser un número', 80 | 'no_albums' => 'No tienes álbumes aún.', 81 | ], 82 | 'messages' => [ 83 | 'set_front' => '¿Seguro de quieres poner ésta foto como la portada del álbum?', 84 | 'delete' => '¿Seguro que quieres eliminar éste álbum?Do you really want to delete this álbum?', 85 | 'delete_photo' => '¿Seguro que quieres eliminar ésta foto?', 86 | 'photos_saved' => '¡Fotos guardadas!', 87 | ], 88 | 'components' => [ 89 | 'photo_description' => 'Componente de foto suelta', 90 | 'album_description' => 'Componente para obtener un álbum de fotos junto con todas sus fotos.', 91 | 'photo_page_label' => 'Página de foto', 92 | 'photo_page_description' => 'Página usada para mostrar una foto suelta', 93 | 'thumb_mode_label' => 'Modo dce miniaturas', 94 | 'thumb_mode_description' => 'Modo de generación de las miniaturas', 95 | 'thumb_width_label' => 'Ancho de miniatura', 96 | 'thumb_width_description' => 'Ancho de las miniaturas que se generarán', 97 | 'thumb_height_label' => 'Alto de Miniatura', 98 | 'thumb_height_description' => 'Altura de las miniaturas que se generarán', 99 | 'photos_on_page_label' => 'Fotos por página', 100 | 'photos_on_page_description' => 'Cantidad de fotos a mostrar en una página (para usar en la paginación)', 101 | 'albums_on_page_label' => 'Álbumes por página', 102 | 'albums_on_page_description' => 'Cantidad de álbumes a mostrar en una página (para usar en la paginación)', 103 | 'albums_list' => 'Lista de álbumes', 104 | 'albums_list_description' => 'Lista todos los álbumes de fotos en el sitio', 105 | 'album_page_label' => 'Página de álbum', 106 | 'album_page_description' => 'Página usada para mostrar los álbumes de fotos', 107 | 'id_label' => 'ID', 108 | 'id_description' => 'Parámetro de identifiación de la foto', 109 | 'random_photos' => 'Fotos aleatorias', 110 | 'random_photos_description' => 'Muestra un número de fotos aleatorias prefefinido', 111 | 'photos_count_label' => 'Fotos a mostrar', 112 | 'photos_count_description' => 'Cantidad de fotos aleatorias para mostrar', 113 | 'cache_lifetime_label' => 'Vida de la caché', 114 | 'cache_lifetime_description' => 'Número de minutos en que las fotos seleccionadas se guardan en caché. 0 para desactivar.', 115 | ], 116 | ]; 117 | -------------------------------------------------------------------------------- /models/Album.php: -------------------------------------------------------------------------------- 1 | 'required', 22 | 'slug' => ['required', 'regex:/^[a-z0-9\/\:_\-\*\[\]\+\?\|]*$/i', 'unique:graker_photoalbums_albums'], 23 | ]; 24 | 25 | /** 26 | * @var array Relations 27 | */ 28 | public $hasMany = [ 29 | 'photos' => [ 30 | 'Graker\PhotoAlbums\Models\Photo', 31 | 'order' => 'sort_order desc', 32 | ] 33 | ]; 34 | public $belongsTo = [ 35 | 'user' => ['Backend\Models\User'], 36 | 'front' => ['Graker\PhotoAlbums\Models\Photo'], 37 | ]; 38 | 39 | 40 | /** 41 | * 42 | * This relation allows us to eager-load 1 latest photo per album 43 | * 44 | * @return mixed 45 | */ 46 | public function latestPhoto() { 47 | return $this->hasOne('Graker\PhotoAlbums\Models\Photo')->latest(); 48 | } 49 | 50 | 51 | /** 52 | * 53 | * This relation allows us to count photos 54 | * 55 | * @return mixed 56 | */ 57 | public function photosCount() { 58 | return $this->hasOne('Graker\PhotoAlbums\Models\Photo') 59 | ->selectRaw('album_id, count(*) as aggregate') 60 | ->orderBy('album_id') 61 | ->groupBy('album_id'); 62 | } 63 | 64 | 65 | /** 66 | * 67 | * Getter for photos count 68 | * 69 | * @return int 70 | */ 71 | public function getPhotosCountAttribute() { 72 | // if relation is not loaded already, let's do it first 73 | if (!array_key_exists('photosCount', $this->relations)) { 74 | $this->load('photosCount'); 75 | } 76 | $related = $this->getRelation('photosCount'); 77 | 78 | return ($related) ? (int) $related->aggregate : 0; 79 | } 80 | 81 | 82 | /** 83 | * 84 | * Returns image file of photo set as album front or image in the latest photo of the album 85 | * 86 | * @return File 87 | */ 88 | public function getImage() { 89 | if ($this->front) { 90 | return $this->front->image; 91 | } 92 | 93 | if ($this->latestPhoto) { 94 | return $this->latestPhoto->image; 95 | } 96 | 97 | return NULL; 98 | } 99 | 100 | 101 | /** 102 | * 103 | * Sets and returns url for this model using provided page name and controller 104 | * For now we expose just id and slug for URL parameters 105 | * 106 | * @param string $pageName 107 | * @param CMS\Classes\Controller $controller 108 | * @return string 109 | */ 110 | public function setUrl($pageName, $controller) { 111 | $params = [ 112 | 'id' => $this->id, 113 | 'slug' => $this->slug, 114 | ]; 115 | 116 | return $this->url = $controller->pageUrl($pageName, $params); 117 | } 118 | 119 | } 120 | -------------------------------------------------------------------------------- /models/Photo.php: -------------------------------------------------------------------------------- 1 | 'required', 25 | ]; 26 | 27 | /** 28 | * @var array of fillable fields to use in mass assignment 29 | */ 30 | protected $fillable = [ 31 | 'title', 'description', 32 | ]; 33 | 34 | /** 35 | * @var array Relations 36 | */ 37 | public $belongsTo = [ 38 | 'user' => ['Backend\Models\User'], 39 | 'album' => ['Graker\PhotoAlbums\Models\Album'], 40 | ]; 41 | public $attachOne = [ 42 | 'image' => ['System\Models\File'], 43 | ]; 44 | 45 | 46 | /** 47 | * 48 | * Returns next photo or NULL if this is the last in the album 49 | * 50 | * @return Photo 51 | */ 52 | public function nextPhoto() { 53 | $next = NULL; 54 | $current_found = FALSE; 55 | 56 | foreach ($this->album->photos as $photo) { 57 | if ($current_found) { 58 | // previous iteration was current photo, so we found the next one 59 | $next = $photo; 60 | break; 61 | } 62 | if ($photo->id == $this->id) { 63 | $current_found = TRUE; 64 | } 65 | } 66 | 67 | return $next; 68 | } 69 | 70 | 71 | /** 72 | * 73 | * Returns previous photo or NULL if this is the first in the album 74 | * 75 | * @return Photo 76 | */ 77 | public function previousPhoto() { 78 | $previous = NULL; 79 | 80 | foreach ($this->album->photos as $photo) { 81 | if ($photo->id == $this->id) { 82 | // found current photo 83 | break; 84 | } else { 85 | $previous = $photo; 86 | } 87 | } 88 | 89 | return $previous; 90 | } 91 | 92 | 93 | /** 94 | * 95 | * Sets and returns url for this model using provided page name and controller 96 | * For now we expose photo id and album's slug 97 | * 98 | * @param string $pageName 99 | * @param CMS\Classes\Controller $controller 100 | * @return string 101 | */ 102 | public function setUrl($pageName, $controller) { 103 | $params = [ 104 | 'id' => $this->id, 105 | 'album_slug' => $this->album->slug, 106 | ]; 107 | 108 | return $this->url = $controller->pageUrl($pageName, $params); 109 | } 110 | 111 | 112 | /** 113 | * beforeDelete() event 114 | * Using it to delete attached 115 | */ 116 | public function beforeDelete() { 117 | if ($this->image) { 118 | $this->image->delete(); 119 | } 120 | } 121 | 122 | } 123 | -------------------------------------------------------------------------------- /models/Settings.php: -------------------------------------------------------------------------------- 1 | 2 | 13 | 14 | 15 | ./tests 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | -------------------------------------------------------------------------------- /tests/RandomPhotosTest.php: -------------------------------------------------------------------------------- 1 | createAlbum(); 26 | $photos[] = $this->createPhoto($album); 27 | $photos[] = $this->createPhoto($album); 28 | $photos[] = $this->createPhoto($album); 29 | $photos[] = $this->createPhoto($album); 30 | $photos[] = $this->createPhoto($album); 31 | $photos[] = $this->createPhoto($album); 32 | $photos[] = $this->createPhoto($album); 33 | 34 | // get random photos 35 | $component = $this->createRandomPhotosComponent(); 36 | $random_photos = $component->photos(); 37 | 38 | // assert all photos are from generated array 39 | self::assertEquals(5, count($random_photos), 'There are 5 random photos'); 40 | $found_all = TRUE; 41 | foreach ($random_photos as $random_photo) { 42 | $found = FALSE; 43 | foreach ($photos as $photo) { 44 | if ($photo->id == $random_photo->id) { 45 | $found = TRUE; 46 | break; 47 | } 48 | } 49 | if (!$found) { 50 | $found_all = FALSE; 51 | break; 52 | } 53 | } 54 | self::assertTrue($found_all, 'All photos exist in original array'); 55 | } 56 | 57 | 58 | /** 59 | * 60 | * Creates album model 61 | * 62 | * @return \Graker\PhotoAlbums\Models\Album 63 | */ 64 | protected function createAlbum() { 65 | $faker = Faker\Factory::create(); 66 | $album = new Album(); 67 | $album->title = $faker->sentence(3); 68 | $album->slug = str_slug($album->title); 69 | $album->description = $faker->text(); 70 | $album->save(); 71 | return $album; 72 | } 73 | 74 | 75 | /** 76 | * 77 | * Creates photo model and put it into album 78 | * 79 | * @param \Graker\PhotoAlbums\Models\Album $album 80 | * @return \Graker\PhotoAlbums\Models\Photo 81 | */ 82 | protected function createPhoto(Album $album) { 83 | $faker = Faker\Factory::create(); 84 | $photo = new Photo(); 85 | $photo->title = $faker->sentence(3); 86 | $photo->description = $faker->text(); 87 | $photo->image = $faker->image(); 88 | $photo->album = $album; 89 | $photo->save(); 90 | return $photo; 91 | } 92 | 93 | 94 | /** 95 | * 96 | * Creates randomPhotos component to test 97 | * 98 | * @return \Graker\PhotoAlbums\Components\RandomPhotos 99 | */ 100 | protected function createRandomPhotosComponent() { 101 | // Spoof all the objects we need to make a page object 102 | $theme = Theme::load('test'); 103 | $page = Page::load($theme, 'index.htm'); 104 | $layout = Layout::load($theme, 'content.htm'); 105 | $controller = new Controller($theme); 106 | $parser = new CodeParser($page); 107 | $pageObj = $parser->source($page, $layout, $controller); 108 | $manager = ComponentManager::instance(); 109 | $object = $manager->makeComponent('randomPhotos', $pageObj); 110 | return $object; 111 | } 112 | } 113 | -------------------------------------------------------------------------------- /updates/add_album_front.php: -------------------------------------------------------------------------------- 1 | integer('front_id')->unsigned()->nullable(); 14 | }); 15 | } 16 | 17 | public function down() 18 | { 19 | Schema::table('graker_photoalbums_albums', function($table) 20 | { 21 | $table->dropColumn('front_id'); 22 | }); 23 | } 24 | 25 | } 26 | -------------------------------------------------------------------------------- /updates/add_sort_order_field.php: -------------------------------------------------------------------------------- 1 | integer('sort_order')->unsigned()->nullable(); 14 | }); 15 | } 16 | 17 | public function down() 18 | { 19 | Schema::table('graker_photoalbums_photos', function($table) 20 | { 21 | $table->dropColumn('sort_order'); 22 | }); 23 | } 24 | 25 | } 26 | -------------------------------------------------------------------------------- /updates/create_albums_table.php: -------------------------------------------------------------------------------- 1 | engine = 'InnoDB'; 14 | $table->increments('id'); 15 | $table->integer('user_id')->unsigned()->nullable()->index(); 16 | $table->string('title')->nullable(); 17 | $table->string('slug')->index(); 18 | $table->text('description')->nullable(); 19 | $table->timestamps(); 20 | }); 21 | } 22 | 23 | public function down() 24 | { 25 | Schema::dropIfExists('graker_photoalbums_albums'); 26 | } 27 | 28 | } 29 | -------------------------------------------------------------------------------- /updates/create_photos_table.php: -------------------------------------------------------------------------------- 1 | engine = 'InnoDB'; 14 | $table->increments('id'); 15 | $table->integer('user_id')->unsigned()->nullable()->index(); 16 | $table->integer('album_id')->unsigned()->nullable()->index(); 17 | $table->string('title')->nullable(); 18 | $table->text('description')->nullable(); 19 | $table->timestamps(); 20 | }); 21 | } 22 | 23 | public function down() 24 | { 25 | Schema::dropIfExists('graker_photoalbums_photos'); 26 | } 27 | 28 | } 29 | -------------------------------------------------------------------------------- /updates/update_sort_order_on_existing_photos.php: -------------------------------------------------------------------------------- 1 | sort_order = $photo->id; 15 | $photo->save(); 16 | } 17 | } 18 | 19 | public function down() 20 | { 21 | } 22 | 23 | } 24 | -------------------------------------------------------------------------------- /updates/version.yaml: -------------------------------------------------------------------------------- 1 | 1.0.1: First version of PhotoAlbums 2 | 1.0.2: 3 | - Update with migrations to create albums and photos table 4 | - create_albums_table.php 5 | - create_photos_table.php 6 | 1.1.0: 7 | - Add ability to select front photo for album from the interface 8 | - add_album_front.php 9 | 1.2.0: 10 | - Added ability to reorder photos in the album 11 | - add_sort_order_field.php 12 | 1.2.1: 13 | - Fill default sort_order values for existing photos 14 | - update_sort_order_on_existing_photos.php 15 | 1.2.2: 16 | - Sqlite support for RandomPhotos component 17 | 1.2.3: 18 | - Added helper method to get album's cover photo 19 | 1.2.4: 20 | - Fix for album front photo eager loading 21 | 1.2.5: 22 | - Fix for photos count in only_full_group_by sql mode 23 | 1.3.0: 24 | - New dialog to insert photos into blog posts 25 | 1.4.0: 26 | - Integration with RainLab.Pages to use Albums and Photos in Menu Items (and Sitemap) 27 | 1.4.1: 28 | - Improved layout of Photo form (thanks to gergo85) 29 | - Improved lang.php strings (thanks to gergo85) 30 | - Localized Menu Item types 31 | 1.4.2: 32 | - Fixed second insert photo icon from occuring in blog post form 33 | -------------------------------------------------------------------------------- /widgets/PhotoSelector.php: -------------------------------------------------------------------------------- 1 | vars['albums'] = NULL; 38 | $this->vars['album'] = $this->album($album_id); 39 | } else { 40 | $this->vars['albums'] = $this->albums(); 41 | $this->vars['album'] = NULL; 42 | } 43 | 44 | 45 | return $this->makePartial('body'); 46 | } 47 | 48 | 49 | /** 50 | * Loads widget assets 51 | */ 52 | protected function loadAssets() { 53 | $this->addJs('js/photoselector.js'); 54 | $this->addCss('css/photoselector.css'); 55 | } 56 | 57 | 58 | /** 59 | * 60 | * Callback for when the dialog is initially open 61 | * 62 | * @return string 63 | */ 64 | public function onDialogOpen() { 65 | return $this->render(); 66 | } 67 | 68 | 69 | /** 70 | * 71 | * Callback to generate albums list 72 | * 73 | * @return array 74 | */ 75 | public function onAlbumListLoad() { 76 | $this->vars['albums'] = $this->albums(); 77 | 78 | return [ 79 | '#listContainer' => $this->makePartial('albums'), 80 | ]; 81 | } 82 | 83 | 84 | /** 85 | * 86 | * Callback to generate photos list 87 | * Photos list is to replace albums list in dialog markup 88 | * 89 | * @return array 90 | */ 91 | public function onAlbumLoad() { 92 | $album_id = input('id'); 93 | $album = $this->album($album_id); 94 | $this->vars['album'] = $album; 95 | 96 | return [ 97 | '#listContainer' => $this->makePartial('photos'), 98 | ]; 99 | } 100 | 101 | 102 | /** 103 | * 104 | * Returns a collection of all user's albums 105 | * 106 | * @return Collection 107 | */ 108 | protected function albums() { 109 | $albums = Album::orderBy('created_at', 'desc') 110 | ->has('photos') 111 | ->with(['latestPhoto' => function ($query) { 112 | $query->with('image'); 113 | }]) 114 | ->with(['front' => function ($query) { 115 | $query->with('image'); 116 | }]) 117 | ->get(); 118 | 119 | foreach ($albums as $album) { 120 | // prepare thumb from $album->front if it is set or from latestPhoto otherwise 121 | $image = ($album->front) ? $album->front->image : $album->latestPhoto->image; 122 | $album->thumb = $image->getThumb( 123 | 160, 124 | 120, 125 | ['mode' => 'crop'] 126 | ); 127 | } 128 | 129 | return $albums; 130 | } 131 | 132 | 133 | /** 134 | * 135 | * Returns album with its photos loaded and prepared for display in dialog 136 | * 137 | * @param int $album_id 138 | * @return Album 139 | */ 140 | protected function album($album_id) { 141 | $album = Album::where('id', $album_id) 142 | ->with(['photos' => function ($query) { 143 | $query->orderBy('sort_order', 'desc'); 144 | $query->with('image'); 145 | // TODO implement pagination 146 | }]) 147 | ->first(); 148 | 149 | if ($album) { 150 | //prepare photo urls and thumbs 151 | foreach ($album->photos as $photo) { 152 | // set thumb 153 | $photo->thumb = $photo->image->getThumb( 154 | 160, 155 | 120, 156 | ['mode' => 'crop'] 157 | ); 158 | // set code 159 | $photo->code = $this->createPhotoCode($photo); 160 | } 161 | } 162 | 163 | return $album; 164 | } 165 | 166 | 167 | /** 168 | * 169 | * Create an insert markdown code for photo from plugin settings 170 | * 171 | * @param Photo $photo 172 | * @return string 173 | */ 174 | protected function createPhotoCode($photo) { 175 | $code_template = Settings::get('code', '![%title%]([photo:%id%])'); 176 | $code = str_replace( 177 | array('%id%', '%title%'), 178 | array($photo->id, $photo->title), 179 | $code_template 180 | ); 181 | return $code; 182 | } 183 | 184 | } 185 | -------------------------------------------------------------------------------- /widgets/photoselector/assets/css/photoselector.css: -------------------------------------------------------------------------------- 1 | #photosList .photo-link.image-link { 2 | display: inline-block; 3 | padding: 7px; 4 | } 5 | 6 | #photosList .photo-link.image-link.selected { 7 | border: 1px solid; 8 | padding: 6px; 9 | border-radius: 6px; 10 | } 11 | 12 | #photosList .photo-link.title-link.selected { 13 | font-weight: bold; 14 | } 15 | 16 | #photosList .back-to-albums { 17 | margin-top: 16px; 18 | margin-bottom: 16px; 19 | display: block; 20 | } 21 | -------------------------------------------------------------------------------- /widgets/photoselector/assets/js/photoselector.js: -------------------------------------------------------------------------------- 1 | 2 | /** 3 | * PhotoSelector dialog 4 | */ 5 | 6 | +function () { 7 | 8 | if ($.oc.photoselector === undefined) { 9 | $.oc.photoselector = {}; 10 | } 11 | 12 | var Base = $.oc.foundation.base, 13 | BaseProto = Base.prototype; 14 | 15 | var PhotoSelector = function (options) { 16 | this.$dialog = $('
    '); 17 | this.options = $.extend({}, PhotoSelector.DEFAULTS, options); 18 | 19 | Base.call(this); 20 | 21 | this.show(); 22 | }; 23 | 24 | 25 | PhotoSelector.prototype = Object.create(BaseProto); 26 | PhotoSelector.prototype.constructor = PhotoSelector; 27 | 28 | 29 | /** 30 | * Load and show the dialog 31 | */ 32 | PhotoSelector.prototype.show = function () { 33 | this.$dialog.one('complete.oc.popup', this.proxy(this.onPopupShown)); 34 | this.$dialog.popup({ 35 | size: 'large', 36 | extraData: {album: this.options.album }, 37 | handler: this.options.alias + '::onDialogOpen' 38 | }); 39 | }; 40 | 41 | 42 | /** 43 | * Callback when the popup is loaded and shown 44 | * 45 | * @param event 46 | * @param element 47 | * @param popup 48 | */ 49 | PhotoSelector.prototype.onPopupShown = function (event, element, popup) { 50 | this.$dialog = popup; 51 | // bind clicks for album thumb and title links 52 | if (this.options.album) { 53 | this.bindPhotosListHandlers(); 54 | } else { 55 | $('#albumsList .album-link', popup).one('click', this.proxy(this.onAlbumClicked)); 56 | } 57 | $('div.photo-selection-dialog').find('button.btn-insert').click(this.proxy(this.onInsertClicked)); 58 | }; 59 | 60 | 61 | /** 62 | * Album clicked callback 63 | * @param event 64 | */ 65 | PhotoSelector.prototype.onAlbumClicked = function (event) { 66 | var link_id = $(event.currentTarget).data('request-data'); 67 | var selector = this; 68 | $.request('onAlbumLoad', { 69 | data: {id: link_id}, 70 | update: {photos: '#listContainer'}, 71 | loading: $.oc.stripeLoadIndicator, 72 | success: function (data) { 73 | this.success(data); 74 | selector.bindPhotosListHandlers(); 75 | } 76 | }); 77 | }; 78 | 79 | 80 | /** 81 | * Bind event handlers for photos list 82 | */ 83 | PhotoSelector.prototype.bindPhotosListHandlers = function () { 84 | // bind photo link click and double click events 85 | $('#photosList').find('a.photo-link').click(this.proxy(this.onPhotoSelected)); 86 | $('#photosList').find('a.photo-link').dblclick(this.proxy(this.onPhotoDoubleClicked)); 87 | // bind back to albums click event 88 | $('#photosList').find('a.back-to-albums').one('click', this.proxy(this.onBackToAlbums)); 89 | }; 90 | 91 | 92 | /** 93 | * 94 | * Photo clicked callback 95 | * 96 | * @param event 97 | */ 98 | PhotoSelector.prototype.onPhotoSelected = function (event) { 99 | // remove old selected classes 100 | $('#photosList').find('a.selected').removeClass('selected'); 101 | 102 | // add new selected classes 103 | var wrapper = $(event.currentTarget).parents('.photo-links-wrapper'); 104 | wrapper.find('a.photo-link').addClass('selected'); 105 | }; 106 | 107 | 108 | /** 109 | * 110 | * Back to albums clicked callback 111 | * 112 | * @param event 113 | */ 114 | PhotoSelector.prototype.onBackToAlbums = function (event) { 115 | var selector = this; 116 | $.request('onAlbumListLoad', { 117 | 'update': { albums: '#listContainer'}, 118 | loading: $.oc.stripeLoadIndicator, 119 | success: function (data) { 120 | this.success(data); 121 | $('#albumsList').find('.album-link').one('click', selector.proxy(selector.onAlbumClicked)); 122 | } 123 | }); 124 | }; 125 | 126 | 127 | /** 128 | * Photo insert button callback 129 | * 130 | * @param event 131 | */ 132 | PhotoSelector.prototype.onInsertClicked = function (event) { 133 | var selected = $('#photosList').find('a.selected').first(); 134 | if (!selected.length) { 135 | // FIXME Localize when it is supported 136 | alert('You have to select a photo first. Click on the photo, then click "Insert". Or just double-click the photo.'); 137 | } else { 138 | var code = selected.data('request-data'); 139 | var album = $('#photosList').data('request-data'); 140 | this.options.onInsert.call(this, code, album); 141 | } 142 | }; 143 | 144 | 145 | /** 146 | * 147 | * Double click callback 148 | * 149 | * @param event 150 | */ 151 | PhotoSelector.prototype.onPhotoDoubleClicked = function (event) { 152 | // select the photo and insert it 153 | var link = $(event.currentTarget); 154 | link.trigger('click'); 155 | $('div.photo-selection-dialog').find('button.btn-insert').trigger('click'); 156 | }; 157 | 158 | 159 | /** 160 | * Hide popup 161 | */ 162 | PhotoSelector.prototype.hide = function () { 163 | if (this.$dialog) { 164 | this.$dialog.trigger('close.oc.popup'); 165 | } 166 | }; 167 | 168 | 169 | /** 170 | * Default options 171 | */ 172 | PhotoSelector.DEFAULTS = { 173 | alias: undefined, 174 | album: 0, 175 | onInsert: undefined 176 | }; 177 | 178 | $.oc.photoselector.popup = PhotoSelector; 179 | 180 | } (window.jQuery); 181 | -------------------------------------------------------------------------------- /widgets/photoselector/partials/_albums.htm: -------------------------------------------------------------------------------- 1 |
    2 | 3 |
    4 |

    5 | 6 | 7 | 8 |

    9 |

    10 | 11 | title; ?> 12 | 13 |

    14 |
    15 | 16 |
    17 | -------------------------------------------------------------------------------- /widgets/photoselector/partials/_body.htm: -------------------------------------------------------------------------------- 1 | 22 | -------------------------------------------------------------------------------- /widgets/photoselector/partials/_photos.htm: -------------------------------------------------------------------------------- 1 |
    2 |

    title; ?>

    3 | photos as $photo) : ?> 4 | 22 | 23 |
    24 | 25 |
    26 |
    27 | --------------------------------------------------------------------------------