diff --git a/.github/workflows/check-version-increase.yml b/.github/workflows/check-version-increase.yml deleted file mode 100644 index 7203e1b..0000000 --- a/.github/workflows/check-version-increase.yml +++ /dev/null @@ -1,24 +0,0 @@ -name: check latest version - -on: - pull_request: - types: - - opened - - synchronize - branches: - - main - paths: - - 'src/**' - -jobs: - lint-and-fmt: - runs-on: ubuntu-latest - timeout-minutes: 1 - - steps: - - uses: actions/checkout@v3 - - - name: Check version - env: - REPO_NAME: ${{ github.repository }} - run: bash scripts/check-version-increase.sh $REPO_NAME diff --git a/.github/workflows/check-version-update.yml b/.github/workflows/check-version-update.yml new file mode 100644 index 0000000..4918bb3 --- /dev/null +++ b/.github/workflows/check-version-update.yml @@ -0,0 +1,41 @@ +name: Check Version Increase + +on: + pull_request: + types: + - opened + - synchronize + branches: + - main + paths: + - "src/**" + +jobs: + get-main-version: + uses: ./.github/workflows/manifest-version.yml + with: + ref: refs/heads/main + checkout-ref: main + + get-current-version: + uses: ./.github/workflows/manifest-version.yml + with: + ref: ${{ github.event.pull_request.head.sha }} + + check-version: + runs-on: ubuntu-latest + needs: [get-main-version, get-current-version] + + steps: + - run: | + echo "current: ${{ needs.get-current-version.outputs.version }}" + + - uses: denoland/setup-deno@v2 + with: + deno-version: v2.x + + - name: compare versions + env: + MAIN_VERSION: ${{ needs.get-main-version.outputs.version }} + CURRENT_VERSION: ${{ needs.get-current-version.outputs.version }} + run: test "$MAIN_VERSION" != "$CURRENT_VERSION" diff --git a/.github/workflows/create-tag-on-pr-merge.yml b/.github/workflows/create-tag-on-pr-merge.yml index 5f8c191..dab7c96 100644 --- a/.github/workflows/create-tag-on-pr-merge.yml +++ b/.github/workflows/create-tag-on-pr-merge.yml @@ -7,40 +7,25 @@ on: types: - closed paths: - - 'src/**' + - "src/**" jobs: + get-version: + uses: ./.github/workflows/manifest-version.yml + with: + ref: ${{ github.event.pull_request.head.sha }} + create-tag: if: github.event.pull_request.merged == true runs-on: ubuntu-latest + needs: get-version timeout-minutes: 1 steps: - - uses: actions/checkout@v3 - - - name: Extract version - run: echo "VERSION=v$(sh scripts/get-manifest-version.sh)" >> $GITHUB_ENV - - - name: Get commit URL - env: - COMMITS_URL: ${{ github.event.pull_request.base.repo.commits_url }} - SHA: ${{ github.sha }} - run: echo "COMMIT_URL=$COMMITS_URL" | sed -e "s#{/sha}#/$SHA#" >> $GITHUB_ENV + - uses: actions/checkout@v4 - - name: Get commit message - run: | - EOF=$(dd if=/dev/urandom bs=15 count=1 status=none | base64) - echo "MESSAGE<<$EOF" >> $GITHUB_ENV - JSON_TEXT=$(curl -s -L -H "Accept: application/vnd.github+json" $COMMIT_URL) - VERSION_STRING=$(echo "$JSON_TEXT" | jq ".commit.message" | sed -E 's/^"//' | sed -E 's/"$//') - echo -e "$VERSION_STRING" >> $GITHUB_ENV - echo "$EOF" >> $GITHUB_ENV + - name: create tag + run: git tag ${{ needs.get-version.outputs.version }} - - name: Add tag ${{ env.VERSION }} - uses: rickstaa/action-create-tag@v1 - id: create-tag - with: - tag: ${{ env.VERSION }} - tag_exists_error: true - commit_sha: ${{ github.sha }} - message: ${{ env.MESSAGE }} + - name: push tag + run: git push origin ${{ needs.get-version.outputs.version }} diff --git a/.github/workflows/lint-fmt-test.yml b/.github/workflows/lint-fmt-test.yml deleted file mode 100644 index 82cdfa4..0000000 --- a/.github/workflows/lint-fmt-test.yml +++ /dev/null @@ -1,28 +0,0 @@ -name: lint, check format and run build as a test - -on: push - -permissions: - contents: read - -jobs: - lint-and-fmt: - runs-on: ubuntu-latest - timeout-minutes: 1 - - steps: - - uses: actions/checkout@v3 - - - name: Setup Deno - uses: denoland/setup-deno@v1 - with: - deno-version: v1.x - - - name: Lint - run: deno lint - - - name: Check format - run: deno fmt --check - - - name: Build - run: deno task build diff --git a/.github/workflows/lint-format.yml b/.github/workflows/lint-format.yml new file mode 100644 index 0000000..41b6cac --- /dev/null +++ b/.github/workflows/lint-format.yml @@ -0,0 +1,46 @@ +name: Lint and Check Format + +on: push + +jobs: + lint: + runs-on: ubuntu-latest + timeout-minutes: 3 + + steps: + - uses: actions/checkout@v4 + + - uses: denoland/setup-deno@v2 + with: + deno-version: v2.x + + - name: lint + run: deno lint + + format: + runs-on: ubuntu-latest + timeout-minutes: 3 + + steps: + - uses: actions/checkout@v4 + + - uses: denoland/setup-deno@v2 + with: + deno-version: v2.x + + - name: check format + run: deno fmt --check + + build: + runs-on: ubuntu-latest + timeout-minutes: 3 + + steps: + - uses: actions/checkout@v4 + + - uses: denoland/setup-deno@v2 + with: + deno-version: v2.x + + - name: build + run: deno task build diff --git a/.github/workflows/manifest-version.yml b/.github/workflows/manifest-version.yml new file mode 100644 index 0000000..dbf7a05 --- /dev/null +++ b/.github/workflows/manifest-version.yml @@ -0,0 +1,41 @@ +name: Manifest Version + +on: + workflow_call: + inputs: + ref: + required: true + type: string + checkout-ref: + type: string + outputs: + version: + value: ${{ jobs.get-version.outputs.version }} + +jobs: + get-version: + runs-on: ubuntu-latest + timeout-minutes: 3 + + outputs: + version: ${{ steps.get-version.outputs.version }} + + steps: + - run: 'echo "ref: ${{ inputs.ref }}"' + + - uses: actions/checkout@v4 + with: + ref: ${{ inputs.checkout-ref || inputs.ref }} + + - uses: actions/checkout@v4 + + - uses: denoland/setup-deno@v2 + with: + deno-version: v2.x + + - name: get version + id: get-version + run: | + VERSION=$(NOCOLOR="true" git show "${{ inputs.ref }}:./src/manifest.jsonc" | deno run scripts/getManifestVersion.ts) + echo "Version: $VERSION" + echo "version=$VERSION" >> $GITHUB_OUTPUT diff --git a/.github/workflows/release-attach-artifact-manual.yml b/.github/workflows/release-attach-artifact-manual.yml deleted file mode 100644 index e2a5ff5..0000000 --- a/.github/workflows/release-attach-artifact-manual.yml +++ /dev/null @@ -1,58 +0,0 @@ -name: attach build artifact to release manually - -on: workflow_dispatch - -permissions: write-all - -env: - TEMP_ASSET_NAME: asset.zip - -jobs: - attach-build-artifact: - runs-on: ubuntu-latest - timeout-minutes: 1 - - steps: - - uses: actions/checkout@v3 - - - name: Get tag from ref - env: - REF: ${{ github.ref }} - run: | - [[ $REF =~ ^refs/tags/ ]] || (echo "This workflow must be run from tag."; exit 1) - TAG_NAME=$(echo $REF | sed -E s#^refs/tags/##) - echo "TAG_NAME=$TAG_NAME" >> $GITHUB_ENV - - - name: Get upload url - env: - RELEASES_URL: ${{ github.event.repository.releases_url }} - run: | - URL=$(echo "$RELEASES_URL" | sed -e s:{/id}:/tags/$TAG_NAME:) - JSON_TEXT=$(curl -sSL $URL) - UPLOAD_URL=$(echo "$JSON_TEXT" | jq ".upload_url" | sed -e 's/\"//g') - UPLOAD_URL=$(echo "$UPLOAD_URL" | sed -e "s/{?name,label}/?name=nitech-moodle-extension-40a-${TAG_NAME}.zip/") - echo "UPLOAD_URL=$UPLOAD_URL" >> $GITHUB_ENV - - - name: Setup Deno - uses: denoland/setup-deno@v1 - with: - deno-version: v1.x - - - name: Build - run: deno task build - - - name: Create zip - run: cd ./dist; zip $TEMP_ASSET_NAME -r . - - - name: Upload release asset - env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - run: | - curl -L \ - -X POST \ - -H "Accept: application/vnd.github+json" \ - -H "Authorization: Bearer $GITHUB_TOKEN" \ - -H "X-GitHub-Api-Version: 2022-11-28" \ - -H "Content-Type: application/octet-stream" \ - $UPLOAD_URL \ - --data-binary "@dist/$TEMP_ASSET_NAME" diff --git a/.github/workflows/release-attach-artifact.yml b/.github/workflows/release-attach-artifact.yml index 9751d26..4f50157 100644 --- a/.github/workflows/release-attach-artifact.yml +++ b/.github/workflows/release-attach-artifact.yml @@ -1,45 +1,36 @@ -name: attach build artifact to release +name: "Release: Attach Artifact" on: release: types: - - published + - released + - prereleased + +permissions: write-all env: - TEMP_ASSET_NAME: asset.zip + FILE_NAME: nitech-moodle-extension-40a-${{ github.event.release.tag_name }}.zip jobs: attach-build-artifact: runs-on: ubuntu-latest - timeout-minutes: 1 + timeout-minutes: 3 steps: - - uses: actions/checkout@v3 - - - name: Set environment variables - env: - TAG_NAME: ${{ github.event.release.tag_name }} - run: | - echo "TAG_NAME=$TAG_NAME" >> $GITHUB_ENV + - uses: actions/checkout@v4 - - name: Setup Deno - uses: denoland/setup-deno@v1 + - uses: denoland/setup-deno@v2 with: - deno-version: v1.x + deno-version: v2.x - - name: Build + - name: build run: deno task build - - name: Create zip - run: cd ./dist; zip $TEMP_ASSET_NAME -r . + - name: zip artifacts + run: cd ./dist; zip $FILE_NAME -r . - - name: Upload release asset - id: upload-release-asset - uses: actions/upload-release-asset@v1 + - name: upload env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - with: - upload_url: ${{ github.event.release.upload_url }} - asset_path: ./dist/${{ env.TEMP_ASSET_NAME }} - asset_name: nitech-moodle-extension-40a-${{ env.TAG_NAME }}.zip - asset_content_type: application/zip + run: | + gh release upload ${{ github.event.release.tag_name }} $FILE_NAME diff --git a/build.ts b/build.ts deleted file mode 100644 index faba26c..0000000 --- a/build.ts +++ /dev/null @@ -1,28 +0,0 @@ -import * as esbuild from 'esbuild'; -import { Command } from "cliffy"; -import { readLines } from "std/io/mod.ts"; -import devConfig from './esbuild.dev.ts'; -import prodConfig from './esbuild.prod.ts'; - -const { options, args } = await new Command() - .option('-d, --dev', 'development mode') - .option('-w, --watch', 'watch mode (development only)') - .parse(Deno.args); - -const config = options.dev ? devConfig : prodConfig; -const ctx = await esbuild.context(config); - -if(options.dev && options.watch) { - await ctx.watch(); - console.log('Watching...'); - - for await(const _ of readLines(Deno.stdin)) { - // manually rebuild - await ctx.rebuild().catch(() => {}); - } -} else { - // just build - await ctx.rebuild(); -} - -esbuild.stop(); diff --git a/build/build.ts b/build/build.ts new file mode 100644 index 0000000..40ce520 --- /dev/null +++ b/build/build.ts @@ -0,0 +1,126 @@ +import * as esbuild from "esbuild"; +import * as fs from "@std/fs"; +import * as JSONC from "@std/jsonc"; +import * as path from "@std/path"; +import { TextLineStream } from "@std/streams"; + +import { Manifest } from "./manifest.ts"; +import { buildOptions as jsBuildOptions } from "./options/javascript.ts"; +import { buildOptions as cssBuildOptions } from "./options/stylesheet.ts"; +import { buildOptions as copyBuildOptions } from "./options/copy.ts"; + +const watchMode = Deno.env.has("WATCH"); +const devMode = watchMode || Deno.env.has("DEV"); + +const srcPath = "./src"; +const destPath = "./dist"; + +const denoConfigFilePath = path.resolve("deno.json"); +const manifestFilePath = path.resolve(srcPath, "manifest.jsonc"); + +const denoConfig = JSONC.parse(await Deno.readTextFile(denoConfigFilePath)); +const manifest = Manifest.parse(await Deno.readTextFile(manifestFilePath)); + +const removeExtension = function (name: string) { + return name.slice(0, -path.extname(name).length); +}; + +const jsEntryPointsRaw = manifest.getScripts().map((filename) => ({ + in: filename, + out: `${removeExtension(filename)}.js`, +})); +const cssEntryPointsRaw = manifest.getStylesheets().map((filename) => ({ + in: filename, + out: `${removeExtension(filename)}.css`, +})); +const jsEntryPoints = jsEntryPointsRaw.map((entry) => ({ + in: path.resolve(srcPath, entry.in), + out: path.resolve(destPath, removeExtension(entry.out)), +})); +const cssEntryPoints = cssEntryPointsRaw.map((entry) => ({ + in: path.resolve(srcPath, entry.in), + out: path.resolve(destPath, removeExtension(entry.out)), +})); +const copyEntryPoints = manifest.getFilesToCopy().map((filename) => ({ + in: path.resolve(srcPath, filename), + out: path.resolve(destPath, removeExtension(filename)), +})); + +const resourceReplacementMap = new Map(); +for (const entry of jsEntryPointsRaw) { + resourceReplacementMap.set(entry.in, entry.out); +} +for (const entry of cssEntryPointsRaw) { + resourceReplacementMap.set(entry.in, entry.out); +} +const newManifest = manifest.resourceFileReplaced(resourceReplacementMap); + +const buildOptions = [ + jsBuildOptions({ + entryPoints: jsEntryPoints, + srcPath, + destPath, + dev: devMode, + jsxFactory: denoConfig.jsxFactory, + jsxFragmentFactory: denoConfig.jsxFragmentFactory, + extensionName: manifest.name, + extensionVersion: manifest.version, + }), + cssBuildOptions({ + entryPoints: cssEntryPoints, + srcPath, + destPath, + dev: devMode, + }), + copyBuildOptions({ + entryPoints: copyEntryPoints, + srcPath, + destPath, + loaderExts: [".html"], + }), +]; +const buildContexts = await Promise.all( + buildOptions.map((buildOptions) => esbuild.context(buildOptions)), +); + +console.log("Preparing to build..."); +await fs.ensureDir(destPath); +await Deno.writeTextFile( + path.resolve(destPath, "manifest.json"), + newManifest.stringify(), +); + +console.log("Building..."); +await Promise.all(buildContexts.map((ctx) => ctx.rebuild())); + +if (!watchMode) { + await Promise.all(buildContexts.map((ctx) => ctx.cancel())); + await Promise.all(buildContexts.map((ctx) => ctx.dispose())); + Deno.exit(0); +} + +await Promise.all(buildContexts.map((ctx) => ctx.watch())); + +console.log("Watching..."); +console.log('press "r" to rebuild'); +console.log('press "q" to exit'); + +const encoder = new TextEncoder(); +const stdinLines = Deno.stdin.readable + .pipeThrough(new TextDecoderStream()) + .pipeThrough(new TextLineStream()); +await Deno.stdout.write(encoder.encode("> ")); + +for await (const line_ of stdinLines) { + const line = line_.trim(); + + if (line === "r" || line === "reload") { + await Promise.all(buildContexts.map((ctx) => ctx.rebuild())); + } else if (line === "q" || line === "exit") { + await Promise.all(buildContexts.map((ctx) => ctx.cancel())); + await Promise.all(buildContexts.map((ctx) => ctx.dispose())); + Deno.exit(0); + } + + await Deno.stdout.write(encoder.encode("> ")); +} diff --git a/build/manifest.ts b/build/manifest.ts new file mode 100644 index 0000000..1cce7dd --- /dev/null +++ b/build/manifest.ts @@ -0,0 +1,115 @@ +import * as JSONC from "@std/jsonc"; + +export type ContentScript = { + matches: string[]; + js?: string[]; + css?: string[]; + run_at: "document_start" | "document_end" | "document_idle"; + match_about_blank: boolean; + match_origin_as_fallback: boolean; +}; + +export type OptionsUi = { + page: string; + open_in_tab: boolean; + browser_style: boolean; +}; + +export type WebExtensionManifest = { + manifest_version: 3; + name: string; + version: string; + + description: string; + + author: string; + content_scripts: ContentScript[]; + content_security_policy: { + extension_pages?: string; + sandbox?: string; + }; + options_ui: OptionsUi; + + permissions: string[]; + host_permissions: string[]; +}; + +export type ExtendedWebExtensionManifest = WebExtensionManifest & { + options_ui: OptionsUi & { + js?: string[]; + css?: string[]; + }; +}; + +export class Manifest { + private manifest: ExtendedWebExtensionManifest; + + get manifest_version(): (typeof this.manifest)["manifest_version"] { + return this.manifest.manifest_version; + } + + get name(): (typeof this.manifest)["name"] { + return this.manifest.name; + } + + get version(): (typeof this.manifest)["version"] { + return this.manifest.version; + } + + private constructor(manifest: Manifest["manifest"]) { + this.manifest = manifest; + } + + static parse(text: string): Manifest { + return new Manifest(JSONC.parse(text) as Manifest["manifest"]); + } + + stringify(): string { + return JSON.stringify({ + ...this.manifest, + options_ui: { + ...this.manifest.options_ui, + js: undefined, + css: undefined, + }, + }); + } + + getScripts(): string[] { + return [ + ...this.manifest.content_scripts.flatMap((entry) => entry.js ?? []), + ...this.manifest.options_ui.js ?? [], + ]; + } + + getStylesheets(): string[] { + return [ + ...this.manifest.content_scripts.flatMap((entry) => entry.css ?? []), + ...this.manifest.options_ui.css ?? [], + ]; + } + + getFilesToCopy(): string[] { + return [this.manifest.options_ui.page]; + } + + resourceFileReplaced(map: Map): Manifest { + const manifest = structuredClone(this.manifest); + const newManifest: ExtendedWebExtensionManifest = { + ...manifest, + content_scripts: manifest.content_scripts.map((contentScript) => ({ + ...contentScript, + js: contentScript.js?.map((js) => map.get(js) ?? js), + css: contentScript.css?.map((css) => map.get(css) ?? css), + })), + options_ui: { + ...manifest.options_ui, + page: map.get(manifest.options_ui.page) ?? manifest.options_ui.page, + js: manifest.options_ui.js?.map((js) => map.get(js) ?? js), + css: manifest.options_ui.css?.map((css) => map.get(css) ?? css), + }, + }; + + return new Manifest(newManifest); + } +} diff --git a/build/options/copy.ts b/build/options/copy.ts new file mode 100644 index 0000000..5ab0d50 --- /dev/null +++ b/build/options/copy.ts @@ -0,0 +1,18 @@ +import * as esbuild from "esbuild"; + +type BuildOptionsOptions = { + entryPoints: esbuild.BuildOptions["entryPoints"]; + srcPath: string; + destPath: string; + loaderExts: string[]; +}; + +export const buildOptions = ( + options: BuildOptionsOptions, +): esbuild.BuildOptions => ({ + entryPoints: options.entryPoints, + outdir: options.destPath, + loader: Object.fromEntries( + options.loaderExts.map((ext) => [ext, "copy" as const]), + ), +}); diff --git a/build/options/javascript.ts b/build/options/javascript.ts new file mode 100644 index 0000000..647fd2c --- /dev/null +++ b/build/options/javascript.ts @@ -0,0 +1,39 @@ +import * as esbuild from "esbuild"; +import { denoPlugins } from "esbuild-deno-loader"; +import { debugSwitchPlugin } from "esbuild-plugin-debug-switch/plugin"; + +type BuildOptionsOptions = { + entryPoints: esbuild.BuildOptions["entryPoints"]; + srcPath: string; + destPath: string; + dev: boolean; + jsxFactory?: string; + jsxFragmentFactory?: string; + extensionName: string; + extensionVersion: string; +}; + +export const buildOptions = ( + options: BuildOptionsOptions, +): esbuild.BuildOptions => ({ + entryPoints: options.entryPoints, + outdir: options.destPath, + platform: "browser", + bundle: true, + sourcemap: options.dev ? "inline" : false, + minify: !options.dev, + jsxDev: options.dev, + jsx: "automatic", + jsxFactory: options.jsxFactory, + jsxFragment: options.jsxFragmentFactory, + plugins: [ + debugSwitchPlugin({ + isDebug: options.dev, + env: { + extensionName: options.extensionName, + extensionVersion: options.extensionVersion, + }, + }), + ...denoPlugins(), + ], +}); diff --git a/build/options/stylesheet.ts b/build/options/stylesheet.ts new file mode 100644 index 0000000..f3734d3 --- /dev/null +++ b/build/options/stylesheet.ts @@ -0,0 +1,23 @@ +import * as esbuild from "esbuild"; +import { sassPlugin } from "esbuild-plugin-sass"; + +type BuildOptionsOptions = { + entryPoints: esbuild.BuildOptions["entryPoints"]; + srcPath: string; + destPath: string; + dev: boolean; +}; + +export const buildOptions = ( + options: BuildOptionsOptions, +): esbuild.BuildOptions => ({ + entryPoints: options.entryPoints, + outdir: options.destPath, + platform: "browser", + bundle: true, + sourcemap: options.dev ? "linked" : false, + minify: !options.dev, + plugins: [ + sassPlugin(), + ], +}); diff --git a/build/plugins/logBuildResult.ts b/build/plugins/logBuildResult.ts new file mode 100644 index 0000000..b0ca0d4 --- /dev/null +++ b/build/plugins/logBuildResult.ts @@ -0,0 +1,44 @@ +import type * as esbuild from "esbuild"; + +const printLog = function ( + numErrors: number, + numWarnings: number, + startTime: Date, + endTime: Date, +) { + const buildTime = endTime.getTime() - startTime.getTime(); + + let message = `${endTime.toLocaleTimeString("en-GB")}: `; + if (numErrors > 0 && numWarnings > 0) { + message += + `Build failed with ${numErrors} errors and ${numWarnings} warnings`; + } else if (numErrors > 0) { + message += `Build failed with ${numErrors} errors`; + } else if (numWarnings > 0) { + message += `Build failed with ${numWarnings} warnings`; + } else { + message += `Build succeeded`; + } + + message += ` in ${buildTime} ms`; +}; + +export const logBuildResultPlugin = (): esbuild.Plugin => ({ + name: "log-and-output-filename", + setup(build) { + // assign to avoid type error + let startTime = new Date(); + + build.onStart(() => { + startTime = new Date(); + }); + + build.onEnd((result) => { + const numErrors = result.errors.length; + const numWarnings = result.warnings.length; + const endTime = new Date(); + + printLog(numErrors, numWarnings, startTime, endTime); + }); + }, +}); diff --git a/deno.json b/deno.json index 2a36a7e..654e9b3 100644 --- a/deno.json +++ b/deno.json @@ -1,10 +1,7 @@ { "compilerOptions": { - "allowJs": true, "strict": true, - "strictNullChecks": true, - "noImplicitThis": true, - "noImplicitAny": true, + "noImplicitOverride": true, "noUnusedParameters": true, "lib": [ "DOM", @@ -16,35 +13,44 @@ "jsxFragmentFactory": "preact.Fragment" }, "lint": { - "files": { - "include": ["src/"], - "exclude": ["dist/"] - }, "rules": { "tags": ["recommended"] } }, "fmt": { - "files": { - "include": ["src/"], - "exclude": ["dist/"] - }, - "options": { - "indentWidth": 2, - "lineWidth": 80, - "proseWrap": "always", - "singleQuote": true, - "useTabs": false - } + "indentWidth": 2, + "lineWidth": 80, + "proseWrap": "always", + "singleQuote": false, + "useTabs": false }, "tasks": { - "build": "deno run --allow-run --allow-read --allow-write --allow-env --allow-net --importmap import_map.json ./build.ts", - "dev": "deno run --allow-run --allow-read --allow-write --allow-env --allow-net --importmap import_map.json ./build.ts --dev", - "watch": "deno run --allow-run --allow-read --allow-write --allow-env --allow-net --importmap import_map.json ./build.ts --dev --watch", - "clean": "rm -rf dist/ && rm -rf cache/", - "cache-all": "bash ./scripts/cache-all.sh", + "build": "deno run --allow-run --allow-read --allow-write --allow-env --allow-net ./build/build.ts", + "dev": "DEV=1 deno task build", + "watch": "WATCH=1 deno task build", + "clean": "rm -rf ./dist", "check-version": "bash ./scripts/checkVersion.sh", "check-version-increase": "bash ./scripts/checkVersionIncrease.sh", "setup-vscode": "deno run --allow-run --allow-read --allow-write scripts/setup-vscode.ts" - } -} \ No newline at end of file + }, + "imports": { + "@std/fs": "jsr:@std/fs@^1.0.4", + "@std/jsonc": "jsr:@std/jsonc@^1.0.1", + "@std/path": "jsr:@std/path@^1.0.6", + "@std/streams": "jsr:@std/streams@^1.0.7", + "@types/webextension-polyfill": "npm:@types/webextension-polyfill@^0.12.1", + "esbuild": "npm:esbuild@0.24.0", + "esbuild-deno-loader": "jsr:@luca/esbuild-deno-loader@^0.11.0", + "esbuild-plugin-debug-switch": "jsr:@tsukina-7mochi/esbuild-plugin-debug-switch@^0.2.0", + "esbuild-plugin-sass": "jsr:@tsukina-7mochi/esbuild-plugin-sass@^0.1.1", + "preact": "npm:preact@^10.24.3", + "preact/compat": "npm:preact@^10.24.3/compat", + "react/jsx-runtime": "npm:preact@^10.24.3/jsx-runtime", + "react/jsx-dev-runtime": "npm:preact@^10.24.3/jsx-dev-runtime", + "webextension-polyfill": "npm:webextension-polyfill@^0.12.0", + "~/": "./src/" + }, + "exclude": [ + "dist/" + ] +} diff --git a/deno.lock b/deno.lock index 26e2035..b4d27dc 100644 --- a/deno.lock +++ b/deno.lock @@ -1,240 +1,318 @@ { - "version": "2", - "remote": { - "https://deno.land/std@0.131.0/_util/assert.ts": "e94f2eb37cebd7f199952e242c77654e43333c1ac4c5c700e929ea3aa5489f74", - "https://deno.land/std@0.131.0/_util/os.ts": "49b92edea1e82ba295ec946de8ffd956ed123e2948d9bd1d3e901b04e4307617", - "https://deno.land/std@0.131.0/path/_constants.ts": "df1db3ffa6dd6d1252cc9617e5d72165cd2483df90e93833e13580687b6083c3", - "https://deno.land/std@0.131.0/path/_interface.ts": "ee3b431a336b80cf445441109d089b70d87d5e248f4f90ff906820889ecf8d09", - "https://deno.land/std@0.131.0/path/_util.ts": "c1e9686d0164e29f7d880b2158971d805b6e0efc3110d0b3e24e4b8af2190d2b", - "https://deno.land/std@0.131.0/path/common.ts": "bee563630abd2d97f99d83c96c2fa0cca7cee103e8cb4e7699ec4d5db7bd2633", - "https://deno.land/std@0.131.0/path/glob.ts": "cb5255638de1048973c3e69e420c77dc04f75755524cb3b2e160fe9277d939ee", - "https://deno.land/std@0.131.0/path/mod.ts": "4275129bb766f0e475ecc5246aa35689eeade419d72a48355203f31802640be7", - "https://deno.land/std@0.131.0/path/posix.ts": "663e4a6fe30a145f56aa41a22d95114c4c5582d8b57d2d7c9ed27ad2c47636bb", - "https://deno.land/std@0.131.0/path/separator.ts": "fe1816cb765a8068afb3e8f13ad272351c85cbc739af56dacfc7d93d710fe0f9", - "https://deno.land/std@0.131.0/path/win32.ts": "e7bdf63e8d9982b4d8a01ef5689425c93310ece950e517476e22af10f41a136e", - "https://deno.land/std@0.162.0/_util/assert.ts": "e94f2eb37cebd7f199952e242c77654e43333c1ac4c5c700e929ea3aa5489f74", - "https://deno.land/std@0.162.0/_util/os.ts": "8a33345f74990e627b9dfe2de9b040004b08ea5146c7c9e8fe9a29070d193934", - "https://deno.land/std@0.162.0/fmt/colors.ts": "9e36a716611dcd2e4865adea9c4bec916b5c60caad4cdcdc630d4974e6bb8bd4", - "https://deno.land/std@0.162.0/fs/_util.ts": "fdc156f897197f261a1c096dcf8ff9267ed0ff42bd5b31f55053a4763a4bae3b", - "https://deno.land/std@0.162.0/fs/copy.ts": "73bdf24f4322648d9bc38ef983b818637ba368351d17aa03644209d3ce3eac31", - "https://deno.land/std@0.162.0/fs/empty_dir.ts": "c15a0aaaf40f8c21cca902aa1e01a789ad0c2fd1b7e2eecf4957053c5dbf707f", - "https://deno.land/std@0.162.0/fs/ensure_dir.ts": "76395fc1c989ca8d2de3aedfa8240eb8f5225cde20f926de957995b063135b80", - "https://deno.land/std@0.162.0/fs/ensure_file.ts": "b8e32ea63aa21221d0219760ba3f741f682d7f7d68d0d24a3ec067c338568152", - "https://deno.land/std@0.162.0/fs/ensure_link.ts": "5cc1c04f18487d7d1edf4c5469705f30b61390ffd24ad7db6df85e7209b32bb2", - "https://deno.land/std@0.162.0/fs/ensure_symlink.ts": "5273557b8c50be69477aa9cb003b54ff2240a336db52a40851c97abce76b96ab", - "https://deno.land/std@0.162.0/fs/eol.ts": "65b1e27320c3eec6fb653b27e20056ee3d015d3e91db388cfefa41616ebc7cb3", - "https://deno.land/std@0.162.0/fs/exists.ts": "6a447912e49eb79cc640adacfbf4b0baf8e17ede6d5bed057062ce33c4fa0d68", - "https://deno.land/std@0.162.0/fs/expand_glob.ts": "d3f62aefc7718d878904d60d91e8e6dbbf86c696d32b6cbbc333637acf7f8571", - "https://deno.land/std@0.162.0/fs/mod.ts": "354a6f972ef4e00c4dd1f1339a8828ef0764c1c23d3c0010af3fcc025d8655b0", - "https://deno.land/std@0.162.0/fs/move.ts": "6d7fa9da60dbc7a32dd7fdbc2ff812b745861213c8e92ba96dace0669b0c378c", - "https://deno.land/std@0.162.0/fs/walk.ts": "d96d4e5b6a3552e8304f28a0fd0b317b812298298449044f8de4932c869388a5", - "https://deno.land/std@0.162.0/path/_constants.ts": "df1db3ffa6dd6d1252cc9617e5d72165cd2483df90e93833e13580687b6083c3", - "https://deno.land/std@0.162.0/path/_interface.ts": "ee3b431a336b80cf445441109d089b70d87d5e248f4f90ff906820889ecf8d09", - "https://deno.land/std@0.162.0/path/_util.ts": "d16be2a16e1204b65f9d0dfc54a9bc472cafe5f4a190b3c8471ec2016ccd1677", - "https://deno.land/std@0.162.0/path/common.ts": "bee563630abd2d97f99d83c96c2fa0cca7cee103e8cb4e7699ec4d5db7bd2633", - "https://deno.land/std@0.162.0/path/glob.ts": "cb5255638de1048973c3e69e420c77dc04f75755524cb3b2e160fe9277d939ee", - "https://deno.land/std@0.162.0/path/mod.ts": "56fec03ad0ebd61b6ab39ddb9b0ddb4c4a5c9f2f4f632e09dd37ec9ebfd722ac", - "https://deno.land/std@0.162.0/path/posix.ts": "6b63de7097e68c8663c84ccedc0fd977656eb134432d818ecd3a4e122638ac24", - "https://deno.land/std@0.162.0/path/separator.ts": "fe1816cb765a8068afb3e8f13ad272351c85cbc739af56dacfc7d93d710fe0f9", - "https://deno.land/std@0.162.0/path/win32.ts": "ee8826dce087d31c5c81cd414714e677eb68febc40308de87a2ce4b40e10fb8d", - "https://deno.land/std@0.162.0/testing/_diff.ts": "a23e7fc2b4d8daa3e158fa06856bedf5334ce2a2831e8bf9e509717f455adb2c", - "https://deno.land/std@0.162.0/testing/_format.ts": "cd11136e1797791045e639e9f0f4640d5b4166148796cad37e6ef75f7d7f3832", - "https://deno.land/std@0.162.0/testing/asserts.ts": "1e340c589853e82e0807629ba31a43c84ebdcdeca910c4a9705715dfdb0f5ce8", - "https://deno.land/std@0.170.0/_util/asserts.ts": "d0844e9b62510f89ce1f9878b046f6a57bf88f208a10304aab50efcb48365272", - "https://deno.land/std@0.170.0/_util/os.ts": "8a33345f74990e627b9dfe2de9b040004b08ea5146c7c9e8fe9a29070d193934", - "https://deno.land/std@0.170.0/encoding/base64.ts": "8605e018e49211efc767686f6f687827d7f5fd5217163e981d8d693105640d7a", - "https://deno.land/std@0.170.0/fmt/colors.ts": "03ad95e543d2808bc43c17a3dd29d25b43d0f16287fe562a0be89bf632454a12", - "https://deno.land/std@0.170.0/path/_constants.ts": "df1db3ffa6dd6d1252cc9617e5d72165cd2483df90e93833e13580687b6083c3", - "https://deno.land/std@0.170.0/path/_interface.ts": "ee3b431a336b80cf445441109d089b70d87d5e248f4f90ff906820889ecf8d09", - "https://deno.land/std@0.170.0/path/_util.ts": "d16be2a16e1204b65f9d0dfc54a9bc472cafe5f4a190b3c8471ec2016ccd1677", - "https://deno.land/std@0.170.0/path/common.ts": "bee563630abd2d97f99d83c96c2fa0cca7cee103e8cb4e7699ec4d5db7bd2633", - "https://deno.land/std@0.170.0/path/glob.ts": "81cc6c72be002cd546c7a22d1f263f82f63f37fe0035d9726aa96fc8f6e4afa1", - "https://deno.land/std@0.170.0/path/mod.ts": "cf7cec7ac11b7048bb66af8ae03513e66595c279c65cfa12bfc07d9599608b78", - "https://deno.land/std@0.170.0/path/posix.ts": "b859684bc4d80edfd4cad0a82371b50c716330bed51143d6dcdbe59e6278b30c", - "https://deno.land/std@0.170.0/path/separator.ts": "fe1816cb765a8068afb3e8f13ad272351c85cbc739af56dacfc7d93d710fe0f9", - "https://deno.land/std@0.170.0/path/win32.ts": "7cebd2bda6657371adc00061a1d23fdd87bcdf64b4843bb148b0b24c11b40f69", - "https://deno.land/std@0.184.0/_util/asserts.ts": "178dfc49a464aee693a7e285567b3d0b555dc805ff490505a8aae34f9cfb1462", - "https://deno.land/std@0.184.0/_util/os.ts": "d932f56d41e4f6a6093d56044e29ce637f8dcc43c5a90af43504a889cf1775e3", - "https://deno.land/std@0.184.0/bytes/bytes_list.ts": "31d664f4d42fa922066405d0e421c56da89d751886ee77bbe25a88bf0310c9d0", - "https://deno.land/std@0.184.0/bytes/concat.ts": "d26d6f3d7922e6d663dacfcd357563b7bf4a380ce5b9c2bbe0c8586662f25ce2", - "https://deno.land/std@0.184.0/bytes/copy.ts": "939d89e302a9761dcf1d9c937c7711174ed74c59eef40a1e4569a05c9de88219", - "https://deno.land/std@0.184.0/fs/_util.ts": "579038bebc3bd35c43a6a7766f7d91fbacdf44bc03468e9d3134297bb99ed4f9", - "https://deno.land/std@0.184.0/fs/copy.ts": "14214efd94fc3aa6db1e4af2b4b9578e50f7362b7f3725d5a14ad259a5df26c8", - "https://deno.land/std@0.184.0/fs/empty_dir.ts": "c3d2da4c7352fab1cf144a1ecfef58090769e8af633678e0f3fabaef98594688", - "https://deno.land/std@0.184.0/fs/ensure_dir.ts": "dc64c4c75c64721d4e3fb681f1382f803ff3d2868f08563ff923fdd20d071c40", - "https://deno.land/std@0.184.0/fs/ensure_file.ts": "c38602670bfaf259d86ca824a94e6cb9e5eb73757fefa4ebf43a90dd017d53d9", - "https://deno.land/std@0.184.0/fs/ensure_link.ts": "c0f5b2f0ec094ed52b9128eccb1ee23362a617457aa0f699b145d4883f5b2fb4", - "https://deno.land/std@0.184.0/fs/ensure_symlink.ts": "5006ab2f458159c56d689b53b1e48d57e05eeb1eaf64e677f7f76a30bc4fdba1", - "https://deno.land/std@0.184.0/fs/eol.ts": "f1f2eb348a750c34500741987b21d65607f352cf7205f48f4319d417fff42842", - "https://deno.land/std@0.184.0/fs/exists.ts": "29c26bca8584a22876be7cb8844f1b6c8fc35e9af514576b78f5c6884d7ed02d", - "https://deno.land/std@0.184.0/fs/expand_glob.ts": "e4f56259a0a70fe23f05215b00de3ac5e6ba46646ab2a06ebbe9b010f81c972a", - "https://deno.land/std@0.184.0/fs/mod.ts": "bc3d0acd488cc7b42627044caf47d72019846d459279544e1934418955ba4898", - "https://deno.land/std@0.184.0/fs/move.ts": "b4f8f46730b40c32ea3c0bc8eb0fd0e8139249a698883c7b3756424cf19785c9", - "https://deno.land/std@0.184.0/fs/walk.ts": "920be35a7376db6c0b5b1caf1486fb962925e38c9825f90367f8f26b5e5d0897", - "https://deno.land/std@0.184.0/io/buf_reader.ts": "abeb92b18426f11d72b112518293a96aef2e6e55f80b84235e8971ac910affb5", - "https://deno.land/std@0.184.0/io/buf_writer.ts": "48c33c8f00b61dcbc7958706741cec8e59810bd307bc6a326cbd474fe8346dfd", - "https://deno.land/std@0.184.0/io/buffer.ts": "17f4410eaaa60a8a85733e8891349a619eadfbbe42e2f319283ce2b8f29723ab", - "https://deno.land/std@0.184.0/io/copy_n.ts": "0cc7ce07c75130f6fc18621ec1911c36e147eb9570664fee0ea12b1988167590", - "https://deno.land/std@0.184.0/io/limited_reader.ts": "6c9a216f8eef39c1ee2a6b37a29372c8fc63455b2eeb91f06d9646f8f759fc8b", - "https://deno.land/std@0.184.0/io/mod.ts": "2665bcccc1fd6e8627cca167c3e92aaecbd9897556b6f69e6d258070ef63fd9b", - "https://deno.land/std@0.184.0/io/multi_reader.ts": "9c2a0a31686c44b277e16da1d97b4686a986edcee48409b84be25eedbc39b271", - "https://deno.land/std@0.184.0/io/read_delim.ts": "c02b93cc546ae8caad8682ae270863e7ace6daec24c1eddd6faabc95a9d876a3", - "https://deno.land/std@0.184.0/io/read_int.ts": "7cb8bcdfaf1107586c3bacc583d11c64c060196cb070bb13ae8c2061404f911f", - "https://deno.land/std@0.184.0/io/read_lines.ts": "c526c12a20a9386dc910d500f9cdea43cba974e853397790bd146817a7eef8cc", - "https://deno.land/std@0.184.0/io/read_long.ts": "f0aaa420e3da1261c5d33c5e729f09922f3d9fa49f046258d4ff7a00d800c71e", - "https://deno.land/std@0.184.0/io/read_range.ts": "28152daf32e43dd9f7d41d8466852b0d18ad766cd5c4334c91fef6e1b3a74eb5", - "https://deno.land/std@0.184.0/io/read_short.ts": "805cb329574b850b84bf14a92c052c59b5977a492cd780c41df8ad40826c1a20", - "https://deno.land/std@0.184.0/io/read_string_delim.ts": "5dc9f53bdf78e7d4ee1e56b9b60352238ab236a71c3e3b2a713c3d78472a53ce", - "https://deno.land/std@0.184.0/io/slice_long_to_bytes.ts": "48d9bace92684e880e46aa4a2520fc3867f9d7ce212055f76ecc11b22f9644b7", - "https://deno.land/std@0.184.0/io/string_reader.ts": "da0f68251b3d5b5112485dfd4d1b1936135c9b4d921182a7edaf47f74c25cc8f", - "https://deno.land/std@0.184.0/io/string_writer.ts": "8a03c5858c24965a54c6538bed15f32a7c72f5704a12bda56f83a40e28e5433e", - "https://deno.land/std@0.184.0/path/_constants.ts": "e49961f6f4f48039c0dfed3c3f93e963ca3d92791c9d478ac5b43183413136e0", - "https://deno.land/std@0.184.0/path/_interface.ts": "6471159dfbbc357e03882c2266d21ef9afdb1e4aa771b0545e90db58a0ba314b", - "https://deno.land/std@0.184.0/path/_util.ts": "d7abb1e0dea065f427b89156e28cdeb32b045870acdf865833ba808a73b576d0", - "https://deno.land/std@0.184.0/path/common.ts": "ee7505ab01fd22de3963b64e46cff31f40de34f9f8de1fff6a1bd2fe79380000", - "https://deno.land/std@0.184.0/path/glob.ts": "d479e0a695621c94d3fd7fe7abd4f9499caf32a8de13f25073451c6ef420a4e1", - "https://deno.land/std@0.184.0/path/mod.ts": "bf718f19a4fdd545aee1b06409ca0805bd1b68ecf876605ce632e932fe54510c", - "https://deno.land/std@0.184.0/path/posix.ts": "8b7c67ac338714b30c816079303d0285dd24af6b284f7ad63da5b27372a2c94d", - "https://deno.land/std@0.184.0/path/separator.ts": "0fb679739d0d1d7bf45b68dacfb4ec7563597a902edbaf3c59b50d5bcadd93b1", - "https://deno.land/std@0.184.0/path/win32.ts": "d186344e5583bcbf8b18af416d13d82b35a317116e6460a5a3953508c3de5bba", - "https://deno.land/std@0.184.0/types.d.ts": "dbaeb2c4d7c526db9828fc8df89d8aecf53b9ced72e0c4568f97ddd8cda616a4", - "https://deno.land/std@0.97.0/_util/assert.ts": "2f868145a042a11d5ad0a3c748dcf580add8a0dbc0e876eaa0026303a5488f58", - "https://deno.land/std@0.97.0/_util/os.ts": "e282950a0eaa96760c0cf11e7463e66babd15ec9157d4c9ed49cc0925686f6a7", - "https://deno.land/std@0.97.0/encoding/base64.ts": "eecae390f1f1d1cae6f6c6d732ede5276bf4b9cd29b1d281678c054dc5cc009e", - "https://deno.land/std@0.97.0/encoding/hex.ts": "f952e0727bddb3b2fd2e6889d104eacbd62e92091f540ebd6459317a61932d9b", - "https://deno.land/std@0.97.0/fs/_util.ts": "f2ce811350236ea8c28450ed822a5f42a0892316515b1cd61321dec13569c56b", - "https://deno.land/std@0.97.0/fs/ensure_dir.ts": "b7c103dc41a3d1dbbb522bf183c519c37065fdc234831a4a0f7d671b1ed5fea7", - "https://deno.land/std@0.97.0/fs/exists.ts": "b0d2e31654819cc2a8d37df45d6b14686c0cc1d802e9ff09e902a63e98b85a00", - "https://deno.land/std@0.97.0/hash/_wasm/hash.ts": "cb6ad1ab429f8ac9d6eae48f3286e08236d662e1a2e5cfd681ba1c0f17375895", - "https://deno.land/std@0.97.0/hash/_wasm/wasm.js": "94b1b997ae6fb4e6d2156bcea8f79cfcd1e512a91252b08800a92071e5e84e1a", - "https://deno.land/std@0.97.0/hash/hasher.ts": "57a9ec05dd48a9eceed319ac53463d9873490feea3832d58679df6eec51c176b", - "https://deno.land/std@0.97.0/hash/mod.ts": "5d032bd34186cda2f8d17fc122d621430953a6030d4b3f11172004715e3e2441", - "https://deno.land/std@0.97.0/path/_constants.ts": "1247fee4a79b70c89f23499691ef169b41b6ccf01887a0abd131009c5581b853", - "https://deno.land/std@0.97.0/path/_interface.ts": "1fa73b02aaa24867e481a48492b44f2598cd9dfa513c7b34001437007d3642e4", - "https://deno.land/std@0.97.0/path/_util.ts": "2e06a3b9e79beaf62687196bd4b60a4c391d862cfa007a20fc3a39f778ba073b", - "https://deno.land/std@0.97.0/path/common.ts": "eaf03d08b569e8a87e674e4e265e099f237472b6fd135b3cbeae5827035ea14a", - "https://deno.land/std@0.97.0/path/glob.ts": "314ad9ff263b895795208cdd4d5e35a44618ca3c6dd155e226fb15d065008652", - "https://deno.land/std@0.97.0/path/mod.ts": "4465dc494f271b02569edbb4a18d727063b5dbd6ed84283ff906260970a15d12", - "https://deno.land/std@0.97.0/path/posix.ts": "f56c3c99feb47f30a40ce9d252ef6f00296fa7c0fcb6dd81211bdb3b8b99ca3b", - "https://deno.land/std@0.97.0/path/separator.ts": "8fdcf289b1b76fd726a508f57d3370ca029ae6976fcde5044007f062e643ff1c", - "https://deno.land/std@0.97.0/path/win32.ts": "77f7b3604e0de40f3a7c698e8a79e7f601dc187035a1c21cb1e596666ce112f8", - "https://deno.land/x/cache@0.2.13/cache.ts": "4005aad54fb9aac9ff02526ffa798032e57f2d7966905fdeb7949263b1c95f2f", - "https://deno.land/x/cache@0.2.13/deps.ts": "6f14e76a1a09f329e3f3830c6e72bd10b53a89a75769d5ea886e5d8603e503e6", - "https://deno.land/x/cache@0.2.13/directories.ts": "ef48531cab3f827252e248596d15cede0de179a2fb15392ae24cf8034519994f", - "https://deno.land/x/cache@0.2.13/file.ts": "5abe7d80c6ac594c98e66eb4262962139f48cd9c49dbe2a77e9608760508a09a", - "https://deno.land/x/cache@0.2.13/file_fetcher.ts": "5c793cc83a5b9377679ec313b2a2321e51bf7ed15380fa82d387f1cdef3b924f", - "https://deno.land/x/cache@0.2.13/helpers.ts": "d1545d6432277b7a0b5ea254d1c51d572b6452a8eadd9faa7ad9c5586a1725c4", - "https://deno.land/x/cache@0.2.13/mod.ts": "3188250d3a013ef6c9eb060e5284cf729083af7944a29e60bb3d8597dd20ebcd", - "https://deno.land/x/cliffy@v0.25.7/_utils/distance.ts": "02af166952c7c358ac83beae397aa2fbca4ad630aecfcd38d92edb1ea429f004", - "https://deno.land/x/cliffy@v0.25.7/ansi/ansi.ts": "7f43d07d31dd7c24b721bb434c39cbb5132029fa4be3dd8938873065f65e5810", - "https://deno.land/x/cliffy@v0.25.7/ansi/ansi_escapes.ts": "885f61f343223f27b8ec69cc138a54bea30542924eacd0f290cd84edcf691387", - "https://deno.land/x/cliffy@v0.25.7/ansi/chain.ts": "31fb9fcbf72fed9f3eb9b9487270d2042ccd46a612d07dd5271b1a80ae2140a0", - "https://deno.land/x/cliffy@v0.25.7/ansi/colors.ts": "5f71993af5bd1aa0a795b15f41692d556d7c89584a601fed75997df844b832c9", - "https://deno.land/x/cliffy@v0.25.7/ansi/cursor_position.ts": "d537491e31d9c254b208277448eff92ff7f55978c4928dea363df92c0df0813f", - "https://deno.land/x/cliffy@v0.25.7/ansi/deps.ts": "0f35cb7e91868ce81561f6a77426ea8bc55dc15e13f84c7352f211023af79053", - "https://deno.land/x/cliffy@v0.25.7/ansi/mod.ts": "bb4e6588e6704949766205709463c8c33b30fec66c0b1846bc84a3db04a4e075", - "https://deno.land/x/cliffy@v0.25.7/ansi/tty.ts": "8fb064c17ead6cdf00c2d3bc87a9fd17b1167f2daa575c42b516f38bdb604673", - "https://deno.land/x/cliffy@v0.25.7/command/_errors.ts": "a9bd23dc816b32ec96c9b8f3057218241778d8c40333b43341138191450965e5", - "https://deno.land/x/cliffy@v0.25.7/command/_utils.ts": "9ab3d69fabab6c335b881b8a5229cbd5db0c68f630a1c307aff988b6396d9baf", - "https://deno.land/x/cliffy@v0.25.7/command/command.ts": "a2b83c612acd65c69116f70dec872f6da383699b83874b70fcf38cddf790443f", - "https://deno.land/x/cliffy@v0.25.7/command/completions/_bash_completions_generator.ts": "43b4abb543d4dc60233620d51e69d82d3b7c44e274e723681e0dce2a124f69f9", - "https://deno.land/x/cliffy@v0.25.7/command/completions/_fish_completions_generator.ts": "d0289985f5cf0bd288c05273bfa286b24c27feb40822eb7fd9d7fee64e6580e8", - "https://deno.land/x/cliffy@v0.25.7/command/completions/_zsh_completions_generator.ts": "14461eb274954fea4953ee75938821f721da7da607dc49bcc7db1e3f33a207bd", - "https://deno.land/x/cliffy@v0.25.7/command/completions/bash.ts": "053aa2006ec327ccecacb00ba28e5eb836300e5c1bec1b3cfaee9ddcf8189756", - "https://deno.land/x/cliffy@v0.25.7/command/completions/complete.ts": "58df61caa5e6220ff2768636a69337923ad9d4b8c1932aeb27165081c4d07d8b", - "https://deno.land/x/cliffy@v0.25.7/command/completions/fish.ts": "9938beaa6458c6cf9e2eeda46a09e8cd362d4f8c6c9efe87d3cd8ca7477402a5", - "https://deno.land/x/cliffy@v0.25.7/command/completions/mod.ts": "aeef7ec8e319bb157c39a4bab8030c9fe8fa327b4c1e94c9c1025077b45b40c0", - "https://deno.land/x/cliffy@v0.25.7/command/completions/zsh.ts": "8b04ab244a0b582f7927d405e17b38602428eeb347a9968a657e7ea9f40e721a", - "https://deno.land/x/cliffy@v0.25.7/command/deprecated.ts": "bbe6670f1d645b773d04b725b8b8e7814c862c9f1afba460c4d599ffe9d4983c", - "https://deno.land/x/cliffy@v0.25.7/command/deps.ts": "275b964ce173770bae65f6b8ebe9d2fd557dc10292cdd1ed3db1735f0d77fa1d", - "https://deno.land/x/cliffy@v0.25.7/command/help/_help_generator.ts": "f7c349cb2ddb737e70dc1f89bcb1943ca9017a53506be0d4138e0aadb9970a49", - "https://deno.land/x/cliffy@v0.25.7/command/help/mod.ts": "09d74d3eb42d21285407cda688074c29595d9c927b69aedf9d05ff3f215820d3", - "https://deno.land/x/cliffy@v0.25.7/command/mod.ts": "d0a32df6b14028e43bb2d41fa87d24bc00f9662a44e5a177b3db02f93e473209", - "https://deno.land/x/cliffy@v0.25.7/command/type.ts": "24e88e3085e1574662b856ccce70d589959648817135d4469fab67b9cce1b364", - "https://deno.land/x/cliffy@v0.25.7/command/types.ts": "ae02eec0ed7a769f7dba2dd5d3a931a61724b3021271b1b565cf189d9adfd4a0", - "https://deno.land/x/cliffy@v0.25.7/command/types/action_list.ts": "33c98d449617c7a563a535c9ceb3741bde9f6363353fd492f90a74570c611c27", - "https://deno.land/x/cliffy@v0.25.7/command/types/boolean.ts": "3879ec16092b4b5b1a0acb8675f8c9250c0b8a972e1e4c7adfba8335bd2263ed", - "https://deno.land/x/cliffy@v0.25.7/command/types/child_command.ts": "f1fca390c7fbfa7a713ca15ef55c2c7656bcbb394d50e8ef54085bdf6dc22559", - "https://deno.land/x/cliffy@v0.25.7/command/types/command.ts": "325d0382e383b725fd8d0ef34ebaeae082c5b76a1f6f2e843fee5dbb1a4fe3ac", - "https://deno.land/x/cliffy@v0.25.7/command/types/enum.ts": "2178345972adf7129a47e5f02856ca3e6852a91442a1c78307dffb8a6a3c6c9f", - "https://deno.land/x/cliffy@v0.25.7/command/types/file.ts": "8618f16ac9015c8589cbd946b3de1988cc4899b90ea251f3325c93c46745140e", - "https://deno.land/x/cliffy@v0.25.7/command/types/integer.ts": "29864725fd48738579d18123d7ee78fed37515e6dc62146c7544c98a82f1778d", - "https://deno.land/x/cliffy@v0.25.7/command/types/number.ts": "aeba96e6f470309317a16b308c82e0e4138a830ec79c9877e4622c682012bc1f", - "https://deno.land/x/cliffy@v0.25.7/command/types/string.ts": "e4dadb08a11795474871c7967beab954593813bb53d9f69ea5f9b734e43dc0e0", - "https://deno.land/x/cliffy@v0.25.7/command/upgrade/mod.ts": "17e2df3b620905583256684415e6c4a31e8de5c59066eb6d6c9c133919292dc4", - "https://deno.land/x/cliffy@v0.25.7/command/upgrade/provider.ts": "d6fb846043232cbd23c57d257100c7fc92274984d75a5fead0f3e4266dc76ab8", - "https://deno.land/x/cliffy@v0.25.7/command/upgrade/provider/deno_land.ts": "24f8d82e38c51e09be989f30f8ad21f9dd41ac1bb1973b443a13883e8ba06d6d", - "https://deno.land/x/cliffy@v0.25.7/command/upgrade/provider/github.ts": "99e1b133dd446c6aa79f69e69c46eb8bc1c968dd331c2a7d4064514a317c7b59", - "https://deno.land/x/cliffy@v0.25.7/command/upgrade/provider/nest_land.ts": "0e07936cea04fa41ac9297f32d87f39152ea873970c54cb5b4934b12fee1885e", - "https://deno.land/x/cliffy@v0.25.7/command/upgrade/upgrade_command.ts": "3640a287d914190241ea1e636774b1b4b0e1828fa75119971dd5304784061e05", - "https://deno.land/x/cliffy@v0.25.7/flags/_errors.ts": "f1fbb6bfa009e7950508c9d491cfb4a5551027d9f453389606adb3f2327d048f", - "https://deno.land/x/cliffy@v0.25.7/flags/_utils.ts": "340d3ecab43cde9489187e1f176504d2c58485df6652d1cdd907c0e9c3ce4cc2", - "https://deno.land/x/cliffy@v0.25.7/flags/_validate_flags.ts": "16eb5837986c6f6f7620817820161a78d66ce92d690e3697068726bbef067452", - "https://deno.land/x/cliffy@v0.25.7/flags/deprecated.ts": "a72a35de3cc7314e5ebea605ca23d08385b218ef171c32a3f135fb4318b08126", - "https://deno.land/x/cliffy@v0.25.7/flags/flags.ts": "68a9dfcacc4983a84c07ba19b66e5e9fccd04389fad215210c60fb414cc62576", - "https://deno.land/x/cliffy@v0.25.7/flags/mod.ts": "b21c2c135cd2437cc16245c5f168a626091631d6d4907ad10db61c96c93bdb25", - "https://deno.land/x/cliffy@v0.25.7/flags/types.ts": "7452ea5296758fb7af89930349ce40d8eb9a43b24b3f5759283e1cb5113075fd", - "https://deno.land/x/cliffy@v0.25.7/flags/types/boolean.ts": "4c026dd66ec9c5436860dc6d0241427bdb8d8e07337ad71b33c08193428a2236", - "https://deno.land/x/cliffy@v0.25.7/flags/types/integer.ts": "b60d4d590f309ddddf066782d43e4dc3799f0e7d08e5ede7dc62a5ee94b9a6d9", - "https://deno.land/x/cliffy@v0.25.7/flags/types/number.ts": "610936e2d29de7c8c304b65489a75ebae17b005c6122c24e791fbed12444d51e", - "https://deno.land/x/cliffy@v0.25.7/flags/types/string.ts": "e89b6a5ce322f65a894edecdc48b44956ec246a1d881f03e97bbda90dd8638c5", - "https://deno.land/x/cliffy@v0.25.7/keycode/key_code.ts": "c4ab0ffd102c2534962b765ded6d8d254631821bf568143d9352c1cdcf7a24be", - "https://deno.land/x/cliffy@v0.25.7/keycode/key_codes.ts": "917f0a2da0dbace08cf29bcfdaaa2257da9fe7e705fff8867d86ed69dfb08cfe", - "https://deno.land/x/cliffy@v0.25.7/keycode/mod.ts": "292d2f295316c6e0da6955042a7b31ab2968ff09f2300541d00f05ed6c2aa2d4", - "https://deno.land/x/cliffy@v0.25.7/mod.ts": "e3515ccf6bd4e4ac89322034e07e2332ed71901e4467ee5bc9d72851893e167b", - "https://deno.land/x/cliffy@v0.25.7/prompt/_generic_input.ts": "737cff2de02c8ce35250f5dd79c67b5fc176423191a2abd1f471a90dd725659e", - "https://deno.land/x/cliffy@v0.25.7/prompt/_generic_list.ts": "79b301bf09eb19f0d070d897f613f78d4e9f93100d7e9a26349ef0bfaa7408d2", - "https://deno.land/x/cliffy@v0.25.7/prompt/_generic_prompt.ts": "8630ce89a66d83e695922df41721cada52900b515385d86def597dea35971bb2", - "https://deno.land/x/cliffy@v0.25.7/prompt/_generic_suggestions.ts": "2a8b619f91e8f9a270811eff557f10f1343a444a527b5fc22c94de832939920c", - "https://deno.land/x/cliffy@v0.25.7/prompt/_utils.ts": "676cca30762656ed1a9bcb21a7254244278a23ffc591750e98a501644b6d2df3", - "https://deno.land/x/cliffy@v0.25.7/prompt/checkbox.ts": "e5a5a9adbb86835dffa2afbd23c6f7a8fe25a9d166485388ef25aba5dc3fbf9e", - "https://deno.land/x/cliffy@v0.25.7/prompt/confirm.ts": "94c8e55de3bbcd53732804420935c432eab29945497d1c47c357d236a89cb5f6", - "https://deno.land/x/cliffy@v0.25.7/prompt/deps.ts": "4c38ab18e55a792c9a136c1c29b2b6e21ea4820c45de7ef4cf517ce94012c57d", - "https://deno.land/x/cliffy@v0.25.7/prompt/figures.ts": "26af0fbfe21497220e4b887bb550fab997498cde14703b98e78faf370fbb4b94", - "https://deno.land/x/cliffy@v0.25.7/prompt/input.ts": "ee45532e0a30c2463e436e08ae291d79d1c2c40872e17364c96d2b97c279bf4d", - "https://deno.land/x/cliffy@v0.25.7/prompt/list.ts": "6780427ff2a932a48c9b882d173c64802081d6cdce9ff618d66ba6504b6abc50", - "https://deno.land/x/cliffy@v0.25.7/prompt/mod.ts": "195aed14d10d279914eaa28c696dec404d576ca424c097a5bc2b4a7a13b66c89", - "https://deno.land/x/cliffy@v0.25.7/prompt/number.ts": "015305a76b50138234dde4fd50eb886c6c7c0baa1b314caf811484644acdc2cf", - "https://deno.land/x/cliffy@v0.25.7/prompt/prompt.ts": "0e7f6a1d43475ee33fb25f7d50749b2f07fc0bcddd9579f3f9af12d05b4a4412", - "https://deno.land/x/cliffy@v0.25.7/prompt/secret.ts": "58745f5231fb2c44294c4acf2511f8c5bfddfa1e12f259580ff90dedea2703d6", - "https://deno.land/x/cliffy@v0.25.7/prompt/select.ts": "1e982eae85718e4e15a3ee10a5ae2233e532d7977d55888f3a309e8e3982b784", - "https://deno.land/x/cliffy@v0.25.7/prompt/toggle.ts": "842c3754a40732f2e80bcd4670098713e402e64bd930e6cab2b787f7ad4d931a", - "https://deno.land/x/cliffy@v0.25.7/table/border.ts": "2514abae4e4f51eda60a5f8c927ba24efd464a590027e900926b38f68e01253c", - "https://deno.land/x/cliffy@v0.25.7/table/cell.ts": "1d787d8006ac8302020d18ec39f8d7f1113612c20801b973e3839de9c3f8b7b3", - "https://deno.land/x/cliffy@v0.25.7/table/deps.ts": "5b05fa56c1a5e2af34f2103fd199e5f87f0507549963019563eae519271819d2", - "https://deno.land/x/cliffy@v0.25.7/table/layout.ts": "46bf10ae5430cf4fbb92f23d588230e9c6336edbdb154e5c9581290562b169f4", - "https://deno.land/x/cliffy@v0.25.7/table/mod.ts": "e74f69f38810ee6139a71132783765feb94436a6619c07474ada45b465189834", - "https://deno.land/x/cliffy@v0.25.7/table/row.ts": "5f519ba7488d2ef76cbbf50527f10f7957bfd668ce5b9169abbc44ec88302645", - "https://deno.land/x/cliffy@v0.25.7/table/table.ts": "ec204c9d08bb3ff1939c5ac7412a4c9ed7d00925d4fc92aff9bfe07bd269258d", - "https://deno.land/x/cliffy@v0.25.7/table/utils.ts": "187bb7dcbcfb16199a5d906113f584740901dfca1007400cba0df7dcd341bc29", - "https://deno.land/x/denoflate@1.2.1/mod.ts": "f5628e44b80b3d80ed525afa2ba0f12408e3849db817d47a883b801f9ce69dd6", - "https://deno.land/x/denoflate@1.2.1/pkg/denoflate.js": "b9f9ad9457d3f12f28b1fb35c555f57443427f74decb403113d67364e4f2caf4", - "https://deno.land/x/denoflate@1.2.1/pkg/denoflate_bg.wasm.js": "d581956245407a2115a3d7e8d85a9641c032940a8e810acbd59ca86afd34d44d", - "https://deno.land/x/denosass@1.0.6/mod.ts": "5e9c142055d658f3acb2b370d0b412c783ed4b27db830f387525fb7f69a7ab3d", - "https://deno.land/x/denosass@1.0.6/src/deps.ts": "cb5fa11799e3def8b593be3b5939d2755a2c7f1f4987f3af1bc4ad90922d3715", - "https://deno.land/x/denosass@1.0.6/src/mod.ts": "d2b63172f98238f77831995a5d6c8a06af5252ad8fbe7b9ec40b60eae86f2164", - "https://deno.land/x/denosass@1.0.6/src/types/module.types.ts": "7a5027482ded1d2967fbe690ef8f928446c5de8811c3333f9b09ae6e8122f9ba", - "https://deno.land/x/denosass@1.0.6/src/wasm/grass.deno.js": "a72432ce8d6b8f9c31e31c71415fdca03fe36aa22417e414bc81e2e21a8a687b", - "https://deno.land/x/esbuild@v0.17.18/mod.d.ts": "dc279a3a46f084484453e617c0cabcd5b8bd1920c0e562e4ea02dfc828c8f968", - "https://deno.land/x/esbuild@v0.17.18/mod.js": "84b5044def8a2e94770b79d295a1dd74f5ee453514c5b4f33571e22e1c882898", - "https://deno.land/x/esbuild_cache_plugin@v0.3.1/deps.ts": "379625c0bb9767b2229cbe3c316a249916f4611a9520fcf1f91069384b654923", - "https://deno.land/x/esbuild_cache_plugin@v0.3.1/mod.ts": "cbcfaca866ca4ba8c729633239c3da811ac4c799d8411aea04dcd2e4c323bdb3", - "https://raw.githubusercontent.com/Tsukina-7mochi/esbuild-plugin-copy-deno/v1.0.8/deps.ts": "168bf97c4e56db75a1075a4ef5549c796465588f98b6326e7661acfe98e58caa", - "https://raw.githubusercontent.com/Tsukina-7mochi/esbuild-plugin-copy-deno/v1.0.8/mod.ts": "48b2bcb4b6210da03d3dfe1be1a17a677089e3bd5be22d889e8a78891bf6ad68", - "https://raw.githubusercontent.com/Tsukina-7mochi/esbuild-plugin-result-deno/v1.0.9/deps.ts": "cce852a277e6ec80c9abf9cd02b3c9a895f02925c4100d44d226a8a0b8cb4db0", - "https://raw.githubusercontent.com/Tsukina-7mochi/esbuild-plugin-result-deno/v1.0.9/mod.ts": "fb11c4ee4102510bfc58811cd3567f11df746b009b0bacc7b1110f79409a38a3", - "https://raw.githubusercontent.com/Tsukina-7mochi/esbuild-plugin-sass-deno/v0.2.6/deps.ts": "c0397e0f91fbef8fd389829fa905af5d9ca53e14ae0d5da6cbdf2cd498ee05ab", - "https://raw.githubusercontent.com/Tsukina-7mochi/esbuild-plugin-sass-deno/v0.2.6/mod.ts": "a83c499a6801370d493d2572b059fd43b46921c30a2827b1122f13d9e6212c5d" + "version": "4", + "specifiers": { + "jsr:@luca/esbuild-deno-loader@0.11": "0.11.0", + "jsr:@std/bytes@^1.0.2": "1.0.2", + "jsr:@std/encoding@^1.0.5": "1.0.5", + "jsr:@std/fs@^1.0.4": "1.0.5", + "jsr:@std/jsonc@^1.0.1": "1.0.1", + "jsr:@std/path@^1.0.6": "1.0.7", + "jsr:@std/path@^1.0.7": "1.0.7", + "jsr:@std/streams@^1.0.7": "1.0.7", + "jsr:@tsukina-7mochi/esbuild-plugin-debug-switch@0.2": "0.2.0", + "jsr:@tsukina-7mochi/esbuild-plugin-sass@~0.1.1": "0.1.1", + "npm:esbuild@0.24": "0.24.0", + "npm:esbuild@0.24.0": "0.24.0", + "npm:preact@^10.24.3": "10.24.3", + "npm:sass@^1.80.3": "1.80.5", + "npm:webextension-polyfill@0.12": "0.12.0" + }, + "jsr": { + "@luca/esbuild-deno-loader@0.11.0": { + "integrity": "c05a989aa7c4ee6992a27be5f15cfc5be12834cab7ff84cabb47313737c51a2c", + "dependencies": [ + "jsr:@std/bytes", + "jsr:@std/encoding", + "jsr:@std/path@^1.0.6" + ] + }, + "@std/bytes@1.0.2": { + "integrity": "fbdee322bbd8c599a6af186a1603b3355e59a5fb1baa139f8f4c3c9a1b3e3d57" + }, + "@std/encoding@1.0.5": { + "integrity": "ecf363d4fc25bd85bd915ff6733a7e79b67e0e7806334af15f4645c569fefc04" + }, + "@std/fs@1.0.5": { + "integrity": "41806ad6823d0b5f275f9849a2640d87e4ef67c51ee1b8fb02426f55e02fd44e", + "dependencies": [ + "jsr:@std/path@^1.0.7" + ] + }, + "@std/jsonc@1.0.1": { + "integrity": "6b36956e2a7cbb08ca5ad7fbec72e661e6217c202f348496ea88747636710dda" + }, + "@std/path@1.0.7": { + "integrity": "76a689e07f0e15dcc6002ec39d0866797e7156629212b28f27179b8a5c3b33a1" + }, + "@std/streams@1.0.7": { + "integrity": "1a93917ca0c58c01b2bfb93647189229b1702677f169b6fb61ad6241cd2e499b", + "dependencies": [ + "jsr:@std/bytes" + ] + }, + "@tsukina-7mochi/esbuild-plugin-debug-switch@0.2.0": { + "integrity": "2d4dd6d548a48cda926d8fcb5e44420476ca994f057f493b3e53aff945a924ae", + "dependencies": [ + "npm:esbuild@0.24" + ] + }, + "@tsukina-7mochi/esbuild-plugin-sass@0.1.1": { + "integrity": "5bbc148b4bbc9ca5425be4d2c6e37257d0bf1dc9ce946f9e3be3bae7b445056e", + "dependencies": [ + "jsr:@std/path@^1.0.6", + "npm:sass" + ] + } }, "npm": { - "specifiers": { - "json5@2.2.3": "json5@2.2.3" - }, - "packages": { - "json5@2.2.3": { - "integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==", - "dependencies": {} - } + "@esbuild/aix-ppc64@0.24.0": { + "integrity": "sha512-WtKdFM7ls47zkKHFVzMz8opM7LkcsIp9amDUBIAWirg70RM71WRSjdILPsY5Uv1D42ZpUfaPILDlfactHgsRkw==" + }, + "@esbuild/android-arm64@0.24.0": { + "integrity": "sha512-Vsm497xFM7tTIPYK9bNTYJyF/lsP590Qc1WxJdlB6ljCbdZKU9SY8i7+Iin4kyhV/KV5J2rOKsBQbB77Ab7L/w==" + }, + "@esbuild/android-arm@0.24.0": { + "integrity": "sha512-arAtTPo76fJ/ICkXWetLCc9EwEHKaeya4vMrReVlEIUCAUncH7M4bhMQ+M9Vf+FFOZJdTNMXNBrWwW+OXWpSew==" + }, + "@esbuild/android-x64@0.24.0": { + "integrity": "sha512-t8GrvnFkiIY7pa7mMgJd7p8p8qqYIz1NYiAoKc75Zyv73L3DZW++oYMSHPRarcotTKuSs6m3hTOa5CKHaS02TQ==" + }, + "@esbuild/darwin-arm64@0.24.0": { + "integrity": "sha512-CKyDpRbK1hXwv79soeTJNHb5EiG6ct3efd/FTPdzOWdbZZfGhpbcqIpiD0+vwmpu0wTIL97ZRPZu8vUt46nBSw==" + }, + "@esbuild/darwin-x64@0.24.0": { + "integrity": "sha512-rgtz6flkVkh58od4PwTRqxbKH9cOjaXCMZgWD905JOzjFKW+7EiUObfd/Kav+A6Gyud6WZk9w+xu6QLytdi2OA==" + }, + "@esbuild/freebsd-arm64@0.24.0": { + "integrity": "sha512-6Mtdq5nHggwfDNLAHkPlyLBpE5L6hwsuXZX8XNmHno9JuL2+bg2BX5tRkwjyfn6sKbxZTq68suOjgWqCicvPXA==" + }, + "@esbuild/freebsd-x64@0.24.0": { + "integrity": "sha512-D3H+xh3/zphoX8ck4S2RxKR6gHlHDXXzOf6f/9dbFt/NRBDIE33+cVa49Kil4WUjxMGW0ZIYBYtaGCa2+OsQwQ==" + }, + "@esbuild/linux-arm64@0.24.0": { + "integrity": "sha512-TDijPXTOeE3eaMkRYpcy3LarIg13dS9wWHRdwYRnzlwlA370rNdZqbcp0WTyyV/k2zSxfko52+C7jU5F9Tfj1g==" + }, + "@esbuild/linux-arm@0.24.0": { + "integrity": "sha512-gJKIi2IjRo5G6Glxb8d3DzYXlxdEj2NlkixPsqePSZMhLudqPhtZ4BUrpIuTjJYXxvF9njql+vRjB2oaC9XpBw==" + }, + "@esbuild/linux-ia32@0.24.0": { + "integrity": "sha512-K40ip1LAcA0byL05TbCQ4yJ4swvnbzHscRmUilrmP9Am7//0UjPreh4lpYzvThT2Quw66MhjG//20mrufm40mA==" + }, + "@esbuild/linux-loong64@0.24.0": { + "integrity": "sha512-0mswrYP/9ai+CU0BzBfPMZ8RVm3RGAN/lmOMgW4aFUSOQBjA31UP8Mr6DDhWSuMwj7jaWOT0p0WoZ6jeHhrD7g==" + }, + "@esbuild/linux-mips64el@0.24.0": { + "integrity": "sha512-hIKvXm0/3w/5+RDtCJeXqMZGkI2s4oMUGj3/jM0QzhgIASWrGO5/RlzAzm5nNh/awHE0A19h/CvHQe6FaBNrRA==" + }, + "@esbuild/linux-ppc64@0.24.0": { + "integrity": "sha512-HcZh5BNq0aC52UoocJxaKORfFODWXZxtBaaZNuN3PUX3MoDsChsZqopzi5UupRhPHSEHotoiptqikjN/B77mYQ==" + }, + "@esbuild/linux-riscv64@0.24.0": { + "integrity": "sha512-bEh7dMn/h3QxeR2KTy1DUszQjUrIHPZKyO6aN1X4BCnhfYhuQqedHaa5MxSQA/06j3GpiIlFGSsy1c7Gf9padw==" + }, + "@esbuild/linux-s390x@0.24.0": { + "integrity": "sha512-ZcQ6+qRkw1UcZGPyrCiHHkmBaj9SiCD8Oqd556HldP+QlpUIe2Wgn3ehQGVoPOvZvtHm8HPx+bH20c9pvbkX3g==" + }, + "@esbuild/linux-x64@0.24.0": { + "integrity": "sha512-vbutsFqQ+foy3wSSbmjBXXIJ6PL3scghJoM8zCL142cGaZKAdCZHyf+Bpu/MmX9zT9Q0zFBVKb36Ma5Fzfa8xA==" + }, + "@esbuild/netbsd-x64@0.24.0": { + "integrity": "sha512-hjQ0R/ulkO8fCYFsG0FZoH+pWgTTDreqpqY7UnQntnaKv95uP5iW3+dChxnx7C3trQQU40S+OgWhUVwCjVFLvg==" + }, + "@esbuild/openbsd-arm64@0.24.0": { + "integrity": "sha512-MD9uzzkPQbYehwcN583yx3Tu5M8EIoTD+tUgKF982WYL9Pf5rKy9ltgD0eUgs8pvKnmizxjXZyLt0z6DC3rRXg==" + }, + "@esbuild/openbsd-x64@0.24.0": { + "integrity": "sha512-4ir0aY1NGUhIC1hdoCzr1+5b43mw99uNwVzhIq1OY3QcEwPDO3B7WNXBzaKY5Nsf1+N11i1eOfFcq+D/gOS15Q==" + }, + "@esbuild/sunos-x64@0.24.0": { + "integrity": "sha512-jVzdzsbM5xrotH+W5f1s+JtUy1UWgjU0Cf4wMvffTB8m6wP5/kx0KiaLHlbJO+dMgtxKV8RQ/JvtlFcdZ1zCPA==" + }, + "@esbuild/win32-arm64@0.24.0": { + "integrity": "sha512-iKc8GAslzRpBytO2/aN3d2yb2z8XTVfNV0PjGlCxKo5SgWmNXx82I/Q3aG1tFfS+A2igVCY97TJ8tnYwpUWLCA==" + }, + "@esbuild/win32-ia32@0.24.0": { + "integrity": "sha512-vQW36KZolfIudCcTnaTpmLQ24Ha1RjygBo39/aLkM2kmjkWmZGEJ5Gn9l5/7tzXA42QGIoWbICfg6KLLkIw6yw==" + }, + "@esbuild/win32-x64@0.24.0": { + "integrity": "sha512-7IAFPrjSQIJrGsK6flwg7NFmwBoSTyF3rl7If0hNUFQU4ilTsEPL6GuMuU9BfIWVVGuRnuIidkSMC+c0Otu8IA==" + }, + "@parcel/watcher-android-arm64@2.4.1": { + "integrity": "sha512-LOi/WTbbh3aTn2RYddrO8pnapixAziFl6SMxHM69r3tvdSm94JtCenaKgk1GRg5FJ5wpMCpHeW+7yqPlvZv7kg==" + }, + "@parcel/watcher-darwin-arm64@2.4.1": { + "integrity": "sha512-ln41eihm5YXIY043vBrrHfn94SIBlqOWmoROhsMVTSXGh0QahKGy77tfEywQ7v3NywyxBBkGIfrWRHm0hsKtzA==" + }, + "@parcel/watcher-darwin-x64@2.4.1": { + "integrity": "sha512-yrw81BRLjjtHyDu7J61oPuSoeYWR3lDElcPGJyOvIXmor6DEo7/G2u1o7I38cwlcoBHQFULqF6nesIX3tsEXMg==" + }, + "@parcel/watcher-freebsd-x64@2.4.1": { + "integrity": "sha512-TJa3Pex/gX3CWIx/Co8k+ykNdDCLx+TuZj3f3h7eOjgpdKM+Mnix37RYsYU4LHhiYJz3DK5nFCCra81p6g050w==" + }, + "@parcel/watcher-linux-arm-glibc@2.4.1": { + "integrity": "sha512-4rVYDlsMEYfa537BRXxJ5UF4ddNwnr2/1O4MHM5PjI9cvV2qymvhwZSFgXqbS8YoTk5i/JR0L0JDs69BUn45YA==" + }, + "@parcel/watcher-linux-arm64-glibc@2.4.1": { + "integrity": "sha512-BJ7mH985OADVLpbrzCLgrJ3TOpiZggE9FMblfO65PlOCdG++xJpKUJ0Aol74ZUIYfb8WsRlUdgrZxKkz3zXWYA==" + }, + "@parcel/watcher-linux-arm64-musl@2.4.1": { + "integrity": "sha512-p4Xb7JGq3MLgAfYhslU2SjoV9G0kI0Xry0kuxeG/41UfpjHGOhv7UoUDAz/jb1u2elbhazy4rRBL8PegPJFBhA==" + }, + "@parcel/watcher-linux-x64-glibc@2.4.1": { + "integrity": "sha512-s9O3fByZ/2pyYDPoLM6zt92yu6P4E39a03zvO0qCHOTjxmt3GHRMLuRZEWhWLASTMSrrnVNWdVI/+pUElJBBBg==" + }, + "@parcel/watcher-linux-x64-musl@2.4.1": { + "integrity": "sha512-L2nZTYR1myLNST0O632g0Dx9LyMNHrn6TOt76sYxWLdff3cB22/GZX2UPtJnaqQPdCRoszoY5rcOj4oMTtp5fQ==" + }, + "@parcel/watcher-win32-arm64@2.4.1": { + "integrity": "sha512-Uq2BPp5GWhrq/lcuItCHoqxjULU1QYEcyjSO5jqqOK8RNFDBQnenMMx4gAl3v8GiWa59E9+uDM7yZ6LxwUIfRg==" + }, + "@parcel/watcher-win32-ia32@2.4.1": { + "integrity": "sha512-maNRit5QQV2kgHFSYwftmPBxiuK5u4DXjbXx7q6eKjq5dsLXZ4FJiVvlcw35QXzk0KrUecJmuVFbj4uV9oYrcw==" + }, + "@parcel/watcher-win32-x64@2.4.1": { + "integrity": "sha512-+DvS92F9ezicfswqrvIRM2njcYJbd5mb9CUgtrHCHmvn7pPPa+nMDRu1o1bYYz/l5IB2NVGNJWiH7h1E58IF2A==" + }, + "@parcel/watcher@2.4.1": { + "integrity": "sha512-HNjmfLQEVRZmHRET336f20H/8kOozUGwk7yajvsonjNxbj2wBTK1WsQuHkD5yYh9RxFGL2EyDHryOihOwUoKDA==", + "dependencies": [ + "@parcel/watcher-android-arm64", + "@parcel/watcher-darwin-arm64", + "@parcel/watcher-darwin-x64", + "@parcel/watcher-freebsd-x64", + "@parcel/watcher-linux-arm-glibc", + "@parcel/watcher-linux-arm64-glibc", + "@parcel/watcher-linux-arm64-musl", + "@parcel/watcher-linux-x64-glibc", + "@parcel/watcher-linux-x64-musl", + "@parcel/watcher-win32-arm64", + "@parcel/watcher-win32-ia32", + "@parcel/watcher-win32-x64", + "detect-libc", + "is-glob", + "micromatch", + "node-addon-api" + ] + }, + "braces@3.0.3": { + "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==", + "dependencies": [ + "fill-range" + ] + }, + "chokidar@4.0.1": { + "integrity": "sha512-n8enUVCED/KVRQlab1hr3MVpcVMvxtZjmEa956u+4YijlmQED223XMSYj2tLuKvr4jcCTzNNMpQDUer72MMmzA==", + "dependencies": [ + "readdirp" + ] + }, + "detect-libc@1.0.3": { + "integrity": "sha512-pGjwhsmsp4kL2RTz08wcOlGN83otlqHeD/Z5T8GXZB+/YcpQ/dgo+lbU8ZsGxV0HIvqqxo9l7mqYwyYMD9bKDg==" + }, + "esbuild@0.24.0": { + "integrity": "sha512-FuLPevChGDshgSicjisSooU0cemp/sGXR841D5LHMB7mTVOmsEHcAxaH3irL53+8YDIeVNQEySh4DaYU/iuPqQ==", + "dependencies": [ + "@esbuild/aix-ppc64", + "@esbuild/android-arm", + "@esbuild/android-arm64", + "@esbuild/android-x64", + "@esbuild/darwin-arm64", + "@esbuild/darwin-x64", + "@esbuild/freebsd-arm64", + "@esbuild/freebsd-x64", + "@esbuild/linux-arm", + "@esbuild/linux-arm64", + "@esbuild/linux-ia32", + "@esbuild/linux-loong64", + "@esbuild/linux-mips64el", + "@esbuild/linux-ppc64", + "@esbuild/linux-riscv64", + "@esbuild/linux-s390x", + "@esbuild/linux-x64", + "@esbuild/netbsd-x64", + "@esbuild/openbsd-arm64", + "@esbuild/openbsd-x64", + "@esbuild/sunos-x64", + "@esbuild/win32-arm64", + "@esbuild/win32-ia32", + "@esbuild/win32-x64" + ] + }, + "fill-range@7.1.1": { + "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==", + "dependencies": [ + "to-regex-range" + ] + }, + "immutable@4.3.7": { + "integrity": "sha512-1hqclzwYwjRDFLjcFxOM5AYkkG0rpFPpr1RLPMEuGczoS7YA8gLhy8SWXYRAA/XwfEHpfo3cw5JGioS32fnMRw==" + }, + "is-extglob@2.1.1": { + "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==" + }, + "is-glob@4.0.3": { + "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", + "dependencies": [ + "is-extglob" + ] + }, + "is-number@7.0.0": { + "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==" + }, + "micromatch@4.0.8": { + "integrity": "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==", + "dependencies": [ + "braces", + "picomatch" + ] + }, + "node-addon-api@7.1.1": { + "integrity": "sha512-5m3bsyrjFWE1xf7nz7YXdN4udnVtXK6/Yfgn5qnahL6bCkf2yKt4k3nuTKAtT4r3IG8JNR2ncsIMdZuAzJjHQQ==" + }, + "picomatch@2.3.1": { + "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==" + }, + "preact@10.24.3": { + "integrity": "sha512-Z2dPnBnMUfyQfSQ+GBdsGa16hz35YmLmtTLhM169uW944hYL6xzTYkJjC07j+Wosz733pMWx0fgON3JNw1jJQA==" + }, + "readdirp@4.0.2": { + "integrity": "sha512-yDMz9g+VaZkqBYS/ozoBJwaBhTbZo3UNYQHNRw1D3UFQB8oHB4uS/tAODO+ZLjGWmUbKnIlOWO+aaIiAxrUWHA==" + }, + "sass@1.80.5": { + "integrity": "sha512-TQd2aoQl/+zsxRMEDSxVdpPIqeq9UFc6pr7PzkugiTx3VYCFPUaa3P4RrBQsqok4PO200Vkz0vXQBNlg7W907g==", + "dependencies": [ + "@parcel/watcher", + "chokidar", + "immutable", + "source-map-js" + ] + }, + "source-map-js@1.2.1": { + "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==" + }, + "to-regex-range@5.0.1": { + "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", + "dependencies": [ + "is-number" + ] + }, + "webextension-polyfill@0.12.0": { + "integrity": "sha512-97TBmpoWJEE+3nFBQ4VocyCdLKfw54rFaJ6EVQYLBCXqCIpLSZkwGgASpv4oPt9gdKCJ80RJlcmNzNn008Ag6Q==" } + }, + "workspace": { + "dependencies": [ + "jsr:@luca/esbuild-deno-loader@0.11", + "jsr:@std/fs@^1.0.4", + "jsr:@std/jsonc@^1.0.1", + "jsr:@std/path@^1.0.6", + "jsr:@std/streams@^1.0.7", + "jsr:@tsukina-7mochi/esbuild-plugin-debug-switch@0.2", + "jsr:@tsukina-7mochi/esbuild-plugin-sass@~0.1.1", + "npm:@types/webextension-polyfill@~0.12.1", + "npm:esbuild@0.24.0", + "npm:preact@^10.24.3", + "npm:webextension-polyfill@0.12" + ] } } diff --git a/esbuild.common.ts b/esbuild.common.ts deleted file mode 100644 index 88229ae..0000000 --- a/esbuild.common.ts +++ /dev/null @@ -1,83 +0,0 @@ -import * as esbuild from 'esbuild'; -import { posix } from 'posix'; -import JSON5 from 'json5'; -import type ManifestType from './src/manifestType.ts'; -import denoConfig from './deno.json' assert { type: 'json' }; -import importmap from './import_map.json' assert { type: 'json' }; - -import sassPlugin from 'esbuild-plugin-sass'; -import { esbuildCachePlugin } from 'esbuild-cache-plugin'; -import copyPlugin from 'esbuild-plugin-copy'; -import resultPlugin from 'esbuild-plugin-result'; -import objectExportJSONPlugin from './plugins/objectExportJSON.ts'; - -const srcPath = 'src'; -const destPath = 'dist'; -const cachePath = 'cache'; - -const manifest = JSON5.parse( - Deno.readTextFileSync(posix.resolve(srcPath, 'manifest.json5')) -) as ManifestType; - -const contentScripts = Array.from(new Set( - manifest['content_scripts'] - .flatMap((entry) => entry['js'] ?? []) - .map((path) => posix.resolve(srcPath, path.replace(/\.js$/, '.ts'))) -)); - -const contentStyleSheets = Array.from(new Set( - manifest['content_scripts'] - .flatMap((entry) => entry['css'] ?? []) - .map((path) => posix.resolve(srcPath, path.replace(/\.css$/, '.scss'))) -)); - -const optionsResources = [ - manifest['options_ui']['page'], - ...(manifest['options_ui']['js'] ?? []).map((path) => path.replace(/\.js$/, '.ts')), - ...(manifest['options_ui']['css'] ?? []).map((path) => path.replace(/\.css$/, '.scss')), -].map((path) => posix.resolve(srcPath, path)); - -// Reflect.deleteProperty と違って Typescript の型チェックが効く -delete manifest.options_ui.js; -delete manifest.options_ui.css; - -const config: Partial = { - entryPoints: [ - ...contentScripts, - ...contentStyleSheets, - ...optionsResources, - ], - bundle: true, - outdir: destPath, - platform: 'browser', - loader: { - '.html': 'copy', - }, - jsxFactory: denoConfig.compilerOptions.jsxFactory, - jsxFragment: denoConfig.compilerOptions.jsxFragmentFactory, - plugins: [ - esbuildCachePlugin({ - directory: cachePath, - importmap, - }), - objectExportJSONPlugin({ - targets: [ - { value: manifest, filename: posix.resolve(destPath, 'manifest.json') }, - ], - }), - sassPlugin(), - copyPlugin({ - baseDir: srcPath, - baseOutDir: destPath, - files: [ - { from: 'imgs/*', to: 'imgs/[name][ext]' }, - ] - }), - resultPlugin(), - ], - logOverride: { - 'unsupported-jsx-comment': 'silent', - }, -} - -export default config; \ No newline at end of file diff --git a/esbuild.dev.ts b/esbuild.dev.ts deleted file mode 100644 index 42e02f8..0000000 --- a/esbuild.dev.ts +++ /dev/null @@ -1,9 +0,0 @@ -import * as esbuild from 'esbuild'; -import commonConfig from './esbuild.common.ts'; - -const config: esbuild.BuildOptions = { - ...commonConfig, - sourcemap: 'inline', -} - -export default config; diff --git a/esbuild.prod.ts b/esbuild.prod.ts deleted file mode 100644 index fccb751..0000000 --- a/esbuild.prod.ts +++ /dev/null @@ -1,10 +0,0 @@ -import * as esbuild from 'esbuild'; -import commonConfig from './esbuild.common.ts'; - -const config: esbuild.BuildOptions = { - ...commonConfig, - minify: true, - sourcemap: 'linked', -} - -export default config; diff --git a/how_to_build.md b/how_to_build.md index 4c0c05f..a8c24a5 100644 --- a/how_to_build.md +++ b/how_to_build.md @@ -16,4 +16,4 @@ $deno task build ``` - `dist` ディレクトリにビルド結果が出力されます + `dist` ディレクトリにビルド結果が出力されます diff --git a/import_map.json b/import_map.json deleted file mode 100644 index 57b185b..0000000 --- a/import_map.json +++ /dev/null @@ -1,21 +0,0 @@ -{ - "imports": { - "cliffy": "https://deno.land/x/cliffy@v0.25.7/mod.ts", - "esbuild": "https://deno.land/x/esbuild@v0.17.18/mod.js", - "esbuild-cache-plugin": "https://deno.land/x/esbuild_cache_plugin@v0.3.1/mod.ts", - "esbuild-plugin-copy": "https://raw.githubusercontent.com/Tsukina-7mochi/esbuild-plugin-copy-deno/v1.0.8/mod.ts", - "esbuild-plugin-result": "https://raw.githubusercontent.com/Tsukina-7mochi/esbuild-plugin-result-deno/v1.0.9/mod.ts", - "esbuild-plugin-sass": "https://raw.githubusercontent.com/Tsukina-7mochi/esbuild-plugin-sass-deno/v0.2.6/mod.ts", - "json5": "npm:json5@2.2.3", - "lodash": "https://deno.land/x/lodash@4.17.19/lodash.js", - "posix": "https://deno.land/std@0.184.0/path/mod.ts", - "preact": "https://esm.sh/preact@10.13.2", - "preact/compat": "https://esm.sh/preact@10.13.2/compat", - "preact/hooks": "https://esm.sh/preact@10.13.2/hooks", - "preact/jsx-runtime": "https://esm.sh/preact@10.13.2/jsx-runtime", - "preact/jsx-dev-runtime": "https://esm.sh/preact@10.13.2/jsx-dev-runtime", - "preact/types": "https://raw.githubusercontent.com/preactjs/preact/10.13.2/src/index.d.ts", - "std/": "https://deno.land/std@0.184.0/", - "webextension-polyfill": "https://esm.sh/webextension-polyfill@0.10.0" - } -} \ No newline at end of file diff --git a/plugins/json5.ts b/plugins/json5.ts deleted file mode 100644 index c22bb01..0000000 --- a/plugins/json5.ts +++ /dev/null @@ -1,19 +0,0 @@ -import * as esbuild from 'esbuild'; -import JSON5 from 'json5'; - -const json5Plugin = (loader?: esbuild.Loader): esbuild.Plugin => ({ - name: 'json5-plugin', - setup(build) { - build.onLoad({ filter: /\.json5$/ }, async (args) => { - const json5Content = await Deno.readTextFile(args.path); - const jsonContent = JSON.stringify(JSON5.parse(json5Content)); - - return { - contents: jsonContent, - loader: loader ?? 'json', - }; - }); - } -}); - -export default json5Plugin; diff --git a/plugins/json5Export.ts b/plugins/json5Export.ts deleted file mode 100644 index dd9e838..0000000 --- a/plugins/json5Export.ts +++ /dev/null @@ -1,35 +0,0 @@ -import * as esbuild from 'esbuild'; -import JSON5 from 'json5'; - -interface Options { - filePatterns: RegExp[], -} -const namespace = 'json5-export'; - -const json5ExportPlugin = (options?: Options): esbuild.Plugin => ({ - name: 'json5-export', - setup(build) { - const patterns = options?.filePatterns ?? []; - for(const pattern of patterns) { - build.onResolve({ filter: pattern }, (args) => ({ - path: args.path.replace(/\.json5$/, '.json'), - namespace, - pluginData: { - originalPath: args.path - } - })); - } - - build.onLoad({ filter: /.*/, namespace }, async (args) => { - const json5Content = await Deno.readTextFile(args.pluginData.originalPath); - const jsonContent = JSON.stringify(JSON5.parse(json5Content)); - - return { - contents: jsonContent, - loader: 'copy', - }; - }); - } -}); - -export default json5ExportPlugin; diff --git a/plugins/objectExportJSON.ts b/plugins/objectExportJSON.ts deleted file mode 100644 index 3965e44..0000000 --- a/plugins/objectExportJSON.ts +++ /dev/null @@ -1,27 +0,0 @@ -import * as esbuild from 'esbuild'; -import * as posix from 'posix'; -import * as fs from 'std/fs/mod.ts'; - -interface Option { - targets: { value: any, filename: string }[] -} - -const objectExportJSONPlugin = (option: Option): esbuild.Plugin => ({ - name: 'object-export-json-plugin', - setup(build) { - build.onStart(async () => { - const writePromises: Promise[] = []; - - for(const target of option.targets) { - const content = JSON.stringify(target.value); - - writePromises.push(fs.ensureDir(posix.dirname(target.filename)) - .then(() => Deno.writeTextFile(target.filename, content))); - } - - await Promise.all(writePromises); - }); - } -}); - -export default objectExportJSONPlugin; diff --git a/privacy_policies.md b/privacy_policies.md new file mode 100644 index 0000000..fa1f8b4 --- /dev/null +++ b/privacy_policies.md @@ -0,0 +1,93 @@ +# Privacy policy for Chrome extensions + +## Chrome 拡張のプライバシーポリシー + +本プライバシーポリシーは、nitechCreate(以下、「当開発者」)が開発した Google +Chrome +の拡張機能(Extension)(以下、「拡張機能」とします。)の利用において、利用者の個人情報もしくはそれに準ずる情報を取り扱う際に、当開発者が遵守する方針を示したものです。 + +### 基本方針 + +当開発者は、個人情報の重要性を認識し、個人情報を保護することが社会的責務であると考え、個人情報に関する法令を遵守し、拡張機能で取扱う個人情報の取得、利用、管理を適正に行います。 + +### 適用範囲 + +本プライバシーポリシーは、当開発者が開発した拡張機能においてのみ適用されます。 + +### 個人情報の取得と利用目的 + +当開発者は、個人情報を収集する機能を要した拡張機能を公開しません。 +**ただし、一部個人情報を利用者のブラウザの LocalStorage に保存します。** + +#### 取得方法 + +特定の Web サイトへアクセスした際に授業などの情報を取得する。 + +#### 利用目的 + +##### 利便性の向上 + +拡張機能では、機能の内容により、下記の情報を **「ブラウザの +LocalStorage」へ保存します**。 + +- 拡張機能で用いる授業等の情報 + +これにより、拡張機能の次回利用時などに授業等の情報が保存されているため、欠落があっても表示されて利便性が向上します。 +**個人情報は当開発者に送信されず拡張機能利用者のブラウザの LocalStorage +に保存されます**。 + +##### 保存期間について + +拡張機能内でデータの扱いとして、LocalStorage を使用します。 LocalStorage +には保存期間が存在しないためデータの保存期間は拡張機能のアンインストール時までとします。 + +#### 個人情報の取り扱いの同意について + +当開発者が開発を行った拡張機能では、拡張機能のインストールを行う前に、当プライバシーポリシーをご一読頂くようにお願いします。 +インストールをされた時点で、当プライバシーポリシーに同意されたとみなします。 + +#### Cookie による個人情報の取得 + +拡張機能では、Cookie を利用することがあります。 +Cookie(クッキー)とは、ウェブサイトを利用したときに、ブラウザとサーバーとの間で送受信した利用履歴や入力内容などを、訪問者のコンピュータにファイルとして保存しておく仕組みです。 + +##### 利用目的について + +拡張機能の利用者の利便性を向上するために活用します。 +ログイン処理や画面遷移を拡張機能から行う際に、Cookie +を活用することでユーザーの手間を省いてデータの処理が可能となります。 +なお、拡張機能ではプライバシー保護のため、拡張機能の目的とする情報以外の Cookie +を送信しません。 + +##### 保存期間について + +Cookie +の保存期間は利用者のブラウザーにて設定されているデフォルトの期間保存されます。 + +### 個人情報の管理 + +当開発者は、拡張機能内における個人情報の管理について、以下を徹底します。 + +#### 情報の正確性の確保 + +利用者が入力したデータにおいて、常に正しい情報を保持します。 + +#### 安全管理措置 + +拡張機能において、情報の漏洩、滅失を防止するために拡張機能内において利用目的外のサーバーへの情報送信を行いません。 + +#### 個人情報の第三者への提供について + +当開発者の開発する拡張機能は、利用者から提供いただいた個人情報を、訪問者本人の同意を得ることなく第三者に提供することはありません。 +また、今後第三者提供を行うことになった場合には、提供する情報と提供目的などを提示し、訪問者から同意を得た場合のみ第三者提供を行います。 + +#### 問い合わせ先 + +拡張機能、又は個人情報の取扱いに関しては、下記の連絡先よりお問い合わせください。 + +- X アカウント(DM やリプライ): https://x.com/nitechCreate +- お問い合わせフォーム: https://forms.gle/obR3yYBi3q5jH3KW8 + +## 策定日 + +2024 年 10 月 22 日 策定 diff --git a/readme.dev.md b/readme.dev.md index 47bb6ab..3bc9a54 100644 --- a/readme.dev.md +++ b/readme.dev.md @@ -7,25 +7,31 @@ - すべての利用者が一斉にリクエストを行うような機能を避ける - ... - すでに存在する DOM を削除して挿入しない - - Moodle の機能によって追加されているイベントリスナの動作が怪しくなる可能性がある + - Moodle + の機能によって追加されているイベントリスナの動作が怪しくなる可能性がある - Moodle の DOM 構造、スタイルを再利用する - - 例えばダッシュボードにブロックを追加するならすでに存在するブロックと同様の構造・クラス・属性の DOM を挿入する + - 例えばダッシュボードにブロックを追加するならすでに存在するブロックと同様の構造・クラス・属性の + DOM を挿入する - ドロップダウンリストなどロジックを自前で実装しなくて済むことがあります - デザインの統一は重要です ## ワークフローについて -`main` ブランチから派生したブランチで作業し、 `main` ブランチに Pull request を作成するという前提で作成しています。 +`main` ブランチから派生したブランチで作業し、 `main` ブランチに Pull request +を作成するという前提で作成しています。 - attach build artifact to release (`release-attach-artifact.yml`) - Release 時にビルドを実行し、ビルド成果物を Assets に追加する -- attach build artifact to release manually (`release-attach-artifact-manual.yml`) +- attach build artifact to release manually + (`release-attach-artifact-manual.yml`) - 上記と同様のワークフローを手動で実行できるようにしたもの - check latest version (`check-version-increase.yml`) - `main` ブランチへの PR を作成した際にバージョンが増えていることを確認する - - バージョンは [Semantic Versioning 2.0](https://semver.org/lang/ja/) に準拠しています + - バージョンは [Semantic Versioning 2.0](https://semver.org/lang/ja/) + に準拠しています - create- tag when PR is merged (`create-tag-on-pr-merge.yml`) - - `main` ブランチへの PR がマージされたときに `manifest.json5` に書かれたバージョンのタグを作成する + - `main` ブランチへの PR がマージされたときに `manifest.json5` + に書かれたバージョンのタグを作成する - lint, check format and run build as test (`lint-fmt-test.yml`) - push の際に linter, formatter によるチェックとビルドが成功するかを確認する @@ -45,11 +51,14 @@ ### VSCode での開発環境のセットアップ 1. [ビルド方法](how_to_build.md) に従って環境を構築する -2. [Deno 用拡張機能](https://marketplace.visualstudio.com/items?itemName=denoland.vscode-deno) をインストールする +2. [Deno 用拡張機能](https://marketplace.visualstudio.com/items?itemName=denoland.vscode-deno) + をインストールする 3. 次のコマンドを実行する: - ```sh - $deno task setup-vscode - ``` + +```sh +$deno task setup-vscode +``` + 4. VSCodeをリロードまたは再起動する ### ビルドなど @@ -66,13 +75,15 @@ $deno task build ``` -- 開発用ビルド (import map が埋め込まれます; `web_accessible_resources` に指定すれば本番用とほぼ変わりません) +- 開発用ビルド (import map が埋め込まれます; `web_accessible_resources` + に指定すれば本番用とほぼ変わりません) ```sh $deno task dev ``` -- 開発用ビルド (watch; ファイル変更時に自動で再ビルドします; 一部プラグインによる出力は watch されません; Enter で手動再ビルド) +- 開発用ビルド (watch; ファイル変更時に自動で再ビルドします; + 一部プラグインによる出力は watch されません; Enter で手動再ビルド) ```sh $deno task watch @@ -92,7 +103,8 @@ ### ビルド設定について -- `manifest.json5` からビルドするファイルの情報を生成しているため、拡張機能で読み込むファイルを指定すれば勝手にビルドしてくれます +- `manifest.json5` + からビルドするファイルの情報を生成しているため、拡張機能で読み込むファイルを指定すれば勝手にビルドしてくれます - 基本的に esbuild の設定はほとんどしなくて良いはずです ## ディレクトリ・ファイル構成 @@ -109,7 +121,8 @@ - `common`: すべてのページで共通のリソース - ... (各ページごとのリソース) - `popup`: ポップアップ (アイコンのクリック) 環境 - - `manifest.json5`: 拡張機能の設定ファイル (`manifest.json` として出力されます) + - `manifest.json5`: 拡張機能の設定ファイル (`manifest.json` + として出力されます) - `manifestTypes.ts`: `manifest.json5` の型定義 - `build.ts`: ビルドスクリプト - `deno.json`: Deno の環境設定ファイル diff --git a/readme.md b/readme.md index c6f8dee..638cfd4 100644 --- a/readme.md +++ b/readme.md @@ -1,48 +1,106 @@ -# Web Extension for NITech Moodle 4.0 - -[![release](https://img.shields.io/github/v/release/nitech-create/nitech-moodle-extension-40a?include_prereleases)](https://github.com/nitech-create/nitech-moodle-extension-40a/releases/latest) - -開発をお手伝いしてくださる方: [開発者向けドキュメント](./readme.dev.md) - -## 概要 - -名古屋工業大学のオンライン授業サポートシステムとして採用されている Moodle (4.0) の機能を改善・拡張して使いやすくするブラウザ用拡張機能です。非公式であり、問題が起きても責任は取れません。 - -Web Extension for Moodle 4.0 of NITech. - -### 主な機能 - -- ダッシュボード ( トップページ ) に講義へのショートカットアクセスを追加 - - 今受けている講義だけを曜日・時間でソートして一覧表示 -- 動画を画面内で大きく表示するように変更 -- 時間割表の追加 (予定) -- 課題などのイベント締め切りにカウントダウン表示の追加 -- ナビゲーションのコース表示をわかりやすいように変更 -- ヘッダーのコース表示をわかりやすいように変更 -- 強制ダウンロードリンクの無効化 -- 全体的なスタイルの修正 - -## ブラウザ対応状況 - -| ブラウザ | 対応状況 | -| ------------------------------------- | ------------------------ | -| Chrome (Windows 11, 111.0.5563.147) | 開発中 | -| Microsoft Edge (情報基盤センター推奨) | 開発中 | -| Firefox | 現在非対応(今後対応予定) | - -## 利用方法 - -### Chrome Web Store からインストール - -準備中です - -### GitHub からインストール - -1. [Releases](https://github.com/nitech-create/nitech-moodle-extension-40a/releases) から .zip ファイルをダウンロードする - - または [ビルド方法](./how_to_build.md) に従ってビルドする -2. 拡張機能ページを開く - - `chrome://extensions` をURL欄に入力する - - またはEdgeブラウザ右上のクッキーみたいなアイコンを押して、「拡張機能の管理」をクリック -3. 開発者モードを有効にします -4. `manifest.json` が含まれるフォルダまたはダウンロードした .zip ファイルをドロップ - - または「パッケージ化されていない拡張機能を読み込む」 +# NITech Moodle Extension (40a) + +Web Extension for NITech Moodle 4.0 + +[Chrome Web Store で公開中](https://chromewebstore.google.com/detail/nitech-moodle-extension-4/gghacnecolaclhlihmlhffgkmeojehff) + +[![release](https://img.shields.io/github/v/release/nitech-create/nitech-moodle-extension-40a?include_prereleases)](https://github.com/nitech-create/nitech-moodle-extension-40a/releases/latest) + +開発をお手伝いしてくださる方: [開発者向けドキュメント](./readme.dev.md) + +## 概要 + +名古屋工業大学のオンライン授業サポートシステムとして採用されている Moodle (4.0) +の機能を改善・拡張して使いやすくするブラウザ用拡張機能です。非公式であり、問題が起きても責任は取れません。 + +### 主な機能 + +- ダッシュボード ( トップページ ) に講義へのショートカットアクセスを追加 + - 今受けている講義だけを曜日・時間でソートして一覧表示 +- 動画を画面内で大きく表示するように変更 +- 時間割表の追加 (予定) +- 課題などのイベント締め切りにカウントダウン表示の追加 +- ナビゲーションのコース表示をわかりやすいように変更 +- ヘッダーのコース表示をわかりやすいように変更 +- 強制ダウンロードリンクの無効化 +- 全体的なスタイルの修正 + +## ブラウザ対応状況 + +| ブラウザ | 対応状況 | +| ------------------------------------- | ------------------------ | +| Chrome (Windows 11, 111.0.5563.147) | 開発中 | +| Microsoft Edge (情報基盤センター推奨) | 開発中 | +| Firefox | 現在非対応(今後対応予定) | + +## 利用方法 + +### Chrome Web Store からインストール + +[Chrome Web Store で公開中](https://chromewebstore.google.com/detail/nitech-moodle-extension-4/gghacnecolaclhlihmlhffgkmeojehff) + +### GitHub からインストール + +1. [Releases](https://github.com/nitech-create/nitech-moodle-extension-40a/(releases)) + から .zip ファイルをダウンロードする + - または [ビルド方法](./how_to_build.md) に従ってビルドする +2. 拡張機能ページを開く + - `chrome://extensions` を URL 欄に入力する + - または Edge + ブラウザ右上のクッキーみたいなアイコンを押して、「拡張機能の管理」をクリック +3. 開発者モードを有効にします +4. `manifest.json` が含まれるフォルダまたはダウンロードした .zip + ファイルをドロップ + - # または「パッケージ化されていない拡張機能を読み込む」 + +# Web Extension for NITech Moodle 4.0 + +[![release](https://img.shields.io/github/v/release/nitech-create/nitech-moodle-extension-40a?include_prereleases)](https://github.com/nitech-create/nitech-moodle-extension-40a/releases/latest) + +開発をお手伝いしてくださる方: [開発者向けドキュメント](./readme.dev.md) + +## 概要 + +名古屋工業大学のオンライン授業サポートシステムとして採用されている Moodle (4.0) +の機能を改善・拡張して使いやすくするブラウザ用拡張機能です。非公式であり、問題が起きても責任は取れません。 + +Web Extension for Moodle 4.0 of NITech. + +### 主な機能 + +- ダッシュボード ( トップページ ) に講義へのショートカットアクセスを追加 + - 今受けている講義だけを曜日・時間でソートして一覧表示 +- 動画を画面内で大きく表示するように変更 +- 時間割表の追加 (予定) +- 課題などのイベント締め切りにカウントダウン表示の追加 +- ナビゲーションのコース表示をわかりやすいように変更 +- ヘッダーのコース表示をわかりやすいように変更 +- 強制ダウンロードリンクの無効化 +- 全体的なスタイルの修正 + +## ブラウザ対応状況 + +| ブラウザ | 対応状況 | +| ------------------------------------- | ------------------------ | +| Chrome (Windows 11, 111.0.5563.147) | 開発中 | +| Microsoft Edge (情報基盤センター推奨) | 開発中 | +| Firefox | 現在非対応(今後対応予定) | + +## 利用方法 + +### Chrome Web Store からインストール + +準備中です + +### GitHub からインストール + +1. [Releases](https://github.com/nitech-create/nitech-moodle-extension-40a/(releases)) + から .zip ファイルをダウンロードする + - または [ビルド方法](./how_to_build.md) に従ってビルドする +2. 拡張機能ページを開く + - `chrome://extensions` を URL 欄に入力する + - または Edge + ブラウザ右上のクッキーみたいなアイコンを押して、「拡張機能の管理」をクリック +3. 開発者モードを有効にします +4. `manifest.json` が含まれるフォルダまたはダウンロードした .zip + ファイルをドロップ - または「パッケージ化されていない拡張機能を読み込む」 diff --git a/scripts/cache-all.sh b/scripts/cache-all.sh deleted file mode 100644 index c534685..0000000 --- a/scripts/cache-all.sh +++ /dev/null @@ -1,10 +0,0 @@ -options="" -for arg in "$@"; do - if [[ "$arg" == "--reload" ]]; then - options="$options --reload" - fi -done - -for file in $(find . -name "*.ts"); do - deno cache $options $file --import-map ./import_map.json -done diff --git a/scripts/check-version-increase.sh b/scripts/check-version-increase.sh deleted file mode 100644 index 3ba3dcd..0000000 --- a/scripts/check-version-increase.sh +++ /dev/null @@ -1,25 +0,0 @@ -# Check if the version set in manifest.json5 is increased from latest release -# useage: bash check-version.sh nitech-create/nitech-moodle-extension-40a - -manifest_version="$(sh $(dirname $0)/get-manifest-version.sh)" - -if [ $# -lt 1 ]; then - echo "Repository name (user/repo) is needed." - exit 1 -fi - -api_endpoint="https://api.github.com/repos/$1/releases/latest" -release_version=$(curl -sSL -H "Accept: application/vnd.github+json" $api_endpoint \ - | jq ".tag_name" | sed -E 's/^"v?//' | sed -E 's/"$//') - -# semver -semver_tempfile=$(mktemp) -curl -sSL "https://raw.githubusercontent.com/parleer/semver-bash/master/semver" > $semver_tempfile -source $semver_tempfile - -if $(semverGT "$manifest_version" "$release_version"); then - exit 0 -else - echo "The version in manifest file ($manifest_version) is less or equals to latest release ($release_version)" - exit 1 -fi diff --git a/scripts/check-version.sh b/scripts/check-version.sh deleted file mode 100644 index 8c45c63..0000000 --- a/scripts/check-version.sh +++ /dev/null @@ -1,8 +0,0 @@ -# Check if the version set in manifest.json5 matches the one of the CLI argument -# useage: bash check-version.sh v1.0.0 - -manifest_version=$(sh $(dirname $0)/get-manifest-version.sh) -if [ "$manifest_version" != $(echo "$1" | sed -E 's/^v?//') ]; then - echo "The version ($1) does not matchese to the one in manifest file ($manifest_version)." 1>&2 - exit 1 -fi diff --git a/scripts/get-manifest-version.sh b/scripts/get-manifest-version.sh deleted file mode 100644 index e17f7da..0000000 --- a/scripts/get-manifest-version.sh +++ /dev/null @@ -1,4 +0,0 @@ -cat "$(dirname $0)/../src/manifest.json5" \ - | grep -P 'version\s*:\s*"\d+\.\d+\.\d+[^"]*"' \ - | sed -E 's/^\s*version\s*:\s*"//' \ - | sed -E 's/",?\s*$//' diff --git a/scripts/getManifestVersion.ts b/scripts/getManifestVersion.ts new file mode 100644 index 0000000..ad60e7c --- /dev/null +++ b/scripts/getManifestVersion.ts @@ -0,0 +1,17 @@ +import * as JSONC from "@std/jsonc"; + +let content = ""; + +await Deno.stdin.readable + .pipeThrough(new TextDecoderStream()) + .pipeTo( + new WritableStream({ + write(chunk) { + content += chunk; + }, + }), + ); + +const manifest = JSONC.parse(content); + +console.log(manifest.version); diff --git a/scripts/setup-vscode.ts b/scripts/setup-vscode.ts deleted file mode 100644 index 418a164..0000000 --- a/scripts/setup-vscode.ts +++ /dev/null @@ -1,43 +0,0 @@ -const configFilePath = './.vscode/settings.json'; -const configJSON = await (async () => { - try { - const stat = await Deno.lstat(configFilePath); - const a = 2; - if (stat.isFile) { - return Deno.readTextFile(configFilePath); - } - - return Promise.reject(`${configFilePath} exists but is NOT a file`); - } catch { - return '{}'; - } -})(); - -console.info("\x1b[1mInstall Deno for VSCode on https://marketplace.visualstudio.com/items?itemName=denoland.vscode-deno\x1b[0m"); - -const config = JSON.parse(configJSON); -const langSettings = { - 'editor.defaultFormatter': 'denoland.vscode-deno', - 'editor.tabSize': 2, - 'editor.insertSpaces': true, -}; - -config['deno.enable'] = true; -config['deno.unstable'] = true; -config['deno.lint'] = true; -config['deno.config'] = './deno.json'; -config['deno.importMap'] = './import_map.json'; -config['[javascript]'] = langSettings; -config['[javascriptreact]'] = langSettings; -config['[typescript]'] = langSettings; -config['[typescriptreact]'] = langSettings; -config['[json]'] = langSettings; - -await Deno.writeTextFile(configFilePath, JSON.stringify(config)); - -const p = Deno.run({ - cmd: ['deno', 'fmt', configFilePath], -}); -const status = await p.status(); - -Deno.exit(status.code); diff --git a/src/common/course.ts b/src/common/course.ts deleted file mode 100644 index 5b2bb45..0000000 --- a/src/common/course.ts +++ /dev/null @@ -1,113 +0,0 @@ -interface RegularLectureCourse { - type: 'regular-lecture'; - /** 講義名 */ - name: string; - /** moodle での表示名 */ - fullName: string; - /** 開講する年度 */ - fullYear: number; - /** 第一部 / 第二部 / 大学院 */ - curriculumPart: 1 | 2 | 4; - /** 講義番号 */ - code: number; - /** 開講する時期 (前期 / 後期 / 第1クォーター など) */ - semester: '1/2' | '2/2' | '1/1' | '1/4' | '2/4' | '3/4' | '4/4' | 'unfixed'; - /** 開講する曜日 */ - weekOfDay: 'sun' | 'mon' | 'tue' | 'wed' | 'thu' | 'fri' | 'sat'; - /** 開口する時間 (コマ) [`start`, `end`] */ - period: [number, number]; - /** moodle ページのID */ - pageId: number; - /** moodle のコース省略名 */ - shortName: string; -} - -interface SpecialCourse { - type: 'special'; - /** 講義名 */ - name: string; - /** moodle での表示名 */ - fullName: string; - /** 開講する年度 */ - fullYear?: number; - /** moodle ページのID */ - pageId: number; - /** moodle のコース省略名 */ - shortName: string; -} - -type Course = RegularLectureCourse | SpecialCourse; - -const semesterToTextMap = { - '1/2': '前期', - '2/2': '後期', - '1/1': '通年', - '1/4': '第1クォーター', - '2/4': '第2クォーター', - '3/4': '第3クォーター', - '4/4': '第4クォーター', - 'unfixed': '未定', -} as const; - -const textToSemesterMap = { - '前期': '1/2', - '後期': '2/2', - '通年': '1/1', - '第1クォーター': '1/4', - '第2クォーター': '2/4', - '第3クォーター': '3/4', - '第4クォーター': '4/4', - '未定': 'unfixed', -} as const; - -const semesterOrdering = { - '1/2': 1, - '2/2': 2, - '1/1': 3, - '1/4': 4, - '2/4': 5, - '3/4': 6, - '4/4': 7, - 'unfixed': 8, -} as const; - -const weekOfDayToTextMap = { - 'sun': '日曜', - 'mon': '月曜', - 'tue': '火曜', - 'wed': '水曜', - 'thu': '木曜', - 'fri': '金曜', - 'sat': '土曜', -} as const; - -const textToWeekOfDayMap = { - '日曜': 'sun', - '月曜': 'mon', - '火曜': 'tue', - '水曜': 'wed', - '木曜': 'thu', - '金曜': 'fri', - '土曜': 'sat', -} as const; - -const weekOfDayOrdering = { - 'sun': 6, - 'mon': 0, - 'tue': 1, - 'wed': 2, - 'thu': 3, - 'fri': 4, - 'sat': 5, -} as const; - -export type { Course, RegularLectureCourse, SpecialCourse }; - -export { - semesterOrdering, - semesterToTextMap, - textToSemesterMap, - textToWeekOfDayMap, - weekOfDayOrdering, - weekOfDayToTextMap, -}; diff --git a/src/common/debounceCallback.ts b/src/common/debounceCallback.ts new file mode 100644 index 0000000..986c2fa --- /dev/null +++ b/src/common/debounceCallback.ts @@ -0,0 +1,15 @@ +export type Callback = (...args: Args) => void; + +export const debounceCallback = function < + Args extends unknown[], + ThisType = unknown, +>( + callback: Callback, + timeout: number, +): Callback { + let timer: number | undefined = undefined; + return function (this: ThisType, ...args: Args) { + clearTimeout(timer); + timer = setTimeout(() => callback.apply(this, args), timeout); + }; +}; diff --git a/src/common/model/course.ts b/src/common/model/course.ts new file mode 100644 index 0000000..b252e97 --- /dev/null +++ b/src/common/model/course.ts @@ -0,0 +1,233 @@ +// deno-fmt-ignore +type Period = + | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 + | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20; +type Semester = 1 | 2 | 3 | 4; +type WeekOfDay = "sun" | "mon" | "tue" | "wed" | "thu" | "fri" | "sat"; + +const identifierLikePattern = + /^(?(?\d{4})|\w{2})(?\w)(?\w\d{3})$/; +const periodPattern = /(?([1-9]|1\d|20))-(?([1-9]|1\d|20))限/; + +const weekOfDayIndex = ( + weekOfDay: NonNullable, +): number => { + return ["sun", "mon", "tue", "wed", "thu", "fri", "sat"].indexOf(weekOfDay); +}; + +const findSemester = function (text: string): [Semester, Semester] | null { + if (text.includes("前期")) { + return [1, 2]; + } else if (text.includes("後期")) { + return [3, 4]; + } else if (text.includes("第1クォーター")) { + return [1, 1]; + } else if (text.includes("第2クォーター")) { + return [2, 2]; + } else if (text.includes("第3クォーター")) { + return [3, 3]; + } else if (text.includes("第4クォーター")) { + return [4, 4]; + } + + return null; +}; + +const findWeekOfDay = function (text: string): WeekOfDay | null { + if (text.includes("日曜")) { + return "sun"; + } else if (text.includes("月曜")) { + return "mon"; + } else if (text.includes("火曜")) { + return "tue"; + } else if (text.includes("水曜")) { + return "wed"; + } else if (text.includes("木曜")) { + return "thu"; + } else if (text.includes("金曜")) { + return "fri"; + } else if (text.includes("土曜")) { + return "sat"; + } + + return null; +}; + +export type CourseJson = { + id: string; + /** course name */ + name: string; + /** course name in moodle */ + fullName: string; + /** course name in moodle */ + systemCourseName?: string; + /** the year the course is offered */ + fullYear?: number; + /** the semester the course is offered */ + semester?: [Semester, Semester]; + /** the semester the course is offered */ + weekOfDay?: WeekOfDay; + /** the period in timetable the course is offered */ + period?: [Period, Period]; +}; + +/** + * Courses that are registered in moodle that are being offered + * or have been offered in the past + */ +export class Course { + id: string; + /** course name */ + name: string; + /** course name displayed in moodle */ + fullName: string; + /** course name in moodle */ + systemCourseName?: string; + /** the year the course is offered */ + fullYear?: number; + /** the semester the course is offered */ + semester?: [Semester, Semester]; + /** the day of the week the course is offered */ + weekOfDay?: WeekOfDay; + /** the period in timetable the course is offered */ + period?: [Period, Period]; + + constructor(init: CourseJson) { + this.id = init.id; + this.name = init.name; + this.fullName = init.fullName; + this.systemCourseName = init.systemCourseName; + this.fullYear = init.fullYear; + this.semester = init.semester; + this.weekOfDay = init.weekOfDay; + this.period = init.period; + } + + static parse(text: string): Omit { + const cleanText = text.trim().replace(/\s+/g, " "); + const segments = cleanText.split(" "); + const normalizedText = cleanText.normalize("NFKC").toLowerCase(); + + const name = (() => { + // string "コース名" may be embedded for screen reader + const nameIndex = segments.indexOf("コース名"); + if (nameIndex > 0 && nameIndex + 1 < segments.length) { + return segments.slice(nameIndex + 1).join(" "); + } + return cleanText; + })(); + const fullName = cleanText; + + let systemCourseName: string | undefined = undefined; + let fullYear: number | undefined = undefined; + for (const segment of segments) { + const match = segment.match(identifierLikePattern); + if (match === null) continue; + + const year = match.groups?.year; + const seg1 = match.groups?.seg1; + const seg2 = match.groups?.seg2; + const seg3 = match.groups?.seg3; + + if (year) { + fullYear = parseInt(year); + } + if (seg1 && seg2 && seg3) { + systemCourseName = `${seg1.slice(-2)}-${seg2}-${seg3}`; + } + } + + const semester = findSemester(normalizedText) ?? undefined; + + const weekOfDay = findWeekOfDay(normalizedText) ?? undefined; + const periodMatch = normalizedText.match(periodPattern); + let period: [Period, Period] | undefined = undefined; + if (periodMatch) { + const start = periodMatch.groups?.start; + const end = periodMatch.groups?.end; + if (start && end) { + period = [parseInt(start), parseInt(end)] as [Period, Period]; + } + } + + return { + name, + fullName, + systemCourseName, + fullYear, + semester, + weekOfDay, + period, + }; + } + + static fromJson(json: CourseJson): Course { + return new Course(json); + } + + toJson(): CourseJson { + return { + id: this.id, + name: this.name, + fullName: this.fullName, + systemCourseName: this.systemCourseName, + fullYear: this.fullYear, + semester: this.semester, + weekOfDay: this.weekOfDay, + period: this.period, + }; + } + + // TODO: accept sort options + compare(that: Course): number { + if (this.fullYear && that.fullYear) { + if (this.fullYear !== that.fullYear) { + return this.fullYear - that.fullYear; + } + } else if (this.fullYear) { + return -1; + } else if (that.fullYear) { + return 1; + } + + if (this.semester && that.semester) { + if (this.semester[0] !== that.semester[0]) { + return this.semester[0] - that.semester[0]; + } + } else if (this.semester?.[0]) { + return -1; + } else if (that.semester?.[0]) { + return 1; + } + + if (this.weekOfDay && that.weekOfDay) { + const thisWeekOfDay = weekOfDayIndex(this.weekOfDay); + const thatWeekOfDay = weekOfDayIndex(that.weekOfDay); + if (thisWeekOfDay !== thatWeekOfDay) { + return thisWeekOfDay - thatWeekOfDay; + } + } else if (this.weekOfDay) { + return -1; + } else if (that.weekOfDay) { + return 1; + } + + if (this.period && that.period) { + if (this.period[0] !== that.period[0]) { + return this.period[0] - that.period[0]; + } + } else if (this.period) { + return -1; + } else if (that.period) { + return 1; + } + + if (this.name < that.name) { + return -1; + } else if (this.name > that.name) { + return 1; + } + + return 0; + } +} diff --git a/src/common/model/preferences.ts b/src/common/model/preferences.ts new file mode 100644 index 0000000..10e5495 --- /dev/null +++ b/src/common/model/preferences.ts @@ -0,0 +1,33 @@ +export type Preferences = { + // features for all pages + removeForceDownload: { + enabled: boolean; + }; + replaceBreadcrumbCourseName: { + enabled: boolean; + }; + replaceNavigationCourseName: { + enabled: boolean; + }; + + // features for dashboard page + dashboardEventsCountdown: { + enabled: boolean; + }; + dashboardQuickCourseLinks: { + enabled: boolean; + }; + + // video page + scormAutoCollapseToc: { + enabled: boolean; + }; + scormAutoPlay: { + enabled: boolean; + }; + + // login page + loginAutoSubmit: { + enabled: boolean; + }; +}; diff --git a/src/common/options.ts b/src/common/options.ts deleted file mode 100644 index 9995b50..0000000 --- a/src/common/options.ts +++ /dev/null @@ -1,18 +0,0 @@ -interface FeatureOption { - enabled: boolean; - [key: string]: unknown; -} - -interface Options { - features: { - [key: string]: FeatureOption; - }; -} - -const defaultFeatureOption = { - enabled: true, -}; - -export type { FeatureOption, Options }; - -export { defaultFeatureOption }; diff --git a/src/common/storage/course.ts b/src/common/storage/course.ts deleted file mode 100644 index ef66350..0000000 --- a/src/common/storage/course.ts +++ /dev/null @@ -1,24 +0,0 @@ -import * as storage from './storage.ts'; -import { Course } from '../course.ts'; - -const storageCourseKey = 'courses'; - -const mergeCourseList = function (value: Course[], source: Course[]) { - const map = new Map(); - source.forEach((course) => map.set(course.fullName, course)); - value.forEach((course) => map.set(course.fullName, course)); - - return Array.from(map.values()); -}; - -const getCourses = async function () { - return await storage.get(storageCourseKey); -}; - -const storeCourseByMerge = async function (courses: Course[]) { - await storage.update(storageCourseKey, (prevCourses) => { - return mergeCourseList(courses, prevCourses); - }); -}; - -export { getCourses, mergeCourseList, storeCourseByMerge }; diff --git a/src/common/storage/courses/actions.ts b/src/common/storage/courses/actions.ts new file mode 100644 index 0000000..da055c1 --- /dev/null +++ b/src/common/storage/courses/actions.ts @@ -0,0 +1,19 @@ +import type { CourseJson } from "~/common/model/course.ts"; + +export type SaveCoursesAction = { + type: "saveCourses"; + payload: { + courses: CourseJson[]; + }; +}; + +export type MergeAndSaveCoursesAction = { + type: "mergeAndSaveCourses"; + payload: { + courses: CourseJson[]; + }; +}; + +export type CoursesAction = + | SaveCoursesAction + | MergeAndSaveCoursesAction; diff --git a/src/common/storage/courses/index.ts b/src/common/storage/courses/index.ts new file mode 100644 index 0000000..078bfbe --- /dev/null +++ b/src/common/storage/courses/index.ts @@ -0,0 +1,36 @@ +import * as storage from "../storage.ts"; +import type { CourseJson } from "~/common/model/course.ts"; +import { coursesReducer } from "./reducer.ts"; +import type { CoursesAction } from "./actions.ts"; +import { Course } from "~/common/model/course.ts"; + +const storageArea = "local"; + +const initialCourses: CourseJson[] = []; +let cachedCourses: CourseJson[] | null = null; + +export const getCoursesJson = async function (): Promise { + if (cachedCourses !== null) { + return cachedCourses; + } + + const courses = await storage.get("courses", storageArea); + cachedCourses = courses ?? initialCourses; + + return cachedCourses; +}; + +export const getCourses = async function (): Promise { + return (await getCoursesJson()).map(Course.fromJson); +}; + +export const reduceAndSaveCourses = async function ( + action: CoursesAction, +): Promise { + cachedCourses = coursesReducer(await getCoursesJson(), action); + return storage.set( + "courses", + cachedCourses, + storageArea, + ); +}; diff --git a/src/common/storage/courses/reducer.ts b/src/common/storage/courses/reducer.ts new file mode 100644 index 0000000..663acb0 --- /dev/null +++ b/src/common/storage/courses/reducer.ts @@ -0,0 +1,27 @@ +import type { CourseJson } from "~/common/model/course.ts"; +import type { CoursesAction } from "./actions.ts"; + +export const coursesReducer = function ( + courses: CourseJson[], + action: CoursesAction, +): CourseJson[] { + const { payload } = action; + + if (action.type === "saveCourses") { + return payload.courses; + } else if (action.type === "mergeAndSaveCourses") { + const idCourseMap = new Map(); + + for (const course of courses) { + idCourseMap.set(course.id, course); + } + for (const course of payload.courses) { + idCourseMap.set(course.id, course); + } + + return [...idCourseMap.values()]; + } + + const _: never = action; + throw Error(`Unknown action type: ${(action as CoursesAction).type}`); +}; diff --git a/src/common/storage/options.ts b/src/common/storage/options.ts deleted file mode 100644 index 9375867..0000000 --- a/src/common/storage/options.ts +++ /dev/null @@ -1,20 +0,0 @@ -// @deno-types=npm:@types/lodash -import * as lodash from 'lodash'; -import * as storage from './storage.ts'; -import { Options } from '../options.ts'; - -const storageOptionsKey = 'options'; - -const getOptions = async function () { - return await storage.get(storageOptionsKey); -}; - -const storeOptionsByMerge = async function (options: Partial) { - await storage.update(storageOptionsKey, (prevOptions) => { - return lodash.merge(prevOptions, options); - }); -}; - -export type { Options }; - -export { getOptions, storeOptionsByMerge }; diff --git a/src/common/storage/preferences/action.ts b/src/common/storage/preferences/action.ts new file mode 100644 index 0000000..6e164c4 --- /dev/null +++ b/src/common/storage/preferences/action.ts @@ -0,0 +1,65 @@ +export type PatchRemoveForceDownloadAction = { + type: "patchRemoveForceDownload"; + payload: { + enabled?: boolean; + }; +}; + +export type PatchReplaceBreadcrumbCourseNameAction = { + type: "patchReplaceBreadcrumbCourseName"; + payload: { + enabled?: boolean; + }; +}; + +export type PatchReplaceNavigationCourseNameAction = { + type: "patchReplaceNavigationCourseName"; + payload: { + enabled?: boolean; + }; +}; + +export type PatchDashboardEventsCountdownAction = { + type: "patchDashboardEventsCountdown"; + payload: { + enabled?: boolean; + }; +}; + +export type PatchDashboardQuickCourseLinksAction = { + type: "patchDashboardQuickCourseLinks"; + payload: { + enabled?: boolean; + }; +}; + +export type PatchScormAutoCollapseTocAction = { + type: "patchScormAutoCollapseToc"; + payload: { + enabled?: boolean; + }; +}; + +export type PatchScormAutoPlayAction = { + type: "patchScormAutoPlay"; + payload: { + enabled?: boolean; + }; +}; + +export type PatchLoginAutoSubmitAction = { + type: "patchLoginAutoSubmit"; + payload: { + enabled?: boolean; + }; +}; + +export type PreferencesAction = + | PatchRemoveForceDownloadAction + | PatchReplaceBreadcrumbCourseNameAction + | PatchReplaceNavigationCourseNameAction + | PatchDashboardEventsCountdownAction + | PatchDashboardQuickCourseLinksAction + | PatchScormAutoCollapseTocAction + | PatchScormAutoPlayAction + | PatchLoginAutoSubmitAction; diff --git a/src/common/storage/preferences/index.ts b/src/common/storage/preferences/index.ts new file mode 100644 index 0000000..e2fb3b3 --- /dev/null +++ b/src/common/storage/preferences/index.ts @@ -0,0 +1,57 @@ +import * as storage from "../storage.ts"; +import type { Preferences } from "~/common/model/preferences.ts"; +import { preferencesReducer } from "./reducer.ts"; +import type { PreferencesAction } from "./action.ts"; + +const storageArea = "local"; + +export const initialPreferences: Preferences = { + removeForceDownload: { + enabled: true, + }, + replaceBreadcrumbCourseName: { + enabled: true, + }, + replaceNavigationCourseName: { + enabled: true, + }, + dashboardEventsCountdown: { + enabled: true, + }, + dashboardQuickCourseLinks: { + enabled: true, + }, + scormAutoCollapseToc: { + enabled: true, + }, + scormAutoPlay: { + enabled: false, + }, + loginAutoSubmit: { + enabled: true, + }, +}; +let cachedPreferences: Preferences | null = null; + +export const getPreferences = async function (): Promise { + if (cachedPreferences !== null) { + return cachedPreferences; + } + + const preferences = await storage.get("preferences", storageArea); + cachedPreferences = preferences ?? initialPreferences; + + return cachedPreferences; +}; + +export const reduceAndSavePreferences = async function ( + action: PreferencesAction, +): Promise { + cachedPreferences = preferencesReducer(await getPreferences(), action); + await storage.set( + "preferences", + cachedPreferences, + storageArea, + ); + return cachedPreferences; +}; diff --git a/src/common/storage/preferences/reducer.ts b/src/common/storage/preferences/reducer.ts new file mode 100644 index 0000000..1b97440 --- /dev/null +++ b/src/common/storage/preferences/reducer.ts @@ -0,0 +1,78 @@ +import type { Preferences } from "~/common/model/preferences.ts"; +import type { PreferencesAction } from "./action.ts"; + +export const preferencesReducer = function ( + preferences: Preferences, + action: PreferencesAction, +): Preferences { + const { payload } = action; + + if (action.type === "patchRemoveForceDownload") { + return { + ...preferences, + removeForceDownload: { + ...preferences.removeForceDownload, + ...payload, + }, + }; + } else if (action.type === "patchReplaceBreadcrumbCourseName") { + return { + ...preferences, + replaceBreadcrumbCourseName: { + ...preferences.replaceBreadcrumbCourseName, + ...payload, + }, + }; + } else if (action.type === "patchReplaceNavigationCourseName") { + return { + ...preferences, + replaceNavigationCourseName: { + ...preferences.replaceNavigationCourseName, + ...payload, + }, + }; + } else if (action.type === "patchDashboardEventsCountdown") { + return { + ...preferences, + dashboardEventsCountdown: { + ...preferences.dashboardEventsCountdown, + ...payload, + }, + }; + } else if (action.type === "patchDashboardQuickCourseLinks") { + return { + ...preferences, + dashboardQuickCourseLinks: { + ...preferences.dashboardQuickCourseLinks, + ...payload, + }, + }; + } else if (action.type === "patchScormAutoCollapseToc") { + return { + ...preferences, + scormAutoCollapseToc: { + ...preferences.scormAutoCollapseToc, + ...payload, + }, + }; + } else if (action.type === "patchScormAutoPlay") { + return { + ...preferences, + scormAutoPlay: { + ...preferences.scormAutoPlay, + ...payload, + }, + }; + } else if (action.type === "patchLoginAutoSubmit") { + return { + ...preferences, + loginAutoSubmit: { + ...preferences.loginAutoSubmit, + ...payload, + }, + }; + } + + const _: never = action; + throw Error(`Unknown action type: ${(action as PreferencesAction).type}`); +}; diff --git a/src/common/storage/storage.ts b/src/common/storage/storage.ts index af8ce93..78e9987 100644 --- a/src/common/storage/storage.ts +++ b/src/common/storage/storage.ts @@ -1,59 +1,34 @@ -import browser from 'webextension-polyfill'; -// @deno-types=npm:@types/lodash -import * as lodash from 'lodash'; -import type { Course } from '../course.ts'; -import { Options } from '../options.ts'; +// @deno-types="@types/webextension-polyfill" +import browser from "webextension-polyfill"; -/** `storage["local" | "managed" | "sync"]` に保存されている値の型 */ -interface StoredValue { - courses: Course[]; - options: Options; -} +import type { CourseJson } from "~/common/model/course.ts"; +import type { Preferences } from "~/common/model/preferences.ts"; -type StorageArea = 'local' | 'managed' | 'sync'; +type Subtract = T extends U ? never : T; -const defaultValue: StoredValue = { - courses: [], - options: { - features: {}, - }, +type StorageSchema = { + courses: CourseJson[]; + preferences: Preferences; + version: number; }; +type StorageKey = Subtract; -/** ストレージから値を取得 */ -const get = async function ( - key: Key, - storageArea: StorageArea = 'local', -): Promise { - const value = await browser.storage[storageArea].get(key) as Partial< - StoredValue - >; +type StorageAreaName = "local" | "sync" | "managed" | "session"; - return lodash.defaultsDeep(value, defaultValue)[key]; +const storageGet = async function ( + key: K, + storageArea: StorageAreaName, +): Promise { + const data = await browser.storage[storageArea].get(key); + return data[key] as StorageSchema[K]; }; -/** 最後に作成された (Promise-chain の最後尾の) `Promise` */ -let lastPromise: Promise = Promise.resolve(); -/** - * ストレージに保存されている値を更新する; - * 複数に同時の非同期更新が起こらないことを保証する - */ -const update = async function ( - key: Key, - reducer: (prev: StoredValue[Key]) => StoredValue[Key], - storageArea: StorageArea = 'local', -) { - const storeValue = () => - new Promise((resolve) => { - get(key).then((prevValue) => { - browser.storage[storageArea].set({ - [key]: reducer(lodash.defaultsDeep(prevValue, defaultValue[key])), - }); - resolve(); - }); - }); - - lastPromise = lastPromise.then(storeValue); - await lastPromise; +const storageSet = async function ( + key: K, + value: StorageSchema[K], + storageArea: StorageAreaName, +): Promise { + await browser.storage[storageArea].set({ [key]: value, version: 1 }); }; -export { get, update }; +export { storageGet as get, storageSet as set }; diff --git a/src/contentScripts/all/removeForceDownload/index.ts b/src/contentScripts/all/removeForceDownload/index.ts new file mode 100644 index 0000000..a96b56d --- /dev/null +++ b/src/contentScripts/all/removeForceDownload/index.ts @@ -0,0 +1,52 @@ +import { isDebug } from "esbuild-plugin-debug-switch"; + +import { getPreferences } from "~/common/storage/preferences/index.ts"; +import { registerMutationObserverCallback } from "~/contentScripts/common/mutationObserverCallback.ts"; + +const parseUrl = function (url: string): URL | null { + try { + return new URL(url); + } catch { + return null; + } +}; + +const removeForceDownload = () => { + if (isDebug) { + console.log("Removing forcedownload"); + } + + document.querySelectorAll("a").forEach((link) => { + const url = parseUrl(link.href); + if (url === null) return; + + link.href = forceDownloadRemoved(url).toString(); + }); +}; + +const forceDownloadRemoved = function (url: URL): URL { + const removed = new URL(url); + removed.searchParams.delete("forcedownload", "1"); + + return removed; +}; + +const main = async () => { + const preferences = await getPreferences(); + if (!preferences.removeForceDownload.enabled) return; + + if (isDebug) { + console.log("RemoveForceDownload is enabled."); + } + + removeForceDownload(); + registerMutationObserverCallback( + removeForceDownload, + { + rootElement: document.body, + observerOptions: { childList: true, subtree: true }, + }, + ); +}; + +main(); diff --git a/src/contentScripts/all/replaceBreadcrumbCourseName/index.ts b/src/contentScripts/all/replaceBreadcrumbCourseName/index.ts new file mode 100644 index 0000000..acd6443 --- /dev/null +++ b/src/contentScripts/all/replaceBreadcrumbCourseName/index.ts @@ -0,0 +1,51 @@ +import { isDebug } from "esbuild-plugin-debug-switch"; + +import { getPreferences } from "~/common/storage/preferences/index.ts"; +import { getCourses } from "~/common/storage/courses/index.ts"; +import { registerMutationObserverCallback } from "~/contentScripts/common/mutationObserverCallback.ts"; + +const replaceBreadcrumbCourseName = function ( + replacementMap: Map, +) { + const breadcrumb = document.querySelector("#page-header nav ol.breadcrumb"); + if (!breadcrumb) return; + + const links = Array.from( + breadcrumb.querySelectorAll("li a"), + ) as HTMLAnchorElement[]; + for (const link of links) { + const content = link.textContent?.trim(); + if (!content) continue; + + link.textContent = replacementMap.get(content) ?? content; + } +}; + +const main = async function () { + const preferences = await getPreferences(); + if (!preferences.replaceBreadcrumbCourseName.enabled) return; + + if (isDebug) { + console.log("ReplaveBreadcrumbCourseName is enabled."); + } + + const courses = await getCourses(); + const replacementMap = new Map(); + for (const course of courses) { + if (!course.systemCourseName) continue; + replacementMap.set(course.systemCourseName, course.name); + } + + replaceBreadcrumbCourseName(replacementMap); + registerMutationObserverCallback( + () => { + replaceBreadcrumbCourseName(replacementMap); + }, + { + rootElement: document.body, + observerOptions: { childList: true, subtree: true }, + }, + ); +}; + +main(); diff --git a/src/contentScripts/all/replaceNavigationCourseName/index.ts b/src/contentScripts/all/replaceNavigationCourseName/index.ts new file mode 100644 index 0000000..2a96bfa --- /dev/null +++ b/src/contentScripts/all/replaceNavigationCourseName/index.ts @@ -0,0 +1,52 @@ +import { isDebug } from "esbuild-plugin-debug-switch"; + +import { getPreferences } from "~/common/storage/preferences/index.ts"; +import { getCourses } from "~/common/storage/courses/index.ts"; +import { registerMutationObserverCallback } from "~/contentScripts/common/mutationObserverCallback.ts"; + +const replaceNavigationCourseName = function ( + replacementMap: Map, +) { + const navigation = document.querySelector('*[role="navigation"]'); + if (!navigation) return; + + const links = Array.from( + navigation.querySelectorAll('*[role="treeitem"] a'), + ) as HTMLAnchorElement[]; + + for (const link of links) { + const content = link.textContent?.trim(); + if (!content) continue; + + link.textContent = replacementMap.get(content) ?? content; + } +}; + +const main = async function () { + const preferences = await getPreferences(); + if (!preferences.replaceNavigationCourseName.enabled) return; + + if (isDebug) { + console.log("ReplaceNavigationCourseName is enabled."); + } + + const courses = await getCourses(); + const replacementMap = new Map(); + for (const course of courses) { + if (!course.systemCourseName) continue; + replacementMap.set(course.systemCourseName, course.name); + } + + replaceNavigationCourseName(replacementMap); + registerMutationObserverCallback( + () => { + replaceNavigationCourseName(replacementMap); + }, + { + rootElement: document.body, + observerOptions: { childList: true, subtree: true }, + }, + ); +}; + +main(); diff --git a/src/contentScripts/all/startMessage/index.ts b/src/contentScripts/all/startMessage/index.ts new file mode 100644 index 0000000..daf92c5 --- /dev/null +++ b/src/contentScripts/all/startMessage/index.ts @@ -0,0 +1,8 @@ +// Show message when the extension is loaded. + +import { env, isDebug } from "esbuild-plugin-debug-switch"; + +console.log(`${env.extensionName} ${env.extensionVersion} is loaded.`); +if (isDebug) { + console.log("Debug mode is enabled."); +} diff --git a/src/contentScripts/common/mutationObserverCallback.ts b/src/contentScripts/common/mutationObserverCallback.ts new file mode 100644 index 0000000..320b639 --- /dev/null +++ b/src/contentScripts/common/mutationObserverCallback.ts @@ -0,0 +1,27 @@ +import { debounceCallback } from "~/common/debounceCallback.ts"; + +export type RegisterMutationObserverCallbackOptions = { + rootElement: HTMLElement; + observerOptions: MutationObserverInit; + debounceTimeout?: number; +}; + +/** + * Registers a mutation observer with a specified callback function that + * triggers when observed mutations occur. The callback function can return + * `true` to automatically disconnect the observer after being called. + * Debounces the callback execution with a specified timeout to prevent frequent calls. + */ +export const registerMutationObserverCallback = function ( + callback: (...args: Parameters) => boolean | void, + options: RegisterMutationObserverCallbackOptions, +) { + const debounceTimeout = options?.debounceTimeout ?? 500; + + const observer = new MutationObserver( + debounceCallback((...args) => { + if (callback(...args)) observer.disconnect(); + }, debounceTimeout), + ); + observer.observe(options.rootElement, options.observerOptions); +}; diff --git a/src/contentScripts/dashboard/eventCountdown/components/countdown.tsx b/src/contentScripts/dashboard/eventCountdown/components/countdown.tsx new file mode 100644 index 0000000..1205648 --- /dev/null +++ b/src/contentScripts/dashboard/eventCountdown/components/countdown.tsx @@ -0,0 +1,36 @@ +import { useRemainingTime } from "../hooks/useRemainingTime.ts"; + +const createTimeText = function (seconds: number): string { + if (seconds < 60) { + return `${seconds}秒`; + } else if (seconds < 3600) { + return `${Math.floor(seconds / 60)}分`; + } else if (seconds < 86400) { + return `${Math.floor(seconds / 3600)}時間`; + } else { + return `${Math.floor(seconds / 86400)}日`; + } +}; + +export type CountdownProps = { + targetDate: Date; +}; + +export const Countdown = function (props: CountdownProps) { + const remainingTime = useRemainingTime(props.targetDate); + + const expired = remainingTime < 0; + const timeText = createTimeText(Math.floor(Math.abs(remainingTime / 1000))); + + const decorationClass = expired + ? "text-muted" + : (remainingTime < 86_400_000 ? "red" : ""); + + return ( +
+
+ {expired ? `${timeText}前` : `あと${timeText}`} +
+
+ ); +}; diff --git a/src/contentScripts/dashboard/eventCountdown/hooks/useRemainingTime.ts b/src/contentScripts/dashboard/eventCountdown/hooks/useRemainingTime.ts new file mode 100644 index 0000000..769ee98 --- /dev/null +++ b/src/contentScripts/dashboard/eventCountdown/hooks/useRemainingTime.ts @@ -0,0 +1,35 @@ +import { useCallback, useEffect, useState } from "preact/hooks"; + +const calculateUpdateTimeout = function (milliseconds: number): number { + const seconds = milliseconds / 1_000; + if (seconds < 60) { + return 1_000 - (milliseconds % 1_000); + } else if (seconds < 3_600) { + return 60_000 - (milliseconds % 60_000); + } else if (seconds < 86_400) { + return 3_600_000 - (milliseconds % 3_600_000); + } else { + return 86_400_000 - (milliseconds % 86_400_000); + } +}; + +export const useRemainingTime = function (targetDate: Date) { + const [remainingTime, setRemainingTime] = useState(0); + + const updateRemainingTime = useCallback(() => { + const now = new Date(); + const diff = targetDate.getTime() - now.getTime(); + setRemainingTime(diff); + }, [targetDate]); + + useEffect(updateRemainingTime, [targetDate]); + + useEffect(() => { + const delay = calculateUpdateTimeout(Math.abs(remainingTime)); + const timeoutId = setTimeout(updateRemainingTime, delay); + + return () => clearTimeout(timeoutId); + }, [remainingTime]); + + return remainingTime; +}; diff --git a/src/contentScripts/dashboard/eventCountdown/index.ts b/src/contentScripts/dashboard/eventCountdown/index.ts new file mode 100644 index 0000000..3117d88 --- /dev/null +++ b/src/contentScripts/dashboard/eventCountdown/index.ts @@ -0,0 +1,60 @@ +import * as preact from "preact"; +import { isDebug } from "esbuild-plugin-debug-switch"; + +import { getPreferences } from "~/common/storage/preferences/index.ts"; +import { registerMutationObserverCallback } from "~/contentScripts/common/mutationObserverCallback.ts"; +import { Countdown } from "./components/countdown.tsx"; + +const CLASS_NAME = "ext40a-event-countdown"; + +const renderEventCountdowns = function () { + const calendarRoot = document.querySelector( + '*[data-block="calendar_upcoming"]', + ); + if (!calendarRoot) return; + + const items = Array.from( + calendarRoot.querySelectorAll( + '*[data-region="event-item"] > *:nth-child(2)', + ), + ); + for (const item of items) { + if (item.classList.contains(CLASS_NAME)) continue; + item.classList.add(CLASS_NAME); + + const link = item.querySelector(".date a"); + if (!link) continue; + + // e.g., https://cms7.ict.nitech.ac.jp/moodle40a/calendar/view.php?view=day&time=1680940800 + const time = new URL(link.href).searchParams.get("time"); + if (!time) continue; + + const date = new Date(Number(time) * 1000); + + const root = document.createElement("div"); + root.className = `${CLASS_NAME} small`; + + item.appendChild(root); + preact.render( + preact.createElement(Countdown, { targetDate: date }), + root, + ); + } +}; + +const main = async function () { + const preferences = await getPreferences(); + if (!preferences.dashboardEventsCountdown.enabled) return; + + if (isDebug) { + console.log("EventCountdown is enabled."); + } + + renderEventCountdowns(); + registerMutationObserverCallback(renderEventCountdowns, { + rootElement: document.body, + observerOptions: { childList: true, subtree: true }, + }); +}; + +main(); diff --git a/src/content_scripts/dashboard/dashboard.scss b/src/contentScripts/dashboard/main.scss similarity index 52% rename from src/content_scripts/dashboard/dashboard.scss rename to src/contentScripts/dashboard/main.scss index 5ad0927..62456e1 100644 --- a/src/content_scripts/dashboard/dashboard.scss +++ b/src/contentScripts/dashboard/main.scss @@ -1,6 +1,4 @@ -@import './quickCourseView.scss'; - -// セクション下の謎の空白を修正 +// delete blanks under sections div .block_myoverview .content { min-height: unset; } @@ -8,7 +6,7 @@ div .block_myoverview .paged-content-page-container { min-height: unset; } -// コース概要のリンク要素の大きさを修正 +// enlarge link element in course overview section.block_myoverview ul li div.row { display: flex; margin: 0; @@ -22,7 +20,19 @@ section.block_myoverview ul li div.row { } } -// リスト末尾の下境界線を消す +// delete border line at the end of list section.block ul li:last-child { border-bottom: none; } + +// fix padding and alignment of event calendar +section.block_calendar_upcoming { + div.event[data-region="event-item"] { + align-items: center; + padding: 12px 0 !important; + } + + div.footer { + padding-top: 8px; + } +} diff --git a/src/contentScripts/dashboard/quickCourseLinks/components/filterDropdown.tsx b/src/contentScripts/dashboard/quickCourseLinks/components/filterDropdown.tsx new file mode 100644 index 0000000..50810e1 --- /dev/null +++ b/src/contentScripts/dashboard/quickCourseLinks/components/filterDropdown.tsx @@ -0,0 +1,61 @@ +import type { Filter } from "../filter.ts"; + +export type FilterDropdownProps = { + options: Filter[]; + filter: Filter; + setFilter: (filter: Filter) => void; +}; + +export const FilterDropdown = ( + props: FilterDropdownProps, +) => { + const { options, filter, setFilter } = props; + + return ( +
+
+ + +
+
+ ); +}; diff --git a/src/contentScripts/dashboard/quickCourseLinks/components/liteItem.tsx b/src/contentScripts/dashboard/quickCourseLinks/components/liteItem.tsx new file mode 100644 index 0000000..5f325d6 --- /dev/null +++ b/src/contentScripts/dashboard/quickCourseLinks/components/liteItem.tsx @@ -0,0 +1,39 @@ +import type { Course } from "~/common/model/course.ts"; + +export type ListItemProps = Pick< + Course, + "id" | "name" | "weekOfDay" | "period" +>; + +const weekOfDayName: Record, string> = { + sun: "日曜", + mon: "月曜", + tue: "火曜", + wed: "水曜", + thu: "木曜", + fri: "金曜", + sat: "土曜", +}; + +const coursePageUrl = (id: string) => + `https://cms7.ict.nitech.ac.jp/moodle40a/course/view.php?id=${id}`; + +export const ListItem = function ( + props: ListItemProps, +) { + const { id, name, weekOfDay, period } = props; + + return ( +
  • + + {weekOfDay + ? {weekOfDayName[weekOfDay]} + : null} + {period + ? {period[0]}-{period[1]}限 + : null} + {name} + +
  • + ); +}; diff --git a/src/contentScripts/dashboard/quickCourseLinks/components/quickCourseLinks.tsx b/src/contentScripts/dashboard/quickCourseLinks/components/quickCourseLinks.tsx new file mode 100644 index 0000000..803ddb2 --- /dev/null +++ b/src/contentScripts/dashboard/quickCourseLinks/components/quickCourseLinks.tsx @@ -0,0 +1,82 @@ +import { useEffect, useMemo, useState } from "preact/compat"; + +import { useCourses } from "../hooks/useCourses.ts"; +import { useFilter } from "../hooks/useFilters.ts"; +import { ListItem } from "./liteItem.tsx"; +import { pickFilterByDate } from "../filter.ts"; +import { FilterDropdown } from "./filterDropdown.tsx"; + +export const QuickCourseLinks = function () { + const [filterInitialized, setFilterInitialized] = useState(false); + const courses = useCourses(); + const { filter, setFilter, options: filterOptions } = useFilter( + courses ?? [], + ); + + useEffect(() => { + if (!courses) return; + if (filterInitialized) return; + + setFilter(pickFilterByDate(filterOptions) ?? filter); + setFilterInitialized(true); + }, [ + courses, + filter, + filterOptions, + filterInitialized, + ]); + + const filteredCourses = useMemo(() => { + const sorted = (courses ?? []).toSorted((a, b) => a.compare(b)); + + if (!filter) return sorted; + return sorted.filter((course) => filter.test(course)); + }, [courses, filter]); + + return ( + <> +
    +
    コースリンク
    +
    +
    +
    + +
    +
    +
    +
    +
    + {filteredCourses.map((course) => ( + + ))} +
    +
    +
    +
    +
    +
    +
    +
    + + ); +}; diff --git a/src/contentScripts/dashboard/quickCourseLinks/filter.ts b/src/contentScripts/dashboard/quickCourseLinks/filter.ts new file mode 100644 index 0000000..00c1ded --- /dev/null +++ b/src/contentScripts/dashboard/quickCourseLinks/filter.ts @@ -0,0 +1,79 @@ +import type { Course } from "~/common/model/course.ts"; + +export class Filter { + year?: number; + // matches to course when any semester is included + semesters?: number[]; + label: string; + + constructor( + label: string, + year?: number, + semesters?: number[], + ) { + this.year = year; + this.semesters = semesters; + this.label = label; + } + + test(course: Course): boolean { + // match any courses if no year is specified + if (!this.year) return true; + if (course.fullYear !== this.year) return false; + + // match any semester if no semester is specified + if (!this.semesters) return true; + return this.semesters.some((s) => { + if (!course.semester) return false; + return course.semester[0] <= s && s <= course.semester[1]; + }); + } +} + +export const filtersMatchesToCourses = function (courses: Course[]): Filter[] { + let years = courses + .map((course) => course.fullYear) + .filter((v) => v !== undefined); + years = Array.from(new Set(years)); + years.sort().reverse(); + + return years.flatMap((year) => [ + new Filter(`${year}年`, year), + new Filter(`${year}年 第1クォーター`, year, [1]), + new Filter(`${year}年 第2クォーター`, year, [2]), + new Filter(`${year}年 第3クォーター`, year, [3]), + new Filter(`${year}年 第4クォーター`, year, [4]), + new Filter(`${year}年 前期`, year, [1, 2]), + new Filter(`${year}年 後期`, year, [3, 4]), + ]).filter((filter) => courses.some((course) => filter.test(course))); +}; + +export const pickFilterByDate = function ( + filters: Filter[], + date: Date = new Date(), +): Filter | null { + const year = date.getFullYear(); + const semester = (() => { + // deno-fmt-ignore + switch (date.getMonth()) { + case 0: return 4; + case 1: return 4; + case 2: return 4; + case 3: return 1; + case 4: return 1; + case 5: return 2; + case 6: return 2; + case 7: return 2; + case 8: return 2; + case 9: return 3; + case 10: return 3; + case 11: return 4; + default: return -1; + } + })(); + const filter = filters.find((filter) => { + return filter.year === year && filter.semesters?.includes(semester); + }); + + return filter ?? null; +}; diff --git a/src/contentScripts/dashboard/quickCourseLinks/hooks/useCourses.ts b/src/contentScripts/dashboard/quickCourseLinks/hooks/useCourses.ts new file mode 100644 index 0000000..55efba6 --- /dev/null +++ b/src/contentScripts/dashboard/quickCourseLinks/hooks/useCourses.ts @@ -0,0 +1,14 @@ +import { useEffect, useState } from "preact/hooks"; + +import type { Course } from "~/common/model/course.ts"; +import { getCourses } from "~/common/storage/courses/index.ts"; + +export const useCourses = function () { + const [courses, setCourses] = useState(null); + + useEffect(() => { + getCourses().then(setCourses); + }, []); + + return courses; +}; diff --git a/src/contentScripts/dashboard/quickCourseLinks/hooks/useFilters.ts b/src/contentScripts/dashboard/quickCourseLinks/hooks/useFilters.ts new file mode 100644 index 0000000..fc0d634 --- /dev/null +++ b/src/contentScripts/dashboard/quickCourseLinks/hooks/useFilters.ts @@ -0,0 +1,16 @@ +import { useMemo, useState } from "preact/hooks"; + +import { Filter } from "../filter.ts"; +import { filtersMatchesToCourses } from "../filter.ts"; +import type { Course } from "~/common/model/course.ts"; + +export const useFilter = function (courses: Course[]) { + const defaultFilter = useMemo(() => (new Filter("すべて")), []); + const options = useMemo(() => { + const filters = filtersMatchesToCourses(courses); + return [...filters, defaultFilter]; + }, [courses]); + const [filter, setFilter] = useState(defaultFilter); + + return { filter, setFilter, options }; +}; diff --git a/src/contentScripts/dashboard/quickCourseLinks/index.ts b/src/contentScripts/dashboard/quickCourseLinks/index.ts new file mode 100644 index 0000000..7752def --- /dev/null +++ b/src/contentScripts/dashboard/quickCourseLinks/index.ts @@ -0,0 +1,44 @@ +import * as preact from "preact"; +import { isDebug } from "esbuild-plugin-debug-switch"; + +import { getPreferences } from "~/common/storage/preferences/index.ts"; +import { registerMutationObserverCallback } from "~/contentScripts/common/mutationObserverCallback.ts"; +import { QuickCourseLinks } from "./components/quickCourseLinks.tsx"; + +const CARD_DATA_BLOCK = "ext40a-quick-course-links"; + +const renderQuickCourseLinks = function () { + const blocksRoot = document.getElementById("block-region-content"); + if (!blocksRoot) return; + + // do not use `blocksRoot` as root of preact element, preact will delete + // the first child node. + if (document.querySelector(`*[data-block=${CARD_DATA_BLOCK}`)) return; + const root = document.createElement("section"); + root.dataset["block"] = CARD_DATA_BLOCK; + root.className = "block card mb-3"; + + blocksRoot.insertBefore(root, blocksRoot.childNodes[0] ?? null); + + preact.render( + preact.createElement(QuickCourseLinks, null), + root, + ); +}; + +const main = async function () { + const preferences = await getPreferences(); + if (!preferences.dashboardQuickCourseLinks.enabled) return; + + if (isDebug) { + console.log("QuickCourseLinks is enabled."); + } + + renderQuickCourseLinks(); + registerMutationObserverCallback(renderQuickCourseLinks, { + rootElement: document.body, + observerOptions: { childList: true, subtree: true }, + }); +}; + +main(); diff --git a/src/contentScripts/dashboard/readAndStoreCourses/index.ts b/src/contentScripts/dashboard/readAndStoreCourses/index.ts new file mode 100644 index 0000000..636e0c0 --- /dev/null +++ b/src/contentScripts/dashboard/readAndStoreCourses/index.ts @@ -0,0 +1,71 @@ +import { isDebug } from "esbuild-plugin-debug-switch"; + +import { Course } from "~/common/model/course.ts"; +import { reduceAndSaveCourses } from "~/common/storage/courses/index.ts"; +import { registerMutationObserverCallback } from "~/contentScripts/common/mutationObserverCallback.ts"; + +const readAndStoreCoureses = function () { + if (isDebug) { + console.log("Reading courses"); + } + + const myOverview = document.querySelector('*[data-block="myoverview"]'); + if (!myOverview) return; + + const pagenationDropdownItem = myOverview.querySelector([ + '*[data-region="courses-view"]', + '*[data-region="paging-control-limit-container"]', + 'a[data-limit="0"]', + ].join(" ")); + if (!pagenationDropdownItem) return; + if (pagenationDropdownItem.getAttribute("aria-current") !== "true") { + pagenationDropdownItem.click(); + return; + } + + const links = Array.from(myOverview.querySelectorAll( + '*[data-region="courses-view"] ul a.coursename', + )) as HTMLAnchorElement[]; + const coursesJson = links.map((link) => { + try { + const id = new URL(link.href).searchParams.get("id"); + if (!id) return null; + + return Course.fromJson({ + id, + ...Course.parse(link.textContent || ""), + }).toJson(); + } catch { + return null; + } + }).filter((course) => course !== null); + + const actionType = location.pathname === "/moodle40a/my/courses.php" + ? "saveCourses" + : "mergeAndSaveCourses"; + reduceAndSaveCourses({ + type: actionType, + payload: { + courses: coursesJson, + }, + }); + + return true; +}; + +const main = function () { + if (isDebug) { + console.log("Running ReadAndStoreCoureses"); + } + + readAndStoreCoureses(); + registerMutationObserverCallback( + readAndStoreCoureses, + { + rootElement: document.body, + observerOptions: { childList: true, subtree: true }, + }, + ); +}; + +main(); diff --git a/src/contentScripts/scorm/autoCollapseToc/index.ts b/src/contentScripts/scorm/autoCollapseToc/index.ts new file mode 100644 index 0000000..808174b --- /dev/null +++ b/src/contentScripts/scorm/autoCollapseToc/index.ts @@ -0,0 +1,35 @@ +import { isDebug } from "esbuild-plugin-debug-switch"; + +import { getPreferences } from "~/common/storage/preferences/index.ts"; +import { registerMutationObserverCallback } from "~/contentScripts/common/mutationObserverCallback.ts"; + +const collapseToc = function () { + const toc = document.getElementById("scorm_toc"); + const toggleCollapseButton = document.getElementById("scorm_toc_toggle_btn"); + if (!toc || !toggleCollapseButton) return; + + if (toc.classList.contains("disabled")) return; + + // do not toggle toggle class .disabled directly becausemoodle handles other + // elements at the same time when the button is pressed + toggleCollapseButton.click(); + + return true; +}; + +const main = async function () { + const preferences = await getPreferences(); + if (!preferences.scormAutoCollapseToc.enabled) return; + + if (isDebug) { + console.log("CollapseToc is enabled."); + } + + collapseToc(); + registerMutationObserverCallback(collapseToc, { + rootElement: document.body, + observerOptions: { childList: true, subtree: true }, + }); +}; + +main(); diff --git a/src/content_scripts/scorm/scorm.scss b/src/contentScripts/scormContents/main.scss similarity index 100% rename from src/content_scripts/scorm/scorm.scss rename to src/contentScripts/scormContents/main.scss diff --git a/src/content_scripts/allPages/allPages.ts b/src/content_scripts/allPages/allPages.ts deleted file mode 100644 index d025fec..0000000 --- a/src/content_scripts/allPages/allPages.ts +++ /dev/null @@ -1,19 +0,0 @@ -import debugMode from '../common/debugMode.ts'; -import loadFeature from '../common/loadFeature.ts'; -import removeForceDownload from './removeForceDownload.ts'; -import replaceNavigationText from './replaceNavigationText.ts'; -import replaceHeaderCourseName from './replaceHeaderCourseName.ts'; - -globalThis.addEventListener('load', () => { - console.log('Extension loaded.'); - - loadFeature( - [ - removeForceDownload, - replaceNavigationText, - replaceHeaderCourseName, - ], - new URL(location.href), - debugMode, - ); -}); diff --git a/src/content_scripts/allPages/removeForceDownload.ts b/src/content_scripts/allPages/removeForceDownload.ts deleted file mode 100644 index afbb391..0000000 --- a/src/content_scripts/allPages/removeForceDownload.ts +++ /dev/null @@ -1,22 +0,0 @@ -import type { Feature } from '../common/types.ts'; - -/** 強制ダウンロードのリンクをブラウザで開くようにする */ -const removeForceDownload: Feature = { - uniqueName: 'all-pages-remove-force-download', - hostnameFilter: 'cms7.ict.nitech.ac.jp', - pathnameFilter: /^\/moodle40a\//, - loader: async (options) => { - if (!options.enabled) { - return; - } - - // 読み込み待ちのため遅延を入れる - await new Promise((resolve) => setTimeout(resolve, 1000)); - - document.querySelectorAll('a').forEach((link) => { - link.href = link.href.replace('forcedownload=1', ''); - }); - }, -}; - -export default removeForceDownload; diff --git a/src/content_scripts/allPages/replaceHeaderCourseName.ts b/src/content_scripts/allPages/replaceHeaderCourseName.ts deleted file mode 100644 index dd39a8c..0000000 --- a/src/content_scripts/allPages/replaceHeaderCourseName.ts +++ /dev/null @@ -1,46 +0,0 @@ -import type { Feature } from '../common/types.ts'; -import { getCourses } from '../../common/storage/course.ts'; - -/** ヘッダーのコース表示名をわかりやすい表示に変更する */ -const replaceHeaderCourseName: Feature = { - uniqueName: 'all-pages-replace-header-course-name', - hostnameFilter: 'cms7.ict.nitech.ac.jp', - pathnameFilter: /^\/moodle40a\//, - loader: async (options) => { - if (!options.enabled) { - return; - } - - const elHeader = document.getElementById('page-header'); - if (!elHeader) { - return; - } - - const courses = await getCourses(); - const courseNameMap = new Map(); - for (const course of courses) { - if (course.type === 'regular-lecture') { - courseNameMap.set( - course.shortName, - `${course.name} ${course.shortName}`, - ); - } else { - courseNameMap.set(course.shortName, course.fullName); - } - } - - const elBreadcrumbLinks = elHeader.querySelectorAll( - 'nav li a', - ); - elBreadcrumbLinks.forEach((elLink) => { - const shortName = elLink.textContent ?? ''; - const courseName = courseNameMap.get(shortName); - - if (typeof courseName === 'string') { - elLink.textContent = courseName; - } - }); - }, -}; - -export default replaceHeaderCourseName; diff --git a/src/content_scripts/allPages/replaceNavigationText.ts b/src/content_scripts/allPages/replaceNavigationText.ts deleted file mode 100644 index 8eabd01..0000000 --- a/src/content_scripts/allPages/replaceNavigationText.ts +++ /dev/null @@ -1,48 +0,0 @@ -import type { Feature } from '../common/types.ts'; -import { getCourses } from '../../common/storage/course.ts'; - -/** ナビゲーションのコース表示名をわかりやすい表示に変更する */ -const replaceNavigationText: Feature = { - uniqueName: 'all-pages-replace-navigation-texts', - hostnameFilter: 'cms7.ict.nitech.ac.jp', - pathnameFilter: /^\/moodle40a\//, - loader: async (options) => { - if (!options.enabled) { - return; - } - - const elNavigation = document.querySelector('section.block_navigation'); - if (!elNavigation) { - return; - } - - const courses = await getCourses(); - const courseNameMap = new Map(); - for (const course of courses) { - courseNameMap.set(course.shortName, course.name); - } - - const elMyCourse = document.evaluate( - `.//a[contains(text(), "マイコース")]/../../ul`, - elNavigation, - null, - XPathResult.UNORDERED_NODE_ITERATOR_TYPE, - null, - ).iterateNext() as HTMLElement | null; - if (!elMyCourse) { - return; - } - - const elMyCourseItems = elMyCourse.querySelectorAll('li a'); - elMyCourseItems.forEach((elItem) => { - const shortName = elItem.textContent?.trim() ?? ''; - const courseName = courseNameMap.get(shortName); - - if (typeof courseName === 'string') { - elItem.textContent = courseName; - } - }); - }, -}; - -export default replaceNavigationText; diff --git a/src/content_scripts/common/debugMode.ts b/src/content_scripts/common/debugMode.ts deleted file mode 100644 index af7e177..0000000 --- a/src/content_scripts/common/debugMode.ts +++ /dev/null @@ -1,3 +0,0 @@ -const debugMode = true; - -export default debugMode; diff --git a/src/content_scripts/common/loadFeature.ts b/src/content_scripts/common/loadFeature.ts deleted file mode 100644 index 1d7c742..0000000 --- a/src/content_scripts/common/loadFeature.ts +++ /dev/null @@ -1,164 +0,0 @@ -// @deno-types=npm:@types/lodash -import * as lodash from 'lodash'; -import type { Feature, FeatureUniqueName } from '../common/types.ts'; -import { defaultFeatureOption } from '../../common/options.ts'; -import { getOptions } from '../../common/storage/options.ts'; - -/** feature を依存関係に従ってトポロジカルソートする */ -// DFS を用いて探索 -const sortFeatures = function (features: Feature[]) { - // 重複チェック - const featureNames = features.map((feature) => feature.uniqueName); - if (new Set(featureNames).size !== features.length) { - throw Error( - `Multiple features has the same unique name or features duplicated`, - ); - } - - const featureNameMap = new Map(); - for (const feature of features) { - featureNameMap.set(feature.uniqueName, feature); - } - - const visited = new Set(); - const result: Feature[] = []; - const visit = function ( - feature: Feature, - localVisited: Set, - ) { - if (visited.has(feature.uniqueName)) { - return; - } - if (localVisited.has(feature.uniqueName)) { - throw Error( - `Circular dependency detected on resolving feature ${feature.uniqueName}`, - ); - } - - visited.add(feature.uniqueName); - localVisited.add(feature.uniqueName); - - for (const depFeatureName of feature.dependencies ?? []) { - const depFeature = featureNameMap.get(depFeatureName); - if (!depFeature) { - throw Error( - `Feature ${depFeatureName} is not provided to feature loader`, - ); - } - visit(depFeature, localVisited); - } - - result.push(feature); - }; - - for (const feature of features) { - visit(feature, new Set()); - } - - return result; -}; - -/** - * 文字列か正規表現で対象文字列をテストする; - * `test` が文字列の場合は完全一致, 正規表現の場合は `RegExp.test` の結果 - */ -const testByStringOrRegExp = function (test: string | RegExp, target: string) { - if (typeof test === 'string') { - return test === target; - } else if (test instanceof RegExp) { - return test.test(target); - } - return false; -}; - -/** `Feature` を依存関係を解決しながら読み込む */ -const loadFeature = async function ( - features: Feature[], - contextUrl: URL, - showLog = false, -) { - const options = await getOptions(); - const contextHost = contextUrl.hostname; - const contextPath = contextUrl.pathname; - // ここで URL のフィルターをかけたほうが処理量は減るが、 - // 特定のページでのみ依存関係の解決に失敗するとバグの発見がしづらいため - // 実行時に URL をチェックする - const sortedFeatures = sortFeatures(features); - const loaderPromiseMap = new Map>(); - - const rootPromiseEventTarget = new EventTarget(); - const rootPromise = new Promise((resolve) => { - rootPromiseEventTarget.addEventListener('start', () => resolve()); - }); - - for (const feature of sortedFeatures) { - const depFeaturePromises = (feature.dependencies ?? []).map((name) => - loaderPromiseMap.get(name) - ); - if (depFeaturePromises.some((v) => v === undefined)) { - throw Error( - `Failed to resolve feature ${feature.uniqueName}: dependency feature does not exist`, - ); - } - - if (depFeaturePromises.length === 0) { - depFeaturePromises.push(rootPromise); - } - - // 各 Feature を実行する Promise を作成 - loaderPromiseMap.set( - feature.uniqueName, - Promise.all(depFeaturePromises).then(() => { - if (!testByStringOrRegExp(feature.hostnameFilter, contextHost)) { - if (showLog) { - console.log( - `[FeatureLoader] Skipping ${feature.uniqueName}: hostname does not match`, - ); - } - return; - } - if (!testByStringOrRegExp(feature.pathnameFilter, contextPath)) { - if (showLog) { - console.log( - `[FeatureLoader] Skipping ${feature.uniqueName}: pathname does not match`, - ); - } - return; - } - - if (showLog) { - console.log(`[FeatureLoader] Loading ${feature.uniqueName}`); - } - - const option = lodash.defaultsDeep( - options.features[feature.uniqueName], - defaultFeatureOption, - ); - - if (feature.propagateError === false) { - // 失敗しても警告として出力するだけ - return Promise.resolve(feature.loader(option)).catch( - (err: unknown) => { - console.warn( - `Uncaught error in feature loader ${feature.uniqueName}: `, - err, - ); - }, - ); - } - - return Promise.resolve(feature.loader(option)).catch((err: unknown) => { - return Promise.reject(Error( - `Uncaught error in feature loader ${feature.uniqueName}`, - { cause: err }, - )); - }); - }), - ); - } - - rootPromiseEventTarget.dispatchEvent(new CustomEvent('start')); - await Promise.all(loaderPromiseMap.values()); -}; - -export default loadFeature; diff --git a/src/content_scripts/common/types.ts b/src/content_scripts/common/types.ts deleted file mode 100644 index ac38642..0000000 --- a/src/content_scripts/common/types.ts +++ /dev/null @@ -1,21 +0,0 @@ -import { FeatureOption } from '../../common/options.ts'; - -export type FeatureUniqueName = string; - -/** - * 独立した機能を表す - */ -export interface Feature { - /** 機能の一意な名前 */ - uniqueName: FeatureUniqueName; - /** ホスト名がマッチ (または一致) した場合に実行される */ - hostnameFilter: RegExp | string; - /** パス名がマッチ (または一致) した場合に実行される */ - pathnameFilter: RegExp | string; - /** 依存する機能の `uniqueName` */ - dependencies?: FeatureUniqueName[]; - /** 機能の本体 (同期でも非同期でも良い) */ - loader: (options: Required) => void | Promise; - /** エラーを伝播するかどうか (デフォルト: `true`) */ - propagateError?: boolean; -} diff --git a/src/content_scripts/dashboard/dashboard.ts b/src/content_scripts/dashboard/dashboard.ts deleted file mode 100644 index 01bf196..0000000 --- a/src/content_scripts/dashboard/dashboard.ts +++ /dev/null @@ -1,19 +0,0 @@ -import debugMode from '../common/debugMode.ts'; -import loadFeature from '../common/loadFeature.ts'; -import waitForPageLoad from './waitForPageLoad.ts'; -import renderQuickCourseView from './renderQuickCourseView.ts'; -import updateCourseRepository from './updateCourseRepository.ts'; -import addEventsCountdown from './eventsCountdown.ts'; - -globalThis.addEventListener('load', () => { - loadFeature( - [ - waitForPageLoad, - renderQuickCourseView, - updateCourseRepository, - addEventsCountdown, - ], - new URL(location.href), - debugMode, - ); -}); diff --git a/src/content_scripts/dashboard/eventsCountdown.ts b/src/content_scripts/dashboard/eventsCountdown.ts deleted file mode 100644 index a63b335..0000000 --- a/src/content_scripts/dashboard/eventsCountdown.ts +++ /dev/null @@ -1,88 +0,0 @@ -/** @jsxImportSource preact */ - -import type { Feature } from '../common/types.ts'; -import { - EventsCountdownProps, - renderEventsCountdown, -} from './eventsCountdown/EventsCountdown.tsx'; - -type AddEventCountdownOptions = { - enabled: boolean; -}; - -const CalendarLinkDateNumRegExp = /\?.*time=(\d+).*$/; - -/** 直近イベントにカウントダウンを追加 */ -const addEventsCountdown: Feature = { - uniqueName: 'dashboard-events-countdown', - hostnameFilter: 'cms7.ict.nitech.ac.jp', - pathnameFilter: /^\/moodle40a\/my\/(index\.php)?$/, - loader: (options) => { - if (!options.enabled) { - return; - } - - const elUpcomingEvents = document.querySelector( - 'section.block_calendar_upcoming', - ); - if (!elUpcomingEvents) { - return; - } - - const appRoot = document.createElement('div'); - appRoot.id = 'nitech_moodle_ext_events_countdown_root'; - elUpcomingEvents.append(appRoot); - - const elEventItems = Array.from( - elUpcomingEvents.querySelectorAll( - 'div[data-region="event-item"]', - ), - ); - - // 締め切り時間とそれを描画する DOM 要素のリストを作成 - const eventItems: EventsCountdownProps['items'] = []; - for (const elEventItem of elEventItems) { - const elLabel = elEventItem.querySelector('div.date'); - if (!elLabel) { - continue; - } - const elLink = elLabel.querySelector('a'); - if (!elLink) { - continue; - } - // elLink.href の例: - // https://cms7.ict.nitech.ac.jp/moodle40a/calendar/view.php?view=day&time=1680940800 - const dateNumMatch = CalendarLinkDateNumRegExp.exec(elLink.href); - if (!dateNumMatch) { - continue; - } - - const expireDate = new Date(parseInt(dateNumMatch[1]) * 1000); - - const elCountdown = document.createElement('div'); - elCountdown.className = 'nitech-moodle-ext-event-countdown'; - elLabel.appendChild(elCountdown); - - eventItems.push({ - expireDate, - portalTarget: elCountdown, - }); - } - - renderEventsCountdown({ items: eventItems }, appRoot); - - // ツリーが変化した際 (イベントの削除など) にカウントダウンが削除されるため - // ツリーの変更を監視する - const observer = new MutationObserver(() => { - observer.disconnect(); - // `preact.render(...)` ではうまくいかなかった - addEventsCountdown.loader(options); - }); - observer.observe(elUpcomingEvents, { - childList: true, - subtree: true, - }); - }, -}; - -export default addEventsCountdown; diff --git a/src/content_scripts/dashboard/eventsCountdown/EventsCountdown.tsx b/src/content_scripts/dashboard/eventsCountdown/EventsCountdown.tsx deleted file mode 100644 index cd89283..0000000 --- a/src/content_scripts/dashboard/eventsCountdown/EventsCountdown.tsx +++ /dev/null @@ -1,112 +0,0 @@ -/** @jsxImportSource preact */ - -// @deno-types="preact/types" -import * as preact from 'preact'; -import { createPortal, useState } from 'preact/compat'; - -/** @param duration 期間の長さ (秒) */ -const createDurationText = function (duration: number) { - if (duration < 60) { - // 60秒未満 - return `${duration}秒`; - } else if (duration < 3600) { - // 60分未満 - return `${Math.floor(duration / 60)}分`; - } else if (duration < 86400) { - // 24時間未満 - return `${Math.floor(duration / 3600)}時間`; - } else { - return `${Math.floor(duration / 86400)}日`; - } -}; - -/** 次に描画を更新すべき時間を返す */ -const msUntilNextUpdate = function (duration: number, currentTime: number) { - /** 更新間隔 (s) */ - let refreshRate = 1; - - if (duration < 60) { - // 60秒未満; 秒単位で更新 - refreshRate = 1; - } else if (duration < 3600) { - // 60分未満; 分単位で更新 - refreshRate = 60; - } else if (duration < 86400) { - // 24時間未満; 時間単位で更新 - refreshRate = 3600; - } else { - // 24時間以上; 日単位で更新 - refreshRate = 86400; - } - - const refreshRateMs = refreshRate * 1000; - - return refreshRateMs - currentTime % refreshRateMs; -}; - -interface CountdownProps { - expireDate: Date; - portalTarget: HTMLElement; -} - -const Countdown = (props: CountdownProps) => { - const [currentTime, setCurrentTime] = useState(Date.now()); - - const duration = Math.floor( - (props.expireDate.getTime() - currentTime) / 1000, - ); - setTimeout( - () => setCurrentTime(Date.now()), - msUntilNextUpdate(Math.abs(duration), currentTime), - ); - - let className = ''; - if (duration < 0) { - className = 'exceeded text-muted'; - } else if (duration < 86400) { - className = 'imminent red'; - } - - return createPortal( - ( - - {duration >= 0 - ? `残り${createDurationText(duration)}` - : `${createDurationText(-duration)}前`} - - ), - props.portalTarget, - ); -}; - -interface EventsCountdownProps { - items: { - expireDate: Date; - portalTarget: HTMLElement; - }[]; -} - -const EventsCountdown = (props: EventsCountdownProps) => ( - <> - {props.items.map((item) => ( - - ))} - -); - -const renderEventsCountdown = function ( - props: EventsCountdownProps, - targetElement: HTMLElement, -) { - preact.render( - , - targetElement, - ); -}; - -export { renderEventsCountdown }; - -export type { EventsCountdownProps }; diff --git a/src/content_scripts/dashboard/quickCourseView.scss b/src/content_scripts/dashboard/quickCourseView.scss deleted file mode 100644 index beff657..0000000 --- a/src/content_scripts/dashboard/quickCourseView.scss +++ /dev/null @@ -1,5 +0,0 @@ -#moodle_ext_quick_course_view { - li a { - display: block; - } -} \ No newline at end of file diff --git a/src/content_scripts/dashboard/quickCourseView/CourseItem.tsx b/src/content_scripts/dashboard/quickCourseView/CourseItem.tsx deleted file mode 100644 index 63fc623..0000000 --- a/src/content_scripts/dashboard/quickCourseView/CourseItem.tsx +++ /dev/null @@ -1,33 +0,0 @@ -/** @jsxImportSource preact */ - -// @deno-types="preact/types" -import * as preact from 'preact'; -import { Course } from '../../../common/course.ts'; -import { weekOfDayMap } from './defs.ts'; - -const CourseItem = ( - props: { course: Course }, -) => { - const course = props.course; - if (course.type === 'regular-lecture') { - return ( - - {weekOfDayMap[course.weekOfDay]} {course.period[0]}-{course.period[1]}限 - {' '} - {course.name} - - ); - } else { - return ( - - {course.name} - - ); - } -}; - -export default CourseItem; diff --git a/src/content_scripts/dashboard/quickCourseView/ListGroup.tsx b/src/content_scripts/dashboard/quickCourseView/ListGroup.tsx deleted file mode 100644 index 79e69a1..0000000 --- a/src/content_scripts/dashboard/quickCourseView/ListGroup.tsx +++ /dev/null @@ -1,21 +0,0 @@ -/** @jsxImportSource preact */ - -// @deno-types="preact/types" -import * as preact from 'preact'; - -const ListGroup = ( - props: { items: { key: string; element: preact.ComponentChild }[] }, -) => ( -
      - {props.items.map((item) => ( -
    • - {item.element} -
    • - ))} -
    -); - -export default ListGroup; diff --git a/src/content_scripts/dashboard/quickCourseView/QuickCourseView.tsx b/src/content_scripts/dashboard/quickCourseView/QuickCourseView.tsx deleted file mode 100644 index e5de3e3..0000000 --- a/src/content_scripts/dashboard/quickCourseView/QuickCourseView.tsx +++ /dev/null @@ -1,178 +0,0 @@ -/** @jsxImportSource preact */ - -// @deno-types="preact/types" -import * as preact from 'preact'; -import * as hooks from 'preact/hooks'; -import { Course, RegularLectureCourse } from '../../../common/course.ts'; -import QuickCourseViewControl from './QuickCourseViewControl.tsx'; -import QuickCourseViewBody from './QuickCourseViewBody.tsx'; -import { - Filter, - semesterMap, - semesterOrdering, - weekOfDayOrdering, -} from './defs.ts'; - -const filterCourse = function (courses: Course[], filter: string) { - if (filter === 'all') { - return courses; - } - - const [yearStr, semester] = filter.split('-'); - const year = parseInt(yearStr); - const yearFiltered = courses.filter((course) => course.fullYear === year); - - if (typeof semester !== 'string') { - return yearFiltered; - } - - // TODO: 範囲に共通部分がある場合 (前期と通年, 前期とQ1など) もマッチさせる - - return yearFiltered - .filter((course) => - course.type === 'regular-lecture' && course.semester === semester - ); -}; - -const compareCourse = function (course1: Course, course2: Course) { - if ( - course1.type === 'regular-lecture' && course2.type === 'regular-lecture' - ) { - // 通常の講義 - if (course1.fullYear !== course2.fullYear) { - return course1.fullYear - course2.fullYear; - } - if (course1.semester !== course2.semester) { - return semesterOrdering[course1.semester] - - semesterOrdering[course2.semester]; - } - if (course1.weekOfDay !== course2.weekOfDay) { - return weekOfDayOrdering[course1.weekOfDay] - - weekOfDayOrdering[course2.weekOfDay]; - } - const periodDiff = (course1.period[1] - course2.period[1]) * 10 - - (course1.period[0] - course2.period[0]); - if (periodDiff !== 0) { - return periodDiff; - } - - return course1.code - course2.code; - } else if (course1.type === 'special' && course2.type === 'special') { - // 特殊な講義 - if (course1.fullYear !== course2.fullYear) { - return (course1.fullYear ?? 10000) - (course2.fullYear ?? 10000); - } - - return course1.fullName < course2.fullName ? -1 : 1; - } else if (course1.type !== 'regular-lecture') { - // 通常の講義を優先する - return 1; - } else if (course2.type !== 'regular-lecture') { - // 通常の講義を優先する - return -1; - } - - return 0; -}; - -const QuickCourseView = (props: { courses: Course[] }) => { - const courses = props.courses; - const regularCourses = courses - .filter((course) => - course.type === 'regular-lecture' - ) as RegularLectureCourse[]; - - const date = new Date(); - const shiftedDate = new Date( - date.getFullYear(), - date.getMonth() - 3, - date.getDate(), - ); - // 現在の年と前期/後期のフィルターを最初に選択する - const [filter, setFilter] = hooks.useState( - `${shiftedDate.getFullYear()}-${ - shiftedDate.getMonth() < 6 ? '1/2' : '2/2' - }`, - ); - - const filters = [ - { display: 'すべて', value: 'all' }, - ]; - // 年, 学期の組によるフィルター - const semestersMap = new Map< - string, - [RegularLectureCourse['fullYear'], RegularLectureCourse['semester']] - >(); - regularCourses.forEach((course) => { - semestersMap.set(`${course.fullYear}-${course.semester}`, [ - course.fullYear, - course.semester, - ]); - }); - semestersMap.set( - `${shiftedDate.getFullYear()}-${ - shiftedDate.getMonth() < 6 ? '1/2' : '2/2' - }`, - [shiftedDate.getFullYear(), shiftedDate.getMonth() < 6 ? '1/2' : '2/2'], - ); - const semesters = Array.from(semestersMap.values()).sort((a, b) => { - if (a[0] !== b[0]) { - return a[0] - b[0]; - } - return semesterOrdering[a[1]] - semesterOrdering[b[1]]; - }); - // 年だけのフィルター - const years = Array.from( - new Set(regularCourses.map((course) => course.fullYear)), - ).sort(); - - semesters.forEach(([year, semester]) => { - filters.push({ - display: `${year}年 ${semesterMap[semester]}`, - value: `${year}-${semester}`, - }); - }); - years.forEach((year) => { - filters.push({ - display: `${year}年`, - value: `${year}`, - }); - }); - - return ( -
    -
    コースリンク
    -
    -
    -
    - - -
    -
    -
    -
    - ); -}; - -const renderQuickCourseView = function ( - courses: Course[], - targetElement: HTMLElement, -) { - preact.render( - , - targetElement, - ); -}; - -export { renderQuickCourseView }; diff --git a/src/content_scripts/dashboard/quickCourseView/QuickCourseViewBody.tsx b/src/content_scripts/dashboard/quickCourseView/QuickCourseViewBody.tsx deleted file mode 100644 index 3160847..0000000 --- a/src/content_scripts/dashboard/quickCourseView/QuickCourseViewBody.tsx +++ /dev/null @@ -1,39 +0,0 @@ -/** @jsxImportSource preact */ - -// @deno-types="preact/types" -import * as preact from 'preact'; -import { Course } from '../../../common/course.ts'; -import ListGroup from './ListGroup.tsx'; -import CourseItem from './CourseItem.tsx'; - -const QuickCourseViewBody = (props: { courses: Course[] }) => ( -
    -
    -
    -
    -
    - ({ - key: course.fullName, - element: , - }))} - /> -
    -
    -
    -
    -
    -
    -); - -export default QuickCourseViewBody; diff --git a/src/content_scripts/dashboard/quickCourseView/QuickCourseViewControl.tsx b/src/content_scripts/dashboard/quickCourseView/QuickCourseViewControl.tsx deleted file mode 100644 index 5fc6bcc..0000000 --- a/src/content_scripts/dashboard/quickCourseView/QuickCourseViewControl.tsx +++ /dev/null @@ -1,65 +0,0 @@ -/** @jsxImportSource preact */ - -// @deno-types="preact/types" -import * as preact from 'preact'; -import * as hooks from 'preact/hooks'; -import { Filter } from './defs.ts'; - -const QuickCourseViewControl = ( - props: { - filterDisplay: string; - filterValue: Filter; - setFilter: hooks.StateUpdater; - filterList: { display: string; value: Filter }[]; - }, -) => ( -
    -
    - - -
    -
    -); - -export default QuickCourseViewControl; diff --git a/src/content_scripts/dashboard/quickCourseView/defs.ts b/src/content_scripts/dashboard/quickCourseView/defs.ts deleted file mode 100644 index 00d2bba..0000000 --- a/src/content_scripts/dashboard/quickCourseView/defs.ts +++ /dev/null @@ -1,8 +0,0 @@ -export type Filter = string; - -export { - semesterOrdering, - semesterToTextMap as semesterMap, - weekOfDayOrdering, - weekOfDayToTextMap as weekOfDayMap, -} from '../../../common/course.ts'; diff --git a/src/content_scripts/dashboard/renderQuickCourseView.ts b/src/content_scripts/dashboard/renderQuickCourseView.ts deleted file mode 100644 index 783b131..0000000 --- a/src/content_scripts/dashboard/renderQuickCourseView.ts +++ /dev/null @@ -1,38 +0,0 @@ -import updateCourseRepository from './updateCourseRepository.ts'; -import type { Feature } from '../common/types.ts'; -import { renderQuickCourseView as renderQuickCourseViewElement } from './quickCourseView/QuickCourseView.tsx'; -import { getCourses } from '../../common/storage/course.ts'; - -const renderQuickCourseView: Feature = { - uniqueName: 'dashboard-quick-course-view', - hostnameFilter: 'cms7.ict.nitech.ac.jp', - pathnameFilter: /^\/moodle40a\/my\/(index\.php)?$/, - dependencies: [updateCourseRepository.uniqueName], - loader: (options) => { - if (!options.enabled) { - return; - } - - return new Promise((resolve, reject) => { - const cardBlock = document.querySelector('aside#block-region-content'); - if (cardBlock === null) { - reject( - `[${renderQuickCourseView.uniqueName}]: Cannot find element to render quick course view in.`, - ); - return; - } - - const wrapperSection = document.createElement('section'); - wrapperSection.className = 'block_quickcourseview block card mb-3'; - wrapperSection.dataset['block'] = 'quickcourseview'; - cardBlock.insertBefore(wrapperSection, cardBlock.childNodes?.[0] ?? null); - - getCourses().then((courses) => { - renderQuickCourseViewElement(courses, wrapperSection); - resolve(); - }); - }); - }, -}; - -export default renderQuickCourseView; diff --git a/src/content_scripts/dashboard/updateCourseRepository.ts b/src/content_scripts/dashboard/updateCourseRepository.ts deleted file mode 100644 index 9a1236a..0000000 --- a/src/content_scripts/dashboard/updateCourseRepository.ts +++ /dev/null @@ -1,133 +0,0 @@ -import { Course, RegularLectureCourse } from '../../common/course.ts'; -import { storeCourseByMerge } from '../../common/storage/course.ts'; -import type { Feature } from '../common/types.ts'; -import waitForPageLoad from './waitForPageLoad.ts'; -import { textToSemesterMap, textToWeekOfDayMap } from '../../common/course.ts'; - -const regularCourseRegExp = - /^(.+)\s*(\d{4})(\d)(\d{4})\s*(前期|後期|第(?:1|2|3|4)クォーター)\s*((?:日|月|火|水|木|金|土)曜)\s*(\d\d?)-(\d\d?)限.*$/; - -interface DecodedLectureCourse { - type: RegularLectureCourse['type']; - name: RegularLectureCourse['name']; - fullName: RegularLectureCourse['fullName']; - fullYear: RegularLectureCourse['fullYear']; - curriculumPart: RegularLectureCourse['curriculumPart']; - code: RegularLectureCourse['code']; - semester: RegularLectureCourse['semester']; - weekOfDay: RegularLectureCourse['weekOfDay']; - period: RegularLectureCourse['period']; -} - -const convertFullWidthToHalfWidth = function (input: string): string { - return input.replace( - /[0-9]/g, - (s) => String.fromCharCode(s.charCodeAt(0) - 0xFEE0), - ); -}; - -const decodeRegularLectureCourseText = function ( - text: string, -): DecodedLectureCourse { - const match = regularCourseRegExp.exec(convertFullWidthToHalfWidth(text)); - if (match === null) { - throw Error(`"${text}" does not matches to the pattern`); - } - - const curriculumPart = parseInt(match[3]); - if (curriculumPart !== 1 && curriculumPart !== 2 && curriculumPart !== 4) { - throw Error(`${curriculumPart} is not a valid curriculum part`); - } - if (!(match[5] in textToSemesterMap)) { - throw Error(`${match[5]} is not a valid semester`); - } - const semester = - textToSemesterMap[match[5] as keyof typeof textToSemesterMap]; - if (!(match[6] in textToWeekOfDayMap)) { - throw Error(`${match[6]} is not a valid week of day`); - } - const weekOfDay = - textToWeekOfDayMap[match[6] as keyof typeof textToWeekOfDayMap]; - - return { - type: 'regular-lecture', - name: match[1].trim(), - fullName: text, - fullYear: parseInt(match[2]), - curriculumPart, - code: parseInt(match[4]), - semester, - weekOfDay, - period: [parseInt(match[7]), parseInt(match[8])], - }; -}; - -const pageLinkIdRegExp = /id=(\d+)/; -/** コースのリストを「コース概要」のセクションから - * 読み取り、ストレージに保存する */ -const updateCourseRepository: Feature = { - uniqueName: 'dashboard-update-course-repository', - hostnameFilter: 'cms7.ict.nitech.ac.jp', - pathnameFilter: /^\/moodle40a\/my\/(index\.php)?$/, - dependencies: [waitForPageLoad.uniqueName], - loader: async (options) => { - if (!options.enabled) { - return; - } - - const thisYear = new Date().getFullYear(); - const thisYearStr = `${thisYear}`; - const elMyOverview = document.querySelector('section.block_myoverview'); - if (!elMyOverview) { - throw Error( - `[${updateCourseRepository.uniqueName}] Failed to get "my overview" section`, - ); - } - - // コース表示はマルチページになっている - // 実際になっている様子を確認していないのでちゃんと動くかは要検証 - const courses: Course[] = []; - const elItemLinks = Array.from( - elMyOverview.querySelectorAll('ul.list-group li a.aalink.coursename'), - ) as HTMLAnchorElement[]; - for (const elItemLink of elItemLinks) { - const search = new URL(elItemLink.href).search; - const pageIdMatch = pageLinkIdRegExp.exec(search); - const pageId = parseInt(pageIdMatch?.[1] ?? '0'); - const elShortName = elItemLink.previousElementSibling?.querySelector( - 'div > div', - ); - - if (!elShortName) { - continue; - } - const shortName = elShortName.textContent ?? ''; - - const text = Array.from(elItemLink.childNodes) - .filter((v) => v.nodeType === 3) - .map((v) => v.textContent) - .join('') - .trim(); - try { - courses.push({ - ...decodeRegularLectureCourseText(text), - pageId, - shortName, - }); - } catch { - courses.push({ - type: 'special', - name: text, - fullName: text, - fullYear: text.includes(thisYearStr) ? thisYear : undefined, - pageId, - shortName, - }); - } - } - - await storeCourseByMerge(courses); - }, -}; - -export default updateCourseRepository; diff --git a/src/content_scripts/dashboard/waitForPageLoad.ts b/src/content_scripts/dashboard/waitForPageLoad.ts deleted file mode 100644 index 48c2f9d..0000000 --- a/src/content_scripts/dashboard/waitForPageLoad.ts +++ /dev/null @@ -1,51 +0,0 @@ -// @deno-types=npm:@types/lodash -import * as lodash from 'lodash'; -import type { Feature } from '../common/types.ts'; - -const defaultOption = { - enabled: true, - timeout: 5000, -}; - -/** ダッシュボードの内容が読み込まれるまで待つ */ -const waitForPageLoad: Feature = { - uniqueName: 'dashboard-wait-for-page-load', - hostnameFilter: 'cms7.ict.nitech.ac.jp', - pathnameFilter: /^\/moodle40a\/my\/(index\.php)?$/, - propagateError: false, - loader: (options_) => { - if (!options_.enabled) { - return; - } - const options = lodash.merge(options_, defaultOption); - - return new Promise((resolve, reject) => { - const startTime = Date.now(); - - const checkPageContent = () => { - const loadingContent = document.querySelector( - 'section.block_myoverview div[data-region="paged-content-page"]', - ); - - if (loadingContent !== null) { - // この時点ではまだコース一覧が読み込まれていないため待つ - // Mutation observer とかでうまくいくかもしれないけれど - // コース数が0だと更新が入らなくて動かないかも? - setTimeout(resolve, 1000); - } else { - const timePassed = Date.now() - startTime; - - if (timePassed > options.timeout) { - reject(Error(`[${waitForPageLoad.uniqueName}] timeout`)); - } else { - setTimeout(checkPageContent, 100); - } - } - }; - - checkPageContent(); - }); - }, -}; - -export default waitForPageLoad; diff --git a/src/content_scripts/scorm/collapseToc.ts b/src/content_scripts/scorm/collapseToc.ts deleted file mode 100644 index 26dc870..0000000 --- a/src/content_scripts/scorm/collapseToc.ts +++ /dev/null @@ -1,29 +0,0 @@ -import type { Feature } from '../common/types.ts'; - -/** TOCを折りたたむ */ -const collapseToc: Feature = { - uniqueName: 'scorm-collapse-toc', - hostnameFilter: 'cms7.ict.nitech.ac.jp', - pathnameFilter: /^\/moodle40a\/mod\/scorm/, - loader: (options) => { - if (!options.enabled) { - return; - } - - const elScormToc = document.getElementById('scorm_toc'); - console.log('EXT: ', elScormToc); - - if (!elScormToc) { - return; - } - if (elScormToc.classList.contains('disabled')) { - // すでに折りたたまれている - return; - } - - const elTocToggleButton = document.getElementById('scorm_toc_toggle_btn'); - elTocToggleButton?.click(); - }, -}; - -export default collapseToc; diff --git a/src/content_scripts/scorm/scorm.ts b/src/content_scripts/scorm/scorm.ts deleted file mode 100644 index b5de336..0000000 --- a/src/content_scripts/scorm/scorm.ts +++ /dev/null @@ -1,13 +0,0 @@ -import debugMode from '../common/debugMode.ts'; -import loadFeature from '../common/loadFeature.ts'; -import collapseToc from './collapseToc.ts'; - -globalThis.addEventListener('load', () => { - loadFeature( - [ - collapseToc, - ], - new URL(location.href), - debugMode, - ); -}); diff --git a/src/manifest.json5 b/src/manifest.json5 deleted file mode 100644 index 5b55884..0000000 --- a/src/manifest.json5 +++ /dev/null @@ -1,69 +0,0 @@ -{ - // Required - manifest_version: 3, - name: "NITech Moodle Extension (40a)", - homepage_url: "https://github.com/nitech-create/nitech-moodle-extension-40a", - version: "0.9.6", - - // Recommended - // action: {}, - // default_locale: "ja", - description: "名古屋工業大学の Moodle (4.0) を使いやすくするChrome拡張機能です。情報基盤センターとは無関係で非公式なものであり、また問題が起きても責任は取れません。Web Extension for Moodle 4.0 of NITech.", - // icons: {}, - - // Optional - author: "nitech Create", - // background: {} - content_scripts: [ - { - // all pages - matches: ["https://cms7.ict.nitech.ac.jp/moodle40a/*/*"], - js: ["content_scripts/allPages/allPages.js"], - }, - { - // dashboard - matches: [ - "https://cms7.ict.nitech.ac.jp/moodle40a/my/", - "https://cms7.ict.nitech.ac.jp/moodle40a/my/index.php", - ], - js: ["content_scripts/dashboard/dashboard.js"], - css: ["content_scripts/dashboard/dashboard.css"], - }, - { - // video page - matches: [ - "https://cms7.ict.nitech.ac.jp/moodle40a/mod/scorm/*", - "https://cms7.ict.nitech.ac.jp/moodle40a/mod/scorm/*/*", - ], - js: ["content_scripts/scorm/scorm.js"], - }, - { - // scorm content page - matches: [ - "https://cms7.ict.nitech.ac.jp/moodle40a/pluginfile.php/*/mod_scorm/content/*/*", - ], - all_frames: true, - css: ["content_scripts/scorm/scorm.css"], - }, - ], - options_ui: { - page: "options/options.html", - open_in_tab: true, - browser_style: true, - // 本当は存在しないフィールド, ビルドのために追加 - js: ["options/options.js"], - css: ["options/options.css"], - }, - content_security_policy: { - extension_pages: "script-src 'self'; object-src 'self';", - }, - permissions: ["storage"], - host_permissions: ["https://cms7.ict.nitech.ac.jp/moodle40a/*/*"], - web_accessible_resources: [ - // source maps - { - resources: ["content_scripts/*/*.map"], - matches: [""], - }, - ], -} diff --git a/src/manifest.jsonc b/src/manifest.jsonc new file mode 100644 index 0000000..882c3e0 --- /dev/null +++ b/src/manifest.jsonc @@ -0,0 +1,82 @@ +{ + // Required + "manifest_version": 3, + "name": "NITech Moodle Extension (40a)", + "homepage_url": "https://github.com/nitech-create/nitech-moodle-extension-40a", + "version": "0.10.0", + + // Recommended + // "action": {}, + // "default_locale": "ja", + "description": "名古屋工業大学の Moodle (4.0) を使いやすくするChrome拡張機能です。情報基盤センターとは無関係で非公式なものであり、また問題が起きても責任は取れません。Web Extension for Moodle 4.0 of NITech.", + // "icons": {}, + + // Optional + "author": "nitech Create", + + "content_scripts": [ + // all pages + { + "matches": ["https://cms7.ict.nitech.ac.jp/moodle40a/*/*"], + "js": [ + "./contentScripts/all/startMessage/index.ts", + "./contentScripts/all/removeForceDownload/index.ts", + "./contentScripts/all/replaceBreadcrumbCourseName/index.ts", + "./contentScripts/all/replaceNavigationCourseName/index.ts" + ] + }, + // dashboard + { + "matches": [ + "https://cms7.ict.nitech.ac.jp/moodle40a/my/", + "https://cms7.ict.nitech.ac.jp/moodle40a/my/index.php" + ], + "js": [ + "./contentScripts/dashboard/readAndStoreCourses/index.ts", + "./contentScripts/dashboard/quickCourseLinks/index.ts", + "./contentScripts/dashboard/eventCountdown/index.ts" + ], + "css": ["./contentScripts/dashboard/main.scss"] + }, + // my course config page + { + "matches": ["https://cms7.ict.nitech.ac.jp/moodle40a/my/courses.php"], + "js": [ + // this is **a special case** to use script in other directory + "./contentScripts/dashboard/readAndStoreCourses/index.ts" + ] + }, + // scorm page + { + "matches": [ + "https://cms7.ict.nitech.ac.jp/moodle40a/mod/scorm/*", + "https://cms7.ict.nitech.ac.jp/moodle40a/mod/scorm/*/*" + ], + "js": ["./contentScripts/scorm/autoCollapseToc/index.ts"] + }, + { + "matches": [ + "https://cms7.ict.nitech.ac.jp/moodle40a/pluginfile.php/*/mod_scorm/content/*/*" + ], + "all_frames": true, + "css": ["./contentScripts/scormContents/main.scss"] + } + ], + + "options_ui": { + "page": "./options/index.html", + "open_in_tab": true, + "browser_style": true, + // this field is not exist in the manifest file definition + // /added for the build process + "js": ["./options/index.ts"], + "css": ["./options/index.scss"] + }, + + "content_security_policy": { + "extension_pages": "script-src 'self'; object-src 'self';" + }, + + "permissions": ["storage"], + "host_permissions": ["https://cms7.ict.nitech.ac.jp/moodle40a/*/*"] +} diff --git a/src/manifestType.ts b/src/manifestType.ts deleted file mode 100644 index 6c60ae5..0000000 --- a/src/manifestType.ts +++ /dev/null @@ -1,35 +0,0 @@ -type RunAt = 'document_start' | 'document_end' | 'document_idle'; - -interface ContentScript { - matches: string[]; - js?: string[]; - css?: string[]; - run_at: RunAt; - match_about_blank: boolean; - match_origin_as_fallback: boolean; -} - -export default interface ManifestType { - manifest_version: 3; - name: string; - version: string; - - description: string; - - author: string; - content_scripts: ContentScript[]; - content_security_policy: { - extension_pages?: string; - sandbox?: string; - }; - options_ui: { - page: string; - open_in_tab: boolean; - browser_style: boolean; - js?: string[]; - css?: string[]; - }; - - permissions: string[]; - host_permissions: string[]; -} diff --git a/src/options/app/App.tsx b/src/options/app/App.tsx deleted file mode 100644 index f990bca..0000000 --- a/src/options/app/App.tsx +++ /dev/null @@ -1,57 +0,0 @@ -/** @jsxImportSource preact */ - -// @deno-types="preact/types" -import * as preact from 'preact'; -import * as hooks from 'preact/hooks'; -// @deno-types=npm:@types/lodash -import * as lodash from 'lodash'; -import FeatureOptions from './FeatureOptions.tsx'; - -import { - getOptions, - storeOptionsByMerge, -} from '../../common/storage/options.ts'; -import { FeatureOption, Options } from '../../common/options.ts'; - -interface AppProps { - initialOptions: Options; -} - -const App = (props: AppProps) => { - const [options, setOptionsRaw] = hooks.useState(props.initialOptions); - - hooks.useEffect(() => { - getOptions().then(setOptionsRaw); - }); - const setFeatureOptions = ( - key: keyof Options['features'], - value: Partial, - ) => { - const newPartialOption = lodash.defaultsDeep( - value, - options.features[key], - ) as FeatureOption; - const newOptions = lodash.merge(options, { - features: { [key]: newPartialOption }, - }) as Options; - setOptionsRaw(newOptions); - storeOptionsByMerge(newOptions); - }; - - return ( - <> - - - ); -}; - -const renderApp = function (targetElement: HTMLElement) { - getOptions().then((options) => { - preact.render(, targetElement); - }); -}; - -export { renderApp }; diff --git a/src/options/app/FeatureOptions.tsx b/src/options/app/FeatureOptions.tsx deleted file mode 100644 index 2b51517..0000000 --- a/src/options/app/FeatureOptions.tsx +++ /dev/null @@ -1,48 +0,0 @@ -/** @jsxImportSource preact */ - -// @deno-types="preact/types" -import * as preact from 'preact'; -import { FeatureOption, Options } from '../../common/options.ts'; -import optionText from './optionText.json' assert { type: 'json' }; - -import ToggleBox from './ToggleBox.tsx'; - -interface FeatureOptionsProps { - options: Options['features']; - setOptions: ( - key: keyof Options['features'], - value: Partial, - ) => void; -} - -const FeatureOptions = (props: FeatureOptionsProps) => { - const featureUniqueNames = Object.keys(props.options) - .filter(( - uniqName, - ) => (uniqName in optionText.features)) as (keyof typeof optionText[ - 'features' - ])[]; - - return ( -
    -

    機能設定

    - -
      - {featureUniqueNames.map((uniqueName) => ( - { - props.setOptions(uniqueName, { - enabled: !props.options[uniqueName].enabled, - }); - }} - /> - ))} -
    -
    - ); -}; - -export default FeatureOptions; diff --git a/src/options/app/ToggleBox.tsx b/src/options/app/ToggleBox.tsx deleted file mode 100644 index cff5532..0000000 --- a/src/options/app/ToggleBox.tsx +++ /dev/null @@ -1,32 +0,0 @@ -/** @jsxImportSource preact */ - -// @deno-types="preact/types" -import * as preact from 'preact'; - -interface ToggleBoxProps { - uniqueId: string; - labelText: string; - checked: boolean; - onClick: () => void; -} - -const ToggleBox = (props: ToggleBoxProps) => ( -
  • -
    - -
    -
    - -
    -
  • -); - -export default ToggleBox; diff --git a/src/options/app/optionText.json b/src/options/app/optionText.json deleted file mode 100644 index 2b4d304..0000000 --- a/src/options/app/optionText.json +++ /dev/null @@ -1,22 +0,0 @@ -{ - "features": { - "all-pages-remove-force-download": { - "_category": "強制ダウンロードリンクの非強制化" - }, - "all-pages-replace-header-course-name": { - "_category": "ヘッダーの短縮コース名の置き換え" - }, - "all-pages-replace-navigation-texts": { - "_category": "ナビゲーションの短縮コース名の置き換え" - }, - "dashboard-events-countdown": { - "_category": "ダッシュボードの直近イベントにカウントダウンを表示" - }, - "dashboard-quick-course-view": { - "_category": "ダッシュボードにコースへのショートカットを追加" - }, - "scorm-collapse-toc": { - "_category": "動画の目次を折りたたむ" - } - } -} diff --git a/src/options/components/EditPreferences.tsx b/src/options/components/EditPreferences.tsx new file mode 100644 index 0000000..119bc3f --- /dev/null +++ b/src/options/components/EditPreferences.tsx @@ -0,0 +1,145 @@ +import * as preact from "preact"; + +import { usePreferences } from "../hooks/usePreferences.ts"; + +const PreferencesItem = function ( + props: { + checked: boolean; + onClick: preact.JSX.MouseEventHandler; + children: preact.ComponentChildren; + }, +) { + return ( +
  • + +
  • + ); +}; + +export const EditPreferences = function () { + const preferences = usePreferences(); + + if (!preferences) { + return
    loading...
    ; + } + + return ( + <> +
    +

    全てのページ

    +
      + + preferences.setRemoveForceDownload({ + enabled: e.currentTarget.checked, + })} + > +

      + リンクの強制ダウンロードを無効化する +

      +

      + {/* 改行が気持ち悪いが formatter のバグで無視できないので一旦放置 */} + moodle では PDF + などのリンクに強制的にダウンロードさせるように設定することができます。 + この設定により PDF + などがブラウザの規定の動作で開かずダウンロードされてしまうのを防ぎます。 +

      +
      + + preferences.setReplaceBreadcrumbCourseName({ + enabled: e.currentTarget.checked, + })} + > +

      + ヘッダーナビゲーションのコース名をわかりやすい名前にする +

      +

      + 各ページのヘッダーにある現在のページの位置を表すリンクのテキストをわかりやすい名前に置き換えます。 +

      +
      + + preferences.setReplaceNavigationCourseName({ + enabled: e.currentTarget.checked, + })} + > +

      + ナビゲーションのコース名をわかりやすい名前にする +

      +

      + 各ページにあるナビゲーションメニューにおいて、コースリンクのテキストをわかりやすい名前に置き換えます。 +

      +
      +
    +
    + +
    +

    ダッシュボード

    +
      + + preferences.setDashboardEventsCountdown({ + enabled: e.currentTarget.checked, + })} + > +

      + 直近イベントに残り時間を表示する +

      +

      + ダッシュボードにある課題などが表示される「直近イベント」において、表示されているイベントの残り時間を表示します。 + この残り時間はリアルタイムでカウントダウンされます + (表示中に終了時刻が変わった場合はページの再読み込みが必要です)。 +

      +
      + + preferences.setDashboardQuickCourseLinks({ + enabled: e.currentTarget.checked, + })} + > +

      + 開講中のコースにアクセスしやすくするメニューを表示する +

      +

      + ダッシュボードに現在開講しているコースを曜日・時間順に表示するクイックメニューを表示します。 +

      +
      +
    +
    + +
    +

    動画ページ

    +
      + + preferences.setScormAutoCollapseToc({ + enabled: e.currentTarget.checked, + })} + > +

      + ページを開いたときに目次を折りたたむ +

      +

      + 動画ページに表示される目次をデフォルトで折りたたみます。 + 画面上により大きく動画を表示することができます。 +

      +
      +
    +
    + + ); +}; diff --git a/src/options/hooks/usePreferences.ts b/src/options/hooks/usePreferences.ts new file mode 100644 index 0000000..6cffd05 --- /dev/null +++ b/src/options/hooks/usePreferences.ts @@ -0,0 +1,117 @@ +import { useCallback, useEffect, useMemo, useState } from "preact/hooks"; + +import type { Preferences } from "~/common/model/preferences.ts"; +import type { PreferencesAction } from "~/common/storage/preferences/action.ts"; +import { + getPreferences, + reduceAndSavePreferences, +} from "~/common/storage/preferences/index.ts"; + +type Payload = + (PreferencesAction & { type: T })["payload"]; + +export const usePreferences = function () { + const [preferences, setPreferences] = useState(null); + + useEffect(() => { + getPreferences().then(setPreferences); + }, []); + + if (!preferences) return null; + + const createReduceCallback = function ( + type: T, + ) { + return async (payload: Payload) => { + const newPreferences = await reduceAndSavePreferences({ type, payload }); + setPreferences(newPreferences); + }; + }; + + const removeForceDownload = useMemo( + () => preferences.removeForceDownload, + [preferences], + ); + const setRemoveForceDownload = useCallback( + createReduceCallback("patchRemoveForceDownload"), + [preferences], + ); + + const replaceBreadcrumbCourseName = useMemo( + () => preferences.replaceBreadcrumbCourseName, + [preferences], + ); + const setReplaceBreadcrumbCourseName = useCallback( + createReduceCallback("patchReplaceBreadcrumbCourseName"), + [preferences], + ); + + const replaceNavigationCourseName = useMemo( + () => preferences.replaceNavigationCourseName, + [preferences], + ); + const setReplaceNavigationCourseName = useCallback( + createReduceCallback("patchReplaceNavigationCourseName"), + [preferences], + ); + + const dashboardEventsCountdown = useMemo( + () => preferences.dashboardEventsCountdown, + [preferences], + ); + const setDashboardEventsCountdown = useCallback( + createReduceCallback("patchDashboardEventsCountdown"), + [preferences], + ); + + const dashboardQuickCourseLinks = useMemo( + () => preferences.dashboardQuickCourseLinks, + [preferences], + ); + const setDashboardQuickCourseLinks = useCallback( + createReduceCallback("patchDashboardQuickCourseLinks"), + [preferences], + ); + + const scormAutoCollapseToc = useMemo( + () => preferences.scormAutoCollapseToc, + [preferences], + ); + const setScormAutoCollapseToc = useCallback( + createReduceCallback("patchScormAutoCollapseToc"), + [preferences], + ); + + const scormAutoPlay = useMemo(() => preferences.scormAutoPlay, [preferences]); + const setScormAutoPlay = useCallback( + createReduceCallback("patchScormAutoPlay"), + [preferences], + ); + + const loginAutoSubmit = useMemo(() => preferences.loginAutoSubmit, [ + preferences, + ]); + const setLoginAutoSubmit = useCallback( + createReduceCallback("patchLoginAutoSubmit"), + [preferences], + ); + + return { + removeForceDownload, + setRemoveForceDownload, + replaceBreadcrumbCourseName, + setReplaceBreadcrumbCourseName, + replaceNavigationCourseName, + setReplaceNavigationCourseName, + dashboardEventsCountdown, + setDashboardEventsCountdown, + dashboardQuickCourseLinks, + setDashboardQuickCourseLinks, + scormAutoCollapseToc, + setScormAutoCollapseToc, + scormAutoPlay, + setScormAutoPlay, + loginAutoSubmit, + setLoginAutoSubmit, + }; +}; diff --git a/src/options/index.html b/src/options/index.html new file mode 100644 index 0000000..a0553b8 --- /dev/null +++ b/src/options/index.html @@ -0,0 +1,20 @@ + + + + + + + + 設定 - NITech Moodle Extension (40a) + + + + + +
    +

    設定 - NITech Moodle Extension (40a)

    + +
    +
    + + diff --git a/src/options/index.scss b/src/options/index.scss new file mode 100644 index 0000000..66a8fd9 --- /dev/null +++ b/src/options/index.scss @@ -0,0 +1,71 @@ +html, body { + margin: 0; + padding: 0; + color: #404040; + + line-height: 1.5; + font-weight: 400; + font-size: 100%; + + font-synthesis: none; + text-rendering: optimizeLegibility; + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; +} + +main { + max-width: 60em; + margin: 0 auto; + padding: 1em; +} + +#preferences { + .preference-group + .preference-group { + margin-top: 1em; + } + + .preference-group { + h3 { + margin: 0; + padding: 0.25em 0; + } + + ul { + margin: 0; + padding: 0; + } + + li { + list-style: none; + + & + li { + border-top: 1px solid #d0d0d0; + } + + &:hover { + background-color: #f8f8f8; + } + + &:active { + background-color: #f0f0f0; + } + + label { + display: flex; + padding: 0.5em 0.5em; + gap: 0.5em; + + > *:first-child { + flex-grow: 1; + } + + p { + &.description { + font-size: 0.8em; + color: #606060; + } + } + } + } + } +} diff --git a/src/options/index.ts b/src/options/index.ts new file mode 100644 index 0000000..edc3803 --- /dev/null +++ b/src/options/index.ts @@ -0,0 +1,13 @@ +import * as preact from "preact"; + +import { EditPreferences } from "./components/EditPreferences.tsx"; + +globalThis.addEventListener("DOMContentLoaded", () => { + const root = document.getElementById("preferences"); + if (!root) throw Error("element #preferences is not found"); + + preact.render( + preact.createElement(EditPreferences, null), + root, + ); +}); diff --git a/src/options/options.html b/src/options/options.html deleted file mode 100644 index e5987cb..0000000 --- a/src/options/options.html +++ /dev/null @@ -1,22 +0,0 @@ - - - - - - - 設定 - NITech Moodle Extension (40a) - - - - -
    -
    -

    設定

    -
    - NITech Moodle Extension (40a) -
    -
    -
    -
    - - \ No newline at end of file diff --git a/src/options/options.scss b/src/options/options.scss deleted file mode 100644 index 472c51b..0000000 --- a/src/options/options.scss +++ /dev/null @@ -1,113 +0,0 @@ -$c-bg: #ffffff; -$c-bg-hover: #f0f0f0; -$c-bg-active: #e0e0e0; -$c-separator: #c0c0c0; -$c-toggle-bg-off: #bdc1c6; -$c-toggle-bg-on: #8bb8f2; -$c-toggle-knob-off: #ffffff; -$c-toggle-knob-on: #1a73e8; - -body { - margin: 0; - padding: 0; - font-size: 14px; - background-color: $c-bg; - - > main { - max-width: 60em; - min-height: 100vh; - margin: 0 auto; - padding: 1em 1em; - box-shadow: 0 0 5px rgba(0, 0, 0, 0.5); - } -} - -header { - > * { - display: inline-block; - margin-right: 0.5em; - } -} - -main { - ul { - list-style: none; - margin: 0; - padding: 0; - - li { - display: flex; - gap: 1em; - - &:hover { - background-color: $c-bg-hover; - } - - &:active { - background-color: $c-bg-active; - } - - +li { - border-top: 1px solid $c-separator; - } - - label { - cursor: pointer; - } - - div.label { - flex-grow: 1; - - label { - padding: 0.75em 1em; - display: block; - height: 100%; - } - } - - div.control { - display: flex; - align-items: center; - - input { - display: none; - - $sw-width: 2em; - $sw-height: 0.8em; - $sw-knob-size: 1.2em; - - + label { - margin-right: 1em; - display: block; - width: $sw-width; - height: $sw-height; - border-radius: 0.5em; - background-color: $c-toggle-bg-off; - transition: all 300ms 0s ease; - - &::after { - display: block; - position: absolute; - width: $sw-knob-size; - height: $sw-knob-size; - content: ""; - border-radius: 50%; - box-shadow: 0 0 5px rgba(0, 0, 0, 0.5); - transform: translate(-$sw-knob-size / 2, ($sw-height - $sw-knob-size) / 2); - background-color: $c-toggle-knob-off; - transition: all 300ms 0s ease; - } - } - &:checked + label { - background-color: $c-toggle-bg-on; - - &::after { - transform: translate($sw-width - $sw-knob-size / 2, ($sw-height - $sw-knob-size) / 2); - background-color: $c-toggle-knob-on; - } - } - } - } - } - } -} diff --git a/src/options/options.ts b/src/options/options.ts deleted file mode 100644 index 3392416..0000000 --- a/src/options/options.ts +++ /dev/null @@ -1,9 +0,0 @@ -import { renderApp } from './app/App.tsx'; - -globalThis.addEventListener('load', () => { - const elAppRoot = document.getElementById('app_root'); - if (!elAppRoot) { - throw Error(`element #app_root is not found`); - } - renderApp(elAppRoot); -});