Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
79 commits
Select commit Hold shift + click to select a range
bf8c793
Fix #1023 - Integrate v3.0.2 TinyMCE fixes and remove the image optim…
ignaciogros Jan 17, 2026
d96ded6
The default alt text for an image selected from the File Manager shou…
ignaciogros Jan 17, 2026
0b6dbc9
FIGURE elements, like IMG elements, should have a max-width of 100% r…
ignaciogros Jan 17, 2026
7955808
Preserve existing classes and captions (custom license).
ignaciogros Jan 17, 2026
02d3ef3
Update the test to check if the accessibility warning is displayed wh…
ignaciogros Jan 17, 2026
49adead
Update test to handle the accessibility warning dialog.
ignaciogros Jan 17, 2026
b115f9a
Fix "TinyMCE Advanced Editor (CodeMagic)" test.
ignaciogros Jan 17, 2026
2c5175d
Merge branch 'main' into 1023-tinymce-plugins-integrate-v302-fixes
ignaciogros Jan 17, 2026
d4d43ed
Fix PDF preview test to work with either <iframe> or <embed>.
ignaciogros Jan 17, 2026
9f40727
Merge branch 'main' into 1023-tinymce-plugins-integrate-v302-fixes
erseco Jan 17, 2026
14988c7
Fill in alt text in the "exeimage" plugin to avoid accessibility warn…
ignaciogros Jan 17, 2026
5605f5a
Remove the 'Image Optimizer' test (that optimizer is no longer in eXe).
ignaciogros Jan 17, 2026
4c8e487
Merge branch 'main' of https://github.com/exelearning/exelearning int…
ignaciogros Jan 17, 2026
b175009
Merge branch '1023-tinymce-plugins-integrate-v302-fixes' of https://g…
ignaciogros Jan 17, 2026
1b0c337
Fixed tests: 'should persist image after save and reload' and 'should…
ignaciogros Jan 17, 2026
4b52de3
Fix lint issue.
ignaciogros Jan 18, 2026
ff9fdc3
Fix 'Undo/Redo iDevice Icon' test.
ignaciogros Jan 18, 2026
04fd0e8
Fix CodeMagic editor not loading with nested BASE_PATH.
ignaciogros Jan 18, 2026
dc9c1c4
Merge branch 'main' of github.com:exelearning/exelearning
erseco Jan 18, 2026
3d7ed8b
Fix Mermaid not loading with nested BASE_PATH.
ignaciogros Jan 18, 2026
ddebbe6
Merge branch 'main' into 1042-tinymce-issues-when-using-a-different-b…
ignaciogros Jan 18, 2026
45e8af2
Merge branch 'main' of github.com:exelearning/exelearning
erseco Jan 18, 2026
03559c7
Code simplification.
ignaciogros Jan 18, 2026
a65e43b
Merge branch 'main' of github.com:exelearning/exelearning into 1042-t…
erseco Jan 18, 2026
ab2b8b8
Merge branch 'main' of github.com:exelearning/exelearning
erseco Jan 18, 2026
b6eb752
Merge branch 'main' into 1042-tinymce-issues-when-using-a-different-b…
erseco Jan 18, 2026
9215514
Merge branch '1042-tinymce-issues-when-using-a-different-base_path' o…
erseco Jan 18, 2026
21f6981
Merge branch 'main' of github.com:exelearning/exelearning
erseco Jan 18, 2026
f9df16d
Unify and refactor base-path resolution2
erseco Jan 18, 2026
d38bf33
Update e2e tests
erseco Jan 18, 2026
4ba9f83
Fixed export of images and CDATA in cli
erseco Jan 19, 2026
b54d67e
Initial work recovering the data
erseco Jan 19, 2026
c001a15
Fix convert asset:// urls to real path
erseco Jan 19, 2026
964bac4
Fix convert asset:// urls to real path
erseco Jan 19, 2026
85aafee
Merge branch 'main' of github.com:exelearning/exelearning
erseco Jan 19, 2026
928d45d
Added the context_path prefix
erseco Jan 19, 2026
dcc42e8
Simplified to use uuid.ext and no more uuid/path
erseco Jan 19, 2026
106b016
Fix the
erseco Jan 19, 2026
9a89d57
Fix duplicated content/resources/
erseco Jan 19, 2026
2745da9
Improved e2e tests
erseco Jan 19, 2026
0b3fd2b
Merge branch 'release/3.1-fix-commandline-export' of github.com:exele…
erseco Jan 19, 2026
b9dd46a
Merge branch '1052-image-gallery-images-not-visible-importing-elp-29'…
erseco Jan 19, 2026
65790d2
Fix tests and improve e2e
erseco Jan 19, 2026
9dd82ec
Merge branch '1052-image-gallery-images-not-visible-importing-elp-29'…
erseco Jan 19, 2026
636d072
Rename LICENSES file and fix svg icons
erseco Jan 19, 2026
051e457
Fix blob in preview
erseco Jan 20, 2026
5eafaf3
Merge branch 'main' into hotfix/always-convert-asset-to-path-in-export
ignaciogros Jan 20, 2026
0fd1f59
Merge branch 'main' into hotfix/always-convert-asset-to-path-in-export
ignaciogros Jan 20, 2026
c3f8e80
Merge branch 'main' into release/3.1-refactor-base-path-resolve-in-fr…
erseco Jan 20, 2026
9487623
Merge branch 'main' into 1052-image-gallery-images-not-visible-import…
erseco Jan 20, 2026
6a565b9
Fix pagination, add support for jsidevice, and duplicated filename
erseco Jan 20, 2026
d66f53b
Added e2e test and fix the component export to work as the xml export…
erseco Jan 20, 2026
ca37d05
Merge branch 'hotfix/always-convert-asset-to-path-in-export' of githu…
erseco Jan 20, 2026
d4eefb4
Merge branch 'gallery-backup' of github.com:ateeducacion/exelearning …
erseco Jan 20, 2026
424bd6b
Fix the odeexport to mimic the browser one
erseco Jan 20, 2026
b34f471
Merge branch 'hotfix/always-convert-asset-to-path-in-export' of githu…
erseco Jan 20, 2026
ff9b831
Merge branch 'main' of github.com:exelearning/exelearning
erseco Jan 20, 2026
979d332
Change the page export to use local approach
erseco Jan 20, 2026
74a1b99
Merge branch 'main' into 1058-images-missing-when-exporting-pages-fro…
erseco Jan 20, 2026
182ec2e
Merge branch 'main' into 1052-image-gallery-images-not-visible-import…
erseco Jan 20, 2026
15b40be
Merge branch '1052-image-gallery-images-not-visible-importing-elp-29'…
erseco Jan 20, 2026
e7e5257
Merge branch 'main' of github.com:exelearning/exelearning
erseco Jan 20, 2026
a9aec96
Merge branch 'main' into release/3.1-refactor-base-path-resolve-in-fr…
erseco Jan 20, 2026
6ec0c0d
Merge branch 'main' into 1058-images-missing-when-exporting-pages-fro…
erseco Jan 20, 2026
5a9c6aa
Merge branch 'main' of github.com:exelearning/exelearning
erseco Jan 20, 2026
f48b05c
Merge branch 'main' into 1058-images-missing-when-exporting-pages-fro…
erseco Jan 20, 2026
86bffcf
Fix export icon on pages
erseco Jan 20, 2026
8b2e6dc
Merge branch '1058-images-missing-when-exporting-pages-from-older-res…
erseco Jan 20, 2026
8306c9f
Fix gallery and add e2e tests
erseco Jan 21, 2026
877adda
Fix map
erseco Jan 21, 2026
68b418d
Fixed problem in e2e
erseco Jan 21, 2026
236d7e3
Merge branch 'main' into 1052-image-gallery-images-not-visible-import…
erseco Jan 21, 2026
65a5805
Centralized json solution to blob<->asset url resolution
erseco Jan 21, 2026
fe016bc
Increased coverage
erseco Jan 21, 2026
2281671
Merge branch '1052-image-gallery-images-not-visible-importing-elp-29'
erseco Jan 21, 2026
48bd38d
Merge remote-tracking branch 'origin/main' into release/3.1-refactor-…
erseco Jan 21, 2026
e9a46c3
Fixed tests and upgraded from main
erseco Jan 21, 2026
fb1870c
Merge main into release/3.1-refactor-base-path-resolve-in-frontend
erseco Jan 22, 2026
4920349
Cherrypicked from main
erseco Jan 22, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
61 changes: 61 additions & 0 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -558,6 +558,67 @@ The application MUST support importing legacy .elp files from pre-v3.0 eXeLearni
- Use `assets/styles/components/_collaborative.scss` for Yjs/collaboration-related styles
- The only exception is dynamically loading external CSS files (themes, iDevices) at runtime via `loadStyleByInsertingIt()`

### Frontend Asset URLs — Use `resolveAssetUrl()`

**NEVER construct asset URLs manually** in frontend JavaScript. Always use `eXeLearning.resolveAssetUrl()` to get properly versioned URLs with the correct basePath.

#### Why?

The application can be installed at different URL paths (e.g., `/`, `/exelearning/`, `/web/app/`). Additionally, static assets are cache-busted with a version string. Manually constructing URLs will break when:
- The app is deployed at a non-root path (`BASE_PATH` environment variable)
- The version changes (cache invalidation)

#### How to use

```javascript
// BAD - Manual URL construction (NEVER do this)
const url = '/libs/jquery/jquery.min.js';
const url = basePath + '/app/common/mermaid.js';
const url = `${eXeLearning.config.basePath}/libs/tinymce/tinymce.min.js`;

// GOOD - Use resolveAssetUrl()
const url = eXeLearning.resolveAssetUrl('/libs/jquery/jquery.min.js');
// Returns: /basepath/v4.0.0/libs/jquery/jquery.min.js
```

#### Context-specific usage

| Context | How to use |
|---------|-----------|
| **Workarea (main app)** | `eXeLearning.resolveAssetUrl(path)` |
| **iframes** (e.g., interactive-video editor) | `window.parent.eXeLearning?.resolveAssetUrl?.(path)` with fallback |
| **Exports** | Use relative paths (`./libs/` or `../libs/`) - function not available |

#### In iframes (check availability first)

```javascript
// In iframe code (e.g., iDevice editors)
const parentResolveAssetUrl = window.parent.eXeLearning?.resolveAssetUrl?.bind(window.parent.eXeLearning);
const basePath = window.parent.eXeLearning?.config?.basePath || '';

const assetUrl = (path) => parentResolveAssetUrl
? parentResolveAssetUrl(path)
: basePath + path; // Fallback for exports
```

#### What resolveAssetUrl returns

```javascript
// With basePath="/exelearning" and version="v4.0.0"
eXeLearning.resolveAssetUrl('/libs/jquery.js')
// → "/exelearning/v4.0.0/libs/jquery.js"

// With empty basePath and version="v4.0.0"
eXeLearning.resolveAssetUrl('/app/common/mermaid.js')
// → "/v4.0.0/app/common/mermaid.js"
```

#### Where it's defined

- **Definition**: `views/workarea/workarea.njk` (in the global `eXeLearning` object)
- **Available in**: All frontend JavaScript after page loads
- **NOT available in**: Exported HTML (use relative paths instead)

## E2E Testing with Playwright

### Test Credentials
Expand Down
10 changes: 2 additions & 8 deletions public/app/app.js
Original file line number Diff line number Diff line change
Expand Up @@ -490,14 +490,8 @@ export default class App {
});
}

// COMPATIBILITY SHIM: Create eXeLearning.symfony for legacy iDevices
// Legacy iDevices (like interactive-video) reference eXeLearning.symfony.baseURL
// This shim maps them to the new eXeLearning.config structure
window.eXeLearning.symfony = {
baseURL: window.eXeLearning.config.baseURL || '',
basePath: window.eXeLearning.config.basePath || '',
fullURL: window.eXeLearning.config.fullURL || '',
};
// Note: resolveAssetUrl is defined in the template (workarea.njk) to ensure
// it's available before any script loads, including common.js IIFE
}

setupSessionMonitor() {
Expand Down
85 changes: 64 additions & 21 deletions public/app/app.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -151,43 +151,86 @@ describe('App utility methods', () => {

window.location = originalLocation;
});
});

it('creates symfony compatibility shim from config', () => {
describe('resolveAssetUrl', () => {
// resolveAssetUrl is defined in the template (workarea.njk) before app.js loads
// Simulate this by defining it in beforeEach before calling parseExelearningConfig
beforeEach(() => {
window.eXeLearning.user = '{"id":1}';
window.eXeLearning.config = '{"isOfflineInstallation":false,"baseURL":"http://localhost","basePath":"/app","fullURL":"http://localhost/app"}';
window.eXeLearning.config = '{"basePath": "/mybase"}';
window.eXeLearning.version = 'v1.0.0';

// Define resolveAssetUrl as the template would (before any script runs)
window.eXeLearning.resolveAssetUrl = function(path) {
const basePath = (this.config?.basePath || '').replace(/\/+$/, '');
const normalizedPath = path.startsWith('/') ? path : '/' + path;
return this.version
? basePath + '/' + this.version + normalizedPath
: basePath + normalizedPath;
};

const originalLocation = window.location;
delete window.location;
window.location = { href: 'http://localhost/test', protocol: 'http:' };

appInstance.parseExelearningConfig();

// Verify symfony compatibility shim is created
expect(window.eXeLearning.symfony).toBeDefined();
expect(window.eXeLearning.symfony.baseURL).toBe('http://localhost');
expect(window.eXeLearning.symfony.basePath).toBe('/app');
expect(window.eXeLearning.symfony.fullURL).toBe('http://localhost/app');

window.location = originalLocation;
});

it('creates symfony compatibility shim with empty defaults', () => {
window.eXeLearning.user = '{"id":1}';
window.eXeLearning.config = '{"isOfflineInstallation":true}';
it('composes URL with basePath and version', () => {
expect(window.eXeLearning.resolveAssetUrl('/app/test.js')).toBe(
'/mybase/v1.0.0/app/test.js'
);
});

const originalLocation = window.location;
delete window.location;
window.location = { href: 'http://localhost/test', protocol: 'http:' };
it('handles nested basePath', () => {
window.eXeLearning.config = { basePath: '/a/b/c' };
expect(window.eXeLearning.resolveAssetUrl('/libs/x.js')).toBe(
'/a/b/c/v1.0.0/libs/x.js'
);
});

appInstance.parseExelearningConfig();
it('handles empty basePath', () => {
window.eXeLearning.config = { basePath: '' };
expect(window.eXeLearning.resolveAssetUrl('/app/x.js')).toBe(
'/v1.0.0/app/x.js'
);
});

// Verify symfony compatibility shim defaults to empty strings
expect(window.eXeLearning.symfony).toBeDefined();
expect(window.eXeLearning.symfony.baseURL).toBe('');
expect(window.eXeLearning.symfony.basePath).toBe('');
expect(window.eXeLearning.symfony.fullURL).toBe('');
it('normalizes path without leading slash', () => {
expect(window.eXeLearning.resolveAssetUrl('app/test.js')).toBe(
'/mybase/v1.0.0/app/test.js'
);
});

window.location = originalLocation;
it('handles missing version', () => {
window.eXeLearning.version = '';
expect(window.eXeLearning.resolveAssetUrl('/app/x.js')).toBe(
'/mybase/app/x.js'
);
});

it('handles undefined version', () => {
window.eXeLearning.version = undefined;
expect(window.eXeLearning.resolveAssetUrl('/app/x.js')).toBe(
'/mybase/app/x.js'
);
});

it('strips trailing slashes from basePath', () => {
window.eXeLearning.config = { basePath: '/mybase///' };
expect(window.eXeLearning.resolveAssetUrl('/app/x.js')).toBe(
'/mybase/v1.0.0/app/x.js'
);
});

it('handles undefined config', () => {
window.eXeLearning.config = undefined;
expect(window.eXeLearning.resolveAssetUrl('/app/x.js')).toBe(
'/v1.0.0/app/x.js'
);
});
});

Expand Down
141 changes: 51 additions & 90 deletions public/app/common/common.js
Original file line number Diff line number Diff line change
Expand Up @@ -24,43 +24,13 @@
*/

window.MathJax = window.MathJax || (function() {
var isWorkarea = typeof window.eXeLearning !== 'undefined' || document.querySelector('script[src*="app/common/exe_math"]');
// Detect context: workarea vs export
// In workarea: eXeLearning.resolveAssetUrl is defined in template before any script loads
// In exports: use relative paths (./libs or ../libs)
var isWorkarea = typeof window.eXeLearning?.resolveAssetUrl === 'function';
var isIndex = document.documentElement.id === 'exe-index';
// For workarea: use versioned path from eXeLearning config or detect from script tags
// For exports: use relative paths (./libs or ../libs)
var version = (window.eXeLearning && window.eXeLearning.version) || '';
var configBasePath = '';
if (isWorkarea) {
// Try to detect version and basePath from existing script tags (e.g., /web/exelearning/v0.0.0-alpha/app/...)
var scriptTag = document.querySelector('script[src*="/app/common/"]');
if (scriptTag) {
var src = scriptTag.src;
// Extract version (e.g., v0.0.0-alpha)
var versionMatch = src.match(/\/(v[\d.]+[^/]*)\//);
if (versionMatch) version = versionMatch[1];
// Extract basePath - everything before /v... or /app/
// URL might be: /web/exelearning/v0.0.0/app/common/... or /v0.0.0/app/common/...
try {
var url = new URL(src);
var pathname = url.pathname;
// Find where the versioned path or /app/ starts
var appIndex = pathname.indexOf('/app/common/');
if (appIndex > 0) {
var beforeApp = pathname.substring(0, appIndex);
// If there's a version, remove it from the path
if (version && beforeApp.endsWith('/' + version)) {
configBasePath = beforeApp.substring(0, beforeApp.length - version.length - 1);
} else {
configBasePath = beforeApp;
}
}
} catch (e) {
// If URL parsing fails, leave configBasePath empty
}
}
}
var basePath = isWorkarea
? (version ? configBasePath + '/' + version + '/app/common/exe_math' : configBasePath + '/app/common/exe_math')
? window.eXeLearning.resolveAssetUrl('/app/common/exe_math')
: (isIndex ? './libs/exe_math' : '../libs/exe_math');

var externalExtensions = [
Expand Down Expand Up @@ -200,6 +170,18 @@ var $exe = {
},
// Load MathJax or just create the links to the code and/or image
init: function () {
// Check if LaTeX is already pre-rendered to SVG
// Pre-rendered content has class exe-math-rendered
var preRenderedMath = $(".exe-math-rendered").length > 0;

// If we have pre-rendered math, ALL LaTeX was pre-rendered during export
// No need to load MathJax library (~1MB)
if (preRenderedMath) {
// All LaTeX is pre-rendered - just create links, no MathJax needed
$exe.math.createLinks();
return;
}

$("body").addClass("exe-auto-math"); // Always load it
var math = $(".exe-math");
var mathjax = false;
Expand Down Expand Up @@ -281,7 +263,12 @@ var $exe = {

if (typeof window.mermaid === 'undefined') {
const script = document.createElement("script");
script.src = enginePath;
// Load Mermaid from the right path
// In workarea: resolveAssetUrl is defined in template
// In exports: use this.engine (relative path set above)
script.src = typeof window.eXeLearning?.resolveAssetUrl === 'function'
? window.eXeLearning.resolveAssetUrl('/app/common/mermaid/mermaid.min.js')
: this.engine;
script.async = true;
script.onload = function () {
mermaid = window.mermaid;
Expand Down Expand Up @@ -365,9 +352,17 @@ var $exe = {
}
},
init: function () {
// Check if Mermaid diagrams are already pre-rendered to SVG
// Pre-rendered diagrams have class exe-mermaid-rendered
var preRenderedMermaid = $(".exe-mermaid-rendered").length > 0;

// If we have pre-rendered mermaid, ALL diagrams were pre-rendered during export
// No need to load the Mermaid library (~2.7MB)
if (preRenderedMermaid) {
return;
}

// Check for mermaid elements that need rendering
// Pre-rendered diagrams have class exe-mermaid-rendered (not .mermaid)
// so they won't be matched by this selector.
// Include ALL .mermaid elements (even data-processed="pending" which means
// a previous render failed) so Mermaid library gets loaded and they can retry.
var mermaidNodes = $(".mermaid");
Expand Down Expand Up @@ -619,18 +614,12 @@ var $exe = {
hasTooltips: function () {
if ($("A.exe-tooltip").length > 0) {
var p = "";
if (typeof (eXeLearning) !== 'undefined') {
// TODO: UNIFY - Fallback for branch compatibility.
// In 'main' branch: eXeLearning.symfony.fullURL exists (added by Symfony backend)
// In this branch: only eXeLearning.config.fullURL exists (set in workarea.njk)
// To unify: Either add 'symfony' property to workarea.njk template,
// or update main branch to use 'config' consistently.
p = (eXeLearning.symfony?.fullURL || eXeLearning.config?.fullURL || '') + "/app/common/exe_tooltips/";
if (typeof eXeLearning !== 'undefined' && typeof eXeLearning.resolveAssetUrl === 'function') {
// Workarea context - use resolveAssetUrl for proper basePath and versioning
p = eXeLearning.resolveAssetUrl('/app/common/exe_tooltips/');
} else {
var ref = window.location.href;
// Check if it's the home page
p = "libs/exe_tooltips/";
if (!document.getElementById("exe-index")) p = "../" + p;
// Export context - use relative paths
p = document.getElementById("exe-index") ? "libs/exe_tooltips/" : "../libs/exe_tooltips/";
}
$exe.loadScript(p + "exe_tooltips.js", "$exe.tooltips.init('" + p + "')")
}
Expand Down Expand Up @@ -1461,54 +1450,26 @@ var $exeDevices = {
}

self._loading = true;
// For exports: use relative paths. For workarea: use versioned path if available
var isExport = $("html").prop("id") == "exe-index" || !document.querySelector('script[src*="/app/common/"]');
var basePath;
if (isExport) {
basePath = $("html").prop("id") == "exe-index" ? "./libs/exe_math" : "../libs/exe_math";
} else {
// Workarea: detect version and basePath from script tags
var version = (window.eXeLearning && window.eXeLearning.version) || '';
var configBasePath = '';
// Try to get basePath from parsed config first (if available)
if (window.eXeLearning && window.eXeLearning.config && typeof window.eXeLearning.config === 'object') {
configBasePath = window.eXeLearning.config.basePath || '';
}
// Detect version and basePath from script tags as fallback
var scriptTag = document.querySelector('script[src*="/app/common/"]');
if (scriptTag) {
var src = scriptTag.src;
if (!version) {
var versionMatch = src.match(/\/(v[\d.]+[^/]*)\//);
if (versionMatch) version = versionMatch[1];
}
// Detect basePath from script src if not already set
if (!configBasePath) {
try {
var url = new URL(src);
var pathname = url.pathname;
var appIndex = pathname.indexOf('/app/common/');
if (appIndex > 0) {
var beforeApp = pathname.substring(0, appIndex);
if (version && beforeApp.endsWith('/' + version)) {
configBasePath = beforeApp.substring(0, beforeApp.length - version.length - 1);
} else {
configBasePath = beforeApp;
}
}
} catch (e) {}
}
}
basePath = version ? configBasePath + '/' + version + '/app/common/exe_math' : configBasePath + '/app/common/exe_math';
}
// Detect context: workarea vs export
// In workarea: resolveAssetUrl is defined in template
// In exports: use relative paths (./libs or ../libs)
var isWorkarea = typeof window.eXeLearning?.resolveAssetUrl === 'function';
var isIndex = $("html").prop("id") == "exe-index";
var basePath = isWorkarea
? window.eXeLearning.resolveAssetUrl('/app/common/exe_math')
: (isIndex ? "./libs/exe_math" : "../libs/exe_math");
// Script source: use resolveAssetUrl in workarea, relative path in exports
var scriptSrc = isWorkarea
? window.eXeLearning.resolveAssetUrl('/app/common/exe_math/tex-mml-svg.js')
: (isIndex ? "./libs/exe_math/tex-mml-svg.js" : "../libs/exe_math/tex-mml-svg.js");
if (!window.MathJax) {
window.MathJax = self.engineConfig;
}
if (!window.MathJax.loader) window.MathJax.loader = {};
if (!window.MathJax.loader.paths) window.MathJax.loader.paths = {};
window.MathJax.loader.paths.mathjax = basePath;
var script = document.createElement('script');
script.src = self.engine;
script.src = scriptSrc;
script.async = true;
script.onload = function () {
var checkReady = function () {
Expand Down
Loading
Loading