Initial development for download animation

This commit is contained in:
Gabriel
2025-04-17 17:03:46 +02:00
parent 303e6066e2
commit 99ed27ca61
10 changed files with 436 additions and 0 deletions

View File

@@ -42,4 +42,5 @@ Services.scriptloader.loadSubScript("chrome://browser/content/zen-components/Zen
Services.scriptloader.loadSubScript("chrome://browser/content/zen-components/ZenViewSplitter.mjs", this);
Services.scriptloader.loadSubScript("chrome://browser/content/zen-components/ZenGlanceManager.mjs", this);
Services.scriptloader.loadSubScript("chrome://browser/content/zen-components/ZenMediaController.mjs", this);
Services.scriptloader.loadSubScript("chrome://browser/content/zen-components/ZenDownloadAnimation.mjs", this);
</script>

View File

@@ -69,6 +69,8 @@
content/browser/zen-components/ZenMediaController.mjs (../../zen/media/ZenMediaController.mjs)
content/browser/zen-styles/zen-media-controls.css (../../zen/media/zen-media-controls.css)
content/browser/zen-components/ZenDownloadAnimation.mjs (../../zen/downloads/ZenDownloadAnimation.mjs)
# Images
content/browser/zen-images/gradient.png (../../zen/images/gradient.png)
content/browser/zen-images/brand-header.svg (../../zen/images/brand-header.svg)
@@ -78,6 +80,14 @@
content/browser/zen-images/grain-bg.png (../../zen/images/grain-bg.png)
content/browser/zen-images/note-indicator.svg (../../zen/images/note-indicator.svg)
content/browser/zen-images/downloads-icons/application.svg (../../zen/images/downloads-icons/application.svg)
content/browser/zen-images/downloads-icons/archive.svg (../../zen/images/downloads-icons/archive.svg)
content/browser/zen-images/downloads-icons/audio.svg (../../zen/images/downloads-icons/audio.svg)
content/browser/zen-images/downloads-icons/document.svg (../../zen/images/downloads-icons/document.svg)
content/browser/zen-images/downloads-icons/download.svg (../../zen/images/downloads-icons/download.svg)
content/browser/zen-images/downloads-icons/image.svg (../../zen/images/downloads-icons/image.svg)
content/browser/zen-images/downloads-icons/video.svg (../../zen/images/downloads-icons/video.svg)
# Fonts
content/browser/zen-fonts/JunicodeVF-Italic.woff2 (../../zen/fonts/JunicodeVF-Italic.woff2)
content/browser/zen-fonts/JunicodeVF-Roman.woff2 (../../zen/fonts/JunicodeVF-Roman.woff2)

View File

@@ -0,0 +1,380 @@
var { Downloads } = ChromeUtils.importESModule('resource://gre/modules/Downloads.sys.mjs');
{
const CONFIG = Object.freeze({
ANIMATION: {
APPEAR_DURATION: 400,
FADE_DURATION: 300,
ARC_STEPS: 30,
DISTANCE_MULTIPLIER: 2, // Animation duration = distance * multiplier
MAX_ARC_HEIGHT: 200,
ARC_HEIGHT_RATIO: 0.8, // Arc height = distance * ratio (capped at MAX_ARC_HEIGHT)
SCALE_END: 0.5, // Final scale at destination
},
FILE_TYPES: {
document: ['pdf', 'doc', 'docx', 'txt', 'rtf', 'odt', 'xls', 'xlsx', 'ppt', 'pptx'],
image: ['jpg', 'jpeg', 'png', 'gif', 'svg', 'webp', 'bmp', 'tiff', 'ico'],
audio: ['mp3', 'wav', 'ogg', 'flac', 'aac', 'm4a', 'wma'],
video: ['mp4', 'webm', 'avi', 'mkv', 'mov', 'wmv', 'flv', 'm4v'],
archive: ['zip', 'rar', 'tar', 'gz', '7z', 'bz2', 'xz'],
application: ['exe', 'dmg', 'pkg', 'apk', 'msi', 'deb', 'rpm'],
},
});
class ZenAnimationController {
constructor() {
this._lastClickPosition = null;
this._lastClickTime = 0;
}
async init() {
this._ensureAnimationComponent();
this._setupClickListener();
console.log('Animation controller initialized');
}
_ensureAnimationComponent() {
if (!document.body) {
// Document not ready, try again later
setTimeout(() => this._ensureAnimationComponent(), 100);
return;
}
if (!document.querySelector('zen-download-animation')) {
const downloadAnimation = document.createElement('zen-download-animation');
document.body.appendChild(downloadAnimation);
console.log('Download animation component added to document body');
}
}
_setupClickListener() {
const handleClick = (event) => {
this._lastClickPosition = {
clientX: event.clientX,
clientY: event.clientY,
};
};
// Add regular click listener
document.addEventListener('click', handleClick, true);
// Add right-click (contextmenu) listener
document.addEventListener('contextmenu', handleClick, true);
// Track mousedown events for more reliable position capture
document.addEventListener(
'mousedown',
(event) => {
// Only track right mouse button (button 2)
if (event.button === 2) {
handleClick(event);
}
},
true
);
console.log('Global click and contextmenu listeners registered');
}
getLastClickPosition() {
if (this._lastClickPosition) {
return this._lastClickPosition;
}
return null;
}
getFileTypeFromPath(pathname) {
if (!pathname) return 'generic';
try {
const extension = pathname.split('.').pop().toLowerCase();
// Check each file type category
for (const [type, extensions] of Object.entries(CONFIG.FILE_TYPES)) {
if (extensions.includes(extension)) {
return type;
}
}
} catch (error) {
console.warn('Error parsing URL for file type:', error);
}
return 'generic';
}
animateDownload(startPosition, fileType) {
this._triggerAnimation(startPosition, fileType);
}
_triggerAnimation(startPosition, fileType) {
const animationElement = document.querySelector('zen-download-animation');
if (animationElement) {
animationElement.initializeAnimation(startPosition, fileType);
} else {
console.error('Animation component not found in the DOM');
this._ensureAnimationComponent();
}
}
}
class ZenDownloadAnimationElement extends HTMLElement {
constructor() {
super();
this.attachShadow({ mode: 'open' });
this._createStyles();
}
_createStyles() {
const style = document.createElement('style');
style.textContent = `
:host {
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
pointer-events: none;
z-index: 9999;
}
.download-animation {
position: absolute;
width: 32px;
height: 32px;
background-size: contain;
background-repeat: no-repeat;
background-position: center;
opacity: 0;
transform: translate(-50%, -50%);
will-change: transform, opacity;
}
.document {
background-image: url("chrome://browser/content/zen-images/downloads-icons/document.svg");
}
.image {
background-image: url("chrome://browser/content/zen-images/downloads-icons/image.svg");
}
.audio {
background-image: url("chrome://browser/content/zen-images/downloads-icons/audio.svg");
}
.video {
background-image: url("chrome://browser/content/zen-images/downloads-icons/video.svg");
}
.archive {
background-image: url("chrome://browser/content/zen-images/downloads-icons/archive.svg");
}
.application {
background-image: url("chrome://browser/content/zen-images/downloads-icons/application.svg");
}
.generic {
background-image: url("chrome://browser/content/zen-images/downloads-icons/download.svg");
}
`;
this.shadowRoot.appendChild(style);
}
initializeAnimation(startPosition, fileType) {
if (!startPosition) {
console.log('No start position provided, skipping animation');
return;
}
// Find the download button
const downloadsButton = document.getElementById('downloads-button');
if (!downloadsButton) {
console.warn('Downloads button not found, skipping animation');
return;
}
// Calculate end position (center of downloads button)
const buttonRect = downloadsButton.getBoundingClientRect();
const endPosition = {
clientX: buttonRect.left + buttonRect.width / 2,
clientY: buttonRect.top + buttonRect.height / 2,
};
const animationElement = this._createAnimationElement(startPosition, fileType);
const distance = this._calculateDistance(startPosition, endPosition);
const arcHeight = Math.min(distance * CONFIG.ANIMATION.ARC_HEIGHT_RATIO, CONFIG.ANIMATION.MAX_ARC_HEIGHT);
this._runAnimationSequence(animationElement, startPosition, endPosition, distance, arcHeight, downloadsButton);
}
_createAnimationElement(startPosition, fileType) {
const animationElement = document.createElement('div');
animationElement.className = `download-animation ${fileType || 'generic'}`;
animationElement.style.position = 'absolute';
animationElement.style.left = `${startPosition.clientX}px`;
animationElement.style.top = `${startPosition.clientY}px`;
animationElement.style.opacity = '0';
animationElement.style.transform = 'translate(-50%, -50%)';
this.shadowRoot.appendChild(animationElement);
return animationElement;
}
_calculateDistance(start, end) {
const distanceX = end.clientX - start.clientX;
const distanceY = end.clientY - start.clientY;
return Math.sqrt(distanceX * distanceX + distanceY * distanceY);
}
_runAnimationSequence(element, start, end, distance, arcHeight, downloadsButton) {
try {
const distanceX = end.clientX - start.clientX;
const distanceY = end.clientY - start.clientY;
this._runAnimation(element, distanceX, distanceY, distance, arcHeight, downloadsButton);
} catch (error) {
console.error('Error in animation sequence:', error);
this._cleanupAnimation(element);
}
}
_runAnimation(element, distanceX, distanceY, distance, arcHeight, downloadsButton) {
// Appear with a pop effect
gZenUIManager.motion.animate(
element,
{
opacity: [0, 1],
scale: [0.5, 1.2, 1],
},
{
duration: CONFIG.ANIMATION.APPEAR_DURATION / 1000, // Convert to seconds for motion module
ease: [0.34, 1.56, 0.64, 1], // Spring-like overshoot
onComplete: () => {
// Create the arc trajectory animation
this._createArcAnimation(element, distanceX, distanceY, distance, arcHeight).onfinish = () => {
// Add feedback to the downloads button
this._animateButtonFeedback(downloadsButton);
// Fade out the animation element
this._fadeOutAnimation(element);
};
},
}
);
}
_createArcAnimation(element, distanceX, distanceY, distance, arcHeight) {
const keyframes = [];
const steps = CONFIG.ANIMATION.ARC_STEPS;
for (let i = 0; i <= steps; i++) {
const progress = i / steps;
// Calculate horizontal position (linear)
const x = distanceX * progress;
// Calculate vertical position (parabolic arc)
const adjustedProgress = progress * 2 - 1; // -1 to 1
const verticalOffset = -arcHeight * (1 - adjustedProgress * adjustedProgress);
const y = distanceY * progress + verticalOffset;
// Scale down as it reaches the destination
let scale = 1 - (1 - CONFIG.ANIMATION.SCALE_END) * progress;
keyframes.push({
offset: progress,
transform: `translate(calc(${x}px - 50%), calc(${y}px - 50%)) rotate(0deg) scale(${scale})`,
});
}
return element.animate(keyframes, {
duration: distance * CONFIG.ANIMATION.DISTANCE_MULTIPLIER,
easing: 'cubic-bezier(0.37, 0, 0.63, 1)',
fill: 'forwards',
});
}
_animateButtonFeedback(button) {
button.animate(
[
{ boxShadow: '0 0 0 0 rgba(0, 128, 255, 0)', transform: 'scale(1)' },
{ boxShadow: '0 0 8px 2px rgba(0, 128, 255, 0.5)', transform: 'scale(1.08)' },
{ boxShadow: '0 0 0 0 rgba(0, 128, 255, 0)', transform: 'scale(1)' },
],
{
duration: 500,
easing: 'cubic-bezier(0.34, 1.56, 0.64, 1)',
}
);
}
_fadeOutAnimation(element) {
element.animate([{ opacity: 1 }, { opacity: 0 }], {
duration: CONFIG.ANIMATION.FADE_DURATION,
fill: 'forwards',
}).onfinish = () => this._cleanupAnimation(element);
}
_cleanupAnimation(element) {
if (element && element.parentNode) {
element.parentNode.removeChild(element);
}
}
}
class ZenDownloadAnimation {
constructor() {
this.animationController = new ZenAnimationController();
}
async init() {
console.log('Initializing download manager...');
try {
// Initialize the animation controller
await this.animationController.init();
// Set up download listeners
await this._setupDownloadListeners();
console.log('Download animation initialized successfully');
} catch (error) {
console.error('Failed to initialize download animation:', error);
}
}
async _setupDownloadListeners() {
try {
const list = await Downloads.getList(Downloads.ALL);
list.addView({
onDownloadAdded: (download) => {
console.log('New download detected:', download);
this._handleNewDownload(download);
},
});
console.log('Download listeners set up successfully');
} catch (error) {
console.error('Failed to set up download listeners:', error);
throw error;
}
}
_handleNewDownload(download) {
// Get the last click position
const clickPosition = this.animationController.getLastClickPosition();
console.log('Download initiated:', download.source.url);
if (!clickPosition) {
console.log('No recent click position available for animation');
return;
}
// Get the file type from the URL
const fileType = this.animationController.getFileTypeFromPath(download.target.path);
console.log(`Animating download for ${fileType} file from ${download.source.url}`);
this.animationController.animateDownload(clickPosition, fileType);
}
}
customElements.define('zen-download-animation', ZenDownloadAnimationElement);
const zenDownloadAnimation = new ZenDownloadAnimation();
zenDownloadAnimation.init().catch(console.error);
}

View File

@@ -0,0 +1,7 @@
<svg width="32" height="32" viewBox="0 0 32 32" fill="none" xmlns="http://www.w3.org/2000/svg">
<rect x="6" y="3" width="20" height="26" rx="2" fill="#FFFFFF" stroke="#2C7BE5" stroke-width="2"/>
<rect x="10" y="9" width="5" height="5" rx="1" fill="#2C7BE5"/>
<rect x="17" y="9" width="5" height="5" rx="1" fill="#2C7BE5"/>
<rect x="10" y="18" width="5" height="5" rx="1" fill="#2C7BE5"/>
<rect x="17" y="18" width="5" height="5" rx="1" fill="#2C7BE5"/>
</svg>

After

Width:  |  Height:  |  Size: 470 B

View File

@@ -0,0 +1,9 @@
<svg width="32" height="32" viewBox="0 0 32 32" fill="none" xmlns="http://www.w3.org/2000/svg">
<rect x="6" y="3" width="20" height="26" rx="2" fill="#FFFFFF" stroke="#2C7BE5" stroke-width="2"/>
<rect x="12" y="3" width="8" height="26" fill="#FFFFFF"/>
<path d="M12 7H20" stroke="#2C7BE5" stroke-width="2"/>
<path d="M12 11H20" stroke="#2C7BE5" stroke-width="2"/>
<path d="M12 15H20" stroke="#2C7BE5" stroke-width="2"/>
<path d="M12 19H20" stroke="#2C7BE5" stroke-width="2"/>
<path d="M12 23H20" stroke="#2C7BE5" stroke-width="2"/>
</svg>

After

Width:  |  Height:  |  Size: 553 B

View File

@@ -0,0 +1,7 @@
<svg width="32" height="32" viewBox="0 0 32 32" fill="none" xmlns="http://www.w3.org/2000/svg">
<rect x="6" y="3" width="20" height="26" rx="2" fill="#FFFFFF" stroke="#2C7BE5" stroke-width="2"/>
<path d="M13 10V22" stroke="#2C7BE5" stroke-width="2" stroke-linecap="round"/>
<path d="M19 10V22" stroke="#2C7BE5" stroke-width="2" stroke-linecap="round"/>
<path d="M13 12C11.8954 12 11 12.8954 11 14V18C11 19.1046 11.8954 20 13 20" stroke="#2C7BE5" stroke-width="2"/>
<path d="M19 12C20.1046 12 21 12.8954 21 14V18C21 19.1046 20.1046 20 19 20" stroke="#2C7BE5" stroke-width="2"/>
</svg>

After

Width:  |  Height:  |  Size: 594 B

View File

@@ -0,0 +1,6 @@
<svg width="32" height="32" viewBox="0 0 32 32" fill="none" xmlns="http://www.w3.org/2000/svg">
<rect x="6" y="3" width="20" height="26" rx="2" fill="#FFFFFF" stroke="#2C7BE5" stroke-width="2"/>
<line x1="10" y1="10" x2="22" y2="10" stroke="#2C7BE5" stroke-width="2" stroke-linecap="round"/>
<line x1="10" y1="16" x2="22" y2="16" stroke="#2C7BE5" stroke-width="2" stroke-linecap="round"/>
<line x1="10" y1="22" x2="18" y2="22" stroke="#2C7BE5" stroke-width="2" stroke-linecap="round"/>
</svg>

After

Width:  |  Height:  |  Size: 501 B

View File

@@ -0,0 +1,6 @@
<svg width="32" height="32" viewBox="0 0 32 32" fill="none" xmlns="http://www.w3.org/2000/svg">
<rect x="6" y="3" width="20" height="26" rx="2" fill="#FFFFFF" stroke="#2C7BE5" stroke-width="2"/>
<path d="M16 10V18" stroke="#2C7BE5" stroke-width="2" stroke-linecap="round"/>
<path d="M13 15L16 18L19 15" stroke="#2C7BE5" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M11 22H21" stroke="#2C7BE5" stroke-width="2" stroke-linecap="round"/>
</svg>

After

Width:  |  Height:  |  Size: 480 B

View File

@@ -0,0 +1,5 @@
<svg width="32" height="32" viewBox="0 0 32 32" fill="none" xmlns="http://www.w3.org/2000/svg">
<rect x="6" y="3" width="20" height="26" rx="2" fill="#FFFFFF" stroke="#2C7BE5" stroke-width="2"/>
<circle cx="12" cy="12" r="2" fill="#2C7BE5"/>
<path d="M6 22L13 16L18 21L23 16V27H9C7.34315 27 6 25.6569 6 24V22Z" fill="#2C7BE5"/>
</svg>

After

Width:  |  Height:  |  Size: 341 B

View File

@@ -0,0 +1,5 @@
<svg width="32" height="32" viewBox="0 0 32 32" fill="none" xmlns="http://www.w3.org/2000/svg">
<rect x="6" y="3" width="20" height="26" rx="2" fill="#FFFFFF" stroke="#2C7BE5" stroke-width="2"/>
<rect x="9" y="9" width="14" height="14" rx="2" stroke="#2C7BE5" stroke-width="2"/>
<path d="M17 13.5L14 16L17 18.5V13.5Z" fill="#2C7BE5" stroke="#2C7BE5" stroke-linecap="round" stroke-linejoin="round"/>
</svg>

After

Width:  |  Height:  |  Size: 412 B