venus_media_library 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 (28) hide show
  1. checksums.yaml +7 -0
  2. data/CHANGELOG.md +31 -0
  3. data/MIT-LICENSE +20 -0
  4. data/README.md +342 -0
  5. data/Rakefile +6 -0
  6. data/app/assets/javascripts/venus_media_library/venus_media_library.js +171 -0
  7. data/app/assets/stylesheets/venus_media_library/application.css +15 -0
  8. data/app/assets/stylesheets/venus_media_library/picker.css +82 -0
  9. data/app/controllers/venus_media_library/application_controller.rb +19 -0
  10. data/app/controllers/venus_media_library/images_controller.rb +82 -0
  11. data/app/controllers/venus_media_library/pickers_controller.rb +22 -0
  12. data/app/helpers/venus_media_library/application_helper.rb +4 -0
  13. data/app/helpers/venus_media_library/images_helper.rb +46 -0
  14. data/app/helpers/venus_media_library/picker_helper.rb +144 -0
  15. data/app/jobs/venus_media_library/application_job.rb +4 -0
  16. data/app/mailers/venus_media_library/application_mailer.rb +6 -0
  17. data/app/models/venus_media_library/application_record.rb +5 -0
  18. data/app/views/layouts/venus_media_library/application.html.erb +17 -0
  19. data/app/views/venus_media_library/images/_image.html.erb +10 -0
  20. data/app/views/venus_media_library/images/index.html.erb +23 -0
  21. data/app/views/venus_media_library/pickers/_modal.html.erb +16 -0
  22. data/app/views/venus_media_library/pickers/_picker.html.erb +27 -0
  23. data/config/routes.rb +9 -0
  24. data/lib/tasks/venus_media_library_tasks.rake +4 -0
  25. data/lib/venus_media_library/engine.rb +25 -0
  26. data/lib/venus_media_library/version.rb +3 -0
  27. data/lib/venus_media_library.rb +73 -0
  28. metadata +104 -0
@@ -0,0 +1,19 @@
1
+ module VenusMediaLibrary
2
+ class ApplicationController < ActionController::Base
3
+ # Run the host-configured access gate (if any) before every engine action.
4
+ # The engine ships open; the host restricts the picker/upload endpoints by
5
+ # setting VenusMediaLibrary.configuration.authenticate_with to a proc.
6
+ before_action :authenticate_venus_media_library_access!
7
+
8
+ private
9
+
10
+ def authenticate_venus_media_library_access!
11
+ gate = VenusMediaLibrary.configuration.authenticate_with
12
+ return unless gate.respond_to?(:call)
13
+
14
+ # instance_exec so the proc runs in this controller's context and can use
15
+ # host helpers (current_user, redirect_to, head, main_app, ...).
16
+ instance_exec(&gate)
17
+ end
18
+ end
19
+ end
@@ -0,0 +1,82 @@
1
+ module VenusMediaLibrary
2
+ # Lists and uploads images backed by Active Storage. Storage-agnostic: it uses
3
+ # whatever service the host app configures (Disk in dev, S3 in prod, ...).
4
+ class ImagesController < ApplicationController
5
+ include VenusMediaLibrary::ImagesHelper
6
+
7
+ # GET /images
8
+ # Lists image blobs, newest first, with simple offset pagination.
9
+ # Responds with an HTML grid or a JSON payload for the picker.
10
+ def index
11
+ @page = [ params.fetch(:page, 1).to_i, 1 ].max
12
+ @per_page = per_page
13
+ offset = (@page - 1) * @per_page
14
+
15
+ scope = image_blobs
16
+ @total_count = scope.count
17
+ @blobs = scope.order(created_at: :desc).offset(offset).limit(@per_page).to_a
18
+ @has_more = offset + @blobs.size < @total_count
19
+ @images = @blobs.map { |blob| ml_image_payload(blob) }
20
+
21
+ respond_to do |format|
22
+ format.html # index.html.erb
23
+ format.json do
24
+ render json: { images: @images, page: @page, has_more: @has_more, total: @total_count }
25
+ end
26
+ end
27
+ end
28
+
29
+ # POST /images
30
+ # Accepts an uploaded file and stores it via Active Storage.
31
+ def create
32
+ uploaded = params[:file] || params.dig(:image, :file)
33
+
34
+ if uploaded.blank?
35
+ return respond_error("No file was uploaded.", :unprocessable_entity)
36
+ end
37
+
38
+ unless allowed_content_type?(uploaded.content_type)
39
+ return respond_error("Content type #{uploaded.content_type} is not allowed.", :unprocessable_entity)
40
+ end
41
+
42
+ blob = ActiveStorage::Blob.create_and_upload!(
43
+ io: uploaded.tempfile,
44
+ filename: uploaded.original_filename,
45
+ content_type: uploaded.content_type,
46
+ service_name: VenusMediaLibrary.configuration.storage_service
47
+ )
48
+
49
+ respond_to do |format|
50
+ format.json { render json: ml_image_payload(blob), status: :created }
51
+ format.html { redirect_to images_path }
52
+ end
53
+ end
54
+
55
+ private
56
+
57
+ def image_blobs
58
+ ActiveStorage::Blob.where("content_type LIKE ?", "image/%")
59
+ end
60
+
61
+ def per_page
62
+ value = params[:per_page].presence&.to_i
63
+ value && value.positive? ? value : VenusMediaLibrary.configuration.per_page
64
+ end
65
+
66
+ def allowed_content_type?(content_type)
67
+ return false if content_type.blank?
68
+
69
+ allowed = VenusMediaLibrary.configuration.allowed_content_types
70
+ return content_type.start_with?("image/") if allowed.blank?
71
+
72
+ allowed.include?(content_type) || content_type.start_with?("image/")
73
+ end
74
+
75
+ def respond_error(message, status)
76
+ respond_to do |format|
77
+ format.json { render json: { error: message }, status: status }
78
+ format.html { redirect_to images_path, alert: message }
79
+ end
80
+ end
81
+ end
82
+ end
@@ -0,0 +1,22 @@
1
+ module VenusMediaLibrary
2
+ # Renders the picker modal body as a Turbo Frame. `target` is the DOM id of the
3
+ # host input that a chosen image URL / signed_id should be written into.
4
+ class PickersController < ApplicationController
5
+ include VenusMediaLibrary::ImagesHelper
6
+
7
+ def show
8
+ @target = params[:target].to_s
9
+ @page = [ params.fetch(:page, 1).to_i, 1 ].max
10
+ per = VenusMediaLibrary.configuration.per_page
11
+ offset = (@page - 1) * per
12
+
13
+ scope = ActiveStorage::Blob.where("content_type LIKE ?", "image/%")
14
+ @has_more = offset + per < scope.count
15
+ @images = scope.order(created_at: :desc).offset(offset).limit(per).map { |blob| ml_image_payload(blob) }
16
+
17
+ render partial: "venus_media_library/pickers/picker",
18
+ locals: { target: @target, images: @images, page: @page, has_more: @has_more },
19
+ layout: false
20
+ end
21
+ end
22
+ end
@@ -0,0 +1,4 @@
1
+ module VenusMediaLibrary
2
+ module ApplicationHelper
3
+ end
4
+ end
@@ -0,0 +1,46 @@
1
+ module VenusMediaLibrary
2
+ # Shared image URL/payload logic. Included in the views (automatically) and in
3
+ # ImagesController so the HTML grid and the JSON response stay in sync.
4
+ module ImagesHelper
5
+ # A serializable description of a blob used by the JSON API and the tiles.
6
+ def ml_image_payload(blob)
7
+ {
8
+ id: blob.id,
9
+ signed_id: blob.signed_id,
10
+ filename: blob.filename.to_s,
11
+ content_type: blob.content_type,
12
+ byte_size: blob.byte_size,
13
+ created_at: blob.created_at,
14
+ url: ml_blob_url(blob),
15
+ thumb_url: ml_thumb_url(blob)
16
+ }
17
+ end
18
+
19
+ # Permanent URL to the original file. Active Storage route helpers live on the
20
+ # host app, so they are reached through the `main_app` proxy. With
21
+ # url_type = :proxy the URL streams through Rails (works for private buckets
22
+ # and external crawlers); the default :redirect 302s to the storage URL.
23
+ def ml_blob_url(blob)
24
+ if VenusMediaLibrary.configuration.url_type == :proxy
25
+ main_app.rails_storage_proxy_url(blob)
26
+ else
27
+ main_app.rails_blob_url(blob)
28
+ end
29
+ rescue StandardError
30
+ main_app.rails_blob_path(blob)
31
+ end
32
+
33
+ # Small variant used for the grid thumbnail. Falls back to the original URL
34
+ # when the blob can't be variated (e.g. SVG).
35
+ def ml_thumb_url(blob)
36
+ if blob.variable?
37
+ w, h = VenusMediaLibrary.configuration.thumbnail_size
38
+ main_app.rails_representation_url(blob.variant(resize_to_limit: [ w, h ]))
39
+ else
40
+ ml_blob_url(blob)
41
+ end
42
+ rescue StandardError
43
+ ml_blob_url(blob)
44
+ end
45
+ end
46
+ end
@@ -0,0 +1,144 @@
1
+ module VenusMediaLibrary
2
+ # Host-facing helper. Included into the host app's views by the engine (see
3
+ # VenusMediaLibrary::Engine), so apps can drop the picker next to any URL field.
4
+ #
5
+ # <%= form_with model: @page do |f| %>
6
+ # <%= media_picker_field f, :og_image %>
7
+ # <% end %>
8
+ #
9
+ # Renders a text input (the field the host already had) plus a "Choose from
10
+ # library" button wired to open the modal. Selecting an image writes its URL
11
+ # into the input; a hidden `<field>_signed_id` input also receives the blob's
12
+ # signed id so the host can attach it if desired.
13
+ module PickerHelper
14
+ # form - a form builder (form_with / form_for)
15
+ # field - the attribute holding the image URL (e.g. :og_image)
16
+ # opts:
17
+ # :label - button label (default "Choose from library")
18
+ # :placeholder - text input placeholder
19
+ # :signed_id_field - name for the hidden signed-id input
20
+ # (default "<field>_signed_id"); pass false to skip
21
+ # :input_html - extra HTML options merged into the text input
22
+ # :class - wrapper CSS class
23
+ def media_picker_field(form, field, **opts)
24
+ input_id = "#{form.object_name}_#{field}".parameterize.tr("-", "_")
25
+ label = opts.fetch(:label, "Choose from library")
26
+ wrapper = opts.fetch(:class, "ml-field")
27
+ input_html = {
28
+ id: input_id,
29
+ class: "ml-field__input",
30
+ placeholder: opts[:placeholder]
31
+ }.merge(opts[:input_html] || {})
32
+
33
+ signed_id_field = opts.fetch(:signed_id_field, "#{field}_signed_id")
34
+
35
+ # Endpoint the JS uses to load the picker frame for this field.
36
+ picker_src = venus_media_library.picker_path(target: input_id)
37
+
38
+ content_tag(:div, class: wrapper, data: { ml_field: input_id }) do
39
+ parts = []
40
+ parts << form.text_field(field, input_html)
41
+
42
+ if signed_id_field
43
+ hidden_id = "#{input_id}_signed_id"
44
+ parts << form.hidden_field(signed_id_field, id: hidden_id, data: { ml_signed_id_for: input_id })
45
+ end
46
+
47
+ parts << button_tag(label,
48
+ type: "button",
49
+ class: "ml-btn ml-btn--primary ml-field__button",
50
+ data: { ml_open: true, ml_target: input_id, ml_src: picker_src })
51
+
52
+ safe_join(parts)
53
+ end + venus_media_library_modal_shell
54
+ end
55
+
56
+ # Attach-mode picker. Use this for an Active Storage attachment (has_one_attached)
57
+ # rather than a URL column: the picker writes the chosen blob's signed_id into a
58
+ # hidden field NAMED FOR THE ATTACHMENT (e.g. product[cover]), so Rails attaches
59
+ # that blob on save.
60
+ #
61
+ # <%= form_with model: @product do |f| %>
62
+ # <%= media_attach_field f, :cover %>
63
+ # <% end %>
64
+ #
65
+ # The visible input is a read-only preview (it is NOT submitted). The hidden
66
+ # attachment field is `disabled` until an image is picked, because submitting an
67
+ # empty string for a has_one_attached would DETACH the current file; the JS sets
68
+ # the signed_id and enables the field on selection.
69
+ #
70
+ # form - a form builder bound to the record
71
+ # attachment - the has_one_attached name (e.g. :cover)
72
+ # opts:
73
+ # :label - button label (default "Choose from library")
74
+ # :placeholder - preview input placeholder
75
+ # :input_html - extra HTML options merged into the preview input
76
+ # :class - wrapper CSS class
77
+ def media_attach_field(form, attachment, **opts)
78
+ input_id = "#{form.object_name}_#{attachment}".parameterize.tr("-", "_")
79
+ label = opts.fetch(:label, "Choose from library")
80
+ wrapper = opts.fetch(:class, "ml-field")
81
+
82
+ preview_html = {
83
+ id: input_id,
84
+ class: "ml-field__input",
85
+ type: "text",
86
+ readonly: true,
87
+ value: ml_attachment_preview_value(form.object, attachment),
88
+ placeholder: opts[:placeholder]
89
+ }.merge(opts[:input_html] || {})
90
+
91
+ picker_src = venus_media_library.picker_path(target: input_id)
92
+
93
+ content_tag(:div, class: wrapper, data: { ml_field: input_id }) do
94
+ parts = []
95
+
96
+ # Read-only preview of the current/chosen image. No name -> not submitted.
97
+ parts << tag.input(**preview_html)
98
+
99
+ # Hidden field named for the attachment. Disabled (so a blank value can't
100
+ # detach on save) until the JS writes a signed_id and enables it.
101
+ parts << form.hidden_field(attachment,
102
+ value: "",
103
+ disabled: true,
104
+ id: "#{input_id}_signed_id",
105
+ data: { ml_signed_id_for: input_id })
106
+
107
+ parts << button_tag(label,
108
+ type: "button",
109
+ class: "ml-btn ml-btn--primary ml-field__button",
110
+ data: { ml_open: true, ml_target: input_id, ml_src: picker_src })
111
+
112
+ safe_join(parts)
113
+ end + venus_media_library_modal_shell
114
+ end
115
+
116
+ # Best-effort URL for the currently attached file, used to seed the preview.
117
+ # Rescues to the filename (or blank) so a missing host/route never breaks the form.
118
+ def ml_attachment_preview_value(record, attachment)
119
+ attached = record.respond_to?(attachment) && record.public_send(attachment)
120
+ return "" unless attached && attached.respond_to?(:attached?) && attached.attached?
121
+
122
+ if VenusMediaLibrary.configuration.url_type == :proxy
123
+ rails_storage_proxy_url(attached)
124
+ else
125
+ rails_blob_url(attached)
126
+ end
127
+ rescue StandardError
128
+ begin
129
+ attached && attached.attached? ? attached.filename.to_s : ""
130
+ rescue StandardError
131
+ ""
132
+ end
133
+ end
134
+
135
+ # Renders the shared modal shell once per page. The picker frame is lazy
136
+ # loaded into it when a button is clicked.
137
+ def venus_media_library_modal_shell
138
+ return "".html_safe if @_venus_media_library_modal_rendered
139
+
140
+ @_venus_media_library_modal_rendered = true
141
+ render partial: "venus_media_library/pickers/modal"
142
+ end
143
+ end
144
+ end
@@ -0,0 +1,4 @@
1
+ module VenusMediaLibrary
2
+ class ApplicationJob < ActiveJob::Base
3
+ end
4
+ end
@@ -0,0 +1,6 @@
1
+ module VenusMediaLibrary
2
+ class ApplicationMailer < ActionMailer::Base
3
+ default from: "from@example.com"
4
+ layout "mailer"
5
+ end
6
+ end
@@ -0,0 +1,5 @@
1
+ module VenusMediaLibrary
2
+ class ApplicationRecord < ActiveRecord::Base
3
+ self.abstract_class = true
4
+ end
5
+ end
@@ -0,0 +1,17 @@
1
+ <!DOCTYPE html>
2
+ <html>
3
+ <head>
4
+ <title>Media library</title>
5
+ <%= csrf_meta_tags %>
6
+ <%= csp_meta_tag %>
7
+
8
+ <%= yield :head %>
9
+
10
+ <%= stylesheet_link_tag "venus_media_library/application", media: "all" %>
11
+ </head>
12
+ <body>
13
+
14
+ <%= yield %>
15
+
16
+ </body>
17
+ </html>
@@ -0,0 +1,10 @@
1
+ <%# Renders one selectable image tile. `image` is a payload hash from the controller. %>
2
+ <button type="button"
3
+ class="ml-tile"
4
+ data-ml-signed-id="<%= image[:signed_id] %>"
5
+ data-ml-url="<%= image[:url] %>"
6
+ data-ml-filename="<%= image[:filename] %>"
7
+ title="<%= image[:filename] %>">
8
+ <img src="<%= image[:thumb_url] %>" alt="<%= image[:filename] %>" loading="lazy">
9
+ <span class="ml-tile__name"><%= image[:filename] %></span>
10
+ </button>
@@ -0,0 +1,23 @@
1
+ <div class="ml-library">
2
+ <header class="ml-library__header">
3
+ <h1>Media Library</h1>
4
+ <p class="ml-library__count"><%= @total_count %> image<%= "s" unless @total_count == 1 %></p>
5
+ </header>
6
+
7
+ <% if @images.empty? %>
8
+ <p class="ml-empty">No images yet. Upload one to get started.</p>
9
+ <% else %>
10
+ <div class="ml-grid" data-ml-grid>
11
+ <%= render partial: "venus_media_library/images/image", collection: @images, as: :image %>
12
+ </div>
13
+ <% end %>
14
+
15
+ <nav class="ml-pagination">
16
+ <% if @page > 1 %>
17
+ <%= link_to "← Newer", images_path(page: @page - 1), class: "ml-btn" %>
18
+ <% end %>
19
+ <% if @has_more %>
20
+ <%= link_to "Older →", images_path(page: @page + 1), class: "ml-btn" %>
21
+ <% end %>
22
+ </nav>
23
+ </div>
@@ -0,0 +1,16 @@
1
+ <%# Shared modal shell. Rendered once per page by media_picker_field. The picker
2
+ Turbo Frame is loaded into #venus_media_library_picker on open. %>
3
+ <div id="ml-modal" class="ml-modal" hidden aria-hidden="true" role="dialog" aria-modal="true" aria-label="Media Library">
4
+ <div class="ml-modal__backdrop" data-ml-close></div>
5
+ <div class="ml-modal__dialog">
6
+ <div class="ml-modal__header">
7
+ <h2 class="ml-modal__title">Media Library</h2>
8
+ <button type="button" class="ml-modal__close" data-ml-close aria-label="Close">&times;</button>
9
+ </div>
10
+ <div class="ml-modal__body">
11
+ <turbo-frame id="venus_media_library_picker">
12
+ <p class="ml-empty">Loading…</p>
13
+ </turbo-frame>
14
+ </div>
15
+ </div>
16
+ </div>
@@ -0,0 +1,27 @@
1
+ <%# Picker body loaded into the modal as a Turbo Frame. `target` is the id of the
2
+ host input to receive the chosen image. %>
3
+ <turbo-frame id="venus_media_library_picker" class="ml-picker" data-ml-target="<%= target %>">
4
+ <div class="ml-picker__toolbar">
5
+ <label class="ml-btn ml-btn--primary">
6
+ Upload image
7
+ <input type="file" accept="image/*" hidden data-ml-upload>
8
+ </label>
9
+ <span class="ml-picker__hint" data-ml-status></span>
10
+ </div>
11
+
12
+ <% if images.empty? %>
13
+ <p class="ml-empty">No images yet. Upload one above.</p>
14
+ <% else %>
15
+ <div class="ml-grid" data-ml-grid>
16
+ <%= render partial: "venus_media_library/images/image", collection: images, as: :image %>
17
+ </div>
18
+ <% end %>
19
+
20
+ <% if has_more %>
21
+ <div class="ml-picker__more">
22
+ <a href="<%= picker_path(target: target, page: page + 1) %>"
23
+ data-turbo-frame="venus_media_library_picker"
24
+ class="ml-btn">Load older</a>
25
+ </div>
26
+ <% end %>
27
+ </turbo-frame>
data/config/routes.rb ADDED
@@ -0,0 +1,9 @@
1
+ VenusMediaLibrary::Engine.routes.draw do
2
+ # Image library: index lists images (HTML grid + JSON), create handles uploads.
3
+ resources :images, only: [ :index, :create ]
4
+
5
+ # Turbo Frame that renders the picker modal body for a given target field.
6
+ get "picker", to: "pickers#show", as: :picker
7
+
8
+ root to: "images#index"
9
+ end
@@ -0,0 +1,4 @@
1
+ # desc "Explaining what the task does"
2
+ # task :venus_media_library do
3
+ # # Task goes here
4
+ # end
@@ -0,0 +1,25 @@
1
+ module VenusMediaLibrary
2
+ class Engine < ::Rails::Engine
3
+ isolate_namespace VenusMediaLibrary
4
+
5
+ # Make the host-facing picker helper available in the host app's views so
6
+ # apps can call `media_picker_field` without any manual include.
7
+ initializer "venus_media_library.host_helpers" do
8
+ ActiveSupport.on_load(:action_view) do
9
+ include VenusMediaLibrary::PickerHelper
10
+ end
11
+ end
12
+
13
+ # Ensure the engine's JS is discoverable by Propshaft/Sprockets in the host.
14
+ initializer "venus_media_library.assets" do |app|
15
+ if app.config.respond_to?(:assets)
16
+ app.config.assets.paths << root.join("app/assets/javascripts")
17
+ app.config.assets.precompile += %w[venus_media_library/venus_media_library.js]
18
+ end
19
+ end
20
+
21
+ config.generators do |g|
22
+ g.test_framework :rspec
23
+ end
24
+ end
25
+ end
@@ -0,0 +1,3 @@
1
+ module VenusMediaLibrary
2
+ VERSION = "0.1.0"
3
+ end
@@ -0,0 +1,73 @@
1
+ require "venus_media_library/version"
2
+ require "venus_media_library/engine"
3
+
4
+ module VenusMediaLibrary
5
+ # Host apps configure the engine through `VenusMediaLibrary.configure`:
6
+ #
7
+ # VenusMediaLibrary.configure do |config|
8
+ # config.allowed_content_types = %w[image/png image/jpeg image/webp]
9
+ # config.thumbnail_size = [ 300, 300 ]
10
+ # config.per_page = 40
11
+ # config.storage_service = :amazon
12
+ # end
13
+ class Configuration
14
+ # Content types accepted by the uploader and shown in the library grid.
15
+ # Every entry is matched literally; the index additionally shows any blob
16
+ # whose content_type starts with "image/".
17
+ attr_accessor :allowed_content_types
18
+
19
+ # [width, height] used for the grid thumbnail variant.
20
+ attr_accessor :thumbnail_size
21
+
22
+ # Number of images per page in the index.
23
+ attr_accessor :per_page
24
+
25
+ # Optional Active Storage service name to attach uploads to. When nil the
26
+ # host app's default service (`config.active_storage.service`) is used, so
27
+ # the engine stays storage-agnostic (Disk in dev, S3 in prod, etc.).
28
+ attr_accessor :storage_service
29
+
30
+ # How image URLs in the payload/picker are built:
31
+ # :redirect (default) -> rails_blob_url (302s to the storage URL)
32
+ # :proxy -> rails_storage_proxy_url (Rails streams the bytes)
33
+ # Use :proxy when the bucket is private and images must be publicly fetchable
34
+ # by external crawlers (e.g. an og:image on a private S3 bucket in proxy mode).
35
+ # Still storage-agnostic: both are Active Storage route helpers.
36
+ attr_accessor :url_type
37
+
38
+ # Optional access gate. A proc run in the engine controller's context before
39
+ # every action (via instance_exec), so it can call host helpers like
40
+ # `current_user`, `redirect_to`, or `head`. Left nil the engine is open; the
41
+ # host sets this to restrict the picker/upload endpoints to admins.
42
+ #
43
+ # VenusMediaLibrary.configure do |c|
44
+ # c.authenticate_with = -> { redirect_to main_app.root_path unless current_user&.admin? }
45
+ # end
46
+ attr_accessor :authenticate_with
47
+
48
+ def initialize
49
+ @allowed_content_types = %w[image/png image/jpeg image/jpg image/gif image/webp image/svg+xml]
50
+ @thumbnail_size = [ 300, 300 ]
51
+ @per_page = 40
52
+ @storage_service = nil
53
+ @url_type = :redirect
54
+ @authenticate_with = nil
55
+ end
56
+ end
57
+
58
+ class << self
59
+ def configuration
60
+ @configuration ||= Configuration.new
61
+ end
62
+
63
+ def configure
64
+ yield(configuration) if block_given?
65
+ configuration
66
+ end
67
+
68
+ # Test/host reset hook.
69
+ def reset_configuration!
70
+ @configuration = Configuration.new
71
+ end
72
+ end
73
+ end
metadata ADDED
@@ -0,0 +1,104 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: venus_media_library
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.0
5
+ platform: ruby
6
+ authors:
7
+ - Good Works On Earth
8
+ bindir: bin
9
+ cert_chain: []
10
+ date: 1980-01-02 00:00:00.000000000 Z
11
+ dependencies:
12
+ - !ruby/object:Gem::Dependency
13
+ name: rails
14
+ requirement: !ruby/object:Gem::Requirement
15
+ requirements:
16
+ - - ">="
17
+ - !ruby/object:Gem::Version
18
+ version: '7.1'
19
+ type: :runtime
20
+ prerelease: false
21
+ version_requirements: !ruby/object:Gem::Requirement
22
+ requirements:
23
+ - - ">="
24
+ - !ruby/object:Gem::Version
25
+ version: '7.1'
26
+ - !ruby/object:Gem::Dependency
27
+ name: image_processing
28
+ requirement: !ruby/object:Gem::Requirement
29
+ requirements:
30
+ - - "~>"
31
+ - !ruby/object:Gem::Version
32
+ version: '1.12'
33
+ type: :runtime
34
+ prerelease: false
35
+ version_requirements: !ruby/object:Gem::Requirement
36
+ requirements:
37
+ - - "~>"
38
+ - !ruby/object:Gem::Version
39
+ version: '1.12'
40
+ description: |-
41
+ Media Library is a mountable Rails engine that lists images stored in Active Storage
42
+ (S3, Google Cloud Storage, Disk — whatever the host app configures) and gives content
43
+ editors a modal image picker. Drop `media_picker_field` next to any URL field (such as
44
+ an og:image field) so editors can select an existing image or upload a new one instead
45
+ of typing a path. Storage-agnostic, namespaced, and publishable.
46
+ email:
47
+ - hello@goodworksonearth.org
48
+ executables: []
49
+ extensions: []
50
+ extra_rdoc_files: []
51
+ files:
52
+ - CHANGELOG.md
53
+ - MIT-LICENSE
54
+ - README.md
55
+ - Rakefile
56
+ - app/assets/javascripts/venus_media_library/venus_media_library.js
57
+ - app/assets/stylesheets/venus_media_library/application.css
58
+ - app/assets/stylesheets/venus_media_library/picker.css
59
+ - app/controllers/venus_media_library/application_controller.rb
60
+ - app/controllers/venus_media_library/images_controller.rb
61
+ - app/controllers/venus_media_library/pickers_controller.rb
62
+ - app/helpers/venus_media_library/application_helper.rb
63
+ - app/helpers/venus_media_library/images_helper.rb
64
+ - app/helpers/venus_media_library/picker_helper.rb
65
+ - app/jobs/venus_media_library/application_job.rb
66
+ - app/mailers/venus_media_library/application_mailer.rb
67
+ - app/models/venus_media_library/application_record.rb
68
+ - app/views/layouts/venus_media_library/application.html.erb
69
+ - app/views/venus_media_library/images/_image.html.erb
70
+ - app/views/venus_media_library/images/index.html.erb
71
+ - app/views/venus_media_library/pickers/_modal.html.erb
72
+ - app/views/venus_media_library/pickers/_picker.html.erb
73
+ - config/routes.rb
74
+ - lib/tasks/venus_media_library_tasks.rake
75
+ - lib/venus_media_library.rb
76
+ - lib/venus_media_library/engine.rb
77
+ - lib/venus_media_library/version.rb
78
+ homepage: https://github.com/goodworksonearth/venus_media_library
79
+ licenses:
80
+ - MIT
81
+ metadata:
82
+ source_code_uri: https://github.com/goodworksonearth/venus_media_library
83
+ changelog_uri: https://github.com/goodworksonearth/venus_media_library/blob/main/CHANGELOG.md
84
+ bug_tracker_uri: https://github.com/goodworksonearth/venus_media_library/issues
85
+ rubygems_mfa_required: 'true'
86
+ rdoc_options: []
87
+ require_paths:
88
+ - lib
89
+ required_ruby_version: !ruby/object:Gem::Requirement
90
+ requirements:
91
+ - - ">="
92
+ - !ruby/object:Gem::Version
93
+ version: '3.2'
94
+ required_rubygems_version: !ruby/object:Gem::Requirement
95
+ requirements:
96
+ - - ">="
97
+ - !ruby/object:Gem::Version
98
+ version: '0'
99
+ requirements: []
100
+ rubygems_version: 3.6.9
101
+ specification_version: 4
102
+ summary: A mountable Rails engine that turns Active Storage into a browsable media
103
+ library with an image picker.
104
+ test_files: []