A lot of things.

This commit is contained in:
2026-06-08 16:25:52 +02:00
parent 6f6d6b9b84
commit f529f74823
94 changed files with 9178 additions and 107 deletions

View File

@@ -17,6 +17,9 @@
@import './components/notifications.css';
@import './components/settings.css';
@import './components/queue.css';
@import './components/drafts.css';
@import './components/modcp.css';
@import './components/tools.css';
@import './components/easymde.css';

View File

@@ -182,3 +182,9 @@
.spin {
animation: spin 1s infinite linear;
}
.search-button {
background: none;
border: none;
cursor: pointer;
}

View File

@@ -0,0 +1,160 @@
.drafts-count {
font-size: 0.85rem;
color: var(--text2);
margin-bottom: 15px;
padding-bottom: 10px;
border-bottom: 1px solid var(--border);
}
.drafts-empty {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
gap: 15px;
padding: 80px 20px;
background-color: var(--bg2);
border: 1px dashed var(--border);
text-align: center;
color: var(--text2);
h3 {
font-size: 1.1rem;
color: var(--text);
margin: 0;
}
p {
font-size: 0.9rem;
margin: 0;
}
}
.drafts-list {
display: flex;
flex-direction: column;
gap: 15px;
}
.drafts-item {
display: flex;
gap: 20px;
background-color: var(--bg2);
border: 1px solid var(--border);
border-left: 3px solid var(--rhpz-orange);
padding: 20px;
transition: border-color 0.15s;
&:hover {
border-color: var(--rhpz-orange);
}
}
.drafts-cover {
width: 80px;
height: 80px;
flex-shrink: 0;
background-color: var(--bg);
border: 1px solid var(--border);
overflow: hidden;
display: flex;
align-items: center;
justify-content: center;
img {
width: 100%;
height: 100%;
object-fit: contain;
}
}
.drafts-cover-placeholder {
color: var(--border);
}
.drafts-info {
flex: 1;
min-width: 0;
display: flex;
flex-direction: column;
gap: 12px;
}
.drafts-top {
display: flex;
justify-content: space-between;
align-items: flex-start;
gap: 15px;
}
.drafts-title {
font-size: 1rem;
font-weight: 600;
color: var(--text);
margin-bottom: 6px;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.drafts-meta {
display: flex;
gap: 6px;
flex-wrap: wrap;
}
.drafts-dates {
display: flex;
flex-direction: column;
gap: 4px;
text-align: right;
font-size: 0.78rem;
color: var(--text2);
flex-shrink: 0;
span {
display: flex;
align-items: center;
justify-content: flex-end;
gap: 5px;
}
}
.drafts-progress {
display: flex;
align-items: center;
gap: 10px;
}
.drafts-progress-bar {
flex: 1;
height: 4px;
background-color: var(--bg4);
overflow: hidden;
}
.drafts-progress-fill {
height: 100%;
background-color: var(--rhpz-orange);
transition: width 0.3s ease;
.complete {
background-color: var(--success);
}
}
.drafts-progress-label {
font-size: 0.75rem;
color: var(--text2);
white-space: nowrap;
}
.drafts-actions {
display: flex;
flex-direction: column;
gap: 8px;
justify-content: center;
flex-shrink: 0;
.btn {
white-space: nowrap;
}
}

View File

@@ -0,0 +1,320 @@
.modcp-wrapper {
display: flex;
gap: 0;
align-items: flex-start;
min-height: calc(100vh - 60px);
}
.modcp-sidebar {
width: 220px;
flex-shrink: 0;
background-color: var(--bg2);
border: 1px solid var(--border);
position: sticky;
top: 0;
align-self: flex-start;
margin-right: 15px;
}
.modcp-sidebar-header {
display: flex;
align-items: center;
gap: 8px;
padding: 14px 16px;
font-weight: 600;
font-size: 0.88rem;
color: var(--text);
border-bottom: 1px solid var(--border);
background-color: var(--bg3);
text-transform: uppercase;
letter-spacing: 0.5px;
}
.modcp-nav { padding: 8px 0; }
.modcp-nav-group { margin-bottom: 4px; }
.modcp-nav-label {
display: block;
padding: 8px 16px 4px;
font-size: 0.7rem;
font-weight: 600;
text-transform: uppercase;
letter-spacing: 0.8px;
color: var(--text2);
}
.modcp-nav-item {
display: flex;
align-items: center;
gap: 9px;
padding: 8px 16px;
font-size: 0.88rem;
color: var(--text);
text-decoration: none;
border-left: 3px solid transparent;
transition: background-color 0.1s, border-color 0.1s;
&:hover {
background-color: var(--bg3);
text-decoration: none;
}
.active {
background-color: var(--bg3);
border-left-color: var(--rhpz-orange);
color: var(--text);
font-weight: 600;
}
}
.modcp-nav-badge {
margin-left: auto;
background-color: var(--rhpz-orange);
color: #111;
font-size: 0.65rem;
font-weight: 700;
min-width: 18px;
height: 18px;
border-radius: 9px;
display: flex;
align-items: center;
justify-content: center;
padding: 0 5px;
}
.modcp-content {
flex: 1;
min-width: 0;
background-color: var(--bg2);
border: 1px solid var(--border);
padding: 25px;
}
.modcp-page-title {
display: flex;
align-items: center;
gap: 10px;
font-size: 1.3rem;
font-weight: 600;
color: var(--text);
margin-bottom: 25px;
padding-bottom: 15px;
border-bottom: 1px solid var(--border);
}
.modcp-count {
margin-left: auto;
font-size: 0.85rem;
font-weight: normal;
color: var(--text2);
}
.modcp-section-title {
font-size: 0.78rem;
font-weight: 600;
text-transform: uppercase;
letter-spacing: 0.7px;
color: var(--text2);
margin-bottom: 12px;
}
.modcp-stats {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(150px, 1fr));
gap: 12px;
margin-bottom: 25px;
}
.modcp-stat-card {
display: flex;
align-items: center;
gap: 12px;
padding: 16px;
background-color: var(--bg3);
border: 1px solid var(--border);
border-left: 3px solid var(--border);
text-decoration: none;
transition: border-color 0.15s, background-color 0.15s;
color: var(--text);
&:hover {
background-color: var(--bg4);
text-decoration: none;
}
}
.modcp-stat-card--orange { border-left-color: var(--rhpz-orange); }
.modcp-stat-card--danger { border-left-color: var(--error); }
.modcp-stat-card--muted { cursor: default; }
.modcp-stat-icon { color: var(--text2); }
.modcp-stat-card--orange .modcp-stat-icon { color: var(--rhpz-orange); }
.modcp-stat-card--danger .modcp-stat-icon { color: var(--error); }
.modcp-stat-info { display: flex; flex-direction: column; }
.modcp-stat-value { font-size: 1.4rem; font-weight: 700; color: var(--text); line-height: 1; }
.modcp-stat-label { font-size: 0.75rem; color: var(--text2); margin-top: 3px; }
.modcp-quick-actions {
display: flex;
flex-wrap: wrap;
gap: 8px;
margin-bottom: 25px;
}
.modcp-quick-btn {
display: inline-flex;
align-items: center;
gap: 7px;
padding: 8px 14px;
background-color: var(--bg3);
border: 1px solid var(--border);
color: var(--text);
font-size: 0.85rem;
text-decoration: none;
transition: background-color 0.1s, border-color 0.1s;
&:hover {
background-color: var(--bg4);
border-color: var(--rhpz-orange);
text-decoration: none;
}
}
.modcp-list { display: flex; flex-direction: column; }
.modcp-list-item {
display: flex;
align-items: center;
gap: 15px;
padding: 12px 15px;
border-bottom: 1px solid var(--border);
transition: background-color 0.1s;
}
.modcp-list-item:last-child { border-bottom: none; }
.modcp-list-item:hover { background-color: var(--bg3); }
.modcp-list-item--deleted { opacity: 0.8; }
.modcp-list-item-cover {
width: 44px;
height: 44px;
flex-shrink: 0;
background-color: var(--bg);
border: 1px solid var(--border);
display: flex;
align-items: center;
justify-content: center;
overflow: hidden;
color: var(--border);
}
.modcp-list-item-cover img {
width: 100%;
height: 100%;
object-fit: contain;
}
.modcp-list-item-info {
flex: 1;
min-width: 0;
display: flex;
flex-direction: column;
gap: 4px;
}
.modcp-list-item-title {
font-size: 0.92rem;
font-weight: 600;
color: var(--text);
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.modcp-list-item-meta {
display: flex;
align-items: center;
gap: 6px;
font-size: 0.78rem;
color: var(--text2);
}
.modcp-list-item-actions {
display: flex;
gap: 6px;
flex-shrink: 0;
}
.modcp-list-item-edit {
display: flex;
gap: 6px;
flex: 1;
align-items: center;
}
.modcp-list-item-edit .form-input {
flex: 1;
padding: 5px 10px;
font-size: 0.88rem;
}
.modcp-list-see-all {
display: block;
text-align: center;
padding: 10px;
font-size: 0.85rem;
color: var(--rhpz-orange);
border-top: 1px solid var(--border);
text-decoration: none;
}
.modcp-add-form {
background-color: var(--bg3);
border: 1px solid var(--border);
padding: 15px;
margin-bottom: 20px;
}
.modcp-add-form-inner {
display: flex;
gap: 8px;
align-items: center;
}
.modcp-add-form-inner .form-input { flex: 1; }
.modcp-empty {
display: flex;
flex-direction: column;
align-items: center;
gap: 12px;
padding: 50px 20px;
color: var(--text2);
text-align: center;
}
.mod-alert {
display: flex;
align-items: center;
gap: 8px;
padding: 10px 15px;
margin-bottom: 20px;
font-size: 0.88rem;
border: 1px solid;
}
.mod-alert--success {
background-color: rgba(129, 199, 132, 0.08);
border-color: rgba(129, 199, 132, 0.3);
color: var(--success);
}
.modcp-list-item-edit--game {
flex-wrap: wrap;
gap: 6px;
}
.modcp-list-item-edit--game .form-input { min-width: 180px; flex: 2; }
.modcp-list-item-edit--game .form-select { flex: 1; min-width: 120px; }

View File

@@ -0,0 +1,83 @@
.patcher-container {
background-color: var(--bg2);
border: 1px solid var(--border);
padding: 25px;
margin-bottom: 20px;
}
.patcher-grid {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 20px;
}
@media (max-width: 768px) {
.patcher-grid {
grid-template-columns: 1fr;
}
}
.patcher-dropzone {
border: 2px dashed var(--border);
background-color: var(--bg3);
padding: 55px 20px;
text-align: center;
cursor: pointer;
transition: all 0.2s ease;
display: flex;
flex-direction: column;
align-items: center;
gap: 15px;
}
.patcher-dropzone:hover, .patcher-dropzone.dragover {
border-color: var(--rhpz-orange);
background-color: var(--bg4);
}
.patcher-dropzone.has-file {
border-color: var(--success);
background-color: rgba(129, 199, 132, 0.02);
}
.patcher-status-box {
margin-top: 20px;
padding: 15px;
border: 1px solid var(--border);
background-color: var(--bg3);
font-size: 0.95rem;
line-height: 1.4;
}
.btn:disabled {
opacity: 0.4;
cursor: not-allowed;
background-color: var(--bg3);
border-color: var(--border);
color: var(--text2);
}
.embed-patch-box {
border: 1px solid var(--border);
background-color: var(--bg3);
padding: 25px;
height: 85%;
display: flex;
flex-direction: column;
justify-content: center;
gap: 15px;
}
.embed-patch-box-icon {
display: flex;
align-items: center;
gap: 15px;
}
.embed-patch-box-icon-block {
width: 48px;
height: 48px;
background-color: var(--bg2);
border: 1px solid var(--border);
display: flex;
align-items: center;
justify-content: center;
}

View File

@@ -23,24 +23,6 @@
cursor: pointer;
}
.search-bar {
display: flex;
align-items: center;
background-color: var(--bg);
border: 1px solid var(--border);
border-radius: 2px;
padding: 5px 10px;
width: 300px;
input {
background: none;
border: none;
color: var(--text);
outline: none;
margin-left: 8px;
width: 100%;
}
}
.topbar-actions {
display: flex;
gap: 8px;
@@ -53,6 +35,24 @@
}
.search-bar {
display: flex;
align-items: center;
background-color: var(--bg);
border: 1px solid var(--border);
border-radius: 2px;
padding: 5px 10px;
width: 300px;
input {
background: none;
border: none;
color: var(--text);
outline: none;
margin-left: 8px;
width: 100%;
}
}
#content {
flex-grow: 1;
padding: 30px;

View File

@@ -14,8 +14,8 @@
gap: 30px;
.entry-cover {
width: 200px;
height: 280px;
width: 220px;
height: 220px;
background-color: var(--bg);
border: 1px solid var(--border);
display: flex;
@@ -29,7 +29,8 @@
img {
width: 100%;
height: 100%;
object-fit: cover;
object-fit: contain;
padding: 8px;
}
}

114
resources/js/RomPatcher.js Normal file
View File

@@ -0,0 +1,114 @@
export function RomPatcher( initialPatches = {} ) {
let patchesArray = [];
if (initialPatches) {
patchesArray = Array.isArray(initialPatches) ? initialPatches : [initialPatches];
}
patchesArray = patchesArray.filter(p => p && p.file);
return {
/**
* @type {string}
*/
romFileName: '',
/**
* @type {string}
*/
patchFileName: '',
/**
* @type {boolean}
*/
isRomDragOver: false,
/**
* @type {boolean}
*/
isPatchDragOver: false,
/**
* @type {boolean}
*/
showStatusBox: false,
/**
* @type {object}
*/
patchesData: patchesArray,
hasEmbedded: patchesArray.length > 0,
init() {
const CONFIG = {language: 'en', requireValidation: false};
if (!RomPatcherWeb.isInitialized()){
if (this.hasEmbedded) {
RomPatcherWeb.initialize(CONFIG, ...this.patchesData);
} else {
RomPatcherWeb.initialize(CONFIG);
}
}
const STAT_BOX = document.getElementById('patcher-status');
const OBSERVER = new MutationObserver(() => {
const CRC = document.getElementById('rom-patcher-span-crc32').textContent;
const DESC = document.getElementById('rom-patcher-patch-description').textContent;
const PATCH = document.getElementById('rom-patcher-patch-requirements-value').textContent;
this.showStatusBox = CRC.trim().length > 0 || DESC.trim().length > 0 || PATCH.trim().length > 0;
});
OBSERVER.observe(STAT_BOX, { childList: true, subtree: true, characterData: true });
},
/**
*
* @param {string} id
*/
triggerFileInput(id){
const I = document.getElementById(id);
if( !I.disabled )
I.click();
},
/**
*
* @param {Event} e
* @param {string} type
*/
handleInputChange(e, type){
const file = e.target.files[0];
if(file){
if( type === 'rom' ) this.romFileName = file.name;
if( type === 'patch' ) this.patchFileName = file.name;
}
},
/**
*
* @param {Event} e
* @param {string} type
*/
handleDrop(e, type ){
const file = e.dataTransfer.files[0];
if( !file )
return;
const ID = type === 'rom' ? 'rom-patcher-input-file-rom' : 'rom-patcher-input-file-patch';
const I = document.getElementById(ID);
if( I.disabled )
return;
I.files = e.dataTransfer.files;
if( type === 'rom' ) this.romFileName = file.name;
if( type === 'patch' ) this.patchFileName = file.name;
I.dispatchEvent(new Event('change', { bubbles: true }));
}
}
}

View File

@@ -66,6 +66,16 @@ export function FSFileData(name, totalChunks, rawFile ) {
*/
state: 'public',
/**
* If the online patcher is enabled
*/
meta_online_patcher: false,
/**
* If this patch is a secondary patch.
*/
meta_secondary_online_patcher: false,
/**
* Look if this file is currently uploading.
* @returns {boolean}

View File

@@ -7,6 +7,7 @@ import hovercard from "./hovercard.js";
import notifications from "./notifications.js";
import conversations from "./conversations.js";
import settings from "./settings.js";
import {RomPatcher} from "./RomPatcher.js";
/**
* Get config defined in meta.blade.php
@@ -43,3 +44,6 @@ Alpine.store('conversations', conversations() );
// Settings
Alpine.store('settings', settings() );
// ROMPatcher
window.RomPatcher = RomPatcher;

View File

@@ -372,6 +372,12 @@ window.Submission = function(){
this.errorKey = null; // Reset.
this.duringSubmissionProcess = true;
const STATE = document.querySelector('select[name="submit-state"]')?.value;
if( STATE === 'draft' ){
e.target.submit();
return;
}
if( !this.verifyForm() ){
this.scrollToError();

View File

@@ -63,10 +63,10 @@
</div>
<div class="hovercard-actions">
<a href="#" class="btn" title="View profile">
<a :href="`{{ xfRoute('members') }}/${$store.hovercard.data.user_id}/`" class="btn" title="View profile">
<i data-lucide="user" size="14"></i>
</a>
<a href="#" class="btn" title="Send message">
<a :href="`{{ xfRoute('direct-messages/add') }}?to=${$store.hovercard.data.username.replace(' ', '+')}`" class="btn" title="Send message">
<i data-lucide="mail" size="14"></i>
</a>
</div>

View File

@@ -15,10 +15,12 @@
<div class="menu-group">
<div class="menu-group-title">{{ $menu['name'] }}</div>
@foreach( $menu['items'] as $item )
<a href="{{ isset($item['xf_route']) ? xfRoute($item['xf_route']) : route($item['route']) }}"
@class(['menu-item', 'active' => request()->routeIs( $item['route'] ?? '' )]) >
<i data-lucide="{{ $item['icon'] }}"></i><span>{{ $item['name'] }}</span>
</a>
@if( !isset( $item['condition'] ) || $item['condition']() )
<a href="{{ isset($item['xf_route']) ? xfRoute($item['xf_route']) : route($item['route']) }}"
@class(['menu-item', 'active' => request()->routeIs( $item['route'] ?? '' )]) >
<i data-lucide="{{ $item['icon'] }}"></i><span>{{ $item['name'] }}</span>
</a>
@endif
@endforeach
</div>
@endforeach

View File

@@ -0,0 +1,6 @@
<form class="search-bar" style="margin-bottom: 15px;">
<input type="text" name="{{ $param }}" placeholder="{{ $placeholder }}" value="{{ request($param) }}" autocomplete="off" />
<button type="submit" class="search-button">
<i data-lucide="search" size="18" color="var(--text2)"></i>
</button>
</form>

View File

@@ -1,6 +1,7 @@
<div class="submit-level" x-data="{
nsfw: null,
state: '{{ old('submit-state', $defaultState) }}',
deleteOpen: false,
init(){
this.$watch('nsfw', (val) => {
if( val && this.state === 'published' ) {
@@ -9,6 +10,13 @@
});
}
}" x-init="init()">
@if($isEdit)
<div>
<button type="button" class="btn danger" @click="deleteOpen = true; $dispatch('modal:opened')">
<i data-lucide="trash-2" size="13"></i> Delete
</button>
</div>
@endif
<div>
@if( section_must_be( [ 'romhacks', 'homebrew' ], $section ) && !$isEdit )
<label class="nsfw-label"><input id="nsfw-checkbox" type="checkbox" name="nsfw-entry" x-model="nsfw" style="transform: scale(1.5)"> NSFW</label>
@@ -25,4 +33,49 @@
@endif
@endforeach
</select>
@if($isEdit)
<template x-teleport="body">
<div
class="modal-overlay"
x-cloak
x-show="deleteOpen"
x-transition.opacity
@click.self="deleteOpen = false"
@keydown.escape.window="deleteOpen = false"
@modal:opened.window="refreshIcons($el)"
>
<div class="modal-window" x-show="deleteOpen" x-transition>
<div class="modal-header">
<span class="modal-title">Delete entry</span>
<button type="button" class="modal-close" @click="deleteOpen = false">
<i data-lucide="x" size="20"></i>
</button>
</div>
<div class="modal-body">
<p style="margin-bottom: 1.5rem; color: var(--text, #333);">
Are you sure you want to delete this entry? This action cannot be undone.
</p>
<form action="{{ route('submit.destroy', ['section' => $section, 'entry' => $entry ]) }}" method="POST">
@csrf
@method('DELETE')
<div class="queue-mod-actions">
<button type="button" class="btn" @click="deleteOpen = false">
Cancel
</button>
<button type="submit" class="btn danger">
<i data-lucide="trash-2" size="14"></i>
Confirm deletion
</button>
</div>
</form>
</div>
</div>
</div>
</template>
@endif
</div>

View File

@@ -4,14 +4,16 @@
<i data-lucide="menu"></i>
</button>
<div class="search-bar">
<i data-lucide="search" size="18" color="var(--text2)"></i>
<input type="text">Search</input>
</div>
<form class="search-bar" action="{{ route('entries.index') }}">
<input type="text" name="s" placeholder="Search" />
<button type="submit" class="search-button">
<i data-lucide="search" size="18" color="var(--text2)"></i>
</button>
</form>
<div class="topbar-actions">
@if( !\Auth::guest() && \Auth::user()->is_admin === 1 )
@can('is-admin')
@php $topbarAdminSeparator = true; @endphp
<a href="{{ config('app.forum_url') . '/admin.php' }}" class="btn">
<i data-lucide="landmark" size="18"></i>
@@ -19,15 +21,15 @@
<a href="{{ config('app.url') . '/manage' }}" class="btn">
<i data-lucide="shield-cog" size="18"></i>
</a>
@endif
@endcan
@if( $topbarAdminSeparator )
<div class="vertical-separator"></div>
@endif
@if( !\Auth::guest() && \Auth::user()->is_moderator === 1 )
@can('is-mod')
@php $topbarModSeparator = true; @endphp
<a href="#" class="btn">
<a href="{{ route('modcp.index') }}" class="btn">
<i data-lucide="siren" size="18"></i>
</a>
<a href="{{ xfRoute('approval-queue') }}" class="btn">
@@ -36,7 +38,7 @@
<a href="{{ xfRoute('reports') }}" class="btn">
<i data-lucide="triangle-alert" size="18"></i>
</a>
@endif
@endcan
@if( $topbarModSeparator )
<div class="vertical-separator"></div>

View File

@@ -0,0 +1,54 @@
<div class="drafts-item">
<div class="drafts-cover">
@if($entry->main_image)
<img src="{{ Storage::url($entry->main_image) }}">
@else
<div class="drafts-cover-placeholder">
<i data-lucide="image" size="24"></i>
</div>
@endif
</div>
<div class="drafts-info">
<div class="drafts-top">
<div>
<h3 class="drafts-title">
{{ $entry->complete_title }}
</h3>
<div class="drafts-meta">
<span class="badge {{ $entry->type }}">
{{ \App\Livewire\Database::ENTRY_TYPES[$entry->type] }}
</span>
@if( $entry->getRealPlatform() )
<span class="badge">{{ $entry->getRealPlatform()->name }}</span>
@endif
@if( $entry->version )
<span class="badge">{{ $entry->version }}</span>
@endif
</div>
</div>
<div class="drafts-dates">
<span>
<i data-lucide="pencil" size="12"></i>
Last edited {{ $draft->updated_at->diffForHumans() }}
</span>
<span>
<i data-lucide="calendar" size="12"></i>
Created {{ $draft->created_at->format('d M Y') }}
</span>
</div>
</div>
<div class="drafts-actions">
<a href="{{ route('submit.edit', ['section' =>$entry->type, 'entry' => $entry]) }}" class="btn primary">
<i data-lucide="pen" size="13"></i>
Continue editing
</a>
<a href="{{ route('entries.show', ['section' => $entry->type, 'entry' => $entry ] ) }}" class="btn" target="_blank">
<i data-lucide="eye" size="13"></i>
Preview
</a>
</div>
</div>
</div>

View File

@@ -0,0 +1,25 @@
@extends('layouts.app')
@section('page-title', "My Drafts - " . config('app.name'))
@section('content')
<div class="page-title">
My Drafts
</div>
@if($drafts->isEmpty())
<div class="drafts-empty">
<i data-lucide="pen" size="48"></i>
<p>No drafts here</p>
</div>
@else
<div class="drafts-count">
<span>{{ $drafts->total() }} draft{{ $drafts->total() > 1 ? 's' : '' }}</span>
</div>
<div class="drafts-list">
@foreach($drafts as $draft)
@include('entries.draft_item', ['entry' => $draft])
@endforeach
</div>
{{ $drafts->links() }}
@endif
@endsection

View File

@@ -83,28 +83,28 @@
</div>
<div class="entry-meta-grid">
@if( $entry->game )
<x-entry-meta-item label="Game Name" value="{{ $entry->game->name }}" />
<x-entry-meta-item label="Game Name" value="{{ $entry->game->name }}" route="{!! databaseRoute( [ 'games' => [ $entry->game->id ], 'platforms' => [ $entry->getRealPlatform()?->id ] ] ) !!}" />
@endif
@if( $entry->getRealPlatform() )
<x-entry-meta-item label="Platform" value="{{ ($entry->getRealPlatform())->name }}" />
<x-entry-meta-item label="Platform" value="{{ ($entry->getRealPlatform())->name }}" route="{!! databaseRoute( ['platforms' => [ $entry->getRealPlatform()->id ] ] ) !!}" />
@endif
@if( $entry->game && $entry->game->genre )
<x-entry-meta-item label="Genre" value="{{ $entry->game->genre->name }}" />
<x-entry-meta-item label="Genre" value="{{ $entry->game->genre->name }}" route="{!! databaseRoute( ['genres' => [ $entry->game->genre->id ] ]) !!}" />
@endif
@if( $entry->languages->isNotEmpty() )
<x-entry-meta-item label="Language" value="{{ $entry->languages->pluck('name')->implode(', ') }}" route="none" />
<x-entry-meta-item label="Language" value="{{ $entry->languages->pluck('name')->implode(', ') }}" route="{!! databaseRoute( [ 'languages' => $entry->languages->pluck('id')->toArray() ]) !!}" />
@endif
@if( $entry->status_id )
<x-entry-meta-item label="Status" value="{{ $entry->status->name }}" />
<x-entry-meta-item label="Status" value="{{ $entry->status->name }}" route="{!! databaseRoute( ['statuses' => [ $entry->status->id ] ]) !!}" />
@endif
@if( $entry->modifications->isNotEmpty() )
<x-entry-meta-item label="Type of hack" value="{{ $entry->modifications->pluck('name')->implode(', ') }}" route="{!! databaseRoute( [ 'modifications' => $entry->modifications->pluck('id')->toArray() ] ) !!}" />
@endif
@if( $entry->version )
<x-entry-meta-item label="Version" value="{{ $entry->version }}" route="none" />
@endif
@if( $entry->release_date )
<x-entry-meta-item label="Release Date" value="{{ $entry->release_date }}" />
@endif
@if( $entry->modifications->isNotEmpty() )
<x-entry-meta-item label="Type of hack" value="{{ $entry->modifications->pluck('name')->implode(', ') }}" route="none" />
<x-entry-meta-item label="Release Date" value="{{ $entry->release_date->format('Y-m-d') }}" route="none" />
@endif
</div>
<div class="hack-actions" style="display:flex;gap:10px;">
@@ -206,7 +206,7 @@
@endforeach
</div>
@endif
@if( $entry->staff_credits )
@if( $entry->parseStaffCredits() )
<x-entry-section-title label="Staff Credits" icon="users-round" />
<div class="entry-description">
<ul>

View File

@@ -0,0 +1,95 @@
@extends('layouts.app')
@section('content')
<div class="modcp-wrapper">
<aside class="modcp-sidebar">
<div class="modcp-sidebar-header">
<i data-lucide="shield" size="16"></i>
Mod CP
</div>
<nav class="modcp-nav">
<div class="modcp-nav-group">
<span class="modcp-nav-label">Overview</span>
<a href="{{ route('modcp.index') }}" class="modcp-nav-item" {{ request()->routeIs('modcp.index') ? 'active' : '' }}>
<i data-lucide="layout-dashboard" size="15"></i>
Dashboard
</a>
<a href="{{ route('queue.index') }}" class="modcp-nav-item" {{ request()->routeIs('queue.index') ? 'active' : '' }}>
<i data-lucide="gavel" size="15"></i>
Submissions Queue
@if(( $pending = \App\Models\Entry::where('state','pending')->count() ) > 0)
<span class="modcp-nav-badge">{{ $pending }}</span>
@endif
</a>
</div>
<div class="modcp-nav-group">
<span class="modcp-nav-label">Content</span>
<a href="{{ route('modcp.locked') }}" class="modcp-nav-item" {{ request()->routeIs('modcp.locked') ? 'active' : '' }}>
<i data-lucide="lock" size="15"></i>
Locked entries
</a>
@can('is-admin')
<a href="{{ route('modcp.draft') }}" class="modcp-nav-item" {{ request()->routeIs('modcp.draft') ? 'active' : '' }}>
<i data-lucide="scissors" size="15"></i>
Draft entries
</a>
<a href="{{ route('modcp.hidden') }}" class="modcp-nav-item" {{ request()->routeIs('modcp.hidden') ? 'active' : '' }}>
<i data-lucide="eye-off" size="15"></i>
Hidden entries
</a>
<a href="{{ route('modcp.deleted') }}" class="modcp-nav-item" {{ request()->routeIs('modcp.deleted') ? 'active' : '' }}>
<i data-lucide="trash-2" size="15"></i>
Deleted entries
</a>
@endcan
</div>
<div class="modcp-nav-group">
<span class="modcp-nav-label">Resources</span>
<a href="{{ route('modcp.games.index') }}" class="modcp-nav-item" {{ request()->routeIs('modcp.games.*') ? 'active' : '' }}">
<i data-lucide="gamepad-2" size="15"></i>
Games
</a>
<a href="{{ route('modcp.languages.index') }}" class="modcp-nav-item" {{ request()->routeIs('modcp.languages.*') ? 'active' : '' }}">
<i data-lucide="languages" size="15"></i>
Languages
</a>
<a href="{{ route('modcp.authors.index') }}" class="modcp-nav-item" {{ request()->routeIs('modcp.authors.*') ? 'active' : '' }}">
<i data-lucide="users" size="15"></i>
Authors
</a>
@can('is-admin')
<a href="{{ route('modcp.platforms.index') }}" class="modcp-nav-item" {{ request()->routeIs('modcp.platforms.*') ? 'active' : '' }}">
<i data-lucide="gamepad-directional" size="15"></i>
Platforms
</a>
<a href="{{ route('modcp.genres.index') }}" class="modcp-nav-item" {{ request()->routeIs('modcp.genres.*') ? 'active' : '' }}">
<i data-lucide="box" size="15"></i>
Genres
</a>
@endcan
</div>
<div class="modcp-nav-group">
<span class="modcp-nav-label">Community</span>
<a href="{{ xfRoute('reports') }}" class="modcp-nav-item">
<i data-lucide="triangle-alert" size="15"></i>
Reports
</a>
<a href="{{ xfRoute('approval-queue') }}" class="modcp-nav-item">
<i data-lucide="message-circle-check" size="15"></i>
Approval Queue
</a>
</div>
</nav>
</aside>
<div class="modcp-content">
@yield('modcp-content')
</div>
</div>
@endsection

View File

@@ -37,6 +37,9 @@
{{-- Platforms --}}
<x-database-filter-without-mode title="Platform" :items="$allPlatforms" model="platforms"/>
{{-- Genres --}}
<x-database-filter-without-mode title="Genre" :items="$allGenres" model="genres"/>
{{-- Statuses --}}
<x-database-filter-without-mode title="Status" :items="$allStatuses" model="statuses"/>

View File

@@ -0,0 +1,89 @@
@extends('layouts.modcp')
@section('modcp-content')
<div class="modcp-page-title">
Authors
<span class="modcp-count">{{ $items->total() }}</span>
</div>
<x-mod-c-p-search placeholder="Search an author..."/>
<div class="modcp-add-form">
<form action="{{ route('modcp.authors.store') }}" method="POST">
@csrf
<div class="modcp-add-form-inner">
<input type="text" name="name" class="form-input"
placeholder="Author name..." required>
<div style="width:20%">
<livewire:xf-user-selector />
</div>
<input type="text" name="website" class="form-input"
placeholder="Website">
<button type="submit" class="btn primary">
<i data-lucide="plus" size="14"></i> Add
</button>
</div>
</form>
</div>
<div class="modcp-list">
@forelse($items as $author)
<div class="modcp-list-item" x-data="{ editing: false }">
<div class="modcp-list-item-info" x-show="!editing">
<span class="modcp-list-item-title">{{ $author->name }}</span>
<span class="modcp-list-item-meta">
<span class="badge">{{ $author->website ?? '—' }}</span>
@if(($xfUser = $author->user()) !== null )
<span class="badge">
<x-xf-username-link :user="$xfUser" />
</span>
@endif
· {{ $author->entries_count }} {{ Str::plural('entry', $author->entries_count) }}
</span>
</div>
<form action="{{ route('modcp.authors.update', $author) }}" method="POST"
class="modcp-list-item-edit modcp-list-item-edit--game"
x-show="editing" x-cloak>
@csrf @method('PATCH')
<input type="text" name="name" class="form-input"
placeholder="Author name..." value="{{ $author->name }}" required>
<div style="width:20%">
<livewire:xf-user-selector :initial-user-id="$author->user_id" />
</div>
<input type="text" name="website" class="form-input"
placeholder="Website" value="{{ $author->website }}">
<button type="submit" class="btn primary">
<i data-lucide="check" size="13"></i>
</button>
<button type="button" class="btn" @click="editing = false">
<i data-lucide="x" size="13"></i>
</button>
</form>
<div class="modcp-list-item-actions" x-show="!editing">
<button type="button" class="btn" @click="editing = true">
<i data-lucide="pen" size="13"></i>
</button>
<form action="{{ route('modcp.authors.destroy', $author) }}" method="POST"
style="display:inline"
onsubmit="return confirm('Delete {{ addslashes($author->name) }}?')">
@csrf @method('DELETE')
<button type="submit" class="btn danger"
{{ $author->entries_count > 0 ? 'disabled title=Has entries' : '' }}>
<i data-lucide="trash-2" size="13"></i>
</button>
</form>
</div>
</div>
@empty
<div class="modcp-empty"><p>No authors yet.</p></div>
@endforelse
</div>
{{ $items->links() }}
@endsection

View File

@@ -0,0 +1,61 @@
@extends('layouts.modcp')
@section('page-title', 'Deleted entries - ' . config('app.name') )
@section('modcp-content')
<div class="modcp-page-title">
<i data-lucide="trash-2" size="20"></i>
Deleted entries
<span class="modcp-count">{{ $entries->total() }}</span>
</div>
@if($entries->isEmpty())
<div class="modcp-empty">
<i data-lucide="check-circle" size="36"></i>
<p>No deleted entries.</p>
</div>
@else
<div class="modcp-list">
@foreach($entries as $entry)
<div class="modcp-list-item modcp-list-item--deleted">
<div class="modcp-list-item-cover">
@if($entry->main_image)
<img src="{{ Storage::url($entry->main_image) }}" alt="">
@else
<i data-lucide="image" size="20"></i>
@endif
</div>
<div class="modcp-list-item-info">
<span class="modcp-list-item-title">{{ $entry->complete_title ?? $entry->title }}</span>
<span class="modcp-list-item-meta">
<span class="badge {{ $entry->type }}">{{ $entry->type }}</span>
@php $daysLeft = max(0, 7 - (int) now()->diffInDays($entry->deleted_at)) @endphp
<span style="color: var(--error)">
Deleted {{ $entry->deleted_at->diffForHumans() }}
@if($daysLeft > 0) · purged in {{ $daysLeft }}d @endif
</span>
</span>
</div>
<div class="modcp-list-item-actions">
<form action="{{ route('modcp.restore', $entry->id) }}" method="POST" style="display:inline">
@csrf @method('PATCH')
<button type="submit" class="btn success">
<i data-lucide="rotate-ccw" size="13"></i> Restore
</button>
</form>
<form action="{{ route('modcp.destroy', $entry->id) }}" method="POST" style="display:inline"
@submit="if (!confirm('Permanently delete this entry?')) $event.preventDefault()">
@csrf @method('DELETE')
<button type="submit" class="btn danger">
<i data-lucide="trash-2" size="13"></i> Purge
</button>
</form>
</div>
</div>
@endforeach
</div>
{{ $entries->links() }}
@endif
@endsection

View File

@@ -0,0 +1,52 @@
@extends('layouts.modcp')
@section('page-title', $pageTitle . ' - ' . config('app.name') )
@section('modcp-content')
<div class="modcp-page-title">
{{ $pageTitle }}
<span class="modcp-count">{{ $entries->count() }}</span>
</div>
@if($entries->isEmpty())
<div class="modcp-empty">
<i data-lucide="check-circle" size="36"></i>
<p>No {{ $state }} entries.</p>
</div>
@else
<div class="modcp-list">
@foreach($entries as $entry)
<div class="modcp-list-item">
<div class="modcp-list-item-cover">
@if($entry->main_image)
<img src="{{ Storage::url($entry->main_image) }}" alt="">
@else
<i data-lucide="image" size="20"></i>
@endif
</div>
<div class="modcp-list-item-info">
<span class="modcp-list-item-title">{{ $entry->complete_title }}</span>
<span class="modcp-list-item-meta">
<span class="badge {{ $entry->type }}">{{ \App\Livewire\Database::ENTRY_TYPES[$entry->type] }}</span>
@if($entry->getRealPlatform())
<span class="badge">{{ $entry->getRealPlatform()->name }}</span>
@endif
Added {{ $entry->created_at->format('d M Y') }} by<x-xf-username-link :user-id="$entry->user_id" />
</span>
</div>
<div class="modcp-list-item-actions">
<a href="{{ route('entries.show', ['section' => $entry->type, 'entry' => $entry]) }}"
class="btn" target="_blank">
<i data-lucide="eye" size="13"></i> View
</a>
<a href="{{ route('submit.edit', [$entry->type, $entry->id]) }}"
class="btn">
<i data-lucide="pen" size="13"></i> Edit
</a>
</div>
</div>
@endforeach
</div>
{{ $entries->links() }}
@endif
@endsection

View File

@@ -0,0 +1,103 @@
@extends('layouts.modcp')
@section('modcp-content')
<div class="modcp-page-title">
Games
<span class="modcp-count">{{ $items->total() }}</span>
</div>
<x-mod-c-p-search placeholder="Search a game..." />
<div class="modcp-add-form">
<form action="{{ route('modcp.games.store') }}" method="POST">
@csrf
<div class="modcp-add-form-inner">
<input type="text" name="name" class="form-input"
placeholder="Game name..." required>
<select name="platform_id" class="form-select" required style="width: 20%">
<option value="" disabled selected>Platform...</option>
@foreach($platforms as $platform)
<option value="{{ $platform->id }}">{{ $platform->name }}</option>
@endforeach
</select>
<select name="genre_id" class="form-select" required style="width: 20%">
<option value="" disabled selected>Genre...</option>
@foreach($genres as $genre)
<option value="{{ $genre->id }}">{{ $genre->name }}</option>
@endforeach
</select>
<button type="submit" class="btn primary">
<i data-lucide="plus" size="14"></i> Add
</button>
</div>
</form>
</div>
<div class="modcp-list">
@forelse($items as $game)
<div class="modcp-list-item" x-data="{ editing: false }">
<div class="modcp-list-item-info" x-show="!editing">
<span class="modcp-list-item-title">{{ $game->name }}</span>
<span class="modcp-list-item-meta">
<span class="badge">{{ $game->platform->name ?? '—' }}</span>
<span class="badge">{{ $game->genre->name ?? '—' }}</span>
· {{ $game->entries_count }} {{ Str::plural('entry', $game->entries_count) }}
</span>
</div>
<form action="{{ route('modcp.games.update', $game) }}" method="POST"
class="modcp-list-item-edit modcp-list-item-edit--game"
x-show="editing" x-cloak>
@csrf @method('PATCH')
<input type="text" name="name" class="form-input"
value="{{ $game->name }}" required>
<select name="platform_id" class="form-select form-select--small" required>
@foreach($platforms as $platform)
<option value="{{ $platform->id }}"
{{ $game->platform_id == $platform->id ? 'selected' : '' }}>
{{ $platform->name }}
</option>
@endforeach
</select>
<select name="genre_id" class="form-select form-select--small" required>
@foreach($genres as $genre)
<option value="{{ $genre->id }}"
{{ $game->genre_id == $genre->id ? 'selected' : '' }}>
{{ $genre->name }}
</option>
@endforeach
</select>
<button type="submit" class="btn primary">
<i data-lucide="check" size="13"></i>
</button>
<button type="button" class="btn" @click="editing = false">
<i data-lucide="x" size="13"></i>
</button>
</form>
<div class="modcp-list-item-actions" x-show="!editing">
<button type="button" class="btn" @click="editing = true">
<i data-lucide="pen" size="13"></i>
</button>
<form action="{{ route('modcp.games.destroy', $game) }}" method="POST"
style="display:inline"
onsubmit="return confirm('Delete {{ addslashes($game->name) }}?')">
@csrf @method('DELETE')
<button type="submit" class="btn danger"
{{ $game->entries_count > 0 ? 'disabled title=Has entries' : '' }}>
<i data-lucide="trash-2" size="13"></i>
</button>
</form>
</div>
</div>
@empty
<div class="modcp-empty"><p>No games yet.</p></div>
@endforelse
</div>
{{ $items->links() }}
@endsection

View File

@@ -0,0 +1,91 @@
@extends('layouts.modcp')
@section('page-title', "Dashboard - " . config('app.name') )
@section('modcp-content')
<div class="modcp-page-title">
Dashboard
</div>
<div class="modcp-stats">
<a href="{{ route('queue.index') }}" class="modcp-stat-card modcp-stat-card--orange">
<div class="modcp-stat-icon"><i data-lucide="clipboard-list" size="22"></i></div>
<div class="modcp-stat-info">
<span class="modcp-stat-value">{{ $stats['pending'] }}</span>
<span class="modcp-stat-label">In queue</span>
</div>
</a>
<a href="{{ route('modcp.locked') }}" class="modcp-stat-card">
<div class="modcp-stat-icon"><i data-lucide="lock" size="22"></i></div>
<div class="modcp-stat-info">
<span class="modcp-stat-value">{{ $stats['locked'] }}</span>
<span class="modcp-stat-label">Locked</span>
</div>
</a>
@can('is-admin')
<a href="{{ route('modcp.draft') }}" class="modcp-stat-card">
<div class="modcp-stat-icon"><i data-lucide="scissors" size="22"></i></div>
<div class="modcp-stat-info">
<span class="modcp-stat-value">{{ $stats['draft'] }}</span>
<span class="modcp-stat-label">Draft</span>
</div>
</a>
<a href="{{ route('modcp.hidden') }}" class="modcp-stat-card">
<div class="modcp-stat-icon"><i data-lucide="eye-off" size="22"></i></div>
<div class="modcp-stat-info">
<span class="modcp-stat-value">{{ $stats['hidden'] }}</span>
<span class="modcp-stat-label">Hidden</span>
</div>
</a>
<a href="{{ route('modcp.deleted') }}" class="modcp-stat-card modcp-stat-card--danger">
<div class="modcp-stat-icon"><i data-lucide="trash-2" size="22"></i></div>
<div class="modcp-stat-info">
<span class="modcp-stat-value">{{ $stats['deleted'] }}</span>
<span class="modcp-stat-label">Deleted</span>
</div>
</a>
@endcan
<div class="modcp-stat-card modcp-stat-card--muted">
<div class="modcp-stat-icon"><i data-lucide="database" size="22"></i></div>
<div class="modcp-stat-info">
<span class="modcp-stat-value">{{ $stats['total'] }}</span>
<span class="modcp-stat-label">Total entries</span>
</div>
</div>
</div>
@if($recentDeleted->isNotEmpty())
<div class="modcp-section-title" style="margin-top: 25px;">Recently deleted</div>
<div class="modcp-list">
@foreach($recentDeleted as $entry)
<div class="modcp-list-item">
<div class="modcp-list-item-info">
<span class="modcp-list-item-title">{{ $entry->complete_title ?? $entry->title }}</span>
<span class="modcp-list-item-meta">
<span class="badge {{ $entry->type }}">{{ $entry->type }}</span>
Deleted {{ $entry->deleted_at->diffForHumans() }}
</span>
</div>
<div class="modcp-list-item-actions">
<form action="{{ route('modcp.restore', $entry->id) }}" method="POST" style="display:inline">
@csrf @method('PATCH')
<button type="submit" class="btn success">
<i data-lucide="rotate-ccw" size="13"></i> Restore
</button>
</form>
<form action="{{ route('modcp.destroy', $entry->id) }}" method="POST" style="display:inline"
onsubmit="return confirm('Permanently delete?')">
@csrf @method('DELETE')
<button type="submit" class="btn danger">
<i data-lucide="trash-2" size="13"></i> Purge
</button>
</form>
</div>
</div>
@endforeach
<a href="{{ route('modcp.deleted') }}" class="modcp-list-see-all">
See all deleted entries
</a>
</div>
@endif
@endsection

View File

@@ -0,0 +1,76 @@
@extends('layouts.modcp')
@section('page-title', $title . ' - ' . config('app.name'))
@section('modcp-content')
<div class="modcp-page-title">
{{ $title }}
<span class="modcp-count">{{ $items->total() }}</span>
</div>
<x-mod-c-p-search placeholder="Search a {{ $singular }}..." />
<div class="modcp-add-form">
<form action="{{ route($storeRoute) }}" method="POST" class="modcp-add-form-inner">
@csrf
<input type="text" name="name" class="form-input" placeholder="Add new {{ strtolower($singular) }}..." required>
@if(isset($extraFields))
@foreach($extraFields as $field)
<input type="text" name="{{ $field['name'] }}" class="form-input" placeholder="{{ $field['placeholder'] }}">
@endforeach
@endif
<button type="submit" class="btn primary">
<i data-lucide="plus" size="14"></i> Add
</button>
</form>
</div>
<div class="modcp-list">
@forelse($items as $item)
<div class="modcp-list-item" x-data="{ editing: false }">
<div class="modcp-list-item-info" x-show="!editing">
<span class="modcp-list-item-title">{{ $item->name }}</span>
<span class="modcp-list-item-meta">
slug: {{ $item->slug }}
@isset($item->entries_count)
· {{ $item->entries_count }} {{ Str::plural('entry', $item->entries_count) }}
@endisset
</span>
</div>
<form action="{{ route($updateRoute, $item) }}" method="POST"
class="modcp-list-item-edit" x-show="editing" x-cloak>
@csrf @method('PATCH')
<input type="text" name="name" class="form-input" value="{{ $item->name }}">
<button type="submit" class="btn primary">
<i data-lucide="check" size="13"></i>
</button>
<button type="button" class="btn" @click="editing = false">
<i data-lucide="x" size="13"></i>
</button>
</form>
<div class="modcp-list-item-actions" x-show="!editing">
<button type="button" class="btn" @click="editing = true">
<i data-lucide="pen" size="13"></i>
</button>
<form action="{{ route($destroyRoute, $item) }}" method="POST" style="display:inline"
onsubmit="return confirm('Delete {{ $item->name }}?')">
@csrf @method('DELETE')
<button type="submit" class="btn danger">
<i data-lucide="trash-2" size="13"></i>
</button>
</form>
</div>
</div>
@empty
<div class="modcp-empty">
<p>No {{ strtolower($title) }} yet.</p>
</div>
@endforelse
</div>
{{ $items->links() }}
@endsection

View File

@@ -137,7 +137,7 @@
@endif
<x-form-group-title label="{{ $words['attachments'] }}" icon="paperclip" />
<x-main-image-field :old-path="old('main-image', $entry->main_image ?? '')" />
<x-main-image-field :old-path="old('main-image', $entry->main_image ?? '') ?? ''" />
<x-gallery-field :old-paths="old('gallery', $entry->gallery->pluck('image')->toArray() ?? [] )"/>
@error('gallery')
<x-form-error-text message="{{ $message }}" />
@@ -181,6 +181,18 @@
<livewire:xf-user-selector :initial-user-id="$entry->user_id" />
</div>
</div>
<div class="form-group grid-c2">
<div>
<x-form-field-title name="XenForo Comments Thread ID" />
<input type="text" name="comments_thread_id" class="form-input" value="{{ old('comments_thread_id', $entry->comments_thread_id) }}">
</div>
</div>
<div class="form-group">
<x-form-field-title name="Metadata" required="true" />
<div class="form-type-of-checkboxes form-group level" id="entry-metadata">
<label><input class="form-checkbox" type="checkbox" name="featured" value="1" {{ old('featured', $entry->featured ) ? 'checked' : '' }}>Featured entry</label>
</div>
</div>
@endcan
@cannot('moderate', $entry)

View File

@@ -36,21 +36,18 @@
'upload-item-done': file.done,
'upload-item-error': file.error
}">
<template x-if="!file.done && !file.error">
<i data-lucide="loader-2" class="spin"></i>
</template>
<template x-if="file.error">
<i data-lucide="alert-circle"></i>
</template>
<template x-if="file.done && file.state === 'public'">
<i data-lucide="eye" class="file-state-icon file-state-icon--public"></i>
</template>
<template x-if="file.done && file.state === 'private'">
<i data-lucide="eye-off" class="file-state-icon file-state-icon--private"></i>
</template>
<template x-if="file.done && file.state === 'archived'">
<i data-lucide="archive" class="file-state-icon file-state-icon--archived"></i>
</template>
<div class="file-status-icons">
<i x-show="!file.done && !file.error" data-lucide="loader-2" class="spin"></i>
<i x-show="file.error" data-lucide="alert-circle"></i>
<i x-show="file.done && file.state === 'public'" data-lucide="eye" class="file-state-icon file-state-icon--public"></i>
<i x-show="file.done && file.state === 'private'" data-lucide="eye-off" class="file-state-icon file-state-icon--private"></i>
<i x-show="file.done && file.state === 'archived'" data-lucide="archive" class="file-state-icon file-state-icon--archived"></i>
</div>
<div class="upload-item-info">
<span class="upload-item-name" x-text="file.name"></span>
@@ -103,13 +100,46 @@
</div>
</div>
@endif
<div class="upload-item-actions">
<div class="upload-item-actions" x-data="{ showMetadata: false }">
<button type="button" class="btn" x-show="file.error" @click="handleRetryFile(i)">
<i data-lucide="refresh-cw"></i>
</button>
@if($isEdit)
<button type="button" class="btn" x-show="file.done" @click="showMetadata = true">
<i data-lucide="settings"></i>
</button>
@endif
<button type="button" class="btn" x-show="file.done || file.error" @click="handleRemoveFile(i)">
<i data-lucide="x"></i>
</button>
<template x-teleport="body">
<div class="modal-overlay"
x-cloak
x-show="showMetadata"
x-transition.opacity.duration.300ms
@click.self="showMetadata = false"
@keydown.escape.window="showMetadata = false">
<div class="modal-window" x-show="showMetadata" x-transition>
<div class="modal-header">
<span class="modal-title" style="display: flex; align-items: center; gap: 8px;">
File Settings: <span x-text="file.name" style="color: var(--rhpz-orange);"></span>
</span>
<button type="button" class="modal-close" @click="showMetadata = false">
<i data-lucide="x" size="20"></i>
</button>
</div>
<div class="modal-content">
<div class="form-group">
<x-form-group-title label="Online patcher" />
<label><input type="checkbox" class="form-checkbox" :name="'files_metadata[' + file.uuid + '][online_patcher]'" x-model="file.meta_online_patcher"> Enable it</label>
<label x-show="file.meta_online_patcher"><input type="checkbox" class="form-checkbox" :name="'files_metadata[' + file.meta_secondary_online_patcher + '][secondary_online_patcher]'" x-model="file.meta_secondary_online_patcher"> Mark as a secondary patch</label>
</div>
</div>
</div>
</div>
</template>
</div>
<input type="hidden" name="files_uuid[]" :value="file.uuid" x-show="file.done">
@if($isEdit)

View File

@@ -0,0 +1,92 @@
@extends('layouts.app')
@section('page-title', "ROM Patcher - " . config('app.name'))
@push('scripts')
<script type="text/javascript" src="{{ asset('rom-patcher-js/RomPatcher.webapp.js') }}"></script>
@endpush
@section('content')
<div class="page-title">
<span>ROM Patcher</span>
</div>
<div id="rom-patcher-container" class="patcher-container" x-data="RomPatcher({{ Js::from($patches ?? []) }})">
<div class="patcher-grid">
<div class="form-group">
<label class="form-label">1. Original ROM</label>
<div class="patcher-dropzone" id="rom-dropzone"
:class="{ 'dragover': isRomDragOver, 'has-file': romFileName !== '' }"
@click="triggerFileInput('rom-patcher-input-file-rom')"
@dragover.prevent="isRomDragOver = true"
@dragleave.prevent="isRomDragOver = false"
@drop.prevent="isRomDragOver = false; handleDrop($event, 'rom')">
<input type="file" id="rom-patcher-input-file-rom" style="display: none;" @change="handleInputChange($event, 'rom')" disabled />
<i data-lucide="gamepad-2" size="40" :style="romFileName ? 'color: var(--rhpz-orange)' : 'color: var(--text2)'"></i>
<span style="color: var(--text); font-weight: 500;" x-text="romFileName ? romFileName : 'Drag n\'drop your file here or click'"></span>
</div>
</div>
<div class="form-group">
<label class="form-label">2. Patch file</label>
<div class="patcher-dropzone" id="patch-dropzone"
x-show="!hasEmbedded"
:class="{ 'dragover': isPatchDragOver, 'has-file': patchFileName !== '' }"
@click="triggerFileInput('rom-patcher-input-file-patch')"
@dragover.prevent="isPatchDragOver = true"
@dragleave.prevent="isPatchDragOver = false"
@drop.prevent="isPatchDragOver = false; handleDrop($event, 'patch')">
<input type="file" id="rom-patcher-input-file-patch" style="display: none;" @change="handleInputChange($event, 'patch')" disabled />
<i data-lucide="file-archive" size="40" :style="patchFileName ? 'color: var(--rhpz-orange)' : 'color: var(--text2)'"></i>
<span style="color: var(--text); font-weight: 500;" x-text="patchFileName ? patchFileName : 'Drag n\'drop your file here or click'"></span>
</div>
<div x-show="hasEmbedded" class="embed-patch-box">
<div class="embed-patch-box-icon">
<div class="embed-patch-box-icon-block">
<i data-lucide="package" size="24" color="var(--rhpz-orange)"></i>
</div>
<div>
<div style="font-weight: 600; color: var(--text); font-size: 1.1rem;">Patch file</div>
<div style="font-size: 0.85rem; color: var(--text2);">Select if there is multiple patch files</div>
</div>
</div>
<div class="rom-patcher-container-input" style="margin-top: 10px;">
<select id="rom-patcher-select-patch" class="form-select" style="width: 100%; cursor: pointer;"></select>
</div>
</div>
</div>
</div>
<div class="patcher-status-box" id="patcher-status" x-show="showStatusBox" x-transition x-cloak style="margin-top: 20px;">
<div class="rom-patcher-row" style="color: var(--text2);">
<div style="color: var(--rhpz-orange); font-weight: bold;">Checksums:</div>
<ul style="margin-bottom: 0">
<li>CRC32: <span id="rom-patcher-span-crc32"></span></li>
<li>MD5: <span id="rom-patcher-span-md5"></span></li>
<li>SHA-1: <span id="rom-patcher-span-sha1"></span></li>
</ul>
</div>
<span id="rom-patcher-span-rom-info"></span>
<div class="rom-patcher-row margin-bottom" id="rom-patcher-row-patch-description">
<div style="color: var(--rhpz-orange); font-weight: bold;">Description:</div>
<div id="rom-patcher-patch-description"></div>
</div>
<div class="rom-patcher-row margin-bottom" id="rom-patcher-row-patch-requirements">
<div id="rom-patcher-patch-requirements-type" style="color: var(--rhpz-orange); font-weight: bold;">ROM requirements:</div>
<div id="rom-patcher-patch-requirements-value"></div>
</div>
</div>
<div style="margin-top: 25px; border-top: 1px solid var(--border); padding-top: 20px; text-align: right;">
<button type="button" class="btn primary" id="rom-patcher-button-apply" disabled>
<i data-lucide="wrench" size="16"></i> Apply patch
</button>
</div>
</div>
@endsection