Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
e61ee85885 | ||
|
|
51edff809a | ||
|
|
94a82eac07 | ||
|
|
7692b1c9dd | ||
|
|
02a62cce31 | ||
|
|
2802d780f3 |
+22
@@ -0,0 +1,22 @@
|
|||||||
|
# VS Code
|
||||||
|
.vscode/
|
||||||
|
*.code-workspace
|
||||||
|
|
||||||
|
# Windows
|
||||||
|
Thumbs.db
|
||||||
|
Desktop.ini
|
||||||
|
|
||||||
|
# macOS
|
||||||
|
.DS_Store
|
||||||
|
|
||||||
|
# Build and release output
|
||||||
|
releases/*.zip
|
||||||
|
dist/
|
||||||
|
build/
|
||||||
|
|
||||||
|
# Temporary files
|
||||||
|
*.tmp
|
||||||
|
*.temp
|
||||||
|
*.log
|
||||||
|
|
||||||
|
release/
|
||||||
@@ -0,0 +1,242 @@
|
|||||||
|
Hudra
|
||||||
|
Purpose
|
||||||
|
|
||||||
|
Hudra is a browser extension that turns structured QLearn lesson pages into a clean, classroom-friendly presentation.
|
||||||
|
|
||||||
|
Hudra presents existing QLearn content in a slide-oriented or presentation-oriented format.
|
||||||
|
|
||||||
|
It is not a QLearn page editor.
|
||||||
|
|
||||||
|
Core product promise
|
||||||
|
|
||||||
|
Hudra should allow a teacher to open an existing QLearn lesson and present it clearly without rebuilding the content in separate presentation software.
|
||||||
|
|
||||||
|
The original QLearn page remains the source of truth.
|
||||||
|
|
||||||
|
Responsibilities
|
||||||
|
|
||||||
|
Hudra may:
|
||||||
|
|
||||||
|
detect supported QLearn lesson pages;
|
||||||
|
identify presentation sections from page structure;
|
||||||
|
interpret supported headings as presentation boundaries;
|
||||||
|
present page content in a slide-like view;
|
||||||
|
provide previous and next navigation;
|
||||||
|
support keyboard navigation;
|
||||||
|
provide fullscreen or classroom presentation modes;
|
||||||
|
display progress through the presentation;
|
||||||
|
preserve embedded content where practical;
|
||||||
|
provide graceful fallback behaviour for unsupported elements;
|
||||||
|
restore the normal QLearn page when presentation mode closes;
|
||||||
|
remember appropriate user preferences.
|
||||||
|
Product boundaries
|
||||||
|
|
||||||
|
Hudra must not:
|
||||||
|
|
||||||
|
edit or permanently rewrite QLearn page content;
|
||||||
|
save changes into the QLearn editor;
|
||||||
|
become a general page-formatting tool;
|
||||||
|
add teacher notes to the source page;
|
||||||
|
become a general distraction-removal extension;
|
||||||
|
duplicate Pendra’s editing features;
|
||||||
|
duplicate Lildra’s general focus-mode features;
|
||||||
|
require users to rebuild lessons specifically for Hudra unless a documented structure is necessary.
|
||||||
|
Relationship to other Napper applications
|
||||||
|
Lildra
|
||||||
|
|
||||||
|
Lildra reduces surrounding QLearn clutter while leaving the content in its original continuous-page form.
|
||||||
|
|
||||||
|
Hudra provides a presentation experience.
|
||||||
|
|
||||||
|
A feature belongs in Hudra when it is specifically useful while presenting lesson content.
|
||||||
|
|
||||||
|
Pendra
|
||||||
|
|
||||||
|
Pendra helps teachers edit, structure and enhance QLearn pages.
|
||||||
|
|
||||||
|
Hudra may interpret structure created by Pendra, but it must not perform Pendra’s editing work.
|
||||||
|
|
||||||
|
Content interpretation
|
||||||
|
|
||||||
|
Hudra should rely on semantic page structure wherever possible.
|
||||||
|
|
||||||
|
Preferred signals include:
|
||||||
|
|
||||||
|
valid heading hierarchy;
|
||||||
|
clearly defined content sections;
|
||||||
|
predictable QLearn content containers;
|
||||||
|
explicit Hudra-compatible markers only where normal semantic structure is insufficient.
|
||||||
|
|
||||||
|
Do not rely unnecessarily on fragile visual selectors or exact class names.
|
||||||
|
|
||||||
|
QLearn may change its DOM structure. Detection should therefore be defensive and easy to update.
|
||||||
|
|
||||||
|
Behaviour principles
|
||||||
|
Never permanently alter lesson content.
|
||||||
|
Presentation mode must be reversible.
|
||||||
|
Closing Hudra should restore the original page state.
|
||||||
|
Missing elements must not cause the entire extension to fail.
|
||||||
|
Unsupported content should remain visible where possible.
|
||||||
|
Avoid hiding important content silently.
|
||||||
|
Provide clear feedback when a page cannot be presented reliably.
|
||||||
|
Do not activate on unrelated pages.
|
||||||
|
Avoid interfering with normal QLearn controls when Hudra is inactive.
|
||||||
|
User experience
|
||||||
|
|
||||||
|
Hudra should feel calm, predictable and suitable for classroom use.
|
||||||
|
|
||||||
|
Prioritise:
|
||||||
|
|
||||||
|
large readable content;
|
||||||
|
clear navigation;
|
||||||
|
minimal controls;
|
||||||
|
obvious presentation state;
|
||||||
|
reliable keyboard interaction;
|
||||||
|
smooth restoration of the normal page;
|
||||||
|
minimal setup.
|
||||||
|
|
||||||
|
Avoid:
|
||||||
|
|
||||||
|
crowded toolbars;
|
||||||
|
unnecessary configuration;
|
||||||
|
distracting animation;
|
||||||
|
ambiguous icons;
|
||||||
|
features unrelated to presenting content.
|
||||||
|
Accessibility
|
||||||
|
Preserve meaningful heading and document structure where practical.
|
||||||
|
Maintain keyboard navigation.
|
||||||
|
Ensure controls have accessible names.
|
||||||
|
Use sufficient colour contrast.
|
||||||
|
Avoid relying on colour alone to convey state.
|
||||||
|
Preserve alternative text for images.
|
||||||
|
Do not remove captions or transcripts from embedded content.
|
||||||
|
Avoid trapping keyboard focus.
|
||||||
|
Technical principles
|
||||||
|
Prefer plain JavaScript, HTML and CSS.
|
||||||
|
Avoid unnecessary frameworks and dependencies.
|
||||||
|
Keep the extension independently installable.
|
||||||
|
Request only the permissions Hudra genuinely needs.
|
||||||
|
Do not include remote executable code.
|
||||||
|
Do not include API keys, passwords or private credentials.
|
||||||
|
Validate DOM elements before accessing them.
|
||||||
|
Isolate QLearn selectors and detection logic where practical.
|
||||||
|
Avoid duplicated selectors across multiple files.
|
||||||
|
Use clear, descriptive names.
|
||||||
|
Keep functions focused.
|
||||||
|
Add comments only when behaviour is not obvious.
|
||||||
|
Remove temporary debugging output before release.
|
||||||
|
QLearn safety
|
||||||
|
|
||||||
|
Hudra operates inside an external platform that may change without notice.
|
||||||
|
|
||||||
|
Code must:
|
||||||
|
|
||||||
|
check that expected elements exist;
|
||||||
|
handle missing or changed selectors;
|
||||||
|
avoid assumptions about a single page layout;
|
||||||
|
fail without breaking the underlying QLearn page;
|
||||||
|
avoid deleting source elements;
|
||||||
|
avoid destructive DOM operations;
|
||||||
|
retain enough state to restore the original page;
|
||||||
|
document selectors that are especially fragile.
|
||||||
|
Permissions and privacy
|
||||||
|
|
||||||
|
Hudra should request the smallest possible set of browser permissions.
|
||||||
|
|
||||||
|
Any stored data should normally be limited to local preferences, such as:
|
||||||
|
|
||||||
|
display options;
|
||||||
|
presentation preferences;
|
||||||
|
last-used settings.
|
||||||
|
|
||||||
|
Do not collect or transmit:
|
||||||
|
|
||||||
|
student information;
|
||||||
|
lesson content;
|
||||||
|
QLearn credentials;
|
||||||
|
browsing history;
|
||||||
|
analytics;
|
||||||
|
personal data;
|
||||||
|
|
||||||
|
unless the repository documentation and privacy policy are deliberately updated to reflect verified behaviour.
|
||||||
|
|
||||||
|
Repository expectations
|
||||||
|
|
||||||
|
The repository should contain:
|
||||||
|
|
||||||
|
README.md
|
||||||
|
AGENTS.md
|
||||||
|
CHANGELOG.md
|
||||||
|
manifest.json
|
||||||
|
source files
|
||||||
|
icons
|
||||||
|
release tooling
|
||||||
|
relevant documentation
|
||||||
|
tests where practical
|
||||||
|
|
||||||
|
Generated release packages should not be committed unless there is a deliberate reason.
|
||||||
|
|
||||||
|
Release output should remain ignored through .gitignore.
|
||||||
|
|
||||||
|
Versioning
|
||||||
|
|
||||||
|
Hudra uses its own version number.
|
||||||
|
|
||||||
|
Use semantic versioning where practical:
|
||||||
|
|
||||||
|
patch: fixes without new user-facing capability;
|
||||||
|
minor: backwards-compatible features;
|
||||||
|
major: substantial or potentially disruptive changes.
|
||||||
|
|
||||||
|
The version in manifest.json must match the intended release.
|
||||||
|
|
||||||
|
Use release tags such as:
|
||||||
|
|
||||||
|
v0.9.3
|
||||||
|
v1.0.0
|
||||||
|
v1.1.0
|
||||||
|
|
||||||
|
Release packages should use names such as:
|
||||||
|
|
||||||
|
Hudra-v1.0.0.zip
|
||||||
|
Release requirements
|
||||||
|
|
||||||
|
Before preparing a release:
|
||||||
|
|
||||||
|
Confirm the manifest version.
|
||||||
|
Review requested permissions.
|
||||||
|
Run available checks and tests.
|
||||||
|
Test Hudra manually on representative QLearn pages.
|
||||||
|
Test activation and deactivation.
|
||||||
|
Test keyboard navigation.
|
||||||
|
Test fullscreen behaviour if supported.
|
||||||
|
Confirm the normal QLearn page is restored correctly.
|
||||||
|
Check the extension package contains only required runtime files.
|
||||||
|
Exclude development files, previous releases and generated folders.
|
||||||
|
Update CHANGELOG.md.
|
||||||
|
Update documentation when behaviour changes.
|
||||||
|
Identify remaining manual testing requirements.
|
||||||
|
Codex working rules
|
||||||
|
|
||||||
|
When asked to implement a feature:
|
||||||
|
|
||||||
|
Inspect the relevant implementation before editing.
|
||||||
|
Confirm that the feature belongs within Hudra’s scope.
|
||||||
|
Prefer the smallest reliable change.
|
||||||
|
Do not broaden permissions without explaining why.
|
||||||
|
Do not silently change public behaviour.
|
||||||
|
Preserve compatibility with existing structured pages where possible.
|
||||||
|
Update documentation and changelog entries when appropriate.
|
||||||
|
Run available validation.
|
||||||
|
Summarise changes clearly.
|
||||||
|
State what still requires testing inside QLearn.
|
||||||
|
|
||||||
|
When asked to review the repository:
|
||||||
|
|
||||||
|
distinguish confirmed defects from possible risks;
|
||||||
|
do not rewrite functioning code solely for stylistic preference;
|
||||||
|
identify fragile QLearn selectors;
|
||||||
|
identify destructive or irreversible DOM changes;
|
||||||
|
identify unnecessary permissions;
|
||||||
|
identify files accidentally included in release packages;
|
||||||
|
identify differences between documented and actual behaviour.
|
||||||
+80
-13
@@ -1,16 +1,83 @@
|
|||||||
# Hudra changelog
|
# Hudra
|
||||||
|
|
||||||
## 0.9.0
|
Turn HTML lessons into classroom presentations.
|
||||||
|
|
||||||
- Removed the startup fade and wordmark so presentations begin immediately.
|
## Hudra 0.10 Beta 1 — 2026-07-26
|
||||||
- Added live audience zoom controls from 70% to 150%.
|
|
||||||
- Added audience scroll up/down controls with press-and-hold support.
|
|
||||||
- Added mouse-wheel audience scrolling and Shift + wheel zooming over the current preview.
|
|
||||||
- Added Reset view and live view status.
|
|
||||||
- Kept the product name simply **Hudra**.
|
|
||||||
- Retained dual-screen presenter view, slide previews, timing, clicker support, audience blanking and video controls.
|
|
||||||
|
|
||||||
## 0.9.1
|
- Restrict presentation startup to supported QLearn and Instructure lesson pages.
|
||||||
- Added one narrowly scoped feature: slides containing exactly one image now fit that image to the full audience viewport using `object-fit: contain`.
|
- Show an accessible in-page message when Hudra cannot start.
|
||||||
- Landscape images fit to width and portrait images fit to height without cropping or distortion.
|
- Restore active presenter sessions after the extension service worker restarts.
|
||||||
- All other slide types retain the original Hudra 0.9 behaviour.
|
- Remove the unnecessary `tabs` permission.
|
||||||
|
- Preserve rich paragraph markup during slide pagination.
|
||||||
|
- Package only browser-extension runtime files in release builds.
|
||||||
|
|
||||||
|
Build once.
|
||||||
|
Present everywhere.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Why Hudra?
|
||||||
|
|
||||||
|
Teachers transitioning from OneNote and PowerPoint to QLearn often maintain two versions of the same lesson.
|
||||||
|
|
||||||
|
Hudra removes that duplication by turning the existing QLearn lesson into the presentation.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Features
|
||||||
|
|
||||||
|
✔ Automatic slide generation
|
||||||
|
|
||||||
|
✔ Presenter dashboard
|
||||||
|
|
||||||
|
✔ Dual-screen presentation
|
||||||
|
|
||||||
|
✔ Timer & clock
|
||||||
|
|
||||||
|
✔ Audience controls
|
||||||
|
|
||||||
|
✔ Keyboard navigation
|
||||||
|
|
||||||
|
✔ Image optimisation
|
||||||
|
|
||||||
|
✔ Browser extension
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Philosophy
|
||||||
|
|
||||||
|
The HTML lesson is the source of truth.
|
||||||
|
|
||||||
|
Hudra doesn't replace your LMS.
|
||||||
|
|
||||||
|
It simply gives it Presentation Mode.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Roadmap
|
||||||
|
|
||||||
|
Current beta release
|
||||||
|
|
||||||
|
Hudra 0.10 Beta 1
|
||||||
|
|
||||||
|
See ROADMAP.md
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Documentation
|
||||||
|
|
||||||
|
Vision
|
||||||
|
|
||||||
|
Architecture
|
||||||
|
|
||||||
|
Development Decisions
|
||||||
|
|
||||||
|
Roadmap
|
||||||
|
|
||||||
|
Contributing
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## License
|
||||||
|
|
||||||
|
MIT
|
||||||
|
|||||||
@@ -0,0 +1,40 @@
|
|||||||
|
# Contributing to Hudra
|
||||||
|
|
||||||
|
Hudra is currently an early-stage project.
|
||||||
|
|
||||||
|
## Development principles
|
||||||
|
|
||||||
|
Changes should be:
|
||||||
|
|
||||||
|
- focused
|
||||||
|
- easy to understand
|
||||||
|
- independently testable
|
||||||
|
- backwards compatible where practical
|
||||||
|
|
||||||
|
## Before making a change
|
||||||
|
|
||||||
|
1. Create or identify an issue.
|
||||||
|
2. Describe the problem being solved.
|
||||||
|
3. Define clear acceptance criteria.
|
||||||
|
4. Make the smallest practical change.
|
||||||
|
5. Test against the stable lesson examples.
|
||||||
|
6. Confirm existing presentation behaviour still works.
|
||||||
|
|
||||||
|
## Commit messages
|
||||||
|
|
||||||
|
Use short, descriptive commit messages, for example:
|
||||||
|
|
||||||
|
- Fix audience zoom reset
|
||||||
|
- Add presenter keyboard shortcut
|
||||||
|
- Improve image-only slide sizing
|
||||||
|
- Update installation documentation
|
||||||
|
|
||||||
|
## Releases
|
||||||
|
|
||||||
|
Stable releases should include:
|
||||||
|
|
||||||
|
- an updated manifest version
|
||||||
|
- changelog entry
|
||||||
|
- Git commit
|
||||||
|
- Git tag
|
||||||
|
- release ZIP
|
||||||
@@ -0,0 +1,21 @@
|
|||||||
|
MIT License
|
||||||
|
|
||||||
|
Copyright (c) 2026 Kaden Napper
|
||||||
|
|
||||||
|
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||||
|
of this software and associated documentation files (the "Software"), to deal
|
||||||
|
in the Software without restriction, including without limitation the rights
|
||||||
|
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||||
|
copies of the Software, and to permit persons to whom the Software is
|
||||||
|
furnished to do so, subject to the following conditions:
|
||||||
|
|
||||||
|
The above copyright notice and this permission notice shall be included in all
|
||||||
|
copies or substantial portions of the Software.
|
||||||
|
|
||||||
|
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
|
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||||
|
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||||
|
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||||
|
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||||
|
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||||
|
SOFTWARE.
|
||||||
@@ -3,6 +3,9 @@ Hudra
|
|||||||
|
|
||||||
Hudra transforms structured Canvas/QLearn pages into responsive presentations. All lesson processing remains local in the browser.
|
Hudra transforms structured Canvas/QLearn pages into responsive presentations. All lesson processing remains local in the browser.
|
||||||
|
|
||||||
|
CURRENT BETA
|
||||||
|
Hudra 0.10 Beta 1
|
||||||
|
|
||||||
INSTALL
|
INSTALL
|
||||||
1. Extract this ZIP file.
|
1. Extract this ZIP file.
|
||||||
2. Open edge://extensions, chrome://extensions or brave://extensions.
|
2. Open edge://extensions, chrome://extensions or brave://extensions.
|
||||||
|
|||||||
+48
@@ -0,0 +1,48 @@
|
|||||||
|
# Hudra Roadmap
|
||||||
|
|
||||||
|
Hudra is developed incrementally, with stability and simplicity prioritised
|
||||||
|
over large feature releases.
|
||||||
|
|
||||||
|
## Current beta release
|
||||||
|
|
||||||
|
### Hudra 0.10 Beta 1
|
||||||
|
|
||||||
|
- Supported-page detection for QLearn and Canvas
|
||||||
|
- Resilient presenter controls across service-worker restarts
|
||||||
|
- Rich paragraph markup preserved during pagination
|
||||||
|
- Reduced browser permissions
|
||||||
|
- Runtime-only release packaging
|
||||||
|
|
||||||
|
## Current stable release
|
||||||
|
|
||||||
|
### Hudra 0.9.1
|
||||||
|
|
||||||
|
- Automatic slide generation from HTML headings
|
||||||
|
- Presenter dashboard
|
||||||
|
- Dual-screen presentation
|
||||||
|
- Current and next slide previews
|
||||||
|
- Timer and clock
|
||||||
|
- Audience zoom and scrolling
|
||||||
|
- Keyboard and presentation remote support
|
||||||
|
- Image-only slide optimisation
|
||||||
|
|
||||||
|
## Near-term priorities
|
||||||
|
|
||||||
|
### Hudra 0.10
|
||||||
|
|
||||||
|
- Presenter interface refinement
|
||||||
|
- Improved reliability
|
||||||
|
- Documentation
|
||||||
|
- Classroom testing
|
||||||
|
- Accessibility review
|
||||||
|
|
||||||
|
## Future exploration
|
||||||
|
|
||||||
|
- Video-focused slide layouts
|
||||||
|
- Presenter annotations
|
||||||
|
- Laser pointer
|
||||||
|
- Improved media handling
|
||||||
|
- Wider LMS compatibility
|
||||||
|
- Managed browser deployment
|
||||||
|
|
||||||
|
Future features will be developed and tested individually to avoid regressions.
|
||||||
+18
@@ -0,0 +1,18 @@
|
|||||||
|
# Hudra Security Policy
|
||||||
|
|
||||||
|
Please report suspected Hudra vulnerabilities privately by emailing
|
||||||
|
support@napper.au with the subject **Hudra security report**.
|
||||||
|
|
||||||
|
Include:
|
||||||
|
|
||||||
|
- the Hudra version and browser version;
|
||||||
|
- the affected feature;
|
||||||
|
- clear reproduction steps; and
|
||||||
|
- the potential security or privacy impact.
|
||||||
|
|
||||||
|
Do not include passwords, authentication details, student records or private
|
||||||
|
lesson content. Use a minimal synthetic example if content is needed to explain
|
||||||
|
the issue.
|
||||||
|
|
||||||
|
Please do not disclose a suspected vulnerability publicly before the Hudra
|
||||||
|
maintainers have had a reasonable opportunity to investigate and respond.
|
||||||
+171
-1
@@ -2,6 +2,80 @@ const sessions = new Map();
|
|||||||
const tabToSession = new Map();
|
const tabToSession = new Map();
|
||||||
const windowToSession = new Map();
|
const windowToSession = new Map();
|
||||||
const previousWindowStates = new Map();
|
const previousWindowStates = new Map();
|
||||||
|
const sessionStorageKey = "hudraActiveSessions";
|
||||||
|
let persistStatePromise = Promise.resolve();
|
||||||
|
|
||||||
|
function rebuildSessionIndexes() {
|
||||||
|
tabToSession.clear();
|
||||||
|
windowToSession.clear();
|
||||||
|
|
||||||
|
sessions.forEach((session) => {
|
||||||
|
if (session.audienceTabId != null) {
|
||||||
|
tabToSession.set(session.audienceTabId, session.id);
|
||||||
|
}
|
||||||
|
if (session.audienceWindowId != null) {
|
||||||
|
windowToSession.set(session.audienceWindowId, session.id);
|
||||||
|
}
|
||||||
|
if (session.controlWindowId != null) {
|
||||||
|
windowToSession.set(session.controlWindowId, session.id);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async function restoreSessionState() {
|
||||||
|
const stored = await chrome.storage.session.get(sessionStorageKey);
|
||||||
|
const state = stored[sessionStorageKey];
|
||||||
|
if (!state) return;
|
||||||
|
|
||||||
|
for (const storedSession of state.sessions || []) {
|
||||||
|
if (!storedSession?.id) continue;
|
||||||
|
sessions.set(storedSession.id, {
|
||||||
|
...storedSession,
|
||||||
|
pageTitle: "Hudra",
|
||||||
|
current: 0,
|
||||||
|
count: 0,
|
||||||
|
audienceHidden: false,
|
||||||
|
audienceZoom: 100,
|
||||||
|
audienceScrollY: 0,
|
||||||
|
currentPreview: "",
|
||||||
|
nextPreview: ""
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const [windowId, windowState] of state.previousWindowStates || []) {
|
||||||
|
previousWindowStates.set(Number(windowId), windowState);
|
||||||
|
}
|
||||||
|
|
||||||
|
rebuildSessionIndexes();
|
||||||
|
}
|
||||||
|
|
||||||
|
const sessionStateReady = restoreSessionState().catch((error) => {
|
||||||
|
console.warn("Hudra could not restore its active sessions:", error);
|
||||||
|
});
|
||||||
|
|
||||||
|
async function ensureSessionStateReady() {
|
||||||
|
await sessionStateReady;
|
||||||
|
}
|
||||||
|
|
||||||
|
function persistSessionState() {
|
||||||
|
const state = {
|
||||||
|
sessions: [...sessions.values()].map((session) => ({
|
||||||
|
id: session.id,
|
||||||
|
mode: session.mode,
|
||||||
|
sourceTabId: session.sourceTabId,
|
||||||
|
audienceTabId: session.audienceTabId,
|
||||||
|
audienceWindowId: session.audienceWindowId,
|
||||||
|
controlWindowId: session.controlWindowId
|
||||||
|
})),
|
||||||
|
previousWindowStates: [...previousWindowStates.entries()]
|
||||||
|
};
|
||||||
|
|
||||||
|
persistStatePromise = persistStatePromise
|
||||||
|
.catch(() => {})
|
||||||
|
.then(() => chrome.storage.session.set({ [sessionStorageKey]: state }));
|
||||||
|
|
||||||
|
return persistStatePromise;
|
||||||
|
}
|
||||||
|
|
||||||
function makeSessionId() {
|
function makeSessionId() {
|
||||||
return `${Date.now()}-${Math.random().toString(36).slice(2)}`;
|
return `${Date.now()}-${Math.random().toString(36).slice(2)}`;
|
||||||
@@ -32,6 +106,57 @@ function chooseSecondaryDisplay(displays) {
|
|||||||
return { primary, secondary };
|
return { primary, secondary };
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function isSupportedLessonUrl(value) {
|
||||||
|
try {
|
||||||
|
const url = new URL(value);
|
||||||
|
const hostname = url.hostname.toLowerCase();
|
||||||
|
return url.protocol === "https:" && (
|
||||||
|
hostname === "qlearn.eq.edu.au" ||
|
||||||
|
hostname.endsWith(".qlearn.eq.edu.au") ||
|
||||||
|
hostname.endsWith(".instructure.com")
|
||||||
|
);
|
||||||
|
} catch (error) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function showPageNotice(tabId, message) {
|
||||||
|
await chrome.scripting.executeScript({
|
||||||
|
target: { tabId },
|
||||||
|
files: ["page-detection.js"]
|
||||||
|
});
|
||||||
|
|
||||||
|
await chrome.scripting.executeScript({
|
||||||
|
target: { tabId },
|
||||||
|
func: (noticeMessage) => window.__hudraPageDetector.showNotice(noticeMessage),
|
||||||
|
args: [message]
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async function inspectLessonPage(tabId) {
|
||||||
|
await chrome.scripting.executeScript({
|
||||||
|
target: { tabId },
|
||||||
|
files: ["page-detection.js"]
|
||||||
|
});
|
||||||
|
|
||||||
|
const results = await chrome.scripting.executeScript({
|
||||||
|
target: { tabId },
|
||||||
|
func: () => window.__hudraPageDetector.inspect()
|
||||||
|
});
|
||||||
|
|
||||||
|
return results[0]?.result || { supported: false, reason: "INSPECTION_FAILED" };
|
||||||
|
}
|
||||||
|
|
||||||
|
function messageForUnsupportedPage(reason) {
|
||||||
|
if (reason === "NO_LESSON_TITLE") {
|
||||||
|
return "Hudra couldn’t start because this lesson has no H1 title.";
|
||||||
|
}
|
||||||
|
if (reason === "EMPTY_LESSON") {
|
||||||
|
return "Hudra couldn’t start because this lesson has no presentable content.";
|
||||||
|
}
|
||||||
|
return "Hudra couldn’t start because this page does not contain a supported lesson.";
|
||||||
|
}
|
||||||
|
|
||||||
async function waitForTabComplete(tabId, timeoutMs = 20000) {
|
async function waitForTabComplete(tabId, timeoutMs = 20000) {
|
||||||
const existing = await chrome.tabs.get(tabId).catch(() => null);
|
const existing = await chrome.tabs.get(tabId).catch(() => null);
|
||||||
if (existing?.status === "complete") return;
|
if (existing?.status === "complete") return;
|
||||||
@@ -57,7 +182,7 @@ async function waitForTabComplete(tabId, timeoutMs = 20000) {
|
|||||||
async function injectPresenter(tabId, config) {
|
async function injectPresenter(tabId, config) {
|
||||||
await chrome.scripting.executeScript({
|
await chrome.scripting.executeScript({
|
||||||
target: { tabId },
|
target: { tabId },
|
||||||
files: ["presenter.js"]
|
files: ["page-detection.js", "presenter.js"]
|
||||||
});
|
});
|
||||||
|
|
||||||
await chrome.tabs.sendMessage(tabId, {
|
await chrome.tabs.sendMessage(tabId, {
|
||||||
@@ -67,6 +192,7 @@ async function injectPresenter(tabId, config) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async function startSingleScreen(tab) {
|
async function startSingleScreen(tab) {
|
||||||
|
await ensureSessionStateReady();
|
||||||
if (!tab.id || tab.windowId == null) return;
|
if (!tab.id || tab.windowId == null) return;
|
||||||
|
|
||||||
const sessionId = makeSessionId();
|
const sessionId = makeSessionId();
|
||||||
@@ -93,12 +219,14 @@ async function startSingleScreen(tab) {
|
|||||||
sessions.set(sessionId, session);
|
sessions.set(sessionId, session);
|
||||||
tabToSession.set(tab.id, sessionId);
|
tabToSession.set(tab.id, sessionId);
|
||||||
windowToSession.set(tab.windowId, sessionId);
|
windowToSession.set(tab.windowId, sessionId);
|
||||||
|
await persistSessionState();
|
||||||
|
|
||||||
await injectPresenter(tab.id, { mode: "single", sessionId });
|
await injectPresenter(tab.id, { mode: "single", sessionId });
|
||||||
await chrome.windows.update(tab.windowId, { state: "fullscreen" });
|
await chrome.windows.update(tab.windowId, { state: "fullscreen" });
|
||||||
}
|
}
|
||||||
|
|
||||||
async function startDualScreen(tab, primary, secondary) {
|
async function startDualScreen(tab, primary, secondary) {
|
||||||
|
await ensureSessionStateReady();
|
||||||
if (!tab.id || !tab.url) return;
|
if (!tab.id || !tab.url) return;
|
||||||
|
|
||||||
const sessionId = makeSessionId();
|
const sessionId = makeSessionId();
|
||||||
@@ -153,6 +281,7 @@ async function startDualScreen(tab, primary, secondary) {
|
|||||||
tabToSession.set(audienceTab.id, sessionId);
|
tabToSession.set(audienceTab.id, sessionId);
|
||||||
windowToSession.set(audienceWindow.id, sessionId);
|
windowToSession.set(audienceWindow.id, sessionId);
|
||||||
if (controlWindow.id != null) windowToSession.set(controlWindow.id, sessionId);
|
if (controlWindow.id != null) windowToSession.set(controlWindow.id, sessionId);
|
||||||
|
await persistSessionState();
|
||||||
|
|
||||||
await waitForTabComplete(audienceTab.id);
|
await waitForTabComplete(audienceTab.id);
|
||||||
await injectPresenter(audienceTab.id, { mode: "audience", sessionId });
|
await injectPresenter(audienceTab.id, { mode: "audience", sessionId });
|
||||||
@@ -180,6 +309,7 @@ async function startDualScreen(tab, primary, secondary) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async function endSession(sessionId, reason = "ended") {
|
async function endSession(sessionId, reason = "ended") {
|
||||||
|
await ensureSessionStateReady();
|
||||||
const session = sessions.get(sessionId);
|
const session = sessions.get(sessionId);
|
||||||
if (!session) return;
|
if (!session) return;
|
||||||
|
|
||||||
@@ -197,10 +327,12 @@ async function endSession(sessionId, reason = "ended") {
|
|||||||
|
|
||||||
const previousState = previousWindowStates.get(session.audienceWindowId) || "maximized";
|
const previousState = previousWindowStates.get(session.audienceWindowId) || "maximized";
|
||||||
previousWindowStates.delete(session.audienceWindowId);
|
previousWindowStates.delete(session.audienceWindowId);
|
||||||
|
await persistSessionState();
|
||||||
await chrome.windows.update(session.audienceWindowId, { state: previousState }).catch(async () => {
|
await chrome.windows.update(session.audienceWindowId, { state: previousState }).catch(async () => {
|
||||||
await chrome.windows.update(session.audienceWindowId, { state: "maximized" }).catch(() => {});
|
await chrome.windows.update(session.audienceWindowId, { state: "maximized" }).catch(() => {});
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
|
await persistSessionState();
|
||||||
if (session.audienceWindowId != null) {
|
if (session.audienceWindowId != null) {
|
||||||
await chrome.windows.remove(session.audienceWindowId).catch(() => {});
|
await chrome.windows.remove(session.audienceWindowId).catch(() => {});
|
||||||
}
|
}
|
||||||
@@ -211,6 +343,7 @@ async function endSession(sessionId, reason = "ended") {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async function sendCommand(sessionId, command, value) {
|
async function sendCommand(sessionId, command, value) {
|
||||||
|
await ensureSessionStateReady();
|
||||||
const session = sessions.get(sessionId);
|
const session = sessions.get(sessionId);
|
||||||
if (!session?.audienceTabId) return;
|
if (!session?.audienceTabId) return;
|
||||||
|
|
||||||
@@ -227,6 +360,7 @@ async function sendCommand(sessionId, command, value) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async function toggleHudraForTab(tab) {
|
async function toggleHudraForTab(tab) {
|
||||||
|
await ensureSessionStateReady();
|
||||||
if (!tab?.id) return;
|
if (!tab?.id) return;
|
||||||
|
|
||||||
const windowSessionId = tab.windowId != null ? windowToSession.get(tab.windowId) : null;
|
const windowSessionId = tab.windowId != null ? windowToSession.get(tab.windowId) : null;
|
||||||
@@ -241,6 +375,30 @@ async function toggleHudraForTab(tab) {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (!isSupportedLessonUrl(tab.url)) {
|
||||||
|
await showPageNotice(
|
||||||
|
tab.id,
|
||||||
|
"Hudra works on QLearn and Canvas lesson pages hosted by Instructure."
|
||||||
|
).catch(() => {});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
let inspection;
|
||||||
|
try {
|
||||||
|
inspection = await inspectLessonPage(tab.id);
|
||||||
|
} catch (error) {
|
||||||
|
await showPageNotice(
|
||||||
|
tab.id,
|
||||||
|
"Hudra couldn’t inspect this page. Wait for it to finish loading and try again."
|
||||||
|
).catch(() => {});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!inspection.supported) {
|
||||||
|
await showPageNotice(tab.id, messageForUnsupportedPage(inspection.reason)).catch(() => {});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const displays = await getUsableDisplays();
|
const displays = await getUsableDisplays();
|
||||||
const { primary, secondary } = chooseSecondaryDisplay(displays);
|
const { primary, secondary } = chooseSecondaryDisplay(displays);
|
||||||
@@ -270,6 +428,7 @@ chrome.commands.onCommand.addListener(async (command) => {
|
|||||||
|
|
||||||
chrome.runtime.onMessage.addListener((message, sender, sendResponse) => {
|
chrome.runtime.onMessage.addListener((message, sender, sendResponse) => {
|
||||||
if (message?.type === "QLEARN_PRESENTER_STATE") {
|
if (message?.type === "QLEARN_PRESENTER_STATE") {
|
||||||
|
ensureSessionStateReady().then(() => {
|
||||||
const session = sessions.get(message.sessionId);
|
const session = sessions.get(message.sessionId);
|
||||||
if (!session) return;
|
if (!session) return;
|
||||||
|
|
||||||
@@ -295,6 +454,7 @@ chrome.runtime.onMessage.addListener((message, sender, sendResponse) => {
|
|||||||
currentPreview: session.currentPreview,
|
currentPreview: session.currentPreview,
|
||||||
nextPreview: session.nextPreview
|
nextPreview: session.nextPreview
|
||||||
}).catch(() => {});
|
}).catch(() => {});
|
||||||
|
});
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -309,7 +469,14 @@ chrome.runtime.onMessage.addListener((message, sender, sendResponse) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (message?.type === "QLEARN_PRESENTER_GET_SESSION") {
|
if (message?.type === "QLEARN_PRESENTER_GET_SESSION") {
|
||||||
|
ensureSessionStateReady().then(() => {
|
||||||
const session = sessions.get(message.sessionId);
|
const session = sessions.get(message.sessionId);
|
||||||
|
if (session?.audienceTabId != null) {
|
||||||
|
chrome.tabs.sendMessage(session.audienceTabId, {
|
||||||
|
type: "QLEARN_PRESENTER_COMMAND",
|
||||||
|
command: "reportState"
|
||||||
|
}).catch(() => {});
|
||||||
|
}
|
||||||
sendResponse(session ? {
|
sendResponse(session ? {
|
||||||
sessionId: session.id,
|
sessionId: session.id,
|
||||||
mode: session.mode,
|
mode: session.mode,
|
||||||
@@ -322,11 +489,13 @@ chrome.runtime.onMessage.addListener((message, sender, sendResponse) => {
|
|||||||
currentPreview: session.currentPreview,
|
currentPreview: session.currentPreview,
|
||||||
nextPreview: session.nextPreview
|
nextPreview: session.nextPreview
|
||||||
} : null);
|
} : null);
|
||||||
|
});
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
chrome.windows.onRemoved.addListener((windowId) => {
|
chrome.windows.onRemoved.addListener((windowId) => {
|
||||||
|
ensureSessionStateReady().then(() => {
|
||||||
const sessionId = windowToSession.get(windowId);
|
const sessionId = windowToSession.get(windowId);
|
||||||
if (!sessionId) return;
|
if (!sessionId) return;
|
||||||
const session = sessions.get(sessionId);
|
const session = sessions.get(sessionId);
|
||||||
@@ -337,3 +506,4 @@ chrome.windows.onRemoved.addListener((windowId) => {
|
|||||||
endSession(sessionId, "window-closed").catch(() => {});
|
endSession(sessionId, "window-closed").catch(() => {});
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
});
|
||||||
|
|||||||
@@ -0,0 +1,73 @@
|
|||||||
|
# Hudra Architecture
|
||||||
|
|
||||||
|
## Overview
|
||||||
|
|
||||||
|
Hudra is a Manifest V3 browser extension that presents the existing DOM of an
|
||||||
|
HTML lesson.
|
||||||
|
|
||||||
|
It does not convert the lesson into a proprietary format.
|
||||||
|
|
||||||
|
## Slide detection
|
||||||
|
|
||||||
|
The current presentation model uses HTML headings:
|
||||||
|
|
||||||
|
- H1 identifies the lesson title
|
||||||
|
- H2 identifies the beginning of a new slide
|
||||||
|
- H3 identifies the beginning of a new slide
|
||||||
|
- H4 identifies a subsection within the current slide
|
||||||
|
- content following an H2 or H3 belongs to that slide until the next H2 or H3
|
||||||
|
|
||||||
|
Pages without H2 or H3 headings are presented as a single slide.
|
||||||
|
|
||||||
|
## Supported pages
|
||||||
|
|
||||||
|
Hudra starts only on secure QLearn pages at `qlearn.eq.edu.au` (including
|
||||||
|
subdomains) and Canvas pages on an `instructure.com` subdomain. A supported
|
||||||
|
page must also contain a recognised lesson-content container and an H1 lesson
|
||||||
|
title. Unsupported pages receive a temporary in-page notice and are not put
|
||||||
|
into presentation mode.
|
||||||
|
|
||||||
|
## Main components
|
||||||
|
|
||||||
|
### background.js
|
||||||
|
|
||||||
|
Handles extension-level behaviour and presentation launch logic.
|
||||||
|
Active session routing and window-restoration metadata is kept in
|
||||||
|
`chrome.storage.session` so presenter controls survive service-worker restarts.
|
||||||
|
Lesson content and slide previews are not persisted.
|
||||||
|
|
||||||
|
### controls.html
|
||||||
|
|
||||||
|
Provides the presenter dashboard interface.
|
||||||
|
|
||||||
|
### controls.css
|
||||||
|
|
||||||
|
Styles the presenter controls.
|
||||||
|
|
||||||
|
### controls.js
|
||||||
|
|
||||||
|
Manages presenter navigation, controls and audience communication.
|
||||||
|
|
||||||
|
### presenter.js
|
||||||
|
|
||||||
|
Creates and manages the audience presentation experience.
|
||||||
|
|
||||||
|
### page-detection.js
|
||||||
|
|
||||||
|
Contains the shared lesson-container and title detection used before launch
|
||||||
|
and when presentation mode starts.
|
||||||
|
|
||||||
|
### manifest.json
|
||||||
|
|
||||||
|
Defines the extension metadata, permissions and version.
|
||||||
|
|
||||||
|
## Design constraints
|
||||||
|
|
||||||
|
Hudra should avoid:
|
||||||
|
|
||||||
|
- cloning complex media unnecessarily
|
||||||
|
- fragile LMS-specific DOM assumptions
|
||||||
|
- large content-detection systems
|
||||||
|
- destructive modification of the original lesson
|
||||||
|
|
||||||
|
New media features should be implemented and tested independently.
|
||||||
@@ -0,0 +1,149 @@
|
|||||||
|
# Chrome Web Store submission — Hudra 0.10 Beta 1
|
||||||
|
|
||||||
|
Prepared: 29 July 2026
|
||||||
|
|
||||||
|
## Package
|
||||||
|
|
||||||
|
- Product name: `Hudra`
|
||||||
|
- Manifest version: `3`
|
||||||
|
- Extension version: `0.10.0`
|
||||||
|
- Display version: `0.10 Beta 1`
|
||||||
|
- Existing package: `release/Hudra-v0.10.0.zip`
|
||||||
|
- This documentation update does not require a new package upload or build.
|
||||||
|
- Primary category: `Productivity`
|
||||||
|
- Language: `English (Australia)` if available; otherwise `English`
|
||||||
|
|
||||||
|
## Short description
|
||||||
|
|
||||||
|
Turn structured QLearn and Canvas lessons into clear classroom presentations with presenter and audience views.
|
||||||
|
|
||||||
|
## Detailed description
|
||||||
|
|
||||||
|
Hudra turns structured QLearn and Canvas lesson pages into clear, classroom-ready presentations.
|
||||||
|
|
||||||
|
Open a supported lesson and launch Hudra from the browser toolbar or with Alt+P. Hudra converts the existing page into a slide-style teaching experience without requiring the lesson to be rebuilt in separate presentation software.
|
||||||
|
|
||||||
|
Features include:
|
||||||
|
• Dedicated presenter and audience views
|
||||||
|
• Current and next slide previews
|
||||||
|
• Keyboard and on-screen navigation
|
||||||
|
• Classroom clock and timer
|
||||||
|
• Dual-screen presentation support
|
||||||
|
• Audience zoom and scrolling controls
|
||||||
|
• Support for structured lesson content and embedded media
|
||||||
|
• Quick restoration of the original lesson page when presenting ends
|
||||||
|
|
||||||
|
Hudra processes lesson content and generated slide previews temporarily in browser memory to provide the requested presentation. Lesson content and previews are not written to extension storage or transmitted to Napper.au.
|
||||||
|
|
||||||
|
Hudra stores only temporary presentation-session metadata, such as presentation mode and tab and window identifiers, in session-scoped browser storage. This information coordinates and restores an active presentation and is cleared when the browser session ends.
|
||||||
|
|
||||||
|
Hudra requires no account and contains no advertising, analytics, telemetry, tracking or remote executable code.
|
||||||
|
|
||||||
|
Hudra is an independent project and is not affiliated with or endorsed by Instructure or the Queensland Department of Education.
|
||||||
|
|
||||||
|
Support: https://napper.au/hudra/support
|
||||||
|
Privacy policy: https://napper.au/hudra/privacy
|
||||||
|
Terms of use: https://napper.au/hudra/terms
|
||||||
|
Source code: https://git.napper.au/napper/hudra
|
||||||
|
|
||||||
|
## Single purpose
|
||||||
|
|
||||||
|
Hudra converts the active supported QLearn or Canvas lesson page into a temporary, reversible classroom presentation with presenter and audience controls.
|
||||||
|
|
||||||
|
## Permission justifications
|
||||||
|
|
||||||
|
### `activeTab`
|
||||||
|
|
||||||
|
Provides temporary, user-invoked access after the toolbar click or keyboard shortcut. Hudra uses it to inspect the selected page and either start presentation mode or display an unsupported-page notice.
|
||||||
|
|
||||||
|
### `scripting`
|
||||||
|
|
||||||
|
Injects only packaged page-detection and presentation code into the user-selected supported page. No remote code is downloaded or executed.
|
||||||
|
|
||||||
|
### `storage`
|
||||||
|
|
||||||
|
Stores temporary active-presentation metadata in `chrome.storage.session` so presenter and audience windows can remain coordinated and the session can recover if the extension service worker restarts. Lesson content is not persisted.
|
||||||
|
|
||||||
|
### `system.display`
|
||||||
|
|
||||||
|
Detects available displays so Hudra can place presenter and audience windows correctly for dual-screen classroom presentation.
|
||||||
|
|
||||||
|
### Host permissions
|
||||||
|
|
||||||
|
`https://*.qlearn.eq.edu.au/*` and `https://*.instructure.com/*` provide access only on supported QLearn and Canvas lesson hosts. This access is particularly required for the newly created dual-screen audience tab because the original tab's `activeTab` grant does not transfer to that new tab. Hudra still activates only after an explicit toolbar click or keyboard shortcut and does not activate on unrelated websites.
|
||||||
|
|
||||||
|
## Privacy practices answers
|
||||||
|
|
||||||
|
- Handles website content: `Yes` — lesson content and generated previews are processed temporarily in browser memory to create the requested presentation.
|
||||||
|
- Handles web browsing activity: select only if the dashboard defines access to the active lesson URL/page as browsing activity. Hudra does not collect or transmit browsing history.
|
||||||
|
- Personally identifiable information: `No`, unless future functionality deliberately reads or retains such information.
|
||||||
|
- Authentication information: `No` — Hudra does not read or transmit credentials or authentication cookies.
|
||||||
|
- Financial, health, personal communications, location or form data: `No`.
|
||||||
|
- Data sold to third parties: `No`.
|
||||||
|
- Data used for advertising or creditworthiness: `No`.
|
||||||
|
- Data transmitted off the device by Hudra: `No`. Existing lesson and embedded resources continue to load from their original providers.
|
||||||
|
- Limited Use certification: certify after confirming these answers match the dashboard wording.
|
||||||
|
|
||||||
|
Privacy policy URL required: publish `docs/PRIVACY.md` at a stable public HTTPS URL, then enter that URL in the dashboard.
|
||||||
|
|
||||||
|
## Graphic assets
|
||||||
|
|
||||||
|
Upload screenshots in this order:
|
||||||
|
|
||||||
|
1. `store-assets/ready-to-upload/01-start-with-qlearn.png`
|
||||||
|
2. `store-assets/ready-to-upload/02-present-on-one-screen.png`
|
||||||
|
3. `store-assets/ready-to-upload/03-clean-audience-view.png`
|
||||||
|
4. `store-assets/ready-to-upload/04-presenter-dashboard.png`
|
||||||
|
5. `store-assets/ready-to-upload/05-before-and-after.png`
|
||||||
|
|
||||||
|
Additional assets:
|
||||||
|
|
||||||
|
- Store icon: `store-assets/ready-to-upload/store-icon-128.png`
|
||||||
|
- Small promotional tile: `store-assets/ready-to-upload/small-promo-440x280.png`
|
||||||
|
- Optional marquee: `store-assets/ready-to-upload/marquee-promo-1400x560.png`
|
||||||
|
|
||||||
|
## Distribution recommendation
|
||||||
|
|
||||||
|
For the first beta, use `Unlisted` visibility if testing with a controlled teacher group. Use `Public` only when the support and privacy-policy URLs are live and representative QLearn manual testing is complete.
|
||||||
|
|
||||||
|
## Reviewer notes
|
||||||
|
|
||||||
|
Hudra works only on supported QLearn and Canvas lesson pages. To test it, open a structured lesson page under one of the declared host patterns, click the Hudra toolbar icon, and use the on-screen controls or keyboard navigation. Dual-screen Presenter View requires a second display; single-screen mode can be tested with one display.
|
||||||
|
|
||||||
|
Hudra does not modify or save the source lesson. Exiting presentation mode restores the normal page.
|
||||||
|
|
||||||
|
If the reviewer cannot access a QLearn environment, provide a test Canvas/Instructure lesson URL and test account through the dashboard's secure reviewer-instructions field. Do not place credentials in this document or in the extension package.
|
||||||
|
|
||||||
|
## Manual checks before submission
|
||||||
|
|
||||||
|
- Test activation and exit on representative QLearn and Canvas lesson pages.
|
||||||
|
- Confirm unsupported pages show a clear message and remain unchanged.
|
||||||
|
- Test previous, next, first and last navigation.
|
||||||
|
- Test Escape and the toolbar button restore the original page.
|
||||||
|
- Test dual-screen window placement with an attached second display.
|
||||||
|
- Test audience zoom, scroll, hide and video controls.
|
||||||
|
- Confirm the privacy-policy and support URLs are publicly accessible.
|
||||||
|
- Confirm the screenshots contain no private production lesson or student information.
|
||||||
|
|
||||||
|
## Dashboard fields to review for the existing 0.10 Beta 1 application
|
||||||
|
|
||||||
|
Chrome Web Store:
|
||||||
|
|
||||||
|
- **Package**: do not upload a new package; retain the existing 0.10.0 submission.
|
||||||
|
- **Store listing > Detailed description**: replace with the exact detailed description above.
|
||||||
|
- **Store listing > Short description**: use the short description above.
|
||||||
|
- **Privacy > Single purpose description**: use the single-purpose statement above.
|
||||||
|
- **Privacy > Permission justifications**: update `activeTab`, `scripting`, `storage`, `system.display` and host-permission explanations to the text above.
|
||||||
|
- **Privacy > Data usage**: declare website content processing; confirm lesson content/previews are memory-only, session metadata is session-scoped, and no data is sold, used for advertising or transmitted to Napper.au.
|
||||||
|
- **Privacy policy URL**: `https://napper.au/hudra/privacy`.
|
||||||
|
- **Support URL**: `https://napper.au/hudra/support`.
|
||||||
|
- **Distribution** and **Test instructions**: retain the intended visibility and update reviewer notes if the supplied test lesson/account changes.
|
||||||
|
|
||||||
|
Microsoft Edge Add-ons:
|
||||||
|
|
||||||
|
- **Package**: do not upload a new package; retain the existing 0.10.0 submission.
|
||||||
|
- **Properties > Description** and **Short description**: use the exact detailed and short descriptions above.
|
||||||
|
- **Properties > Version notes**: no version-note change is required because this is supporting documentation for the existing build.
|
||||||
|
- **Privacy policy URL**: `https://napper.au/hudra/privacy`.
|
||||||
|
- **Support website**: `https://napper.au/hudra/support`.
|
||||||
|
- **Notes for certification**: use the reviewer notes above and provide any test account only through the dashboard's secure field.
|
||||||
@@ -0,0 +1,31 @@
|
|||||||
|
# Hudra Development Decisions
|
||||||
|
|
||||||
|
## Decision 001 — HTML is the source of truth
|
||||||
|
|
||||||
|
Hudra presents the existing lesson rather than requiring teachers to maintain
|
||||||
|
a separate presentation.
|
||||||
|
|
||||||
|
## Decision 002 — No proprietary lesson format
|
||||||
|
|
||||||
|
Teachers should continue authoring normal HTML in QLearn, Canvas or another
|
||||||
|
web-based platform.
|
||||||
|
|
||||||
|
## Decision 003 — Simplicity over automation
|
||||||
|
|
||||||
|
Hudra should avoid complicated content heuristics unless they are reliable and
|
||||||
|
provide a clear classroom benefit.
|
||||||
|
|
||||||
|
## Decision 004 — Stable features are added incrementally
|
||||||
|
|
||||||
|
Media-specific improvements should be developed and tested one at a time.
|
||||||
|
|
||||||
|
## Decision 005 — Hudra 0.9.1 is the stable baseline
|
||||||
|
|
||||||
|
Hudra 0.9.1 is the known-good version and is tagged in Git as v0.9.1.
|
||||||
|
|
||||||
|
## Decision 006 — Public and internal versions may differ
|
||||||
|
|
||||||
|
The visible product branding may show a major/minor version such as Hudra 0.9.
|
||||||
|
|
||||||
|
The browser extension manifest may use a patch version such as 0.9.1 for
|
||||||
|
maintenance and debugging.
|
||||||
@@ -0,0 +1,47 @@
|
|||||||
|
# Hudra Privacy Policy
|
||||||
|
|
||||||
|
Effective date: 29 July 2026
|
||||||
|
|
||||||
|
Hudra is a browser extension that turns supported QLearn and Canvas lesson pages into a classroom presentation. Hudra processes lesson page content locally in the user's browser to provide this feature.
|
||||||
|
|
||||||
|
## Information Hudra handles
|
||||||
|
|
||||||
|
When the user starts Hudra, the extension reads the content and structure of the active supported lesson page. This website content may contain information entered into or displayed by QLearn or Canvas. Lesson content and generated current/next slide previews are processed temporarily in browser memory during the active presentation.
|
||||||
|
|
||||||
|
Hudra keeps only temporary presentation-session metadata in `chrome.storage.session`, including the presentation mode and presentation tab and window identifiers. This metadata coordinates and restores an active presentation and is cleared when the browser session ends.
|
||||||
|
|
||||||
|
## How information is used
|
||||||
|
|
||||||
|
Lesson page content is used only to generate and display the presentation requested by the user. Temporary session information is used only to coordinate presenter and audience windows and restore the normal lesson page when presentation mode closes.
|
||||||
|
|
||||||
|
## Collection, transmission and sharing
|
||||||
|
|
||||||
|
Hudra does not transmit lesson content or generated slide previews to Napper.au. It does not transmit browsing activity, credentials, personal information or presentation-session metadata to Napper.au or other third parties.
|
||||||
|
|
||||||
|
Hudra includes no advertising, analytics, telemetry, tracking or remote executable code. It does not sell or share user data.
|
||||||
|
|
||||||
|
Existing lesson resources, such as images, videos and embedded media, continue to load from the original providers referenced by the lesson page. Hudra does not proxy those resources through Napper.au.
|
||||||
|
|
||||||
|
## Storage and retention
|
||||||
|
|
||||||
|
Hudra does not write lesson content or generated slide previews to `chrome.storage`. Active presentation metadata is stored only in Chrome's session-scoped extension storage and is not synchronized to external services.
|
||||||
|
|
||||||
|
## Permissions
|
||||||
|
|
||||||
|
- `activeTab` provides temporary access after a toolbar click or keyboard shortcut so Hudra can inspect the selected page or show an unsupported-page notice.
|
||||||
|
- `scripting` injects only packaged page-detection and presentation code.
|
||||||
|
- `storage` holds temporary session metadata only, using `chrome.storage.session`.
|
||||||
|
- `system.display` detects displays for presenter and audience window placement.
|
||||||
|
- QLearn and Instructure host access permits Hudra to run on supported lesson pages. It is particularly required for a newly created dual-screen audience tab because the original tab's `activeTab` grant does not transfer to that tab.
|
||||||
|
|
||||||
|
Hudra activates only after an explicit toolbar click or keyboard shortcut.
|
||||||
|
|
||||||
|
## Changes to this policy
|
||||||
|
|
||||||
|
If Hudra's data practices change, this policy and the Chrome Web Store privacy disclosures will be updated before the changed version is published.
|
||||||
|
|
||||||
|
## Contact
|
||||||
|
|
||||||
|
Privacy questions can be submitted through the support link on Hudra's Chrome Web Store listing.
|
||||||
|
|
||||||
|
The use of information received from Google APIs will adhere to the Chrome Web Store User Data Policy, including the Limited Use requirements.
|
||||||
@@ -0,0 +1,48 @@
|
|||||||
|
# Hudra Vision
|
||||||
|
|
||||||
|
## Purpose
|
||||||
|
|
||||||
|
Hudra turns an existing HTML lesson into a classroom presentation.
|
||||||
|
|
||||||
|
It is designed primarily for QLearn and Canvas, while remaining compatible
|
||||||
|
with ordinary HTML pages.
|
||||||
|
|
||||||
|
## The problem
|
||||||
|
|
||||||
|
Teachers transitioning from OneNote and PowerPoint to QLearn often maintain
|
||||||
|
two versions of the same lesson:
|
||||||
|
|
||||||
|
- the QLearn page used by students
|
||||||
|
- the PowerPoint used for classroom presentation
|
||||||
|
|
||||||
|
This creates duplicated work and version-control problems.
|
||||||
|
|
||||||
|
## The idea
|
||||||
|
|
||||||
|
The HTML lesson should be the single source of truth.
|
||||||
|
|
||||||
|
Teachers should build the lesson once and use that same lesson:
|
||||||
|
|
||||||
|
- in the classroom
|
||||||
|
- online
|
||||||
|
- for students learning from home
|
||||||
|
- as a presentation
|
||||||
|
|
||||||
|
## Core message
|
||||||
|
|
||||||
|
> Build once. Present everywhere.
|
||||||
|
|
||||||
|
## Design principles
|
||||||
|
|
||||||
|
Hudra should be:
|
||||||
|
|
||||||
|
- simple
|
||||||
|
- fast
|
||||||
|
- reliable
|
||||||
|
- lightweight
|
||||||
|
- predictable
|
||||||
|
- easy for teachers to understand
|
||||||
|
|
||||||
|
Hudra should not introduce a proprietary lesson format.
|
||||||
|
|
||||||
|
It should enhance existing lesson content rather than rebuild or convert it.
|
||||||
Binary file not shown.
|
After Width: | Height: | Size: 48 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 38 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 37 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 36 KiB |
+15
-3
@@ -1,20 +1,32 @@
|
|||||||
{
|
{
|
||||||
"manifest_version": 3,
|
"manifest_version": 3,
|
||||||
"name": "Hudra",
|
"name": "Hudra",
|
||||||
"version": "0.9.1",
|
"version": "0.10.0",
|
||||||
|
"version_name": "0.10 Beta 1",
|
||||||
"description": "Hudra turns Canvas/QLearn pages into responsive presentations with automatic pagination, dual-screen presenter view, audience controls, clicker support and local-only processing.",
|
"description": "Hudra turns Canvas/QLearn pages into responsive presentations with automatic pagination, dual-screen presenter view, audience controls, clicker support and local-only processing.",
|
||||||
"permissions": [
|
"permissions": [
|
||||||
"activeTab",
|
"activeTab",
|
||||||
"scripting",
|
"scripting",
|
||||||
"tabs",
|
"storage",
|
||||||
"system.display"
|
"system.display"
|
||||||
],
|
],
|
||||||
"host_permissions": [
|
"host_permissions": [
|
||||||
"https://*.instructure.com/*",
|
"https://*.instructure.com/*",
|
||||||
"https://*.qlearn.eq.edu.au/*"
|
"https://*.qlearn.eq.edu.au/*"
|
||||||
],
|
],
|
||||||
|
"icons": {
|
||||||
|
"16": "icons/HudraLogo16.png",
|
||||||
|
"32": "icons/HudraLogo32.png",
|
||||||
|
"48": "icons/HudraLogo48.png",
|
||||||
|
"128": "icons/HudraLogo128.png"
|
||||||
|
},
|
||||||
"action": {
|
"action": {
|
||||||
"default_title": "Start or exit Hudra"
|
"default_title": "Start or exit Hudra",
|
||||||
|
"default_icon": {
|
||||||
|
"16": "icons/HudraLogo16.png",
|
||||||
|
"32": "icons/HudraLogo32.png",
|
||||||
|
"48": "icons/HudraLogo48.png"
|
||||||
|
}
|
||||||
},
|
},
|
||||||
"background": {
|
"background": {
|
||||||
"service_worker": "background.js"
|
"service_worker": "background.js"
|
||||||
|
|||||||
@@ -0,0 +1,122 @@
|
|||||||
|
(() => {
|
||||||
|
if (window.__hudraPageDetector) return;
|
||||||
|
|
||||||
|
const contentRootSelectors = [
|
||||||
|
"#content .user_content",
|
||||||
|
".show-content .user_content",
|
||||||
|
".wiki-page-content .user_content",
|
||||||
|
".user_content",
|
||||||
|
"[role='main'] .page-content",
|
||||||
|
"[role='main'] .content"
|
||||||
|
];
|
||||||
|
|
||||||
|
const titleSelectors = [
|
||||||
|
"h1.page-title",
|
||||||
|
"h1.wiki-page-title",
|
||||||
|
"#content h1"
|
||||||
|
];
|
||||||
|
|
||||||
|
function hasMeaningfulContent(element) {
|
||||||
|
return Boolean(
|
||||||
|
element?.textContent?.trim() ||
|
||||||
|
element?.querySelector("img, video, iframe, svg, canvas, picture, table")
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function resolve() {
|
||||||
|
let contentRoot = null;
|
||||||
|
let contentRootSelector = null;
|
||||||
|
|
||||||
|
for (const selector of contentRootSelectors) {
|
||||||
|
const candidate = document.querySelector(selector);
|
||||||
|
if (candidate) {
|
||||||
|
contentRoot = candidate;
|
||||||
|
contentRootSelector = selector;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!contentRoot) {
|
||||||
|
return { supported: false, reason: "NO_LESSON_CONTAINER" };
|
||||||
|
}
|
||||||
|
|
||||||
|
const pageTitleElement =
|
||||||
|
contentRoot.querySelector("h1") ||
|
||||||
|
document.querySelector(titleSelectors.join(", "));
|
||||||
|
|
||||||
|
if (!pageTitleElement) {
|
||||||
|
return { supported: false, reason: "NO_LESSON_TITLE" };
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!hasMeaningfulContent(contentRoot)) {
|
||||||
|
return { supported: false, reason: "EMPTY_LESSON" };
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
supported: true,
|
||||||
|
reason: null,
|
||||||
|
contentRoot,
|
||||||
|
contentRootSelector,
|
||||||
|
pageTitleElement
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function inspect() {
|
||||||
|
const result = resolve();
|
||||||
|
return {
|
||||||
|
supported: result.supported,
|
||||||
|
reason: result.reason,
|
||||||
|
contentRootSelector: result.contentRootSelector || null
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function showNotice(message) {
|
||||||
|
document.getElementById("hudra-page-notice")?.remove();
|
||||||
|
|
||||||
|
const notice = document.createElement("div");
|
||||||
|
notice.id = "hudra-page-notice";
|
||||||
|
notice.setAttribute("role", "status");
|
||||||
|
notice.setAttribute("aria-live", "polite");
|
||||||
|
notice.style.cssText = [
|
||||||
|
"position:fixed",
|
||||||
|
"z-index:2147483647",
|
||||||
|
"right:20px",
|
||||||
|
"bottom:20px",
|
||||||
|
"max-width:420px",
|
||||||
|
"box-sizing:border-box",
|
||||||
|
"padding:16px 48px 16px 18px",
|
||||||
|
"border:1px solid #bac4d2",
|
||||||
|
"border-radius:10px",
|
||||||
|
"background:#fff",
|
||||||
|
"color:#1f2937",
|
||||||
|
"box-shadow:0 8px 28px rgba(15,23,42,.2)",
|
||||||
|
"font:16px/1.4 system-ui,sans-serif"
|
||||||
|
].join(";");
|
||||||
|
|
||||||
|
const text = document.createElement("span");
|
||||||
|
text.textContent = message;
|
||||||
|
notice.appendChild(text);
|
||||||
|
|
||||||
|
const close = document.createElement("button");
|
||||||
|
close.type = "button";
|
||||||
|
close.setAttribute("aria-label", "Dismiss Hudra message");
|
||||||
|
close.textContent = "×";
|
||||||
|
close.style.cssText = [
|
||||||
|
"position:absolute",
|
||||||
|
"top:8px",
|
||||||
|
"right:10px",
|
||||||
|
"border:0",
|
||||||
|
"background:transparent",
|
||||||
|
"color:#374151",
|
||||||
|
"font:24px/1 system-ui,sans-serif",
|
||||||
|
"cursor:pointer"
|
||||||
|
].join(";");
|
||||||
|
close.addEventListener("click", () => notice.remove());
|
||||||
|
notice.appendChild(close);
|
||||||
|
|
||||||
|
document.body.appendChild(notice);
|
||||||
|
window.setTimeout(() => notice.remove(), 8000);
|
||||||
|
}
|
||||||
|
|
||||||
|
window.__hudraPageDetector = { inspect, resolve, showNotice };
|
||||||
|
})();
|
||||||
+12
-47
@@ -10,36 +10,19 @@
|
|||||||
const presenterMode = config.mode || "single";
|
const presenterMode = config.mode || "single";
|
||||||
const sessionId = config.sessionId || "standalone";
|
const sessionId = config.sessionId || "standalone";
|
||||||
|
|
||||||
const contentRoot =
|
const detectedPage = window.__hudraPageDetector?.resolve();
|
||||||
document.querySelector(
|
|
||||||
"#content .user_content, " +
|
|
||||||
".show-content .user_content, " +
|
|
||||||
".wiki-page-content .user_content, " +
|
|
||||||
".user_content"
|
|
||||||
) ||
|
|
||||||
document.querySelector(
|
|
||||||
"[role='main'] .page-content, " +
|
|
||||||
"[role='main'] .content"
|
|
||||||
) ||
|
|
||||||
document.querySelector("#content") ||
|
|
||||||
document.body;
|
|
||||||
|
|
||||||
const pageTitleElement =
|
if (!detectedPage?.supported) {
|
||||||
contentRoot.querySelector("h1") ||
|
window.__hudraPageDetector?.showNotice(
|
||||||
document.querySelector(
|
"Hudra couldn’t start because this page does not contain a supported lesson."
|
||||||
"#content h1, .page-title, .wiki-page-title, h1.page-title"
|
|
||||||
);
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const { contentRoot, pageTitleElement } = detectedPage;
|
||||||
|
|
||||||
let pageTitle = pageTitleElement?.textContent?.trim() || "";
|
let pageTitle = pageTitleElement?.textContent?.trim() || "";
|
||||||
|
|
||||||
if (!pageTitle) {
|
|
||||||
pageTitle = document.title.replace(/\s*:\s*[^:]+$/, "").trim();
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!pageTitle) {
|
|
||||||
pageTitle = "Lesson";
|
|
||||||
}
|
|
||||||
|
|
||||||
function isHidden(element) {
|
function isHidden(element) {
|
||||||
if (!element || element.hidden) return true;
|
if (!element || element.hidden) return true;
|
||||||
|
|
||||||
@@ -101,11 +84,6 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!slideDefinitions.length) {
|
|
||||||
alert("No presentable page content was found.");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const presentationLayer = document.createElement("div");
|
const presentationLayer = document.createElement("div");
|
||||||
presentationLayer.id = "qlearn-presenter-layer";
|
presentationLayer.id = "qlearn-presenter-layer";
|
||||||
document.body.appendChild(presentationLayer);
|
document.body.appendChild(presentationLayer);
|
||||||
@@ -699,20 +677,6 @@
|
|||||||
return heading;
|
return heading;
|
||||||
}
|
}
|
||||||
|
|
||||||
function splitParagraph(paragraph) {
|
|
||||||
const text = paragraph.textContent.trim();
|
|
||||||
const sentences = text.match(/[^.!?]+[.!?]+|[^.!?]+$/g) || [text];
|
|
||||||
if (sentences.length < 2) return [paragraph.cloneNode(true)];
|
|
||||||
const midpoint = Math.ceil(sentences.length / 2);
|
|
||||||
return [sentences.slice(0, midpoint), sentences.slice(midpoint)]
|
|
||||||
.filter((part) => part.length)
|
|
||||||
.map((part) => {
|
|
||||||
const copy = paragraph.cloneNode(false);
|
|
||||||
copy.textContent = part.join(" ").trim();
|
|
||||||
return copy;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
function splitList(list) {
|
function splitList(list) {
|
||||||
const items = [...list.children].filter((item) => item.tagName === "LI");
|
const items = [...list.children].filter((item) => item.tagName === "LI");
|
||||||
if (items.length < 2) return [list.cloneNode(true)];
|
if (items.length < 2) return [list.cloneNode(true)];
|
||||||
@@ -726,9 +690,6 @@
|
|||||||
|
|
||||||
function divideOversizedBlock(block) {
|
function divideOversizedBlock(block) {
|
||||||
if (block.matches("ul, ol")) return splitList(block);
|
if (block.matches("ul, ol")) return splitList(block);
|
||||||
if (block.matches("p") && block.textContent.trim().length > 220) {
|
|
||||||
return splitParagraph(block);
|
|
||||||
}
|
|
||||||
return [block.cloneNode(true)];
|
return [block.cloneNode(true)];
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1371,6 +1332,9 @@
|
|||||||
},
|
},
|
||||||
getSlideCount() {
|
getSlideCount() {
|
||||||
return slideWrappers.length;
|
return slideWrappers.length;
|
||||||
|
},
|
||||||
|
reportState() {
|
||||||
|
sendPresenterState();
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -1399,6 +1363,7 @@
|
|||||||
if (command === "scrollBy") window.__qlearnPresenter.scrollBy(message.value);
|
if (command === "scrollBy") window.__qlearnPresenter.scrollBy(message.value);
|
||||||
if (command === "resetView") window.__qlearnPresenter.resetView();
|
if (command === "resetView") window.__qlearnPresenter.resetView();
|
||||||
if (command === "goTo") window.__qlearnPresenter.goTo(message.value);
|
if (command === "goTo") window.__qlearnPresenter.goTo(message.value);
|
||||||
|
if (command === "reportState") window.__qlearnPresenter.reportState();
|
||||||
if (command === "exit") window.__qlearnPresenter.exit(false);
|
if (command === "exit") window.__qlearnPresenter.exit(false);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|||||||
+126
@@ -0,0 +1,126 @@
|
|||||||
|
# Hudra release builder
|
||||||
|
# Reads the version from manifest.json, creates a clean release folder,
|
||||||
|
# and produces a Chrome Web Store-ready ZIP.
|
||||||
|
|
||||||
|
$ErrorActionPreference = "Stop"
|
||||||
|
|
||||||
|
# Project locations
|
||||||
|
$ProjectRoot = $PSScriptRoot
|
||||||
|
$ManifestPath = Join-Path $ProjectRoot "manifest.json"
|
||||||
|
$ReleaseRoot = Join-Path $ProjectRoot "release"
|
||||||
|
|
||||||
|
# Check that manifest.json exists
|
||||||
|
if (-not (Test-Path $ManifestPath)) {
|
||||||
|
Write-Host "ERROR: manifest.json was not found." -ForegroundColor Red
|
||||||
|
Write-Host "Place release.ps1 in the same folder as manifest.json."
|
||||||
|
exit 1
|
||||||
|
}
|
||||||
|
|
||||||
|
# Read extension name and version
|
||||||
|
try {
|
||||||
|
$Manifest = Get-Content $ManifestPath -Raw | ConvertFrom-Json
|
||||||
|
}
|
||||||
|
catch {
|
||||||
|
Write-Host "ERROR: manifest.json is not valid JSON." -ForegroundColor Red
|
||||||
|
exit 1
|
||||||
|
}
|
||||||
|
|
||||||
|
$ExtensionName = $Manifest.name
|
||||||
|
$Version = $Manifest.version
|
||||||
|
|
||||||
|
if ([string]::IsNullOrWhiteSpace($ExtensionName)) {
|
||||||
|
$ExtensionName = "Hudra"
|
||||||
|
}
|
||||||
|
|
||||||
|
if ([string]::IsNullOrWhiteSpace($Version)) {
|
||||||
|
Write-Host "ERROR: No version was found in manifest.json." -ForegroundColor Red
|
||||||
|
exit 1
|
||||||
|
}
|
||||||
|
|
||||||
|
$SafeName = $ExtensionName -replace '[^a-zA-Z0-9_-]', '-'
|
||||||
|
$ReleaseName = "$SafeName-v$Version"
|
||||||
|
|
||||||
|
$StagingFolder = Join-Path $ReleaseRoot $ReleaseName
|
||||||
|
$ZipPath = Join-Path $ReleaseRoot "$ReleaseName.zip"
|
||||||
|
|
||||||
|
Write-Host ""
|
||||||
|
Write-Host "Building $ExtensionName $Version..." -ForegroundColor Cyan
|
||||||
|
Write-Host ""
|
||||||
|
|
||||||
|
# Only files required by the browser extension at runtime belong in the ZIP.
|
||||||
|
# Everything else is excluded by default, including release output, previous
|
||||||
|
# releases, scripts, repository metadata, documentation and development files.
|
||||||
|
$IncludedItems = @(
|
||||||
|
"manifest.json",
|
||||||
|
"background.js",
|
||||||
|
"page-detection.js",
|
||||||
|
"presenter.js",
|
||||||
|
"controls.html",
|
||||||
|
"controls.css",
|
||||||
|
"controls.js",
|
||||||
|
"icons"
|
||||||
|
)
|
||||||
|
|
||||||
|
# Recreate the staging folder
|
||||||
|
if (Test-Path $StagingFolder) {
|
||||||
|
Remove-Item $StagingFolder -Recurse -Force
|
||||||
|
}
|
||||||
|
|
||||||
|
New-Item -ItemType Directory -Path $StagingFolder -Force | Out-Null
|
||||||
|
|
||||||
|
# Copy only the allowlisted runtime files into the clean staging folder
|
||||||
|
$IncludedItems | ForEach-Object {
|
||||||
|
$SourcePath = Join-Path $ProjectRoot $_
|
||||||
|
|
||||||
|
if (-not (Test-Path $SourcePath)) {
|
||||||
|
Write-Host "ERROR: Required runtime item was not found: $_" -ForegroundColor Red
|
||||||
|
exit 1
|
||||||
|
}
|
||||||
|
|
||||||
|
Copy-Item `
|
||||||
|
-Path $SourcePath `
|
||||||
|
-Destination $StagingFolder `
|
||||||
|
-Recurse `
|
||||||
|
-Force
|
||||||
|
}
|
||||||
|
|
||||||
|
# Confirm that the copied release contains manifest.json
|
||||||
|
$CopiedManifest = Join-Path $StagingFolder "manifest.json"
|
||||||
|
|
||||||
|
if (-not (Test-Path $CopiedManifest)) {
|
||||||
|
Write-Host "ERROR: manifest.json was not copied into the release folder." -ForegroundColor Red
|
||||||
|
exit 1
|
||||||
|
}
|
||||||
|
|
||||||
|
# Remove any previous ZIP for this version
|
||||||
|
if (Test-Path $ZipPath) {
|
||||||
|
Remove-Item $ZipPath -Force
|
||||||
|
}
|
||||||
|
|
||||||
|
# ZIP the contents, ensuring manifest.json is at the ZIP root
|
||||||
|
$FilesToZip = Join-Path $StagingFolder "*"
|
||||||
|
Compress-Archive `
|
||||||
|
-Path $FilesToZip `
|
||||||
|
-DestinationPath $ZipPath `
|
||||||
|
-CompressionLevel Optimal `
|
||||||
|
-Force
|
||||||
|
|
||||||
|
# Verify the ZIP was created
|
||||||
|
if (-not (Test-Path $ZipPath)) {
|
||||||
|
Write-Host "ERROR: The release ZIP could not be created." -ForegroundColor Red
|
||||||
|
exit 1
|
||||||
|
}
|
||||||
|
|
||||||
|
$ZipSize = [math]::Round((Get-Item $ZipPath).Length / 1KB, 1)
|
||||||
|
|
||||||
|
Write-Host "Release created successfully!" -ForegroundColor Green
|
||||||
|
Write-Host ""
|
||||||
|
Write-Host "Release folder:"
|
||||||
|
Write-Host " $StagingFolder" -ForegroundColor Yellow
|
||||||
|
Write-Host ""
|
||||||
|
Write-Host "ZIP file:"
|
||||||
|
Write-Host " $ZipPath" -ForegroundColor Yellow
|
||||||
|
Write-Host ""
|
||||||
|
Write-Host "ZIP size: $ZipSize KB"
|
||||||
|
Write-Host ""
|
||||||
|
Write-Host "Ready for Gitea and the Chrome Web Store." -ForegroundColor Green
|
||||||
Reference in New Issue
Block a user