diff --git a/fxmanifest.lua b/fxmanifest.lua index db518a2..a08cd79 100644 --- a/fxmanifest.lua +++ b/fxmanifest.lua @@ -18,7 +18,8 @@ client_scripts { 'client/functions.lua', 'client/loops.lua', 'client/events.lua', - 'client/drawtext.lua' + 'client/drawtext.lua', + 'client/editor.lua' } server_scripts { @@ -38,9 +39,11 @@ files { 'html/index.html', 'html/css/*.css', 'html/js/*.js', - 'shared/jobs.json', + 'shared/config.json', 'shared/gangs.json', 'shared/items.json', + 'shared/jobs.json', + 'shared/player_defaults.json', 'shared/vehicles.json', 'shared/weapons.json' } diff --git a/html/css/editor.css b/html/css/editor.css index 96bc1ec..613acba 100644 --- a/html/css/editor.css +++ b/html/css/editor.css @@ -1,95 +1,6 @@ -@import url("https://fonts.googleapis.com/css2?family=Exo+2:wght@300;400;500;600;700&display=swap"); - -:root { - /* Primary colors */ - --md-primary: #f44336; - --md-on-primary: #ffffff; - --md-primary-container: #ffdad6; - --md-on-primary-container: #410002; - - /* Secondary colors */ - --md-secondary: #d32f2f; - --md-on-secondary: #ffffff; - --md-secondary-container: #ffdad5; - --md-on-secondary-container: #410001; - - /* Tertiary colors */ - --md-tertiary: #ff8a65; - --md-on-tertiary: #ffffff; - --md-tertiary-container: #ffdacc; - --md-on-tertiary-container: #410002; - - /* Surface colors (dark theme) */ - --md-surface: #1c1b1f; - --md-on-surface: #e6e1e5; - --md-surface-variant: #49454f; - --md-on-surface-variant: #c9c5d0; - --md-surface-container-lowest: #0f0d13; - --md-surface-container-low: #1d1b20; - --md-surface-container: #211f26; - --md-surface-container-high: #2b2930; - --md-surface-container-highest: #36343b; - - /* Error colors */ - --md-error: #b3261e; - --md-on-error: #ffffff; - --md-error-container: #93000a; - --md-on-error-container: #ffdad5; - - /* Notification colors */ - --md-success: #9bd880; - --md-on-success: #193800; - --md-success-container: #275000; - --md-on-success-container: #b6f397; - - --md-warning: #ffba47; - --md-on-warning: #422b00; - --md-warning-container: #5f3f00; - --md-on-warning-container: #ffddb0; - - --md-info: #b3c5ff; - --md-on-info: #002a77; - --md-info-container: #003ea7; - --md-on-info-container: #dae1ff; - - /* Neutrals */ - --md-outline: #79747e; - --md-outline-variant: #49454f; - --md-inverse-surface: #e6e1e5; - --md-inverse-on-surface: #1c1b1f; - --md-scrim: rgba(0, 0, 0, 0.6); - --md-shadow: rgba(0, 0, 0, 0.15); - - /* Elevation */ - --md-elevation-1: 0px 1px 3px 1px rgba(0, 0, 0, 0.15); - --md-elevation-2: 0px 2px 6px 2px rgba(0, 0, 0, 0.15); - --md-elevation-3: 0px 4px 8px 3px rgba(0, 0, 0, 0.15); - - /* Shapes */ - --md-radius-small: 8px; - --md-radius-medium: 12px; - --md-radius-large: 16px; - - /* Typography */ - --font-primary: "Exo 2", sans-serif; - --font-weight-regular: 400; - --font-weight-medium: 500; - --font-weight-bold: 700; -} - -* { - margin: 0; - padding: 0; - box-sizing: border-box; - font-family: var(--font-primary); -} - -body { - background-color: transparent; - color: var(--md-on-surface); - overflow: hidden; -} +@import url("styleguide.css"); +/* Editor container base */ #editor-container { position: absolute; top: 50%; @@ -97,273 +8,193 @@ body { transform: translate(-50%, -50%); width: 80vw; height: 80vh; - background-color: var(--md-surface); - border-radius: var(--md-radius-medium); - box-shadow: var(--md-elevation-3); display: flex; flex-direction: column; + background-color: var(--md-dark-surface); + border-radius: var(--md-radius-medium); + box-shadow: var(--md-elevation-3); overflow: hidden; } /* Header */ -header { - padding: 16px; - background-color: var(--md-surface-container-high); +#editor-container > header { display: flex; justify-content: space-between; align-items: center; + padding: 10px; + background-color: var(--md-dark-surface-container-high); } - -header h1 { - font-size: 22px; - color: var(--md-primary); +#editor-container > header h1 { + margin: 0; + font-size: var(--md-typescale-title-large-size); + color: var(--md-dark-on-surface); } - -.controls { +#editor-container > header .controls { display: flex; gap: 12px; } - -button { - padding: 8px 16px; - border: none; - border-radius: var(--md-radius-small); - cursor: pointer; +#save-btn, +#close-btn { display: flex; align-items: center; gap: 8px; - font-weight: 500; + padding: 8px 16px; + border: none; + border-radius: var(--md-radius-small); + font-size: var(--md-typescale-body-medium-size); + cursor: pointer; transition: background-color 0.2s; } - #save-btn { - background-color: var(--md-success); - color: var(--md-on-success); + background-color: var(--md-dark-primary); + color: var(--md-dark-on-primary); } - #save-btn:hover { - box-shadow: var(--md-elevation-1); - filter: brightness(1.1); + background-color: var(--md-dark-primary-container); } - #close-btn { - background-color: var(--md-secondary); - color: var(--md-on-secondary); + background-color: var(--md-dark-secondary); + color: var(--md-dark-on-secondary); +} +#close-btn:hover { + background-color: var(--md-dark-secondary-container); } -#close-btn:hover { +/* Main layout */ +#editor-container .content { + flex: 1; + display: flex; + overflow: hidden; +} + +/* Sidebar */ +#editor-container .sidebar { + width: 240px; + padding: 16px; + background-color: var(--md-dark-surface-container); + box-shadow: inset -1px 0 0 0 var(--md-dark-outline-variant); +} +#editor-container .sidebar h2 { + margin-top: 0; + margin-bottom: 12px; + font-size: var(--md-typescale-title-small-size); + color: var(--md-dark-on-surface); +} +#editor-container .file-list { + list-style: none; + margin: 0; + padding: 0; +} +#editor-container .file-list li { + display: flex; + align-items: center; + gap: 8px; + padding: 8px; + border-radius: var(--md-radius-small); + cursor: pointer; + transition: background-color 0.2s; +} +#editor-container .file-list li:hover { + background-color: rgba(255, 255, 255, var(--md-state-hover-dark-opacity)); +} +#editor-container .file-list li.active { + color: var(--md-dark-on-primary); +} + +/* Main content area */ +#editor-container .main-content { + flex: 1; + display: flex; + flex-direction: column; + padding: 16px; + overflow: hidden; +} +#editor-container .search-container { + margin-bottom: 12px; +} +#search-input { + width: 100%; + padding: 8px 12px; + border: 1px solid var(--md-dark-outline-variant); + border-radius: var(--md-radius-small); + background-color: var(--md-dark-surface-container-low); + color: var(--md-dark-on-surface); + font-size: var(--md-typescale-body-medium-size); + transition: border-color 0.2s; +} +#search-input:focus { + border-color: var(--md-dark-primary); + outline: none; +} + +/* Data container and cards */ +#data-container { + flex: 1; + display: grid; + grid-template-columns: repeat(auto-fill, minmax(200px, 1fr)); + gap: 12px; + overflow-y: auto; + padding: 8px; +} +.data-card { + background-color: var(--md-dark-surface-container-low); + border-radius: var(--md-radius-medium); box-shadow: var(--md-elevation-1); - filter: brightness(1.1); + padding: 16px; + display: flex; + flex-direction: column; + justify-content: space-between; + transition: transform 0.1s, box-shadow 0.1s; +} +.data-card:hover { + transform: translateY(-2px); + box-shadow: var(--md-elevation-2); +} +.data-card h3 { + margin: 0 0 8px 0; + font-size: var(--md-typescale-title-medium-size); + color: var(--md-dark-on-surface); +} +.btn-edit { + align-self: flex-end; + padding: 4px 8px; + border: none; + border-radius: var(--md-radius-small); + background-color: var(--md-dark-secondary); + color: var(--md-dark-on-secondary); + cursor: pointer; + transition: background-color 0.2s; +} +.btn-edit:hover { + background-color: var(--md-dark-secondary-container); } /* Floating Action Button */ -.fab { +#add-item-fab { position: fixed; - right: 24px; bottom: 24px; + right: 24px; width: 56px; height: 56px; border-radius: 50%; - background-color: var(--md-primary); - color: var(--md-on-primary); - box-shadow: var(--md-elevation-3); + background-color: var(--md-dark-primary); + color: var(--md-dark-on-primary); display: flex; align-items: center; justify-content: center; cursor: pointer; + box-shadow: var(--md-elevation-3); transition: transform 0.2s; } - -.fab:hover { +#add-item-fab:hover { transform: translateY(-4px); box-shadow: var(--md-elevation-2); } -.fab:active { - transform: translateY(0); -} - -/* Content layout */ -.content { - display: flex; - flex: 1; - overflow: hidden; - height: calc(100vh - 130px); - max-height: calc(100% - 60px); -} - -.sidebar { - background-color: var(--md-surface-container-high); - width: 220px; - padding: 16px; -} - -.sidebar h2 { - font-size: 16px; - font-weight: 500; - margin-bottom: 16px; - color: var(--md-on-surface); -} - -.file-list { - list-style: none; -} - -.file-list li { - padding: 12px; - border-radius: var(--md-radius-small); - cursor: pointer; - display: flex; - gap: 12px; -} - -.file-list li .material-symbols-rounded { - font-size: 20px; -} - -.file-list li:hover { - background-color: var(--md-surface-container-high); - transform: translateY(-2px); - box-shadow: var(--md-elevation-1); -} - -.file-list li.active { - background-color: var(--md-primary); - color: var(--md-on-primary); - transform: translateY(-2px); - box-shadow: var(--md-elevation-1); -} - -.main-content { - flex: 1; - padding: 16px; - overflow-y: auto; - display: flex; - flex-direction: column; - background-color: var(--md-surface-container-low); -} - -/* Search Input */ -.search-container { - margin-bottom: 16px; -} - -.search-wrapper { - position: relative; - width: 100%; -} - -.search-icon { - position: absolute; - left: 12px; - top: 50%; - transform: translateY(-50%); - color: var(--md-outline); - pointer-events: none; -} - -#search-input { - width: 100%; - padding: 12px 12px 12px 40px; - border: 1px solid var(--md-outline-variant); - border-radius: var(--md-radius-small); - background-color: var(--md-surface-container); - color: var(--md-on-surface); -} - -#search-input:focus { - border-color: var(--md-primary); - outline: none; -} - -#data-container { - flex: 1; - overflow-y: auto; - max-height: calc(100vh - 200px); - scrollbar-width: thin; /* For Firefox */ - scrollbar-color: var(--md-outline-variant) transparent; -} - -#data-container::-webkit-scrollbar { - width: 6px; /* Scrollbar width for WebKit browsers */ -} - -#data-container::-webkit-scrollbar-thumb { - background-color: var(--md-outline-variant); /* Thumb color */ - border-radius: 3px; /* Rounded corners for the thumb */ -} - -#data-container::-webkit-scrollbar-track { - background-color: transparent; /* Track color */ -} - -/* Cards for displaying data items */ -.data-card { - background-color: var(--md-surface-container); - border-radius: var(--md-radius-medium); - padding: 16px; - margin-bottom: 12px; - cursor: pointer; - transition: transform 0.2s, box-shadow 0.2s; - overflow: hidden; - word-break: break-word; - position: relative; -} - -.data-card::after { - content: ""; - position: absolute; - top: 0; - left: 0; - width: 100%; - height: 100%; - background-color: var(--md-primary); - opacity: 0; - transition: opacity 0.2s; - pointer-events: none; -} - -.data-card:hover { - background-color: var(--md-surface-container-high); - transform: translateY(-2px); - box-shadow: var(--md-elevation-1); -} - -.data-card:hover::after { - opacity: 0.05; -} - -.data-card h3 { - color: var(--md-primary); - margin-bottom: 12px; - font-weight: 500; -} - -.data-properties { - display: grid; - grid-template-columns: repeat(auto-fill, minmax(150px, 1fr)); - gap: 12px; -} - -.property { - display: flex; - flex-direction: column; -} - -.property-label { - font-size: 12px; - color: var(--md-outline); -} - -.property-value { - font-size: 14px; - overflow: hidden; - text-overflow: ellipsis; - white-space: nowrap; - max-width: 100%; -} - /* Modal */ +#edit-modal.hidden { + display: none; +} #edit-modal { position: absolute; top: 0; @@ -371,304 +202,104 @@ button { width: 100%; height: 100%; display: grid; - place-items: center; /* Perfectly centers the modal */ - background-color: rgba(0, 0, 0, 0.6); - backdrop-filter: blur(2px); + place-items: center; + background-color: var(--md-dark-scrim); z-index: 1000; } - -#edit-modal.hidden { - animation: modal-disappear 0.2s ease-out forwards; - display: none; -} - .modal-content { - background-color: var(--md-surface-container); - border-radius: var(--md-radius-large); - width: 600px; + background-color: var(--md-dark-surface); + border-radius: var(--md-radius-medium); + width: 500px; max-width: 90%; - max-height: 90vh; display: flex; flex-direction: column; - box-shadow: var(--md-elevation-3); - animation: modal-appear 0.2s ease-out; + box-shadow: var(--md-elevation-4); overflow: hidden; } - .modal-header { - padding: 20px; display: flex; justify-content: space-between; align-items: center; - border-bottom: 1px solid var(--md-outline-variant); - background-color: var(--md-surface-container-high); + padding: 12px; + background-color: var(--md-dark-surface-container-high); + border-bottom: 1px solid var(--md-dark-outline-variant); } - .modal-header h2 { - font-size: 20px; - font-weight: 400; - color: var(--md-primary); + margin: 0; + font-size: var(--md-typescale-title-medium-size); + color: var(--md-dark-on-surface); } - #close-modal-btn { background: none; - color: var(--md-on-surface-variant); + border: none; font-size: 24px; - padding: 0; - width: 32px; - height: 32px; + color: var(--md-dark-on-surface-variant); + cursor: pointer; display: flex; - justify-content: center; align-items: center; - border-radius: 50%; + justify-content: center; } - -#close-modal-btn:hover { - color: var(--md-error); - background-color: var(--md-surface-container-high); -} - .modal-body { - padding: 24px; + padding: 16px; overflow-y: auto; - max-height: 60vh; - scrollbar-width: thin; - scrollbar-color: var(--md-outline-variant) transparent; + flex: 1; } - -.modal-body::-webkit-scrollbar { - width: 6px; -} - -.modal-body::-webkit-scrollbar-thumb { - background-color: var(--md-outline-variant); - border-radius: 3px; -} - .modal-footer { - padding: 16px 24px; display: flex; justify-content: flex-end; gap: 12px; - border-top: 1px solid var(--md-outline-variant); - background-color: var(--md-surface-container-high); + padding: 12px; + background-color: var(--md-dark-surface-container-high); + border-top: 1px solid var(--md-dark-outline-variant); } - -#save-item-btn { - background-color: var(--md-success); - color: var(--md-on-success); -} - -#save-item-btn:hover { - box-shadow: var(--md-elevation-1); - filter: brightness(1.1); -} - #cancel-edit-btn { - background-color: var(--md-secondary); - color: var(--md-on-secondary); -} - -#cancel-edit-btn:hover { - box-shadow: var(--md-elevation-1); -} - -/* Form elements */ -.form-group { - margin-bottom: 16px; -} - -.form-group label { - display: block; - margin-bottom: 8px; - color: var(--md-on-surface); - font-size: 14px; -} - -.form-control { - width: 100%; - padding: 12px; - border: 1px solid var(--md-outline-variant); + padding: 8px 16px; + border: none; border-radius: var(--md-radius-small); - background-color: var(--md-surface-container-low); - color: var(--md-on-surface); - transition: border-color 0.2s; - scrollbar-width: thin; - scrollbar-color: var(--md-outline-variant) transparent; -} - -.form-control:focus { - border-color: var(--md-primary); - outline: none; -} - -textarea.form-control { - min-height: 100px; - font-family: monospace; -} - -.form-control::-webkit-scrollbar { - width: 6px; -} - -.form-control::-webkit-scrollbar-thumb { - background-color: var(--md-outline-variant); - border-radius: 3px; -} - -.complex-object { - border: 1px solid var(--md-outline-variant); - border-radius: var(--md-radius-small); - padding: 12px; - margin-top: 8px; - background-color: var(--md-surface-container); -} - -.complex-object-header { - display: flex; - justify-content: space-between; - align-items: center; - margin-bottom: 12px; -} - -.complex-object-title { - font-weight: 500; - color: var(--md-primary); -} - -.add-item-btn { - background-color: var(--md-primary-container); - color: var(--md-on-primary-container); - padding: 4px 8px; - font-size: 12px; - border-radius: var(--md-radius-small); -} - -.add-item-btn:hover { - filter: brightness(1.1); -} - -/* For handling arrays */ -.array-item { - border: 1px solid var(--md-outline-variant); - border-radius: var(--md-radius-small); - padding: 12px; - margin-bottom: 12px; - position: relative; - background-color: var(--md-surface-container-low); -} - -.remove-item-btn { - position: absolute; - top: 8px; - right: 8px; - background-color: var(--md-error-container); - color: var(--md-on-error-container); - width: 24px; - height: 24px; - font-size: 12px; - display: flex; - justify-content: center; - align-items: center; - transition: all 0.2s; -} - -.remove-item-btn:hover { - filter: brightness(1.1); -} - -/* Improved checkbox styling */ -input[type="checkbox"] { - /* Remove default styling */ - appearance: none; - -webkit-appearance: none; - - /* Create custom checkbox */ - width: 20px; - height: 20px; - border: 2px solid var(--md-outline-variant); - border-radius: 4px; - background-color: var(--md-surface-container-low); - position: relative; + background-color: var(--md-dark-secondary); + color: var(--md-dark-on-secondary); cursor: pointer; - vertical-align: middle; - margin-right: 8px; - transition: all 0.2s; + transition: background-color 0.2s; +} +#cancel-edit-btn:hover { + background-color: var(--md-dark-secondary-container); +} +#save-item-btn { + padding: 8px 16px; + border: none; + border-radius: var(--md-radius-small); + background-color: var(--md-dark-primary); + color: var(--md-dark-on-primary); + cursor: pointer; + transition: background-color 0.2s; +} +#save-item-btn:hover { + background-color: var(--md-dark-primary-container); } -/* Checked state */ -input[type="checkbox"]:checked { - background-color: var(--md-primary); - border-color: var(--md-primary); +/* small monospace preview under each card title */ +.data-preview { + margin: 8px 0; + font-family: monospace; + white-space: pre-wrap; + color: var(--md-dark-on-surface-variant); } -/* Checkmark */ -input[type="checkbox"]:checked::after { - content: "✓"; - position: absolute; - top: 50%; - left: 50%; - transform: translate(-50%, -50%); - color: var(--md-on-primary); - font-size: 14px; - opacity: 0; - animation: fadeIn 0.2s forwards; +/* nested object fieldsets styling */ +.nested-fieldset { + border: 1px solid var(--md-dark-outline-variant); + padding: 12px; + margin: 8px 0; + border-radius: var(--md-radius-small); + background-color: var(--md-dark-surface-container-low); +} +.nested-fieldset legend { + font-weight: 600; + padding: 0 6px; + color: var(--md-dark-on-surface); } -/* Hover state */ -input[type="checkbox"]:hover { - border-color: var(--md-primary); -} - -input[type="checkbox"]:active { - transform: scale(0.9); -} - -/* Add this to the form-group for boolean/checkbox fields */ -.form-group.checkbox-group { - display: flex; - align-items: center; -} - -.form-group.checkbox-group label { - margin-bottom: 0; - margin-left: 8px; - order: 2; -} - -.form-group.checkbox-group input { - order: 1; -} - -@keyframes modal-appear { - from { - opacity: 0; - transform: scale(0.9); - } - to { - opacity: 1; - transform: scale(1); - } -} - -@keyframes modal-disappear { - from { - opacity: 1; - transform: scale(1); - } - to { - opacity: 0; - transform: scale(0.9); - } -} - -@keyframes fadeIn { - from { - opacity: 0; - transform: translate(-50%, -60%); - } - to { - opacity: 1; - transform: translate(-50%, -50%); - } +/* indent array entries a bit */ +.array-container > .form-group { + margin-left: 16px; } diff --git a/html/css/styleguide.css b/html/css/styleguide.css new file mode 100644 index 0000000..c1c4369 --- /dev/null +++ b/html/css/styleguide.css @@ -0,0 +1,228 @@ +:root { + /* Light Theme */ + --md-light-primary: #f44336; + --md-light-on-primary: #ffffff; + --md-light-primary-container: #ffdad6; + --md-light-on-primary-container: #410002; + --md-light-secondary: #d32f2f; + --md-light-on-secondary: #ffffff; + --md-light-secondary-container: #ffdad5; + --md-light-on-secondary-container: #410001; + --md-light-tertiary: #ff8a65; + --md-light-on-tertiary: #ffffff; + --md-light-tertiary-container: #ffdacc; + --md-light-on-tertiary-container: #410002; + --md-light-surface: #fffbff; + --md-light-on-surface: #1c1b1f; + --md-light-surface-variant: #e7e0ec; + --md-light-on-surface-variant: #49454f; + --md-light-surface-container-lowest: #ffffff; + --md-light-surface-container-low: #f7f2fa; + --md-light-surface-container: #f3edf7; + --md-light-surface-container-high: #ece8f0; + --md-light-surface-container-highest: #e6e1e5; + --md-light-error: #b3261e; + --md-light-on-error: #ffffff; + --md-light-error-container: #f9dedc; + --md-light-on-error-container: #410e0b; + --md-light-outline: #79747e; + --md-light-outline-variant: #c4c7c5; + --md-light-inverse-surface: #313033; + --md-light-inverse-on-surface: #f4eff4; + --md-light-scrim: rgba(0, 0, 0, 0.4); + --md-light-shadow: rgba(0, 0, 0, 0.08); + --md-light-success: #386a20; + --md-light-on-success: #ffffff; + --md-light-success-container: #b6f397; + --md-light-on-success-container: #0c2000; + --md-light-warning: #825500; + --md-light-on-warning: #ffffff; + --md-light-warning-container: #ffddb0; + --md-light-on-warning-container: #281800; + --md-light-info: #0055d5; + --md-light-on-info: #ffffff; + --md-light-info-container: #dae1ff; + --md-light-on-info-container: #001847; + + /* Dark Theme (your existing variables) */ + --md-dark-primary: #f44336; + --md-dark-on-primary: #ffffff; + --md-dark-primary-container: #ffdad6; + --md-dark-on-primary-container: #410002; + --md-dark-secondary: #d32f2f; + --md-dark-on-secondary: #ffffff; + --md-dark-secondary-container: #ffdad5; + --md-dark-on-secondary-container: #410001; + --md-dark-tertiary: #ff8a65; + --md-dark-on-tertiary: #ffffff; + --md-dark-tertiary-container: #ffdacc; + --md-dark-on-tertiary-container: #410002; + --md-dark-surface: #1c1b1f; + --md-dark-on-surface: #e6e1e5; + --md-dark-surface-container-lowest: #0f0d13; + --md-dark-surface-container-low: #1d1b20; + --md-dark-surface-container: #211f26; + --md-dark-surface-container-high: #2b2930; + --md-dark-surface-container-highest: #36343b; + --md-dark-error: #b3261e; + --md-dark-on-error: #ffffff; + --md-dark-error-container: #93000a; + --md-dark-on-error-container: #ffdad5; + --md-dark-outline: #79747e; + --md-dark-outline-variant: #49454f; + --md-dark-inverse-surface: #e6e1e5; + --md-dark-inverse-on-surface: #1c1b1f; + --md-dark-scrim: rgba(0, 0, 0, 0.6); + --md-dark-shadow: rgba(0, 0, 0, 0.15); + --md-dark-success: #9bd880; + --md-dark-on-success: #193800; + --md-dark-success-container: #275000; + --md-dark-on-success-container: #b6f397; + --md-dark-warning: #ffba47; + --md-dark-on-warning: #422b00; + --md-dark-warning-container: #5f3f00; + --md-dark-on-warning-container: #ffddb0; + --md-dark-info: #b3c5ff; + --md-dark-on-info: #002a77; + --md-dark-info-container: #003ea7; + --md-dark-on-info-container: #dae1ff; + + /* System states */ + --md-state-hover-light-opacity: 0.08; + --md-state-focus-light-opacity: 0.12; + --md-state-pressed-light-opacity: 0.12; + --md-state-dragged-light-opacity: 0.16; + --md-state-hover-dark-opacity: 0.08; + --md-state-focus-dark-opacity: 0.12; + --md-state-pressed-dark-opacity: 0.12; + --md-state-dragged-dark-opacity: 0.16; + + /* Elevation */ + --md-elevation-1: 0px 1px 3px 1px rgba(0, 0, 0, 0.15); + --md-elevation-2: 0px 2px 6px 2px rgba(0, 0, 0, 0.15); + --md-elevation-3: 0px 4px 8px 3px rgba(0, 0, 0, 0.15); + --md-elevation-4: 0px 6px 10px 4px rgba(0, 0, 0, 0.15); + --md-elevation-5: 0px 8px 12px 6px rgba(0, 0, 0, 0.15); + + /* Shapes */ + --md-radius-small: 8px; + --md-radius-medium: 12px; + --md-radius-large: 16px; + --md-radius-extra-large: 28px; + + /* Typography */ + --md-typescale-display-large-size: 57px; + --md-typescale-display-medium-size: 45px; + --md-typescale-display-small-size: 36px; + --md-typescale-headline-large-size: 32px; + --md-typescale-headline-medium-size: 28px; + --md-typescale-headline-small-size: 24px; + --md-typescale-title-large-size: 22px; + --md-typescale-title-medium-size: 16px; + --md-typescale-title-small-size: 14px; + --md-typescale-body-large-size: 16px; + --md-typescale-body-medium-size: 14px; + --md-typescale-body-small-size: 12px; + --md-typescale-label-large-size: 14px; + --md-typescale-label-medium-size: 12px; + --md-typescale-label-small-size: 11px; + + /* Exo 2 Typography */ + /* @import url("https://fonts.googleapis.com/css2?family=Exo+2:wght@300;400;500;600;700&display=swap"); */ + --font-primary: "Exo 2", sans-serif; + --font-weight-regular: 400; + --font-weight-medium: 500; + --font-weight-bold: 700; +} + +/* Default to light theme */ +:root { + --md-primary: var(--md-light-primary); + --md-on-primary: var(--md-light-on-primary); + --md-primary-container: var(--md-light-primary-container); + --md-on-primary-container: var(--md-light-on-primary-container); + --md-secondary: var(--md-light-secondary); + --md-on-secondary: var(--md-light-on-secondary); + --md-secondary-container: var(--md-light-secondary-container); + --md-on-secondary-container: var(--md-light-on-secondary-container); + --md-tertiary: var(--md-light-tertiary); + --md-on-tertiary: var(--md-light-on-tertiary); + --md-tertiary-container: var(--md-light-tertiary-container); + --md-on-tertiary-container: var(--md-light-on-tertiary-container); + --md-surface: var(--md-light-surface); + --md-on-surface: var(--md-light-on-surface); + --md-surface-variant: var(--md-light-surface-variant); + --md-on-surface-variant: var(--md-light-on-surface-variant); + --md-surface-container-lowest: var(--md-light-surface-container-lowest); + --md-surface-container-low: var(--md-light-surface-container-low); + --md-surface-container: var(--md-light-surface-container); + --md-surface-container-high: var(--md-light-surface-container-high); + --md-surface-container-highest: var(--md-light-surface-container-highest); + --md-error: var(--md-light-error); + --md-on-error: var(--md-light-on-error); + --md-error-container: var(--md-light-error-container); + --md-on-error-container: var(--md-light-on-error-container); + --md-outline: var(--md-light-outline); + --md-outline-variant: var(--md-light-outline-variant); + --md-inverse-surface: var(--md-light-inverse-surface); + --md-inverse-on-surface: var(--md-light-inverse-on-surface); + --md-scrim: var(--md-light-scrim); + --md-shadow: var(--md-light-shadow); + --md-success: var(--md-light-success); + --md-on-success: var(--md-light-on-success); + --md-success-container: var(--md-light-success-container); + --md-on-success-container: var(--md-light-on-success-container); + --md-warning: var(--md-light-warning); + --md-on-warning: var(--md-light-on-warning); + --md-warning-container: var(--md-light-warning-container); + --md-on-warning-container: var(--md-light-on-warning-container); + --md-info: var(--md-light-info); + --md-on-info: var(--md-light-on-info); + --md-info-container: var(--md-light-info-container); + --md-on-info-container: var(--md-light-on-info-container); +} + +/* Dark theme class */ +.dark-theme { + --md-primary: var(--md-dark-primary); + --md-on-primary: var(--md-dark-on-primary); + --md-primary-container: var(--md-dark-primary-container); + --md-on-primary-container: var(--md-dark-on-primary-container); + --md-secondary: var(--md-dark-secondary); + --md-on-secondary: var(--md-dark-on-secondary); + --md-secondary-container: var(--md-dark-secondary-container); + --md-on-secondary-container: var(--md-dark-on-secondary-container); + --md-tertiary: var(--md-dark-tertiary); + --md-on-tertiary: var(--md-dark-on-tertiary); + --md-tertiary-container: var(--md-dark-tertiary-container); + --md-on-tertiary-container: var(--md-dark-on-tertiary-container); + --md-surface: var(--md-dark-surface); + --md-on-surface: var(--md-dark-on-surface); + --md-surface-container-lowest: var(--md-dark-surface-container-lowest); + --md-surface-container-low: var(--md-dark-surface-container-low); + --md-surface-container: var(--md-dark-surface-container); + --md-surface-container-high: var(--md-dark-surface-container-high); + --md-surface-container-highest: var(--md-dark-surface-container-highest); + --md-error: var(--md-dark-error); + --md-on-error: var(--md-dark-on-error); + --md-error-container: var(--md-dark-error-container); + --md-on-error-container: var(--md-dark-on-error-container); + --md-outline: var(--md-dark-outline); + --md-outline-variant: var(--md-dark-outline-variant); + --md-inverse-surface: var(--md-dark-inverse-surface); + --md-inverse-on-surface: var(--md-dark-inverse-on-surface); + --md-scrim: var(--md-dark-scrim); + --md-shadow: var(--md-dark-shadow); + --md-success: var(--md-dark-success); + --md-on-success: var(--md-dark-on-success); + --md-success-container: var(--md-dark-success-container); + --md-on-success-container: var(--md-dark-on-success-container); + --md-warning: var(--md-dark-warning); + --md-on-warning: var(--md-dark-on-warning); + --md-warning-container: var(--md-dark-warning-container); + --md-on-warning-container: var(--md-dark-on-warning-container); + --md-info: var(--md-dark-info); + --md-on-info: var(--md-dark-on-info); + --md-info-container: var(--md-dark-info-container); + --md-on-info-container: var(--md-dark-on-info-container); +} diff --git a/html/js/editor.js b/html/js/editor.js index 785c886..fb34337 100644 --- a/html/js/editor.js +++ b/html/js/editor.js @@ -1,579 +1,329 @@ -// Main data store -let configData = {}; -let currentFile = "config"; -let currentEditItem = null; -let editPath = null; +// editor.js – NUI-based config editor with type-aware, recursive editing -// Elements -const dataContainer = document.getElementById("data-container"); -const searchInput = document.getElementById("search-input"); -const saveBtn = document.getElementById("save-btn"); -const closeBtn = document.getElementById("close-btn"); -const editModal = document.getElementById("edit-modal"); -const closeModalBtn = document.getElementById("close-modal-btn"); -const saveItemBtn = document.getElementById("save-item-btn"); -const cancelEditBtn = document.getElementById("cancel-edit-btn"); -const editorForm = document.getElementById("editor-form"); -const fileListItems = document.querySelectorAll(".file-list li"); -const fabButton = document.getElementById("add-item-fab"); -let fileInput; +// State +let allData = {}; +let currentFile = null; -// Initialize -document.addEventListener("DOMContentLoaded", () => { - // Add event listeners - searchInput.addEventListener("input", filterData); - saveBtn.addEventListener("click", saveChanges); - closeBtn.addEventListener("click", closeEditor); - closeModalBtn.addEventListener("click", closeModal); - saveItemBtn.addEventListener("click", saveItemChanges); - cancelEditBtn.addEventListener("click", closeModal); - fabButton.addEventListener("click", addItemModal); - - fileListItems.forEach((item) => { - item.addEventListener("click", () => { - // Update active state - fileListItems.forEach((i) => i.classList.remove("active")); - item.classList.add("active"); - - // Change current file - currentFile = item.dataset.file; - - // Load file data - in a real implementation, this would trigger a fetch to the server - // For now, we'll just use the data we already have - currentFile = item.dataset.file; - renderData(); - }); - }); - - // Listen for NUI messages from FiveM - window.addEventListener("message", function (event) { - const data = event.data; - - if (data.action === "populateData") { - // Expect data.data to be an object with each file's data - configData = data.data; - renderData(); - } - }); -}); - -const templates = { - jobs: { - defaultDuty: false, - offDutyPay: false, - type: "", - label: "", - grades: [ - { - name: "", - isboss: false, - payment: 0, - }, - ], - }, - gangs: { - label: "", - grades: [ - { - name: "", - isboss: false, - level: 0, - }, - ], - }, - items: { - unique: false, - shouldClose: false, - useable: false, - name: "", - label: "", - weight: 0, - type: "", - image: "", - description: "", - ammotype: "", - }, - vehicles: { - brand: "", - price: 0, - name: "", - category: "", - model: "", - type: "", - shop: ["pdm"], - }, - weapons: { - name: "", - label: "", - damageReason: "", - weapontype: "", - ammotype: "", - }, -}; - -function addItemModal() { - const modal = document.getElementById("edit-modal"); - const editorForm = document.getElementById("editor-form"); - - // Clear the form and set it up for adding a new item - editorForm.innerHTML = ""; // Clear any existing content - modal.classList.remove("hidden"); // Show the modal - - // Get the template for the current category - const template = templates[currentFile] || {}; - - // Add fields for the new item based on the template - Object.entries(template).forEach(([key, value]) => { - const formGroup = document.createElement("div"); - formGroup.className = "form-group"; - - const label = document.createElement("label"); - label.textContent = key; - formGroup.appendChild(label); - - if (typeof value === "boolean" || typeof value === "string" || typeof value === "number") { - const field = createInputField({ - key, - value, - onChange: () => {}, // No need to handle it now, we'll read from DOM on save - }); - formGroup.appendChild(field.querySelector("input")); - } else if (Array.isArray(value)) { - const arrayField = createArrayField({ - key, - array: value, - onItemChange: () => {}, // Again, no live tracking needed - onItemRemove: (index) => { - value.splice(index, 1); - addItemModal(); // re-render modal - }, - onItemAdd: () => { - // Clone first item or fallback - if (value.length > 0 && typeof value[0] === "object") { - const newItem = {}; - Object.entries(value[0]).forEach(([k, v]) => { - newItem[k] = typeof v === "number" ? 0 : typeof v === "boolean" ? false : ""; - }); - value.push(newItem); - } else { - value.push(""); - } - addItemModal(); // re-render modal - }, - }); - arrayField.id = `new-item-${key}`; - formGroup.appendChild(arrayField); - } else { - const input = document.createElement("input"); - input.type = "text"; - input.className = "form-control"; - input.value = value; - formGroup.appendChild(input); - } - - editorForm.appendChild(formGroup); - }); - - // Add Save and Cancel buttons - const saveButton = document.getElementById("save-item-btn"); - saveButton.onclick = saveNewItem; - - const cancelButton = document.getElementById("cancel-edit-btn"); - cancelButton.onclick = () => { - modal.classList.add("hidden"); // Hide the modal - }; -} - -function saveNewItem() { - const template = templates[currentFile] || {}; - const newItem = {}; - - // Populate the new item based on the template - Object.entries(template).forEach(([key, value]) => { - if (Array.isArray(value)) { - // Handle arrays - const arrayContainer = document.getElementById(`new-item-${key}`); - const arrayItems = Array.from(arrayContainer.querySelectorAll(".array-item")); - newItem[key] = arrayItems.map((itemGroup) => { - const item = {}; - Array.from(itemGroup.querySelectorAll("input")).forEach((input) => { - const itemKey = input.dataset.itemKey; - item[itemKey] = input.type === "number" ? parseFloat(input.value) || 0 : input.value; - }); - return item; - }); - } else { - // Handle simple values - const input = document.getElementById(`new-item-${key}`); - if (input) { - newItem[key] = input.type === "checkbox" ? input.checked : input.value; - } - } - }); - - // Add the new item to the current category - if (!configData[currentFile]) { - configData[currentFile] = {}; - } - const itemName = document.getElementById("new-item-name").value.trim(); - if (!itemName) { - alert("Please provide a name for the new item."); - return; - } - configData[currentFile][itemName] = newItem; - - // Update the UI +// Select a file and render its data +function selectFile(file, liElement) { + document.querySelectorAll(".file-list li").forEach((li) => li.classList.remove("active")); + liElement.classList.add("active"); + currentFile = file; renderData(); - - // Close the modal - document.getElementById("edit-modal").classList.add("hidden"); } -// Render data in the container +// Render JSON data for the current file function renderData() { - dataContainer.innerHTML = ""; - - if (!configData[currentFile]) { - dataContainer.innerHTML = "

No data available for this file.

"; + if (!currentFile) return; + const data = allData[currentFile]; + if (!data) { + console.error(`No data found for ${currentFile}`); return; } + buildEditor(data); +} - const data = configData[currentFile]; - const searchText = searchInput.value.toLowerCase(); - - // For object-based data (like jobs) +// Build main editor UI from data object +function buildEditor(data) { + const container = document.getElementById("data-container"); + container.innerHTML = ""; Object.entries(data).forEach(([key, value]) => { - // If there's a search term, filter the data - if (searchText && !key.toLowerCase().includes(searchText) && !JSON.stringify(value).toLowerCase().includes(searchText)) { - return; - } - const card = document.createElement("div"); card.className = "data-card"; card.dataset.key = key; + card.innerHTML = `

${key}

`; - // Create card header - const header = document.createElement("h3"); - header.textContent = key; - card.appendChild(header); + // value preview + const preview = document.createElement("div"); + preview.className = "data-preview"; + if (typeof value === "boolean") { + preview.textContent = value; + } else if (Array.isArray(value)) { + preview.textContent = `[${value.map((v) => (typeof v === "object" ? JSON.stringify(v) : v)).join(", ")}]`; + } else if (typeof value === "object" && value !== null) { + preview.textContent = JSON.stringify(value); + } else { + preview.textContent = value; + } + card.appendChild(preview); - // Create properties section - const properties = document.createElement("div"); - properties.className = "data-properties"; + const editBtn = document.createElement("button"); + editBtn.className = "btn-edit"; + editBtn.innerHTML = `edit Edit`; + editBtn.addEventListener("click", () => openEditModal(key, value)); + card.appendChild(editBtn); - // Add main properties (not going too deep) - Object.entries(value).forEach(([propKey, propValue]) => { - // Skip complex objects for the card view - if (typeof propValue === "object" && propValue !== null) { - return; - } - - const property = document.createElement("div"); - property.className = "property"; - - const label = document.createElement("span"); - label.className = "property-label"; - label.textContent = propKey; - - const val = document.createElement("span"); - val.className = "property-value"; - val.textContent = propValue; - - property.appendChild(label); - property.appendChild(val); - properties.appendChild(property); - }); - - card.appendChild(properties); - - // Add click event to edit - card.addEventListener("click", () => { - editItem(key, value); - }); - - dataContainer.appendChild(card); + container.appendChild(card); }); } -// Create form for editing -function createForm(obj) { - editorForm.innerHTML = ""; - - const sortedKeys = Object.keys(obj).sort((a, b) => { - const typeOrder = (value) => { - if (typeof value === "boolean") return 0; - if (typeof value === "string" || typeof value === "number") return 1; - if (Array.isArray(value)) return 2; - return 3; - }; - return typeOrder(obj[a]) - typeOrder(obj[b]); - }); - - sortedKeys.forEach((key) => { - const value = obj[key]; - const formGroup = document.createElement("div"); - formGroup.className = "form-group"; +// Recursive field builder +function buildFormFields(container, data, prefix = "") { + Object.entries(data).forEach(([field, val]) => { + const group = document.createElement("div"); + group.className = "form-group"; const label = document.createElement("label"); - label.htmlFor = `field-${key}`; - label.textContent = key; - formGroup.appendChild(label); + label.htmlFor = "field-" + prefix + field; + label.textContent = field; + group.appendChild(label); - if (typeof value === "boolean" || typeof value === "string" || typeof value === "number") { - const field = createInputField({ - key, - value, - onChange: (val) => updateValue(key, val), - }); - formGroup.appendChild(field.querySelector("input")); - } else if (Array.isArray(value)) { - const arrayField = createArrayField({ - key, - array: value, - onItemChange: (index, itemKey, val) => { - if (itemKey) { - updateArrayObjectValue(key, index, itemKey, val); - } else { - updateArrayValue(key, index, val); - } - }, - onItemRemove: (index) => removeArrayItem(key, index), - onItemAdd: () => addArrayItem(key, value), - }); - formGroup.appendChild(arrayField); - } else { - // Fallback for objects or unknown types - const objectContainer = document.createElement("div"); - objectContainer.className = "complex-object"; + if (typeof val === "boolean") { + const input = document.createElement("input"); + input.type = "checkbox"; + input.checked = val; + input.name = prefix + field; + input.id = "field-" + prefix + field; + input.className = "form-control"; + group.appendChild(input); + } else if (Array.isArray(val)) { + // Array handling: differentiate primitives and objects + val.forEach((item, idx) => { + if (item !== null && typeof item === "object") { + // Nested object in array: use fieldset + const fieldset = document.createElement("fieldset"); + fieldset.className = "nested-fieldset"; + const legend = document.createElement("legend"); + legend.textContent = `${field}[${idx}]`; + fieldset.appendChild(legend); + buildFormFields(fieldset, item, prefix + field + `[${idx}].`); + group.appendChild(fieldset); + } else { + // Primitive in array + const itemGroup = document.createElement("div"); + itemGroup.className = "form-group"; + const itemLabel = document.createElement("label"); + itemLabel.htmlFor = `${prefix}${field}[${idx}]`; + itemLabel.textContent = `${field}[${idx}]`; - const header = document.createElement("div"); - header.className = "complex-object-header"; - objectContainer.appendChild(header); + const input = document.createElement("input"); + input.type = typeof item === "boolean" ? "checkbox" : "text"; + if (typeof item === "boolean") input.checked = item; + else input.value = item; + input.name = `${prefix}${field}[${idx}]`; + input.id = `field-${prefix}${field}[${idx}]`; + input.className = "form-control"; - const textarea = document.createElement("textarea"); - textarea.className = "form-control"; - textarea.value = JSON.stringify(value, null, 2); - textarea.dataset.key = key; - - textarea.addEventListener("input", (e) => { - try { - const parsed = JSON.parse(e.target.value); - updateValue(key, parsed); - textarea.style.borderColor = "#3d3d3d"; - } catch (err) { - textarea.style.borderColor = "#ff4d4d"; + itemGroup.append(itemLabel, input); + group.appendChild(itemGroup); } }); - - objectContainer.appendChild(textarea); - formGroup.appendChild(objectContainer); - } - - editorForm.appendChild(formGroup); - }); -} - -// UTILS - -function createInputField({ key, value, onChange }) { - const formGroup = document.createElement("div"); - formGroup.className = "form-group"; - - const label = document.createElement("label"); - label.textContent = key; - formGroup.appendChild(label); - - const input = document.createElement("input"); - - if (typeof value === "boolean") { - input.type = "checkbox"; - input.checked = value; - } else if (typeof value === "number") { - input.type = "number"; - input.value = value; - } else { - input.type = "text"; - input.value = value; - } - - input.className = "form-control"; - - input.addEventListener("input", (e) => { - let newValue; - if (input.type === "checkbox") { - newValue = input.checked; - } else if (input.type === "number") { - newValue = parseFloat(e.target.value) || 0; - } else { - newValue = e.target.value; - } - - onChange(newValue); - }); - - formGroup.appendChild(input); - return formGroup; -} - -function createArrayField({ key, array, onItemChange, onItemRemove, onItemAdd }) { - const container = document.createElement("div"); - container.className = "complex-object"; - - const header = document.createElement("div"); - header.className = "complex-object-header"; - - const addBtn = document.createElement("button"); - addBtn.className = "add-item-btn"; - addBtn.textContent = "Add Item"; - addBtn.addEventListener("click", (e) => { - e.preventDefault(); - onItemAdd(); - }); - - header.appendChild(addBtn); - container.appendChild(header); - - array.forEach((item, index) => { - const arrayItem = document.createElement("div"); - arrayItem.className = "array-item"; - - // Remove button - const removeBtn = document.createElement("button"); - removeBtn.className = "remove-item-btn"; - removeBtn.textContent = "x"; - removeBtn.addEventListener("click", (e) => { - e.preventDefault(); - e.stopPropagation(); - onItemRemove(index); - }); - arrayItem.appendChild(removeBtn); - - if (typeof item === "object" && item !== null) { - Object.entries(item).forEach(([itemKey, itemValue]) => { - const field = createInputField({ - key: itemKey, - value: itemValue, - onChange: (val) => onItemChange(index, itemKey, val), - }); - arrayItem.appendChild(field); - }); + } else if (val !== null && typeof val === "object") { + // Nested object + const fieldset = document.createElement("fieldset"); + fieldset.className = "nested-fieldset"; + const legend = document.createElement("legend"); + legend.textContent = field; + fieldset.appendChild(legend); + buildFormFields(fieldset, val, prefix + field + "."); + group.appendChild(fieldset); } else { const input = document.createElement("input"); + input.type = "text"; + input.value = val; + input.name = prefix + field; + input.id = "field-" + prefix + field; input.className = "form-control"; - input.value = item; - input.addEventListener("input", (e) => { - onItemChange(index, null, e.target.value); - }); - arrayItem.appendChild(input); + group.appendChild(input); } - - container.appendChild(arrayItem); + container.appendChild(group); }); - - return container; } -// Filter data based on search input -function filterData() { - renderData(); -} +// Open modal to edit a key +function openEditModal(key, value) { + const modal = document.getElementById("edit-modal"); + modal.querySelector("h2").textContent = `Edit '${key}'`; + const formContainer = document.getElementById("editor-form"); + formContainer.innerHTML = ""; -// Edit an item -function editItem(key, value) { - currentEditItem = { key, value: JSON.parse(JSON.stringify(value)) }; - editPath = null; // Reset path for root level editing + const form = document.createElement("form"); + form.id = "editor-form-inner"; - // Create form for editing - createForm(value); + // Determine if value is object or primitive + const isObj = value !== null && typeof value === "object"; + const editableData = isObj ? value : { [key]: value }; + const prefix = isObj ? "" : key + "."; - // Show the modal - editModal.classList.remove("hidden"); -} + buildFormFields(form, editableData, prefix); + formContainer.appendChild(form); -function setDeepValue(obj, pathArray, value) { - const lastKey = pathArray.pop(); - const target = pathArray.reduce((acc, key) => acc[key], obj); - target[lastKey] = value; -} + // Save handler + document.getElementById("save-item-btn").onclick = async (e) => { + e.preventDefault(); + const formElem = document.getElementById("editor-form-inner"); + const updated = { ...allData[currentFile] }; + const resultValue = {}; -function updateValue(key, value) { - const path = editPath ? editPath.split(".").concat(key) : [key]; - setDeepValue(currentEditItem.value, path, value); -} + Array.from(formElem.elements).forEach((el) => { + if (!el.name) return; + const name = el.name; + let val; + if (el.type === "checkbox") val = el.checked; + else if (el.tagName === "TEXTAREA") { + try { + val = JSON.parse(el.value); + } catch { + val = el.value; + } + } else val = el.value; -function updateArrayValue(arrayKey, index, value) { - const path = editPath ? editPath.split(".").concat([arrayKey, index]) : [arrayKey, index]; - setDeepValue(currentEditItem.value, path, value); -} - -function updateArrayObjectValue(arrayKey, index, itemKey, value) { - const path = editPath ? editPath.split(".").concat([arrayKey, index, itemKey]) : [arrayKey, index, itemKey]; - setDeepValue(currentEditItem.value, path, value); -} - -function updateArray(arrayKey, action, index = null, newItem = null) { - const path = editPath ? editPath.split(".").concat([arrayKey]) : [arrayKey]; - const target = path.reduce((acc, key) => acc[key], currentEditItem.value); - - if (action === "add") { - target.push(newItem); - } else if (action === "remove" && index !== null) { - target.splice(index, 1); - } - - createForm(currentEditItem.value); -} - -function addArrayItem(arrayKey, array) { - let newItem; - if (array.length > 0 && typeof array[0] === "object" && array[0] !== null) { - newItem = {}; - Object.keys(array[0]).forEach((key) => { - const sample = array[0][key]; - newItem[key] = typeof sample === "number" ? 0 : typeof sample === "boolean" ? false : ""; + // assign into nested structure + const path = name.split(/\.|\[|\]/).filter((p) => p); + let target = resultValue; + path.forEach((part, idx) => { + const last = idx === path.length - 1; + if (last) target[part] = val; + else { + target[part] = target[part] || {}; + target = target[part]; + } + }); }); - } else { - newItem = typeof array[0] === "number" ? 0 : ""; + + updated[key] = Array.isArray(value) || (value !== null && typeof value === "object") ? resultValue : resultValue[key]; + await saveConfig(updated); + }; + + setupModalClose(); + modal.classList.remove("hidden"); +} + +// Open modal to add a new key/value +function openNewItemModal() { + const modal = document.getElementById("edit-modal"); + modal.querySelector("h2").textContent = "Add New Item"; + const formContainer = document.getElementById("editor-form"); + formContainer.innerHTML = ""; + + const form = document.createElement("form"); + form.id = "editor-form-inner"; + + // Key input + const keyGroup = document.createElement("div"); + keyGroup.className = "form-group"; + const keyLabel = document.createElement("label"); + keyLabel.htmlFor = "new-key"; + keyLabel.textContent = "Key"; + const keyInput = document.createElement("input"); + keyInput.type = "text"; + keyInput.id = "new-key"; + keyInput.name = "key"; + keyInput.className = "form-control"; + keyGroup.append(keyLabel, keyInput); + form.appendChild(keyGroup); + + // Value input + const valGroup = document.createElement("div"); + valGroup.className = "form-group"; + const valLabel = document.createElement("label"); + valLabel.htmlFor = "new-value"; + valLabel.textContent = "Value"; + const valInput = document.createElement("input"); + valInput.type = "text"; + valInput.id = "new-value"; + valInput.name = "value"; + valInput.className = "form-control"; + valGroup.append(valLabel, valInput); + form.appendChild(valGroup); + + formContainer.appendChild(form); + + // Add handler + document.getElementById("save-item-btn").onclick = async (e) => { + e.preventDefault(); + const key = document.getElementById("new-key").value.trim(); + const raw = document.getElementById("new-value").value; + if (!key) { + console.error("Key cannot be empty."); + return; + } + let val; + if (raw.toLowerCase() === "true" || raw.toLowerCase() === "false") val = raw.toLowerCase() === "true"; + else if ((raw.startsWith("{") && raw.endsWith("}")) || (raw.startsWith("[") && raw.endsWith("]"))) { + try { + val = JSON.parse(raw); + } catch { + val = raw; + } + } else val = raw; + const updated = { ...allData[currentFile] }; + updated[key] = val; + await saveConfig(updated); + }; + + setupModalClose(); + modal.classList.remove("hidden"); +} + +// Save full config via NUI +async function saveConfig(configData) { + try { + const res = await fetch(`https://${GetParentResourceName()}/saveConfig`, { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ file: currentFile, data: configData }), + }); + const result = await res.json(); + if (result === "ok") { + allData[currentFile] = configData; + console.log("Saved successfully!"); + closeModal(); + renderData(); + } else throw new Error(result); + } catch (err) { + console.error("Save failed.", err); } - updateArray(arrayKey, "add", null, newItem); } -function removeArrayItem(arrayKey, index) { - updateArray(arrayKey, "remove", index); +// Modal close handlers +function setupModalClose() { + document.getElementById("close-modal-btn").onclick = closeModal; + document.getElementById("cancel-edit-btn").onclick = (e) => { + e.preventDefault(); + closeModal(); + }; } -// Save changes to the edited item -function saveItemChanges() { - configData[currentFile][currentEditItem.key] = currentEditItem.value; - closeModal(); - renderData(); -} - -// Close the edit modal function closeModal() { - editModal.classList.add("hidden"); - currentEditItem = null; - editPath = null; + document.getElementById("edit-modal").classList.add("hidden"); } -// Save all changes to the server -function saveChanges() { - // Send data back to the client - fetch(`https://${GetParentResourceName()}/saveConfig`, { - method: "POST", - headers: { - "Content-Type": "application/json; charset=UTF-8", - }, - body: JSON.stringify({ - file: currentFile, - data: configData[currentFile], - }), - }).catch((err) => console.error("Error:", err)); +// Search/filter cards +function setupSearch() { + document.getElementById("search-input").addEventListener("input", (e) => { + const term = e.target.value.toLowerCase(); + document.querySelectorAll(".data-card").forEach((card) => { + card.style.display = card.dataset.key.toLowerCase().includes(term) ? "" : "none"; + }); + }); } -// Close the editor -function closeEditor() { - fetch(`https://${GetParentResourceName()}/closeEditor`, { - method: "POST", - }).catch((err) => console.error("Error:", err)); +// Keyboard shortcuts +function setupKeyboard() { + window.addEventListener("keydown", (e) => { + if (e.key === "Escape") closeModal(); + }); } + +// Initialize UI +window.addEventListener("DOMContentLoaded", () => { + document.querySelectorAll(".file-list li").forEach((li) => { + const file = li.dataset.file; + li.addEventListener("click", () => selectFile(file, li)); + }); + document.getElementById("add-item-fab").onclick = openNewItemModal; + setupSearch(); + setupKeyboard(); +}); + +// Listen for NUI messages and auto-select first file +window.addEventListener("message", (event) => { + if (event.data.action === "populateData") { + allData = event.data.data; + if (!currentFile) { + const firstLi = document.querySelector(".file-list li"); + if (firstLi) firstLi.click(); + } + } +}); diff --git a/shared/loader.lua b/shared/loader.lua index 4582430..4a2d110 100644 --- a/shared/loader.lua +++ b/shared/loader.lua @@ -2,12 +2,10 @@ local resourceName = GetCurrentResourceName() -- Load Config -if IsDuplicityVersion() then - local configData = LoadResourceFile(resourceName, 'shared/config.json') - if not configData then return print('^1[QBCore] Failed to Load Config JSON File') end - QBCore.Config = json.decode(configData) or {} - print('^2[QBCore] Loaded Config.json') -end +local Config = LoadResourceFile(resourceName, 'shared/config.json') +if not Config then return print('^1[QBCore] Failed to Load Config JSON File') end +QBCore.Config = json.decode(Config) or {} +print('^2[QBCore] Loaded Config.json') -- Load Vehicles