play files inside statistics
This commit is contained in:
parent
53b9fec7bd
commit
6a27d47222
@ -1251,3 +1251,8 @@ footer .audio-player-container {
|
|||||||
background-color: rgba(246, 195, 68, 0.25);
|
background-color: rgba(246, 195, 68, 0.25);
|
||||||
border-radius: 4px;
|
border-radius: 4px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.file-access-actions {
|
||||||
|
flex-wrap: nowrap;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|||||||
@ -639,10 +639,21 @@ function initAppPage() {
|
|||||||
|
|
||||||
// Load initial directory based on URL.
|
// Load initial directory based on URL.
|
||||||
let initialSubpath = '';
|
let initialSubpath = '';
|
||||||
|
const pendingFolderOpen = getPendingFolderOpen();
|
||||||
if (window.location.pathname.indexOf('/path/') === 0) {
|
if (window.location.pathname.indexOf('/path/') === 0) {
|
||||||
initialSubpath = window.location.pathname.substring(6); // remove "/path/"
|
initialSubpath = window.location.pathname.substring(6); // remove "/path/"
|
||||||
}
|
}
|
||||||
loadDirectory(initialSubpath);
|
if (pendingFolderOpen?.folder !== undefined) {
|
||||||
|
initialSubpath = pendingFolderOpen.folder || '';
|
||||||
|
}
|
||||||
|
loadDirectory(initialSubpath).then(() => {
|
||||||
|
if (pendingFolderOpen?.file) {
|
||||||
|
highlightPendingFile(pendingFolderOpen.file);
|
||||||
|
}
|
||||||
|
if (pendingFolderOpen) {
|
||||||
|
clearPendingFolderOpen();
|
||||||
|
}
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@ -731,6 +742,32 @@ function syncThemeColor() {
|
|||||||
|
|
||||||
// syncThemeColor is called during app init.
|
// syncThemeColor is called during app init.
|
||||||
|
|
||||||
|
function getPendingFolderOpen() {
|
||||||
|
try {
|
||||||
|
const raw = sessionStorage.getItem('pendingFolderOpen');
|
||||||
|
return raw ? JSON.parse(raw) : null;
|
||||||
|
} catch {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function clearPendingFolderOpen() {
|
||||||
|
try {
|
||||||
|
sessionStorage.removeItem('pendingFolderOpen');
|
||||||
|
} catch {
|
||||||
|
// ignore
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function highlightPendingFile(filePath) {
|
||||||
|
if (!filePath) return;
|
||||||
|
const target = document.querySelector(`.play-file[data-url=\"${filePath}\"]`);
|
||||||
|
if (target) {
|
||||||
|
target.classList.add('search-highlight');
|
||||||
|
target.scrollIntoView({ behavior: 'smooth', block: 'center' });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if (window.PageRegistry && typeof window.PageRegistry.register === 'function') {
|
if (window.PageRegistry && typeof window.PageRegistry.register === 'function') {
|
||||||
window.PageRegistry.register('app', { init: initAppPage });
|
window.PageRegistry.register('app', { init: initAppPage });
|
||||||
} else {
|
} else {
|
||||||
|
|||||||
98
static/file_access.js
Normal file
98
static/file_access.js
Normal file
@ -0,0 +1,98 @@
|
|||||||
|
(() => {
|
||||||
|
const audioExts = ['.mp3', '.wav', '.ogg', '.m4a', '.flac'];
|
||||||
|
|
||||||
|
const safeDecode = (value) => {
|
||||||
|
try {
|
||||||
|
return decodeURIComponent(value);
|
||||||
|
} catch {
|
||||||
|
return value;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const normalizePath = (path) => {
|
||||||
|
if (!path) return '';
|
||||||
|
return path
|
||||||
|
.replace(/\\/g, '/')
|
||||||
|
.replace(/^\/+/, '');
|
||||||
|
};
|
||||||
|
|
||||||
|
const encodePath = (path) => {
|
||||||
|
const normalized = normalizePath(path);
|
||||||
|
if (!normalized) return '';
|
||||||
|
return normalized
|
||||||
|
.split('/')
|
||||||
|
.map(segment => encodeURIComponent(safeDecode(segment)))
|
||||||
|
.join('/');
|
||||||
|
};
|
||||||
|
|
||||||
|
const isAudio = (path) =>
|
||||||
|
audioExts.some(ext => path.toLowerCase().endsWith(ext));
|
||||||
|
|
||||||
|
const highlightFile = (filePath) => {
|
||||||
|
const normalized = normalizePath(filePath);
|
||||||
|
const target = document.querySelector(`.play-file[data-url="${normalized}"]`);
|
||||||
|
if (target) {
|
||||||
|
target.classList.add('search-highlight');
|
||||||
|
target.scrollIntoView({ behavior: 'smooth', block: 'center' });
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const openFolder = (folderPath, filePath) => {
|
||||||
|
const normalizedFolder = normalizePath(folderPath);
|
||||||
|
const normalizedFile = normalizePath(filePath);
|
||||||
|
const payload = { folder: normalizedFolder || '', file: normalizedFile || '' };
|
||||||
|
sessionStorage.setItem('pendingFolderOpen', JSON.stringify(payload));
|
||||||
|
|
||||||
|
const url = normalizedFolder ? `/path/${encodePath(normalizedFolder)}` : '/';
|
||||||
|
if (window.PageRouter && typeof window.PageRouter.loadPage === 'function') {
|
||||||
|
window.PageRouter.loadPage(url, { push: true, scroll: true });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
window.location.href = url;
|
||||||
|
};
|
||||||
|
|
||||||
|
const initFileAccess = () => {
|
||||||
|
const table = document.getElementById('file-access-table');
|
||||||
|
if (!table || table.dataset.bound) return;
|
||||||
|
table.dataset.bound = '1';
|
||||||
|
|
||||||
|
table.querySelectorAll('.play-access-btn').forEach(btn => {
|
||||||
|
const path = normalizePath(btn.dataset.path || '');
|
||||||
|
if (!path) return;
|
||||||
|
|
||||||
|
if (!isAudio(path)) {
|
||||||
|
btn.innerHTML = '<i class="bi bi-download"></i>';
|
||||||
|
btn.setAttribute('title', 'Download');
|
||||||
|
btn.setAttribute('aria-label', 'Download');
|
||||||
|
btn.addEventListener('click', () => {
|
||||||
|
window.location.href = `/media/${encodePath(path)}`;
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
btn.addEventListener('click', () => {
|
||||||
|
const activePlayer = (typeof player !== 'undefined' && player) ? player : window.player;
|
||||||
|
if (activePlayer && typeof activePlayer.loadTrack === 'function') {
|
||||||
|
activePlayer.loadTrack(path);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
table.querySelectorAll('.folder-open-btn').forEach(btn => {
|
||||||
|
const folder = normalizePath(btn.dataset.folder || '');
|
||||||
|
const file = normalizePath(btn.dataset.file || '');
|
||||||
|
btn.addEventListener('click', () => {
|
||||||
|
// If we're already on the app page, load directly.
|
||||||
|
if (window.PageRegistry?.getCurrent?.() === 'app' && typeof loadDirectory === 'function') {
|
||||||
|
loadDirectory(folder).then(() => highlightFile(file));
|
||||||
|
} else {
|
||||||
|
openFolder(folder, file);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
if (window.PageRegistry && typeof window.PageRegistry.register === 'function') {
|
||||||
|
window.PageRegistry.register('file_access', { init: initFileAccess });
|
||||||
|
}
|
||||||
|
})();
|
||||||
@ -125,6 +125,8 @@
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
window.PageRouter = { loadPage };
|
||||||
|
|
||||||
if (document.readyState === 'loading') {
|
if (document.readyState === 'loading') {
|
||||||
document.addEventListener('DOMContentLoaded', boot);
|
document.addEventListener('DOMContentLoaded', boot);
|
||||||
} else {
|
} else {
|
||||||
|
|||||||
@ -114,6 +114,7 @@
|
|||||||
<script src="{{ url_for('static', filename='search.js') }}"></script>
|
<script src="{{ url_for('static', filename='search.js') }}"></script>
|
||||||
<script src="{{ url_for('static', filename='messages.js') }}"></script>
|
<script src="{{ url_for('static', filename='messages.js') }}"></script>
|
||||||
<script src="{{ url_for('static', filename='calendar.js') }}"></script>
|
<script src="{{ url_for('static', filename='calendar.js') }}"></script>
|
||||||
|
<script src="{{ url_for('static', filename='file_access.js') }}"></script>
|
||||||
<script src="{{ url_for('static', filename='connections.js') }}"></script>
|
<script src="{{ url_for('static', filename='connections.js') }}"></script>
|
||||||
<script src="{{ url_for('static', filename='dashboard.js') }}"></script>
|
<script src="{{ url_for('static', filename='dashboard.js') }}"></script>
|
||||||
<script src="{{ url_for('static', filename='folder_secret_config_editor.js') }}"></script>
|
<script src="{{ url_for('static', filename='folder_secret_config_editor.js') }}"></script>
|
||||||
|
|||||||
@ -93,18 +93,30 @@
|
|||||||
<div class="card-body">
|
<div class="card-body">
|
||||||
{% if top20_files %}
|
{% if top20_files %}
|
||||||
<div class="table-responsive">
|
<div class="table-responsive">
|
||||||
<table class="table table-striped">
|
<table class="table table-striped" id="file-access-table">
|
||||||
<thead>
|
<thead>
|
||||||
<tr>
|
<tr>
|
||||||
<th>Access Count</th>
|
<th>Access Count</th>
|
||||||
<th>File Path</th>
|
<th>File Path</th>
|
||||||
|
<th>Aktion</th>
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
{% for row in top20_files %}
|
{% for row in top20_files %}
|
||||||
|
{% set parent_path = row.rel_path.rsplit('/', 1)[0] if '/' in row.rel_path else '' %}
|
||||||
<tr>
|
<tr>
|
||||||
<td>{{ row.access_count }}</td>
|
<td>{{ row.access_count }}</td>
|
||||||
<td>{{ row.rel_path }}</td>
|
<td>{{ row.rel_path }}</td>
|
||||||
|
<td>
|
||||||
|
<div class="d-flex gap-2 file-access-actions">
|
||||||
|
<button class="btn btn-light btn-sm play-access-btn" data-path="{{ row.rel_path | e }}" title="Play" aria-label="Play">
|
||||||
|
<i class="bi bi-volume-up"></i>
|
||||||
|
</button>
|
||||||
|
<button class="btn btn-light btn-sm folder-open-btn" data-folder="{{ parent_path | e }}" data-file="{{ row.rel_path | e }}" title="Ordner öffnen" aria-label="Ordner öffnen">
|
||||||
|
<i class="bi bi-folder"></i>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
</tbody>
|
</tbody>
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user