// ==UserScript==
// @name Snapify
// @version 1.0
// @description Let Snapify reveal KIIT answer sheets with a simple traversal trick
// @author Prajwal Panth
// @license MIT
// @match http://btecheval.kiitresults.com/midfeb2025stview/cs/ImageDisplay.aspx*
// @grant GM_addStyle
// @grant GM_setValue
// @grant GM_getValue
// @icon https://www.google.com/s2/favicons?sz=64&domain=kiit.ac.in
// @require https://cdnjs.cloudflare.com/ajax/libs/jspdf/2.5.1/jspdf.umd.min.js
// @require https://cdnjs.cloudflare.com/ajax/libs/html2canvas/1.4.1/html2canvas.min.js
// @namespace https://greasyfork.org/users/1460116
// ==/UserScript==
GM_addStyle(`
:root {
/* Refined Color Palette (Inspired by Bolt/Shadcn) */
--background: #ffffff; /* White */
--foreground: #111827; /* Gray 900 */
--card: #ffffff;
--card-foreground: #111827;
--popover: #ffffff;
--popover-foreground: #111827;
--primary: #2563eb; /* Blue 600 */
--primary-foreground: #ffffff; /* White */
--secondary: #f3f4f6; /* Gray 100 */
--secondary-foreground: #1f2937; /* Gray 800 */
--muted: #f9fafb; /* Gray 50 */
--muted-foreground: #6b7280; /* Gray 500 */
--accent: #e5e7eb; /* Gray 200 */
--accent-foreground: #111827; /* Gray 900 */
--destructive: #dc2626; /* Red 600 */
--destructive-foreground: #ffffff; /* White */
--border: #e5e7eb; /* Gray 200 */
--input: #e5e7eb; /* Gray 200 */
--input-foreground: #111827;
--ring: #3b82f6; /* Blue 500 - Focus Ring */
/* Constants */
--radius: 0.375rem; /* Slightly smaller radius */
--font-sans: ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, "Noto Sans", sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji";
/* Shadows */
--shadow-sm: 0 1px 2px 0 rgb(0 0 0 / 0.05);
--shadow: 0 1px 3px 0 rgb(0 0 0 / 0.1), 0 1px 2px -1px rgb(0 0 0 / 0.1);
--shadow-md: 0 4px 6px -1px rgb(0 0 0 / 0.1), 0 2px 4px -2px rgb(0 0 0 / 0.1);
--shadow-lg: 0 10px 15px -3px rgb(0 0 0 / 0.1), 0 4px 6px -4px rgb(0 0 0 / 0.1);
}
.dark {
--background: #111827; /* Gray 900 */
--foreground: #f9fafb; /* Gray 50 */
--card: #1f2937; /* Gray 800 */
--card-foreground: #f9fafb; /* Gray 50 */
--popover: #1f2937; /* Gray 800 */
--popover-foreground: #f9fafb; /* Gray 50 */
--primary: #3b82f6; /* Blue 500 */
--primary-foreground: #ffffff; /* White */
--secondary: #374151; /* Gray 700 */
--secondary-foreground: #f3f4f6; /* Gray 100 */
--muted: #374151; /* Gray 700 */
--muted-foreground: #9ca3af; /* Gray 400 - Lightened */
--accent: #4b5563; /* Gray 600 */
--accent-foreground: #f9fafb; /* Gray 50 */
--destructive: #ef4444; /* Red 500 */
--destructive-foreground: #ffffff; /* White */
--border: #374151; /* Gray 700 */
--input: #374151; /* Gray 700 */
--input-foreground: #f9fafb;
--ring: #60a5fa; /* Blue 400 - Lighter Focus Ring */
}
/* Base styles */
body {
font-family: var(--font-sans);
background-color: var(--background);
color: var(--foreground);
margin-top: 20px !important; /* Ensure space from top */
}
#Form1 {
margin-top: 20px;
}
#cimg {
display: block;
max-width: 90%; /* Give some breathing room */
margin: 20px auto;
border: 1px solid var(--border);
box-shadow: var(--shadow);
border-radius: var(--radius);
transition: transform 0.2s ease, max-width 0.3s ease, margin 0.3s ease;
}
/* Main UI Panel */
.kitee-ui {
position: fixed;
top: 20px;
right: 20px;
background-color: var(--card);
border: 1px solid var(--border);
box-shadow: var(--shadow-lg);
padding: 16px;
width: 340px; /* Preferred width */
max-width: calc(100vw - 40px); /* Prevent overflow */
font-family: var(--font-sans);
font-size: 14px;
color: var(--card-foreground);
border-radius: var(--radius);
z-index: 9999;
transition: all 0.3s ease-in-out;
overflow: hidden;
display: flex; /* Use flex for main UI structure */
flex-direction: column; /* Stack toggle button and content */
}
.kitee-ui.collapsed {
width: 44px; /* Match toggle button size */
height: 44px;
padding: 0;
box-shadow: var(--shadow-md);
border-radius: 50%;
/* When collapsed, toggle button is the only content */
overflow: visible; /* Allow tooltip to show */
}
/* Toggle Button needs careful positioning */
.toggle-btn {
position: absolute; top: 6px; left: 6px; /* Position inside padding */
width: 32px; height: 32px; display: flex; align-items: center; justify-content: center;
font-size: 20px; cursor: pointer; background-color: transparent; /* Transparent */
border: none; border-radius: var(--radius); color: var(--muted-foreground);
z-index: 10; transition: background-color 0.15s ease, color 0.15s ease;
/* Take it out of normal flow */
flex-shrink: 0; /* Prevent shrinking */
}
.toggle-btn:hover { background-color: var(--secondary); color: var(--secondary-foreground); }
.toggle-btn:focus-visible { outline: 2px solid var(--ring); outline-offset: 1px; background-color: var(--secondary); }
.kitee-ui.collapsed .toggle-btn {
/* Reset position when collapsed - becomes the main element */
position: static; /* Back to normal flow inside the collapsed container */
width: 100%; height: 100%; /* Fill collapsed container */
background-color: var(--card);
color: var(--card-foreground);
box-shadow: none; /* Shadow is on the collapsed ui div */
border: none; /* Border is on the collapsed ui div */
border-radius: 50%;
display: flex; align-items: center; justify-content: center;
}
.kitee-ui.collapsed .toggle-btn:hover { background-color: var(--secondary); }
.kitee-ui .content-wrapper {
opacity: 1;
flex-grow: 1; /* Allow content to take remaining space */
max-height: calc(85vh - 32px); /* Limit height, accounting for padding */
overflow-y: auto;
overflow-x: hidden;
transition: opacity 0.2s ease-in-out 0.1s, max-height 0.3s ease-in-out;
padding-right: 5px; /* Space for scrollbar */
margin-top: 21px; /* Space for absolute positioned toggle button */
display: flex; /* Use flex for content sections */
flex-direction: column;
}
/* Scrollbar styling */
.kitee-ui .content-wrapper::-webkit-scrollbar { width: 6px; }
.kitee-ui .content-wrapper::-webkit-scrollbar-track { background: transparent; }
.kitee-ui .content-wrapper::-webkit-scrollbar-thumb { background-color: var(--border); border-radius: 3px; }
.kitee-ui .content-wrapper::-webkit-scrollbar-thumb:hover { background-color: var(--muted-foreground); }
.kitee-ui.collapsed .content-wrapper {
opacity: 0;
max-height: 0;
pointer-events: none;
padding-right: 0;
margin-top: 0; /* No margin needed when collapsed */
flex-grow: 0; /* Don't take space */
display: none; /* Completely remove from layout */
}
/* Header */
.kitee-header {
display: flex; align-items: center; justify-content: space-between;
margin-bottom: 16px; padding-bottom: 12px; border-bottom: 1px solid var(--border);
flex-shrink: 0; /* Prevent shrinking */
}
.kitee-title {
font-weight: 600; font-size: 16px; display: flex; align-items: center; gap: 8px;
}
.header-controls { display: flex; align-items: center; gap: 6px; }
/* Sections */
.kitee-ui section {
margin-bottom: 16px; padding-bottom: 16px; border-bottom: 1px solid var(--border);
flex-shrink: 0; /* Prevent sections from shrinking */
}
/* .kitee-ui section:last-of-type { Removed this - footer handles the last element now */
/* margin-bottom: 0; padding-bottom: 0; border-bottom: none; */
/* } */
/* Roll Number Section */
.roll-display { display: flex; align-items: center; justify-content: space-between; margin-bottom: 12px; }
.roll-label {
font-weight: 500; overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
flex-grow: 1; margin-right: 8px;
}
.roll-history { display: flex; flex-wrap: wrap; gap: 6px; max-height: 62px; overflow-y: auto; padding-top: 4px;}
.roll-history-item {
background-color: var(--secondary); color: var(--secondary-foreground); font-size: 12px;
padding: 3px 8px; border-radius: calc(var(--radius) / 1.5); cursor: pointer;
transition: background-color 0.15s ease, border-color 0.15s ease;
border: 1px solid var(--border); white-space: nowrap;
}
.roll-history-item:hover { background-color: var(--accent); }
.roll-history-item:focus-visible { outline: 2px solid var(--ring); outline-offset: 1px; }
/* Button styles */
.btn {
display: inline-flex; align-items: center; justify-content: center;
border-radius: var(--radius); font-weight: 500; font-size: 13px;
height: 32px; padding: 0 10px;
transition: background-color 0.15s ease, border-color 0.15s ease, opacity 0.15s ease, box-shadow 0.15s ease;
cursor: pointer; border: 1px solid transparent; white-space: nowrap; gap: 6px;
line-height: 1;
}
.btn:focus-visible { outline: 2px solid var(--ring); outline-offset: 2px; }
.btn-primary { background-color: var(--primary); color: var(--primary-foreground); border-color: var(--primary); }
.btn-primary:hover:not(:disabled) { background-color: #1d4ed8; border-color: #1d4ed8; }
.dark .btn-primary:hover:not(:disabled) { background-color: #60a5fa; border-color: #60a5fa; }
.btn-secondary { background-color: var(--secondary); color: var(--secondary-foreground); border-color: var(--border); }
.btn-secondary:hover:not(:disabled) { background-color: var(--accent); }
.btn-destructive { background-color: var(--destructive); color: var(--destructive-foreground); border-color: var(--destructive); }
.btn-destructive:hover:not(:disabled) { background-color: #b91c1c; border-color: #b91c1c; }
.dark .btn-destructive:hover:not(:disabled) { background-color: #f87171; border-color: #f87171; }
.btn-icon { width: 32px; padding: 0; }
.btn:disabled { opacity: 0.6; cursor: not-allowed; }
/* Navigation */
.page-section { display: flex; flex-direction: column; gap: 10px; }
.page-nav { display: flex; align-items: center; justify-content: space-between; gap: 8px; }
.page-info { font-weight: 500; font-size: 13px; text-align: center; min-width: 90px; color: var(--muted-foreground); }
.page-jump { display: flex; align-items: center; gap: 6px; }
.page-jump-input {
width: 55px; height: 32px; border-radius: var(--radius); border: 1px solid var(--border);
background-color: var(--background); color: var(--input-foreground);
padding: 0 8px; text-align: center; font-size: 13px;
}
.page-jump-input:focus { border-color: var(--ring); outline: 1px solid var(--ring); }
.page-jump-input::-webkit-outer-spin-button, .page-jump-input::-webkit-inner-spin-button { -webkit-appearance: none; margin: 0; }
.page-jump-input[type=number] { -moz-appearance: textfield; }
.page-jump .btn { width: 32px; }
/* Actions */
.actions-section { display: flex; flex-wrap: wrap; gap: 8px; justify-content: space-between; }
.actions-section .btn { flex-grow: 1; font-size: 12px; }
.actions-section .btn-primary { flex-grow: 2; }
/* Status Indicator */
.status-indicator {
display: flex; align-items: center; font-size: 12px; margin-top: 8px;
color: var(--muted-foreground); height: 20px; transition: color 0.3s ease;
}
.loading-spinner {
display: none; border: 2px solid var(--accent); border-top: 2px solid var(--primary);
border-radius: 50%; width: 14px; height: 14px;
animation: spin 1s linear infinite; margin-right: 6px;
}
.status-indicator.loading .loading-spinner { display: inline-block; }
.status-indicator.loading .status-text { color: var(--primary); font-weight: 500;}
.status-indicator.success .status-text { color: #16a34a !important; font-weight: 500; }
.dark .status-indicator.success .status-text { color: #4ade80 !important; }
.status-indicator.error .status-text { color: var(--destructive) !important; font-weight: 500; }
/* Tooltip */
.tooltip { position: relative; }
.tooltip::after {
content: attr(data-tooltip); position: absolute; bottom: 115%; left: 50%;
transform: translateX(-50%); background-color: #1f2937; color: #f9fafb;
padding: 4px 8px; border-radius: var(--radius); font-size: 11px;
white-space: nowrap; z-index: 10000; box-shadow: var(--shadow-md);
opacity: 0; visibility: hidden; transition: opacity 0.2s ease 0.1s, visibility 0.2s ease 0.1s;
pointer-events: none;
}
.tooltip:hover::after { opacity: 1; visibility: visible; }
/* Shortcuts List */
.shortcuts-section { font-size: 12px; color: var(--muted-foreground); }
.shortcuts-title { font-weight: 500; color: var(--card-foreground); margin-bottom: 8px; }
.shortcuts-list { list-style-type: none; padding-left: 0; margin-top: 0; }
.shortcuts-list li { margin-bottom: 6px; display: flex; justify-content: space-between; align-items: center; }
.shortcuts-list kbd {
font-family: inherit; background-color: var(--secondary); color: var(--secondary-foreground);
padding: 2px 6px; border-radius: 4px; border: 1px solid var(--border);
font-size: 11px; font-weight: 500; margin-left: 8px; box-shadow: var(--shadow-sm);
}
.shortcuts-list .key-combo { display: flex; gap: 4px; align-items: center; }
/* NEW: Footer Styles */
.kitee-footer {
margin-top: 16px; /* Space above footer */
padding-top: 12px; /* Space below last section */
border-top: 1px solid var(--border);
font-size: 11px;
color: var(--muted-foreground);
display: flex;
align-items: center;
justify-content: center; /* Center content */
gap: 8px; /* Space between text and icon */
flex-shrink: 0; /* Prevent shrinking */
}
.kitee-footer a {
color: var(--muted-foreground);
display: inline-flex; /* Align icon correctly */
align-items: center;
transition: color 0.15s ease;
}
.kitee-footer a:hover {
color: var(--foreground); /* Make icon darker on hover */
}
.kitee-footer a:focus-visible {
outline: 2px solid var(--ring);
outline-offset: 2px;
border-radius: var(--radius); /* Add radius to focus outline */
}
.kitee-footer svg {
width: 16px;
height: 16px;
}
/* Fullscreen Mode */
body.fullscreen-mode #cimg {
position: fixed; top: 50%; left: 50%; transform: translate(-50%, -50%);
max-height: 95vh; max-width: 95vw; width: auto; height: auto; z-index: 9990;
box-shadow: var(--shadow-lg); border: 2px solid var(--background); border-radius: 0;
object-fit: contain; cursor: zoom-out;
}
.fullscreen-backdrop {
position: fixed; top: 0; left: 0; right: 0; bottom: 0;
background-color: rgba(0, 0, 0, 0.85); z-index: 9980; cursor: zoom-out;
animation: fadeIn 0.3s ease;
}
/* PDF Export Progress */
.pdf-progress-overlay {
position: fixed; top: 0; left: 0; right: 0; bottom: 0;
background-color: rgba(0, 0, 0, 0.7); display: flex; flex-direction: column;
align-items: center; justify-content: center; z-index: 10000; color: white;
text-align: center; font-family: var(--font-sans); animation: fadeIn 0.3s ease;
}
.pdf-progress-content {
background-color: var(--card); color: var(--card-foreground);
padding: 25px 35px; border-radius: var(--radius); box-shadow: var(--shadow-lg);
display: flex; flex-direction: column; align-items: center; min-width: 300px;
}
.pdf-progress-spinner {
border: 4px solid var(--secondary); border-top: 4px solid var(--primary);
border-radius: 50%; width: 30px; height: 30px;
animation: spin 1.5s linear infinite; margin-bottom: 15px;
}
.pdf-progress-text { font-size: 16px; font-weight: 500; margin-bottom: 10px; }
.pdf-progress-details { font-size: 13px; color: var(--muted-foreground); margin-bottom: 15px; }
.pdf-progress-bar-container {
width: 100%; height: 8px; background-color: var(--secondary);
border-radius: 4px; overflow: hidden; border: 1px solid var(--border);
}
.pdf-progress-bar {
height: 100%; background-color: var(--primary); width: 0%;
transition: width 0.3s ease; border-radius: 4px;
}
/* Hide default KITEE UI elements */
#dg1, form[name="Form1"] > table:first-of-type { display: none !important; }
/* Animation */
@keyframes spin { 0% { transform: rotate(0deg); } 100% { transform: rotate(360deg); } }
@keyframes fadeIn { from { opacity: 0; } to { opacity: 1; } }
.fade-in { animation: fadeIn 0.3s ease forwards; }
`);
(function() {
'use strict';
// --- State Management ---
const state = {
currentPage: 1,
totalPages: 0,
rollNumber: GM_getValue('roll') || '',
rollHistory: JSON.parse(GM_getValue('rollHistory') || '[]'),
subject: document.querySelector("#LblCouseCode")?.textContent?.trim() || 'UNKNOWN_SUB',
examType: document.querySelector("#LblCourse")?.textContent?.trim() || 'Exam',
year: parseInt(window.location.href.match(/midfeb(\d{4})stview/i)?.[1] || '2025', 10),
semester: 6,
isDarkMode: GM_getValue('darkMode', window.matchMedia && window.matchMedia('(prefers-color-scheme: dark)').matches),
isCollapsed: GM_getValue('collapsed', false),
isFullscreen: false,
status: { type: '', message: '', timeoutId: null },
pdfGenerating: false,
pdfProgress: 0,
pdfTotalPages: 0,
};
// --- Constants ---
const MAX_ROLL_HISTORY = 5;
const STATUS_CLEAR_DELAY = 3500;
const A4_WIDTH_PT = 595.28;
const A4_HEIGHT_PT = 841.89;
const PDF_MARGIN_PT = 30;
// --- Icons ---
const icons = {
collapse: `<svg xmlns="http://www.w3.org/2000/svg" width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polyline points="15 18 9 12 15 6"></polyline></svg>`,
expand: `<svg xmlns="http://www.w3.org/2000/svg" width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polyline points="9 18 15 12 9 6"></polyline></svg>`,
edit: `<svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M11 4H4a2 2 0 0 0-2 2v14a2 2 0 0 0 2 2h14a2 2 0 0 0 2-2v-7"></path><path d="M18.5 2.5a2.121 2.121 0 0 1 3 3L12 15l-4 1 1-4 9.5-9.5z"></path></svg>`,
refresh: `<svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polyline points="23 4 23 10 17 10"></polyline><path d="M20.49 15a9 9 0 1 1-2.12-9.36L23 10"></path></svg>`,
prev: `<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polyline points="15 18 9 12 15 6"></polyline></svg>`,
next: `<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polyline points="9 18 15 12 9 6"></polyline></svg>`,
first: `<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polyline points="11 17 6 12 11 7"></polyline><polyline points="18 17 13 12 18 7"></polyline></svg>`,
last: `<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polyline points="13 17 18 12 13 7"></polyline><polyline points="6 17 11 12 6 7"></polyline></svg>`,
fullscreen: `<svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M8 3H5a2 2 0 0 0-2 2v3m18 0V5a2 2 0 0 0-2-2h-3m0 18h3a2 2 0 0 0 2-2v-3M3 16v3a2 2 0 0 0 2 2h3"></path></svg>`,
exitFullscreen: `<svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M8 3v3a2 2 0 0 1-2 2H3m18 0h-3a2 2 0 0 1-2-2V3m0 18v-3a2 2 0 0 1 2-2h3M3 16h3a2 2 0 0 1 2 2v3"></path></svg>`,
jump: `<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><line x1="5" y1="12" x2="19" y2="12"></line><polyline points="12 5 19 12 12 19"></polyline></svg>`,
dark: `<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M21 12.79A9 9 0 1 1 11.21 3 7 7 0 0 0 21 12.79z"></path></svg>`,
light: `<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="5"></circle><line x1="12" y1="1" x2="12" y2="3"></line><line x1="12" y1="21" x2="12" y2="23"></line><line x1="4.22" y1="4.22" x2="5.64" y2="5.64"></line><line x1="18.36" y1="18.36" x2="19.78" y2="19.78"></line><line x1="1" y1="12" x2="3" y2="12"></line><line x1="21" y1="12" x2="23" y2="12"></line><line x1="4.22" y1="19.78" x2="5.64" y2="18.36"></line><line x1="18.36" y1="5.64" x2="19.78" y2="4.22"></line></svg>`,
logo: `<svg xmlns="http://www.w3.org/2000/svg" width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M2 3h6a4 4 0 0 1 4 4v14a3 3 0 0 0-3-3H2z"></path><path d="M22 3h-6a4 4 0 0 0-4 4v14a3 3 0 0 1 3-3h7z"></path></svg>`,
pdf: `<svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z"></path><polyline points="14 2 14 8 20 8"></polyline><line x1="16" y1="13" x2="8" y2="13"></line><line x1="16" y1="17" x2="8" y2="17"></line><polyline points="10 9 9 9 8 9"></polyline></svg>`,
// NEW: GitHub Icon
github: `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16" fill="currentColor" aria-hidden="true"><path d="M8 0C3.58 0 0 3.58 0 8c0 3.54 2.29 6.53 5.47 7.59.4.07.55-.17.55-.38 0-.19-.01-.82-.01-1.49-2.01.37-2.53-.49-2.69-.94-.09-.23-.48-.94-.82-1.13-.28-.15-.68-.52-.01-.53.63-.01 1.08.58 1.23.82.72 1.21 1.87.87 2.33.66.07-.52.28-.87.51-1.07-1.78-.2-3.64-.89-3.64-3.95 0-.87.31-1.59.82-2.15-.08-.2-.36-1.02.08-2.12 0 0 .67-.21 2.2.82.64-.18 1.32-.27 2-.27.68 0 1.36.09 2 .27 1.53-1.04 2.2-.82 2.2-.82.44 1.1.16 1.92.08 2.12.51.56.82 1.27.82 2.15 0 3.07-1.87 3.75-3.65 3.95.29.25.54.73.54 1.48 0 1.07-.01 1.93-.01 2.2 0 .21.15.46.55.38A8.013 8.013 0 0016 8c0-4.42-3.58-8-8-8z"></path></svg>`
};
// --- DOM Elements Cache ---
const elements = {};
// --- Utility Functions & Core Logic ---
// ... (generateKITEEUrl, setStatus, addToRollHistory, UI Update functions, Core Logic functions: updateImage, navigateToPage, promptRollNumber, toggles, exportToPDF, PDF progress, handleKeyPress - all remain the same as v2.2) ...
// --- Utility Functions ---
function generateKITEEUrl(subject, roll, page, year = state.year, semester = state.semester) {
if (!year || !semester || !subject || !roll || !page) {
console.error("Missing parameters for URL generation:", { subject, roll, page, year, semester });
setStatus('error', 'Missing info for URL');
return null;
}
const paddedPage = String(page).padStart(3, '0');
// Verified path structure assumption - EC2R might be specific. If issues arise, this might need adjustment.
const basePath = `../../../../../../../../KITEE/${year}_${semester}/${subject}/EC2R/${roll}/${paddedPage}|DU|1`;
return `GenerateImage.aspx?pgno=${encodeURIComponent(basePath)}`;
}
function setStatus(type, message, clear = true) {
if (state.status.timeoutId) {
clearTimeout(state.status.timeoutId);
state.status.timeoutId = null;
}
state.status.type = type;
state.status.message = message;
if (elements.statusIndicator && elements.statusText && elements.loadingSpinner) {
elements.statusIndicator.className = `status-indicator ${type}`;
elements.statusText.textContent = message;
elements.loadingSpinner.style.display = type === 'loading' ? 'inline-block' : 'none';
if (clear && (type === 'success' || type === 'error')) {
state.status.timeoutId = setTimeout(() => {
setStatus('', ''); // Clear status after delay
}, STATUS_CLEAR_DELAY);
}
}
// Disable/Enable buttons during loading
const isLoading = type === 'loading';
const buttonsToDisable = [
elements.firstBtn, elements.prevBtn, elements.nextBtn, elements.lastBtn,
elements.jumpBtn, elements.refreshBtn, elements.fullscreenBtn, elements.pdfBtn,
elements.editRollBtn, ...(elements.rollHistoryContainer?.children || [])
];
buttonsToDisable.forEach(btn => {
if (btn) btn.disabled = isLoading;
});
if (elements.jumpInput) elements.jumpInput.disabled = isLoading;
}
function addToRollHistory(newRoll) {
if (!newRoll) return;
const upperCaseRoll = newRoll.toUpperCase(); // Store consistently
const filteredHistory = state.rollHistory.filter(r => r !== upperCaseRoll);
const updatedHistory = [upperCaseRoll, ...filteredHistory];
state.rollHistory = updatedHistory.slice(0, MAX_ROLL_HISTORY);
GM_setValue('rollHistory', JSON.stringify(state.rollHistory));
updateRollHistoryUI();
}
// --- UI Update Functions ---
function updateRollDisplay() {
const displayRoll = state.rollNumber || 'Not set';
if (elements.rollLabel) {
elements.rollLabel.textContent = `Roll: ${displayRoll}`;
elements.rollLabel.title = `Roll: ${displayRoll}`;
}
if (elements.pdfBtn) {
const pdfBtnText = elements.pdfBtn.querySelector('span');
if(pdfBtnText) {
pdfBtnText.textContent = state.rollNumber ? `Export PDF (${state.rollNumber})` : 'Export PDF';
}
}
}
function updateRollHistoryUI() {
if (!elements.rollHistoryContainer) return;
elements.rollHistoryContainer.innerHTML = ''; // Clear existing items
if (state.rollHistory.length === 0) {
// No need for empty message if section just looks empty
} else {
state.rollHistory.forEach(roll => {
const item = document.createElement('button');
item.className = 'roll-history-item tooltip';
item.textContent = roll;
item.setAttribute('data-tooltip', `Load roll ${roll}`);
item.onclick = () => {
if (state.rollNumber !== roll && !state.pdfGenerating) { // Prevent change during PDF export
state.rollNumber = roll;
GM_setValue('roll', roll);
updateRollDisplay();
state.currentPage = 1;
updateImage();
updatePageInfo();
setStatus('success', `Loaded roll ${roll}`);
// No need to re-add to history here, it's already there.
}
};
elements.rollHistoryContainer.appendChild(item);
});
}
}
function updatePageInfo() {
if (elements.pageInfo && elements.jumpInput) {
const total = state.totalPages > 0 ? state.totalPages : '?';
elements.pageInfo.textContent = `Page ${state.currentPage} of ${total}`;
elements.jumpInput.value = state.currentPage;
elements.jumpInput.max = state.totalPages > 0 ? state.totalPages : 1;
}
if (elements.firstBtn && elements.prevBtn && elements.nextBtn && elements.lastBtn) {
const onFirstPage = state.currentPage <= 1;
const onLastPage = state.currentPage >= state.totalPages && state.totalPages > 0;
const noPages = state.totalPages === 0;
elements.firstBtn.disabled = onFirstPage || noPages || state.pdfGenerating;
elements.prevBtn.disabled = onFirstPage || noPages || state.pdfGenerating;
elements.nextBtn.disabled = onLastPage || noPages || state.pdfGenerating;
elements.lastBtn.disabled = onLastPage || noPages || state.pdfGenerating;
if(elements.jumpInput) elements.jumpInput.disabled = noPages || state.pdfGenerating;
if(elements.jumpBtn) elements.jumpBtn.disabled = noPages || state.pdfGenerating;
}
}
function updateTheme() {
if (state.isDarkMode) {
document.documentElement.classList.add('dark');
} else {
document.documentElement.classList.remove('dark');
}
if (elements.themeToggle) {
elements.themeToggle.innerHTML = state.isDarkMode ? icons.light : icons.dark;
elements.themeToggle.setAttribute('data-tooltip', state.isDarkMode ? 'Switch to Light Mode' : 'Switch to Dark Mode');
}
if (elements.pdfProgressOverlay && elements.pdfProgressOverlay.style.display !== 'none') {
// PDF progress overlay styling is handled by CSS variables now
}
}
function updateCollapseState() {
if (!elements.ui || !elements.toggleBtn) return;
if (state.isCollapsed) {
elements.ui.classList.add('collapsed');
elements.toggleBtn.innerHTML = icons.expand;
elements.toggleBtn.setAttribute('data-tooltip', 'Expand Panel (C)');
} else {
elements.ui.classList.remove('collapsed');
elements.toggleBtn.innerHTML = icons.collapse;
elements.toggleBtn.setAttribute('data-tooltip', 'Collapse Panel (C)');
}
GM_setValue('collapsed', state.isCollapsed);
}
function updateFullscreenState() {
const fullscreenBtnSpan = elements.fullscreenBtn?.querySelector('span');
if (state.isFullscreen) {
document.body.classList.add('fullscreen-mode');
if (!elements.fullscreenBackdrop) {
elements.fullscreenBackdrop = document.createElement('div');
elements.fullscreenBackdrop.className = 'fullscreen-backdrop';
elements.fullscreenBackdrop.onclick = toggleFullscreen;
document.body.appendChild(elements.fullscreenBackdrop);
elements.fullscreenBackdrop.classList.add('fade-in');
}
elements.fullscreenBackdrop.style.display = 'block';
if (elements.fullscreenBtn) {
elements.fullscreenBtn.innerHTML = `${icons.exitFullscreen} <span>Exit Fullscreen</span>`;
elements.fullscreenBtn.setAttribute('data-tooltip', 'Exit Fullscreen (F or Esc)');
}
if (elements.imageElement) elements.imageElement.addEventListener('click', toggleFullscreen);
} else {
document.body.classList.remove('fullscreen-mode');
if (elements.fullscreenBackdrop) {
elements.fullscreenBackdrop.style.display = 'none';
}
if (elements.fullscreenBtn) {
elements.fullscreenBtn.innerHTML = `${icons.fullscreen} <span>Fullscreen</span>`;
elements.fullscreenBtn.setAttribute('data-tooltip', 'Enter Fullscreen (F)');
}
if (elements.imageElement) elements.imageElement.removeEventListener('click', toggleFullscreen);
}
}
// --- Core Logic Functions ---
async function updateImage(page = state.currentPage) {
if (!state.rollNumber) {
setStatus('error', 'Roll number not set.', false); return Promise.reject('No roll number');
}
if (!state.subject || state.subject === 'UNKNOWN_SUB') {
setStatus('error', 'Subject code not found.', false); return Promise.reject('No subject code');
}
if (!elements.imageElement) {
setStatus('error', 'Image element not found.', false); return Promise.reject('No image element');
}
const imageUrl = generateKITEEUrl(state.subject, state.rollNumber, page);
if (!imageUrl) return Promise.reject('URL generation failed');
setStatus('loading', `Loading page ${page}...`, false);
return new Promise((resolve, reject) => {
const img = elements.imageElement;
let resolved = false; // Prevent multiple resolves/rejects
let loadTimeout = null; // Store timeout ID
const loadHandler = () => {
cleanup();
if (!resolved) {
resolved = true;
if(state.status.type === 'loading' && state.status.message.includes(`Loading page ${page}`)) {
setStatus('success', `Page ${page} loaded`);
}
state.currentPage = page;
updatePageInfo();
resolve();
}
};
const errorHandler = (err) => {
cleanup();
if (!resolved) {
resolved = true;
console.error(`Error loading image for page ${page}:`, err instanceof Event ? 'Load Error Event' : err);
if(state.status.type === 'loading' && state.status.message.includes(`Loading page ${page}`)) {
setStatus('error', `Failed to load page ${page}`);
}
reject(new Error(`Failed to load image for page ${page}`));
}
};
const cleanup = () => {
img.removeEventListener('load', loadHandler);
img.removeEventListener('error', errorHandler);
if (loadTimeout) clearTimeout(loadTimeout);
loadTimeout = null;
};
img.addEventListener('load', loadHandler);
img.addEventListener('error', errorHandler);
loadTimeout = setTimeout(() => {
if (!resolved) {
errorHandler(new Error('Image load timed out'));
}
}, 15000); // 15 second timeout
img.src = imageUrl;
});
}
function navigateToPage(page) {
page = parseInt(page, 10);
if (isNaN(page) || page < 1 || (page > state.totalPages && state.totalPages > 0) || page === state.currentPage || state.pdfGenerating) {
if (isNaN(page) || page < 1 || (page > state.totalPages && state.totalPages > 0)) {
setStatus('error', 'Invalid page number');
}
if (elements.jumpInput) elements.jumpInput.value = state.currentPage;
return;
}
updateImage(page).catch(err => console.warn("Navigation error ignored:", err)); // Log error but don't block UI
}
function promptRollNumber() {
if (state.pdfGenerating) {
setStatus('error', 'Cannot change roll during PDF export.');
return;
}
const currentRoll = state.rollNumber;
const newRoll = prompt('Enter KIIT Roll Number:', currentRoll);
if (newRoll && newRoll.trim().toUpperCase() !== currentRoll) {
state.rollNumber = newRoll.trim().toUpperCase();
GM_setValue('roll', state.rollNumber);
addToRollHistory(state.rollNumber);
updateRollDisplay();
state.currentPage = 1;
updatePageInfo(); // Update page info immediately
updateImage().catch(err => console.error("Error loading image for new roll:", err)); // Load image for the new roll
setStatus('success', `Roll number updated to ${state.rollNumber}`);
}
}
function toggleDarkMode() {
state.isDarkMode = !state.isDarkMode;
GM_setValue('darkMode', state.isDarkMode);
updateTheme();
}
function toggleCollapse() {
state.isCollapsed = !state.isCollapsed;
updateCollapseState();
}
function toggleFullscreen() {
state.isFullscreen = !state.isFullscreen;
updateFullscreenState();
}
async function exportToPDF() {
if (state.pdfGenerating) {
setStatus('error', 'PDF generation already in progress.'); return;
}
if (!state.rollNumber || state.totalPages <= 0) {
setStatus('error', 'Set roll number & ensure pages are detected.'); return;
}
if (typeof jspdf === 'undefined' || typeof html2canvas === 'undefined') {
setStatus('error', 'PDF libraries not loaded.');
console.error('jsPDF or html2canvas missing. Check @require directives and network.');
return;
}
state.pdfGenerating = true;
state.pdfProgress = 0;
state.pdfTotalPages = state.totalPages;
const originalPage = state.currentPage;
showPdfProgress();
setStatus('loading', 'Starting PDF export...', false);
updatePageInfo(); // Update button disabled states
const pdfDoc = new jspdf.jsPDF({
orientation: 'p', unit: 'pt', format: 'a4',
putOnlyUsedFonts: true, floatPrecision: 16
});
const availableWidth = A4_WIDTH_PT - 2 * PDF_MARGIN_PT;
const availableHeight = A4_HEIGHT_PT - 2 * PDF_MARGIN_PT;
let pagesExported = 0;
try {
for (let i = 1; i <= state.totalPages; i++) {
state.pdfProgress = i;
updatePdfProgress();
setStatus('loading', `Exporting page ${i}/${state.totalPages}...`, false);
try {
await updateImage(i);
const imgElement = elements.imageElement;
if (!imgElement || !imgElement.complete || imgElement.naturalWidth === 0) {
throw new Error(`Image element invalid after load for page ${i}`);
}
const canvas = await html2canvas(imgElement, {
scale: 2, useCORS: true, logging: false,
backgroundColor: state.isDarkMode ? '#111827' : '#ffffff',
});
const imgData = canvas.toDataURL('image/jpeg', 0.85);
const imgProps = pdfDoc.getImageProperties(imgData);
let imgWidth = imgProps.width;
let imgHeight = imgProps.height;
const ratio = imgWidth / imgHeight;
if (imgWidth > availableWidth) {
imgWidth = availableWidth;
imgHeight = imgWidth / ratio;
}
if (imgHeight > availableHeight) {
imgHeight = availableHeight;
imgWidth = imgHeight * ratio;
}
const x = PDF_MARGIN_PT + (availableWidth - imgWidth) / 2;
const y = PDF_MARGIN_PT + (availableHeight - imgHeight) / 2; // Center vertically too
if (pagesExported > 0) {
pdfDoc.addPage();
}
pdfDoc.addImage(imgData, 'JPEG', x, y, imgWidth, imgHeight);
pagesExported++;
} catch (pageError) {
console.warn(`Skipping page ${i} due to error:`, pageError);
setStatus('error', `Skipped page ${i} (Check console)`);
await new Promise(resolve => setTimeout(resolve, 300)); // Shorter delay
}
} // End of loop
if (pagesExported === 0) {
throw new Error("No pages could be exported.");
}
const filename = `${state.subject}_${state.rollNumber}_${state.examType}.pdf`.replace(/[^a-z0-9_.-]/gi, '_');
pdfDoc.save(filename);
setStatus('success', `PDF exported (${pagesExported}/${state.totalPages} pages)`);
} catch (error) {
console.error('PDF Export Failed:', error);
setStatus('error', `PDF export failed: ${error.message}`);
} finally {
state.pdfGenerating = false;
hidePdfProgress();
updatePageInfo(); // Re-enable buttons
// Go back to original page only if it's still valid
if (originalPage >= 1 && originalPage <= state.totalPages) {
updateImage(originalPage).catch(err => console.warn("Failed to return to original page after PDF export:", err));
}
}
}
function showPdfProgress() {
if (!elements.pdfProgressOverlay) {
elements.pdfProgressOverlay = document.createElement('div');
elements.pdfProgressOverlay.className = 'pdf-progress-overlay';
elements.pdfProgressOverlay.innerHTML = `
<div class="pdf-progress-content">
<div class="pdf-progress-spinner"></div>
<div class="pdf-progress-text">Generating PDF...</div>
<div class="pdf-progress-details">Page 1 of ${state.pdfTotalPages}</div>
<div class="pdf-progress-bar-container">
<div class="pdf-progress-bar"></div>
</div>
</div>
`;
document.body.appendChild(elements.pdfProgressOverlay);
elements.pdfProgressDetails = elements.pdfProgressOverlay.querySelector('.pdf-progress-details');
elements.pdfProgressBar = elements.pdfProgressOverlay.querySelector('.pdf-progress-bar');
updateTheme(); // Apply theme
}
elements.pdfProgressOverlay.style.display = 'flex';
updatePdfProgress();
}
function updatePdfProgress() {
if (elements.pdfProgressOverlay && state.pdfGenerating) {
const progressPercent = state.pdfTotalPages > 0 ? (state.pdfProgress / state.pdfTotalPages) * 100 : 0;
if (elements.pdfProgressDetails) {
elements.pdfProgressDetails.textContent = `Page ${state.pdfProgress} of ${state.pdfTotalPages}`;
}
if (elements.pdfProgressBar) {
elements.pdfProgressBar.style.width = `${progressPercent}%`;
}
}
}
function hidePdfProgress() {
if (elements.pdfProgressOverlay) {
elements.pdfProgressOverlay.style.display = 'none';
}
}
// --- Keyboard Shortcut Handler ---
function handleKeyPress(event) {
if (event.target.tagName === 'INPUT' || event.target.tagName === 'TEXTAREA' || state.pdfGenerating) {
return;
}
if (event.altKey || event.ctrlKey || event.metaKey) {
return;
}
let buttonClicked = null;
switch (event.key.toUpperCase()) {
case 'ARROWLEFT': buttonClicked = elements.prevBtn; break;
case 'ARROWRIGHT': buttonClicked = elements.nextBtn; break;
case 'HOME': buttonClicked = elements.firstBtn; break;
case 'END': buttonClicked = elements.lastBtn; break;
case 'F': toggleFullscreen(); break;
case 'R': buttonClicked = elements.refreshBtn; break;
case 'C': toggleCollapse(); break;
case 'P': buttonClicked = elements.pdfBtn; break;
case 'E': buttonClicked = elements.editRollBtn; break;
case 'ESCAPE':
if (state.isFullscreen) { event.preventDefault(); toggleFullscreen(); }
break;
}
if (buttonClicked && !buttonClicked.disabled) {
event.preventDefault();
buttonClicked.click();
buttonClicked.style.transform = 'scale(0.97)'; // Visual feedback
setTimeout(() => { buttonClicked.style.transform = 'scale(1)'; }, 100);
}
}
// --- UI Creation ---
function createUI() {
const ui = document.createElement('div');
ui.className = 'kitee-ui fade-in';
elements.ui = ui;
const toggleBtn = document.createElement('button');
toggleBtn.className = 'toggle-btn tooltip';
toggleBtn.onclick = toggleCollapse;
elements.toggleBtn = toggleBtn;
ui.appendChild(toggleBtn);
const contentWrapper = document.createElement('div');
contentWrapper.className = 'content-wrapper';
elements.contentWrapper = contentWrapper;
// Header
const header = document.createElement('header');
header.className = 'kitee-header';
const title = document.createElement('div');
title.className = 'kitee-title';
title.innerHTML = `${icons.logo} <span>KITEE Navigator</span>`;
const headerControls = document.createElement('div');
headerControls.className = 'header-controls';
const themeToggle = document.createElement('button');
themeToggle.className = 'btn btn-secondary btn-icon tooltip';
themeToggle.onclick = toggleDarkMode;
elements.themeToggle = themeToggle;
headerControls.appendChild(themeToggle);
header.append(title, headerControls);
contentWrapper.appendChild(header);
// Roll Number Section
const rollSection = document.createElement('section');
const rollDisplay = document.createElement('div');
rollDisplay.className = 'roll-display';
const rollLabel = document.createElement('div');
rollLabel.className = 'roll-label';
elements.rollLabel = rollLabel;
const editRollBtn = document.createElement('button');
editRollBtn.className = 'btn btn-secondary btn-icon tooltip';
editRollBtn.innerHTML = icons.edit;
editRollBtn.setAttribute('data-tooltip', 'Edit Roll Number (E)');
editRollBtn.onclick = promptRollNumber;
elements.editRollBtn = editRollBtn;
rollDisplay.append(rollLabel, editRollBtn);
const rollHistoryContainer = document.createElement('div');
rollHistoryContainer.className = 'roll-history';
elements.rollHistoryContainer = rollHistoryContainer;
rollSection.append(rollDisplay, rollHistoryContainer);
contentWrapper.appendChild(rollSection);
// Page Navigation Section
const pageSection = document.createElement('section');
pageSection.className = 'page-section';
const pageNav = document.createElement('div');
pageNav.className = 'page-nav';
const navControls1 = document.createElement('div');
navControls1.style.display = 'flex'; navControls1.style.gap = '6px';
const firstBtn = document.createElement('button');
firstBtn.className = 'btn btn-secondary btn-icon tooltip'; firstBtn.innerHTML = icons.first;
firstBtn.setAttribute('data-tooltip', 'First Page (Home)'); firstBtn.onclick = () => navigateToPage(1);
elements.firstBtn = firstBtn;
const prevBtn = document.createElement('button');
prevBtn.className = 'btn btn-secondary btn-icon tooltip'; prevBtn.innerHTML = icons.prev;
prevBtn.setAttribute('data-tooltip', 'Previous Page (←)'); prevBtn.onclick = () => navigateToPage(state.currentPage - 1);
elements.prevBtn = prevBtn;
navControls1.append(firstBtn, prevBtn);
const pageInfo = document.createElement('div');
pageInfo.className = 'page-info'; elements.pageInfo = pageInfo;
const navControls2 = document.createElement('div');
navControls2.style.display = 'flex'; navControls2.style.gap = '6px';
const nextBtn = document.createElement('button');
nextBtn.className = 'btn btn-secondary btn-icon tooltip'; nextBtn.innerHTML = icons.next;
nextBtn.setAttribute('data-tooltip', 'Next Page (→)'); nextBtn.onclick = () => navigateToPage(state.currentPage + 1);
elements.nextBtn = nextBtn;
const lastBtn = document.createElement('button');
lastBtn.className = 'btn btn-secondary btn-icon tooltip'; lastBtn.innerHTML = icons.last;
lastBtn.setAttribute('data-tooltip', 'Last Page (End)'); lastBtn.onclick = () => state.totalPages > 0 && navigateToPage(state.totalPages);
elements.lastBtn = lastBtn;
navControls2.append(nextBtn, lastBtn);
pageNav.append(navControls1, pageInfo, navControls2);
const pageJump = document.createElement('div');
pageJump.className = 'page-jump';
const jumpInput = document.createElement('input');
jumpInput.className = 'page-jump-input'; jumpInput.type = 'number'; jumpInput.min = 1; jumpInput.placeholder = "Go";
elements.jumpInput = jumpInput;
const jumpBtn = document.createElement('button');
jumpBtn.className = 'btn btn-secondary btn-icon tooltip';
jumpBtn.innerHTML = icons.jump; jumpBtn.setAttribute('data-tooltip', 'Jump to Page');
jumpBtn.onclick = () => navigateToPage(jumpInput.value);
elements.jumpBtn = jumpBtn;
jumpInput.onkeydown = (e) => { if (e.key === 'Enter' && !jumpBtn.disabled) jumpBtn.click(); };
pageJump.append(jumpInput, jumpBtn);
pageSection.append(pageNav, pageJump);
contentWrapper.appendChild(pageSection);
// Actions Section
const actionsSection = document.createElement('section');
actionsSection.className = 'actions-section';
const refreshBtn = document.createElement('button');
refreshBtn.className = 'btn btn-secondary tooltip';
refreshBtn.innerHTML = `${icons.refresh} <span>Refresh</span>`;
refreshBtn.setAttribute('data-tooltip', 'Reload Image (R)');
refreshBtn.onclick = () => updateImage();
elements.refreshBtn = refreshBtn;
const fullscreenBtn = document.createElement('button');
fullscreenBtn.className = 'btn btn-secondary tooltip';
fullscreenBtn.innerHTML = `${icons.fullscreen} <span>Fullscreen</span>`;
fullscreenBtn.setAttribute('data-tooltip', 'Toggle Fullscreen (F)');
fullscreenBtn.onclick = toggleFullscreen;
elements.fullscreenBtn = fullscreenBtn;
const pdfBtn = document.createElement('button');
pdfBtn.className = 'btn btn-primary tooltip';
pdfBtn.innerHTML = `${icons.pdf} <span>Export PDF</span>`;
pdfBtn.setAttribute('data-tooltip', 'Export All Pages as PDF (P)');
pdfBtn.onclick = exportToPDF;
elements.pdfBtn = pdfBtn;
actionsSection.append(refreshBtn, fullscreenBtn, pdfBtn);
contentWrapper.appendChild(actionsSection);
// Status Section
const statusSection = document.createElement('section');
statusSection.style.paddingBottom = '0'; statusSection.style.borderBottom = 'none';
const statusIndicator = document.createElement('div');
statusIndicator.className = 'status-indicator';
const loadingSpinner = document.createElement('div'); loadingSpinner.className = 'loading-spinner';
const statusText = document.createElement('span'); statusText.className = 'status-text';
statusIndicator.append(loadingSpinner, statusText);
elements.statusIndicator = statusIndicator; elements.loadingSpinner = loadingSpinner; elements.statusText = statusText;
statusSection.appendChild(statusIndicator);
contentWrapper.appendChild(statusSection);
// Keyboard Shortcuts Info Section
const shortcutsSection = document.createElement('section');
shortcutsSection.className = 'shortcuts-section';
shortcutsSection.innerHTML = `
<div class="shortcuts-title">Keyboard Shortcuts</div>
<ul class="shortcuts-list">
<li>Previous / Next Page <span class="key-combo"><kbd>←</kbd> / <kbd>→</kbd></span></li>
<li>First / Last Page <span class="key-combo"><kbd>Home</kbd> / <kbd>End</kbd></span></li>
<li>Toggle Fullscreen <kbd>F</kbd></li>
<li>Reload Image <kbd>R</kbd></li>
<li>Collapse / Expand <kbd>C</kbd></li>
<li>Export PDF <kbd>P</kbd></li>
<li>Edit Roll No <kbd>E</kbd></li>
<li>Exit Fullscreen <kbd>Esc</kbd></li>
</ul>
`;
contentWrapper.appendChild(shortcutsSection);
// NEW: Footer Section
const footer = document.createElement('footer');
footer.className = 'kitee-footer';
const footerText = document.createElement('span');
footerText.innerHTML = 'Made with ❤️ by Prajwal Panth'; // Use innerHTML for the heart emoji
const githubLink = document.createElement('a');
githubLink.href = 'https://github.com/prajwal-panth';
githubLink.target = '_blank';
githubLink.rel = 'noopener noreferrer';
githubLink.className = 'tooltip';
githubLink.setAttribute('data-tooltip', 'Visit GitHub Profile');
githubLink.innerHTML = icons.github; // Add the GitHub icon
footer.appendChild(footerText);
footer.appendChild(githubLink);
contentWrapper.appendChild(footer); // Add footer to the scrollable content
ui.appendChild(contentWrapper);
document.body.appendChild(ui);
}
// --- Initialization ---
function init() {
console.log("Snapify Initializing...");
elements.imageElement = document.querySelector('#cimg');
if (!elements.imageElement) {
console.error("Error: Could not find main image element (#cimg). Script cannot function.");
alert("Snapify Navigator Error: Main image element #cimg not found. Script disabled.");
return;
}
const pageLinks = document.querySelectorAll('#dg1 a');
state.totalPages = pageLinks.length;
if (state.totalPages === 0) {
console.warn("Warning: Could not find page links in #dg1. Total pages may be 0 or incorrect. PDF export might not work.");
} else {
console.log(`Found ${state.totalPages} pages.`);
}
createUI();
// Initial setup after UI creation
updateTheme();
updateCollapseState();
updateRollDisplay();
updateRollHistoryUI();
updatePageInfo();
if (!state.rollNumber) {
setTimeout(promptRollNumber, 150);
} else {
addToRollHistory(state.rollNumber);
updateImage(state.currentPage)
.catch(err => console.error("Initial image load failed:", err));
}
document.addEventListener('keydown', handleKeyPress);
console.log("Snapify Initialized.");
if (state.totalPages > 0 || state.rollNumber) {
setStatus('success', 'Navigator Ready', true);
} else {
setStatus('error', 'Ready, but no pages detected or roll set.', false);
}
}
// --- Run ---
if (document.readyState === "complete" || document.readyState === "interactive") {
setTimeout(init, 100);
} else {
window.addEventListener('DOMContentLoaded', init);
}
})();