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.
- checksums.yaml +7 -0
- data/MIT-LICENSE +20 -0
- data/README.md +28 -0
- data/Rakefile +11 -0
- data/app/assets/javascripts/beautiful_photons/bulk_select_controller.js +76 -0
- data/app/assets/javascripts/beautiful_photons/crop_editor_controller.js +108 -0
- data/app/assets/javascripts/beautiful_photons/focal_point_controller.js +16 -0
- data/app/assets/javascripts/beautiful_photons/index.js +13 -0
- data/app/assets/javascripts/beautiful_photons/reorder_controller.js +45 -0
- data/app/assets/stylesheets/beautiful_photons/application.css +15 -0
- data/app/controllers/beautiful_photons/admin/base_controller.rb +10 -0
- data/app/controllers/beautiful_photons/admin/categories_controller.rb +37 -0
- data/app/controllers/beautiful_photons/admin/galleries_controller.rb +100 -0
- data/app/controllers/beautiful_photons/admin/photos_controller.rb +75 -0
- data/app/controllers/beautiful_photons/admin/standalones_controller.rb +54 -0
- data/app/controllers/beautiful_photons/api/base_controller.rb +15 -0
- data/app/controllers/beautiful_photons/api/v1/galleries_controller.rb +60 -0
- data/app/controllers/beautiful_photons/api/v1/gallery_photos_controller.rb +60 -0
- data/app/controllers/beautiful_photons/api/v1/photos_controller.rb +65 -0
- data/app/controllers/beautiful_photons/application_controller.rb +4 -0
- data/app/controllers/beautiful_photons/standalone_photos_controller.rb +16 -0
- data/app/helpers/beautiful_photons/application_helper.rb +4 -0
- data/app/helpers/beautiful_photons/photo_helper.rb +103 -0
- data/app/jobs/beautiful_photons/application_job.rb +4 -0
- data/app/mailers/beautiful_photons/application_mailer.rb +6 -0
- data/app/models/beautiful_photons/application_record.rb +5 -0
- data/app/models/beautiful_photons/category.rb +7 -0
- data/app/models/beautiful_photons/gallery.rb +17 -0
- data/app/models/beautiful_photons/gallery_photo.rb +22 -0
- data/app/models/beautiful_photons/photo.rb +29 -0
- data/app/models/beautiful_photons/standalone.rb +23 -0
- data/app/views/beautiful_photons/admin/categories/index.html.erb +28 -0
- data/app/views/beautiful_photons/admin/galleries/add_photos_page.html.erb +30 -0
- data/app/views/beautiful_photons/admin/galleries/edit.html.erb +17 -0
- data/app/views/beautiful_photons/admin/galleries/index.html.erb +13 -0
- data/app/views/beautiful_photons/admin/galleries/new.html.erb +17 -0
- data/app/views/beautiful_photons/admin/galleries/show.html.erb +84 -0
- data/app/views/beautiful_photons/admin/photos/edit.html.erb +35 -0
- data/app/views/beautiful_photons/admin/photos/index.html.erb +60 -0
- data/app/views/beautiful_photons/admin/photos/new.html.erb +10 -0
- data/app/views/beautiful_photons/admin/photos/show.html.erb +66 -0
- data/app/views/beautiful_photons/admin/standalones/change_photo.html.erb +39 -0
- data/app/views/beautiful_photons/admin/standalones/edit_crop.html.erb +38 -0
- data/app/views/beautiful_photons/admin/standalones/edit_mobile_crop.html.erb +37 -0
- data/app/views/beautiful_photons/admin/standalones/index.html.erb +27 -0
- data/app/views/beautiful_photons/admin/standalones/show.html.erb +52 -0
- data/app/views/beautiful_photons/standalone_photos/show.html.erb +27 -0
- data/app/views/layouts/beautiful_photons/admin.html.erb +37 -0
- data/config/importmap.rb +5 -0
- data/config/routes.rb +36 -0
- data/db/migrate/20260318000001_create_beautiful_photons_photos.rb +15 -0
- data/db/migrate/20260318000002_create_beautiful_photons_galleries.rb +13 -0
- data/db/migrate/20260318000003_create_beautiful_photons_gallery_photos.rb +15 -0
- data/db/migrate/20260318000004_create_beautiful_photons_standalones.rb +13 -0
- data/db/migrate/20260318000005_add_crop_to_beautiful_photons_standalones.rb +12 -0
- data/db/migrate/20260318185017_create_beautiful_photons_categories.rb +11 -0
- data/db/migrate/20260318194125_add_category_to_beautiful_photons_photos.rb +6 -0
- data/lib/beautiful_photons/engine.rb +30 -0
- data/lib/beautiful_photons/version.rb +3 -0
- data/lib/beautiful_photons.rb +31 -0
- data/lib/generators/beautiful_photons/install_generator.rb +24 -0
- data/lib/tasks/beautiful_photons_tasks.rake +4 -0
- 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,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,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
|