big refactoring
This commit is contained in:
@@ -196,9 +196,107 @@
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
/* Language selector panel styles */
|
||||
.language-selector-panel {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.language-subtitle {
|
||||
color: #64748b;
|
||||
font-size: 16px;
|
||||
margin-bottom: 30px;
|
||||
}
|
||||
|
||||
.language-options {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 12px;
|
||||
margin-bottom: 25px;
|
||||
}
|
||||
|
||||
.language-option {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 15px 20px;
|
||||
border: 2px solid #e2e8f0;
|
||||
border-radius: 10px;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s ease;
|
||||
background-color: #f9fafb;
|
||||
}
|
||||
|
||||
.language-option:hover {
|
||||
border-color: #ff5e3a;
|
||||
background-color: #fff;
|
||||
}
|
||||
|
||||
.language-option.selected {
|
||||
border-color: #ff5e3a;
|
||||
background-color: #fff5f3;
|
||||
}
|
||||
|
||||
.language-option input[type="radio"] {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.language-radio {
|
||||
width: 22px;
|
||||
height: 22px;
|
||||
border: 2px solid #cbd5e1;
|
||||
border-radius: 50%;
|
||||
margin-right: 15px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
transition: all 0.2s ease;
|
||||
}
|
||||
|
||||
.language-option.selected .language-radio {
|
||||
border-color: #ff5e3a;
|
||||
}
|
||||
|
||||
.language-radio::after {
|
||||
content: '';
|
||||
width: 12px;
|
||||
height: 12px;
|
||||
background-color: #ff5e3a;
|
||||
border-radius: 50%;
|
||||
opacity: 0;
|
||||
transition: opacity 0.2s ease;
|
||||
}
|
||||
|
||||
.language-option.selected .language-radio::after {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.language-flag {
|
||||
font-size: 28px;
|
||||
margin-right: 15px;
|
||||
}
|
||||
|
||||
.language-name {
|
||||
font-size: 16px;
|
||||
font-weight: 500;
|
||||
color: #1e293b;
|
||||
}
|
||||
|
||||
.language-native {
|
||||
font-size: 14px;
|
||||
color: #64748b;
|
||||
margin-left: auto;
|
||||
}
|
||||
|
||||
@media (max-width: 480px) {
|
||||
.auth-panel {
|
||||
width: 90%;
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
.language-option {
|
||||
padding: 12px 15px;
|
||||
}
|
||||
|
||||
.language-flag {
|
||||
font-size: 24px;
|
||||
}
|
||||
}
|
||||
|
||||
+516
-47
@@ -13,6 +13,59 @@ body {
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
/* Custom scrollbar styling */
|
||||
::-webkit-scrollbar {
|
||||
width: 8px;
|
||||
}
|
||||
|
||||
::-webkit-scrollbar-track {
|
||||
background: transparent;
|
||||
}
|
||||
|
||||
::-webkit-scrollbar-thumb {
|
||||
background-color: rgba(0, 0, 0, 0.2);
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
::-webkit-scrollbar-thumb:hover {
|
||||
background-color: rgba(0, 0, 0, 0.3);
|
||||
}
|
||||
|
||||
/* Firefox scrollbar */
|
||||
* {
|
||||
scrollbar-width: thin;
|
||||
scrollbar-color: rgba(0, 0, 0, 0.2) transparent;
|
||||
}
|
||||
|
||||
/* Global select styling */
|
||||
select {
|
||||
padding: 8px 32px 8px 12px;
|
||||
border: 1px solid #e2e8f0;
|
||||
border-radius: 8px;
|
||||
background-color: white;
|
||||
font-size: 14px;
|
||||
color: #2d3748;
|
||||
cursor: pointer;
|
||||
appearance: none;
|
||||
-webkit-appearance: none;
|
||||
-moz-appearance: none;
|
||||
background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='12' height='12' viewBox='0 0 12 12'%3E%3Cpath fill='%23718096' d='M6 8L1 3h10z'/%3E%3C/svg%3E");
|
||||
background-repeat: no-repeat;
|
||||
background-position: right 10px center;
|
||||
transition: border-color 0.15s ease, box-shadow 0.15s ease;
|
||||
font-family: inherit;
|
||||
}
|
||||
|
||||
select:hover {
|
||||
border-color: #cbd5e0;
|
||||
}
|
||||
|
||||
select:focus {
|
||||
outline: none;
|
||||
border-color: #ff5e3a;
|
||||
box-shadow: 0 0 0 3px rgba(255, 94, 58, 0.1);
|
||||
}
|
||||
|
||||
/* Sidebar */
|
||||
.sidebar {
|
||||
width: 250px;
|
||||
@@ -131,17 +184,20 @@ body {
|
||||
/* Top bar */
|
||||
.top-bar {
|
||||
height: 70px;
|
||||
min-height: 70px;
|
||||
max-height: 70px;
|
||||
background-color: white;
|
||||
border-bottom: 1px solid #e6e6e6;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 0 30px;
|
||||
justify-content: space-between;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.search-container {
|
||||
flex-grow: 1;
|
||||
max-width: 500px;
|
||||
max-width: 600px;
|
||||
position: relative;
|
||||
margin-right: 20px;
|
||||
display: flex;
|
||||
@@ -150,40 +206,78 @@ body {
|
||||
|
||||
.search-container input {
|
||||
width: 100%;
|
||||
padding: 10px 15px 10px 40px;
|
||||
border-radius: 50px;
|
||||
border: none;
|
||||
background-color: #f0f3f7;
|
||||
padding: 12px 50px 12px 44px;
|
||||
border-radius: 12px;
|
||||
border: 2px solid #e2e8f0;
|
||||
background-color: #f8fafc;
|
||||
font-size: 14px;
|
||||
height: 40px;
|
||||
height: 46px;
|
||||
color: #1a202c;
|
||||
transition: all 0.2s ease;
|
||||
}
|
||||
|
||||
.search-container input:hover {
|
||||
border-color: #cbd5e0;
|
||||
background-color: #fff;
|
||||
}
|
||||
|
||||
.search-container input:focus {
|
||||
outline: none;
|
||||
border-color: #ff5e3a;
|
||||
background-color: #fff;
|
||||
box-shadow: 0 0 0 4px rgba(255, 94, 58, 0.1);
|
||||
}
|
||||
|
||||
.search-container input::placeholder {
|
||||
color: #a0aec0;
|
||||
}
|
||||
|
||||
.search-icon {
|
||||
position: absolute;
|
||||
left: 15px;
|
||||
left: 16px;
|
||||
top: 50%;
|
||||
transform: translateY(-50%);
|
||||
color: #8895a7;
|
||||
color: #a0aec0;
|
||||
font-size: 16px;
|
||||
pointer-events: none;
|
||||
transition: color 0.2s ease;
|
||||
}
|
||||
|
||||
.search-container input:focus + .search-icon,
|
||||
.search-container:focus-within .search-icon {
|
||||
color: #ff5e3a;
|
||||
}
|
||||
|
||||
.search-button {
|
||||
background-color: #ff5e3a;
|
||||
position: absolute;
|
||||
right: 6px;
|
||||
top: 50%;
|
||||
transform: translateY(-50%);
|
||||
background: linear-gradient(135deg, #ff5e3a 0%, #ff2d55 100%);
|
||||
color: white;
|
||||
border: none;
|
||||
border-radius: 50%;
|
||||
border-radius: 10px;
|
||||
width: 36px;
|
||||
height: 36px;
|
||||
margin-left: 8px;
|
||||
cursor: pointer;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
transition: background-color 0.2s;
|
||||
transition: all 0.2s ease;
|
||||
box-shadow: 0 2px 8px rgba(255, 94, 58, 0.3);
|
||||
}
|
||||
|
||||
.search-button:hover {
|
||||
background-color: #e64a29;
|
||||
transform: translateY(-50%) scale(1.05);
|
||||
box-shadow: 0 4px 12px rgba(255, 94, 58, 0.4);
|
||||
}
|
||||
|
||||
.search-button:active {
|
||||
transform: translateY(-50%) scale(0.98);
|
||||
}
|
||||
|
||||
.search-button i {
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
/* Styles for search results */
|
||||
@@ -217,36 +311,144 @@ body {
|
||||
.user-controls {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.logout-btn {
|
||||
margin-left: 15px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
background-color: #f8fafc;
|
||||
border: 1px solid #e2e8f0;
|
||||
border-radius: 10px;
|
||||
color: #64748b;
|
||||
cursor: pointer;
|
||||
font-size: 18px;
|
||||
transition: color 0.2s;
|
||||
font-size: 16px;
|
||||
transition: all 0.2s ease;
|
||||
}
|
||||
|
||||
.logout-btn:hover {
|
||||
background-color: #fef2f2;
|
||||
border-color: #fecaca;
|
||||
color: #ef4444;
|
||||
transform: translateY(-1px);
|
||||
box-shadow: 0 4px 12px rgba(239, 68, 68, 0.15);
|
||||
}
|
||||
|
||||
.logout-btn:active {
|
||||
transform: translateY(0);
|
||||
}
|
||||
|
||||
/* Language Selector - Custom Dropdown */
|
||||
.language-selector {
|
||||
position: relative;
|
||||
margin-right: 15px;
|
||||
}
|
||||
|
||||
.language-selector-toggle {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
padding: 8px 14px;
|
||||
background-color: #f0f3f7;
|
||||
border: 1px solid #e2e8f0;
|
||||
border-radius: 50px;
|
||||
cursor: pointer;
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
color: #4a5568;
|
||||
transition: all 0.2s ease;
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
.language-selector-toggle:hover {
|
||||
background-color: #e2e8f0;
|
||||
border-color: #cbd5e0;
|
||||
}
|
||||
|
||||
.language-selector-toggle i {
|
||||
font-size: 14px;
|
||||
color: #718096;
|
||||
}
|
||||
|
||||
.language-selector-toggle .lang-code {
|
||||
font-weight: 600;
|
||||
color: #2d3748;
|
||||
}
|
||||
|
||||
.language-selector-toggle .dropdown-arrow {
|
||||
font-size: 10px;
|
||||
color: #718096;
|
||||
transition: transform 0.2s ease;
|
||||
margin-left: 2px;
|
||||
}
|
||||
|
||||
.language-selector.open .dropdown-arrow {
|
||||
transform: rotate(180deg);
|
||||
}
|
||||
|
||||
.language-selector-dropdown {
|
||||
position: absolute;
|
||||
top: calc(100% + 8px);
|
||||
right: 0;
|
||||
min-width: 160px;
|
||||
background-color: white;
|
||||
border-radius: 12px;
|
||||
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.15);
|
||||
border: 1px solid #e2e8f0;
|
||||
opacity: 0;
|
||||
visibility: hidden;
|
||||
transform: translateY(-10px);
|
||||
transition: all 0.2s ease;
|
||||
z-index: 1000;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.language-selector.open .language-selector-dropdown {
|
||||
opacity: 1;
|
||||
visibility: visible;
|
||||
transform: translateY(0);
|
||||
}
|
||||
|
||||
.language-option {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
padding: 12px 16px;
|
||||
cursor: pointer;
|
||||
font-size: 14px;
|
||||
color: #4a5568;
|
||||
transition: background-color 0.15s ease;
|
||||
}
|
||||
|
||||
.language-option:hover {
|
||||
background-color: #f7fafc;
|
||||
}
|
||||
|
||||
.language-option.active {
|
||||
background-color: #fff5f3;
|
||||
color: #ff5e3a;
|
||||
}
|
||||
|
||||
.language-selector {
|
||||
margin-right: 15px;
|
||||
padding: 5px 12px;
|
||||
background-color: #f0f3f7;
|
||||
border-radius: 6px;
|
||||
cursor: pointer;
|
||||
font-size: 14px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
.language-option .lang-flag {
|
||||
font-size: 18px;
|
||||
line-height: 1;
|
||||
}
|
||||
|
||||
.language-selector::after {
|
||||
content: "▼";
|
||||
font-size: 8px;
|
||||
margin-left: 5px;
|
||||
color: #718096;
|
||||
.language-option .lang-name {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.language-option .lang-check {
|
||||
color: #ff5e3a;
|
||||
font-size: 12px;
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
.language-option.active .lang-check {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.user-avatar {
|
||||
@@ -265,7 +467,8 @@ body {
|
||||
.content-area {
|
||||
flex-grow: 1;
|
||||
padding: 20px;
|
||||
overflow-y: auto;
|
||||
overflow-y: scroll;
|
||||
scrollbar-gutter: stable;
|
||||
}
|
||||
|
||||
.page-title {
|
||||
@@ -283,68 +486,144 @@ body {
|
||||
|
||||
.action-buttons {
|
||||
display: flex;
|
||||
gap: 10px;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.btn {
|
||||
padding: 10px 20px;
|
||||
border-radius: 50px;
|
||||
padding: 12px 24px;
|
||||
border-radius: 12px;
|
||||
border: none;
|
||||
cursor: pointer;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
gap: 8px;
|
||||
transition: all 0.2s ease;
|
||||
}
|
||||
|
||||
.btn i {
|
||||
font-size: 15px;
|
||||
}
|
||||
|
||||
.btn-primary {
|
||||
background-color: #ff5e3a;
|
||||
background: linear-gradient(135deg, #ff5e3a 0%, #ff2d55 100%);
|
||||
color: white;
|
||||
box-shadow: 0 4px 15px rgba(255, 94, 58, 0.3);
|
||||
}
|
||||
|
||||
.btn-primary:hover {
|
||||
transform: translateY(-2px);
|
||||
box-shadow: 0 6px 20px rgba(255, 94, 58, 0.4);
|
||||
}
|
||||
|
||||
.btn-primary:active {
|
||||
transform: translateY(0);
|
||||
box-shadow: 0 2px 10px rgba(255, 94, 58, 0.3);
|
||||
}
|
||||
|
||||
.btn-secondary {
|
||||
background-color: #f0f3f7;
|
||||
color: #333;
|
||||
background-color: #f8fafc;
|
||||
color: #4a5568;
|
||||
border: 2px solid #e2e8f0;
|
||||
}
|
||||
|
||||
.btn-secondary:hover {
|
||||
background-color: #edf2f7;
|
||||
border-color: #cbd5e0;
|
||||
transform: translateY(-2px);
|
||||
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.08);
|
||||
}
|
||||
|
||||
.btn-secondary:active {
|
||||
transform: translateY(0);
|
||||
background-color: #e2e8f0;
|
||||
}
|
||||
|
||||
.btn-danger {
|
||||
background: linear-gradient(135deg, #ef4444 0%, #dc2626 100%);
|
||||
color: white;
|
||||
box-shadow: 0 4px 15px rgba(239, 68, 68, 0.3);
|
||||
}
|
||||
|
||||
.btn-danger:hover {
|
||||
transform: translateY(-2px);
|
||||
box-shadow: 0 6px 20px rgba(239, 68, 68, 0.4);
|
||||
}
|
||||
|
||||
.btn-danger:active {
|
||||
transform: translateY(0);
|
||||
box-shadow: 0 2px 10px rgba(239, 68, 68, 0.3);
|
||||
}
|
||||
|
||||
/* View Toggle Buttons */
|
||||
.view-toggle {
|
||||
display: flex;
|
||||
border-radius: 8px;
|
||||
overflow: hidden;
|
||||
gap: 6px;
|
||||
padding: 4px;
|
||||
background-color: #f0f3f7;
|
||||
border-radius: 12px;
|
||||
border: 1px solid #e2e8f0;
|
||||
}
|
||||
|
||||
.toggle-btn {
|
||||
background-color: #f0f3f7;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 40px;
|
||||
height: 36px;
|
||||
background-color: transparent;
|
||||
border: none;
|
||||
padding: 8px 15px;
|
||||
border-radius: 8px;
|
||||
cursor: pointer;
|
||||
color: #64748b;
|
||||
font-size: 16px;
|
||||
transition: all 0.2s ease;
|
||||
}
|
||||
|
||||
.toggle-btn:hover {
|
||||
background-color: #e2e8f0;
|
||||
color: #4a5568;
|
||||
}
|
||||
|
||||
.toggle-btn.active {
|
||||
background-color: #e6e6e6;
|
||||
background-color: white;
|
||||
color: #ff5e3a;
|
||||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.08);
|
||||
}
|
||||
|
||||
.toggle-btn i {
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
/* Files grid */
|
||||
.files-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fill, minmax(200px, 1fr));
|
||||
grid-template-columns: repeat(auto-fill, minmax(200px, 240px));
|
||||
gap: 20px;
|
||||
justify-content: start;
|
||||
}
|
||||
|
||||
.file-card {
|
||||
background-color: white;
|
||||
border-radius: 8px;
|
||||
border: 1px solid #e2e8f0;
|
||||
padding: 20px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
box-shadow: 0 1px 3px rgba(0,0,0,0.05);
|
||||
cursor: pointer;
|
||||
transition: transform 0.2s, box-shadow 0.2s;
|
||||
transition: transform 0.2s, box-shadow 0.2s, border-color 0.2s;
|
||||
width: 100%;
|
||||
min-height: 180px;
|
||||
}
|
||||
|
||||
.file-card:hover {
|
||||
transform: translateY(-2px);
|
||||
box-shadow: 0 5px 15px rgba(0,0,0,0.05);
|
||||
box-shadow: 0 5px 15px rgba(0,0,0,0.08);
|
||||
border-color: #cbd5e0;
|
||||
}
|
||||
|
||||
.file-card.dragging {
|
||||
@@ -888,6 +1167,176 @@ body {
|
||||
color: #718096;
|
||||
}
|
||||
|
||||
/* Modern Modal Overlay */
|
||||
.modal-overlay {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background-color: rgba(0, 0, 0, 0.5);
|
||||
display: none;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
z-index: 3000;
|
||||
opacity: 0;
|
||||
transition: opacity 0.2s ease;
|
||||
}
|
||||
|
||||
.modal-overlay.active {
|
||||
display: flex;
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.modal-container {
|
||||
background-color: white;
|
||||
border-radius: 16px;
|
||||
width: 420px;
|
||||
max-width: 90%;
|
||||
box-shadow: 0 20px 60px rgba(0, 0, 0, 0.3);
|
||||
transform: scale(0.9) translateY(-20px);
|
||||
transition: transform 0.2s ease;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.modal-overlay.active .modal-container {
|
||||
transform: scale(1) translateY(0);
|
||||
}
|
||||
|
||||
.modal-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 20px 24px;
|
||||
border-bottom: 1px solid #e2e8f0;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.modal-icon {
|
||||
width: 44px;
|
||||
height: 44px;
|
||||
background: linear-gradient(135deg, #ff5e3a 0%, #ff2d55 100%);
|
||||
border-radius: 12px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
margin-right: 14px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.modal-icon i {
|
||||
color: white;
|
||||
font-size: 20px;
|
||||
}
|
||||
|
||||
.modal-header h3 {
|
||||
font-size: 18px;
|
||||
font-weight: 600;
|
||||
color: #1a202c;
|
||||
margin: 0;
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.modal-close-btn {
|
||||
position: absolute;
|
||||
top: 16px;
|
||||
right: 16px;
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
border: none;
|
||||
background: #f0f3f7;
|
||||
border-radius: 8px;
|
||||
cursor: pointer;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
color: #64748b;
|
||||
transition: all 0.15s ease;
|
||||
}
|
||||
|
||||
.modal-close-btn:hover {
|
||||
background: #e2e8f0;
|
||||
color: #1a202c;
|
||||
}
|
||||
|
||||
.modal-body {
|
||||
padding: 24px;
|
||||
}
|
||||
|
||||
.modal-body label {
|
||||
display: block;
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
color: #4a5568;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.modal-input {
|
||||
width: 100%;
|
||||
padding: 12px 16px;
|
||||
font-size: 15px;
|
||||
border: 2px solid #e2e8f0;
|
||||
border-radius: 10px;
|
||||
background: #f8fafc;
|
||||
color: #1a202c;
|
||||
transition: all 0.15s ease;
|
||||
outline: none;
|
||||
}
|
||||
|
||||
.modal-input:focus {
|
||||
border-color: #ff5e3a;
|
||||
background: white;
|
||||
box-shadow: 0 0 0 3px rgba(255, 94, 58, 0.1);
|
||||
}
|
||||
|
||||
.modal-input::placeholder {
|
||||
color: #a0aec0;
|
||||
}
|
||||
|
||||
.modal-footer {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
gap: 12px;
|
||||
padding: 16px 24px;
|
||||
background: #f8fafc;
|
||||
border-top: 1px solid #e2e8f0;
|
||||
}
|
||||
|
||||
.modal-footer .btn {
|
||||
padding: 10px 20px;
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
border-radius: 10px;
|
||||
cursor: pointer;
|
||||
transition: all 0.15s ease;
|
||||
}
|
||||
|
||||
.modal-footer .btn-secondary {
|
||||
background: white;
|
||||
border: 1px solid #e2e8f0;
|
||||
color: #4a5568;
|
||||
}
|
||||
|
||||
.modal-footer .btn-secondary:hover {
|
||||
background: #f0f3f7;
|
||||
border-color: #cbd5e0;
|
||||
}
|
||||
|
||||
.modal-footer .btn-primary {
|
||||
background: linear-gradient(135deg, #ff5e3a 0%, #ff2d55 100%);
|
||||
border: none;
|
||||
color: white;
|
||||
box-shadow: 0 2px 8px rgba(255, 94, 58, 0.3);
|
||||
}
|
||||
|
||||
.modal-footer .btn-primary:hover {
|
||||
box-shadow: 0 4px 12px rgba(255, 94, 58, 0.4);
|
||||
transform: translateY(-1px);
|
||||
}
|
||||
|
||||
.modal-footer .btn-primary:active {
|
||||
transform: translateY(0);
|
||||
}
|
||||
|
||||
/* Dialogs (Rename, Move, Share) */
|
||||
.rename-dialog, .share-dialog {
|
||||
position: fixed;
|
||||
@@ -1104,14 +1553,34 @@ header {
|
||||
.filter-group label {
|
||||
font-size: 14px;
|
||||
color: #4a5568;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.filter-group select {
|
||||
padding: 8px 12px;
|
||||
padding: 8px 32px 8px 12px;
|
||||
border: 1px solid #e2e8f0;
|
||||
border-radius: 6px;
|
||||
border-radius: 8px;
|
||||
background-color: white;
|
||||
font-size: 14px;
|
||||
color: #2d3748;
|
||||
cursor: pointer;
|
||||
appearance: none;
|
||||
-webkit-appearance: none;
|
||||
-moz-appearance: none;
|
||||
background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='12' height='12' viewBox='0 0 12 12'%3E%3Cpath fill='%23718096' d='M6 8L1 3h10z'/%3E%3C/svg%3E");
|
||||
background-repeat: no-repeat;
|
||||
background-position: right 10px center;
|
||||
transition: border-color 0.15s ease, box-shadow 0.15s ease;
|
||||
}
|
||||
|
||||
.filter-group select:hover {
|
||||
border-color: #cbd5e0;
|
||||
}
|
||||
|
||||
.filter-group select:focus {
|
||||
outline: none;
|
||||
border-color: #ff5e3a;
|
||||
box-shadow: 0 0 0 3px rgba(255, 94, 58, 0.1);
|
||||
}
|
||||
|
||||
.search-box {
|
||||
|
||||
+37
-9
@@ -17,6 +17,7 @@
|
||||
<!-- Scripts -->
|
||||
<script src="/js/i18n.js"></script>
|
||||
<script src="/js/languageSelector.js"></script>
|
||||
<script src="/js/modal.js"></script>
|
||||
<script src="/js/ui.js"></script>
|
||||
<script src="/js/contextMenus.js"></script>
|
||||
<script src="/js/fileOperations.js"></script>
|
||||
@@ -79,11 +80,11 @@
|
||||
</div>
|
||||
|
||||
<div class="storage-container">
|
||||
<div class="storage-title">Almacenamiento</div>
|
||||
<div class="storage-title" data-i18n="storage.title">Storage</div>
|
||||
<div class="storage-bar">
|
||||
<div class="storage-fill"></div>
|
||||
</div>
|
||||
<div class="storage-info">Calculando...</div>
|
||||
<div class="storage-info" data-i18n="storage.calculating">Calculating...</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -93,14 +94,16 @@
|
||||
<div class="top-bar">
|
||||
<div class="search-container">
|
||||
<i class="fas fa-search search-icon"></i>
|
||||
<input type="text" data-i18n-placeholder="actions.search" placeholder="Buscar archivos, carpetas...">
|
||||
<button id="search-button" class="search-button"><i class="fas fa-search"></i></button>
|
||||
<input type="text" id="search-input" data-i18n-placeholder="actions.search" placeholder="Buscar archivos, carpetas...">
|
||||
<button id="search-button" class="search-button" data-i18n-title="actions.search_btn" title="Search">
|
||||
<i class="fas fa-search"></i>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="user-controls">
|
||||
<div id="language-selector" class="language-selector">ES</div>
|
||||
<div class="user-avatar">MR</div>
|
||||
<div id="logout-btn" class="logout-btn" title="Cerrar sesión">
|
||||
<div id="language-selector"></div>
|
||||
<div class="user-avatar" id="user-avatar">AD</div>
|
||||
<div id="logout-btn" class="logout-btn" data-i18n-title="actions.logout" title="Log out">
|
||||
<i class="fas fa-sign-out-alt"></i>
|
||||
</div>
|
||||
</div>
|
||||
@@ -112,10 +115,12 @@
|
||||
<div class="actions-bar">
|
||||
<div class="action-buttons">
|
||||
<button class="btn btn-primary" id="upload-btn">
|
||||
<i class="fas fa-upload" style="margin-right: 5px;"></i> <span data-i18n="actions.upload">Subir</span>
|
||||
<i class="fas fa-cloud-upload-alt"></i>
|
||||
<span data-i18n="actions.upload">Subir</span>
|
||||
</button>
|
||||
<button class="btn btn-secondary" id="new-folder-btn">
|
||||
<i class="fas fa-folder-plus" style="margin-right: 5px;"></i> <span data-i18n="actions.new_folder">Nueva carpeta</span>
|
||||
<i class="fas fa-folder-plus"></i>
|
||||
<span data-i18n="actions.new_folder">Nueva carpeta</span>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
@@ -164,5 +169,28 @@
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Input Modal (for New Folder, Rename, etc.) -->
|
||||
<div id="input-modal" class="modal-overlay">
|
||||
<div class="modal-container">
|
||||
<div class="modal-header">
|
||||
<div class="modal-icon">
|
||||
<i id="modal-icon" class="fas fa-folder-plus"></i>
|
||||
</div>
|
||||
<h3 id="modal-title" data-i18n="dialogs.new_folder_title">Nueva carpeta</h3>
|
||||
<button class="modal-close-btn" id="modal-close-btn">
|
||||
<i class="fas fa-times"></i>
|
||||
</button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<label id="modal-label" for="modal-input" data-i18n="dialogs.folder_name">Nombre de la carpeta</label>
|
||||
<input type="text" id="modal-input" class="modal-input" placeholder="" autocomplete="off">
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button class="btn btn-secondary" id="modal-cancel-btn" data-i18n="actions.cancel">Cancelar</button>
|
||||
<button class="btn btn-primary" id="modal-confirm-btn" data-i18n="actions.confirm">Crear</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
+100
-9
@@ -181,8 +181,8 @@ function setupEventListeners() {
|
||||
});
|
||||
|
||||
// New folder button
|
||||
elements.newFolderBtn.addEventListener('click', () => {
|
||||
const folderName = prompt(window.i18n ? window.i18n.t('dialogs.new_name') : 'Nombre de la carpeta:');
|
||||
elements.newFolderBtn.addEventListener('click', async () => {
|
||||
const folderName = await window.Modal.promptNewFolder();
|
||||
if (folderName) {
|
||||
fileOps.createFolder(folderName);
|
||||
}
|
||||
@@ -256,7 +256,7 @@ function setupEventListeners() {
|
||||
elements.actionsBar.innerHTML = `
|
||||
<div class="action-buttons">
|
||||
<button class="btn btn-danger" id="empty-trash-btn">
|
||||
<i class="fas fa-trash" style="margin-right: 5px;"></i>
|
||||
<i class="fas fa-trash-alt"></i>
|
||||
<span>${window.i18n ? window.i18n.t('trash.empty_trash') : 'Vaciar papelera'}</span>
|
||||
</button>
|
||||
</div>
|
||||
@@ -330,8 +330,8 @@ function setupEventListeners() {
|
||||
}
|
||||
});
|
||||
|
||||
document.getElementById('new-folder-btn').addEventListener('click', () => {
|
||||
const folderName = prompt(window.i18n ? window.i18n.t('dialogs.new_name') : 'Nombre de la carpeta:');
|
||||
document.getElementById('new-folder-btn').addEventListener('click', async () => {
|
||||
const folderName = await window.Modal.promptNewFolder();
|
||||
if (folderName) {
|
||||
fileOps.createFolder(folderName);
|
||||
}
|
||||
@@ -899,8 +899,8 @@ function switchToFilesView() {
|
||||
}
|
||||
});
|
||||
|
||||
document.getElementById('new-folder-btn').addEventListener('click', () => {
|
||||
const folderName = prompt(window.i18n ? window.i18n.t('dialogs.new_name') : 'Nombre de la carpeta:');
|
||||
document.getElementById('new-folder-btn').addEventListener('click', async () => {
|
||||
const folderName = await window.Modal.promptNewFolder();
|
||||
if (folderName) {
|
||||
fileOps.createFolder(folderName);
|
||||
}
|
||||
@@ -1136,6 +1136,59 @@ window.switchToSharedView = switchToSharedView;
|
||||
window.switchToFavoritesView = switchToFavoritesView;
|
||||
window.switchToRecentFilesView = switchToRecentFilesView;
|
||||
|
||||
/**
|
||||
* Fetch updated user data from the server (including storage usage)
|
||||
* This calls the /api/auth/me endpoint which also triggers storage recalculation
|
||||
*/
|
||||
async function refreshUserData() {
|
||||
const TOKEN_KEY = 'oxicloud_token';
|
||||
const USER_DATA_KEY = 'oxicloud_user';
|
||||
|
||||
const token = localStorage.getItem(TOKEN_KEY);
|
||||
console.log('refreshUserData called, token:', token ? token.substring(0, 20) + '...' : 'null');
|
||||
|
||||
if (!token || token === 'mock_token_emergency_bypass' || token === 'emergency_token') {
|
||||
console.log('No valid token, skipping user data refresh');
|
||||
return null;
|
||||
}
|
||||
|
||||
try {
|
||||
console.log('Fetching /api/auth/me...');
|
||||
const response = await fetch('/api/auth/me', {
|
||||
method: 'GET',
|
||||
headers: {
|
||||
'Authorization': `Bearer ${token}`,
|
||||
'Content-Type': 'application/json'
|
||||
}
|
||||
});
|
||||
|
||||
console.log('/api/auth/me response status:', response.status);
|
||||
|
||||
if (!response.ok) {
|
||||
console.warn('Failed to fetch user data:', response.status);
|
||||
return null;
|
||||
}
|
||||
|
||||
const userData = await response.json();
|
||||
console.log('Refreshed user data from server:', userData);
|
||||
console.log('Storage from server: used=', userData.storage_used_bytes, 'quota=', userData.storage_quota_bytes);
|
||||
|
||||
// Update local storage with fresh data
|
||||
localStorage.setItem(USER_DATA_KEY, JSON.stringify(userData));
|
||||
|
||||
// Update storage display with actual values
|
||||
updateStorageUsageDisplay(userData);
|
||||
|
||||
return userData;
|
||||
} catch (error) {
|
||||
console.error('Error refreshing user data:', error);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
// Expose refreshUserData globally
|
||||
window.refreshUserData = refreshUserData;
|
||||
|
||||
/**
|
||||
* Check if user is authenticated and load user's home folder
|
||||
*/
|
||||
@@ -1192,6 +1245,22 @@ function checkAuthentication() {
|
||||
if (userAvatar) {
|
||||
userAvatar.textContent = userInitials;
|
||||
}
|
||||
|
||||
// Show cached storage first, then try to refresh from server
|
||||
updateStorageUsageDisplay(userData);
|
||||
|
||||
// Try to get updated storage from server (if we have a real token)
|
||||
const token = localStorage.getItem(TOKEN_KEY);
|
||||
if (token && token !== 'mock_token_emergency_bypass' && token !== 'emergency_token') {
|
||||
console.log('Bypass mode: Attempting to refresh storage from server...');
|
||||
refreshUserData().then(freshData => {
|
||||
if (freshData) {
|
||||
console.log('Bypass mode: Storage updated from server');
|
||||
}
|
||||
}).catch(err => {
|
||||
console.warn('Bypass mode: Could not refresh user data:', err);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// Reset all counters to prevent loops
|
||||
@@ -1238,9 +1307,19 @@ function checkAuthentication() {
|
||||
userAvatar.textContent = userInitials;
|
||||
}
|
||||
|
||||
// Update storage usage information
|
||||
// Update storage usage information with cached data first (for fast display)
|
||||
updateStorageUsageDisplay(userData);
|
||||
|
||||
// Then refresh user data from server in the background to get updated storage
|
||||
// This triggers the backend to recalculate storage and returns fresh data
|
||||
refreshUserData().then(freshData => {
|
||||
if (freshData) {
|
||||
console.log('Storage usage updated from server');
|
||||
}
|
||||
}).catch(err => {
|
||||
console.warn('Could not refresh user data:', err);
|
||||
});
|
||||
|
||||
// Find and load the user's home folder
|
||||
findUserHomeFolder(userData.username);
|
||||
} else {
|
||||
@@ -1506,7 +1585,19 @@ function updateStorageUsageDisplay(userData) {
|
||||
}
|
||||
|
||||
if (storageInfo) {
|
||||
storageInfo.textContent = `${usagePercentage}% usado (${usedFormatted} / ${quotaFormatted})`;
|
||||
// Remove data-i18n attribute to prevent i18n from overwriting our value
|
||||
storageInfo.removeAttribute('data-i18n');
|
||||
|
||||
// Use i18n if available
|
||||
if (window.i18n && window.i18n.t) {
|
||||
storageInfo.textContent = window.i18n.t('storage.used', {
|
||||
percentage: usagePercentage,
|
||||
used: usedFormatted,
|
||||
total: quotaFormatted
|
||||
});
|
||||
} else {
|
||||
storageInfo.textContent = `${usagePercentage}% used (${usedFormatted} / ${quotaFormatted})`;
|
||||
}
|
||||
}
|
||||
|
||||
console.log(`Updated storage display: ${usagePercentage}% (${usedFormatted} / ${quotaFormatted})`);
|
||||
|
||||
+220
-11
@@ -15,9 +15,189 @@ const TOKEN_KEY = 'oxicloud_token';
|
||||
const REFRESH_TOKEN_KEY = 'oxicloud_refresh_token';
|
||||
const TOKEN_EXPIRY_KEY = 'oxicloud_token_expiry';
|
||||
const USER_DATA_KEY = 'oxicloud_user';
|
||||
const LOCALE_KEY = 'oxicloud-locale';
|
||||
const FIRST_RUN_KEY = 'oxicloud_first_run_completed';
|
||||
|
||||
// Language selector texts (used before i18n is loaded)
|
||||
const LANGUAGE_TEXTS = {
|
||||
en: {
|
||||
title: 'Welcome to OxiCloud',
|
||||
subtitle: 'Please select your language',
|
||||
continue: 'Continue'
|
||||
},
|
||||
es: {
|
||||
title: 'Bienvenido a OxiCloud',
|
||||
subtitle: 'Por favor, selecciona tu idioma',
|
||||
continue: 'Continuar'
|
||||
},
|
||||
zh: {
|
||||
title: '欢迎使用 OxiCloud',
|
||||
subtitle: '请选择您的语言',
|
||||
continue: '继续'
|
||||
}
|
||||
};
|
||||
|
||||
// Check if this is a first run (no locale saved)
|
||||
function isFirstRun() {
|
||||
return !localStorage.getItem(LOCALE_KEY);
|
||||
}
|
||||
|
||||
// Check system status from the server
|
||||
async function checkSystemStatus() {
|
||||
try {
|
||||
const response = await fetch('/api/auth/status');
|
||||
if (!response.ok) {
|
||||
console.warn('Could not check system status, assuming initialized');
|
||||
return { initialized: true, admin_count: 1, registration_allowed: true };
|
||||
}
|
||||
return await response.json();
|
||||
} catch (error) {
|
||||
console.error('Error checking system status:', error);
|
||||
return { initialized: true, admin_count: 1, registration_allowed: true };
|
||||
}
|
||||
}
|
||||
|
||||
// Initialize language selector panel
|
||||
function initLanguageSelector() {
|
||||
const languagePanel = document.getElementById('language-panel');
|
||||
const languageOptions = document.querySelectorAll('.language-option');
|
||||
const continueBtn = document.getElementById('language-continue');
|
||||
let selectedLanguage = null;
|
||||
|
||||
if (!languagePanel) return;
|
||||
|
||||
// Handle language option clicks
|
||||
languageOptions.forEach(option => {
|
||||
option.addEventListener('click', () => {
|
||||
// Remove selected class from all options
|
||||
languageOptions.forEach(opt => opt.classList.remove('selected'));
|
||||
// Add selected class to clicked option
|
||||
option.classList.add('selected');
|
||||
// Check the radio button
|
||||
option.querySelector('input[type="radio"]').checked = true;
|
||||
// Store selected language
|
||||
selectedLanguage = option.getAttribute('data-lang');
|
||||
// Enable continue button
|
||||
continueBtn.disabled = false;
|
||||
|
||||
// Update UI texts based on selected language
|
||||
updateLanguagePanelTexts(selectedLanguage);
|
||||
});
|
||||
});
|
||||
|
||||
// Handle continue button click
|
||||
continueBtn.addEventListener('click', async () => {
|
||||
if (!selectedLanguage) return;
|
||||
|
||||
// Save locale preference
|
||||
localStorage.setItem(LOCALE_KEY, selectedLanguage);
|
||||
localStorage.setItem(FIRST_RUN_KEY, 'true');
|
||||
|
||||
// Update i18n if available
|
||||
if (window.i18n && window.i18n.setLocale) {
|
||||
await window.i18n.setLocale(selectedLanguage);
|
||||
}
|
||||
|
||||
// Hide language panel
|
||||
languagePanel.style.display = 'none';
|
||||
|
||||
// Check system status to determine which panel to show
|
||||
const systemStatus = await checkSystemStatus();
|
||||
console.log('System status after language selection:', systemStatus);
|
||||
|
||||
if (!systemStatus.initialized) {
|
||||
// No admin exists - show admin setup
|
||||
console.log('No admin exists, showing admin setup panel');
|
||||
document.getElementById('login-panel').style.display = 'none';
|
||||
document.getElementById('register-panel').style.display = 'none';
|
||||
document.getElementById('admin-setup-panel').style.display = 'block';
|
||||
|
||||
// Hide the "Already set up? Sign in" link
|
||||
const backToLoginLink = document.getElementById('back-to-login');
|
||||
if (backToLoginLink) {
|
||||
backToLoginLink.parentElement.style.display = 'none';
|
||||
}
|
||||
} else {
|
||||
// Admin exists - show login panel
|
||||
document.getElementById('login-panel').style.display = 'block';
|
||||
}
|
||||
|
||||
// Translate the page with new locale
|
||||
if (window.i18n && window.i18n.translatePage) {
|
||||
window.i18n.translatePage();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Update language panel texts based on selected language
|
||||
function updateLanguagePanelTexts(lang) {
|
||||
const texts = LANGUAGE_TEXTS[lang] || LANGUAGE_TEXTS.en;
|
||||
const titleEl = document.getElementById('language-title');
|
||||
const subtitleEl = document.getElementById('language-subtitle');
|
||||
const continueBtn = document.getElementById('language-continue');
|
||||
|
||||
if (titleEl) titleEl.textContent = texts.title;
|
||||
if (subtitleEl) subtitleEl.textContent = texts.subtitle;
|
||||
if (continueBtn) continueBtn.textContent = texts.continue;
|
||||
}
|
||||
|
||||
// Show appropriate panel based on system status and first run
|
||||
async function showInitialPanel() {
|
||||
const languagePanel = document.getElementById('language-panel');
|
||||
const loginPanel = document.getElementById('login-panel');
|
||||
const adminSetupPanel = document.getElementById('admin-setup-panel');
|
||||
const registerPanel = document.getElementById('register-panel');
|
||||
|
||||
if (!languagePanel || !loginPanel) return;
|
||||
|
||||
// ALWAYS check if this is user's first run (language selection) FIRST
|
||||
// Language selection should happen before anything else
|
||||
if (isFirstRun()) {
|
||||
// First run - show language selector first
|
||||
// After language is selected, the continue button handler will check system status
|
||||
console.log('First run - showing language selector');
|
||||
languagePanel.style.display = 'block';
|
||||
loginPanel.style.display = 'none';
|
||||
registerPanel.style.display = 'none';
|
||||
adminSetupPanel.style.display = 'none';
|
||||
return;
|
||||
}
|
||||
|
||||
// Language already selected - now check system status
|
||||
const systemStatus = await checkSystemStatus();
|
||||
console.log('System status:', systemStatus);
|
||||
|
||||
if (!systemStatus.initialized) {
|
||||
// No admin exists - this is a fresh install, show admin setup
|
||||
console.log('Fresh install detected - showing admin setup');
|
||||
languagePanel.style.display = 'none';
|
||||
loginPanel.style.display = 'none';
|
||||
registerPanel.style.display = 'none';
|
||||
adminSetupPanel.style.display = 'block';
|
||||
|
||||
// Hide the "Already set up? Sign in" link since there's no admin yet
|
||||
const backToLoginLink = document.getElementById('back-to-login');
|
||||
if (backToLoginLink) {
|
||||
backToLoginLink.parentElement.style.display = 'none';
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
// System is initialized - show login panel
|
||||
languagePanel.style.display = 'none';
|
||||
loginPanel.style.display = 'block';
|
||||
registerPanel.style.display = 'none';
|
||||
adminSetupPanel.style.display = 'none';
|
||||
|
||||
// Hide the admin setup link if admin already exists
|
||||
const showAdminSetupLink = document.getElementById('show-admin-setup');
|
||||
if (showAdminSetupLink && systemStatus.admin_count > 0) {
|
||||
showAdminSetupLink.parentElement.style.display = 'none';
|
||||
}
|
||||
}
|
||||
|
||||
// DOM elements
|
||||
let loginPanel, registerPanel, adminSetupPanel;
|
||||
let loginPanel, registerPanel, adminSetupPanel, languagePanel;
|
||||
let loginForm, registerForm, adminSetupForm;
|
||||
let loginError, registerError, registerSuccess, adminSetupError;
|
||||
|
||||
@@ -29,6 +209,7 @@ function initLoginElements() {
|
||||
return false;
|
||||
}
|
||||
|
||||
languagePanel = document.getElementById('language-panel');
|
||||
loginPanel = document.getElementById('login-panel');
|
||||
registerPanel = document.getElementById('register-panel');
|
||||
adminSetupPanel = document.getElementById('admin-setup-panel');
|
||||
@@ -41,6 +222,9 @@ function initLoginElements() {
|
||||
registerError = document.getElementById('register-error');
|
||||
registerSuccess = document.getElementById('register-success');
|
||||
adminSetupError = document.getElementById('admin-setup-error');
|
||||
|
||||
// Initialize language selector
|
||||
initLanguageSelector();
|
||||
|
||||
// Panel toggles
|
||||
document.getElementById('show-register').addEventListener('click', () => {
|
||||
@@ -138,6 +322,14 @@ document.addEventListener('DOMContentLoaded', () => {
|
||||
}
|
||||
authInitialized = true;
|
||||
|
||||
// Show appropriate panel (language selector on first run, login otherwise, or admin setup if no admin)
|
||||
// This is async so we call it and let it run
|
||||
showInitialPanel().then(() => {
|
||||
console.log('Initial panel shown based on system status');
|
||||
}).catch(err => {
|
||||
console.error('Error showing initial panel:', err);
|
||||
});
|
||||
|
||||
// Siempre limpiar los contadores al cargar la página de login
|
||||
// para asegurar que no quedamos atrapados en un bucle
|
||||
console.log('Login page loaded, clearing all counters');
|
||||
@@ -308,7 +500,8 @@ if (isLoginPage && registerForm) {
|
||||
|
||||
// Validate passwords match
|
||||
if (password !== confirmPassword) {
|
||||
registerError.textContent = 'Las contraseñas no coinciden';
|
||||
const errorMsg = window.i18n ? window.i18n.t('auth.passwords_mismatch') : 'Las contraseñas no coinciden';
|
||||
registerError.textContent = errorMsg;
|
||||
registerError.style.display = 'block';
|
||||
return;
|
||||
}
|
||||
@@ -317,7 +510,8 @@ if (isLoginPage && registerForm) {
|
||||
const data = await register(username, email, password);
|
||||
|
||||
// Show success message
|
||||
registerSuccess.textContent = '¡Cuenta creada con éxito! Puedes iniciar sesión ahora.';
|
||||
const successMsg = window.i18n ? window.i18n.t('auth.account_success') : '¡Cuenta creada con éxito! Puedes iniciar sesión ahora.';
|
||||
registerSuccess.textContent = successMsg;
|
||||
registerSuccess.style.display = 'block';
|
||||
|
||||
// Clear form
|
||||
@@ -329,7 +523,8 @@ if (isLoginPage && registerForm) {
|
||||
registerPanel.style.display = 'none';
|
||||
}, 2000);
|
||||
} catch (error) {
|
||||
registerError.textContent = error.message || 'Error al registrar cuenta';
|
||||
const errorMsg = window.i18n ? window.i18n.t('auth.admin_create_error') : 'Error al registrar cuenta';
|
||||
registerError.textContent = error.message || errorMsg;
|
||||
registerError.style.display = 'block';
|
||||
}
|
||||
});
|
||||
@@ -340,8 +535,10 @@ if (isLoginPage && adminSetupForm) {
|
||||
adminSetupForm.addEventListener('submit', async (e) => {
|
||||
e.preventDefault();
|
||||
|
||||
// Clear previous errors
|
||||
// Clear previous errors/success messages
|
||||
adminSetupError.style.display = 'none';
|
||||
const adminSetupSuccess = document.getElementById('admin-setup-success');
|
||||
if (adminSetupSuccess) adminSetupSuccess.style.display = 'none';
|
||||
|
||||
const email = document.getElementById('admin-email').value;
|
||||
const password = document.getElementById('admin-password').value;
|
||||
@@ -349,7 +546,8 @@ if (isLoginPage && adminSetupForm) {
|
||||
|
||||
// Validate passwords match
|
||||
if (password !== confirmPassword) {
|
||||
adminSetupError.textContent = 'Las contraseñas no coinciden';
|
||||
const errorMsg = window.i18n ? window.i18n.t('auth.passwords_mismatch') : 'Las contraseñas no coinciden';
|
||||
adminSetupError.textContent = errorMsg;
|
||||
adminSetupError.style.display = 'block';
|
||||
return;
|
||||
}
|
||||
@@ -358,13 +556,24 @@ if (isLoginPage && adminSetupForm) {
|
||||
// Register admin account
|
||||
const data = await register('admin', email, password, 'admin');
|
||||
|
||||
// Show success and switch to login
|
||||
alert('¡Cuenta de administrador creada con éxito! Ahora puedes iniciar sesión.');
|
||||
// Show success message in the GUI instead of alert
|
||||
const successMsg = window.i18n ? window.i18n.t('auth.admin_success') : '¡Cuenta de administrador creada con éxito! Ahora puedes iniciar sesión.';
|
||||
|
||||
if (adminSetupSuccess) {
|
||||
adminSetupSuccess.textContent = successMsg;
|
||||
adminSetupSuccess.style.display = 'block';
|
||||
}
|
||||
|
||||
// Wait 2 seconds then switch to login panel
|
||||
setTimeout(() => {
|
||||
loginPanel.style.display = 'block';
|
||||
adminSetupPanel.style.display = 'none';
|
||||
if (adminSetupSuccess) adminSetupSuccess.style.display = 'none';
|
||||
}, 2000);
|
||||
|
||||
loginPanel.style.display = 'block';
|
||||
adminSetupPanel.style.display = 'none';
|
||||
} catch (error) {
|
||||
adminSetupError.textContent = error.message || 'Error al crear cuenta de administrador';
|
||||
const errorMsg = window.i18n ? window.i18n.t('auth.admin_create_error') : 'Error al crear cuenta de administrador';
|
||||
adminSetupError.textContent = error.message || errorMsg;
|
||||
adminSetupError.style.display = 'block';
|
||||
}
|
||||
});
|
||||
|
||||
+25
-53
@@ -21,20 +21,26 @@ const fileOps = {
|
||||
for (let i = 0; i < totalFiles; i++) {
|
||||
const file = files[i];
|
||||
const formData = new FormData();
|
||||
|
||||
// IMPORTANT: folder_id MUST be added BEFORE file for multipart processing
|
||||
// The backend reads fields in order, and needs folder_id before processing the file
|
||||
const targetFolderId = window.app.currentPath || window.app.userHomeFolderId;
|
||||
|
||||
if (targetFolderId) {
|
||||
formData.append('folder_id', targetFolderId);
|
||||
}
|
||||
|
||||
// Add the file AFTER folder_id
|
||||
formData.append('file', file);
|
||||
|
||||
if (window.app.currentPath) {
|
||||
formData.append('folder_id', window.app.currentPath);
|
||||
}
|
||||
|
||||
try {
|
||||
console.log(`Uploading file to current path: ${window.app.currentPath || 'root'}`);
|
||||
console.log(`Uploading file to folder: ${targetFolderId || 'root'}`);
|
||||
|
||||
// Usamos la URL correcta para la subida de archivos
|
||||
console.log('Formulario a enviar:', {
|
||||
file: file.name,
|
||||
size: file.size,
|
||||
folder_id: window.app.currentPath || 'root'
|
||||
folder_id: targetFolderId || 'root'
|
||||
});
|
||||
|
||||
const response = await fetch('/api/files/upload', {
|
||||
@@ -62,63 +68,29 @@ const fileOps = {
|
||||
const responseData = await response.json();
|
||||
console.log(`Successfully uploaded ${file.name}`, responseData);
|
||||
|
||||
// Agregar el archivo a la vista inmediatamente para mostrar retroalimentación instantánea
|
||||
// Esto permite que el usuario vea el archivo aunque el refresco posterior falle
|
||||
if (window.ui && window.ui.addFileToView) {
|
||||
console.log('Añadiendo archivo subido directamente a la vista:', responseData);
|
||||
window.ui.addFileToView(responseData);
|
||||
window.ui.updateFileIcons();
|
||||
}
|
||||
// Show success notification immediately
|
||||
window.ui.showNotification('Archivo subido', `${file.name} completado`);
|
||||
|
||||
if (i === totalFiles - 1) {
|
||||
// Last file uploaded
|
||||
console.log('Recargando lista de archivos después de subida');
|
||||
// Last file uploaded - wait and reload once
|
||||
console.log('Último archivo subido, esperando antes de recargar...');
|
||||
|
||||
// Wait for backend to persist
|
||||
await new Promise(resolve => setTimeout(resolve, 800));
|
||||
|
||||
// Single reload with force refresh
|
||||
try {
|
||||
// Esperar 1500ms para asegurar que los mapeos de ID se guarden
|
||||
// Tiempo aumentado significativamente para permitir al backend completar persistencia
|
||||
await new Promise(resolve => setTimeout(resolve, 1500));
|
||||
|
||||
// Forzar recarga de archivos con parámetro de bypass de caché
|
||||
const timestamp = new Date().getTime();
|
||||
const filesUrl = `/api/files?t=${timestamp}&folder_id=${window.app.currentPath || ''}`;
|
||||
|
||||
console.log(`Recargando lista de archivos desde: ${filesUrl}`);
|
||||
const filesResponse = await fetch(filesUrl, {
|
||||
cache: 'no-store',
|
||||
headers: {
|
||||
'Cache-Control': 'no-cache, no-store, must-revalidate',
|
||||
'Pragma': 'no-cache'
|
||||
}
|
||||
});
|
||||
|
||||
if (filesResponse.ok) {
|
||||
const files = await filesResponse.json();
|
||||
console.log(`Recarga de archivos completada, obtenidos ${files.length} archivos`);
|
||||
}
|
||||
|
||||
// Esperar un momento más antes de la recarga final
|
||||
await new Promise(resolve => setTimeout(resolve, 500));
|
||||
|
||||
// Hacer una única recarga final forzando refresco completo
|
||||
await window.loadFiles({forceRefresh: true});
|
||||
|
||||
// Asegurarse de que no haya recargas adicionales o duplicados
|
||||
} catch (reloadError) {
|
||||
console.error("Error durante recarga de archivos:", reloadError);
|
||||
// Intentar recargar lista de nuevo en caso de error
|
||||
await window.loadFiles({forceRefresh: true});
|
||||
// Segundo intento con retraso y forzando refresco
|
||||
setTimeout(() => window.loadFiles({forceRefresh: true}), 500);
|
||||
console.error("Error recargando archivos:", reloadError);
|
||||
}
|
||||
|
||||
// Hide upload UI
|
||||
setTimeout(() => {
|
||||
document.getElementById('dropzone').style.display = 'none';
|
||||
const dropzone = document.getElementById('dropzone');
|
||||
if (dropzone) dropzone.style.display = 'none';
|
||||
uploadProgressDiv.style.display = 'none';
|
||||
}, 1000);
|
||||
|
||||
// Show success notification
|
||||
window.ui.showNotification('Archivo subido', `${file.name} completado`);
|
||||
}, 500);
|
||||
}
|
||||
} else {
|
||||
const errorData = await response.text();
|
||||
|
||||
+141
-28
@@ -1,16 +1,17 @@
|
||||
/**
|
||||
* Language Selector Component for OxiCloud
|
||||
* Custom styled dropdown with flags
|
||||
*/
|
||||
|
||||
// Language codes and names
|
||||
// Language codes, names, and flag emojis
|
||||
const languages = [
|
||||
{ code: 'en', name: 'English' },
|
||||
{ code: 'es', name: 'Español' },
|
||||
{ code: 'zh', name: '中文' }
|
||||
{ code: 'en', name: 'English', flag: '🇬🇧' },
|
||||
{ code: 'es', name: 'Español', flag: '🇪🇸' },
|
||||
{ code: 'zh', name: '中文', flag: '🇨🇳' }
|
||||
];
|
||||
|
||||
/**
|
||||
* Creates and initializes a language selector component
|
||||
* Creates and initializes a custom language selector component
|
||||
* @param {string} containerId - ID of the container element
|
||||
*/
|
||||
function createLanguageSelector(containerId = 'language-selector') {
|
||||
@@ -23,45 +24,157 @@ function createLanguageSelector(containerId = 'language-selector') {
|
||||
document.body.appendChild(container);
|
||||
}
|
||||
|
||||
// Create dropdown
|
||||
const select = document.createElement('select');
|
||||
select.className = 'language-select';
|
||||
select.setAttribute('aria-label', 'Select language');
|
||||
// Ensure container has the right class
|
||||
container.className = 'language-selector';
|
||||
|
||||
// Add options
|
||||
// Get current language
|
||||
const currentLocale = window.i18n ? window.i18n.getCurrentLocale() : 'en';
|
||||
const currentLang = languages.find(l => l.code === currentLocale) || languages[0];
|
||||
|
||||
// Create toggle button
|
||||
const toggle = document.createElement('div');
|
||||
toggle.className = 'language-selector-toggle';
|
||||
toggle.setAttribute('role', 'button');
|
||||
toggle.setAttribute('aria-haspopup', 'listbox');
|
||||
toggle.setAttribute('aria-expanded', 'false');
|
||||
toggle.setAttribute('tabindex', '0');
|
||||
toggle.innerHTML = `
|
||||
<i class="fas fa-globe"></i>
|
||||
<span class="lang-code">${currentLang.code.toUpperCase()}</span>
|
||||
<i class="fas fa-chevron-down dropdown-arrow"></i>
|
||||
`;
|
||||
|
||||
// Create dropdown menu
|
||||
const dropdown = document.createElement('div');
|
||||
dropdown.className = 'language-selector-dropdown';
|
||||
dropdown.setAttribute('role', 'listbox');
|
||||
|
||||
// Add language options
|
||||
languages.forEach(lang => {
|
||||
const option = document.createElement('option');
|
||||
option.value = lang.code;
|
||||
option.textContent = lang.name;
|
||||
select.appendChild(option);
|
||||
const option = document.createElement('div');
|
||||
option.className = `language-option${lang.code === currentLocale ? ' active' : ''}`;
|
||||
option.setAttribute('role', 'option');
|
||||
option.setAttribute('data-lang', lang.code);
|
||||
option.setAttribute('aria-selected', lang.code === currentLocale);
|
||||
option.innerHTML = `
|
||||
<span class="lang-flag">${lang.flag}</span>
|
||||
<span class="lang-name">${lang.name}</span>
|
||||
<i class="fas fa-check lang-check"></i>
|
||||
`;
|
||||
|
||||
option.addEventListener('click', async (e) => {
|
||||
e.stopPropagation();
|
||||
await selectLanguage(lang.code, container);
|
||||
});
|
||||
|
||||
dropdown.appendChild(option);
|
||||
});
|
||||
|
||||
// Set current language
|
||||
const currentLocale = window.i18n ? window.i18n.getCurrentLocale() : 'en';
|
||||
select.value = currentLocale;
|
||||
// Clear and build container
|
||||
container.innerHTML = '';
|
||||
container.appendChild(toggle);
|
||||
container.appendChild(dropdown);
|
||||
|
||||
// Add change event
|
||||
select.addEventListener('change', async (e) => {
|
||||
const locale = e.target.value;
|
||||
if (window.i18n) {
|
||||
await window.i18n.setLocale(locale);
|
||||
// Toggle dropdown on click
|
||||
toggle.addEventListener('click', (e) => {
|
||||
e.stopPropagation();
|
||||
toggleDropdown(container);
|
||||
});
|
||||
|
||||
// Keyboard support
|
||||
toggle.addEventListener('keydown', (e) => {
|
||||
if (e.key === 'Enter' || e.key === ' ') {
|
||||
e.preventDefault();
|
||||
toggleDropdown(container);
|
||||
} else if (e.key === 'Escape') {
|
||||
closeDropdown(container);
|
||||
}
|
||||
});
|
||||
|
||||
// Add to container
|
||||
container.innerHTML = '';
|
||||
container.appendChild(select);
|
||||
// Close dropdown when clicking outside
|
||||
document.addEventListener('click', (e) => {
|
||||
if (!container.contains(e.target)) {
|
||||
closeDropdown(container);
|
||||
}
|
||||
});
|
||||
|
||||
// Add event listener for locale changes
|
||||
// Listen for locale changes from i18n system
|
||||
window.addEventListener('localeChanged', (e) => {
|
||||
select.value = e.detail.locale;
|
||||
updateSelectedLanguage(e.detail.locale, container);
|
||||
});
|
||||
|
||||
return container;
|
||||
}
|
||||
|
||||
/**
|
||||
* Toggle dropdown open/closed
|
||||
*/
|
||||
function toggleDropdown(container) {
|
||||
const isOpen = container.classList.contains('open');
|
||||
if (isOpen) {
|
||||
closeDropdown(container);
|
||||
} else {
|
||||
openDropdown(container);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Open dropdown
|
||||
*/
|
||||
function openDropdown(container) {
|
||||
container.classList.add('open');
|
||||
const toggle = container.querySelector('.language-selector-toggle');
|
||||
if (toggle) {
|
||||
toggle.setAttribute('aria-expanded', 'true');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Close dropdown
|
||||
*/
|
||||
function closeDropdown(container) {
|
||||
container.classList.remove('open');
|
||||
const toggle = container.querySelector('.language-selector-toggle');
|
||||
if (toggle) {
|
||||
toggle.setAttribute('aria-expanded', 'false');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Select a language
|
||||
*/
|
||||
async function selectLanguage(langCode, container) {
|
||||
// Update i18n if available
|
||||
if (window.i18n) {
|
||||
await window.i18n.setLocale(langCode);
|
||||
}
|
||||
|
||||
updateSelectedLanguage(langCode, container);
|
||||
closeDropdown(container);
|
||||
}
|
||||
|
||||
/**
|
||||
* Update the UI to reflect selected language
|
||||
*/
|
||||
function updateSelectedLanguage(langCode, container) {
|
||||
const lang = languages.find(l => l.code === langCode) || languages[0];
|
||||
|
||||
// Update toggle button text
|
||||
const langCodeSpan = container.querySelector('.lang-code');
|
||||
if (langCodeSpan) {
|
||||
langCodeSpan.textContent = lang.code.toUpperCase();
|
||||
}
|
||||
|
||||
// Update active state on options
|
||||
const options = container.querySelectorAll('.language-option');
|
||||
options.forEach(option => {
|
||||
const isActive = option.getAttribute('data-lang') === langCode;
|
||||
option.classList.toggle('active', isActive);
|
||||
option.setAttribute('aria-selected', isActive);
|
||||
});
|
||||
}
|
||||
|
||||
// Create language selector when DOM is ready
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
// Create language selector
|
||||
createLanguageSelector();
|
||||
});
|
||||
|
||||
@@ -0,0 +1,214 @@
|
||||
/**
|
||||
* Modal System for OxiCloud
|
||||
* Provides modern, styled modals to replace browser prompts/alerts
|
||||
*/
|
||||
|
||||
const Modal = {
|
||||
// Modal element references
|
||||
overlay: null,
|
||||
container: null,
|
||||
icon: null,
|
||||
title: null,
|
||||
label: null,
|
||||
input: null,
|
||||
cancelBtn: null,
|
||||
confirmBtn: null,
|
||||
closeBtn: null,
|
||||
|
||||
// Current callback
|
||||
onConfirm: null,
|
||||
onCancel: null,
|
||||
|
||||
/**
|
||||
* Initialize modal system
|
||||
*/
|
||||
init() {
|
||||
this.overlay = document.getElementById('input-modal');
|
||||
if (!this.overlay) {
|
||||
console.warn('Modal overlay not found');
|
||||
return;
|
||||
}
|
||||
|
||||
this.icon = document.getElementById('modal-icon');
|
||||
this.title = document.getElementById('modal-title');
|
||||
this.label = document.getElementById('modal-label');
|
||||
this.input = document.getElementById('modal-input');
|
||||
this.cancelBtn = document.getElementById('modal-cancel-btn');
|
||||
this.confirmBtn = document.getElementById('modal-confirm-btn');
|
||||
this.closeBtn = document.getElementById('modal-close-btn');
|
||||
|
||||
// Event listeners
|
||||
this.cancelBtn.addEventListener('click', () => this.close(false));
|
||||
this.closeBtn.addEventListener('click', () => this.close(false));
|
||||
this.confirmBtn.addEventListener('click', () => this.confirm());
|
||||
|
||||
// Close on overlay click
|
||||
this.overlay.addEventListener('click', (e) => {
|
||||
if (e.target === this.overlay) {
|
||||
this.close(false);
|
||||
}
|
||||
});
|
||||
|
||||
// Handle Enter and Escape keys
|
||||
this.input.addEventListener('keydown', (e) => {
|
||||
if (e.key === 'Enter') {
|
||||
e.preventDefault();
|
||||
this.confirm();
|
||||
} else if (e.key === 'Escape') {
|
||||
this.close(false);
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
/**
|
||||
* Show input modal (replacement for prompt())
|
||||
* @param {Object} options - Modal configuration
|
||||
* @param {string} options.title - Modal title
|
||||
* @param {string} options.label - Input label
|
||||
* @param {string} options.placeholder - Input placeholder
|
||||
* @param {string} options.value - Initial input value
|
||||
* @param {string} options.icon - Font Awesome icon class (e.g., 'fa-folder-plus')
|
||||
* @param {string} options.confirmText - Confirm button text
|
||||
* @param {string} options.cancelText - Cancel button text
|
||||
* @returns {Promise<string|null>} - Resolves with input value or null if cancelled
|
||||
*/
|
||||
prompt(options = {}) {
|
||||
return new Promise((resolve) => {
|
||||
const {
|
||||
title = 'Input',
|
||||
label = '',
|
||||
placeholder = '',
|
||||
value = '',
|
||||
icon = 'fa-keyboard',
|
||||
confirmText = null,
|
||||
cancelText = null
|
||||
} = options;
|
||||
|
||||
// Set modal content
|
||||
this.icon.className = `fas ${icon}`;
|
||||
this.title.textContent = title;
|
||||
this.label.textContent = label;
|
||||
this.input.placeholder = placeholder;
|
||||
this.input.value = value;
|
||||
|
||||
// Set button text (use i18n if available)
|
||||
if (confirmText) {
|
||||
this.confirmBtn.textContent = confirmText;
|
||||
} else if (window.i18n) {
|
||||
this.confirmBtn.textContent = window.i18n.t('actions.confirm');
|
||||
}
|
||||
|
||||
if (cancelText) {
|
||||
this.cancelBtn.textContent = cancelText;
|
||||
} else if (window.i18n) {
|
||||
this.cancelBtn.textContent = window.i18n.t('actions.cancel');
|
||||
}
|
||||
|
||||
// Set callbacks
|
||||
this.onConfirm = () => {
|
||||
const inputValue = this.input.value.trim();
|
||||
resolve(inputValue || null);
|
||||
};
|
||||
this.onCancel = () => resolve(null);
|
||||
|
||||
// Show modal
|
||||
this.open();
|
||||
});
|
||||
},
|
||||
|
||||
/**
|
||||
* Show modal for creating new folder
|
||||
* @returns {Promise<string|null>}
|
||||
*/
|
||||
promptNewFolder() {
|
||||
const t = window.i18n ? window.i18n.t.bind(window.i18n) : (k) => k;
|
||||
|
||||
return this.prompt({
|
||||
title: t('dialogs.new_folder_title') || 'Nueva carpeta',
|
||||
label: t('dialogs.folder_name') || 'Nombre de la carpeta',
|
||||
placeholder: t('dialogs.folder_placeholder') || 'Mi carpeta',
|
||||
icon: 'fa-folder-plus',
|
||||
confirmText: t('actions.create') || 'Crear'
|
||||
});
|
||||
},
|
||||
|
||||
/**
|
||||
* Show modal for renaming
|
||||
* @param {string} currentName - Current name of file/folder
|
||||
* @param {boolean} isFolder - Whether it's a folder
|
||||
* @returns {Promise<string|null>}
|
||||
*/
|
||||
promptRename(currentName, isFolder = false) {
|
||||
const t = window.i18n ? window.i18n.t.bind(window.i18n) : (k) => k;
|
||||
|
||||
return this.prompt({
|
||||
title: t('dialogs.rename_title') || 'Renombrar',
|
||||
label: t('dialogs.new_name') || 'Nuevo nombre',
|
||||
placeholder: '',
|
||||
value: currentName,
|
||||
icon: isFolder ? 'fa-folder' : 'fa-file',
|
||||
confirmText: t('actions.rename') || 'Renombrar'
|
||||
});
|
||||
},
|
||||
|
||||
/**
|
||||
* Open the modal
|
||||
*/
|
||||
open() {
|
||||
if (!this.overlay) return;
|
||||
|
||||
// Show overlay
|
||||
this.overlay.style.display = 'flex';
|
||||
|
||||
// Trigger animation
|
||||
requestAnimationFrame(() => {
|
||||
this.overlay.classList.add('active');
|
||||
});
|
||||
|
||||
// Focus input after animation
|
||||
setTimeout(() => {
|
||||
this.input.focus();
|
||||
this.input.select();
|
||||
}, 100);
|
||||
},
|
||||
|
||||
/**
|
||||
* Close the modal
|
||||
* @param {boolean} confirmed - Whether the action was confirmed
|
||||
*/
|
||||
close(confirmed = false) {
|
||||
if (!this.overlay) return;
|
||||
|
||||
this.overlay.classList.remove('active');
|
||||
|
||||
setTimeout(() => {
|
||||
this.overlay.style.display = 'none';
|
||||
|
||||
if (!confirmed && this.onCancel) {
|
||||
this.onCancel();
|
||||
}
|
||||
|
||||
// Clear callbacks
|
||||
this.onConfirm = null;
|
||||
this.onCancel = null;
|
||||
}, 200);
|
||||
},
|
||||
|
||||
/**
|
||||
* Confirm the action
|
||||
*/
|
||||
confirm() {
|
||||
if (this.onConfirm) {
|
||||
this.onConfirm();
|
||||
}
|
||||
this.close(true);
|
||||
}
|
||||
};
|
||||
|
||||
// Initialize when DOM is ready
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
Modal.init();
|
||||
});
|
||||
|
||||
// Export for use in other modules
|
||||
window.Modal = Modal;
|
||||
+41
-2
@@ -26,7 +26,10 @@
|
||||
"copy": "Copy",
|
||||
"notify": "Notify",
|
||||
"send": "Send",
|
||||
"clear_recent": "Clear recent"
|
||||
"clear_recent": "Clear recent",
|
||||
"logout": "Log out",
|
||||
"create": "Create",
|
||||
"search_btn": "Search"
|
||||
},
|
||||
"share": {
|
||||
"dialogTitle": "Share Link",
|
||||
@@ -168,6 +171,10 @@
|
||||
"dialogs": {
|
||||
"rename_folder": "Rename folder",
|
||||
"new_name": "New name",
|
||||
"new_folder_title": "New folder",
|
||||
"folder_name": "Folder name",
|
||||
"folder_placeholder": "My folder",
|
||||
"rename_title": "Rename",
|
||||
"move_file": "Move file",
|
||||
"select_destination": "Select destination folder",
|
||||
"root": "Root",
|
||||
@@ -245,7 +252,16 @@
|
||||
"admin_email": "Admin email",
|
||||
"admin_password": "Admin password",
|
||||
"create_admin": "Create administrator",
|
||||
"back_to_login": "Already set up?"
|
||||
"back_to_login": "Already set up?",
|
||||
"admin_success": "Administrator account created successfully! You can now sign in.",
|
||||
"account_success": "Account created successfully! You can now sign in.",
|
||||
"passwords_mismatch": "Passwords do not match",
|
||||
"admin_create_error": "Error creating administrator account"
|
||||
},
|
||||
"storage": {
|
||||
"title": "Storage",
|
||||
"calculating": "Calculating...",
|
||||
"used": "{{percentage}}% used ({{used}} / {{total}})"
|
||||
},
|
||||
"viewer": {
|
||||
"unsupported_file": "This file type cannot be previewed.",
|
||||
@@ -253,5 +269,28 @@
|
||||
"zoom_in": "Zoom in",
|
||||
"zoom_out": "Zoom out",
|
||||
"zoom_reset": "Reset zoom"
|
||||
},
|
||||
"language_selector": {
|
||||
"title": "Welcome to OxiCloud",
|
||||
"subtitle": "Please select your language",
|
||||
"continue": "Continue",
|
||||
"languages": {
|
||||
"en": "English",
|
||||
"es": "Español",
|
||||
"zh": "中文"
|
||||
}
|
||||
},
|
||||
"favorites": {
|
||||
"empty_state": "No favorites yet",
|
||||
"empty_hint": "Star files or folders to add them to your favorites",
|
||||
"add": "Add to favorites",
|
||||
"remove": "Remove from favorites"
|
||||
},
|
||||
"recent": {
|
||||
"title": "Recent",
|
||||
"clear": "Clear recent",
|
||||
"accessed": "Accessed",
|
||||
"empty_state": "No recent files",
|
||||
"empty_hint": "Files you open will appear here"
|
||||
}
|
||||
}
|
||||
+41
-2
@@ -145,7 +145,10 @@
|
||||
"copy": "Copiar",
|
||||
"notify": "Notificar",
|
||||
"send": "Enviar",
|
||||
"clear_recent": "Limpiar recientes"
|
||||
"clear_recent": "Limpiar recientes",
|
||||
"logout": "Cerrar sesión",
|
||||
"create": "Crear",
|
||||
"search_btn": "Buscar"
|
||||
},
|
||||
"files": {
|
||||
"name": "Nombre",
|
||||
@@ -168,6 +171,10 @@
|
||||
"dialogs": {
|
||||
"rename_folder": "Renombrar carpeta",
|
||||
"new_name": "Nuevo nombre",
|
||||
"new_folder_title": "Nueva carpeta",
|
||||
"folder_name": "Nombre de la carpeta",
|
||||
"folder_placeholder": "Mi carpeta",
|
||||
"rename_title": "Renombrar",
|
||||
"move_file": "Mover archivo",
|
||||
"select_destination": "Selecciona la carpeta destino",
|
||||
"root": "Raíz",
|
||||
@@ -245,7 +252,16 @@
|
||||
"admin_email": "Email administrador",
|
||||
"admin_password": "Contraseña administrador",
|
||||
"create_admin": "Crear administrador",
|
||||
"back_to_login": "¿Ya está configurado?"
|
||||
"back_to_login": "¿Ya está configurado?",
|
||||
"admin_success": "¡Cuenta de administrador creada con éxito! Ahora puedes iniciar sesión.",
|
||||
"account_success": "¡Cuenta creada con éxito! Ahora puedes iniciar sesión.",
|
||||
"passwords_mismatch": "Las contraseñas no coinciden",
|
||||
"admin_create_error": "Error al crear cuenta de administrador"
|
||||
},
|
||||
"storage": {
|
||||
"title": "Almacenamiento",
|
||||
"calculating": "Calculando...",
|
||||
"used": "{{percentage}}% usado ({{used}} / {{total}})"
|
||||
},
|
||||
"viewer": {
|
||||
"unsupported_file": "Este tipo de archivo no se puede previsualizar.",
|
||||
@@ -253,5 +269,28 @@
|
||||
"zoom_in": "Acercar",
|
||||
"zoom_out": "Alejar",
|
||||
"zoom_reset": "Restablecer zoom"
|
||||
},
|
||||
"language_selector": {
|
||||
"title": "Bienvenido a OxiCloud",
|
||||
"subtitle": "Por favor, selecciona tu idioma",
|
||||
"continue": "Continuar",
|
||||
"languages": {
|
||||
"en": "English",
|
||||
"es": "Español",
|
||||
"zh": "中文"
|
||||
}
|
||||
},
|
||||
"favorites": {
|
||||
"empty_state": "Aún no hay favoritos",
|
||||
"empty_hint": "Marca archivos o carpetas con estrella para añadirlos a favoritos",
|
||||
"add": "Añadir a favoritos",
|
||||
"remove": "Quitar de favoritos"
|
||||
},
|
||||
"recent": {
|
||||
"title": "Recientes",
|
||||
"clear": "Limpiar recientes",
|
||||
"accessed": "Accedido",
|
||||
"empty_state": "No hay archivos recientes",
|
||||
"empty_hint": "Los archivos que abras aparecerán aquí"
|
||||
}
|
||||
}
|
||||
+41
-2
@@ -26,7 +26,10 @@
|
||||
"copy": "复制",
|
||||
"notify": "通知",
|
||||
"send": "发送",
|
||||
"clear_recent": "清除最近"
|
||||
"clear_recent": "清除最近",
|
||||
"logout": "退出登录",
|
||||
"create": "创建",
|
||||
"search_btn": "搜索"
|
||||
},
|
||||
"share": {
|
||||
"dialogTitle": "共享链接",
|
||||
@@ -130,6 +133,10 @@
|
||||
"dialogs": {
|
||||
"rename_folder": "重命名文件夹",
|
||||
"new_name": "新名称",
|
||||
"new_folder_title": "新建文件夹",
|
||||
"folder_name": "文件夹名称",
|
||||
"folder_placeholder": "我的文件夹",
|
||||
"rename_title": "重命名",
|
||||
"move_file": "移动文件",
|
||||
"select_destination": "选择目标文件夹",
|
||||
"root": "根目录",
|
||||
@@ -207,7 +214,16 @@
|
||||
"admin_email": "管理员电子邮件",
|
||||
"admin_password": "管理员密码",
|
||||
"create_admin": "创建管理员",
|
||||
"back_to_login": "已设置完成?"
|
||||
"back_to_login": "已设置完成?",
|
||||
"admin_success": "管理员账号创建成功!您现在可以登录。",
|
||||
"account_success": "账号创建成功!您现在可以登录。",
|
||||
"passwords_mismatch": "密码不匹配",
|
||||
"admin_create_error": "创建管理员账号时出错"
|
||||
},
|
||||
"storage": {
|
||||
"title": "存储空间",
|
||||
"calculating": "计算中...",
|
||||
"used": "{{percentage}}% 已使用 ({{used}} / {{total}})"
|
||||
},
|
||||
"viewer": {
|
||||
"unsupported_file": "无法预览此文件类型。",
|
||||
@@ -215,5 +231,28 @@
|
||||
"zoom_in": "放大",
|
||||
"zoom_out": "缩小",
|
||||
"zoom_reset": "重置缩放"
|
||||
},
|
||||
"language_selector": {
|
||||
"title": "欢迎使用 OxiCloud",
|
||||
"subtitle": "请选择您的语言",
|
||||
"continue": "继续",
|
||||
"languages": {
|
||||
"en": "English",
|
||||
"es": "Español",
|
||||
"zh": "中文"
|
||||
}
|
||||
},
|
||||
"favorites": {
|
||||
"empty_state": "还没有收藏",
|
||||
"empty_hint": "为文件或文件夹添加星标以将其添加到收藏夹",
|
||||
"add": "添加到收藏夹",
|
||||
"remove": "从收藏夹移除"
|
||||
},
|
||||
"recent": {
|
||||
"title": "最近",
|
||||
"clear": "清除最近",
|
||||
"accessed": "访问于",
|
||||
"empty_state": "没有最近文件",
|
||||
"empty_hint": "您打开的文件将显示在这里"
|
||||
}
|
||||
}
|
||||
|
||||
+42
-1
@@ -17,7 +17,47 @@
|
||||
</head>
|
||||
<body>
|
||||
<div class="auth-container">
|
||||
<div class="auth-panel" id="login-panel">
|
||||
<!-- Language Selector Panel - Shown first on fresh install -->
|
||||
<div class="auth-panel language-selector-panel" id="language-panel" style="display: none;">
|
||||
<div class="auth-logo">
|
||||
<div class="auth-logo-icon">
|
||||
<svg viewBox="0 0 500 500">
|
||||
<path d="M345 310c32 0 58-26 58-58s-26-58-58-58c-6.2 0-12 0.9-17.5 2.7C318 166 289 143 255 143c-34.3 0-63.1 22.6-73 53.7C176.9 195.7 171 195 165 195c-32 0-58 26-58 58s26 58 58 58h180z" fill="#fff"/>
|
||||
</svg>
|
||||
</div>
|
||||
<div class="auth-logo-text">OxiCloud</div>
|
||||
</div>
|
||||
|
||||
<h2 class="auth-title" id="language-title">Welcome to OxiCloud</h2>
|
||||
<p class="language-subtitle" id="language-subtitle">Please select your language</p>
|
||||
|
||||
<div class="language-options" id="language-options">
|
||||
<label class="language-option" data-lang="en">
|
||||
<input type="radio" name="language" value="en">
|
||||
<span class="language-radio"></span>
|
||||
<span class="language-flag">🇬🇧</span>
|
||||
<span class="language-name">English</span>
|
||||
</label>
|
||||
|
||||
<label class="language-option" data-lang="es">
|
||||
<input type="radio" name="language" value="es">
|
||||
<span class="language-radio"></span>
|
||||
<span class="language-flag">🇪🇸</span>
|
||||
<span class="language-name">Español</span>
|
||||
</label>
|
||||
|
||||
<label class="language-option" data-lang="zh">
|
||||
<input type="radio" name="language" value="zh">
|
||||
<span class="language-radio"></span>
|
||||
<span class="language-flag">🇨🇳</span>
|
||||
<span class="language-name">中文</span>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<button type="button" class="auth-button" id="language-continue" disabled>Continue</button>
|
||||
</div>
|
||||
|
||||
<div class="auth-panel" id="login-panel" style="display: none;">
|
||||
<div class="auth-logo">
|
||||
<div class="auth-logo-icon">
|
||||
<svg viewBox="0 0 500 500">
|
||||
@@ -174,6 +214,7 @@
|
||||
</div>
|
||||
|
||||
<div class="auth-error" id="admin-setup-error"></div>
|
||||
<div class="auth-success" id="admin-setup-success"></div>
|
||||
|
||||
<form class="auth-form" id="admin-setup-form">
|
||||
<div class="auth-input-group">
|
||||
|
||||
+8
-11
@@ -44,11 +44,11 @@
|
||||
</div>
|
||||
|
||||
<div class="storage-container">
|
||||
<div class="storage-title">Almacenamiento</div>
|
||||
<div class="storage-title" data-i18n="storage.title">Storage</div>
|
||||
<div class="storage-bar">
|
||||
<div class="storage-fill"></div>
|
||||
</div>
|
||||
<div class="storage-info">56% usado (5.6GB / 10GB)</div>
|
||||
<div class="storage-info" data-i18n="storage.calculating">Calculating...</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -59,18 +59,15 @@
|
||||
<div class="search-container">
|
||||
<i class="fas fa-search search-icon"></i>
|
||||
<input type="text" id="shared-search" data-i18n-placeholder="actions.search" placeholder="Search shared items...">
|
||||
<button id="shared-search-btn" class="search-button"><i class="fas fa-search"></i></button>
|
||||
<button id="shared-search-btn" class="search-button" data-i18n-title="actions.search_btn" title="Search">
|
||||
<i class="fas fa-search"></i>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="user-controls">
|
||||
<div id="language-selector" class="language-selector">
|
||||
<select id="language-selector-dropdown">
|
||||
<option value="en">English</option>
|
||||
<option value="es">Español</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="user-avatar">MR</div>
|
||||
<div id="logout-btn" class="logout-btn" title="Cerrar sesión">
|
||||
<div id="language-selector"></div>
|
||||
<div class="user-avatar" id="user-avatar">AD</div>
|
||||
<div id="logout-btn" class="logout-btn" data-i18n-title="actions.logout" title="Log out">
|
||||
<i class="fas fa-sign-out-alt"></i>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user