web payable and asset view

This commit is contained in:
2026-08-02 22:54:37 +02:00
parent 73cc567e1f
commit 242c808add
8 changed files with 86 additions and 79 deletions
+1 -2
View File
@@ -3,7 +3,7 @@ ActiveAdmin.register Software do
: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, :web_playable, :_destroy,
releases_attributes: [ :id, :version, :_destroy,
{ release_assets_attributes: [ :id, :kind, :path, :_destroy ] } ]
menu priority: 2, label: "🎮 Softwares"
@@ -127,7 +127,6 @@ ActiveAdmin.register Software do
f.inputs "Releases" do
f.has_many :releases, allow_destroy: true, new_record: true do |r|
r.input :version
r.input :web_playable
r.has_many :release_assets, allow_destroy: true, new_record: true do |ra|
ra.input :kind, as: :select, collection: ReleaseAsset::KINDS, include_blank: false
ra.input :path
+2 -2
View File
@@ -18,13 +18,13 @@ class Release < ApplicationRecord
return unless software&.platform == "c64"
has_html = release_assets.reject(&:marked_for_destruction?).any? { |a| a.kind == "html" }
if has_html || web_playable?
if has_html
errors.add(:base, "C64 releases cannot be web playable (download only)")
end
end
def self.ransackable_attributes(auth_object = nil)
%w[created_at deleted_at id software_id updated_at version web_playable]
%w[created_at deleted_at id software_id updated_at version]
end
def self.ransackable_associations(auth_object = nil)
@@ -2,9 +2,10 @@ module SoftwareResponseBuilder
private
def build_response(software, releases)
sorted = releases.sort_by { |r| r.created_at || Time.at(0) }.reverse
latest = sorted.reject { |r| r.version.to_s.start_with?("dev-") }.first
web_playable = sorted.find { |r| r.release_assets.any? { |a| a.kind == "html" } }
sorted = releases.sort_by { |r| r.created_at || Time.at(0) }.reverse
latest = sorted.reject { |r| r.version.to_s.start_with?("dev-") }.first
# web-playable, ha az utolsó (stabil) release-nek van webes assetje
web_playable = latest if latest&.release_assets&.any? { |a| a.kind == "html" }
total_downloads = releases.sum { |r| r.association(:downloads).loaded? ? r.downloads.size : 0 }
SoftwareDetailSerializer.render_as_hash(software,
@@ -0,0 +1,5 @@
class RemoveWebPlayableFromReleases < ActiveRecord::Migration[8.1]
def change
remove_column :releases, :web_playable, :boolean, default: false
end
end
+1 -2
View File
@@ -10,7 +10,7 @@
#
# It's strongly recommended that you check this file into your version control system.
ActiveRecord::Schema[8.1].define(version: 2026_08_02_000002) do
ActiveRecord::Schema[8.1].define(version: 2026_08_02_000003) do
create_table "admin_users", charset: "utf8mb4", collation: "utf8mb4_0900_ai_ci", force: :cascade do |t|
t.datetime "created_at", null: false
t.string "email", default: "", null: false
@@ -188,7 +188,6 @@ ActiveRecord::Schema[8.1].define(version: 2026_08_02_000002) do
t.bigint "software_id", unsigned: true
t.datetime "updated_at", precision: 3
t.string "version", limit: 64
t.boolean "web_playable", default: false
t.index ["deleted_at"], name: "idx_releases_deleted_at"
t.index ["software_id"], name: "idx_releases_software_id"
end
+7 -2
View File
@@ -146,8 +146,13 @@ export default {
play: '▶ Play',
downloads: 'downloads',
links: 'Links',
binariesTitle: 'Download for desktop',
binary: {
assetsTitle: 'Downloads',
assetDownload: 'Download',
assetOpen: 'Open',
assetKind: {
cartridge: 'Game file',
source: 'Source code',
docs: 'Documentation',
win_x86: 'Windows (32-bit)',
win_x64: 'Windows (64-bit)',
linux_x86: 'Linux (32-bit)',
+7 -2
View File
@@ -146,8 +146,13 @@ export default {
play: '▶ Játék',
downloads: 'letöltés',
links: 'Linkek',
binariesTitle: 'Letöltés asztali gépre',
binary: {
assetsTitle: 'Letöltések',
assetDownload: 'Letöltés',
assetOpen: 'Megnyitás',
assetKind: {
cartridge: 'Játékfájl',
source: 'Forráskód',
docs: 'Dokumentáció',
win_x86: 'Windows (32 bit)',
win_x64: 'Windows (64 bit)',
linux_x86: 'Linux (32 bit)',
@@ -92,27 +92,22 @@
<p class="text-indigo-100 text-sm">{{ t('catalogShow.released') }} {{ formatDateTime(latestStable.UpdatedAt) }}</p>
</div>
<div class="grid grid-cols-2 sm:grid-cols-4 md:grid-cols-2 xl:grid-cols-4 gap-2 w-full md:w-auto">
<a v-if="latestStable.htmlFolderPath" :href="latestStable.htmlFolderPath" target="_blank" class="flex items-center justify-center py-3 px-4 bg-white text-indigo-900 font-bold rounded-xl transition-all hover:bg-indigo-50 shadow-lg shadow-black/10 text-sm whitespace-nowrap">{{ t('catalogShow.playNow') }}</a>
<a v-if="latestStable.cartridgePath" :href="downloadUrl(latestStable.cartridgePath)" class="flex items-center justify-center py-3 px-4 bg-indigo-500/30 border border-indigo-400/30 text-white font-bold rounded-xl transition-all hover:bg-indigo-500/40 text-sm whitespace-nowrap">{{ t('catalogShow.download') }}</a>
<a v-if="latestStable.sourcePath" :href="downloadUrl(latestStable.sourcePath)" class="flex items-center justify-center py-3 px-4 bg-indigo-500/30 border border-indigo-400/30 text-white font-bold rounded-xl transition-all hover:bg-indigo-500/40 text-sm whitespace-nowrap">{{ t('catalogShow.source') }}</a>
<a v-if="latestStable.docsFolderPath" :href="latestStable.docsFolderPath" target="_blank" class="flex items-center justify-center py-3 px-4 bg-indigo-500/30 border border-indigo-400/30 text-white font-bold rounded-xl transition-all hover:bg-indigo-500/40 text-sm whitespace-nowrap">{{ t('catalogShow.docs') }}</a>
</div>
<a v-if="latestStable.htmlFolderPath" :href="latestStable.htmlFolderPath" target="_blank" class="flex items-center justify-center py-3 px-6 bg-white text-indigo-900 font-bold rounded-xl transition-all hover:bg-indigo-50 shadow-lg shadow-black/10 text-sm whitespace-nowrap">{{ t('catalogShow.playNow') }}</a>
</div>
<div v-if="binaryAssets(latestStable).length" class="mt-6 pt-6 border-t border-white/15">
<div class="text-[10px] font-black uppercase tracking-widest text-indigo-200 mb-3">{{ t('catalogShow.binariesTitle') }}</div>
<div class="flex flex-wrap gap-2">
<a
v-for="asset in binaryAssets(latestStable)"
:key="asset.kind"
:href="downloadUrl(asset.path)"
:class="['flex items-center justify-center py-2 px-4 font-bold rounded-xl transition-all text-sm whitespace-nowrap',
asset.kind === primaryBinaryKind(latestStable)
? 'bg-white text-indigo-900 hover:bg-indigo-50 shadow-lg shadow-black/10'
: 'bg-indigo-500/30 border border-indigo-400/30 text-white hover:bg-indigo-500/40']"
>💾 {{ t('catalogShow.binary.' + asset.kind) }}</a>
</div>
<div v-if="tableAssets(latestStable).length" class="mt-6 pt-6 border-t border-white/15">
<div class="text-[10px] font-black uppercase tracking-widest text-indigo-200 mb-3">{{ t('catalogShow.assetsTitle') }}</div>
<table class="text-sm">
<tbody>
<tr v-for="asset in tableAssets(latestStable)" :key="asset.kind" class="border-b border-white/10 last:border-0">
<td class="py-1.5 pr-10 text-indigo-100">{{ t('catalogShow.assetKind.' + asset.kind) }}</td>
<td class="py-1.5">
<a v-if="asset.kind === 'docs'" :href="asset.path" target="_blank" class="text-white font-bold hover:underline">{{ t('catalogShow.assetOpen') }}</a>
<a v-else :href="downloadUrl(asset.path)" class="text-white font-bold hover:underline">{{ t('catalogShow.assetDownload') }}</a>
</td>
</tr>
</tbody>
</table>
</div>
</div>
@@ -137,13 +132,18 @@
<span class="font-bold text-gray-900 text-lg">{{ release.version }}</span>
<span v-if="release.downloadCount" class="text-xs text-gray-400 font-medium"> {{ release.downloadCount }}</span>
</div>
<div class="flex flex-wrap gap-4">
<a v-if="release.htmlFolderPath" :href="release.htmlFolderPath" target="_blank" class="text-purple-600 hover:text-purple-900 font-bold text-sm flex items-center gap-1">{{ t('catalogShow.play') }}</a>
<a v-if="release.cartridgePath" :href="downloadUrl(release.cartridgePath)" class="text-blue-600 hover:text-blue-900 font-bold text-sm flex items-center gap-1">{{ t('catalogShow.download') }}</a>
<a v-if="release.sourcePath" :href="downloadUrl(release.sourcePath)" class="text-green-600 hover:text-green-900 font-bold text-sm flex items-center gap-1">{{ t('catalogShow.source') }}</a>
<a v-if="release.docsFolderPath" :href="release.docsFolderPath" target="_blank" class="text-yellow-600 hover:text-yellow-900 font-bold text-sm flex items-center gap-1">{{ t('catalogShow.docs') }}</a>
<a v-for="asset in binaryAssets(release)" :key="asset.kind" :href="downloadUrl(asset.path)" class="text-indigo-600 hover:text-indigo-900 font-bold text-sm flex items-center gap-1">💾 {{ t('catalogShow.binary.' + asset.kind) }}</a>
</div>
<a v-if="release.htmlFolderPath" :href="release.htmlFolderPath" target="_blank" class="text-purple-600 hover:text-purple-900 font-bold text-sm">{{ t('catalogShow.play') }}</a>
<table v-if="tableAssets(release).length" class="mt-2 text-sm">
<tbody>
<tr v-for="asset in tableAssets(release)" :key="asset.kind" class="border-b border-gray-100 last:border-0">
<td class="py-1 pr-8 text-gray-500">{{ t('catalogShow.assetKind.' + asset.kind) }}</td>
<td class="py-1">
<a v-if="asset.kind === 'docs'" :href="asset.path" target="_blank" class="text-blue-600 hover:text-blue-900 font-bold">{{ t('catalogShow.assetOpen') }}</a>
<a v-else :href="downloadUrl(asset.path)" class="text-blue-600 hover:text-blue-900 font-bold">{{ t('catalogShow.assetDownload') }}</a>
</td>
</tr>
</tbody>
</table>
</td>
<td class="px-8 py-4 text-gray-500 align-top pt-5">{{ formatDateTime(release.UpdatedAt) }}</td>
</tr>
@@ -155,11 +155,18 @@
<tr v-for="release in devReleases" :key="release.version" class="bg-yellow-50/10 hover:bg-yellow-50/30 transition-colors">
<td class="px-8 py-4">
<div class="font-bold text-yellow-800 text-lg mb-2">{{ release.version }}</div>
<div class="flex flex-wrap gap-4">
<a v-if="release.htmlFolderPath" :href="release.htmlFolderPath" target="_blank" class="text-purple-600 hover:text-purple-900 font-bold text-sm flex items-center gap-1">{{ t('catalogShow.play') }}</a>
<a v-if="release.cartridgePath" :href="downloadUrl(release.cartridgePath)" class="text-blue-600 hover:text-blue-900 font-bold text-sm flex items-center gap-1">{{ t('catalogShow.download') }}</a>
<a v-for="asset in binaryAssets(release)" :key="asset.kind" :href="downloadUrl(asset.path)" class="text-indigo-600 hover:text-indigo-900 font-bold text-sm flex items-center gap-1">💾 {{ t('catalogShow.binary.' + asset.kind) }}</a>
</div>
<a v-if="release.htmlFolderPath" :href="release.htmlFolderPath" target="_blank" class="text-purple-600 hover:text-purple-900 font-bold text-sm">{{ t('catalogShow.play') }}</a>
<table v-if="tableAssets(release).length" class="mt-2 text-sm">
<tbody>
<tr v-for="asset in tableAssets(release)" :key="asset.kind" class="border-b border-gray-100 last:border-0">
<td class="py-1 pr-8 text-gray-500">{{ t('catalogShow.assetKind.' + asset.kind) }}</td>
<td class="py-1">
<a v-if="asset.kind === 'docs'" :href="asset.path" target="_blank" class="text-blue-600 hover:text-blue-900 font-bold">{{ t('catalogShow.assetOpen') }}</a>
<a v-else :href="downloadUrl(asset.path)" class="text-blue-600 hover:text-blue-900 font-bold">{{ t('catalogShow.assetDownload') }}</a>
</td>
</tr>
</tbody>
</table>
</td>
<td class="px-8 py-4 text-gray-500 align-top pt-5">{{ formatDateTime(release.UpdatedAt) }}</td>
</tr>
@@ -181,13 +188,18 @@
</div>
<span v-if="release.version.startsWith('dev-')" class="px-2 py-0.5 bg-yellow-100 text-yellow-800 text-[10px] font-black uppercase rounded">Dev</span>
</div>
<div class="grid grid-cols-2 gap-2">
<a v-if="release.htmlFolderPath" :href="release.htmlFolderPath" target="_blank" class="flex items-center justify-center py-2 bg-purple-100 text-purple-700 font-bold rounded-lg text-sm">{{ t('catalogShow.play') }}</a>
<a v-if="release.cartridgePath" :href="downloadUrl(release.cartridgePath)" class="flex items-center justify-center py-2 bg-blue-100 text-blue-700 font-bold rounded-lg text-sm">{{ t('catalogShow.download') }}</a>
<a v-if="release.sourcePath" :href="downloadUrl(release.sourcePath)" class="flex items-center justify-center py-2 bg-green-100 text-green-700 font-bold rounded-lg text-sm">{{ t('catalogShow.source') }}</a>
<a v-if="release.docsFolderPath" :href="release.docsFolderPath" target="_blank" class="flex items-center justify-center py-2 bg-yellow-100 text-yellow-700 font-bold rounded-lg text-sm">{{ t('catalogShow.docs') }}</a>
<a v-for="asset in binaryAssets(release)" :key="asset.kind" :href="downloadUrl(asset.path)" class="flex items-center justify-center py-2 bg-indigo-100 text-indigo-700 font-bold rounded-lg text-sm">💾 {{ t('catalogShow.binary.' + asset.kind) }}</a>
</div>
<a v-if="release.htmlFolderPath" :href="release.htmlFolderPath" target="_blank" class="flex items-center justify-center py-2 bg-purple-100 text-purple-700 font-bold rounded-lg text-sm">{{ t('catalogShow.play') }}</a>
<table v-if="tableAssets(release).length" class="mt-3 w-full text-sm">
<tbody>
<tr v-for="asset in tableAssets(release)" :key="asset.kind" class="border-b border-gray-100 last:border-0">
<td class="py-1.5 pr-4 text-gray-500">{{ t('catalogShow.assetKind.' + asset.kind) }}</td>
<td class="py-1.5 text-right">
<a v-if="asset.kind === 'docs'" :href="asset.path" target="_blank" class="text-blue-600 font-bold">{{ t('catalogShow.assetOpen') }}</a>
<a v-else :href="downloadUrl(asset.path)" class="text-blue-600 font-bold">{{ t('catalogShow.assetDownload') }}</a>
</td>
</tr>
</tbody>
</table>
</div>
</div>
</div>
@@ -265,36 +277,17 @@ const latestStable = computed(() => stableReleases.value[0] ?? null)
const downloadUrl = (path: string) =>
`/api/download?path=${encodeURIComponent(path.replace('/file/', ''))}`
const BINARY_KINDS = ['win_x64', 'win_x86', 'linux_x64', 'linux_x86', 'mac_universal', 'mac_arm64', 'mac_x64']
// az asset-táblázat sorai és sorrendjük; a html nem letöltés, azt a Play gomb viszi
const TABLE_KIND_ORDER = [
'cartridge', 'source', 'docs',
'win_x64', 'win_x86', 'linux_x64', 'linux_x86',
'mac_universal', 'mac_arm64', 'mac_x64',
]
// a látogató OS-éhez illő targetek, preferencia-sorrendben
function osPreferredKinds(): string[] {
const ua = navigator.userAgent
if (/Windows/i.test(ua)) {
return /WOW64|Win64|x64|amd64/i.test(ua) ? ['win_x64', 'win_x86'] : ['win_x86', 'win_x64']
}
if (/Macintosh|Mac OS X/i.test(ua)) return ['mac_universal', 'mac_arm64', 'mac_x64']
if (/Linux/i.test(ua)) return ['linux_x64', 'linux_x86']
return []
}
const preferredKinds = osPreferredKinds()
const kindRank = (kind: string) => {
const preferred = preferredKinds.indexOf(kind)
return preferred >= 0 ? preferred : preferredKinds.length + BINARY_KINDS.indexOf(kind)
}
const binaryAssets = (release: Release) =>
const tableAssets = (release: Release) =>
(release.assets ?? [])
.filter(a => BINARY_KINDS.includes(a.kind))
.sort((a, b) => kindRank(a.kind) - kindRank(b.kind))
// a kiemelt gomb: a látogató OS-éhez legjobban illő elérhető bináris
const primaryBinaryKind = (release: Release) => {
const first = binaryAssets(release)[0]
return first && preferredKinds.includes(first.kind) ? first.kind : null
}
.filter(a => TABLE_KIND_ORDER.includes(a.kind))
.sort((a, b) => TABLE_KIND_ORDER.indexOf(a.kind) - TABLE_KIND_ORDER.indexOf(b.kind))
onMounted(async () => {
try {