alchemy_cms 7.4.10 → 8.0.0.b

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.

Potentially problematic release.


This version of alchemy_cms might be problematic. Click here for more details.

Files changed (425) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +16 -5
  3. data/app/assets/builds/alchemy/admin/page-select.css +1 -1
  4. data/app/assets/builds/alchemy/admin/print.css +1 -1
  5. data/app/assets/builds/alchemy/admin.css +2 -2
  6. data/app/assets/builds/alchemy/dark-theme.css +1 -0
  7. data/app/assets/builds/alchemy/light-theme.css +1 -0
  8. data/app/assets/builds/alchemy/theme.css +1 -0
  9. data/app/assets/builds/alchemy/welcome.css +1 -1
  10. data/app/assets/builds/tinymce/skins/content/alchemy/content.min.css +1 -1
  11. data/app/assets/builds/tinymce/skins/content/alchemy-dark/content.min.css +1 -0
  12. data/app/assets/builds/tinymce/skins/ui/alchemy/content.min.css +1 -0
  13. data/app/assets/builds/tinymce/skins/ui/alchemy/skin.min.css +1 -1
  14. data/app/assets/builds/tinymce/skins/ui/alchemy-dark/content.min.css +1 -0
  15. data/app/assets/builds/tinymce/skins/ui/alchemy-dark/skin.min.css +1 -0
  16. data/app/assets/config/alchemy_manifest.js +0 -2
  17. data/app/assets/images/alchemy/element_icons/default.svg +1 -0
  18. data/app/assets/images/alchemy/icons-sprite.svg +1 -0
  19. data/app/components/alchemy/admin/element_select.rb +39 -0
  20. data/app/components/alchemy/admin/resource/applied_filter.rb +29 -0
  21. data/app/components/alchemy/admin/resource/checkbox_filter.rb +36 -0
  22. data/app/components/alchemy/admin/resource/datepicker_filter.rb +42 -0
  23. data/app/components/alchemy/admin/resource/select_filter.rb +43 -0
  24. data/app/components/alchemy/admin/toolbar_button.rb +5 -2
  25. data/app/components/alchemy/ingredients/number_view.rb +18 -0
  26. data/app/controllers/alchemy/admin/attachments_controller.rb +10 -15
  27. data/app/controllers/alchemy/admin/clipboard_controller.rb +2 -6
  28. data/app/controllers/alchemy/admin/elements_controller.rb +3 -1
  29. data/app/controllers/alchemy/admin/languages_controller.rb +1 -1
  30. data/app/controllers/alchemy/admin/pages_controller.rb +15 -15
  31. data/app/controllers/alchemy/admin/pictures_controller.rb +27 -37
  32. data/app/controllers/alchemy/admin/resources_controller.rb +16 -106
  33. data/app/controllers/alchemy/attachments_controller.rb +43 -14
  34. data/app/controllers/alchemy/messages_controller.rb +1 -1
  35. data/app/controllers/alchemy/pages_controller.rb +26 -4
  36. data/app/controllers/concerns/alchemy/admin/resource_filter.rb +93 -0
  37. data/app/decorators/alchemy/element_editor.rb +5 -48
  38. data/app/decorators/alchemy/ingredient_editor.rb +3 -53
  39. data/app/helpers/alchemy/admin/attachments_helper.rb +5 -5
  40. data/app/helpers/alchemy/admin/base_helper.rb +14 -84
  41. data/app/helpers/alchemy/admin/pages_helper.rb +1 -1
  42. data/app/helpers/alchemy/base_helper.rb +0 -30
  43. data/app/helpers/alchemy/elements_block_helper.rb +0 -14
  44. data/app/helpers/alchemy/pages_helper.rb +1 -1
  45. data/{lib → app/helpers}/alchemy/resources_helper.rb +5 -45
  46. data/app/javascript/alchemy_admin/components/action.js +2 -0
  47. data/app/javascript/alchemy_admin/components/alchemy_html_element.js +3 -3
  48. data/app/javascript/alchemy_admin/components/auto_submit.js +20 -0
  49. data/app/javascript/alchemy_admin/components/datepicker.js +18 -7
  50. data/app/javascript/alchemy_admin/components/element_editor/delete_element_button.js +8 -7
  51. data/app/javascript/alchemy_admin/components/element_editor.js +25 -15
  52. data/app/javascript/alchemy_admin/components/element_select.js +43 -0
  53. data/app/javascript/alchemy_admin/components/index.js +3 -0
  54. data/app/javascript/alchemy_admin/components/link_buttons.js +6 -2
  55. data/app/javascript/alchemy_admin/components/remote_select.js +9 -2
  56. data/app/javascript/alchemy_admin/components/tags_autocomplete.js +5 -1
  57. data/app/javascript/alchemy_admin/components/tinymce.js +93 -14
  58. data/app/javascript/alchemy_admin/components/update_check.js +42 -0
  59. data/app/javascript/alchemy_admin/components/uploader/file_upload.js +15 -8
  60. data/app/javascript/alchemy_admin/components/uploader/progress.js +12 -6
  61. data/app/javascript/alchemy_admin/components/uploader.js +4 -2
  62. data/app/javascript/alchemy_admin/confirm_dialog.js +27 -57
  63. data/app/javascript/alchemy_admin/dialog.js +1 -1
  64. data/app/javascript/alchemy_admin/dirty.js +3 -2
  65. data/app/javascript/alchemy_admin/file_editors.js +1 -1
  66. data/app/javascript/alchemy_admin/i18n.js +15 -16
  67. data/app/javascript/alchemy_admin/image_loader.js +4 -2
  68. data/app/javascript/alchemy_admin/initializer.js +1 -49
  69. data/app/javascript/alchemy_admin/picture_editors.js +7 -4
  70. data/app/javascript/alchemy_admin/picture_selector.js +4 -4
  71. data/app/javascript/alchemy_admin/utils/ajax.js +51 -44
  72. data/app/javascript/alchemy_admin.js +3 -8
  73. data/app/jobs/alchemy/delete_picture_job.rb +12 -0
  74. data/app/models/alchemy/admin/filters/base.rb +38 -0
  75. data/app/models/alchemy/admin/filters/checkbox.rb +24 -0
  76. data/app/models/alchemy/admin/filters/datepicker.rb +53 -0
  77. data/app/models/alchemy/admin/filters/select.rb +70 -0
  78. data/app/models/alchemy/admin/resource_name.rb +27 -0
  79. data/app/models/alchemy/attachment.rb +49 -44
  80. data/app/models/alchemy/base_record.rb +2 -0
  81. data/app/models/alchemy/element/definitions.rb +1 -1
  82. data/app/models/alchemy/element/element_ingredients.rb +6 -6
  83. data/app/models/alchemy/element/presenters.rb +3 -12
  84. data/app/models/alchemy/element.rb +10 -27
  85. data/app/models/alchemy/element_definition.rb +190 -0
  86. data/app/models/alchemy/ingredient.rb +10 -43
  87. data/app/models/alchemy/ingredient_definition.rb +134 -0
  88. data/app/models/alchemy/ingredient_validator.rb +7 -3
  89. data/app/models/alchemy/ingredients/number.rb +19 -0
  90. data/app/models/alchemy/language.rb +2 -21
  91. data/app/models/alchemy/message.rb +3 -7
  92. data/app/models/alchemy/node.rb +1 -1
  93. data/app/models/alchemy/page/{page_layouts.rb → definitions.rb} +12 -19
  94. data/app/models/alchemy/page/fixed_attributes.rb +1 -1
  95. data/app/models/alchemy/page/page_elements.rb +13 -14
  96. data/app/models/alchemy/page/page_naming.rb +3 -11
  97. data/app/models/alchemy/page/page_natures.rb +20 -15
  98. data/app/models/alchemy/page/page_scopes.rb +1 -1
  99. data/app/models/alchemy/page.rb +12 -39
  100. data/app/models/alchemy/page_definition.rb +115 -0
  101. data/app/models/alchemy/picture.rb +71 -99
  102. data/app/models/alchemy/picture_variant.rb +115 -5
  103. data/{lib → app/models}/alchemy/resource.rb +4 -18
  104. data/{lib → app/models}/alchemy/searchable_resource.rb +15 -0
  105. data/app/models/alchemy/site/layout.rb +5 -5
  106. data/app/models/alchemy/site.rb +1 -21
  107. data/app/models/alchemy/storage_adapter/active_storage/attachment_url.rb +41 -0
  108. data/app/models/alchemy/storage_adapter/active_storage/picture_url.rb +55 -0
  109. data/app/models/alchemy/storage_adapter/active_storage/preprocessor.rb +40 -0
  110. data/app/models/alchemy/storage_adapter/active_storage.rb +173 -0
  111. data/app/models/alchemy/{attachment/url.rb → storage_adapter/dragonfly/attachment_url.rb} +12 -12
  112. data/app/models/alchemy/storage_adapter/dragonfly/picture_url.rb +75 -0
  113. data/app/models/alchemy/{picture → storage_adapter/dragonfly}/preprocessor.rb +4 -4
  114. data/app/models/alchemy/storage_adapter/dragonfly.rb +205 -0
  115. data/app/models/alchemy/storage_adapter.rb +74 -0
  116. data/app/models/concerns/alchemy/picture_thumbnails.rb +19 -6
  117. data/app/models/concerns/alchemy/relatable_resource.rb +28 -0
  118. data/app/serializers/alchemy/element_serializer.rb +0 -1
  119. data/app/services/alchemy/dragonfly_to_image_processing.rb +100 -0
  120. data/app/stylesheets/alchemy/_custom-properties.scss +162 -0
  121. data/app/stylesheets/alchemy/_defaults.scss +3 -0
  122. data/app/stylesheets/alchemy/_extends.scss +69 -0
  123. data/app/{assets/stylesheets → stylesheets}/alchemy/_mixins.scss +40 -68
  124. data/app/stylesheets/alchemy/_themes.scss +540 -0
  125. data/app/stylesheets/alchemy/_variables.scss +5 -0
  126. data/app/{assets/stylesheets → stylesheets}/alchemy/admin/archive.scss +45 -42
  127. data/app/stylesheets/alchemy/admin/attachments.scss +17 -0
  128. data/app/{assets/stylesheets → stylesheets}/alchemy/admin/base.scss +20 -15
  129. data/app/stylesheets/alchemy/admin/buttons.scss +135 -0
  130. data/app/{assets/stylesheets → stylesheets}/alchemy/admin/clipboard.scss +2 -2
  131. data/app/{assets/stylesheets → stylesheets}/alchemy/admin/dashboard.scss +13 -16
  132. data/app/{assets/stylesheets → stylesheets}/alchemy/admin/dialogs.scss +33 -16
  133. data/app/stylesheets/alchemy/admin/element-select.scss +11 -0
  134. data/app/{assets/stylesheets → stylesheets}/alchemy/admin/elements.scss +239 -133
  135. data/app/{assets/stylesheets → stylesheets}/alchemy/admin/errors.scss +5 -5
  136. data/app/stylesheets/alchemy/admin/filters.scss +57 -0
  137. data/app/{assets/stylesheets → stylesheets}/alchemy/admin/flatpickr.scss +54 -76
  138. data/app/{assets/stylesheets → stylesheets}/alchemy/admin/form_fields.scss +10 -11
  139. data/app/{assets/stylesheets → stylesheets}/alchemy/admin/forms.scss +28 -21
  140. data/app/{assets/stylesheets → stylesheets}/alchemy/admin/frame.scss +20 -18
  141. data/app/stylesheets/alchemy/admin/hints.scss +5 -0
  142. data/app/{assets/stylesheets → stylesheets}/alchemy/admin/icons.scss +1 -1
  143. data/app/{assets/stylesheets → stylesheets}/alchemy/admin/image_library.scss +21 -61
  144. data/app/{assets/stylesheets → stylesheets}/alchemy/admin/images.scss +1 -1
  145. data/app/stylesheets/alchemy/admin/labels.scss +5 -0
  146. data/app/{assets/stylesheets → stylesheets}/alchemy/admin/lists.scss +3 -3
  147. data/app/{assets/stylesheets → stylesheets}/alchemy/admin/navigation.scss +55 -59
  148. data/app/{assets/stylesheets → stylesheets}/alchemy/admin/node-select.scss +1 -10
  149. data/app/{assets/stylesheets → stylesheets}/alchemy/admin/nodes.scss +21 -18
  150. data/app/{assets/stylesheets → stylesheets}/alchemy/admin/notices.scss +20 -19
  151. data/app/{assets/stylesheets → stylesheets}/alchemy/admin/page-select.scss +18 -2
  152. data/app/stylesheets/alchemy/admin/pagination.scss +137 -0
  153. data/app/stylesheets/alchemy/admin/preview_window.scss +46 -0
  154. data/app/{assets/stylesheets → stylesheets}/alchemy/admin/print.scss +1 -1
  155. data/app/stylesheets/alchemy/admin/resource_info.scss +148 -0
  156. data/app/{assets/stylesheets → stylesheets}/alchemy/admin/search.scss +10 -7
  157. data/app/{assets/stylesheets → stylesheets}/alchemy/admin/selects.scss +85 -46
  158. data/app/{assets/stylesheets → stylesheets}/alchemy/admin/shoelace.scss +37 -68
  159. data/app/{assets/stylesheets → stylesheets}/alchemy/admin/sitemap.scss +39 -34
  160. data/app/{assets/stylesheets → stylesheets}/alchemy/admin/tables.scss +9 -7
  161. data/app/stylesheets/alchemy/admin/tags.scss +143 -0
  162. data/app/{assets/stylesheets → stylesheets}/alchemy/admin/toolbar.scss +6 -6
  163. data/app/{assets/stylesheets → stylesheets}/alchemy/admin/typography.scss +3 -6
  164. data/app/{assets/stylesheets → stylesheets}/alchemy/admin/upload.scss +7 -5
  165. data/app/stylesheets/alchemy/admin.scss +44 -0
  166. data/app/stylesheets/alchemy/dark-theme.scss +5 -0
  167. data/app/stylesheets/alchemy/light-theme.scss +6 -0
  168. data/app/stylesheets/alchemy/theme.scss +13 -0
  169. data/app/stylesheets/alchemy/welcome.scss +75 -0
  170. data/app/stylesheets/tinymce/skins/content/alchemy/content.scss +69 -0
  171. data/app/{assets/stylesheets/tinymce/skins/content/alchemy → stylesheets/tinymce/skins/content/alchemy-dark}/content.scss +12 -12
  172. data/app/stylesheets/tinymce/skins/ui/alchemy/content.scss +1 -0
  173. data/app/{assets/stylesheets → stylesheets}/tinymce/skins/ui/alchemy/skin.scss +158 -176
  174. data/app/stylesheets/tinymce/skins/ui/alchemy-dark/content.scss +1 -0
  175. data/app/stylesheets/tinymce/skins/ui/alchemy-dark/skin.scss +3784 -0
  176. data/app/views/alchemy/admin/attachments/_files_list.html.erb +22 -12
  177. data/app/views/alchemy/admin/attachments/_overlay_file_list.html.erb +1 -1
  178. data/app/views/alchemy/admin/attachments/assign.js.erb +4 -3
  179. data/app/views/alchemy/admin/attachments/show.html.erb +55 -43
  180. data/app/views/alchemy/admin/{elements/_clipboard_button.html.erb → clipboard/_button.html.erb} +3 -5
  181. data/app/views/alchemy/admin/clipboard/_update_nested_element_button.turbo_stream.erb +11 -0
  182. data/app/views/alchemy/admin/clipboard/clear.turbo_stream.erb +4 -0
  183. data/app/views/alchemy/admin/clipboard/index.html.erb +15 -13
  184. data/app/views/alchemy/admin/clipboard/insert.turbo_stream.erb +18 -0
  185. data/app/views/alchemy/admin/clipboard/remove.turbo_stream.erb +9 -0
  186. data/app/views/alchemy/admin/crop.html.erb +1 -1
  187. data/app/views/alchemy/admin/dashboard/info.html.erb +17 -31
  188. data/app/views/alchemy/admin/elements/_element.html.erb +4 -8
  189. data/app/views/alchemy/admin/elements/_form.html.erb +9 -9
  190. data/app/views/alchemy/admin/elements/_header.html.erb +5 -1
  191. data/app/views/alchemy/admin/elements/_toolbar.html.erb +4 -6
  192. data/app/views/alchemy/admin/elements/create.turbo_stream.erb +2 -1
  193. data/app/views/alchemy/admin/elements/index.html.erb +2 -2
  194. data/app/views/alchemy/admin/ingredients/_file_fields.html.erb +3 -16
  195. data/app/views/alchemy/admin/ingredients/_picture_fields.html.erb +0 -9
  196. data/app/views/alchemy/admin/languages/_form.html.erb +1 -1
  197. data/app/views/alchemy/admin/languages/_table.html.erb +1 -1
  198. data/app/views/alchemy/admin/languages/index.html.erb +5 -2
  199. data/app/views/alchemy/admin/layoutpages/index.html.erb +1 -12
  200. data/app/views/alchemy/admin/pages/_form.html.erb +2 -2
  201. data/app/views/alchemy/admin/pages/_page.html.erb +2 -3
  202. data/app/views/alchemy/admin/pages/_toolbar.html.erb +1 -15
  203. data/app/views/alchemy/admin/pages/index.html.erb +1 -1
  204. data/app/views/alchemy/admin/partials/_remote_search_form.html.erb +9 -12
  205. data/app/views/alchemy/admin/partials/_search_form.html.erb +4 -9
  206. data/app/views/alchemy/admin/pictures/_archive.html.erb +16 -29
  207. data/app/views/alchemy/admin/pictures/_archive_overlay.html.erb +3 -7
  208. data/app/views/alchemy/admin/pictures/_filter_and_size_bar.html.erb +1 -1
  209. data/app/views/alchemy/admin/pictures/_form.html.erb +1 -1
  210. data/app/views/alchemy/admin/pictures/_infos.html.erb +21 -52
  211. data/app/views/alchemy/admin/pictures/_library_sidebar.html.erb +7 -0
  212. data/app/views/alchemy/admin/pictures/_picture.html.erb +14 -20
  213. data/app/views/alchemy/admin/pictures/_picture_to_assign.html.erb +20 -16
  214. data/app/views/alchemy/admin/pictures/_sorting_select.html.erb +13 -0
  215. data/app/views/alchemy/admin/pictures/_tag_list.html.erb +1 -1
  216. data/app/views/alchemy/admin/pictures/edit_multiple.html.erb +1 -6
  217. data/app/views/alchemy/admin/pictures/index.html.erb +5 -19
  218. data/app/views/alchemy/admin/pictures/show.html.erb +10 -5
  219. data/app/views/alchemy/admin/resources/_applied_filters.html.erb +8 -0
  220. data/app/views/alchemy/admin/resources/_filter_bar.html.erb +5 -25
  221. data/app/views/alchemy/admin/resources/_pagination.html.erb +6 -0
  222. data/app/views/alchemy/admin/resources/_per_page_select.html.erb +4 -2
  223. data/app/views/alchemy/admin/resources/_resource_table.html.erb +1 -1
  224. data/app/views/alchemy/admin/resources/_resource_usage_info.html.erb +36 -0
  225. data/app/views/alchemy/admin/resources/_table_header.html.erb +1 -15
  226. data/app/views/alchemy/admin/sites/index.html.erb +5 -1
  227. data/app/views/alchemy/admin/styleguide/index.html.erb +118 -58
  228. data/app/views/alchemy/admin/tags/index.html.erb +1 -1
  229. data/app/views/alchemy/admin/tinymce/_setup.html.erb +7 -7
  230. data/app/{javascript/alchemy_admin/locales/en.js → views/alchemy/admin/translations/_en.js} +5 -2
  231. data/app/views/alchemy/admin/uploader/_button.html.erb +1 -1
  232. data/app/views/alchemy/admin/uploader/_setup.html.erb +4 -4
  233. data/app/views/alchemy/base/error_notice.html.erb +1 -1
  234. data/app/views/alchemy/ingredients/_number_editor.html.erb +24 -0
  235. data/app/views/alchemy/no_index.html.erb +31 -0
  236. data/app/views/alchemy/welcome.html.erb +12 -10
  237. data/app/views/kaminari/alchemy/_first_page.html.erb +5 -3
  238. data/app/views/kaminari/alchemy/_last_page.html.erb +5 -3
  239. data/app/views/kaminari/alchemy/_next_page.html.erb +5 -3
  240. data/app/views/kaminari/alchemy/_paginator.html.erb +18 -13
  241. data/app/views/kaminari/alchemy/_prev_page.html.erb +5 -3
  242. data/app/views/layouts/alchemy/admin.html.erb +25 -24
  243. data/config/alchemy/config.yml +3 -2
  244. data/config/initializers/dragonfly.rb +0 -1
  245. data/config/initializers/mime_types.rb +1 -0
  246. data/config/locales/alchemy.en.yml +57 -21
  247. data/config/routes.rb +0 -2
  248. data/db/migrate/20250905140323_add_created_at_index_to_pictures_and_attachments.rb +14 -0
  249. data/lib/alchemy/admin/preview_url.rb +4 -5
  250. data/lib/alchemy/cache_digests/template_tracker.rb +6 -9
  251. data/lib/alchemy/config_missing.rb +14 -0
  252. data/lib/alchemy/configuration/base_option.rb +24 -0
  253. data/lib/alchemy/configuration/boolean_option.rb +16 -0
  254. data/lib/alchemy/configuration/class_option.rb +15 -0
  255. data/lib/alchemy/configuration/class_set_option.rb +46 -0
  256. data/lib/alchemy/configuration/integer_list_option.rb +13 -0
  257. data/lib/alchemy/configuration/integer_option.rb +12 -0
  258. data/lib/alchemy/configuration/list_option.rb +22 -0
  259. data/lib/alchemy/configuration/regexp_option.rb +11 -0
  260. data/lib/alchemy/configuration/string_list_option.rb +13 -0
  261. data/lib/alchemy/configuration/string_option.rb +11 -0
  262. data/lib/alchemy/configuration.rb +115 -0
  263. data/lib/alchemy/configuration_methods.rb +3 -1
  264. data/lib/alchemy/configurations/default_language.rb +12 -0
  265. data/lib/alchemy/configurations/default_site.rb +10 -0
  266. data/lib/alchemy/configurations/format_matchers.rb +11 -0
  267. data/lib/alchemy/configurations/mailer.rb +16 -0
  268. data/lib/alchemy/configurations/main.rb +223 -0
  269. data/lib/alchemy/configurations/page_cache.rb +19 -0
  270. data/lib/alchemy/configurations/preview.rb +32 -0
  271. data/lib/alchemy/configurations/sitemap.rb +10 -0
  272. data/lib/alchemy/configurations/uploader.rb +34 -0
  273. data/lib/alchemy/engine.rb +81 -24
  274. data/lib/alchemy/hints.rb +3 -7
  275. data/lib/alchemy/install/tasks.rb +0 -12
  276. data/lib/alchemy/on_page_layout.rb +2 -2
  277. data/lib/alchemy/propshaft/tinymce_asset.rb +15 -0
  278. data/lib/alchemy/seeder.rb +2 -2
  279. data/lib/alchemy/tasks/tidy.rb +18 -0
  280. data/lib/alchemy/tasks/usage.rb +4 -4
  281. data/lib/alchemy/test_support/config_stubbing.rb +1 -7
  282. data/lib/alchemy/test_support/factories/attachment_factory.rb +13 -2
  283. data/lib/alchemy/test_support/factories/language_factory.rb +1 -1
  284. data/lib/alchemy/test_support/factories/page_factory.rb +2 -3
  285. data/lib/alchemy/test_support/factories/picture_factory.rb +31 -2
  286. data/lib/alchemy/test_support/factories/site_factory.rb +2 -2
  287. data/lib/alchemy/test_support/having_crop_action_examples.rb +2 -2
  288. data/lib/alchemy/test_support/having_picture_thumbnails_examples.rb +80 -26
  289. data/lib/alchemy/test_support/relatable_resource_examples.rb +58 -0
  290. data/lib/alchemy/test_support/shared_ingredient_examples.rb +5 -5
  291. data/lib/alchemy/tinymce.rb +0 -1
  292. data/lib/alchemy/upgrader/eight_zero.rb +14 -0
  293. data/lib/alchemy/upgrader.rb +33 -20
  294. data/lib/alchemy/version.rb +1 -1
  295. data/lib/alchemy.rb +185 -172
  296. data/lib/alchemy_cms.rb +1 -7
  297. data/lib/generators/alchemy/ingredient/ingredient_generator.rb +0 -3
  298. data/lib/generators/alchemy/install/files/_article.html.erb +6 -4
  299. data/lib/generators/alchemy/install/files/alchemy.en.yml +22 -3
  300. data/lib/generators/alchemy/install/files/application.html.erb +5 -0
  301. data/lib/generators/alchemy/install/install_generator.rb +25 -23
  302. data/lib/generators/alchemy/install/templates/alchemy.rb.tt +200 -0
  303. data/lib/generators/alchemy/install/templates/dragonfly.rb.tt +0 -1
  304. data/lib/generators/alchemy/install/templates/elements.yml.tt +3 -1
  305. data/lib/generators/alchemy/install/templates/menus.yml.tt +1 -1
  306. data/lib/generators/alchemy/install/templates/page_layouts.yml.tt +2 -2
  307. data/lib/generators/alchemy/page_layouts/page_layouts_generator.rb +2 -2
  308. data/lib/tasks/alchemy/assets.rake +14 -0
  309. data/lib/tasks/alchemy/tidy.rake +6 -0
  310. data/lib/tasks/alchemy/upgrade.rake +12 -47
  311. data/vendor/assets/stylesheets/tinymce/skins/content/dark/content.min.css +1 -0
  312. data/vendor/assets/stylesheets/tinymce/skins/content/default/content.min.css +1 -0
  313. data/vendor/assets/stylesheets/tinymce/skins/ui/oxide/skin.min.css +1 -0
  314. data/vendor/assets/stylesheets/tinymce/skins/ui/oxide-dark/content.min.css +1 -0
  315. data/vendor/assets/stylesheets/tinymce/skins/ui/oxide-dark/skin.min.css +1 -0
  316. data/vendor/javascript/clipboard.min.js +1 -1
  317. data/vendor/javascript/cropperjs.min.js +1 -1
  318. data/vendor/javascript/handlebars.min.js +3 -3
  319. data/vendor/javascript/jquery.min.js +1 -1
  320. data/vendor/javascript/select2.min.js +3 -3
  321. data/vendor/javascript/shoelace.min.js +92 -76
  322. data/vendor/javascript/sortable.min.js +2 -2
  323. data/vendor/javascript/tinymce.min.js +1 -1
  324. data/vendor/javascript/ungap-custom-elements.min.js +2 -2
  325. metadata +223 -208
  326. data/CHANGELOG.md +0 -2041
  327. data/CODE_OF_CONDUCT.md +0 -13
  328. data/CONTRIBUTING.md +0 -73
  329. data/Gemfile +0 -71
  330. data/Rakefile +0 -102
  331. data/SECURITY.md +0 -13
  332. data/alchemy_cms.gemspec +0 -88
  333. data/app/assets/builds/alchemy/admin/page-select.css.map +0 -1
  334. data/app/assets/builds/alchemy/admin/print.css.map +0 -1
  335. data/app/assets/builds/alchemy/admin.css.map +0 -1
  336. data/app/assets/builds/alchemy/custom-properties.css +0 -1
  337. data/app/assets/builds/alchemy/custom-properties.css.map +0 -1
  338. data/app/assets/builds/alchemy/welcome.css.map +0 -1
  339. data/app/assets/builds/tinymce/skins/content/alchemy/content.min.css.map +0 -1
  340. data/app/assets/builds/tinymce/skins/ui/alchemy/skin.min.css.map +0 -1
  341. data/app/assets/javascripts/alchemy/admin.js +0 -10
  342. data/app/assets/stylesheets/alchemy/_defaults.scss +0 -3
  343. data/app/assets/stylesheets/alchemy/_deprecated_variables.scss +0 -45
  344. data/app/assets/stylesheets/alchemy/_deprecation.scss +0 -17
  345. data/app/assets/stylesheets/alchemy/_extends.scss +0 -62
  346. data/app/assets/stylesheets/alchemy/_variables.scss +0 -201
  347. data/app/assets/stylesheets/alchemy/admin/attachments.scss +0 -40
  348. data/app/assets/stylesheets/alchemy/admin/buttons.scss +0 -123
  349. data/app/assets/stylesheets/alchemy/admin/hints.scss +0 -5
  350. data/app/assets/stylesheets/alchemy/admin/labels.scss +0 -3
  351. data/app/assets/stylesheets/alchemy/admin/pagination.scss +0 -92
  352. data/app/assets/stylesheets/alchemy/admin/preview_window.scss +0 -33
  353. data/app/assets/stylesheets/alchemy/admin/resource_info.scss +0 -42
  354. data/app/assets/stylesheets/alchemy/admin/tags.scss +0 -158
  355. data/app/assets/stylesheets/alchemy/admin.scss +0 -42
  356. data/app/assets/stylesheets/alchemy/custom-properties.css +0 -98
  357. data/app/assets/stylesheets/alchemy/welcome.scss +0 -57
  358. data/app/assets/stylesheets/tinymce/skins/ui/alchemy/content.css +0 -711
  359. data/app/assets/stylesheets/tinymce/skins/ui/alchemy/content.inline.css +0 -705
  360. data/app/assets/stylesheets/tinymce/skins/ui/alchemy/content.inline.min.css +0 -7
  361. data/app/assets/stylesheets/tinymce/skins/ui/alchemy/content.min.css +0 -7
  362. data/app/assets/stylesheets/tinymce/skins/ui/alchemy/content.mobile.css +0 -29
  363. data/app/assets/stylesheets/tinymce/skins/ui/alchemy/content.mobile.min.css +0 -7
  364. data/app/assets/stylesheets/tinymce/skins/ui/alchemy/skin.mobile.css +0 -677
  365. data/app/assets/stylesheets/tinymce/skins/ui/alchemy/skin.mobile.min.css +0 -7
  366. data/app/controllers/alchemy/elements_controller.rb +0 -32
  367. data/app/helpers/alchemy/admin/elements_helper.rb +0 -25
  368. data/app/models/alchemy/element/dom_id.rb +0 -31
  369. data/app/models/alchemy/picture/calculations.rb +0 -49
  370. data/app/models/alchemy/picture/transformations.rb +0 -115
  371. data/app/models/alchemy/picture/url.rb +0 -54
  372. data/app/views/alchemy/admin/attachments/destroy.js.erb +0 -1
  373. data/app/views/alchemy/admin/clipboard/clear.js.erb +0 -3
  374. data/app/views/alchemy/admin/clipboard/insert.js.erb +0 -29
  375. data/app/views/alchemy/admin/clipboard/remove.js.erb +0 -10
  376. data/app/views/alchemy/admin/resources/_filter.html.erb +0 -12
  377. data/app/views/alchemy/admin/resources/_resource.html.erb +0 -34
  378. data/app/views/alchemy/admin/resources/_table.html.erb +0 -29
  379. data/app/views/alchemy/elements/show.html.erb +0 -1
  380. data/app/views/alchemy/elements/show.js.erb +0 -1
  381. data/app/views/alchemy/ingredients/_audio_view.html.erb +0 -1
  382. data/app/views/alchemy/ingredients/_boolean_view.html.erb +0 -1
  383. data/app/views/alchemy/ingredients/_datetime_view.html.erb +0 -3
  384. data/app/views/alchemy/ingredients/_file_view.html.erb +0 -4
  385. data/app/views/alchemy/ingredients/_headline_view.html.erb +0 -4
  386. data/app/views/alchemy/ingredients/_html_view.html.erb +0 -1
  387. data/app/views/alchemy/ingredients/_link_view.html.erb +0 -4
  388. data/app/views/alchemy/ingredients/_node_view.html.erb +0 -1
  389. data/app/views/alchemy/ingredients/_page_view.html.erb +0 -1
  390. data/app/views/alchemy/ingredients/_picture_view.html.erb +0 -4
  391. data/app/views/alchemy/ingredients/_richtext_view.html.erb +0 -3
  392. data/app/views/alchemy/ingredients/_select_view.html.erb +0 -1
  393. data/app/views/alchemy/ingredients/_text_view.html.erb +0 -4
  394. data/app/views/alchemy/ingredients/_video_view.html.erb +0 -3
  395. data/babel.config.js +0 -12
  396. data/bin/importmap +0 -4
  397. data/bin/rails +0 -9
  398. data/bin/rspec +0 -3
  399. data/bin/setup +0 -30
  400. data/bin/start +0 -17
  401. data/bun.lockb +0 -0
  402. data/bundles/shoelace.js +0 -12
  403. data/bundles/tinymce.js +0 -22
  404. data/config/initializers/assets.rb +0 -4
  405. data/eslint.config.js +0 -17
  406. data/lib/alchemy/config.rb +0 -114
  407. data/lib/alchemy/element_definition.rb +0 -73
  408. data/lib/alchemy/page_layout.rb +0 -73
  409. data/lib/alchemy/resource_filter.rb +0 -40
  410. data/lib/alchemy/upgrader/seven_point_four.rb +0 -26
  411. data/lib/alchemy/upgrader/seven_point_three.rb +0 -52
  412. data/lib/alchemy/upgrader/tasks/.keep +0 -0
  413. data/lib/generators/alchemy/ingredient/templates/view.html.erb +0 -1
  414. data/lib/generators/alchemy/install/files/alchemy_admin.js +0 -1
  415. data/lib/generators/alchemy/install/files/all.js +0 -11
  416. data/lib/generators/alchemy/install/files/article.css +0 -25
  417. data/rollup.config.mjs +0 -108
  418. data/vendor/assets/images/remixicon.symbol.svg +0 -11
  419. /data/app/{assets/stylesheets → stylesheets}/alchemy/_fonts.scss +0 -0
  420. /data/app/{assets/stylesheets → stylesheets}/alchemy/admin/attachment-select.scss +0 -0
  421. /data/app/{assets/stylesheets → stylesheets}/alchemy/admin/flash.scss +0 -0
  422. /data/app/{assets/stylesheets → stylesheets}/alchemy/admin/list_filter.scss +0 -0
  423. /data/app/{assets/stylesheets → stylesheets}/alchemy/admin/spinner.scss +0 -0
  424. /data/app/{assets/stylesheets → stylesheets}/tinymce/skins/skintool.json +0 -0
  425. /data/app/{assets/stylesheets → stylesheets}/tinymce/skins/ui/alchemy/fonts/tinymce-mobile.woff +0 -0
@@ -0,0 +1,40 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Alchemy
4
+ class StorageAdapter
5
+ class ActiveStorage::Preprocessor
6
+ def initialize(attachable)
7
+ @attachable = attachable
8
+ end
9
+
10
+ # Preprocess images after upload
11
+ #
12
+ # Define preprocessing options in the Alchemy.config
13
+ #
14
+ # preprocess_image_resize [String] - Downsizing example: '1000x1000>'
15
+ #
16
+ def call
17
+ max_image_size = Alchemy.config.get(:preprocess_image_resize)
18
+ if max_image_size.present?
19
+ self.class.process_thumb(attachable, size: max_image_size)
20
+ end
21
+ end
22
+
23
+ attr_reader :attachable
24
+
25
+ class << self
26
+ def generate_thumbs!(attachable)
27
+ Alchemy::Picture::THUMBNAIL_SIZES.values.each do |size|
28
+ process_thumb(attachable, size: size, flatten: true)
29
+ end
30
+ end
31
+
32
+ def process_thumb(attachable, options = {})
33
+ attachable.variant :thumb,
34
+ **Alchemy::DragonflyToImageProcessing.call(options),
35
+ preprocessed: true
36
+ end
37
+ end
38
+ end
39
+ end
40
+ end
@@ -0,0 +1,173 @@
1
+ module Alchemy
2
+ class StorageAdapter
3
+ module ActiveStorage
4
+ module PictureClassMethods
5
+ def self.included(base)
6
+ base.has_one_attached :image_file do |attachable|
7
+ # Only works in Rails 7.1+
8
+ # https://github.com/rails/rails/pull/47473
9
+ Alchemy.storage_adapter.preprocessor_class.new(attachable).call
10
+ Alchemy.storage_adapter.preprocessor_class.generate_thumbs!(attachable)
11
+ end
12
+ end
13
+ end
14
+
15
+ module AttachmentClassMethods
16
+ def self.included(base)
17
+ base.has_one_attached :file
18
+ end
19
+ end
20
+
21
+ extend self
22
+
23
+ def attachment_url_class
24
+ AttachmentUrl
25
+ end
26
+
27
+ def preprocessor_class
28
+ Preprocessor
29
+ end
30
+
31
+ def picture_url_class
32
+ PictureUrl
33
+ end
34
+
35
+ def file_formats(class_name, scope:)
36
+ attachment_scope = case class_name
37
+ when "Alchemy::Attachment" then scope.with_attached_file
38
+ when "Alchemy::Picture" then scope.with_attached_image_file
39
+ end
40
+
41
+ attachment_scope.pluck("active_storage_blobs.content_type").uniq.tap(&:compact!).presence || []
42
+ end
43
+
44
+ # @param [String]
45
+ # @return [Array<String>]
46
+ def searchable_alchemy_resource_attributes(class_name)
47
+ case class_name
48
+ when "Alchemy::Attachment"
49
+ %w[name file_blob_filename]
50
+ when "Alchemy::Picture"
51
+ %w[name image_file_blob_filename]
52
+ end
53
+ end
54
+
55
+ # @param [String]
56
+ # @return [Array<String>]
57
+ def ransackable_attributes(_class_name)
58
+ %w[name]
59
+ end
60
+
61
+ # @param [String]
62
+ # @return [Array<String>]
63
+ def ransackable_associations(class_name)
64
+ case class_name
65
+ when "Alchemy::Attachment"
66
+ %w[file_blob]
67
+ when "Alchemy::Picture"
68
+ %w[image_file_blob]
69
+ end
70
+ end
71
+
72
+ def rescuable_errors
73
+ ::ActiveStorage::Error
74
+ end
75
+
76
+ # @param [String]
77
+ # @return [Alchemy::Picture::ActiveRecord_Relation]
78
+ def by_file_format_scope(file_format)
79
+ Picture.with_attached_image_file.joins(:image_file_blob).where(active_storage_blobs: {content_type: file_format})
80
+ end
81
+
82
+ # @param [String]
83
+ # @return [Alchemy::Atachment::ActiveRecord_Relation]
84
+ def by_file_type_scope(file_type)
85
+ Attachment.with_attached_file.joins(:file_blob).where(active_storage_blobs: {content_type: file_type})
86
+ end
87
+
88
+ # @param [Alchemy::Attachment]
89
+ # @return [String]
90
+ def file_name(attachment)
91
+ attachment.file&.filename&.to_s
92
+ end
93
+
94
+ # @param [Alchemy::Attachment]
95
+ # @return [Integer]
96
+ def file_size(attachment)
97
+ attachment.file&.byte_size
98
+ end
99
+
100
+ # @param [Alchemy::Attachment]
101
+ # @return [String]
102
+ def file_mime_type(attachment)
103
+ attachment.file&.content_type
104
+ end
105
+
106
+ # @param [Alchemy::Attachment]
107
+ # @return [String]
108
+ def file_extension(attachment)
109
+ attachment.file&.filename&.extension
110
+ end
111
+
112
+ # @param [Alchemy::Picture]
113
+ # @return [TrueClass, FalseClass]
114
+ def has_convertible_format?(picture)
115
+ picture.image_file&.variable?
116
+ end
117
+
118
+ # @param [Alchemy::Picture]
119
+ # @return [String]
120
+ def image_file_name(picture)
121
+ picture.image_file&.filename&.to_s
122
+ end
123
+
124
+ # @param [Alchemy::Picture]
125
+ # @return [String]
126
+ def image_file_format(picture)
127
+ picture.image_file&.content_type
128
+ end
129
+
130
+ # @param [Alchemy::Picture]
131
+ # @return [Integer]
132
+ def image_file_size(picture)
133
+ picture.image_file&.byte_size
134
+ end
135
+
136
+ # @param [Alchemy::Picture]
137
+ # @return [Integer]
138
+ def image_file_width(picture)
139
+ picture.image_file&.metadata&.fetch(:width, nil)
140
+ end
141
+
142
+ # @param [Alchemy::Picture]
143
+ # @return [Integer]
144
+ def image_file_height(picture)
145
+ picture.image_file&.metadata&.fetch(:height, nil)
146
+ end
147
+
148
+ # @param [Alchemy::Picture]
149
+ # @return [Integer]
150
+ def image_file_extension(picture)
151
+ picture.image_file&.filename&.extension&.downcase
152
+ end
153
+
154
+ # @param [Alchemy::Picture]
155
+ # @return [TrueClass, FalseClass]
156
+ def image_file_present?(picture)
157
+ picture.image_file.attached?
158
+ end
159
+
160
+ # @param Alchemy::Picture::ActiveRecord_Relation
161
+ # @return Alchemy::Picture::ActiveRecord_Relation
162
+ def preloaded_pictures(pictures)
163
+ pictures.with_attached_image_file
164
+ end
165
+
166
+ # @param [Alchemy::Attachment]
167
+ # @return [TrueClass, FalseClass]
168
+ def set_attachment_name?(attachment)
169
+ attachment.file.changed?
170
+ end
171
+ end
172
+ end
173
+ end
@@ -1,14 +1,12 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Alchemy
4
- class Attachment < BaseRecord
5
- # The class representing an URL to an attachment
4
+ class StorageAdapter
5
+ # The class representing an URL to an Dragonfly attachment
6
6
  #
7
- # Set a different one
8
- #
9
- # Alchemy::Attachment.url_class = MyRemoteUrlClass
10
- #
11
- class Url
7
+ class Dragonfly::AttachmentUrl
8
+ attr_reader :attachment
9
+
12
10
  def initialize(attachment)
13
11
  @attachment = attachment
14
12
  end
@@ -23,11 +21,13 @@ module Alchemy
23
21
  # @return [String]
24
22
  #
25
23
  def call(options = {})
26
- options[:format] ||= @attachment.suffix
27
- if options.delete(:download)
28
- routes.download_attachment_path(@attachment, options)
29
- else
30
- routes.show_attachment_path(@attachment, options)
24
+ if attachment.file
25
+ options[:format] ||= attachment.suffix
26
+ if options.delete(:download)
27
+ routes.download_attachment_path(attachment, options)
28
+ else
29
+ routes.show_attachment_path(attachment, options)
30
+ end
31
31
  end
32
32
  end
33
33
 
@@ -0,0 +1,75 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Alchemy
4
+ class StorageAdapter
5
+ class Dragonfly::PictureUrl
6
+ TRANSFORMATION_OPTIONS = [
7
+ :crop,
8
+ :crop_from,
9
+ :crop_size,
10
+ :flatten,
11
+ :format,
12
+ :quality,
13
+ :size,
14
+ :upsample
15
+ ]
16
+
17
+ attr_reader :picture, :variant, :thumb
18
+
19
+ # @param [Alchemy::Picture]
20
+ #
21
+ def initialize(picture)
22
+ @picture = picture
23
+ end
24
+
25
+ # The URL to a variant of a picture
26
+ #
27
+ # @return [String]
28
+ #
29
+ def call(variant_options = {})
30
+ set_variant(variant_options)
31
+
32
+ params = {
33
+ basename: picture.name,
34
+ ext: variant.render_format,
35
+ name: picture.name
36
+ }
37
+
38
+ return variant.image.url(params) unless processible_image?
39
+
40
+ "/#{uid}"
41
+ end
42
+
43
+ private
44
+
45
+ def set_variant(options = {})
46
+ @variant = PictureVariant.new(picture, options.slice(*TRANSFORMATION_OPTIONS))
47
+ end
48
+
49
+ def processible_image?
50
+ variant.image.is_a?(::Dragonfly::Job)
51
+ end
52
+
53
+ def uid
54
+ signature = PictureThumb::Signature.call(variant)
55
+ if find_thumb_by(signature)
56
+ thumb.uid
57
+ else
58
+ uid = PictureThumb::Uid.call(signature, variant)
59
+ ActiveRecord::Base.connected_to(role: ActiveRecord.writing_role) do
60
+ PictureThumb::Create.call(variant, signature, uid)
61
+ end
62
+ uid
63
+ end
64
+ end
65
+
66
+ def find_thumb_by(signature)
67
+ @thumb = if picture.thumbs.loaded?
68
+ picture.thumbs.find { |t| t.signature == signature }
69
+ else
70
+ picture.thumbs.find_by(signature: signature)
71
+ end
72
+ end
73
+ end
74
+ end
75
+ end
@@ -1,20 +1,20 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Alchemy
4
- class Picture < BaseRecord
5
- class Preprocessor
4
+ class StorageAdapter
5
+ class Dragonfly::Preprocessor
6
6
  def initialize(image_file)
7
7
  @image_file = image_file
8
8
  end
9
9
 
10
10
  # Preprocess images after upload
11
11
  #
12
- # Define preprocessing options in the Alchemy::Config
12
+ # Define preprocessing options in the Alchemy.config
13
13
  #
14
14
  # preprocess_image_resize [String] - Downsizing example: '1000x1000>'
15
15
  #
16
16
  def call
17
- max_image_size = Alchemy::Config.get(:preprocess_image_resize)
17
+ max_image_size = Alchemy.config.preprocess_image_resize
18
18
  image_file.thumb!(max_image_size) if max_image_size.present?
19
19
  # Auto orient the image so EXIF orientation data is taken into account
20
20
  image_file.auto_orient!
@@ -0,0 +1,205 @@
1
+ module Alchemy
2
+ class StorageAdapter
3
+ module Dragonfly
4
+ module PictureClassMethods
5
+ def self.included(base)
6
+ base.class_eval do
7
+ dragonfly_accessor :image_file, app: :alchemy_pictures do
8
+ # Preprocess after uploading the picture
9
+ after_assign do |image|
10
+ if has_convertible_format?
11
+ Alchemy.storage_adapter.preprocessor_class.new(image).call
12
+ end
13
+ end
14
+ end
15
+
16
+ has_many :thumbs, class_name: "Alchemy::PictureThumb", dependent: :destroy
17
+
18
+ before_save do
19
+ self.image_file_name = sanitized_filename(image_file_name)
20
+ end
21
+
22
+ # Create important thumbnails upfront
23
+ after_create -> { PictureThumb.generate_thumbs!(self) },
24
+ if: :has_convertible_format?
25
+ end
26
+ end
27
+ end
28
+
29
+ module AttachmentClassMethods
30
+ def self.included(base)
31
+ base.class_eval do
32
+ dragonfly_accessor :file, app: :alchemy_attachments do
33
+ after_assign { |file|
34
+ write_attribute(:file_mime_type, file.mime_type)
35
+ }
36
+ end
37
+
38
+ before_save do
39
+ self.file_name = sanitized_filename(file_name)
40
+ end
41
+ end
42
+ end
43
+ end
44
+
45
+ extend self
46
+
47
+ CONVERTIBLE_FILE_FORMATS = %w[gif jpg jpeg png webp].freeze
48
+
49
+ # Allows to set a custom Attachment Url class
50
+ def attachment_url_class=(klass)
51
+ @_attachment_url_class = klass
52
+ end
53
+
54
+ # Returns the class used to generate attachment urls
55
+ # @return [Class] - defaults to Alchemy::StorageAdapter::Dragonfly::AttachmentUrl
56
+ def attachment_url_class
57
+ @_attachment_url_class ||= AttachmentUrl
58
+ end
59
+
60
+ def preprocessor_class
61
+ Preprocessor
62
+ end
63
+
64
+ # Allows to set a custom Picture Url class
65
+ def picture_url_class=(klass)
66
+ @_picture_url_class = klass
67
+ end
68
+
69
+ # Returns the class used to generate picture urls
70
+ # @return [Class] - defaults to Alchemy::StorageAdapter::Dragonfly::PictureUrl
71
+ def picture_url_class
72
+ @_picture_url_class ||= PictureUrl
73
+ end
74
+
75
+ def file_formats(class_name, scope:)
76
+ mime_type_column = case class_name
77
+ when "Alchemy::Attachment" then :file_mime_type
78
+ when "Alchemy::Picture" then :image_file_format
79
+ end
80
+
81
+ scope.reorder(mime_type_column).distinct.pluck(mime_type_column).compact.presence || []
82
+ end
83
+
84
+ # @param [String]
85
+ # @return [Array<String>]
86
+ def searchable_alchemy_resource_attributes(class_name)
87
+ case class_name
88
+ when "Alchemy::Attachment"
89
+ %w[name file_name]
90
+ when "Alchemy::Picture"
91
+ %w[name image_file_name]
92
+ end
93
+ end
94
+ alias_method :ransackable_attributes, :searchable_alchemy_resource_attributes
95
+
96
+ # @param [String]
97
+ # @return [Array<String>]
98
+ def ransackable_associations(_class_name)
99
+ %w[]
100
+ end
101
+
102
+ def rescuable_errors
103
+ ::Dragonfly::Job::Fetch::NotFound
104
+ end
105
+
106
+ # @param [String]
107
+ # @return [Alchemy::Picture::ActiveRecord_Relation]
108
+ def by_file_format_scope(file_format)
109
+ Picture.where(image_file_format: file_format)
110
+ end
111
+
112
+ # @param [String]
113
+ # @return [Alchemy::Attachment::ActiveRecord_Relation]
114
+ def by_file_type_scope(file_type)
115
+ Attachment.where(file_mime_type: file_type)
116
+ end
117
+
118
+ # @param [Alchemy::Attachment]
119
+ # @return [String]
120
+ def file_name(attachment)
121
+ attachment.read_attribute(:file_name)
122
+ end
123
+
124
+ # @param [Alchemy::Attachment]
125
+ # @return [Integer]
126
+ def file_size(attachment)
127
+ attachment.read_attribute(:file_size)
128
+ end
129
+
130
+ # @param [Alchemy::Attachment]
131
+ # @return [String]
132
+ def file_mime_type(attachment)
133
+ attachment.read_attribute(:file_mime_type)
134
+ end
135
+
136
+ # @param [Alchemy::Attachment]
137
+ # @return [String]
138
+ def file_extension(attachment)
139
+ content_type = file_mime_type(attachment)
140
+ Marcel::Magic.new(content_type).extensions.first if content_type
141
+ end
142
+
143
+ # @param [Alchemy::Picture]
144
+ # @return [TrueClass, FalseClass]
145
+ def has_convertible_format?(picture)
146
+ image_file_extension(picture).in?(CONVERTIBLE_FILE_FORMATS)
147
+ end
148
+
149
+ # @param [Alchemy::Picture]
150
+ # @return [String]
151
+ def image_file_name(picture)
152
+ picture.read_attribute(:image_file_name)
153
+ end
154
+
155
+ # @param [Alchemy::Picture]
156
+ # @return [String]
157
+ def image_file_format(picture)
158
+ ext = picture.read_attribute(:image_file_format)
159
+ Marcel::MimeType.for(extension: ext) if ext
160
+ end
161
+
162
+ # @param [Alchemy::Picture]
163
+ # @return [Integer]
164
+ def image_file_size(picture)
165
+ picture.read_attribute(:image_file_size)
166
+ end
167
+
168
+ # @param [Alchemy::Picture]
169
+ # @return [Integer]
170
+ def image_file_width(picture)
171
+ picture.read_attribute(:image_file_width)
172
+ end
173
+
174
+ # @param [Alchemy::Picture]
175
+ # @return [Integer]
176
+ def image_file_height(picture)
177
+ picture.read_attribute(:image_file_height)
178
+ end
179
+
180
+ # @param [Alchemy::Picture]
181
+ # @return [String]
182
+ def image_file_extension(picture)
183
+ picture.read_attribute(:image_file_format)
184
+ end
185
+
186
+ # @param [Alchemy::Picture]
187
+ # @return [TrueClass, FalseClass]
188
+ def image_file_present?(picture)
189
+ !!picture.image_file
190
+ end
191
+
192
+ # @param Alchemy::Picture::ActiveRecord_Relation
193
+ # @return Alchemy::Picture::ActiveRecord_Relation
194
+ def preloaded_pictures(pictures)
195
+ pictures.includes(:thumbs)
196
+ end
197
+
198
+ # @param [Alchemy::Attachment]
199
+ # @return [TrueClass, FalseClass]
200
+ def set_attachment_name?(attachment)
201
+ attachment.file_name_changed?
202
+ end
203
+ end
204
+ end
205
+ end
@@ -0,0 +1,74 @@
1
+ module Alchemy
2
+ class StorageAdapter
3
+ class UnknownAdapterError < StandardError; end
4
+
5
+ attr_reader :name, :adapter
6
+
7
+ delegate(
8
+ :attachment_url_class,
9
+ :by_file_format_scope,
10
+ :by_file_type_scope,
11
+ :file_extension,
12
+ :file_formats,
13
+ :file_mime_type,
14
+ :file_name,
15
+ :file_size,
16
+ :has_convertible_format?,
17
+ :image_file_extension,
18
+ :image_file_format,
19
+ :image_file_height,
20
+ :image_file_name,
21
+ :image_file_present?,
22
+ :image_file_size,
23
+ :image_file_width,
24
+ :picture_url_class,
25
+ :preloaded_pictures,
26
+ :preprocessor_class,
27
+ :ransackable_associations,
28
+ :ransackable_attributes,
29
+ :rescuable_errors,
30
+ :searchable_alchemy_resource_attributes,
31
+ :set_attachment_name?,
32
+ to: :adapter
33
+ )
34
+
35
+ def initialize(name)
36
+ @name = name.to_sym
37
+ @adapter = adapter_class
38
+ end
39
+
40
+ def ==(other)
41
+ name == other.to_sym
42
+ end
43
+
44
+ def active_storage?
45
+ name == :active_storage
46
+ end
47
+
48
+ def dragonfly?
49
+ name == :dragonfly
50
+ end
51
+
52
+ def picture_class_methods
53
+ adapter::PictureClassMethods
54
+ end
55
+
56
+ def attachment_class_methods
57
+ adapter::AttachmentClassMethods
58
+ end
59
+
60
+ private
61
+
62
+ def adapter_class
63
+ case name
64
+ when :active_storage
65
+ ActiveStorage
66
+ when :dragonfly
67
+ Dragonfly
68
+ else
69
+ raise UnknownAdapterError,
70
+ "Unknown storage adapter: #{name}. Please use either 'active_storage' or 'dragonfly'."
71
+ end
72
+ end
73
+ end
74
+ end
@@ -28,7 +28,7 @@ module Alchemy
28
28
  #
29
29
  # @option options format [String]
30
30
  # The format the picture should be rendered in.
31
- # Defaults to the +image_output_format+ from the +Alchemy::Config+.
31
+ # Defaults to the +image_output_format+ from the +Alchemy.config+.
32
32
  #
33
33
  # @option options crop [Boolean]
34
34
  # If set to true the picture will be cropped to fit the size value.
@@ -84,7 +84,7 @@ module Alchemy
84
84
  crop_from: crop && crop_from.presence || default_crop_from&.join("x"),
85
85
  crop_size: crop && crop_size.presence || default_crop_size&.join("x"),
86
86
  flatten: true,
87
- format: picture&.image_file_format || "jpg"
87
+ format: picture&.image_file_extension || "jpg"
88
88
  }
89
89
  end
90
90
 
@@ -102,14 +102,27 @@ module Alchemy
102
102
 
103
103
  # Show image cropping link for ingredient
104
104
  def allow_image_cropping?
105
- settings[:crop] && picture&.can_be_cropped_to?(
106
- settings[:size],
107
- settings[:upsample]
108
- ) && !!picture.image_file
105
+ settings[:crop] && picture &&
106
+ Alchemy.storage_adapter.image_file_present?(picture) &&
107
+ can_be_cropped_to?
109
108
  end
110
109
 
111
110
  private
112
111
 
112
+ # An Image smaller than dimensions
113
+ # can not be cropped to given size - unless upsample is true.
114
+ #
115
+ def can_be_cropped_to?
116
+ return true if settings[:upsample]
117
+
118
+ dimensions = inferred_dimensions_from_string(settings[:size])
119
+ if dimensions
120
+ picture.image_file_width > dimensions[0] && picture.image_file_height > dimensions[1]
121
+ else
122
+ false
123
+ end
124
+ end
125
+
113
126
  def default_crop_size
114
127
  return nil unless settings[:crop] && settings[:size]
115
128