beautiful_photons 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (63) hide show
  1. checksums.yaml +7 -0
  2. data/MIT-LICENSE +20 -0
  3. data/README.md +28 -0
  4. data/Rakefile +11 -0
  5. data/app/assets/javascripts/beautiful_photons/bulk_select_controller.js +76 -0
  6. data/app/assets/javascripts/beautiful_photons/crop_editor_controller.js +108 -0
  7. data/app/assets/javascripts/beautiful_photons/focal_point_controller.js +16 -0
  8. data/app/assets/javascripts/beautiful_photons/index.js +13 -0
  9. data/app/assets/javascripts/beautiful_photons/reorder_controller.js +45 -0
  10. data/app/assets/stylesheets/beautiful_photons/application.css +15 -0
  11. data/app/controllers/beautiful_photons/admin/base_controller.rb +10 -0
  12. data/app/controllers/beautiful_photons/admin/categories_controller.rb +37 -0
  13. data/app/controllers/beautiful_photons/admin/galleries_controller.rb +100 -0
  14. data/app/controllers/beautiful_photons/admin/photos_controller.rb +75 -0
  15. data/app/controllers/beautiful_photons/admin/standalones_controller.rb +54 -0
  16. data/app/controllers/beautiful_photons/api/base_controller.rb +15 -0
  17. data/app/controllers/beautiful_photons/api/v1/galleries_controller.rb +60 -0
  18. data/app/controllers/beautiful_photons/api/v1/gallery_photos_controller.rb +60 -0
  19. data/app/controllers/beautiful_photons/api/v1/photos_controller.rb +65 -0
  20. data/app/controllers/beautiful_photons/application_controller.rb +4 -0
  21. data/app/controllers/beautiful_photons/standalone_photos_controller.rb +16 -0
  22. data/app/helpers/beautiful_photons/application_helper.rb +4 -0
  23. data/app/helpers/beautiful_photons/photo_helper.rb +103 -0
  24. data/app/jobs/beautiful_photons/application_job.rb +4 -0
  25. data/app/mailers/beautiful_photons/application_mailer.rb +6 -0
  26. data/app/models/beautiful_photons/application_record.rb +5 -0
  27. data/app/models/beautiful_photons/category.rb +7 -0
  28. data/app/models/beautiful_photons/gallery.rb +17 -0
  29. data/app/models/beautiful_photons/gallery_photo.rb +22 -0
  30. data/app/models/beautiful_photons/photo.rb +29 -0
  31. data/app/models/beautiful_photons/standalone.rb +23 -0
  32. data/app/views/beautiful_photons/admin/categories/index.html.erb +28 -0
  33. data/app/views/beautiful_photons/admin/galleries/add_photos_page.html.erb +30 -0
  34. data/app/views/beautiful_photons/admin/galleries/edit.html.erb +17 -0
  35. data/app/views/beautiful_photons/admin/galleries/index.html.erb +13 -0
  36. data/app/views/beautiful_photons/admin/galleries/new.html.erb +17 -0
  37. data/app/views/beautiful_photons/admin/galleries/show.html.erb +84 -0
  38. data/app/views/beautiful_photons/admin/photos/edit.html.erb +35 -0
  39. data/app/views/beautiful_photons/admin/photos/index.html.erb +60 -0
  40. data/app/views/beautiful_photons/admin/photos/new.html.erb +10 -0
  41. data/app/views/beautiful_photons/admin/photos/show.html.erb +66 -0
  42. data/app/views/beautiful_photons/admin/standalones/change_photo.html.erb +39 -0
  43. data/app/views/beautiful_photons/admin/standalones/edit_crop.html.erb +38 -0
  44. data/app/views/beautiful_photons/admin/standalones/edit_mobile_crop.html.erb +37 -0
  45. data/app/views/beautiful_photons/admin/standalones/index.html.erb +27 -0
  46. data/app/views/beautiful_photons/admin/standalones/show.html.erb +52 -0
  47. data/app/views/beautiful_photons/standalone_photos/show.html.erb +27 -0
  48. data/app/views/layouts/beautiful_photons/admin.html.erb +37 -0
  49. data/config/importmap.rb +5 -0
  50. data/config/routes.rb +36 -0
  51. data/db/migrate/20260318000001_create_beautiful_photons_photos.rb +15 -0
  52. data/db/migrate/20260318000002_create_beautiful_photons_galleries.rb +13 -0
  53. data/db/migrate/20260318000003_create_beautiful_photons_gallery_photos.rb +15 -0
  54. data/db/migrate/20260318000004_create_beautiful_photons_standalones.rb +13 -0
  55. data/db/migrate/20260318000005_add_crop_to_beautiful_photons_standalones.rb +12 -0
  56. data/db/migrate/20260318185017_create_beautiful_photons_categories.rb +11 -0
  57. data/db/migrate/20260318194125_add_category_to_beautiful_photons_photos.rb +6 -0
  58. data/lib/beautiful_photons/engine.rb +30 -0
  59. data/lib/beautiful_photons/version.rb +3 -0
  60. data/lib/beautiful_photons.rb +31 -0
  61. data/lib/generators/beautiful_photons/install_generator.rb +24 -0
  62. data/lib/tasks/beautiful_photons_tasks.rake +4 -0
  63. metadata +147 -0
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: dc71fb4c64aaa2737993fec77cf061891a14bd3e0afd0a0793348fcd084a4fe4
4
+ data.tar.gz: f7428c0d6c45ebe3e8826e3ab40a474b71af803bbca0c1c9bd9dea571bb40fdd
5
+ SHA512:
6
+ metadata.gz: d691b43d0baba68b43866a11fe9e73e8037a08014655ad08754a95fe36babe3fd82cd6a508dca6403b2f6bd6775783dcc5956604a9696687c2a23a212e954524
7
+ data.tar.gz: b5217d76a14f64cf8a15556e00b13aec39f96a24ad4c473d9074caa34ae4305318f7691a22cd091647484d16d41e663cdccc64ad55db4f0c1266daaa5ae515fc
data/MIT-LICENSE ADDED
@@ -0,0 +1,20 @@
1
+ Copyright tylercschneider
2
+
3
+ Permission is hereby granted, free of charge, to any person obtaining
4
+ a copy of this software and associated documentation files (the
5
+ "Software"), to deal in the Software without restriction, including
6
+ without limitation the rights to use, copy, modify, merge, publish,
7
+ distribute, sublicense, and/or sell copies of the Software, and to
8
+ permit persons to whom the Software is furnished to do so, subject to
9
+ the following conditions:
10
+
11
+ The above copyright notice and this permission notice shall be
12
+ included in all copies or substantial portions of the Software.
13
+
14
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
15
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
16
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
17
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
18
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
19
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
20
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,28 @@
1
+ # BeautifulPhotons
2
+ Short description and motivation.
3
+
4
+ ## Usage
5
+ How to use my plugin.
6
+
7
+ ## Installation
8
+ Add this line to your application's Gemfile:
9
+
10
+ ```ruby
11
+ gem "beautiful_photons"
12
+ ```
13
+
14
+ And then execute:
15
+ ```bash
16
+ $ bundle
17
+ ```
18
+
19
+ Or install it yourself as:
20
+ ```bash
21
+ $ gem install beautiful_photons
22
+ ```
23
+
24
+ ## Contributing
25
+ Contribution directions go here.
26
+
27
+ ## License
28
+ The gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT).
data/Rakefile ADDED
@@ -0,0 +1,11 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "rake/testtask"
4
+
5
+ Rake::TestTask.new(:test) do |t|
6
+ t.libs << "test"
7
+ t.pattern = "test/**/*_test.rb"
8
+ t.verbose = false
9
+ end
10
+
11
+ task default: :test
@@ -0,0 +1,76 @@
1
+ import { Controller } from "@hotwired/stimulus"
2
+
3
+ export default class extends Controller {
4
+ static targets = ["checkbox", "removeButton", "selectAllButton", "unselectAllButton", "modal", "modalMessage", "form"]
5
+ static values = { message: { type: String, default: "Remove {count} from this gallery?" } }
6
+
7
+ connect() {
8
+ this.updateButton()
9
+ }
10
+
11
+ toggle() {
12
+ this.updateButton()
13
+ }
14
+
15
+ selectAll() {
16
+ this.checkboxTargets.forEach(cb => cb.checked = true)
17
+ this.updateButton()
18
+ }
19
+
20
+ unselectAll() {
21
+ this.checkboxTargets.forEach(cb => cb.checked = false)
22
+ this.updateButton()
23
+ }
24
+
25
+ updateButton() {
26
+ const count = this.selectedCount
27
+ const total = this.checkboxTargets.length
28
+ const btn = this.removeButtonTarget
29
+ btn.disabled = count === 0
30
+ btn.style.opacity = count === 0 ? "0.4" : "1"
31
+ btn.style.pointerEvents = count === 0 ? "none" : "auto"
32
+
33
+ if (this.hasSelectAllButtonTarget) {
34
+ const allSelected = count === total
35
+ this.selectAllButtonTarget.disabled = allSelected
36
+ this.selectAllButtonTarget.style.opacity = allSelected ? "0.4" : "1"
37
+ this.selectAllButtonTarget.style.pointerEvents = allSelected ? "none" : "auto"
38
+ }
39
+
40
+ if (this.hasUnselectAllButtonTarget) {
41
+ const noneSelected = count === 0
42
+ this.unselectAllButtonTarget.disabled = noneSelected
43
+ this.unselectAllButtonTarget.style.opacity = noneSelected ? "0.4" : "1"
44
+ this.unselectAllButtonTarget.style.pointerEvents = noneSelected ? "none" : "auto"
45
+ }
46
+ }
47
+
48
+ showConfirm(event) {
49
+ event.preventDefault()
50
+ const count = this.selectedCount
51
+ if (count === 0) return
52
+
53
+ const label = `${count} photo${count === 1 ? "" : "s"}`
54
+ this.modalMessageTarget.textContent = this.messageValue.replace("{count}", label)
55
+ this.modalTarget.classList.remove("hidden")
56
+ }
57
+
58
+ confirmRemove() {
59
+ this.modalTarget.classList.add("hidden")
60
+ this.formTarget.requestSubmit()
61
+ }
62
+
63
+ cancelRemove() {
64
+ this.modalTarget.classList.add("hidden")
65
+ }
66
+
67
+ closeOnBackdrop(event) {
68
+ if (event.target === this.modalTarget) {
69
+ this.cancelRemove()
70
+ }
71
+ }
72
+
73
+ get selectedCount() {
74
+ return this.checkboxTargets.filter(cb => cb.checked).length
75
+ }
76
+ }
@@ -0,0 +1,108 @@
1
+ import { Controller } from "@hotwired/stimulus"
2
+
3
+ export default class extends Controller {
4
+ static targets = ["image", "viewport", "cropX", "cropY", "cropZoom", "zoomSlider"]
5
+ static values = { aspect: String }
6
+
7
+ connect() {
8
+ this.dragging = false
9
+ this.dragStartX = 0
10
+ this.dragStartY = 0
11
+ this.startCropX = 0
12
+ this.startCropY = 0
13
+ this.viewportTarget.style.display = "block"
14
+ this.updateViewport()
15
+ }
16
+
17
+ updateViewport() {
18
+ const cropX = parseFloat(this.cropXTarget.value)
19
+ const cropY = parseFloat(this.cropYTarget.value)
20
+ const zoom = parseFloat(this.cropZoomTarget.value)
21
+
22
+ // Viewport size is inverse of zoom — higher zoom = smaller viewport (tighter crop)
23
+ const viewportPct = Math.min(100, 100 / zoom)
24
+ const [aw, ah] = this.parseAspect()
25
+
26
+ // Calculate viewport dimensions that fit the aspect ratio within the image
27
+ const imgRect = this.imageTarget.getBoundingClientRect()
28
+ const imgAspect = imgRect.width / imgRect.height
29
+ const targetAspect = aw / ah
30
+
31
+ let vpW, vpH
32
+ if (targetAspect > imgAspect) {
33
+ // Wider than image — constrain by width
34
+ vpW = viewportPct
35
+ vpH = viewportPct * (imgAspect / targetAspect)
36
+ } else {
37
+ // Taller than image — constrain by height
38
+ vpH = viewportPct
39
+ vpW = viewportPct * (targetAspect / imgAspect)
40
+ }
41
+
42
+ // Position: crop_x/crop_y is the center of the viewport as % of image
43
+ const left = cropX - vpW / 2
44
+ const top = cropY - vpH / 2
45
+
46
+ const vp = this.viewportTarget
47
+ vp.style.width = `${vpW}%`
48
+ vp.style.height = `${vpH}%`
49
+ vp.style.left = `${this.clamp(left, 0, 100 - vpW)}%`
50
+ vp.style.top = `${this.clamp(top, 0, 100 - vpH)}%`
51
+
52
+ if (this.hasZoomSliderTarget) {
53
+ this.zoomSliderTarget.value = zoom
54
+ }
55
+ }
56
+
57
+ startDrag(event) {
58
+ event.preventDefault()
59
+ this.dragging = true
60
+ const touch = event.touches ? event.touches[0] : event
61
+ this.dragStartX = touch.clientX
62
+ this.dragStartY = touch.clientY
63
+ this.startCropX = parseFloat(this.cropXTarget.value)
64
+ this.startCropY = parseFloat(this.cropYTarget.value)
65
+
66
+ const move = (e) => this.drag(e)
67
+ const stop = () => {
68
+ this.dragging = false
69
+ document.removeEventListener("mousemove", move)
70
+ document.removeEventListener("mouseup", stop)
71
+ document.removeEventListener("touchmove", move)
72
+ document.removeEventListener("touchend", stop)
73
+ }
74
+
75
+ document.addEventListener("mousemove", move)
76
+ document.addEventListener("mouseup", stop)
77
+ document.addEventListener("touchmove", move, { passive: false })
78
+ document.addEventListener("touchend", stop)
79
+ }
80
+
81
+ drag(event) {
82
+ if (!this.dragging) return
83
+ event.preventDefault()
84
+
85
+ const touch = event.touches ? event.touches[0] : event
86
+ const imgRect = this.imageTarget.getBoundingClientRect()
87
+ const dx = (touch.clientX - this.dragStartX) / imgRect.width * 100
88
+ const dy = (touch.clientY - this.dragStartY) / imgRect.height * 100
89
+
90
+ this.cropXTarget.value = this.clamp(this.startCropX + dx, 0, 100).toFixed(1)
91
+ this.cropYTarget.value = this.clamp(this.startCropY + dy, 0, 100).toFixed(1)
92
+ this.updateViewport()
93
+ }
94
+
95
+ sliderZoom(event) {
96
+ this.cropZoomTarget.value = parseFloat(event.target.value).toFixed(2)
97
+ this.updateViewport()
98
+ }
99
+
100
+ parseAspect() {
101
+ const parts = (this.aspectValue || "1:1").split(":")
102
+ return [parseFloat(parts[0]), parseFloat(parts[1])]
103
+ }
104
+
105
+ clamp(val, min, max) {
106
+ return Math.max(min, Math.min(max, val))
107
+ }
108
+ }
@@ -0,0 +1,16 @@
1
+ import { Controller } from "@hotwired/stimulus"
2
+
3
+ export default class extends Controller {
4
+ static targets = ["image", "pin", "focalX", "focalY"]
5
+
6
+ pick(event) {
7
+ const rect = this.imageTarget.getBoundingClientRect()
8
+ const x = ((event.clientX - rect.left) / rect.width * 100).toFixed(1)
9
+ const y = ((event.clientY - rect.top) / rect.height * 100).toFixed(1)
10
+
11
+ this.focalXTarget.value = x
12
+ this.focalYTarget.value = y
13
+ this.pinTarget.style.left = `${x}%`
14
+ this.pinTarget.style.top = `${y}%`
15
+ }
16
+ }
@@ -0,0 +1,13 @@
1
+ import FocalPointController from "beautiful_photons/focal_point_controller"
2
+ import ReorderController from "beautiful_photons/reorder_controller"
3
+ import CropEditorController from "beautiful_photons/crop_editor_controller"
4
+ import BulkSelectController from "beautiful_photons/bulk_select_controller"
5
+
6
+ export function registerControllers(application) {
7
+ application.register("focal-point", FocalPointController)
8
+ application.register("reorder", ReorderController)
9
+ application.register("crop-editor", CropEditorController)
10
+ application.register("bulk-select", BulkSelectController)
11
+ }
12
+
13
+ export { FocalPointController, ReorderController, CropEditorController, BulkSelectController }
@@ -0,0 +1,45 @@
1
+ import { Controller } from "@hotwired/stimulus"
2
+
3
+ export default class extends Controller {
4
+ static targets = ["item"]
5
+ static values = { url: String }
6
+
7
+ get photoIds() {
8
+ return this.itemTargets.map((item) => item.dataset.photoId)
9
+ }
10
+
11
+ dragstart(event) {
12
+ this.draggedItem = event.currentTarget
13
+ event.dataTransfer.effectAllowed = "move"
14
+ }
15
+
16
+ dragover(event) {
17
+ event.preventDefault()
18
+ const target = event.currentTarget
19
+ if (target === this.draggedItem) return
20
+
21
+ const rect = target.getBoundingClientRect()
22
+ const midY = rect.top + rect.height / 2
23
+
24
+ if (event.clientY < midY) {
25
+ target.parentNode.insertBefore(this.draggedItem, target)
26
+ } else {
27
+ target.parentNode.insertBefore(this.draggedItem, target.nextSibling)
28
+ }
29
+ }
30
+
31
+ async drop(event) {
32
+ event.preventDefault()
33
+ await this.persist()
34
+ }
35
+
36
+ async persist() {
37
+ await fetch(this.urlValue, {
38
+ method: "PATCH",
39
+ headers: {
40
+ "Content-Type": "application/json",
41
+ },
42
+ body: JSON.stringify({ photo_ids: this.photoIds }),
43
+ })
44
+ }
45
+ }
@@ -0,0 +1,15 @@
1
+ /*
2
+ * This is a manifest file that'll be compiled into application.css, which will include all the files
3
+ * listed below.
4
+ *
5
+ * Any CSS and SCSS file within this directory, lib/assets/stylesheets, vendor/assets/stylesheets,
6
+ * or any plugin's vendor/assets/stylesheets directory can be referenced here using a relative path.
7
+ *
8
+ * You're free to add application-wide styles to this file and they'll appear at the bottom of the
9
+ * compiled file so the styles you add here take precedence over styles defined in any other CSS/SCSS
10
+ * files in this directory. Styles in this file should be added after the last require_* statement.
11
+ * It is generally better to create a new file per style scope.
12
+ *
13
+ *= require_tree .
14
+ *= require_self
15
+ */
@@ -0,0 +1,10 @@
1
+ module BeautifulPhotons
2
+ module Admin
3
+ class BaseController < ::ApplicationController
4
+ before_action { send(BeautifulPhotons.config.authentication_method) }
5
+ layout "beautiful_photons/admin"
6
+
7
+ helper KeystoneUi::Engine.helpers
8
+ end
9
+ end
10
+ end
@@ -0,0 +1,37 @@
1
+ # frozen_string_literal: true
2
+
3
+ module BeautifulPhotons
4
+ module Admin
5
+ class CategoriesController < BaseController
6
+ skip_forgery_protection
7
+
8
+ def index
9
+ @categories = BeautifulPhotons::Category.all.order(:name)
10
+ @category = BeautifulPhotons::Category.new
11
+ end
12
+
13
+ def create
14
+ @category = BeautifulPhotons::Category.new(category_params)
15
+
16
+ if @category.save
17
+ redirect_to beautiful_photons.categories_path, notice: "Category created."
18
+ else
19
+ @categories = BeautifulPhotons::Category.all.order(:name)
20
+ render :index, status: :unprocessable_entity
21
+ end
22
+ end
23
+
24
+ def destroy
25
+ @category = BeautifulPhotons::Category.find(params[:id])
26
+ @category.destroy!
27
+ redirect_to beautiful_photons.categories_path, notice: "Category deleted."
28
+ end
29
+
30
+ private
31
+
32
+ def category_params
33
+ params.require(:category).permit(:name)
34
+ end
35
+ end
36
+ end
37
+ end
@@ -0,0 +1,100 @@
1
+ # frozen_string_literal: true
2
+
3
+ module BeautifulPhotons
4
+ module Admin
5
+ class GalleriesController < BaseController
6
+ skip_forgery_protection
7
+
8
+ def index
9
+ @galleries = BeautifulPhotons::Gallery.all.order(:title)
10
+ end
11
+
12
+ def new
13
+ @gallery = BeautifulPhotons::Gallery.new
14
+ end
15
+
16
+ def create
17
+ @gallery = BeautifulPhotons::Gallery.new(gallery_params)
18
+
19
+ if @gallery.save
20
+ redirect_to beautiful_photons.gallery_path(@gallery)
21
+ else
22
+ render :new, status: :unprocessable_entity
23
+ end
24
+ end
25
+
26
+ def edit
27
+ @gallery = BeautifulPhotons::Gallery.find(params[:id])
28
+ end
29
+
30
+ def update
31
+ @gallery = BeautifulPhotons::Gallery.find(params[:id])
32
+ @gallery.update!(gallery_params)
33
+ redirect_to beautiful_photons.gallery_path(@gallery)
34
+ end
35
+
36
+ def destroy
37
+ @gallery = BeautifulPhotons::Gallery.find(params[:id])
38
+ @gallery.destroy!
39
+ redirect_to beautiful_photons.galleries_path, notice: "Gallery deleted."
40
+ end
41
+
42
+ def show
43
+ @gallery = BeautifulPhotons::Gallery.find(params[:id])
44
+ @gallery_photos = @gallery.gallery_photos.includes(photo: :category).order(:position)
45
+ @category_stats = @gallery.gallery_photos
46
+ .joins(photo: :category)
47
+ .group("beautiful_photons_categories.name")
48
+ .count
49
+ end
50
+
51
+ def add_photos_page
52
+ @gallery = BeautifulPhotons::Gallery.find(params[:id])
53
+ existing_ids = @gallery.gallery_photos.pluck(:photo_id)
54
+ @available_photos = BeautifulPhotons::Photo.where.not(id: existing_ids).order(created_at: :desc)
55
+ end
56
+
57
+ def reorder
58
+ gallery = BeautifulPhotons::Gallery.find(params[:id])
59
+ photo_ids = params[:photo_ids]
60
+
61
+ photo_ids.each_with_index do |photo_id, index|
62
+ gallery.gallery_photos.where(photo_id: photo_id).update_all(position: index + 1)
63
+ end
64
+
65
+ head :ok
66
+ end
67
+
68
+ def add_photos
69
+ @gallery = BeautifulPhotons::Gallery.find(params[:id])
70
+ max_position = @gallery.gallery_photos.maximum(:position) || 0
71
+ ids = Array(params[:photo_ids])
72
+ category = params[:category].presence
73
+
74
+ ids.each_with_index do |photo_id, index|
75
+ @gallery.gallery_photos.find_or_create_by!(photo_id: photo_id, category: category) do |gp|
76
+ gp.position = max_position + index + 1
77
+ end
78
+ end
79
+
80
+ redirect_to beautiful_photons.gallery_path(@gallery),
81
+ notice: "Added #{ids.size} #{"photo".pluralize(ids.size)} to gallery."
82
+ end
83
+
84
+ def remove_photos
85
+ @gallery = BeautifulPhotons::Gallery.find(params[:id])
86
+ ids = Array(params[:photo_ids])
87
+ @gallery.gallery_photos.where(photo_id: ids).destroy_all
88
+
89
+ redirect_to beautiful_photons.gallery_path(@gallery),
90
+ notice: "Removed #{ids.size} #{"photo".pluralize(ids.size)} from gallery."
91
+ end
92
+
93
+ private
94
+
95
+ def gallery_params
96
+ params.require(:gallery).permit(:name, :title, :description)
97
+ end
98
+ end
99
+ end
100
+ end
@@ -0,0 +1,75 @@
1
+ module BeautifulPhotons
2
+ module Admin
3
+ class PhotosController < BaseController
4
+ skip_forgery_protection
5
+
6
+ def index
7
+ @photos = BeautifulPhotons::Photo.all.order(created_at: :desc)
8
+ end
9
+
10
+ def show
11
+ @photo = BeautifulPhotons::Photo.find(params[:id])
12
+ end
13
+
14
+ def new
15
+ @photo = BeautifulPhotons::Photo.new
16
+ end
17
+
18
+ def create
19
+ @photo = BeautifulPhotons::Photo.new(photo_params)
20
+
21
+ if @photo.save
22
+ redirect_to beautiful_photons.photo_path(@photo), notice: "Photo uploaded."
23
+ else
24
+ render :new, status: :unprocessable_entity
25
+ end
26
+ end
27
+
28
+ def bulk_create
29
+ images = Array(params[:images])
30
+ count = 0
31
+
32
+ images.each do |image|
33
+ title = File.basename(image.original_filename, ".*").titleize
34
+ photo = BeautifulPhotons::Photo.new(title: title)
35
+ photo.image.attach(image)
36
+ if photo.save
37
+ count += 1
38
+ end
39
+ end
40
+
41
+ redirect_to beautiful_photons.photos_path,
42
+ notice: "Uploaded #{count} #{"photo".pluralize(count)}."
43
+ end
44
+
45
+ def edit
46
+ @photo = BeautifulPhotons::Photo.find(params[:id])
47
+ end
48
+
49
+ def update
50
+ @photo = BeautifulPhotons::Photo.find(params[:id])
51
+ @photo.update!(photo_params)
52
+ redirect_to beautiful_photons.photo_path(@photo)
53
+ end
54
+
55
+ def destroy
56
+ @photo = BeautifulPhotons::Photo.find(params[:id])
57
+ @photo.destroy!
58
+ redirect_to beautiful_photons.photos_path, notice: "Photo deleted."
59
+ end
60
+
61
+ def bulk_destroy
62
+ ids = Array(params[:photo_ids])
63
+ BeautifulPhotons::Photo.where(id: ids).destroy_all
64
+ redirect_to beautiful_photons.photos_path,
65
+ notice: "Deleted #{ids.size} #{"photo".pluralize(ids.size)}."
66
+ end
67
+
68
+ private
69
+
70
+ def photo_params
71
+ params.require(:photo).permit(:title, :description, :image, :focal_x, :focal_y, :mobile_focal_x, :mobile_focal_y, :published)
72
+ end
73
+ end
74
+ end
75
+ end
@@ -0,0 +1,54 @@
1
+ module BeautifulPhotons
2
+ module Admin
3
+ class StandalonesController < BaseController
4
+ def index
5
+ @standalones = Standalone.includes(:photo).order(:key)
6
+ end
7
+
8
+ def show
9
+ @standalone = Standalone.find(params[:id])
10
+ end
11
+
12
+ def edit_crop
13
+ @standalone = Standalone.find(params[:id])
14
+ end
15
+
16
+ def edit_mobile_crop
17
+ @standalone = Standalone.find(params[:id])
18
+ end
19
+
20
+ def change_photo
21
+ @standalone = Standalone.find(params[:id])
22
+ @photos = Photo.all.order(created_at: :desc)
23
+ end
24
+
25
+ def update
26
+ @standalone = Standalone.find(params[:id])
27
+ @standalone.update!(standalone_params)
28
+
29
+ message = if standalone_params.key?(:crop_x) || standalone_params.key?(:mobile_crop_x)
30
+ "Crop saved."
31
+ elsif standalone_params[:photo_id].present?
32
+ "Photo assigned."
33
+ elsif standalone_params.key?(:photo_id)
34
+ "Photo removed."
35
+ else
36
+ "Updated."
37
+ end
38
+
39
+ redirect_to standalone_path(@standalone), notice: message
40
+ end
41
+
42
+ private
43
+
44
+ def standalone_params
45
+ permitted = params.require(:standalone).permit(
46
+ :photo_id, :crop_x, :crop_y, :crop_zoom,
47
+ :mobile_crop_x, :mobile_crop_y, :mobile_crop_zoom
48
+ )
49
+ permitted[:photo_id] = permitted[:photo_id].presence
50
+ permitted
51
+ end
52
+ end
53
+ end
54
+ end
@@ -0,0 +1,15 @@
1
+ module BeautifulPhotons
2
+ module Api
3
+ class BaseController < ::ApplicationController
4
+ skip_forgery_protection
5
+
6
+ before_action :authenticate_api!
7
+
8
+ private
9
+
10
+ def authenticate_api!
11
+ send(BeautifulPhotons.config.api_authentication_method)
12
+ end
13
+ end
14
+ end
15
+ end