Merge pull request #317 from EdouardVanbelle/refactor/i18n
This commit is contained in:
@@ -134,7 +134,7 @@ async function loadFiles(options = { insertHistory: true }) {
|
|||||||
ui.showError(`
|
ui.showError(`
|
||||||
<div class="files-loading-spinner">
|
<div class="files-loading-spinner">
|
||||||
<div class="spinner"></div>
|
<div class="spinner"></div>
|
||||||
<span>${i18n ? i18n.t('files.loading') : 'Loading files…'}</span>
|
<span>${i18n.t('files.loading')}</span>
|
||||||
</div>
|
</div>
|
||||||
`);
|
`);
|
||||||
}, 100);
|
}, 100);
|
||||||
|
|||||||
+8
-15
@@ -166,9 +166,7 @@ function setActionsBarMode(mode, force = false) {
|
|||||||
elements.gridViewBtn = document.getElementById('grid-view-btn');
|
elements.gridViewBtn = document.getElementById('grid-view-btn');
|
||||||
elements.listViewBtn = document.getElementById('list-view-btn');
|
elements.listViewBtn = document.getElementById('list-view-btn');
|
||||||
|
|
||||||
if (i18n?.translateElement) {
|
i18n.translateElement(elements.actionsBar);
|
||||||
i18n.translateElement(elements.actionsBar);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (mode === 'files') {
|
if (mode === 'files') {
|
||||||
setupUploadDropdown();
|
setupUploadDropdown();
|
||||||
@@ -395,7 +393,7 @@ function initApp() {
|
|||||||
});
|
});
|
||||||
|
|
||||||
// Wait for translations to load before checking authentication
|
// Wait for translations to load before checking authentication
|
||||||
if (i18n?.isLoaded?.()) {
|
if (i18n.isLoaded()) {
|
||||||
// Translations already loaded, proceed with authentication
|
// Translations already loaded, proceed with authentication
|
||||||
checkAuthentication();
|
checkAuthentication();
|
||||||
} else {
|
} else {
|
||||||
@@ -408,7 +406,7 @@ function initApp() {
|
|||||||
|
|
||||||
// Set a timeout as a fallback in case translations take too long
|
// Set a timeout as a fallback in case translations take too long
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
if (!i18n?.isLoaded?.()) {
|
if (!i18n.isLoaded()) {
|
||||||
console.warn('Translations loading timeout, proceeding with authentication anyway');
|
console.warn('Translations loading timeout, proceeding with authentication anyway');
|
||||||
checkAuthentication();
|
checkAuthentication();
|
||||||
}
|
}
|
||||||
@@ -727,16 +725,11 @@ function updateStorageUsageDisplay(userData) {
|
|||||||
// Remove data-i18n attribute to prevent i18n from overwriting our value
|
// Remove data-i18n attribute to prevent i18n from overwriting our value
|
||||||
storageInfo.removeAttribute('data-i18n');
|
storageInfo.removeAttribute('data-i18n');
|
||||||
|
|
||||||
// Use i18n if available
|
storageInfo.textContent = i18n.t('storage.used', {
|
||||||
if (i18n?.t) {
|
percentage: usagePercentage,
|
||||||
storageInfo.textContent = i18n.t('storage.used', {
|
used: usedFormatted,
|
||||||
percentage: usagePercentage,
|
total: quotaFormatted
|
||||||
used: usedFormatted,
|
});
|
||||||
total: quotaFormatted
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
storageInfo.textContent = `${usagePercentage}% used (${usedFormatted} / ${quotaFormatted})`;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
console.log(`Updated storage display: ${usagePercentage}% (${usedFormatted} / ${quotaFormatted})`);
|
console.log(`Updated storage display: ${usagePercentage}% (${usedFormatted} / ${quotaFormatted})`);
|
||||||
|
|||||||
@@ -151,8 +151,8 @@ function setCurrentSection(section) {
|
|||||||
|
|
||||||
// Update page title
|
// Update page title
|
||||||
const titleKey = `nav.${section}`;
|
const titleKey = `nav.${section}`;
|
||||||
const defaultTitle = section.charAt(0).toUpperCase() + section.slice(1);
|
// TODO check why no more used: const defaultTitle = section.charAt(0).toUpperCase() + section.slice(1);
|
||||||
appElements.pageTitle.textContent = i18n ? i18n.t(titleKey) : defaultTitle;
|
appElements.pageTitle.textContent = i18n.t(titleKey);
|
||||||
appElements.pageTitle.setAttribute('data-i18n', titleKey);
|
appElements.pageTitle.setAttribute('data-i18n', titleKey);
|
||||||
|
|
||||||
// Hide sharedView when switching to any other section
|
// Hide sharedView when switching to any other section
|
||||||
|
|||||||
+10
-11
@@ -15,14 +15,13 @@ async function loadTrashItems() {
|
|||||||
try {
|
try {
|
||||||
if (multiSelect) multiSelect.clear();
|
if (multiSelect) multiSelect.clear();
|
||||||
ui.resetFilesList(); // ensure also list visible & error hidden
|
ui.resetFilesList(); // ensure also list visible & error hidden
|
||||||
const _tt = i18n?.t ? i18n.t : (k) => k.split('.').pop();
|
|
||||||
elements.filesList.innerHTML = `
|
elements.filesList.innerHTML = `
|
||||||
<div class="list-header trash-header">
|
<div class="list-header trash-header">
|
||||||
<div data-i18n="files.name">${_tt('files.name')}</div>
|
<div data-i18n="files.name">${i18n.t('files.name')}</div>
|
||||||
<div data-i18n="files.type">${_tt('files.type')}</div>
|
<div data-i18n="files.type">${i18n.t('files.type')}</div>
|
||||||
<div data-i18n="trash.original_location">${_tt('trash.original_location')}</div>
|
<div data-i18n="trash.original_location">${i18n.t('trash.original_location')}</div>
|
||||||
<div data-i18n="trash.deleted_date">${_tt('trash.deleted_date')}</div>
|
<div data-i18n="trash.deleted_date">${i18n.t('trash.deleted_date')}</div>
|
||||||
<div data-i18n="trash.actions">${_tt('trash.actions')}</div>
|
<div data-i18n="trash.actions">${i18n.t('trash.actions')}</div>
|
||||||
</div>
|
</div>
|
||||||
`;
|
`;
|
||||||
|
|
||||||
@@ -33,7 +32,7 @@ async function loadTrashItems() {
|
|||||||
if (trashItems.length === 0) {
|
if (trashItems.length === 0) {
|
||||||
ui.showError(`
|
ui.showError(`
|
||||||
<i class="fas fa-trash empty-state-icon"></i>
|
<i class="fas fa-trash empty-state-icon"></i>
|
||||||
<p>${i18n ? i18n.t('trash.empty_state') : 'The trash is empty'}</p>
|
<p>${i18n.t('trash.empty_state')}</p>
|
||||||
`);
|
`);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -58,12 +57,12 @@ function addTrashItemToView(item) {
|
|||||||
let iconSpecialClass = '';
|
let iconSpecialClass = '';
|
||||||
if (!isFile) {
|
if (!isFile) {
|
||||||
iconClass = item.icon_class || 'fas fa-folder';
|
iconClass = item.icon_class || 'fas fa-folder';
|
||||||
typeLabel = i18n ? i18n.t('files.file_types.folder') : 'Folder';
|
typeLabel = i18n.t('files.file_types.folder');
|
||||||
} else {
|
} else {
|
||||||
iconClass = item.icon_class || (ui?.getIconClass ? ui.getIconClass(item.name) : 'fas fa-file');
|
iconClass = item.icon_class || (ui?.getIconClass ? ui.getIconClass(item.name) : 'fas fa-file');
|
||||||
iconSpecialClass = ui?.getIconSpecialClass ? ui.getIconSpecialClass(item.name) : '';
|
iconSpecialClass = ui?.getIconSpecialClass ? ui.getIconSpecialClass(item.name) : '';
|
||||||
const cat = item.category || '';
|
const cat = item.category || '';
|
||||||
typeLabel = cat ? (i18n ? i18n.t(`files.file_types.${cat.toLowerCase()}`) || cat : cat) : i18n ? i18n.t('files.file_types.document') : 'Document';
|
typeLabel = cat ? i18n.t(`files.file_types.${cat.toLowerCase()}`) || cat : i18n.t('files.file_types.document');
|
||||||
}
|
}
|
||||||
|
|
||||||
const isFolder = !isFile;
|
const isFolder = !isFile;
|
||||||
@@ -86,10 +85,10 @@ function addTrashItemToView(item) {
|
|||||||
<div class="path-cell">${escapeHtml(item.original_path || '--')}</div>
|
<div class="path-cell">${escapeHtml(item.original_path || '--')}</div>
|
||||||
<div class="date-cell">${escapeHtml(formattedDate)}</div>
|
<div class="date-cell">${escapeHtml(formattedDate)}</div>
|
||||||
<div class="actions-cell">
|
<div class="actions-cell">
|
||||||
<button class="btn-restore" title="${i18n ? i18n.t('trash.restore') : 'Restore'}">
|
<button class="btn-restore" title="${i18n.t('trash.restore')}">
|
||||||
<i class="fas fa-undo"></i>
|
<i class="fas fa-undo"></i>
|
||||||
</button>
|
</button>
|
||||||
<button class="btn-delete" title="${i18n ? i18n.t('trash.delete_permanently') : 'Delete permanently'}">
|
<button class="btn-delete" title="${i18n.t('trash.delete_permanently')}">
|
||||||
<i class="fas fa-trash"></i>
|
<i class="fas fa-trash"></i>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
+8
-14
@@ -542,17 +542,11 @@ const ui = {
|
|||||||
breadcrumb.innerHTML = '';
|
breadcrumb.innerHTML = '';
|
||||||
const path = app.breadcrumbPath; // [{id, name}, ...]
|
const path = app.breadcrumbPath; // [{id, name}, ...]
|
||||||
|
|
||||||
// Helper function to safely get translation text
|
|
||||||
const getTranslatedText = (key, defaultValue) => {
|
|
||||||
if (!i18n?.t) return defaultValue;
|
|
||||||
return i18n.t(key);
|
|
||||||
};
|
|
||||||
|
|
||||||
// -- Home icon (always present, clickable to go to root) --
|
// -- Home icon (always present, clickable to go to root) --
|
||||||
const homeIcon = document.createElement('span');
|
const homeIcon = document.createElement('span');
|
||||||
homeIcon.className = 'breadcrumb-item breadcrumb-home';
|
homeIcon.className = 'breadcrumb-item breadcrumb-home';
|
||||||
homeIcon.innerHTML = '<i class="fas fa-home"></i>';
|
homeIcon.innerHTML = '<i class="fas fa-home"></i>';
|
||||||
homeIcon.title = getTranslatedText('breadcrumb.home', 'Home');
|
homeIcon.title = i18n.t('breadcrumb.home');
|
||||||
|
|
||||||
// Home is always clickable if we have a home folder
|
// Home is always clickable if we have a home folder
|
||||||
if (app.userHomeFolderId) {
|
if (app.userHomeFolderId) {
|
||||||
@@ -1265,7 +1259,7 @@ const ui = {
|
|||||||
<div class="file-badge file-badge-favorite ${isFav ? '' : 'hidden'}"><i class="fas fa-star favorite-star-inline"></i></div>
|
<div class="file-badge file-badge-favorite ${isFav ? '' : 'hidden'}"><i class="fas fa-star favorite-star-inline"></i></div>
|
||||||
<div class="file-badge file-badge-shared ${isShared ? '' : 'hidden'}"><i class="fas fa-share-alt"></i></div>
|
<div class="file-badge file-badge-shared ${isShared ? '' : 'hidden'}"><i class="fas fa-share-alt"></i></div>
|
||||||
</div>
|
</div>
|
||||||
<div class="type-cell">${i18n ? i18n.t('files.file_types.folder') : 'Folder'}</div>
|
<div class="type-cell">${i18n.t('files.file_types.folder')}</div>
|
||||||
<div class="size-cell">--</div>
|
<div class="size-cell">--</div>
|
||||||
<div class="date-cell">${formattedDate}</div>
|
<div class="date-cell">${formattedDate}</div>
|
||||||
<div class="action-cell">
|
<div class="action-cell">
|
||||||
@@ -1288,7 +1282,7 @@ const ui = {
|
|||||||
const iconClass = file.icon_class || this.getIconClass(file.name);
|
const iconClass = file.icon_class || this.getIconClass(file.name);
|
||||||
const iconSpecialClass = file.icon_special_class || this.getIconSpecialClass(file.name);
|
const iconSpecialClass = file.icon_special_class || this.getIconSpecialClass(file.name);
|
||||||
const cat = file.category || '';
|
const cat = file.category || '';
|
||||||
const typeLabel = cat ? (i18n ? i18n.t(`files.file_types.${cat.toLowerCase()}`) || cat : cat) : i18n ? i18n.t('files.file_types.document') : 'Document';
|
const typeLabel = cat ? i18n.t(`files.file_types.${cat.toLowerCase()}`) || cat : i18n.t('files.file_types.document');
|
||||||
const fileSize = file.size_formatted || formatFileSize(file.size);
|
const fileSize = file.size_formatted || formatFileSize(file.size);
|
||||||
const formattedDate = formatDateTime(file.modified_at);
|
const formattedDate = formatDateTime(file.modified_at);
|
||||||
const isFav = favorites?.isFavorite(file.id, 'file');
|
const isFav = favorites?.isFavorite(file.id, 'file');
|
||||||
@@ -1352,7 +1346,7 @@ const ui = {
|
|||||||
<div></div><!-- actions -->
|
<div></div><!-- actions -->
|
||||||
</div>`;
|
</div>`;
|
||||||
|
|
||||||
if (i18n?.translateElement) i18n.translateElement(filesList);
|
i18n.translateElement(filesList);
|
||||||
|
|
||||||
filesList.classList.remove('hidden');
|
filesList.classList.remove('hidden');
|
||||||
filesContainerError?.classList.add('hidden');
|
filesContainerError?.classList.add('hidden');
|
||||||
@@ -1376,7 +1370,7 @@ const ui = {
|
|||||||
const filesList = document.getElementById('files-list');
|
const filesList = document.getElementById('files-list');
|
||||||
if (filesContainerError) filesContainerError.innerHTML = content;
|
if (filesContainerError) filesContainerError.innerHTML = content;
|
||||||
|
|
||||||
if (i18n?.translateElement) i18n.translateElement(filesContainerError);
|
i18n.translateElement(filesContainerError);
|
||||||
|
|
||||||
filesContainerError?.classList.remove('hidden');
|
filesContainerError?.classList.remove('hidden');
|
||||||
filesList?.classList.add('hidden');
|
filesList?.classList.add('hidden');
|
||||||
@@ -1642,9 +1636,9 @@ if (document.readyState === 'loading') {
|
|||||||
* @returns {Promise<boolean>} true if confirmed, false if cancelled
|
* @returns {Promise<boolean>} true if confirmed, false if cancelled
|
||||||
*/
|
*/
|
||||||
function showConfirmDialog({ title, message, confirmText, cancelText, danger = true } = {}) {
|
function showConfirmDialog({ title, message, confirmText, cancelText, danger = true } = {}) {
|
||||||
const ct = confirmText || (i18n ? i18n.t('actions.delete') : 'Delete');
|
const ct = confirmText || i18n.t('actions.delete');
|
||||||
const cc = cancelText || (i18n ? i18n.t('actions.cancel') : 'Cancel');
|
const cc = cancelText || i18n.t('actions.cancel');
|
||||||
const t = title || (i18n ? i18n.t('dialogs.confirm_title') : 'Confirm action');
|
const t = title || i18n.t('dialogs.confirm_title');
|
||||||
|
|
||||||
return new Promise((resolve) => {
|
return new Promise((resolve) => {
|
||||||
// Remove any previous confirm dialog
|
// Remove any previous confirm dialog
|
||||||
|
|||||||
@@ -209,10 +209,9 @@ function showUserProfileModal() {
|
|||||||
const usedBytes = userData.storage_used_bytes || 0;
|
const usedBytes = userData.storage_used_bytes || 0;
|
||||||
const quotaBytes = userData.storage_quota_bytes == null ? 10 * 1024 * 1024 * 1024 : userData.storage_quota_bytes;
|
const quotaBytes = userData.storage_quota_bytes == null ? 10 * 1024 * 1024 * 1024 : userData.storage_quota_bytes;
|
||||||
const percentage = quotaBytes > 0 ? Math.min(Math.round((usedBytes / quotaBytes) * 100), 100) : 0;
|
const percentage = quotaBytes > 0 ? Math.min(Math.round((usedBytes / quotaBytes) * 100), 100) : 0;
|
||||||
|
// FIXME: use classes
|
||||||
const barColor = percentage > 90 ? '#ef4444' : percentage > 70 ? '#f59e0b' : '#22c55e';
|
const barColor = percentage > 90 ? '#ef4444' : percentage > 70 ? '#f59e0b' : '#22c55e';
|
||||||
|
|
||||||
const t = (key, fallback) => (i18n?.t ? i18n.t(key) || fallback : fallback);
|
|
||||||
|
|
||||||
const existing = document.getElementById('profile-modal-overlay');
|
const existing = document.getElementById('profile-modal-overlay');
|
||||||
if (existing) existing.remove();
|
if (existing) existing.remove();
|
||||||
|
|
||||||
@@ -225,11 +224,11 @@ function showUserProfileModal() {
|
|||||||
<div class="about-modal-avatar">${initials}</div>
|
<div class="about-modal-avatar">${initials}</div>
|
||||||
<h3 class="about-modal-username">${username}</h3>
|
<h3 class="about-modal-username">${username}</h3>
|
||||||
<p class="about-modal-email">${email}</p>
|
<p class="about-modal-email">${email}</p>
|
||||||
<span class="about-modal-role ${role === 'admin' ? 'about-modal-role-admin' : 'about-modal-role-user'}">${role === 'admin' ? '🛡️ Admin' : `👤 ${t('user_menu.role_user', 'User')}`}</span>
|
<span class="about-modal-role ${role === 'admin' ? 'about-modal-role-admin' : 'about-modal-role-user'}">${role === 'admin' ? '🛡️ Admin' : `👤 ${i18n.t('user_menu.role_user')}`}</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="about-modal-storage">
|
<div class="about-modal-storage">
|
||||||
<div class="about-modal-storage-label">
|
<div class="about-modal-storage-label">
|
||||||
<i class="fas fa-database"></i>${t('storage.title', 'Storage')}
|
<i class="fas fa-database"></i>${i18n.t('storage.title')}
|
||||||
</div>
|
</div>
|
||||||
<div class="about-modal-bar-bg">
|
<div class="about-modal-bar-bg">
|
||||||
<div class="about-modal-bar-fill" id="about-bar-fill"></div>
|
<div class="about-modal-bar-fill" id="about-bar-fill"></div>
|
||||||
@@ -237,7 +236,7 @@ function showUserProfileModal() {
|
|||||||
<div class="about-modal-bar-text">${percentage}% · ${formatFileSize(usedBytes)} / ${formatQuotaSize(quotaBytes)}</div>
|
<div class="about-modal-bar-text">${percentage}% · ${formatFileSize(usedBytes)} / ${formatQuotaSize(quotaBytes)}</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="about-modal-footer">
|
<div class="about-modal-footer">
|
||||||
<button id="profile-modal-close" class="about-modal-close-btn">${t('actions.close', 'Close')}</button>
|
<button id="profile-modal-close" class="about-modal-close-btn">${i18n.t('actions.close')}</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
`;
|
`;
|
||||||
|
|||||||
@@ -66,7 +66,7 @@ function createLanguageSelector(containerId = 'language-selector') {
|
|||||||
|
|
||||||
// Get current language
|
// Get current language
|
||||||
const languages = getAvailableLanguages();
|
const languages = getAvailableLanguages();
|
||||||
const currentLocale = i18n ? i18n.getCurrentLocale() : 'en';
|
const currentLocale = i18n.getCurrentLocale();
|
||||||
const currentLang = languages.find((l) => l.code === currentLocale) || languages[0];
|
const currentLang = languages.find((l) => l.code === currentLocale) || languages[0];
|
||||||
|
|
||||||
// Set initial HTML attributes
|
// Set initial HTML attributes
|
||||||
@@ -185,10 +185,7 @@ function closeDropdown(container) {
|
|||||||
* Select a language
|
* Select a language
|
||||||
*/
|
*/
|
||||||
async function selectLanguage(langCode, container) {
|
async function selectLanguage(langCode, container) {
|
||||||
// Update i18n if available
|
await i18n.setLocale(langCode);
|
||||||
if (i18n) {
|
|
||||||
await i18n.setLocale(langCode);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Update HTML lang attribute and dir for RTL languages
|
// Update HTML lang attribute and dir for RTL languages
|
||||||
updateHtmlAttributes(langCode);
|
updateHtmlAttributes(langCode);
|
||||||
|
|||||||
+9
-15
@@ -108,16 +108,15 @@ const Modal = {
|
|||||||
this.input.placeholder = placeholder;
|
this.input.placeholder = placeholder;
|
||||||
this.input.value = value;
|
this.input.value = value;
|
||||||
|
|
||||||
// Set button text (use i18n if available)
|
|
||||||
if (confirmText) {
|
if (confirmText) {
|
||||||
this.confirmBtn.textContent = confirmText;
|
this.confirmBtn.textContent = confirmText;
|
||||||
} else if (i18n) {
|
} else {
|
||||||
this.confirmBtn.textContent = i18n.t('actions.confirm');
|
this.confirmBtn.textContent = i18n.t('actions.confirm');
|
||||||
}
|
}
|
||||||
|
|
||||||
if (cancelText) {
|
if (cancelText) {
|
||||||
this.cancelBtn.textContent = cancelText;
|
this.cancelBtn.textContent = cancelText;
|
||||||
} else if (i18n) {
|
} else {
|
||||||
this.cancelBtn.textContent = i18n.t('actions.cancel');
|
this.cancelBtn.textContent = i18n.t('actions.cancel');
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -138,14 +137,12 @@ const Modal = {
|
|||||||
* @returns {Promise<string|null>}
|
* @returns {Promise<string|null>}
|
||||||
*/
|
*/
|
||||||
promptNewFolder() {
|
promptNewFolder() {
|
||||||
const t = i18n ? i18n.t.bind(i18n) : (k) => k;
|
|
||||||
|
|
||||||
return this.prompt({
|
return this.prompt({
|
||||||
title: t('dialogs.new_folder_title') || 'New folder',
|
title: i18n.t('dialogs.new_folder_title'),
|
||||||
label: t('dialogs.folder_name') || 'Folder name',
|
label: i18n.t('dialogs.folder_name'),
|
||||||
placeholder: t('dialogs.folder_placeholder') || 'My folder',
|
placeholder: i18n.t('dialogs.folder_placeholder'),
|
||||||
icon: 'fa-folder-plus',
|
icon: 'fa-folder-plus',
|
||||||
confirmText: t('actions.create') || 'Create'
|
confirmText: i18n.t('actions.create')
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
|
||||||
@@ -156,18 +153,15 @@ const Modal = {
|
|||||||
* @returns {Promise<string|null>}
|
* @returns {Promise<string|null>}
|
||||||
*/
|
*/
|
||||||
promptRename(currentName, isFolder = false) {
|
promptRename(currentName, isFolder = false) {
|
||||||
const t = i18n ? i18n.t.bind(i18n) : (k) => k;
|
|
||||||
|
|
||||||
// For files, we want to select only the name part (without extension)
|
|
||||||
this._selectNameOnly = !isFolder;
|
this._selectNameOnly = !isFolder;
|
||||||
|
|
||||||
return this.prompt({
|
return this.prompt({
|
||||||
title: t('dialogs.rename_title') || 'Renombrar',
|
title: i18n.t('dialogs.rename_title'),
|
||||||
label: t('dialogs.new_name') || 'Nuevo nombre',
|
label: i18n.t('dialogs.new_name'),
|
||||||
placeholder: '',
|
placeholder: '',
|
||||||
value: currentName,
|
value: currentName,
|
||||||
icon: isFolder ? 'fa-folder' : 'fa-file',
|
icon: isFolder ? 'fa-folder' : 'fa-file',
|
||||||
confirmText: t('actions.rename') || 'Renombrar'
|
confirmText: i18n.t('actions.rename')
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
|
||||||
|
|||||||
@@ -149,9 +149,8 @@ const notifications = (() => {
|
|||||||
item.className = 'notif-item';
|
item.className = 'notif-item';
|
||||||
item.id = batchId;
|
item.id = batchId;
|
||||||
|
|
||||||
const t = i18n?.t || ((k) => k);
|
const uploadingText = folderName ? `📁 ${i18n.t('upload.uploading')} ${_esc(folderName)}…` : i18n.t('upload.uploading');
|
||||||
const uploadingText = folderName ? `📁 ${t('upload.uploading')} ${_esc(folderName)}…` : t('upload.uploading');
|
const filesLabel = i18n.t('upload.files');
|
||||||
const filesLabel = t('upload.files');
|
|
||||||
|
|
||||||
item.innerHTML = `
|
item.innerHTML = `
|
||||||
<div class="notif-item-icon upload"><i class="fas fa-cloud-upload-alt"></i></div>
|
<div class="notif-item-icon upload"><i class="fas fa-cloud-upload-alt"></i></div>
|
||||||
@@ -254,8 +253,7 @@ const notifications = (() => {
|
|||||||
const pctEl = $(`${batchId}-pct`);
|
const pctEl = $(`${batchId}-pct`);
|
||||||
const statsEl = $(`${batchId}-stats`);
|
const statsEl = $(`${batchId}-stats`);
|
||||||
|
|
||||||
const t = i18n?.t || ((k) => k);
|
const filesLabel = i18n.t('upload.files');
|
||||||
const filesLabel = t('upload.files');
|
|
||||||
|
|
||||||
if (fillEl) fillEl.style.width = `${pctVal}%`;
|
if (fillEl) fillEl.style.width = `${pctVal}%`;
|
||||||
if (pctEl) pctEl.textContent = `${pctVal}%`;
|
if (pctEl) pctEl.textContent = `${pctVal}%`;
|
||||||
@@ -282,8 +280,7 @@ const notifications = (() => {
|
|||||||
const curEl = $(`${batchId}-current`);
|
const curEl = $(`${batchId}-current`);
|
||||||
if (curEl) curEl.textContent = '';
|
if (curEl) curEl.textContent = '';
|
||||||
|
|
||||||
const t = i18n?.t || ((k) => k);
|
const completeText = i18n.t('upload.complete', {
|
||||||
const completeText = t('upload.complete', {
|
|
||||||
count: successCount,
|
count: successCount,
|
||||||
total: totalFiles
|
total: totalFiles
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -511,10 +511,7 @@ function initLanguageSelector() {
|
|||||||
localStorage.setItem(LOCALE_KEY, selectedLanguage);
|
localStorage.setItem(LOCALE_KEY, selectedLanguage);
|
||||||
localStorage.setItem(FIRST_RUN_KEY, 'true');
|
localStorage.setItem(FIRST_RUN_KEY, 'true');
|
||||||
|
|
||||||
// Update i18n if available
|
await i18n.setLocale(selectedLanguage);
|
||||||
if (i18n?.setLocale) {
|
|
||||||
await i18n.setLocale(selectedLanguage);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Hide language panel
|
// Hide language panel
|
||||||
hidePanel(languagePanel);
|
hidePanel(languagePanel);
|
||||||
@@ -635,7 +632,7 @@ async function configureOidcLoginUI() {
|
|||||||
// Update button text with provider name
|
// Update button text with provider name
|
||||||
const btnTextEl = oidcBtn.querySelector('span');
|
const btnTextEl = oidcBtn.querySelector('span');
|
||||||
if (btnTextEl && oidcInfo.provider_name) {
|
if (btnTextEl && oidcInfo.provider_name) {
|
||||||
const template = i18n?.t ? i18n.t('auth.sso_login_provider') : 'Sign in with {{provider}}';
|
const template = i18n.t('auth.sso_login_provider');
|
||||||
btnTextEl.textContent = template.replace('{{provider}}', oidcInfo.provider_name);
|
btnTextEl.textContent = template.replace('{{provider}}', oidcInfo.provider_name);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -942,8 +939,7 @@ if (isLoginPage && registerForm) {
|
|||||||
|
|
||||||
// Validate passwords match
|
// Validate passwords match
|
||||||
if (password !== confirmPassword) {
|
if (password !== confirmPassword) {
|
||||||
const errorMsg = i18n ? i18n.t('auth.passwords_mismatch') : 'Passwords do not match';
|
registerError.textContent = i18n.t('auth.passwords_mismatch');
|
||||||
registerError.textContent = errorMsg;
|
|
||||||
registerError.style.display = 'block';
|
registerError.style.display = 'block';
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -951,9 +947,7 @@ if (isLoginPage && registerForm) {
|
|||||||
try {
|
try {
|
||||||
await register(username, email, password);
|
await register(username, email, password);
|
||||||
|
|
||||||
// Show success message
|
registerSuccess.textContent = i18n.t('auth.account_success');
|
||||||
const successMsg = i18n ? i18n.t('auth.account_success') : 'Account created successfully! You can now log in.';
|
|
||||||
registerSuccess.textContent = successMsg;
|
|
||||||
registerSuccess.style.display = 'block';
|
registerSuccess.style.display = 'block';
|
||||||
|
|
||||||
// Clear form
|
// Clear form
|
||||||
@@ -965,8 +959,7 @@ if (isLoginPage && registerForm) {
|
|||||||
hidePanel(registerPanel);
|
hidePanel(registerPanel);
|
||||||
}, 2000);
|
}, 2000);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
const errorMsg = i18n ? i18n.t('auth.admin_create_error') : 'Error registering account';
|
registerError.textContent = error.message || i18n.t('auth.admin_create_error');
|
||||||
registerError.textContent = error.message || errorMsg;
|
|
||||||
registerError.style.display = 'block';
|
registerError.style.display = 'block';
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
@@ -988,8 +981,7 @@ if (isLoginPage && adminSetupForm) {
|
|||||||
|
|
||||||
// Validate passwords match
|
// Validate passwords match
|
||||||
if (password !== confirmPassword) {
|
if (password !== confirmPassword) {
|
||||||
const errorMsg = i18n ? i18n.t('auth.passwords_mismatch') : 'Passwords do not match';
|
adminSetupError.textContent = i18n.t('auth.passwords_mismatch');
|
||||||
adminSetupError.textContent = errorMsg;
|
|
||||||
adminSetupError.style.display = 'block';
|
adminSetupError.style.display = 'block';
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -1012,11 +1004,8 @@ if (isLoginPage && adminSetupForm) {
|
|||||||
}
|
}
|
||||||
await response.json();
|
await response.json();
|
||||||
|
|
||||||
// Show success message in the GUI instead of alert
|
|
||||||
const successMsg = i18n ? i18n.t('auth.admin_success') : 'Admin account created successfully! You can now log in.';
|
|
||||||
|
|
||||||
if (adminSetupSuccess) {
|
if (adminSetupSuccess) {
|
||||||
adminSetupSuccess.textContent = successMsg;
|
adminSetupSuccess.textContent = i18n.t('auth.admin_success');
|
||||||
adminSetupSuccess.style.display = 'block';
|
adminSetupSuccess.style.display = 'block';
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1027,8 +1016,7 @@ if (isLoginPage && adminSetupForm) {
|
|||||||
if (adminSetupSuccess) adminSetupSuccess.style.display = 'none';
|
if (adminSetupSuccess) adminSetupSuccess.style.display = 'none';
|
||||||
}, 2000);
|
}, 2000);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
const errorMsg = i18n ? i18n.t('auth.admin_create_error') : 'Error creating admin account';
|
adminSetupError.textContent = error.message || i18n.t('auth.admin_create_error');
|
||||||
adminSetupError.textContent = error.message || errorMsg;
|
|
||||||
adminSetupError.style.display = 'block';
|
adminSetupError.style.display = 'block';
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -27,7 +27,7 @@ const contextMenus = {
|
|||||||
if (!option) return;
|
if (!option) return;
|
||||||
const label = option.querySelector('span');
|
const label = option.querySelector('span');
|
||||||
if (!label) return;
|
if (!label) return;
|
||||||
label.textContent = i18n ? i18n.t(isFavorite ? 'actions.unfavorite' : 'actions.favorite') : isFavorite ? 'Remove from favorites' : 'Add to favorites';
|
label.textContent = i18n.t(isFavorite ? 'actions.unfavorite' : 'actions.favorite');
|
||||||
},
|
},
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -382,7 +382,7 @@ const contextMenus = {
|
|||||||
renameInput.value = folder.name;
|
renameInput.value = folder.name;
|
||||||
// Update header text
|
// Update header text
|
||||||
const headerSpan = renameDialog.querySelector('.rename-dialog-header span');
|
const headerSpan = renameDialog.querySelector('.rename-dialog-header span');
|
||||||
if (headerSpan) headerSpan.textContent = i18n ? i18n.t('dialogs.rename_folder') : 'Rename folder';
|
if (headerSpan) headerSpan.textContent = i18n.t('dialogs.rename_folder');
|
||||||
renameDialog?.classList.remove('hidden');
|
renameDialog?.classList.remove('hidden');
|
||||||
renameInput.focus();
|
renameInput.focus();
|
||||||
renameInput.select();
|
renameInput.select();
|
||||||
@@ -402,7 +402,7 @@ const contextMenus = {
|
|||||||
renameInput.value = file.name;
|
renameInput.value = file.name;
|
||||||
// Update header text
|
// Update header text
|
||||||
const headerSpan = renameDialog.querySelector('.rename-dialog-header span');
|
const headerSpan = renameDialog.querySelector('.rename-dialog-header span');
|
||||||
if (headerSpan) headerSpan.textContent = i18n ? i18n.t('dialogs.rename_file') : 'Rename file';
|
if (headerSpan) headerSpan.textContent = i18n.t('dialogs.rename_file');
|
||||||
renameDialog?.classList.remove('hidden');
|
renameDialog?.classList.remove('hidden');
|
||||||
renameInput.focus();
|
renameInput.focus();
|
||||||
renameInput.select();
|
renameInput.select();
|
||||||
@@ -471,7 +471,7 @@ const contextMenus = {
|
|||||||
|
|
||||||
// Update dialog title (preserve icon)
|
// Update dialog title (preserve icon)
|
||||||
const dialogHeader = document.getElementById('move-file-dialog').querySelector('.rename-dialog-header');
|
const dialogHeader = document.getElementById('move-file-dialog').querySelector('.rename-dialog-header');
|
||||||
const titleText = mode === 'file' ? (i18n ? i18n.t('dialogs.move_file') : 'Move file') : i18n ? i18n.t('dialogs.move_folder') : 'Move folder';
|
const titleText = mode === 'file' ? i18n.t('dialogs.move_file') : i18n.t('dialogs.move_folder');
|
||||||
dialogHeader.innerHTML = `<i class="fas fa-arrows-alt dialog-header-icon"></i> <span>${titleText}</span>`;
|
dialogHeader.innerHTML = `<i class="fas fa-arrows-alt dialog-header-icon"></i> <span>${titleText}</span>`;
|
||||||
|
|
||||||
// Load folders for the starting location
|
// Load folders for the starting location
|
||||||
@@ -496,7 +496,7 @@ const contextMenus = {
|
|||||||
async renameItem() {
|
async renameItem() {
|
||||||
const newName = document.getElementById('rename-input').value.trim();
|
const newName = document.getElementById('rename-input').value.trim();
|
||||||
if (!newName) {
|
if (!newName) {
|
||||||
alert(i18n ? i18n.t('errors.empty_name') : 'Name cannot be empty');
|
alert(i18n.t('errors.empty_name'));
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -587,7 +587,7 @@ const contextMenus = {
|
|||||||
currentFolderOption.className = 'folder-select-item folder-select-current';
|
currentFolderOption.className = 'folder-select-item folder-select-current';
|
||||||
currentFolderOption.innerHTML = `
|
currentFolderOption.innerHTML = `
|
||||||
<i class="fas fa-check-circle check-icon"></i>
|
<i class="fas fa-check-circle check-icon"></i>
|
||||||
<span>${i18n ? i18n.t('dialogs.select_this_folder') : 'Select this folder'}</span>
|
<span>${i18n.t('dialogs.select_this_folder')}</span>
|
||||||
`;
|
`;
|
||||||
currentFolderOption.addEventListener('click', () => {
|
currentFolderOption.addEventListener('click', () => {
|
||||||
document.querySelectorAll('.folder-select-item').forEach((item) => {
|
document.querySelectorAll('.folder-select-item').forEach((item) => {
|
||||||
@@ -606,7 +606,7 @@ const contextMenus = {
|
|||||||
parentOption.className = 'folder-select-item folder-navigate-up';
|
parentOption.className = 'folder-select-item folder-navigate-up';
|
||||||
parentOption.innerHTML = `
|
parentOption.innerHTML = `
|
||||||
<i class="fas fa-level-up-alt"></i>
|
<i class="fas fa-level-up-alt"></i>
|
||||||
<span>${i18n ? i18n.t('dialogs.go_to_parent') : '.. (parent folder)'}</span>
|
<span>${i18n.t('dialogs.go_to_parent')}</span>
|
||||||
`;
|
`;
|
||||||
parentOption.addEventListener('click', () => {
|
parentOption.addEventListener('click', () => {
|
||||||
// Navigate to parent folder
|
// Navigate to parent folder
|
||||||
@@ -664,7 +664,7 @@ const contextMenus = {
|
|||||||
homeOption.className = 'folder-select-item folder-select-current';
|
homeOption.className = 'folder-select-item folder-select-current';
|
||||||
homeOption.innerHTML = `
|
homeOption.innerHTML = `
|
||||||
<i class="fas fa-check-circle check-icon"></i>
|
<i class="fas fa-check-circle check-icon"></i>
|
||||||
<span>${i18n ? i18n.t('dialogs.move_to_home') : 'Move to Home folder'}</span>
|
<span>${i18n.t('dialogs.move_to_home')}</span>
|
||||||
`;
|
`;
|
||||||
homeOption.addEventListener('click', () => {
|
homeOption.addEventListener('click', () => {
|
||||||
document.querySelectorAll('.folder-select-item').forEach((item) => {
|
document.querySelectorAll('.folder-select-item').forEach((item) => {
|
||||||
@@ -678,7 +678,7 @@ const contextMenus = {
|
|||||||
// Inside a subfolder with no children - show empty message
|
// Inside a subfolder with no children - show empty message
|
||||||
const emptyMsg = document.createElement('div');
|
const emptyMsg = document.createElement('div');
|
||||||
emptyMsg.className = 'folder-select-empty';
|
emptyMsg.className = 'folder-select-empty';
|
||||||
emptyMsg.innerHTML = `<i class="fas fa-folder-open"></i> <span>${i18n ? i18n.t('dialogs.no_subfolders') : 'No subfolders to navigate'}</span>`;
|
emptyMsg.innerHTML = `<i class="fas fa-folder-open"></i> <span>${i18n.t('dialogs.no_subfolders')}</span>`;
|
||||||
folderSelectContainer.appendChild(emptyMsg);
|
folderSelectContainer.appendChild(emptyMsg);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -686,9 +686,7 @@ const contextMenus = {
|
|||||||
app.selectedTargetFolderId = parentFolderId || '';
|
app.selectedTargetFolderId = parentFolderId || '';
|
||||||
|
|
||||||
// Translate new elements
|
// Translate new elements
|
||||||
if (i18n?.translateElement) {
|
i18n.translateElement(folderSelectContainer);
|
||||||
i18n.translateElement(folderSelectContainer);
|
|
||||||
}
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error loading folders:', error);
|
console.error('Error loading folders:', error);
|
||||||
}
|
}
|
||||||
@@ -798,8 +796,7 @@ const contextMenus = {
|
|||||||
const dialogHeader = shareDialog.querySelector('.share-dialog-header');
|
const dialogHeader = shareDialog.querySelector('.share-dialog-header');
|
||||||
if (dialogHeader) {
|
if (dialogHeader) {
|
||||||
const headerSpan = dialogHeader.querySelector('span');
|
const headerSpan = dialogHeader.querySelector('span');
|
||||||
const titleText =
|
const titleText = itemType === 'file' ? i18n.t('dialogs.share_file') : i18n.t('dialogs.share_folder');
|
||||||
itemType === 'file' ? (i18n ? i18n.t('dialogs.share_file') : 'Share file') : i18n ? i18n.t('dialogs.share_folder') : 'Share folder';
|
|
||||||
if (headerSpan) {
|
if (headerSpan) {
|
||||||
headerSpan.textContent = titleText;
|
headerSpan.textContent = titleText;
|
||||||
} else {
|
} else {
|
||||||
@@ -900,9 +897,9 @@ const contextMenus = {
|
|||||||
const shareId = btn.getAttribute('data-share-id');
|
const shareId = btn.getAttribute('data-share-id');
|
||||||
|
|
||||||
showConfirmDialog({
|
showConfirmDialog({
|
||||||
title: i18n ? i18n.t('dialogs.confirm_delete_share') : 'Delete link',
|
title: i18n.t('dialogs.confirm_delete_share'),
|
||||||
message: i18n ? i18n.t('dialogs.confirm_delete_share_msg') : 'Are you sure you want to delete this shared link?',
|
message: i18n.t('dialogs.confirm_delete_share_msg'),
|
||||||
confirmText: i18n ? i18n.t('actions.delete') : 'Delete'
|
confirmText: i18n.t('actions.delete')
|
||||||
}).then(async (confirmed) => {
|
}).then(async (confirmed) => {
|
||||||
if (confirmed) {
|
if (confirmed) {
|
||||||
await fileSharing.removeSharedLink(shareId);
|
await fileSharing.removeSharedLink(shareId);
|
||||||
@@ -997,10 +994,7 @@ const contextMenus = {
|
|||||||
ui.setSharedVisualState(item.id, item.type, true);
|
ui.setSharedVisualState(item.id, item.type, true);
|
||||||
|
|
||||||
// Show success message
|
// Show success message
|
||||||
ui.showNotification(
|
ui.showNotification(i18n.t('notifications.link_created'), i18n.t('notifications.share_success'));
|
||||||
i18n ? i18n.t('notifications.link_created') : 'Link created',
|
|
||||||
i18n ? i18n.t('notifications.share_success') : 'Shared link created successfully'
|
|
||||||
);
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error creating shared link:', error);
|
console.error('Error creating shared link:', error);
|
||||||
ui.showNotification('Error', error.message || 'Could not create shared link');
|
ui.showNotification('Error', error.message || 'Could not create shared link');
|
||||||
@@ -1088,7 +1082,7 @@ const contextMenus = {
|
|||||||
|
|
||||||
// Update files info
|
// Update files info
|
||||||
if (filesInfo) {
|
if (filesInfo) {
|
||||||
filesInfo.innerHTML = `<strong>${i18n ? i18n.t('music.selected_files', 'Selected:') : 'Selected:'} </strong>${file.name}`;
|
filesInfo.innerHTML = `<strong>${i18n.t('music.selected_files')} </strong>${file.name}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Reset selection
|
// Reset selection
|
||||||
@@ -1112,17 +1106,15 @@ const contextMenus = {
|
|||||||
this._renderPlaylistSelect(container, playlists);
|
this._renderPlaylistSelect(container, playlists);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error('Error loading playlists:', err);
|
console.error('Error loading playlists:', err);
|
||||||
container.innerHTML = `<div class="folder-select-empty">${i18n ? i18n.t('music.load_error', 'Error loading playlists') : 'Error loading playlists'}</div>`;
|
container.innerHTML = `<div class="folder-select-empty">${i18n.t('music.load_error')}</div>`;
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
_renderPlaylistSelect(container, playlists) {
|
_renderPlaylistSelect(container, playlists) {
|
||||||
const t = (key, fallback) => (i18n ? i18n.t(key, fallback) : fallback);
|
|
||||||
|
|
||||||
container.innerHTML = '';
|
container.innerHTML = '';
|
||||||
|
|
||||||
if (playlists.length === 0) {
|
if (playlists.length === 0) {
|
||||||
container.innerHTML = `<div class="folder-select-empty">${t('music.no_playlists', 'No playlists yet. Create one first!')}</div>`;
|
container.innerHTML = `<div class="folder-select-empty">${i18n.t('music.no_playlists')}</div>`;
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1133,7 +1125,7 @@ const contextMenus = {
|
|||||||
item.innerHTML = `
|
item.innerHTML = `
|
||||||
<i class="fas fa-list"></i>
|
<i class="fas fa-list"></i>
|
||||||
<span>${this._escapeHtml(playlist.name)}</span>
|
<span>${this._escapeHtml(playlist.name)}</span>
|
||||||
<span class="playlist-track-count">${playlist.track_count || 0} ${t('music.tracks', 'tracks')}</span>
|
<span class="playlist-track-count">${playlist.track_count || 0} ${i18n.t('music.tracks')}</span>
|
||||||
`;
|
`;
|
||||||
|
|
||||||
item.addEventListener('click', () => {
|
item.addEventListener('click', () => {
|
||||||
@@ -1176,10 +1168,7 @@ const contextMenus = {
|
|||||||
}
|
}
|
||||||
|
|
||||||
await resp.json();
|
await resp.json();
|
||||||
ui.showNotification(
|
ui.showNotification(i18n.t('music.added'), `${files.length} ${files.length === 1 ? 'track' : 'tracks'} ${i18n.t('music.added_to_playlist')}`);
|
||||||
i18n ? i18n.t('music.added', 'Added!') : 'Added!',
|
|
||||||
`${files.length} ${files.length === 1 ? 'track' : 'tracks'} ${i18n ? i18n.t('music.added_to_playlist', 'added to playlist') : 'added to playlist'}`
|
|
||||||
);
|
|
||||||
|
|
||||||
this.closePlaylistDialog();
|
this.closePlaylistDialog();
|
||||||
|
|
||||||
@@ -1189,7 +1178,7 @@ const contextMenus = {
|
|||||||
}
|
}
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error('Error adding to playlist:', err);
|
console.error('Error adding to playlist:', err);
|
||||||
ui.showNotification(i18n ? i18n.t('music.error', 'Error') : 'Error', err.message || i18n.t('music.add_error', 'Could not add tracks to playlist'));
|
ui.showNotification(i18n.t('music.error'), err.message || i18n.t('music.add_error'));
|
||||||
if (addBtn) addBtn.disabled = false;
|
if (addBtn) addBtn.disabled = false;
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -30,12 +30,12 @@ const fileOps = {
|
|||||||
|
|
||||||
/** Start a new upload batch in the notification bell */
|
/** Start a new upload batch in the notification bell */
|
||||||
_initUploadToast(totalFiles, folderName) {
|
_initUploadToast(totalFiles, folderName) {
|
||||||
this._currentBatchId = notifications ? notifications.addUploadBatch(totalFiles, folderName) : null;
|
this._currentBatchId = notifications.addUploadBatch(totalFiles, folderName);
|
||||||
},
|
},
|
||||||
|
|
||||||
/** Finalise the batch in the notification bell */
|
/** Finalise the batch in the notification bell */
|
||||||
_finishUploadToast(successCount, totalFiles) {
|
_finishUploadToast(successCount, totalFiles) {
|
||||||
if (notifications && this._currentBatchId) {
|
if (this._currentBatchId) {
|
||||||
notifications.finishBatch(this._currentBatchId, successCount, totalFiles);
|
notifications.finishBatch(this._currentBatchId, successCount, totalFiles);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -361,7 +361,7 @@ const fileOps = {
|
|||||||
progressBar.style.width = `${(uploadedCount / totalFiles) * 100}%`;
|
progressBar.style.width = `${(uploadedCount / totalFiles) * 100}%`;
|
||||||
}
|
}
|
||||||
// Notify bell of per-file completion
|
// Notify bell of per-file completion
|
||||||
if (notifications && batchId) {
|
if (batchId) {
|
||||||
try {
|
try {
|
||||||
notifications.fileCompleted(batchId, result.ok);
|
notifications.fileCompleted(batchId, result.ok);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
@@ -383,7 +383,7 @@ const fileOps = {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
if (result.isQuotaError) {
|
if (result.isQuotaError) {
|
||||||
const msg = result.errorMsg || i18n?.t('storage_quota_exceeded') || 'Storage quota exceeded';
|
const msg = result.errorMsg || i18n.t('storage_quota_exceeded');
|
||||||
if (notifications) {
|
if (notifications) {
|
||||||
notifications.addNotification({
|
notifications.addNotification({
|
||||||
icon: 'fa-exclamation-triangle',
|
icon: 'fa-exclamation-triangle',
|
||||||
@@ -591,7 +591,7 @@ const fileOps = {
|
|||||||
console.warn(`[SKIP] #${idx} ${rel} — cannot read 0-byte file (FIFO/pipe?), skipping`);
|
console.warn(`[SKIP] #${idx} ${rel} — cannot read 0-byte file (FIFO/pipe?), skipping`);
|
||||||
uploadedCount++;
|
uploadedCount++;
|
||||||
successCount++;
|
successCount++;
|
||||||
if (notifications && batchId) {
|
if (batchId) {
|
||||||
try {
|
try {
|
||||||
notifications.fileCompleted(batchId, true);
|
notifications.fileCompleted(batchId, true);
|
||||||
} catch (_) {}
|
} catch (_) {}
|
||||||
@@ -623,7 +623,7 @@ const fileOps = {
|
|||||||
|
|
||||||
uploadedCount++;
|
uploadedCount++;
|
||||||
|
|
||||||
if (notifications && batchId) {
|
if (batchId) {
|
||||||
try {
|
try {
|
||||||
notifications.fileCompleted(batchId, result.ok);
|
notifications.fileCompleted(batchId, result.ok);
|
||||||
} catch (_) {}
|
} catch (_) {}
|
||||||
@@ -994,10 +994,7 @@ const fileOps = {
|
|||||||
console.log('Response status:', response.status);
|
console.log('Response status:', response.status);
|
||||||
|
|
||||||
if (response.ok) {
|
if (response.ok) {
|
||||||
ui.showNotification(
|
ui.showNotification(i18n.t('notifications.file_renamed'), i18n.t('notifications.file_renamed_to', { name: newName }));
|
||||||
i18n ? i18n.t('notifications.file_renamed') : 'File renamed',
|
|
||||||
i18n ? i18n.t('notifications.file_renamed_to', { name: newName }) : `File renamed to "${newName}"`
|
|
||||||
);
|
|
||||||
return true;
|
return true;
|
||||||
} else {
|
} else {
|
||||||
const errorText = await response.text();
|
const errorText = await response.text();
|
||||||
@@ -1075,9 +1072,9 @@ const fileOps = {
|
|||||||
*/
|
*/
|
||||||
async deleteFile(fileId, fileName) {
|
async deleteFile(fileId, fileName) {
|
||||||
const confirmed = await showConfirmDialog({
|
const confirmed = await showConfirmDialog({
|
||||||
title: i18n ? i18n.t('dialogs.confirm_delete') : 'Move to trash',
|
title: i18n.t('dialogs.confirm_delete'),
|
||||||
message: i18n ? i18n.t('dialogs.confirm_delete_file', { name: fileName }) : `Are you sure you want to move the file "${fileName}" to trash?`,
|
message: i18n.t('dialogs.confirm_delete_file', { name: fileName }),
|
||||||
confirmText: i18n ? i18n.t('actions.delete') : 'Delete'
|
confirmText: i18n.t('actions.delete')
|
||||||
});
|
});
|
||||||
if (!confirmed) return false;
|
if (!confirmed) return false;
|
||||||
|
|
||||||
@@ -1123,11 +1120,9 @@ const fileOps = {
|
|||||||
*/
|
*/
|
||||||
async deleteFolder(folderId, folderName) {
|
async deleteFolder(folderId, folderName) {
|
||||||
const confirmed = await showConfirmDialog({
|
const confirmed = await showConfirmDialog({
|
||||||
title: i18n ? i18n.t('dialogs.confirm_delete') : 'Move to trash',
|
title: i18n.t('dialogs.confirm_delete'),
|
||||||
message: i18n
|
message: i18n.t('dialogs.confirm_delete_folder', { name: folderName }),
|
||||||
? i18n.t('dialogs.confirm_delete_folder', { name: folderName })
|
confirmText: i18n.t('actions.delete')
|
||||||
: `Are you sure you want to move the folder "${folderName}" and all its contents to trash?`,
|
|
||||||
confirmText: i18n ? i18n.t('actions.delete') : 'Delete'
|
|
||||||
});
|
});
|
||||||
if (!confirmed) return false;
|
if (!confirmed) return false;
|
||||||
|
|
||||||
@@ -1234,11 +1229,9 @@ const fileOps = {
|
|||||||
*/
|
*/
|
||||||
async deletePermanently(trashId) {
|
async deletePermanently(trashId) {
|
||||||
const confirmed = await showConfirmDialog({
|
const confirmed = await showConfirmDialog({
|
||||||
title: i18n ? i18n.t('dialogs.confirm_permanent_delete') : 'Delete permanently',
|
title: i18n.t('dialogs.confirm_permanent_delete'),
|
||||||
message: i18n
|
message: i18n.t('dialogs.confirm_permanent_delete_msg'),
|
||||||
? i18n.t('dialogs.confirm_permanent_delete_msg')
|
confirmText: i18n.t('actions.delete_permanently')
|
||||||
: 'Are you sure you want to permanently delete this item? This action cannot be undone.',
|
|
||||||
confirmText: i18n ? i18n.t('actions.delete_permanently') : 'Delete permanently'
|
|
||||||
});
|
});
|
||||||
if (!confirmed) return false;
|
if (!confirmed) return false;
|
||||||
|
|
||||||
@@ -1268,9 +1261,9 @@ const fileOps = {
|
|||||||
*/
|
*/
|
||||||
async emptyTrash() {
|
async emptyTrash() {
|
||||||
const confirmed = await showConfirmDialog({
|
const confirmed = await showConfirmDialog({
|
||||||
title: i18n ? i18n.t('dialogs.confirm_empty_trash') : 'Empty trash',
|
title: i18n.t('dialogs.confirm_empty_trash'),
|
||||||
message: i18n ? i18n.t('trash.empty_confirm') : 'Are you sure you want to empty the trash? This action will permanently delete all items.',
|
message: i18n.t('trash.empty_confirm'),
|
||||||
confirmText: i18n ? i18n.t('actions.empty_trash') : 'Empty trash'
|
confirmText: i18n.t('actions.empty_trash')
|
||||||
});
|
});
|
||||||
if (!confirmed) return false;
|
if (!confirmed) return false;
|
||||||
|
|
||||||
|
|||||||
@@ -50,12 +50,8 @@ const multiSelect = {
|
|||||||
// ── Helpers for i18n ────────────────────────────────────
|
// ── Helpers for i18n ────────────────────────────────────
|
||||||
|
|
||||||
_t(key, vars) {
|
_t(key, vars) {
|
||||||
if (i18n && typeof i18n.t === 'function') {
|
const val = i18n.t(key, vars);
|
||||||
const val = i18n.t(key, vars);
|
return val !== key ? val : null;
|
||||||
// If i18n returned the key itself, it's missing → fall back
|
|
||||||
if (val && val !== key) return val;
|
|
||||||
}
|
|
||||||
return null;
|
|
||||||
},
|
},
|
||||||
|
|
||||||
// ── Selection state management ──────────────────────────
|
// ── Selection state management ──────────────────────────
|
||||||
|
|||||||
@@ -110,10 +110,7 @@ const favorites = {
|
|||||||
|
|
||||||
// Notify user
|
// Notify user
|
||||||
if (ui?.showNotification) {
|
if (ui?.showNotification) {
|
||||||
ui.showNotification(
|
ui.showNotification(i18n.t('favorites.added_title'), `"${name}" ${i18n.t('favorites.added_msg')}`);
|
||||||
i18n ? i18n.t('favorites.added_title') : 'Added to favorites',
|
|
||||||
`"${name}" ${i18n ? i18n.t('favorites.added_msg') : 'added to favorites'}`
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
@@ -145,10 +142,7 @@ const favorites = {
|
|||||||
this._cache.delete(this._cacheKey(id, type));
|
this._cache.delete(this._cacheKey(id, type));
|
||||||
|
|
||||||
if (ui?.showNotification) {
|
if (ui?.showNotification) {
|
||||||
ui.showNotification(
|
ui.showNotification(i18n.t('favorites.removed_title'), `"${itemName}" ${i18n.t('favorites.removed_msg')}`);
|
||||||
i18n ? i18n.t('favorites.removed_title') : 'Removed from favorites',
|
|
||||||
`"${itemName}" ${i18n ? i18n.t('favorites.removed_msg') : 'removed from favorites'}`
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
@@ -182,8 +176,8 @@ const favorites = {
|
|||||||
if (this._cache.size === 0) {
|
if (this._cache.size === 0) {
|
||||||
ui.showError(`
|
ui.showError(`
|
||||||
<i class="fas fa-star empty-state-icon"></i>
|
<i class="fas fa-star empty-state-icon"></i>
|
||||||
<p>${i18n ? i18n.t('favorites.empty_state') : 'No favorite items'}</p>
|
<p>${i18n.t('favorites.empty_state')}</p>
|
||||||
<p>${i18n ? i18n.t('favorites.empty_hint') : 'To mark as favorite, right-click on any file or folder'}</p>
|
<p>${i18n.t('favorites.empty_hint')}</p>
|
||||||
`);
|
`);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|||||||
+110
-168
@@ -87,9 +87,6 @@ const musicView = {
|
|||||||
if (!this._container) return;
|
if (!this._container) return;
|
||||||
|
|
||||||
// FIXME should call directly
|
// FIXME should call directly
|
||||||
const t = (key, _fallback = '') => {
|
|
||||||
return i18n.t(key);
|
|
||||||
};
|
|
||||||
|
|
||||||
// Empty state: no playlists at all — show full-width centered onboarding
|
// Empty state: no playlists at all — show full-width centered onboarding
|
||||||
if (this.playlists.length === 0) {
|
if (this.playlists.length === 0) {
|
||||||
@@ -98,11 +95,11 @@ const musicView = {
|
|||||||
<div class="music-empty-state-icon">
|
<div class="music-empty-state-icon">
|
||||||
<i class="fas fa-music"></i>
|
<i class="fas fa-music"></i>
|
||||||
</div>
|
</div>
|
||||||
<h3 class="music-empty-state-title">${t('music.no_playlists', 'No playlists yet')}</h3>
|
<h3 class="music-empty-state-title">${i18n.t('music.no_playlists')}</h3>
|
||||||
<p class="music-empty-state-desc">${t('music.empty_hint', 'Create your first playlist to start organizing your music')}</p>
|
<p class="music-empty-state-desc">${i18n.t('music.empty_hint')}</p>
|
||||||
<button class="btn btn-primary" id="music-create-playlist-btn">
|
<button class="btn btn-primary" id="music-create-playlist-btn">
|
||||||
<i class="fas fa-plus"></i>
|
<i class="fas fa-plus"></i>
|
||||||
<span>${t('music.create_playlist', 'Create Playlist')}</span>
|
<span>${i18n.t('music.create_playlist')}</span>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
`;
|
`;
|
||||||
@@ -118,8 +115,8 @@ const musicView = {
|
|||||||
<div class="music-content">
|
<div class="music-content">
|
||||||
<div class="music-sidebar">
|
<div class="music-sidebar">
|
||||||
<div class="music-sidebar-header">
|
<div class="music-sidebar-header">
|
||||||
<h3>${t('music.playlists', 'Playlists')}</h3>
|
<h3>${i18n.t('music.playlists')}</h3>
|
||||||
<button class="music-sidebar-add-btn" id="music-create-playlist-btn" title="${t('music.create_playlist', 'Create Playlist')}">
|
<button class="music-sidebar-add-btn" id="music-create-playlist-btn" title="${i18n.t('music.create_playlist')}">
|
||||||
<i class="fas fa-plus"></i>
|
<i class="fas fa-plus"></i>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
@@ -128,44 +125,44 @@ const musicView = {
|
|||||||
<div class="music-main">
|
<div class="music-main">
|
||||||
<div class="music-welcome">
|
<div class="music-welcome">
|
||||||
<i class="fas fa-music"></i>
|
<i class="fas fa-music"></i>
|
||||||
<h3>${t('music.select_playlist', 'Select a playlist')}</h3>
|
<h3>${i18n.t('music.select_playlist')}</h3>
|
||||||
<p>${t('music.select_hint', 'Choose a playlist from the sidebar or create a new one')}</p>
|
<p>${i18n.t('music.select_hint')}</p>
|
||||||
</div>
|
</div>
|
||||||
<div class="music-playlist-detail hidden" id="music-playlist-detail">
|
<div class="music-playlist-detail hidden" id="music-playlist-detail">
|
||||||
<div class="music-playlist-header">
|
<div class="music-playlist-header">
|
||||||
<div class="music-playlist-cover" id="music-playlist-cover" title="${t('music.set_cover', 'Set cover')}">
|
<div class="music-playlist-cover" id="music-playlist-cover" title="${i18n.t('music.set_cover')}">
|
||||||
<i class="fas fa-music"></i>
|
<i class="fas fa-music"></i>
|
||||||
</div>
|
</div>
|
||||||
<div class="music-playlist-info">
|
<div class="music-playlist-info">
|
||||||
<h2 id="music-playlist-name"></h2>
|
<h2 id="music-playlist-name"></h2>
|
||||||
<p id="music-playlist-meta"></p>
|
<p id="music-playlist-meta"></p>
|
||||||
<span class="music-public-badge hidden" id="music-public-badge">
|
<span class="music-public-badge hidden" id="music-public-badge">
|
||||||
<i class="fas fa-globe"></i> <span id="music-public-text">${t('music.public', 'Public')}</span>
|
<i class="fas fa-globe"></i> <span id="music-public-text">${i18n.t('music.public')}</span>
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="music-playlist-actions">
|
<div class="music-playlist-actions">
|
||||||
<button class="btn btn-secondary" id="music-play-all-btn">
|
<button class="btn btn-secondary" id="music-play-all-btn">
|
||||||
<i class="fas fa-play"></i>
|
<i class="fas fa-play"></i>
|
||||||
<span>${t('music.play_all', 'Play All')}</span>
|
<span>${i18n.t('music.play_all')}</span>
|
||||||
</button>
|
</button>
|
||||||
<button class="btn btn-secondary" id="music-shuffle-btn">
|
<button class="btn btn-secondary" id="music-shuffle-btn">
|
||||||
<i class="fas fa-shuffle"></i>
|
<i class="fas fa-shuffle"></i>
|
||||||
</button>
|
</button>
|
||||||
<button class="btn btn-secondary" id="music-add-tracks-btn">
|
<button class="btn btn-secondary" id="music-add-tracks-btn">
|
||||||
<i class="fas fa-plus"></i>
|
<i class="fas fa-plus"></i>
|
||||||
<span>${t('music.add_tracks', 'Add Tracks')}</span>
|
<span>${i18n.t('music.add_tracks')}</span>
|
||||||
</button>
|
</button>
|
||||||
<button class="btn btn-secondary" id="music-edit-playlist-btn" title="${t('music.edit', 'Edit')}">
|
<button class="btn btn-secondary" id="music-edit-playlist-btn" title="${i18n.t('music.edit')}">
|
||||||
<i class="fas fa-pen"></i>
|
<i class="fas fa-pen"></i>
|
||||||
</button>
|
</button>
|
||||||
<button class="btn btn-secondary" id="music-share-playlist-btn" title="${t('music.share', 'Share')}">
|
<button class="btn btn-secondary" id="music-share-playlist-btn" title="${i18n.t('music.share')}">
|
||||||
<i class="fas fa-share-alt"></i>
|
<i class="fas fa-share-alt"></i>
|
||||||
</button>
|
</button>
|
||||||
<button class="btn btn-secondary" id="music-manage-shares-btn" title="${t('music.manage_shares', 'Manage Shares')}">
|
<button class="btn btn-secondary" id="music-manage-shares-btn" title="${i18n.t('music.manage_shares')}">
|
||||||
<i class="fas fa-users"></i>
|
<i class="fas fa-users"></i>
|
||||||
</button>
|
</button>
|
||||||
<button class="btn btn-secondary" id="music-toggle-public-btn" title="${t('music.toggle_public', 'Toggle public')}">
|
<button class="btn btn-secondary" id="music-toggle-public-btn" title="${i18n.t('music.toggle_public')}">
|
||||||
<i class="fas fa-globe"></i>
|
<i class="fas fa-globe"></i>
|
||||||
</button>
|
</button>
|
||||||
<button class="btn btn-secondary" id="music-delete-playlist-btn">
|
<button class="btn btn-secondary" id="music-delete-playlist-btn">
|
||||||
@@ -186,15 +183,11 @@ const musicView = {
|
|||||||
const listEl = document.getElementById('music-playlist-list');
|
const listEl = document.getElementById('music-playlist-list');
|
||||||
if (!listEl) return;
|
if (!listEl) return;
|
||||||
|
|
||||||
const t = (key, fallback = '') => {
|
|
||||||
return i18n?.t ? i18n.t(key) : fallback || key;
|
|
||||||
};
|
|
||||||
|
|
||||||
if (this.playlists.length === 0) {
|
if (this.playlists.length === 0) {
|
||||||
listEl.innerHTML = `
|
listEl.innerHTML = `
|
||||||
<div class="music-empty">
|
<div class="music-empty">
|
||||||
<i class="fas fa-music"></i>
|
<i class="fas fa-music"></i>
|
||||||
<p>${t('music.no_playlists', 'No playlists yet')}</p>
|
<p>${i18n.t('music.no_playlists')}</p>
|
||||||
</div>
|
</div>
|
||||||
`;
|
`;
|
||||||
return;
|
return;
|
||||||
@@ -209,7 +202,7 @@ const musicView = {
|
|||||||
</div>
|
</div>
|
||||||
<div class="music-playlist-item-info">
|
<div class="music-playlist-item-info">
|
||||||
<span class="music-playlist-item-name">${this._escapeHtml(p.name)}</span>
|
<span class="music-playlist-item-name">${this._escapeHtml(p.name)}</span>
|
||||||
<span class="music-playlist-item-count">${p.track_count || 0} ${t('music.tracks', 'tracks')}</span>
|
<span class="music-playlist-item-count">${p.track_count || 0} ${i18n.t('music.tracks')}</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
`
|
`
|
||||||
@@ -291,10 +284,7 @@ const musicView = {
|
|||||||
if (detailEl) detailEl.classList.remove('hidden');
|
if (detailEl) detailEl.classList.remove('hidden');
|
||||||
if (nameEl) nameEl.textContent = playlist.name;
|
if (nameEl) nameEl.textContent = playlist.name;
|
||||||
if (metaEl) {
|
if (metaEl) {
|
||||||
const t = (key, fallback = '') => {
|
metaEl.textContent = `${playlist.track_count || 0} ${i18n.t('music.tracks')}`;
|
||||||
return i18n?.t ? i18n.t(key) : fallback || key;
|
|
||||||
};
|
|
||||||
metaEl.textContent = `${playlist.track_count || 0} ${t('music.tracks', 'tracks')}`;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Cover art
|
// Cover art
|
||||||
@@ -314,8 +304,7 @@ const musicView = {
|
|||||||
}
|
}
|
||||||
const togglePublicBtn = document.getElementById('music-toggle-public-btn');
|
const togglePublicBtn = document.getElementById('music-toggle-public-btn');
|
||||||
if (togglePublicBtn) {
|
if (togglePublicBtn) {
|
||||||
const t2 = (key, fallback = '') => (i18n?.t ? i18n.t(key) : fallback || key);
|
togglePublicBtn.title = playlist.is_public ? i18n.t('music.make_private') : i18n.t('music.make_public');
|
||||||
togglePublicBtn.title = playlist.is_public ? t2('music.make_private', 'Make private') : t2('music.make_public', 'Make public');
|
|
||||||
togglePublicBtn.classList.toggle('active', playlist.is_public);
|
togglePublicBtn.classList.toggle('active', playlist.is_public);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -352,15 +341,11 @@ const musicView = {
|
|||||||
const trackListEl = document.getElementById('music-track-list');
|
const trackListEl = document.getElementById('music-track-list');
|
||||||
if (!trackListEl) return;
|
if (!trackListEl) return;
|
||||||
|
|
||||||
const t = (key, fallback = '') => {
|
|
||||||
return i18n?.t ? i18n.t(key) : fallback || key;
|
|
||||||
};
|
|
||||||
|
|
||||||
if (this.currentTracks.length === 0) {
|
if (this.currentTracks.length === 0) {
|
||||||
trackListEl.innerHTML = `
|
trackListEl.innerHTML = `
|
||||||
<div class="music-empty">
|
<div class="music-empty">
|
||||||
<i class="fas fa-music"></i>
|
<i class="fas fa-music"></i>
|
||||||
<p>${t('music.no_tracks', 'No tracks in this playlist')}</p>
|
<p>${i18n.t('music.no_tracks')}</p>
|
||||||
</div>
|
</div>
|
||||||
`;
|
`;
|
||||||
return;
|
return;
|
||||||
@@ -370,9 +355,9 @@ const musicView = {
|
|||||||
<div class="music-track-header">
|
<div class="music-track-header">
|
||||||
<span class="music-track-col music-track-drag"></span>
|
<span class="music-track-col music-track-drag"></span>
|
||||||
<span class="music-track-col music-track-num">#</span>
|
<span class="music-track-col music-track-num">#</span>
|
||||||
<span class="music-track-col music-track-title">${t('music.title', 'Title')}</span>
|
<span class="music-track-col music-track-title">${i18n.t('music.title')}</span>
|
||||||
<span class="music-track-col music-track-artist">${t('music.artist', 'Artist')}</span>
|
<span class="music-track-col music-track-artist">${i18n.t('music.artist')}</span>
|
||||||
<span class="music-track-col music-track-album">${t('music.album', 'Album')}</span>
|
<span class="music-track-col music-track-album">${i18n.t('music.album')}</span>
|
||||||
<span class="music-track-col music-track-duration"><i class="far fa-clock"></i></span>
|
<span class="music-track-col music-track-duration"><i class="far fa-clock"></i></span>
|
||||||
<span class="music-track-col music-track-actions"></span>
|
<span class="music-track-col music-track-actions"></span>
|
||||||
</div>
|
</div>
|
||||||
@@ -387,13 +372,13 @@ const musicView = {
|
|||||||
</span>
|
</span>
|
||||||
<span class="music-track-col music-track-title">
|
<span class="music-track-col music-track-title">
|
||||||
<i class="fas fa-music music-track-icon"></i>
|
<i class="fas fa-music music-track-icon"></i>
|
||||||
<span class="music-track-name">${this._escapeHtml(track.title || track.file_name || t('music.unknown_title', 'Unknown'))}</span>
|
<span class="music-track-name">${this._escapeHtml(track.title || track.file_name || i18n.t('music.unknown_title'))}</span>
|
||||||
</span>
|
</span>
|
||||||
<span class="music-track-col music-track-artist">${this._escapeHtml(track.artist || t('music.unknown_artist', 'Unknown Artist'))}</span>
|
<span class="music-track-col music-track-artist">${this._escapeHtml(track.artist || i18n.t('music.unknown_artist'))}</span>
|
||||||
<span class="music-track-col music-track-album">${this._escapeHtml(track.album || '-')}</span>
|
<span class="music-track-col music-track-album">${this._escapeHtml(track.album || '-')}</span>
|
||||||
<span class="music-track-col music-track-duration">${this._formatDuration(track.duration_secs)}</span>
|
<span class="music-track-col music-track-duration">${this._formatDuration(track.duration_secs)}</span>
|
||||||
<span class="music-track-col music-track-actions">
|
<span class="music-track-col music-track-actions">
|
||||||
<button class="music-track-remove-btn" title="${t('music.remove', 'Remove')}"><i class="fas fa-times"></i></button>
|
<button class="music-track-remove-btn" title="${i18n.t('music.remove')}"><i class="fas fa-times"></i></button>
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
`
|
`
|
||||||
@@ -467,10 +452,7 @@ const musicView = {
|
|||||||
_playTrack(idx) {
|
_playTrack(idx) {
|
||||||
if (!this.currentTracks[idx]) return;
|
if (!this.currentTracks[idx]) return;
|
||||||
|
|
||||||
const t = (key, fallback = '') => {
|
musicPlayer.setQueue(this.currentTracks, this.currentPlaylist?.name || i18n.t('music.playlists'));
|
||||||
return i18n?.t ? i18n.t(key) : fallback || key;
|
|
||||||
};
|
|
||||||
musicPlayer.setQueue(this.currentTracks, this.currentPlaylist?.name || t('music.playlists', 'Playlist'));
|
|
||||||
musicPlayer.playTrack(idx);
|
musicPlayer.playTrack(idx);
|
||||||
},
|
},
|
||||||
|
|
||||||
@@ -487,25 +469,18 @@ const musicView = {
|
|||||||
const j = Math.floor(Math.random() * (i + 1));
|
const j = Math.floor(Math.random() * (i + 1));
|
||||||
[shuffled[i], shuffled[j]] = [shuffled[j], shuffled[i]];
|
[shuffled[i], shuffled[j]] = [shuffled[j], shuffled[i]];
|
||||||
}
|
}
|
||||||
const t = (key, fallback = '') => {
|
musicPlayer.setQueue(shuffled, this.currentPlaylist?.name || i18n.t('music.shuffle'));
|
||||||
return i18n?.t ? i18n.t(key) : fallback || key;
|
|
||||||
};
|
|
||||||
musicPlayer.setQueue(shuffled, this.currentPlaylist?.name || t('music.shuffle', 'Shuffle'));
|
|
||||||
musicPlayer.playTrack(0);
|
musicPlayer.playTrack(0);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
async _showCreatePlaylistDialog() {
|
async _showCreatePlaylistDialog() {
|
||||||
const t = (key, fallback = '') => {
|
|
||||||
return i18n?.t ? i18n.t(key) : fallback || key;
|
|
||||||
};
|
|
||||||
|
|
||||||
const name = await Modal.prompt({
|
const name = await Modal.prompt({
|
||||||
title: t('music.create_playlist', 'Create Playlist'),
|
title: i18n.t('music.create_playlist'),
|
||||||
label: t('music.playlist_name', 'Playlist name'),
|
label: i18n.t('music.playlist_name'),
|
||||||
placeholder: t('music.playlist_name', 'Playlist name'),
|
placeholder: i18n.t('music.playlist_name'),
|
||||||
icon: 'fa-music',
|
icon: 'fa-music',
|
||||||
confirmText: t('music.create', 'Create')
|
confirmText: i18n.t('music.create')
|
||||||
});
|
});
|
||||||
if (!name?.trim()) return;
|
if (!name?.trim()) return;
|
||||||
|
|
||||||
@@ -513,9 +488,6 @@ const musicView = {
|
|||||||
},
|
},
|
||||||
|
|
||||||
async _createPlaylist(name) {
|
async _createPlaylist(name) {
|
||||||
const t = (key, fallback = '') => {
|
|
||||||
return i18n?.t ? i18n.t(key) : fallback || key;
|
|
||||||
};
|
|
||||||
const createBtn = document.getElementById('music-create-playlist-btn');
|
const createBtn = document.getElementById('music-create-playlist-btn');
|
||||||
if (createBtn) createBtn.disabled = true;
|
if (createBtn) createBtn.disabled = true;
|
||||||
try {
|
try {
|
||||||
@@ -536,7 +508,7 @@ const musicView = {
|
|||||||
notifications.addNotification({
|
notifications.addNotification({
|
||||||
icon: 'fa-check-circle',
|
icon: 'fa-check-circle',
|
||||||
iconClass: 'upload',
|
iconClass: 'upload',
|
||||||
title: t('music.create_playlist', 'Create Playlist'),
|
title: i18n.t('music.create_playlist'),
|
||||||
text: name
|
text: name
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -546,7 +518,7 @@ const musicView = {
|
|||||||
notifications.addNotification({
|
notifications.addNotification({
|
||||||
icon: 'fa-exclamation-circle',
|
icon: 'fa-exclamation-circle',
|
||||||
iconClass: 'error',
|
iconClass: 'error',
|
||||||
title: t('music.error', 'Error'),
|
title: i18n.t('music.error'),
|
||||||
text: err.message
|
text: err.message
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -556,20 +528,16 @@ const musicView = {
|
|||||||
},
|
},
|
||||||
|
|
||||||
async _deletePlaylist() {
|
async _deletePlaylist() {
|
||||||
const t = (key, fallback = '') => {
|
|
||||||
return i18n?.t ? i18n.t(key) : fallback || key;
|
|
||||||
};
|
|
||||||
|
|
||||||
if (!this.currentPlaylist) return;
|
if (!this.currentPlaylist) return;
|
||||||
|
|
||||||
const confirmed = await new Promise((resolve) => {
|
const confirmed = await new Promise((resolve) => {
|
||||||
Modal.prompt({
|
Modal.prompt({
|
||||||
title: t('music.delete', 'Delete'),
|
title: i18n.t('music.delete'),
|
||||||
label: t('music.confirm_delete', 'Delete this playlist?'),
|
label: i18n.t('music.confirm_delete'),
|
||||||
placeholder: '',
|
placeholder: '',
|
||||||
value: this.currentPlaylist.name,
|
value: this.currentPlaylist.name,
|
||||||
icon: 'fa-trash',
|
icon: 'fa-trash',
|
||||||
confirmText: t('music.delete', 'Delete')
|
confirmText: i18n.t('music.delete')
|
||||||
}).then((val) => resolve(val !== null));
|
}).then((val) => resolve(val !== null));
|
||||||
});
|
});
|
||||||
if (!confirmed) return;
|
if (!confirmed) return;
|
||||||
@@ -591,7 +559,7 @@ const musicView = {
|
|||||||
this.currentTracks = [];
|
this.currentTracks = [];
|
||||||
this._renderPlaylists();
|
this._renderPlaylists();
|
||||||
if (notifications) {
|
if (notifications) {
|
||||||
notifications.addNotification({ icon: 'fa-check-circle', iconClass: 'upload', title: t('music.delete', 'Delete'), text: deletedName });
|
notifications.addNotification({ icon: 'fa-check-circle', iconClass: 'upload', title: i18n.t('music.delete'), text: deletedName });
|
||||||
}
|
}
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error('Delete playlist error:', err);
|
console.error('Delete playlist error:', err);
|
||||||
@@ -599,7 +567,7 @@ const musicView = {
|
|||||||
notifications.addNotification({
|
notifications.addNotification({
|
||||||
icon: 'fa-exclamation-circle',
|
icon: 'fa-exclamation-circle',
|
||||||
iconClass: 'error',
|
iconClass: 'error',
|
||||||
title: t('music.error', 'Error'),
|
title: i18n.t('music.error'),
|
||||||
text: err.message
|
text: err.message
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -644,17 +612,14 @@ const musicView = {
|
|||||||
|
|
||||||
async _showEditPlaylistDialog() {
|
async _showEditPlaylistDialog() {
|
||||||
if (!this.currentPlaylist) return;
|
if (!this.currentPlaylist) return;
|
||||||
const t = (key, fallback = '') => {
|
|
||||||
return i18n?.t ? i18n.t(key) : fallback || key;
|
|
||||||
};
|
|
||||||
|
|
||||||
const newName = await Modal.prompt({
|
const newName = await Modal.prompt({
|
||||||
title: t('music.edit', 'Edit'),
|
title: i18n.t('music.edit'),
|
||||||
label: t('music.playlist_name', 'Playlist name'),
|
label: i18n.t('music.playlist_name'),
|
||||||
placeholder: t('music.playlist_name', 'Playlist name'),
|
placeholder: i18n.t('music.playlist_name'),
|
||||||
value: this.currentPlaylist.name,
|
value: this.currentPlaylist.name,
|
||||||
icon: 'fa-pen',
|
icon: 'fa-pen',
|
||||||
confirmText: t('actions.confirm', 'Save')
|
confirmText: i18n.t('actions.confirm')
|
||||||
});
|
});
|
||||||
if (!newName?.trim() || newName.trim() === this.currentPlaylist.name) return;
|
if (!newName?.trim() || newName.trim() === this.currentPlaylist.name) return;
|
||||||
|
|
||||||
@@ -681,7 +646,7 @@ const musicView = {
|
|||||||
notifications.addNotification({
|
notifications.addNotification({
|
||||||
icon: 'fa-exclamation-circle',
|
icon: 'fa-exclamation-circle',
|
||||||
iconClass: 'error',
|
iconClass: 'error',
|
||||||
title: t('music.error', 'Error'),
|
title: i18n.t('music.error'),
|
||||||
text: err.message
|
text: err.message
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -690,16 +655,13 @@ const musicView = {
|
|||||||
|
|
||||||
async _showSharePlaylistDialog() {
|
async _showSharePlaylistDialog() {
|
||||||
if (!this.currentPlaylist) return;
|
if (!this.currentPlaylist) return;
|
||||||
const t = (key, fallback = '') => {
|
|
||||||
return i18n?.t ? i18n.t(key) : fallback || key;
|
|
||||||
};
|
|
||||||
|
|
||||||
const userId = await Modal.prompt({
|
const userId = await Modal.prompt({
|
||||||
title: t('music.share', 'Share'),
|
title: i18n.t('music.share'),
|
||||||
label: t('music.share_with_user', 'User ID or email'),
|
label: i18n.t('music.share_with_user'),
|
||||||
placeholder: t('music.share_with_user', 'User ID or email'),
|
placeholder: i18n.t('music.share_with_user'),
|
||||||
icon: 'fa-share-alt',
|
icon: 'fa-share-alt',
|
||||||
confirmText: t('music.share', 'Share')
|
confirmText: i18n.t('music.share')
|
||||||
});
|
});
|
||||||
if (!userId?.trim()) return;
|
if (!userId?.trim()) return;
|
||||||
|
|
||||||
@@ -717,8 +679,8 @@ const musicView = {
|
|||||||
notifications.addNotification({
|
notifications.addNotification({
|
||||||
icon: 'fa-check-circle',
|
icon: 'fa-check-circle',
|
||||||
iconClass: 'upload',
|
iconClass: 'upload',
|
||||||
title: t('music.share', 'Share'),
|
title: i18n.t('music.share'),
|
||||||
text: t('music.added', 'Added!')
|
text: i18n.t('music.added')
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
@@ -727,7 +689,7 @@ const musicView = {
|
|||||||
notifications.addNotification({
|
notifications.addNotification({
|
||||||
icon: 'fa-exclamation-circle',
|
icon: 'fa-exclamation-circle',
|
||||||
iconClass: 'error',
|
iconClass: 'error',
|
||||||
title: t('music.error', 'Error'),
|
title: i18n.t('music.error'),
|
||||||
text: err.message
|
text: err.message
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -736,9 +698,6 @@ const musicView = {
|
|||||||
|
|
||||||
async _showAddTracksDialog() {
|
async _showAddTracksDialog() {
|
||||||
if (!this.currentPlaylist) return;
|
if (!this.currentPlaylist) return;
|
||||||
const t = (key, fallback = '') => {
|
|
||||||
return i18n?.t ? i18n.t(key) : fallback || key;
|
|
||||||
};
|
|
||||||
|
|
||||||
// ── Build modal overlay ──
|
// ── Build modal overlay ──
|
||||||
const overlay = document.createElement('div');
|
const overlay = document.createElement('div');
|
||||||
@@ -746,23 +705,23 @@ const musicView = {
|
|||||||
overlay.innerHTML = `
|
overlay.innerHTML = `
|
||||||
<div class="music-picker-modal">
|
<div class="music-picker-modal">
|
||||||
<div class="music-picker-header">
|
<div class="music-picker-header">
|
||||||
<h3><i class="fas fa-music"></i> ${t('music.add_tracks', 'Add Tracks')}</h3>
|
<h3><i class="fas fa-music"></i> ${i18n.t('music.add_tracks')}</h3>
|
||||||
<button class="music-picker-close" title="${t('common.close', 'Close')}">×</button>
|
<button class="music-picker-close" title="${i18n.t('actions.close')}">×</button>
|
||||||
</div>
|
</div>
|
||||||
<div class="music-picker-search">
|
<div class="music-picker-search">
|
||||||
<i class="fas fa-search"></i>
|
<i class="fas fa-search"></i>
|
||||||
<input type="text" id="music-picker-query"
|
<input type="text" id="music-picker-query"
|
||||||
placeholder="${t('music.search_audio', 'Search audio files…')}" autocomplete="off">
|
placeholder="${i18n.t('music.search_audio')}" autocomplete="off">
|
||||||
</div>
|
</div>
|
||||||
<div class="music-picker-list" id="music-picker-list">
|
<div class="music-picker-list" id="music-picker-list">
|
||||||
<div class="music-picker-loading"><i class="fas fa-spinner fa-spin"></i> ${t('music.loading', 'Loading…')}</div>
|
<div class="music-picker-loading"><i class="fas fa-spinner fa-spin"></i> ${i18n.t('music.loading')}</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="music-picker-footer">
|
<div class="music-picker-footer">
|
||||||
<span class="music-picker-selected-count" id="music-picker-count">0 ${t('music.selected', 'selected')}</span>
|
<span class="music-picker-selected-count" id="music-picker-count">0 ${i18n.t('music.selected')}</span>
|
||||||
<div class="music-picker-actions">
|
<div class="music-picker-actions">
|
||||||
<button class="btn btn-secondary music-picker-cancel">${t('common.cancel', 'Cancel')}</button>
|
<button class="btn btn-secondary music-picker-cancel">${i18n.t('actions.cancel')}</button>
|
||||||
<button class="btn btn-primary music-picker-add" id="music-picker-add-btn" disabled>
|
<button class="btn btn-primary music-picker-add" id="music-picker-add-btn" disabled>
|
||||||
<i class="fas fa-plus"></i> ${t('music.add', 'Add')}
|
<i class="fas fa-plus"></i> ${i18n.t('music.add')}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -792,7 +751,7 @@ const musicView = {
|
|||||||
const AUDIO_EXTENSIONS = 'mp3,ogg,flac,wav,aac,m4a,wma,opus,webm';
|
const AUDIO_EXTENSIONS = 'mp3,ogg,flac,wav,aac,m4a,wma,opus,webm';
|
||||||
|
|
||||||
const fetchAudioFiles = async (query = '') => {
|
const fetchAudioFiles = async (query = '') => {
|
||||||
listEl.innerHTML = `<div class="music-picker-loading"><i class="fas fa-spinner fa-spin"></i> ${t('music.loading', 'Loading…')}</div>`;
|
listEl.innerHTML = `<div class="music-picker-loading"><i class="fas fa-spinner fa-spin"></i> ${i18n.t('music.loading')}</div>`;
|
||||||
try {
|
try {
|
||||||
const params = new URLSearchParams({ type_filter: AUDIO_EXTENSIONS, limit: '200', recursive: 'true' });
|
const params = new URLSearchParams({ type_filter: AUDIO_EXTENSIONS, limit: '200', recursive: 'true' });
|
||||||
if (query.trim()) params.set('query', query.trim());
|
if (query.trim()) params.set('query', query.trim());
|
||||||
@@ -802,13 +761,13 @@ const musicView = {
|
|||||||
renderFiles(data.files || []);
|
renderFiles(data.files || []);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error('Audio search error:', err);
|
console.error('Audio search error:', err);
|
||||||
listEl.innerHTML = `<div class="music-picker-empty"><i class="fas fa-exclamation-triangle"></i> ${t('music.search_error', 'Could not load audio files')}</div>`;
|
listEl.innerHTML = `<div class="music-picker-empty"><i class="fas fa-exclamation-triangle"></i> ${i18n.t('music.search_error')}</div>`;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const renderFiles = (files) => {
|
const renderFiles = (files) => {
|
||||||
if (files.length === 0) {
|
if (files.length === 0) {
|
||||||
listEl.innerHTML = `<div class="music-picker-empty"><i class="fas fa-folder-open"></i> ${t('music.no_audio_files', 'No audio files found')}</div>`;
|
listEl.innerHTML = `<div class="music-picker-empty"><i class="fas fa-folder-open"></i> ${i18n.t('music.no_audio_files')}</div>`;
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
listEl.innerHTML = '';
|
listEl.innerHTML = '';
|
||||||
@@ -831,7 +790,7 @@ const musicView = {
|
|||||||
selectedIds.delete(file.id);
|
selectedIds.delete(file.id);
|
||||||
row.classList.remove('selected');
|
row.classList.remove('selected');
|
||||||
}
|
}
|
||||||
countEl.textContent = `${selectedIds.size} ${t('music.selected', 'selected')}`;
|
countEl.textContent = `${selectedIds.size} ${i18n.t('music.selected')}`;
|
||||||
addBtn.disabled = selectedIds.size === 0;
|
addBtn.disabled = selectedIds.size === 0;
|
||||||
});
|
});
|
||||||
listEl.appendChild(row);
|
listEl.appendChild(row);
|
||||||
@@ -849,7 +808,7 @@ const musicView = {
|
|||||||
addBtn.addEventListener('click', async () => {
|
addBtn.addEventListener('click', async () => {
|
||||||
if (selectedIds.size === 0) return;
|
if (selectedIds.size === 0) return;
|
||||||
addBtn.disabled = true;
|
addBtn.disabled = true;
|
||||||
addBtn.innerHTML = `<i class="fas fa-spinner fa-spin"></i> ${t('music.adding', 'Adding…')}`;
|
addBtn.innerHTML = `<i class="fas fa-spinner fa-spin"></i> ${i18n.t('music.adding')}`;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const resp = await fetch(`/api/playlists/${this.currentPlaylist.id}/tracks`, {
|
const resp = await fetch(`/api/playlists/${this.currentPlaylist.id}/tracks`, {
|
||||||
@@ -864,8 +823,8 @@ const musicView = {
|
|||||||
notifications.addNotification({
|
notifications.addNotification({
|
||||||
icon: 'fa-check-circle',
|
icon: 'fa-check-circle',
|
||||||
iconClass: 'upload',
|
iconClass: 'upload',
|
||||||
title: t('music.add_tracks', 'Add Tracks'),
|
title: i18n.t('music.add_tracks'),
|
||||||
text: `${selectedIds.size} ${t('music.added_to_playlist', 'added to playlist')}`
|
text: `${selectedIds.size} ${i18n.t('music.added_to_playlist')}`
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
close();
|
close();
|
||||||
@@ -876,7 +835,7 @@ const musicView = {
|
|||||||
this.currentPlaylist.track_count = playlist.track_count;
|
this.currentPlaylist.track_count = playlist.track_count;
|
||||||
this._renderPlaylistList();
|
this._renderPlaylistList();
|
||||||
const metaEl = document.getElementById('music-playlist-meta');
|
const metaEl = document.getElementById('music-playlist-meta');
|
||||||
if (metaEl) metaEl.textContent = `${playlist.track_count} ${t('music.tracks', 'tracks')}`;
|
if (metaEl) metaEl.textContent = `${playlist.track_count} ${i18n.t('music.tracks')}`;
|
||||||
}
|
}
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error('Add tracks error:', err);
|
console.error('Add tracks error:', err);
|
||||||
@@ -884,12 +843,12 @@ const musicView = {
|
|||||||
notifications.addNotification({
|
notifications.addNotification({
|
||||||
icon: 'fa-exclamation-circle',
|
icon: 'fa-exclamation-circle',
|
||||||
iconClass: 'error',
|
iconClass: 'error',
|
||||||
title: t('music.error', 'Error'),
|
title: i18n.t('music.error'),
|
||||||
text: t('music.add_error', 'Could not add tracks to playlist')
|
text: i18n.t('music.add_error')
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
addBtn.disabled = false;
|
addBtn.disabled = false;
|
||||||
addBtn.innerHTML = `<i class="fas fa-plus"></i> ${t('music.add', 'Add')}`;
|
addBtn.innerHTML = `<i class="fas fa-plus"></i> ${i18n.t('music.add')}`;
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -900,7 +859,6 @@ const musicView = {
|
|||||||
|
|
||||||
async _removeTrackFromPlaylist(_trackId, fileId) {
|
async _removeTrackFromPlaylist(_trackId, fileId) {
|
||||||
if (!this.currentPlaylist) return;
|
if (!this.currentPlaylist) return;
|
||||||
const t = (key, fallback = '') => (i18n?.t ? i18n.t(key) : fallback || key);
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const resp = await fetch(`/api/playlists/${this.currentPlaylist.id}/tracks/${encodeURIComponent(fileId)}`, {
|
const resp = await fetch(`/api/playlists/${this.currentPlaylist.id}/tracks/${encodeURIComponent(fileId)}`, {
|
||||||
@@ -914,8 +872,8 @@ const musicView = {
|
|||||||
notifications.addNotification({
|
notifications.addNotification({
|
||||||
icon: 'fa-check-circle',
|
icon: 'fa-check-circle',
|
||||||
iconClass: 'upload',
|
iconClass: 'upload',
|
||||||
title: t('music.remove', 'Remove'),
|
title: i18n.t('music.remove'),
|
||||||
text: t('music.track_removed', 'Track removed')
|
text: i18n.t('music.track_removed')
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
await this._loadPlaylistTracks(this.currentPlaylist.id);
|
await this._loadPlaylistTracks(this.currentPlaylist.id);
|
||||||
@@ -925,7 +883,7 @@ const musicView = {
|
|||||||
this.currentPlaylist.track_count = playlist.track_count;
|
this.currentPlaylist.track_count = playlist.track_count;
|
||||||
this._renderPlaylistList();
|
this._renderPlaylistList();
|
||||||
const metaEl = document.getElementById('music-playlist-meta');
|
const metaEl = document.getElementById('music-playlist-meta');
|
||||||
if (metaEl) metaEl.textContent = `${playlist.track_count} ${t('music.tracks', 'tracks')}`;
|
if (metaEl) metaEl.textContent = `${playlist.track_count} ${i18n.t('music.tracks')}`;
|
||||||
}
|
}
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error('Remove track error:', err);
|
console.error('Remove track error:', err);
|
||||||
@@ -933,7 +891,7 @@ const musicView = {
|
|||||||
notifications.addNotification({
|
notifications.addNotification({
|
||||||
icon: 'fa-exclamation-circle',
|
icon: 'fa-exclamation-circle',
|
||||||
iconClass: 'error',
|
iconClass: 'error',
|
||||||
title: t('music.error', 'Error'),
|
title: i18n.t('music.error'),
|
||||||
text: err.message
|
text: err.message
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -942,7 +900,6 @@ const musicView = {
|
|||||||
|
|
||||||
async _reorderTrack(fromIdx, toIdx) {
|
async _reorderTrack(fromIdx, toIdx) {
|
||||||
if (!this.currentPlaylist) return;
|
if (!this.currentPlaylist) return;
|
||||||
const t = (key, fallback = '') => (i18n?.t ? i18n.t(key) : fallback || key);
|
|
||||||
|
|
||||||
const tracks = [...this.currentTracks];
|
const tracks = [...this.currentTracks];
|
||||||
const [moved] = tracks.splice(fromIdx, 1);
|
const [moved] = tracks.splice(fromIdx, 1);
|
||||||
@@ -965,7 +922,7 @@ const musicView = {
|
|||||||
notifications.addNotification({
|
notifications.addNotification({
|
||||||
icon: 'fa-exclamation-circle',
|
icon: 'fa-exclamation-circle',
|
||||||
iconClass: 'error',
|
iconClass: 'error',
|
||||||
title: t('music.error', 'Error'),
|
title: i18n.t('music.error'),
|
||||||
text: err.message
|
text: err.message
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -975,7 +932,6 @@ const musicView = {
|
|||||||
|
|
||||||
async _showManageSharesDialog() {
|
async _showManageSharesDialog() {
|
||||||
if (!this.currentPlaylist) return;
|
if (!this.currentPlaylist) return;
|
||||||
const t = (key, fallback = '') => (i18n?.t ? i18n.t(key) : fallback || key);
|
|
||||||
|
|
||||||
const existing = document.getElementById('music-shares-dialog');
|
const existing = document.getElementById('music-shares-dialog');
|
||||||
if (existing) existing.remove();
|
if (existing) existing.remove();
|
||||||
@@ -986,19 +942,19 @@ const musicView = {
|
|||||||
dialog.innerHTML = `
|
dialog.innerHTML = `
|
||||||
<div class="music-shares-panel">
|
<div class="music-shares-panel">
|
||||||
<div class="music-shares-header">
|
<div class="music-shares-header">
|
||||||
<h3><i class="fas fa-users"></i> ${t('music.manage_shares', 'Manage Shares')}</h3>
|
<h3><i class="fas fa-users"></i> ${i18n.t('music.manage_shares')}</h3>
|
||||||
<button class="music-shares-close-btn"><i class="fas fa-times"></i></button>
|
<button class="music-shares-close-btn"><i class="fas fa-times"></i></button>
|
||||||
</div>
|
</div>
|
||||||
<div class="music-shares-body">
|
<div class="music-shares-body">
|
||||||
<div class="music-shares-loading"><i class="fas fa-spinner fa-spin"></i></div>
|
<div class="music-shares-loading"><i class="fas fa-spinner fa-spin"></i></div>
|
||||||
</div>
|
</div>
|
||||||
<div class="music-shares-add">
|
<div class="music-shares-add">
|
||||||
<input type="text" id="music-share-user-input" placeholder="${t('music.share_with_user', 'User ID or email')}" class="music-shares-input">
|
<input type="text" id="music-share-user-input" placeholder="${i18n.t('music.share_with_user')}" class="music-shares-input">
|
||||||
<label class="music-shares-write-label">
|
<label class="music-shares-write-label">
|
||||||
<input type="checkbox" id="music-share-write-input"> ${t('music.can_write', 'Can edit')}
|
<input type="checkbox" id="music-share-write-input"> ${i18n.t('music.can_write')}
|
||||||
</label>
|
</label>
|
||||||
<button class="btn btn-primary btn-sm" id="music-share-add-btn">
|
<button class="btn btn-primary btn-sm" id="music-share-add-btn">
|
||||||
<i class="fas fa-plus"></i> ${t('music.share', 'Share')}
|
<i class="fas fa-plus"></i> ${i18n.t('music.share')}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -1031,8 +987,8 @@ const musicView = {
|
|||||||
notifications.addNotification({
|
notifications.addNotification({
|
||||||
icon: 'fa-check-circle',
|
icon: 'fa-check-circle',
|
||||||
iconClass: 'upload',
|
iconClass: 'upload',
|
||||||
title: t('music.share', 'Share'),
|
title: i18n.t('music.share'),
|
||||||
text: t('music.added', 'Added!')
|
text: i18n.t('music.added')
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
@@ -1040,7 +996,7 @@ const musicView = {
|
|||||||
notifications.addNotification({
|
notifications.addNotification({
|
||||||
icon: 'fa-exclamation-circle',
|
icon: 'fa-exclamation-circle',
|
||||||
iconClass: 'error',
|
iconClass: 'error',
|
||||||
title: t('music.error', 'Error'),
|
title: i18n.t('music.error'),
|
||||||
text: err.message
|
text: err.message
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -1052,7 +1008,6 @@ const musicView = {
|
|||||||
|
|
||||||
async _loadSharesList(dialog) {
|
async _loadSharesList(dialog) {
|
||||||
if (!this.currentPlaylist) return;
|
if (!this.currentPlaylist) return;
|
||||||
const t = (key, fallback = '') => (i18n?.t ? i18n.t(key) : fallback || key);
|
|
||||||
const body = dialog.querySelector('.music-shares-body');
|
const body = dialog.querySelector('.music-shares-body');
|
||||||
if (!body) return;
|
if (!body) return;
|
||||||
|
|
||||||
@@ -1067,7 +1022,7 @@ const musicView = {
|
|||||||
const shares = await resp.json();
|
const shares = await resp.json();
|
||||||
|
|
||||||
if (shares.length === 0) {
|
if (shares.length === 0) {
|
||||||
body.innerHTML = `<p class="music-shares-empty">${t('music.no_shares', 'No shares yet')}</p>`;
|
body.innerHTML = `<p class="music-shares-empty">${i18n.t('music.no_shares')}</p>`;
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1076,8 +1031,8 @@ const musicView = {
|
|||||||
(s) => `
|
(s) => `
|
||||||
<div class="music-share-item" data-user-id="${this._escapeHtml(s.user_id)}">
|
<div class="music-share-item" data-user-id="${this._escapeHtml(s.user_id)}">
|
||||||
<span class="music-share-user"><i class="fas fa-user"></i> ${this._escapeHtml(s.user_id)}</span>
|
<span class="music-share-user"><i class="fas fa-user"></i> ${this._escapeHtml(s.user_id)}</span>
|
||||||
<span class="music-share-perm">${s.can_write ? t('music.can_write', 'Can edit') : t('music.read_only', 'Read only')}</span>
|
<span class="music-share-perm">${s.can_write ? i18n.t('music.can_write') : i18n.t('music.read_only')}</span>
|
||||||
<button class="music-share-remove-btn" title="${t('music.remove_share', 'Remove share')}"><i class="fas fa-times"></i></button>
|
<button class="music-share-remove-btn" title="${i18n.t('music.remove_share')}"><i class="fas fa-times"></i></button>
|
||||||
</div>
|
</div>
|
||||||
`
|
`
|
||||||
)
|
)
|
||||||
@@ -1097,7 +1052,6 @@ const musicView = {
|
|||||||
|
|
||||||
async _removeShare(userId, dialog) {
|
async _removeShare(userId, dialog) {
|
||||||
if (!this.currentPlaylist) return;
|
if (!this.currentPlaylist) return;
|
||||||
const t = (key, fallback = '') => (i18n?.t ? i18n.t(key) : fallback || key);
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const resp = await fetch(`/api/playlists/${this.currentPlaylist.id}/share/${encodeURIComponent(userId)}`, {
|
const resp = await fetch(`/api/playlists/${this.currentPlaylist.id}/share/${encodeURIComponent(userId)}`, {
|
||||||
@@ -1112,7 +1066,7 @@ const musicView = {
|
|||||||
notifications.addNotification({
|
notifications.addNotification({
|
||||||
icon: 'fa-exclamation-circle',
|
icon: 'fa-exclamation-circle',
|
||||||
iconClass: 'error',
|
iconClass: 'error',
|
||||||
title: t('music.error', 'Error'),
|
title: i18n.t('music.error'),
|
||||||
text: err.message
|
text: err.message
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -1121,7 +1075,6 @@ const musicView = {
|
|||||||
|
|
||||||
async _togglePublic() {
|
async _togglePublic() {
|
||||||
if (!this.currentPlaylist) return;
|
if (!this.currentPlaylist) return;
|
||||||
const t = (key, fallback = '') => (i18n?.t ? i18n.t(key) : fallback || key);
|
|
||||||
const newValue = !this.currentPlaylist.is_public;
|
const newValue = !this.currentPlaylist.is_public;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
@@ -1142,16 +1095,16 @@ const musicView = {
|
|||||||
|
|
||||||
const btn = document.getElementById('music-toggle-public-btn');
|
const btn = document.getElementById('music-toggle-public-btn');
|
||||||
if (btn) {
|
if (btn) {
|
||||||
btn.title = newValue ? t('music.make_private', 'Make private') : t('music.make_public', 'Make public');
|
btn.title = newValue ? i18n.t('music.make_private') : i18n.t('music.make_public');
|
||||||
btn.classList.toggle('active', newValue);
|
btn.classList.toggle('active', newValue);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (notifications) {
|
if (notifications) {
|
||||||
const status = newValue ? t('music.public', 'Public') : t('music.private', 'Private');
|
const status = newValue ? i18n.t('music.public') : i18n.t('music.private');
|
||||||
notifications.addNotification({
|
notifications.addNotification({
|
||||||
icon: 'fa-check-circle',
|
icon: 'fa-check-circle',
|
||||||
iconClass: 'upload',
|
iconClass: 'upload',
|
||||||
title: t('music.toggle_public', 'Visibility'),
|
title: i18n.t('music.toggle_public'),
|
||||||
text: status
|
text: status
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -1161,7 +1114,7 @@ const musicView = {
|
|||||||
notifications.addNotification({
|
notifications.addNotification({
|
||||||
icon: 'fa-exclamation-circle',
|
icon: 'fa-exclamation-circle',
|
||||||
iconClass: 'error',
|
iconClass: 'error',
|
||||||
title: t('music.error', 'Error'),
|
title: i18n.t('music.error'),
|
||||||
text: err.message
|
text: err.message
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -1170,7 +1123,6 @@ const musicView = {
|
|||||||
|
|
||||||
async _showCoverPicker() {
|
async _showCoverPicker() {
|
||||||
if (!this.currentPlaylist) return;
|
if (!this.currentPlaylist) return;
|
||||||
const t = (key, fallback = '') => (i18n?.t ? i18n.t(key) : fallback || key);
|
|
||||||
|
|
||||||
const input = document.createElement('input');
|
const input = document.createElement('input');
|
||||||
input.type = 'file';
|
input.type = 'file';
|
||||||
@@ -1220,8 +1172,8 @@ const musicView = {
|
|||||||
notifications.addNotification({
|
notifications.addNotification({
|
||||||
icon: 'fa-check-circle',
|
icon: 'fa-check-circle',
|
||||||
iconClass: 'upload',
|
iconClass: 'upload',
|
||||||
title: t('music.set_cover', 'Set cover'),
|
title: i18n.t('music.set_cover'),
|
||||||
text: t('music.cover_updated', 'Cover updated')
|
text: i18n.t('music.cover_updated')
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
@@ -1230,7 +1182,7 @@ const musicView = {
|
|||||||
notifications.addNotification({
|
notifications.addNotification({
|
||||||
icon: 'fa-exclamation-circle',
|
icon: 'fa-exclamation-circle',
|
||||||
iconClass: 'error',
|
iconClass: 'error',
|
||||||
title: t('music.error', 'Error'),
|
title: i18n.t('music.error'),
|
||||||
text: err.message
|
text: err.message
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -1289,25 +1241,25 @@ const musicPlayer = {
|
|||||||
<i class="fas fa-music"></i>
|
<i class="fas fa-music"></i>
|
||||||
</div>
|
</div>
|
||||||
<div class="player-track-details">
|
<div class="player-track-details">
|
||||||
<span class="player-track-name">${i18n?.t('music.not_playing', 'Not playing') || 'Not playing'}</span>
|
<span class="player-track-name">${i18n.t('music.not_playing')}</span>
|
||||||
<span class="player-track-artist"></span>
|
<span class="player-track-artist"></span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="player-controls">
|
<div class="player-controls">
|
||||||
<div class="player-buttons">
|
<div class="player-buttons">
|
||||||
<button class="player-btn" id="player-shuffle-btn" title="${i18n?.t('music.shuffle', 'Shuffle') || 'Shuffle'}">
|
<button class="player-btn" id="player-shuffle-btn" title="${i18n.t('music.shuffle')}">
|
||||||
<i class="fas fa-shuffle"></i>
|
<i class="fas fa-shuffle"></i>
|
||||||
</button>
|
</button>
|
||||||
<button class="player-btn" id="player-prev-btn" title="${i18n?.t('music.previous', 'Previous') || 'Previous'}">
|
<button class="player-btn" id="player-prev-btn" title="${i18n.t('music.previous')}">
|
||||||
<i class="fas fa-backward"></i>
|
<i class="fas fa-backward"></i>
|
||||||
</button>
|
</button>
|
||||||
<button class="player-btn player-btn-main" id="player-play-btn" title="${i18n?.t('music.play', 'Play') || 'Play'}">
|
<button class="player-btn player-btn-main" id="player-play-btn" title="${i18n.t('music.play')}">
|
||||||
<i class="fas fa-play"></i>
|
<i class="fas fa-play"></i>
|
||||||
</button>
|
</button>
|
||||||
<button class="player-btn" id="player-next-btn" title="${i18n?.t('music.next', 'Next') || 'Next'}">
|
<button class="player-btn" id="player-next-btn" title="${i18n.t('music.next')}">
|
||||||
<i class="fas fa-forward"></i>
|
<i class="fas fa-forward"></i>
|
||||||
</button>
|
</button>
|
||||||
<button class="player-btn" id="player-repeat-btn" title="${i18n?.t('music.repeat', 'Repeat') || 'Repeat'}">
|
<button class="player-btn" id="player-repeat-btn" title="${i18n.t('music.repeat')}">
|
||||||
<i class="fas fa-repeat"></i>
|
<i class="fas fa-repeat"></i>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
@@ -1321,22 +1273,22 @@ const musicPlayer = {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="player-extra">
|
<div class="player-extra">
|
||||||
<button class="player-btn player-btn-small" id="player-playlist-btn" title="${i18n?.t('music.queue', 'Queue') || 'Queue'}">
|
<button class="player-btn player-btn-small" id="player-playlist-btn" title="${i18n.t('music.queue')}">
|
||||||
<i class="fas fa-list"></i>
|
<i class="fas fa-list"></i>
|
||||||
</button>
|
</button>
|
||||||
<button class="player-btn player-btn-small" id="player-vol-btn" title="${i18n?.t('music.volume', 'Volume') || 'Volume'}">
|
<button class="player-btn player-btn-small" id="player-vol-btn" title="${i18n.t('music.volume')}">
|
||||||
<i class="fas fa-volume-up"></i>
|
<i class="fas fa-volume-up"></i>
|
||||||
</button>
|
</button>
|
||||||
<div class="player-volume-slider" id="player-volume-slider">
|
<div class="player-volume-slider" id="player-volume-slider">
|
||||||
<input type="range" min="0" max="100" value="70" id="player-volume-input">
|
<input type="range" min="0" max="100" value="70" id="player-volume-input">
|
||||||
</div>
|
</div>
|
||||||
<button class="player-btn player-btn-small player-close-btn" id="player-close-btn" title="${i18n?.t('actions.close', 'Close') || 'Close'}">
|
<button class="player-btn player-btn-small player-close-btn" id="player-close-btn" title="${i18n.t('actions.close')}">
|
||||||
<i class="fas fa-times"></i>
|
<i class="fas fa-times"></i>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
<div class="player-queue hidden" id="player-queue">
|
<div class="player-queue hidden" id="player-queue">
|
||||||
<div class="player-queue-header">
|
<div class="player-queue-header">
|
||||||
<h3>${i18n?.t('music.queue', 'Queue') || 'Queue'}</h3>
|
<h3>${i18n.t('music.queue')}</h3>
|
||||||
<button class="player-btn player-btn-small" id="player-close-queue-btn">
|
<button class="player-btn player-btn-small" id="player-close-queue-btn">
|
||||||
<i class="fas fa-times"></i>
|
<i class="fas fa-times"></i>
|
||||||
</button>
|
</button>
|
||||||
@@ -1648,16 +1600,13 @@ const musicPlayer = {
|
|||||||
console.error('Audio error:', e);
|
console.error('Audio error:', e);
|
||||||
this.isPlaying = false;
|
this.isPlaying = false;
|
||||||
this._updateUI();
|
this._updateUI();
|
||||||
const t = (key, fallback = '') => {
|
|
||||||
return i18n?.t ? i18n.t(key) : fallback || key;
|
|
||||||
};
|
|
||||||
if (notifications) {
|
if (notifications) {
|
||||||
const trackName = this.currentTrack?.title || this.currentTrack?.file_name || t('music.unknown_title', 'Unknown');
|
const trackName = this.currentTrack?.title || this.currentTrack?.file_name || i18n.t('music.unknown_title');
|
||||||
notifications.addNotification({
|
notifications.addNotification({
|
||||||
icon: 'fa-exclamation-circle',
|
icon: 'fa-exclamation-circle',
|
||||||
iconClass: 'error',
|
iconClass: 'error',
|
||||||
title: t('music.error', 'Error'),
|
title: i18n.t('music.error'),
|
||||||
text: `${t('music.playback_error', 'Playback failed')}: ${trackName}`
|
text: `${i18n.t('music.playback_error')}: ${trackName}`
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -1681,12 +1630,9 @@ const musicPlayer = {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (trackName) {
|
if (trackName) {
|
||||||
const t = (key, fallback = '') => {
|
|
||||||
return i18n?.t ? i18n.t(key) : fallback || key;
|
|
||||||
};
|
|
||||||
trackName.textContent = this.currentTrack
|
trackName.textContent = this.currentTrack
|
||||||
? this.currentTrack.title || this.currentTrack.file_name || t('music.unknown_title', 'Unknown')
|
? this.currentTrack.title || this.currentTrack.file_name || i18n.t('music.unknown_title')
|
||||||
: t('music.not_playing', 'Not playing');
|
: i18n.t('music.not_playing');
|
||||||
}
|
}
|
||||||
|
|
||||||
if (trackArtist) {
|
if (trackArtist) {
|
||||||
@@ -1736,15 +1682,11 @@ const musicPlayer = {
|
|||||||
const queueList = document.getElementById('player-queue-list');
|
const queueList = document.getElementById('player-queue-list');
|
||||||
if (!queueList) return;
|
if (!queueList) return;
|
||||||
|
|
||||||
const t = (key, fallback = '') => {
|
|
||||||
return i18n?.t ? i18n.t(key) : fallback || key;
|
|
||||||
};
|
|
||||||
|
|
||||||
if (this.queue.length === 0) {
|
if (this.queue.length === 0) {
|
||||||
queueList.innerHTML = `
|
queueList.innerHTML = `
|
||||||
<div class="player-queue-empty">
|
<div class="player-queue-empty">
|
||||||
<i class="fas fa-music"></i>
|
<i class="fas fa-music"></i>
|
||||||
<p>${t('music.queue_empty', 'Queue is empty')}</p>
|
<p>${i18n.t('music.queue_empty')}</p>
|
||||||
</div>
|
</div>
|
||||||
`;
|
`;
|
||||||
return;
|
return;
|
||||||
@@ -1756,8 +1698,8 @@ const musicPlayer = {
|
|||||||
<div class="player-queue-item ${idx === this.currentIndex ? 'active' : ''}" data-idx="${idx}">
|
<div class="player-queue-item ${idx === this.currentIndex ? 'active' : ''}" data-idx="${idx}">
|
||||||
<span class="queue-item-num">${idx + 1}</span>
|
<span class="queue-item-num">${idx + 1}</span>
|
||||||
<span class="queue-item-info">
|
<span class="queue-item-info">
|
||||||
<span class="queue-item-name">${this._escapeHtml(track.title || track.file_name || t('music.unknown_title', 'Unknown'))}</span>
|
<span class="queue-item-name">${this._escapeHtml(track.title || track.file_name || i18n.t('music.unknown_title'))}</span>
|
||||||
<span class="queue-item-artist">${this._escapeHtml(track.artist || t('music.unknown_artist', 'Unknown Artist'))}</span>
|
<span class="queue-item-artist">${this._escapeHtml(track.artist || i18n.t('music.unknown_artist'))}</span>
|
||||||
</span>
|
</span>
|
||||||
<span class="queue-item-duration">${this._formatDuration(track.duration_secs)}</span>
|
<span class="queue-item-duration">${this._formatDuration(track.duration_secs)}</span>
|
||||||
<button class="queue-item-remove" data-idx="${idx}">
|
<button class="queue-item-remove" data-idx="${idx}">
|
||||||
|
|||||||
@@ -410,11 +410,10 @@ const photosView = {
|
|||||||
|
|
||||||
/** Render the group mode toolbar */
|
/** Render the group mode toolbar */
|
||||||
_renderToolbar() {
|
_renderToolbar() {
|
||||||
const t = (k, d) => (i18n ? i18n.t(k) : d);
|
|
||||||
const modes = [
|
const modes = [
|
||||||
['daily', t('photos.view_daily', 'Day')],
|
['daily', i18n.t('photos.view_daily')],
|
||||||
['monthly', t('photos.view_monthly', 'Month')],
|
['monthly', i18n.t('photos.view_monthly')],
|
||||||
['yearly', t('photos.view_yearly', 'Year')]
|
['yearly', i18n.t('photos.view_yearly')]
|
||||||
];
|
];
|
||||||
let html = '<div class="photos-toolbar"><div class="view-toggle">';
|
let html = '<div class="photos-toolbar"><div class="view-toggle">';
|
||||||
for (const [mode, label] of modes) {
|
for (const [mode, label] of modes) {
|
||||||
@@ -427,12 +426,11 @@ const photosView = {
|
|||||||
|
|
||||||
/** Render empty state */
|
/** Render empty state */
|
||||||
_renderEmpty() {
|
_renderEmpty() {
|
||||||
const t = (k, d) => (i18n ? i18n.t(k) : d);
|
|
||||||
this._container.innerHTML = `
|
this._container.innerHTML = `
|
||||||
<div class="photos-empty">
|
<div class="photos-empty">
|
||||||
<i class="fas fa-images"></i>
|
<i class="fas fa-images"></i>
|
||||||
<p class="photos-empty-title">${t('photos.empty_state', 'No photos yet')}</p>
|
<p class="photos-empty-title">${i18n.t('photos.empty_state')}</p>
|
||||||
<p>${t('photos.empty_hint', 'Upload images or videos to see them here')}</p>
|
<p>${i18n.t('photos.empty_hint')}</p>
|
||||||
</div>`;
|
</div>`;
|
||||||
},
|
},
|
||||||
|
|
||||||
@@ -520,10 +518,9 @@ const photosView = {
|
|||||||
document.body.appendChild(bar);
|
document.body.appendChild(bar);
|
||||||
}
|
}
|
||||||
|
|
||||||
const t = (k, d) => (i18n ? i18n.t(k) : d);
|
|
||||||
const count = this.selected.size;
|
const count = this.selected.size;
|
||||||
bar.innerHTML = `
|
bar.innerHTML = `
|
||||||
<span class="selection-count">${count} ${t('photos.items_selected', 'selected')}</span>
|
<span class="selection-count">${count} ${i18n.t('photos.items_selected')}</span>
|
||||||
<button id="photos-sel-download" title="Download"><i class="fas fa-download"></i></button>
|
<button id="photos-sel-download" title="Download"><i class="fas fa-download"></i></button>
|
||||||
<button id="photos-sel-delete" title="Delete"><i class="fas fa-trash"></i></button>
|
<button id="photos-sel-delete" title="Delete"><i class="fas fa-trash"></i></button>
|
||||||
<button id="photos-sel-clear" title="Clear"><i class="fas fa-times"></i></button>
|
<button id="photos-sel-clear" title="Clear"><i class="fas fa-times"></i></button>
|
||||||
|
|||||||
@@ -114,8 +114,8 @@ const recent = {
|
|||||||
if (recentItems.length === 0) {
|
if (recentItems.length === 0) {
|
||||||
ui.showError(`
|
ui.showError(`
|
||||||
<i class="fas fa-clock empty-state-icon"></i>
|
<i class="fas fa-clock empty-state-icon"></i>
|
||||||
<p>${i18n ? i18n.t('recent.empty_state') : 'No recent files'}</p>
|
<p>${i18n.t('recent.empty_state')}</p>
|
||||||
<p>${i18n ? i18n.t('recent.empty_hint') : 'Files you open will appear here'}</p>
|
<p>${i18n.t('recent.empty_hint')}</p>
|
||||||
`);
|
`);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -8,13 +8,6 @@ let usersPage = 0;
|
|||||||
const PAGE_SIZE = 50;
|
const PAGE_SIZE = 50;
|
||||||
let totalUsers = 0;
|
let totalUsers = 0;
|
||||||
|
|
||||||
/* ── i18n helper — falls back to key if i18n not ready ── */
|
|
||||||
function t(key, params) {
|
|
||||||
if (i18n && typeof i18n.t === 'function') return i18n.t(key, params);
|
|
||||||
// fallback: strip prefix and humanise
|
|
||||||
return key.split('.').pop().replace(/_/g, ' ');
|
|
||||||
}
|
|
||||||
|
|
||||||
/** Escape a string for safe embedding inside a JS string literal within an HTML attribute. */
|
/** Escape a string for safe embedding inside a JS string literal within an HTML attribute. */
|
||||||
function _escJs(s) {
|
function _escJs(s) {
|
||||||
if (typeof s !== 'string') return '';
|
if (typeof s !== 'string') return '';
|
||||||
@@ -54,14 +47,14 @@ function formatBytes(bytes) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function timeAgo(dateStr) {
|
function timeAgo(dateStr) {
|
||||||
if (!dateStr) return t('admin.never');
|
if (!dateStr) return i18n.t('admin.never');
|
||||||
const d = new Date(dateStr);
|
const d = new Date(dateStr);
|
||||||
const now = new Date();
|
const now = new Date();
|
||||||
const secs = Math.floor((now - d) / 1000);
|
const secs = Math.floor((now - d) / 1000);
|
||||||
if (secs < 60) return t('admin.just_now');
|
if (secs < 60) return i18n.t('admin.just_now');
|
||||||
if (secs < 3600) return t('admin.minutes_ago', { n: Math.floor(secs / 60) });
|
if (secs < 3600) return i18n.t('admin.minutes_ago', { n: Math.floor(secs / 60) });
|
||||||
if (secs < 86400) return t('admin.hours_ago', { n: Math.floor(secs / 3600) });
|
if (secs < 86400) return i18n.t('admin.hours_ago', { n: Math.floor(secs / 3600) });
|
||||||
if (secs < 2592000) return t('admin.days_ago', { n: Math.floor(secs / 86400) });
|
if (secs < 2592000) return i18n.t('admin.days_ago', { n: Math.floor(secs / 86400) });
|
||||||
return d.toLocaleDateString();
|
return d.toLocaleDateString();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -159,9 +152,9 @@ async function loadDashboard() {
|
|||||||
const bar = document.getElementById('ds-bar');
|
const bar = document.getElementById('ds-bar');
|
||||||
bar.style.width = `${Math.min(d.storage_usage_percent, 100)}%`;
|
bar.style.width = `${Math.min(d.storage_usage_percent, 100)}%`;
|
||||||
bar.className = `progress-fill ${d.storage_usage_percent > 90 ? 'red' : d.storage_usage_percent > 70 ? 'orange' : 'green'}`;
|
bar.className = `progress-fill ${d.storage_usage_percent > 90 ? 'red' : d.storage_usage_percent > 70 ? 'orange' : 'green'}`;
|
||||||
document.getElementById('ds-auth').textContent = d.auth_enabled ? t('admin.enabled') : t('admin.disabled');
|
document.getElementById('ds-auth').textContent = d.auth_enabled ? i18n.t('admin.enabled') : i18n.t('admin.disabled');
|
||||||
document.getElementById('ds-oidc').textContent = d.oidc_configured ? t('admin.active') : t('admin.off');
|
document.getElementById('ds-oidc').textContent = d.oidc_configured ? i18n.t('admin.active') : i18n.t('admin.off');
|
||||||
document.getElementById('ds-quotas-flag').textContent = d.quotas_enabled ? t('admin.enabled') : t('admin.disabled');
|
document.getElementById('ds-quotas-flag').textContent = d.quotas_enabled ? i18n.t('admin.enabled') : i18n.t('admin.disabled');
|
||||||
|
|
||||||
if (typeof d.registration_enabled !== 'undefined') {
|
if (typeof d.registration_enabled !== 'undefined') {
|
||||||
document.getElementById('ds-registration').checked = d.registration_enabled;
|
document.getElementById('ds-registration').checked = d.registration_enabled;
|
||||||
@@ -184,7 +177,7 @@ async function loadDashboard() {
|
|||||||
|
|
||||||
async function loadUsers() {
|
async function loadUsers() {
|
||||||
const tbody = document.getElementById('users-tbody');
|
const tbody = document.getElementById('users-tbody');
|
||||||
tbody.innerHTML = `<tr><td colspan="7" class="table-loading-cell"><i class="fas fa-spinner fa-spin"></i> ${escapeHtml(t('admin.loading_users'))}</td></tr>`;
|
tbody.innerHTML = `<tr><td colspan="7" class="table-loading-cell"><i class="fas fa-spinner fa-spin"></i> ${escapeHtml(i18n.t('admin.loading_users'))}</td></tr>`;
|
||||||
try {
|
try {
|
||||||
const resp = await fetch(`${API}/admin/users?limit=${PAGE_SIZE}&offset=${usersPage * PAGE_SIZE}`, {
|
const resp = await fetch(`${API}/admin/users?limit=${PAGE_SIZE}&offset=${usersPage * PAGE_SIZE}`, {
|
||||||
headers: headers(),
|
headers: headers(),
|
||||||
@@ -193,7 +186,7 @@ async function loadUsers() {
|
|||||||
if (!resp.ok) {
|
if (!resp.ok) {
|
||||||
tbody.innerHTML =
|
tbody.innerHTML =
|
||||||
'<tr><td colspan="7" class="table-status-error"><i class="fas fa-exclamation-circle"></i> ' +
|
'<tr><td colspan="7" class="table-status-error"><i class="fas fa-exclamation-circle"></i> ' +
|
||||||
escapeHtml(t('admin.failed_load_users')) +
|
escapeHtml(i18n.t('admin.failed_load_users')) +
|
||||||
'</td></tr>';
|
'</td></tr>';
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -201,7 +194,7 @@ async function loadUsers() {
|
|||||||
totalUsers = data.total;
|
totalUsers = data.total;
|
||||||
const users = data.users;
|
const users = data.users;
|
||||||
if (users.length === 0) {
|
if (users.length === 0) {
|
||||||
tbody.innerHTML = `<tr><td colspan="7" class="table-status-empty">${escapeHtml(t('admin.no_users_found'))}</td></tr>`;
|
tbody.innerHTML = `<tr><td colspan="7" class="table-status-empty">${escapeHtml(i18n.t('admin.no_users_found'))}</td></tr>`;
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -221,12 +214,12 @@ async function loadUsers() {
|
|||||||
'"><i class="fas fa-key badge-admin-icon-small"></i> ' +
|
'"><i class="fas fa-key badge-admin-icon-small"></i> ' +
|
||||||
escapeHtml(u.auth_provider) +
|
escapeHtml(u.auth_provider) +
|
||||||
'</span>'
|
'</span>'
|
||||||
: `<span class="badge badge-local">${escapeHtml(t('admin.local'))}</span>`;
|
: `<span class="badge badge-local">${escapeHtml(i18n.t('admin.local'))}</span>`;
|
||||||
return (
|
return (
|
||||||
'<tr>' +
|
'<tr>' +
|
||||||
'<td><div class="user-info"><span class="user-name">' +
|
'<td><div class="user-info"><span class="user-name">' +
|
||||||
escapeHtml(u.username) +
|
escapeHtml(u.username) +
|
||||||
(isSelf ? ` <span class="user-self-badge">${escapeHtml(t('admin.you_badge'))}</span>` : '') +
|
(isSelf ? ` <span class="user-self-badge">${escapeHtml(i18n.t('admin.you_badge'))}</span>` : '') +
|
||||||
'</span><span class="user-email">' +
|
'</span><span class="user-email">' +
|
||||||
escapeHtml(u.email) +
|
escapeHtml(u.email) +
|
||||||
'</span></div></td>' +
|
'</span></div></td>' +
|
||||||
@@ -242,7 +235,7 @@ async function loadUsers() {
|
|||||||
'<td><span class="badge badge-' +
|
'<td><span class="badge badge-' +
|
||||||
(u.active ? 'active' : 'inactive') +
|
(u.active ? 'active' : 'inactive') +
|
||||||
'">' +
|
'">' +
|
||||||
(u.active ? escapeHtml(t('admin.active')) : escapeHtml(t('admin.inactive'))) +
|
(u.active ? escapeHtml(i18n.t('admin.active')) : escapeHtml(i18n.t('admin.inactive'))) +
|
||||||
'</span></td>' +
|
'</span></td>' +
|
||||||
'<td><div class="quota-bar"><div class="progress-bar quota-progress-fixed"><div class="progress-fill ' +
|
'<td><div class="quota-bar"><div class="progress-bar quota-progress-fixed"><div class="progress-fill ' +
|
||||||
quotaColor +
|
quotaColor +
|
||||||
@@ -262,7 +255,7 @@ async function loadUsers() {
|
|||||||
'" data-quota="' +
|
'" data-quota="' +
|
||||||
u.storage_quota_bytes +
|
u.storage_quota_bytes +
|
||||||
'" title="' +
|
'" title="' +
|
||||||
escapeHtml(t('admin.edit_quota_title')) +
|
escapeHtml(i18n.t('admin.edit_quota_title')) +
|
||||||
'"><i class="fas fa-box"></i></button>' +
|
'"><i class="fas fa-box"></i></button>' +
|
||||||
(isOidc
|
(isOidc
|
||||||
? ''
|
? ''
|
||||||
@@ -271,14 +264,14 @@ async function loadUsers() {
|
|||||||
'" data-uname="' +
|
'" data-uname="' +
|
||||||
_escJs(u.username) +
|
_escJs(u.username) +
|
||||||
'" title="' +
|
'" title="' +
|
||||||
escapeHtml(t('admin.reset_password_title')) +
|
escapeHtml(i18n.t('admin.reset_password_title')) +
|
||||||
'"><i class="fas fa-key"></i></button>') +
|
'"><i class="fas fa-key"></i></button>') +
|
||||||
'<button class="btn btn-sm btn-secondary admin-action-btn" data-action="toggle-role" data-uid="' +
|
'<button class="btn btn-sm btn-secondary admin-action-btn" data-action="toggle-role" data-uid="' +
|
||||||
_escJs(u.id) +
|
_escJs(u.id) +
|
||||||
'" data-role="' +
|
'" data-role="' +
|
||||||
_escJs(u.role) +
|
_escJs(u.role) +
|
||||||
'" title="' +
|
'" title="' +
|
||||||
escapeHtml(t('admin.toggle_role_title')) +
|
escapeHtml(i18n.t('admin.toggle_role_title')) +
|
||||||
'"' +
|
'"' +
|
||||||
(isSelf ? ' disabled' : '') +
|
(isSelf ? ' disabled' : '') +
|
||||||
'><i class="fas fa-' +
|
'><i class="fas fa-' +
|
||||||
@@ -291,7 +284,7 @@ async function loadUsers() {
|
|||||||
'" data-active="' +
|
'" data-active="' +
|
||||||
u.active +
|
u.active +
|
||||||
'" title="' +
|
'" title="' +
|
||||||
(u.active ? escapeHtml(t('admin.deactivate_title')) : escapeHtml(t('admin.activate_title'))) +
|
(u.active ? escapeHtml(i18n.t('admin.deactivate_title')) : escapeHtml(i18n.t('admin.activate_title'))) +
|
||||||
'"' +
|
'"' +
|
||||||
(isSelf && u.active ? ' disabled' : '') +
|
(isSelf && u.active ? ' disabled' : '') +
|
||||||
'><i class="fas fa-' +
|
'><i class="fas fa-' +
|
||||||
@@ -302,7 +295,7 @@ async function loadUsers() {
|
|||||||
'" data-uname="' +
|
'" data-uname="' +
|
||||||
_escJs(u.username) +
|
_escJs(u.username) +
|
||||||
'" title="' +
|
'" title="' +
|
||||||
escapeHtml(t('admin.delete_title')) +
|
escapeHtml(i18n.t('admin.delete_title')) +
|
||||||
'"' +
|
'"' +
|
||||||
(isSelf ? ' disabled' : '') +
|
(isSelf ? ' disabled' : '') +
|
||||||
'><i class="fas fa-trash-alt"></i></button>' +
|
'><i class="fas fa-trash-alt"></i></button>' +
|
||||||
@@ -331,13 +324,13 @@ async function loadUsers() {
|
|||||||
|
|
||||||
const from = usersPage * PAGE_SIZE + 1;
|
const from = usersPage * PAGE_SIZE + 1;
|
||||||
const to = Math.min((usersPage + 1) * PAGE_SIZE, totalUsers);
|
const to = Math.min((usersPage + 1) * PAGE_SIZE, totalUsers);
|
||||||
document.getElementById('users-info').textContent = t('admin.showing_users', { from: from, to: to, total: totalUsers });
|
document.getElementById('users-info').textContent = i18n.t('admin.showing_users', { from: from, to: to, total: totalUsers });
|
||||||
document.getElementById('prev-btn').disabled = usersPage === 0;
|
document.getElementById('prev-btn').disabled = usersPage === 0;
|
||||||
document.getElementById('next-btn').disabled = (usersPage + 1) * PAGE_SIZE >= totalUsers;
|
document.getElementById('next-btn').disabled = (usersPage + 1) * PAGE_SIZE >= totalUsers;
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
tbody.innerHTML =
|
tbody.innerHTML =
|
||||||
'<tr><td colspan="7" class="table-status-error"><i class="fas fa-exclamation-circle"></i> ' +
|
'<tr><td colspan="7" class="table-status-error"><i class="fas fa-exclamation-circle"></i> ' +
|
||||||
escapeHtml(t('admin.error_network', { message: e.message })) +
|
escapeHtml(i18n.t('admin.error_network', { message: e.message })) +
|
||||||
'</td></tr>';
|
'</td></tr>';
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -357,7 +350,7 @@ function nextPage() {
|
|||||||
|
|
||||||
async function toggleRole(userId, currentRole) {
|
async function toggleRole(userId, currentRole) {
|
||||||
const newRole = currentRole === 'admin' ? 'user' : 'admin';
|
const newRole = currentRole === 'admin' ? 'user' : 'admin';
|
||||||
const ok = await showConfirm(t('admin.confirm_role_change', { role: newRole }));
|
const ok = await showConfirm(i18n.t('admin.confirm_role_change', { role: newRole }));
|
||||||
if (!ok) return;
|
if (!ok) return;
|
||||||
try {
|
try {
|
||||||
const resp = await fetch(`${API}/admin/users/${userId}/role`, {
|
const resp = await fetch(`${API}/admin/users/${userId}/role`, {
|
||||||
@@ -369,15 +362,15 @@ async function toggleRole(userId, currentRole) {
|
|||||||
if (resp.ok) loadUsers();
|
if (resp.ok) loadUsers();
|
||||||
else {
|
else {
|
||||||
const e = await resp.json();
|
const e = await resp.json();
|
||||||
alert(e.message || t('admin.error_generic'));
|
alert(e.message || i18n.t('admin.error_generic'));
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
alert(t('admin.error_network', { message: e.message }));
|
alert(i18n.t('admin.error_network', { message: e.message }));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function toggleActive(userId, currentActive) {
|
async function toggleActive(userId, currentActive) {
|
||||||
const msg = currentActive ? t('admin.confirm_deactivate') : t('admin.confirm_activate');
|
const msg = currentActive ? i18n.t('admin.confirm_deactivate') : i18n.t('admin.confirm_activate');
|
||||||
const ok = await showConfirm(msg);
|
const ok = await showConfirm(msg);
|
||||||
if (!ok) return;
|
if (!ok) return;
|
||||||
try {
|
try {
|
||||||
@@ -390,15 +383,15 @@ async function toggleActive(userId, currentActive) {
|
|||||||
if (resp.ok) loadUsers();
|
if (resp.ok) loadUsers();
|
||||||
else {
|
else {
|
||||||
const e = await resp.json();
|
const e = await resp.json();
|
||||||
alert(e.message || t('admin.error_generic'));
|
alert(e.message || i18n.t('admin.error_generic'));
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
alert(t('admin.error_network', { message: e.message }));
|
alert(i18n.t('admin.error_network', { message: e.message }));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function deleteUser(userId, username) {
|
async function deleteUser(userId, username) {
|
||||||
const ok = await showConfirm(t('admin.confirm_delete_user', { name: username }));
|
const ok = await showConfirm(i18n.t('admin.confirm_delete_user', { name: username }));
|
||||||
if (!ok) return;
|
if (!ok) return;
|
||||||
try {
|
try {
|
||||||
const resp = await fetch(`${API}/admin/users/${userId}`, {
|
const resp = await fetch(`${API}/admin/users/${userId}`, {
|
||||||
@@ -411,10 +404,10 @@ async function deleteUser(userId, username) {
|
|||||||
loadDashboard();
|
loadDashboard();
|
||||||
} else {
|
} else {
|
||||||
const e = await resp.json();
|
const e = await resp.json();
|
||||||
alert(e.message || t('admin.error_generic'));
|
alert(e.message || i18n.t('admin.error_generic'));
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
alert(t('admin.error_network', { message: e.message }));
|
alert(i18n.t('admin.error_network', { message: e.message }));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -448,10 +441,10 @@ async function saveQuota() {
|
|||||||
loadDashboard();
|
loadDashboard();
|
||||||
} else {
|
} else {
|
||||||
const e = await resp.json();
|
const e = await resp.json();
|
||||||
alert(e.message || t('admin.error_generic'));
|
alert(e.message || i18n.t('admin.error_generic'));
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
alert(t('admin.error_network', { message: e.message }));
|
alert(i18n.t('admin.error_network', { message: e.message }));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -482,19 +475,19 @@ async function submitCreateUser() {
|
|||||||
|
|
||||||
const errorEl = document.getElementById('cu-error');
|
const errorEl = document.getElementById('cu-error');
|
||||||
if (username.length < 3) {
|
if (username.length < 3) {
|
||||||
errorEl.textContent = t('admin.error_username_short');
|
errorEl.textContent = i18n.t('admin.error_username_short');
|
||||||
errorEl.className = 'alert alert-error';
|
errorEl.className = 'alert alert-error';
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (password.length < 8) {
|
if (password.length < 8) {
|
||||||
errorEl.textContent = t('admin.error_password_short');
|
errorEl.textContent = i18n.t('admin.error_password_short');
|
||||||
errorEl.className = 'alert alert-error';
|
errorEl.className = 'alert alert-error';
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const btn = document.getElementById('cu-submit');
|
const btn = document.getElementById('cu-submit');
|
||||||
btn.disabled = true;
|
btn.disabled = true;
|
||||||
btn.innerHTML = `<i class="fas fa-spinner fa-spin"></i> ${escapeHtml(t('admin.creating'))}`;
|
btn.innerHTML = `<i class="fas fa-spinner fa-spin"></i> ${escapeHtml(i18n.t('admin.creating'))}`;
|
||||||
try {
|
try {
|
||||||
const resp = await fetch(`${API}/admin/users`, {
|
const resp = await fetch(`${API}/admin/users`, {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
@@ -514,15 +507,15 @@ async function submitCreateUser() {
|
|||||||
loadDashboard();
|
loadDashboard();
|
||||||
} else {
|
} else {
|
||||||
const e = await resp.json().catch(() => ({}));
|
const e = await resp.json().catch(() => ({}));
|
||||||
errorEl.textContent = e.message || t('admin.error_create_user');
|
errorEl.textContent = e.message || i18n.t('admin.error_create_user');
|
||||||
errorEl.className = 'alert alert-error';
|
errorEl.className = 'alert alert-error';
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
errorEl.textContent = t('admin.error_network', { message: e.message });
|
errorEl.textContent = i18n.t('admin.error_network', { message: e.message });
|
||||||
errorEl.className = 'alert alert-error';
|
errorEl.className = 'alert alert-error';
|
||||||
}
|
}
|
||||||
btn.disabled = false;
|
btn.disabled = false;
|
||||||
btn.innerHTML = `<i class="fas fa-user-plus"></i> ${escapeHtml(t('admin.create_user'))}`;
|
btn.innerHTML = `<i class="fas fa-user-plus"></i> ${escapeHtml(i18n.t('admin.create_user'))}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
let resetPwUserId = '';
|
let resetPwUserId = '';
|
||||||
@@ -543,14 +536,14 @@ async function submitResetPassword() {
|
|||||||
const password = document.getElementById('rp-password').value;
|
const password = document.getElementById('rp-password').value;
|
||||||
const errorEl = document.getElementById('rp-error');
|
const errorEl = document.getElementById('rp-error');
|
||||||
if (password.length < 8) {
|
if (password.length < 8) {
|
||||||
errorEl.textContent = t('admin.error_password_short');
|
errorEl.textContent = i18n.t('admin.error_password_short');
|
||||||
errorEl.className = 'alert alert-error';
|
errorEl.className = 'alert alert-error';
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const btn = document.getElementById('rp-submit');
|
const btn = document.getElementById('rp-submit');
|
||||||
btn.disabled = true;
|
btn.disabled = true;
|
||||||
btn.innerHTML = `<i class="fas fa-spinner fa-spin"></i> ${escapeHtml(t('admin.resetting'))}`;
|
btn.innerHTML = `<i class="fas fa-spinner fa-spin"></i> ${escapeHtml(i18n.t('admin.resetting'))}`;
|
||||||
try {
|
try {
|
||||||
const resp = await fetch(`${API}/admin/users/${resetPwUserId}/password`, {
|
const resp = await fetch(`${API}/admin/users/${resetPwUserId}/password`, {
|
||||||
method: 'PUT',
|
method: 'PUT',
|
||||||
@@ -562,15 +555,15 @@ async function submitResetPassword() {
|
|||||||
closeResetPasswordModal();
|
closeResetPasswordModal();
|
||||||
} else {
|
} else {
|
||||||
const e = await resp.json().catch(() => ({}));
|
const e = await resp.json().catch(() => ({}));
|
||||||
errorEl.textContent = e.message || t('admin.error_generic');
|
errorEl.textContent = e.message || i18n.t('admin.error_generic');
|
||||||
errorEl.className = 'alert alert-error';
|
errorEl.className = 'alert alert-error';
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
errorEl.textContent = t('admin.error_network', { message: e.message });
|
errorEl.textContent = i18n.t('admin.error_network', { message: e.message });
|
||||||
errorEl.className = 'alert alert-error';
|
errorEl.className = 'alert alert-error';
|
||||||
}
|
}
|
||||||
btn.disabled = false;
|
btn.disabled = false;
|
||||||
btn.innerHTML = `<i class="fas fa-save"></i> ${escapeHtml(t('admin.reset_btn'))}`;
|
btn.innerHTML = `<i class="fas fa-save"></i> ${escapeHtml(i18n.t('admin.reset_btn'))}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
async function toggleRegistration(enabled) {
|
async function toggleRegistration(enabled) {
|
||||||
@@ -588,13 +581,13 @@ async function toggleRegistration(enabled) {
|
|||||||
if (!enabled) showElement('registration-warning', 'flex');
|
if (!enabled) showElement('registration-warning', 'flex');
|
||||||
else hideElement('registration-warning');
|
else hideElement('registration-warning');
|
||||||
const e = await resp.json().catch(() => ({}));
|
const e = await resp.json().catch(() => ({}));
|
||||||
alert(e.message || t('admin.error_generic'));
|
alert(e.message || i18n.t('admin.error_generic'));
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
document.getElementById('ds-registration').checked = !enabled;
|
document.getElementById('ds-registration').checked = !enabled;
|
||||||
if (!enabled) showElement('registration-warning', 'flex');
|
if (!enabled) showElement('registration-warning', 'flex');
|
||||||
else hideElement('registration-warning');
|
else hideElement('registration-warning');
|
||||||
alert(t('admin.error_network', { message: e.message }));
|
alert(i18n.t('admin.error_network', { message: e.message }));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -626,7 +619,7 @@ async function testConnection() {
|
|||||||
}
|
}
|
||||||
const btn = document.getElementById('discover-btn');
|
const btn = document.getElementById('discover-btn');
|
||||||
btn.disabled = true;
|
btn.disabled = true;
|
||||||
btn.innerHTML = `<i class="fas fa-spinner fa-spin"></i> ${escapeHtml(t('admin.discovering'))}`;
|
btn.innerHTML = `<i class="fas fa-spinner fa-spin"></i> ${escapeHtml(i18n.t('admin.discovering'))}`;
|
||||||
const resultDiv = document.getElementById('discovery-result');
|
const resultDiv = document.getElementById('discovery-result');
|
||||||
try {
|
try {
|
||||||
const resp = await fetch(`${API}/admin/settings/oidc/test`, {
|
const resp = await fetch(`${API}/admin/settings/oidc/test`, {
|
||||||
@@ -654,13 +647,13 @@ async function testConnection() {
|
|||||||
resultDiv.innerHTML = `<div class="discovery-result fail"><i class="fas fa-times-circle"></i> Error: ${escapeHtml(e.message)}</div>`;
|
resultDiv.innerHTML = `<div class="discovery-result fail"><i class="fas fa-times-circle"></i> Error: ${escapeHtml(e.message)}</div>`;
|
||||||
}
|
}
|
||||||
btn.disabled = false;
|
btn.disabled = false;
|
||||||
btn.innerHTML = `<i class="fas fa-search"></i> ${escapeHtml(t('admin.auto_discover'))}`;
|
btn.innerHTML = `<i class="fas fa-search"></i> ${escapeHtml(i18n.t('admin.auto_discover'))}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
async function saveOidcSettings() {
|
async function saveOidcSettings() {
|
||||||
const btn = document.getElementById('save-btn');
|
const btn = document.getElementById('save-btn');
|
||||||
btn.disabled = true;
|
btn.disabled = true;
|
||||||
btn.innerHTML = `<i class="fas fa-spinner fa-spin"></i> ${escapeHtml(t('admin.saving'))}`;
|
btn.innerHTML = `<i class="fas fa-spinner fa-spin"></i> ${escapeHtml(i18n.t('admin.saving'))}`;
|
||||||
const body = {
|
const body = {
|
||||||
enabled: document.getElementById('oidc-enabled').checked,
|
enabled: document.getElementById('oidc-enabled').checked,
|
||||||
issuer_url: document.getElementById('issuer-url').value.trim(),
|
issuer_url: document.getElementById('issuer-url').value.trim(),
|
||||||
@@ -680,18 +673,18 @@ async function saveOidcSettings() {
|
|||||||
body: JSON.stringify(body)
|
body: JSON.stringify(body)
|
||||||
});
|
});
|
||||||
if (resp.ok) {
|
if (resp.ok) {
|
||||||
const status = body.enabled ? t('admin.active').toLowerCase() : t('admin.disabled').toLowerCase();
|
const status = body.enabled ? i18n.t('admin.active').toLowerCase() : i18n.t('admin.disabled').toLowerCase();
|
||||||
showOidcStatus(t('admin.settings_saved', { status: status }), 'success');
|
showOidcStatus(i18n.t('admin.settings_saved', { status: status }), 'success');
|
||||||
loadDashboard();
|
loadDashboard();
|
||||||
} else {
|
} else {
|
||||||
const e = await resp.json().catch(() => ({}));
|
const e = await resp.json().catch(() => ({}));
|
||||||
showOidcStatus(`Error: ${e.message || resp.statusText}`, 'error');
|
showOidcStatus(`Error: ${e.message || resp.statusText}`, 'error');
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
showOidcStatus(t('admin.error_network', { message: e.message }), 'error');
|
showOidcStatus(i18n.t('admin.error_network', { message: e.message }), 'error');
|
||||||
}
|
}
|
||||||
btn.disabled = false;
|
btn.disabled = false;
|
||||||
btn.innerHTML = `<i class="fas fa-save"></i> ${escapeHtml(t('admin.save_btn'))}`;
|
btn.innerHTML = `<i class="fas fa-save"></i> ${escapeHtml(i18n.t('admin.save_btn'))}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* ── Storage tab ── */
|
/* ── Storage tab ── */
|
||||||
@@ -752,7 +745,7 @@ async function loadStorage() {
|
|||||||
|
|
||||||
// Secret hints
|
// Secret hints
|
||||||
if (s.s3_access_key_set) {
|
if (s.s3_access_key_set) {
|
||||||
document.getElementById('storage-access-key').placeholder = t('admin.storage_key_placeholder') || 'Leave empty to keep current value';
|
document.getElementById('storage-access-key').placeholder = i18n.t('admin.storage_key_placeholder') || 'Leave empty to keep current value';
|
||||||
}
|
}
|
||||||
if (s.s3_secret_key_set) {
|
if (s.s3_secret_key_set) {
|
||||||
showElement('storage-secret-hint');
|
showElement('storage-secret-hint');
|
||||||
@@ -772,7 +765,7 @@ async function loadStorage() {
|
|||||||
document.getElementById('storage-total-size').textContent = s.total_bytes_stored != null ? formatBytes(s.total_bytes_stored) : '—';
|
document.getElementById('storage-total-size').textContent = s.total_bytes_stored != null ? formatBytes(s.total_bytes_stored) : '—';
|
||||||
document.getElementById('storage-dedup-ratio').textContent = s.dedup_ratio != null ? `${s.dedup_ratio.toFixed(2)}x` : '—';
|
document.getElementById('storage-dedup-ratio').textContent = s.dedup_ratio != null ? `${s.dedup_ratio.toFixed(2)}x` : '—';
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
showStorageStatus(t('admin.error_network', { message: e.message }), 'error');
|
showStorageStatus(i18n.t('admin.error_network', { message: e.message }), 'error');
|
||||||
}
|
}
|
||||||
|
|
||||||
// Also load migration status
|
// Also load migration status
|
||||||
@@ -782,7 +775,7 @@ async function loadStorage() {
|
|||||||
async function saveStorageSettings() {
|
async function saveStorageSettings() {
|
||||||
const btn = document.getElementById('btn-save-storage');
|
const btn = document.getElementById('btn-save-storage');
|
||||||
btn.disabled = true;
|
btn.disabled = true;
|
||||||
btn.innerHTML = `<i class="fas fa-spinner fa-spin"></i> ${escapeHtml(t('admin.saving'))}`;
|
btn.innerHTML = `<i class="fas fa-spinner fa-spin"></i> ${escapeHtml(i18n.t('admin.saving'))}`;
|
||||||
|
|
||||||
const backend = document.querySelector('input[name="storage-backend"]:checked').value;
|
const backend = document.querySelector('input[name="storage-backend"]:checked').value;
|
||||||
const body = {
|
const body = {
|
||||||
@@ -803,23 +796,23 @@ async function saveStorageSettings() {
|
|||||||
body: JSON.stringify(body)
|
body: JSON.stringify(body)
|
||||||
});
|
});
|
||||||
if (resp.ok) {
|
if (resp.ok) {
|
||||||
showStorageStatus(t('admin.storage_saved') || 'Storage settings saved successfully', 'success');
|
showStorageStatus(i18n.t('admin.storage_saved') || 'Storage settings saved successfully', 'success');
|
||||||
loadStorage();
|
loadStorage();
|
||||||
} else {
|
} else {
|
||||||
const e = await resp.json().catch(() => ({}));
|
const e = await resp.json().catch(() => ({}));
|
||||||
showStorageStatus(`Error: ${e.message || resp.statusText}`, 'error');
|
showStorageStatus(`Error: ${e.message || resp.statusText}`, 'error');
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
showStorageStatus(t('admin.error_network', { message: e.message }), 'error');
|
showStorageStatus(i18n.t('admin.error_network', { message: e.message }), 'error');
|
||||||
}
|
}
|
||||||
btn.disabled = false;
|
btn.disabled = false;
|
||||||
btn.innerHTML = `<i class="fas fa-save"></i> ${escapeHtml(t('admin.storage_save') || 'Save')}`;
|
btn.innerHTML = `<i class="fas fa-save"></i> ${escapeHtml(i18n.t('admin.storage_save') || 'Save')}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
async function testStorageConnection() {
|
async function testStorageConnection() {
|
||||||
const btn = document.getElementById('btn-test-storage');
|
const btn = document.getElementById('btn-test-storage');
|
||||||
btn.disabled = true;
|
btn.disabled = true;
|
||||||
btn.innerHTML = `<i class="fas fa-spinner fa-spin"></i> ${escapeHtml(t('admin.testing') || 'Testing...')}`;
|
btn.innerHTML = `<i class="fas fa-spinner fa-spin"></i> ${escapeHtml(i18n.t('admin.testing') || 'Testing...')}`;
|
||||||
|
|
||||||
const backend = document.querySelector('input[name="storage-backend"]:checked').value;
|
const backend = document.querySelector('input[name="storage-backend"]:checked').value;
|
||||||
const body = {
|
const body = {
|
||||||
@@ -841,17 +834,17 @@ async function testStorageConnection() {
|
|||||||
});
|
});
|
||||||
const r = await resp.json();
|
const r = await resp.json();
|
||||||
if (r.connected) {
|
if (r.connected) {
|
||||||
let msg = `${t('admin.storage_test_success') || 'Connection successful'} (${escapeHtml(r.backend_type)})`;
|
let msg = `${i18n.t('admin.storage_test_success') || 'Connection successful'} (${escapeHtml(r.backend_type)})`;
|
||||||
if (r.available_bytes != null) msg += ` — ${formatBytes(r.available_bytes)} available`;
|
if (r.available_bytes != null) msg += ` — ${formatBytes(r.available_bytes)} available`;
|
||||||
showStorageStatus(msg, 'success');
|
showStorageStatus(msg, 'success');
|
||||||
} else {
|
} else {
|
||||||
showStorageStatus(`${t('admin.storage_test_failure') || 'Connection failed'}: ${escapeHtml(r.message)}`, 'error');
|
showStorageStatus(`${i18n.t('admin.storage_test_failure') || 'Connection failed'}: ${escapeHtml(r.message)}`, 'error');
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
showStorageStatus(t('admin.error_network', { message: e.message }), 'error');
|
showStorageStatus(i18n.t('admin.error_network', { message: e.message }), 'error');
|
||||||
}
|
}
|
||||||
btn.disabled = false;
|
btn.disabled = false;
|
||||||
btn.innerHTML = `<i class="fas fa-vial"></i> ${escapeHtml(t('admin.storage_test_connection') || 'Test Connection')}`;
|
btn.innerHTML = `<i class="fas fa-vial"></i> ${escapeHtml(i18n.t('admin.storage_test_connection') || 'Test Connection')}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* ── Migration ── */
|
/* ── Migration ── */
|
||||||
@@ -953,14 +946,14 @@ async function startMigration() {
|
|||||||
body: JSON.stringify({ concurrency: 4 })
|
body: JSON.stringify({ concurrency: 4 })
|
||||||
});
|
});
|
||||||
if (resp.ok) {
|
if (resp.ok) {
|
||||||
showMigrationMsg(t('admin.migration_started') || 'Migration started', 'success');
|
showMigrationMsg(i18n.t('admin.migration_started') || 'Migration started', 'success');
|
||||||
loadMigrationStatus();
|
loadMigrationStatus();
|
||||||
} else {
|
} else {
|
||||||
const e = await resp.json().catch(() => ({}));
|
const e = await resp.json().catch(() => ({}));
|
||||||
showMigrationMsg(`Error: ${e.message || resp.statusText}`, 'error');
|
showMigrationMsg(`Error: ${e.message || resp.statusText}`, 'error');
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
showMigrationMsg(t('admin.error_network', { message: e.message }), 'error');
|
showMigrationMsg(i18n.t('admin.error_network', { message: e.message }), 'error');
|
||||||
}
|
}
|
||||||
btn.disabled = false;
|
btn.disabled = false;
|
||||||
}
|
}
|
||||||
@@ -973,7 +966,7 @@ async function pauseMigration() {
|
|||||||
credentials: 'same-origin'
|
credentials: 'same-origin'
|
||||||
});
|
});
|
||||||
if (resp.ok) {
|
if (resp.ok) {
|
||||||
showMigrationMsg(t('admin.migration_paused_msg') || 'Migration paused', 'success');
|
showMigrationMsg(i18n.t('admin.migration_paused_msg') || 'Migration paused', 'success');
|
||||||
loadMigrationStatus();
|
loadMigrationStatus();
|
||||||
}
|
}
|
||||||
} catch (_e) {
|
} catch (_e) {
|
||||||
@@ -989,7 +982,7 @@ async function resumeMigration() {
|
|||||||
credentials: 'same-origin'
|
credentials: 'same-origin'
|
||||||
});
|
});
|
||||||
if (resp.ok) {
|
if (resp.ok) {
|
||||||
showMigrationMsg(t('admin.migration_resumed_msg') || 'Migration resumed', 'success');
|
showMigrationMsg(i18n.t('admin.migration_resumed_msg') || 'Migration resumed', 'success');
|
||||||
loadMigrationStatus();
|
loadMigrationStatus();
|
||||||
}
|
}
|
||||||
} catch (_e) {
|
} catch (_e) {
|
||||||
@@ -1000,7 +993,7 @@ async function resumeMigration() {
|
|||||||
async function verifyMigration() {
|
async function verifyMigration() {
|
||||||
const btn = document.getElementById('btn-verify-migration');
|
const btn = document.getElementById('btn-verify-migration');
|
||||||
btn.disabled = true;
|
btn.disabled = true;
|
||||||
btn.innerHTML = `<i class="fas fa-spinner fa-spin"></i> ${escapeHtml(t('admin.migration_verifying') || 'Verifying...')}`;
|
btn.innerHTML = `<i class="fas fa-spinner fa-spin"></i> ${escapeHtml(i18n.t('admin.migration_verifying') || 'Verifying...')}`;
|
||||||
const resultDiv = document.getElementById('migration-verify-result');
|
const resultDiv = document.getElementById('migration-verify-result');
|
||||||
try {
|
try {
|
||||||
const resp = await fetch(`${API}/admin/storage/migration/verify`, {
|
const resp = await fetch(`${API}/admin/storage/migration/verify`, {
|
||||||
@@ -1012,19 +1005,19 @@ async function verifyMigration() {
|
|||||||
const r = await resp.json();
|
const r = await resp.json();
|
||||||
resultDiv.style.display = '';
|
resultDiv.style.display = '';
|
||||||
if (r.passed) {
|
if (r.passed) {
|
||||||
resultDiv.innerHTML = `<div class="discovery-result ok"><strong><i class="fas fa-check-circle"></i> ${escapeHtml(t('admin.migration_verify_passed') || 'Verification passed')}</strong><p>${r.sample_checked} blobs checked, ${r.pg_blob_count} total in database</p></div>`;
|
resultDiv.innerHTML = `<div class="discovery-result ok"><strong><i class="fas fa-check-circle"></i> ${escapeHtml(i18n.t('admin.migration_verify_passed') || 'Verification passed')}</strong><p>${r.sample_checked} blobs checked, ${r.pg_blob_count} total in database</p></div>`;
|
||||||
} else {
|
} else {
|
||||||
const issues = [];
|
const issues = [];
|
||||||
if (r.missing_in_target.length) issues.push(`${r.missing_in_target.length} missing`);
|
if (r.missing_in_target.length) issues.push(`${r.missing_in_target.length} missing`);
|
||||||
if (r.size_mismatches.length) issues.push(`${r.size_mismatches.length} size mismatches`);
|
if (r.size_mismatches.length) issues.push(`${r.size_mismatches.length} size mismatches`);
|
||||||
resultDiv.innerHTML = `<div class="discovery-result fail"><strong><i class="fas fa-times-circle"></i> ${escapeHtml(t('admin.migration_verify_failed') || 'Verification failed')}</strong><p>${issues.join(', ')}</p></div>`;
|
resultDiv.innerHTML = `<div class="discovery-result fail"><strong><i class="fas fa-times-circle"></i> ${escapeHtml(i18n.t('admin.migration_verify_failed') || 'Verification failed')}</strong><p>${issues.join(', ')}</p></div>`;
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
resultDiv.style.display = '';
|
resultDiv.style.display = '';
|
||||||
resultDiv.innerHTML = `<div class="discovery-result fail"><i class="fas fa-times-circle"></i> Error: ${escapeHtml(e.message)}</div>`;
|
resultDiv.innerHTML = `<div class="discovery-result fail"><i class="fas fa-times-circle"></i> Error: ${escapeHtml(e.message)}</div>`;
|
||||||
}
|
}
|
||||||
btn.disabled = false;
|
btn.disabled = false;
|
||||||
btn.innerHTML = `<i class="fas fa-check-double"></i> ${escapeHtml(t('admin.migration_verify') || 'Verify Integrity')}`;
|
btn.innerHTML = `<i class="fas fa-check-double"></i> ${escapeHtml(i18n.t('admin.migration_verify') || 'Verify Integrity')}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
async function completeMigration() {
|
async function completeMigration() {
|
||||||
@@ -1035,14 +1028,14 @@ async function completeMigration() {
|
|||||||
credentials: 'same-origin'
|
credentials: 'same-origin'
|
||||||
});
|
});
|
||||||
if (resp.ok) {
|
if (resp.ok) {
|
||||||
showMigrationMsg(t('admin.migration_completed_msg') || 'Migration finalized. Restart the server to use the new backend.', 'success');
|
showMigrationMsg(i18n.t('admin.migration_completed_msg') || 'Migration finalized. Restart the server to use the new backend.', 'success');
|
||||||
loadMigrationStatus();
|
loadMigrationStatus();
|
||||||
} else {
|
} else {
|
||||||
const e = await resp.json().catch(() => ({}));
|
const e = await resp.json().catch(() => ({}));
|
||||||
showMigrationMsg(`Error: ${e.message || resp.statusText}`, 'error');
|
showMigrationMsg(`Error: ${e.message || resp.statusText}`, 'error');
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
showMigrationMsg(t('admin.error_network', { message: e.message }), 'error');
|
showMigrationMsg(i18n.t('admin.error_network', { message: e.message }), 'error');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1105,13 +1098,13 @@ function showAccessDenied() {
|
|||||||
|
|
||||||
/* ── Apply i18n when translations load / change ── */
|
/* ── Apply i18n when translations load / change ── */
|
||||||
document.addEventListener('translationsLoaded', () => {
|
document.addEventListener('translationsLoaded', () => {
|
||||||
if (i18n?.translatePage) i18n.translatePage();
|
i18n.translatePage();
|
||||||
// Re-render dynamic content that uses t()
|
// Re-render dynamic content that uses i18n.t()
|
||||||
loadDashboard();
|
loadDashboard();
|
||||||
if (activeTabName === 'users') loadUsers();
|
if (activeTabName === 'users') loadUsers();
|
||||||
});
|
});
|
||||||
document.addEventListener('localeChanged', () => {
|
document.addEventListener('localeChanged', () => {
|
||||||
if (i18n?.translatePage) i18n.translatePage();
|
i18n.translatePage();
|
||||||
loadDashboard();
|
loadDashboard();
|
||||||
if (activeTabName === 'users') loadUsers();
|
if (activeTabName === 'users') loadUsers();
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -3,12 +3,6 @@ import { i18n } from '../../core/i18n.js';
|
|||||||
|
|
||||||
const API = '/api';
|
const API = '/api';
|
||||||
|
|
||||||
/* ── i18n helper — falls back to key if i18n not ready ── */
|
|
||||||
function t(key, params) {
|
|
||||||
if (i18n && typeof i18n.t === 'function') return i18n.t(key, params);
|
|
||||||
return key.split('.').pop().replace(/_/g, ' ');
|
|
||||||
}
|
|
||||||
|
|
||||||
function headers() {
|
function headers() {
|
||||||
return { 'Content-Type': 'application/json', ...getCsrfHeaders() };
|
return { 'Content-Type': 'application/json', ...getCsrfHeaders() };
|
||||||
}
|
}
|
||||||
@@ -22,14 +16,14 @@ function formatBytes(bytes) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function timeAgo(dateStr) {
|
function timeAgo(dateStr) {
|
||||||
if (!dateStr) return t('profile.never');
|
if (!dateStr) return i18n.t('profile.never');
|
||||||
const d = new Date(dateStr);
|
const d = new Date(dateStr);
|
||||||
const now = new Date();
|
const now = new Date();
|
||||||
const secs = Math.floor((now - d) / 1000);
|
const secs = Math.floor((now - d) / 1000);
|
||||||
if (secs < 60) return t('profile.just_now');
|
if (secs < 60) return i18n.t('profile.just_now');
|
||||||
if (secs < 3600) return t('profile.minutes_ago', { n: Math.floor(secs / 60) });
|
if (secs < 3600) return i18n.t('profile.minutes_ago', { n: Math.floor(secs / 60) });
|
||||||
if (secs < 86400) return t('profile.hours_ago', { n: Math.floor(secs / 3600) });
|
if (secs < 86400) return i18n.t('profile.hours_ago', { n: Math.floor(secs / 3600) });
|
||||||
if (secs < 2592000) return t('profile.days_ago', { n: Math.floor(secs / 86400) });
|
if (secs < 2592000) return i18n.t('profile.days_ago', { n: Math.floor(secs / 86400) });
|
||||||
return d.toLocaleDateString();
|
return d.toLocaleDateString();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -53,15 +47,15 @@ async function init() {
|
|||||||
const badge = document.getElementById('p-role-badge');
|
const badge = document.getElementById('p-role-badge');
|
||||||
if (user.role === 'admin') {
|
if (user.role === 'admin') {
|
||||||
badge.className = 'role-badge role-badge-admin';
|
badge.className = 'role-badge role-badge-admin';
|
||||||
badge.innerHTML = `<i class="fas fa-shield-alt"></i> ${t('profile.role_admin')}`;
|
badge.innerHTML = `<i class="fas fa-shield-alt"></i> ${i18n.t('profile.role_admin')}`;
|
||||||
} else {
|
} else {
|
||||||
badge.className = 'role-badge role-badge-user';
|
badge.className = 'role-badge role-badge-user';
|
||||||
badge.innerHTML = `<i class="fas fa-user"></i> ${t('profile.role_user')}`;
|
badge.innerHTML = `<i class="fas fa-user"></i> ${i18n.t('profile.role_user')}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
document.getElementById('p-detail-username').textContent = user.username;
|
document.getElementById('p-detail-username').textContent = user.username;
|
||||||
document.getElementById('p-detail-email').textContent = user.email || '—';
|
document.getElementById('p-detail-email').textContent = user.email || '—';
|
||||||
document.getElementById('p-detail-role').textContent = user.role === 'admin' ? t('profile.role_admin') : t('profile.role_user');
|
document.getElementById('p-detail-role').textContent = user.role === 'admin' ? i18n.t('profile.role_admin') : i18n.t('profile.role_user');
|
||||||
document.getElementById('p-detail-login').textContent = timeAgo(user.last_login_at);
|
document.getElementById('p-detail-login').textContent = timeAgo(user.last_login_at);
|
||||||
|
|
||||||
const used = user.storage_used_bytes || 0;
|
const used = user.storage_used_bytes || 0;
|
||||||
@@ -75,7 +69,7 @@ async function init() {
|
|||||||
const bar = document.getElementById('p-storage-bar');
|
const bar = document.getElementById('p-storage-bar');
|
||||||
bar.style.width = `${pct}%`;
|
bar.style.width = `${pct}%`;
|
||||||
bar.className = `storage-fill ${pct > 90 ? 'red' : pct > 70 ? 'orange' : 'green'}`;
|
bar.className = `storage-fill ${pct > 90 ? 'red' : pct > 70 ? 'orange' : 'green'}`;
|
||||||
document.getElementById('p-storage-text').textContent = `${formatBytes(used)} / ${quota > 0 ? formatBytes(quota) : t('profile.unlimited')}`;
|
document.getElementById('p-storage-text').textContent = `${formatBytes(used)} / ${quota > 0 ? formatBytes(quota) : i18n.t('profile.unlimited')}`;
|
||||||
|
|
||||||
if (user.auth_provider && user.auth_provider !== 'local') {
|
if (user.auth_provider && user.auth_provider !== 'local') {
|
||||||
document.getElementById('password-section').classList.add('hidden');
|
document.getElementById('password-section').classList.add('hidden');
|
||||||
@@ -116,18 +110,18 @@ async function changePassword(e) {
|
|||||||
const statusEl = document.getElementById('pw-status');
|
const statusEl = document.getElementById('pw-status');
|
||||||
|
|
||||||
if (newPw !== confirmPw) {
|
if (newPw !== confirmPw) {
|
||||||
statusEl.innerHTML = `<div class="alert alert-error"><i class="fas fa-exclamation-circle"></i> ${escapeHtml(t('profile.passwords_no_match'))}</div>`;
|
statusEl.innerHTML = `<div class="alert alert-error"><i class="fas fa-exclamation-circle"></i> ${escapeHtml(i18n.t('profile.passwords_no_match'))}</div>`;
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (newPw.length < 8) {
|
if (newPw.length < 8) {
|
||||||
statusEl.innerHTML = `<div class="alert alert-error"><i class="fas fa-exclamation-circle"></i> ${escapeHtml(t('profile.password_too_short'))}</div>`;
|
statusEl.innerHTML = `<div class="alert alert-error"><i class="fas fa-exclamation-circle"></i> ${escapeHtml(i18n.t('profile.password_too_short'))}</div>`;
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
const btn = document.getElementById('pw-submit');
|
const btn = document.getElementById('pw-submit');
|
||||||
btn.disabled = true;
|
btn.disabled = true;
|
||||||
btn.innerHTML = `<i class="fas fa-spinner fa-spin"></i> ${escapeHtml(t('profile.updating'))}`;
|
btn.innerHTML = `<i class="fas fa-spinner fa-spin"></i> ${escapeHtml(i18n.t('profile.updating'))}`;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const resp = await fetch(`${API}/auth/change-password`, {
|
const resp = await fetch(`${API}/auth/change-password`, {
|
||||||
@@ -141,24 +135,24 @@ async function changePassword(e) {
|
|||||||
});
|
});
|
||||||
|
|
||||||
if (resp.ok) {
|
if (resp.ok) {
|
||||||
statusEl.innerHTML = `<div class="alert alert-success"><i class="fas fa-check-circle"></i> ${escapeHtml(t('profile.password_updated'))}</div>`;
|
statusEl.innerHTML = `<div class="alert alert-success"><i class="fas fa-check-circle"></i> ${escapeHtml(i18n.t('profile.password_updated'))}</div>`;
|
||||||
document.getElementById('password-form').reset();
|
document.getElementById('password-form').reset();
|
||||||
} else {
|
} else {
|
||||||
const err = await resp.json().catch(() => ({}));
|
const err = await resp.json().catch(() => ({}));
|
||||||
statusEl.innerHTML =
|
statusEl.innerHTML =
|
||||||
'<div class="alert alert-error"><i class="fas fa-exclamation-circle"></i> ' +
|
'<div class="alert alert-error"><i class="fas fa-exclamation-circle"></i> ' +
|
||||||
escapeHtml(err.message || t('profile.password_change_failed')) +
|
escapeHtml(err.message || i18n.t('profile.password_change_failed')) +
|
||||||
'</div>';
|
'</div>';
|
||||||
}
|
}
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
statusEl.innerHTML =
|
statusEl.innerHTML =
|
||||||
'<div class="alert alert-error"><i class="fas fa-exclamation-circle"></i> ' +
|
'<div class="alert alert-error"><i class="fas fa-exclamation-circle"></i> ' +
|
||||||
escapeHtml(t('profile.error_network', { message: err.message })) +
|
escapeHtml(i18n.t('profile.error_network', { message: err.message })) +
|
||||||
'</div>';
|
'</div>';
|
||||||
}
|
}
|
||||||
|
|
||||||
btn.disabled = false;
|
btn.disabled = false;
|
||||||
btn.innerHTML = `<i class="fas fa-save"></i> ${escapeHtml(t('profile.update_password'))}`;
|
btn.innerHTML = `<i class="fas fa-save"></i> ${escapeHtml(i18n.t('profile.update_password'))}`;
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -177,15 +171,15 @@ function renderPwRow(pw) {
|
|||||||
const created = document.createElement('td');
|
const created = document.createElement('td');
|
||||||
created.textContent = new Date(pw.created_at).toLocaleDateString();
|
created.textContent = new Date(pw.created_at).toLocaleDateString();
|
||||||
const lastUsed = document.createElement('td');
|
const lastUsed = document.createElement('td');
|
||||||
lastUsed.textContent = pw.last_used_at ? timeAgo(pw.last_used_at) : t('profile.never');
|
lastUsed.textContent = pw.last_used_at ? timeAgo(pw.last_used_at) : i18n.t('profile.never');
|
||||||
const status = document.createElement('td');
|
const status = document.createElement('td');
|
||||||
const badge = document.createElement('span');
|
const badge = document.createElement('span');
|
||||||
if (pw.active !== false) {
|
if (pw.active !== false) {
|
||||||
badge.className = 'badge badge-active';
|
badge.className = 'badge badge-active';
|
||||||
badge.textContent = t('profile.active');
|
badge.textContent = i18n.t('profile.active');
|
||||||
} else {
|
} else {
|
||||||
badge.className = 'badge badge-expired';
|
badge.className = 'badge badge-expired';
|
||||||
badge.textContent = t('profile.revoked');
|
badge.textContent = i18n.t('profile.revoked');
|
||||||
}
|
}
|
||||||
status.appendChild(badge);
|
status.appendChild(badge);
|
||||||
const actions = document.createElement('td');
|
const actions = document.createElement('td');
|
||||||
@@ -193,7 +187,7 @@ function renderPwRow(pw) {
|
|||||||
const btn = document.createElement('button');
|
const btn = document.createElement('button');
|
||||||
btn.className = 'btn btn-danger-sm';
|
btn.className = 'btn btn-danger-sm';
|
||||||
btn.innerHTML = '<i class="fas fa-trash"></i>';
|
btn.innerHTML = '<i class="fas fa-trash"></i>';
|
||||||
btn.title = t('profile.revoke_title');
|
btn.title = i18n.t('profile.revoke_title');
|
||||||
btn.addEventListener('click', () => {
|
btn.addEventListener('click', () => {
|
||||||
revokeAppPassword(pw.id, pw.label);
|
revokeAppPassword(pw.id, pw.label);
|
||||||
});
|
});
|
||||||
@@ -265,12 +259,12 @@ async function createAppPassword() {
|
|||||||
const btn = document.getElementById('app-pw-generate');
|
const btn = document.getElementById('app-pw-generate');
|
||||||
|
|
||||||
if (!label) {
|
if (!label) {
|
||||||
statusEl.innerHTML = `<div class="alert alert-error"><i class="fas fa-exclamation-circle"></i> ${escapeHtml(t('profile.error_label_required'))}</div>`;
|
statusEl.innerHTML = `<div class="alert alert-error"><i class="fas fa-exclamation-circle"></i> ${escapeHtml(i18n.t('profile.error_label_required'))}</div>`;
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
btn.disabled = true;
|
btn.disabled = true;
|
||||||
btn.innerHTML = `<i class="fas fa-spinner fa-spin"></i> ${escapeHtml(t('profile.generating'))}`;
|
btn.innerHTML = `<i class="fas fa-spinner fa-spin"></i> ${escapeHtml(i18n.t('profile.generating'))}`;
|
||||||
statusEl.innerHTML = '';
|
statusEl.innerHTML = '';
|
||||||
|
|
||||||
try {
|
try {
|
||||||
@@ -284,7 +278,7 @@ async function createAppPassword() {
|
|||||||
const err = await resp.json().catch(() => ({}));
|
const err = await resp.json().catch(() => ({}));
|
||||||
statusEl.innerHTML =
|
statusEl.innerHTML =
|
||||||
'<div class="alert alert-error"><i class="fas fa-exclamation-circle"></i> ' +
|
'<div class="alert alert-error"><i class="fas fa-exclamation-circle"></i> ' +
|
||||||
escapeHtml(err.message || t('profile.error_create_pw')) +
|
escapeHtml(err.message || i18n.t('profile.error_create_pw')) +
|
||||||
'</div>';
|
'</div>';
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -298,7 +292,7 @@ async function createAppPassword() {
|
|||||||
statusEl.innerHTML = `<div class="alert alert-error"><i class="fas fa-exclamation-circle"></i> ${err.message}</div>`;
|
statusEl.innerHTML = `<div class="alert alert-error"><i class="fas fa-exclamation-circle"></i> ${err.message}</div>`;
|
||||||
} finally {
|
} finally {
|
||||||
btn.disabled = false;
|
btn.disabled = false;
|
||||||
btn.innerHTML = `<i class="fas fa-plus"></i> ${escapeHtml(t('profile.generate'))}`;
|
btn.innerHTML = `<i class="fas fa-plus"></i> ${escapeHtml(i18n.t('profile.generate'))}`;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -314,7 +308,7 @@ function copyAppPassword() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async function revokeAppPassword(id, label) {
|
async function revokeAppPassword(id, label) {
|
||||||
if (!confirm(t('profile.confirm_revoke', { label: label }))) return;
|
if (!confirm(i18n.t('profile.confirm_revoke', { label: label }))) return;
|
||||||
try {
|
try {
|
||||||
const resp = await fetch(`${API}/auth/app-passwords/${encodeURIComponent(id)}`, {
|
const resp = await fetch(`${API}/auth/app-passwords/${encodeURIComponent(id)}`, {
|
||||||
method: 'DELETE',
|
method: 'DELETE',
|
||||||
@@ -326,10 +320,10 @@ async function revokeAppPassword(id, label) {
|
|||||||
loadAppPasswords();
|
loadAppPasswords();
|
||||||
} else {
|
} else {
|
||||||
const err = await resp.json().catch(() => ({}));
|
const err = await resp.json().catch(() => ({}));
|
||||||
alert(err.message || t('profile.error_revoke'));
|
alert(err.message || i18n.t('profile.error_revoke'));
|
||||||
}
|
}
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
alert(t('profile.error_network', { message: err.message }));
|
alert(i18n.t('profile.error_network', { message: err.message }));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -178,14 +178,14 @@ const sharedView = {
|
|||||||
<label><input type="checkbox" id="sv-permission-reshare"> <span data-i18n="share.permissionReshare">Reshare</span></label>
|
<label><input type="checkbox" id="sv-permission-reshare"> <span data-i18n="share.permissionReshare">Reshare</span></label>
|
||||||
</div>
|
</div>
|
||||||
<div class="share-password-section">
|
<div class="share-password-section">
|
||||||
<label><input type="checkbox" id="sv-enable-password"> <span data-i18n="share.enablePassword">Password protection</span></label>
|
<label><input type="checkbox" id="sv-enable-password"> <span data-i18n="share.password">Password protection</span></label>
|
||||||
<div class="password-input-group">
|
<div class="password-input-group">
|
||||||
<input type="text" id="sv-share-password" disabled placeholder="Enter password">
|
<input type="text" id="sv-share-password" disabled placeholder="Enter password">
|
||||||
<button id="sv-generate-password" class="button small" data-i18n="share.generatePassword">Generate</button>
|
<button id="sv-generate-password" class="button small" data-i18n="share.generatePassword">Generate</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="share-expiration-section">
|
<div class="share-expiration-section">
|
||||||
<label><input type="checkbox" id="sv-enable-expiration"> <span data-i18n="share.enableExpiration">Set expiration</span></label>
|
<label><input type="checkbox" id="sv-enable-expiration"> <span data-i18n="share.expiration">Set expiration</span></label>
|
||||||
<input type="date" id="sv-share-expiration" disabled>
|
<input type="date" id="sv-share-expiration" disabled>
|
||||||
</div>
|
</div>
|
||||||
<div class="share-actions">
|
<div class="share-actions">
|
||||||
@@ -205,11 +205,11 @@ const sharedView = {
|
|||||||
</div>
|
</div>
|
||||||
<div class="notification-form">
|
<div class="notification-form">
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<label data-i18n="share.notifyEmail">Email:</label>
|
<label data-i18n="share.notifyEmailLabel">Email:</label>
|
||||||
<input type="email" id="sv-notification-email" placeholder="recipient@example.com">
|
<input type="email" id="sv-notification-email" placeholder="recipient@example.com">
|
||||||
</div>
|
</div>
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<label data-i18n="share.notifyMessage">Message (optional):</label>
|
<label data-i18n="share.notifyMessageLabel">Message (optional):</label>
|
||||||
<textarea id="sv-notification-message" rows="3"></textarea>
|
<textarea id="sv-notification-message" rows="3"></textarea>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -220,9 +220,7 @@ const sharedView = {
|
|||||||
</div>
|
</div>
|
||||||
`;
|
`;
|
||||||
|
|
||||||
if (i18n?.translateElement) {
|
i18n.translateElement(container);
|
||||||
i18n.translateElement(container);
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
|
|
||||||
// Attach event listeners
|
// Attach event listeners
|
||||||
@@ -662,8 +660,7 @@ const sharedView = {
|
|||||||
},
|
},
|
||||||
|
|
||||||
translate(key, defaultText) {
|
translate(key, defaultText) {
|
||||||
if (i18n?.t) return i18n.t(key, defaultText);
|
return i18n.t(key, defaultText);
|
||||||
return defaultText;
|
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
+56
-4
@@ -307,7 +307,8 @@
|
|||||||
"generated_link": "الرابط المُنشأ",
|
"generated_link": "الرابط المُنشأ",
|
||||||
"notify": "إرسال إشعار",
|
"notify": "إرسال إشعار",
|
||||||
"recipient": "المستلم",
|
"recipient": "المستلم",
|
||||||
"message": "الرسالة"
|
"message": "الرسالة",
|
||||||
|
"move_to_home": "نقل إلى المجلد الرئيسي"
|
||||||
},
|
},
|
||||||
"dropzone": {
|
"dropzone": {
|
||||||
"drag_files": "اسحب الملفات هنا أو انقر للاختيار",
|
"drag_files": "اسحب الملفات هنا أو انقر للاختيار",
|
||||||
@@ -439,7 +440,9 @@
|
|||||||
"item_deleted_permanently": "تم حذف العنصر نهائياً",
|
"item_deleted_permanently": "تم حذف العنصر نهائياً",
|
||||||
"trash_emptied": "تم تفريغ سلة المهملات بنجاح",
|
"trash_emptied": "تم تفريغ سلة المهملات بنجاح",
|
||||||
"title": "الإشعارات",
|
"title": "الإشعارات",
|
||||||
"empty": "لا توجد إشعارات"
|
"empty": "لا توجد إشعارات",
|
||||||
|
"link_created": "تم إنشاء الرابط",
|
||||||
|
"share_success": "تم إنشاء رابط المشاركة بنجاح"
|
||||||
},
|
},
|
||||||
"batch": {
|
"batch": {
|
||||||
"one_selected": "عنصر واحد محدد",
|
"one_selected": "عنصر واحد محدد",
|
||||||
@@ -566,7 +569,50 @@
|
|||||||
"error_password_short": "كلمة المرور 8 أحرف على الأقل",
|
"error_password_short": "كلمة المرور 8 أحرف على الأقل",
|
||||||
"error_generic": "فشل",
|
"error_generic": "فشل",
|
||||||
"error_network": "خطأ في الشبكة: {{message}}",
|
"error_network": "خطأ في الشبكة: {{message}}",
|
||||||
"error_create_user": "فشل إنشاء المستخدم"
|
"error_create_user": "فشل إنشاء المستخدم",
|
||||||
|
"tab_storage": "التخزين",
|
||||||
|
"storage_title": "إعداد التخزين",
|
||||||
|
"storage_current_backend": "الواجهة الخلفية الحالية",
|
||||||
|
"storage_total_blobs": "إجمالي الكتل",
|
||||||
|
"storage_total_size": "الحجم الإجمالي",
|
||||||
|
"storage_dedup_ratio": "نسبة إزالة التكرار",
|
||||||
|
"storage_backend": "الواجهة الخلفية",
|
||||||
|
"storage_local": "محلي",
|
||||||
|
"storage_s3": "متوافق مع S3",
|
||||||
|
"storage_provider_preset": "إعداد مسبق للمزود",
|
||||||
|
"storage_preset_custom": "مخصص",
|
||||||
|
"storage_endpoint_url": "رابط نقطة النهاية",
|
||||||
|
"storage_endpoint_hint": "اتركه فارغاً لـ AWS S3",
|
||||||
|
"storage_bucket": "الحاوية",
|
||||||
|
"storage_region": "المنطقة",
|
||||||
|
"storage_access_key": "مفتاح الوصول",
|
||||||
|
"storage_secret_key": "المفتاح السري",
|
||||||
|
"storage_secret_configured": "تم إعداد المفتاح",
|
||||||
|
"storage_key_placeholder": "أدخل مفتاحاً جديداً",
|
||||||
|
"storage_path_style": "فرض أسلوب المسار",
|
||||||
|
"storage_path_style_hint": "مطلوب لـ MinIO وبعض الخدمات المتوافقة مع S3",
|
||||||
|
"storage_test_connection": "اختبار الاتصال",
|
||||||
|
"storage_test_success": "نجح الاتصال",
|
||||||
|
"storage_test_failure": "فشل الاتصال",
|
||||||
|
"storage_save": "حفظ الإعداد",
|
||||||
|
"storage_saved": "تم حفظ الإعداد",
|
||||||
|
"storage_migration": "ترحيل البيانات",
|
||||||
|
"storage_migration_coming_soon": "أدوات الترحيل قريباً",
|
||||||
|
"migration_status_label": "حالة الترحيل",
|
||||||
|
"migration_start": "بدء الترحيل",
|
||||||
|
"migration_pause": "إيقاف مؤقت",
|
||||||
|
"migration_resume": "استئناف",
|
||||||
|
"migration_verify": "التحقق",
|
||||||
|
"migration_complete": "إكمال",
|
||||||
|
"migration_started": "بدأ الترحيل",
|
||||||
|
"migration_paused_msg": "الترحيل متوقف مؤقتاً",
|
||||||
|
"migration_resumed_msg": "استُؤنف الترحيل",
|
||||||
|
"migration_completed_msg": "اكتمل الترحيل بنجاح",
|
||||||
|
"migration_verifying": "جارٍ التحقق...",
|
||||||
|
"migration_verify_passed": "اجتاز التحقق",
|
||||||
|
"migration_verify_failed": "فشل التحقق",
|
||||||
|
"migration_failed_blobs": "كتل فاشلة",
|
||||||
|
"testing": "جارٍ الاختبار..."
|
||||||
},
|
},
|
||||||
"profile": {
|
"profile": {
|
||||||
"page_title": "الملف الشخصي",
|
"page_title": "الملف الشخصي",
|
||||||
@@ -627,5 +673,11 @@
|
|||||||
"error_create_pw": "فشل إنشاء كلمة المرور",
|
"error_create_pw": "فشل إنشاء كلمة المرور",
|
||||||
"confirm_revoke": "إلغاء كلمة المرور \"{{label}}\"؟ ستتوقف العملاء عن العمل.",
|
"confirm_revoke": "إلغاء كلمة المرور \"{{label}}\"؟ ستتوقف العملاء عن العمل.",
|
||||||
"error_revoke": "فشل الإلغاء"
|
"error_revoke": "فشل الإلغاء"
|
||||||
}
|
},
|
||||||
|
"upload": {
|
||||||
|
"uploading": "جارٍ الرفع...",
|
||||||
|
"files": "ملفات",
|
||||||
|
"complete": "{{count}} / {{total}} تم الرفع"
|
||||||
|
},
|
||||||
|
"storage_quota_exceeded": "تجاوز حصة التخزين"
|
||||||
}
|
}
|
||||||
|
|||||||
+56
-4
@@ -307,7 +307,8 @@
|
|||||||
"message": "Nachricht",
|
"message": "Nachricht",
|
||||||
"go_to_parent": ".. (parent folder)",
|
"go_to_parent": ".. (parent folder)",
|
||||||
"no_subfolders": "No subfolders",
|
"no_subfolders": "No subfolders",
|
||||||
"select_this_folder": "Select this folder"
|
"select_this_folder": "Select this folder",
|
||||||
|
"move_to_home": "In den Home-Ordner verschieben"
|
||||||
},
|
},
|
||||||
"dropzone": {
|
"dropzone": {
|
||||||
"drag_files": "Dateien hierher ziehen oder klicken zum Auswählen",
|
"drag_files": "Dateien hierher ziehen oder klicken zum Auswählen",
|
||||||
@@ -439,7 +440,9 @@
|
|||||||
"item_deleted_permanently": "Element endgültig gelöscht",
|
"item_deleted_permanently": "Element endgültig gelöscht",
|
||||||
"trash_emptied": "Papierkorb erfolgreich geleert",
|
"trash_emptied": "Papierkorb erfolgreich geleert",
|
||||||
"title": "Benachrichtigungen",
|
"title": "Benachrichtigungen",
|
||||||
"empty": "Keine Benachrichtigungen"
|
"empty": "Keine Benachrichtigungen",
|
||||||
|
"link_created": "Link erstellt",
|
||||||
|
"share_success": "Freigabelink erfolgreich erstellt"
|
||||||
},
|
},
|
||||||
"batch": {
|
"batch": {
|
||||||
"one_selected": "1 Element ausgewählt",
|
"one_selected": "1 Element ausgewählt",
|
||||||
@@ -566,7 +569,50 @@
|
|||||||
"error_password_short": "Passwort muss mindestens 8 Zeichen haben",
|
"error_password_short": "Passwort muss mindestens 8 Zeichen haben",
|
||||||
"error_generic": "Fehlgeschlagen",
|
"error_generic": "Fehlgeschlagen",
|
||||||
"error_network": "Netzwerkfehler: {{message}}",
|
"error_network": "Netzwerkfehler: {{message}}",
|
||||||
"error_create_user": "Benutzer erstellen fehlgeschlagen"
|
"error_create_user": "Benutzer erstellen fehlgeschlagen",
|
||||||
|
"tab_storage": "Speicher",
|
||||||
|
"storage_title": "Speicherkonfiguration",
|
||||||
|
"storage_current_backend": "Aktuelles Backend",
|
||||||
|
"storage_total_blobs": "Gesamt-Blobs",
|
||||||
|
"storage_total_size": "Gesamtgröße",
|
||||||
|
"storage_dedup_ratio": "Deduplizierungsrate",
|
||||||
|
"storage_backend": "Backend",
|
||||||
|
"storage_local": "Lokal",
|
||||||
|
"storage_s3": "S3-kompatibel",
|
||||||
|
"storage_provider_preset": "Anbieter-Voreinstellung",
|
||||||
|
"storage_preset_custom": "Benutzerdefiniert",
|
||||||
|
"storage_endpoint_url": "Endpunkt-URL",
|
||||||
|
"storage_endpoint_hint": "Leer lassen für AWS S3",
|
||||||
|
"storage_bucket": "Bucket",
|
||||||
|
"storage_region": "Region",
|
||||||
|
"storage_access_key": "Zugriffsschlüssel",
|
||||||
|
"storage_secret_key": "Geheimschlüssel",
|
||||||
|
"storage_secret_configured": "Schlüssel konfiguriert",
|
||||||
|
"storage_key_placeholder": "Neuen Schlüssel eingeben",
|
||||||
|
"storage_path_style": "Pfadstil erzwingen",
|
||||||
|
"storage_path_style_hint": "Erforderlich für MinIO und einige S3-kompatible Dienste",
|
||||||
|
"storage_test_connection": "Verbindung testen",
|
||||||
|
"storage_test_success": "Verbindung erfolgreich",
|
||||||
|
"storage_test_failure": "Verbindung fehlgeschlagen",
|
||||||
|
"storage_save": "Konfiguration speichern",
|
||||||
|
"storage_saved": "Konfiguration gespeichert",
|
||||||
|
"storage_migration": "Datenmigration",
|
||||||
|
"storage_migration_coming_soon": "Migrationstools demnächst verfügbar",
|
||||||
|
"migration_status_label": "Migrationsstatus",
|
||||||
|
"migration_start": "Migration starten",
|
||||||
|
"migration_pause": "Pausieren",
|
||||||
|
"migration_resume": "Fortsetzen",
|
||||||
|
"migration_verify": "Verifizieren",
|
||||||
|
"migration_complete": "Abschließen",
|
||||||
|
"migration_started": "Migration gestartet",
|
||||||
|
"migration_paused_msg": "Migration pausiert",
|
||||||
|
"migration_resumed_msg": "Migration fortgesetzt",
|
||||||
|
"migration_completed_msg": "Migration erfolgreich abgeschlossen",
|
||||||
|
"migration_verifying": "Wird verifiziert...",
|
||||||
|
"migration_verify_passed": "Verifizierung erfolgreich",
|
||||||
|
"migration_verify_failed": "Verifizierung fehlgeschlagen",
|
||||||
|
"migration_failed_blobs": "Fehlgeschlagene Blobs",
|
||||||
|
"testing": "Wird getestet..."
|
||||||
},
|
},
|
||||||
"profile": {
|
"profile": {
|
||||||
"page_title": "Profil",
|
"page_title": "Profil",
|
||||||
@@ -627,5 +673,11 @@
|
|||||||
"error_create_pw": "App-Passwort erstellen fehlgeschlagen",
|
"error_create_pw": "App-Passwort erstellen fehlgeschlagen",
|
||||||
"confirm_revoke": "App-Passwort \"{{label}}\" widerrufen? Clients werden nicht mehr funktionieren.",
|
"confirm_revoke": "App-Passwort \"{{label}}\" widerrufen? Clients werden nicht mehr funktionieren.",
|
||||||
"error_revoke": "Widerrufen fehlgeschlagen"
|
"error_revoke": "Widerrufen fehlgeschlagen"
|
||||||
}
|
},
|
||||||
|
"upload": {
|
||||||
|
"uploading": "Wird hochgeladen...",
|
||||||
|
"files": "Dateien",
|
||||||
|
"complete": "{{count}} / {{total}} hochgeladen"
|
||||||
|
},
|
||||||
|
"storage_quota_exceeded": "Speicherplatz erschöpft"
|
||||||
}
|
}
|
||||||
|
|||||||
+12
-3
@@ -307,7 +307,8 @@
|
|||||||
"generated_link": "Generated Link",
|
"generated_link": "Generated Link",
|
||||||
"notify": "Send Notification",
|
"notify": "Send Notification",
|
||||||
"recipient": "Recipient",
|
"recipient": "Recipient",
|
||||||
"message": "Message"
|
"message": "Message",
|
||||||
|
"move_to_home": "Move to Home folder"
|
||||||
},
|
},
|
||||||
"dropzone": {
|
"dropzone": {
|
||||||
"drag_files": "Drag files here or click to select",
|
"drag_files": "Drag files here or click to select",
|
||||||
@@ -439,7 +440,9 @@
|
|||||||
"item_deleted_permanently": "Item permanently deleted",
|
"item_deleted_permanently": "Item permanently deleted",
|
||||||
"trash_emptied": "Trash emptied successfully",
|
"trash_emptied": "Trash emptied successfully",
|
||||||
"title": "Notifications",
|
"title": "Notifications",
|
||||||
"empty": "No notifications"
|
"empty": "No notifications",
|
||||||
|
"link_created": "Link created",
|
||||||
|
"share_success": "Shared link created successfully"
|
||||||
},
|
},
|
||||||
"batch": {
|
"batch": {
|
||||||
"one_selected": "1 item selected",
|
"one_selected": "1 item selected",
|
||||||
@@ -670,5 +673,11 @@
|
|||||||
"error_create_pw": "Failed to create app password",
|
"error_create_pw": "Failed to create app password",
|
||||||
"confirm_revoke": "Revoke app password \"{{label}}\"? Clients using this password will stop working.",
|
"confirm_revoke": "Revoke app password \"{{label}}\"? Clients using this password will stop working.",
|
||||||
"error_revoke": "Failed to revoke app password"
|
"error_revoke": "Failed to revoke app password"
|
||||||
}
|
},
|
||||||
|
"upload": {
|
||||||
|
"uploading": "Uploading...",
|
||||||
|
"files": "files",
|
||||||
|
"complete": "{{count}} / {{total}} uploaded"
|
||||||
|
},
|
||||||
|
"storage_quota_exceeded": "Storage quota exceeded"
|
||||||
}
|
}
|
||||||
|
|||||||
+12
-3
@@ -307,7 +307,8 @@
|
|||||||
"generated_link": "Enlace Generado",
|
"generated_link": "Enlace Generado",
|
||||||
"notify": "Enviar Notificación",
|
"notify": "Enviar Notificación",
|
||||||
"recipient": "Destinatario",
|
"recipient": "Destinatario",
|
||||||
"message": "Mensaje"
|
"message": "Mensaje",
|
||||||
|
"move_to_home": "Mover a la carpeta de inicio"
|
||||||
},
|
},
|
||||||
"dropzone": {
|
"dropzone": {
|
||||||
"drag_files": "Arrastra archivos aquí o haz clic para seleccionar",
|
"drag_files": "Arrastra archivos aquí o haz clic para seleccionar",
|
||||||
@@ -439,7 +440,9 @@
|
|||||||
"item_deleted_permanently": "Elemento eliminado permanentemente",
|
"item_deleted_permanently": "Elemento eliminado permanentemente",
|
||||||
"trash_emptied": "Papelera vaciada correctamente",
|
"trash_emptied": "Papelera vaciada correctamente",
|
||||||
"title": "Notificaciones",
|
"title": "Notificaciones",
|
||||||
"empty": "Sin notificaciones"
|
"empty": "Sin notificaciones",
|
||||||
|
"link_created": "Enlace creado",
|
||||||
|
"share_success": "Enlace compartido creado correctamente"
|
||||||
},
|
},
|
||||||
"batch": {
|
"batch": {
|
||||||
"one_selected": "1 elemento seleccionado",
|
"one_selected": "1 elemento seleccionado",
|
||||||
@@ -670,5 +673,11 @@
|
|||||||
"error_create_pw": "Error al crear contraseña de aplicación",
|
"error_create_pw": "Error al crear contraseña de aplicación",
|
||||||
"confirm_revoke": "¿Revocar contraseña \"{{label}}\"? Los clientes que la usen dejarán de funcionar.",
|
"confirm_revoke": "¿Revocar contraseña \"{{label}}\"? Los clientes que la usen dejarán de funcionar.",
|
||||||
"error_revoke": "Error al revocar contraseña"
|
"error_revoke": "Error al revocar contraseña"
|
||||||
}
|
},
|
||||||
|
"upload": {
|
||||||
|
"uploading": "Subiendo...",
|
||||||
|
"files": "archivos",
|
||||||
|
"complete": "{{count}} / {{total}} subidos"
|
||||||
|
},
|
||||||
|
"storage_quota_exceeded": "Cuota de almacenamiento superada"
|
||||||
}
|
}
|
||||||
|
|||||||
+56
-4
@@ -307,7 +307,8 @@
|
|||||||
"move_folder": "Move folder",
|
"move_folder": "Move folder",
|
||||||
"no_subfolders": "No subfolders",
|
"no_subfolders": "No subfolders",
|
||||||
"rename_file": "Rename file",
|
"rename_file": "Rename file",
|
||||||
"select_this_folder": "Select this folder"
|
"select_this_folder": "Select this folder",
|
||||||
|
"move_to_home": "انتقال به پوشه خانگی"
|
||||||
},
|
},
|
||||||
"dropzone": {
|
"dropzone": {
|
||||||
"drag_files": "پروندهها را اینجا بکشید یا برای انتخاب کلیک کنید",
|
"drag_files": "پروندهها را اینجا بکشید یا برای انتخاب کلیک کنید",
|
||||||
@@ -553,7 +554,50 @@
|
|||||||
"error_password_short": "رمز عبور حداقل ۸ کاراکتر",
|
"error_password_short": "رمز عبور حداقل ۸ کاراکتر",
|
||||||
"error_generic": "خطا",
|
"error_generic": "خطا",
|
||||||
"error_network": "خطای شبکه: {{message}}",
|
"error_network": "خطای شبکه: {{message}}",
|
||||||
"error_create_user": "خطا در ایجاد کاربر"
|
"error_create_user": "خطا در ایجاد کاربر",
|
||||||
|
"tab_storage": "فضای ذخیرهسازی",
|
||||||
|
"storage_title": "تنظیمات فضای ذخیرهسازی",
|
||||||
|
"storage_current_backend": "بکاند فعلی",
|
||||||
|
"storage_total_blobs": "مجموع بلوبها",
|
||||||
|
"storage_total_size": "حجم کل",
|
||||||
|
"storage_dedup_ratio": "نسبت حذف تکراری",
|
||||||
|
"storage_backend": "بکاند",
|
||||||
|
"storage_local": "محلی",
|
||||||
|
"storage_s3": "سازگار با S3",
|
||||||
|
"storage_provider_preset": "پیشتنظیم ارائهدهنده",
|
||||||
|
"storage_preset_custom": "سفارشی",
|
||||||
|
"storage_endpoint_url": "آدرس نقطه پایانی",
|
||||||
|
"storage_endpoint_hint": "برای AWS S3 خالی بگذارید",
|
||||||
|
"storage_bucket": "باکت",
|
||||||
|
"storage_region": "منطقه",
|
||||||
|
"storage_access_key": "کلید دسترسی",
|
||||||
|
"storage_secret_key": "کلید مخفی",
|
||||||
|
"storage_secret_configured": "کلید تنظیم شد",
|
||||||
|
"storage_key_placeholder": "کلید جدید وارد کنید",
|
||||||
|
"storage_path_style": "اجبار سبک مسیر",
|
||||||
|
"storage_path_style_hint": "برای MinIO و برخی سرویسهای سازگار با S3 لازم است",
|
||||||
|
"storage_test_connection": "آزمایش اتصال",
|
||||||
|
"storage_test_success": "اتصال موفق",
|
||||||
|
"storage_test_failure": "اتصال ناموفق",
|
||||||
|
"storage_save": "ذخیره تنظیمات",
|
||||||
|
"storage_saved": "تنظیمات ذخیره شد",
|
||||||
|
"storage_migration": "انتقال داده",
|
||||||
|
"storage_migration_coming_soon": "ابزارهای انتقال به زودی",
|
||||||
|
"migration_status_label": "وضعیت انتقال",
|
||||||
|
"migration_start": "شروع انتقال",
|
||||||
|
"migration_pause": "توقف",
|
||||||
|
"migration_resume": "ادامه",
|
||||||
|
"migration_verify": "تأیید",
|
||||||
|
"migration_complete": "تکمیل",
|
||||||
|
"migration_started": "انتقال شروع شد",
|
||||||
|
"migration_paused_msg": "انتقال متوقف شد",
|
||||||
|
"migration_resumed_msg": "انتقال ادامه یافت",
|
||||||
|
"migration_completed_msg": "انتقال با موفقیت تکمیل شد",
|
||||||
|
"migration_verifying": "در حال تأیید...",
|
||||||
|
"migration_verify_passed": "تأیید موفق",
|
||||||
|
"migration_verify_failed": "تأیید ناموفق",
|
||||||
|
"migration_failed_blobs": "بلوبهای ناموفق",
|
||||||
|
"testing": "در حال آزمایش..."
|
||||||
},
|
},
|
||||||
"profile": {
|
"profile": {
|
||||||
"page_title": "پروفایل",
|
"page_title": "پروفایل",
|
||||||
@@ -626,6 +670,14 @@
|
|||||||
"item_deleted_permanently": "آیتم برای همیشه حذف شد",
|
"item_deleted_permanently": "آیتم برای همیشه حذف شد",
|
||||||
"trash_emptied": "زبالهدان خالی شد",
|
"trash_emptied": "زبالهدان خالی شد",
|
||||||
"title": "اعلانها",
|
"title": "اعلانها",
|
||||||
"empty": "بدون اعلان"
|
"empty": "بدون اعلان",
|
||||||
}
|
"link_created": "پیوند ایجاد شد",
|
||||||
|
"share_success": "پیوند اشتراکگذاری با موفقیت ایجاد شد"
|
||||||
|
},
|
||||||
|
"upload": {
|
||||||
|
"uploading": "در حال آپلود...",
|
||||||
|
"files": "فایلها",
|
||||||
|
"complete": "{{count}} / {{total}} آپلود شد"
|
||||||
|
},
|
||||||
|
"storage_quota_exceeded": "سهمیه فضای ذخیرهسازی تجاوز کرده است"
|
||||||
}
|
}
|
||||||
|
|||||||
+56
-4
@@ -307,7 +307,8 @@
|
|||||||
"message": "Message",
|
"message": "Message",
|
||||||
"go_to_parent": ".. (parent folder)",
|
"go_to_parent": ".. (parent folder)",
|
||||||
"no_subfolders": "No subfolders",
|
"no_subfolders": "No subfolders",
|
||||||
"select_this_folder": "Select this folder"
|
"select_this_folder": "Select this folder",
|
||||||
|
"move_to_home": "Déplacer vers le dossier personnel"
|
||||||
},
|
},
|
||||||
"dropzone": {
|
"dropzone": {
|
||||||
"drag_files": "Glissez des fichiers ici ou cliquez pour sélectionner",
|
"drag_files": "Glissez des fichiers ici ou cliquez pour sélectionner",
|
||||||
@@ -439,7 +440,9 @@
|
|||||||
"item_deleted_permanently": "Élément supprimé définitivement",
|
"item_deleted_permanently": "Élément supprimé définitivement",
|
||||||
"trash_emptied": "Corbeille vidée avec succès",
|
"trash_emptied": "Corbeille vidée avec succès",
|
||||||
"empty": "No notifications",
|
"empty": "No notifications",
|
||||||
"title": "Notifications"
|
"title": "Notifications",
|
||||||
|
"link_created": "Lien créé",
|
||||||
|
"share_success": "Lien de partage créé avec succès"
|
||||||
},
|
},
|
||||||
"batch": {
|
"batch": {
|
||||||
"one_selected": "1 élément sélectionné",
|
"one_selected": "1 élément sélectionné",
|
||||||
@@ -566,7 +569,50 @@
|
|||||||
"error_password_short": "Le mot de passe doit contenir au moins 8 caractères",
|
"error_password_short": "Le mot de passe doit contenir au moins 8 caractères",
|
||||||
"error_generic": "Échec",
|
"error_generic": "Échec",
|
||||||
"error_network": "Erreur réseau : {{message}}",
|
"error_network": "Erreur réseau : {{message}}",
|
||||||
"error_create_user": "Impossible de créer l'utilisateur"
|
"error_create_user": "Impossible de créer l'utilisateur",
|
||||||
|
"tab_storage": "Stockage",
|
||||||
|
"storage_title": "Configuration du stockage",
|
||||||
|
"storage_current_backend": "Backend actuel",
|
||||||
|
"storage_total_blobs": "Total des blobs",
|
||||||
|
"storage_total_size": "Taille totale",
|
||||||
|
"storage_dedup_ratio": "Taux de déduplication",
|
||||||
|
"storage_backend": "Backend",
|
||||||
|
"storage_local": "Local",
|
||||||
|
"storage_s3": "Compatible S3",
|
||||||
|
"storage_provider_preset": "Préréglage du fournisseur",
|
||||||
|
"storage_preset_custom": "Personnalisé",
|
||||||
|
"storage_endpoint_url": "URL du point de terminaison",
|
||||||
|
"storage_endpoint_hint": "Laisser vide pour AWS S3",
|
||||||
|
"storage_bucket": "Bucket",
|
||||||
|
"storage_region": "Région",
|
||||||
|
"storage_access_key": "Clé d'accès",
|
||||||
|
"storage_secret_key": "Clé secrète",
|
||||||
|
"storage_secret_configured": "Clé configurée",
|
||||||
|
"storage_key_placeholder": "Saisir une nouvelle clé",
|
||||||
|
"storage_path_style": "Forcer le style de chemin",
|
||||||
|
"storage_path_style_hint": "Requis pour MinIO et certains services compatibles S3",
|
||||||
|
"storage_test_connection": "Tester la connexion",
|
||||||
|
"storage_test_success": "Connexion réussie",
|
||||||
|
"storage_test_failure": "Échec de la connexion",
|
||||||
|
"storage_save": "Enregistrer la configuration",
|
||||||
|
"storage_saved": "Configuration enregistrée",
|
||||||
|
"storage_migration": "Migration des données",
|
||||||
|
"storage_migration_coming_soon": "Outils de migration bientôt disponibles",
|
||||||
|
"migration_status_label": "État de la migration",
|
||||||
|
"migration_start": "Démarrer la migration",
|
||||||
|
"migration_pause": "Pause",
|
||||||
|
"migration_resume": "Reprendre",
|
||||||
|
"migration_verify": "Vérifier",
|
||||||
|
"migration_complete": "Terminer",
|
||||||
|
"migration_started": "Migration démarrée",
|
||||||
|
"migration_paused_msg": "Migration en pause",
|
||||||
|
"migration_resumed_msg": "Migration reprise",
|
||||||
|
"migration_completed_msg": "Migration terminée avec succès",
|
||||||
|
"migration_verifying": "Vérification en cours...",
|
||||||
|
"migration_verify_passed": "Vérification réussie",
|
||||||
|
"migration_verify_failed": "Échec de la vérification",
|
||||||
|
"migration_failed_blobs": "Blobs échoués",
|
||||||
|
"testing": "Test en cours..."
|
||||||
},
|
},
|
||||||
"profile": {
|
"profile": {
|
||||||
"page_title": "Profil",
|
"page_title": "Profil",
|
||||||
@@ -627,5 +673,11 @@
|
|||||||
"error_create_pw": "Impossible de créer le mot de passe",
|
"error_create_pw": "Impossible de créer le mot de passe",
|
||||||
"confirm_revoke": "Révoquer le mot de passe \"{{label}}\" ? Les clients l'utilisant ne fonctionneront plus.",
|
"confirm_revoke": "Révoquer le mot de passe \"{{label}}\" ? Les clients l'utilisant ne fonctionneront plus.",
|
||||||
"error_revoke": "Échec de la révocation"
|
"error_revoke": "Échec de la révocation"
|
||||||
}
|
},
|
||||||
|
"upload": {
|
||||||
|
"uploading": "Téléchargement en cours...",
|
||||||
|
"files": "fichiers",
|
||||||
|
"complete": "{{count}} / {{total}} téléchargés"
|
||||||
|
},
|
||||||
|
"storage_quota_exceeded": "Quota de stockage dépassé"
|
||||||
}
|
}
|
||||||
|
|||||||
+56
-4
@@ -307,7 +307,8 @@
|
|||||||
"generated_link": "जनरेट किया गया लिंक",
|
"generated_link": "जनरेट किया गया लिंक",
|
||||||
"notify": "सूचना भेजें",
|
"notify": "सूचना भेजें",
|
||||||
"recipient": "प्राप्तकर्ता",
|
"recipient": "प्राप्तकर्ता",
|
||||||
"message": "संदेश"
|
"message": "संदेश",
|
||||||
|
"move_to_home": "होम फ़ोल्डर में ले जाएं"
|
||||||
},
|
},
|
||||||
"dropzone": {
|
"dropzone": {
|
||||||
"drag_files": "फ़ाइलें यहाँ खींचें या चुनने के लिए क्लिक करें",
|
"drag_files": "फ़ाइलें यहाँ खींचें या चुनने के लिए क्लिक करें",
|
||||||
@@ -439,7 +440,9 @@
|
|||||||
"item_deleted_permanently": "आइटम स्थायी रूप से हटाया गया",
|
"item_deleted_permanently": "आइटम स्थायी रूप से हटाया गया",
|
||||||
"trash_emptied": "रद्दी सफलतापूर्वक खाली की गई",
|
"trash_emptied": "रद्दी सफलतापूर्वक खाली की गई",
|
||||||
"title": "सूचनाएँ",
|
"title": "सूचनाएँ",
|
||||||
"empty": "कोई सूचना नहीं"
|
"empty": "कोई सूचना नहीं",
|
||||||
|
"link_created": "लिंक बनाया गया",
|
||||||
|
"share_success": "शेयर लिंक सफलतापूर्वक बनाया गया"
|
||||||
},
|
},
|
||||||
"batch": {
|
"batch": {
|
||||||
"one_selected": "1 आइटम चयनित",
|
"one_selected": "1 आइटम चयनित",
|
||||||
@@ -566,7 +569,50 @@
|
|||||||
"error_password_short": "पासवर्ड कम से कम 8 अक्षर",
|
"error_password_short": "पासवर्ड कम से कम 8 अक्षर",
|
||||||
"error_generic": "विफल",
|
"error_generic": "विफल",
|
||||||
"error_network": "नेटवर्क त्रुटि: {{message}}",
|
"error_network": "नेटवर्क त्रुटि: {{message}}",
|
||||||
"error_create_user": "उपयोगकर्ता बनाने में विफल"
|
"error_create_user": "उपयोगकर्ता बनाने में विफल",
|
||||||
|
"tab_storage": "स्टोरेज",
|
||||||
|
"storage_title": "स्टोरेज कॉन्फ़िगरेशन",
|
||||||
|
"storage_current_backend": "वर्तमान बैकएंड",
|
||||||
|
"storage_total_blobs": "कुल ब्लॉब्स",
|
||||||
|
"storage_total_size": "कुल आकार",
|
||||||
|
"storage_dedup_ratio": "डीडुप्लिकेशन अनुपात",
|
||||||
|
"storage_backend": "बैकएंड",
|
||||||
|
"storage_local": "स्थानीय",
|
||||||
|
"storage_s3": "S3 संगत",
|
||||||
|
"storage_provider_preset": "प्रदाता प्रीसेट",
|
||||||
|
"storage_preset_custom": "कस्टम",
|
||||||
|
"storage_endpoint_url": "एंडपॉइंट URL",
|
||||||
|
"storage_endpoint_hint": "AWS S3 के लिए खाली छोड़ें",
|
||||||
|
"storage_bucket": "बकेट",
|
||||||
|
"storage_region": "क्षेत्र",
|
||||||
|
"storage_access_key": "एक्सेस की",
|
||||||
|
"storage_secret_key": "सीक्रेट की",
|
||||||
|
"storage_secret_configured": "की कॉन्फ़िगर की गई",
|
||||||
|
"storage_key_placeholder": "नई की दर्ज करें",
|
||||||
|
"storage_path_style": "पाथ स्टाइल फ़ोर्स करें",
|
||||||
|
"storage_path_style_hint": "MinIO और कुछ S3-संगत सेवाओं के लिए आवश्यक",
|
||||||
|
"storage_test_connection": "कनेक्शन परीक्षण",
|
||||||
|
"storage_test_success": "कनेक्शन सफल",
|
||||||
|
"storage_test_failure": "कनेक्शन विफल",
|
||||||
|
"storage_save": "कॉन्फ़िगरेशन सहेजें",
|
||||||
|
"storage_saved": "कॉन्फ़िगरेशन सहेजी गई",
|
||||||
|
"storage_migration": "डेटा माइग्रेशन",
|
||||||
|
"storage_migration_coming_soon": "माइग्रेशन टूल्स जल्द आ रहे हैं",
|
||||||
|
"migration_status_label": "माइग्रेशन स्थिति",
|
||||||
|
"migration_start": "माइग्रेशन शुरू करें",
|
||||||
|
"migration_pause": "रोकें",
|
||||||
|
"migration_resume": "फिर से शुरू करें",
|
||||||
|
"migration_verify": "सत्यापित करें",
|
||||||
|
"migration_complete": "पूर्ण करें",
|
||||||
|
"migration_started": "माइग्रेशन शुरू हुआ",
|
||||||
|
"migration_paused_msg": "माइग्रेशन रोका गया",
|
||||||
|
"migration_resumed_msg": "माइग्रेशन फिर से शुरू हुआ",
|
||||||
|
"migration_completed_msg": "माइग्रेशन सफलतापूर्वक पूर्ण हुआ",
|
||||||
|
"migration_verifying": "सत्यापन हो रहा है...",
|
||||||
|
"migration_verify_passed": "सत्यापन पास",
|
||||||
|
"migration_verify_failed": "सत्यापन विफल",
|
||||||
|
"migration_failed_blobs": "विफल ब्लॉब्स",
|
||||||
|
"testing": "परीक्षण हो रहा है..."
|
||||||
},
|
},
|
||||||
"profile": {
|
"profile": {
|
||||||
"page_title": "प्रोफ़ाइल",
|
"page_title": "प्रोफ़ाइल",
|
||||||
@@ -627,5 +673,11 @@
|
|||||||
"error_create_pw": "ऐप पासवर्ड बनाने में विफल",
|
"error_create_pw": "ऐप पासवर्ड बनाने में विफल",
|
||||||
"confirm_revoke": "ऐप पासवर्ड \"{{label}}\" रद्द करें? इसका उपयोग करने वाले क्लाइंट काम करना बंद कर देंगे।",
|
"confirm_revoke": "ऐप पासवर्ड \"{{label}}\" रद्द करें? इसका उपयोग करने वाले क्लाइंट काम करना बंद कर देंगे।",
|
||||||
"error_revoke": "रद्द करने में विफल"
|
"error_revoke": "रद्द करने में विफल"
|
||||||
}
|
},
|
||||||
|
"upload": {
|
||||||
|
"uploading": "अपलोड हो रहा है...",
|
||||||
|
"files": "फ़ाइलें",
|
||||||
|
"complete": "{{count}} / {{total}} अपलोड हुए"
|
||||||
|
},
|
||||||
|
"storage_quota_exceeded": "स्टोरेज कोटा पार हो गया"
|
||||||
}
|
}
|
||||||
|
|||||||
+56
-4
@@ -307,7 +307,8 @@
|
|||||||
"message": "Messaggio",
|
"message": "Messaggio",
|
||||||
"go_to_parent": ".. (parent folder)",
|
"go_to_parent": ".. (parent folder)",
|
||||||
"no_subfolders": "No subfolders",
|
"no_subfolders": "No subfolders",
|
||||||
"select_this_folder": "Select this folder"
|
"select_this_folder": "Select this folder",
|
||||||
|
"move_to_home": "Sposta nella cartella home"
|
||||||
},
|
},
|
||||||
"dropzone": {
|
"dropzone": {
|
||||||
"drag_files": "Trascina i file qui o clicca per selezionare",
|
"drag_files": "Trascina i file qui o clicca per selezionare",
|
||||||
@@ -439,7 +440,9 @@
|
|||||||
"item_deleted_permanently": "Elemento eliminato definitivamente",
|
"item_deleted_permanently": "Elemento eliminato definitivamente",
|
||||||
"trash_emptied": "Cestino svuotato con successo",
|
"trash_emptied": "Cestino svuotato con successo",
|
||||||
"empty": "No notifications",
|
"empty": "No notifications",
|
||||||
"title": "Notifications"
|
"title": "Notifications",
|
||||||
|
"link_created": "Link creato",
|
||||||
|
"share_success": "Link di condivisione creato con successo"
|
||||||
},
|
},
|
||||||
"batch": {
|
"batch": {
|
||||||
"one_selected": "1 elemento selezionato",
|
"one_selected": "1 elemento selezionato",
|
||||||
@@ -566,7 +569,50 @@
|
|||||||
"error_password_short": "La password deve avere almeno 8 caratteri",
|
"error_password_short": "La password deve avere almeno 8 caratteri",
|
||||||
"error_generic": "Fallito",
|
"error_generic": "Fallito",
|
||||||
"error_network": "Errore di rete: {{message}}",
|
"error_network": "Errore di rete: {{message}}",
|
||||||
"error_create_user": "Impossibile creare l'utente"
|
"error_create_user": "Impossibile creare l'utente",
|
||||||
|
"tab_storage": "Archiviazione",
|
||||||
|
"storage_title": "Configurazione archiviazione",
|
||||||
|
"storage_current_backend": "Backend corrente",
|
||||||
|
"storage_total_blobs": "Blob totali",
|
||||||
|
"storage_total_size": "Dimensione totale",
|
||||||
|
"storage_dedup_ratio": "Rapporto deduplicazione",
|
||||||
|
"storage_backend": "Backend",
|
||||||
|
"storage_local": "Locale",
|
||||||
|
"storage_s3": "Compatibile S3",
|
||||||
|
"storage_provider_preset": "Preset fornitore",
|
||||||
|
"storage_preset_custom": "Personalizzato",
|
||||||
|
"storage_endpoint_url": "URL endpoint",
|
||||||
|
"storage_endpoint_hint": "Lasciare vuoto per AWS S3",
|
||||||
|
"storage_bucket": "Bucket",
|
||||||
|
"storage_region": "Regione",
|
||||||
|
"storage_access_key": "Chiave di accesso",
|
||||||
|
"storage_secret_key": "Chiave segreta",
|
||||||
|
"storage_secret_configured": "Chiave configurata",
|
||||||
|
"storage_key_placeholder": "Inserisci nuova chiave",
|
||||||
|
"storage_path_style": "Forza stile percorso",
|
||||||
|
"storage_path_style_hint": "Richiesto per MinIO e alcuni servizi compatibili S3",
|
||||||
|
"storage_test_connection": "Testa connessione",
|
||||||
|
"storage_test_success": "Connessione riuscita",
|
||||||
|
"storage_test_failure": "Connessione fallita",
|
||||||
|
"storage_save": "Salva configurazione",
|
||||||
|
"storage_saved": "Configurazione salvata",
|
||||||
|
"storage_migration": "Migrazione dati",
|
||||||
|
"storage_migration_coming_soon": "Strumenti di migrazione in arrivo",
|
||||||
|
"migration_status_label": "Stato migrazione",
|
||||||
|
"migration_start": "Avvia migrazione",
|
||||||
|
"migration_pause": "Pausa",
|
||||||
|
"migration_resume": "Riprendi",
|
||||||
|
"migration_verify": "Verifica",
|
||||||
|
"migration_complete": "Completa",
|
||||||
|
"migration_started": "Migrazione avviata",
|
||||||
|
"migration_paused_msg": "Migrazione in pausa",
|
||||||
|
"migration_resumed_msg": "Migrazione ripresa",
|
||||||
|
"migration_completed_msg": "Migrazione completata con successo",
|
||||||
|
"migration_verifying": "Verifica in corso...",
|
||||||
|
"migration_verify_passed": "Verifica superata",
|
||||||
|
"migration_verify_failed": "Verifica fallita",
|
||||||
|
"migration_failed_blobs": "Blob falliti",
|
||||||
|
"testing": "Test in corso..."
|
||||||
},
|
},
|
||||||
"profile": {
|
"profile": {
|
||||||
"page_title": "Profilo",
|
"page_title": "Profilo",
|
||||||
@@ -627,5 +673,11 @@
|
|||||||
"error_create_pw": "Impossibile creare la password",
|
"error_create_pw": "Impossibile creare la password",
|
||||||
"confirm_revoke": "Revocare la password \"{{label}}\"? I client che la usano smetteranno di funzionare.",
|
"confirm_revoke": "Revocare la password \"{{label}}\"? I client che la usano smetteranno di funzionare.",
|
||||||
"error_revoke": "Revoca fallita"
|
"error_revoke": "Revoca fallita"
|
||||||
}
|
},
|
||||||
|
"upload": {
|
||||||
|
"uploading": "Caricamento in corso...",
|
||||||
|
"files": "file",
|
||||||
|
"complete": "{{count}} / {{total}} caricati"
|
||||||
|
},
|
||||||
|
"storage_quota_exceeded": "Quota di archiviazione superata"
|
||||||
}
|
}
|
||||||
|
|||||||
+56
-4
@@ -307,7 +307,8 @@
|
|||||||
"generated_link": "生成されたリンク",
|
"generated_link": "生成されたリンク",
|
||||||
"notify": "通知を送信",
|
"notify": "通知を送信",
|
||||||
"recipient": "宛先",
|
"recipient": "宛先",
|
||||||
"message": "メッセージ"
|
"message": "メッセージ",
|
||||||
|
"move_to_home": "ホームフォルダへ移動"
|
||||||
},
|
},
|
||||||
"dropzone": {
|
"dropzone": {
|
||||||
"drag_files": "ファイルをここにドラッグするか、クリックして選択",
|
"drag_files": "ファイルをここにドラッグするか、クリックして選択",
|
||||||
@@ -439,7 +440,9 @@
|
|||||||
"item_deleted_permanently": "アイテムを完全に削除しました",
|
"item_deleted_permanently": "アイテムを完全に削除しました",
|
||||||
"trash_emptied": "ゴミ箱を正常に空にしました",
|
"trash_emptied": "ゴミ箱を正常に空にしました",
|
||||||
"title": "通知",
|
"title": "通知",
|
||||||
"empty": "通知はありません"
|
"empty": "通知はありません",
|
||||||
|
"link_created": "リンクを作成しました",
|
||||||
|
"share_success": "共有リンクを正常に作成しました"
|
||||||
},
|
},
|
||||||
"batch": {
|
"batch": {
|
||||||
"one_selected": "1件選択中",
|
"one_selected": "1件選択中",
|
||||||
@@ -566,7 +569,50 @@
|
|||||||
"error_password_short": "パスワードは8文字以上",
|
"error_password_short": "パスワードは8文字以上",
|
||||||
"error_generic": "失敗",
|
"error_generic": "失敗",
|
||||||
"error_network": "ネットワークエラー: {{message}}",
|
"error_network": "ネットワークエラー: {{message}}",
|
||||||
"error_create_user": "ユーザー作成失敗"
|
"error_create_user": "ユーザー作成失敗",
|
||||||
|
"tab_storage": "ストレージ",
|
||||||
|
"storage_title": "ストレージ設定",
|
||||||
|
"storage_current_backend": "現在のバックエンド",
|
||||||
|
"storage_total_blobs": "総ブロブ数",
|
||||||
|
"storage_total_size": "合計サイズ",
|
||||||
|
"storage_dedup_ratio": "重複排除率",
|
||||||
|
"storage_backend": "バックエンド",
|
||||||
|
"storage_local": "ローカル",
|
||||||
|
"storage_s3": "S3互換",
|
||||||
|
"storage_provider_preset": "プロバイダープリセット",
|
||||||
|
"storage_preset_custom": "カスタム",
|
||||||
|
"storage_endpoint_url": "エンドポイントURL",
|
||||||
|
"storage_endpoint_hint": "AWS S3の場合は空欄のまま",
|
||||||
|
"storage_bucket": "バケット",
|
||||||
|
"storage_region": "リージョン",
|
||||||
|
"storage_access_key": "アクセスキー",
|
||||||
|
"storage_secret_key": "シークレットキー",
|
||||||
|
"storage_secret_configured": "キーが設定済み",
|
||||||
|
"storage_key_placeholder": "新しいキーを入力",
|
||||||
|
"storage_path_style": "パススタイルを強制",
|
||||||
|
"storage_path_style_hint": "MinIOおよび一部のS3互換サービスに必要",
|
||||||
|
"storage_test_connection": "接続テスト",
|
||||||
|
"storage_test_success": "接続成功",
|
||||||
|
"storage_test_failure": "接続失敗",
|
||||||
|
"storage_save": "設定を保存",
|
||||||
|
"storage_saved": "設定を保存しました",
|
||||||
|
"storage_migration": "データ移行",
|
||||||
|
"storage_migration_coming_soon": "移行ツールは近日公開予定",
|
||||||
|
"migration_status_label": "移行状況",
|
||||||
|
"migration_start": "移行を開始",
|
||||||
|
"migration_pause": "一時停止",
|
||||||
|
"migration_resume": "再開",
|
||||||
|
"migration_verify": "検証",
|
||||||
|
"migration_complete": "完了",
|
||||||
|
"migration_started": "移行を開始しました",
|
||||||
|
"migration_paused_msg": "移行を一時停止しました",
|
||||||
|
"migration_resumed_msg": "移行を再開しました",
|
||||||
|
"migration_completed_msg": "移行が正常に完了しました",
|
||||||
|
"migration_verifying": "検証中...",
|
||||||
|
"migration_verify_passed": "検証に合格",
|
||||||
|
"migration_verify_failed": "検証に失敗",
|
||||||
|
"migration_failed_blobs": "失敗したブロブ",
|
||||||
|
"testing": "テスト中..."
|
||||||
},
|
},
|
||||||
"profile": {
|
"profile": {
|
||||||
"page_title": "プロフィール",
|
"page_title": "プロフィール",
|
||||||
@@ -627,5 +673,11 @@
|
|||||||
"error_create_pw": "アプリパスワードの作成に失敗しました",
|
"error_create_pw": "アプリパスワードの作成に失敗しました",
|
||||||
"confirm_revoke": "アプリパスワード「{{label}}」を失効させますか?使用中のクライアントは動作しなくなります。",
|
"confirm_revoke": "アプリパスワード「{{label}}」を失効させますか?使用中のクライアントは動作しなくなります。",
|
||||||
"error_revoke": "失効に失敗しました"
|
"error_revoke": "失効に失敗しました"
|
||||||
}
|
},
|
||||||
|
"upload": {
|
||||||
|
"uploading": "アップロード中...",
|
||||||
|
"files": "ファイル",
|
||||||
|
"complete": "{{count}} / {{total}} アップロード済み"
|
||||||
|
},
|
||||||
|
"storage_quota_exceeded": "ストレージ容量を超過しました"
|
||||||
}
|
}
|
||||||
|
|||||||
+56
-4
@@ -307,7 +307,8 @@
|
|||||||
"generated_link": "생성된 링크",
|
"generated_link": "생성된 링크",
|
||||||
"notify": "알림 보내기",
|
"notify": "알림 보내기",
|
||||||
"recipient": "수신자",
|
"recipient": "수신자",
|
||||||
"message": "메시지"
|
"message": "메시지",
|
||||||
|
"move_to_home": "홈 폴더로 이동"
|
||||||
},
|
},
|
||||||
"dropzone": {
|
"dropzone": {
|
||||||
"drag_files": "여기에 파일을 드래그하거나 클릭하여 선택하세요",
|
"drag_files": "여기에 파일을 드래그하거나 클릭하여 선택하세요",
|
||||||
@@ -439,7 +440,9 @@
|
|||||||
"item_deleted_permanently": "항목이 영구적으로 삭제되었습니다",
|
"item_deleted_permanently": "항목이 영구적으로 삭제되었습니다",
|
||||||
"trash_emptied": "휴지통이 성공적으로 비워졌습니다",
|
"trash_emptied": "휴지통이 성공적으로 비워졌습니다",
|
||||||
"title": "알림",
|
"title": "알림",
|
||||||
"empty": "알림이 없습니다"
|
"empty": "알림이 없습니다",
|
||||||
|
"link_created": "링크 생성됨",
|
||||||
|
"share_success": "공유 링크가 성공적으로 생성되었습니다"
|
||||||
},
|
},
|
||||||
"batch": {
|
"batch": {
|
||||||
"one_selected": "1개 선택됨",
|
"one_selected": "1개 선택됨",
|
||||||
@@ -566,7 +569,50 @@
|
|||||||
"error_password_short": "비밀번호 최소 8자",
|
"error_password_short": "비밀번호 최소 8자",
|
||||||
"error_generic": "실패",
|
"error_generic": "실패",
|
||||||
"error_network": "네트워크 오류: {{message}}",
|
"error_network": "네트워크 오류: {{message}}",
|
||||||
"error_create_user": "사용자 생성 실패"
|
"error_create_user": "사용자 생성 실패",
|
||||||
|
"tab_storage": "저장소",
|
||||||
|
"storage_title": "저장소 구성",
|
||||||
|
"storage_current_backend": "현재 백엔드",
|
||||||
|
"storage_total_blobs": "총 블롭 수",
|
||||||
|
"storage_total_size": "총 크기",
|
||||||
|
"storage_dedup_ratio": "중복 제거 비율",
|
||||||
|
"storage_backend": "백엔드",
|
||||||
|
"storage_local": "로컬",
|
||||||
|
"storage_s3": "S3 호환",
|
||||||
|
"storage_provider_preset": "공급자 프리셋",
|
||||||
|
"storage_preset_custom": "사용자 지정",
|
||||||
|
"storage_endpoint_url": "엔드포인트 URL",
|
||||||
|
"storage_endpoint_hint": "AWS S3의 경우 비워두세요",
|
||||||
|
"storage_bucket": "버킷",
|
||||||
|
"storage_region": "지역",
|
||||||
|
"storage_access_key": "액세스 키",
|
||||||
|
"storage_secret_key": "시크릿 키",
|
||||||
|
"storage_secret_configured": "키 구성됨",
|
||||||
|
"storage_key_placeholder": "새 키 입력",
|
||||||
|
"storage_path_style": "경로 스타일 강제",
|
||||||
|
"storage_path_style_hint": "MinIO 및 일부 S3 호환 서비스에 필요",
|
||||||
|
"storage_test_connection": "연결 테스트",
|
||||||
|
"storage_test_success": "연결 성공",
|
||||||
|
"storage_test_failure": "연결 실패",
|
||||||
|
"storage_save": "구성 저장",
|
||||||
|
"storage_saved": "구성이 저장되었습니다",
|
||||||
|
"storage_migration": "데이터 마이그레이션",
|
||||||
|
"storage_migration_coming_soon": "마이그레이션 도구 곧 출시",
|
||||||
|
"migration_status_label": "마이그레이션 상태",
|
||||||
|
"migration_start": "마이그레이션 시작",
|
||||||
|
"migration_pause": "일시 중지",
|
||||||
|
"migration_resume": "재개",
|
||||||
|
"migration_verify": "확인",
|
||||||
|
"migration_complete": "완료",
|
||||||
|
"migration_started": "마이그레이션 시작됨",
|
||||||
|
"migration_paused_msg": "마이그레이션 일시 중지됨",
|
||||||
|
"migration_resumed_msg": "마이그레이션 재개됨",
|
||||||
|
"migration_completed_msg": "마이그레이션이 성공적으로 완료되었습니다",
|
||||||
|
"migration_verifying": "확인 중...",
|
||||||
|
"migration_verify_passed": "확인 통과",
|
||||||
|
"migration_verify_failed": "확인 실패",
|
||||||
|
"migration_failed_blobs": "실패한 블롭",
|
||||||
|
"testing": "테스트 중..."
|
||||||
},
|
},
|
||||||
"profile": {
|
"profile": {
|
||||||
"page_title": "프로필",
|
"page_title": "프로필",
|
||||||
@@ -627,5 +673,11 @@
|
|||||||
"error_create_pw": "앱 비밀번호 생성 실패",
|
"error_create_pw": "앱 비밀번호 생성 실패",
|
||||||
"confirm_revoke": "앱 비밀번호 \"{{label}}\"을(를) 취소하시겠습니까? 이 비밀번호를 사용하는 클라이언트가 작동하지 않게 됩니다.",
|
"confirm_revoke": "앱 비밀번호 \"{{label}}\"을(를) 취소하시겠습니까? 이 비밀번호를 사용하는 클라이언트가 작동하지 않게 됩니다.",
|
||||||
"error_revoke": "취소 실패"
|
"error_revoke": "취소 실패"
|
||||||
}
|
},
|
||||||
|
"upload": {
|
||||||
|
"uploading": "업로드 중...",
|
||||||
|
"files": "파일",
|
||||||
|
"complete": "{{count}} / {{total}} 업로드됨"
|
||||||
|
},
|
||||||
|
"storage_quota_exceeded": "저장 공간 할당량 초과"
|
||||||
}
|
}
|
||||||
|
|||||||
+56
-4
@@ -307,7 +307,8 @@
|
|||||||
"message": "Bericht",
|
"message": "Bericht",
|
||||||
"go_to_parent": ".. (parent folder)",
|
"go_to_parent": ".. (parent folder)",
|
||||||
"no_subfolders": "No subfolders",
|
"no_subfolders": "No subfolders",
|
||||||
"select_this_folder": "Select this folder"
|
"select_this_folder": "Select this folder",
|
||||||
|
"move_to_home": "Verplaatsen naar de thuismap"
|
||||||
},
|
},
|
||||||
"dropzone": {
|
"dropzone": {
|
||||||
"drag_files": "Sleep bestanden hierheen of klik om te selecteren",
|
"drag_files": "Sleep bestanden hierheen of klik om te selecteren",
|
||||||
@@ -439,7 +440,9 @@
|
|||||||
"item_deleted_permanently": "Item permanent verwijderd",
|
"item_deleted_permanently": "Item permanent verwijderd",
|
||||||
"trash_emptied": "Prullenbak succesvol geleegd",
|
"trash_emptied": "Prullenbak succesvol geleegd",
|
||||||
"title": "Notificaties",
|
"title": "Notificaties",
|
||||||
"empty": "Geen notificaties"
|
"empty": "Geen notificaties",
|
||||||
|
"link_created": "Link aangemaakt",
|
||||||
|
"share_success": "Deellink succesvol aangemaakt"
|
||||||
},
|
},
|
||||||
"batch": {
|
"batch": {
|
||||||
"one_selected": "1 item geselecteerd",
|
"one_selected": "1 item geselecteerd",
|
||||||
@@ -566,7 +569,50 @@
|
|||||||
"error_password_short": "Wachtwoord moet minimaal 8 tekens bevatten",
|
"error_password_short": "Wachtwoord moet minimaal 8 tekens bevatten",
|
||||||
"error_generic": "Mislukt",
|
"error_generic": "Mislukt",
|
||||||
"error_network": "Netwerkfout: {{message}}",
|
"error_network": "Netwerkfout: {{message}}",
|
||||||
"error_create_user": "Kan gebruiker niet aanmaken"
|
"error_create_user": "Kan gebruiker niet aanmaken",
|
||||||
|
"tab_storage": "Opslag",
|
||||||
|
"storage_title": "Opslagconfiguratie",
|
||||||
|
"storage_current_backend": "Huidig backend",
|
||||||
|
"storage_total_blobs": "Totaal blobs",
|
||||||
|
"storage_total_size": "Totale grootte",
|
||||||
|
"storage_dedup_ratio": "Deduplicatieverhouding",
|
||||||
|
"storage_backend": "Backend",
|
||||||
|
"storage_local": "Lokaal",
|
||||||
|
"storage_s3": "S3-compatibel",
|
||||||
|
"storage_provider_preset": "Providerinstelling",
|
||||||
|
"storage_preset_custom": "Aangepast",
|
||||||
|
"storage_endpoint_url": "Eindpunt-URL",
|
||||||
|
"storage_endpoint_hint": "Leeg laten voor AWS S3",
|
||||||
|
"storage_bucket": "Bucket",
|
||||||
|
"storage_region": "Regio",
|
||||||
|
"storage_access_key": "Toegangssleutel",
|
||||||
|
"storage_secret_key": "Geheime sleutel",
|
||||||
|
"storage_secret_configured": "Sleutel geconfigureerd",
|
||||||
|
"storage_key_placeholder": "Nieuwe sleutel invoeren",
|
||||||
|
"storage_path_style": "Padstijl forceren",
|
||||||
|
"storage_path_style_hint": "Vereist voor MinIO en sommige S3-compatibele diensten",
|
||||||
|
"storage_test_connection": "Verbinding testen",
|
||||||
|
"storage_test_success": "Verbinding geslaagd",
|
||||||
|
"storage_test_failure": "Verbinding mislukt",
|
||||||
|
"storage_save": "Configuratie opslaan",
|
||||||
|
"storage_saved": "Configuratie opgeslagen",
|
||||||
|
"storage_migration": "Gegevensmigratie",
|
||||||
|
"storage_migration_coming_soon": "Migratietools binnenkort beschikbaar",
|
||||||
|
"migration_status_label": "Migratiestatus",
|
||||||
|
"migration_start": "Migratie starten",
|
||||||
|
"migration_pause": "Pauzeren",
|
||||||
|
"migration_resume": "Hervatten",
|
||||||
|
"migration_verify": "Verifiëren",
|
||||||
|
"migration_complete": "Voltooien",
|
||||||
|
"migration_started": "Migratie gestart",
|
||||||
|
"migration_paused_msg": "Migratie gepauzeerd",
|
||||||
|
"migration_resumed_msg": "Migratie hervat",
|
||||||
|
"migration_completed_msg": "Migratie succesvol voltooid",
|
||||||
|
"migration_verifying": "Bezig met verifiëren...",
|
||||||
|
"migration_verify_passed": "Verificatie geslaagd",
|
||||||
|
"migration_verify_failed": "Verificatie mislukt",
|
||||||
|
"migration_failed_blobs": "Mislukte blobs",
|
||||||
|
"testing": "Bezig met testen..."
|
||||||
},
|
},
|
||||||
"profile": {
|
"profile": {
|
||||||
"page_title": "Profiel",
|
"page_title": "Profiel",
|
||||||
@@ -627,5 +673,11 @@
|
|||||||
"error_create_pw": "App-wachtwoord aanmaken mislukt",
|
"error_create_pw": "App-wachtwoord aanmaken mislukt",
|
||||||
"confirm_revoke": "App-wachtwoord \"{{label}}\" intrekken? Clients die dit wachtwoord gebruiken zullen stoppen.",
|
"confirm_revoke": "App-wachtwoord \"{{label}}\" intrekken? Clients die dit wachtwoord gebruiken zullen stoppen.",
|
||||||
"error_revoke": "Intrekken mislukt"
|
"error_revoke": "Intrekken mislukt"
|
||||||
}
|
},
|
||||||
|
"upload": {
|
||||||
|
"uploading": "Bezig met uploaden...",
|
||||||
|
"files": "bestanden",
|
||||||
|
"complete": "{{count}} / {{total}} geüpload"
|
||||||
|
},
|
||||||
|
"storage_quota_exceeded": "Opslagquotum overschreden"
|
||||||
}
|
}
|
||||||
|
|||||||
+56
-4
@@ -307,7 +307,8 @@
|
|||||||
"message": "Mensagem",
|
"message": "Mensagem",
|
||||||
"go_to_parent": ".. (parent folder)",
|
"go_to_parent": ".. (parent folder)",
|
||||||
"no_subfolders": "No subfolders",
|
"no_subfolders": "No subfolders",
|
||||||
"select_this_folder": "Select this folder"
|
"select_this_folder": "Select this folder",
|
||||||
|
"move_to_home": "Mover para a pasta inicial"
|
||||||
},
|
},
|
||||||
"dropzone": {
|
"dropzone": {
|
||||||
"drag_files": "Arraste arquivos aqui ou clique para selecionar",
|
"drag_files": "Arraste arquivos aqui ou clique para selecionar",
|
||||||
@@ -439,7 +440,9 @@
|
|||||||
"item_deleted_permanently": "Item excluído permanentemente",
|
"item_deleted_permanently": "Item excluído permanentemente",
|
||||||
"trash_emptied": "Lixeira esvaziada com sucesso",
|
"trash_emptied": "Lixeira esvaziada com sucesso",
|
||||||
"empty": "No notifications",
|
"empty": "No notifications",
|
||||||
"title": "Notifications"
|
"title": "Notifications",
|
||||||
|
"link_created": "Link criado",
|
||||||
|
"share_success": "Link de partilha criado com sucesso"
|
||||||
},
|
},
|
||||||
"batch": {
|
"batch": {
|
||||||
"one_selected": "1 item selecionado",
|
"one_selected": "1 item selecionado",
|
||||||
@@ -566,7 +569,50 @@
|
|||||||
"error_password_short": "A senha deve ter pelo menos 8 caracteres",
|
"error_password_short": "A senha deve ter pelo menos 8 caracteres",
|
||||||
"error_generic": "Falha",
|
"error_generic": "Falha",
|
||||||
"error_network": "Erro de rede: {{message}}",
|
"error_network": "Erro de rede: {{message}}",
|
||||||
"error_create_user": "Falha ao criar usuário"
|
"error_create_user": "Falha ao criar usuário",
|
||||||
|
"tab_storage": "Armazenamento",
|
||||||
|
"storage_title": "Configuração de armazenamento",
|
||||||
|
"storage_current_backend": "Backend atual",
|
||||||
|
"storage_total_blobs": "Total de blobs",
|
||||||
|
"storage_total_size": "Tamanho total",
|
||||||
|
"storage_dedup_ratio": "Taxa de deduplicação",
|
||||||
|
"storage_backend": "Backend",
|
||||||
|
"storage_local": "Local",
|
||||||
|
"storage_s3": "Compatível com S3",
|
||||||
|
"storage_provider_preset": "Predefinição do fornecedor",
|
||||||
|
"storage_preset_custom": "Personalizado",
|
||||||
|
"storage_endpoint_url": "URL do endpoint",
|
||||||
|
"storage_endpoint_hint": "Deixar em branco para AWS S3",
|
||||||
|
"storage_bucket": "Bucket",
|
||||||
|
"storage_region": "Região",
|
||||||
|
"storage_access_key": "Chave de acesso",
|
||||||
|
"storage_secret_key": "Chave secreta",
|
||||||
|
"storage_secret_configured": "Chave configurada",
|
||||||
|
"storage_key_placeholder": "Introduzir nova chave",
|
||||||
|
"storage_path_style": "Forçar estilo de caminho",
|
||||||
|
"storage_path_style_hint": "Necessário para MinIO e alguns serviços compatíveis com S3",
|
||||||
|
"storage_test_connection": "Testar ligação",
|
||||||
|
"storage_test_success": "Ligação bem-sucedida",
|
||||||
|
"storage_test_failure": "Falha na ligação",
|
||||||
|
"storage_save": "Guardar configuração",
|
||||||
|
"storage_saved": "Configuração guardada",
|
||||||
|
"storage_migration": "Migração de dados",
|
||||||
|
"storage_migration_coming_soon": "Ferramentas de migração em breve",
|
||||||
|
"migration_status_label": "Estado da migração",
|
||||||
|
"migration_start": "Iniciar migração",
|
||||||
|
"migration_pause": "Pausar",
|
||||||
|
"migration_resume": "Retomar",
|
||||||
|
"migration_verify": "Verificar",
|
||||||
|
"migration_complete": "Concluir",
|
||||||
|
"migration_started": "Migração iniciada",
|
||||||
|
"migration_paused_msg": "Migração pausada",
|
||||||
|
"migration_resumed_msg": "Migração retomada",
|
||||||
|
"migration_completed_msg": "Migração concluída com sucesso",
|
||||||
|
"migration_verifying": "A verificar...",
|
||||||
|
"migration_verify_passed": "Verificação aprovada",
|
||||||
|
"migration_verify_failed": "Verificação falhou",
|
||||||
|
"migration_failed_blobs": "Blobs com falha",
|
||||||
|
"testing": "A testar..."
|
||||||
},
|
},
|
||||||
"profile": {
|
"profile": {
|
||||||
"page_title": "Perfil",
|
"page_title": "Perfil",
|
||||||
@@ -627,5 +673,11 @@
|
|||||||
"error_create_pw": "Falha ao criar senha de aplicativo",
|
"error_create_pw": "Falha ao criar senha de aplicativo",
|
||||||
"confirm_revoke": "Revogar senha \"{{label}}\"? Clientes que usam esta senha deixarão de funcionar.",
|
"confirm_revoke": "Revogar senha \"{{label}}\"? Clientes que usam esta senha deixarão de funcionar.",
|
||||||
"error_revoke": "Falha ao revogar"
|
"error_revoke": "Falha ao revogar"
|
||||||
}
|
},
|
||||||
|
"upload": {
|
||||||
|
"uploading": "A carregar...",
|
||||||
|
"files": "ficheiros",
|
||||||
|
"complete": "{{count}} / {{total}} carregados"
|
||||||
|
},
|
||||||
|
"storage_quota_exceeded": "Cota de armazenamento excedida"
|
||||||
}
|
}
|
||||||
|
|||||||
+56
-4
@@ -307,7 +307,8 @@
|
|||||||
"generated_link": "Сгенерированная ссылка",
|
"generated_link": "Сгенерированная ссылка",
|
||||||
"notify": "Отправить уведомление",
|
"notify": "Отправить уведомление",
|
||||||
"recipient": "Получатель",
|
"recipient": "Получатель",
|
||||||
"message": "Сообщение"
|
"message": "Сообщение",
|
||||||
|
"move_to_home": "Переместить в домашнюю папку"
|
||||||
},
|
},
|
||||||
"dropzone": {
|
"dropzone": {
|
||||||
"drag_files": "Перетащите файлы сюда или нажмите для выбора",
|
"drag_files": "Перетащите файлы сюда или нажмите для выбора",
|
||||||
@@ -439,7 +440,9 @@
|
|||||||
"item_deleted_permanently": "Элемент удалён навсегда",
|
"item_deleted_permanently": "Элемент удалён навсегда",
|
||||||
"trash_emptied": "Корзина успешно очищена",
|
"trash_emptied": "Корзина успешно очищена",
|
||||||
"title": "Уведомления",
|
"title": "Уведомления",
|
||||||
"empty": "Нет уведомлений"
|
"empty": "Нет уведомлений",
|
||||||
|
"link_created": "Ссылка создана",
|
||||||
|
"share_success": "Ссылка для общего доступа успешно создана"
|
||||||
},
|
},
|
||||||
"batch": {
|
"batch": {
|
||||||
"one_selected": "Выбран 1 элемент",
|
"one_selected": "Выбран 1 элемент",
|
||||||
@@ -566,7 +569,50 @@
|
|||||||
"error_password_short": "Пароль минимум 8 символов",
|
"error_password_short": "Пароль минимум 8 символов",
|
||||||
"error_generic": "Ошибка",
|
"error_generic": "Ошибка",
|
||||||
"error_network": "Ошибка сети: {{message}}",
|
"error_network": "Ошибка сети: {{message}}",
|
||||||
"error_create_user": "Не удалось создать"
|
"error_create_user": "Не удалось создать",
|
||||||
|
"tab_storage": "Хранилище",
|
||||||
|
"storage_title": "Настройка хранилища",
|
||||||
|
"storage_current_backend": "Текущий бэкенд",
|
||||||
|
"storage_total_blobs": "Всего блобов",
|
||||||
|
"storage_total_size": "Общий размер",
|
||||||
|
"storage_dedup_ratio": "Коэффициент дедупликации",
|
||||||
|
"storage_backend": "Бэкенд",
|
||||||
|
"storage_local": "Локальный",
|
||||||
|
"storage_s3": "Совместимый с S3",
|
||||||
|
"storage_provider_preset": "Пресет провайдера",
|
||||||
|
"storage_preset_custom": "Пользовательский",
|
||||||
|
"storage_endpoint_url": "URL конечной точки",
|
||||||
|
"storage_endpoint_hint": "Оставьте пустым для AWS S3",
|
||||||
|
"storage_bucket": "Бакет",
|
||||||
|
"storage_region": "Регион",
|
||||||
|
"storage_access_key": "Ключ доступа",
|
||||||
|
"storage_secret_key": "Секретный ключ",
|
||||||
|
"storage_secret_configured": "Ключ настроен",
|
||||||
|
"storage_key_placeholder": "Введите новый ключ",
|
||||||
|
"storage_path_style": "Принудительный стиль пути",
|
||||||
|
"storage_path_style_hint": "Требуется для MinIO и некоторых S3-совместимых сервисов",
|
||||||
|
"storage_test_connection": "Проверить соединение",
|
||||||
|
"storage_test_success": "Соединение успешно",
|
||||||
|
"storage_test_failure": "Ошибка соединения",
|
||||||
|
"storage_save": "Сохранить конфигурацию",
|
||||||
|
"storage_saved": "Конфигурация сохранена",
|
||||||
|
"storage_migration": "Миграция данных",
|
||||||
|
"storage_migration_coming_soon": "Инструменты миграции скоро появятся",
|
||||||
|
"migration_status_label": "Статус миграции",
|
||||||
|
"migration_start": "Начать миграцию",
|
||||||
|
"migration_pause": "Пауза",
|
||||||
|
"migration_resume": "Возобновить",
|
||||||
|
"migration_verify": "Проверить",
|
||||||
|
"migration_complete": "Завершить",
|
||||||
|
"migration_started": "Миграция начата",
|
||||||
|
"migration_paused_msg": "Миграция приостановлена",
|
||||||
|
"migration_resumed_msg": "Миграция возобновлена",
|
||||||
|
"migration_completed_msg": "Миграция успешно завершена",
|
||||||
|
"migration_verifying": "Проверка...",
|
||||||
|
"migration_verify_passed": "Проверка пройдена",
|
||||||
|
"migration_verify_failed": "Проверка не пройдена",
|
||||||
|
"migration_failed_blobs": "Неудачные блобы",
|
||||||
|
"testing": "Тестирование..."
|
||||||
},
|
},
|
||||||
"profile": {
|
"profile": {
|
||||||
"page_title": "Профиль",
|
"page_title": "Профиль",
|
||||||
@@ -627,5 +673,11 @@
|
|||||||
"error_create_pw": "Не удалось создать пароль",
|
"error_create_pw": "Не удалось создать пароль",
|
||||||
"confirm_revoke": "Отозвать пароль «{{label}}»? Клиенты перестанут работать.",
|
"confirm_revoke": "Отозвать пароль «{{label}}»? Клиенты перестанут работать.",
|
||||||
"error_revoke": "Не удалось отозвать"
|
"error_revoke": "Не удалось отозвать"
|
||||||
}
|
},
|
||||||
|
"upload": {
|
||||||
|
"uploading": "Загрузка...",
|
||||||
|
"files": "файлов",
|
||||||
|
"complete": "{{count}} / {{total}} загружено"
|
||||||
|
},
|
||||||
|
"storage_quota_exceeded": "Превышена квота хранилища"
|
||||||
}
|
}
|
||||||
|
|||||||
+56
-4
@@ -307,7 +307,8 @@
|
|||||||
"move_folder": "Move folder",
|
"move_folder": "Move folder",
|
||||||
"no_subfolders": "No subfolders",
|
"no_subfolders": "No subfolders",
|
||||||
"rename_file": "Rename file",
|
"rename_file": "Rename file",
|
||||||
"select_this_folder": "Select this folder"
|
"select_this_folder": "Select this folder",
|
||||||
|
"move_to_home": "移动到主文件夹"
|
||||||
},
|
},
|
||||||
"dropzone": {
|
"dropzone": {
|
||||||
"drag_files": "将文件拖到这里,或点击选择",
|
"drag_files": "将文件拖到这里,或点击选择",
|
||||||
@@ -553,7 +554,50 @@
|
|||||||
"error_password_short": "密码至少需要8个字符",
|
"error_password_short": "密码至少需要8个字符",
|
||||||
"error_generic": "失败",
|
"error_generic": "失败",
|
||||||
"error_network": "网络错误:{{message}}",
|
"error_network": "网络错误:{{message}}",
|
||||||
"error_create_user": "创建用户失败"
|
"error_create_user": "创建用户失败",
|
||||||
|
"tab_storage": "存储",
|
||||||
|
"storage_title": "存储配置",
|
||||||
|
"storage_current_backend": "当前后端",
|
||||||
|
"storage_total_blobs": "总块数",
|
||||||
|
"storage_total_size": "总大小",
|
||||||
|
"storage_dedup_ratio": "去重比率",
|
||||||
|
"storage_backend": "后端",
|
||||||
|
"storage_local": "本地",
|
||||||
|
"storage_s3": "S3 兼容",
|
||||||
|
"storage_provider_preset": "提供商预设",
|
||||||
|
"storage_preset_custom": "自定义",
|
||||||
|
"storage_endpoint_url": "端点 URL",
|
||||||
|
"storage_endpoint_hint": "AWS S3 请留空",
|
||||||
|
"storage_bucket": "存储桶",
|
||||||
|
"storage_region": "地区",
|
||||||
|
"storage_access_key": "访问密钥",
|
||||||
|
"storage_secret_key": "密钥",
|
||||||
|
"storage_secret_configured": "密钥已配置",
|
||||||
|
"storage_key_placeholder": "输入新密钥",
|
||||||
|
"storage_path_style": "强制路径风格",
|
||||||
|
"storage_path_style_hint": "MinIO 及某些 S3 兼容服务需要此选项",
|
||||||
|
"storage_test_connection": "测试连接",
|
||||||
|
"storage_test_success": "连接成功",
|
||||||
|
"storage_test_failure": "连接失败",
|
||||||
|
"storage_save": "保存配置",
|
||||||
|
"storage_saved": "配置已保存",
|
||||||
|
"storage_migration": "数据迁移",
|
||||||
|
"storage_migration_coming_soon": "迁移工具即将推出",
|
||||||
|
"migration_status_label": "迁移状态",
|
||||||
|
"migration_start": "开始迁移",
|
||||||
|
"migration_pause": "暂停",
|
||||||
|
"migration_resume": "继续",
|
||||||
|
"migration_verify": "验证",
|
||||||
|
"migration_complete": "完成",
|
||||||
|
"migration_started": "迁移已开始",
|
||||||
|
"migration_paused_msg": "迁移已暂停",
|
||||||
|
"migration_resumed_msg": "迁移已继续",
|
||||||
|
"migration_completed_msg": "迁移成功完成",
|
||||||
|
"migration_verifying": "正在验证...",
|
||||||
|
"migration_verify_passed": "验证通过",
|
||||||
|
"migration_verify_failed": "验证失败",
|
||||||
|
"migration_failed_blobs": "失败的块",
|
||||||
|
"testing": "正在测试..."
|
||||||
},
|
},
|
||||||
"profile": {
|
"profile": {
|
||||||
"page_title": "个人资料",
|
"page_title": "个人资料",
|
||||||
@@ -626,6 +670,14 @@
|
|||||||
"item_deleted_permanently": "项目已永久删除",
|
"item_deleted_permanently": "项目已永久删除",
|
||||||
"trash_emptied": "回收站已清空",
|
"trash_emptied": "回收站已清空",
|
||||||
"title": "通知",
|
"title": "通知",
|
||||||
"empty": "暂无通知"
|
"empty": "暂无通知",
|
||||||
}
|
"link_created": "链接已创建",
|
||||||
|
"share_success": "分享链接创建成功"
|
||||||
|
},
|
||||||
|
"upload": {
|
||||||
|
"uploading": "正在上传...",
|
||||||
|
"files": "个文件",
|
||||||
|
"complete": "已上传 {{count}} / {{total}}"
|
||||||
|
},
|
||||||
|
"storage_quota_exceeded": "存储配额已超限"
|
||||||
}
|
}
|
||||||
|
|||||||
+1
-1
@@ -1,6 +1,6 @@
|
|||||||
// OxiCloud Service Worker
|
// OxiCloud Service Worker
|
||||||
// FIXME: generate cache name according build ?
|
// FIXME: generate cache name according build ?
|
||||||
const CACHE_NAME = 'oxicloud-cache-v19';
|
const CACHE_NAME = 'oxicloud-cache-v20';
|
||||||
|
|
||||||
// Only cache static assets — NOT HTML files.
|
// Only cache static assets — NOT HTML files.
|
||||||
// HTML files are served network-first so browsers always get the latest
|
// HTML files are served network-first so browsers always get the latest
|
||||||
|
|||||||
Reference in New Issue
Block a user