Skip to content

Commit 2cc8bab

Browse files
committed
v1.0.2: Add Dropbox support and usability improvements.
1 parent 54c5ed7 commit 2cc8bab

9 files changed

Lines changed: 168 additions & 42 deletions

File tree

CHANGELOG.md

Lines changed: 18 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,13 @@
11
# CHANGELOG
22

3+
## v1.0.2 – 2026-02-02
4+
5+
- Support loading content from Dropbox.
6+
- Usability improvements: add contextual help and improve the top bar layout.
7+
- Ask for confirmation before closing the current content.
8+
9+
---
10+
311
## v1.0.1 – 2026-01-27
412

513
- Support loading content from Google Drive shared links (requires Google Apps Script proxy configuration).
@@ -8,13 +16,13 @@
816

917
## v1.0.0 – 2026-01-08
1018

11-
- First official release of eXeViewer
12-
- Client-side web application to view eXeLearning content packages (.zip/.elpx) directly in the browser
13-
- All processing happens locally: no files are uploaded to any server
14-
- Load content from local files (drag and drop or file browser) or from URLs
15-
- Support for Nextcloud and ownCloud shared links
16-
- Generate shareable links for content loaded from URLs
17-
- Installable as a Progressive Web App (PWA) for offline use
18-
- Multi-language support (English and Spanish)
19-
- Dark mode (follows system preference)
20-
- Responsive interface
19+
- First official release of eXeViewer.
20+
- Client-side web application to view eXeLearning content packages (.zip/.elpx) directly in the browser.
21+
- All processing happens locally: no files are uploaded to any server.
22+
- Load content from local files (drag and drop or file browser) or from URLs.
23+
- Support for Nextcloud and ownCloud shared links.
24+
- Generate shareable links for content loaded from URLs.
25+
- Installable as a Progressive Web App (PWA) for offline use.
26+
- Multi-language support (English and Spanish).
27+
- Dark mode (follows system preference).
28+
- Responsive interface.

README.md

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ The application runs entirely in your browser. When you load a file from your de
1414

1515
- Load `.zip` or `.elpx` files from your device (drag and drop or file browser)
1616
- Load content from a URL (direct links to `.zip` or `.elpx` files)
17-
- Support for shared links from Nextcloud or ownCloud
17+
- Support for shared links from Nextcloud, ownCloud and Dropbox
1818
- Google Drive support (requires proxy configuration)
1919
- Generate shareable links when viewing URL-loaded content
2020
- In-memory extraction (no files written to disk)
@@ -42,7 +42,7 @@ The file never leaves your device. All processing happens in your browser.
4242

4343
This works with:
4444
- Direct links to files on any server
45-
- Shared links from Nextcloud and ownCloud
45+
- Shared links from Nextcloud, ownCloud and Dropbox
4646
- Shared links from Google Drive (requires proxy configuration, see below)
4747

4848
### Google Drive support
@@ -81,7 +81,7 @@ When you load content from a URL, two buttons appear in the top bar:
8181

8282
**This solves a common problem**: many eXeLearning users create content but don’t have a place to publish it. With eXeViewer:
8383

84-
1. Upload your `.zip` or `.elpx` file to a cloud service (Nextcloud, ownCloud, or any file server)
84+
1. Upload your `.zip` or `.elpx` file to a cloud service (Nextcloud, ownCloud, Dropbox or any file server)
8585
2. Generate a share link from your cloud service
8686
3. Paste the link in eXeViewer
8787
4. Click the "Share" button to get a viewer URL

README_es.md

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ La aplicación se ejecuta completamente en tu navegador. Cuando cargas un ficher
1414

1515
- Carga ficheros `.zip` o `.elpx` desde tu dispositivo (arrastrándolos o seleccionándolos)
1616
- Carga contenido desde una URL (enlaces directos a ficheros `.zip` o `.elpx`)
17-
- Admite enlaces compartidos de Nextcloud y ownCloud
17+
- Admite enlaces compartidos de Nextcloud, ownCloud y Dropbox
1818
- Soporte para Google Drive (requiere configuración de proxy)
1919
- Genera enlaces para compartir cuando visualizas contenido cargado desde URL
2020
- Extracción en memoria (no se escribe nada en disco)
@@ -42,7 +42,7 @@ El fichero nunca sale de tu dispositivo. Todo el procesamiento ocurre en tu nave
4242

4343
Funciona con:
4444
- Enlaces directos a ficheros .zip o .elpx en cualquier servidor
45-
- Enlaces compartidos de Nextcloud y ownCloud
45+
- Enlaces compartidos de Nextcloud, ownCloud y Dropbox
4646
- Enlaces compartidos de Google Drive (requiere configuración de proxy, ver más abajo)
4747

4848
### Soporte de Google Drive
@@ -81,7 +81,7 @@ Cuando cargas contenido desde una URL, aparecen dos botones en la barra superior
8181

8282
**Esto resuelve un problema común**: muchos usuarios de eXeLearning crean contenido pero no tienen dónde publicarlo. Con eXeViewer:
8383

84-
1. Sube tu fichero `.zip` o `.elpx` a un servicio en la nube (Nextcloud, ownCloud o cualquier servidor)
84+
1. Sube tu fichero `.zip` o `.elpx` a un servicio en la nube (Nextcloud, ownCloud, Dropbox o cualquier servidor)
8585
2. Genera un enlace para compartir desde tu servicio en la nube
8686
3. Pega el enlace en eXeViewer
8787
4. Haz clic en el botón "Compartir" para obtener una URL del visor

css/styles.css

Lines changed: 21 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -92,9 +92,12 @@ body {
9292
#welcomeLanguageBtn {
9393
min-width: 100px;
9494
}
95+
96+
/* Small buttons (only icon) */
9597
#topNavbar #btnDownload,
9698
#topNavbar #btnShare,
97-
#topNavbar #btnNewWindow {
99+
#topNavbar #btnNewWindow,
100+
#topNavbar #btnLoadNew {
98101
min-width: 0;
99102
border-color: transparent;
100103
}
@@ -208,6 +211,19 @@ body {
208211
font-size: 0.95rem;
209212
}
210213

214+
.url-help-icon {
215+
color: var(--text-muted);
216+
cursor: pointer;
217+
margin-left: 0.25rem;
218+
opacity: 0.7;
219+
transition: opacity 0.15s ease-in-out;
220+
}
221+
222+
.url-help-icon:hover,
223+
.url-help-icon:focus {
224+
opacity: 1;
225+
}
226+
211227
.url-input-group {
212228
display: flex;
213229
gap: 0.5rem;
@@ -450,6 +466,10 @@ body {
450466
min-height: 250px;
451467
}
452468

469+
#exitModal .modal-body {
470+
min-height: 0;
471+
}
472+
453473
/* Success text with good contrast in both modes */
454474
.alert-success-text {
455475
color: var(--success-text);

index.html

Lines changed: 34 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -55,7 +55,7 @@
5555
</span>
5656

5757
<div class="d-flex align-items-center">
58-
<span id="packageName" class="text-light me-3 d-none d-sm-inline"></span>
58+
<span id="packageName" class="text-light me-3 visually-hidden"></span>
5959

6060
<!-- Download Button (hidden by default, shown only for URL-loaded content in browser) -->
6161
<button id="btnDownload" class="btn btn-outline-light btn-sm me-2 d-none" type="button"
@@ -78,8 +78,15 @@
7878
<span class="visually-hidden" data-i18n="navbar.openNewWindow">Open in new window</span>
7979
</button>
8080

81-
<!-- Language Selector -->
82-
<div class="dropdown me-2">
81+
<!-- Exit Button (hidden by default, shown when content is loaded) -->
82+
<button id="btnLoadNew" class="btn btn-outline-light btn-sm me-2 d-none" type="button"
83+
data-bs-toggle="tooltip" data-bs-placement="bottom" data-i18n-title="navbar.loadAnother">
84+
<i class="bi bi-x-lg me-1"></i>
85+
<span class="visually-hidden" data-i18n="navbar.newFile">Exit</span>
86+
</button>
87+
88+
<!-- Language Selector (always visible, fixed position) -->
89+
<div class="dropdown">
8390
<button id="languageDropdownBtn" class="btn btn-outline-light btn-sm dropdown-toggle" type="button" data-bs-toggle="dropdown" aria-expanded="false" aria-label="Select language" data-i18n-aria="accessibility.languageSelector">
8491
<i class="bi bi-globe me-1"></i><span data-i18n="language.en">English</span>
8592
</button>
@@ -88,12 +95,6 @@
8895
<li><a class="dropdown-item" href="#" data-lang="es">Español</a></li>
8996
</ul>
9097
</div>
91-
92-
<button id="btnLoadNew" class="btn btn-outline-light btn-sm" type="button"
93-
data-bs-toggle="tooltip" data-bs-placement="bottom" data-i18n-title="navbar.loadAnother">
94-
<i class="bi bi-escape me-1"></i>
95-
<span class="d-none d-sm-inline" data-i18n="navbar.newFile">Exit</span>
96-
</button>
9798
</div>
9899
</div>
99100
</nav>
@@ -145,6 +146,11 @@ <h1 class="mb-3 h3" data-i18n="app.title">eXeViewer</h1>
145146
<!-- URL Input Section (outside drop zone) -->
146147
<div class="url-section mt-4">
147148
<label id="urlLabel" for="urlInput" class="url-section-title mb-2">Load from URL</label>
149+
<span id="urlHelpIcon" class="url-help-icon" tabindex="0" role="button"
150+
data-bs-toggle="tooltip" data-bs-placement="top"
151+
title="">
152+
<i class="bi bi-question-circle" aria-hidden="true"></i>
153+
</span>
148154
<div class="url-input-group">
149155
<input type="url" id="urlInput" class="form-control"
150156
aria-describedby="urlLabel"
@@ -231,6 +237,25 @@ <h5 class="modal-title" id="shareModalLabel" data-i18n="share.modalTitle">Share
231237
</div>
232238
</div>
233239

240+
<!-- Exit Confirmation Modal -->
241+
<div class="modal fade" id="exitModal" tabindex="-1" aria-labelledby="exitModalLabel" aria-hidden="true" aria-modal="true">
242+
<div class="modal-dialog modal-dialog-centered">
243+
<div class="modal-content">
244+
<div class="modal-header">
245+
<h5 class="modal-title" id="exitModalLabel" data-i18n="exit.modalTitle">Exit</h5>
246+
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close" data-i18n-aria="accessibility.closeModal"></button>
247+
</div>
248+
<div class="modal-body">
249+
<p data-i18n="exit.message">You will return to the main page, where you can load content.</p>
250+
</div>
251+
<div class="modal-footer">
252+
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal" data-i18n="exit.cancel">Cancel</button>
253+
<button type="button" class="btn btn-primary" id="btnConfirmExit" data-i18n="exit.confirm">Exit</button>
254+
</div>
255+
</div>
256+
</div>
257+
</div>
258+
234259
<!-- Bootstrap JS -->
235260
<script src="vendor/bootstrap/js/bootstrap.bundle.min.js"></script>
236261

js/app.js

Lines changed: 66 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@
99
// Configuration
1010
const config = {
1111
// Application version (displayed in footer)
12-
version: '1.0.1',
12+
version: '1.0.2',
1313
// Automatically restore and display content from IndexedDB on page load
1414
autoRestoreContent: true,
1515
// Open external links in a new window/tab (prevents navigation issues in iframes)
@@ -68,7 +68,12 @@
6868
// Footer element
6969
footerInfo: null,
7070
// URL label element
71-
urlLabel: null
71+
urlLabel: null,
72+
// URL help icon element
73+
urlHelpIcon: null,
74+
// Exit modal elements
75+
exitModal: null,
76+
btnConfirmExit: null
7277
};
7378

7479
/**
@@ -171,6 +176,11 @@
171176
elements.footerInfo = document.getElementById('footerInfo');
172177
// URL label element
173178
elements.urlLabel = document.getElementById('urlLabel');
179+
// URL help icon element
180+
elements.urlHelpIcon = document.getElementById('urlHelpIcon');
181+
// Exit modal elements
182+
elements.exitModal = document.getElementById('exitModal');
183+
elements.btnConfirmExit = document.getElementById('btnConfirmExit');
174184
}
175185

176186
/**
@@ -208,8 +218,18 @@
208218
function updateUrlLabel() {
209219
if (!elements.urlLabel) return;
210220

211-
const key = config.gasProxyUrl ? 'welcome.loadFromUrlWithDrive' : 'welcome.loadFromUrl';
212-
elements.urlLabel.textContent = i18n.t(key);
221+
elements.urlLabel.textContent = i18n.t('welcome.loadFromUrl');
222+
223+
// Update tooltip on help icon
224+
if (elements.urlHelpIcon) {
225+
const tooltipKey = config.gasProxyUrl ? 'welcome.loadFromUrlTooltipWithDrive' : 'welcome.loadFromUrlTooltip';
226+
elements.urlHelpIcon.setAttribute('title', i18n.t(tooltipKey));
227+
// Refresh Bootstrap tooltip if already initialized
228+
const tooltipInstance = bootstrap.Tooltip.getInstance(elements.urlHelpIcon);
229+
if (tooltipInstance) {
230+
tooltipInstance.setContent({ '.tooltip-inner': i18n.t(tooltipKey) });
231+
}
232+
}
213233
}
214234

215235
/**
@@ -220,6 +240,8 @@
220240
state.currentPackageName = i18n.t('navbar.restoredContent');
221241
elements.packageName.textContent = state.currentPackageName;
222242
elements.packageName.title = state.currentPackageName;
243+
// Show package name visually only for restored content
244+
elements.packageName.classList.remove('visually-hidden');
223245
}
224246
}
225247

@@ -535,7 +557,7 @@
535557
}
536558

537559
/**
538-
* Convert special URLs (Google Drive, ownCloud/Nextcloud) to direct download links
560+
* Convert special URLs (Google Drive, ownCloud/Nextcloud, Dropbox) to direct download links
539561
* @param {string} url - The original URL
540562
* @returns {string} The converted URL for direct download
541563
*/
@@ -564,6 +586,15 @@
564586
return `${urlObj.origin}${cleanPath}/download`;
565587
}
566588

589+
// Dropbox shared links
590+
// Format: https://www.dropbox.com/s/HASH/filename.zip?dl=0
591+
// or: https://www.dropbox.com/scl/fi/HASH/filename.zip?rlkey=xxx&dl=0
592+
// Convert to: same URL with dl=1 for direct download
593+
if (urlObj.hostname === 'www.dropbox.com' || urlObj.hostname === 'dropbox.com') {
594+
urlObj.searchParams.set('dl', '1');
595+
return urlObj.toString();
596+
}
597+
567598
// Return original URL if no conversion needed
568599
return url;
569600
} catch (e) {
@@ -960,14 +991,19 @@
960991
if (state.currentPackageName) {
961992
elements.packageName.textContent = state.currentPackageName;
962993
elements.packageName.title = state.currentPackageName;
994+
// Show package name visually only for restored content
995+
if (state.isRestoredContent) {
996+
elements.packageName.classList.remove('visually-hidden');
997+
}
963998
}
964999

9651000
// Update share and download button visibility
9661001
updateShareButtonVisibility();
9671002
updateDownloadButtonVisibility();
9681003

969-
// Show open in new window button
1004+
// Show open in new window and exit buttons
9701005
elements.btnNewWindow.classList.remove('d-none');
1006+
elements.btnLoadNew.classList.remove('d-none');
9711007

9721008
// Set up history states: first mark current state as welcome, then push viewer state
9731009
const welcomeState = { isWelcome: true };
@@ -1008,6 +1044,8 @@
10081044
elements.btnDownload.classList.add('d-none');
10091045
elements.btnShare.classList.add('d-none');
10101046
elements.btnNewWindow.classList.add('d-none');
1047+
elements.btnLoadNew.classList.add('d-none');
1048+
elements.packageName.classList.add('visually-hidden');
10111049
elements.welcomeScreen.classList.remove('d-none');
10121050

10131051
// Clear file input and URL input
@@ -1157,11 +1195,27 @@
11571195
event.preventDefault();
11581196
});
11591197

1160-
// Load new file button
1198+
// Load new file button - show confirmation modal
11611199
elements.btnLoadNew.addEventListener('click', () => {
1200+
const modal = new bootstrap.Modal(elements.exitModal);
1201+
modal.show();
1202+
});
1203+
1204+
// Confirm exit button in modal
1205+
elements.btnConfirmExit.addEventListener('click', () => {
1206+
const modal = bootstrap.Modal.getInstance(elements.exitModal);
1207+
modal.hide();
11621208
resetApplication();
11631209
});
11641210

1211+
// Handle Enter key in exit modal (like native confirm)
1212+
elements.exitModal.addEventListener('keydown', (event) => {
1213+
if (event.key === 'Enter') {
1214+
event.preventDefault();
1215+
elements.btnConfirmExit.click();
1216+
}
1217+
});
1218+
11651219
// URL input - load button click
11661220
elements.btnLoadUrl.addEventListener('click', () => {
11671221
downloadFromUrl(elements.urlInput.value);
@@ -1208,6 +1262,9 @@
12081262
if (elements.btnLoadNew) {
12091263
new bootstrap.Tooltip(elements.btnLoadNew);
12101264
}
1265+
if (elements.urlHelpIcon) {
1266+
new bootstrap.Tooltip(elements.urlHelpIcon);
1267+
}
12111268

12121269
// Setup language selector
12131270
setupLanguageSelector();
@@ -1460,6 +1517,7 @@
14601517
elements.viewerContainer.classList.remove('d-none');
14611518
elements.topNavbar.classList.remove('d-none');
14621519
elements.btnNewWindow.classList.remove('d-none');
1520+
elements.btnLoadNew.classList.remove('d-none');
14631521
updateShareButtonVisibility();
14641522
updateDownloadButtonVisibility();
14651523
}
@@ -1489,6 +1547,7 @@
14891547
elements.btnDownload.classList.add('d-none');
14901548
elements.btnShare.classList.add('d-none');
14911549
elements.btnNewWindow.classList.add('d-none');
1550+
elements.btnLoadNew.classList.add('d-none');
14921551
elements.welcomeScreen.classList.remove('d-none');
14931552
// Don't change iframe.src here - it would invalidate forward history
14941553
}

0 commit comments

Comments
 (0)