Wave changes
0
.gitignore
vendored
Executable file → Normal file
0
.packages/archives/elpa/archive-contents
Executable file → Normal file
0
.packages/archives/elpa/archive-contents.signed
Executable file → Normal file
0
.packages/archives/melpa/archive-contents
Executable file → Normal file
0
.packages/htmlize-20250724.1703/htmlize-autoloads.el
Executable file → Normal file
0
.packages/htmlize-20250724.1703/htmlize-pkg.el
Executable file → Normal file
0
.packages/htmlize-20250724.1703/htmlize.el
Executable file → Normal file
0
.packages/htmlize-20250724.1703/htmlize.elc
Executable file → Normal file
5
.projectile
Normal file
@@ -0,0 +1,5 @@
|
||||
-.venv/
|
||||
-output/
|
||||
-backup/
|
||||
-*~
|
||||
-.packages/
|
||||
10
Makefile
Executable file → Normal file
@@ -4,7 +4,7 @@ VENV := .venv
|
||||
PY := $(VENV)/bin/python
|
||||
PIP := $(VENV)/bin/pip
|
||||
|
||||
all: fix-permissions clean-output build search clean-venv
|
||||
all: clean-output build search clean-venv
|
||||
|
||||
build:
|
||||
@echo "Building project..."
|
||||
@@ -34,7 +34,11 @@ norm:
|
||||
fix-permissions:
|
||||
echo "shakkal123" | sudo -S chown -R zaine:zaine .
|
||||
|
||||
restore-permissions:
|
||||
echo "shakkal123" | sudo -S chown -R 33:33 .
|
||||
|
||||
perm-change-logs:
|
||||
echo "shakkal123" | sudo -S chown -R zaine:zaine org-web-build.log
|
||||
|
||||
# Show help message
|
||||
help:
|
||||
@@ -45,4 +49,6 @@ help:
|
||||
@echo " make clean-venv - Remove venv directory"
|
||||
@echo " make norm - Move all files that end with ~ to the backup folder"
|
||||
@echo " make search - creates the search index"
|
||||
@echo " make help - Show this help message"
|
||||
@echo " make fix-permissions - Fix permissions for the current user"
|
||||
@echo " make restore-permissions - Restore permissions to www-data"
|
||||
@echo " make help - Show this help message!"
|
||||
|
||||
54
Makefile.sync-conflict-20260325-224704-NE5VEIB
Normal file
@@ -0,0 +1,54 @@
|
||||
.PHONY: all clean norm help
|
||||
|
||||
VENV := .venv
|
||||
PY := $(VENV)/bin/python
|
||||
PIP := $(VENV)/bin/pip
|
||||
|
||||
all: clean-output build search clean-venv
|
||||
|
||||
build:
|
||||
@echo "Building project..."
|
||||
emacs -Q --script build-site.el
|
||||
|
||||
search: $(VENV)
|
||||
@echo "Generating search index"
|
||||
$(PY) search-index-json.py
|
||||
|
||||
$(VENV):
|
||||
@echo "Generating virtual environment"
|
||||
python3 -m venv $(VENV)
|
||||
$(PIP) install -r requirements.txt
|
||||
|
||||
clean-output:
|
||||
@echo "Cleaning output directory..."
|
||||
rm -rf output/
|
||||
|
||||
clean-venv:
|
||||
@echo "Cleaning virtual environment..."
|
||||
rm -rf $(VENV)
|
||||
|
||||
norm:
|
||||
@echo "sorting out the backups..."
|
||||
find . -path ./backups -prune -o -type f -name '*~' -exec mv {} backups/ \;
|
||||
|
||||
fix-permissions:
|
||||
echo "shakkal123" | sudo -S chown -R zaine:zaine .
|
||||
|
||||
restore-permissions:
|
||||
echo "shakkal123" | sudo -S chown -R 33:33 .
|
||||
|
||||
perm-change-logs:
|
||||
echo "shakkal123" | sudo -S chown -R zaine:zaine org-web-build.log
|
||||
|
||||
# Show help message
|
||||
help:
|
||||
@echo "Available targets:"
|
||||
@echo " make - Full rebuild"
|
||||
@echo " make build - Rebuild the output directory"
|
||||
@echo " make clean-output - Remove output directory"
|
||||
@echo " make clean-venv - Remove venv directory"
|
||||
@echo " make norm - Move all files that end with ~ to the backup folder"
|
||||
@echo " make search - creates the search index"
|
||||
@echo " make fix-permissions - Fix permissions for the current user"
|
||||
@echo " make restore-permissions - Restore permissions to www-data"
|
||||
@echo " make help - Show this help message!"
|
||||
0
assets/New Document.rnote
Executable file → Normal file
0
assets/emacs-calibre.rnote
Executable file → Normal file
0
assets/icons/apple-touch-icon.png
Executable file → Normal file
|
Before Width: | Height: | Size: 226 KiB After Width: | Height: | Size: 226 KiB |
0
assets/icons/icons8-favicon-50.png
Executable file → Normal file
|
Before Width: | Height: | Size: 1.2 KiB After Width: | Height: | Size: 1.2 KiB |
0
assets/icons/icons8-film-tape-100.png
Executable file → Normal file
|
Before Width: | Height: | Size: 3.8 KiB After Width: | Height: | Size: 3.8 KiB |
0
assets/icons/icons8-z-key-64.png
Executable file → Normal file
|
Before Width: | Height: | Size: 1.1 KiB After Width: | Height: | Size: 1.1 KiB |
0
assets/images/1_to_2.svg
Executable file → Normal file
|
Before Width: | Height: | Size: 163 KiB After Width: | Height: | Size: 163 KiB |
BIN
assets/images/blogs/2026-03-23-install-emacs.png
Normal file
|
After Width: | Height: | Size: 36 KiB |
BIN
assets/images/blogs/2026-03-24-lunch.jpg
Normal file
|
After Width: | Height: | Size: 2.4 MiB |
0
assets/images/stress-in-workplace.png → assets/images/career/2025-10-10-stress-in-workplace.png
Executable file → Normal file
|
Before Width: | Height: | Size: 66 KiB After Width: | Height: | Size: 66 KiB |
0
assets/images/user_stories.jpg → assets/images/career/2025-10-10-user_stories.jpg
Executable file → Normal file
|
Before Width: | Height: | Size: 929 KiB After Width: | Height: | Size: 929 KiB |
0
assets/images/retrospective_boat_model.png → assets/images/career/2025-11-05-retrospective_boat_model.png
Executable file → Normal file
|
Before Width: | Height: | Size: 291 KiB After Width: | Height: | Size: 291 KiB |
|
Before Width: | Height: | Size: 508 KiB After Width: | Height: | Size: 508 KiB |
0
assets/images/diagrams/airflow.png → assets/images/career/2025-11-15-airflow.png
Executable file → Normal file
|
Before Width: | Height: | Size: 88 KiB After Width: | Height: | Size: 88 KiB |
BIN
assets/images/career/2025-12-02-retros.jpg
Normal file
|
After Width: | Height: | Size: 284 KiB |
0
assets/images/diagrams/airflow-flow.png → assets/images/career/2026-01-17-airflow-flow.png
Executable file → Normal file
|
Before Width: | Height: | Size: 64 KiB After Width: | Height: | Size: 64 KiB |
0
assets/images/diagrams/datamart-high-level.png → assets/images/career/2026-01-17-datamart-high-level.png
Executable file → Normal file
|
Before Width: | Height: | Size: 44 KiB After Width: | Height: | Size: 44 KiB |
0
assets/images/diagrams/high-level-dora.png → assets/images/career/2026-01-17-high-level-dora.png
Executable file → Normal file
|
Before Width: | Height: | Size: 38 KiB After Width: | Height: | Size: 38 KiB |
|
Before Width: | Height: | Size: 138 KiB After Width: | Height: | Size: 138 KiB |
0
assets/images/owasp/Screenshot 2025-10-16 134403.png → assets/images/career/owasp/Screenshot 2025-10-16 134403.png
Executable file → Normal file
|
Before Width: | Height: | Size: 86 KiB After Width: | Height: | Size: 86 KiB |
0
assets/images/owasp/Screenshot 2025-10-16 134438.png → assets/images/career/owasp/Screenshot 2025-10-16 134438.png
Executable file → Normal file
|
Before Width: | Height: | Size: 118 KiB After Width: | Height: | Size: 118 KiB |
0
assets/images/owasp/Screenshot 2025-10-16 134457.png → assets/images/career/owasp/Screenshot 2025-10-16 134457.png
Executable file → Normal file
|
Before Width: | Height: | Size: 62 KiB After Width: | Height: | Size: 62 KiB |
0
assets/images/owasp/Screenshot 2025-10-16 134512.png → assets/images/career/owasp/Screenshot 2025-10-16 134512.png
Executable file → Normal file
|
Before Width: | Height: | Size: 96 KiB After Width: | Height: | Size: 96 KiB |
0
assets/images/owasp/swappy-20251106-214613.png → assets/images/career/owasp/swappy-20251106-214613.png
Executable file → Normal file
|
Before Width: | Height: | Size: 103 KiB After Width: | Height: | Size: 103 KiB |
0
assets/images/owasp/swappy-20251106-214648.png → assets/images/career/owasp/swappy-20251106-214648.png
Executable file → Normal file
|
Before Width: | Height: | Size: 104 KiB After Width: | Height: | Size: 104 KiB |
0
assets/images/owasp/swappy-20251106-214700.png → assets/images/career/owasp/swappy-20251106-214700.png
Executable file → Normal file
|
Before Width: | Height: | Size: 113 KiB After Width: | Height: | Size: 113 KiB |
0
assets/images/owasp/swappy-20251106-214709.png → assets/images/career/owasp/swappy-20251106-214709.png
Executable file → Normal file
|
Before Width: | Height: | Size: 98 KiB After Width: | Height: | Size: 98 KiB |
0
assets/images/owasp/swappy-20251106-214717.png → assets/images/career/owasp/swappy-20251106-214717.png
Executable file → Normal file
|
Before Width: | Height: | Size: 35 KiB After Width: | Height: | Size: 35 KiB |
0
assets/images/wireframes/Screenshot 2025-10-31 112139.png → assets/images/career/wireframes/Screenshot 2025-10-31 112139.png
Executable file → Normal file
|
Before Width: | Height: | Size: 27 KiB After Width: | Height: | Size: 27 KiB |
0
assets/images/wireframes/Screenshot 2025-10-31 112206.png → assets/images/career/wireframes/Screenshot 2025-10-31 112206.png
Executable file → Normal file
|
Before Width: | Height: | Size: 59 KiB After Width: | Height: | Size: 59 KiB |
0
assets/images/wireframes/Screenshot 2025-10-31 112427.png → assets/images/career/wireframes/Screenshot 2025-10-31 112427.png
Executable file → Normal file
|
Before Width: | Height: | Size: 57 KiB After Width: | Height: | Size: 57 KiB |
0
assets/images/wireframes/Screenshot 2025-10-31 115621.png → assets/images/career/wireframes/Screenshot 2025-10-31 115621.png
Executable file → Normal file
|
Before Width: | Height: | Size: 46 KiB After Width: | Height: | Size: 46 KiB |
0
assets/images/wireframes/Screenshot 2025-10-31 115949.png → assets/images/career/wireframes/Screenshot 2025-10-31 115949.png
Executable file → Normal file
|
Before Width: | Height: | Size: 65 KiB After Width: | Height: | Size: 65 KiB |
0
assets/images/wireframes/Screenshot 2025-10-31 120038.png → assets/images/career/wireframes/Screenshot 2025-10-31 120038.png
Executable file → Normal file
|
Before Width: | Height: | Size: 75 KiB After Width: | Height: | Size: 75 KiB |
0
assets/images/wireframes/Screenshot 2025-10-31 120203.png → assets/images/career/wireframes/Screenshot 2025-10-31 120203.png
Executable file → Normal file
|
Before Width: | Height: | Size: 68 KiB After Width: | Height: | Size: 68 KiB |
0
assets/images/gr.png
Executable file → Normal file
|
Before Width: | Height: | Size: 27 KiB After Width: | Height: | Size: 27 KiB |
0
assets/images/hzone/patpat.png
Executable file → Normal file
|
Before Width: | Height: | Size: 203 KiB After Width: | Height: | Size: 203 KiB |
0
assets/images/org-roam-ui-graph.png
Executable file → Normal file
|
Before Width: | Height: | Size: 296 KiB After Width: | Height: | Size: 296 KiB |
0
assets/images/reviews/02-12-25-retros.jpg
Executable file → Normal file
|
Before Width: | Height: | Size: 284 KiB After Width: | Height: | Size: 284 KiB |
0
assets/images/reviews/07-12-25-daily-retros.jpg
Executable file → Normal file
|
Before Width: | Height: | Size: 1.4 MiB After Width: | Height: | Size: 1.4 MiB |
0
assets/images/reviews/15-11-2025-guac.png
Executable file → Normal file
|
Before Width: | Height: | Size: 1.1 MiB After Width: | Height: | Size: 1.1 MiB |
0
assets/images/reviews/16-11-25-daily-retros.jpg
Executable file → Normal file
|
Before Width: | Height: | Size: 1.3 MiB After Width: | Height: | Size: 1.3 MiB |
0
assets/images/reviews/23-11-2025-netdata.png
Executable file → Normal file
|
Before Width: | Height: | Size: 142 KiB After Width: | Height: | Size: 142 KiB |
0
assets/images/reviews/23-11-2025-reader.png
Executable file → Normal file
|
Before Width: | Height: | Size: 1.4 MiB After Width: | Height: | Size: 1.4 MiB |
0
assets/images/reviews/23-11-2025-vnc.png
Executable file → Normal file
|
Before Width: | Height: | Size: 783 KiB After Width: | Height: | Size: 783 KiB |
0
assets/images/reviews/23-11-25-daily-retros.jpg
Executable file → Normal file
|
Before Width: | Height: | Size: 3.2 MiB After Width: | Height: | Size: 3.2 MiB |
0
assets/images/reviews/30-11-2025-acutal.png
Executable file → Normal file
|
Before Width: | Height: | Size: 147 KiB After Width: | Height: | Size: 147 KiB |
0
assets/images/reviews/30-11-25-daily-retros.jpg
Executable file → Normal file
|
Before Width: | Height: | Size: 2.4 MiB After Width: | Height: | Size: 2.4 MiB |
0
assets/images/reviews/PDT-bruno-vehicle-web-api.png
Executable file → Normal file
|
Before Width: | Height: | Size: 29 KiB After Width: | Height: | Size: 29 KiB |
0
assets/images/reviews/c-sharp-vehicle-breakpoints.png
Executable file → Normal file
|
Before Width: | Height: | Size: 438 KiB After Width: | Height: | Size: 438 KiB |
0
assets/images/competencies/competencies-09-11-25.png → assets/images/reviews/competencies/competencies-09-11-25.png
Executable file → Normal file
|
Before Width: | Height: | Size: 254 KiB After Width: | Height: | Size: 254 KiB |
0
assets/images/competencies/dev-plan-09-11-25.png → assets/images/reviews/competencies/dev-plan-09-11-25.png
Executable file → Normal file
|
Before Width: | Height: | Size: 230 KiB After Width: | Height: | Size: 230 KiB |
0
assets/images/reviews/probation-objectives/probation-objectives-1.png
Executable file → Normal file
|
Before Width: | Height: | Size: 128 KiB After Width: | Height: | Size: 128 KiB |
0
assets/images/reviews/probation-objectives/probation-objectives-2.png
Executable file → Normal file
|
Before Width: | Height: | Size: 128 KiB After Width: | Height: | Size: 128 KiB |
0
assets/images/reviews/probation-objectives/probation-objectives-3.png
Executable file → Normal file
|
Before Width: | Height: | Size: 123 KiB After Width: | Height: | Size: 123 KiB |
0
assets/images/reviews/probation-objectives/probation-objectives-4.png
Executable file → Normal file
|
Before Width: | Height: | Size: 118 KiB After Width: | Height: | Size: 118 KiB |
0
assets/images/reviews/probation-objectives/probation-objectives-5.png
Executable file → Normal file
|
Before Width: | Height: | Size: 32 KiB After Width: | Height: | Size: 32 KiB |
0
assets/images/reviews/probation-objectives/rotation-calendar.png
Executable file → Normal file
|
Before Width: | Height: | Size: 81 KiB After Width: | Height: | Size: 81 KiB |
BIN
assets/images/reviews/timesheets/2026-03-22-timesheet.png
Normal file
|
After Width: | Height: | Size: 19 KiB |
0
assets/images/reviews/timesheets/timesheet-01-02.png
Executable file → Normal file
|
Before Width: | Height: | Size: 11 KiB After Width: | Height: | Size: 11 KiB |
0
assets/images/reviews/timesheets/timesheet-08-02.png
Executable file → Normal file
|
Before Width: | Height: | Size: 13 KiB After Width: | Height: | Size: 13 KiB |
0
assets/images/syntax/assets.png
Executable file → Normal file
|
Before Width: | Height: | Size: 226 KiB After Width: | Height: | Size: 226 KiB |
0
assets/images/syntax/filebrowser.png
Executable file → Normal file
|
Before Width: | Height: | Size: 564 KiB After Width: | Height: | Size: 564 KiB |
0
assets/images/syntax/not_clutter.png
Executable file → Normal file
|
Before Width: | Height: | Size: 192 KiB After Width: | Height: | Size: 192 KiB |
0
assets/rnote/07-12-2025-retrospectives.rnote
Executable file → Normal file
0
assets/rnote/09-11-2025-nginx-architecture.rnote
Executable file → Normal file
0
assets/rnote/09-11-2025-retrospectives.rnote
Executable file → Normal file
0
assets/rnote/10-08-2025-weekly-review.rnote
Executable file → Normal file
0
assets/rnote/2025-08-08-emacs.rnote
Executable file → Normal file
0
assets/rnote/23-11-2025-retrospectives.rnote
Executable file → Normal file
0
assets/rnote/30-11-2025-retrospectives.rnote
Executable file → Normal file
0
assets/rnote/retros-template.rnote
Executable file → Normal file
0
assets/scripts/bigger-picture.min.js
vendored
Executable file → Normal file
0
assets/scripts/comments.js
Executable file → Normal file
0
assets/scripts/competency-status-board.js
Executable file → Normal file
0
assets/scripts/gallery-init.js
Executable file → Normal file
0
assets/scripts/lunr.js
Executable file → Normal file
2811
assets/scripts/mermaid.min.js
vendored
Normal file
0
assets/scripts/notes.js
Executable file → Normal file
61
assets/scripts/script.js
Executable file → Normal file
@@ -225,3 +225,64 @@ document.addEventListener("DOMContentLoaded", () => {
|
||||
});
|
||||
|
||||
|
||||
// Make Mermaid diagrams zoomable
|
||||
window.addEventListener('load', function() {
|
||||
// Initialize Mermaid (if not already)
|
||||
mermaid.initialize({ startOnLoad: false, theme: 'neutral' });
|
||||
|
||||
// Find all Mermaid divs
|
||||
document.querySelectorAll('.mermaid').forEach(el => {
|
||||
// Decode HTML entities
|
||||
el.innerHTML = el.innerHTML
|
||||
.replace(/>/g, '>')
|
||||
.replace(/</g, '<')
|
||||
.replace(/&/g, '&');
|
||||
|
||||
// Wrap in container if not already wrapped
|
||||
if (!el.parentElement.classList.contains('mermaid-container')) {
|
||||
const container = document.createElement('div');
|
||||
container.classList.add('mermaid-container');
|
||||
|
||||
// Add zoom controls
|
||||
const controls = document.createElement('div');
|
||||
controls.classList.add('mermaid-zoom-controls');
|
||||
controls.innerHTML = `
|
||||
<button class="zoom-in">+</button>
|
||||
<button class="zoom-out">-</button>
|
||||
`;
|
||||
container.appendChild(controls);
|
||||
|
||||
// Move the mermaid diagram into container
|
||||
container.appendChild(el.cloneNode(true));
|
||||
el.replaceWith(container);
|
||||
|
||||
// Select the SVG inside
|
||||
const svg = container.querySelector('.mermaid svg');
|
||||
let scale = 1;
|
||||
|
||||
// Zoom functions
|
||||
controls.querySelector('.zoom-in').addEventListener('click', () => {
|
||||
scale += 0.1;
|
||||
svg.style.transform = `scale(${scale})`;
|
||||
});
|
||||
|
||||
controls.querySelector('.zoom-out').addEventListener('click', () => {
|
||||
scale = Math.max(0.1, scale - 0.1);
|
||||
svg.style.transform = `scale(${scale})`;
|
||||
});
|
||||
|
||||
// Optional: scroll wheel zoom
|
||||
container.addEventListener('wheel', (e) => {
|
||||
if (e.ctrlKey) { // zoom only with ctrl+wheel
|
||||
e.preventDefault();
|
||||
if (e.deltaY < 0) scale += 0.05;
|
||||
else scale = Math.max(0.1, scale - 0.05);
|
||||
svg.style.transform = `scale(${scale})`;
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
// Finally, render Mermaid diagrams
|
||||
mermaid.run({ querySelector: '.mermaid' });
|
||||
});
|
||||
|
||||
0
assets/scripts/search.js
Executable file → Normal file
0
assets/scripts/svg-pan-zoom.min.js
vendored
Executable file → Normal file
@@ -174,8 +174,8 @@
|
||||
const motalahForm = $("#motalah-form");
|
||||
const motalahDateEl = $("#motalah-date");
|
||||
const motalahDurEl = $("#motalah-duration");
|
||||
const motalahBooksEl= $("#motalah-books");
|
||||
const motalahNotesEl= $("#motalah-notes");
|
||||
const motalahBooksEl = $("#motalah-books");
|
||||
const motalahNotesEl = $("#motalah-notes");
|
||||
|
||||
// ── Book search UI ────────────────────────────────────────
|
||||
// selectedBookIds: Set of ids currently picked in the modal
|
||||
@@ -229,6 +229,7 @@
|
||||
bookSearchResults.classList.add("open");
|
||||
}
|
||||
|
||||
if (bookSearchInput) {
|
||||
bookSearchInput.addEventListener("input", () => {
|
||||
if (calibreBooks.length === 0) return;
|
||||
renderBookResults(bookSearchInput.value);
|
||||
@@ -252,7 +253,9 @@
|
||||
renderSelectedChips();
|
||||
renderBookResults(bookSearchInput.value);
|
||||
});
|
||||
}
|
||||
|
||||
if (selectedChipsWrap) {
|
||||
selectedChipsWrap.addEventListener("click", e => {
|
||||
const btn = e.target.closest("button[data-id]");
|
||||
if (!btn) return;
|
||||
@@ -263,6 +266,7 @@
|
||||
renderBookResults(bookSearchInput.value);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Close dropdown on outside click
|
||||
document.addEventListener("click", e => {
|
||||
@@ -285,7 +289,7 @@
|
||||
renderSelectedChips();
|
||||
motalahModal.showModal();
|
||||
}
|
||||
|
||||
if (motalahForm) {
|
||||
motalahForm.addEventListener("submit", async e => {
|
||||
e.preventDefault();
|
||||
const selectedBooks = [...selectedBookIds];
|
||||
@@ -310,6 +314,7 @@
|
||||
alert("Error saving session: " + err.message);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
$("#motalah-cancel").addEventListener("click", () => motalahModal.close());
|
||||
$("#log-motalah-btn").addEventListener("click", openMotalahModal);
|
||||
@@ -323,14 +328,14 @@
|
||||
// ── Boot ─────────────────────────────────────────────────
|
||||
document.addEventListener("DOMContentLoaded", async () => {
|
||||
setTodayLabel();
|
||||
await Promise.all([loadAll(), loadMotalah()]); // ← change this line
|
||||
await Promise.all([loadAll(), loadMotalah()]);
|
||||
renderToday();
|
||||
renderNafl();
|
||||
renderMeetingPanel();
|
||||
renderKhatm();
|
||||
renderHistory();
|
||||
renderChart();
|
||||
renderMotalah(); // ← add this
|
||||
renderMotalah();
|
||||
renderHeatmap();
|
||||
bindControls();
|
||||
});
|
||||
@@ -437,7 +442,7 @@
|
||||
}
|
||||
|
||||
function buildRatingCard(meta, val) {
|
||||
const pips = [1,2,3,4,5].map(n => {
|
||||
const pips = [1, 2, 3, 4, 5].map(n => {
|
||||
const filled = val >= n;
|
||||
const isMin = n === meta.target;
|
||||
return `<span class="card-pip ${filled ? "filled" : ""} ${isMin ? "min-marker" : ""}"
|
||||
@@ -507,9 +512,12 @@
|
||||
}
|
||||
}
|
||||
|
||||
if (typeEl){
|
||||
typeEl.addEventListener("change", updateModalFields);
|
||||
}
|
||||
$("#modal-cancel").addEventListener("click", () => modal.close());
|
||||
|
||||
if (form){
|
||||
form.addEventListener("submit", async (e) => {
|
||||
e.preventDefault();
|
||||
const type = typeEl.value;
|
||||
@@ -541,7 +549,7 @@
|
||||
alert("Could not save entry: " + err.message);
|
||||
}
|
||||
});
|
||||
|
||||
}
|
||||
// ════════════════════════════════════════════════════════
|
||||
// NAFL PRAYERS
|
||||
// Stored in wird_entries: wirdType = "salatul_tawbah" |
|
||||
@@ -717,7 +725,7 @@
|
||||
if (entries.length >= 2) {
|
||||
let gap = 0;
|
||||
for (let i = 1; i < entries.length; i++)
|
||||
gap += daysBetween(entries[i-1].date, entries[i].date);
|
||||
gap += daysBetween(entries[i - 1].date, entries[i].date);
|
||||
avgDays = Math.round(gap / (entries.length - 1));
|
||||
}
|
||||
|
||||
@@ -871,7 +879,7 @@
|
||||
} else {
|
||||
const dayEntries = allEntries.filter(e => e.date === iso && e.wirdType === type);
|
||||
if (isRating) {
|
||||
const last = dayEntries.sort((a,b) => a.createdAt > b.createdAt ? 1 : -1).pop();
|
||||
const last = dayEntries.sort((a, b) => a.createdAt > b.createdAt ? 1 : -1).pop();
|
||||
vals.push(last ? Number(last.value) : 0);
|
||||
} else {
|
||||
vals.push(dayEntries.reduce((s, e) => s + Number(e.value), 0));
|
||||
@@ -882,8 +890,8 @@
|
||||
const nonZero = vals.filter(v => v > 0);
|
||||
const avg = nonZero.length
|
||||
? (isRating
|
||||
? (nonZero.reduce((a,b)=>a+b,0)/nonZero.length).toFixed(1)
|
||||
: Math.round(nonZero.reduce((a,b)=>a+b,0)/nonZero.length))
|
||||
? (nonZero.reduce((a, b) => a + b, 0) / nonZero.length).toFixed(1)
|
||||
: Math.round(nonZero.reduce((a, b) => a + b, 0) / nonZero.length))
|
||||
: 0;
|
||||
const max = nonZero.length ? Math.max(...nonZero) : 0;
|
||||
const streak = calcStreak(type);
|
||||
@@ -927,7 +935,7 @@
|
||||
label: "Target",
|
||||
data: vals.map(() => meta.target),
|
||||
borderColor: "rgba(74,124,89,.5)",
|
||||
borderDash: [5,4],
|
||||
borderDash: [5, 4],
|
||||
pointRadius: 0, fill: false, tension: 0,
|
||||
},
|
||||
],
|
||||
@@ -986,7 +994,7 @@
|
||||
} else {
|
||||
const entries = allEntries.filter(e => e.date === iso && e.wirdType === type);
|
||||
if (meta && meta.type === "rating") {
|
||||
const last = entries.sort((a,b) => a.createdAt > b.createdAt ? 1 : -1).pop();
|
||||
const last = entries.sort((a, b) => a.createdAt > b.createdAt ? 1 : -1).pop();
|
||||
total = last ? Number(last.value) : 0;
|
||||
} else {
|
||||
total = entries.reduce((s, e) => s + Number(e.value), 0);
|
||||
@@ -1021,8 +1029,7 @@
|
||||
const monthLabelEl = $("#heatmap-month-labels");
|
||||
monthLabelEl.innerHTML = "";
|
||||
let lastMonth = null;
|
||||
// 15px per col (12px cell + 3px gap)
|
||||
let colsPassed = 0;
|
||||
// 15px per col (12px cell + 3px gap); labels are position:absolute
|
||||
for (let w = 0; w < HEATMAP_WEEKS; w++) {
|
||||
const d = new Date(startDate);
|
||||
d.setDate(d.getDate() + w * 7);
|
||||
@@ -1030,10 +1037,9 @@
|
||||
if (mo !== lastMonth) {
|
||||
const lbl = document.createElement("span");
|
||||
lbl.className = "hm-month-lbl";
|
||||
lbl.style.paddingLeft = (w - colsPassed) * 15 + "px";
|
||||
lbl.style.left = w * 15 + "px";
|
||||
lbl.textContent = mo;
|
||||
monthLabelEl.appendChild(lbl);
|
||||
colsPassed = w;
|
||||
lastMonth = mo;
|
||||
}
|
||||
}
|
||||
@@ -1226,5 +1232,415 @@
|
||||
$("#trend-range").addEventListener("change", renderChart);
|
||||
$("#heatmap-type").addEventListener("change", renderHeatmap);
|
||||
}
|
||||
/* ============================================================
|
||||
wird-export.js
|
||||
Client-side PDF export for the Wird Tracker.
|
||||
Depends on: jsPDF + jspdf-autotable (loaded via CDN).
|
||||
All data is read from allEntries / motalahEntries already
|
||||
held in memory by wird-tracker.js — no extra API calls.
|
||||
|
||||
HOW TO INTEGRATE
|
||||
─────────────────
|
||||
1. Paste this block INSIDE the existing IIFE in
|
||||
wird-tracker.js, just before the closing })();
|
||||
|
||||
2. Add the HTML snippet (wird-export-html.html) to the
|
||||
template and the CDN <script> tags after chart.js.
|
||||
============================================================ */
|
||||
|
||||
// ── Export modal elements ─────────────────────────────────
|
||||
const exportModal = document.getElementById("export-modal");
|
||||
const exportForm = document.getElementById("export-form");
|
||||
const exportFromEl = document.getElementById("export-from");
|
||||
const exportToEl = document.getElementById("export-to");
|
||||
const exportBtnLabel = document.getElementById("export-btn-label");
|
||||
const exportSubmitBtn = document.getElementById("export-submit");
|
||||
|
||||
// ── Open / close ──────────────────────────────────────────
|
||||
document.getElementById("export-pdf-btn").addEventListener("click", () => {
|
||||
const today = isoDate();
|
||||
const thirtyDaysAgo = (() => {
|
||||
const d = new Date(); d.setDate(d.getDate() - 30); return isoDate(d);
|
||||
})();
|
||||
exportFromEl.value = thirtyDaysAgo;
|
||||
exportToEl.value = today;
|
||||
exportModal.showModal();
|
||||
});
|
||||
|
||||
document.getElementById("export-cancel").addEventListener("click", () => exportModal.close());
|
||||
|
||||
// ── Preset buttons ────────────────────────────────────────
|
||||
document.querySelectorAll(".export-preset").forEach(btn => {
|
||||
btn.addEventListener("click", () => {
|
||||
const days = parseInt(btn.dataset.preset);
|
||||
const to = new Date();
|
||||
const from = new Date();
|
||||
|
||||
if (days === 365) {
|
||||
// "This year" = Jan 1 of current year
|
||||
from.setMonth(0, 1);
|
||||
} else {
|
||||
from.setDate(from.getDate() - (days - 1));
|
||||
}
|
||||
|
||||
exportFromEl.value = isoDate(from);
|
||||
exportToEl.value = isoDate(to);
|
||||
|
||||
// Visual feedback on active preset
|
||||
document.querySelectorAll(".export-preset").forEach(b => b.classList.remove("active"));
|
||||
btn.classList.add("active");
|
||||
});
|
||||
});
|
||||
|
||||
// Clear active preset when user manually changes dates
|
||||
[exportFromEl, exportToEl].forEach(el => {
|
||||
el.addEventListener("change", () => {
|
||||
document.querySelectorAll(".export-preset").forEach(b => b.classList.remove("active"));
|
||||
});
|
||||
});
|
||||
|
||||
// ── Generate PDF ──────────────────────────────────────────
|
||||
exportForm.addEventListener("submit", async e => {
|
||||
e.preventDefault();
|
||||
|
||||
const fromIso = exportFromEl.value;
|
||||
const toIso = exportToEl.value;
|
||||
|
||||
if (!fromIso || !toIso || fromIso > toIso) {
|
||||
alert("Please select a valid date range.");
|
||||
return;
|
||||
}
|
||||
|
||||
const includeNafl = document.getElementById("export-include-nafl").checked;
|
||||
const includeKhatm = document.getElementById("export-include-khatm").checked;
|
||||
const includeMotalah = document.getElementById("export-include-motalah").checked;
|
||||
|
||||
// Loading state
|
||||
exportSubmitBtn.disabled = true;
|
||||
exportBtnLabel.textContent = "Generating…";
|
||||
|
||||
try {
|
||||
await generatePDF({ fromIso, toIso, includeNafl, includeKhatm, includeMotalah });
|
||||
exportModal.close();
|
||||
} catch (err) {
|
||||
console.error("PDF export error:", err);
|
||||
alert("Could not generate PDF: " + err.message);
|
||||
} finally {
|
||||
exportSubmitBtn.disabled = false;
|
||||
exportBtnLabel.textContent = "Download PDF";
|
||||
}
|
||||
});
|
||||
|
||||
// ── Core PDF generation ───────────────────────────────────
|
||||
async function generatePDF({ fromIso, toIso, includeNafl, includeKhatm, includeMotalah }) {
|
||||
const { jsPDF } = window.jspdf;
|
||||
const doc = new jsPDF({ orientation: "portrait", unit: "mm", format: "a4" });
|
||||
|
||||
const PAGE_W = 210;
|
||||
const MARGIN = 16;
|
||||
const COL_W = PAGE_W - MARGIN * 2;
|
||||
const ACCENT = [124, 92, 58]; // --wird-accent (light mode safe)
|
||||
const MUTED = [130, 130, 130];
|
||||
const FG = [30, 30, 30];
|
||||
const BORDER = [220, 215, 208];
|
||||
|
||||
// ── Cover / header ─────────────────────────────────────
|
||||
doc.setFillColor(...ACCENT);
|
||||
doc.rect(0, 0, PAGE_W, 28, "F");
|
||||
|
||||
doc.setFont("helvetica", "bold");
|
||||
doc.setFontSize(16);
|
||||
doc.setTextColor(255, 255, 255);
|
||||
doc.text("Wird Tracker — Export", MARGIN, 16);
|
||||
|
||||
doc.setFont("helvetica", "normal");
|
||||
doc.setFontSize(8);
|
||||
doc.setTextColor(255, 255, 255, 0.75);
|
||||
doc.text(
|
||||
`${fmtDisplay(fromIso)} → ${fmtDisplay(toIso)} · generated ${fmtDisplay(isoDate())}`,
|
||||
MARGIN, 23
|
||||
);
|
||||
|
||||
let cursorY = 38;
|
||||
|
||||
// ── Helper: section heading ────────────────────────────
|
||||
function sectionHeading(title, y) {
|
||||
doc.setFillColor(...BORDER);
|
||||
doc.rect(MARGIN, y, COL_W, 7, "F");
|
||||
doc.setFont("helvetica", "bold");
|
||||
doc.setFontSize(8.5);
|
||||
doc.setTextColor(...ACCENT);
|
||||
doc.text(title.toUpperCase(), MARGIN + 3, y + 5);
|
||||
return y + 10;
|
||||
}
|
||||
|
||||
// ── Helper: summary stat row ───────────────────────────
|
||||
function statRow(label, value, y) {
|
||||
doc.setFont("helvetica", "normal");
|
||||
doc.setFontSize(8.5);
|
||||
doc.setTextColor(...MUTED);
|
||||
doc.text(label, MARGIN + 3, y);
|
||||
doc.setFont("helvetica", "bold");
|
||||
doc.setTextColor(...FG);
|
||||
doc.text(String(value), MARGIN + 60, y);
|
||||
return y + 6;
|
||||
}
|
||||
|
||||
// ── Helper: autoTable wrapper ──────────────────────────
|
||||
function addTable(columns, rows, startY) {
|
||||
doc.autoTable({
|
||||
startY,
|
||||
head: [columns.map(c => c.header)],
|
||||
body: rows,
|
||||
margin: { left: MARGIN, right: MARGIN },
|
||||
styles: {
|
||||
font: "helvetica",
|
||||
fontSize: 8,
|
||||
cellPadding: 2.5,
|
||||
textColor: FG,
|
||||
lineColor: BORDER,
|
||||
lineWidth: 0.2,
|
||||
overflow: "linebreak",
|
||||
},
|
||||
headStyles: {
|
||||
fillColor: BORDER,
|
||||
textColor: ACCENT,
|
||||
fontStyle: "bold",
|
||||
fontSize: 7.5,
|
||||
},
|
||||
alternateRowStyles: { fillColor: [250, 248, 245] },
|
||||
columnStyles: columns.reduce((acc, c, i) => {
|
||||
if (c.width) acc[i] = { cellWidth: c.width };
|
||||
return acc;
|
||||
}, {}),
|
||||
});
|
||||
return doc.lastAutoTable.finalY + 8;
|
||||
}
|
||||
|
||||
// ── 1. Daily Wirds summary ─────────────────────────────
|
||||
cursorY = sectionHeading("Daily Awrād", cursorY);
|
||||
|
||||
// Build per-day totals for the range
|
||||
const dayMap = {}; // { "2025-01-14": { durood: 300, istighfar: 100, ... } }
|
||||
for (const e of allEntries) {
|
||||
if (e.date < fromIso || e.date > toIso) continue;
|
||||
if (!WIRD_META[e.wirdType]) continue;
|
||||
if (!includeNafl && NAFL_WIRD[e.wirdType]) continue;
|
||||
if (!includeKhatm && e.wirdType === "khatm") continue;
|
||||
if (!dayMap[e.date]) dayMap[e.date] = {};
|
||||
const meta = WIRD_META[e.wirdType];
|
||||
if (meta && meta.type === "rating") {
|
||||
dayMap[e.date][e.wirdType] = Math.max(
|
||||
dayMap[e.date][e.wirdType] || 0, Number(e.value)
|
||||
);
|
||||
} else {
|
||||
dayMap[e.date][e.wirdType] = (dayMap[e.date][e.wirdType] || 0) + Number(e.value);
|
||||
}
|
||||
}
|
||||
|
||||
// Build summary stats
|
||||
const daysInRange = Object.keys(dayMap).length;
|
||||
const dailyTypes = Object.keys(DAILY_WIRD);
|
||||
const daysAllMet = Object.values(dayMap).filter(dm =>
|
||||
dailyTypes.every(t => (dm[t] || 0) >= DAILY_WIRD[t].target)
|
||||
).length;
|
||||
|
||||
cursorY = statRow("Date range", `${fmtDisplay(fromIso)} → ${fmtDisplay(toIso)}`, cursorY);
|
||||
cursorY = statRow("Days with entries", daysInRange, cursorY);
|
||||
cursorY = statRow("Days all targets met", daysAllMet, cursorY);
|
||||
cursorY += 3;
|
||||
|
||||
// Per-wird totals table
|
||||
const summaryRows = dailyTypes.map(type => {
|
||||
const meta = DAILY_WIRD[type];
|
||||
const values = Object.values(dayMap).map(dm => dm[type] || 0).filter(v => v > 0);
|
||||
const total = meta.type === "rating"
|
||||
? (values.length ? (values.reduce((a,b)=>a+b,0)/values.length).toFixed(1) + " avg" : "—")
|
||||
: values.reduce((a,b)=>a+b,0).toLocaleString();
|
||||
const days = values.length;
|
||||
const target = meta.type === "rating"
|
||||
? `≥ ${meta.target} / 5`
|
||||
: `${meta.target.toLocaleString()} ${meta.unit}`;
|
||||
const metDays = meta.type === "rating"
|
||||
? values.filter(v => v >= meta.target).length
|
||||
: values.filter(v => v >= meta.target).length;
|
||||
return [meta.label, total, target, `${metDays} / ${days}`, days ? `${Math.round(metDays/days*100)}%` : "—"];
|
||||
});
|
||||
|
||||
cursorY = addTable(
|
||||
[
|
||||
{ header: "Wird", width: 42 },
|
||||
{ header: "Total / Avg", width: 30 },
|
||||
{ header: "Target", width: 35 },
|
||||
{ header: "Days Met", width: 25 },
|
||||
{ header: "Hit %", width: 20 },
|
||||
],
|
||||
summaryRows,
|
||||
cursorY
|
||||
);
|
||||
|
||||
// ── 2. Detailed daily log ──────────────────────────────
|
||||
if (doc.lastAutoTable.finalY > 200) {
|
||||
doc.addPage();
|
||||
cursorY = 18;
|
||||
}
|
||||
|
||||
cursorY = sectionHeading("Detailed Daily Log", cursorY);
|
||||
|
||||
const logEntries = [...allEntries]
|
||||
.filter(e => {
|
||||
if (e.date < fromIso || e.date > toIso) return false;
|
||||
if (!WIRD_META[e.wirdType]) return false;
|
||||
if (!includeNafl && NAFL_WIRD[e.wirdType]) return false;
|
||||
if (!includeKhatm && e.wirdType === "khatm") return false;
|
||||
return true;
|
||||
})
|
||||
.sort((a, b) => b.date.localeCompare(a.date));
|
||||
|
||||
const HIDE_IN_LOG = new Set();
|
||||
if (!includeNafl) Object.keys(NAFL_WIRD).forEach(k => HIDE_IN_LOG.add(k));
|
||||
if (!includeKhatm) HIDE_IN_LOG.add("khatm");
|
||||
|
||||
const logRows = logEntries
|
||||
.filter(e => !HIDE_IN_LOG.has(e.wirdType))
|
||||
.map(e => {
|
||||
const meta = WIRD_META[e.wirdType];
|
||||
const cls = statusClass(e.wirdType, Number(e.value));
|
||||
const vs = cls === "over" ? "above" : cls === "done" ? "met" : cls === "miss" ? "below" : "—";
|
||||
return [
|
||||
e.date,
|
||||
meta ? meta.label : e.wirdType,
|
||||
fmtValue(e.wirdType, e.value),
|
||||
vs,
|
||||
e.notes ? e.notes.replace(/\n/g, " ").slice(0, 60) : "",
|
||||
];
|
||||
});
|
||||
|
||||
cursorY = addTable(
|
||||
[
|
||||
{ header: "Date", width: 22 },
|
||||
{ header: "Wird", width: 40 },
|
||||
{ header: "Amount", width: 35 },
|
||||
{ header: "vs Target", width: 22 },
|
||||
{ header: "Notes", width: null },
|
||||
],
|
||||
logRows.length ? logRows : [["—", "No entries in range", "", "", ""]],
|
||||
cursorY
|
||||
);
|
||||
|
||||
// ── 3. Nafl section ────────────────────────────────────
|
||||
if (includeNafl) {
|
||||
if (doc.lastAutoTable.finalY > 200) { doc.addPage(); cursorY = 18; }
|
||||
else { cursorY = doc.lastAutoTable.finalY + 6; }
|
||||
|
||||
cursorY = sectionHeading("Nafl Prayers", cursorY);
|
||||
|
||||
const naflRows = allEntries
|
||||
.filter(e => NAFL_WIRD[e.wirdType] && e.date >= fromIso && e.date <= toIso && Number(e.value) >= 1)
|
||||
.sort((a,b) => b.date.localeCompare(a.date))
|
||||
.map(e => [e.date, NAFL_WIRD[e.wirdType]?.label || e.wirdType, "✓ prayed"]);
|
||||
|
||||
cursorY = addTable(
|
||||
[
|
||||
{ header: "Date", width: 28 },
|
||||
{ header: "Prayer", width: 60 },
|
||||
{ header: "Status", width: 30 },
|
||||
],
|
||||
naflRows.length ? naflRows : [["—", "No nafl prayers in range", ""]],
|
||||
cursorY
|
||||
);
|
||||
}
|
||||
|
||||
// ── 4. Khatm section ───────────────────────────────────
|
||||
if (includeKhatm) {
|
||||
if (doc.lastAutoTable.finalY > 200) { doc.addPage(); cursorY = 18; }
|
||||
else { cursorY = doc.lastAutoTable.finalY + 6; }
|
||||
|
||||
cursorY = sectionHeading("Khatm — Qurʾān Completions", cursorY);
|
||||
|
||||
const khatmEntries = allEntries
|
||||
.filter(e => e.wirdType === "khatm" && e.date >= fromIso && e.date <= toIso && Number(e.value) >= 1)
|
||||
.sort((a,b) => b.date.localeCompare(a.date));
|
||||
|
||||
const khatmRows = khatmEntries.map(e => {
|
||||
let recType = "", noteText = e.notes || "";
|
||||
try {
|
||||
const parsed = JSON.parse(e.notes);
|
||||
recType = KHATM_TYPES[parsed.type] || parsed.type || "";
|
||||
noteText = parsed.notes || "";
|
||||
} catch (_) {}
|
||||
return [e.date, recType || "—", noteText.slice(0, 60) || "—"];
|
||||
});
|
||||
|
||||
cursorY = addTable(
|
||||
[
|
||||
{ header: "Date", width: 28 },
|
||||
{ header: "Type", width: 40 },
|
||||
{ header: "Notes", width: null },
|
||||
],
|
||||
khatmRows.length ? khatmRows : [["—", "No khatms in range", ""]],
|
||||
cursorY
|
||||
);
|
||||
}
|
||||
|
||||
// ── 5. Mutālaʿah section ──────────────────────────────
|
||||
if (includeMotalah) {
|
||||
if (doc.lastAutoTable.finalY > 200) { doc.addPage(); cursorY = 18; }
|
||||
else { cursorY = doc.lastAutoTable.finalY + 6; }
|
||||
|
||||
cursorY = sectionHeading("Mutālaʿah — Study Sessions", cursorY);
|
||||
|
||||
const mInRange = motalahEntries
|
||||
.filter(e => e.date >= fromIso && e.date <= toIso)
|
||||
.sort((a,b) => b.date.localeCompare(a.date));
|
||||
|
||||
const totalMins = mInRange.reduce((s,e) => s + Number(e.durationMinutes), 0);
|
||||
cursorY = statRow("Sessions", mInRange.length, cursorY);
|
||||
cursorY = statRow("Total study time", `${totalMins} min (${(totalMins/60).toFixed(1)} hrs)`, cursorY);
|
||||
cursorY += 3;
|
||||
|
||||
const motalahRows = mInRange.map(e => {
|
||||
const bookList = (e.bookIds || [])
|
||||
.map(id => {
|
||||
const b = calibreBooks.find(b => String(b.id) === String(id));
|
||||
return b ? b.title : String(id);
|
||||
}).join(", ");
|
||||
return [
|
||||
e.date,
|
||||
`${e.durationMinutes} min`,
|
||||
bookList.slice(0, 50) || "—",
|
||||
(e.notes || "").slice(0, 50) || "—",
|
||||
];
|
||||
});
|
||||
|
||||
cursorY = addTable(
|
||||
[
|
||||
{ header: "Date", width: 22 },
|
||||
{ header: "Duration", width: 22 },
|
||||
{ header: "Books", width: 70 },
|
||||
{ header: "Notes", width: null },
|
||||
],
|
||||
motalahRows.length ? motalahRows : [["—", "No sessions in range", "", ""]],
|
||||
cursorY
|
||||
);
|
||||
}
|
||||
|
||||
// ── Footer on every page ──────────────────────────────
|
||||
const pageCount = doc.internal.getNumberOfPages();
|
||||
for (let i = 1; i <= pageCount; i++) {
|
||||
doc.setPage(i);
|
||||
doc.setFont("helvetica", "normal");
|
||||
doc.setFontSize(7);
|
||||
doc.setTextColor(...MUTED);
|
||||
doc.text(
|
||||
`Wird Tracker · Page ${i} of ${pageCount}`,
|
||||
PAGE_W / 2, 292,
|
||||
{ align: "center" }
|
||||
);
|
||||
}
|
||||
|
||||
// ── Save ───────────────────────────────────────────────
|
||||
const filename = `wird-${fromIso}-to-${toIso}.pdf`;
|
||||
doc.save(filename);
|
||||
}
|
||||
})();
|
||||
1236
assets/scripts/wird-tracker.sync-conflict-20260325-231828-NE5VEIB.js
Normal file
0
assets/sketchnotes/07-12-2025-retrospectives.svg
Executable file → Normal file
|
Before Width: | Height: | Size: 568 KiB After Width: | Height: | Size: 568 KiB |
0
assets/sketchnotes/09-11-2025-nginx-architecture.svg
Executable file → Normal file
|
Before Width: | Height: | Size: 1.8 MiB After Width: | Height: | Size: 1.8 MiB |
0
assets/sketchnotes/09-11-2025-retrospectives.svg
Executable file → Normal file
|
Before Width: | Height: | Size: 1024 KiB After Width: | Height: | Size: 1024 KiB |
0
assets/sketchnotes/10-08-2025-weekly-review.svg
Executable file → Normal file
|
Before Width: | Height: | Size: 2.2 MiB After Width: | Height: | Size: 2.2 MiB |