Locus / index.html
VSPAN's picture
Update index.html
f70eda6 verified
<!DOCTYPE html>
<html lang="ru">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0, user-scalable=no">
<title>Локус</title>
<link rel="icon" type="Logo/png" href="Logo/favicon-96x96.png" sizes="96x96" />
<link rel="icon" type="Logo/svg+xml" href="Logo/favicon.svg" />
<link rel="shortcut icon" href="Logo/favicon.ico" />
<link rel="apple-touch-icon" sizes="180x180" href="Logo/apple-touch-icon.png" />
<meta name="apple-mobile-web-app-title" content="Локус" />
<link rel="manifest" href="data:application/manifest+json;base64,ewogICJuYW1lIjogItCb0L7QutGD0YEiLAogICJzaG9ydF9uYW1lIjogItCb0L7QutGD0YEiLAogICJpY29ucyI6IFsKICAgIHsKICAgICAgInNyYyI6ICIvd2ViLWFwcC1tYW5pZmVzdC0xOTJ4MTkyLnBuZyIsCiAgICAgICJzaXplcyI6ICIxOTJ4MTkyIiwKICAgICAgInR5cGUiOiAiaW1hZ2UvcG5nIiwKICAgICAgInB1cnBvc2UiOiAibWFza2FibGUiCiAgICB9LAogICAgewogICAgICAic3JjIjogIi93ZWItYXBwLW1hbmlmZXN0LTUxMng1MTIucG5nIiwKICAgICAgInNpemVzIjogIjUxMng1MTIiLAogICAgICAidHlwZSI6ICJpbWFnZS9wbmciLAogICAgICAicHVycG9zZSI6ICJtYXNrYWJsZSIKICAgIH0KICBdLAogICJ0aGVtZV9jb2xvciI6ICIjZTZkZGM1IiwKICAiYmFja2dyb3VuZF9jb2xvciI6ICIjZjRlY2Q4IiwKICAiZGlzcGxheSI6ICJzdGFuZGFsb25lIgp9" />
<style>
:root {
--bg-main: #f9f0e1; --bg-panel: #ece2ca; --bg-element: #f0e6d0; --accent-primary: #767f48; --accent-secondary: #909662; --accent-tertiary: #cfbbbb; --text-primary: #474e28; --text-secondary: #5b643c; --danger-primary: #a52a2a; --success-primary: #388E3C; --warning-primary: #f5a623; --info-primary: #4a90e2; --border-color: rgba(0, 0, 0, 0.1); --shadow-soft: 0 4px 12px rgba(0, 0, 0, 0.1); --shadow-medium: 0 8px 24px rgba(0, 0, 0, 0.15); --font-main: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif; --transition-cubic: all 0.35s cubic-bezier(0.4, 0, 0.2, 1);
--global-radius: 12px;
--radius-soft: 8px;
--slot-min-width: 160px; --slot-height: 160px; --slot-image-max-height: 70px;
}
::-webkit-scrollbar { width: 10px; height: 10px; } ::-webkit-scrollbar-track { background: var(--bg-element); } ::-webkit-scrollbar-thumb { background-color: var(--accent-secondary); border-radius: 10px; border: 2px solid var(--bg-element); } ::-webkit-scrollbar-thumb:hover { background-color: var(--accent-primary); }
.theme-dark { --bg-main: #2c3e50; --bg-panel: #34495e; --bg-element: #283747; --accent-primary: #1abc9c; --accent-secondary: #16a085; --accent-tertiary: #95a5a6; --text-primary: #ecf0f1; --text-secondary: #bdc3c7; --danger-primary: #e74c3c; --success-primary: #2ecc71; --warning-primary: #f39c12; --info-primary: #3498db; --border-color: rgba(255, 255, 255, 0.1); }
.theme-dark .explorer-controls, .theme-dark .favorites-controls, .theme-dark .character-header { background: #283747; } .theme-dark .contents-item-thumb { background: #2c3e50; } .theme-dark .folder-header:hover { background: #3c546b; } .theme-dark .note-card { background: #34495e; } .theme-dark .file-grid-item { background: #283747; } .theme-dark .item-slot { background: #2c3e50; } .theme-dark #sidebar-resizer:hover { background-color: var(--accent-primary); } .theme-dark .sets-container, .theme-dark .set-card, .theme-dark .sets-management-box { background: var(--bg-element); } .theme-dark .set-card h3 { color: var(--accent-primary); }
.theme-sepia { --bg-main: #f4ecd8; --bg-panel: #e6ddc5; --bg-element: #ebe2cd; --accent-primary: #8c7853; --accent-secondary: #a6916f; --accent-tertiary: #c8bca6; --text-primary: #5c4d32; --text-secondary: #74634f; --danger-primary: #c0392b; --success-primary: #27ae60; --warning-primary: #d35400; --info-primary: #2980b9; --border-color: rgba(0, 0, 0, 0.1); }
.theme-sepia .explorer-controls, .theme-sepia .favorites-controls, .theme-sepia .character-header { background: #fbf5e8; } .theme-sepia .contents-item-thumb { background: #ebe2cd; } .theme-sepia .folder-header:hover { background: #f0e6d0; } .theme-sepia .note-card { background: #fbf5e8; } .theme-sepia .file-grid-item { background: #ebe2cd; } .theme-sepia .item-slot { background: #f4ecd8; } .theme-sepia #sidebar-resizer:hover { background-color: var(--accent-primary); } .theme-sepia .sets-container, .theme-sepia .set-card, .theme-sepia .sets-management-box { background: var(--bg-element); } .theme-sepia .set-card h3 { color: var(--accent-primary); }
* { margin: 0; padding: 0; box-sizing: border-box; font-family: var(--font-main); }
html, body { height: 100%; background: var(--bg-main); color: var(--text-primary); font-size: 16px; overflow: hidden; transition: background-color 0.4s, color 0.4s; }
img { image-rendering: -webkit-optimize-contrast; image-rendering: crisp-edges; image-rendering: pixelated; }
body.modal-open, body.touch-drag-active { overflow: hidden; }
body.is-resizing { cursor: col-resize; user-select: none; }
#app-container { display: flex; flex-direction: column; height: 100vh; }
@keyframes fadeInUp { from { opacity: 0; transform: translateY(20px); } to { opacity: 1; transform: translateY(0); } } .fade-in-up { animation: fadeInUp 0.5s var(--transition-cubic) both; }
.styled-button { background: var(--accent-secondary); color: var(--bg-main); border: 1px solid var(--border-color); border-radius: var(--radius-soft); padding: 10px 20px; cursor: pointer; font-size: 14px; font-weight: 600; display: inline-flex; align-items: center; gap: 8px; transition: var(--transition-cubic); white-space: nowrap; box-shadow: var(--shadow-soft); }
.styled-button:hover:not(:disabled) { background: var(--accent-primary); border-color: var(--accent-primary); color: #fff; transform: translateY(-2px); box-shadow: 0 6px 12px rgba(106, 153, 78, 0.3); }
.styled-button.primary { background: var(--accent-primary); border-color: var(--accent-primary); color: #fff; }
.styled-button:disabled { opacity: 0.5; cursor: not-allowed; transform: none; background: var(--bg-element); box-shadow: none; }
.app-header { background: var(--bg-panel); border-bottom: 1px solid var(--border-color); padding: 12px 20px; display: flex; gap: 12px; align-items: center; z-index: 100; box-shadow: var(--shadow-soft); }
.app-main-view { position: relative; display: grid; grid-template-columns: 320px 1fr; flex-grow: 1; overflow: hidden; }
.sidebar-panel { display: flex; flex-direction: column; background: var(--bg-panel); border-right: 1px solid var(--border-color); overflow: hidden; height: 100%; }
#sidebar-resizer { position: absolute; top: 0; bottom: 0; left: 318px; width: 5px; cursor: col-resize; z-index: 100; transition: background-color 0.2s; } #sidebar-resizer:hover { background-color: rgba(118, 127, 72, 0.5); }
.explorer-controls { padding: 12px; border-bottom: 1px solid var(--border-color); background: var(--bg-element); }
.search-input { width: 100%; padding: 10px 12px; background: var(--bg-main); border: 1px solid var(--border-color); border-radius: var(--radius-soft); color: var(--text-primary); font-size: 14px; transition: var(--transition-cubic); } .search-input:focus { outline: none; border-color: var(--accent-primary); box-shadow: 0 0 0 3px rgba(106, 153, 78, 0.2); }
.explorer-tabs { display: flex; background: var(--bg-element); border-bottom: 1px solid var(--border-color); } .explorer-tab-btn { flex: 1; background: none; border: none; padding: 10px; font-size: 14px; font-weight: 600; cursor: pointer; color: var(--text-secondary); border-bottom: 3px solid transparent; transition: var(--transition-cubic); } .explorer-tab-btn:hover { background: var(--bg-main); } .explorer-tab-btn.active { color: var(--accent-primary); border-bottom-color: var(--accent-primary); background: var(--bg-panel); }
.sidebar-content-wrapper { position: relative; flex: 1; overflow: hidden; }
.sidebar-content { position: absolute; inset: 0; overflow-y: auto; opacity: 1; visibility: visible; transition: opacity 0.3s, visibility 0.3s; display: flex; flex-direction: column;} .sidebar-content:not(.active) { opacity: 0; visibility: hidden; }
.sidebar-content > .explorer-placeholder { padding: 10px; flex-grow: 1; } .favorites-controls { display: flex; gap: 10px; padding: 10px; border-bottom: 1px solid var(--border-color); background: var(--bg-element); flex-shrink: 0;} #favoritesContent .file-grid { padding: 10px; flex-grow: 1; overflow-y: auto; }
.file-grid-item .tags-container { padding: 0 8px 4px; display: flex; flex-wrap: wrap; gap: 4px; justify-content: center; } .item-tag { background: var(--accent-secondary); color: var(--bg-main); padding: 2px 6px; font-size: 10px; font-weight: 600; border-radius: var(--radius-soft); }
#favoritesContent .file-grid-item .item-actions-overlay { position: absolute; top: 4px; right: 4px; display: flex; gap: 4px; opacity: 0; transition: var(--transition-cubic); z-index: 2; } #favoritesContent .file-grid-item:hover .item-actions-overlay { opacity: 1; } #favoritesContent .item-actions-overlay .icon-button { background: rgba(255, 255, 255, 0.7); backdrop-filter: blur(2px); border-radius: 50%; width: 24px; height: 24px; font-size: 14px; color: var(--text-secondary); } #favoritesContent .item-actions-overlay .icon-button:hover { background: var(--accent-primary); color: #fff; transform: scale(1.1); } #favoritesContent .item-actions-overlay .unfavorite-btn:hover { background: var(--danger-primary); }
.file-grid-item:hover { transform: translateY(-3px); box-shadow: var(--shadow-medium); border-color: var(--accent-primary); }
.explorer-tree, .folder-content-wrapper .explorer-tree { list-style: none; padding-left: 0; } .explorer-folder { margin: 4px 0; } .folder-header { display: flex; align-items: center; padding: 8px; border-radius: var(--radius-soft); cursor: pointer; user-select: none; transition: background-color var(--transition-cubic); } .folder-header:hover { background: var(--bg-main); } .folder-toggle-icon { width: 18px; height: 18px; display: flex; align-items: center; justify-content: center; margin-right: 8px; transition: transform 0.2s; flex-shrink: 0; } .explorer-folder.open > .folder-header .folder-toggle-icon { transform: rotate(90deg); } .folder-name-text { flex: 1; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; font-weight: 500; } .folder-content-wrapper { margin-left: 18px; display: none; padding-left: 10px; border-left: 1px solid var(--border-color); } .explorer-folder.open > .folder-content-wrapper { display: block; }
.file-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(90px, 1fr)); gap: 12px; padding: 10px; }
.file-grid-item { position: relative; border-radius: var(--global-radius); overflow: hidden; background: var(--bg-element); cursor: pointer; transition: transform var(--transition-cubic), box-shadow var(--transition-cubic); aspect-ratio: 1; display: flex; flex-direction: column; border: 1px solid var(--border-color); user-select: none; -webkit-user-select: none;} .file-thumb-wrapper { flex: 1; width: 100%; display: flex; align-items: center; justify-content: center; padding: 5px; overflow: hidden; } .file-thumb-wrapper img { max-width: 100%; max-height: 100%; object-fit: contain; } .file-name-label { font-size: 12px; padding: 4px 8px; text-align: center; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; background: rgba(0,0,0,0.03); border-top: 1px solid var(--border-color); color: var(--text-secondary); }
.explorer-placeholder { display: flex; flex-direction: column; align-items: center; justify-content: center; height: 100%; padding: 20px; text-align: center; color: var(--text-secondary); } .file-grid > .explorer-placeholder { grid-column: 1 / -1; }
.main-content-panel { display: flex; flex-direction: column; }
.character-header { position: relative; text-align: center; border-bottom: 1px solid var(--border-color); z-index: 2; background: var(--bg-element); transition: all 0.4s cubic-bezier(0.4, 0, 0.2, 1); }
#characterNameWrapper { padding: 20px 60px 20px 24px; transition: var(--transition-cubic); }
.character-header.collapsed #characterNameWrapper { max-height: 0; padding-top: 0; padding-bottom: 0; opacity: 0; overflow: hidden; }
#characterName { font-size: 2.5rem; font-weight: 700; background: linear-gradient(45deg, var(--accent-primary), #8fbc8f); -webkit-background-clip: text; background-clip: text; color: transparent; cursor: pointer; outline: none; border: 2px solid transparent; letter-spacing: 1px; padding: 5px 10px; border-radius: var(--radius-soft); transition: var(--transition-cubic); }
#characterName:hover { text-shadow: 0 0 15px var(--accent-secondary); }
#characterName[contenteditable="true"] { color: var(--text-primary); background: var(--bg-main); -webkit-text-fill-color: initial; border-color: var(--accent-primary); box-shadow: 0 0 0 3px rgba(106, 153, 78, 0.2); }
#headerCollapseToggle { display: flex; position: absolute; top: 16px; right: 16px; transform: none; background: var(--bg-main); border: 1px solid var(--border-color); width: 32px; height: 32px; border-radius: 50%; cursor: pointer; align-items: center; justify-content: center; transition: var(--transition-cubic); box-shadow: var(--shadow-soft); }
#headerCollapseToggle:hover { background: var(--accent-secondary); color: white; border-color: var(--accent-primary); }
#headerCollapseToggle .icon-chevron { transition: transform 0.3s; }
.character-header.collapsed #headerCollapseToggle .icon-chevron { transform: rotate(180deg); }
.content-tabs { position: relative; display: flex; border-bottom: 1px solid var(--border-color); padding: 0 24px; z-index: 1; flex-shrink: 0; background: var(--bg-panel); }
.tab-button { background: none; border: none; color: var(--text-secondary); padding: 12px 18px; font-size: 1rem; cursor: pointer; transition: var(--transition-cubic); border-bottom: 3px solid transparent; z-index: 2; } .tab-button:hover { color: var(--text-primary); } .tab-button.active { color: var(--accent-primary); font-weight: 600; }
#tabMarker { position: absolute; bottom: -1px; height: 3px; background-color: var(--accent-primary); border-radius: 3px; transition: all 0.4s cubic-bezier(0.4, 0, 0.2, 1); z-index: 1; }
.content-area-wrapper { flex-grow: 1; overflow: hidden; position: relative; }
.tab-content { position: absolute; inset: 0; overflow-y: auto; padding: 24px; opacity: 1; visibility: visible; transition: opacity 0.4s, transform 0.4s; transform: translateY(0); } .tab-content:not(.active) { opacity: 0; visibility: hidden; transform: translateY(10px); }
.slot-section-header { font-size: 1.1rem; letter-spacing: 0.5px; font-weight: 600; margin-bottom: 16px; padding-bottom: 10px; border-bottom: 1px solid var(--border-color); color: var(--accent-primary); display: flex; align-items: center; gap: 10px; }
.slots-grid-layout { display: grid; grid-template-columns: repeat(auto-fill, minmax(var(--slot-min-width), 1fr)); gap: 18px; }
.item-slot { background: var(--bg-element); box-shadow: var(--shadow-soft); border: 1px solid var(--border-color); position: relative; border-radius: var(--global-radius); animation: fadeInUp 0.5s var(--transition-cubic) both; display: flex; flex-direction: column; overflow: hidden; height: var(--slot-height); cursor: pointer; user-select: none; -webkit-user-select: none; } .item-slot.occupied { cursor: grab; } .item-slot:hover { border-color: var(--accent-primary); }
.slot-header-bar { background: rgba(0,0,0,0.02); border-bottom: 1px solid var(--border-color); border-top-left-radius: var(--global-radius); border-top-right-radius: var(--global-radius); padding: 8px 12px; flex-shrink: 0; display: flex; align-items: center; gap: 4px; } .slot-title-text { font-weight: 600; } .slot-content-area { flex-grow: 1; display: grid; place-items: center; padding: 5px; position: relative; } .slot-item-wrapper { text-align: center; } .slot-item-image { max-width: 90%; max-height: var(--slot-image-max-height); object-fit: contain; transition: transform 0.2s; } .slot-item-image:hover { transform: scale(1.1); } .slot-item-caption-wrapper { width: 100%; text-align: center; margin-top: 8px; } .slot-item-caption { font-size: 14px; font-weight: 500; }
.item-container-icon { background: var(--accent-secondary); color: var(--bg-main); border-radius: 50%; box-shadow: var(--shadow-soft); font-size: 16px; font-weight: 700; display: flex; align-items: center; justify-content: center; z-index: 3; pointer-events: none; width: 28px; height: 28px; flex-shrink: 0; } .item-container-icon span { font-size: 12px; margin-left: 1px; }
.slot-item-quantity-badge { position: absolute; bottom: 8px; right: 8px; background: var(--accent-primary); color: var(--bg-main); font-size: 12px; font-weight: 700; width: 24px; height: 24px; border-radius: 50%; display: flex; align-items: center; justify-content: center; border: 2px solid var(--bg-element); box-shadow: var(--shadow-soft); pointer-events: none; z-index: 5; }
.item-slot.dragover-active, .file-grid-item.dragover-active { background: var(--accent-secondary) !important; border-color: var(--accent-primary) !important; border-style: solid !important; transform: scale(1.05); }
.drop-zone { position: fixed; bottom: -100px; left: 50%; transform: translateX(-50%); background: var(--danger-primary); color: white; padding: 15px 30px; border-radius: 20px; box-shadow: var(--shadow-medium); z-index: 9999; transition: var(--transition-cubic); font-weight: 600; pointer-events: none; } .drop-zone.visible { transform: translateX(-50%) translateY(-120px); pointer-events: all; } .drop-zone.dragover { transform: translateX(-50%) translateY(-120px) scale(1.1); background: #d32f2f; }
.notes-container { display: flex; flex-direction: column; gap: 20px; } .note-card { background: var(--bg-element); border: 1px solid var(--border-color); border-radius: var(--global-radius); box-shadow: var(--shadow-soft); display: flex; flex-direction: column; } .note-content { padding: 16px; flex-grow: 1; } .note-content h3 { font-size: 1.2rem; color: var(--accent-primary); border-bottom: 1px solid var(--border-color); padding-bottom: 8px; margin-bottom: 12px; } .note-content p { line-height: 1.6; margin-bottom: 1em; color: var(--text-secondary); } .note-content strong { color: var(--text-primary); font-weight: 600; } .note-content em { color: var(--accent-primary); font-style: italic; } .note-content ul { padding-left: 20px; } .note-content li { margin-bottom: 0.5em; } .note-editor { width: 100%; height: 200px; background: var(--bg-main); border: 1px solid var(--border-color); color: var(--text-primary); padding: 12px; font-size: 1rem; line-height: 1.6; border-radius: var(--radius-soft); resize: vertical; } .note-actions { display: flex; justify-content: flex-end; gap: 10px; padding: 12px; background: var(--bg-panel); border-top: 1px solid var(--border-color); border-bottom-left-radius: var(--global-radius); border-bottom-right-radius: var(--global-radius); }
.spacer { flex-grow: 1; } .icon-button { background: transparent; border: none; color: var(--text-secondary); padding: 4px; border-radius: 50%; width: 28px; height: 28px; display: flex; align-items: center; justify-content: center; font-size: 18px; cursor: pointer; transition: var(--transition-cubic); } .icon-button:hover:not(:disabled) { background: var(--bg-main); color: var(--text-primary); } .icon-button:disabled { opacity: 0.4; cursor: not-allowed; } .editable-title { cursor: pointer; transition: color var(--transition-cubic); padding: 2px 4px; border-radius: var(--radius-soft); flex-grow: 1; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; display: flex; align-items: center; gap: 8px; } .editable-title .edit-icon { opacity: 0; transition: var(--transition-cubic); } .editable-title:hover .edit-icon { opacity: 0.7; } .constructor-add-section-btn { margin-top: 10px; } .title-edit-input { background: #fff; border: 1px solid var(--accent-primary); color: var(--text-primary); border-radius: var(--radius-soft); padding: 2px 4px; font: inherit; width: 100%; }
.modal-backdrop { position: fixed; inset: 0; background: rgba(0, 0, 0, 0.5); backdrop-filter: blur(5px); display: flex; align-items: center; justify-content: center; z-index: 1100; opacity: 0; visibility: hidden; transition: var(--transition-cubic); } .modal-backdrop.visible { opacity: 1; visibility: visible; } .modal-window { background: var(--bg-panel); color: var(--text-primary); border-radius: var(--global-radius); width: 90%; max-width: 500px; max-height: 90vh; box-shadow: var(--shadow-medium); transform: scale(0.95) translateY(20px); transition: var(--transition-cubic); display: flex; flex-direction: column; } .modal-backdrop.visible .modal-window { transform: scale(1) translateY(0); } .modal-header { padding: 16px 20px; border-bottom: 1px solid var(--border-color); display: flex; justify-content: space-between; align-items: center; } .modal-title-text { font-size: 18px; font-weight: 600; } .modal-close-button { background: none; border: none; font-size: 24px; color: var(--text-secondary); cursor: pointer; transition: var(--transition-cubic); } .modal-close-button:hover { color: var(--text-primary); transform: rotate(90deg); } .modal-body-content { padding: 20px; overflow-y: auto; } .modal-footer-bar { padding: 16px 20px; display: flex; justify-content: flex-end; gap: 12px; border-top: 1px solid var(--border-color); background: var(--bg-main); border-bottom-left-radius: var(--global-radius); border-bottom-right-radius: var(--global-radius); } .form-group { margin-bottom: 16px; } .form-group label { display: block; font-size: 13px; color: var(--text-secondary); margin-bottom: 6px; } .form-control { width: 100%; padding: 10px 12px; background: var(--bg-main); border: 1px solid var(--border-color); border-radius: var(--radius-soft); color: var(--text-primary); font-size: 14px; } textarea.form-control { min-height: 100px; resize: vertical; }
.form-check { display: flex; align-items: center; gap: 8px; margin-top: 16px; user-select: none; } .form-check label { margin-bottom: 0; cursor: pointer; } .form-check input { width: auto; cursor: pointer; }
#imageViewer { position: fixed; inset: 0; background: rgba(0,0,0,0.85); backdrop-filter: blur(10px); display: flex; align-items: center; justify-content: center; z-index: 1200; cursor: default; opacity: 0; visibility: hidden; transition: var(--transition-cubic); user-select: none; -webkit-user-select: none; }
#imageViewer.visible { opacity: 1; visibility: visible; }
#viewerImage { max-width: 95vw; max-height: 95vh; transition: transform 0.2s cubic-bezier(0.4, 0, 0.2, 1); cursor: grab; transform-origin: center center; }
#viewerImage:active { cursor: grabbing; }
#imageViewer.is-panning #viewerImage { transition: none; }
#imageViewerClose { position: absolute; top: 20px; right: 20px; font-size: 2.5rem; color: white; cursor: pointer; transition: var(--transition-cubic); line-height: 1; z-index: 1201; }
#imageViewerClose:hover { transform: scale(1.1) rotate(90deg); color: var(--accent-primary); }
.image-viewer-actions { position: absolute; bottom: 20px; padding: 10px; background: rgba(0, 0, 0, 0.4); backdrop-filter: blur(5px); border-radius: var(--global-radius); display: flex; gap: 12px; z-index: 1201; transition: var(--transition-cubic); transform: translateY(150%); }
#imageViewer.visible .image-viewer-actions { transform: translateY(0); transition-delay: 0.2s; }
.image-viewer-actions .styled-button { background: rgba(255, 255, 255, 0.9); color: #333; font-size: 14px; padding: 8px 16px; border-radius: var(--radius-soft); }
.image-viewer-actions .styled-button:hover:not(:disabled) { background: var(--accent-primary); color: #fff; }
.image-viewer-actions .styled-button.is-favorite { background: #f9a825; color: #fff; }
#slotTooltip { position: fixed; background: var(--bg-panel); border: 1px solid var(--border-color); border-radius: var(--radius-soft); padding: 12px; z-index: 1300; max-width: 300px; box-shadow: var(--shadow-medium); pointer-events: none; opacity: 0; transition: opacity 0.1s; } #tooltipTitle { font-weight: 600; margin-bottom: 8px; color: var(--accent-primary); font-size: 16px; word-break: break-word; } #tooltipDesc { font-size: 14px; line-height: 1.5; color: var(--text-secondary); white-space: pre-wrap; word-break: break-word; } #tooltipDesc:not(:empty) { border-top: 1px solid var(--border-color); padding-top: 8px; margin-top: 8px; }
#toastContainer { position: fixed; bottom: 20px; right: 20px; z-index: 9999; display: flex; flex-direction: column; gap: 12px; } .toast-message { padding: 12px 18px; background: var(--bg-panel); border-radius: var(--radius-soft); max-width: 350px; box-shadow: var(--shadow-medium); font-size: 14px; animation: toastInAnimation 0.4s, toastOutAnimation 0.4s 3.6s forwards; border-left: 4px solid var(--accent-primary); } .toast-message.error { border-left-color: var(--danger-primary); } .toast-message.success { border-left-color: var(--success-primary); } @keyframes toastInAnimation { from { transform: translateX(120%); opacity: 0; } to { transform: translateX(0); opacity: 1; } } @keyframes toastOutAnimation { from { opacity: 1; } to { opacity: 0; visibility: hidden; } }
.drag-ghost { position: absolute; left: -9999px; background: var(--accent-primary); color: #f7ecdb; padding: 8px 12px; border-radius: var(--radius-soft); font-size: 14px; display: flex; align-items: center; gap: 8px; box-shadow: var(--shadow-medium); font-weight: 600; z-index: 9999; pointer-events: none; }
#contentsModal .modal-window { max-width: 800px; } .contents-list { display: flex; flex-direction: column; gap: 12px; } .contents-item { background: var(--bg-element); border-radius: var(--global-radius); padding: 12px; display: grid; grid-template-columns: 60px 1fr auto; gap: 15px; align-items: center; border: 1px solid var(--border-color); transition: var(--transition-cubic); } .contents-item.is-container { border: 2px dashed var(--accent-primary); } .contents-item.dragover-active { background: var(--accent-secondary) !important; border-color: var(--accent-primary) !important; transform: scale(1.02); } .contents-item.is-child-item { margin-left: 30px; border-left: 4px solid var(--accent-primary); } .contents-item-thumb { width: 60px; height: 60px; border-radius: var(--radius-soft); background: var(--bg-main); display: flex; align-items: center; justify-content: center; overflow: hidden; border: 1px solid var(--border-color); } .contents-item-thumb img { max-width: 100%; max-height: 100%; object-fit: contain; cursor: pointer; } .contents-item-details { display: flex; flex-direction: column; gap: 5px; min-width: 0; overflow: hidden; } .contents-item-name { font-weight: 600; font-size: 16px; overflow-wrap: break-word; hyphens: auto; } .contents-item-desc { font-size: 13px; color: var(--text-secondary); white-space: pre-wrap; overflow-wrap: break-word; hyphens: auto; } .contents-item-actions { display: flex; gap: 8px; } .contents-item-actions .icon-button { background: var(--bg-main); box-shadow: var(--shadow-soft); } .contents-item-actions .icon-button.is-container-toggle { color: var(--accent-primary); border: 2px solid var(--accent-primary); } .contents-item-actions .icon-button.danger:hover { background: var(--danger-primary); color: #fff; } .contents-item-actions .icon-button.favorite-btn.is-favorite { color: #f9a825; transform: scale(1.1); }
.sets-container { display: flex; flex-direction: column; gap: 12px; }
.set-card { background: var(--bg-element); border: 1px solid var(--border-color); border-radius: var(--global-radius); box-shadow: var(--shadow-soft); display: flex; align-items: center; gap: 16px; padding: 12px 16px; transition: var(--transition-cubic); }
.set-card:hover { border-color: var(--accent-primary); transform: translateY(-2px); box-shadow: var(--shadow-medium); } .set-card h3 { font-size: 1.1rem; color: var(--accent-primary); font-weight: 600; margin: 0; flex-grow: 1; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; } .set-card-info { font-size: 0.9rem; color: var(--text-secondary); flex-shrink: 0; } .set-card-actions { display: flex; gap: 8px; flex-shrink: 0; } .set-card-actions .styled-button { padding: 6px 12px; font-size: 12px; border-radius: var(--radius-soft); }
#saveStatusIndicator { width: 14px; height: 14px; border-radius: 50%; transition: all 0.4s; flex-shrink: 0; cursor: help; } #saveStatusIndicator.status-saved { background-color: var(--success-primary); } #saveStatusIndicator.status-unsaved { background-color: var(--warning-primary); } #saveStatusIndicator.status-saving { background-color: var(--info-primary); animation: pulse-blue 1.5s infinite; } #saveStatusIndicator.status-error { background-color: var(--danger-primary); }
@keyframes pulse-blue { 0% { box-shadow: 0 0 0 0 rgba(74, 144, 226, 0.5); } 70% { box-shadow: 0 0 0 8px rgba(74, 144, 226, 0); } 100% { box-shadow: 0 0 0 0 rgba(74, 144, 226, 0); } }
.sets-management-box { margin-top: 24px; padding: 20px; background: var(--bg-element); border: 1px solid var(--border-color); border-radius: var(--global-radius); display: flex; flex-direction: column; gap: 16px; } .sets-management-box h3 { margin: 0 0 4px 0; color: var(--accent-primary); border-bottom: 1px solid var(--border-color); padding-bottom: 8px; } .sets-management-actions { display: flex; gap: 12px; justify-content: center; }
.reset-options-list { display: flex; flex-direction: column; gap: 12px; }
.tour-backdrop { position: fixed; inset: 0; z-index: 2000; opacity: 0; visibility: hidden; transition: all 0.4s; } .tour-backdrop.active { opacity: 1; visibility: visible; } .tour-highlight-box { position: absolute; border-radius: var(--radius-soft); box-shadow: 0 0 0 9999px rgba(0,0,0,0.65); transition: all 0.4s cubic-bezier(0.4, 0, 0.2, 1); pointer-events: none; } .tour-tooltip { position: absolute; z-index: 2001; background: var(--bg-panel); color: var(--text-primary); padding: 16px; border-radius: var(--global-radius); width: 320px; max-width: 90vw; box-shadow: var(--shadow-medium); border: 1px solid var(--border-color); display: flex; flex-direction: column; gap: 12px; transition: all 0.4s; } .tour-tooltip-header { display: flex; justify-content: space-between; align-items: center; border-bottom: 1px solid var(--border-color); padding-bottom: 8px; } .tour-tooltip-title { font-size: 1.1rem; font-weight: 600; color: var(--accent-primary); } .tour-tooltip-step-counter { font-size: 0.8rem; font-weight: 500; color: var(--text-secondary); } .tour-tooltip-text { font-size: 0.95rem; line-height: 1.6; } .tour-tooltip-actions { display: flex; justify-content: space-between; align-items: center; margin-top: 8px; } .tour-tooltip-actions .styled-button { padding: 6px 14px; font-size: 13px; }
.tour-close-btn { background: none; border: none; font-size: 20px; cursor: pointer; color: var(--text-secondary); transition: var(--transition-cubic); line-height: 1; padding: 0 4px; } .tour-close-btn:hover { color: var(--text-primary); transform: scale(1.1); }
#orientation-lock { display: none; position: fixed; inset: 0; background: var(--bg-main); z-index: 9999; flex-direction: column; justify-content: center; align-items: center; text-align: center; }
#orientation-lock-icon { font-size: 5rem; animation: rotate-phone 2s infinite ease-in-out; }
#orientation-lock-text { font-size: 1.2rem; margin-top: 1.5rem; font-weight: 600; color: var(--text-primary); }
@keyframes rotate-phone { 0%, 100% { transform: rotate(0deg); } 50% { transform: rotate(-90deg); } }
@media (orientation: portrait) {
#app-container { display: none !important; }
#orientation-lock { display: flex !important; }
}
@media (max-width: 768px) {
.app-main-view { grid-template-columns: 240px 1fr; }
#sidebar-resizer { left: 238px; }
#characterName { font-size: 1.8rem; }
}
</style>
</head>
<body class="">
<div id="app-container">
<header class="app-header">
<button id="selectFolderBtn" class="styled-button primary"><span>📁</span> Выбрать ресурсы</button>
<button id="importBtn" class="styled-button" disabled><span>📥</span> Импорт</button>
<select id="exportSelector" class="form-control" style="width: auto;" disabled><option value="default" selected disabled>📤 Экспорт</option><option value="json">Экспорт в .json</option><option value="text_all">Экспорт в .txt</option></select>
<div class="spacer"></div>
<div id="saveStatusIndicator"></div>
<button id="helpBtn" class="icon-button" title="Начать интерактивный тур">?</button>
<select id="themeSelector" class="form-control" title="Выбор темы оформления"><option value="light">Светлая</option><option value="dark">Темная</option><option value="sepia">Сепия</option><option value="system">Системная</option></select>
<button id="factoryResetBtn" class="styled-button"><span>🔄</span> Сброс</button>
</header>
<main class="app-main-view">
<aside class="sidebar-panel">
<div class="explorer-tabs"><button class="explorer-tab-btn active" data-sidebar-tab="resources">Ресурсы</button><button class="explorer-tab-btn" data-sidebar-tab="favorites">Избранное</button></div>
<div class="sidebar-content-wrapper">
<div id="resourcesContent" class="sidebar-content active"><div class="explorer-controls"><input type="search" id="resourcesSearchInput" class="search-input" placeholder="🔍 Поиск по ресурсам..." disabled></div><div class="file-grid" id="resourcesGrid"><div class="explorer-placeholder"><div style="font-size: 4rem; margin-bottom: 1rem;">🗂️</div><div>Начните с выбора папки с ресурсами.</div></div></div></div>
<div id="favoritesContent" class="sidebar-content"><div class="favorites-controls"><input type="search" id="favoritesSearchInput" class="search-input" placeholder="🔍 Поиск в избранном..."><select id="favoritesSort" class="form-control" style="width: 150px;"><option value="newest">Сначала новые</option><option value="name_asc">По имени (А-Я)</option><option value="name_desc">По имени (Я-А)</option></select></div><div class="file-grid" id="favoritesGrid"><div class="explorer-placeholder"><div style="font-size: 4rem; margin-bottom: 1rem;"></div><div>Здесь будут избранные предметы.</div></div></div></div>
</div>
</aside>
<div id="sidebar-resizer"></div>
<section class="main-content-panel"><div class="character-header" id="characterHeader"><div id="characterNameWrapper"><h1 id="characterName" title="Нажмите, чтобы изменить имя персонажа">Безымянный Герой</h1></div><button id="headerCollapseToggle" title="Свернуть/развернуть заголовок"><span class="icon-chevron"></span></button></div><div class="content-tabs"><button class="tab-button active" data-tab="equipment">Экипировка</button><button class="tab-button" data-tab="sets">Наборы</button><button class="tab-button" data-tab="notes">Заметки</button><div id="tabMarker"></div></div><div class="content-area-wrapper"><div id="equipment" class="tab-content active"></div><div id="sets" class="tab-content"></div><div id="notes" class="tab-content"></div></div></section>
</main>
<div id="dropZone" class="drop-zone">🗑️ Перетащите сюда для удаления</div>
</div>
<div id="orientation-lock">
<div id="orientation-lock-icon">📱</div>
<p id="orientation-lock-text">Пожалуйста, переверните устройство</p>
</div>
<input type="file" id="folderInput" webkitdirectory multiple hidden><input type="file" id="importInput" accept=".json" hidden><input type="file" id="importSetsInput" accept=".json" hidden>
<div class="modal-backdrop" id="contentsModal"><div class="modal-window"><header class="modal-header"><h2 class="modal-title-text" id="contentsModalTitle">Содержимое Слота</h2><button class="modal-close-button">×</button></header><div class="modal-body-content"><div class="contents-list" id="contentsModalList"></div></div></div></div>
<div class="modal-backdrop" id="editModal"><div class="modal-window"><header class="modal-header"><h2 class="modal-title-text">Редактирование Предмета</h2><button class="modal-close-button">×</button></header><div class="modal-body-content"><div class="form-group"><label>Оригинальное имя</label><input type="text" class="form-control" id="modalOriginalName" readonly></div><div class="form-group"><label for="modalName">Название</label><input type="text" class="form-control" id="modalName" placeholder="Например, 'Кольчужный шлем'"></div><div class="form-group"><label for="modalDesc">Описание</label><textarea class="form-control" id="modalDesc" placeholder="Например, 'Прочный шлем...'"></textarea></div><div class="form-group" id="quantityFormGroup"><label for="modalQuantity">Количество</label><input type="number" class="form-control" id="modalQuantity" min="1" value="1"></div><div class="form-group"><label for="modalTags">Теги (через запятую)</label><input type="text" class="form-control" id="modalTags" placeholder="броня, квестовый, легендарный"></div><div class="form-group form-check"><input type="checkbox" id="modalIsContainer"><label for="modalIsContainer">Сделать контейнером</label></div></div><footer class="modal-footer-bar"><button id="modalCancel" class="styled-button">Отмена</button><button id="modalSave" class="styled-button primary">Сохранить</button></footer></div></div>
<div class="modal-backdrop" id="setNameModal"><div class="modal-window"><header class="modal-header"><h2 class="modal-title-text" id="setNameModalTitle">Название набора</h2><button class="modal-close-button">×</button></header><div class="modal-body-content"><div class="form-group"><label for="setNameInput">Введите название для нового набора</label><input type="text" class="form-control" id="setNameInput" placeholder="Например, 'Боевой набор'"></div></div><footer class="modal-footer-bar"><button id="setNameCancel" class="styled-button">Отмена</button><button id="setNameSave" class="styled-button primary">Сохранить</button></footer></div></div>
<div class="modal-backdrop" id="resetModal"><div class="modal-window"><header class="modal-header"><h2 class="modal-title-text">Выборочный сброс</h2><button class="modal-close-button">×</button></header><div class="modal-body-content"><p>Выберите данные для сброса. <b>Наборы экипировки не будут затронуты.</b></p><div class="reset-options-list" style="margin-top: 16px;"><div class="form-check"><input type="checkbox" id="resetLayout" checked><label for="resetLayout">Раскладка</label></div><div class="form-check"><input type="checkbox" id="resetCharName" checked><label for="resetCharName">Имя персонажа</label></div><div class="form-check"><input type="checkbox" id="resetNotes" checked><label for="resetNotes">Заметки</label></div><div class="form-check"><input type="checkbox" id="resetFavorites"><label for="resetFavorites">Избранное</label></div></div></div><footer class="modal-footer-bar"><button id="resetCancel" class="styled-button">Отмена</button><button id="resetConfirm" class="styled-button primary">Выполнить сброс</button></footer></div></div>
<div id="imageViewer"><div id="imageViewerClose">×</div><img src="" alt="Увеличенное изображение" id="viewerImage"><div class="image-viewer-actions"><button id="viewerResetBtn" class="styled-button"><span>🔄</span> Сброс</button><button id="viewerEditBtn" class="styled-button"><span>✏️</span> Редактировать</button><button id="viewerFavoriteBtn" class="styled-button"><span></span> В избранное</button></div></div>
<div id="slotTooltip"><div id="tooltipTitle"></div><div id="tooltipDesc"></div></div>
<div id="toastContainer"></div>
<script>
'use strict';
function createDOMElement(tag, className = '', innerHTML = '') { const el = document.createElement(tag); if (className) el.className = className; if (innerHTML) el.innerHTML = innerHTML; return el; }
function humanizeFilename(filename) { return filename.split('.').slice(0, -1).join('.').replace(/[-_]/g, ' ').replace(/\b\w/g, char => char.toUpperCase()); }
function showToast(message, type = 'info') { const cont = document.getElementById('toastContainer'); const toast = createDOMElement('div', `toast-message ${type}`, message); cont.appendChild(toast); setTimeout(() => toast.remove(), 4000); }
function downloadFile(content, fileName, contentType) { const a = createDOMElement('a'); a.href = URL.createObjectURL(new Blob([content], { type: contentType })); a.download = fileName; document.body.appendChild(a); a.click(); document.body.removeChild(a); URL.revokeObjectURL(a.href); }
function createDragGhost(file, name) { const ghostEl = createDOMElement('div', 'drag-ghost'); ghostEl.innerHTML = `<span><img src="${file.url}" width="24" height="24" style="border-radius: 4px; object-fit: contain;"></span><span>${name}</span>`; return ghostEl; }
const BASE_LAYOUT_CONFIG = () => JSON.parse(JSON.stringify([
{ id: `section_default_char`, title: "Экипировка Персонажа", slots: [
{ id: "default_head", title: "Голова", items: [] },
{ id: "default_amulet", title: "Ожерелье", items: [] },
{ id: "default_shoulders", title: "Плечи", items: [] },
{ id: "default_chest", title: "Нагрудник", items: [] },
{ id: "default_cloak", title: "Плащ", items: [] },
{ id: "default_gloves", title: "Перчатки", items: [] },
{ id: "default_belt", title: "Пояс", items: [] },
{ id: "default_legs", title: "Поножи", items: [] },
{ id: "default_boots", title: "Ботинки", items: [] },
] },
{ id: `section_default_acc`, title: "Бижутерия и Оружие", slots: [
{ id: "default_ring_1", title: "Кольцо 1", items: [] },
{ id: "default_ring_2", title: "Кольцо 2", items: [] },
{ id: "default_mainhand", title: "Основная рука", items: [] },
{ id: "default_offhand", title: "Вторая рука", items: [] },
] },
{ id: `section_default_inv`, title: "Инвентарь", slots: Array.from({ length: 10 }, (_, i) => ({ id: `default_inventory_${i+1}`, title: `Сумка ${i + 1}`, items: [] })) }
]));
function createBlueprintFromItem(item) {
const blueprint = { ...item };
delete blueprint.id;
if (blueprint.isContainer && Array.isArray(blueprint.contents)) {
blueprint.contents = blueprint.contents.map(createBlueprintFromItem);
}
return blueprint;
}
function createItemFromBlueprint(blueprint) {
const newItem = { ...blueprint };
newItem.id = `item_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`;
if (newItem.isContainer && Array.isArray(newItem.contents)) {
newItem.contents = newItem.contents.map(createItemFromBlueprint);
}
return newItem;
}
const AppState = { rootDirectoryName: null, files: new Map(), fileHierarchy: {}, layout: [], characterName: "Безымянный Герой", notes: [], favorites: [], equipmentSets: [], isLoaded: false, isDirty: false, saveTimeout: null, activeSearchTerm: '', activeFavoritesSearchTerm: '', activeFavoritesSort: 'newest', dragging: { isTouch: false, draggedItemId: null, path: null, isFavoriteDrag: false, favoriteItemData: null, targetSlotId: null, isDeletionDrop: false, longPressTimeout: null, ghost: null, lastHoveredElement: null, touchStartX: 0, touchStartY: 0 }, editModal: { visible: false, targetItem: null }, contentsModal: { visible: false, targetSlotId: null }, setNameModal: { visible: false, mode: 'create', setId: null, callback: null }, resetModal: { visible: false },
imageViewer: {
active: false, context: null, scale: 1, posX: 0, posY: 0, isPanning: false, panStartX: 0, panStartY: 0
}
};
const DBPersistence = { db: null, DB_NAME: "PhoenixConstructorDB_v12", STORE_NAME: "FileStore", async init() { return new Promise((resolve, reject) => { if (this.db) return resolve(); const request = indexedDB.open(this.DB_NAME, 1); request.onerror = e => reject("Ошибка IndexedDB: " + e.target.errorCode); request.onsuccess = e => { this.db = e.target.result; resolve(); }; request.onupgradeneeded = e => { const db = e.target.result; if (!db.objectStoreNames.contains(this.STORE_NAME)) db.createObjectStore(this.STORE_NAME, { keyPath: "path" }); }; }); }, async saveFiles(files) { if (!this.db) await this.init(); const transaction = this.db.transaction([this.STORE_NAME], "readwrite"); const store = transaction.objectStore(this.STORE_NAME); await new Promise(resolve => store.clear().onsuccess = resolve); for (const file of files) store.add({ path: file.webkitRelativePath, name: file.name, blob: new Blob([file], { type: file.type }) }); return new Promise(resolve => transaction.oncomplete = resolve); }, async loadFiles() { if (!this.db) await this.init(); return new Promise((resolve) => { const request = this.db.transaction([this.STORE_NAME], "readonly").objectStore(this.STORE_NAME).getAll(); request.onsuccess = () => resolve(request.result.map(f => ({ path: f.path, name: f.name, url: URL.createObjectURL(f.blob) }))); request.onerror = () => resolve([]); }); } };
function requestSave() { clearTimeout(AppState.saveTimeout); if (!AppState.isDirty) { AppState.isDirty = true; updateSaveStatus('unsaved'); } AppState.saveTimeout = setTimeout(saveFullState, 1500); }
function saveFullState() { updateSaveStatus('saving'); try { localStorage.setItem('phoenixLayout', JSON.stringify(AppState.layout)); localStorage.setItem('phoenixCharName', AppState.characterName); localStorage.setItem('phoenixNotes', JSON.stringify(AppState.notes)); localStorage.setItem('phoenixFavorites', JSON.stringify(AppState.favorites)); localStorage.setItem('phoenixEquipmentSets', JSON.stringify(AppState.equipmentSets)); AppState.isDirty = false; setTimeout(() => updateSaveStatus('saved'), 300); } catch (e) { console.error("Ошибка сохранения состояния:", e); showToast("Не удалось сохранить сессию.", 'error'); updateSaveStatus('error'); } }
function loadStateFromLocalStorage() { const layout = localStorage.getItem('phoenixLayout'); const charName = localStorage.getItem('phoenixCharName'); const notes = localStorage.getItem('phoenixNotes'); const favorites = localStorage.getItem('phoenixFavorites'); const sets = localStorage.getItem('phoenixEquipmentSets'); const theme = localStorage.getItem('phoenixTheme') || 'system'; const sidebarWidth = localStorage.getItem('phoenixSidebarWidth') || '320px'; AppState.layout = layout ? JSON.parse(layout) : BASE_LAYOUT_CONFIG(); AppState.characterName = charName || "Безымянный Герой"; AppState.notes = notes ? JSON.parse(notes) : []; AppState.favorites = favorites ? JSON.parse(favorites) : []; AppState.equipmentSets = sets ? JSON.parse(sets) : []; applyTheme(theme, true); document.querySelector('.app-main-view').style.gridTemplateColumns = `${sidebarWidth} 1fr`; document.getElementById('sidebar-resizer').style.left = `${parseInt(sidebarWidth) - 2.5}px`; }
function findSlot(slotId) { for (const section of AppState.layout) { if (section.slots) { const slot = section.slots.find(s => s.id === slotId); if (slot) return slot; } } return null; }
function findItemById(itemId) { for (const section of AppState.layout) { for (const slot of section.slots) { for (const item of slot.items) { if (item.id === itemId) return { item, slot, container: null }; if (item.isContainer && item.contents) { for (const contentItem of item.contents) { if (contentItem.id === itemId) return { item: contentItem, slot, container: item }; } } } } } return null; }
function createItem(path) {
const file = AppState.files.get(path);
if (!file) return null;
return {
id: `item_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`,
path: path,
name: humanizeFilename(file.name),
desc: '',
tags: '',
quantity: 1,
isContainer: false,
contents: [],
};
}
const TourManager={currentStep:-1,isActive:false,elements:{},
steps:[
{selector:'#selectFolderBtn',title:'Шаг 1: Ресурсы',text:'Добро пожаловать! Начните с нажатия этой кнопки, чтобы выбрать папку с вашими изображениями (иконками предметов). Это основа всего.',position:'bottom'},
{selector:'#resourcesGrid',title:'Шаг 2: Предметы',text:'Ваши ресурсы появятся здесь. Перетащите любой предмет из этой панели в слот для экипировки справа. Кликните по предмету, чтобы увеличить его.',position:'right'},
{selector:'.item-slot',title:'Шаг 3: Слоты',text:'Это слот для предмета. Перетаскивайте предметы сюда. Кликните по занятому слоту, чтобы отредактировать предмет или управлять содержимым контейнера.',position:'bottom'},
{selector:'#characterName',title:'Шаг 4: Имя персонажа',text:'Это имя вашего персонажа. Нажмите на него, чтобы изменить.',position:'bottom'},
{selector:'[data-sidebar-tab="favorites"]',title:'Шаг 5: Избранное',text:'На этой вкладке будут показаны предметы, которые вы отметите звездочкой. Это удобно для быстрого доступа.',position:'right', onBefore:() => document.querySelector('[data-sidebar-tab="favorites"]').click()},
{selector:'[data-sidebar-tab="resources"]',title:'Возврат к ресурсам',text:'Вернемся на вкладку с ресурсами.',position:'right', onBefore:() => document.querySelector('[data-sidebar-tab="resources"]').click()},
{selector:'[data-tab="sets"]',title:'Шаг 6: Наборы',text:'А теперь самое главное. Здесь вы можете сохранять и загружать полные конфигурации вашей экипировки. Давайте посмотрим.',position:'bottom', onBefore:() => document.querySelector('[data-tab="sets"]').click()},
{selector:'.sets-container + .styled-button',title:'Шаг 7: Сохранение набора',text:'Когда вы соберете нужную экипировку на первой вкладке, нажмите эту кнопку, чтобы сохранить ее как именованный набор.',position:'top'},
{selector:'[data-tab="notes"]',title:'Шаг 8: Заметки',text:'Этот раздел предназначен для ваших записей, мыслей или описания персонажа. Поддерживается простое форматирование.',position:'bottom', onBefore:() => document.querySelector('[data-tab="notes"]').click()},
{selector:'[data-tab="equipment"]',title:'Возврат к экипировке',text:'Теперь вернемся к основной вкладке.',position:'bottom', onBefore:() => document.querySelector('[data-tab="equipment"]').click()},
{selector:'#importBtn',title:'Шаг 9: Импорт и Экспорт',text:'Вы можете импортировать и экспортировать полные данные персонажа (включая экипировку, заметки и наборы) в формате JSON.',position:'bottom'},
{selector:'#themeSelector',title:'Шаг 10: Внешний вид',text:'Настройте внешний вид приложения по своему вкусу. Все изменения сохраняются автоматически.',position:'bottom'},
{selector:'#factoryResetBtn',title:'Шаг 11: Сброс',text:'Эта кнопка позволяет выборочно сбросить данные: раскладку слотов, имя, заметки. Наборы при этом не затрагиваются.',position:'bottom'},
{isFinal:true,title:'Все готово!',text:'Вы изучили основы. Теперь вы готовы создавать свои конфигурации. Этот тур можно запустить снова в любой момент, нажав на иконку [?] вверху.'}
],
init(){if(!localStorage.getItem('phoenixOnboardingCompleted'))this.start();},
start(){if(this.isActive)return;this.isActive=true;this.createDOMElements();this.currentStep=-1;this.nextStep();},
end(){if(!this.isActive)return;this.isActive=false;this.elements.backdrop.classList.remove('active');setTimeout(()=>{this.elements.backdrop.remove();this.elements={};},400);localStorage.setItem('phoenixOnboardingCompleted','true');},
restart(){this.end();setTimeout(()=>this.start(), 100);},
createDOMElements(){
this.elements.backdrop=createDOMElement('div','tour-backdrop');
this.elements.highlightBox=createDOMElement('div','tour-highlight-box');
this.elements.tooltip=createDOMElement('div','tour-tooltip',`<div class="tour-tooltip-header"><div class="tour-tooltip-title"></div><div class="tour-tooltip-step-counter"></div><button class="tour-close-btn" title="Пропустить">×</button></div><div class="tour-tooltip-text"></div><div class="tour-tooltip-actions"><button id="tourSecondaryBtn" class="styled-button"></button><button id="tourPrimaryBtn" class="styled-button primary"></button></div>`);
this.elements.backdrop.append(this.elements.highlightBox,this.elements.tooltip);
document.body.appendChild(this.elements.backdrop);
this.elements.backdrop.classList.add('active');
this.elements.tooltip.querySelector('.tour-close-btn').onclick = () => this.end();
},
nextStep(){this.currentStep++;if(this.currentStep>=this.steps.length){this.end();}else{this.showStep(this.currentStep);}},
prevStep(){if(this.currentStep > 0){this.currentStep--;this.showStep(this.currentStep);}},
async showStep(index){
const step=this.steps[index];
if(step.onBefore){await step.onBefore();}
const primaryBtn=this.elements.tooltip.querySelector('#tourPrimaryBtn');
const secondaryBtn=this.elements.tooltip.querySelector('#tourSecondaryBtn');
this.elements.tooltip.querySelector('.tour-tooltip-title').textContent=step.title;
this.elements.tooltip.querySelector('.tour-tooltip-text').innerHTML=step.text;
if(step.isFinal){
this.elements.highlightBox.style.cssText='top: 50%; left: 50%; width: 0; height: 0; border-radius: 50%; transform: translate(-50%, -50%);';
this.elements.tooltip.style.cssText='top: 50%; left: 50%; transform: translate(-50%, -50%);';
this.elements.tooltip.querySelector('.tour-tooltip-step-counter').textContent='';
primaryBtn.textContent='Завершить';
primaryBtn.onclick=()=>this.end();
secondaryBtn.textContent='Повторить';
secondaryBtn.onclick=()=>this.restart();
secondaryBtn.style.visibility = 'visible';
return;
}
this.elements.tooltip.querySelector('.tour-tooltip-step-counter').textContent=`${index+1} / ${this.steps.length-1}`;
primaryBtn.textContent='Далее';
primaryBtn.onclick=()=>this.nextStep();
secondaryBtn.textContent='Назад';
secondaryBtn.onclick=()=>this.prevStep();
secondaryBtn.style.visibility = index > 0 ? 'visible' : 'hidden';
let target=document.querySelector(step.selector);
if(!target || target.offsetParent === null){console.warn(`Tour step ${index}: element "${step.selector}" not found or not visible, trying next step.`);setTimeout(()=>this.nextStep(), 50);return;}
const rect=target.getBoundingClientRect();
const padding=4;
this.elements.highlightBox.style.cssText=`top: ${rect.top-padding}px; left: ${rect.left-padding}px; width: ${rect.width+padding*2}px; height: ${rect.height+padding*2}px;`;
const tooltipRect=this.elements.tooltip.getBoundingClientRect();
const positions={top:{top:rect.top-tooltipRect.height-15,left:rect.left+rect.width/2-tooltipRect.width/2},bottom:{top:rect.bottom+15,left:rect.left+rect.width/2-tooltipRect.width/2},left:{top:rect.top+rect.height/2-tooltipRect.height/2,left:rect.left-tooltipRect.width-15},right:{top:rect.top+rect.height/2-tooltipRect.height/2,left:rect.right+15}};
let pos=positions[step.position]||positions.bottom;
if(pos.left<10)pos.left=10;
if(pos.left+tooltipRect.width>window.innerWidth-10)pos.left=window.innerWidth-tooltipRect.width-10;
if(pos.top<10)pos.top=10;
if(pos.top+tooltipRect.height>window.innerHeight-10)pos.top=window.innerHeight-tooltipRect.height-10;
this.elements.tooltip.style.cssText=`top: ${pos.top}px; left: ${pos.left}px;`;
}
};
function renderFullLayout(){const equipmentContainer=document.getElementById('equipment');equipmentContainer.innerHTML='';const fragment=document.createDocumentFragment();AppState.layout.forEach((section,index)=>{const sectionEl=createSectionDOM(section);sectionEl.style.animationDelay=`${index*80}ms`;fragment.appendChild(sectionEl);});equipmentContainer.appendChild(fragment);const addSectionBtn=createDOMElement('button','styled-button constructor-add-section-btn fade-in-up','➕ Добавить новую секцию');addSectionBtn.style.animationDelay=`${AppState.layout.length*80}ms`;addSectionBtn.addEventListener('click',handleAddNewSection);equipmentContainer.appendChild(addSectionBtn);renderNotesLayout();renderFavoritesList();renderSetsLayout();}
function createSectionDOM(section){const container=createDOMElement('div','slot-section-container fade-in-up');container.dataset.sectionId=section.id;const header=createDOMElement('div','slot-section-header');const title=createDOMElement('span','editable-title',`${section.title} <span class="edit-icon" style="opacity: 0;">✏️</span>`);title.title='Нажмите, чтобы переименовать секцию';title.addEventListener('click',()=>handleRename(title,'section',section.id));const addSlotBtn=createDOMElement('button','icon-button','➕');addSlotBtn.title='Добавить новый слот';addSlotBtn.addEventListener('click',()=>handleAddNewSlot(section.id));const removeSectionBtn=createDOMElement('button','icon-button','🗑️');removeSectionBtn.title='Удалить эту секцию';removeSectionBtn.addEventListener('click',()=>handleRemoveSection(section.id));header.append(title,addSlotBtn,removeSectionBtn);container.appendChild(header);const grid=createDOMElement('div','slots-grid-layout');if(section.slots){section.slots.forEach(slot=>{grid.appendChild(createSlotDOM(slot));});}container.appendChild(grid);return container;}
function createSlotDOM(slot){const slotEl=createDOMElement('div','item-slot');slotEl.dataset.slotId=slot.id;slotEl.addEventListener('dragover',handleDragOver);slotEl.addEventListener('dragleave',handleDragLeave);slotEl.addEventListener('drop',handleDropOnSlot);slotEl.addEventListener('mouseenter',e=>handleSlotTooltip(e,slot.id,'show'));slotEl.addEventListener('mousemove',e=>handleSlotTooltip(e,slot.id,'move'));slotEl.addEventListener('mouseleave',()=>handleSlotTooltip(null,null,'hide'));updateSlotDOM(slot.id,slotEl);return slotEl;}
function updateSlotDOM(slotId, element = null) {
const slotEl = element || document.querySelector(`[data-slot-id="${slotId}"]`);
const slotData = findSlot(slotId);
if (!slotEl || !slotData) return;
const isOccupied = slotData.items && slotData.items.length > 0;
const item = isOccupied ? slotData.items[0] : null;
const file = item ? AppState.files.get(item.path) : null;
slotEl.innerHTML = '';
slotEl.draggable = isOccupied;
slotEl.classList.toggle('occupied', isOccupied);
const titleEl = createDOMElement('div', 'slot-title-text editable-title', `${slotData.title} <span class="edit-icon">✏️</span>`);
titleEl.title = 'Нажмите, чтобы переименовать слот';
titleEl.addEventListener('click', e => { e.stopPropagation(); handleRename(titleEl, 'slot', slotId); });
const removeBtn = createDOMElement('button', 'icon-button', '🗑️');
removeBtn.title = 'Удалить этот слот';
removeBtn.addEventListener('click', e => { e.stopPropagation(); handleRemoveSlot(slotId); });
const header = createDOMElement('div', 'slot-header-bar');
header.append(titleEl);
if (isOccupied && item.isContainer) {
const containerIcon = createDOMElement('div', 'item-container-icon', '📦');
if (item.contents && item.contents.length > 0) {
containerIcon.innerHTML = `📦<span>${item.contents.length}</span>`;
}
header.append(containerIcon);
}
header.append(removeBtn);
let content;
if (isOccupied && file) {
content = createDOMElement('div', 'slot-content-area');
const wrapper = createDOMElement('div', 'slot-item-wrapper');
const img = createDOMElement('img', 'slot-item-image');
img.src = file.url;
img.alt = file.name;
img.draggable = false;
const captionWrapper = createDOMElement('div', 'slot-item-caption-wrapper');
const caption = createDOMElement('span', 'slot-item-caption', item.name);
captionWrapper.appendChild(caption);
wrapper.append(img, captionWrapper);
content.appendChild(wrapper);
} else {
content = createDOMElement('div', 'slot-content-area', '<div class="slot-placeholder-text">Пусто</div>');
}
slotEl.append(header, content);
if (isOccupied && !item.isContainer && item.quantity > 1) {
const quantityBadge = createDOMElement('div', 'slot-item-quantity-badge', item.quantity);
slotEl.appendChild(quantityBadge);
}
slotEl.ondragstart = (e) => { if (isOccupied) handleDragStart(e, item.id); };
slotEl.onclick = () => { if (isOccupied) openContentsModal(slotId); };
addTouchDragListeners(slotEl);
}
function renderNotesLayout(){const container=document.getElementById('notes');if(!container)return;container.innerHTML='';const notesContainer=createDOMElement('div','notes-container');if(AppState.notes&&AppState.notes.length>0){AppState.notes.forEach(note=>notesContainer.appendChild(createNoteCard(note)));}const addNoteBtn=createDOMElement('button','styled-button','➕ Добавить заметку');addNoteBtn.addEventListener('click',handleAddNote);container.append(notesContainer,addNoteBtn);}
function createNoteCard(note){const card=createDOMElement('div','note-card fade-in-up');card.dataset.noteId=note.id;const content=createDOMElement('div','note-content');content.innerHTML=parseMarkdown(note.content);const actions=createDOMElement('div','note-actions');const editBtn=createDOMElement('button','styled-button','Редактировать');editBtn.addEventListener('click',()=>toggleNoteEditMode(note.id,true));const deleteBtn=createDOMElement('button','styled-button','Удалить');deleteBtn.addEventListener('click',()=>handleRemoveNote(note.id));actions.append(editBtn,deleteBtn);card.append(content,actions);return card;}
function renderSetsLayout(){const container=document.getElementById('sets');if(!container)return;container.innerHTML='';const setsContainer=createDOMElement('div','sets-container');if(AppState.equipmentSets&&AppState.equipmentSets.length>0){AppState.equipmentSets.forEach(set=>{setsContainer.appendChild(createSetCard(set));});}else{const placeholder=createDOMElement('div','explorer-placeholder');placeholder.innerHTML=`<div style="font-size: 4rem; margin-bottom: 1rem;">🎽</div><div>У вас пока нет сохраненных наборов.</div>`;setsContainer.appendChild(placeholder);}
const saveSetBtn=createDOMElement('button','styled-button primary','💾 Сохранить текущую экипировку как набор');saveSetBtn.style.marginTop='20px';saveSetBtn.addEventListener('click',handleSaveCurrentSet);
container.append(setsContainer, saveSetBtn);
const mgmtBox=createDOMElement('div','sets-management-box');mgmtBox.innerHTML=`<h3>Управление наборами</h3><div class="sets-management-actions"><button id="importSetsBtn" class="styled-button"><span>📥</span> Импорт наборов</button><button id="exportSetsBtn" class="styled-button"><span>📤</span> Экспорт наборов</button></div>`;container.appendChild(mgmtBox);document.getElementById('importSetsBtn').addEventListener('click',()=>document.getElementById('importSetsInput').click());document.getElementById('exportSetsBtn').addEventListener('click',handleExportSets);}
function createSetCard(set){const card=createDOMElement('div','set-card fade-in-up');card.dataset.setId=set.id;const title=createDOMElement('h3','',set.name);title.title=set.name;const itemCount=Object.keys(set.configuration).length;const info=createDOMElement('div','set-card-info',`Предметов: ${itemCount}`);const actionsWrapper=createDOMElement('div','set-card-actions');const loadBtn=createDOMElement('button','styled-button primary','🚀');loadBtn.title="Применить";loadBtn.addEventListener('click',()=>handleLoadSet(set.id));const renameBtn=createDOMElement('button','styled-button','✏️');renameBtn.title="Переименовать";renameBtn.addEventListener('click',()=>handleRenameSet(set.id));const deleteBtn=createDOMElement('button','styled-button','🗑️');deleteBtn.title="Удалить";deleteBtn.addEventListener('click',()=>handleDeleteSet(set.id));actionsWrapper.append(renameBtn,deleteBtn,loadBtn);card.append(title,info,actionsWrapper);return card;}
function openSetNameModal(mode,setId,callback){AppState.setNameModal={visible:true,mode,setId,callback};const modal=document.getElementById('setNameModal');const title=modal.querySelector('#setNameModalTitle');const input=modal.querySelector('#setNameInput');const label=modal.querySelector('label[for="setNameInput"]');if(mode==='rename'){title.textContent="Переименовать набор";label.textContent="Введите новое название для набора";input.value=AppState.equipmentSets.find(s=>s.id===setId)?.name||'';}else{title.textContent="Новый набор";label.textContent="Введите название для нового набора";input.value='';}modal.classList.add('visible');document.body.classList.add('modal-open');input.focus();input.select();}
function closeSetNameModal(){document.getElementById('setNameModal').classList.remove('visible');document.body.classList.remove('modal-open');AppState.setNameModal={visible:false,mode:'create',setId:null,callback:null};}
function handleSetNameSave(){const{callback}=AppState.setNameModal;const input=document.getElementById('setNameInput');const name=input.value.trim();if(name&&callback){callback(name);}else if(!name){showToast("Название набора не может быть пустым.",'error');}closeSetNameModal();}
function toggleNoteEditMode(noteId,isEditing){const card=document.querySelector(`[data-note-id="${noteId}"]`);if(!card)return;const noteData=AppState.notes.find(n=>n.id===noteId);if(!noteData)return;if(isEditing){const editor=createDOMElement('textarea','note-editor');editor.value=noteData.content;const saveBtn=createDOMElement('button','styled-button primary','Сохранить');saveBtn.addEventListener('click',()=>{noteData.content=editor.value;toggleNoteEditMode(noteId,false);});const cancelBtn=createDOMElement('button','styled-button','Отмена');cancelBtn.addEventListener('click',()=>toggleNoteEditMode(noteId,false));card.innerHTML='';card.append(editor,createDOMElement('div','note-actions',''));card.lastChild.append(cancelBtn,saveBtn);editor.focus();}else{card.replaceWith(createNoteCard(noteData));requestSave();}}
function parseMarkdown(text){if(!text)return'';return text.replace(/</g,"&lt;").replace(/>/g,"&gt;").replace(/^\s*# (.+)/gm,'<h3>$1</h3>').replace(/\*\*(.+?)\*\*/g,'<strong>$1</strong>').replace(/\*(.+?)\*/g,'<em>$1</em>').replace(/^\s*-\s(.+)/gm,'<li>$1</li>').replace(/<\/li><li>/g,'</li>\n<li>').replace(/^(<li>.*<\/li>)$/gm,'<ul>$1</ul>').replace(/<\/ul>\s*<ul>/g,'').split(/\n\s*\n/).map(p=>p.trim()?`<p>${p.replace(/\n/g,'<br>')}</p>`:'').join('');}
function updateSaveStatus(status){const indicator=document.getElementById('saveStatusIndicator');const messages={saved:'Все изменения сохранены',unsaved:'Есть несохраненные изменения',saving:'Идет сохранение...',error:'Ошибка сохранения'};indicator.className='status-'+status;indicator.title=messages[status]||'';}
function updateTabMarker(){const activeTab=document.querySelector('.tab-button.active');const marker=document.getElementById('tabMarker');if(activeTab&&marker){marker.style.width=`${activeTab.offsetWidth}px`;marker.style.left=`${activeTab.offsetLeft}px`;}}
function handleAddNote(){const newNote={id:`note_${Date.now()}`,content:"# Новая заметка\n\n- Опишите здесь что-нибудь важное."};if(!AppState.notes)AppState.notes=[];AppState.notes.push(newNote);const newCard=createNoteCard(newNote);const container=document.querySelector('#notes .notes-container');if(container)container.appendChild(newCard);else document.getElementById('notes').appendChild(newCard);toggleNoteEditMode(newNote.id,true);}
function handleRemoveNote(noteId){if(confirm("Удалить эту заметку?")){const index=AppState.notes.findIndex(n=>n.id===noteId);if(index>-1){AppState.notes.splice(index,1);document.querySelector(`[data-note-id="${noteId}"]`)?.remove();}requestSave();}}
function handleAddNewSection(){const newSection={id:`section_${Date.now()}`,title:"Новая секция",slots:[]};AppState.layout.push(newSection);const newSectionEl=createSectionDOM(newSection);const container=document.getElementById('equipment');container.insertBefore(newSectionEl,container.querySelector('.constructor-add-section-btn'));showToast("Новая секция добавлена.",'success');requestSave();}
function handleAddNewSlot(sectionId){const section=AppState.layout.find(s=>s.id===sectionId);if(!section)return;const newSlot={id:`slot_${Date.now()}`,title:"Новый слот",items:[]};if(!section.slots)section.slots=[];section.slots.push(newSlot);const grid=document.querySelector(`[data-section-id="${sectionId}"] .slots-grid-layout`);if(grid)grid.appendChild(createSlotDOM(newSlot));showToast("Новый слот добавлен.",'success');requestSave();}
function handleRemoveSection(sectionId){const section=AppState.layout.find(s=>s.id===sectionId);if(section&&confirm(`Удалить секцию "${section.title}"?`)){const index=AppState.layout.findIndex(s=>s.id===sectionId);if(index>-1)AppState.layout.splice(index,1);document.querySelector(`[data-section-id="${sectionId}"]`)?.remove();showToast(`Секция "${section.title}" удалена.`,'info');requestSave();}}
function handleRemoveSlot(slotId){if(confirm("Вы уверены, что хотите удалить этот слот?")){for(const section of AppState.layout){const index=section.slots.findIndex(s=>s.id===slotId);if(index>-1){const removed=section.slots.splice(index,1)[0];document.querySelector(`[data-slot-id="${slotId}"]`)?.remove();showToast(`Слот "${removed.title}" удален.`,'info');requestSave();return;}}}}
function handleRename(element,type,id){const originalText=element.childNodes[0].textContent.trim();const input=createDOMElement('input','title-edit-input');input.type='text';input.value=originalText;element.innerHTML='';element.appendChild(input);input.focus();input.select();const save=()=>{const newTitle=input.value.trim()||originalText;let target;if(type==='section')target=AppState.layout.find(s=>s.id===id);else target=findSlot(id);if(target){target.title=newTitle;}element.innerHTML=`${newTitle} <span class="edit-icon">✏️</span>`;requestSave();};input.addEventListener('blur',save);input.addEventListener('keydown',e=>{if(e.key==='Enter')input.blur();if(e.key==='Escape'){input.value=originalText;input.blur();}});}
function handleCharacterNameClick(event){const target=event.target;target.contentEditable="true";target.focus();try{document.execCommand('selectAll',false,null);}catch(err){window.getSelection().selectAllChildren(target);}}
function handleSaveCurrentSet() { openSetNameModal('create', null, (name) => { const newSet = { id: `set_${Date.now()}`, name: name, configuration: {} }; AppState.layout.forEach(section => { section.slots.forEach(slot => { if (slot.items && slot.items.length > 0) { newSet.configuration[slot.id] = createBlueprintFromItem(slot.items[0]); } }); }); if (Object.keys(newSet.configuration).length === 0) { return showToast("Нечего сохранять. Экипируйте хотя бы один предмет.", 'error'); } AppState.equipmentSets.push(newSet); showToast(`Набор "${name}" успешно сохранен.`, 'success'); requestSave(); renderSetsLayout(); }); }
function handleLoadSet(setId) {
const setToLoad = AppState.equipmentSets.find(s => s.id === setId);
if (!setToLoad) return showToast("Ошибка: набор не найден.", "error");
if (!setToLoad.configuration || typeof setToLoad.configuration !== 'object') return showToast("Ошибка: данные набора повреждены.", "error");
const itemsToDisplace = AppState.layout.flatMap(s => s.slots).filter(slot => slot.items.length > 0).length;
const allSlots = AppState.layout.flatMap(s => s.slots);
const destinationSlotsCount = Object.keys(setToLoad.configuration).length;
const availableSlotsForDisplacedItems = allSlots.length - destinationSlotsCount;
const overflowCount = itemsToDisplace - availableSlotsForDisplacedItems;
let confirmMsg = `Применить набор "${setToLoad.name}"?`;
if (overflowCount > 0) {
confirmMsg = `ВНИМАНИЕ: Недостаточно свободных слотов для всех снимаемых предметов. ${overflowCount} предмет(ов) будут утеряны. Продолжить?`;
}
if (!confirm(confirmMsg)) return;
const displacedItems = [];
AppState.layout.forEach(section => {
section.slots.forEach(slot => {
if (slot.items.length > 0) {
displacedItems.push(...slot.items);
}
slot.items = [];
});
});
for (const slotId in setToLoad.configuration) {
const targetSlot = findSlot(slotId);
if (targetSlot) {
targetSlot.items = [createItemFromBlueprint(setToLoad.configuration[slotId])];
}
}
let unplacedItemsCount = 0;
displacedItems.forEach(item => {
const freeSlot = findFirstEmptySlot();
if (freeSlot) {
freeSlot.items = [item];
} else {
unplacedItemsCount++;
}
});
if (unplacedItemsCount > 0) {
showToast(`Не удалось разместить ${unplacedItemsCount} предмет(ов). Они были удалены.`, "warning");
}
showToast(`Набор "${setToLoad.name}" применен.`, "success");
renderFullLayout();
requestSave();
}
function handleRenameSet(setId){const set=AppState.equipmentSets.find(s=>s.id===setId);if(set)openSetNameModal('rename',setId,(newName)=>{set.name=newName;requestSave();renderSetsLayout();showToast("Набор переименован.",'success');});}
function handleDeleteSet(setId){const set=AppState.equipmentSets.find(s=>s.id===setId);if(set&&confirm(`Удалить набор "${set.name}"?`)){AppState.equipmentSets=AppState.equipmentSets.filter(s=>s.id!==setId);requestSave();renderSetsLayout();showToast(`Набор "${set.name}" удален.`,'info');}}
function buildFileHierarchy(files){const root={};files.forEach(file=>{if(!file.webkitRelativePath)return;const pathParts=file.webkitRelativePath.split('/').slice(1);let currentLevel=root;pathParts.forEach((part,index)=>{if(index===pathParts.length-1){if(!currentLevel.files)currentLevel.files=[];currentLevel.files.push({name:part,path:file.webkitRelativePath});}else{if(!currentLevel.dirs)currentLevel.dirs={};if(!currentLevel.dirs[part])currentLevel.dirs[part]={};currentLevel=currentLevel.dirs[part];}});});return root;}
function renderFileExplorer(){const grid=document.getElementById('resourcesGrid');grid.innerHTML='';const filteredHierarchy=AppState.activeSearchTerm?filterFileHierarchy(AppState.fileHierarchy,AppState.activeSearchTerm.toLowerCase()):AppState.fileHierarchy;if(!filteredHierarchy||(Object.keys(filteredHierarchy).length===0&&(!filteredHierarchy.files||filteredHierarchy.files.length===0))){grid.innerHTML='<div class="explorer-placeholder"><div style="font-size: 4rem; margin-bottom: 1rem;">🗂️</div><div>Начните с выбора папки с ресурсами.</div></div>';return;}const tree=createDOMElement('ul','explorer-tree');renderFolderNode(tree,filteredHierarchy,AppState.rootDirectoryName);grid.appendChild(tree);}
function renderFolderNode(parentEl,nodeData,nodeName){const folderEl=createDOMElement('li','explorer-folder');if(AppState.activeSearchTerm)folderEl.classList.add('open');const headerEl=createDOMElement('div','folder-header',`<span class="folder-toggle-icon">▶</span><span class="folder-name-text" title="${nodeName}">${nodeName}</span>`);headerEl.addEventListener('click',e=>{e.stopPropagation();folderEl.classList.toggle('open');});const contentWrapper=createDOMElement('div','folder-content-wrapper');if(nodeData.dirs){const subTree=createDOMElement('ul','explorer-tree');Object.keys(nodeData.dirs).sort().forEach(dirName=>renderFolderNode(subTree,nodeData.dirs[dirName],dirName));contentWrapper.appendChild(subTree);}if(nodeData.files&&nodeData.files.length>0){const fileGrid=createDOMElement('div','file-grid');nodeData.files.sort((a,b)=>a.name.localeCompare(b.name)).forEach(fileInfo=>{const fileData=AppState.files.get(fileInfo.path);if(!fileData)return;const itemEl=createDOMElement('div','file-grid-item');itemEl.dataset.path=fileInfo.path;const humanizedName=humanizeFilename(fileData.name);itemEl.title=humanizedName;itemEl.innerHTML=`<div class="file-thumb-wrapper"><img src="${fileData.url}" alt="${humanizedName}" draggable="false"></div><div class="file-name-label" title="${humanizedName}">${humanizedName}</div>`;itemEl.draggable=true;itemEl.ondragstart=e=>handleDragStart(e,null,fileInfo.path);addTouchDragListeners(itemEl);itemEl.onclick=()=>ImageViewer.open({url:fileData.url,itemPath:fileInfo.path});fileGrid.appendChild(itemEl);});contentWrapper.appendChild(fileGrid);}folderEl.append(headerEl,contentWrapper);parentEl.appendChild(folderEl);}
function filterFileHierarchy(node,term){if(!node)return null;const result={};if(node.files){const matchedFiles=node.files.filter(f=>humanizeFilename(f.name).toLowerCase().includes(term));if(matchedFiles.length>0)result.files=matchedFiles;}if(node.dirs){const matchedDirs={};for(const dirName in node.dirs){const filteredSubDir=filterFileHierarchy(node.dirs[dirName],term);if(filteredSubDir&&((filteredSubDir.dirs&&Object.keys(filteredSubDir.dirs).length>0)||(filteredSubDir.files&&filteredSubDir.files.length>0)))matchedDirs[dirName]=filteredSubDir;}if(Object.keys(matchedDirs).length>0)result.dirs=matchedDirs;}return result;}
function renderFavoritesList(){const gridElement=document.getElementById("favoritesGrid");const searchTerm=AppState.activeFavoritesSearchTerm.toLowerCase();let filteredFavorites=[...AppState.favorites].filter(item=>(item.name.toLowerCase().includes(searchTerm)||item.desc.toLowerCase().includes(searchTerm)||item.tags.toLowerCase().includes(searchTerm)));switch(AppState.activeFavoritesSort){case'name_asc':filteredFavorites.sort((a,b)=>a.name.localeCompare(b.name));break;case'name_desc':filteredFavorites.sort((a,b)=>b.name.localeCompare(a.name));break;default:filteredFavorites.sort((a,b)=>(b.timestamp||0)-(a.timestamp||0));break;}gridElement.innerHTML="";if(!filteredFavorites||filteredFavorites.length===0){gridElement.innerHTML='<div class="explorer-placeholder">Ничего не найдено.</div>';return;}filteredFavorites.forEach(item=>{const fileData=AppState.files.get(item.path);const itemElement=createDOMElement("div","file-grid-item");itemElement.dataset.itemId=item.id;itemElement.title=`${item.name}\n${item.desc||''}`;const thumbWrapper=createDOMElement("div","file-thumb-wrapper",fileData?`<img src="${fileData.url}" alt="${item.name}" draggable="false">`:'');if(!fileData){thumbWrapper.style.cssText="align-items:center;justify-content:center;font-size:2rem;color:var(--text-secondary);background:rgba(0,0,0,0.05);";thumbWrapper.textContent="?";}const tagsContainer=createDOMElement("div","tags-container");if(item.tags){item.tags.split(',').forEach(tag=>{tag=tag.trim();if(tag)tagsContainer.innerHTML+=`<span class="item-tag">${tag}</span>`;});}itemElement.onclick=()=>{if(fileData)ImageViewer.open({url:fileData.url,itemPath:item.path,itemId:item.id});else showToast("Источник изображения отсутствует.","error");};const actionsOverlay=createDOMElement("div","item-actions-overlay");const editBtn=createDOMElement("button","icon-button edit-btn","✏️");editBtn.title="Редактировать";editBtn.onclick=(e)=>{e.stopPropagation();openEditModal(item);};const unfavoriteBtn=createDOMElement("button","icon-button unfavorite-btn","×");unfavoriteBtn.title="Удалить из избранного";unfavoriteBtn.onclick=(e)=>{e.stopPropagation();handleRemoveFromFavorites(item.id);};actionsOverlay.append(editBtn,unfavoriteBtn);itemElement.append(thumbWrapper,createDOMElement("div","file-name-label",item.name),tagsContainer,actionsOverlay);itemElement.draggable=true;itemElement.ondragstart=(e)=>{if(!fileData){e.preventDefault();return;}handleDragStart(e,null,item.path,item);};addTouchDragListeners(itemElement);gridElement.appendChild(itemElement);});}
function handleToggleFavorite(item,event=null,buttonEl=null){const targetButton=buttonEl||(event?event.currentTarget:null);const favIndex=AppState.favorites.findIndex(fav=>fav.id===item.id);if(favIndex>-1){AppState.favorites.splice(favIndex,1);if(targetButton){targetButton.classList.remove('is-favorite');targetButton.innerHTML=targetButton.matches('.icon-button')?'⭐':'<span>⭐</span> В избранное';targetButton.title="Добавить в избранное";}showToast("Предмет убран из избранного.");}else{item.timestamp=Date.now();AppState.favorites.push(JSON.parse(JSON.stringify(item)));if(targetButton){targetButton.classList.add('is-favorite');targetButton.innerHTML=targetButton.matches('.icon-button')?'🌟':'<span>🌟</span> В избранном';targetButton.title="Убрать из избранного";}showToast("Предмет добавлен в избранное!",'success');}requestSave();renderFavoritesList();if(AppState.contentsModal.visible)renderContentsModal();if(AppState.imageViewer.active)ImageViewer.renderActions();}
function handleRemoveFromFavorites(itemId){const favIndex=AppState.favorites.findIndex(fav=>fav.id===itemId);if(favIndex>-1){AppState.favorites.splice(favIndex,1);showToast("Предмет убран из избранного.");requestSave();renderFavoritesList();}}
function openContentsModal(slotId){AppState.contentsModal={visible:true,targetSlotId:slotId};renderContentsModal();document.getElementById('contentsModal').classList.add('visible');document.body.classList.add('modal-open');}
function closeContentsModal(){document.getElementById('contentsModal').classList.remove('visible');document.body.classList.remove('modal-open');AppState.contentsModal.visible=false;}
function renderContentsModal(){const slotId=AppState.contentsModal.targetSlotId;const slot=findSlot(slotId);if(!slot)return;const listEl=document.getElementById('contentsModalList');listEl.innerHTML='';document.getElementById('contentsModalTitle').textContent=`Слот: ${slot.title}`;const fragment=document.createDocumentFragment();if(slot.items.length>0){const mainItem=slot.items[0];const createContentItemDOM=(itemData,parentItem=null)=>{const itemEl=createDOMElement('div','contents-item');itemEl.dataset.itemId=itemData.id;if(parentItem)itemEl.classList.add('is-child-item');if(itemData.isContainer&&!parentItem)itemEl.classList.add('is-container');const file=AppState.files.get(itemData.path);const thumb=createDOMElement('div','contents-item-thumb',file?`<img src="${file.url}" alt="${itemData.name}">`:'');if(!file){thumb.style.cssText="font-size:2rem;color:var(--text-secondary);";thumb.textContent='?';}thumb.onclick=()=>{if(file)ImageViewer.open({url:file.url,itemPath:itemData.path,itemId:itemData.id});};const details=createDOMElement('div','contents-item-details',`<div class="contents-item-name">${itemData.name}</div><div class="contents-item-desc">${itemData.desc||'Нет описания'}</div>`);const actions=createDOMElement('div','contents-item-actions');const editBtn=createDOMElement('button','icon-button','✏️');editBtn.title='Редактировать';editBtn.onclick=()=>{closeContentsModal();openEditModal(itemData);};const deleteBtn=createDOMElement('button','icon-button danger','🗑️');deleteBtn.title='Удалить';deleteBtn.onclick=()=>{if(parentItem)findItemById(parentItem.id).item.contents=findItemById(parentItem.id).item.contents.filter(i=>i.id!==itemData.id);else slot.items=[];renderContentsModal();updateSlotDOM(slot.id);requestSave();};actions.append(editBtn,deleteBtn);const favBtn=createDOMElement('button','icon-button favorite-btn','⭐');const isFavorite=AppState.favorites.some(f=>f.id===itemData.id);if(isFavorite){favBtn.classList.add('is-favorite');favBtn.innerHTML='🌟';}favBtn.title=isFavorite?"Убрать":"В избранное";favBtn.onclick=(e)=>handleToggleFavorite(itemData,e);actions.prepend(favBtn);if(parentItem){const ejectBtn=createDOMElement('button','icon-button','⏏️');ejectBtn.title="Извлечь";ejectBtn.onclick=()=>handleEjectItem(parentItem,itemData);actions.appendChild(ejectBtn);}else{const containerBtn=createDOMElement('button','icon-button','📦');containerBtn.title="Сделать контейнером";if(itemData.isContainer)containerBtn.classList.add('is-container-toggle');containerBtn.onclick=()=>{itemData.isContainer=!itemData.isContainer;if(itemData.isContainer&&!itemData.contents)itemData.contents=[];renderContentsModal();updateSlotDOM(slot.id);requestSave();};actions.appendChild(containerBtn);}itemEl.append(thumb,details,actions);return itemEl;};const mainItemEl=createContentItemDOM(mainItem);if(mainItemEl)fragment.appendChild(mainItemEl);if(mainItem.isContainer&&mainItem.contents)mainItem.contents.forEach(contentItem=>{const contentItemEl=createContentItemDOM(contentItem,mainItem);if(contentItemEl)fragment.appendChild(contentItemEl);});}else{listEl.innerHTML='<div class="explorer-placeholder">Пусто</div>';}listEl.appendChild(fragment);}
function openEditModal(item) {
if (!item) return;
const file = AppState.files.get(item.path);
AppState.editModal.targetItem = item;
document.getElementById('modalOriginalName').value = file ? file.name : '(Источник изображения отсутствует)';
document.getElementById('modalName').value = item.name || '';
document.getElementById('modalDesc').value = item.desc || '';
document.getElementById('modalTags').value = item.tags || '';
const isContainerCheckbox = document.getElementById('modalIsContainer');
const quantityGroup = document.getElementById('quantityFormGroup');
const quantityInput = document.getElementById('modalQuantity');
isContainerCheckbox.checked = item.isContainer;
quantityInput.value = item.quantity || 1;
const toggleQuantityVisibility = () => {
quantityGroup.style.display = isContainerCheckbox.checked ? 'none' : 'block';
};
toggleQuantityVisibility();
isContainerCheckbox.onchange = toggleQuantityVisibility;
document.getElementById('editModal').classList.add('visible');
document.body.classList.add('modal-open');
}
function closeEditModal(){document.getElementById('editModal').classList.remove('visible');document.body.classList.remove('modal-open');AppState.editModal.targetItem=null;}
function updateItemInState(updatedItem){const favIndex=AppState.favorites.findIndex(fav=>fav.id===updatedItem.id);if(favIndex>-1)AppState.favorites[favIndex]={...AppState.favorites[favIndex],...updatedItem};AppState.layout.forEach(section=>{section.slots.forEach(slot=>{slot.items.forEach((item,index)=>{if(item.id===updatedItem.id)slot.items[index]={...item,...updatedItem};if(item.isContainer&&item.contents){item.contents.forEach((contentItem,contentIndex)=>{if(contentItem.id===updatedItem.id)item.contents[contentIndex]={...contentItem,...updatedItem};});}});});});}
function handleSaveItemAction() {
const itemToUpdate = AppState.editModal.targetItem;
if (!itemToUpdate) return;
const fileData = AppState.files.get(itemToUpdate.path);
const defaultName = fileData ? humanizeFilename(fileData.name) : itemToUpdate.name;
const isContainer = document.getElementById("modalIsContainer").checked;
const updatedData = {
name: document.getElementById("modalName").value.trim() || defaultName,
desc: document.getElementById("modalDesc").value.trim(),
tags: document.getElementById("modalTags").value.trim(),
isContainer: isContainer,
quantity: isContainer ? 1 : (parseInt(document.getElementById("modalQuantity").value, 10) || 1),
};
if (updatedData.isContainer && !itemToUpdate.contents) {
updatedData.contents = [];
}
const finalUpdatedItem = { ...itemToUpdate, ...updatedData };
updateItemInState(finalUpdatedItem);
showToast("Предмет обновлен.", 'success');
renderFullLayout();
requestSave();
closeEditModal();
}
function handleEjectItem(containerItem,itemToEject){const containerRef=findItemById(containerItem.id)?.item;if(!containerRef||!containerRef.isContainer)return;const itemIndex=containerRef.contents.findIndex(i=>i.id===itemToEject.id);if(itemIndex===-1)return;const freeSlot=findFirstEmptySlot();if(freeSlot){const[ejectedItem]=containerRef.contents.splice(itemIndex,1);freeSlot.items.push(ejectedItem);updateSlotDOM(freeSlot.id);renderContentsModal();requestSave();showToast(`Предмет '${ejectedItem.name}' перемещен в инвентарь.`,'success');}else{showToast("Нет свободных слотов в инвентаре!","error");}}
function findFirstEmptySlot() { return AppState.layout.flatMap(section => section.slots).find(slot => slot.items.length === 0) || null; }
const ImageViewer = {
elements: {
viewer: document.getElementById('imageViewer'),
image: document.getElementById('viewerImage'),
closeBtn: document.getElementById('imageViewerClose'),
resetBtn: document.getElementById('viewerResetBtn'),
editBtn: document.getElementById('viewerEditBtn'),
favoriteBtn: document.getElementById('viewerFavoriteBtn'),
},
open(context) {
if (!context || !context.url) return;
const state = AppState.imageViewer;
state.active = true;
state.context = context;
this.elements.image.src = context.url;
this.resetState();
this.renderActions();
this.elements.viewer.classList.add('visible');
document.body.classList.add('modal-open');
},
close() {
const state = AppState.imageViewer;
state.active = false;
state.context = null;
this.elements.viewer.classList.remove('visible');
document.body.classList.remove('modal-open');
},
resetState() {
const state = AppState.imageViewer;
state.scale = 3.5;
state.posX = 0;
state.posY = 0;
this.updateTransform();
},
updateTransform() {
const { scale, posX, posY } = AppState.imageViewer;
this.elements.image.style.transform = `translate(${posX}px, ${posY}px) scale(${scale})`;
},
renderActions() {
const { context } = AppState.imageViewer;
if (!context) return;
const { editBtn, favoriteBtn } = this.elements;
let item = null;
if (context.itemId) {
item = findItemById(context.itemId)?.item || AppState.favorites.find(f => f.id === context.itemId);
}
editBtn.disabled = !item;
editBtn.onclick = item ? () => { this.close(); openEditModal(item); } : null;
const isFavorite = !!AppState.favorites.find(f => (item && f.id === item.id) || (!item && f.path === context.itemPath));
favoriteBtn.classList.toggle('is-favorite', isFavorite);
favoriteBtn.innerHTML = isFavorite ? '<span>🌟</span> В избранном' : '<span>⭐</span> В избранное';
favoriteBtn.onclick = () => this.handleFavoriteClick();
},
handleFavoriteClick() {
const { context } = AppState.imageViewer;
if (!context) return;
let item = null;
if (context.itemId) {
item = findItemById(context.itemId)?.item || AppState.favorites.find(f => f.id === context.itemId);
}
if (!item) {
item = AppState.favorites.find(f => f.path === context.itemPath) || createItem(context.itemPath);
}
if (!item) return showToast("Не удалось создать предмет.", "error");
handleToggleFavorite(item, null, this.elements.favoriteBtn);
},
handleZoom(event) {
event.preventDefault();
const state = AppState.imageViewer;
const zoomIntensity = 0.2;
const delta = event.deltaY > 0 ? -1 : 1;
const newScale = state.scale + delta * zoomIntensity;
state.scale = Math.max(1, Math.min(newScale, 8));
if (state.scale === 1) {
this.resetState();
} else {
this.updateTransform();
}
},
panStart(event) {
const state = AppState.imageViewer;
if (state.scale <= 1) return;
event.preventDefault();
state.isPanning = true;
state.panStartX = (event.touches ? event.touches[0].clientX : event.clientX) - state.posX;
state.panStartY = (event.touches ? event.touches[0].clientY : event.clientY) - state.posY;
this.elements.viewer.classList.add('is-panning');
},
panMove(event) {
const state = AppState.imageViewer;
if (!state.isPanning) return;
event.preventDefault();
state.posX = (event.touches ? event.touches[0].clientX : event.clientX) - state.panStartX;
state.posY = (event.touches ? event.touches[0].clientY : event.clientY) - state.panStartY;
this.updateTransform();
},
panEnd() {
const state = AppState.imageViewer;
state.isPanning = false;
this.elements.viewer.classList.remove('is-panning');
},
init() {
this.elements.closeBtn.addEventListener('click', () => this.close());
this.elements.resetBtn.addEventListener('click', () => this.resetState());
this.elements.viewer.addEventListener('click', (e) => {
if (e.target === this.elements.viewer) this.close();
});
this.elements.image.addEventListener('wheel', (e) => this.handleZoom(e), { passive: false });
this.elements.image.addEventListener('mousedown', (e) => this.panStart(e));
window.addEventListener('mousemove', (e) => this.panMove(e));
window.addEventListener('mouseup', () => this.panEnd());
this.elements.image.addEventListener('touchstart', (e) => this.panStart(e), { passive: false });
window.addEventListener('touchmove', (e) => this.panMove(e), { passive: false });
window.addEventListener('touchend', () => this.panEnd());
}
};
/* === End ImageViewer Module === */
function handleSlotTooltip(event,slotId,action){const tooltip=document.getElementById('slotTooltip');if(action==='hide')return tooltip.style.opacity='0';const slot=findSlot(slotId);if(!slot||slot.items.length===0)return;if(action==='show'){const item=slot.items[0];const containerCount=item.isContainer&&item.contents?item.contents.length:0;let desc=item.desc;if(item.isContainer)desc+="\n\n(Контейнер)";if(containerCount>0)desc+=`\nСодержит: ${containerCount} предмет(ов)`;document.getElementById('tooltipTitle').textContent=item.name;document.getElementById('tooltipDesc').textContent=desc;tooltip.style.opacity='1';}const moveTooltip=(el,e,offset=10)=>{let x=e.pageX+offset,y=e.pageY+offset;const rect=el.getBoundingClientRect();if(x+rect.width>window.innerWidth)x=e.pageX-rect.width-offset;if(y+rect.height>window.innerHeight)y=e.pageY-rect.height-offset;el.style.left=`${x}px`;el.style.top=`${y}px`;};moveTooltip(tooltip,event,20);}
function openResetModal(){document.getElementById('resetModal').classList.add('visible');document.body.classList.add('modal-open');}
function closeResetModal(){document.getElementById('resetModal').classList.remove('visible');document.body.classList.remove('modal-open');}
function executeReset(){let changesMade=false;if(document.getElementById('resetLayout').checked){AppState.layout=BASE_LAYOUT_CONFIG();changesMade=true;}if(document.getElementById('resetCharName').checked){AppState.characterName="Безымянный Герой";document.getElementById('characterName').textContent=AppState.characterName;changesMade=true;}if(document.getElementById('resetNotes').checked){AppState.notes=[];changesMade=true;}if(document.getElementById('resetFavorites').checked){AppState.favorites=[];changesMade=true;}if(changesMade){renderFullLayout();requestSave();showToast("Выбранные данные сброшены.",'success');}closeResetModal();}
function generateTextExport() {
let output = `Имя: ${AppState.characterName}\n\n`;
const formatItem = (item, indent) => {
const nameDisplay = (item.quantity > 1 && !item.isContainer) ? `${item.name} (x${item.quantity})` : item.name;
let str = `${' '.repeat(indent)}${nameDisplay}\n`;
if (item.desc) { str += `${' '.repeat(indent + 2)}(${item.desc})\n`; }
if (item.isContainer && item.contents?.length > 0) {
item.contents.forEach(subItem => { str += formatItem(subItem, indent + 2); });
}
return str;
};
AppState.layout.forEach(section => {
let sectionOutput = '';
section.slots.forEach(slot => {
if (slot.items.length > 0) {
sectionOutput += `"${slot.title}":\n`;
sectionOutput += formatItem(slot.items[0], 2);
}
});
if (sectionOutput) { output += `${section.title}:\n\n${sectionOutput}\n`; }
});
return output.trim();
}
function handleExportAction(){const selector=document.getElementById('exportSelector');if(selector.value==='default')return;const type=selector.value;selector.value='default';if(type==='json'){const exportData={meta:{app:'Constructor-Phoenix',version:'PRIME-FINAL.13'},characterName:AppState.characterName,notes:AppState.notes,layout:AppState.layout,favorites:AppState.favorites,equipmentSets:AppState.equipmentSets};downloadFile(JSON.stringify(exportData,null,2),'phoenix_build_full.json','application/json');showToast("Полная конфигурация экспортирована.",'success');}else if(type==='text_all'){downloadFile(generateTextExport(),'phoenix_build_report.txt','text/plain');showToast("Текстовый отчет экспортирован.",'success');}}
function handleImportAction(event){const file=event.target.files[0];if(!file)return;const reader=new FileReader();reader.onload=async(e)=>{try{const data=JSON.parse(e.target.result);if(!data.layout)throw new Error("Неверный формат файла.");AppState.layout=data.layout||BASE_LAYOUT_CONFIG();AppState.characterName=data.characterName||"Безымянный Герой";AppState.notes=data.notes||[];AppState.favorites=data.favorites||[];AppState.equipmentSets=data.equipmentSets||[];document.getElementById('characterName').textContent=AppState.characterName;renderFullLayout();showToast("Конфигурация успешно импортирована.",'success');requestSave();}catch(error){showToast(`Ошибка импорта: ${error.message}`,'error');}};reader.readAsText(file);event.target.value=null;}
function handleExportSets(){if(AppState.equipmentSets.length===0)return showToast("Нет наборов для экспорта.","info");const exportData={meta:{type:"PhoenixSets",version:"1.0"},sets:AppState.equipmentSets};downloadFile(JSON.stringify(exportData,null,2),`phoenix_sets_${Date.now()}.json`,'application/json');showToast(`${AppState.equipmentSets.length} набор(ов) экспортировано.`,"success");}
function handleImportSets(event){const file=event.target.files[0];if(!file)return;const reader=new FileReader();reader.onload=(e)=>{try{const data=JSON.parse(e.target.result);if(!data.meta||data.meta.type!=="PhoenixSets"||!Array.isArray(data.sets))throw new Error("Файл не является корректным файлом наборов.");const existingNames=new Set(AppState.equipmentSets.map(s=>s.name));let importedCount=0;data.sets.forEach(newSet=>{if(!existingNames.has(newSet.name)){AppState.equipmentSets.push(newSet);importedCount++;}});if(importedCount>0){showToast(`Импортировано ${importedCount} новых наборов.`,'success');renderSetsLayout();requestSave();}else{showToast("Нет новых наборов для импорта.","info");}}catch(error){showToast(`Ошибка импорта наборов: ${error.message}`,"error");}};reader.readAsText(file);event.target.value=null;}
async function handleFolderSelection(event){const files=Array.from(event.target.files);if(!files||files.length===0)return;showToast("Идет обработка ресурсов...","info");await DBPersistence.saveFiles(files);processSelectedFiles(files);}
async function processSelectedFiles(files) { const imageFiles = files.filter(f => /\.(jpe?g|png|gif|webp|svg)$/i.test(f.name)); if (imageFiles.length === 0) { showToast("В выбранной папке нет изображений.", 'error'); return resetUI(); } resetUI(); toggleActionButtons(false); const totalFiles = imageFiles.length; AppState.rootDirectoryName = imageFiles[0].webkitRelativePath.split('/')[0]; AppState.files = new Map(); showToast(`Начата обработка ${totalFiles} файлов...`, 'info'); const grid = document.getElementById('resourcesGrid'); grid.innerHTML = `<div class="explorer-placeholder" id="loading-placeholder"><div style="font-size: 4rem; margin-bottom: 1rem;">⏳</div><div id="loading-status">Подготовка... 0%</div></div>`; await new Promise(r => setTimeout(r, 0)); const statusEl = document.getElementById('loading-status'); if (!statusEl) { showToast("Критическая ошибка: не удалось обновить статус.", "error"); return; } for (let i = 0; i < totalFiles; i += 50) { const chunk = imageFiles.slice(i, i + 50); for (const file of chunk) { AppState.files.set(file.webkitRelativePath, { path: file.webkitRelativePath, name: file.name, url: URL.createObjectURL(file) }); } statusEl.textContent = `Обработка... ${Math.round(((i + chunk.length) / totalFiles) * 100)}%`; await new Promise(r => setTimeout(r, 15)); } statusEl.textContent = 'Построение дерева файлов...'; await new Promise(r => setTimeout(r, 20)); AppState.fileHierarchy = buildFileHierarchy(imageFiles); AppState.isLoaded = true; renderFileExplorer(); toggleActionButtons(true); renderFavoritesList(); showToast(`Ресурсы (${totalFiles} шт.) успешно загружены.`, 'success'); }
function processLoadedFiles(filesFromDB){if(!filesFromDB.length)return;resetUI(false);try{AppState.rootDirectoryName=filesFromDB[0].path.split('/')[0];}catch(e){return;}const filesForHierarchy=[];for(const{path,url,name}of filesFromDB){AppState.files.set(path,{name,path,url});filesForHierarchy.push({webkitRelativePath:path,name:name});}AppState.fileHierarchy=buildFileHierarchy(filesForHierarchy);AppState.isLoaded=true;renderFileExplorer();toggleActionButtons(true);renderFavoritesList();}
function resetUI(revokeURLs=true){if(revokeURLs){AppState.files.forEach(file=>{if(file.url&&file.url.startsWith('blob:'))URL.revokeObjectURL(file.url);});AppState.files.clear();}AppState.fileHierarchy={};AppState.isLoaded=false;renderFileExplorer();toggleActionButtons(false);}
function toggleActionButtons(enabled){document.getElementById('importBtn').disabled=!enabled;document.getElementById('exportSelector').disabled=!enabled;document.getElementById('resourcesSearchInput').disabled=!enabled;}
function setupSidebarResizing() { const resizer = document.getElementById('sidebar-resizer'); const mainView = document.querySelector('.app-main-view'); const body = document.body; let isResizing = false; resizer.addEventListener('mousedown', e => { e.preventDefault(); isResizing = true; body.classList.add('is-resizing'); }); document.addEventListener('mousemove', e => { if (!isResizing) return; let newWidth = e.clientX; newWidth = Math.max(200, Math.min(newWidth, 800)); mainView.style.gridTemplateColumns = `${newWidth}px 1fr`; resizer.style.left = `${newWidth - 2.5}px`; }); document.addEventListener('mouseup', () => { if (isResizing) { isResizing = false; body.classList.remove('is-resizing'); localStorage.setItem('phoenixSidebarWidth', mainView.style.gridTemplateColumns.split(' ')[0]); } }); }
function handleDragStart(event, itemId, path = null, favoriteItemData = {}) { if (AppState.dragging.isTouch) return; const itemPath = itemId ? findItemById(itemId)?.item.path : path; if (!itemPath) return; Object.assign(AppState.dragging, { isTouch: false, draggedItemId: itemId, path: itemPath, isFavoriteDrag: !!favoriteItemData.id, favoriteItemData }); event.dataTransfer.setData('text/plain', itemPath); const file = AppState.files.get(itemPath); if (!file) return; const ghostName = favoriteItemData.name || (itemId ? findItemById(itemId)?.item.name : humanizeFilename(file.name)); const ghost = createDragGhost(file, ghostName); document.body.appendChild(ghost); event.dataTransfer.setDragImage(ghost, 20, 20); setTimeout(() => ghost.remove(), 0); document.getElementById('dropZone').classList.add('visible'); }
function addTouchDragListeners(element) { element.addEventListener('touchstart', handleTouchStart, { passive: false }); }
function cancelLongPress() { clearTimeout(AppState.dragging.longPressTimeout); AppState.dragging.longPressTimeout = null; window.removeEventListener('touchmove', handleInitialTouchMove); window.removeEventListener('touchend', cancelLongPress); window.removeEventListener('touchcancel', cancelLongPress); }
function handleInitialTouchMove(e) { const D = AppState.dragging; if (!D.longPressTimeout) return; const touch = e.touches[0]; const dx = Math.abs(touch.clientX - D.touchStartX); const dy = Math.abs(touch.clientY - D.touchStartY); if (dx > 10 || dy > 10) { cancelLongPress(); } }
function handleTouchStart(e) { const D = AppState.dragging; cancelLongPress(); const target = e.currentTarget; const isDraggable = target.classList.contains('occupied') || target.classList.contains('file-grid-item'); if (!isDraggable) return; const touch = e.touches[0]; D.touchStartX = touch.clientX; D.touchStartY = touch.clientY; D.longPressTimeout = setTimeout(() => { D.longPressTimeout = null; e.preventDefault(); document.body.classList.add('touch-drag-active'); D.isTouch = true; try { if(navigator.vibrate) navigator.vibrate(50); } catch (err) {} let itemId = null, path = null, favoriteItemData = {}; if (target.dataset.slotId) { const slot = findSlot(target.dataset.slotId); if(slot && slot.items.length > 0) itemId = slot.items[0].id; } else if (target.dataset.path) { path = target.dataset.path; } else if (target.dataset.itemId) { itemId = target.dataset.itemId; favoriteItemData = AppState.favorites.find(f => f.id === itemId) || {}; } const itemPath = itemId ? (findItemById(itemId)?.item.path || favoriteItemData.path) : path; if (!itemPath) { cleanUpDragState(); return; } Object.assign(D, { draggedItemId: itemId, path: itemPath, isFavoriteDrag: !!favoriteItemData.id, favoriteItemData }); const file = AppState.files.get(itemPath); if (!file) { cleanUpDragState(); return; } const ghostName = favoriteItemData.name || (itemId ? findItemById(itemId)?.item.name : humanizeFilename(file.name)); D.ghost = createDragGhost(file, ghostName); Object.assign(D.ghost.style, { position: 'fixed', zIndex: '9999', pointerEvents: 'none' }); document.body.appendChild(D.ghost); handleDragMove(e); document.getElementById('dropZone').classList.add('visible'); window.addEventListener('touchmove', handleDragMove, { passive: false }); window.addEventListener('touchend', handleTouchEnd, { once: true }); window.addEventListener('touchcancel', handleTouchEnd, { once: true }); }, 350); window.addEventListener('touchmove', handleInitialTouchMove, { passive: true }); window.addEventListener('touchend', cancelLongPress, { once: true }); window.addEventListener('touchcancel', cancelLongPress, { once: true }); }
function handleDragMove(e) { if (AppState.dragging.isTouch && e.cancelable) e.preventDefault(); const D = AppState.dragging; if (!D.isTouch || !D.ghost) return; const touch = e.touches[0] || e.changedTouches[0]; if (!touch) return; D.ghost.style.left = `${touch.clientX - 20}px`; D.ghost.style.top = `${touch.clientY - 20}px`; D.ghost.style.display = 'none'; const elementUnderTouch = document.elementFromPoint(touch.clientX, touch.clientY); D.ghost.style.display = ''; if (D.lastHoveredElement && D.lastHoveredElement !== elementUnderTouch) { D.lastHoveredElement.classList.remove('dragover-active', 'dragover'); } const dropZone = document.getElementById('dropZone'); const slotTarget = elementUnderTouch?.closest('.item-slot'); D.isDeletionDrop = false; D.targetSlotId = null; if (elementUnderTouch?.closest('#dropZone')) { dropZone.classList.add('dragover'); D.isDeletionDrop = true; D.lastHoveredElement = dropZone; } else if (slotTarget) { slotTarget.classList.add('dragover-active'); D.targetSlotId = slotTarget.dataset.slotId; D.lastHoveredElement = slotTarget; } else { D.lastHoveredElement = null; } }
function handleTouchEnd(e) { finalizeDrop(); }
function handleDragEnd() { if (AppState.dragging.isTouch) return; finalizeDrop(); }
function finalizeDrop() { const { draggedItemId, isFavoriteDrag, favoriteItemData, path, targetSlotId, isDeletionDrop } = AppState.dragging; if (targetSlotId) { const targetSlot = findSlot(targetSlotId); if (targetSlot) { const targetItem = targetSlot.items[0]; if (targetItem && targetItem.isContainer) { if (targetItem.id !== draggedItemId) { let newItem; if (draggedItemId) { const source = findItemById(draggedItemId); if (source) { newItem = source.item; if (source.container) source.container.contents = source.container.contents.filter(i => i.id !== newItem.id); else source.slot.items = []; updateSlotDOM(source.slot.id); } } else { newItem = isFavoriteDrag ? createItemFromBlueprint(favoriteItemData) : createItem(path); } if (newItem) { if(!targetItem.contents) targetItem.contents = []; targetItem.contents.push(newItem); showToast("Предмет добавлен в контейнер.", 'success'); updateSlotDOM(targetSlot.id); requestSave(); } } } else { if (draggedItemId) { const source = findItemById(draggedItemId); if (source && source.slot.id !== targetSlot.id) { const movedItem = source.item; if (targetItem) { if (source.container) { showToast("Нельзя обменять предмет из контейнера.", "error"); } else { source.slot.items = [targetItem]; targetSlot.items = [movedItem]; } } else { if (source.container) source.container.contents = source.container.contents.filter(i => i.id !== movedItem.id); else source.slot.items = []; targetSlot.items = [movedItem]; } updateSlotDOM(targetSlot.id); updateSlotDOM(source.slot.id); } } else { if (targetItem) { showToast("Слот уже занят.", 'error'); } else { const newItem = isFavoriteDrag ? createItemFromBlueprint(favoriteItemData) : createItem(path); if (newItem) { targetSlot.items = [newItem]; updateSlotDOM(targetSlot.id); } } } requestSave(); } } }
else if (isDeletionDrop && draggedItemId) { const source = findItemById(draggedItemId); if (source) { const { item, slot, container } = source; if (container) { container.contents = container.contents.filter(i => i.id !== item.id); if (AppState.contentsModal.visible) renderContentsModal(); updateSlotDOM(findSlot(slot.id).id); } else { slot.items = []; updateSlotDOM(slot.id); } requestSave(); showToast("Предмет удален.", 'info'); } }
cleanUpDragState();
}
function cleanUpDragState() { cancelLongPress(); document.body.classList.remove('touch-drag-active'); document.getElementById('dropZone').classList.remove('visible', 'dragover'); if (AppState.dragging.lastHoveredElement) { AppState.dragging.lastHoveredElement.classList.remove('dragover-active', 'dragover'); } if (AppState.dragging.ghost) { AppState.dragging.ghost.remove(); } window.removeEventListener('touchmove', handleDragMove); window.removeEventListener('touchend', handleTouchEnd); window.removeEventListener('touchcancel', handleTouchEnd); AppState.dragging = { isTouch: false, draggedItemId: null, path: null, isFavoriteDrag: false, favoriteItemData: null, targetSlotId: null, isDeletionDrop: false, longPressTimeout: null, ghost: null, lastHoveredElement: null, touchStartX: 0, touchStartY: 0 }; }
function handleDragOver(event) { event.preventDefault(); event.currentTarget.classList.add('dragover-active'); }
function handleDragLeave(event) { event.preventDefault(); event.currentTarget.classList.remove('dragover-active'); }
function handleDropOnSlot(event) { event.preventDefault(); event.currentTarget.classList.remove('dragover-active'); const targetSlotId = event.currentTarget.closest('[data-slot-id]').dataset.slotId; if (targetSlotId) AppState.dragging.targetSlotId = targetSlotId; }
function handleDropOnZone(event) { event.preventDefault(); event.currentTarget.classList.remove('dragover'); AppState.dragging.isDeletionDrop = true; }
function applyTheme(themeName, isInitialLoad = false) {
localStorage.setItem('phoenixTheme', themeName);
let effectiveTheme = themeName;
if (themeName === 'system') {
effectiveTheme = window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light';
}
document.body.className = `theme-${effectiveTheme}`;
document.getElementById('themeSelector').value = themeName;
if (!isInitialLoad) {
showToast(`Тема изменена на: ${themeName === 'system' ? 'Системная' : themeName === 'light' ? 'Светлая' : themeName === 'dark' ? 'Темная' : 'Сепия'}`, 'info');
}
}
function bindCoreEventListeners() {
document.getElementById('selectFolderBtn').addEventListener('click', () => document.getElementById('folderInput').click());
document.getElementById('importBtn').addEventListener('click', () => document.getElementById('importInput').click());
document.getElementById('folderInput').addEventListener('change', handleFolderSelection);
document.getElementById('importInput').addEventListener('change', handleImportAction);
document.getElementById('importSetsInput').addEventListener('change', handleImportSets);
document.getElementById('exportSelector').addEventListener('change', handleExportAction);
document.getElementById('factoryResetBtn').addEventListener('click', openResetModal);
document.getElementById('helpBtn').addEventListener('click', () => TourManager.start());
document.getElementById('themeSelector').addEventListener('change', e => applyTheme(e.target.value));
setupSidebarResizing();
let searchTimeout;
document.getElementById('resourcesSearchInput').addEventListener('input', e => { clearTimeout(searchTimeout); searchTimeout = setTimeout(() => { AppState.activeSearchTerm = e.target.value; renderFileExplorer(); }, 300); });
let favSearchTimeout;
document.getElementById('favoritesSearchInput').addEventListener('input', e => { clearTimeout(favSearchTimeout); favSearchTimeout = setTimeout(() => { AppState.activeFavoritesSearchTerm = e.target.value; renderFavoritesList(); }, 300); });
document.getElementById('favoritesSort').addEventListener('change', e => { AppState.activeFavoritesSort = e.target.value; renderFavoritesList(); });
window.matchMedia('(prefers-color-scheme: dark)').addEventListener('change', () => { if (localStorage.getItem('phoenixTheme') === 'system') { applyTheme('system'); } });
document.getElementById('editModal').querySelector('.modal-close-button').addEventListener('click', closeEditModal);
document.getElementById('modalCancel').addEventListener('click', closeEditModal);
document.getElementById('modalSave').addEventListener('click', handleSaveItemAction);
document.getElementById('contentsModal').querySelector('.modal-close-button').addEventListener('click', closeContentsModal);
document.getElementById('resetModal').querySelector('.modal-close-button').addEventListener('click', closeResetModal);
document.getElementById('resetCancel').addEventListener('click', closeResetModal);
document.getElementById('resetConfirm').addEventListener('click', executeReset);
ImageViewer.init();
document.getElementById('setNameModal').querySelector('.modal-close-button').addEventListener('click', closeSetNameModal);
document.getElementById('setNameCancel').addEventListener('click', closeSetNameModal);
document.getElementById('setNameSave').addEventListener('click', handleSetNameSave);
document.querySelectorAll('.tab-button').forEach(button => button.addEventListener('click', e => { document.querySelectorAll('.tab-button').forEach(btn => btn.classList.remove('active')); document.querySelectorAll('.tab-content').forEach(content => content.classList.remove('active')); e.target.classList.add('active'); document.getElementById(e.target.dataset.tab)?.classList.add('active'); updateTabMarker(); }));
const charNameEl = document.getElementById('characterName');
charNameEl.addEventListener('click', handleCharacterNameClick);
charNameEl.addEventListener('blur', () => { charNameEl.contentEditable = 'false'; AppState.characterName = charNameEl.textContent.trim() || 'Безымянный Герой'; requestSave(); });
charNameEl.addEventListener('keydown', e => { if(e.key === 'Enter') { e.preventDefault(); charNameEl.blur(); }});
document.getElementById('headerCollapseToggle').addEventListener('click', () => {
const header = document.getElementById('characterHeader');
header.classList.toggle('collapsed');
sessionStorage.setItem('phoenixHeaderCollapsed', header.classList.contains('collapsed'));
});
document.getElementById('dropZone').addEventListener('dragover', e => { e.preventDefault(); e.currentTarget.classList.add('dragover'); });
document.getElementById('dropZone').addEventListener('dragleave', e => e.currentTarget.classList.remove('dragover'));
document.getElementById('dropZone').addEventListener('drop', handleDropOnZone);
document.addEventListener('dragend', handleDragEnd);
document.querySelectorAll('.explorer-tab-btn').forEach(button => button.addEventListener('click', () => { document.querySelectorAll('.explorer-tab-btn').forEach(btn => btn.classList.remove('active')); document.querySelectorAll('.sidebar-content').forEach(p => p.classList.remove('active')); button.classList.add('active'); document.getElementById(button.dataset.sidebarTab + 'Content')?.classList.add('active'); }));
window.addEventListener('resize', () => { updateTabMarker(); if (AppState.imageViewer.active) { ImageViewer.resetState(); } });
}
async function initializeApplication() {
console.log("Phoenix Protocol v.PRIME-FINAL.13: Systems nominal. Core layout integrity restored. All protocols stable.");
loadStateFromLocalStorage();
if (sessionStorage.getItem('phoenixHeaderCollapsed') === 'true') {
document.getElementById('characterHeader').classList.add('collapsed');
}
try {
await DBPersistence.init();
const files = await DBPersistence.loadFiles();
if (files && files.length > 0) {
console.log(`Восстановлено ${files.length} файлов из IndexedDB.`);
processLoadedFiles(files);
} else {
if (localStorage.getItem('phoenixOnboardingCompleted')) {
showToast("Для начала работы выберите папку с ресурсами.", 'info');
}
}
} catch(err) {
console.error("Не удалось инициализировать IndexedDB:", err);
showToast("Ошибка локального хранилища: " + err.message, 'error');
}
document.getElementById('characterName').textContent = AppState.characterName;
renderFullLayout();
bindCoreEventListeners();
updateSaveStatus('saved');
updateTabMarker();
TourManager.init();
}
document.addEventListener("DOMContentLoaded", initializeApplication);
</script>
</body>
</html>