mirror of
https://github.com/denizsafak/abogen.git
synced 2026-07-18 13:40:27 +02:00
feat: Implement folder browsing modal for Audiobookshelf integration and enhance folder selection UI
This commit is contained in:
+227
-40
@@ -17,6 +17,17 @@ const outputAreas = {
|
|||||||
|
|
||||||
const normalizationAudio = document.querySelector('[data-role="normalization-preview-audio"]');
|
const normalizationAudio = document.querySelector('[data-role="normalization-preview-audio"]');
|
||||||
|
|
||||||
|
const folderModal = document.querySelector('[data-role="audiobookshelf-folder-modal"]');
|
||||||
|
const folderModalOverlay = folderModal ? folderModal.querySelector('[data-role="audiobookshelf-folder-overlay"]') : null;
|
||||||
|
const folderList = folderModal ? folderModal.querySelector('[data-role="audiobookshelf-folder-list"]') : null;
|
||||||
|
const folderStatusMessage = folderModal ? folderModal.querySelector('[data-role="audiobookshelf-folder-status"]') : null;
|
||||||
|
const folderFilter = folderModal ? folderModal.querySelector('[data-role="audiobookshelf-folder-filter"]') : null;
|
||||||
|
const folderEmptyState = folderModal ? folderModal.querySelector('[data-role="audiobookshelf-folder-empty"]') : null;
|
||||||
|
const defaultFolderEmptyMessage = folderEmptyState ? folderEmptyState.textContent : 'No folders match your filter.';
|
||||||
|
let folderModalOpener = null;
|
||||||
|
let folderModalPreviousFocus = null;
|
||||||
|
let audiobookshelfFolderSource = [];
|
||||||
|
|
||||||
function setStatus(target, message, state) {
|
function setStatus(target, message, state) {
|
||||||
if (!target) {
|
if (!target) {
|
||||||
return;
|
return;
|
||||||
@@ -93,6 +104,197 @@ function parseNumber(value, fallback) {
|
|||||||
return Number.isFinite(parsed) ? parsed : fallback;
|
return Number.isFinite(parsed) ? parsed : fallback;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function normalizeFolderToken(value) {
|
||||||
|
return String(value || '').trim().toLowerCase();
|
||||||
|
}
|
||||||
|
|
||||||
|
function setFolderModalStatus(message, state) {
|
||||||
|
if (!folderStatusMessage) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
folderStatusMessage.textContent = message || '';
|
||||||
|
if (state) {
|
||||||
|
folderStatusMessage.dataset.state = state;
|
||||||
|
folderStatusMessage.hidden = false;
|
||||||
|
} else {
|
||||||
|
delete folderStatusMessage.dataset.state;
|
||||||
|
folderStatusMessage.hidden = !message;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function clearFolderModalContents() {
|
||||||
|
if (folderList) {
|
||||||
|
folderList.innerHTML = '';
|
||||||
|
}
|
||||||
|
if (folderEmptyState) {
|
||||||
|
folderEmptyState.textContent = defaultFolderEmptyMessage;
|
||||||
|
folderEmptyState.hidden = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function openFolderModal(opener) {
|
||||||
|
if (!folderModal) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
folderModalOpener = opener || null;
|
||||||
|
folderModalPreviousFocus = document.activeElement instanceof HTMLElement ? document.activeElement : null;
|
||||||
|
folderModal.hidden = false;
|
||||||
|
folderModal.dataset.open = 'true';
|
||||||
|
document.body.classList.add('modal-open');
|
||||||
|
if (folderFilter) {
|
||||||
|
folderFilter.value = '';
|
||||||
|
folderFilter.disabled = true;
|
||||||
|
}
|
||||||
|
clearFolderModalContents();
|
||||||
|
setFolderModalStatus('Loading folders...', 'loading');
|
||||||
|
}
|
||||||
|
|
||||||
|
function closeFolderModal(event) {
|
||||||
|
if (event) {
|
||||||
|
event.preventDefault();
|
||||||
|
event.stopPropagation();
|
||||||
|
}
|
||||||
|
if (!folderModal || folderModal.hidden) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
folderModal.dataset.open = 'false';
|
||||||
|
folderModal.hidden = true;
|
||||||
|
document.body.classList.remove('modal-open');
|
||||||
|
audiobookshelfFolderSource = [];
|
||||||
|
if (folderFilter) {
|
||||||
|
folderFilter.value = '';
|
||||||
|
folderFilter.disabled = false;
|
||||||
|
}
|
||||||
|
clearFolderModalContents();
|
||||||
|
setFolderModalStatus('', null);
|
||||||
|
const focusTarget = folderModalPreviousFocus && typeof folderModalPreviousFocus.focus === 'function'
|
||||||
|
? folderModalPreviousFocus
|
||||||
|
: folderModalOpener;
|
||||||
|
if (focusTarget && typeof focusTarget.focus === 'function') {
|
||||||
|
focusTarget.focus({ preventScroll: false });
|
||||||
|
}
|
||||||
|
folderModalPreviousFocus = null;
|
||||||
|
folderModalOpener = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
function renderFolderList(query) {
|
||||||
|
if (!folderList) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
folderList.innerHTML = '';
|
||||||
|
const normalizedQuery = normalizeFolderToken(query);
|
||||||
|
const matches = audiobookshelfFolderSource.filter((entry) => {
|
||||||
|
const tokens = [
|
||||||
|
normalizeFolderToken(entry.name),
|
||||||
|
normalizeFolderToken(entry.path),
|
||||||
|
normalizeFolderToken(entry.id),
|
||||||
|
];
|
||||||
|
return !normalizedQuery || tokens.some((token) => token.includes(normalizedQuery));
|
||||||
|
});
|
||||||
|
if (!matches.length) {
|
||||||
|
if (folderEmptyState) {
|
||||||
|
folderEmptyState.textContent = normalizedQuery ? defaultFolderEmptyMessage : 'No folders found for this library.';
|
||||||
|
folderEmptyState.hidden = false;
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (folderEmptyState) {
|
||||||
|
folderEmptyState.textContent = defaultFolderEmptyMessage;
|
||||||
|
folderEmptyState.hidden = true;
|
||||||
|
}
|
||||||
|
matches.forEach((entry) => {
|
||||||
|
const button = document.createElement('button');
|
||||||
|
button.type = 'button';
|
||||||
|
button.className = 'folder-picker__item';
|
||||||
|
button.setAttribute('role', 'option');
|
||||||
|
if (entry.id) {
|
||||||
|
button.dataset.folderId = entry.id;
|
||||||
|
}
|
||||||
|
const displayName = entry.name || entry.path || entry.id || 'Unnamed folder';
|
||||||
|
const nameEl = document.createElement('span');
|
||||||
|
nameEl.className = 'folder-picker__item-name';
|
||||||
|
nameEl.textContent = displayName;
|
||||||
|
button.appendChild(nameEl);
|
||||||
|
if (entry.path && (!entry.name || entry.path.toLowerCase() !== entry.name.toLowerCase())) {
|
||||||
|
const pathEl = document.createElement('span');
|
||||||
|
pathEl.className = 'folder-picker__item-path';
|
||||||
|
pathEl.textContent = entry.path;
|
||||||
|
button.appendChild(pathEl);
|
||||||
|
}
|
||||||
|
if (entry.id) {
|
||||||
|
const idEl = document.createElement('span');
|
||||||
|
idEl.className = 'folder-picker__item-id';
|
||||||
|
idEl.textContent = entry.id;
|
||||||
|
button.appendChild(idEl);
|
||||||
|
}
|
||||||
|
button.addEventListener('click', () => handleFolderSelection(entry));
|
||||||
|
folderList.appendChild(button);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function populateFolderPicker(entries) {
|
||||||
|
audiobookshelfFolderSource = Array.isArray(entries) ? entries : [];
|
||||||
|
if (!audiobookshelfFolderSource.length) {
|
||||||
|
if (folderFilter) {
|
||||||
|
folderFilter.value = '';
|
||||||
|
folderFilter.disabled = true;
|
||||||
|
}
|
||||||
|
setFolderModalStatus('No folders found for this library.', 'info');
|
||||||
|
if (folderEmptyState) {
|
||||||
|
folderEmptyState.textContent = 'No folders found for this library.';
|
||||||
|
folderEmptyState.hidden = false;
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (folderFilter) {
|
||||||
|
folderFilter.disabled = false;
|
||||||
|
folderFilter.value = '';
|
||||||
|
folderFilter.focus({ preventScroll: true });
|
||||||
|
}
|
||||||
|
setFolderModalStatus('', null);
|
||||||
|
if (folderEmptyState) {
|
||||||
|
folderEmptyState.textContent = defaultFolderEmptyMessage;
|
||||||
|
folderEmptyState.hidden = true;
|
||||||
|
}
|
||||||
|
renderFolderList('');
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleFolderSelection(entry) {
|
||||||
|
const folderInput = form ? form.querySelector('#audiobookshelf_folder_id') : null;
|
||||||
|
if (folderInput) {
|
||||||
|
folderInput.value = entry.id || '';
|
||||||
|
folderInput.dispatchEvent(new Event('input', { bubbles: true }));
|
||||||
|
}
|
||||||
|
closeFolderModal();
|
||||||
|
const status = statusSelectors.audiobookshelf;
|
||||||
|
if (status) {
|
||||||
|
const label = entry.name || entry.path || entry.id || 'selected folder';
|
||||||
|
setStatus(status, `Selected folder '${label}'.`, 'success');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function initFolderPicker() {
|
||||||
|
if (!folderModal) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const closeButtons = folderModal.querySelectorAll('[data-action="audiobookshelf-folder-close"]');
|
||||||
|
closeButtons.forEach((button) => {
|
||||||
|
button.addEventListener('click', closeFolderModal);
|
||||||
|
});
|
||||||
|
if (folderModalOverlay) {
|
||||||
|
folderModalOverlay.addEventListener('click', closeFolderModal);
|
||||||
|
}
|
||||||
|
if (folderFilter) {
|
||||||
|
folderFilter.addEventListener('input', () => renderFolderList(folderFilter.value));
|
||||||
|
}
|
||||||
|
folderModal.addEventListener('keydown', (event) => {
|
||||||
|
if (event.key === 'Escape') {
|
||||||
|
event.preventDefault();
|
||||||
|
closeFolderModal();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
function collectLLMFields() {
|
function collectLLMFields() {
|
||||||
const baseUrl = form.querySelector('#llm_base_url');
|
const baseUrl = form.querySelector('#llm_base_url');
|
||||||
const apiKey = form.querySelector('#llm_api_key');
|
const apiKey = form.querySelector('#llm_api_key');
|
||||||
@@ -406,7 +608,7 @@ async function testAudiobookshelf(button) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function listAudiobookshelfFolders(button) {
|
async function browseAudiobookshelfFolders(button) {
|
||||||
const status = statusSelectors.audiobookshelf;
|
const status = statusSelectors.audiobookshelf;
|
||||||
const fields = collectAudiobookshelfFields();
|
const fields = collectAudiobookshelfFields();
|
||||||
if (!status) {
|
if (!status) {
|
||||||
@@ -418,7 +620,11 @@ async function listAudiobookshelfFolders(button) {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
clearStatus(status);
|
clearStatus(status);
|
||||||
setStatus(status, 'Loading folders…');
|
openFolderModal(button);
|
||||||
|
if (!folderModal) {
|
||||||
|
setStatus(status, 'Folder picker is unavailable in this view.', 'error');
|
||||||
|
return;
|
||||||
|
}
|
||||||
button.disabled = true;
|
button.disabled = true;
|
||||||
try {
|
try {
|
||||||
const response = await fetch('/api/integrations/audiobookshelf/folders', {
|
const response = await fetch('/api/integrations/audiobookshelf/folders', {
|
||||||
@@ -431,49 +637,29 @@ async function listAudiobookshelfFolders(button) {
|
|||||||
throw new Error(payload.error || 'Folder lookup failed.');
|
throw new Error(payload.error || 'Folder lookup failed.');
|
||||||
}
|
}
|
||||||
const folders = Array.isArray(payload.folders) ? payload.folders : [];
|
const folders = Array.isArray(payload.folders) ? payload.folders : [];
|
||||||
|
const modalActive = folderModal && !folderModal.hidden;
|
||||||
if (!folders.length) {
|
if (!folders.length) {
|
||||||
setStatus(status, payload.message || 'No folders found for this library.', 'info');
|
const message = payload.message || 'No folders found for this library.';
|
||||||
|
setStatus(status, message, 'info');
|
||||||
|
if (modalActive) {
|
||||||
|
clearFolderModalContents();
|
||||||
|
setFolderModalStatus(message, 'info');
|
||||||
|
}
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const list = folders
|
if (!modalActive) {
|
||||||
.map((entry, index) => {
|
setStatus(status, 'Folders loaded.', 'info');
|
||||||
const id = String(entry.id || '').trim();
|
|
||||||
const name = String(entry.name || '').trim();
|
|
||||||
const path = String(entry.path || '').trim();
|
|
||||||
const descriptorParts = [];
|
|
||||||
if (name) {
|
|
||||||
descriptorParts.push(name);
|
|
||||||
}
|
|
||||||
if (path && (!name || path.toLowerCase() !== name.toLowerCase())) {
|
|
||||||
descriptorParts.push(path);
|
|
||||||
}
|
|
||||||
if (!descriptorParts.length && id) {
|
|
||||||
descriptorParts.push(id);
|
|
||||||
}
|
|
||||||
const descriptor = descriptorParts.join(' — ') || '(unnamed folder)';
|
|
||||||
return `${index + 1}. ${descriptor}`;
|
|
||||||
})
|
|
||||||
.join('\n');
|
|
||||||
const choice = window.prompt(`Select a folder by number:\n\n${list}`, '1');
|
|
||||||
if (choice === null) {
|
|
||||||
setStatus(status, 'Folder selection cancelled.', 'info');
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const selection = Number.parseInt(choice, 10);
|
populateFolderPicker(folders);
|
||||||
if (!Number.isFinite(selection) || selection < 1 || selection > folders.length) {
|
setStatus(status, 'Choose a folder below.', 'info');
|
||||||
setStatus(status, 'Enter a number from the list to select a folder.', 'error');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
const selected = folders[selection - 1];
|
|
||||||
const folderInput = form.querySelector('#audiobookshelf_folder_id');
|
|
||||||
if (folderInput) {
|
|
||||||
folderInput.value = selected.id || '';
|
|
||||||
folderInput.dispatchEvent(new Event('input', { bubbles: true }));
|
|
||||||
}
|
|
||||||
const label = String(selected.name || selected.path || selected.id || '').trim() || 'selected folder';
|
|
||||||
setStatus(status, `Selected folder '${label}'.`, 'success');
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
setStatus(status, error instanceof Error ? error.message : 'Folder lookup failed.', 'error');
|
const message = error instanceof Error ? error.message : 'Folder lookup failed.';
|
||||||
|
setStatus(status, message, 'error');
|
||||||
|
if (folderModal && !folderModal.hidden) {
|
||||||
|
clearFolderModalContents();
|
||||||
|
setFolderModalStatus(message, 'error');
|
||||||
|
}
|
||||||
} finally {
|
} finally {
|
||||||
button.disabled = false;
|
button.disabled = false;
|
||||||
}
|
}
|
||||||
@@ -590,7 +776,7 @@ function initActions() {
|
|||||||
}
|
}
|
||||||
const audiobookshelfBrowseButton = document.querySelector('[data-action="audiobookshelf-list-folders"]');
|
const audiobookshelfBrowseButton = document.querySelector('[data-action="audiobookshelf-list-folders"]');
|
||||||
if (audiobookshelfBrowseButton) {
|
if (audiobookshelfBrowseButton) {
|
||||||
audiobookshelfBrowseButton.addEventListener('click', () => listAudiobookshelfFolders(audiobookshelfBrowseButton));
|
audiobookshelfBrowseButton.addEventListener('click', () => browseAudiobookshelfFolders(audiobookshelfBrowseButton));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -610,5 +796,6 @@ if (form) {
|
|||||||
initNavigation();
|
initNavigation();
|
||||||
initSampleSelector();
|
initSampleSelector();
|
||||||
initActions();
|
initActions();
|
||||||
|
initFolderPicker();
|
||||||
initLLMStateWatchers();
|
initLLMStateWatchers();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -335,6 +335,100 @@ body {
|
|||||||
border-radius: 28px;
|
border-radius: 28px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.modal__content.folder-picker-modal {
|
||||||
|
width: min(600px, 95vw);
|
||||||
|
}
|
||||||
|
|
||||||
|
.folder-picker {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.folder-picker__controls {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.folder-picker__controls input {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.folder-picker__status {
|
||||||
|
margin: 0;
|
||||||
|
font-size: 0.9rem;
|
||||||
|
color: var(--muted);
|
||||||
|
}
|
||||||
|
|
||||||
|
.folder-picker__status[data-state="loading"] {
|
||||||
|
color: var(--muted);
|
||||||
|
}
|
||||||
|
|
||||||
|
.folder-picker__status[data-state="error"] {
|
||||||
|
color: var(--danger);
|
||||||
|
}
|
||||||
|
|
||||||
|
.folder-picker__status[data-state="info"],
|
||||||
|
.folder-picker__status[data-state="success"] {
|
||||||
|
color: var(--accent);
|
||||||
|
}
|
||||||
|
|
||||||
|
.folder-picker__list {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 0.65rem;
|
||||||
|
max-height: 20rem;
|
||||||
|
overflow-y: auto;
|
||||||
|
padding-right: 0.25rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.folder-picker__item {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: flex-start;
|
||||||
|
gap: 0.25rem;
|
||||||
|
width: 100%;
|
||||||
|
border: 1px solid rgba(148, 163, 184, 0.22);
|
||||||
|
border-radius: 16px;
|
||||||
|
background: rgba(15, 23, 42, 0.6);
|
||||||
|
padding: 0.8rem 1rem;
|
||||||
|
color: var(--text);
|
||||||
|
cursor: pointer;
|
||||||
|
transition: border-color 0.2s ease, box-shadow 0.2s ease, transform 0.2s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.folder-picker__item:hover,
|
||||||
|
.folder-picker__item:focus-visible {
|
||||||
|
border-color: rgba(56, 189, 248, 0.5);
|
||||||
|
box-shadow: 0 0 0 4px rgba(56, 189, 248, 0.12);
|
||||||
|
outline: none;
|
||||||
|
transform: translateY(-1px);
|
||||||
|
}
|
||||||
|
|
||||||
|
.folder-picker__item-name {
|
||||||
|
font-weight: 600;
|
||||||
|
font-size: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.folder-picker__item-path {
|
||||||
|
font-size: 0.85rem;
|
||||||
|
color: var(--muted);
|
||||||
|
}
|
||||||
|
|
||||||
|
.folder-picker__item-id {
|
||||||
|
font-size: 0.75rem;
|
||||||
|
color: var(--muted);
|
||||||
|
background: rgba(148, 163, 184, 0.18);
|
||||||
|
padding: 0.15rem 0.4rem;
|
||||||
|
border-radius: 999px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.folder-picker__empty {
|
||||||
|
margin: 0;
|
||||||
|
font-size: 0.9rem;
|
||||||
|
color: var(--muted);
|
||||||
|
}
|
||||||
|
|
||||||
.reader-modal {
|
.reader-modal {
|
||||||
width: min(960px, 95vw);
|
width: min(960px, 95vw);
|
||||||
height: min(90vh, 720px);
|
height: min(90vh, 720px);
|
||||||
@@ -1995,6 +2089,18 @@ button.step-indicator__item:focus-visible {
|
|||||||
gap: 0.35rem;
|
gap: 0.35rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.field__input-with-button {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 0.65rem;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.field__input-with-button > input {
|
||||||
|
flex: 1 1 16rem;
|
||||||
|
min-width: 12rem;
|
||||||
|
}
|
||||||
|
|
||||||
.preview-card {
|
.preview-card {
|
||||||
border: 1px solid rgba(148, 163, 184, 0.18);
|
border: 1px solid rgba(148, 163, 184, 0.18);
|
||||||
border-radius: 16px;
|
border-radius: 16px;
|
||||||
|
|||||||
@@ -446,9 +446,11 @@
|
|||||||
</div>
|
</div>
|
||||||
<div class="field__group">
|
<div class="field__group">
|
||||||
<label for="audiobookshelf_folder_id">Folder (name or ID)</label>
|
<label for="audiobookshelf_folder_id">Folder (name or ID)</label>
|
||||||
|
<div class="field__input-with-button">
|
||||||
<input type="text" id="audiobookshelf_folder_id" name="audiobookshelf_folder_id" value="{{ integrations.audiobookshelf.folder_id }}">
|
<input type="text" id="audiobookshelf_folder_id" name="audiobookshelf_folder_id" value="{{ integrations.audiobookshelf.folder_id }}">
|
||||||
<p class="hint">Enter the folder exactly as it appears in Audiobookshelf or paste the folder ID. You can also browse the available folders below.</p>
|
|
||||||
<button type="button" class="button button--ghost button--small" data-action="audiobookshelf-list-folders">Browse folders</button>
|
<button type="button" class="button button--ghost button--small" data-action="audiobookshelf-list-folders">Browse folders</button>
|
||||||
|
</div>
|
||||||
|
<p class="hint">Enter the folder exactly as it appears in Audiobookshelf, paste the folder ID, or browse the available folders.</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="field field--inline">
|
<div class="field field--inline">
|
||||||
@@ -492,6 +494,30 @@
|
|||||||
</section>
|
</section>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div class="modal" data-role="audiobookshelf-folder-modal" hidden>
|
||||||
|
<div class="modal__overlay" data-role="audiobookshelf-folder-overlay" tabindex="-1"></div>
|
||||||
|
<div class="modal__content card card--modal folder-picker-modal" role="dialog" aria-modal="true" aria-labelledby="audiobookshelf-folder-picker-title">
|
||||||
|
<header class="modal__header">
|
||||||
|
<p class="modal__eyebrow">Audiobookshelf</p>
|
||||||
|
<h2 class="modal__title" id="audiobookshelf-folder-picker-title">Select folder</h2>
|
||||||
|
<button type="button" class="button button--ghost button--small" data-action="audiobookshelf-folder-close" aria-label="Close folder picker">Close</button>
|
||||||
|
</header>
|
||||||
|
<div class="modal__body">
|
||||||
|
<div class="folder-picker" data-role="audiobookshelf-folder-picker">
|
||||||
|
<div class="folder-picker__controls">
|
||||||
|
<input type="search" data-role="audiobookshelf-folder-filter" placeholder="Filter folders" autocomplete="off" spellcheck="false">
|
||||||
|
</div>
|
||||||
|
<p class="folder-picker__status" data-role="audiobookshelf-folder-status"></p>
|
||||||
|
<div class="folder-picker__list" data-role="audiobookshelf-folder-list" role="listbox"></div>
|
||||||
|
<p class="folder-picker__empty" data-role="audiobookshelf-folder-empty" hidden>No folders match your filter.</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<footer class="modal__footer">
|
||||||
|
<button type="button" class="button button--ghost" data-action="audiobookshelf-folder-close">Cancel</button>
|
||||||
|
</footer>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div class="settings__actions">
|
<div class="settings__actions">
|
||||||
<button type="submit" class="button">Save Settings</button>
|
<button type="submit" class="button">Save Settings</button>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
Reference in New Issue
Block a user