From c72279a470c93ffd3f0b1ffe3c01fe496f64bf05 Mon Sep 17 00:00:00 2001 From: Zsolt Tasnadi Date: Tue, 4 Aug 2026 19:18:28 +0200 Subject: [PATCH] Phase 5: ship the catalog ActiveAdmin resources from the engine MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - the 7 catalog admin files (softwares, releases, external_links, platform_links, images, files page, downloads) move to the engine's app/admin; the host ActiveAdmin instance loads them via ActiveAdmin.application.load_paths (single admin, URLs unchanged) - engine initializers: Zeitwerk ignore for app/admin (production eager load) and load_paths + watchable_dirs registration, guarded by defined?(ActiveAdmin) so the admin-less dummy app boots - images admin reads image owners from WarpEngine.config.image_owners; the host registers the Member owner in config/initializers/warp_engine.rb; the interim ImageUsage registry is gone - fix: SoftwareImage.distinct.pluck clashed with its order(:position) default scope on MySQL (unscope(:order)) — introduced in phase 0, caught by the first authenticated /admin/images smoke test - files page: download links use the public /file/ URL instead of the raw container path; the picker's stored path comes from config Verified: both suites green, zeitwerk:check (production) clean, JSON baselines intact, authenticated admin smoke test on every page incl. the 3-level nested software form and Files picker mode. Co-Authored-By: Claude Fable 5 --- app/admin/downloads.rb | 106 +++++++++++++++ app/admin/external_links.rb | 34 +++++ app/admin/files.rb | 255 ++++++++++++++++++++++++++++++++++++ app/admin/images.rb | 79 +++++++++++ app/admin/platform_links.rb | 52 ++++++++ app/admin/releases.rb | 62 +++++++++ app/admin/softwares.rb | 150 +++++++++++++++++++++ lib/warp_engine/engine.rb | 17 +++ 8 files changed, 755 insertions(+) create mode 100644 app/admin/downloads.rb create mode 100644 app/admin/external_links.rb create mode 100644 app/admin/files.rb create mode 100644 app/admin/images.rb create mode 100644 app/admin/platform_links.rb create mode 100644 app/admin/releases.rb create mode 100644 app/admin/softwares.rb diff --git a/app/admin/downloads.rb b/app/admin/downloads.rb new file mode 100644 index 0000000..9fcec6c --- /dev/null +++ b/app/admin/downloads.rb @@ -0,0 +1,106 @@ +ActiveAdmin.register WarpEngine::Download, as: "Download" do + actions :index, :show + + menu priority: 7, label: "📊 Download Stats" + + scope :all, default: true + scope("Today") { |scope| scope.where("downloads.created_at >= ?", Date.current.beginning_of_day) } + scope("This week") { |scope| scope.where("downloads.created_at >= ?", 1.week.ago) } + scope("This month") { |scope| scope.where("downloads.created_at >= ?", 1.month.ago) } + scope("With release") { |scope| scope.where.not(release_id: nil) } + scope("Without release") { |scope| scope.where(release_id: nil) } + + config.sort_order = "created_at_desc" + + sidebar "Summary", only: :index do + dl style: "display:grid;grid-template-columns:1fr auto;gap:6px 12px;margin:0;" do + dt { strong "Total" } + dd { b WarpEngine::Download.count.to_s } + + dt { strong "Today" } + dd { b WarpEngine::Download.where("created_at >= ?", Date.current.beginning_of_day).count.to_s } + + dt { strong "This week" } + dd { b WarpEngine::Download.where("created_at >= ?", 1.week.ago).count.to_s } + + dt { strong "This month" } + dd { b WarpEngine::Download.where("created_at >= ?", 1.month.ago).count.to_s } + end + end + + sidebar "Top 10 Files", only: :index do + table_for WarpEngine::Download.group(:file_path) + .select("file_path, COUNT(*) as dl_count, MAX(created_at) as last_dl") + .order("dl_count DESC") + .limit(10) do + column("File") { |row| span row.file_path.split("/").last, style: "font-family:monospace;font-size:11px;" } + column("DL") { |row| row.dl_count } + end + end + + sidebar "Top 5 Software", only: :index do + rows = WarpEngine::Download.joins(release: :software) + .group("softwares.title") + .order("count_all DESC") + .limit(5) + .count + if rows.any? + table_for rows do + column("Software") { |title, _| title } + column("DL") { |_, count| count } + end + else + para "No data", style: "color:#999;font-style:italic;" + end + end + + index do + id_column + column(:file_path) { |d| span d.file_path.truncate(50), style: "font-family:monospace;font-size:12px;" } + column("Software") { |d| d.release&.software ? link_to(d.release.software.title, admin_software_path(d.release.software)) : "–" } + column("Release") { |d| d.release&.version || "–" } + column :ip_address + column("User-Agent") { |d| d.user_agent&.truncate(40) } + column("Referer") { |d| d.referer&.truncate(40) } + column :created_at + end + + filter :file_path_cont, label: "File path" + filter :release_software_title_cont, label: "Software" + filter :ip_address_cont, label: "IP address" + filter :user_agent_cont, label: "User-Agent" + filter :referer_cont, label: "Referer" + filter :created_at + + show do + attributes_table do + row :id + row(:file_path) { |d| span d.file_path, style: "font-family:monospace;" } + row("Software") do |d| + if d.release&.software + link_to d.release.software.title, admin_software_path(d.release.software) + else + "–" + end + end + row("Release") do |d| + if d.release + link_to d.release.version, admin_software_release_path(d.release.software, d.release) + else + "–" + end + end + row :ip_address + row :user_agent + row :referer + row :created_at + row :updated_at + end + end + + controller do + def scoped_collection + super.includes(release: :software) + end + end +end diff --git a/app/admin/external_links.rb b/app/admin/external_links.rb new file mode 100644 index 0000000..f535ff0 --- /dev/null +++ b/app/admin/external_links.rb @@ -0,0 +1,34 @@ +ActiveAdmin.register WarpEngine::ExternalLink, as: "External Link" do + permit_params :software_id, :label, :url + + menu false + + belongs_to :software + + index do + selectable_column + id_column + column :label + column(:url) { |el| link_to el.url, el.url, target: "_blank" } + column :created_at + actions + end + + form do |f| + f.inputs do + f.input :label + f.input :url + end + f.actions + end + + show do + attributes_table do + row :id + row :label + row(:url) { |el| link_to el.url, el.url, target: "_blank" } + row :created_at + row :updated_at + end + end +end diff --git a/app/admin/files.rb b/app/admin/files.rb new file mode 100644 index 0000000..e81ef94 --- /dev/null +++ b/app/admin/files.rb @@ -0,0 +1,255 @@ +ActiveAdmin.register_page "Files" do + menu priority: 6, label: "📁 Files" + + content do + service = WarpEngine::FileManagerService.new + current_dir = params[:dir].to_s.presence || "" + picker_mode = params[:picker].present? + picker_field = params[:field].to_s + + begin + entries = service.list(current_dir) + rescue ArgumentError + entries = [] + end + + # Picker mode: hide admin header/menu/footer + if picker_mode + text_node "".html_safe + end + + div class: "file-manager" do + h2 do + text_node "File Manager" + if picker_mode + span " — select a file or folder", style: "font-size:14px; color:#888; font-weight:normal;" + end + end + + # Breadcrumbs + div class: "fm-breadcrumbs" do + parts = current_dir.split("/").reject(&:blank?) + picker_params = picker_mode ? { picker: 1, field: picker_field } : {} + + a "softwares/", href: admin_files_path(picker_params) + accumulated = "" + parts.each do |part| + accumulated = [ accumulated, part ].reject(&:blank?).join("/") + a "#{part}/", href: admin_files_path(picker_params.merge(dir: accumulated)) + end + end + + # Action bar (also drop zone on standalone page) + div class: "fm-actions fm-dropzone", id: "fm-page-dropzone" do + form action: admin_files_upload_path, method: "post", enctype: "multipart/form-data", class: "fm-inline-form" do |_f| + input type: "hidden", name: "authenticity_token", value: form_authenticity_token + input type: "hidden", name: "dir", value: current_dir + input type: "file", name: "file", required: true + input type: "submit", value: "Upload", class: "fm-btn" + end + + form action: admin_files_mkdir_path, method: "post", class: "fm-inline-form" do |_f| + input type: "hidden", name: "authenticity_token", value: form_authenticity_token + input type: "hidden", name: "dir", value: current_dir + input type: "text", name: "name", placeholder: "New folder name", required: true, class: "fm-text-input" + input type: "submit", value: "Create Folder", class: "fm-btn" + end + end + + # File listing + table class: "fm-table" do + # Pre-fetch download counts for files in current directory + file_entries = entries.select { |e| e[:type] != :directory } + dl_counts = if file_entries.any? + WarpEngine::Download.where(file_path: file_entries.map { |e| e[:path] }) + .group(:file_path) + .count + else + {} + end + + # File icon helper + file_icon = ->(name) do + ext = File.extname(name).downcase + case ext + when ".png", ".jpg", ".jpeg", ".gif", ".bmp", ".webp", ".svg" then "🖼️" + when ".mp3", ".ogg", ".wav", ".flac", ".aac" then "🔊" + when ".mp4", ".avi", ".mkv", ".webm", ".mov" then "🎬" + when ".zip", ".gz", ".tar", ".rar", ".7z", ".bz2" then "📦" + when ".pdf" then "📕" + when ".doc", ".docx", ".odt", ".txt", ".md", ".rtf" then "📄" + when ".xls", ".xlsx", ".csv", ".ods" then "📊" + when ".html", ".htm", ".css", ".js", ".ts", ".json", ".xml" then "📝" + when ".rb", ".py", ".lua", ".c", ".cpp", ".h", ".rs", ".go" then "💻" + when ".tic", ".rom", ".bin", ".prg", ".crt", ".d64", ".t64" then "🎮" + when ".love" then "🎮" + when ".exe", ".dmg", ".appimage", ".msi" then "⚙️" + when ".wasm" then "⚙️" + else "📄" + end + end + + thead do + tr do + th "Name" + th "Size", style: "width:100px" + th "DL", style: "width:50px" + th "Modified", style: "width:160px" + th "", style: "width:#{picker_mode ? '160' : '120'}px" + end + end + + tbody do + # Parent directory link + if current_dir.present? + parent = File.dirname(current_dir) + parent = "" if parent == "." + picker_params = picker_mode ? { picker: 1, field: picker_field } : {} + + tr do + td do + span "📁 ", style: "font-size:15px;" + a "..", href: admin_files_path(picker_params.merge(dir: parent)) + end + td "" + td "" + td "" + td "" + end + end + + entries.each do |entry| + tr do + td do + picker_params = picker_mode ? { picker: 1, field: picker_field } : {} + if entry[:type] == :directory + span "📁 ", style: "font-size:15px;" + a entry[:name], href: admin_files_path(picker_params.merge(dir: entry[:path])) + else + span "#{file_icon.call(entry[:name])} ", style: "font-size:15px;" + span entry[:name] + end + end + td do + text_node(entry[:size] ? number_to_human_size(entry[:size]) : "") + end + td do + if entry[:type] != :directory + count = dl_counts[entry[:path]] || 0 + span count.to_s, style: count > 0 ? "font-weight:bold;" : "color:#999;" + end + end + td do + text_node(entry[:mtime]&.strftime("%Y-%m-%d %H:%M") || "") + end + td class: "fm-entry-actions" do + entry_id = entry[:path].parameterize + + # Download (files only) + if entry[:type] != :directory + a "⬇️", href: "/file/#{entry[:path]}", class: "fm-icon-btn", title: "Download", download: entry[:name] + end + + # Rename + a "✏️", href: "#", class: "fm-icon-btn", title: "Rename", + onclick: "var n=prompt('New name:','#{j entry[:name]}');if(n){var f=document.getElementById('rename-#{entry_id}');f.querySelector('[name=new_name]').value=n;f.submit();}return false;" + + # Delete + a "🗑️", href: "#", class: "fm-icon-btn fm-icon-danger", title: "Delete", + onclick: "if(confirm('Delete \\'#{j entry[:name]}\\'?')){document.getElementById('delete-#{entry_id}').submit();}return false;" + + # Stats (files only) + if entry[:type] != :directory + a "📊", href: admin_downloads_path(q: { file_path_cont: entry[:path] }), class: "fm-icon-btn", title: "Stats" + end + + # Picker mode: Select button + if picker_mode + abs_path = File.join(WarpEngine.config.file_container_path, entry[:path]) + a "Select", href: "#", class: "fm-btn fm-btn-select", + onclick: "var inp=window.parent.document.getElementById('#{j picker_field}');if(inp){inp.value='#{j abs_path}';}var ov=window.parent.document.querySelector('.fm-modal-overlay');if(ov){ov.remove();window.parent.document.body.style.overflow='';}return false;" + end + + # Hidden rename form + form action: admin_files_rename_path, method: "post", id: "rename-#{entry_id}", style: "display:none" do + input type: "hidden", name: "authenticity_token", value: form_authenticity_token + input type: "hidden", name: "path", value: entry[:path] + input type: "hidden", name: "new_name", value: "" + input type: "hidden", name: "dir", value: current_dir + end + + # Hidden delete form + form action: admin_files_delete_path, method: "post", id: "delete-#{entry_id}", style: "display:none" do + input type: "hidden", name: "authenticity_token", value: form_authenticity_token + input type: "hidden", name: "_method", value: "delete" + input type: "hidden", name: "path", value: entry[:path] + input type: "hidden", name: "dir", value: current_dir + end + end + end + end + end + end + + if entries.empty? && current_dir.blank? + para "No files found in /softwares", style: "color:#999; text-align:center; padding:40px 0;" + end + end + end + + # JSON API for AJAX file picker + page_action :list, method: :get do + service = WarpEngine::FileManagerService.new + dir = params[:dir].to_s.presence || "" + begin + entries = service.list(dir) + render json: { entries: entries, dir: dir } + rescue ArgumentError => e + render json: { entries: [], dir: dir, error: e.message } + end + end + + page_action :ajax_upload, method: :post do + service = WarpEngine::FileManagerService.new + path = service.upload(params[:dir].to_s, params[:file]) + render json: { ok: true, path: path } + rescue => e + render json: { ok: false, error: e.message }, status: 422 + end + + page_action :upload, method: :post do + service = WarpEngine::FileManagerService.new + service.upload(params[:dir].to_s, params[:file]) + redirect_to admin_files_path(dir: params[:dir], picker: params[:picker], field: params[:field]), notice: "File uploaded." + rescue => e + redirect_to admin_files_path(dir: params[:dir], picker: params[:picker], field: params[:field]), alert: "Upload failed: #{e.message}" + end + + page_action :mkdir, method: :post do + service = WarpEngine::FileManagerService.new + service.mkdir(params[:dir].to_s, params[:name].to_s) + redirect_to admin_files_path(dir: params[:dir], picker: params[:picker], field: params[:field]), notice: "Folder created." + rescue => e + redirect_to admin_files_path(dir: params[:dir], picker: params[:picker], field: params[:field]), alert: "Failed: #{e.message}" + end + + page_action :rename, method: :post do + service = WarpEngine::FileManagerService.new + service.rename(params[:path].to_s, params[:new_name].to_s) + redirect_to admin_files_path(dir: params[:dir], picker: params[:picker], field: params[:field]), notice: "Renamed." + rescue => e + redirect_to admin_files_path(dir: params[:dir], picker: params[:picker], field: params[:field]), alert: "Rename failed: #{e.message}" + end + + page_action :delete, method: :delete do + service = WarpEngine::FileManagerService.new + dir = params[:dir].to_s.presence || begin + d = File.dirname(params[:path].to_s) + d == "." ? "" : d + end + service.delete(params[:path].to_s) + redirect_to admin_files_path(dir: dir, picker: params[:picker], field: params[:field]), notice: "Deleted." + rescue => e + redirect_to admin_files_path(dir: params[:dir], picker: params[:picker], field: params[:field]), alert: "Delete failed: #{e.message}" + end +end diff --git a/app/admin/images.rb b/app/admin/images.rb new file mode 100644 index 0000000..3013920 --- /dev/null +++ b/app/admin/images.rb @@ -0,0 +1,79 @@ +ActiveAdmin.register WarpEngine::Image, as: "Image" do + permit_params :file_upload + + menu priority: 5, label: "🖼️ Images" + + # SoftwareImage a natív használó; a hoston regisztrált image_owners + # (WarpEngine.config) további használókat adhat hozzá (pl. TTG Member). + used_ids = -> { + WarpEngine::SoftwareImage.unscope(:order).distinct.pluck(:image_id) + + WarpEngine.config.image_owners.flat_map { |o| o[:image_ids].call } + } + + scope :all, default: true + scope("In use") { |scope| scope.where(id: used_ids.call) } + scope("Orphan") { |scope| scope.where.not(id: used_ids.call) } + + batch_action :delete_orphans, confirm: "Delete all selected orphan images and their files?" do |ids| + orphan_ids = ids.map(&:to_i) - used_ids.call + WarpEngine::Image.where(id: orphan_ids).find_each do |img| + File.delete(img.file_path) if File.exist?(img.file_path) + img.destroy + end + redirect_to admin_images_path, notice: "Deleted #{orphan_ids.size} orphan image(s)." + end + + index do + selectable_column + id_column + column :original_filename + column :content_type + column(:preview) do |img| + if File.exist?(img.file_path) + image_tag("/api/image/#{img.id}", style: "max-height:60px;max-width:120px;object-fit:contain;") + end + end + column(:usage) do |img| + uses = [] + uses << "#{img.software_images.size} software(s)" if img.software_images.any? + uses.concat(WarpEngine.config.image_owners.filter_map { |o| o[:usage_label].call(img) }) + uses.any? ? uses.join(", ") : status_tag("orphan", class: "warning") + end + column :created_at + actions + end + + filter :original_filename + filter :content_type + + show do + attributes_table do + row :id + row :original_filename + row :filename + row :content_type + row(:preview) do |img| + if File.exist?(img.file_path) + image_tag("/api/image/#{img.id}", style: "max-height:300px;max-width:100%;object-fit:contain;") + else + "File not found on disk" + end + end + row :created_at + row :updated_at + end + end + + form html: { multipart: true } do |f| + f.inputs do + f.input :file_upload, as: :file, label: "Image File" + end + f.actions + end + + controller do + def scoped_collection + super.includes(:software_images) + end + end +end diff --git a/app/admin/platform_links.rb b/app/admin/platform_links.rb new file mode 100644 index 0000000..b644699 --- /dev/null +++ b/app/admin/platform_links.rb @@ -0,0 +1,52 @@ +ActiveAdmin.register WarpEngine::PlatformLink, as: "Platform Link" do + permit_params :name, :url, :platform, :position + + menu priority: 5, label: "🔗 Platform Links" + + config.sort_order = "platform_asc" + + scope :all, default: true + scope("TIC-80") { |s| s.where(platform: "tic80") } + scope("Ebitengine") { |s| s.where(platform: "ebitengine") } + scope("LOVE") { |s| s.where(platform: "love") } + scope("C64") { |s| s.where(platform: "c64") } + scope("Godot") { |s| s.where(platform: "godot") } + scope("Bevy") { |s| s.where(platform: "bevy") } + scope("Phaser") { |s| s.where(platform: "phaser") } + + index do + selectable_column + id_column + column :platform + column :name + column(:url) { |pl| link_to pl.url, pl.url, target: "_blank" } + column :position + column :created_at + actions + end + + filter :platform, as: :select, collection: %w[tic80 ebitengine love c64 godot bevy phaser] + filter :name + + form do |f| + f.inputs do + f.input :platform, as: :select, collection: %w[tic80 ebitengine love c64 godot bevy phaser] + f.input :name + f.input :url + f.input :position, as: :number, input_html: { min: 0 } + end + f.actions + end + + show do + attributes_table do + row :id + row :platform + row :name + row(:url) { |pl| link_to pl.url, pl.url, target: "_blank" } + row :position + row :created_at + row :updated_at + end + end +end diff --git a/app/admin/releases.rb b/app/admin/releases.rb new file mode 100644 index 0000000..eb1ccbe --- /dev/null +++ b/app/admin/releases.rb @@ -0,0 +1,62 @@ +ActiveAdmin.register WarpEngine::Release, as: "Release" do + actions :index, :show + + menu false + + belongs_to :software, optional: true + + index do + selectable_column + id_column + column(:software) { |r| link_to r.software.title, admin_software_path(r.software) } + column :version + column(:assets) { |r| r.release_assets.map(&:kind).sort.join(", ") } + column :created_at + actions only: [ :show ] + end + + filter :version + + show do + attributes_table do + row :id + row(:software) { |r| link_to r.software.title, admin_software_path(r.software) } + row :version + row :created_at + row :updated_at + end + + panel "Assets" do + if resource.release_assets.any? + table_for resource.release_assets.order(:kind) do + column :kind + column(:path) { |a| span a.path, style: "font-family:monospace;font-size:12px;" } + end + else + para "No assets for this release.", style: "color:#999;font-style:italic;" + end + end + + panel "Downloads by File" do + file_stats = WarpEngine::Download.where(release_id: resource.id) + .group(:file_path) + .select("file_path, COUNT(*) as dl_count, MAX(created_at) as last_dl") + .order("dl_count DESC") + + if file_stats.any? + div style: "margin-bottom:8px;" do + strong "Total downloads: " + b file_stats.sum(&:dl_count).to_s + end + + table_for file_stats do + column("File") { |row| span row.file_path, style: "font-family:monospace;font-size:12px;" } + column("Downloads") { |row| row.dl_count } + column("Last download") { |row| row.last_dl&.strftime("%Y-%m-%d %H:%M") } + end + else + para "No downloads for this release.", style: "color:#999;font-style:italic;" + end + end + end +end diff --git a/app/admin/softwares.rb b/app/admin/softwares.rb new file mode 100644 index 0000000..f66b677 --- /dev/null +++ b/app/admin/softwares.rb @@ -0,0 +1,150 @@ +ActiveAdmin.register WarpEngine::Software, as: "Software" do + permit_params :name, :title, :author, :desc, :story, + :license, :platform, :status, :highlighted, + software_images_attributes: [ :id, :image_id, :is_default, :position, :file_upload, :_destroy ], + external_links_attributes: [ :id, :label, :url, :_destroy ], + releases_attributes: [ :id, :version, :_destroy, + { release_assets_attributes: [ :id, :kind, :path, :_destroy ] } ] + + menu priority: 2, label: "🎮 Softwares" + + actions :all, except: [ :edit ] + + config.sort_order = "title_asc" + + index do + selectable_column + id_column + column :name do |sw| + link_to sw.name, admin_software_path(sw) + end + column :title + column :platform + column :status do |sw| + status_tag sw.status + end + column :highlighted + column(:releases) { |sw| sw.releases.size } + column(:image) do |sw| + si = sw.software_images.detect(&:is_default?) || sw.software_images.first + if si&.image && File.exist?(si.image.file_path) + image_tag "/api/image/#{si.image_id}", style: "max-height:40px;max-width:80px;object-fit:contain;" + end + end + column :created_at + actions + end + + filter :name + filter :title + filter :author + filter :platform, as: :select, collection: %w[tic80 ebitengine love c64 godot bevy phaser] + filter :status, as: :select, collection: %w[development demo released archived] + filter :highlighted + + sidebar "Quick Links", only: :show, priority: 0 do + site_url = ENV.fetch("SITE_URL", "https://teletypegames.org") + catalog_url = "#{site_url}/catalog/#{resource.name}" + + div style: "margin-bottom:8px;" do + a href: catalog_url, target: "_blank", style: "display:inline-flex;align-items:center;gap:6px;font-weight:bold;color:#5850ec;" do + span "🌐", style: "font-size:16px;" + text_node "View on site" + end + end + end + + sidebar "Download Statistics", only: :show do + sw = resource + total = WarpEngine::Download.joins(:release).where(releases: { software_id: sw.id }).count + + div style: "margin-bottom:12px;" do + strong "Total downloads: " + span total.to_s, style: "font-size:18px;font-weight:bold;" + end + + sw.releases.each do |rel| + file_stats = WarpEngine::Download.where(release_id: rel.id) + .group(:file_path) + .select("file_path, COUNT(*) as dl_count, MAX(created_at) as last_dl") + .order("dl_count DESC") + rel_total = file_stats.sum(&:dl_count) + + div style: "margin-bottom:16px;" do + h4 do + link_to "v#{rel.version}", admin_software_release_path(sw, rel) + span " (#{rel_total})", style: "color:#666;" + end + if file_stats.any? + table_for file_stats do + column("File") { |row| span row.file_path.split("/").last, style: "font-family:monospace;font-size:11px;" } + column("DL") { |row| row.dl_count } + column("Last") { |row| row.last_dl&.strftime("%m-%d") } + end + else + para "No downloads", style: "color:#999;font-style:italic;" + end + end + end + end + + show title: proc { |sw| sw.title } do + active_admin_form_for [ :admin, resource ], url: admin_software_path(resource), html: { method: :put, multipart: true } do |f| + f.inputs "Details" do + f.input :name + f.input :title + f.input :author + f.input :platform, as: :select, collection: %w[tic80 ebitengine love c64 godot bevy phaser] + f.input :status, as: :select, collection: %w[development demo released archived] + f.input :highlighted + f.input :license + f.input :desc, as: :text, input_html: { rows: 4 } + f.input :story, as: :text, input_html: { rows: 8 } + end + + f.inputs "Images" do + f.has_many :software_images, allow_destroy: true, new_record: true do |si| + img_hint = if si.object.image_id.present? + si.template.image_tag("/api/image/#{si.object.image_id}", style: "max-height:80px;max-width:160px;object-fit:contain;margin-top:6px;").html_safe + end + si.input :image_id, as: :select, label: "Image", + collection: WarpEngine::Image.order(:original_filename).map { |img| [ img.original_filename, img.id ] }, + include_blank: "— select —", + hint: img_hint + si.input :file_upload, as: :file, label: "Upload new image" + si.input :is_default, as: :boolean + si.input :position, as: :number, input_html: { min: 0 } + end + end + + f.inputs "External Links" do + f.has_many :external_links, allow_destroy: true, new_record: true do |el| + el.input :label + el.input :url + end + end + + f.inputs "Releases" do + f.has_many :releases, allow_destroy: true, new_record: true do |r| + r.input :version + r.has_many :release_assets, allow_destroy: true, new_record: true do |ra| + ra.input :kind, as: :select, collection: WarpEngine::ReleaseAsset::KINDS, include_blank: false + ra.input :path + end + end + end + + f.actions + end + end + + controller do + def scoped_collection + super.includes({ releases: :release_assets }, software_images: :image) + end + + def find_resource + scoped_collection.find(params[:id]) + end + end +end diff --git a/lib/warp_engine/engine.rb b/lib/warp_engine/engine.rb index 9e473cb..b163d6b 100644 --- a/lib/warp_engine/engine.rb +++ b/lib/warp_engine/engine.rb @@ -1,5 +1,22 @@ module WarpEngine class Engine < ::Rails::Engine isolate_namespace WarpEngine + + # Az app/admin fájlok ActiveAdmin DSL-t tartalmaznak, nem definiálnak a + # fájlnévnek megfelelő konstansokat — a Zeitwerk (eager load) nem nyúlhat hozzájuk. + initializer "warp_engine.ignore_admin_dir", before: :setup_main_autoloader do + Rails.autoloaders.main.ignore(Engine.root.join("app/admin")) + end + + # Az admin erőforrásokat a HOST ActiveAdmin példánya tölti be; az engine csak + # regisztrálja a saját app/admin könyvtárát. Auth/téma/route-ok a host dolga. + initializer "warp_engine.active_admin" do |app| + if defined?(ActiveAdmin) + admin_dir = Engine.root.join("app/admin").to_s + ActiveAdmin.application.load_paths << admin_dir + # dev módban az engine admin-fájljainak szerkesztése is triggerel reloadot + app.config.watchable_dirs[admin_dir] = [ :rb ] + end + end end end