2 Commits
Author SHA1 Message Date
git df3ce58bd4 docs: align published and security guidance 2026-07-29 19:41:53 +10:00
git 6ddda00eb3 Prepare Lildra 0.3.1 Web Store submission 2026-07-27 21:16:04 +10:00
23 changed files with 577 additions and 2 deletions
+3
View File
@@ -19,3 +19,6 @@ Thumbs.db
# Local browser test profiles
.chrome-profile/
.edge-profile/
# Local source captures may contain private Canvas identifiers
screenshots/
+3
View File
@@ -14,6 +14,9 @@ Do not add frameworks, build tools, dependencies, analytics, telemetry, remote s
- `background.js` handles the toolbar and command, then injects `lildra.js` into the active tab.
- `lildra.js` validates the URL and Canvas DOM, injects scoped layout CSS and two inert UI elements, watches for dynamic title changes, and owns complete toggle cleanup.
- `icons/` contains the packaged toolbar/store icons.
- `tools/build-release.ps1` creates and validates the runtime-only Web Store ZIP.
- `tools/build-store-assets.ps1` deterministically composites local source captures into store artwork.
- `store-assets/ready-to-upload/` contains final listing artwork; local `screenshots/` inputs are ignored because they may contain private Canvas identifiers.
There is intentionally no content script registered for persistent execution. Re-injecting `lildra.js` toggles the current page off through its page-scoped state object.
+6
View File
@@ -4,6 +4,12 @@ All notable changes to Lildra are documented here. Versions follow Semantic Vers
## Unreleased
## 0.3.1 - 2026-07-27
### Fixed
- Normalised packaged icons with transparent padding for Chrome Web Store presentation.
## 0.3.0 - 2026-07-27
### Changed
+7 -1
View File
@@ -14,6 +14,8 @@ Lildra is a small Chrome and Edge Manifest V3 extension that gives Canvas/QLearn
Lildra requests only `activeTab` and `scripting`. It has no analytics, remote code, settings, popup, or persistent site access.
Lildra is intended to be requested for use by Queensland Department of Education school staff. It is an independent Napper.au project and is not approved or endorsed by the Queensland Department of Education or any educational institution.
## Install locally
1. Open `chrome://extensions` in Chrome or `edge://extensions` in Edge.
@@ -29,4 +31,8 @@ Follow the manual checklist in [AGENTS.md](AGENTS.md). There is no build step an
## Privacy and security
Lildra runs only after an explicit toolbar click or keyboard command. It does not collect, store, or transmit data. The package contains no remote scripts.
Lildra runs only after an explicit toolbar click or keyboard command. It processes the selected supported lesson locally to validate the route and Canvas structure, apply its focused layout, and display the lesson title. It does not retain or transmit lesson content, titles, addresses, or usage information.
The extension has no persistent site access, storage, analytics, telemetry, advertising, tracking, remote code, or extension-initiated network requests. Canvas, QLearn, and embedded lesson resources continue communicating with their existing providers independently of Lildra.
See the [privacy policy](docs/PRIVACY.md), [security policy](SECURITY.md), and [security review summary](docs/SECURITY_REVIEW.md).
+14
View File
@@ -0,0 +1,14 @@
# Security policy
Report suspected Lildra vulnerabilities privately to [support@napper.au](mailto:support@napper.au) with the subject **Lildra security report**.
Please include:
- the Lildra version and browser;
- the affected page type;
- 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 test case or redacted information where possible.
Please do not disclose a suspected vulnerability publicly before there has been a reasonable opportunity to investigate and respond.
+195
View File
@@ -0,0 +1,195 @@
# Chrome Web Store submission: Lildra 0.3.1
Prepared: 27 July 2026
This document is a drafting and validation aid. Compare all privacy answers with the current dashboard wording before certification. Do not upload reviewer credentials to the repository or package.
## Product details
- Product name: Lildra
- Version: 0.3.1
- Display version: 0.3.1 (no separate `version_name`)
- Recommended primary category: Productivity
- Language: English (Australia)
- Package: `Lildra-v0.3.1-chrome-web-store.zip`
- Support URL: https://napper.au/lildra/support
- Privacy-policy URL: https://napper.au/lildra/privacy
- Homepage: https://napper.au/lildra
- Public source: https://git.napper.au/napper/lildra
- Publication status: publicly listed in the Chrome Web Store and Microsoft Edge Add-ons
### Short description
> A distraction-free reading layout for Canvas and QLearn lesson pages.
The description is 70 characters and is within the 132-character limit.
### Detailed description
> Lildra gives supported Canvas and QLearn lesson pages a cleaner, distraction-free reading layout while preserving the original lesson content.
>
> - Hides Canvas global and course navigation after you activate it.
> - Keeps the existing lesson centred at a bounded, responsive reading width.
> - Shows a compact, single-line title that follows dynamically rendered Canvas page titles.
> - Toggles from the extension toolbar or with Alt+L.
> - Restores the normal page when toggled off.
> - Contains no analytics, remote code, settings, or persistent site access.
>
> Lildra works only on supported HTTPS Canvas course page and wiki content routes with the expected Canvas lesson structure. It does not support dashboards, module lists, assignments, discussions, quizzes, or arbitrary websites. It does not move, clone, reconstruct, or otherwise edit lesson content.
>
> Lildra is an independent Napper.au project and is not affiliated with or endorsed by Instructure, Google, the Queensland Department of Education, or any educational institution.
### Single-purpose statement
> Lildra's single purpose is to provide a user-invoked, distraction-free reading layout on supported Canvas and QLearn lesson pages while preserving the existing lesson content.
## Permission justifications
### `activeTab`
Lildra uses `activeTab` to receive temporary access to the current tab only after the user clicks the toolbar action or invokes the Alt+L extension command. It needs this access to check the selected tab's HTTPS Canvas route and page structure and to apply or remove the focused layout. A narrower persistent host pattern would not provide user-invoked access across independently hosted Canvas installations; persistent host access would also grant broader access than Lildra needs. Page information remains on the device and is not transmitted by Lildra.
### `scripting`
Lildra uses `chrome.scripting.executeScript()` to inject the packaged `lildra.js` file into the temporarily authorised active tab. This is required because the extension is intentionally inactive until the user chooses to run it and does not register a persistent content script. The injected script contains no remote code and transmits no accessed information.
### Host permissions
Lildra requests no `host_permissions`. Temporary page access comes only from `activeTab` after an explicit user gesture.
## Manifest and code audit
- Manifest V3: confirmed.
- Name/version: `Lildra` / `0.3.1`.
- Manifest description: 70 characters.
- Permissions: only `activeTab` and `scripting`, both used by `background.js`.
- Host permissions: none.
- Remote executable code: none.
- Credentials/API keys: none found.
- Storage APIs: none.
- Network APIs in extension code: none (`fetch`, `XMLHttpRequest`, `WebSocket`, and `sendBeacon` absent).
- Analytics/telemetry SDKs: none.
- Runtime references: validated during package build.
- ZIP contents: limited to the manifest, two scripts, and four referenced icons.
## Privacy-practices dashboard draft
Use the following as a conservative code-based draft, then compare each answer with the dashboard's current definitions.
| Category | Draft answer | Reason |
| --- | --- | --- |
| Personally identifiable information | No as an intended category; review wording | Lildra does not seek identity data, but a lesson title could incidentally contain a person's name and is processed locally. |
| Health information | No | No code seeks or uses health data. Incidental lesson content is not extracted or transmitted. |
| Financial or payment information | No | No payment or financial APIs or fields are accessed. |
| Authentication information | No | Lildra does not access passwords, cookies, tokens, or authentication records. |
| Personal communications | No | Lildra does not access email, chat, messages, or communications APIs. |
| Location | No | No location API or location data is accessed. |
| Web browsing activity | Review dashboard wording; disclose current active URL handling if included | The active supported tab URL is checked locally after a user gesture. Lildra does not collect history, monitor other tabs, retain URLs, or transmit them. |
| Website content | Yes | Canvas DOM structure and the visible page title are processed locally to validate the page and provide the layout. |
| User-generated content | Review dashboard wording; likely Yes if lesson pages qualify | A lesson or title may be authored by a teacher or another user. It is processed only in the active page and is not retained or transmitted. |
| Form data | No | Lildra does not read, intercept, or submit form fields. |
Data-use declarations:
- Sold: No.
- Used for advertising or profiling: No.
- Used for creditworthiness or lending: No.
- Transmitted off-device by Lildra: No.
- Shared with third parties by Lildra: No.
- Used only for the disclosed single purpose: Yes.
- Human access to page information: No.
- Retention: No persistent extension retention; temporary state ends when Lildra is disabled or the page closes.
Limited Use statement:
> The use of information received from Google APIs will adhere to the Chrome Web Store User Data Policy, including the Limited Use requirements.
## Screenshot sequence
All screenshots are 1280×800 PNG, square-cornered, and use deterministic crops of the supplied real interface.
1. `01-original-canvas-page.png` — the normal Canvas lesson with global and course navigation visible.
2. `02-lildra-focused-reading.png` — Lildra's primary focused reading layout.
3. `03-sticky-dynamic-title.png` — the compact title bar above the lesson.
4. `04-original-content-preserved.png` — representative headings, text, learning goals, and imagery preserved in the lesson.
5. `05-before-and-after.png` — the same lesson before and after Lildra activation.
Browser chrome was cropped to remove the production hostname, course identifiers, and unrelated extension icons. The private sandbox course name was covered in the remaining Canvas breadcrumb. No Lildra UI was generated or reconstructed.
## Store icon validation
The previous packaged icon was fully opaque and had an unintended white square background. For 0.3.1, the established mark was technically normalised without redesigning it:
- 128×128 PNG: confirmed.
- Alpha transparency and transparent corners: confirmed.
- Visible artwork: centred at approximately 96×96 pixels.
- Packaged and promotional icon: derived from the same normalised asset.
- 16, 32, 48, and 128 sizes: generated and require final visual confirmation in Chrome on both light and dark themes.
## Reviewer instructions
> Lildra has one purpose: it provides a user-invoked, distraction-free reading layout on supported HTTPS Canvas and QLearn course page/wiki lesson routes.
>
> To test, install the extension and open an HTTPS Canvas page matching `/courses/{course}/pages/{page}` or `/courses/{course}/wiki/{page}` that contains the standard Canvas `#application #wrapper` and `#wiki_page_show` structure. Click the Lildra toolbar icon or invoke Alt+L. Canvas global navigation and the left course navigation should hide, the existing lesson should remain centred without content reconstruction, and a compact title bar should appear. Click the icon or invoke Alt+L again to restore the normal page.
>
> On unsupported routes such as dashboards, module lists, assignments, discussions, or quizzes, Lildra performs no layout change. It also performs no change when the expected Canvas page structure is absent.
>
> A Canvas/QLearn account and suitable lesson URL are required because the extension does not include or control a public test environment. If reviewer access is required, provide credentials only through the Chrome Web Store dashboard's secure reviewer-credentials field; never place credentials in the extension ZIP or repository.
## Public website alignment
Current published alignment:
- `/lildra`, `/lildra/privacy`, `/lildra/support`, and `/lildra/terms` are live.
- The public pages accurately describe explicit activation, supported page types, lack of persistent host access, local processing, permissions, and independence.
- The public privacy page discloses local access to the active URL, title, and Canvas structure.
- The website displays version 0.3.1 and uses **Local processing only** to explain that the selected lesson is processed in the browser and is not stored or transmitted by Lildra.
- Normal installation links point to the Chrome Web Store and Microsoft Edge Add-ons.
- Version 0.3.1 is publicly listed in both stores.
Local processing is not the same as handling no website content. Lildra temporarily processes the selected supported page's address, Canvas DOM structure, and visible title on the device. It does not collect that information into an extension dataset, persistently store it, or transmit it. Keep the conservative **Website content: Yes** dashboard disclosure.
## Microsoft Edge listing alignment
- [ ] Chrome and Edge detailed descriptions describe the same single purpose and supported routes.
- [ ] Both listings show version 0.3.1.
- [ ] Both listings use https://napper.au/lildra/support as the support URL.
- [ ] Both listings use https://napper.au/lildra/privacy as the privacy-policy URL.
- [ ] Both listings describe local processing without claiming that Lildra handles no website content.
- [ ] Both listings identify the public source at https://git.napper.au/napper/lildra where the dashboard supports it.
## Manual submission and testing checklist
- [ ] Install from `Lildra-v0.3.1-chrome-web-store.zip` in current stable Chrome.
- [ ] Confirm Chrome reports no manifest or service-worker errors.
- [ ] Activate using both the toolbar button and Alt+L on representative QLearn and generic Canvas wiki/page lessons.
- [ ] Toggle off and confirm all navigation, layout, styles, observers, and injected elements are restored/removed.
- [ ] Confirm unsupported dashboards, modules, assignments, discussions, quizzes, HTTP pages, and unrelated sites do not change.
- [ ] Test a supported route with missing or renamed Canvas DOM elements; Lildra should make no change.
- [ ] Test long and dynamically rendered titles, including one-line ellipsis.
- [ ] Test keyboard focus and screen-reader output for duplicate titles or inaccessible content.
- [ ] Test narrow/mobile-width viewport behaviour.
- [ ] Test long lessons and unusual heading structures.
- [ ] Test images, video, embeds, captions, transcripts, links, and wide tables.
- [ ] Refresh and restart Chrome; confirm Lildra does not persist without another explicit activation.
- [ ] Confirm there are no preference-storage behaviours to test or clear.
- [ ] Confirm the only permission explanation/prompt is consistent with `activeTab` and `scripting` behaviour.
- [ ] Inspect DevTools Network while toggling; confirm Lildra adds no unexpected requests.
- [ ] Check the 16/32/48/128 icons on light and dark browser themes.
- [ ] Recheck all five screenshots for private names, identifiers, URLs, unrelated extensions, and accurate captions.
- [ ] Upload the privacy-policy URL and compare every dashboard privacy answer against its current wording.
- [ ] Provide any reviewer credentials only through the dashboard's secure field.
## Published-listing maintenance
Version 0.3.1 is publicly listed in the Chrome Web Store and Microsoft Edge Add-ons. Before a future release, repeat the manual testing and dashboard review above, then ensure both stores, the website, privacy policy, support page, terms, and public source remain aligned. Dashboard values and live listing text must be checked manually because they are not controlled by this repository.
## Official references
- [Supplying Images](https://developer.chrome.com/docs/webstore/images)
- [Creating a great listing page](https://developer.chrome.com/docs/webstore/best-listing)
- [Privacy Policies](https://developer.chrome.com/docs/webstore/program-policies/privacy)
- [User Data FAQ](https://developer.chrome.com/docs/webstore/program-policies/user-data-faq)
- [Limited Use](https://developer.chrome.com/docs/webstore/program-policies/limited-use)
- [Disclosure Requirements](https://developer.chrome.com/docs/webstore/program-policies/disclosure-requirements)
+57
View File
@@ -0,0 +1,57 @@
# Lildra Privacy Policy
Effective date: 27 July 2026
Lildra is a browser extension that provides a distraction-free reading layout on supported Canvas and QLearn lesson pages. This policy explains how the extension handles information.
## Information handled by Lildra
After the user clicks Lildra's toolbar button or invokes its keyboard shortcut, Lildra temporarily accesses the selected tab. It processes:
- the active tab address, to confirm that it is an HTTPS Canvas course page or wiki route;
- Canvas page structure, to confirm that the page is supported and hide specified navigation elements; and
- the visible lesson title, to display and update Lildra's compact title bar.
The active lesson may contain website content or user-generated educational content. Lildra processes the active supported page locally in the user's browser solely to provide its disclosed focus-mode functionality. Page content is not transmitted to the developer or third parties.
Lildra does not seek passwords, authentication cookies, health or financial information, private communications, precise location, or information from unrelated tabs.
Lildra has no persistent site access. Its `activeTab` permission grants temporary access to the selected tab only after the user activates the extension with the toolbar button or keyboard shortcut.
## Use, storage and retention
Information from the selected tab is used only to validate the page, apply the requested focused layout, display its current title, and restore the normal layout when Lildra is disabled.
Lildra does not request Chrome's storage permission and does not use `chrome.storage`, `localStorage`, or `sessionStorage`. It does not retain lesson content, titles, addresses, settings, or usage records. Temporary in-page state exists only while the page remains open or Lildra remains active.
## Transmission and sharing
Lildra does not transmit page content, page titles, addresses, or usage information to Napper.au or another service. It contains no analytics, telemetry, advertising, tracking, remote executable code, or extension-initiated network requests. Lildra does not sell user information or share it with advertisers, data brokers, or unrelated third parties.
Canvas, QLearn, and embedded lesson resources continue to communicate with their existing providers independently of Lildra.
## Chrome Web Store Limited Use
The use of information received from Google APIs will adhere to the Chrome Web Store User Data Policy, including the Limited Use requirements. Information is used only to provide Lildra's single user-facing purpose and is not used for advertising, creditworthiness, lending, or unrelated purposes.
## Support correspondence
If a user voluntarily emails support, the message is handled by the relevant email providers and may contain information the user chooses to include. Support messages are retained only as reasonably needed to respond, maintain security, and meet legal obligations. Users should not send passwords, student records, authentication information, or private lesson content.
## User choices and deletion
Users can stop local processing by toggling Lildra off, closing the page, disabling the extension, or uninstalling it. Because Lildra stores no extension dataset, there is no separate local data to clear. Users may contact support to request deletion of information they voluntarily sent by email.
## Website
The public Lildra website may receive ordinary server information needed to deliver pages, such as an IP address, browser type, requested page, and request time. This may appear in short-term operational or security logs. The website cannot access Canvas or QLearn lesson content.
## Changes
This policy may be updated if Lildra's features, permissions, or data practices change. The effective date will be revised, and material changes will be communicated through appropriate website, listing, extension, or release channels.
## Contact
Privacy questions and deletion requests can be sent to [support@napper.au](mailto:support@napper.au). Lildra is an independent Napper.au project based in Queensland, Australia.
Lildra is intended to be requested for use by Queensland Department of Education school staff. The project is not approved or endorsed by the Queensland Department of Education or any educational institution.
+41
View File
@@ -0,0 +1,41 @@
# Lildra security review summary
This summary is intended to help Queensland Department of Education reviewers assess Lildra 0.3.1. Lildra is an independent Napper.au project intended to be requested for use by EQ school staff; it is not approved or endorsed by the Queensland Department of Education or any educational institution.
## Scope and architecture
Lildra has one purpose: after an explicit toolbar click or Alt+L command, it provides a distraction-free reading layout on a supported HTTPS Canvas/QLearn course lesson or wiki page. It preserves the native lesson DOM and does not activate on dashboards, assignments, discussions, quizzes, modules, account pages, HTTP pages, or unrelated sites.
The extension checks both the route (`/courses/{course}/pages/{page}` or `/courses/{course}/wiki/{page}`) and expected Canvas DOM elements before making any change. It uses only:
- `activeTab`, for temporary access to the selected tab after explicit user action; and
- `scripting`, to inject only the packaged `lildra.js` file.
There are no `host_permissions` and no persistent content script or persistent site access. The extension has no dependencies, remote executable code, analytics, telemetry, advertising, tracking, storage APIs, or extension-initiated network APIs.
## Local data flow and cleanup
On activation, Lildra locally checks the selected tab's HTTPS route and Canvas structure, reads the visible lesson title, injects scoped layout CSS and two decorative labels, and observes title changes. It does not retain or transmit lesson content, titles, addresses, or usage information. Canvas, QLearn, and embedded resources continue communicating with their existing providers independently of Lildra.
Activating Lildra again removes its observer, pending animation frame, root class, style element, title bar, version label, and page-scoped state. Closing or navigating away from the page also ends that in-page state.
## Public references
- Website: https://napper.au/lildra
- Privacy: https://napper.au/lildra/privacy
- Support: https://napper.au/lildra/support
- Terms: https://napper.au/lildra/terms
- Public source: https://git.napper.au/napper/lildra
- Distribution: Chrome Web Store and Microsoft Edge Add-ons, linked from the Lildra website
- Vulnerability reporting: [SECURITY.md](../SECURITY.md)
## Manual verification
1. Inspect `manifest.json`; confirm Manifest V3, version 0.3.1, only `activeTab` and `scripting`, and no `host_permissions`.
2. Load the repository unpacked in current Chrome and Edge and confirm there are no extension errors.
3. On a representative QLearn lesson, toggle with the toolbar and Alt+L. Confirm navigation hides, native lesson content remains in place, and toggling off restores the page completely.
4. Confirm unsupported routes and pages missing the expected Canvas DOM do not change.
5. Inspect browser network activity while toggling and confirm Lildra initiates no requests; separately confirm normal Canvas and embedded-resource requests continue.
6. Test dynamic titles, narrow and desktop viewports, keyboard use, and screen-reader output.
The fuller privacy disclosure is in [PRIVACY.md](PRIVACY.md), and the complete pre-release manual procedure is in [AGENTS.md](../AGENTS.md).
Binary file not shown.

Before

Width:  |  Height:  |  Size: 53 KiB

After

Width:  |  Height:  |  Size: 13 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 32 KiB

After

Width:  |  Height:  |  Size: 627 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 35 KiB

After

Width:  |  Height:  |  Size: 1.6 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 38 KiB

After

Width:  |  Height:  |  Size: 2.9 KiB

+1 -1
View File
@@ -1,7 +1,7 @@
{
"manifest_version": 3,
"name": "Lildra",
"version": "0.3.0",
"version": "0.3.1",
"description": "A distraction-free reading layout for Canvas and QLearn lesson pages.",
"permissions": [
"activeTab",
Binary file not shown.

After

Width:  |  Height:  |  Size: 313 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 227 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 162 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 231 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 396 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 219 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 40 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 13 KiB

+50
View File
@@ -0,0 +1,50 @@
param(
[string]$Root = (Split-Path -Parent $PSScriptRoot)
)
$ErrorActionPreference = "Stop"
$manifest = Get-Content -Raw -LiteralPath (Join-Path $Root "manifest.json") | ConvertFrom-Json
$version = $manifest.version
$outputDir = Join-Path $Root "store-assets\ready-to-upload"
$package = Join-Path $outputDir "Lildra-v$version-chrome-web-store.zip"
$stage = Join-Path $Root ".store-package-staging"
if (Test-Path -LiteralPath $stage) {
throw "Staging directory already exists: $stage"
}
New-Item -ItemType Directory -Force -Path (Join-Path $stage "icons") | Out-Null
try {
Copy-Item -LiteralPath (Join-Path $Root "manifest.json"), (Join-Path $Root "background.js"), (Join-Path $Root "lildra.js") -Destination $stage
foreach ($size in 16, 32, 48, 128) {
Copy-Item -LiteralPath (Join-Path $Root "icons\LildraLogo$size.png") -Destination (Join-Path $stage "icons")
}
if (Test-Path -LiteralPath $package) { Remove-Item -LiteralPath $package }
Compress-Archive -Path (Join-Path $stage "*") -DestinationPath $package -CompressionLevel Optimal
} finally {
if (Test-Path -LiteralPath $stage) { Remove-Item -LiteralPath $stage -Recurse -Force }
}
Add-Type -AssemblyName System.IO.Compression.FileSystem
$zip = [System.IO.Compression.ZipFile]::OpenRead($package)
try {
$inventory = @($zip.Entries | ForEach-Object { $_.FullName.Replace("\", "/") })
if ($inventory -notcontains "manifest.json") { throw "manifest.json is not at the ZIP root" }
$expected = @(
"manifest.json", "background.js", "lildra.js",
"icons/LildraLogo16.png", "icons/LildraLogo32.png",
"icons/LildraLogo48.png", "icons/LildraLogo128.png"
)
if (Compare-Object $expected $inventory) { throw "Unexpected ZIP inventory" }
} finally {
$zip.Dispose()
}
$hash = Get-FileHash -Algorithm SHA256 -LiteralPath $package
[pscustomobject]@{
Path = $package
Size = (Get-Item -LiteralPath $package).Length
SHA256 = $hash.Hash
Inventory = $inventory -join ", "
}
+200
View File
@@ -0,0 +1,200 @@
param(
[string]$Root = (Split-Path -Parent $PSScriptRoot)
)
$ErrorActionPreference = "Stop"
Add-Type -AssemblyName System.Drawing
$sourceDir = Join-Path $Root "screenshots"
$outputDir = Join-Path $Root "store-assets\ready-to-upload"
New-Item -ItemType Directory -Force -Path $outputDir | Out-Null
$purpleDark = [System.Drawing.ColorTranslator]::FromHtml("#281052")
$purple = [System.Drawing.ColorTranslator]::FromHtml("#6D35D8")
$purpleBright = [System.Drawing.ColorTranslator]::FromHtml("#8250E8")
$white = [System.Drawing.Color]::White
$ink = [System.Drawing.ColorTranslator]::FromHtml("#171429")
function New-Canvas([int]$Width, [int]$Height) {
$bitmap = New-Object System.Drawing.Bitmap($Width, $Height, [System.Drawing.Imaging.PixelFormat]::Format32bppArgb)
$bitmap.SetResolution(96, 96)
return $bitmap
}
function Save-Png($Bitmap, [string]$Name) {
$path = Join-Path $outputDir $Name
$Bitmap.Save($path, [System.Drawing.Imaging.ImageFormat]::Png)
return $path
}
function Set-Quality($Graphics) {
$Graphics.SmoothingMode = [System.Drawing.Drawing2D.SmoothingMode]::HighQuality
$Graphics.InterpolationMode = [System.Drawing.Drawing2D.InterpolationMode]::HighQualityBicubic
$Graphics.PixelOffsetMode = [System.Drawing.Drawing2D.PixelOffsetMode]::HighQuality
$Graphics.TextRenderingHint = [System.Drawing.Text.TextRenderingHint]::ClearTypeGridFit
}
function Draw-Cover($Graphics, $Image, [System.Drawing.Rectangle]$Destination, [System.Drawing.Rectangle]$Source) {
$sourceRatio = $Source.Width / $Source.Height
$destinationRatio = $Destination.Width / $Destination.Height
if ($sourceRatio -gt $destinationRatio) {
$cropWidth = [int]($Source.Height * $destinationRatio)
$Source.X += [int](($Source.Width - $cropWidth) / 2)
$Source.Width = $cropWidth
} else {
$cropHeight = [int]($Source.Width / $destinationRatio)
$Source.Y += [int](($Source.Height - $cropHeight) / 2)
$Source.Height = $cropHeight
}
$Graphics.DrawImage($Image, $Destination, $Source, [System.Drawing.GraphicsUnit]::Pixel)
}
function Draw-Header($Graphics, [string]$Title, [string]$Subtitle) {
$brush = New-Object System.Drawing.Drawing2D.LinearGradientBrush(
(New-Object System.Drawing.Rectangle(0, 0, 1280, 112)),
$purpleBright,
$purpleDark,
0
)
$titleFont = New-Object System.Drawing.Font("Segoe UI", 27, [System.Drawing.FontStyle]::Bold)
$subtitleFont = New-Object System.Drawing.Font("Segoe UI", 15, [System.Drawing.FontStyle]::Regular)
try {
$Graphics.FillRectangle($brush, 0, 0, 1280, 112)
$Graphics.DrawString($Title, $titleFont, [System.Drawing.Brushes]::White, 38, 20)
$Graphics.DrawString($Subtitle, $subtitleFont, [System.Drawing.Brushes]::White, 40, 67)
} finally {
$brush.Dispose(); $titleFont.Dispose(); $subtitleFont.Dispose()
}
}
function Redact-SandboxName($Graphics) {
$cover = New-Object System.Drawing.SolidBrush([System.Drawing.Color]::White)
try { $Graphics.FillRectangle($cover, 128, 124, 150, 34) } finally { $cover.Dispose() }
}
function Build-Screenshot([string]$Name, [string]$SourceName, [string]$Title, [string]$Subtitle, [System.Drawing.Rectangle]$Crop, [bool]$Redact) {
$source = [System.Drawing.Image]::FromFile((Join-Path $sourceDir $SourceName))
$canvas = New-Canvas 1280 800
$graphics = [System.Drawing.Graphics]::FromImage($canvas)
try {
Set-Quality $graphics
Draw-Header $graphics $Title $Subtitle
Draw-Cover $graphics $source (New-Object System.Drawing.Rectangle(0, 112, 1280, 688)) $Crop
if ($Redact) { Redact-SandboxName $graphics }
Save-Png $canvas $Name | Out-Null
} finally {
$graphics.Dispose(); $canvas.Dispose(); $source.Dispose()
}
}
$viewportCrop = New-Object System.Drawing.Rectangle(0, 100, 1920, 1040)
Build-Screenshot "01-original-canvas-page.png" "original.png" "A normal Canvas lesson page" "Global and course navigation surround the lesson." $viewportCrop $true
Build-Screenshot "02-lildra-focused-reading.png" "lildra.png" "A calmer reading view" "Lildra hides surrounding navigation and centres the lesson." $viewportCrop $false
Build-Screenshot "03-sticky-dynamic-title.png" "lildra.png" "The lesson title stays visible" "A compact title bar follows the current Canvas page title." (New-Object System.Drawing.Rectangle(170, 100, 1580, 850)) $false
Build-Screenshot "04-original-content-preserved.png" "lildra.png" "Original lesson content remains" "Headings, text and images remain in the original page." (New-Object System.Drawing.Rectangle(270, 240, 1500, 850)) $false
$before = [System.Drawing.Image]::FromFile((Join-Path $sourceDir "original.png"))
$after = [System.Drawing.Image]::FromFile((Join-Path $sourceDir "lildra.png"))
$comparison = New-Canvas 1280 800
$graphics = [System.Drawing.Graphics]::FromImage($comparison)
try {
Set-Quality $graphics
Draw-Header $graphics "Before and after" "The same lesson, with surrounding Canvas navigation hidden."
Draw-Cover $graphics $before (New-Object System.Drawing.Rectangle(0, 112, 632, 688)) (New-Object System.Drawing.Rectangle(0, 100, 1050, 1040))
Draw-Cover $graphics $after (New-Object System.Drawing.Rectangle(648, 112, 632, 688)) (New-Object System.Drawing.Rectangle(250, 100, 1050, 1040))
$divider = New-Object System.Drawing.SolidBrush($purple)
$labelFont = New-Object System.Drawing.Font("Segoe UI", 15, [System.Drawing.FontStyle]::Bold)
try {
$graphics.FillRectangle($divider, 632, 112, 16, 688)
$graphics.FillRectangle([System.Drawing.Brushes]::White, 18, 130, 92, 34)
$graphics.FillRectangle([System.Drawing.Brushes]::White, 666, 130, 92, 34)
$graphics.DrawString("Before", $labelFont, (New-Object System.Drawing.SolidBrush($ink)), 28, 134)
$graphics.DrawString("Lildra", $labelFont, (New-Object System.Drawing.SolidBrush($purple)), 676, 134)
# Remove the private sandbox course name from the before panel.
$graphics.FillRectangle([System.Drawing.Brushes]::White, 102, 124, 145, 36)
} finally { $divider.Dispose(); $labelFont.Dispose() }
Save-Png $comparison "05-before-and-after.png" | Out-Null
} finally {
$graphics.Dispose(); $comparison.Dispose(); $before.Dispose(); $after.Dispose()
}
# Create a store icon candidate from the established logo. Only edge-connected
# near-white canvas pixels are removed; the white L inside the purple mark remains.
$originalIcon = [System.Drawing.Image]::FromFile((Join-Path $Root "icons\LildraLogoHiRes.png"))
$sourceIcon = New-Canvas $originalIcon.Width $originalIcon.Height
$sourceGraphics = [System.Drawing.Graphics]::FromImage($sourceIcon)
try {
$sourceGraphics.DrawImageUnscaled($originalIcon, 0, 0)
} finally {
$sourceGraphics.Dispose(); $originalIcon.Dispose()
}
$visited = New-Object 'bool[,]' $sourceIcon.Width, $sourceIcon.Height
$queue = New-Object 'System.Collections.Generic.Queue[System.Drawing.Point]'
for ($x = 0; $x -lt $sourceIcon.Width; $x++) {
$queue.Enqueue([System.Drawing.Point]::new($x, 0))
$queue.Enqueue([System.Drawing.Point]::new($x, ($sourceIcon.Height - 1)))
}
for ($y = 0; $y -lt $sourceIcon.Height; $y++) {
$queue.Enqueue([System.Drawing.Point]::new(0, $y))
$queue.Enqueue([System.Drawing.Point]::new(($sourceIcon.Width - 1), $y))
}
while ($queue.Count -gt 0) {
$point = $queue.Dequeue()
if ($point.X -lt 0 -or $point.Y -lt 0 -or $point.X -ge $sourceIcon.Width -or $point.Y -ge $sourceIcon.Height -or $visited[$point.X, $point.Y]) { continue }
$visited[$point.X, $point.Y] = $true
$pixel = $sourceIcon.GetPixel($point.X, $point.Y)
if ($pixel.R -lt 215 -or $pixel.G -lt 215 -or $pixel.B -lt 215) { continue }
$sourceIcon.SetPixel($point.X, $point.Y, [System.Drawing.Color]::Transparent)
$queue.Enqueue([System.Drawing.Point]::new(($point.X + 1), $point.Y))
$queue.Enqueue([System.Drawing.Point]::new(($point.X - 1), $point.Y))
$queue.Enqueue([System.Drawing.Point]::new($point.X, ($point.Y + 1)))
$queue.Enqueue([System.Drawing.Point]::new($point.X, ($point.Y - 1)))
}
$storeIcon = New-Canvas 128 128
$graphics = [System.Drawing.Graphics]::FromImage($storeIcon)
try {
Set-Quality $graphics
$graphics.Clear([System.Drawing.Color]::Transparent)
$graphics.DrawImage($sourceIcon, (New-Object System.Drawing.Rectangle(16, 16, 96, 96)))
Save-Png $storeIcon "store-icon-128.png" | Out-Null
} finally { $graphics.Dispose(); $storeIcon.Dispose(); $sourceIcon.Dispose() }
$icon = [System.Drawing.Image]::FromFile((Join-Path $outputDir "store-icon-128.png"))
$focused = [System.Drawing.Image]::FromFile((Join-Path $sourceDir "lildra.png"))
foreach ($promo in @(
@{Name="small-promo-440x280.png"; Width=440; Height=280; Icon=112; Screenshot=$false},
@{Name="marquee-promo-1400x560.png"; Width=1400; Height=560; Icon=170; Screenshot=$true}
)) {
$canvas = New-Canvas $promo.Width $promo.Height
$graphics = [System.Drawing.Graphics]::FromImage($canvas)
$gradient = New-Object System.Drawing.Drawing2D.LinearGradientBrush(
(New-Object System.Drawing.Rectangle(0, 0, $promo.Width, $promo.Height)),
$purpleBright,
$purpleDark,
12
)
$nameFont = New-Object System.Drawing.Font("Segoe UI", $(if($promo.Width -gt 500){58}else{34}), [System.Drawing.FontStyle]::Bold)
$tagFont = New-Object System.Drawing.Font("Segoe UI", $(if($promo.Width -gt 500){24}else{15}), [System.Drawing.FontStyle]::Regular)
try {
Set-Quality $graphics
$graphics.FillRectangle($gradient, 0, 0, $promo.Width, $promo.Height)
$iconX = $(if($promo.Width -gt 500){80}else{34})
$iconY = [int](($promo.Height - $promo.Icon) / 2)
$graphics.DrawImage($icon, $iconX, $iconY, $promo.Icon, $promo.Icon)
$textX = $iconX + $promo.Icon + $(if($promo.Width -gt 500){42}else{22})
$graphics.DrawString("Lildra", $nameFont, [System.Drawing.Brushes]::White, $textX, $(if($promo.Width -gt 500){170}else{105}))
if ($promo.Width -gt 500) {
$graphics.DrawString("Distraction-free lesson reading", $tagFont, [System.Drawing.Brushes]::White, $textX + 2, 250)
}
if ($promo.Screenshot) {
$panel = New-Object System.Drawing.Rectangle(785, 55, 570, 450)
Draw-Cover $graphics $focused $panel $viewportCrop
}
Save-Png $canvas $promo.Name | Out-Null
} finally {
$gradient.Dispose(); $nameFont.Dispose(); $tagFont.Dispose(); $graphics.Dispose(); $canvas.Dispose()
}
}
$icon.Dispose(); $focused.Dispose()
Get-ChildItem -LiteralPath $outputDir -File | Sort-Object Name | Select-Object Name, Length